Skip to content

Commit 1f9babd

Browse files
committed
perf: take Langfuse trace linking off the eval item critical path
Finding a gen-ai trace means polling until Langfuse has ingested it -- anywhere from one round trip to the full retry budget. None of that work produces a verdict; the pass/fail is already decided by the time it starts. Charging the item's latency for it both slowed the run and made the reported agent latency wrong. Each evaluate_agentic_* now hands its Langfuse block to a linker instead of running it inline. The CLI injects a BackgroundTraceLinker that collects the queue and drains it after the agent phase, before any report is rendered, so scores are always final before the command exits. Direct library callers keep the synchronous default and are behaviour-compatible. Alongside that: - Per-phase latency (agent / judge / simulated user / Langfuse) recorded per run and aggregated across K runs, exposed as an additive latency_breakdown_s. - --concurrency reaches the agentic path, partitioned by an explicit PARALLEL_SAFE_TEST_KINDS allowlist; anything absent runs serially. - --timers gates the per-turn [timer] output (off by default). --judge-model selects the LLM-as-judge model (default gpt-4o). - An unreadable judge response raises JudgeResponseError instead of scoring 0. - An item's user_context (a WIDGET/VIEW attachment the question refers to) is relayed to the chat request as userContext, and survives the Langfuse dataset round trip. FIXES FOUND WHILE REVIEWING THE ABOVE Each was reproduced broken first, then re-verified by re-injecting the defect and confirming the new test fails. Judge contract: - A judge fault on one run of K discarded every run already graded, dropped their Langfuse scores, and reported an item whose pass@K was ALREADY satisfied as a failure -- the same "a parse bug reads as a pass-rate drop" that JudgeResponseError exists to prevent, one layer up. A fault is now confined to its own run; pass@K holds on the graded runs, pass^K requires every run graded, and an item with no graded run at all errors. dashboard_summary needed it most: it judges once PER CRITERION, so one bad body lost all of them. - choices == [] (content filters, gateway error envelopes) escaped as a bare IndexError, past the typed error, with none of the body or metadata. - {"score": 2} was reported as a confident FAIL -- int(score) == 1 was the last place an invented 0 survived. Only 0 and 1 are verdicts now. - {"score": "1"} regressed; JSON mode quotes numbers routinely, so it is coerced again. - The temperature fallback matched "temperature" in str(exc), and the openai SDK stringifies the whole response body into the message. Gateways echo the request inside it, so any 400 -- context_length_exceeded included -- was misread, silently dropping temperature=0 from every later verdict. Now read off the provider's structured error. - openai>=1.45 is required: 1.40-1.44 lack max_completion_tokens (verified against the wheel) and would TypeError on every judge call. Interrupts: - A bare `with ThreadPoolExecutor(...)` exits via shutdown(wait=True) with cancel_futures left False, so Ctrl-C ran every QUEUED item to completion first. Reproduced: SIGINT at 0.30s, interrupt observed at 9.00s, all 6 items run. Both pools now cancel; drain() had the same defect and sits outside the abandon() guard, so it cancels on the pool itself. Langfuse: - The sessionId filter never reached the server: _TraceAPI.list had no such parameter and it is the only client make_langfuse_client returns. So every attempt downloaded a full limit=100 page of the whole window and filtered it locally -- and the endpoint returns newest-first, so once a window held more than 100 traces the item's OWN trace was evicted and it spent its entire budget on a page that could never contain it. - The 120s budget is affordable only because the batch blocks nobody. Direct library callers poll inline, on their own critical path (the tavern e2e suite under a step timeout), where 120s tripled a miss from ~35s to ~110s. The budget now follows the mode: 35s inline (identical to the old ladder, to the second), 120s batched. - A local --dataset cannot be attached to a Langfuse run, but linking still happens off exported credentials, so every conversation earned a raw 404 from dataset-run-items at the very end of the run. Now warned before the run starts and reported once per run with its cause. Reporting: - pass@K answers "did any run pass", so a 5/5 item and a 1/5 item were identical in every output -- quality_score reads the best run alone. Every agentic kind already computed the count and dropped it. Now surfaced as runs_passed / pass_power_k per item and passed_all_runs per run, with "4/5 runs passed" in the console. - agentic_conversation takes no k and drives its fixture once, but runs = k was set unconditionally, so --runs 5 claimed five runs and divided one conversation's latency by five. - An errored item lost the phase timings it had managed to take. Tests that certified nothing: - The window-pinning guard counted call arity, so inlining _dt.now() inside _link_traces -- the exact drift its docstring describes -- passed. - abandon() had no effective coverage: the only test reaching it never calls drain(), so replacing its body with `pass` left 69 tests green. - test_resolve_connection_uses_profile read an exported GOODDATA_TOKEN instead of the profile it stubs, and one test popped TAVERN_E2E_SKIP_TRACE_LINK with no guard, unsetting it for every module collected afterwards. Docs: - Audited every factual claim in the README. The retry budget is no longer one number; runs_passed / pass_power_k / passed_all_runs were undocumented; the local-dataset linking behaviour was unexplained; and the experiment run name was wrong (and had been on master): the code builds {dataset_name}_{timestamp}_{model} with _effort-{level} and _run{N} suffixes. - --concurrency's --help omitted agentic_kda_skill from the forced-serial list. SIMPLIFICATION PASS types-check was failing on this branch, and the local check disagreed because CI runs `uv run ty` (the locked version), not `uvx ty` (latest). Two real diagnostics in langfuse_source._infer_test_kind: isinstance(metadata, dict) does not narrow the following subscript past object. Binding the value before the isinstance check fixes it and drops a double lookup. A first attempt also proved that py314 defers annotation evaluation (PEP 649), so a missing TypeVar passed the suite here and would have NameError'd at import on py310-313. The eight evaluate_agentic_* functions each carried a byte-identical ~36-line Langfuse prologue, so every change above had to be made eight times -- and two AST-walking tests existed only to stop the eight copies from drifting. That block is now one helper: RunIdentity / RunTraceContext / submit_trace_scoring. `def _link_traces` 8 -> 1, `build_run_context(` 9 -> 2, `suffix_needed` 12 -> 0. The structural tests were retargeted at the invariant's new home rather than deleted, and conversation.py resolves its dataset name eagerly so a queued task no longer retains the whole ConversationFixture until drain. Also: emit_line replaces six hand-rolled stdout.write+flush pairs; a shared _first_of collapses three input-then-metadata lookup ladders; runs_total replaces an expression duplicated between ItemReport and the console renderer; PhaseTimings.as_dict() is wired into the JSON report, which was dead code, as was the langfuse_s field it now populates; the unused context-manager protocol and the dead total_s are gone; _response_metadata's five copy-pasted try/except blocks collapse to one guarded helper; summary._grade no longer writes detail[key] for its caller to overwrite. Comment bloat trimmed where one idea was stated four to eight times over. The three slowest tests were time.sleep(0.3) negative assertions -- slow, and timing-flaky on a loaded box. They now join the pool for real, which is what would actually let a queued task start. Behaviour preservation was checked rather than assumed: all 33 score-writing calls were compared against the pre-refactor tree (identical modulo the mechanical renames), as were the conversation-id expressions, the run-name suffix policy and the dataset names. The three retargeted structural tests and the three interrupt tests were each mutation-tested by re-breaking the code they guard and confirming the right test fails. SECOND SIMPLIFICATION PASS The first pass traded duplicated logic for duplicated argument plumbing of about the same size, so it removed only ~74 net lines. This pass went after the plumbing: - The eight *AssertionError classes each redeclared the same seven-attribute payload (and two of the eight declared `timings` while the runner getattr'd it from all eight). They now share an AgenticAssertionError base in core/models.py. - The eight-line preamble (datetime aliases, client fallback, window_start) that opened every evaluate_agentic_* is one call to open_trace_window(). - RunTraceContext gained observe()/score()/quality(), so the deferred _langfuse import and the five-argument observe() call disappear from all eight kinds. - Seven of eight kinds built their `detail` dict twice -- once on the failure path, once on success -- with nothing keeping the two literals in step. Hoisted to one local per kind, proven equivalent by AST comparison on both paths. Across the eight agentic kinds: `__tracebackhide__` 8 -> 0, deferred `_langfuse import (` 8 -> 0, `from datetime import` 17 -> 2, `try_make_langfuse_client` 18 -> 4, `suffix_needed` 12 -> 0, `def _link_traces` 8 -> 1. Tests: the six recurring patch-block shapes are now file-local context managers, 41 copies of an inline MagicMock scaffold are gone, and the three slowest tests (0.3s sleeps used as negative assertions, flaky on a loaded box) now join the pool for real. Suite wall time 4.1s -> 2.6s. REVIEW FINDINGS FIXED Eight CodeRabbit findings, each reproduced before it was touched, each now covered by a regression test that fails when the fix is reverted: - An item that errored after earlier runs passed reached runs_passed == runs_total and was reported as pass^K -- unanimity claimed for an item whose last run had no verdict at all. - The JSON report counted an errored item as both `failed` and `errored`, because `failed` was computed by subtraction. - A blank `test_kind` ("") beat both structural inference and the CLI --kind default, so the item was skipped as an unsupported kind. - Avg/run divided by the requested K while the Runs column showed runs_effective, so agentic_conversation reported a per-run latency for four runs that never happened. Fixed on ItemReport.avg_latency_s so the JSON report gets it too. - A CLI test let _apply_judge_model write GD_EVAL_JUDGE_MODEL straight into os.environ without monkeypatch recording it, leaking the judge model into every later test. - Ctrl-C during the batched drain could hang for the rest of the batch budget. cancel_futures only drops what has not started; a poll already running sits in find_traces_per_conversation's backoff, and the interpreter joins executor workers at exit. Verified in the real CLI against staging, with Langfuse pointed at a closed port: Ctrl-C during the drain exited after 105.4s before this change and 1.1s after. The linker now publishes a cancellation Event for the duration of one drain -- fresh per drain, so one run's interrupt cannot stop the next --model pass -- and the backoff is served in slices that check it. A set event is left in place when the drain unwinds, because shutdown(wait=False) returns before the workers notice and clearing it would send a late worker back to an uninterruptible sleep. - Fixing the blank test_kind above introduced a second bug: _first_of returned the first *string* it found, so a blank expectedOutput.test_kind shadowed a valid metadata.test_kind. The sources are now checked one at a time. - The README claimed both that a direct library caller gets langfuse_s = 0.0 and that langfuse_s is populated whenever credentials are exported. Linking does happen either way; only the CLI path measures its duration. LIVE VALIDATION Measured on staging (18 agentic_general_question items x 2 runs, gpt-5.6-luna): --concurrency 1, master 462s vs 233s here; --concurrency 2, master 774s vs 114s. --concurrency has no effect on master's agentic path, so both master runs did the same serial work and differed only by ingestion lag -- and one of them orphaned two traces. Reported latency per run falls 12.7s -> 5.2s, which is not a speedup but the removal of Langfuse polling from a number that is supposed to measure the agent. Pass rates unchanged: 18/18 on both trees, plus 33/33 on the 33-item guardrail dataset. pass^K differed there (32 vs 29); re-running the three differing items three times per tree put master at 2/3, 1/3, 2/3 and this branch at 2/3, 2/3, 2/3, so that gap is agent non-determinism on borderline refusal prompts, not a regression. REVIEW FEEDBACK Rationale for the change itself has been taken back out of the source comments and left here, where it belongs: the openai>=1.45 justification above stood duplicated above the pin in pyproject.toml, the pass@K/pass^K asymmetry ran to a twelve-line essay arguing its own history, and the same "keeping two literals in step was a standing drift hazard" paragraph had been pasted into eight modules. Comments that state a live constraint a future editor must not break -- the two ThreadPoolExecutor shutdown notes, the PARALLEL_SAFE_TEST_KINDS invariant, the `is not None` session filter -- are kept. Comment lines on this diff's source additions: 337 -> 301. A ticket key that had been left in a test docstring is gone with them; the GDAI-2179 literals that remain are Langfuse dataset names, which is what a real one is called. Verification: 685 pass, 0 failures -- also per test file standalone, in reverse module order, and with GOODDATA_TOKEN, TAVERN_E2E_SKIP_TRACE_LINK, GD_EVAL_TIMERS and GD_EVAL_JUDGE_MODEL exported. ruff check, ruff format --check and `uv run ty` clean. Behaviour preservation was proven, not assumed: all 41 score/observe calls and every detail dict compared against the pre-refactor tree, 1126 assertion statements compared across 35 test files, and the six safety-critical tests mutation-tested by re-breaking the code they guard. risk: medium
1 parent 12861e2 commit 1f9babd

