refactor: move extension markers into manifests

This commit is contained in:
Peter Steinberger
2026-04-18 20:16:25 +01:00
parent a5d6330f87
commit 85912849cc
18 changed files with 278 additions and 400 deletions

View File

@@ -155,6 +155,7 @@ Those belong in your plugin code and `package.json`.
| `modelSupport` | No | `object` | Manifest-owned shorthand model-family metadata used to auto-load the plugin before runtime. |
| `cliBackends` | No | `string[]` | CLI inference backend ids owned by this plugin. Used for startup auto-activation from explicit config refs. |
| `syntheticAuthRefs` | No | `string[]` | Provider or CLI backend refs whose plugin-owned synthetic auth hook should be probed during cold model discovery before runtime loads. |
| `nonSecretAuthMarkers` | No | `string[]` | Bundled-plugin-owned placeholder API key values that represent non-secret local, OAuth, or ambient credential state. |
| `commandAliases` | No | `object[]` | Command names owned by this plugin that should produce plugin-aware config and CLI diagnostics before runtime loads. |
| `providerAuthEnvVars` | No | `Record<string, string[]>` | Cheap provider-auth env metadata that OpenClaw can inspect without loading plugin code. |
| `providerAuthAliases` | No | `Record<string, string>` | Provider ids that should reuse another provider id for auth lookup, for example a coding provider that shares the base provider API key and auth profiles. |
@@ -605,6 +606,10 @@ See [Configuration reference](/gateway/configuration) for the full `plugins.*` s
auth hooks that must be visible to cold model discovery before the runtime
registry exists. Only list refs whose runtime provider or CLI backend actually
implements `resolveSyntheticAuth`.
- `nonSecretAuthMarkers` is the cheap metadata path for bundled plugin-owned
placeholder API keys such as local, OAuth, or ambient credential markers.
Core treats these as non-secrets for auth display and secret audits without
hardcoding the owning provider.
- `channelEnvVars` is the cheap metadata path for shell-env fallback, setup
prompts, and similar channel surfaces that should not boot plugin runtime
just to inspect env names.

View File

@@ -3,6 +3,7 @@
"enabledByDefault": true,
"providers": ["anthropic-vertex"],
"providerDiscoveryEntry": "./provider-discovery.ts",
"nonSecretAuthMarkers": ["gcp-vertex-credentials"],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@@ -2,6 +2,7 @@
"id": "lmstudio",
"enabledByDefault": true,
"providers": ["lmstudio"],
"nonSecretAuthMarkers": ["lmstudio-local"],
"providerAuthEnvVars": {
"lmstudio": ["LM_API_TOKEN"]
},

View File

