mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:30:43 +00:00
feat: add model list auth index
This commit is contained in:
@@ -7,12 +7,18 @@ 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 { resolveProviderIdForAuth } from "./provider-auth-aliases.js";
|
||||
import { normalizeProviderIdForAuth } from "./provider-id.js";
|
||||
|
||||
export type EnvApiKeyResult = {
|
||||
apiKey: string;
|
||||
source: string;
|
||||
};
|
||||
|
||||
export type EnvApiKeyLookupOptions = {
|
||||
aliasMap?: Readonly<Record<string, string>>;
|
||||
candidateMap?: Readonly<Record<string, readonly string[]>>;
|
||||
};
|
||||
|
||||
function hasGoogleVertexAdcCredentials(env: NodeJS.ProcessEnv): boolean {
|
||||
const explicitCredentialsPath = normalizeOptionalSecretInput(env.GOOGLE_APPLICATION_CREDENTIALS);
|
||||
if (explicitCredentialsPath) {
|
||||
@@ -39,9 +45,13 @@ function resolveGoogleVertexEnvApiKey(env: NodeJS.ProcessEnv): string | undefine
|
||||
export function resolveEnvApiKey(
|
||||
provider: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
options: EnvApiKeyLookupOptions = {},
|
||||
): EnvApiKeyResult | null {
|
||||
const normalized = resolveProviderIdForAuth(provider, { env });
|
||||
const candidateMap = resolveProviderEnvApiKeyCandidates({ env });
|
||||
const normalizedProvider = normalizeProviderIdForAuth(provider);
|
||||
const normalized = options.aliasMap
|
||||
? (options.aliasMap[normalizedProvider] ?? normalizedProvider)
|
||||
: resolveProviderIdForAuth(provider, { env });
|
||||
const candidateMap = options.candidateMap ?? resolveProviderEnvApiKeyCandidates({ env });
|
||||
const applied = new Set(getShellEnvAppliedKeys());
|
||||
const pick = (envVar: string): EnvApiKeyResult | null => {
|
||||
const value = normalizeOptionalSecretInput(env[envVar]);
|
||||
|
||||
79
src/commands/models/list.auth-index.test.ts
Normal file
79
src/commands/models/list.auth-index.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { AuthProfileStore } from "../../agents/auth-profiles/types.js";
|
||||
import { createModelListAuthIndex } from "./list.auth-index.js";
|
||||
|
||||
vi.mock("../../plugins/installed-plugin-index-store.js", () => ({
|
||||
readPersistedInstalledPluginIndexSync: vi.fn(() => null),
|
||||
}));
|
||||
|
||||
const emptyStore: AuthProfileStore = {
|
||||
version: 1,
|
||||
profiles: {},
|
||||
};
|
||||
|
||||
describe("createModelListAuthIndex", () => {
|
||||
it("normalizes auth aliases from profiles", () => {
|
||||
const index = createModelListAuthIndex({
|
||||
cfg: {},
|
||||
authStore: {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"byteplus:default": {
|
||||
type: "api_key",
|
||||
provider: "byteplus",
|
||||
key: "sk-test",
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {},
|
||||
});
|
||||
|
||||
expect(index.hasProviderAuth("byteplus")).toBe(true);
|
||||
expect(index.hasProviderAuth("byteplus-plan")).toBe(true);
|
||||
});
|
||||
|
||||
it("records env-backed providers without resolving env candidates per row", () => {
|
||||
const index = createModelListAuthIndex({
|
||||
cfg: {},
|
||||
authStore: emptyStore,
|
||||
env: {
|
||||
MOONSHOT_API_KEY: "sk-test",
|
||||
},
|
||||
});
|
||||
|
||||
expect(index.hasProviderAuth("moonshot")).toBe(true);
|
||||
expect(index.hasProviderAuth("openai")).toBe(false);
|
||||
});
|
||||
|
||||
it("records configured provider API keys", () => {
|
||||
const index = createModelListAuthIndex({
|
||||
cfg: {
|
||||
models: {
|
||||
providers: {
|
||||
"custom-openai": {
|
||||
api: "openai-completions",
|
||||
apiKey: "sk-configured",
|
||||
baseUrl: "https://custom.example/v1",
|
||||
models: [{ id: "local-model" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
authStore: emptyStore,
|
||||
env: {},
|
||||
});
|
||||
|
||||
expect(index.hasProviderAuth("custom-openai")).toBe(true);
|
||||
});
|
||||
|
||||
it("uses injected synthetic auth refs without loading provider runtime", () => {
|
||||
const index = createModelListAuthIndex({
|
||||
cfg: {},
|
||||
authStore: emptyStore,
|
||||
env: {},
|
||||
syntheticAuthProviderRefs: ["codex"],
|
||||
});
|
||||
|
||||
expect(index.hasProviderAuth("codex")).toBe(true);
|
||||
});
|
||||
});
|
||||
83
src/commands/models/list.auth-index.ts
Normal file
83
src/commands/models/list.auth-index.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import type { AuthProfileStore } from "../../agents/auth-profiles/types.js";
|
||||
import { resolveProviderEnvApiKeyCandidates } from "../../agents/model-auth-env-vars.js";
|
||||
import { resolveEnvApiKey } from "../../agents/model-auth-env.js";
|
||||
import { resolveAwsSdkEnvVarName } from "../../agents/model-auth-runtime-shared.js";
|
||||
import { hasUsableCustomProviderApiKey } from "../../agents/model-auth.js";
|
||||
import { resolveProviderAuthAliasMap } from "../../agents/provider-auth-aliases.js";
|
||||
import { normalizeProviderIdForAuth } from "../../agents/provider-id.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { readPersistedInstalledPluginIndexSync } from "../../plugins/installed-plugin-index-store.js";
|
||||
|
||||
export type ModelListAuthIndex = {
|
||||
hasProviderAuth(provider: string): boolean;
|
||||
};
|
||||
|
||||
export type CreateModelListAuthIndexParams = {
|
||||
cfg: OpenClawConfig;
|
||||
authStore: AuthProfileStore;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
syntheticAuthProviderRefs?: readonly string[];
|
||||
};
|
||||
|
||||
export const EMPTY_MODEL_LIST_AUTH_INDEX: ModelListAuthIndex = {
|
||||
hasProviderAuth: () => false,
|
||||
};
|
||||
|
||||
function normalizeAuthProvider(
|
||||
provider: string,
|
||||
aliasMap: Readonly<Record<string, string>>,
|
||||
): string {
|
||||
const normalized = normalizeProviderIdForAuth(provider);
|
||||
return aliasMap[normalized] ?? normalized;
|
||||
}
|
||||
|
||||
function listPersistedSyntheticAuthProviderRefs(): readonly string[] {
|
||||
const index = readPersistedInstalledPluginIndexSync();
|
||||
return index?.plugins.flatMap((plugin) => plugin.syntheticAuthRefs ?? []) ?? [];
|
||||
}
|
||||
|
||||
export function createModelListAuthIndex(
|
||||
params: CreateModelListAuthIndexParams,
|
||||
): ModelListAuthIndex {
|
||||
const env = params.env ?? process.env;
|
||||
const aliasMap = resolveProviderAuthAliasMap({ config: params.cfg, env });
|
||||
const envCandidateMap = resolveProviderEnvApiKeyCandidates({ config: params.cfg, env });
|
||||
const authenticatedProviders = new Set<string>();
|
||||
const addProvider = (provider: string | undefined) => {
|
||||
if (!provider?.trim()) {
|
||||
return;
|
||||
}
|
||||
authenticatedProviders.add(normalizeAuthProvider(provider, aliasMap));
|
||||
};
|
||||
|
||||
for (const credential of Object.values(params.authStore.profiles ?? {})) {
|
||||
addProvider(credential.provider);
|
||||
}
|
||||
|
||||
for (const provider of Object.keys(envCandidateMap)) {
|
||||
if (resolveEnvApiKey(provider, env, { aliasMap, candidateMap: envCandidateMap })) {
|
||||
addProvider(provider);
|
||||
}
|
||||
}
|
||||
|
||||
if (resolveAwsSdkEnvVarName(env)) {
|
||||
addProvider("amazon-bedrock");
|
||||
}
|
||||
|
||||
for (const provider of Object.keys(params.cfg.models?.providers ?? {})) {
|
||||
if (hasUsableCustomProviderApiKey(params.cfg, provider, env)) {
|
||||
addProvider(provider);
|
||||
}
|
||||
}
|
||||
|
||||
for (const provider of params.syntheticAuthProviderRefs ??
|
||||
listPersistedSyntheticAuthProviderRefs()) {
|
||||
addProvider(provider);
|
||||
}
|
||||
|
||||
return {
|
||||
hasProviderAuth(provider: string): boolean {
|
||||
return authenticatedProviders.has(normalizeAuthProvider(provider, aliasMap));
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user