44 files changed

Lines changed: 6112 additions & 1262 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,10 @@ packages/gooddata-sdk/tests/export/exports/default/
4242
AGENTS.md
4343
.aiassistant/rules/aida.md
4444
.junie/guidelines.md
45+
46+
# gooddata-eval local run artifacts. Root-anchored on purpose: a bare `datasets/` would
47+
# also shadow packages/gooddata-pandas/tests/.../ldm/datasets/, which is tracked.
48+
/packages/gooddata-eval/EVAL_results
49+
/packages/gooddata-eval/datasets/
50+
# MCP tool logs, written to a relative path by whatever is started from the repo root
51+
/logs/

packages/gooddata-eval/README.md

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ gd-eval run \
114114
|---|---|
115115
| `--dataset PATH` | Flat folder of JSON files — one question per file. |
116116
| `--langfuse-dataset NAME` | Pull items by name from a Langfuse dataset. Requires `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`, `LANGFUSE_HOST`. |
117+
| `--kind TEST_KIND` | Fallback `test_kind` for dataset items that do not embed one. Defaults to `visualization`; use e.g. `agentic_metric_skill` for multi-turn agentic evaluation. Items that declare their own `test_kind` ignore this. |
117118

118119
#### Model selection
119120

@@ -126,21 +127,62 @@ gd-eval run \
126127
| Flag | Default | Description |
127128
|---|---|---|
128129
| `--runs K` | `2` | Independent runs per item (pass@K). An item passes if any run passes. |
129-
| `--concurrency K` | `1` | Number of items evaluated concurrently. `1` = sequential (default). Increase to load-test the agent under simultaneous requests. Progress output interleaves when K > 1. |
130+
| `--concurrency K` | `1` | Number of items evaluated concurrently. `1` = sequential (default). Increase to load-test the agent under simultaneous requests — see *Concurrency and workspace safety* below. |
131+
| `--judge-model MODEL` | `gpt-4o` | Model used for LLM-as-judge scoring — `agentic_general_question`, `agentic_guardrail`, `general_question`, `guardrail` and `dashboard_summary`. Also settable via `GD_EVAL_JUDGE_MODEL`. Two things to weigh before changing it: the gpt-5 family rejects `temperature=0`, so verdicts stop being reproducible (the run warns when this happens); and choosing the same model the agent runs means the judge grades its own family's output. |
130132
| `--reasoning-effort LEVEL` | server default | `LOW`, `MEDIUM` or `HIGH`, sent as `options.reasoningEffort` on every chat message. Requires the `enableGenAiReasoningEffort` feature flag on the target organization — without it the server ignores the value. Applies to chat items only; `dashboard_summary` items go through the summary endpoint, which has no such option. |
131133

