One conflict, in StructuredOutputError::repair_message. main (#709)
added a `previous_error` parameter and richer validation-error
rendering; this branch had replaced the inline expectation match with
OutputSchemaKind::expectation().
Resolved by keeping both: main's new signature and section assembly,
calling schema.expectation() for the expectation text. The method
already supersedes main's inline match and carries this branch's intent
of embedding the resolved JSON Schema instead of naming it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replace the two envelope-level tests with focused `ApiUsage` tests that
match the file's existing `token_counts_*` convention.
The streaming and non-streaming tests were the same test paid for twice:
`ApiResponse::usage` and `StreamChunk::usage` are both `Option<ApiUsage>`,
so the envelope cannot change the result. Envelope-level usage decoding is
already covered by `stream_chunk_usage_parsing`.
Also pin the precedence rule this change introduces — nested detail wins
over the flat spelling, and an empty `completion_tokens_details` still
falls back — and document it on `token_counts`. Revert the unrelated
`cost` doc edit that dropped the OpenRouter reference.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both contract helpers dispatched on OutputSchemaKind, so they belong on
the type. Moves expectation() and agent_prompt() into an impl block and
drops the free functions.
Splits the combined agent test: assertions no longer run inside the
backend's run(), where a failure surfaces as a panic from execute().
Adds coverage for the Routing branch of the contract, which was
previously untested.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Resolve the model catalog table conflict in docs/public/core-concepts/models.mdx
by keeping both changes: this branch's `kimi` -> `moonshot` provider rename for
the Kimi rows, and main's new DeepSeek V4 rows.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Addresses Copilot review feedback on the repeated-failure check.
serde_json runs with preserve_order, and jsonschema builds the
additionalProperties `unexpected` list by walking the instance in
document order. So the same leftover keys emitted in a different order
produced a different Vec and compared as a different problem, which
suppressed the "unchanged from your previous repair" nudge.
Sorting at capture also makes the MAX_UNEXPECTED_PROPERTIES truncation
pick the same subset every time instead of an order-dependent one, and
stabilizes the rendered message.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Path safety now lives in one place. The NUL-byte and drive-letter rules
move from a server-only helper into the store's own filename validation,
so uploads reject those paths at write time instead of only the ZIP read
path catching them. The download still re-checks, because artifacts
stored before the rule existed can still carry an unsafe path, but it
now skips a bad path rather than failing the whole archive.
Promote is_boundary_stage to RunProjection and drop the three identical
private copies. The ZIP download used a node-name match instead, which
would have dropped artifacts from a working node that happened to be
named "start".
Compress the archive. Entries were Stored while the response was also
excluded from transfer compression, so text artifacts moved at full
size. async_zip gains the deflate feature; async-compression and flate2
were already in the lock file.
Log archive failures unconditionally. The send-succeeded guard meant a
client that had already disconnected left no record at all, which is the
case where the log is the only evidence.
Also: collapse the duplicate 500 arms, drop the dead stage-ID tiebreaker
and the cached order in the selection map, name the accessible label
after the visible one, share the run URL prefix between the two download
href builders, and document the mid-stream truncation behavior in the
OpenAPI description.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three places translated a provider error code into a ProviderErrorKind:
error_from_status_code for HTTP error bodies, and a private table in each
of the openai_responses and anthropic_messages stream decoders. The tables
disagreed, so the same failure classified differently depending on which
path saw it.
Most visibly, OpenAI returns HTTP 429 with error.type "insufficient_quota"
when an account is out of credit. The streaming decoder mapped that to
QuotaExceeded, but the non-streaming path fell through to the plain
429 => RateLimit arm, so a spent quota was retried with backoff and never
triggered failover.
Move the code table into error.rs as kind_from_error_code, returning None
when the code says nothing so each caller keeps its own default. All three
call sites now share it.
In error_from_status_code, unambiguous statuses (401, 403, 404, 408, 413,
5xx) still win outright. A 429 defers to the code only when it reports a
spent quota. Ambiguous statuses (400, 422, ...) prefer the structured code
over the existing message-substring guessing, which now runs only when
there is no code.
Two classifications improve as a side effect of merging the tables:
not_found_error now maps to NotFound rather than Server for openai, and
request_too_large maps to ContextLength rather than InvalidRequest.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up review of the repair-error work. Behavior is the same or better;
the machinery is smaller.
Fixes a false "unchanged from your previous repair" nudge. same_problem_as
fell through to `_ => true`, so any two non-Required issues at the same
instance path, schema path and keyword compared equal. A model that removed
one unexpected property and added another was told it had changed nothing.
SchemaValidationIssue already derives PartialEq, so the 17-line comparison
is now `previous.contains(issue)`.
Drops the hand-written Type and Enum rendering. jsonschema already renders
both, and its messages name the offending value, which the hand-written
ones did not. Also switches masked() back to to_string(): masking replaced
the bad value with a placeholder, working against the goal of an actionable
message, and buys no privacy since the full response is already in the
prompt.
Resolves the schema fragment when the issue is captured rather than
threading Option<&OutputSchemaKind> through rendering. That reverts the
command.rs change and drops the test-only messages() shim. The fragment is
now attached only to Other, where it adds information; for required, type,
enum and additionalProperties it just repeated the prose.
Also: caps the model-controlled unexpected-property list so a wide object
cannot turn the repair prompt into megabytes; drops evaluation_path, which
was dead except under $ref, where it printed a pointer that does not
resolve; drops the keyword field, already named by the schema path; and
records the previous error only after the agent session accepted the
repair, since failover rebuilds the session from the original prompt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The parallel stage summary rendered a Duration tile directly below
StageMetaBar, which already shows the same stage's duration with a live
ticking clock and a started-at tooltip. The two disagreed while running:
the meta bar counted up, the tile showed the static word "running". The
cancelled-stage bug lived only in the duplicate.
Drop the tile. The meta bar owns duration for every stage renderer, and
it was already correct for cancelled, pending and skipped stages. That
removes the three-way duration branch, the "--" sentinel decode, and the
ACTIVE_STAGE_STATES and formatDurationMs imports.
With the tile gone, ParallelOverview.durationMs is dead, as were
successCount, failureCount and isComplete — the renderer counts the
branch rows it draws. ParallelOverview reduces to branch identity.
For run event write failures, log the first at error with run_id and
event name, the rest at debug, and summarize new losses at flush. A
broken sink fails for every event, so a bare error would emit one
"investigate me" line per event for the life of the run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Return String from the sanitize helpers instead of Cow: every call
site feeds the result into json!, which allocates anyway, so the
borrowed fast path only cost extra branches and Cow-variant tests.
- Route all toolUse/toolResult construction through private
tool_use_block/tool_result_block constructors that own the sanitize
calls, so the toolUse/toolResult pairing invariant is enforced by
construction rather than by call-site discipline.
- Drop a test assertion the type system already guarantees (encoding
takes &Request, so it cannot mutate the input) and assert wiring
tests against the sanitize helpers instead of re-pinning the exact
replacement literals in a second file.
No wire-format changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A fork carries a checkpoint from its source run, but its first
run.created event contains only a sandbox plan. Resume previously tried
to reconnect that planned sandbox and failed because no instance
exists. Now a fork resume with a Planned sandbox record builds a fresh
sandbox instead; later fork resumes still reconnect the ready instance,
and a same-run resume with an uninitialized sandbox still fails the
precondition check.
Also consolidates the test module's three near-identical InitOptions
literals into a shared test_init_options helper.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot review flagged two backward-compatibility breaks with events
persisted by pre-model-keyed releases; both are stored data that can
never be rewritten, so accept the old shapes on read:
- FailoverProps: original_provider/original_model/attempt are Option
again with serde defaults. New events always set them; failover events
recorded before model-keyed fallbacks lack them. Restores the
historical-event test.
- RunModelSettings: temporary custom deserializer accepts the legacy
flat-array fallbacks shape inside stored run.created events, keying
the chain under the requested model name when one is set. Remove once
pre-0.311 run logs are out of the support window.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot review flagged that a failover event's from route can be a
candidate that failed during activation and never served traffic. That
is intentional — events chain (one event's to is the next one's from)
so the stream records every candidate tried, with the error explaining
why each was abandoned. Document it at the emit site.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Consolidation pass over the fallback feature, no intended behavior
changes beyond noted validation and event-shape cleanups:
- Unify the two parallel notice types: FallbackPlanNotice is gone;
ModelFallbackNotice now owns the runtime NoNearbyReasoningLevel case
and the shared ChainEmpty wording. Notices emit through a new
Emitter::notice_scoped with their own level, and each distinct notice
is emitted once per run instead of on every LLM call.
- Move canonical_model_id onto Catalog so chain keys are written and
read through one function; reject provider-qualified fallback keys,
which could never match at dispatch and were silently dead config.
- Type FallbackTarget as ProviderId/ModelId, removing repeated
ProviderId::new re-wrapping at every use site.
- Derive FallbackPlan's current route from a position index instead of
storing current/requested_controls copies; advance() no longer has
unreachable None branches.
- Bundle the agent invocation's live state (session, bridge, lease,
forwarder, accounting) into LiveAgentInvocation; failover_agent_session
drops from 21 parameters to 7 and the six copies of the
abort/discard/classify teardown collapse into two methods.
- Share one route_request builder between one_shot and its failover
loop; complete_one_shot_request takes the request by value instead of
deep-cloning the message payload per call.
- Event::Failover carries FailoverProps directly; the props' original
route and attempt fields are now required, and reasoning efforts are
typed ReasoningEffort instead of strings.
- Reuse RunModelSettings/RunModelControls in fabro-api via
with_replacement, add the missing controls property to the OpenAPI
schema, regenerate the TS client, and add the type-identity/JSON
parity test.
- Smaller cleanups: ReasoningEffort::closest_supported uses enum
discriminants; ModelFallbackPolicy gains len(); resolve_model_fallbacks
takes a provider slice; duplicate-target filtering lives only in the
resolver.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Addresses review feedback on the fallback notice work.
A provider-only fallback such as `openrouter` needs the primary model's
catalog entry to find the closest capability match. When the primary is
itself a passthrough selector there is no entry, so every provider-only
candidate was skipped with "provider `X` has no compatible model" even
when that provider had plenty. Adds a `PrimaryNotInCatalog` notice that
names the missing primary instead of blaming the provider.
Also from review:
- `code()` was a wildcard fallthrough, which docs/internal/events-strategy.md
forbids for new variants. Now exhaustive.
- `NoConfiguredOffering` discarded the `providers` list that
`NoEligibleOffering` hands it. The notice now names the providers that do
offer the model.
- `ModelFallbackNotice::reference` was a rendered `String`; it is now the
`ModelRef` it came from, which also drops the per-candidate double
allocation the previous refactor introduced.
- `ResolvedStartLlm` unpacked and repacked `ResolvedFallbackChain`
field-for-field; it now holds it directly.
- Added `FallbackTarget: Display` as `provider:model`, replacing two
hand-written `"{}:{}"` format strings.
- `Catalog::select` still inlined the `require_provider` body.
- Emission moved to `ModelFallbackNotice::emit_all`, covered by a new test
proving notices reach the event stream with the right level, code, and
message. Nothing tested that hand-off before.
Documented in `resolve_fallback_chain` why an unknown provider stays a hard
error while an unconfigured one is skipped, and that an unqualified unknown
selector pins to the primary's provider.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>