mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(providers): skip unreadable OpenAI responses tool schemas
This commit is contained in:
@@ -122,6 +122,53 @@ describe("convertResponsesTools", () => {
|
||||
convertResponsesTools([zeta, alpha]).map((tool) => expectResponsesFunctionTool(tool).name),
|
||||
).toEqual(["alpha", "zeta"]);
|
||||
});
|
||||
|
||||
it("skips unreadable tool schemas while preserving healthy strict tools", () => {
|
||||
const unreadableSchemaTool = {
|
||||
name: "bad_schema",
|
||||
description: "Bad schema",
|
||||
get parameters(): never {
|
||||
throw new Error("parameters getter exploded");
|
||||
},
|
||||
} satisfies Tool;
|
||||
const unreadableNestedSchemaTool = {
|
||||
name: "bad_nested_schema",
|
||||
description: "Bad nested schema",
|
||||
parameters: {
|
||||
type: "object",
|
||||
get properties(): never {
|
||||
throw new Error("properties getter exploded");
|
||||
},
|
||||
},
|
||||
} satisfies Tool;
|
||||
const healthyTool = {
|
||||
name: "lookup_weather",
|
||||
description: "Get forecast",
|
||||
parameters: {},
|
||||
} satisfies Tool;
|
||||
|
||||
const converted = convertResponsesTools(
|
||||
[unreadableSchemaTool, unreadableNestedSchemaTool, healthyTool],
|
||||
{
|
||||
model: nativeOpenAIModel,
|
||||
},
|
||||
);
|
||||
|
||||
expect(converted).toEqual([
|
||||
{
|
||||
type: "function",
|
||||
name: "lookup_weather",
|
||||
description: "Get forecast",
|
||||
strict: true,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: [],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("convertResponsesMessages", () => {
|
||||
|
||||
@@ -24,6 +24,11 @@ type ResponsesFunctionTool = {
|
||||
parameters: Record<string, unknown>;
|
||||
strict?: boolean | null;
|
||||
};
|
||||
type ResponsesToolInput = {
|
||||
name: string;
|
||||
description?: string;
|
||||
parameters: unknown;
|
||||
};
|
||||
|
||||
// Converts OpenClaw tool schemas to OpenAI Responses tools, including strict-mode compatibility.
|
||||
const log = createSubsystemLogger("llm/openai-responses");
|
||||
@@ -35,27 +40,58 @@ export function convertResponsesTools(
|
||||
tools: Tool[],
|
||||
options?: ConvertResponsesToolsOptions,
|
||||
): OpenAITool[] {
|
||||
const readableTools = snapshotResponsesToolInputs(tools);
|
||||
const strictSetting = resolveResponsesStrictToolSetting(options);
|
||||
const strict = resolveResponsesStrictToolFlag(tools, strictSetting, options?.model);
|
||||
const strict = resolveResponsesStrictToolFlag(readableTools, strictSetting, options?.model);
|
||||
// Sort tools before request construction so prompt-cache bytes stay deterministic.
|
||||
return sortResponsesToolsByName(tools).map((tool) => {
|
||||
const result: ResponsesFunctionTool = {
|
||||
type: "function",
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: normalizeOpenAIStrictToolParameters(
|
||||
tool.parameters,
|
||||
strict === true,
|
||||
options?.model?.compat as OpenAIToolSchemaCompat,
|
||||
) as Record<string, unknown>,
|
||||
};
|
||||
if (strict !== undefined) {
|
||||
result.strict = strict;
|
||||
return sortResponsesToolsByName(readableTools).flatMap((tool) => {
|
||||
try {
|
||||
const result: ResponsesFunctionTool = {
|
||||
type: "function",
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: normalizeOpenAIStrictToolParameters(
|
||||
tool.parameters,
|
||||
strict === true,
|
||||
options?.model?.compat as OpenAIToolSchemaCompat,
|
||||
) as Record<string, unknown>,
|
||||
};
|
||||
if (strict !== undefined) {
|
||||
result.strict = strict;
|
||||
}
|
||||
return [result as OpenAITool];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
return result as OpenAITool;
|
||||
});
|
||||
}
|
||||
|
||||
function snapshotResponsesToolInputs(tools: readonly Tool[]): ResponsesToolInput[] {
|
||||
return tools.flatMap((tool) => {
|
||||
try {
|
||||
const description = tool.description;
|
||||
const parameters = materializeResponsesToolParameters(tool.parameters);
|
||||
return [
|
||||
{
|
||||
name: tool.name,
|
||||
...(description !== undefined ? { description } : {}),
|
||||
parameters,
|
||||
},
|
||||
];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function materializeResponsesToolParameters(parameters: unknown): unknown {
|
||||
const encoded = JSON.stringify(parameters ?? {});
|
||||
if (encoded === undefined) {
|
||||
throw new Error("OpenAI Responses tool parameters are not JSON serializable");
|
||||
}
|
||||
return JSON.parse(encoded) as unknown;
|
||||
}
|
||||
|
||||
function resolveResponsesStrictToolSetting(
|
||||
options: ConvertResponsesToolsOptions | undefined,
|
||||
): boolean | null | undefined {
|
||||
@@ -72,13 +108,18 @@ function resolveResponsesStrictToolSetting(
|
||||
}
|
||||
|
||||
function resolveResponsesStrictToolFlag(
|
||||
tools: Tool[],
|
||||
tools: readonly { name?: unknown; parameters: unknown }[],
|
||||
strictSetting: boolean | null | undefined,
|
||||
model: Model | undefined,
|
||||
): boolean | undefined {
|
||||
const strict = resolveOpenAIStrictToolFlagForInventory(tools, strictSetting);
|
||||
let strict: boolean | undefined;
|
||||
try {
|
||||
strict = resolveOpenAIStrictToolFlagForInventory(tools, strictSetting);
|
||||
} catch {
|
||||
strict = strictSetting === true ? false : strictSetting === false ? false : undefined;
|
||||
}
|
||||
if (strictSetting === true && strict === false && model && log.isEnabled("debug", "any")) {
|
||||
const diagnostics = findOpenAIStrictToolSchemaDiagnostics(tools);
|
||||
const diagnostics = readResponsesStrictToolSchemaDiagnostics(tools);
|
||||
if (shouldLogStrictToolDowngradeDiagnostic(diagnostics, model)) {
|
||||
const sample = diagnostics.slice(0, 5).map((entry) => ({
|
||||
tool: entry.toolName ?? `tool[${entry.toolIndex}]`,
|
||||
@@ -100,6 +141,16 @@ function resolveResponsesStrictToolFlag(
|
||||
return strict;
|
||||
}
|
||||
|
||||
function readResponsesStrictToolSchemaDiagnostics(
|
||||
tools: readonly { name?: unknown; parameters: unknown }[],
|
||||
): ReturnType<typeof findOpenAIStrictToolSchemaDiagnostics> {
|
||||
try {
|
||||
return findOpenAIStrictToolSchemaDiagnostics(tools);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function shouldLogStrictToolDowngradeDiagnostic(
|
||||
diagnostics: ReturnType<typeof findOpenAIStrictToolSchemaDiagnostics>,
|
||||
model: Model,
|
||||
|
||||
Reference in New Issue
Block a user