From dc86bc2e151c5b112170cca5c85e5e2750b328a4 Mon Sep 17 00:00:00 2001 From: roboomp Date: Sun, 26 Jul 2026 15:42:24 +0000 Subject: [PATCH 1/4] fix(ai): sent interleaved beta through signing proxies - Sent the interleaved-thinking beta for adaptive models on recognized non-official signing endpoints. - Covered signing and non-signing proxy header behavior. - Documented the replay failure fix in the AI changelog. Fixes #6717 --- packages/ai/CHANGELOG.md | 4 +++ packages/ai/src/providers/anthropic.ts | 3 +- packages/ai/test/anthropic-alignment.test.ts | 32 ++++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index cd71558b2..38d371957 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed adaptive-thinking Anthropic models omitting the interleaved-thinking beta on signature-enforcing proxies, which caused persisted interleaved assistant turns to fail on replay ([#6717](https://github.com/can1357/oh-my-pi/issues/6717)). + ## [17.1.4] - 2026-07-26 ### Added diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 869847735..8e4f4a477 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -2850,7 +2850,8 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A } = args; const compat = model.compat; const disableStrictTools = disableStrictToolsOverride ?? compat.disableStrictTools; - const needsInterleavedBeta = interleavedThinking && !model.thinking?.supportsDisplay; + const needsInterleavedBeta = + interleavedThinking && (!model.thinking?.supportsDisplay || (compat.signingEndpoint && !compat.officialEndpoint)); const oauthToken = isOAuth ?? isAnthropicOAuthToken(apiKey); const baseUrl = resolveAnthropicBaseUrl(model, apiKey); const supportsEagerToolInputStreaming = resolveEagerToolInputStreamingSupport(model, baseUrl); diff --git a/packages/ai/test/anthropic-alignment.test.ts b/packages/ai/test/anthropic-alignment.test.ts index cbe94a480..ba4926cdb 100644 --- a/packages/ai/test/anthropic-alignment.test.ts +++ b/packages/ai/test/anthropic-alignment.test.ts @@ -1804,6 +1804,38 @@ describe("Anthropic request fingerprint alignment", () => { expect(modern.defaultHeaders["anthropic-beta"] ?? "").not.toContain("interleaved-thinking-2025-05-14"); }); + it("adds the interleaved-thinking beta for adaptive models only on signing proxies", () => { + const adaptiveProxySpec: ModelSpec<"anthropic-messages"> = { + ...ANTHROPIC_MODEL_SPEC, + id: "claude-opus-4-8", + name: "Claude Opus 4.8", + provider: "custom-anthropic", + baseUrl: "https://proxy.example.com/anthropic", + thinking: { + mode: "anthropic-adaptive", + efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh], + supportsDisplay: true, + }, + }; + const signingProxy = buildAnthropicClientOptions({ + model: buildModel({ + ...adaptiveProxySpec, + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/account/gateway/anthropic", + }), + apiKey: "sk-proxy-test", + interleavedThinking: true, + }); + const nonSigningProxy = buildAnthropicClientOptions({ + model: buildModel(adaptiveProxySpec), + apiKey: "sk-proxy-test", + interleavedThinking: true, + }); + + expect(signingProxy.defaultHeaders["anthropic-beta"]).toContain("interleaved-thinking-2025-05-14"); + expect(nonSigningProxy.defaultHeaders["anthropic-beta"] ?? "").not.toContain("interleaved-thinking-2025-05-14"); + }); + it("adds legacy fine-grained tool-streaming beta only for tool requests on incompatible models", () => { const incompatibleModel: Model<"anthropic-messages"> = buildModel({ ...ANTHROPIC_MODEL_SPEC, From 5c3a7d8d5f07cb45e172005b079f598852e09f5e Mon Sep 17 00:00:00 2001 From: roboomp Date: Sun, 26 Jul 2026 15:48:54 +0000 Subject: [PATCH 2/4] fix(ai): excluded vertex rawpredict from interleaved http beta - Gated the signing-endpoint interleaved beta on non-vertex providers so Vertex rawPredict keeps body-only betas. - Asserted the google-vertex adaptive path omits the anthropic-beta HTTP header. Fixes #6717 --- packages/ai/src/providers/anthropic.ts | 10 +++++++++- packages/ai/test/anthropic-alignment.test.ts | 13 +++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 8e4f4a477..ea5254434 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -2850,8 +2850,16 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A } = args; const compat = model.compat; const disableStrictTools = disableStrictToolsOverride ?? compat.disableStrictTools; + // Adaptive models (`supportsDisplay`) get native interleaved thinking on the + // official API, so the beta is only needed on non-official signing proxies + // that still validate replayed multi-thinking-block turns against it (#6717). + // Vertex rawPredict is excluded like every other HTTP-beta path here: it can + // only accept betas in the JSON body (`anthropic_beta`), never as this + // `anthropic-beta` HTTP header, so advertising it there earns a 400 (#5614). const needsInterleavedBeta = - interleavedThinking && (!model.thinking?.supportsDisplay || (compat.signingEndpoint && !compat.officialEndpoint)); + interleavedThinking && + (!model.thinking?.supportsDisplay || + (compat.signingEndpoint && !compat.officialEndpoint && model.provider !== "google-vertex")); const oauthToken = isOAuth ?? isAnthropicOAuthToken(apiKey); const baseUrl = resolveAnthropicBaseUrl(model, apiKey); const supportsEagerToolInputStreaming = resolveEagerToolInputStreamingSupport(model, baseUrl); diff --git a/packages/ai/test/anthropic-alignment.test.ts b/packages/ai/test/anthropic-alignment.test.ts index ba4926cdb..d75420403 100644 --- a/packages/ai/test/anthropic-alignment.test.ts +++ b/packages/ai/test/anthropic-alignment.test.ts @@ -1831,9 +1831,22 @@ describe("Anthropic request fingerprint alignment", () => { apiKey: "sk-proxy-test", interleavedThinking: true, }); + // Vertex rawPredict is a signing endpoint but only accepts betas in the + // JSON body (`anthropic_beta`); the HTTP header would 400 (#5614). + const vertexProxy = buildAnthropicClientOptions({ + model: buildModel({ + ...adaptiveProxySpec, + provider: "google-vertex", + baseUrl: + "https://us-east5-aiplatform.googleapis.com/v1/projects/p/locations/us-east5/publishers/anthropic/models/claude-opus-4-8:rawPredict", + }), + apiKey: "vertex-adc", + interleavedThinking: true, + }); expect(signingProxy.defaultHeaders["anthropic-beta"]).toContain("interleaved-thinking-2025-05-14"); expect(nonSigningProxy.defaultHeaders["anthropic-beta"] ?? "").not.toContain("interleaved-thinking-2025-05-14"); + expect(vertexProxy.defaultHeaders["anthropic-beta"] ?? "").not.toContain("interleaved-thinking-2025-05-14"); }); it("adds legacy fine-grained tool-streaming beta only for tool requests on incompatible models", () => { From 6c254ca4f199d62f3bca6eb167c1ec8396fac7e1 Mon Sep 17 00:00:00 2001 From: roboomp Date: Sun, 26 Jul 2026 15:58:12 +0000 Subject: [PATCH 3/4] fix(ai): classified interleaved beta by effective endpoint - Classified known signing proxies from the resolved request URL instead of materialized model compat. - Excluded Vertex rawPredict by URL regardless of provider id. - Covered canonical-model reroutes and custom-provider Vertex routes. Fixes #6717 --- packages/ai/src/providers/anthropic.ts | 14 ++++++--- packages/ai/test/anthropic-alignment.test.ts | 32 +++++++++++++------- packages/catalog/src/compat/anthropic.ts | 24 ++++++++++++--- 3 files changed, 49 insertions(+), 21 deletions(-) diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index ea5254434..3b109a6a5 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -2,7 +2,8 @@ import * as nodeCrypto from "node:crypto"; import * as fs from "node:fs"; import { scheduler } from "node:timers/promises"; import * as tls from "node:tls"; -import { isOfficialAnthropicApiUrl } from "@oh-my-pi/pi-catalog/compat/anthropic"; +import { isAnthropicSigningProxyUrl, isOfficialAnthropicApiUrl } from "@oh-my-pi/pi-catalog/compat/anthropic"; +import { isVertexRawPredictUrl } from "@oh-my-pi/pi-catalog/hosts"; import { mapEffortToAnthropicAdaptiveEffort } from "@oh-my-pi/pi-catalog/model-thinking"; import { calculateCost, getBundledModel } from "@oh-my-pi/pi-catalog/models"; import { isAnthropicOAuthToken } from "@oh-my-pi/pi-catalog/utils"; @@ -2850,18 +2851,21 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A } = args; const compat = model.compat; const disableStrictTools = disableStrictToolsOverride ?? compat.disableStrictTools; + const baseUrl = resolveAnthropicBaseUrl(model, apiKey); // Adaptive models (`supportsDisplay`) get native interleaved thinking on the - // official API, so the beta is only needed on non-official signing proxies - // that still validate replayed multi-thinking-block turns against it (#6717). + // official API, so only known non-official signing routes need the beta + // (#6717). Classify the effective URL: Foundry and provider overrides can + // reroute a model without rebuilding its materialized compat. // Vertex rawPredict is excluded like every other HTTP-beta path here: it can // only accept betas in the JSON body (`anthropic_beta`), never as this // `anthropic-beta` HTTP header, so advertising it there earns a 400 (#5614). const needsInterleavedBeta = interleavedThinking && (!model.thinking?.supportsDisplay || - (compat.signingEndpoint && !compat.officialEndpoint && model.provider !== "google-vertex")); + (!isOfficialAnthropicApiUrl(baseUrl) && + isAnthropicSigningProxyUrl(baseUrl) && + !isVertexRawPredictUrl(baseUrl ?? ""))); const oauthToken = isOAuth ?? isAnthropicOAuthToken(apiKey); - const baseUrl = resolveAnthropicBaseUrl(model, apiKey); const supportsEagerToolInputStreaming = resolveEagerToolInputStreamingSupport(model, baseUrl); const needsFineGrainedToolStreamingBeta = hasTools && isOfficialAnthropicApiUrl(baseUrl) && !supportsEagerToolInputStreaming; diff --git a/packages/ai/test/anthropic-alignment.test.ts b/packages/ai/test/anthropic-alignment.test.ts index d75420403..7028654a6 100644 --- a/packages/ai/test/anthropic-alignment.test.ts +++ b/packages/ai/test/anthropic-alignment.test.ts @@ -1804,7 +1804,7 @@ describe("Anthropic request fingerprint alignment", () => { expect(modern.defaultHeaders["anthropic-beta"] ?? "").not.toContain("interleaved-thinking-2025-05-14"); }); - it("adds the interleaved-thinking beta for adaptive models only on signing proxies", () => { + it("uses the effective route for adaptive interleaved-thinking beta headers", () => { const adaptiveProxySpec: ModelSpec<"anthropic-messages"> = { ...ANTHROPIC_MODEL_SPEC, id: "claude-opus-4-8", @@ -1817,12 +1817,21 @@ describe("Anthropic request fingerprint alignment", () => { supportsDisplay: true, }, }; + const signingProxyUrl = "https://gateway.ai.cloudflare.com/v1/account/gateway/anthropic"; const signingProxy = buildAnthropicClientOptions({ - model: buildModel({ - ...adaptiveProxySpec, - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/account/gateway/anthropic", - }), + model: buildModel({ ...adaptiveProxySpec, baseUrl: signingProxyUrl }), + apiKey: "sk-proxy-test", + interleavedThinking: true, + }); + const canonicalModel = buildModel({ + ...adaptiveProxySpec, + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + }); + const reroutedSigningProxy = buildAnthropicClientOptions({ + // Runtime provider overrides replace baseUrl without rebuilding the + // canonical model's official-endpoint compat. + model: { ...canonicalModel, baseUrl: signingProxyUrl }, apiKey: "sk-proxy-test", interleavedThinking: true, }); @@ -1831,12 +1840,12 @@ describe("Anthropic request fingerprint alignment", () => { apiKey: "sk-proxy-test", interleavedThinking: true, }); - // Vertex rawPredict is a signing endpoint but only accepts betas in the - // JSON body (`anthropic_beta`); the HTTP header would 400 (#5614). - const vertexProxy = buildAnthropicClientOptions({ + // Vertex rawPredict is signing regardless of provider id, but only + // accepts betas in the JSON body (`anthropic_beta`) (#5614). + const vertexRawPredict = buildAnthropicClientOptions({ model: buildModel({ ...adaptiveProxySpec, - provider: "google-vertex", + provider: "custom-vertex", baseUrl: "https://us-east5-aiplatform.googleapis.com/v1/projects/p/locations/us-east5/publishers/anthropic/models/claude-opus-4-8:rawPredict", }), @@ -1845,8 +1854,9 @@ describe("Anthropic request fingerprint alignment", () => { }); expect(signingProxy.defaultHeaders["anthropic-beta"]).toContain("interleaved-thinking-2025-05-14"); + expect(reroutedSigningProxy.defaultHeaders["anthropic-beta"]).toContain("interleaved-thinking-2025-05-14"); expect(nonSigningProxy.defaultHeaders["anthropic-beta"] ?? "").not.toContain("interleaved-thinking-2025-05-14"); - expect(vertexProxy.defaultHeaders["anthropic-beta"] ?? "").not.toContain("interleaved-thinking-2025-05-14"); + expect(vertexRawPredict.defaultHeaders["anthropic-beta"] ?? "").not.toContain("interleaved-thinking-2025-05-14"); }); it("adds legacy fine-grained tool-streaming beta only for tool requests on incompatible models", () => { diff --git a/packages/catalog/src/compat/anthropic.ts b/packages/catalog/src/compat/anthropic.ts index af3a5ac6c..f5e1be87f 100644 --- a/packages/catalog/src/compat/anthropic.ts +++ b/packages/catalog/src/compat/anthropic.ts @@ -4,7 +4,7 @@ * defaults come from provider ids, strict host checks, and model-id * classification, with explicit spec overrides assigned on top. */ -import { modelMatchesHost } from "../hosts"; +import { hostMatchesUrl, modelMatchesHost } from "../hosts"; import { hasOpus47ApiRestrictions, isAnthropicFableOrMythosModel, @@ -96,6 +96,23 @@ function isAzureAnthropicRoute(baseUrl?: string): boolean { return baseUrl !== undefined && AZURE_ANTHROPIC_URL_MARKER.test(baseUrl); } +/** + * Known non-official URLs that enforce Anthropic thinking signatures on replay. + * + * Runtime routing calls this with the effective URL because a model's resolved + * compat can be stale after Foundry or a provider base-URL override reroutes it. + */ +export function isAnthropicSigningProxyUrl(baseUrl?: string): boolean { + return ( + hostMatchesUrl(baseUrl, "githubCopilot") || + hostMatchesUrl(baseUrl, "zenmux") || + isCloudflareAnthropicGateway(baseUrl) || + isVertexAnthropicRoute(baseUrl) || + isBedrockAnthropicRoute(baseUrl) || + isAzureAnthropicRoute(baseUrl) + ); +} + /** Build the resolved anthropic-messages compat record for a model spec. */ export function buildAnthropicCompat(spec: ModelSpec<"anthropic-messages">): ResolvedAnthropicCompat { const baseUrl = spec.baseUrl; @@ -113,11 +130,8 @@ export function buildAnthropicCompat(spec: ModelSpec<"anthropic-messages">): Res // (issue #4192). const isZenmux = modelMatchesHost(spec, "zenmux"); const requiresThinkingEnabled = modelMatchesHost(spec, "moonshotNative") && matchesKimiMandatoryThinkingModel(spec); - const isVertex = isVertexAnthropicRoute(baseUrl); - const isBedrock = isBedrockAnthropicRoute(baseUrl); const isAzure = isAzureAnthropicRoute(baseUrl); - const signingEndpoint = - official || isCopilot || isZenmux || isCloudflareAnthropicGateway(baseUrl) || isVertex || isBedrock || isAzure; + const signingEndpoint = official || isCopilot || isZenmux || isAnthropicSigningProxyUrl(baseUrl); const compat: ResolvedAnthropicCompat = { officialEndpoint: official, signingEndpoint, From 8a0cb21092a98bd044de8526cb591fc0e0bd7490 Mon Sep 17 00:00:00 2001 From: roboomp Date: Sun, 26 Jul 2026 16:03:56 +0000 Subject: [PATCH 4/4] fix(ai): excluded copilot urls from interleaved http beta - Excluded effective github-copilot URLs from the interleaved beta path so custom-provider and rerouted Copilot hosts don't send the rejected header. - Covered a custom provider on a Copilot host omitting the beta. Fixes #6717 --- packages/ai/src/providers/anthropic.ts | 15 ++++++++++----- packages/ai/test/anthropic-alignment.test.ts | 12 ++++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 3b109a6a5..dc08672d4 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -3,7 +3,7 @@ import * as fs from "node:fs"; import { scheduler } from "node:timers/promises"; import * as tls from "node:tls"; import { isAnthropicSigningProxyUrl, isOfficialAnthropicApiUrl } from "@oh-my-pi/pi-catalog/compat/anthropic"; -import { isVertexRawPredictUrl } from "@oh-my-pi/pi-catalog/hosts"; +import { hostMatchesUrl, isVertexRawPredictUrl } from "@oh-my-pi/pi-catalog/hosts"; import { mapEffortToAnthropicAdaptiveEffort } from "@oh-my-pi/pi-catalog/model-thinking"; import { calculateCost, getBundledModel } from "@oh-my-pi/pi-catalog/models"; import { isAnthropicOAuthToken } from "@oh-my-pi/pi-catalog/utils"; @@ -2856,15 +2856,20 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A // official API, so only known non-official signing routes need the beta // (#6717). Classify the effective URL: Foundry and provider overrides can // reroute a model without rebuilding its materialized compat. - // Vertex rawPredict is excluded like every other HTTP-beta path here: it can - // only accept betas in the JSON body (`anthropic_beta`), never as this - // `anthropic-beta` HTTP header, so advertising it there earns a 400 (#5614). + // Two signing routes still can't take the beta as this `anthropic-beta` HTTP + // header, so they're excluded: Vertex rawPredict accepts betas only in the + // JSON body (`anthropic_beta`) and 400s on the header (#5614), and GitHub + // Copilot rejects Anthropic betas outright — the `github-copilot` provider + // branch below strips them, but a custom provider id or a canonical model + // rerouted to `api.githubcopilot.com` / `copilot-api.*` reaches the generic + // header builder instead, so exclude those effective URLs here too. const needsInterleavedBeta = interleavedThinking && (!model.thinking?.supportsDisplay || (!isOfficialAnthropicApiUrl(baseUrl) && isAnthropicSigningProxyUrl(baseUrl) && - !isVertexRawPredictUrl(baseUrl ?? ""))); + !isVertexRawPredictUrl(baseUrl ?? "") && + !hostMatchesUrl(baseUrl, "githubCopilot"))); const oauthToken = isOAuth ?? isAnthropicOAuthToken(apiKey); const supportsEagerToolInputStreaming = resolveEagerToolInputStreamingSupport(model, baseUrl); const needsFineGrainedToolStreamingBeta = diff --git a/packages/ai/test/anthropic-alignment.test.ts b/packages/ai/test/anthropic-alignment.test.ts index 7028654a6..cf9eb9797 100644 --- a/packages/ai/test/anthropic-alignment.test.ts +++ b/packages/ai/test/anthropic-alignment.test.ts @@ -1852,11 +1852,23 @@ describe("Anthropic request fingerprint alignment", () => { apiKey: "vertex-adc", interleavedThinking: true, }); + // A custom provider on a Copilot host is signing, but the proxy rejects + // Anthropic betas outright and this path bypasses the provider branch. + const copilotUrlProxy = buildAnthropicClientOptions({ + model: buildModel({ + ...adaptiveProxySpec, + provider: "custom-copilot", + baseUrl: "https://api.githubcopilot.com", + }), + apiKey: "ghu_test", + interleavedThinking: true, + }); expect(signingProxy.defaultHeaders["anthropic-beta"]).toContain("interleaved-thinking-2025-05-14"); expect(reroutedSigningProxy.defaultHeaders["anthropic-beta"]).toContain("interleaved-thinking-2025-05-14"); expect(nonSigningProxy.defaultHeaders["anthropic-beta"] ?? "").not.toContain("interleaved-thinking-2025-05-14"); expect(vertexRawPredict.defaultHeaders["anthropic-beta"] ?? "").not.toContain("interleaved-thinking-2025-05-14"); + expect(copilotUrlProxy.defaultHeaders["anthropic-beta"] ?? "").not.toContain("interleaved-thinking-2025-05-14"); }); it("adds legacy fine-grained tool-streaming beta only for tool requests on incompatible models", () => {