mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-23 07:28:09 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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: {} },
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user