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');
|
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 () => {
|
it('prefers native /v1/responses for claude-family /v1/responses requests that explicitly ask for encrypted reasoning', async () => {
|
||||||
selectChannelMock.mockReturnValue({
|
selectChannelMock.mockReturnValue({
|
||||||
channel: { id: 11, routeId: 22 },
|
channel: { id: 11, routeId: 22 },
|
||||||
|
|||||||
@@ -216,6 +216,48 @@ describe('resolveUpstreamEndpointCandidates', () => {
|
|||||||
expect(order).toEqual(['responses', 'chat', 'messages']);
|
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 () => {
|
it('learns a better endpoint from explicit upstream protocol errors', async () => {
|
||||||
recordUpstreamEndpointFailure({
|
recordUpstreamEndpointFailure({
|
||||||
siteId: baseContext.site.id,
|
siteId: baseContext.site.id,
|
||||||
|
|||||||
@@ -714,15 +714,37 @@ function inferSuggestedEndpointFromError(errorText?: string | null): UpstreamEnd
|
|||||||
|
|
||||||
function shouldBlockEndpointByError(status: number, errorText?: string | null): boolean {
|
function shouldBlockEndpointByError(status: number, errorText?: string | null): boolean {
|
||||||
if (isEndpointDispatchDeniedError(status, errorText)) return true;
|
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();
|
const text = (errorText || '').toLowerCase();
|
||||||
return (
|
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('please use /v1/')
|
||||||
|| text.includes('does not allow /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 {
|
export function resetUpstreamEndpointRuntimeState(): void {
|
||||||
endpointRuntimeStates.clear();
|
endpointRuntimeStates.clear();
|
||||||
}
|
}
|
||||||
@@ -739,6 +761,8 @@ export function recordUpstreamEndpointSuccess(input: {
|
|||||||
wantsNativeResponsesReasoning?: boolean;
|
wantsNativeResponsesReasoning?: boolean;
|
||||||
};
|
};
|
||||||
}): void {
|
}): void {
|
||||||
|
if (!shouldRememberSuccessfulEndpoint(input)) return;
|
||||||
|
|
||||||
const nowMs = Date.now();
|
const nowMs = Date.now();
|
||||||
const key = buildEndpointRuntimeStateKey({
|
const key = buildEndpointRuntimeStateKey({
|
||||||
siteId: input.siteId,
|
siteId: input.siteId,
|
||||||
|
|||||||
Reference in New Issue
Block a user