feat: Add support for OpenAI Responses endpoint and enhance proxy retry logic

- Updated proxyRetryPolicy to retry on all 4xx and 5xx status codes.
- Introduced 'responses' target format in TestChatRequestPayload.
- Implemented extractResponsesContent function to handle responses format in ModelTester.
- Enhanced ModelTester to support responses target format and display appropriate labels.
- Updated ProxyLogs to extract and display upstream paths from error messages.
- Added tests to ensure responses target format is preserved during payload sync.
- Created responses proxy route to handle requests to the new OpenAI Responses endpoint.
- Updated upstreamEndpoint logic to include responses endpoint handling.
- Added modelService tests to validate token route rebuilding logic.
This commit is contained in:
cita
2026-03-01 15:21:16 +08:00
parent 44af9a91cf
commit 4e22b379a8
21 changed files with 1794 additions and 56 deletions
+58
View File
@@ -247,6 +247,21 @@ function parseClaudeMessageContent(content: unknown): string {
return extractTextAndReasoning(content).content;
}
function parseResponsesOutputText(payload: Record<string, unknown>): string {
const direct = typeof payload.output_text === 'string' ? payload.output_text : '';
if (direct) return direct;
const output = Array.isArray(payload.output) ? payload.output : [];
const parts: string[] = [];
for (const item of output) {
if (!isRecord(item)) continue;
const parsed = extractTextAndReasoning(item.content ?? item);
if (parsed.content) parts.push(parsed.content);
}
return parts.join('\n\n');
}
function convertClaudeRequestToOpenAiBody(body: Record<string, unknown>): {
model: string;
stream: boolean;
@@ -354,6 +369,19 @@ export function parseDownstreamChatRequest(
};
}
const hasMessages = Array.isArray(raw.messages) && raw.messages.length > 0;
if (!hasMessages) {
const hint = raw.input !== undefined
? 'messages is required for /v1/chat/completions. For Responses payload, use /v1/responses.'
: 'messages is required';
return {
error: {
statusCode: 400,
payload: { error: { message: hint, type: 'invalid_request_error' } },
},
};
}
return {
value: {
requestedModel: model,
@@ -396,6 +424,17 @@ export function normalizeUpstreamFinalResponse(
};
}
if (isRecord(payload) && ((payload as any).object === 'response' || Array.isArray((payload as any).output))) {
return {
id: isNonEmptyString(payload.id) ? payload.id : fallbackId,
model: isNonEmptyString(payload.model) ? payload.model : fallbackModel,
created: ensureIntegerTimestamp(payload.created, now),
content: parseResponsesOutputText(payload) || fallbackText,
reasoningContent: '',
finishReason: normalizeStopReason(payload.finish_reason ?? payload.status) || 'stop',
};
}
if (isRecord(payload) && Array.isArray(payload.candidates)) {
const candidate = payload.candidates[0] || {};
const parsedCandidate = extractTextAndReasoning(candidate?.content?.parts || candidate?.content);
@@ -468,6 +507,25 @@ export function normalizeUpstreamStreamEvent(
}
const type = typeof payload.type === 'string' ? payload.type : '';
if (type.startsWith('response.output_text')) {
const deltaText = typeof payload.delta === 'string'
? payload.delta
: extractTextAndReasoning(payload.delta).content;
return {
contentDelta: deltaText || undefined,
};
}
if (type === 'response.completed' && isRecord((payload as any).response)) {
const responsePayload = (payload as any).response as Record<string, unknown>;
if (isNonEmptyString(responsePayload.id)) context.id = responsePayload.id;
if (isNonEmptyString(responsePayload.model)) context.model = responsePayload.model;
return {
finishReason: normalizeStopReason(responsePayload.status) || 'stop',
done: true,
};
}
const message = isRecord(payload.message) ? payload.message : null;
if (message) {