From a64d51e077db878501625bc347fba3e8c0b12667 Mon Sep 17 00:00:00 2001 From: Alec Khoury Date: Mon, 17 Aug 2026 13:44:51 -0500 Subject: [PATCH 1/4] feat(eval-author): default Author clients to medium reasoning effort Add CompletionClientOptions (reasoning_effort + completion_params) on resolve_model_clients, wire EvalAuthorConfig defaults through standalone and Mode 1 nested Author clients, and document the knobs. Signed-off-by: Alec Khoury --- ...26-08-17-eval-author-completion-options.md | 41 +++++++ ...7-eval-author-completion-options-design.md | 98 ++++++++++++++++ .../nemo_platform_plugin/nooa_model_client.py | 36 +++++- .../tests/test_nooa_model_client.py | 108 ++++++++++++++++++ plugins/nemo-eval-author/README.md | 5 + .../eval_author/README.md | 29 ++++- .../eval_author/config.yaml | 2 + .../eval_author/models.py | 17 ++- .../eval_author/run.py | 10 +- .../tests/test_eval_author_run.py | 75 +++++++++++- plugins/nemo-experimentalist/README.md | 8 +- .../benchmarks/configs/tau3-quality.yaml | 1 + .../benchmarks/configs/tau3-smoke.yaml | 1 + .../configs/terminal-bench-quality.yaml | 1 + .../configs/terminal-bench-smoke.yaml | 1 + .../examples/smoke-agent/configs/short.yaml | 1 + .../experimentalist-smoke.yaml | 1 + .../src/nemo_experimentalist_plugin/config.py | 9 +- .../experimentalist/runner.py | 42 +++++-- .../skills/nemo-experimentalist/SKILL.md | 3 + .../test_dataset_staging_runner.py | 27 +++++ .../tests/test_eval_author_config.py | 8 ++ 22 files changed, 505 insertions(+), 19 deletions(-) create mode 100644 docs/superpowers/plans/2026-08-17-eval-author-completion-options.md create mode 100644 docs/superpowers/specs/2026-08-17-eval-author-completion-options-design.md diff --git a/docs/superpowers/plans/2026-08-17-eval-author-completion-options.md b/docs/superpowers/plans/2026-08-17-eval-author-completion-options.md new file mode 100644 index 0000000000..c385e36767 --- /dev/null +++ b/docs/superpowers/plans/2026-08-17-eval-author-completion-options.md @@ -0,0 +1,41 @@ +# Eval Author completion options Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add `CompletionClientOptions` (`reasoning_effort` + `completion_params`) to `nooa_model_client`, and wire Eval Author with default `reasoning_effort="medium"`. + +**Architecture:** Options are resolved once at `resolve_model_clients` and baked into each `CompletionClient`. Eval Author builds options from `EvalAuthorConfig`. Other consumers unchanged when options are omitted. + +**Tech Stack:** Python, dataclasses, Pydantic, pytest, existing Nooa `CompletionClient` kwargs. + +**Spec:** `docs/superpowers/specs/2026-08-17-eval-author-completion-options-design.md` + +--- + +### Task 1: `CompletionClientOptions` + resolve wiring + +**Files:** +- Modify: `packages/nemo_platform_plugin/src/nemo_platform_plugin/nooa_model_client.py` +- Modify/Create: `packages/nemo_platform_plugin/tests/test_nooa_model_client.py` + +- [ ] Add frozen `CompletionClientOptions` +- [ ] Thread `options` through `_completion_client` / `resolve_model_clients` +- [ ] Merge order: base → `completion_params` → explicit `reasoning_effort` if not None +- [ ] Unit tests for omit / medium / params override / effort wins + +### Task 2: Eval Author config + run + +**Files:** +- Modify: `plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/models.py` +- Modify: `plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/run.py` +- Modify: `plugins/nemo-eval-author/tests/test_eval_author_run.py` + +- [ ] `reasoning_effort: str | None = "medium"` +- [ ] `completion_params: dict[str, Any] = Field(default_factory=dict)` +- [ ] Pass `CompletionClientOptions` into `resolve_model_clients` +- [ ] Test default medium + explicit None + params forwarded (mock resolve) + +### Task 3: Verify + +- [ ] `uv run --frozen pytest packages/nemo_platform_plugin/tests/test_nooa_model_client.py plugins/nemo-eval-author/tests/test_eval_author_run.py -q` +- [ ] Carry design doc onto the branch if missing diff --git a/docs/superpowers/specs/2026-08-17-eval-author-completion-options-design.md b/docs/superpowers/specs/2026-08-17-eval-author-completion-options-design.md new file mode 100644 index 0000000000..7d8585d30b --- /dev/null +++ b/docs/superpowers/specs/2026-08-17-eval-author-completion-options-design.md @@ -0,0 +1,98 @@ +# Eval Author completion options + +Date: 2026-08-17 +Status: implemented on branch `eval-author-completion-options/akhoury` + +## Problem + +Eval Author (and other Nooa-backed agents) build `CompletionClient`s through +`nemo_platform_plugin.nooa_model_client` with no way to set `reasoning_effort` or +other LiteLLM/CompletionClient kwargs. On `main`, the field is omitted and the +provider default applies. Smoke Mode 1 evidence shows Author quality is sensitive +to effort; we want Author to default to `medium` without hardcoding it inside +`_completion_client` for every consumer. + +## Goals + +- Let Eval Author specify OpenAI-style `reasoning_effort` (default **`medium`**). +- Let callers pass arbitrary `completion_params` for non-OpenAI backends (or extra + OpenAI knobs) without a second one-off API. +- Keep Analyst and Experimentalist behavior unchanged until they opt in. +- `None` / empty params must match today’s omit-the-field behavior for consumers + that do not set options. + +## Non-goals (v1) + +- Per-component Experimentalist override map. +- Analyst wiring. +- CLI flags (follow-up). +- Provider-specific translation (e.g. Anthropic `thinking` helpers). Unsupported + keys continue to rely on existing `drop_params=True`. + +## Design + +### Shared: `CompletionClientOptions` + +In `packages/nemo_platform_plugin/.../nooa_model_client.py`: + +```python +@dataclass(frozen=True) +class CompletionClientOptions: + reasoning_effort: str | None = None + completion_params: Mapping[str, Any] = field(default_factory=dict) +``` + +`resolve_model_clients(client, refs=None, options: CompletionClientOptions | None = None)` +threads options into `_completion_client`. + +Merge order when constructing `CompletionClient`: + +1. Existing base kwargs (`api_base`, `drop_params`, `_skip_responses_api_bridge`, …). +2. Spread `completion_params`. +3. If `reasoning_effort is not None`, set `reasoning_effort=...` **last** so the + explicit field wins over the same key inside `completion_params`. + +If `options` is `None` or both fields are unset, wire behavior matches `main` +today (no `reasoning_effort` kwarg). + +Apply the same merge for OpenAI- and Anthropic-shaped clients. No format-specific +branching in v1. + +### Eval Author + +`EvalAuthorConfig` gains: + +```python +reasoning_effort: str | None = "medium" +completion_params: dict[str, Any] = Field(default_factory=dict) +``` + +`run_eval_author` builds `CompletionClientOptions` from config and passes it to +`resolve_model_clients`. Mode 1 inherits this when Experimentalist invokes Author: +the runner nested-resolves Author-scoped clients from `config.eval_author` (default +`medium`) so Experimentalist's outer default/fast pair stays unchanged. + +Setting `reasoning_effort=None` in config restores provider-default omit for a +given run. + +### Defaults summary + +| Consumer | v1 default | +| --- | --- | +| `resolve_model_clients` (no options) | omit effort (unchanged) | +| Eval Author config | `reasoning_effort="medium"` | +| Analyst / Experimentalist agents | unchanged (no options passed) | + +## Tests + +- Unit (`test_nooa_model_client`): merge order; `None` omits effort; params + forwarded; explicit effort overrides duplicate key in params. +- Eval Author: default config resolves with `reasoning_effort="medium"`; explicit + `None` omits; `completion_params` reach `resolve_model_clients` (mock). + +## Follow-ups + +- Optional Experimentalist YAML under `eval_author:` for these fields. +- CLI flags for standalone Author runs. +- Run-level + per-role map (design C) for Experimentalist components. +- Anthropic-oriented helpers if silent `drop_params` proves insufficient. diff --git a/packages/nemo_platform_plugin/src/nemo_platform_plugin/nooa_model_client.py b/packages/nemo_platform_plugin/src/nemo_platform_plugin/nooa_model_client.py index db0cd22a77..521b94d780 100644 --- a/packages/nemo_platform_plugin/src/nemo_platform_plugin/nooa_model_client.py +++ b/packages/nemo_platform_plugin/src/nemo_platform_plugin/nooa_model_client.py @@ -9,10 +9,11 @@ public plugin contract. """ -from collections.abc import Iterator +from collections.abc import Iterator, Mapping from contextlib import contextmanager from contextvars import ContextVar -from dataclasses import dataclass +from dataclasses import dataclass, field +from typing import Any from nemo_platform import AsyncNeMoPlatform from nemo_platform.config import get_context @@ -35,6 +36,20 @@ class ConfiguredModelRefs: fast: str +@dataclass(frozen=True) +class CompletionClientOptions: + """Optional kwargs applied when building each run-scoped CompletionClient. + + ``reasoning_effort`` is the OpenAI-shaped shortcut. When ``None``, the field + is omitted so the provider default applies. ``completion_params`` are spread + into ``CompletionClient`` for backend-specific knobs; an explicit + ``reasoning_effort`` wins over the same key inside ``completion_params``. + """ + + reasoning_effort: str | None = None + completion_params: Mapping[str, Any] = field(default_factory=dict) + + @dataclass(frozen=True) class ConfiguredModelClients: """Resolved Nooa clients for default and low-latency agent work.""" @@ -84,10 +99,21 @@ def _parse_model_ref(model_ref: str) -> tuple[str, str]: return workspace, name +def _client_option_kwargs(options: CompletionClientOptions | None) -> dict[str, Any]: + """Merge completion options: params first, then explicit reasoning_effort.""" + if options is None: + return {} + kwargs: dict[str, Any] = dict(options.completion_params) + if options.reasoning_effort is not None: + kwargs["reasoning_effort"] = options.reasoning_effort + return kwargs + + def _completion_client( client: AsyncNeMoPlatform, model_entity: ModelEntity, served_model_name: str, + options: CompletionClientOptions | None = None, ) -> CompletionClient: """Build a Nooa client that routes through the Model Entity's Platform URL.""" api_base = client.models.get_model_entity_route_openai_url(model_entity) @@ -96,6 +122,7 @@ def _completion_client( # response bytes. Nooa needs decoded JSON/SSE, so make that requirement # explicit at this adapter boundary for every configured agent client. extra_headers[_ACCEPT_ENCODING_HEADER] = _IDENTITY_ENCODING + option_kwargs = _client_option_kwargs(options) # Backend format is the Platform-facing wire contract, not the upstream # provider identity. The LiteLLM prefix selects the adapter for that shape. if model_entity.backend_format == _OPENAI_FORMAT: @@ -114,6 +141,7 @@ def _completion_client( # LiteLLM versions otherwise bridge GPT-5.4+ tool calls with any # reasoning_effort value (including "none") to /responses. _skip_responses_api_bridge=True, + **option_kwargs, ) elif model_entity.backend_format == _ANTHROPIC_FORMAT: api_base = api_base.removesuffix("/v1") @@ -131,6 +159,7 @@ def _completion_client( base_model=litellm_model, extra_headers=extra_headers, drop_params=True, + **option_kwargs, ) @@ -159,6 +188,7 @@ async def _served_model_name( async def resolve_model_clients( client: AsyncNeMoPlatform, refs: ConfiguredModelRefs | None = None, + options: CompletionClientOptions | None = None, ) -> ConfiguredModelClients: """Resolve configured Model Entities and construct each distinct client once.""" selected = refs or configured_model_refs() @@ -171,7 +201,7 @@ async def resolve_model_clients( workspace, name = _parse_model_ref(model_ref) entity = await client.models.retrieve(name, workspace=workspace) served_model_name = await _served_model_name(client, entity, provider_cache) - resolved[model_ref] = _completion_client(client, entity, served_model_name) + resolved[model_ref] = _completion_client(client, entity, served_model_name, options) except Exception as resolution_error: for model_client in resolved.values(): try: diff --git a/packages/nemo_platform_plugin/tests/test_nooa_model_client.py b/packages/nemo_platform_plugin/tests/test_nooa_model_client.py index 8614efb4f4..3c643e2b5c 100644 --- a/packages/nemo_platform_plugin/tests/test_nooa_model_client.py +++ b/packages/nemo_platform_plugin/tests/test_nooa_model_client.py @@ -7,6 +7,7 @@ import pytest from nemo_platform_plugin import nooa_model_client from nemo_platform_plugin.nooa_model_client import ( + CompletionClientOptions, ConfiguredModelClients, ConfiguredModelRefs, activate_model_clients, @@ -248,3 +249,110 @@ async def test_resolve_model_clients_closes_constructed_client_after_failure(mon ) constructed.aclose.assert_awaited_once() + + +def _openai_entity(name: str = "gpt-4-1") -> SimpleNamespace: + return SimpleNamespace( + workspace="default", + name=name, + backend_format="OPENAI_CHAT", + model_providers=[], + api_endpoint=None, + ) + + +async def test_resolve_model_clients_omits_reasoning_effort_by_default(monkeypatch): + client = MagicMock() + client.models.retrieve = AsyncMock(return_value=_openai_entity()) + client.models.get_model_entity_route_openai_url.return_value = "http://platform/model/gpt-4-1/-/v1" + client.models.get_client_default_headers.return_value = {} + factory = MagicMock(return_value=MagicMock()) + monkeypatch.setattr(nooa_model_client, "CompletionClient", factory) + + await resolve_model_clients( + client, + ConfiguredModelRefs(default="default/gpt-4-1", fast="default/gpt-4-1"), + ) + + assert "reasoning_effort" not in factory.call_args.kwargs + + +async def test_resolve_model_clients_passes_reasoning_effort(monkeypatch): + client = MagicMock() + client.models.retrieve = AsyncMock(return_value=_openai_entity()) + client.models.get_model_entity_route_openai_url.return_value = "http://platform/model/gpt-4-1/-/v1" + client.models.get_client_default_headers.return_value = {} + factory = MagicMock(return_value=MagicMock()) + monkeypatch.setattr(nooa_model_client, "CompletionClient", factory) + + await resolve_model_clients( + client, + ConfiguredModelRefs(default="default/gpt-4-1", fast="default/gpt-4-1"), + CompletionClientOptions(reasoning_effort="medium"), + ) + + assert factory.call_args.kwargs["reasoning_effort"] == "medium" + + +async def test_resolve_model_clients_forwards_completion_params(monkeypatch): + client = MagicMock() + client.models.retrieve = AsyncMock(return_value=_openai_entity()) + client.models.get_model_entity_route_openai_url.return_value = "http://platform/model/gpt-4-1/-/v1" + client.models.get_client_default_headers.return_value = {} + factory = MagicMock(return_value=MagicMock()) + monkeypatch.setattr(nooa_model_client, "CompletionClient", factory) + + await resolve_model_clients( + client, + ConfiguredModelRefs(default="default/gpt-4-1", fast="default/gpt-4-1"), + CompletionClientOptions(completion_params={"temperature": 0.0, "thinking": {"type": "enabled"}}), + ) + + assert factory.call_args.kwargs["temperature"] == 0.0 + assert factory.call_args.kwargs["thinking"] == {"type": "enabled"} + assert "reasoning_effort" not in factory.call_args.kwargs + + +async def test_resolve_model_clients_reasoning_effort_overrides_completion_params(monkeypatch): + client = MagicMock() + client.models.retrieve = AsyncMock(return_value=_openai_entity()) + client.models.get_model_entity_route_openai_url.return_value = "http://platform/model/gpt-4-1/-/v1" + client.models.get_client_default_headers.return_value = {} + factory = MagicMock(return_value=MagicMock()) + monkeypatch.setattr(nooa_model_client, "CompletionClient", factory) + + await resolve_model_clients( + client, + ConfiguredModelRefs(default="default/gpt-4-1", fast="default/gpt-4-1"), + CompletionClientOptions( + reasoning_effort="medium", + completion_params={"reasoning_effort": "minimal", "temperature": 0.2}, + ), + ) + + assert factory.call_args.kwargs["reasoning_effort"] == "medium" + assert factory.call_args.kwargs["temperature"] == 0.2 + + +async def test_resolve_model_clients_applies_options_to_anthropic_clients(monkeypatch): + model_entity = SimpleNamespace( + workspace="default", + name="claude-sonnet-4", + backend_format="ANTHROPIC_MESSAGES", + model_providers=[], + api_endpoint=None, + ) + client = MagicMock() + client.models.retrieve = AsyncMock(return_value=model_entity) + client.models.get_model_entity_route_openai_url.return_value = "http://platform/model/claude-sonnet-4/-/v1" + client.models.get_client_default_headers.return_value = {} + factory = MagicMock(return_value=MagicMock()) + monkeypatch.setattr(nooa_model_client, "CompletionClient", factory) + + await resolve_model_clients( + client, + ConfiguredModelRefs(default="default/claude-sonnet-4", fast="default/claude-sonnet-4"), + CompletionClientOptions(completion_params={"thinking": {"type": "enabled", "budget_tokens": 2048}}), + ) + + assert factory.call_args.kwargs["thinking"] == {"type": "enabled", "budget_tokens": 2048} diff --git a/plugins/nemo-eval-author/README.md b/plugins/nemo-eval-author/README.md index 62b2ab7005..385d8746ba 100644 --- a/plugins/nemo-eval-author/README.md +++ b/plugins/nemo-eval-author/README.md @@ -56,6 +56,11 @@ For non-interactive and isolated environments, `NEMO_DEFAULT_MODEL` and `NEMO_FAST_MODEL` can override the stored selections. Values must still use `workspace/model-name` and refer to Model Entities on the target Platform. +Completion options (not model IDs) live on `EvalAuthorConfig`: +`reasoning_effort` defaults to `"medium"`, and `completion_params` can pass +backend-specific kwargs. See +[`eval_author/README.md`](src/nemo_eval_author_plugin/eval_author/README.md#evalauthorconfig-model-and-completion-options). + A `nemo agents eval-author` CLI is registered under `nemo.cli.agents` and mounted by the agents plugin. Verb scaffolding is in place (`discover`, `audit`, `propose`, `run`, `doctor`); bodies are still diff --git a/plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/README.md b/plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/README.md index a98af388b0..d0a180b0ba 100644 --- a/plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/README.md +++ b/plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/README.md @@ -38,7 +38,34 @@ The preset includes the run inputs needed by `run_eval_author(...)`: - `task_template`: local or `fileset://` evaluator task template URI for production traces. - `experiment_dir`: local Eval Author working directory. - `workspace`, `base_url`, `mode`, and `evaluator_type`: platform and evaluator routing. -- `eval_author.max_summary_tokens` and `eval_author.max_traces`: agent tuning parameters. +- `eval_author.*`: agent tuning — see below. + +### `EvalAuthorConfig` (model and completion options) + +| Field | Default | Meaning | +| --- | --- | --- | +| `max_summary_tokens` | `80000` | Token budget for the fast-model summarizer. | +| `max_traces` | `10` | Insight `trace_refs` to analyze in depth. | +| `max_validation_repair_attempts` | `5` | Repair attempts after Insight verifier validation fails. | +| `reasoning_effort` | `"medium"` | OpenAI-shaped effort passed into `CompletionClient`. Set to `null` to omit the field (provider default). | +| `completion_params` | `{}` | Extra kwargs forwarded to `CompletionClient` (non-OpenAI backends or other OpenAI knobs). An explicit `reasoning_effort` wins over the same key here. | + +Standalone `run_eval_author(...)` builds clients with these options. In +Experimentalist Insight mode, the runner nested-resolves Author-scoped clients +from the run config's `eval_author` block (same defaults), then restores the +outer Experimentalist default/fast pair for the optimization loop. + +Example override in Experimentalist `--config` YAML: + +```yaml +eval_author: + max_traces: 5 + reasoning_effort: medium # default; use null to omit + # completion_params: + # thinking: + # type: enabled + # budget_tokens: 2048 +``` ## Materialized Insight Suite diff --git a/plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/config.yaml b/plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/config.yaml index 559b5e62c7..0f9c6ed505 100644 --- a/plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/config.yaml +++ b/plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/config.yaml @@ -23,3 +23,5 @@ experiment_dir: tmp/eval_author eval_author: max_summary_tokens: 80000 max_traces: 10 + reasoning_effort: medium + # completion_params: {} diff --git a/plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/models.py b/plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/models.py index 67dba19207..ccadeedcfc 100644 --- a/plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/models.py +++ b/plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/models.py @@ -3,7 +3,7 @@ """Small boundary models for Eval Author.""" -from typing import Self +from typing import Any, Self from nemo_experimentalist_plugin.entities import Dataset from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator @@ -65,6 +65,21 @@ class EvalAuthorConfig(BaseModel): le=10, description="Max Eval Author repair attempts after mandatory Insight verifier validation fails.", ) + reasoning_effort: str | None = Field( + default="medium", + description=( + "OpenAI-shaped reasoning_effort passed to CompletionClient. " + "Default medium. Set to None to omit the field (provider default)." + ), + ) + completion_params: dict[str, Any] = Field( + default_factory=dict, + description=( + "Extra kwargs forwarded to CompletionClient (for non-OpenAI backends or " + "additional OpenAI knobs). An explicit reasoning_effort wins over the " + "same key here." + ), + ) class EvalAuthorResult(BaseModel): diff --git a/plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/run.py b/plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/run.py index 08303119ad..ee50238352 100644 --- a/plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/run.py +++ b/plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/run.py @@ -19,6 +19,7 @@ from nemo_insights_plugin.entities import Insight from nemo_platform import AsyncNeMoPlatform from nemo_platform_plugin.nooa_model_client import ( + CompletionClientOptions, ConfiguredModelClients, ConfiguredModelRefs, activate_model_clients, @@ -82,7 +83,14 @@ async def run_eval_author( client = make_client(base_url) model_clients: ConfiguredModelClients | None = None try: - model_clients = await resolve_model_clients(client, selected_model_refs) + model_clients = await resolve_model_clients( + client, + selected_model_refs, + CompletionClientOptions( + reasoning_effort=config.reasoning_effort, + completion_params=config.completion_params, + ), + ) backend = make_experimentalist_backend( client=client, experiments_output=str(experiment_dir), diff --git a/plugins/nemo-eval-author/tests/test_eval_author_run.py b/plugins/nemo-eval-author/tests/test_eval_author_run.py index f0d86c96d5..28b6c4c2fa 100644 --- a/plugins/nemo-eval-author/tests/test_eval_author_run.py +++ b/plugins/nemo-eval-author/tests/test_eval_author_run.py @@ -45,12 +45,15 @@ def model_clients(monkeypatch: pytest.MonkeyPatch) -> ClosingModelClients: default="workspace-a/default-model", fast="workspace-a/fast-model", ) + resolve_calls: list[tuple[object, ...]] = [] - async def resolve(*_: object) -> ClosingModelClients: + async def resolve(*args: object) -> ClosingModelClients: + resolve_calls.append(args) return clients monkeypatch.setattr(eval_author_run, "configured_model_refs", lambda: refs) monkeypatch.setattr(eval_author_run, "resolve_model_clients", resolve) + clients.resolve_calls = resolve_calls # type: ignore[attr-defined] return clients @@ -225,6 +228,76 @@ async def test_run_eval_author_resolves_inputs_and_returns_datasets( assert model_clients.closed +def test_eval_author_config_defaults_reasoning_effort_to_medium() -> None: + config = EvalAuthorConfig() + assert config.reasoning_effort == "medium" + assert config.completion_params == {} + + +@pytest.mark.asyncio +async def test_run_eval_author_passes_completion_options_to_resolve( + monkeypatch: pytest.MonkeyPatch, + model_clients: ClosingModelClients, + tmp_path: Path, +) -> None: + client = ClosingClient() + insight = Insight( + workspace="workspace-a", + title="failure", + description="description", + agent="insight-agent", + trace_refs=["trace-1"], + ) + backend = FakeBackend(insight) + dataset_factory = FakeDatasetFactory() + eval_author = FakeEvalAuthor() + monkeypatch.setattr(eval_author_run, "make_client", lambda _: client) + monkeypatch.setattr(eval_author_run, "make_experimentalist_backend", lambda **_: backend) + monkeypatch.setattr(eval_author_run, "DatasetFactory", lambda: dataset_factory) + monkeypatch.setattr(eval_author_run, "build_eval_author_agent", lambda **_: eval_author) + template = tmp_path / "template" + template.mkdir() + (template / "task.toml").write_text("template\n", encoding="utf-8") + train = tmp_path / "train" + validation = tmp_path / "validation" + train.mkdir() + validation.mkdir() + + await eval_author_run.run_eval_author( + insight="insight-123", + train_dataset=DatasetRef(uri=str(train)), + validation_dataset=DatasetRef(uri=str(validation)), + task_template=DatasetRef(uri=str(template)), + experiment_dir=tmp_path / "experiment-default", + workspace="workspace-a", + base_url="http://platform.test", + config=EvalAuthorConfig(), + ) + await eval_author_run.run_eval_author( + insight="insight-123", + train_dataset=DatasetRef(uri=str(train)), + validation_dataset=DatasetRef(uri=str(validation)), + task_template=DatasetRef(uri=str(template)), + experiment_dir=tmp_path / "experiment-omit", + workspace="workspace-a", + base_url="http://platform.test", + config=EvalAuthorConfig( + reasoning_effort=None, + completion_params={"temperature": 0.0}, + ), + ) + + resolve_calls = model_clients.resolve_calls # type: ignore[attr-defined] + assert len(resolve_calls) == 2 + default_options = resolve_calls[0][2] + omit_options = resolve_calls[1][2] + assert isinstance(default_options, eval_author_run.CompletionClientOptions) + assert default_options.reasoning_effort == "medium" + assert dict(default_options.completion_params) == {} + assert omit_options.reasoning_effort is None + assert dict(omit_options.completion_params) == {"temperature": 0.0} + + def test_public_apis_accept_train_validation_and_generated_task_inputs() -> None: orchestration = inspect.signature(eval_author_run.run_eval_author).parameters agent_run = inspect.signature(EvalAuthor.run).parameters diff --git a/plugins/nemo-experimentalist/README.md b/plugins/nemo-experimentalist/README.md index 5041c9e206..e17dc0ada2 100644 --- a/plugins/nemo-experimentalist/README.md +++ b/plugins/nemo-experimentalist/README.md @@ -78,6 +78,8 @@ single leader, so complementary strengths stay alive across rounds. analyze traces or host an Insight API. - [NeMo Eval Author](../nemo-eval-author/README.md) builds the Insight-specific evaluation suite, invoked automatically in Insight mode. + Authoring uses `eval_author` run settings (including `reasoning_effort`, + default `medium`); see the Eval Author README for the full config surface. - **Harbor** runs the task containers that score every candidate. - **NeMo Experiments** mirrors each run and its candidates as an experiment group, so the lineage is visible in Studio. Structure only — rewards and @@ -171,7 +173,7 @@ win. "Required" below means required *when the profile does not supply it*. | `--train-dataset` | Local Harbor dataset or registry ref — the split candidates are proposed against. | Yes. | | `--validation-dataset` | The held-out split that selects the winner. | Yes. | | `--task-template` | Directory holding one Harbor task template (`task.toml` with placeholders); Eval Author fills a copy per failing trace. | Insight-driven mode only. | -| `--config` | Run configuration: round and candidate limits plus `source`, `storage`, `goal_config`, `coder`, `analyzer`, `proposer`, `evaluator`, `eval_author`. Rejects a `models:` key. | No — defaults apply. | +| `--config` | Run configuration: round and candidate limits plus `source`, `storage`, `goal_config`, `coder`, `analyzer`, `proposer`, `evaluator`, `eval_author` (Insight-mode Author tuning, including `reasoning_effort`, default `medium`). Rejects a `models:` key. | No — defaults apply. | | `--workspace` | NeMo workspace for traces and run metadata. | No — profile, else `default`. | | `--base-url` | URL of the running platform. | No — `NMP_BASE_URL`, else `http://localhost:8080`. | | `--experiment-dir` | Where `eval-and-optimize/` is written. Also `-o`, `--output`, `--experiments-output`. | No — see [Output](#output). | @@ -285,7 +287,9 @@ Objectives are what candidates are Pareto-ranked on, with minimized metrics sign-inverted; regression metrics are deliberately kept out of that ranking. In an Insight-driven run, Eval Author's authored Insight metrics take over as the objective and your configured targets move to `regression_metrics`, so the -Insight gets fixed without giving up what the run already cared about. +Insight gets fixed without giving up what the run already cared about. Eval +Author itself defaults to `reasoning_effort: medium` under `eval_author` in +`--config`; set it to `null` to omit the field. The agent under test is configured separately, and none of the variables above reach it. What arrives in the evaluation container is whatever each Harbor task diff --git a/plugins/nemo-experimentalist/benchmarks/configs/tau3-quality.yaml b/plugins/nemo-experimentalist/benchmarks/configs/tau3-quality.yaml index 0461d875b5..e2cf56cf89 100644 --- a/plugins/nemo-experimentalist/benchmarks/configs/tau3-quality.yaml +++ b/plugins/nemo-experimentalist/benchmarks/configs/tau3-quality.yaml @@ -29,3 +29,4 @@ optimizer: eval_author: max_traces: 10 max_validation_repair_attempts: 5 + reasoning_effort: medium diff --git a/plugins/nemo-experimentalist/benchmarks/configs/tau3-smoke.yaml b/plugins/nemo-experimentalist/benchmarks/configs/tau3-smoke.yaml index ddb518059d..0482e3aa05 100644 --- a/plugins/nemo-experimentalist/benchmarks/configs/tau3-smoke.yaml +++ b/plugins/nemo-experimentalist/benchmarks/configs/tau3-smoke.yaml @@ -27,3 +27,4 @@ optimizer: eval_author: max_traces: 3 max_validation_repair_attempts: 2 + reasoning_effort: medium diff --git a/plugins/nemo-experimentalist/benchmarks/configs/terminal-bench-quality.yaml b/plugins/nemo-experimentalist/benchmarks/configs/terminal-bench-quality.yaml index 5f4e3d577c..01b6d96e8c 100644 --- a/plugins/nemo-experimentalist/benchmarks/configs/terminal-bench-quality.yaml +++ b/plugins/nemo-experimentalist/benchmarks/configs/terminal-bench-quality.yaml @@ -23,3 +23,4 @@ optimizer: eval_author: max_traces: 10 max_validation_repair_attempts: 5 + reasoning_effort: medium diff --git a/plugins/nemo-experimentalist/benchmarks/configs/terminal-bench-smoke.yaml b/plugins/nemo-experimentalist/benchmarks/configs/terminal-bench-smoke.yaml index 942e051505..3ebfae6378 100644 --- a/plugins/nemo-experimentalist/benchmarks/configs/terminal-bench-smoke.yaml +++ b/plugins/nemo-experimentalist/benchmarks/configs/terminal-bench-smoke.yaml @@ -24,3 +24,4 @@ optimizer: eval_author: max_traces: 3 max_validation_repair_attempts: 2 + reasoning_effort: medium diff --git a/plugins/nemo-experimentalist/examples/smoke-agent/configs/short.yaml b/plugins/nemo-experimentalist/examples/smoke-agent/configs/short.yaml index b55362393f..2c804bdbff 100644 --- a/plugins/nemo-experimentalist/examples/smoke-agent/configs/short.yaml +++ b/plugins/nemo-experimentalist/examples/smoke-agent/configs/short.yaml @@ -94,3 +94,4 @@ outcome_evaluator_config: # shapes recorded for every smoke-agent group. eval_author: max_traces: 5 + reasoning_effort: medium diff --git a/plugins/nemo-experimentalist/examples/tau3-nooa-agent/experimentalist-smoke.yaml b/plugins/nemo-experimentalist/examples/tau3-nooa-agent/experimentalist-smoke.yaml index 4b6c043500..397ca476a4 100644 --- a/plugins/nemo-experimentalist/examples/tau3-nooa-agent/experimentalist-smoke.yaml +++ b/plugins/nemo-experimentalist/examples/tau3-nooa-agent/experimentalist-smoke.yaml @@ -19,3 +19,4 @@ outcome_evaluator_config: eval_author: max_traces: 3 max_validation_repair_attempts: 2 + reasoning_effort: medium diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/config.py b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/config.py index 886246955f..c12cd138e2 100644 --- a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/config.py +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/config.py @@ -214,7 +214,14 @@ def reject_legacy_curator_config(cls, data: Any) -> Any: default_factory=dict, description="Config for the selected 'evaluation' component; its own model validates it.", ) - eval_author: EvalAuthorConfig = Field(default_factory=EvalAuthorConfig) + eval_author: EvalAuthorConfig = Field( + default_factory=EvalAuthorConfig, + description=( + "Insight-mode Eval Author settings. Defaults match EvalAuthorConfig, including " + "reasoning_effort='medium' and empty completion_params. Override in run YAML under " + "eval_author; set reasoning_effort to null to omit the field (provider default)." + ), + ) @model_validator(mode="after") def validate_metric_contract(self) -> Self: diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/runner.py b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/runner.py index ccb68b22a0..71ca88af2e 100644 --- a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/runner.py +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/runner.py @@ -239,17 +239,41 @@ async def _prepare_inputs(self) -> PreparedInputs: # Lazy: a run without an Insight never authors an eval suite, so it must not # fail to import when the Eval Author package is absent. from nemo_eval_author_plugin.eval_author.agent import EvalAuthor # noqa: PLC0415 + from nemo_platform_plugin.nooa_model_client import ( # noqa: PLC0415 + CompletionClientOptions, + activate_model_clients, + get_configured_model_refs, + resolve_model_clients, + ) - authored = await EvalAuthor( - experiment_dir=self._root, config=self._config.eval_author, reporter=self._reporter - ).run( - insight=insight, - agent_path=agent_dir, - task_template=dataset_factory.build_task_template(self._config.outcome_evaluator, template_ref), - train_dataset=datasets["train"], - validation_dataset=datasets["validation"], - client=self._backend.client, + # Author gets its own clients so eval_author.reasoning_effort / + # completion_params apply without changing the outer Experimentalist pair. + author_clients = await resolve_model_clients( + self._backend.client, + get_configured_model_refs(), + CompletionClientOptions( + reasoning_effort=self._config.eval_author.reasoning_effort, + completion_params=self._config.eval_author.completion_params, + ), ) + try: + with activate_model_clients(author_clients): + authored = await EvalAuthor( + experiment_dir=self._root, + config=self._config.eval_author, + reporter=self._reporter, + ).run( + insight=insight, + agent_path=agent_dir, + task_template=dataset_factory.build_task_template( + self._config.outcome_evaluator, template_ref + ), + train_dataset=datasets["train"], + validation_dataset=datasets["validation"], + client=self._backend.client, + ) + finally: + await author_clients.aclose() datasets["train"] = authored.train_dataset datasets["validation"] = authored.validation_dataset diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/skills/nemo-experimentalist/SKILL.md b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/skills/nemo-experimentalist/SKILL.md index 0cf2c1c9b1..4dc592ef7c 100644 --- a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/skills/nemo-experimentalist/SKILL.md +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/skills/nemo-experimentalist/SKILL.md @@ -260,6 +260,8 @@ to select the winner. | `builder_config.max_fix_attempts` | `1` | `2` (default) | Maximum repair iterations when a candidate fails its integration check. | | `outcome_evaluator_config.n_attempts` | `1` | `1`; increase only when task results are noisy | Repeats each evaluation trial. | | `eval_author.max_traces` | `3` | `10` | Representative Insight traces deeply analyzed in Insight-driven mode. | +| `eval_author.reasoning_effort` | `"medium"` | `"medium"` | OpenAI-shaped effort for Author clients. Set `null` to omit (provider default). | +| `eval_author.completion_params` | `{}` | `{}` | Extra `CompletionClient` kwargs for non-OpenAI backends or other knobs. | A small explicit smoke configuration looks like this: @@ -281,6 +283,7 @@ outcome_evaluator_config: n_attempts: 1 eval_author: max_traces: 3 + reasoning_effort: medium ``` ### Create a low-cost smoke dataset diff --git a/plugins/nemo-experimentalist/tests/experimentalist/test_dataset_staging_runner.py b/plugins/nemo-experimentalist/tests/experimentalist/test_dataset_staging_runner.py index 6ee92cd68c..54fa8b9d66 100644 --- a/plugins/nemo-experimentalist/tests/experimentalist/test_dataset_staging_runner.py +++ b/plugins/nemo-experimentalist/tests/experimentalist/test_dataset_staging_runner.py @@ -38,6 +38,29 @@ def _write_tree(root: Path, content: str) -> None: (tests / "test.sh").write_text(content, encoding="utf-8") +def _stub_author_model_clients(monkeypatch: pytest.MonkeyPatch) -> list[object]: + """Mode 1 now resolves Author-scoped clients; keep staging tests offline.""" + resolve_calls: list[object] = [] + + class _Clients: + async def aclose(self) -> None: + return None + + async def resolve(client: object, refs: object = None, options: object = None) -> _Clients: + resolve_calls.append(options) + return _Clients() + + monkeypatch.setattr( + "nemo_platform_plugin.nooa_model_client.resolve_model_clients", + resolve, + ) + monkeypatch.setattr( + "nemo_platform_plugin.nooa_model_client.get_configured_model_refs", + lambda: SimpleNamespace(default="default/m", fast="default/m"), + ) + return resolve_calls + + @pytest.mark.asyncio async def test_insight_run_stages_inputs_and_stops_at_eval_author_handoff( monkeypatch: pytest.MonkeyPatch, @@ -95,6 +118,7 @@ async def run( ) monkeypatch.setattr(runner_module, "DatasetFactory", RecordingDatasetFactory) monkeypatch.setattr("nemo_eval_author_plugin.eval_author.agent.EvalAuthor", MutatingEvalAuthor) + resolve_calls = _stub_author_model_clients(monkeypatch) agent_dir = tmp_path / "agent" agent_dir.mkdir() @@ -129,6 +153,8 @@ async def run( f"runner built datasets without allow_empty in insight mode: {build_options}" ) assert not (template.parent / "generated-task").exists() + assert len(resolve_calls) == 1 + assert getattr(resolve_calls[0], "reasoning_effort", None) == "medium" @pytest.mark.asyncio @@ -180,6 +206,7 @@ async def run(self, *, train_dataset, validation_dataset, **kwargs: object) -> A "distribute_insight_suite_tasks", lambda suite, train, validation: distributed.update(suite=suite, train=train, validation=validation), ) + _stub_author_model_clients(monkeypatch) agent_dir = tmp_path / "agent" agent_dir.mkdir() diff --git a/plugins/nemo-experimentalist/tests/test_eval_author_config.py b/plugins/nemo-experimentalist/tests/test_eval_author_config.py index 997cf65ae0..38fd51a4a7 100644 --- a/plugins/nemo-experimentalist/tests/test_eval_author_config.py +++ b/plugins/nemo-experimentalist/tests/test_eval_author_config.py @@ -13,6 +13,14 @@ def test_evolutionary_optimizer_uses_top_level_eval_author_config() -> None: assert type(config) is EvalAuthorConfig assert config.max_validation_repair_attempts == 5 + assert config.reasoning_effort == "medium" + assert config.completion_params == {} + + +def test_partial_eval_author_yaml_keeps_medium_reasoning_effort() -> None: + config = EvolutionaryOptimizerConfig.model_validate({"eval_author": {"max_traces": 5}}) + assert config.eval_author.max_traces == 5 + assert config.eval_author.reasoning_effort == "medium" def test_evolutionary_optimizer_tolerates_unknown_eval_author_config() -> None: From c4f15a9744d4ca3eba71c4b5675334b7f579456e Mon Sep 17 00:00:00 2001 From: Alec Khoury Date: Mon, 17 Aug 2026 13:54:18 -0500 Subject: [PATCH 2/4] docs(eval-author): require medium-or-higher reasoning for consistent Author metrics Signed-off-by: Alec Khoury --- plugins/nemo-eval-author/README.md | 6 ++++-- .../nemo_eval_author_plugin/eval_author/README.md | 11 +++++++++-- .../nemo_eval_author_plugin/eval_author/config.yaml | 1 + .../nemo_eval_author_plugin/eval_author/models.py | 4 +++- plugins/nemo-experimentalist/README.md | 13 +++++++------ .../examples/smoke-agent/configs/short.yaml | 1 + .../src/nemo_experimentalist_plugin/config.py | 4 ++-- .../skills/nemo-experimentalist/SKILL.md | 3 ++- 8 files changed, 29 insertions(+), 14 deletions(-) diff --git a/plugins/nemo-eval-author/README.md b/plugins/nemo-eval-author/README.md index 385d8746ba..4fd64fe7a4 100644 --- a/plugins/nemo-eval-author/README.md +++ b/plugins/nemo-eval-author/README.md @@ -56,8 +56,10 @@ For non-interactive and isolated environments, `NEMO_DEFAULT_MODEL` and `NEMO_FAST_MODEL` can override the stored selections. Values must still use `workspace/model-name` and refer to Model Entities on the target Platform. -Completion options (not model IDs) live on `EvalAuthorConfig`: -`reasoning_effort` defaults to `"medium"`, and `completion_params` can pass +Completion options (not model IDs) live on `EvalAuthorConfig`. +`reasoning_effort` defaults to `"medium"` and **must stay at `medium` or +higher** for consistent metric authoring; weaker settings (or omitting the +field) often produce flat authored metrics. `completion_params` can pass backend-specific kwargs. See [`eval_author/README.md`](src/nemo_eval_author_plugin/eval_author/README.md#evalauthorconfig-model-and-completion-options). diff --git a/plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/README.md b/plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/README.md index d0a180b0ba..e610f08dfd 100644 --- a/plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/README.md +++ b/plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/README.md @@ -42,12 +42,19 @@ The preset includes the run inputs needed by `run_eval_author(...)`: ### `EvalAuthorConfig` (model and completion options) +**Use `reasoning_effort` of `medium` or higher** (`high`, and any stronger +provider value your model accepts). Below `medium` — including `minimal`, +`low`, `none`, or omitting the field so the provider picks a weak default — +Eval Author often authors flat, non-discriminating metrics that look fine on a +broken baseline. The default is `"medium"` for that reason. Prefer raising +effort over lowering it when Author quality is inconsistent. + | Field | Default | Meaning | | --- | --- | --- | | `max_summary_tokens` | `80000` | Token budget for the fast-model summarizer. | | `max_traces` | `10` | Insight `trace_refs` to analyze in depth. | | `max_validation_repair_attempts` | `5` | Repair attempts after Insight verifier validation fails. | -| `reasoning_effort` | `"medium"` | OpenAI-shaped effort passed into `CompletionClient`. Set to `null` to omit the field (provider default). | +| `reasoning_effort` | `"medium"` | OpenAI-shaped effort for Author clients. Keep at `medium` or higher for consistent metric authoring; do not set `null` / `minimal` / `low` unless you are deliberately testing failure modes. | | `completion_params` | `{}` | Extra kwargs forwarded to `CompletionClient` (non-OpenAI backends or other OpenAI knobs). An explicit `reasoning_effort` wins over the same key here. | Standalone `run_eval_author(...)` builds clients with these options. In @@ -60,7 +67,7 @@ Example override in Experimentalist `--config` YAML: ```yaml eval_author: max_traces: 5 - reasoning_effort: medium # default; use null to omit + reasoning_effort: medium # required floor for consistent Author metrics; use high if needed # completion_params: # thinking: # type: enabled diff --git a/plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/config.yaml b/plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/config.yaml index 0f9c6ed505..3e53670449 100644 --- a/plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/config.yaml +++ b/plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/config.yaml @@ -23,5 +23,6 @@ experiment_dir: tmp/eval_author eval_author: max_summary_tokens: 80000 max_traces: 10 + # medium or higher required for consistent Author metrics reasoning_effort: medium # completion_params: {} diff --git a/plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/models.py b/plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/models.py index ccadeedcfc..d9c1174436 100644 --- a/plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/models.py +++ b/plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/models.py @@ -69,7 +69,9 @@ class EvalAuthorConfig(BaseModel): default="medium", description=( "OpenAI-shaped reasoning_effort passed to CompletionClient. " - "Default medium. Set to None to omit the field (provider default)." + "Default medium. Keep at medium or higher for consistent metric " + "authoring; weaker values (or None, which omits the field) often " + "produce flat authored metrics." ), ) completion_params: dict[str, Any] = Field( diff --git a/plugins/nemo-experimentalist/README.md b/plugins/nemo-experimentalist/README.md index e17dc0ada2..476b4c8ccc 100644 --- a/plugins/nemo-experimentalist/README.md +++ b/plugins/nemo-experimentalist/README.md @@ -78,8 +78,9 @@ single leader, so complementary strengths stay alive across rounds. analyze traces or host an Insight API. - [NeMo Eval Author](../nemo-eval-author/README.md) builds the Insight-specific evaluation suite, invoked automatically in Insight mode. - Authoring uses `eval_author` run settings (including `reasoning_effort`, - default `medium`); see the Eval Author README for the full config surface. + Authoring uses `eval_author` run settings; keep `reasoning_effort` at + `medium` or higher so authored metrics stay discriminating. See the Eval + Author README for the full config surface. - **Harbor** runs the task containers that score every candidate. - **NeMo Experiments** mirrors each run and its candidates as an experiment group, so the lineage is visible in Studio. Structure only — rewards and @@ -173,7 +174,7 @@ win. "Required" below means required *when the profile does not supply it*. | `--train-dataset` | Local Harbor dataset or registry ref — the split candidates are proposed against. | Yes. | | `--validation-dataset` | The held-out split that selects the winner. | Yes. | | `--task-template` | Directory holding one Harbor task template (`task.toml` with placeholders); Eval Author fills a copy per failing trace. | Insight-driven mode only. | -| `--config` | Run configuration: round and candidate limits plus `source`, `storage`, `goal_config`, `coder`, `analyzer`, `proposer`, `evaluator`, `eval_author` (Insight-mode Author tuning, including `reasoning_effort`, default `medium`). Rejects a `models:` key. | No — defaults apply. | +| `--config` | Run configuration: round and candidate limits plus `source`, `storage`, `goal_config`, `coder`, `analyzer`, `proposer`, `evaluator`, `eval_author` (Insight-mode Author tuning; keep `reasoning_effort` at `medium` or higher). Rejects a `models:` key. | No — defaults apply. | | `--workspace` | NeMo workspace for traces and run metadata. | No — profile, else `default`. | | `--base-url` | URL of the running platform. | No — `NMP_BASE_URL`, else `http://localhost:8080`. | | `--experiment-dir` | Where `eval-and-optimize/` is written. Also `-o`, `--output`, `--experiments-output`. | No — see [Output](#output). | @@ -287,9 +288,9 @@ Objectives are what candidates are Pareto-ranked on, with minimized metrics sign-inverted; regression metrics are deliberately kept out of that ranking. In an Insight-driven run, Eval Author's authored Insight metrics take over as the objective and your configured targets move to `regression_metrics`, so the -Insight gets fixed without giving up what the run already cared about. Eval -Author itself defaults to `reasoning_effort: medium` under `eval_author` in -`--config`; set it to `null` to omit the field. +Insight gets fixed without giving up what the run already cared about. Keep +`eval_author.reasoning_effort` at `medium` or higher (the default is `medium`); +weaker effort often yields flat authored metrics that do not track the repair. The agent under test is configured separately, and none of the variables above reach it. What arrives in the evaluation container is whatever each Harbor task diff --git a/plugins/nemo-experimentalist/examples/smoke-agent/configs/short.yaml b/plugins/nemo-experimentalist/examples/smoke-agent/configs/short.yaml index 2c804bdbff..5a750a9915 100644 --- a/plugins/nemo-experimentalist/examples/smoke-agent/configs/short.yaml +++ b/plugins/nemo-experimentalist/examples/smoke-agent/configs/short.yaml @@ -94,4 +94,5 @@ outcome_evaluator_config: # shapes recorded for every smoke-agent group. eval_author: max_traces: 5 + # medium or higher — required for consistent Author metrics reasoning_effort: medium diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/config.py b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/config.py index c12cd138e2..c081307b29 100644 --- a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/config.py +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/config.py @@ -218,8 +218,8 @@ def reject_legacy_curator_config(cls, data: Any) -> Any: default_factory=EvalAuthorConfig, description=( "Insight-mode Eval Author settings. Defaults match EvalAuthorConfig, including " - "reasoning_effort='medium' and empty completion_params. Override in run YAML under " - "eval_author; set reasoning_effort to null to omit the field (provider default)." + "reasoning_effort='medium'. Keep reasoning_effort at medium or higher for " + "consistent discriminating metrics; weaker values often author flat metrics." ), ) diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/skills/nemo-experimentalist/SKILL.md b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/skills/nemo-experimentalist/SKILL.md index 4dc592ef7c..10536921b3 100644 --- a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/skills/nemo-experimentalist/SKILL.md +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/skills/nemo-experimentalist/SKILL.md @@ -260,7 +260,7 @@ to select the winner. | `builder_config.max_fix_attempts` | `1` | `2` (default) | Maximum repair iterations when a candidate fails its integration check. | | `outcome_evaluator_config.n_attempts` | `1` | `1`; increase only when task results are noisy | Repeats each evaluation trial. | | `eval_author.max_traces` | `3` | `10` | Representative Insight traces deeply analyzed in Insight-driven mode. | -| `eval_author.reasoning_effort` | `"medium"` | `"medium"` | OpenAI-shaped effort for Author clients. Set `null` to omit (provider default). | +| `eval_author.reasoning_effort` | `"medium"` | `"medium"` or `"high"` | OpenAI-shaped effort for Author clients. **Must be `medium` or higher** for consistent discriminating metrics; do not use `null` / `minimal` / `low` except when testing failure modes. | | `eval_author.completion_params` | `{}` | `{}` | Extra `CompletionClient` kwargs for non-OpenAI backends or other knobs. | A small explicit smoke configuration looks like this: @@ -283,6 +283,7 @@ outcome_evaluator_config: n_attempts: 1 eval_author: max_traces: 3 + # medium or higher — required for consistent Author metrics reasoning_effort: medium ``` From 49eb43511ba6ed232b161ef961d4e73d20d6c894 Mon Sep 17 00:00:00 2001 From: Alec Khoury Date: Mon, 17 Aug 2026 13:54:55 -0500 Subject: [PATCH 3/4] chore: drop internal design/plan docs from the PR Signed-off-by: Alec Khoury --- ...26-08-17-eval-author-completion-options.md | 41 -------- ...7-eval-author-completion-options-design.md | 98 ------------------- 2 files changed, 139 deletions(-) delete mode 100644 docs/superpowers/plans/2026-08-17-eval-author-completion-options.md delete mode 100644 docs/superpowers/specs/2026-08-17-eval-author-completion-options-design.md diff --git a/docs/superpowers/plans/2026-08-17-eval-author-completion-options.md b/docs/superpowers/plans/2026-08-17-eval-author-completion-options.md deleted file mode 100644 index c385e36767..0000000000 --- a/docs/superpowers/plans/2026-08-17-eval-author-completion-options.md +++ /dev/null @@ -1,41 +0,0 @@ -# Eval Author completion options Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add `CompletionClientOptions` (`reasoning_effort` + `completion_params`) to `nooa_model_client`, and wire Eval Author with default `reasoning_effort="medium"`. - -**Architecture:** Options are resolved once at `resolve_model_clients` and baked into each `CompletionClient`. Eval Author builds options from `EvalAuthorConfig`. Other consumers unchanged when options are omitted. - -**Tech Stack:** Python, dataclasses, Pydantic, pytest, existing Nooa `CompletionClient` kwargs. - -**Spec:** `docs/superpowers/specs/2026-08-17-eval-author-completion-options-design.md` - ---- - -### Task 1: `CompletionClientOptions` + resolve wiring - -**Files:** -- Modify: `packages/nemo_platform_plugin/src/nemo_platform_plugin/nooa_model_client.py` -- Modify/Create: `packages/nemo_platform_plugin/tests/test_nooa_model_client.py` - -- [ ] Add frozen `CompletionClientOptions` -- [ ] Thread `options` through `_completion_client` / `resolve_model_clients` -- [ ] Merge order: base → `completion_params` → explicit `reasoning_effort` if not None -- [ ] Unit tests for omit / medium / params override / effort wins - -### Task 2: Eval Author config + run - -**Files:** -- Modify: `plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/models.py` -- Modify: `plugins/nemo-eval-author/src/nemo_eval_author_plugin/eval_author/run.py` -- Modify: `plugins/nemo-eval-author/tests/test_eval_author_run.py` - -- [ ] `reasoning_effort: str | None = "medium"` -- [ ] `completion_params: dict[str, Any] = Field(default_factory=dict)` -- [ ] Pass `CompletionClientOptions` into `resolve_model_clients` -- [ ] Test default medium + explicit None + params forwarded (mock resolve) - -### Task 3: Verify - -- [ ] `uv run --frozen pytest packages/nemo_platform_plugin/tests/test_nooa_model_client.py plugins/nemo-eval-author/tests/test_eval_author_run.py -q` -- [ ] Carry design doc onto the branch if missing diff --git a/docs/superpowers/specs/2026-08-17-eval-author-completion-options-design.md b/docs/superpowers/specs/2026-08-17-eval-author-completion-options-design.md deleted file mode 100644 index 7d8585d30b..0000000000 --- a/docs/superpowers/specs/2026-08-17-eval-author-completion-options-design.md +++ /dev/null @@ -1,98 +0,0 @@ -# Eval Author completion options - -Date: 2026-08-17 -Status: implemented on branch `eval-author-completion-options/akhoury` - -## Problem - -Eval Author (and other Nooa-backed agents) build `CompletionClient`s through -`nemo_platform_plugin.nooa_model_client` with no way to set `reasoning_effort` or -other LiteLLM/CompletionClient kwargs. On `main`, the field is omitted and the -provider default applies. Smoke Mode 1 evidence shows Author quality is sensitive -to effort; we want Author to default to `medium` without hardcoding it inside -`_completion_client` for every consumer. - -## Goals - -- Let Eval Author specify OpenAI-style `reasoning_effort` (default **`medium`**). -- Let callers pass arbitrary `completion_params` for non-OpenAI backends (or extra - OpenAI knobs) without a second one-off API. -- Keep Analyst and Experimentalist behavior unchanged until they opt in. -- `None` / empty params must match today’s omit-the-field behavior for consumers - that do not set options. - -## Non-goals (v1) - -- Per-component Experimentalist override map. -- Analyst wiring. -- CLI flags (follow-up). -- Provider-specific translation (e.g. Anthropic `thinking` helpers). Unsupported - keys continue to rely on existing `drop_params=True`. - -## Design - -### Shared: `CompletionClientOptions` - -In `packages/nemo_platform_plugin/.../nooa_model_client.py`: - -```python -@dataclass(frozen=True) -class CompletionClientOptions: - reasoning_effort: str | None = None - completion_params: Mapping[str, Any] = field(default_factory=dict) -``` - -`resolve_model_clients(client, refs=None, options: CompletionClientOptions | None = None)` -threads options into `_completion_client`. - -Merge order when constructing `CompletionClient`: - -1. Existing base kwargs (`api_base`, `drop_params`, `_skip_responses_api_bridge`, …). -2. Spread `completion_params`. -3. If `reasoning_effort is not None`, set `reasoning_effort=...` **last** so the - explicit field wins over the same key inside `completion_params`. - -If `options` is `None` or both fields are unset, wire behavior matches `main` -today (no `reasoning_effort` kwarg). - -Apply the same merge for OpenAI- and Anthropic-shaped clients. No format-specific -branching in v1. - -### Eval Author - -`EvalAuthorConfig` gains: - -```python -reasoning_effort: str | None = "medium" -completion_params: dict[str, Any] = Field(default_factory=dict) -``` - -`run_eval_author` builds `CompletionClientOptions` from config and passes it to -`resolve_model_clients`. Mode 1 inherits this when Experimentalist invokes Author: -the runner nested-resolves Author-scoped clients from `config.eval_author` (default -`medium`) so Experimentalist's outer default/fast pair stays unchanged. - -Setting `reasoning_effort=None` in config restores provider-default omit for a -given run. - -### Defaults summary - -| Consumer | v1 default | -| --- | --- | -| `resolve_model_clients` (no options) | omit effort (unchanged) | -| Eval Author config | `reasoning_effort="medium"` | -| Analyst / Experimentalist agents | unchanged (no options passed) | - -## Tests - -- Unit (`test_nooa_model_client`): merge order; `None` omits effort; params - forwarded; explicit effort overrides duplicate key in params. -- Eval Author: default config resolves with `reasoning_effort="medium"`; explicit - `None` omits; `completion_params` reach `resolve_model_clients` (mock). - -## Follow-ups - -- Optional Experimentalist YAML under `eval_author:` for these fields. -- CLI flags for standalone Author runs. -- Run-level + per-role map (design C) for Experimentalist components. -- Anthropic-oriented helpers if silent `drop_params` proves insufficient. From f464765ef08cd5b2e4a6de53bcc34ae8c6ec03db Mon Sep 17 00:00:00 2001 From: Alec Khoury Date: Mon, 17 Aug 2026 13:58:09 -0500 Subject: [PATCH 4/4] test: slim completion-options coverage to the merge and wiring checks Signed-off-by: Alec Khoury --- .../tests/test_nooa_model_client.py | 107 +----------------- .../tests/test_eval_author_run.py | 72 +----------- .../test_dataset_staging_runner.py | 18 +-- .../tests/test_eval_author_config.py | 7 -- 4 files changed, 13 insertions(+), 191 deletions(-) diff --git a/packages/nemo_platform_plugin/tests/test_nooa_model_client.py b/packages/nemo_platform_plugin/tests/test_nooa_model_client.py index 3c643e2b5c..3c2012882c 100644 --- a/packages/nemo_platform_plugin/tests/test_nooa_model_client.py +++ b/packages/nemo_platform_plugin/tests/test_nooa_model_client.py @@ -251,108 +251,13 @@ async def test_resolve_model_clients_closes_constructed_client_after_failure(mon constructed.aclose.assert_awaited_once() -def _openai_entity(name: str = "gpt-4-1") -> SimpleNamespace: - return SimpleNamespace( - workspace="default", - name=name, - backend_format="OPENAI_CHAT", - model_providers=[], - api_endpoint=None, - ) - - -async def test_resolve_model_clients_omits_reasoning_effort_by_default(monkeypatch): - client = MagicMock() - client.models.retrieve = AsyncMock(return_value=_openai_entity()) - client.models.get_model_entity_route_openai_url.return_value = "http://platform/model/gpt-4-1/-/v1" - client.models.get_client_default_headers.return_value = {} - factory = MagicMock(return_value=MagicMock()) - monkeypatch.setattr(nooa_model_client, "CompletionClient", factory) - - await resolve_model_clients( - client, - ConfiguredModelRefs(default="default/gpt-4-1", fast="default/gpt-4-1"), - ) - - assert "reasoning_effort" not in factory.call_args.kwargs - - -async def test_resolve_model_clients_passes_reasoning_effort(monkeypatch): - client = MagicMock() - client.models.retrieve = AsyncMock(return_value=_openai_entity()) - client.models.get_model_entity_route_openai_url.return_value = "http://platform/model/gpt-4-1/-/v1" - client.models.get_client_default_headers.return_value = {} - factory = MagicMock(return_value=MagicMock()) - monkeypatch.setattr(nooa_model_client, "CompletionClient", factory) - - await resolve_model_clients( - client, - ConfiguredModelRefs(default="default/gpt-4-1", fast="default/gpt-4-1"), - CompletionClientOptions(reasoning_effort="medium"), - ) - - assert factory.call_args.kwargs["reasoning_effort"] == "medium" - - -async def test_resolve_model_clients_forwards_completion_params(monkeypatch): - client = MagicMock() - client.models.retrieve = AsyncMock(return_value=_openai_entity()) - client.models.get_model_entity_route_openai_url.return_value = "http://platform/model/gpt-4-1/-/v1" - client.models.get_client_default_headers.return_value = {} - factory = MagicMock(return_value=MagicMock()) - monkeypatch.setattr(nooa_model_client, "CompletionClient", factory) - - await resolve_model_clients( - client, - ConfiguredModelRefs(default="default/gpt-4-1", fast="default/gpt-4-1"), - CompletionClientOptions(completion_params={"temperature": 0.0, "thinking": {"type": "enabled"}}), - ) - - assert factory.call_args.kwargs["temperature"] == 0.0 - assert factory.call_args.kwargs["thinking"] == {"type": "enabled"} - assert "reasoning_effort" not in factory.call_args.kwargs - - -async def test_resolve_model_clients_reasoning_effort_overrides_completion_params(monkeypatch): - client = MagicMock() - client.models.retrieve = AsyncMock(return_value=_openai_entity()) - client.models.get_model_entity_route_openai_url.return_value = "http://platform/model/gpt-4-1/-/v1" - client.models.get_client_default_headers.return_value = {} - factory = MagicMock(return_value=MagicMock()) - monkeypatch.setattr(nooa_model_client, "CompletionClient", factory) - - await resolve_model_clients( - client, - ConfiguredModelRefs(default="default/gpt-4-1", fast="default/gpt-4-1"), +def test_client_option_kwargs_merge_and_omit() -> None: + assert nooa_model_client._client_option_kwargs(None) == {} + assert nooa_model_client._client_option_kwargs(CompletionClientOptions()) == {} + merged = nooa_model_client._client_option_kwargs( CompletionClientOptions( reasoning_effort="medium", completion_params={"reasoning_effort": "minimal", "temperature": 0.2}, - ), - ) - - assert factory.call_args.kwargs["reasoning_effort"] == "medium" - assert factory.call_args.kwargs["temperature"] == 0.2 - - -async def test_resolve_model_clients_applies_options_to_anthropic_clients(monkeypatch): - model_entity = SimpleNamespace( - workspace="default", - name="claude-sonnet-4", - backend_format="ANTHROPIC_MESSAGES", - model_providers=[], - api_endpoint=None, - ) - client = MagicMock() - client.models.retrieve = AsyncMock(return_value=model_entity) - client.models.get_model_entity_route_openai_url.return_value = "http://platform/model/claude-sonnet-4/-/v1" - client.models.get_client_default_headers.return_value = {} - factory = MagicMock(return_value=MagicMock()) - monkeypatch.setattr(nooa_model_client, "CompletionClient", factory) - - await resolve_model_clients( - client, - ConfiguredModelRefs(default="default/claude-sonnet-4", fast="default/claude-sonnet-4"), - CompletionClientOptions(completion_params={"thinking": {"type": "enabled", "budget_tokens": 2048}}), + ) ) - - assert factory.call_args.kwargs["thinking"] == {"type": "enabled", "budget_tokens": 2048} + assert merged == {"reasoning_effort": "medium", "temperature": 0.2} diff --git a/plugins/nemo-eval-author/tests/test_eval_author_run.py b/plugins/nemo-eval-author/tests/test_eval_author_run.py index 28b6c4c2fa..e299010f19 100644 --- a/plugins/nemo-eval-author/tests/test_eval_author_run.py +++ b/plugins/nemo-eval-author/tests/test_eval_author_run.py @@ -226,76 +226,8 @@ async def test_run_eval_author_resolves_inputs_and_returns_datasets( ) assert client.closed assert model_clients.closed - - -def test_eval_author_config_defaults_reasoning_effort_to_medium() -> None: - config = EvalAuthorConfig() - assert config.reasoning_effort == "medium" - assert config.completion_params == {} - - -@pytest.mark.asyncio -async def test_run_eval_author_passes_completion_options_to_resolve( - monkeypatch: pytest.MonkeyPatch, - model_clients: ClosingModelClients, - tmp_path: Path, -) -> None: - client = ClosingClient() - insight = Insight( - workspace="workspace-a", - title="failure", - description="description", - agent="insight-agent", - trace_refs=["trace-1"], - ) - backend = FakeBackend(insight) - dataset_factory = FakeDatasetFactory() - eval_author = FakeEvalAuthor() - monkeypatch.setattr(eval_author_run, "make_client", lambda _: client) - monkeypatch.setattr(eval_author_run, "make_experimentalist_backend", lambda **_: backend) - monkeypatch.setattr(eval_author_run, "DatasetFactory", lambda: dataset_factory) - monkeypatch.setattr(eval_author_run, "build_eval_author_agent", lambda **_: eval_author) - template = tmp_path / "template" - template.mkdir() - (template / "task.toml").write_text("template\n", encoding="utf-8") - train = tmp_path / "train" - validation = tmp_path / "validation" - train.mkdir() - validation.mkdir() - - await eval_author_run.run_eval_author( - insight="insight-123", - train_dataset=DatasetRef(uri=str(train)), - validation_dataset=DatasetRef(uri=str(validation)), - task_template=DatasetRef(uri=str(template)), - experiment_dir=tmp_path / "experiment-default", - workspace="workspace-a", - base_url="http://platform.test", - config=EvalAuthorConfig(), - ) - await eval_author_run.run_eval_author( - insight="insight-123", - train_dataset=DatasetRef(uri=str(train)), - validation_dataset=DatasetRef(uri=str(validation)), - task_template=DatasetRef(uri=str(template)), - experiment_dir=tmp_path / "experiment-omit", - workspace="workspace-a", - base_url="http://platform.test", - config=EvalAuthorConfig( - reasoning_effort=None, - completion_params={"temperature": 0.0}, - ), - ) - - resolve_calls = model_clients.resolve_calls # type: ignore[attr-defined] - assert len(resolve_calls) == 2 - default_options = resolve_calls[0][2] - omit_options = resolve_calls[1][2] - assert isinstance(default_options, eval_author_run.CompletionClientOptions) - assert default_options.reasoning_effort == "medium" - assert dict(default_options.completion_params) == {} - assert omit_options.reasoning_effort is None - assert dict(omit_options.completion_params) == {"temperature": 0.0} + options = model_clients.resolve_calls[0][2] # type: ignore[attr-defined] + assert options.reasoning_effort == "medium" def test_public_apis_accept_train_validation_and_generated_task_inputs() -> None: diff --git a/plugins/nemo-experimentalist/tests/experimentalist/test_dataset_staging_runner.py b/plugins/nemo-experimentalist/tests/experimentalist/test_dataset_staging_runner.py index 54fa8b9d66..79492f1eb5 100644 --- a/plugins/nemo-experimentalist/tests/experimentalist/test_dataset_staging_runner.py +++ b/plugins/nemo-experimentalist/tests/experimentalist/test_dataset_staging_runner.py @@ -38,27 +38,21 @@ def _write_tree(root: Path, content: str) -> None: (tests / "test.sh").write_text(content, encoding="utf-8") -def _stub_author_model_clients(monkeypatch: pytest.MonkeyPatch) -> list[object]: - """Mode 1 now resolves Author-scoped clients; keep staging tests offline.""" - resolve_calls: list[object] = [] +def _stub_author_model_clients(monkeypatch: pytest.MonkeyPatch) -> None: + """Mode 1 resolves Author-scoped clients; keep staging tests offline.""" class _Clients: async def aclose(self) -> None: return None - async def resolve(client: object, refs: object = None, options: object = None) -> _Clients: - resolve_calls.append(options) + async def resolve(*_a: object, **_k: object) -> _Clients: return _Clients() - monkeypatch.setattr( - "nemo_platform_plugin.nooa_model_client.resolve_model_clients", - resolve, - ) + monkeypatch.setattr("nemo_platform_plugin.nooa_model_client.resolve_model_clients", resolve) monkeypatch.setattr( "nemo_platform_plugin.nooa_model_client.get_configured_model_refs", lambda: SimpleNamespace(default="default/m", fast="default/m"), ) - return resolve_calls @pytest.mark.asyncio @@ -118,7 +112,7 @@ async def run( ) monkeypatch.setattr(runner_module, "DatasetFactory", RecordingDatasetFactory) monkeypatch.setattr("nemo_eval_author_plugin.eval_author.agent.EvalAuthor", MutatingEvalAuthor) - resolve_calls = _stub_author_model_clients(monkeypatch) + _stub_author_model_clients(monkeypatch) agent_dir = tmp_path / "agent" agent_dir.mkdir() @@ -153,8 +147,6 @@ async def run( f"runner built datasets without allow_empty in insight mode: {build_options}" ) assert not (template.parent / "generated-task").exists() - assert len(resolve_calls) == 1 - assert getattr(resolve_calls[0], "reasoning_effort", None) == "medium" @pytest.mark.asyncio diff --git a/plugins/nemo-experimentalist/tests/test_eval_author_config.py b/plugins/nemo-experimentalist/tests/test_eval_author_config.py index 38fd51a4a7..da20c32a35 100644 --- a/plugins/nemo-experimentalist/tests/test_eval_author_config.py +++ b/plugins/nemo-experimentalist/tests/test_eval_author_config.py @@ -14,13 +14,6 @@ def test_evolutionary_optimizer_uses_top_level_eval_author_config() -> None: assert type(config) is EvalAuthorConfig assert config.max_validation_repair_attempts == 5 assert config.reasoning_effort == "medium" - assert config.completion_params == {} - - -def test_partial_eval_author_yaml_keeps_medium_reasoning_effort() -> None: - config = EvolutionaryOptimizerConfig.model_validate({"eval_author": {"max_traces": 5}}) - assert config.eval_author.max_traces == 5 - assert config.eval_author.reasoning_effort == "medium" def test_evolutionary_optimizer_tolerates_unknown_eval_author_config() -> None: