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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
- **统一控制台**:管理账号、模型、客户端密钥、代理设置,查看额度和请求日志。
- **Docker 运维**:持久化账号数据;可选安装宿主机更新器,在控制台下载更新、确认升级和回滚。

Qoder 国内版、WorkBuddy、Trae 的适配代码已实现,真账号验收仍未完成;Devin 与 Command Code 为实验性接入,不承诺生产可用。托管更新的真实升级 / 回滚验收也尚未完成。
Qoder 国内版、WorkBuddy、Trae 的适配代码已实现,真账号验收仍未完成;Devin 与 Command Code 为实验性接入,不承诺生产可用。托管更新的真实升级 / 回滚验收也尚未完成。Kimi Code(中国区)已完成单账号端到端验收:设备码登录、模型发现、三种接口的对话 / 流式 / 工具调用 / 视觉,以及额度读取;多账号行为只有自动化测试覆盖。

## 快速开始

Expand Down
2 changes: 1 addition & 1 deletion README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Deploy with Docker, manage accounts in the web console, and connect clients thro
- **Web console**: manage accounts, models, client keys, and proxies; view quotas and request logs.
- **Docker operations**: persistent account data; an optional host updater lets you download, confirm, and roll back updates from the console.

Qoder CN, WorkBuddy, and Trae adapters are implemented, but live-account acceptance is still pending. Devin and Command Code are experimental, not production-ready. Live upgrade and rollback acceptance is also pending.
Qoder CN, WorkBuddy, and Trae adapters are implemented, but live-account acceptance is still pending. Devin and Command Code are experimental, not production-ready. Live upgrade and rollback acceptance is also pending. The Kimi Code CN adapter has single-account end-to-end acceptance: device-code sign-in, model discovery, chat/streaming/tools/vision on all three APIs, and quota readout; multi-account behavior is covered by automated tests only.

## Quick start

Expand Down
13 changes: 13 additions & 0 deletions changelog/unreleased/kimi-code-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
### English

- Add the China-region Kimi Code provider: sign in with the official device-code flow (or import the credential the Kimi Code client already stored), discover the subscription's real model list with its context window, thinking efforts, vision and tool support, and serve `/v1/chat/completions`, `/v1/responses` and `/v1/messages` from it.
- Kimi Code accounts show the four subscription usage windows (5-hour, 7-day, monthly, monthly-code) in the console, refresh their access token automatically, and can be routed alongside every other provider in the account pool.
- Kimi Code models accept a console reasoning default on the Providers page; a request that does not specify a level uses it, then the model's own default.
- Kimi Code is import-only: import the credential the client already stored, but export is not offered, because the credential is a bearer token pair and exporting it would hand out the account.

### 中文

- 新增中国区 Kimi Code Provider:用官方设备码授权登录(也可导入 Kimi Code 客户端已保存的凭证),自动发现该订阅真实可用的模型,以及每个模型的上下文、Thinking 档位、视觉和工具支持,并通过它提供 `/v1/chat/completions`、`/v1/responses` 和 `/v1/messages`。
- Kimi Code 账号会在控制台显示四个订阅额度窗口(5 小时、7 天、月度、月度 Code),自动刷新访问令牌,并和其他供应商一样进入账号池参与调度。
- Kimi Code 模型可在「供应商」页设置控制台默认思考强度:请求未指定档位时先用它,再用模型自身的默认档位。
- Kimi Code 只支持导入、不支持导出:可以导入客户端已保存的凭证,但控制台不提供导出,因为凭证是一对 bearer token,导出等于把账号交出去。
9 changes: 5 additions & 4 deletions deploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,11 @@ Messages / Responses are stateless compatibility APIs, not full official API
implementations. They do not store server-side conversations or execute
upstream-specific tools. Image input follows the upstream model catalog:
WorkBuddy's `supportsImages` models (DeepSeek V4 / V4.1, GLM, Kimi, and
others), Qoder's vision models, QwenWork `is_vl` models, experimental Devin
base64 data URLs, and Codex image modalities all pass through, and each
model's declared capability is exposed as `supports_images` on `/v1/models`
and `/api/models`. Trae, Command, and Zhipu do not declare image input.
others), Qoder's vision models, QwenWork `is_vl` models, Kimi Code models that
declare image input, experimental Devin base64 data URLs, and Codex image
modalities all pass through, and each model's declared capability is exposed as
`supports_images` on `/v1/models` and `/api/models`. Trae, Command, and Zhipu do
not declare image input.

