fix(xai): restore config-backed auth discovery

This commit is contained in:
Peter Steinberger 2026-03-28 19:21:54 +00:00
parent 2a950157b1
commit 1e424990a2
8 changed files with 695 additions and 92 deletions

View File

@ -39,6 +39,13 @@ describe("xai stream wrappers", () => {
}),
).toBe("grok-3-fast");
expect(captureWrappedModelId({ modelId: "grok-4", fastMode: true })).toBe("grok-4-fast");
expect(
captureWrappedModelId({
modelId: "grok-3",
fastMode: true,
api: "openai-responses",
}),
).toBe("grok-3-fast");
});
it("leaves unsupported or disabled models unchanged", () => {
@ -83,4 +90,31 @@ describe("xai stream wrappers", () => {
expect(payload).not.toHaveProperty("reasoning_effort");
expect(payload.tools[0]?.function).not.toHaveProperty("strict");
});
it("strips unsupported reasoning controls from xai payloads", () => {
const payload: Record<string, unknown> = {
reasoning: { effort: "high" },
reasoningEffort: "high",
reasoning_effort: "high",
};
const baseStreamFn: StreamFn = (_model, _context, options) => {
options?.onPayload?.(payload, {} as Model<"openai-responses">);
return {} as ReturnType<StreamFn>;
};
const wrapped = createXaiToolPayloadCompatibilityWrapper(baseStreamFn);
void wrapped(
{
api: "openai-responses",
provider: "xai",
id: "grok-4-fast",
} as Model<"openai-responses">,
{ messages: [] } as Context,
{},
);
expect(payload).not.toHaveProperty("reasoning");
expect(payload).not.toHaveProperty("reasoningEffort");
expect(payload).not.toHaveProperty("reasoning_effort");
});
});

View File

