From 0e4a61695fa80a3d246279623f791fa96034c6f0 Mon Sep 17 00:00:00 2001 From: Danil Silantyev Date: Sat, 4 Jul 2026 12:49:44 +0700 Subject: [PATCH] fix(applicability): match all proxy Anthropic message routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit shouldTransformAnthropicMessages() gated the path with !path.endsWith('/v1/messages'), but createProxy() routes three exact paths: /v1/messages, /anthropic/v1/messages, and /anthropic/messages. So an SDK/host using the public helper to pre-gate got unsupported_path on /anthropic/messages — a request the proxy actually transforms — while endsWith would also have wrongly accepted an unrelated /foo/v1/messages. - lift isAnthropicMessagesPath() into applicability.ts (proxy.ts already imports from it; no cycle) as the single source of truth - proxy.ts imports it instead of its own copy - shouldTransformAnthropicMessages() uses it, so helper and router agree - public-api tests cover /anthropic/v1/messages and /anthropic/messages (eligible) plus /v1/messages/count_tokens (still unsupported_path) --- src/core/applicability.ts | 14 +++++++++++++- src/core/proxy.ts | 8 +------- tests/public-api.test.ts | 10 ++++++++++ 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/core/applicability.ts b/src/core/applicability.ts index f90655a..445fb21 100644 --- a/src/core/applicability.ts +++ b/src/core/applicability.ts @@ -90,13 +90,25 @@ export function isPxpipeSupportedGptModel(model: string | null | undefined): boo return isAllowed(model); } +/** Canonical set of Anthropic Messages routes pxpipe transforms. Shared with + * createProxy (src/core/proxy.ts) so the public applicability helper and the + * proxy router can never disagree on which paths are eligible — they did: the + * proxy accepts /anthropic/messages, but the helper's old `endsWith` check + * rejected it (and would have wrongly accepted /foo/v1/messages). Exact matches + * only, so /v1/messages/count_tokens stays unsupported. */ +export function isAnthropicMessagesPath(pathname: string): boolean { + return pathname === '/v1/messages' + || pathname === '/anthropic/v1/messages' + || pathname === '/anthropic/messages'; +} + export function shouldTransformAnthropicMessages( input: PxpipeApplicabilityInput, ): { eligible: boolean; reason: PxpipeApplicabilityReason } { if (input.method !== undefined && input.method !== null && input.method.toUpperCase() !== 'POST') { return { eligible: false, reason: 'unsupported_method' }; } - if (input.path !== undefined && input.path !== null && !input.path.endsWith('/v1/messages')) { + if (input.path !== undefined && input.path !== null && !isAnthropicMessagesPath(input.path)) { return { eligible: false, reason: 'unsupported_path' }; } if (input.bodyBytes !== undefined && input.bodyBytes !== null && input.bodyBytes <= 0) { diff --git a/src/core/proxy.ts b/src/core/proxy.ts index 05525e1..d2d7935 100644 --- a/src/core/proxy.ts +++ b/src/core/proxy.ts @@ -5,7 +5,7 @@ import { transformRequest, type TransformOptions, type TransformInfo } from './transform.js'; import { transformOpenAIChatCompletions, transformOpenAIResponses } from './openai.js'; -import { isPxpipeSupportedGptModel, isPxpipeSupportedModel } from './applicability.js'; +import { isAnthropicMessagesPath, isPxpipeSupportedGptModel, isPxpipeSupportedModel } from './applicability.js'; import { buildBaselineCountTokensBody, buildCacheablePrefixCountTokensBody, @@ -508,12 +508,6 @@ function isProviderPrefixedPath(pathname: string): boolean { return PASSTHROUGH_PREFIXES.some((prefix) => pathname.startsWith(prefix)); } -function isAnthropicMessagesPath(pathname: string): boolean { - return pathname === '/v1/messages' - || pathname === '/anthropic/v1/messages' - || pathname === '/anthropic/messages'; -} - function isOpenAIChatPath(pathname: string): boolean { return pathname === '/v1/chat/completions' || pathname === '/openai/v1/chat/completions'; } diff --git a/tests/public-api.test.ts b/tests/public-api.test.ts index e10a42a..51d1f3b 100644 --- a/tests/public-api.test.ts +++ b/tests/public-api.test.ts @@ -143,6 +143,16 @@ describe('public library API', () => { path: '/v1/messages', bodyBytes: 10, }).reason).toBe('unsupported_method'); + // Provider-prefixed routes createProxy() also transforms must be eligible + // here too — the old endsWith('/v1/messages') check rejected /anthropic/messages. + for (const path of ['/anthropic/v1/messages', '/anthropic/messages']) { + expect(shouldTransformAnthropicMessages({ + model: 'claude-fable-5', + method: 'POST', + path, + bodyBytes: 10, + })).toEqual({ eligible: true, reason: 'eligible' }); + } expect(shouldTransformAnthropicMessages({ model: 'claude-fable-5', method: 'POST',