feat: Add normalized types and utility functions for handling chat formats

- Introduced `NormalizedContentBlock`, `NormalizedUsage`, and related types for structured data handling.
- Implemented utility functions for creating and merging normalized usage statistics.
- Added a new module for shared transformers to manage chat formats.

feat: Implement app local state management

- Created functions to clear application installation state and manage local storage keys.
- Added constants for theme and user profile storage keys.

test: Add tests for BrandIcon component rendering

- Implemented tests to ensure BrandIcon does not force a colored background for recognized brands.

test: Enhance ModelAnalysisPanel tests

- Added tests for rendering token summaries with compact units in ModelAnalysisPanel.

test: Add tests for ModernSelect component

- Implemented tests to verify icon rendering for selected options in ModernSelect.

test: Add number formatting tests

- Created tests for `formatCompactTokenMetric` to validate token formatting logic.

feat: Implement account creation intent handling

- Added logic to handle session and API key creation intents in the Accounts page.

test: Add tests for dashboard performance card

- Implemented tests to verify rendering of performance metrics in the dashboard.

feat: Implement default connection segment resolution

- Added logic to determine initial connection segments based on platform types.

test: Add tests for settings factory reset functionality

- Implemented tests to verify the factory reset process and local state clearing.

test: Add tests for site creation redirect logic

- Implemented tests to verify redirection based on site capabilities during creation.

test: Add tests for token routes refresh decision action

- Implemented tests to verify refresh decision actions in TokenRoutes.

test: Add tests for token routes snapshot load

