Merge pull request #5109 from any-victor/fix/xai-oauth-replay-shapes

fix(ai): adapted xai responses replay shapes
This commit is contained in:
Can Bölük
2026-07-11 00:15:50 +02:00
committed by GitHub
7 changed files with 424 additions and 34 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed xAI OAuth Responses continuations replaying OpenAI-only `custom_tool_call`/`custom_tool_call_output` history and `input_image.detail: "original"` frames; replay now downgrades those to xAI-compatible function calls and `detail: "auto"`. ([#5002](https://github.com/can1357/oh-my-pi/issues/5002))
## [16.4.1] - 2026-07-10
### Changed
+98 -7
View File
@@ -1357,6 +1357,65 @@ export function convertResponsesInputContent(
return normalizedContent.length > 0 ? normalizedContent : undefined;
}
/**
* Map freeform custom-tool wire names back to the internal tool name for
* providers that only accept function_call / function_call_output.
* Built once per request; `apply_patch` → `edit` is the OMP default.
*/
function buildCustomToolWireNameMap(tools: readonly Tool[] | undefined): ReadonlyMap<string, string> | undefined {
if (!tools?.length) return undefined;
const map = new Map<string, string>();
for (const tool of tools) {
if (tool.customWireName) map.set(tool.customWireName, tool.name);
}
return map.size > 0 ? map : undefined;
}
function resolveReplayCustomToolName(wireName: string, wireNameMap: ReadonlyMap<string, string> | undefined): string {
return wireNameMap?.get(wireName) ?? (wireName === "apply_patch" ? "edit" : wireName);
}
/**
* Downgrade OpenAI-only custom tool items when the target model does not
* advertise freeform custom tools (`applyPatchToolType === "freeform"`).
* No-op (returns the same array reference) when freeform is supported.
*/
function adaptResponsesReplayItemsForModel(
input: ResponseInput,
supportsCustomToolCalls: boolean,
wireNameMap: ReadonlyMap<string, string> | undefined,
): ResponseInput {
if (supportsCustomToolCalls) return input;
let changed = false;
const adapted: ResponseInput = [];
for (const item of input) {
if (item.type === "custom_tool_call") {
changed = true;
adapted.push({
type: "function_call",
...(item.id ? { id: item.id } : {}),
call_id: item.call_id,
name: resolveReplayCustomToolName(item.name, wireNameMap),
arguments: JSON.stringify({ input: item.input }),
...(item.namespace ? { namespace: item.namespace } : {}),
});
continue;
}
if (item.type === "custom_tool_call_output") {
changed = true;
adapted.push({
type: "function_call_output",
call_id: item.call_id,
output: item.output,
});
continue;
}
adapted.push(item);
}
return changed ? adapted : input;
}
export interface BuildResponsesInputOptions<TApi extends Api> {
model: Model<TApi>;
context: Context;
@@ -1381,6 +1440,15 @@ export function buildResponsesInput<TApi extends Api>(options: BuildResponsesInp
messages.push({ role: options.systemRole as "system" | "developer", content: systemPrompt });
}
// Compat is resolved by the catalog (e.g. Copilot / xai-oauth reject
// `detail: "original"`). Do not re-branch on provider id here.
const supportsImageDetailOriginal = options.supportsImageDetailOriginal;
// Freeform custom tools (`custom_tool_call`) only when the catalog says so;
// same gate as tool conversion (`applyPatchToolType === "freeform"`).
const supportsCustomToolCalls = options.model.applyPatchToolType === "freeform";
const customToolWireNameMap = supportsCustomToolCalls
? undefined
: buildCustomToolWireNameMap(options.context.tools);
let knownCallIds = new Set<string>();
const customCallIds = new Set<string>();
const transformedMessages = transformMessages(
@@ -1408,7 +1476,12 @@ export function buildResponsesInput<TApi extends Api>(options: BuildResponsesInp
}) ??
false);
if (historyItems && shouldReplayPayloadItems) {
messages.push(...sanitizeOpenAIResponsesHistoryItemsForReplay(filterReasoning(historyItems)));
const sanitizedItems = sanitizeOpenAIResponsesHistoryItemsForReplay(filterReasoning(historyItems), {
supportsImageDetailOriginal,
});
messages.push(
...adaptResponsesReplayItemsForModel(sanitizedItems, supportsCustomToolCalls, customToolWireNameMap),
);
knownCallIds = collectKnownCallIds(messages);
for (const id of collectCustomCallIds(messages)) customCallIds.add(id);
msgIndex++;
@@ -1417,7 +1490,7 @@ export function buildResponsesInput<TApi extends Api>(options: BuildResponsesInp
const content = convertResponsesInputContent(
msg.content,
options.model.input.includes("image"),
options.supportsImageDetailOriginal,
supportsImageDetailOriginal,
);
if (!content) continue;
messages.push({
@@ -1445,9 +1518,17 @@ export function buildResponsesInput<TApi extends Api>(options: BuildResponsesInp
const historyItems = providerPayload?.items;
let suppressHiddenEmptyFallback = false;
if (historyItems) {
const sanitizedHistoryItems = sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(
const rawSanitizedHistoryItems = sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(
filterReasoning(historyItems),
{ supportsImageDetailOriginal },
);
const sanitizedHistoryItems = rawSanitizedHistoryItems
? adaptResponsesReplayItemsForModel(
rawSanitizedHistoryItems,
supportsCustomToolCalls,
customToolWireNameMap,
)
: undefined;
if (nativeReplayEnabled && sanitizedHistoryItems) {
if (providerPayload?.dt) {
messages.push(...sanitizedHistoryItems);
@@ -1470,6 +1551,8 @@ export function buildResponsesInput<TApi extends Api>(options: BuildResponsesInp
suppressHiddenEmptyFallback ? false : includeThinkingSignatures,
customCallIds,
options.preserveAssistantMessageIds,
supportsCustomToolCalls,
customToolWireNameMap,
);
const outputItems = suppressHiddenEmptyFallback
? sanitizeOpenAIResponsesAssistantFallbackItemsForReplay(convertedOutputItems)
@@ -1482,9 +1565,10 @@ export function buildResponsesInput<TApi extends Api>(options: BuildResponsesInp
msg,
options.model,
options.strictResponsesPairing,
options.supportsImageDetailOriginal,
supportsImageDetailOriginal,
knownCallIds,
customCallIds,
supportsCustomToolCalls,
);
}
msgIndex++;
@@ -1517,6 +1601,8 @@ export function convertResponsesAssistantMessage<TApi extends Api>(
includeThinkingSignatures = true,
customCallIds?: Set<string>,
preserveMessageIds = false,
supportsCustomToolCalls = true,
customToolWireNameMap?: ReadonlyMap<string, string>,
): ResponseInput {
const outputItems: ResponseInput = [];
let unsignedTextBlocks = 0;
@@ -1588,7 +1674,7 @@ export function convertResponsesAssistantMessage<TApi extends Api>(
itemId = undefined;
}
knownCallIds.add(normalized.callId);
if (block.customWireName) {
if (block.customWireName && supportsCustomToolCalls) {
const rawInput = typeof block.arguments?.input === "string" ? block.arguments.input : "";
customCallIds?.add(normalized.callId);
outputItems.push({
@@ -1600,11 +1686,15 @@ export function convertResponsesAssistantMessage<TApi extends Api>(
} as ResponseInput[number]);
continue;
}
const functionName =
block.customWireName && !supportsCustomToolCalls
? resolveReplayCustomToolName(block.customWireName, customToolWireNameMap)
: block.name;
outputItems.push({
type: "function_call",
...(itemId ? { id: itemId } : {}),
call_id: normalized.callId,
name: block.name,
name: functionName,
arguments: stringifyJson(block.arguments) ?? "null",
});
}
@@ -1620,6 +1710,7 @@ export function appendResponsesToolResultMessages<TApi extends Api>(
supportsImageDetailOriginal: boolean,
knownCallIds: ReadonlySet<string>,
customCallIds?: ReadonlySet<string>,
supportsCustomToolCalls = true,
): void {
const supportsImages = model.input.includes("image");
const textResult = toolResult.content
@@ -1649,7 +1740,7 @@ export function appendResponsesToolResultMessages<TApi extends Api>(
} as ResponseInput[number]);
return;
}
if (customCallIds?.has(normalized.callId)) {
if (supportsCustomToolCalls && customCallIds?.has(normalized.callId)) {
messages.push({
type: "custom_tool_call_output",
call_id: normalized.callId,
+49 -4
View File
@@ -65,10 +65,50 @@ export function truncateResponseItemId(id: string, prefix: string): string {
return `${prefix}_${Bun.hash(id).toString(36)}`;
}
export function sanitizeOpenAIResponsesHistoryItemsForReplay(items: Array<Record<string, unknown>>): ResponseInput {
interface OpenAIResponsesReplaySanitizeOptions {
supportsImageDetailOriginal?: boolean;
}
/**
* Clamp `detail: "original"` only where Responses input_image parts live —
* top-level items and `message.content[]`. Avoids a deep tree walk/clone of
* every history node on providers that reject native-resolution images.
*/
function clampReplayItemImageDetail(
item: Record<string, unknown>,
supportsImageDetailOriginal: boolean,
): Record<string, unknown> {
if (supportsImageDetailOriginal) return item;
if (item.type === "input_image" && item.detail === "original") {
return { ...item, detail: "auto" };
}
if (item.type !== "message" || !Array.isArray(item.content)) return item;
let changed = false;
const content = item.content.map(part => {
if (!part || typeof part !== "object" || Array.isArray(part)) return part;
const record = part as Record<string, unknown>;
if (record.type !== "input_image" || record.detail !== "original") return part;
changed = true;
return { ...record, detail: "auto" };
});
return changed ? { ...item, content } : item;
}
export function sanitizeOpenAIResponsesHistoryItemsForReplay(
items: Array<Record<string, unknown>>,
options: OpenAIResponsesReplaySanitizeOptions = {},
): ResponseInput {
const normalizedCallIds = new Map<string, string>();
const supportsImageDetailOriginal = options.supportsImageDetailOriginal !== false;
return items.flatMap(item => {
const sanitized = sanitizeOpenAIResponsesHistoryItemForReplay(item, normalizedCallIds);
const sanitized = sanitizeOpenAIResponsesHistoryItemForReplay(
item,
normalizedCallIds,
supportsImageDetailOriginal,
);
return sanitized ? [sanitized] : [];
});
}
@@ -82,8 +122,9 @@ export function sanitizeOpenAIResponsesHistoryItemsForReplay(items: Array<Record
*/
export function sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(
items: Array<Record<string, unknown>>,
options: OpenAIResponsesReplaySanitizeOptions = {},
): ResponseInput | undefined {
const sanitized = sanitizeOpenAIResponsesHistoryItemsForReplay(items);
const sanitized = sanitizeOpenAIResponsesHistoryItemsForReplay(items, options);
let hasReplayableAssistantOutput = false;
for (const item of sanitized) {
@@ -153,6 +194,7 @@ export function sanitizeOpenAIResponsesAssistantFallbackItemsForReplay(items: Re
function sanitizeOpenAIResponsesHistoryItemForReplay(
item: Record<string, unknown>,
normalizedCallIds: Map<string, string>,
supportsImageDetailOriginal: boolean,
): OpenAIResponsesReplayItem | undefined {
if (item.type === "item_reference") return undefined;
if (item.type === "image_generation_call") return sanitizeOpenAIResponsesImageGenerationCallForReplay(item);
@@ -164,7 +206,10 @@ function sanitizeOpenAIResponsesHistoryItemForReplay(
sanitizedItem.call_id = normalizeReplayedResponsesHistoryCallId(item.call_id, normalizedCallIds);
}
return sanitizedItem as unknown as OpenAIResponsesReplayItem;
return clampReplayItemImageDetail(
sanitizedItem,
supportsImageDetailOriginal,
) as unknown as OpenAIResponsesReplayItem;
}
function sanitizeOpenAIResponsesReasoningItemForReplay(item: Record<string, unknown>): OpenAIResponsesReplayItem {
@@ -5,11 +5,12 @@ import {
} from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
import { type OpenAIResponsesOptions, streamOpenAIResponses } from "@oh-my-pi/pi-ai/providers/openai-responses";
import { buildResponsesInput } from "@oh-my-pi/pi-ai/providers/openai-shared";
import type { Context, Model, ModelSpec, ProviderSessionState } from "@oh-my-pi/pi-ai/types";
import type { Context, Model, ModelSpec, ProviderSessionState, Tool } from "@oh-my-pi/pi-ai/types";
import { createOpenAIResponsesHistoryPayload, truncateResponseItemId } from "@oh-my-pi/pi-ai/utils";
import { buildModel } from "@oh-my-pi/pi-catalog/build";
import { getBundledModel } from "@oh-my-pi/pi-catalog/models";
import { type GeneratedProvider, getBundledModel } from "@oh-my-pi/pi-catalog/models";
import * as piUtils from "@oh-my-pi/pi-utils";
import { type } from "arktype";
const TEST_INSTALLATION_ID = "00000000-0000-4000-8000-000000000001";
@@ -35,13 +36,42 @@ function createCodexToken(accountId: string): string {
return `${header}.${payload}.signature`;
}
function getOpenAIReasoningModel(
provider: Parameters<typeof getBundledModel>[0],
id: string,
): Model<"openai-responses"> {
return getBundledModel(provider, id) as Model<"openai-responses">;
function getOpenAIReasoningModel(provider: GeneratedProvider, id: string): Model<"openai-responses"> {
const model = getBundledModel<"openai-responses">(provider, id);
return model;
}
const ISSUE_5002_PATCH = "*** Begin Patch\n*** End Patch\n";
const ISSUE_5002_TOOL_OUTPUT = "patch applied";
const issue5002XaiOAuthModel = buildModel({
id: "grok-build",
name: "Grok Build",
api: "openai-responses",
provider: "xai-oauth",
baseUrl: "https://api.x.ai/v1",
reasoning: true,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 256000,
maxTokens: 64000,
} satisfies ModelSpec<"openai-responses">);
const issue5002ZeroUsage = {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
};
const issue5002EditTool: Tool = {
name: "edit",
customWireName: "apply_patch",
description: "Apply a hashline patch",
parameters: type({ input: "string" }),
customFormat: { syntax: "lark", definition: 'start: "*** Begin Patch" LF\nLF: /\\n/' },
};
const preservedHistoryItems = [
{ type: "message", role: "user", content: [{ type: "input_text", text: "Preserved user" }] },
{ type: "compaction", encrypted_content: "enc_123" },
@@ -298,6 +328,38 @@ function findResponsesInputItem(input: unknown[] | undefined, type: string): Rec
}) as Record<string, unknown> | undefined;
}
function isIssue5002Record(value: unknown): value is Record<string, unknown> {
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
return true;
}
function findResponsesInputItemByCallId(
input: unknown[],
type: string,
callId: string,
): Record<string, unknown> | undefined {
for (const item of input) {
if (!isIssue5002Record(item)) continue;
if (item.type === type && item.call_id === callId) return item;
}
return undefined;
}
function collectResponsesInputImageDetails(input: unknown): string[] {
const details: string[] = [];
const visit = (node: unknown): void => {
if (Array.isArray(node)) {
for (const child of node) visit(child);
return;
}
if (!isIssue5002Record(node)) return;
if (node.type === "input_image" && typeof node.detail === "string") details.push(node.detail);
for (const key in node) visit(node[key]);
};
visit(input);
return details;
}
function containsUserInputText(input: unknown[] | undefined, text: string): boolean {
return (input ?? []).some(item => {
if (!item || typeof item !== "object") return false;
@@ -366,11 +428,188 @@ describe("OpenAI responses history payload", () => {
});
assertWireOrder(openaiItems);
const codexModel = getBundledModel("openai-codex", "gpt-5.2-codex") as Model<"openai-codex-responses">;
const codexModel = getBundledModel<"openai-codex-responses">("openai-codex", "gpt-5.2-codex");
const codexItems = convertCodexResponsesMessages(codexModel, makeContext("openai-codex"));
assertWireOrder(codexItems);
});
it("adapts reconstructed apply_patch replay for xai-oauth while preserving OpenAI custom replay", () => {
const context: Context = {
messages: [
{
role: "user",
content: [
{ type: "text", text: "previous frame" },
{ type: "image", mimeType: "image/png", data: "ZmFrZQ==", detail: "original" },
],
timestamp: Date.now(),
},
{
role: "assistant",
content: [
{
type: "toolCall",
id: "call_apply",
name: "apply_patch",
arguments: { input: ISSUE_5002_PATCH },
customWireName: "apply_patch",
},
],
api: "openai-responses",
provider: "openai",
model: "gpt-5-mini",
usage: issue5002ZeroUsage,
stopReason: "toolUse",
timestamp: Date.now(),
},
{
role: "toolResult",
toolCallId: "call_apply",
toolName: "edit",
content: [{ type: "text", text: ISSUE_5002_TOOL_OUTPUT }],
isError: false,
timestamp: Date.now(),
},
],
tools: [issue5002EditTool],
};
const xaiInput = buildResponsesInput({
model: issue5002XaiOAuthModel,
context,
strictResponsesPairing: false,
supportsImageDetailOriginal: issue5002XaiOAuthModel.compat.supportsImageDetailOriginal,
nativeHistory: { replay: true, filterReasoning: issue5002XaiOAuthModel.compat.filterReasoningHistory },
});
expect(findResponsesInputItemByCallId(xaiInput, "function_call", "call_apply")).toEqual({
type: "function_call",
call_id: "call_apply",
name: "edit",
arguments: JSON.stringify({ input: ISSUE_5002_PATCH }),
});
expect(findResponsesInputItemByCallId(xaiInput, "function_call_output", "call_apply")).toEqual({
type: "function_call_output",
call_id: "call_apply",
output: ISSUE_5002_TOOL_OUTPUT,
});
expect(JSON.stringify(xaiInput)).not.toContain("custom_tool_call");
expect(collectResponsesInputImageDetails(xaiInput)).toEqual(["auto"]);
const openaiModel = getOpenAIReasoningModel("openai", "gpt-5-mini");
const openaiInput = buildResponsesInput({
model: openaiModel,
context,
strictResponsesPairing: false,
supportsImageDetailOriginal: openaiModel.compat.supportsImageDetailOriginal,
nativeHistory: { replay: true, filterReasoning: openaiModel.compat.filterReasoningHistory },
});
expect(findResponsesInputItemByCallId(openaiInput, "custom_tool_call", "call_apply")).toEqual({
type: "custom_tool_call",
call_id: "call_apply",
name: "apply_patch",
input: ISSUE_5002_PATCH,
});
expect(findResponsesInputItemByCallId(openaiInput, "custom_tool_call_output", "call_apply")).toEqual({
type: "custom_tool_call_output",
call_id: "call_apply",
output: ISSUE_5002_TOOL_OUTPUT,
});
expect(collectResponsesInputImageDetails(openaiInput)).toEqual(["original"]);
});
it("adapts persisted native apply_patch Responses items for xai-oauth continuations", () => {
const nativeHistoryItems = [
{
type: "message",
role: "user",
content: [
{ type: "input_text", text: "previous native frame" },
{ type: "input_image", detail: "original", image_url: "data:image/png;base64,ZmFrZQ==" },
],
},
{ type: "custom_tool_call", call_id: "call_native_apply", name: "apply_patch", input: ISSUE_5002_PATCH },
{
type: "custom_tool_call_output",
call_id: "call_native_apply",
output: ISSUE_5002_TOOL_OUTPUT,
},
];
const xaiContext: Context = {
messages: [
{
role: "assistant",
content: [{ type: "text", text: "fallback should not be replayed" }],
api: "openai-responses",
provider: "xai-oauth",
model: issue5002XaiOAuthModel.id,
usage: issue5002ZeroUsage,
stopReason: "stop",
providerPayload: createOpenAIResponsesHistoryPayload("xai-oauth", nativeHistoryItems),
timestamp: Date.now(),
},
{ role: "user", content: "continue", timestamp: Date.now() },
],
};
const xaiInput = buildResponsesInput({
model: issue5002XaiOAuthModel,
context: xaiContext,
strictResponsesPairing: false,
supportsImageDetailOriginal: issue5002XaiOAuthModel.compat.supportsImageDetailOriginal,
nativeHistory: { replay: true, filterReasoning: issue5002XaiOAuthModel.compat.filterReasoningHistory },
});
expect(findResponsesInputItemByCallId(xaiInput, "function_call", "call_native_apply")).toEqual({
type: "function_call",
call_id: "call_native_apply",
name: "edit",
arguments: JSON.stringify({ input: ISSUE_5002_PATCH }),
});
expect(findResponsesInputItemByCallId(xaiInput, "function_call_output", "call_native_apply")).toEqual({
type: "function_call_output",
call_id: "call_native_apply",
output: ISSUE_5002_TOOL_OUTPUT,
});
expect(JSON.stringify(xaiInput)).not.toContain("custom_tool_call");
expect(collectResponsesInputImageDetails(xaiInput)).toEqual(["auto"]);
const openaiModel = getOpenAIReasoningModel("openai", "gpt-5-mini");
const openaiContext: Context = {
messages: [
{
role: "assistant",
content: [{ type: "text", text: "fallback should not be replayed" }],
api: "openai-responses",
provider: "openai",
model: openaiModel.id,
usage: issue5002ZeroUsage,
stopReason: "stop",
providerPayload: createOpenAIResponsesHistoryPayload("openai", nativeHistoryItems),
timestamp: Date.now(),
},
{ role: "user", content: "continue", timestamp: Date.now() },
],
};
const openaiInput = buildResponsesInput({
model: openaiModel,
context: openaiContext,
strictResponsesPairing: false,
supportsImageDetailOriginal: openaiModel.compat.supportsImageDetailOriginal,
nativeHistory: { replay: true, filterReasoning: openaiModel.compat.filterReasoningHistory },
});
expect(findResponsesInputItemByCallId(openaiInput, "custom_tool_call", "call_native_apply")).toEqual({
type: "custom_tool_call",
call_id: "call_native_apply",
name: "apply_patch",
input: ISSUE_5002_PATCH,
});
expect(findResponsesInputItemByCallId(openaiInput, "custom_tool_call_output", "call_native_apply")).toEqual({
type: "custom_tool_call_output",
call_id: "call_native_apply",
output: ISSUE_5002_TOOL_OUTPUT,
});
expect(collectResponsesInputImageDetails(openaiInput)).toEqual(["original"]);
});
it("prepends multiple OpenAI developer instructions in order without changing prompt cache key routing", async () => {
const model = getOpenAIReasoningModel("openai", "gpt-5-mini");
const payload = (await captureResponsesPayload(
@@ -1063,7 +1302,7 @@ describe("OpenAI responses history payload", () => {
{ role: "user", content: "Resume", timestamp: Date.now() },
],
};
const model = getBundledModel("openai-codex", "gpt-5.2-codex") as Model<"openai-codex-responses">;
const model = getBundledModel<"openai-codex-responses">("openai-codex", "gpt-5.2-codex");
const payload = (await captureCodexPayload(model, context)) as { input?: unknown[] };
const functionCallItem = findResponsesInputItem(payload.input, "function_call");
const functionCallOutputItem = findResponsesInputItem(payload.input, "function_call_output");
+7 -6
View File
@@ -595,12 +595,13 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
// Azure OpenAI and GitHub Copilot Responses paths require tool results
// to strictly match prior tool calls when building Responses inputs.
strictResponsesPairing: isAzure || spec.provider === "github-copilot",
// GitHub Copilot's Responses endpoint rejects the `detail: "original"`
// image hint with a 400; every other host preserves native-resolution
// frames (snapcompact relies on `original`). Detect Copilot by provider id
// or base-URL host (mirroring the Anthropic compat builder) so a model
// pointed at the Copilot host under a different provider id still clamps.
supportsImageDetailOriginal: !modelMatchesHost({ provider: spec.provider, baseUrl }, "githubCopilot"),
// GitHub Copilot and xAI OAuth reject `detail: "original"` (400 / 422).
// Every other host preserves native-resolution frames (snapcompact relies
// on `original`). Detect Copilot by provider id or base-URL host so a
// model pointed at the Copilot host under a different provider id still
// clamps; xai-oauth is provider-id only (same host family as paid `xai`).
supportsImageDetailOriginal:
spec.provider !== "xai-oauth" && !modelMatchesHost({ provider: spec.provider, baseUrl }, "githubCopilot"),
reasoningEffortMap: {},
supportsReasoningParams: true,
thinkingFormat,
+16 -8
View File
@@ -87250,7 +87250,8 @@
"includeEncryptedReasoning": false,
"filterReasoningHistory": true,
"omitReasoningEffort": true,
"supportsReasoningEffort": false
"supportsReasoningEffort": false,
"supportsImageDetailOriginal": false
}
},
"grok-4.20-0309-reasoning": {
@@ -87279,7 +87280,8 @@
"includeEncryptedReasoning": false,
"filterReasoningHistory": true,
"omitReasoningEffort": true,
"supportsReasoningEffort": false
"supportsReasoningEffort": false,
"supportsImageDetailOriginal": false
}
},
"grok-4.20-multi-agent-0309": {
@@ -87320,7 +87322,8 @@
"includeEncryptedReasoning": false,
"filterReasoningHistory": true,
"omitReasoningEffort": false,
"supportsReasoningEffort": true
"supportsReasoningEffort": true,
"supportsImageDetailOriginal": false
}
},
"grok-4.3": {
@@ -87362,7 +87365,8 @@
"includeEncryptedReasoning": false,
"filterReasoningHistory": true,
"omitReasoningEffort": false,
"supportsReasoningEffort": true
"supportsReasoningEffort": true,
"supportsImageDetailOriginal": false
}
},
"grok-4.5": {
@@ -87404,7 +87408,8 @@
"includeEncryptedReasoning": false,
"filterReasoningHistory": true,
"omitReasoningEffort": false,
"supportsReasoningEffort": true
"supportsReasoningEffort": true,
"supportsImageDetailOriginal": false
}
},
"grok-build": {
@@ -87433,7 +87438,8 @@
"includeEncryptedReasoning": false,
"filterReasoningHistory": true,
"omitReasoningEffort": true,
"supportsReasoningEffort": false
"supportsReasoningEffort": false,
"supportsImageDetailOriginal": false
}
},
"grok-build-0.1": {
@@ -87462,7 +87468,8 @@
"includeEncryptedReasoning": false,
"filterReasoningHistory": true,
"omitReasoningEffort": true,
"supportsReasoningEffort": false
"supportsReasoningEffort": false,
"supportsImageDetailOriginal": false
}
},
"grok-composer-2.5-fast": {
@@ -87490,7 +87497,8 @@
"includeEncryptedReasoning": false,
"filterReasoningHistory": true,
"omitReasoningEffort": true,
"supportsReasoningEffort": false
"supportsReasoningEffort": false,
"supportsImageDetailOriginal": false
}
}
},
@@ -1176,6 +1176,7 @@ function withXaiOAuthCompatDefaults(model: ModelSpec<"openai-responses">): Model
...(model.compat ?? {}),
includeEncryptedReasoning: model.compat?.includeEncryptedReasoning ?? false,
filterReasoningHistory: model.compat?.filterReasoningHistory ?? true,
supportsImageDetailOriginal: model.compat?.supportsImageDetailOriginal ?? false,
omitReasoningEffort: model.compat?.omitReasoningEffort ?? !isGrokReasoningEffortCapable(model.id),
};
return { ...model, compat };
@@ -1218,6 +1219,7 @@ function mergeCuratedIntoModel(
reasoningEffortMap: { ...XAI_REASONING_EFFORT_MAP, ...(base.compat?.reasoningEffortMap ?? {}) },
includeEncryptedReasoning: base.compat?.includeEncryptedReasoning ?? false,
filterReasoningHistory: base.compat?.filterReasoningHistory ?? true,
supportsImageDetailOriginal: base.compat?.supportsImageDetailOriginal ?? false,
omitReasoningEffort: !effortCapable,
supportsReasoningEffort: effortCapable,
};