Skip to content

Provider discovery and model surface: outbound User-Agent, Volcengine Responses, Token Plan opt-in, Command Code ladders - #5198

Merged
lidge-jun merged 9 commits into
devfrom
codex/L3-provider-model-surface
Sep 19, 2026
Merged

lidge-jun merged 9 commits into
devfrom
codex/L3-provider-model-surface

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Sep 19, 2026

Copy link
Copy Markdown
Owner

Summary

Provider discovery and model surface, one branch targeting dev. Each item had an open contributor PR; three are carried with attribution and one is implemented here. Two of the four proposals changed a default for existing users, and both were narrowed.

Commit Issue Source
549d968d41 fill a default User-Agent on proxy-originated provider outbound #5104 #5186 (@mdwsk88)
ac967a0782 point the Volcengine Coding Plan preset at the native Responses API #5159 #5173 (@juzijia)
fdd9b8e9ec document and lock the Alibaba Token Plan Responses opt-in #5097 #5188 (@mdwsk88)
f005bd56db let an operator ladder outrank the shipped effort table at the wire #5096
3be1eccfeb finish wiring the Volcengine replay-drop flag through registry and routing #5159 review response
9e4e605c8c make the operator ladder override an explicit declaration #5096 review response
913d1b68b0 record the Command Code ladder authority in behavior identity #5096 review response
fe142d5d17 recount the provider preset totals from the registry and pin them documentation drift

Closes #5104
Closes #5159

#5097 and #5096 are each addressed in part and deliberately not closed; see below.

