feat(plugins): move provider runtimes into bundled plugins

This commit is contained in:
Peter Steinberger
2026-03-15 16:09:15 -07:00
parent dd40741e18
commit 4adcfa3256
11 changed files with 193 additions and 340 deletions

View File

@@ -52,6 +52,13 @@ Current bundled examples:
hints, and runtime token exchange
- `openai-codex`: forward-compat model fallback, transport normalization, and
default transport params
- `moonshot`: shared transport, plugin-owned thinking payload normalization
- `kilocode`: shared transport, plugin-owned request headers, reasoning payload
normalization, Gemini transcript hints, and cache-TTL policy
- `byteplus`, `cloudflare-ai-gateway`, `huggingface`, `kimi-coding`,
`minimax`, `minimax-portal`, `modelstudio`, `nvidia`, `qianfan`,
`qwen-portal`, `synthetic`, `together`, `venice`, `vercel-ai-gateway`,
`volcengine`, and `xiaomi`: plugin-owned catalogs only
That covers providers that still fit OpenClaw's normal transports. A provider
that needs a totally custom request executor is a separate, deeper extension
@@ -194,12 +201,26 @@ OpenClaw ships with the piai catalog. These providers require **no**
See [/providers/kilocode](/providers/kilocode) for setup details.
### Other built-in providers
### Other bundled provider plugins
- OpenRouter: `openrouter` (`OPENROUTER_API_KEY`)
- Example model: `openrouter/anthropic/claude-sonnet-4-5`
- Kilo Gateway: `kilocode` (`KILOCODE_API_KEY`)
- Example model: `kilocode/anthropic/claude-opus-4.6`
- MiniMax: `minimax` (`MINIMAX_API_KEY`)
- Moonshot: `moonshot` (`MOONSHOT_API_KEY`)
- Kimi Coding: `kimi-coding` (`KIMI_API_KEY` or `KIMICODE_API_KEY`)
- Qianfan: `qianfan` (`QIANFAN_API_KEY`)
- Model Studio: `modelstudio` (`MODELSTUDIO_API_KEY`)
- NVIDIA: `nvidia` (`NVIDIA_API_KEY`)
- Together: `together` (`TOGETHER_API_KEY`)
- Venice: `venice` (`VENICE_API_KEY`)
- Xiaomi: `xiaomi` (`XIAOMI_API_KEY`)
- Vercel AI Gateway: `vercel-ai-gateway` (`AI_GATEWAY_API_KEY`)
- Hugging Face Inference: `huggingface` (`HUGGINGFACE_HUB_TOKEN` or `HF_TOKEN`)
- Cloudflare AI Gateway: `cloudflare-ai-gateway` (`CLOUDFLARE_AI_GATEWAY_API_KEY`)
- Volcengine: `volcengine` (`VOLCANO_ENGINE_API_KEY`)
- BytePlus: `byteplus` (`BYTEPLUS_API_KEY`)
- xAI: `xai` (`XAI_API_KEY`)
- Mistral: `mistral` (`MISTRAL_API_KEY`)
- Example model: `mistral/mistral-large-latest`
@@ -209,13 +230,17 @@ See [/providers/kilocode](/providers/kilocode) for setup details.
- GLM models on Cerebras use ids `zai-glm-4.7` and `zai-glm-4.6`.
- OpenAI-compatible base URL: `https://api.cerebras.ai/v1`.
- GitHub Copilot: `github-copilot` (`COPILOT_GITHUB_TOKEN` / `GH_TOKEN` / `GITHUB_TOKEN`)
- Hugging Face Inference: `huggingface` (`HUGGINGFACE_HUB_TOKEN` or `HF_TOKEN`) — OpenAI-compatible router; example model: `huggingface/deepseek-ai/DeepSeek-R1`; CLI: `openclaw onboard --auth-choice huggingface-api-key`. See [Hugging Face (Inference)](/providers/huggingface).
- Hugging Face Inference example model: `huggingface/deepseek-ai/DeepSeek-R1`; CLI: `openclaw onboard --auth-choice huggingface-api-key`. See [Hugging Face (Inference)](/providers/huggingface).
## Providers via `models.providers` (custom/base URL)
Use `models.providers` (or `models.json`) to add **custom** providers or
OpenAI/Anthropiccompatible proxies.
Many of the bundled provider plugins below already publish a default catalog.
Use explicit `models.providers.<id>` entries only when you want to override the
default base URL, headers, or model list.
### Moonshot AI (Kimi)
Moonshot uses OpenAI-compatible endpoints, so configure it as a custom provider:
@@ -275,10 +300,9 @@ Kimi Coding uses Moonshot AI's Anthropic-compatible endpoint:
### Qwen OAuth (free tier)
Qwen provides OAuth access to Qwen Coder + Vision via a device-code flow.
Enable the bundled plugin, then log in:
The bundled provider plugin is enabled by default, so just log in:
```bash
openclaw plugins enable qwen-portal-auth
openclaw models auth login --provider qwen-portal --set-default
```

