diff --git a/README.md b/README.md
index 0115b67..e96fe6d 100644
--- a/README.md
+++ b/README.md
@@ -723,7 +723,7 @@ Metapi 完全自托管,所有数据(账号、令牌、路由、日志)均
## ⭐ Star History
-[](https://www.star-history.com/#cita-777/metapi&type=date&legend=top-left)
+[](https://www.star-history.com/#cita-777/metapi&type=date&legend=top-left)
---
diff --git a/src/server/routes/proxy/chat.stream.test.ts b/src/server/routes/proxy/chat.stream.test.ts
index 3a00403..b9e5253 100644
--- a/src/server/routes/proxy/chat.stream.test.ts
+++ b/src/server/routes/proxy/chat.stream.test.ts
@@ -796,6 +796,171 @@ describe('chat proxy stream behavior', () => {
expect(options.headers.accept).toBe('text/event-stream');
});
+ it('preserves function_call/function_call_output when /v1/responses falls back to /v1/chat/completions', async () => {
+ fetchModelPricingCatalogMock.mockResolvedValue({
+ models: [
+ {
+ modelName: 'upstream-gpt',
+ supportedEndpointTypes: ['/v1/chat/completions'],
+ },
+ ],
+ groupRatio: {},
+ });
+
+ fetchMock.mockResolvedValue(new Response(JSON.stringify({
+ id: 'chatcmpl_responses_fallback_chat',
+ object: 'chat.completion',
+ created: 1_706_000_111,
+ model: 'upstream-gpt',
+ choices: [{
+ index: 0,
+ message: { role: 'assistant', content: 'ok' },
+ finish_reason: 'stop',
+ }],
+ usage: { prompt_tokens: 9, completion_tokens: 3, total_tokens: 12 },
+ }), {
+ status: 200,
+ headers: { 'content-type': 'application/json' },
+ }));
+
+ const response = await app.inject({
+ method: 'POST',
+ url: '/v1/responses',
+ payload: {
+ model: 'gpt-5.2',
+ input: [
+ {
+ type: 'message',
+ role: 'user',
+ content: [{ type: 'input_text', text: 'find readme' }],
+ },
+ {
+ type: 'function_call',
+ call_id: 'call_abc',
+ name: 'Glob',
+ arguments: '{"pattern":"README*"}',
+ },
+ {
+ type: 'function_call_output',
+ call_id: 'call_abc',
+ output: '{"matches":1}',
+ },
+ ],
+ tools: [{
+ type: 'function',
+ name: 'Glob',
+ description: 'Search files',
+ parameters: {
+ type: 'object',
+ properties: { pattern: { type: 'string' } },
+ required: ['pattern'],
+ },
+ strict: true,
+ }],
+ tool_choice: {
+ type: 'function',
+ name: 'Glob',
+ },
+ },
+ });
+
+ expect(response.statusCode).toBe(200);
+
+ const [targetUrl, options] = fetchMock.mock.calls[0] as [string, any];
+ expect(targetUrl).toContain('/v1/chat/completions');
+
+ const forwarded = JSON.parse(options.body);
+ expect(Array.isArray(forwarded.messages)).toBe(true);
+
+ const assistantWithToolCall = forwarded.messages.find((item: any) =>
+ item?.role === 'assistant'
+ && Array.isArray(item?.tool_calls)
+ && item.tool_calls.length > 0,
+ );
+ expect(assistantWithToolCall).toBeTruthy();
+ expect(assistantWithToolCall.tool_calls[0].id).toBe('call_abc');
+ expect(assistantWithToolCall.tool_calls[0].function?.name).toBe('Glob');
+ expect(assistantWithToolCall.tool_calls[0].function?.arguments).toContain('README*');
+
+ const toolMessage = forwarded.messages.find((item: any) => item?.role === 'tool');
+ expect(toolMessage).toBeTruthy();
+ expect(toolMessage.tool_call_id).toBe('call_abc');
+ expect(toolMessage.content).toContain('matches');
+
+ expect(forwarded.tools?.[0]?.function?.name).toBe('Glob');
+ expect(forwarded.tool_choice?.function?.name).toBe('Glob');
+ });
+
+ it('preserves function_call/function_call_output when /v1/responses falls back to /v1/messages', async () => {
+ fetchModelPricingCatalogMock.mockResolvedValue({
+ models: [
+ {
+ modelName: 'upstream-gpt',
+ supportedEndpointTypes: ['anthropic'],
+ },
+ ],
+ groupRatio: {},
+ });
+
+ fetchMock.mockResolvedValue(new Response(JSON.stringify({
+ id: 'msg_responses_fallback_messages',
+ type: 'message',
+ model: 'upstream-gpt',
+ content: [{ type: 'text', text: 'ok' }],
+ stop_reason: 'end_turn',
+ usage: { input_tokens: 8, output_tokens: 2, total_tokens: 10 },
+ }), {
+ status: 200,
+ headers: { 'content-type': 'application/json' },
+ }));
+
+ const response = await app.inject({
+ method: 'POST',
+ url: '/v1/responses',
+ payload: {
+ model: 'gpt-5.2',
+ input: [
+ {
+ type: 'message',
+ role: 'user',
+ content: [{ type: 'input_text', text: 'find readme' }],
+ },
+ {
+ type: 'function_call',
+ call_id: 'call_abc',
+ name: 'Glob',
+ arguments: '{"pattern":"README*"}',
+ },
+ {
+ type: 'function_call_output',
+ call_id: 'call_abc',
+ output: '{"matches":1}',
+ },
+ ],
+ },
+ });
+
+ expect(response.statusCode).toBe(200);
+
+ const [targetUrl, options] = fetchMock.mock.calls[0] as [string, any];
+ expect(targetUrl).toContain('/v1/messages');
+
+ const forwarded = JSON.parse(options.body);
+ expect(Array.isArray(forwarded.messages)).toBe(true);
+
+ const assistantMessage = forwarded.messages.find((item: any) => item?.role === 'assistant');
+ expect(Array.isArray(assistantMessage?.content)).toBe(true);
+ expect(assistantMessage.content.some((part: any) => part?.type === 'tool_use')).toBe(true);
+
+ const userToolResultMessage = forwarded.messages.find((item: any) =>
+ item?.role === 'user'
+ && Array.isArray(item?.content)
+ && item.content.some((part: any) => part?.type === 'tool_result'),
+ );
+ expect(userToolResultMessage).toBeTruthy();
+ expect(userToolResultMessage.content[0].tool_use_id).toBe('call_abc');
+ });
+
it('routes /v1/responses to /v1/messages when upstream catalog is anthropic-only', async () => {
fetchModelPricingCatalogMock.mockResolvedValue({
models: [
@@ -1196,6 +1361,95 @@ describe('chat proxy stream behavior', () => {
expect(response.body).toContain('\\"pattern\\":\\"README*\\"');
});
+ it('uses response.function_call_arguments.done when upstream omits delta on /v1/chat/completions', async () => {
+ fetchModelPricingCatalogMock.mockResolvedValue({
+ models: [
+ {
+ modelName: 'upstream-gpt',
+ supportedEndpointTypes: ['/v1/responses'],
+ },
+ ],
+ groupRatio: {},
+ });
+
+ const encoder = new TextEncoder();
+ const upstreamBody = new ReadableStream({
+ start(controller) {
+ controller.enqueue(encoder.encode('event: response.created\ndata: {"type":"response.created","response":{"id":"resp_done_only","model":"upstream-gpt","created_at":1706000000,"status":"in_progress","output":[]}}\n\n'));
+ controller.enqueue(encoder.encode('event: response.output_item.added\ndata: {"type":"response.output_item.added","output_index":0,"item":{"type":"function_call","id":"fc_1","call_id":"call_done_only","name":"Glob"}}\n\n'));
+ controller.enqueue(encoder.encode('event: response.function_call_arguments.done\ndata: {"type":"response.function_call_arguments.done","output_index":0,"call_id":"call_done_only","arguments":"{\\"pattern\\":\\"README*\\"}"}\n\n'));
+ controller.enqueue(encoder.encode('event: response.completed\ndata: {"type":"response.completed","response":{"id":"resp_done_only","model":"upstream-gpt","status":"completed","usage":{"input_tokens":5,"output_tokens":3,"total_tokens":8}}}\n\n'));
+ controller.enqueue(encoder.encode('data: [DONE]\n\n'));
+ controller.close();
+ },
+ });
+
+ fetchMock.mockResolvedValue(new Response(upstreamBody, {
+ status: 200,
+ headers: { 'content-type': 'text/event-stream; charset=utf-8' },
+ }));
+
+ const response = await app.inject({
+ method: 'POST',
+ url: '/v1/chat/completions',
+ payload: {
+ model: 'claude-haiku-4-5-20251001',
+ stream: true,
+ messages: [{ role: 'user', content: 'find readme' }],
+ },
+ });
+
+ expect(response.statusCode).toBe(200);
+ expect(response.body).toContain('"tool_calls"');
+ expect(response.body).toContain('"id":"call_done_only"');
+ expect(response.body).toContain('"name":"Glob"');
+ expect(response.body).toContain('\\"pattern\\":\\"README*\\"');
+ });
+
+ it('does not duplicate tool arguments when upstream sends both delta and done on /v1/chat/completions', async () => {
+ fetchModelPricingCatalogMock.mockResolvedValue({
+ models: [
+ {
+ modelName: 'upstream-gpt',
+ supportedEndpointTypes: ['/v1/responses'],
+ },
+ ],
+ groupRatio: {},
+ });
+
+ const encoder = new TextEncoder();
+ const upstreamBody = new ReadableStream({
+ start(controller) {
+ controller.enqueue(encoder.encode('event: response.created\ndata: {"type":"response.created","response":{"id":"resp_delta_done","model":"upstream-gpt","created_at":1706000000,"status":"in_progress","output":[]}}\n\n'));
+ controller.enqueue(encoder.encode('event: response.output_item.added\ndata: {"type":"response.output_item.added","output_index":0,"item":{"type":"function_call","id":"fc_1","call_id":"call_delta_done","name":"Glob"}}\n\n'));
+ controller.enqueue(encoder.encode('event: response.function_call_arguments.delta\ndata: {"type":"response.function_call_arguments.delta","output_index":0,"call_id":"call_delta_done","delta":"{\\"pattern\\":\\"README*\\"}"}\n\n'));
+ controller.enqueue(encoder.encode('event: response.function_call_arguments.done\ndata: {"type":"response.function_call_arguments.done","output_index":0,"call_id":"call_delta_done","arguments":"{\\"pattern\\":\\"README*\\"}"}\n\n'));
+ controller.enqueue(encoder.encode('event: response.completed\ndata: {"type":"response.completed","response":{"id":"resp_delta_done","model":"upstream-gpt","status":"completed","usage":{"input_tokens":5,"output_tokens":3,"total_tokens":8}}}\n\n'));
+ controller.enqueue(encoder.encode('data: [DONE]\n\n'));
+ controller.close();
+ },
+ });
+
+ fetchMock.mockResolvedValue(new Response(upstreamBody, {
+ status: 200,
+ headers: { 'content-type': 'text/event-stream; charset=utf-8' },
+ }));
+
+ const response = await app.inject({
+ method: 'POST',
+ url: '/v1/chat/completions',
+ payload: {
+ model: 'claude-haiku-4-5-20251001',
+ stream: true,
+ messages: [{ role: 'user', content: 'find readme' }],
+ },
+ });
+
+ expect(response.statusCode).toBe(200);
+ const matches = response.body.match(/\\"pattern\\":\\"README\*\\"/g) || [];
+ expect(matches.length).toBe(1);
+ });
+
it('preserves non-stream function_call output when /v1/chat/completions falls back to /v1/responses', async () => {
fetchModelPricingCatalogMock.mockResolvedValue({
models: [
diff --git a/src/server/routes/proxy/chatFormats.ts b/src/server/routes/proxy/chatFormats.ts
index ce6c10a..dddfce5 100644
--- a/src/server/routes/proxy/chatFormats.ts
+++ b/src/server/routes/proxy/chatFormats.ts
@@ -11,7 +11,7 @@ export type StreamTransformContext = {
created: number;
roleSent: boolean;
doneSent: boolean;
- toolCalls: Record;
+ toolCalls: Record;
};
export type ClaudeDownstreamContext = {
@@ -167,13 +167,15 @@ export function normalizeStopReason(raw: unknown): string | null {
|| value === 'end'
|| value === 'eos'
|| value === 'finished'
+ || value === 'completed'
|| value === 'stop_sequence'
) {
return 'stop';
}
if (
- value === 'max_tokens'
+ value === 'incomplete'
+ || value === 'max_tokens'
|| value === 'length'
|| value === 'max_output_tokens'
|| value === 'max_tokens_exceeded'
@@ -835,11 +837,33 @@ export function normalizeUpstreamStreamEvent(
: (isNonEmptyString((payload as any).item_id) ? (payload as any).item_id : undefined)
);
const toolName = isNonEmptyString((payload as any).name) ? (payload as any).name : undefined;
- const argumentsDelta = (
- typeof payload.delta === 'string'
- ? payload.delta
- : (typeof (payload as any).arguments === 'string' ? (payload as any).arguments : undefined)
+ const rawArguments = (
+ type === 'response.function_call_arguments.done'
+ ? (typeof (payload as any).arguments === 'string' ? (payload as any).arguments : undefined)
+ : (
+ typeof payload.delta === 'string'
+ ? payload.delta
+ : (typeof (payload as any).arguments === 'string' ? (payload as any).arguments : undefined)
+ )
);
+ let argumentsDelta = rawArguments;
+ if (type === 'response.function_call_arguments.done' && typeof rawArguments === 'string') {
+ const existingArguments = context.toolCalls[outputIndex]?.arguments || '';
+ if (existingArguments && rawArguments.startsWith(existingArguments)) {
+ const missingSuffix = rawArguments.slice(existingArguments.length);
+ argumentsDelta = missingSuffix.length > 0 ? missingSuffix : undefined;
+ } else if (existingArguments === rawArguments) {
+ argumentsDelta = undefined;
+ }
+ }
+
+ const knownTool = context.toolCalls[outputIndex] || {};
+ const shouldBackfillId = !!toolCallId && !knownTool.id;
+ const shouldBackfillName = !!toolName && !knownTool.name;
+ if (argumentsDelta === undefined && !shouldBackfillId && !shouldBackfillName) {
+ return {};
+ }
+
return {
toolCallDeltas: [{
index: outputIndex,
@@ -1019,9 +1043,11 @@ function buildOpenAiStreamChunk(
const existing = context.toolCalls[index] || {};
const id = toolDelta.id || existing.id || `call_meta_${index}`;
const name = toolDelta.name || existing.name || '';
+ const nextArguments = `${existing.arguments || ''}${toolDelta.argumentsDelta ?? ''}`;
context.toolCalls[index] = {
id,
name: name || existing.name,
+ arguments: nextArguments,
};
const fn: Record = {};
diff --git a/src/server/routes/proxy/responses.ts b/src/server/routes/proxy/responses.ts
index 520f283..5acb88e 100644
--- a/src/server/routes/proxy/responses.ts
+++ b/src/server/routes/proxy/responses.ts
@@ -35,6 +35,10 @@ function isRecord(value: unknown): value is Record {
return !!value && typeof value === 'object';
}
+function asTrimmedString(value: unknown): string {
+ return typeof value === 'string' ? value.trim() : '';
+}
+
function normalizeText(value: unknown): string {
if (typeof value === 'string') return value;
if (Array.isArray(value)) {
@@ -53,31 +57,303 @@ function normalizeText(value: unknown): string {
return '';
}
+function stringifyJson(value: unknown): string {
+ try {
+ return JSON.stringify(value);
+ } catch {
+ return '';
+ }
+}
+
+function normalizeToolArguments(value: unknown): string {
+ if (typeof value === 'string') return value;
+ if (value === undefined || value === null) return '';
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
+ if (Array.isArray(value) || isRecord(value)) return stringifyJson(value);
+ return '';
+}
+
+function normalizeToolOutput(value: unknown): string {
+ const text = normalizeText(value).trim();
+ if (text) return text;
+ if (typeof value === 'string') return value;
+ if (value === undefined || value === null) return '';
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
+ if (Array.isArray(value) || isRecord(value)) return stringifyJson(value);
+ return '';
+}
+
+type OpenAiToolCall = {
+ id: string;
+ type: 'function';
+ function: {
+ name: string;
+ arguments: string;
+ };
+};
+
+function toOpenAiToolCall(item: Record, 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: normalizeToolArguments(item.arguments),
+ },
+ };
+}
+
+function extractTextAndToolsFromResponsesMessage(
+ rawContent: unknown,
+ fallbackPrefix: string,
+): {
+ text: string;
+ toolCalls: OpenAiToolCall[];
+ toolResults: Array<{ id: string; content: string }>;
+} {
+ const contentItems = Array.isArray(rawContent) ? rawContent : [rawContent];
+ const textParts: string[] = [];
+ const toolCalls: OpenAiToolCall[] = [];
+ const toolResults: Array<{ id: string; content: string }> = [];
+
+ for (let index = 0; index < contentItems.length; index += 1) {
+ const part = contentItems[index];
+ if (!isRecord(part)) {
+ const text = normalizeText(part).trim();
+ if (text) textParts.push(text);
+ continue;
+ }
+
+ const partType = asTrimmedString(part.type).toLowerCase();
+ if (partType === 'tool_use') {
+ const callId = asTrimmedString(part.id) || `call_${fallbackPrefix}_${index}`;
+ const name = asTrimmedString(part.name);
+ if (!name) continue;
+
+ toolCalls.push({
+ id: callId,
+ type: 'function',
+ function: {
+ name,
+ arguments: normalizeToolArguments(part.input),
+ },
+ });
+ continue;
+ }
+
+ if (partType === 'tool_result') {
+ const id = asTrimmedString(part.tool_use_id) || asTrimmedString(part.id);
+ if (!id) continue;
+ toolResults.push({
+ id,
+ content: normalizeToolOutput(part.content),
+ });
+ continue;
+ }
+
+ const text = normalizeText(part).trim();
+ if (text) textParts.push(text);
+ }
+
+ return {
+ text: textParts.join('\n').trim(),
+ toolCalls,
+ toolResults,
+ };
+}
+
+function convertResponsesToolsToOpenAi(rawTools: unknown): unknown {
+ if (!Array.isArray(rawTools)) return rawTools;
+
+ const converted = rawTools
+ .map((item) => {
+ if (!isRecord(item)) return item;
+ const type = asTrimmedString(item.type).toLowerCase();
+
+ 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 = { 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,
+ };
+ });
+
+ return converted;
+}
+
+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;
+}
+
function convertResponsesBodyToOpenAiBody(
body: Record,
modelName: string,
stream: boolean,
): Record {
- const messages: Array<{ role: string; content: string }> = [];
+ const messages: Array> = [];
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;
+ const content = normalizeToolOutput(outputRaw);
+ messages.push({
+ role: 'tool',
+ tool_call_id: toolCallId,
+ content,
+ });
+ };
+
+ const pushUserMessage = (content: string) => {
+ if (!content) return;
+ messages.push({ role: 'user', content });
+ };
+
+ const processInputItem = (item: unknown) => {
+ if (typeof item === 'string') {
+ flushPendingToolCalls();
+ const text = item.trim();
+ if (text) pushUserMessage(text);
+ return;
+ }
+
+ if (!isRecord(item)) return;
+
+ const itemType = asTrimmedString(item.type).toLowerCase();
+ if (itemType === 'function_call') {
+ const toolCall = toOpenAiToolCall(item, functionCallIndex);
+ functionCallIndex += 1;
+ if (!toolCall) return;
+ pendingToolCalls.push(toolCall);
+ return;
+ }
+
+ if (itemType === 'function_call_output') {
+ flushPendingToolCalls();
+ pushToolOutputMessage(item.call_id ?? item.id, item.output ?? item.content);
+ return;
+ }
+
+ flushPendingToolCalls();
+
+ const role = asTrimmedString(item.role).toLowerCase() || 'user';
+ const messageContent = item.content ?? item.input ?? item;
+ const parsed = extractTextAndToolsFromResponsesMessage(
+ messageContent,
+ `${Date.now()}_${messages.length}`,
+ );
+
+ if (role === 'assistant') {
+ if (!parsed.text && parsed.toolCalls.length <= 0) return;
+
+ const assistantMessage: Record = {
+ role: 'assistant',
+ content: parsed.text,
+ };
+ if (parsed.toolCalls.length > 0) {
+ assistantMessage.tool_calls = parsed.toolCalls;
+ }
+ messages.push(assistantMessage);
+ return;
+ }
+
+ const normalizedRole = role === 'system' || role === 'developer'
+ ? 'system'
+ : role === 'tool'
+ ? 'tool'
+ : 'user';
+
+ if (normalizedRole === 'tool') {
+ pushToolOutputMessage(item.tool_call_id ?? item.call_id ?? item.id, messageContent);
+ return;
+ }
+
+ if (parsed.text) {
+ messages.push({
+ role: normalizedRole,
+ content: parsed.text,
+ });
+ }
+
+ for (const toolResult of parsed.toolResults) {
+ messages.push({
+ role: 'tool',
+ tool_call_id: toolResult.id,
+ content: toolResult.content,
+ });
+ }
+ };
if (typeof input === 'string') {
const text = input.trim();
- if (text) messages.push({ role: 'user', content: text });
+ if (text) {
+ messages.push({ role: 'user', content: text });
+ }
} else if (Array.isArray(input)) {
for (const item of input) {
- if (!isRecord(item)) continue;
- const role = typeof item.role === 'string' ? item.role : 'user';
- const text = normalizeText(item.content ?? item).trim();
- if (!text) continue;
- messages.push({ role: role === 'assistant' ? 'assistant' : (role === 'system' ? 'system' : 'user'), content: text });
+ processInputItem(item);
}
} else if (isRecord(input)) {
- const text = normalizeText(input).trim();
- if (text) messages.push({ role: 'user', content: text });
+ processInputItem(input);
}
+ flushPendingToolCalls();
- const instructions = typeof body.instructions === 'string' ? body.instructions.trim() : '';
+ const instructions = asTrimmedString(body.instructions);
if (instructions) {
messages.unshift({ role: 'system', content: instructions });
}
@@ -97,8 +373,9 @@ function convertResponsesBodyToOpenAiBody(
if (typeof body.max_output_tokens === 'number' && Number.isFinite(body.max_output_tokens)) {
payload.max_tokens = body.max_output_tokens;
}
- if (body.tools !== undefined) payload.tools = body.tools;
- if (body.tool_choice !== undefined) payload.tool_choice = body.tool_choice;
+ 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);
return payload;
}