forked from boos/BoosAPI
feat: enhance response serialization for compact mode
- Added support for preserving native response.compaction payloads when compact serialization is requested in `serializeResponsesFinalPayload`. - Implemented serialization of compact mode as `response.compaction` instead of an ordinary response object. - Introduced a new type `ResponsesFinalSerializationMode` to manage serialization modes. - Refactored `serializeResponsesFinalPayload` to handle different serialization modes and usage payloads. - Added tests to ensure correct behavior for compact serialization and preservation of upstream payloads. - Updated favicon assets and documentation configuration for VitePress.
This commit is contained in:
@@ -114,6 +114,7 @@ describe('proxy route architecture boundaries', () => {
|
||||
expect(source).not.toContain('const consumeSseBuffer = (incoming: string): string => {');
|
||||
expect(source).not.toContain('openAiResponsesTransformer.aggregator.complete(');
|
||||
expect(source).not.toContain('reply.raw.end();');
|
||||
expect(source).toContain('reply.hijack();');
|
||||
expect(source).toContain('openAiResponsesTransformer.proxyStream.createSession(');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -137,4 +137,49 @@ describe('responses proxy compact upstream routing', () => {
|
||||
const [targetUrl] = fetchMock.mock.calls[0] as [string, any];
|
||||
expect(targetUrl).toContain('/v1/responses/compact');
|
||||
});
|
||||
|
||||
it('preserves native response.compaction payloads instead of coercing them into object=response', async () => {
|
||||
fetchMock.mockResolvedValue(new Response(JSON.stringify({
|
||||
id: 'cmp_123',
|
||||
object: 'response.compaction',
|
||||
input_tokens: 1234,
|
||||
output_tokens: 321,
|
||||
total_tokens: 1555,
|
||||
output: [
|
||||
{
|
||||
id: 'rs_123',
|
||||
type: 'compaction',
|
||||
encrypted_content: 'enc-compact-payload',
|
||||
},
|
||||
],
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
}));
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/responses/compact',
|
||||
payload: {
|
||||
model: 'gpt-5.2',
|
||||
input: 'hello',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toEqual({
|
||||
id: 'cmp_123',
|
||||
object: 'response.compaction',
|
||||
input_tokens: 1234,
|
||||
output_tokens: 321,
|
||||
total_tokens: 1555,
|
||||
output: [
|
||||
{
|
||||
id: 'rs_123',
|
||||
type: 'compaction',
|
||||
encrypted_content: 'enc-compact-payload',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,4 +29,24 @@ describe('responses proxy compact route', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects streaming /v1/responses/compact requests because compact is non-streaming', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/responses/compact',
|
||||
payload: {
|
||||
model: 'gpt-5.2',
|
||||
input: 'hello',
|
||||
stream: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.json()).toEqual({
|
||||
error: {
|
||||
message: 'stream is not supported on /v1/responses/compact',
|
||||
type: 'invalid_request_error',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -502,4 +502,265 @@ describe('serializeConvertedResponsesEvents', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not synthesize duplicate message done events after original text completion already arrived', () => {
|
||||
const state = createOpenAiResponsesAggregateState('gpt-5');
|
||||
const streamContext = createStreamTransformContext('gpt-5');
|
||||
const usage = {
|
||||
promptTokens: 2,
|
||||
completionTokens: 4,
|
||||
totalTokens: 6,
|
||||
};
|
||||
|
||||
serializeConvertedResponsesEvents({
|
||||
state,
|
||||
streamContext,
|
||||
usage,
|
||||
event: {
|
||||
responsesEventType: 'response.output_item.added',
|
||||
responsesPayload: {
|
||||
type: 'response.output_item.added',
|
||||
output_index: 0,
|
||||
item: {
|
||||
id: 'msg_1',
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
status: 'in_progress',
|
||||
content: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
serializeConvertedResponsesEvents({
|
||||
state,
|
||||
streamContext,
|
||||
usage,
|
||||
event: {
|
||||
responsesEventType: 'response.content_part.added',
|
||||
responsesPayload: {
|
||||
type: 'response.content_part.added',
|
||||
output_index: 0,
|
||||
item_id: 'msg_1',
|
||||
content_index: 0,
|
||||
part: {
|
||||
type: 'output_text',
|
||||
text: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
serializeConvertedResponsesEvents({
|
||||
state,
|
||||
streamContext,
|
||||
usage,
|
||||
event: {
|
||||
responsesEventType: 'response.output_text.delta',
|
||||
responsesPayload: {
|
||||
type: 'response.output_text.delta',
|
||||
output_index: 0,
|
||||
item_id: 'msg_1',
|
||||
delta: 'hello ',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
serializeConvertedResponsesEvents({
|
||||
state,
|
||||
streamContext,
|
||||
usage,
|
||||
event: {
|
||||
responsesEventType: 'response.output_text.done',
|
||||
responsesPayload: {
|
||||
type: 'response.output_text.done',
|
||||
output_index: 0,
|
||||
item_id: 'msg_1',
|
||||
text: 'hello world',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
serializeConvertedResponsesEvents({
|
||||
state,
|
||||
streamContext,
|
||||
usage,
|
||||
event: {
|
||||
responsesEventType: 'response.content_part.done',
|
||||
responsesPayload: {
|
||||
type: 'response.content_part.done',
|
||||
output_index: 0,
|
||||
item_id: 'msg_1',
|
||||
content_index: 0,
|
||||
part: {
|
||||
type: 'output_text',
|
||||
text: 'hello world',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const completionLines = completeResponsesStream(state, streamContext, usage);
|
||||
const events = parseSseEvents(completionLines);
|
||||
|
||||
expect(events.map((entry) => entry.event ?? (entry.payload === '[DONE]' ? '[DONE]' : 'data'))).toEqual([
|
||||
'response.output_item.done',
|
||||
'response.completed',
|
||||
'[DONE]',
|
||||
]);
|
||||
|
||||
expect(events[0]?.payload).toMatchObject({
|
||||
type: 'response.output_item.done',
|
||||
output_index: 0,
|
||||
item: {
|
||||
id: 'msg_1',
|
||||
type: 'message',
|
||||
status: 'completed',
|
||||
},
|
||||
});
|
||||
expect(events[1]?.payload).toMatchObject({
|
||||
type: 'response.completed',
|
||||
response: {
|
||||
status: 'completed',
|
||||
output_text: 'hello world',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not synthesize duplicate reasoning done events after original reasoning completion already arrived', () => {
|
||||
const state = createOpenAiResponsesAggregateState('gpt-5');
|
||||
const streamContext = createStreamTransformContext('gpt-5');
|
||||
const usage = {
|
||||
promptTokens: 2,
|
||||
completionTokens: 4,
|
||||
totalTokens: 6,
|
||||
};
|
||||
|
||||
serializeConvertedResponsesEvents({
|
||||
state,
|
||||
streamContext,
|
||||
usage,
|
||||
event: {
|
||||
responsesEventType: 'response.output_item.added',
|
||||
responsesPayload: {
|
||||
type: 'response.output_item.added',
|
||||
output_index: 0,
|
||||
item: {
|
||||
id: 'rs_1',
|
||||
type: 'reasoning',
|
||||
status: 'in_progress',
|
||||
summary: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
serializeConvertedResponsesEvents({
|
||||
state,
|
||||
streamContext,
|
||||
usage,
|
||||
event: {
|
||||
responsesEventType: 'response.reasoning_summary_part.added',
|
||||
responsesPayload: {
|
||||
type: 'response.reasoning_summary_part.added',
|
||||
item_id: 'rs_1',
|
||||
output_index: 0,
|
||||
summary_index: 0,
|
||||
part: {
|
||||
type: 'summary_text',
|
||||
text: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
serializeConvertedResponsesEvents({
|
||||
state,
|
||||
streamContext,
|
||||
usage,
|
||||
event: {
|
||||
responsesEventType: 'response.reasoning_summary_text.delta',
|
||||
responsesPayload: {
|
||||
type: 'response.reasoning_summary_text.delta',
|
||||
item_id: 'rs_1',
|
||||
output_index: 0,
|
||||
summary_index: 0,
|
||||
delta: 'plan ',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
serializeConvertedResponsesEvents({
|
||||
state,
|
||||
streamContext,
|
||||
usage,
|
||||
event: {
|
||||
responsesEventType: 'response.reasoning_summary_text.done',
|
||||
responsesPayload: {
|
||||
type: 'response.reasoning_summary_text.done',
|
||||
item_id: 'rs_1',
|
||||
output_index: 0,
|
||||
summary_index: 0,
|
||||
text: 'plan first',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
serializeConvertedResponsesEvents({
|
||||
state,
|
||||
streamContext,
|
||||
usage,
|
||||
event: {
|
||||
responsesEventType: 'response.reasoning_summary_part.done',
|
||||
responsesPayload: {
|
||||
type: 'response.reasoning_summary_part.done',
|
||||
item_id: 'rs_1',
|
||||
output_index: 0,
|
||||
summary_index: 0,
|
||||
part: {
|
||||
type: 'summary_text',
|
||||
text: 'plan first',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const completionLines = completeResponsesStream(state, streamContext, usage);
|
||||
const events = parseSseEvents(completionLines);
|
||||
|
||||
expect(events.map((entry) => entry.event ?? (entry.payload === '[DONE]' ? '[DONE]' : 'data'))).toEqual([
|
||||
'response.output_item.done',
|
||||
'response.completed',
|
||||
'[DONE]',
|
||||
]);
|
||||
|
||||
expect(events[0]?.payload).toMatchObject({
|
||||
type: 'response.output_item.done',
|
||||
output_index: 0,
|
||||
item: {
|
||||
id: 'rs_1',
|
||||
type: 'reasoning',
|
||||
status: 'completed',
|
||||
},
|
||||
});
|
||||
expect(events[1]?.payload).toMatchObject({
|
||||
type: 'response.completed',
|
||||
response: {
|
||||
status: 'completed',
|
||||
output: [
|
||||
{
|
||||
id: 'rs_1',
|
||||
type: 'reasoning',
|
||||
status: 'completed',
|
||||
summary: [
|
||||
{
|
||||
type: 'summary_text',
|
||||
text: 'plan first',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,6 +55,12 @@ type ResponsesUsageSummary = {
|
||||
};
|
||||
|
||||
type AggregateOutputItem = Record<string, unknown>;
|
||||
type AggregateOutputMetaCarrier = AggregateOutputItem & { [key: symbol]: true | undefined };
|
||||
|
||||
const outputTextDoneMarker = Symbol('responses.output_text.done');
|
||||
const contentPartDoneMarker = Symbol('responses.content_part.done');
|
||||
const reasoningSummaryTextDoneMarker = Symbol('responses.reasoning_summary_text.done');
|
||||
const reasoningSummaryPartDoneMarker = Symbol('responses.reasoning_summary_part.done');
|
||||
|
||||
export type OpenAiResponsesAggregateState = {
|
||||
modelName: string;
|
||||
@@ -90,6 +96,14 @@ export function createOpenAiResponsesAggregateState(modelName: string): OpenAiRe
|
||||
};
|
||||
}
|
||||
|
||||
function markTerminalMarker(item: AggregateOutputItem, marker: symbol): void {
|
||||
(item as AggregateOutputMetaCarrier)[marker] = true;
|
||||
}
|
||||
|
||||
function hasTerminalMarker(item: AggregateOutputItem, marker: symbol): boolean {
|
||||
return (item as AggregateOutputMetaCarrier)[marker] === true;
|
||||
}
|
||||
|
||||
function mergeUsageExtras(
|
||||
state: OpenAiResponsesAggregateState,
|
||||
usagePayload: unknown,
|
||||
@@ -523,8 +537,20 @@ function applyOriginalResponsesPayload(
|
||||
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;
|
||||
const existingPart = isRecord(content[contentIndex]) ? content[contentIndex] as AggregateOutputItem : null;
|
||||
const incomingPart = cloneRecord(payload.part);
|
||||
if (incomingPart) {
|
||||
content[contentIndex] = existingPart
|
||||
? {
|
||||
...existingPart,
|
||||
...incomingPart,
|
||||
}
|
||||
: incomingPart;
|
||||
}
|
||||
const part = isRecord(content[contentIndex]) ? content[contentIndex] as AggregateOutputItem : existingPart;
|
||||
if (eventType === 'response.content_part.done' && part) {
|
||||
markTerminalMarker(part, contentPartDoneMarker);
|
||||
}
|
||||
return serializeOriginalResponsesEvent(eventType, payload);
|
||||
}
|
||||
case 'response.output_text.delta':
|
||||
@@ -533,6 +559,7 @@ function applyOriginalResponsesPayload(
|
||||
const textPart = ensureMessageOutputTextPart(state, outputIndex);
|
||||
if (eventType === 'response.output_text.done') {
|
||||
textPart.part.text = typeof payload.text === 'string' ? payload.text : String(payload.text ?? '');
|
||||
markTerminalMarker(textPart.part, outputTextDoneMarker);
|
||||
} else {
|
||||
textPart.part.text = `${typeof textPart.part.text === 'string' ? textPart.part.text : ''}${typeof payload.delta === 'string' ? payload.delta : ''}`;
|
||||
}
|
||||
@@ -580,6 +607,11 @@ function applyOriginalResponsesPayload(
|
||||
...part,
|
||||
};
|
||||
summaryState.item.summary = summary;
|
||||
if (eventType === 'response.reasoning_summary_part.done') {
|
||||
markTerminalMarker(summary[summaryState.summaryIndex] as AggregateOutputItem, reasoningSummaryPartDoneMarker);
|
||||
}
|
||||
} else if (eventType === 'response.reasoning_summary_part.done') {
|
||||
markTerminalMarker(summaryState.summary, reasoningSummaryPartDoneMarker);
|
||||
}
|
||||
return serializeOriginalResponsesEvent(eventType, payload);
|
||||
}
|
||||
@@ -588,6 +620,7 @@ function applyOriginalResponsesPayload(
|
||||
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 ?? '');
|
||||
markTerminalMarker(summaryState.summary, reasoningSummaryTextDoneMarker);
|
||||
} else {
|
||||
summaryState.summary.text = `${typeof summaryState.summary.text === 'string' ? summaryState.summary.text : ''}${typeof payload.delta === 'string' ? payload.delta : ''}`;
|
||||
}
|
||||
@@ -790,39 +823,51 @@ function buildSyntheticTerminalItemDoneEvents(
|
||||
const part = content[0];
|
||||
if (isRecord(part)) {
|
||||
const text = typeof part.text === 'string' ? part.text : String(part.text ?? '');
|
||||
lines.push(serializeSse('response.output_text.done', {
|
||||
type: 'response.output_text.done',
|
||||
output_index: index,
|
||||
item_id: itemId,
|
||||
text,
|
||||
}));
|
||||
lines.push(serializeSse('response.content_part.done', {
|
||||
type: 'response.content_part.done',
|
||||
output_index: index,
|
||||
item_id: itemId,
|
||||
content_index: 0,
|
||||
part: cloneJson(part),
|
||||
}));
|
||||
if (!hasTerminalMarker(part, outputTextDoneMarker)) {
|
||||
lines.push(serializeSse('response.output_text.done', {
|
||||
type: 'response.output_text.done',
|
||||
output_index: index,
|
||||
item_id: itemId,
|
||||
text,
|
||||
}));
|
||||
markTerminalMarker(part, outputTextDoneMarker);
|
||||
}
|
||||
if (!hasTerminalMarker(part, contentPartDoneMarker)) {
|
||||
lines.push(serializeSse('response.content_part.done', {
|
||||
type: 'response.content_part.done',
|
||||
output_index: index,
|
||||
item_id: itemId,
|
||||
content_index: 0,
|
||||
part: cloneJson(part),
|
||||
}));
|
||||
markTerminalMarker(part, contentPartDoneMarker);
|
||||
}
|
||||
}
|
||||
} else if (itemType === 'reasoning') {
|
||||
const summary = Array.isArray(item.summary) ? item.summary as AggregateOutputItem[] : [];
|
||||
const part = summary[0];
|
||||
if (isRecord(part)) {
|
||||
const text = typeof part.text === 'string' ? part.text : String(part.text ?? '');
|
||||
lines.push(serializeSse('response.reasoning_summary_text.done', {
|
||||
type: 'response.reasoning_summary_text.done',
|
||||
item_id: itemId,
|
||||
output_index: index,
|
||||
summary_index: 0,
|
||||
text,
|
||||
}));
|
||||
lines.push(serializeSse('response.reasoning_summary_part.done', {
|
||||
type: 'response.reasoning_summary_part.done',
|
||||
item_id: itemId,
|
||||
output_index: index,
|
||||
summary_index: 0,
|
||||
part: cloneJson(part),
|
||||
}));
|
||||
if (!hasTerminalMarker(part, reasoningSummaryTextDoneMarker)) {
|
||||
lines.push(serializeSse('response.reasoning_summary_text.done', {
|
||||
type: 'response.reasoning_summary_text.done',
|
||||
item_id: itemId,
|
||||
output_index: index,
|
||||
summary_index: 0,
|
||||
text,
|
||||
}));
|
||||
markTerminalMarker(part, reasoningSummaryTextDoneMarker);
|
||||
}
|
||||
if (!hasTerminalMarker(part, reasoningSummaryPartDoneMarker)) {
|
||||
lines.push(serializeSse('response.reasoning_summary_part.done', {
|
||||
type: 'response.reasoning_summary_part.done',
|
||||
item_id: itemId,
|
||||
output_index: index,
|
||||
summary_index: 0,
|
||||
part: cloneJson(part),
|
||||
}));
|
||||
markTerminalMarker(part, reasoningSummaryPartDoneMarker);
|
||||
}
|
||||
}
|
||||
} else if (itemType === 'function_call') {
|
||||
const callId = asTrimmedString(item.call_id) || itemId;
|
||||
|
||||
@@ -3,6 +3,112 @@ import { describe, expect, it } from 'vitest';
|
||||
import { serializeResponsesFinalPayload } from './outbound.js';
|
||||
|
||||
describe('serializeResponsesFinalPayload', () => {
|
||||
it('preserves native response.compaction payloads when compact serialization is requested', () => {
|
||||
const upstreamPayload = {
|
||||
id: 'cmp_123',
|
||||
object: 'response.compaction',
|
||||
created_at: 1700000000,
|
||||
output: [
|
||||
{
|
||||
id: 'rs_123',
|
||||
type: 'compaction',
|
||||
encrypted_content: 'enc-compact-payload',
|
||||
},
|
||||
],
|
||||
usage: {
|
||||
input_tokens: 1234,
|
||||
output_tokens: 321,
|
||||
total_tokens: 1555,
|
||||
},
|
||||
};
|
||||
|
||||
const payload = serializeResponsesFinalPayload({
|
||||
upstreamPayload,
|
||||
normalized: {
|
||||
id: 'cmp_123',
|
||||
model: 'gpt-5',
|
||||
created: 1700000000,
|
||||
content: '',
|
||||
reasoningContent: '',
|
||||
finishReason: 'stop',
|
||||
toolCalls: [],
|
||||
},
|
||||
usage: {
|
||||
promptTokens: 1234,
|
||||
completionTokens: 321,
|
||||
totalTokens: 1555,
|
||||
},
|
||||
serializationMode: 'compact',
|
||||
} as any);
|
||||
|
||||
expect(payload).toEqual(upstreamPayload);
|
||||
});
|
||||
|
||||
it('serializes compact mode as response.compaction instead of an ordinary response object', () => {
|
||||
const payload = serializeResponsesFinalPayload({
|
||||
upstreamPayload: {
|
||||
id: 'chatcmpl_compact',
|
||||
object: 'chat.completion',
|
||||
created: 1700000000,
|
||||
model: 'gpt-5',
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
finish_reason: 'stop',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: 'hello',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
normalized: {
|
||||
id: 'chatcmpl_compact',
|
||||
model: 'gpt-5',
|
||||
created: 1700000000,
|
||||
content: 'hello',
|
||||
reasoningContent: '',
|
||||
finishReason: 'stop',
|
||||
toolCalls: [],
|
||||
},
|
||||
usage: {
|
||||
promptTokens: 11,
|
||||
completionTokens: 7,
|
||||
totalTokens: 18,
|
||||
},
|
||||
serializationMode: 'compact',
|
||||
} as any);
|
||||
|
||||
expect(payload).toEqual({
|
||||
id: 'resp_chatcmpl_compact',
|
||||
object: 'response.compaction',
|
||||
created_at: 1700000000,
|
||||
output: [
|
||||
{
|
||||
id: 'msg_chatcmpl_compact',
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
status: 'completed',
|
||||
content: [
|
||||
{
|
||||
type: 'output_text',
|
||||
text: 'hello',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
usage: {
|
||||
input_tokens: 11,
|
||||
output_tokens: 7,
|
||||
total_tokens: 18,
|
||||
},
|
||||
});
|
||||
expect(payload).not.toHaveProperty('created');
|
||||
expect(payload).not.toHaveProperty('status');
|
||||
expect(payload).not.toHaveProperty('model');
|
||||
expect(payload).not.toHaveProperty('output_text');
|
||||
});
|
||||
|
||||
it('preserves top-level chat annotations on synthetic assistant messages', () => {
|
||||
const payload = serializeResponsesFinalPayload({
|
||||
upstreamPayload: {
|
||||
|
||||
@@ -64,6 +64,8 @@ export type ResponsesToolCall = {
|
||||
arguments: string;
|
||||
};
|
||||
|
||||
export type ResponsesFinalSerializationMode = 'response' | 'compact';
|
||||
|
||||
type ResponsesOutputItem = Record<string, unknown>;
|
||||
|
||||
function extractToolCallsFromUpstream(payload: unknown): ResponsesToolCall[] {
|
||||
@@ -280,14 +282,45 @@ export function normalizeResponsesFinalPayload(
|
||||
return normalizeUpstreamFinalResponse(payload, modelName, fallbackText);
|
||||
}
|
||||
|
||||
function buildResponsesUsagePayload(
|
||||
usage: ResponsesUsageSummary,
|
||||
syntheticUsageDetails: {
|
||||
inputTokensDetails?: Record<string, unknown>;
|
||||
outputTokensDetails?: Record<string, unknown>;
|
||||
},
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
input_tokens: usage.promptTokens,
|
||||
output_tokens: usage.completionTokens,
|
||||
total_tokens: usage.totalTokens,
|
||||
...(usage.inputTokensDetails ?? syntheticUsageDetails.inputTokensDetails
|
||||
? { input_tokens_details: cloneJson(usage.inputTokensDetails ?? syntheticUsageDetails.inputTokensDetails) }
|
||||
: {}),
|
||||
...(usage.outputTokensDetails ?? syntheticUsageDetails.outputTokensDetails
|
||||
? { output_tokens_details: cloneJson(usage.outputTokensDetails ?? syntheticUsageDetails.outputTokensDetails) }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function serializeResponsesFinalPayload(input: {
|
||||
upstreamPayload: unknown;
|
||||
normalized: NormalizedFinalResponse;
|
||||
usage: ResponsesUsageSummary;
|
||||
serializationMode?: ResponsesFinalSerializationMode;
|
||||
}): Record<string, unknown> {
|
||||
const { upstreamPayload, normalized, usage } = input;
|
||||
if (isRecord(upstreamPayload) && upstreamPayload.object === 'response') {
|
||||
return upstreamPayload;
|
||||
const {
|
||||
upstreamPayload,
|
||||
normalized,
|
||||
usage,
|
||||
serializationMode = 'response',
|
||||
} = input;
|
||||
if (isRecord(upstreamPayload)) {
|
||||
if (upstreamPayload.object === 'response.compaction') {
|
||||
return upstreamPayload;
|
||||
}
|
||||
if (serializationMode === 'response' && upstreamPayload.object === 'response') {
|
||||
return upstreamPayload;
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedId = typeof normalized.id === 'string' && normalized.id.trim()
|
||||
@@ -371,6 +404,17 @@ export function serializeResponsesFinalPayload(input: {
|
||||
}
|
||||
}
|
||||
|
||||
const usagePayload = buildResponsesUsagePayload(usage, syntheticUsageDetails);
|
||||
if (serializationMode === 'compact') {
|
||||
return {
|
||||
id: responseId,
|
||||
object: 'response.compaction',
|
||||
created_at: normalized.created,
|
||||
output,
|
||||
usage: usagePayload,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: responseId,
|
||||
object: 'response',
|
||||
@@ -379,17 +423,7 @@ export function serializeResponsesFinalPayload(input: {
|
||||
model: normalized.model,
|
||||
output,
|
||||
output_text: normalized.content || collectOutputTextFromItems(output),
|
||||
usage: {
|
||||
input_tokens: usage.promptTokens,
|
||||
output_tokens: usage.completionTokens,
|
||||
total_tokens: usage.totalTokens,
|
||||
...(usage.inputTokensDetails ?? syntheticUsageDetails.inputTokensDetails
|
||||
? { input_tokens_details: cloneJson(usage.inputTokensDetails ?? syntheticUsageDetails.inputTokensDetails) }
|
||||
: {}),
|
||||
...(usage.outputTokensDetails ?? syntheticUsageDetails.outputTokensDetails
|
||||
? { output_tokens_details: cloneJson(usage.outputTokensDetails ?? syntheticUsageDetails.outputTokensDetails) }
|
||||
: {}),
|
||||
},
|
||||
usage: usagePayload,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user