- Implemented tests to verify loading of cached snapshot data in TokenRoutes.
This commit is contained in:
cita
2026-03-08 12:08:57 +08:00
parent ed05b37626
commit 23922a5965
115 changed files with 18597 additions and 4411 deletions
@@ -0,0 +1,121 @@
import { type NormalizedFinalResponse, type NormalizedStreamEvent } from '../../shared/normalized.js';
import type {
OpenAiChatNormalizedFinalResponse,
OpenAiChatNormalizedStreamEvent,
OpenAiChatUsageDetails,
} from './model.js';
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object';
}
function pushUniqueAnnotation(
annotations: Array<Record<string, unknown>>,
seenUrls: Set<string>,
candidate: unknown,
) {
if (!isRecord(candidate)) return;
const citation = isRecord(candidate.url_citation) ? candidate.url_citation : null;
const url = citation && typeof citation.url === 'string' ? citation.url.trim() : '';
if (!url || seenUrls.has(url)) return;
seenUrls.add(url);
annotations.push(candidate);
}
function mergeUsageDetails(
target: OpenAiChatUsageDetails | undefined,
next: OpenAiChatUsageDetails | undefined,
): OpenAiChatUsageDetails | undefined {
if (!target && !next) return undefined;
return {
...(target || {}),
...(next || {}),
};
}
export type OpenAiChatAggregateState = {
content: string[];
reasoning: string[];
toolCalls: Array<{ id: string; name: string; arguments: string }>;
finishReason: string | null;
annotations: Array<Record<string, unknown>>;
annotationUrls: Set<string>;
citations: Set<string>;
usageDetails?: OpenAiChatUsageDetails;
};
export function createOpenAiChatAggregateState(): OpenAiChatAggregateState {
return {
content: [],
reasoning: [],
toolCalls: [],
finishReason: null,
annotations: [],
annotationUrls: new Set<string>(),
citations: new Set<string>(),
usageDetails: undefined,
};
}
export function applyOpenAiChatStreamEvent(
state: OpenAiChatAggregateState,
event: OpenAiChatNormalizedStreamEvent,
): OpenAiChatAggregateState {
if (event.contentDelta) state.content.push(event.contentDelta);
if (event.reasoningDelta) state.reasoning.push(event.reasoningDelta);
if (event.finishReason !== undefined) state.finishReason = event.finishReason ?? null;
if (Array.isArray(event.toolCallDeltas)) {
for (const delta of event.toolCallDeltas) {
if (!delta.id && !delta.name && !delta.argumentsDelta) continue;
const index = Number.isFinite(delta.index) ? Math.max(0, Math.trunc(delta.index)) : state.toolCalls.length;
while (state.toolCalls.length <= index) {
state.toolCalls.push({ id: '', name: '', arguments: '' });
}
const existing = state.toolCalls[index];
if (delta.id) existing.id = delta.id;
if (delta.name) existing.name = delta.name;
if (delta.argumentsDelta) existing.arguments += delta.argumentsDelta;
}
}
if (Array.isArray(event.annotations)) {
for (const annotation of event.annotations) {
pushUniqueAnnotation(state.annotations, state.annotationUrls, annotation);
}
}
if (Array.isArray(event.citations)) {
for (const citation of event.citations) {
if (typeof citation === 'string' && citation.trim()) {
state.citations.add(citation.trim());
}
}
}
state.usageDetails = mergeUsageDetails(state.usageDetails, event.usageDetails);
return state;
}
export function finalizeOpenAiChatAggregate(
state: OpenAiChatAggregateState,
normalized: OpenAiChatNormalizedFinalResponse,
): OpenAiChatNormalizedFinalResponse {
const mergedToolCalls = state.toolCalls.filter((item) => item.id || item.name || item.arguments);
if (
state.content.length === 0
&& state.reasoning.length === 0
&& mergedToolCalls.length === 0
&& state.annotations.length === 0
&& state.citations.size === 0
&& !state.usageDetails
) {
return normalized;
}
return {
...normalized,
content: state.content.join('') || normalized.content,
reasoningContent: state.reasoning.join('') || normalized.reasoningContent,
finishReason: state.finishReason ?? normalized.finishReason,
toolCalls: mergedToolCalls.length > 0 ? mergedToolCalls : normalized.toolCalls,
annotations: state.annotations.length > 0 ? state.annotations : normalized.annotations,
citations: state.citations.size > 0 ? Array.from(state.citations).sort() : normalized.citations,
usageDetails: mergeUsageDetails(normalized.usageDetails, state.usageDetails),
};
}
@@ -0,0 +1,238 @@
import type {
OpenAiChatNormalizedFinalResponse,
OpenAiChatNormalizedStreamEvent,
OpenAiChatRequestMetadata,
OpenAiChatUsageDetails,
} from './model.js';
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object' && !Array.isArray(value);
}
function asRecord(value: unknown): Record<string, unknown> | null {
return isRecord(value) ? value : null;
}
function toStringArray(value: unknown): string[] {
if (!Array.isArray(value)) return [];
return value
.map((item) => {
if (typeof item === 'string') return item.trim();
if (isRecord(item)) {
const direct = typeof item.url === 'string'
? item.url
: typeof item.uri === 'string'
? item.uri
: '';
return direct.trim();
}
return '';
})
.filter((item) => item.length > 0);
}
function toNumericRecord(value: unknown): Record<string, number> | undefined {
if (!isRecord(value)) return undefined;
const entries = Object.entries(value)
.map(([key, raw]) => {
const numeric = typeof raw === 'number' ? raw : Number(raw);
if (!Number.isFinite(numeric)) return null;
return [key, numeric] as const;
})
.filter((entry): entry is readonly [string, number] => !!entry);
if (entries.length <= 0) return undefined;
return Object.fromEntries(entries);
}
function stableKey(value: unknown): string {
if (Array.isArray(value)) {
return `[${value.map((item) => stableKey(item)).join(',')}]`;
}
if (isRecord(value)) {
const entries = Object.entries(value)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, nested]) => `${JSON.stringify(key)}:${stableKey(nested)}`);
return `{${entries.join(',')}}`;
}
return JSON.stringify(value);
}
function annotationUrl(annotation: Record<string, unknown>): string {
const urlCitation = asRecord(annotation.url_citation) ?? asRecord(annotation.urlCitation);
const direct = typeof annotation.url === 'string' ? annotation.url : '';
const nested = urlCitation && typeof urlCitation.url === 'string' ? urlCitation.url : '';
return (direct || nested).trim();
}
export function dedupeAnnotations(input: unknown): Array<Record<string, unknown>> {
if (!Array.isArray(input)) return [];
const seen = new Set<string>();
const result: Array<Record<string, unknown>> = [];
for (const item of input) {
if (!isRecord(item)) continue;
const key = annotationUrl(item) || stableKey(item);
if (!key || seen.has(key)) continue;
seen.add(key);
result.push(item);
}
return result;
}
export function dedupeCitations(input: unknown): string[] {
const citations = new Set<string>();
for (const citation of toStringArray(input)) {
citations.add(citation);
}
if (Array.isArray(input)) {
for (const item of input) {
if (!isRecord(item)) continue;
const nestedUrl = annotationUrl(item);
if (nestedUrl) citations.add(nestedUrl);
}
}
return Array.from(citations);
}
function extractAnnotationsFromPayload(payload: Record<string, unknown>): Array<Record<string, unknown>> {
const collected: Array<Record<string, unknown>> = [];
const append = (value: unknown) => {
for (const item of dedupeAnnotations(value)) {
collected.push(item);
}
};
append(payload.annotations);
const choices = Array.isArray(payload.choices) ? payload.choices : [];
for (const choice of choices) {
const choiceRecord = asRecord(choice);
if (!choiceRecord) continue;
append(asRecord(choiceRecord.message)?.annotations);
append(asRecord(choiceRecord.delta)?.annotations);
}
return dedupeAnnotations(collected);
}
function extractCitationsFromPayload(
payload: Record<string, unknown>,
annotations: Array<Record<string, unknown>>,
): string[] {
const citations = new Set<string>();
for (const citation of dedupeCitations(payload.citations)) {
citations.add(citation);
}
const transformerMetadata = asRecord(payload.transformer_metadata) ?? asRecord(payload.transformerMetadata);
if (transformerMetadata) {
for (const citation of dedupeCitations(transformerMetadata.citations)) {
citations.add(citation);
}
}
for (const annotation of annotations) {
const url = annotationUrl(annotation);
if (url) citations.add(url);
}
return Array.from(citations);
}
export function extractChatUsageDetails(payload: Record<string, unknown>): OpenAiChatUsageDetails | undefined {
const usage = asRecord(payload.usage);
if (!usage) return undefined;
const promptDetails = toNumericRecord(usage.prompt_tokens_details ?? usage.promptTokensDetails);
const completionDetails = toNumericRecord(usage.completion_tokens_details ?? usage.completionTokensDetails);
if (!promptDetails && !completionDetails) return undefined;
return {
...(promptDetails ? { prompt_tokens_details: promptDetails } : {}),
...(completionDetails ? { completion_tokens_details: completionDetails } : {}),
};
}
export function mergeChatUsageDetails(
base: OpenAiChatUsageDetails | undefined,
next: OpenAiChatUsageDetails | undefined,
): OpenAiChatUsageDetails | undefined {
if (!base && !next) return undefined;
const mergeNumericMaps = (
left: Record<string, number> | undefined,
right: Record<string, number> | undefined,
): Record<string, number> | undefined => {
if (!left && !right) return undefined;
return Object.fromEntries(
Array.from(new Set([...Object.keys(left || {}), ...Object.keys(right || {})]))
.map((key) => [key, Math.max(left?.[key] || 0, right?.[key] || 0)]),
);
};
return {
...(mergeNumericMaps(base?.prompt_tokens_details, next?.prompt_tokens_details)
? { prompt_tokens_details: mergeNumericMaps(base?.prompt_tokens_details, next?.prompt_tokens_details)! }
: {}),
...(mergeNumericMaps(base?.completion_tokens_details, next?.completion_tokens_details)
? { completion_tokens_details: mergeNumericMaps(base?.completion_tokens_details, next?.completion_tokens_details)! }
: {}),
};
}
export function extractUsagePayload(payload: Record<string, unknown>): Record<string, unknown> | undefined {
const usage = asRecord(payload.usage);
if (!usage) return undefined;
return usage;
}
export function extractChatResponseExtras(payload: unknown): Pick<
OpenAiChatNormalizedFinalResponse,
'annotations' | 'citations' | 'usageDetails' | 'usagePayload'
> & Pick<OpenAiChatNormalizedStreamEvent, 'annotations' | 'citations' | 'usageDetails' | 'usagePayload'> {
const record = asRecord(payload);
if (!record) return {};
const annotations = extractAnnotationsFromPayload(record);
const citations = extractCitationsFromPayload(record, annotations);
const usageDetails = extractChatUsageDetails(record);
const usagePayload = extractUsagePayload(record);
return {
...(annotations.length > 0 ? { annotations } : {}),
...(citations.length > 0 ? { citations } : {}),
...(usageDetails ? { usageDetails } : {}),
...(usagePayload ? { usagePayload } : {}),
};
}
export function extractChatRequestMetadata(body: unknown): OpenAiChatRequestMetadata | undefined {
const raw = asRecord(body);
if (!raw) return undefined;
const streamOptions = asRecord(raw.stream_options) ?? asRecord(raw.streamOptions);
const metadata: OpenAiChatRequestMetadata = {
...(raw.modalities !== undefined ? { modalities: raw.modalities } : {}),
...(raw.audio !== undefined ? { audio: raw.audio } : {}),
...(raw.reasoning_effort !== undefined ? { reasoningEffort: raw.reasoning_effort } : {}),
...(raw.reasoning_budget !== undefined ? { reasoningBudget: raw.reasoning_budget } : {}),
...(raw.reasoning_summary !== undefined ? { reasoningSummary: raw.reasoning_summary } : {}),
...(raw.service_tier !== undefined ? { serviceTier: raw.service_tier } : {}),
...(raw.top_logprobs !== undefined ? { topLogprobs: raw.top_logprobs } : {}),
...(raw.logit_bias !== undefined ? { logitBias: raw.logit_bias } : {}),
...(raw.prompt_cache_key !== undefined ? { promptCacheKey: raw.prompt_cache_key } : {}),
...(raw.safety_identifier !== undefined ? { safetyIdentifier: raw.safety_identifier } : {}),
...(raw.verbosity !== undefined ? { verbosity: raw.verbosity } : {}),
...(raw.response_format !== undefined ? { responseFormat: raw.response_format } : {}),
...(streamOptions && streamOptions.include_usage !== undefined
? { streamOptionsIncludeUsage: Boolean(streamOptions.include_usage) }
: {}),
};
return Object.keys(metadata).length > 0 ? metadata : undefined;
}
@@ -0,0 +1,19 @@
import { parseDownstreamChatRequest } from '../../shared/normalized.js';
import { extractChatRequestMetadata } from './helpers.js';
import type { OpenAiChatParsedRequest } from './model.js';
export const openAiChatInbound = {
parse(body: unknown): { value?: OpenAiChatParsedRequest; error?: { statusCode: number; payload: unknown } } {
const parsed = parseDownstreamChatRequest(body, 'openai') as {
value?: OpenAiChatParsedRequest;
error?: { statusCode: number; payload: unknown };
};
if (!parsed.value) return parsed;
parsed.value = {
...parsed.value,
requestMetadata: extractChatRequestMetadata(body),
};
return parsed;
},
};
@@ -0,0 +1,234 @@
import { describe, expect, it } from 'vitest';
import { createClaudeDownstreamContext } from '../../shared/normalized.js';
import { openAiChatTransformer } from './index.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('openAiChatTransformer.inbound', () => {
it('captures chat request metadata fields without changing upstream body', () => {
const result = openAiChatTransformer.transformRequest({
model: 'gpt-5',
messages: [{ role: 'user', content: 'hello' }],
modalities: ['text', 'audio'],
audio: { voice: 'alloy', format: 'mp3' },
reasoning_effort: 'high',
reasoning_budget: 1024,
reasoning_summary: 'detailed',
service_tier: 'priority',
top_logprobs: 3,
logit_bias: { '42': 5 },
prompt_cache_key: 'cache-key',
safety_identifier: 'safety-id',
verbosity: 'low',
response_format: { type: 'json_object' },
stream_options: { include_usage: true },
});
expect(result.error).toBeUndefined();
expect(result.value?.upstreamBody).toMatchObject({
modalities: ['text', 'audio'],
audio: { voice: 'alloy', format: 'mp3' },
reasoning_effort: 'high',
reasoning_budget: 1024,
reasoning_summary: 'detailed',
service_tier: 'priority',
top_logprobs: 3,
logit_bias: { '42': 5 },
prompt_cache_key: 'cache-key',
safety_identifier: 'safety-id',
verbosity: 'low',
response_format: { type: 'json_object' },
stream_options: { include_usage: true },
});
expect((result.value as any)?.requestMetadata).toEqual({
modalities: ['text', 'audio'],
audio: { voice: 'alloy', format: 'mp3' },
reasoningEffort: 'high',
reasoningBudget: 1024,
reasoningSummary: 'detailed',
serviceTier: 'priority',
topLogprobs: 3,
logitBias: { '42': 5 },
promptCacheKey: 'cache-key',
safetyIdentifier: 'safety-id',
verbosity: 'low',
responseFormat: { type: 'json_object' },
streamOptionsIncludeUsage: true,
});
});
});
describe('openAiChatTransformer.outbound', () => {
it('carries annotations, citations, and detailed usage through final serialization', () => {
const normalized = openAiChatTransformer.transformFinalResponse({
id: 'chatcmpl-1',
model: 'gpt-5',
created: 123,
choices: [{
index: 0,
finish_reason: 'stop',
message: {
role: 'assistant',
content: 'hello',
reasoning_content: 'think',
annotations: [
{ type: 'url_citation', url_citation: { url: 'https://a.example' } },
{ type: 'url_citation', url_citation: { url: 'https://a.example' } },
],
},
}],
citations: ['https://c.example', 'https://c.example'],
usage: {
prompt_tokens: 11,
completion_tokens: 7,
total_tokens: 18,
prompt_tokens_details: { cached_tokens: 3 },
completion_tokens_details: { reasoning_tokens: 2 },
},
}, 'gpt-5');
const payload = openAiChatTransformer.serializeFinalResponse(normalized, {
promptTokens: 11,
completionTokens: 7,
totalTokens: 18,
});
const choice = (payload as any).choices[0];
expect(choice.message.annotations).toEqual([
{ type: 'url_citation', url_citation: { url: 'https://a.example' } },
]);
expect((payload as any).citations).toEqual(['https://c.example', 'https://a.example']);
expect((payload as any).usage).toMatchObject({
prompt_tokens: 11,
completion_tokens: 7,
total_tokens: 18,
prompt_tokens_details: { cached_tokens: 3 },
completion_tokens_details: { reasoning_tokens: 2 },
});
});
});
describe('openAiChatTransformer.stream', () => {
it('preserves annotations, citations, and usage payload on serialized stream chunks', () => {
const context = openAiChatTransformer.createStreamContext('gpt-5');
const event = openAiChatTransformer.transformStreamEvent({
id: 'chatcmpl-1',
model: 'gpt-5',
choices: [{
index: 0,
finish_reason: null,
delta: {
role: 'assistant',
content: 'hello',
reasoning_content: 'why',
annotations: [
{ type: 'url_citation', url_citation: { url: 'https://a.example' } },
],
},
}],
citations: ['https://c.example', 'https://a.example'],
usage: {
prompt_tokens: 11,
completion_tokens: 7,
total_tokens: 18,
prompt_tokens_details: { cached_tokens: 3 },
},
}, context, 'gpt-5');
const payloads = parseSsePayloads(
openAiChatTransformer.serializeStreamEvent(event, context, createClaudeDownstreamContext()),
);
expect(payloads[0]).toMatchObject({
citations: ['https://c.example', 'https://a.example'],
usage: {
prompt_tokens: 11,
completion_tokens: 7,
total_tokens: 18,
prompt_tokens_details: { cached_tokens: 3 },
},
});
expect(((payloads[0] as any).choices[0] as any).delta.annotations).toEqual([
{ type: 'url_citation', url_citation: { url: 'https://a.example' } },
]);
});
});
describe('openAiChatTransformer.aggregator', () => {
it('deduplicates annotations/citations while aggregating reasoning, tool calls, and usage details', () => {
const state = openAiChatTransformer.aggregator.createState();
openAiChatTransformer.aggregator.applyEvent(state, {
contentDelta: 'hel',
reasoningDelta: 'why',
toolCallDeltas: [{
index: 0,
id: 'call_1',
name: 'search',
argumentsDelta: '{"q":"cat"',
}],
annotations: [
{ type: 'url_citation', url_citation: { url: 'https://a.example' } },
],
citations: ['https://c.example'],
usageDetails: {
prompt_tokens_details: { cached_tokens: 2 },
},
} as any);
openAiChatTransformer.aggregator.applyEvent(state, {
contentDelta: 'lo',
reasoningDelta: ' now',
toolCallDeltas: [{
index: 0,
id: 'call_1',
name: 'search',
argumentsDelta: ',"k":1}',
}],
annotations: [
{ type: 'url_citation', url_citation: { url: 'https://a.example' } },
{ type: 'url_citation', url_citation: { url: 'https://b.example' } },
],
citations: ['https://c.example', 'https://d.example'],
usageDetails: {
completion_tokens_details: { reasoning_tokens: 4 },
},
finishReason: 'tool_calls',
} as any);
const normalized = openAiChatTransformer.aggregator.finalize(state, {
id: 'chatcmpl-1',
model: 'gpt-5',
created: 123,
content: '',
reasoningContent: '',
finishReason: 'stop',
toolCalls: [],
});
expect(normalized).toMatchObject({
content: 'hello',
reasoningContent: 'why now',
finishReason: 'tool_calls',
toolCalls: [{
id: 'call_1',
name: 'search',
arguments: '{"q":"cat","k":1}',
}],
citations: ['https://c.example', 'https://d.example'],
usageDetails: {
prompt_tokens_details: { cached_tokens: 2 },
completion_tokens_details: { reasoning_tokens: 4 },
},
});
expect((normalized as any).annotations).toEqual([
{ type: 'url_citation', url_citation: { url: 'https://a.example' } },
{ type: 'url_citation', url_citation: { url: 'https://b.example' } },
]);
});
});
@@ -0,0 +1,59 @@
import { type NormalizedFinalResponse, type NormalizedStreamEvent, type ParsedDownstreamChatRequest, type StreamTransformContext } from '../../shared/normalized.js';
import { openAiChatInbound } from './inbound.js';
import { openAiChatOutbound } from './outbound.js';
import { openAiChatStream } from './stream.js';
import { openAiChatUsage } from './usage.js';
import { createOpenAiChatAggregateState, applyOpenAiChatStreamEvent, finalizeOpenAiChatAggregate } from './aggregator.js';
export const openAiChatTransformer = {
protocol: 'openai/chat' as const,
inbound: openAiChatInbound,
outbound: openAiChatOutbound,
stream: openAiChatStream,
usage: openAiChatUsage,
aggregator: {
createState: createOpenAiChatAggregateState,
applyEvent: applyOpenAiChatStreamEvent,
finalize: finalizeOpenAiChatAggregate,
},
transformRequest(body: unknown): ReturnType<typeof openAiChatInbound.parse> {
return openAiChatInbound.parse(body);
},
createStreamContext(modelName: string): StreamTransformContext {
return openAiChatStream.createContext(modelName);
},
transformFinalResponse(payload: unknown, modelName: string, fallbackText = ''): NormalizedFinalResponse {
return openAiChatOutbound.normalizeFinal(payload, modelName, fallbackText);
},
transformStreamEvent(payload: unknown, context: StreamTransformContext, modelName: string): NormalizedStreamEvent {
return openAiChatStream.normalizeEvent(payload, context, modelName);
},
serializeStreamEvent(
event: NormalizedStreamEvent,
context: StreamTransformContext,
claudeContext: Parameters<typeof openAiChatStream.serializeEvent>[2],
): string[] {
return openAiChatStream.serializeEvent(event, context, claudeContext);
},
serializeDone(
context: StreamTransformContext,
claudeContext: Parameters<typeof openAiChatStream.serializeDone>[1],
): string[] {
return openAiChatStream.serializeDone(context, claudeContext);
},
serializeFinalResponse(
normalized: NormalizedFinalResponse,
usage: Parameters<typeof openAiChatOutbound.serializeFinal>[1],
) {
return openAiChatOutbound.serializeFinal(normalized, usage);
},
buildSyntheticChunks(normalized: NormalizedFinalResponse) {
return openAiChatOutbound.buildSyntheticChunks(normalized);
},
pullSseEvents(buffer: string) {
return openAiChatStream.pullSseEvents(buffer);
},
};
export type OpenAiChatTransformer = typeof openAiChatTransformer;
export type OpenAiChatParsedRequest = ParsedDownstreamChatRequest;
@@ -0,0 +1,44 @@
import type {
NormalizedFinalResponse,
NormalizedStreamEvent,
ParsedDownstreamChatRequest,
} from '../../shared/normalized.js';
export type OpenAiChatRequestMetadata = {
modalities?: unknown;
audio?: unknown;
reasoningEffort?: unknown;
reasoningBudget?: unknown;
reasoningSummary?: unknown;
serviceTier?: unknown;
topLogprobs?: unknown;
logitBias?: unknown;
promptCacheKey?: unknown;
safetyIdentifier?: unknown;
verbosity?: unknown;
responseFormat?: unknown;
streamOptionsIncludeUsage?: boolean | null;
};
export type OpenAiChatUsageDetails = {
prompt_tokens_details?: Record<string, number>;
completion_tokens_details?: Record<string, number>;
};
export type OpenAiChatParsedRequest = ParsedDownstreamChatRequest & {
requestMetadata?: OpenAiChatRequestMetadata;
};
export type OpenAiChatNormalizedFinalResponse = NormalizedFinalResponse & {
annotations?: Array<Record<string, unknown>>;
citations?: string[];
usageDetails?: OpenAiChatUsageDetails;
usagePayload?: Record<string, unknown>;
};
export type OpenAiChatNormalizedStreamEvent = NormalizedStreamEvent & {
annotations?: Array<Record<string, unknown>>;
citations?: string[];
usageDetails?: OpenAiChatUsageDetails;
usagePayload?: Record<string, unknown>;
};
@@ -0,0 +1,99 @@
import {
buildSyntheticOpenAiChunks,
normalizeUpstreamFinalResponse,
serializeFinalResponse,
type NormalizedFinalResponse,
} from '../../shared/normalized.js';
import { 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 {
return {
...normalizeUpstreamFinalResponse(payload, modelName, fallbackText),
...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>;
if (Array.isArray(chatNormalized.citations) && chatNormalized.citations.length > 0) {
payload.citations = chatNormalized.citations;
}
if (Array.isArray(chatNormalized.annotations) && chatNormalized.annotations.length > 0) {
const choices = Array.isArray(payload.choices) ? payload.choices : [];
const firstChoice = choices[0];
if (isRecord(firstChoice) && isRecord(firstChoice.message)) {
firstChoice.message.annotations = chatNormalized.annotations;
}
}
const mergedUsage = mergeUsagePayload(payload.usage, chatNormalized);
if (mergedUsage) {
payload.usage = {
...(isRecord(payload.usage) ? payload.usage : {}),
...mergedUsage,
};
}
return payload;
},
buildSyntheticChunks(normalized: NormalizedFinalResponse) {
const chunks = buildSyntheticOpenAiChunks(normalized);
const chatNormalized = normalized as OpenAiChatNormalizedFinalResponse;
if (chunks.length <= 0) return chunks;
if (Array.isArray(chatNormalized.citations) && chatNormalized.citations.length > 0) {
chunks[0] = {
...chunks[0],
citations: chatNormalized.citations,
};
}
if (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;
},
};
@@ -0,0 +1,109 @@
import {
createClaudeDownstreamContext,
createStreamTransformContext,
normalizeUpstreamStreamEvent,
pullSseEventsWithDone,
serializeNormalizedStreamEvent,
serializeStreamDone,
type ClaudeDownstreamContext,
type StreamTransformContext,
} from '../../shared/normalized.js';
import { 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 {
return {
...normalizeUpstreamStreamEvent(payload, context, modelName),
...extractChatResponseExtras(payload),
};
},
serializeEvent(
event: OpenAiChatNormalizedStreamEvent,
context: StreamTransformContext,
downstreamContext?: ClaudeDownstreamContext,
): string[] {
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);
},
};
@@ -0,0 +1,6 @@
import { createEmptyNormalizedUsage, mergeNormalizedUsage } from '../../shared/normalized.js';
export const openAiChatUsage = {
empty: createEmptyNormalizedUsage,
merge: mergeNormalizedUsage,
};
@@ -0,0 +1,336 @@
import { describe, expect, it } from 'vitest';
import {
createOpenAiResponsesAggregateState,
serializeConvertedResponsesEvents,
type OpenAiResponsesAggregateState,
completeResponsesStream,
} from './aggregator.js';
import { openAiResponsesStream } from './stream.js';
function collectSerializedEvents(input: string[]): Array<{ event: string; data: any }> {
const events: Array<{ event: string; data: any }> = [];
for (const chunk of input) {
const blocks = chunk.split('\n\n').filter((item) => item.trim().length > 0);
for (const block of blocks) {
const lines = block.split('\n');
const eventLine = lines.find((line) => line.startsWith('event: '));
const dataLine = lines.find((line) => line.startsWith('data: '));
if (!dataLine) continue;
const rawData = dataLine.slice('data: '.length);
if (rawData === '[DONE]') {
events.push({ event: eventLine?.slice('event: '.length) || 'done', data: '[DONE]' });
continue;
}
events.push({
event: eventLine?.slice('event: '.length) || '',
data: JSON.parse(rawData),
});
}
}
return events;
}
function feedResponsesEvent(
state: OpenAiResponsesAggregateState,
payload: Record<string, unknown>,
): Array<{ event: string; data: any }> {
const context = openAiResponsesStream.createContext('gpt-5');
const normalizedEvent = openAiResponsesStream.normalizeEvent(payload, context, 'gpt-5');
return collectSerializedEvents(
serializeConvertedResponsesEvents({
state,
streamContext: context,
event: normalizedEvent,
usage: {
promptTokens: 0,
completionTokens: 0,
totalTokens: 0,
},
}),
);
}
describe('openAiResponsesStream / aggregator', () => {
it('keeps reasoning separate from assistant text for normalized events and merges usage details', () => {
const state = createOpenAiResponsesAggregateState('gpt-5');
const context = openAiResponsesStream.createContext('gpt-5');
const contentEvent = {
role: 'assistant',
contentDelta: 'hello',
};
const reasoningEvent = {
reasoningDelta: 'plan first',
responsesPayload: {
usage: {
input_tokens: 5,
output_tokens: 3,
total_tokens: 8,
output_tokens_details: { reasoning_tokens: 2 },
},
},
};
collectSerializedEvents(
serializeConvertedResponsesEvents({
state,
streamContext: context,
event: contentEvent as any,
usage: { promptTokens: 5, completionTokens: 3, totalTokens: 8 },
}),
);
const reasoningEmitted = collectSerializedEvents(
serializeConvertedResponsesEvents({
state,
streamContext: context,
event: reasoningEvent as any,
usage: { promptTokens: 5, completionTokens: 3, totalTokens: 8 },
}),
);
expect(reasoningEmitted.some((item) => item.event === 'response.reasoning_summary_part.added')).toBe(true);
expect(reasoningEmitted.some((item) => item.event === 'response.reasoning_summary_text.delta')).toBe(true);
const completed = collectSerializedEvents(
completeResponsesStream(state, context, { promptTokens: 5, completionTokens: 3, totalTokens: 8 }),
).find((item) => item.event === 'response.completed');
expect(completed?.data.response.output).toEqual([
{
id: expect.any(String),
type: 'message',
role: 'assistant',
status: 'completed',
content: [
{
type: 'output_text',
text: 'hello',
},
],
},
{
id: expect.any(String),
type: 'reasoning',
status: 'completed',
summary: [
{
type: 'summary_text',
text: 'plan first',
},
],
},
]);
expect(completed?.data.response.usage).toMatchObject({
input_tokens: 5,
output_tokens: 3,
total_tokens: 8,
output_tokens_details: { reasoning_tokens: 2 },
});
});
it('tracks reasoning summary events into completed response output', () => {
const state = createOpenAiResponsesAggregateState('gpt-5');
const emitted = [
...feedResponsesEvent(state, {
type: 'response.output_item.added',
output_index: 0,
item: {
id: 'rs_1',
type: 'reasoning',
status: 'in_progress',
summary: [],
},
}),
...feedResponsesEvent(state, {
type: 'response.reasoning_summary_part.added',
item_id: 'rs_1',
output_index: 0,
summary_index: 0,
part: {
type: 'summary_text',
text: '',
},
}),
...feedResponsesEvent(state, {
type: 'response.reasoning_summary_text.delta',
item_id: 'rs_1',
output_index: 0,
summary_index: 0,
delta: 'Plan',
}),
...feedResponsesEvent(state, {
type: 'response.reasoning_summary_text.done',
item_id: 'rs_1',
output_index: 0,
summary_index: 0,
text: 'Plan it out',
}),
...feedResponsesEvent(state, {
type: 'response.reasoning_summary_part.done',
item_id: 'rs_1',
output_index: 0,
summary_index: 0,
part: {
type: 'summary_text',
text: 'Plan it out',
},
}),
...feedResponsesEvent(state, {
type: 'response.output_item.done',
output_index: 0,
item: {
id: 'rs_1',
type: 'reasoning',
status: 'completed',
summary: [],
},
}),
...feedResponsesEvent(state, {
type: 'response.completed',
response: {
id: 'resp_reasoning',
object: 'response',
created_at: 1700000000,
status: 'completed',
model: 'gpt-5',
output: [],
},
}),
];
expect(emitted.some((item) => item.event === 'response.reasoning_summary_part.added')).toBe(true);
expect(emitted.some((item) => item.event === 'response.reasoning_summary_text.done')).toBe(true);
const completed = emitted.find((item) => item.event === 'response.completed');
expect(completed?.data.response.output).toEqual([
{
id: 'rs_1',
type: 'reasoning',
status: 'completed',
summary: [
{
type: 'summary_text',
text: 'Plan it out',
},
],
},
]);
});
it('tracks custom tool input and image generation results', () => {
const state = createOpenAiResponsesAggregateState('gpt-5');
const emitted = [
...feedResponsesEvent(state, {
type: 'response.output_item.added',
output_index: 0,
item: {
id: 'ct_1',
type: 'custom_tool_call',
status: 'in_progress',
call_id: 'ct_1',
name: 'browser',
input: '',
},
}),
...feedResponsesEvent(state, {
type: 'response.custom_tool_call_input.delta',
item_id: 'ct_1',
output_index: 0,
delta: 'open ',
}),
...feedResponsesEvent(state, {
type: 'response.custom_tool_call_input.done',
item_id: 'ct_1',
output_index: 0,
input: 'open example.com',
}),
...feedResponsesEvent(state, {
type: 'response.output_item.done',
output_index: 0,
item: {
id: 'ct_1',
type: 'custom_tool_call',
status: 'completed',
call_id: 'ct_1',
name: 'browser',
input: 'open example.com',
},
}),
...feedResponsesEvent(state, {
type: 'response.output_item.added',
output_index: 1,
item: {
id: 'img_1',
type: 'image_generation_call',
status: 'in_progress',
result: null,
},
}),
...feedResponsesEvent(state, {
type: 'response.image_generation_call.partial_image',
item_id: 'img_1',
output_index: 1,
partial_image_index: 0,
partial_image_b64: 'abc123',
}),
...feedResponsesEvent(state, {
type: 'response.image_generation_call.completed',
item_id: 'img_1',
output_index: 1,
result: 'data:image/png;base64,final',
}),
...feedResponsesEvent(state, {
type: 'response.output_item.done',
output_index: 1,
item: {
id: 'img_1',
type: 'image_generation_call',
status: 'completed',
result: 'data:image/png;base64,final',
},
}),
...feedResponsesEvent(state, {
type: 'response.completed',
response: {
id: 'resp_tools',
object: 'response',
created_at: 1700000001,
status: 'completed',
model: 'gpt-5',
output: [],
},
}),
];
expect(emitted.some((item) => item.event === 'response.custom_tool_call_input.done')).toBe(true);
expect(emitted.some((item) => item.event === 'response.image_generation_call.completed')).toBe(true);
const completed = emitted.find((item) => item.event === 'response.completed');
expect(completed?.data.response.output).toEqual([
{
id: 'ct_1',
type: 'custom_tool_call',
status: 'completed',
call_id: 'ct_1',
name: 'browser',
input: 'open example.com',
},
{
id: 'img_1',
type: 'image_generation_call',
status: 'completed',
result: 'data:image/png;base64,final',
partial_images: [
{
partial_image_index: 0,
partial_image_b64: 'abc123',
},
],
},
]);
});
});
@@ -0,0 +1,762 @@
import { type StreamTransformContext } from '../../shared/normalized.js';
import type { OpenAiResponsesStreamEvent } from './stream.js';
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object' && !Array.isArray(value);
}
function asTrimmedString(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
function cloneRecord(value: unknown): Record<string, unknown> | null {
if (!isRecord(value)) return null;
return { ...value };
}
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 ensureOutputItemId(rawId: string, prefix: string, index: number): string {
const trimmed = rawId.trim();
if (trimmed) return trimmed;
return `${prefix}_${index}`;
}
function serializeSse(event: string, payload: unknown): string {
return `event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`;
}
function serializeDone(): string {
return 'data: [DONE]\n\n';
}
type ResponsesUsageSummary = {
promptTokens: number;
completionTokens: number;
totalTokens: number;
cacheReadTokens?: number;
cacheCreationTokens?: number;
promptTokensIncludeCache?: boolean | null;
};
type AggregateOutputItem = Record<string, unknown>;
export type OpenAiResponsesAggregateState = {
modelName: string;
responseId: string | null;
createdAt: number | null;
outputItems: Array<AggregateOutputItem | undefined>;
outputIndexById: Record<string, number>;
messageIndex: number | null;
reasoningIndexById: Record<string, number>;
functionIndexById: Record<string, number>;
customToolIndexById: Record<string, number>;
imageGenerationIndexById: Record<string, number>;
usageExtras: Record<string, unknown>;
completed: boolean;
failed: boolean;
};
export function createOpenAiResponsesAggregateState(modelName: string): OpenAiResponsesAggregateState {
return {
modelName,
responseId: null,
createdAt: null,
outputItems: [],
outputIndexById: {},
messageIndex: null,
reasoningIndexById: {},
functionIndexById: {},
customToolIndexById: {},
imageGenerationIndexById: {},
usageExtras: {},
completed: false,
failed: false,
};
}
function mergeUsageExtras(
state: OpenAiResponsesAggregateState,
usagePayload: unknown,
): void {
if (!isRecord(usagePayload)) return;
for (const [key, value] of Object.entries(usagePayload)) {
if (key === 'input_tokens' || key === 'output_tokens' || key === 'total_tokens') continue;
state.usageExtras[key] = cloneJson(value);
}
}
function rememberOutputId(state: OpenAiResponsesAggregateState, index: number, item: AggregateOutputItem) {
const itemId = asTrimmedString(item.id);
if (itemId) {
state.outputIndexById[itemId] = index;
}
const itemType = asTrimmedString(item.type).toLowerCase();
const callId = asTrimmedString(item.call_id);
if (itemType === 'function_call') {
if (callId) state.functionIndexById[callId] = index;
if (itemId) state.functionIndexById[itemId] = index;
}
if (itemType === 'custom_tool_call') {
if (callId) state.customToolIndexById[callId] = index;
if (itemId) state.customToolIndexById[itemId] = index;
}
if (itemType === 'image_generation_call' && itemId) {
state.imageGenerationIndexById[itemId] = index;
}
if (itemType === 'reasoning' && itemId) {
state.reasoningIndexById[itemId] = index;
}
if (itemType === 'message' && state.messageIndex === null) {
state.messageIndex = index;
}
}
function setOutputItem(
state: OpenAiResponsesAggregateState,
index: number,
item: AggregateOutputItem,
): AggregateOutputItem {
const current = cloneRecord(state.outputItems[index]) || {};
const incoming = cloneJson(item);
const next = {
...current,
...incoming,
};
if (Array.isArray(current.summary) && (!Array.isArray(incoming.summary) || incoming.summary.length <= 0)) {
next.summary = current.summary;
}
if (Array.isArray(current.content) && (!Array.isArray(incoming.content) || incoming.content.length <= 0)) {
next.content = current.content;
}
if (
Array.isArray(current.partial_images)
&& (!Array.isArray(incoming.partial_images) || incoming.partial_images.length <= 0)
) {
next.partial_images = current.partial_images;
}
state.outputItems[index] = next;
rememberOutputId(state, index, next);
return next;
}
function ensureOutputItem(
state: OpenAiResponsesAggregateState,
index: number,
factory: () => AggregateOutputItem,
): AggregateOutputItem {
const existing = state.outputItems[index];
if (existing) return existing;
return setOutputItem(state, index, factory());
}
function resolveOutputIndex(
state: OpenAiResponsesAggregateState,
rawIndex: unknown,
...candidateIds: Array<unknown>
): number {
if (typeof rawIndex === 'number' && Number.isFinite(rawIndex)) {
return Math.max(0, Math.trunc(rawIndex));
}
for (const rawId of candidateIds) {
const itemId = asTrimmedString(rawId);
if (itemId && state.outputIndexById[itemId] !== undefined) {
return state.outputIndexById[itemId];
}
}
return state.outputItems.length;
}
function ensureMessageItem(state: OpenAiResponsesAggregateState, indexHint?: number): { index: number; item: AggregateOutputItem } {
const index = state.messageIndex ?? indexHint ?? 0;
const item = ensureOutputItem(state, index, () => ({
id: ensureOutputItemId('', 'msg', index),
type: 'message',
role: 'assistant',
status: 'in_progress',
content: [],
}));
if (!Array.isArray(item.content)) item.content = [];
if (state.messageIndex === null) state.messageIndex = index;
return { index, item };
}
function ensureMessageOutputTextPart(
state: OpenAiResponsesAggregateState,
indexHint?: number,
): { index: number; item: AggregateOutputItem; part: AggregateOutputItem; created: boolean } {
const { index, item } = ensureMessageItem(state, indexHint);
const content = Array.isArray(item.content) ? item.content as AggregateOutputItem[] : [];
if (!Array.isArray(item.content)) item.content = content;
let part = content[0];
const created = !isRecord(part) || asTrimmedString(part.type).toLowerCase() !== 'output_text';
if (created) {
part = { type: 'output_text', text: '' };
content[0] = part;
}
return { index, item, part, created };
}
function ensureReasoningItem(
state: OpenAiResponsesAggregateState,
itemIdRaw: unknown,
indexHint?: unknown,
): { index: number; item: AggregateOutputItem; created: boolean } {
const itemId = asTrimmedString(itemIdRaw);
const existingIndex = itemId ? state.reasoningIndexById[itemId] : undefined;
const index = existingIndex ?? resolveOutputIndex(state, indexHint, itemId);
const created = !state.outputItems[index];
const item = ensureOutputItem(state, index, () => ({
id: ensureOutputItemId(itemId, 'rs', index),
type: 'reasoning',
status: 'in_progress',
summary: [],
}));
if (!Array.isArray(item.summary)) item.summary = [];
return { index, item, created };
}
function ensureReasoningSummaryPart(
state: OpenAiResponsesAggregateState,
itemIdRaw: unknown,
summaryIndexRaw: unknown,
indexHint?: unknown,
): { item: AggregateOutputItem; summary: AggregateOutputItem; index: number; summaryIndex: number; created: boolean } {
const reasoningState = ensureReasoningItem(state, itemIdRaw, indexHint);
const summaryIndex = typeof summaryIndexRaw === 'number' && Number.isFinite(summaryIndexRaw)
? Math.max(0, Math.trunc(summaryIndexRaw))
: 0;
const summary = Array.isArray(reasoningState.item.summary) ? reasoningState.item.summary as AggregateOutputItem[] : [];
if (!Array.isArray(reasoningState.item.summary)) reasoningState.item.summary = summary;
let part = summary[summaryIndex];
const created = !isRecord(part);
if (!isRecord(part)) {
part = { type: 'summary_text', text: '' };
summary[summaryIndex] = part;
}
return { item: reasoningState.item, summary: part, index: reasoningState.index, summaryIndex, created: reasoningState.created || created };
}
function ensureFunctionCallItem(
state: OpenAiResponsesAggregateState,
callIdRaw: unknown,
nameRaw: unknown,
indexHint?: number,
): { index: number; item: AggregateOutputItem; created: boolean } {
const callId = asTrimmedString(callIdRaw);
const name = asTrimmedString(nameRaw);
const existingIndex = callId ? state.functionIndexById[callId] : undefined;
const index = existingIndex ?? resolveOutputIndex(state, indexHint, callId);
const created = !state.outputItems[index];
const item = ensureOutputItem(state, index, () => ({
id: ensureOutputItemId(callId, 'fc', index),
type: 'function_call',
status: 'in_progress',
call_id: ensureOutputItemId(callId, 'call', index),
name,
arguments: '',
}));
if (callId) item.call_id = ensureOutputItemId(callId, 'call', index);
if (name) item.name = name;
if (typeof item.arguments !== 'string') item.arguments = '';
return { index, item, created };
}
function ensureCustomToolItem(
state: OpenAiResponsesAggregateState,
itemIdRaw: unknown,
callIdRaw: unknown,
nameRaw: unknown,
indexHint?: number,
): { index: number; item: AggregateOutputItem; created: boolean } {
const itemId = asTrimmedString(itemIdRaw);
const callId = asTrimmedString(callIdRaw);
const name = asTrimmedString(nameRaw);
const existingIndex = (callId && state.customToolIndexById[callId] !== undefined)
? state.customToolIndexById[callId]
: (itemId && state.customToolIndexById[itemId] !== undefined ? state.customToolIndexById[itemId] : undefined);
const index = existingIndex ?? resolveOutputIndex(state, indexHint, itemId, callId);
const created = !state.outputItems[index];
const item = ensureOutputItem(state, index, () => ({
id: ensureOutputItemId(itemId, 'ct', index),
type: 'custom_tool_call',
status: 'in_progress',
call_id: ensureOutputItemId(callId || itemId, 'call', index),
name,
input: '',
}));
if (callId || itemId) item.call_id = ensureOutputItemId(callId || itemId, 'call', index);
if (name) item.name = name;
if (typeof item.input !== 'string') item.input = '';
return { index, item, created };
}
function ensureImageGenerationItem(
state: OpenAiResponsesAggregateState,
itemIdRaw: unknown,
indexHint?: number,
): { index: number; item: AggregateOutputItem; created: boolean } {
const itemId = asTrimmedString(itemIdRaw);
const existingIndex = itemId ? state.imageGenerationIndexById[itemId] : undefined;
const index = existingIndex ?? resolveOutputIndex(state, indexHint, itemId);
const created = !state.outputItems[index];
const item = ensureOutputItem(state, index, () => ({
id: ensureOutputItemId(itemId, 'img', index),
type: 'image_generation_call',
status: 'in_progress',
result: null,
partial_images: [],
}));
if (!Array.isArray(item.partial_images)) item.partial_images = [];
return { index, item, created };
}
function buildUsagePayload(usage: ResponsesUsageSummary): Record<string, unknown> {
const payload: Record<string, unknown> = {
input_tokens: usage.promptTokens,
output_tokens: usage.completionTokens,
total_tokens: usage.totalTokens,
};
const inputDetails: Record<string, unknown> = {};
if ((usage.cacheReadTokens || 0) > 0) inputDetails.cached_tokens = usage.cacheReadTokens;
if ((usage.cacheCreationTokens || 0) > 0) inputDetails.cache_creation_tokens = usage.cacheCreationTokens;
if (Object.keys(inputDetails).length > 0) payload.input_tokens_details = inputDetails;
return {
...payload,
};
}
function collectOutputText(state: OpenAiResponsesAggregateState): string {
const parts: string[] = [];
for (const item of state.outputItems) {
if (!isRecord(item)) continue;
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 type = asTrimmedString(part.type).toLowerCase();
if ((type === 'output_text' || type === 'text') && typeof part.text === 'string' && part.text) {
parts.push(part.text);
}
}
}
return parts.join('');
}
function materializeResponse(
state: OpenAiResponsesAggregateState,
streamContext: StreamTransformContext,
usage: ResponsesUsageSummary,
responseTemplate?: Record<string, unknown> | null,
statusOverride?: 'completed' | 'failed',
): Record<string, unknown> {
const base = cloneRecord(responseTemplate) || {};
const responseId = ensureResponseId(
asTrimmedString(base.id)
|| state.responseId
|| streamContext.id
|| state.modelName,
);
const createdAt = (
typeof base.created_at === 'number' && Number.isFinite(base.created_at)
? base.created_at
: (typeof base.created === 'number' && Number.isFinite(base.created) ? base.created : null)
) ?? state.createdAt ?? Math.floor(Date.now() / 1000);
const output = state.outputItems
.filter((item): item is AggregateOutputItem => isRecord(item))
.map((item) => {
const status = asTrimmedString(item.status).toLowerCase();
return {
...item,
status: status && status !== 'in_progress'
? status
: (state.failed ? 'failed' : 'completed'),
};
});
return {
...base,
id: responseId,
object: 'response',
created_at: createdAt,
status: statusOverride ?? (state.failed ? 'failed' : 'completed'),
model: asTrimmedString(base.model) || streamContext.model || state.modelName,
output,
output_text: collectOutputText(state),
usage: buildUsagePayload(usage),
...Object.keys(state.usageExtras).length > 0 ? { usage: { ...buildUsagePayload(usage), ...state.usageExtras } } : {},
};
}
function serializeOriginalResponsesEvent(eventType: string, payload: Record<string, unknown>): string[] {
return [serializeSse(eventType, payload)];
}
function computeNovelDelta(existingText: string, incomingDelta: string): string {
if (!incomingDelta) return '';
if (!existingText) return incomingDelta;
if (existingText.endsWith(incomingDelta)) return '';
if (incomingDelta.startsWith(existingText)) {
return incomingDelta.slice(existingText.length);
}
if (existingText.includes(incomingDelta)) return '';
const maxOverlap = Math.min(existingText.length, incomingDelta.length);
for (let overlap = maxOverlap; overlap > 0; overlap -= 1) {
if (existingText.slice(-overlap) === incomingDelta.slice(0, overlap)) {
return incomingDelta.slice(overlap);
}
}
return incomingDelta;
}
function applyOriginalResponsesPayload(
state: OpenAiResponsesAggregateState,
eventType: string,
payload: Record<string, unknown>,
streamContext: StreamTransformContext,
usage: ResponsesUsageSummary,
): string[] {
switch (eventType) {
case 'response.output_item.added':
case 'response.output_item.done': {
const outputIndex = resolveOutputIndex(state, payload.output_index, (payload.item as Record<string, unknown> | undefined)?.id);
const item = cloneRecord(payload.item) || {};
if (Object.keys(item).length > 0) {
const existing = isRecord(state.outputItems[outputIndex]) ? cloneRecord(state.outputItems[outputIndex]) || {} : {};
const next = {
...existing,
...item,
};
if ((!Array.isArray(item.content) || item.content.length === 0) && Array.isArray(existing.content)) {
next.content = existing.content;
}
if ((!Array.isArray(item.summary) || item.summary.length === 0) && Array.isArray(existing.summary)) {
next.summary = existing.summary;
}
if ((!Array.isArray(item.partial_images) || item.partial_images.length === 0) && Array.isArray(existing.partial_images)) {
next.partial_images = existing.partial_images;
}
setOutputItem(state, outputIndex, next);
}
return serializeOriginalResponsesEvent(eventType, {
...payload,
item: state.outputItems[outputIndex] ?? payload.item,
});
}
case 'response.content_part.added':
case 'response.content_part.done': {
const outputIndex = resolveOutputIndex(state, payload.output_index, payload.item_id);
const contentIndex = typeof payload.content_index === 'number' && Number.isFinite(payload.content_index)
? Math.max(0, Math.trunc(payload.content_index))
: 0;
const message = ensureMessageItem(state, outputIndex).item;
const content = Array.isArray(message.content) ? message.content as AggregateOutputItem[] : [];
if (!Array.isArray(message.content)) message.content = content;
const part = cloneRecord(payload.part);
if (part) content[contentIndex] = part;
return serializeOriginalResponsesEvent(eventType, payload);
}
case 'response.output_text.delta':
case 'response.output_text.done': {
const outputIndex = resolveOutputIndex(state, payload.output_index, payload.item_id);
const textPart = ensureMessageOutputTextPart(state, outputIndex);
if (eventType === 'response.output_text.done') {
textPart.part.text = typeof payload.text === 'string' ? payload.text : String(payload.text ?? '');
} else {
textPart.part.text = `${typeof textPart.part.text === 'string' ? textPart.part.text : ''}${typeof payload.delta === 'string' ? payload.delta : ''}`;
}
return serializeOriginalResponsesEvent(eventType, payload);
}
case 'response.function_call_arguments.delta':
case 'response.function_call_arguments.done': {
const entry = ensureFunctionCallItem(
state,
payload.call_id ?? payload.item_id,
payload.name,
resolveOutputIndex(state, payload.output_index, payload.item_id, payload.call_id),
);
if (eventType === 'response.function_call_arguments.done') {
entry.item.arguments = typeof payload.arguments === 'string' ? payload.arguments : String(payload.arguments ?? '');
} else {
entry.item.arguments = `${typeof entry.item.arguments === 'string' ? entry.item.arguments : ''}${typeof payload.delta === 'string' ? payload.delta : ''}`;
}
return serializeOriginalResponsesEvent(eventType, payload);
}
case 'response.custom_tool_call_input.delta':
case 'response.custom_tool_call_input.done': {
const entry = ensureCustomToolItem(
state,
payload.item_id,
payload.call_id,
payload.name,
resolveOutputIndex(state, payload.output_index, payload.item_id, payload.call_id),
);
if (eventType === 'response.custom_tool_call_input.done') {
entry.item.input = typeof payload.input === 'string' ? payload.input : String(payload.input ?? '');
} else {
entry.item.input = `${typeof entry.item.input === 'string' ? entry.item.input : ''}${typeof payload.delta === 'string' ? payload.delta : ''}`;
}
return serializeOriginalResponsesEvent(eventType, payload);
}
case 'response.reasoning_summary_part.added':
case 'response.reasoning_summary_part.done': {
const summaryState = ensureReasoningSummaryPart(state, payload.item_id, payload.summary_index, payload.output_index);
const part = cloneRecord(payload.part);
if (part) {
const summary = Array.isArray(summaryState.item.summary) ? summaryState.item.summary as AggregateOutputItem[] : [];
summary[summaryState.summaryIndex] = {
...summaryState.summary,
...part,
};
summaryState.item.summary = summary;
}
return serializeOriginalResponsesEvent(eventType, payload);
}
case 'response.reasoning_summary_text.delta':
case 'response.reasoning_summary_text.done': {
const summaryState = ensureReasoningSummaryPart(state, payload.item_id, payload.summary_index, payload.output_index);
if (eventType === 'response.reasoning_summary_text.done') {
summaryState.summary.text = typeof payload.text === 'string' ? payload.text : String(payload.text ?? '');
} else {
summaryState.summary.text = `${typeof summaryState.summary.text === 'string' ? summaryState.summary.text : ''}${typeof payload.delta === 'string' ? payload.delta : ''}`;
}
return serializeOriginalResponsesEvent(eventType, payload);
}
case 'response.image_generation_call.generating':
case 'response.image_generation_call.in_progress':
case 'response.image_generation_call.partial_image':
case 'response.image_generation_call.completed': {
const entry = ensureImageGenerationItem(
state,
payload.item_id,
resolveOutputIndex(state, payload.output_index, payload.item_id),
);
if (eventType === 'response.image_generation_call.partial_image') {
const partialImages = Array.isArray(entry.item.partial_images) ? entry.item.partial_images as AggregateOutputItem[] : [];
partialImages.push({
partial_image_index: payload.partial_image_index,
partial_image_b64: payload.partial_image_b64,
});
entry.item.partial_images = partialImages;
}
if (payload.result !== undefined) {
entry.item.result = payload.result;
}
if (eventType === 'response.image_generation_call.completed') {
entry.item.status = 'completed';
}
return serializeOriginalResponsesEvent(eventType, payload);
}
case 'response.completed': {
mergeUsageExtras(state, payload.response && isRecord(payload.response) ? payload.response.usage : payload.usage);
state.completed = true;
const responsePayload = cloneRecord(payload.response);
const materialized = materializeResponse(state, streamContext, usage, responsePayload, 'completed');
return [serializeSse('response.completed', { ...payload, response: materialized })];
}
case 'response.failed': {
mergeUsageExtras(state, payload.response && isRecord(payload.response) ? payload.response.usage : payload.usage);
state.failed = true;
const responsePayload = cloneRecord(payload.response);
const materialized = materializeResponse(state, streamContext, usage, responsePayload, 'failed');
return [serializeSse('response.failed', { ...payload, response: materialized })];
}
default:
mergeUsageExtras(state, payload.usage);
return serializeOriginalResponsesEvent(eventType, payload);
}
}
function buildSyntheticMessageEvents(
state: OpenAiResponsesAggregateState,
delta: string,
): string[] {
const { index, item } = ensureMessageItem(state);
const textPartState = ensureMessageOutputTextPart(state, index);
const lines: string[] = [];
const currentText = typeof textPartState.part.text === 'string' ? textPartState.part.text : '';
const novelDelta = computeNovelDelta(currentText, delta);
if (textPartState.created) {
lines.push(serializeSse('response.output_item.added', {
type: 'response.output_item.added',
output_index: index,
item,
}));
lines.push(serializeSse('response.content_part.added', {
type: 'response.content_part.added',
output_index: index,
item_id: item.id,
content_index: 0,
part: { type: 'output_text', text: '' },
}));
}
if (novelDelta) {
textPartState.part.text = `${currentText}${novelDelta}`;
lines.push(serializeSse('response.output_text.delta', {
type: 'response.output_text.delta',
output_index: index,
item_id: item.id,
delta: novelDelta,
}));
}
return lines;
}
function buildSyntheticReasoningEvents(
state: OpenAiResponsesAggregateState,
delta: string,
): string[] {
const summaryState = ensureReasoningSummaryPart(state, '', 0, state.outputItems.length);
const itemId = asTrimmedString(summaryState.item.id);
const lines: string[] = [];
const currentText = typeof summaryState.summary.text === 'string' ? summaryState.summary.text : '';
const novelDelta = computeNovelDelta(currentText, delta);
if (summaryState.created) {
lines.push(serializeSse('response.output_item.added', {
type: 'response.output_item.added',
output_index: summaryState.index,
item: summaryState.item,
}));
lines.push(serializeSse('response.reasoning_summary_part.added', {
type: 'response.reasoning_summary_part.added',
item_id: itemId,
output_index: summaryState.index,
summary_index: 0,
part: { type: 'summary_text', text: '' },
}));
}
if (novelDelta) {
summaryState.summary.text = `${currentText}${novelDelta}`;
lines.push(serializeSse('response.reasoning_summary_text.delta', {
type: 'response.reasoning_summary_text.delta',
item_id: itemId,
output_index: summaryState.index,
summary_index: 0,
delta: novelDelta,
}));
}
return lines;
}
function buildSyntheticToolEvents(
state: OpenAiResponsesAggregateState,
event: OpenAiResponsesStreamEvent,
): string[] {
const lines: string[] = [];
if (!Array.isArray(event.toolCallDeltas)) return lines;
for (const toolDelta of event.toolCallDeltas) {
const entry = ensureFunctionCallItem(state, toolDelta.id, toolDelta.name, toolDelta.index);
if (entry.created) {
lines.push(serializeSse('response.output_item.added', {
type: 'response.output_item.added',
output_index: entry.index,
item: entry.item,
}));
}
if (toolDelta.argumentsDelta !== undefined && toolDelta.argumentsDelta.length > 0) {
entry.item.arguments = `${typeof entry.item.arguments === 'string' ? entry.item.arguments : ''}${toolDelta.argumentsDelta}`;
lines.push(serializeSse('response.function_call_arguments.delta', {
type: 'response.function_call_arguments.delta',
item_id: entry.item.id,
call_id: entry.item.call_id,
output_index: entry.index,
name: entry.item.name,
delta: toolDelta.argumentsDelta,
}));
}
}
return lines;
}
export function serializeConvertedResponsesEvents(input: {
state: OpenAiResponsesAggregateState;
streamContext: StreamTransformContext;
event: OpenAiResponsesStreamEvent;
usage: ResponsesUsageSummary;
}): string[] {
const { state, streamContext, event, usage } = input;
mergeUsageExtras(state, event.responsesPayload && isRecord(event.responsesPayload) ? event.responsesPayload.usage : undefined);
if (event.responsesEventType && event.responsesPayload) {
return applyOriginalResponsesPayload(
state,
event.responsesEventType,
event.responsesPayload,
streamContext,
usage,
);
}
const lines: string[] = [];
if (event.contentDelta) {
lines.push(...buildSyntheticMessageEvents(state, event.contentDelta));
}
if (event.reasoningDelta) {
lines.push(...buildSyntheticReasoningEvents(state, event.reasoningDelta));
}
lines.push(...buildSyntheticToolEvents(state, event));
return lines;
}
export function completeResponsesStream(
state: OpenAiResponsesAggregateState,
streamContext: StreamTransformContext,
usage: ResponsesUsageSummary,
): string[] {
if (state.failed || state.completed) {
return [serializeDone()];
}
state.completed = true;
return [
serializeSse('response.completed', {
type: 'response.completed',
response: materializeResponse(state, streamContext, usage, null, 'completed'),
}),
serializeDone(),
];
}
export function failResponsesStream(
state: OpenAiResponsesAggregateState,
streamContext: StreamTransformContext,
usage: ResponsesUsageSummary,
payload: unknown,
): string[] {
if (state.failed) {
return [serializeDone()];
}
state.failed = true;
const errorPayload = cloneRecord(payload);
const message = (
isRecord(errorPayload?.error) && typeof errorPayload.error.message === 'string'
? errorPayload.error.message
: (typeof errorPayload?.message === 'string' ? errorPayload.message : 'upstream stream failed')
);
return [
serializeSse('response.failed', {
type: 'response.failed',
response: materializeResponse(state, streamContext, usage, cloneRecord(errorPayload?.response), 'failed'),
error: {
message,
type: 'upstream_error',
},
}),
serializeDone(),
];
}
@@ -0,0 +1,409 @@
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 toTextBlockType(role: string): 'input_text' | 'output_text' {
return role === 'assistant' ? 'output_text' : 'input_text';
}
function normalizeImageUrlValue(value: unknown): string | Record<string, unknown> | null {
if (typeof value === 'string' && value.trim().length > 0) return value;
if (!isRecord(value)) return null;
const url = asTrimmedString(value.url);
if (url) return { ...value, url };
const imageUrl = asTrimmedString(value.image_url);
if (imageUrl) return imageUrl;
return Object.keys(value).length > 0 ? value : null;
}
function normalizeAudioInputValue(value: unknown): Record<string, unknown> | null {
if (!isRecord(value)) return null;
const data = asTrimmedString(value.data);
const format = asTrimmedString(value.format);
if (!data && !format) return Object.keys(value).length > 0 ? value : null;
return {
...value,
...(data ? { data } : {}),
...(format ? { format } : {}),
};
}
function normalizeResponsesContentItem(
item: unknown,
role: string,
): Record<string, unknown> | null {
if (typeof item === 'string') {
const text = item.trim();
return text ? { type: toTextBlockType(role), text } : null;
}
if (!isRecord(item)) return null;
const type = asTrimmedString(item.type).toLowerCase();
if (!type) {
const text = asTrimmedString(item.text ?? item.content ?? item.output_text);
return text ? { type: toTextBlockType(role), text } : null;
}
if (type === 'input_text' || type === 'output_text' || type === 'text') {
const text = asTrimmedString(item.text ?? item.content ?? item.output_text);
if (!text) return null;
return {
...item,
type: type === 'text' ? toTextBlockType(role) : type,
text,
};
}
if (type === 'input_image' || type === 'image_url') {
const imageUrl = normalizeImageUrlValue(item.image_url ?? item.url);
if (!imageUrl) return null;
return {
...item,
type: 'input_image',
image_url: imageUrl,
};
}
if (type === 'input_audio') {
const inputAudio = normalizeAudioInputValue(item.input_audio);
if (!inputAudio) return null;
return {
...item,
type: 'input_audio',
input_audio: inputAudio,
};
}
if (type === 'function_call' || type === 'function_call_output') {
return item;
}
return item;
}
export function normalizeResponsesMessageContent(content: unknown, role: string): unknown {
if (Array.isArray(content)) {
const normalized = content
.map((item) => normalizeResponsesContentItem(item, role))
.filter((item): item is Record<string, unknown> => !!item);
return normalized.length > 0 ? normalized : content;
}
const single = normalizeResponsesContentItem(content, role);
if (single) return [single];
return content;
}
function toResponsesInputMessageFromText(text: string): Record<string, unknown> {
return {
type: 'message',
role: 'user',
content: [{ type: 'input_text', text }],
};
}
export function normalizeResponsesMessageItem(item: Record<string, unknown>): Record<string, unknown> {
const type = asTrimmedString(item.type).toLowerCase();
if (type === 'function_call' || type === 'function_call_output') {
return item;
}
const role = asTrimmedString(item.role).toLowerCase() || 'user';
const normalizedContent = normalizeResponsesMessageContent(item.content ?? item.text, role);
if (type === 'message') {
return {
...item,
role,
content: normalizedContent,
};
}
if (asTrimmedString(item.role)) {
return {
type: 'message',
role,
content: normalizedContent,
};
}
if (typeof item.content === 'string') {
const text = item.content.trim();
return text ? toResponsesInputMessageFromText(text) : item;
}
return item;
}
export function normalizeResponsesInputForCompatibility(input: unknown): unknown {
if (typeof input === 'string') {
const normalized = input.trim();
if (!normalized) return input;
return [toResponsesInputMessageFromText(normalized)];
}
if (Array.isArray(input)) {
return input.map((item) => {
if (typeof item === 'string') {
const normalized = item.trim();
return normalized ? toResponsesInputMessageFromText(normalized) : item;
}
if (!isRecord(item)) return item;
return normalizeResponsesMessageItem(item);
});
}
if (isRecord(input)) {
return [normalizeResponsesMessageItem(input)];
}
return input;
}
function extractTextFromResponsesContent(content: unknown): string {
if (typeof content === 'string') return content.trim();
if (!Array.isArray(content)) return '';
return content
.map((item) => {
if (!isRecord(item)) return '';
return asTrimmedString(item.text ?? item.content ?? item.output_text);
})
.filter((item) => item.length > 0)
.join('\n');
}
function safeJsonStringify(value: unknown): string {
try {
return JSON.stringify(value);
} catch {
return '';
}
}
function normalizeOpenAiToolArguments(raw: unknown): string {
if (typeof raw === 'string') return raw;
if (raw === undefined || raw === null) return '';
if (typeof raw === 'number' || typeof raw === 'boolean') return String(raw);
if (Array.isArray(raw) || isRecord(raw)) {
return safeJsonStringify(raw);
}
return '';
}
function normalizeToolMessageContent(raw: unknown): string {
if (raw === undefined || raw === null) return '';
if (typeof raw === 'string') return raw;
if (typeof raw === 'number' || typeof raw === 'boolean') return String(raw);
if (Array.isArray(raw)) {
const normalized = normalizeResponsesMessageContent(raw, 'user');
const text = extractTextFromResponsesContent(normalized);
return text || safeJsonStringify(raw);
}
if (isRecord(raw)) {
const normalized = normalizeResponsesMessageContent(raw, 'user');
const text = extractTextFromResponsesContent(normalized);
return text || safeJsonStringify(raw);
}
return '';
}
function toFiniteNumber(value: unknown): number | null {
return typeof value === 'number' && Number.isFinite(value) ? value : null;
}
const ALLOWED_RESPONSES_FIELDS = new Set([
'model',
'input',
'instructions',
'max_output_tokens',
'max_completion_tokens',
'temperature',
'top_p',
'truncation',
'tools',
'tool_choice',
'parallel_tool_calls',
'metadata',
'reasoning',
'store',
'stream',
'user',
'previous_response_id',
'text',
'audio',
'include',
'response_format',
'service_tier',
'stop',
'n',
]);
export function convertOpenAiBodyToResponsesBody(
openaiBody: Record<string, unknown>,
modelName: string,
stream: boolean,
): Record<string, unknown> {
const rawMessages = Array.isArray(openaiBody.messages) ? openaiBody.messages : [];
const systemContents: string[] = [];
const inputItems: Array<Record<string, unknown>> = [];
for (const item of rawMessages) {
if (!isRecord(item)) continue;
const role = asTrimmedString(item.role).toLowerCase() || 'user';
if (role === 'system' || role === 'developer') {
const normalized = normalizeResponsesMessageContent(item.content, 'user');
const content = extractTextFromResponsesContent(normalized).trim();
if (content) systemContents.push(content);
continue;
}
if (role === 'assistant') {
const normalizedAssistantContent = normalizeResponsesMessageContent(item.content, 'assistant');
if (Array.isArray(normalizedAssistantContent) && normalizedAssistantContent.length > 0) {
inputItems.push({
type: 'message',
role: 'assistant',
content: normalizedAssistantContent,
});
}
const rawToolCalls = Array.isArray(item.tool_calls) ? item.tool_calls : [];
for (let index = 0; index < rawToolCalls.length; index += 1) {
const toolCall = rawToolCalls[index];
if (!isRecord(toolCall)) continue;
const functionPart = isRecord(toolCall.function) ? toolCall.function : {};
const callId = asTrimmedString(toolCall.id) || `call_${Date.now()}_${index}`;
const name = (
asTrimmedString(functionPart.name)
|| asTrimmedString(toolCall.name)
|| `tool_${index}`
);
const argumentsValue = normalizeOpenAiToolArguments(
functionPart.arguments ?? toolCall.arguments,
);
inputItems.push({
type: 'function_call',
call_id: callId,
name,
arguments: argumentsValue,
});
}
continue;
}
if (role === 'tool') {
const callId = asTrimmedString(item.tool_call_id) || asTrimmedString(item.id);
if (!callId) continue;
inputItems.push({
type: 'function_call_output',
call_id: callId,
output: normalizeToolMessageContent(item.content),
});
continue;
}
const normalizedUserContent = normalizeResponsesMessageContent(item.content, 'user');
if (Array.isArray(normalizedUserContent) && normalizedUserContent.length > 0) {
inputItems.push({
type: 'message',
role: 'user',
content: normalizedUserContent,
});
}
}
const maxOutputTokens = (
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 (systemContents.length > 0) {
body.instructions = systemContents.join('\n\n');
}
const temperature = toFiniteNumber(openaiBody.temperature);
if (temperature !== null) body.temperature = temperature;
const topP = toFiniteNumber(openaiBody.top_p);
if (topP !== null) body.top_p = topP;
if (openaiBody.metadata !== undefined) body.metadata = openaiBody.metadata;
if (openaiBody.reasoning !== undefined) body.reasoning = openaiBody.reasoning;
if (openaiBody.parallel_tool_calls !== undefined) body.parallel_tool_calls = openaiBody.parallel_tool_calls;
if (openaiBody.tools !== undefined) body.tools = openaiBody.tools;
if (openaiBody.tool_choice !== undefined) body.tool_choice = openaiBody.tool_choice;
return {
...body,
input: normalizeResponsesInputForCompatibility(body.input),
};
}
export function sanitizeResponsesBodyForProxy(
body: Record<string, unknown>,
modelName: string,
stream: boolean,
): Record<string, unknown> {
let normalized: Record<string, unknown> = {
...body,
model: modelName,
stream,
};
if (normalized.input === undefined) {
if (Array.isArray((normalized as Record<string, unknown>).messages)) {
normalized = convertOpenAiBodyToResponsesBody(normalized, modelName, stream);
} else {
const prompt = asTrimmedString((normalized as Record<string, unknown>).prompt);
if (prompt) {
normalized = {
...normalized,
input: [toResponsesInputMessageFromText(prompt)],
};
}
}
} else {
normalized = {
...normalized,
input: normalizeResponsesInputForCompatibility(normalized.input),
};
}
const sanitized: Record<string, unknown> = {};
for (const [key, value] of Object.entries(normalized)) {
if (!ALLOWED_RESPONSES_FIELDS.has(key)) continue;
if (key === 'max_completion_tokens') continue;
sanitized[key] = value;
}
const maxOutputTokens = toFiniteNumber(normalized.max_output_tokens);
if (maxOutputTokens !== null && maxOutputTokens > 0) {
sanitized.max_output_tokens = Math.trunc(maxOutputTokens);
} else {
const maxCompletionTokens = toFiniteNumber(normalized.max_completion_tokens);
if (maxCompletionTokens !== null && maxCompletionTokens > 0) {
sanitized.max_output_tokens = Math.trunc(maxCompletionTokens);
}
}
sanitized.model = modelName;
sanitized.stream = stream;
return sanitized;
}
@@ -0,0 +1,261 @@
import { describe, expect, it } from 'vitest';
import {
convertOpenAiBodyToResponsesBody,
convertResponsesBodyToOpenAiBody,
sanitizeResponsesBodyForProxy,
} from './conversion.js';
describe('sanitizeResponsesBodyForProxy', () => {
it('preserves newer Responses request fields needed by the proxy', () => {
const result = sanitizeResponsesBodyForProxy(
{
model: 'gpt-5',
input: 'hello',
safety_identifier: 'safe-user-1',
max_tool_calls: 3,
prompt_cache_key: 'cache-key',
prompt_cache_retention: { scope: 'session' },
stream_options: { include_obfuscation: true },
background: true,
text: { format: { type: 'text' }, verbosity: 'high' },
},
'gpt-5',
true,
);
expect(result).toMatchObject({
model: 'gpt-5',
stream: true,
safety_identifier: 'safe-user-1',
max_tool_calls: 3,
prompt_cache_key: 'cache-key',
prompt_cache_retention: { scope: 'session' },
stream_options: { include_obfuscation: true },
background: true,
text: { format: { type: 'text' }, verbosity: 'high' },
});
});
});
describe('convertOpenAiBodyToResponsesBody', () => {
it('maps extra request fields and preserves custom/image_generation tools', () => {
const result = convertOpenAiBodyToResponsesBody(
{
model: 'gpt-5',
messages: [{ role: 'user', content: 'draw a cat' }],
safety_identifier: 'safe-user-2',
max_tool_calls: 2,
prompt_cache_key: 'prompt-key',
prompt_cache_retention: { scope: 'workspace' },
stream_options: { include_obfuscation: true },
background: false,
verbosity: 'low',
tools: [
{
type: 'custom',
name: 'browser',
description: 'browse the web',
format: { type: 'text' },
},
{
type: 'image_generation',
background: 'transparent',
size: '1024x1024',
},
],
},
'gpt-5',
false,
);
expect(result).toMatchObject({
model: 'gpt-5',
stream: false,
safety_identifier: 'safe-user-2',
max_tool_calls: 2,
prompt_cache_key: 'prompt-key',
prompt_cache_retention: { scope: 'workspace' },
stream_options: { include_obfuscation: true },
background: false,
text: { verbosity: 'low' },
tools: [
{
type: 'custom',
name: 'browser',
description: 'browse the web',
format: { type: 'text' },
},
{
type: 'image_generation',
background: 'transparent',
size: '1024x1024',
},
],
});
});
});
describe('convertResponsesBodyToOpenAiBody', () => {
it('preserves richer Responses request fields back onto the OpenAI-compatible body', () => {
const result = convertResponsesBodyToOpenAiBody(
{
model: 'gpt-5',
input: [{ type: 'message', role: 'user', content: [{ type: 'input_text', text: 'hello' }] }],
safety_identifier: 'safe-user-3',
max_tool_calls: 4,
prompt_cache_key: 'prompt-key-2',
prompt_cache_retention: { scope: 'project' },
stream_options: { include_obfuscation: true },
background: true,
text: { format: { type: 'json_object' }, verbosity: 'high' },
tools: [
{
type: 'custom',
name: 'browser',
format: { type: 'grammar', syntax: 'lark' },
},
{
type: 'image_generation',
background: 'transparent',
partial_images: 2,
output_format: 'png',
},
],
},
'gpt-5',
true,
);
expect(result).toMatchObject({
model: 'gpt-5',
stream: true,
safety_identifier: 'safe-user-3',
max_tool_calls: 4,
prompt_cache_key: 'prompt-key-2',
prompt_cache_retention: { scope: 'project' },
stream_options: { include_obfuscation: true },
background: true,
verbosity: 'high',
tools: [
{
type: 'custom',
name: 'browser',
format: { type: 'grammar', syntax: 'lark' },
},
{
type: 'image_generation',
background: 'transparent',
partial_images: 2,
output_format: 'png',
},
],
});
});
it('converts custom tool calls and outputs into OpenAI-compatible tool messages', () => {
const result = convertResponsesBodyToOpenAiBody(
{
model: 'gpt-5',
input: [
{
type: 'custom_tool_call',
id: 'ct_1',
call_id: 'ct_1',
name: 'browser',
input: 'open example.com',
},
{
type: 'custom_tool_call_output',
call_id: 'ct_1',
output: 'done',
},
],
},
'gpt-5',
false,
);
expect(result.messages).toEqual([
{
role: 'assistant',
content: '',
tool_calls: [
{
id: 'ct_1',
type: 'function',
function: {
name: 'browser',
arguments: 'open example.com',
},
},
],
},
{
role: 'tool',
tool_call_id: 'ct_1',
content: 'done',
},
]);
});
it('converts reasoning items back into assistant content instead of dropping them', () => {
const result = convertResponsesBodyToOpenAiBody(
{
model: 'gpt-5',
input: [
{
type: 'reasoning',
id: 'rs_1',
status: 'completed',
summary: [
{ type: 'summary_text', text: 'Think step by step' },
],
},
],
},
'gpt-5',
false,
);
expect(result.messages).toEqual([
{
role: 'assistant',
content: [
{
type: 'text',
text: 'Think step by step',
},
],
},
]);
});
it('preserves remaining request fields needed for OpenAI-compatible downstream fallback', () => {
const result = convertResponsesBodyToOpenAiBody(
{
model: 'gpt-5',
input: 'hello',
user: 'user-123',
include: ['reasoning.encrypted_content'],
previous_response_id: 'resp_prev',
truncation: 'auto',
service_tier: 'priority',
top_logprobs: 4,
},
'gpt-5',
true,
);
expect(result).toMatchObject({
model: 'gpt-5',
stream: true,
user: 'user-123',
include: ['reasoning.encrypted_content'],
previous_response_id: 'resp_prev',
truncation: 'auto',
service_tier: 'priority',
top_logprobs: 4,
});
});
});
@@ -0,0 +1,698 @@
import {
normalizeResponsesInputForCompatibility as normalizeResponsesInputForCompatibilityViaCompatibility,
normalizeResponsesMessageItem,
} from './compatibility.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 toFiniteNumber(value: unknown): number | null {
return typeof value === 'number' && Number.isFinite(value) ? value : null;
}
function safeJsonStringify(value: unknown): string {
try {
return JSON.stringify(value);
} catch {
return '';
}
}
function cloneRecord(value: unknown): Record<string, unknown> | null {
if (!isRecord(value)) return null;
return { ...value };
}
function cloneJsonValue<T>(value: T): T {
if (Array.isArray(value)) {
return value.map((item) => cloneJsonValue(item)) as T;
}
if (isRecord(value)) {
return Object.fromEntries(
Object.entries(value).map(([key, item]) => [key, cloneJsonValue(item)]),
) as T;
}
return value;
}
function parseJsonString(raw: string): unknown {
const trimmed = raw.trim();
if (!trimmed) return {};
try {
return JSON.parse(trimmed);
} catch {
return { value: raw };
}
}
export function extractTextContent(value: unknown): string {
if (typeof value === 'string') return value;
if (Array.isArray(value)) {
return value
.map((item) => extractTextContent(item))
.filter((item) => item.length > 0)
.join('\n');
}
if (!isRecord(value)) return '';
const direct = asTrimmedString(
value.text
?? value.content
?? value.input_text
?? value.output_text
?? value.reasoning
?? value.reasoning_content
?? value.thinking,
);
if (direct) return direct;
if (Array.isArray(value.parts)) return extractTextContent(value.parts);
if (Array.isArray(value.content)) return extractTextContent(value.content);
if (Array.isArray(value.output)) return extractTextContent(value.output);
return '';
}
function normalizeOpenAiToolArguments(raw: unknown): string {
if (typeof raw === 'string') return raw;
if (raw === undefined || raw === null) return '';
if (typeof raw === 'number' || typeof raw === 'boolean') return String(raw);
if (Array.isArray(raw) || isRecord(raw)) return safeJsonStringify(raw);
return '';
}
function normalizeToolOutput(raw: unknown): string {
const text = extractTextContent(raw).trim();
if (text) return text;
if (raw === undefined || raw === null) return '';
if (typeof raw === 'string') return raw;
if (typeof raw === 'number' || typeof raw === 'boolean') return String(raw);
if (Array.isArray(raw) || isRecord(raw)) return safeJsonStringify(raw);
return '';
}
export function normalizeResponsesInputForCompatibility(input: unknown): unknown {
return normalizeResponsesInputForCompatibilityViaCompatibility(input);
}
function toResponsesInputMessageFromText(text: string): Record<string, unknown> {
return {
type: 'message',
role: 'user',
content: [{ type: 'input_text', text }],
};
}
function convertOpenAiToolsToResponses(rawTools: unknown): unknown {
if (!Array.isArray(rawTools)) return rawTools;
const converted = rawTools
.map((item) => {
if (!isRecord(item)) return null;
const type = asTrimmedString(item.type).toLowerCase();
if (type === 'function' && isRecord(item.function)) {
const fn = item.function;
const name = asTrimmedString(fn.name);
if (!name) return null;
const mapped: Record<string, unknown> = {
type: 'function',
name,
};
const description = asTrimmedString(fn.description);
if (description) mapped.description = description;
if (fn.parameters !== undefined) mapped.parameters = fn.parameters;
if (fn.strict !== undefined) mapped.strict = fn.strict;
return mapped;
}
if (type === 'function' && asTrimmedString(item.name)) {
return item;
}
if (type === 'image_generation') {
return item;
}
if (type === 'custom' && asTrimmedString(item.name)) {
return item;
}
return null;
})
.filter((item): item is Record<string, unknown> => !!item);
return converted.length > 0 ? converted : rawTools;
}
function convertOpenAiToolChoiceToResponses(rawToolChoice: unknown): unknown {
if (rawToolChoice === undefined) return undefined;
if (typeof rawToolChoice === 'string') return rawToolChoice;
if (!isRecord(rawToolChoice)) return rawToolChoice;
const type = asTrimmedString(rawToolChoice.type).toLowerCase();
if (type === 'function' && isRecord(rawToolChoice.function)) {
const name = asTrimmedString(rawToolChoice.function.name);
if (!name) return 'required';
return { type: 'function', name };
}
return rawToolChoice;
}
function normalizeResponsesBodyForCompatibility(
body: Record<string, unknown>,
): Record<string, unknown> {
const nextInput = normalizeResponsesInputForCompatibility(body.input);
if (nextInput === body.input) return body;
return {
...body,
input: nextInput,
};
}
export function normalizeResponsesMessageContent(role: string, content: unknown): Array<Record<string, unknown>> {
const normalized = normalizeResponsesMessageItem({
type: 'message',
role,
content,
});
if (isRecord(normalized) && Array.isArray(normalized.content)) {
return normalized.content.filter((item): item is Record<string, unknown> => isRecord(item));
}
return [];
}
const ALLOWED_RESPONSES_FIELDS = new Set([
'model',
'input',
'instructions',
'max_output_tokens',
'max_completion_tokens',
'temperature',
'top_p',
'truncation',
'tools',
'tool_choice',
'parallel_tool_calls',
'metadata',
'reasoning',
'store',
'safety_identifier',
'stream',
'stream_options',
'user',
'max_tool_calls',
'prompt_cache_key',
'prompt_cache_retention',
'background',
'previous_response_id',
'text',
'audio',
'include',
'response_format',
'service_tier',
'stop',
'n',
]);
export function sanitizeResponsesBodyForProxy(
body: Record<string, unknown>,
modelName: string,
stream: boolean,
): Record<string, unknown> {
let normalized = normalizeResponsesBodyForCompatibility({
...body,
model: modelName,
stream,
});
if (normalized.input === undefined) {
if (Array.isArray((normalized as Record<string, unknown>).messages)) {
normalized = normalizeResponsesBodyForCompatibility(
convertOpenAiBodyToResponsesBody(normalized, modelName, stream),
);
} else {
const prompt = asTrimmedString((normalized as Record<string, unknown>).prompt);
if (prompt) {
normalized = {
...normalized,
input: [toResponsesInputMessageFromText(prompt)],
};
}
}
}
const sanitized: Record<string, unknown> = {};
for (const [key, value] of Object.entries(normalized)) {
if (!ALLOWED_RESPONSES_FIELDS.has(key)) continue;
if (key === 'max_completion_tokens') continue;
sanitized[key] = value;
}
const maxOutputTokens = toFiniteNumber(normalized.max_output_tokens);
if (maxOutputTokens !== null && maxOutputTokens > 0) {
sanitized.max_output_tokens = Math.trunc(maxOutputTokens);
} else {
const maxCompletionTokens = toFiniteNumber(normalized.max_completion_tokens);
if (maxCompletionTokens !== null && maxCompletionTokens > 0) {
sanitized.max_output_tokens = Math.trunc(maxCompletionTokens);
}
}
sanitized.model = modelName;
sanitized.stream = stream;
return sanitized;
}
export function convertOpenAiBodyToResponsesBody(
openaiBody: Record<string, unknown>,
modelName: string,
stream: boolean,
): Record<string, unknown> {
const rawMessages = Array.isArray(openaiBody.messages) ? openaiBody.messages : [];
const systemContents: string[] = [];
const inputItems: Array<Record<string, unknown>> = [];
for (const item of rawMessages) {
if (!isRecord(item)) continue;
const role = asTrimmedString(item.role).toLowerCase() || 'user';
if (role === 'system' || role === 'developer') {
const content = extractTextContent(item.content).trim();
if (content) systemContents.push(content);
continue;
}
if (role === 'assistant') {
const normalizedContent = normalizeResponsesMessageContent('assistant', item.content);
if (normalizedContent.length > 0) {
inputItems.push({
type: 'message',
role: 'assistant',
content: normalizedContent,
});
}
const rawToolCalls = Array.isArray(item.tool_calls) ? item.tool_calls : [];
for (let index = 0; index < rawToolCalls.length; index += 1) {
const toolCall = rawToolCalls[index];
if (!isRecord(toolCall)) continue;
const functionPart = isRecord(toolCall.function) ? toolCall.function : {};
const callId = asTrimmedString(toolCall.id) || `call_${Date.now()}_${index}`;
const name = (
asTrimmedString(functionPart.name)
|| asTrimmedString(toolCall.name)
|| `tool_${index}`
);
const argumentsValue = normalizeOpenAiToolArguments(
functionPart.arguments ?? toolCall.arguments,
);
inputItems.push({
type: 'function_call',
call_id: callId,
name,
arguments: argumentsValue,
});
}
continue;
}
if (role === 'tool') {
const callId = asTrimmedString(item.tool_call_id) || asTrimmedString(item.id);
if (!callId) continue;
inputItems.push({
type: 'function_call_output',
call_id: callId,
output: normalizeToolOutput(item.content),
});
continue;
}
const normalizedContent = normalizeResponsesMessageContent('user', item.content);
if (normalizedContent.length <= 0) continue;
inputItems.push({
type: 'message',
role: 'user',
content: normalizedContent,
});
}
const maxOutputTokens = (
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 (systemContents.length > 0) {
body.instructions = systemContents.join('\n\n');
}
const temperature = toFiniteNumber(openaiBody.temperature);
if (temperature !== null) body.temperature = temperature;
const topP = toFiniteNumber(openaiBody.top_p);
if (topP !== null) body.top_p = topP;
if (openaiBody.metadata !== undefined) body.metadata = openaiBody.metadata;
if (openaiBody.reasoning !== undefined) body.reasoning = openaiBody.reasoning;
if (openaiBody.parallel_tool_calls !== undefined) body.parallel_tool_calls = openaiBody.parallel_tool_calls;
if (openaiBody.tools !== undefined) body.tools = convertOpenAiToolsToResponses(openaiBody.tools);
if (openaiBody.safety_identifier !== undefined) body.safety_identifier = openaiBody.safety_identifier;
if (openaiBody.max_tool_calls !== undefined) body.max_tool_calls = openaiBody.max_tool_calls;
if (openaiBody.prompt_cache_key !== undefined) body.prompt_cache_key = openaiBody.prompt_cache_key;
if (openaiBody.prompt_cache_retention !== undefined) {
body.prompt_cache_retention = openaiBody.prompt_cache_retention;
}
if (openaiBody.background !== undefined) body.background = openaiBody.background;
if (openaiBody.user !== undefined) body.user = openaiBody.user;
if (openaiBody.include !== undefined) body.include = cloneJsonValue(openaiBody.include);
if (openaiBody.previous_response_id !== undefined) body.previous_response_id = openaiBody.previous_response_id;
if (openaiBody.truncation !== undefined) body.truncation = openaiBody.truncation;
if (openaiBody.service_tier !== undefined) body.service_tier = openaiBody.service_tier;
if (openaiBody.top_logprobs !== undefined) body.top_logprobs = openaiBody.top_logprobs;
if (openaiBody.stream_options !== undefined) body.stream_options = openaiBody.stream_options;
const textConfig = cloneRecord(openaiBody.text) || {};
const verbosity = (
asTrimmedString(openaiBody.verbosity)
|| asTrimmedString(isRecord(openaiBody.text) ? openaiBody.text.verbosity : undefined)
);
if (verbosity) {
textConfig.verbosity = verbosity;
}
if (Object.keys(textConfig).length > 0) {
body.text = textConfig;
}
const responsesToolChoice = convertOpenAiToolChoiceToResponses(openaiBody.tool_choice);
if (responsesToolChoice !== undefined) body.tool_choice = responsesToolChoice;
return normalizeResponsesBodyForCompatibility(body);
}
type OpenAiToolCall = {
id: string;
type: 'function';
function: {
name: string;
arguments: string;
};
};
function toOpenAiToolCall(item: Record<string, unknown>, fallbackIndex: number): OpenAiToolCall | null {
const callId = (
asTrimmedString(item.call_id)
|| asTrimmedString(item.id)
|| `call_${Date.now()}_${fallbackIndex}`
);
const name = asTrimmedString(item.name);
if (!name) return null;
return {
id: callId,
type: 'function',
function: {
name,
arguments: normalizeOpenAiToolArguments(item.arguments ?? item.input),
},
};
}
function normalizeOpenAiContentBlock(item: Record<string, unknown>): string | Record<string, unknown> | null {
const type = asTrimmedString(item.type).toLowerCase();
if (!type) {
const text = extractTextContent(item).trim();
return text ? { type: 'text', text } : null;
}
if (
type === 'input_text'
|| type === 'output_text'
|| type === 'text'
|| type === 'summary_text'
|| type === 'reasoning_text'
) {
const text = extractTextContent(item).trim();
return text ? { type: 'text', text } : null;
}
if (type === 'input_image') {
const imageUrl = item.image_url ?? item.url;
if (imageUrl === undefined) return null;
return {
type: 'image_url',
image_url: imageUrl,
};
}
if (type === 'input_audio' && item.input_audio !== undefined) {
return {
type: 'input_audio',
input_audio: item.input_audio,
};
}
if (type === 'reasoning' || type === 'thinking' || type === 'redacted_reasoning') {
const text = extractTextContent(item).trim();
return text ? { type: 'text', text } : null;
}
return item;
}
function toOpenAiMessageContent(content: unknown): string | Array<string | Record<string, unknown>> {
if (typeof content === 'string') return content;
if (!Array.isArray(content)) {
if (isRecord(content)) {
const single = normalizeOpenAiContentBlock(content);
if (!single) return '';
return typeof single === 'string' ? single : [single];
}
return '';
}
const blocks = content
.map((item) => {
if (typeof item === 'string') return item.trim() ? item : null;
if (!isRecord(item)) return null;
return normalizeOpenAiContentBlock(item);
})
.filter((item): item is string | Record<string, unknown> => !!item);
if (blocks.length === 1 && typeof blocks[0] === 'string') {
return blocks[0];
}
return blocks;
}
function convertResponsesToolsToOpenAi(rawTools: unknown): unknown {
if (!Array.isArray(rawTools)) return rawTools;
return rawTools.map((item) => {
if (!isRecord(item)) return item;
const type = asTrimmedString(item.type).toLowerCase();
if (type === 'custom' || type === 'image_generation') return item;
if (type !== 'function') return item;
if (isRecord(item.function) && asTrimmedString(item.function.name)) return item;
const name = asTrimmedString(item.name);
if (!name) return item;
const fn: Record<string, unknown> = { name };
const description = asTrimmedString(item.description);
if (description) fn.description = description;
if (item.parameters !== undefined) fn.parameters = item.parameters;
if (item.strict !== undefined) fn.strict = item.strict;
return {
type: 'function',
function: fn,
};
});
}
function convertResponsesToolChoiceToOpenAi(rawToolChoice: unknown): unknown {
if (rawToolChoice === undefined) return undefined;
if (typeof rawToolChoice === 'string') return rawToolChoice;
if (!isRecord(rawToolChoice)) return rawToolChoice;
const type = asTrimmedString(rawToolChoice.type).toLowerCase();
if (type === 'function') {
if (isRecord(rawToolChoice.function) && asTrimmedString(rawToolChoice.function.name)) {
return rawToolChoice;
}
const name = asTrimmedString(rawToolChoice.name);
if (!name) return 'required';
return {
type: 'function',
function: { name },
};
}
if (type === 'auto' || type === 'none' || type === 'required') {
return type;
}
return rawToolChoice;
}
export function convertResponsesBodyToOpenAiBody(
body: Record<string, unknown>,
modelName: string,
stream: boolean,
): Record<string, unknown> {
const messages: Array<Record<string, unknown>> = [];
const input = body.input;
let functionCallIndex = 0;
let pendingToolCalls: OpenAiToolCall[] = [];
const flushPendingToolCalls = () => {
if (pendingToolCalls.length <= 0) return;
messages.push({
role: 'assistant',
content: '',
tool_calls: pendingToolCalls,
});
pendingToolCalls = [];
};
const pushToolOutputMessage = (callIdRaw: unknown, outputRaw: unknown) => {
const toolCallId = asTrimmedString(callIdRaw);
if (!toolCallId) return;
messages.push({
role: 'tool',
tool_call_id: toolCallId,
content: normalizeToolOutput(outputRaw),
});
};
const processInputItem = (item: unknown) => {
if (typeof item === 'string') {
flushPendingToolCalls();
const text = item.trim();
if (text) messages.push({ role: 'user', content: text });
return;
}
if (!isRecord(item)) return;
const itemType = asTrimmedString(item.type).toLowerCase();
if (itemType === 'function_call' || itemType === 'custom_tool_call') {
const toolCall = toOpenAiToolCall(item, functionCallIndex);
functionCallIndex += 1;
if (toolCall) pendingToolCalls.push(toolCall);
return;
}
if (itemType === 'function_call_output' || itemType === 'custom_tool_call_output') {
flushPendingToolCalls();
pushToolOutputMessage(item.call_id ?? item.id, item.output ?? item.content);
return;
}
if (itemType === 'reasoning') {
flushPendingToolCalls();
const reasoningContent = toOpenAiMessageContent(item.summary ?? item.content ?? item);
const hasReasoningContent = typeof reasoningContent === 'string'
? reasoningContent.trim().length > 0
: Array.isArray(reasoningContent) && reasoningContent.length > 0;
if (!hasReasoningContent) return;
messages.push({
role: 'assistant',
content: reasoningContent,
});
return;
}
flushPendingToolCalls();
const role = asTrimmedString(item.role).toLowerCase() || 'user';
const normalizedRole = role === 'developer' ? 'system' : role;
const content = toOpenAiMessageContent(item.content ?? item.input ?? item);
if (normalizedRole === 'tool') {
pushToolOutputMessage(item.tool_call_id ?? item.call_id ?? item.id, item.content);
return;
}
const hasContent = typeof content === 'string'
? content.trim().length > 0
: Array.isArray(content) && content.length > 0;
if (!hasContent) return;
messages.push({
role: normalizedRole,
content,
});
};
if (typeof input === 'string') {
const text = input.trim();
if (text) messages.push({ role: 'user', content: text });
} else if (Array.isArray(input)) {
for (const item of input) processInputItem(item);
} else if (isRecord(input)) {
processInputItem(input);
}
flushPendingToolCalls();
const instructions = asTrimmedString(body.instructions);
if (instructions) {
messages.unshift({ role: 'system', content: instructions });
}
const payload: Record<string, unknown> = {
model: modelName,
stream,
messages,
};
if (typeof body.temperature === 'number' && Number.isFinite(body.temperature)) {
payload.temperature = body.temperature;
}
if (typeof body.top_p === 'number' && Number.isFinite(body.top_p)) {
payload.top_p = body.top_p;
}
if (typeof body.max_output_tokens === 'number' && Number.isFinite(body.max_output_tokens)) {
payload.max_tokens = body.max_output_tokens;
}
if (body.parallel_tool_calls !== undefined) payload.parallel_tool_calls = body.parallel_tool_calls;
if (body.tools !== undefined) payload.tools = convertResponsesToolsToOpenAi(body.tools);
if (body.tool_choice !== undefined) payload.tool_choice = convertResponsesToolChoiceToOpenAi(body.tool_choice);
if (body.safety_identifier !== undefined) payload.safety_identifier = body.safety_identifier;
if (body.max_tool_calls !== undefined) payload.max_tool_calls = body.max_tool_calls;
if (body.prompt_cache_key !== undefined) payload.prompt_cache_key = body.prompt_cache_key;
if (body.prompt_cache_retention !== undefined) payload.prompt_cache_retention = body.prompt_cache_retention;
if (body.background !== undefined) payload.background = body.background;
if (body.user !== undefined) payload.user = body.user;
if (body.include !== undefined) payload.include = cloneJsonValue(body.include);
if (body.previous_response_id !== undefined) payload.previous_response_id = body.previous_response_id;
if (body.truncation !== undefined) payload.truncation = body.truncation;
if (body.service_tier !== undefined) payload.service_tier = body.service_tier;
if (body.top_logprobs !== undefined) payload.top_logprobs = body.top_logprobs;
if (body.stream_options !== undefined) payload.stream_options = body.stream_options;
if (isRecord(body.text) && asTrimmedString(body.text.verbosity)) {
payload.verbosity = asTrimmedString(body.text.verbosity);
}
return payload;
}
@@ -0,0 +1,6 @@
import { normalizeResponsesInputForCompatibility, normalizeResponsesMessageItem } from './compatibility.js';
export const openAiResponsesInbound = {
normalizeInput: normalizeResponsesInputForCompatibility,
normalizeMessage: normalizeResponsesMessageItem,
};
@@ -0,0 +1,66 @@
import { type StreamTransformContext } from '../../shared/normalized.js';
import {
convertOpenAiBodyToResponsesBody,
convertResponsesBodyToOpenAiBody,
normalizeResponsesInputForCompatibility,
normalizeResponsesMessageContent,
sanitizeResponsesBodyForProxy,
} from './conversion.js';
import { normalizeResponsesMessageItem } from './compatibility.js';
import {
type OpenAiResponsesAggregateState,
completeResponsesStream,
createOpenAiResponsesAggregateState,
failResponsesStream,
serializeConvertedResponsesEvents,
} from './aggregator.js';
import { openAiResponsesOutbound } from './outbound.js';
import { openAiResponsesStream } from './stream.js';
import { openAiResponsesUsage } from './usage.js';
export const openAiResponsesTransformer = {
protocol: 'openai/responses' as const,
inbound: {
normalizeInput: normalizeResponsesInputForCompatibility,
normalizeMessage: normalizeResponsesMessageItem,
normalizeContent: normalizeResponsesMessageContent,
sanitizeProxyBody: sanitizeResponsesBodyForProxy,
fromOpenAiBody: convertOpenAiBodyToResponsesBody,
toOpenAiBody: convertResponsesBodyToOpenAiBody,
},
outbound: openAiResponsesOutbound,
stream: openAiResponsesStream,
usage: openAiResponsesUsage,
aggregator: {
createState: createOpenAiResponsesAggregateState,
serialize: serializeConvertedResponsesEvents,
complete: completeResponsesStream,
fail: failResponsesStream,
},
transformRequest(body: unknown) {
return body;
},
createStreamContext(modelName: string): StreamTransformContext {
return openAiResponsesStream.createContext(modelName);
},
transformFinalResponse(payload: unknown, modelName: string, fallbackText = '') {
return openAiResponsesOutbound.normalizeFinal(payload, modelName, fallbackText);
},
transformStreamEvent(payload: unknown, context: StreamTransformContext, modelName: string) {
return openAiResponsesStream.normalizeEvent(payload, context, modelName);
},
pullSseEvents(buffer: string) {
return openAiResponsesStream.pullSseEvents(buffer);
},
};
export type OpenAiResponsesTransformer = typeof openAiResponsesTransformer;
export type OpenAiResponsesAggregate = OpenAiResponsesAggregateState;
export {
convertOpenAiBodyToResponsesBody,
convertResponsesBodyToOpenAiBody,
normalizeResponsesInputForCompatibility,
normalizeResponsesMessageContent,
normalizeResponsesMessageItem,
sanitizeResponsesBodyForProxy,
};
@@ -0,0 +1,182 @@
import {
normalizeUpstreamFinalResponse,
type NormalizedFinalResponse,
} from '../../shared/normalized.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 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;
};
export type ResponsesToolCall = {
id: string;
name: string;
arguments: string;
};
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 [];
}
export function normalizeResponsesFinalPayload(
payload: unknown,
modelName: string,
fallbackText = '',
): NormalizedFinalResponse {
return normalizeUpstreamFinalResponse(payload, modelName, fallbackText);
}
export function serializeResponsesFinalPayload(input: {
upstreamPayload: unknown;
normalized: NormalizedFinalResponse;
usage: ResponsesUsageSummary;
}): Record<string, unknown> {
const { upstreamPayload, normalized, usage } = input;
if (isRecord(upstreamPayload) && 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 toolCalls = extractToolCallsFromUpstream(upstreamPayload);
const output: Array<Record<string, unknown>> = [];
if (normalized.reasoningContent) {
output.push({
id: ensureMessageId(`${normalizedId}_reasoning`),
type: 'reasoning',
status: 'completed',
summary: [{
type: 'summary_text',
text: normalized.reasoningContent,
}],
});
}
if (normalized.content || toolCalls.length === 0) {
output.push({
id: messageId,
type: 'message',
role: 'assistant',
status: 'completed',
content: [{
type: 'output_text',
text: normalized.content || '',
}],
});
}
for (const toolCall of toolCalls) {
output.push({
id: toolCall.id,
type: 'function_call',
status: 'completed',
call_id: toolCall.id,
name: toolCall.name,
arguments: toolCall.arguments,
});
}
return {
id: responseId,
object: 'response',
created: normalized.created,
status: 'completed',
model: normalized.model,
output,
output_text: normalized.content || '',
usage: {
input_tokens: usage.promptTokens,
output_tokens: usage.completionTokens,
total_tokens: usage.totalTokens,
},
};
}
export const openAiResponsesOutbound = {
normalizeFinal: normalizeResponsesFinalPayload,
serializeFinal: serializeResponsesFinalPayload,
};
export {
serializeResponsesFinalPayload as toResponsesPayload,
};
@@ -0,0 +1,80 @@
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.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,
};
@@ -0,0 +1,6 @@
import { createEmptyNormalizedUsage, mergeNormalizedUsage } from '../../shared/normalized.js';
export const openAiResponsesUsage = {
empty: createEmptyNormalizedUsage,
merge: mergeNormalizedUsage,
};