User-Agent on proxy-originated outbound (#5104)

Model discovery, connection tests, the Ollama show probe and the Antigravity quota probe are assembled by the proxy, so there is no client request to inherit a User-Agent from and the pinned Node-style transport sends none. WAF-fronted gateways answer with 403, which surfaces as "provider added but no models". The wrapper now fills User-Agent: opencodex when the caller named none, case-insensitively across all three HeadersInit shapes.

Two changes on top of #5186. The regression suite stubbed pinnedGet/pinnedPost, so it could not see the caller-owned-executor branch, which leaves the wrapper without touching the pinned transport — a fill applied only on the pinned path would have left that branch UA-less and still 403 behind the same WAF; there is now a case for it. And the claim that a caller keeps its header-name spelling is dropped from the comment, the structure doc and a test name: the pinned and SOCKS transports both rebuild the set through new Headers(), which lowercases every name, so the value is what survives.

Volcengine Coding Plan on native Responses (#5159)

The preset moves to openai-responses with responsesPath: /responses, declares supportsServiceTier: false, and keeps the retired Chat destination as an alias. Validated Ark continuations reject the reasoning item the previous turn returned, so the entry sets a new provider-scoped dropResponsesReasoningItems flag; it is lossy, so it is documented as such and can be set to false.

The startup config migration from #5173 is not carried. It would have rewritten every stored canonical Chat row to Responses on the next boot. It borrowed the shape of the Z.AI wire migration while inverting the property that makes that one safe: zai-responses-migration.ts gates on providerMatchesRegistryTransport and therefore only rewrites rows the router already canonicalizes at request time, which is why its own comment calls it behavior-preserving by construction. A Volcengine Chat row is not canonicalized — volcengine-coding-plan is a preserveCustomDestination key entry, so the adapter mismatch makes routedProviderConfig return the stored row untouched — and a version marker introduced now cannot tell the old default apart from a deliberate pre-upgrade Chat choice. The preset default therefore applies to new rows only, existing rows keep their wire, a regression case pins that, and the docs say how to switch by hand.

Review then found the flag reached the adapter but not three places that must know about it: the exhaustive registry field classification (a compile error), the compatibility behavior record, and the resolved static policy. It also missed the one routing path this narrowing makes reachable — a row saved on Chat that opts a single model into Responses through modelAdapters leaves routedProviderConfig by the early return, so the flag has to be filled there from the destination matcher as well. All four are fixed, with the wire body asserted rather than the flag alone.

Alibaba Token Plan Responses opt-in (#5097)

The issue asked for a validated opt-in or a default flip. This lands the opt-in and declines the flip. #5188 proposed pinning qwen3.8-flash, qwen3.7-plus and glm-5.3 through modelWireDefaults, which would move every existing Codex user of those models onto a different upstream with no config change, and one delta is unresolved: the entry's preserveReasoningContentModels is read by the Chat adapter, while the Responses serializer reads preserveResponsesReasoningContent, which this entry does not set. Pinned models would replay with blanked reasoning content — strictly less state than they carry today. Z.AI and DeepSeek set both flags together and their entry comments say why. Blanking is the fail-safe direction, so leaving the default alone costs nobody a working setup, while setting the Responses flag on faith could 400 a continuation on a gateway nobody has verified it against.

The new suite holds both halves: the three models resolve to openai-responses once opted in and the request actually reaches /responses rather than being flipped back by the handleResponses replay, the wire default stays Chat on every inbound, and a guard fails if a Responses wire default is ever declared for this entry without preserveResponsesReasoningContent beside it. That guard is the precondition for the flip the issue asks for, so #5097 stays open.

Command Code effort ladders (#5096)

The adapter resolved its wire effort as commandCodeReasoningEfforts() ?? configuredReasoningEfforts(), so a model with a row in the shipped table ignored providers.command-code.modelReasoningEfforts outright while a model without one honoured it. The catalog never agreed with that split — it advertises the picker from configuredReasoningEfforts — so an operator who widened a pinned row saw the wider ladder offered in Codex and then watched the adapter strip the rung on the way out, with no error to explain it.

Configuration can now win, but only when the provider declares modelReasoningEffortsAuthoritative. The first attempt inferred provenance by comparing the configured row against the shipped table, and review rejected it: providerConfigSeed copies the whole table into every materialized preset, and enrichment and routing both keep a persisted row over the current seed, so a row written by an older release keeps its old value and would start looking like an operator edit the moment the shipped table is corrected — at which point the stale row would outrank the correction and disable the rejection repair. Since the follow-up this issue asks for is exactly a table correction, that misfire was not hypothetical. A declared flag cannot be produced by seeding, so its presence is evidence and a value comparison is not.

With the flag set the ladder resolves through the same function the catalog uses, so the picker and the wire agree, and a rung the upstream refuses is returned as that error instead of being replayed without the effort. Without it, a configured row — seeded, stale or hand-written — changes nothing. Because the flag changes both the bytes sent and the recovery taken, it is part of the compatibility behavior record and the resolved static policy; two routes that disagree about it are not the same subject.

One alias asymmetry the override made reachable is fixed too: the xhigh branch aliases to max only when the ladder does not advertise xhigh, but the ultra branch aliased whenever max existed, so an authoritative ladder offering ultra would have advertised it in the picker and quietly sent max. No shipped row offers ultra, so the built-in table is unaffected.

The issue's stated root cause is disproven. #5096 attributes the clobber to applyDirectReasoningEffortContracts(), which returns immediately unless entry.directReasoningEffortModels is populated; neither the command-code nor the commandcode entry declares it, so that function is a no-op for this provider and editing it would have fixed nothing. The real shadowing is the ?? at the adapter. Configuration is not erased or reverted on restart — it is ignored at request construction for known rows.

The data half is not adopted. The issue also reports seven shipped rows narrower than the live API accepts and 38 live models with no row. The table's own provenance rules require per-row evidence; the measurements are a third party's and cannot be reproduced without a GOAT-plan key; the ids double as the router's known-ids decode source through knownModelIdsForProvider, so a mis-cased id has routing consequences; and at least one proposed widening contradicts an alias the file documents from the model profile (xhigh -> max on deepseek/deepseek-v4-flash). With this change an operator can apply the measured ladders from config today, and the reporter offered to open the full 46-model table as its own PR, which is where that provenance belongs. #5096 therefore stays open for that half.

Preset-count drift

Seventeen pages restate how many built-in presets ship, and sixteen had drifted: the English provider guide said 95 total with 79 key-based, while the seven translated guides, all eight quickstarts including the English one, and structure/ops/docs-and-release.md still said 94 and 78. Nothing caught it, because both numbers read as plausible and no check compared either to the registry.

The registry says 95. Every authKind declaration in the two entry files that compose PROVIDER_REGISTRY is a string literal, and they group as 79 key, 12 oauth, 3 local, 1 forward — which is what the English guide already claimed. The other sixteen places now agree, and a new tests/ci-workflows/docs-provider-preset-counts.test.ts derives both numbers from PROVIDER_REGISTRY and asserts them per page, so the next preset fails every locale at once instead of drifting. Each page is anchored by a locale-specific phrase rather than by its number, so rewording a sentence fails the check and asks to be re-anchored.

Verification

No local verification was run, deliberately. No test suite, no individual test file, no bun run typecheck, no build, no install, and no ocx invocation. A previous local run on this machine destroyed real ~/.opencodex state, so this work is restricted to static reasoning and source oracles, with hosted CI on the exact head as the only execution evidence.

  • Every carried change was read against its consumer in source rather than trusted from the source PR: providerModelWireDefault and resolveWireProtocolOverride for the wire defaults, providerMatchesRegistryTransport and routedProviderConfig for the Volcengine narrowing, sanitizeReasoningInputContent and preserveResponsesReasoningContent for the reasoning delta, configuredReasoningEfforts and providerConfigSeed for the Command Code precedence.
  • Three rounds of adversarial static review by parallel reviewers — on each source PR before carrying, and twice on this branch's own diff. They produced both narrowings, the added executor-branch test, the registry classification that would not have compiled, the routing backfill, the replacement of the provenance heuristic, and the behavior-identity fix.
  • Exhaustive unions this branch extends are registered: dropResponsesReasoningItems in the satisfies Record<keyof ProviderRegistryEntry, ...> classification, both new config fields in the satisfies Record<keyof OcxProviderConfig, ...> field policy and the persisted schema, reasoning.effortsAuthoritative in the closed behavior key set, and all three added test files in scripts/test-layout/layout.json and tests/fixtures/test-layout-expected.json.
  • File-size ratchet: no file this branch touches carries a baseline cap.
  • Rebased onto c81d43053b, which repaired the three failures this branch previously inherited from a red dev — the file-size ratchet on two files and three Reserve actual catalog finalization lifecycle cases, none of them touched here.
  • Execution evidence is the exact-head CI run on this PR.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

@coderabbitai

coderabbitai Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Repository: lidge-jun/opencodex/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 2a3cbb86-9b58-43cc-a411-343dc03d3781

📥 Commits

Reviewing files that changed from the base of the PR and between c81d430 and 6861bc3.

📒 Files selected for processing (44)
  • docs-site/src/content/docs/fr/getting-started/quickstart.md
  • docs-site/src/content/docs/fr/guides/providers.md
  • docs-site/src/content/docs/getting-started/quickstart.md
  • docs-site/src/content/docs/guides/providers.md
  • docs-site/src/content/docs/ja/getting-started/quickstart.md
  • docs-site/src/content/docs/ja/guides/providers.md
  • docs-site/src/content/docs/ko/getting-started/quickstart.md
  • docs-site/src/content/docs/ko/guides/providers.md
  • docs-site/src/content/docs/reference/configuration/providers.md
  • docs-site/src/content/docs/ru/getting-started/quickstart.md
  • docs-site/src/content/docs/ru/guides/providers.md
  • docs-site/src/content/docs/tr/getting-started/quickstart.md
  • docs-site/src/content/docs/tr/guides/providers.md
  • docs-site/src/content/docs/zh-cn/getting-started/quickstart.md
  • docs-site/src/content/docs/zh-cn/guides/providers.md
  • docs-site/src/content/docs/zh-tw/getting-started/quickstart.md
  • docs-site/src/content/docs/zh-tw/guides/providers.md
  • scripts/test-layout/layout.json
  • src/adapters/command-code.ts
  • src/adapters/openai-responses/passthrough.ts
  • src/adapters/openai-responses/reasoning.ts
  • src/config/schema/leaf-validators.ts
  • src/lab/subject/behavior-fingerprint.ts
  • src/lib/provider-outbound.ts
  • src/providers/derive.ts
  • src/providers/registry/entries-extended.ts
  • src/providers/registry/model-ids.ts
  • src/providers/registry/types.ts
  • src/providers/resolved-model-policy.ts
  • src/router.ts
  • src/routing/compatibility/behavior.ts
  • src/server/auth-cors.ts
  • src/types/provider.ts
  • structure/ops/docs-and-release.md
  • structure/providers-and-adapters.md
  • structure/transports/inventory.md
  • tests/ci-workflows/docs-provider-preset-counts.test.ts
  • tests/fixtures/test-layout-expected.json
  • tests/gui/volcengine-providers.test.ts
  • tests/providers/alibaba-token-plan-responses-optin.test.ts
  • tests/providers/command-code-provider.test.ts
  • tests/providers/provider-outbound.test.ts
  • tests/providers/zai-reasoning-replay.test.ts
  • tests/service/service-tier-capability.test.ts
 ______________________________________________________________________
< Race conditions: you *caught* one. Unfortunately you released three. >
 ----------------------------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

lidge-jun and others added 8 commits September 20, 2026 03:59
…r outbound

Proxy-originated provider requests -- model discovery, connection tests, the
Ollama show probe, the Antigravity quota probe -- are assembled by the proxy
itself, so there is no client request to inherit a User-Agent from, and the
pinned Node-style transport sends none. WAF-fronted gateways answer a UA-less
request with 403, which surfaces as "provider added but no models" because the
pending initial-model-selection state hides every row (#5104).

The outbound wrapper now fills User-Agent: opencodex when the caller names no
User-Agent of its own. The check is case-insensitive and covers all three
HeadersInit shapes, so registry static headers, provider headers values, and
vendor client fingerprints (Copilot, Kimi CLI, Antigravity) keep their value and
never gain a second User-Agent beside it. Inference traffic never reaches this
wrapper: its only call sites are catalog model discovery, the management
connection test, the Antigravity quota probe, and the Ollama show enrichment.

Carried from #5186 with two corrections. The regression suite now also asserts
the caller-owned-executor branch, which leaves the wrapper without touching the
pinned transport the original tests stubbed -- a fill applied only on the pinned
path would have left that branch UA-less and still 403 behind the same WAF. And
the claim that a caller keeps its header-name spelling is dropped from the
comment, the structure doc, and a test name: the pinned and SOCKS transports
both rebuild the set through new Headers(), which lowercases every name, so the
value is what survives.

Closes #5104

Co-authored-by: mdwsk88 <11055210+mdwsk88@users.noreply.github.com>
… Responses API

Ark Coding Plan documents a native Responses endpoint at /api/coding/v3/responses,
but the built-in preset still shipped openai-chat, so Codex clients paid for a
translation hop the gateway does not need (#5159). The preset now carries
adapter openai-responses with responsesPath /responses, declares
supportsServiceTier: false so an unsupported service_tier fails closed instead of
reaching the gateway, and keeps the retired Chat destination as an alias so an
existing row still resolves this entry's metadata.

Validated Ark continuations reject the reasoning item the previous turn returned,
answering 400 InvalidParameter, so the entry sets a new provider-scoped
dropResponsesReasoningItems flag. It removes replayed reasoning items from
continuation input without enabling orphan tool repair. The flag is lossy --
summaries, item ids and encrypted_content go with the item -- so it is documented
as such in the configuration reference and an operator can set it to false.

Carried from #5173 with the startup config migration removed. That migration
would have rewritten every stored canonical Chat row to Responses on the next
boot. It borrowed the shape of the Z.AI wire migration while inverting the
property that makes that one safe: zai-responses-migration.ts gates on
providerMatchesRegistryTransport and therefore only rewrites rows the router
already canonicalizes at request time, which is why its comment can call itself
behavior-preserving by construction. A Volcengine Chat row is not canonicalized
-- volcengine-coding-plan is a preserveCustomDestination key entry, so the
adapter mismatch makes routedProviderConfig return the stored row untouched --
and a version marker introduced now cannot distinguish the old default from a
deliberate pre-upgrade Chat choice. The preset default therefore applies to new
rows only, existing rows keep their wire, and the docs say how to switch by hand.
Dropping that migration also drops the src/server/index.ts hunk, which would have
taken the file from 892 to 895 lines against its 893-line ratchet cap once merged
with dev.

Closes #5159

Co-authored-by: cubebox <58514883+juzijia@users.noreply.github.com>
…t-in

Alibaba Token Plan (Beijing) serves its models over an OpenAI-compatible
Responses API on the same /compatible-mode/v1 base and ships an official Codex
integration guide on wire_api = "responses". Three models carry live end-to-end
evidence on that gateway -- qwen3.8-flash, qwen3.7-plus and glm-5.3 -- covering
custom tools, reasoning replay, streaming and multi-turn continuation (#5097).

The issue asked for a validated opt-in or a default flip. This lands the opt-in
and deliberately declines the flip. #5188 proposed pinning those three models
through modelWireDefaults, which would move every existing Codex user of them
onto a different upstream with no config change, and one delta is unresolved:
the entry's preserveReasoningContentModels is read by the CHAT adapter, while
the Responses serializer reads preserveResponsesReasoningContent, which this
entry does not set. Pinned models would therefore replay with blanked reasoning
content -- strictly less state than they carry on the Chat wire today. Z.AI and
DeepSeek set both flags together, and their entry comments say why. Blanking is
the fail-safe direction, so leaving the default alone costs nobody a working
setup; setting the Responses flag on faith could 400 a continuation.

The registry entry records the evidence and the open precondition, the
modelAdapters reference documents the opt-in, and the new suite holds both
halves: the three models resolve to openai-responses once opted in and the
request actually reaches /responses rather than being flipped back by the
handleResponses replay, the wire default stays Chat on every inbound, and a
guard fails if a Responses wire default is ever declared for this entry without
preserveResponsesReasoningContent beside it.

Refs #5097

Co-authored-by: mdwsk88 <11055210+mdwsk88@users.noreply.github.com>
…table at the wire

The Command Code adapter resolved its wire effort as
commandCodeReasoningEfforts() ?? configuredReasoningEfforts(), so a model WITH a
row in the shipped table ignored providers.command-code.modelReasoningEfforts
outright while a model WITHOUT one honoured it. The catalog never agreed with
that split: it advertises the picker from configuredReasoningEfforts, so an
operator who widened a pinned row saw the wider ladder offered in Codex and then
watched the adapter strip the rung on the way out, with no error to explain it
(#5096).

An operator row now resolves through the same function the catalog uses, so the
picker and the wire cannot disagree, and sanitization, tier healing and
learned-refusal dropping apply to it. Rows the operator never touched keep the
shipped table, including a value learned by a profile refresh.

The seeded copy is what makes this subtle, and it is why a plain config-first
flip would have been wrong: providerConfigSeed writes the whole shipped table
into every materialized preset, so "the config has a row for this model" proves
nothing about who wrote it. Only a row that DIFFERS from the shipped value counts
as a decision, and the comparison is against the shipped value rather than the
resolved one so that a profile refresh narrowing a ladder is never mistaken for
an operator edit. A regression case asserts that a preset carrying the seeded
table produces byte-identical wire efforts to carrying no config at all.

An operator-authorized rung also stops being silently downgraded. The
effort-rejection path exists for rungs the shipped table guessed wrong, where
replaying without the effort is a repair; when the operator wrote the ladder, the
same replay would answer at the provider default and hide a wrong configuration
behind a successful-looking response, so the upstream rejection is returned
unchanged and no profile fetch is made.

Scope: this closes the structural half of #5096 only. The issue also reports that
seven shipped rows are narrower than the live API accepts and that 38 live models
have no row. Those rows are not adopted here. The table's own provenance rules
require per-row evidence, the measurements are a third party's and cannot be
reproduced without a GOAT-plan key, the ids double as the router's known-ids
decode source via knownModelIdsForProvider so a mis-cased id has routing
consequences, and at least one proposed widening contradicts an alias this file
documents from the model profile (xhigh -> max on deepseek/deepseek-v4-flash).
With this change an operator can apply the measured ladders from config today,
and the reporter offered to open the full 46-model table as its own PR, which is
where that provenance belongs.

Refs #5096
… registry and routing

Adversarial static review of the branch found the carried #5173 flag reached the
adapter but not three places that must know about it.

src/providers/registry/model-ids.ts classifies every ProviderRegistryEntry key
through a satisfies Record<keyof ProviderRegistryEntry, ...> clause. Adding
dropResponsesReasoningItems to the interface without classifying it does not
compile, and the parity test rejects an entry carrying an unclassified field.
The flag names no model id, so it is NONE.

The compatibility behavior record described reasoning replay through
preserveResponses alone, so two routes that disagree about whether replayed
reasoning items are dropped produced the same behavior fingerprint and could
share compatibility evidence. Dropping an item changes the continuation body, so
it is now part of reasoning.replayMode. The resolved static policy projection
omitted the field for the same reason and now carries it.

routedProviderConfig returns early for a row whose adapter no longer matches its
registry entry, which is exactly the shape this branch deliberately leaves alone:
a Volcengine Coding Plan config saved on Chat. That row still reaches the
Responses adapter when one model opts in through modelAdapters, and it arrived
without the flag, so the continuation forwarded the reasoning item Ark answers
400 to. The flag belongs to the destination rather than to the provider-wide
wire, so it is filled on that path too, from the destination matcher that already
refuses templated and overridable base URLs. An explicit value still wins.

Also updates the ja, ko, fr, ru and zh-TW provider guides, which still described
Agent Plan as the only native Responses preset and so contradicted the English
and zh-CN source, and softens an overclaiming test comment: the routing case
pins what a user observes, and the absence of a startup migration is the absence
of a module rather than something that case can prove.
…aration

Adversarial static review rejected the provenance test the previous commit used.
It decided a configured row was an operator decision when that row DIFFERED from
the shipped table. That is not sound: providerConfigSeed copies the whole table
into every materialized preset, and enrichment (derive.ts) and routing
(mergeStringArrayRecord) both keep a persisted row over the current seed. A row
written by an older release therefore keeps its old value, and the moment the
shipped table is corrected that untouched seed starts looking like an operator
edit -- at which point it would outrank the correction AND disable the
effort-rejection repair. The follow-up this issue asks for is exactly a table
correction, so the misfire was not hypothetical.

Provenance is now declared instead of inferred. A provider opts in with
modelReasoningEffortsAuthoritative, which providerConfigSeed never writes, so its
presence can only have come from a human. Without it a configured row changes
nothing at the wire, seeded or stale or hand-written; with it, the ladder
resolves through the same function the catalog uses and a refused rung returns
the upstream error rather than being replayed without the effort.

Also fixes an alias asymmetry the override made reachable. The xhigh branch
aliases to max only when the ladder does not advertise xhigh, but the ultra
branch aliased whenever max existed. An authoritative ladder offering ultra would
have advertised ultra in the picker and quietly sent max -- the same
catalog/wire disagreement this change exists to remove. No shipped row offers
ultra, so the built-in table is unaffected.

The regression cases follow: an authoritative ladder widens, narrows, and sends
ultra as itself; a seeded map and a fully widened stale map both produce
byte-identical wire efforts to carrying no config at all; and an authorized rung
the upstream refuses surfaces the 400 with one generate call and no profile
fetch.

Refs #5096
…ntity

Three independent adversarial reviews converged on the same gap: commit 6 added
a flag that changes the wire effort AND suppresses the downgrade retry, but the
compatibility resolver recorded only the configured ladder. Two routes with the
same provider, destination, adapter, model and ladder therefore produced the same
behavior fingerprint while sending different bytes and recovering differently, so
evidence collected under one could admit the other. That is the same defect class
commit 5 fixed for dropResponsesReasoningItems, left unfixed for its sibling.

reasoning.effortsAuthoritative joins the closed behavior key set and is emitted
from the production resolver, and the resolved static policy carries the flag as
an operator-owned value so a policy reader no longer reports the same effective
ladder for two providers that send different efforts.

Also documents both new contracts in structure/providers-and-adapters.md, which
owns this source area: the Coding Plan native Responses default, the lossy
replay drop and why it is filled on the early-return path, why there is no
startup migration, and why the Command Code ladder override is a declared flag
rather than an inference. Softens the dropResponsesReasoningItems reference row,
which promised the upstream sees no previous-turn reasoning state at all — the
flag removes reasoning items from the forwarded input and does not touch
previous_response_id. Corrects a test comment left describing the provenance
inference commit 6 replaced.
Seventeen pages restate how many built-in presets ship, and sixteen of them had
drifted. The English provider guide said 95 total with 79 key-based; the ja, ko,
fr, ru, tr, zh-CN and zh-TW guides, all eight quickstarts including the English
one, and structure/ops/docs-and-release.md still said 94 and 78. Nothing caught
it, because both numbers read as plausible and no check compared either to the
registry.

The registry says 95. Every authKind declaration in the two entry files that
compose PROVIDER_REGISTRY is a string literal, and they group as 79 key, 12
oauth, 3 local, 1 forward, which is what the English guide already claimed. The
other sixteen places now say the same.

AGENTS.md asks for a count to be derived from the thing it describes rather than
restated, and this is the failure it describes: a preset lands, whoever adds it
updates the English guide, and fifteen translated or secondary copies quietly
keep the old number. A new ci-workflows check derives the total and the
key-based split from PROVIDER_REGISTRY and asserts them against each page, so
the next preset fails every locale at once instead of drifting. Each page is
located by a locale-specific phrase rather than by its number, so rewording a
sentence fails the check and asks to be re-anchored — a sentence nobody can
locate is a sentence nobody is checking.
@lidge-jun
lidge-jun force-pushed the codex/L3-provider-model-surface branch from 717a4bf to fe142d5 Compare September 19, 2026 19:02
The new count check locates each page by a locale-specific phrase and asserts
exactly one line carries it. Rewording the structure ops sentence pushed
"documented split is" across a line break, so the anchor matched nothing and the
check failed in test 2/4 and macos 1/2 — which is the behavior it was written
for: a sentence nobody can locate is a sentence nobody is checking. Reflowed so
the anchor, the total and the key-based split sit on one line again.
@lidge-jun
lidge-jun marked this pull request as ready for review September 19, 2026 19:51
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 19, 2026 19:51
@lidge-jun
lidge-jun merged commit 98b9b34 into dev Sep 19, 2026
28 checks passed
@lidge-jun
lidge-jun deleted the codex/L3-provider-model-surface branch September 19, 2026 19:51
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 19, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-19T19:55:10.856763Z 6861bc3 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 68 / 80

이 풀리퀘스트는 제공자 쪽 문제 네 개를 dev 한 브랜치에 넣는다. 아직 초안이다. 원격 검사는 대부분 통과했다. macOS 키링과 macOS 테스트 2/2는 이 글을 쓸 때 아직 돌고 있었다.

첫째, 프록시가 스스로 보내는 요청에 보낸 사람 칸을 채운다. 그 칸 이름이 User-Agent다. 모델 목록, 연결 시험, Ollama 확인은 사용자 요청을 그대로 넘기는 길이 아니다. 브라우저가 적어 둔 칸을 물려받을 곳이 없고, 바로 붙는 전송도 기본값을 안 넣는다. 앞단 방화벽은 칸이 빈 요청을 403으로 거절한다. 화면에는 제공자를 넣었는데 모델이 없는 것처럼 보인다. 이슈는 #5104다. 호출자가 이미 칸을 적었으면 그 값을 둔다. 없으면 opencodex를 넣는다. 모델을 부르는 추론 요청은 이 길을 안 탄다.

둘째, Volcengine 코딩 플랜의 새 기본선을 채팅 완성에서 Responses로 바꾼다. 주소는 같다. 직전 답의 생각 조각을 다음 요청에 넣으면 Ark가 400을 준다. 그래서 그 조각만 빼고 보낸다. 이미 저장해 둔 채팅 줄은 옮기지 않는다. 채팅 줄에서 모델 하나만 Responses로 고르면, 빼기 칸은 그 요청에도 따라간다. 이슈는 #5159다.

셋째, 알리바바 토큰 플랜은 모델 세 개를 Responses로 고를 수 있다는 것만 문서와 테스트로 잠근다. 세 개는 qwen3.8-flash, qwen3.7-plus, glm-5.3이다. 기본은 채팅 그대로다. 세 모델을 기본으로 바꾸면, 설정을 안 건드린 사람이 다른 길로 가고, 생각 글이 비어서 나간다. 그 칸을 확인하기 전에는 기본을 안 뒤집는다. 이슈 #5097은 열린 채로 둔다.

넷째, Command Code는 설정에 적은 노력 단계가, 표에 이미 있는 모델에서는 무시됐다. 목록에는 넓은 단계가 보이는데, 보낼 때는 좁은 표가 이겼다. 이제는 modelReasoningEffortsAuthoritative를 켜야 설정이 이긴다. 표와 값이 다른지만 보고 사람 수정으로 치면, 옛 버전이 저장해 둔 줄이 다음 수정표를 이긴다. 그래서 스위치를 따로 둔다. 표 자체를 46개 모델로 넓히는 절반은 안 넣었다. 이슈 #5096은 열린 채로 둔다.

문서에 적힌 프리셋 개수도 레지스트리에서 다시 세어 95와 79로 맞추고, 다음부터 어긋나면 테스트가 실패하게 했다.

라인 - src/adapters/command-code.tsoperatorChoseCommandCodeLadder. 스위치는 제공자 전체다. src/router.tsmergeStringArrayRecord는 요청 때 레지스트리 표를 먼저 깔고, 사용자가 적은 줄만 그 위에 덮는다. 한 모델만 넓히려고 스위치를 켜도, 손대지 않은 모델은 표에 줄이 있으므로 같이 켜진다. 그 모델들은 업스트림이 단계를 거절해도, 단계를 빼고 한 번 더 보내지 않고 400을 그대로 돌려준다. 프리셋을 만들 때 providerConfigSeed가 표를 통째로 복사하는 것과 같다.

라인 - docs-site/src/content/docs/ko/reference/configuration/providers.md. 영어 설정 표에는 dropResponsesReasoningItemsmodelReasoningEffortsAuthoritative가 있다. 한국어 표에는 그 두 줄이 없다. 같은 번역 표도 같다. 한국어로 읽는 사람은 이 페이지에서 스위치를 못 찾는다.

메인테이너의 판단이 필요한 지점

이미 설치된 코딩 플랜을 채팅에 남겨 둘지. #5173은 다음 부팅에 Responses로 옮기려 했다. 이 브랜치는 그 이전을 빼 버렸다. 채팅으로 저장해 둔 줄은 요청 때 레지스트리 기본으로 안 바뀐다. 부팅 때 바꾸면 지금 쓰는 길이 바뀐다. 옛 기본과, 일부러 고른 채팅을, 지금 넣어 둔 숫자로는 구분할 수도 없다. 옮기지 않는 쪽이 맞다. 이미 400을 보는 사람은 설정을 직접 Responses로 바꿔야 한다.

알리바바 세 모델을 기본으로 고정하지 않은 것도 같다. #5188의 고정은 여기서 거절됐다. #5097은 닫지 않는 게 맞다.

Command Code 스위치가 표에 있는 모델 전부에서 400 재시도를 끄는 것을 그대로 둘지. 문서에 그 한 줄을 적을지, 나중에 모델 한 줄만 지정하게 바꿀지.

너의 추천

방향은 유지하면 된다. User-Agent, 코딩 플랜의 새 기본, 이전을 안 하는 이유, 알리바바를 기본으로 안 뒤집는 이유는 코드와 테스트가 맞다. 머지 전에 스위치 설명에 한 줄만 보태라. 이름이 command-code인 제공자는 표를 통째로 합치므로, 스위치를 켜면 손대지 않은 모델도 400을 그대로 받는다고 적으면 된다. 모델마다 스위치를 새로 만들 필요는 없다. #5186, #5173, #5188은 이 풀리퀘스트가 대신하므로 닫으면 된다. #5173의 부팅 이전은 가져오지 않는 게 맞다. types.tsconfig.ts 분할로 이 글을 무효로 볼 이유는 없다. 새 칸은 이미 나뉜 src/types/provider.tssrc/config/schema/leaf-validators.ts에 들어가 있다. 초안을 풀고, 남은 macOS 검사가 끝나면 머지하면 된다.

이 댓글은 grok-bot이 작성했습니다

lidge-jun added a commit that referenced this pull request Sep 20, 2026
…ery limits (#5216, #5215) (#5294)

* fix(gui): resolve combos by what they are, and describe failover as it runs

Two user-visible strings on the compaction-routing surface described behaviour
the code does not have.

The panel decided whether a selection was a combo by testing for a "combo/"
prefix. A combo reached through an alias carries no prefix, so it was described
as an ordinary provider and none of its targets were named -- the answer existed
and the operator could not see it. The panel now asks what the selection
resolves to: the combo list is keyed by the public model id the server already
computes, which is the alias when one is set and "combo/<id>" otherwise, so both
spellings answer the same way. It reads that list through parseComboList, the
same reader the combo workspace uses, so the selector rule is not spelled out a
second time here.

The combo warning told the operator that a covered compaction goes to every
target, including failover targets. It does not. core-combo.ts dispatches one
target per loop iteration, returns as soon as one responds, and advances only
after a retryable failure. An operator reading the old text would budget fan-out
cost and fan-out latency for something that never happens. The warning now says
the targets are attempted in order and the first that answers is used, which is
both what happens and what someone debugging a slow compaction needs.

Wording changed in all ten locales. The regression covers the aliased combo the
prefix test could not see, and asserts the ordering sentence rather than the
fan-out claim.

Closes #5216

* docs: check the provider discovery limits against the registry

The provider guides restate a byte ceiling and a row ceiling for thirteen
fixed-host presets, in eight pages, and nothing compared any copy to the
registry. #5198 fixed a preset count that had drifted across sixteen files for
months for exactly that reason; these limits are the same shape one layer down.

Every number is now read from that preset's modelDiscovery and asserted against
every shipped guide, so lowering a ceiling fails in all eight locales at once
instead of leaving seven translations describing the old one. A grouped section
must first agree in the registry before one sentence may speak for two presets,
which is what makes the Nscale/Vultr and Command Code sentences legitimate
rather than convenient.

Sections are located by brand name and the presence of a KiB or MiB token, not
by a translated sentence. A restated anchor phrase is the same hand-copied value
the guard exists to remove, and the brand names are Latin in all eight published
locales. The byte ceiling is compared as an exact token set rather than a
substring, so a stale number left beside the current one fails.

The structure record claimed the guides carried identical limits. That claim was
false when it was written: the Korean guide had no Featherless section, so it
documented twelve of the thirteen limited presets. The section is added and the
prose is replaced by a description of what is actually asserted.

Closes #5215

* fix(gui,test): close three defects an adversarial pass found in this lane

Combo target lookup read a plain object by the selected model id. A combo id is
free-form, so an alias of "constructor" or "toString" resolved to an inherited
Object member and the renderer tried to join a function. Read it with
Object.hasOwn.

Recognizing a combo only through the fetched list lost the canonical prefix as
a signal of its own. When /api/combos has not answered yet or failed, a
"combo/x" selection was described as an ordinary provider named "combo" -- worse
than the alias gap this lane set out to fix, because that path is reachable
whenever the management API is briefly unavailable. The prefix is kept as an
independent signal and the target names fall back to the existing
"its configured target providers" wording.

The documentation guard compared the row ceiling as a substring of the whole
paragraph, so the byte ceiling's own digits could satisfy it: a Hyperbolic
paragraph saying "256 KiB and 128 raw rows" would have passed an expected 256
rows. Row numbers are now read from the prose with the unit tokens removed. All
104 locale/section combinations still pass, verified by transcribing the test's
own logic over the eight guides.

* docs(devlog): record lane G onboarding, update and screen improvements

Why the recovery path cannot live in the dashboard, what each of the six targets needed, the differential between the two workspace pull requests with the three findings that decide their sequencing, and the one src/ defect this lane identified and left stated rather than half-fixed.

* fix(gui): key the combo lookup by Map, not by a caller-configured object key

A combo's public model id is free-form and operator-configured, and
readComboProviders wrote it straight into an object literal. That is a
prototype-pollution sink on the write side, and the read side returned an
inherited member for an alias of "constructor" or "toString" -- the previous
commit guarded the read with Object.hasOwn and left the write as it was.

A Map removes both. There is no prototype to shadow, the guard disappears, and
the failed-fetch fallback returns an empty Map rather than an empty object, so
the two branches keep the same type.

* fix(gui): build the combo target list in one pass

React Doctor's js-flatmap-filter fired on the map().filter(Boolean) this lane
introduced at CompactionRoutingPanel.tsx:51 -- one new warning in one file, and
the job's blocking threshold is warning. flatMap does the same work in a single
pass. The related js-combine-iterations rule is switched off in
gui/doctor.config.json, but this is a different rule and is enabled, so this is
a real new finding rather than an accepted one.
mdwsk88 added a commit to mdwsk88/opencodex that referenced this pull request Sep 21, 2026
…sponses wire

Rebased onto dev after lidge-jun#5198 landed the opt-in path. The pin that lidge-jun#5198
deliberately held back is now justified with its own live evidence on the
Beijing gateway (260922):

- a two-turn replay round-tripping a reasoning item WITH its plaintext
  content array is accepted (HTTP 200, correct continuation), so
  preserveResponsesReasoningContent is set beside the pins — the Z.AI /
  DeepSeek pairing the lidge-jun#5198 comment called for;
- qwen3.7-plus (the one pinned model in thinkingBudgetModels) accepts its
  full low/medium/high/xhigh/max ladder as reasoning.effort strings on the
  Responses wire, so the numeric thinking_budget translation is not needed;
- qwen3.8-flash / glm-5.3 replay and effort acceptance re-confirmed.

Tests: the wire-defaults suite gains the qwen3.7-plus upstream-URL case the
review asked for, plus an end-to-end plaintext-replay case asserting the
flag preserves (and its absence blanks) reasoning content through
handleResponses. The lidge-jun#5198 opt-in suite is updated to the pinned world:
chat/anthropic inbound stay on chat, the pins are exactly the three
live-verified models scoped to responses inbound, and an unpinned family
member still opts in/out through modelAdapters. Its flag guard now
exercises the real pins.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant