mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(providers): skip unreadable OpenAI completion tool schemas
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import type { ChatCompletionChunk } from "openai/resources/chat/completions.js";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../../agents/system-prompt-cache-boundary.js";
|
||||
import type { Context, Model } from "../types.js";
|
||||
import type { Context, Model, Tool } from "../types.js";
|
||||
|
||||
type DeepPartial<T> = { [P in keyof T]?: DeepPartial<T[P]> };
|
||||
type OpenAICompatibleDelta = DeepPartial<ChatCompletionChunk["choices"][number]["delta"]> & {
|
||||
@@ -117,6 +117,60 @@ function makeFinishChunk(
|
||||
}
|
||||
|
||||
describe("OpenAI-compatible completions params", () => {
|
||||
it("skips unreadable tool schemas while preserving healthy tools", async () => {
|
||||
const unreadableSchemaTool = {
|
||||
name: "bad_schema",
|
||||
description: "Bad schema",
|
||||
get parameters(): never {
|
||||
throw new Error("parameters getter exploded");
|
||||
},
|
||||
} satisfies Tool;
|
||||
const healthyTool = {
|
||||
name: "lookup",
|
||||
description: "Lookup",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: { query: { type: "string" } },
|
||||
required: ["query"],
|
||||
},
|
||||
} satisfies Tool;
|
||||
let capturedTools: unknown;
|
||||
|
||||
const stream = streamOpenAICompletions(
|
||||
createModel(32_000),
|
||||
{
|
||||
messages: [{ role: "user", content: "lookup", timestamp: 1 }],
|
||||
tools: [unreadableSchemaTool, healthyTool],
|
||||
} satisfies Context,
|
||||
{
|
||||
apiKey: "sk-test",
|
||||
onPayload(payload) {
|
||||
capturedTools = (payload as { tools?: unknown }).tools;
|
||||
throw new Error("stop before network");
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const result = await stream.result();
|
||||
|
||||
expect(result.stopReason).toBe("error");
|
||||
expect(capturedTools).toEqual([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "lookup",
|
||||
description: "Lookup",
|
||||
strict: false,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: { query: { type: "string" } },
|
||||
required: ["query"],
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("clamps requested max tokens to the model output cap", async () => {
|
||||
let capturedMaxTokens: unknown;
|
||||
const stream = streamOpenAICompletions(createModel(32_000), context, {
|
||||
|
||||
@@ -1155,16 +1155,26 @@ function convertTools(
|
||||
tools: Tool[],
|
||||
compat: ResolvedOpenAICompletionsCompat,
|
||||
): OpenAI.Chat.Completions.ChatCompletionTool[] {
|
||||
return tools.map((tool) => ({
|
||||
type: "function",
|
||||
function: {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: tool.parameters as Record<string, unknown>, // TypeBox already generates JSON Schema
|
||||
// Only include strict if provider supports it. Some reject unknown fields.
|
||||
...(compat.supportsStrictMode && { strict: false }),
|
||||
},
|
||||
}));
|
||||
return tools.flatMap((tool) => {
|
||||
let parameters: Record<string, unknown>;
|
||||
try {
|
||||
parameters = tool.parameters as Record<string, unknown>; // TypeBox already generates JSON Schema
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters,
|
||||
// Only include strict if provider supports it. Some reject unknown fields.
|
||||
...(compat.supportsStrictMode && { strict: false }),
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
function parseChunkUsage(
|
||||
|
||||
Reference in New Issue
Block a user