fix(ai): map groq qwen3 reasoning effort values closes #1745

This commit is contained in:
Mario Zechner 2026-03-03 16:44:42 +01:00
parent 42579dd923
commit 7b96041068
5 changed files with 91 additions and 3 deletions

View file

@ -428,7 +428,7 @@ function buildParams(model: Model<"openai-completions">, context: Context, optio
(params as any).enable_thinking = !!options?.reasoningEffort; (params as any).enable_thinking = !!options?.reasoningEffort;
} else if (options?.reasoningEffort && model.reasoning && compat.supportsReasoningEffort) { } else if (options?.reasoningEffort && model.reasoning && compat.supportsReasoningEffort) {
// OpenAI-style reasoning_effort // OpenAI-style reasoning_effort
params.reasoning_effort = options.reasoningEffort; (params as any).reasoning_effort = mapReasoningEffort(options.reasoningEffort, compat.reasoningEffortMap);
} }
// OpenRouter provider routing preferences // OpenRouter provider routing preferences
@ -450,6 +450,13 @@ function buildParams(model: Model<"openai-completions">, context: Context, optio
return params; return params;
} }
function mapReasoningEffort(
effort: NonNullable<OpenAICompletionsOptions["reasoningEffort"]>,
reasoningEffortMap: Partial<Record<NonNullable<OpenAICompletionsOptions["reasoningEffort"]>, string>>,
): string {
return reasoningEffortMap[effort] ?? effort;
}
function maybeAddOpenRouterAnthropicCacheControl( function maybeAddOpenRouterAnthropicCacheControl(
model: Model<"openai-completions">, model: Model<"openai-completions">,
messages: ChatCompletionMessageParam[], messages: ChatCompletionMessageParam[],
@ -777,13 +784,26 @@ function detectCompat(model: Model<"openai-completions">): Required<OpenAIComple
const useMaxTokens = provider === "mistral" || baseUrl.includes("mistral.ai") || baseUrl.includes("chutes.ai"); const useMaxTokens = provider === "mistral" || baseUrl.includes("mistral.ai") || baseUrl.includes("chutes.ai");
const isGrok = provider === "xai" || baseUrl.includes("api.x.ai"); const isGrok = provider === "xai" || baseUrl.includes("api.x.ai");
const isGroq = provider === "groq" || baseUrl.includes("groq.com");
const isMistral = provider === "mistral" || baseUrl.includes("mistral.ai"); const isMistral = provider === "mistral" || baseUrl.includes("mistral.ai");
const reasoningEffortMap =
isGroq && model.id === "qwen/qwen3-32b"
? {
minimal: "default",
low: "default",
medium: "default",
high: "default",
xhigh: "default",
}
: {};
return { return {
supportsStore: !isNonStandard, supportsStore: !isNonStandard,
supportsDeveloperRole: !isNonStandard, supportsDeveloperRole: !isNonStandard,
supportsReasoningEffort: !isGrok && !isZai, supportsReasoningEffort: !isGrok && !isZai,
reasoningEffortMap,
supportsUsageInStreaming: true, supportsUsageInStreaming: true,
maxTokensField: useMaxTokens ? "max_tokens" : "max_completion_tokens", maxTokensField: useMaxTokens ? "max_tokens" : "max_completion_tokens",
requiresToolResultName: isMistral, requiresToolResultName: isMistral,
@ -809,6 +829,7 @@ function getCompat(model: Model<"openai-completions">): Required<OpenAICompletio
supportsStore: model.compat.supportsStore ?? detected.supportsStore, supportsStore: model.compat.supportsStore ?? detected.supportsStore,
supportsDeveloperRole: model.compat.supportsDeveloperRole ?? detected.supportsDeveloperRole, supportsDeveloperRole: model.compat.supportsDeveloperRole ?? detected.supportsDeveloperRole,
supportsReasoningEffort: model.compat.supportsReasoningEffort ?? detected.supportsReasoningEffort, supportsReasoningEffort: model.compat.supportsReasoningEffort ?? detected.supportsReasoningEffort,
reasoningEffortMap: model.compat.reasoningEffortMap ?? detected.reasoningEffortMap,
supportsUsageInStreaming: model.compat.supportsUsageInStreaming ?? detected.supportsUsageInStreaming, supportsUsageInStreaming: model.compat.supportsUsageInStreaming ?? detected.supportsUsageInStreaming,
maxTokensField: model.compat.maxTokensField ?? detected.maxTokensField, maxTokensField: model.compat.maxTokensField ?? detected.maxTokensField,
requiresToolResultName: model.compat.requiresToolResultName ?? detected.requiresToolResultName, requiresToolResultName: model.compat.requiresToolResultName ?? detected.requiresToolResultName,

View file

@ -235,6 +235,8 @@ export interface OpenAICompletionsCompat {
supportsDeveloperRole?: boolean; supportsDeveloperRole?: boolean;
/** Whether the provider supports `reasoning_effort`. Default: auto-detected from URL. */ /** Whether the provider supports `reasoning_effort`. Default: auto-detected from URL. */
supportsReasoningEffort?: boolean; supportsReasoningEffort?: boolean;
/** Optional mapping from pi-ai reasoning levels to provider/model-specific `reasoning_effort` values. */
reasoningEffortMap?: Partial<Record<ThinkingLevel, string>>;
/** Whether the provider supports `stream_options: { include_usage: true }` for token usage in streaming responses. Default: true. */ /** Whether the provider supports `stream_options: { include_usage: true }` for token usage in streaming responses. Default: true. */
supportsUsageInStreaming?: boolean; supportsUsageInStreaming?: boolean;
/** Which field to use for max tokens. Default: auto-detected from URL. */ /** Which field to use for max tokens. Default: auto-detected from URL. */

View file

@ -119,4 +119,60 @@ describe("openai-completions tool_choice", () => {
expect(tool?.strict).toBeUndefined(); expect(tool?.strict).toBeUndefined();
expect("strict" in (tool ?? {})).toBe(false); expect("strict" in (tool ?? {})).toBe(false);
}); });
it("maps groq qwen3 reasoning levels to default reasoning_effort", async () => {
const model = getModel("groq", "qwen/qwen3-32b")!;
let payload: unknown;
await streamSimple(
model,
{
messages: [
{
role: "user",
content: "Hi",
timestamp: Date.now(),
},
],
},
{
apiKey: "test",
reasoning: "medium",
onPayload: (params: unknown) => {
payload = params;
},
},
).result();
const params = (payload ?? mockState.lastParams) as { reasoning_effort?: string };
expect(params.reasoning_effort).toBe("default");
});
it("keeps normal reasoning_effort for groq models without compat mapping", async () => {
const model = getModel("groq", "openai/gpt-oss-20b")!;
let payload: unknown;
await streamSimple(
model,
{
messages: [
{
role: "user",
content: "Hi",
timestamp: Date.now(),
},
],
},
{
apiKey: "test",
reasoning: "medium",
onPayload: (params: unknown) => {
payload = params;
},
},
).result();
const params = (payload ?? mockState.lastParams) as { reasoning_effort?: string };
expect(params.reasoning_effort).toBe("medium");
});
}); });

