mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 17:51:22 +00:00
fix: improve claude cli live discovery
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
resolvePluginDiscoveryProviders,
|
||||
runProviderCatalog,
|
||||
} from "../plugins/provider-discovery.js";
|
||||
import { resolveOwningPluginIdsForProvider } from "../plugins/providers.js";
|
||||
import { ensureAuthProfileStore } from "./auth-profiles/store.js";
|
||||
import {
|
||||
isNonSecretApiKeyMarker,
|
||||
@@ -64,7 +65,12 @@ function resolveLiveProviderCatalogTimeoutMs(env: NodeJS.ProcessEnv): number | n
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : 15_000;
|
||||
}
|
||||
|
||||
function resolveProviderDiscoveryFilter(env: NodeJS.ProcessEnv): string[] | undefined {
|
||||
function resolveProviderDiscoveryFilter(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): string[] | undefined {
|
||||
const { config, workspaceDir, env } = params;
|
||||
const testRaw = env.OPENCLAW_TEST_ONLY_PROVIDER_PLUGIN_IDS?.trim();
|
||||
if (testRaw) {
|
||||
const ids = testRaw
|
||||
@@ -78,15 +84,48 @@ function resolveProviderDiscoveryFilter(env: NodeJS.ProcessEnv): string[] | unde
|
||||
if (!live) {
|
||||
return undefined;
|
||||
}
|
||||
const raw = env.OPENCLAW_LIVE_PROVIDERS?.trim();
|
||||
if (!raw || raw === "all") {
|
||||
const rawValues = [
|
||||
env.OPENCLAW_LIVE_PROVIDERS?.trim(),
|
||||
env.OPENCLAW_LIVE_GATEWAY_PROVIDERS?.trim(),
|
||||
].filter((value): value is string => Boolean(value && value !== "all"));
|
||||
if (rawValues.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const ids = raw
|
||||
.split(",")
|
||||
const ids = rawValues
|
||||
.flatMap((value) => value.split(","))
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
return ids.length > 0 ? [...new Set(ids)] : undefined;
|
||||
if (ids.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const pluginIds = new Set<string>();
|
||||
for (const id of ids) {
|
||||
const owners =
|
||||
resolveOwningPluginIdsForProvider({
|
||||
provider: id,
|
||||
config,
|
||||
workspaceDir,
|
||||
env,
|
||||
}) ?? [];
|
||||
if (owners.length > 0) {
|
||||
for (const owner of owners) {
|
||||
pluginIds.add(owner);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
pluginIds.add(id);
|
||||
}
|
||||
return pluginIds.size > 0
|
||||
? [...pluginIds].toSorted((left, right) => left.localeCompare(right))
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function resolveProviderDiscoveryFilterForTest(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): string[] | undefined {
|
||||
return resolveProviderDiscoveryFilter(params);
|
||||
}
|
||||
|
||||
function mergeImplicitProviderSet(
|
||||
@@ -315,7 +354,11 @@ export async function resolveImplicitProviders(
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env,
|
||||
onlyPluginIds: resolveProviderDiscoveryFilter(env),
|
||||
onlyPluginIds: resolveProviderDiscoveryFilter({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env,
|
||||
}),
|
||||
});
|
||||
|
||||
for (const order of PLUGIN_DISCOVERY_ORDERS) {
|
||||
|
||||
40
src/agents/models-config.providers.live-filter.test.ts
Normal file
40
src/agents/models-config.providers.live-filter.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveProviderDiscoveryFilterForTest } from "./models-config.providers.implicit.js";
|
||||
|
||||
describe("resolveProviderDiscoveryFilterForTest", () => {
|
||||
it("maps live provider backend ids to owning plugin ids", () => {
|
||||
expect(
|
||||
resolveProviderDiscoveryFilterForTest({
|
||||
env: {
|
||||
OPENCLAW_LIVE_TEST: "1",
|
||||
OPENCLAW_LIVE_PROVIDERS: "claude-cli",
|
||||
VITEST: "1",
|
||||
} as NodeJS.ProcessEnv,
|
||||
}),
|
||||
).toEqual(["anthropic"]);
|
||||
});
|
||||
|
||||
it("honors gateway live provider filters too", () => {
|
||||
expect(
|
||||
resolveProviderDiscoveryFilterForTest({
|
||||
env: {
|
||||
OPENCLAW_LIVE_TEST: "1",
|
||||
OPENCLAW_LIVE_GATEWAY_PROVIDERS: "claude-cli",
|
||||
VITEST: "1",
|
||||
} as NodeJS.ProcessEnv,
|
||||
}),
|
||||
).toEqual(["anthropic"]);
|
||||
});
|
||||
|
||||
it("keeps explicit plugin-id filters when no owning provider plugin exists", () => {
|
||||
expect(
|
||||
resolveProviderDiscoveryFilterForTest({
|
||||
env: {
|
||||
OPENCLAW_LIVE_TEST: "1",
|
||||
OPENCLAW_LIVE_PROVIDERS: "openrouter",
|
||||
VITEST: "1",
|
||||
} as NodeJS.ProcessEnv,
|
||||
}),
|
||||
).toEqual(["openrouter"]);
|
||||
});
|
||||
});
|
||||
91
src/agents/pi-model-discovery.synthetic-auth.test.ts
Normal file
91
src/agents/pi-model-discovery.synthetic-auth.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { saveAuthProfileStore } from "./auth-profiles.js";
|
||||
|
||||
const loadPluginManifestRegistry = vi.hoisted(() =>
|
||||
vi.fn(() => ({
|
||||
plugins: [
|
||||
{
|
||||
id: "anthropic",
|
||||
origin: "bundled",
|
||||
providers: ["anthropic"],
|
||||
cliBackends: ["claude-cli"],
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
})),
|
||||
);
|
||||
|
||||
const resolveProviderSyntheticAuthWithPlugin = vi.hoisted(() =>
|
||||
vi.fn((params: { provider: string }) =>
|
||||
params.provider === "claude-cli"
|
||||
? {
|
||||
apiKey: "claude-cli-access-token",
|
||||
source: "Claude CLI native auth",
|
||||
mode: "oauth" as const,
|
||||
}
|
||||
: undefined,
|
||||
),
|
||||
);
|
||||
|
||||
vi.mock("../plugins/manifest-registry.js", () => ({
|
||||
loadPluginManifestRegistry,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/provider-runtime.js", () => ({
|
||||
applyProviderResolvedModelCompatWithPlugins: () => undefined,
|
||||
applyProviderResolvedTransportWithPlugin: () => undefined,
|
||||
normalizeProviderResolvedModelWithPlugin: () => undefined,
|
||||
resolveProviderSyntheticAuthWithPlugin,
|
||||
resolveExternalAuthProfilesWithPlugins: () => [],
|
||||
}));
|
||||
|
||||
async function withAgentDir(run: (agentDir: string) => Promise<void>): Promise<void> {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-pi-synthetic-auth-"));
|
||||
try {
|
||||
await run(agentDir);
|
||||
} finally {
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
describe("pi model discovery synthetic auth", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
loadPluginManifestRegistry.mockClear();
|
||||
resolveProviderSyntheticAuthWithPlugin.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("mirrors plugin-owned synthetic cli auth into pi auth storage", async () => {
|
||||
await withAgentDir(async (agentDir) => {
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {},
|
||||
},
|
||||
agentDir,
|
||||
);
|
||||
|
||||
const { discoverAuthStorage } = await import("./pi-model-discovery.js");
|
||||
const authStorage = discoverAuthStorage(agentDir);
|
||||
|
||||
expect(loadPluginManifestRegistry).toHaveBeenCalled();
|
||||
expect(resolveProviderSyntheticAuthWithPlugin).toHaveBeenCalledWith({
|
||||
provider: "claude-cli",
|
||||
context: {
|
||||
config: undefined,
|
||||
provider: "claude-cli",
|
||||
providerConfig: undefined,
|
||||
},
|
||||
});
|
||||
expect(authStorage.hasAuth("claude-cli")).toBe(true);
|
||||
await expect(authStorage.getApiKey("claude-cli")).resolves.toBe("claude-cli-access-token");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,11 +6,13 @@ import type {
|
||||
AuthStorage as PiAuthStorage,
|
||||
ModelRegistry as PiModelRegistry,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
import { normalizeModelCompat } from "../plugins/provider-model-compat.js";
|
||||
import {
|
||||
applyProviderResolvedModelCompatWithPlugins,
|
||||
applyProviderResolvedTransportWithPlugin,
|
||||
normalizeProviderResolvedModelWithPlugin,
|
||||
resolveProviderSyntheticAuthWithPlugin,
|
||||
} from "../plugins/provider-runtime.js";
|
||||
import type { ProviderRuntimeModel } from "../plugins/types.js";
|
||||
import { ensureAuthProfileStore } from "./auth-profiles.js";
|
||||
@@ -245,6 +247,36 @@ function resolvePiCredentials(agentDir: string): PiCredentialMap {
|
||||
key: resolved.apiKey,
|
||||
};
|
||||
}
|
||||
const syntheticProviders = new Set<string>();
|
||||
for (const plugin of loadPluginManifestRegistry().plugins) {
|
||||
for (const provider of plugin.providers) {
|
||||
syntheticProviders.add(provider);
|
||||
}
|
||||
for (const backend of plugin.cliBackends) {
|
||||
syntheticProviders.add(backend);
|
||||
}
|
||||
}
|
||||
for (const provider of syntheticProviders) {
|
||||
if (credentials[provider]) {
|
||||
continue;
|
||||
}
|
||||
const resolved = resolveProviderSyntheticAuthWithPlugin({
|
||||
provider,
|
||||
context: {
|
||||
config: undefined,
|
||||
provider,
|
||||
providerConfig: undefined,
|
||||
},
|
||||
});
|
||||
const apiKey = resolved?.apiKey?.trim();
|
||||
if (!apiKey) {
|
||||
continue;
|
||||
}
|
||||
credentials[provider] = {
|
||||
type: "api_key",
|
||||
key: apiKey,
|
||||
};
|
||||
}
|
||||
return credentials;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ function makeProvider(params: {
|
||||
label?: string;
|
||||
order?: ProviderDiscoveryOrder;
|
||||
mode?: "catalog" | "discovery";
|
||||
aliases?: string[];
|
||||
hookAliases?: string[];
|
||||
}): ProviderPlugin {
|
||||
const hook = {
|
||||
...(params.order ? { order: params.order } : {}),
|
||||
@@ -21,6 +23,8 @@ function makeProvider(params: {
|
||||
id: params.id,
|
||||
label: params.label ?? params.id,
|
||||
auth: [],
|
||||
...(params.aliases ? { aliases: params.aliases } : {}),
|
||||
...(params.hookAliases ? { hookAliases: params.hookAliases } : {}),
|
||||
...(params.mode === "discovery" ? { discovery: hook } : { catalog: hook }),
|
||||
};
|
||||
}
|
||||
@@ -154,6 +158,37 @@ describe("normalizePluginDiscoveryResult", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "maps a single provider result to aliases and hook aliases",
|
||||
provider: makeProvider({
|
||||
id: "Anthropic",
|
||||
aliases: ["anthropic-api"],
|
||||
hookAliases: ["claude-cli"],
|
||||
}),
|
||||
result: {
|
||||
provider: makeModelProviderConfig({
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
api: "anthropic-messages",
|
||||
}),
|
||||
},
|
||||
expected: {
|
||||
anthropic: {
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
api: "anthropic-messages",
|
||||
models: [],
|
||||
},
|
||||
"anthropic-api": {
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
api: "anthropic-messages",
|
||||
models: [],
|
||||
},
|
||||
"claude-cli": {
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
api: "anthropic-messages",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "normalizes keys for multi-provider discovery results",
|
||||
provider: makeProvider({ id: "ignored" }),
|
||||
|
||||
@@ -62,7 +62,19 @@ export function normalizePluginDiscoveryResult(params: {
|
||||
}
|
||||
|
||||
if ("provider" in result) {
|
||||
return { [normalizeProviderId(params.provider.id)]: result.provider };
|
||||
const normalized: Record<string, ModelProviderConfig> = {};
|
||||
for (const providerId of [
|
||||
params.provider.id,
|
||||
...(params.provider.aliases ?? []),
|
||||
...(params.provider.hookAliases ?? []),
|
||||
]) {
|
||||
const normalizedKey = normalizeProviderId(providerId);
|
||||
if (!normalizedKey) {
|
||||
continue;
|
||||
}
|
||||
normalized[normalizedKey] = result.provider;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
const normalized: Record<string, ModelProviderConfig> = {};
|
||||
|
||||
Reference in New Issue
Block a user