feat(server): add proactive channel probes and load-aware stable routing / 服务端新增主动探活与负载感知稳定路由 (#330)
* wip: preserve local changes before upstream merge * Improve proxy routing stability and add stable-first strategy * Sync explicit-group routing strategy to eligible source routes * Fix proxy retry and runtime health review issues * fix: make proxy channel retry attempts configurable * fix: harden proxy retry recovery and bookkeeping * fix: address pr 257 review comments * feat(proxy): add sticky session channel coordination * feat(server): add proactive channel probes and load-aware stable routing * test(server): stabilize session load routing selection assertion * fix(proxy): harden codex forwarding stability * fix(router): rotate stable-first channels by healthy order * fix(server): address probe review feedback * fix(server): address probe review follow-ups * fix(ci): address CodeQL and core test failures * feat(server): refine stable-first routing recovery and observation * Finish PR 330 settings and review fixes * Fix chat site endpoint CI mock * Address latest PR 330 review comments --------- Co-authored-by: Cita <juricek.chen@gmail.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
extractResponsesTerminalResponseId,
|
||||
isResponsesPreviousResponseNotFoundError,
|
||||
shouldInferResponsesPreviousResponseId,
|
||||
stripResponsesPreviousResponseId,
|
||||
withResponsesPreviousResponseId,
|
||||
} from './continuation.js';
|
||||
|
||||
describe('responses continuation helpers', () => {
|
||||
it('infers previous_response_id only for tool-output follow-up turns that omit it', () => {
|
||||
expect(shouldInferResponsesPreviousResponseId({
|
||||
input: [{
|
||||
type: 'function_call_output',
|
||||
call_id: 'call_1',
|
||||
output: '{"ok":true}',
|
||||
}],
|
||||
}, 'resp_prev_1')).toBe(true);
|
||||
|
||||
expect(shouldInferResponsesPreviousResponseId({
|
||||
previous_response_id: 'resp_existing',
|
||||
input: [{
|
||||
type: 'function_call_output',
|
||||
call_id: 'call_1',
|
||||
output: '{"ok":true}',
|
||||
}],
|
||||
}, 'resp_prev_1')).toBe(false);
|
||||
|
||||
expect(shouldInferResponsesPreviousResponseId({
|
||||
input: [{
|
||||
type: 'message',
|
||||
role: 'user',
|
||||
content: [{ type: 'input_text', text: 'hello' }],
|
||||
}],
|
||||
}, 'resp_prev_1')).toBe(false);
|
||||
});
|
||||
|
||||
it('adds and removes previous_response_id without mutating other fields', () => {
|
||||
const body = {
|
||||
model: 'gpt-5.4',
|
||||
input: [{
|
||||
type: 'function_call_output',
|
||||
call_id: 'call_1',
|
||||
output: '{"ok":true}',
|
||||
}],
|
||||
};
|
||||
|
||||
const withPrevious = withResponsesPreviousResponseId(body, 'resp_prev_1');
|
||||
expect(withPrevious).toMatchObject({
|
||||
model: 'gpt-5.4',
|
||||
previous_response_id: 'resp_prev_1',
|
||||
});
|
||||
|
||||
const stripped = stripResponsesPreviousResponseId(withPrevious);
|
||||
expect(stripped.removed).toBe(true);
|
||||
expect(stripped.body.previous_response_id).toBeUndefined();
|
||||
expect(stripped.body.input).toEqual(body.input);
|
||||
});
|
||||
|
||||
it('detects previous_response_not_found from either raw text or structured payloads', () => {
|
||||
expect(isResponsesPreviousResponseNotFoundError({
|
||||
rawErrText: JSON.stringify({
|
||||
error: {
|
||||
code: 'previous_response_not_found',
|
||||
message: 'previous_response_not_found',
|
||||
},
|
||||
}),
|
||||
})).toBe(true);
|
||||
|
||||
expect(isResponsesPreviousResponseNotFoundError({
|
||||
payload: {
|
||||
type: 'response.failed',
|
||||
response: {
|
||||
error: {
|
||||
message: 'Previous response id not found',
|
||||
type: 'invalid_request_error',
|
||||
},
|
||||
},
|
||||
},
|
||||
})).toBe(true);
|
||||
|
||||
expect(isResponsesPreviousResponseNotFoundError({
|
||||
rawErrText: JSON.stringify({
|
||||
error: {
|
||||
code: 'rate_limit_exceeded',
|
||||
message: 'too many requests',
|
||||
},
|
||||
}),
|
||||
})).toBe(false);
|
||||
});
|
||||
|
||||
it('extracts terminal response ids from responses payloads and websocket terminal events', () => {
|
||||
expect(extractResponsesTerminalResponseId({
|
||||
id: 'resp_1',
|
||||
object: 'response',
|
||||
status: 'completed',
|
||||
})).toBe('resp_1');
|
||||
|
||||
expect(extractResponsesTerminalResponseId({
|
||||
type: 'response.failed',
|
||||
response: {
|
||||
id: 'resp_2',
|
||||
status: 'failed',
|
||||
error: {
|
||||
message: 'tool failed',
|
||||
},
|
||||
},
|
||||
})).toBe('resp_2');
|
||||
|
||||
expect(extractResponsesTerminalResponseId({
|
||||
type: 'response.created',
|
||||
response: {
|
||||
id: 'resp_3',
|
||||
status: 'in_progress',
|
||||
},
|
||||
})).toBeNull();
|
||||
|
||||
expect(extractResponsesTerminalResponseId({
|
||||
id: 'resp_in_progress',
|
||||
object: 'response',
|
||||
status: 'in_progress',
|
||||
})).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function asTrimmedString(value: unknown): string {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
|
||||
const RESPONSES_TOOL_OUTPUT_TYPES = new Set([
|
||||
'function_call_output',
|
||||
'custom_tool_call_output',
|
||||
]);
|
||||
|
||||
const RESPONSES_TERMINAL_STATUSES = new Set([
|
||||
'completed',
|
||||
'failed',
|
||||
'incomplete',
|
||||
]);
|
||||
|
||||
function collectResponsesErrorFragments(value: unknown): string[] {
|
||||
if (!value) return [];
|
||||
if (typeof value === 'string') return [value];
|
||||
if (!isRecord(value)) return [];
|
||||
|
||||
const fragments = [
|
||||
value.type,
|
||||
value.code,
|
||||
value.message,
|
||||
value.reason,
|
||||
]
|
||||
.map((entry) => asTrimmedString(entry))
|
||||
.filter(Boolean);
|
||||
|
||||
if (isRecord(value.error)) {
|
||||
fragments.push(...collectResponsesErrorFragments(value.error));
|
||||
}
|
||||
|
||||
if (isRecord(value.response)) {
|
||||
fragments.push(...collectResponsesErrorFragments(value.response));
|
||||
}
|
||||
|
||||
if (isRecord(value.incomplete_details)) {
|
||||
fragments.push(...collectResponsesErrorFragments(value.incomplete_details));
|
||||
}
|
||||
|
||||
return fragments;
|
||||
}
|
||||
|
||||
function hasResponsesToolOutput(input: unknown): boolean {
|
||||
if (!Array.isArray(input)) return false;
|
||||
return input.some((item) => {
|
||||
if (!isRecord(item)) return false;
|
||||
const type = asTrimmedString(item.type).toLowerCase();
|
||||
if (!RESPONSES_TOOL_OUTPUT_TYPES.has(type)) return false;
|
||||
return asTrimmedString(item.call_id ?? item.id).length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
export function shouldInferResponsesPreviousResponseId(
|
||||
body: Record<string, unknown> | null | undefined,
|
||||
candidatePreviousResponseId: unknown,
|
||||
): candidatePreviousResponseId is string {
|
||||
if (!body) return false;
|
||||
if (asTrimmedString(body.previous_response_id)) return false;
|
||||
const candidate = asTrimmedString(candidatePreviousResponseId);
|
||||
if (!candidate) return false;
|
||||
return hasResponsesToolOutput(body.input);
|
||||
}
|
||||
|
||||
export function withResponsesPreviousResponseId(
|
||||
body: Record<string, unknown>,
|
||||
previousResponseId: string,
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
...body,
|
||||
previous_response_id: previousResponseId.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
export function stripResponsesPreviousResponseId(
|
||||
body: Record<string, unknown>,
|
||||
): { body: Record<string, unknown>; removed: boolean } {
|
||||
if (!Object.prototype.hasOwnProperty.call(body, 'previous_response_id')) {
|
||||
return { body, removed: false };
|
||||
}
|
||||
const next = { ...body };
|
||||
delete next.previous_response_id;
|
||||
return { body: next, removed: true };
|
||||
}
|
||||
|
||||
export function isResponsesPreviousResponseNotFoundError(input: {
|
||||
rawErrText?: string | null;
|
||||
payload?: unknown;
|
||||
}): boolean {
|
||||
const fragments = [
|
||||
...collectResponsesErrorFragments(input.payload),
|
||||
];
|
||||
const rawErrText = asTrimmedString(input.rawErrText);
|
||||
if (rawErrText) fragments.push(rawErrText);
|
||||
const combined = fragments.join(' ').toLowerCase();
|
||||
if (!combined) return false;
|
||||
return (
|
||||
combined.includes('previous_response_not_found')
|
||||
|| /previous[\s_-]*response(?:[\s_-]*(?:id|identifier))?[\s_-]*not[\s_-]*found/i.test(combined)
|
||||
);
|
||||
}
|
||||
|
||||
export function extractResponsesTerminalResponseId(payload: unknown): string | null {
|
||||
if (!isRecord(payload)) return null;
|
||||
|
||||
const type = asTrimmedString(payload.type).toLowerCase();
|
||||
if (
|
||||
type === 'response.completed'
|
||||
|| type === 'response.failed'
|
||||
|| type === 'response.incomplete'
|
||||
) {
|
||||
if (isRecord(payload.response)) {
|
||||
const responseId = asTrimmedString(payload.response.id);
|
||||
return responseId || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const object = asTrimmedString(payload.object).toLowerCase();
|
||||
const status = asTrimmedString(payload.status).toLowerCase();
|
||||
if (
|
||||
(object === 'response' && RESPONSES_TERMINAL_STATUSES.has(status))
|
||||
|| RESPONSES_TERMINAL_STATUSES.has(status)
|
||||
) {
|
||||
const responseId = asTrimmedString(payload.id);
|
||||
return responseId || null;
|
||||
}
|
||||
|
||||
if (isRecord(payload.response)) {
|
||||
const nestedStatus = asTrimmedString(payload.response.status).toLowerCase();
|
||||
if (RESPONSES_TERMINAL_STATUSES.has(nestedStatus)) {
|
||||
const responseId = asTrimmedString(payload.response.id);
|
||||
return responseId || null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user