Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .changeset/google-genai-base-url.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@moonshot-ai/kosong": patch
"@moonshot-ai/agent-core": patch
"kimi-code-docs": patch
---

Honor `base_url` for the `google-genai` and `vertexai` providers. A configured base URL was previously ignored and requests always went to `generativelanguage.googleapis.com`; it is now forwarded to the Google GenAI SDK (with `GOOGLE_GEMINI_BASE_URL` / `GOOGLE_VERTEX_BASE_URL` env fallbacks), so Gemini-compatible proxies and gateways can be used. Give the host root only — the SDK appends the API version segment itself.
13 changes: 13 additions & 0 deletions docs/en/configuration/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,17 @@ type = "google-genai"
api_key = "xxxxx"
```

To route through a Gemini-compatible proxy or gateway, set `base_url` (or the `GOOGLE_GEMINI_BASE_URL` env var); when omitted, the SDK default `https://generativelanguage.googleapis.com` is used.

> Give the **host root only**. The Google GenAI SDK appends the API version and path itself (e.g. `/v1beta/models/<model>:generateContent`), so a trailing `/v1beta` would produce a doubled `/v1beta/v1beta/…`.

```toml
[providers.gemini]
type = "google-genai"
api_key = "xxxxx"
base_url = "https://your-gateway.example"
```

## `vertexai`

Shares the same implementation as `google-genai`; setting `type = "vertexai"` switches to the Vertex AI access path.
Expand All @@ -139,6 +150,8 @@ gcloud auth application-default login # one-time authentication
kimi
```

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.

## OAuth and credential injection

The Kimi Code managed service uses OAuth rather than static API keys. After running `/login`, the built-in authentication toolchain automatically writes and refreshes credentials — no manual configuration is needed in `config.toml` for this.
Expand Down
13 changes: 13 additions & 0 deletions docs/zh/configuration/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,17 @@ type = "google-genai"
api_key = "xxxxx"
```

如需经由兼容 Gemini 协议的代理/网关访问,可设置 `base_url`(或 `GOOGLE_GEMINI_BASE_URL` 环境变量);不填时使用 SDK 默认地址 `https://generativelanguage.googleapis.com`。

> 只填**主机根地址**。Google GenAI SDK 会自行追加 API 版本与路径(如 `/v1beta/models/<model>:generateContent`),所以结尾带 `/v1beta` 会导致路径重复成 `/v1beta/v1beta/…`。

```toml
[providers.gemini]
type = "google-genai"
api_key = "xxxxx"
base_url = "https://your-gateway.example"
```

## `vertexai`

