mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-07 18:02:53 +00:00
* refactor: extract agent core package Introduce packages/agent-core as the OpenClaw-owned home for reusable agent loop, harness, session, prompt, and runtime dependency contracts. * refactor: extract shared llm runtime Move provider model registries, stream wrappers, OAuth helpers, and LLM utilities into src/llm with plugin-sdk barrels instead of depending on the old embedded runtime layout. * refactor: remove pi runtime internals Rename remaining Pi-shaped agent surfaces to OpenClaw agent runtime names, delete obsolete Pi docs and package graph checks, and add the third-party notice for incorporated code. * refactor: tighten agent session runtime Make agent-core/runtime dependencies explicit, consolidate compaction and session transcript helpers, and move model/session helpers behind OpenClaw-owned contracts. * refactor: remove static model and pi auth paths Drop static model catalogs and Pi auth bridges, move model/provider facts to manifest-owned runtime contracts, and harden internal embedded-agent utilities. * refactor: remove legacy provider compat paths * docs: remove agent parity notes * fix: skip provider wildcard metadata parsing * refactor: share session extension sdk loading * refactor: inline acpx proxy error formatter * refactor: fold edit recovery into edit tool * fix: accept extension batch separator * test: align startup provider plugin expectations * fix: restore provider-scoped release discovery * test: align static asset packaging expectations * fix: run static provider catalogs during scoped discovery * fix: add provider entry catalogs for scoped live discovery * fix: load lightweight provider catalog entries * fix: refresh provider-scoped plugin metadata * fix: keep provider catalog entries on release live path * fix: keep static manifest models in release live checks * fix: harden release model discovery * fix: reduce OpenAI live cache probe reasoning * fix: disable OpenAI cache probe reasoning * ci: extend OpenAI gateway live timeout * fix: extend live gateway model budget * fix: stabilize release validation regressions * fix: honor provider aliases in model rows * fix: stabilize release validation lanes * fix: stabilize release memory qa * ci: stabilize release validation lanes * ci: prefer ipv4 for live docker node calls * fix: restore shared tool-call stream wrapper * ci: remove legacy pi test shard alias * fix: clean up embedded agent test drift * fix: stabilize runtime alias status * fix: clean up embedded agent ci drift * fix: restore release ci invariants * fix: clean up post-rebase runtime drift * fix: restore release ci checks * fix: restore release ci after rebase * fix: remove stale pi runtime path * test: align compaction runtime expectations * test: update plugin prerelease expectations * fix: handle claude live tool approvals * fix: stabilize release validation gates * fix: finish agent runtime import * test: finish post-rebase agent runtime mocks * fix: keep codex compaction native * fix: stabilize codex app-server hook tests * test: isolate codex diagnostic active run * test: remove codex diagnostic completion race # Conflicts: # extensions/codex/src/app-server/run-attempt.test.ts * ci: fix full release manifest performance run id * refactor: narrow llm plugin sdk boundary * chore: drop generated google boundary stamps * fix: repair rebase fallout * fix: clean up rebased runtime references * fix: decode codex jwt payloads as base64url * fix: preserve shipped pi runtime alias * fix: add scoped sdk virtual modules * fix: decode llm codex oauth jwt as base64url * fix: avoid stale vertex adc negative cache * fix: harden tool arg decoding and codeql path * fix: keep vertex adc negative checks live * refactor: consolidate codex jwt and edit helpers * fix: await codex oauth node runtime imports * fix: preserve sdk tool and notice contracts * fix: preserve shipped compat config boundaries * fix: align codex oauth callback host * fix: terminate agent-core loop streams on failure * fix: keep codex oauth callback alive during fallback * ci: include session tools in critical codeql scans * fix: keep Cloudflare Anthropic provider auth header * docs: redirect legacy pi runtime pages * fix: honor bundled web provider compat discovery * fix: protect session output spill files * fix: keep legacy agent dir env blocked * fix: contain auto-discovered skill symlinks * fix: harden agent core sdk proxy surfaces * fix: restore approval reaction sdk compat * fix: keep live docker runs bounded * fix: keep codex oauth redirect host aligned * fix: resolve post-rebase agent runtime drift * fix: redact anthropic oauth parse failures * fix: preserve responses strict tool shaping * fix: repair agent runtime rebase cleanup * docs: redirect retired parity pages * fix: bound auto-discovered resources to roots * fix: repair post-rebase agent test drift * fix: preserve bundled provider allowlist migration * fix: preserve manifest-owned provider aliases * fix: declare photon image dependency * fix: keep provider headers out of proxy body * fix: preserve shipped env aliases * fix: refresh control ui i18n generated state * fix: quote read fallback paths * fix: preview edits through configured backend * test: satisfy core test typecheck * fix: preserve ZAI usage auth fallback * test: repair codex diagnostic test * fix: repair agent runtime rebase drift * test: finish embedded runner import rename * fix: repair agent runtime rebase integrations * test: align compaction oauth fallback expectations * fix: allow sdk-auth session models * fix: update doctor tool schema import * fix: preserve bedrock plugin region * fix: stream harmony-like prose immediately * ci: include session runtime in codeql shards * fix: repair latest rebase integrations * fix: honor explicit codex websocket transport * fix: keep openai-compatible credentials provider-scoped * fix: refresh sdk api baseline after rebase * fix: route cli runtime aliases through openclaw harness * test: rename stale harness mock expectation * test: rename embedded agent overflow calls * test: clean embedded auth test wording * test: use openclaw stream types in deepinfra cache test * fix: refresh sdk api baseline on latest main * fix: honor bundled discovery compat allowlists * fix: refresh sdk api baseline after latest rebase * fix: remove stale rebase imports * test: rename stale model catalog mock * test: mock renamed doctor runtime modules * fix: map canonical kimi env auth * fix: use internal model registry in bench script * fix: migrate deepinfra provider catalog entry * fix: enforce builtin tool suppression * fix: route compaction auth and proxy payloads safely * refactor: prune unused llm registry leftovers * test: update codex hooks session import * test: fix model picker ci coverage * test: align model picker auth mock types
1243 lines
38 KiB
TypeScript
1243 lines
38 KiB
TypeScript
import { createTestWizardPrompter } from "openclaw/plugin-sdk/plugin-test-runtime";
|
|
import { NON_ENV_SECRETREF_MARKER } from "openclaw/plugin-sdk/provider-auth-runtime";
|
|
import { createNonExitingRuntime } from "openclaw/plugin-sdk/runtime-env";
|
|
import { withEnv, withEnvAsync, withFetchPreconnect } from "openclaw/plugin-sdk/test-env";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { buildXaiCatalogModels, resolveXaiCatalogEntry } from "./model-definitions.js";
|
|
import { isModernXaiModel, resolveXaiForwardCompatModel } from "./provider-models.js";
|
|
import { resolveFallbackXaiAuth } from "./src/tool-auth-shared.js";
|
|
import { wrapXaiWebSearchError } from "./src/web-search-shared.js";
|
|
import { testing } from "./test-api.js";
|
|
import { createXaiWebSearchProvider as createXaiWebSearchContractProvider } from "./web-search-contract-api.js";
|
|
import { createXaiWebSearchProvider } from "./web-search.js";
|
|
|
|
const providerAuthRuntimeMocks = vi.hoisted(() => ({
|
|
resolveApiKeyForProvider: vi.fn(),
|
|
}));
|
|
|
|
const providerAuthMocks = vi.hoisted(() => ({
|
|
ensureAuthProfileStore: vi.fn(),
|
|
listUsableProviderAuthProfileIds: vi.fn(() => ({ agentDir: "", profileIds: [] as string[] })),
|
|
}));
|
|
|
|
vi.mock("openclaw/plugin-sdk/provider-auth", async (importOriginal) => {
|
|
const original = await importOriginal<typeof import("openclaw/plugin-sdk/provider-auth")>();
|
|
return {
|
|
...original,
|
|
ensureAuthProfileStore: providerAuthMocks.ensureAuthProfileStore,
|
|
listUsableProviderAuthProfileIds: providerAuthMocks.listUsableProviderAuthProfileIds,
|
|
};
|
|
});
|
|
|
|
vi.mock("openclaw/plugin-sdk/provider-auth-runtime", async (importOriginal) => {
|
|
const original =
|
|
await importOriginal<typeof import("openclaw/plugin-sdk/provider-auth-runtime")>();
|
|
return {
|
|
...original,
|
|
resolveApiKeyForProvider: providerAuthRuntimeMocks.resolveApiKeyForProvider,
|
|
};
|
|
});
|
|
|
|
vi.mock("openclaw/plugin-sdk/provider-web-search", async (importOriginal) => {
|
|
const original = await importOriginal<typeof import("openclaw/plugin-sdk/provider-web-search")>();
|
|
return {
|
|
...original,
|
|
postTrustedWebToolsJson: async (
|
|
params: {
|
|
url: string;
|
|
apiKey: string;
|
|
body: Record<string, unknown>;
|
|
extraHeaders?: Record<string, string>;
|
|
},
|
|
parseResponse: (response: Response) => Promise<unknown>,
|
|
) => {
|
|
const response = await globalThis.fetch(params.url, {
|
|
method: "POST",
|
|
headers: {
|
|
...params.extraHeaders,
|
|
Accept: "application/json",
|
|
Authorization: `Bearer ${params.apiKey}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(params.body),
|
|
});
|
|
if (!response.ok) {
|
|
const detail =
|
|
typeof response.text === "function"
|
|
? await response.text()
|
|
: response.statusText || String(response.status);
|
|
throw new Error(`xAI API error (${response.status}): ${detail || response.statusText}`);
|
|
}
|
|
return await parseResponse(response);
|
|
},
|
|
};
|
|
});
|
|
|
|
const {
|
|
extractXaiWebSearchContent,
|
|
resolveXaiInlineCitations,
|
|
resolveXaiToolSearchConfig,
|
|
resolveXaiWebSearchCredential,
|
|
resolveXaiWebSearchModel,
|
|
resolveXaiWebSearchTimeoutSeconds,
|
|
} = testing;
|
|
|
|
function installXaiWebSearchFetch() {
|
|
const mockFetch = vi.fn((_input?: unknown, _init?: unknown) =>
|
|
Promise.resolve({
|
|
ok: true,
|
|
json: () =>
|
|
Promise.resolve({
|
|
output: [
|
|
{
|
|
type: "message",
|
|
content: [{ type: "output_text", text: "Grounded Grok answer" }],
|
|
},
|
|
],
|
|
}),
|
|
} as Response),
|
|
);
|
|
global.fetch = withFetchPreconnect(mockFetch);
|
|
return mockFetch;
|
|
}
|
|
|
|
function firstFetchUrl(mockFetch: ReturnType<typeof installXaiWebSearchFetch>) {
|
|
const [call] = mockFetch.mock.calls;
|
|
if (!call) {
|
|
throw new Error("expected xai web search fetch call");
|
|
}
|
|
const [url] = call;
|
|
return String(url);
|
|
}
|
|
|
|
function fetchCallHeader(
|
|
mockFetch: { mock: { calls: unknown[][] } },
|
|
index: number,
|
|
name: string,
|
|
): string | undefined {
|
|
const init = mockFetch.mock.calls[index]?.[1] as RequestInit | undefined;
|
|
const headers = init?.headers;
|
|
if (!headers) {
|
|
return undefined;
|
|
}
|
|
if (headers instanceof Headers) {
|
|
return headers.get(name) ?? undefined;
|
|
}
|
|
if (Array.isArray(headers)) {
|
|
const lowerName = name.toLowerCase();
|
|
return headers.find(([key]) => key.toLowerCase() === lowerName)?.[1];
|
|
}
|
|
return (headers as Record<string, string | undefined>)[name];
|
|
}
|
|
|
|
function expectCatalogEntry(
|
|
modelId: string,
|
|
expected: {
|
|
id?: string;
|
|
reasoning?: boolean;
|
|
input?: string[];
|
|
contextWindow?: number;
|
|
maxTokens?: number;
|
|
cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
},
|
|
) {
|
|
const entry = resolveXaiCatalogEntry(modelId);
|
|
expect(entry?.id).toBe(expected.id ?? modelId);
|
|
if ("reasoning" in expected) {
|
|
expect(entry?.reasoning).toBe(expected.reasoning);
|
|
}
|
|
if (expected.input) {
|
|
expect(entry?.input).toEqual(expected.input);
|
|
}
|
|
if (expected.contextWindow !== undefined) {
|
|
expect(entry?.contextWindow).toBe(expected.contextWindow);
|
|
}
|
|
if (expected.maxTokens !== undefined) {
|
|
expect(entry?.maxTokens).toBe(expected.maxTokens);
|
|
}
|
|
if (expected.cost) {
|
|
expect(entry?.cost).toEqual(expected.cost);
|
|
}
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
providerAuthMocks.ensureAuthProfileStore.mockReset();
|
|
providerAuthMocks.listUsableProviderAuthProfileIds.mockReset();
|
|
providerAuthMocks.listUsableProviderAuthProfileIds.mockReturnValue({
|
|
agentDir: "",
|
|
profileIds: [],
|
|
});
|
|
providerAuthRuntimeMocks.resolveApiKeyForProvider.mockReset();
|
|
});
|
|
|
|
describe("xai web search config resolution", () => {
|
|
it("advertises xAI auth profiles in runtime and setup contracts", () => {
|
|
expect(createXaiWebSearchProvider().authProviderId).toBe("xai");
|
|
expect(createXaiWebSearchContractProvider().authProviderId).toBe("xai");
|
|
});
|
|
|
|
it("prefers configured api keys and resolves grok scoped defaults", () => {
|
|
expect(resolveXaiWebSearchCredential({ grok: { apiKey: "xai-secret" } })).toBe("xai-secret");
|
|
expect(resolveXaiWebSearchModel()).toBe("grok-4-1-fast");
|
|
expect(resolveXaiInlineCitations()).toBe(false);
|
|
});
|
|
|
|
it("uses config apiKey when provided", () => {
|
|
expect(resolveXaiWebSearchCredential({ grok: { apiKey: "xai-test-key" } })).toBe(
|
|
"xai-test-key",
|
|
);
|
|
});
|
|
|
|
it("returns undefined when no apiKey is available", () => {
|
|
withEnv({ XAI_API_KEY: undefined }, () => {
|
|
expect(resolveXaiWebSearchCredential({})).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
it("resolves env SecretRefs without requiring a runtime snapshot", () => {
|
|
withEnv({ XAI_WEB_SEARCH_KEY: "xai-env-ref-key" }, () => {
|
|
expect(
|
|
resolveXaiWebSearchCredential({
|
|
grok: {
|
|
apiKey: {
|
|
source: "env",
|
|
provider: "default",
|
|
id: "XAI_WEB_SEARCH_KEY",
|
|
},
|
|
},
|
|
}),
|
|
).toBe("xai-env-ref-key");
|
|
});
|
|
});
|
|
|
|
it("merges canonical plugin config into the tool search config", () => {
|
|
const searchConfig = resolveXaiToolSearchConfig({
|
|
config: {
|
|
plugins: {
|
|
entries: {
|
|
xai: {
|
|
enabled: true,
|
|
config: {
|
|
webSearch: {
|
|
apiKey: "plugin-key",
|
|
inlineCitations: true,
|
|
model: "grok-4-fast-reasoning",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
searchConfig: { provider: "grok" },
|
|
});
|
|
|
|
expect(resolveXaiWebSearchCredential(searchConfig)).toBe("plugin-key");
|
|
expect(resolveXaiInlineCitations(searchConfig)).toBe(true);
|
|
expect(resolveXaiWebSearchModel(searchConfig)).toBe("grok-4-fast");
|
|
});
|
|
|
|
it("treats unresolved non-env SecretRefs as missing credentials instead of using env fallback", async () => {
|
|
await withEnvAsync({ XAI_API_KEY: "ambient-xai-test-key" }, async () => {
|
|
const provider = createXaiWebSearchProvider();
|
|
const maybeTool = provider.createTool({
|
|
config: {
|
|
plugins: {
|
|
entries: {
|
|
xai: {
|
|
enabled: true,
|
|
config: {
|
|
webSearch: {
|
|
apiKey: {
|
|
source: "file",
|
|
provider: "vault",
|
|
id: "/providers/xai/web-search",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
if (!maybeTool) {
|
|
throw new Error("expected xai web search tool");
|
|
}
|
|
|
|
const result = await maybeTool.execute({ query: "OpenClaw" });
|
|
expect(result.error).toBe("missing_xai_api_key");
|
|
expect(result.message).toContain("use web_fetch for a specific URL or the browser tool");
|
|
});
|
|
});
|
|
|
|
it("uses xAI OAuth auth before API-key fallback for web search", async () => {
|
|
providerAuthRuntimeMocks.resolveApiKeyForProvider.mockResolvedValue({
|
|
apiKey: "oauth-web-search-token",
|
|
source: "profile:xai:default",
|
|
mode: "oauth",
|
|
profileId: "xai:default",
|
|
});
|
|
const mockFetch = installXaiWebSearchFetch();
|
|
const provider = createXaiWebSearchProvider();
|
|
const tool = provider.createTool({
|
|
config: {
|
|
agents: {
|
|
list: [{ id: "main", default: true, agentDir: "/tmp/openclaw-xai-main-agent" }],
|
|
},
|
|
plugins: {
|
|
entries: {
|
|
xai: {
|
|
config: {
|
|
webSearch: {
|
|
apiKey: "configured-xai-key",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
if (!tool) {
|
|
throw new Error("Expected xAI web search tool");
|
|
}
|
|
|
|
await tool.execute({ query: "OpenClaw Grok OAuth web search" });
|
|
|
|
expect(providerAuthRuntimeMocks.resolveApiKeyForProvider).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
provider: "xai",
|
|
agentDir: "/tmp/openclaw-xai-main-agent",
|
|
}),
|
|
);
|
|
expect(fetchCallHeader(mockFetch, 0, "Authorization")).toBe("Bearer oauth-web-search-token");
|
|
});
|
|
|
|
it("uses the active agentDir for xAI OAuth web search auth", async () => {
|
|
providerAuthRuntimeMocks.resolveApiKeyForProvider.mockResolvedValue({
|
|
apiKey: "active-agent-oauth-token",
|
|
source: "profile:xai:active",
|
|
mode: "oauth",
|
|
profileId: "xai:active",
|
|
});
|
|
const mockFetch = installXaiWebSearchFetch();
|
|
const provider = createXaiWebSearchProvider();
|
|
const tool = provider.createTool({
|
|
agentDir: "/tmp/openclaw-xai-active-agent",
|
|
config: {
|
|
agents: {
|
|
list: [
|
|
{ id: "main", default: true, agentDir: "/tmp/openclaw-xai-main-agent" },
|
|
{ id: "side", agentDir: "/tmp/openclaw-xai-active-agent" },
|
|
],
|
|
},
|
|
},
|
|
});
|
|
if (!tool) {
|
|
throw new Error("Expected xAI web search tool");
|
|
}
|
|
|
|
await tool.execute({ query: "OpenClaw Grok active agent OAuth web search" });
|
|
|
|
expect(providerAuthRuntimeMocks.resolveApiKeyForProvider).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
provider: "xai",
|
|
agentDir: "/tmp/openclaw-xai-active-agent",
|
|
}),
|
|
);
|
|
expect(fetchCallHeader(mockFetch, 0, "Authorization")).toBe("Bearer active-agent-oauth-token");
|
|
});
|
|
|
|
it("refreshes xAI OAuth auth and retries web search after a 401", async () => {
|
|
providerAuthRuntimeMocks.resolveApiKeyForProvider
|
|
.mockResolvedValueOnce({
|
|
apiKey: "expired-oauth-token",
|
|
source: "profile:xai:default",
|
|
mode: "oauth",
|
|
profileId: "xai:default",
|
|
})
|
|
.mockResolvedValueOnce({
|
|
apiKey: "fresh-oauth-token",
|
|
source: "profile:xai:default",
|
|
mode: "oauth",
|
|
profileId: "xai:default",
|
|
});
|
|
const mockFetch = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 401,
|
|
statusText: "Unauthorized",
|
|
text: () => Promise.resolve("expired"),
|
|
} as Response)
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: () =>
|
|
Promise.resolve({
|
|
output: [
|
|
{
|
|
type: "message",
|
|
content: [{ type: "output_text", text: "Fresh OAuth Grok answer" }],
|
|
},
|
|
],
|
|
}),
|
|
} as Response);
|
|
global.fetch = withFetchPreconnect(mockFetch);
|
|
const provider = createXaiWebSearchProvider();
|
|
const tool = provider.createTool({
|
|
config: {
|
|
agents: {
|
|
list: [{ id: "main", default: true, agentDir: "/tmp/openclaw-xai-main-agent" }],
|
|
},
|
|
tools: {
|
|
web: {
|
|
search: {
|
|
provider: "grok",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
if (!tool) {
|
|
throw new Error("Expected xAI web search tool");
|
|
}
|
|
|
|
const result = await tool.execute({ query: "OpenClaw Grok OAuth refresh test" });
|
|
|
|
expect(result.content).toContain("Fresh OAuth Grok answer");
|
|
expect(providerAuthRuntimeMocks.resolveApiKeyForProvider).toHaveBeenNthCalledWith(
|
|
2,
|
|
expect.objectContaining({
|
|
provider: "xai",
|
|
agentDir: "/tmp/openclaw-xai-main-agent",
|
|
profileId: "xai:default",
|
|
lockedProfile: true,
|
|
forceRefresh: true,
|
|
}),
|
|
);
|
|
expect(fetchCallHeader(mockFetch, 0, "Authorization")).toBe("Bearer expired-oauth-token");
|
|
expect(fetchCallHeader(mockFetch, 1, "Authorization")).toBe("Bearer fresh-oauth-token");
|
|
});
|
|
|
|
it("falls back to xAI API-key auth when OAuth refresh cannot recover", async () => {
|
|
providerAuthRuntimeMocks.resolveApiKeyForProvider
|
|
.mockResolvedValueOnce({
|
|
apiKey: "expired-oauth-token",
|
|
source: "profile:xai:default",
|
|
mode: "oauth",
|
|
profileId: "xai:default",
|
|
})
|
|
.mockResolvedValueOnce({
|
|
apiKey: "expired-oauth-token",
|
|
source: "profile:xai:default",
|
|
mode: "oauth",
|
|
profileId: "xai:default",
|
|
})
|
|
.mockResolvedValueOnce({
|
|
apiKey: "xai-env-fallback-key",
|
|
source: "XAI_API_KEY",
|
|
mode: "api-key",
|
|
});
|
|
const mockFetch = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 401,
|
|
statusText: "Unauthorized",
|
|
text: () => Promise.resolve("revoked"),
|
|
} as Response)
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: () =>
|
|
Promise.resolve({
|
|
output: [
|
|
{
|
|
type: "message",
|
|
content: [{ type: "output_text", text: "API key fallback Grok answer" }],
|
|
},
|
|
],
|
|
}),
|
|
} as Response);
|
|
global.fetch = withFetchPreconnect(mockFetch);
|
|
const provider = createXaiWebSearchProvider();
|
|
const tool = provider.createTool({
|
|
config: {
|
|
agents: {
|
|
list: [{ id: "main", default: true, agentDir: "/tmp/openclaw-xai-main-agent" }],
|
|
},
|
|
tools: {
|
|
web: {
|
|
search: {
|
|
provider: "grok",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
if (!tool) {
|
|
throw new Error("Expected xAI web search tool");
|
|
}
|
|
|
|
const result = await tool.execute({ query: "OpenClaw Grok API fallback test" });
|
|
|
|
expect(result.content).toContain("API key fallback Grok answer");
|
|
expect(providerAuthRuntimeMocks.resolveApiKeyForProvider).toHaveBeenNthCalledWith(
|
|
3,
|
|
expect.objectContaining({
|
|
provider: "xai",
|
|
agentDir: "/tmp/openclaw-xai-main-agent",
|
|
credentialPrecedence: "env-first",
|
|
}),
|
|
);
|
|
expect(fetchCallHeader(mockFetch, 1, "Authorization")).toBe("Bearer xai-env-fallback-key");
|
|
});
|
|
|
|
it("falls back to an xAI API-key auth profile when stale OAuth remains first", async () => {
|
|
providerAuthRuntimeMocks.resolveApiKeyForProvider
|
|
.mockResolvedValueOnce({
|
|
apiKey: "expired-oauth-token",
|
|
source: "profile:xai:default",
|
|
mode: "oauth",
|
|
profileId: "xai:default",
|
|
})
|
|
.mockResolvedValueOnce({
|
|
apiKey: "expired-oauth-token",
|
|
source: "profile:xai:default",
|
|
mode: "oauth",
|
|
profileId: "xai:default",
|
|
})
|
|
.mockResolvedValueOnce({
|
|
apiKey: "expired-oauth-token",
|
|
source: "profile:xai:default",
|
|
mode: "oauth",
|
|
profileId: "xai:default",
|
|
})
|
|
.mockResolvedValueOnce({
|
|
apiKey: "xai-profile-api-key",
|
|
source: "profile:xai:key",
|
|
mode: "api-key",
|
|
profileId: "xai:key",
|
|
});
|
|
providerAuthMocks.listUsableProviderAuthProfileIds.mockReturnValue({
|
|
agentDir: "/tmp/openclaw-xai-main-agent",
|
|
profileIds: ["xai:default", "xai:key"],
|
|
});
|
|
providerAuthMocks.ensureAuthProfileStore.mockReturnValue({
|
|
version: 1,
|
|
active: "xai:default",
|
|
profiles: {
|
|
"xai:default": {
|
|
provider: "xai",
|
|
type: "oauth",
|
|
access: "expired-oauth-token",
|
|
refresh: "refresh-oauth-token",
|
|
expires: Date.now() + 3_600_000,
|
|
},
|
|
"xai:key": {
|
|
provider: "xai",
|
|
type: "api_key",
|
|
keyRef: { source: "env", id: "XAI_API_KEY" },
|
|
},
|
|
},
|
|
});
|
|
const mockFetch = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 401,
|
|
statusText: "Unauthorized",
|
|
text: () => Promise.resolve("revoked"),
|
|
} as Response)
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: () =>
|
|
Promise.resolve({
|
|
output: [
|
|
{
|
|
type: "message",
|
|
content: [{ type: "output_text", text: "Profile API key Grok answer" }],
|
|
},
|
|
],
|
|
}),
|
|
} as Response);
|
|
global.fetch = withFetchPreconnect(mockFetch);
|
|
const provider = createXaiWebSearchProvider();
|
|
const tool = provider.createTool({
|
|
config: {
|
|
agents: {
|
|
list: [{ id: "main", default: true, agentDir: "/tmp/openclaw-xai-main-agent" }],
|
|
},
|
|
tools: {
|
|
web: {
|
|
search: {
|
|
provider: "grok",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
if (!tool) {
|
|
throw new Error("Expected xAI web search tool");
|
|
}
|
|
|
|
const result = await tool.execute({ query: "OpenClaw Grok profile fallback test" });
|
|
|
|
expect(result.content).toContain("Profile API key Grok answer");
|
|
expect(providerAuthRuntimeMocks.resolveApiKeyForProvider).toHaveBeenNthCalledWith(
|
|
4,
|
|
expect.objectContaining({
|
|
provider: "xai",
|
|
agentDir: "/tmp/openclaw-xai-main-agent",
|
|
profileId: "xai:key",
|
|
lockedProfile: true,
|
|
}),
|
|
);
|
|
expect(fetchCallHeader(mockFetch, 1, "Authorization")).toBe("Bearer xai-profile-api-key");
|
|
});
|
|
|
|
it("falls back to env auth after a stale xAI API-key auth profile returns unauthorized", async () => {
|
|
providerAuthRuntimeMocks.resolveApiKeyForProvider
|
|
.mockResolvedValueOnce({
|
|
apiKey: "stale-profile-key",
|
|
source: "profile:xai:default",
|
|
mode: "api-key",
|
|
profileId: "xai:default",
|
|
})
|
|
.mockResolvedValueOnce({
|
|
apiKey: "xai-env-fallback-key",
|
|
source: "XAI_API_KEY",
|
|
mode: "api-key",
|
|
});
|
|
const mockFetch = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 401,
|
|
statusText: "Unauthorized",
|
|
text: () => Promise.resolve("stale api key"),
|
|
} as Response)
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: () =>
|
|
Promise.resolve({
|
|
output: [
|
|
{
|
|
type: "message",
|
|
content: [{ type: "output_text", text: "Env fallback Grok answer" }],
|
|
},
|
|
],
|
|
}),
|
|
} as Response);
|
|
global.fetch = withFetchPreconnect(mockFetch);
|
|
const provider = createXaiWebSearchProvider();
|
|
const tool = provider.createTool({
|
|
config: {
|
|
agents: {
|
|
list: [{ id: "main", default: true, agentDir: "/tmp/openclaw-xai-main-agent" }],
|
|
},
|
|
tools: {
|
|
web: {
|
|
search: {
|
|
provider: "grok",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
if (!tool) {
|
|
throw new Error("Expected xAI web search tool");
|
|
}
|
|
|
|
const result = await tool.execute({ query: "OpenClaw Grok API-key fallback test" });
|
|
|
|
expect(result.content).toContain("Env fallback Grok answer");
|
|
expect(providerAuthRuntimeMocks.resolveApiKeyForProvider).toHaveBeenNthCalledWith(
|
|
2,
|
|
expect.objectContaining({
|
|
provider: "xai",
|
|
agentDir: "/tmp/openclaw-xai-main-agent",
|
|
credentialPrecedence: "env-first",
|
|
}),
|
|
);
|
|
expect(fetchCallHeader(mockFetch, 0, "Authorization")).toBe("Bearer stale-profile-key");
|
|
expect(fetchCallHeader(mockFetch, 1, "Authorization")).toBe("Bearer xai-env-fallback-key");
|
|
});
|
|
|
|
it("offers plugin-owned xSearch setup after Grok is selected", async () => {
|
|
const provider = createXaiWebSearchProvider();
|
|
const select = vi.fn().mockResolvedValueOnce("yes").mockResolvedValueOnce("grok-4-1-fast");
|
|
const prompter = createTestWizardPrompter({
|
|
select: select as never,
|
|
});
|
|
|
|
const next = await provider.runSetup?.({
|
|
config: {
|
|
plugins: {
|
|
entries: {
|
|
xai: {
|
|
enabled: true,
|
|
config: {
|
|
webSearch: {
|
|
apiKey: "xai-test-key",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
tools: {
|
|
web: {
|
|
search: {
|
|
provider: "grok",
|
|
enabled: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
runtime: createNonExitingRuntime(),
|
|
prompter,
|
|
});
|
|
|
|
const xSearch = next?.plugins?.entries?.xai?.config?.xSearch as
|
|
| { enabled?: boolean; model?: string }
|
|
| undefined;
|
|
expect(xSearch?.enabled).toBe(true);
|
|
expect(xSearch?.model).toBe("grok-4-1-fast");
|
|
});
|
|
|
|
it("keeps explicit xSearch disablement untouched during provider-owned setup", async () => {
|
|
const provider = createXaiWebSearchProvider();
|
|
const config = {
|
|
plugins: {
|
|
entries: {
|
|
xai: {
|
|
config: {
|
|
xSearch: {
|
|
enabled: false,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
tools: {
|
|
web: {
|
|
search: {
|
|
provider: "grok",
|
|
enabled: true,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
const prompter = createTestWizardPrompter();
|
|
|
|
const next = await provider.runSetup?.({
|
|
config,
|
|
runtime: createNonExitingRuntime(),
|
|
prompter,
|
|
});
|
|
|
|
expect(next).toEqual(config);
|
|
expect(prompter.note).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("reuses the plugin web search api key for provider auth fallback", () => {
|
|
expect(
|
|
resolveFallbackXaiAuth({
|
|
plugins: {
|
|
entries: {
|
|
xai: {
|
|
config: {
|
|
webSearch: {
|
|
apiKey: "xai-provider-fallback", // pragma: allowlist secret
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as never),
|
|
).toEqual({
|
|
apiKey: "xai-provider-fallback",
|
|
source: "plugins.entries.xai.config.webSearch.apiKey",
|
|
});
|
|
});
|
|
|
|
it("reuses the legacy grok web search api key for provider auth fallback", () => {
|
|
expect(
|
|
resolveFallbackXaiAuth({
|
|
tools: {
|
|
web: {
|
|
search: {
|
|
grok: {
|
|
apiKey: "xai-legacy-fallback", // pragma: allowlist secret
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as never),
|
|
).toEqual({
|
|
apiKey: "xai-legacy-fallback",
|
|
source: "tools.web.search.grok.apiKey",
|
|
});
|
|
});
|
|
|
|
it("returns a managed marker for SecretRef-backed plugin auth fallback", () => {
|
|
expect(
|
|
resolveFallbackXaiAuth({
|
|
plugins: {
|
|
entries: {
|
|
xai: {
|
|
config: {
|
|
webSearch: {
|
|
apiKey: { source: "file", provider: "vault", id: "/xai/api-key" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as never),
|
|
).toEqual({
|
|
apiKey: NON_ENV_SECRETREF_MARKER,
|
|
source: "plugins.entries.xai.config.webSearch.apiKey",
|
|
});
|
|
});
|
|
|
|
it("uses default model when not specified", () => {
|
|
expect(resolveXaiWebSearchModel({})).toBe("grok-4-1-fast");
|
|
expect(resolveXaiWebSearchModel(undefined)).toBe("grok-4-1-fast");
|
|
});
|
|
|
|
it("uses a Grok-specific 60s default timeout while preserving overrides", () => {
|
|
expect(resolveXaiWebSearchTimeoutSeconds({})).toBe(60);
|
|
expect(resolveXaiWebSearchTimeoutSeconds(undefined)).toBe(60);
|
|
expect(resolveXaiWebSearchTimeoutSeconds({ timeoutSeconds: 15 })).toBe(15);
|
|
});
|
|
|
|
it("uses config model when provided", () => {
|
|
expect(resolveXaiWebSearchModel({ grok: { model: "grok-4-fast-reasoning" } })).toBe(
|
|
"grok-4-fast",
|
|
);
|
|
});
|
|
|
|
it("routes Grok web search through plugin webSearch.baseUrl", async () => {
|
|
const mockFetch = installXaiWebSearchFetch();
|
|
const provider = createXaiWebSearchProvider();
|
|
const tool = provider.createTool({
|
|
config: {
|
|
plugins: {
|
|
entries: {
|
|
xai: {
|
|
config: {
|
|
webSearch: {
|
|
apiKey: "xai-config-test",
|
|
baseUrl: "https://api.x.ai/proxy/v1/",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
searchConfig: { provider: "grok" },
|
|
});
|
|
if (!tool) {
|
|
throw new Error("Expected xAI web search tool");
|
|
}
|
|
|
|
await tool.execute({ query: "OpenClaw Grok proxy test" });
|
|
|
|
expect(firstFetchUrl(mockFetch)).toBe("https://api.x.ai/proxy/v1/responses");
|
|
});
|
|
|
|
it("reports malformed xAI web search JSON as a provider error", async () => {
|
|
const mockFetch = vi.fn((_input?: unknown, _init?: unknown) =>
|
|
Promise.resolve({
|
|
ok: true,
|
|
json: () => Promise.reject(new SyntaxError("Unexpected token")),
|
|
} as Response),
|
|
);
|
|
global.fetch = withFetchPreconnect(mockFetch);
|
|
const provider = createXaiWebSearchProvider();
|
|
const tool = provider.createTool({
|
|
config: {
|
|
plugins: {
|
|
entries: {
|
|
xai: {
|
|
config: {
|
|
webSearch: {
|
|
apiKey: "xai-test-key", // pragma: allowlist secret
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
if (!tool) {
|
|
throw new Error("Expected tool definition");
|
|
}
|
|
|
|
await expect(tool.execute({ query: "OpenClaw" })).rejects.toThrow(
|
|
"xAI web search failed: malformed JSON response",
|
|
);
|
|
});
|
|
|
|
it("rejects xAI web search success JSON without answer text", async () => {
|
|
const mockFetch = vi.fn((_input?: unknown, _init?: unknown) =>
|
|
Promise.resolve({
|
|
ok: true,
|
|
json: () => Promise.resolve({ output: [] }),
|
|
} as Response),
|
|
);
|
|
global.fetch = withFetchPreconnect(mockFetch);
|
|
const provider = createXaiWebSearchProvider();
|
|
const tool = provider.createTool({
|
|
config: {
|
|
plugins: {
|
|
entries: {
|
|
xai: {
|
|
config: {
|
|
webSearch: {
|
|
apiKey: "xai-test-key", // pragma: allowlist secret
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
if (!tool) {
|
|
throw new Error("Expected tool definition");
|
|
}
|
|
|
|
await expect(tool.execute({ query: "OpenClaw" })).rejects.toThrow(
|
|
"xAI web search failed: malformed JSON response",
|
|
);
|
|
});
|
|
|
|
it("normalizes deprecated grok 4.20 beta model ids to GA ids", () => {
|
|
expect(
|
|
resolveXaiWebSearchModel({
|
|
grok: { model: "grok-4.20-experimental-beta-0304-reasoning" },
|
|
}),
|
|
).toBe("grok-4.20-beta-latest-reasoning");
|
|
expect(
|
|
resolveXaiWebSearchModel({
|
|
grok: { model: "grok-4.20-experimental-beta-0304-non-reasoning" },
|
|
}),
|
|
).toBe("grok-4.20-beta-latest-non-reasoning");
|
|
});
|
|
|
|
it("defaults inlineCitations to false", () => {
|
|
expect(resolveXaiInlineCitations({})).toBe(false);
|
|
expect(resolveXaiInlineCitations(undefined)).toBe(false);
|
|
});
|
|
|
|
it("respects inlineCitations config", () => {
|
|
expect(resolveXaiInlineCitations({ grok: { inlineCitations: true } })).toBe(true);
|
|
expect(resolveXaiInlineCitations({ grok: { inlineCitations: false } })).toBe(false);
|
|
});
|
|
|
|
it("builds wrapped payloads with optional inline citations", () => {
|
|
const payload = testing.buildXaiWebSearchPayload({
|
|
query: "q",
|
|
provider: "grok",
|
|
model: "grok-4-fast",
|
|
tookMs: 12,
|
|
content: "body",
|
|
citations: ["https://a.test"],
|
|
});
|
|
expect(payload.query).toBe("q");
|
|
expect(payload.provider).toBe("grok");
|
|
expect(payload.model).toBe("grok-4-fast");
|
|
expect(payload.tookMs).toBe(12);
|
|
expect(payload.citations).toEqual(["https://a.test"]);
|
|
const externalContent = payload.externalContent as { wrapped?: boolean } | undefined;
|
|
expect(externalContent?.wrapped).toBe(true);
|
|
});
|
|
|
|
it("converts internal xAI timeout aborts into structured tool errors", () => {
|
|
const abort = new DOMException("This operation was aborted", "AbortError");
|
|
|
|
expect(() => wrapXaiWebSearchError(abort, 60)).toThrow("xAI web search timed out after 60s");
|
|
|
|
try {
|
|
wrapXaiWebSearchError(abort, 60);
|
|
} catch (error) {
|
|
expect(error).toBeInstanceOf(Error);
|
|
expect((error as Error).name).toBe("Error");
|
|
expect((error as Error).cause).toBe(abort);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("xai web search response parsing", () => {
|
|
it("extracts content from Responses API message blocks", () => {
|
|
const result = extractXaiWebSearchContent({
|
|
output: [
|
|
{
|
|
type: "message",
|
|
content: [{ type: "output_text", text: "hello from output" }],
|
|
},
|
|
],
|
|
});
|
|
expect(result.text).toBe("hello from output");
|
|
expect(result.annotationCitations).toStrictEqual([]);
|
|
});
|
|
|
|
it("extracts url_citation annotations from content blocks", () => {
|
|
const result = extractXaiWebSearchContent({
|
|
output: [
|
|
{
|
|
type: "message",
|
|
content: [
|
|
{
|
|
type: "output_text",
|
|
text: "hello with citations",
|
|
annotations: [
|
|
{ type: "url_citation", url: "https://example.com/a" },
|
|
{ type: "url_citation", url: "https://example.com/b" },
|
|
{ type: "url_citation", url: "https://example.com/a" },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
expect(result.text).toBe("hello with citations");
|
|
expect(result.annotationCitations).toEqual(["https://example.com/a", "https://example.com/b"]);
|
|
});
|
|
|
|
it("falls back to deprecated output_text", () => {
|
|
const result = extractXaiWebSearchContent({ output_text: "hello from output_text" });
|
|
expect(result.text).toBe("hello from output_text");
|
|
expect(result.annotationCitations).toStrictEqual([]);
|
|
});
|
|
|
|
it("returns undefined text when no content found", () => {
|
|
const result = extractXaiWebSearchContent({});
|
|
expect(result.text).toBeUndefined();
|
|
expect(result.annotationCitations).toStrictEqual([]);
|
|
});
|
|
|
|
it("extracts output_text blocks directly in output array", () => {
|
|
const result = extractXaiWebSearchContent({
|
|
output: [
|
|
{ type: "web_search_call" },
|
|
{
|
|
type: "output_text",
|
|
text: "direct output text",
|
|
annotations: [{ type: "url_citation", url: "https://example.com/direct" }],
|
|
},
|
|
],
|
|
});
|
|
expect(result.text).toBe("direct output text");
|
|
expect(result.annotationCitations).toEqual(["https://example.com/direct"]);
|
|
});
|
|
});
|
|
|
|
describe("xai provider models", () => {
|
|
it("publishes only current selectable chat models newest first", () => {
|
|
expect(buildXaiCatalogModels().map((model) => model.id)).toEqual([
|
|
"grok-build-0.1",
|
|
"grok-4.3",
|
|
"grok-4.20-beta-latest-reasoning",
|
|
"grok-4.20-beta-latest-non-reasoning",
|
|
]);
|
|
});
|
|
|
|
it("publishes Grok 4.3 as the default chat model", () => {
|
|
expectCatalogEntry("grok-4.3", {
|
|
id: "grok-4.3",
|
|
reasoning: true,
|
|
input: ["text", "image"],
|
|
contextWindow: 1_000_000,
|
|
maxTokens: 64_000,
|
|
cost: { input: 1.25, output: 2.5, cacheRead: 0.2, cacheWrite: 0 },
|
|
});
|
|
});
|
|
|
|
it("keeps retired Grok fast slugs resolving for compatibility", () => {
|
|
expectCatalogEntry("grok-4-1-fast", {
|
|
id: "grok-4-1-fast",
|
|
reasoning: true,
|
|
input: ["text", "image"],
|
|
contextWindow: 2_000_000,
|
|
maxTokens: 30_000,
|
|
});
|
|
});
|
|
|
|
it("resolves Grok Build and its official code aliases", () => {
|
|
const expected = {
|
|
id: "grok-build-0.1",
|
|
reasoning: true,
|
|
input: ["text", "image"],
|
|
contextWindow: 256_000,
|
|
cost: { input: 1, output: 2, cacheRead: 0.2, cacheWrite: 0 },
|
|
};
|
|
expectCatalogEntry("grok-build-0.1", expected);
|
|
expectCatalogEntry("grok-code-fast-1", expected);
|
|
expectCatalogEntry("grok-code-fast", expected);
|
|
expectCatalogEntry("grok-code-fast-1-0825", expected);
|
|
});
|
|
|
|
it("publishes Grok 4.20 reasoning and non-reasoning models", () => {
|
|
expectCatalogEntry("grok-4.20-beta-latest-reasoning", {
|
|
id: "grok-4.20-beta-latest-reasoning",
|
|
reasoning: true,
|
|
input: ["text", "image"],
|
|
contextWindow: 2_000_000,
|
|
});
|
|
expectCatalogEntry("grok-4.20-beta-latest-non-reasoning", {
|
|
id: "grok-4.20-beta-latest-non-reasoning",
|
|
reasoning: false,
|
|
contextWindow: 2_000_000,
|
|
});
|
|
});
|
|
|
|
it("keeps older Grok aliases resolving with current limits", () => {
|
|
expectCatalogEntry("grok-4-1-fast-reasoning", {
|
|
id: "grok-4-1-fast-reasoning",
|
|
reasoning: true,
|
|
contextWindow: 2_000_000,
|
|
maxTokens: 30_000,
|
|
});
|
|
expectCatalogEntry("grok-4.20-reasoning", {
|
|
id: "grok-4.20-reasoning",
|
|
reasoning: true,
|
|
contextWindow: 2_000_000,
|
|
maxTokens: 30_000,
|
|
});
|
|
});
|
|
|
|
it("publishes the remaining Grok 3 family in the OpenClaw catalog", () => {
|
|
expectCatalogEntry("grok-3-mini-fast", {
|
|
id: "grok-3-mini-fast",
|
|
reasoning: true,
|
|
contextWindow: 131_072,
|
|
maxTokens: 8_192,
|
|
});
|
|
expectCatalogEntry("grok-3-fast", {
|
|
id: "grok-3-fast",
|
|
reasoning: false,
|
|
contextWindow: 131_072,
|
|
maxTokens: 8_192,
|
|
});
|
|
});
|
|
|
|
it("marks current Grok families as modern while excluding multi-agent ids", () => {
|
|
expect(isModernXaiModel("grok-4.3")).toBe(true);
|
|
expect(isModernXaiModel("grok-build-0.1")).toBe(true);
|
|
expect(isModernXaiModel("grok-4.20-beta-latest-reasoning")).toBe(true);
|
|
expect(isModernXaiModel("grok-code-fast-1")).toBe(true);
|
|
expect(isModernXaiModel("grok-3-mini-fast")).toBe(false);
|
|
expect(isModernXaiModel("grok-4.20-multi-agent-experimental-beta-0304")).toBe(false);
|
|
});
|
|
|
|
it("builds forward-compatible runtime models for newer Grok ids", () => {
|
|
const grok41 = resolveXaiForwardCompatModel({
|
|
providerId: "xai",
|
|
ctx: {
|
|
provider: "xai",
|
|
modelId: "grok-4-1-fast",
|
|
modelRegistry: { find: () => null } as never,
|
|
providerConfig: {
|
|
api: "openai-responses",
|
|
baseUrl: "https://api.x.ai/v1",
|
|
},
|
|
},
|
|
});
|
|
const grok420 = resolveXaiForwardCompatModel({
|
|
providerId: "xai",
|
|
ctx: {
|
|
provider: "xai",
|
|
modelId: "grok-4.20-beta-latest-reasoning",
|
|
modelRegistry: { find: () => null } as never,
|
|
providerConfig: {
|
|
api: "openai-responses",
|
|
baseUrl: "https://api.x.ai/v1",
|
|
},
|
|
},
|
|
});
|
|
const grok43Alias = resolveXaiForwardCompatModel({
|
|
providerId: "xai",
|
|
ctx: {
|
|
provider: "xai",
|
|
modelId: "grok-4.3-latest",
|
|
modelRegistry: { find: () => null } as never,
|
|
providerConfig: {
|
|
api: "openai-responses",
|
|
baseUrl: "https://api.x.ai/v1",
|
|
},
|
|
},
|
|
});
|
|
const grok3Mini = resolveXaiForwardCompatModel({
|
|
providerId: "xai",
|
|
ctx: {
|
|
provider: "xai",
|
|
modelId: "grok-3-mini-fast",
|
|
modelRegistry: { find: () => null } as never,
|
|
providerConfig: {
|
|
api: "openai-responses",
|
|
baseUrl: "https://api.x.ai/v1",
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(grok41?.provider).toBe("xai");
|
|
expect(grok41?.id).toBe("grok-4-1-fast");
|
|
expect(grok41?.api).toBe("openai-responses");
|
|
expect(grok41?.baseUrl).toBe("https://api.x.ai/v1");
|
|
expect(grok41?.reasoning).toBe(true);
|
|
expect(grok41?.contextWindow).toBe(2_000_000);
|
|
expect(grok41?.maxTokens).toBe(30_000);
|
|
|
|
expect(grok43Alias?.provider).toBe("xai");
|
|
expect(grok43Alias?.id).toBe("grok-4.3-latest");
|
|
expect(grok43Alias?.api).toBe("openai-responses");
|
|
expect(grok43Alias?.baseUrl).toBe("https://api.x.ai/v1");
|
|
expect(grok43Alias?.reasoning).toBe(true);
|
|
expect(grok43Alias?.thinkingLevelMap).toEqual({
|
|
off: null,
|
|
minimal: "low",
|
|
low: "low",
|
|
medium: "medium",
|
|
high: "high",
|
|
xhigh: "high",
|
|
});
|
|
expect(grok43Alias?.input).toEqual(["text", "image"]);
|
|
expect(grok43Alias?.contextWindow).toBe(1_000_000);
|
|
expect(grok43Alias?.maxTokens).toBe(64_000);
|
|
|
|
expect(grok420?.provider).toBe("xai");
|
|
expect(grok420?.id).toBe("grok-4.20-beta-latest-reasoning");
|
|
expect(grok420?.api).toBe("openai-responses");
|
|
expect(grok420?.baseUrl).toBe("https://api.x.ai/v1");
|
|
expect(grok420?.reasoning).toBe(true);
|
|
expect(grok420?.input).toEqual(["text", "image"]);
|
|
expect(grok420?.contextWindow).toBe(2_000_000);
|
|
expect(grok420?.maxTokens).toBe(30_000);
|
|
|
|
expect(grok3Mini?.provider).toBe("xai");
|
|
expect(grok3Mini?.id).toBe("grok-3-mini-fast");
|
|
expect(grok3Mini?.api).toBe("openai-responses");
|
|
expect(grok3Mini?.baseUrl).toBe("https://api.x.ai/v1");
|
|
expect(grok3Mini?.reasoning).toBe(true);
|
|
expect(grok3Mini?.contextWindow).toBe(131_072);
|
|
expect(grok3Mini?.maxTokens).toBe(8_192);
|
|
});
|
|
|
|
it("refuses the unsupported multi-agent endpoint ids", () => {
|
|
const model = resolveXaiForwardCompatModel({
|
|
providerId: "xai",
|
|
ctx: {
|
|
provider: "xai",
|
|
modelId: "grok-4.20-multi-agent-experimental-beta-0304",
|
|
modelRegistry: { find: () => null } as never,
|
|
providerConfig: {
|
|
api: "openai-responses",
|
|
baseUrl: "https://api.x.ai/v1",
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(model).toBeUndefined();
|
|
});
|
|
});
|