fix(xai): request encrypted reasoning include for all reasoning models (#95686)

Merged via squash.

Prepared head SHA: 8b3be0aaab
Co-authored-by: geraint0923 <923382+geraint0923@users.noreply.github.com>
Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com>
Reviewed-by: @fuller-stack-dev
This commit is contained in:
Mark
2026-06-22 10:50:50 -07:00
committed by GitHub
parent b335381247
commit e8a31ddbce
6 changed files with 115 additions and 5 deletions

View File

@@ -523,7 +523,11 @@ Legacy aliases still normalize to the canonical bundled ids:
`agents.defaults.models["xai/<model>"].params.tool_stream` to `false` to
disable it.
- The bundled xAI wrapper strips unsupported strict tool-schema flags and
reasoning payload keys before sending native xAI requests.
reasoning *effort* payload keys before sending native xAI requests. Only
`grok-4.3` / `grok-4.3-*` advertise configurable reasoning effort; all
other reasoning-capable xAI models still request
`include: ["reasoning.encrypted_content"]` so prior encrypted reasoning
can be replayed on follow-up turns.
- `web_search`, `x_search`, and `code_execution` are exposed as OpenClaw
tools. OpenClaw enables the specific xAI built-in it needs inside each tool
request instead of attaching all native tools to every chat turn.

View File

@@ -188,6 +188,33 @@ describe("xai provider plugin", () => {
"grok-composer-2.5-fast",
"grok-build",
]);
const composer = result.provider.models.find((model) => model.id === "grok-composer-2.5-fast");
if (!composer) {
throw new Error("expected OAuth Composer model");
}
expect(composer.reasoning).toBe(true);
expect(result.provider.models.find((model) => model.id === "grok-build")?.reasoning).toBe(true);
const normalizedComposer = provider.normalizeResolvedModel?.({
provider: "xai",
modelId: composer.id,
model: { ...composer, provider: "xai" },
} as never);
if (!normalizedComposer) {
throw new Error("expected normalized OAuth Composer model");
}
const capture = createXaiPayloadCaptureStream();
const wrapped = provider.wrapStreamFn?.({
provider: "xai",
modelId: normalizedComposer.id,
extraParams: {},
streamFn: capture.streamFn,
} as never);
if (!wrapped) {
throw new Error("expected xAI stream wrapper");
}
void wrapped(normalizedComposer as never, { messages: [] } as never, {});
expect(capture.getCapturedPayload()).not.toHaveProperty("reasoning");
expect(capture.getCapturedPayload()?.include).toEqual(["reasoning.encrypted_content"]);
expect(providerAuthRuntimeMocks.resolveApiKeyForProvider).toHaveBeenCalledWith({
provider: "xai",
cfg: { models: {} },

View File

@@ -23,6 +23,9 @@ const XAI_GROK_OAUTH_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
const XAI_GROK_OAUTH_MODELS_ENDPOINT = `${XAI_GROK_OAUTH_BASE_URL}/models`;
const XAI_MODELS_CACHE_TTL_MS = 60_000;
const XAI_GROK_OAUTH_MODELS_CACHE_TTL_MS = 60_000;
// Composer emits replayable Responses reasoning, but the OAuth catalog omits that capability.
// Keep it classified here or the stream wrapper will omit encrypted reasoning from replay.
const XAI_GROK_OAUTH_REASONING_MODEL_IDS = new Set(["grok-composer-2.5-fast"]);
const XAI_UNKNOWN_MODEL_COST = {
input: 0,
output: 0,
@@ -146,11 +149,13 @@ function buildXaiOauthModelFromLiveRow(row: unknown): ModelDefinitionConfig | un
readLiveModelPositiveInteger(row, ["max_completion_tokens", "maxCompletionTokens"]) ??
fallback?.maxTokens ??
XAI_DEFAULT_MAX_TOKENS;
const reasoning =
const supportsReasoningEffort =
readLiveModelBoolean(row, "supports_reasoning_effort") ??
readLiveModelBoolean(row, "supportsReasoningEffort") ??
fallback?.reasoning ??
false;
readLiveModelBoolean(row, "supportsReasoningEffort");
const reasoning =
supportsReasoningEffort === true ||
fallback?.reasoning === true ||
XAI_GROK_OAUTH_REASONING_MODEL_IDS.has(modelId);
return {
id: modelId,

View File

@@ -1,4 +1,6 @@
// Xai plugin module implements runtime model compat behavior.
// Reasoning effort is configurable only for grok-4.3*; encrypted reasoning include/replay is
// handled separately in stream.ts for all reasoning-capable xAI models.
import { applyXaiModelCompat } from "./model-compat.js";
type XaiRuntimeModelCompat = {

View File

@@ -395,6 +395,55 @@ describe("xai stream wrappers", () => {
expect(payload).not.toHaveProperty("reasoning_effort");
});
it("still requests encrypted reasoning include when effort is unsupported", () => {
const payload: Record<string, unknown> = {
reasoning: { effort: "high" },
input: [],
};
const baseStreamFn: StreamFn = (model, _context, options) => {
options?.onPayload?.(payload, model);
return {} as ReturnType<StreamFn>;
};
const wrapped = createXaiToolPayloadCompatibilityWrapper(baseStreamFn);
void wrapped(
{
api: "openai-responses",
provider: "xai",
id: "grok-build-0.1",
reasoning: true,
compat: { supportsReasoningEffort: false },
} as unknown as Model<"openai-responses">,
{ messages: [] } as Context,
{},
);
expect(payload).not.toHaveProperty("reasoning");
expect(payload.include).toEqual(["reasoning.encrypted_content"]);
});
it("merges encrypted reasoning include with existing include entries", () => {
const payload: Record<string, unknown> = {
include: ["file_search_call.results"],
};
runXaiToolPayloadWrapper({
payload,
modelId: "grok-build-0.1",
});
expect(payload.include).toEqual(["file_search_call.results", "reasoning.encrypted_content"]);
});
it("does not request encrypted reasoning include for non-reasoning xai models", () => {
const payload: Record<string, unknown> = {};
runXaiToolPayloadWrapper({
payload,
modelId: "grok-4-fast-non-reasoning",
});
expect(payload).not.toHaveProperty("include");
});
it("keeps native xAI Responses thinking efforts before the shared runtime dispatches payloads", async () => {
const payload = await captureXaiResponsesPayloadWithThinking();

View File

@@ -53,6 +53,26 @@ function supportsReasoningControls(model: { compat?: unknown; reasoning?: unknow
return model.reasoning === true && compat?.supportsReasoningEffort !== false;
}
const XAI_REASONING_ENCRYPTED_CONTENT_INCLUDE = "reasoning.encrypted_content";
/** xAI-only: request encrypted reasoning for every reasoning-capable model, even when effort is unsupported. */
function ensureXaiResponsesEncryptedReasoningInclude(
payloadObj: Record<string, unknown>,
model: { api?: unknown; provider?: unknown; reasoning?: unknown },
): void {
if (model.provider !== "xai" || model.api !== "openai-responses" || model.reasoning !== true) {
return;
}
const existing = payloadObj.include;
const include = Array.isArray(existing)
? existing.filter((entry): entry is string => typeof entry === "string")
: [];
if (!include.includes(XAI_REASONING_ENCRYPTED_CONTENT_INCLUDE)) {
include.push(XAI_REASONING_ENCRYPTED_CONTENT_INCLUDE);
}
payloadObj.include = include;
}
const TOOL_RESULT_IMAGE_REPLAY_TEXT = "Attached image(s) from tool result:";
type ReplayableInputImagePart =
@@ -181,10 +201,13 @@ export function createXaiToolPayloadCompatibilityWrapper(
}
normalizeXaiResponsesToolResultPayload(payloadObj, model);
if (!supportsReasoningControls(model)) {
// Only grok-4.3* advertises configurable effort; drop effort fields elsewhere.
delete payloadObj.reasoning;
delete payloadObj.reasoningEffort;
delete payloadObj.reasoning_effort;
}
// All reasoning xAI models should still request + later replay encrypted_content.
ensureXaiResponsesEncryptedReasoningInclude(payloadObj, model);
}
return originalOnPayload?.(payload, model);
},