Merge PR #7454: feat(catalog): route paid xAI through Responses like SuperGrok (@geraint0923)
This commit is contained in:
@@ -235,13 +235,16 @@ Reasoning fields are not interchangeable.
|
||||
- Compat needs both policies: disable reasoning for any tool choice, and disable
|
||||
reasoning only for forced tool choice.
|
||||
|
||||
### xAI Grok through Responses/SuperGrok
|
||||
### xAI Grok through Responses (`xai` and `xai-oauth`)
|
||||
|
||||
Keep these independent:
|
||||
Both the paid API-key provider (`xai` / `XAI_API_KEY`) and SuperGrok OAuth
|
||||
(`xai-oauth`) chat over `https://api.x.ai/v1/responses`. Keep these independent:
|
||||
|
||||
- omit `reasoning.effort`
|
||||
- include or drop encrypted reasoning replay
|
||||
- filter reasoning-history wrappers
|
||||
- omit `reasoning.effort` unless the model is on the Grok effort-capable allowlist
|
||||
- omit `reasoning.summary` (the host rejects it; do not fall back to `"auto"`)
|
||||
- omit presence/frequency penalties (`/v1/responses` rejects them for every Grok model)
|
||||
- include `reasoning.encrypted_content` on the request
|
||||
- replay encrypted reasoning items on later turns
|
||||
|
||||
Some models reject only one of those fields; do not collapse them into one
|
||||
"Grok mode" branch.
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Stopped treating `XAI_API_KEY` as SuperGrok (`xai-oauth`) sign-in for availability, so paid-key-only setups default to `xai/grok-4.5` instead of the zero-cost SuperGrok catalog path. Explicit `xai-oauth/…` selectors still accept the paid key via the existing env fallback.
|
||||
- Omitted unsupported `reasoning.summary` on paid xAI Responses requests (`xai/grok-4.5`), matching SuperGrok, so a thinking level no longer serializes `summary: "auto"`.
|
||||
- Omitted presence/frequency penalties on all first-party xAI Responses models, including non-reasoning ids such as `xai/grok-2`.
|
||||
|
||||
## [17.3.4] - 2026-08-14
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -2680,18 +2680,32 @@ export class AuthStorage {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any form of auth is configured for a provider.
|
||||
* Unlike getApiKey(), this doesn't refresh OAuth tokens.
|
||||
* Dedicated auth for default-model availability (picker / `getAvailable`).
|
||||
* Unlike {@link getApiKey}, this does not refresh OAuth tokens, and unlike
|
||||
* {@link hasResolvableAuth} it ignores cross-provider env aliases so
|
||||
* `XAI_API_KEY` does not auto-select SuperGrok (`xai-oauth`).
|
||||
*/
|
||||
hasAuth(provider: string): boolean {
|
||||
if (this.#runtimeOverrides.has(provider)) return true;
|
||||
if (this.#configOverrides.has(provider)) return true;
|
||||
if (this.#getCredentialsForProvider(provider).length > 0) return true;
|
||||
if (getEnvApiKey(provider)) return true;
|
||||
if (this.#hasDedicatedEnvAuth(provider)) return true;
|
||||
if (this.#fallbackResolver?.(provider)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a request could resolve a key for this provider, including
|
||||
* cross-provider env aliases (`xai-oauth` borrowing `XAI_API_KEY`).
|
||||
* Use this for explicit model preflight (`xai-oauth/grok-4.5`); use
|
||||
* {@link hasAuth} for auto-availability so the default picker stays on
|
||||
* paid `xai` when only `XAI_API_KEY` is set.
|
||||
*/
|
||||
hasResolvableAuth(provider: string): boolean {
|
||||
if (this.hasAuth(provider)) return true;
|
||||
return Boolean(getEnvApiKey(provider));
|
||||
}
|
||||
|
||||
/**
|
||||
* True iff a dedicated, non-env credential source is configured for this
|
||||
* provider — i.e. anything in the cascade EXCEPT `getEnvApiKey(provider)`.
|
||||
@@ -2711,6 +2725,22 @@ export class AuthStorage {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Env auth that belongs to this provider, not a cross-provider alias.
|
||||
*
|
||||
* `getEnvApiKey("xai-oauth")` also accepts `XAI_API_KEY` so an explicit
|
||||
* `xai-oauth/…` stream can still borrow the paid key. Availability and
|
||||
* origin must not: otherwise an API-key-only setup marks SuperGrok as
|
||||
* signed in and `pickDefaultAvailableModel` prefers `xai-oauth/grok-4.5`
|
||||
* over paid `xai/grok-4.5`.
|
||||
*/
|
||||
#hasDedicatedEnvAuth(provider: string): boolean {
|
||||
if (provider === "xai-oauth") {
|
||||
return Boolean($env.XAI_OAUTH_TOKEN?.trim());
|
||||
}
|
||||
return Boolean(getEnvApiKey(provider));
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify where a provider's auth comes from, following the same precedence
|
||||
* as {@link AuthStorage.getApiKey}: runtime override → config override →
|
||||
@@ -2727,7 +2757,7 @@ export class AuthStorage {
|
||||
if (stored.some(credential => credential.type === "api_key" && credential.source === "login")) {
|
||||
return { kind: "api_key" };
|
||||
}
|
||||
if (getEnvApiKey(provider)) return { kind: "env", envVar: getEnvApiKeyName(provider) };
|
||||
if (this.#hasDedicatedEnvAuth(provider)) return { kind: "env", envVar: getEnvApiKeyName(provider) };
|
||||
if (stored.some(credential => credential.type === "api_key")) return { kind: "api_key" };
|
||||
if (this.#fallbackResolver?.(provider)) return { kind: "fallback" };
|
||||
return undefined;
|
||||
|
||||
@@ -1575,17 +1575,19 @@ function buildParams(
|
||||
if (options?.minP !== undefined) {
|
||||
params.min_p = options.minP;
|
||||
}
|
||||
if (options?.presencePenalty !== undefined) {
|
||||
params.presence_penalty = options.presencePenalty;
|
||||
}
|
||||
if (options?.repetitionPenalty !== undefined) {
|
||||
params.repetition_penalty = options.repetitionPenalty;
|
||||
}
|
||||
if (options?.frequencyPenalty !== undefined) {
|
||||
params.frequency_penalty = options.frequencyPenalty;
|
||||
if (initialCompat.supportsPenaltyAndStopParams) {
|
||||
if (options?.presencePenalty !== undefined) {
|
||||
params.presence_penalty = options.presencePenalty;
|
||||
}
|
||||
if (options?.repetitionPenalty !== undefined) {
|
||||
params.repetition_penalty = options.repetitionPenalty;
|
||||
}
|
||||
if (options?.frequencyPenalty !== undefined) {
|
||||
params.frequency_penalty = options.frequencyPenalty;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (options?.stopSequences?.length) {
|
||||
if (options?.stopSequences?.length && initialCompat.supportsPenaltyAndStopParams) {
|
||||
const seqs = options.stopSequences;
|
||||
params.stop = seqs.length === 1 ? seqs[0] : seqs.slice(0, 4);
|
||||
}
|
||||
|
||||
@@ -1285,12 +1285,11 @@ export function buildParams(
|
||||
filterReasoningHistory: options?.filterReasoningHistory,
|
||||
omitReasoningEffort: options?.omitReasoningEffort,
|
||||
});
|
||||
const reasoningSummary =
|
||||
model.provider === "xai-oauth"
|
||||
? options?.reasoning === undefined
|
||||
? undefined
|
||||
: null
|
||||
: options?.reasoningSummary;
|
||||
const reasoningSummary = model.compat.supportsReasoningSummary
|
||||
? options?.reasoningSummary
|
||||
: options?.reasoning === undefined
|
||||
? undefined
|
||||
: null;
|
||||
applyResponsesCompatPolicy(params, reasoningPolicy, {
|
||||
reasoningSummary,
|
||||
forceReasoningOff: options?.forceReasoningOff,
|
||||
|
||||
@@ -3308,7 +3308,7 @@ export function applyCommonResponsesSamplingParams<P extends CommonResponsesPara
|
||||
params: P,
|
||||
options: CommonSamplingOptions | undefined,
|
||||
model: Pick<Model, "provider" | "api" | "id" | "omitMaxOutputTokens" | "maxTokens"> & {
|
||||
compat: Pick<ResolvedOpenAISharedCompat, "supportsSamplingParams">;
|
||||
compat: Pick<ResolvedOpenAISharedCompat, "supportsSamplingParams" | "supportsPenaltyAndStopParams">;
|
||||
},
|
||||
): void {
|
||||
if (options?.maxTokens && !model.omitMaxOutputTokens) {
|
||||
@@ -3325,8 +3325,10 @@ export function applyCommonResponsesSamplingParams<P extends CommonResponsesPara
|
||||
if (options?.topP !== undefined) params.top_p = options.topP;
|
||||
if (options?.topK !== undefined) params.top_k = options.topK;
|
||||
if (options?.minP !== undefined) params.min_p = options.minP;
|
||||
if (options?.presencePenalty !== undefined) params.presence_penalty = options.presencePenalty;
|
||||
if (options?.repetitionPenalty !== undefined) params.repetition_penalty = options.repetitionPenalty;
|
||||
if (model.compat.supportsPenaltyAndStopParams) {
|
||||
if (options?.presencePenalty !== undefined) params.presence_penalty = options.presencePenalty;
|
||||
if (options?.repetitionPenalty !== undefined) params.repetition_penalty = options.repetitionPenalty;
|
||||
}
|
||||
}
|
||||
applyOpenAIServiceTier(params, options?.serviceTier, model);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ const compat: ResolvedOpenAICompat = {
|
||||
supportsReasoningEffort: true,
|
||||
supportsReasoningParams: true,
|
||||
supportsSamplingParams: true,
|
||||
supportsPenaltyAndStopParams: true,
|
||||
alwaysSendMaxTokens: false,
|
||||
isOpenRouterHost: false,
|
||||
isVercelGatewayHost: false,
|
||||
|
||||
@@ -5,9 +5,6 @@ import type { AssistantMessage, Context, FetchImpl, Model, SimpleStreamOptions,
|
||||
import { buildOpenAICompat } from "@oh-my-pi/pi-catalog/compat/openai";
|
||||
import { getBundledModel } from "@oh-my-pi/pi-catalog/models";
|
||||
|
||||
const model = getBundledModel<"openai-completions">("xai", "grok-code-fast-1");
|
||||
if (!model) throw new Error("Expected bundled xAI Grok model");
|
||||
if (model.api !== "openai-completions") throw new Error(`Expected Chat Completions model, received ${model.api}`);
|
||||
const context: Context = { messages: [{ role: "user", content: "hello", timestamp: 0 }] };
|
||||
|
||||
const openAI56ResponsesModel = getBundledModel<"openai-responses">("openai", "gpt-5.6");
|
||||
@@ -44,7 +41,7 @@ function chatCompletionsSse(): Response {
|
||||
id: "chatcmpl-affinity",
|
||||
object: "chat.completion.chunk",
|
||||
created: 0,
|
||||
model: model.id,
|
||||
model: openAI56CompletionsModel.id,
|
||||
choices: [{ index: 0, delta, finish_reason: finishReason }],
|
||||
});
|
||||
|
||||
@@ -56,7 +53,7 @@ function chatCompletionsSse(): Response {
|
||||
|
||||
async function captureRequest(
|
||||
options: OpenAICompletionsOptions,
|
||||
requestModel: Model<"openai-completions"> = model,
|
||||
requestModel: Model<"openai-completions"> = openAI56CompletionsModel,
|
||||
requestContext: Context = context,
|
||||
): Promise<{ headers: Headers; body: Record<string, unknown> }> {
|
||||
let requestHeaders: Headers | undefined;
|
||||
@@ -83,7 +80,7 @@ async function captureRequest(
|
||||
|
||||
async function captureSimpleRequest(
|
||||
options: SimpleStreamOptions,
|
||||
requestModel: Model<"openai-completions"> = model,
|
||||
requestModel: Model<"openai-completions"> = openAI56CompletionsModel,
|
||||
requestContext: Context = context,
|
||||
): Promise<{ headers: Headers; body: Record<string, unknown> }> {
|
||||
let requestHeaders: Headers | undefined;
|
||||
@@ -104,51 +101,6 @@ async function captureSimpleRequest(
|
||||
return { headers: requestHeaders, body };
|
||||
}
|
||||
|
||||
describe("openai-completions xAI cache affinity", () => {
|
||||
const cases: Array<{
|
||||
name: string;
|
||||
options: OpenAICompletionsOptions;
|
||||
expectedHeader: string | null;
|
||||
}> = [
|
||||
{
|
||||
name: "uses sessionId when no prompt cache key is provided",
|
||||
options: { sessionId: "session-fallback" },
|
||||
expectedHeader: "session-fallback",
|
||||
},
|
||||
{
|
||||
name: "keeps the prompt cache key stable across a distinct side-channel session",
|
||||
options: { promptCacheKey: "stable-cache-key", sessionId: "side-channel-session" },
|
||||
expectedHeader: "stable-cache-key",
|
||||
},
|
||||
{
|
||||
name: "omits automatic affinity when caching is disabled",
|
||||
options: {
|
||||
promptCacheKey: "disabled-cache-key",
|
||||
sessionId: "disabled-session",
|
||||
cacheRetention: "none",
|
||||
},
|
||||
expectedHeader: null,
|
||||
},
|
||||
{
|
||||
name: "preserves a caller-provided mixed-case affinity header",
|
||||
options: {
|
||||
promptCacheKey: "automatic-cache-key",
|
||||
sessionId: "automatic-session",
|
||||
headers: { "X-Grok-Conv-Id": "caller-affinity" },
|
||||
},
|
||||
expectedHeader: "caller-affinity",
|
||||
},
|
||||
];
|
||||
|
||||
for (const { name, options, expectedHeader } of cases) {
|
||||
it(name, async () => {
|
||||
const { headers } = await captureRequest(options);
|
||||
|
||||
expect(headers.get("x-grok-conv-id")).toBe(expectedHeader);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("OpenAI Chat Completions explicit prompt cache policy", () => {
|
||||
const historicalContext: Context = {
|
||||
messages: [
|
||||
|
||||
@@ -210,6 +210,7 @@ describe("openai-completions compatibility", () => {
|
||||
toolStrictMode: "none",
|
||||
supportsReasoningParams: true,
|
||||
supportsSamplingParams: true,
|
||||
supportsPenaltyAndStopParams: true,
|
||||
alwaysSendMaxTokens: false,
|
||||
isOpenRouterHost: false,
|
||||
isVercelGatewayHost: false,
|
||||
|
||||
@@ -51,6 +51,7 @@ const compat: ResolvedOpenAICompat = {
|
||||
toolStrictMode: "none",
|
||||
supportsReasoningParams: true,
|
||||
supportsSamplingParams: true,
|
||||
supportsPenaltyAndStopParams: true,
|
||||
alwaysSendMaxTokens: false,
|
||||
isOpenRouterHost: false,
|
||||
isVercelGatewayHost: false,
|
||||
|
||||
@@ -57,6 +57,20 @@ const xaiOAuthResponsesModel: Model<"openai-responses"> = {
|
||||
reasoning: true,
|
||||
}),
|
||||
};
|
||||
const xaiApiKeyResponsesModel: Model<"openai-responses"> = {
|
||||
...model,
|
||||
id: "grok-code-fast-1",
|
||||
name: "Grok Code Fast 1",
|
||||
provider: "xai",
|
||||
baseUrl: "https://api.x.ai/v1",
|
||||
compat: buildOpenAIResponsesCompat({
|
||||
id: "grok-code-fast-1",
|
||||
name: "Grok Code Fast 1",
|
||||
provider: "xai",
|
||||
baseUrl: "https://api.x.ai/v1",
|
||||
reasoning: true,
|
||||
}),
|
||||
};
|
||||
|
||||
const openAI56ResponsesModel: Model<"openai-responses"> = {
|
||||
...model,
|
||||
@@ -675,6 +689,16 @@ describe("openai-responses cache affinity", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("sets x-grok-conv-id cache affinity for paid xai Responses requests", async () => {
|
||||
const captured = await captureDispatchedOpenAIResponseHeaders(
|
||||
{ sessionId: "session-fallback" },
|
||||
xaiApiKeyResponsesModel,
|
||||
);
|
||||
|
||||
expect(getHeader(captured.headers, "x-grok-conv-id")).toBe("session-fallback");
|
||||
expect(captured.body?.prompt_cache_key).toBe("session-fallback");
|
||||
});
|
||||
|
||||
it("sets OpenRouter Responses session_id from sessionId in the body", async () => {
|
||||
const captured = await captureOpenAIResponseHeaders(
|
||||
{ sessionId: "workflow-123", promptCacheKey: "cache-key-123" },
|
||||
|
||||
@@ -1000,7 +1000,7 @@ describe("Generate E2E Tests", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe.skipIf(!e2eApiKey("XAI_API_KEY"))("xAI Provider (grok-code-fast-1 via OpenAI Completions)", () => {
|
||||
describe.skipIf(!e2eApiKey("XAI_API_KEY"))("xAI Provider (grok-code-fast-1 via OpenAI Responses)", () => {
|
||||
const llm = getBundledModel("xai", "grok-code-fast-1");
|
||||
|
||||
it(
|
||||
|
||||
@@ -29,6 +29,52 @@ describe("xAI API login wiring", () => {
|
||||
expect(getEnvApiKey("xai")).toBe("xai-env-key");
|
||||
});
|
||||
|
||||
test("XAI_API_KEY alone does not mark SuperGrok as available", async () => {
|
||||
const originalOauthToken = Bun.env.XAI_OAUTH_TOKEN;
|
||||
Bun.env.XAI_API_KEY = "xai-env-key";
|
||||
delete Bun.env.XAI_OAUTH_TOKEN;
|
||||
const store = new SqliteAuthCredentialStore(new Database(":memory:"));
|
||||
const storage = new AuthStorage(store);
|
||||
await storage.reload();
|
||||
try {
|
||||
expect(storage.hasAuth("xai")).toBe(true);
|
||||
expect(storage.hasAuth("xai-oauth")).toBe(false);
|
||||
expect(storage.hasResolvableAuth("xai")).toBe(true);
|
||||
expect(storage.hasResolvableAuth("xai-oauth")).toBe(true);
|
||||
expect(getEnvApiKey("xai-oauth")).toBe("xai-env-key");
|
||||
expect(storage.getCredentialOrigin("xai")).toEqual({ kind: "env", envVar: "XAI_API_KEY" });
|
||||
expect(storage.getCredentialOrigin("xai-oauth")).toBeUndefined();
|
||||
} finally {
|
||||
if (originalOauthToken === undefined) {
|
||||
delete Bun.env.XAI_OAUTH_TOKEN;
|
||||
} else {
|
||||
Bun.env.XAI_OAUTH_TOKEN = originalOauthToken;
|
||||
}
|
||||
store.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("XAI_OAUTH_TOKEN marks SuperGrok available without a paid API key", async () => {
|
||||
const originalOauthToken = Bun.env.XAI_OAUTH_TOKEN;
|
||||
delete Bun.env.XAI_API_KEY;
|
||||
Bun.env.XAI_OAUTH_TOKEN = "xai-oauth-env";
|
||||
const store = new SqliteAuthCredentialStore(new Database(":memory:"));
|
||||
const storage = new AuthStorage(store);
|
||||
await storage.reload();
|
||||
try {
|
||||
expect(storage.hasAuth("xai")).toBe(false);
|
||||
expect(storage.hasAuth("xai-oauth")).toBe(true);
|
||||
expect(storage.getCredentialOrigin("xai-oauth")).toEqual({ kind: "env" });
|
||||
} finally {
|
||||
if (originalOauthToken === undefined) {
|
||||
delete Bun.env.XAI_OAUTH_TOKEN;
|
||||
} else {
|
||||
Bun.env.XAI_OAUTH_TOKEN = originalOauthToken;
|
||||
}
|
||||
store.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("AuthStorage.login('xai') validates against /models and stores the pasted key", async () => {
|
||||
const fetchCalls: Array<{ url: string; init: RequestInit | undefined }> = [];
|
||||
const fetchMock: FetchImpl = vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { buildParams } from "@oh-my-pi/pi-ai/providers/openai-responses";
|
||||
import type { Context } from "@oh-my-pi/pi-ai/types";
|
||||
import type { AssistantMessage, Context, Model } from "@oh-my-pi/pi-ai/types";
|
||||
import { Effort } from "@oh-my-pi/pi-catalog/effort";
|
||||
import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
|
||||
import { getBundledModel } from "@oh-my-pi/pi-catalog/models";
|
||||
@@ -38,6 +38,23 @@ describe("effort-dial-less reasoner encoding (regression)", () => {
|
||||
expect(grokR.thinking).toBeUndefined();
|
||||
});
|
||||
|
||||
test("paid xai/grok-code-fast-1 reasons but carries no thinking config", () => {
|
||||
const grokCodeFast = getBundledModel("xai", "grok-code-fast-1");
|
||||
if (!grokCodeFast) throw new Error("xai/grok-code-fast-1 must be in bundled models.json");
|
||||
expect(grokCodeFast.api).toBe("openai-responses");
|
||||
expect(grokCodeFast.reasoning).toBe(true);
|
||||
expect(grokCodeFast.thinking).toBeUndefined();
|
||||
expect(getSupportedEfforts(grokCodeFast)).toEqual([]);
|
||||
});
|
||||
|
||||
test("paid xai/grok-4.3 keeps its effort dial", () => {
|
||||
const grok43 = getBundledModel("xai", "grok-4.3");
|
||||
if (!grok43) throw new Error("xai/grok-4.3 must be in bundled models.json");
|
||||
expect(grok43.api).toBe("openai-responses");
|
||||
expect(grok43.thinking).toBeDefined();
|
||||
expect(getSupportedEfforts(grok43).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("the no-dial encoding stays scoped to openai-responses*", () => {
|
||||
const claude = getBundledModel("anthropic", "claude-sonnet-4-6");
|
||||
if (!claude) throw new Error("anthropic/claude-sonnet-4-6 must be in bundled models.json");
|
||||
@@ -57,6 +74,7 @@ describe("xAI OAuth Responses reasoning payload (regression)", () => {
|
||||
const { params } = buildParams(grok45, singleUserContext, undefined, undefined);
|
||||
|
||||
expect(params.reasoning).toBeUndefined();
|
||||
expect(params.include).toContain("reasoning.encrypted_content");
|
||||
});
|
||||
|
||||
test("xai-oauth/grok-4.5 omits unsupported reasoning summary", () => {
|
||||
@@ -66,5 +84,142 @@ describe("xAI OAuth Responses reasoning payload (regression)", () => {
|
||||
const { params } = buildParams(grok45, singleUserContext, { reasoning: Effort.High }, undefined);
|
||||
|
||||
expect(params.reasoning).toEqual({ effort: "high" });
|
||||
expect(params.include).toContain("reasoning.encrypted_content");
|
||||
});
|
||||
|
||||
test("paid xai/grok-4.5 omits unsupported reasoning summary", () => {
|
||||
const grok45 = getBundledModel<"openai-responses">("xai", "grok-4.5");
|
||||
if (!grok45) throw new Error("xai/grok-4.5 must be in bundled models.json");
|
||||
|
||||
const { params } = buildParams(grok45, singleUserContext, { reasoning: Effort.High }, undefined);
|
||||
|
||||
expect(params.reasoning).toEqual({ effort: "high" });
|
||||
expect(params.include).toContain("reasoning.encrypted_content");
|
||||
});
|
||||
|
||||
test("paid xai/grok-4.5 requests encrypted reasoning content", () => {
|
||||
const grok45 = getBundledModel<"openai-responses">("xai", "grok-4.5");
|
||||
if (!grok45) throw new Error("xai/grok-4.5 must be in bundled models.json");
|
||||
|
||||
const { params } = buildParams(grok45, singleUserContext, { reasoning: Effort.High }, undefined);
|
||||
|
||||
expect(params.include).toContain("reasoning.encrypted_content");
|
||||
});
|
||||
|
||||
test("paid xai/grok-4.5 omits presence_penalty on reasoning models", () => {
|
||||
const grok45 = getBundledModel<"openai-responses">("xai", "grok-4.5");
|
||||
if (!grok45) throw new Error("xai/grok-4.5 must be in bundled models.json");
|
||||
|
||||
const { params } = buildParams(
|
||||
grok45,
|
||||
singleUserContext,
|
||||
{ reasoning: Effort.High, presencePenalty: 0.4, temperature: 0.2 },
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(params).not.toHaveProperty("presence_penalty");
|
||||
expect(params.temperature).toBe(0.2);
|
||||
});
|
||||
|
||||
test("paid xai/grok-2 omits presence_penalty on non-reasoning Responses models", () => {
|
||||
const grok2 = getBundledModel<"openai-responses">("xai", "grok-2");
|
||||
if (!grok2) throw new Error("xai/grok-2 must be in bundled models.json");
|
||||
|
||||
const { params } = buildParams(grok2, singleUserContext, { presencePenalty: 0.4, temperature: 0.2 }, undefined);
|
||||
|
||||
expect(params).not.toHaveProperty("presence_penalty");
|
||||
expect(params.temperature).toBe(0.2);
|
||||
});
|
||||
|
||||
test("paid xai/grok-4.5 clamps minimal reasoning effort to low", () => {
|
||||
const grok45 = getBundledModel<"openai-responses">("xai", "grok-4.5");
|
||||
if (!grok45) throw new Error("xai/grok-4.5 must be in bundled models.json");
|
||||
|
||||
const { params } = buildParams(grok45, singleUserContext, { reasoning: Effort.Minimal }, undefined);
|
||||
|
||||
expect(params.reasoning).toEqual({ effort: "low" });
|
||||
});
|
||||
|
||||
test("xai-oauth/grok-4.5 clamps minimal reasoning effort to low", () => {
|
||||
const grok45 = getBundledModel<"openai-responses">("xai-oauth", "grok-4.5");
|
||||
if (!grok45) throw new Error("xai-oauth/grok-4.5 must be in bundled models.json");
|
||||
|
||||
const { params } = buildParams(grok45, singleUserContext, { reasoning: Effort.Minimal }, undefined);
|
||||
|
||||
expect(params.reasoning).toEqual({ effort: "low" });
|
||||
});
|
||||
|
||||
test("xai-oauth/grok-4.5 replays encrypted reasoning on the next turn", () => {
|
||||
const grok45 = getBundledModel<"openai-responses">("xai-oauth", "grok-4.5");
|
||||
if (!grok45) throw new Error("xai-oauth/grok-4.5 must be in bundled models.json");
|
||||
|
||||
const { params } = buildParams(grok45, followUpContextWithEncryptedReasoning(grok45), undefined, undefined);
|
||||
|
||||
expect(params.include).toContain("reasoning.encrypted_content");
|
||||
expect(findEncryptedReasoning(params.input)).toEqual({
|
||||
type: "reasoning",
|
||||
id: "rs_xai_next_turn",
|
||||
encrypted_content: "enc_next_turn",
|
||||
});
|
||||
});
|
||||
|
||||
test("paid xai/grok-4.5 replays encrypted reasoning on the next turn", () => {
|
||||
const grok45 = getBundledModel<"openai-responses">("xai", "grok-4.5");
|
||||
if (!grok45) throw new Error("xai/grok-4.5 must be in bundled models.json");
|
||||
|
||||
const { params } = buildParams(grok45, followUpContextWithEncryptedReasoning(grok45), undefined, undefined);
|
||||
|
||||
expect(findEncryptedReasoning(params.input)).toEqual({
|
||||
type: "reasoning",
|
||||
id: "rs_xai_next_turn",
|
||||
encrypted_content: "enc_next_turn",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function followUpContextWithEncryptedReasoning(model: Model<"openai-responses">): Context {
|
||||
const assistant: AssistantMessage = {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "thinking",
|
||||
thinking: "internal plan",
|
||||
thinkingSignature: JSON.stringify({
|
||||
type: "reasoning",
|
||||
id: "rs_xai_next_turn",
|
||||
encrypted_content: "enc_next_turn",
|
||||
}),
|
||||
},
|
||||
{ type: "text", text: "done" },
|
||||
],
|
||||
api: "openai-responses",
|
||||
provider: model.provider,
|
||||
model: model.id,
|
||||
usage: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 0,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
},
|
||||
stopReason: "stop",
|
||||
timestamp: 1,
|
||||
};
|
||||
return {
|
||||
messages: [
|
||||
{ role: "user", content: "first", timestamp: 0 },
|
||||
assistant,
|
||||
{ role: "user", content: "continue", timestamp: 2 },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function findEncryptedReasoning(input: unknown): Record<string, unknown> | undefined {
|
||||
if (!Array.isArray(input)) return undefined;
|
||||
return input.find(item => {
|
||||
if (!item || typeof item !== "object") return false;
|
||||
const candidate = item as { type?: unknown; encrypted_content?: unknown };
|
||||
return candidate.type === "reasoning" && typeof candidate.encrypted_content === "string";
|
||||
}) as Record<string, unknown> | undefined;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,26 @@
|
||||
### Added
|
||||
|
||||
- Added support for GLM-5.3 on the z.AI provider. GLM-5.3 introduces a uniform wire-exact `low`/`high`/`max` reasoning-effort ladder on every host (replacing GLM-5.2's host-specific dialects), makes thinking mandatory (`thinking.type` must always be `enabled`; disabling is no longer supported), and defaults to `max` effort. The model is pinned to 1M context and set as the z.AI provider default.
|
||||
### Changed
|
||||
|
||||
- Switched the paid xAI provider (`xai` / `XAI_API_KEY`) from Chat Completions to the OpenAI Responses API (`POST https://api.x.ai/v1/responses`), matching SuperGrok `xai-oauth`. Prompt-cache affinity (`x-grok-conv-id`), reasoning-effort allowlisting, and encrypted-reasoning replay rules are now shared across both first-party xAI hosts.
|
||||
- Changed the paid xAI (`XAI_API_KEY`) default model from `grok-4-fast-non-reasoning` to `grok-4.5`.
|
||||
- Changed the SuperGrok (`xai-oauth`) default model from `grok-4.3` to `grok-4.5`.
|
||||
- Requested `reasoning.encrypted_content` on first-party xAI Responses calls (`xai` and `xai-oauth`) via the `include` parameter.
|
||||
- Replayed xAI encrypted reasoning items on later Responses turns instead of stripping `type: "reasoning"` history.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Invalidated stale paid-xAI model-cache rows written under Chat Completions so the Responses migration takes effect immediately instead of waiting for TTL expiry.
|
||||
- Clamped paid xAI Responses `minimal` reasoning effort to `low` (same wire map as SuperGrok) so `xai/grok-4.5` does not 400.
|
||||
- Suppressed presence/frequency penalties and stop sequences on xAI reasoning models so a configured `presencePenalty` does not 400 after the `grok-4.5` default change.
|
||||
- Stopped emitting stale `thinking.efforts` dials on paid xAI Responses catalog rows that reject `reasoning.effort` (`grok-code-fast-1`, `grok-build-0.1`, `grok-4.20-0309-reasoning`, and other off-allowlist reasoners).
|
||||
- Marked first-party xAI Responses hosts (`xai` and `xai-oauth`) as not supporting `reasoning.summary`, so paid `xai/grok-4.5` effort requests omit the unsupported field instead of sending `summary: "auto"`.
|
||||
- Removed unsupported `xhigh` (and `max`) thinking tiers from first-party Grok 4.5 / 4.3 / 3-mini Responses rows; leftover `xhigh`/`max` requests clamp to `high`. `grok-4.6*` and `grok-4.20-multi-agent*` advertise unmapped `xhigh`.
|
||||
- Stopped baking `reasoningEffortMap` on first-party xAI catalog rows that omit `reasoning.effort` (`omitReasoningEffort: true`).
|
||||
- Suppressed presence/frequency penalties on every first-party xAI Responses model, including non-reasoning ids such as `grok-2`; xAI's `/v1/responses` marks those fields unsupported.
|
||||
- Routed `grok-4.6` (added on main) through first-party xAI Responses and advertised its documented `xhigh` effort tier (4.5 stays 4-tier).
|
||||
|
||||
## [17.3.4] - 2026-08-14
|
||||
|
||||
### Added
|
||||
|
||||
@@ -21,6 +21,7 @@ import { resolveModelThinking } from "../src/model-thinking";
|
||||
import { isOllamaCloudOutputCapped, OLLAMA_CLOUD_MAX_OUTPUT_TOKENS } from "../src/provider-models/ollama";
|
||||
import {
|
||||
ALIBABA_TOKEN_PLAN_STATIC_MODELS,
|
||||
applyXaiResponsesThinkingPolicy,
|
||||
OPENAI_GPT_56_LONG_CONTEXT_COSTS,
|
||||
resolveWaferServerlessThinkingFormat,
|
||||
} from "../src/provider-models/openai-compat";
|
||||
@@ -353,6 +354,10 @@ export function applyOllamaCloudOutputCap(models: ModelSpec<Api>[]): void {
|
||||
}
|
||||
|
||||
function applyGeneratedModelPolicy(model: ModelSpec<Api>): void {
|
||||
if ((model.provider === "xai" || model.provider === "xai-oauth") && model.api === "openai-responses") {
|
||||
const updated = applyXaiResponsesThinkingPolicy(model as ModelSpec<"openai-responses">);
|
||||
model.compat = updated.compat;
|
||||
}
|
||||
const copilotLimits = model.provider === "github-copilot" ? COPILOT_GENERATED_LIMITS[model.id] : undefined;
|
||||
if (copilotLimits) {
|
||||
model.contextWindow = copilotLimits.contextWindow;
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
isDeepseekModelIdOrName,
|
||||
isGlm52ReasoningEffortModelId,
|
||||
isGrokReasoningEffortCapable,
|
||||
isGrokXHighEffortCapable,
|
||||
isKimiK3ModelId,
|
||||
isKimiK26ModelId,
|
||||
isKimiModelId,
|
||||
@@ -177,6 +178,22 @@ const MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat["reasoningEffortMap"]>
|
||||
xhigh: "high",
|
||||
};
|
||||
|
||||
/** Shared `minimal → low` clamp. xhigh-capable Grok keeps `xhigh` unmapped. */
|
||||
const XAI_RESPONSES_MINIMAL_EFFORT_MAP: NonNullable<OpenAICompat["reasoningEffortMap"]> = {
|
||||
minimal: "low",
|
||||
};
|
||||
/** Grok 4.5 / 4.3 / 3-mini: leftover `xhigh`/`max` clamp to `high`. */
|
||||
const XAI_RESPONSES_CLAMPED_EFFORT_MAP: NonNullable<OpenAICompat["reasoningEffortMap"]> = {
|
||||
minimal: "low",
|
||||
xhigh: "high",
|
||||
max: "high",
|
||||
};
|
||||
|
||||
/** Wire effort remap for first-party xAI Responses. */
|
||||
export function xaiResponsesReasoningEffortMap(modelId: string): NonNullable<OpenAICompat["reasoningEffortMap"]> {
|
||||
return isGrokXHighEffortCapable(modelId) ? XAI_RESPONSES_MINIMAL_EFFORT_MAP : XAI_RESPONSES_CLAMPED_EFFORT_MAP;
|
||||
}
|
||||
|
||||
function mergeModelReasoningEffortMap(
|
||||
compat: ResolvedOpenAISharedCompat,
|
||||
modelId: string,
|
||||
@@ -471,6 +488,8 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
|
||||
// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit
|
||||
// temperature/top_p/… with a 400 on every serving host (#5606).
|
||||
supportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),
|
||||
// xAI reasoning models 400 on presence/frequency penalties and stop.
|
||||
supportsPenaltyAndStopParams: !(isGrok && Boolean(spec.reasoning)),
|
||||
reasoningEffortMap: {},
|
||||
supportsUsageInStreaming: !isCerebras,
|
||||
// Kimi (including via OpenRouter and Fireworks router-form IDs such as
|
||||
@@ -684,34 +703,46 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
|
||||
const isLocalServingBackend =
|
||||
(!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) && LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider)) ||
|
||||
hasLocalLoopbackBaseUrl(baseUrl);
|
||||
const isXaiHost = modelMatchesHost({ provider: spec.provider, baseUrl }, "xai");
|
||||
|
||||
const compat: ResolvedOpenAIResponsesCompat = {
|
||||
supportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, "githubCopilot"),
|
||||
supportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),
|
||||
supportsReasoningEffort: spec.provider !== "xai-oauth" || isGrokReasoningEffortCapable(id),
|
||||
// Paid `xai` and SuperGrok `xai-oauth` share api.x.ai `/v1/responses`.
|
||||
// Only the Grok effort-capable allowlist accepts `reasoning.effort`;
|
||||
// other reasoners (grok-build, grok-code-fast-1, …) 400 if it is sent.
|
||||
supportsReasoningEffort: !isXaiHost || isGrokReasoningEffortCapable(id),
|
||||
supportsLongPromptCacheRetention: isOpenAIUrl,
|
||||
supportsPromptCacheBreakpoints,
|
||||
promptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? "30m" : undefined,
|
||||
// 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 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`).
|
||||
// GitHub Copilot and first-party xAI `/v1/responses` 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.
|
||||
supportsImageDetailOriginal:
|
||||
spec.provider !== "xai-oauth" && !modelMatchesHost({ provider: spec.provider, baseUrl }, "githubCopilot"),
|
||||
reasoningEffortMap: {},
|
||||
!isXaiHost && !modelMatchesHost({ provider: spec.provider, baseUrl }, "githubCopilot"),
|
||||
// api.x.ai rejects `reasoning.summary` (SuperGrok and paid key alike).
|
||||
supportsReasoningSummary: !isXaiHost,
|
||||
reasoningEffortMap: isXaiHost ? { ...xaiResponsesReasoningEffortMap(id) } : {},
|
||||
supportsReasoningParams: true,
|
||||
// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit
|
||||
// temperature/top_p/… with a 400 on every serving host (#5606).
|
||||
supportsSamplingParams: !isOpenAISamplingRestrictedModelId(id),
|
||||
// xAI `/v1/responses` rejects presence/frequency penalties for every
|
||||
// model, not only reasoners (https://docs.x.ai/developers/rest-api-reference/inference/chat).
|
||||
supportsPenaltyAndStopParams: !isXaiHost,
|
||||
thinkingFormat,
|
||||
reasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),
|
||||
omitReasoningEffort: false,
|
||||
includeEncryptedReasoning: spec.provider !== "xai-oauth",
|
||||
filterReasoningHistory: spec.provider === "xai-oauth" || (isOpenRouter && isAnthropicModel),
|
||||
// Ask xAI `/v1/responses` for `reasoning.encrypted_content` and replay
|
||||
// those items on later turns. OpenRouter Anthropic still filters
|
||||
// reasoning wrappers independently.
|
||||
includeEncryptedReasoning: true,
|
||||
filterReasoningHistory: isOpenRouter && isAnthropicModel,
|
||||
disableReasoningOnForcedToolChoice: isKimiModel,
|
||||
disableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,
|
||||
supportsToolChoice: true,
|
||||
@@ -752,13 +783,24 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
|
||||
MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),
|
||||
emptyLengthFinishIsContextError: spec.provider === "ollama",
|
||||
usesOpenAIToolCallIdLimit: spec.provider === "openai",
|
||||
promptCacheSessionHeader: spec.provider === "xai-oauth" ? "x-grok-conv-id" : undefined,
|
||||
promptCacheSessionHeader: isXaiHost ? "x-grok-conv-id" : undefined,
|
||||
streamFirstEventTimeoutMs: isLocalServingBackend ? 0 : spec.compat?.streamFirstEventTimeoutMs,
|
||||
streamIdleTimeoutMs: isLocalServingBackend
|
||||
? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS
|
||||
: spec.compat?.streamIdleTimeoutMs,
|
||||
};
|
||||
applyCompatOverrides(compat, spec.compat);
|
||||
if (isXaiHost) {
|
||||
const canonical = xaiResponsesReasoningEffortMap(id);
|
||||
compat.reasoningEffortMap = { ...compat.reasoningEffortMap, ...canonical };
|
||||
// xhigh-capable Grok advertises unmapped `xhigh`; drop a stale clamp
|
||||
// from previous snapshots so 4.6 / 16-agent mode is not rewritten to `high`.
|
||||
for (const key of ["xhigh", "max"] as const) {
|
||||
if (!(key in canonical)) {
|
||||
delete compat.reasoningEffortMap[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (spec.compat?.reasoningDisableMode === undefined) {
|
||||
compat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);
|
||||
}
|
||||
@@ -776,6 +818,7 @@ function pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnly
|
||||
strictResponsesPairing: compat.strictResponsesPairing,
|
||||
supportsImageDetailOriginal: compat.supportsImageDetailOriginal,
|
||||
supportsObfuscationOptOut: compat.supportsObfuscationOptOut,
|
||||
supportsReasoningSummary: compat.supportsReasoningSummary,
|
||||
isVercelGatewayHost: compat.isVercelGatewayHost,
|
||||
} satisfies ResponsesOnlyCompat;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export const KNOWN_HOSTS = {
|
||||
},
|
||||
umans: { providers: ["umans"], urlMarkers: ["api.code.umans.ai"] },
|
||||
xiaomi: { providers: ["xiaomi"], providerPrefixes: ["xiaomi-token-plan-"], urlMarkers: ["xiaomimimo.com"] },
|
||||
xai: { providers: ["xai"], urlMarkers: ["api.x.ai"] },
|
||||
xai: { providers: ["xai", "xai-oauth"], urlMarkers: ["api.x.ai"] },
|
||||
mistral: { providers: ["mistral"], urlMarkers: ["mistral.ai"] },
|
||||
together: { providers: ["together"], urlMarkers: ["api.together.xyz"] },
|
||||
baseten: { providers: ["baseten"], urlMarkers: ["baseten.co"] },
|
||||
|
||||
@@ -110,7 +110,13 @@ export const isGrokModelId = memo((modelId: string): boolean => {
|
||||
return /(?:^|[./_-])grok(?:[-.]|$)/i.test(modelId);
|
||||
});
|
||||
|
||||
const GROK_EFFORT_CAPABLE_PREFIXES = ["grok-3-mini", "grok-4.20-multi-agent", "grok-4.3", "grok-4.5"] as const;
|
||||
const GROK_EFFORT_CAPABLE_PREFIXES = [
|
||||
"grok-3-mini",
|
||||
"grok-4.20-multi-agent",
|
||||
"grok-4.3",
|
||||
"grok-4.5",
|
||||
"grok-4.6",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Grok SKUs that expose the wire `reasoning.effort` dial. Other Grok reasoners
|
||||
@@ -123,6 +129,27 @@ export const isGrokReasoningEffortCapable = memo((modelId: string): boolean => {
|
||||
return GROK_EFFORT_CAPABLE_PREFIXES.some(prefix => bare.startsWith(prefix));
|
||||
});
|
||||
|
||||
/**
|
||||
* `grok-4.20-multi-agent*` uses `reasoning.effort` to pick agent count
|
||||
* (`xhigh` is the 16-agent mode). Other first-party Grok effort SKUs stay on
|
||||
* `low|medium|high` unless {@link isGrokXHighEffortCapable} (currently
|
||||
* `grok-4.6*` plus multi-agent).
|
||||
* https://docs.x.ai/developers/model-capabilities/text/reasoning
|
||||
*/
|
||||
export const isGrokMultiAgentModelId = memo((modelId: string): boolean => {
|
||||
return bareModelId(modelId).trim().toLowerCase().startsWith("grok-4.20-multi-agent");
|
||||
});
|
||||
|
||||
/**
|
||||
* First-party Grok SKUs whose Responses wire accepts `reasoning.effort: "xhigh"`.
|
||||
* `grok-4.6*` documents xhigh as a reasoning depth; multi-agent uses it as
|
||||
* 16-agent mode. `grok-4.5` / `grok-4.3` / `grok-3-mini` do not.
|
||||
*/
|
||||
export const isGrokXHighEffortCapable = memo((modelId: string): boolean => {
|
||||
if (isGrokMultiAgentModelId(modelId)) return true;
|
||||
return bareModelId(modelId).trim().toLowerCase().startsWith("grok-4.6");
|
||||
});
|
||||
|
||||
/**
|
||||
* MiniMax M2-generation family (M2, M2.1, M2.5, M2.7, including `-highspeed`/
|
||||
* `-lightning`/`-her`/`-turbo` variants, dotless aliases like `minimax-m21`,
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
isDeepseekV4FlashModelId,
|
||||
isGlm52ReasoningEffortModelId,
|
||||
isGlm53ReasoningEffortModelId,
|
||||
isGrokXHighEffortCapable,
|
||||
isKimiK3ModelId,
|
||||
isMimoModelIdOrName,
|
||||
isMinimaxM2FamilyModelId,
|
||||
@@ -404,6 +405,11 @@ function getModelDefinedEfforts<TApi extends Api>(
|
||||
// Baseten's gpt-oss router mirrors its GLM route: high/max only.
|
||||
return HIGH_MAX_REASONING_EFFORTS;
|
||||
}
|
||||
// First-party Grok: `grok-4.6*` and `grok-4.20-multi-agent*` advertise
|
||||
// `xhigh`. Other effort-capable SKUs stay on `minimal/low/medium/high`.
|
||||
if (modelMatchesHost({ provider: spec.provider, baseUrl: spec.baseUrl ?? "" }, "xai")) {
|
||||
return isGrokXHighEffortCapable(spec.id) ? DEFAULT_REASONING_EFFORTS_WITH_XHIGH : DEFAULT_REASONING_EFFORTS;
|
||||
}
|
||||
return isOpenAICompatReasoningApi(spec.api) &&
|
||||
(isMinimaxM2FamilyModelId(spec.id) ||
|
||||
isOpenAIGptOssModelId(spec.id) ||
|
||||
|
||||
+303
-178
@@ -103716,7 +103716,7 @@
|
||||
"grok-2": {
|
||||
"id": "grok-2",
|
||||
"name": "Grok 2",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": false,
|
||||
@@ -103730,12 +103730,16 @@
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 131072,
|
||||
"maxTokens": 8192
|
||||
"maxTokens": 8192,
|
||||
"compat": {
|
||||
"supportsReasoningEffort": false,
|
||||
"omitReasoningEffort": true
|
||||
}
|
||||
},
|
||||
"grok-2-1212": {
|
||||
"id": "grok-2-1212",
|
||||
"name": "Grok 2 (1212)",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": false,
|
||||
@@ -103749,12 +103753,16 @@
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 131072,
|
||||
"maxTokens": 8192
|
||||
"maxTokens": 8192,
|
||||
"compat": {
|
||||
"supportsReasoningEffort": false,
|
||||
"omitReasoningEffort": true
|
||||
}
|
||||
},
|
||||
"grok-2-latest": {
|
||||
"id": "grok-2-latest",
|
||||
"name": "Grok 2 Latest",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": false,
|
||||
@@ -103768,12 +103776,16 @@
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 131072,
|
||||
"maxTokens": 8192
|
||||
"maxTokens": 8192,
|
||||
"compat": {
|
||||
"supportsReasoningEffort": false,
|
||||
"omitReasoningEffort": true
|
||||
}
|
||||
},
|
||||
"grok-2-vision": {
|
||||
"id": "grok-2-vision",
|
||||
"name": "Grok 2 Vision",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": false,
|
||||
@@ -103788,12 +103800,16 @@
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 8192,
|
||||
"maxTokens": 4096
|
||||
"maxTokens": 4096,
|
||||
"compat": {
|
||||
"supportsReasoningEffort": false,
|
||||
"omitReasoningEffort": true
|
||||
}
|
||||
},
|
||||
"grok-2-vision-1212": {
|
||||
"id": "grok-2-vision-1212",
|
||||
"name": "Grok 2 Vision (1212)",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": false,
|
||||
@@ -103808,12 +103824,16 @@
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 8192,
|
||||
"maxTokens": 4096
|
||||
"maxTokens": 4096,
|
||||
"compat": {
|
||||
"supportsReasoningEffort": false,
|
||||
"omitReasoningEffort": true
|
||||
}
|
||||
},
|
||||
"grok-2-vision-latest": {
|
||||
"id": "grok-2-vision-latest",
|
||||
"name": "Grok 2 Vision Latest",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": false,
|
||||
@@ -103828,12 +103848,16 @@
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 8192,
|
||||
"maxTokens": 4096
|
||||
"maxTokens": 4096,
|
||||
"compat": {
|
||||
"supportsReasoningEffort": false,
|
||||
"omitReasoningEffort": true
|
||||
}
|
||||
},
|
||||
"grok-3": {
|
||||
"id": "grok-3",
|
||||
"name": "Grok 3",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": false,
|
||||
@@ -103847,12 +103871,16 @@
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 131072,
|
||||
"maxTokens": 8192
|
||||
"maxTokens": 8192,
|
||||
"compat": {
|
||||
"supportsReasoningEffort": false,
|
||||
"omitReasoningEffort": true
|
||||
}
|
||||
},
|
||||
"grok-3-fast": {
|
||||
"id": "grok-3-fast",
|
||||
"name": "Grok 3 Fast",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": false,
|
||||
@@ -103866,12 +103894,16 @@
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 131072,
|
||||
"maxTokens": 8192
|
||||
"maxTokens": 8192,
|
||||
"compat": {
|
||||
"supportsReasoningEffort": false,
|
||||
"omitReasoningEffort": true
|
||||
}
|
||||
},
|
||||
"grok-3-fast-latest": {
|
||||
"id": "grok-3-fast-latest",
|
||||
"name": "Grok 3 Fast Latest",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": false,
|
||||
@@ -103885,12 +103917,16 @@
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 131072,
|
||||
"maxTokens": 8192
|
||||
"maxTokens": 8192,
|
||||
"compat": {
|
||||
"supportsReasoningEffort": false,
|
||||
"omitReasoningEffort": true
|
||||
}
|
||||
},
|
||||
"grok-3-latest": {
|
||||
"id": "grok-3-latest",
|
||||
"name": "Grok 3 Latest",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": false,
|
||||
@@ -103904,12 +103940,16 @@
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 131072,
|
||||
"maxTokens": 8192
|
||||
"maxTokens": 8192,
|
||||
"compat": {
|
||||
"supportsReasoningEffort": false,
|
||||
"omitReasoningEffort": true
|
||||
}
|
||||
},
|
||||
"grok-3-mini": {
|
||||
"id": "grok-3-mini",
|
||||
"name": "Grok 3 Mini",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": true,
|
||||
@@ -103931,13 +103971,25 @@
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
],
|
||||
"effortMap": {
|
||||
"minimal": "low"
|
||||
}
|
||||
},
|
||||
"compat": {
|
||||
"reasoningEffortMap": {
|
||||
"minimal": "low",
|
||||
"xhigh": "high",
|
||||
"max": "high"
|
||||
},
|
||||
"supportsReasoningEffort": true,
|
||||
"omitReasoningEffort": false
|
||||
}
|
||||
},
|
||||
"grok-3-mini-fast": {
|
||||
"id": "grok-3-mini-fast",
|
||||
"name": "Grok 3 Mini Fast",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": true,
|
||||
@@ -103959,13 +104011,25 @@
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
],
|
||||
"effortMap": {
|
||||
"minimal": "low"
|
||||
}
|
||||
},
|
||||
"compat": {
|
||||
"reasoningEffortMap": {
|
||||
"minimal": "low",
|
||||
"xhigh": "high",
|
||||
"max": "high"
|
||||
},
|
||||
"supportsReasoningEffort": true,
|
||||
"omitReasoningEffort": false
|
||||
}
|
||||
},
|
||||
"grok-3-mini-fast-latest": {
|
||||
"id": "grok-3-mini-fast-latest",
|
||||
"name": "Grok 3 Mini Fast Latest",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": true,
|
||||
@@ -103987,13 +104051,25 @@
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
],
|
||||
"effortMap": {
|
||||
"minimal": "low"
|
||||
}
|
||||
},
|
||||
"compat": {
|
||||
"reasoningEffortMap": {
|
||||
"minimal": "low",
|
||||
"xhigh": "high",
|
||||
"max": "high"
|
||||
},
|
||||
"supportsReasoningEffort": true,
|
||||
"omitReasoningEffort": false
|
||||
}
|
||||
},
|
||||
"grok-3-mini-latest": {
|
||||
"id": "grok-3-mini-latest",
|
||||
"name": "Grok 3 Mini Latest",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": true,
|
||||
@@ -104015,13 +104091,25 @@
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
],
|
||||
"effortMap": {
|
||||
"minimal": "low"
|
||||
}
|
||||
},
|
||||
"compat": {
|
||||
"reasoningEffortMap": {
|
||||
"minimal": "low",
|
||||
"xhigh": "high",
|
||||
"max": "high"
|
||||
},
|
||||
"supportsReasoningEffort": true,
|
||||
"omitReasoningEffort": false
|
||||
}
|
||||
},
|
||||
"grok-4": {
|
||||
"id": "grok-4",
|
||||
"name": "Grok 4",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": true,
|
||||
@@ -104036,20 +104124,15 @@
|
||||
},
|
||||
"contextWindow": 256000,
|
||||
"maxTokens": 64000,
|
||||
"thinking": {
|
||||
"mode": "effort",
|
||||
"efforts": [
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
"compat": {
|
||||
"supportsReasoningEffort": false,
|
||||
"omitReasoningEffort": true
|
||||
}
|
||||
},
|
||||
"grok-4-1-fast": {
|
||||
"id": "grok-4-1-fast",
|
||||
"name": "Grok 4.1 Fast",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": true,
|
||||
@@ -104065,20 +104148,15 @@
|
||||
},
|
||||
"contextWindow": 2000000,
|
||||
"maxTokens": 30000,
|
||||
"thinking": {
|
||||
"mode": "effort",
|
||||
"efforts": [
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
"compat": {
|
||||
"supportsReasoningEffort": false,
|
||||
"omitReasoningEffort": true
|
||||
}
|
||||
},
|
||||
"grok-4-1-fast-non-reasoning": {
|
||||
"id": "grok-4-1-fast-non-reasoning",
|
||||
"name": "Grok 4.1 Fast (Non-Reasoning)",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": false,
|
||||
@@ -104093,12 +104171,16 @@
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 2000000,
|
||||
"maxTokens": 30000
|
||||
"maxTokens": 30000,
|
||||
"compat": {
|
||||
"supportsReasoningEffort": false,
|
||||
"omitReasoningEffort": true
|
||||
}
|
||||
},
|
||||
"grok-4-fast": {
|
||||
"id": "grok-4-fast",
|
||||
"name": "Grok 4 Fast",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": true,
|
||||
@@ -104114,20 +104196,15 @@
|
||||
},
|
||||
"contextWindow": 2000000,
|
||||
"maxTokens": 30000,
|
||||
"thinking": {
|
||||
"mode": "effort",
|
||||
"efforts": [
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
"compat": {
|
||||
"supportsReasoningEffort": false,
|
||||
"omitReasoningEffort": true
|
||||
}
|
||||
},
|
||||
"grok-4-fast-non-reasoning": {
|
||||
"id": "grok-4-fast-non-reasoning",
|
||||
"name": "Grok 4 Fast (Non-Reasoning)",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": false,
|
||||
@@ -104142,12 +104219,16 @@
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 2000000,
|
||||
"maxTokens": 30000
|
||||
"maxTokens": 30000,
|
||||
"compat": {
|
||||
"supportsReasoningEffort": false,
|
||||
"omitReasoningEffort": true
|
||||
}
|
||||
},
|
||||
"grok-4.20-0309-non-reasoning": {
|
||||
"id": "grok-4.20-0309-non-reasoning",
|
||||
"name": "Grok 4.20 (Non-Reasoning)",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": false,
|
||||
@@ -104162,12 +104243,16 @@
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 1000000,
|
||||
"maxTokens": 30000
|
||||
"maxTokens": 30000,
|
||||
"compat": {
|
||||
"supportsReasoningEffort": false,
|
||||
"omitReasoningEffort": true
|
||||
}
|
||||
},
|
||||
"grok-4.20-0309-reasoning": {
|
||||
"id": "grok-4.20-0309-reasoning",
|
||||
"name": "Grok 4.20 (Reasoning)",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": true,
|
||||
@@ -104183,21 +104268,15 @@
|
||||
},
|
||||
"contextWindow": 1000000,
|
||||
"maxTokens": 30000,
|
||||
"thinking": {
|
||||
"mode": "effort",
|
||||
"efforts": [
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
],
|
||||
"requiresEffort": true
|
||||
"compat": {
|
||||
"supportsReasoningEffort": false,
|
||||
"omitReasoningEffort": true
|
||||
}
|
||||
},
|
||||
"grok-4.20-beta-latest-non-reasoning": {
|
||||
"id": "grok-4.20-beta-latest-non-reasoning",
|
||||
"name": "Grok 4.20 Beta (Non-Reasoning)",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": false,
|
||||
@@ -104212,12 +104291,16 @@
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 2000000,
|
||||
"maxTokens": 30000
|
||||
"maxTokens": 30000,
|
||||
"compat": {
|
||||
"supportsReasoningEffort": false,
|
||||
"omitReasoningEffort": true
|
||||
}
|
||||
},
|
||||
"grok-4.20-beta-latest-reasoning": {
|
||||
"id": "grok-4.20-beta-latest-reasoning",
|
||||
"name": "Grok 4.20 Beta (Reasoning)",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": true,
|
||||
@@ -104233,21 +104316,15 @@
|
||||
},
|
||||
"contextWindow": 2000000,
|
||||
"maxTokens": 30000,
|
||||
"thinking": {
|
||||
"mode": "effort",
|
||||
"efforts": [
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
],
|
||||
"requiresEffort": true
|
||||
"compat": {
|
||||
"supportsReasoningEffort": false,
|
||||
"omitReasoningEffort": true
|
||||
}
|
||||
},
|
||||
"grok-4.20-multi-agent-beta-latest": {
|
||||
"id": "grok-4.20-multi-agent-beta-latest",
|
||||
"name": "Grok 4.20 Multi-Agent Beta",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": true,
|
||||
@@ -104269,14 +104346,25 @@
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"effortMap": {
|
||||
"minimal": "low"
|
||||
}
|
||||
},
|
||||
"compat": {
|
||||
"reasoningEffortMap": {
|
||||
"minimal": "low"
|
||||
},
|
||||
"supportsReasoningEffort": true,
|
||||
"omitReasoningEffort": false
|
||||
}
|
||||
},
|
||||
"grok-4.3": {
|
||||
"id": "grok-4.3",
|
||||
"name": "Grok 4.3",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": true,
|
||||
@@ -104299,13 +104387,25 @@
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
],
|
||||
"effortMap": {
|
||||
"minimal": "low"
|
||||
}
|
||||
},
|
||||
"compat": {
|
||||
"reasoningEffortMap": {
|
||||
"minimal": "low",
|
||||
"xhigh": "high",
|
||||
"max": "high"
|
||||
},
|
||||
"supportsReasoningEffort": true,
|
||||
"omitReasoningEffort": false
|
||||
}
|
||||
},
|
||||
"grok-4.5": {
|
||||
"id": "grok-4.5",
|
||||
"name": "Grok 4.5",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": true,
|
||||
@@ -104328,13 +104428,25 @@
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
],
|
||||
"effortMap": {
|
||||
"minimal": "low"
|
||||
}
|
||||
},
|
||||
"compat": {
|
||||
"reasoningEffortMap": {
|
||||
"minimal": "low",
|
||||
"xhigh": "high",
|
||||
"max": "high"
|
||||
},
|
||||
"supportsReasoningEffort": true,
|
||||
"omitReasoningEffort": false
|
||||
}
|
||||
},
|
||||
"grok-4.6": {
|
||||
"id": "grok-4.6",
|
||||
"name": "Grok 4.6",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": true,
|
||||
@@ -104356,14 +104468,25 @@
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"effortMap": {
|
||||
"minimal": "low"
|
||||
}
|
||||
},
|
||||
"compat": {
|
||||
"reasoningEffortMap": {
|
||||
"minimal": "low"
|
||||
},
|
||||
"supportsReasoningEffort": true,
|
||||
"omitReasoningEffort": false
|
||||
}
|
||||
},
|
||||
"grok-beta": {
|
||||
"id": "grok-beta",
|
||||
"name": "Grok Beta",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": false,
|
||||
@@ -104377,12 +104500,16 @@
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 131072,
|
||||
"maxTokens": 4096
|
||||
"maxTokens": 4096,
|
||||
"compat": {
|
||||
"supportsReasoningEffort": false,
|
||||
"omitReasoningEffort": true
|
||||
}
|
||||
},
|
||||
"grok-build-0.1": {
|
||||
"id": "grok-build-0.1",
|
||||
"name": "Grok Build 0.1",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": true,
|
||||
@@ -104398,20 +104525,15 @@
|
||||
},
|
||||
"contextWindow": 256000,
|
||||
"maxTokens": 256000,
|
||||
"thinking": {
|
||||
"mode": "effort",
|
||||
"efforts": [
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
"compat": {
|
||||
"supportsReasoningEffort": false,
|
||||
"omitReasoningEffort": true
|
||||
}
|
||||
},
|
||||
"grok-code-fast-1": {
|
||||
"id": "grok-code-fast-1",
|
||||
"name": "Grok Code Fast 1",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": true,
|
||||
@@ -104426,20 +104548,15 @@
|
||||
},
|
||||
"contextWindow": 256000,
|
||||
"maxTokens": 10000,
|
||||
"thinking": {
|
||||
"mode": "effort",
|
||||
"efforts": [
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
"compat": {
|
||||
"supportsReasoningEffort": false,
|
||||
"omitReasoningEffort": true
|
||||
}
|
||||
},
|
||||
"grok-vision-beta": {
|
||||
"id": "grok-vision-beta",
|
||||
"name": "Grok Vision Beta",
|
||||
"api": "openai-completions",
|
||||
"api": "openai-responses",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": false,
|
||||
@@ -104454,7 +104571,11 @@
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 8192,
|
||||
"maxTokens": 4096
|
||||
"maxTokens": 4096,
|
||||
"compat": {
|
||||
"supportsReasoningEffort": false,
|
||||
"omitReasoningEffort": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"xai-oauth": {
|
||||
@@ -104480,11 +104601,8 @@
|
||||
"supportsComputerUse": false,
|
||||
"supportsComputerUseConfig": false,
|
||||
"compat": {
|
||||
"reasoningEffortMap": {
|
||||
"minimal": "low"
|
||||
},
|
||||
"includeEncryptedReasoning": false,
|
||||
"filterReasoningHistory": true,
|
||||
"includeEncryptedReasoning": true,
|
||||
"filterReasoningHistory": false,
|
||||
"supportsImageDetailOriginal": false,
|
||||
"omitReasoningEffort": true,
|
||||
"supportsReasoningEffort": false
|
||||
@@ -104512,11 +104630,8 @@
|
||||
"supportsComputerUse": false,
|
||||
"supportsComputerUseConfig": false,
|
||||
"compat": {
|
||||
"reasoningEffortMap": {
|
||||
"minimal": "low"
|
||||
},
|
||||
"includeEncryptedReasoning": false,
|
||||
"filterReasoningHistory": true,
|
||||
"includeEncryptedReasoning": true,
|
||||
"filterReasoningHistory": false,
|
||||
"supportsImageDetailOriginal": false,
|
||||
"omitReasoningEffort": true,
|
||||
"supportsReasoningEffort": false
|
||||
@@ -104559,8 +104674,8 @@
|
||||
"reasoningEffortMap": {
|
||||
"minimal": "low"
|
||||
},
|
||||
"includeEncryptedReasoning": false,
|
||||
"filterReasoningHistory": true,
|
||||
"includeEncryptedReasoning": true,
|
||||
"filterReasoningHistory": false,
|
||||
"supportsImageDetailOriginal": false,
|
||||
"omitReasoningEffort": false,
|
||||
"supportsReasoningEffort": true
|
||||
@@ -104591,8 +104706,7 @@
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh"
|
||||
"high"
|
||||
],
|
||||
"effortMap": {
|
||||
"minimal": "low"
|
||||
@@ -104602,10 +104716,12 @@
|
||||
"supportsComputerUseConfig": false,
|
||||
"compat": {
|
||||
"reasoningEffortMap": {
|
||||
"minimal": "low"
|
||||
"minimal": "low",
|
||||
"xhigh": "high",
|
||||
"max": "high"
|
||||
},
|
||||
"includeEncryptedReasoning": false,
|
||||
"filterReasoningHistory": true,
|
||||
"includeEncryptedReasoning": true,
|
||||
"filterReasoningHistory": false,
|
||||
"supportsImageDetailOriginal": false,
|
||||
"omitReasoningEffort": false,
|
||||
"supportsReasoningEffort": true
|
||||
@@ -104630,6 +104746,52 @@
|
||||
},
|
||||
"contextWindow": 500000,
|
||||
"maxTokens": 500000,
|
||||
"thinking": {
|
||||
"mode": "effort",
|
||||
"efforts": [
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
],
|
||||
"effortMap": {
|
||||
"minimal": "low"
|
||||
}
|
||||
},
|
||||
"supportsComputerUse": false,
|
||||
"supportsComputerUseConfig": false,
|
||||
"compat": {
|
||||
"reasoningEffortMap": {
|
||||
"minimal": "low",
|
||||
"xhigh": "high",
|
||||
"max": "high"
|
||||
},
|
||||
"includeEncryptedReasoning": true,
|
||||
"filterReasoningHistory": false,
|
||||
"supportsImageDetailOriginal": false,
|
||||
"omitReasoningEffort": false,
|
||||
"supportsReasoningEffort": true
|
||||
}
|
||||
},
|
||||
"grok-4.6": {
|
||||
"id": "grok-4.6",
|
||||
"name": "Grok 4.6",
|
||||
"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": 500000,
|
||||
"maxTokens": 500000,
|
||||
"thinking": {
|
||||
"mode": "effort",
|
||||
"efforts": [
|
||||
@@ -104649,41 +104811,13 @@
|
||||
"reasoningEffortMap": {
|
||||
"minimal": "low"
|
||||
},
|
||||
"includeEncryptedReasoning": false,
|
||||
"filterReasoningHistory": true,
|
||||
"includeEncryptedReasoning": true,
|
||||
"filterReasoningHistory": false,
|
||||
"supportsImageDetailOriginal": false,
|
||||
"omitReasoningEffort": false,
|
||||
"supportsReasoningEffort": true
|
||||
}
|
||||
},
|
||||
"grok-4.6": {
|
||||
"id": "grok-4.6",
|
||||
"name": "Grok 4.6",
|
||||
"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": 500000,
|
||||
"maxTokens": 500000,
|
||||
"supportsComputerUse": false,
|
||||
"supportsComputerUseConfig": false,
|
||||
"compat": {
|
||||
"includeEncryptedReasoning": false,
|
||||
"filterReasoningHistory": true,
|
||||
"supportsImageDetailOriginal": false,
|
||||
"omitReasoningEffort": true
|
||||
}
|
||||
},
|
||||
"grok-build": {
|
||||
"id": "grok-build",
|
||||
"name": "Grok Build",
|
||||
@@ -104706,11 +104840,8 @@
|
||||
"supportsComputerUse": false,
|
||||
"supportsComputerUseConfig": false,
|
||||
"compat": {
|
||||
"reasoningEffortMap": {
|
||||
"minimal": "low"
|
||||
},
|
||||
"includeEncryptedReasoning": false,
|
||||
"filterReasoningHistory": true,
|
||||
"includeEncryptedReasoning": true,
|
||||
"filterReasoningHistory": false,
|
||||
"supportsImageDetailOriginal": false,
|
||||
"omitReasoningEffort": true,
|
||||
"supportsReasoningEffort": false
|
||||
@@ -104738,11 +104869,8 @@
|
||||
"supportsComputerUse": false,
|
||||
"supportsComputerUseConfig": false,
|
||||
"compat": {
|
||||
"reasoningEffortMap": {
|
||||
"minimal": "low"
|
||||
},
|
||||
"includeEncryptedReasoning": false,
|
||||
"filterReasoningHistory": true,
|
||||
"includeEncryptedReasoning": true,
|
||||
"filterReasoningHistory": false,
|
||||
"supportsImageDetailOriginal": false,
|
||||
"omitReasoningEffort": true,
|
||||
"supportsReasoningEffort": false
|
||||
@@ -104769,11 +104897,8 @@
|
||||
"supportsComputerUse": false,
|
||||
"supportsComputerUseConfig": false,
|
||||
"compat": {
|
||||
"reasoningEffortMap": {
|
||||
"minimal": "low"
|
||||
},
|
||||
"includeEncryptedReasoning": false,
|
||||
"filterReasoningHistory": true,
|
||||
"includeEncryptedReasoning": true,
|
||||
"filterReasoningHistory": false,
|
||||
"supportsImageDetailOriginal": false,
|
||||
"omitReasoningEffort": true,
|
||||
"supportsReasoningEffort": false
|
||||
@@ -112025,4 +112150,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,13 +478,13 @@ export const CATALOG_PROVIDERS = [
|
||||
},
|
||||
{
|
||||
id: "xai",
|
||||
defaultModel: "grok-4-fast-non-reasoning",
|
||||
defaultModel: "grok-4.5",
|
||||
envVars: ["XAI_API_KEY"],
|
||||
createModelManagerOptions: (config: ModelManagerConfig) => xaiModelManagerOptions(config),
|
||||
},
|
||||
{
|
||||
id: "xai-oauth",
|
||||
defaultModel: "grok-4.3",
|
||||
defaultModel: "grok-4.5",
|
||||
envVars: ["XAI_OAUTH_TOKEN", "XAI_API_KEY"],
|
||||
createModelManagerOptions: (config: ModelManagerConfig) => xaiOAuthModelManagerOptions(config),
|
||||
catalogDiscovery: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { USER_AGENT } from "@oh-my-pi/pi-utils";
|
||||
import * as logger from "@oh-my-pi/pi-utils/logger";
|
||||
import { xaiResponsesReasoningEffortMap } from "../compat/openai";
|
||||
import {
|
||||
DEFAULT_OPENAI_COMPATIBLE_DISCOVERY_TIMEOUT_MS,
|
||||
fetchOpenAICompatibleModels,
|
||||
@@ -1257,8 +1258,23 @@ export interface XaiModelManagerConfig {
|
||||
fetch?: FetchImpl;
|
||||
}
|
||||
|
||||
export function xaiModelManagerOptions(config?: XaiModelManagerConfig): ModelManagerOptions<"openai-completions"> {
|
||||
return createSimpleOpenAICompletionsOptions("xai", "https://api.x.ai/v1", config);
|
||||
export function xaiModelManagerOptions(config?: XaiModelManagerConfig): ModelManagerOptions<"openai-responses"> {
|
||||
return {
|
||||
...createOpenAICompatibleModelManagerOptions({
|
||||
api: "openai-responses",
|
||||
providerId: "xai",
|
||||
defaultBaseUrl: "https://api.x.ai/v1",
|
||||
config,
|
||||
requireApiKey: true,
|
||||
mapModel: mapWithBundledReference,
|
||||
}),
|
||||
// Completions → Responses migration: a fresh authoritative cache written
|
||||
// by the old resolver stores `api: "openai-completions"` for these ids.
|
||||
// Without a drop list, `online-if-uncached` skips the network and
|
||||
// `mergeDynamicModel` lets the cached api win over the new static
|
||||
// Responses entries until TTL expiry.
|
||||
dropCachedModelIdsOnStaticMismatch: getBundledModels("xai").map(model => model.id),
|
||||
};
|
||||
}
|
||||
|
||||
export interface XaiOAuthModelManagerConfig {
|
||||
@@ -1316,6 +1332,7 @@ export const XAI_OAUTH_CURATED_MODELS: readonly XAICuratedModel[] = [
|
||||
},
|
||||
{ id: "grok-4.3", contextWindow: 1_000_000, name: "Grok 4.3", input: ["text", "image"] },
|
||||
{ id: "grok-4.5", contextWindow: 500_000, name: "Grok 4.5", input: ["text", "image"] },
|
||||
{ id: "grok-4.6", contextWindow: 500_000, name: "Grok 4.6", input: ["text", "image"] },
|
||||
// grok-4.20-multi-agent-0309 is text-only per the bundled catalog; omit `input` for the default.
|
||||
{ id: "grok-4.20-multi-agent-0309", contextWindow: 2_000_000, name: "Grok 4.20 (Multi-Agent)" },
|
||||
{
|
||||
@@ -1352,21 +1369,47 @@ const XAI_NON_CHAT_PREFIXES = ["grok-imagine-", "grok-stt-", "grok-voice-"] as c
|
||||
function withXaiOAuthCompatDefaults(model: ModelSpec<"openai-responses">): ModelSpec<"openai-responses"> {
|
||||
const compat = {
|
||||
...(model.compat ?? {}),
|
||||
includeEncryptedReasoning: model.compat?.includeEncryptedReasoning ?? false,
|
||||
filterReasoningHistory: model.compat?.filterReasoningHistory ?? true,
|
||||
includeEncryptedReasoning: model.compat?.includeEncryptedReasoning ?? true,
|
||||
filterReasoningHistory: model.compat?.filterReasoningHistory ?? false,
|
||||
supportsImageDetailOriginal: model.compat?.supportsImageDetailOriginal ?? false,
|
||||
omitReasoningEffort: model.compat?.omitReasoningEffort ?? !isGrokReasoningEffortCapable(model.id),
|
||||
};
|
||||
return { ...model, compat };
|
||||
}
|
||||
|
||||
// Hermes-agent parity: only the `minimal -> low` clamp is applied (see
|
||||
// hermes-agent/agent/transports/codex.py:92 `_effort_clamp = {"minimal":
|
||||
// "low"}`). Hermes sends `xhigh` to xAI verbatim and we match that contract
|
||||
// — let xAI decide if the level is valid for the specific Grok model.
|
||||
// `resolveModelThinking` folds this into `model.thinking.effortMap`, downstream
|
||||
// of the omitReasoningEffort gate in pi-ai's stream.ts.
|
||||
const XAI_REASONING_EFFORT_MAP = { minimal: "low" } as const;
|
||||
// Hermes-agent parity for `minimal -> low` (see hermes-agent/agent/transports/
|
||||
// codex.py:92). Multi-agent Grok keeps `xhigh` unmapped (agent-count mode);
|
||||
// other first-party SKUs clamp leftover `xhigh`/`max` to `high`.
|
||||
// `resolveModelThinking` folds this into `model.thinking.effortMap`.
|
||||
|
||||
/**
|
||||
* Bake first-party xAI Responses effort-dial metadata onto a catalog spec.
|
||||
*
|
||||
* models.dev marks many Grok SKUs as reasoners and the thinking rebake would
|
||||
* otherwise emit a default `minimal/low/medium/high` dial. api.x.ai only
|
||||
* accepts `reasoning.effort` for {@link isGrokReasoningEffortCapable} ids —
|
||||
* off-allowlist reasoners (`grok-code-fast-1`, `grok-build-0.1`,
|
||||
* `grok-4.20-0309-reasoning`, …) 400 if the param is sent. SuperGrok
|
||||
* (`xai-oauth`) already curates this via {@link mergeCuratedIntoModel}; paid
|
||||
* `xai` rows come from stencil.so and need the same wire facts in the exported
|
||||
* `models.json` so direct catalog readers do not present an unsupported dial.
|
||||
*
|
||||
* Explicit `compat.supportsReasoningEffort` / `omitReasoningEffort` win.
|
||||
*/
|
||||
export function applyXaiResponsesThinkingPolicy(model: ModelSpec<"openai-responses">): ModelSpec<"openai-responses"> {
|
||||
const effortCapable = model.compat?.supportsReasoningEffort ?? isGrokReasoningEffortCapable(model.id);
|
||||
const compat = {
|
||||
...(model.compat ?? {}),
|
||||
supportsReasoningEffort: effortCapable,
|
||||
omitReasoningEffort: model.compat?.omitReasoningEffort ?? !effortCapable,
|
||||
};
|
||||
if (effortCapable) {
|
||||
compat.reasoningEffortMap = { ...xaiResponsesReasoningEffortMap(model.id) };
|
||||
} else {
|
||||
delete compat.reasoningEffortMap;
|
||||
}
|
||||
return { ...model, compat };
|
||||
}
|
||||
|
||||
// xai-oauth's /v1/models exposes no per-request output limit on the OAuth
|
||||
// (Grok Build / SuperGrok) surface, so the curated catalog owns `maxTokens`
|
||||
@@ -1381,9 +1424,9 @@ const XAI_REASONING_EFFORT_MAP = { minimal: "low" } as const;
|
||||
// reasoning metadata and fetchOpenAICompatibleModels defaults reasoning to
|
||||
// false). Caller supplies a `base` Model (either a freshly synthesised seed
|
||||
// or a dynamic-fetched entry); the helper layers curated fields on top.
|
||||
// The `minimal -> low` effort clamp (XAI_REASONING_EFFORT_MAP) is always
|
||||
// merged in so dynamic-fetched models — which arrive without curated
|
||||
// compat keys — still get the clamp applyResponsesReasoningParams expects.
|
||||
// The effort remap from {@link xaiResponsesReasoningEffortMap} is merged
|
||||
// only onto effort-capable rows. Off-allowlist reasoners omit the wire
|
||||
// param, so a map on those specs is dead weight.
|
||||
// The effort-dial pair (`supportsReasoningEffort`/`omitReasoningEffort`) is
|
||||
// authoritative: a stale flag on `base` (previous snapshot or dynamic fetch)
|
||||
// must not outlive an allowlist change in identity/family.ts.
|
||||
@@ -1394,13 +1437,17 @@ function mergeCuratedIntoModel(
|
||||
const effortCapable = curated.supportsReasoningEffort ?? isGrokReasoningEffortCapable(curated.id);
|
||||
const compat = {
|
||||
...(base.compat ?? {}),
|
||||
reasoningEffortMap: { ...XAI_REASONING_EFFORT_MAP, ...(base.compat?.reasoningEffortMap ?? {}) },
|
||||
includeEncryptedReasoning: base.compat?.includeEncryptedReasoning ?? false,
|
||||
filterReasoningHistory: base.compat?.filterReasoningHistory ?? true,
|
||||
includeEncryptedReasoning: base.compat?.includeEncryptedReasoning ?? true,
|
||||
filterReasoningHistory: false,
|
||||
supportsImageDetailOriginal: base.compat?.supportsImageDetailOriginal ?? false,
|
||||
omitReasoningEffort: !effortCapable,
|
||||
supportsReasoningEffort: effortCapable,
|
||||
};
|
||||
if (effortCapable) {
|
||||
compat.reasoningEffortMap = { ...xaiResponsesReasoningEffortMap(curated.id) };
|
||||
} else {
|
||||
delete compat.reasoningEffortMap;
|
||||
}
|
||||
return {
|
||||
...base,
|
||||
contextWindow: curated.contextWindow,
|
||||
@@ -1502,7 +1549,7 @@ export function buildXaiOAuthStaticSeed(baseUrl?: string): ModelSpec<"openai-res
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: curated.contextWindow,
|
||||
maxTokens: curated.contextWindow,
|
||||
compat: { reasoningEffortMap: XAI_REASONING_EFFORT_MAP },
|
||||
compat: { reasoningEffortMap: xaiResponsesReasoningEffortMap(curated.id) },
|
||||
};
|
||||
return mergeCuratedIntoModel(base, curated);
|
||||
});
|
||||
@@ -5713,6 +5760,15 @@ function openAiCompletionsDescriptor(
|
||||
return simpleModelsDevDescriptor(modelsDevKey, providerId, "openai-completions", baseUrl, options);
|
||||
}
|
||||
|
||||
function openAiResponsesDescriptor(
|
||||
modelsDevKey: string,
|
||||
providerId: string,
|
||||
baseUrl: string,
|
||||
options: Omit<ModelsDevProviderDescriptor, "modelsDevKey" | "providerId" | "api" | "baseUrl"> = {},
|
||||
): ModelsDevProviderDescriptor {
|
||||
return simpleModelsDevDescriptor(modelsDevKey, providerId, "openai-responses", baseUrl, options);
|
||||
}
|
||||
|
||||
function anthropicMessagesDescriptor(
|
||||
modelsDevKey: string,
|
||||
providerId: string,
|
||||
@@ -5837,7 +5893,9 @@ const MODELS_DEV_PROVIDER_DESCRIPTORS_CORE: readonly ModelsDevProviderDescriptor
|
||||
defaultContextWindow: 131072,
|
||||
}),
|
||||
// --- xAI ---
|
||||
openAiCompletionsDescriptor("xai", "xai", "https://api.x.ai/v1"),
|
||||
openAiResponsesDescriptor("xai", "xai", "https://api.x.ai/v1", {
|
||||
transformModel: model => applyXaiResponsesThinkingPolicy(model as ModelSpec<"openai-responses">),
|
||||
}),
|
||||
// --- DeepSeek ---
|
||||
openAiCompletionsDescriptor("deepseek", "deepseek", "https://api.deepseek.com", {
|
||||
// Only ship the v4 family as built-ins; older deepseek-chat / deepseek-reasoner
|
||||
|
||||
@@ -363,6 +363,13 @@ export interface OpenAICompat {
|
||||
* model id. Default: true. Issue #5606.
|
||||
*/
|
||||
supportsSamplingParams?: boolean;
|
||||
/**
|
||||
* Whether presence/frequency penalties and stop sequences may be sent.
|
||||
* First-party xAI `/v1/responses` rejects penalty fields for every model.
|
||||
* xAI reasoning models also reject them (and `stop`) on chat completions.
|
||||
* When unset, auto-detected. Default: true.
|
||||
*/
|
||||
supportsPenaltyAndStopParams?: boolean;
|
||||
/** Always send a max-token field when the caller did not provide one. Default: auto-detected (Kimi-family models derive TPM limits from max_tokens). */
|
||||
alwaysSendMaxTokens?: boolean;
|
||||
/** Whether Responses-API tool-call/result history must be strictly paired. Default: auto-detected (Azure OpenAI, GitHub Copilot). */
|
||||
@@ -578,6 +585,7 @@ export interface ResolvedOpenAISharedCompat {
|
||||
reasoningEffortMap: Partial<Record<Effort, string>>;
|
||||
supportsReasoningParams: boolean;
|
||||
supportsSamplingParams: boolean;
|
||||
supportsPenaltyAndStopParams: boolean;
|
||||
thinkingFormat: OpenAIReasoningFormat;
|
||||
/** Kimi Code transport selected by live per-model protocol metadata. */
|
||||
kimiApiFormat?: OpenAICompat["kimiApiFormat"];
|
||||
@@ -643,6 +651,7 @@ export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat &
|
||||
| "reasoningEffortMap"
|
||||
| "supportsReasoningParams"
|
||||
| "supportsSamplingParams"
|
||||
| "supportsPenaltyAndStopParams"
|
||||
| "thinkingFormat"
|
||||
| "kimiApiFormat"
|
||||
| "reasoningDisableMode"
|
||||
@@ -711,6 +720,12 @@ export interface ResolvedOpenAIResponsesCompat extends ResolvedOpenAISharedCompa
|
||||
strictResponsesPairing: boolean;
|
||||
supportsImageDetailOriginal: boolean;
|
||||
supportsObfuscationOptOut: boolean;
|
||||
/**
|
||||
* Whether `reasoning.summary` may be sent. First-party xAI `/v1/responses`
|
||||
* rejects the field; handlers pass `null` so the wire omits it instead of
|
||||
* filling `"auto"`.
|
||||
*/
|
||||
supportsReasoningSummary: boolean;
|
||||
streamIdleTimeoutMs?: number;
|
||||
vercelGatewayRouting?: OpenAICompat["vercelGatewayRouting"];
|
||||
/** The model sits behind Vercel AI Gateway's Responses endpoint. */
|
||||
|
||||
@@ -223,12 +223,15 @@ describe("buildModel", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("xAI-OAuth Responses reasoning-effort suppression", () => {
|
||||
const grokResponsesSpec = (id: string): ModelSpec<"openai-responses"> => ({
|
||||
describe("xAI Responses reasoning-effort suppression", () => {
|
||||
const grokResponsesSpec = (
|
||||
id: string,
|
||||
provider: "xai" | "xai-oauth" = "xai-oauth",
|
||||
): ModelSpec<"openai-responses"> => ({
|
||||
id,
|
||||
name: id,
|
||||
api: "openai-responses",
|
||||
provider: "xai-oauth",
|
||||
provider,
|
||||
baseUrl: "https://api.x.ai/v1",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
@@ -248,6 +251,47 @@ describe("xAI-OAuth Responses reasoning-effort suppression", () => {
|
||||
expect(buildOpenAIResponsesCompat(grokResponsesSpec("grok-4.3")).supportsReasoningEffort).toBe(true);
|
||||
});
|
||||
|
||||
it("applies the same Responses dialect to paid xai and xai-oauth", () => {
|
||||
const paid = buildOpenAIResponsesCompat(grokResponsesSpec("grok-4.3", "xai"));
|
||||
const oauth = buildOpenAIResponsesCompat(grokResponsesSpec("grok-4.3", "xai-oauth"));
|
||||
expect(paid.promptCacheSessionHeader).toBe("x-grok-conv-id");
|
||||
expect(oauth.promptCacheSessionHeader).toBe("x-grok-conv-id");
|
||||
expect(paid.includeEncryptedReasoning).toBe(true);
|
||||
expect(oauth.includeEncryptedReasoning).toBe(true);
|
||||
expect(paid.filterReasoningHistory).toBe(false);
|
||||
expect(oauth.filterReasoningHistory).toBe(false);
|
||||
expect(paid.supportsImageDetailOriginal).toBe(false);
|
||||
expect(oauth.supportsImageDetailOriginal).toBe(false);
|
||||
expect(paid.supportsReasoningEffort).toBe(true);
|
||||
expect(oauth.supportsReasoningEffort).toBe(true);
|
||||
expect(paid.reasoningEffortMap).toEqual({ minimal: "low", xhigh: "high", max: "high" });
|
||||
expect(oauth.reasoningEffortMap).toEqual({ minimal: "low", xhigh: "high", max: "high" });
|
||||
expect(
|
||||
buildOpenAIResponsesCompat(grokResponsesSpec("grok-4.20-multi-agent-0309", "xai")).reasoningEffortMap,
|
||||
).toEqual({ minimal: "low" });
|
||||
expect(paid.supportsPenaltyAndStopParams).toBe(false);
|
||||
expect(oauth.supportsPenaltyAndStopParams).toBe(false);
|
||||
expect(paid.supportsReasoningSummary).toBe(false);
|
||||
expect(oauth.supportsReasoningSummary).toBe(false);
|
||||
});
|
||||
|
||||
it("suppresses penalty params on every first-party xAI Responses model", () => {
|
||||
const reasoning = buildOpenAIResponsesCompat(grokResponsesSpec("grok-4.5", "xai"));
|
||||
const nonReasoning = buildOpenAIResponsesCompat({
|
||||
...grokResponsesSpec("grok-2", "xai"),
|
||||
reasoning: false,
|
||||
});
|
||||
expect(reasoning.supportsPenaltyAndStopParams).toBe(false);
|
||||
expect(nonReasoning.supportsPenaltyAndStopParams).toBe(false);
|
||||
});
|
||||
|
||||
it("omits effort for paid xai models off the Grok allowlist", () => {
|
||||
const compat = buildOpenAIResponsesCompat(grokResponsesSpec("grok-code-fast-1", "xai"));
|
||||
expect(compat.supportsReasoningEffort).toBe(false);
|
||||
expect(compat.omitReasoningEffort).toBe(true);
|
||||
expect(buildModel(grokResponsesSpec("grok-code-fast-1", "xai")).thinking).toBeUndefined();
|
||||
});
|
||||
|
||||
it("lets an explicit compat.supportsReasoningEffort override the allowlist default", () => {
|
||||
const compat = buildOpenAIResponsesCompat({
|
||||
...grokResponsesSpec("grok-build"),
|
||||
@@ -256,7 +300,7 @@ describe("xAI-OAuth Responses reasoning-effort suppression", () => {
|
||||
expect(compat.supportsReasoningEffort).toBe(true);
|
||||
});
|
||||
|
||||
it("does not suppress effort for a non-xai-oauth provider with a grok-like id", () => {
|
||||
it("does not suppress effort for a non-xAI provider with a grok-like id", () => {
|
||||
const compat = buildOpenAIResponsesCompat({
|
||||
...grokResponsesSpec("grok-build"),
|
||||
provider: "openai",
|
||||
|
||||
@@ -501,6 +501,43 @@ describe("generated model policies", () => {
|
||||
expect(models[2]?.applyPatchToolType).toBeUndefined();
|
||||
expect(models[3]?.applyPatchToolType).toBeUndefined();
|
||||
});
|
||||
|
||||
it("strips paid xAI Responses effort dials for off-allowlist reasoners", () => {
|
||||
const models: ModelSpec<"openai-responses">[] = [
|
||||
createSpec({
|
||||
id: "grok-code-fast-1",
|
||||
api: "openai-responses",
|
||||
provider: "xai",
|
||||
thinking: { mode: "effort", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] },
|
||||
}),
|
||||
createSpec({
|
||||
id: "grok-4.5",
|
||||
api: "openai-responses",
|
||||
provider: "xai",
|
||||
thinking: { mode: "effort", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] },
|
||||
}),
|
||||
createSpec({
|
||||
id: "grok-code-fast-1",
|
||||
api: "openai-responses",
|
||||
provider: "openrouter",
|
||||
thinking: { mode: "effort", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] },
|
||||
}),
|
||||
];
|
||||
|
||||
applyGeneratedModelPolicies(models);
|
||||
|
||||
expect(models[0]?.thinking).toBeUndefined();
|
||||
expect(models[0]?.compat).toMatchObject({
|
||||
supportsReasoningEffort: false,
|
||||
omitReasoningEffort: true,
|
||||
});
|
||||
expect(models[0]?.compat).not.toHaveProperty("reasoningEffortMap");
|
||||
expect(models[1]?.thinking?.efforts).toEqual([Effort.Minimal, Effort.Low, Effort.Medium, Effort.High]);
|
||||
expect(models[1]?.compat?.supportsReasoningEffort).toBe(true);
|
||||
// Non-xAI hosts are outside this policy — no baked no-dial compat.
|
||||
expect(models[2]?.thinking).toBeDefined();
|
||||
expect(models[2]?.compat?.supportsReasoningEffort).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyOllamaCloudOutputCap", () => {
|
||||
|
||||
@@ -5,7 +5,9 @@ import {
|
||||
isGeminiModelId,
|
||||
isGlmVisionModelId,
|
||||
isGrokModelId,
|
||||
isGrokMultiAgentModelId,
|
||||
isGrokReasoningEffortCapable,
|
||||
isGrokXHighEffortCapable,
|
||||
isKimiK26ModelId,
|
||||
isKimiModelId,
|
||||
isMinimaxM2FamilyModelId,
|
||||
@@ -339,6 +341,7 @@ describe("isGrokReasoningEffortCapable", () => {
|
||||
expect(isGrokReasoningEffortCapable("grok-4.20-multi-agent")).toBe(true);
|
||||
expect(isGrokReasoningEffortCapable("xai-oauth/grok-4.3")).toBe(true);
|
||||
expect(isGrokReasoningEffortCapable("xai-oauth/grok-4.5")).toBe(true);
|
||||
expect(isGrokReasoningEffortCapable("xai-oauth/grok-4.6")).toBe(true);
|
||||
expect(isGrokReasoningEffortCapable("openrouter/xai/grok-3-mini")).toBe(true);
|
||||
});
|
||||
|
||||
@@ -349,3 +352,35 @@ describe("isGrokReasoningEffortCapable", () => {
|
||||
expect(isGrokReasoningEffortCapable("")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isGrokMultiAgentModelId", () => {
|
||||
test("matches grok-4.20-multi-agent SKUs across namespaces", () => {
|
||||
expect(isGrokMultiAgentModelId("grok-4.20-multi-agent")).toBe(true);
|
||||
expect(isGrokMultiAgentModelId("grok-4.20-multi-agent-0309")).toBe(true);
|
||||
expect(isGrokMultiAgentModelId("xai/grok-4.20-multi-agent-beta-latest")).toBe(true);
|
||||
});
|
||||
|
||||
test("rejects other Grok ids", () => {
|
||||
expect(isGrokMultiAgentModelId("grok-4.5")).toBe(false);
|
||||
expect(isGrokMultiAgentModelId("grok-4.6")).toBe(false);
|
||||
expect(isGrokMultiAgentModelId("grok-4.20-0309-reasoning")).toBe(false);
|
||||
expect(isGrokMultiAgentModelId("")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isGrokXHighEffortCapable", () => {
|
||||
test("matches grok-4.6 and multi-agent SKUs across namespaces", () => {
|
||||
expect(isGrokXHighEffortCapable("grok-4.6")).toBe(true);
|
||||
expect(isGrokXHighEffortCapable("xai/grok-4.6")).toBe(true);
|
||||
expect(isGrokXHighEffortCapable("xai-oauth/grok-4.6")).toBe(true);
|
||||
expect(isGrokXHighEffortCapable("grok-4.20-multi-agent-0309")).toBe(true);
|
||||
});
|
||||
|
||||
test("rejects Grok SKUs that clamp leftover xhigh to high", () => {
|
||||
expect(isGrokXHighEffortCapable("grok-4.5")).toBe(false);
|
||||
expect(isGrokXHighEffortCapable("grok-4.3")).toBe(false);
|
||||
expect(isGrokXHighEffortCapable("grok-3-mini")).toBe(false);
|
||||
expect(isGrokXHighEffortCapable("grok-build")).toBe(false);
|
||||
expect(isGrokXHighEffortCapable("")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -884,6 +884,65 @@ describe("model thinking runtime helpers", () => {
|
||||
expect(() => requireSupportedEffort(opus46, Effort.XHigh)).toThrow(/not supported/);
|
||||
});
|
||||
|
||||
it("does not expose xhigh on first-party Grok 4.5 Responses models", () => {
|
||||
const paid = createModel({
|
||||
id: "grok-4.5",
|
||||
api: "openai-responses",
|
||||
provider: "xai",
|
||||
baseUrl: "https://api.x.ai/v1",
|
||||
});
|
||||
const oauth = createModel({
|
||||
id: "grok-4.5",
|
||||
api: "openai-responses",
|
||||
provider: "xai-oauth",
|
||||
baseUrl: "https://api.x.ai/v1",
|
||||
});
|
||||
|
||||
expect(paid.thinking?.efforts).toEqual([Effort.Minimal, Effort.Low, Effort.Medium, Effort.High]);
|
||||
expect(oauth.thinking?.efforts).toEqual([Effort.Minimal, Effort.Low, Effort.Medium, Effort.High]);
|
||||
expect(() => requireSupportedEffort(paid, Effort.XHigh)).toThrow(/not supported/);
|
||||
});
|
||||
|
||||
it("exposes xhigh on first-party Grok 4.6 Responses models", () => {
|
||||
const paid = createModel({
|
||||
id: "grok-4.6",
|
||||
api: "openai-responses",
|
||||
provider: "xai",
|
||||
baseUrl: "https://api.x.ai/v1",
|
||||
});
|
||||
const oauth = createModel({
|
||||
id: "grok-4.6",
|
||||
api: "openai-responses",
|
||||
provider: "xai-oauth",
|
||||
baseUrl: "https://api.x.ai/v1",
|
||||
});
|
||||
|
||||
expect(paid.thinking?.efforts).toEqual([Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh]);
|
||||
expect(oauth.thinking?.efforts).toEqual([Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh]);
|
||||
expect(requireSupportedEffort(paid, Effort.XHigh)).toBe(Effort.XHigh);
|
||||
expect(paid.compat.reasoningEffortMap?.xhigh).toBeUndefined();
|
||||
});
|
||||
|
||||
it("exposes xhigh on first-party Grok multi-agent Responses models", () => {
|
||||
const paid = createModel({
|
||||
id: "grok-4.20-multi-agent-beta-latest",
|
||||
api: "openai-responses",
|
||||
provider: "xai",
|
||||
baseUrl: "https://api.x.ai/v1",
|
||||
});
|
||||
const oauth = createModel({
|
||||
id: "grok-4.20-multi-agent-0309",
|
||||
api: "openai-responses",
|
||||
provider: "xai-oauth",
|
||||
baseUrl: "https://api.x.ai/v1",
|
||||
});
|
||||
|
||||
expect(paid.thinking?.efforts).toEqual([Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh]);
|
||||
expect(oauth.thinking?.efforts).toEqual([Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh]);
|
||||
expect(requireSupportedEffort(paid, Effort.XHigh)).toBe(Effort.XHigh);
|
||||
expect(paid.compat.reasoningEffortMap?.xhigh).toBeUndefined();
|
||||
});
|
||||
|
||||
it("rejects effort requests against un-built reasoning specs", () => {
|
||||
const spec = {
|
||||
id: "broken-reasoner",
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { resolveProviderModels } from "@oh-my-pi/pi-catalog/model-manager";
|
||||
import { getBundledModels } from "@oh-my-pi/pi-catalog/models";
|
||||
import { CATALOG_PROVIDERS } from "@oh-my-pi/pi-catalog/provider-models/descriptors";
|
||||
import { xaiModelManagerOptions } from "@oh-my-pi/pi-catalog/provider-models/openai-compat";
|
||||
import type { ModelSpec } from "@oh-my-pi/pi-catalog/types";
|
||||
|
||||
const XAI_RESPONSES_SPEC: ModelSpec<"openai-responses"> = {
|
||||
id: "grok-4.5",
|
||||
name: "Grok 4.5",
|
||||
api: "openai-responses",
|
||||
provider: "xai",
|
||||
baseUrl: "https://api.x.ai/v1",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 2, output: 6, cacheRead: 0.3, cacheWrite: 0 },
|
||||
contextWindow: 500_000,
|
||||
maxTokens: 500_000,
|
||||
};
|
||||
const XAI_COMPLETIONS_SPEC: ModelSpec<"openai-completions"> = {
|
||||
id: "grok-4.5",
|
||||
name: "Grok 4.5",
|
||||
api: "openai-completions",
|
||||
provider: "xai",
|
||||
baseUrl: "https://api.x.ai/v1",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 2, output: 6, cacheRead: 0.3, cacheWrite: 0 },
|
||||
contextWindow: 500_000,
|
||||
maxTokens: 500_000,
|
||||
};
|
||||
|
||||
describe("paid xai (XAI_API_KEY) Responses contract", () => {
|
||||
it("registers xai on the catalog Responses discovery path", () => {
|
||||
const entry = CATALOG_PROVIDERS.find(provider => provider.id === "xai");
|
||||
expect(entry, "xai catalog descriptor").toBeDefined();
|
||||
expect(entry!.defaultModel).toBe("grok-4.5");
|
||||
expect(entry!.envVars).toContain("XAI_API_KEY");
|
||||
const options = xaiModelManagerOptions({ apiKey: "test-key" });
|
||||
expect(options.providerId).toBe("xai");
|
||||
expect(options.fetchDynamicModels, "live /v1/models overlay").toBeTypeOf("function");
|
||||
expect(options.dropCachedModelIdsOnStaticMismatch).toEqual(getBundledModels("xai").map(model => model.id));
|
||||
expect(options.dropCachedModelIdsOnStaticMismatch).toContain("grok-4.5");
|
||||
});
|
||||
|
||||
it("bundles every paid xai chat model on openai-responses", () => {
|
||||
const models = getBundledModels("xai");
|
||||
expect(models.length).toBeGreaterThan(0);
|
||||
for (const model of models) {
|
||||
expect(model.api, `${model.provider}/${model.id}`).toBe("openai-responses");
|
||||
expect(model.baseUrl).toBe("https://api.x.ai/v1");
|
||||
}
|
||||
});
|
||||
|
||||
it("drops stale Chat Completions cache rows so Responses takes effect immediately", async () => {
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "pi-catalog-xai-completions-cache-"));
|
||||
const dbPath = path.join(tempDir, "models.db");
|
||||
try {
|
||||
await resolveProviderModels(
|
||||
{
|
||||
providerId: "xai",
|
||||
staticModels: [XAI_COMPLETIONS_SPEC],
|
||||
fetchDynamicModels: async () => [XAI_COMPLETIONS_SPEC],
|
||||
cacheDbPath: dbPath,
|
||||
},
|
||||
"online",
|
||||
);
|
||||
|
||||
let fetches = 0;
|
||||
const migrated = await resolveProviderModels(
|
||||
{
|
||||
...xaiModelManagerOptions(),
|
||||
staticModels: [XAI_RESPONSES_SPEC],
|
||||
cacheDbPath: dbPath,
|
||||
fetchDynamicModels: async () => {
|
||||
fetches += 1;
|
||||
return [XAI_RESPONSES_SPEC];
|
||||
},
|
||||
},
|
||||
"online-if-uncached",
|
||||
);
|
||||
|
||||
expect(fetches).toBe(1);
|
||||
expect(migrated.models.find(model => model.id === "grok-4.5")?.api).toBe("openai-responses");
|
||||
|
||||
const offline = await resolveProviderModels(
|
||||
{
|
||||
...xaiModelManagerOptions(),
|
||||
staticModels: [XAI_RESPONSES_SPEC],
|
||||
cacheDbPath: dbPath,
|
||||
fetchDynamicModels: async () => null,
|
||||
},
|
||||
"offline",
|
||||
);
|
||||
expect(offline.models.find(model => model.id === "grok-4.5")?.api).toBe("openai-responses");
|
||||
} finally {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import MODELS_JSON from "@oh-my-pi/pi-catalog/models.json" with { type: "json" };
|
||||
import { CATALOG_PROVIDERS, DEFAULT_MODEL_PER_PROVIDER } from "@oh-my-pi/pi-catalog/provider-models/descriptors";
|
||||
import { buildXaiOAuthStaticSeed } from "@oh-my-pi/pi-catalog/provider-models/openai-compat";
|
||||
import type { ModelSpec } from "@oh-my-pi/pi-catalog/types";
|
||||
|
||||
// Pins the invariant: bundled `models.json` carries every entry the runtime
|
||||
// curated catalog (XAI_OAUTH_CURATED_MODELS, surfaced via
|
||||
// buildXaiOAuthStaticSeed) emits. Without this, editing the curated list
|
||||
// without regenerating `models.json` silently regresses the boot-time
|
||||
// default-model resolver — the registry sees the runtime seed only after
|
||||
// `refresh()`, but interactive boot resolves the persisted default
|
||||
// synchronously from `#loadModels()`, which reads only `models.json`.
|
||||
//
|
||||
// Failure here means: run `bun run gen:models` and commit the diff.
|
||||
describe("xai-oauth bundled catalog (regression)", () => {
|
||||
const bundled =
|
||||
(MODELS_JSON as unknown as Record<string, Record<string, ModelSpec<"openai-responses">>>)["xai-oauth"] ?? {};
|
||||
const seed = buildXaiOAuthStaticSeed();
|
||||
|
||||
it("defaults SuperGrok selection to grok-4.5", () => {
|
||||
const entry = CATALOG_PROVIDERS.find(provider => provider.id === "xai-oauth");
|
||||
expect(entry?.defaultModel).toBe("grok-4.5");
|
||||
expect(DEFAULT_MODEL_PER_PROVIDER["xai-oauth"]).toBe("grok-4.5");
|
||||
expect(bundled["grok-4.5"], "xai-oauth/grok-4.5 must be bundled for the default").toBeDefined();
|
||||
});
|
||||
|
||||
it("bundles every curated id", () => {
|
||||
const seededIds = seed.map(model => model.id).sort();
|
||||
const bundledIds = Object.keys(bundled).sort();
|
||||
expect(bundledIds).toEqual(seededIds);
|
||||
});
|
||||
|
||||
for (const seededModel of seed) {
|
||||
it(`matches contract for ${seededModel.id}`, () => {
|
||||
const bundledEntry = bundled[seededModel.id];
|
||||
expect(bundledEntry, `xai-oauth/${seededModel.id} missing from models.json`).toBeDefined();
|
||||
expect(bundledEntry.id).toBe(seededModel.id);
|
||||
expect(bundledEntry.name).toBe(seededModel.name);
|
||||
expect(bundledEntry.provider).toBe("xai-oauth");
|
||||
expect(bundledEntry.api).toBe("openai-responses");
|
||||
expect(bundledEntry.contextWindow).toBe(seededModel.contextWindow);
|
||||
expect(bundledEntry.reasoning).toBe(seededModel.reasoning);
|
||||
// Input modality must survive both the curated seed and the bundle.
|
||||
// Without this the static fallback used on offline boot strips
|
||||
// vision capability silently (Codex PR #1127 review).
|
||||
expect(bundledEntry.input).toEqual(seededModel.input);
|
||||
expect(bundledEntry.compat?.supportsReasoningEffort).toBe(seededModel.compat?.supportsReasoningEffort);
|
||||
});
|
||||
}
|
||||
|
||||
// Absolute contract for the user-specified SuperGrok addition. The parity
|
||||
// loop above can't catch a value typo (e.g. 2_000_000) or a flipped
|
||||
// reasoning flag — both sides regenerate from the same seed together — so
|
||||
// pin the literal attributes here.
|
||||
it("exposes grok-composer-2.5-fast as a non-reasoning 200K text model", () => {
|
||||
const composer = seed.find(model => model.id === "grok-composer-2.5-fast");
|
||||
expect(composer, "grok-composer-2.5-fast must be in the SuperGrok curated seed").toBeDefined();
|
||||
expect(composer!.reasoning).toBe(false);
|
||||
expect(composer!.contextWindow).toBe(200_000);
|
||||
expect(composer!.input).toEqual(["text"]);
|
||||
// The bundled models.json entry is byte-identical to the generator's
|
||||
// deterministic xai-oauth output: gen:models pushes
|
||||
// buildXaiOAuthStaticSeed() (offline — xai-oauth has no upstream catalog
|
||||
// source) and applyGeneratedModelPolicies(), so a regen reproduces these
|
||||
// exact bytes; only unrelated other-provider network churn was excluded
|
||||
// to keep the diff scoped. Pin its zero-cost invariant (overlay-stable
|
||||
// for the SuperGrok subscription), which the parity loop above never
|
||||
// compares. (maxTokens is pinned by the maxTokens-equals-contextWindow
|
||||
// test below.)
|
||||
expect(bundled["grok-composer-2.5-fast"]?.cost).toEqual({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
|
||||
});
|
||||
|
||||
// The OAuth surface's /v1/models reports no per-request output limit, so the
|
||||
// curated catalog owns maxTokens — set to mirror each model's contextWindow
|
||||
// (the openai-responses wire still clamps the actual request to
|
||||
// OPENAI_MAX_OUTPUT_TOKENS). Pin maxTokens === contextWindow on both the
|
||||
// static-seed and bundled paths so a null placeholder can
|
||||
// never silently leak back into the bundle.
|
||||
it("sets maxTokens equal to contextWindow for every xai-oauth model", () => {
|
||||
for (const model of seed) {
|
||||
expect(model.maxTokens, `seed ${model.id} maxTokens`).toBe(model.contextWindow);
|
||||
expect(bundled[model.id]?.maxTokens, `bundled ${model.id} maxTokens`).toBe(model.contextWindow);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,172 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { Effort } from "@oh-my-pi/pi-catalog/effort";
|
||||
import MODELS_JSON from "@oh-my-pi/pi-catalog/models.json" with { type: "json" };
|
||||
import {
|
||||
MODELS_DEV_PROVIDER_DESCRIPTORS,
|
||||
mapModelsDevToModels,
|
||||
} from "@oh-my-pi/pi-catalog/provider-models/openai-compat";
|
||||
import type { ModelSpec } from "@oh-my-pi/pi-catalog/types";
|
||||
import { applyGeneratedModelPolicies } from "../scripts/generated-policies";
|
||||
|
||||
const XAI_MODELS_DEV_FIXTURE = {
|
||||
xai: {
|
||||
models: {
|
||||
"grok-4.5": {
|
||||
name: "Grok 4.5",
|
||||
tool_call: true,
|
||||
reasoning: true,
|
||||
modalities: { input: ["text", "image"] },
|
||||
limit: { context: 500_000, output: 500_000 },
|
||||
cost: { input: 2, output: 6, cache_read: 0.3 },
|
||||
},
|
||||
"grok-4.6": {
|
||||
name: "Grok 4.6",
|
||||
tool_call: true,
|
||||
reasoning: true,
|
||||
modalities: { input: ["text", "image"] },
|
||||
limit: { context: 500_000, output: 500_000 },
|
||||
cost: { input: 2, output: 6, cache_read: 0.5 },
|
||||
},
|
||||
"grok-code-fast-1": {
|
||||
name: "Grok Code Fast 1",
|
||||
tool_call: true,
|
||||
reasoning: true,
|
||||
modalities: { input: ["text"] },
|
||||
limit: { context: 256_000, output: 10_000 },
|
||||
cost: { input: 0.2, output: 1.5 },
|
||||
},
|
||||
"grok-build-0.1": {
|
||||
name: "Grok Build 0.1",
|
||||
tool_call: true,
|
||||
reasoning: true,
|
||||
modalities: { input: ["text", "image"] },
|
||||
limit: { context: 256_000, output: 256_000 },
|
||||
cost: { input: 0, output: 0 },
|
||||
},
|
||||
"grok-4.20-0309-reasoning": {
|
||||
name: "Grok 4.20 (Reasoning)",
|
||||
tool_call: true,
|
||||
reasoning: true,
|
||||
modalities: { input: ["text", "image"] },
|
||||
limit: { context: 2_000_000, output: 64_000 },
|
||||
cost: { input: 2, output: 6 },
|
||||
},
|
||||
"grok-2": {
|
||||
name: "Grok 2",
|
||||
tool_call: true,
|
||||
reasoning: false,
|
||||
modalities: { input: ["text"] },
|
||||
limit: { context: 131_072, output: 8192 },
|
||||
cost: { input: 2, output: 10 },
|
||||
},
|
||||
"grok-4.20-multi-agent-beta-latest": {
|
||||
name: "Grok 4.20 (Multi-Agent)",
|
||||
tool_call: true,
|
||||
reasoning: true,
|
||||
modalities: { input: ["text"] },
|
||||
limit: { context: 2_000_000, output: 64_000 },
|
||||
cost: { input: 2, output: 6 },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe("paid xAI Responses thinking policy", () => {
|
||||
it("bakes the effort-dial allowlist on stencil.so → openai-responses mapping", () => {
|
||||
const mapped = mapModelsDevToModels(XAI_MODELS_DEV_FIXTURE, MODELS_DEV_PROVIDER_DESCRIPTORS).filter(
|
||||
model => model.provider === "xai",
|
||||
);
|
||||
const byId = Object.fromEntries(mapped.map(model => [model.id, model]));
|
||||
|
||||
expect(byId["grok-4.5"]?.api).toBe("openai-responses");
|
||||
expect(byId["grok-4.5"]?.compat).toMatchObject({
|
||||
supportsReasoningEffort: true,
|
||||
omitReasoningEffort: false,
|
||||
reasoningEffortMap: { minimal: "low" },
|
||||
});
|
||||
for (const id of ["grok-code-fast-1", "grok-build-0.1", "grok-4.20-0309-reasoning"] as const) {
|
||||
expect(byId[id]?.reasoning, id).toBe(true);
|
||||
expect(byId[id]?.compat, id).toMatchObject({
|
||||
supportsReasoningEffort: false,
|
||||
omitReasoningEffort: true,
|
||||
});
|
||||
expect(byId[id]?.compat, id).not.toHaveProperty("reasoningEffortMap");
|
||||
}
|
||||
expect(byId["grok-2"]?.compat).toMatchObject({
|
||||
supportsReasoningEffort: false,
|
||||
omitReasoningEffort: true,
|
||||
});
|
||||
expect(byId["grok-2"]?.compat).not.toHaveProperty("reasoningEffortMap");
|
||||
});
|
||||
|
||||
it("strips stale thinking dials from off-allowlist paid xAI reasoners during generation", () => {
|
||||
const mapped = mapModelsDevToModels(XAI_MODELS_DEV_FIXTURE, MODELS_DEV_PROVIDER_DESCRIPTORS).filter(
|
||||
model => model.provider === "xai",
|
||||
);
|
||||
// Snapshot-era Completions rows still carry a default effort ladder after the
|
||||
// api flip; the generator must not re-emit that dial for Responses.
|
||||
const snapshotStale = mapped.find(model => model.id === "grok-code-fast-1");
|
||||
expect(snapshotStale).toBeDefined();
|
||||
snapshotStale!.thinking = { mode: "effort", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] };
|
||||
|
||||
applyGeneratedModelPolicies(mapped);
|
||||
const byId = Object.fromEntries(mapped.map(model => [model.id, model]));
|
||||
|
||||
expect(byId["grok-4.5"]?.thinking).toEqual({
|
||||
mode: "effort",
|
||||
efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High],
|
||||
effortMap: { minimal: "low" },
|
||||
});
|
||||
expect(byId["grok-4.6"]?.thinking).toEqual({
|
||||
mode: "effort",
|
||||
efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh],
|
||||
effortMap: { minimal: "low" },
|
||||
});
|
||||
expect(byId["grok-4.6"]?.compat).toMatchObject({
|
||||
supportsReasoningEffort: true,
|
||||
reasoningEffortMap: { minimal: "low" },
|
||||
});
|
||||
expect(byId["grok-4.6"]?.compat).not.toMatchObject({
|
||||
reasoningEffortMap: { xhigh: "high" },
|
||||
});
|
||||
expect(byId["grok-4.20-multi-agent-beta-latest"]?.thinking).toEqual({
|
||||
mode: "effort",
|
||||
efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh],
|
||||
effortMap: { minimal: "low" },
|
||||
});
|
||||
expect(byId["grok-4.20-multi-agent-beta-latest"]?.compat).toMatchObject({
|
||||
supportsReasoningEffort: true,
|
||||
reasoningEffortMap: { minimal: "low" },
|
||||
});
|
||||
expect(byId["grok-4.20-multi-agent-beta-latest"]?.compat).not.toMatchObject({
|
||||
reasoningEffortMap: { xhigh: "high" },
|
||||
});
|
||||
for (const id of ["grok-code-fast-1", "grok-build-0.1", "grok-4.20-0309-reasoning"] as const) {
|
||||
expect(byId[id]?.reasoning, id).toBe(true);
|
||||
expect(byId[id]?.thinking, id).toBeUndefined();
|
||||
expect(byId[id]?.compat, id).toMatchObject({ supportsReasoningEffort: false });
|
||||
}
|
||||
});
|
||||
|
||||
it("exports no-dial rows in the bundled models.json snapshot", () => {
|
||||
const bundled =
|
||||
(MODELS_JSON as unknown as Record<string, Record<string, ModelSpec<"openai-responses">>>).xai ?? {};
|
||||
for (const id of ["grok-code-fast-1", "grok-build-0.1", "grok-4.20-0309-reasoning"] as const) {
|
||||
expect(bundled[id], `xai/${id} missing from models.json`).toBeDefined();
|
||||
expect(bundled[id]?.reasoning, id).toBe(true);
|
||||
expect(bundled[id]?.thinking, id).toBeUndefined();
|
||||
expect(bundled[id]?.compat?.supportsReasoningEffort, id).toBe(false);
|
||||
}
|
||||
expect(bundled["grok-4.5"]?.thinking?.efforts).toEqual([Effort.Minimal, Effort.Low, Effort.Medium, Effort.High]);
|
||||
expect(bundled["grok-4.5"]?.thinking?.efforts).not.toContain(Effort.XHigh);
|
||||
expect(bundled["grok-4.5"]?.compat?.supportsReasoningEffort).toBe(true);
|
||||
expect(bundled["grok-4.6"]?.thinking?.efforts).toContain(Effort.XHigh);
|
||||
expect(bundled["grok-4.6"]?.compat).not.toMatchObject({
|
||||
reasoningEffortMap: { xhigh: "high" },
|
||||
});
|
||||
expect(bundled["grok-4.20-multi-agent-beta-latest"]?.thinking?.efforts).toContain(Effort.XHigh);
|
||||
expect(bundled["grok-4.20-multi-agent-beta-latest"]?.compat).not.toMatchObject({
|
||||
reasoningEffortMap: { xhigh: "high" },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,16 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- Routed paid xAI models (`XAI_API_KEY` / `xai/…`) through the Responses API used by SuperGrok OAuth instead of Chat Completions.
|
||||
- Changed the default model for `XAI_API_KEY` (`xai`) from `grok-4-fast-non-reasoning` to `grok-4.5`.
|
||||
- Changed the default model for SuperGrok OAuth (`xai-oauth`) from `grok-4.3` to `grok-4.5`.
|
||||
- Included `reasoning.encrypted_content` in Responses `include` for paid xAI and SuperGrok OAuth models.
|
||||
- Replayed encrypted xAI reasoning on follow-up Responses turns for `xai` and `xai-oauth`.
|
||||
- Kept automatic model selection on paid `xai/grok-4.5` when only `XAI_API_KEY` is set, instead of preferring SuperGrok `xai-oauth/grok-4.5`. Explicit `xai-oauth/grok-4.5` still works with that paid key.
|
||||
- Stopped sending presence/frequency penalties and stop sequences to xAI reasoning models such as `grok-4.5`, which reject them.
|
||||
|
||||
## [17.3.4] - 2026-08-14
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -1667,17 +1667,22 @@ export class ModelRegistry {
|
||||
*
|
||||
* Side-effect-free and synchronous: a command-backed key (`!cmd`) counts as
|
||||
* configured by its presence alone — the program is NOT executed — and OAuth
|
||||
* tokens are NOT refreshed (`authStorage.hasAuth`). This is what keeps the
|
||||
* tokens are NOT refreshed (`authStorage.hasResolvableAuth`). This is what keeps the
|
||||
* model-switch pre-flight off the event loop's hot path; the real key
|
||||
* (command execution + OAuth refresh) is resolved lazily per request via
|
||||
* {@link ModelRegistry.resolver}.
|
||||
*
|
||||
* Cross-provider env aliases count here (`xai-oauth` can borrow `XAI_API_KEY`)
|
||||
* so an explicit `xai-oauth/…` selector does not fail with "No API key".
|
||||
* Default-model availability still uses {@link AuthStorage.hasAuth}, which
|
||||
* ignores that alias so SuperGrok is not auto-selected from a paid key.
|
||||
*/
|
||||
hasConfiguredAuth(model: Model<Api>): boolean {
|
||||
const keyConfig = this.#customProviderApiKeys.get(model.provider);
|
||||
return (
|
||||
isCommandConfigValue(keyConfig) ||
|
||||
this.#keylessProviders.has(model.provider) ||
|
||||
this.authStorage.hasAuth(model.provider)
|
||||
this.authStorage.hasResolvableAuth(model.provider)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -437,6 +437,36 @@ describe("pickDefaultAvailableModel", () => {
|
||||
expect(result?.provider).toBe("zhipu-coding-plan");
|
||||
expect(result?.id).toBe("glm-5.1");
|
||||
});
|
||||
|
||||
test("prefers SuperGrok over paid xAI when both defaults are present", () => {
|
||||
const paid = buildModel({
|
||||
id: "grok-4.5",
|
||||
name: "Grok 4.5",
|
||||
api: "openai-responses",
|
||||
provider: "xai",
|
||||
baseUrl: "https://api.x.ai/v1",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 2, output: 6, cacheRead: 0.3, cacheWrite: 0 },
|
||||
contextWindow: 500000,
|
||||
maxTokens: 500000,
|
||||
});
|
||||
const oauth = buildModel({
|
||||
id: "grok-4.5",
|
||||
name: "Grok 4.5",
|
||||
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: 500000,
|
||||
maxTokens: 500000,
|
||||
});
|
||||
|
||||
expect(pickDefaultAvailableModel([paid, oauth])?.provider).toBe("xai-oauth");
|
||||
expect(pickDefaultAvailableModel([paid])?.provider).toBe("xai");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseModelPattern", () => {
|
||||
|
||||
Reference in New Issue
Block a user