134+
**Concurrency and workspace safety.** Agentic kinds that create workspace objects
135+
(`agentic_metric_skill`, `agentic_alert_skill`, `agentic_conversation`, `agentic_kda_skill`) always run one at a
136+
time whatever `--concurrency` says — a metric or alert created and dropped mid-run would otherwise be visible to
137+
another item reading the same catalog. **That protection is for the agentic kinds only:** the single-turn
138+
`metric_skill` and `alert_skill` kinds are still fanned out and the agent performs the same server-side writes on
139+
that path, so avoid raising `--concurrency` on a dataset of those against a shared workspace. Progress output
140+
interleaves when K > 1, and per-item latencies rise, so they stop being clean single-request measurements.
141+
132142
#### Output
133143

134144
| Flag | Description |
135145
|---|---|
136146
| `--json PATH` | Write a JSON report to this path. Always uses the nested `{models, runs, comparison}` shape even for a single model. |
137147
| `--quiet` | Suppress per-item progress. Per-model result tables and the comparison summary are still printed. |
148+
| `--preserve-failed` | Keep failed conversations on the server instead of deleting them, so they can be inspected afterwards. Applies to the single-turn chat path; agentic kinds manage their own conversation lifecycle. |
149+
| `--timers` | Print per-turn `[timer]` diagnostics — GoodData response, judge, and simulated-user seconds as they happen. Off by default: an 18-item `--runs 2` run emits ~72 lines and buries the progress output. The same measurements are always in the JSON report's `latency_breakdown_s`, so this only adds a live view. Also settable via `GD_EVAL_TIMERS=1`. |
138150

