diff --git a/src/server/routes/proxy/chat.stream.test.ts b/src/server/routes/proxy/chat.stream.test.ts
index 156b280..1ac6b81 100644
--- a/src/server/routes/proxy/chat.stream.test.ts
+++ b/src/server/routes/proxy/chat.stream.test.ts
@@ -1845,6 +1845,67 @@ describe('chat proxy stream behavior', () => {
expect(secondUrl).toContain('/v1/chat/completions');
});
+ it('falls back to /v1/responses for /v1/chat/completions when messages/chat endpoints return 502', async () => {
+ selectChannelMock.mockReturnValue({
+ channel: { id: 11, routeId: 22 },
+ site: { name: 'generic-site', url: 'https://generic.example.com', platform: 'new-api' },
+ account: { id: 33, username: 'demo-user' },
+ tokenName: 'default',
+ tokenValue: 'sk-generic',
+ actualModel: 'claude-haiku-4-5-20251001',
+ });
+
+ fetchMock
+ .mockResolvedValueOnce(new Response(
+ '
502 Bad GatewayCloudflare',
+ {
+ status: 502,
+ headers: { 'content-type': 'text/html; charset=UTF-8' },
+ },
+ ))
+ .mockResolvedValueOnce(new Response(
+ '502 Bad GatewayCloudflare',
+ {
+ status: 502,
+ headers: { 'content-type': 'text/html; charset=UTF-8' },
+ },
+ ))
+ .mockResolvedValueOnce(new Response(JSON.stringify({
+ id: 'resp_anyrouter_fallback',
+ object: 'response',
+ model: 'claude-haiku-4-5-20251001',
+ status: 'completed',
+ output_text: 'ok via responses fallback after 502',
+ usage: { input_tokens: 5, output_tokens: 2, total_tokens: 7 },
+ }), {
+ status: 200,
+ headers: { 'content-type': 'application/json' },
+ }));
+
+ const response = await app.inject({
+ method: 'POST',
+ url: '/v1/chat/completions',
+ payload: {
+ model: 'claude-haiku-4-5-20251001',
+ stream: false,
+ messages: [{ role: 'user', content: 'hello' }],
+ },
+ });
+
+ expect(response.statusCode).toBe(200);
+ expect(fetchMock).toHaveBeenCalledTimes(3);
+
+ const [firstUrl] = fetchMock.mock.calls[0] as [string, any];
+ const [secondUrl] = fetchMock.mock.calls[1] as [string, any];
+ const [thirdUrl] = fetchMock.mock.calls[2] as [string, any];
+ expect(firstUrl).toContain('/v1/messages');
+ expect(secondUrl).toContain('/v1/chat/completions');
+ expect(thirdUrl).toContain('/v1/responses');
+
+ const body = response.json();
+ expect(body?.choices?.[0]?.message?.content).toContain('ok via responses fallback after 502');
+ });
+
it('forces openai platform to use /v1/chat/completions for claude downstream requests', async () => {
selectChannelMock.mockReturnValue({
channel: { id: 11, routeId: 22 },
diff --git a/src/server/routes/proxy/chat.ts b/src/server/routes/proxy/chat.ts
index 70a3b06..149c779 100644
--- a/src/server/routes/proxy/chat.ts
+++ b/src/server/routes/proxy/chat.ts
@@ -285,7 +285,8 @@ async function handleChatProxyRequest(
return null;
},
shouldDowngrade: (ctx) => (
- isEndpointDowngradeError(ctx.response.status, ctx.rawErrText)
+ ctx.response.status >= 500
+ || isEndpointDowngradeError(ctx.response.status, ctx.rawErrText)
|| isMessagesRequiredError(ctx.rawErrText)
),
onDowngrade: (ctx) => {