v0.5.0 — 2026-09-07
The reliability release. Everything below came out of running upshift against real
migration incidents (the private rescue campaign: 109 OpenAI-track and 54 Anthropic-track
cases) and fixing what broke, with a regression test per finding. Headline changes:
- Verification scope is explicit everywhere (
request_contract/adapted_agent/
native_application); no output claims application-level verification unless the
application ran. - Native runner: an
agent.jsonrunnerblock lets the application run itself (Python and
Node reference runners, protocol v1, authorization gate, minimal environment). upshift verify-patch: the exact exported patch is applied to a clean copy and every
case's first request is rebuilt and compared with the run that verified it.- Honest verdicts:
INCONCLUSIVEwith reason codes; collateral protection measured and
reported (never assumed); fresh final verification separate from candidate selection;
evidence identity that refuses stale reuse; SAFE wording says what was measured. - Endpoint translation is a tested table: reasoning, output caps, tool choice, sampling,
seed, state-linking,response_format; every drop is recorded; local SDK failures are
sdk_validation, never a manufactured HTTP status; repairs that change a capability or
cost carry a disclosure and rank last. - Repair loop: every sibling candidate is screened before one is accepted; adjudication
cannot be skipped; transient provider errors are retried and never counted as behaviour. - Capture: continuation policy, machine-readable unsupported-fields findings.
- Docs: the real data flow (what reaches the provider, what
adaptsends to the
extraction model), a capabilities table with three evidence levels, a provider matrix
with unverified rows marked.
Note: --budget (max repair candidates) now defaults to 24 because sibling screening
counts every candidate screened; use --max-cost-usd to bound spend.
Reliability follow-ups
Five findings from the OpenAI and Anthropic rescue tracks, each of which produced a wrong or
unsupported ANSWER rather than a crash. Sources are the private ops repo's case files.
- The repair loop screens every sibling candidate before it accepts one (
ghisdk-127,
ghc-062). Greedy acceptance let the playbook's emission order decide the verdict: waku
accepted a candidate that restored 8 of 9 cases and published STAY PINNED while a sibling in
the same generation restored all nine, and crispen shipped a repair with a disclosed change
of guarantee ahead of the maintainer's own equivalent fix. Restorers are now ranked by full
restoration, then cases restored, then the absence of a disclosure, then the playbook rank,
and verified in that order. Screening costs one screen run per sibling and--budgetstill
bounds the candidates TRIED, so its default is 24 rather than 6. - A candidate whose adjudication cannot run is rejected, not accepted (
ghisdk-052). When
the cost ceiling stops the N reps that settle a contested case, the candidate is rejected,
the suspects are recorded asadjudication_skipped, and the verdict is
INCONCLUSIVE(cost_ceiling) — never SAFE WITH PATCH on the strength of the sample that raised
the suspicion. - Transient provider failures are retried, and never recorded as behaviour (
ghi56-019,
ghi56-006,ghi56-021). A flex capacity 429 was written down three times as a permanently
failing rep, moving the pass rate the verdict is computed from. 429/5xx/timeouts/connection
errors are now retried per rep (3 attempts, jittered exponential backoff, ≤90s) outside the
SDK's own retries; an unrecovered one is recorded astransient_provider_error, which the
differ files underharness_errorand the verdict treats as INCONCLUSIVE. Billing still
aborts, auth is not retried, and a 400 is never retried or reclassified. New
--retry-erroredonrun/upgradere-runs, on a resume, exactly the reps whose
recorded error was non-behavioural. response_formatis carried, not deleted (ghisdk-051). adapt used to drop it, which
removed the entire subject of a structured-output incident and produced an agent that asked
for no structured output at all. It is now a param, translated per endpoint: verbatim on
chat/completions,text.format(with the json_schema object flattened) on/v1/responses,
dropped with a recorded reason on Anthropic's messages. A nullable tool parameter also keeps
its null branch (ghisdk-052): every spelling is canonicalised toanyOfwith null.- A disclosed strict-schema repair (
ghisdk-051,p2-001). New signature
api_error_schema_invalidand new candidateschema-strict-compat:<schema>, applying
OpenAI's three documented strict rules to one named schema. Disclosed: optional fields become
required-and-nullable, so downstream code must accept null.
Capture fixes (previously v0.4.1-dev)
Three fixes found by running capture mode on its first real case outside pydantic-ai
(rescue-ops cases/A-075, litellm 1.83.9). The first is a false pass, in the one direction
this product must never fail.
- A capture-derived agent now replays the framework's per-turn shape.
adapt --from-capturewrote ONE episode-leveltool_choice, picked by frequency over the recorded
requests. A framework that forces a tool on turn 1 and then goesauto— pydantic-ai and
litellm both do, and so does every structured-output and routing agent — became an adapter
that forced a tool on every turn. Under a forced choice the model cannot answer in text,
so the replayed episode called tools untilmax_turnsand failed its ownturns_at_most
on the baseline model; with nothing passing on the baseline there was no regression to
find, and the verdict readSAFE, "the candidate model is a drop-in replacement", over a
suite that never worked once.agent.jsonturn_params(ADAPTER.md): param overrides applied overparamsby
assistant-turn index, the last entry repeating,nullmeaning the field was not sent on
that turn.agent_loopapplies turn N's overrides to turn N's request; the repair loop
reads the sequence too, so removing a forcedtool_choiceremoves it from every entry
instead of finding nothing to repair.adapt --from-capturederives it for exactly the params one recorded conversation
sent differently across its turns, and leaves those out ofparams. Captures that
disagree with each other in a way no sequence can express are not resolved quietly:
every variant and its count land inADAPT_EDITS.mdunder CONFLICTS, and
adapt --from-capture --strictexits non-zero. A tie is broken by canonical text and
reported — never by which request the recorder happened to see first, which is what
Counter.most_commonwas doing.
BASELINE_BROKEN— the guard that catches this independently of capture. A run whose
baseline model passed no case measured nothing about the candidate and can never be
SAFE. Checked before every other verdict, and unable to mask one (a regression needs a
case the baseline passed).upshift upgradealso says it between the two legs, before the
candidate run spends money on a comparison that cannot mean anything.- Captures, run records and adapt records are stamped with the version that actually ran.
upshift.__version__was a hand-maintained literal three releases behindpyproject.toml,
so every one of them claimed0.1.0whileupshift --versionreported the truth. It now
reads the installed package metadata, the waycli._version()always did. Provenance only:
no measurement changes, and the 169 already-committed lab records carry the stale string. - Tool fields the adapter cannot carry are reported, not dropped in silence. The
chat-style shape holdsname/description/input_schema; everything else a recorded tool
carried is now a per-tool note inADAPT_EDITS.md, louder for a server tool — a
computer_20241022with noinput_schemaat all was becoming a plain custom tool with an
empty schema, with nothing anywhere saying so. Structural deviation 2 stops claiming
byte-identity and names the two fields dropped on purpose.
Also unreleased, from the OpenAI migration-rescue track (product gaps the lab hit while
running 54 cases):
-
Endpoint routing translates the output-token cap.
/v1/responsesspells it
max_output_tokens; an agent written against/v1/chat/completionscarriesmax_tokens
(classic families) ormax_completion_tokens(gpt-5*/o-series), and passing either to the
Responses SDK raisesTypeError: Responses.create() got an unexpected keyword argument 'max_completion_tokens'before a request is sent — crashing the whole run rather than
recording a failed rep. Since endpoint routing is the documented repair for the gpt-5.5+ /
gpt-5.6 "function tools ... in /v1/chat/completions" 400, the untranslated cap made that
repair unusable for any agent that sets one.map_paramsnow maps both spellings to
max_output_tokensonresponses; an explicitly-spelledmax_output_tokenswins. -
Pricing for the rest of the gpt-5.6 family.
upshift costreported "unknown rate" for
gpt-5.6-terra and gpt-5.6-luna. Standard-tier rates per 1M tokens, from
https://developers.openai.com/api/docs/pricing (fetched 2026-09-03): sol $4.00 in /
$0.40 cached / $20.00 out (unchanged), terra $2.00 / $0.20 / $12.00, luna $0.20 / $0.02 /
$1.20. The published flex and batch rows are exactly half of standard and the cached rows
exactly 10% of input, which is what the existing tier and cache multipliers already do. -
upshift runandupshift upgradetake--max-cost-usd. Onlyadapt, which makes one
paid call, had a spend ceiling;runandupgrademake thousands — baseline reps,
candidate reps, then a screen and a full-suite verify per repair candidate — and a lab
overran a $5 per-case cap inside a singleupgrade. The ceiling is priced, not estimated:
it sums the recorded token usage under this run id (forupgrade, the whole--tag
family) through the samepricingmoduleupshift costuses, and is checked before every
rep is dispatched and again between phases and repair candidates. On reaching it the
command stops before the next API call, leaves every completed rep on disk (rerun the same
command with a higher ceiling to resume), prints the priced total and the phase that
stopped, and exits 3 — distinct from a STAY PINNED verdict (1) and a usage error (2).
No verdict is emitted, and aCOST_STOPPED.jsonmarker lands besidediff.jsonso a
partial pipeline can never be read as a finished one; it is deleted when one finishes.
Unpriced models fail closed: a model with no published rate warns loudly at startup and
its usage is charged at the highest rate in the table, never at $0. -
Pricing for the gpt-5.2 family and gpt-5-mini.
upshift costreported "unknown rate" for
every model outside the 5.5/5.6 families, and an unpriced leg is exactly what a spend
ceiling must not treat as free. Standard-tier rates per 1M tokens, from
https://developers.openai.com/api/docs/pricing (fetched 2026-09-03): gpt-5.2 $1.75 in /
$0.175 cached / $14.00 out, gpt-5.2-pro $21.00 / — / $168.00, gpt-5-mini $0.25 / $0.025 /
$2.00. gpt-5.2-pro is listed separately because longest-prefix matching would otherwise
price a-prorun at thegpt-5.2rate and understate it twelvefold. -
adaptreconstructs prompts written as Python implicit string concatenation correctly.
("You are a screener. " "Return JSON with keys a, b.")is one string to the interpreter,
and adapt was inserting a newline between the two literals while labelling both verbatim —
a generated agent that sends a prompt the upstream agent never sends. The gate now records
each chunk's span inside the source literal it came from, and chunks that sit end to end
inside one literal are joined with "" exactly as Python joins them; chunks from separate
statements, from non-Python sources, or that cannot be placed keep the newline join.
Capture mode (previously v0.4.0-dev)
Framework agents, without reading a framework. If the failing request is built inside
pydantic-ai, litellm, LangChain, the Vercel AI SDK, the Claude Agent SDK or opencode, there is
nothing to lift into five adapter files — so record the wire instead. (36 of the 52 Anthropic
rescue cases closed UNSUPPORTED_FRAMEWORK for exactly this.)
upshift capture— a local forwarding recorder (stdlib only) that writes down the
/v1/messagesrequests your agent really sends. Loopback-only by default; upstream status
and body relayed verbatim, a 400 included; credential and account-identifier headers
recorded as present, never as a value; SSE relayed chunk by chunk and reassembled, so a
streaming agent adapts like a non-streaming one; requests grouped into conversations by
theirmessagesarray;--simrecords against the bundled simulator for $0. Both
/v1/messagesand the bare/messagesare accepted.upshift adapt --from-capture— the five files, built from the recorded bytes. No model
call, no source file read. Checks are derived, never invented; thinking blocks never reach a
case; the generated backend replays recorded tool results and fails honestly on arguments it
never saw.ATTRIBUTION.mdandADAPT_EDITS.mdname every source and every deviation.- Framework mapping — an accepted repair is now reported against the knob that expresses
it in the framework the agent was captured from, with the file and line each mapping was
verified at, for eight frameworks (docs/framework-mapping.md). InREPORT.md, in the
terminal, and as a comment block above the firstdiff --gitline ofupgrade.patch. A
knob a framework does not have is reported as "not mapped", never guessed. agent.jsonvolatile_suffix— one recorded sample of a per-request block some agents
regenerate on every call (a live facts block), appended at request-building time and never
accumulated in the history.agent.jsonterminal_tools— tools whose call ends the episode, derived from the
capture (atool_useno later request ever answered). pydantic-ai'sfinal_resultis the
canonical case; without this the replay hands the model a result the framework never
produced and the model calls the tool again, failing the case on the baseline model.- Live smoke: a real pydantic-ai agent,
claude-fable-5→claude-fable-5-1, captured and
upgraded end to end —SAFE WITH PATCH, 3/3 restored, $0.99
(reports/capture-pydantic-ai-smoke.md). A smoke at
N=3, explicitly not evidence.
Full engineering report at this tag: reports/reliability-upgrade-v0.5.md