diff --git a/CHANGELOG.md b/CHANGELOG.md
index 429330040a69..fabbd38ece7f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai
### Fixes
+- **ClawRouter auth profiles:** resolve credential-scoped catalog models during agent runs when the proxy key is stored in an auth profile, and document plugin and model allowlists.
- **Telegram durability:** recover stalled ingress claims, retry restart-dropped media, survive transient polling errors, dead-letter poison updates, preserve forwarded rich text, route plugin callbacks correctly, and fall back safely when rich final replies are rejected. (#97118, #98102, #98735, #98775, #98776, #97174, #98786) Thanks @vincentkoc, @luoyanglang, @DaveArcher18, @obviyus, and @goldmar.
- **Agent and context reliability:** preserve runtime overrides and steered subagent tasks, improve harness-aware context estimation and compaction prechecks, time out silent local streams, recover mid-stream failures, and cap Gateway run-cache growth. (#92237, #77539, #97928, #97861, #98525, #95430, #77973) Thanks @sercada, @amittell, @liuhao1024, @yetval, @osolmaz, @lzyyzznl, @vincentkoc, @alexelgier, and @fede-kamel.
- **Provider and network safety:** bound oversized or malformed responses across Moonshot, MiniMax, Anthropic OAuth, Discord, Matrix, SMS, browser, update, embeddings, Tlön, and Inworld paths. (#96502, #96322, #96644, #97693, #97662, #97999, #98455, #98508, #98554, #98496, #98660) Thanks @hugenshen, @cursoragent, @lsr911, @solodmd, @Alix-007, @wings1029, @lzyyzznl, @sunlit-deng, @vincentkoc, and @Pandah97.
diff --git a/docs/providers/clawrouter.md b/docs/providers/clawrouter.md
index d12c90eef9af..9cdc9e03b295 100644
--- a/docs/providers/clawrouter.md
+++ b/docs/providers/clawrouter.md
@@ -37,11 +37,13 @@ issued ClawRouter credential.
```bash
export CLAWROUTER_API_KEY="..."
openclaw onboard --auth-choice clawrouter-api-key
+ openclaw plugins enable clawrouter
```
- The plugin is included with OpenClaw and enabled by default. For a custom
- deployment, set `models.providers.clawrouter.baseUrl` to the ClawRouter
- origin; the default is `https://clawrouter.openclaw.ai`.
+ The plugin is bundled with OpenClaw. If your configuration sets
+ `plugins.allow`, add `clawrouter` to that list before enabling it. For a
+ custom deployment, set `models.providers.clawrouter.baseUrl` to the
+ ClawRouter origin; the default is `https://clawrouter.openclaw.ai`.
@@ -51,7 +53,8 @@ issued ClawRouter credential.
Use the returned model refs exactly as shown. They retain the upstream
namespace, such as `clawrouter/openai/...`, `clawrouter/anthropic/...`, or
- `clawrouter/google/...`.
+ `clawrouter/google/...`. If `agents.defaults.models` is an allowlist in your
+ configuration, add each selected ClawRouter ref to it.
@@ -121,13 +124,14 @@ the same ClawRouter policy can change the remaining percentage.
## Troubleshooting
-| Symptom | Check |
-| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
-| No ClawRouter models | Confirm the credential is active, its policy grants at least one ready model provider, and `CLAWROUTER_API_KEY` is available to the OpenClaw process. |
-| A configured ClawRouter model is missing | Inspect its `/v1/catalog` capability and route format. Unsupported transport contracts are intentionally filtered. |
-| `401` or `403` from catalog or usage | Reissue or re-scope the ClawRouter credential; OpenClaw does not fall back to upstream provider keys. |
-| Model call fails after discovery | Check the provider connection and upstream health in ClawRouter, then retry after its readiness state recovers. |
-| Usage has totals but no percentage | The policy is unmetered; add a monthly budget in ClawRouter to expose a percentage window. |
+| Symptom | Check |
+| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
+| No ClawRouter models | Confirm the plugin is enabled and allowed by `plugins.allow`, then check that the credential is active and grants at least one ready provider. |
+| A configured ClawRouter model is missing | Inspect its `/v1/catalog` capability and route format. Unsupported transport contracts are intentionally filtered. |
+| `Unknown model: clawrouter/...` | Add the exact catalog ref to `agents.defaults.models` when that configuration map is being used as an allowlist. |
+| `401` or `403` from catalog or usage | Reissue or re-scope the ClawRouter credential; OpenClaw does not fall back to upstream provider keys. |
+| Model call fails after discovery | Check the provider connection and upstream health in ClawRouter, then retry after its readiness state recovers. |
+| Usage has totals but no percentage | The policy is unmetered; add a monthly budget in ClawRouter to expose a percentage window. |
## Security behavior
diff --git a/extensions/clawrouter/index.test.ts b/extensions/clawrouter/index.test.ts
index e6f51bb29382..5d42803a316b 100644
--- a/extensions/clawrouter/index.test.ts
+++ b/extensions/clawrouter/index.test.ts
@@ -53,6 +53,8 @@ describe("ClawRouter plugin", () => {
inspectToolSchemas: expect.any(Function),
normalizeResolvedModel: expect.any(Function),
normalizeToolSchemas: expect.any(Function),
+ prepareDynamicModel: expect.any(Function),
+ resolveDynamicModel: expect.any(Function),
resolveUsageAuth: expect.any(Function),
sanitizeReplayHistory: expect.any(Function),
wrapSimpleCompletionStreamFn: expect.any(Function),
@@ -177,6 +179,63 @@ describe("ClawRouter plugin", () => {
).rejects.toThrow(/401/u);
});
+ it("resolves configured catalog models through a stored auth profile", async () => {
+ providerAuthRuntimeMocks.resolveApiKeyForProvider.mockResolvedValue({
+ apiKey: "resolved-proxy-key",
+ mode: "api-key",
+ source: "auth profile",
+ });
+ vi.stubGlobal(
+ "fetch",
+ vi.fn(async () => Response.json(LIVE_CATALOG)),
+ );
+ const provider = await registerSingleProviderPlugin(plugin);
+ const context = {
+ config: { models: {} },
+ agentDir: "/agent",
+ workspaceDir: "/workspace",
+ provider: "clawrouter",
+ modelId: "openai/gpt-5.5",
+ modelRegistry: { find: vi.fn(() => null) },
+ authProfileId: "clawrouter-profile",
+ authProfileMode: "api_key",
+ };
+
+ expect(provider?.resolveDynamicModel?.(context as never)).toBeUndefined();
+ await provider?.prepareDynamicModel?.(context as never);
+
+ expect(provider?.resolveDynamicModel?.(context as never)).toMatchObject({
+ id: "openai/gpt-5.5",
+ provider: "clawrouter",
+ api: "openai-responses",
+ baseUrl: "https://clawrouter.openclaw.ai/v1",
+ params: {
+ clawrouterRoute: {
+ api: "openai-responses",
+ baseUrl: "https://clawrouter.openclaw.ai/v1",
+ },
+ },
+ });
+ expect(providerAuthRuntimeMocks.resolveApiKeyForProvider).toHaveBeenCalledWith({
+ provider: "clawrouter",
+ cfg: { models: {} },
+ agentDir: "/agent",
+ workspaceDir: "/workspace",
+ profileId: "clawrouter-profile",
+ lockedProfile: true,
+ });
+ expect(
+ provider?.resolveDynamicModel?.({
+ ...context,
+ authProfileId: "another-profile",
+ } as never),
+ ).toBeUndefined();
+
+ providerAuthRuntimeMocks.resolveApiKeyForProvider.mockResolvedValue(undefined);
+ await provider?.prepareDynamicModel?.(context as never);
+ expect(provider?.resolveDynamicModel?.(context as never)).toBeUndefined();
+ });
+
it("dispatches replay and tool policies by upstream protocol family", async () => {
const provider = await registerSingleProviderPlugin(plugin);
diff --git a/extensions/clawrouter/index.ts b/extensions/clawrouter/index.ts
index a7d6bcedac3c..2ad9b90371b7 100644
--- a/extensions/clawrouter/index.ts
+++ b/extensions/clawrouter/index.ts
@@ -1,11 +1,17 @@
// ClawRouter plugin entrypoint registers credential-scoped model routing and quota reporting.
-import { definePluginEntry, type ProviderAuthMethod } from "openclaw/plugin-sdk/plugin-entry";
+import {
+ definePluginEntry,
+ type ProviderAuthMethod,
+ type ProviderResolveDynamicModelContext,
+ type ProviderRuntimeModel,
+} from "openclaw/plugin-sdk/plugin-entry";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";
import { buildProviderToolCompatFamilyHooks } from "openclaw/plugin-sdk/provider-tools";
import {
buildClawRouterProviderConfig,
normalizeClawRouterApiBaseUrl,
+ normalizeClawRouterRootUrl,
normalizeClawRouterResolvedModel,
} from "./provider-catalog.js";
import { wrapClawRouterProviderStream } from "./stream.js";
@@ -52,13 +58,45 @@ function buildApiKeyAuth(): ProviderAuthMethod {
});
}
-function configuredBaseUrl(config: {
- models?: { providers?: Record };
-}): string | undefined {
- const value = config.models?.providers?.[PROVIDER_ID]?.baseUrl;
+function configuredBaseUrl(
+ config: { models?: { providers?: Record } } | null | undefined,
+): string | undefined {
+ const value = config?.models?.providers?.[PROVIDER_ID]?.baseUrl;
return typeof value === "string" ? value : undefined;
}
+function dynamicModelScope(ctx: ProviderResolveDynamicModelContext): string {
+ return JSON.stringify([
+ ctx.agentDir ?? "",
+ ctx.workspaceDir ?? "",
+ ctx.authProfileId ?? "",
+ normalizeClawRouterRootUrl(ctx.providerConfig?.baseUrl ?? configuredBaseUrl(ctx.config)),
+ ]);
+}
+
+function buildRuntimeModels(
+ providerConfig: Awaited>,
+): Map {
+ const models = new Map();
+ for (const model of providerConfig.models) {
+ const api = model.api ?? providerConfig.api;
+ const baseUrl = model.baseUrl ?? providerConfig.baseUrl;
+ if (!api || !baseUrl) {
+ continue;
+ }
+ models.set(model.id, {
+ ...model,
+ api,
+ baseUrl,
+ provider: PROVIDER_ID,
+ input: model.input.filter(
+ (entry): entry is "text" | "image" => entry === "text" || entry === "image",
+ ),
+ });
+ }
+ return models;
+}
+
function resolveToolFamily(modelId: string) {
const normalized = modelId.toLowerCase();
if (normalized.startsWith("deepseek/")) {
@@ -75,6 +113,8 @@ export default definePluginEntry({
name: "ClawRouter",
description: "Managed multi-provider model routing and quotas",
register(api) {
+ const dynamicModels = new Map>();
+
api.registerProvider({
id: PROVIDER_ID,
label: "ClawRouter",
@@ -116,6 +156,31 @@ export default definePluginEntry({
};
},
},
+ resolveDynamicModel: (ctx) => dynamicModels.get(dynamicModelScope(ctx))?.get(ctx.modelId),
+ prepareDynamicModel: async (ctx) => {
+ const scope = dynamicModelScope(ctx);
+ dynamicModels.delete(scope);
+ const { resolveApiKeyForProvider } =
+ await import("openclaw/plugin-sdk/provider-auth-runtime");
+ const apiKey = (
+ await resolveApiKeyForProvider({
+ provider: PROVIDER_ID,
+ cfg: ctx.config,
+ ...(ctx.agentDir ? { agentDir: ctx.agentDir } : {}),
+ ...(ctx.workspaceDir ? { workspaceDir: ctx.workspaceDir } : {}),
+ ...(ctx.authProfileId ? { profileId: ctx.authProfileId, lockedProfile: true } : {}),
+ })
+ )?.apiKey;
+ if (!apiKey) {
+ return;
+ }
+ const providerConfig = await buildClawRouterProviderConfig({
+ apiKey,
+ discoveryApiKey: apiKey,
+ baseUrl: ctx.providerConfig?.baseUrl ?? configuredBaseUrl(ctx.config),
+ });
+ dynamicModels.set(scope, buildRuntimeModels(providerConfig));
+ },
normalizeConfig: ({ providerConfig }) => {
const baseUrl = normalizeClawRouterApiBaseUrl(providerConfig.baseUrl);
return baseUrl !== providerConfig.baseUrl ? { ...providerConfig, baseUrl } : undefined;