Merge pull request #5196 from earendil-works/fix/opencode-thinking-requests
fix(ai): handle OpenCode reasoning params
This commit is contained in:
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed OpenCode Go Kimi K2.6 thinking requests to send `thinking` objects instead of invalid string values, and fixed OpenCode Zen Grok Build thinking requests to omit unsupported `reasoning_effort` ([#5169](https://github.com/earendil-works/pi/issues/5169)).
|
||||||
|
|
||||||
## [0.77.0] - 2026-05-28
|
## [0.77.0] - 2026-05-28
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -938,7 +938,7 @@ interface OpenAICompletionsCompat {
|
|||||||
requiresAssistantAfterToolResult?: boolean; // Whether tool results must be followed by an assistant message (default: false)
|
requiresAssistantAfterToolResult?: boolean; // Whether tool results must be followed by an assistant message (default: false)
|
||||||
requiresThinkingAsText?: boolean; // Whether thinking blocks must be converted to text (default: false)
|
requiresThinkingAsText?: boolean; // Whether thinking blocks must be converted to text (default: false)
|
||||||
requiresReasoningContentOnAssistantMessages?: boolean; // Whether all replayed assistant messages must include empty reasoning_content when reasoning is enabled (default: auto-detected for DeepSeek)
|
requiresReasoningContentOnAssistantMessages?: boolean; // Whether all replayed assistant messages must include empty reasoning_content when reasoning is enabled (default: auto-detected for DeepSeek)
|
||||||
thinkingFormat?: 'openai' | 'openrouter' | 'deepseek' | 'together' | 'zai' | 'qwen' | 'qwen-chat-template'; // Format for reasoning param: 'openai' uses reasoning_effort, 'openrouter' uses reasoning: { effort }, 'deepseek' uses thinking: { type } plus reasoning_effort, 'together' uses reasoning: { enabled } plus reasoning_effort when supported, 'zai' uses enable_thinking, 'qwen' uses enable_thinking, 'qwen-chat-template' uses chat_template_kwargs.enable_thinking (default: openai)
|
thinkingFormat?: 'openai' | 'openrouter' | 'deepseek' | 'together' | 'zai' | 'qwen' | 'qwen-chat-template'; // Format for reasoning param: 'openai' uses reasoning_effort, 'openrouter' uses reasoning: { effort }, 'deepseek' uses thinking: { type } plus reasoning_effort when supported, 'together' uses reasoning: { enabled } plus reasoning_effort when supported, 'zai' uses enable_thinking, 'qwen' uses enable_thinking, 'qwen-chat-template' uses chat_template_kwargs.enable_thinking (default: openai)
|
||||||
cacheControlFormat?: 'anthropic'; // Anthropic-style cache_control on system prompt, last tool, and last user/assistant text content
|
cacheControlFormat?: 'anthropic'; // Anthropic-style cache_control on system prompt, last tool, and last user/assistant text content
|
||||||
openRouterRouting?: OpenRouterRouting; // OpenRouter routing preferences (default: {})
|
openRouterRouting?: OpenRouterRouting; // OpenRouter routing preferences (default: {})
|
||||||
vercelGatewayRouting?: VercelGatewayRouting; // Vercel AI Gateway routing preferences (default: {})
|
vercelGatewayRouting?: VercelGatewayRouting; // Vercel AI Gateway routing preferences (default: {})
|
||||||
|
|||||||
@@ -275,7 +275,12 @@ function applyThinkingLevelMetadata(model: Model<any>): void {
|
|||||||
mergeThinkingLevelMap(model, { off: null });
|
mergeThinkingLevelMap(model, { off: null });
|
||||||
}
|
}
|
||||||
if (model.provider === "opencode-go" && model.id === "kimi-k2.6") {
|
if (model.provider === "opencode-go" && model.id === "kimi-k2.6") {
|
||||||
mergeThinkingLevelMap(model, { off: "none" });
|
// OpenCode Go exposes Kimi K2.6 thinking as on/off, not distinct effort tiers.
|
||||||
|
mergeThinkingLevelMap(model, { minimal: null, low: null, medium: null });
|
||||||
|
}
|
||||||
|
if (model.provider === "opencode" && model.id === "grok-build-0.1") {
|
||||||
|
// OpenCode Zen Grok Build reasons by default but rejects explicit reasoningEffort.
|
||||||
|
mergeThinkingLevelMap(model, { off: null, minimal: null, low: null, medium: null });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -896,6 +901,10 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
|
|||||||
baseUrl = `${variant.basePath}/v1`;
|
baseUrl = `${variant.basePath}/v1`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (variant.provider === "opencode" && modelId === "grok-build-0.1") {
|
||||||
|
compat = { ...(compat ?? {}), supportsReasoningEffort: false };
|
||||||
|
}
|
||||||
|
|
||||||
// Fix known mismatches between models.dev npm data and actual
|
// Fix known mismatches between models.dev npm data and actual
|
||||||
// OpenCode Go endpoint behaviour. models.dev reports these models
|
// OpenCode Go endpoint behaviour. models.dev reports these models
|
||||||
// as @ai-sdk/anthropic, but the OpenCode Go endpoints either don't
|
// as @ai-sdk/anthropic, but the OpenCode Go endpoints either don't
|
||||||
@@ -909,7 +918,9 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
|
|||||||
baseUrl = `${variant.basePath}/v1`;
|
baseUrl = `${variant.basePath}/v1`;
|
||||||
}
|
}
|
||||||
if (modelId === "kimi-k2.6") {
|
if (modelId === "kimi-k2.6") {
|
||||||
compat = { ...(compat ?? {}), thinkingFormat: "string-thinking" };
|
// OpenCode Go Kimi K2.6 accepts Anthropic-style thinking objects
|
||||||
|
// and rejects string thinking values or combined reasoning_effort.
|
||||||
|
compat = { ...(compat ?? {}), thinkingFormat: "deepseek", supportsReasoningEffort: false };
|
||||||
}
|
}
|
||||||
if (modelId === "qwen3.5-plus" || modelId === "qwen3.6-plus") {
|
if (modelId === "qwen3.5-plus" || modelId === "qwen3.6-plus") {
|
||||||
api = "openai-completions";
|
api = "openai-completions";
|
||||||
|
|||||||
@@ -571,7 +571,7 @@ function buildParams(
|
|||||||
};
|
};
|
||||||
} else if (compat.thinkingFormat === "deepseek" && model.reasoning) {
|
} else if (compat.thinkingFormat === "deepseek" && model.reasoning) {
|
||||||
(params as any).thinking = { type: options?.reasoningEffort ? "enabled" : "disabled" };
|
(params as any).thinking = { type: options?.reasoningEffort ? "enabled" : "disabled" };
|
||||||
if (options?.reasoningEffort) {
|
if (options?.reasoningEffort && compat.supportsReasoningEffort) {
|
||||||
(params as any).reasoning_effort =
|
(params as any).reasoning_effort =
|
||||||
model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort;
|
model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -389,7 +389,7 @@ export interface OpenAICompletionsCompat {
|
|||||||
requiresThinkingAsText?: boolean;
|
requiresThinkingAsText?: boolean;
|
||||||
/** Whether all replayed assistant messages must include an empty reasoning_content field when reasoning is enabled. Default: auto-detected from URL. */
|
/** Whether all replayed assistant messages must include an empty reasoning_content field when reasoning is enabled. Default: auto-detected from URL. */
|
||||||
requiresReasoningContentOnAssistantMessages?: boolean;
|
requiresReasoningContentOnAssistantMessages?: boolean;
|
||||||
/** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "openrouter" uses reasoning: { effort }, "deepseek" uses thinking: { type } plus reasoning_effort, "together" uses reasoning: { enabled } plus reasoning_effort when supported, "zai" uses top-level enable_thinking: boolean, "qwen" uses top-level enable_thinking: boolean, "qwen-chat-template" uses chat_template_kwargs.enable_thinking, and "string-thinking" uses top-level thinking: string. Default: "openai". */
|
/** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "openrouter" uses reasoning: { effort }, "deepseek" uses thinking: { type } plus reasoning_effort when supported, "together" uses reasoning: { enabled } plus reasoning_effort when supported, "zai" uses top-level enable_thinking: boolean, "qwen" uses top-level enable_thinking: boolean, "qwen-chat-template" uses chat_template_kwargs.enable_thinking, and "string-thinking" uses top-level thinking: string. Default: "openai". */
|
||||||
thinkingFormat?:
|
thinkingFormat?:
|
||||||
| "openai"
|
| "openai"
|
||||||
| "openrouter"
|
| "openrouter"
|
||||||
|
|||||||
@@ -995,7 +995,7 @@ describe("openai-completions tool_choice", () => {
|
|||||||
expect(messages[0]).not.toHaveProperty("reasoning");
|
expect(messages[0]).not.toHaveProperty("reasoning");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sends thinking none for OpenCode Go Kimi K2.6 when thinking is off", async () => {
|
it("sends thinking disabled for OpenCode Go Kimi K2.6 when thinking is off", async () => {
|
||||||
const model = getModel("opencode-go", "kimi-k2.6")!;
|
const model = getModel("opencode-go", "kimi-k2.6")!;
|
||||||
let payload: unknown;
|
let payload: unknown;
|
||||||
|
|
||||||
@@ -1012,12 +1012,12 @@ describe("openai-completions tool_choice", () => {
|
|||||||
},
|
},
|
||||||
).result();
|
).result();
|
||||||
|
|
||||||
const params = (payload ?? mockState.lastParams) as { thinking?: string; reasoning_effort?: string };
|
const params = (payload ?? mockState.lastParams) as { thinking?: unknown; reasoning_effort?: string };
|
||||||
expect(params.thinking).toBe("none");
|
expect(params.thinking).toEqual({ type: "disabled" });
|
||||||
expect(params.reasoning_effort).toBeUndefined();
|
expect(params.reasoning_effort).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sends thinking effort for OpenCode Go Kimi K2.6 when thinking is enabled", async () => {
|
it("sends thinking enabled for OpenCode Go Kimi K2.6 when thinking is enabled", async () => {
|
||||||
const model = getModel("opencode-go", "kimi-k2.6")!;
|
const model = getModel("opencode-go", "kimi-k2.6")!;
|
||||||
let payload: unknown;
|
let payload: unknown;
|
||||||
|
|
||||||
@@ -1035,8 +1035,30 @@ describe("openai-completions tool_choice", () => {
|
|||||||
},
|
},
|
||||||
).result();
|
).result();
|
||||||
|
|
||||||
const params = (payload ?? mockState.lastParams) as { thinking?: string; reasoning_effort?: string };
|
const params = (payload ?? mockState.lastParams) as { thinking?: unknown; reasoning_effort?: string };
|
||||||
expect(params.thinking).toBe("high");
|
expect(params.thinking).toEqual({ type: "enabled" });
|
||||||
|
expect(params.reasoning_effort).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits reasoning effort for OpenCode Grok Build", async () => {
|
||||||
|
const model = getModel("opencode", "grok-build-0.1")!;
|
||||||
|
let payload: unknown;
|
||||||
|
|
||||||
|
await streamSimple(
|
||||||
|
model,
|
||||||
|
{
|
||||||
|
messages: [{ role: "user", content: "Hi", timestamp: Date.now() }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiKey: "test",
|
||||||
|
reasoning: "high",
|
||||||
|
onPayload: (params: unknown) => {
|
||||||
|
payload = params;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
).result();
|
||||||
|
|
||||||
|
const params = (payload ?? mockState.lastParams) as { reasoning_effort?: string };
|
||||||
expect(params.reasoning_effort).toBeUndefined();
|
expect(params.reasoning_effort).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,18 @@ describe("getSupportedThinkingLevels", () => {
|
|||||||
expect(getSupportedThinkingLevels(model!)).toEqual(["off", "high", "xhigh"]);
|
expect(getSupportedThinkingLevels(model!)).toEqual(["off", "high", "xhigh"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("includes only high plus off for OpenCode Go Kimi K2.6", () => {
|
||||||
|
const model = getModel("opencode-go", "kimi-k2.6");
|
||||||
|
expect(model).toBeDefined();
|
||||||
|
expect(getSupportedThinkingLevels(model!)).toEqual(["off", "high"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes only high for OpenCode Grok Build", () => {
|
||||||
|
const model = getModel("opencode", "grok-build-0.1");
|
||||||
|
expect(model).toBeDefined();
|
||||||
|
expect(getSupportedThinkingLevels(model!)).toEqual(["high"]);
|
||||||
|
});
|
||||||
|
|
||||||
it("includes only high/xhigh plus off for DeepSeek V4 Flash on OpenRouter", () => {
|
it("includes only high/xhigh plus off for DeepSeek V4 Flash on OpenRouter", () => {
|
||||||
const model = getModel("openrouter", "deepseek/deepseek-v4-flash");
|
const model = getModel("openrouter", "deepseek/deepseek-v4-flash");
|
||||||
expect(model).toBeDefined();
|
expect(model).toBeDefined();
|
||||||
|
|||||||
Reference in New Issue
Block a user