139151
#### Langfuse sink
140152

141153
| Flag | Description |
142154
|---|---|
143-
| `--langfuse` | Log scores and traces to Langfuse after each item. Requires `--langfuse-dataset`. Creates one named experiment run per model (`gd-eval-{timestamp}-{model}`, suffixed `-effort-{level}` when `--reasoning-effort` is set so runs differing only by effort stay separate). Requires `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`, `LANGFUSE_HOST`. |
155+
| `--langfuse` | Log scores and traces to Langfuse after each item. Requires `--langfuse-dataset`. Names each experiment run `{dataset_name}_{timestamp}_{model}`, suffixed `_effort-{level}` when `--reasoning-effort` is set (so runs differing only by effort stay separate) and `_run{N}` per run when `--runs` > 1 — e.g. `general_question_2026-09-02-11-13_gpt-5.2_run0`. Requires `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`, `LANGFUSE_HOST`. |
156+
157+
Set `TAVERN_E2E_SKIP_TRACE_LINK=1` to skip trace lookup entirely (scores are then orphaned; the run says so).
158+
159+
**A local `--dataset` cannot be attached to a Langfuse run.** `--langfuse` is refused alongside `--dataset`
160+
because a local folder's item ids are not Langfuse dataset item ids. But trace linking does not depend on that
161+
flag — each `evaluate_agentic_*` builds its own client whenever `LANGFUSE_PUBLIC_KEY`/`LANGFUSE_SECRET_KEY` are
162+
exported — so a local run still finds its traces and writes its scores onto them, and only the per-run grouping
163+
fails, with one `404 from dataset-run-items` reported per run. The run warns about this before it starts. Use
164+
`--langfuse-dataset` when you want runs that are comparable across models, or `TAVERN_E2E_SKIP_TRACE_LINK=1` to
165+
skip linking altogether.
166+
167+
**When trace linking happens.** Finding a gen-ai trace means polling until Langfuse has ingested it, which is
168+
lag measured in seconds to minutes. That work produces no verdict — the pass/fail is already decided — so it
169+
does not run inline per item. Every item's Langfuse block is queued and the whole batch runs *after* the agent
170+
phase, draining before any report is written. Two consequences worth knowing:
171+
172+
- **No item's `latency_s` includes trace linking.** Its cost is reported separately as
173+
`latency_breakdown_s.langfuse_s`, and the run prints
174+
`[langfuse] trace linking finished in Xs for N item(s); slowest Ys`. If `slowest` approaches the **120s**
175+
batched retry budget, links are timing out and scores are being orphaned — look for
176+
`[langfuse] WARNING: no trace found for conversation ...`.
177+
- **The budget depends on who is waiting.** 120s is affordable only because the batch blocks nobody. A direct
178+
library caller (`evaluate_agentic_*` without a `submit_trace_link`) polls inline, on its own critical path, and
179+
gets **35s** instead — the same cost as before batching existed, so no inline caller pays for a budget raised
180+
on the CLI's behalf. Either way a trace that is already ingested costs nothing: the loop looks before it sleeps.
181+
Scores are always final before the command exits — the run blocks on the batch. Interrupting with Ctrl-C drops
182+
whatever is still queued rather than making you wait it out: both the queued trace links and, under
183+
`--concurrency`, the items that have not started. The handful of items already in flight still have to finish —
184+
worker threads are joined at exit and an in-progress agent call cannot be cancelled — so expect to wait up to one
185+
`--concurrency`-wide wave, not the rest of the dataset.
144186

