Skip to content

fix(runner): fail loud on an unresolvable requested model (F-007) - #5240

Merged
mmabrouk merged 2 commits into
big-agentsfrom
fix/strict-model-resolution
Jul 12, 2026
Merged

fix(runner): fail loud on an unresolvable requested model (F-007)#5240
mmabrouk merged 2 commits into
big-agentsfrom
fix/strict-model-resolution

Conversation

@mmabrouk

Copy link
Copy Markdown
Member

Context

A user picks a model in the agent playground and silently gets a different one, often the pricier default. On the sandbox-agent (ACP) path, a per-request model override was strict only for the Claude managed-connection case. Every other path (all Pi runs, and Claude without a custom base URL) took the lenient branch in applyModel: when the harness rejected the requested id, the runner logged one line and kept the harness default. The run always succeeded, so the drop was invisible. For Claude that default is Sonnet, so a rejected id billed the expensive model. This is finding F-007 (docs/design/agent-workflows/projects/qa/findings.md).

Two symptoms:

  • Pi could not select a non-default model at all when the requested id needed suffix resolution under strict mode (strict threw before the suffix retry ran).
  • Every harness silently fell back instead of failing, so a wrong or unreachable id produced a different model with no error.

What changed

Strict resolution is now the default on every harness path. A run that requests a model either runs that model or fails loudly. This is Part 2 of the model-config proposal (docs/design/agent-workflows/projects/model-config/proposal.md), aligned with the harness-filtered picker (agent-model-picker, PR #4815 lineage): the picker only offers harness-valid ids, and the runner is the fail-loud backstop.

  • applyModel (services/runner/src/engines/sandbox_agent/model.ts): tries the requested id, then resolves it against the harness's own selectable ids via pickModel (so a bare gpt-5.5 reaches Pi's openai-codex/gpt-5.5), and only then either returns the resolved id or throws ModelNotSettableError naming the requested id and the valid options. Suffix resolution now runs before the failure, which is what makes Pi selection work. strict defaults to true.
  • allowedModels (same file): read the pi-acp choice value (it read id and silently returned []), so the error message and any pre-validation see the real selectable set.
  • sandbox_agent.ts: strictModel is decoupled from the Claude-only applyClaudeConnectionEnv and computed by modelResolutionStrict(), strict by default. applyClaudeConnectionEnv keeps its Claude env side-effect and returns void.

Before / after

  • Before: model: "claude-haiku-4-5-20251001" on a Claude run -> rejected -> silent fallback to Sonnet, billed silently. model: "gpt-5.5" on Pi -> dropped, ran the default.
  • After: model: "gpt-5.5" on Pi -> resolves to the harness id and runs. An unresolvable id -> the run fails with model '<id>' is not available on this run (...). Valid models for this harness: <list>.

Scope / risk

  • Runner-only, no wire/SDK/FE change. No protocol.ts change.
  • A run that requests no model is unchanged: it keeps the harness default.
  • The legacy warn-and-fallback is preserved behind AGENTA_AGENT_MODEL_STRICT=false for operators who need it.
  • Behavior change: a requested-but-unresolvable model now fails the run instead of silently substituting the default. The harness-filtered picker only surfaces valid ids, so normal playground use resolves; the failure is the intended F-007 outcome.

How to QA

Prerequisites: the runner unit suite (services/runner, Node 24), and optionally a live EE dev stack with a Pi harness.

Unit:

  1. cd services/runner && pnpm exec vitest run tests/unit/sandbox-agent-model.test.ts
  2. Expected: 10/10 pass, covering: requested id accepted; no model requested -> default; bare Pi id resolves to the harness id under strict; unresolvable id -> ModelNotSettableError naming the id and options; strict: false -> warn-and-fallback; allowedModels reads value.

Live (Pi run):

  1. Run a Pi harness run with an explicitly requested valid model -> the run uses that model.
  2. Run one with a bogus model id -> the run fails with the naming message above, no silent fallback.

Edge cases: AGENTA_AGENT_MODEL_STRICT=false restores the old fallback; a Claude alias (haiku) still resolves; a full Claude model id that no alias matches now fails loudly instead of billing Sonnet.

Links finding F-007 (docs/design/agent-workflows/projects/qa/findings.md, findings-status.md).

https://claude.ai/code/session_018MaXPNpvzN22kngHno3VMj

A per-request model override on the sandbox-agent path was strict only for the
Claude managed-connection case. Every other path (all Pi runs) silently fell
back to the harness default when the requested id was rejected, so a user who
picked a model could get a different, often pricier one with no error.

