diff --git a/.github/labeler.yml b/.github/labeler.yml index 1f75d56fdd53..a169ec108c3a 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -381,11 +381,6 @@ - changed-files: - any-glob-to-any-file: - "extensions/anthropic/**" -"extensions: clawrouter": - - changed-files: - - any-glob-to-any-file: - - "extensions/clawrouter/**" - - "docs/providers/clawrouter.md" "extensions: cloudflare-ai-gateway": - changed-files: - any-glob-to-any-file: diff --git a/docs/docs.json b/docs/docs.json index 1b3f393e7129..810e4a6c5ca0 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -1418,7 +1418,6 @@ "providers/cerebras", "providers/chutes", "providers/claude-max-api-proxy", - "providers/clawrouter", "providers/cloudflare-ai-gateway", "providers/comfy", "providers/deepgram", diff --git a/docs/plugins/plugin-inventory.md b/docs/plugins/plugin-inventory.md index b229ed92c202..27f016c9171c 100644 --- a/docs/plugins/plugin-inventory.md +++ b/docs/plugins/plugin-inventory.md @@ -51,7 +51,7 @@ Each entry lists the package, distribution route, and description. ## Core npm package -91 plugins +90 plugins - **[admin-http-rpc](/plugins/reference/admin-http-rpc)** (`@openclaw/admin-http-rpc`) - included in OpenClaw. OpenClaw admin HTTP RPC endpoint. @@ -75,8 +75,6 @@ Each entry lists the package, distribution route, and description. - **[chutes](/plugins/reference/chutes)** (`@openclaw/chutes-provider`) - included in OpenClaw. Adds Chutes model provider support to OpenClaw. -- **[clawrouter](/plugins/reference/clawrouter)** (`@openclaw/clawrouter-provider`) - included in OpenClaw. Adds ClawRouter model provider support to OpenClaw. - - **[clickclack](/plugins/reference/clickclack)** (`@openclaw/clickclack`) - included in OpenClaw. Adds the Clickclack channel surface for sending and receiving OpenClaw messages. - **[cloudflare-ai-gateway](/plugins/reference/cloudflare-ai-gateway)** (`@openclaw/cloudflare-ai-gateway-provider`) - included in OpenClaw. Adds Cloudflare AI Gateway model provider support to OpenClaw. diff --git a/docs/plugins/reference.md b/docs/plugins/reference.md index d04bc31a2f59..6a70cf7aeb34 100644 --- a/docs/plugins/reference.md +++ b/docs/plugins/reference.md @@ -15,5 +15,5 @@ This page is generated from `extensions/*/package.json` and pnpm plugins:inventory:gen ``` -Use [Plugin inventory](/plugins/plugin-inventory) to browse all 128 +Use [Plugin inventory](/plugins/plugin-inventory) to browse all 127 generated plugin reference pages by distribution, package, and description. diff --git a/docs/plugins/reference/clawrouter.md b/docs/plugins/reference/clawrouter.md deleted file mode 100644 index c758d769160e..000000000000 --- a/docs/plugins/reference/clawrouter.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -summary: "Adds ClawRouter model provider support to OpenClaw." -read_when: - - You are installing, configuring, or auditing the clawrouter plugin -title: "ClawRouter plugin" ---- - -# ClawRouter plugin - -Adds ClawRouter model provider support to OpenClaw. - -## Distribution - -- Package: `@openclaw/clawrouter-provider` -- Install route: included in OpenClaw - -## Surface - -providers: clawrouter - -## Related docs - -- [clawrouter](/providers/clawrouter) diff --git a/docs/providers/clawrouter.md b/docs/providers/clawrouter.md deleted file mode 100644 index d943a370562a..000000000000 --- a/docs/providers/clawrouter.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -summary: "Use one managed ClawRouter key to access approved providers in OpenClaw" -title: "ClawRouter" -read_when: - - You have a ClawRouter proxy key - - Your team centrally manages provider access and grants - - You want OpenClaw to discover only the models you can use ---- - -ClawRouter gives OpenClaw one managed credential for approved model providers. -The upstream API keys, OAuth grants, and subscription credentials stay in -ClawRouter. - -## Setup - -Authenticate with the proxy key issued by your ClawRouter administrator: - -```bash -openclaw onboard --auth-choice clawrouter-api-key -``` - -Or provide it through the environment: - -```bash -export CLAWROUTER_API_KEY="clawrouter-live-..." -``` - -OpenClaw asks `https://clawrouter.openclaw.ai/v1/catalog` for the models granted -to that key and caches the credential-scoped result for a short period. Model -references keep the ClawRouter provider prefix: - -```bash -openclaw models list --provider clawrouter -openclaw models set clawrouter/openai/gpt-5.5-mini -``` - -ClawRouter publishes the real transport for each model. OpenClaw uses the -unified OpenAI route when available and the provider-native Anthropic or Gemini -route when required. - -## Custom deployment - -For a self-hosted ClawRouter, configure its API base URL: - -```json5 -{ - models: { - providers: { - clawrouter: { - baseUrl: "https://clawrouter.example/v1", - }, - }, - }, -} -``` - -The proxy key still determines which providers and models are visible and -usable. Grant changes appear after the short discovery cache expires. diff --git a/docs/providers/index.md b/docs/providers/index.md index 3eec37ef867e..02bcafa299df 100644 --- a/docs/providers/index.md +++ b/docs/providers/index.md @@ -33,7 +33,6 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/Mattermost (plugi - [BytePlus (International)](/concepts/model-providers#byteplus-international) - [Cerebras](/providers/cerebras) - [Chutes](/providers/chutes) -- [ClawRouter](/providers/clawrouter) - [Cloudflare AI Gateway](/providers/cloudflare-ai-gateway) - [ComfyUI](/providers/comfy) - [DeepSeek](/providers/deepseek) diff --git a/extensions/clawrouter/index.test.ts b/extensions/clawrouter/index.test.ts deleted file mode 100644 index 24c81f655c58..000000000000 --- a/extensions/clawrouter/index.test.ts +++ /dev/null @@ -1,228 +0,0 @@ -import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; -import { capturePluginRegistration } from "openclaw/plugin-sdk/plugin-test-runtime"; -import { clearLiveCatalogCacheForTests } from "openclaw/plugin-sdk/provider-catalog-live-runtime"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -const providerAuthRuntimeMocks = vi.hoisted(() => ({ - resolveApiKeyForProvider: vi.fn(), -})); - -vi.mock("openclaw/plugin-sdk/provider-auth-runtime", () => providerAuthRuntimeMocks); - -import plugin from "./index.js"; - -const LIVE_CATALOG = { - providers: [ - { - id: "openai", - displayName: "OpenAI", - openaiCompatible: true, - nativeBaseUrl: "/v1/native/openai", - routes: [ - { - path: "/v1/responses", - methods: ["POST"], - requestFormat: "openai.responses", - }, - ], - models: [ - { - id: "openai/gpt-5.5-mini", - upstream: "gpt-5.5-mini", - capabilities: ["llm.responses"], - }, - ], - }, - ], -}; - -describe("clawrouter provider plugin", () => { - beforeEach(() => { - clearLiveCatalogCacheForTests(); - providerAuthRuntimeMocks.resolveApiKeyForProvider.mockReset(); - }); - - afterEach(() => { - vi.unstubAllGlobals(); - }); - - it("registers managed proxy-key auth and transport routing hooks", () => { - const captured = capturePluginRegistration(plugin); - const provider = captured.providers[0]; - - expect(provider).toMatchObject({ - id: "clawrouter", - label: "ClawRouter", - docsPath: "/providers/clawrouter", - envVars: ["CLAWROUTER_API_KEY"], - isModernModelRef: expect.any(Function), - buildReplayPolicy: expect.any(Function), - normalizeResolvedModel: expect.any(Function), - sanitizeReplayHistory: expect.any(Function), - wrapSimpleCompletionStreamFn: expect.any(Function), - wrapStreamFn: expect.any(Function), - }); - expect(provider?.auth[0]).toMatchObject({ - id: "api-key", - label: "ClawRouter proxy key", - kind: "api_key", - }); - expect(provider?.wrapSimpleCompletionStreamFn).toBe(provider?.wrapStreamFn); - }); - - it("attaches the resolved proxy key only when dispatching a request", () => { - const provider = capturePluginRegistration(plugin).providers[0]; - const calls: Array[0]> = []; - const baseStreamFn: StreamFn = (model) => { - calls.push(model); - return {} as ReturnType; - }; - const wrapped = provider?.wrapStreamFn?.({ - provider: "clawrouter", - modelId: "anthropic/default", - streamFn: baseStreamFn, - } as never); - - void wrapped?.( - { - provider: "clawrouter", - api: "anthropic-messages", - id: "anthropic/default", - headers: { "X-Request-ID": "request-1" }, - params: { - clawrouterRoute: { - api: "anthropic-messages", - baseUrl: "https://clawrouter.example/v1/native/anthropic", - upstreamModel: "claude-sonnet-4-5-20250929", - }, - }, - } as never, - {} as never, - { apiKey: "runtime-proxy-key" } as never, - ); - void wrapped?.( - { - provider: "clawrouter", - api: "anthropic-messages", - id: "anthropic/default", - params: { - clawrouterRoute: { - api: "anthropic-messages", - baseUrl: "https://clawrouter.example/v1/native/anthropic", - upstreamModel: "claude-sonnet-4-5-20250929", - }, - }, - } as never, - {} as never, - { apiKey: "CLAWROUTER_API_KEY" } as never, - ); - - expect(calls[0]?.headers).toEqual({ - "X-Request-ID": "request-1", - Authorization: "Bearer runtime-proxy-key", - }); - expect(calls[0]?.id).toBe("claude-sonnet-4-5-20250929"); - expect(calls[0]?.params).toBeUndefined(); - expect(calls[1]?.headers).toBeUndefined(); - expect(calls[1]?.id).toBe("claude-sonnet-4-5-20250929"); - expect(calls[1]?.params).toBeUndefined(); - }); - - it("resolves managed secret refs before credential-scoped discovery", async () => { - providerAuthRuntimeMocks.resolveApiKeyForProvider.mockResolvedValue({ - apiKey: "resolved-proxy-key", - mode: "api-key", - source: "models.json secretref", - }); - const fetchMock = vi.fn(async () => Response.json(LIVE_CATALOG)); - vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch); - const provider = capturePluginRegistration(plugin).providers[0]; - - const result = await provider?.catalog?.run({ - config: { models: {} }, - agentDir: "/agent", - workspaceDir: "/workspace", - env: {}, - resolveProviderAuth: () => ({ - apiKey: "secretref-managed", - discoveryApiKey: undefined, - mode: "api_key", - source: "profile", - profileId: "clawrouter-profile", - }), - resolveProviderApiKey: () => ({ - apiKey: "secretref-managed", - discoveryApiKey: undefined, - }), - }); - - if (!result || !("provider" in result)) { - throw new Error("expected ClawRouter catalog provider result"); - } - expect(result.provider.apiKey).toBe("secretref-managed"); - expect(result.provider.models.map((model) => model.id)).toEqual(["openai/gpt-5.5-mini"]); - expect(providerAuthRuntimeMocks.resolveApiKeyForProvider).toHaveBeenCalledWith({ - provider: "clawrouter", - cfg: { models: {} }, - agentDir: "/agent", - workspaceDir: "/workspace", - profileId: "clawrouter-profile", - lockedProfile: true, - }); - const fetchCall = fetchMock.mock.calls[0] as unknown as [string, RequestInit] | undefined; - expect(new Headers(fetchCall?.[1]?.headers).get("Authorization")).toBe( - "Bearer resolved-proxy-key", - ); - }); - - it("normalizes configured ClawRouter roots to the API base URL", () => { - const provider = capturePluginRegistration(plugin).providers[0]; - const normalized = provider?.normalizeConfig?.({ - provider: "clawrouter", - providerConfig: { - baseUrl: "https://clawrouter.example/", - models: [], - }, - } as never); - - expect(normalized).toMatchObject({ - baseUrl: "https://clawrouter.example/v1", - }); - }); - - it("keeps replay handling aligned with each discovered transport", () => { - const provider = capturePluginRegistration(plugin).providers[0]; - const buildReplayPolicy = provider?.buildReplayPolicy; - - expect( - buildReplayPolicy?.({ - provider: "clawrouter", - modelApi: "anthropic-messages", - modelId: "anthropic/default", - } as never), - ).toMatchObject({ - preserveNativeAnthropicToolUseIds: true, - preserveSignatures: true, - validateAnthropicTurns: true, - }); - expect( - buildReplayPolicy?.({ - provider: "clawrouter", - modelApi: "google-generative-ai", - modelId: "google/gemini-default", - } as never), - ).toMatchObject({ - validateGeminiTurns: true, - }); - expect( - buildReplayPolicy?.({ - provider: "clawrouter", - modelApi: "openai-responses", - modelId: "openai/gpt-5.5-mini", - } as never), - ).toMatchObject({ - validateGeminiTurns: false, - validateAnthropicTurns: false, - }); - }); -}); diff --git a/extensions/clawrouter/index.ts b/extensions/clawrouter/index.ts deleted file mode 100644 index e021b1972d8e..000000000000 --- a/extensions/clawrouter/index.ts +++ /dev/null @@ -1,126 +0,0 @@ -// ClawRouter plugin entrypoint registers credential-scoped model routing. -import { definePluginEntry, type ProviderAuthMethod } from "openclaw/plugin-sdk/plugin-entry"; -import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key"; -import { - buildGoogleGeminiReplayPolicy, - buildNativeAnthropicReplayPolicyForModel, - buildPassthroughGeminiSanitizingReplayPolicy, - resolveTaggedReasoningOutputMode, - sanitizeGoogleGeminiReplayHistory, -} from "openclaw/plugin-sdk/provider-model-shared"; -import { - buildClawRouterProviderConfig, - normalizeClawRouterApiBaseUrl, - normalizeClawRouterResolvedModel, -} from "./provider-catalog.js"; -import { wrapClawRouterProviderStream } from "./stream.js"; - -const PROVIDER_ID = "clawrouter"; -const ENV_VAR = "CLAWROUTER_API_KEY"; - -function buildApiKeyAuth(): ProviderAuthMethod { - return createProviderApiKeyAuthMethod({ - providerId: PROVIDER_ID, - methodId: "api-key", - label: "ClawRouter proxy key", - hint: "Credential-scoped access to approved providers", - optionKey: "clawrouterApiKey", - flagName: "--clawrouter-api-key", - envVar: ENV_VAR, - promptMessage: "Enter ClawRouter proxy key", - noteTitle: "ClawRouter", - noteMessage: [ - "Use the proxy key issued by your ClawRouter administrator.", - "OpenClaw discovers only the models granted to that key.", - ].join("\n"), - wizard: { - choiceId: "clawrouter-api-key", - choiceLabel: "ClawRouter proxy key", - choiceHint: "Approved providers through one managed key", - groupId: PROVIDER_ID, - groupLabel: "ClawRouter", - groupHint: "Managed provider access", - }, - }); -} - -export default definePluginEntry({ - id: PROVIDER_ID, - name: "ClawRouter Provider", - description: "Bundled ClawRouter provider plugin", - register(api) { - api.registerProvider({ - id: PROVIDER_ID, - label: "ClawRouter", - docsPath: "/providers/clawrouter", - envVars: [ENV_VAR], - auth: [buildApiKeyAuth()], - catalog: { - order: "simple", - run: async (ctx) => { - const auth = ctx.resolveProviderAuth(PROVIDER_ID); - let discoveryApiKey = auth.discoveryApiKey; - if (!discoveryApiKey) { - try { - const { resolveApiKeyForProvider } = - await import("openclaw/plugin-sdk/provider-auth-runtime"); - discoveryApiKey = ( - await resolveApiKeyForProvider({ - provider: PROVIDER_ID, - cfg: ctx.config, - ...(ctx.agentDir ? { agentDir: ctx.agentDir } : {}), - ...(ctx.workspaceDir ? { workspaceDir: ctx.workspaceDir } : {}), - ...(auth.profileId ? { profileId: auth.profileId, lockedProfile: true } : {}), - }) - )?.apiKey; - } catch { - return null; - } - } - const apiKey = auth.apiKey ?? discoveryApiKey; - if (!apiKey || !discoveryApiKey) { - return null; - } - const configuredBaseUrl = ctx.config.models?.providers?.[PROVIDER_ID]?.baseUrl; - try { - return { - provider: await buildClawRouterProviderConfig({ - apiKey, - discoveryApiKey, - baseUrl: configuredBaseUrl, - }), - }; - } catch { - return null; - } - }, - }, - normalizeConfig: ({ providerConfig }) => { - const baseUrl = normalizeClawRouterApiBaseUrl(providerConfig.baseUrl); - return baseUrl !== providerConfig.baseUrl ? { ...providerConfig, baseUrl } : undefined; - }, - normalizeResolvedModel: ({ model }) => normalizeClawRouterResolvedModel(model), - wrapSimpleCompletionStreamFn: wrapClawRouterProviderStream, - wrapStreamFn: wrapClawRouterProviderStream, - buildReplayPolicy: ({ modelApi, modelId }) => { - if (modelApi === "anthropic-messages") { - return buildNativeAnthropicReplayPolicyForModel(modelId); - } - if (modelApi === "google-generative-ai") { - return buildGoogleGeminiReplayPolicy(); - } - if (modelApi === "openai-completions" || modelApi === "openai-responses") { - return buildPassthroughGeminiSanitizingReplayPolicy(modelId); - } - return undefined; - }, - sanitizeReplayHistory: (ctx) => - ctx.modelApi === "google-generative-ai" - ? sanitizeGoogleGeminiReplayHistory(ctx) - : undefined, - resolveReasoningOutputMode: (ctx) => - ctx.modelApi === "google-generative-ai" ? resolveTaggedReasoningOutputMode() : undefined, - isModernModelRef: () => true, - }); - }, -}); diff --git a/extensions/clawrouter/openclaw.plugin.json b/extensions/clawrouter/openclaw.plugin.json deleted file mode 100644 index 941b21f188f8..000000000000 --- a/extensions/clawrouter/openclaw.plugin.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "id": "clawrouter", - "activation": { - "onStartup": false - }, - "enabledByDefault": true, - "providers": ["clawrouter"], - "setup": { - "providers": [ - { - "id": "clawrouter", - "envVars": ["CLAWROUTER_API_KEY"] - } - ] - }, - "providerAuthChoices": [ - { - "provider": "clawrouter", - "method": "api-key", - "choiceId": "clawrouter-api-key", - "choiceLabel": "ClawRouter proxy key", - "choiceHint": "Approved providers through one managed key", - "groupId": "clawrouter", - "groupLabel": "ClawRouter", - "groupHint": "Managed provider access", - "optionKey": "clawrouterApiKey", - "cliFlag": "--clawrouter-api-key", - "cliOption": "--clawrouter-api-key ", - "cliDescription": "ClawRouter proxy key" - } - ], - "configSchema": { - "type": "object", - "additionalProperties": false, - "properties": {} - } -} diff --git a/extensions/clawrouter/package.json b/extensions/clawrouter/package.json deleted file mode 100644 index ac75818d24ef..000000000000 --- a/extensions/clawrouter/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "@openclaw/clawrouter-provider", - "version": "2026.6.9", - "private": true, - "description": "OpenClaw ClawRouter provider plugin", - "type": "module", - "devDependencies": { - "@openclaw/plugin-sdk": "workspace:*" - }, - "openclaw": { - "extensions": [ - "./index.ts" - ] - } -} diff --git a/extensions/clawrouter/provider-catalog.test.ts b/extensions/clawrouter/provider-catalog.test.ts deleted file mode 100644 index cfe60afeb76c..000000000000 --- a/extensions/clawrouter/provider-catalog.test.ts +++ /dev/null @@ -1,266 +0,0 @@ -import type { ProviderRuntimeModel } from "openclaw/plugin-sdk/plugin-entry"; -import { - clearLiveCatalogCacheForTests, - type LiveModelCatalogFetchGuard, -} from "openclaw/plugin-sdk/provider-catalog-live-runtime"; -import { beforeEach, describe, expect, it, vi, type MockedFunction } from "vitest"; -import { - buildClawRouterProviderConfig, - normalizeClawRouterResolvedModel, - prepareClawRouterRequestModel, -} from "./provider-catalog.js"; - -const CATALOG = { - version: "clawrouter.client-catalog.v1", - providers: [ - { - id: "openai", - displayName: "OpenAI", - openaiCompatible: true, - nativeBaseUrl: "/v1/native/openai", - routes: [ - { - path: "/v1/responses", - methods: ["POST"], - requestFormat: "openai.responses", - responseFormat: "openai.responses", - }, - ], - models: [ - { - id: "openai/gpt-5.5-mini", - upstream: "gpt-5.5-mini", - capabilities: ["llm.responses", "llm.chat"], - }, - ], - }, - { - id: "anthropic", - displayName: "Anthropic", - openaiCompatible: false, - nativeBaseUrl: "/v1/native/anthropic", - routes: [ - { - path: "/v1/messages", - methods: ["POST"], - requestFormat: "anthropic.messages", - responseFormat: "anthropic.messages", - }, - ], - models: [ - { - id: "anthropic/default", - upstream: "claude-sonnet-4-5-20250929", - capabilities: ["llm.messages"], - }, - ], - }, - { - id: "google-gemini", - displayName: "Google Gemini", - openaiCompatible: false, - nativeBaseUrl: "/v1/native/google-gemini", - routes: [ - { - path: "/v1beta/models/${model}:generateContent", - methods: ["POST"], - requestFormat: "google.generate_content", - responseFormat: "google.generate_content", - }, - { - path: "/v1beta/models/${model}:streamGenerateContent", - methods: ["POST"], - requestFormat: "google.generate_content", - responseFormat: "google.generate_content", - }, - ], - models: [ - { - id: "google/gemini-default", - upstream: "gemini", - capabilities: ["llm.generate", "llm.stream"], - }, - ], - }, - { - id: "cohere", - displayName: "Cohere", - openaiCompatible: false, - nativeBaseUrl: "/v1/native/cohere", - routes: [ - { - path: "/v2/chat", - methods: ["POST"], - requestFormat: "cohere.chat", - responseFormat: "cohere.chat", - }, - ], - models: [ - { - id: "cohere/default", - upstream: "command-a", - capabilities: ["llm.chat"], - }, - ], - }, - ], -}; - -function buildFetchGuard(catalog: unknown = CATALOG): { - fetchGuard: LiveModelCatalogFetchGuard; - fetchGuardMock: MockedFunction; -} { - const fetchGuardMock: MockedFunction = vi.fn(async () => ({ - response: new Response(JSON.stringify(catalog)), - finalUrl: "https://clawrouter.example/v1/catalog", - release: async () => undefined, - })); - return { fetchGuard: fetchGuardMock, fetchGuardMock }; -} - -describe("clawrouter provider catalog", () => { - beforeEach(() => { - clearLiveCatalogCacheForTests(); - }); - - it("maps credential-scoped catalog rows to their real provider transports", async () => { - const { fetchGuard, fetchGuardMock } = buildFetchGuard(); - const provider = await buildClawRouterProviderConfig({ - apiKey: "clawrouter-test-key", - baseUrl: "https://clawrouter.example/v1", - fetchGuard, - }); - - expect(fetchGuardMock).toHaveBeenCalledOnce(); - expect(provider).toMatchObject({ - api: "openai-responses", - apiKey: "clawrouter-test-key", - baseUrl: "https://clawrouter.example/v1", - }); - expect(provider.models.map((model) => model.id)).toEqual([ - "anthropic/default", - "google/gemini-default", - "openai/gpt-5.5-mini", - ]); - - expect(provider.models.find((model) => model.id === "openai/gpt-5.5-mini")).toMatchObject({ - api: "openai-responses", - baseUrl: "https://clawrouter.example/v1", - }); - expect(provider.models.find((model) => model.id === "anthropic/default")).toMatchObject({ - api: "anthropic-messages", - baseUrl: "https://clawrouter.example/v1/native/anthropic", - }); - expect(provider.models.find((model) => model.id === "google/gemini-default")).toMatchObject({ - api: "google-generative-ai", - baseUrl: "https://clawrouter.example/v1/native/google-gemini/v1beta", - }); - - const anthropic = provider.models.find((model) => model.id === "anthropic/default"); - const normalized = normalizeClawRouterResolvedModel({ - ...anthropic, - baseUrl: provider.baseUrl, - provider: "clawrouter", - } as ProviderRuntimeModel); - expect(normalized).toMatchObject({ - id: "anthropic/default", - api: "anthropic-messages", - baseUrl: "https://clawrouter.example/v1/native/anthropic", - }); - expect(prepareClawRouterRequestModel(normalized as ProviderRuntimeModel)).toMatchObject({ - id: "claude-sonnet-4-5-20250929", - params: undefined, - }); - const gemini = provider.models.find((model) => model.id === "google/gemini-default"); - const normalizedGemini = normalizeClawRouterResolvedModel({ - ...gemini, - baseUrl: provider.baseUrl, - provider: "clawrouter", - } as ProviderRuntimeModel); - expect(normalizedGemini).toMatchObject({ - id: "google/gemini-default", - api: "google-generative-ai", - baseUrl: "https://clawrouter.example/v1/native/google-gemini/v1beta", - }); - expect(prepareClawRouterRequestModel(normalizedGemini as ProviderRuntimeModel)).toMatchObject({ - id: "gemini", - params: undefined, - }); - expect(JSON.stringify(provider.models)).not.toContain("clawrouter-test-key"); - }); - - it("caches the auth-scoped catalog for the discovery TTL", async () => { - const { fetchGuard, fetchGuardMock } = buildFetchGuard(); - const params = { - apiKey: "clawrouter-test-key", - baseUrl: "https://clawrouter.example", - fetchGuard, - }; - - await buildClawRouterProviderConfig(params); - await buildClawRouterProviderConfig(params); - - expect(fetchGuardMock).toHaveBeenCalledOnce(); - const headers = fetchGuardMock.mock.calls[0]?.[0].init?.headers; - expect(headers).toBeInstanceOf(Headers); - expect((headers as Headers).get("authorization")).toBe("Bearer clawrouter-test-key"); - }); - - it("does not advertise Gemini models without an explicit streaming route", async () => { - const generateOnlyCatalog = structuredClone(CATALOG); - generateOnlyCatalog.providers[2].routes = generateOnlyCatalog.providers[2].routes.filter( - (route) => !route.path.includes(":streamGenerateContent"), - ); - generateOnlyCatalog.providers[2].models[0].capabilities = ["llm.generate"]; - const { fetchGuard } = buildFetchGuard(generateOnlyCatalog); - - const provider = await buildClawRouterProviderConfig({ - apiKey: "clawrouter-test-key", - baseUrl: "https://clawrouter.example", - fetchGuard, - }); - - expect(provider.models.map((model) => model.id)).not.toContain("google/gemini-default"); - }); - - it("keeps credential-scoped route metadata isolated on each catalog result", async () => { - const firstCatalog = structuredClone(CATALOG); - firstCatalog.providers[1].models[0].upstream = "first-upstream"; - const first = buildFetchGuard(firstCatalog); - const firstProvider = await buildClawRouterProviderConfig({ - apiKey: "first-key", - baseUrl: "https://clawrouter.example", - fetchGuard: first.fetchGuard, - }); - const firstAnthropic = firstProvider.models.find((model) => model.id === "anthropic/default"); - - const secondCatalog = structuredClone(CATALOG); - secondCatalog.providers[1].models[0].upstream = "second-upstream"; - const second = buildFetchGuard(secondCatalog); - const secondProvider = await buildClawRouterProviderConfig({ - apiKey: "second-key", - baseUrl: "https://clawrouter.example", - fetchGuard: second.fetchGuard, - }); - const secondAnthropic = secondProvider.models.find((model) => model.id === "anthropic/default"); - - expect( - prepareClawRouterRequestModel( - normalizeClawRouterResolvedModel({ - ...firstAnthropic, - baseUrl: firstProvider.baseUrl, - provider: "clawrouter", - } as ProviderRuntimeModel) as ProviderRuntimeModel, - ).id, - ).toBe("first-upstream"); - expect( - prepareClawRouterRequestModel( - normalizeClawRouterResolvedModel({ - ...secondAnthropic, - baseUrl: secondProvider.baseUrl, - provider: "clawrouter", - } as ProviderRuntimeModel) as ProviderRuntimeModel, - ).id, - ).toBe("second-upstream"); - }); -}); diff --git a/extensions/clawrouter/provider-catalog.ts b/extensions/clawrouter/provider-catalog.ts deleted file mode 100644 index ec2fd9120558..000000000000 --- a/extensions/clawrouter/provider-catalog.ts +++ /dev/null @@ -1,328 +0,0 @@ -// ClawRouter provider catalog maps credential-scoped routes to OpenClaw transports. -import type { ProviderRuntimeModel } from "openclaw/plugin-sdk/plugin-entry"; -import { - getCachedLiveProviderModelRows, - type LiveModelCatalogFetchGuard, -} from "openclaw/plugin-sdk/provider-catalog-live-runtime"; -import type { - ModelDefinitionConfig, - ModelProviderConfig, -} from "openclaw/plugin-sdk/provider-model-shared"; - -export const CLAWROUTER_DEFAULT_BASE_URL = "https://clawrouter.openclaw.ai"; - -const PROVIDER_ID = "clawrouter"; -const CATALOG_CACHE_TTL_MS = 60_000; -const ROUTE_METADATA_KEY = "clawrouterRoute"; -const DEFAULT_CONTEXT_WINDOW = 200_000; -const DEFAULT_MAX_TOKENS = 32_768; -const DEFAULT_COST = { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, -}; - -type CatalogRoute = { - path: string; - requestFormat: string; - methods: string[]; -}; - -type CatalogModel = { - id: string; - upstream: string; - capabilities: string[]; -}; - -type CatalogProvider = { - id: string; - displayName: string; - openaiCompatible: boolean; - nativeBaseUrl: string; - routes: CatalogRoute[]; - models: CatalogModel[]; -}; - -type RoutedModel = { - definition: ModelDefinitionConfig; -}; - -type RouteMetadata = { - api: NonNullable; - baseUrl: string; - upstreamModel?: string; -}; - -function readRecord(value: unknown): Record | undefined { - return value && typeof value === "object" && !Array.isArray(value) - ? (value as Record) - : undefined; -} - -function readString(value: unknown): string | undefined { - return typeof value === "string" && value.trim() ? value.trim() : undefined; -} - -function readStringArray(value: unknown): string[] { - return Array.isArray(value) - ? value.map(readString).filter((entry): entry is string => Boolean(entry)) - : []; -} - -function readCatalogRows(body: unknown): readonly unknown[] { - const providers = readRecord(body)?.providers; - if (!Array.isArray(providers)) { - throw new Error("ClawRouter catalog response must contain providers[]"); - } - return providers; -} - -function parseCatalogRoute(value: unknown): CatalogRoute | undefined { - const row = readRecord(value); - const path = readString(row?.path); - const requestFormat = readString(row?.requestFormat); - if (!path || !requestFormat) { - return undefined; - } - return { - path, - requestFormat, - methods: readStringArray(row?.methods).map((method) => method.toUpperCase()), - }; -} - -function parseCatalogModel(value: unknown): CatalogModel | undefined { - const row = readRecord(value); - const id = readString(row?.id); - const upstream = readString(row?.upstream); - if (!id || !upstream) { - return undefined; - } - return { - id, - upstream, - capabilities: readStringArray(row?.capabilities), - }; -} - -function parseCatalogProvider(value: unknown): CatalogProvider | undefined { - const row = readRecord(value); - const id = readString(row?.id); - const nativeBaseUrl = readString(row?.nativeBaseUrl); - if (!id || !nativeBaseUrl || !nativeBaseUrl.startsWith("/v1/native/")) { - return undefined; - } - return { - id, - displayName: readString(row?.displayName) ?? id, - openaiCompatible: row?.openaiCompatible === true, - nativeBaseUrl, - routes: Array.isArray(row?.routes) - ? row.routes.map(parseCatalogRoute).filter((route): route is CatalogRoute => Boolean(route)) - : [], - models: Array.isArray(row?.models) - ? row.models.map(parseCatalogModel).filter((model): model is CatalogModel => Boolean(model)) - : [], - }; -} - -function trimTrailingSlashes(value: string): string { - return value.replace(/\/+$/, ""); -} - -export function normalizeClawRouterRootUrl(baseUrl: string | undefined): string { - const normalized = trimTrailingSlashes(baseUrl?.trim() || CLAWROUTER_DEFAULT_BASE_URL); - return normalized.endsWith("/v1") ? normalized.slice(0, -3) : normalized; -} - -export function normalizeClawRouterApiBaseUrl(baseUrl: string | undefined): string { - return `${normalizeClawRouterRootUrl(baseUrl)}/v1`; -} - -function supportsCapability(model: CatalogModel, ...capabilities: string[]): boolean { - return capabilities.some((capability) => model.capabilities.includes(capability)); -} - -function findNativeRoute( - provider: CatalogProvider, - requestFormat: string, -): CatalogRoute | undefined { - return provider.routes.find( - (route) => route.methods.includes("POST") && route.requestFormat === requestFormat, - ); -} - -function googleNativeBaseUrl(rootUrl: string, provider: CatalogProvider, route: CatalogRoute) { - const modelPathIndex = route.path.indexOf("/models/${model}"); - if (modelPathIndex <= 0) { - return undefined; - } - return `${rootUrl}${provider.nativeBaseUrl}${route.path.slice(0, modelPathIndex)}`; -} - -function buildRoutedModel( - rootUrl: string, - provider: CatalogProvider, - model: CatalogModel, -): RoutedModel | undefined { - let api: NonNullable; - let baseUrl: string; - let upstreamModel: string | undefined; - - if (provider.openaiCompatible && supportsCapability(model, "llm.responses")) { - api = "openai-responses"; - baseUrl = `${rootUrl}/v1`; - } else if (provider.openaiCompatible && supportsCapability(model, "llm.chat")) { - api = "openai-completions"; - baseUrl = `${rootUrl}/v1`; - } else if ( - supportsCapability(model, "llm.messages") && - findNativeRoute(provider, "anthropic.messages") - ) { - api = "anthropic-messages"; - baseUrl = `${rootUrl}${provider.nativeBaseUrl}`; - upstreamModel = model.upstream; - } else { - const googleRoute = - supportsCapability(model, "llm.stream") && - provider.routes.find( - (route) => - route.methods.includes("POST") && - route.requestFormat === "google.generate_content" && - route.path.includes(":streamGenerateContent"), - ); - const googleBaseUrl = googleRoute - ? googleNativeBaseUrl(rootUrl, provider, googleRoute) - : undefined; - if (!googleBaseUrl) { - return undefined; - } - api = "google-generative-ai"; - baseUrl = googleBaseUrl; - upstreamModel = model.upstream; - } - - return { - definition: { - id: model.id, - name: `${provider.displayName}: ${model.id}`, - api, - baseUrl, - reasoning: false, - input: ["text"], - cost: DEFAULT_COST, - contextWindow: DEFAULT_CONTEXT_WINDOW, - maxTokens: DEFAULT_MAX_TOKENS, - params: { - [ROUTE_METADATA_KEY]: { - api, - baseUrl, - ...(upstreamModel ? { upstreamModel } : {}), - } satisfies RouteMetadata, - }, - }, - }; -} - -function buildDiscoveredModels( - rootUrl: string, - providers: CatalogProvider[], -): ModelDefinitionConfig[] { - const models = new Map(); - for (const provider of providers) { - for (const model of provider.models) { - const routed = buildRoutedModel(rootUrl, provider, model); - if (!routed || models.has(routed.definition.id)) { - continue; - } - models.set(routed.definition.id, routed.definition); - } - } - return [...models.values()].toSorted((left, right) => left.id.localeCompare(right.id)); -} - -export async function buildClawRouterProviderConfig(params: { - apiKey: string; - discoveryApiKey?: string; - baseUrl?: string; - fetchGuard?: LiveModelCatalogFetchGuard; -}): Promise { - const rootUrl = normalizeClawRouterRootUrl(params.baseUrl); - const rows = await getCachedLiveProviderModelRows({ - providerId: PROVIDER_ID, - endpoint: `${rootUrl}/v1/catalog`, - apiKey: params.apiKey, - discoveryApiKey: params.discoveryApiKey, - fetchGuard: params.fetchGuard, - readRows: readCatalogRows, - ttlMs: CATALOG_CACHE_TTL_MS, - shouldCacheRows: (providers) => providers.length > 0, - auditContext: "clawrouter-model-discovery", - }); - const providers = rows - .map(parseCatalogProvider) - .filter((provider): provider is CatalogProvider => Boolean(provider)); - return { - baseUrl: `${rootUrl}/v1`, - api: "openai-responses", - apiKey: params.apiKey, - models: buildDiscoveredModels(rootUrl, providers), - }; -} - -function readRouteMetadata(params: ProviderRuntimeModel["params"]): RouteMetadata | undefined { - const row = readRecord(params?.[ROUTE_METADATA_KEY]); - const baseUrl = readString(row?.baseUrl); - const api = readString(row?.api); - if ( - !baseUrl || - (api !== "openai-responses" && - api !== "openai-completions" && - api !== "anthropic-messages" && - api !== "google-generative-ai") - ) { - return undefined; - } - return { - api, - baseUrl, - ...(readString(row?.upstreamModel) ? { upstreamModel: readString(row?.upstreamModel) } : {}), - }; -} - -function stripRouteMetadata( - params: ProviderRuntimeModel["params"], -): ProviderRuntimeModel["params"] { - if (!params || !(ROUTE_METADATA_KEY in params)) { - return params; - } - const { [ROUTE_METADATA_KEY]: _routeMetadata, ...remaining } = params; - return Object.keys(remaining).length > 0 ? remaining : undefined; -} - -export function normalizeClawRouterResolvedModel( - model: ProviderRuntimeModel, -): ProviderRuntimeModel | undefined { - const route = readRouteMetadata(model.params); - if (!route) { - return undefined; - } - return { - ...model, - api: route.api, - baseUrl: route.baseUrl, - }; -} - -export function prepareClawRouterRequestModel(model: ProviderRuntimeModel): ProviderRuntimeModel { - const route = readRouteMetadata(model.params); - if (!route) { - return model; - } - return { - ...model, - params: stripRouteMetadata(model.params), - ...(route.upstreamModel && route.upstreamModel !== model.id ? { id: route.upstreamModel } : {}), - }; -} diff --git a/extensions/clawrouter/stream.ts b/extensions/clawrouter/stream.ts deleted file mode 100644 index ddedad9167f7..000000000000 --- a/extensions/clawrouter/stream.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; -import type { ProviderWrapStreamFnContext } from "openclaw/plugin-sdk/plugin-entry"; -import { prepareClawRouterRequestModel } from "./provider-catalog.js"; - -const ENV_API_KEY_MARKER = "CLAWROUTER_API_KEY"; - -function withBearerAuthorization( - headers: Record | undefined, - apiKey: string, -): Record { - const next: Record = {}; - for (const [name, value] of Object.entries(headers ?? {})) { - if (name.toLowerCase() !== "authorization") { - next[name] = value; - } - } - next.Authorization = `Bearer ${apiKey}`; - return next; -} - -function createClawRouterStreamWrapper(underlying: StreamFn | undefined): StreamFn | undefined { - if (!underlying) { - return undefined; - } - return (model, context, options) => { - const apiKey = options?.apiKey?.trim(); - const preparedModel = prepareClawRouterRequestModel(model); - if (!apiKey || apiKey === ENV_API_KEY_MARKER) { - return underlying(preparedModel, context, options); - } - return underlying( - { - ...preparedModel, - headers: withBearerAuthorization(preparedModel.headers, apiKey), - }, - context, - options, - ); - }; -} - -export function wrapClawRouterProviderStream( - ctx: ProviderWrapStreamFnContext, -): StreamFn | undefined { - return createClawRouterStreamWrapper(ctx.streamFn); -} diff --git a/extensions/clawrouter/tsconfig.json b/extensions/clawrouter/tsconfig.json deleted file mode 100644 index b8a85a99ac3d..000000000000 --- a/extensions/clawrouter/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "../tsconfig.package-boundary.base.json", - "compilerOptions": { - "rootDir": "." - }, - "include": ["./*.ts", "./src/**/*.ts"], - "exclude": [ - "./**/*.test.ts", - "./dist/**", - "./node_modules/**", - "./src/test-support/**", - "./src/**/*test-helpers.ts", - "./src/**/*test-harness.ts", - "./src/**/*test-support.ts" - ] -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2cb07f1494bb..c603140412eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -491,12 +491,6 @@ importers: specifier: workspace:* version: link:../../packages/plugin-sdk - extensions/clawrouter: - devDependencies: - '@openclaw/plugin-sdk': - specifier: workspace:* - version: link:../../packages/plugin-sdk - extensions/clickclack: dependencies: ws: diff --git a/scripts/generate-plugin-inventory-doc.mjs b/scripts/generate-plugin-inventory-doc.mjs index 427c81a29707..d2c36cb7eecc 100644 --- a/scripts/generate-plugin-inventory-doc.mjs +++ b/scripts/generate-plugin-inventory-doc.mjs @@ -109,7 +109,6 @@ function humanizeId(value) { ["byteplus", "BytePlus"], ["codex", "Codex"], ["cli", "CLI"], - ["clawrouter", "ClawRouter"], ["comfy", "ComfyUI"], ["dashscope", "DashScope"], ["deepgram", "Deepgram"], diff --git a/src/agents/btw.test.ts b/src/agents/btw.test.ts index 4d93aae58c19..3a0da03e34ea 100644 --- a/src/agents/btw.test.ts +++ b/src/agents/btw.test.ts @@ -1198,7 +1198,6 @@ describe("runBtwSideQuestion", () => { expect(result).toEqual({ text: "Ollama Cloud answer." }); const registerParams = expectRecordFields(mockArg(registerProviderStreamForModelMock, 0, 0), { workspaceDir: "/tmp/workspace", - applyProviderWrapper: true, }); expectRecordFields(registerParams.model, { provider: "ollama", diff --git a/src/agents/btw.ts b/src/agents/btw.ts index ab1cd34ca208..091505f1493b 100644 --- a/src/agents/btw.ts +++ b/src/agents/btw.ts @@ -719,7 +719,6 @@ export async function runBtwSideQuestion( agentDir: params.agentDir, workspaceDir, env: process.env, - applyProviderWrapper: true, }); const streamFn = resolveEmbeddedAgentStreamFn({ currentStreamFn: streamSimple, diff --git a/src/agents/provider-stream.test.ts b/src/agents/provider-stream.test.ts deleted file mode 100644 index b7dab1ffcbb5..000000000000 --- a/src/agents/provider-stream.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { Model } from "../llm/types.js"; - -const mocks = vi.hoisted(() => ({ - ensureCustomApiRegistered: vi.fn(), - createBoundaryAwareStreamFnForModel: vi.fn(), - createTransportAwareStreamFnForModel: vi.fn(), - resolveProviderStreamFn: vi.fn(), - wrapProviderStreamFn: vi.fn(), -})); - -vi.mock("../plugins/provider-runtime.js", () => ({ - resolveProviderStreamFn: mocks.resolveProviderStreamFn, - wrapProviderStreamFn: mocks.wrapProviderStreamFn, -})); -vi.mock("./custom-api-registry.js", () => ({ - ensureCustomApiRegistered: mocks.ensureCustomApiRegistered, -})); -vi.mock("./provider-transport-stream.js", () => ({ - createBoundaryAwareStreamFnForModel: mocks.createBoundaryAwareStreamFnForModel, - createTransportAwareStreamFnForModel: mocks.createTransportAwareStreamFnForModel, -})); - -import { registerProviderStreamForModel } from "./provider-stream.js"; - -const MODEL = { - id: "anthropic/default", - name: "Anthropic default", - api: "anthropic-messages", - provider: "clawrouter", - baseUrl: "https://clawrouter.example/v1/native/anthropic", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 200_000, - maxTokens: 32_768, -} satisfies Model<"anthropic-messages">; - -describe("registerProviderStreamForModel", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("applies an opted-in provider wrapper after selecting the boundary transport", () => { - const baseStream = vi.fn(); - const wrappedStream = vi.fn(); - mocks.createBoundaryAwareStreamFnForModel.mockReturnValue(baseStream); - mocks.wrapProviderStreamFn.mockReturnValue(wrappedStream); - - expect( - registerProviderStreamForModel({ - model: MODEL, - cfg: { models: {} }, - agentDir: "/agent", - workspaceDir: "/workspace", - applyProviderWrapper: true, - }), - ).toBe(wrappedStream); - expect(mocks.createBoundaryAwareStreamFnForModel).toHaveBeenCalledWith(MODEL, { - cfg: { models: {} }, - agentDir: "/agent", - workspaceDir: "/workspace", - env: undefined, - }); - expect(mocks.wrapProviderStreamFn).toHaveBeenCalledWith({ - provider: "clawrouter", - config: { models: {} }, - workspaceDir: "/workspace", - env: undefined, - context: { - config: { models: {} }, - agentDir: "/agent", - workspaceDir: "/workspace", - provider: "clawrouter", - modelId: "anthropic/default", - model: MODEL, - streamFn: baseStream, - }, - }); - expect(mocks.ensureCustomApiRegistered).toHaveBeenCalledWith( - "anthropic-messages", - wrappedStream, - ); - }); - - it("leaves existing callers unwrapped by default", () => { - const baseStream = vi.fn(); - mocks.resolveProviderStreamFn.mockReturnValue(baseStream); - - expect(registerProviderStreamForModel({ model: MODEL })).toBe(baseStream); - expect(mocks.wrapProviderStreamFn).not.toHaveBeenCalled(); - }); -}); diff --git a/src/agents/provider-stream.ts b/src/agents/provider-stream.ts index db124d7c2cff..8f30c7da63e6 100644 --- a/src/agents/provider-stream.ts +++ b/src/agents/provider-stream.ts @@ -5,12 +5,9 @@ */ import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { Api, Model } from "../llm/types.js"; -import { resolveProviderStreamFn, wrapProviderStreamFn } from "../plugins/provider-runtime.js"; +import { resolveProviderStreamFn } from "../plugins/provider-runtime.js"; import { ensureCustomApiRegistered } from "./custom-api-registry.js"; -import { - createBoundaryAwareStreamFnForModel, - createTransportAwareStreamFnForModel, -} from "./provider-transport-stream.js"; +import { createTransportAwareStreamFnForModel } from "./provider-transport-stream.js"; import type { StreamFn } from "./runtime/index.js"; /** Resolves and registers the stream function for a provider-backed model. */ @@ -21,15 +18,8 @@ export function registerProviderStreamForModel(params: { workspaceDir?: string; env?: NodeJS.ProcessEnv; allowRuntimePluginLoad?: boolean; - applyProviderWrapper?: boolean; }): StreamFn | undefined { - const transportContext = { - cfg: params.cfg, - agentDir: params.agentDir, - workspaceDir: params.workspaceDir, - env: params.env, - }; - const baseStreamFn = + const streamFn = resolveProviderStreamFn({ provider: params.model.provider, config: params.cfg, @@ -45,30 +35,15 @@ export function registerProviderStreamForModel(params: { model: params.model, }, }) ?? - createTransportAwareStreamFnForModel(params.model, transportContext) ?? - (params.applyProviderWrapper - ? createBoundaryAwareStreamFnForModel(params.model, transportContext) - : undefined); - if (!baseStreamFn) { + createTransportAwareStreamFnForModel(params.model, { + cfg: params.cfg, + agentDir: params.agentDir, + workspaceDir: params.workspaceDir, + env: params.env, + }); + if (!streamFn) { return undefined; } - const streamFn = params.applyProviderWrapper - ? (wrapProviderStreamFn({ - provider: params.model.provider, - config: params.cfg, - workspaceDir: params.workspaceDir, - env: params.env, - context: { - config: params.cfg, - agentDir: params.agentDir, - workspaceDir: params.workspaceDir, - provider: params.model.provider, - modelId: params.model.id, - model: params.model, - streamFn: baseStreamFn, - }, - }) ?? baseStreamFn) - : baseStreamFn; // Register custom APIs only after a concrete stream exists, so later callers // can route by model.api without reloading provider runtime hooks. ensureCustomApiRegistered(params.model.api, streamFn); diff --git a/src/config/zod-schema.core.ts b/src/config/zod-schema.core.ts index 6186b9763436..7aa6a06cf213 100644 --- a/src/config/zod-schema.core.ts +++ b/src/config/zod-schema.core.ts @@ -433,7 +433,6 @@ const BUILT_IN_MODEL_PROVIDER_OVERLAY_IDS = new Set([ "byteplus-plan", "cerebras", "chutes", - "clawrouter", "cloudflare-ai-gateway", "codex", "comfy",