forked from boos/BoosAPI
[codex] add setting to disable cross-protocol fallback (#332)
* Add toggle to disable cross-protocol fallback * Tighten runtime settings payload typing
This commit is contained in:
@@ -127,6 +127,7 @@ describe('chat proxy stream behavior', () => {
|
||||
overrideRaw: [],
|
||||
filter: [],
|
||||
};
|
||||
(config as any).disableCrossProtocolFallback = false;
|
||||
config.proxyEmptyContentFailEnabled = false;
|
||||
config.proxyErrorKeywords = [];
|
||||
});
|
||||
@@ -3077,6 +3078,43 @@ describe('chat proxy stream behavior', () => {
|
||||
expect(body?.choices?.[0]?.message?.content).toContain('ok via responses fallback after 502');
|
||||
});
|
||||
|
||||
it('stops after the first failed protocol when cross protocol fallback is disabled', async () => {
|
||||
(config as any).disableCrossProtocolFallback = true;
|
||||
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(
|
||||
'<!DOCTYPE html><html><head><title>502 Bad Gateway</title></head><body>Cloudflare</body></html>',
|
||||
{
|
||||
status: 502,
|
||||
headers: { 'content-type': 'text/html; charset=UTF-8' },
|
||||
},
|
||||
));
|
||||
|
||||
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(502);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
const [firstUrl] = fetchMock.mock.calls[0] as [string, any];
|
||||
expect(firstUrl).toContain('/v1/messages');
|
||||
const body = response.json();
|
||||
expect(body?.error?.message).toContain('/v1/messages');
|
||||
});
|
||||
|
||||
it('continues to /v1/responses when /v1/messages dispatch is denied for /v1/chat/completions', async () => {
|
||||
selectChannelMock.mockReturnValue({
|
||||
channel: { id: 11, routeId: 22 },
|
||||
|
||||
@@ -156,6 +156,40 @@ describe('executeEndpointFlow', () => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('does not downgrade to the next endpoint when cross protocol fallback is disabled', async () => {
|
||||
fetchMock
|
||||
.mockResolvedValueOnce(toUndiciResponse(new Response(JSON.stringify({
|
||||
error: { message: 'unsupported endpoint', type: 'invalid_request_error' },
|
||||
}), {
|
||||
status: 404,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
})))
|
||||
.mockResolvedValueOnce(toUndiciResponse(new Response(JSON.stringify({ ok: true }), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
})));
|
||||
|
||||
const onDowngrade = vi.fn();
|
||||
const result = await executeEndpointFlow({
|
||||
siteUrl: 'https://example.com',
|
||||
endpointCandidates: ['responses', 'chat'],
|
||||
buildRequest: (endpoint) => endpoint === 'responses'
|
||||
? requestFor('/v1/responses')
|
||||
: { ...requestFor('/v1/chat/completions'), endpoint },
|
||||
shouldDowngrade: () => true,
|
||||
disableCrossProtocolFallback: true,
|
||||
onDowngrade,
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.status).toBe(404);
|
||||
expect(result.errText).toContain('/v1/responses');
|
||||
}
|
||||
expect(onDowngrade).not.toHaveBeenCalled();
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('emits attempt callbacks for failed and successful endpoint probes', async () => {
|
||||
fetchMock
|
||||
.mockResolvedValueOnce(toUndiciResponse(new Response(JSON.stringify({
|
||||
|
||||
@@ -65,6 +65,7 @@ export function withUpstreamPath(path: string, message: string): string {
|
||||
type ExecuteEndpointFlowInput = {
|
||||
siteUrl: string;
|
||||
proxyUrl?: string | null;
|
||||
disableCrossProtocolFallback?: boolean;
|
||||
endpointCandidates: UpstreamEndpoint[];
|
||||
buildRequest: (endpoint: UpstreamEndpoint, endpointIndex: number) => BuiltEndpointRequest;
|
||||
dispatchRequest?: (
|
||||
@@ -189,6 +190,12 @@ export async function executeEndpointFlow(input: ExecuteEndpointFlowInput): Prom
|
||||
}, 'onAttemptFailure');
|
||||
|
||||
const isLastEndpoint = endpointIndex >= endpointCount - 1;
|
||||
if (input.disableCrossProtocolFallback && !isLastEndpoint) {
|
||||
finalStatus = response.status;
|
||||
finalErrText = errText;
|
||||
finalRawErrText = rawErrText;
|
||||
break;
|
||||
}
|
||||
const shouldDowngrade = !isLastEndpoint && !!input.shouldDowngrade?.(baseContext);
|
||||
if (shouldDowngrade) {
|
||||
await runEndpointFlowHook(input.onDowngrade, {
|
||||
|
||||
Reference in New Issue
Block a user