Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions apps/docs/content/features/reasoning.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Add the `reasoning_effort` parameter directly to your request:
- `medium` - Balanced reasoning for most tasks
- `high` - Deep reasoning for complex problems
- `xhigh` - Very deep reasoning for the most complex problems
- `max` - Highest reasoning tier, above `xhigh`. Supported by Anthropic thinking models and OpenAI GPT-5.6 models. Effort tiers are never downgraded by the gateway: providers that accept an effort parameter receive the value unchanged (unsupported values result in a provider error), while providers that take a thinking budget instead (Anthropic, Google) have each tier translated to a native budget
- `max` - Highest reasoning tier, above `xhigh`. Supported by Anthropic thinking models and OpenAI GPT-5.6 models. Effort tiers are never downgraded by the gateway: providers that accept an effort parameter receive the value unchanged (unsupported values result in a provider error), while providers that take a thinking budget instead (Anthropic, Google, Alibaba) have each tier translated to a native budget

<Callout type="info">
OpenAI's reasoning models do not all accept the same effort values. The
Expand Down Expand Up @@ -78,7 +78,7 @@ Use the unified `reasoning` configuration object with an `effort` field:
- `medium` - Balanced reasoning for most tasks
- `high` - Deep reasoning for complex problems
- `xhigh` - Very deep reasoning for the most complex problems
- `max` - Highest reasoning tier, above `xhigh`. Supported by Anthropic thinking models and OpenAI GPT-5.6 models. Effort tiers are never downgraded by the gateway: providers that accept an effort parameter receive the value unchanged (unsupported values result in a provider error), while providers that take a thinking budget instead (Anthropic, Google) have each tier translated to a native budget
- `max` - Highest reasoning tier, above `xhigh`. Supported by Anthropic thinking models and OpenAI GPT-5.6 models. Effort tiers are never downgraded by the gateway: providers that accept an effort parameter receive the value unchanged (unsupported values result in a provider error), while providers that take a thinking budget instead (Anthropic, Google, Alibaba) have each tier translated to a native budget