与 `google-genai` 共用实现,`type = "vertexai"` 时切换到 Vertex AI 访问路径。
Expand All @@ -139,6 +150,8 @@ gcloud auth application-default login # 一次性完成认证
kimi
```

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

## OAuth 与凭证注入

Kimi Code 托管服务使用 OAuth 而非静态 API 密钥。运行 `/login` 后,内置的认证工具链会自动写入并刷新凭证,`config.toml` 里无需手动配置这部分内容。
Expand Down
26 changes: 17 additions & 9 deletions packages/agent-core/src/session/provider-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ function toKosongProviderConfig(
return {
type: 'google-genai',
model,
baseUrl: providerValue(provider.baseUrl, provider.env, 'GOOGLE_GEMINI_BASE_URL'),
apiKey: providerApiKey(provider),
...defaultHeadersField({
...envCustomHeaders,
Expand All @@ -329,14 +330,21 @@ function toKosongProviderConfig(
}),
};
case 'vertexai': {
const useServiceAccount = hasVertexAIServiceEnv(provider);
// Resolve the effective endpoint once (config `base_url` or the
// GOOGLE_VERTEX_BASE_URL env fallback) and use it for BOTH forwarding and
// location detection, so the env fallback behaves exactly like
// `base_url` — including deriving the region from an
// `*-aiplatform.googleapis.com` host for the service-account path.
const baseUrl = providerValue(provider.baseUrl, provider.env, 'GOOGLE_VERTEX_BASE_URL');
const useServiceAccount = hasVertexAIServiceEnv(provider, baseUrl);
return {
type: 'vertexai',
model,
vertexai: useServiceAccount,
baseUrl,
apiKey: useServiceAccount ? undefined : providerApiKey(provider),
project: vertexAIProject(provider),
location: vertexAILocation(provider),
location: vertexAILocation(provider, baseUrl),
...defaultHeadersField({
...envCustomHeaders,
...kimiUserAgentHeader(kimiRequestHeaders),
Expand Down Expand Up @@ -404,19 +412,19 @@ function providerApiKey(provider: ProviderConfig): string | undefined {
}
}

function hasVertexAIServiceEnv(provider: ProviderConfig): boolean {
return vertexAIProject(provider) !== undefined && vertexAILocation(provider) !== undefined;
function hasVertexAIServiceEnv(provider: ProviderConfig, baseUrl: string | undefined): boolean {
return vertexAIProject(provider) !== undefined && vertexAILocation(provider, baseUrl) !== undefined;
}

function vertexAIProject(provider: ProviderConfig): string | undefined {
return envValue(provider.env, 'GOOGLE_CLOUD_PROJECT');
}

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

function providerValue(
Expand Down
133 changes: 133 additions & 0 deletions packages/agent-core/test/harness/runtime-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,139 @@ describe('resolveThinkingEffort', () => {
});
});

describe('google base URL forwarding', () => {
it('forwards base_url to the google-genai provider config', () => {
const resolved = resolveRuntimeProvider({
config: {
defaultModel: 'gemini',
providers: {
gemini: {
type: 'google-genai',
apiKey: 'g-key',
baseUrl: 'https://qianxun.example/v1beta',
},
},
models: {
gemini: { provider: 'gemini', model: 'gemini-2.5-pro', maxContextSize: 1_000_000 },
},
},
});

expect(resolved.provider).toMatchObject({
type: 'google-genai',
model: 'gemini-2.5-pro',
baseUrl: 'https://qianxun.example/v1beta',
});
});

it('reads GOOGLE_GEMINI_BASE_URL from provider env as a fallback', () => {
const resolved = resolveRuntimeProvider({
config: {
defaultModel: 'gemini',
providers: {
gemini: {
type: 'google-genai',
apiKey: 'g-key',
env: { GOOGLE_GEMINI_BASE_URL: 'https://env.example/v1beta' },
},
},
models: {
gemini: { provider: 'gemini', model: 'gemini-2.5-pro', maxContextSize: 1_000_000 },
},
},
});

expect(resolved.provider).toMatchObject({
type: 'google-genai',
baseUrl: 'https://env.example/v1beta',
});
});

it('forwards a custom proxy base_url to the vertexai provider config', () => {
const resolved = resolveRuntimeProvider({
config: {
defaultModel: 'gemini',
providers: {
vertex: {
type: 'vertexai',
apiKey: 'v-key',
baseUrl: 'https://qianxun.example/vertex',
},
},
models: {
gemini: { provider: 'vertex', model: 'gemini-1.5-pro', maxContextSize: 1_000_000 },
},
},
});

expect(resolved.provider).toMatchObject({
type: 'vertexai',
model: 'gemini-1.5-pro',
baseUrl: 'https://qianxun.example/vertex',
});
});

it('forwards base_url to vertexai while still deriving location from an aiplatform host', () => {
// Backward compatibility: an aiplatform host must keep populating `location`
// (existing GCP behavior) while the base URL is now also forwarded so the
// SDK targets the configured endpoint verbatim.
const resolved = resolveRuntimeProvider({
config: {
defaultModel: 'gemini',
providers: {
vertex: {
type: 'vertexai',
apiKey: 'v-key',
baseUrl: 'https://us-central1-aiplatform.googleapis.com',
},
},
models: {
gemini: { provider: 'vertex', model: 'gemini-1.5-pro', maxContextSize: 1_000_000 },
},
},
});

expect(resolved.provider).toMatchObject({
type: 'vertexai',
baseUrl: 'https://us-central1-aiplatform.googleapis.com',
location: 'us-central1',
});
});

it('derives vertex location from the GOOGLE_VERTEX_BASE_URL env fallback so ADC mode is selected', () => {
// The env fallback must behave exactly like config `base_url`: when the
// regional endpoint is supplied via GOOGLE_VERTEX_BASE_URL (with a project
// but no explicit GOOGLE_CLOUD_LOCATION), location derivation must still see
// it, so the provider resolves to service-account (ADC) mode rather than
// silently downgrading to API-key Gemini routing.
const resolved = resolveRuntimeProvider({
config: {
defaultModel: 'gemini',
providers: {
vertex: {
type: 'vertexai',
env: {
GOOGLE_CLOUD_PROJECT: 'my-proj',
GOOGLE_VERTEX_BASE_URL: 'https://us-central1-aiplatform.googleapis.com',
},
},
},
models: {
gemini: { provider: 'vertex', model: 'gemini-1.5-pro', maxContextSize: 1_000_000 },
},
},
});

expect(resolved.provider).toMatchObject({
type: 'vertexai',
vertexai: true,
baseUrl: 'https://us-central1-aiplatform.googleapis.com',
project: 'my-proj',
location: 'us-central1',
});
});
});

describe('per-model protocol routing', () => {
it('routes a protocol:anthropic model on a kimi provider through the anthropic transport with the REST base stripped of /v1', () => {
const resolved = resolveRuntimeProvider({
Expand Down
32 changes: 25 additions & 7 deletions packages/kosong/src/providers/google-genai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ function normalizeGoogleGenAIFinishReason(raw: unknown): {
export interface GoogleGenAIOptions {
apiKey?: string | undefined;
model: string;
/**
* Override the endpoint the SDK talks to (forwarded as
* `httpOptions.baseUrl`). When unset, the SDK falls back to its default
* (`generativelanguage.googleapis.com` for Gemini, the regional
* `*-aiplatform.googleapis.com` for Vertex). Set this to route through a
* Gemini-compatible proxy/gateway.
*/
baseUrl?: string | undefined;
vertexai?: boolean | undefined;
project?: string | undefined;
location?: string | undefined;
Expand Down Expand Up @@ -682,6 +690,7 @@ export class GoogleGenAIChatProvider implements ChatProvider {
private _vertexai: boolean;
private _stream: boolean;
private _apiKey: string | undefined;
private _baseUrl: string | undefined;
private _project: string | undefined;
private _location: string | undefined;
private _defaultHeaders: Record<string, string> | undefined;
Expand All @@ -695,6 +704,8 @@ export class GoogleGenAIChatProvider implements ChatProvider {

const apiKey = options.apiKey ?? process.env['GOOGLE_API_KEY'];
this._apiKey = apiKey === undefined || apiKey.length === 0 ? undefined : apiKey;
this._baseUrl =
options.baseUrl === undefined || options.baseUrl.length === 0 ? undefined : options.baseUrl;
this._project = options.project;
this._location = options.location;
this._defaultHeaders = options.defaultHeaders;
Expand All @@ -704,6 +715,19 @@ export class GoogleGenAIChatProvider implements ChatProvider {
}

private _buildClient(apiKey: string | undefined): GenAIClient {
// The Google GenAI SDK reads the endpoint and headers from `httpOptions`,
// deep-merging them over its defaults: a `baseUrl` here overrides the
// default host (`generativelanguage.googleapis.com` / Vertex regional),
// and a `User-Agent` overrides the SDK default (`google-genai-sdk/<ver> …`)
// while preserving the other default headers (`x-goog-api-client`,
// `Content-Type`). Build the object once so both can coexist.
const httpOptions: { headers?: Record<string, string>; baseUrl?: string } = {};
if (this._defaultHeaders !== undefined) {
httpOptions.headers = this._defaultHeaders;
}
if (this._baseUrl !== undefined) {
httpOptions.baseUrl = this._baseUrl;
}
return new GenAIClient({
apiKey,
...(this._vertexai
Expand All @@ -713,13 +737,7 @@ export class GoogleGenAIChatProvider implements ChatProvider {
location: this._location,
}
: {}),
// The Google GenAI SDK deep-merges `httpOptions.headers` into its
// default request headers, so a `User-Agent` here overrides the SDK
// default (`google-genai-sdk/<ver> …`) while preserving the other
// defaults (`x-goog-api-client`, `Content-Type`).
...(this._defaultHeaders !== undefined
? { httpOptions: { headers: this._defaultHeaders } }
: {}),
...(Object.keys(httpOptions).length > 0 ? { httpOptions } : {}),
});
}

Expand Down
Loading
Loading