mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-11 02:16:06 +00:00
fix(providers): resolve ClawRouter auth-profile models (#99759)
This commit is contained in:
committed by
GitHub
parent
1d6cbbc428
commit
8250b8d02e
@@ -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.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
</Step>
|
||||
<Step title="List granted models">
|
||||
@@ -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.
|
||||
|
||||
</Step>
|
||||
<Step title="Select a model">
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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, { baseUrl?: unknown }> };
|
||||
}): string | undefined {
|
||||
const value = config.models?.providers?.[PROVIDER_ID]?.baseUrl;
|
||||
function configuredBaseUrl(
|
||||
config: { models?: { providers?: Record<string, { baseUrl?: unknown }> } } | 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<ReturnType<typeof buildClawRouterProviderConfig>>,
|
||||
): Map<string, ProviderRuntimeModel> {
|
||||
const models = new Map<string, ProviderRuntimeModel>();
|
||||
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<string, Map<string, ProviderRuntimeModel>>();
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user