refactor(xai): move bundled xai runtime into plugin

Co-authored-by: Harold Hunt <harold@pwrdrvr.com>
This commit is contained in:
Peter Steinberger
2026-03-28 05:01:59 +00:00
parent 85064256a2
commit c4e6fdf94d
25 changed files with 655 additions and 527 deletions

View File

@@ -4,6 +4,7 @@ import { applyXaiModelCompat, buildXaiProvider } from "./api.js";
import { applyXaiConfig, XAI_DEFAULT_MODEL_REF } from "./onboard.js";
import { isModernXaiModel, resolveXaiForwardCompatModel } from "./provider-models.js";
import {
createXaiFastModeWrapper,
createXaiToolCallArgumentDecodingWrapper,
createXaiToolPayloadCompatibilityWrapper,
} from "./stream.js";
@@ -47,13 +48,14 @@ export default defineSingleProviderPluginEntry({
tool_stream: true,
};
},
wrapStreamFn: (ctx) =>
createToolStreamWrapper(
createXaiToolCallArgumentDecodingWrapper(
createXaiToolPayloadCompatibilityWrapper(ctx.streamFn),
),
ctx.extraParams?.tool_stream !== false,
),
wrapStreamFn: (ctx) => {
let streamFn = createXaiToolPayloadCompatibilityWrapper(ctx.streamFn);
if (typeof ctx.extraParams?.fastMode === "boolean") {
streamFn = createXaiFastModeWrapper(streamFn, ctx.extraParams.fastMode);
}
streamFn = createXaiToolCallArgumentDecodingWrapper(streamFn);
return createToolStreamWrapper(streamFn, ctx.extraParams?.tool_stream !== false);
},
normalizeResolvedModel: ({ model }) => applyXaiModelCompat(model),
resolveDynamicModel: (ctx) => resolveXaiForwardCompatModel({ providerId: PROVIDER_ID, ctx }),
isModernModelRef: ({ modelId }) => isModernXaiModel(modelId),

View File

@@ -1,60 +0,0 @@
import { describe, expect, it } from "vitest";
import { withEnv } from "../../../test/helpers/extensions/env.js";
import { __testing } from "./grok-web-search-provider.js";
describe("grok web search provider", () => {
it("uses config apiKey when provided", () => {
expect(__testing.resolveGrokApiKey({ apiKey: "xai-test-key" })).toBe("xai-test-key");
});
it("falls back to env apiKey", () => {
withEnv({ XAI_API_KEY: "xai-env-key" }, () => {
expect(__testing.resolveGrokApiKey({})).toBe("xai-env-key");
});
});
it("uses config model when provided", () => {
expect(__testing.resolveGrokModel({ model: "grok-4-fast" })).toBe("grok-4-fast");
});
it("normalizes deprecated grok 4.20 beta ids to GA ids", () => {
expect(
__testing.resolveGrokModel({ model: "grok-4.20-experimental-beta-0304-reasoning" }),
).toBe("grok-4.20-beta-latest-reasoning");
expect(
__testing.resolveGrokModel({ model: "grok-4.20-experimental-beta-0304-non-reasoning" }),
).toBe("grok-4.20-beta-latest-non-reasoning");
});
it("falls back to default model", () => {
expect(__testing.resolveGrokModel({})).toBe("grok-4-1-fast");
});
it("resolves inline citations flag", () => {
expect(__testing.resolveGrokInlineCitations({ inlineCitations: true })).toBe(true);
expect(__testing.resolveGrokInlineCitations({ inlineCitations: false })).toBe(false);
expect(__testing.resolveGrokInlineCitations({})).toBe(false);
});
it("extracts content and annotation citations", () => {
expect(
__testing.extractGrokContent({
output: [
{
type: "message",
content: [
{
type: "output_text",
text: "Result",
annotations: [{ type: "url_citation", url: "https://example.com" }],
},
],
},
],
}),
).toEqual({
text: "Result",
annotationCitations: ["https://example.com"],
});
});
});

View File