145187
### JSON report shape
146188

@@ -162,6 +204,66 @@ The JSON report always uses the nested multi-model shape:
162204

163205
Winner is selected by **pass rate → quality score → latency** (lower latency wins all-equal ties).
164206

207+
Each item reports **how many of its runs passed**, not only whether one did:
208+
209+
```json
210+
"runs": 5, "runs_passed": 4, "pass_at_k": true, "pass_power_k": false
211+
```
212+
213+
`pass_at_k` is "did any run pass" and is what `passed` counts. `runs_passed` is the fact that separates a
214+
reliable item from a coin-flip — without it a 5/5 item and a 1/5 item are identical in every field, because
215+
`quality_score` is derived from the best run alone. `pass_power_k` is true only when every run passed, and the
216+
run summary carries `passed_all_runs` beside `passed`; a large gap between the two means the model is
217+
inconsistent rather than wrong. The console shows `4/5 runs passed` in `Notes` for a non-unanimous pass and
218+
stays quiet for a unanimous one, and its summary line reads `3/4 passed, 1 on every run`.
219+
220+
`runs` is what the item actually ran, which is not always the requested `--runs`: `agentic_conversation` takes
221+
no K and drives its fixture exactly once.
222+
223+
Each item additionally carries a per-phase breakdown:
224+
225+
```json
226+
"latency_breakdown_s": {
227+
"agent_s": 4.02, // GoodData's own response time — the system under test
228+
"judge_s": 1.31, // LLM-as-judge scoring, post-hoc
229+
"simulated_user_s": 0.0, // our simulated user composing the next turn (multi-turn kinds)
230+
"langfuse_s": 5.70 // trace lookup + score writing, off the critical path
231+
}
232+
```
233+
234+
An item may also carry `unscored_runs` / `judge_errors` in its `detail` (and a
235+
`dashboard_summary` item `ungraded_criteria`). These appear only when the LLM judge returned something
236+
unreadable for part of an item. Such a run — or, for `dashboard_summary`, such a criterion — is excluded from
237+
pass@K and from the quality score rather than counted as a failure: scoring it 0 would be indistinguishable from
238+
the judge genuinely failing the answer, which is the confusion `JudgeResponseError` exists to end. `pass@K` still
239+
holds on the runs that *were* graded, so an item can pass with `unscored_runs` set; `pass^K` cannot, because a
240+
run nobody graded leaves "all K passed" unverified. When *no* run or criterion could be graded the item errors
241+
instead of reporting failures. Their presence means the pass@K was computed over fewer runs than `--runs` asked
242+
for, so treat the result as weaker evidence and check the judge (`GD_EVAL_JUDGE_DIAGNOSTICS=1`, or raise
243+
`JUDGE_MAX_COMPLETION_TOKENS` if the cause is `finish_reason=length`).
244+
245+
`agent_s` + `judge_s` + `simulated_user_s` are the instrumented parts of the item's `latency_s`; they do not add
246+
up to it exactly, because `latency_s` is wall-clock around the whole item and also covers the conversation
247+
create/delete round trips, SDK construction and any cleanup. `langfuse_s` sits **beside** `latency_s`, never
248+
inside it, because trace linking runs outside every item's critical path (see above) — summing all four would
249+
re-inflate exactly what that design removes.
250+
251+
A phase that a kind does not have reports `0.0` rather than an invented number, so read the zeroes as "not
252+
applicable here", not "instant". Today:
253+
254+
| Field | Populated by |
255+
|---|---|
256+
| `agent_s` | `agentic_general_question`, `agentic_metric_skill` |
257+
| `judge_s` | `agentic_general_question` only — `agentic_metric_skill` compares MAQL by string, it has no LLM judge |
258+
| `simulated_user_s` | `agentic_metric_skill` only — `agentic_general_question` is single-turn, it has no simulated user |
259+
| `langfuse_s` | every agentic kind, but only on the `gd-eval` path and only when Langfuse credentials are present |
260+
261+
The other six agentic kinds report `0.0` for the first three. Trace linking itself happens whenever
262+
`LANGFUSE_PUBLIC_KEY`/`LANGFUSE_SECRET_KEY` are exported, with or without `--langfuse`, because each
263+
`evaluate_agentic_*` falls back to `try_make_langfuse_client()`. But its *duration* is measured by the CLI
264+
runner rather than by `evaluate_agentic_*`, so a direct library caller sees `langfuse_s: 0.0` even though its
265+
linking ran. Pass `TAVERN_E2E_SKIP_TRACE_LINK=1` to opt out of linking altogether.
266+
165267
---
166268

167269
## `gd-eval models`

packages/gooddata-eval/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ classifiers = [
3030
]
3131

3232
[project.optional-dependencies]
33-
llm-judge = ["openai>=1.40,<2.0"]
33+
llm-judge = ["openai>=1.45,<2.0"]
3434

3535
[project.scripts]
3636
gd-eval = "gooddata_eval.cli.main:main"

0 commit comments

Comments
 (0)