Skip to content

Commit 791103d

Browse files
committed
fix(providers): resolve ClawRouter auth-profile models
1 parent a1063aa commit 791103d

4 files changed

Lines changed: 145 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai
2727

2828
### Fixes
2929

30+
- **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.
3031
- **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.
3132
- **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.
3233
- **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.

docs/providers/clawrouter.md

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,13 @@ issued ClawRouter credential.
3737
```bash
3838
export CLAWROUTER_API_KEY="..."
3939
openclaw onboard --auth-choice clawrouter-api-key
40+
openclaw plugins enable clawrouter
4041
```
4142

42-
The plugin is included with OpenClaw and enabled by default. For a custom
43-
deployment, set `models.providers.clawrouter.baseUrl` to the ClawRouter
44-
origin; the default is `https://clawrouter.openclaw.ai`.
43+
The plugin is bundled with OpenClaw. If your configuration sets
44+
`plugins.allow`, add `clawrouter` to that list before enabling it. For a
45+
custom deployment, set `models.providers.clawrouter.baseUrl` to the
46+
ClawRouter origin; the default is `https://clawrouter.openclaw.ai`.
4547

4648
</Step>
4749
<Step title="List granted models">
@@ -51,7 +53,8 @@ issued ClawRouter credential.
5153

5254
Use the returned model refs exactly as shown. They retain the upstream
5355
namespace, such as `clawrouter/openai/...`, `clawrouter/anthropic/...`, or
54-
`clawrouter/google/...`.
56+
`clawrouter/google/...`. If `agents.defaults.models` is an allowlist in your
57+
configuration, add each selected ClawRouter ref to it.
5558

5659
</Step>
5760
<Step title="Select a model">
@@ -121,13 +124,14 @@ the same ClawRouter policy can change the remaining percentage.
121124

122125
## Troubleshooting
123126

124-
| Symptom | Check |
125-
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
126-
| 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. |
127-
| A configured ClawRouter model is missing | Inspect its `/v1/catalog` capability and route format. Unsupported transport contracts are intentionally filtered. |
128-
| `401` or `403` from catalog or usage | Reissue or re-scope the ClawRouter credential; OpenClaw does not fall back to upstream provider keys. |
129-
| Model call fails after discovery | Check the provider connection and upstream health in ClawRouter, then retry after its readiness state recovers. |
130-
| Usage has totals but no percentage | The policy is unmetered; add a monthly budget in ClawRouter to expose a percentage window. |
127+
| Symptom | Check |
128+
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
129+
| 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. |
130+
| A configured ClawRouter model is missing | Inspect its `/v1/catalog` capability and route format. Unsupported transport contracts are intentionally filtered. |
131+
| `Unknown model: clawrouter/...` | Add the exact catalog ref to `agents.defaults.models` when that configuration map is being used as an allowlist. |
132+
| `401` or `403` from catalog or usage | Reissue or re-scope the ClawRouter credential; OpenClaw does not fall back to upstream provider keys. |
133+
| Model call fails after discovery | Check the provider connection and upstream health in ClawRouter, then retry after its readiness state recovers. |
134+
| Usage has totals but no percentage | The policy is unmetered; add a monthly budget in ClawRouter to expose a percentage window. |
131135

132136
## Security behavior
133137

extensions/clawrouter/index.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ describe("ClawRouter plugin", () => {
5353
inspectToolSchemas: expect.any(Function),
5454
normalizeResolvedModel: expect.any(Function),
5555
normalizeToolSchemas: expect.any(Function),
56+
prepareDynamicModel: expect.any(Function),
57+
resolveDynamicModel: expect.any(Function),
5658
resolveUsageAuth: expect.any(Function),
5759
sanitizeReplayHistory: expect.any(Function),
5860
wrapSimpleCompletionStreamFn: expect.any(Function),
@@ -177,6 +179,63 @@ describe("ClawRouter plugin", () => {
177179
).rejects.toThrow(/401/u);
178180
});
179181

182+
it("resolves configured catalog models through a stored auth profile", async () => {
183+
providerAuthRuntimeMocks.resolveApiKeyForProvider.mockResolvedValue({
184+
apiKey: "resolved-proxy-key",
185+
mode: "api-key",
186+
source: "auth profile",
187+
});
188+
vi.stubGlobal(
189+
"fetch",
190+
vi.fn(async () => Response.json(LIVE_CATALOG)),
191+
);
192+
const provider = await registerSingleProviderPlugin(plugin);
193+
const context = {
194+
config: { models: {} },
195+
agentDir: "/agent",
196+
workspaceDir: "/workspace",
197+
provider: "clawrouter",
198+
modelId: "openai/gpt-5.5",
199+
modelRegistry: { find: vi.fn(() => null) },
200+
authProfileId: "clawrouter-profile",
201+
authProfileMode: "api_key",
202+
};
203+
204+
expect(provider?.resolveDynamicModel?.(context as never)).toBeUndefined();
205+
await provider?.prepareDynamicModel?.(context as never);
206+
207+
expect(provider?.resolveDynamicModel?.(context as never)).toMatchObject({
208+
id: "openai/gpt-5.5",
209+
provider: "clawrouter",
210+
api: "openai-responses",
211+
baseUrl: "https://clawrouter.openclaw.ai/v1",
212+
params: {
213+
clawrouterRoute: {
214+
api: "openai-responses",
215+
baseUrl: "https://clawrouter.openclaw.ai/v1",
216+
},
217+
},
218+
});
219+
expect(providerAuthRuntimeMocks.resolveApiKeyForProvider).toHaveBeenCalledWith({
220+
provider: "clawrouter",
221+
cfg: { models: {} },
222+
agentDir: "/agent",
223+
workspaceDir: "/workspace",
224+
profileId: "clawrouter-profile",
225+
lockedProfile: true,
226+
});
227+
expect(
228+
provider?.resolveDynamicModel?.({
229+
...context,
230+
authProfileId: "another-profile",
231+
} as never),
232+
).toBeUndefined();
233+
234+
providerAuthRuntimeMocks.resolveApiKeyForProvider.mockResolvedValue(undefined);
235+
await provider?.prepareDynamicModel?.(context as never);
236+
expect(provider?.resolveDynamicModel?.(context as never)).toBeUndefined();
237+
});
238+
180239
it("dispatches replay and tool policies by upstream protocol family", async () => {
181240
const provider = await registerSingleProviderPlugin(plugin);
182241

extensions/clawrouter/index.ts

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
// ClawRouter plugin entrypoint registers credential-scoped model routing and quota reporting.
2-
import { definePluginEntry, type ProviderAuthMethod } from "openclaw/plugin-sdk/plugin-entry";
2+
import {
3+
definePluginEntry,
4+
type ProviderAuthMethod,
5+
type ProviderResolveDynamicModelContext,
6+
type ProviderRuntimeModel,
7+
} from "openclaw/plugin-sdk/plugin-entry";
38
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
49
import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";
510
import { buildProviderToolCompatFamilyHooks } from "openclaw/plugin-sdk/provider-tools";
611
import {
712
buildClawRouterProviderConfig,
813
normalizeClawRouterApiBaseUrl,
14+
normalizeClawRouterRootUrl,
915
normalizeClawRouterResolvedModel,
1016
} from "./provider-catalog.js";
1117
import { wrapClawRouterProviderStream } from "./stream.js";
@@ -52,13 +58,45 @@ function buildApiKeyAuth(): ProviderAuthMethod {
5258
});
5359
}
5460

55-
function configuredBaseUrl(config: {
56-
models?: { providers?: Record<string, { baseUrl?: unknown }> };
57-
}): string | undefined {
58-
const value = config.models?.providers?.[PROVIDER_ID]?.baseUrl;
61+
function configuredBaseUrl(
62+
config: { models?: { providers?: Record<string, { baseUrl?: unknown }> } } | null | undefined,
63+
): string | undefined {
64+
const value = config?.models?.providers?.[PROVIDER_ID]?.baseUrl;
5965
return typeof value === "string" ? value : undefined;
6066
}
6167

68+
function dynamicModelScope(ctx: ProviderResolveDynamicModelContext): string {
69+
return JSON.stringify([
70+
ctx.agentDir ?? "",
71+
ctx.workspaceDir ?? "",
72+
ctx.authProfileId ?? "",
73+
normalizeClawRouterRootUrl(ctx.providerConfig?.baseUrl ?? configuredBaseUrl(ctx.config)),
74+
]);
75+
}
76+
77+
function buildRuntimeModels(
78+
providerConfig: Awaited<ReturnType<typeof buildClawRouterProviderConfig>>,
79+
): Map<string, ProviderRuntimeModel> {
80+
const models = new Map<string, ProviderRuntimeModel>();
81+
for (const model of providerConfig.models) {
82+
const api = model.api ?? providerConfig.api;
83+
const baseUrl = model.baseUrl ?? providerConfig.baseUrl;
84+
if (!api || !baseUrl) {
85+
continue;
86+
}
87+
models.set(model.id, {
88+
...model,
89+
api,
90+
baseUrl,
91+
provider: PROVIDER_ID,
92+
input: model.input.filter(
93+
(entry): entry is "text" | "image" => entry === "text" || entry === "image",
94+
),
95+
});
96+
}
97+
return models;
98+
}
99+
62100
function resolveToolFamily(modelId: string) {
63101
const normalized = modelId.toLowerCase();
64102
if (normalized.startsWith("deepseek/")) {
@@ -75,6 +113,8 @@ export default definePluginEntry({
75113
name: "ClawRouter",
76114
description: "Managed multi-provider model routing and quotas",
77115
register(api) {
116+
const dynamicModels = new Map<string, Map<string, ProviderRuntimeModel>>();
117+
78118
api.registerProvider({
79119
id: PROVIDER_ID,
80120
label: "ClawRouter",
@@ -116,6 +156,31 @@ export default definePluginEntry({
116156
};
117157
},
118158
},
159+
resolveDynamicModel: (ctx) => dynamicModels.get(dynamicModelScope(ctx))?.get(ctx.modelId),
160+
prepareDynamicModel: async (ctx) => {
161+
const scope = dynamicModelScope(ctx);
162+
dynamicModels.delete(scope);
163+
const { resolveApiKeyForProvider } =
164+
await import("openclaw/plugin-sdk/provider-auth-runtime");
165+
const apiKey = (
166+
await resolveApiKeyForProvider({
167+
provider: PROVIDER_ID,
168+
cfg: ctx.config,
169+
...(ctx.agentDir ? { agentDir: ctx.agentDir } : {}),
170+
...(ctx.workspaceDir ? { workspaceDir: ctx.workspaceDir } : {}),
171+
...(ctx.authProfileId ? { profileId: ctx.authProfileId, lockedProfile: true } : {}),
172+
})
173+
)?.apiKey;
174+
if (!apiKey) {
175+
return;
176+
}
177+
const providerConfig = await buildClawRouterProviderConfig({
178+
apiKey,
179+
discoveryApiKey: apiKey,
180+
baseUrl: ctx.providerConfig?.baseUrl ?? configuredBaseUrl(ctx.config),
181+
});
182+
dynamicModels.set(scope, buildRuntimeModels(providerConfig));
183+
},
119184
normalizeConfig: ({ providerConfig }) => {
120185
const baseUrl = normalizeClawRouterApiBaseUrl(providerConfig.baseUrl);
121186
return baseUrl !== providerConfig.baseUrl ? { ...providerConfig, baseUrl } : undefined;

0 commit comments

Comments
 (0)