fix(providers): cover ClawRouter runtime auth paths

This commit is contained in:
Vincent Koc
2026-06-17 07:56:34 +08:00
committed by Vincent Koc
parent 8a4d92d362
commit 5af0ccfd5f
2 changed files with 115 additions and 5 deletions

View File

@@ -1,9 +1,51 @@
import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
import { capturePluginRegistration } from "openclaw/plugin-sdk/plugin-test-runtime";
import { describe, expect, it } from "vitest";
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];
@@ -17,6 +59,7 @@ describe("clawrouter provider plugin", () => {
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({
@@ -24,6 +67,7 @@ describe("clawrouter provider plugin", () => {
label: "ClawRouter proxy key",
kind: "api_key",
});
expect(provider?.wrapSimpleCompletionStreamFn).toBe(provider?.wrapStreamFn);
});
it("attaches the resolved proxy key only when dispatching a request", () => {
@@ -66,6 +110,53 @@ describe("clawrouter provider plugin", () => {
expect(calls[1]?.headers).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?.({

View File

@@ -58,9 +58,27 @@ export default definePluginEntry({
catalog: {
order: "simple",
run: async (ctx) => {
const auth = ctx.resolveProviderApiKey(PROVIDER_ID);
const apiKey = auth.apiKey ?? auth.discoveryApiKey;
if (!apiKey) {
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;
@@ -68,7 +86,7 @@ export default definePluginEntry({
return {
provider: await buildClawRouterProviderConfig({
apiKey,
discoveryApiKey: auth.discoveryApiKey,
discoveryApiKey,
baseUrl: configuredBaseUrl,
}),
};
@@ -82,6 +100,7 @@ export default definePluginEntry({
return baseUrl !== providerConfig.baseUrl ? { ...providerConfig, baseUrl } : undefined;
},
normalizeResolvedModel: ({ model }) => normalizeClawRouterResolvedModel(model),
wrapSimpleCompletionStreamFn: wrapClawRouterProviderStream,
wrapStreamFn: wrapClawRouterProviderStream,
buildReplayPolicy: ({ modelApi, modelId }) => {
if (modelApi === "anthropic-messages") {