View File

@@ -164,12 +164,29 @@ Important trust note:
- [Nostr](/channels/nostr) — `@openclaw/nostr`
- [Zalo](/channels/zalo) — `@openclaw/zalo`
- [Microsoft Teams](/channels/msteams) — `@openclaw/msteams`
- BytePlus provider catalog — bundled as `byteplus` (enabled by default)
- Cloudflare AI Gateway provider catalog — bundled as `cloudflare-ai-gateway` (enabled by default)
- Google Antigravity OAuth (provider auth) — bundled as `google-antigravity-auth` (disabled by default)
- Gemini CLI OAuth (provider auth) — bundled as `google-gemini-cli-auth` (disabled by default)
- GitHub Copilot provider runtime — bundled as `github-copilot` (enabled by default)
- Hugging Face provider catalog — bundled as `huggingface` (enabled by default)
- Kilo Gateway provider runtime — bundled as `kilocode` (enabled by default)
- Kimi Coding provider catalog — bundled as `kimi-coding` (enabled by default)
- MiniMax provider catalog — bundled as `minimax` (enabled by default)
- MiniMax OAuth (provider auth + catalog) — bundled as `minimax-portal-auth` (enabled by default)
- Model Studio provider catalog — bundled as `modelstudio` (enabled by default)
- Moonshot provider runtime — bundled as `moonshot` (enabled by default)
- NVIDIA provider catalog — bundled as `nvidia` (enabled by default)
- OpenAI Codex provider runtime — bundled as `openai-codex` (enabled by default)
- OpenRouter provider runtime — bundled as `openrouter` (enabled by default)
- Qwen OAuth (provider auth) — bundled as `qwen-portal-auth` (disabled by default)
- Qianfan provider catalog — bundled as `qianfan` (enabled by default)
- Qwen OAuth (provider auth + catalog) — bundled as `qwen-portal-auth` (enabled by default)
- Synthetic provider catalog — bundled as `synthetic` (enabled by default)
- Together provider catalog — bundled as `together` (enabled by default)
- Venice provider catalog — bundled as `venice` (enabled by default)
- Vercel AI Gateway provider catalog — bundled as `vercel-ai-gateway` (enabled by default)
- Volcengine provider catalog — bundled as `volcengine` (enabled by default)
- Xiaomi provider catalog — bundled as `xiaomi` (enabled by default)
- Copilot Proxy (provider auth) — local VS Code Copilot Proxy bridge; distinct from built-in `github-copilot` device login (bundled, disabled by default)
Native OpenClaw plugins are **TypeScript modules** loaded at runtime via jiti.
@@ -323,6 +340,16 @@ api.registerProvider({
- OpenRouter uses `capabilities`, `wrapStreamFn`, and `isCacheTtlEligible`
to keep provider-specific request headers, routing metadata, reasoning
patches, and prompt-cache policy out of core.
- Moonshot uses `catalog` plus `wrapStreamFn` because it still uses the shared
OpenAI transport but needs provider-owned thinking payload normalization.
- Kilocode uses `catalog`, `capabilities`, `wrapStreamFn`, and
`isCacheTtlEligible` because it needs provider-owned request headers,
reasoning payload normalization, Gemini transcript hints, and Anthropic
cache-TTL gating.
- Catalog-only bundled providers such as `byteplus`, `cloudflare-ai-gateway`,
`huggingface`, `kimi-coding`, `minimax`, `minimax-portal`, `modelstudio`,
`nvidia`, `qianfan`, `qwen-portal`, `synthetic`, `together`, `venice`,
`vercel-ai-gateway`, `volcengine`, and `xiaomi` use `catalog` only.
## Load pipeline
@@ -561,18 +588,44 @@ OpenClaw scans, in order:
- `~/.openclaw/extensions/*.ts`
- `~/.openclaw/extensions/*/index.ts`
4. Bundled extensions (shipped with OpenClaw, mostly disabled by default)
4. Bundled extensions (shipped with OpenClaw; mixed default-on/default-off)
- `<openclaw>/extensions/*`
Most bundled plugins must be enabled explicitly via
`plugins.entries.<id>.enabled` or `openclaw plugins enable <id>`.
Many bundled provider plugins are enabled by default so model catalogs/runtime
hooks stay available without extra setup. Others still require explicit
enablement via `plugins.entries.<id>.enabled` or
`openclaw plugins enable <id>`.
Default-on bundled plugin exceptions:
Default-on bundled plugin examples:
- `byteplus`
- `cloudflare-ai-gateway`
- `device-pair`
- `github-copilot`
- `huggingface`
- `kilocode`
- `kimi-coding`
- `minimax`
- `minimax-portal-auth`
- `modelstudio`
- `moonshot`
- `nvidia`
- `ollama`
- `openai-codex`
- `openrouter`
- `phone-control`
- `qianfan`
- `qwen-portal-auth`
- `sglang`
- `synthetic`
- `talk-voice`
- `together`
- `venice`
- `vercel-ai-gateway`
- `vllm`
- `volcengine`
- `xiaomi`
- active memory slot plugin (default slot: `memory-core`)
Installed plugins are enabled by default, but can be disabled the same way.
@@ -628,9 +681,8 @@ Enablement is resolved after discovery:
- channel config implicitly enables the bundled channel plugin
- exclusive slots can force-enable the selected plugin for that slot
In current core, bundled default-on ids include local/provider helpers such as
`ollama`, `sglang`, `vllm`, plus `device-pair`, `phone-control`, and
`talk-voice`.
In current core, bundled default-on ids include the local/provider helpers
above plus the active memory slot plugin.
### Package packs

View File

@@ -6,6 +6,9 @@ import {
type ProviderAuthResult,
type ProviderCatalogContext,
} from "openclaw/plugin-sdk/minimax-portal-auth";
import { ensureAuthProfileStore, listProfilesForProvider } from "../../src/agents/auth-profiles.js";
import { MINIMAX_OAUTH_MARKER } from "../../src/agents/model-auth-markers.js";
import { buildMinimaxPortalProvider } from "../../src/agents/models-config.providers.static.js";
import { loginMiniMaxPortalOAuth, type MiniMaxRegion } from "./oauth.js";
const PROVIDER_ID = "minimax-portal";
@@ -13,8 +16,6 @@ const PROVIDER_LABEL = "MiniMax";
const DEFAULT_MODEL = "MiniMax-M2.5";
const DEFAULT_BASE_URL_CN = "https://api.minimaxi.com/anthropic";
const DEFAULT_BASE_URL_GLOBAL = "https://api.minimax.io/anthropic";
const DEFAULT_CONTEXT_WINDOW = 200000;
const DEFAULT_MAX_TOKENS = 8192;
function getDefaultBaseUrl(region: MiniMaxRegion): string {
return region === "cn" ? DEFAULT_BASE_URL_CN : DEFAULT_BASE_URL_GLOBAL;
@@ -24,55 +25,24 @@ function modelRef(modelId: string): string {
return `${PROVIDER_ID}/${modelId}`;
}
function buildModelDefinition(params: {
id: string;
name: string;
input: Array<"text" | "image">;
reasoning?: boolean;
}) {
return {
id: params.id,
name: params.name,
reasoning: params.reasoning ?? false,
input: params.input,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: DEFAULT_CONTEXT_WINDOW,
maxTokens: DEFAULT_MAX_TOKENS,
};
}
function buildProviderCatalog(params: { baseUrl: string; apiKey: string }) {
return {
...buildMinimaxPortalProvider(),
baseUrl: params.baseUrl,
apiKey: params.apiKey,
api: "anthropic-messages" as const,
models: [
buildModelDefinition({
id: "MiniMax-M2.5",
name: "MiniMax M2.5",
input: ["text"],
}),
buildModelDefinition({
id: "MiniMax-M2.5-highspeed",
name: "MiniMax M2.5 Highspeed",
input: ["text"],
reasoning: true,
}),
buildModelDefinition({
id: "MiniMax-M2.5-Lightning",
name: "MiniMax M2.5 Lightning",
input: ["text"],
reasoning: true,
}),
],
};
}
function resolveCatalog(ctx: ProviderCatalogContext) {
const explicitProvider = ctx.config.models?.providers?.[PROVIDER_ID];
const apiKey =
ctx.resolveProviderApiKey(PROVIDER_ID).apiKey ??
(typeof explicitProvider?.apiKey === "string" ? explicitProvider.apiKey.trim() : undefined);
const envApiKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey;
const authStore = ensureAuthProfileStore(ctx.agentDir, {
allowKeychainPrompt: false,
});
const hasProfiles = listProfilesForProvider(authStore, PROVIDER_ID).length > 0;
const explicitApiKey =
typeof explicitProvider?.apiKey === "string" ? explicitProvider.apiKey.trim() : undefined;
const apiKey = envApiKey ?? explicitApiKey ?? (hasProfiles ? MINIMAX_OAUTH_MARKER : undefined);
if (!apiKey) {
return null;
}
@@ -167,7 +137,6 @@ const minimaxPortalPlugin = {
id: PROVIDER_ID,
label: PROVIDER_LABEL,
docsPath: "/providers/minimax",
aliases: ["minimax"],
catalog: {
run: async (ctx: ProviderCatalogContext) => resolveCatalog(ctx),
},

View File

@@ -5,6 +5,8 @@ import {
type ProviderAuthContext,
type ProviderCatalogContext,
} from "openclaw/plugin-sdk/qwen-portal-auth";
import { ensureAuthProfileStore, listProfilesForProvider } from "../../src/agents/auth-profiles.js";
import { QWEN_OAUTH_MARKER } from "../../src/agents/model-auth-markers.js";
import { loginQwenPortalOAuth } from "./oauth.js";
const PROVIDER_ID = "qwen-portal";
@@ -58,9 +60,14 @@ function buildProviderCatalog(params: { baseUrl: string; apiKey: string }) {
function resolveCatalog(ctx: ProviderCatalogContext) {
const explicitProvider = ctx.config.models?.providers?.[PROVIDER_ID];
const apiKey =
ctx.resolveProviderApiKey(PROVIDER_ID).apiKey ??
(typeof explicitProvider?.apiKey === "string" ? explicitProvider.apiKey.trim() : undefined);
const envApiKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey;
const authStore = ensureAuthProfileStore(ctx.agentDir, {
allowKeychainPrompt: false,
});
const hasProfiles = listProfilesForProvider(authStore, PROVIDER_ID).length > 0;
const explicitApiKey =
typeof explicitProvider?.apiKey === "string" ? explicitProvider.apiKey.trim() : undefined;
const apiKey = envApiKey ?? explicitApiKey ?? (hasProfiles ? QWEN_OAUTH_MARKER : undefined);
if (!apiKey) {
return null;
}

View File

@@ -7,10 +7,8 @@ import {
MOONSHOT_CN_BASE_URL,
} from "../commands/onboard-auth.models.js";
import { captureEnv } from "../test-utils/env.js";
import {
applyNativeStreamingUsageCompat,
resolveImplicitProviders,
} from "./models-config.providers.js";
import { resolveImplicitProvidersForTest } from "./models-config.e2e-harness.js";
import { applyNativeStreamingUsageCompat } from "./models-config.providers.js";
import { buildMoonshotProvider } from "./models-config.providers.static.js";
describe("moonshot implicit provider (#33637)", () => {
@@ -20,7 +18,7 @@ describe("moonshot implicit provider (#33637)", () => {
process.env.MOONSHOT_API_KEY = "sk-test-cn";
try {
const providers = await resolveImplicitProviders({
const providers = await resolveImplicitProvidersForTest({
agentDir,
explicitProviders: {
moonshot: {
@@ -55,7 +53,7 @@ describe("moonshot implicit provider (#33637)", () => {
process.env.MOONSHOT_API_KEY = "sk-test-custom";
try {
const providers = await resolveImplicitProviders({
const providers = await resolveImplicitProvidersForTest({
agentDir,
explicitProviders: {
moonshot: {
@@ -79,7 +77,7 @@ describe("moonshot implicit provider (#33637)", () => {
process.env.MOONSHOT_API_KEY = "sk-test";
try {
const providers = await resolveImplicitProviders({ agentDir });
const providers = await resolveImplicitProvidersForTest({ agentDir });
expect(providers?.moonshot).toBeDefined();
expect(providers?.moonshot?.baseUrl).toBe(MOONSHOT_AI_BASE_URL);
expect(providers?.moonshot?.models?.[0]?.compat?.supportsUsageInStreaming).toBeUndefined();

View File

@@ -4,35 +4,9 @@ import { isRecord } from "../utils.js";
import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js";
import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.js";
import { discoverBedrockModels } from "./bedrock-discovery.js";
import {
buildCloudflareAiGatewayModelDefinition,
resolveCloudflareAiGatewayBaseUrl,
} from "./cloudflare-ai-gateway.js";
import { normalizeGoogleModelId } from "./model-id-normalization.js";
import { resolveOllamaApiBase } from "./models-config.providers.discovery.js";
import {
buildHuggingfaceProvider,
buildKilocodeProviderWithDiscovery,
buildVeniceProvider,
buildVercelAiGatewayProvider,
resolveOllamaApiBase,
} from "./models-config.providers.discovery.js";
import {
buildBytePlusCodingProvider,
buildBytePlusProvider,
buildDoubaoCodingProvider,
buildDoubaoProvider,
buildKimiCodingProvider,
buildKilocodeProvider,
buildMinimaxPortalProvider,
buildMinimaxProvider,
buildModelStudioProvider,
buildMoonshotProvider,
buildNvidiaProvider,
buildQianfanProvider,
buildQwenPortalProvider,
buildSyntheticProvider,
buildTogetherProvider,
buildXiaomiProvider,
QIANFAN_BASE_URL,
QIANFAN_DEFAULT_MODEL_ID,
XIAOMI_DEFAULT_MODEL_ID,
@@ -57,8 +31,6 @@ import {
runProviderCatalog,
} from "../plugins/provider-discovery.js";
import {
MINIMAX_OAUTH_MARKER,
QWEN_OAUTH_MARKER,
isNonSecretApiKeyMarker,
resolveNonEnvSecretRefApiKeyMarker,
resolveNonEnvSecretRefHeaderValueMarker,
@@ -647,47 +619,6 @@ type ImplicitProviderContext = ImplicitProviderParams & {
resolveProviderApiKey: ProviderApiKeyResolver;
};
type ImplicitProviderLoader = (
ctx: ImplicitProviderContext,
) => Promise<Record<string, ProviderConfig> | undefined>;
function withApiKey(
providerKey: string,
build: (params: {
apiKey: string;
discoveryApiKey?: string;
explicitProvider?: ProviderConfig;
}) => ProviderConfig | Promise<ProviderConfig>,
): ImplicitProviderLoader {
return async (ctx) => {
const { apiKey, discoveryApiKey } = ctx.resolveProviderApiKey(providerKey);
if (!apiKey) {
return undefined;
}
return {
[providerKey]: await build({
apiKey,
discoveryApiKey,
explicitProvider: ctx.explicitProviders?.[providerKey],
}),
};
};
}
function withProfilePresence(
providerKey: string,
build: () => ProviderConfig | Promise<ProviderConfig>,
): ImplicitProviderLoader {
return async (ctx) => {
if (listProfilesForProvider(ctx.authStore, providerKey).length === 0) {
return undefined;
}
return {
[providerKey]: await build(),
};
};
}
function mergeImplicitProviderSet(
target: Record<string, ProviderConfig>,
additions: Record<string, ProviderConfig> | undefined,
@@ -700,155 +631,6 @@ function mergeImplicitProviderSet(
}
}
const SIMPLE_IMPLICIT_PROVIDER_LOADERS: ImplicitProviderLoader[] = [
withApiKey("minimax", async ({ apiKey }) => ({ ...buildMinimaxProvider(), apiKey })),
withApiKey("moonshot", async ({ apiKey, explicitProvider }) => {
const explicitBaseUrl = explicitProvider?.baseUrl;
return {
...buildMoonshotProvider(),
...(typeof explicitBaseUrl === "string" && explicitBaseUrl.trim()
? { baseUrl: explicitBaseUrl.trim() }
: {}),
apiKey,
};
}),
withApiKey("kimi-coding", async ({ apiKey, explicitProvider }) => {
const builtInProvider = buildKimiCodingProvider();
const explicitBaseUrl = explicitProvider?.baseUrl;
const explicitHeaders = isRecord(explicitProvider?.headers)
? (explicitProvider.headers as ProviderConfig["headers"])
: undefined;
return {
...builtInProvider,
...(typeof explicitBaseUrl === "string" && explicitBaseUrl.trim()
? { baseUrl: explicitBaseUrl.trim() }
: {}),
...(explicitHeaders
? {
headers: {
...builtInProvider.headers,
...explicitHeaders,
},
}
: {}),
apiKey,
};
}),
withApiKey("synthetic", async ({ apiKey }) => ({ ...buildSyntheticProvider(), apiKey })),
withApiKey("venice", async ({ apiKey }) => ({ ...(await buildVeniceProvider()), apiKey })),
withApiKey("xiaomi", async ({ apiKey }) => ({ ...buildXiaomiProvider(), apiKey })),
withApiKey("vercel-ai-gateway", async ({ apiKey }) => ({
...(await buildVercelAiGatewayProvider()),
apiKey,
})),
withApiKey("together", async ({ apiKey }) => ({ ...buildTogetherProvider(), apiKey })),
withApiKey("huggingface", async ({ apiKey, discoveryApiKey }) => ({
...(await buildHuggingfaceProvider(discoveryApiKey)),
apiKey,
})),
withApiKey("qianfan", async ({ apiKey }) => ({ ...buildQianfanProvider(), apiKey })),
withApiKey("modelstudio", async ({ apiKey, explicitProvider }) => {
const explicitBaseUrl = explicitProvider?.baseUrl;
return {
...buildModelStudioProvider(),
...(typeof explicitBaseUrl === "string" && explicitBaseUrl.trim()
? { baseUrl: explicitBaseUrl.trim() }
: {}),
apiKey,
};
}),
withApiKey("nvidia", async ({ apiKey }) => ({ ...buildNvidiaProvider(), apiKey })),
withApiKey("kilocode", async ({ apiKey }) => ({
...(await buildKilocodeProviderWithDiscovery()),
apiKey,
})),
];
const PROFILE_IMPLICIT_PROVIDER_LOADERS: ImplicitProviderLoader[] = [
async (ctx) => {
const envKey = resolveEnvApiKeyVarName("minimax-portal", ctx.env);
const hasProfiles = listProfilesForProvider(ctx.authStore, "minimax-portal").length > 0;
if (!envKey && !hasProfiles) {
return undefined;
}
return {
"minimax-portal": {
...buildMinimaxPortalProvider(),
apiKey: MINIMAX_OAUTH_MARKER,
},
};
},
withProfilePresence("qwen-portal", async () => ({
...buildQwenPortalProvider(),
apiKey: QWEN_OAUTH_MARKER,
})),
];
const PAIRED_IMPLICIT_PROVIDER_LOADERS: ImplicitProviderLoader[] = [
async (ctx) => {
const volcengineKey = ctx.resolveProviderApiKey("volcengine").apiKey;
if (!volcengineKey) {
return undefined;
}
return {
volcengine: { ...buildDoubaoProvider(), apiKey: volcengineKey },
"volcengine-plan": {
...buildDoubaoCodingProvider(),
apiKey: volcengineKey,
},
};
},
async (ctx) => {
const byteplusKey = ctx.resolveProviderApiKey("byteplus").apiKey;
if (!byteplusKey) {
return undefined;
}
return {
byteplus: { ...buildBytePlusProvider(), apiKey: byteplusKey },
"byteplus-plan": {
...buildBytePlusCodingProvider(),
apiKey: byteplusKey,
},
};
},
];
async function resolveCloudflareAiGatewayImplicitProvider(
ctx: ImplicitProviderContext,
): Promise<Record<string, ProviderConfig> | undefined> {
const cloudflareProfiles = listProfilesForProvider(ctx.authStore, "cloudflare-ai-gateway");
for (const profileId of cloudflareProfiles) {
const cred = ctx.authStore.profiles[profileId];
if (cred?.type !== "api_key") {
continue;
}
const accountId = cred.metadata?.accountId?.trim();
const gatewayId = cred.metadata?.gatewayId?.trim();
if (!accountId || !gatewayId) {
continue;
}
const baseUrl = resolveCloudflareAiGatewayBaseUrl({ accountId, gatewayId });
if (!baseUrl) {
continue;
}
const envVarApiKey = resolveEnvApiKeyVarName("cloudflare-ai-gateway", ctx.env);
const profileApiKey = resolveApiKeyFromCredential(cred, ctx.env)?.apiKey;
const apiKey = envVarApiKey ?? profileApiKey ?? "";
if (!apiKey) {
continue;
}
return {
"cloudflare-ai-gateway": {
baseUrl,
api: "anthropic-messages",
apiKey,
models: [buildCloudflareAiGatewayModelDefinition()],
},
};
}
return undefined;
}
async function resolvePluginImplicitProviders(
ctx: ImplicitProviderContext,
order: import("../plugins/types.js").ProviderDiscoveryOrder,
@@ -860,10 +642,23 @@ async function resolvePluginImplicitProviders(
});
const byOrder = groupPluginDiscoveryProvidersByOrder(providers);
const discovered: Record<string, ProviderConfig> = {};
const catalogConfig =
ctx.explicitProviders && Object.keys(ctx.explicitProviders).length > 0
? {
...ctx.config,
models: {
...ctx.config?.models,
providers: {
...ctx.config?.models?.providers,
...ctx.explicitProviders,
},
},
}
: (ctx.config ?? {});
for (const provider of byOrder[order]) {
const result = await runProviderCatalog({
provider,
config: ctx.config ?? {},
config: catalogConfig,
agentDir: ctx.agentDir,
workspaceDir: ctx.workspaceDir,
env: ctx.env,
@@ -912,19 +707,9 @@ export async function resolveImplicitProviders(
resolveProviderApiKey,
};
for (const loader of SIMPLE_IMPLICIT_PROVIDER_LOADERS) {
mergeImplicitProviderSet(providers, await loader(context));
}
mergeImplicitProviderSet(providers, await resolvePluginImplicitProviders(context, "simple"));
for (const loader of PROFILE_IMPLICIT_PROVIDER_LOADERS) {
mergeImplicitProviderSet(providers, await loader(context));
}
mergeImplicitProviderSet(providers, await resolvePluginImplicitProviders(context, "profile"));
for (const loader of PAIRED_IMPLICIT_PROVIDER_LOADERS) {
mergeImplicitProviderSet(providers, await loader(context));
}
mergeImplicitProviderSet(providers, await resolvePluginImplicitProviders(context, "paired"));
mergeImplicitProviderSet(providers, await resolveCloudflareAiGatewayImplicitProvider(context));
mergeImplicitProviderSet(providers, await resolvePluginImplicitProviders(context, "late"));
const implicitBedrock = await resolveImplicitBedrockProvider({

View File

@@ -2,6 +2,7 @@ import type { StreamFn } from "@mariozechner/pi-agent-core";
import type { Context, Model } from "@mariozechner/pi-ai";
import { createAssistantMessageEventStream } from "@mariozechner/pi-ai";
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import { applyExtraParamsToAgent } from "./extra-params.js";
type StreamPayload = {
@@ -17,8 +18,17 @@ function runOpenRouterPayload(payload: StreamPayload, modelId: string) {
return createAssistantMessageEventStream();
};
const agent = { streamFn: baseStreamFn };
const cfg = {
plugins: {
entries: {
openrouter: {
enabled: true,
},
},
},
} satisfies OpenClawConfig;
applyExtraParamsToAgent(agent, undefined, "openrouter", modelId);
applyExtraParamsToAgent(agent, cfg, "openrouter", modelId);
const model = {
api: "openai-completions",

View File

@@ -20,8 +20,8 @@ import {
import { log } from "./logger.js";
import {
createMoonshotThinkingWrapper,
createSiliconFlowThinkingWrapper,
resolveMoonshotThinkingType,
createSiliconFlowThinkingWrapper,
shouldApplyMoonshotPayloadCompat,
shouldApplySiliconFlowThinkingOffCompat,
} from "./moonshot-stream-wrappers.js";
@@ -33,7 +33,6 @@ import {
resolveOpenAIFastMode,
resolveOpenAIServiceTier,
} from "./openai-stream-wrappers.js";
import { createKilocodeWrapper, isProxyReasoningUnsupported } from "./proxy-stream-wrappers.js";
/**
* Resolve provider-specific extra params from model config.
@@ -366,42 +365,33 @@ export function applyExtraParamsToAgent(
agent.streamFn = createSiliconFlowThinkingWrapper(agent.streamFn);
}
if (shouldApplyMoonshotPayloadCompat({ provider, modelId })) {
const moonshotThinkingType = resolveMoonshotThinkingType({
agent.streamFn = createAnthropicToolPayloadCompatibilityWrapper(agent.streamFn);
const providerStreamBase = agent.streamFn;
const pluginWrappedStreamFn = wrapProviderStreamFn({
provider,
config: cfg,
context: {
config: cfg,
provider,
modelId,
extraParams: effectiveExtraParams,
thinkingLevel,
streamFn: providerStreamBase,
},
});
agent.streamFn = pluginWrappedStreamFn ?? providerStreamBase;
const providerWrapperHandled =
pluginWrappedStreamFn !== undefined && pluginWrappedStreamFn !== providerStreamBase;
if (!providerWrapperHandled && shouldApplyMoonshotPayloadCompat({ provider, modelId })) {
// Preserve the legacy Moonshot compatibility path when no plugin wrapper
// actually handled the stream function. This covers tests/disabled plugins
// and Ollama Cloud Kimi models until they gain a dedicated runtime hook.
const thinkingType = resolveMoonshotThinkingType({
configuredThinking: effectiveExtraParams?.thinking,
thinkingLevel,
});
if (moonshotThinkingType) {
log.debug(
`applying Moonshot thinking=${moonshotThinkingType} payload wrapper for ${provider}/${modelId}`,
);
}
agent.streamFn = createMoonshotThinkingWrapper(agent.streamFn, moonshotThinkingType);
}
agent.streamFn = createAnthropicToolPayloadCompatibilityWrapper(agent.streamFn);
agent.streamFn =
wrapProviderStreamFn({
provider,
config: cfg,
context: {
config: cfg,
provider,
modelId,
extraParams: effectiveExtraParams,
thinkingLevel,
streamFn: agent.streamFn,
},
}) ?? agent.streamFn;
if (provider === "kilocode") {
log.debug(`applying Kilocode feature header for ${provider}/${modelId}`);
// kilo/auto is a dynamic routing model — skip reasoning injection
// (same rationale as OpenRouter "auto"). See: openclaw/openclaw#24851
// Also skip for models known to reject reasoning.effort (e.g. x-ai/*).
const kilocodeThinkingLevel =
modelId === "kilo/auto" || isProxyReasoningUnsupported(modelId) ? undefined : thinkingLevel;
agent.streamFn = createKilocodeWrapper(agent.streamFn, kilocodeThinkingLevel);
agent.streamFn = createMoonshotThinkingWrapper(agent.streamFn, thinkingType);
}
if (provider === "amazon-bedrock" && !isAnthropicBedrockModel(modelId)) {

View File

@@ -16,6 +16,15 @@ const resolveProviderCapabilitiesWithPluginMock = vi.fn((params: { provider: str
return {
dropThinkingBlockModelHints: ["claude"],
};
case "kilocode":
return {
geminiThoughtSignatureSanitization: true,
geminiThoughtSignatureModelHints: ["gemini"],
};
case "kimi-coding":
return {
preserveAnthropicThinkingSignatures: false,
};
default:
return undefined;
}

View File

@@ -36,11 +36,6 @@ const PROVIDER_CAPABILITIES: Record<string, Partial<ProviderCapabilities>> = {
providerFamily: "anthropic",
dropThinkingBlockModelHints: ["claude"],
},
// kimi-coding natively supports Anthropic tool framing (input_schema);
// converting to OpenAI format causes XML text fallback instead of tool_use blocks.
"kimi-coding": {
preserveAnthropicThinkingSignatures: false,
},
mistral: {
transcriptToolCallIdMode: "strict9",
transcriptToolCallIdModelHints: [
@@ -66,10 +61,6 @@ const PROVIDER_CAPABILITIES: Record<string, Partial<ProviderCapabilities>> = {
geminiThoughtSignatureSanitization: true,
geminiThoughtSignatureModelHints: ["gemini"],
},
kilocode: {
geminiThoughtSignatureSanitization: true,
geminiThoughtSignatureModelHints: ["gemini"],
},
};
export function resolveProviderCapabilities(provider?: string | null): ProviderCapabilities {

View File

@@ -24,15 +24,33 @@ export type NormalizedPluginsConfig = {
};
export const BUNDLED_ENABLED_BY_DEFAULT = new Set<string>([
"byteplus",
"cloudflare-ai-gateway",
"device-pair",
"github-copilot",
"huggingface",
"kilocode",
"kimi-coding",
"minimax",
"minimax-portal-auth",
"modelstudio",
"moonshot",
"nvidia",
"ollama",
"openai-codex",
"openrouter",
"phone-control",
"qianfan",
"qwen-portal-auth",
"sglang",
"synthetic",
"talk-voice",
"together",
"venice",
"vercel-ai-gateway",
"vllm",
"volcengine",
"xiaomi",
]);
const normalizeList = (value: unknown): string[] => {