```bash
curl -X POST "https://api.llmgateway.io/v1/chat/completions" \
Expand Down Expand Up @@ -142,7 +142,8 @@ For models that support it, you can specify an exact token budget for reasoning
<Callout type="info">
When `reasoning.max_tokens` is specified, it overrides `reasoning.effort` and
`reasoning_effort`. Supported by Anthropic Claude and Google Gemini thinking
models.
models, plus Alibaba-hosted thinking models (forwarded as DashScope's
`thinking_budget`).
</Callout>

### Example Request
Expand Down
252 changes: 252 additions & 0 deletions packages/actions/src/prepare-request-body.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,258 @@ describe("prepareRequestBody - Moonshot thinking", () => {
});
});

describe("prepareRequestBody - Alibaba thinking", () => {
async function prepare(options: {
model: string;
reasoningEffort?:
| "none"
| "minimal"
| "low"
| "medium"
| "high"
| "xhigh"
| "max";
reasoningMaxTokens?: number;
maxTokens?: number;
supportsReasoning?: boolean;
}) {
return (await prepareRequestBody(
"alibaba",
options.model,
null,
options.model,
[{ role: "user", content: "Hello!" }],
false, // stream
undefined, // temperature
options.maxTokens, // max_tokens
undefined, // top_p
undefined, // frequency_penalty
undefined, // presence_penalty
undefined, // response_format
undefined, // tools
undefined, // tool_choice
options.reasoningEffort, // reasoning_effort
options.supportsReasoning ?? true, // supportsReasoning
false, // isProd
20, // maxImageSizeMB
null, // userPlan
undefined, // sensitive_word_check
undefined, // image_config
undefined, // effort
undefined, // imageGenerations
undefined, // webSearchTool
options.reasoningMaxTokens, // reasoning_max_tokens
)) as any;
}

test("maps reasoning_effort to enable_thinking with a native thinking_budget and never forwards reasoning_effort", async () => {
const requestBody = await prepare({
model: "qwen3.7-max",
reasoningEffort: "high",
});
expect(requestBody.enable_thinking).toBe(true);
expect(requestBody.thinking_budget).toBe(24576);
expect(requestBody.reasoning_effort).toBeUndefined();
});

test("maps none to an explicit thinking disable", async () => {
const requestBody = await prepare({
model: "qwen3.7-max",
reasoningEffort: "none",
});
expect(requestBody.enable_thinking).toBe(false);
expect(requestBody.thinking_budget).toBeUndefined();
expect(requestBody.reasoning_effort).toBeUndefined();
});

test("keeps the provider default when no reasoning parameter is requested", async () => {
const requestBody = await prepare({ model: "qwen3.7-max" });
expect(requestBody.enable_thinking).toBeUndefined();
expect(requestBody.thinking_budget).toBeUndefined();
expect(requestBody.reasoning_effort).toBeUndefined();
});

test("forwards reasoning.max_tokens as thinking_budget verbatim", async () => {
const requestBody = await prepare({
model: "glm-5.2",
reasoningMaxTokens: 1234,
});
expect(requestBody.enable_thinking).toBe(true);
expect(requestBody.thinking_budget).toBe(1234);
});

test("keeps thinking_budget below the caller's max_tokens", async () => {
const requestBody = await prepare({
model: "glm-5.2",
reasoningEffort: "medium",
maxTokens: 100,
});
expect(requestBody.enable_thinking).toBe(true);
expect(requestBody.thinking_budget).toBe(99);
expect(requestBody.max_tokens).toBe(100);
});

test("sends nothing for mappings without budget-controlled thinking", async () => {
const requestBody = await prepare({
model: "kimi-k2.5",
reasoningEffort: "high",
});
expect(requestBody.enable_thinking).toBeUndefined();
expect(requestBody.thinking_budget).toBeUndefined();
expect(requestBody.reasoning_effort).toBeUndefined();
});
});

describe("prepareRequestBody - MiniMax thinking", () => {
async function prepare(options: {
model: string;
reasoningEffort?:
| "none"
| "minimal"
| "low"
| "medium"
| "high"
| "xhigh"
| "max";
}) {
return (await prepareRequestBody(
"minimax",
options.model,
null,
options.model,
[{ role: "user", content: "Hello!" }],
false, // stream
undefined, // temperature
undefined, // max_tokens
undefined, // top_p
undefined, // frequency_penalty
undefined, // presence_penalty
undefined, // response_format
undefined, // tools
undefined, // tool_choice
options.reasoningEffort, // reasoning_effort
true, // supportsReasoning
)) as any;
}

test("maps reasoning_effort to thinking adaptive and never forwards reasoning_effort", async () => {
const requestBody = await prepare({
model: "minimax-m3",
reasoningEffort: "high",
});
expect(requestBody.thinking).toEqual({ type: "adaptive" });
expect(requestBody.reasoning_effort).toBeUndefined();
});

test("maps none to thinking disabled on models that can turn thinking off", async () => {
const requestBody = await prepare({
model: "minimax-m3",
reasoningEffort: "none",
});
expect(requestBody.thinking).toEqual({ type: "disabled" });
expect(requestBody.reasoning_effort).toBeUndefined();
});

test("keeps the provider default when no reasoning_effort is requested", async () => {
const requestBody = await prepare({ model: "minimax-m3" });
expect(requestBody.thinking).toBeUndefined();
expect(requestBody.reasoning_effort).toBeUndefined();
});

test("never sends disabled to always-on models (M2.x family)", async () => {
const requestBody = await prepare({
model: "minimax-m2.7",
reasoningEffort: "none",
});
expect(requestBody.thinking).toBeUndefined();
expect(requestBody.reasoning_effort).toBeUndefined();
});

test("sends thinking adaptive to always-on models when effort is requested", async () => {
const requestBody = await prepare({
model: "minimax-m2.7",
reasoningEffort: "medium",
});
expect(requestBody.thinking).toEqual({ type: "adaptive" });
expect(requestBody.extra_body).toEqual({ reasoning_split: true });
});
});

describe("prepareRequestBody - Xiaomi thinking", () => {
async function prepare(options: {
model: string;
reasoningEffort?:
| "none"
| "minimal"
| "low"
| "medium"
| "high"
| "xhigh"
| "max";
}) {
return (await prepareRequestBody(
"xiaomi",
options.model,
null,
options.model,
[{ role: "user", content: "Hello!" }],
false, // stream
undefined, // temperature
undefined, // max_tokens
undefined, // top_p
undefined, // frequency_penalty
undefined, // presence_penalty
undefined, // response_format
undefined, // tools
undefined, // tool_choice
options.reasoningEffort, // reasoning_effort
true, // supportsReasoning
)) as any;
}

test("forwards native effort tiers verbatim without a thinking parameter", async () => {
const requestBody = await prepare({
model: "mimo-v2.5-pro",
reasoningEffort: "high",
});
expect(requestBody.reasoning_effort).toBe("high");
expect(requestBody.thinking).toBeUndefined();
});

test("maps none to thinking disabled and never forwards reasoning_effort", async () => {
const requestBody = await prepare({
model: "mimo-v2.5-pro",
reasoningEffort: "none",
});
expect(requestBody.thinking).toEqual({ type: "disabled" });
expect(requestBody.reasoning_effort).toBeUndefined();
});

test("keeps the provider default when no reasoning_effort is requested", async () => {
const requestBody = await prepare({ model: "mimo-v2.5-pro" });
expect(requestBody.thinking).toBeUndefined();
expect(requestBody.reasoning_effort).toBeUndefined();
});

test("forwards unsupported tiers verbatim so the provider rejects them", async () => {
const requestBody = await prepare({
model: "mimo-v2.5",
reasoningEffort: "xhigh",
});
expect(requestBody.reasoning_effort).toBe("xhigh");
expect(requestBody.thinking).toBeUndefined();
});

test("drops none for mappings that do not declare it", async () => {
const requestBody = await prepare({
model: "mimo-v2-pro",
reasoningEffort: "none",
});
expect(requestBody.thinking).toBeUndefined();
expect(requestBody.reasoning_effort).toBeUndefined();
});
});

describe("prepareRequestBody - reasoning_effort max", () => {
async function prepare(options: {
provider: Parameters<typeof prepareRequestBody>[0];
Expand Down
Loading
Loading