@@ -1,171 +0,0 @@
import { Type } from "@sinclair/typebox";
import {
buildSearchCacheKey,
buildUnsupportedSearchFilterResponse,
DEFAULT_SEARCH_COUNT,
getScopedCredentialValue,
MAX_SEARCH_COUNT,
readCachedSearchPayload,
readConfiguredSecretString,
readNumberParam,
readProviderEnvValue,
readStringParam,
mergeScopedSearchConfig,
resolveProviderWebSearchPluginConfig,
resolveSearchCacheTtlMs,
resolveSearchCount,
resolveSearchTimeoutSeconds,
setScopedCredentialValue,
setProviderWebSearchPluginConfigValue,
type SearchConfigRecord,
type WebSearchProviderPlugin,
type WebSearchProviderToolDefinition,
writeCachedSearchPayload,
} from "openclaw/plugin-sdk/provider-web-search";
import {
buildXaiWebSearchPayload,
extractXaiWebSearchContent,
requestXaiWebSearch,
resolveXaiInlineCitations,
resolveXaiSearchConfig,
resolveXaiWebSearchModel,
} from "./web-search-shared.js";
function resolveGrokApiKey(grok?: Record<string, unknown>): string | undefined {
return (
readConfiguredSecretString(grok?.apiKey, "tools.web.search.grok.apiKey") ??
readProviderEnvValue(["XAI_API_KEY"])
);
}
function createGrokSchema() {
return Type.Object({
query: Type.String({ description: "Search query string." }),
count: Type.Optional(
Type.Number({
description: "Number of results to return (1-10).",
minimum: 1,
maximum: MAX_SEARCH_COUNT,
}),
),
country: Type.Optional(Type.String({ description: "Not supported by Grok." })),
language: Type.Optional(Type.String({ description: "Not supported by Grok." })),
freshness: Type.Optional(Type.String({ description: "Not supported by Grok." })),
date_after: Type.Optional(Type.String({ description: "Not supported by Grok." })),
date_before: Type.Optional(Type.String({ description: "Not supported by Grok." })),
});
}
function createGrokToolDefinition(
searchConfig?: SearchConfigRecord,
): WebSearchProviderToolDefinition {
return {
description:
"Search the web using xAI Grok. Returns AI-synthesized answers with citations from real-time web search.",
parameters: createGrokSchema(),
execute: async (args) => {
const params = args as Record<string, unknown>;
const unsupportedResponse = buildUnsupportedSearchFilterResponse(params, "grok");
if (unsupportedResponse) {
return unsupportedResponse;
}
const grokConfig = resolveXaiSearchConfig(searchConfig);
const apiKey = resolveGrokApiKey(grokConfig);
if (!apiKey) {
return {
error: "missing_xai_api_key",
message:
"web_search (grok) needs an xAI API key. Set XAI_API_KEY in the Gateway environment, or configure tools.web.search.grok.apiKey.",
docs: "https://docs.openclaw.ai/tools/web",
};
}
const query = readStringParam(params, "query", { required: true });
const count =
readNumberParam(params, "count", { integer: true }) ??
searchConfig?.maxResults ??
undefined;
const model = resolveXaiWebSearchModel(searchConfig);
const inlineCitations = resolveXaiInlineCitations(searchConfig);
const cacheKey = buildSearchCacheKey([
"grok",
query,
resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
model,
inlineCitations,
]);
const cached = readCachedSearchPayload(cacheKey);
if (cached) {
return cached;
}
const start = Date.now();
const result = await requestXaiWebSearch({
query,
apiKey,
model,
timeoutSeconds: resolveSearchTimeoutSeconds(searchConfig),
inlineCitations,
});
const payload = buildXaiWebSearchPayload({
query,
provider: "grok",
model,
tookMs: Date.now() - start,
content: result.content,
citations: result.citations,
inlineCitations: result.inlineCitations,
});
writeCachedSearchPayload(cacheKey, payload, resolveSearchCacheTtlMs(searchConfig));
return payload;
},
};
}
export function createGrokWebSearchProvider(): WebSearchProviderPlugin {
return {
id: "grok",
label: "Grok (xAI)",
hint: "Requires xAI API key · xAI web-grounded responses",
credentialLabel: "xAI API key",
envVars: ["XAI_API_KEY"],
placeholder: "xai-...",
signupUrl: "https://console.x.ai/",
docsUrl: "https://docs.openclaw.ai/tools/web",
autoDetectOrder: 30,
credentialPath: "plugins.entries.xai.config.webSearch.apiKey",
inactiveSecretPaths: ["plugins.entries.xai.config.webSearch.apiKey"],
getCredentialValue: (searchConfig) => getScopedCredentialValue(searchConfig, "grok"),
setCredentialValue: (searchConfigTarget, value) =>
setScopedCredentialValue(searchConfigTarget, "grok", value),
getConfiguredCredentialValue: (config) =>
resolveProviderWebSearchPluginConfig(config, "xai")?.apiKey,
setConfiguredCredentialValue: (configTarget, value) => {
setProviderWebSearchPluginConfigValue(configTarget, "xai", "apiKey", value);
},
createTool: (ctx) =>
createGrokToolDefinition(
mergeScopedSearchConfig(
ctx.searchConfig as SearchConfigRecord | undefined,
"grok",
resolveProviderWebSearchPluginConfig(ctx.config, "xai"),
) as SearchConfigRecord | undefined,
),
};
}
export const __testing = {
resolveGrokApiKey,
resolveGrokModel: (grok?: Record<string, unknown>) =>
resolveXaiWebSearchModel(grok ? { grok } : undefined),
resolveGrokInlineCitations: (grok?: Record<string, unknown>) =>
resolveXaiInlineCitations(grok ? { grok } : undefined),
extractGrokContent: extractXaiWebSearchContent,
extractXaiWebSearchContent,
resolveXaiInlineCitations,
resolveXaiSearchConfig,
resolveXaiWebSearchModel,
requestXaiWebSearch,
buildXaiWebSearchPayload,
} as const;

View File

@@ -0,0 +1,69 @@
import type { StreamFn } from "@mariozechner/pi-agent-core";
import type { Context, Model } from "@mariozechner/pi-ai";
import { describe, expect, it } from "vitest";
import { createXaiFastModeWrapper, createXaiToolPayloadCompatibilityWrapper } from "./stream.js";
function captureWrappedModelId(params: { modelId: string; fastMode: boolean }): string {
let capturedModelId = "";
const baseStreamFn: StreamFn = (model) => {
capturedModelId = model.id;
return {} as ReturnType<StreamFn>;
};
const wrapped = createXaiFastModeWrapper(baseStreamFn, params.fastMode);
void wrapped(
{
api: "openai-completions",
provider: "xai",
id: params.modelId,
} as Model<"openai-completions">,
{ messages: [] } as Context,
{},
);
return capturedModelId;
}
describe("xai stream wrappers", () => {
it("rewrites supported Grok models to fast variants when fast mode is enabled", () => {
expect(captureWrappedModelId({ modelId: "grok-3", fastMode: true })).toBe("grok-3-fast");
expect(captureWrappedModelId({ modelId: "grok-4", fastMode: true })).toBe("grok-4-fast");
});
it("leaves unsupported or disabled models unchanged", () => {
expect(captureWrappedModelId({ modelId: "grok-3-fast", fastMode: true })).toBe("grok-3-fast");
expect(captureWrappedModelId({ modelId: "grok-3", fastMode: false })).toBe("grok-3");
});
it("strips function.strict from tool payloads", () => {
const payload = {
tools: [
{
type: "function",
function: {
name: "write",
parameters: { type: "object", properties: {} },
strict: true,
},
},
],
};
const baseStreamFn: StreamFn = (_model, _context, options) => {
options?.onPayload?.(payload, {} as Model<"openai-completions">);
return {} as ReturnType<StreamFn>;
};
const wrapped = createXaiToolPayloadCompatibilityWrapper(baseStreamFn);
void wrapped(
{
api: "openai-completions",
provider: "xai",
id: "grok-4-1-fast-reasoning",
} as Model<"openai-completions">,
{ messages: [] } as Context,
{},
);
expect(payload.tools[0]?.function).not.toHaveProperty("strict");
});
});

View File