## 6. Managed updates (optional)

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/api/overview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,11 @@ export function updateProviderMaxMode(provider: string, modelKey: string, maxMod
})
}

export function updateProviderReasoning(provider: 'trae' | 'workbuddy', modelKey: string, reasoningEffort: string) {
export function updateProviderReasoning(provider: 'trae' | 'workbuddy' | 'kimi', modelKey: string, reasoningEffort: string) {
return api<{
model: string
provider: string
max_mode: boolean
max_mode?: boolean
reasoning_effort?: string
context_custom: boolean
}>(`/api/models/${provider}/${encodeURIComponent(modelKey)}`, {
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/components/AddAccountModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const labelKeys: Record<string, string> = {
'zhipu-cn': 'accountTypeZhipuCN',
'qwenwork-global': 'accountTypeQwenworkGlobal',
'qwenwork-cn': 'accountTypeQwenworkCN',
'kimi-cn': 'accountTypeKimiCN',
}

const hintKeys: Record<string, string> = {
Expand All @@ -78,6 +79,7 @@ const hintKeys: Record<string, string> = {
'zhipu-cn': 'accountTypeZhipuCNHint',
'qwenwork-global': 'accountTypeQwenworkGlobalHint',
'qwenwork-cn': 'accountTypeQwenworkCNHint',
'kimi-cn': 'accountTypeKimiCNHint',
}

function AccountTypeSkeleton({ ariaLabel }: { ariaLabel: string }) {
Expand Down Expand Up @@ -240,7 +242,11 @@ export function AddAccountModal({ isOpen, onClose, onAdded }: Props) {
return {
max_inflight: parsedMaxInFlight(),
priority: parsedPriority(),
drop_system_prompt: showDropSystem ? dropSystemPrompt : true,
// WorkBuddy exposes the switch because upstream content screening rejects
// third-party system prompts. Kimi Code has no such screening, so a new
// Kimi account keeps caller system prompts. Every other provider keeps the
// historical drop=true default.
drop_system_prompt: showDropSystem ? dropSystemPrompt : activeOption?.provider !== 'kimi',
proxy_url: proxyUrl.trim(),
}
}
Expand Down
23 changes: 23 additions & 0 deletions frontend/src/components/KimiMark.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
type Props = {
size?: number
className?: string
}

// Kimi crescent mark. Kimi's own glyph is a crescent, so the console reuses that
// silhouette instead of inventing a letterform for the provider row.
export function KimiMark({ size = 16, className = '' }: Props) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
className={`block shrink-0 text-foreground ${className}`.trim()}
aria-hidden="true"
>
<path
fill="currentColor"
d="M21 12.79A9 9 0 1 1 11.21 3a7 7 0 0 0 9.79 9.79Z"
/>
</svg>
)
}
4 changes: 4 additions & 0 deletions frontend/src/components/ProviderMark.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CommandMark } from '@/components/CommandMark'
import { CodexMark } from '@/components/CodexMark'
import { DevinMark } from '@/components/DevinMark'
import { KimiMark } from '@/components/KimiMark'
import { QoderMark } from '@/components/QoderMark'
import { QwenworkMark } from '@/components/QwenworkMark'
import { TraeMark } from '@/components/TraeMark'
Expand Down Expand Up @@ -37,5 +38,8 @@ export function ProviderMark({ provider, size = 16, className = '' }: Props) {
if (id === 'qwenwork') {
return <QwenworkMark size={size} className={className} />
}
if (id === 'kimi') {
return <KimiMark size={size} className={className} />
}
return <QoderMark size={size} className={className} />
}
8 changes: 6 additions & 2 deletions frontend/src/i18n/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const messages: Record<Lang, Dict> = {
routesHint: 'OpenAI / Anthropic-compatible routes',
clientConfig: 'Client configuration',
clientConfigHint: 'Use the same console key as Bearer token',
providerPrefixHint: 'Pin a provider with a qoder/, workbuddy/, trae/, devin/, command/, zhipu/, or qwenwork/ model prefix. A named API key can also limit which providers are listed and routed. Bare IDs follow the cross-provider pool setting on System.',
providerPrefixHint: 'Pin a provider with a qoder/, workbuddy/, trae/, devin/, command/, codex/, zhipu/, qwenwork/, or kimi/ model prefix. A named API key can also limit which providers are listed and routed. Bare IDs follow the cross-provider pool setting on System.',
authentication: 'Authentication',
curlExample: 'curl example',
quickTestHint: 'Send a real request through the selected account',
Expand Down Expand Up @@ -792,6 +792,8 @@ export const messages: Record<Lang, Dict> = {
accountTypeQwenworkGlobalHint: 'QwenWork international (qwenwork.ai) via browser OAuth or imported credential. The authorization link is single-use: open it directly in a browser already signed in to the target account, and do not send it through chat tools.',
accountTypeQwenworkCN: 'QwenWork CN',
accountTypeQwenworkCNHint: '千问办公 China (qwenwork.cn) via browser OAuth or imported credential. The authorization link is single-use: open it directly in a browser already signed in to the target account, and do not send it through chat tools. The CN region has not yet been verified with a real account.',
accountTypeKimiCN: 'Kimi Code CN',
accountTypeKimiCNHint: 'Kimi Code China subscription via device-code sign-in against auth.kimi.com. Uses the same OAuth client as the official CLI, so the login you already completed with Kimi Code can be pasted in as its credential JSON.',
addAccountDesc: 'Create an isolated account session. Each account keeps its own runtime home.',
tabBrowser: 'Browser login',
tabPat: 'PAT',
Expand Down Expand Up @@ -942,7 +944,7 @@ export const messages: Record<Lang, Dict> = {
routesHint: 'OpenAI / Anthropic 兼容端点',
clientConfig: '客户端配置',
clientConfigHint: '使用与控制台相同的密钥作为 Bearer Token',
providerPrefixHint: '用 qoder/、workbuddy/、trae/、devin/、command/、zhipu/ 或 qwenwork/ 前缀指定供应商。客户端 API 密钥也可以限制可见和可调度的供应商。不带前缀时是否跨供应商,由系统设置里的模型池决定。',
providerPrefixHint: '用 qoder/、workbuddy/、trae/、devin/、command/、codex/、zhipu/、qwenwork/ 或 kimi/ 前缀指定供应商。客户端 API 密钥也可以限制可见和可调度的供应商。不带前缀时是否跨供应商,由系统设置里的模型池决定。',
authentication: '认证方式',
curlExample: 'curl 示例',
quickTestHint: '通过所选账号发送一条真实请求',
Expand Down Expand Up @@ -1721,6 +1723,8 @@ export const messages: Record<Lang, Dict> = {
accountTypeQwenworkGlobalHint: 'QwenWork 国际版(qwenwork.ai),浏览器授权或导入凭证。授权链接只能使用一次:请在已登录目标账号的浏览器窗口里直接打开,不要经聊天工具发送。',
accountTypeQwenworkCN: '千问办公 国内版',
accountTypeQwenworkCNHint: '千问办公国内版(qwenwork.cn),浏览器授权或导入凭证。授权链接只能使用一次:请在已登录目标账号的浏览器窗口里直接打开,不要经聊天工具发送。国内版尚未经真实账号验收。',
accountTypeKimiCN: 'Kimi Code 国内版',
accountTypeKimiCNHint: 'Kimi Code 中国区订阅,走 auth.kimi.com 的设备码授权,与官方 CLI 使用同一个 OAuth 客户端。已在本机登录 Kimi Code 的,可直接粘贴它的凭证 JSON 导入。',
addAccountDesc: '创建一个隔离会话并登录。每个账号使用独立的运行时目录。',
tabBrowser: '浏览器登录',
tabPat: 'PAT',
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/lib/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ export function isQwenworkProvider(provider?: string) {
return String(provider || '').toLowerCase() === 'qwenwork'
}

export function isKimiProvider(provider?: string) {
return String(provider || '').toLowerCase() === 'kimi'
}

export function accountProviderFamilyLabel(
provider: string | undefined,
t: (key: string) => string,
Expand All @@ -38,6 +42,7 @@ export function accountProviderFamilyLabel(
if (isCommandProvider(providerID)) return 'Command Code'
if (isZhipuProvider(providerID)) return 'Zhipu GLM'
if (isQwenworkProvider(providerID)) return 'QwenWork'
if (isKimiProvider(providerID)) return 'Kimi Code'
if (isQoderProvider(providerID)) return 'Qoder'
return provider || t('account')
}
Expand Down Expand Up @@ -67,6 +72,9 @@ export function accountProviderLabel(
if (isQwenworkProvider(providerID)) {
return regionID === 'cn' ? t('accountTypeQwenworkCN') : t('accountTypeQwenworkGlobal')
}
if (isKimiProvider(providerID)) {
return t('accountTypeKimiCN')
}
if (isQoderProvider(providerID)) {
return regionID === 'cn' ? t('accountTypeQoderCN') : t('accountTypeQoderGlobal')
}
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/pages/ProvidersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,9 @@ export function ProvidersPage() {

async function onReasoningChange(model: ModelInfo, effort: string) {
const provider = modelProvider(model)
if (provider !== 'trae' && provider !== 'workbuddy') return
// Providers with a settable reasoning default: Trae, WorkBuddy, and Kimi
// Code. Anything else has no console-saved level to write.
if (provider !== 'trae' && provider !== 'workbuddy' && provider !== 'kimi') return
const key = modelSettingsKey(model)
setSavingKey(key)
setMessage('')
Expand Down
2 changes: 2 additions & 0 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/caigee-cmd/cli2api/internal/providers/codex"
"github.com/caigee-cmd/cli2api/internal/providers/command"
"github.com/caigee-cmd/cli2api/internal/providers/devin"
"github.com/caigee-cmd/cli2api/internal/providers/kimi"
"github.com/caigee-cmd/cli2api/internal/providers/qoder"
"github.com/caigee-cmd/cli2api/internal/providers/qwenwork"
"github.com/caigee-cmd/cli2api/internal/providers/trae"
Expand Down Expand Up @@ -120,6 +121,7 @@ func New(cfg config.Config) *App {
providerReg.Register(codex.NewClient(store).Adapter())
providerReg.Register(zhipu.NewClient(store).Adapter())
providerReg.Register(qwenwork.NewClient(store).Adapter())
providerReg.Register(kimi.NewClient(store).Adapter())
qoderClient := qoder.NewClient()
qoderClient.Bind(manager.AccountURL, manager.ProxyAPIKey)
qoderClient.SetDirect(qoder.NewDirect(store, func(ctx context.Context, accountID string) (string, error) {
Expand Down
2 changes: 1 addition & 1 deletion internal/app/architecture_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ func isConcreteProvider(imp string) bool {
rest := strings.TrimPrefix(imp, prefix)
root, _, _ := strings.Cut(rest, "/")
switch root {
case "qoder", "workbuddy", "trae", "devin", "command", "codex", "zhipu", "qwenwork":
case "qoder", "workbuddy", "trae", "devin", "command", "codex", "zhipu", "qwenwork", "kimi":
return true
default:
return false
Expand Down
10 changes: 9 additions & 1 deletion internal/console/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
func splitModelSettingPath(raw, queryProvider string) (provider, modelKey string) {
raw = strings.TrimPrefix(raw, "/api/models/")
provider = strings.ToLower(strings.TrimSpace(queryProvider))
for _, prefix := range []string{"trae/", "workbuddy/", "qoder/", "devin/", "command/", "codex/", "zhipu/", "qwenwork/"} {
for _, prefix := range []string{"trae/", "workbuddy/", "qoder/", "devin/", "command/", "codex/", "zhipu/", "qwenwork/", "kimi/"} {
if strings.HasPrefix(strings.ToLower(raw), prefix) {
provider = strings.TrimSuffix(prefix, "/")
raw = raw[len(prefix):]
Expand Down Expand Up @@ -83,6 +83,14 @@ func modelSettingResponse(setting control.ModelSetting) map[string]any {
"max_mode": setting.MaxMode, "reasoning_effort": setting.ReasoningEffort,
"context_custom": setting.ContextCustom,
}
case "kimi":
// Kimi Code has no max-mode tier, so only the reasoning default is
// part of its setting payload.
return map[string]any{
"model": setting.Model, "provider": setting.Provider,
"reasoning_effort": setting.ReasoningEffort,
"context_custom": setting.ContextCustom,
}
case "qoder":
return map[string]any{
"model": setting.Model, "provider": setting.Provider,
Expand Down
31 changes: 31 additions & 0 deletions internal/console/models_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package console

import (
"testing"

"github.com/caigee-cmd/cli2api/internal/control"
)

// TestModelSettingResponseKimiCarriesReasoningOnly pins the PATCH/GET payload
// shape for Kimi Code: the console writes and reads a reasoning default, and
// there is no max-mode tier to report.
func TestModelSettingResponseKimiCarriesReasoningOnly(t *testing.T) {
response := modelSettingResponse(control.ModelSetting{
Provider: "kimi",
Model: "kimi-for-coding",
ReasoningEffort: "max",
ContextCustom: true,
})
if response["reasoning_effort"] != "max" {
t.Fatalf("reasoning_effort = %#v", response["reasoning_effort"])
}
if response["context_custom"] != true {
t.Fatalf("context_custom = %#v", response["context_custom"])
}
if response["provider"] != "kimi" || response["model"] != "kimi-for-coding" {
t.Fatalf("identity = %#v", response)
}
if _, ok := response["max_mode"]; ok {
t.Fatal("kimi has no max-mode switch to report")
}
}
2 changes: 1 addition & 1 deletion internal/control/catalog_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ func DecorateModelsWithContext(ctx context.Context, settings *Settings, models [
item["catalog_context_length"] = catalogWindow
}
switch provider {
case "trae", "workbuddy":
case "trae", "workbuddy", "kimi":
decorateProviderSettings(ctx, settings, item, provider, settingsKey)
case "qoder":
stored, hasStored := configured[settingsKey]
Expand Down
2 changes: 1 addition & 1 deletion internal/control/catalog_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ func TestDecorateModelsWithContextAppliesTraeMaxMode(t *testing.T) {
}

func TestDecorateProviderSettingsRestoresDefaultReasoningState(test *testing.T) {
for _, provider := range []string{"trae", "workbuddy"} {
for _, provider := range []string{"trae", "workbuddy", "kimi"} {
test.Run(provider, func(test *testing.T) {
services, _, _, _ := newTestServices()
source := []map[string]any{{
Expand Down
8 changes: 6 additions & 2 deletions internal/control/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func (s *Settings) ReadModelSetting(ctx context.Context, provider, modelID strin
provider = strings.ToLower(strings.TrimSpace(provider))
out := ModelSetting{Provider: provider, Model: modelID}
switch provider {
case "trae", "workbuddy":
case "trae", "workbuddy", "kimi":
setting, err := s.GetProviderModelSetting(ctx, provider, modelID)
if err != nil {
return ModelSetting{}, operationError("model_setting_failed", err.Error())
Expand Down Expand Up @@ -164,7 +164,7 @@ func (s *Settings) UpdateModelSetting(ctx context.Context, provider, modelID str
modelID = ModelContextKey(modelID)
provider = strings.ToLower(strings.TrimSpace(provider))
switch provider {
case "trae", "workbuddy":
case "trae", "workbuddy", "kimi":
setting, err := s.UpdateProviderModelSetting(ctx, provider, modelID, patch)
if err != nil {
return ModelSetting{}, err
Expand Down Expand Up @@ -210,6 +210,10 @@ func (s *Settings) UpdateProviderModelSetting(ctx context.Context, provider, mod
if provider == "workbuddy" && patch.MaxMode != nil {
return accounts.ProviderModelSetting{}, operationError("invalid_request", "workbuddy has no max-mode switch")
}
// Kimi Code sells a single context tier; only reasoning effort is settable.
if provider == "kimi" && patch.MaxMode != nil {
return accounts.ProviderModelSetting{}, operationError("invalid_request", "kimi has no max-mode switch")
}
setting, err := s.GetProviderModelSetting(ctx, provider, modelID)
if err != nil {
return accounts.ProviderModelSetting{}, operationError("model_setting_failed", err.Error())
Expand Down
Loading
Loading