[codex] fix xAI OAuth test and reasoning effort (#5157)

Integrated into release/v3.8.39. xAI reasoning-effort normalization (max/xhigh→high) + OAuth test config; 46/46 xai-translator tests pass on merge result. CI reds base-stale.
This commit is contained in:
Nguyen Minh
2026-06-27 22:09:06 +07:00
committed by GitHub
parent f82bd3129e
commit 87ac498d8a
4 changed files with 91 additions and 32 deletions

View File

@@ -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",
@@ -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,

View File

@@ -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)
@@ -69,7 +76,7 @@ interface ApplyThinkingOptions {
*/
export function applyThinking(
request: ThinkingRequest,
options: ApplyThinkingOptions = {},
options: ApplyThinkingOptions = {}
): ThinkingRequest {
if (!request || typeof request !== "object") return request;
const out: ThinkingRequest = { ...request };
@@ -77,14 +84,19 @@ export function applyThinking(
// 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;
}

View File

@@ -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 ────────────────────────────────────────────────────────────────────
@@ -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;
}
@@ -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;
@@ -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";
@@ -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;

View File

@@ -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 ──────────────────────────────────────────────────────────
@@ -67,6 +60,21 @@ 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);
@@ -74,6 +82,13 @@ test("applyThinking: rewrites OpenAI Chat reasoning_effort into reasoning.effort
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);
@@ -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");
@@ -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" });
});
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);