Skip to content

fix(transport): fill a default User-Agent on proxy-originated provider outbound - #5186

Closed
mdwsk88 wants to merge 2 commits into
lidge-jun:devfrom
mdwsk88:fix/provider-outbound-default-user-agent
Closed

mdwsk88 wants to merge 2 commits into
lidge-jun:devfrom
mdwsk88:fix/provider-outbound-default-user-agent

Conversation

@mdwsk88

@mdwsk88 mdwsk88 commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Proxy-originated provider requests (model discovery, connection tests, quota probes) left the proxy with no User-Agent: the provider outbound wrapper builds requests without one, and the pinned Node-style transport sends none by default. WAF/CDN front ends that require a User-Agent answer these fetches with 403, which surfaced as "provider added but no models" — the pending initial-model-selection state hides all rows, and the only visible trace is a discovery failure line in the service log ([Provider compatibility] Proxy-originated model discovery sends no User-Agent; WAF-fronted gateways reject it with HTTP 403 #5104).
  • The outbound wrapper now fills User-Agent: opencodex when the caller names no User-Agent of their own. Registry static headers, provider headers values, and vendor-specific client fingerprints (Copilot, Kimi CLI, Antigravity) keep their own value and spelling; the fill is case-insensitive and only claims the name nobody spoke for. Inference traffic never uses this wrapper, so the client-fingerprint rationale of [Feature] Add an OpenCodex-specific User-Agent header #1751 is unaffected, and the caller-UA fallback of fix(responses): preserve caller User-Agent #4702 is not touched.
  • Structure doc and provider-configuration reference updated: structure/transports/inventory.md documents the wrapper default, and the headers row in the docs-site configuration reference now says what the default is and how to override it.

Verification

  • bun test tests/providers/provider-outbound.test.ts tests/providers/provider-outbound-private-network.test.ts tests/providers/command-code-fakeip-discovery.test.ts — 46 pass, including seven regressions: default fill on GET, caller value and spelling preserved, fill with no headers at all, the same default on the POST diagnostic path, and the Headers-instance, array-form, and caller-Headers shapes.
  • bun run typecheck — clean. bun run structure:check — clean. bun run privacy:scan — clean.
  • bun run test:changed — 21,812 tests: 21,770 pass, 38 skip, 4 fail. Three of the failures reproduce identically on the base commit without this change (bubblewrap executable-permission semantics and a shim-probe timeout on this macOS host); the fourth passed in isolation (load flake). None touch the provider outbound path.
  • Live evidence from the issue: the same key-auth gateway returns 403 for the UA-less Node-style request and 200 with any User-Agent; the provider-level headers workaround stays a supported override.

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.

Review readiness checklist

This PR stays in draft until every box below is ticked. Tick all four boxes once the requirements are met:

  • All CI tests are green on my local testing.

  • I pushed my PR to the latest dev commit.

  • I resolved all correct Codex and CodeRabbit findings.

  • My PR is ready for review.

Closes #5104

Design and root-cause analysis (including the controlled curl/Bun/Node-style comparison against the gateway) live in #5104.

…r outbound

Proxy-originated requests through the provider outbound wrapper (model discovery, connection tests, quota probes) carried no User-Agent: there is no client request to inherit one from and the pinned Node-style transport sends none, so WAF-fronted gateways answered discovery with 403 and the provider looked like it had no models (lidge-jun#5104). The wrapper now fills User-Agent: opencodex when the caller names none; registry static headers, provider headers, and vendor client fingerprints keep their value and spelling. Inference traffic never uses this wrapper, so the client-fingerprint rationale of lidge-jun#1751 is unaffected.
@coderabbitai

coderabbitai Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

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.

@github-actions github-actions Bot added the bug Something isn't working label Sep 19, 2026
@github-actions

github-actions Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

⏳ DRAFT

  • review readiness checklist open (0/4 boxes ticked).

What to do

  • Tick all four boxes in the PR description once you're done (currently 0/4).

Review readiness checklist

  • ⬜ All CI tests are green on my local testing.
  • ⬜ I pushed my PR to the latest dev commit.
  • ⬜ I resolved all correct Codex and CodeRabbit findings.
  • ⬜ My PR is ready for review.

0/4 boxes ticked.

This PR stays in draft until every box above is ticked.

@lidge-jun

Copy link
Copy Markdown
Owner

리뷰 · 우선순위 64 / 80

이 풀리퀘스트는 프록시가 스스로 보내는 업스트림 요청에 User-Agent를 채운다. User-Agent는 요청을 누가 보냈는지 적는 칸이다. 모델 목록 조회와 연결 시험은 사용자 요청을 그대로 넘기는 길이 아니다. 브라우저가 보낸 칸을 물려받을 곳이 없다. 바로 연결하는 Node 전송도 기본값을 안 넣는다. 회사 게이트웨이 방화벽은 그 칸이 빈 요청을 403으로 거절한다. 모델을 못 받아오고, 처음 고르기가 대기 중이면 목록 줄이 전부 숨는다. 화면에는 제공자를 넣었는데 모델이 없는 것처럼 보인다. 로그에만 discovery 403이 남는다. 이슈는 #5104다.

고친 자리는 src/lib/provider-outbound.tswithDefaultOutboundUserAgent다. GET이든 POST든 이 래퍼로 나가기 전에 한 번 적용된다. 헤더에 User-Agent가 이미 있으면 그 값과 철자를 그대로 둔다. User-Agentuser-agent는 같은 칸으로 본다. 없으면 User-Agent: opencodex를 넣는다. 호출자가 넘긴 헤더 객체는 바꾸지 않고, 복사한 뒤에 칸을 붙인다. 레지스트리 고정 헤더, 제공자 headers, Antigravity 지문은 buildModelsRequest나 호출부가 먼저 넣는다. 여기서 덮이지 않는다. 사용자가 모델을 호출하는 요청은 이 래퍼를 안 쓴다. #1751이 지키려던 클라이언트 지문은 그대로다.

이 머신에서 전송만 따로 확인했다. Node http.request는 헤더에 user-agent: opencodex가 있으면 그대로 보내고, 그 칸이 없으면 빈 채로 나간다. Bun fetch는 칸을 안 주면 Bun/1.4.2를 붙이고, 주면 그 값으로 바꾼다. 버그가 난 자리는 프록시 없이 바로 붙는 Node 길이다. 프록시를 타는 fetch 길은 원래 Bun 값이 나갔다. 이 수정 뒤 호출자가 칸을 안 적으면 그 길도 opencodex가 된다.

테스트는 일반 객체 헤더 네 경우다. 기본으로 채움, 호출자 값과 철자 유지, 헤더가 아예 없음, POST 진단. 저자는 관련 테스트 43개가 통과했다고 적었다. 여기서 그 테스트를 다시 돌리지는 않았다. 베이스는 dev다. #5104를 닫는 다른 열린 풀리퀘스트는 없다. 아직 초안이고, 본문의 준비 체크 네 칸은 비어 있다.

라인 - docs-site/src/content/docs/reference/configuration/providers.mdheaders 설명은 기본 User-Agent가 모델 목록 조회와 연결 시험에만 간다고 한다. Ollama 클라우드 /api/showproviderOutboundPost를 탄다. 잡힌 헤더에 User-Agent가 없으면 같은 opencodex가 붙는다. structure/transports/inventory.md는 이 래퍼의 요청마다라고 맞게 적혀 있다.

라인 - tests/providers/provider-outbound.test.ts의 새 테스트는 { authorization: ... } 같은 일반 객체만 넣는다. 구현은 Headers 객체와 [이름, 값] 목록도 처리한다. 지금 호출부는 buildModelsRequest, Antigravity 사용량 조회, Ollama show가 모두 일반 객체다. 실제로 나가는 길은 테스트가 본다. 나머지 두 모양은 테스트가 없다.

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

기본 문자열을 opencodex로 둘지, 버전을 붙일지. opencode-free 쪽 주석은 버전 숫자를 박으면 그 숫자가 업스트림 일정에 맞춰 곧 거짓이 된다고 적는다. GitHub Copilot OAuth도 이미 User-Agent: opencodex를 쓴다. 프록시 fetch 길에서 나가던 Bun/1.4.2opencodex로 바꾸는 것도 같은 선택이다. 저자는 test:changed에서 실패 4개가 이 변경과 무관하다고 적었다. 그 2만 개 묶음은 여기서 다시 실행하지 않았다.

너의 추천

추론 요청이 아니라, 프록시가 스스로 보내는 래퍼에서만 빈 칸을 채우면 #5104가 닫힌다. 문서 headers 줄을 "이 래퍼를 타는 요청"으로 고치면 구조 문서와 같아진다. Headers와 목록 테스트는 없어도 머지를 막을 일은 아니다. 기본값은 opencodex로 두면 된다. 초안 체크를 채운 다음 머지하면 된다.

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

The headers row narrowed the default to model discovery and connection tests, but every request through the provider outbound wrapper carries it — including the Ollama show enrichment — which is what the structure inventory already documents. Align the row with that wording and cover the Headers-instance and array-form header shapes the implementation handles but the suite had not exercised.
@lidge-jun

Copy link
Copy Markdown
Owner

추가 리뷰 · 우선순위 72 / 80

지난 리뷰 뒤에 커밋이 하나 붙었다. 업스트림으로 보내는 코드는 그대로다. 고친 것은 문서 한 줄과 테스트 세 개다.

지난 리뷰는 설정 문서의 headers 설명이 좁다고 했다. 기본 User-Agent가 모델 목록 조회와 연결 시험에만 간다고 적혀 있었다. Ollama 클라우드 /api/show도 같은 길을 탄다. 그 줄이 이제 그 길을 타는 요청 전부로 바뀌었다. 모델 목록, 연결 시험, Ollama show처럼 프록시가 스스로 보내는 진단이 들어간다. structure/transports/inventory.md와 말이 같다.

테스트도 빈칸을 채웠다. Headers 객체에 User-Agent가 없으면 opencodex를 넣는다. [이름, 값] 목록도 같다. Headers에 이미 user-agent가 있으면 그 값을 둔다. 같이 넣은 authorization도 남는다. 일반 객체 테스트는 원래 있었다.

라인 - tests/providers/provider-outbound.test.ts는 목록 모양에서 "이미 User-Agent가 있으면 둔다"를 안 본다. Headers 쪽은 본다. 이름을 비교하는 코드는 목록도 소문자로 본다. 결과는 같다. 머지를 막을 정도는 아니다.

라인 - 이 브랜치는 dev보다 커밋 세 개 뒤다. #5167, #5187, #5171이다. User-Agent 파일과 안 겹친다. GitHub는 충돌 없이 합칠 수 있다고 본다. 본문의 "최신 dev에 올렸다" 칸은 비어 있다. 아직 초안이다. 리뷰 준비 칸 네 개도 비어 있다.

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

기본 문자열은 그대로 opencodex다. 버전을 붙일지는 지난번과 같다. 안 붙이는 쪽을 추천한다. 최신 dev를 받을지는 체크리스트 규칙이다. 지금 충돌은 없다.

너의 추천

문서 범위와 테스트 빈칸은 닫혔다. 목록의 유지 테스트는 없어도 된다. 초안 체크를 채우고 머지하면 된다. #5104를 닫는 다른 열린 풀리퀘스트는 없다.

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

lidge-jun added a commit that referenced this pull request Sep 19, 2026
…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>
lidge-jun added a commit that referenced this pull request Sep 19, 2026
… Responses, Token Plan opt-in, Command Code ladders (#5198)

* fix(transport): fill a default User-Agent on proxy-originated provider 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>

* fix(providers): point the Volcengine Coding Plan preset at the native 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>

* feat(registry): document and lock the Alibaba Token Plan Responses opt-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>

* fix(command-code): let an operator ladder outrank the shipped effort 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

* fix(providers): finish wiring the Volcengine replay-drop flag through 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.

* fix(command-code): make the operator ladder override an explicit declaration

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

* fix(compat): record the Command Code ladder authority in behavior identity

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.

* docs: recount the provider preset totals from the registry and pin them

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.

* docs: keep the structure preset-split line on one line for its own check

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.

---------

Co-authored-by: lidge-jun <lidge-jun@users.noreply.github.com>
Co-authored-by: mdwsk88 <11055210+mdwsk88@users.noreply.github.com>
Co-authored-by: cubebox <58514883+juzijia@users.noreply.github.com>
@lidge-jun

Copy link
Copy Markdown
Owner

The change from this PR landed on dev as part of #5198, squash-merged as 98b9b344e1, with a Co-authored-by trailer naming you. It closes #5104.

Proxy-originated outbound now fills a default User-Agent, so a WAF-fronted gateway no longer answers 403 to a header-less discovery request, and an operator-set header still wins.

Closing because the content is on dev. Thank you for the fix.

@lidge-jun lidge-jun closed this Sep 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants