Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/google-vertex-service-account.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@moonshot-ai/acp-adapter": patch
"@moonshot-ai/agent-core": patch
"@moonshot-ai/agent-core-v2": patch
"@moonshot-ai/kosong": patch
---

feat: add GCP service account file support for google-vertex and google-vertex-anthropic
34 changes: 20 additions & 14 deletions docs/en/configuration/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,27 +130,33 @@ api_key = "xxxxx"
base_url = "https://your-gateway.example"
```

## `vertexai`
## `google-vertex` / `vertexai`

Shares the same implementation as `google-genai`; setting `type = "vertexai"` switches to the Vertex AI access path.
Setting `type = "google-vertex"` (or `type = "vertexai"`) connects to Google Gemini models on Google Vertex AI.

Authentication follows the standard Google Cloud ADC flow (`gcloud auth application-default login` or a `GOOGLE_APPLICATION_CREDENTIALS` service account JSON) — this part is unrelated to Kimi Code. **The project ID and region must be written in the `[providers.vertexai.env]` sub-table** — simply `export GOOGLE_CLOUD_PROJECT` in the shell will not be read by the CLI.
Authentication supports a GCP service account JSON file (`service_account_file`, with `~` path expansion support), an environment variable (`GOOGLE_APPLICATION_CREDENTIALS`), or the standard ADC flow (`gcloud auth application-default login`).

```toml
[providers.vertexai]
type = "vertexai"

[providers.vertexai.env]
GOOGLE_CLOUD_PROJECT = "my-gcp-project"
GOOGLE_CLOUD_LOCATION = "us-central1"
[providers.vertex]
type = "google-vertex"
service_account_file = "~/.secrets/my-service-account.json"
location = "us-central1"
```

```sh
gcloud auth application-default login # one-time authentication
kimi
```
When `service_account_file` is specified, Kimi Code automatically reads `project_id` from the service account JSON if `project` is omitted.

To route Vertex requests through a custom (e.g. proxied) endpoint, set `base_url` (or the `GOOGLE_VERTEX_BASE_URL` env var).

## `google-vertex-anthropic`

To route Vertex requests through a custom (e.g. proxied) endpoint, set `base_url` (or the `GOOGLE_VERTEX_BASE_URL` env var); when omitted, the SDK default regional `*-aiplatform.googleapis.com` host is used. As with `google-genai`, give the host root only — the SDK appends `/v1beta1/publishers/google/models/…` itself.
Setting `type = "google-vertex-anthropic"` connects to Anthropic Claude models served on Google Vertex AI.

```toml
[providers.vertex-claude]
type = "google-vertex-anthropic"
service_account_file = "~/.secrets/my-service-account.json"
location = "us-east5"
```

## OAuth and credential injection

Expand Down
34 changes: 20 additions & 14 deletions docs/zh/configuration/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,27 +130,33 @@ api_key = "xxxxx"
base_url = "https://your-gateway.example"
```

## `vertexai`
## `google-vertex` / `vertexai`

与 `google-genai` 共用实现,`type = "vertexai"` 时切换到 Vertex AI 访问路径
设置 `type = "google-vertex"`(或 `type = "vertexai"`)连接 Google Vertex AI 上的 Gemini 模型

认证走 Google Cloud 标准 ADC 流程(`gcloud auth application-default login` 或 `GOOGLE_APPLICATION_CREDENTIALS` 服务账号 JSON),这部分与 Kimi Code 无关。**项目 ID 和区域必须写在 `[providers.vertexai.env]` 子表里**——直接在 shell 里 `export GOOGLE_CLOUD_PROJECT` 不会被 CLI 读取
认证支持 GCP 服务账号 JSON 文件(`service_account_file`,支持 `~` 路径展开)、环境变量(`GOOGLE_APPLICATION_CREDENTIALS`)或标准 ADC 流程(`gcloud auth application-default login`)

```toml
[providers.vertexai]
type = "vertexai"

[providers.vertexai.env]
GOOGLE_CLOUD_PROJECT = "my-gcp-project"
GOOGLE_CLOUD_LOCATION = "us-central1"
[providers.vertex]
type = "google-vertex"
service_account_file = "~/.secrets/my-service-account.json"
location = "us-central1"
```

```sh
gcloud auth application-default login # 一次性完成认证
kimi
```
指定 `service_account_file` 时,如果未填 `project`,Kimi Code 会自动从服务账号 JSON 中读取 `project_id`。

如需让 Vertex 请求走自定义(如代理)端点,可设置 `base_url`(或 `GOOGLE_VERTEX_BASE_URL` 环境变量)。

## `google-vertex-anthropic`

如需让 Vertex 请求走自定义(如代理)端点,可设置 `base_url`(或 `GOOGLE_VERTEX_BASE_URL` 环境变量);不填时使用 SDK 默认的区域化 `*-aiplatform.googleapis.com` 地址。与 `google-genai` 一样,只填主机根地址——SDK 会自行追加 `/v1beta1/publishers/google/models/…`。
设置 `type = "google-vertex-anthropic"` 连接 Google Vertex AI 托管的 Anthropic Claude 模型。

```toml
[providers.vertex-claude]
type = "google-vertex-anthropic"
service_account_file = "~/.secrets/my-service-account.json"
location = "us-east5"
```

## OAuth 与凭证注入

Expand Down
6 changes: 6 additions & 0 deletions packages/acp-adapter/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,15 @@ function providerHasNonOAuthCredentials(provider: ProviderConfig): boolean {
case 'google-genai':
return hasProviderValue(provider, 'GOOGLE_API_KEY');
case 'vertexai':
case 'google-vertex':
case 'google-vertex-anthropic':
return (
hasProviderValue(provider, 'VERTEXAI_API_KEY') ||
hasEnvValue(provider, 'GOOGLE_API_KEY') ||
hasEnvValue(provider, 'ANTHROPIC_API_KEY') ||
nonEmptyString(provider.serviceAccountFile) !== undefined ||
hasEnvValue(provider, 'GOOGLE_APPLICATION_CREDENTIALS') ||
hasEnvValue(provider, 'SERVICE_ACCOUNT_FILE') ||
(hasEnvValue(provider, 'GOOGLE_CLOUD_PROJECT') &&
(hasEnvValue(provider, 'GOOGLE_CLOUD_LOCATION') ||
vertexAILocationFromBaseUrl(provider.baseUrl) !== undefined))
Expand Down
16 changes: 16 additions & 0 deletions packages/acp-adapter/test/auth-gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,22 @@ describe('AcpServer auth gate', () => {
expect(createCalls).toHaveLength(0);
});

it('accepts Vertex AI service-account file config', async () => {
const { harness, createCalls } = makeHarnessWithConfig(
configuredModelConfig({
type: 'google-vertex',
serviceAccountFile: '~/.secrets/sa.json',
}),
);

const { agentStream, clientStream } = makeInMemoryStreamPair();
startAcpServer(harness, agentStream);
const client = new ClientSideConnection((_a) => new StubClient(), clientStream);

await client.newSession({ cwd: '/tmp/vertexai', mcpServers: [] });
expect(createCalls).toHaveLength(1);
});

it('keeps the OAuth token short-circuit even when config loading fails', async () => {
const createCalls: Array<{ id?: string; workDir: string }> = [];
const harness = {
Expand Down
3 changes: 3 additions & 0 deletions packages/agent-core-v2/docs/config-manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,9 @@ merge_all_available_skills = true
# default_model: string
# type: string
# api_key: string
# service_account_file: string
# project: string
# location: string
# oauth: object
# storage: "file" | "keyring"
# key: string
Expand Down
1 change: 1 addition & 0 deletions packages/agent-core-v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"@antfu/utils": "^9.3.0",
"@anthropic-ai/sdk": "^0.95.2",
"@google/genai": "^1.49.0",
"google-auth-library": "^10.6.2",
"@jsquash/webp": "^1.5.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"@moonshot-ai/kimi-code-oauth": "workspace:^",
Expand Down
3 changes: 3 additions & 0 deletions packages/agent-core-v2/src/app/kosongConfig/configSection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ export const ProviderConfigSchema = z.object({

type: ProviderTypeSchema.optional(),
apiKey: z.string().optional(),
serviceAccountFile: z.string().optional(),
project: z.string().optional(),
location: z.string().optional(),
oauth: OAuthRefSchema.optional(),
env: StringRecordSchema.optional(),
source: z.record(z.string(), z.unknown()).optional(),
Expand Down
57 changes: 49 additions & 8 deletions packages/agent-core-v2/src/kosong/model/catalogService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
matchKnownAnthropicModelProfile,
matchUnknownClaudeProfile,
} from '../provider/bases/anthropic/anthropic-profile';
import { expandHomePath, tryReadProjectIdFromServiceAccount } from '../provider/bases/vertex-utils';
import {
IProviderService,
type ProviderConfig,
Expand Down Expand Up @@ -558,25 +559,43 @@ function buildProtocolProviderOptions(
const options: MutableProtocolProviderOptions = {};

switch (protocol) {
case 'anthropic':
case 'anthropic': {
if (model.maxOutputSize !== undefined) options.defaultMaxTokens = model.maxOutputSize;
if (model.supportEfforts !== undefined) options.supportEfforts = model.supportEfforts;
if (model.adaptiveThinking !== undefined) options.adaptiveThinking = model.adaptiveThinking;
if (model.betaApi !== undefined) options.betaApi = model.betaApi;
const saFile = vertexAIServiceAccountFile(provider);
const project = vertexAIProject(provider, saFile);
const location = vertexAILocation(provider, baseUrl);
const isVertex = provider?.type === 'google-vertex-anthropic' || saFile !== undefined;
if (isVertex) {
options.vertexai = true;
if (saFile !== undefined) options.serviceAccountFile = saFile;
Comment on lines +572 to +573

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Forward Vertex auth options into the protocol adapters

When these providers are configured through kap-server, the catalog records the new options here, but anthropic.contrib.ts forwards none of vertexai, project, location, or serviceAccountFile, while google-genai.contrib.ts forwards everything except serviceAccountFile. Consequently google-vertex-anthropic falls back to native Anthropic authentication, and google-vertex ignores the selected key file and tries ambient ADC instead; pass the new provider options through both protocol-base constructors.

AGENTS.md reference: AGENTS.md:L22-L22

Useful? React with 👍 / 👎.

if (project !== undefined) options.project = project;
if (location !== undefined) options.location = location;
}
break;
}
case 'openai': {
const reasoningKey = nonEmpty(model.reasoningKey);
if (reasoningKey !== undefined) options.reasoningKey = reasoningKey;
if (model.offEffort !== undefined) options.offEffort = model.offEffort;
break;
}
case 'google-genai': {
const project = vertexAIProject(provider);
const saFile = vertexAIServiceAccountFile(provider);
const project = vertexAIProject(provider, saFile);
const location = vertexAILocation(provider, baseUrl);
if (project !== undefined && location !== undefined) {
const isVertex =
provider?.type === 'vertexai' ||
provider?.type === 'google-vertex' ||
saFile !== undefined ||
(project !== undefined && location !== undefined);
if (isVertex) {
options.vertexai = true;
options.project = project;
options.location = location;
if (saFile !== undefined) options.serviceAccountFile = saFile;
if (project !== undefined) options.project = project;
if (location !== undefined) options.location = location;
}
break;
}
Expand Down Expand Up @@ -614,15 +633,37 @@ function profileForAttribution(
return { profile: known, inferred: false };
}

function vertexAIProject(provider: ProviderConfig | undefined): string | undefined {
return envValue(provider?.env, 'GOOGLE_CLOUD_PROJECT');
function vertexAIServiceAccountFile(provider: ProviderConfig | undefined): string | undefined {
const configured =
nonEmpty(provider?.serviceAccountFile) ??
(typeof provider?.source?.['service_account_file'] === 'string'
? nonEmpty(provider.source['service_account_file'] as string)
: undefined);
const rawPath =
configured ??
envValue(provider?.env, 'GOOGLE_APPLICATION_CREDENTIALS') ??
envValue(provider?.env, 'SERVICE_ACCOUNT_FILE');
return expandHomePath(rawPath);
}

function vertexAIProject(provider: ProviderConfig | undefined, saFile?: string): string | undefined {
const saPath = saFile ?? vertexAIServiceAccountFile(provider);
return (
nonEmpty(provider?.project) ??
envValue(provider?.env, 'GOOGLE_CLOUD_PROJECT') ??
tryReadProjectIdFromServiceAccount(saPath)
);
}

function vertexAILocation(
provider: ProviderConfig | undefined,
baseUrl: string | undefined,
): string | undefined {
return envValue(provider?.env, 'GOOGLE_CLOUD_LOCATION') ?? locationFromVertexAIBaseUrl(baseUrl);
return (
nonEmpty(provider?.location) ??
envValue(provider?.env, 'GOOGLE_CLOUD_LOCATION') ??
locationFromVertexAIBaseUrl(baseUrl)
);
}

function envValue(env: Record<string, string> | undefined, key: string): string | undefined {
Expand Down
1 change: 1 addition & 0 deletions packages/agent-core-v2/src/kosong/protocol/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface ProtocolProviderOptions {
readonly vertexai?: boolean;
readonly project?: string;
readonly location?: string;
readonly serviceAccountFile?: string;
}

export interface ProtocolAdapterConfig {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ registerProtocolBase({
adaptiveThinking: config.providerOptions?.adaptiveThinking,
supportEfforts: config.providerOptions?.supportEfforts,
betaApi: config.providerOptions?.betaApi,
vertexai: config.providerOptions?.vertexai,
project: config.providerOptions?.project,
location: config.providerOptions?.location,
serviceAccountFile: config.providerOptions?.serviceAccountFile,
metadata:
config.providerOptions?.metadata === undefined
? undefined
Expand Down
Loading