View file

@ -23,6 +23,7 @@ const compat: Required<OpenAICompletionsCompat> = {
supportsStore: true, supportsStore: true,
supportsDeveloperRole: true, supportsDeveloperRole: true,
supportsReasoningEffort: true, supportsReasoningEffort: true,
reasoningEffortMap: {},
supportsUsageInStreaming: true, supportsUsageInStreaming: true,
maxTokensField: "max_completion_tokens", maxTokensField: "max_completion_tokens",
requiresToolResultName: false, requiresToolResultName: false,

View file

@ -172,10 +172,17 @@ models: [{
// ... // ...
compat: { compat: {
supportsDeveloperRole: false, // use "system" instead of "developer" supportsDeveloperRole: false, // use "system" instead of "developer"
supportsReasoningEffort: false, // disable reasoning_effort param supportsReasoningEffort: true,
reasoningEffortMap: { // map pi-ai levels to provider values
minimal: "default",
low: "default",
medium: "default",
high: "default",
xhigh: "default"
},
maxTokensField: "max_tokens", // instead of "max_completion_tokens" maxTokensField: "max_tokens", // instead of "max_completion_tokens"
requiresToolResultName: true, // tool results need name field requiresToolResultName: true, // tool results need name field
requiresMistralToolIds: true // tool IDs must be 9 alphanumeric chars requiresMistralToolIds: true,
thinkingFormat: "qwen" // uses enable_thinking: true thinkingFormat: "qwen" // uses enable_thinking: true
} }
}] }]
@ -568,6 +575,7 @@ interface ProviderModelConfig {
supportsStore?: boolean; supportsStore?: boolean;
supportsDeveloperRole?: boolean; supportsDeveloperRole?: boolean;
supportsReasoningEffort?: boolean; supportsReasoningEffort?: boolean;
reasoningEffortMap?: Partial<Record<"minimal" | "low" | "medium" | "high" | "xhigh", string>>;
supportsUsageInStreaming?: boolean; supportsUsageInStreaming?: boolean;
maxTokensField?: "max_completion_tokens" | "max_tokens"; maxTokensField?: "max_completion_tokens" | "max_tokens";
requiresToolResultName?: boolean; requiresToolResultName?: boolean;