forked from boos/BoosAPI
[codex] fix responses-to-chat tool stream fidelity (#378)
* fix responses chat tool stream fidelity * fix responses tool call follow-up review comments
This commit is contained in:
@@ -18,6 +18,18 @@ function asRecord(value: unknown): Record<string, unknown> | null {
|
|||||||
return isRecord(value) ? value : null;
|
return isRecord(value) ? value : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 toStringArray(value: unknown): string[] {
|
function toStringArray(value: unknown): string[] {
|
||||||
if (!Array.isArray(value)) return [];
|
if (!Array.isArray(value)) return [];
|
||||||
return value
|
return value
|
||||||
@@ -375,11 +387,19 @@ function extractCitationsFromPayload(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function extractChatUsageDetails(payload: Record<string, unknown>): OpenAiChatUsageDetails | undefined {
|
export function extractChatUsageDetails(payload: Record<string, unknown>): OpenAiChatUsageDetails | undefined {
|
||||||
const usage = asRecord(payload.usage);
|
const usage = asRecord(payload.usage) ?? asRecord(asRecord(payload.response)?.usage);
|
||||||
if (!usage) return undefined;
|
if (!usage) return undefined;
|
||||||
|
|
||||||
const promptDetails = toNumericRecord(usage.prompt_tokens_details ?? usage.promptTokensDetails);
|
const promptDetails = toNumericRecord(
|
||||||
const completionDetails = toNumericRecord(usage.completion_tokens_details ?? usage.completionTokensDetails);
|
usage.prompt_tokens_details
|
||||||
|
?? usage.promptTokensDetails
|
||||||
|
?? usage.input_tokens_details,
|
||||||
|
);
|
||||||
|
const completionDetails = toNumericRecord(
|
||||||
|
usage.completion_tokens_details
|
||||||
|
?? usage.completionTokensDetails
|
||||||
|
?? usage.output_tokens_details,
|
||||||
|
);
|
||||||
if (!promptDetails && !completionDetails) return undefined;
|
if (!promptDetails && !completionDetails) return undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -416,9 +436,25 @@ export function mergeChatUsageDetails(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function extractUsagePayload(payload: Record<string, unknown>): Record<string, unknown> | undefined {
|
export function extractUsagePayload(payload: Record<string, unknown>): Record<string, unknown> | undefined {
|
||||||
const usage = asRecord(payload.usage);
|
const usage = asRecord(payload.usage) ?? asRecord(asRecord(payload.response)?.usage);
|
||||||
if (!usage) return undefined;
|
if (!usage) return undefined;
|
||||||
return usage;
|
const mappedUsage = cloneJson(usage);
|
||||||
|
|
||||||
|
const promptTokens = mappedUsage.prompt_tokens ?? mappedUsage.input_tokens;
|
||||||
|
const completionTokens = mappedUsage.completion_tokens ?? mappedUsage.output_tokens;
|
||||||
|
if (promptTokens !== undefined) mappedUsage.prompt_tokens = promptTokens;
|
||||||
|
if (completionTokens !== undefined) mappedUsage.completion_tokens = completionTokens;
|
||||||
|
delete mappedUsage.input_tokens;
|
||||||
|
delete mappedUsage.output_tokens;
|
||||||
|
|
||||||
|
const promptDetails = mappedUsage.prompt_tokens_details ?? mappedUsage.input_tokens_details;
|
||||||
|
const completionDetails = mappedUsage.completion_tokens_details ?? mappedUsage.output_tokens_details;
|
||||||
|
if (promptDetails !== undefined) mappedUsage.prompt_tokens_details = cloneJson(promptDetails);
|
||||||
|
if (completionDetails !== undefined) mappedUsage.completion_tokens_details = cloneJson(completionDetails);
|
||||||
|
delete mappedUsage.input_tokens_details;
|
||||||
|
delete mappedUsage.output_tokens_details;
|
||||||
|
|
||||||
|
return mappedUsage;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractChatResponseExtras(payload: unknown): Pick<
|
export function extractChatResponseExtras(payload: unknown): Pick<
|
||||||
|
|||||||
@@ -372,6 +372,37 @@ describe('openAiChatTransformer.outbound', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('openAiChatTransformer.stream', () => {
|
describe('openAiChatTransformer.stream', () => {
|
||||||
|
it('serializes assistant starter chunks for tool-first responses streams', () => {
|
||||||
|
const context = openAiChatTransformer.createStreamContext('gpt-5');
|
||||||
|
const event = openAiChatTransformer.transformStreamEvent({
|
||||||
|
type: 'response.created',
|
||||||
|
response: {
|
||||||
|
id: 'resp-tool-start',
|
||||||
|
model: 'gpt-5',
|
||||||
|
created_at: 1706000000,
|
||||||
|
status: 'in_progress',
|
||||||
|
output: [],
|
||||||
|
},
|
||||||
|
}, context, 'gpt-5');
|
||||||
|
|
||||||
|
const payloads = parseSsePayloads(
|
||||||
|
openAiChatTransformer.serializeStreamEvent(event, context, createClaudeDownstreamContext()),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(payloads[0]).toMatchObject({
|
||||||
|
id: 'resp-tool-start',
|
||||||
|
model: 'gpt-5',
|
||||||
|
choices: [{
|
||||||
|
index: 0,
|
||||||
|
delta: {
|
||||||
|
role: 'assistant',
|
||||||
|
content: '',
|
||||||
|
},
|
||||||
|
finish_reason: null,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('preserves annotations, citations, and usage payload on serialized stream chunks', () => {
|
it('preserves annotations, citations, and usage payload on serialized stream chunks', () => {
|
||||||
const context = openAiChatTransformer.createStreamContext('gpt-5');
|
const context = openAiChatTransformer.createStreamContext('gpt-5');
|
||||||
const event = openAiChatTransformer.transformStreamEvent({
|
const event = openAiChatTransformer.transformStreamEvent({
|
||||||
@@ -416,6 +447,40 @@ describe('openAiChatTransformer.stream', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('maps nested responses usage into chat-completions usage chunks', () => {
|
||||||
|
const context = openAiChatTransformer.createStreamContext('gpt-5');
|
||||||
|
const event = openAiChatTransformer.transformStreamEvent({
|
||||||
|
type: 'response.completed',
|
||||||
|
response: {
|
||||||
|
id: 'resp-usage',
|
||||||
|
model: 'gpt-5',
|
||||||
|
status: 'completed',
|
||||||
|
output: [],
|
||||||
|
usage: {
|
||||||
|
input_tokens: 11,
|
||||||
|
output_tokens: 7,
|
||||||
|
total_tokens: 18,
|
||||||
|
input_tokens_details: { cached_tokens: 3 },
|
||||||
|
output_tokens_details: { reasoning_tokens: 2 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, context, 'gpt-5');
|
||||||
|
|
||||||
|
const payloads = parseSsePayloads(
|
||||||
|
openAiChatTransformer.serializeStreamEvent(event, context, createClaudeDownstreamContext()),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(payloads[0]).toMatchObject({
|
||||||
|
usage: {
|
||||||
|
prompt_tokens: 11,
|
||||||
|
completion_tokens: 7,
|
||||||
|
total_tokens: 18,
|
||||||
|
prompt_tokens_details: { cached_tokens: 3 },
|
||||||
|
completion_tokens_details: { reasoning_tokens: 2 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('serializes multi-choice stream chunks without collapsing choice-specific deltas', () => {
|
it('serializes multi-choice stream chunks without collapsing choice-specific deltas', () => {
|
||||||
const context = openAiChatTransformer.createStreamContext('gpt-5');
|
const context = openAiChatTransformer.createStreamContext('gpt-5');
|
||||||
const event = openAiChatTransformer.transformStreamEvent({
|
const event = openAiChatTransformer.transformStreamEvent({
|
||||||
|
|||||||
@@ -8,6 +8,14 @@ import {
|
|||||||
serializeNormalizedStreamEvent,
|
serializeNormalizedStreamEvent,
|
||||||
} from './chatFormatsCore.js';
|
} from './chatFormatsCore.js';
|
||||||
|
|
||||||
|
function parseOpenAiSsePayload(lines: string[]): Record<string, unknown> {
|
||||||
|
const dataLine = lines.find((line) => line.startsWith('data: ') && line.trim() !== 'data: [DONE]');
|
||||||
|
if (!dataLine) {
|
||||||
|
throw new Error('expected serialized SSE payload');
|
||||||
|
}
|
||||||
|
return JSON.parse(dataLine.slice(6)) as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
describe('chatFormatsCore inline think parsing', () => {
|
describe('chatFormatsCore inline think parsing', () => {
|
||||||
it('tracks split think tags across stream chunks', () => {
|
it('tracks split think tags across stream chunks', () => {
|
||||||
const context = createStreamTransformContext('gpt-test');
|
const context = createStreamTransformContext('gpt-test');
|
||||||
@@ -158,6 +166,39 @@ describe('chatFormatsCore inline think parsing', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('emits an assistant starter chunk for tool-first responses streams', () => {
|
||||||
|
const context = createStreamTransformContext('gpt-test');
|
||||||
|
const claudeContext = createClaudeDownstreamContext();
|
||||||
|
|
||||||
|
const startEvent = normalizeUpstreamStreamEvent({
|
||||||
|
type: 'response.created',
|
||||||
|
response: {
|
||||||
|
id: 'resp_tool_start',
|
||||||
|
model: 'gpt-test',
|
||||||
|
created_at: 1706000000,
|
||||||
|
status: 'in_progress',
|
||||||
|
output: [],
|
||||||
|
},
|
||||||
|
}, context, 'gpt-test');
|
||||||
|
|
||||||
|
const payload = parseOpenAiSsePayload(
|
||||||
|
serializeNormalizedStreamEvent('openai', startEvent, context, claudeContext),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(payload).toMatchObject({
|
||||||
|
id: 'resp_tool_start',
|
||||||
|
model: 'gpt-test',
|
||||||
|
choices: [{
|
||||||
|
index: 0,
|
||||||
|
delta: {
|
||||||
|
role: 'assistant',
|
||||||
|
content: '',
|
||||||
|
},
|
||||||
|
finish_reason: null,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps responses tool-call indices stable when response.completed replays mixed output arrays', () => {
|
it('keeps responses tool-call indices stable when response.completed replays mixed output arrays', () => {
|
||||||
const context = createStreamTransformContext('gpt-test');
|
const context = createStreamTransformContext('gpt-test');
|
||||||
const claudeContext = createClaudeDownstreamContext();
|
const claudeContext = createClaudeDownstreamContext();
|
||||||
@@ -207,11 +248,200 @@ describe('chatFormatsCore inline think parsing', () => {
|
|||||||
}, context, 'gpt-test')).toEqual({
|
}, context, 'gpt-test')).toEqual({
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
contentDelta: 'working on it',
|
contentDelta: 'working on it',
|
||||||
finishReason: 'stop',
|
finishReason: 'tool_calls',
|
||||||
done: true,
|
done: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps terminal tool-only responses completions marked as tool_calls after earlier streamed deltas', () => {
|
||||||
|
const context = createStreamTransformContext('gpt-test');
|
||||||
|
const claudeContext = createClaudeDownstreamContext();
|
||||||
|
|
||||||
|
const toolStarted = normalizeUpstreamStreamEvent({
|
||||||
|
type: 'response.output_item.added',
|
||||||
|
output_index: 0,
|
||||||
|
item: {
|
||||||
|
id: 'fc_1',
|
||||||
|
type: 'function_call',
|
||||||
|
call_id: 'call_tool_only',
|
||||||
|
name: 'lookup',
|
||||||
|
},
|
||||||
|
}, context, 'gpt-test');
|
||||||
|
serializeNormalizedStreamEvent('openai', toolStarted, context, claudeContext);
|
||||||
|
|
||||||
|
const toolArgs = normalizeUpstreamStreamEvent({
|
||||||
|
type: 'response.function_call_arguments.delta',
|
||||||
|
output_index: 0,
|
||||||
|
call_id: 'call_tool_only',
|
||||||
|
delta: '{"q":"x"}',
|
||||||
|
}, context, 'gpt-test');
|
||||||
|
serializeNormalizedStreamEvent('openai', toolArgs, context, claudeContext);
|
||||||
|
|
||||||
|
expect(normalizeUpstreamStreamEvent({
|
||||||
|
type: 'response.completed',
|
||||||
|
response: {
|
||||||
|
id: 'resp_tool_only',
|
||||||
|
model: 'gpt-test',
|
||||||
|
status: 'completed',
|
||||||
|
usage: { input_tokens: 3, output_tokens: 2, total_tokens: 5 },
|
||||||
|
},
|
||||||
|
}, context, 'gpt-test')).toEqual({
|
||||||
|
finishReason: 'tool_calls',
|
||||||
|
done: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps terminal tool-only responses completions marked as tool_calls before serialization side effects', () => {
|
||||||
|
const context = createStreamTransformContext('gpt-test');
|
||||||
|
|
||||||
|
expect(normalizeUpstreamStreamEvent({
|
||||||
|
type: 'response.output_item.added',
|
||||||
|
output_index: 0,
|
||||||
|
item: {
|
||||||
|
id: 'fc_pre_serialization',
|
||||||
|
type: 'function_call',
|
||||||
|
call_id: 'call_pre_serialization',
|
||||||
|
name: 'lookup',
|
||||||
|
},
|
||||||
|
}, context, 'gpt-test')).toEqual({
|
||||||
|
toolCallDeltas: [{
|
||||||
|
index: 0,
|
||||||
|
id: 'call_pre_serialization',
|
||||||
|
name: 'lookup',
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(normalizeUpstreamStreamEvent({
|
||||||
|
type: 'response.function_call_arguments.delta',
|
||||||
|
output_index: 0,
|
||||||
|
call_id: 'call_pre_serialization',
|
||||||
|
delta: '{"q":"x"}',
|
||||||
|
}, context, 'gpt-test')).toEqual({
|
||||||
|
toolCallDeltas: [{
|
||||||
|
index: 0,
|
||||||
|
id: 'call_pre_serialization',
|
||||||
|
argumentsDelta: '{"q":"x"}',
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(normalizeUpstreamStreamEvent({
|
||||||
|
type: 'response.completed',
|
||||||
|
response: {
|
||||||
|
id: 'resp_pre_serialization',
|
||||||
|
model: 'gpt-test',
|
||||||
|
status: 'completed',
|
||||||
|
usage: { input_tokens: 3, output_tokens: 2, total_tokens: 5 },
|
||||||
|
},
|
||||||
|
}, context, 'gpt-test')).toEqual({
|
||||||
|
finishReason: 'tool_calls',
|
||||||
|
done: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not backfill historical tool identity into later arguments-only deltas', () => {
|
||||||
|
const context = createStreamTransformContext('gpt-test');
|
||||||
|
const claudeContext = createClaudeDownstreamContext();
|
||||||
|
|
||||||
|
const started = normalizeUpstreamStreamEvent({
|
||||||
|
type: 'response.output_item.added',
|
||||||
|
output_index: 0,
|
||||||
|
item: {
|
||||||
|
id: 'fc_identity',
|
||||||
|
type: 'function_call',
|
||||||
|
call_id: 'call_identity',
|
||||||
|
name: 'lookup',
|
||||||
|
},
|
||||||
|
}, context, 'gpt-test');
|
||||||
|
serializeNormalizedStreamEvent('openai', started, context, claudeContext);
|
||||||
|
|
||||||
|
const argumentDelta = normalizeUpstreamStreamEvent({
|
||||||
|
type: 'response.function_call_arguments.delta',
|
||||||
|
output_index: 0,
|
||||||
|
call_id: 'call_identity',
|
||||||
|
delta: '{"q":"x"}',
|
||||||
|
}, context, 'gpt-test');
|
||||||
|
|
||||||
|
const payload = parseOpenAiSsePayload(
|
||||||
|
serializeNormalizedStreamEvent('openai', argumentDelta, context, claudeContext),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(payload).toMatchObject({
|
||||||
|
choices: [{
|
||||||
|
index: 0,
|
||||||
|
delta: {
|
||||||
|
tool_calls: [{
|
||||||
|
index: 0,
|
||||||
|
function: {
|
||||||
|
arguments: '{"q":"x"}',
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
finish_reason: null,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
const toolCall = (((payload.choices as any[])[0] as any).delta.tool_calls[0]) as Record<string, unknown>;
|
||||||
|
expect(toolCall.id).toBeUndefined();
|
||||||
|
expect(toolCall.type).toBeUndefined();
|
||||||
|
expect((toolCall.function as Record<string, unknown>).name).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('backfills late real tool identity after earlier id-less argument deltas', () => {
|
||||||
|
const context = createStreamTransformContext('gpt-test');
|
||||||
|
const claudeContext = createClaudeDownstreamContext();
|
||||||
|
|
||||||
|
const argumentDelta = normalizeUpstreamStreamEvent({
|
||||||
|
type: 'response.function_call_arguments.delta',
|
||||||
|
output_index: 0,
|
||||||
|
delta: '{"q":"x"}',
|
||||||
|
}, context, 'gpt-test');
|
||||||
|
|
||||||
|
expect(argumentDelta).toEqual({
|
||||||
|
toolCallDeltas: [{
|
||||||
|
index: 0,
|
||||||
|
argumentsDelta: '{"q":"x"}',
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
serializeNormalizedStreamEvent('openai', argumentDelta, context, claudeContext);
|
||||||
|
|
||||||
|
const started = normalizeUpstreamStreamEvent({
|
||||||
|
type: 'response.output_item.done',
|
||||||
|
output_index: 0,
|
||||||
|
item: {
|
||||||
|
id: 'fc_late_identity',
|
||||||
|
type: 'function_call',
|
||||||
|
call_id: 'call_late_identity',
|
||||||
|
name: 'lookup',
|
||||||
|
arguments: '{"q":"x"}',
|
||||||
|
},
|
||||||
|
}, context, 'gpt-test');
|
||||||
|
|
||||||
|
const payload = parseOpenAiSsePayload(
|
||||||
|
serializeNormalizedStreamEvent('openai', started, context, claudeContext),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(payload).toMatchObject({
|
||||||
|
choices: [{
|
||||||
|
index: 0,
|
||||||
|
delta: {
|
||||||
|
tool_calls: [{
|
||||||
|
index: 0,
|
||||||
|
id: 'call_late_identity',
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'lookup',
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
finish_reason: null,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
expect(context.toolCalls[0]).toEqual({
|
||||||
|
id: 'call_late_identity',
|
||||||
|
name: 'lookup',
|
||||||
|
arguments: '{"q":"x"}',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('preserves terminal response.completed payload output when it carries the only final content', () => {
|
it('preserves terminal response.completed payload output when it carries the only final content', () => {
|
||||||
const context = createStreamTransformContext('gpt-test');
|
const context = createStreamTransformContext('gpt-test');
|
||||||
|
|
||||||
|
|||||||
@@ -820,8 +820,8 @@ function buildResponsesToolCallDeltaFromItem(
|
|||||||
return {
|
return {
|
||||||
toolCallDeltas: [{
|
toolCallDeltas: [{
|
||||||
index: canonicalIndex,
|
index: canonicalIndex,
|
||||||
id: toolCallId,
|
...(shouldBackfillId && toolCallId ? { id: toolCallId } : {}),
|
||||||
name: toolName,
|
...(shouldBackfillName && toolName ? { name: toolName } : {}),
|
||||||
argumentsDelta: argumentsDelta || undefined,
|
argumentsDelta: argumentsDelta || undefined,
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
@@ -1533,6 +1533,14 @@ export function normalizeUpstreamStreamEvent(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === 'response.created' && isRecord((payload as any).response)) {
|
||||||
|
const responsePayload = (payload as any).response as Record<string, unknown>;
|
||||||
|
if (isNonEmptyString(responsePayload.id)) context.id = responsePayload.id;
|
||||||
|
if (isNonEmptyString(responsePayload.model)) context.model = responsePayload.model;
|
||||||
|
context.created = ensureIntegerTimestamp((responsePayload as any).created_at ?? responsePayload.created, context.created);
|
||||||
|
return { role: 'assistant' };
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'response.failed' || type === 'response.incomplete' || type === 'error') {
|
if (type === 'response.failed' || type === 'response.incomplete' || type === 'error') {
|
||||||
const responsePayload = isRecord((payload as any).response) ? (payload as any).response : null;
|
const responsePayload = isRecord((payload as any).response) ? (payload as any).response : null;
|
||||||
const finishReason = normalizeStopReason(
|
const finishReason = normalizeStopReason(
|
||||||
@@ -1619,9 +1627,9 @@ export function normalizeUpstreamStreamEvent(
|
|||||||
return {
|
return {
|
||||||
toolCallDeltas: [{
|
toolCallDeltas: [{
|
||||||
index: canonicalIndex,
|
index: canonicalIndex,
|
||||||
id: toolCallId,
|
...(shouldBackfillId && toolCallId ? { id: toolCallId } : {}),
|
||||||
name: toolName,
|
...(shouldBackfillName && toolName ? { name: toolName } : {}),
|
||||||
argumentsDelta,
|
...(argumentsDelta !== undefined ? { argumentsDelta } : {}),
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1659,9 +1667,9 @@ export function normalizeUpstreamStreamEvent(
|
|||||||
return {
|
return {
|
||||||
toolCallDeltas: [{
|
toolCallDeltas: [{
|
||||||
index: canonicalIndex,
|
index: canonicalIndex,
|
||||||
id: toolCallId,
|
...(shouldBackfillId && toolCallId ? { id: toolCallId } : {}),
|
||||||
name: toolName,
|
...(shouldBackfillName && toolName ? { name: toolName } : {}),
|
||||||
argumentsDelta: argumentsDelta || undefined,
|
...(argumentsDelta ? { argumentsDelta } : {}),
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1702,19 +1710,20 @@ export function normalizeUpstreamStreamEvent(
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
index: canonicalIndex,
|
index: canonicalIndex,
|
||||||
id: toolCall.id,
|
...(shouldBackfillId && toolCall.id ? { id: toolCall.id } : {}),
|
||||||
name: toolCall.name,
|
...(shouldBackfillName && toolCall.name ? { name: toolCall.name } : {}),
|
||||||
argumentsDelta: argumentsDelta || undefined,
|
...(argumentsDelta ? { argumentsDelta } : {}),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((item): item is NonNullable<typeof item> => !!item);
|
.filter((item): item is NonNullable<typeof item> => !!item);
|
||||||
|
const hasKnownToolCalls = toolCalls.length > 0 || context.nextResponsesToolCallIndex > 0;
|
||||||
return {
|
return {
|
||||||
...(contentDelta || reasoningDelta ? { role: 'assistant' as const } : {}),
|
...(contentDelta || reasoningDelta ? { role: 'assistant' as const } : {}),
|
||||||
...(contentDelta ? { contentDelta } : {}),
|
...(contentDelta ? { contentDelta } : {}),
|
||||||
...(reasoningDelta ? { reasoningDelta } : {}),
|
...(reasoningDelta ? { reasoningDelta } : {}),
|
||||||
...(responsesReasoning.reasoningSignature ? { reasoningSignature: responsesReasoning.reasoningSignature } : {}),
|
...(responsesReasoning.reasoningSignature ? { reasoningSignature: responsesReasoning.reasoningSignature } : {}),
|
||||||
...(toolCallDeltas.length > 0 ? { toolCallDeltas } : {}),
|
...(toolCallDeltas.length > 0 ? { toolCallDeltas } : {}),
|
||||||
finishReason: toolCallDeltas.length > 0
|
finishReason: hasKnownToolCalls
|
||||||
? 'tool_calls'
|
? 'tool_calls'
|
||||||
: (normalizeStopReason(responsePayload.status) || 'stop'),
|
: (normalizeStopReason(responsePayload.status) || 'stop'),
|
||||||
done: true,
|
done: true,
|
||||||
@@ -1883,25 +1892,26 @@ function buildOpenAiStreamChunk(
|
|||||||
const toolCalls = event.toolCallDeltas.map((toolDelta) => {
|
const toolCalls = event.toolCallDeltas.map((toolDelta) => {
|
||||||
const index = Number.isFinite(toolDelta.index) ? Math.max(0, Math.trunc(toolDelta.index)) : 0;
|
const index = Number.isFinite(toolDelta.index) ? Math.max(0, Math.trunc(toolDelta.index)) : 0;
|
||||||
const existing = context.toolCalls[index] || {};
|
const existing = context.toolCalls[index] || {};
|
||||||
const id = toolDelta.id || existing.id || `call_meta_${index}`;
|
const id = toolDelta.id || existing.id;
|
||||||
const name = toolDelta.name || existing.name || '';
|
const name = toolDelta.name || existing.name || '';
|
||||||
const nextArguments = `${existing.arguments || ''}${toolDelta.argumentsDelta ?? ''}`;
|
const nextArguments = `${existing.arguments || ''}${toolDelta.argumentsDelta ?? ''}`;
|
||||||
|
// Keep synthetic call_meta_* ids as serialization-only fallbacks so later real ids can still backfill.
|
||||||
context.toolCalls[index] = {
|
context.toolCalls[index] = {
|
||||||
id,
|
...(id ? { id } : {}),
|
||||||
name: name || existing.name,
|
...(name || existing.name ? { name: name || existing.name } : {}),
|
||||||
arguments: nextArguments,
|
arguments: nextArguments,
|
||||||
};
|
};
|
||||||
|
|
||||||
const fn: Record<string, unknown> = {};
|
const fn: Record<string, unknown> = {};
|
||||||
if (name) fn.name = name;
|
if (toolDelta.name) fn.name = toolDelta.name;
|
||||||
if (toolDelta.argumentsDelta !== undefined) fn.arguments = toolDelta.argumentsDelta;
|
if (toolDelta.argumentsDelta !== undefined) fn.arguments = toolDelta.argumentsDelta;
|
||||||
|
const serializedToolCall: Record<string, unknown> = {
|
||||||
return {
|
|
||||||
index,
|
index,
|
||||||
id,
|
|
||||||
type: 'function',
|
|
||||||
function: fn,
|
|
||||||
};
|
};
|
||||||
|
if (toolDelta.id) serializedToolCall.id = toolDelta.id;
|
||||||
|
if (toolDelta.id || toolDelta.name) serializedToolCall.type = 'function';
|
||||||
|
if (Object.keys(fn).length > 0) serializedToolCall.function = fn;
|
||||||
|
return serializedToolCall;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (toolCalls.length > 0) {
|
if (toolCalls.length > 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user