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
18 changes: 17 additions & 1 deletion src/app/api/providers/[id]/test/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,21 @@ const OAUTH_TEST_CONFIG = {
authPrefix: "Bearer ",
refreshable: true,
},
xai: {
url: "https://api.x.ai/v1/chat/completions",
method: "POST",
authHeader: "Authorization",
authPrefix: "Bearer ",
extraHeaders: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "grok-4.3",
messages: [{ role: "user", content: "ping" }],
max_tokens: 1,
stream: false,
reasoning: { effort: "high" },
}),
refreshable: true,
},
github: {
url: "https://api.github.com/user",
method: "GET",
Expand Down Expand Up @@ -561,7 +576,8 @@ export async function testOAuthConnection(
// 400 because the probe body is invalid. A 400 from such a provider means auth
// succeeded; only 401/403 means the token is bad.
const accepted =
res.ok || (Array.isArray(config.acceptStatuses) && config.acceptStatuses.includes(res.status));
res.ok ||
(Array.isArray(config.acceptStatuses) && config.acceptStatuses.includes(res.status));
if (accepted) {
return {
valid: true,
Expand Down
20 changes: 16 additions & 4 deletions src/lib/providers/xai/thinking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ const VALID_EFFORTS = new Set(["minimal", "low", "medium", "high"]);

export type ReasoningEffort = "minimal" | "low" | "medium" | "high";

export function normalizeXaiReasoningEffort(effort: unknown): ReasoningEffort | undefined {
if (typeof effort !== "string") return undefined;
const normalized = effort.toLowerCase();
if (normalized === "max" || normalized === "xhigh") return "high";
return VALID_EFFORTS.has(normalized) ? (normalized as ReasoningEffort) : undefined;
}

/**
* Map a numeric token budget to a discrete effort tier.
* <=0 → undefined (disabled)
Expand Down Expand Up @@ -69,22 +76,27 @@ interface ApplyThinkingOptions {
*/
export function applyThinking(
request: ThinkingRequest,
options: ApplyThinkingOptions = {},
options: ApplyThinkingOptions = {}
): ThinkingRequest {
if (!request || typeof request !== "object") return request;
const out: ThinkingRequest = { ...request };

// 1) Already xAI-native? Honor and stop.
if (out.reasoning && typeof out.reasoning === "object") {
const reasoning = out.reasoning as Record<string, unknown>;
if (typeof reasoning.effort === "string" && VALID_EFFORTS.has(reasoning.effort)) {
const normalizedEffort = normalizeXaiReasoningEffort(reasoning.effort);
if (normalizedEffort) {
if (reasoning.effort !== normalizedEffort) {
out.reasoning = { ...reasoning, effort: normalizedEffort };
}
return out;
}
}

// 2) OpenAI Chat reasoning_effort
if (typeof out.reasoning_effort === "string" && VALID_EFFORTS.has(out.reasoning_effort)) {
out.reasoning = { effort: out.reasoning_effort as ReasoningEffort };
const reasoningEffort = normalizeXaiReasoningEffort(out.reasoning_effort);
if (reasoningEffort) {
out.reasoning = { effort: reasoningEffort };
delete out.reasoning_effort;
return out;
}
Expand Down
20 changes: 13 additions & 7 deletions src/lib/providers/xai/translators/openai-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* - aggregated xAI response.completed → OpenAI ChatCompletion JSON
* - per-event xAI SSE → OpenAI ChatCompletion stream chunks
*/
import { normalizeXaiReasoningEffort } from "../thinking.ts";

// ─── Types ────────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -192,8 +193,7 @@ export function chatRequestToXaiResponses(req: OpenAiChatRequest): XaiResponsesR
input.push({
type: "function_call_output",
call_id: m.tool_call_id,
output:
typeof m.content === "string" ? m.content : JSON.stringify(m.content ?? ""),
output: typeof m.content === "string" ? m.content : JSON.stringify(m.content ?? ""),
});
continue;
}
Expand Down Expand Up @@ -229,8 +229,15 @@ export function chatRequestToXaiResponses(req: OpenAiChatRequest): XaiResponsesR
if (req.response_format) out.text = { format: req.response_format };
if (req.parallel_tool_calls != null) out.parallel_tool_calls = req.parallel_tool_calls;
if (req.seed != null) out.seed = req.seed;
if (req.reasoning_effort) out.reasoning = { effort: req.reasoning_effort };
if (req.reasoning) out.reasoning = req.reasoning;
if (req.reasoning_effort) {
const effort = normalizeXaiReasoningEffort(req.reasoning_effort);
if (effort) out.reasoning = { effort };
}
if (req.reasoning && typeof req.reasoning === "object") {
const reasoning = req.reasoning as Record<string, unknown>;
const effort = normalizeXaiReasoningEffort(reasoning.effort);
out.reasoning = effort ? { ...reasoning, effort } : reasoning;
}
if (req.tool_choice) out.tool_choice = req.tool_choice;

const tools = req.tools ? toolsPassthrough(req.tools) : undefined;
Expand Down Expand Up @@ -272,7 +279,7 @@ function extractAssistantTextAndCalls(completed: XaiCompleted): {
*/
export function xaiCompletedToChatJson(
completed: XaiCompleted,
origReq: OpenAiChatRequest | null = null,
origReq: OpenAiChatRequest | null = null
): object {
const { text, toolCalls, refusal } = extractAssistantTextAndCalls(completed);
const finishReason = toolCalls.length ? "tool_calls" : "stop";
Expand All @@ -296,8 +303,7 @@ export function xaiCompletedToChatJson(
out.usage = {
prompt_tokens: u.input_tokens ?? u.prompt_tokens ?? 0,
completion_tokens: u.output_tokens ?? u.completion_tokens ?? 0,
total_tokens:
u.total_tokens ?? ((u.input_tokens ?? 0) + (u.output_tokens ?? 0)),
total_tokens: u.total_tokens ?? (u.input_tokens ?? 0) + (u.output_tokens ?? 0),
};
}
return out;
Expand Down
65 changes: 45 additions & 20 deletions tests/unit/xai-translators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,16 @@
import test from "node:test";
import assert from "node:assert/strict";

const { budgetToEffort, applyThinking } = await import(
"../../src/lib/providers/xai/thinking.ts"
);
const { chatRequestToXaiResponses, xaiCompletedToChatJson } = await import(
"../../src/lib/providers/xai/translators/openai-chat.ts"
);
const {
openaiResponsesRequestToXai,
xaiCompletedToOpenaiResponses,
xaiSseEventToOpenaiResponses,
} = await import("../../src/lib/providers/xai/translators/openai-responses.ts");
const { claudeRequestToXaiResponses, xaiCompletedToClaudeJson } = await import(
"../../src/lib/providers/xai/translators/claude.ts"
);
const { geminiRequestToXaiResponses, xaiCompletedToGeminiJson } = await import(
"../../src/lib/providers/xai/translators/gemini.ts"
);
const { budgetToEffort, applyThinking, normalizeXaiReasoningEffort } =
await import("../../src/lib/providers/xai/thinking.ts");
const { chatRequestToXaiResponses, xaiCompletedToChatJson } =
await import("../../src/lib/providers/xai/translators/openai-chat.ts");
const { openaiResponsesRequestToXai, xaiCompletedToOpenaiResponses, xaiSseEventToOpenaiResponses } =
await import("../../src/lib/providers/xai/translators/openai-responses.ts");
const { claudeRequestToXaiResponses, xaiCompletedToClaudeJson } =
await import("../../src/lib/providers/xai/translators/claude.ts");
const { geminiRequestToXaiResponses, xaiCompletedToGeminiJson } =
await import("../../src/lib/providers/xai/translators/gemini.ts");

// ─── budgetToEffort ──────────────────────────────────────────────────────────

Expand Down Expand Up @@ -67,13 +60,35 @@ test("applyThinking: honors xAI-native reasoning.effort verbatim", () => {
assert.equal((out as Record<string, unknown>).foo, 1);
});

test("normalizeXaiReasoningEffort: downgrades max/xhigh to xAI-supported high", () => {
assert.equal(normalizeXaiReasoningEffort("max"), "high");
assert.equal(normalizeXaiReasoningEffort("xhigh"), "high");
assert.equal(normalizeXaiReasoningEffort("HIGH"), "high");
assert.equal(normalizeXaiReasoningEffort("ultra"), undefined);
});

test("applyThinking: normalizes xAI-native max/xhigh to high", () => {
const maxOut = applyThinking({ reasoning: { effort: "max", summary: "auto" } });
assert.deepStrictEqual(maxOut.reasoning, { effort: "high", summary: "auto" });

const xhighOut = applyThinking({ reasoning: { effort: "xhigh" } });
assert.deepStrictEqual(xhighOut.reasoning, { effort: "high" });
});

test("applyThinking: rewrites OpenAI Chat reasoning_effort into reasoning.effort", () => {
const req = { reasoning_effort: "medium" };
const out = applyThinking(req);
assert.deepStrictEqual(out.reasoning, { effort: "medium" });
assert.equal(out.reasoning_effort, undefined);
});

test("applyThinking: rewrites OpenAI Chat max reasoning_effort into high", () => {
const req = { reasoning_effort: "max" };
const out = applyThinking(req);
assert.deepStrictEqual(out.reasoning, { effort: "high" });
assert.equal(out.reasoning_effort, undefined);
});

test("applyThinking: ignores invalid reasoning_effort values", () => {
const req = { reasoning_effort: "ultra" };
const out = applyThinking(req);
Expand Down Expand Up @@ -138,9 +153,7 @@ test("chatRequestToXaiResponses: converts system message to instructions", () =>
test("chatRequestToXaiResponses: converts tool message to function_call_output", () => {
const req = {
model: "grok-4",
messages: [
{ role: "tool", content: "result text", tool_call_id: "call_abc" },
],
messages: [{ role: "tool", content: "result text", tool_call_id: "call_abc" }],
};
const out = chatRequestToXaiResponses(req);
assert.equal(out.input[0].type, "function_call_output");
Expand All @@ -154,6 +167,18 @@ test("chatRequestToXaiResponses: promotes reasoning_effort to reasoning field",
assert.deepStrictEqual(out.reasoning, { effort: "high" });
});

test("chatRequestToXaiResponses: normalizes max reasoning_effort for xAI", () => {
const req = { model: "grok-4.3", messages: [], reasoning_effort: "max" };
const out = chatRequestToXaiResponses(req);
assert.deepStrictEqual(out.reasoning, { effort: "high" });
});
Comment on lines +170 to +174

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To ensure the new normalization logic for req.reasoning is fully covered and to prevent future regressions, we should add a unit test case validating that chatRequestToXaiResponses correctly normalizes the effort field inside req.reasoning.

test("chatRequestToXaiResponses: normalizes max reasoning_effort for xAI", () => {
  const req = { model: "grok-4.3", messages: [], reasoning_effort: "max" };
  const out = chatRequestToXaiResponses(req);
  assert.deepStrictEqual(out.reasoning, { effort: "high" });
});

test("chatRequestToXaiResponses: normalizes reasoning.effort for xAI", () => {
  const req = { model: "grok-4.3", messages: [], reasoning: { effort: "max", summary: "auto" } };
  const out = chatRequestToXaiResponses(req);
  assert.deepStrictEqual(out.reasoning, { effort: "high", summary: "auto" });
});
References
  1. Always include tests when changing production code (Rule 9 of the Repository Style Guide). (link)


test("chatRequestToXaiResponses: normalizes reasoning.effort for xAI", () => {
const req = { model: "grok-4.3", messages: [], reasoning: { effort: "max", summary: "auto" } };
const out = chatRequestToXaiResponses(req);
assert.deepStrictEqual(out.reasoning, { effort: "high", summary: "auto" });
});

test("chatRequestToXaiResponses: maps max_tokens to max_output_tokens", () => {
const req = { model: "grok-4", messages: [], max_tokens: 512 };
const out = chatRequestToXaiResponses(req);
Expand Down