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
105 changes: 103 additions & 2 deletions packages/actions/src/prepare-request-body.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { describe, expect, test } from "vitest";

import { models } from "@llmgateway/models";

import {
hashPromptCacheKey,
hashSessionCacheKey,
prepareRequestBody,
RequestError,
} from "./prepare-request-body.js";

import type { AnthropicRequestBody } from "@llmgateway/models";
import type {
AnthropicRequestBody,
ProviderModelMapping,
} from "@llmgateway/models";

function getCacheControl(block: unknown): unknown {
if (block && typeof block === "object" && "cache_control" in block) {
Expand Down Expand Up @@ -1101,6 +1106,102 @@ describe("prepareRequestBody - Moonshot thinking", () => {
expect(requestBody.max_completion_tokens).toBe(4096);
expect(requestBody.max_tokens).toBeUndefined();
});

type Effort = Parameters<typeof prepareRequestBody>[14];
const xaiEffortCases: Array<
[Parameters<typeof prepareRequestBody>[0], string, Extract<Effort, string>]
> = [
// xAI mappings that declare reasoning: true but previously omitted
// reasoning_effort from supportedParameters, silently dropping any
// effort value before the request was built (#3436, #3403).
["azure-ai-foundry", "grok-4-3", "medium"],
["azure-ai-foundry", "grok-4-1-fast-reasoning", "high"],
["xai", "grok-4", "high"],
];

test.each(xaiEffortCases)(
"forwards reasoning_effort for %s/%s once declared in supportedParameters",
async (provider, model, effort) => {
// Direct capability assertion on the mapping itself: the fix's
// whole point is that these mappings now declare
// reasoning_effort in supportedParameters. A serialization-only
// test could keep passing while the capability check silently
// regresses (e.g. a refactor that stops reading
// supportedParameters) — asserting the array directly catches
// that class of regression.
const modelDef = models.find((m) => m.id === model);
const mapping = modelDef?.providers.find(
(p) => p.providerId === provider,
) as ProviderModelMapping | undefined;
expect(mapping?.supportedParameters).toContain("reasoning_effort");

const requestBody = (await prepareRequestBody(
provider,
model,
null,
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
effort,
true, // supportsReasoning
)) as unknown as Record<string, unknown>;
expect(requestBody.reasoning_effort).toBe(effort);
},
);

// The mirror image of the table above, and the more fragile half: these
// Grok deployments reason unconditionally but answer `reasoning_effort`
// with 400 "does not support parameter reasoningEffort" (verified against
// api.x.ai). Declaring the parameter for them — the obvious-looking
// completion of #3436/#3403 — turns every effort-bearing request into an
// upstream 400, so the drop is load-bearing and must stay asserted.
const xaiEffortRejectingCases: Array<
[Parameters<typeof prepareRequestBody>[0], string]
> = [
["vertex-openai", "grok-4-20-reasoning"],
["xai", "grok-4-20-beta-0309-reasoning"],
["xai", "grok-build-0-1"],
];

test.each(xaiEffortRejectingCases)(
"drops reasoning_effort for %s/%s, which rejects it upstream",
async (provider, model) => {
const modelDef = models.find((m) => m.id === model);
const mapping = modelDef?.providers.find(
(p) => p.providerId === provider,
) as ProviderModelMapping | undefined;
expect(mapping?.reasoning).toBe(true);
expect(mapping?.supportedParameters).not.toContain("reasoning_effort");

const requestBody = (await prepareRequestBody(
provider,
model,
null,
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
"high",
true, // supportsReasoning
)) as unknown as Record<string, unknown>;
expect(requestBody.reasoning_effort).toBeUndefined();
},
);
});

describe("prepareRequestBody - Alibaba thinking", () => {
Expand Down Expand Up @@ -4304,7 +4405,7 @@ describe("prepareRequestBody - max_tokens forwarding", () => {
expect(requestBody.max_tokens).toBe(8192);
expect(requestBody.max_completion_tokens).toBeUndefined();
expect(requestBody.top_p).toBe(0.9);
expect(requestBody.reasoning_effort).toBeUndefined();
expect(requestBody.reasoning_effort).toBe("medium");
expect(requestBody.inferenceConfig).toBeUndefined();
});
});
Expand Down
31 changes: 29 additions & 2 deletions packages/models/src/models/xai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,15 @@ export const xaiModels = [
reasoning: true,
tools: true,
jsonOutput: true,
supportedParameters: xaiSupportedParamsNoFreqPresence,
supportedParameters: [
"temperature",
"max_tokens",
"top_p",
"response_format",
"tools",
"tool_choice",
"reasoning_effort",
],
},
],
},
Expand Down Expand Up @@ -425,7 +433,15 @@ export const xaiModels = [
reasoningOutput: "omit",
tools: true,
jsonOutput: true,
supportedParameters: xaiSupportedParamsNoFreqPresence,
supportedParameters: [
"temperature",
"max_tokens",
"top_p",
"response_format",
"tools",
"tool_choice",
"reasoning_effort",
],
},
],
},
Expand Down Expand Up @@ -573,6 +589,10 @@ export const xaiModels = [
reasoning: true,
tools: true,
jsonOutput: true,
// Always reasons, but rejects the effort knob: xAI answers
// `reasoning_effort` with 400 "does not support parameter
// reasoningEffort", so the omission here is deliberate and
// makes the gateway drop the value instead of forwarding it.
supportedParameters: xaiSupportedParamsNoFreqPresence,
},
],
Expand Down Expand Up @@ -641,6 +661,9 @@ export const xaiModels = [
reasoningOutput: "omit",
tools: true,
jsonOutput: true,
// Same upstream model as xai/grok-4-20-beta-0309-reasoning,
// which 400s on `reasoning_effort` — leaving it undeclared
// keeps the gateway from forwarding a value Grok 4.20 rejects.
supportedParameters: xaiSupportedParamsNoFreqPresence,
},
],
Expand Down Expand Up @@ -770,6 +793,7 @@ export const xaiModels = [
"top_p",
"tools",
"tool_choice",
"reasoning_effort",
],
},
],
Expand Down Expand Up @@ -981,6 +1005,9 @@ export const xaiModels = [
tools: true,
jsonOutput: true,
reasoning: true,
// Reasons by default but rejects the effort knob: xAI answers
// `reasoning_effort` with 400 "does not support parameter
// reasoningEffort", so it stays undeclared on purpose.
supportedParameters: xaiSupportedParamsNoFreqPresence,
},
],
Expand Down
Loading