Preserve the active worktree snapshot on dev for follow-on integration

The root checkout already contained a large in-progress proxy, transformer,
and settings refactor. This commit captures that exact work on a dedicated
local dev branch without disturbing the original dirty worktree, so the
changes can be continued or split from dev instead of living only as
uncommitted state.

Constraint: The source checkout was already dirty on another branch, so the move had to preserve that worktree while excluding local .idea and .omx residue from the commit
Rejected: Switch the dirty root checkout directly to dev | risked disrupting the original branch and in-progress state
Confidence: medium
Scope-risk: broad
Reversibility: clean
Directive: Treat this as a transfer snapshot; fix the failing oauth/accountTokens/normalized/docs test regressions before merge or publication
Tested: git diff --check; npm run typecheck; npm run repo:drift-check
Not-tested: Clean test pass; npm test currently fails in oauth routes, accountTokens sync, shared normalized helpers, gemini route expectations, architecture-boundaries, and docs vitepress config
This commit is contained in:
cita-777
2026-04-17 19:41:14 +08:00
parent 6cca74b039
commit 88f40e91f7
133 changed files with 13780 additions and 5670 deletions
@@ -71,6 +71,45 @@ describe('openAiChatTransformer.inbound', () => {
});
});
it('round-trips continuation turnState through chat metadata bridging', () => {
const built = openAiChatTransformer.buildProtocolRequest({
operation: 'generate',
surface: 'openai-chat',
cliProfile: 'codex',
requestedModel: 'gpt-5',
stream: false,
messages: [{ role: 'user', parts: [{ type: 'text', text: 'hello' }] }],
continuation: {
sessionId: 'chat-session-1',
turnState: 'turn-state-chat-1',
},
});
expect(built).toMatchObject({
metadata: {
user_id: 'chat-session-1',
metapi_turn_state: 'turn-state-chat-1',
},
});
const parsed = openAiChatTransformer.parseRequest({
model: 'gpt-5',
messages: [{ role: 'user', content: 'hello' }],
metadata: {
user_id: 'chat-session-1',
metapi_turn_state: 'turn-state-chat-1',
},
});
expect(parsed.error).toBeUndefined();
expect(parsed.value).toMatchObject({
continuation: {
sessionId: 'chat-session-1',
turnState: 'turn-state-chat-1',
},
});
});
it('captures chat request metadata fields without changing upstream body', () => {
const result = openAiChatTransformer.transformRequest({
model: 'gpt-5',
@@ -277,6 +316,65 @@ describe('openAiChatTransformer.outbound', () => {
expect((normalized as any).citations).toEqual(['https://shared.example/citation']);
});
it('maps final Responses payload terminal statuses to sub2api-like chat finish reasons', () => {
const incompleteStop = openAiChatTransformer.transformFinalResponse({
id: 'resp_final_stop',
object: 'response',
model: 'gpt-5',
status: 'incomplete',
output: [],
}, 'gpt-5');
const incompleteLength = openAiChatTransformer.transformFinalResponse({
id: 'resp_final_length',
object: 'response',
model: 'gpt-5',
status: 'incomplete',
incomplete_details: {
reason: 'max_output_tokens',
},
output: [],
}, 'gpt-5');
const failedStop = openAiChatTransformer.transformFinalResponse({
id: 'resp_final_failed',
object: 'response',
model: 'gpt-5',
status: 'failed',
output: [],
}, 'gpt-5');
expect(openAiChatTransformer.serializeFinalResponse(incompleteStop, {
promptTokens: 1,
completionTokens: 1,
totalTokens: 2,
})).toMatchObject({
choices: [{
finish_reason: 'stop',
}],
});
expect(openAiChatTransformer.serializeFinalResponse(incompleteLength, {
promptTokens: 1,
completionTokens: 1,
totalTokens: 2,
})).toMatchObject({
choices: [{
finish_reason: 'length',
}],
});
expect(openAiChatTransformer.serializeFinalResponse(failedStop, {
promptTokens: 1,
completionTokens: 1,
totalTokens: 2,
})).toMatchObject({
choices: [{
finish_reason: 'stop',
}],
});
});
it('serializes multi-choice chat responses with per-choice annotations, reasoning, tool calls, and shared citations', () => {
const normalized = openAiChatTransformer.transformFinalResponse({
id: 'chatcmpl-multi',
@@ -481,6 +579,67 @@ describe('openAiChatTransformer.stream', () => {
});
});
it('serializes response.incomplete terminal events with sub2api-like finish reasons', () => {
const context = openAiChatTransformer.createStreamContext('gpt-5');
const stopEvent = openAiChatTransformer.transformStreamEvent({
type: 'response.incomplete',
response: {
id: 'resp-incomplete-stop',
model: 'gpt-5',
status: 'incomplete',
},
}, context, 'gpt-5');
const stopPayloads = parseSsePayloads(
openAiChatTransformer.serializeStreamEvent(stopEvent, context, createClaudeDownstreamContext()),
);
expect((stopPayloads[0] as any).choices[0]).toMatchObject({
finish_reason: 'stop',
});
const lengthEvent = openAiChatTransformer.transformStreamEvent({
type: 'response.incomplete',
response: {
id: 'resp-incomplete-length',
model: 'gpt-5',
status: 'incomplete',
incomplete_details: {
reason: 'max_output_tokens',
},
},
}, context, 'gpt-5');
const lengthPayloads = parseSsePayloads(
openAiChatTransformer.serializeStreamEvent(lengthEvent, context, createClaudeDownstreamContext()),
);
expect((lengthPayloads[0] as any).choices[0]).toMatchObject({
finish_reason: 'length',
});
});
it('serializes response.failed terminal events with stop finish_reason instead of error', () => {
const context = openAiChatTransformer.createStreamContext('gpt-5');
const event = openAiChatTransformer.transformStreamEvent({
type: 'response.failed',
response: {
id: 'resp-failed-stop',
model: 'gpt-5',
status: 'failed',
},
}, context, 'gpt-5');
const payloads = parseSsePayloads(
openAiChatTransformer.serializeStreamEvent(event, context, createClaudeDownstreamContext()),
);
expect((payloads[0] as any).choices[0]).toMatchObject({
finish_reason: 'stop',
});
});
it('serializes multi-choice stream chunks without collapsing choice-specific deltas', () => {
const context = openAiChatTransformer.createStreamContext('gpt-5');
const event = openAiChatTransformer.transformStreamEvent({
+20 -41
View File
@@ -1,12 +1,20 @@
import { canonicalRequestFromOpenAiBody, canonicalRequestToOpenAiChatBody } from '../../canonical/request.js';
import type { CanonicalRequestEnvelope } from '../../canonical/types.js';
import type { ProtocolBuildContext, ProtocolParseContext } from '../../contracts.js';
import { type NormalizedFinalResponse, type NormalizedStreamEvent, type StreamTransformContext } from '../../shared/normalized.js';
import { createChatEndpointStrategy } from '../../shared/chatEndpointStrategy.js';
import { openAiChatInbound } from './inbound.js';
import { openAiChatOutbound } from './outbound.js';
import { createChatProxyStreamSession } from './proxyStream.js';
import { openAiChatStream } from './stream.js';
import {
buildCanonicalRequestToOpenAiChatBody,
parseOpenAiChatRequestToCanonical,
} from './requestBridge.js';
import {
openAiChatResponseBridge,
buildNormalizedFinalToOpenAiChatChunks,
buildNormalizedFinalToOpenAiChatPayload,
normalizeOpenAiChatFinalToNormalized,
} from './responseBridge.js';
import { openAiChatStream } from './streamBridge.js';
import { openAiChatUsage } from './usage.js';
import { createOpenAiChatAggregateState, applyOpenAiChatStreamEvent, finalizeOpenAiChatAggregate } from './aggregator.js';
import type {
@@ -17,7 +25,7 @@ import type {
export const openAiChatTransformer = {
protocol: 'openai/chat' as const,
inbound: openAiChatInbound,
outbound: openAiChatOutbound,
outbound: openAiChatResponseBridge,
stream: openAiChatStream,
usage: openAiChatUsage,
compatibility: {
@@ -35,41 +43,13 @@ export const openAiChatTransformer = {
body: unknown,
ctx?: ProtocolParseContext,
): { value?: CanonicalRequestEnvelope; error?: { statusCode: number; payload: unknown } } {
const parsed = openAiChatInbound.parse(body);
if (parsed.error) {
return { error: parsed.error };
}
if (!parsed.value) {
return {
error: {
statusCode: 400,
payload: {
error: {
message: 'invalid chat request',
type: 'invalid_request_error',
},
},
},
};
}
return {
value: canonicalRequestFromOpenAiBody({
body: parsed.value.parsed.upstreamBody,
surface: 'openai-chat',
cliProfile: ctx?.cliProfile,
operation: ctx?.operation,
metadata: ctx?.metadata,
passthrough: ctx?.passthrough,
continuation: ctx?.continuation,
}),
};
return parseOpenAiChatRequestToCanonical(body, ctx);
},
buildProtocolRequest(
request: CanonicalRequestEnvelope,
_ctx?: ProtocolBuildContext,
): Record<string, unknown> {
return canonicalRequestToOpenAiChatBody(request);
return buildCanonicalRequestToOpenAiChatBody(request);
},
transformRequest(body: unknown): ReturnType<typeof openAiChatInbound.parse> {
return openAiChatInbound.parse(body);
@@ -78,7 +58,7 @@ export const openAiChatTransformer = {
return openAiChatStream.createContext(modelName);
},
transformFinalResponse(payload: unknown, modelName: string, fallbackText = ''): NormalizedFinalResponse {
return openAiChatOutbound.normalizeFinal(payload, modelName, fallbackText);
return normalizeOpenAiChatFinalToNormalized(payload, modelName, fallbackText);
},
transformStreamEvent(payload: unknown, context: StreamTransformContext, modelName: string): NormalizedStreamEvent {
return openAiChatStream.normalizeEvent(payload, context, modelName);
@@ -98,9 +78,9 @@ export const openAiChatTransformer = {
},
serializeFinalResponse(
normalized: NormalizedFinalResponse,
usage: Parameters<typeof openAiChatOutbound.serializeFinal>[1],
usage: Parameters<typeof buildNormalizedFinalToOpenAiChatPayload>[1],
) {
return openAiChatOutbound.serializeFinal(normalized, usage);
return buildNormalizedFinalToOpenAiChatPayload(normalized, usage);
},
serializeUpstreamFinalAsStream(
payload: unknown,
@@ -108,16 +88,15 @@ export const openAiChatTransformer = {
fallbackText: string,
streamContext: StreamTransformContext,
) {
const normalizedFinal = openAiChatOutbound.normalizeFinal(payload, modelName, fallbackText);
const normalizedFinal = normalizeOpenAiChatFinalToNormalized(payload, modelName, fallbackText);
streamContext.id = normalizedFinal.id;
streamContext.model = normalizedFinal.model;
streamContext.created = normalizedFinal.created;
return openAiChatOutbound
.buildSyntheticChunks(normalizedFinal)
return buildNormalizedFinalToOpenAiChatChunks(normalizedFinal)
.map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`);
},
buildSyntheticChunks(normalized: NormalizedFinalResponse) {
return openAiChatOutbound.buildSyntheticChunks(normalized);
return buildNormalizedFinalToOpenAiChatChunks(normalized);
},
pullSseEvents(buffer: string) {
return openAiChatStream.pullSseEvents(buffer);
+7 -189
View File
@@ -1,189 +1,7 @@
import {
buildSyntheticOpenAiChunks,
normalizeUpstreamFinalResponse,
serializeFinalResponse,
type NormalizedFinalResponse,
} from '../../shared/normalized.js';
import { extractChatChoices, extractChatResponseExtras } from './helpers.js';
import type { OpenAiChatNormalizedFinalResponse } from './model.js';
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object' && !Array.isArray(value);
}
function mergeUsagePayload(
usage: unknown,
normalized: OpenAiChatNormalizedFinalResponse,
): Record<string, unknown> | undefined {
const merged = isRecord(usage) ? { ...usage } : {};
const usagePayload = normalized.usagePayload && isRecord(normalized.usagePayload)
? normalized.usagePayload
: null;
if (usagePayload) {
for (const [key, value] of Object.entries(usagePayload)) {
if (key === 'prompt_tokens' || key === 'completion_tokens' || key === 'total_tokens') continue;
if (merged[key] === undefined) merged[key] = value;
}
}
if (normalized.usageDetails?.prompt_tokens_details) {
merged.prompt_tokens_details = normalized.usageDetails.prompt_tokens_details;
}
if (normalized.usageDetails?.completion_tokens_details) {
merged.completion_tokens_details = normalized.usageDetails.completion_tokens_details;
}
return Object.keys(merged).length > 0 ? merged : undefined;
}
export const openAiChatOutbound = {
normalizeFinal(payload: unknown, modelName: string, fallbackText = ''): OpenAiChatNormalizedFinalResponse {
const choices = extractChatChoices(payload);
const primaryChoice = choices[0];
return {
...normalizeUpstreamFinalResponse(payload, modelName, fallbackText),
...(choices.length > 0 ? { choices } : {}),
...(primaryChoice?.annotations?.length ? { annotations: primaryChoice.annotations } : {}),
...(primaryChoice?.citations?.length ? { citations: primaryChoice.citations } : {}),
...extractChatResponseExtras(payload),
};
},
serializeFinal(normalized: NormalizedFinalResponse, usage?: unknown) {
const chatNormalized = normalized as OpenAiChatNormalizedFinalResponse;
const payload = serializeFinalResponse(
'openai',
normalized,
usage as { promptTokens: number; completionTokens: number; totalTokens: number },
) as Record<string, unknown>;
const choices = Array.isArray(chatNormalized.choices) && chatNormalized.choices.length > 0
? chatNormalized.choices
: [{
index: 0,
content: normalized.content,
reasoningContent: normalized.reasoningContent,
toolCalls: normalized.toolCalls,
finishReason: normalized.toolCalls.length > 0 ? 'tool_calls' : normalized.finishReason,
annotations: chatNormalized.annotations,
citations: chatNormalized.citations,
}];
payload.choices = choices.map((choice) => {
const message: Record<string, unknown> = {
role: choice.role || 'assistant',
content: choice.content,
};
if (choice.reasoningContent) {
message.reasoning_content = choice.reasoningContent;
}
if (Array.isArray(choice.toolCalls) && choice.toolCalls.length > 0) {
message.tool_calls = choice.toolCalls.map((toolCall) => ({
id: toolCall.id,
type: 'function',
function: {
name: toolCall.name,
arguments: toolCall.arguments,
},
}));
if (!choice.content) {
message.content = '';
}
}
if (Array.isArray(choice.annotations) && choice.annotations.length > 0) {
message.annotations = choice.annotations;
}
return {
index: choice.index,
message,
finish_reason: choice.finishReason || (choice.toolCalls?.length ? 'tool_calls' : 'stop'),
};
}).sort((left, right) => left.index - right.index);
if (Array.isArray(chatNormalized.citations) && chatNormalized.citations.length > 0) {
payload.citations = chatNormalized.citations;
}
const mergedUsage = mergeUsagePayload(payload.usage, chatNormalized);
if (mergedUsage) {
payload.usage = {
...(isRecord(payload.usage) ? payload.usage : {}),
...mergedUsage,
};
}
return payload;
},
buildSyntheticChunks(normalized: NormalizedFinalResponse) {
const chatNormalized = normalized as OpenAiChatNormalizedFinalResponse;
const choices = Array.isArray(chatNormalized.choices) && chatNormalized.choices.length > 0
? chatNormalized.choices
: undefined;
const chunks = choices && choices.length > 1
? [
{
id: normalized.id,
object: 'chat.completion.chunk',
created: normalized.created,
model: normalized.model,
choices: choices.map((choice) => {
const delta: Record<string, unknown> = {
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) => ({
index: toolIndex,
id: toolCall.id,
type: 'function',
function: {
name: toolCall.name,
arguments: toolCall.arguments,
},
}));
}
if (Array.isArray(choice.annotations) && choice.annotations.length > 0) {
delta.annotations = choice.annotations;
}
return {
index: choice.index,
delta,
finish_reason: null,
};
}).sort((left, right) => left.index - right.index),
},
{
id: normalized.id,
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),
},
]
: buildSyntheticOpenAiChunks(normalized);
if (chunks.length <= 0) return chunks;
if (Array.isArray(chatNormalized.citations) && chatNormalized.citations.length > 0) {
chunks[0] = {
...chunks[0],
citations: chatNormalized.citations,
};
}
if (!choices && Array.isArray(chatNormalized.annotations) && chatNormalized.annotations.length > 0) {
const firstChunk = isRecord(chunks[0]) ? chunks[0] : null;
const firstChoice = firstChunk && Array.isArray(firstChunk.choices) ? firstChunk.choices[0] : null;
if (isRecord(firstChoice) && isRecord(firstChoice.delta)) {
firstChoice.delta.annotations = chatNormalized.annotations;
}
}
return chunks;
},
};
export {
openAiChatOutbound,
openAiChatResponseBridge,
buildNormalizedFinalToOpenAiChatChunks,
buildNormalizedFinalToOpenAiChatPayload,
normalizeOpenAiChatFinalToNormalized,
} from './responseBridge.js';
@@ -2,8 +2,11 @@ import { anthropicMessagesTransformer } from '../../anthropic/messages/index.js'
import { createProxyStreamLifecycle } from '../../shared/protocolLifecycle.js';
import { type DownstreamFormat, type ParsedSseEvent } from '../../shared/normalized.js';
import { createOpenAiChatAggregateState, applyOpenAiChatStreamEvent, finalizeOpenAiChatAggregate } from './aggregator.js';
import { openAiChatOutbound } from './outbound.js';
import { openAiChatStream } from './stream.js';
import {
buildNormalizedFinalToOpenAiChatChunks,
normalizeOpenAiChatFinalToNormalized,
} from './responseBridge.js';
import { openAiChatStream } from './streamBridge.js';
import { config } from '../../../config.js';
type StreamReader = {
@@ -50,7 +53,7 @@ export function createChatProxyStreamSession(input: ChatProxyStreamSessionInput)
status: 'completed',
errorMessage: null,
};
let terminalNormalizedFinal: ReturnType<typeof openAiChatOutbound.normalizeFinal> | null = null;
let terminalNormalizedFinal: ReturnType<typeof normalizeOpenAiChatFinalToNormalized> | null = null;
let forwardedDownstreamOutput = false;
const pendingWrites: string[] = [];
@@ -206,7 +209,7 @@ export function createChatProxyStreamSession(input: ChatProxyStreamSessionInput)
const needsTerminalFinishChunk = Array.from(chatAggregateState.choices.values())
.some((choice) => !choice.finishReason);
if (needsTerminalFinishChunk) {
const terminalChunk = openAiChatOutbound.buildSyntheticChunks(
const terminalChunk = buildNormalizedFinalToOpenAiChatChunks(
finalizeOpenAiChatAggregate(chatAggregateState, {
id: streamContext.id,
model: streamContext.model,
@@ -306,14 +309,13 @@ export function createChatProxyStreamSession(input: ChatProxyStreamSessionInput)
}
}
if (input.downstreamFormat === 'openai') {
const normalizedFinal = openAiChatOutbound.normalizeFinal(payload, input.modelName, fallbackText);
const normalizedFinal = normalizeOpenAiChatFinalToNormalized(payload, input.modelName, fallbackText);
terminalNormalizedFinal = normalizedFinal;
streamContext.id = normalizedFinal.id;
streamContext.model = normalizedFinal.model;
streamContext.created = normalizedFinal.created;
emitLines(
openAiChatOutbound
.buildSyntheticChunks(normalizedFinal)
buildNormalizedFinalToOpenAiChatChunks(normalizedFinal)
.map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`),
{ meaningful: true },
);
@@ -0,0 +1,115 @@
import { describe, expect, it } from 'vitest';
import {
buildCanonicalRequestToOpenAiChatBody,
parseOpenAiChatRequestToCanonical,
} from './requestBridge.js';
describe('openai chat request bridge', () => {
it('parses OpenAI Chat bodies into canonical envelopes', () => {
const result = parseOpenAiChatRequestToCanonical({
model: 'gpt-5',
stream: true,
messages: [{ role: 'user', content: 'hello' }],
prompt_cache_key: 'cache-key',
reasoning_effort: 'high',
reasoning_budget: 1024,
});
expect(result.error).toBeUndefined();
expect(result.value).toMatchObject({
operation: 'generate',
surface: 'openai-chat',
requestedModel: 'gpt-5',
continuation: {
promptCacheKey: 'cache-key',
},
reasoning: {
effort: 'high',
budgetTokens: 1024,
},
});
});
it('builds OpenAI Chat bodies from canonical envelopes', () => {
const body = buildCanonicalRequestToOpenAiChatBody({
operation: 'generate',
surface: 'openai-chat',
cliProfile: 'codex',
requestedModel: 'gpt-5',
stream: true,
messages: [{ role: 'user', parts: [{ type: 'text', text: 'hello' }] }],
reasoning: {
effort: 'medium',
budgetTokens: 512,
},
continuation: {
promptCacheKey: 'cache-key',
},
});
expect(body).toMatchObject({
model: 'gpt-5',
stream: true,
messages: [{ role: 'user', content: 'hello' }],
prompt_cache_key: 'cache-key',
reasoning_effort: 'medium',
reasoning_budget: 512,
});
});
it('preserves assistant reasoning history alongside tool calls when parsing chat requests', () => {
const result = parseOpenAiChatRequestToCanonical({
model: 'gpt-5',
stream: false,
messages: [
{
role: 'assistant',
content: '',
reasoning_content: 'plan quietly',
tool_calls: [{
id: 'call_1',
type: 'function',
function: {
name: 'Glob',
arguments: '{"pattern":"README*"}',
},
}],
},
{
role: 'tool',
tool_call_id: 'call_1',
content: 'README.md',
},
],
});
expect(result.error).toBeUndefined();
expect(result.value?.messages).toEqual([
{
role: 'assistant',
parts: [
{
type: 'text',
text: 'plan quietly',
thought: true,
},
{
type: 'tool_call',
id: 'call_1',
name: 'Glob',
argumentsJson: '{"pattern":"README*"}',
},
],
},
{
role: 'tool',
parts: [{
type: 'tool_result',
toolCallId: 'call_1',
resultText: 'README.md',
}],
},
]);
});
});
@@ -0,0 +1,45 @@
import { canonicalRequestFromOpenAiBody, canonicalRequestToOpenAiChatBody } from '../../canonical/openAiRequestBridge.js';
import type { CanonicalRequestEnvelope } from '../../canonical/types.js';
import type { ProtocolParseContext } from '../../contracts.js';
import { openAiChatInbound } from './inbound.js';
export function parseOpenAiChatRequestToCanonical(
body: unknown,
ctx?: ProtocolParseContext,
): { value?: CanonicalRequestEnvelope; error?: { statusCode: number; payload: unknown } } {
const parsed = openAiChatInbound.parse(body);
if (parsed.error) {
return { error: parsed.error };
}
if (!parsed.value) {
return {
error: {
statusCode: 400,
payload: {
error: {
message: 'invalid chat request',
type: 'invalid_request_error',
},
},
},
};
}
return {
value: canonicalRequestFromOpenAiBody({
body: parsed.value.parsed.upstreamBody,
surface: 'openai-chat',
cliProfile: ctx?.cliProfile,
operation: ctx?.operation,
metadata: ctx?.metadata,
passthrough: ctx?.passthrough,
continuation: ctx?.continuation,
}),
};
}
export function buildCanonicalRequestToOpenAiChatBody(
request: CanonicalRequestEnvelope,
): Record<string, unknown> {
return canonicalRequestToOpenAiChatBody(request);
}
@@ -0,0 +1,161 @@
import { describe, expect, it } from 'vitest';
import {
buildNormalizedFinalToOpenAiChatChunks,
buildNormalizedFinalToOpenAiChatPayload,
normalizeOpenAiChatFinalToNormalized,
openAiChatResponseBridge,
} from './responseBridge.js';
import { openAiChatOutbound } from './outbound.js';
describe('openai chat response bridge', () => {
it('normalizes final chat payloads into normalized responses', () => {
const normalized = normalizeOpenAiChatFinalToNormalized({
id: 'chatcmpl-1',
model: 'gpt-5',
created: 123,
choices: [{
index: 0,
finish_reason: 'stop',
message: {
role: 'assistant',
content: 'hello',
reasoning_content: 'think',
},
}],
}, 'gpt-5');
expect(normalized).toMatchObject({
id: 'chatcmpl-1',
model: 'gpt-5',
content: 'hello',
reasoningContent: 'think',
finishReason: 'stop',
});
});
it('builds final chat payloads and synthetic chunks from normalized responses', () => {
const normalized = {
id: 'chatcmpl-1',
model: 'gpt-5',
created: 123,
content: 'hello',
reasoningContent: 'think',
finishReason: 'stop',
toolCalls: [],
};
const payload = buildNormalizedFinalToOpenAiChatPayload(normalized as any, {
promptTokens: 11,
completionTokens: 7,
totalTokens: 18,
});
const chunks = buildNormalizedFinalToOpenAiChatChunks(normalized as any);
expect(payload).toMatchObject({
id: 'chatcmpl-1',
model: 'gpt-5',
usage: {
prompt_tokens: 11,
completion_tokens: 7,
total_tokens: 18,
},
choices: [{
index: 0,
finish_reason: 'stop',
message: {
role: 'assistant',
content: 'hello',
reasoning_content: 'think',
},
}],
});
expect(chunks[0]).toMatchObject({
id: 'chatcmpl-1',
model: 'gpt-5',
choices: [{
index: 0,
delta: {
role: 'assistant',
content: 'hello',
},
}],
});
});
it('preserves tool-call semantics when bridging normalized tool-turn finals back to chat payloads', () => {
const normalized = {
id: 'chatcmpl-tool-1',
model: 'gpt-5',
created: 123,
content: '',
reasoningContent: 'plan first',
finishReason: 'tool_calls',
toolCalls: [{
id: 'call_1',
name: 'lookup_weather',
arguments: '{"city":"Shanghai"}',
}],
};
const payload = buildNormalizedFinalToOpenAiChatPayload(normalized as any, {
promptTokens: 11,
completionTokens: 7,
totalTokens: 18,
});
const chunks = buildNormalizedFinalToOpenAiChatChunks(normalized as any);
expect(payload).toMatchObject({
id: 'chatcmpl-tool-1',
model: 'gpt-5',
choices: [{
index: 0,
finish_reason: 'tool_calls',
message: {
role: 'assistant',
content: '',
reasoning_content: 'plan first',
tool_calls: [{
id: 'call_1',
type: 'function',
function: {
name: 'lookup_weather',
arguments: '{"city":"Shanghai"}',
},
}],
},
}],
});
expect(chunks[0]).toMatchObject({
id: 'chatcmpl-tool-1',
model: 'gpt-5',
choices: [{
index: 0,
delta: {
role: 'assistant',
content: '',
reasoning_content: 'plan first',
tool_calls: [{
index: 0,
id: 'call_1',
type: 'function',
function: {
name: 'lookup_weather',
arguments: '{"city":"Shanghai"}',
},
}],
},
}],
});
expect(chunks[1]).toMatchObject({
choices: [{
index: 0,
finish_reason: 'tool_calls',
}],
});
});
it('keeps the outbound facade pointed at the response bridge object', () => {
expect(openAiChatOutbound).toBe(openAiChatResponseBridge);
});
});
@@ -0,0 +1,210 @@
import {
buildSyntheticOpenAiChunks,
normalizeUpstreamFinalResponse,
serializeFinalResponse,
type NormalizedFinalResponse,
} from '../../shared/normalized.js';
import { extractChatChoices, extractChatResponseExtras } from './helpers.js';
import type { OpenAiChatNormalizedFinalResponse } from './model.js';
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object' && !Array.isArray(value);
}
function mergeUsagePayload(
usage: unknown,
normalized: OpenAiChatNormalizedFinalResponse,
): Record<string, unknown> | undefined {
const merged = isRecord(usage) ? { ...usage } : {};
const usagePayload = normalized.usagePayload && isRecord(normalized.usagePayload)
? normalized.usagePayload
: null;
if (usagePayload) {
for (const [key, value] of Object.entries(usagePayload)) {
if (key === 'prompt_tokens' || key === 'completion_tokens' || key === 'total_tokens') continue;
if (merged[key] === undefined) merged[key] = value;
}
}
if (normalized.usageDetails?.prompt_tokens_details) {
merged.prompt_tokens_details = normalized.usageDetails.prompt_tokens_details;
}
if (normalized.usageDetails?.completion_tokens_details) {
merged.completion_tokens_details = normalized.usageDetails.completion_tokens_details;
}
return Object.keys(merged).length > 0 ? merged : undefined;
}
export function normalizeOpenAiChatFinalToNormalized(
payload: unknown,
modelName: string,
fallbackText = '',
): OpenAiChatNormalizedFinalResponse {
const choices = extractChatChoices(payload);
const primaryChoice = choices[0];
return {
...normalizeUpstreamFinalResponse(payload, modelName, fallbackText),
...(choices.length > 0 ? { choices } : {}),
...(primaryChoice?.annotations?.length ? { annotations: primaryChoice.annotations } : {}),
...(primaryChoice?.citations?.length ? { citations: primaryChoice.citations } : {}),
...extractChatResponseExtras(payload),
};
}
export function buildNormalizedFinalToOpenAiChatPayload(
normalized: NormalizedFinalResponse,
usage?: unknown,
) {
const chatNormalized = normalized as OpenAiChatNormalizedFinalResponse;
const payload = serializeFinalResponse(
'openai',
normalized,
usage as { promptTokens: number; completionTokens: number; totalTokens: number },
) as Record<string, unknown>;
const choices = Array.isArray(chatNormalized.choices) && chatNormalized.choices.length > 0
? chatNormalized.choices
: [{
index: 0,
content: normalized.content,
reasoningContent: normalized.reasoningContent,
toolCalls: normalized.toolCalls,
finishReason: normalized.toolCalls.length > 0 ? 'tool_calls' : normalized.finishReason,
annotations: chatNormalized.annotations,
citations: chatNormalized.citations,
}];
payload.choices = choices.map((choice) => {
const message: Record<string, unknown> = {
role: choice.role || 'assistant',
content: choice.content,
};
if (choice.reasoningContent) {
message.reasoning_content = choice.reasoningContent;
}
if (Array.isArray(choice.toolCalls) && choice.toolCalls.length > 0) {
message.tool_calls = choice.toolCalls.map((toolCall) => ({
id: toolCall.id,
type: 'function',
function: {
name: toolCall.name,
arguments: toolCall.arguments,
},
}));
if (!choice.content) {
message.content = '';
}
}
if (Array.isArray(choice.annotations) && choice.annotations.length > 0) {
message.annotations = choice.annotations;
}
return {
index: choice.index,
message,
finish_reason: choice.finishReason || (choice.toolCalls?.length ? 'tool_calls' : 'stop'),
};
}).sort((left, right) => left.index - right.index);
if (Array.isArray(chatNormalized.citations) && chatNormalized.citations.length > 0) {
payload.citations = chatNormalized.citations;
}
const mergedUsage = mergeUsagePayload(payload.usage, chatNormalized);
if (mergedUsage) {
payload.usage = {
...(isRecord(payload.usage) ? payload.usage : {}),
...mergedUsage,
};
}
return payload;
}
export function buildNormalizedFinalToOpenAiChatChunks(normalized: NormalizedFinalResponse) {
const chatNormalized = normalized as OpenAiChatNormalizedFinalResponse;
const choices = Array.isArray(chatNormalized.choices) && chatNormalized.choices.length > 0
? chatNormalized.choices
: undefined;
const chunks = choices && choices.length > 1
? [
{
id: normalized.id,
object: 'chat.completion.chunk',
created: normalized.created,
model: normalized.model,
choices: choices.map((choice) => {
const delta: Record<string, unknown> = {
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) => ({
index: toolIndex,
id: toolCall.id,
type: 'function',
function: {
name: toolCall.name,
arguments: toolCall.arguments,
},
}));
}
if (Array.isArray(choice.annotations) && choice.annotations.length > 0) {
delta.annotations = choice.annotations;
}
return {
index: choice.index,
delta,
finish_reason: null,
};
}).sort((left, right) => left.index - right.index),
},
{
id: normalized.id,
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),
},
]
: buildSyntheticOpenAiChunks(normalized);
if (chunks.length <= 0) return chunks;
if (Array.isArray(chatNormalized.citations) && chatNormalized.citations.length > 0) {
chunks[0] = {
...chunks[0],
citations: chatNormalized.citations,
};
}
if (!choices && Array.isArray(chatNormalized.annotations) && chatNormalized.annotations.length > 0) {
const firstChunk = isRecord(chunks[0]) ? chunks[0] : null;
const firstChoice = firstChunk && Array.isArray(firstChunk.choices) ? firstChunk.choices[0] : null;
if (isRecord(firstChoice) && isRecord(firstChoice.delta)) {
firstChoice.delta.annotations = chatNormalized.annotations;
}
}
return chunks;
}
export const openAiChatResponseBridge = {
normalizeFinal: normalizeOpenAiChatFinalToNormalized,
serializeFinal: buildNormalizedFinalToOpenAiChatPayload,
buildSyntheticChunks: buildNormalizedFinalToOpenAiChatChunks,
};
export const openAiChatOutbound = openAiChatResponseBridge;
export {
buildNormalizedFinalToOpenAiChatChunks as buildSyntheticOpenAiChatChunksFromNormalized,
buildNormalizedFinalToOpenAiChatPayload as serializeOpenAiChatFinalPayload,
normalizeOpenAiChatFinalToNormalized as normalizeOpenAiChatFinalPayload,
};
+1 -190
View File
@@ -1,190 +1 @@
import {
createClaudeDownstreamContext,
createStreamTransformContext,
normalizeUpstreamStreamEvent,
pullSseEventsWithDone,
serializeNormalizedStreamEvent,
serializeStreamDone,
type ClaudeDownstreamContext,
type StreamTransformContext,
} from '../../shared/normalized.js';
import { extractChatChoiceEvents, extractChatResponseExtras } from './helpers.js';
import type { OpenAiChatNormalizedStreamEvent } from './model.js';
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object' && !Array.isArray(value);
}
function parseSerializedSse(lines: string[]): Array<{ index: number; payload: Record<string, unknown> }> {
return lines
.map((line, index) => {
if (!line.startsWith('data: ') || line.trim() === 'data: [DONE]') return null;
try {
return {
index,
payload: JSON.parse(line.slice(6)) as Record<string, unknown>,
};
} catch {
return null;
}
})
.filter((item): item is { index: number; payload: Record<string, unknown> } => !!item);
}
export const openAiChatStream = {
createContext(modelName: string): StreamTransformContext {
return createStreamTransformContext(modelName);
},
normalizeEvent(payload: unknown, context: StreamTransformContext, modelName: string): OpenAiChatNormalizedStreamEvent {
const normalized = normalizeUpstreamStreamEvent(payload, context, modelName);
const choiceEvents = extractChatChoiceEvents(payload);
const primaryChoice = choiceEvents[0];
const normalizedChoiceEvents = choiceEvents.map((choiceEvent, index) => (
index === 0
? {
...choiceEvent,
role: normalized.role !== undefined ? normalized.role : choiceEvent.role,
contentDelta: normalized.contentDelta,
reasoningDelta: normalized.reasoningDelta,
toolCallDeltas: normalized.toolCallDeltas !== undefined
? normalized.toolCallDeltas
: choiceEvent.toolCallDeltas,
finishReason: normalized.finishReason !== undefined
? normalized.finishReason
: choiceEvent.finishReason,
}
: choiceEvent
));
return {
...normalized,
...(primaryChoice
? {
choiceIndex: primaryChoice.index,
annotations: primaryChoice.annotations,
citations: primaryChoice.citations,
}
: {}),
...(normalizedChoiceEvents.length > 0 ? { choiceEvents: normalizedChoiceEvents } : {}),
...extractChatResponseExtras(payload),
};
},
serializeEvent(
event: OpenAiChatNormalizedStreamEvent,
context: StreamTransformContext,
downstreamContext?: ClaudeDownstreamContext,
): string[] {
if (
Array.isArray(event.choiceEvents)
&& event.choiceEvents.length > 0
&& (event.choiceEvents.length > 1 || event.choiceEvents[0]?.index !== 0)
) {
const usagePayload = event.usagePayload
? {
...(isRecord(event.usagePayload) ? event.usagePayload : {}),
...(event.usageDetails?.prompt_tokens_details
? { prompt_tokens_details: event.usageDetails.prompt_tokens_details }
: {}),
...(event.usageDetails?.completion_tokens_details
? { completion_tokens_details: event.usageDetails.completion_tokens_details }
: {}),
}
: undefined;
return [`data: ${JSON.stringify({
id: context.id || `chatcmpl-${Date.now()}`,
object: 'chat.completion.chunk',
created: context.created,
model: context.model,
choices: event.choiceEvents
.map((choiceEvent) => {
const delta: Record<string, unknown> = {};
if (choiceEvent.role) delta.role = choiceEvent.role;
if (choiceEvent.contentDelta !== undefined) delta.content = choiceEvent.contentDelta;
if (choiceEvent.reasoningDelta) delta.reasoning_content = choiceEvent.reasoningDelta;
if (Array.isArray(choiceEvent.toolCallDeltas) && choiceEvent.toolCallDeltas.length > 0) {
delta.tool_calls = choiceEvent.toolCallDeltas.map((toolCall) => ({
index: toolCall.index,
...(toolCall.id ? { id: toolCall.id } : {}),
type: 'function',
function: {
...(toolCall.name ? { name: toolCall.name } : {}),
...(toolCall.argumentsDelta !== undefined ? { arguments: toolCall.argumentsDelta } : {}),
},
}));
}
if (Array.isArray(choiceEvent.annotations) && choiceEvent.annotations.length > 0) {
delta.annotations = choiceEvent.annotations;
}
return {
index: choiceEvent.index,
delta,
finish_reason: choiceEvent.finishReason ?? null,
};
})
.sort((left, right) => left.index - right.index),
...(Array.isArray(event.citations) && event.citations.length > 0 ? { citations: event.citations } : {}),
...(usagePayload && Object.keys(usagePayload).length > 0 ? { usage: usagePayload } : {}),
})}\n\n`];
}
const lines = serializeNormalizedStreamEvent(
'openai',
event,
context,
downstreamContext ?? createClaudeDownstreamContext(),
);
if (
(!Array.isArray(event.annotations) || event.annotations.length <= 0)
&& (!Array.isArray(event.citations) || event.citations.length <= 0)
&& !event.usagePayload
&& !event.usageDetails
) {
return lines;
}
const parsedEvents = parseSerializedSse(lines);
if (parsedEvents.length <= 0) return lines;
for (const parsed of parsedEvents) {
const payload = parsed.payload;
if (Array.isArray(event.citations) && event.citations.length > 0) {
payload.citations = event.citations;
}
const firstChoice = Array.isArray(payload.choices) ? payload.choices[0] : null;
if (Array.isArray(event.annotations) && event.annotations.length > 0 && isRecord(firstChoice) && isRecord(firstChoice.delta)) {
firstChoice.delta.annotations = event.annotations;
}
if (event.usagePayload || event.usageDetails) {
payload.usage = {
...(isRecord(event.usagePayload) ? event.usagePayload : {}),
...(event.usageDetails?.prompt_tokens_details
? { prompt_tokens_details: event.usageDetails.prompt_tokens_details }
: {}),
...(event.usageDetails?.completion_tokens_details
? { completion_tokens_details: event.usageDetails.completion_tokens_details }
: {}),
};
}
lines[parsed.index] = `data: ${JSON.stringify(payload)}\n\n`;
}
return lines;
},
serializeDone(
context: StreamTransformContext,
downstreamContext?: ClaudeDownstreamContext,
): string[] {
return serializeStreamDone(
'openai',
context,
downstreamContext ?? createClaudeDownstreamContext(),
);
},
pullSseEvents(buffer: string) {
return pullSseEventsWithDone(buffer);
},
};
export { openAiChatStream } from './streamBridge.js';
@@ -0,0 +1,196 @@
import { describe, expect, it } from 'vitest';
import { createClaudeDownstreamContext } from '../../shared/normalized.js';
import { openAiChatStream } from './streamBridge.js';
function parseSsePayloads(lines: string[]): Array<Record<string, unknown>> {
return lines
.filter((line) => line.startsWith('data: ') && line.trim() !== 'data: [DONE]')
.map((line) => JSON.parse(line.slice(6)) as Record<string, unknown>);
}
describe('openai chat stream bridge', () => {
it('normalizes stream payloads with choice metadata', () => {
const context = openAiChatStream.createContext('gpt-5');
const normalized = openAiChatStream.normalizeEvent({
id: 'chatcmpl-stream-1',
model: 'gpt-5',
choices: [{
index: 0,
finish_reason: null,
delta: {
role: 'assistant',
content: 'hello',
reasoning_content: 'think',
},
}],
}, context, 'gpt-5');
expect(normalized).toMatchObject({
choiceIndex: 0,
role: 'assistant',
contentDelta: 'hello',
reasoningDelta: 'think',
});
});
it('serializes stream events and done markers as openai chat chunks', () => {
const context = openAiChatStream.createContext('gpt-5');
const lines = openAiChatStream.serializeEvent({
role: 'assistant',
contentDelta: 'hello',
} as any, context, createClaudeDownstreamContext());
const doneLines = openAiChatStream.serializeDone(context, createClaudeDownstreamContext());
const payloads = parseSsePayloads(lines);
expect(payloads[0]).toMatchObject({
model: 'gpt-5',
choices: [{
index: 0,
delta: {
role: 'assistant',
content: 'hello',
},
}],
});
expect(doneLines.join('')).toContain('[DONE]');
});
it('round-trips tool-call deltas without flattening them into plain text', () => {
const context = openAiChatStream.createContext('gpt-5');
const normalized = openAiChatStream.normalizeEvent({
id: 'chatcmpl-stream-tool-1',
model: 'gpt-5',
choices: [{
index: 0,
finish_reason: null,
delta: {
role: 'assistant',
tool_calls: [{
index: 0,
id: 'call_1',
type: 'function',
function: {
name: 'lookup_weather',
arguments: '{"city":"Shanghai"}',
},
}],
},
}],
}, context, 'gpt-5');
expect(normalized).toMatchObject({
choiceIndex: 0,
choiceEvents: [{
index: 0,
role: 'assistant',
toolCallDeltas: [{
index: 0,
id: 'call_1',
name: 'lookup_weather',
argumentsDelta: '{"city":"Shanghai"}',
}],
}],
});
const payloads = parseSsePayloads(
openAiChatStream.serializeEvent(normalized, context, createClaudeDownstreamContext()),
);
expect(payloads[0]).toMatchObject({
model: 'gpt-5',
choices: [{
index: 0,
delta: {
role: 'assistant',
tool_calls: [{
index: 0,
id: 'call_1',
type: 'function',
function: {
name: 'lookup_weather',
arguments: '{"city":"Shanghai"}',
},
}],
},
finish_reason: null,
}],
});
});
it('does not replay historical tool identity on later multi-choice argument deltas', () => {
const context = openAiChatStream.createContext('gpt-5');
const started = openAiChatStream.normalizeEvent({
id: 'chatcmpl-stream-multi-tool-1',
model: 'gpt-5',
choices: [
{
index: 0,
finish_reason: null,
delta: {
role: 'assistant',
content: 'choice-0',
},
},
{
index: 1,
finish_reason: null,
delta: {
role: 'assistant',
tool_calls: [{
index: 0,
id: 'call_1',
type: 'function',
function: {
name: 'lookup_weather',
},
}],
},
},
],
}, context, 'gpt-5');
openAiChatStream.serializeEvent(started, context, createClaudeDownstreamContext());
const continued = openAiChatStream.normalizeEvent({
id: 'chatcmpl-stream-multi-tool-1',
model: 'gpt-5',
choices: [
{
index: 1,
finish_reason: null,
delta: {
tool_calls: [{
index: 0,
function: {
arguments: '{"city":"Shanghai"}',
},
}],
},
},
],
}, context, 'gpt-5');
const payloads = parseSsePayloads(
openAiChatStream.serializeEvent(continued, context, createClaudeDownstreamContext()),
);
const toolCall = ((((payloads[0] as any).choices[0] as any).delta.tool_calls[0]) as Record<string, unknown>);
expect((payloads[0] as any).choices[0]).toMatchObject({
index: 1,
delta: {
tool_calls: [{
index: 0,
function: {
arguments: '{"city":"Shanghai"}',
},
}],
},
finish_reason: null,
});
expect(toolCall.id).toBeUndefined();
expect(toolCall.type).toBeUndefined();
expect((toolCall.function as Record<string, unknown>).name).toBeUndefined();
});
});
@@ -0,0 +1,218 @@
import {
createClaudeDownstreamContext,
createStreamTransformContext,
normalizeUpstreamStreamEvent,
pullSseEventsWithDone,
serializeNormalizedStreamEvent,
serializeStreamDone,
type ClaudeDownstreamContext,
type StreamTransformContext,
} from '../../shared/normalized.js';
import { extractChatChoiceEvents, extractChatResponseExtras } from './helpers.js';
import type { OpenAiChatNormalizedStreamEvent } from './model.js';
const multiChoiceToolCallState = new WeakMap<
StreamTransformContext,
Record<string, { id?: string; name?: string; arguments?: string }>
>();
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object' && !Array.isArray(value);
}
function getMultiChoiceToolState(
context: StreamTransformContext,
): Record<string, { id?: string; name?: string; arguments?: string }> {
const existing = multiChoiceToolCallState.get(context);
if (existing) return existing;
const created: Record<string, { id?: string; name?: string; arguments?: string }> = {};
multiChoiceToolCallState.set(context, created);
return created;
}
function parseSerializedSse(lines: string[]): Array<{ index: number; payload: Record<string, unknown> }> {
return lines
.map((line, index) => {
if (!line.startsWith('data: ') || line.trim() === 'data: [DONE]') return null;
try {
return {
index,
payload: JSON.parse(line.slice(6)) as Record<string, unknown>,
};
} catch {
return null;
}
})
.filter((item): item is { index: number; payload: Record<string, unknown> } => !!item);
}
export const openAiChatStream = {
createContext(modelName: string): StreamTransformContext {
return createStreamTransformContext(modelName);
},
normalizeEvent(payload: unknown, context: StreamTransformContext, modelName: string): OpenAiChatNormalizedStreamEvent {
const normalized = normalizeUpstreamStreamEvent(payload, context, modelName);
const choiceEvents = extractChatChoiceEvents(payload);
const primaryChoice = choiceEvents[0];
const normalizedChoiceEvents = choiceEvents.map((choiceEvent, index) => (
index === 0
? {
...choiceEvent,
role: normalized.role !== undefined ? normalized.role : choiceEvent.role,
contentDelta: normalized.contentDelta,
reasoningDelta: normalized.reasoningDelta,
toolCallDeltas: normalized.toolCallDeltas !== undefined
? normalized.toolCallDeltas
: choiceEvent.toolCallDeltas,
finishReason: normalized.finishReason !== undefined
? normalized.finishReason
: choiceEvent.finishReason,
}
: choiceEvent
));
return {
...normalized,
...(primaryChoice
? {
choiceIndex: primaryChoice.index,
annotations: primaryChoice.annotations,
citations: primaryChoice.citations,
}
: {}),
...(normalizedChoiceEvents.length > 0 ? { choiceEvents: normalizedChoiceEvents } : {}),
...extractChatResponseExtras(payload),
};
},
serializeEvent(
event: OpenAiChatNormalizedStreamEvent,
context: StreamTransformContext,
downstreamContext?: ClaudeDownstreamContext,
): string[] {
if (
Array.isArray(event.choiceEvents)
&& event.choiceEvents.length > 0
&& (event.choiceEvents.length > 1 || event.choiceEvents[0]?.index !== 0)
) {
const usagePayload = event.usagePayload
? {
...(isRecord(event.usagePayload) ? event.usagePayload : {}),
...(event.usageDetails?.prompt_tokens_details
? { prompt_tokens_details: event.usageDetails.prompt_tokens_details }
: {}),
...(event.usageDetails?.completion_tokens_details
? { completion_tokens_details: event.usageDetails.completion_tokens_details }
: {}),
}
: undefined;
return [`data: ${JSON.stringify({
id: context.id || `chatcmpl-${Date.now()}`,
object: 'chat.completion.chunk',
created: context.created,
model: context.model,
choices: event.choiceEvents
.map((choiceEvent) => {
const delta: Record<string, unknown> = {};
if (choiceEvent.role) delta.role = choiceEvent.role;
if (choiceEvent.contentDelta !== undefined) delta.content = choiceEvent.contentDelta;
if (choiceEvent.reasoningDelta) delta.reasoning_content = choiceEvent.reasoningDelta;
if (Array.isArray(choiceEvent.toolCallDeltas) && choiceEvent.toolCallDeltas.length > 0) {
const toolState = getMultiChoiceToolState(context);
delta.tool_calls = choiceEvent.toolCallDeltas.map((toolCall) => {
const toolIndex = Number.isFinite(toolCall.index) ? Math.max(0, Math.trunc(toolCall.index)) : 0;
const stateKey = `${choiceEvent.index}:${toolIndex}`;
const existing = toolState[stateKey] || {};
toolState[stateKey] = {
...(toolCall.id || existing.id ? { id: toolCall.id || existing.id } : {}),
...(toolCall.name || existing.name ? { name: toolCall.name || existing.name } : {}),
arguments: `${existing.arguments || ''}${toolCall.argumentsDelta ?? ''}`,
};
const functionPayload: Record<string, unknown> = {};
if (toolCall.name) functionPayload.name = toolCall.name;
if (toolCall.argumentsDelta !== undefined) functionPayload.arguments = toolCall.argumentsDelta;
return {
index: toolIndex,
...(toolCall.id ? { id: toolCall.id } : {}),
...(toolCall.id || toolCall.name ? { type: 'function' } : {}),
...(Object.keys(functionPayload).length > 0 ? { function: functionPayload } : {}),
};
});
}
if (Array.isArray(choiceEvent.annotations) && choiceEvent.annotations.length > 0) {
delta.annotations = choiceEvent.annotations;
}
return {
index: choiceEvent.index,
delta,
finish_reason: choiceEvent.finishReason ?? null,
};
})
.sort((left, right) => left.index - right.index),
...(Array.isArray(event.citations) && event.citations.length > 0 ? { citations: event.citations } : {}),
...(usagePayload && Object.keys(usagePayload).length > 0 ? { usage: usagePayload } : {}),
})}\n\n`];
}
const lines = serializeNormalizedStreamEvent(
'openai',
event,
context,
downstreamContext ?? createClaudeDownstreamContext(),
);
if (
(!Array.isArray(event.annotations) || event.annotations.length <= 0)
&& (!Array.isArray(event.citations) || event.citations.length <= 0)
&& !event.usagePayload
&& !event.usageDetails
) {
return lines;
}
const parsedEvents = parseSerializedSse(lines);
if (parsedEvents.length <= 0) return lines;
for (const parsed of parsedEvents) {
const payload = parsed.payload;
if (Array.isArray(event.citations) && event.citations.length > 0) {
payload.citations = event.citations;
}
const firstChoice = Array.isArray(payload.choices) ? payload.choices[0] : null;
if (Array.isArray(event.annotations) && event.annotations.length > 0 && isRecord(firstChoice) && isRecord(firstChoice.delta)) {
firstChoice.delta.annotations = event.annotations;
}
if (event.usagePayload || event.usageDetails) {
payload.usage = {
...(isRecord(event.usagePayload) ? event.usagePayload : {}),
...(event.usageDetails?.prompt_tokens_details
? { prompt_tokens_details: event.usageDetails.prompt_tokens_details }
: {}),
...(event.usageDetails?.completion_tokens_details
? { completion_tokens_details: event.usageDetails.completion_tokens_details }
: {}),
};
}
lines[parsed.index] = `data: ${JSON.stringify(payload)}\n\n`;
}
return lines;
},
serializeDone(
context: StreamTransformContext,
downstreamContext?: ClaudeDownstreamContext,
): string[] {
return serializeStreamDone(
'openai',
context,
downstreamContext ?? createClaudeDownstreamContext(),
);
},
pullSseEvents(buffer: string) {
return pullSseEventsWithDone(buffer);
},
};
@@ -1,5 +1,5 @@
import { type StreamTransformContext } from '../../shared/normalized.js';
import type { OpenAiResponsesStreamEvent } from './stream.js';
import type { OpenAiResponsesStreamEvent } from './streamBridge.js';
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object' && !Array.isArray(value);
@@ -1,8 +1,10 @@
import { describe, expect, it } from 'vitest';
import { normalizeCodexResponsesBodyForProxy } from './codexCompatibility.js';
const CODEX_DEFAULT_INSTRUCTIONS = 'You are a helpful coding assistant.';
describe('normalizeCodexResponsesBodyForProxy', () => {
it('normalizes codex responses bodies before proxying upstream', () => {
it('extracts codex system input into top-level instructions before proxying upstream', () => {
const body = normalizeCodexResponsesBodyForProxy({
input: [
{
@@ -10,7 +12,13 @@ describe('normalizeCodexResponsesBodyForProxy', () => {
role: 'system',
content: [{ type: 'input_text', text: 'be precise' }],
},
{
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: 'hello' }],
},
],
instructions: 'keep edits narrow',
max_output_tokens: 512,
max_completion_tokens: 256,
max_tokens: 128,
@@ -22,16 +30,28 @@ describe('normalizeCodexResponsesBodyForProxy', () => {
input: [
{
type: 'message',
role: 'developer',
content: [{ type: 'input_text', text: 'be precise' }],
role: 'user',
content: [{ type: 'input_text', text: 'hello' }],
},
],
instructions: '',
instructions: 'be precise\n\nkeep edits narrow',
store: false,
temperature: 0.3,
});
});
it('supplies a non-empty default instructions string for codex when missing', () => {
const body = normalizeCodexResponsesBodyForProxy({
input: 'hello',
}, 'codex');
expect(body).toEqual({
input: 'hello',
instructions: CODEX_DEFAULT_INSTRUCTIONS,
store: false,
});
});
it('leaves non-codex bodies untouched', () => {
const source = {
input: 'hello',
@@ -6,15 +6,76 @@ function asTrimmedString(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
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 '';
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;
if (typeof content.input_text === 'string') return content.input_text;
return '';
}
function extractSystemMessagesToInstructions(
body: Record<string, unknown>,
): Record<string, unknown> {
if (!Array.isArray(body.input) || body.input.length <= 0) return body;
const extractedSystemTexts: string[] = [];
const remainingInput: unknown[] = [];
for (const item of body.input) {
if (!isRecord(item)) {
remainingInput.push(item);
continue;
}
if (asTrimmedString(item.type).toLowerCase() !== 'message') {
remainingInput.push(item);
continue;
}
if (asTrimmedString(item.role).toLowerCase() !== 'system') {
remainingInput.push(item);
continue;
}
const text = extractTextFromContent(item.content);
if (text.trim()) {
extractedSystemTexts.push(text);
}
}
if (extractedSystemTexts.length <= 0) return body;
const extractedInstructions = extractedSystemTexts.join('\n\n');
const existingInstructions = typeof body.instructions === 'string' ? body.instructions : '';
const nextInstructions = existingInstructions.trim()
? `${extractedInstructions}\n\n${existingInstructions}`
: extractedInstructions;
return {
...body,
input: remainingInput,
instructions: nextInstructions,
};
}
function ensureCodexResponsesInstructions(
body: Record<string, unknown>,
sitePlatform: string,
): Record<string, unknown> {
if (sitePlatform !== 'codex') return body;
if (typeof body.instructions === 'string') return body;
if (typeof body.instructions === 'string' && body.instructions.trim()) return body;
return {
...body,
instructions: '',
instructions: CODEX_DEFAULT_INSTRUCTIONS,
};
}
@@ -41,35 +102,12 @@ function stripCodexUnsupportedResponsesFields(
return next;
}
function convertCodexSystemRoleToDeveloper(input: unknown): unknown {
if (!Array.isArray(input)) return input;
return input.map((item) => {
if (!isRecord(item)) return item;
if (asTrimmedString(item.type).toLowerCase() !== 'message') return item;
if (asTrimmedString(item.role).toLowerCase() !== 'system') return item;
return {
...item,
role: 'developer',
};
});
}
function applyCodexResponsesCompatibility(
body: Record<string, unknown>,
sitePlatform: string,
): Record<string, unknown> {
if (sitePlatform !== 'codex') return body;
const next: Record<string, unknown> = {
...body,
input: convertCodexSystemRoleToDeveloper(body.input),
};
if (typeof next.instructions !== 'string') {
next.instructions = '';
}
return next;
return extractSystemMessagesToInstructions(body);
}
export function normalizeCodexResponsesBodyForProxy(
@@ -7,6 +7,10 @@ import {
convertOpenAiBodyToResponsesBody,
sanitizeResponsesBodyForProxy,
} from './conversion.js';
import {
hasEndpointMismatchHint,
inferRequiredEndpointFromProtocolError,
} from '../../shared/endpointCompatibility.js';
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object';
@@ -18,11 +22,17 @@ function asTrimmedString(value: unknown): string {
export function buildResponsesCompatibilityBodies(
body: Record<string, unknown>,
options?: {
sitePlatform?: string;
},
): Record<string, unknown>[] {
const candidates: Record<string, unknown>[] = [];
const seen = new Set<string>();
const useSemanticDedupe = asTrimmedString(options?.sitePlatform).toLowerCase() === 'sub2api';
try {
const originalKey = JSON.stringify(body);
const originalKey = useSemanticDedupe
? fingerprintCompatibilityValue(body)
: JSON.stringify(body);
if (originalKey) seen.add(originalKey);
} catch {
// ignore non-serializable bodies
@@ -32,7 +42,9 @@ export function buildResponsesCompatibilityBodies(
if (!next) return;
let key = '';
try {
key = JSON.stringify(next);
key = useSemanticDedupe
? fingerprintCompatibilityValue(next)
: JSON.stringify(next);
} catch {
return;
}
@@ -77,19 +89,28 @@ export function buildResponsesCompatibilityBodies(
'background',
'top_logprobs',
] as const;
const normalizedSitePlatform = asTrimmedString(options?.sitePlatform).toLowerCase();
if (normalizedSitePlatform === 'sub2api' && body.store !== undefined) {
richCandidate.store = cloneJsonValue(body.store);
}
for (const key of passthroughFields) {
if (body[key] === undefined) continue;
richCandidate[key] = cloneJsonValue(body[key]);
}
push(richCandidate);
}
push(buildStrictResponsesBody(body));
if (!shouldPreserveResponsesCompatibilitySemantics(options?.sitePlatform, body)) {
push(buildStrictResponsesBody(body));
}
return candidates;
}
export function buildResponsesCompatibilityHeaderCandidates(
headers: Record<string, string>,
stream: boolean,
options?: {
sitePlatform?: string;
},
): Record<string, string>[] {
const candidates: Record<string, string>[] = [];
const seen = new Set<string>();
@@ -106,6 +127,12 @@ export function buildResponsesCompatibilityHeaderCandidates(
push(headers);
const normalizedSitePlatform = asTrimmedString(options?.sitePlatform).toLowerCase();
if (normalizedSitePlatform === 'sub2api') {
push(buildSub2ApiResponsesCompatibilityHeaders(headers, stream));
return candidates;
}
const minimal: Record<string, string> = {};
for (const [rawKey, rawValue] of Object.entries(headers)) {
const key = rawKey.toLowerCase();
@@ -129,6 +156,7 @@ export function shouldRetryResponsesCompatibility(input: {
endpoint: string;
status: number;
rawErrText: string;
body: Record<string, unknown>;
}): boolean {
if (input.endpoint !== 'responses') return false;
if (input.status !== 400) return false;
@@ -138,6 +166,7 @@ export function shouldRetryResponsesCompatibility(input: {
const message = parsedError.message.trim().toLowerCase();
const compact = `${type} ${code} ${message}`.trim();
const rawCompact = (input.rawErrText || '').toLowerCase();
const requiredEndpoint = inferRequiredEndpointFromProtocolError(input.rawErrText);
if (
compact.includes('invalid_api_key')
@@ -150,11 +179,30 @@ export function shouldRetryResponsesCompatibility(input: {
return false;
}
if (type === 'upstream_error' || code === 'upstream_error') return true;
if (message === 'upstream_error' || message === 'upstream request failed') return true;
if (rawCompact.includes('upstream_error')) return true;
const hasCompatibilityHint = (
compact.includes('unsupported')
|| compact.includes('not supported')
|| compact.includes('application/json')
|| compact.includes('content-type')
|| compact.includes('request validation failed')
|| requiredEndpoint !== null
|| hasEndpointMismatchHint(input.rawErrText)
|| compact.includes('invalid_request_error')
|| compact.includes('bad_response_status_code')
|| compact.includes('openai_error')
|| compact.includes('unsupported legacy protocol')
);
if (hasCompatibilityHint) return true;
return true;
const compatibilityCandidates = buildResponsesCompatibilityBodies(input.body);
if (compatibilityCandidates.length > 0) {
if (type === 'upstream_error' || code === 'upstream_error') return true;
if (message === 'upstream_error' || message === 'upstream request failed') return true;
if (rawCompact.includes('upstream_error')) return true;
if (compact.length > 0) return true;
}
return false;
}
export function shouldDowngradeResponsesChatToMessages(
@@ -164,7 +212,7 @@ export function shouldDowngradeResponsesChatToMessages(
): boolean {
if (!endpointPath.includes('/chat/completions')) return false;
if (status < 400 || status >= 500) return false;
return /messages\s+is\s+required/i.test(upstreamErrorText);
return inferRequiredEndpointFromProtocolError(upstreamErrorText) === 'messages';
}
function toFiniteNumber(value: unknown): number | null {
@@ -183,6 +231,20 @@ function cloneJsonValue<T>(value: T): T {
return value;
}
function fingerprintCompatibilityValue(value: unknown): string {
if (value === undefined) return '__undefined__';
if (value === null || typeof value !== 'object') {
return JSON.stringify(value);
}
if (Array.isArray(value)) {
return `[${value.map((item) => fingerprintCompatibilityValue(item)).join(',')}]`;
}
const entries = Object.entries(value)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, item]) => `${JSON.stringify(key)}:${fingerprintCompatibilityValue(item)}`);
return `{${entries.join(',')}}`;
}
function parseUpstreamErrorShape(rawText: string): {
type: string;
code: string;
@@ -208,6 +270,60 @@ function getExplicitResponsesInstructions(body: Record<string, unknown>): string
return typeof body.instructions === 'string' ? body.instructions.trim() : '';
}
function shouldPreserveResponsesCompatibilitySemantics(
sitePlatform: string | undefined,
body: Record<string, unknown>,
): boolean {
if (asTrimmedString(sitePlatform).toLowerCase() !== 'sub2api') return false;
return [
'store',
'include',
'reasoning',
'previous_response_id',
'truncation',
'text',
'service_tier',
'safety_identifier',
'max_tool_calls',
'prompt_cache_key',
'prompt_cache_retention',
'background',
'top_logprobs',
].some((key) => body[key] !== undefined);
}
const SUB2API_RESPONSES_COMPATIBILITY_HEADER_ALLOWLIST = new Set([
'accept',
'accept-language',
'authorization',
'content-type',
'conversation-id',
'conversation_id',
'openai-beta',
'originator',
'session-id',
'session_id',
'user-agent',
'x-api-key',
'x-codex-turn-metadata',
'x-codex-turn-state',
]);
function buildSub2ApiResponsesCompatibilityHeaders(
headers: Record<string, string>,
stream: boolean,
): Record<string, string> {
const next: Record<string, string> = {};
for (const [rawKey, rawValue] of Object.entries(headers)) {
const key = rawKey.toLowerCase();
if (!SUB2API_RESPONSES_COMPATIBILITY_HEADER_ALLOWLIST.has(key)) continue;
next[key] = rawValue;
}
if (!next['content-type']) next['content-type'] = 'application/json';
if (stream && !next.accept) next.accept = 'text/event-stream';
return next;
}
function stripResponsesMetadata(
body: Record<string, unknown>,
): Record<string, unknown> | null {
@@ -7,10 +7,12 @@ import {
} from './conversion.js';
import {
buildResponsesCompatibilityBodies,
buildResponsesCompatibilityHeaderCandidates,
convertOpenAiBodyToResponsesBody as convertOpenAiBodyToResponsesBodyViaCompatibility,
normalizeResponsesInputForCompatibility as normalizeResponsesInputForCompatibilityViaCompatibility,
normalizeResponsesMessageContent as normalizeResponsesMessageContentViaCompatibility,
sanitizeResponsesBodyForProxy as sanitizeResponsesBodyForProxyViaCompatibility,
shouldRetryResponsesCompatibility,
} from './compatibility.js';
import {
normalizeResponsesInputForCompatibility,
@@ -704,6 +706,77 @@ describe('convertOpenAiBodyToResponsesBody', () => {
expect(result.include).toBeUndefined();
});
it('does not synthesize max_output_tokens when chat-style input omits all token-limit fields', () => {
const result = convertOpenAiBodyToResponsesBody(
{
model: 'gpt-5',
messages: [{ role: 'user', content: 'hello' }],
},
'gpt-5',
false,
);
expect(result.max_output_tokens).toBeUndefined();
});
it('clamps explicit chat token limits to the sub2api floor when converting to Responses bodies', () => {
const result = convertOpenAiBodyToResponsesBody(
{
model: 'gpt-5',
messages: [{ role: 'user', content: 'hello' }],
max_tokens: 64,
},
'gpt-5',
false,
);
expect(result.max_output_tokens).toBe(128);
});
it('uses sub2api-compatible placeholders for empty tool arguments and empty tool outputs', () => {
const result = convertOpenAiBodyToResponsesBody(
{
model: 'gpt-5',
messages: [
{
role: 'assistant',
tool_calls: [
{
id: 'call_empty_1',
type: 'function',
function: {
name: 'lookup_weather',
arguments: '',
},
},
],
},
{
role: 'tool',
tool_call_id: 'call_empty_1',
content: '',
},
],
},
'gpt-5',
false,
);
expect(result.input).toEqual([
{
type: 'function_call',
call_id: 'call_empty_1',
name: 'lookup_weather',
arguments: '{}',
},
{
type: 'function_call_output',
call_id: 'call_empty_1',
output: '(empty)',
},
]);
});
it('maps OpenAI file-style content blocks into inline-only Responses input_file blocks', () => {
const result = convertOpenAiBodyToResponsesBody(
{
@@ -1568,6 +1641,87 @@ describe('convertResponsesBodyToOpenAiBody', () => {
});
});
it('does not generate extra compatibility bodies for sub2api when responses-only semantics are already present', () => {
const candidates = buildResponsesCompatibilityBodies({
model: 'gpt-5',
input: 'hello',
stream: true,
store: false,
include: ['reasoning.encrypted_content'],
reasoning: {
effort: 'high',
},
previous_response_id: 'resp_prev_9',
prompt_cache_key: 'cache-key-9',
service_tier: 'priority',
background: true,
}, {
sitePlatform: 'sub2api',
});
expect(candidates).toEqual([]);
});
it('builds a sub2api-safe compatibility header candidate that preserves session and codex passthrough headers', () => {
const candidates = buildResponsesCompatibilityHeaderCandidates({
accept: 'text/event-stream',
authorization: 'Bearer sk-test',
'content-type': 'application/json',
'accept-language': 'zh-CN',
'openai-beta': 'responses-2025-03-11',
originator: 'codex_cli_rs',
'user-agent': 'client-ua/1.0',
session_id: 'session-123',
conversation_id: 'conversation-123',
'x-codex-turn-state': 'turn-state',
'x-codex-turn-metadata': 'turn-metadata',
'x-stainless-lang': 'typescript',
version: '0.202.0',
}, true, {
sitePlatform: 'sub2api',
});
expect(candidates).toContainEqual({
accept: 'text/event-stream',
authorization: 'Bearer sk-test',
'content-type': 'application/json',
'accept-language': 'zh-CN',
'openai-beta': 'responses-2025-03-11',
originator: 'codex_cli_rs',
'user-agent': 'client-ua/1.0',
session_id: 'session-123',
conversation_id: 'conversation-123',
'x-codex-turn-state': 'turn-state',
'x-codex-turn-metadata': 'turn-metadata',
});
expect(candidates).not.toContainEqual({
accept: 'text/event-stream',
authorization: 'Bearer sk-test',
'content-type': 'application/json',
});
});
it('deduplicates semantically identical sub2api retry bodies even when field order differs', () => {
const candidates = buildResponsesCompatibilityBodies({
model: 'gpt-5',
input: 'hello',
previous_response_id: 'resp_prev_9',
include: ['reasoning.encrypted_content'],
reasoning: {
effort: 'high',
},
prompt_cache_key: 'cache-key-9',
service_tier: 'priority',
background: true,
stream: true,
store: false,
}, {
sitePlatform: 'sub2api',
});
expect(candidates).toEqual([]);
});
it('preserves tools and tool_choice across all compatibility retry bodies', () => {
const candidates = buildResponsesCompatibilityBodies({
model: 'gpt-5',
@@ -1618,6 +1772,39 @@ describe('convertResponsesBodyToOpenAiBody', () => {
}
});
it('does not retry auth-shaped failures and still retries generic upstream errors through compatibility variants', () => {
expect(shouldRetryResponsesCompatibility({
endpoint: 'responses',
status: 400,
rawErrText: JSON.stringify({
error: {
message: 'invalid_api_key',
type: 'authentication_error',
},
}),
body: {
model: 'gpt-5',
input: 'hello',
},
})).toBe(false);
expect(shouldRetryResponsesCompatibility({
endpoint: 'responses',
status: 400,
rawErrText: JSON.stringify({
error: {
message: '',
type: 'upstream_error',
},
}),
body: {
model: 'gpt-5',
input: 'hello',
metadata: { trace_id: 'req-1' },
},
})).toBe(true);
});
it('maps native tool choices and strict function tools into OpenAI-compatible fallback bodies', () => {
const result = convertResponsesBodyToOpenAiBody(
{
@@ -439,6 +439,8 @@ const RESPONSES_COMPATIBILITY_FILTER_FIELDS = new Set([
'verbosity',
]);
const MIN_RESPONSES_MAX_OUTPUT_TOKENS = 128;
export function sanitizeResponsesBodyForProxy(
body: Record<string, unknown>,
modelName: string,
@@ -571,7 +573,7 @@ export function convertOpenAiBodyToResponsesBody(
type: 'function_call',
call_id: callId,
name: getShortToolName(name, toolNameMap),
arguments: argumentsValue,
arguments: argumentsValue || '{}',
});
}
continue;
@@ -580,10 +582,16 @@ export function convertOpenAiBodyToResponsesBody(
if (role === 'tool') {
const callId = asTrimmedString(item.tool_call_id) || asTrimmedString(item.id);
if (!callId) continue;
const output = normalizeToolOutput(item.content);
inputItems.push({
type: 'function_call_output',
call_id: callId,
output: normalizeToolOutput(item.content),
output: (
(typeof output === 'string' && output === '')
|| (Array.isArray(output) && output.length === 0)
)
? '(empty)'
: output,
});
continue;
}
@@ -597,20 +605,25 @@ export function convertOpenAiBodyToResponsesBody(
});
}
const maxOutputTokens = (
const requestedMaxOutputTokens = (
toFiniteNumber(openaiBody.max_output_tokens)
?? toFiniteNumber(openaiBody.max_completion_tokens)
?? toFiniteNumber(openaiBody.max_tokens)
?? 4096
);
const body: Record<string, unknown> = {
model: modelName,
stream,
max_output_tokens: maxOutputTokens,
input: inputItems,
};
if (requestedMaxOutputTokens !== null && requestedMaxOutputTokens > 0) {
body.max_output_tokens = Math.max(
MIN_RESPONSES_MAX_OUTPUT_TOKENS,
Math.trunc(requestedMaxOutputTokens),
);
}
if (systemContents.length > 0) {
body.instructions = systemContents.join('\n\n');
}
@@ -72,6 +72,41 @@ describe('openAiResponsesTransformer.inbound', () => {
});
});
it('round-trips continuation turnState through responses metadata bridging', () => {
const built = openAiResponsesTransformer.buildProtocolRequest({
operation: 'generate',
surface: 'openai-responses',
cliProfile: 'codex',
requestedModel: 'gpt-5',
stream: false,
messages: [{ role: 'user', parts: [{ type: 'text', text: 'hello' }] }],
continuation: {
turnState: 'turn-state-responses-1',
},
});
expect(built).toMatchObject({
metadata: {
metapi_turn_state: 'turn-state-responses-1',
},
});
const parsed = openAiResponsesTransformer.parseRequest({
model: 'gpt-5',
input: 'hello',
metadata: {
metapi_turn_state: 'turn-state-responses-1',
},
});
expect(parsed.error).toBeUndefined();
expect(parsed.value).toMatchObject({
continuation: {
turnState: 'turn-state-responses-1',
},
});
});
it('returns a protocol request envelope with a normalized responses body', () => {
const result = openAiResponsesTransformer.transformRequest({
model: 'gpt-5',
@@ -1,4 +1,3 @@
import { canonicalRequestFromOpenAiBody, canonicalRequestToOpenAiChatBody } from '../../canonical/request.js';
import type { CanonicalRequestEnvelope } from '../../canonical/types.js';
import type { ProtocolBuildContext, ProtocolParseContext } from '../../contracts.js';
import { type StreamTransformContext } from '../../shared/normalized.js';
@@ -23,11 +22,19 @@ import {
failResponsesStream,
serializeConvertedResponsesEvents,
} from './aggregator.js';
import { openAiResponsesOutbound } from './outbound.js';
import { openAiResponsesInbound } from './inbound.js';
import { createResponsesProxyStreamSession } from './proxyStream.js';
import {
buildCanonicalRequestToOpenAiResponsesBody,
parseOpenAiResponsesRequestToCanonical,
} from './requestBridge.js';
import {
openAiResponsesResponseBridge,
buildNormalizedFinalToOpenAiResponsesPayload,
normalizeOpenAiResponsesFinalToNormalized,
} from './responseBridge.js';
import { createResponsesEndpointStrategy } from './routeCompatibility.js';
import { openAiResponsesStream } from './stream.js';
import { openAiResponsesStream } from './streamBridge.js';
import { openAiResponsesUsage } from './usage.js';
import type {
OpenAiResponsesParsedRequest as OpenAiResponsesParsedRequestModel,
@@ -45,7 +52,7 @@ export const openAiResponsesTransformer = {
fromOpenAiBody: convertOpenAiBodyToResponsesBody,
toOpenAiBody: convertResponsesBodyToOpenAiBody,
},
outbound: openAiResponsesOutbound,
outbound: openAiResponsesResponseBridge,
stream: openAiResponsesStream,
usage: openAiResponsesUsage,
compatibility: {
@@ -68,59 +75,13 @@ export const openAiResponsesTransformer = {
body: unknown,
ctx?: ProtocolParseContext,
): { value?: CanonicalRequestEnvelope; error?: { statusCode: number; payload: unknown } } {
const parsed = openAiResponsesInbound.parse(body, {
defaultEncryptedReasoningInclude: ctx?.defaultEncryptedReasoningInclude,
});
if (parsed.error) {
return { error: parsed.error };
}
if (!parsed.value) {
return {
error: {
statusCode: 400,
payload: {
error: {
message: 'invalid responses request',
type: 'invalid_request_error',
},
},
},
};
}
const responsesBody = parsed.value.parsed.normalizedBody;
const openAiBody = convertResponsesBodyToOpenAiBody(
responsesBody,
typeof responsesBody.model === 'string' ? responsesBody.model : parsed.value.model,
responsesBody.stream === true,
{ defaultEncryptedReasoningInclude: ctx?.defaultEncryptedReasoningInclude },
);
return {
value: canonicalRequestFromOpenAiBody({
body: openAiBody,
surface: 'openai-responses',
cliProfile: ctx?.cliProfile,
operation: ctx?.operation,
metadata: ctx?.metadata,
passthrough: ctx?.passthrough,
continuation: ctx?.continuation,
}),
};
return parseOpenAiResponsesRequestToCanonical(body, ctx);
},
buildProtocolRequest(
request: CanonicalRequestEnvelope,
_ctx?: ProtocolBuildContext,
): Record<string, unknown> {
const openAiBody = canonicalRequestToOpenAiChatBody(request);
if (request.reasoning) {
openAiBody.reasoning = {
...(request.reasoning.effort ? { effort: request.reasoning.effort } : {}),
...(request.reasoning.budgetTokens !== undefined ? { budget_tokens: request.reasoning.budgetTokens } : {}),
...(request.reasoning.summary ? { summary: request.reasoning.summary } : {}),
};
}
return convertOpenAiBodyToResponsesBody(openAiBody, request.requestedModel, request.stream);
return buildCanonicalRequestToOpenAiResponsesBody(request);
},
transformRequest(
body: unknown,
@@ -132,7 +93,7 @@ export const openAiResponsesTransformer = {
return openAiResponsesStream.createContext(modelName);
},
transformFinalResponse(payload: unknown, modelName: string, fallbackText = '') {
return openAiResponsesOutbound.normalizeFinal(payload, modelName, fallbackText);
return normalizeOpenAiResponsesFinalToNormalized(payload, modelName, fallbackText);
},
transformStreamEvent(payload: unknown, context: StreamTransformContext, modelName: string) {
return openAiResponsesStream.normalizeEvent(payload, context, modelName);
@@ -140,6 +101,15 @@ export const openAiResponsesTransformer = {
pullSseEvents(buffer: string) {
return openAiResponsesStream.pullSseEvents(buffer);
},
serializeFinalResponse(
normalized: Parameters<typeof buildNormalizedFinalToOpenAiResponsesPayload>[0]['normalized'],
input: Omit<Parameters<typeof buildNormalizedFinalToOpenAiResponsesPayload>[0], 'normalized'>,
) {
return buildNormalizedFinalToOpenAiResponsesPayload({
...input,
normalized,
});
},
};
export type OpenAiResponsesTransformer = typeof openAiResponsesTransformer;
@@ -1,449 +1,12 @@
import {
normalizeUpstreamFinalResponse,
type NormalizedFinalResponse,
} from '../../shared/normalized.js';
import { decodeOpenAiEncryptedReasoning } from '../../shared/reasoningTransport.js';
import { decodeResponsesMcpCompatToolCall } from './mcpCompatibility.js';
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object';
}
function asTrimmedString(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
function cloneJson<T>(value: T): T {
if (Array.isArray(value)) {
return value.map((item) => cloneJson(item)) as T;
}
if (isRecord(value)) {
return Object.fromEntries(
Object.entries(value).map(([key, item]) => [key, cloneJson(item)]),
) as T;
}
return value;
}
function ensureResponseId(rawId: string): string {
const trimmed = rawId.trim() || `resp_${Date.now()}`;
return trimmed.startsWith('resp_') ? trimmed : `resp_${trimmed}`;
}
function ensureMessageId(rawId: string): string {
const trimmed = rawId.trim() || `msg_${Date.now()}`;
return trimmed.startsWith('msg_') ? trimmed : `msg_${trimmed}`;
}
function ensureFunctionCallId(rawId: string): string {
const trimmed = rawId.trim();
if (!trimmed) return `call_${Date.now()}`;
return trimmed.startsWith('call_') ? trimmed : `call_${trimmed}`;
}
function stringifyToolInput(input: unknown): string {
if (typeof input === 'string') return input;
if (!input || typeof input !== 'object') return '';
try {
return JSON.stringify(input);
} catch {
return '';
}
}
export type ResponsesUsageSummary = {
promptTokens: number;
completionTokens: number;
totalTokens: number;
inputTokensDetails?: Record<string, unknown>;
outputTokensDetails?: Record<string, unknown>;
};
export type ResponsesToolCall = {
id: string;
name: string;
arguments: string;
};
export type ResponsesFinalSerializationMode = 'response' | 'compact';
type ResponsesOutputItem = Record<string, unknown>;
function extractToolCallsFromUpstream(payload: unknown): ResponsesToolCall[] {
if (!isRecord(payload)) return [];
if (Array.isArray(payload.choices)) {
const choice = payload.choices[0];
const message = isRecord((choice as any)?.message) ? (choice as any).message : {};
const toolCalls = Array.isArray((message as any).tool_calls) ? (message as any).tool_calls : [];
return toolCalls
.map((item: unknown, index: number) => {
if (!isRecord(item)) return null;
const fn = isRecord(item.function) ? item.function : {};
const id = typeof item.id === 'string' && item.id.trim().length > 0
? item.id
: `call_${index}`;
const name = typeof fn.name === 'string' ? fn.name : '';
const args = typeof fn.arguments === 'string' ? fn.arguments : '';
return {
id: ensureFunctionCallId(id),
name,
arguments: args,
};
})
.filter((item): item is ResponsesToolCall => !!item);
}
if (payload.type === 'message' && Array.isArray(payload.content)) {
return payload.content
.map((item: unknown, index: number) => {
if (!isRecord(item) || item.type !== 'tool_use') return null;
const id = typeof item.id === 'string' && item.id.trim().length > 0
? item.id
: `call_${index}`;
const name = typeof item.name === 'string' ? item.name : '';
const args = stringifyToolInput(item.input);
return {
id: ensureFunctionCallId(id),
name,
arguments: args,
};
})
.filter((item): item is ResponsesToolCall => !!item);
}
return [];
}
function extractToolCallsFromNormalized(normalized: NormalizedFinalResponse): ResponsesToolCall[] {
return Array.isArray(normalized.toolCalls)
? normalized.toolCalls
.map((item) => {
const id = ensureFunctionCallId(asTrimmedString(item.id));
const name = asTrimmedString(item.name);
if (!name) return null;
return {
id,
name,
arguments: typeof item.arguments === 'string' ? item.arguments : '',
};
})
.filter((item): item is ResponsesToolCall => !!item)
: [];
}
function extractAnnotationsFromUpstream(payload: unknown): unknown[] {
if (!isRecord(payload)) return [];
if (Array.isArray(payload.choices)) {
const choice = payload.choices[0];
const message = isRecord((choice as any)?.message) ? (choice as any).message : {};
const annotations = Array.isArray((message as any).annotations) ? (message as any).annotations : [];
return annotations.map((item) => cloneJson(item));
}
if (payload.type === 'message' && Array.isArray(payload.content)) {
const annotations = payload.content
.filter((item): item is Record<string, unknown> => isRecord(item))
.flatMap((item) => (Array.isArray(item.annotations) ? item.annotations : []));
return annotations.map((item) => cloneJson(item));
}
return [];
}
function extractSyntheticOutputItemsFromUpstream(payload: unknown): ResponsesOutputItem[] {
if (!isRecord(payload) || !Array.isArray(payload.output)) return [];
return payload.output
.map((rawItem): ResponsesOutputItem | null => {
if (!isRecord(rawItem)) return null;
const itemType = asTrimmedString(rawItem.type).toLowerCase();
if (!itemType) return null;
if (itemType === 'message') {
return {
id: asTrimmedString(rawItem.id) || ensureMessageId(''),
type: 'message',
role: asTrimmedString(rawItem.role) || 'assistant',
status: asTrimmedString(rawItem.status) || 'completed',
content: Array.isArray(rawItem.content) ? cloneJson(rawItem.content) : [],
};
}
if (itemType === 'reasoning') {
return {
id: asTrimmedString(rawItem.id) || ensureMessageId(''),
type: 'reasoning',
status: asTrimmedString(rawItem.status) || 'completed',
encrypted_content: rawItem.encrypted_content,
summary: Array.isArray(rawItem.summary) ? cloneJson(rawItem.summary) : [],
};
}
if (itemType === 'function_call') {
return {
id: asTrimmedString(rawItem.id) || ensureFunctionCallId(''),
type: 'function_call',
status: asTrimmedString(rawItem.status) || 'completed',
call_id: asTrimmedString(rawItem.call_id) || ensureFunctionCallId(asTrimmedString(rawItem.id)),
name: asTrimmedString(rawItem.name),
arguments: typeof rawItem.arguments === 'string' ? rawItem.arguments : '',
};
}
if (itemType === 'custom_tool_call') {
return {
id: asTrimmedString(rawItem.id) || ensureFunctionCallId(''),
type: 'custom_tool_call',
status: asTrimmedString(rawItem.status) || 'completed',
call_id: asTrimmedString(rawItem.call_id) || ensureFunctionCallId(asTrimmedString(rawItem.id)),
name: asTrimmedString(rawItem.name),
input: typeof rawItem.input === 'string' ? rawItem.input : '',
};
}
if (itemType === 'function_call_output' || itemType === 'custom_tool_call_output') {
return {
id: asTrimmedString(rawItem.id) || ensureMessageId(''),
type: itemType,
status: asTrimmedString(rawItem.status) || 'completed',
call_id: asTrimmedString(rawItem.call_id),
output: cloneJson(rawItem.output),
};
}
if (itemType === 'image_generation_call') {
return {
id: asTrimmedString(rawItem.id) || ensureMessageId(''),
type: 'image_generation_call',
status: asTrimmedString(rawItem.status) || 'completed',
result: cloneJson(rawItem.result),
partial_images: Array.isArray(rawItem.partial_images) ? cloneJson(rawItem.partial_images) : [],
...(rawItem.background !== undefined ? { background: cloneJson(rawItem.background) } : {}),
...(rawItem.mime_type !== undefined ? { mime_type: cloneJson(rawItem.mime_type) } : {}),
...(rawItem.output_format !== undefined ? { output_format: cloneJson(rawItem.output_format) } : {}),
...(rawItem.quality !== undefined ? { quality: cloneJson(rawItem.quality) } : {}),
...(rawItem.size !== undefined ? { size: cloneJson(rawItem.size) } : {}),
...(rawItem.revised_prompt !== undefined ? { revised_prompt: cloneJson(rawItem.revised_prompt) } : {}),
};
}
if (itemType.startsWith('mcp_')) {
return cloneJson(rawItem);
}
return null;
})
.filter((item): item is ResponsesOutputItem => item !== null);
}
function collectOutputTextFromItems(items: ResponsesOutputItem[]): string {
const textParts: string[] = [];
for (const item of items) {
if (asTrimmedString(item.type).toLowerCase() !== 'message') continue;
const content = Array.isArray(item.content) ? item.content : [];
for (const part of content) {
if (!isRecord(part)) continue;
const partType = asTrimmedString(part.type).toLowerCase();
if ((partType === 'output_text' || partType === 'text') && typeof part.text === 'string') {
textParts.push(part.text);
}
}
}
return textParts.join('');
}
function extractSyntheticUsageDetails(upstreamPayload: unknown): {
inputTokensDetails?: Record<string, unknown>;
outputTokensDetails?: Record<string, unknown>;
} {
if (!isRecord(upstreamPayload) || !isRecord(upstreamPayload.usage)) {
return {};
}
const usage = upstreamPayload.usage;
const promptDetails = (
(isRecord(usage.input_tokens_details) ? usage.input_tokens_details : null)
?? (isRecord(usage.prompt_tokens_details) ? usage.prompt_tokens_details : null)
);
const completionDetails = (
(isRecord(usage.output_tokens_details) ? usage.output_tokens_details : null)
?? (isRecord(usage.completion_tokens_details) ? usage.completion_tokens_details : null)
);
return {
...(promptDetails ? { inputTokensDetails: cloneJson(promptDetails) } : {}),
...(completionDetails ? { outputTokensDetails: cloneJson(completionDetails) } : {}),
};
}
export function normalizeResponsesFinalPayload(
payload: unknown,
modelName: string,
fallbackText = '',
): NormalizedFinalResponse {
return normalizeUpstreamFinalResponse(payload, modelName, fallbackText);
}
function buildResponsesUsagePayload(
usage: ResponsesUsageSummary,
syntheticUsageDetails: {
inputTokensDetails?: Record<string, unknown>;
outputTokensDetails?: Record<string, unknown>;
},
): Record<string, unknown> {
return {
input_tokens: usage.promptTokens,
output_tokens: usage.completionTokens,
total_tokens: usage.totalTokens,
...(usage.inputTokensDetails ?? syntheticUsageDetails.inputTokensDetails
? { input_tokens_details: cloneJson(usage.inputTokensDetails ?? syntheticUsageDetails.inputTokensDetails) }
: {}),
...(usage.outputTokensDetails ?? syntheticUsageDetails.outputTokensDetails
? { output_tokens_details: cloneJson(usage.outputTokensDetails ?? syntheticUsageDetails.outputTokensDetails) }
: {}),
};
}
export function serializeResponsesFinalPayload(input: {
upstreamPayload: unknown;
normalized: NormalizedFinalResponse;
usage: ResponsesUsageSummary;
serializationMode?: ResponsesFinalSerializationMode;
}): Record<string, unknown> {
const {
upstreamPayload,
normalized,
usage,
serializationMode = 'response',
} = input;
if (isRecord(upstreamPayload)) {
if (upstreamPayload.object === 'response.compaction') {
return upstreamPayload;
}
if (serializationMode === 'response' && upstreamPayload.object === 'response') {
return upstreamPayload;
}
}
const normalizedId = typeof normalized.id === 'string' && normalized.id.trim()
? normalized.id.trim()
: `resp_${Date.now()}`;
const responseId = ensureResponseId(normalizedId);
const messageId = ensureMessageId(normalizedId);
const syntheticOutput = extractSyntheticOutputItemsFromUpstream(upstreamPayload);
const syntheticUsageDetails = extractSyntheticUsageDetails(upstreamPayload);
const toolCalls = (() => {
const extracted = extractToolCallsFromUpstream(upstreamPayload);
if (extracted.length > 0) return extracted;
return extractToolCallsFromNormalized(normalized);
})();
const rawReasoningSignature = typeof normalized.reasoningSignature === 'string'
? normalized.reasoningSignature.trim()
: '';
const encryptedReasoning = decodeOpenAiEncryptedReasoning(rawReasoningSignature)
?? (rawReasoningSignature && !rawReasoningSignature.startsWith('metapi:') ? rawReasoningSignature : null);
const annotations = extractAnnotationsFromUpstream(upstreamPayload);
const output: Array<Record<string, unknown>> = syntheticOutput.map((item) => cloneJson(item));
const hasReasoningItem = output.some((item) => asTrimmedString(item.type).toLowerCase() === 'reasoning');
const hasMessageItem = output.some((item) => asTrimmedString(item.type).toLowerCase() === 'message');
const hasToolLikeItem = output.some((item) => {
const itemType = asTrimmedString(item.type).toLowerCase();
return (
itemType === 'function_call'
|| itemType === 'custom_tool_call'
|| itemType === 'function_call_output'
|| itemType === 'custom_tool_call_output'
|| itemType === 'image_generation_call'
);
});
if ((normalized.reasoningContent || encryptedReasoning) && !hasReasoningItem) {
const reasoningItem: Record<string, unknown> = {
id: ensureMessageId(`${normalizedId}_reasoning`),
type: 'reasoning',
status: 'completed',
summary: normalized.reasoningContent
? [{
type: 'summary_text',
text: normalized.reasoningContent,
}]
: [],
};
if (encryptedReasoning) {
reasoningItem.encrypted_content = encryptedReasoning;
}
output.push(reasoningItem);
}
if ((normalized.content || (!hasToolLikeItem && output.length === 0 && toolCalls.length === 0)) && !hasMessageItem) {
const textPart: Record<string, unknown> = {
type: 'output_text',
text: normalized.content || '',
};
if (annotations.length > 0) {
textPart.annotations = annotations;
}
output.push({
id: messageId,
type: 'message',
role: 'assistant',
status: 'completed',
content: [textPart],
});
}
if (toolCalls.length > 0 && !hasToolLikeItem) {
for (const toolCall of toolCalls) {
const mcpItem = decodeResponsesMcpCompatToolCall(toolCall.name, toolCall.arguments);
if (mcpItem) {
output.push(mcpItem);
continue;
}
output.push({
id: toolCall.id,
type: 'function_call',
status: 'completed',
call_id: toolCall.id,
name: toolCall.name,
arguments: toolCall.arguments,
});
}
}
const usagePayload = buildResponsesUsagePayload(usage, syntheticUsageDetails);
if (serializationMode === 'compact') {
return {
id: responseId,
object: 'response.compaction',
created_at: normalized.created,
output,
usage: usagePayload,
};
}
return {
id: responseId,
object: 'response',
created_at: normalized.created,
status: 'completed',
model: normalized.model,
output,
output_text: normalized.content || collectOutputTextFromItems(output),
usage: usagePayload,
};
}
export const openAiResponsesOutbound = {
normalizeFinal: normalizeResponsesFinalPayload,
serializeFinal: serializeResponsesFinalPayload,
};
export type {
ResponsesFinalSerializationMode,
ResponsesToolCall,
ResponsesUsageSummary,
} from './responseBridge.js';
export {
serializeResponsesFinalPayload as toResponsesPayload,
};
openAiResponsesOutbound,
openAiResponsesResponseBridge,
buildNormalizedFinalToOpenAiResponsesPayload as serializeResponsesFinalPayload,
buildNormalizedFinalToOpenAiResponsesPayload as toResponsesPayload,
normalizeOpenAiResponsesFinalToNormalized as normalizeResponsesFinalPayload,
} from './responseBridge.js';
@@ -1,8 +1,13 @@
import { createProxyStreamLifecycle } from '../../shared/protocolLifecycle.js';
import { type ParsedSseEvent } from '../../shared/normalized.js';
import { completeResponsesStream, createOpenAiResponsesAggregateState, failResponsesStream, serializeConvertedResponsesEvents } from './aggregator.js';
import { openAiResponsesOutbound } from './outbound.js';
import { openAiResponsesStream } from './stream.js';
import {
hasMeaningfulResponsesOutputItem,
hasMeaningfulResponsesPayloadOutput,
openAiResponsesStream,
preserveMeaningfulResponsesTerminalPayload,
serializeResponsesUpstreamFinalAsStream,
} from './streamBridge.js';
import { config } from '../../../config.js';
type StreamReader = {
@@ -45,42 +50,8 @@ function asTrimmedString(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
function hasNonEmptyString(value: unknown): boolean {
return typeof value === 'string' && value.trim().length > 0;
}
function hasMeaningfulContentPart(part: unknown): boolean {
if (!isRecord(part)) return false;
const partType = typeof part.type === 'string' ? part.type.trim().toLowerCase() : '';
if (partType === 'output_text' || partType === 'text') {
return hasNonEmptyString(part.text);
}
return partType.length > 0;
}
function hasMeaningfulOutputItem(item: unknown): boolean {
if (!isRecord(item)) return false;
const itemType = typeof item.type === 'string' ? item.type.trim().toLowerCase() : '';
if (itemType === 'message') {
return Array.isArray(item.content) && item.content.some((part) => hasMeaningfulContentPart(part));
}
if (itemType === 'reasoning') {
return (
(Array.isArray(item.summary) && item.summary.some((part) => hasMeaningfulContentPart(part)))
|| hasNonEmptyString(item.encrypted_content)
);
}
return itemType.length > 0;
}
function hasMeaningfulResponsesPayloadOutput(payload: unknown): boolean {
if (!isRecord(payload)) return false;
if (hasNonEmptyString(payload.output_text)) return true;
return Array.isArray(payload.output) && payload.output.some((item) => hasMeaningfulOutputItem(item));
}
function hasMeaningfulAggregateOutput(state: ReturnType<typeof createOpenAiResponsesAggregateState>): boolean {
return state.outputItems.some((item) => hasMeaningfulOutputItem(item));
return state.outputItems.some((item) => hasMeaningfulResponsesOutputItem(item));
}
function shouldFailEmptyResponsesCompletion(input: {
@@ -116,59 +87,6 @@ function getResponsesStreamFailureMessage(payload: unknown, fallback = 'upstream
return fallback;
}
function preserveMeaningfulTerminalResponsesPayload(
lines: string[],
eventType: 'response.completed' | 'response.incomplete',
payload: Record<string, unknown>,
): string[] {
const responsePayload = isRecord(payload.response) ? payload.response : null;
if (!responsePayload || !hasMeaningfulResponsesPayloadOutput(responsePayload)) {
return lines;
}
const parsed = openAiResponsesStream.pullSseEvents(lines.join(''));
let replaced = false;
const nextLines: string[] = [];
for (const event of parsed.events) {
if (event.data === '[DONE]') {
nextLines.push('data: [DONE]\n\n');
continue;
}
let parsedPayload: unknown = null;
try {
parsedPayload = JSON.parse(event.data);
} catch {
nextLines.push(`${event.event ? `event: ${event.event}\n` : ''}data: ${event.data}\n\n`);
continue;
}
if (
isRecord(parsedPayload)
&& asTrimmedString(parsedPayload.type) === eventType
&& isRecord(parsedPayload.response)
&& !hasMeaningfulResponsesPayloadOutput(parsedPayload.response)
) {
replaced = true;
nextLines.push(
`event: ${event.event || eventType}\ndata: ${JSON.stringify({
...parsedPayload,
response: {
...parsedPayload.response,
...responsePayload,
},
})}\n\n`,
);
continue;
}
nextLines.push(`${event.event ? `event: ${event.event}\n` : ''}data: ${event.data}\n\n`);
}
return replaced ? nextLines : lines;
}
export function createResponsesProxyStreamSession(input: ResponsesProxyStreamSessionInput) {
const streamContext = openAiResponsesStream.createContext(input.modelName);
const responsesState = createOpenAiResponsesAggregateState(input.modelName);
@@ -268,9 +186,9 @@ export function createResponsesProxyStreamSession(input: ResponsesProxyStreamSes
usage: input.getUsage(),
});
if (isIncompleteEvent) {
convertedLines = preserveMeaningfulTerminalResponsesPayload(convertedLines, 'response.incomplete', parsedPayload);
convertedLines = preserveMeaningfulResponsesTerminalPayload(convertedLines, 'response.incomplete', parsedPayload);
} else if (eventBlock.event === 'response.completed' || payloadType === 'response.completed') {
convertedLines = preserveMeaningfulTerminalResponsesPayload(convertedLines, 'response.completed', parsedPayload);
convertedLines = preserveMeaningfulResponsesTerminalPayload(convertedLines, 'response.completed', parsedPayload);
}
if (
(eventBlock.event === 'response.completed' || payloadType === 'response.completed')
@@ -314,27 +232,22 @@ export function createResponsesProxyStreamSession(input: ResponsesProxyStreamSes
const payloadType = (isRecord(payload) && typeof payload.type === 'string')
? payload.type
: '';
const payloadStatus = isRecord(payload) && typeof payload.status === 'string'
? payload.status
: '';
if (payloadType === 'error' || payloadType === 'response.failed') {
fail(payload);
response?.end();
return terminalResult;
}
const normalizedFinal = openAiResponsesOutbound.normalizeFinal(payload, input.modelName, fallbackText);
const serializedFinal = serializeResponsesUpstreamFinalAsStream({
payload,
modelName: input.modelName,
fallbackText,
usage: input.getUsage(),
});
const { normalizedFinal, streamPayload, isIncompletePayload, lines } = serializedFinal;
streamContext.id = normalizedFinal.id;
streamContext.model = normalizedFinal.model;
streamContext.created = normalizedFinal.created;
const streamPayload = openAiResponsesOutbound.serializeFinal({
upstreamPayload: payload,
normalized: normalizedFinal,
usage: input.getUsage(),
serializationMode: 'response',
});
const isIncompletePayload = payloadType === 'response.incomplete' || payloadStatus === 'incomplete';
if (!isIncompletePayload && shouldFailEmptyResponsesCompletion({
payload: { type: 'response.completed', response: streamPayload },
state: responsesState,
@@ -349,36 +262,13 @@ export function createResponsesProxyStreamSession(input: ResponsesProxyStreamSes
response?.end();
return terminalResult;
}
const createdPayload = {
...streamPayload,
status: 'in_progress',
output: [],
output_text: '',
};
if (isIncompletePayload) {
finalized = true;
terminalResult = {
status: 'completed',
errorMessage: null,
};
input.writeLines([
`event: response.created\ndata: ${JSON.stringify({ type: 'response.created', response: createdPayload })}\n\n`,
`event: response.incomplete\ndata: ${JSON.stringify({ type: 'response.incomplete', response: streamPayload })}\n\n`,
'data: [DONE]\n\n',
]);
} else {
finalized = true;
terminalResult = {
status: 'completed',
errorMessage: null,
};
input.writeLines([
`event: response.created\ndata: ${JSON.stringify({ type: 'response.created', response: createdPayload })}\n\n`,
`event: response.completed\ndata: ${JSON.stringify({ type: 'response.completed', response: streamPayload })}\n\n`,
'data: [DONE]\n\n',
]);
}
finalized = true;
terminalResult = {
status: 'completed',
errorMessage: null,
};
input.writeLines(lines);
response?.end();
return terminalResult;
},
@@ -0,0 +1,126 @@
import { describe, expect, it } from 'vitest';
import {
buildCanonicalRequestToOpenAiResponsesBody,
parseOpenAiResponsesRequestToCanonical,
} from './requestBridge.js';
describe('openai responses request bridge', () => {
it('parses OpenAI Responses bodies into canonical envelopes', () => {
const result = parseOpenAiResponsesRequestToCanonical({
model: 'gpt-5',
input: 'hello',
previous_response_id: 'resp_prev_1',
prompt_cache_key: 'cache-key',
reasoning: {
effort: 'high',
},
});
expect(result.error).toBeUndefined();
expect(result.value).toMatchObject({
operation: 'generate',
surface: 'openai-responses',
requestedModel: 'gpt-5',
continuation: {
previousResponseId: 'resp_prev_1',
promptCacheKey: 'cache-key',
},
reasoning: {
effort: 'high',
},
});
});
it('builds OpenAI Responses bodies from canonical envelopes', () => {
const body = buildCanonicalRequestToOpenAiResponsesBody({
operation: 'generate',
surface: 'openai-responses',
cliProfile: 'codex',
requestedModel: 'gpt-5',
stream: true,
messages: [{ role: 'user', parts: [{ type: 'text', text: 'hello' }] }],
continuation: {
previousResponseId: 'resp_prev_1',
promptCacheKey: 'cache-key',
},
reasoning: {
effort: 'high',
},
});
expect(body).toMatchObject({
model: 'gpt-5',
stream: true,
store: false,
previous_response_id: 'resp_prev_1',
prompt_cache_key: 'cache-key',
include: ['reasoning.encrypted_content'],
reasoning: {
effort: 'high',
},
});
});
it('builds Responses tool-turn history from canonical assistant reasoning and tool results', () => {
const body = buildCanonicalRequestToOpenAiResponsesBody({
operation: 'generate',
surface: 'openai-responses',
cliProfile: 'codex',
requestedModel: 'gpt-5',
stream: false,
messages: [
{
role: 'assistant',
parts: [
{
type: 'text',
text: 'plan quietly',
thought: true,
},
{
type: 'tool_call',
id: 'call_1',
name: 'Glob',
argumentsJson: '{"pattern":"README*"}',
},
],
},
{
role: 'tool',
parts: [{
type: 'tool_result',
toolCallId: 'call_1',
resultText: 'README.md',
}],
},
],
});
expect(body).toMatchObject({
model: 'gpt-5',
store: false,
include: ['reasoning.encrypted_content'],
input: [
{
type: 'reasoning',
summary: [{
type: 'summary_text',
text: 'plan quietly',
}],
},
{
type: 'function_call',
call_id: 'call_1',
name: 'Glob',
arguments: '{"pattern":"README*"}',
},
{
type: 'function_call_output',
call_id: 'call_1',
output: 'README.md',
},
],
});
});
});
@@ -0,0 +1,87 @@
import { canonicalRequestFromOpenAiBody, canonicalRequestToOpenAiChatBody } from '../../canonical/openAiRequestBridge.js';
import type { CanonicalRequestEnvelope } from '../../canonical/types.js';
import type { ProtocolParseContext } from '../../contracts.js';
import {
convertOpenAiBodyToResponsesBody,
convertResponsesBodyToOpenAiBody,
} from './conversion.js';
import { openAiResponsesInbound } from './inbound.js';
function normalizeIncludeList(value: unknown): string[] {
if (typeof value === 'string') {
const trimmed = value.trim();
return trimmed ? [trimmed] : [];
}
if (!Array.isArray(value)) return [];
return value
.map((item) => (typeof item === 'string' ? item.trim() : ''))
.filter((item) => item.length > 0);
}
export function parseOpenAiResponsesRequestToCanonical(
body: unknown,
ctx?: ProtocolParseContext,
): { value?: CanonicalRequestEnvelope; error?: { statusCode: number; payload: unknown } } {
const parsed = openAiResponsesInbound.parse(body, {
defaultEncryptedReasoningInclude: ctx?.defaultEncryptedReasoningInclude,
});
if (parsed.error) {
return { error: parsed.error };
}
if (!parsed.value) {
return {
error: {
statusCode: 400,
payload: {
error: {
message: 'invalid responses request',
type: 'invalid_request_error',
},
},
},
};
}
const responsesBody = parsed.value.parsed.normalizedBody;
const openAiBody = convertResponsesBodyToOpenAiBody(
responsesBody,
typeof responsesBody.model === 'string' ? responsesBody.model : parsed.value.model,
responsesBody.stream === true,
{ defaultEncryptedReasoningInclude: ctx?.defaultEncryptedReasoningInclude },
);
return {
value: canonicalRequestFromOpenAiBody({
body: openAiBody,
surface: 'openai-responses',
cliProfile: ctx?.cliProfile,
operation: ctx?.operation,
metadata: ctx?.metadata,
passthrough: ctx?.passthrough,
continuation: ctx?.continuation,
}),
};
}
export function buildCanonicalRequestToOpenAiResponsesBody(
request: CanonicalRequestEnvelope,
): Record<string, unknown> {
const openAiBody = canonicalRequestToOpenAiChatBody(request);
if (request.reasoning) {
openAiBody.reasoning = {
...(request.reasoning.effort ? { effort: request.reasoning.effort } : {}),
...(request.reasoning.budgetTokens !== undefined ? { budget_tokens: request.reasoning.budgetTokens } : {}),
...(request.reasoning.summary ? { summary: request.reasoning.summary } : {}),
};
}
const body = convertOpenAiBodyToResponsesBody(openAiBody, request.requestedModel, request.stream);
const mergedInclude = Array.from(new Set([
'reasoning.encrypted_content',
...normalizeIncludeList(body.include),
]));
return {
...body,
include: mergedInclude,
store: false,
};
}
@@ -0,0 +1,217 @@
import { describe, expect, it } from 'vitest';
import {
buildNormalizedFinalToOpenAiResponsesPayload,
normalizeOpenAiResponsesFinalToNormalized,
openAiResponsesResponseBridge,
} from './responseBridge.js';
import { openAiResponsesOutbound } from './outbound.js';
describe('openai responses response bridge', () => {
it('normalizes upstream responses finals into normalized final responses', () => {
const normalized = normalizeOpenAiResponsesFinalToNormalized({
id: 'resp_native_1',
object: 'response',
created_at: 1700000000,
model: 'gpt-5',
output: [
{
id: 'msg_1',
type: 'message',
role: 'assistant',
status: 'completed',
content: [
{
type: 'output_text',
text: 'hello',
},
],
},
],
}, 'gpt-5');
expect(normalized).toMatchObject({
id: 'resp_native_1',
model: 'gpt-5',
content: 'hello',
});
});
it('builds responses payloads from normalized final responses', () => {
const payload = buildNormalizedFinalToOpenAiResponsesPayload({
upstreamPayload: {
id: 'opaque_1',
model: 'gpt-5',
},
normalized: {
id: 'opaque_1',
model: 'gpt-5',
created: 1700000000,
content: 'hello',
reasoningContent: '',
finishReason: 'stop',
toolCalls: [],
},
usage: {
promptTokens: 11,
completionTokens: 7,
totalTokens: 18,
},
});
expect(payload).toEqual({
id: 'resp_opaque_1',
object: 'response',
created_at: 1700000000,
status: 'completed',
model: 'gpt-5',
output: [
{
id: 'msg_opaque_1',
type: 'message',
role: 'assistant',
status: 'completed',
content: [
{
type: 'output_text',
text: 'hello',
},
],
},
],
output_text: 'hello',
usage: {
input_tokens: 11,
output_tokens: 7,
total_tokens: 18,
},
});
});
it('preserves function-call chains from response-like upstream payloads instead of flattening them into text', () => {
const payload = buildNormalizedFinalToOpenAiResponsesPayload({
upstreamPayload: {
id: 'opaque_tool_chain_1',
model: 'gpt-5',
output: [
{
id: 'rs_1',
type: 'reasoning',
status: 'completed',
summary: [
{
type: 'summary_text',
text: 'plan first',
},
],
},
{
id: 'fc_1',
type: 'function_call',
status: 'completed',
call_id: 'call_1',
name: 'lookup_weather',
arguments: '{"city":"Shanghai"}',
},
{
id: 'fco_1',
type: 'function_call_output',
status: 'completed',
call_id: 'call_1',
output: [
{
type: 'output_text',
text: '22C',
},
],
},
{
id: 'msg_1',
type: 'message',
role: 'assistant',
status: 'completed',
content: [
{
type: 'output_text',
text: 'done',
},
],
},
],
},
normalized: {
id: 'opaque_tool_chain_1',
model: 'gpt-5',
created: 1700000000,
content: 'done',
reasoningContent: 'plan first',
finishReason: 'stop',
toolCalls: [{
id: 'call_1',
name: 'lookup_weather',
arguments: '{"city":"Shanghai"}',
}],
},
usage: {
promptTokens: 11,
completionTokens: 7,
totalTokens: 18,
},
});
expect(payload).toMatchObject({
id: 'resp_opaque_tool_chain_1',
object: 'response',
output_text: 'done',
output: [
{
id: 'rs_1',
type: 'reasoning',
status: 'completed',
summary: [
{
type: 'summary_text',
text: 'plan first',
},
],
},
{
id: 'fc_1',
type: 'function_call',
status: 'completed',
call_id: 'call_1',
name: 'lookup_weather',
arguments: '{"city":"Shanghai"}',
},
{
id: 'fco_1',
type: 'function_call_output',
status: 'completed',
call_id: 'call_1',
output: [
{
type: 'output_text',
text: '22C',
},
],
},
{
id: 'msg_1',
type: 'message',
role: 'assistant',
status: 'completed',
content: [
{
type: 'output_text',
text: 'done',
},
],
},
],
});
});
it('keeps the outbound facade pointed at the response bridge object', () => {
expect(openAiResponsesOutbound).toBe(openAiResponsesResponseBridge);
});
});
@@ -0,0 +1,453 @@
import {
normalizeUpstreamFinalResponse,
type NormalizedFinalResponse,
} from '../../shared/normalized.js';
import { decodeOpenAiEncryptedReasoning } from '../../shared/reasoningTransport.js';
import { decodeResponsesMcpCompatToolCall } from './mcpCompatibility.js';
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object';
}
function asTrimmedString(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
function cloneJson<T>(value: T): T {
if (Array.isArray(value)) {
return value.map((item) => cloneJson(item)) as T;
}
if (isRecord(value)) {
return Object.fromEntries(
Object.entries(value).map(([key, item]) => [key, cloneJson(item)]),
) as T;
}
return value;
}
function ensureResponseId(rawId: string): string {
const trimmed = rawId.trim() || `resp_${Date.now()}`;
return trimmed.startsWith('resp_') ? trimmed : `resp_${trimmed}`;
}
function ensureMessageId(rawId: string): string {
const trimmed = rawId.trim() || `msg_${Date.now()}`;
return trimmed.startsWith('msg_') ? trimmed : `msg_${trimmed}`;
}
function ensureFunctionCallId(rawId: string): string {
const trimmed = rawId.trim();
if (!trimmed) return `call_${Date.now()}`;
return trimmed.startsWith('call_') ? trimmed : `call_${trimmed}`;
}
function stringifyToolInput(input: unknown): string {
if (typeof input === 'string') return input;
if (!input || typeof input !== 'object') return '';
try {
return JSON.stringify(input);
} catch {
return '';
}
}
export type ResponsesUsageSummary = {
promptTokens: number;
completionTokens: number;
totalTokens: number;
inputTokensDetails?: Record<string, unknown>;
outputTokensDetails?: Record<string, unknown>;
};
export type ResponsesToolCall = {
id: string;
name: string;
arguments: string;
};
export type ResponsesFinalSerializationMode = 'response' | 'compact';
type ResponsesOutputItem = Record<string, unknown>;
function extractToolCallsFromUpstream(payload: unknown): ResponsesToolCall[] {
if (!isRecord(payload)) return [];
if (Array.isArray(payload.choices)) {
const choice = payload.choices[0];
const message = isRecord((choice as any)?.message) ? (choice as any).message : {};
const toolCalls = Array.isArray((message as any).tool_calls) ? (message as any).tool_calls : [];
return toolCalls
.map((item: unknown, index: number) => {
if (!isRecord(item)) return null;
const fn = isRecord(item.function) ? item.function : {};
const id = typeof item.id === 'string' && item.id.trim().length > 0
? item.id
: `call_${index}`;
const name = typeof fn.name === 'string' ? fn.name : '';
const args = typeof fn.arguments === 'string' ? fn.arguments : '';
return {
id: ensureFunctionCallId(id),
name,
arguments: args,
};
})
.filter((item): item is ResponsesToolCall => !!item);
}
if (payload.type === 'message' && Array.isArray(payload.content)) {
return payload.content
.map((item: unknown, index: number) => {
if (!isRecord(item) || item.type !== 'tool_use') return null;
const id = typeof item.id === 'string' && item.id.trim().length > 0
? item.id
: `call_${index}`;
const name = typeof item.name === 'string' ? item.name : '';
const args = stringifyToolInput(item.input);
return {
id: ensureFunctionCallId(id),
name,
arguments: args,
};
})
.filter((item): item is ResponsesToolCall => !!item);
}
return [];
}
function extractToolCallsFromNormalized(normalized: NormalizedFinalResponse): ResponsesToolCall[] {
return Array.isArray(normalized.toolCalls)
? normalized.toolCalls
.map((item) => {
const id = ensureFunctionCallId(asTrimmedString(item.id));
const name = asTrimmedString(item.name);
if (!name) return null;
return {
id,
name,
arguments: typeof item.arguments === 'string' ? item.arguments : '',
};
})
.filter((item): item is ResponsesToolCall => !!item)
: [];
}
function extractAnnotationsFromUpstream(payload: unknown): unknown[] {
if (!isRecord(payload)) return [];
if (Array.isArray(payload.choices)) {
const choice = payload.choices[0];
const message = isRecord((choice as any)?.message) ? (choice as any).message : {};
const annotations = Array.isArray((message as any).annotations) ? (message as any).annotations : [];
return annotations.map((item) => cloneJson(item));
}
if (payload.type === 'message' && Array.isArray(payload.content)) {
const annotations = payload.content
.filter((item): item is Record<string, unknown> => isRecord(item))
.flatMap((item) => (Array.isArray(item.annotations) ? item.annotations : []));
return annotations.map((item) => cloneJson(item));
}
return [];
}
function extractSyntheticOutputItemsFromUpstream(payload: unknown): ResponsesOutputItem[] {
if (!isRecord(payload) || !Array.isArray(payload.output)) return [];
return payload.output
.map((rawItem): ResponsesOutputItem | null => {
if (!isRecord(rawItem)) return null;
const itemType = asTrimmedString(rawItem.type).toLowerCase();
if (!itemType) return null;
if (itemType === 'message') {
return {
id: asTrimmedString(rawItem.id) || ensureMessageId(''),
type: 'message',
role: asTrimmedString(rawItem.role) || 'assistant',
status: asTrimmedString(rawItem.status) || 'completed',
content: Array.isArray(rawItem.content) ? cloneJson(rawItem.content) : [],
};
}
if (itemType === 'reasoning') {
return {
id: asTrimmedString(rawItem.id) || ensureMessageId(''),
type: 'reasoning',
status: asTrimmedString(rawItem.status) || 'completed',
encrypted_content: rawItem.encrypted_content,
summary: Array.isArray(rawItem.summary) ? cloneJson(rawItem.summary) : [],
};
}
if (itemType === 'function_call') {
return {
id: asTrimmedString(rawItem.id) || ensureFunctionCallId(''),
type: 'function_call',
status: asTrimmedString(rawItem.status) || 'completed',
call_id: asTrimmedString(rawItem.call_id) || ensureFunctionCallId(asTrimmedString(rawItem.id)),
name: asTrimmedString(rawItem.name),
arguments: typeof rawItem.arguments === 'string' ? rawItem.arguments : '',
};
}
if (itemType === 'custom_tool_call') {
return {
id: asTrimmedString(rawItem.id) || ensureFunctionCallId(''),
type: 'custom_tool_call',
status: asTrimmedString(rawItem.status) || 'completed',
call_id: asTrimmedString(rawItem.call_id) || ensureFunctionCallId(asTrimmedString(rawItem.id)),
name: asTrimmedString(rawItem.name),
input: typeof rawItem.input === 'string' ? rawItem.input : '',
};
}
if (itemType === 'function_call_output' || itemType === 'custom_tool_call_output') {
return {
id: asTrimmedString(rawItem.id) || ensureMessageId(''),
type: itemType,
status: asTrimmedString(rawItem.status) || 'completed',
call_id: asTrimmedString(rawItem.call_id),
output: cloneJson(rawItem.output),
};
}
if (itemType === 'image_generation_call') {
return {
id: asTrimmedString(rawItem.id) || ensureMessageId(''),
type: 'image_generation_call',
status: asTrimmedString(rawItem.status) || 'completed',
result: cloneJson(rawItem.result),
partial_images: Array.isArray(rawItem.partial_images) ? cloneJson(rawItem.partial_images) : [],
...(rawItem.background !== undefined ? { background: cloneJson(rawItem.background) } : {}),
...(rawItem.mime_type !== undefined ? { mime_type: cloneJson(rawItem.mime_type) } : {}),
...(rawItem.output_format !== undefined ? { output_format: cloneJson(rawItem.output_format) } : {}),
...(rawItem.quality !== undefined ? { quality: cloneJson(rawItem.quality) } : {}),
...(rawItem.size !== undefined ? { size: cloneJson(rawItem.size) } : {}),
...(rawItem.revised_prompt !== undefined ? { revised_prompt: cloneJson(rawItem.revised_prompt) } : {}),
};
}
if (itemType.startsWith('mcp_')) {
return cloneJson(rawItem);
}
return null;
})
.filter((item): item is ResponsesOutputItem => item !== null);
}
function collectOutputTextFromItems(items: ResponsesOutputItem[]): string {
const textParts: string[] = [];
for (const item of items) {
if (asTrimmedString(item.type).toLowerCase() !== 'message') continue;
const content = Array.isArray(item.content) ? item.content : [];
for (const part of content) {
if (!isRecord(part)) continue;
const partType = asTrimmedString(part.type).toLowerCase();
if ((partType === 'output_text' || partType === 'text') && typeof part.text === 'string') {
textParts.push(part.text);
}
}
}
return textParts.join('');
}
function extractSyntheticUsageDetails(upstreamPayload: unknown): {
inputTokensDetails?: Record<string, unknown>;
outputTokensDetails?: Record<string, unknown>;
} {
if (!isRecord(upstreamPayload) || !isRecord(upstreamPayload.usage)) {
return {};
}
const usage = upstreamPayload.usage;
const promptDetails = (
(isRecord(usage.input_tokens_details) ? usage.input_tokens_details : null)
?? (isRecord(usage.prompt_tokens_details) ? usage.prompt_tokens_details : null)
);
const completionDetails = (
(isRecord(usage.output_tokens_details) ? usage.output_tokens_details : null)
?? (isRecord(usage.completion_tokens_details) ? usage.completion_tokens_details : null)
);
return {
...(promptDetails ? { inputTokensDetails: cloneJson(promptDetails) } : {}),
...(completionDetails ? { outputTokensDetails: cloneJson(completionDetails) } : {}),
};
}
export function normalizeOpenAiResponsesFinalToNormalized(
payload: unknown,
modelName: string,
fallbackText = '',
): NormalizedFinalResponse {
return normalizeUpstreamFinalResponse(payload, modelName, fallbackText);
}
function buildResponsesUsagePayload(
usage: ResponsesUsageSummary,
syntheticUsageDetails: {
inputTokensDetails?: Record<string, unknown>;
outputTokensDetails?: Record<string, unknown>;
},
): Record<string, unknown> {
return {
input_tokens: usage.promptTokens,
output_tokens: usage.completionTokens,
total_tokens: usage.totalTokens,
...(usage.inputTokensDetails ?? syntheticUsageDetails.inputTokensDetails
? { input_tokens_details: cloneJson(usage.inputTokensDetails ?? syntheticUsageDetails.inputTokensDetails) }
: {}),
...(usage.outputTokensDetails ?? syntheticUsageDetails.outputTokensDetails
? { output_tokens_details: cloneJson(usage.outputTokensDetails ?? syntheticUsageDetails.outputTokensDetails) }
: {}),
};
}
export function buildNormalizedFinalToOpenAiResponsesPayload(input: {
upstreamPayload: unknown;
normalized: NormalizedFinalResponse;
usage: ResponsesUsageSummary;
serializationMode?: ResponsesFinalSerializationMode;
}): Record<string, unknown> {
const {
upstreamPayload,
normalized,
usage,
serializationMode = 'response',
} = input;
if (isRecord(upstreamPayload)) {
if (upstreamPayload.object === 'response.compaction') {
return upstreamPayload;
}
if (serializationMode === 'response' && upstreamPayload.object === 'response') {
return upstreamPayload;
}
}
const normalizedId = typeof normalized.id === 'string' && normalized.id.trim()
? normalized.id.trim()
: `resp_${Date.now()}`;
const responseId = ensureResponseId(normalizedId);
const messageId = ensureMessageId(normalizedId);
const syntheticOutput = extractSyntheticOutputItemsFromUpstream(upstreamPayload);
const syntheticUsageDetails = extractSyntheticUsageDetails(upstreamPayload);
const toolCalls = (() => {
const extracted = extractToolCallsFromUpstream(upstreamPayload);
if (extracted.length > 0) return extracted;
return extractToolCallsFromNormalized(normalized);
})();
const rawReasoningSignature = typeof normalized.reasoningSignature === 'string'
? normalized.reasoningSignature.trim()
: '';
const encryptedReasoning = decodeOpenAiEncryptedReasoning(rawReasoningSignature)
?? (rawReasoningSignature && !rawReasoningSignature.startsWith('metapi:') ? rawReasoningSignature : null);
const annotations = extractAnnotationsFromUpstream(upstreamPayload);
const output: Array<Record<string, unknown>> = syntheticOutput.map((item) => cloneJson(item));
const hasReasoningItem = output.some((item) => asTrimmedString(item.type).toLowerCase() === 'reasoning');
const hasMessageItem = output.some((item) => asTrimmedString(item.type).toLowerCase() === 'message');
const hasToolLikeItem = output.some((item) => {
const itemType = asTrimmedString(item.type).toLowerCase();
return (
itemType === 'function_call'
|| itemType === 'custom_tool_call'
|| itemType === 'function_call_output'
|| itemType === 'custom_tool_call_output'
|| itemType === 'image_generation_call'
);
});
if ((normalized.reasoningContent || encryptedReasoning) && !hasReasoningItem) {
const reasoningItem: Record<string, unknown> = {
id: ensureMessageId(`${normalizedId}_reasoning`),
type: 'reasoning',
status: 'completed',
summary: normalized.reasoningContent
? [{
type: 'summary_text',
text: normalized.reasoningContent,
}]
: [],
};
if (encryptedReasoning) {
reasoningItem.encrypted_content = encryptedReasoning;
}
output.push(reasoningItem);
}
if ((normalized.content || (!hasToolLikeItem && output.length === 0 && toolCalls.length === 0)) && !hasMessageItem) {
const textPart: Record<string, unknown> = {
type: 'output_text',
text: normalized.content || '',
};
if (annotations.length > 0) {
textPart.annotations = annotations;
}
output.push({
id: messageId,
type: 'message',
role: 'assistant',
status: 'completed',
content: [textPart],
});
}
if (toolCalls.length > 0 && !hasToolLikeItem) {
for (const toolCall of toolCalls) {
const mcpItem = decodeResponsesMcpCompatToolCall(toolCall.name, toolCall.arguments);
if (mcpItem) {
output.push(mcpItem);
continue;
}
output.push({
id: toolCall.id,
type: 'function_call',
status: 'completed',
call_id: toolCall.id,
name: toolCall.name,
arguments: toolCall.arguments,
});
}
}
const usagePayload = buildResponsesUsagePayload(usage, syntheticUsageDetails);
if (serializationMode === 'compact') {
return {
id: responseId,
object: 'response.compaction',
created_at: normalized.created,
output,
usage: usagePayload,
};
}
return {
id: responseId,
object: 'response',
created_at: normalized.created,
status: 'completed',
model: normalized.model,
output,
output_text: normalized.content || collectOutputTextFromItems(output),
usage: usagePayload,
};
}
export {
normalizeOpenAiResponsesFinalToNormalized as normalizeResponsesFinalPayload,
buildNormalizedFinalToOpenAiResponsesPayload as serializeResponsesFinalPayload,
buildNormalizedFinalToOpenAiResponsesPayload as toResponsesPayload,
};
export const openAiResponsesResponseBridge = {
normalizeFinal: normalizeOpenAiResponsesFinalToNormalized,
serializeFinal: buildNormalizedFinalToOpenAiResponsesPayload,
};
export const openAiResponsesOutbound = openAiResponsesResponseBridge;
@@ -38,6 +38,7 @@ type UpstreamResponse = Exclude<EndpointRecoverResult, null>['upstream'];
type CreateResponsesEndpointStrategyInput = {
isStream: boolean;
requiresNativeResponsesFileUrl: boolean;
sitePlatform?: string;
dispatchRequest: (
request: CompatibilityRequest,
targetUrl?: string,
@@ -51,12 +52,47 @@ export function createResponsesEndpointStrategy(input: CreateResponsesEndpointSt
endpoint: ctx.request.endpoint,
status: ctx.response.status,
rawErrText: ctx.rawErrText,
body: ctx.request.body,
})) {
const compatibilityBodies = buildResponsesCompatibilityBodies(ctx.request.body);
const normalizedSitePlatform = String(input.sitePlatform || '').trim().toLowerCase();
const compatibilityBodies = buildResponsesCompatibilityBodies(ctx.request.body, {
sitePlatform: input.sitePlatform,
});
const compatibilityHeaders = buildResponsesCompatibilityHeaderCandidates(
ctx.request.headers,
input.isStream,
{
sitePlatform: input.sitePlatform,
},
);
const alternateCompatibilityHeaders = compatibilityHeaders.slice(1);
if (
normalizedSitePlatform === 'sub2api'
&& compatibilityBodies.length === 0
&& alternateCompatibilityHeaders.length > 0
) {
for (const compatibilityHeadersCandidate of alternateCompatibilityHeaders) {
const compatibilityRequest = {
...ctx.request,
headers: compatibilityHeadersCandidate,
};
const compatibilityResponse = await input.dispatchRequest(
compatibilityRequest,
ctx.targetUrl,
);
if (compatibilityResponse.ok) {
return {
upstream: compatibilityResponse,
upstreamPath: compatibilityRequest.path,
};
}
ctx.request = compatibilityRequest;
ctx.response = compatibilityResponse;
ctx.rawErrText = await compatibilityResponse.text().catch(() => 'unknown error');
}
}
for (const compatibilityHeadersCandidate of compatibilityHeaders) {
for (const compatibilityBody of compatibilityBodies) {
@@ -1,81 +1,11 @@
import {
createStreamTransformContext,
normalizeUpstreamStreamEvent,
pullSseEventsWithDone,
type NormalizedStreamEvent,
type StreamTransformContext,
} from '../../shared/normalized.js';
import {
completeResponsesStream,
createOpenAiResponsesAggregateState,
failResponsesStream,
serializeConvertedResponsesEvents,
type OpenAiResponsesAggregateState,
} from './aggregator.js';
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object';
}
export type OpenAiResponsesStreamEvent = NormalizedStreamEvent & {
responsesEventType?: string;
responsesPayload?: Record<string, unknown>;
usagePayload?: Record<string, unknown>;
};
export const openAiResponsesStream = {
eventNames: [
'response.created',
'response.in_progress',
'response.output_item.added',
'response.output_item.done',
'response.content_part.added',
'response.content_part.done',
'response.output_text.delta',
'response.output_text.done',
'response.function_call_arguments.delta',
'response.function_call_arguments.done',
'response.custom_tool_call_input.delta',
'response.custom_tool_call_input.done',
'response.reasoning_summary_part.added',
'response.reasoning_summary_part.done',
'response.reasoning_summary_text.delta',
'response.reasoning_summary_text.done',
'response.image_generation_call.generating',
'response.image_generation_call.in_progress',
'response.image_generation_call.partial_image',
'response.image_generation_call.completed',
'response.completed',
'response.incomplete',
'response.failed',
] as const,
createContext(modelName: string): StreamTransformContext {
return createStreamTransformContext(modelName);
},
normalizeEvent(payload: unknown, context: StreamTransformContext, modelName: string): OpenAiResponsesStreamEvent {
const normalized = normalizeUpstreamStreamEvent(payload, context, modelName) as OpenAiResponsesStreamEvent;
if (isRecord(payload) && typeof payload.type === 'string' && payload.type.startsWith('response.')) {
normalized.responsesEventType = payload.type;
normalized.responsesPayload = payload;
}
if (isRecord(payload) && isRecord(payload.usage)) {
normalized.usagePayload = payload.usage;
}
return normalized;
},
pullSseEvents(buffer: string) {
return pullSseEventsWithDone(buffer);
},
};
export type ResponsesStreamState = OpenAiResponsesAggregateState;
export function createResponsesStreamState(modelName: string): ResponsesStreamState {
return createOpenAiResponsesAggregateState(modelName);
}
export type {
OpenAiResponsesStreamEvent,
ResponsesStreamState,
} from './streamBridge.js';
export {
completeResponsesStream,
createResponsesStreamState,
failResponsesStream,
openAiResponsesStream,
serializeConvertedResponsesEvents,
};
} from './streamBridge.js';
@@ -0,0 +1,420 @@
import { describe, expect, it } from 'vitest';
import {
openAiResponsesStream,
preserveMeaningfulResponsesTerminalPayload,
serializeResponsesUpstreamFinalAsStream,
} from './streamBridge.js';
function parseSseEvents(lines: string[]): Array<{ event: string | null; payload: Record<string, unknown> | '[DONE]' }> {
return lines
.flatMap((line) => line.split('\n\n').filter((block) => block.trim().length > 0))
.map((block) => {
const eventLine = block
.split('\n')
.find((line) => line.startsWith('event: '));
const dataLine = block
.split('\n')
.find((line) => line.startsWith('data: '));
if (!dataLine) return null;
if (dataLine === 'data: [DONE]') {
return {
event: eventLine ? eventLine.slice('event: '.length) : null,
payload: '[DONE]' as const,
};
}
return {
event: eventLine ? eventLine.slice('event: '.length) : null,
payload: JSON.parse(dataLine.slice('data: '.length)) as Record<string, unknown>,
};
})
.filter((item): item is { event: string | null; payload: Record<string, unknown> | '[DONE]' } => !!item);
}
describe('openai responses stream bridge', () => {
it('normalizes response stream payload metadata onto normalized events', () => {
const context = openAiResponsesStream.createContext('gpt-5');
const payload = {
type: 'response.completed',
response: {
id: 'resp_1',
status: 'completed',
},
usage: {
input_tokens: 11,
output_tokens: 7,
total_tokens: 18,
},
};
const normalized = openAiResponsesStream.normalizeEvent(payload, context, 'gpt-5');
expect(normalized.responsesEventType).toBe('response.completed');
expect(normalized.responsesPayload).toEqual(payload);
expect(normalized.usagePayload).toEqual(payload.usage);
});
it('pulls response SSE events and preserves the DONE terminator', () => {
const parsed = openAiResponsesStream.pullSseEvents([
'event: response.completed',
'data: {"type":"response.completed"}',
'',
'data: [DONE]',
'',
'',
].join('\n'));
expect(parsed.events).toEqual([
{
event: 'response.completed',
data: '{"type":"response.completed"}',
},
{
event: '',
data: '[DONE]',
},
]);
expect(parsed.rest).toBe('');
});
it('serializes upstream final payloads into response.created and response.completed SSE lines', () => {
const serialized = serializeResponsesUpstreamFinalAsStream({
payload: {
id: 'resp_fallback_1',
object: 'response',
status: 'completed',
model: 'gpt-5',
output_text: 'hello from fallback',
output: [
{
id: 'msg_fallback_1',
type: 'message',
role: 'assistant',
status: 'completed',
content: [{ type: 'output_text', text: 'hello from fallback' }],
},
],
},
modelName: 'gpt-5',
fallbackText: 'hello from fallback',
usage: {
promptTokens: 11,
completionTokens: 7,
totalTokens: 18,
},
});
expect(serialized.isIncompletePayload).toBe(false);
expect(serialized.lines.join('')).toContain('event: response.created');
expect(serialized.lines.join('')).toContain('event: response.completed');
expect(serialized.lines.join('')).toContain('"output_text":"hello from fallback"');
expect(serialized.lines.join('')).toContain('data: [DONE]');
});
it('serializes fallback final tool and reasoning output items as canonical Responses SSE events before completion', () => {
const serialized = serializeResponsesUpstreamFinalAsStream({
payload: {
id: 'resp_semantic_fallback_1',
object: 'response',
status: 'completed',
model: 'gpt-5',
output_text: 'final answer',
output: [
{
id: 'rs_1',
type: 'reasoning',
status: 'completed',
summary: [{ type: 'summary_text', text: 'plan quietly' }],
},
{
id: 'call_1',
type: 'function_call',
status: 'completed',
call_id: 'call_1',
name: 'lookup_weather',
arguments: '{"city":"Shanghai"}',
},
{
id: 'msg_1',
type: 'message',
role: 'assistant',
status: 'completed',
content: [{ type: 'output_text', text: 'final answer' }],
},
],
},
modelName: 'gpt-5',
fallbackText: 'final answer',
usage: {
promptTokens: 11,
completionTokens: 7,
totalTokens: 18,
},
});
const events = parseSseEvents(serialized.lines);
expect(events.map((entry) => entry.event)).toEqual([
'response.created',
'response.reasoning_summary_text.done',
'response.reasoning_summary_part.done',
'response.output_item.done',
'response.function_call_arguments.done',
'response.output_item.done',
'response.output_text.done',
'response.content_part.done',
'response.output_item.done',
'response.completed',
null,
]);
expect(events[4]?.payload).toMatchObject({
type: 'response.function_call_arguments.done',
call_id: 'call_1',
name: 'lookup_weather',
arguments: '{"city":"Shanghai"}',
});
expect(events[9]?.payload).toMatchObject({
type: 'response.completed',
response: {
id: 'resp_semantic_fallback_1',
output_text: 'final answer',
output: [
{
id: 'rs_1',
type: 'reasoning',
},
{
id: 'call_1',
type: 'function_call',
call_id: 'call_1',
arguments: '{"city":"Shanghai"}',
},
{
id: 'msg_1',
type: 'message',
},
],
},
});
expect(events[10]?.payload).toBe('[DONE]');
});
it('preserves meaningful terminal response payload output when the synthesized terminal event is empty', () => {
const preserved = preserveMeaningfulResponsesTerminalPayload(
[
'event: response.completed\n',
`data: ${JSON.stringify({
type: 'response.completed',
response: {
id: 'resp_empty',
status: 'completed',
output: [],
output_text: '',
},
})}\n\n`,
'data: [DONE]\n\n',
],
'response.completed',
{
type: 'response.completed',
response: {
id: 'resp_full',
status: 'completed',
output: [
{
id: 'msg_full',
type: 'message',
role: 'assistant',
status: 'completed',
content: [{ type: 'output_text', text: 'restored text' }],
},
],
output_text: 'restored text',
},
},
);
expect(preserved.join('')).toContain('"output_text":"restored text"');
expect(preserved.join('')).toContain('data: [DONE]');
});
it('keeps response tool-call chains intact in completed SSE fallback payloads', () => {
const serialized = serializeResponsesUpstreamFinalAsStream({
payload: {
id: 'resp_tool_stream_1',
object: 'response',
status: 'completed',
model: 'gpt-5',
output_text: 'done',
output: [
{
id: 'fc_1',
type: 'function_call',
status: 'completed',
call_id: 'call_1',
name: 'lookup_weather',
arguments: '{"city":"Shanghai"}',
},
{
id: 'fco_1',
type: 'function_call_output',
status: 'completed',
call_id: 'call_1',
output: '22C',
},
{
id: 'msg_1',
type: 'message',
role: 'assistant',
status: 'completed',
content: [{ type: 'output_text', text: 'done' }],
},
],
},
modelName: 'gpt-5',
fallbackText: 'done',
usage: {
promptTokens: 11,
completionTokens: 7,
totalTokens: 18,
},
});
expect(serialized.lines.join('')).toContain('event: response.completed');
expect(serialized.lines.join('')).toContain('"type":"function_call"');
expect(serialized.lines.join('')).toContain('"type":"function_call_output"');
expect(serialized.lines.join('')).toContain('"call_id":"call_1"');
expect(serialized.lines.join('')).toContain('"output_text":"done"');
});
it('keeps incomplete fallback item statuses aligned with the terminal response status when item statuses are omitted', () => {
const serialized = serializeResponsesUpstreamFinalAsStream({
payload: {
id: 'resp_incomplete_1',
object: 'response',
status: 'incomplete',
model: 'gpt-5',
output_text: 'partial',
output: [
{
id: 'msg_partial_1',
type: 'message',
role: 'assistant',
content: [{ type: 'output_text', text: 'partial' }],
},
],
},
modelName: 'gpt-5',
fallbackText: 'partial',
usage: {
promptTokens: 11,
completionTokens: 7,
totalTokens: 18,
},
});
const events = parseSseEvents(serialized.lines);
expect(events.map((entry) => entry.event)).toEqual([
'response.created',
'response.output_text.done',
'response.content_part.done',
'response.output_item.done',
'response.incomplete',
null,
]);
expect(events[3]?.payload).toMatchObject({
type: 'response.output_item.done',
item: {
id: 'msg_partial_1',
type: 'message',
status: 'incomplete',
},
});
expect(events[4]?.payload).toMatchObject({
type: 'response.incomplete',
response: {
id: 'resp_incomplete_1',
status: 'incomplete',
output: [
{
id: 'msg_partial_1',
type: 'message',
status: 'incomplete',
},
],
},
});
});
it('serializes failed fallback wrappers as response.failed and preserves the upstream error payload', () => {
const serialized = serializeResponsesUpstreamFinalAsStream({
payload: {
type: 'response.failed',
error: {
message: 'upstream exploded',
type: 'upstream_error',
},
response: {
id: 'resp_failed_1',
status: 'failed',
model: 'gpt-5',
output_text: 'partial',
output: [
{
id: 'msg_failed_1',
type: 'message',
role: 'assistant',
content: [{ type: 'output_text', text: 'partial' }],
},
],
},
},
modelName: 'gpt-5',
fallbackText: 'partial',
usage: {
promptTokens: 11,
completionTokens: 7,
totalTokens: 18,
},
});
const events = parseSseEvents(serialized.lines);
expect(events.map((entry) => entry.event)).toEqual([
'response.created',
'response.output_text.done',
'response.content_part.done',
'response.output_item.done',
'response.failed',
null,
]);
expect(events[3]?.payload).toMatchObject({
type: 'response.output_item.done',
item: {
id: 'msg_failed_1',
type: 'message',
status: 'failed',
},
});
expect(events[4]?.payload).toMatchObject({
type: 'response.failed',
error: {
message: 'upstream exploded',
type: 'upstream_error',
},
response: {
id: 'resp_failed_1',
status: 'failed',
output: [
{
id: 'msg_failed_1',
type: 'message',
status: 'failed',
},
],
},
});
});
});
@@ -0,0 +1,305 @@
import {
createStreamTransformContext,
normalizeUpstreamStreamEvent,
pullSseEventsWithDone,
type NormalizedStreamEvent,
type StreamTransformContext,
} from '../../shared/normalized.js';
import {
completeResponsesStream,
createOpenAiResponsesAggregateState,
failResponsesStream,
serializeConvertedResponsesEvents,
type OpenAiResponsesAggregateState,
} from './aggregator.js';
import {
buildNormalizedFinalToOpenAiResponsesPayload,
normalizeOpenAiResponsesFinalToNormalized,
type ResponsesUsageSummary,
} from './responseBridge.js';
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object';
}
function asTrimmedString(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
function cloneJson<T>(value: T): T {
if (Array.isArray(value)) {
return value.map((item) => cloneJson(item)) as T;
}
if (isRecord(value)) {
return Object.fromEntries(
Object.entries(value).map(([key, item]) => [key, cloneJson(item)]),
) as T;
}
return value;
}
function hasNonEmptyString(value: unknown): boolean {
return typeof value === 'string' && value.trim().length > 0;
}
function hasMeaningfulContentPart(part: unknown): boolean {
if (!isRecord(part)) return false;
const partType = asTrimmedString(part.type).toLowerCase();
if (partType === 'output_text' || partType === 'text') {
return hasNonEmptyString(part.text);
}
return partType.length > 0;
}
export function hasMeaningfulResponsesOutputItem(item: unknown): boolean {
if (!isRecord(item)) return false;
const itemType = asTrimmedString(item.type).toLowerCase();
if (itemType === 'message') {
return Array.isArray(item.content) && item.content.some((part) => hasMeaningfulContentPart(part));
}
if (itemType === 'reasoning') {
return (
(Array.isArray(item.summary) && item.summary.some((part) => hasMeaningfulContentPart(part)))
|| hasNonEmptyString(item.encrypted_content)
);
}
return itemType.length > 0;
}
export function hasMeaningfulResponsesPayloadOutput(payload: unknown): boolean {
if (!isRecord(payload)) return false;
if (hasNonEmptyString(payload.output_text)) return true;
return Array.isArray(payload.output) && payload.output.some((item) => hasMeaningfulResponsesOutputItem(item));
}
export function preserveMeaningfulResponsesTerminalPayload(
lines: string[],
eventType: 'response.completed' | 'response.incomplete',
payload: Record<string, unknown>,
): string[] {
const responsePayload = isRecord(payload.response) ? payload.response : null;
if (!responsePayload || !hasMeaningfulResponsesPayloadOutput(responsePayload)) {
return lines;
}
const parsed = openAiResponsesStream.pullSseEvents(lines.join(''));
let replaced = false;
const nextLines: string[] = [];
for (const event of parsed.events) {
if (event.data === '[DONE]') {
nextLines.push('data: [DONE]\n\n');
continue;
}
let parsedPayload: unknown = null;
try {
parsedPayload = JSON.parse(event.data);
} catch {
nextLines.push(`${event.event ? `event: ${event.event}\n` : ''}data: ${event.data}\n\n`);
continue;
}
if (
isRecord(parsedPayload)
&& asTrimmedString(parsedPayload.type) === eventType
&& isRecord(parsedPayload.response)
&& !hasMeaningfulResponsesPayloadOutput(parsedPayload.response)
) {
replaced = true;
nextLines.push(
`event: ${event.event || eventType}\ndata: ${JSON.stringify({
...parsedPayload,
response: {
...parsedPayload.response,
...responsePayload,
},
})}\n\n`,
);
continue;
}
nextLines.push(`${event.event ? `event: ${event.event}\n` : ''}data: ${event.data}\n\n`);
}
return replaced ? nextLines : lines;
}
export function serializeResponsesUpstreamFinalAsStream(input: {
payload: unknown;
modelName: string;
fallbackText: string;
usage: ResponsesUsageSummary;
}): {
normalizedFinal: ReturnType<typeof normalizeOpenAiResponsesFinalToNormalized>;
streamPayload: Record<string, unknown>;
createdPayload: Record<string, unknown>;
isIncompletePayload: boolean;
lines: string[];
} {
const { payload, modelName, fallbackText, usage } = input;
const normalizedFinal = normalizeOpenAiResponsesFinalToNormalized(payload, modelName, fallbackText);
const streamPayload = buildNormalizedFinalToOpenAiResponsesPayload({
upstreamPayload: payload,
normalized: normalizedFinal,
usage,
serializationMode: 'response',
});
const payloadType = (isRecord(payload) && typeof payload.type === 'string')
? payload.type
: '';
const payloadStatus = isRecord(payload) && typeof payload.status === 'string'
? payload.status
: '';
const isFailedPayload = payloadType === 'response.failed' || payloadStatus === 'failed';
const isIncompletePayload = payloadType === 'response.incomplete' || payloadStatus === 'incomplete';
const terminalStatus = isFailedPayload ? 'failed' : (isIncompletePayload ? 'incomplete' : 'completed');
const upstreamTerminalResponse = isRecord(payload) && isRecord(payload.response)
? cloneJson(payload.response)
: null;
const terminalResponsePayload = upstreamTerminalResponse
? {
...streamPayload,
...upstreamTerminalResponse,
...(Array.isArray(upstreamTerminalResponse.output) ? { output: cloneJson(upstreamTerminalResponse.output) } : {}),
...(typeof upstreamTerminalResponse.output_text === 'string'
? { output_text: upstreamTerminalResponse.output_text }
: {}),
status: terminalStatus,
}
: {
...streamPayload,
status: terminalStatus,
};
const createdPayload = {
...terminalResponsePayload,
status: 'in_progress',
output: [],
output_text: '',
};
const terminalEventType = isFailedPayload
? 'response.failed'
: (isIncompletePayload ? 'response.incomplete' : 'response.completed');
const fallbackState = createOpenAiResponsesAggregateState(normalizedFinal.model || modelName);
const fallbackContext = createStreamTransformContext(normalizedFinal.model || modelName);
fallbackContext.id = normalizedFinal.id;
fallbackContext.model = normalizedFinal.model;
fallbackContext.created = normalizedFinal.created;
const lines = [
`event: response.created\ndata: ${JSON.stringify({ type: 'response.created', response: createdPayload })}\n\n`,
];
const outputItems = Array.isArray(terminalResponsePayload.output) ? terminalResponsePayload.output : [];
for (let outputIndex = 0; outputIndex < outputItems.length; outputIndex += 1) {
const item = outputItems[outputIndex];
if (!isRecord(item)) continue;
const eventItem = cloneJson(item);
const itemStatus = asTrimmedString(eventItem.status).toLowerCase();
if ((!itemStatus || itemStatus === 'in_progress') && terminalStatus !== 'completed') {
eventItem.status = terminalStatus;
}
lines.push(...serializeConvertedResponsesEvents({
state: fallbackState,
streamContext: fallbackContext,
usage,
event: {
responsesEventType: 'response.output_item.done',
responsesPayload: {
type: 'response.output_item.done',
output_index: outputIndex,
item: eventItem,
},
},
}));
}
lines.push(...serializeConvertedResponsesEvents({
state: fallbackState,
streamContext: fallbackContext,
usage,
event: {
responsesEventType: terminalEventType,
responsesPayload: {
type: terminalEventType,
response: terminalResponsePayload,
...(terminalEventType === 'response.failed' && isRecord(payload) && payload.error !== undefined
? { error: cloneJson(payload.error) }
: {}),
},
},
}));
lines.push('data: [DONE]\n\n');
return {
normalizedFinal,
streamPayload,
createdPayload,
isIncompletePayload,
lines,
};
}
export type OpenAiResponsesStreamEvent = NormalizedStreamEvent & {
responsesEventType?: string;
responsesPayload?: Record<string, unknown>;
usagePayload?: Record<string, unknown>;
};
export const openAiResponsesStream = {
eventNames: [
'response.created',
'response.in_progress',
'response.output_item.added',
'response.output_item.done',
'response.content_part.added',
'response.content_part.done',
'response.output_text.delta',
'response.output_text.done',
'response.function_call_arguments.delta',
'response.function_call_arguments.done',
'response.custom_tool_call_input.delta',
'response.custom_tool_call_input.done',
'response.reasoning_summary_part.added',
'response.reasoning_summary_part.done',
'response.reasoning_summary_text.delta',
'response.reasoning_summary_text.done',
'response.image_generation_call.generating',
'response.image_generation_call.in_progress',
'response.image_generation_call.partial_image',
'response.image_generation_call.completed',
'response.completed',
'response.incomplete',
'response.failed',
] as const,
createContext(modelName: string): StreamTransformContext {
return createStreamTransformContext(modelName);
},
normalizeEvent(payload: unknown, context: StreamTransformContext, modelName: string): OpenAiResponsesStreamEvent {
const normalized = normalizeUpstreamStreamEvent(payload, context, modelName) as OpenAiResponsesStreamEvent;
if (isRecord(payload) && typeof payload.type === 'string' && payload.type.startsWith('response.')) {
normalized.responsesEventType = payload.type;
normalized.responsesPayload = payload;
}
if (isRecord(payload) && isRecord(payload.usage)) {
normalized.usagePayload = payload.usage;
}
return normalized;
},
pullSseEvents(buffer: string) {
return pullSseEventsWithDone(buffer);
},
};
export type ResponsesStreamState = OpenAiResponsesAggregateState;
export function createResponsesStreamState(modelName: string): ResponsesStreamState {
return createOpenAiResponsesAggregateState(modelName);
}
export {
completeResponsesStream,
failResponsesStream,
serializeConvertedResponsesEvents,
};