Skip to content

feat(claude): send eligible managed-key Messages natively (PF-08) - #5816

Closed
lidge-jun wants to merge 20 commits into
feat/pf07-native-chat-combosfrom
feat/pf08-managed-messages-native
Closed

lidge-jun wants to merge 20 commits into
feat/pf07-native-chat-combosfrom
feat/pf08-managed-messages-native

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Summary

PF-08 of the protocol-first-class unit (devlog/_plan/260924_protocol_first_class/020_engine_and_codecs.md#pf-08-managed-native-messages). Stacked on #5815 (PF-07).

Behind protocols.rollout.managedMessagesNative (default off): a Messages request whose settled route is a direct, key-auth anthropic provider is sent to /v1/messages natively with the proxy-managed key, instead of replaying through the internal Responses body.

  • src/adapters/anthropic.ts: URL resolution, anthropic-version pin and key-auth header code move unchanged into exported helpers so the native builder uses exactly what the adapter uses.
  • src/adapters/anthropic/passthrough.ts: buildAnthropicMessagesPassthroughRequest — provider key only (reads no caller header; refuses OAuth and forward auth), operator provider.headers as the adapter applies them, and the source body with the wire model and a field allowlist.
  • src/server/messages-native-eligibility.ts: nativeMessagesDeclineReason names the first rule that keeps a route off the lane (rollout-disabled, cross-wire-ir, auth-mode-not-native, combo-or-policy-route, effort-row/fast-row, vision-preprocessing, and new bridge-only-policy). The ingress, count_tokens and the planner all ask it.
  • Operator policy that only the bridge applies is never skipped silently: pinned route effort, blocked-skill elision, or a web-search sidecar that would engage all keep the request on the bridge with bridge-only-policy (new reason code; PROTOCOL_CONTRACT_VERSION → 2026-09-25.1). With the switch on, a declined route records its reason on the bridge trace.
  • src/server/messages-native.ts: handleNativeMessages on the PF-05 primitives — one attempt, finish-once final row, spend tracker per physical send, 401/429 key rotation rebuilding the request from the builder each time, same-target 429 replay, SSE relay with the Anthropic log tap, JSON for non-stream callers. The response model echoes the selector the client sent, as the bridge does.
  • Caller-forward passthrough (the caller's own Anthropic credential) is still decided first and independently, unchanged.
  • count_tokens estimates over the body the native lane would send when the route is eligible.

Recorded gaps (devlog 040): stabilizePromptCache is a Claude-app cache optimization and does not run on the native lane; caller anthropic-beta is not forwarded (allowlist is PF-10).

Verification

  • bun x tsc --noEmit: exit 0 on this head.

  • bun run structure:check, bun run privacy:scan: passed.

  • Tests (tests/adapters/anthropic/anthropic-messages-passthrough.test.ts, tests/responses/messages-native-eligibility.test.ts, tests/responses/messages-native-bridge-policy.test.ts, tests/claude-integration/messages-native.test.ts, tests/claude-integration/messages-native-decline-trace.test.ts) were written and registered but run in the full local suite below.

  • Full local run on the stack head (feat(protocols): protocol paths as a first-class concern — PF-01..PF-12 #5820, which contains this change): bun run test — the only failures are Lab CL-03/CL-07/CL-08/SEC-02 and release helper timeouts, which fail identically on a checkout without this stack (local environment), plus service/toggle cases that pass when run alone; cd gui && bun test --isolate tests — 2398 pass, 0 fail.

  • CI on this head: all required checks pass.

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.

@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 25, 2026 04:08
@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • ^dev$
  • ^preview$

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

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

Review profile: ASSERTIVE

Plan: Advanced

Run ID: eaa0b5ad-95aa-491e-968e-7e96e21503b1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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 enhancement New feature or request label Sep 25, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a2af3afb90

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/claude/inbound.ts
if (names.length === 0) return false;
const callIds = blockedSkillCallIds(body.messages, names);
for (const msg of body.messages) {
if (!isRec(msg) || msg.role !== "user" || !Array.isArray(msg.content)) continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep string-form blocked skill bundles on the bridge

When a user message carries its content as a string rather than a block array, this condition skips it even though userMessageToItems applies maybeElideSkillText to that valid string form. With managedMessagesNative enabled, a large blocked-skill payload beginning with the skill marker is therefore judged eligible and forwarded intact, bypassing claudeCode.blockedSkills; check string content with the same elision predicate before continuing.

Useful? React with 👍 / 👎.

Comment on lines +529 to +534
const relayed = tapAnthropicSseForLog(source, logCtx, (status, meta) => {
try {
cleanupAbort();
bindUsage(logCtx.usage);
finishLog(status, undefined, meta.closeReason);
if (meta.closeReason !== "terminal") upstream.abort();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat in-stream Anthropic errors as failed attempts

When Anthropic accepts the request with HTTP 200 and later terminates the SSE stream with event: error, tapAnthropicSseForLog does not recognize that frame and invokes this callback with status 200 at EOF. The managed native request and its active attempt are consequently recorded as successful even though the caller received an error; use an error-aware stream tap or extend the existing tap to propagate the terminal error status.

AGENTS.md reference: src/AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

@chatgpt-codex-connector

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-25T04:13:01.334195Z a2af3af PR opened
ℹ️ 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.

@Ingwannu Ingwannu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head a2af3afb90241c00d7c89df4b6478125e7607189. Blocking SSE framing bug: echoRequestedModel() recognizes frames only by two adjacent LF bytes. Valid SSE may use CRLF line endings and therefore separates events with \r\n\r\n; those bytes never match frameEnd, so the scanner eventually relays the stream unchanged and the client sees the provider wire model instead of its requested selector. The stated model-echo contract must hold for both legal line-ending forms. Please parse framing with the existing SSE decoder or support LF and CRLF across chunk boundaries, and add a CRLF/chunk-split regression. The five focused builder, eligibility, bridge-policy, native integration, and decline-trace suites passed 28/28 under CPUQuota=200%, MemoryMax=4G, MemorySwapMax=0, TasksMax=128. Exact-head CI still fails in test shards and GUI gates, and lower PF layers #5814/#5815 remain blocked.

…ders

The adapter's endpoint, pinned anthropic-version and key placement move into exported
helpers so a second Messages sender can reuse them instead of restating them.
…rce body

Allowlisted source fields, the wire model and the provider's own key; no caller header
is read, so the managed lane cannot inherit caller-forward authority.
One pure rule for the ingress, count_tokens and the planner: switch, anthropic adapter,
key auth, no combo or policy, no synthetic effort or fast row, no vision preprocessing.
Modelled on native Chat: shared attempt, final log, spend tracker, proactive key pick,
401/429 key failover and replay; SSE relays through the existing Anthropic log tap.
Decided after route settlement and the managed-client steps; the caller-forward branch
keeps its own earlier decision. The reject guard judges the native path when it applies.
With the switch on and an eligible route, count_tokens estimates the allowlisted body the
native lane would send; otherwise the existing estimate is unchanged.
With the switch on, a Messages candidate is judged by the same decline rule the ingress
uses; with it off the preview is unchanged.
The builder keeps only allowlisted fields and the adapter's headers; each decline reason
is named; the planner reports native only with the switch on and a managed key.
Allowlisted body and provider key on the wire, 401/429 key failover, stream relay and
usage, JSON callers, switch-off bridge, caller-forward precedence and count_tokens.
Protocol paths owns the rule, builder and authority split; the Responses transport doc
lists the lane among the native senders; the devlog inventory records what stays bridged.
A native lane that would skip a pinned effort, skill elision or a sidecar declines with
bridge-only-policy. New reason code, so the contract version moves to 2026-09-25.1.
A pure predicate over the same inputs the translator uses, so a caller can keep a request
on the path that applies claudeCode.blockedSkills.
…lies

A pinned route effort, a blocked-skill bundle the translator would elide, or a web-search
tool the sidecar could serve now declines the native lane with bridge-only-policy.
…aude settings

The ingress, count_tokens and the key-reselection recheck pass the translated model id and
the ingress's Claude view, so the effort pin and blocked skills read what the bridge reads.
With the switch on, the bridge entry mark carries the decline reason, so the trace explains
the bridge; features are scanned once so both marks report the caller's own settings.
The planner passes the resolved selector, so a pinned route effort reports bridge-only-policy;
body-dependent policy (skills, web search) is not predictable from features.
The translated lane answers with the client's selector; the native lane now rewrites
message_start (stream) or the message (JSON) the same way instead of the wire id.
JSON and stream answers now carry the client's selector, matching the translated lane.
Pinned effort, blocked-skill elision and the web-search sidecar decline the native lane;
the planner reports the pin; a declined route's bridge trace names the reason only when on.
…odel echo

States which operator policy keeps Messages on the bridge, that stabilizePromptCache is a
recorded gap rather than a decline rule, and that native answers echo the client's selector.
@lidge-jun
lidge-jun force-pushed the feat/pf07-native-chat-combos branch from 031af07 to 6dc5f8c Compare September 25, 2026 04:32
@lidge-jun
lidge-jun force-pushed the feat/pf08-managed-messages-native branch from a2af3af to 53e4783 Compare September 25, 2026 04:32
@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 62 / 80

이 PR은 스위치 protocols.rollout.managedMessagesNative가 켜져 있을 때만 동작합니다. 기본값은 꺼져 있습니다. 켜지면, 길이 정해진 뒤 그 길이 우리 키로 붙는 Anthropic이고 직접 경로일 때, Messages 요청을 Responses로 다시 만들지 않고 Anthropic /v1/messages로 보냅니다. 본문은 허용된 칸만 남기고, 모델 이름은 실제 와이어 모델로 바꿉니다. 키는 설정에 있는 우리 키만 넣습니다. 손님이 보낸 Anthropic 키는 예전처럼 그 앞 갈래에서 따로 처리합니다.

거절 이유는 한곳으로 모았습니다. 스위치가 꺼짐, Anthropic이 아님, 키가 아님, 콤보나 정책 경로, effort 줄, fast 줄, 이미지를 미리 가공해야 함, 다리에서만 하는 운영 규칙입니다. 운영 규칙은 고정된 effort, 막힌 스킬 문서를 빼는 경우, 웹 검색 도구가 붙을 수 있는 경우입니다. 거절되면 다리로 가고, 스위치가 켜져 있으면 그 이유가 흔적에 남습니다. 답의 모델 칸에는 손님이 보낸 이름을 다시 적습니다. count_tokens도 이 길로 갈 본문을 셉니다.

베이스 브랜치는 feat/pf07-native-chat-combos입니다. #5815 위에 쌓여 있습니다. types.ts와 config.ts를 나누는 PR은 아닙니다.

src/server/messages-native.ts echoRequestedModel — 빈 줄을 찾을 때 LF 두 개(\n\n)만 봅니다. 줄 끝이 CRLF(\r\n\r\n)이면 프레임이 안 갈라져서 message_start의 모델 이름을 고치지 않습니다. 손님은 자기가 보낸 이름 대신 와이어 모델 이름을 받습니다. 테스트 스트림은 LF만 만듭니다.

src/server/messages-native.ts 스트림 응답 — 업스트림이 HTTP 200을 주고 나중에 event: error로 끊으면, 로그는 성공(200)으로 닫힙니다. 키를 바꾸지도 않습니다. 같은 파일에서 JSON으로 접는 경로는 message.type === "error"를 502로 바꿉니다. 스트림만 다릅니다. 이미 손님에게 바이트를 넘긴 뒤라 다시 보내기는 어렵습니다. 성공으로 적히는 것은 그 요청의 끝과 다릅니다.

src/server/claude-messages.ts 다리 흔적 — effort 줄이나 fast 줄이면 거절 이유가 effort-row 또는 fast-row로 두 번 들어갑니다. 자격 함수가 이미 그 이유를 돌려주고, 흔적 배열이 같은 이유를 앞에 한 번 더 붙입니다.

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

모델 이름 다시 쓰기를 LF만 보장할지, CRLF도 고칠지입니다. 이 레포의 기존 Anthropic 로그 탭도 \n\n만 나눕니다. Anthropic이 항상 LF로만 주면 지금 코드로 충분합니다. 스펙상 CRLF도 맞는 줄 끝이면, 고치기 전에 손님 화면의 모델 이름이 와이어 이름으로 남을 수 있습니다.

작성자는 이 헤드에서 새 테스트를 돌리지 않았고, CI가 첫 실행이라고 적었습니다. 보내는 파일 messages-native.ts는 702줄입니다.

이 PR의 베이스는 #5815 브랜치입니다. 그 PR이 바뀌거나 닫히면 이 diff도 같이 흔들립니다.

너의 추천

스위치 기본값은 꺼져 있습니다. 머지 전에 echoRequestedModel가 \r\n\r\n도 프레임으로 나누게 하고, 그 경우 테스트를 하나 넣으면 됩니다. 스트림 안의 event: error는 로그 상태만이라도 실패로 남기면 됩니다. 다리 흔적의 이유 코드는 한 번만 남기면 됩니다. 이 PR이 추가한 테스트 다섯 파일을 한 번 돌린 뒤에 머지하는 편이 안전합니다. types.ts·config.ts 분리 PR이 아니므로 중복으로 닫을 대상은 없습니다. #5815 다음에 두면 됩니다.

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

@devin-ai-integration devin-ai-integration Bot added the priority: P3 Low: new provider/client integration, large or experimental feature (>2000 LOC or >50 files), RFC/ro label Sep 25, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

Maintainer triage: priority: P3 — protocol-first-class series PF-08 (default off).

Criteria (P3): Low: new provider/client integration, large or experimental feature (>2000 LOC or >50 files), RFC/roadmap, or long-stale branch.

Related / overlapping PRs:

lidge-jun added a commit that referenced this pull request Sep 25, 2026
…12 (#5820)

Squash of the protocol-first-class stack #5808, #5809, #5810, #5811, #5812, #5813, #5814, #5815, #5816, #5817, #5819 and #5820. Every new lane sits behind a protocols.rollout switch that defaults off.
@lidge-jun

Copy link
Copy Markdown
Owner Author

Landed in dev as part of the single squash of the protocol-first-class stack: #5820 (0f4c8d4).

@lidge-jun lidge-jun closed this Sep 25, 2026
@lidge-jun
lidge-jun deleted the feat/pf08-managed-messages-native branch September 26, 2026 01:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request priority: P3 Low: new provider/client integration, large or experimental feature (>2000 LOC or >50 files), RFC/ro

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants