diff --git a/.changeset/acp-api-key-auth.md b/.changeset/acp-api-key-auth.md new file mode 100644 index 0000000000..23411b95f2 --- /dev/null +++ b/.changeset/acp-api-key-auth.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Allow ACP sessions to use configured API-key providers. diff --git a/docs/en/guides/ides.md b/docs/en/guides/ides.md index f275af8ac6..e63dd822f5 100644 --- a/docs/en/guides/ides.md +++ b/docs/en/guides/ides.md @@ -4,9 +4,9 @@ Kimi Code CLI supports integration into IDEs via the [Agent Client Protocol (ACP ## Prerequisites -Before configuring your IDE, make sure Kimi Code CLI is installed and you have completed the login setup. +Before configuring your IDE, make sure Kimi Code CLI is installed and has usable authentication: complete the OAuth login flow or configure a provider with an API key. -The ACP adapter is exposed as the `kimi acp` subcommand. The IDE launches it as a child process and communicates over stdin/stdout using JSON-RPC. Each time the IDE creates a session, the CLI reuses its existing authentication state — no need to log in again. +The ACP adapter is exposed as the `kimi acp` subcommand. The IDE launches it as a child process and communicates over stdin/stdout using JSON-RPC. Each time the IDE creates a session, the CLI reuses its existing OAuth login or configured API key — no need to authenticate again. ::: tip Path note Child processes launched from an IDE GUI on macOS typically do **not** inherit the terminal shell's `PATH`. If `kimi` is not in a system directory like `/usr/local/bin`, use the absolute path in your IDE configuration. Run `which kimi` in a terminal to find the active path. @@ -82,12 +82,12 @@ Pick **Kimi Code CLI** from Paseo's built-in ACP provider catalog, or add a cust } ``` -Paseo's generic ACP adapter does not drive the login flow, so complete the terminal login first (see [Prerequisites](#prerequisites)) — otherwise session creation fails with `Authentication required`. +Paseo's generic ACP adapter does not drive the login flow. Complete the terminal OAuth login or configure a provider with an API key first (see [Prerequisites](#prerequisites)) — otherwise session creation fails with `Authentication required`. ## Troubleshooting -- **Session disconnects immediately / IDE shows "agent exited"**: usually a wrong `command` path or a missing login. Run `kimi acp` in a terminal first to verify — if it blocks waiting for stdin, the CLI itself is fine and the problem is in the IDE configuration; if it exits immediately with an error, follow the error message (most commonly you need to run `/login`). -- **IDE shows "auth required"**: the CLI has no usable authentication token. Exit the IDE, run `kimi` in a terminal to complete login, then restart the IDE. +- **Session disconnects immediately / IDE shows "agent exited"**: usually a wrong `command` path or missing authentication. Run `kimi acp` in a terminal first to verify — if it blocks waiting for stdin, the CLI itself is fine and the problem is in the IDE configuration; if it exits immediately with an error, follow the error message (most commonly you need to run `/login` or configure a provider with an API key). +- **IDE shows "auth required"**: the CLI has no usable OAuth login or configured API key. Exit the IDE, complete login or configure a provider in a terminal, then restart the IDE. - **MCP tools not visible**: check the [`kimi acp` reference](../reference/kimi-acp.md) capability table to confirm that the MCP transport type configured in your IDE is supported. The Kimi Code CLI ACP adapter currently supports `http`, `stdio`, and `sse` transports; `acp` transport MCP servers are silently dropped and a warning is written to the log. ## Next steps diff --git a/docs/zh/guides/ides.md b/docs/zh/guides/ides.md index a8193ed4ea..3602b1fca0 100644 --- a/docs/zh/guides/ides.md +++ b/docs/zh/guides/ides.md @@ -4,9 +4,9 @@ Kimi Code CLI 支持通过 [Agent Client Protocol (ACP)](https://agentclientprot ## 前置准备 -在配置 IDE 之前,请确保已安装 Kimi Code CLI 并完成登录配置。 +在配置 IDE 之前,请确保已安装 Kimi Code CLI,并完成 OAuth 登录或配置带有 API 密钥的 provider。 -ACP 适配层暴露子命令 `kimi acp`,IDE 通过子进程方式启动它,并在标准输入/输出上跑 JSON-RPC。每次 IDE 创建会话时,CLI 会复用它的鉴权状态——不需要重复登录。 +ACP 适配层暴露子命令 `kimi acp`,IDE 通过子进程方式启动它,并在标准输入/输出上跑 JSON-RPC。每次 IDE 创建会话时,CLI 会复用已有的 OAuth 登录或已配置的 API 密钥——不需要重复鉴权。 ::: tip 路径提示 macOS 下从 IDE GUI 启动的子进程通常**不会**继承终端 shell 的 `PATH`,所以如果 `kimi` 不在 `/usr/local/bin` 这类系统目录里,IDE 配置中要使用绝对路径。终端里运行 `which kimi` 可以查到当前生效的路径。 @@ -82,12 +82,12 @@ JetBrains 这一侧对 `command` 字段处理较严格——务必填写**绝对 } ``` -Paseo 的通用 ACP 适配层不会帮你走登录流程,所以请先完成终端登录(见[前置准备](#前置准备))——否则创建会话会以 `Authentication required` 失败。 +Paseo 的通用 ACP 适配层不会帮你走登录流程,所以请先完成终端 OAuth 登录或配置带有 API 密钥的 provider(见[前置准备](#前置准备))——否则创建会话会以 `Authentication required` 失败。 ## 故障排查 -- **会话立刻被中断 / IDE 提示 "agent exited"**:通常是 `command` 路径不对或 kimi 没登录。先在终端跑一次 `kimi acp` 验证:如果阻塞等待标准输入则说明 CLI 本身没问题,问题在 IDE 配置;如果立刻报错则按报错提示处理(多数是没 `/login`)。 -- **IDE 显示 "auth required"**:表示 CLI 没有可用的鉴权令牌。退出 IDE,在终端执行 `kimi` 完成登录后再启动 IDE 即可。 +- **会话立刻被中断 / IDE 提示 "agent exited"**:通常是 `command` 路径不对或缺少鉴权。先在终端跑一次 `kimi acp` 验证:如果阻塞等待标准输入则说明 CLI 本身没问题,问题在 IDE 配置;如果立刻报错则按报错提示处理(多数是没 `/login`,或还没配置带有 API 密钥的 provider)。 +- **IDE 显示 "auth required"**:表示 CLI 没有可用的 OAuth 登录或已配置的 API 密钥。退出 IDE,在终端完成登录或配置 provider 后再启动 IDE 即可。 - **MCP 工具看不到**:参考 [`kimi acp`](../reference/kimi-acp.md) 中的能力表确认 IDE 配的 MCP 传输类型是否被支持。当前 Kimi Code CLI 的 ACP 适配层支持 `http`、`stdio` 与 `sse` 三种传输方式;`acp` 传输的 MCP server 会被静默丢弃并在日志中给出 warn。 ## 下一步 diff --git a/packages/node-sdk/src/auth.ts b/packages/node-sdk/src/auth.ts index 4f02747f88..77883d3863 100644 --- a/packages/node-sdk/src/auth.ts +++ b/packages/node-sdk/src/auth.ts @@ -5,6 +5,7 @@ import { writeConfigFile, type KimiConfig, type OAuthRef, + type ProviderConfig, } from '@moonshot-ai/agent-core'; import { applyManagedKimiCodeConfig, @@ -119,7 +120,26 @@ export class KimiAuthFacade { } async status(providerName?: string | undefined): Promise { - return this.toolkit.status(providerName, this.resolveRuntimeManagedAuth(providerName).oauthRef); + const status = await this.toolkit.status( + providerName, + this.resolveRuntimeManagedAuth(providerName).oauthRef, + ); + const config = loadRuntimeConfigSafe(this.options.configPath).config; + const apiKeyProviderNames = new Set( + Object.entries(config.providers) + .filter(([, provider]) => hasConfiguredApiKey(provider)) + .map(([name]) => name), + ); + const providers = status.providers.map((entry) => { + const hasApiKey = apiKeyProviderNames.delete(entry.providerName); + return { ...entry, hasToken: entry.hasToken || hasApiKey }; + }); + if (providerName !== undefined || apiKeyProviderNames.size === 0) return { providers }; + + for (const providerName of apiKeyProviderNames) { + providers.push({ providerName, hasToken: true }); + } + return { providers }; } async login( @@ -317,3 +337,28 @@ export class KimiAuthFacade { }).oauthRef; } } + +function hasConfiguredApiKey(provider: ProviderConfig): boolean { + if (nonEmpty(provider.apiKey)) return true; + + switch (provider.type) { + case 'anthropic': + return nonEmpty(provider.env?.['ANTHROPIC_API_KEY']); + case 'openai': + case 'openai_responses': + return nonEmpty(provider.env?.['OPENAI_API_KEY']); + case 'kimi': + return nonEmpty(provider.env?.['KIMI_API_KEY']); + case 'google-genai': + return nonEmpty(provider.env?.['GOOGLE_API_KEY']); + case 'vertexai': + return ( + nonEmpty(provider.env?.['VERTEXAI_API_KEY']) || + nonEmpty(provider.env?.['GOOGLE_API_KEY']) + ); + } +} + +function nonEmpty(value: string | undefined): boolean { + return (value?.trim().length ?? 0) > 0; +} diff --git a/packages/node-sdk/test/auth-facade.test.ts b/packages/node-sdk/test/auth-facade.test.ts index 978bd25780..28cec7caac 100644 --- a/packages/node-sdk/test/auth-facade.test.ts +++ b/packages/node-sdk/test/auth-facade.test.ts @@ -203,6 +203,71 @@ oauth = { storage = "file", key = "${oauthKey}", oauth_host = "https://auth.dev. }); }); + it('reports configured API-key providers as authenticated', async () => { + await writeFile( + join(homeDir, 'config.toml'), + ` +[providers."moonshot-cn"] +type = "kimi" +base_url = "https://api.example.test/v1" +api_key = "YOUR_API_KEY" +`, + ); + const harness = createKimiHarness({ homeDir, identity: TEST_IDENTITY }); + + await expect(harness.auth.status()).resolves.toEqual({ + providers: [ + { providerName: KIMI_CODE_PROVIDER_NAME, hasToken: false }, + { providerName: 'moonshot-cn', hasToken: true }, + ], + }); + await expect(harness.auth.status('moonshot-cn')).resolves.toEqual({ + providers: [{ providerName: 'moonshot-cn', hasToken: true }], + }); + }); + + it('reports env-backed API-key providers as authenticated', async () => { + await writeFile( + join(homeDir, 'config.toml'), + ` +[providers."moonshot-env"] +type = "kimi" +base_url = "https://api.example.test/v1" + +[providers."moonshot-env".env] +KIMI_API_KEY = "YOUR_API_KEY" +`, + ); + const harness = createKimiHarness({ homeDir, identity: TEST_IDENTITY }); + + await expect(harness.auth.status()).resolves.toEqual({ + providers: [ + { providerName: KIMI_CODE_PROVIDER_NAME, hasToken: false }, + { providerName: 'moonshot-env', hasToken: true }, + ], + }); + await expect(harness.auth.status('moonshot-env')).resolves.toEqual({ + providers: [{ providerName: 'moonshot-env', hasToken: true }], + }); + }); + + it('does not duplicate a provider with both OAuth and an API key', async () => { + await new FileTokenStorage(join(homeDir, 'credentials')).save('kimi-code', freshToken()); + await writeFile( + join(homeDir, 'config.toml'), + ` +[providers."managed:kimi-code"] +type = "kimi" +api_key = "YOUR_API_KEY" +`, + ); + const harness = createKimiHarness({ homeDir, identity: TEST_IDENTITY }); + + await expect(harness.auth.status()).resolves.toEqual({ + providers: [{ providerName: KIMI_CODE_PROVIDER_NAME, hasToken: true }], + }); + }); + it('provisions SDK config using an existing Kimi OAuth token', async () => { await new FileTokenStorage(join(homeDir, 'credentials')).save('kimi-code', freshToken()); const fetchMock = vi.fn(