mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:20:43 +00:00
refactor: move synthetic auth refs to manifests
This commit is contained in:
@@ -96,6 +96,7 @@ Those belong in your plugin code and `package.json`.
|
||||
"modelPrefixes": ["router-"]
|
||||
},
|
||||
"cliBackends": ["openrouter-cli"],
|
||||
"syntheticAuthRefs": ["openrouter-cli"],
|
||||
"providerAuthEnvVars": {
|
||||
"openrouter": ["OPENROUTER_API_KEY"]
|
||||
},
|
||||
@@ -153,6 +154,7 @@ Those belong in your plugin code and `package.json`.
|
||||
| `providers` | No | `string[]` | Provider ids owned by this plugin. |
|
||||
| `modelSupport` | No | `object` | Manifest-owned shorthand model-family metadata used to auto-load the plugin before runtime. |
|
||||
| `cliBackends` | No | `string[]` | CLI inference backend ids owned by this plugin. Used for startup auto-activation from explicit config refs. |
|
||||
| `syntheticAuthRefs` | No | `string[]` | Provider or CLI backend refs whose plugin-owned synthetic auth hook should be probed during cold model discovery before runtime loads. |
|
||||
| `commandAliases` | No | `object[]` | Command names owned by this plugin that should produce plugin-aware config and CLI diagnostics before runtime loads. |
|
||||
| `providerAuthEnvVars` | No | `Record<string, string[]>` | Cheap provider-auth env metadata that OpenClaw can inspect without loading plugin code. |
|
||||
| `providerAuthAliases` | No | `Record<string, string>` | Provider ids that should reuse another provider id for auth lookup, for example a coding provider that shares the base provider API key and auth profiles. |
|
||||
@@ -599,6 +601,10 @@ See [Configuration reference](/gateway/configuration) for the full `plugins.*` s
|
||||
- `providerAuthAliases` lets provider variants reuse another provider's auth
|
||||
env vars, auth profiles, config-backed auth, and API-key onboarding choice
|
||||
without hardcoding that relationship in core.
|
||||
- `syntheticAuthRefs` is the cheap metadata path for provider-owned synthetic
|
||||
auth hooks that must be visible to cold model discovery before the runtime
|
||||
registry exists. Only list refs whose runtime provider or CLI backend actually
|
||||
implements `resolveSyntheticAuth`.
|
||||
- `channelEnvVars` is the cheap metadata path for shell-env fallback, setup
|
||||
prompts, and similar channel surfaces that should not boot plugin runtime
|
||||
just to inspect env names.
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"modelPrefixes": ["claude-"]
|
||||
},
|
||||
"cliBackends": ["claude-cli"],
|
||||
"syntheticAuthRefs": ["claude-cli"],
|
||||
"providerAuthEnvVars": {
|
||||
"anthropic": ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"]
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"enabledByDefault": true,
|
||||
"providers": ["ollama"],
|
||||
"providerDiscoveryEntry": "./provider-discovery.ts",
|
||||
"syntheticAuthRefs": ["ollama"],
|
||||
"providerAuthEnvVars": {
|
||||
"ollama": ["OLLAMA_API_KEY"]
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"id": "xai",
|
||||
"enabledByDefault": true,
|
||||
"providers": ["xai"],
|
||||
"syntheticAuthRefs": ["xai"],
|
||||
"providerAuthEnvVars": {
|
||||
"xai": ["XAI_API_KEY"]
|
||||
},
|
||||
|
||||
@@ -17,17 +17,6 @@ beforeEach(() => {
|
||||
});
|
||||
return createMoonshotThinkingWrapper(params.context.streamFn, thinkingType);
|
||||
}
|
||||
if (params.provider === "ollama") {
|
||||
const modelId = params.context.model?.id ?? params.context.modelId;
|
||||
if (typeof modelId === "string" && /^kimi-k2\.5(?::|$)/i.test(modelId)) {
|
||||
const thinkingType = resolveMoonshotThinkingType({
|
||||
configuredThinking: params.context.extraParams?.thinking,
|
||||
thinkingLevel: params.context.thinkingLevel,
|
||||
});
|
||||
return createMoonshotThinkingWrapper(params.context.streamFn, thinkingType);
|
||||
}
|
||||
return params.context.streamFn;
|
||||
}
|
||||
return params.context.streamFn;
|
||||
},
|
||||
});
|
||||
@@ -37,7 +26,7 @@ afterEach(() => {
|
||||
extraParamsTesting.resetProviderRuntimeDepsForTest();
|
||||
});
|
||||
|
||||
describe("applyExtraParamsToAgent Moonshot and Ollama Kimi", () => {
|
||||
describe("applyExtraParamsToAgent Moonshot", () => {
|
||||
it("maps thinkingLevel=off to Moonshot thinking.type=disabled", () => {
|
||||
const payload = runExtraParamsPayloadCase({
|
||||
provider: "moonshot",
|
||||
@@ -94,41 +83,4 @@ describe("applyExtraParamsToAgent Moonshot and Ollama Kimi", () => {
|
||||
|
||||
expect(payload.thinking).toEqual({ type: "disabled" });
|
||||
});
|
||||
|
||||
it("applies Moonshot payload compatibility to Ollama Kimi cloud models", () => {
|
||||
const payload = runExtraParamsPayloadCase({
|
||||
provider: "ollama",
|
||||
modelId: "kimi-k2.5:cloud",
|
||||
thinkingLevel: "low",
|
||||
payload: { tool_choice: "required" },
|
||||
});
|
||||
|
||||
expect(payload.thinking).toEqual({ type: "enabled" });
|
||||
expect(payload.tool_choice).toBe("auto");
|
||||
});
|
||||
|
||||
it("maps thinkingLevel=off for Ollama Kimi cloud models through Moonshot compatibility", () => {
|
||||
const payload = runExtraParamsPayloadCase({
|
||||
provider: "ollama",
|
||||
modelId: "kimi-k2.5:cloud",
|
||||
thinkingLevel: "off",
|
||||
});
|
||||
|
||||
expect(payload.thinking).toEqual({ type: "disabled" });
|
||||
});
|
||||
|
||||
it("disables thinking instead of broadening pinned Ollama Kimi cloud tool_choice", () => {
|
||||
const payload = runExtraParamsPayloadCase({
|
||||
provider: "ollama",
|
||||
modelId: "kimi-k2.5:cloud",
|
||||
thinkingLevel: "low",
|
||||
payload: { tool_choice: { type: "function", function: { name: "read" } } },
|
||||
});
|
||||
|
||||
expect(payload.thinking).toEqual({ type: "disabled" });
|
||||
expect(payload.tool_choice).toEqual({
|
||||
type: "function",
|
||||
function: { name: "read" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ beforeEach(() => {
|
||||
extraParamsTesting.setProviderRuntimeDepsForTest({
|
||||
prepareProviderExtraParams: ({ context }) => context.extraParams,
|
||||
wrapProviderStreamFn: ({ provider, context }) => {
|
||||
if (provider !== "ollama" || context.thinkingLevel !== "off") {
|
||||
if (provider !== "local-provider" || context.thinkingLevel !== "off") {
|
||||
return context.streamFn;
|
||||
}
|
||||
const baseStreamFn = context.streamFn;
|
||||
@@ -44,19 +44,19 @@ afterEach(() => {
|
||||
extraParamsTesting.resetProviderRuntimeDepsForTest();
|
||||
});
|
||||
|
||||
describe("extra-params: Ollama plugin handoff", () => {
|
||||
describe("extra-params: provider runtime handoff", () => {
|
||||
it("passes thinking-off intent through the provider runtime wrapper seam", () => {
|
||||
const payload = runExtraParamsCase({
|
||||
applyProvider: "ollama",
|
||||
applyModelId: "qwen3.5:9b",
|
||||
applyProvider: "local-provider",
|
||||
applyModelId: "local-model:9b",
|
||||
model: {
|
||||
api: "ollama",
|
||||
provider: "ollama",
|
||||
id: "qwen3.5:9b",
|
||||
api: "openai-completions",
|
||||
provider: "local-provider",
|
||||
id: "local-model:9b",
|
||||
} as unknown as Model<"openai-completions">,
|
||||
thinkingLevel: "off",
|
||||
payload: {
|
||||
model: "qwen3.5:9b",
|
||||
model: "local-model:9b",
|
||||
messages: [],
|
||||
stream: true,
|
||||
options: {
|
||||
@@ -70,7 +70,7 @@ describe("extra-params: Ollama plugin handoff", () => {
|
||||
expect((payload.options as Record<string, unknown>).think).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not apply the plugin wrapper for non-ollama providers", () => {
|
||||
it("does not apply the plugin wrapper for other providers", () => {
|
||||
const payload = runExtraParamsCase({
|
||||
applyProvider: "openai",
|
||||
applyModelId: "gpt-5.4",
|
||||
@@ -91,16 +91,16 @@ describe("extra-params: Ollama plugin handoff", () => {
|
||||
|
||||
it("does not apply the plugin wrapper when thinkingLevel is not off", () => {
|
||||
const payload = runExtraParamsCase({
|
||||
applyProvider: "ollama",
|
||||
applyModelId: "qwen3.5:9b",
|
||||
applyProvider: "local-provider",
|
||||
applyModelId: "local-model:9b",
|
||||
model: {
|
||||
api: "ollama",
|
||||
provider: "ollama",
|
||||
id: "qwen3.5:9b",
|
||||
api: "openai-completions",
|
||||
provider: "local-provider",
|
||||
id: "local-model:9b",
|
||||
} as unknown as Model<"openai-completions">,
|
||||
thinkingLevel: "high",
|
||||
payload: {
|
||||
model: "qwen3.5:9b",
|
||||
model: "local-model:9b",
|
||||
messages: [],
|
||||
stream: true,
|
||||
options: {
|
||||
@@ -382,6 +382,7 @@ describe("loadPluginManifestRegistry", () => {
|
||||
providerAuthEnvVars: {
|
||||
openai: ["OPENAI_API_KEY"],
|
||||
},
|
||||
syntheticAuthRefs: ["openai-cli"],
|
||||
providerAuthAliases: {
|
||||
"openai-codex": "openai",
|
||||
},
|
||||
@@ -407,6 +408,7 @@ describe("loadPluginManifestRegistry", () => {
|
||||
expect(registry.plugins[0]?.providerAuthEnvVars).toEqual({
|
||||
openai: ["OPENAI_API_KEY"],
|
||||
});
|
||||
expect(registry.plugins[0]?.syntheticAuthRefs).toEqual(["openai-cli"]);
|
||||
expect(registry.plugins[0]?.providerAuthAliases).toEqual({
|
||||
"openai-codex": "openai",
|
||||
});
|
||||
|
||||
@@ -86,6 +86,7 @@ export type PluginManifestRecord = {
|
||||
providerDiscoverySource?: string;
|
||||
modelSupport?: PluginManifestModelSupport;
|
||||
cliBackends: string[];
|
||||
syntheticAuthRefs?: string[];
|
||||
commandAliases?: PluginManifestCommandAlias[];
|
||||
providerAuthEnvVars?: Record<string, string[]>;
|
||||
providerAuthAliases?: Record<string, string>;
|
||||
@@ -328,6 +329,7 @@ function buildRecord(params: {
|
||||
: undefined,
|
||||
modelSupport: params.manifest.modelSupport,
|
||||
cliBackends: params.manifest.cliBackends ?? [],
|
||||
syntheticAuthRefs: params.manifest.syntheticAuthRefs ?? [],
|
||||
commandAliases: params.manifest.commandAliases,
|
||||
providerAuthEnvVars: params.manifest.providerAuthEnvVars,
|
||||
providerAuthAliases: params.manifest.providerAuthAliases,
|
||||
@@ -398,6 +400,7 @@ function buildBundleRecord(params: {
|
||||
channels: [],
|
||||
providers: [],
|
||||
cliBackends: [],
|
||||
syntheticAuthRefs: [],
|
||||
skills: params.manifest.skills ?? [],
|
||||
settingsFiles: params.manifest.settingsFiles ?? [],
|
||||
hooks: params.manifest.hooks ?? [],
|
||||
|
||||
@@ -163,6 +163,11 @@ export type PluginManifest = {
|
||||
modelSupport?: PluginManifestModelSupport;
|
||||
/** Cheap startup activation lookup for plugin-owned CLI inference backends. */
|
||||
cliBackends?: string[];
|
||||
/**
|
||||
* Provider or CLI backend refs whose plugin-owned synthetic auth hook should
|
||||
* be probed during cold model discovery before the runtime registry exists.
|
||||
*/
|
||||
syntheticAuthRefs?: string[];
|
||||
/**
|
||||
* Plugin-owned command aliases that should resolve to this plugin during
|
||||
* config diagnostics before runtime loads.
|
||||
@@ -701,6 +706,7 @@ export function loadPluginManifest(
|
||||
const providerDiscoveryEntry = normalizeOptionalString(raw.providerDiscoveryEntry);
|
||||
const modelSupport = normalizeManifestModelSupport(raw.modelSupport);
|
||||
const cliBackends = normalizeTrimmedStringList(raw.cliBackends);
|
||||
const syntheticAuthRefs = normalizeTrimmedStringList(raw.syntheticAuthRefs);
|
||||
const commandAliases = normalizeManifestCommandAliases(raw.commandAliases);
|
||||
const providerAuthEnvVars = normalizeStringListRecord(raw.providerAuthEnvVars);
|
||||
const providerAuthAliases = normalizeStringRecord(raw.providerAuthAliases);
|
||||
@@ -735,6 +741,7 @@ export function loadPluginManifest(
|
||||
providerDiscoveryEntry,
|
||||
modelSupport,
|
||||
cliBackends,
|
||||
syntheticAuthRefs,
|
||||
commandAliases,
|
||||
providerAuthEnvVars,
|
||||
providerAuthAliases,
|
||||
|
||||
69
src/plugins/synthetic-auth.runtime.test.ts
Normal file
69
src/plugins/synthetic-auth.runtime.test.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const getPluginRegistryState = vi.hoisted(() => vi.fn());
|
||||
const loadPluginManifestRegistry = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./runtime-state.js", () => ({
|
||||
getPluginRegistryState,
|
||||
}));
|
||||
|
||||
vi.mock("./manifest-registry.js", () => ({
|
||||
loadPluginManifestRegistry,
|
||||
}));
|
||||
|
||||
import { resolveRuntimeSyntheticAuthProviderRefs } from "./synthetic-auth.runtime.js";
|
||||
|
||||
describe("synthetic auth runtime refs", () => {
|
||||
beforeEach(() => {
|
||||
getPluginRegistryState.mockReset();
|
||||
loadPluginManifestRegistry.mockReset().mockReturnValue({ plugins: [] });
|
||||
});
|
||||
|
||||
it("uses manifest-owned synthetic auth refs before the runtime registry exists", () => {
|
||||
loadPluginManifestRegistry.mockReturnValue({
|
||||
plugins: [
|
||||
{ syntheticAuthRefs: [" local-provider ", "local-provider", "local-cli"] },
|
||||
{ syntheticAuthRefs: ["remote-provider"] },
|
||||
{ syntheticAuthRefs: [] },
|
||||
],
|
||||
});
|
||||
|
||||
expect(resolveRuntimeSyntheticAuthProviderRefs()).toEqual([
|
||||
"local-provider",
|
||||
"local-cli",
|
||||
"remote-provider",
|
||||
]);
|
||||
expect(loadPluginManifestRegistry).toHaveBeenCalledWith({ cache: true });
|
||||
});
|
||||
|
||||
it("prefers the active runtime registry when plugins are already loaded", () => {
|
||||
getPluginRegistryState.mockReturnValue({
|
||||
activeRegistry: {
|
||||
providers: [
|
||||
{
|
||||
provider: {
|
||||
id: "runtime-provider",
|
||||
resolveSyntheticAuth: () => undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
provider: {
|
||||
id: "plain-provider",
|
||||
},
|
||||
},
|
||||
],
|
||||
cliBackends: [
|
||||
{
|
||||
backend: {
|
||||
id: "runtime-cli",
|
||||
resolveSyntheticAuth: () => undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolveRuntimeSyntheticAuthProviderRefs()).toEqual(["runtime-provider", "runtime-cli"]);
|
||||
expect(loadPluginManifestRegistry).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { normalizeProviderId } from "../agents/provider-id.js";
|
||||
import { loadPluginManifestRegistry } from "./manifest-registry.js";
|
||||
import { getPluginRegistryState } from "./runtime-state.js";
|
||||
const BUNDLED_SYNTHETIC_AUTH_PROVIDER_REFS = ["claude-cli", "ollama", "xai"] as const;
|
||||
|
||||
function uniqueProviderRefs(values: readonly string[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
@@ -17,6 +17,14 @@ function uniqueProviderRefs(values: readonly string[]): string[] {
|
||||
return next;
|
||||
}
|
||||
|
||||
function resolveManifestSyntheticAuthProviderRefs(): string[] {
|
||||
return uniqueProviderRefs(
|
||||
loadPluginManifestRegistry({ cache: true }).plugins.flatMap(
|
||||
(plugin) => plugin.syntheticAuthRefs ?? [],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveRuntimeSyntheticAuthProviderRefs(): string[] {
|
||||
const registry = getPluginRegistryState()?.activeRegistry;
|
||||
if (registry) {
|
||||
@@ -37,5 +45,5 @@ export function resolveRuntimeSyntheticAuthProviderRefs(): string[] {
|
||||
.map((entry) => entry.backend.id),
|
||||
]);
|
||||
}
|
||||
return uniqueProviderRefs(BUNDLED_SYNTHETIC_AUTH_PROVIDER_REFS);
|
||||
return resolveManifestSyntheticAuthProviderRefs();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user