From 78cbfd3798284f69939743ee11e6f2dd9095c8ef Mon Sep 17 00:00:00 2001
From: ozymandiashh <234437643+ozymandiashh@users.noreply.github.com>
Date: Mon, 11 May 2026 19:02:28 +0300
Subject: [PATCH] Add Kimi provider
---
CHANGELOG.md | 11 +
README.md | 7 +-
assets/providers/kimi.svg | 5 +
docs/architecture.md | 6 +-
docs/providers/README.md | 1 +
docs/providers/kimi.md | 62 +++
gnome/indicator.js | 2 +
gnome/prefs.js | 1 +
mac/Sources/CodeBurnMenubar/AppStore.swift | 2 +
.../CodeBurnMenubar/Views/AgentTabStrip.swift | 1 +
package.json | 1 +
src/dashboard.tsx | 2 +
src/models.ts | 15 +
src/providers/index.ts | 3 +-
src/providers/kimi.ts | 394 ++++++++++++++++++
tests/models-hoist.test.ts | 12 +
tests/provider-registry.test.ts | 10 +-
tests/providers/kimi.test.ts | 192 +++++++++
18 files changed, 721 insertions(+), 6 deletions(-)
create mode 100644 assets/providers/kimi.svg
create mode 100644 docs/providers/kimi.md
create mode 100644 src/providers/kimi.ts
create mode 100644 tests/providers/kimi.test.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e7dd43d7..1b83f28a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
# Changelog
+## Unreleased
+
+### Added (CLI)
+- **Kimi Code CLI provider.** CodeBurn now reads Kimi session usage from
+ `$KIMI_SHARE_DIR/sessions/` or `~/.kimi/sessions/`, including subagent
+ `wire.jsonl` files. The parser consumes Kimi's official `StatusUpdate`
+ token usage fields (`input_other`, `input_cache_read`,
+ `input_cache_creation`, `output`), normalizes Kimi tool names such as
+ `Shell`, `ReadFile`, and `WriteFile`, and maps hidden managed Kimi Code
+ model aliases to priced Kimi K2 entries.
+
## 0.9.8 - 2026-05-10
### Added (CLI)
diff --git a/README.md b/README.md
index b3700224..e9f5af92 100644
--- a/README.md
+++ b/README.md
@@ -113,6 +113,7 @@ Arrow keys switch between Today, 7 Days, 30 Days, Month, and 6 Months (use `--fr
|
| Roo Code | Yes | [roo-code.md](docs/providers/roo-code.md) |
|
| KiloCode | Yes | [kilo-code.md](docs/providers/kilo-code.md) |
|
| Qwen | Yes | [qwen.md](docs/providers/qwen.md) |
+|
| Kimi Code CLI | Yes | [kimi.md](docs/providers/kimi.md) |
|
| Goose | Yes | [goose.md](docs/providers/goose.md) |
|
| Antigravity | Yes | [antigravity.md](docs/providers/antigravity.md) |
|
| Crush | Yes | [crush.md](docs/providers/crush.md) |
@@ -380,7 +381,9 @@ These are starting points, not verdicts. A 60% cache hit on a single experimenta
**Roo Code / KiloCode** are Cline-family VS Code extensions. CodeBurn reads `ui_messages.json` from each task directory in VS Code's `globalStorage`, filtering `type: "say"` entries with `say: "api_req_started"` to extract token counts.
-CodeBurn deduplicates messages (by API message ID for Claude, by cumulative token cross-check for Codex, by conversation/timestamp for Cursor, by session ID for Gemini, by session+message ID for OpenCode, by responseId for Pi/OMP), filters by date range per entry, and classifies each turn.
+**Kimi Code CLI** stores session logs under `$KIMI_SHARE_DIR/sessions///` or `~/.kimi/sessions///`. CodeBurn reads `wire.jsonl` `StatusUpdate.token_usage` records, maps `input_other`, `input_cache_read`, `input_cache_creation`, and `output` into the standard token columns, and includes subagent sessions under each session's `subagents/` folder.
+
+CodeBurn deduplicates messages (by API message ID for Claude, by cumulative token cross-check for Codex, by conversation/timestamp for Cursor, by session ID for Gemini, by session+message ID for OpenCode, by responseId for Pi/OMP, by session+message ID for Kimi), filters by date range per entry, and classifies each turn.
## Environment Variables
@@ -390,6 +393,8 @@ CodeBurn deduplicates messages (by API message ID for Claude, by cumulative toke
| `CLAUDE_CONFIG_DIRS` | OS-delimited list of Claude data directories to scan together (e.g. `~/.claude-work:~/.claude-personal`). Sessions merge into one row per project. Overrides `CLAUDE_CONFIG_DIR` when set. |
| `CODEX_HOME` | Override Codex data directory (default: `~/.codex`) |
| `FACTORY_DIR` | Override Droid data directory (default: `~/.factory`) |
+| `KIMI_SHARE_DIR` | Override Kimi Code CLI share directory (default: `~/.kimi`) |
+| `KIMI_MODEL_NAME` | Override Kimi model name when Kimi sessions do not record the model |
| `QWEN_DATA_DIR` | Override Qwen data directory (default: `~/.qwen/projects`) |
## Sponsoring CodeBurn
diff --git a/assets/providers/kimi.svg b/assets/providers/kimi.svg
new file mode 100644
index 00000000..c09b36fe
--- /dev/null
+++ b/assets/providers/kimi.svg
@@ -0,0 +1,5 @@
+
diff --git a/docs/architecture.md b/docs/architecture.md
index 9b1ea14f..c7f1a4a6 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -128,9 +128,9 @@ type Provider = {
}
```
-`src/providers/index.ts` registers eighteen providers across two tiers:
+`src/providers/index.ts` registers nineteen providers across two tiers:
-- **Eager**: `claude`, `codex`, `copilot`, `droid`, `gemini`, `kilo-code`, `kiro`, `openclaw`, `pi`, `omp`, `qwen`, `roo-code`. Imported at module load.
+- **Eager**: `claude`, `codex`, `copilot`, `droid`, `gemini`, `kilo-code`, `kiro`, `openclaw`, `pi`, `omp`, `qwen`, `kimi`, `roo-code`. Imported at module load.
- **Lazy**: `antigravity`, `goose`, `cursor`, `opencode`, `cursor-agent`, `crush`. Imported via dynamic `import()` so the heavy dependencies (SQLite, protobuf) do not touch users who do not have those tools installed.
Both lists hit the same `getAllProviders()` aggregator. A failed lazy import is silent and excludes that provider from the run.
@@ -181,7 +181,7 @@ The `prepublishOnly` hook in `package.json` runs `npm run build` so `npm publish
- `tests/` root (27 files) covers CLI, parser, optimize, cache, format, models, plans.
- `tests/security/` (1 file) covers prototype-pollution guards.
-- `tests/providers/` (14 files) covers per-provider parsing.
+- `tests/providers/` (15 files) covers per-provider parsing.
- `tests/fixtures/` holds redacted real-world session data.
Five providers ship without dedicated test files today: `antigravity`, `claude`, `gemini`, `goose`, `qwen`. Closing this gap is a standing good-first-issue.
diff --git a/docs/providers/README.md b/docs/providers/README.md
index 05f43dbf..1f3c03f9 100644
--- a/docs/providers/README.md
+++ b/docs/providers/README.md
@@ -17,6 +17,7 @@ For the architectural picture, see `../architecture.md`.
| [Gemini](gemini.md) | JSON / JSONL | `src/providers/gemini.ts` | none |
| [KiloCode](kilo-code.md) | JSON | `src/providers/kilo-code.ts` | `tests/providers/kilo-code.test.ts` |
| [Kiro](kiro.md) | JSON | `src/providers/kiro.ts` | `tests/providers/kiro.test.ts` |
+| [Kimi](kimi.md) | JSONL | `src/providers/kimi.ts` | `tests/providers/kimi.test.ts` |
| [OpenClaw](openclaw.md) | JSONL | `src/providers/openclaw.ts` | `tests/providers/openclaw.test.ts` |
| [Pi](pi.md) | JSONL | `src/providers/pi.ts` | `tests/providers/pi.test.ts` |
| [OMP](omp.md) | JSONL | `src/providers/pi.ts` | `tests/providers/omp.test.ts` |
diff --git a/docs/providers/kimi.md b/docs/providers/kimi.md
new file mode 100644
index 00000000..19d6876e
--- /dev/null
+++ b/docs/providers/kimi.md
@@ -0,0 +1,62 @@
+# Kimi
+
+Kimi Code CLI session parser.
+
+- **Source:** `src/providers/kimi.ts`
+- **Loading:** eager (`src/providers/index.ts`)
+- **Test:** `tests/providers/kimi.test.ts`
+
+## Where it reads from
+
+`$KIMI_SHARE_DIR/sessions/` if set, otherwise `~/.kimi/sessions/`.
+
+Kimi stores sessions by work-directory hash:
+
+```text
+~/.kimi/
+ kimi.json
+ config.toml
+ sessions/
+ /
+ /
+ context.jsonl
+ wire.jsonl
+ state.json
+ subagents/
+ /
+ context.jsonl
+ wire.jsonl
+```
+
+`kimi.json` maps each work-directory hash back to the original working path. CodeBurn uses that to display the project basename; if the metadata file is missing, the hash directory name is used.
+
+## Storage Format
+
+CodeBurn reads `wire.jsonl`. Each data line is a persisted wire record:
+
+```json
+{"timestamp":1776162403,"message":{"type":"StatusUpdate","payload":{"message_id":"msg-1","token_usage":{"input_other":100,"input_cache_read":25,"input_cache_creation":10,"output":40}}}}
+```
+
+`TurnBegin` / `SteerInput` provide the user prompt, `ToolCall` / `ToolCallRequest` provide tool names and shell commands, and `StatusUpdate.token_usage` provides the billable token counts.
+
+## Caching
+
+None.
+
+## Deduplication
+
+Per `kimi::`, falling back to the status-update line index if the message id is absent.
+
+## Quirks
+
+- Kimi's official `TokenUsage` separates `input_other`, `input_cache_read`, `input_cache_creation`, and `output`. CodeBurn maps those directly into input, cache read, cache write, and output.
+- The current Kimi wire schema does not persist the model on every usage update. CodeBurn uses `KIMI_MODEL_NAME` when set, then the active `~/.kimi/config.toml` default model, then `kimi-auto`.
+- `kimi-auto`, `kimi-code`, and `kimi-for-coding` are priced as `kimi-k2-thinking` so managed Kimi Code sessions do not show as `$0` when the exact backend model is hidden.
+- Subagent sessions are discovered from `subagents//wire.jsonl` and parsed as separate Kimi sessions under the same project.
+
+## When Fixing A Bug Here
+
+1. Reproduce with a tiny `wire.jsonl` fixture in `tests/providers/kimi.test.ts`.
+2. If token totals look wrong, inspect `StatusUpdate.token_usage` first; `context.jsonl` only stores context checkpoints and cumulative counts, not per-step billing detail.
+3. If tools are missing, check whether Kimi emitted `ToolCall`, `ToolCallRequest`, or nested `SubagentEvent`; CodeBurn intentionally counts subagent wire files separately to avoid double-counting parent mirrors.
diff --git a/gnome/indicator.js b/gnome/indicator.js
index c2f8266e..533f6441 100644
--- a/gnome/indicator.js
+++ b/gnome/indicator.js
@@ -41,6 +41,7 @@ const PROVIDERS = [
{ id: 'gemini', label: 'Gemini' },
{ id: 'kilo-code', label: 'Kilo Code' },
{ id: 'kiro', label: 'Kiro' },
+ { id: 'kimi', label: 'Kimi' },
{ id: 'roo-code', label: 'Roo Code' },
];
@@ -69,6 +70,7 @@ const PROVIDER_PATHS = {
codex: '.codex/sessions',
cursor: '.config/Cursor/User/globalStorage/state.vscdb',
copilot: '.copilot/session-state',
+ kimi: '.kimi/sessions',
pi: '.pi/agent/sessions',
};
diff --git a/gnome/prefs.js b/gnome/prefs.js
index 2b9d477b..08d4b824 100644
--- a/gnome/prefs.js
+++ b/gnome/prefs.js
@@ -13,6 +13,7 @@ const PROVIDERS = [
{ id: 'goose', label: 'Goose' },
{ id: 'kilo-code', label: 'Kilo Code' },
{ id: 'kiro', label: 'Kiro' },
+ { id: 'kimi', label: 'Kimi' },
{ id: 'openclaw', label: 'OpenClaw' },
{ id: 'opencode', label: 'OpenCode' },
{ id: 'pi', label: 'Pi' },
diff --git a/mac/Sources/CodeBurnMenubar/AppStore.swift b/mac/Sources/CodeBurnMenubar/AppStore.swift
index 00b27e8b..9e238cea 100644
--- a/mac/Sources/CodeBurnMenubar/AppStore.swift
+++ b/mac/Sources/CodeBurnMenubar/AppStore.swift
@@ -726,6 +726,7 @@ enum ProviderFilter: String, CaseIterable, Identifiable {
case droid = "Droid"
case gemini = "Gemini"
case kiro = "Kiro"
+ case kimi = "Kimi"
case kiloCode = "KiloCode"
case openclaw = "OpenClaw"
case opencode = "OpenCode"
@@ -758,6 +759,7 @@ enum ProviderFilter: String, CaseIterable, Identifiable {
case .gemini: "gemini"
case .kiloCode: "kilo-code"
case .kiro: "kiro"
+ case .kimi: "kimi"
case .openclaw: "openclaw"
case .opencode: "opencode"
case .pi: "pi"
diff --git a/mac/Sources/CodeBurnMenubar/Views/AgentTabStrip.swift b/mac/Sources/CodeBurnMenubar/Views/AgentTabStrip.swift
index 6561cc97..b5f1570a 100644
--- a/mac/Sources/CodeBurnMenubar/Views/AgentTabStrip.swift
+++ b/mac/Sources/CodeBurnMenubar/Views/AgentTabStrip.swift
@@ -347,6 +347,7 @@ extension ProviderFilter {
case .gemini: return Color(red: 0x44/255.0, green: 0x85/255.0, blue: 0xF4/255.0)
case .kiloCode: return Color(red: 0x00/255.0, green: 0x96/255.0, blue: 0x88/255.0)
case .kiro: return Color(red: 0x4A/255.0, green: 0x9E/255.0, blue: 0xC4/255.0)
+ case .kimi: return Color(red: 0xA4/255.0, green: 0xC6/255.0, blue: 0x39/255.0)
case .openclaw: return Color(red: 0xDA/255.0, green: 0x70/255.0, blue: 0x56/255.0)
case .opencode: return Color(red: 0x5B/255.0, green: 0x83/255.0, blue: 0x5B/255.0)
case .pi: return Color(red: 0xB2/255.0, green: 0x6B/255.0, blue: 0x3D/255.0)
diff --git a/package.json b/package.json
index a58098db..301f9f64 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"claude-code",
"cursor",
"codex",
+ "kimi",
"opencode",
"pi",
"ai-coding",
diff --git a/src/dashboard.tsx b/src/dashboard.tsx
index b46dbcce..ac3eb349 100644
--- a/src/dashboard.tsx
+++ b/src/dashboard.tsx
@@ -54,6 +54,7 @@ const PROVIDER_COLORS: Record = {
cursor: '#00B4D8',
opencode: '#A78BFA',
pi: '#F472B6',
+ kimi: '#B6E34A',
all: '#FF8C42',
}
@@ -515,6 +516,7 @@ const PROVIDER_DISPLAY_NAMES: Record = {
cursor: 'Cursor',
opencode: 'OpenCode',
pi: 'Pi',
+ kimi: 'Kimi',
}
function getProviderDisplayName(name: string): string { return PROVIDER_DISPLAY_NAMES[name] ?? name }
diff --git a/src/models.ts b/src/models.ts
index e4441e0a..a40fb59a 100644
--- a/src/models.ts
+++ b/src/models.ts
@@ -170,6 +170,9 @@ const BUILTIN_ALIASES: Record = {
'cline-auto': 'claude-sonnet-4-5',
'openclaw-auto': 'claude-sonnet-4-5',
'qwen-auto': 'claude-sonnet-4-5',
+ 'kimi-auto': 'kimi-k2-thinking',
+ 'kimi-code': 'kimi-k2-thinking',
+ 'kimi-for-coding': 'kimi-k2-thinking',
// Cursor emits dot-version tier-last names plus tier/reasoning suffixes
// that LiteLLM does not index (`-high`, `-low`, `-medium`, `-thinking`,
// `-high-thinking`, `-fast-mode`). Missing aliases here surface as $0 in
@@ -355,6 +358,7 @@ const autoModelNames: Record = {
'cline-auto': 'Cline (auto)',
'openclaw-auto': 'OpenClaw (auto)',
'qwen-auto': 'Qwen (auto)',
+ 'kimi-auto': 'Kimi (auto)',
}
const SHORT_NAMES: Record = {
@@ -398,6 +402,17 @@ const SHORT_NAMES: Record = {
'gemini-3-flash-preview': 'Gemini 3 Flash',
'gemini-2.5-pro': 'Gemini 2.5 Pro',
'gemini-2.5-flash': 'Gemini 2.5 Flash',
+ 'kimi-k2-thinking-turbo': 'Kimi K2 Thinking Turbo',
+ 'kimi-k2-thinking': 'Kimi K2 Thinking',
+ 'kimi-thinking-preview': 'Kimi Thinking',
+ 'kimi-k2.6': 'Kimi K2.6',
+ 'kimi-k2.5': 'Kimi K2.5',
+ 'kimi-k2p5': 'Kimi K2.5',
+ 'kimi-k2-instruct': 'Kimi K2 Instruct',
+ 'kimi-k2-0905': 'Kimi K2',
+ 'kimi-k2': 'Kimi K2',
+ 'kimi-latest': 'Kimi Latest',
+ 'moonshot-v1': 'Moonshot v1',
'deepseek-coder-max': 'DeepSeek Coder Max',
'deepseek-coder': 'DeepSeek Coder',
'deepseek-r1': 'DeepSeek R1',
diff --git a/src/providers/index.ts b/src/providers/index.ts
index 38ed4902..f35b4c57 100644
--- a/src/providers/index.ts
+++ b/src/providers/index.ts
@@ -5,6 +5,7 @@ import { droid } from './droid.js'
import { gemini } from './gemini.js'
import { kiloCode } from './kilo-code.js'
import { kiro } from './kiro.js'
+import { kimi } from './kimi.js'
import { openclaw } from './openclaw.js'
import { pi, omp } from './pi.js'
import { qwen } from './qwen.js'
@@ -101,7 +102,7 @@ async function loadCrush(): Promise {
}
}
-const coreProviders: Provider[] = [claude, codex, copilot, droid, gemini, kiloCode, kiro, openclaw, pi, omp, qwen, rooCode]
+const coreProviders: Provider[] = [claude, codex, copilot, droid, gemini, kiloCode, kiro, openclaw, pi, omp, qwen, kimi, rooCode]
export async function getAllProviders(): Promise {
const [ag, gs, cursor, opencode, cursorAgent, crush] = await Promise.all([loadAntigravity(), loadGoose(), loadCursor(), loadOpenCode(), loadCursorAgent(), loadCrush()])
diff --git a/src/providers/kimi.ts b/src/providers/kimi.ts
new file mode 100644
index 00000000..75242cc8
--- /dev/null
+++ b/src/providers/kimi.ts
@@ -0,0 +1,394 @@
+import { createHash } from 'crypto'
+import { readdir, readFile, stat } from 'fs/promises'
+import { basename, dirname, join } from 'path'
+import { homedir } from 'os'
+
+import { extractBashCommands } from '../bash-utils.js'
+import { readSessionLines } from '../fs-utils.js'
+import { calculateCost, getShortModelName } from '../models.js'
+import type { ParsedProviderCall, Provider, SessionParser, SessionSource } from './types.js'
+
+type JsonObject = Record
+
+const toolNameMap: Record = {
+ Shell: 'Bash',
+ Bash: 'Bash',
+ bash: 'Bash',
+ ReadFile: 'Read',
+ ReadMediaFile: 'Read',
+ WriteFile: 'Write',
+ StrReplaceFile: 'Edit',
+ Grep: 'Grep',
+ Glob: 'Glob',
+ SearchWeb: 'WebSearch',
+ FetchURL: 'WebFetch',
+ Agent: 'Agent',
+ AgentTool: 'Agent',
+ TaskList: 'Agent',
+ TaskOutput: 'Agent',
+ TaskStop: 'Agent',
+ AskUserQuestion: 'AskUser',
+ SetTodoList: 'TodoWrite',
+ Think: 'Think',
+ EnterPlanMode: 'EnterPlanMode',
+ ExitPlanMode: 'ExitPlanMode',
+ SendDMail: 'DMail',
+}
+
+function asObject(value: unknown): JsonObject | null {
+ return value && typeof value === 'object' && !Array.isArray(value) ? value as JsonObject : null
+}
+
+function stringField(obj: JsonObject | null, key: string): string | undefined {
+ const value = obj?.[key]
+ return typeof value === 'string' ? value : undefined
+}
+
+function numericField(obj: JsonObject, ...keys: string[]): number {
+ for (const key of keys) {
+ const raw = obj[key]
+ const n = typeof raw === 'number' ? raw : typeof raw === 'string' ? Number(raw) : NaN
+ if (Number.isFinite(n) && n > 0) return Math.trunc(n)
+ }
+ return 0
+}
+
+function getShareDir(overrideDir?: string): string {
+ return overrideDir ?? process.env['KIMI_SHARE_DIR'] ?? join(homedir(), '.kimi')
+}
+
+function md5(text: string): string {
+ return createHash('md5').update(text, 'utf-8').digest('hex')
+}
+
+function projectNameFromPath(pathValue: string): string {
+ const cleaned = pathValue.replace(/\/+$/, '')
+ return basename(cleaned) || cleaned || 'kimi'
+}
+
+async function loadProjectNames(shareDir: string): Promise