Make strict resolution the default on every harness path: applyModel now tries
the requested id, then resolves it against the harness's own selectable ids
(so a bare "gpt-5.5" reaches Pi's "openai-codex/gpt-5.5"), and only then either
returns the resolved id or throws ModelNotSettableError naming the requested id
and the valid options. A run that requests no model still keeps the harness
default. The legacy warn-and-fallback stays available behind
AGENTA_AGENT_MODEL_STRICT=false. Also fixes allowedModels to read the pi-acp
choice `value` (it read `id` and silently returned []).

Claude-Session: https://claude.ai/code/session_018MaXPNpvzN22kngHno3VMj
@vercel

vercel Bot commented Jul 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview, Comment Jul 12, 2026 8:39pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 38e92729-4a21-409f-bbed-9cab9927a0fe

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Model selection now resolves harness-supported ids, throws structured errors in strict mode, and falls back only when explicitly non-strict. Environment acquisition derives strictness independently from Claude connection setup.

Changes

Model resolution behavior

Layer / File(s) Summary
Model selection contract and validation
services/runner/src/engines/sandbox_agent/model.ts, services/runner/tests/unit/sandbox-agent-model.test.ts
Adds ModelNotSettableError, reads option value fields, defaults to strict mode, retries resolved ids, and tests strict failure and explicit non-strict fallback.
Independent strictness wiring
services/runner/src/engines/sandbox_agent.ts
Separates Claude environment setup from strictness calculation and derives strictness from AGENTA_AGENT_MODEL_STRICT.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Runner
  participant applyModel
  participant Harness
  Runner->>applyModel: apply requested model with strictness
  applyModel->>Harness: set requested model
  Harness-->>applyModel: reject or accept model
  applyModel->>Harness: retry resolved selectable model
  applyModel-->>Runner: return model or throw ModelNotSettableError
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: fail loudly when a requested model cannot be resolved.
Description check ✅ Passed The description is detailed and directly matches the model-resolution changes in the pull request.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/strict-model-resolution

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. bug Something isn't working labels Jul 11, 2026
options: { strict?: boolean } = {},
): Promise<string | undefined> {
if (!wanted) return undefined;
const strict = options.strict ?? true;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decision: strict defaults to true on every harness path (F-007). This flips the model-config proposal's staged default (Part 2b): a requested model that cannot be set fails the run instead of silently substituting the harness default. No model requested is unaffected (early return above).

// pi-acp builds each choice as `{ value: model.modelId, name, description }` and sandbox-agent
// reads `entry.value`; older shapes used `id`. Read `value` first so this returns the real
// selectable ids (reading only `id` silently returned [] for pi-acp).
return choices.map((c: any) => c.value ?? c.id).filter(Boolean);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug fix: pi-acp builds each choice as { value: model.modelId, ... }, so mapping c.id returned []. Reading value first surfaces the real selectable set, so the ModelNotSettableError message can name valid options (the 'valid options source').

* no model is unaffected either way — it keeps the harness default.
*/
function modelResolutionStrict(): boolean {
return process.env.AGENTA_AGENT_MODEL_STRICT !== "false";

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decision: the fallback is kept behind an explicit env flag (default strict). AGENTA_AGENT_MODEL_STRICT=false restores the legacy warn-and-fallback. Env flag (not a wire field) keeps the /run contract and goldens unchanged; strictModel is now computed here instead of piggybacking on the Claude-only applyClaudeConnectionEnv return value.

}
}
if (strict) {
throw new ModelNotSettableError(wanted, fallbackAllowed, (err as Error).message);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This throw is reached only AFTER the suffix-resolution retry above fails. That ordering is the Pi selection fix: a valid bare id (gpt-5.5) resolves to the harness id and never reaches here; only a genuinely unresolvable id does. The message names the requested id and the valid options, so the failure is actionable instead of a silent fallback.

@mmabrouk

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
services/runner/src/engines/sandbox_agent/model.ts (1)

9-24: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider setting Error.cause for proper error chaining.

The cause parameter is only interpolated into the message string but not passed to super() as { cause }. Setting the standard cause option preserves the original error for stack traces and programmatic inspection by downstream handlers.

♻️ Proposed improvement
   super(
     `model '${requested}' is not available on this run (${cause}). ` +
       `Valid models for this harness: ${options}.`,
-  );
+  , { cause });

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 308fabce-1fe5-400e-bffe-2891141f84cd

📥 Commits

Reviewing files that changed from the base of the PR and between b00a00c and 4202bfd.

📒 Files selected for processing (3)
  • services/runner/src/engines/sandbox_agent.ts
  • services/runner/src/engines/sandbox_agent/model.ts
  • services/runner/tests/unit/sandbox-agent-model.test.ts

@mmabrouk mmabrouk left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@mmabrouk mmabrouk added the lgtm This PR has been approved by a maintainer label Jul 12, 2026
Adds a third pickModel tier that strips a trailing context-window hint so a bare
alias (sonnet, fable) resolves to the harness's reported id (sonnet[1m],
fable[1m]) when that is the only variant on offer. Only widens to an
equal-or-larger context; never downgrades a [1m] request. This is what makes
the sonnet/fable aliases actually settle instead of failing loud under strict
resolution. Also folds in a prettier reformat of daytona.ts (no logic change).

Claude-Session: https://claude.ai/code/session_0127AM79khCdvD2b8BG2joZL
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:M This PR changes 30-99 lines, ignoring generated files. labels Jul 12, 2026
@mmabrouk
mmabrouk merged commit d6339a2 into big-agents Jul 12, 2026
11 of 12 checks passed
@mmabrouk
mmabrouk deleted the fix/strict-model-resolution branch July 12, 2026 20:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working lgtm This PR has been approved by a maintainer size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant