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
5 changes: 5 additions & 0 deletions .changeset/fallback-bundled-model-capabilities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix capability detection for configured models by using bundled catalog metadata, with an explicit catalog provider override for compatible endpoints.
1 change: 1 addition & 0 deletions docs/en/configuration/config-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ Each entry in the `providers` table defines an API provider, keyed by a unique n
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `type` | `string` | Yes | Provider type: `kimi`, `anthropic`, `openai`, `openai_responses`, `google-genai`, `vertexai` |
| `catalog_provider` | `string` | No | Exact provider id in the bundled [models.dev](https://models.dev/) snapshot used for capability metadata. It does not change the API protocol selected by `type`. Usually written automatically by `/provider`; set it by hand for an OpenAI-compatible third-party provider such as `deepseek` |
| `api_key` | `string` | No | API key, written in plain text in the config file |
| `base_url` | `string` | No | API base URL |
| `oauth` | `table` | No | OAuth credential reference (`storage` and `key` fields); injected automatically by the login flow — normally no need to write this by hand |
Expand Down
21 changes: 19 additions & 2 deletions docs/en/configuration/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The `type` field in the `providers` table determines which protocol implementati
| `google-genai` | Google GenAI | Gemini API |
| `vertexai` | Google GenAI on Vertex | Google Cloud Vertex AI |

All providers communicate with models in streaming mode by default. Capabilities such as thinking, vision, and tool use are matched automatically by model name prefix — you typically do not need to declare them manually.
All providers communicate with models in streaming mode by default. Kimi Code first reads capability metadata from its bundled models.dev snapshot, then falls back to built-in model matchers. For an official endpoint, `type` selects the corresponding catalog provider automatically. For a third-party endpoint that only shares a protocol, set `catalog_provider` explicitly; `type` continues to control the API protocol.

**Credential priority**: `api_key` direct field > `[providers.<name>.env]` sub-table key > if both are absent, startup fails with an error. The CLI does not fall back to shell environment variables for credentials — see [Config overrides: provider credentials](./overrides.md#provider-credentials).

Expand Down Expand Up @@ -59,7 +59,7 @@ api_key = "sk-xxxxx"

## `anthropic`

For connecting to the Claude API. Standard Claude models automatically enable vision, tool use, and Thinking (where supported); custom or uncovered models need `capabilities` declared explicitly on `[models.<alias>]`.
For connecting to the Claude API. The default `anthropic` catalog identity lets Kimi Code auto-detect vision, tool call, and Thinking capabilities from the bundled models.dev snapshot before falling back to its built-in model matchers. Only custom models that neither source covers need `capabilities` declared explicitly on `[models.<alias>]`.

- Default `base_url`: follows Anthropic SDK default
- Credential key names: `ANTHROPIC_API_KEY`, `ANTHROPIC_BASE_URL`
Expand Down Expand Up @@ -93,6 +93,23 @@ base_url = "https://api.openai.com/v1"
api_key = "sk-xxxxx"
```

For an OpenAI-compatible third-party endpoint, keep `type = "openai"` because it controls the wire protocol, and set `catalog_provider` to the exact models.dev provider id used for capability metadata:

```toml
[providers.deepseek]
type = "openai"
catalog_provider = "deepseek"
base_url = "https://api.deepseek.com"
api_key = "sk-xxxxx"

[models."deepseek/v4-pro"]
provider = "deepseek"
model = "deepseek-v4-pro"
max_context_size = 1000000
```

Kimi Code looks up the configured `model` within the selected catalog provider. It does not guess a vendor from the local provider name or scan unrelated providers for a matching model id. If a gateway replaces the canonical model id with an arbitrary deployment alias, declare capabilities explicitly instead.

## `openai_responses`

Corresponds to OpenAI's newer Responses API, always operating in streaming mode. Configuration is the same as `openai`.
Expand Down
1 change: 1 addition & 0 deletions docs/zh/configuration/config-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ timeout = 5
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `type` | `string` | 是 | 供应商类型:`kimi`、`anthropic`、`openai`、`openai_responses`、`google-genai`、`vertexai` |
| `catalog_provider` | `string` | 否 | 随 CLI 发布的 [models.dev](https://models.dev/) 快照中的精确供应商 ID,用于读取能力元数据,不会改变 `type` 选择的 API 协议。通常由 `/provider` 自动写入;手动配置 DeepSeek 等 OpenAI 兼容供应商时可显式设置,例如 `deepseek` |
| `api_key` | `string` | 否 | API 密钥,明文写在配置文件里 |
| `base_url` | `string` | 否 | API 基础 URL |
| `oauth` | `table` | 否 | OAuth 凭据引用(`storage`、`key` 两个字段),由登录流程自动注入,通常无需手写 |
Expand Down
21 changes: 19 additions & 2 deletions docs/zh/configuration/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Kimi Code CLI 支持同时接入多家 LLM 平台——用 Kimi Code 托管服
| `google-genai` | Google GenAI | Gemini API |
| `vertexai` | Google GenAI on Vertex | Google Cloud Vertex AI |

所有供应商默认以流式方式与模型交互。thinking、视觉、工具调用等能力按模型名前缀自动匹配,通常不需要手动声明
所有供应商默认以流式方式与模型交互。Kimi Code 会先从随 CLI 发布的 models.dev 快照读取能力元数据,再回退到内置模型匹配规则。对官方端点,`type` 会自动选择对应的目录供应商;对只是共用协议的第三方端点,需要显式设置 `catalog_provider`,而 `type` 仍只负责选择 API 协议

**凭证优先级**:`api_key` 直接字段 > `[providers.<name>.env]` 子表键 > 两者都缺时启动报错。CLI 不会从 shell 环境变量自动取凭证——详见[配置覆盖:供应商凭证](./overrides.md#供应商凭证)。

Expand Down Expand Up @@ -59,7 +59,7 @@ api_key = "sk-xxxxx"

## `anthropic`

用于对接 Claude API。标准 Claude 模型自动启用视觉、工具调用及 Thinking(如支持);自定义或未覆盖的模型需在 `[models.<alias>]` 里显式声明 `capabilities`。
用于对接 Claude API。默认的 `anthropic` 目录身份让 Kimi Code 先从随 CLI 发布的 models.dev 快照中自动识别视觉、工具调用及 Thinking 能力,再回退到内置模型匹配规则;只有两者都无法覆盖的自定义模型,才需要在 `[models.<alias>]` 里显式声明 `capabilities`。

- 默认 `base_url`:跟随 Anthropic SDK 默认值
- 凭证键名:`ANTHROPIC_API_KEY`、`ANTHROPIC_BASE_URL`
Expand Down Expand Up @@ -93,6 +93,23 @@ base_url = "https://api.openai.com/v1"
api_key = "sk-xxxxx"
```

配置 OpenAI 兼容的第三方端点时,保留 `type = "openai"`,因为它决定在线协议;同时把 `catalog_provider` 设为 models.dev 中用于读取能力元数据的精确供应商 ID:

```toml
[providers.deepseek]
type = "openai"
catalog_provider = "deepseek"
base_url = "https://api.deepseek.com"
api_key = "sk-xxxxx"

[models."deepseek/v4-pro"]
provider = "deepseek"
model = "deepseek-v4-pro"
max_context_size = 1000000
```

Kimi Code 会在指定的目录供应商中查找配置的 `model`。它不会根据本地供应商名称猜厂商,也不会扫描其他供应商寻找同名模型。如果网关把规范模型 ID 换成任意部署别名,请改为显式声明能力。

## `openai_responses`

对应 OpenAI 较新的 Responses API,始终以流式方式工作。配置方式与 `openai` 相同。
Expand Down
1 change: 1 addition & 0 deletions packages/agent-core-v2/docs/config-manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ merge_all_available_skills = true
# base_url: string
# custom_headers: record<string, string>
# default_model: string
# catalog_provider: string
# type: string
# api_key: string
# oauth: object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export const ProviderConfigSchema = z.object({
baseUrl: z.string().optional(),
customHeaders: StringRecordSchema.optional(),
defaultModel: z.string().optional(),
catalogProvider: z.string().min(1).optional(),

type: ProviderTypeSchema.optional(),
apiKey: z.string().optional(),
Expand Down
77 changes: 76 additions & 1 deletion packages/agent-core-v2/src/app/kosongConfig/modelsDev.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/**
* `kosongConfig` domain (L3) — the third-party models.dev directory: its
* api.json schema mirrored as types, plus the normalization that turns a
* directory entry into an import decision.
* directory entry into an import decision or resolves one model's capability
* from a bundled snapshot.
*
* models.dev is an EXTERNAL schema that evolves on its own, so its mirror
* lives here in the app layer, NOT in kosong — kosong's type surface stays
Expand Down Expand Up @@ -123,6 +124,15 @@ const KNOWN_WIRE_TYPES = [
/** The enumerated subset of {@link ProviderType} the models.dev import knows. */
type KnownWireType = (typeof KNOWN_WIRE_TYPES)[number];

const DEFAULT_MODELS_DEV_PROVIDER_BY_TYPE: Readonly<Record<string, string>> = {
anthropic: 'anthropic',
openai: 'openai',
openai_responses: 'openai',
'google-genai': 'google',
vertexai: 'google-vertex',
kimi: 'moonshotai',
};

function isWireType(value: unknown): value is KnownWireType {
return typeof value === 'string' && (KNOWN_WIRE_TYPES as readonly string[]).includes(value);
}
Expand Down Expand Up @@ -444,6 +454,71 @@ export function modelsDevProviderModels(entry: ModelsDevProviderEntry): ModelsDe
});
}

function normalizeModelsDevPlatformKey(key: string): string {
return key
.toLowerCase()
.replace(/^(?:us|eu|apac|global)\./, '')
.replace(/^(?:anthropic|ai21|amazon|cohere|deepseek|meta|mistral|writer)\./, '')
.replace(/-v\d+(?::\d+)?$/, '');
}

function normalizeModelsDevDeploymentKey(key: string): string {
return normalizeModelsDevPlatformKey(key).replace(/@.*$/, '');
}

function normalizeModelsDevFamilyKey(key: string): string {
return normalizeModelsDevDeploymentKey(key)
.replace(/-\d{8}$/, '')
.replace(/-\d{4}-\d{2}-\d{2}$/, '');
}

export interface ModelsDevCapabilityMatch {
readonly capability: ModelCapability;
readonly providerId: string;
}

export function resolveModelsDevCapabilityProvider(query: {
readonly protocol: ProviderType;
readonly providerType?: string;
readonly catalogProvider?: string;
readonly vertexai?: boolean;
}): string | undefined {
return (
query.catalogProvider ??
(query.vertexai === true
? 'google-vertex'
: DEFAULT_MODELS_DEV_PROVIDER_BY_TYPE[query.providerType ?? query.protocol])
);
}

export function getModelsDevModelCapability(
catalog: ModelsDevCatalog | undefined,
providerId: string,
modelName: string,
): ModelsDevCapabilityMatch | undefined {
if (catalog === undefined) return undefined;
const models = catalog[providerId]?.models;
if (models === undefined) return undefined;
const exact = models[modelName] ?? models[modelName.toLowerCase()];
if (exact !== undefined) {
const model = modelsDevModelToCapability(exact);
if (model !== undefined) return { capability: model.capability, providerId };
}
for (const normalize of [
normalizeModelsDevPlatformKey,
normalizeModelsDevDeploymentKey,
normalizeModelsDevFamilyKey,
]) {
const target = normalize(modelName);
for (const [key, entry] of Object.entries(models)) {
if (normalize(key) !== target) continue;
const model = modelsDevModelToCapability(entry);
if (model !== undefined) return { capability: model.capability, providerId };
}
}
return undefined;
}

/**
* Gateway providers (zenmux, opencode, azure, …) may declare a per-model
* `provider` override when a model is served over a different protocol or
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* `app/kosongConfig` domain (L3) — contributes the bundled models.dev
* snapshot as a capability source without making the L2 provider domain
* depend on app configuration. The multi-megabyte snapshot is parsed lazily
* on the first lookup and never refreshed from the network.
*/

import { registerModelCapabilityResolver } from '#/kosong/provider/modelCapabilityResolver';

import { BUILT_IN_MODELS_DEV_JSON } from './builtInModelsDev';
import {
getModelsDevModelCapability,
type ModelsDevCatalog,
resolveModelsDevCapabilityProvider,
} from './modelsDev';
import { loadBuiltInModelsDevCatalog } from './modelsDevUpstream';

let catalog: ModelsDevCatalog | undefined | null = null;

registerModelCapabilityResolver((query) => {
if (catalog === null) {
catalog = loadBuiltInModelsDevCatalog(BUILT_IN_MODELS_DEV_JSON);
}
const providerId = resolveModelsDevCapabilityProvider({
protocol: query.protocol,
providerType: query.providerType,
catalogProvider: query.catalogProvider,
vertexai: query.providerOptions?.vertexai,
});
if (providerId === undefined) return undefined;
const match = getModelsDevModelCapability(catalog, providerId, query.modelName);
if (match === undefined) return undefined;
return {
capability: match.capability,
source: {
kind: 'builtin',
detail: `models.dev built-in snapshot provider '${match.providerId}'`,
},
};
});
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,12 @@ export class ModelsDevImportService implements IModelsDevImportService {
// `undefined` when the wire needs none, so a stale on-disk value is
// really cleared. The global default pointers are deliberately left
// alone.
const provider: ProviderConfig = { type: resolution.wire };
provider.baseUrl = resolution.baseUrl;
provider.apiKey = options.apiKey ?? existing?.apiKey;
const provider: ProviderConfig = {
type: resolution.wire,
catalogProvider: catalogId,
baseUrl: resolution.baseUrl,
apiKey: options.apiKey ?? existing?.apiKey,
};
await config.replace(PROVIDERS_SECTION, { ...providers, [targetId]: provider });

// Two-pass alias swap: pass 1 drops the provider's aliases for real
Expand Down
1 change: 1 addition & 0 deletions packages/agent-core-v2/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export * from '#/kosong/protocol/protocolBase';
export * from '#/kosong/protocol/protocolTrait';
import '#/app/kosongConfig/envOverlay';
import '#/app/kosongConfig/secondaryModelOverlay';
import '#/app/kosongConfig/modelsDevCapability.contrib';
export * from '#/kosong/model/completionBudget';
export * from '#/kosong/model/hostRequestHeaders';
export * from '#/kosong/model/model';
Expand Down
20 changes: 12 additions & 8 deletions packages/agent-core-v2/src/kosong/model/catalogService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
* referenced provider vendor's declared `baseProtocol`; endpoint and
* credential env fallbacks resolve through `resolveProviderEndpoint` against
* the config env bag; host-header forwarding follows the vendor definition's
* `hostHeaders`; capability detection is `resolveCapability(protocol, name,
* providerType)`.
* `hostHeaders`; capability detection receives the resolved provider mode and
* explicit models.dev provider identity separately from wire identity.
*
* Caching (load-bearing): assembled entries are invalidated ONLY by the
* model/provider config-change events. Tests that mutate config
Expand Down Expand Up @@ -379,10 +379,20 @@ export class ModelCatalog extends Disposable implements IModelCatalog {
);
}

const providerOptions = buildProtocolProviderOptions(
model,
protocol,
providerConfig,
resolvedBaseUrl,
);
const explainedCapability = this.protocolRegistry.explainCapability(
protocol,
wireName,
providerType,
{
catalogProvider: providerConfig?.catalogProvider,
providerOptions,
},
);
trace.capture(TRACE.detectedCapability, explainedCapability.capability);
trace.capture(TRACE.capabilitySource, explainedCapability.source);
Expand All @@ -392,12 +402,6 @@ export class ModelCatalog extends Disposable implements IModelCatalog {
model.maxContextSize,
model.maxInputSize,
);
const providerOptions = buildProtocolProviderOptions(
model,
protocol,
providerConfig,
resolvedBaseUrl,
);
if (providerOptions !== undefined) {
attributeProviderOptions(trace, providerOptions, providerConfig?.env);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core-v2/src/kosong/model/inspection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ function attributeCapabilities(
);
continue;
}
if (detected?.[key] === true) {
if (detected !== undefined && detectedSource.kind !== 'none') {
sources.set(path, detectedSource);
continue;
}
Expand Down
19 changes: 12 additions & 7 deletions packages/agent-core-v2/src/kosong/protocol/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ export interface ExplainedCapability {
readonly source: InspectionSource;
}

export interface CapabilityResolutionContext {
readonly catalogProvider?: string;
readonly providerOptions?: ProtocolProviderOptions;
}

export interface IProtocolAdapterRegistry {
readonly _serviceBrand: undefined;

Expand Down Expand Up @@ -120,27 +125,27 @@ export interface IProtocolAdapterRegistry {
resolveProviderBaseId(protocol: Protocol, providerType?: string): ProtocolBaseId;

/**
* Capability resolution with the fixed fallback chain: pair definition's
* declared capability → trait `capability` hooks (last declarer wins) →
* the base's own catalog. `UNKNOWN_CAPABILITY` when nothing knows the
* model.
* Capability resolution with the fixed fallback chain: trait `capability`
* hooks (last declarer wins) → registered external sources → the base's own
* catalog. `UNKNOWN_CAPABILITY` when nothing knows the model.
*/
resolveCapability(
protocol: Protocol,
modelName: string,
providerType?: string,
context?: CapabilityResolutionContext,
): ModelCapability;

/**
* The provenance-preserving twin of `resolveCapability` — the same chain,
* but reports which level answered (definition / trait / base / none), so
* inspection views can attribute a detected capability instead of just
* serving it.
* but reports which source answered, so inspection views can attribute a
* detected capability instead of just serving it.
*/
explainCapability(
protocol: Protocol,
modelName: string,
providerType?: string,
context?: CapabilityResolutionContext,
): ExplainedCapability;

/**
Expand Down
Loading