Skip to content
Draft
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/quick-frogs-lie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@openai/agents-extensions': minor
---

Fix open ai compatible models misuse '' in tools arguments call when an empty object is the valid option
64 changes: 60 additions & 4 deletions packages/agents-extensions/src/aiSdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,36 @@ function convertToAiSdkOutput(
);
}

function schemaAcceptsObject(schema: JSONSchema7 | undefined): boolean {
if (!schema) {
return false;
}
const schemaType = schema.type;
if (Array.isArray(schemaType)) {
if (schemaType.includes('object')) {
return true;
}
} else if (schemaType === 'object') {
return true;
}
return Boolean(schema.properties || schema.additionalProperties);
}

function expectsObjectArguments(
tool: SerializedTool | SerializedHandoff | undefined,
): boolean {
if (!tool) {
return false;
}
if ('toolName' in tool) {
return schemaAcceptsObject(tool.inputJsonSchema as JSONSchema7 | undefined);
}
if (tool.type === 'function') {
return schemaAcceptsObject(tool.parameters as JSONSchema7 | undefined);
}
return false;
}

/**
* @internal
* Converts a tool to a language model V2 tool.
Expand Down Expand Up @@ -481,15 +511,41 @@ export class AiSdkModel implements Model {
(c: any) => c && c.type === 'tool-call',
);
const hasToolCalls = toolCalls.length > 0;

const toolsNameToToolMap = new Map<
string,
SerializedTool | SerializedHandoff
>(request.tools.map((tool) => [tool.name, tool] as const));

for (const handoff of request.handoffs) {
toolsNameToToolMap.set(handoff.toolName, handoff);
}
for (const toolCall of toolCalls) {
const requestedTool =
typeof toolCall.toolName === 'string'
? toolsNameToToolMap.get(toolCall.toolName)
: undefined;

if (!requestedTool && toolCall.toolName) {
this.#logger.warn(
`Received tool call for unknown tool '${toolCall.toolName}'.`,
);
}

let toolCallArguments: string;
if (typeof toolCall.input === 'string') {
toolCallArguments =
toolCall.input === '' && expectsObjectArguments(requestedTool)
? JSON.stringify({})
: toolCall.input;
} else {
toolCallArguments = JSON.stringify(toolCall.input ?? {});
}
output.push({
type: 'function_call',
callId: toolCall.toolCallId,
name: toolCall.toolName,
arguments:
typeof toolCall.input === 'string'
? toolCall.input
: JSON.stringify(toolCall.input ?? {}),
arguments: toolCallArguments,
status: 'completed',
providerData: hasToolCalls ? result.providerMetadata : undefined,
});
Expand Down
104 changes: 104 additions & 0 deletions packages/agents-extensions/test/aiSdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,110 @@ describe('AiSdkModel.getResponse', () => {
]);
});

test('normalizes empty string tool input for object schemas', async () => {
const model = new AiSdkModel(
stubModel({
async doGenerate() {
return {
content: [
{
type: 'tool-call',
toolCallId: 'call-1',
toolName: 'objectTool',
input: '',
},
],
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
providerMetadata: { meta: true },
response: { id: 'id' },
finishReason: 'tool-calls',
warnings: [],
} as any;
},
}),
);

const res = await withTrace('t', () =>
model.getResponse({
input: 'hi',
tools: [
{
type: 'function',
name: 'objectTool',
description: 'accepts object',
parameters: {
type: 'object',
properties: {},
additionalProperties: false,
},
} as any,
],
handoffs: [],
modelSettings: {},
outputType: 'text',
tracing: false,
} as any),
);

expect(res.output).toHaveLength(1);
expect(res.output[0]).toMatchObject({
type: 'function_call',
arguments: '{}',
});
});

test('normalizes empty string tool input for handoff schemas', async () => {
const model = new AiSdkModel(
stubModel({
async doGenerate() {
return {
content: [
{
type: 'tool-call',
toolCallId: 'handoff-call',
toolName: 'handoffTool',
input: '',
},
],
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
providerMetadata: { meta: true },
response: { id: 'id' },
finishReason: 'tool-calls',
warnings: [],
} as any;
},
}),
);

const res = await withTrace('t', () =>
model.getResponse({
input: 'hi',
tools: [],
handoffs: [
{
toolName: 'handoffTool',
toolDescription: 'handoff accepts object',
inputJsonSchema: {
type: 'object',
properties: {},
additionalProperties: false,
},
strictJsonSchema: true,
} as any,
],
modelSettings: {},
outputType: 'text',
tracing: false,
} as any),
);

expect(res.output).toHaveLength(1);
expect(res.output[0]).toMatchObject({
type: 'function_call',
arguments: '{}',
});
});

test('forwards toolChoice to AI SDK (generate)', async () => {
const seen: any[] = [];
const model = new AiSdkModel(
Expand Down