omp: add patch that fixes deepseek
This commit is contained in:
@@ -38,7 +38,12 @@ in
|
||||
{
|
||||
home.packages = [
|
||||
(inputs.llm-agents.packages.${pkgs.stdenv.hostPlatform.system}.omp.overrideAttrs (old: {
|
||||
patches = (old.patches or [ ]) ++ [ ];
|
||||
patches = (old.patches or [ ]) ++ [
|
||||
# Retry without strict tools when DeepSeek (via OpenRouter) rejects strict-mode `anyOf`
|
||||
# nullable unions with `Invalid tool parameters schema : field \`anyOf\`: missing field \`type\``.
|
||||
# Upstream PR: pending; applies cleanly against v14.2.1.
|
||||
../../patches/omp/0001-openai-completions-retry-without-strict-on-deepseek-openrouter.patch
|
||||
];
|
||||
}))
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
Subject: [PATCH] fix(openai-completions): retry without strict tools for DeepSeek-via-OpenRouter anyOf rejections
|
||||
|
||||
The retry-on-strict-tool-error path in openai-completions failed to recover when
|
||||
DeepSeek (and similar backends fronted by OpenRouter) reject strict-mode tool
|
||||
schemas with errors of the form:
|
||||
|
||||
Invalid tool parameters schema : field `anyOf`: missing field `type`
|
||||
|
||||
Two reasons:
|
||||
|
||||
1. Retry only triggered in "all_strict" mode. OpenRouter defaults to "mixed"
|
||||
(per-tool strict), so the early return prevented retry.
|
||||
2. The error-message regex required "strict" near "tool". DeepSeek's message
|
||||
never mentions "strict".
|
||||
|
||||
Fix:
|
||||
- Allow retry whenever any tool was sent with strict (i.e. mode != "none").
|
||||
- Recognize "Invalid tool parameters" in the regex.
|
||||
|
||||
Includes a regression test reproducing the exact DeepSeek error body via
|
||||
OpenRouter mixed-strict mode.
|
||||
|
||||
Applies cleanly against v14.2.1.
|
||||
|
||||
---
|
||||
diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts
|
||||
index e58189607..3c20631c1 100644
|
||||
--- a/packages/ai/src/providers/openai-completions.ts
|
||||
+++ b/packages/ai/src/providers/openai-completions.ts
|
||||
@@ -1245,7 +1245,10 @@ function shouldRetryWithoutStrictTools(
|
||||
toolStrictMode: AppliedToolStrictMode,
|
||||
tools: Tool[] | undefined,
|
||||
): boolean {
|
||||
- if (!tools || tools.length === 0 || toolStrictMode !== "all_strict") {
|
||||
+ // Retry whenever any tool was sent with `strict: true`. OpenRouter routes to underlying
|
||||
+ // providers (e.g. DeepSeek) whose schema validators reject the strict-mode `anyOf` shape
|
||||
+ // even when omp emitted strict per-tool ("mixed"), not just provider-wide ("all_strict").
|
||||
+ if (!tools || tools.length === 0 || toolStrictMode === "none") {
|
||||
return false;
|
||||
}
|
||||
const status = extractHttpStatusFromError(error) ?? capturedErrorResponse?.status;
|
||||
@@ -1255,7 +1258,14 @@ function shouldRetryWithoutStrictTools(
|
||||
const messageParts = [error instanceof Error ? error.message : undefined, capturedErrorResponse?.bodyText]
|
||||
.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
||||
.join("\n");
|
||||
- return /wrong_api_format|mixed values for 'strict'|tool[s]?\b.*strict|\bstrict\b.*tool/i.test(messageParts);
|
||||
+ // Patterns:
|
||||
+ // - `wrong_api_format`, `mixed values for 'strict'`: OpenAI rejecting mixed strict flags.
|
||||
+ // - `tool ... strict` / `strict ... tool`: generic strict-tool complaints.
|
||||
+ // - `Invalid tool parameters schema`: DeepSeek (via OpenRouter) rejecting strict-mode
|
||||
+ // nullable unions because their validator demands `type` alongside `anyOf`.
|
||||
+ return /wrong_api_format|mixed values for 'strict'|tool[s]?\b.*strict|\bstrict\b.*tool|invalid tool parameters/i.test(
|
||||
+ messageParts,
|
||||
+ );
|
||||
}
|
||||
|
||||
function mapStopReason(reason: ChatCompletionChunk.Choice["finish_reason"] | string): {
|
||||
diff --git a/packages/ai/test/openai-tool-strict-mode.test.ts b/packages/ai/test/openai-tool-strict-mode.test.ts
|
||||
index 2bf17e6d8..24d5a09d5 100644
|
||||
--- a/packages/ai/test/openai-tool-strict-mode.test.ts
|
||||
+++ b/packages/ai/test/openai-tool-strict-mode.test.ts
|
||||
@@ -231,6 +231,64 @@ describe("OpenAI tool strict mode", () => {
|
||||
expect(result.content).toContainEqual({ type: "text", text: "Hello" });
|
||||
expect(strictFlags).toEqual([[true], [false]]);
|
||||
});
|
||||
+ it("retries with non-strict tool schemas when OpenRouter backend rejects strict anyOf nullable unions", async () => {
|
||||
+ // Reproduces deepseek/deepseek-v4-pro via OpenRouter rejecting the strict-mode schema with:
|
||||
+ // 400 Provider returned error
|
||||
+ // {"error":{"message":"Invalid tool parameters schema : field `anyOf`: missing field `type`",...}}
|
||||
+ // OpenRouter is in mixed-strict mode by default (per-tool strict), so the original retry condition
|
||||
+ // (only "all_strict") prevented recovery. The retry now triggers whenever any tool sent strict=true.
|
||||
+ const model = getBundledModel("openrouter", "deepseek/deepseek-v3.2") as Model<"openai-completions">;
|
||||
+ const strictFlags: boolean[][] = [];
|
||||
+ global.fetch = Object.assign(
|
||||
+ async (_input: string | URL | Request, init?: RequestInit): Promise<Response> => {
|
||||
+ const bodyText = typeof init?.body === "string" ? init.body : "";
|
||||
+ const payload = JSON.parse(bodyText) as {
|
||||
+ tools?: Array<{ function?: { strict?: boolean } }>;
|
||||
+ };
|
||||
+ strictFlags.push((payload.tools ?? []).map(tool => tool.function?.strict === true));
|
||||
+ if (strictFlags.length === 1) {
|
||||
+ return new Response(
|
||||
+ JSON.stringify({
|
||||
+ error: {
|
||||
+ message: "Invalid tool parameters schema : field `anyOf`: missing field `type`",
|
||||
+ type: "invalid_request_error",
|
||||
+ param: null,
|
||||
+ code: "invalid_request_error",
|
||||
+ },
|
||||
+ }),
|
||||
+ {
|
||||
+ status: 400,
|
||||
+ headers: { "content-type": "application/json" },
|
||||
+ },
|
||||
+ );
|
||||
+ }
|
||||
+ return createSseResponse([
|
||||
+ {
|
||||
+ id: "chatcmpl-or",
|
||||
+ object: "chat.completion.chunk",
|
||||
+ created: 0,
|
||||
+ model: model.id,
|
||||
+ choices: [{ index: 0, delta: { content: "Hello" } }],
|
||||
+ },
|
||||
+ {
|
||||
+ id: "chatcmpl-or",
|
||||
+ object: "chat.completion.chunk",
|
||||
+ created: 0,
|
||||
+ model: model.id,
|
||||
+ choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
||||
+ },
|
||||
+ "[DONE]",
|
||||
+ ]);
|
||||
+ },
|
||||
+ { preconnect: originalFetch.preconnect },
|
||||
+ );
|
||||
+
|
||||
+ const result = await streamOpenAICompletions(model, testContext, { apiKey: "test-key" }).result();
|
||||
+ expect(result.stopReason).toBe("stop");
|
||||
+ expect(result.content).toContainEqual({ type: "text", text: "Hello" });
|
||||
+ expect(strictFlags).toEqual([[true], [false]]);
|
||||
+ });
|
||||
+
|
||||
|
||||
it("sends strict=true for openai-responses tool schemas on OpenAI", async () => {
|
||||
const model = getBundledModel("openai", "gpt-5-mini") as Model<"openai-responses">;
|
||||
Reference in New Issue
Block a user