mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-26 08:31:55 +00:00
feat(plugins): move provider runtimes into bundled plugins
This commit is contained in:
@@ -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 pi‑ai 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/Anthropic‑compatible 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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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[] => {
|
||||
|
||||
Reference in New Issue
Block a user