fix(catalog): omitted forced tool choice for go responses

Applied OpenCode Go DeepSeek tool-choice compat to both OpenAI APIs, so the Responses route drops named selectors while preserving tools.

Added generated-policy and request-payload regression coverage.

Fixes #8243
This commit is contained in:
roboomp
2026-08-11 11:17:36 +00:00
parent 45e12e5bb7
commit f13ee010d0
5 changed files with 755 additions and 213 deletions
+27 -2
View File
@@ -1,6 +1,7 @@
import { describe, expect, it } from "bun:test";
import { type } from "@oh-my-pi/omptype";
import { streamOpenAICompletions } from "@oh-my-pi/pi-ai/providers/openai-completions";
import { type OpenAICompletionsOptions, streamOpenAICompletions } from "@oh-my-pi/pi-ai/providers/openai-completions";
import { type OpenAIResponsesOptions, streamOpenAIResponses } from "@oh-my-pi/pi-ai/providers/openai-responses";
import type { Context, Model, Tool } from "@oh-my-pi/pi-ai/types";
import { getBundledModel } from "@oh-my-pi/pi-catalog/models";
@@ -23,7 +24,7 @@ function abortedSignal(): AbortSignal {
async function capturePayload(
model: Model<"openai-completions">,
opts: Parameters<typeof streamOpenAICompletions>[2],
opts: OpenAICompletionsOptions,
): Promise<Record<string, unknown>> {
const { promise, resolve } = Promise.withResolvers<unknown>();
streamOpenAICompletions(model, context, {
@@ -35,12 +36,36 @@ async function capturePayload(
return (await promise) as Record<string, unknown>;
}
async function captureResponsesPayload(
model: Model<"openai-responses">,
opts: OpenAIResponsesOptions,
): Promise<Record<string, unknown>> {
const { promise, resolve } = Promise.withResolvers<unknown>();
streamOpenAIResponses(model, context, {
...opts,
apiKey: "test-key",
signal: abortedSignal(),
onPayload: payload => resolve(payload),
});
return (await promise) as Record<string, unknown>;
}
describe("OpenCode Go tool_choice compatibility", () => {
it("marks deepseek-v4-pro as not supporting tool_choice via compat override", () => {
const model = getBundledModel("opencode-go", "deepseek-v4-pro") as Model<"openai-completions">;
expect(model.compat?.supportsToolChoice).toBe(false);
});
it("omits forced tool_choice from DeepSeek Flash Responses payloads while preserving tools", async () => {
const model = getBundledModel("opencode-go", "deepseek-v4-flash") as Model<"openai-responses">;
expect(model.compat.supportsToolChoice).toBe(false);
const body = await captureResponsesPayload(model, {
toolChoice: { type: "tool", name: "echo" },
});
expect(body.tools).toEqual([expect.objectContaining({ type: "function", name: "echo" })]);
expect(body.tool_choice).toBeUndefined();
});
it("marks mimo-v2.5-pro as not supporting tool_choice via compat override", () => {
const model = getBundledModel("opencode-go", "mimo-v2.5-pro") as Model<"openai-completions">;
expect(model.compat?.supportsToolChoice).toBe(false);
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed `opencode-go/deepseek-v4-flash` Responses requests sending forced named `tool_choice` selectors that Console Go rejects while thinking mode is active.
## [17.2.12] - 2026-08-08
### Fixed
@@ -391,7 +391,7 @@ function applyGeneratedModelPolicy(model: ModelSpec<Api>): void {
};
}
if (
model.api === "openai-completions" &&
(model.api === "openai-completions" || model.api === "openai-responses") &&
model.provider === "opencode-go" &&
(model.id === "deepseek-v4-flash" || model.id === "deepseek-v4-pro")
) {
File diff suppressed because it is too large Load Diff
@@ -319,11 +319,11 @@ describe("generated model policies", () => {
expect(models[0]?.compat?.supportsToolChoice).toBe(false);
});
it("sets OpenCode Go DeepSeek V4 tool-call request compat", () => {
const models: ModelSpec<"openai-completions">[] = [
it("sets OpenCode Go DeepSeek V4 tool-call request compat for both OpenAI APIs", () => {
const models: ModelSpec<Api>[] = [
createSpec({
id: "deepseek-v4-flash",
api: "openai-completions",
api: "openai-responses",
provider: "opencode-go",
}),
createSpec({