mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-20 21:23:23 +00:00
feat(plugins): support provider auth aliases
This commit is contained in:
@@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Changes
|
||||
|
||||
- iOS: pin release versioning to an explicit CalVer in `apps/ios/version.json`, keep TestFlight iteration on the same short version until maintainers intentionally promote the next gateway version, and add the documented `pnpm ios:version:pin -- --from-gateway` workflow for release trains. (#63001) Thanks @ngutman.
|
||||
- Plugins/provider-auth: let provider manifests declare `providerAuthAliases` so provider variants can share env vars, auth profiles, config-backed auth, and API-key onboarding choices without core-specific wiring.
|
||||
|
||||
### Fixes
|
||||
|
||||
|
||||
@@ -23,10 +23,11 @@ For model selection rules, see [/concepts/models](/concepts/models).
|
||||
- Provider plugins can inject model catalogs via `registerProvider({ catalog })`;
|
||||
OpenClaw merges that output into `models.providers` before writing
|
||||
`models.json`.
|
||||
- Provider manifests can declare `providerAuthEnvVars` so generic env-based
|
||||
auth probes do not need to load plugin runtime. The remaining core env-var
|
||||
map is now just for non-plugin/core providers and a few generic-precedence
|
||||
cases such as Anthropic API-key-first onboarding.
|
||||
- Provider manifests can declare `providerAuthEnvVars` and
|
||||
`providerAuthAliases` so generic env-based auth probes and provider variants
|
||||
do not need to load plugin runtime. The remaining core env-var map is now
|
||||
just for non-plugin/core providers and a few generic-precedence cases such
|
||||
as Anthropic API-key-first onboarding.
|
||||
- Provider plugins can also own provider runtime behavior via
|
||||
`normalizeModelId`, `normalizeTransport`, `normalizeConfig`,
|
||||
`applyNativeStreamingUsageCompat`, `resolveConfigApiKey`,
|
||||
|
||||
@@ -610,9 +610,10 @@ conversation, and it runs after core approval handling finishes.
|
||||
Provider plugins now have two layers:
|
||||
|
||||
- manifest metadata: `providerAuthEnvVars` for cheap provider env-auth lookup
|
||||
before runtime load, `channelEnvVars` for cheap channel env/setup lookup
|
||||
before runtime load, plus `providerAuthChoices` for cheap onboarding/auth-choice
|
||||
labels and CLI flag metadata before runtime load
|
||||
before runtime load, `providerAuthAliases` for provider variants that share
|
||||
auth, `channelEnvVars` for cheap channel env/setup lookup before runtime
|
||||
load, plus `providerAuthChoices` for cheap onboarding/auth-choice labels and
|
||||
CLI flag metadata before runtime load
|
||||
- config-time hooks: `catalog` / legacy `discovery` plus `applyConfigDefaults`
|
||||
- runtime hooks: `normalizeModelId`, `normalizeTransport`,
|
||||
`normalizeConfig`,
|
||||
@@ -640,8 +641,10 @@ needing a whole custom inference transport.
|
||||
|
||||
Use manifest `providerAuthEnvVars` when the provider has env-based credentials
|
||||
that generic auth/status/model-picker paths should see without loading plugin
|
||||
runtime. Use manifest `providerAuthChoices` when onboarding/auth-choice CLI
|
||||
surfaces should know the provider's choice id, group labels, and simple
|
||||
runtime. Use manifest `providerAuthAliases` when one provider id should reuse
|
||||
another provider id's env vars, auth profiles, config-backed auth, and API-key
|
||||
onboarding choice. Use manifest `providerAuthChoices` when onboarding/auth-choice
|
||||
CLI surfaces should know the provider's choice id, group labels, and simple
|
||||
one-flag auth wiring without loading provider runtime. Keep provider runtime
|
||||
`envVars` for operator-facing hints such as onboarding labels or OAuth
|
||||
client-id/client-secret setup vars.
|
||||
|
||||
@@ -93,6 +93,9 @@ Those belong in your plugin code and `package.json`.
|
||||
"providerAuthEnvVars": {
|
||||
"openrouter": ["OPENROUTER_API_KEY"]
|
||||
},
|
||||
"providerAuthAliases": {
|
||||
"openrouter-coding": "openrouter"
|
||||
},
|
||||
"channelEnvVars": {
|
||||
"openrouter-chatops": ["OPENROUTER_CHATOPS_TOKEN"]
|
||||
},
|
||||
@@ -145,6 +148,7 @@ Those belong in your plugin code and `package.json`.
|
||||
| `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. |
|
||||
| `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. |
|
||||
| `channelEnvVars` | No | `Record<string, string[]>` | Cheap channel env metadata that OpenClaw can inspect without loading plugin code. Use this for env-driven channel setup or auth surfaces that generic startup/config helpers should see. |
|
||||
| `providerAuthChoices` | No | `object[]` | Cheap auth-choice metadata for onboarding pickers, preferred-provider resolution, and simple CLI flag wiring. |
|
||||
| `contracts` | No | `object` | Static bundled capability snapshot for speech, realtime transcription, realtime voice, media-understanding, image-generation, music-generation, video-generation, web-fetch, web search, and tool ownership. |
|
||||
@@ -440,6 +444,9 @@ See [Configuration reference](/gateway/configuration) for the full `plugins.*` s
|
||||
- `providerAuthEnvVars` is the cheap metadata path for auth probes, env-marker
|
||||
validation, and similar provider-auth surfaces that should not boot plugin
|
||||
runtime just to inspect env names.
|
||||
- `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.
|
||||
- `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.
|
||||
|
||||
@@ -58,6 +58,9 @@ API key auth, and dynamic model resolution.
|
||||
"providerAuthEnvVars": {
|
||||
"acme-ai": ["ACME_AI_API_KEY"]
|
||||
},
|
||||
"providerAuthAliases": {
|
||||
"acme-ai-coding": "acme-ai"
|
||||
},
|
||||
"providerAuthChoices": [
|
||||
{
|
||||
"provider": "acme-ai",
|
||||
@@ -80,9 +83,10 @@ API key auth, and dynamic model resolution.
|
||||
</CodeGroup>
|
||||
|
||||
The manifest declares `providerAuthEnvVars` so OpenClaw can detect
|
||||
credentials without loading your plugin runtime. `modelSupport` is optional
|
||||
and lets OpenClaw auto-load your provider plugin from shorthand model ids
|
||||
like `acme-large` before runtime hooks exist. If you publish the
|
||||
credentials without loading your plugin runtime. Add `providerAuthAliases`
|
||||
when a provider variant should reuse another provider id's auth. `modelSupport`
|
||||
is optional and lets OpenClaw auto-load your provider plugin from shorthand
|
||||
model ids like `acme-large` before runtime hooks exist. If you publish the
|
||||
provider on ClawHub, those `openclaw.compat` and `openclaw.build` fields
|
||||
are required in `package.json`.
|
||||
|
||||
@@ -707,7 +711,7 @@ Do not use the legacy skill-only publish alias here; plugin packages should use
|
||||
```
|
||||
<bundled-plugin-root>/acme-ai/
|
||||
├── package.json # openclaw.providers metadata
|
||||
├── openclaw.plugin.json # Manifest with providerAuthEnvVars
|
||||
├── openclaw.plugin.json # Manifest with provider auth metadata
|
||||
├── index.ts # definePluginEntry + registerProvider
|
||||
└── src/
|
||||
├── provider.test.ts # Tests
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { registerSingleProviderPlugin } from "../../test/helpers/plugins/plugin-registration.js";
|
||||
import plugin from "./index.js";
|
||||
@@ -32,4 +34,14 @@ describe("byteplus plugin", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("declares its coding provider auth alias in the manifest", () => {
|
||||
const pluginJson = JSON.parse(
|
||||
readFileSync(resolve(import.meta.dirname, "openclaw.plugin.json"), "utf-8"),
|
||||
);
|
||||
|
||||
expect(pluginJson.providerAuthAliases).toEqual({
|
||||
"byteplus-plan": "byteplus",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"providerAuthEnvVars": {
|
||||
"byteplus": ["BYTEPLUS_API_KEY"]
|
||||
},
|
||||
"providerAuthAliases": {
|
||||
"byteplus-plan": "byteplus"
|
||||
},
|
||||
"providerAuthChoices": [
|
||||
{
|
||||
"provider": "byteplus",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { registerSingleProviderPlugin } from "../../test/helpers/plugins/plugin-registration.js";
|
||||
import plugin from "./index.js";
|
||||
@@ -32,4 +34,14 @@ describe("volcengine plugin", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("declares its coding provider auth alias in the manifest", () => {
|
||||
const pluginJson = JSON.parse(
|
||||
readFileSync(resolve(import.meta.dirname, "openclaw.plugin.json"), "utf-8"),
|
||||
);
|
||||
|
||||
expect(pluginJson.providerAuthAliases).toEqual({
|
||||
"volcengine-plan": "volcengine",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"providerAuthEnvVars": {
|
||||
"volcengine": ["VOLCANO_ENGINE_API_KEY"]
|
||||
},
|
||||
"providerAuthAliases": {
|
||||
"volcengine-plan": "volcengine"
|
||||
},
|
||||
"providerAuthChoices": [
|
||||
{
|
||||
"provider": "volcengine",
|
||||
|
||||
@@ -1,15 +1,40 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { resolveAuthProfileOrder } from "./order.js";
|
||||
import { markAuthProfileGood } from "./profiles.js";
|
||||
import { saveAuthProfileStore } from "./store.js";
|
||||
import type { AuthProfileStore } from "./types.js";
|
||||
|
||||
const loadPluginManifestRegistry = vi.hoisted(() =>
|
||||
vi.fn(() => ({
|
||||
plugins: [
|
||||
{
|
||||
id: "fixture-provider",
|
||||
providerAuthAliases: { "fixture-provider-plan": "fixture-provider" },
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
})),
|
||||
);
|
||||
|
||||
vi.mock("../../plugins/manifest-registry.js", () => ({
|
||||
loadPluginManifestRegistry,
|
||||
}));
|
||||
|
||||
describe("resolveAuthProfileOrder", () => {
|
||||
it("accepts base-provider credentials for volcengine-plan auth lookup", () => {
|
||||
beforeEach(() => {
|
||||
loadPluginManifestRegistry.mockClear();
|
||||
});
|
||||
|
||||
it("accepts aliased provider credentials from manifest metadata", () => {
|
||||
const store: AuthProfileStore = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"volcengine:default": {
|
||||
"fixture-provider:default": {
|
||||
type: "api_key",
|
||||
provider: "volcengine",
|
||||
provider: "fixture-provider",
|
||||
key: "sk-test",
|
||||
},
|
||||
},
|
||||
@@ -17,9 +42,39 @@ describe("resolveAuthProfileOrder", () => {
|
||||
|
||||
const order = resolveAuthProfileOrder({
|
||||
store,
|
||||
provider: "volcengine-plan",
|
||||
provider: "fixture-provider-plan",
|
||||
});
|
||||
|
||||
expect(order).toEqual(["volcengine:default"]);
|
||||
expect(order).toEqual(["fixture-provider:default"]);
|
||||
});
|
||||
|
||||
it("marks aliased provider profiles good under the canonical auth provider", async () => {
|
||||
const agentDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-auth-profile-alias-"));
|
||||
try {
|
||||
const store: AuthProfileStore = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"fixture-provider:default": {
|
||||
type: "api_key",
|
||||
provider: "fixture-provider",
|
||||
key: "sk-test",
|
||||
},
|
||||
},
|
||||
};
|
||||
saveAuthProfileStore(store, agentDir);
|
||||
|
||||
await markAuthProfileGood({
|
||||
store,
|
||||
provider: "fixture-provider-plan",
|
||||
profileId: "fixture-provider:default",
|
||||
agentDir,
|
||||
});
|
||||
|
||||
expect(store.lastGood).toEqual({
|
||||
"fixture-provider": "fixture-provider:default",
|
||||
});
|
||||
} finally {
|
||||
await rm(agentDir, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import {
|
||||
findNormalizedProviderValue,
|
||||
normalizeProviderId,
|
||||
normalizeProviderIdForAuth,
|
||||
} from "../model-selection.js";
|
||||
import { findNormalizedProviderValue, normalizeProviderId } from "../model-selection.js";
|
||||
import { resolveProviderIdForAuth } from "../provider-auth-aliases.js";
|
||||
import {
|
||||
evaluateStoredCredentialEligibility,
|
||||
type AuthCredentialReasonCode,
|
||||
@@ -34,17 +31,19 @@ export function resolveAuthProfileEligibility(params: {
|
||||
profileId: string;
|
||||
now?: number;
|
||||
}): AuthProfileEligibility {
|
||||
const providerAuthKey = normalizeProviderIdForAuth(params.provider);
|
||||
const providerAuthKey = resolveProviderIdForAuth(params.provider, { config: params.cfg });
|
||||
const cred = params.store.profiles[params.profileId];
|
||||
if (!cred) {
|
||||
return { eligible: false, reasonCode: "profile_missing" };
|
||||
}
|
||||
if (normalizeProviderIdForAuth(cred.provider) !== providerAuthKey) {
|
||||
if (resolveProviderIdForAuth(cred.provider, { config: params.cfg }) !== providerAuthKey) {
|
||||
return { eligible: false, reasonCode: "provider_mismatch" };
|
||||
}
|
||||
const profileConfig = params.cfg?.auth?.profiles?.[params.profileId];
|
||||
if (profileConfig) {
|
||||
if (normalizeProviderIdForAuth(profileConfig.provider) !== providerAuthKey) {
|
||||
if (
|
||||
resolveProviderIdForAuth(profileConfig.provider, { config: params.cfg }) !== providerAuthKey
|
||||
) {
|
||||
return { eligible: false, reasonCode: "provider_mismatch" };
|
||||
}
|
||||
if (profileConfig.mode !== cred.type) {
|
||||
@@ -72,7 +71,7 @@ export function resolveAuthProfileOrder(params: {
|
||||
}): string[] {
|
||||
const { cfg, store, provider, preferredProfile } = params;
|
||||
const providerKey = normalizeProviderId(provider);
|
||||
const providerAuthKey = normalizeProviderIdForAuth(provider);
|
||||
const providerAuthKey = resolveProviderIdForAuth(provider, { config: cfg });
|
||||
const now = Date.now();
|
||||
|
||||
// Clear any cooldowns that have expired since the last check so profiles
|
||||
@@ -84,7 +83,10 @@ export function resolveAuthProfileOrder(params: {
|
||||
const explicitOrder = storedOrder ?? configuredOrder;
|
||||
const explicitProfiles = cfg?.auth?.profiles
|
||||
? Object.entries(cfg.auth.profiles)
|
||||
.filter(([, profile]) => normalizeProviderIdForAuth(profile.provider) === providerAuthKey)
|
||||
.filter(
|
||||
([, profile]) =>
|
||||
resolveProviderIdForAuth(profile.provider, { config: cfg }) === providerAuthKey,
|
||||
)
|
||||
.map(([profileId]) => profileId)
|
||||
: [];
|
||||
const baseOrder =
|
||||
@@ -98,7 +100,7 @@ export function resolveAuthProfileOrder(params: {
|
||||
resolveAuthProfileEligibility({
|
||||
cfg,
|
||||
store,
|
||||
provider: providerAuthKey,
|
||||
provider,
|
||||
profileId,
|
||||
now,
|
||||
}).eligible;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { normalizeStringEntries } from "../../shared/string-normalization.js";
|
||||
import { normalizeSecretInput } from "../../utils/normalize-secret-input.js";
|
||||
import { normalizeProviderId, normalizeProviderIdForAuth } from "../provider-id.js";
|
||||
import { resolveProviderIdForAuth } from "../provider-auth-aliases.js";
|
||||
import { normalizeProviderId } from "../provider-id.js";
|
||||
import {
|
||||
ensureAuthProfileStore,
|
||||
saveAuthProfileStore,
|
||||
@@ -78,9 +79,9 @@ export async function upsertAuthProfileWithLock(params: {
|
||||
}
|
||||
|
||||
export function listProfilesForProvider(store: AuthProfileStore, provider: string): string[] {
|
||||
const providerKey = normalizeProviderIdForAuth(provider);
|
||||
const providerKey = resolveProviderIdForAuth(provider);
|
||||
return Object.entries(store.profiles)
|
||||
.filter(([, cred]) => normalizeProviderIdForAuth(cred.provider) === providerKey)
|
||||
.filter(([, cred]) => resolveProviderIdForAuth(cred.provider) === providerKey)
|
||||
.map(([id]) => id);
|
||||
}
|
||||
|
||||
@@ -91,14 +92,15 @@ export async function markAuthProfileGood(params: {
|
||||
agentDir?: string;
|
||||
}): Promise<void> {
|
||||
const { store, provider, profileId, agentDir } = params;
|
||||
const providerKey = resolveProviderIdForAuth(provider);
|
||||
const updated = await updateAuthProfileStoreWithLock({
|
||||
agentDir,
|
||||
updater: (freshStore) => {
|
||||
const profile = freshStore.profiles[profileId];
|
||||
if (!profile || profile.provider !== provider) {
|
||||
if (!profile || resolveProviderIdForAuth(profile.provider) !== providerKey) {
|
||||
return false;
|
||||
}
|
||||
freshStore.lastGood = { ...freshStore.lastGood, [provider]: profileId };
|
||||
freshStore.lastGood = { ...freshStore.lastGood, [providerKey]: profileId };
|
||||
return true;
|
||||
},
|
||||
});
|
||||
@@ -107,9 +109,9 @@ export async function markAuthProfileGood(params: {
|
||||
return;
|
||||
}
|
||||
const profile = store.profiles[profileId];
|
||||
if (!profile || profile.provider !== provider) {
|
||||
if (!profile || resolveProviderIdForAuth(profile.provider) !== providerKey) {
|
||||
return;
|
||||
}
|
||||
store.lastGood = { ...store.lastGood, [provider]: profileId };
|
||||
store.lastGood = { ...store.lastGood, [providerKey]: profileId };
|
||||
saveAuthProfileStore(store, agentDir);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@ import {
|
||||
listKnownProviderAuthEnvVarNames,
|
||||
resolveProviderAuthEnvVarCandidates,
|
||||
} from "../secrets/provider-env-vars.js";
|
||||
import type { ProviderEnvVarLookupParams } from "../secrets/provider-env-vars.js";
|
||||
|
||||
export function resolveProviderEnvApiKeyCandidates(): Record<string, readonly string[]> {
|
||||
return resolveProviderAuthEnvVarCandidates();
|
||||
export function resolveProviderEnvApiKeyCandidates(
|
||||
params?: ProviderEnvVarLookupParams,
|
||||
): Record<string, readonly string[]> {
|
||||
return resolveProviderAuthEnvVarCandidates(params);
|
||||
}
|
||||
|
||||
export const PROVIDER_ENV_API_KEY_CANDIDATES = resolveProviderEnvApiKeyCandidates();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { resolvePluginSetupProvider } from "../plugins/setup-registry.js";
|
||||
import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js";
|
||||
import { resolveProviderEnvApiKeyCandidates } from "./model-auth-env-vars.js";
|
||||
import { GCP_VERTEX_CREDENTIALS_MARKER } from "./model-auth-markers.js";
|
||||
import { normalizeProviderIdForAuth } from "./provider-id.js";
|
||||
import { resolveProviderIdForAuth } from "./provider-auth-aliases.js";
|
||||
|
||||
export type EnvApiKeyResult = {
|
||||
apiKey: string;
|
||||
@@ -15,8 +15,8 @@ export function resolveEnvApiKey(
|
||||
provider: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): EnvApiKeyResult | null {
|
||||
const normalized = normalizeProviderIdForAuth(provider);
|
||||
const candidateMap = resolveProviderEnvApiKeyCandidates();
|
||||
const normalized = resolveProviderIdForAuth(provider, { env });
|
||||
const candidateMap = resolveProviderEnvApiKeyCandidates({ env });
|
||||
const applied = new Set(getShellEnvAppliedKeys());
|
||||
const pick = (envVar: string): EnvApiKeyResult | null => {
|
||||
const value = normalizeOptionalSecretInput(env[envVar]);
|
||||
|
||||
@@ -813,19 +813,6 @@ describe("getApiKeyForModel", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("resolveEnvApiKey('volcengine-plan') uses volcengine auth candidates", async () => {
|
||||
await withEnvAsync(
|
||||
{
|
||||
VOLCANO_ENGINE_API_KEY: "volcengine-plan-key",
|
||||
},
|
||||
async () => {
|
||||
const resolved = resolveEnvApiKey("volcengine-plan");
|
||||
expect(resolved?.apiKey).toBe("volcengine-plan-key");
|
||||
expect(resolved?.source).toContain("VOLCANO_ENGINE_API_KEY");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("resolveEnvApiKey('anthropic-vertex') uses the provided env snapshot", async () => {
|
||||
const resolved = resolveEnvApiKey("anthropic-vertex", {
|
||||
GOOGLE_CLOUD_PROJECT_ID: "vertex-project",
|
||||
|
||||
@@ -127,9 +127,9 @@ describe("model-selection", () => {
|
||||
});
|
||||
|
||||
describe("normalizeProviderIdForAuth", () => {
|
||||
it("maps coding-plan variants to base provider for auth lookup", () => {
|
||||
expect(normalizeProviderIdForAuth("volcengine-plan")).toBe("volcengine");
|
||||
expect(normalizeProviderIdForAuth("byteplus-plan")).toBe("byteplus");
|
||||
it("only applies generic provider-id normalization before auth alias lookup", () => {
|
||||
expect(normalizeProviderIdForAuth("qwencloud")).toBe("qwen");
|
||||
expect(normalizeProviderIdForAuth("openai-codex")).toBe("openai-codex");
|
||||
expect(normalizeProviderIdForAuth("openai")).toBe("openai");
|
||||
});
|
||||
});
|
||||
|
||||
75
src/agents/models-config.providers.auth-aliases.test.ts
Normal file
75
src/agents/models-config.providers.auth-aliases.test.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createProviderAuthResolver } from "./models-config.providers.secrets.js";
|
||||
|
||||
const loadPluginManifestRegistry = vi.hoisted(() =>
|
||||
vi.fn(() => ({
|
||||
plugins: [
|
||||
{
|
||||
id: "fixture-provider",
|
||||
providerAuthEnvVars: {
|
||||
"fixture-provider": ["FIXTURE_PROVIDER_API_KEY"],
|
||||
},
|
||||
providerAuthAliases: {
|
||||
"fixture-provider-plan": "fixture-provider",
|
||||
},
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
})),
|
||||
);
|
||||
|
||||
vi.mock("../plugins/manifest-registry.js", () => ({
|
||||
loadPluginManifestRegistry,
|
||||
}));
|
||||
|
||||
describe("provider auth aliases", () => {
|
||||
beforeEach(() => {
|
||||
loadPluginManifestRegistry.mockClear();
|
||||
});
|
||||
|
||||
it("shares manifest env vars across aliased providers", () => {
|
||||
const resolveAuth = createProviderAuthResolver(
|
||||
{
|
||||
FIXTURE_PROVIDER_API_KEY: "test-key", // pragma: allowlist secret
|
||||
} as NodeJS.ProcessEnv,
|
||||
{ version: 1, profiles: {} },
|
||||
);
|
||||
|
||||
expect(resolveAuth("fixture-provider")).toMatchObject({
|
||||
apiKey: "FIXTURE_PROVIDER_API_KEY",
|
||||
mode: "api_key",
|
||||
source: "env",
|
||||
});
|
||||
expect(resolveAuth("fixture-provider-plan")).toMatchObject({
|
||||
apiKey: "FIXTURE_PROVIDER_API_KEY",
|
||||
mode: "api_key",
|
||||
source: "env",
|
||||
});
|
||||
});
|
||||
|
||||
it("reuses env keyRef markers from auth profiles for aliased providers", () => {
|
||||
const resolveAuth = createProviderAuthResolver({} as NodeJS.ProcessEnv, {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"fixture-provider:default": {
|
||||
type: "api_key",
|
||||
provider: "fixture-provider",
|
||||
keyRef: { source: "env", provider: "default", id: "FIXTURE_PROVIDER_API_KEY" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolveAuth("fixture-provider")).toMatchObject({
|
||||
apiKey: "FIXTURE_PROVIDER_API_KEY",
|
||||
mode: "api_key",
|
||||
source: "profile",
|
||||
profileId: "fixture-provider:default",
|
||||
});
|
||||
expect(resolveAuth("fixture-provider-plan")).toMatchObject({
|
||||
apiKey: "FIXTURE_PROVIDER_API_KEY",
|
||||
mode: "api_key",
|
||||
source: "profile",
|
||||
profileId: "fixture-provider:default",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
resolveNonEnvSecretRefHeaderValueMarker,
|
||||
} from "./model-auth-markers.js";
|
||||
import { resolveAwsSdkEnvVarName } from "./model-auth-runtime-shared.js";
|
||||
import { normalizeProviderIdForAuth } from "./provider-id.js";
|
||||
import { resolveProviderIdForAuth } from "./provider-auth-aliases.js";
|
||||
|
||||
type ModelsConfig = NonNullable<OpenClawConfig["models"]>;
|
||||
export type ProviderConfig = NonNullable<ModelsConfig["providers"]>[string];
|
||||
@@ -325,7 +325,7 @@ export function createProviderApiKeyResolver(
|
||||
config?: OpenClawConfig,
|
||||
): ProviderApiKeyResolver {
|
||||
return (provider: string): { apiKey: string | undefined; discoveryApiKey?: string } => {
|
||||
const authProvider = normalizeProviderIdForAuth(provider);
|
||||
const authProvider = resolveProviderIdForAuth(provider, { config, env });
|
||||
const envVar = resolveEnvApiKeyVarName(authProvider, env);
|
||||
if (envVar) {
|
||||
return {
|
||||
@@ -361,7 +361,7 @@ export function createProviderAuthResolver(
|
||||
config?: OpenClawConfig,
|
||||
): ProviderAuthResolver {
|
||||
return (provider: string, options?: { oauthMarker?: string }) => {
|
||||
const authProvider = normalizeProviderIdForAuth(provider);
|
||||
const authProvider = resolveProviderIdForAuth(provider, { config, env });
|
||||
const ids = listProfilesForProvider(authStore, authProvider);
|
||||
let oauthCandidate:
|
||||
| {
|
||||
@@ -446,7 +446,7 @@ function resolveConfigBackedProviderAuth(params: { provider: string; config?: Op
|
||||
// Providers own any provider-specific fallback auth logic via
|
||||
// resolveSyntheticAuth(...). Discovery/bootstrap callers may consume
|
||||
// non-secret markers from source config, but must never persist plaintext.
|
||||
const authProvider = normalizeProviderIdForAuth(params.provider);
|
||||
const authProvider = resolveProviderIdForAuth(params.provider, { config: params.config });
|
||||
const synthetic = resolveProviderSyntheticAuthWithPlugin({
|
||||
provider: authProvider,
|
||||
config: params.config,
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
async function resetProviderRuntimeState() {
|
||||
const [
|
||||
{ clearPluginManifestRegistryCache },
|
||||
{ resetProviderRuntimeHookCacheForTest },
|
||||
{ resetPluginLoaderTestStateForTest },
|
||||
] = await Promise.all([
|
||||
import("../plugins/manifest-registry.js"),
|
||||
import("../plugins/provider-runtime.js"),
|
||||
import("../plugins/loader.test-fixtures.js"),
|
||||
]);
|
||||
resetPluginLoaderTestStateForTest();
|
||||
clearPluginManifestRegistryCache();
|
||||
resetProviderRuntimeHookCacheForTest();
|
||||
}
|
||||
|
||||
let createProviderAuthResolver: typeof import("./models-config.providers.secrets.js").createProviderAuthResolver;
|
||||
|
||||
async function loadSecretsModule() {
|
||||
vi.doUnmock("../plugins/manifest-registry.js");
|
||||
vi.doUnmock("../plugins/provider-runtime.js");
|
||||
vi.doUnmock("../secrets/provider-env-vars.js");
|
||||
vi.resetModules();
|
||||
await resetProviderRuntimeState();
|
||||
({ createProviderAuthResolver } = await import("./models-config.providers.secrets.js"));
|
||||
}
|
||||
|
||||
beforeAll(loadSecretsModule);
|
||||
|
||||
describe("Volcengine and BytePlus providers", () => {
|
||||
it("shares VOLCANO_ENGINE_API_KEY across volcengine auth aliases", () => {
|
||||
const resolveAuth = createProviderAuthResolver(
|
||||
{
|
||||
VOLCANO_ENGINE_API_KEY: "test-key", // pragma: allowlist secret
|
||||
} as NodeJS.ProcessEnv,
|
||||
{ version: 1, profiles: {} },
|
||||
);
|
||||
|
||||
expect(resolveAuth("volcengine")).toMatchObject({
|
||||
apiKey: "VOLCANO_ENGINE_API_KEY",
|
||||
mode: "api_key",
|
||||
source: "env",
|
||||
});
|
||||
expect(resolveAuth("volcengine-plan")).toMatchObject({
|
||||
apiKey: "VOLCANO_ENGINE_API_KEY",
|
||||
mode: "api_key",
|
||||
source: "env",
|
||||
});
|
||||
});
|
||||
|
||||
it("shares BYTEPLUS_API_KEY across byteplus auth aliases", () => {
|
||||
const resolveAuth = createProviderAuthResolver(
|
||||
{
|
||||
BYTEPLUS_API_KEY: "test-key", // pragma: allowlist secret
|
||||
} as NodeJS.ProcessEnv,
|
||||
{ version: 1, profiles: {} },
|
||||
);
|
||||
|
||||
expect(resolveAuth("byteplus")).toMatchObject({
|
||||
apiKey: "BYTEPLUS_API_KEY",
|
||||
mode: "api_key",
|
||||
source: "env",
|
||||
});
|
||||
expect(resolveAuth("byteplus-plan")).toMatchObject({
|
||||
apiKey: "BYTEPLUS_API_KEY",
|
||||
mode: "api_key",
|
||||
source: "env",
|
||||
});
|
||||
});
|
||||
|
||||
it("reuses env keyRef markers from auth profiles for paired providers", () => {
|
||||
const resolveAuth = createProviderAuthResolver({} as NodeJS.ProcessEnv, {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"volcengine:default": {
|
||||
type: "api_key",
|
||||
provider: "volcengine",
|
||||
keyRef: { source: "env", provider: "default", id: "VOLCANO_ENGINE_API_KEY" },
|
||||
},
|
||||
"byteplus:default": {
|
||||
type: "api_key",
|
||||
provider: "byteplus",
|
||||
keyRef: { source: "env", provider: "default", id: "BYTEPLUS_API_KEY" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolveAuth("volcengine")).toMatchObject({
|
||||
apiKey: "VOLCANO_ENGINE_API_KEY",
|
||||
mode: "api_key",
|
||||
source: "profile",
|
||||
profileId: "volcengine:default",
|
||||
});
|
||||
expect(resolveAuth("volcengine-plan")).toMatchObject({
|
||||
apiKey: "VOLCANO_ENGINE_API_KEY",
|
||||
mode: "api_key",
|
||||
source: "profile",
|
||||
profileId: "volcengine:default",
|
||||
});
|
||||
expect(resolveAuth("byteplus")).toMatchObject({
|
||||
apiKey: "BYTEPLUS_API_KEY",
|
||||
mode: "api_key",
|
||||
source: "profile",
|
||||
profileId: "byteplus:default",
|
||||
});
|
||||
expect(resolveAuth("byteplus-plan")).toMatchObject({
|
||||
apiKey: "BYTEPLUS_API_KEY",
|
||||
mode: "api_key",
|
||||
source: "profile",
|
||||
profileId: "byteplus:default",
|
||||
});
|
||||
});
|
||||
});
|
||||
43
src/agents/provider-auth-aliases.ts
Normal file
43
src/agents/provider-auth-aliases.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
import { normalizeProviderId } from "./provider-id.js";
|
||||
|
||||
export type ProviderAuthAliasLookupParams = {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
};
|
||||
|
||||
export function resolveProviderAuthAliasMap(
|
||||
params?: ProviderAuthAliasLookupParams,
|
||||
): Record<string, string> {
|
||||
const registry = loadPluginManifestRegistry({
|
||||
config: params?.config,
|
||||
workspaceDir: params?.workspaceDir,
|
||||
env: params?.env,
|
||||
});
|
||||
const aliases: Record<string, string> = Object.create(null) as Record<string, string>;
|
||||
for (const plugin of registry.plugins) {
|
||||
for (const [alias, target] of Object.entries(plugin.providerAuthAliases ?? {}).toSorted(
|
||||
([left], [right]) => left.localeCompare(right),
|
||||
)) {
|
||||
const normalizedAlias = normalizeProviderId(alias);
|
||||
const normalizedTarget = normalizeProviderId(target);
|
||||
if (normalizedAlias && normalizedTarget) {
|
||||
aliases[normalizedAlias] = normalizedTarget;
|
||||
}
|
||||
}
|
||||
}
|
||||
return aliases;
|
||||
}
|
||||
|
||||
export function resolveProviderIdForAuth(
|
||||
provider: string,
|
||||
params?: ProviderAuthAliasLookupParams,
|
||||
): string {
|
||||
const normalized = normalizeProviderId(provider);
|
||||
if (!normalized) {
|
||||
return normalized;
|
||||
}
|
||||
return resolveProviderAuthAliasMap(params)[normalized] ?? normalized;
|
||||
}
|
||||
@@ -27,16 +27,9 @@ export function normalizeProviderId(provider: string): string {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/** Normalize provider ID for auth lookup. Coding-plan variants share auth with base. */
|
||||
/** Normalize provider ID before manifest-owned auth alias lookup. */
|
||||
export function normalizeProviderIdForAuth(provider: string): string {
|
||||
const normalized = normalizeProviderId(provider);
|
||||
if (normalized === "volcengine-plan") {
|
||||
return "volcengine";
|
||||
}
|
||||
if (normalized === "byteplus-plan") {
|
||||
return "byteplus";
|
||||
}
|
||||
return normalized;
|
||||
return normalizeProviderId(provider);
|
||||
}
|
||||
|
||||
export function findNormalizedProviderValue<T>(
|
||||
|
||||
@@ -2,9 +2,9 @@ import { ensureAuthProfileStore } from "../../agents/auth-profiles.js";
|
||||
import {
|
||||
type ModelAliasIndex,
|
||||
modelKey,
|
||||
normalizeProviderIdForAuth,
|
||||
resolveModelRefFromString,
|
||||
} from "../../agents/model-selection.js";
|
||||
import { resolveProviderIdForAuth } from "../../agents/provider-auth-aliases.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { resolveProfileOverride } from "./directive-handling.auth-profile.js";
|
||||
import type { InlineDirectives } from "./directive-handling.parse.js";
|
||||
@@ -84,8 +84,12 @@ export function resolveModelSelectionFromDirective(params: {
|
||||
: null;
|
||||
const useStoredNumericProfile =
|
||||
Boolean(storedNumericProfileSelection?.selection) &&
|
||||
normalizeProviderIdForAuth(storedNumericProfileSelection?.selection?.provider ?? "") ===
|
||||
normalizeProviderIdForAuth(storedNumericProfile?.profileProvider ?? "");
|
||||
resolveProviderIdForAuth(storedNumericProfileSelection?.selection?.provider ?? "", {
|
||||
config: params.cfg,
|
||||
}) ===
|
||||
resolveProviderIdForAuth(storedNumericProfile?.profileProvider ?? "", {
|
||||
config: params.cfg,
|
||||
});
|
||||
const modelRaw =
|
||||
useStoredNumericProfile && storedNumericProfile ? storedNumericProfile.modelRaw : raw;
|
||||
let modelSelection: ModelDirectiveSelection | undefined;
|
||||
|
||||
@@ -59,6 +59,7 @@ describe("resolvePluginConfigContractsById", () => {
|
||||
modelSupport: undefined,
|
||||
cliBackends: [],
|
||||
channelEnvVars: undefined,
|
||||
providerAuthAliases: undefined,
|
||||
providerAuthChoices: undefined,
|
||||
skills: [],
|
||||
settingsFiles: undefined,
|
||||
|
||||
@@ -382,6 +382,9 @@ describe("loadPluginManifestRegistry", () => {
|
||||
providerAuthEnvVars: {
|
||||
openai: ["OPENAI_API_KEY"],
|
||||
},
|
||||
providerAuthAliases: {
|
||||
"openai-codex": "openai",
|
||||
},
|
||||
providerAuthChoices: [
|
||||
{
|
||||
provider: "openai",
|
||||
@@ -404,6 +407,9 @@ describe("loadPluginManifestRegistry", () => {
|
||||
expect(registry.plugins[0]?.providerAuthEnvVars).toEqual({
|
||||
openai: ["OPENAI_API_KEY"],
|
||||
});
|
||||
expect(registry.plugins[0]?.providerAuthAliases).toEqual({
|
||||
"openai-codex": "openai",
|
||||
});
|
||||
expect(registry.plugins[0]?.enabledByDefault).toBe(true);
|
||||
expect(registry.plugins[0]?.providerAuthChoices).toEqual([
|
||||
{
|
||||
|
||||
@@ -78,6 +78,7 @@ export type PluginManifestRecord = {
|
||||
modelSupport?: PluginManifestModelSupport;
|
||||
cliBackends: string[];
|
||||
providerAuthEnvVars?: Record<string, string[]>;
|
||||
providerAuthAliases?: Record<string, string>;
|
||||
channelEnvVars?: Record<string, string[]>;
|
||||
providerAuthChoices?: PluginManifest["providerAuthChoices"];
|
||||
skills: string[];
|
||||
@@ -311,6 +312,7 @@ function buildRecord(params: {
|
||||
modelSupport: params.manifest.modelSupport,
|
||||
cliBackends: params.manifest.cliBackends ?? [],
|
||||
providerAuthEnvVars: params.manifest.providerAuthEnvVars,
|
||||
providerAuthAliases: params.manifest.providerAuthAliases,
|
||||
channelEnvVars: params.manifest.channelEnvVars,
|
||||
providerAuthChoices: params.manifest.providerAuthChoices,
|
||||
skills: params.manifest.skills ?? [],
|
||||
|
||||
@@ -105,6 +105,8 @@ export type PluginManifest = {
|
||||
cliBackends?: string[];
|
||||
/** Cheap provider-auth env lookup without booting plugin runtime. */
|
||||
providerAuthEnvVars?: Record<string, string[]>;
|
||||
/** Provider ids that should reuse another provider id for auth lookup. */
|
||||
providerAuthAliases?: Record<string, string>;
|
||||
/** Cheap channel env lookup without booting plugin runtime. */
|
||||
channelEnvVars?: Record<string, string[]>;
|
||||
/**
|
||||
@@ -198,6 +200,22 @@ function normalizeStringListRecord(value: unknown): Record<string, string[]> | u
|
||||
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
function normalizeStringRecord(value: unknown): Record<string, string> | undefined {
|
||||
if (!isRecord(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized: Record<string, string> = {};
|
||||
for (const [rawKey, rawValue] of Object.entries(value)) {
|
||||
const key = normalizeOptionalString(rawKey) ?? "";
|
||||
const value = normalizeOptionalString(rawValue) ?? "";
|
||||
if (!key || !value) {
|
||||
continue;
|
||||
}
|
||||
normalized[key] = value;
|
||||
}
|
||||
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
function normalizeManifestContracts(value: unknown): PluginManifestContracts | undefined {
|
||||
if (!isRecord(value)) {
|
||||
return undefined;
|
||||
@@ -516,6 +534,7 @@ export function loadPluginManifest(
|
||||
const modelSupport = normalizeManifestModelSupport(raw.modelSupport);
|
||||
const cliBackends = normalizeTrimmedStringList(raw.cliBackends);
|
||||
const providerAuthEnvVars = normalizeStringListRecord(raw.providerAuthEnvVars);
|
||||
const providerAuthAliases = normalizeStringRecord(raw.providerAuthAliases);
|
||||
const channelEnvVars = normalizeStringListRecord(raw.channelEnvVars);
|
||||
const providerAuthChoices = normalizeProviderAuthChoices(raw.providerAuthChoices);
|
||||
const skills = normalizeTrimmedStringList(raw.skills);
|
||||
@@ -544,6 +563,7 @@ export function loadPluginManifest(
|
||||
modelSupport,
|
||||
cliBackends,
|
||||
providerAuthEnvVars,
|
||||
providerAuthAliases,
|
||||
channelEnvVars,
|
||||
providerAuthChoices,
|
||||
skills,
|
||||
|
||||
@@ -8,6 +8,7 @@ vi.mock("./manifest-registry.js", () => ({
|
||||
|
||||
import {
|
||||
resolveManifestDeprecatedProviderAuthChoice,
|
||||
resolveManifestProviderApiKeyChoice,
|
||||
resolveManifestProviderAuthChoice,
|
||||
resolveManifestProviderAuthChoices,
|
||||
resolveManifestProviderOnboardAuthFlags,
|
||||
@@ -338,4 +339,33 @@ describe("provider auth choice manifest helpers", () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("resolves api-key choices through manifest-owned provider auth aliases", () => {
|
||||
setManifestPlugins([
|
||||
{
|
||||
id: "fixture-provider",
|
||||
origin: "bundled",
|
||||
providerAuthAliases: {
|
||||
"fixture-provider-plan": "fixture-provider",
|
||||
},
|
||||
providerAuthChoices: [
|
||||
{
|
||||
provider: "fixture-provider",
|
||||
method: "api-key",
|
||||
choiceId: "fixture-provider-api-key",
|
||||
choiceLabel: "Fixture Provider API key",
|
||||
optionKey: "fixtureProviderApiKey",
|
||||
cliFlag: "--fixture-provider-api-key",
|
||||
cliOption: "--fixture-provider-api-key <key>",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(
|
||||
resolveManifestProviderApiKeyChoice({
|
||||
providerId: "fixture-provider-plan",
|
||||
})?.choiceId,
|
||||
).toBe("fixture-provider-api-key");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { normalizeProviderIdForAuth } from "../agents/model-selection.js";
|
||||
import { resolveProviderIdForAuth } from "../agents/provider-auth-aliases.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { normalizePluginsConfig, resolveEffectiveEnableState } from "./config-state.js";
|
||||
import { loadPluginManifestRegistry, type PluginManifestRecord } from "./manifest-registry.js";
|
||||
@@ -203,7 +203,7 @@ export function resolveManifestProviderApiKeyChoice(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
includeUntrustedWorkspacePlugins?: boolean;
|
||||
}): ProviderAuthChoiceMetadata | undefined {
|
||||
const normalizedProviderId = normalizeProviderIdForAuth(params.providerId);
|
||||
const normalizedProviderId = resolveProviderIdForAuth(params.providerId, params);
|
||||
if (!normalizedProviderId) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -211,7 +211,7 @@ export function resolveManifestProviderApiKeyChoice(params: {
|
||||
if (!choice.optionKey) {
|
||||
return false;
|
||||
}
|
||||
return normalizeProviderIdForAuth(choice.providerId) === normalizedProviderId;
|
||||
return resolveProviderIdForAuth(choice.providerId, params) === normalizedProviderId;
|
||||
});
|
||||
const preferred = pickPreferredManifestAuthChoice(candidates);
|
||||
return preferred ? stripChoiceOrigin(preferred) : undefined;
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { OAuthCredentials } from "@mariozechner/pi-ai";
|
||||
import { resolveOpenClawAgentDir } from "../agents/agent-paths.js";
|
||||
import { buildAuthProfileId } from "../agents/auth-profiles/identity.js";
|
||||
import { upsertAuthProfile } from "../agents/auth-profiles/profiles.js";
|
||||
import { normalizeProviderIdForAuth } from "../agents/provider-id.js";
|
||||
import { resolveProviderIdForAuth } from "../agents/provider-auth-aliases.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import {
|
||||
@@ -136,7 +136,7 @@ export function applyAuthProfileConfig(
|
||||
preferProfileFirst?: boolean;
|
||||
},
|
||||
): OpenClawConfig {
|
||||
const normalizedProvider = normalizeProviderIdForAuth(params.provider);
|
||||
const normalizedProvider = resolveProviderIdForAuth(params.provider, { config: cfg });
|
||||
const profiles = {
|
||||
...cfg.auth?.profiles,
|
||||
[params.profileId]: {
|
||||
@@ -148,13 +148,16 @@ export function applyAuthProfileConfig(
|
||||
};
|
||||
|
||||
const configuredProviderProfiles = Object.entries(cfg.auth?.profiles ?? {})
|
||||
.filter(([, profile]) => normalizeProviderIdForAuth(profile.provider) === normalizedProvider)
|
||||
.filter(
|
||||
([, profile]) =>
|
||||
resolveProviderIdForAuth(profile.provider, { config: cfg }) === normalizedProvider,
|
||||
)
|
||||
.map(([profileId, profile]) => ({ profileId, mode: profile.mode }));
|
||||
|
||||
// Maintain `auth.order` when it already exists. Additionally, if we detect
|
||||
// mixed auth modes for the same provider, keep the newly selected profile first.
|
||||
const matchingProviderOrderEntries = Object.entries(cfg.auth?.order ?? {}).filter(
|
||||
([providerId]) => normalizeProviderIdForAuth(providerId) === normalizedProvider,
|
||||
([providerId]) => resolveProviderIdForAuth(providerId, { config: cfg }) === normalizedProvider,
|
||||
);
|
||||
const existingProviderOrder =
|
||||
matchingProviderOrderEntries.length > 0
|
||||
@@ -184,7 +187,8 @@ export function applyAuthProfileConfig(
|
||||
matchingProviderOrderEntries.length > 0
|
||||
? Object.fromEntries(
|
||||
Object.entries(cfg.auth?.order ?? {}).filter(
|
||||
([providerId]) => normalizeProviderIdForAuth(providerId) !== normalizedProvider,
|
||||
([providerId]) =>
|
||||
resolveProviderIdForAuth(providerId, { config: cfg }) !== normalizedProvider,
|
||||
),
|
||||
)
|
||||
: cfg.auth?.order;
|
||||
|
||||
@@ -5,6 +5,7 @@ type MockManifestRegistry = {
|
||||
id: string;
|
||||
origin: string;
|
||||
providerAuthEnvVars?: Record<string, string[]>;
|
||||
providerAuthAliases?: Record<string, string>;
|
||||
}>;
|
||||
diagnostics: unknown[];
|
||||
};
|
||||
@@ -33,6 +34,9 @@ describe("provider env vars dynamic manifest metadata", () => {
|
||||
providerAuthEnvVars: {
|
||||
fireworks: ["FIREWORKS_ALT_API_KEY"],
|
||||
},
|
||||
providerAuthAliases: {
|
||||
"fireworks-plan": "fireworks",
|
||||
},
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
@@ -41,6 +45,7 @@ describe("provider env vars dynamic manifest metadata", () => {
|
||||
const mod = await import("./provider-env-vars.js");
|
||||
|
||||
expect(mod.getProviderEnvVars("fireworks")).toEqual(["FIREWORKS_ALT_API_KEY"]);
|
||||
expect(mod.getProviderEnvVars("fireworks-plan")).toEqual(["FIREWORKS_ALT_API_KEY"]);
|
||||
expect(mod.listKnownProviderAuthEnvVarNames()).toContain("FIREWORKS_ALT_API_KEY");
|
||||
expect(mod.listKnownSecretEnvVarNames()).toContain("FIREWORKS_ALT_API_KEY");
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { normalizeProviderId } from "../agents/provider-id.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
|
||||
@@ -12,7 +13,7 @@ const CORE_PROVIDER_SETUP_ENV_VAR_OVERRIDES = {
|
||||
"minimax-cn": ["MINIMAX_API_KEY"],
|
||||
} as const;
|
||||
|
||||
type ProviderEnvVarLookupParams = {
|
||||
export type ProviderEnvVarLookupParams = {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
@@ -58,6 +59,26 @@ function resolveManifestProviderAuthEnvVarCandidates(
|
||||
appendUniqueEnvVarCandidates(candidates, providerId, keys);
|
||||
}
|
||||
}
|
||||
const aliases: Record<string, string> = Object.create(null) as Record<string, string>;
|
||||
for (const plugin of registry.plugins) {
|
||||
for (const [alias, target] of Object.entries(plugin.providerAuthAliases ?? {}).toSorted(
|
||||
([left], [right]) => left.localeCompare(right),
|
||||
)) {
|
||||
const normalizedAlias = normalizeProviderId(alias);
|
||||
const normalizedTarget = normalizeProviderId(target);
|
||||
if (normalizedAlias && normalizedTarget) {
|
||||
aliases[normalizedAlias] = normalizedTarget;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [alias, target] of Object.entries(aliases).toSorted(([left], [right]) =>
|
||||
left.localeCompare(right),
|
||||
)) {
|
||||
const keys = candidates[target];
|
||||
if (keys) {
|
||||
appendUniqueEnvVarCandidates(candidates, alias, keys);
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user