From eb661335d910f9357f80e72685dcb9265659201c Mon Sep 17 00:00:00 2001 From: JUN Date: Sun, 4 Oct 2026 10:54:08 +0900 Subject: [PATCH 1/5] docs: plan focused Claude request preservation replacements --- .../000_plan.md | 33 +++++++++++++++++++ .../010_tool_names.md | 21 ++++++++++++ .../020_native_pool.md | 24 ++++++++++++++ .../030_preferences.md | 26 +++++++++++++++ 4 files changed, 104 insertions(+) create mode 100644 devlog/_plan/261004_claude_request_preservation/000_plan.md create mode 100644 devlog/_plan/261004_claude_request_preservation/010_tool_names.md create mode 100644 devlog/_plan/261004_claude_request_preservation/020_native_pool.md create mode 100644 devlog/_plan/261004_claude_request_preservation/030_preferences.md diff --git a/devlog/_plan/261004_claude_request_preservation/000_plan.md b/devlog/_plan/261004_claude_request_preservation/000_plan.md new file mode 100644 index 00000000000..fd748feb63b --- /dev/null +++ b/devlog/_plan/261004_claude_request_preservation/000_plan.md @@ -0,0 +1,33 @@ +# Claude request preservation replacement roadmap + +Native Claude requests need consistent OAuth tool names, client compatibility and pool dispatch without losing caller content. This unit separates those runtime contracts from the preference that selects them. It refines public sources #6533, #6534 and #6547 into a manual PR chain; source closure remains coordinator-owned. + +## Loop specification +- Shape/trigger: satisfy-spec, assigned next-release Claude lane. +- Goal: focused published replacement PRs with complete source behavior accounting and real-author attribution. +- Non-goals: merges, release/version changes, installed runtime writes, account/credential changes, live upstream traffic, source closure, native stacks, unrelated work. +- Verification: focused Bun behavior tests, typecheck, structure/privacy/layout/ratchet gates; GUI tests and observed render for preference; docs build for docs changes. Full/changed local suites excluded by explicit concurrent-release resource contract; wider coverage belongs to current-head hosted CI. +- Stop: all replacement PRs published, independent review addressed, current-head CI inspected, residual native/live gaps honestly draft-bound. +- Artifacts: this numbered unit; ignored `.tmp/claude-lane/` stores source snapshots, verification logs and non-public security reasoning. +- Outcomes: DONE = publication and complete accounting; unresolved evidence stays explicit/draft; blocked capability is reported without invented success. +- Escalation: scope/authorization change or unavailable indispensable capability. Existing scoped commits/push/PR creation are authorized. +- Tool/credential scope: repository filesystem, GitHub repository reads and scoped branch/PR writes, fake/local test endpoints. No real provider credentials or paid upstream calls. +- Token/cost/wall-clock bounds: no user numeric budget supplied; do not invent one. Managed short-yield commands and bounded leaf tasks; keep work within the requested scope. + +## Baseline and topology +Base `0818ea1812a028e1c14cd0b0511b44863407bc52` from refreshed dev. Sources: #6533 `8e5c78912222b1616709d664c23de254dd71b7c9`; #6534 `829b203b3e65530a4cdaaa121f2da15484c2c9b7`; #6547 `5f3cf4ed0b4a63604133863442002fb9ac824b16`. Original author: Claire Novotny . Inherited tool/pool commits are consumed once, never blindly cherry-picked cumulatively. + +| Cycle | Output | Dependency | +| --- | --- | --- | +| wp0 | Docs-only reviewed roadmap | none | +| wp1 | B1 typed references and inline tools | roadmap | +| wp2 | B2/B3 client compatibility and pooled native dispatch | B1 | +| wp3 | B4 preference, defaults, API, GUI | B2/B3 | + +Existing owners: `src/adapters/anthropic/` shapes the wire; `src/oauth/anthropic-routing.ts` owns pool selection/health; native server modules own physical dispatch; `src/protocols/settings.ts` owns policy resolution; pool settings capability and management routes own validated writes; the existing Anthropic pool component owns UI. Reuse these paths and existing tests. No alternate pool or retry engine. + +## Consultation +Read-only architect proposal/reflection and independent audit recorded before implementation. Every later P revalidates its decade document against current parent tip. Leaves are scoped to disjoint files and never branch or run goals/FSM. + +Architect handle: 01a10493-1a9a-73d2-96f9-86ef0df9518d. Main accepts CL-A01/A05/A08; amends CL-A02 duplicate carries, CL-A03 session priority, CL-A04 lease-before-charge, CL-A06 malformed protocol salvage, CL-A07 CSS sibling and CL-G01–G04 route race/raw malformed/help/preview coverage. Reflection requested on amended roadmap. +Architect reflection on amended roadmap: ALIGNED; all CL-A02/A03/A04/A06/A07 and CL-G01–G04 resolved. Independent audit remains required. No implementation evidence claimed. diff --git a/devlog/_plan/261004_claude_request_preservation/010_tool_names.md b/devlog/_plan/261004_claude_request_preservation/010_tool_names.md new file mode 100644 index 00000000000..399013efffc --- /dev/null +++ b/devlog/_plan/261004_claude_request_preservation/010_tool_names.md @@ -0,0 +1,21 @@ +# B1 — Typed tool references and inline declarations + +Depends on the roadmap only. Branch `codex/next-release-261004-claude-tools`, PR base dev. Carry #6533 in full and the inline-tool portion of #6534/#6547. Identity/beta/preamble and pooling remain later layers. + +## Exact change map +- MODIFY `src/adapters/anthropic/passthrough.ts`: within `anthropicOAuthWireBody`, replace top-level tool-use-only mapping with copy-on-write typed block traversal. Carry #6533 diff plus the inline-name hunks of public commit `e3eefbaedf5e0ba598c5d443ce16e21d94a9f378`. Collect inline `tool_addition.tool.type=tool_definition` client definitions before mapping so preceding references resolve. Rewrite declared custom tool `tool_use.name`, `tool_reference.tool_name`, inline addition/removal `tool.type=tool_reference` names, and inline client definition names. Traverse only message content and typed tool-result content. Preserve arbitrary schemas, arguments, cache markers and unknown blocks. Reject ambiguous wire-name collisions, including typed versus client declarations. +- MODIFY `tests/adapters/anthropic/anthropic-messages-passthrough-oauth.test.ts`: preserve six existing regressions; carry nested-reference and inline-declaration source cases; add targeted ordering, typed collision and opaque-container negatives within ratchet headroom, otherwise register a sibling domain file in both layout maps. +- MODIFY `structure/data-planes/protocol-paths.md`: native OAuth paragraph documents typed reference/inline naming, exact opaque preservation and copy-on-write behavior. +- MODIFY `docs-site/src/content/docs/guides/claude-code.md`: small native tool compatibility note, without claiming pool/default behavior before later layers. + +## Observable acceptance +Declared lookup in nested tool-result content becomes custom_lookup; undeclared and typed builtins stay unchanged. Inline declaration after an earlier reference still prefixes consistently. Inline removal names match declaration. Source body JSON is unchanged; opaque tool input/schema and text/cache TTL/scope remain identical. A collision is refused before dispatch. Key auth gets no OAuth shaping. Tests fail against original behavior then pass with the patch. No network request. + +## Verification +`bun test tests/adapters/anthropic/anthropic-messages-passthrough-oauth.test.ts` reads the target directly; baseline six tests pass after frozen dependency installation. `bun run typecheck`, `bun run structure:check`, `bun run privacy:scan`, `bun test tests/test-layout.test.ts tests/test-layout-tooling.test.ts tests/ci-workflows/file-size-ratchet.test.ts`; docs build per docs-site instructions. Source/public patch anchors are fixed above. Human semantic review proves prose scope, not a phrase-presence test. + +## Source accounting +#6533 all behavior carried. #6534/#6547 inline naming carried here; client compatibility, pool and preferences remain unresolved until their own layers. Include `Co-authored-by: Claire Novotny ` in commit and PR body. + +## B1 focused source review dispositions +Accept independent tool review: collect both top-level and inline typed names before any rename; reject original-name ambiguity and typed wire-name collisions independent of declaration order. Register all client declarations in the prefix collision map before walking uses. Follow only the same typed content containers in both collection and mapping so nested inline definitions cannot silently escape while their neighbors are rewritten; this is conservative structural support, not a claim that upstream accepts nested inline definitions. Add cross-message ordering, inline choice/use, undeclared/typed references, and schema/input/unknown-container opacity regressions. These are bounded refinements within B1's existing collision and typed-container contract. diff --git a/devlog/_plan/261004_claude_request_preservation/020_native_pool.md b/devlog/_plan/261004_claude_request_preservation/020_native_pool.md new file mode 100644 index 00000000000..273ffe074df --- /dev/null +++ b/devlog/_plan/261004_claude_request_preservation/020_native_pool.md @@ -0,0 +1,24 @@ +# B2/B3 — Native client compatibility and pooled dispatch + +Depends on B1. Publish `codex/next-release-261004-claude-native` as manual child of tools. Combining client compatibility and dispatch is intentional: selecting native for a real pooled client without preserving its identity/preamble/betas would admit a request the replacement cannot faithfully send. No preference/default changes in this layer. + +## Exact change map +Use the complete public #6534 diff at `829b203b3e65530a4cdaaa121f2da15484c2c9b7`, excluding B1 hunks already carried, The equivalent #6547 commits `c46145193613d54574c779806192e8ff4f19ace5` (dispose refusal before rebind) and `5f3cf4ed0b4a63604133863442002fb9ac824b16` (blank session fallback) corroborate behavior already in the pinned #6534 head and are not applied again. Adapt to current routing/refusal owners, never restore obsolete ownership from a source patch. + +- MODIFY `src/adapters/anthropic/client-identity.ts`: recognize bounded observed CLI/Desktop Code compatibility bundles; keep WeakMap-backed opaque handles, header exclusion, first-party-only forwarding and operator header precedence. Expose observed-identity predicate for native preamble/beta consumers. +- NEW `src/adapters/anthropic/native-client-preamble.ts`: carry the public source helper that recognizes leading billing plus Claude identity preambles only with an observed native client identity. Preserve source block objects and cache markers; otherwise existing synthetic prefix remains. +- MODIFY `src/adapters/anthropic/beta-allowlist.ts`: add only source-listed feature betas for recognized first-party native clients; retain compatible-host and unrecognized-client deny behavior. +- MODIFY `src/adapters/anthropic/passthrough.ts`: add request-local clientIdentity option to native wire-body builder and OAuth shaping; feed identity predicate to beta allowlist; B1 names remain unchanged. +- MODIFY `src/oauth/anthropic-routing.ts`: reuse shared session-key construction for native ingress, with existing priority: nonblank metadata conversation ID, then session header (`session_id`, then Claude session header), then thread header, then eligible cache-key fallback. Preserve bounded hashing and no raw identifiers in logs. +- MODIFY `src/server/claude-messages.ts`: capture native request session key and client identity at ingress; pass to native dispatch and token counting. Keep existing route/feature guards and caller-forward behavior. +- MODIFY `src/server/messages-native-eligibility.ts`: remove pool/quorum-only declines behind explicit native rollout. Keep provider, destination, tools/proxy processing and feature eligibility guards. +- MODIFY `src/server/messages-native-oauth.ts`: resolve binding with model/session/tried-account scope through existing account-routing and commit owners. Preserve selected UUID plus generation, pause/health eligibility and concurrent-selection fences. Remove obsolete pool-decline-only guard; do not invent separate selection/failover policy. +- MODIFY `src/server/messages-native.ts`: carry bounded pre-output shared refusal classification/recovery for OAuth; cancel rejected response before refresh/rebind; rebuild from original caller body with replacement exact UUID/generation; preserve physical-send budgets and cancellation. Acquire family revalidation lease before spend/send accounting; release on budget refusal, abort and transport failure. Add denied-lease test proving no send charge/record. Re-evaluate model-route policy if it changes while binding is asynchronous. No replay after output. Capture account identity before recording refusal, so a race cannot blame the replacement. Propagate route/session into every binding check. +- MODIFY `tests/adapters/anthropic/anthropic-account-pool.test.ts`, `anthropic-beta-allowlist.test.ts`, `anthropic-client-identity.test.ts`, `anthropic-messages-passthrough-oauth.test.ts`: carry source compatibility/session cases without duplicating B1. +- MODIFY `tests/claude-integration/messages-native-oauth.test.ts`, `tests/responses/messages-native-oauth-eligibility.test.ts`: carry all source loopback dispatch/recovery/identity tests; strengthen model-route exclusion, pause/generation races and post-output no-replay where missing. +- MODIFY `structure/data-planes/protocol-paths.md`, `structure/providers/anthropic-account-pool.md`, `docs-site/src/content/docs/guides/claude-code.md`: replace obsolete pool exclusion with explicit-rollout native contract; no default-on claim yet. + +## Acceptance and verification +Real local fake endpoints exercise two usable accounts, sticky session, model route, eligible shared-quota 429 and classified account 403 before output, bounded attempt exhaustion, refusal cancellation before rebind, and partial-output termination without another physical send. Manual selection, pause, generation or UUID change during async preparation must not emit stale binding. Opaque content/cache markers are equal across rebuild except declared tool/account metadata fields. CLI/Desktop preamble and beta cases use fabricated compatibility headers only. Token counting and preview select same body/lane as runtime without refreshing or selecting credentials. + +Run focused test files above plus existing native bridge/opaque-state tests, `bun run typecheck`, structure/privacy/layout/ratchet gates and docs build. Use fake isolated OAuth homes; no credential or live-provider access. Source regressions may need sibling test modules to respect downward ratchets, registered in both layout inventories. Security review is independent; unshipped discoveries stay ignored scratch. diff --git a/devlog/_plan/261004_claude_request_preservation/030_preferences.md b/devlog/_plan/261004_claude_request_preservation/030_preferences.md new file mode 100644 index 00000000000..06d1e3c1627 --- /dev/null +++ b/devlog/_plan/261004_claude_request_preservation/030_preferences.md @@ -0,0 +1,26 @@ +# B4 — Pool native preference, API and UI + +Depends on B2/B3. Publish `codex/next-release-261004-claude-settings` as a manual child of native. This layer carries #6547 settings behavior while correcting precedence: an explicit operator false rollout remains false. Source tests asserting false becomes true are deliberately replaced by precedence regressions. + +## Field chain and exact changes +The new optional `anthropicAccountPool.nativeMessages` boolean is created by API/UI, serialized by existing config persistence, parsed by schema/config diagnostics, resolved by protocol settings and consumed by eligibility, preview, count_tokens and runtime. DTO/UI normalization must preserve false. + +- MODIFY `src/types/config.ts`: add optional nativeMessages boolean to existing Anthropic pool type. +- MODIFY `src/config/schema/config-schema.ts` and `src/config/diagnostics.ts`: source #6547 conservative malformed-field salvage and strict validated-write diagnostics; malformed field becomes false without dropping providers or sibling pool settings. Preserve explicit native false when a malformed sibling would otherwise drop the entire protocols object; file-load-to-resolver regressions must prove this. Malformed protocol containers retain a conservative disabled native policy after salvage. +- MODIFY `src/protocols/settings.ts`: extend accepted config projection and optional provider scope. An enabled Anthropic pool supplies true only for absent native rollout values; explicit false or malformed present rollout is off. Pool nativeMessages=false disables pooled native selection. Pool-off keeps explicit legacy rollout behavior; another provider never inherits Anthropic default. Invalid protocols/rollout containers fail closed. Native OAuth cannot be true when managed native is false. Include effective/default/provider inputs in policy revision so preview becomes stale after a relevant change. +- MODIFY `src/protocols/plan-snapshot.ts`, `src/server/messages-native-eligibility.ts`, `src/server/claude-messages.ts`: pass settled provider consistently into policy resolver at preview/count/runtime ingress; keep admission and source identity behavior from parent. +- MODIFY `src/oauth/pool-settings-capability.ts`: add Anthropic-only writable capability and nullable DTO field; other providers expose null. Resolve absent preference true but malformed false consistently with config boundary. +- MODIFY `src/server/management/oauth-account-routes.ts`: both unified and legacy routes accept boolean nativeMessages only for Anthropic; reject malformed/provider-inapplicable writes before mutation. Patch latest persisted config through existing atomic mutation owner, preserve unrelated concurrent changes, update live config only after save succeeds. Preserve legacy validation/default/routes null-clear behavior. Return useful non-sensitive save failures. +- MODIFY `gui/src/pool-settings.ts`: request/response type, Anthropic-only normalization, false-preserving 204 fallback and request construction. +- MODIFY `gui/src/components/provider-workspace/AnthropicAccountPoolSettings.tsx`: source #6547 checkbox inside existing details, accurate help that explicit rollout opt-outs still apply, shared save payload across every pool control, persisted opt-out on reload, visible save failure and revert behavior. Disabled while loading/saving as existing controls require. +- NEW `gui/src/styles/anthropic-native-messages.css`: source checkbox/copy alignment using existing tokens; import from component. Main stylesheet has only two lines of ratchet headroom. +- MODIFY `gui/src/i18n/{de,en,fr,ja,ko,pt,ru,tr,vi,zh-TW,zh}.ts`: source strings, amended as needed so explicit rollout overrides are not misrepresented. +- MODIFY `gui/tests/account-pool-strategy.test.tsx`, `gui/tests/anthropic-pool-conditions.test.tsx`, `gui/tests/anthropic-pool-quota-window.test.tsx`: DTO/default/204 tests, opt-out save/reload, failed save visible, controls retain native preference. +- MODIFY `tests/config/config-load-degrade.test.ts`, `tests/config/protocol-settings.test.ts`, `tests/responses/messages-native-eligibility.test.ts`, `tests/server/account-pool-management-api.test.ts`, `tests/responses/protocol-plan-snapshot.test.ts`: source malformed salvage/atomic write tests and corrected precedence truth table, provider scope, revision/preview/count/runtime agreement. +- MODIFY `docs-site/src/content/docs/guides/claude-code.md`, `reference/configuration/providers.md`, `reference/configuration/server.md`, `structure/providers/anthropic-account-pool.md` and affected protocol contract: default only when no explicit opt-out; pool-off rules; cache remains account-specific. Review translated pages and amend contradictory statements. + +## Activation matrix +Pool absent/disabled + flags absent -> off. Pool enabled + flags absent + preference absent/true -> eligible Anthropic native on. Each explicit false flag wins; malformed flag or container fails closed. Preference false wins over explicit true flags for pooled routes. Other providers retain their own rollout. Invalid native preference -> false without losing providers. Raw malformed preference/rollout input is tested in resolver and DTO, not only parsed-file tests. Both API routes reject wrong provider/nonboolean; persistence failure leaves live config unchanged and UI shows error; concurrent unrelated disk edit survives. 204 false write remains false. Preview revision changes when effective policy changes; count sends no requests. GUI checkbox toggles and reload reflects actual saved choice. + +## Verification and publication +Focused backend/GUI tests, root typecheck and GUI lint/typecheck, structure/privacy/layout/ratchet; docs build. Observe local rendered checkbox and first state change with mocked endpoints and fake account data; capture screenshot outside PR branch and publish via pr-assets (or reuse source image only if accurate). Every source hunk is carried/amended/dropped explicitly; all three layers carry Claire Novotny attribution. Current-head hosted checks inspected, broad/native/live missing evidence remains draft-bound. No merge or source closure. From 17632e30f0877d6fadcbca6790e4f2f07c82d694 Mon Sep 17 00:00:00 2001 From: JUN Date: Sun, 4 Oct 2026 11:01:43 +0900 Subject: [PATCH 2/5] fix(anthropic): preserve declared native OAuth tool references Carry deferred and inline tool naming with declaration-first collision checks and opaque copy-on-write preservation. Co-authored-by: Claire Novotny --- .../src/content/docs/guides/claude-code.md | 4 + src/adapters/anthropic/passthrough.ts | 98 +++++-- structure/data-planes/protocol-paths.md | 5 + ...thropic-messages-passthrough-oauth.test.ts | 268 ++++++++++++++++++ 4 files changed, 356 insertions(+), 19 deletions(-) diff --git a/docs-site/src/content/docs/guides/claude-code.md b/docs-site/src/content/docs/guides/claude-code.md index 673898b12fd..9fded76404b 100644 --- a/docs-site/src/content/docs/guides/claude-code.md +++ b/docs-site/src/content/docs/guides/claude-code.md @@ -93,6 +93,10 @@ Operational contract when enabled: See [Configuration](/reference/configuration/providers/#anthropicaccountpool-experimental). +For managed native OAuth requests, declared custom tools use consistent names across deferred +references and inline additions or removals. Tool arguments, schemas and cache markers are retained. +Ambiguous tool declarations are rejected before sending a request. + ## Quickstart ```bash diff --git a/src/adapters/anthropic/passthrough.ts b/src/adapters/anthropic/passthrough.ts index 00af4870fcc..94d1c8cd40c 100644 --- a/src/adapters/anthropic/passthrough.ts +++ b/src/adapters/anthropic/passthrough.ts @@ -106,12 +106,36 @@ function isClientTool(tool: unknown): tool is Rec & { name: string } { return isRec(tool) && typeof tool.name === "string" && (tool.type === undefined || tool.type === "custom"); } +/** Allocate only once the first changed item is encountered. */ +function mapPreservingIdentity(items: T[], mapItem: (item: T) => T): T[] { + let out: T[] | undefined; + for (let i = 0; i < items.length; i++) { + const item = items[i]!; + const mapped = mapItem(item); + if (mapped !== item && !out) out = items.slice(0, i); + if (out) out.push(mapped); + } + return out ?? items; +} + +/** Collection and rewriting follow exactly the same typed content containers. */ +function mapOAuthContentBlocks(blocks: unknown[], mapBlock: (block: Rec) => Rec): unknown[] { + return mapPreservingIdentity(blocks, block => { + if (!isRec(block)) return block; + if (block.type === "tool_result" && Array.isArray(block.content)) { + const content = mapOAuthContentBlocks(block.content, mapBlock); + if (content !== block.content) block = { ...block, content }; + } + return mapBlock(block); + }); +} + /** * The Claude OAuth request shape the adapter produces, applied to a Messages body: the Claude * Code identity as the first system block, and declared client tool names under the OAuth - * prefix — in `tools`, a named `tool_choice` and the history's `tool_use` blocks. Copy-on-write; - * the input is not mutated. Two caller names that meet under the prefix are refused rather than - * guessed at. + * prefix — in declarations, choices, uses and typed references, including tool-result content. + * Copy-on-write; arguments, schemas, unknown containers and cache markers stay opaque. + * Ambiguous original or wire names are refused before any history is rewritten. */ export function anthropicOAuthWireBody(body: Rec): { body: Rec; toolNames: Map } { const out: Rec = { ...body }; @@ -135,29 +159,65 @@ export function anthropicOAuthWireBody(body: Rec): { body: Rec; toolNames: Map(); + const typedNames = new Set(); + const inlineTypedNames = new Set(); + const collectDeclaration = (tool: unknown, inline = false): void => { + if (isClientTool(tool)) declared.add(tool.name); + else if (isRec(tool) && typeof tool.name === "string") { + typedNames.add(tool.name); + if (inline) inlineTypedNames.add(tool.name); + } + }; + if (Array.isArray(body.tools)) body.tools.forEach(tool => collectDeclaration(tool)); + if (Array.isArray(body.messages)) { + for (const message of body.messages) { + if (!isRec(message) || !Array.isArray(message.content)) continue; + mapOAuthContentBlocks(message.content, block => { + if (block.type === "tool_addition" && isRec(block.tool) && block.tool.type === "tool_definition") { + collectDeclaration(block.tool.definition, true); + } + return block; + }); + } + } + for (const name of declared) { + if (typedNames.has(name)) throw new Error(inlineTypedNames.has(name) + ? "inline typed and client tool names collide" : "typed and client tool names collide"); + if (typedNames.has(applyClaudeToolPrefix(name))) throw new Error("typed and client wire tool names collide"); + wireName(name); + } + const renames = (name: unknown): name is string => typeof name === "string" && declared.has(name) && applyClaudeToolPrefix(name) !== name; + const mapDeclaration = (tool: unknown): unknown => isClientTool(tool) && renames(tool.name) + ? { ...tool, name: wireName(tool.name) } : tool; if (Array.isArray(body.tools)) { - out.tools = body.tools.map(tool => { - if (!isClientTool(tool)) return tool; - declared.add(tool.name); - return { ...tool, name: wireName(tool.name) }; - }); + out.tools = mapPreservingIdentity(body.tools, mapDeclaration); } - const renames = (name: unknown): name is string => typeof name === "string" && declared.has(name); if (isRec(body.tool_choice) && body.tool_choice.type === "tool" && renames(body.tool_choice.name)) { out.tool_choice = { ...body.tool_choice, name: wireName(body.tool_choice.name) }; } - const isRenamedUse = (block: unknown): block is Rec & { name: string } => isRec(block) && block.type === "tool_use" && renames(block.name); + const mapBlock = (block: Rec): Rec => { + if (block.type === "tool_use" && renames(block.name)) return { ...block, name: wireName(block.name) }; + if (block.type === "tool_reference" && renames(block.tool_name)) return { ...block, tool_name: wireName(block.tool_name) }; + if ((block.type === "tool_addition" || block.type === "tool_removal") && isRec(block.tool)) { + const tool = block.tool; + if (tool.type === "tool_reference" && renames(tool.name)) { + return { ...block, tool: { ...tool, name: wireName(tool.name) } }; + } + if (block.type === "tool_addition" && tool.type === "tool_definition") { + const definition = mapDeclaration(tool.definition); + if (definition !== tool.definition) return { ...block, tool: { ...tool, definition } }; + } + } + return block; + }; if (Array.isArray(body.messages)) { - out.messages = body.messages.map(message => { - if (!isRec(message) || !Array.isArray(message.content) || !message.content.some(isRenamedUse)) return message; - return { - ...message, - content: message.content.map(block => isRenamedUse(block) ? { ...block, name: wireName(block.name) } : block), - }; + out.messages = mapPreservingIdentity(body.messages, message => { + if (!isRec(message) || !Array.isArray(message.content)) return message; + const content = mapOAuthContentBlocks(message.content, mapBlock); + return content !== message.content ? { ...message, content } : message; }); } return { body: out, toolNames }; diff --git a/structure/data-planes/protocol-paths.md b/structure/data-planes/protocol-paths.md index 3206e5acfe8..1798a3a84d5 100644 --- a/structure/data-planes/protocol-paths.md +++ b/structure/data-planes/protocol-paths.md @@ -352,6 +352,11 @@ The local pool id is never used; malformed, absent and unknown metadata stays un Every rebuild starts from the source body; the binding also checks UUID equality before send. Conflicting provider credential headers fail before dispatch on every OAuth build, including builds without a provider UUID. +Native OAuth Messages collect top-level and typed inline tool declarations before rewriting declared +client names in tool choices, uses, references, additions and removals, including typed tool-result +content. Typed builtin names stay fixed; ambiguous original or wire-name collisions are refused. +The copy-on-write traversal leaves input schemas, tool arguments, unknown containers and cache markers +(including lifetimes) opaque. Nested inline support is structural; it does not assert upstream acceptance. Key-auth and caller-forward requests retain their metadata. The answer's `tool_use` names are mapped back for exactly those names. A 401 or 429 is answered as the bridge answers an unpooled account: no refresh replay, no same-token replay, no rotation. diff --git a/tests/adapters/anthropic/anthropic-messages-passthrough-oauth.test.ts b/tests/adapters/anthropic/anthropic-messages-passthrough-oauth.test.ts index 663fd325295..0d105297e06 100644 --- a/tests/adapters/anthropic/anthropic-messages-passthrough-oauth.test.ts +++ b/tests/adapters/anthropic/anthropic-messages-passthrough-oauth.test.ts @@ -50,6 +50,34 @@ const SOURCE = { ], }; +// Exact deferred-reference fixture from public #6533 (f7175bfb1e). +const DEFERRED_SOURCE = { + model: "selector", + max_tokens: 64, + stream: true, + system: "fixture system", + tools: [ + { name: "lookup", description: "fixture", input_schema: { type: "object", properties: {} } }, + { type: "web_search_20250305", name: "web_search" }, + { type: "bash_20250124", name: "bash" }, + ], + tool_choice: { type: "tool", name: "lookup" }, + messages: [ + { role: "user", content: "fixture question" }, + { role: "assistant", content: [ + { type: "tool_use", id: "toolu_1", name: "lookup", input: { opaque: { type: "tool_reference", tool_name: "lookup" } } }, + { type: "tool_use", id: "toolu_2", name: "bash", input: { command: "true" } }, + ] }, + { role: "user", content: [ + { type: "tool_result", tool_use_id: "toolu_1", content: [ + { type: "text", text: "fixture result", cache_control: { type: "ephemeral", ttl: "1h", scope: "turn" } }, + { type: "tool_reference", tool_name: "lookup" }, + ] }, + { type: "tool_result", tool_use_id: "toolu_2", content: "" }, + ] }, + ], +}; + describe("buildAnthropicMessagesPassthroughRequest with OAuth", () => { test("places the credential and fingerprint exactly as the adapter does", async () => { const built = buildAnthropicMessagesPassthroughRequest(oauthProvider(), "claude-wire", SOURCE); @@ -119,9 +147,249 @@ describe("buildAnthropicMessagesPassthroughRequest with OAuth", () => { expect(() => anthropicOAuthWireBody(body)).toThrow("collide"); }); + // Inline-name regressions from #6534/#6547 (e3eefbaedf). + test("renames inline tool additions before references and removals without mutating source or cache metadata", () => { + const body = { + tools: [{ name: "lookup", input_schema: { type: "object", properties: {} } }], + messages: [{ role: "assistant", content: [ + { type: "tool_addition", tool: { type: "tool_reference", name: "ReadNotifications" }, cache_control: { type: "ephemeral", ttl: "1h" } }, + { type: "tool_addition", tool: { type: "tool_definition", definition: { + name: "ReadNotifications", input_schema: { type: "object", properties: { q: { type: "string" } } }, + } } }, + { type: "tool_removal", tool: { type: "tool_reference", name: "ReadNotifications" } }, + { type: "tool_addition", tool: { type: "tool_reference", name: "lookup" } }, + { type: "tool_removal", tool: { type: "tool_reference", name: "lookup" } }, + { type: "text", text: "keep cache", cache_control: { type: "ephemeral", ttl: "1h", scope: "turn" } }, + ] }], + }; + const original = structuredClone(body); + const shaped = anthropicOAuthWireBody(body); + const blocks = (shaped.body.messages as { content: Record[] }[])[0]!.content; + expect((blocks[0]!.tool as Record).name).toBe("custom_ReadNotifications"); + expect(blocks[0]!.cache_control).toEqual({ type: "ephemeral", ttl: "1h" }); + expect((((blocks[1]!.tool as Record).definition as Record).name)).toBe("custom_ReadNotifications"); + expect((blocks[2]!.tool as Record).name).toBe("custom_ReadNotifications"); + expect((blocks[3]!.tool as Record).name).toBe("custom_lookup"); + expect((blocks[4]!.tool as Record).name).toBe("custom_lookup"); + expect(blocks[5]!.cache_control).toEqual({ type: "ephemeral", ttl: "1h", scope: "turn" }); + expect([...shaped.toolNames]).toEqual([ + ["custom_lookup", "lookup"], + ["custom_ReadNotifications", "ReadNotifications"], + ]); + expect(body).toEqual(original); + }); + + test("typed inline builtin definitions and their references keep their fixed names", () => { + const definition = { type: "bash_20250124", name: "bash", input_schema: { properties: { scope: { type: "string" } } } }; + const content = [{ type: "tool_addition", tool: { type: "tool_definition", definition } }, + { type: "tool_removal", tool: { type: "tool_reference", name: "bash" } }]; + const source = { ...SOURCE, tools: [], messages: [{ role: "system", content }] }; + const built = buildAnthropicMessagesPassthroughRequest(oauthProvider(), "m", source); + expect(built.wireBody.messages).toEqual(source.messages); + expect(built.oauthToolNames?.size).toBe(0); + const collision = { ...source, tools: [{ name: "bash", input_schema: {} }] }; + expect(() => buildAnthropicMessagesPassthroughRequest(oauthProvider(), "m", collision)).toThrow("inline typed and client tool names collide"); + expect(definition.name).toBe("bash"); + }); + test("a key-auth provider gets no OAuth shaping", () => { const shaped = anthropicMessagesNativeWireBody({ baseUrl: "https://api.anthropic.com", authMode: "key" }, "m", SOURCE); expect(shaped.oauthToolNames).toBeUndefined(); expect(shaped.wireBody.system).toBe("fixture system"); }); }); + +describe("native OAuth typed tool-name preservation", () => { + test("#6533 nested reference fixture preserves opaque input and cache metadata", () => { + const before = JSON.stringify(DEFERRED_SOURCE); + const shaped = anthropicOAuthWireBody(DEFERRED_SOURCE); + const messages = shaped.body.messages as typeof DEFERRED_SOURCE.messages; + const content = messages[2]!.content as { content: unknown[] }[]; + expect(content[0]!.content).toEqual([ + { type: "text", text: "fixture result", cache_control: { type: "ephemeral", ttl: "1h", scope: "turn" } }, + { type: "tool_reference", tool_name: "custom_lookup" }, + ]); + const uses = messages[1]!.content as { input: unknown }[]; + expect(uses[0]!.input).toBe((DEFERRED_SOURCE.messages[1]!.content as { input: unknown }[])[0]!.input); + expect([...shaped.toolNames]).toEqual([["custom_lookup", "lookup"]]); + expect(JSON.stringify(DEFERRED_SOURCE)).toBe(before); + }); + + test("collects later cross-message inline declarations before choices, uses and references", () => { + const source = { tool_choice: { type: "tool", name: "later" }, messages: [ + { role: "assistant", content: [ + { type: "tool_use", name: "later", id: "toolu_later", input: {} }, + { type: "tool_reference", tool_name: "later" }, + { type: "tool_removal", tool: { type: "tool_reference", name: "later" } }, + ] }, + { role: "user", content: [{ type: "tool_addition", tool: { type: "tool_definition", definition: { + type: "custom", name: "later", input_schema: { type: "object" }, + } } }] }, + ] }; + const before = JSON.stringify(source); + const shaped = anthropicOAuthWireBody(source); + expect(shaped.body.tool_choice).toEqual({ type: "tool", name: "custom_later" }); + expect(shaped.body.messages).toEqual([ + { role: "assistant", content: [ + { type: "tool_use", name: "custom_later", id: "toolu_later", input: {} }, + { type: "tool_reference", tool_name: "custom_later" }, + { type: "tool_removal", tool: { type: "tool_reference", name: "custom_later" } }, + ] }, + { role: "user", content: [{ type: "tool_addition", tool: { type: "tool_definition", definition: { + type: "custom", name: "custom_later", input_schema: { type: "object" }, + } } }] }, + ]); + expect([...shaped.toolNames]).toEqual([["custom_later", "later"]]); + expect(JSON.stringify(source)).toBe(before); + }); + + test("collects and maps declarations through the same nested tool-result containers", () => { + const content = [ + { type: "tool_reference", tool_name: "nested" }, + { type: "tool_result", tool_use_id: "toolu_outer", content: [ + { type: "tool_result", tool_use_id: "toolu_inner", content: [ + { type: "tool_addition", tool: { type: "tool_definition", definition: { name: "nested", input_schema: {} } } }, + { type: "tool_use", name: "nested", input: {} }, + { type: "tool_removal", tool: { type: "tool_reference", name: "nested" } }, + ] }, + ] }, + ]; + const before = JSON.stringify(content); + const shaped = anthropicOAuthWireBody({ messages: [{ role: "user", content }] }); + expect(shaped.body.messages).toEqual([{ role: "user", content: [ + { type: "tool_reference", tool_name: "custom_nested" }, + { type: "tool_result", tool_use_id: "toolu_outer", content: [ + { type: "tool_result", tool_use_id: "toolu_inner", content: [ + { type: "tool_addition", tool: { type: "tool_definition", definition: { name: "custom_nested", input_schema: {} } } }, + { type: "tool_use", name: "custom_nested", input: {} }, + { type: "tool_removal", tool: { type: "tool_reference", name: "custom_nested" } }, + ] }, + ] }, + ] }]); + expect(JSON.stringify(content)).toBe(before); + }); + + for (const kind of ["original", "wire"] as const) { + for (const typedInline of [false, true]) { + for (const clientInline of [false, true]) { + for (const reverse of [false, true]) { + test(`rejects ${kind} collision: typed inline=${typedInline}, client inline=${clientInline}, reverse=${reverse}`, () => { + const client = { name: "lookup", input_schema: {} }; + const typed = { type: "bash_20250124", name: kind === "original" ? "lookup" : "custom_lookup" }; + const entries = [ + { inline: clientInline, definition: client }, + { inline: typedInline, definition: typed }, + ]; + if (reverse) entries.reverse(); + const source = { + tools: entries.filter(e => !e.inline).map(e => e.definition), + messages: [{ role: "user", content: entries.filter(e => e.inline).map(e => ({ + type: "tool_addition", tool: { type: "tool_definition", definition: e.definition }, + })) }], + }; + const before = JSON.stringify(source); + expect(() => anthropicOAuthWireBody(source)).toThrow("collide"); + expect(JSON.stringify(source)).toBe(before); + }); + } + } + } + } + + for (const reverse of [false, true]) { + test(`registers unused inline client names for prefix collisions: reverse=${reverse}`, () => { + const names = reverse ? ["custom_lookup", "lookup"] : ["lookup", "custom_lookup"]; + const source = { messages: names.map(name => ({ role: "user", content: [{ + type: "tool_addition", tool: { type: "tool_definition", definition: { name, input_schema: {} } }, + }] })) }; + expect(() => anthropicOAuthWireBody(source)).toThrow("collide"); + }); + } + + test("nested typed declarations also reject original and wire collisions", () => { + for (const name of ["lookup", "custom_lookup"]) { + const source = { tools: [{ name: "lookup", input_schema: {} }], messages: [{ role: "user", content: [{ + type: "tool_result", tool_use_id: "toolu_outer", content: [{ + type: "tool_addition", tool: { type: "tool_definition", definition: { type: "bash_20250124", name } }, + }], + }] }] }; + expect(() => anthropicOAuthWireBody(source)).toThrow("collide"); + } + }); + + test("typed and undeclared uses, references and inline references retain identity", () => { + const content = ["bash", "missing"].flatMap(name => [ + { type: "tool_use", name, input: {} }, + { type: "tool_reference", tool_name: name }, + { type: "tool_addition", tool: { type: "tool_reference", name } }, + { type: "tool_removal", tool: { type: "tool_reference", name } }, + ]); + const tools = [{ type: "bash_20250124", name: "bash" }]; + const messages = [{ role: "user", content: [{ type: "tool_result", tool_use_id: "toolu_1", content }] }]; + const tool_choice = { type: "tool", name: "bash" }; + const shaped = anthropicOAuthWireBody({ tools, messages, tool_choice }); + expect(shaped.body.messages).toBe(messages); + expect(shaped.body.tools).toBe(tools); + expect(shaped.body.tool_choice).toBe(tool_choice); + expect(shaped.toolNames.size).toBe(0); + }); + + test("keeps schemas, arguments, unknown containers and cache markers opaque with copy-on-write", () => { + const hidden = { type: "tool_addition", tool: { type: "tool_definition", definition: { name: "hidden", input_schema: {} } } }; + const payload = { type: "tool_reference", tool_name: "lookup", content: [hidden] }; + const cache = { type: "ephemeral", ttl: "1h", scope: "turn" }; + const schema = { type: "object", properties: { value: { const: payload } }, content: [hidden] }; + const unknown = { type: "future_container", content: [payload, hidden] }; + const text = { type: "text", text: "literal lookup", cache_control: cache, content: [hidden] }; + const use = { type: "tool_use", name: "lookup", input: payload, content: [hidden], cache_control: cache }; + const definition = { name: "inline", input_schema: schema, cache_control: cache }; + const addition = { type: "tool_addition", tool: { type: "tool_definition", definition, content: [hidden] }, cache_control: cache }; + const nested = { type: "tool_result", tool_use_id: "toolu_1", content: [use, unknown, text], cache_control: cache }; + const content = [nested, addition, { type: "tool_reference", tool_name: "hidden" }]; + const untouched = { role: "user", content: "unchanged" }; + const source = { tools: [{ name: "lookup", input_schema: schema, cache_control: cache }], messages: [{ role: "user", content }, untouched] }; + const before = JSON.stringify(source); + const shaped = anthropicOAuthWireBody(source); + const messages = shaped.body.messages as { content: Record[] }[]; + const blocks = messages[0]!.content; + const inner = blocks[0]!.content as Record[]; + expect(inner[0]!.name).toBe("custom_lookup"); + expect(inner[0]!.input).toBe(payload); + expect(inner[0]!.content).toBe(use.content); + expect(inner[1]).toBe(unknown); + expect(inner[2]).toBe(text); + expect(blocks[2]).toBe(content[2]); + expect(messages[1]).toBe(untouched); + expect(blocks[0]!.cache_control).toBe(cache); + expect(inner[0]!.cache_control).toBe(cache); + const wireTool = (shaped.body.tools as Record[])[0]!; + expect(wireTool.input_schema).toBe(schema); + expect(wireTool.cache_control).toBe(cache); + const wireInline = blocks[1]!.tool as Record; + expect(wireInline.content).toBe(addition.tool.content); + expect((wireInline.definition as Record).input_schema).toBe(schema); + expect((wireInline.definition as Record).cache_control).toBe(cache); + expect(blocks[1]!.cache_control).toBe(cache); + expect([...shaped.toolNames]).toEqual([["custom_lookup", "lookup"], ["custom_inline", "inline"]]); + expect(JSON.stringify(source)).toBe(before); + }); + + test("already-prefixed client declarations and histories require no copies", () => { + const tools = [{ type: "custom", name: "custom_lookup", input_schema: {} }]; + const messages = [{ role: "assistant", content: [{ type: "tool_use", name: "custom_lookup", input: {} }] }]; + const shaped = anthropicOAuthWireBody({ tools, messages }); + expect(shaped.body.tools).toBe(tools); + expect(shaped.body.messages).toBe(messages); + expect(shaped.toolNames.size).toBe(0); + }); + + test("key auth preserves the exact deferred and inline source histories", () => { + const source = { ...DEFERRED_SOURCE, messages: [...DEFERRED_SOURCE.messages, { role: "user", content: [{ + type: "tool_addition", tool: { type: "tool_definition", definition: { name: "later", input_schema: {} } }, + }] }] }; + const shaped = anthropicMessagesNativeWireBody({ baseUrl: "https://api.anthropic.com", authMode: "key" }, "m", source); + expect(shaped.wireBody.messages).toBe(source.messages); + expect(shaped.wireBody.tools).toBe(source.tools); + expect(shaped.oauthToolNames).toBeUndefined(); + }); +}); From 0c01d516508e1ab449917631ac6dc923d1e148af Mon Sep 17 00:00:00 2001 From: JUN Date: Sun, 4 Oct 2026 11:02:29 +0900 Subject: [PATCH 3/5] fix(anthropic): retain narrowed typed block during copy-on-write --- src/adapters/anthropic/passthrough.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/adapters/anthropic/passthrough.ts b/src/adapters/anthropic/passthrough.ts index 94d1c8cd40c..b8b36460b9d 100644 --- a/src/adapters/anthropic/passthrough.ts +++ b/src/adapters/anthropic/passthrough.ts @@ -124,7 +124,7 @@ function mapOAuthContentBlocks(blocks: unknown[], mapBlock: (block: Rec) => Rec) if (!isRec(block)) return block; if (block.type === "tool_result" && Array.isArray(block.content)) { const content = mapOAuthContentBlocks(block.content, mapBlock); - if (content !== block.content) block = { ...block, content }; + if (content !== block.content) return mapBlock({ ...block, content }); } return mapBlock(block); }); From 221b7dce3b332c2ce2115e71436dece9424160ad Mon Sep 17 00:00:00 2001 From: JUN Date: Sun, 4 Oct 2026 13:26:38 +0900 Subject: [PATCH 4/5] fix(anthropic): retain required beta for inline tool changes --- .../src/content/docs/guides/claude-code.md | 4 +- src/adapters/anthropic/beta-allowlist.ts | 5 +- src/adapters/anthropic/passthrough.ts | 16 ++++- structure/data-planes/protocol-paths.md | 5 +- ...thropic-messages-passthrough-oauth.test.ts | 71 +++++++++++++++++++ 5 files changed, 97 insertions(+), 4 deletions(-) diff --git a/docs-site/src/content/docs/guides/claude-code.md b/docs-site/src/content/docs/guides/claude-code.md index 9fded76404b..1554e1a80cc 100644 --- a/docs-site/src/content/docs/guides/claude-code.md +++ b/docs-site/src/content/docs/guides/claude-code.md @@ -95,7 +95,9 @@ See [Configuration](/reference/configuration/providers/#anthropicaccountpool-exp For managed native OAuth requests, declared custom tools use consistent names across deferred references and inline additions or removals. Tool arguments, schemas and cache markers are retained. -Ambiguous tool declarations are rejected before sending a request. +Ambiguous tool declarations are rejected before sending a request. For inline tool changes in +system messages, send the required `inline-tools-2026-09-15` beta; the first-party native builder +preserves that requested header with the typed blocks. See [Claude’s inline tool contract](https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages). ## Quickstart diff --git a/src/adapters/anthropic/beta-allowlist.ts b/src/adapters/anthropic/beta-allowlist.ts index 249976b4a35..dafaaefe955 100644 --- a/src/adapters/anthropic/beta-allowlist.ts +++ b/src/adapters/anthropic/beta-allowlist.ts @@ -35,6 +35,7 @@ const FIRST_PARTY_BETAS: readonly string[] = [ * needed for the fields the lane forwards. */ const COMPATIBLE_BETAS: readonly string[] = []; +const INLINE_TOOLS_BETA = "inline-tools-2026-09-15"; const ALLOWLISTS: Readonly>> = { "first-party": new Map(FIRST_PARTY_BETAS.map(beta => [beta.toLowerCase(), beta])), @@ -58,6 +59,7 @@ export interface AllowlistedAnthropicBetas { export function allowlistAnthropicBetas( callerHeader: string | null | undefined, providerClass: AnthropicProviderClass, + features: { inlineTools?: boolean } = {}, ): AllowlistedAnthropicBetas { if (typeof callerHeader !== "string" || callerHeader.trim() === "") return { betas: [], dropped: false }; if (callerHeader.length > MAX_CALLER_BETA_HEADER_CHARS) return { betas: [], dropped: true }; @@ -67,7 +69,8 @@ export function allowlistAnthropicBetas( for (const part of callerHeader.split(",")) { const token = part.trim().toLowerCase(); if (!token) continue; - const canonical = allowed.get(token); + const canonical = allowed.get(token) ?? (providerClass === "first-party" && features.inlineTools + && token === INLINE_TOOLS_BETA ? INLINE_TOOLS_BETA : undefined); if (canonical === undefined) dropped = true; else if (!betas.includes(canonical)) betas.push(canonical); } diff --git a/src/adapters/anthropic/passthrough.ts b/src/adapters/anthropic/passthrough.ts index b8b36460b9d..1a073c976ee 100644 --- a/src/adapters/anthropic/passthrough.ts +++ b/src/adapters/anthropic/passthrough.ts @@ -106,6 +106,19 @@ function isClientTool(tool: unknown): tool is Rec & { name: string } { return isRec(tool) && typeof tool.name === "string" && (tool.type === undefined || tool.type === "custom"); } +/** Only documented system-message tool blocks enable the caller's inline-tools beta. */ +function hasInlineToolChanges(body: Rec): boolean { + return Array.isArray(body.messages) && body.messages.some(message => isRec(message) + && message.role === "system" && Array.isArray(message.content) && message.content.some(block => { + if (!isRec(block) || !isRec(block.tool)) return false; + if (block.type !== "tool_addition" && block.type !== "tool_removal") return false; + const tool = block.tool; + return tool.type === "tool_reference" && typeof tool.name === "string" + || block.type === "tool_addition" && tool.type === "tool_definition" + && isRec(tool.definition) && typeof tool.definition.name === "string"; + })); +} + /** Allocate only once the first changed item is encountered. */ function mapPreservingIdentity(items: T[], mapItem: (item: T) => T): T[] { let out: T[] | undefined; @@ -286,7 +299,8 @@ export function buildAnthropicMessagesPassthroughRequest( } } if (domain?.firstPartyAnthropic) applyAnthropicClientIdentity(headers, options.clientIdentity, provider.headers); - const betas = allowlistAnthropicBetas(options.callerAnthropicBeta, domain?.firstPartyAnthropic ? "first-party" : "compatible"); + const betas = allowlistAnthropicBetas(options.callerAnthropicBeta, domain?.firstPartyAnthropic ? "first-party" : "compatible", + { inlineTools: hasInlineToolChanges(wireBody) }); mergeAnthropicBetaHeader(headers, betas.betas); return { url, diff --git a/structure/data-planes/protocol-paths.md b/structure/data-planes/protocol-paths.md index 1798a3a84d5..942d85a9247 100644 --- a/structure/data-planes/protocol-paths.md +++ b/structure/data-planes/protocol-paths.md @@ -299,7 +299,10 @@ Caller betas. The ingress hands over `anthropic-beta` explicitly; caller `Author `x-api-key` never reach the managed provider. `src/adapters/anthropic/beta-allowlist.ts` keeps a value only when it is on the list for the destination's class and re-emits it in the list's own spelling: `interleaved-thinking-2025-05-14` -for `api.anthropic.com`, nothing for an Anthropic-compatible host. Proxy-owned betas (the OAuth +for `api.anthropic.com`, nothing for an Anthropic-compatible host. A caller-requested +`inline-tools-2026-09-15` is also retained for first-party requests whose outgoing system-message +content directly contains typed inline definitions or tool references in additions/removals. +Arguments, schemas and nested/unknown containers do not activate this allowance; no beta is injected. Proxy-owned betas (the OAuth pair) are set by the builder, and an operator's configured beta is merged, not replaced. Any dropped value adds `anthropic-beta-dropped` to the trace; the value itself is never recorded. diff --git a/tests/adapters/anthropic/anthropic-messages-passthrough-oauth.test.ts b/tests/adapters/anthropic/anthropic-messages-passthrough-oauth.test.ts index 0d105297e06..172b7914c5d 100644 --- a/tests/adapters/anthropic/anthropic-messages-passthrough-oauth.test.ts +++ b/tests/adapters/anthropic/anthropic-messages-passthrough-oauth.test.ts @@ -393,3 +393,74 @@ describe("native OAuth typed tool-name preservation", () => { expect(shaped.oauthToolNames).toBeUndefined(); }); }); + +// Complete root builder contract, independent of the later native-client compatibility layer. +describe("inline tool feature beta at the native builder boundary", () => { + const beta = "inline-tools-2026-09-15"; + const definition = { type: "tool_addition", tool: { type: "tool_definition", definition: { + name: "lookup", input_schema: { type: "object" }, + } }, cache_control: { type: "ephemeral", ttl: "1h" } }; + const request = (block: unknown) => ({ max_tokens: 64, tools: [{ name: "initial", input_schema: {} }], + messages: [{ role: "user", content: "fixture" }, { role: "system", content: [block] }] }); + + test("forwards the required requested beta with an inline definition and OAuth names", () => { + const body = request(definition); + const before = structuredClone(body); + const built = buildAnthropicMessagesPassthroughRequest(oauthProvider(), "claude-wire", body, undefined, + { callerAnthropicBeta: `${beta.toUpperCase()},${beta}` }); + expect(built.headers["anthropic-beta"]).toBe(`${ANTHROPIC_OAUTH_BETA},${beta}`); + expect(built.droppedBetas).toBe(false); + const content = (built.wireBody.messages as typeof body.messages)[1]!.content as typeof definition[]; + expect(content[0]!.tool.definition.name).toBe("custom_lookup"); + expect(JSON.parse(built.body)).toEqual(built.wireBody); + expect(content[0]!.cache_control).toEqual({ type: "ephemeral", ttl: "1h" }); + expect(built.oauthToolNames?.get("custom_lookup")).toBe("lookup"); + expect(body).toEqual(before); + }); + + for (const type of ["tool_addition", "tool_removal"]) test(`forwards the beta for ${type} references`, () => { + const body = request({ type, tool: { type: "tool_reference", name: "initial" } }); + const built = buildAnthropicMessagesPassthroughRequest(oauthProvider(), "m", body, undefined, + { callerAnthropicBeta: beta }); + expect(built.headers["anthropic-beta"]).toBe(`${ANTHROPIC_OAUTH_BETA},${beta}`); + expect(built.droppedBetas).toBe(false); + }); + + test("does not synthesize betas and still drops unknown or oversized caller headers", () => { + const body = request(definition); + expect(buildAnthropicMessagesPassthroughRequest(oauthProvider(), "m", body).headers["anthropic-beta"]) + .toBe(ANTHROPIC_OAUTH_BETA); + const built = buildAnthropicMessagesPassthroughRequest(oauthProvider(), "m", body, undefined, + { callerAnthropicBeta: `${beta},unknown-feature` }); + expect(built.headers["anthropic-beta"]).toBe(`${ANTHROPIC_OAUTH_BETA},${beta}`); + expect(built.droppedBetas).toBe(true); + expect(buildAnthropicMessagesPassthroughRequest(oauthProvider(), "m", body, undefined, + { callerAnthropicBeta: `${beta},${"x".repeat(2048)}` }).headers["anthropic-beta"]).toBe(ANTHROPIC_OAUTH_BETA); + }); + + test("key auth gets the feature beta only on a first-party destination", () => { + const first = buildAnthropicMessagesPassthroughRequest(oauthProvider({ authMode: "key" }), "m", request(definition), undefined, + { callerAnthropicBeta: beta }); + expect(first.headers["anthropic-beta"]).toBe(beta); + const compatible = buildAnthropicMessagesPassthroughRequest(oauthProvider({ authMode: "key", baseUrl: "https://compatible.example" }), "m", request(definition), undefined, + { callerAnthropicBeta: beta }); + expect(compatible.headers).not.toHaveProperty("anthropic-beta"); + expect(compatible.droppedBetas).toBe(true); + }); + + test("opaque lookalikes and unsupported placement do not enable beta forwarding", () => { + for (const body of [SOURCE, request({ type: "text", text: JSON.stringify(definition) }), + request({ type: "tool_use", name: "initial", input: definition }), + request({ type: "unknown", content: [definition] }), + request({ type: "tool_addition", tool: { type: "tool_definition", definition: null } }), + request({ type: "tool_removal", tool: { type: "tool_definition", definition: { name: "lookup" } } }), + request({ type: "tool_result", content: [definition] }), + { ...request(definition), messages: [{ role: "assistant", content: [definition] }] }, + { ...SOURCE, tools: [{ name: "initial", input_schema: definition }] }]) { + const built = buildAnthropicMessagesPassthroughRequest(oauthProvider(), "m", body, undefined, + { callerAnthropicBeta: beta }); + expect(built.headers["anthropic-beta"]).toBe(ANTHROPIC_OAUTH_BETA); + expect(built.droppedBetas).toBe(true); + } + }); +}); From 124c85e110203190f288c4381d1aed42b2cc0ba0 Mon Sep 17 00:00:00 2001 From: JUN Date: Sun, 4 Oct 2026 13:33:40 +0900 Subject: [PATCH 5/5] docs: clarify native OAuth scope and repair roadmap formatting --- .../_plan/261004_claude_request_preservation/000_plan.md | 3 +++ .../261004_claude_request_preservation/010_tool_names.md | 7 ++++++- .../261004_claude_request_preservation/020_native_pool.md | 2 ++ .../261004_claude_request_preservation/030_preferences.md | 3 +++ docs-site/src/content/docs/ja/guides/claude-code.md | 4 +++- docs-site/src/content/docs/ko/guides/claude-code.md | 4 +++- docs-site/src/content/docs/ru/guides/claude-code.md | 4 +++- docs-site/src/content/docs/zh-cn/guides/claude-code.md | 4 +++- structure/data-planes/protocol-paths.md | 2 +- 9 files changed, 27 insertions(+), 6 deletions(-) diff --git a/devlog/_plan/261004_claude_request_preservation/000_plan.md b/devlog/_plan/261004_claude_request_preservation/000_plan.md index fd748feb63b..bfe9a65c03c 100644 --- a/devlog/_plan/261004_claude_request_preservation/000_plan.md +++ b/devlog/_plan/261004_claude_request_preservation/000_plan.md @@ -3,6 +3,7 @@ Native Claude requests need consistent OAuth tool names, client compatibility and pool dispatch without losing caller content. This unit separates those runtime contracts from the preference that selects them. It refines public sources #6533, #6534 and #6547 into a manual PR chain; source closure remains coordinator-owned. ## Loop specification + - Shape/trigger: satisfy-spec, assigned next-release Claude lane. - Goal: focused published replacement PRs with complete source behavior accounting and real-author attribution. - Non-goals: merges, release/version changes, installed runtime writes, account/credential changes, live upstream traffic, source closure, native stacks, unrelated work. @@ -15,6 +16,7 @@ Native Claude requests need consistent OAuth tool names, client compatibility an - Token/cost/wall-clock bounds: no user numeric budget supplied; do not invent one. Managed short-yield commands and bounded leaf tasks; keep work within the requested scope. ## Baseline and topology + Base `0818ea1812a028e1c14cd0b0511b44863407bc52` from refreshed dev. Sources: #6533 `8e5c78912222b1616709d664c23de254dd71b7c9`; #6534 `829b203b3e65530a4cdaaa121f2da15484c2c9b7`; #6547 `5f3cf4ed0b4a63604133863442002fb9ac824b16`. Original author: Claire Novotny . Inherited tool/pool commits are consumed once, never blindly cherry-picked cumulatively. | Cycle | Output | Dependency | @@ -27,6 +29,7 @@ Base `0818ea1812a028e1c14cd0b0511b44863407bc52` from refreshed dev. Sources: #65 Existing owners: `src/adapters/anthropic/` shapes the wire; `src/oauth/anthropic-routing.ts` owns pool selection/health; native server modules own physical dispatch; `src/protocols/settings.ts` owns policy resolution; pool settings capability and management routes own validated writes; the existing Anthropic pool component owns UI. Reuse these paths and existing tests. No alternate pool or retry engine. ## Consultation + Read-only architect proposal/reflection and independent audit recorded before implementation. Every later P revalidates its decade document against current parent tip. Leaves are scoped to disjoint files and never branch or run goals/FSM. Architect handle: 01a10493-1a9a-73d2-96f9-86ef0df9518d. Main accepts CL-A01/A05/A08; amends CL-A02 duplicate carries, CL-A03 session priority, CL-A04 lease-before-charge, CL-A06 malformed protocol salvage, CL-A07 CSS sibling and CL-G01–G04 route race/raw malformed/help/preview coverage. Reflection requested on amended roadmap. diff --git a/devlog/_plan/261004_claude_request_preservation/010_tool_names.md b/devlog/_plan/261004_claude_request_preservation/010_tool_names.md index 399013efffc..fe820206f1c 100644 --- a/devlog/_plan/261004_claude_request_preservation/010_tool_names.md +++ b/devlog/_plan/261004_claude_request_preservation/010_tool_names.md @@ -3,19 +3,24 @@ Depends on the roadmap only. Branch `codex/next-release-261004-claude-tools`, PR base dev. Carry #6533 in full and the inline-tool portion of #6534/#6547. Identity/beta/preamble and pooling remain later layers. ## Exact change map + - MODIFY `src/adapters/anthropic/passthrough.ts`: within `anthropicOAuthWireBody`, replace top-level tool-use-only mapping with copy-on-write typed block traversal. Carry #6533 diff plus the inline-name hunks of public commit `e3eefbaedf5e0ba598c5d443ce16e21d94a9f378`. Collect inline `tool_addition.tool.type=tool_definition` client definitions before mapping so preceding references resolve. Rewrite declared custom tool `tool_use.name`, `tool_reference.tool_name`, inline addition/removal `tool.type=tool_reference` names, and inline client definition names. Traverse only message content and typed tool-result content. Preserve arbitrary schemas, arguments, cache markers and unknown blocks. Reject ambiguous wire-name collisions, including typed versus client declarations. - MODIFY `tests/adapters/anthropic/anthropic-messages-passthrough-oauth.test.ts`: preserve six existing regressions; carry nested-reference and inline-declaration source cases; add targeted ordering, typed collision and opaque-container negatives within ratchet headroom, otherwise register a sibling domain file in both layout maps. - MODIFY `structure/data-planes/protocol-paths.md`: native OAuth paragraph documents typed reference/inline naming, exact opaque preservation and copy-on-write behavior. - MODIFY `docs-site/src/content/docs/guides/claude-code.md`: small native tool compatibility note, without claiming pool/default behavior before later layers. ## Observable acceptance + Declared lookup in nested tool-result content becomes custom_lookup; undeclared and typed builtins stay unchanged. Inline declaration after an earlier reference still prefixes consistently. Inline removal names match declaration. Source body JSON is unchanged; opaque tool input/schema and text/cache TTL/scope remain identical. A collision is refused before dispatch. Key auth gets no OAuth shaping. Tests fail against original behavior then pass with the patch. No network request. ## Verification + `bun test tests/adapters/anthropic/anthropic-messages-passthrough-oauth.test.ts` reads the target directly; baseline six tests pass after frozen dependency installation. `bun run typecheck`, `bun run structure:check`, `bun run privacy:scan`, `bun test tests/test-layout.test.ts tests/test-layout-tooling.test.ts tests/ci-workflows/file-size-ratchet.test.ts`; docs build per docs-site instructions. Source/public patch anchors are fixed above. Human semantic review proves prose scope, not a phrase-presence test. ## Source accounting -#6533 all behavior carried. #6534/#6547 inline naming carried here; client compatibility, pool and preferences remain unresolved until their own layers. Include `Co-authored-by: Claire Novotny ` in commit and PR body. + +`#6533` all behavior carried. #6534/#6547 inline naming carried here; client compatibility, pool and preferences remain unresolved until their own layers. Include `Co-authored-by: Claire Novotny ` in commit and PR body. ## B1 focused source review dispositions + Accept independent tool review: collect both top-level and inline typed names before any rename; reject original-name ambiguity and typed wire-name collisions independent of declaration order. Register all client declarations in the prefix collision map before walking uses. Follow only the same typed content containers in both collection and mapping so nested inline definitions cannot silently escape while their neighbors are rewritten; this is conservative structural support, not a claim that upstream accepts nested inline definitions. Add cross-message ordering, inline choice/use, undeclared/typed references, and schema/input/unknown-container opacity regressions. These are bounded refinements within B1's existing collision and typed-container contract. diff --git a/devlog/_plan/261004_claude_request_preservation/020_native_pool.md b/devlog/_plan/261004_claude_request_preservation/020_native_pool.md index 273ffe074df..712ac6d1187 100644 --- a/devlog/_plan/261004_claude_request_preservation/020_native_pool.md +++ b/devlog/_plan/261004_claude_request_preservation/020_native_pool.md @@ -3,6 +3,7 @@ Depends on B1. Publish `codex/next-release-261004-claude-native` as manual child of tools. Combining client compatibility and dispatch is intentional: selecting native for a real pooled client without preserving its identity/preamble/betas would admit a request the replacement cannot faithfully send. No preference/default changes in this layer. ## Exact change map + Use the complete public #6534 diff at `829b203b3e65530a4cdaaa121f2da15484c2c9b7`, excluding B1 hunks already carried, The equivalent #6547 commits `c46145193613d54574c779806192e8ff4f19ace5` (dispose refusal before rebind) and `5f3cf4ed0b4a63604133863442002fb9ac824b16` (blank session fallback) corroborate behavior already in the pinned #6534 head and are not applied again. Adapt to current routing/refusal owners, never restore obsolete ownership from a source patch. - MODIFY `src/adapters/anthropic/client-identity.ts`: recognize bounded observed CLI/Desktop Code compatibility bundles; keep WeakMap-backed opaque handles, header exclusion, first-party-only forwarding and operator header precedence. Expose observed-identity predicate for native preamble/beta consumers. @@ -19,6 +20,7 @@ Use the complete public #6534 diff at `829b203b3e65530a4cdaaa121f2da15484c2c9b7` - MODIFY `structure/data-planes/protocol-paths.md`, `structure/providers/anthropic-account-pool.md`, `docs-site/src/content/docs/guides/claude-code.md`: replace obsolete pool exclusion with explicit-rollout native contract; no default-on claim yet. ## Acceptance and verification + Real local fake endpoints exercise two usable accounts, sticky session, model route, eligible shared-quota 429 and classified account 403 before output, bounded attempt exhaustion, refusal cancellation before rebind, and partial-output termination without another physical send. Manual selection, pause, generation or UUID change during async preparation must not emit stale binding. Opaque content/cache markers are equal across rebuild except declared tool/account metadata fields. CLI/Desktop preamble and beta cases use fabricated compatibility headers only. Token counting and preview select same body/lane as runtime without refreshing or selecting credentials. Run focused test files above plus existing native bridge/opaque-state tests, `bun run typecheck`, structure/privacy/layout/ratchet gates and docs build. Use fake isolated OAuth homes; no credential or live-provider access. Source regressions may need sibling test modules to respect downward ratchets, registered in both layout inventories. Security review is independent; unshipped discoveries stay ignored scratch. diff --git a/devlog/_plan/261004_claude_request_preservation/030_preferences.md b/devlog/_plan/261004_claude_request_preservation/030_preferences.md index 06d1e3c1627..3165b31ab28 100644 --- a/devlog/_plan/261004_claude_request_preservation/030_preferences.md +++ b/devlog/_plan/261004_claude_request_preservation/030_preferences.md @@ -3,6 +3,7 @@ Depends on B2/B3. Publish `codex/next-release-261004-claude-settings` as a manual child of native. This layer carries #6547 settings behavior while correcting precedence: an explicit operator false rollout remains false. Source tests asserting false becomes true are deliberately replaced by precedence regressions. ## Field chain and exact changes + The new optional `anthropicAccountPool.nativeMessages` boolean is created by API/UI, serialized by existing config persistence, parsed by schema/config diagnostics, resolved by protocol settings and consumed by eligibility, preview, count_tokens and runtime. DTO/UI normalization must preserve false. - MODIFY `src/types/config.ts`: add optional nativeMessages boolean to existing Anthropic pool type. @@ -20,7 +21,9 @@ The new optional `anthropicAccountPool.nativeMessages` boolean is created by API - MODIFY `docs-site/src/content/docs/guides/claude-code.md`, `reference/configuration/providers.md`, `reference/configuration/server.md`, `structure/providers/anthropic-account-pool.md` and affected protocol contract: default only when no explicit opt-out; pool-off rules; cache remains account-specific. Review translated pages and amend contradictory statements. ## Activation matrix + Pool absent/disabled + flags absent -> off. Pool enabled + flags absent + preference absent/true -> eligible Anthropic native on. Each explicit false flag wins; malformed flag or container fails closed. Preference false wins over explicit true flags for pooled routes. Other providers retain their own rollout. Invalid native preference -> false without losing providers. Raw malformed preference/rollout input is tested in resolver and DTO, not only parsed-file tests. Both API routes reject wrong provider/nonboolean; persistence failure leaves live config unchanged and UI shows error; concurrent unrelated disk edit survives. 204 false write remains false. Preview revision changes when effective policy changes; count sends no requests. GUI checkbox toggles and reload reflects actual saved choice. ## Verification and publication + Focused backend/GUI tests, root typecheck and GUI lint/typecheck, structure/privacy/layout/ratchet; docs build. Observe local rendered checkbox and first state change with mocked endpoints and fake account data; capture screenshot outside PR branch and publish via pr-assets (or reuse source image only if accurate). Every source hunk is carried/amended/dropped explicitly; all three layers carry Claire Novotny attribution. Current-head hosted checks inspected, broad/native/live missing evidence remains draft-bound. No merge or source closure. diff --git a/docs-site/src/content/docs/ja/guides/claude-code.md b/docs-site/src/content/docs/ja/guides/claude-code.md index 834d27eec40..49495d2afd5 100644 --- a/docs-site/src/content/docs/ja/guides/claude-code.md +++ b/docs-site/src/content/docs/ja/guides/claude-code.md @@ -99,7 +99,9 @@ hook を削除します。Claude Desktop は独立した profile を使用し、 では専用プロキシ admission ヘッダーも有効であること。そのため `ocx claude` を 使うとき "claude.ai connectors are disabled" 警告ももう表示されません。 -本文で変更するのはツール呼び出し ID だけです。Anthropic が拒否する `tool_use.id` や `tool_result.tool_use_id`(`a-zA-Z0-9_-` 以外の文字を含むもの、または 64 文字を超えるもの。セッション中にルーティングモデルが作った ID など)は、呼び出しと結果の対応を保ったまま適合する ID に書き換えます。適合する ID はそのまま送り、空の ID にはローカルで 400 を返します。 +このユーザー認証情報を使うパススルーで変更するのはツール呼び出し ID だけです。Anthropic が拒否する `tool_use.id` や `tool_result.tool_use_id`(`a-zA-Z0-9_-` 以外の文字を含むもの、または 64 文字を超えるもの。セッション中にルーティングモデルが作った ID など)は、呼び出しと結果の対応を保ったまま適合する ID に書き換えます。適合する ID はそのまま送り、空の ID にはローカルで 400 を返します。 + +プロキシが保存済み OAuth 認証情報を使う管理対象のネイティブリクエストでは、宣言されたカスタムツール名も遅延参照とインライン追加・削除で揃えます。引数、スキーマ、キャッシュマーカーは保持します。システムメッセージ内のインライン変更には `inline-tools-2026-09-15` を指定してください。対応する型付きブロックがある場合、ファーストパーティ宛てのビルダーはこのヘッダーを保持します。 `claudeCode.nativePassthrough: false` でオフにでき、`claudeCode.anthropicBaseUrl` で別のアドレスを 指定できます。 diff --git a/docs-site/src/content/docs/ko/guides/claude-code.md b/docs-site/src/content/docs/ko/guides/claude-code.md index d79d7940d72..4fc59c589b4 100644 --- a/docs-site/src/content/docs/ko/guides/claude-code.md +++ b/docs-site/src/content/docs/ko/guides/claude-code.md @@ -122,7 +122,9 @@ hook을 제거해요. Claude Desktop은 별도 profile을 사용하며 shell hoo 프록시 admission 헤더도 유효해야 해요. 그래서 `ocx claude`를 사용할 때 "claude.ai connectors are disabled" 경고도 더 이상 나타나지 않아요. -본문에서 바꾸는 것은 도구 호출 ID뿐이에요. Anthropic이 거부할 `tool_use.id`나 `tool_result.tool_use_id`(`a-zA-Z0-9_-` 밖의 문자가 있거나 64자를 넘는 ID, 예를 들어 세션 앞부분에서 라우팅 모델이 만든 ID)는 호출과 결과의 짝을 유지한 채 규칙에 맞는 ID로 바꿔요. 규칙에 맞는 ID는 그대로 보내고, 빈 ID에는 로컬에서 400을 돌려줘요. +사용자 자격 증명을 전달하는 이 패스스루에서 바꾸는 것은 도구 호출 ID뿐이에요. Anthropic이 거부할 `tool_use.id`나 `tool_result.tool_use_id`(`a-zA-Z0-9_-` 밖의 문자가 있거나 64자를 넘는 ID, 예를 들어 세션 앞부분에서 라우팅 모델이 만든 ID)는 호출과 결과의 짝을 유지한 채 규칙에 맞는 ID로 바꿔요. 규칙에 맞는 ID는 그대로 보내고, 빈 ID에는 로컬에서 400을 돌려줘요. + +프록시에 저장된 OAuth 자격 증명을 쓰는 관리형 네이티브 요청은 선언된 커스텀 도구 이름도 지연 참조와 인라인 추가·제거에 맞춰 바꿔요. 인수, 스키마, 캐시 표시는 유지해요. 시스템 메시지의 인라인 변경에는 `inline-tools-2026-09-15`를 지정하세요. 해당 타입의 블록이 있으면 Anthropic 원본 API로 보내는 빌더가 이 헤더를 유지해요. `claudeCode.nativePassthrough: false`로 끌 수 있고, `claudeCode.anthropicBaseUrl`로 다른 주소를 지정할 수 있어요. diff --git a/docs-site/src/content/docs/ru/guides/claude-code.md b/docs-site/src/content/docs/ru/guides/claude-code.md index a98843179bf..a69515c2f76 100644 --- a/docs-site/src/content/docs/ru/guides/claude-code.md +++ b/docs-site/src/content/docs/ru/guides/claude-code.md @@ -107,7 +107,9 @@ Proxy admission secret в любом provider-заголовке удаляет валиден dedicated proxy admission-header. Это также означает, что предупреждение «claude.ai connectors are disabled» с `ocx claude` больше не появляется. -Единственное изменение тела касается идентификаторов вызовов инструментов. `tool_use.id` или `tool_result.tool_use_id`, которые Anthropic отклонил бы (символы вне `a-zA-Z0-9_-` или длина больше 64), например созданные маршрутизируемой моделью раньше в сессии, переписываются в допустимые с сохранением пары вызов/результат. Допустимые идентификаторы отправляются без изменений, а на пустой идентификатор локально возвращается 400. +В этом пробросе с учётными данными вызывающей стороны изменение тела касается только идентификаторов вызовов инструментов. `tool_use.id` или `tool_result.tool_use_id`, которые Anthropic отклонил бы (символы вне `a-zA-Z0-9_-` или длина больше 64), например созданные маршрутизируемой моделью раньше в сессии, переписываются в допустимые с сохранением пары вызов/результат. Допустимые идентификаторы отправляются без изменений, а на пустой идентификатор локально возвращается 400. + +В управляемых нативных запросах с сохранёнными OAuth-данными прокси также согласует имена объявленных пользовательских инструментов в отложенных ссылках и встроенных добавлениях/удалениях. Аргументы, схемы и маркеры кеша сохраняются. Для встроенных изменений в системных сообщениях передавайте `inline-tools-2026-09-15`: при наличии соответствующих типизированных блоков построитель сохраняет этот заголовок для API Anthropic. Отключается параметром `claudeCode.nativePassthrough: false`; другой адрес задаётся через `claudeCode.anthropicBaseUrl`. diff --git a/docs-site/src/content/docs/zh-cn/guides/claude-code.md b/docs-site/src/content/docs/zh-cn/guides/claude-code.md index 010bbab0068..f57a0af463c 100644 --- a/docs-site/src/content/docs/zh-cn/guides/claude-code.md +++ b/docs-site/src/content/docs/zh-cn/guides/claude-code.md @@ -93,7 +93,9 @@ Anthropic。若任一提供方请求头包含代理准入密钥,该密钥会 解析后返回的模型保持不变;并且在非回环绑定上,专用代理准入请求头有效。这也意味着使用 `ocx claude` 时不再出现 “claude.ai connectors are disabled”警告。 -请求体中唯一会改动的是工具调用 ID。Anthropic 会拒绝的 `tool_use.id` 或 `tool_result.tool_use_id`(含 `a-zA-Z0-9_-` 以外的字符或超过 64 个字符,例如会话早先由路由模型生成的 ID)会被改写为合规 ID,并保持调用与结果的配对。合规 ID 原样发送,空 ID 会在本地直接返回 400。 +在使用调用方凭据的这条透传路径中,请求体只会改动工具调用 ID。Anthropic 会拒绝的 `tool_use.id` 或 `tool_result.tool_use_id`(含 `a-zA-Z0-9_-` 以外的字符或超过 64 个字符,例如会话早先由路由模型生成的 ID)会被改写为合规 ID,并保持调用与结果的配对。合规 ID 原样发送,空 ID 会在本地直接返回 400。 + +使用代理已保存 OAuth 凭据的受管理原生请求还会统一已声明自定义工具在延迟引用和内联添加或删除中的名称。参数、模式和缓存标记保持不变。系统消息中的内联工具变更需要发送 `inline-tools-2026-09-15`;存在相应类型的块时,面向 Anthropic 官方 API 的构建器会保留该请求头。 可以设置 `claudeCode.nativePassthrough: false` 来禁用;也可以通过 `claudeCode.anthropicBaseUrl` 指向其他位置。 diff --git a/structure/data-planes/protocol-paths.md b/structure/data-planes/protocol-paths.md index 942d85a9247..222950a53cc 100644 --- a/structure/data-planes/protocol-paths.md +++ b/structure/data-planes/protocol-paths.md @@ -357,7 +357,7 @@ Conflicting provider credential headers fail before dispatch on every OAuth buil builds without a provider UUID. Native OAuth Messages collect top-level and typed inline tool declarations before rewriting declared client names in tool choices, uses, references, additions and removals, including typed tool-result -content. Typed builtin names stay fixed; ambiguous original or wire-name collisions are refused. +content. Typed built-in names stay fixed; ambiguous original or wire-name collisions are refused. The copy-on-write traversal leaves input schemas, tool arguments, unknown containers and cache markers (including lifetimes) opaque. Nested inline support is structural; it does not assert upstream acceptance. Key-auth and caller-forward requests retain their metadata. The answer's