@ -111,6 +111,20 @@ afterEach(() => {
clearRuntimeConfigSnapshot();
});
async function withoutEnv<T>(key: string, fn: () => Promise<T>): Promise<T> {
const previous = process.env[key];
delete process.env[key];
try {
return await fn();
} finally {
if (previous === undefined) {
delete process.env[key];
} else {
process.env[key] = previous;
}
}
}
function createCustomProviderConfig(
baseUrl: string,
modelId = "llama3",
@ -386,23 +400,25 @@ describe("resolveUsableCustomProviderApiKey", () => {
describe("resolveApiKeyForProvider", () => {
it("reuses the xai plugin web search key without models.providers.xai", async () => {
const resolved = await resolveApiKeyForProvider({
provider: "xai",
cfg: {
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: "xai-plugin-fallback-key", // pragma: allowlist secret
const resolved = await withoutEnv("XAI_API_KEY", () =>
resolveApiKeyForProvider({
provider: "xai",
cfg: {
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: "xai-plugin-fallback-key", // pragma: allowlist secret
},
},
},
},
},
},
},
store: { version: 1, profiles: {} },
});
store: { version: 1, profiles: {} },
}),
);
expect(resolved).toMatchObject({
apiKey: "xai-plugin-fallback-key",
@ -440,11 +456,13 @@ describe("resolveApiKeyForProvider", () => {
};
setRuntimeConfigSnapshot(runtimeConfig, sourceConfig);
const resolved = await resolveApiKeyForProvider({
provider: "xai",
cfg: sourceConfig,
store: { version: 1, profiles: {} },
});
const resolved = await withoutEnv("XAI_API_KEY", () =>
resolveApiKeyForProvider({
provider: "xai",
cfg: sourceConfig,
store: { version: 1, profiles: {} },
}),
);
expect(resolved).toMatchObject({
apiKey: "xai-runtime-key",
@ -455,24 +473,26 @@ describe("resolveApiKeyForProvider", () => {
it("does not reuse xai fallback auth when the xai plugin is disabled", async () => {
await expect(
resolveApiKeyForProvider({
provider: "xai",
cfg: {
plugins: {
entries: {
xai: {
enabled: false,
config: {
webSearch: {
apiKey: "xai-plugin-fallback-key", // pragma: allowlist secret
withoutEnv("XAI_API_KEY", () =>
resolveApiKeyForProvider({
provider: "xai",
cfg: {
plugins: {
entries: {
xai: {
enabled: false,
config: {
webSearch: {
apiKey: "xai-plugin-fallback-key", // pragma: allowlist secret
},
},
},
},
},
},
},
store: { version: 1, profiles: {} },
}),
store: { version: 1, profiles: {} },
}),
),
).rejects.toThrow('No API key found for provider "xai"');
});
});

View File

@ -6,7 +6,6 @@ import type { ModelProviderAuthMode, ModelProviderConfig } from "../config/types
import { coerceSecretRef } from "../config/types.secrets.js";
import { getShellEnvAppliedKeys } from "../infra/shell-env.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { formatApiKeyPreview } from "../plugins/provider-auth-input.js";
import {
buildProviderMissingAuthMessageWithPlugin,
resolveProviderSyntheticAuthWithPlugin,
@ -34,6 +33,7 @@ import {
type ResolvedProviderAuth,
} from "./model-auth-runtime-shared.js";
import { normalizeProviderId } from "./model-selection.js";
import { shouldTraceProviderAuth, summarizeProviderAuthKey } from "./xai-auth-trace.js";
export { ensureAuthProfileStore, resolveAuthProfileOrder } from "./auth-profiles.js";
export { requireApiKey, resolveAwsSdkEnvVarName } from "./model-auth-runtime-shared.js";
@ -41,21 +41,6 @@ export type { ResolvedProviderAuth } from "./model-auth-runtime-shared.js";
const log = createSubsystemLogger("model-auth");
function shouldTraceProviderAuth(provider: string): boolean {
return normalizeProviderId(provider) === "xai";
}
function summarizeProviderAuthKey(apiKey: string | undefined): string {
const trimmed = apiKey?.trim() ?? "";
if (!trimmed) {
return "missing";
}
if (isNonSecretApiKeyMarker(trimmed)) {
return `marker:${trimmed}`;
}
return formatApiKeyPreview(trimmed);
}
function logProviderAuthDecision(params: {
provider: string;
stage: string;

View File

@ -1,7 +1,7 @@
import type { OpenClawConfig } from "../config/config.js";
import { coerceSecretRef, resolveSecretInputRef } from "../config/types.secrets.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { formatApiKeyPreview } from "../plugins/provider-auth-input.js";
import { resolveProviderWebSearchPluginConfig } from "../plugin-sdk/provider-web-search.js";
import { resolveProviderSyntheticAuthWithPlugin } from "../plugins/provider-runtime.js";
import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js";
import { listProfilesForProvider } from "./auth-profiles/profiles.js";
@ -14,6 +14,7 @@ import {
resolveNonEnvSecretRefHeaderValueMarker,
} from "./model-auth-markers.js";
import { resolveAwsSdkEnvVarName } from "./model-auth-runtime-shared.js";
import { shouldTraceProviderAuth, summarizeProviderAuthKey } from "./xai-auth-trace.js";
type ModelsConfig = NonNullable<OpenClawConfig["models"]>;
export type ProviderConfig = NonNullable<ModelsConfig["providers"]>[string];
@ -49,21 +50,6 @@ export type ProviderAuthResolver = (
const ENV_VAR_NAME_RE = /^[A-Z_][A-Z0-9_]*$/;
const log = createSubsystemLogger("agents/model-providers");
function shouldTraceProviderAuth(provider: string): boolean {
return provider.trim().toLowerCase() === "xai";
}
function summarizeProviderAuthKey(apiKey: string | undefined): string {
const trimmed = apiKey?.trim() ?? "";
if (!trimmed) {
return "missing";
}
if (isNonSecretApiKeyMarker(trimmed)) {
return `marker:${trimmed}`;
}
return formatApiKeyPreview(trimmed);
}
export function normalizeApiKeyConfig(value: string): string {
const trimmed = value.trim();
const match = /^\$\{([A-Z0-9_]+)\}$/.exec(trimmed);
@ -438,15 +424,19 @@ function resolveConfigBackedProviderAuth(params: { provider: string; config?: Op
source: "config";
}
| undefined {
const synthetic = resolveProviderSyntheticAuthWithPlugin({
provider: params.provider,
config: params.config,
context: {
config: params.config,
// Providers own any provider-specific fallback auth logic via
// resolveSyntheticAuth(...). Discovery/bootstrap callers may consume
// non-secret markers from source config, but must never persist plaintext.
const synthetic =
resolveProviderSyntheticAuthWithPlugin({
provider: params.provider,
providerConfig: params.config?.models?.providers?.[params.provider],
},
});
config: params.config,
context: {
config: params.config,
provider: params.provider,
providerConfig: params.config?.models?.providers?.[params.provider],
},
}) ?? resolveXaiConfigFallbackAuth(params);
const apiKey = synthetic?.apiKey?.trim();
if (!apiKey) {
if (shouldTraceProviderAuth(params.provider)) {
@ -473,3 +463,68 @@ function resolveConfigBackedProviderAuth(params: { provider: string; config?: Op
source: "config",
};
}
function resolveXaiConfigFallbackAuth(params: { provider: string; config?: OpenClawConfig }):
| {
apiKey: string;
source: string;
mode: "api-key";
}
| undefined {
if (params.provider.trim().toLowerCase() !== "xai") {
return undefined;
}
const xaiPluginEntry = params.config?.plugins?.entries?.xai;
if (xaiPluginEntry?.enabled === false) {
return undefined;
}
const pluginApiKey = normalizeOptionalSecretInput(
resolveProviderWebSearchPluginConfig(
params.config as Record<string, unknown> | undefined,
"xai",
)?.apiKey,
);
if (pluginApiKey) {
return {
apiKey: pluginApiKey,
source: "plugins.entries.xai.config.webSearch.apiKey",
mode: "api-key",
};
}
const pluginApiKeyRef = coerceSecretRef(
resolveProviderWebSearchPluginConfig(
params.config as Record<string, unknown> | undefined,
"xai",
)?.apiKey,
);
if (pluginApiKeyRef) {
return {
apiKey:
pluginApiKeyRef.source === "env"
? pluginApiKeyRef.id.trim()
: resolveNonEnvSecretRefApiKeyMarker(pluginApiKeyRef.source),
source: "plugins.entries.xai.config.webSearch.apiKey",
mode: "api-key",
};
}
const grokApiKey = normalizeOptionalSecretInput(params.config?.tools?.web?.search?.grok?.apiKey);
if (grokApiKey) {
return {
apiKey: grokApiKey,
source: "tools.web.search.grok.apiKey",
mode: "api-key",
};
}
const grokApiKeyRef = coerceSecretRef(params.config?.tools?.web?.search?.grok?.apiKey);
if (!grokApiKeyRef) {
return undefined;
}
return {
apiKey:
grokApiKeyRef.source === "env"
? grokApiKeyRef.id.trim()
: resolveNonEnvSecretRefApiKeyMarker(grokApiKeyRef.source),
source: "tools.web.search.grok.apiKey",
mode: "api-key",
};
}

View File

@ -97,6 +97,7 @@ import { buildSystemPromptReport } from "../../system-prompt-report.js";
import { sanitizeToolCallIdsForCloudCodeAssist } from "../../tool-call-id.js";
import { resolveTranscriptPolicy } from "../../transcript-policy.js";
import { DEFAULT_BOOTSTRAP_FILENAME } from "../../workspace.js";
import { shouldTraceProviderAuth, summarizeProviderAuthKey } from "../../xai-auth-trace.js";
import { isRunnerAbortError } from "../abort.js";
import { isCacheTtlEligibleProvider } from "../cache-ttl.js";
import { resolveCompactionTimeoutMs } from "../compaction-safety-timeout.js";
@ -214,18 +215,6 @@ export {
const MAX_BTW_SNAPSHOT_MESSAGES = 100;
function shouldTraceProviderAuth(provider: string): boolean {
return provider.trim().toLowerCase() === "xai";
}
function summarizeProviderAuthKey(apiKey: string | undefined): string {
const trimmed = apiKey?.trim() ?? "";
if (!trimmed) {
return "missing";
}
return `${trimmed.slice(0, 4)}${trimmed.slice(-4)}`;
}
export function resolveEmbeddedAgentStreamFn(params: {
currentStreamFn: StreamFn | undefined;
providerStreamFn?: StreamFn;

View File

@ -1,6 +1,5 @@
import type { Api, Model } from "@mariozechner/pi-ai";
import type { ThinkLevel } from "../../../auto-reply/thinking.js";
import { formatApiKeyPreview } from "../../../plugins/provider-auth-input.js";
import { prepareProviderRuntimeAuth } from "../../../plugins/provider-runtime.js";
import {
type AuthProfileStore,
@ -16,6 +15,7 @@ import {
type FailoverReason,
} from "../../pi-embedded-helpers.js";
import { clampRuntimeAuthRefreshDelayMs } from "../../runtime-auth-refresh.js";
import { shouldTraceProviderAuth, summarizeProviderAuthKey } from "../../xai-auth-trace.js";
import { describeUnknownError } from "../utils.js";
import {
RUNTIME_AUTH_REFRESH_MARGIN_MS,
@ -37,15 +37,6 @@ type LogLike = {
warn(message: string): void;
};
function shouldTraceProviderAuth(provider: string): boolean {
return provider.trim().toLowerCase() === "xai";
}
function summarizeProviderAuthKey(apiKey: string | undefined): string {
const trimmed = apiKey?.trim() ?? "";
return trimmed ? formatApiKeyPreview(trimmed) : "missing";
}
export function createEmbeddedRunAuthController(params: {
config: RunEmbeddedPiAgentParams["config"];
agentDir: string;

View File

@ -0,0 +1,35 @@
import { isNonSecretApiKeyMarker } from "./model-auth-markers.js";
const DEFAULT_KEY_PREVIEW = { head: 4, tail: 4 };
function formatApiKeyPreview(raw: string): string {
const trimmed = raw.trim();
if (!trimmed) {
return "…";
}
const { head, tail } = DEFAULT_KEY_PREVIEW;
if (trimmed.length <= head + tail) {
const shortHead = Math.min(2, trimmed.length);
const shortTail = Math.min(2, trimmed.length - shortHead);
if (shortTail <= 0) {
return `${trimmed.slice(0, shortHead)}`;
}
return `${trimmed.slice(0, shortHead)}${trimmed.slice(-shortTail)}`;
}
return `${trimmed.slice(0, head)}${trimmed.slice(-tail)}`;
}
export function shouldTraceProviderAuth(provider: string): boolean {
return provider.trim().toLowerCase() === "xai";
}
export function summarizeProviderAuthKey(apiKey: string | undefined): string {
const trimmed = apiKey?.trim() ?? "";
if (!trimmed) {
return "missing";
}
if (isNonSecretApiKeyMarker(trimmed)) {
return `marker:${trimmed}`;
}
return formatApiKeyPreview(trimmed);
}

View File

@ -3,6 +3,57 @@ import os from "node:os";
import path from "node:path";
import type { OAuthCredentials } from "@mariozechner/pi-ai";
import { afterEach, describe, expect, it } from "vitest";
import { applyLitellmProviderConfig } from "../../extensions/litellm/onboard.js";
import {
applyMinimaxApiConfig,
applyMinimaxApiProviderConfig,
} from "../../extensions/minimax/onboard.js";
import { buildMistralModelDefinition as buildBundledMistralModelDefinition } from "../../extensions/mistral/model-definitions.js";
import {
applyMistralConfig,
applyMistralProviderConfig,
MISTRAL_DEFAULT_MODEL_REF,
} from "../../extensions/mistral/onboard.js";
import {
applyOpencodeGoConfig,
applyOpencodeGoProviderConfig,
} from "../../extensions/opencode-go/onboard.js";
import {
applyOpencodeZenConfig,
applyOpencodeZenProviderConfig,
} from "../../extensions/opencode/onboard.js";
import {
applyOpenrouterConfig,
applyOpenrouterProviderConfig,
OPENROUTER_DEFAULT_MODEL_REF,
} from "../../extensions/openrouter/onboard.js";
import {
applySyntheticConfig,
applySyntheticProviderConfig,
SYNTHETIC_DEFAULT_MODEL_REF,
} from "../../extensions/synthetic/onboard.js";
import {
applyXaiConfig,
applyXaiProviderConfig,
XAI_DEFAULT_MODEL_REF,
} from "../../extensions/xai/onboard.js";
import { applyXiaomiConfig, applyXiaomiProviderConfig } from "../../extensions/xiaomi/onboard.js";
import {
ZAI_CODING_CN_BASE_URL,
ZAI_GLOBAL_BASE_URL,
} from "../../extensions/zai/model-definitions.js";
import { applyZaiConfig, applyZaiProviderConfig } from "../../extensions/zai/onboard.js";
import {
createConfigWithFallbacks,
createLegacyProviderConfig,
EXPECTED_FALLBACKS,
} from "../../test/helpers/extensions/onboard-config.js";
import { SYNTHETIC_DEFAULT_MODEL_ID } from "../agents/synthetic-models.js";
import type { OpenClawConfig } from "../config/config.js";
import {
resolveAgentModelFallbackValues,
resolveAgentModelPrimaryValue,
} from "../config/model-input.js";
import { applyAuthProfileConfig } from "../plugins/provider-auth-helpers.js";
import { setMinimaxApiKey, writeOAuthCredentials } from "../plugins/provider-auth-storage.js";
import {
@ -11,6 +62,26 @@ import {
setupAuthTestEnv,
} from "./test-wizard-helpers.js";
function expectPrimaryModelPreserved(cfg: OpenClawConfig): void {
expect(resolveAgentModelPrimaryValue(cfg.agents?.defaults?.model)).toBe(
"anthropic/claude-opus-4-5",
);
}
function expectFallbacksPreserved(cfg: OpenClawConfig): void {
expect(resolveAgentModelFallbackValues(cfg.agents?.defaults?.model)).toEqual([
...EXPECTED_FALLBACKS,
]);
}
function expectAllowlistContains(cfg: OpenClawConfig, modelRef: string): void {
expect(Object.keys(cfg.agents?.defaults?.models ?? {})).toContain(modelRef);
}
function expectAliasPreserved(cfg: OpenClawConfig, modelRef: string, alias: string): void {
expect(cfg.agents?.defaults?.models?.[modelRef]?.alias).toBe(alias);
}
describe("writeOAuthCredentials", () => {
const lifecycle = createAuthTestLifecycle([
"OPENCLAW_STATE_DIR",
@ -320,3 +391,426 @@ describe("applyAuthProfileConfig", () => {
});
});
});
describe("applyMinimaxApiConfig", () => {
it("adds minimax provider with correct settings", () => {
const cfg = applyMinimaxApiConfig({});
expect(cfg.models?.providers?.minimax).toMatchObject({
baseUrl: "https://api.minimax.io/anthropic",
api: "anthropic-messages",
authHeader: true,
});
});
it("keeps reasoning enabled for MiniMax-M2.7", () => {
const cfg = applyMinimaxApiConfig({}, "MiniMax-M2.7");
expect(cfg.models?.providers?.minimax?.models[0]?.reasoning).toBe(true);
});
it("preserves existing model params when adding alias", () => {
const cfg = applyMinimaxApiConfig(
{
agents: {
defaults: {
models: {
"minimax/MiniMax-M2.7": {
alias: "MiniMax",
params: { custom: "value" },
},
},
},
},
},
"MiniMax-M2.7",
);
expect(cfg.agents?.defaults?.models?.["minimax/MiniMax-M2.7"]).toMatchObject({
alias: "Minimax",
params: { custom: "value" },
});
});
it("merges existing minimax provider models", () => {
const cfg = applyMinimaxApiConfig(
createLegacyProviderConfig({
providerId: "minimax",
api: "openai-completions",
}),
);
expect(cfg.models?.providers?.minimax?.baseUrl).toBe("https://api.minimax.io/anthropic");
expect(cfg.models?.providers?.minimax?.api).toBe("anthropic-messages");
expect(cfg.models?.providers?.minimax?.authHeader).toBe(true);
expect(cfg.models?.providers?.minimax?.apiKey).toBe("old-key");
expect(cfg.models?.providers?.minimax?.models.map((m) => m.id)).toEqual([
"old-model",
"MiniMax-M2.7",
]);
});
it("preserves other providers when adding minimax", () => {
const cfg = applyMinimaxApiConfig({
models: {
providers: {
anthropic: {
baseUrl: "https://api.anthropic.com",
apiKey: "anthropic-key", // pragma: allowlist secret
api: "anthropic-messages",
models: [
{
id: "claude-opus-4-5",
name: "Claude Opus 4.5",
reasoning: false,
input: ["text"],
cost: { input: 15, output: 75, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 8192,
},
],
},
},
},
});
expect(cfg.models?.providers?.anthropic).toBeDefined();
expect(cfg.models?.providers?.minimax).toBeDefined();
});
it("preserves existing models mode", () => {
const cfg = applyMinimaxApiConfig({
models: { mode: "replace", providers: {} },
});
expect(cfg.models?.mode).toBe("replace");
});
});
describe("provider config helpers", () => {
it("does not overwrite existing primary model", () => {
const providerConfigAppliers = [applyMinimaxApiProviderConfig, applyZaiProviderConfig];
for (const applyConfig of providerConfigAppliers) {
const cfg = applyConfig({
agents: { defaults: { model: { primary: "anthropic/claude-opus-4-5" } } },
});
expectPrimaryModelPreserved(cfg);
}
});
});
describe("applyZaiConfig", () => {
it("adds zai provider with correct settings", () => {
const cfg = applyZaiConfig({});
expect(cfg.models?.providers?.zai).toMatchObject({
// Default: general (non-coding) endpoint. Coding Plan endpoint is detected during setup.
baseUrl: ZAI_GLOBAL_BASE_URL,
api: "openai-completions",
});
const ids = cfg.models?.providers?.zai?.models?.map((m) => m.id);
expect(ids).toContain("glm-5");
expect(ids).toContain("glm-5-turbo");
expect(ids).toContain("glm-4.7");
expect(ids).toContain("glm-4.7-flash");
expect(ids).toContain("glm-4.7-flashx");
});
it("supports CN endpoint for supported coding models", () => {
for (const modelId of ["glm-4.7-flash", "glm-4.7-flashx"] as const) {
const cfg = applyZaiConfig({}, { endpoint: "coding-cn", modelId });
expect(cfg.models?.providers?.zai?.baseUrl).toBe(ZAI_CODING_CN_BASE_URL);
expect(resolveAgentModelPrimaryValue(cfg.agents?.defaults?.model)).toBe(`zai/${modelId}`);
}
});
});
describe("applySyntheticConfig", () => {
it("adds synthetic provider with correct settings", () => {
const cfg = applySyntheticConfig({});
expect(cfg.models?.providers?.synthetic).toMatchObject({
baseUrl: "https://api.synthetic.new/anthropic",
api: "anthropic-messages",
});
});
it("merges existing synthetic provider models", () => {
const cfg = applySyntheticProviderConfig(
createLegacyProviderConfig({
providerId: "synthetic",
api: "openai-completions",
}),
);
expect(cfg.models?.providers?.synthetic?.baseUrl).toBe("https://api.synthetic.new/anthropic");
expect(cfg.models?.providers?.synthetic?.api).toBe("anthropic-messages");
expect(cfg.models?.providers?.synthetic?.apiKey).toBe("old-key");
const ids = cfg.models?.providers?.synthetic?.models.map((m) => m.id);
expect(ids).toContain("old-model");
expect(ids).toContain(SYNTHETIC_DEFAULT_MODEL_ID);
});
});
describe("primary model defaults", () => {
it("sets correct primary model", () => {
const configCases = [
{
getConfig: () => applyMinimaxApiConfig({}, "MiniMax-M2.7-highspeed"),
primaryModel: "minimax/MiniMax-M2.7-highspeed",
},
{
getConfig: () => applyZaiConfig({}, { modelId: "glm-5" }),
primaryModel: "zai/glm-5",
},
{
getConfig: () => applySyntheticConfig({}),
primaryModel: SYNTHETIC_DEFAULT_MODEL_REF,
},
] as const;
for (const { getConfig, primaryModel } of configCases) {
const cfg = getConfig();
expect(resolveAgentModelPrimaryValue(cfg.agents?.defaults?.model)).toBe(primaryModel);
}
});
});
describe("applyXiaomiConfig", () => {
it("adds Xiaomi provider with correct settings", () => {
const cfg = applyXiaomiConfig({});
expect(cfg.models?.providers?.xiaomi).toMatchObject({
baseUrl: "https://api.xiaomimimo.com/v1",
api: "openai-completions",
});
expect(cfg.models?.providers?.xiaomi?.models.map((m) => m.id)).toEqual([
"mimo-v2-flash",
"mimo-v2-pro",
"mimo-v2-omni",
]);
expect(resolveAgentModelPrimaryValue(cfg.agents?.defaults?.model)).toBe("xiaomi/mimo-v2-flash");
});
it("merges Xiaomi models and keeps existing provider overrides", () => {
const cfg = applyXiaomiProviderConfig(
createLegacyProviderConfig({
providerId: "xiaomi",
api: "openai-completions",
modelId: "custom-model",
modelName: "Custom",
}),
);
expect(cfg.models?.providers?.xiaomi?.baseUrl).toBe("https://api.xiaomimimo.com/v1");
expect(cfg.models?.providers?.xiaomi?.api).toBe("openai-completions");
expect(cfg.models?.providers?.xiaomi?.apiKey).toBe("old-key");
expect(cfg.models?.providers?.xiaomi?.models.map((m) => m.id)).toEqual([
"custom-model",
"mimo-v2-flash",
"mimo-v2-pro",
"mimo-v2-omni",
]);
});
});
describe("applyXaiConfig", () => {
it("adds xAI provider with correct settings", () => {
const cfg = applyXaiConfig({});
expect(cfg.models?.providers?.xai).toMatchObject({
baseUrl: "https://api.x.ai/v1",
api: "openai-responses",
});
expect(resolveAgentModelPrimaryValue(cfg.agents?.defaults?.model)).toBe(XAI_DEFAULT_MODEL_REF);
});
});
describe("applyXaiProviderConfig", () => {
it("merges xAI models and keeps existing provider overrides", () => {
const cfg = applyXaiProviderConfig(
createLegacyProviderConfig({
providerId: "xai",
api: "anthropic-messages",
modelId: "custom-model",
modelName: "Custom",
}),
);
expect(cfg.models?.providers?.xai?.baseUrl).toBe("https://api.x.ai/v1");
expect(cfg.models?.providers?.xai?.api).toBe("openai-responses");
expect(cfg.models?.providers?.xai?.apiKey).toBe("old-key");
expect(cfg.models?.providers?.xai?.models.map((m) => m.id)).toEqual(
expect.arrayContaining([
"custom-model",
"grok-4",
"grok-4-1-fast",
"grok-4.20-beta-latest-reasoning",
"grok-code-fast-1",
]),
);
});
});
describe("applyMistralConfig", () => {
it("adds Mistral provider with correct settings", () => {
const cfg = applyMistralConfig({});
expect(cfg.models?.providers?.mistral).toMatchObject({
baseUrl: "https://api.mistral.ai/v1",
api: "openai-completions",
});
expect(resolveAgentModelPrimaryValue(cfg.agents?.defaults?.model)).toBe(
MISTRAL_DEFAULT_MODEL_REF,
);
});
});
describe("applyMistralProviderConfig", () => {
it("merges Mistral models and keeps existing provider overrides", () => {
const cfg = applyMistralProviderConfig(
createLegacyProviderConfig({
providerId: "mistral",
api: "anthropic-messages",
modelId: "custom-model",
modelName: "Custom",
}),
);
expect(cfg.models?.providers?.mistral?.baseUrl).toBe("https://api.mistral.ai/v1");
expect(cfg.models?.providers?.mistral?.api).toBe("openai-completions");
expect(cfg.models?.providers?.mistral?.apiKey).toBe("old-key");
expect(cfg.models?.providers?.mistral?.models.map((m) => m.id)).toEqual([
"custom-model",
"mistral-large-latest",
]);
const mistralDefault = cfg.models?.providers?.mistral?.models.find(
(model) => model.id === "mistral-large-latest",
);
expect(mistralDefault?.contextWindow).toBe(262144);
expect(mistralDefault?.maxTokens).toBe(16384);
});
it("uses the bundled mistral default model definition", () => {
const bundled = buildBundledMistralModelDefinition();
const cfg = applyMistralProviderConfig({});
const defaultModel = cfg.models?.providers?.mistral?.models.find(
(model) => model.id === bundled.id,
);
expect(defaultModel).toMatchObject({
id: bundled.id,
contextWindow: bundled.contextWindow,
maxTokens: bundled.maxTokens,
});
});
});
describe("fallback preservation helpers", () => {
it("preserves existing model fallbacks", () => {
const fallbackCases = [applyMinimaxApiConfig, applyXaiConfig, applyMistralConfig] as const;
for (const applyConfig of fallbackCases) {
const cfg = applyConfig(createConfigWithFallbacks());
expectFallbacksPreserved(cfg);
}
});
});
describe("provider alias defaults", () => {
it("adds expected alias for provider defaults", () => {
const aliasCases = [
{
applyConfig: () => applyMinimaxApiConfig({}, "MiniMax-M2.7"),
modelRef: "minimax/MiniMax-M2.7",
alias: "Minimax",
},
{
applyConfig: () => applyXaiProviderConfig({}),
modelRef: XAI_DEFAULT_MODEL_REF,
alias: "Grok",
},
{
applyConfig: () => applyMistralProviderConfig({}),
modelRef: MISTRAL_DEFAULT_MODEL_REF,
alias: "Mistral",
},
] as const;
for (const testCase of aliasCases) {
const cfg = testCase.applyConfig();
expect(cfg.agents?.defaults?.models?.[testCase.modelRef]?.alias).toBe(testCase.alias);
}
});
});
describe("allowlist provider helpers", () => {
it("adds allowlist entry and preserves alias", () => {
const providerCases = [
{
applyConfig: applyOpencodeZenProviderConfig,
modelRef: "opencode/claude-opus-4-6",
alias: "My Opus",
},
{
applyConfig: applyOpencodeGoProviderConfig,
modelRef: "opencode-go/kimi-k2.5",
alias: "Kimi",
},
{
applyConfig: applyOpenrouterProviderConfig,
modelRef: OPENROUTER_DEFAULT_MODEL_REF,
alias: "Router",
},
] as const;
for (const { applyConfig, modelRef, alias } of providerCases) {
const withDefault = applyConfig({});
expectAllowlistContains(withDefault, modelRef);
const withAlias = applyConfig({
agents: {
defaults: {
models: {
[modelRef]: { alias },
},
},
},
});
expectAliasPreserved(withAlias, modelRef, alias);
}
});
});
describe("applyLitellmProviderConfig", () => {
it("preserves existing baseUrl and api key while adding the default model", () => {
const cfg = applyLitellmProviderConfig(
createLegacyProviderConfig({
providerId: "litellm",
api: "anthropic-messages",
modelId: "custom-model",
modelName: "Custom",
baseUrl: "https://litellm.example/v1",
apiKey: " old-key ",
}),
);
expect(cfg.models?.providers?.litellm?.baseUrl).toBe("https://litellm.example/v1");
expect(cfg.models?.providers?.litellm?.api).toBe("openai-completions");
expect(cfg.models?.providers?.litellm?.apiKey).toBe("old-key");
expect(cfg.models?.providers?.litellm?.models.map((m) => m.id)).toEqual([
"custom-model",
"claude-opus-4-6",
]);
});
});
describe("default-model config helpers", () => {
it("sets primary model and preserves existing model fallbacks", () => {
const configCases = [
{
applyConfig: applyOpencodeZenConfig,
primaryModel: "opencode/claude-opus-4-6",
},
{
applyConfig: applyOpencodeGoConfig,
primaryModel: "opencode-go/kimi-k2.5",
},
{
applyConfig: applyOpenrouterConfig,
primaryModel: OPENROUTER_DEFAULT_MODEL_REF,
},
] as const;
for (const { applyConfig, primaryModel } of configCases) {
const cfg = applyConfig({});
expect(resolveAgentModelPrimaryValue(cfg.agents?.defaults?.model)).toBe(primaryModel);
const cfgWithFallbacks = applyConfig(createConfigWithFallbacks());
expectFallbacksPreserved(cfgWithFallbacks);
}
});
});