Skip to content

Commit 57dfb6e

Browse files
committed
fix: normalize codex tool function fields
1 parent a296ce0 commit 57dfb6e

File tree

2 files changed

+35
-16
lines changed

2 files changed

+35
-16
lines changed

src/openai.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -209,14 +209,18 @@ type PassthroughToolSchemaKey = "tools" | "tool_choice" | "parallel_tool_calls"
209209
const normalizeCodexToolChoice = (value: unknown): unknown => {
210210
if (!isRecord(value)) return value;
211211
if (getString(value.type) !== "function") return value;
212-
if (typeof value["name"] === "string" && value["name"].trim()) return value;
212+
213+
const normalized: Record<string, unknown> = { ...value };
214+
const topLevelName = getString(normalized.name);
213215
const fn = isRecord(value.function) ? value.function : null;
214-
if (!fn) return value;
216+
if (!fn && !topLevelName) return value;
215217

216-
const name = getString(fn.name);
217-
if (!name) return value;
218+
if (!topLevelName) {
219+
const functionName = getString(fn?.name);
220+
if (!functionName) return value;
221+
normalized.name = functionName;
222+
}
218223

219-
const normalized: Record<string, unknown> = { ...value, name };
220224
delete normalized.function;
221225
return normalized;
222226
};
@@ -229,17 +233,17 @@ const normalizeCodexTools = (value: unknown): unknown => {
229233
const nestedFunction = isRecord(tool.function) ? tool.function : null;
230234
if (!nestedFunction) return tool;
231235

232-
const name = getString(nestedFunction.name);
233-
if (!name) return tool;
234-
if (typeof tool.name === "string" && tool.name.trim()) return tool;
236+
const normalized: Record<string, unknown> = { ...tool };
237+
const topLevelName = getString(normalized.name);
238+
const nestedName = getString(nestedFunction.name);
239+
if (!topLevelName && !nestedName) return tool;
235240

236-
const normalized: Record<string, unknown> = { ...tool, name };
237-
if (!("description" in normalized) && nestedFunction.description !== undefined) {
238-
normalized.description = nestedFunction.description;
241+
if (!topLevelName) {
242+
normalized.name = nestedName;
239243
}
240-
if (!("parameters" in normalized)) {
241-
const nestedParameters = nestedFunction.parameters;
242-
if (nestedParameters !== undefined) normalized.parameters = nestedParameters;
244+
for (const [key, nestedValue] of Object.entries(nestedFunction)) {
245+
if (key in normalized) continue;
246+
normalized[key] = nestedValue;
243247
}
244248
delete normalized.function;
245249
return normalized;

tests/openai-compat.test.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,9 +249,14 @@ Deno.test("openai: normalize function-style tools for codex compatibility", asyn
249249
name: "legacy_tool",
250250
description: "Already top-level tool name.",
251251
parameters: { type: "object", properties: {} },
252+
function: { strict: true },
252253
},
253254
],
254-
tool_choice: { type: "function", function: { name: "fetch_weather" } },
255+
tool_choice: {
256+
type: "function",
257+
name: "forced_choice",
258+
function: { name: "fetch_weather", strict: true },
259+
},
255260
}),
256261
}),
257262
),
@@ -265,11 +270,19 @@ Deno.test("openai: normalize function-style tools for codex compatibility", asyn
265270
assert.equal(recordedTools.length, 2);
266271
assert.equal(recordedTools[0]?.name, "fetch_weather");
267272
assert.equal(recordedTools[1]?.name, "legacy_tool");
273+
assert.equal(recordedTools[0]?.description, "Fetch weather for a city.");
274+
assert.deepEqual(recordedTools[0]?.parameters, {
275+
type: "object",
276+
properties: { city: { type: "string" } },
277+
});
278+
assert.equal(recordedTools[1]?.strict, true);
268279
assert.equal(Object.prototype.hasOwnProperty.call(recordedTools[0], "function"), false);
280+
assert.equal(Object.prototype.hasOwnProperty.call(recordedTools[1], "function"), false);
269281
const recordedToolChoice = recorded["tool_choice"] as Record<string, unknown> | undefined;
270282
assert.ok(recordedToolChoice);
271283
assert.equal(recordedToolChoice.type, "function");
272-
assert.equal(recordedToolChoice["name"], "fetch_weather");
284+
assert.equal(recordedToolChoice["name"], "forced_choice");
285+
assert.equal(Object.prototype.hasOwnProperty.call(recordedToolChoice, "strict"), false);
273286
assert.equal(Object.prototype.hasOwnProperty.call(recordedToolChoice, "function"), false);
274287
});
275288

@@ -310,6 +323,8 @@ Deno.test("openai: normalize function-style tools for codex compatibility", asyn
310323
assert.ok(Array.isArray(recordedTools));
311324
assert.equal(recordedTools.length, 1);
312325
assert.equal(recordedTools[0]?.name, "fetch_weather");
326+
assert.equal(recordedTools[0]?.description, "Fetch weather for a city.");
327+
assert.deepEqual(recordedTools[0]?.parameters, { type: "object", properties: { city: { type: "string" } } });
313328
assert.equal(Object.prototype.hasOwnProperty.call(recordedTools[0], "function"), false);
314329
});
315330
});

0 commit comments

Comments
 (0)