@@ -4,6 +4,7 @@
"legacyPluginIds": ["minimax-portal-auth"],
"providers": ["minimax", "minimax-portal"],
"autoEnableWhenConfiguredProviders": ["minimax", "minimax-portal"],
"nonSecretAuthMarkers": ["minimax-oauth"],
"providerAuthEnvVars": {
"minimax": ["MINIMAX_API_KEY"],
"minimax-portal": ["MINIMAX_OAUTH_TOKEN", "MINIMAX_API_KEY"]

View File

@@ -4,6 +4,7 @@
"providers": ["ollama"],
"providerDiscoveryEntry": "./provider-discovery.ts",
"syntheticAuthRefs": ["ollama"],
"nonSecretAuthMarkers": ["ollama-local"],
"providerAuthEnvVars": {
"ollama": ["OLLAMA_API_KEY"]
},

View File

@@ -4,6 +4,26 @@ import { describe, expect, it } from "vitest";
import { registerSingleProviderPlugin } from "../../test/helpers/plugins/plugin-registration.js";
import plugin from "./index.js";
function createProviderModel(overrides: {
id: string;
api?: string;
baseUrl?: string;
provider?: string;
}) {
return {
id: overrides.id,
name: overrides.id,
api: overrides.api ?? "openai-completions",
provider: overrides.provider ?? "xai",
baseUrl: overrides.baseUrl ?? "https://api.x.ai/v1",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200_000,
maxTokens: 8_192,
};
}
describe("xai provider plugin", () => {
it("owns replay policy for xAI OpenAI-compatible transports", async () => {
const provider = await registerSingleProviderPlugin(plugin);
@@ -112,4 +132,77 @@ describe("xai provider plugin", () => {
} as never),
).toBe(explicit);
});
it("owns forward-compatible Grok model resolution", async () => {
const provider = await registerSingleProviderPlugin(plugin);
expect(
provider.resolveDynamicModel?.({
provider: "xai",
modelId: "grok-4-1-fast-reasoning",
modelRegistry: { find: () => null } as never,
providerConfig: {
api: "openai-completions",
baseUrl: "https://api.x.ai/v1",
},
} as never),
).toMatchObject({
id: "grok-4-1-fast-reasoning",
provider: "xai",
api: "openai-completions",
baseUrl: "https://api.x.ai/v1",
reasoning: true,
contextWindow: 2_000_000,
});
});
it("marks modern Grok refs without accepting multi-agent ids", async () => {
const provider = await registerSingleProviderPlugin(plugin);
expect(
provider.isModernModelRef?.({
provider: "xai",
modelId: "grok-4-1-fast-reasoning",
} as never),
).toBe(true);
expect(
provider.isModernModelRef?.({
provider: "xai",
modelId: "grok-4.20-multi-agent-experimental-beta-0304",
} as never),
).toBe(false);
});
it("owns xai compat flags for direct and downstream routed models", async () => {
const provider = await registerSingleProviderPlugin(plugin);
expect(
provider.normalizeResolvedModel?.({
provider: "xai",
modelId: "grok-4-1-fast",
model: createProviderModel({ id: "grok-4-1-fast" }),
} as never),
).toMatchObject({
compat: {
toolSchemaProfile: "xai",
nativeWebSearchTool: true,
toolCallArgumentsEncoding: "html-entities",
},
});
expect(
provider.contributeResolvedModelCompat?.({
provider: "openrouter",
modelId: "x-ai/grok-4-1-fast",
model: createProviderModel({
id: "x-ai/grok-4-1-fast",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
}),
} as never),
).toMatchObject({
toolSchemaProfile: "xai",
nativeWebSearchTool: true,
toolCallArgumentsEncoding: "html-entities",
});
});
});

View File

@@ -26,6 +26,7 @@ let GCP_VERTEX_CREDENTIALS_MARKER: typeof import("./model-auth-markers.js").GCP_
let NON_ENV_SECRETREF_MARKER: typeof import("./model-auth-markers.js").NON_ENV_SECRETREF_MARKER;
let isKnownEnvApiKeyMarker: typeof import("./model-auth-markers.js").isKnownEnvApiKeyMarker;
let isNonSecretApiKeyMarker: typeof import("./model-auth-markers.js").isNonSecretApiKeyMarker;
let listKnownNonSecretApiKeyMarkers: typeof import("./model-auth-markers.js").listKnownNonSecretApiKeyMarkers;
let resolveOAuthApiKeyMarker: typeof import("./model-auth-markers.js").resolveOAuthApiKeyMarker;
let manifestEnvSnapshot: ReturnType<typeof captureEnv> | undefined;
@@ -42,6 +43,7 @@ async function loadMarkerModules() {
NON_ENV_SECRETREF_MARKER = markersModule.NON_ENV_SECRETREF_MARKER;
isKnownEnvApiKeyMarker = markersModule.isKnownEnvApiKeyMarker;
isNonSecretApiKeyMarker = markersModule.isNonSecretApiKeyMarker;
listKnownNonSecretApiKeyMarkers = markersModule.listKnownNonSecretApiKeyMarkers;
resolveOAuthApiKeyMarker = markersModule.resolveOAuthApiKeyMarker;
}
@@ -66,9 +68,21 @@ describe("model auth markers", () => {
expect(isNonSecretApiKeyMarker(NON_ENV_SECRETREF_MARKER)).toBe(true);
expect(isNonSecretApiKeyMarker(resolveOAuthApiKeyMarker("chutes"))).toBe(true);
expect(isNonSecretApiKeyMarker("ollama-local")).toBe(true);
expect(isNonSecretApiKeyMarker("lmstudio-local")).toBe(true);
expect(isNonSecretApiKeyMarker(GCP_VERTEX_CREDENTIALS_MARKER)).toBe(true);
});
it("reads bundled plugin-owned non-secret markers from manifests", () => {
expect(listKnownNonSecretApiKeyMarkers()).toEqual(
expect.arrayContaining([
"gcp-vertex-credentials",
"lmstudio-local",
"minimax-oauth",
"ollama-local",
]),
);
});
it("does not treat removed provider markers as active auth markers", () => {
expect(isNonSecretApiKeyMarker("qwen-oauth")).toBe(false);
});

