发布

  • [OPIK-7511] [SDK] [PYBE] fix: make no-improvement runs honest — perfect_score, mini-batch sizing, finish_reason (#7650)

    frostbyte_neo 发布于 2026-07-31 12:11:06 +00:00

    • [OPIK-7511] [SDK] [PYBE] fix: make no-improvement runs honest — perfect_score, mini-batch sizing, finish_reason
    • perfect_score default 0.95 -> 1.0: it gates both GEPA's iteration skip and
      (since #7591) the run-level ScoreThresholdStopper, so anything below 1.0
      ends runs with a strong-but-imperfect baseline at zero candidates. Matches
      the gepa package's own default.
    • Studio's GEPA reflection_minibatch_size is no longer pinned to 5: it scales
      as min(max_trials, dataset_size, max(5, ceil(dataset_size / 5))) so coarse
      0/1 metrics get a usable gradient; the env var stays as operator override.
    • finish_reason is now populated on every early-stop path: budget-exhausted
      GEPA runs report "max_trials" instead of falling through to "completed",
      the baseline-perfect skip writes finish_reason="perfect_score" into details
      (BaseOptimizer + ParameterOptimizer), and the python-backend forwards
      finish_reason to the Java backend as metadata.finish_reason alongside
      scoring_health so OPIK-7458 can render the real stop cause.

    Co-Authored-By: Claude Fable 5 noreply@anthropic.com

    • [OPIK-7511] [SDK] style: ruff-format fix in gepa stop-condition tests

    Co-Authored-By: Claude Fable 5 noreply@anthropic.com

    • [OPIK-7511] [SDK] [PYBE] fix: carry scoring_health on the baseline-perfect path + real e2e for finish_reason

    Found by running the new e2e against a live stack: build_early_stop_details
    never included scoring_health, so a perfect-baseline run completed with no
    scoring_health in metadata — exactly the case the UI's 'No usable scores'
    heuristic misfires on (OPIK_7458). Align it with build_final_result, which
    always carries the key.

    The new e2e (tests/e2e/test_studio_finish_reason.py) drives the real
    process_optimizer_job -> optimizer_runner subprocess -> gateway pipeline and
    asserts finish_reason is returned by the subprocess AND persisted as
    metadata.finish_reason, with scoring_health riding the same completion update.

    Co-Authored-By: Claude Fable 5 noreply@anthropic.com

    • [OPIK-7511] [PYBE] test: gate finish_reason e2e on the pinned SDK containing the fix

    The python-backend CI installs the released opik_optimizer pin, which
    predates the SDK half of OPIK-7511 — the perfect-baseline path completes
    without finish_reason there, failing the e2e on the pin rather than on the
    backend code under test. Feature-detect via DEFAULT_PERFECT_SCORE (ships in
    the same SDK changeset) and skip until the pin catches up; the test then
    activates automatically.

    Co-Authored-By: Claude Fable 5 noreply@anthropic.com

    • [OPIK-7511] [PYBE] fix: address code-review findings
    • resolve_reflection_minibatch_size: a malformed env override warns and
      falls back to the dataset-scaled policy instead of failing every run.
    • run_optimization takes the item count from load_and_validate_dataset
      instead of re-fetching the whole dataset a second time.
    • extract_finish_reason logs unknown reasons instead of dropping them
      silently, so a new SDK FinishReason surfaces in logs.
    • extract_completion_metadata combines both extractors; the runner's
      forwarding block is now unit-testable (and unit-tested) instead of only
      covered by the pin-gated e2e.
    • The e2e imports KNOWN_FINISH_REASONS instead of a third copy of the
      allowlist.

    Verified: 229 backend unit tests pass; the live e2e (real gateway LLM
    calls) passes against this exact tree.

    Co-Authored-By: Claude Fable 5 noreply@anthropic.com

    • [OPIK-7511] [SDK] [PYBE] fix: replace the max_trials minibatch cap with a budget-based one

    The max_trials cap silenced exactly the scaling this PR introduces: with
    the default max_trials=10 the reflection mini-batch was pinned at 10 for
    any dataset above ~50 items. The cap's premise was false — the gepa
    engine never gates a reflection iteration on the remaining budget (the
    only budget read, metric_calls_remaining, feeds a callback event), so a
    large batch runs fine, it just costs ~2*batch metric calls per iteration.

    • resolve_reflection_minibatch_size: drop the max_trials cap; keep the
      dataset_size cap; add a budget cap that guarantees at least
      GEPA_MIN_REFLECTION_ITERATIONS (5) reflection iterations within
      max_metric_calls = max_trials * n_samples.
    • SDK: rewrite the 'GEPA reflection will not run' warning as
      _warn_if_reflection_minibatch_exhausts_budget — it now reports the
      real iteration estimate (max_metric_calls // (2*b)).
    • Validate OPTIMIZER_GEPA_REFLECTION_BATCH_SIZE at service startup
      (integer >= 1, error names the var, value truncated) instead of
      lazily warning-and-falling-back per run.
    • Bound dataset size counting to DATASET_SAMPLES (nb_samples fetch)
      instead of materializing the full dataset.
    • Tests: new policy table incl. 1-item/tiny datasets and budget-cap
      cases; warning tests assert the logged iteration count; bound the
      NoImprovementStopper loop; rename new tests to the WHAT__CASE__RESULT
      convention.

    Co-Authored-By: Claude Fable 5 noreply@anthropic.com

    • [OPIK-7511] [SDK] [PYBE] fix: scope perfect_score=1.0 to Studio runs, not the SDK default

    The ticket scopes the perfect_score fix to Studio runs; changing the
    SDK-wide DEFAULT_PERFECT_SCORE also silently changed behaviour for every
    SDK user of every optimizer, and would not reach Studio until the
    python-backend's opik_optimizer pin is bumped. Instead: revert the SDK
    constant to 0.95 and have OptimizerFactory inject perfect_score=1.0 as a
    constructor default (explicit run params still win) — effective on the
    currently pinned opik-optimizer==3.1.0, no pin bump needed. The e2e's
    feature probe for the SDK finish_reason half now keys on
    MIN_EXPECTED_REFLECTION_ITERATIONS (same changeset) instead of the
    reverted constant.

    Co-Authored-By: Claude Fable 5 noreply@anthropic.com

    • [OPIK-7511] [PYBE] test: make the studio e2e runnable against a cloud backend

    Three gaps kept tests/e2e from running anywhere but the unauthenticated
    CI stack: the provider fixture was hardcoded to Anthropic (contradicting
    the OPTSTUDIO_E2E_MODEL override the finish_reason e2e documents), and
    the job message never carried opik_api_key, so on a cloud backend the
    subprocess's status updates and gateway LLM calls authenticated with a
    placeholder and got 401. The fixture is now provider-aware
    (workspace_provider_key: derives the required provider from
    OPTSTUDIO_E2E_MODEL, no-ops when it is already configured in the
    workspace) and the job message passes the workspace API key when
    OPIK_API_KEY is set — local CI stacks keep today's behaviour.

    Verified live: test_finish_reason_is_returned_and_persisted passes
    against dev.comet.com (gpt-5-nano via the workspace OpenAI key) with the
    factory-injected perfect_score=1.0 mechanism from the previous commit.

    Co-Authored-By: Claude Fable 5 noreply@anthropic.com

    • [OPIK-7511] [SDK] [PYBE] fix: stop deciding runs on a single noisy eval

    Live A/B runs exposed that raising perfect_score to 1.0 removed the
    deterministic abort on a strong baseline but left a stochastic one a step
    later. Measured on dev: repeating the SAME eval of the SAME prompt over
    the SAME 20-item dataset scored 0.95/1.00 across 6 repeats on gpt-5-nano
    and 0.90/0.95 on gpt-4o-mini (flipping even on unambiguous items), so
    whether a run searched at all was luck. When the seed program's own full
    eval happened to reach 1.0, gepa's ScoreThresholdStopper ended the run at
    iteration zero and it was reported as finish_reason='perfect_score' while
    the UI showed 'no improvement' at 0.95 — the exact dishonesty this ticket
    is about.

    Two changes:

    • CandidateScoreThresholdStopper ignores program_full_scores_val_set[0]
      (the seed's own eval), so only a CANDIDATE reaching the threshold can
      stop the run. 'Baseline is already good enough' stays owned by
      should_skip_optimization on Opik's baseline eval, which runs earlier, so
      no stop path is lost. _resolve_gepa_finish_reason skips index 0 for the
      same reason — otherwise the label disagrees with the stopper.

    • The task model (the one whose completions are scored) now runs at a
      pinned temperature; pinning made the same eval reproduce 6/6. Not
      applied to the reflection model, which needs sampling diversity to
      propose varied candidates. Models that fix their temperature (the gpt-5
      family accepts only 1) ignore it via litellm drop_params instead of
      failing the run — which is why the stopper fix is needed regardless.

    Verified live on dev: the exact config that previously aborted after 3
    trials with finish_reason='perfect_score' now runs 12 trials and reports
    finish_reason='max_trials' honestly at 0.95.

    Co-Authored-By: Claude Fable 5 noreply@anthropic.com

    • [OPIK-7511] [PYBE] fix: validate the score/temperature config, fix e2e provider resolution

    Review findings on the previous two commits, all four real:

    • OPTIMIZER_PERFECT_SCORE and OPTIMIZER_TASK_TEMPERATURE went through a bare
      float(), which accepts 'nan' and 'inf'. Both feed score comparisons where
      those degrade silently (baseline >= nan is never true, >= inf never stops),
      and the sibling env var in the same file already failed fast — so they now
      share _read_float_env: finite, in range, error names the variable, raised at
      import so a bad deployment config fails at startup. Range is 0.0-1.0 because
      every metric the Studio exposes is normalised; a custom metric on
      another scale should set perfect_score per run instead.

    • OptimizerFactory.build used setdefault for perfect_score, so an explicit
      null from the studio config survived and crashed later inside
      baseline_score >= perfect_score. _resolve_perfect_score now treats None as
      absent, rejects non-finite/non-numeric (and bool, which would pass as 1.0)
      with InvalidOptimizerError naming the field, and preserves 0 — a legal value
      that disables threshold stopping.

    • The e2e provider fixture recognised only bare 'gpt...' ids, so the normal
      gateway-prefixed 'openai/' resolved to anthropic and the backend
      would reject the run for a missing key of the wrong provider. It now honours
      an explicit provider prefix.

    • That fixture also always read ANTHROPIC_API_KEY, so an OpenAI model skipped
      even with OPENAI_API_KEY set. The secret is selected by provider now, and the
      skip message names the variable it looked for.

    New tests: TestFloatEnvValidation (nan/inf/junk/bounds/import-time failure),
    TestPerfectScoreValidation (null falls back, 0 kept, invalid types raise), and
    test_studio_e2e_provider_resolution.py covering prefixed/bare/unknown models and
    the openai branch the reviewer asked for.

    Co-Authored-By: Claude Fable 5 noreply@anthropic.com

    • [OPIK-7511] [SDK] [PYBE] fix: address review — null handling, non-finite scores, error contracts

    Findings from the self-review and the remaining reviewer threads:

    • ensure_default_model_params used setdefault, so an explicit null temperature
      from the studio config was forwarded to litellm, and an explicit
      drop_params=False could fail the run on a fixed-temperature model. None is now
      treated as absent (temperature and max_tokens) and drop_params is forced: it is
      our guard, not a user knob. Same bug class already fixed for perfect_score one
      commit earlier.

    • The threshold stopper and the finish_reason label both read the raw score list,
      so an inf from a custom metric compared >= perfect_score as True and claimed a
      stop that never happened. Both now share _candidate_full_eval_scores, which
      excludes the seed's eval and drops None/non-numeric/bool/non-finite entries —
      the two can no longer disagree.

    • The stopper's broad except returned False silently, which disables threshold
      stopping invisibly (a gepa rename would look like a target never reached). It
      now logs a WARNING with exc_info. The catch stays broad on purpose: a stop
      heuristic must not take a paid run down, and malformed inputs are handled
      structurally rather than by exception.

    • load_and_validate_dataset fetched items outside the try, so transport failures
      escaped its documented DatasetNotFoundError contract. The bounded fetch moved
      inside; EmptyDatasetError stays outside so it cannot be relabelled.

    • The item count fed to the mini-batch counted rows the SDK drops: sampling keeps
      only items with an id (utils/sampling._extract_ids), so the count could exceed
      the real trainset. count_optimizable_items now counts by that rule and is
      shared by both call sites.

    • resolve_reflection_minibatch_size clamped budget_cap to 1 while the comment
      promised at least GEPA_MIN_REFLECTION_ITERATIONS iterations. The clamp is
      explicit and logs, and the comment says the guarantee holds only when the
      budget allows it.

    • Comments that overclaimed: the call-site comment implied pinning makes scoring
      deterministic (it does not for gpt-5, which is the default model), and the
      docstrings baked provider-specific measurements into durable docs. Both now
      state the contract — best-effort pin, noise-tolerant stop conditions — with the
      numbers left in the PR and ticket. The duplicated GEPA_MIN_REFLECTION_ITERATIONS
      records why it cannot be imported from the SDK yet (not in the pinned release)
      and where to collapse it.

    • The temperature-pin test asserted a literal while config reads the env at
      import, so a supported override would fail a correct implementation.

    New tests: task-model pin asserted on the prompt itself (and absent on the
    reflection model), non-finite/non-numeric scores for both stopper and label,
    null/false normalisation, and tests/unit/test_studio_dataset_loading.py for the
    count rule and both error contracts. SDK 994 passed, backend 438 passed.

    Co-Authored-By: Claude Fable 5 noreply@anthropic.com

    • [OPIK-7511] [PYBE] fix: document the temperature range, stop leaking the e2e conftest module

    Two smaller review findings on the previous commit:

    • OPTIMIZER_TASK_TEMPERATURE lost its units/range line when the comment was
      trimmed; operators need it next to the constant, not only inside the
      validation call.

    • The provider-resolution test registered the e2e conftest in sys.modules and
      never removed it, so a later import could pick up the stale copy and make
      collection order-dependent. The fixture now restores the previous entry in a
      finally block.

    Co-Authored-By: Claude Fable 5 noreply@anthropic.com

    • [OPIK-7511] [SDK] [PYBE] fix: reject id-less datasets, don't call an external stop "max_trials"

    Two review findings:

    • load_and_validate_dataset() accepted a dataset whose rows the SDK's
      sampling drops (no id), so the run reached optimization with an empty
      trainset and a mini-batch sized from 0. Reject it up front with
      EmptyDatasetError, which now carries a reason so the message says why
      instead of claiming the dataset is empty.

    • gepa auto-installs a FileStopper on <run_dir>/gepa.stop whenever run_dir
      is set, so a run can exit with neither of our stoppers fired and budget
      still on the clock. _resolve_gepa_finish_reason() labeled that
      "max_trials" — a budget burn that never happened. Compare
      GEPAResult.total_metric_calls against max_metric_calls and report
      "cancelled" instead; an unknown spend keeps the old fallback.

    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com

    • [OPIK-7511] [SDK] [PYBE] fix: bound the reflection mini-batch, gate the external-stop label

    Review follow-ups on #7650.

    Mini-batch had no absolute ceiling: gepa serializes the whole mini-batch
    into one reflection prompt (instruction_proposal.format_samples), so at
    max_trials=10 a 1000-item dataset resolved to 200 traces per proposal —
    neither the dataset_size cap nor the budget cap bounds the prompt.
    GEPA_REFLECTION_MINIBATCH_MAX = 25 keeps the resolution win (26 distinct
    mini-batch sums vs 6 at the old fixed 5) with a bounded prompt.

    "cancelled" from an unspent metric-call budget is now gated on run_dir
    being set. Only then does gepa wire the FileStopper that can produce it;
    without one, an unspent-looking budget could only mean the counter and
    MaxMetricCallsStopper's threshold drifted apart in a future gepa (the
    dependency has no upper bound), which would relabel every ordinary
    budget exit — including every Studio run, since Studio never sets
    run_dir. Studio keeps the plain "max_trials" fallback.

    Dropped the per-call drop_params: opik_optimizer sets
    litellm.drop_params process-wide on import (base_optimizer.py) and the
    runner imports it, so forcing it here was dead weight that also
    overrode an explicit false. A test now asserts the global instead.

    EmptyDatasetError no longer fires on a full page of id-less rows: the
    fetch is capped at DATASET_SAMPLES but the SDK draws sample ids from the
    whole dataset (sampling._extract_ids calls get_items() unbounded), so a
    full page proves nothing about the rows behind it. Only a short page is
    a complete verdict; a truncated one logs and continues.

    Also types EmptyDatasetError's reason as str | None.

    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com


    Co-authored-by: Claude Fable 5 noreply@anthropic.com

    下载附件