mirror of
https://fastgit.cc/github.com/openclaw/openclaw
synced 2026-04-30 14:02:56 +08:00
refactor(plugins): move provider routing metadata to manifests
This commit is contained in:
@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Changes
|
||||
|
||||
- Plugins/providers: move pre-runtime model-id normalization, provider endpoint host metadata, and OpenAI-compatible request-family hints into plugin manifests so core no longer carries bundled-provider routing tables. Thanks @codex.
|
||||
- Plugins/install: allow `OPENCLAW_PLUGIN_STAGE_DIR` to contain layered runtime-dependency roots, resolving read-only preinstalled deps before installing missing deps into the final writable root. Fixes #72396. Thanks @liorb-mountapps.
|
||||
- Control UI: polish the quick settings dashboard grid so common cards align across desktop, tablet, and mobile layouts without wasting horizontal space. Thanks @BunsDev.
|
||||
- Matrix/E2EE: add `openclaw matrix encryption setup` to enable Matrix encryption, bootstrap recovery, and print verification status from one setup flow. Thanks @gumadeiras.
|
||||
|
||||
@@ -78,12 +78,26 @@ or npm install metadata. Those belong in your plugin code and `package.json`.
|
||||
"modelSupport": {
|
||||
"modelPrefixes": ["router-"]
|
||||
},
|
||||
"modelIdNormalization": {
|
||||
"providers": {
|
||||
"openrouter": {
|
||||
"prefixWhenBare": "openrouter"
|
||||
}
|
||||
}
|
||||
},
|
||||
"providerEndpoints": [
|
||||
{
|
||||
"endpointClass": "xai-native",
|
||||
"hosts": ["api.x.ai"]
|
||||
"endpointClass": "openrouter",
|
||||
"hostSuffixes": ["openrouter.ai"]
|
||||
}
|
||||
],
|
||||
"providerRequest": {
|
||||
"providers": {
|
||||
"openrouter": {
|
||||
"family": "openrouter"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cliBackends": ["openrouter-cli"],
|
||||
"syntheticAuthRefs": ["openrouter-cli"],
|
||||
"providerAuthEnvVars": {
|
||||
@@ -145,7 +159,9 @@ or npm install metadata. 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. |
|
||||
| `modelCatalog` | No | `object` | Declarative model catalog metadata for providers owned by this plugin. This is the control-plane contract for future read-only listing, onboarding, model pickers, aliases, and suppression without loading plugin runtime. |
|
||||
| `modelPricing` | No | `object` | Provider-owned external pricing lookup policy. Use it to opt local/self-hosted providers out of remote pricing catalogs or map provider refs to OpenRouter/LiteLLM catalog ids without hardcoding provider ids in core. |
|
||||
| `modelIdNormalization` | No | `object` | Provider-owned model-id alias/prefix cleanup that must run before provider runtime loads. |
|
||||
| `providerEndpoints` | No | `object[]` | Manifest-owned endpoint host/baseUrl metadata for provider routes that core must classify before provider runtime loads. |
|
||||
| `providerRequest` | No | `object` | Cheap provider-family and request-compatibility metadata used by generic request policy before provider runtime loads. |
|
||||
| `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. |
|
||||
@@ -749,6 +765,87 @@ Do not put runtime-only data in `modelCatalog`. If a provider needs account
|
||||
state, an API request, or local process discovery to know the complete model
|
||||
set, declare that provider as `refreshable` or `runtime` in `discovery`.
|
||||
|
||||
## modelIdNormalization reference
|
||||
|
||||
Use `modelIdNormalization` for cheap provider-owned model-id cleanup that must
|
||||
happen before provider runtime loads. This keeps aliases such as short model
|
||||
names, provider-local legacy ids, and proxy prefix rules in the owning plugin
|
||||
manifest instead of in core model-selection tables.
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": ["anthropic", "openrouter"],
|
||||
"modelIdNormalization": {
|
||||
"providers": {
|
||||
"anthropic": {
|
||||
"aliases": {
|
||||
"sonnet-4.6": "claude-sonnet-4-6"
|
||||
}
|
||||
},
|
||||
"openrouter": {
|
||||
"prefixWhenBare": "openrouter"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Provider fields:
|
||||
|
||||
| Field | Type | What it means |
|
||||
| ------------------------------------ | ----------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| `aliases` | `Record<string,string>` | Case-insensitive exact model-id aliases. Values are returned as written. |
|
||||
| `stripPrefixes` | `string[]` | Prefixes to remove before alias lookup, useful for legacy provider/model duplication. |
|
||||
| `prefixWhenBare` | `string` | Prefix to add when the normalized model id does not already contain `/`. |
|
||||
| `prefixWhenBareAfterAliasStartsWith` | `object[]` | Conditional bare-id prefix rules after alias lookup, keyed by `modelPrefix` and `prefix`. |
|
||||
|
||||
## providerEndpoints reference
|
||||
|
||||
Use `providerEndpoints` for endpoint classification that generic request policy
|
||||
must know before provider runtime loads. Core still owns the meaning of each
|
||||
`endpointClass`; plugin manifests own the host and base URL metadata.
|
||||
|
||||
Endpoint fields:
|
||||
|
||||
| Field | Type | What it means |
|
||||
| ------------------------------ | ---------- | ---------------------------------------------------------------------------------------------- |
|
||||
| `endpointClass` | `string` | Known core endpoint class, such as `openrouter`, `moonshot-native`, or `google-vertex`. |
|
||||
| `hosts` | `string[]` | Exact hostnames that map to the endpoint class. |
|
||||
| `hostSuffixes` | `string[]` | Host suffixes that map to the endpoint class. Prefix with `.` for domain suffix-only matching. |
|
||||
| `baseUrls` | `string[]` | Exact normalized HTTP(S) base URLs that map to the endpoint class. |
|
||||
| `googleVertexRegion` | `string` | Static Google Vertex region for exact global hosts. |
|
||||
| `googleVertexRegionHostSuffix` | `string` | Suffix to strip from matching hosts to expose the Google Vertex region prefix. |
|
||||
|
||||
## providerRequest reference
|
||||
|
||||
Use `providerRequest` for cheap request-compatibility metadata that generic
|
||||
request policy needs without loading provider runtime. Keep behavior-specific
|
||||
payload rewriting in provider runtime hooks or shared provider-family helpers.
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": ["vllm"],
|
||||
"providerRequest": {
|
||||
"providers": {
|
||||
"vllm": {
|
||||
"family": "vllm",
|
||||
"openAICompletions": {
|
||||
"supportsStreamingUsage": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Provider fields:
|
||||
|
||||
| Field | Type | What it means |
|
||||
| --------------------- | ------------ | -------------------------------------------------------------------------------------- |
|
||||
| `family` | `string` | Provider family label used by generic request compatibility decisions and diagnostics. |
|
||||
| `compatibilityFamily` | `"moonshot"` | Optional provider-family compatibility bucket for shared request helpers. |
|
||||
| `openAICompletions` | `object` | OpenAI-compatible completions request flags, currently `supportsStreamingUsage`. |
|
||||
|
||||
## modelPricing reference
|
||||
|
||||
Use `modelPricing` when a provider needs control-plane pricing behavior before
|
||||
|
||||
@@ -6,6 +6,18 @@
|
||||
"modelSupport": {
|
||||
"modelPrefixes": ["claude-"]
|
||||
},
|
||||
"modelIdNormalization": {
|
||||
"providers": {
|
||||
"anthropic": {
|
||||
"aliases": {
|
||||
"opus-4.6": "claude-opus-4-6",
|
||||
"opus-4.5": "claude-opus-4-5",
|
||||
"sonnet-4.6": "claude-sonnet-4-6",
|
||||
"sonnet-4.5": "claude-sonnet-4-5"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modelPricing": {
|
||||
"providers": {
|
||||
"anthropic": {
|
||||
@@ -15,6 +27,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"providerEndpoints": [
|
||||
{
|
||||
"endpointClass": "anthropic-public",
|
||||
"hosts": ["api.anthropic.com"]
|
||||
}
|
||||
],
|
||||
"providerRequest": {
|
||||
"providers": {
|
||||
"anthropic": {
|
||||
"family": "anthropic"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cliBackends": ["claude-cli"],
|
||||
"syntheticAuthRefs": ["claude-cli"],
|
||||
"providerAuthEnvVars": {
|
||||
|
||||
@@ -2,6 +2,19 @@
|
||||
"id": "chutes",
|
||||
"enabledByDefault": true,
|
||||
"providers": ["chutes"],
|
||||
"providerEndpoints": [
|
||||
{
|
||||
"endpointClass": "chutes-native",
|
||||
"hosts": ["llm.chutes.ai"]
|
||||
}
|
||||
],
|
||||
"providerRequest": {
|
||||
"providers": {
|
||||
"chutes": {
|
||||
"family": "chutes"
|
||||
}
|
||||
}
|
||||
},
|
||||
"providerAuthEnvVars": {
|
||||
"chutes": ["CHUTES_API_KEY", "CHUTES_OAUTH_TOKEN"]
|
||||
},
|
||||
|
||||
@@ -3,6 +3,19 @@
|
||||
"enabledByDefault": true,
|
||||
"providerDiscoveryEntry": "./provider-discovery.ts",
|
||||
"providers": ["deepseek"],
|
||||
"providerEndpoints": [
|
||||
{
|
||||
"endpointClass": "deepseek-native",
|
||||
"hosts": ["api.deepseek.com"]
|
||||
}
|
||||
],
|
||||
"providerRequest": {
|
||||
"providers": {
|
||||
"deepseek": {
|
||||
"family": "deepseek"
|
||||
}
|
||||
}
|
||||
},
|
||||
"modelCatalog": {
|
||||
"providers": {
|
||||
"deepseek": {
|
||||
|
||||
@@ -2,6 +2,19 @@
|
||||
"id": "github-copilot",
|
||||
"enabledByDefault": true,
|
||||
"providers": ["github-copilot"],
|
||||
"providerEndpoints": [
|
||||
{
|
||||
"endpointClass": "github-copilot-native",
|
||||
"hostSuffixes": [".githubcopilot.com"]
|
||||
}
|
||||
],
|
||||
"providerRequest": {
|
||||
"providers": {
|
||||
"github-copilot": {
|
||||
"family": "github-copilot"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contracts": {
|
||||
"memoryEmbeddingProviders": ["github-copilot"]
|
||||
},
|
||||
|
||||
@@ -1,8 +1,32 @@
|
||||
{
|
||||
"id": "google",
|
||||
"enabledByDefault": true,
|
||||
"providers": ["google", "google-gemini-cli"],
|
||||
"providers": ["google", "google-gemini-cli", "google-vertex"],
|
||||
"autoEnableWhenConfiguredProviders": ["google-gemini-cli"],
|
||||
"modelIdNormalization": {
|
||||
"providers": {
|
||||
"google": {
|
||||
"aliases": {
|
||||
"gemini-3-pro": "gemini-3-pro-preview",
|
||||
"gemini-3-flash": "gemini-3-flash-preview",
|
||||
"gemini-3.1-pro": "gemini-3.1-pro-preview",
|
||||
"gemini-3.1-flash-lite": "gemini-3.1-flash-lite-preview",
|
||||
"gemini-3.1-flash": "gemini-3-flash-preview",
|
||||
"gemini-3.1-flash-preview": "gemini-3-flash-preview"
|
||||
}
|
||||
},
|
||||
"google-vertex": {
|
||||
"aliases": {
|
||||
"gemini-3-pro": "gemini-3-pro-preview",
|
||||
"gemini-3-flash": "gemini-3-flash-preview",
|
||||
"gemini-3.1-pro": "gemini-3.1-pro-preview",
|
||||
"gemini-3.1-flash-lite": "gemini-3.1-flash-lite-preview",
|
||||
"gemini-3.1-flash": "gemini-3-flash-preview",
|
||||
"gemini-3.1-flash-preview": "gemini-3-flash-preview"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modelPricing": {
|
||||
"providers": {
|
||||
"google-gemini-cli": {
|
||||
@@ -15,6 +39,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"providerEndpoints": [
|
||||
{
|
||||
"endpointClass": "google-generative-ai",
|
||||
"hosts": ["generativelanguage.googleapis.com"]
|
||||
},
|
||||
{
|
||||
"endpointClass": "google-vertex",
|
||||
"hosts": ["aiplatform.googleapis.com"],
|
||||
"googleVertexRegion": "global"
|
||||
},
|
||||
{
|
||||
"endpointClass": "google-vertex",
|
||||
"hostSuffixes": ["-aiplatform.googleapis.com"],
|
||||
"googleVertexRegionHostSuffix": "-aiplatform.googleapis.com"
|
||||
}
|
||||
],
|
||||
"providerRequest": {
|
||||
"providers": {
|
||||
"google": {
|
||||
"family": "google"
|
||||
},
|
||||
"google-gemini-cli": {
|
||||
"family": "google"
|
||||
},
|
||||
"google-vertex": {
|
||||
"family": "google"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cliBackends": ["google-gemini-cli"],
|
||||
"providerAuthEnvVars": {
|
||||
"google": ["GEMINI_API_KEY", "GOOGLE_API_KEY"]
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
{
|
||||
"id": "groq",
|
||||
"enabledByDefault": true,
|
||||
"providers": ["groq"],
|
||||
"providerEndpoints": [
|
||||
{
|
||||
"endpointClass": "groq-native",
|
||||
"hosts": ["api.groq.com"]
|
||||
}
|
||||
],
|
||||
"providerRequest": {
|
||||
"providers": {
|
||||
"groq": {
|
||||
"family": "groq"
|
||||
}
|
||||
}
|
||||
},
|
||||
"providerAuthEnvVars": {
|
||||
"groq": ["GROQ_API_KEY"]
|
||||
},
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
"id": "huggingface",
|
||||
"enabledByDefault": true,
|
||||
"providers": ["huggingface"],
|
||||
"modelIdNormalization": {
|
||||
"providers": {
|
||||
"huggingface": {
|
||||
"stripPrefixes": ["huggingface/"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"providerAuthEnvVars": {
|
||||
"huggingface": ["HUGGINGFACE_HUB_TOKEN", "HF_TOKEN"]
|
||||
},
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
"id": "kimi",
|
||||
"enabledByDefault": true,
|
||||
"providers": ["kimi", "kimi-coding"],
|
||||
"providerRequest": {
|
||||
"providers": {
|
||||
"kimi": {
|
||||
"family": "moonshot",
|
||||
"compatibilityFamily": "moonshot"
|
||||
},
|
||||
"kimi-coding": {
|
||||
"family": "moonshot",
|
||||
"compatibilityFamily": "moonshot"
|
||||
}
|
||||
}
|
||||
},
|
||||
"modelPricing": {
|
||||
"providers": {
|
||||
"kimi": {
|
||||
|
||||
@@ -2,6 +2,16 @@
|
||||
"id": "lmstudio",
|
||||
"enabledByDefault": true,
|
||||
"providers": ["lmstudio"],
|
||||
"providerRequest": {
|
||||
"providers": {
|
||||
"lmstudio": {
|
||||
"family": "lmstudio",
|
||||
"openAICompletions": {
|
||||
"supportsStreamingUsage": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modelPricing": {
|
||||
"providers": {
|
||||
"lmstudio": {
|
||||
|
||||
@@ -2,6 +2,19 @@
|
||||
"id": "mistral",
|
||||
"enabledByDefault": true,
|
||||
"providers": ["mistral"],
|
||||
"providerEndpoints": [
|
||||
{
|
||||
"endpointClass": "mistral-public",
|
||||
"hosts": ["api.mistral.ai"]
|
||||
}
|
||||
],
|
||||
"providerRequest": {
|
||||
"providers": {
|
||||
"mistral": {
|
||||
"family": "mistral"
|
||||
}
|
||||
}
|
||||
},
|
||||
"providerAuthEnvVars": {
|
||||
"mistral": ["MISTRAL_API_KEY"]
|
||||
},
|
||||
|
||||
@@ -3,6 +3,20 @@
|
||||
"enabledByDefault": true,
|
||||
"providerDiscoveryEntry": "./provider-discovery.ts",
|
||||
"providers": ["moonshot"],
|
||||
"providerEndpoints": [
|
||||
{
|
||||
"endpointClass": "moonshot-native",
|
||||
"baseUrls": ["https://api.moonshot.ai/v1", "https://api.moonshot.cn/v1"]
|
||||
}
|
||||
],
|
||||
"providerRequest": {
|
||||
"providers": {
|
||||
"moonshot": {
|
||||
"family": "moonshot",
|
||||
"compatibilityFamily": "moonshot"
|
||||
}
|
||||
}
|
||||
},
|
||||
"modelPricing": {
|
||||
"providers": {
|
||||
"moonshot": {
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
"id": "nvidia",
|
||||
"enabledByDefault": true,
|
||||
"providers": ["nvidia"],
|
||||
"modelIdNormalization": {
|
||||
"providers": {
|
||||
"nvidia": {
|
||||
"prefixWhenBare": "nvidia"
|
||||
}
|
||||
}
|
||||
},
|
||||
"providerAuthEnvVars": {
|
||||
"nvidia": ["NVIDIA_API_KEY"]
|
||||
},
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
"enabledByDefault": true,
|
||||
"providers": ["ollama"],
|
||||
"providerDiscoveryEntry": "./provider-discovery.ts",
|
||||
"providerRequest": {
|
||||
"providers": {
|
||||
"ollama": {
|
||||
"family": "ollama"
|
||||
}
|
||||
}
|
||||
},
|
||||
"modelPricing": {
|
||||
"providers": {
|
||||
"ollama": {
|
||||
|
||||
@@ -17,6 +17,30 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"providerEndpoints": [
|
||||
{
|
||||
"endpointClass": "openai-public",
|
||||
"hosts": ["api.openai.com"]
|
||||
},
|
||||
{
|
||||
"endpointClass": "openai-codex",
|
||||
"hosts": ["chatgpt.com"]
|
||||
},
|
||||
{
|
||||
"endpointClass": "azure-openai",
|
||||
"hostSuffixes": [".openai.azure.com"]
|
||||
}
|
||||
],
|
||||
"providerRequest": {
|
||||
"providers": {
|
||||
"openai": {
|
||||
"family": "openai-family"
|
||||
},
|
||||
"openai-codex": {
|
||||
"family": "openai-family"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cliBackends": ["codex-cli"],
|
||||
"providerAuthEnvVars": {
|
||||
"openai": ["OPENAI_API_KEY"]
|
||||
|
||||
@@ -2,6 +2,19 @@
|
||||
"id": "opencode-go",
|
||||
"enabledByDefault": true,
|
||||
"providers": ["opencode-go"],
|
||||
"providerEndpoints": [
|
||||
{
|
||||
"endpointClass": "opencode-native",
|
||||
"hostSuffixes": ["opencode.ai"]
|
||||
}
|
||||
],
|
||||
"providerRequest": {
|
||||
"providers": {
|
||||
"opencode-go": {
|
||||
"family": "opencode"
|
||||
}
|
||||
}
|
||||
},
|
||||
"providerAuthEnvVars": {
|
||||
"opencode-go": ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"]
|
||||
},
|
||||
|
||||
@@ -2,6 +2,19 @@
|
||||
"id": "opencode",
|
||||
"enabledByDefault": true,
|
||||
"providers": ["opencode"],
|
||||
"providerEndpoints": [
|
||||
{
|
||||
"endpointClass": "opencode-native",
|
||||
"hostSuffixes": ["opencode.ai"]
|
||||
}
|
||||
],
|
||||
"providerRequest": {
|
||||
"providers": {
|
||||
"opencode": {
|
||||
"family": "opencode"
|
||||
}
|
||||
}
|
||||
},
|
||||
"providerAuthEnvVars": {
|
||||
"opencode": ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"]
|
||||
},
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
"id": "openrouter",
|
||||
"enabledByDefault": true,
|
||||
"providers": ["openrouter"],
|
||||
"modelIdNormalization": {
|
||||
"providers": {
|
||||
"openrouter": {
|
||||
"prefixWhenBare": "openrouter"
|
||||
}
|
||||
}
|
||||
},
|
||||
"modelPricing": {
|
||||
"providers": {
|
||||
"openrouter": {
|
||||
@@ -12,6 +19,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"providerEndpoints": [
|
||||
{
|
||||
"endpointClass": "openrouter",
|
||||
"hostSuffixes": ["openrouter.ai"]
|
||||
}
|
||||
],
|
||||
"providerRequest": {
|
||||
"providers": {
|
||||
"openrouter": {
|
||||
"family": "openrouter"
|
||||
}
|
||||
}
|
||||
},
|
||||
"providerAuthEnvVars": {
|
||||
"openrouter": ["OPENROUTER_API_KEY"]
|
||||
},
|
||||
|
||||
@@ -1,7 +1,34 @@
|
||||
{
|
||||
"id": "qwen",
|
||||
"enabledByDefault": true,
|
||||
"providers": ["qwen"],
|
||||
"providers": ["qwen", "qwencloud", "modelstudio", "dashscope"],
|
||||
"providerEndpoints": [
|
||||
{
|
||||
"endpointClass": "modelstudio-native",
|
||||
"baseUrls": [
|
||||
"https://coding-intl.dashscope.aliyuncs.com/v1",
|
||||
"https://coding.dashscope.aliyuncs.com/v1",
|
||||
"https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
|
||||
]
|
||||
}
|
||||
],
|
||||
"providerRequest": {
|
||||
"providers": {
|
||||
"qwen": {
|
||||
"family": "modelstudio"
|
||||
},
|
||||
"qwencloud": {
|
||||
"family": "modelstudio"
|
||||
},
|
||||
"modelstudio": {
|
||||
"family": "modelstudio"
|
||||
},
|
||||
"dashscope": {
|
||||
"family": "modelstudio"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contracts": {
|
||||
"mediaUnderstandingProviders": ["qwen"],
|
||||
"videoGenerationProviders": ["qwen"]
|
||||
|
||||
@@ -2,6 +2,16 @@
|
||||
"id": "sglang",
|
||||
"enabledByDefault": true,
|
||||
"providers": ["sglang"],
|
||||
"providerRequest": {
|
||||
"providers": {
|
||||
"sglang": {
|
||||
"family": "sglang",
|
||||
"openAICompletions": {
|
||||
"supportsStreamingUsage": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modelPricing": {
|
||||
"providers": {
|
||||
"sglang": {
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
"id": "together",
|
||||
"enabledByDefault": true,
|
||||
"providers": ["together"],
|
||||
"providerRequest": {
|
||||
"providers": {
|
||||
"together": {
|
||||
"family": "together"
|
||||
}
|
||||
}
|
||||
},
|
||||
"providerAuthEnvVars": {
|
||||
"together": ["TOGETHER_API_KEY"]
|
||||
},
|
||||
|
||||
@@ -2,6 +2,24 @@
|
||||
"id": "vercel-ai-gateway",
|
||||
"enabledByDefault": true,
|
||||
"providers": ["vercel-ai-gateway"],
|
||||
"modelIdNormalization": {
|
||||
"providers": {
|
||||
"vercel-ai-gateway": {
|
||||
"aliases": {
|
||||
"opus-4.6": "claude-opus-4-6",
|
||||
"opus-4.5": "claude-opus-4-5",
|
||||
"sonnet-4.6": "claude-sonnet-4-6",
|
||||
"sonnet-4.5": "claude-sonnet-4-5"
|
||||
},
|
||||
"prefixWhenBareAfterAliasStartsWith": [
|
||||
{
|
||||
"modelPrefix": "claude-",
|
||||
"prefix": "anthropic"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"modelPricing": {
|
||||
"providers": {
|
||||
"vercel-ai-gateway": {
|
||||
|
||||
@@ -2,6 +2,16 @@
|
||||
"id": "vllm",
|
||||
"enabledByDefault": true,
|
||||
"providers": ["vllm"],
|
||||
"providerRequest": {
|
||||
"providers": {
|
||||
"vllm": {
|
||||
"family": "vllm",
|
||||
"openAICompletions": {
|
||||
"supportsStreamingUsage": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modelPricing": {
|
||||
"providers": {
|
||||
"vllm": {
|
||||
|
||||
@@ -3,12 +3,33 @@
|
||||
"enabledByDefault": true,
|
||||
"providers": ["xai"],
|
||||
"providerDiscoveryEntry": "./provider-discovery.ts",
|
||||
"modelIdNormalization": {
|
||||
"providers": {
|
||||
"xai": {
|
||||
"aliases": {
|
||||
"grok-4-fast-reasoning": "grok-4-fast",
|
||||
"grok-4-1-fast-reasoning": "grok-4-1-fast",
|
||||
"grok-4.20-experimental-beta-0304-reasoning": "grok-4.20-beta-latest-reasoning",
|
||||
"grok-4.20-experimental-beta-0304-non-reasoning": "grok-4.20-beta-latest-non-reasoning",
|
||||
"grok-4.20-reasoning": "grok-4.20-beta-latest-reasoning",
|
||||
"grok-4.20-non-reasoning": "grok-4.20-beta-latest-non-reasoning"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"providerEndpoints": [
|
||||
{
|
||||
"endpointClass": "xai-native",
|
||||
"hosts": ["api.x.ai", "api.grok.x.ai"]
|
||||
}
|
||||
],
|
||||
"providerRequest": {
|
||||
"providers": {
|
||||
"xai": {
|
||||
"family": "xai"
|
||||
}
|
||||
}
|
||||
},
|
||||
"syntheticAuthRefs": ["xai"],
|
||||
"providerAuthEnvVars": {
|
||||
"xai": ["XAI_API_KEY"]
|
||||
|
||||
@@ -2,6 +2,19 @@
|
||||
"id": "zai",
|
||||
"enabledByDefault": true,
|
||||
"providers": ["zai"],
|
||||
"providerEndpoints": [
|
||||
{
|
||||
"endpointClass": "zai-native",
|
||||
"hosts": ["api.z.ai"]
|
||||
}
|
||||
],
|
||||
"providerRequest": {
|
||||
"providers": {
|
||||
"zai": {
|
||||
"family": "zai"
|
||||
}
|
||||
}
|
||||
},
|
||||
"modelPricing": {
|
||||
"providers": {
|
||||
"zai": {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
normalizeGooglePreviewModelId,
|
||||
normalizeNativeXaiModelId,
|
||||
} from "../plugin-sdk/provider-model-id-normalize.js";
|
||||
import { normalizeProviderModelIdWithManifest } from "../plugins/manifest-model-id-normalization.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import { normalizeProviderId } from "./provider-id.js";
|
||||
|
||||
@@ -26,62 +23,16 @@ export function modelKey(provider: string, model: string): string {
|
||||
: `${providerId}/${modelId}`;
|
||||
}
|
||||
|
||||
export function normalizeAnthropicModelId(model: string): string {
|
||||
const trimmed = model.trim();
|
||||
if (!trimmed) {
|
||||
return trimmed;
|
||||
}
|
||||
switch (normalizeLowercaseStringOrEmpty(trimmed)) {
|
||||
case "opus-4.6":
|
||||
return "claude-opus-4-6";
|
||||
case "opus-4.5":
|
||||
return "claude-opus-4-5";
|
||||
case "sonnet-4.6":
|
||||
return "claude-sonnet-4-6";
|
||||
case "sonnet-4.5":
|
||||
return "claude-sonnet-4-5";
|
||||
default:
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeHuggingfaceModelId(model: string): string {
|
||||
const trimmed = model.trim();
|
||||
if (!trimmed) {
|
||||
return trimmed;
|
||||
}
|
||||
const prefix = "huggingface/";
|
||||
return normalizeLowercaseStringOrEmpty(trimmed).startsWith(prefix)
|
||||
? trimmed.slice(prefix.length)
|
||||
: trimmed;
|
||||
}
|
||||
|
||||
export function normalizeStaticProviderModelId(provider: string, model: string): string {
|
||||
if (provider === "anthropic") {
|
||||
return normalizeAnthropicModelId(model);
|
||||
}
|
||||
if (provider === "huggingface") {
|
||||
return normalizeHuggingfaceModelId(model);
|
||||
}
|
||||
if (provider === "google" || provider === "google-vertex") {
|
||||
return normalizeGooglePreviewModelId(model);
|
||||
}
|
||||
if (provider === "openrouter" && !model.includes("/")) {
|
||||
return `openrouter/${model}`;
|
||||
}
|
||||
if (provider === "nvidia" && !model.includes("/")) {
|
||||
return `nvidia/${model}`;
|
||||
}
|
||||
if (provider === "xai") {
|
||||
return normalizeNativeXaiModelId(model);
|
||||
}
|
||||
if (provider === "vercel-ai-gateway" && !model.includes("/")) {
|
||||
const normalizedAnthropicModel = normalizeAnthropicModelId(model);
|
||||
if (normalizedAnthropicModel.startsWith("claude-")) {
|
||||
return `anthropic/${normalizedAnthropicModel}`;
|
||||
}
|
||||
}
|
||||
return model;
|
||||
return (
|
||||
normalizeProviderModelIdWithManifest({
|
||||
provider,
|
||||
context: {
|
||||
provider,
|
||||
modelId: model,
|
||||
},
|
||||
}) ?? model
|
||||
);
|
||||
}
|
||||
|
||||
export function parseStaticModelRef(raw: string, defaultProvider: string): StaticModelRef | null {
|
||||
|
||||
@@ -37,37 +37,28 @@ describe("resolveOpenAICompletionsCompatDefaults", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"vllm",
|
||||
"localai",
|
||||
"sglang",
|
||||
"llama-cpp",
|
||||
"llama.cpp",
|
||||
"llamacpp",
|
||||
"jan",
|
||||
"lmstudio",
|
||||
"lm-studio",
|
||||
"text-generation-webui",
|
||||
"tabby",
|
||||
"tabbyapi",
|
||||
])("enables streaming usage compat for known local provider %s", (provider) => {
|
||||
expect(
|
||||
resolveOpenAICompletionsCompatDefaults({
|
||||
provider,
|
||||
endpointClass: "custom",
|
||||
knownProviderFamily: provider,
|
||||
}).supportsUsageInStreaming,
|
||||
).toBe(true);
|
||||
});
|
||||
it.each(["vllm", "sglang", "lmstudio"])(
|
||||
"enables streaming usage compat for manifest-declared local provider %s",
|
||||
(provider) => {
|
||||
expect(
|
||||
resolveOpenAICompletionsCompatDefaults({
|
||||
provider,
|
||||
endpointClass: "custom",
|
||||
knownProviderFamily: provider,
|
||||
supportsOpenAICompletionsStreamingUsageCompat: true,
|
||||
}).supportsUsageInStreaming,
|
||||
).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it("matches known local providers case-insensitively", () => {
|
||||
it("does not infer local streaming usage from provider id alone", () => {
|
||||
expect(
|
||||
resolveOpenAICompletionsCompatDefaults({
|
||||
provider: "vLLM",
|
||||
endpointClass: "local",
|
||||
provider: "vllm",
|
||||
endpointClass: "custom",
|
||||
knownProviderFamily: "vllm",
|
||||
}).supportsUsageInStreaming,
|
||||
).toBe(true);
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ type OpenAICompletionsCompatDefaultsInput = {
|
||||
endpointClass: ProviderEndpointClass;
|
||||
knownProviderFamily: string;
|
||||
supportsNativeStreamingUsageCompat?: boolean;
|
||||
supportsOpenAICompletionsStreamingUsageCompat?: boolean;
|
||||
usesExplicitProxyLikeEndpoint?: boolean;
|
||||
};
|
||||
|
||||
@@ -30,27 +31,6 @@ function isDefaultRouteProvider(provider: string | undefined, ...ids: string[])
|
||||
return provider !== undefined && ids.includes(provider);
|
||||
}
|
||||
|
||||
const KNOWN_LOCAL_STREAMING_USAGE_PROVIDERS = new Set([
|
||||
"jan",
|
||||
"llama-cpp",
|
||||
"llama.cpp",
|
||||
"llamacpp",
|
||||
"lm-studio",
|
||||
"lmstudio",
|
||||
"localai",
|
||||
"sglang",
|
||||
"tabby",
|
||||
"tabbyapi",
|
||||
"text-generation-webui",
|
||||
"vllm",
|
||||
]);
|
||||
|
||||
function isKnownLocalStreamingUsageProvider(...ids: Array<string | undefined>): boolean {
|
||||
return ids.some(
|
||||
(id) => id !== undefined && KNOWN_LOCAL_STREAMING_USAGE_PROVIDERS.has(id.toLowerCase()),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveOpenAICompletionsCompatDefaults(
|
||||
input: OpenAICompletionsCompatDefaultsInput,
|
||||
): OpenAICompletionsCompatDefaults {
|
||||
@@ -59,6 +39,7 @@ export function resolveOpenAICompletionsCompatDefaults(
|
||||
endpointClass,
|
||||
knownProviderFamily,
|
||||
supportsNativeStreamingUsageCompat = false,
|
||||
supportsOpenAICompletionsStreamingUsageCompat = false,
|
||||
usesExplicitProxyLikeEndpoint = false,
|
||||
} = input;
|
||||
const isDefaultRoute = endpointClass === "default";
|
||||
@@ -91,10 +72,6 @@ export function resolveOpenAICompletionsCompatDefaults(
|
||||
endpointClass === "mistral-public" ||
|
||||
knownProviderFamily === "mistral" ||
|
||||
(isDefaultRoute && isDefaultRouteProvider(provider, "chutes"));
|
||||
const supportsKnownLocalStreamingUsage = isKnownLocalStreamingUsageProvider(
|
||||
provider,
|
||||
knownProviderFamily,
|
||||
);
|
||||
return {
|
||||
supportsStore:
|
||||
!isNonStandard && knownProviderFamily !== "mistral" && !usesExplicitProxyLikeEndpoint,
|
||||
@@ -105,7 +82,7 @@ export function resolveOpenAICompletionsCompatDefaults(
|
||||
endpointClass !== "xai-native" &&
|
||||
!usesExplicitProxyLikeEndpoint,
|
||||
supportsUsageInStreaming:
|
||||
supportsKnownLocalStreamingUsage ||
|
||||
supportsOpenAICompletionsStreamingUsageCompat ||
|
||||
(!isNonStandard && (!usesConfiguredNonOpenAIEndpoint || supportsNativeStreamingUsageCompat)),
|
||||
maxTokensField: usesMaxTokens ? "max_tokens" : "max_completion_tokens",
|
||||
thinkingFormat: isDeepSeek
|
||||
@@ -126,6 +103,7 @@ export function resolveOpenAICompletionsCompatDefaultsFromCapabilities(
|
||||
| "endpointClass"
|
||||
| "knownProviderFamily"
|
||||
| "supportsNativeStreamingUsageCompat"
|
||||
| "supportsOpenAICompletionsStreamingUsageCompat"
|
||||
| "usesExplicitProxyLikeEndpoint"
|
||||
> & {
|
||||
provider?: string;
|
||||
|
||||
@@ -3,11 +3,65 @@ import { describe, expect, it, vi } from "vitest";
|
||||
const providerEndpointPlugins = vi.hoisted(() => [
|
||||
{
|
||||
providerEndpoints: [
|
||||
{ endpointClass: "openai-public", hosts: ["api.openai.com"] },
|
||||
{ endpointClass: "openai-codex", hosts: ["chatgpt.com"] },
|
||||
{ endpointClass: "azure-openai", hostSuffixes: [".openai.azure.com"] },
|
||||
{ endpointClass: "anthropic-public", hosts: ["api.anthropic.com"] },
|
||||
{ endpointClass: "mistral-public", hosts: ["api.mistral.ai"] },
|
||||
{ endpointClass: "chutes-native", hosts: ["llm.chutes.ai"] },
|
||||
{ endpointClass: "deepseek-native", hosts: ["api.deepseek.com"] },
|
||||
{ endpointClass: "github-copilot-native", hostSuffixes: [".githubcopilot.com"] },
|
||||
{ endpointClass: "groq-native", hosts: ["api.groq.com"] },
|
||||
{ endpointClass: "opencode-native", hostSuffixes: ["opencode.ai"] },
|
||||
{ endpointClass: "openrouter", hostSuffixes: ["openrouter.ai"] },
|
||||
{ endpointClass: "zai-native", hosts: ["api.z.ai"] },
|
||||
{ endpointClass: "google-generative-ai", hosts: ["generativelanguage.googleapis.com"] },
|
||||
{
|
||||
endpointClass: "google-vertex",
|
||||
hosts: ["aiplatform.googleapis.com"],
|
||||
googleVertexRegion: "global",
|
||||
},
|
||||
{
|
||||
endpointClass: "google-vertex",
|
||||
hostSuffixes: ["-aiplatform.googleapis.com"],
|
||||
googleVertexRegionHostSuffix: "-aiplatform.googleapis.com",
|
||||
},
|
||||
{
|
||||
endpointClass: "moonshot-native",
|
||||
baseUrls: ["https://api.moonshot.ai/v1", "https://api.moonshot.cn/v1"],
|
||||
},
|
||||
{
|
||||
endpointClass: "modelstudio-native",
|
||||
baseUrls: [
|
||||
"https://coding-intl.dashscope.aliyuncs.com/v1",
|
||||
"https://coding.dashscope.aliyuncs.com/v1",
|
||||
"https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
],
|
||||
},
|
||||
{
|
||||
endpointClass: "xai-native",
|
||||
hosts: ["api.x.ai", "api.grok.x.ai"],
|
||||
},
|
||||
],
|
||||
providerRequest: {
|
||||
providers: {
|
||||
anthropic: { family: "anthropic" },
|
||||
chutes: { family: "chutes" },
|
||||
deepseek: { family: "deepseek" },
|
||||
"github-copilot": { family: "github-copilot" },
|
||||
google: { family: "google" },
|
||||
groq: { family: "groq" },
|
||||
kimi: { family: "moonshot", compatibilityFamily: "moonshot" },
|
||||
mistral: { family: "mistral" },
|
||||
moonshot: { family: "moonshot", compatibilityFamily: "moonshot" },
|
||||
openrouter: { family: "openrouter" },
|
||||
qwen: { family: "modelstudio" },
|
||||
together: { family: "together" },
|
||||
xai: { family: "xai" },
|
||||
zai: { family: "zai" },
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@@ -105,6 +105,7 @@ export type ProviderRequestCapabilities = ProviderRequestPolicyResolution & {
|
||||
allowsResponsesStore: boolean;
|
||||
shouldStripResponsesPromptCache: boolean;
|
||||
supportsNativeStreamingUsageCompat: boolean;
|
||||
supportsOpenAICompletionsStreamingUsageCompat: boolean;
|
||||
compatibilityFamily?: ProviderRequestCompatibilityFamily;
|
||||
};
|
||||
|
||||
@@ -123,30 +124,47 @@ const OPENCLAW_ATTRIBUTION_PRODUCT = "OpenClaw";
|
||||
const OPENCLAW_ATTRIBUTION_ORIGINATOR = "openclaw";
|
||||
|
||||
const LOCAL_ENDPOINT_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
||||
const MOONSHOT_NATIVE_BASE_URLS = new Set([
|
||||
"https://api.moonshot.ai/v1",
|
||||
"https://api.moonshot.cn/v1",
|
||||
]);
|
||||
const MODELSTUDIO_NATIVE_BASE_URLS = new Set([
|
||||
"https://coding-intl.dashscope.aliyuncs.com/v1",
|
||||
"https://coding.dashscope.aliyuncs.com/v1",
|
||||
"https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
]);
|
||||
const OPENAI_RESPONSES_APIS = new Set([
|
||||
"openai-responses",
|
||||
"azure-openai-responses",
|
||||
"openai-codex-responses",
|
||||
]);
|
||||
const OPENAI_RESPONSES_PROVIDERS = new Set(["openai", "azure-openai", "azure-openai-responses"]);
|
||||
const MOONSHOT_COMPAT_PROVIDERS = new Set(["moonshot", "kimi"]);
|
||||
const MANIFEST_PROVIDER_ENDPOINT_CLASSES = new Set<ProviderEndpointClass>(["xai-native"]);
|
||||
const MANIFEST_PROVIDER_ENDPOINT_CLASSES = new Set<ProviderEndpointClass>([
|
||||
"anthropic-public",
|
||||
"cerebras-native",
|
||||
"chutes-native",
|
||||
"deepseek-native",
|
||||
"github-copilot-native",
|
||||
"groq-native",
|
||||
"mistral-public",
|
||||
"moonshot-native",
|
||||
"modelstudio-native",
|
||||
"openai-public",
|
||||
"openai-codex",
|
||||
"opencode-native",
|
||||
"azure-openai",
|
||||
"openrouter",
|
||||
"xai-native",
|
||||
"zai-native",
|
||||
"google-generative-ai",
|
||||
"google-vertex",
|
||||
]);
|
||||
type ManifestProviderEndpointCacheEntry = {
|
||||
endpointClass: ProviderEndpointClass;
|
||||
hosts: readonly string[];
|
||||
hostSuffixes: readonly string[];
|
||||
normalizedBaseUrls: readonly string[];
|
||||
googleVertexRegion?: string;
|
||||
googleVertexRegionHostSuffix?: string;
|
||||
};
|
||||
type ManifestProviderRequestCacheEntry = {
|
||||
family?: string;
|
||||
compatibilityFamily?: ProviderRequestCompatibilityFamily;
|
||||
supportsOpenAICompletionsStreamingUsageCompat?: boolean;
|
||||
};
|
||||
let manifestProviderEndpointCache: ManifestProviderEndpointCacheEntry[] | null = null;
|
||||
let manifestProviderRequestCache: Map<string, ManifestProviderRequestCacheEntry> | null = null;
|
||||
|
||||
function formatOpenClawUserAgent(version: string): string {
|
||||
return `${OPENCLAW_ATTRIBUTION_ORIGINATOR}/${version}`;
|
||||
@@ -218,9 +236,16 @@ function loadManifestProviderEndpointCache(): ManifestProviderEndpointCacheEntry
|
||||
entries.push({
|
||||
endpointClass: endpoint.endpointClass,
|
||||
hosts: (endpoint.hosts ?? []).map((host) => host.toLowerCase()),
|
||||
hostSuffixes: (endpoint.hostSuffixes ?? []).map((host) => host.toLowerCase()),
|
||||
normalizedBaseUrls: (endpoint.baseUrls ?? [])
|
||||
.map((baseUrl) => normalizeComparableBaseUrl(baseUrl))
|
||||
.filter((baseUrl): baseUrl is string => baseUrl !== undefined),
|
||||
...(endpoint.googleVertexRegion
|
||||
? { googleVertexRegion: endpoint.googleVertexRegion }
|
||||
: {}),
|
||||
...(endpoint.googleVertexRegionHostSuffix
|
||||
? { googleVertexRegionHostSuffix: endpoint.googleVertexRegionHostSuffix }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -229,19 +254,77 @@ function loadManifestProviderEndpointCache(): ManifestProviderEndpointCacheEntry
|
||||
return manifestProviderEndpointCache;
|
||||
}
|
||||
|
||||
function loadManifestProviderRequestCache(): Map<string, ManifestProviderRequestCacheEntry> {
|
||||
if (!manifestProviderRequestCache) {
|
||||
const registry = loadPluginManifestRegistryForPluginRegistry({ includeDisabled: true });
|
||||
const entries = new Map<string, ManifestProviderRequestCacheEntry>();
|
||||
for (const plugin of registry.plugins) {
|
||||
for (const [provider, request] of Object.entries(plugin.providerRequest?.providers ?? {})) {
|
||||
entries.set(provider, {
|
||||
...(request.family ? { family: request.family } : {}),
|
||||
...(request.compatibilityFamily
|
||||
? { compatibilityFamily: request.compatibilityFamily }
|
||||
: {}),
|
||||
...(request.openAICompletions?.supportsStreamingUsage !== undefined
|
||||
? {
|
||||
supportsOpenAICompletionsStreamingUsageCompat:
|
||||
request.openAICompletions.supportsStreamingUsage,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
manifestProviderRequestCache = entries;
|
||||
}
|
||||
return manifestProviderRequestCache;
|
||||
}
|
||||
|
||||
function resolveManifestProviderRequest(
|
||||
provider: string | undefined,
|
||||
): ManifestProviderRequestCacheEntry | undefined {
|
||||
return provider ? loadManifestProviderRequestCache().get(provider) : undefined;
|
||||
}
|
||||
|
||||
function hostMatchesSuffix(host: string, suffix: string): boolean {
|
||||
if (!suffix) {
|
||||
return false;
|
||||
}
|
||||
return suffix.startsWith(".") || suffix.startsWith("-")
|
||||
? host.endsWith(suffix)
|
||||
: host === suffix || host.endsWith(`.${suffix}`);
|
||||
}
|
||||
|
||||
function buildManifestEndpointResolution(
|
||||
endpoint: ManifestProviderEndpointCacheEntry,
|
||||
host: string,
|
||||
): ProviderEndpointResolution {
|
||||
const regionSuffix = endpoint.googleVertexRegionHostSuffix;
|
||||
const googleVertexRegion =
|
||||
endpoint.googleVertexRegion ??
|
||||
(regionSuffix && host.endsWith(regionSuffix) ? host.slice(0, -regionSuffix.length) : undefined);
|
||||
return {
|
||||
endpointClass: endpoint.endpointClass,
|
||||
hostname: host,
|
||||
...(googleVertexRegion ? { googleVertexRegion } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveManifestProviderEndpoint(params: {
|
||||
host: string;
|
||||
normalizedBaseUrl?: string;
|
||||
}): ProviderEndpointResolution | undefined {
|
||||
for (const endpoint of loadManifestProviderEndpointCache()) {
|
||||
if (endpoint.hosts.includes(params.host)) {
|
||||
return { endpointClass: endpoint.endpointClass, hostname: params.host };
|
||||
return buildManifestEndpointResolution(endpoint, params.host);
|
||||
}
|
||||
if (endpoint.hostSuffixes.some((suffix) => hostMatchesSuffix(params.host, suffix))) {
|
||||
return buildManifestEndpointResolution(endpoint, params.host);
|
||||
}
|
||||
if (
|
||||
params.normalizedBaseUrl &&
|
||||
endpoint.normalizedBaseUrls.includes(params.normalizedBaseUrl)
|
||||
) {
|
||||
return { endpointClass: endpoint.endpointClass, hostname: params.host };
|
||||
return buildManifestEndpointResolution(endpoint, params.host);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
@@ -268,73 +351,13 @@ export function resolveProviderEndpoint(
|
||||
return { endpointClass: "invalid" };
|
||||
}
|
||||
const normalizedBaseUrl = normalizeComparableBaseUrl(baseUrl);
|
||||
if (normalizedBaseUrl && MOONSHOT_NATIVE_BASE_URLS.has(normalizedBaseUrl)) {
|
||||
return { endpointClass: "moonshot-native", hostname: host };
|
||||
}
|
||||
if (normalizedBaseUrl && MODELSTUDIO_NATIVE_BASE_URLS.has(normalizedBaseUrl)) {
|
||||
return { endpointClass: "modelstudio-native", hostname: host };
|
||||
}
|
||||
if (host === "api.openai.com") {
|
||||
return { endpointClass: "openai-public", hostname: host };
|
||||
}
|
||||
if (host === "api.anthropic.com") {
|
||||
return { endpointClass: "anthropic-public", hostname: host };
|
||||
}
|
||||
if (host === "api.mistral.ai") {
|
||||
return { endpointClass: "mistral-public", hostname: host };
|
||||
}
|
||||
if (host === "api.cerebras.ai") {
|
||||
return { endpointClass: "cerebras-native", hostname: host };
|
||||
}
|
||||
if (host === "llm.chutes.ai") {
|
||||
return { endpointClass: "chutes-native", hostname: host };
|
||||
}
|
||||
if (host === "api.deepseek.com") {
|
||||
return { endpointClass: "deepseek-native", hostname: host };
|
||||
}
|
||||
if (host.endsWith(".githubcopilot.com")) {
|
||||
return { endpointClass: "github-copilot-native", hostname: host };
|
||||
}
|
||||
if (host === "api.groq.com") {
|
||||
return { endpointClass: "groq-native", hostname: host };
|
||||
}
|
||||
if (host === "chatgpt.com") {
|
||||
return { endpointClass: "openai-codex", hostname: host };
|
||||
}
|
||||
if (host === "opencode.ai" || host.endsWith(".opencode.ai")) {
|
||||
return { endpointClass: "opencode-native", hostname: host };
|
||||
}
|
||||
if (host === "openrouter.ai" || host.endsWith(".openrouter.ai")) {
|
||||
return { endpointClass: "openrouter", hostname: host };
|
||||
}
|
||||
if (host === "api.z.ai") {
|
||||
return { endpointClass: "zai-native", hostname: host };
|
||||
}
|
||||
if (host.endsWith(".openai.azure.com")) {
|
||||
return { endpointClass: "azure-openai", hostname: host };
|
||||
}
|
||||
if (host === "generativelanguage.googleapis.com") {
|
||||
return { endpointClass: "google-generative-ai", hostname: host };
|
||||
}
|
||||
if (host === "aiplatform.googleapis.com") {
|
||||
return {
|
||||
endpointClass: "google-vertex",
|
||||
hostname: host,
|
||||
googleVertexRegion: "global",
|
||||
};
|
||||
}
|
||||
const googleVertexHost = /^([a-z0-9-]+)-aiplatform\.googleapis\.com$/.exec(host);
|
||||
if (googleVertexHost) {
|
||||
return {
|
||||
endpointClass: "google-vertex",
|
||||
hostname: host,
|
||||
googleVertexRegion: googleVertexHost[1],
|
||||
};
|
||||
}
|
||||
const manifestEndpoint = resolveManifestProviderEndpoint({ host, normalizedBaseUrl });
|
||||
if (manifestEndpoint) {
|
||||
return manifestEndpoint;
|
||||
}
|
||||
if (host === "api.cerebras.ai") {
|
||||
return { endpointClass: "cerebras-native", hostname: host };
|
||||
}
|
||||
if (isLocalEndpointHost(host)) {
|
||||
return { endpointClass: "local", hostname: host };
|
||||
}
|
||||
@@ -342,42 +365,16 @@ export function resolveProviderEndpoint(
|
||||
}
|
||||
|
||||
function resolveKnownProviderFamily(provider: string | undefined): string {
|
||||
const manifestFamily = resolveManifestProviderRequest(provider)?.family;
|
||||
if (manifestFamily) {
|
||||
return manifestFamily;
|
||||
}
|
||||
switch (provider) {
|
||||
case "openai":
|
||||
case "openai-codex":
|
||||
case "azure-openai":
|
||||
case "azure-openai-responses":
|
||||
return "openai-family";
|
||||
case "openrouter":
|
||||
return "openrouter";
|
||||
case "anthropic":
|
||||
return "anthropic";
|
||||
case "chutes":
|
||||
return "chutes";
|
||||
case "deepseek":
|
||||
return "deepseek";
|
||||
case "google":
|
||||
return "google";
|
||||
case "xai":
|
||||
return "xai";
|
||||
case "zai":
|
||||
return "zai";
|
||||
case "moonshot":
|
||||
case "kimi":
|
||||
return "moonshot";
|
||||
case "qwen":
|
||||
case "qwencloud":
|
||||
case "modelstudio":
|
||||
case "dashscope":
|
||||
return "modelstudio";
|
||||
case "github-copilot":
|
||||
return "github-copilot";
|
||||
case "groq":
|
||||
return "groq";
|
||||
case "mistral":
|
||||
return "mistral";
|
||||
case "together":
|
||||
return "together";
|
||||
default:
|
||||
return provider || "unknown";
|
||||
}
|
||||
@@ -632,10 +629,8 @@ export function resolveProviderRequestCapabilities(
|
||||
endpointClass === "google-generative-ai" ||
|
||||
endpointClass === "google-vertex";
|
||||
|
||||
let compatibilityFamily: ProviderRequestCompatibilityFamily | undefined;
|
||||
if (provider && MOONSHOT_COMPAT_PROVIDERS.has(provider)) {
|
||||
compatibilityFamily = "moonshot";
|
||||
}
|
||||
const manifestProviderRequest = resolveManifestProviderRequest(provider);
|
||||
const compatibilityFamily = manifestProviderRequest?.compatibilityFamily;
|
||||
|
||||
const isResponsesApi = isOpenAIResponsesApi(api);
|
||||
const promptCacheKeySupport = readCompatBoolean(input.compat, "supportsPromptCacheKey");
|
||||
@@ -693,6 +688,8 @@ export function resolveProviderRequestCapabilities(
|
||||
// provider key at Moonshot or DashScope and still need streaming usage.
|
||||
supportsNativeStreamingUsageCompat:
|
||||
endpointClass === "moonshot-native" || endpointClass === "modelstudio-native",
|
||||
supportsOpenAICompletionsStreamingUsageCompat:
|
||||
manifestProviderRequest?.supportsOpenAICompletionsStreamingUsageCompat === true,
|
||||
compatibilityFamily,
|
||||
};
|
||||
}
|
||||
|
||||
79
src/plugins/manifest-model-id-normalization.ts
Normal file
79
src/plugins/manifest-model-id-normalization.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import type { PluginManifestModelIdNormalizationProvider } from "./manifest.js";
|
||||
import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js";
|
||||
|
||||
let manifestModelIdNormalizationCache:
|
||||
| Map<string, PluginManifestModelIdNormalizationProvider>
|
||||
| undefined;
|
||||
|
||||
function loadManifestModelIdNormalizationPolicies(): Map<
|
||||
string,
|
||||
PluginManifestModelIdNormalizationProvider
|
||||
> {
|
||||
if (manifestModelIdNormalizationCache) {
|
||||
return manifestModelIdNormalizationCache;
|
||||
}
|
||||
|
||||
const policies = new Map<string, PluginManifestModelIdNormalizationProvider>();
|
||||
const registry = loadPluginManifestRegistryForPluginRegistry({ includeDisabled: true });
|
||||
for (const plugin of registry.plugins) {
|
||||
for (const [provider, policy] of Object.entries(plugin.modelIdNormalization?.providers ?? {})) {
|
||||
policies.set(provider, policy);
|
||||
}
|
||||
}
|
||||
manifestModelIdNormalizationCache = policies;
|
||||
return policies;
|
||||
}
|
||||
|
||||
function hasProviderPrefix(modelId: string): boolean {
|
||||
return modelId.includes("/");
|
||||
}
|
||||
|
||||
function formatPrefixedModelId(prefix: string, modelId: string): string {
|
||||
return `${prefix.replace(/\/+$/u, "")}/${modelId.replace(/^\/+/u, "")}`;
|
||||
}
|
||||
|
||||
export function normalizeProviderModelIdWithManifest(params: {
|
||||
provider: string;
|
||||
context: {
|
||||
provider: string;
|
||||
modelId: string;
|
||||
};
|
||||
}): string | undefined {
|
||||
const policy = loadManifestModelIdNormalizationPolicies().get(params.provider);
|
||||
if (!policy) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let modelId = params.context.modelId.trim();
|
||||
if (!modelId) {
|
||||
return modelId;
|
||||
}
|
||||
|
||||
for (const prefix of policy.stripPrefixes ?? []) {
|
||||
const normalizedPrefix = normalizeLowercaseStringOrEmpty(prefix);
|
||||
if (normalizedPrefix && normalizeLowercaseStringOrEmpty(modelId).startsWith(normalizedPrefix)) {
|
||||
modelId = modelId.slice(prefix.length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
modelId = policy.aliases?.[normalizeLowercaseStringOrEmpty(modelId)] ?? modelId;
|
||||
|
||||
if (!hasProviderPrefix(modelId)) {
|
||||
for (const rule of policy.prefixWhenBareAfterAliasStartsWith ?? []) {
|
||||
if (normalizeLowercaseStringOrEmpty(modelId).startsWith(rule.modelPrefix.toLowerCase())) {
|
||||
return formatPrefixedModelId(rule.prefix, modelId);
|
||||
}
|
||||
}
|
||||
if (policy.prefixWhenBare) {
|
||||
return formatPrefixedModelId(policy.prefixWhenBare, modelId);
|
||||
}
|
||||
}
|
||||
|
||||
return modelId;
|
||||
}
|
||||
|
||||
export function clearManifestModelIdNormalizationCacheForTest(): void {
|
||||
manifestModelIdNormalizationCache = undefined;
|
||||
}
|
||||
@@ -421,9 +421,50 @@ describe("loadPluginManifestRegistry", () => {
|
||||
{
|
||||
endpointClass: "openai-public",
|
||||
hosts: ["API.OPENAI.COM", ""],
|
||||
hostSuffixes: [".openai.azure.com"],
|
||||
baseUrls: ["https://api.openai.com/v1"],
|
||||
googleVertexRegion: "global",
|
||||
googleVertexRegionHostSuffix: "-aiplatform.googleapis.com",
|
||||
},
|
||||
],
|
||||
modelIdNormalization: {
|
||||
providers: {
|
||||
openai: {
|
||||
aliases: {
|
||||
"gpt-latest": "gpt-5.4",
|
||||
},
|
||||
stripPrefixes: ["openai/"],
|
||||
prefixWhenBare: "openai",
|
||||
prefixWhenBareAfterAliasStartsWith: [
|
||||
{
|
||||
modelPrefix: "gpt-",
|
||||
prefix: "openai",
|
||||
},
|
||||
{
|
||||
modelPrefix: "",
|
||||
prefix: "ignored",
|
||||
},
|
||||
],
|
||||
},
|
||||
ignored: {
|
||||
prefixWhenBare: "ignored",
|
||||
},
|
||||
},
|
||||
},
|
||||
providerRequest: {
|
||||
providers: {
|
||||
openai: {
|
||||
family: "openai-family",
|
||||
compatibilityFamily: "moonshot",
|
||||
openAICompletions: {
|
||||
supportsStreamingUsage: true,
|
||||
},
|
||||
},
|
||||
ignored: {
|
||||
family: "ignored",
|
||||
},
|
||||
},
|
||||
},
|
||||
syntheticAuthRefs: ["openai-cli"],
|
||||
nonSecretAuthMarkers: ["openai-cli"],
|
||||
providerAuthAliases: {
|
||||
@@ -455,9 +496,40 @@ describe("loadPluginManifestRegistry", () => {
|
||||
{
|
||||
endpointClass: "openai-public",
|
||||
hosts: ["api.openai.com"],
|
||||
hostSuffixes: [".openai.azure.com"],
|
||||
baseUrls: ["https://api.openai.com/v1"],
|
||||
googleVertexRegion: "global",
|
||||
googleVertexRegionHostSuffix: "-aiplatform.googleapis.com",
|
||||
},
|
||||
]);
|
||||
expect(registry.plugins[0]?.modelIdNormalization).toEqual({
|
||||
providers: {
|
||||
openai: {
|
||||
aliases: {
|
||||
"gpt-latest": "gpt-5.4",
|
||||
},
|
||||
stripPrefixes: ["openai/"],
|
||||
prefixWhenBare: "openai",
|
||||
prefixWhenBareAfterAliasStartsWith: [
|
||||
{
|
||||
modelPrefix: "gpt-",
|
||||
prefix: "openai",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(registry.plugins[0]?.providerRequest).toEqual({
|
||||
providers: {
|
||||
openai: {
|
||||
family: "openai-family",
|
||||
compatibilityFamily: "moonshot",
|
||||
openAICompletions: {
|
||||
supportsStreamingUsage: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(registry.plugins[0]?.syntheticAuthRefs).toEqual(["openai-cli"]);
|
||||
expect(registry.plugins[0]?.nonSecretAuthMarkers).toEqual(["openai-cli"]);
|
||||
expect(registry.plugins[0]?.providerAuthAliases).toEqual({
|
||||
@@ -991,6 +1063,7 @@ describe("loadPluginManifestRegistry", () => {
|
||||
onCommands: ["models"],
|
||||
onChannels: ["web"],
|
||||
onRoutes: ["gateway-webhook"],
|
||||
onConfigPaths: ["browser"],
|
||||
onCapabilities: ["provider", "tool"],
|
||||
},
|
||||
setup: {
|
||||
@@ -1019,6 +1092,7 @@ describe("loadPluginManifestRegistry", () => {
|
||||
onCommands: ["models"],
|
||||
onChannels: ["web"],
|
||||
onRoutes: ["gateway-webhook"],
|
||||
onConfigPaths: ["browser"],
|
||||
onCapabilities: ["provider", "tool"],
|
||||
});
|
||||
expect(registry.plugins[0]?.setup).toEqual({
|
||||
|
||||
@@ -37,9 +37,11 @@ import {
|
||||
type PluginManifestContracts,
|
||||
type PluginManifestMediaUnderstandingProviderMetadata,
|
||||
type PluginManifestModelCatalog,
|
||||
type PluginManifestModelIdNormalization,
|
||||
type PluginManifestModelPricing,
|
||||
type PluginManifestModelSupport,
|
||||
type PluginManifestProviderEndpoint,
|
||||
type PluginManifestProviderRequest,
|
||||
type PluginManifestQaRunner,
|
||||
type PluginManifestSetup,
|
||||
} from "./manifest.js";
|
||||
@@ -115,7 +117,9 @@ export type PluginManifestRecord = {
|
||||
modelSupport?: PluginManifestModelSupport;
|
||||
modelCatalog?: PluginManifestModelCatalog;
|
||||
modelPricing?: PluginManifestModelPricing;
|
||||
modelIdNormalization?: PluginManifestModelIdNormalization;
|
||||
providerEndpoints?: PluginManifestProviderEndpoint[];
|
||||
providerRequest?: PluginManifestProviderRequest;
|
||||
cliBackends: string[];
|
||||
syntheticAuthRefs?: string[];
|
||||
nonSecretAuthMarkers?: string[];
|
||||
@@ -341,7 +345,9 @@ function buildRecord(params: {
|
||||
modelSupport: params.manifest.modelSupport,
|
||||
modelCatalog: params.manifest.modelCatalog,
|
||||
modelPricing: params.manifest.modelPricing,
|
||||
modelIdNormalization: params.manifest.modelIdNormalization,
|
||||
providerEndpoints: params.manifest.providerEndpoints,
|
||||
providerRequest: params.manifest.providerRequest,
|
||||
cliBackends: params.manifest.cliBackends ?? [],
|
||||
syntheticAuthRefs: params.manifest.syntheticAuthRefs ?? [],
|
||||
nonSecretAuthMarkers: params.manifest.nonSecretAuthMarkers ?? [],
|
||||
|
||||
@@ -91,6 +91,22 @@ export type PluginManifestModelPricing = {
|
||||
providers?: Record<string, PluginManifestModelPricingProvider>;
|
||||
};
|
||||
|
||||
export type PluginManifestModelIdPrefixRule = {
|
||||
modelPrefix: string;
|
||||
prefix: string;
|
||||
};
|
||||
|
||||
export type PluginManifestModelIdNormalizationProvider = {
|
||||
aliases?: Record<string, string>;
|
||||
stripPrefixes?: string[];
|
||||
prefixWhenBare?: string;
|
||||
prefixWhenBareAfterAliasStartsWith?: PluginManifestModelIdPrefixRule[];
|
||||
};
|
||||
|
||||
export type PluginManifestModelIdNormalization = {
|
||||
providers?: Record<string, PluginManifestModelIdNormalizationProvider>;
|
||||
};
|
||||
|
||||
export type PluginManifestProviderEndpoint = {
|
||||
/**
|
||||
* Core endpoint class this plugin-owned endpoint should map to. Core must
|
||||
@@ -99,8 +115,26 @@ export type PluginManifestProviderEndpoint = {
|
||||
endpointClass: string;
|
||||
/** Hostnames that should resolve to this endpoint class. */
|
||||
hosts?: string[];
|
||||
/** Host suffixes that should resolve to this endpoint class. */
|
||||
hostSuffixes?: string[];
|
||||
/** Exact normalized base URLs that should resolve to this endpoint class. */
|
||||
baseUrls?: string[];
|
||||
/** Static Google Vertex region metadata for exact global hosts. */
|
||||
googleVertexRegion?: string;
|
||||
/** Host suffix whose prefix should be exposed as the Google Vertex region. */
|
||||
googleVertexRegionHostSuffix?: string;
|
||||
};
|
||||
|
||||
export type PluginManifestProviderRequestProvider = {
|
||||
family?: string;
|
||||
compatibilityFamily?: "moonshot";
|
||||
openAICompletions?: {
|
||||
supportsStreamingUsage?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type PluginManifestProviderRequest = {
|
||||
providers?: Record<string, PluginManifestProviderRequestProvider>;
|
||||
};
|
||||
|
||||
export type PluginManifestActivationCapability = "provider" | "channel" | "tool" | "hook";
|
||||
@@ -234,8 +268,12 @@ export type PluginManifest = {
|
||||
modelCatalog?: PluginManifestModelCatalog;
|
||||
/** Manifest-owned external pricing lookup policy for provider refs. */
|
||||
modelPricing?: PluginManifestModelPricing;
|
||||
/** Manifest-owned model-id normalization used before provider runtime loads. */
|
||||
modelIdNormalization?: PluginManifestModelIdNormalization;
|
||||
/** Cheap provider endpoint metadata used before provider runtime loads. */
|
||||
providerEndpoints?: PluginManifestProviderEndpoint[];
|
||||
/** Cheap provider request metadata used before provider runtime loads. */
|
||||
providerRequest?: PluginManifestProviderRequest;
|
||||
/** Cheap startup activation lookup for plugin-owned CLI inference backends. */
|
||||
cliBackends?: string[];
|
||||
/**
|
||||
@@ -701,6 +739,85 @@ function normalizeManifestModelPricing(
|
||||
return Object.keys(providers).length > 0 ? { providers } : undefined;
|
||||
}
|
||||
|
||||
function normalizeManifestModelIdPrefixRules(
|
||||
value: unknown,
|
||||
): PluginManifestModelIdPrefixRule[] | undefined {
|
||||
if (!Array.isArray(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const rules: PluginManifestModelIdPrefixRule[] = [];
|
||||
for (const rawRule of value) {
|
||||
if (!isRecord(rawRule)) {
|
||||
continue;
|
||||
}
|
||||
const modelPrefix = normalizeOptionalString(rawRule.modelPrefix);
|
||||
const prefix = normalizeOptionalString(rawRule.prefix);
|
||||
if (!modelPrefix || !prefix) {
|
||||
continue;
|
||||
}
|
||||
rules.push({ modelPrefix, prefix });
|
||||
}
|
||||
return rules.length > 0 ? rules : undefined;
|
||||
}
|
||||
|
||||
function normalizeManifestModelIdNormalizationProvider(
|
||||
value: unknown,
|
||||
): PluginManifestModelIdNormalizationProvider | undefined {
|
||||
if (!isRecord(value)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const aliases: Record<string, string> = {};
|
||||
if (isRecord(value.aliases)) {
|
||||
for (const [rawAlias, rawCanonical] of Object.entries(value.aliases)) {
|
||||
const alias = normalizeModelCatalogProviderId(rawAlias);
|
||||
const canonical = normalizeOptionalString(rawCanonical);
|
||||
if (alias && canonical) {
|
||||
aliases[alias] = canonical;
|
||||
}
|
||||
}
|
||||
}
|
||||
const stripPrefixes = normalizeTrimmedStringList(value.stripPrefixes);
|
||||
const prefixWhenBare = normalizeOptionalString(value.prefixWhenBare);
|
||||
const prefixWhenBareAfterAliasStartsWith = normalizeManifestModelIdPrefixRules(
|
||||
value.prefixWhenBareAfterAliasStartsWith,
|
||||
);
|
||||
const normalization = {
|
||||
...(Object.keys(aliases).length > 0 ? { aliases } : {}),
|
||||
...(stripPrefixes.length > 0 ? { stripPrefixes } : {}),
|
||||
...(prefixWhenBare ? { prefixWhenBare } : {}),
|
||||
...(prefixWhenBareAfterAliasStartsWith ? { prefixWhenBareAfterAliasStartsWith } : {}),
|
||||
} satisfies PluginManifestModelIdNormalizationProvider;
|
||||
|
||||
return Object.keys(normalization).length > 0 ? normalization : undefined;
|
||||
}
|
||||
|
||||
function normalizeManifestModelIdNormalization(
|
||||
value: unknown,
|
||||
params: { ownedProviders: ReadonlySet<string> },
|
||||
): PluginManifestModelIdNormalization | undefined {
|
||||
if (!isRecord(value) || !isRecord(value.providers)) {
|
||||
return undefined;
|
||||
}
|
||||
const ownedProviders = new Set(
|
||||
[...params.ownedProviders]
|
||||
.map((provider) => normalizeModelCatalogProviderId(provider))
|
||||
.filter(Boolean),
|
||||
);
|
||||
const providers: Record<string, PluginManifestModelIdNormalizationProvider> = {};
|
||||
for (const [rawProviderId, rawPolicy] of Object.entries(value.providers)) {
|
||||
const providerId = normalizeModelCatalogProviderId(rawProviderId);
|
||||
if (!providerId || !ownedProviders.has(providerId)) {
|
||||
continue;
|
||||
}
|
||||
const policy = normalizeManifestModelIdNormalizationProvider(rawPolicy);
|
||||
if (policy) {
|
||||
providers[providerId] = policy;
|
||||
}
|
||||
}
|
||||
return Object.keys(providers).length > 0 ? { providers } : undefined;
|
||||
}
|
||||
|
||||
function normalizeManifestProviderEndpoints(
|
||||
value: unknown,
|
||||
): PluginManifestProviderEndpoint[] | undefined {
|
||||
@@ -718,20 +835,80 @@ function normalizeManifestProviderEndpoints(
|
||||
continue;
|
||||
}
|
||||
const hosts = normalizeTrimmedStringList(rawEndpoint.hosts).map((host) => host.toLowerCase());
|
||||
const hostSuffixes = normalizeTrimmedStringList(rawEndpoint.hostSuffixes).map((host) =>
|
||||
host.toLowerCase(),
|
||||
);
|
||||
const baseUrls = normalizeTrimmedStringList(rawEndpoint.baseUrls);
|
||||
if (hosts.length === 0 && baseUrls.length === 0) {
|
||||
const googleVertexRegion = normalizeOptionalString(rawEndpoint.googleVertexRegion);
|
||||
const googleVertexRegionHostSuffix = normalizeOptionalString(
|
||||
rawEndpoint.googleVertexRegionHostSuffix,
|
||||
)?.toLowerCase();
|
||||
if (hosts.length === 0 && hostSuffixes.length === 0 && baseUrls.length === 0) {
|
||||
continue;
|
||||
}
|
||||
endpoints.push({
|
||||
endpointClass,
|
||||
...(hosts.length > 0 ? { hosts } : {}),
|
||||
...(hostSuffixes.length > 0 ? { hostSuffixes } : {}),
|
||||
...(baseUrls.length > 0 ? { baseUrls } : {}),
|
||||
...(googleVertexRegion ? { googleVertexRegion } : {}),
|
||||
...(googleVertexRegionHostSuffix ? { googleVertexRegionHostSuffix } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
return endpoints.length > 0 ? endpoints : undefined;
|
||||
}
|
||||
|
||||
function normalizeManifestProviderRequestProvider(
|
||||
value: unknown,
|
||||
): PluginManifestProviderRequestProvider | undefined {
|
||||
if (!isRecord(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const family = normalizeOptionalString(value.family);
|
||||
const compatibilityFamily =
|
||||
normalizeOptionalString(value.compatibilityFamily) === "moonshot" ? "moonshot" : undefined;
|
||||
const supportsStreamingUsage = isRecord(value.openAICompletions)
|
||||
? value.openAICompletions.supportsStreamingUsage
|
||||
: undefined;
|
||||
const openAICompletions =
|
||||
typeof supportsStreamingUsage === "boolean" ? { supportsStreamingUsage } : undefined;
|
||||
const providerRequest = {
|
||||
...(family ? { family } : {}),
|
||||
...(compatibilityFamily ? { compatibilityFamily } : {}),
|
||||
...(openAICompletions && Object.keys(openAICompletions).length > 0
|
||||
? { openAICompletions }
|
||||
: {}),
|
||||
} satisfies PluginManifestProviderRequestProvider;
|
||||
return Object.keys(providerRequest).length > 0 ? providerRequest : undefined;
|
||||
}
|
||||
|
||||
function normalizeManifestProviderRequest(
|
||||
value: unknown,
|
||||
params: { ownedProviders: ReadonlySet<string> },
|
||||
): PluginManifestProviderRequest | undefined {
|
||||
if (!isRecord(value) || !isRecord(value.providers)) {
|
||||
return undefined;
|
||||
}
|
||||
const ownedProviders = new Set(
|
||||
[...params.ownedProviders]
|
||||
.map((provider) => normalizeModelCatalogProviderId(provider))
|
||||
.filter(Boolean),
|
||||
);
|
||||
const providers: Record<string, PluginManifestProviderRequestProvider> = {};
|
||||
for (const [rawProviderId, rawPolicy] of Object.entries(value.providers)) {
|
||||
const providerId = normalizeModelCatalogProviderId(rawProviderId);
|
||||
if (!providerId || !ownedProviders.has(providerId)) {
|
||||
continue;
|
||||
}
|
||||
const policy = normalizeManifestProviderRequestProvider(rawPolicy);
|
||||
if (policy) {
|
||||
providers[providerId] = policy;
|
||||
}
|
||||
}
|
||||
return Object.keys(providers).length > 0 ? { providers } : undefined;
|
||||
}
|
||||
|
||||
function normalizeManifestActivation(value: unknown): PluginManifestActivation | undefined {
|
||||
if (!isRecord(value)) {
|
||||
return undefined;
|
||||
@@ -1040,7 +1217,13 @@ export function loadPluginManifest(
|
||||
const modelPricing = normalizeManifestModelPricing(raw.modelPricing, {
|
||||
ownedProviders: new Set(providers),
|
||||
});
|
||||
const modelIdNormalization = normalizeManifestModelIdNormalization(raw.modelIdNormalization, {
|
||||
ownedProviders: new Set(providers),
|
||||
});
|
||||
const providerEndpoints = normalizeManifestProviderEndpoints(raw.providerEndpoints);
|
||||
const providerRequest = normalizeManifestProviderRequest(raw.providerRequest, {
|
||||
ownedProviders: new Set(providers),
|
||||
});
|
||||
const cliBackends = normalizeTrimmedStringList(raw.cliBackends);
|
||||
const syntheticAuthRefs = normalizeTrimmedStringList(raw.syntheticAuthRefs);
|
||||
const nonSecretAuthMarkers = normalizeTrimmedStringList(raw.nonSecretAuthMarkers);
|
||||
@@ -1082,7 +1265,9 @@ export function loadPluginManifest(
|
||||
modelSupport,
|
||||
modelCatalog,
|
||||
modelPricing,
|
||||
modelIdNormalization,
|
||||
providerEndpoints,
|
||||
providerRequest,
|
||||
cliBackends,
|
||||
syntheticAuthRefs,
|
||||
nonSecretAuthMarkers,
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import { sanitizeForLog } from "../terminal/ansi.js";
|
||||
import { normalizeProviderModelIdWithManifest } from "./manifest-model-id-normalization.js";
|
||||
import { resolvePluginDiscoveryProvidersRuntime } from "./provider-discovery.runtime.js";
|
||||
import {
|
||||
__testing as providerHookRuntimeTesting,
|
||||
@@ -539,7 +540,10 @@ export function normalizeProviderModelIdWithPlugin(params: {
|
||||
context: ProviderNormalizeModelIdContext;
|
||||
}): string | undefined {
|
||||
const plugin = resolveProviderHookPlugin(params);
|
||||
return normalizeOptionalString(plugin?.normalizeModelId?.(params.context));
|
||||
return (
|
||||
normalizeOptionalString(plugin?.normalizeModelId?.(params.context)) ??
|
||||
normalizeProviderModelIdWithManifest(params)
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeProviderTransportWithPlugin(params: {
|
||||
|
||||
Reference in New Issue
Block a user