View File

@@ -1,4 +1,5 @@
import type { SecretRefSource } from "../config/types.secrets.js";
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
import { listKnownProviderEnvApiKeyNames } from "./model-auth-env-vars.js";
export const MINIMAX_OAUTH_MARKER = "minimax-oauth";
@@ -14,6 +15,10 @@ const AWS_SDK_ENV_MARKERS = new Set([
"AWS_ACCESS_KEY_ID",
"AWS_PROFILE",
]);
const CORE_NON_SECRET_API_KEY_MARKERS = [
CUSTOM_LOCAL_AUTH_MARKER,
NON_ENV_SECRETREF_MARKER,
] as const;
// Legacy marker names kept for backward compatibility with existing models.json files.
const LEGACY_ENV_API_KEY_MARKERS = [
@@ -35,6 +40,13 @@ function listKnownEnvApiKeyMarkers(): Set<string> {
]);
}
export function listKnownNonSecretApiKeyMarkers(): string[] {
const bundledMarkers = loadPluginManifestRegistry({ cache: true }).plugins.flatMap((plugin) =>
plugin.origin === "bundled" ? (plugin.nonSecretAuthMarkers ?? []) : [],
);
return [...new Set([...CORE_NON_SECRET_API_KEY_MARKERS, ...bundledMarkers])];
}
export function isAwsSdkAuthMarker(value: string): boolean {
return AWS_SDK_ENV_MARKERS.has(value.trim());
}
@@ -80,12 +92,8 @@ export function isNonSecretApiKeyMarker(
return false;
}
const isKnownMarker =
trimmed === MINIMAX_OAUTH_MARKER ||
isOAuthApiKeyMarker(trimmed) ||
trimmed === OLLAMA_LOCAL_AUTH_MARKER ||
trimmed === CUSTOM_LOCAL_AUTH_MARKER ||
trimmed === GCP_VERTEX_CREDENTIALS_MARKER ||
trimmed === NON_ENV_SECRETREF_MARKER ||
listKnownNonSecretApiKeyMarkers().includes(trimmed) ||
isAwsSdkAuthMarker(trimmed);
if (isKnownMarker) {
return true;

View File

@@ -2,10 +2,10 @@ import { z } from "zod";
import { isSafeScpRemoteHost } from "../infra/scp-host.js";
import { isValidInboundPathRootPattern } from "../media/inbound-path-policy.js";
import {
normalizeTelegramCommandDescription,
normalizeTelegramCommandName,
resolveTelegramCustomCommands,
} from "../plugin-sdk/telegram-command-config.js";
normalizeCommandDescription,
normalizeSlashCommandName,
resolveCustomCommands,
} from "../shared/custom-command-config.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { ToolPolicySchema } from "./zod-schema.agent-runtime.js";
import {
@@ -105,6 +105,12 @@ const SlackCapabilitiesSchema = z.union([
]);
const TelegramErrorPolicySchema = z.enum(["always", "once", "silent"]).optional();
const TelegramCommandNamePattern = /^[a-z0-9_]{1,32}$/;
const TelegramCustomCommandConfig = {
label: "Telegram",
pattern: TelegramCommandNamePattern,
patternDescription: "use a-z, 0-9, underscore; max 32 chars",
} as const;
export const TelegramTopicSchema = z
.object({
requireMention: z.boolean().optional(),
@@ -170,8 +176,8 @@ export const TelegramDirectSchema = z
const TelegramCustomCommandSchema = z
.object({
command: z.string().overwrite(normalizeTelegramCommandName),
description: z.string().overwrite(normalizeTelegramCommandDescription),
command: z.string().overwrite(normalizeSlashCommandName),
description: z.string().overwrite(normalizeCommandDescription),
})
.strict();
@@ -182,10 +188,11 @@ const validateTelegramCustomCommands = (
if (!value.customCommands || value.customCommands.length === 0) {
return;
}
const { issues } = resolveTelegramCustomCommands({
const { issues } = resolveCustomCommands({
commands: value.customCommands,
checkReserved: false,
checkDuplicates: false,
config: TelegramCustomCommandConfig,
});
for (const issue of issues) {
ctx.addIssue({

View File

@@ -1,4 +1,8 @@
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import {
normalizeCommandDescription,
normalizeSlashCommandName,
resolveCustomCommands,
} from "../shared/custom-command-config.js";
export type TelegramCustomCommandInput = {
command?: string | null;
@@ -11,18 +15,18 @@ export type TelegramCustomCommandIssue = {
message: string;
};
const TELEGRAM_COMMAND_NAME_PATTERN_VALUE = /^[a-z0-9_]{1,32}$/;
const TELEGRAM_CUSTOM_COMMAND_CONFIG = {
label: "Telegram",
pattern: TELEGRAM_COMMAND_NAME_PATTERN_VALUE,
patternDescription: "use a-z, 0-9, underscore; max 32 chars",
} as const;
function normalizeTelegramCommandNameImpl(value: string): string {
const trimmed = value.trim();
if (!trimmed) {
return "";
}
const withoutSlash = trimmed.startsWith("/") ? trimmed.slice(1) : trimmed;
return normalizeLowercaseStringOrEmpty(withoutSlash).replace(/-/g, "_");
return normalizeSlashCommandName(value);
}
function normalizeTelegramCommandDescriptionImpl(value: string): string {
return value.trim();
return normalizeCommandDescription(value);
}
function resolveTelegramCustomCommandsImpl(params: {
@@ -34,65 +38,10 @@ function resolveTelegramCustomCommandsImpl(params: {
commands: Array<{ command: string; description: string }>;
issues: TelegramCustomCommandIssue[];
} {
const entries = Array.isArray(params.commands) ? params.commands : [];
const reserved = params.reservedCommands ?? new Set<string>();
const checkReserved = params.checkReserved !== false;
const checkDuplicates = params.checkDuplicates !== false;
const seen = new Set<string>();
const resolved: Array<{ command: string; description: string }> = [];
const issues: TelegramCustomCommandIssue[] = [];
for (let index = 0; index < entries.length; index += 1) {
const entry = entries[index];
const normalized = normalizeTelegramCommandNameImpl(entry?.command ?? "");
if (!normalized) {
issues.push({
index,
field: "command",
message: "Telegram custom command is missing a command name.",
});
continue;
}
if (!TELEGRAM_COMMAND_NAME_PATTERN_VALUE.test(normalized)) {
issues.push({
index,
field: "command",
message: `Telegram custom command "/${normalized}" is invalid (use a-z, 0-9, underscore; max 32 chars).`,
});
continue;
}
if (checkReserved && reserved.has(normalized)) {
issues.push({
index,
field: "command",
message: `Telegram custom command "/${normalized}" conflicts with a native command.`,
});
continue;
}
if (checkDuplicates && seen.has(normalized)) {
issues.push({
index,
field: "command",
message: `Telegram custom command "/${normalized}" is duplicated.`,
});
continue;
}
const description = normalizeTelegramCommandDescriptionImpl(entry?.description ?? "");
if (!description) {
issues.push({
index,
field: "description",
message: `Telegram custom command "/${normalized}" is missing a description.`,
});
continue;
}
if (checkDuplicates) {
seen.add(normalized);
}
resolved.push({ command: normalized, description });
}
return { commands: resolved, issues };
return resolveCustomCommands({
...params,
config: TELEGRAM_CUSTOM_COMMAND_CONFIG,
});
}
export function getTelegramCommandNamePattern(): RegExp {

View File

@@ -3,7 +3,6 @@ import {
describeGithubCopilotProviderDiscoveryContract,
describeMinimaxProviderDiscoveryContract,
describeModelStudioProviderDiscoveryContract,
describeOllamaProviderDiscoveryContract,
describeSglangProviderDiscoveryContract,
describeVllmProviderDiscoveryContract,
} from "../../../test/helpers/plugins/provider-discovery-contract.js";
@@ -12,6 +11,5 @@ describeCloudflareAiGatewayProviderDiscoveryContract();
describeGithubCopilotProviderDiscoveryContract();
describeMinimaxProviderDiscoveryContract();
describeModelStudioProviderDiscoveryContract();
describeOllamaProviderDiscoveryContract();
describeSglangProviderDiscoveryContract();
describeVllmProviderDiscoveryContract();

View File

@@ -5,7 +5,6 @@ import {
describeOpenAIProviderRuntimeContract,
describeOpenRouterProviderRuntimeContract,
describeVeniceProviderRuntimeContract,
describeXAIProviderRuntimeContract,
describeZAIProviderRuntimeContract,
} from "../../../test/helpers/plugins/provider-runtime-contract.js";
@@ -15,5 +14,4 @@ describeGoogleProviderRuntimeContract();
describeOpenAIProviderRuntimeContract();
describeOpenRouterProviderRuntimeContract();
describeVeniceProviderRuntimeContract();
describeXAIProviderRuntimeContract();
describeZAIProviderRuntimeContract();

View File

@@ -383,6 +383,7 @@ describe("loadPluginManifestRegistry", () => {
openai: ["OPENAI_API_KEY"],
},
syntheticAuthRefs: ["openai-cli"],
nonSecretAuthMarkers: ["openai-cli"],
providerAuthAliases: {
"openai-codex": "openai",
},
@@ -409,6 +410,7 @@ describe("loadPluginManifestRegistry", () => {
openai: ["OPENAI_API_KEY"],
});
expect(registry.plugins[0]?.syntheticAuthRefs).toEqual(["openai-cli"]);
expect(registry.plugins[0]?.nonSecretAuthMarkers).toEqual(["openai-cli"]);
expect(registry.plugins[0]?.providerAuthAliases).toEqual({
"openai-codex": "openai",
});

View File

@@ -87,6 +87,7 @@ export type PluginManifestRecord = {
modelSupport?: PluginManifestModelSupport;
cliBackends: string[];
syntheticAuthRefs?: string[];
nonSecretAuthMarkers?: string[];
commandAliases?: PluginManifestCommandAlias[];
providerAuthEnvVars?: Record<string, string[]>;
providerAuthAliases?: Record<string, string>;
@@ -330,6 +331,7 @@ function buildRecord(params: {
modelSupport: params.manifest.modelSupport,
cliBackends: params.manifest.cliBackends ?? [],
syntheticAuthRefs: params.manifest.syntheticAuthRefs ?? [],
nonSecretAuthMarkers: params.manifest.nonSecretAuthMarkers ?? [],
commandAliases: params.manifest.commandAliases,
providerAuthEnvVars: params.manifest.providerAuthEnvVars,
providerAuthAliases: params.manifest.providerAuthAliases,
@@ -401,6 +403,7 @@ function buildBundleRecord(params: {
providers: [],
cliBackends: [],
syntheticAuthRefs: [],
nonSecretAuthMarkers: [],
skills: params.manifest.skills ?? [],
settingsFiles: params.manifest.settingsFiles ?? [],
hooks: params.manifest.hooks ?? [],

View File

@@ -168,6 +168,11 @@ export type PluginManifest = {
* be probed during cold model discovery before the runtime registry exists.
*/
syntheticAuthRefs?: string[];
/**
* Bundled-plugin-owned placeholder API key values that represent non-secret
* local, OAuth, or ambient credential state.
*/
nonSecretAuthMarkers?: string[];
/**
* Plugin-owned command aliases that should resolve to this plugin during
* config diagnostics before runtime loads.
@@ -707,6 +712,7 @@ export function loadPluginManifest(
const modelSupport = normalizeManifestModelSupport(raw.modelSupport);
const cliBackends = normalizeTrimmedStringList(raw.cliBackends);
const syntheticAuthRefs = normalizeTrimmedStringList(raw.syntheticAuthRefs);
const nonSecretAuthMarkers = normalizeTrimmedStringList(raw.nonSecretAuthMarkers);
const commandAliases = normalizeManifestCommandAliases(raw.commandAliases);
const providerAuthEnvVars = normalizeStringListRecord(raw.providerAuthEnvVars);
const providerAuthAliases = normalizeStringRecord(raw.providerAuthAliases);
@@ -742,6 +748,7 @@ export function loadPluginManifest(
modelSupport,
cliBackends,
syntheticAuthRefs,
nonSecretAuthMarkers,
commandAliases,
providerAuthEnvVars,
providerAuthAliases,

View File

@@ -0,0 +1,107 @@
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
export type CustomCommandInput = {
command?: string | null;
description?: string | null;
};
export type CustomCommandIssue = {
index: number;
field: "command" | "description";
message: string;
};
export type CustomCommandConfig = {
label: string;
pattern: RegExp;
patternDescription: string;
prefix?: string;
};
const DEFAULT_PREFIX = "/";
export function normalizeSlashCommandName(value: string): string {
const trimmed = value.trim();
if (!trimmed) {
return "";
}
const withoutSlash = trimmed.startsWith(DEFAULT_PREFIX) ? trimmed.slice(1) : trimmed;
return normalizeLowercaseStringOrEmpty(withoutSlash).replace(/-/g, "_");
}
export function normalizeCommandDescription(value: string): string {
return value.trim();
}
export function resolveCustomCommands(params: {
commands?: CustomCommandInput[] | null;
reservedCommands?: Set<string>;
checkReserved?: boolean;
checkDuplicates?: boolean;
config: CustomCommandConfig;
}): {
commands: Array<{ command: string; description: string }>;
issues: CustomCommandIssue[];
} {
const entries = Array.isArray(params.commands) ? params.commands : [];
const reserved = params.reservedCommands ?? new Set<string>();
const checkReserved = params.checkReserved !== false;
const checkDuplicates = params.checkDuplicates !== false;
const seen = new Set<string>();
const resolved: Array<{ command: string; description: string }> = [];
const issues: CustomCommandIssue[] = [];
const label = params.config.label;
const prefix = params.config.prefix ?? DEFAULT_PREFIX;
for (let index = 0; index < entries.length; index += 1) {
const entry = entries[index];
const normalized = normalizeSlashCommandName(entry?.command ?? "");
if (!normalized) {
issues.push({
index,
field: "command",
message: `${label} custom command is missing a command name.`,
});
continue;
}
if (!params.config.pattern.test(normalized)) {
issues.push({
index,
field: "command",
message: `${label} custom command "${prefix}${normalized}" is invalid (${params.config.patternDescription}).`,
});
continue;
}
if (checkReserved && reserved.has(normalized)) {
issues.push({
index,
field: "command",
message: `${label} custom command "${prefix}${normalized}" conflicts with a native command.`,
});
continue;
}
if (checkDuplicates && seen.has(normalized)) {
issues.push({
index,
field: "command",
message: `${label} custom command "${prefix}${normalized}" is duplicated.`,
});
continue;
}
const description = normalizeCommandDescription(entry?.description ?? "");
if (!description) {
issues.push({
index,
field: "description",
message: `${label} custom command "${prefix}${normalized}" is missing a description.`,
});
continue;
}
if (checkDuplicates) {
seen.add(normalized);
}
resolved.push({ command: normalized, description });
}
return { commands: resolved, issues };
}

View File

@@ -1,7 +1,6 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { AuthProfileStore } from "../../../src/agents/auth-profiles/types.js";
import type { OpenClawConfig } from "../../../src/config/config.js";
import type { ModelDefinitionConfig } from "../../../src/config/types.models.js";
import {
resolveBundledPluginPublicModulePath,
resolveRelativeBundledPluginPublicModuleId,
@@ -9,7 +8,6 @@ import {
import { registerProviders, requireProvider } from "./contracts-testkit.js";
const resolveCopilotApiTokenMock = vi.hoisted(() => vi.fn());
const buildOllamaProviderMock = vi.hoisted(() => vi.fn());
const buildVllmProviderMock = vi.hoisted(() => vi.fn());
const buildSglangProviderMock = vi.hoisted(() => vi.fn());
const ensureAuthProfileStoreMock = vi.hoisted(() => vi.fn());
@@ -39,15 +37,6 @@ const bundledProviderModules = {
pluginId: "qwen",
artifactBasename: "index.js",
}),
ollamaApiModuleId: resolveBundledPluginPublicModulePath({
pluginId: "ollama",
artifactBasename: "api.js",
}),
ollamaIndexModuleUrl: resolveRelativeBundledPluginPublicModuleId({
fromModuleUrl: import.meta.url,
pluginId: "ollama",
artifactBasename: "index.js",
}),
sglangApiModuleId: resolveBundledPluginPublicModulePath({
pluginId: "sglang",
artifactBasename: "api.js",
@@ -73,7 +62,6 @@ type ProviderHandle = Awaited<ReturnType<typeof requireProvider>>;
type DiscoveryState = {
runProviderCatalog: typeof import("../../../src/plugins/provider-discovery.js").runProviderCatalog;
githubCopilotProvider?: ProviderHandle;
ollamaProvider?: ProviderHandle;
vllmProvider?: ProviderHandle;
sglangProvider?: ProviderHandle;
minimaxProvider?: ProviderHandle;
@@ -84,30 +72,12 @@ type DiscoveryState = {
type BundledProviderUnderTest =
| "github-copilot"
| "ollama"
| "vllm"
| "sglang"
| "minimax"
| "modelstudio"
| "cloudflare-ai-gateway";
function createModelConfig(id: string, name = id): ModelDefinitionConfig {
return {
id,
name,
reasoning: false,
input: ["text"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128_000,
maxTokens: 8_192,
};
}
function setRuntimeAuthStore(store?: AuthProfileStore) {
const resolvedStore = store ?? {
version: 1,
@@ -226,15 +196,6 @@ function installDiscoveryHooks(
resolveCopilotApiToken: resolveCopilotApiTokenMock,
};
});
vi.doMock(bundledProviderModules.ollamaApiModuleId, async () => {
return {
OLLAMA_DEFAULT_BASE_URL: "http://127.0.0.1:11434",
buildOllamaProvider: (...args: unknown[]) => buildOllamaProviderMock(...args),
configureOllamaNonInteractive: vi.fn(),
ensureOllamaModelPulled: vi.fn(),
promptAndConfigureOllama: vi.fn(),
};
});
vi.doMock(bundledProviderModules.vllmApiModuleId, async () => {
return {
VLLM_DEFAULT_API_KEY_ENV_VAR: "VLLM_API_KEY",
@@ -266,13 +227,6 @@ function installDiscoveryHooks(
);
}
if (providerIds.includes("ollama")) {
const { default: ollamaPlugin } = await importBundledProviderPlugin<{
default: Parameters<typeof registerProviders>[0];
}>(bundledProviderModules.ollamaIndexModuleUrl);
state.ollamaProvider = requireProvider(await registerProviders(ollamaPlugin), "ollama");
}
if (providerIds.includes("vllm")) {
const { default: vllmPlugin } = await importBundledProviderPlugin<{
default: Parameters<typeof registerProviders>[0];
@@ -321,7 +275,6 @@ function installDiscoveryHooks(
afterEach(() => {
vi.restoreAllMocks();
resolveCopilotApiTokenMock.mockReset();
buildOllamaProviderMock.mockReset();
buildVllmProviderMock.mockReset();
buildSglangProviderMock.mockReset();
ensureAuthProfileStoreMock.mockReset();
@@ -388,108 +341,6 @@ export function describeGithubCopilotProviderDiscoveryContract() {
});
}
export function describeOllamaProviderDiscoveryContract() {
const state = {} as DiscoveryState;
describe("ollama provider discovery contract", () => {
installDiscoveryHooks(state, ["ollama"]);
it("keeps explicit catalog normalization provider-owned", async () => {
await expect(
state.runProviderCatalog({
provider: state.ollamaProvider!,
config: {
models: {
providers: {
ollama: {
baseUrl: "http://ollama-host:11434/v1/",
models: [createModelConfig("llama3.2")],
},
},
},
},
env: {} as NodeJS.ProcessEnv,
resolveProviderApiKey: () => ({ apiKey: undefined }),
resolveProviderAuth: () => ({
apiKey: undefined,
discoveryApiKey: undefined,
mode: "none",
source: "none",
}),
}),
).resolves.toMatchObject({
provider: {
baseUrl: "http://ollama-host:11434",
api: "ollama",
apiKey: "ollama-local",
models: [createModelConfig("llama3.2")],
},
});
expect(buildOllamaProviderMock).not.toHaveBeenCalled();
});
it("keeps empty autodiscovery disabled without keys or explicit config", async () => {
buildOllamaProviderMock.mockResolvedValueOnce({
baseUrl: "http://127.0.0.1:11434",
api: "ollama",
models: [],
});
await expect(
runCatalog(state, {
provider: state.ollamaProvider!,
config: {},
env: {} as NodeJS.ProcessEnv,
resolveProviderApiKey: () => ({ apiKey: undefined }),
resolveProviderAuth: () => ({
apiKey: undefined,
discoveryApiKey: undefined,
mode: "none",
source: "none",
}),
}),
).resolves.toBeNull();
expect(buildOllamaProviderMock).toHaveBeenCalledWith(undefined, { quiet: true });
});
it("keeps empty default-ish provider stubs on the quiet ambient path", async () => {
buildOllamaProviderMock.mockResolvedValueOnce({
baseUrl: "http://127.0.0.1:11434",
api: "ollama",
models: [],
});
await expect(
runCatalog(state, {
provider: state.ollamaProvider!,
config: {
models: {
providers: {
ollama: {
baseUrl: "http://127.0.0.1:11434",
api: "ollama",
models: [],
},
},
},
},
env: {} as NodeJS.ProcessEnv,
resolveProviderApiKey: () => ({ apiKey: undefined }),
resolveProviderAuth: () => ({
apiKey: undefined,
discoveryApiKey: undefined,
mode: "none",
source: "none",
}),
}),
).resolves.toBeNull();
expect(buildOllamaProviderMock).toHaveBeenCalledWith("http://127.0.0.1:11434", {
quiet: true,
});
});
});
}
export function describeVllmProviderDiscoveryContract() {
const state = {} as DiscoveryState;

View File

@@ -1,7 +1,6 @@
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 { resolveRelativeBundledPluginPublicModuleId } from "../../../src/test-utils/bundled-plugin-public-surface.js";
@@ -52,11 +51,6 @@ const providerRuntimeContractModules = {
pluginId: "venice",
artifactBasename: "index.js",
}),
xAIIndexModuleId: resolveRelativeBundledPluginPublicModuleId({
fromModuleUrl: import.meta.url,
pluginId: "xai",
artifactBasename: "index.js",
}),
zaiIndexModuleId: resolveRelativeBundledPluginPublicModuleId({
fromModuleUrl: import.meta.url,
pluginId: "zai",
@@ -156,15 +150,6 @@ const PROVIDER_RUNTIME_CONTRACT_FIXTURES: readonly ProviderRuntimeContractFixtur
default: Parameters<typeof registerProviderPlugin>[0]["plugin"];
}>(providerRuntimeContractModules.veniceIndexModuleId),
},
{
providerIds: ["xai"],
pluginId: "xai",
name: "xAI",
load: async () =>
await importBundledProviderPlugin<{
default: Parameters<typeof registerProviderPlugin>[0]["plugin"];
}>(providerRuntimeContractModules.xAIIndexModuleId),
},
{
providerIds: ["zai"],
pluginId: "zai",
@@ -717,159 +702,6 @@ export function describeOpenAIProviderRuntimeContract() {
});
}
export function describeXAIProviderRuntimeContract() {
describe("xai provider runtime contract", { timeout: CONTRACT_SETUP_TIMEOUT_MS }, () => {
installRuntimeHooks();
it("owns Grok forward-compat resolution for newer fast models", () => {
const provider = requireProviderContractProvider("xai");
const model = provider.resolveDynamicModel?.({
provider: "xai",
modelId: "grok-4-1-fast-reasoning",
modelRegistry: {
find: () => null,
} as never,
providerConfig: {
api: "openai-completions",
baseUrl: "https://api.x.ai/v1",
},
});
expect(model).toMatchObject({
id: "grok-4-1-fast-reasoning",
provider: "xai",
api: "openai-completions",
baseUrl: "https://api.x.ai/v1",
reasoning: true,
contextWindow: 2_000_000,
});
});
it("owns modern-model matching without accepting multi-agent ids", () => {
const provider = requireProviderContractProvider("xai");
expect(
provider.isModernModelRef?.({
provider: "xai",
modelId: "grok-4-1-fast-reasoning",
} as never),
).toBe(true);
expect(
provider.isModernModelRef?.({
provider: "xai",
modelId: "grok-4.20-multi-agent-experimental-beta-0304",
} as never),
).toBe(false);
});
it("owns direct xai compat flags on resolved models", () => {
const provider = requireProviderContractProvider("xai");
expect(
provider.normalizeResolvedModel?.({
provider: "xai",
modelId: "grok-4-1-fast",
model: createModel({
id: "grok-4-1-fast",
provider: "xai",
api: "openai-completions",
baseUrl: "https://api.x.ai/v1",
}),
} as never),
).toMatchObject({
compat: {
toolSchemaProfile: "xai",
nativeWebSearchTool: true,
toolCallArgumentsEncoding: "html-entities",
},
});
});
it("owns downstream xai compat contributions for x-ai routed models", () => {
const provider = requireProviderContractProvider("xai");
expect(
provider.contributeResolvedModelCompat?.({
provider: "openrouter",
modelId: "x-ai/grok-4-1-fast",
model: createModel({
id: "x-ai/grok-4-1-fast",
provider: "openrouter",
api: "openai-completions",
baseUrl: "https://openrouter.ai/api/v1",
}),
} as never),
).toMatchObject({
toolSchemaProfile: "xai",
nativeWebSearchTool: true,
toolCallArgumentsEncoding: "html-entities",
});
});
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");
});
});
}
export function describeOpenRouterProviderRuntimeContract() {
describe("openrouter provider runtime contract", { timeout: CONTRACT_SETUP_TIMEOUT_MS }, () => {
installRuntimeHooks();