fix(sdk): default Claude steps to Opus 5 - #418
Conversation
Session-Id: 01a09c40-ce3b-7f11-a7df-b6b7ccab6fd9
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 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. Comment |
Session-Id: 01a09c40-ce3b-7f11-a7df-b6b7ccab6fd9
Review swarm: maintainabilityMaintainability Review: PR #418 — Default Claude steps to Opus 5Reviewer: maintainability-418 Lens: Could a stranger read this in six months and change it safely?This review examines PR #418 through the maintainability lens only. The question: if someone unfamiliar with this code needs to modify the model resolution logic in six months, will the code guide them correctly? SummaryPASS with critical observations. The implementation introduces a model resolution priority system (step → named agent → adapter default) with budget enforcement. The code is maintainable overall but has one critical implicit contract and one missing failure boundary that would mislead future maintainers. Critical FindingsC1: Implicit sequencing contract between CLI resolution and model resolutionLocation: Issue: The code enforces a strict sequencing dependency that is not documented or enforced by types: // L263-276: CLI resolution accumulates diagnostics into cliResolutionDiagnostics
for (const step of compiled.steps) {
const resolution = resolveCli(step, compiled, options.projectCli);
if (resolution === undefined) {
cliResolutionDiagnostics.push({ ... });
} else {
resolutions.push(resolution);
resolutionByStep.set(step.id, resolution);
}
}
// L277-290: Return early if CLI resolution failed, BEFORE model/budget checks
diagnostics.push(...cliResolutionDiagnostics);
if (cliResolutionDiagnostics.length > 0) {
return { ok: false, gates: ..., resolutions, diagnostics };
}
// Only now run model and budget diagnostics
diagnostics.push(...unknownModelDiagnostics(compiled, options, resolutionByStep));
diagnostics.push(...budgetDiagnostics(compiled, new Map(...)));Why this is a maintainability hazard:
What future maintainers need to know:
Recommendation: Add a comment at the early return explaining the dependency: // Early return is required: unknownModelDiagnostics and budgetDiagnostics
// both depend on resolutionByStep being complete. A partial map would cause
// model checks to incorrectly fall back to step.model and skip adapter defaults.
if (cliResolutionDiagnostics.length > 0) {
return { ok: false, gates: ..., resolutions, diagnostics };
}C2: Missing failure path for adapter default with frozen budget but no priceLocation: Issue: The code refuses an unpriced adapter default under a frozen dollar budget: else if (resolved?.source === 'adapter'
&& !Object.hasOwn(MODEL_PRICING, model)) diagnostics.push({
severity: 'refusal', kind: 'budget_missing_price', stepId: step.id, model,
message: `Model "${model}" has no frozen price for budget accounting.`,
});The hidden assumption: This path assumes the adapter's What happens when the assumption breaks:
Why this is maintainable but fragile:
What makes this safe today:
What would make this safer for six months from now:
Design ObservationsD1: Model resolution priority is clear but has one naming confusionLocation: What's good:
Naming confusion: export function resolveCliModel(executable: string, model?: string): string | undefined {
return resolveCliModelSelection(executable, { step: model }).model;
}The parameter name Fix: Rename the parameter to match its meaning: export function resolveCliModel(executable: string, stepModel?: string): string | undefined {
return resolveCliModelSelection(executable, { step: stepModel }).model;
}This is a minor issue — the function is only called from controlled sites (llm-worker.ts:196, worker.ts:442, worker-cli.ts:387) — but the generic name invites misuse in future call sites. D2: The adapter registry is global mutable state with no guard against partial initializationLocation: Current reality: // Implicit contract: registeredAdapters() is called AFTER all adapters are registered
const model = registeredAdapters()[resolveAdapterKind(executable)].defaultModel;The contract that isn't written:
What happens if the assumption breaks:
Why this is maintainable:
What would make this more obvious to maintainers:
D3: Budget check sequencing is clearLocation: The budget check runs in the right order:
The logic matches the resolution priority (step → named → adapter) and the early returns prevent double-checking. This is readable. One clarity point: The variable name const resolution = resolvedModels.get(step.id);
const model = resolution?.model ?? step.model ?? ...Test CoverageWhat the tests verify:
What the tests do NOT verify:
These gaps are not defects — the uncovered paths are integration-level contract violations rather than code paths — but they mean the implicit contracts (C1, C2) rely on code review and discipline rather than automated verification. Comments that Assert What the Code Does Not DoNone found.All comments describe what happens or why, not what doesn't happen. The doc comments accurately reflect behavior. Tests That Would Not Fail If Behavior BrokeT1: Adapter default priority is tested but not pinned against CLI outputLocation: budget-preflight.test.ts:490-502 The test verifies preflight resolves the adapter default and probes it: expect(result.resolutions).toContainEqual(expect.objectContaining({
stepId: 'ask', cli: 'claude', model: 'claude-opus-5', modelSource: 'adapter',
}));
expect(o.probes.cli).toHaveBeenCalledWith('claude', 'step', 'claude-opus-5');What it doesn't verify: That the worker actually passes But flow-executor-chain.test.ts:602-638 and worker-cli.test.ts:820-840 do verify this — they capture the real argv and assert on T2: Budget refusal for unpriced adapter default is validated but assumes pricing table is in syncLocation: budget-preflight.test.ts:509-517 The test manually constructs a resolution with expect(budgetDiagnostics({...input, steps:[step]}, new Map([
['ask', { model: 'future-default', source: 'adapter' as const }],
]))).toEqual([expect.objectContaining({
kind: 'budget_missing_price', model: 'future-default',
})]);What it doesn't verify: That if Why this is acceptable: The test is explicitly exercising the budget check logic, not the adapter registration logic. The integration path (adapter default → preflight → budget check) is validated by the "runs a dollar-budgeted authored Claude agent" test at flow-executor-chain.test.ts:602-638, which uses a real flow config and would fail if the default were unpriced. So again, the gap is covered at integration level. The unit test's manual construction is appropriate for isolating the budget check logic. VerdictPASS — The code is maintainable with named caveats. What makes it maintainable:
What requires care when changing:
Recommended additions for long-term maintainability:
None of these are blocking defects. The code works correctly and the implicit contracts are validated by tests, even if not explicitly documented. A maintainer who reads the code carefully will understand it. A maintainer in a hurry might remove the early return and create a silent bug. Specific Maintainability QuestionsCould a stranger locate the model resolution logic? Could they understand the priority order? Could they safely add a new adapter with a default model? Could they safely change the priority order? Would they know to update MODEL_PRICING when adding a default? Could they debug a "model has no frozen price" error? ConclusionThe PR is maintainable. The design is sound, the priority logic is clear, and the tests validate correctness. The implicit contracts (C1, C2) are the main hazard: a maintainer could violate them without realizing, and the code would compile and pass partial tests. The fix is documentation, not code changes. Adding three comments (at the early return, at adapter registration, and at budget check) would make the implicit contracts explicit and prevent future corruption. REVIEW_PASSED |
Review swarm: historyNo fresh transcript was produced for run |
Review swarm: structureNo fresh transcript was produced for run |
Review swarm: FAILED
Cloud run: |
Session-Id: 01a09c40-ce3b-7f11-a7df-b6b7ccab6fd9
A dollar-budgeted authored step such as
f.agent("implementer", { cli: "claude", task })was refused whenmodelwas omitted, even though Claude is a recognized adapter with a stable priced model available.This gives the Claude adapter an explicit
claude-opus-5default and resolves the model once in step → named agent → adapter priority order. The same resolved model now drives project allowlist enforcement, frozen-dollar budget preflight, readiness probing, checked kernel input, provider invocation, and usage accounting. Explicit models still win. Codex and unknown/custom CLIs retain the existing fail-closed behavior when no model is declared.The frozen standard Opus 5 rate is $5/M input and $25/M output, matching Anthropic's model documentation: https://platform.claude.com/docs/en/models/opus-5/whats-new-opus-5
The revision also addresses the first review swarm's findings: unresolved CLIs now stop before model, budget, or environment probes; model provenance is centralized; the adapter boundary and priority are documented; and regression coverage includes registered-no-default, custom, unpriced-default, token-budget, named-agent precedence, allowlist, exact argv, and daemon-backed authored execution paths. The schema parity fixture keeps its additional model authority local instead of changing shared testdata.
Validation:
npm run typechecknpm run typecheck:testsnpm run buildclaude-opus-5readiness round trip: exit 0, expected token returnedThe previous broad SDK run had 1,627 passing tests; its remaining local failures were existing tests that hard-code
kernel/target/*whileops/cargo.shdeliberately builds outside the worktree. The hosted Linux artifact workflow suppliesRELAYFLOWD_BINand is the authoritative full-suite gate.Note
Medium Risk
Changes model selection, budget preflight, and worker CLI invocation paths; mis-resolution could affect provider calls and dollar accounting, though explicit step/named models still win.
Overview
Introduces per-CLI adapter default models and a single resolution order (step → named agent → adapter default) used by
flows check, preflight, and workers. The Claude adapter now declaresclaude-opus-5as its default, with frozen pricing added so dollar budgets can accept Claude steps that omit an explicitmodel.Preflight records
modelSourceon CLI resolutions, runs CLI resolution refusals before model allowlist/budget checks, and treats adapter-chosen models like explicit ones for probing, registry enforcement, and frozen-budget pricing (Codex/custom CLIs still fail closed with no default).flows checkmaterializes adapter defaults onto the checked step spec;LlmWorker,AgentWorker, andrunAgentClicallresolveCliModelso execution and spend accounting match preflight.Docs and JSON Schema copy are updated to describe independent CLI vs model priority and the new default behavior.
Reviewed by Cursor Bugbot for commit c77be19. Bugbot is set up for automated code reviews on this repo. Configure here.