diff --git a/src/server/proxy-core/capabilities/responsesCompact.test.ts b/src/server/proxy-core/capabilities/responsesCompact.test.ts index ab38975..f59e799 100644 --- a/src/server/proxy-core/capabilities/responsesCompact.test.ts +++ b/src/server/proxy-core/capabilities/responsesCompact.test.ts @@ -86,14 +86,17 @@ describe('ensureCompactResponsesJsonAcceptHeader', () => { }); }); - it('preserves existing accept values and unrelated platforms', () => { + it('forces compact requests back to application/json even when callers asked for SSE', () => { expect(ensureCompactResponsesJsonAcceptHeader({ - Accept: 'application/json', + Accept: 'text/event-stream', }, { sitePlatform: 'codex', })).toEqual({ - Accept: 'application/json', + accept: 'application/json', }); + }); + + it('preserves unrelated platforms', () => { expect(ensureCompactResponsesJsonAcceptHeader({}, { sitePlatform: 'openai', })).toEqual({}); diff --git a/src/server/proxy-core/capabilities/responsesCompact.ts b/src/server/proxy-core/capabilities/responsesCompact.ts index 9596761..c81ea3d 100644 --- a/src/server/proxy-core/capabilities/responsesCompact.ts +++ b/src/server/proxy-core/capabilities/responsesCompact.ts @@ -42,10 +42,11 @@ export function ensureCompactResponsesJsonAcceptHeader( }, ): Record { if (!shouldForceCompactResponsesJsonAccept(options?.sitePlatform)) return headers; - const existingAccept = asTrimmedString(headers.accept) || asTrimmedString(headers.Accept); - if (existingAccept) return headers; + const nextHeaders = { ...headers }; + delete (nextHeaders as Record).Accept; + delete (nextHeaders as Record).accept; return { - ...headers, + ...nextHeaders, accept: 'application/json', }; } diff --git a/src/server/proxy-core/surfaces/openAiResponsesSurface.ts b/src/server/proxy-core/surfaces/openAiResponsesSurface.ts index c45ea85..7e3985e 100644 --- a/src/server/proxy-core/surfaces/openAiResponsesSurface.ts +++ b/src/server/proxy-core/surfaces/openAiResponsesSurface.ts @@ -105,15 +105,16 @@ function isRecord(value: unknown): value is Record { } function getCodexSessionHeaderValue(headers: Record): string { - for (const [rawKey, rawValue] of Object.entries(headers)) { - const normalizedKey = rawKey.trim().toLowerCase(); - if ( - normalizedKey === 'session_id' - || normalizedKey === 'session-id' - || normalizedKey === 'conversation_id' - || normalizedKey === 'conversation-id' - ) { - return String(rawValue || '').trim(); + const normalizedEntries = Object.entries(headers).map(([rawKey, rawValue]) => [ + rawKey.trim().toLowerCase(), + String(rawValue || '').trim(), + ] as const); + for (const preferredKey of ['session_id', 'session-id', 'conversation_id', 'conversation-id']) { + const match = normalizedEntries.find(([normalizedKey, normalizedValue]) => ( + normalizedKey === preferredKey && normalizedValue + )); + if (match) { + return match[1]; } } return ''; @@ -626,7 +627,7 @@ export async function handleOpenAiResponsesSurfaceRequest( ctx.rawErrText = await readRuntimeResponseText(recoveredResponse).catch(() => 'unknown error'); } } - const compactFallbackEnabled = config.responsesCompactFallbackToResponsesEnabled || !isCodexSite; + const compactFallbackEnabled = config.responsesCompactFallbackToResponsesEnabled; if ( isCompactRequest && compactFallbackEnabled diff --git a/src/server/routes/api/settings.ts b/src/server/routes/api/settings.ts index 31f625b..4186397 100644 --- a/src/server/routes/api/settings.ts +++ b/src/server/routes/api/settings.ts @@ -905,6 +905,7 @@ export async function settingsRoutes(app: FastifyInstance) { const body = parsedBody.data as RuntimeSettingsBody; const changedLabels: string[] = []; const currentRequestIp = extractClientIp(request.ip, request.headers['x-forwarded-for']); + let pendingPayloadRules: typeof config.payloadRules | undefined; const webhookTouched = body.webhookUrl !== undefined || body.webhookEnabled !== undefined; const nextWebhookUrl = body.webhookUrl !== undefined @@ -1149,8 +1150,7 @@ export async function settingsRoutes(app: FastifyInstance) { if (previousRules !== nextRules) { changedLabels.push('Payload 规则'); } - config.payloadRules = parsedPayloadRules.normalized; - await upsertSetting('payload_rules', config.payloadRules); + pendingPayloadRules = parsedPayloadRules.normalized; } if (body.modelAvailabilityProbeEnabled !== undefined) { @@ -1718,6 +1718,11 @@ export async function settingsRoutes(app: FastifyInstance) { upsertSetting('token_router_failure_cooldown_max_sec', normalized); } + if (pendingPayloadRules !== undefined) { + config.payloadRules = pendingPayloadRules; + await upsertSetting('payload_rules', pendingPayloadRules); + } + if (changedLabels.length > 0) { let eventType: 'checkin' | 'balance' | 'proxy' | 'status' | 'token' = 'status'; if (changedLabels.length === 1) { diff --git a/src/server/routes/proxy/responses.compact-upstream.test.ts b/src/server/routes/proxy/responses.compact-upstream.test.ts index e2e0203..0fbbc0a 100644 --- a/src/server/routes/proxy/responses.compact-upstream.test.ts +++ b/src/server/routes/proxy/responses.compact-upstream.test.ts @@ -179,7 +179,7 @@ describe('responses proxy compact upstream routing', () => { expect(targetUrl).toContain('/v1/responses/compact'); }); - it('automatically falls back from compact to ordinary responses for generic openai-compatible upstreams when compact is unsupported', async () => { + it('does not fall back from compact to ordinary responses for generic openai-compatible upstreams when the fallback toggle is off', async () => { selectChannelMock.mockReturnValue({ channel: { id: 11, routeId: 22 }, site: { id: 44, name: 'generic-openai-site', url: 'https://upstream.example.com', platform: 'openai' }, @@ -199,12 +199,12 @@ describe('responses proxy compact upstream routing', () => { headers: { 'content-type': 'application/json' }, })) .mockResolvedValueOnce(new Response(JSON.stringify({ - id: 'resp_123', - object: 'response', - output_text: 'hello from fallback', - usage: { input_tokens: 10, output_tokens: 5, total_tokens: 15 }, + error: { + message: 'Invalid URL (POST /v1/responses/compact)', + type: 'invalid_request_error', + }, }), { - status: 200, + status: 404, headers: { 'content-type': 'application/json' }, })); @@ -217,19 +217,9 @@ describe('responses proxy compact upstream routing', () => { }, }); - expect(response.statusCode).toBe(200); - expect(fetchMock).toHaveBeenCalledTimes(2); + expect(response.statusCode).toBe(404); + expect(fetchMock).toHaveBeenCalledTimes(1); expect(String(fetchMock.mock.calls[0]?.[0] || '')).toContain('/v1/responses/compact'); - expect(String(fetchMock.mock.calls[1]?.[0] || '')).toContain('/v1/responses'); - expect(String(fetchMock.mock.calls[1]?.[0] || '')).not.toContain('/compact'); - expect(response.json()).toMatchObject({ - object: 'response.compaction', - usage: { - input_tokens: 10, - output_tokens: 5, - total_tokens: 15, - }, - }); }); it('optionally falls back from compact to ordinary responses when compact is explicitly unsupported', async () => { @@ -282,6 +272,7 @@ describe('responses proxy compact upstream routing', () => { }); it('automatically falls back from compact when a generic upstream rejects the stream parameter on the compact endpoint', async () => { + config.responsesCompactFallbackToResponsesEnabled = true; selectChannelMock.mockReturnValue({ channel: { id: 11, routeId: 22 }, site: { id: 44, name: 'generic-openai-site', url: 'https://upstream.example.com', platform: 'openai' }, @@ -331,6 +322,7 @@ describe('responses proxy compact upstream routing', () => { }); it('rebuilds compact fallback as an SSE-capable non-compact /v1/responses request for sub2api', async () => { + config.responsesCompactFallbackToResponsesEnabled = true; selectChannelMock.mockReturnValue({ channel: { id: 11, routeId: 22 }, site: { id: 44, name: 'sub2api-site', url: 'https://sub2api.example.com', platform: 'sub2api' }, diff --git a/src/server/services/brandMatcher.test.ts b/src/server/services/brandMatcher.test.ts index 4fe7e67..7ed86d6 100644 --- a/src/server/services/brandMatcher.test.ts +++ b/src/server/services/brandMatcher.test.ts @@ -23,6 +23,13 @@ describe('brandMatcher', () => { expect(getBlockedBrandRules(['OpenRouter', 'OpenRouter', 'Unknown Brand'])).toEqual(['OpenRouter']); }); + it('canonicalizes blocked brand names before filtering', () => { + expect(getBlockedBrandRules([' openrouter ', 'OPENROUTER', 'together ai'])).toEqual([ + 'OpenRouter', + 'Together AI', + ]); + }); + it('uses the shared frontend detection result for global brand blocking', () => { const providerRules = getBlockedBrandRules(['OpenRouter', 'Groq', '百炼', 'LongCat']); expect(isModelBlockedByBrand('openrouter/openrouter-auto', providerRules)).toBe(true); diff --git a/src/server/services/brandMatcher.ts b/src/server/services/brandMatcher.ts index a5f67c4..6dd8968 100644 --- a/src/server/services/brandMatcher.ts +++ b/src/server/services/brandMatcher.ts @@ -1,10 +1,17 @@ -import { getAllBrandNames as getSharedBrandNames, getBrand } from '../shared/modelBrand.js'; +import { getAllBrandNames as getSharedBrandNames, getMatchingBrandNames } from '../shared/modelBrand.js'; export type BlockedBrandRule = string; const KNOWN_BRANDS = new Set(getSharedBrandNames()); +const CANONICAL_BRAND_BY_KEY = new Map( + getSharedBrandNames().map((brand) => [normalizeBrandKey(brand), brand] as const), +); const KNOWN_BRAND_LIST = Array.from(KNOWN_BRANDS); +function normalizeBrandKey(value: string): string { + return String(value || '').trim().replace(/\s+/g, ' ').toLowerCase(); +} + export function getAllBrandNames(): string[] { return KNOWN_BRAND_LIST; } @@ -13,16 +20,16 @@ export function getBlockedBrandRules(blockedBrands: string[]): BlockedBrandRule[ const seen = new Set(); const rules: string[] = []; for (const brand of blockedBrands) { - const normalized = String(brand || '').trim(); - if (!normalized || seen.has(normalized) || !KNOWN_BRANDS.has(normalized)) continue; - seen.add(normalized); - rules.push(normalized); + const canonical = CANONICAL_BRAND_BY_KEY.get(normalizeBrandKey(brand)); + if (!canonical || seen.has(canonical)) continue; + seen.add(canonical); + rules.push(canonical); } return rules; } export function isModelBlockedByBrand(modelName: string, rules: BlockedBrandRule[]): boolean { if (!modelName || rules.length === 0) return false; - const brand = getBrand(modelName); - return !!brand && rules.includes(brand.name); + const matchedBrands = new Set(getMatchingBrandNames(modelName)); + return rules.some((rule) => matchedBrands.has(rule)); } diff --git a/src/server/transformers/anthropic/messages/streamBridge.test.ts b/src/server/transformers/anthropic/messages/streamBridge.test.ts index d2fb442..bd2fd41 100644 --- a/src/server/transformers/anthropic/messages/streamBridge.test.ts +++ b/src/server/transformers/anthropic/messages/streamBridge.test.ts @@ -68,6 +68,47 @@ describe('anthropic messages stream bridge', () => { expect(lines.join('')).toContain('sig-native'); }); + it('preserves whitespace in anthropic streaming text and thinking deltas', () => { + const streamContext = anthropicMessagesStream.createContext('claude-test'); + const downstreamContext = anthropicMessagesStream.createDownstreamContext(); + + const textResult = consumeAnthropicSseEvent( + { + event: 'content_block_delta', + data: JSON.stringify({ + type: 'content_block_delta', + index: 0, + delta: { + type: 'text_delta', + text: ' padded text ', + }, + }), + }, + streamContext, + downstreamContext, + 'claude-test', + ); + const thinkingResult = consumeAnthropicSseEvent( + { + event: 'content_block_delta', + data: JSON.stringify({ + type: 'content_block_delta', + index: 0, + delta: { + type: 'thinking_delta', + thinking: ' padded thinking ', + }, + }), + }, + streamContext, + downstreamContext, + 'claude-test', + ); + + expect(textResult.lines.join('')).toContain(' padded text '); + expect(thinkingResult.lines.join('')).toContain(' padded thinking '); + }); + it('preserves responses output-item ordering when streaming anthropic fallback blocks', () => { const streamContext = anthropicMessagesStream.createContext('claude-test'); const downstreamContext = anthropicMessagesStream.createDownstreamContext(); diff --git a/src/server/transformers/anthropic/messages/streamBridge.ts b/src/server/transformers/anthropic/messages/streamBridge.ts index 1005ffe..65ac7e0 100644 --- a/src/server/transformers/anthropic/messages/streamBridge.ts +++ b/src/server/transformers/anthropic/messages/streamBridge.ts @@ -619,7 +619,7 @@ export function serializeAnthropicFinalAsStream( }, }, )); - const thinkingText = asTrimmedString(block.thinking); + const thinkingText = asUntouchedString(block.thinking); if (thinkingText) { lines.push(...anthropicMessagesStream.serializeEvent( { reasoningDelta: thinkingText }, @@ -655,7 +655,7 @@ export function serializeAnthropicFinalAsStream( kind: 'redacted_thinking', index, }, - redactedThinkingData: asTrimmedString(block.data), + redactedThinkingData: asUntouchedString(block.data), }, }, )); @@ -762,6 +762,10 @@ function toClaudeStopReason(finishReason: string | null | undefined): string { return 'end_turn'; } +function asUntouchedString(value: unknown): string { + return typeof value === 'string' ? value : ''; +} + function normalizeAnthropicRawEvent( payload: AnthropicStreamPayload, context: StreamTransformContext, @@ -812,7 +816,7 @@ function normalizeAnthropicRawEvent( kind: 'redacted_thinking', index, }, - redactedThinkingData: asTrimmedString(contentBlock.data), + redactedThinkingData: asUntouchedString(contentBlock.data), }, }; } @@ -841,7 +845,7 @@ function normalizeAnthropicRawEvent( if (deltaType === 'thinking_delta') { return { - reasoningDelta: asTrimmedString(delta.thinking ?? delta.text) || undefined, + reasoningDelta: asUntouchedString(delta.thinking ?? delta.text) || undefined, }; } @@ -855,7 +859,7 @@ function normalizeAnthropicRawEvent( if (deltaType === 'text_delta') { return { - contentDelta: asTrimmedString(delta.text) || undefined, + contentDelta: asUntouchedString(delta.text) || undefined, }; } diff --git a/src/server/transformers/openai/chat/responseBridge.test.ts b/src/server/transformers/openai/chat/responseBridge.test.ts index e983552..2a15176 100644 --- a/src/server/transformers/openai/chat/responseBridge.test.ts +++ b/src/server/transformers/openai/chat/responseBridge.test.ts @@ -155,6 +155,64 @@ describe('openai chat response bridge', () => { }); }); + it('builds synthetic chunks for multi-choice finals even when some choices omit toolCalls', () => { + const chunks = buildNormalizedFinalToOpenAiChatChunks({ + id: 'chatcmpl-multi-1', + model: 'gpt-5', + created: 123, + content: '', + reasoningContent: '', + finishReason: 'stop', + toolCalls: [], + choices: [ + { + index: 0, + role: 'assistant', + content: '', + toolCalls: [{ + id: 'call_multi_1', + name: 'lookup_weather', + arguments: '{"city":"Paris"}', + }], + finishReason: 'tool_calls', + }, + { + index: 1, + role: 'assistant', + content: 'done', + finishReason: 'stop', + }, + ], + } as any); + + expect(chunks[0]).toMatchObject({ + choices: [ + { + index: 0, + delta: { + tool_calls: [{ + id: 'call_multi_1', + type: 'function', + }], + }, + }, + { + index: 1, + delta: { + role: 'assistant', + content: 'done', + }, + }, + ], + }); + expect(chunks[1]).toMatchObject({ + choices: [ + { index: 0, finish_reason: 'tool_calls' }, + { index: 1, finish_reason: 'stop' }, + ], + }); + }); + it('keeps the outbound facade pointed at the response bridge object', () => { expect(openAiChatOutbound).toBe(openAiChatResponseBridge); }); diff --git a/src/server/transformers/openai/chat/responseBridge.ts b/src/server/transformers/openai/chat/responseBridge.ts index b507fe8..e6c6bc9 100644 --- a/src/server/transformers/openai/chat/responseBridge.ts +++ b/src/server/transformers/openai/chat/responseBridge.ts @@ -58,10 +58,24 @@ export function buildNormalizedFinalToOpenAiChatPayload( usage?: unknown, ) { const chatNormalized = normalized as OpenAiChatNormalizedFinalResponse; + const normalizedUsage = isRecord(usage) + && typeof usage.promptTokens === 'number' + && typeof usage.completionTokens === 'number' + && typeof usage.totalTokens === 'number' + ? { + promptTokens: usage.promptTokens, + completionTokens: usage.completionTokens, + totalTokens: usage.totalTokens, + } + : { + promptTokens: 0, + completionTokens: 0, + totalTokens: 0, + }; const payload = serializeFinalResponse( 'openai', normalized, - usage as { promptTokens: number; completionTokens: number; totalTokens: number }, + normalizedUsage, ) as Record; const choices = Array.isArray(chatNormalized.choices) && chatNormalized.choices.length > 0 @@ -136,13 +150,14 @@ export function buildNormalizedFinalToOpenAiChatChunks(normalized: NormalizedFin created: normalized.created, model: normalized.model, choices: choices.map((choice) => { + const toolCalls = Array.isArray(choice.toolCalls) ? choice.toolCalls : []; const delta: Record = { role: choice.role || 'assistant', content: choice.content || '', }; if (choice.reasoningContent) delta.reasoning_content = choice.reasoningContent; - if (choice.toolCalls.length > 0) { - delta.tool_calls = choice.toolCalls.map((toolCall, toolIndex) => ({ + if (toolCalls.length > 0) { + delta.tool_calls = toolCalls.map((toolCall, toolIndex) => ({ index: toolIndex, id: toolCall.id, type: 'function', @@ -167,11 +182,14 @@ export function buildNormalizedFinalToOpenAiChatChunks(normalized: NormalizedFin object: 'chat.completion.chunk', created: normalized.created, model: normalized.model, - choices: choices.map((choice) => ({ - index: choice.index, - delta: {}, - finish_reason: choice.finishReason || (choice.toolCalls.length > 0 ? 'tool_calls' : 'stop'), - })).sort((left, right) => left.index - right.index), + choices: choices.map((choice) => { + const toolCalls = Array.isArray(choice.toolCalls) ? choice.toolCalls : []; + return { + index: choice.index, + delta: {}, + finish_reason: choice.finishReason || (toolCalls.length > 0 ? 'tool_calls' : 'stop'), + }; + }).sort((left, right) => left.index - right.index), }, ] : buildSyntheticOpenAiChunks(normalized); diff --git a/src/server/transformers/openai/responses/codexCompatibility.test.ts b/src/server/transformers/openai/responses/codexCompatibility.test.ts index 8a4c7d2..0ddd823 100644 --- a/src/server/transformers/openai/responses/codexCompatibility.test.ts +++ b/src/server/transformers/openai/responses/codexCompatibility.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it } from 'vitest'; -import { normalizeCodexResponsesBodyForProxy } from './codexCompatibility.js'; - -const CODEX_DEFAULT_INSTRUCTIONS = 'You are a helpful coding assistant.'; +import { + CODEX_DEFAULT_INSTRUCTIONS, + normalizeCodexResponsesBodyForProxy, +} from './codexCompatibility.js'; describe('normalizeCodexResponsesBodyForProxy', () => { it('extracts codex system input into top-level instructions before proxying upstream', () => { @@ -22,6 +23,7 @@ describe('normalizeCodexResponsesBodyForProxy', () => { max_output_tokens: 512, max_completion_tokens: 256, max_tokens: 128, + stream_options: { include_usage: true }, temperature: 0.3, store: true, }, 'codex'); @@ -40,6 +42,62 @@ describe('normalizeCodexResponsesBodyForProxy', () => { }); }); + it('keeps non-text system content in input while lifting only the text blocks into instructions', () => { + const imageBlock = { + type: 'input_image', + image_url: 'https://example.com/reference.png', + }; + + const body = normalizeCodexResponsesBodyForProxy({ + input: [ + { + type: 'message', + role: 'system', + content: [ + { type: 'input_text', text: 'keep the image context' }, + imageBlock, + ], + }, + { + type: 'message', + role: 'user', + content: [{ type: 'input_text', text: 'hello' }], + }, + ], + }, 'codex'); + + expect(body.instructions).toBe('keep the image context'); + expect(body.input).toEqual([ + { + type: 'message', + role: 'system', + content: [imageBlock], + }, + { + type: 'message', + role: 'user', + content: [{ type: 'input_text', text: 'hello' }], + }, + ]); + }); + + it('joins multiple text blocks with separators instead of squashing them together', () => { + const body = normalizeCodexResponsesBodyForProxy({ + input: [ + { + type: 'message', + role: 'system', + content: [ + { type: 'input_text', text: 'first line' }, + { type: 'input_text', text: 'second line' }, + ], + }, + ], + }, 'codex'); + + expect(body.instructions).toBe('first line\nsecond line'); + }); + it('supplies a non-empty default instructions string for codex when missing', () => { const body = normalizeCodexResponsesBodyForProxy({ input: 'hello', diff --git a/src/server/transformers/openai/responses/codexCompatibility.ts b/src/server/transformers/openai/responses/codexCompatibility.ts index 4632f48..67570aa 100644 --- a/src/server/transformers/openai/responses/codexCompatibility.ts +++ b/src/server/transformers/openai/responses/codexCompatibility.ts @@ -6,17 +6,9 @@ function asTrimmedString(value: unknown): string { return typeof value === 'string' ? value.trim() : ''; } -const CODEX_DEFAULT_INSTRUCTIONS = 'You are a helpful coding assistant.'; +export const CODEX_DEFAULT_INSTRUCTIONS = 'You are a helpful coding assistant.'; -function extractTextFromContent(content: unknown): string { - if (typeof content === 'string') return content; - if (Array.isArray(content)) { - return content - .map((item) => extractTextFromContent(item)) - .filter((item) => item.length > 0) - .join(''); - } - if (!isRecord(content)) return ''; +function extractTextValue(content: Record): string { if (typeof content.text === 'string') return content.text; if (typeof content.content === 'string') return content.content; if (typeof content.output_text === 'string') return content.output_text; @@ -24,6 +16,75 @@ function extractTextFromContent(content: unknown): string { return ''; } +function isTextOnlyContentRecord(content: Record): boolean { + const blockType = asTrimmedString(content.type).toLowerCase(); + if (blockType === 'input_text' || blockType === 'output_text' || blockType === 'text') { + return true; + } + const keys = Object.keys(content); + return keys.length > 0 && keys.every((key) => ( + key === 'type' + || key === 'text' + || key === 'content' + || key === 'output_text' + || key === 'input_text' + )); +} + +function splitExtractedSystemContent(content: unknown): { + extractedTexts: string[]; + remainingContent?: unknown; +} { + if (typeof content === 'string') { + const trimmed = content.trim(); + return trimmed ? { extractedTexts: [trimmed] } : { extractedTexts: [] }; + } + + if (Array.isArray(content)) { + const extractedTexts: string[] = []; + const remainingItems: unknown[] = []; + + for (const item of content) { + if (typeof item === 'string') { + const trimmed = item.trim(); + if (trimmed) extractedTexts.push(trimmed); + else remainingItems.push(item); + continue; + } + + if (!isRecord(item)) { + remainingItems.push(item); + continue; + } + + const textValue = extractTextValue(item).trim(); + if (textValue && isTextOnlyContentRecord(item)) { + extractedTexts.push(textValue); + continue; + } + + remainingItems.push(item); + } + + return { + extractedTexts, + ...(remainingItems.length > 0 ? { remainingContent: remainingItems } : {}), + }; + } + + if (!isRecord(content)) return { extractedTexts: [] }; + + const textValue = extractTextValue(content).trim(); + if (textValue && isTextOnlyContentRecord(content)) { + return { extractedTexts: [textValue] }; + } + + return { + extractedTexts: [], + remainingContent: content, + }; +} + function extractSystemMessagesToInstructions( body: Record, ): Record { @@ -46,10 +107,19 @@ function extractSystemMessagesToInstructions( continue; } - const text = extractTextFromContent(item.content); - if (text.trim()) { - extractedSystemTexts.push(text); + const { extractedTexts, remainingContent } = splitExtractedSystemContent(item.content); + if (extractedTexts.length > 0) { + extractedSystemTexts.push(extractedTexts.join('\n')); + if (remainingContent !== undefined) { + remainingInput.push({ + ...item, + content: remainingContent, + }); + } + continue; } + + remainingInput.push(item); } if (extractedSystemTexts.length <= 0) return body; @@ -99,6 +169,7 @@ function stripCodexUnsupportedResponsesFields( delete next.max_output_tokens; delete next.max_completion_tokens; delete next.max_tokens; + delete next.stream_options; return next; }