@@ -1,6 +1,20 @@
import type { StreamFn } from "@mariozechner/pi-agent-core";
import { streamSimple } from "@mariozechner/pi-ai";
const XAI_FAST_MODEL_IDS = new Map<string, string>([
["grok-3", "grok-3-fast"],
["grok-3-mini", "grok-3-mini-fast"],
["grok-4", "grok-4-fast"],
["grok-4-0709", "grok-4-fast"],
]);
function resolveXaiFastModelId(modelId: unknown): string | undefined {
if (typeof modelId !== "string") {
return undefined;
}
return XAI_FAST_MODEL_IDS.get(modelId.trim());
}
function stripUnsupportedStrictFlag(tool: unknown): unknown {
if (!tool || typeof tool !== "object") {
return tool;
@@ -40,6 +54,25 @@ export function createXaiToolPayloadCompatibilityWrapper(
};
}
export function createXaiFastModeWrapper(
baseStreamFn: StreamFn | undefined,
fastMode: boolean,
): StreamFn {
const underlying = baseStreamFn ?? streamSimple;
return (model, context, options) => {
if (!fastMode || model.api !== "openai-completions" || model.provider !== "xai") {
return underlying(model, context, options);
}
const fastModelId = resolveXaiFastModelId(model.id);
if (!fastModelId) {
return underlying(model, context, options);
}
return underlying({ ...model, id: fastModelId }, context, options);
};
}
function decodeHtmlEntities(value: string): string {
return value
.replaceAll("&quot;", '"')

View File

@@ -1 +0,0 @@
export { __testing } from "./src/grok-web-search-provider.js";

View File

@@ -1,44 +1,109 @@
import {
getScopedCredentialValue,
resolveWebSearchProviderCredential,
} from "openclaw/plugin-sdk/provider-web-search";
import { describe, expect, it } from "vitest";
import { withEnv } from "../../test/helpers/extensions/env.js";
import { resolveXaiCatalogEntry } from "./model-definitions.js";
import { isModernXaiModel, resolveXaiForwardCompatModel } from "./provider-models.js";
import { __testing as grokProviderTesting } from "./src/grok-web-search-provider.js";
import { __testing } from "./web-search.js";
import { __testing, createXaiWebSearchProvider } from "./web-search.js";
const { extractXaiWebSearchContent, resolveXaiInlineCitations, resolveXaiWebSearchModel } =
__testing;
const {
extractXaiWebSearchContent,
resolveXaiInlineCitations,
resolveXaiToolSearchConfig,
resolveXaiWebSearchCredential,
resolveXaiWebSearchModel,
} = __testing;
describe("xai web search config resolution", () => {
it("prefers configured api keys and resolves grok scoped defaults", () => {
expect(grokProviderTesting.resolveGrokApiKey({ apiKey: "xai-secret" })).toBe("xai-secret");
expect(grokProviderTesting.resolveGrokModel()).toBe("grok-4-1-fast");
expect(grokProviderTesting.resolveGrokInlineCitations()).toBe(false);
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", () => {
const searchConfig = { grok: { apiKey: "xai-test-key" } }; // pragma: allowlist secret
expect(
resolveWebSearchProviderCredential({
credentialValue: getScopedCredentialValue(searchConfig, "grok"),
path: "tools.web.search.grok.apiKey",
envVars: ["XAI_API_KEY"],
}),
).toBe("xai-test-key");
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(
resolveWebSearchProviderCredential({
credentialValue: getScopedCredentialValue({}, "grok"),
path: "tools.web.search.grok.apiKey",
envVars: ["XAI_API_KEY"],
resolveXaiWebSearchCredential({
grok: {
apiKey: {
source: "env",
provider: "default",
id: "XAI_WEB_SEARCH_KEY",
},
},
}),
).toBeUndefined();
).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 throwing", async () => {
await withEnv({ XAI_API_KEY: undefined }, 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",
},
},
},
},
},
},
},
});
expect(maybeTool).toBeTruthy();
if (!maybeTool) {
throw new Error("expected xai web search tool");
}
await expect(maybeTool.execute({ query: "OpenClaw" })).resolves.toMatchObject({
error: "missing_xai_api_key",
});
});
});
@@ -78,7 +143,7 @@ describe("xai web search config resolution", () => {
it("builds wrapped payloads with optional inline citations", () => {
expect(
grokProviderTesting.buildXaiWebSearchPayload({
__testing.buildXaiWebSearchPayload({
query: "q",
provider: "grok",
model: "grok-4-fast",

View File

@@ -3,14 +3,18 @@ import {
DEFAULT_CACHE_TTL_MINUTES,
DEFAULT_TIMEOUT_SECONDS,
getScopedCredentialValue,
mergeScopedSearchConfig,
normalizeCacheKey,
readCache,
readNumberParam,
readStringParam,
resolveCacheTtlMs,
resolveProviderWebSearchPluginConfig,
resolveTimeoutSeconds,
resolveWebSearchProviderCredential,
setProviderWebSearchPluginConfigValue,
setScopedCredentialValue,
type SearchConfigRecord,
type WebSearchProviderPlugin,
writeCache,
} from "openclaw/plugin-sdk/provider-web-search";
@@ -67,6 +71,25 @@ function runXaiWebSearch(params: {
})();
}
function resolveXaiToolSearchConfig(ctx: {
config?: Record<string, unknown>;
searchConfig?: Record<string, unknown>;
}): SearchConfigRecord | undefined {
return mergeScopedSearchConfig(
ctx.searchConfig as SearchConfigRecord | undefined,
"grok",
resolveProviderWebSearchPluginConfig(ctx.config, "xai"),
) as SearchConfigRecord | undefined;
}
function resolveXaiWebSearchCredential(searchConfig?: SearchConfigRecord): string | undefined {
return resolveWebSearchProviderCredential({
credentialValue: getScopedCredentialValue(searchConfig, "grok"),
path: "tools.web.search.grok.apiKey",
envVars: ["XAI_API_KEY"],
});
}
export function createXaiWebSearchProvider(): WebSearchProviderPlugin {
return {
id: "grok",
@@ -85,61 +108,67 @@ export function createXaiWebSearchProvider(): WebSearchProviderPlugin {
getScopedCredentialValue(searchConfig, "grok"),
setCredentialValue: (searchConfigTarget: Record<string, unknown>, value: unknown) =>
setScopedCredentialValue(searchConfigTarget, "grok", value),
createTool: (ctx: { searchConfig?: Record<string, unknown> }) => ({
description:
"Search the web using xAI Grok. Returns AI-synthesized answers with citations from real-time web search.",
parameters: Type.Object({
query: Type.String({ description: "Search query string." }),
count: Type.Optional(
Type.Number({
description: "Number of results to return (1-10).",
minimum: 1,
maximum: 10,
}),
),
}),
execute: async (args: Record<string, unknown>) => {
const apiKey = resolveWebSearchProviderCredential({
credentialValue: getScopedCredentialValue(ctx.searchConfig, "grok"),
path: "tools.web.search.grok.apiKey",
envVars: ["XAI_API_KEY"],
});
if (!apiKey) {
return {
error: "missing_xai_api_key",
message:
"web_search (grok) needs an xAI API key. Set XAI_API_KEY in the Gateway environment, or configure plugins.entries.xai.config.webSearch.apiKey.",
docs: "https://docs.openclaw.ai/tools/web",
};
}
const query = readStringParam(args, "query", { required: true });
void readNumberParam(args, "count", { integer: true });
return await runXaiWebSearch({
query,
model: resolveXaiWebSearchModel(ctx.searchConfig),
apiKey,
timeoutSeconds: resolveTimeoutSeconds(
(ctx.searchConfig?.timeoutSeconds as number | undefined) ?? undefined,
DEFAULT_TIMEOUT_SECONDS,
getConfiguredCredentialValue: (config) =>
resolveProviderWebSearchPluginConfig(config, "xai")?.apiKey,
setConfiguredCredentialValue: (configTarget, value) => {
setProviderWebSearchPluginConfigValue(configTarget, "xai", "apiKey", value);
},
createTool: (ctx) => {
const searchConfig = resolveXaiToolSearchConfig(ctx);
return {
description:
"Search the web using xAI Grok. Returns AI-synthesized answers with citations from real-time web search.",
parameters: Type.Object({
query: Type.String({ description: "Search query string." }),
count: Type.Optional(
Type.Number({
description: "Number of results to return (1-10).",
minimum: 1,
maximum: 10,
}),
),
inlineCitations: resolveXaiInlineCitations(ctx.searchConfig),
cacheTtlMs: resolveCacheTtlMs(
(ctx.searchConfig?.cacheTtlMinutes as number | undefined) ?? undefined,
DEFAULT_CACHE_TTL_MINUTES,
),
});
},
}),
}),
execute: async (args: Record<string, unknown>) => {
const apiKey = resolveXaiWebSearchCredential(searchConfig);
if (!apiKey) {
return {
error: "missing_xai_api_key",
message:
"web_search (grok) needs an xAI API key. Set XAI_API_KEY in the Gateway environment, or configure plugins.entries.xai.config.webSearch.apiKey.",
docs: "https://docs.openclaw.ai/tools/web",
};
}
const query = readStringParam(args, "query", { required: true });
void readNumberParam(args, "count", { integer: true });
return await runXaiWebSearch({
query,
model: resolveXaiWebSearchModel(searchConfig),
apiKey,
timeoutSeconds: resolveTimeoutSeconds(
(searchConfig?.timeoutSeconds as number | undefined) ?? undefined,
DEFAULT_TIMEOUT_SECONDS,
),
inlineCitations: resolveXaiInlineCitations(searchConfig),
cacheTtlMs: resolveCacheTtlMs(
(searchConfig?.cacheTtlMinutes as number | undefined) ?? undefined,
DEFAULT_CACHE_TTL_MINUTES,
),
});
},
};
},
};
}
export const __testing = {
buildXaiWebSearchPayload,
extractXaiWebSearchContent,
resolveXaiToolSearchConfig,
resolveXaiInlineCitations,
resolveXaiWebSearchCredential,
resolveXaiWebSearchModel,
requestXaiWebSearch,
};

View File

@@ -9,8 +9,8 @@ vi.mock("../plugins/provider-runtime.js", () => ({
resolveProviderModernModelRef: providerRuntimeMocks.resolveProviderModernModelRef,
}));
import { normalizeModelCompat } from "../plugins/provider-model-compat.js";
import { isHighSignalLiveModelRef, isModernModelRef } from "./live-model-filter.js";
import { normalizeModelCompat } from "./model-compat.js";
const baseModel = (): Model<Api> =>
({

View File

@@ -1,6 +1,7 @@
import type { StreamFn } from "@mariozechner/pi-agent-core";
import type { Context, Model, SimpleStreamOptions } from "@mariozechner/pi-ai";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createXaiFastModeWrapper } from "../../extensions/xai/stream.js";
import { createConfiguredOllamaCompatNumCtxWrapper } from "../plugin-sdk/ollama.js";
import { __testing as extraParamsTesting } from "./pi-embedded-runner/extra-params.js";
import {
@@ -58,6 +59,12 @@ beforeEach(() => {
if (params.provider === "ollama") {
return createConfiguredOllamaCompatNumCtxWrapper(params.context);
}
if (params.provider === "xai") {
return createXaiFastModeWrapper(
params.context.streamFn,
params.context.extraParams?.fastMode === true,
);
}
if (params.provider !== "openrouter") {
return params.context.streamFn;
}

View File

@@ -38,7 +38,6 @@ import {
resolveOpenAIServiceTier,
} from "./openai-stream-wrappers.js";
import { streamWithPayloadPatch } from "./stream-payload-utils.js";
import { createXaiFastModeWrapper } from "./xai-stream-wrappers.js";
const defaultProviderRuntimeDeps = {
prepareProviderExtraParams: prepareProviderExtraParamsRuntime,
@@ -381,13 +380,6 @@ function applyPostPluginStreamWrappers(
ctx.agent.streamFn,
ctx.effectiveExtraParams.fastMode,
);
log.debug(
`applying xAI fast mode=${ctx.effectiveExtraParams.fastMode} for ${ctx.provider}/${ctx.modelId}`,
);
ctx.agent.streamFn = createXaiFastModeWrapper(
ctx.agent.streamFn,
ctx.effectiveExtraParams.fastMode,
);
}
const openAIFastMode = resolveOpenAIFastMode(ctx.effectiveExtraParams);

View File

@@ -1,5 +1,5 @@
import type { Api, Model } from "@mariozechner/pi-ai";
import { normalizeModelCompat } from "../model-compat.js";
import { normalizeModelCompat } from "../../plugins/provider-model-compat.js";
import { normalizeProviderId } from "../model-selection.js";
function isOpenAIApiBaseUrl(baseUrl?: string): boolean {

View File

@@ -3,6 +3,7 @@ import type { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
import type { OpenClawConfig } from "../../config/config.js";
import type { ModelDefinitionConfig } from "../../config/types.js";
import { resolveGoogleGenerativeAiTransport } from "../../plugin-sdk/google.js";
import { normalizeModelCompat } from "../../plugins/provider-model-compat.js";
import {
buildProviderUnknownModelHintWithPlugin,
clearProviderRuntimeHookCache,
@@ -14,7 +15,6 @@ import { resolveOpenClawAgentDir } from "../agent-paths.js";
import { DEFAULT_CONTEXT_TOKENS } from "../defaults.js";
import { buildModelAliasLines } from "../model-alias-lines.js";
import { isSecretRefHeaderValueMarker } from "../model-auth-markers.js";
import { normalizeModelCompat } from "../model-compat.js";
import { findNormalizedProviderValue, normalizeProviderId } from "../model-selection.js";
import {
buildSuppressedBuiltInModelError,

View File

@@ -26,6 +26,7 @@ import {
resolveTelegramReactionLevel,
} from "../../../plugin-sdk/telegram-runtime.js";
import { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js";
import { resolveToolCallArgumentsEncoding } from "../../../plugins/provider-model-compat.js";
import { isSubagentSessionKey } from "../../../routing/session-key.js";
import { buildTtsSystemPromptHint } from "../../../tts/tts.js";
import { resolveUserPath } from "../../../utils.js";
@@ -54,7 +55,6 @@ import { isTimeoutError } from "../../failover-error.js";
import { resolveImageSanitizationLimits } from "../../image-sanitization.js";
import { buildModelAliasLines } from "../../model-alias-lines.js";
import { resolveModelAuthMode } from "../../model-auth.js";
import { resolveToolCallArgumentsEncoding } from "../../model-compat.js";
import { resolveDefaultModelForAgent } from "../../model-selection.js";
import { supportsModelTools } from "../../model-tool-support.js";
import { createOpenAIWebSocketStreamFn, releaseWsSession } from "../../openai-ws-stream.js";

View File

@@ -1,39 +0,0 @@
import type { StreamFn } from "@mariozechner/pi-agent-core";
import type { Context, Model } from "@mariozechner/pi-ai";
import { describe, expect, it } from "vitest";
import { createXaiFastModeWrapper } from "./xai-stream-wrappers.js";
function captureWrappedModelId(params: { modelId: string; fastMode: boolean }): string {
let capturedModelId = "";
const baseStreamFn: StreamFn = (model) => {
capturedModelId = model.id;
return {} as ReturnType<StreamFn>;
};
const wrapped = createXaiFastModeWrapper(baseStreamFn, params.fastMode);
void wrapped(
{
api: "openai-completions",
provider: "xai",
id: params.modelId,
} as Model<"openai-completions">,
{ messages: [] } as Context,
{},
);
return capturedModelId;
}
describe("xai fast mode wrapper", () => {
it("rewrites Grok 3 models to fast variants", () => {
expect(captureWrappedModelId({ modelId: "grok-3", fastMode: true })).toBe("grok-3-fast");
expect(captureWrappedModelId({ modelId: "grok-3-mini", fastMode: true })).toBe(
"grok-3-mini-fast",
);
});
it("leaves unsupported or disabled models unchanged", () => {
expect(captureWrappedModelId({ modelId: "grok-3-fast", fastMode: true })).toBe("grok-3-fast");
expect(captureWrappedModelId({ modelId: "grok-3", fastMode: false })).toBe("grok-3");
});
});

View File

@@ -1,35 +0,0 @@
import type { StreamFn } from "@mariozechner/pi-agent-core";
import { streamSimple } from "@mariozechner/pi-ai";
const XAI_FAST_MODEL_IDS = new Map<string, string>([
["grok-3", "grok-3-fast"],
["grok-3-mini", "grok-3-mini-fast"],
["grok-4", "grok-4-fast"],
["grok-4-0709", "grok-4-fast"],
]);
function resolveXaiFastModelId(modelId: unknown): string | undefined {
if (typeof modelId !== "string") {
return undefined;
}
return XAI_FAST_MODEL_IDS.get(modelId.trim());
}
export function createXaiFastModeWrapper(
baseStreamFn: StreamFn | undefined,
fastMode: boolean,
): StreamFn {
const underlying = baseStreamFn ?? streamSimple;
return (model, context, options) => {
if (!fastMode || model.api !== "openai-completions" || model.provider !== "xai") {
return underlying(model, context, options);
}
const fastModelId = resolveXaiFastModelId(model.id);
if (!fastModelId) {
return underlying(model, context, options);
}
return underlying({ ...model, id: fastModelId }, context, options);
};
}

View File

@@ -4,6 +4,7 @@ import type { ModelCompatConfig } from "../config/types.models.js";
import type { ToolLoopDetectionConfig } from "../config/types.tools.js";
import { resolveMergedSafeBinProfileFixtures } from "../infra/exec-safe-bin-runtime-policy.js";
import { logWarn } from "../logger.js";
import { hasNativeWebSearchTool } from "../plugins/provider-model-compat.js";
import { getPluginToolMeta } from "../plugins/tools.js";
import { isSubagentSessionKey } from "../routing/session-key.js";
import { resolveGatewayMessageChannel } from "../utils/message-channel.js";
@@ -18,7 +19,6 @@ import {
import { listChannelAgentTools } from "./channel-tools.js";
import { resolveImageSanitizationLimits } from "./image-sanitization.js";
import type { ModelAuthMode } from "./model-auth.js";
import { hasNativeWebSearchTool } from "./model-compat.js";
import { createOpenClawTools } from "./openclaw-tools.js";
import { wrapToolWithAbortSignal } from "./pi-tools.abort.js";
import { wrapToolWithBeforeToolCallHook } from "./pi-tools.before-tool-call.js";

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { stripXaiUnsupportedKeywords } from "./clean-for-xai.js";
import { stripXaiUnsupportedKeywords } from "../../plugin-sdk/provider-tools.js";
describe("stripXaiUnsupportedKeywords", () => {
it("strips minLength and maxLength from string properties", () => {

View File

@@ -1,6 +0,0 @@
import {
stripXaiUnsupportedKeywords,
XAI_UNSUPPORTED_SCHEMA_KEYWORDS,
} from "../../plugin-sdk/provider-tools.js";
export { stripXaiUnsupportedKeywords, XAI_UNSUPPORTED_SCHEMA_KEYWORDS };

View File

@@ -1,4 +1,4 @@
import { normalizeResolvedSecretInputString } from "../../config/types.secrets.js";
import { normalizeSecretInputString, resolveSecretInputRef } from "../../config/types.secrets.js";
import { normalizeSecretInput } from "../../utils/normalize-secret-input.js";
export function resolveWebSearchProviderCredential(params: {
@@ -6,15 +6,22 @@ export function resolveWebSearchProviderCredential(params: {
path: string;
envVars: string[];
}): string | undefined {
const fromConfigRaw = normalizeResolvedSecretInputString({
value: params.credentialValue,
path: params.path,
});
const fromConfigRaw = normalizeSecretInputString(params.credentialValue);
const fromConfig = normalizeSecretInput(fromConfigRaw);
if (fromConfig) {
return fromConfig;
}
const credentialRef = resolveSecretInputRef({
value: params.credentialValue,
}).ref;
if (credentialRef?.source === "env") {
const fromEnvRef = normalizeSecretInput(process.env[credentialRef.id]);
if (fromEnvRef) {
return fromEnvRef;
}
}
for (const envVar of params.envVars) {
const fromEnv = normalizeSecretInput(process.env[envVar]);
if (fromEnv) {

View File

@@ -0,0 +1,121 @@
import type { Api, Model } from "@mariozechner/pi-ai";
import type { ModelCompatConfig } from "../config/types.models.js";
function extractModelCompat(
modelOrCompat: { compat?: unknown } | ModelCompatConfig | undefined,
): ModelCompatConfig | undefined {
if (!modelOrCompat || typeof modelOrCompat !== "object") {
return undefined;
}
if ("compat" in modelOrCompat) {
const compat = (modelOrCompat as { compat?: unknown }).compat;
return compat && typeof compat === "object" ? (compat as ModelCompatConfig) : undefined;
}
return modelOrCompat as ModelCompatConfig;
}
export function applyModelCompatPatch<T extends { compat?: ModelCompatConfig }>(
model: T,
patch: ModelCompatConfig,
): T {
const nextCompat = { ...model.compat, ...patch };
if (
model.compat &&
Object.entries(patch).every(
([key, value]) => model.compat?.[key as keyof ModelCompatConfig] === value,
)
) {
return model;
}
return {
...model,
compat: nextCompat,
};
}
export function hasToolSchemaProfile(
modelOrCompat: { compat?: unknown } | ModelCompatConfig | undefined,
profile: string,
): boolean {
return extractModelCompat(modelOrCompat)?.toolSchemaProfile === profile;
}
export function hasNativeWebSearchTool(
modelOrCompat: { compat?: unknown } | ModelCompatConfig | undefined,
): boolean {
return extractModelCompat(modelOrCompat)?.nativeWebSearchTool === true;
}
export function resolveToolCallArgumentsEncoding(
modelOrCompat: { compat?: unknown } | ModelCompatConfig | undefined,
): ModelCompatConfig["toolCallArgumentsEncoding"] | undefined {
return extractModelCompat(modelOrCompat)?.toolCallArgumentsEncoding;
}
function isOpenAiCompletionsModel(model: Model<Api>): model is Model<"openai-completions"> {
return model.api === "openai-completions";
}
function isOpenAINativeEndpoint(baseUrl: string): boolean {
try {
const host = new URL(baseUrl).hostname.toLowerCase();
return host === "api.openai.com";
} catch {
return false;
}
}
function isAnthropicMessagesModel(model: Model<Api>): model is Model<"anthropic-messages"> {
return model.api === "anthropic-messages";
}
function normalizeAnthropicBaseUrl(baseUrl: string): string {
return baseUrl.replace(/\/v1\/?$/, "");
}
export function normalizeModelCompat(model: Model<Api>): Model<Api> {
const baseUrl = model.baseUrl ?? "";
if (isAnthropicMessagesModel(model) && baseUrl) {
const normalized = normalizeAnthropicBaseUrl(baseUrl);
if (normalized !== baseUrl) {
return { ...model, baseUrl: normalized } as Model<"anthropic-messages">;
}
}
if (!isOpenAiCompletionsModel(model)) {
return model;
}
const compat = model.compat ?? undefined;
const needsForce = baseUrl ? !isOpenAINativeEndpoint(baseUrl) : false;
if (!needsForce) {
return model;
}
const forcedDeveloperRole = compat?.supportsDeveloperRole === true;
const hasStreamingUsageOverride = compat?.supportsUsageInStreaming !== undefined;
const targetStrictMode = compat?.supportsStrictMode ?? false;
if (
compat?.supportsDeveloperRole !== undefined &&
hasStreamingUsageOverride &&
compat?.supportsStrictMode !== undefined
) {
return model;
}
return {
...model,
compat: compat
? {
...compat,
supportsDeveloperRole: forcedDeveloperRole || false,
...(hasStreamingUsageOverride ? {} : { supportsUsageInStreaming: false }),
supportsStrictMode: targetStrictMode,
}
: {
supportsDeveloperRole: false,
supportsUsageInStreaming: false,
supportsStrictMode: false,
},
} as typeof model;
}

View File

@@ -1,4 +1,4 @@
import { normalizeModelCompat } from "../agents/model-compat.js";
import { normalizeModelCompat } from "./provider-model-compat.js";
import type { ProviderResolveDynamicModelContext, ProviderRuntimeModel } from "./types.js";
export function matchesExactOrPrefix(id: string, values: readonly string[]): boolean {

View File

@@ -1,9 +1,6 @@
import { afterEach, describe, expect, it } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { createEmptyPluginRegistry } from "../plugins/registry.js";
import { setActivePluginRegistry } from "../plugins/runtime.js";
import { activateSecretsRuntimeSnapshot, clearSecretsRuntimeSnapshot } from "../secrets/runtime.js";
import { runWebSearch } from "./runtime.js";
import type { PluginWebSearchProviderEntry } from "../plugins/types.js";
type TestPluginWebSearchConfig = {
webSearch?: {
@@ -11,37 +8,92 @@ type TestPluginWebSearchConfig = {
};
};
const { resolveBundledPluginWebSearchProvidersMock, resolveRuntimeWebSearchProvidersMock } =
vi.hoisted(() => ({
resolveBundledPluginWebSearchProvidersMock: vi.fn<() => PluginWebSearchProviderEntry[]>(
() => [],
),
resolveRuntimeWebSearchProvidersMock: vi.fn<() => PluginWebSearchProviderEntry[]>(() => []),
}));
vi.mock("../plugins/web-search-providers.js", () => ({
resolveBundledPluginWebSearchProviders: resolveBundledPluginWebSearchProvidersMock,
}));
vi.mock("../plugins/web-search-providers.runtime.js", () => ({
resolvePluginWebSearchProviders: resolveRuntimeWebSearchProvidersMock,
resolveRuntimeWebSearchProviders: resolveRuntimeWebSearchProvidersMock,
}));
function createProvider(params: {
pluginId: string;
id: string;
credentialPath: string;
autoDetectOrder?: number;
requiresCredential?: boolean;
getCredentialValue?: PluginWebSearchProviderEntry["getCredentialValue"];
getConfiguredCredentialValue?: PluginWebSearchProviderEntry["getConfiguredCredentialValue"];
createTool?: PluginWebSearchProviderEntry["createTool"];
}): PluginWebSearchProviderEntry {
return {
pluginId: params.pluginId,
id: params.id,
label: params.id,
hint: `${params.id} runtime provider`,
envVars: [`${params.id.toUpperCase()}_API_KEY`],
placeholder: `${params.id}-...`,
signupUrl: `https://example.com/${params.id}`,
credentialPath: params.credentialPath,
autoDetectOrder: params.autoDetectOrder,
requiresCredential: params.requiresCredential,
getCredentialValue: params.getCredentialValue ?? (() => undefined),
setCredentialValue: () => {},
getConfiguredCredentialValue: params.getConfiguredCredentialValue,
createTool:
params.createTool ??
(() => ({
description: params.id,
parameters: {},
execute: async (args) => ({ ...args, provider: params.id }),
})),
};
}
describe("web search runtime", () => {
let runWebSearch: typeof import("./runtime.js").runWebSearch;
let activateSecretsRuntimeSnapshot: typeof import("../secrets/runtime.js").activateSecretsRuntimeSnapshot;
let clearSecretsRuntimeSnapshot: typeof import("../secrets/runtime.js").clearSecretsRuntimeSnapshot;
beforeEach(async () => {
vi.resetModules();
resolveBundledPluginWebSearchProvidersMock.mockReset();
resolveRuntimeWebSearchProvidersMock.mockReset();
resolveBundledPluginWebSearchProvidersMock.mockReturnValue([]);
resolveRuntimeWebSearchProvidersMock.mockReturnValue([]);
({ runWebSearch } = await import("./runtime.js"));
({ activateSecretsRuntimeSnapshot, clearSecretsRuntimeSnapshot } =
await import("../secrets/runtime.js"));
});
afterEach(() => {
setActivePluginRegistry(createEmptyPluginRegistry());
clearSecretsRuntimeSnapshot();
});
it("executes searches through the active plugin registry", async () => {
const registry = createEmptyPluginRegistry();
registry.webSearchProviders.push({
pluginId: "custom-search",
pluginName: "Custom Search",
provider: {
resolveRuntimeWebSearchProvidersMock.mockReturnValue([
createProvider({
pluginId: "custom-search",
id: "custom",
label: "Custom Search",
hint: "Custom runtime provider",
envVars: ["CUSTOM_SEARCH_API_KEY"],
placeholder: "custom-...",
signupUrl: "https://example.com/signup",
credentialPath: "tools.web.search.custom.apiKey",
autoDetectOrder: 1,
getCredentialValue: () => "configured",
setCredentialValue: () => {},
createTool: () => ({
description: "custom",
parameters: {},
execute: async (args) => ({ ...args, ok: true }),
}),
},
source: "test",
});
setActivePluginRegistry(registry);
}),
]);
await expect(
runWebSearch({
@@ -55,48 +107,25 @@ describe("web search runtime", () => {
});
it("auto-detects a provider from canonical plugin-owned credentials", async () => {
const registry = createEmptyPluginRegistry();
registry.webSearchProviders.push({
const provider = createProvider({
pluginId: "custom-search",
pluginName: "Custom Search",
provider: {
id: "custom",
label: "Custom Search",
hint: "Custom runtime provider",
envVars: ["CUSTOM_SEARCH_API_KEY"],
placeholder: "custom-...",
signupUrl: "https://example.com/signup",
credentialPath: "plugins.entries.custom-search.config.webSearch.apiKey",
autoDetectOrder: 1,
getCredentialValue: () => undefined,
setCredentialValue: () => {},
getConfiguredCredentialValue: (config) => {
const pluginConfig = config?.plugins?.entries?.["custom-search"]?.config as
| TestPluginWebSearchConfig
| undefined;
return pluginConfig?.webSearch?.apiKey;
},
setConfiguredCredentialValue: (configTarget, value) => {
configTarget.plugins = {
...configTarget.plugins,
entries: {
...configTarget.plugins?.entries,
"custom-search": {
enabled: true,
config: { webSearch: { apiKey: value } },
},
},
};
},
createTool: () => ({
description: "custom",
parameters: {},
execute: async (args) => ({ ...args, ok: true }),
}),
id: "custom",
credentialPath: "plugins.entries.custom-search.config.webSearch.apiKey",
autoDetectOrder: 1,
getConfiguredCredentialValue: (config) => {
const pluginConfig = config?.plugins?.entries?.["custom-search"]?.config as
| TestPluginWebSearchConfig
| undefined;
return pluginConfig?.webSearch?.apiKey;
},
source: "test",
createTool: () => ({
description: "custom",
parameters: {},
execute: async (args) => ({ ...args, ok: true }),
}),
});
setActivePluginRegistry(registry);
resolveRuntimeWebSearchProvidersMock.mockReturnValue([provider]);
resolveBundledPluginWebSearchProvidersMock.mockReturnValue([provider]);
const config: OpenClawConfig = {
plugins: {
@@ -124,32 +153,68 @@ describe("web search runtime", () => {
});
});
it("treats non-env SecretRefs as configured credentials for provider auto-detect", async () => {
const provider = createProvider({
pluginId: "custom-search",
id: "custom",
credentialPath: "plugins.entries.custom-search.config.webSearch.apiKey",
autoDetectOrder: 1,
getConfiguredCredentialValue: (config) => {
const pluginConfig = config?.plugins?.entries?.["custom-search"]?.config as
| TestPluginWebSearchConfig
| undefined;
return pluginConfig?.webSearch?.apiKey;
},
createTool: () => ({
description: "custom",
parameters: {},
execute: async (args) => ({ ...args, ok: true }),
}),
});
resolveRuntimeWebSearchProvidersMock.mockReturnValue([provider]);
resolveBundledPluginWebSearchProvidersMock.mockReturnValue([provider]);
const config: OpenClawConfig = {
plugins: {
entries: {
"custom-search": {
enabled: true,
config: {
webSearch: {
apiKey: {
source: "file",
provider: "vault",
id: "/providers/custom-search/apiKey",
},
},
},
},
},
},
};
await expect(
runWebSearch({
config,
args: { query: "hello" },
}),
).resolves.toEqual({
provider: "custom",
result: { query: "hello", ok: true },
});
});
it("falls back to a keyless provider when no credentials are available", async () => {
const registry = createEmptyPluginRegistry();
registry.webSearchProviders.push({
pluginId: "duckduckgo",
pluginName: "DuckDuckGo",
provider: {
resolveRuntimeWebSearchProvidersMock.mockReturnValue([
createProvider({
pluginId: "duckduckgo",
id: "duckduckgo",
label: "DuckDuckGo Search (experimental)",
hint: "Keyless fallback",
requiresCredential: false,
envVars: [],
placeholder: "(no key needed)",
signupUrl: "https://duckduckgo.com/",
credentialPath: "",
autoDetectOrder: 100,
requiresCredential: false,
getCredentialValue: () => "duckduckgo-no-key-needed",
setCredentialValue: () => {},
createTool: () => ({
description: "duckduckgo",
parameters: {},
execute: async (args) => ({ ...args, provider: "duckduckgo" }),
}),
},
source: "test",
});
setActivePluginRegistry(registry);
}),
]);
await expect(
runWebSearch({
@@ -163,21 +228,13 @@ describe("web search runtime", () => {
});
it("prefers the active runtime-selected provider when callers omit runtime metadata", async () => {
const registry = createEmptyPluginRegistry();
registry.webSearchProviders.push({
pluginId: "alpha-search",
pluginName: "Alpha Search",
provider: {
resolveRuntimeWebSearchProvidersMock.mockReturnValue([
createProvider({
pluginId: "alpha-search",
id: "alpha",
label: "Alpha Search",
hint: "Alpha runtime provider",
envVars: ["ALPHA_SEARCH_API_KEY"],
placeholder: "alpha-...",
signupUrl: "https://example.com/alpha",
credentialPath: "tools.web.search.alpha.apiKey",
autoDetectOrder: 1,
getCredentialValue: () => "alpha-configured",
setCredentialValue: () => {},
createTool: ({ runtimeMetadata }) => ({
description: "alpha",
parameters: {},
@@ -187,23 +244,13 @@ describe("web search runtime", () => {
runtimeSelectedProvider: runtimeMetadata?.selectedProvider,
}),
}),
},
source: "test",
});
registry.webSearchProviders.push({
pluginId: "beta-search",
pluginName: "Beta Search",
provider: {
}),
createProvider({
pluginId: "beta-search",
id: "beta",
label: "Beta Search",
hint: "Beta runtime provider",
envVars: ["BETA_SEARCH_API_KEY"],
placeholder: "beta-...",
signupUrl: "https://example.com/beta",
credentialPath: "tools.web.search.beta.apiKey",
autoDetectOrder: 2,
getCredentialValue: () => "beta-configured",
setCredentialValue: () => {},
createTool: ({ runtimeMetadata }) => ({
description: "beta",
parameters: {},
@@ -213,10 +260,9 @@ describe("web search runtime", () => {
runtimeSelectedProvider: runtimeMetadata?.selectedProvider,
}),
}),
},
source: "test",
});
setActivePluginRegistry(registry);
}),
]);
activateSecretsRuntimeSnapshot({
sourceConfig: {},
config: {},

View File

@@ -1,5 +1,5 @@
import type { OpenClawConfig } from "../config/config.js";
import { normalizeResolvedSecretInputString } from "../config/types.secrets.js";
import { normalizeSecretInputString, resolveSecretInputRef } from "../config/types.secrets.js";
import { logVerbose } from "../globals.js";
import type {
PluginWebSearchProviderEntry,
@@ -86,12 +86,18 @@ function hasEntryCredential(
const rawValue =
provider.getConfiguredCredentialValue?.(config) ??
provider.getCredentialValue(search as Record<string, unknown> | undefined);
const fromConfig = normalizeSecretInput(
normalizeResolvedSecretInputString({
value: rawValue,
path: provider.credentialPath,
}),
);
const configuredRef = resolveSecretInputRef({
value: rawValue,
}).ref;
if (configuredRef && configuredRef.source !== "env") {
return true;
}
const fromConfig = normalizeSecretInput(normalizeSecretInputString(rawValue));
if (configuredRef?.source === "env") {
return Boolean(
normalizeSecretInput(process.env[configuredRef.id]) || readProviderEnvValue(provider.envVars),
);
}
return Boolean(fromConfig || readProviderEnvValue(provider.envVars));
}

View File

@@ -1,6 +1,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import type { StreamFn } from "@mariozechner/pi-agent-core";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { ProviderPlugin, ProviderRuntimeModel } from "../../../src/plugins/types.js";
import {
@@ -676,6 +677,68 @@ export function describeXAIProviderRuntimeContract() {
},
});
});
it("owns xai tool_stream defaults", () => {
const provider = requireProviderContractProvider("xai");
expect(
provider.prepareExtraParams?.({
provider: "xai",
modelId: "grok-4-1-fast-reasoning",
extraParams: { temperature: 0.2 },
}),
).toEqual({
temperature: 0.2,
tool_stream: true,
});
expect(
provider.prepareExtraParams?.({
provider: "xai",
modelId: "grok-4-1-fast-reasoning",
extraParams: { tool_stream: false },
}),
).toEqual({
tool_stream: false,
});
});
it("owns xai fast-mode model rewriting through the plugin stream hook", () => {
const provider = requireProviderContractProvider("xai");
let capturedModelId = "";
const baseStreamFn: StreamFn = (model) => {
capturedModelId = model.id;
return {
push() {},
async result() {
return undefined;
},
async *[Symbol.asyncIterator]() {
// Minimal async stream surface for xAI decode wrappers.
},
} as unknown as ReturnType<StreamFn>;
};
const streamFn = provider.wrapStreamFn?.({
provider: "xai",
modelId: "grok-4",
extraParams: { fastMode: true },
streamFn: baseStreamFn,
});
expect(streamFn).toBeTypeOf("function");
void streamFn?.(
createModel({
id: "grok-4",
provider: "xai",
api: "openai-completions",
baseUrl: "https://api.x.ai/v1",
}) as never,
{ messages: [] } as never,
{},
);
expect(capturedModelId).toBe("grok-4-fast");
});
});
}