forked from boos/BoosAPI
fix responses endpoint memory
This commit is contained in:
@@ -2032,6 +2032,87 @@ describe('chat proxy stream behavior', () => {
|
||||
expect(targetUrl).toContain('/v1/messages');
|
||||
});
|
||||
|
||||
it('does not stick generic /v1/responses traffic to /v1/messages after a fallback success', async () => {
|
||||
selectChannelMock.mockReturnValue({
|
||||
channel: { id: 11, routeId: 22 },
|
||||
site: { name: 'generic-site', url: 'https://upstream.example.com', platform: 'new-api' },
|
||||
account: { id: 33, username: 'demo-user' },
|
||||
tokenName: 'default',
|
||||
tokenValue: 'sk-demo',
|
||||
actualModel: 'upstream-gpt',
|
||||
});
|
||||
|
||||
fetchMock
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify({
|
||||
error: { message: 'Gateway time-out', type: 'upstream_error' },
|
||||
}), {
|
||||
status: 504,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
}))
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify({
|
||||
error: { message: 'Bad gateway', type: 'upstream_error' },
|
||||
}), {
|
||||
status: 502,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
}))
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify({
|
||||
id: 'msg_fallback_1',
|
||||
type: 'message',
|
||||
model: 'upstream-gpt',
|
||||
content: [{ type: 'text', text: 'ok via messages fallback' }],
|
||||
stop_reason: 'end_turn',
|
||||
usage: { input_tokens: 7, output_tokens: 3, total_tokens: 10 },
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
}))
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify({
|
||||
id: 'resp_recovered_1',
|
||||
object: 'response',
|
||||
model: 'upstream-gpt',
|
||||
status: 'completed',
|
||||
output_text: 'ok via recovered responses',
|
||||
usage: { input_tokens: 6, output_tokens: 2, total_tokens: 8 },
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
}));
|
||||
|
||||
const firstResponse = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/responses',
|
||||
payload: {
|
||||
model: 'gpt-5.4',
|
||||
input: 'hello',
|
||||
},
|
||||
});
|
||||
|
||||
expect(firstResponse.statusCode).toBe(200);
|
||||
expect(firstResponse.json().output_text).toContain('ok via messages fallback');
|
||||
|
||||
const secondResponse = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/responses',
|
||||
payload: {
|
||||
model: 'gpt-5.4',
|
||||
input: 'hello again',
|
||||
},
|
||||
});
|
||||
|
||||
expect(secondResponse.statusCode).toBe(200);
|
||||
expect(secondResponse.json().output_text).toContain('ok via recovered responses');
|
||||
expect(fetchMock).toHaveBeenCalledTimes(4);
|
||||
|
||||
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];
|
||||
const [fourthUrl] = fetchMock.mock.calls[3] as [string, any];
|
||||
expect(firstUrl).toContain('/v1/responses');
|
||||
expect(secondUrl).toContain('/v1/chat/completions');
|
||||
expect(thirdUrl).toContain('/v1/messages');
|
||||
expect(fourthUrl).toContain('/v1/responses');
|
||||
});
|
||||
|
||||
it('prefers native /v1/responses for claude-family /v1/responses requests that explicitly ask for encrypted reasoning', async () => {
|
||||
selectChannelMock.mockReturnValue({
|
||||
channel: { id: 11, routeId: 22 },
|
||||
|
||||
@@ -216,6 +216,48 @@ describe('resolveUpstreamEndpointCandidates', () => {
|
||||
expect(order).toEqual(['responses', 'chat', 'messages']);
|
||||
});
|
||||
|
||||
it('does not remember messages fallback success for generic /v1/responses requests', async () => {
|
||||
recordUpstreamEndpointSuccess({
|
||||
siteId: baseContext.site.id,
|
||||
endpoint: 'messages',
|
||||
downstreamFormat: 'responses',
|
||||
modelName: 'gpt-5.3',
|
||||
});
|
||||
|
||||
const order = await resolveUpstreamEndpointCandidates(
|
||||
{
|
||||
...baseContext,
|
||||
site: { ...baseContext.site, platform: 'new-api' },
|
||||
},
|
||||
'gpt-5.3',
|
||||
'responses',
|
||||
);
|
||||
|
||||
expect(order).toEqual(['responses', 'chat', 'messages']);
|
||||
});
|
||||
|
||||
it('does not block generic /v1/responses endpoints on transient upstream errors', async () => {
|
||||
recordUpstreamEndpointFailure({
|
||||
siteId: baseContext.site.id,
|
||||
endpoint: 'responses',
|
||||
downstreamFormat: 'responses',
|
||||
modelName: 'gpt-5.3',
|
||||
status: 504,
|
||||
errorText: '{"error":{"message":"Gateway time-out","type":"upstream_error"}}',
|
||||
});
|
||||
|
||||
const order = await resolveUpstreamEndpointCandidates(
|
||||
{
|
||||
...baseContext,
|
||||
site: { ...baseContext.site, platform: 'new-api' },
|
||||
},
|
||||
'gpt-5.3',
|
||||
'responses',
|
||||
);
|
||||
|
||||
expect(order).toEqual(['responses', 'chat', 'messages']);
|
||||
});
|
||||
|
||||
it('learns a better endpoint from explicit upstream protocol errors', async () => {
|
||||
recordUpstreamEndpointFailure({
|
||||
siteId: baseContext.site.id,
|
||||
|
||||
@@ -714,15 +714,37 @@ function inferSuggestedEndpointFromError(errorText?: string | null): UpstreamEnd
|
||||
|
||||
function shouldBlockEndpointByError(status: number, errorText?: string | null): boolean {
|
||||
if (isEndpointDispatchDeniedError(status, errorText)) return true;
|
||||
if (isEndpointDowngradeError(status, errorText)) return true;
|
||||
if (status === 404 || status === 405 || status === 415 || status === 501) return true;
|
||||
if (isUnsupportedMediaTypeError(status, errorText)) return true;
|
||||
|
||||
const text = (errorText || '').toLowerCase();
|
||||
return (
|
||||
text.includes('unsupported legacy protocol')
|
||||
text.includes('convert_request_failed')
|
||||
|| text.includes('endpoint_not_found')
|
||||
|| text.includes('unknown_endpoint')
|
||||
|| text.includes('unsupported_endpoint')
|
||||
|| text.includes('unsupported_path')
|
||||
|| text.includes('not_found_error')
|
||||
|| text.includes('unsupported legacy protocol')
|
||||
|| text.includes('please use /v1/')
|
||||
|| text.includes('does not allow /v1/')
|
||||
|| text.includes('unknown endpoint')
|
||||
|| text.includes('unsupported endpoint')
|
||||
|| text.includes('unsupported path')
|
||||
|| text.includes('unrecognized request url')
|
||||
|| text.includes('no route matched')
|
||||
|| text.includes('does not exist')
|
||||
);
|
||||
}
|
||||
|
||||
function shouldRememberSuccessfulEndpoint(input: {
|
||||
endpoint: UpstreamEndpoint;
|
||||
downstreamFormat: EndpointPreference;
|
||||
}): boolean {
|
||||
if (input.downstreamFormat !== 'responses') return true;
|
||||
return input.endpoint === 'responses';
|
||||
}
|
||||
|
||||
export function resetUpstreamEndpointRuntimeState(): void {
|
||||
endpointRuntimeStates.clear();
|
||||
}
|
||||
@@ -739,6 +761,8 @@ export function recordUpstreamEndpointSuccess(input: {
|
||||
wantsNativeResponsesReasoning?: boolean;
|
||||
};
|
||||
}): void {
|
||||
if (!shouldRememberSuccessfulEndpoint(input)) return;
|
||||
|
||||
const nowMs = Date.now();
|
||||
const key = buildEndpointRuntimeStateKey({
|
||||
siteId: input.siteId,
|
||||
|
||||
Reference in New Issue
Block a user