diff --git a/src/server/routes/proxy/upstreamEndpoint.test.ts b/src/server/routes/proxy/upstreamEndpoint.test.ts index 09a6d4d..77595f1 100644 --- a/src/server/routes/proxy/upstreamEndpoint.test.ts +++ b/src/server/routes/proxy/upstreamEndpoint.test.ts @@ -28,7 +28,7 @@ describe('resolveUpstreamEndpointCandidates', () => { fetchModelPricingCatalogMock.mockResolvedValue(null); }); - it('uses responses -> messages -> chat for unknown platforms', async () => { + it('uses downstream-aligned endpoint priority for unknown platforms', async () => { const openaiOrder = await resolveUpstreamEndpointCandidates( { ...baseContext, @@ -37,7 +37,7 @@ describe('resolveUpstreamEndpointCandidates', () => { 'gpt-5.3', 'openai', ); - expect(openaiOrder).toEqual(['responses', 'messages', 'chat']); + expect(openaiOrder).toEqual(['chat', 'messages', 'responses']); const claudeOrder = await resolveUpstreamEndpointCandidates( { @@ -47,7 +47,7 @@ describe('resolveUpstreamEndpointCandidates', () => { 'gpt-5.3', 'claude', ); - expect(claudeOrder).toEqual(['responses', 'messages', 'chat']); + expect(claudeOrder).toEqual(['messages', 'chat', 'responses']); const responsesOrder = await resolveUpstreamEndpointCandidates( { @@ -57,7 +57,7 @@ describe('resolveUpstreamEndpointCandidates', () => { 'gpt-5.3', 'responses', ); - expect(responsesOrder).toEqual(['responses', 'messages', 'chat']); + expect(responsesOrder).toEqual(['responses', 'chat', 'messages']); }); it('keeps explicit platform priority rules', async () => { diff --git a/src/server/routes/proxy/upstreamEndpoint.ts b/src/server/routes/proxy/upstreamEndpoint.ts index c5917d5..e5808ec 100644 --- a/src/server/routes/proxy/upstreamEndpoint.ts +++ b/src/server/routes/proxy/upstreamEndpoint.ts @@ -381,9 +381,17 @@ function preferredEndpointOrder(downstreamFormat: EndpointPreference, sitePlatfo return ['messages']; } - // Unknown/generic upstreams: try the most expressive protocol first, - // then degrade to anthropic messages, and finally chat completions. - return ['responses', 'messages', 'chat']; + // Unknown/generic upstreams: prefer endpoint family that matches the + // downstream API surface, then degrade progressively. + if (downstreamFormat === 'responses') { + return ['responses', 'chat', 'messages']; + } + + if (downstreamFormat === 'claude') { + return ['messages', 'chat', 'responses']; + } + + return ['chat', 'messages', 'responses']; } export async function resolveUpstreamEndpointCandidates( @@ -438,13 +446,15 @@ export async function resolveUpstreamEndpointCandidates( if (supported.size === 0) return preferred; - const filtered = preferred.filter((endpoint) => supported.has(endpoint)); - if (filtered.length === 0) return preferred; + const firstSupported = preferred.find((endpoint) => supported.has(endpoint)); + if (!firstSupported) return preferred; - // Keep non-catalog endpoints as best-effort fallbacks because some - // upstreams expose incomplete/incorrect endpoint metadata. - const fallback = preferred.filter((endpoint) => !filtered.includes(endpoint)); - return [...filtered, ...fallback]; + // Catalog metadata can be incomplete/inaccurate, so only use it to pick + // the first attempt. Keep downstream-driven fallback order unchanged. + return [ + firstSupported, + ...preferred.filter((endpoint) => endpoint !== firstSupported), + ]; } catch { return preferred; }