mirror of https://github.com/openclaw/openclaw.git
Anthropic: fix claude-cli runtime auth
This commit is contained in:
parent
9d315cdf42
commit
cd348659ce
|
|
@ -1,5 +1,18 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { registerSingleProviderPlugin } from "../../test/helpers/plugins/plugin-registration.js";
|
||||
|
||||
const { readClaudeCliCredentialsCachedMock } = vi.hoisted(() => ({
|
||||
readClaudeCliCredentialsCachedMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/provider-auth", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/provider-auth")>();
|
||||
return {
|
||||
...actual,
|
||||
readClaudeCliCredentialsCached: readClaudeCliCredentialsCachedMock,
|
||||
};
|
||||
});
|
||||
|
||||
import anthropicPlugin from "./index.js";
|
||||
|
||||
describe("anthropic provider replay hooks", () => {
|
||||
|
|
@ -83,4 +96,30 @@ describe("anthropic provider replay hooks", () => {
|
|||
next?.agents?.defaults?.models?.["anthropic/claude-opus-4-5"]?.params?.cacheRetention,
|
||||
).toBe("short");
|
||||
});
|
||||
|
||||
it("resolves claude-cli synthetic auth without allowing keychain prompts", async () => {
|
||||
readClaudeCliCredentialsCachedMock.mockReset();
|
||||
readClaudeCliCredentialsCachedMock.mockReturnValue({
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "access-token",
|
||||
refresh: "refresh-token",
|
||||
expires: 123,
|
||||
});
|
||||
|
||||
const provider = await registerSingleProviderPlugin(anthropicPlugin);
|
||||
|
||||
expect(
|
||||
provider.resolveSyntheticAuth?.({
|
||||
provider: "claude-cli",
|
||||
} as never),
|
||||
).toEqual({
|
||||
apiKey: "access-token",
|
||||
source: "Claude CLI native auth",
|
||||
mode: "oauth",
|
||||
});
|
||||
expect(readClaudeCliCredentialsCachedMock).toHaveBeenCalledWith({
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
ensureApiKeyFromOptionEnvOrPrompt,
|
||||
listProfilesForProvider,
|
||||
normalizeApiKeyInput,
|
||||
readClaudeCliCredentialsCached,
|
||||
type OpenClawConfig as ProviderAuthConfig,
|
||||
suggestOAuthProfileIdForLegacyDefault,
|
||||
type AuthProfileStore,
|
||||
|
|
@ -25,6 +26,7 @@ import { cloneFirstTemplateModel } from "openclaw/plugin-sdk/provider-model-shar
|
|||
import { fetchClaudeUsage } from "openclaw/plugin-sdk/provider-usage";
|
||||
import { buildAnthropicCliBackend } from "./cli-backend.js";
|
||||
import { buildAnthropicCliMigrationResult, hasClaudeCliAuth } from "./cli-migration.js";
|
||||
import { CLAUDE_CLI_BACKEND_ID } from "./cli-shared.js";
|
||||
import {
|
||||
applyAnthropicConfigDefaults,
|
||||
normalizeAnthropicProviderConfig,
|
||||
|
|
@ -212,6 +214,10 @@ function resolveAnthropic46ForwardCompatModel(params: {
|
|||
modelId: trimmedModelId,
|
||||
templateIds,
|
||||
ctx: params.ctx,
|
||||
patch:
|
||||
params.ctx.provider.trim().toLowerCase() === CLAUDE_CLI_BACKEND_ID
|
||||
? { provider: CLAUDE_CLI_BACKEND_ID }
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -278,6 +284,24 @@ function buildAnthropicAuthDoctorHint(params: {
|
|||
].join("\n");
|
||||
}
|
||||
|
||||
function resolveClaudeCliSyntheticAuth() {
|
||||
const credential = readClaudeCliCredentialsCached({ allowKeychainPrompt: false });
|
||||
if (!credential) {
|
||||
return undefined;
|
||||
}
|
||||
return credential.type === "oauth"
|
||||
? {
|
||||
apiKey: credential.access,
|
||||
source: "Claude CLI native auth",
|
||||
mode: "oauth" as const,
|
||||
}
|
||||
: {
|
||||
apiKey: credential.token,
|
||||
source: "Claude CLI native auth",
|
||||
mode: "token" as const,
|
||||
};
|
||||
}
|
||||
|
||||
async function runAnthropicCliMigration(ctx: ProviderAuthContext): Promise<ProviderAuthResult> {
|
||||
if (!hasClaudeCliAuth()) {
|
||||
throw new Error(
|
||||
|
|
@ -347,6 +371,7 @@ export function registerAnthropicPlugin(api: OpenClawPluginApi): void {
|
|||
id: providerId,
|
||||
label: "Anthropic",
|
||||
docsPath: "/providers/models",
|
||||
hookAliases: [CLAUDE_CLI_BACKEND_ID],
|
||||
envVars: ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"],
|
||||
deprecatedProfileIds: [claudeCliProfileId],
|
||||
oauthProfileIdRepairs: [
|
||||
|
|
@ -425,6 +450,10 @@ export function registerAnthropicPlugin(api: OpenClawPluginApi): void {
|
|||
normalizeConfig: ({ providerConfig }) => normalizeAnthropicProviderConfig(providerConfig),
|
||||
applyConfigDefaults: ({ config, env }) => applyAnthropicConfigDefaults({ config, env }),
|
||||
resolveDynamicModel: (ctx) => resolveAnthropicForwardCompatModel(ctx),
|
||||
resolveSyntheticAuth: ({ provider }) =>
|
||||
provider.trim().toLowerCase() === CLAUDE_CLI_BACKEND_ID
|
||||
? resolveClaudeCliSyntheticAuth()
|
||||
: undefined,
|
||||
buildReplayPolicy: buildAnthropicReplayPolicy,
|
||||
isModernModelRef: ({ modelId }) => matchesAnthropicModernModel(modelId),
|
||||
resolveReasoningOutputMode: () => "native",
|
||||
|
|
|
|||
|
|
@ -78,6 +78,13 @@ vi.mock("../plugins/provider-runtime.js", () => ({
|
|||
}
|
||||
return undefined;
|
||||
}
|
||||
if (params.provider === "claude-cli") {
|
||||
return {
|
||||
apiKey: "claude-cli-access-token",
|
||||
source: "Claude CLI native auth",
|
||||
mode: "oauth" as const,
|
||||
};
|
||||
}
|
||||
if (params.provider !== "ollama") {
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -486,6 +493,28 @@ describe("resolveApiKeyForProvider", () => {
|
|||
).rejects.toThrow('No API key found for provider "xai"');
|
||||
});
|
||||
|
||||
it("reuses native Claude CLI auth for the claude-cli provider", async () => {
|
||||
const resolved = await resolveApiKeyForProvider({
|
||||
provider: "claude-cli",
|
||||
cfg: {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "claude-cli/claude-sonnet-4-6",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
store: { version: 1, profiles: {} },
|
||||
});
|
||||
|
||||
expect(resolved).toEqual({
|
||||
apiKey: "claude-cli-access-token",
|
||||
source: "Claude CLI native auth",
|
||||
mode: "oauth",
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers explicit api-key provider config over ambient auth profiles", async () => {
|
||||
const resolved = await resolveApiKeyForProvider({
|
||||
provider: "openai",
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ const ZAI_GLM5_CASE = {
|
|||
|
||||
function createRuntimeHooks() {
|
||||
return createProviderRuntimeTestMock({
|
||||
handledDynamicProviders: ["anthropic", "zai", "openai-codex"],
|
||||
handledDynamicProviders: ["anthropic", "claude-cli", "zai", "openai-codex"],
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -123,6 +123,28 @@ function runAnthropicSonnetForwardCompatFallback() {
|
|||
});
|
||||
}
|
||||
|
||||
function runClaudeCliSonnetForwardCompatFallback() {
|
||||
expectResolvedForwardCompatFallbackWithRegistryResult({
|
||||
result: resolveModelWithRegistry({
|
||||
provider: "claude-cli",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
agentDir: "/tmp/agent",
|
||||
modelRegistry: createRegistry([
|
||||
{
|
||||
provider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
model: ANTHROPIC_SONNET_TEMPLATE,
|
||||
},
|
||||
]),
|
||||
runtimeHooks: createRuntimeHooks(),
|
||||
}),
|
||||
expectedModel: {
|
||||
...ANTHROPIC_SONNET_EXPECTED,
|
||||
provider: "claude-cli",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function runZaiForwardCompatFallback() {
|
||||
const result = resolveModelWithRegistry({
|
||||
provider: ZAI_GLM5_CASE.provider,
|
||||
|
|
@ -154,5 +176,10 @@ describe("resolveModel forward-compat tail", () => {
|
|||
runAnthropicSonnetForwardCompatFallback,
|
||||
);
|
||||
|
||||
it(
|
||||
"preserves the claude-cli provider for anthropic forward-compat fallback models",
|
||||
runClaudeCliSonnetForwardCompatFallback,
|
||||
);
|
||||
|
||||
it("builds a zai forward-compat fallback for glm-5", runZaiForwardCompatFallback);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -324,7 +324,8 @@ function buildDynamicModel(
|
|||
maxTokens: patch.maxTokens ?? DEFAULT_CONTEXT_WINDOW,
|
||||
});
|
||||
}
|
||||
case "anthropic": {
|
||||
case "anthropic":
|
||||
case "claude-cli": {
|
||||
if (lower !== "claude-opus-4-6" && lower !== "claude-sonnet-4-6") {
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -337,13 +338,13 @@ function buildDynamicModel(
|
|||
template,
|
||||
modelId,
|
||||
{
|
||||
provider: "anthropic",
|
||||
provider: params.provider,
|
||||
api: "anthropic-messages",
|
||||
baseUrl: ANTHROPIC_BASE_URL,
|
||||
reasoning: true,
|
||||
},
|
||||
{
|
||||
provider: "anthropic",
|
||||
provider: params.provider,
|
||||
api: "anthropic-messages",
|
||||
baseUrl: ANTHROPIC_BASE_URL,
|
||||
reasoning: true,
|
||||
|
|
|
|||
|
|
@ -300,6 +300,25 @@ describe("provider-runtime", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("returns no runtime plugin when the provider has no owning plugin", () => {
|
||||
it("matches providers by hook alias for runtime hook lookup", () => {
|
||||
resolveOwningPluginIdsForProviderMock.mockReturnValue(["anthropic"]);
|
||||
resolvePluginProvidersMock.mockReturnValue([
|
||||
{
|
||||
id: "anthropic",
|
||||
label: "Anthropic",
|
||||
hookAliases: ["claude-cli"],
|
||||
auth: [],
|
||||
},
|
||||
]);
|
||||
|
||||
expectProviderRuntimePluginLoad({
|
||||
provider: "claude-cli",
|
||||
expectedPluginId: "anthropic",
|
||||
expectedOnlyPluginIds: ["anthropic"],
|
||||
});
|
||||
});
|
||||
|
||||
it("returns no runtime plugin when the provider has no owning plugin", () => {
|
||||
expectProviderRuntimePluginLoad({
|
||||
provider: "anthropic",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ let setActivePluginRegistry: SetActivePluginRegistry;
|
|||
function createManifestProviderPlugin(params: {
|
||||
id: string;
|
||||
providerIds: string[];
|
||||
cliBackends?: string[];
|
||||
origin?: "bundled" | "workspace";
|
||||
enabledByDefault?: boolean;
|
||||
modelSupport?: { modelPrefixes?: string[]; modelPatterns?: string[] };
|
||||
|
|
@ -34,7 +35,7 @@ function createManifestProviderPlugin(params: {
|
|||
id: params.id,
|
||||
enabledByDefault: params.enabledByDefault,
|
||||
channels: [],
|
||||
cliBackends: [],
|
||||
cliBackends: params.cliBackends ?? [],
|
||||
providers: params.providerIds,
|
||||
modelSupport: params.modelSupport,
|
||||
skills: [],
|
||||
|
|
@ -62,6 +63,7 @@ function setOwningProviderManifestPlugins() {
|
|||
createManifestProviderPlugin({
|
||||
id: "openai",
|
||||
providerIds: ["openai", "openai-codex"],
|
||||
cliBackends: ["codex-cli"],
|
||||
modelSupport: {
|
||||
modelPrefixes: ["gpt-", "o1", "o3", "o4"],
|
||||
},
|
||||
|
|
@ -69,6 +71,7 @@ function setOwningProviderManifestPlugins() {
|
|||
createManifestProviderPlugin({
|
||||
id: "anthropic",
|
||||
providerIds: ["anthropic"],
|
||||
cliBackends: ["claude-cli"],
|
||||
modelSupport: {
|
||||
modelPrefixes: ["claude-"],
|
||||
},
|
||||
|
|
@ -85,6 +88,7 @@ function setOwningProviderManifestPluginsWithWorkspace() {
|
|||
createManifestProviderPlugin({
|
||||
id: "openai",
|
||||
providerIds: ["openai", "openai-codex"],
|
||||
cliBackends: ["codex-cli"],
|
||||
modelSupport: {
|
||||
modelPrefixes: ["gpt-", "o1", "o3", "o4"],
|
||||
},
|
||||
|
|
@ -92,6 +96,7 @@ function setOwningProviderManifestPluginsWithWorkspace() {
|
|||
createManifestProviderPlugin({
|
||||
id: "anthropic",
|
||||
providerIds: ["anthropic"],
|
||||
cliBackends: ["claude-cli"],
|
||||
modelSupport: {
|
||||
modelPrefixes: ["claude-"],
|
||||
},
|
||||
|
|
@ -275,6 +280,13 @@ describe("resolvePluginProviders", () => {
|
|||
({ setActivePluginRegistry } = await import("./runtime.js"));
|
||||
});
|
||||
|
||||
it("maps cli backend ids to owning plugin ids via manifests", () => {
|
||||
setOwningProviderManifestPlugins();
|
||||
|
||||
expectOwningPluginIds("claude-cli", ["anthropic"]);
|
||||
expectOwningPluginIds("codex-cli", ["openai"]);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePluginRegistry(createEmptyPluginRegistry());
|
||||
resolveRuntimePluginRegistryMock.mockReset();
|
||||
|
|
|
|||
|
|
@ -203,8 +203,14 @@ export function resolveOwningPluginIdsForProvider(params: {
|
|||
|
||||
const registry = resolveManifestRegistry(params);
|
||||
const pluginIds = registry.plugins
|
||||
.filter((plugin) =>
|
||||
plugin.providers.some((providerId) => normalizeProviderId(providerId) === normalizedProvider),
|
||||
.filter(
|
||||
(plugin) =>
|
||||
plugin.providers.some(
|
||||
(providerId) => normalizeProviderId(providerId) === normalizedProvider,
|
||||
) ||
|
||||
plugin.cliBackends.some(
|
||||
(backendId) => normalizeProviderId(backendId) === normalizedProvider,
|
||||
),
|
||||
)
|
||||
.map((plugin) => plugin.id);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue