Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-responses-tool-arguments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix tool calls from OpenAI Responses-compatible providers when final arguments correct streamed partial arguments.
Original file line number Diff line number Diff line change
Expand Up @@ -733,86 +733,64 @@ export class OpenAIResponsesStreamedMessage implements StreamedMessage {
private async *_convertStreamResponse(
response: AsyncIterable<RawObject>,
): AsyncGenerator<StreamedMessagePart> {
const functionCallArgumentsByIndex = new Map<number | string, string>();
let unindexedFunctionCallArguments: string | undefined;

const hasFunctionCallArguments = (streamIndex: number | string | undefined): boolean =>
streamIndex === undefined
? unindexedFunctionCallArguments !== undefined
: functionCallArgumentsByIndex.has(streamIndex);
type FunctionCallState = {
toolCall: ToolCall;
accumulatedArguments: string;
functionDoneArguments?: string;
committed: boolean;
};

const getFunctionCallArguments = (streamIndex: number | string | undefined): string =>
streamIndex === undefined
? (unindexedFunctionCallArguments as string)
: functionCallArgumentsByIndex.get(streamIndex)!;
const functionCallsByIndex = new Map<number | string, FunctionCallState>();
let unindexedFunctionCall: FunctionCallState | undefined;

const setFunctionCallArguments = (
const getFunctionCall = (
streamIndex: number | string | undefined,
argumentsValue: string,
): void => {
if (streamIndex === undefined) {
unindexedFunctionCallArguments = argumentsValue;
} else {
functionCallArgumentsByIndex.set(streamIndex, argumentsValue);
context: string,
): FunctionCallState => {
const state =
streamIndex === undefined
? unindexedFunctionCall
: functionCallsByIndex.get(streamIndex);
if (state === undefined) {
failResponsesDecode(
context,
`received function-call arguments for unknown stream index ${formatResponseStreamIndex(streamIndex)}.`,
);
}
return state;
};

const appendFunctionCallArguments = (
streamIndex: number | string | undefined,
argumentsPart: string,
context: string,
): void => {
if (!hasFunctionCallArguments(streamIndex)) {
failResponsesDecode(
context,
`received function-call arguments for unknown stream index ${formatResponseStreamIndex(streamIndex)}.`,
);
}
setFunctionCallArguments(
streamIndex,
getFunctionCallArguments(streamIndex) + argumentsPart,
);
const state = getFunctionCall(streamIndex, context);
state.accumulatedArguments += argumentsPart;
};

const yieldFinalArgumentsSuffix = function* (
const commitFunctionCall = function* (
streamIndex: number | string | undefined,
finalArguments: string,
context: string,
): Generator<StreamedMessagePart> {
if (!hasFunctionCallArguments(streamIndex)) {
failResponsesDecode(
context,
`received final function-call arguments for unknown stream index ${formatResponseStreamIndex(streamIndex)}.`,
);
}

const accumulatedArguments = getFunctionCallArguments(streamIndex);
if (finalArguments === accumulatedArguments) {
const state = getFunctionCall(streamIndex, context);
if (state.committed) {
return;
}
state.committed = true;

if (!finalArguments.startsWith(accumulatedArguments)) {
throw new ChatProviderError(
`OpenAI Responses final function-call arguments for stream index ${formatResponseStreamIndex(
streamIndex,
)} do not match the streamed argument deltas.`,
);
}

const suffix = finalArguments.slice(accumulatedArguments.length);
setFunctionCallArguments(streamIndex, finalArguments);
if (suffix.length === 0) {
if (streamIndex === undefined) {
unindexedFunctionCall = undefined;
yield { ...state.toolCall, arguments: finalArguments };
return;
}

const part: StreamedMessagePart = {
yield {
type: 'tool_call_part',
argumentsPart: suffix,
argumentsPart: finalArguments,
index: streamIndex,
};
if (streamIndex !== undefined) {
(part as { index: number | string }).index = streamIndex;
}
yield part;
};

try {
Expand Down Expand Up @@ -845,18 +823,31 @@ export class OpenAIResponsesStreamedMessage implements StreamedMessage {
const item = readResponseOutputItem(chunk['item'], `${type}.item`);
const outputIndex = readNumberField(chunk, 'output_index');
if (item.type === 'function_call') {
if (unindexedFunctionCall !== undefined) {
failResponsesDecode(
`${type}.item`,
'cannot start a function call while an unindexed function call is unresolved.',
);
}
const streamIndex = responseStreamIndex(item.itemId, outputIndex);
setFunctionCallArguments(streamIndex, item.arguments ?? '');
const tc: ToolCall = {
type: 'function',
id: functionCallId(item.callId),
name: requireFunctionCallName(item),
arguments: item.arguments ?? null,
arguments: null,
};
const state: FunctionCallState = {
toolCall: tc,
accumulatedArguments: item.arguments ?? '',
committed: false,
};
if (streamIndex !== undefined) {
tc._streamIndex = streamIndex;
functionCallsByIndex.set(streamIndex, state);
yield tc;
} else {
unindexedFunctionCall = state;
}
yield tc;
}
break;
}
Expand All @@ -871,7 +862,7 @@ export class OpenAIResponsesStreamedMessage implements StreamedMessage {
yield thinkPart;
} else if (item.type === 'function_call' && typeof item.arguments === 'string') {
const streamIndex = responseStreamIndex(item.itemId, outputIndex);
yield* yieldFinalArgumentsSuffix(streamIndex, item.arguments, type);
yield* commitFunctionCall(streamIndex, item.arguments, type);
}
break;
}
Expand All @@ -881,15 +872,7 @@ export class OpenAIResponsesStreamedMessage implements StreamedMessage {
readNumberField(chunk, 'output_index'),
);
const argumentsPart = requireStringField(chunk, 'delta', type);
const part: StreamedMessagePart = {
type: 'tool_call_part',
argumentsPart,
};
appendFunctionCallArguments(streamIndex, argumentsPart, type);
if (streamIndex !== undefined) {
(part as { index: number | string }).index = streamIndex;
}
yield part;
break;
}
case 'response.function_call_arguments.done': {
Expand All @@ -898,7 +881,8 @@ export class OpenAIResponsesStreamedMessage implements StreamedMessage {
readStringField(chunk, 'item_id'),
readNumberField(chunk, 'output_index'),
);
yield* yieldFinalArgumentsSuffix(streamIndex, functionArguments, type);
const state = getFunctionCall(streamIndex, type);
state.functionDoneArguments ??= functionArguments;
break;
}
case 'response.reasoning_summary_part.added':
Expand Down Expand Up @@ -952,6 +936,24 @@ export class OpenAIResponsesStreamedMessage implements StreamedMessage {
} catch (error: unknown) {
throw convertOpenAIError(error);
}

for (const [streamIndex, state] of functionCallsByIndex) {
if (!state.committed) {
yield* commitFunctionCall(
streamIndex,
state.functionDoneArguments ?? state.accumulatedArguments,
'OpenAI Responses stream completion',
);
}
}
if (unindexedFunctionCall !== undefined) {
const state = unindexedFunctionCall;
yield* commitFunctionCall(
undefined,
state.functionDoneArguments ?? state.accumulatedArguments,
'OpenAI Responses stream completion',
);
}
}
}
export class OpenAIResponsesChatProvider implements ChatProvider {
Expand Down
Loading