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
This commit is contained in:
roboomp
2026-07-26 15:58:12 +00:00
parent 5c3a7d8d5f
commit 6c254ca4f1
3 changed files with 49 additions and 21 deletions
+9 -5
View File
@@ -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;
+21 -11
View File
@@ -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", () => {
+19 -5
View File
@@ -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,