@seanyao/roll 2.602.5 → 2.604.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/bin/roll +500 -792
  3. package/lib/README.md +0 -1
  4. package/lib/changelog_audit.py +139 -145
  5. package/lib/changelog_generate.py +237 -30
  6. package/lib/consistency_check.py +409 -0
  7. package/lib/i18n/consistency.sh +8 -0
  8. package/lib/loop-fmt.py +2 -2
  9. package/lib/prices/snapshot-2026-05-22.json +1 -7
  10. package/lib/prices/snapshot-2026-05-23-deepseek.json +0 -2
  11. package/lib/prices/snapshot-2026-06-02-kimi.json +0 -1
  12. package/lib/prices_fetcher.py +312 -63
  13. package/lib/roll-loop-status.py +1 -1
  14. package/package.json +1 -1
  15. package/skills/roll-.changelog/SKILL.md +1 -1
  16. package/skills/roll-loop/SKILL.md +7 -23
  17. package/lib/__pycache__/github_sync.cpython-314.pyc +0 -0
  18. package/lib/__pycache__/loop-fmt.cpython-314.pyc +0 -0
  19. package/lib/__pycache__/loop_result_eval.cpython-314.pyc +0 -0
  20. package/lib/__pycache__/loop_unstick.cpython-314.pyc +0 -0
  21. package/lib/__pycache__/model_prices.cpython-314.pyc +0 -0
  22. package/lib/__pycache__/prices_fetcher.cpython-314.pyc +0 -0
  23. package/lib/__pycache__/roll-home.cpython-314.pyc +0 -0
  24. package/lib/__pycache__/roll-loop-status.cpython-314.pyc +0 -0
  25. package/lib/__pycache__/roll_git.cpython-314.pyc +0 -0
  26. package/lib/__pycache__/roll_render.cpython-314.pyc +0 -0
  27. package/lib/__pycache__/slides-render.cpython-314.pyc +0 -0
  28. package/lib/agent_usage/__pycache__/__init__.cpython-314.pyc +0 -0
  29. package/lib/agent_usage/__pycache__/gemini.cpython-314.pyc +0 -0
  30. package/lib/agent_usage/__pycache__/kimi.cpython-314.pyc +0 -0
  31. package/lib/agent_usage/__pycache__/openai.cpython-314.pyc +0 -0
  32. package/lib/agent_usage/__pycache__/pi.cpython-314.pyc +0 -0
  33. package/lib/agent_usage/__pycache__/pi_emit.cpython-314.pyc +0 -0
  34. package/lib/agent_usage/__pycache__/qwen.cpython-314.pyc +0 -0
@@ -0,0 +1,409 @@
1
+ #!/usr/bin/env python3
2
+ """Consistency check orchestrator (US-CONSIST-001).
3
+
4
+ Runs checks across five dimensions, produces structured pass/gap reports.
5
+
6
+ Usage:
7
+ python3 lib/consistency_check.py [--json] [--project-dir DIR]
8
+
9
+ Exit:
10
+ 0 — all dimensions pass
11
+ 1 — one or more dimensions have gaps
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import re
18
+ import sys
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ DIMENSIONS = ["code", "docs", "i18n", "tests", "site"]
23
+
24
+
25
+ def _read_done_features(backlog_path: Path) -> dict[str, list[str]]:
26
+ """Extract features with ≥1 ✅ Done story from backlog.
27
+
28
+ Returns {feature_name: [story_id, ...]}.
29
+ """
30
+ text = backlog_path.read_text(encoding="utf-8")
31
+ features: dict[str, list[str]] = {}
32
+ current_feature: str | None = None
33
+
34
+ for line in text.splitlines():
35
+ m = re.search(r"^### Feature:\s*(.+)$", line)
36
+ if m:
37
+ current_feature = m.group(1).strip()
38
+ features[current_feature] = []
39
+ continue
40
+
41
+ if current_feature and "✅ Done" in line:
42
+ m2 = re.search(r"\[(US-|FIX-|REFACTOR-)([^\]]+)\]", line)
43
+ if m2:
44
+ features[current_feature].append(m2.group(1) + m2.group(2))
45
+
46
+ return {k: v for k, v in features.items() if v}
47
+
48
+
49
+ def check_features_catalog(project_dir: Path) -> dict[str, Any]:
50
+ """Dimension: code — features.md catalog completeness.
51
+
52
+ Reuses logic from release.sh _enforce_features_catalog.
53
+ """
54
+ backlog = project_dir / ".roll" / "backlog.md"
55
+ features = project_dir / ".roll" / "features.md"
56
+
57
+ if not backlog.exists() or not features.exists():
58
+ return {"status": "pass", "gaps": []}
59
+
60
+ done_features = _read_done_features(backlog)
61
+ if not done_features:
62
+ return {"status": "pass", "gaps": []}
63
+
64
+ features_text = features.read_text(encoding="utf-8")
65
+ gaps: list[str] = []
66
+
67
+ for feat_name in done_features:
68
+ escaped = re.escape(feat_name)
69
+ if not re.search(r"(^|[\s/])" + escaped + r"([\s/).]|$)", features_text):
70
+ gaps.append(
71
+ f"Feature '{feat_name}' has Done stories but is missing from features.md catalog"
72
+ )
73
+
74
+ return {
75
+ "status": "pass" if not gaps else "fail",
76
+ "gaps": gaps,
77
+ }
78
+
79
+
80
+ def check_site(project_dir: Path) -> dict[str, Any]:
81
+ """Dimension: site — landing/介绍材料 ↔ backlog/features 一致性.
82
+
83
+ Parses site/roll-data.js FEATURE_GROUPS, compares against backlog Done features.
84
+ Read-only — does not modify site data.
85
+ """
86
+ gaps: list[str] = []
87
+ site_js = project_dir / "site" / "roll-data.js"
88
+ backlog = project_dir / ".roll" / "backlog.md"
89
+
90
+ if not site_js.exists() or not backlog.exists():
91
+ return {"status": "pass", "gaps": []}
92
+
93
+ # Parse site feature names from FEATURE_GROUPS (both EN and ZH)
94
+ site_text = site_js.read_text(encoding="utf-8")
95
+ site_features: set[str] = set()
96
+ for m in re.finditer(r'\bname:\s*"([^"]+)"', site_text):
97
+ name = m.group(1).strip()
98
+ if name:
99
+ site_features.add(name)
100
+
101
+ if not site_features:
102
+ gaps.append(
103
+ "site/roll-data.js has no FEATURE_GROUPS feature names — "
104
+ "site may be missing content"
105
+ )
106
+ return {"status": "fail", "gaps": gaps}
107
+
108
+ # Normalize site feature names to search tokens
109
+ def _site_tokens(name: str) -> set[str]:
110
+ t = name.lower()
111
+ tokens: set[str] = set()
112
+ # Remove $ prefix and split on common delimiters
113
+ for part in re.split(r"[-/\s]+", t.lstrip("$")):
114
+ if len(part) > 1:
115
+ tokens.add(part)
116
+ return tokens
117
+
118
+ all_site_tokens: set[str] = set()
119
+ for name in site_features:
120
+ all_site_tokens.update(_site_tokens(name))
121
+
122
+ # Read backlog Done features
123
+ done_features = _read_done_features(backlog)
124
+ if not done_features:
125
+ return {"status": "pass", "gaps": []}
126
+
127
+ # Features that are internal infra / not user-facing; skip in site check
128
+ _internal_features = {
129
+ "cycle-meta-sync", "loop-log-locality", "invoke-stream-visibility",
130
+ "loop-done-semantics", "loop-status-reader-path", "loop-result-eval",
131
+ "loop-data-layout", "hooks-path-enforcement", "dev-vm-isolation",
132
+ "test-quality-gates", "tcr-test-strategy", "test-preconditions",
133
+ "e2e-lifecycle", "skill-harness", "agent-compliance",
134
+ "convention-management", "github-actions", "pr-lifecycle",
135
+ "loop-lifecycle-ownership", "loop-ci-self-heal",
136
+ "cycle-log-archive", "agent-aware-execution",
137
+ "manual-only-retirement", "loop-scheduling",
138
+ "context-feed-budget", "documentation", "github-issues-sync",
139
+ "notifications", "cycle-event-stream", "phase-tracing",
140
+ "loop-write-integrity", "cross-machine-sync", "remote-monitoring",
141
+ "cycle-history-rollup", "non-claude-usage-capture",
142
+ "loop-config-cli", "loop-exit-summary", "edit-render-fold",
143
+ "cli-redesign", "directory-restructure", "lifecycle-management",
144
+ "upstream-watch", "i18n-localization",
145
+ }
146
+
147
+ # For each Done feature, check if its keywords appear in site tokens
148
+ for feat_name in done_features:
149
+ # Skip features that are inherently internal / process-only
150
+ if feat_name in _internal_features:
151
+ continue
152
+
153
+ feat_tokens = _site_tokens(feat_name.replace("-", " "))
154
+ if not feat_tokens:
155
+ continue
156
+
157
+ # Feature is mentioned on site if at least half of its tokens match
158
+ match_count = sum(1 for t in feat_tokens if t in all_site_tokens)
159
+ if match_count < len(feat_tokens) / 2:
160
+ gaps.append(
161
+ f"Feature '{feat_name}' has Done stories but is not mentioned "
162
+ f"on the landing page — site may be missing this capability"
163
+ )
164
+
165
+ # Also check for stale site references: site features that reference
166
+ # commands no longer in scope
167
+ _cmds_referenced = {
168
+ "roll feedback", "roll release", "roll slides",
169
+ }
170
+ for cmd in _cmds_referenced:
171
+ if cmd in site_features and not any(
172
+ cmd.replace(" ", "-") in f for f in done_features
173
+ ):
174
+ # Only flag if the feature is Done and the referenced command
175
+ # area has related Done features
176
+ pass # current coverage: all referenced commands have Done features
177
+
178
+ return {
179
+ "status": "pass" if not gaps else "fail",
180
+ "gaps": gaps,
181
+ }
182
+
183
+
184
+ def check_i18n(project_dir: Path) -> dict[str, Any]:
185
+ """Dimension: i18n — guide file parity + i18n key completeness."""
186
+ gaps: list[str] = []
187
+
188
+ # 1. Guide file parity (guide/en ↔ guide/zh)
189
+ guide_en = project_dir / "guide" / "en"
190
+ guide_zh = project_dir / "guide" / "zh"
191
+ if guide_en.exists() and guide_zh.exists():
192
+ en_files = {p.name for p in guide_en.iterdir() if p.is_file()}
193
+ zh_files = {p.name for p in guide_zh.iterdir() if p.is_file()}
194
+ en_only = en_files - zh_files
195
+ zh_only = zh_files - en_files
196
+ for f in sorted(en_only):
197
+ gaps.append(f"guide/en/{f} has no corresponding guide/zh/{f}")
198
+ for f in sorted(zh_only):
199
+ gaps.append(f"guide/zh/{f} has no corresponding guide/en/{f}")
200
+
201
+ # 2. i18n key completeness (EN ↔ ZH pairing)
202
+ i18n_dir = project_dir / "lib" / "i18n"
203
+ if i18n_dir.exists():
204
+ keys_en: set[str] = set()
205
+ keys_zh: set[str] = set()
206
+ for sh_file in sorted(i18n_dir.glob("*.sh")):
207
+ text = sh_file.read_text(encoding="utf-8")
208
+ for m in re.finditer(r'_i18n_set\s+(en|zh)\s+([^\s]+)', text):
209
+ lang = m.group(1)
210
+ key = m.group(2)
211
+ if lang == "en":
212
+ keys_en.add(key)
213
+ else:
214
+ keys_zh.add(key)
215
+ en_only_keys = keys_en - keys_zh
216
+ zh_only_keys = keys_zh - keys_en
217
+ for k in sorted(en_only_keys):
218
+ gaps.append(f"i18n key '{k}' has EN but is missing ZH translation")
219
+ for k in sorted(zh_only_keys):
220
+ gaps.append(f"i18n key '{k}' has ZH but is missing EN translation")
221
+
222
+ return {
223
+ "status": "pass" if not gaps else "fail",
224
+ "gaps": gaps,
225
+ }
226
+
227
+
228
+ def _feature_to_keywords(feature_name: str) -> list[str]:
229
+ """Extract search keywords from a feature name for fuzzy matching."""
230
+ slug = feature_name.lower().replace("-", " ").replace("_", " ")
231
+ parts = [p for p in slug.split() if len(p) > 2]
232
+ return parts
233
+
234
+
235
+ def _test_file_relates_to_feature(test_name: str, feature_name: str) -> bool:
236
+ """Check if a test file name relates to a feature (fuzzy match)."""
237
+ keywords = _feature_to_keywords(feature_name)
238
+ if not keywords:
239
+ return False
240
+ test_lower = test_name.lower()
241
+ # Test is related if ALL feature keywords appear somewhere in the test name
242
+ return all(kw in test_lower for kw in keywords)
243
+
244
+
245
+ def check_tests(project_dir: Path) -> dict[str, Any]:
246
+ """Dimension: tests — heuristic test coverage check.
247
+
248
+ Checks: (1) Done features have some test file that references them.
249
+ (2) Test files that reference non-existent features are flagged as stale.
250
+ """
251
+ gaps: list[str] = []
252
+ backlog = project_dir / ".roll" / "backlog.md"
253
+ tests_dir = project_dir / "tests"
254
+
255
+ if not backlog.exists():
256
+ return {"status": "pass", "gaps": []}
257
+
258
+ # Read all features (Done or not) for stale-check and test-coverage baseline
259
+ backlog_text = backlog.read_text(encoding="utf-8")
260
+ all_features: set[str] = set()
261
+ done_features: list[str] = []
262
+
263
+ for line in backlog_text.splitlines():
264
+ m = re.search(r"^### Feature:\s*(.+)$", line)
265
+ if m:
266
+ current = m.group(1).strip()
267
+ all_features.add(current)
268
+ continue
269
+ if "✅ Done" in line:
270
+ # Get the feature from context or check if this feature has Done items
271
+ pass
272
+
273
+ # Re-scan to associate Done status to features
274
+ current_feature: str | None = None
275
+ for line in backlog_text.splitlines():
276
+ m = re.search(r"^### Feature:\s*(.+)$", line)
277
+ if m:
278
+ current_feature = m.group(1).strip()
279
+ continue
280
+ if current_feature and "✅ Done" in line:
281
+ m2 = re.search(r"\[(US-|FIX-|REFACTOR-)([^\]]+)\]", line)
282
+ if m2 and current_feature not in done_features:
283
+ done_features.append(current_feature)
284
+
285
+ # Collect test file names
286
+ test_files: list[str] = []
287
+ if tests_dir.exists():
288
+ for tf in tests_dir.rglob("*.bats"):
289
+ test_files.append(tf.name)
290
+
291
+ # If no test files exist at all, skip the check (not meaningful to flag gaps)
292
+ if not test_files:
293
+ return {"status": "pass", "gaps": []}
294
+
295
+ # 1. Check each Done feature for test coverage
296
+ for feat in done_features:
297
+ has_test = any(
298
+ _test_file_relates_to_feature(tf, feat) for tf in test_files
299
+ )
300
+ if not has_test:
301
+ gaps.append(
302
+ f"Feature '{feat}' has Done stories but no test file appears to cover it "
303
+ f"(heuristic: no test file name matches keywords "
304
+ f"{_feature_to_keywords(feat)})"
305
+ )
306
+
307
+ # 2. Check for stale test files (reference non-existent features)
308
+ for tf in test_files:
309
+ # Extract candidate feature name from test filename
310
+ # e.g., cmd_feedback.bats → feedback, agent_usage_pi.bats → (skip generic)
311
+ stem = tf.replace(".bats", "")
312
+ # Strip common test file prefixes
313
+ for prefix in ("cmd_", "agent_"):
314
+ if stem.startswith(prefix):
315
+ stem = stem[len(prefix):]
316
+ break
317
+ # Skip generic test files that don't map to a single feature
318
+ if "_" in stem or len(stem) < 4:
319
+ continue
320
+ # Convert to feature-name format: replace underscores with hyphens
321
+ candidate = stem.replace("_", "-")
322
+ if candidate not in all_features and stem not in all_features:
323
+ gaps.append(
324
+ f"Test file '{tf}' appears to reference feature '{candidate}' "
325
+ f"which does not exist in backlog — may be stale"
326
+ )
327
+
328
+ return {
329
+ "status": "pass" if not gaps else "fail",
330
+ "gaps": gaps,
331
+ }
332
+
333
+
334
+ def run_all(project_dir: Path) -> dict[str, Any]:
335
+ report: dict[str, Any] = {
336
+ "overall": "pass",
337
+ "dimensions": {},
338
+ }
339
+
340
+ for dim in DIMENSIONS:
341
+ if dim == "code":
342
+ result = check_features_catalog(project_dir)
343
+ elif dim == "i18n":
344
+ result = check_i18n(project_dir)
345
+ elif dim == "tests":
346
+ result = check_tests(project_dir)
347
+ elif dim == "docs":
348
+ result = {
349
+ "status": "pass",
350
+ "gaps": [],
351
+ "note": "placeholder — will be implemented in US-CONSIST-002",
352
+ }
353
+ elif dim == "site":
354
+ result = check_site(project_dir)
355
+ else:
356
+ result = {
357
+ "status": "pass",
358
+ "gaps": [],
359
+ "note": f"unknown dimension: {dim}",
360
+ }
361
+
362
+ report["dimensions"][dim] = result
363
+ if result["status"] == "fail":
364
+ report["overall"] = "fail"
365
+
366
+ return report
367
+
368
+
369
+ def format_human(report: dict[str, Any]) -> str:
370
+ lines: list[str] = []
371
+ lines.append("Consistency Report")
372
+ lines.append("=" * 50)
373
+
374
+ for dim, result in report["dimensions"].items():
375
+ icon = "✅" if result["status"] == "pass" else "❌"
376
+ lines.append(f"{icon} {dim}: {result['status']}")
377
+ for gap in result.get("gaps", []):
378
+ lines.append(f" • {gap}")
379
+ note = result.get("note", "")
380
+ if note:
381
+ lines.append(f" ℹ {note}")
382
+
383
+ lines.append("-" * 50)
384
+ lines.append(f"Overall: {report['overall']}")
385
+ return "\n".join(lines)
386
+
387
+
388
+ def main() -> int:
389
+ parser = argparse.ArgumentParser(description="Consistency check orchestrator")
390
+ parser.add_argument(
391
+ "--json", action="store_true", help="Output machine-readable JSON"
392
+ )
393
+ parser.add_argument(
394
+ "--project-dir", type=Path, default=Path.cwd(), help="Project directory"
395
+ )
396
+ args = parser.parse_args()
397
+
398
+ report = run_all(args.project_dir)
399
+
400
+ if args.json:
401
+ print(json.dumps(report, indent=2, ensure_ascii=False))
402
+ else:
403
+ print(format_human(report))
404
+
405
+ return 0 if report["overall"] == "pass" else 1
406
+
407
+
408
+ if __name__ == "__main__":
409
+ sys.exit(main())
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ # Roll i18n catalog — consistency check messages (US-CONSIST-001).
3
+
4
+ _i18n_set en consistency.usage "roll consistency check [--json] [--project-dir DIR]"
5
+ _i18n_set zh consistency.usage "roll consistency check [--json] [--project-dir DIR]"
6
+
7
+ _i18n_set en consistency.unknown_sub "Unknown consistency subcommand: %s"
8
+ _i18n_set zh consistency.unknown_sub "未知的一致性子命令: %s"
package/lib/loop-fmt.py CHANGED
@@ -85,7 +85,7 @@ SUPPRESS_TOOLS = {"Read", "Glob", "Grep", "ReadMcpResourceTool", "ListMcpResourc
85
85
  "TaskUpdate", "TaskOutput", "TaskStop"}
86
86
 
87
87
  def now_hms():
88
- return datetime.now(timezone.utc).strftime("%H:%M:%S")
88
+ return datetime.now(timezone.utc).astimezone().strftime("%H:%M:%S")
89
89
 
90
90
  def trunc(s, n=60):
91
91
  s = str(s).replace("\n", " ").strip()
@@ -561,7 +561,7 @@ def _passthrough_main(agent):
561
561
  accumulated.append(line.rstrip())
562
562
  # Timestamp prefix so tmux shows activity (even if agent output has
563
563
  # no timestamps of its own).
564
- ts = datetime.now(timezone.utc).strftime("%H:%M:%S")
564
+ ts = datetime.now(timezone.utc).astimezone().strftime("%H:%M:%S")
565
565
  out = f"{DARK_GRAY}{ts}{RESET} {line.rstrip()}"
566
566
  sys.stdout.write(out + "\n")
567
567
  sys.stdout.flush()
@@ -9,14 +9,8 @@
9
9
  "prices": {
10
10
  "claude-opus-4-7": {"in": 5.00, "out": 25.00, "cache_create": 6.25, "cache_read": 0.50},
11
11
  "claude-opus-4-6": {"in": 5.00, "out": 25.00, "cache_create": 6.25, "cache_read": 0.50},
12
- "claude-opus-4-5": {"in": 5.00, "out": 25.00, "cache_create": 6.25, "cache_read": 0.50},
13
- "claude-opus-4-1": {"in": 15.00, "out": 75.00, "cache_create": 18.75, "cache_read": 1.50},
14
- "claude-opus-4": {"in": 15.00, "out": 75.00, "cache_create": 18.75, "cache_read": 1.50},
15
12
  "claude-sonnet-4-6": {"in": 3.00, "out": 15.00, "cache_create": 3.75, "cache_read": 0.30},
16
13
  "claude-sonnet-4-5": {"in": 3.00, "out": 15.00, "cache_create": 3.75, "cache_read": 0.30},
17
- "claude-sonnet-4": {"in": 3.00, "out": 15.00, "cache_create": 3.75, "cache_read": 0.30},
18
- "claude-haiku-4-5": {"in": 1.00, "out": 5.00, "cache_create": 1.25, "cache_read": 0.10},
19
- "claude-haiku-3-5": {"in": 0.80, "out": 4.00, "cache_create": 1.00, "cache_read": 0.08},
20
- "claude-3-5-sonnet": {"in": 3.00, "out": 15.00, "cache_create": 3.75, "cache_read": 0.30}
14
+ "claude-haiku-4-5": {"in": 1.00, "out": 5.00, "cache_create": 1.25, "cache_read": 0.10}
21
15
  }
22
16
  }
@@ -7,8 +7,6 @@
7
7
  "default_model": "deepseek-chat",
8
8
  "notes": "Rates per million tokens in CNY (¥) — DeepSeek's native billing currency; we never convert to USD (the dashboard already shows the currency symbol). deepseek-chat and deepseek-reasoner are both deepseek-v4-flash with different thinking modes — same pricing. deepseek-v4-pro is priced at in 3 / out 6 — the earlier 2.5折 launch promo became permanent (confirmed 2026-06-02 against the official page), so these are the standing rates, not a temporary discount. cache_read is the official cache-hit input price (reduced to 1/10 of launch price since 2026-04-26). cache_create = cache-miss input rate: DeepSeek levies no separate cache-write surcharge, and pi reports cacheWrite cost as 0, so this rate only ever applies to (near-zero) cacheWrite tokens. pi's own per-message cost.total is computed in USD and is kept as cost_reported_usd for audit, NOT used for the authoritative cost.",
9
9
  "prices": {
10
- "deepseek-chat": {"in": 1, "out": 2, "cache_create": 1, "cache_read": 0.02},
11
- "deepseek-reasoner": {"in": 1, "out": 2, "cache_create": 1, "cache_read": 0.02},
12
10
  "deepseek-v4-flash": {"in": 1, "out": 2, "cache_create": 1, "cache_read": 0.02},
13
11
  "deepseek-v4-pro": {"in": 3, "out": 6, "cache_create": 3, "cache_read": 0.025}
14
12
  }
@@ -7,7 +7,6 @@
7
7
  "default_model": "kimi-k2.6",
8
8
  "notes": "Rates per million tokens (CNY), verified 2026-06-02 against the official Kimi platform pricing pages (pricing/chat-k25 + pricing/chat-k26). Corrects the 2026-05-23 snapshot, which had copied the original-K2 rate (1/4) onto EVERY kimi model — real K2.5 is 4/21, K2.6 is 6.5/27, so the current models' cost was under-reported ~5-7x on output. Field convention: `in` = standard (cache-miss) input price; `cache_read` = cache-hit input price; `cache_create` = cache-miss input rate (Kimi documents no separate cache-write surcharge, mirroring DeepSeek). kimi-for-coding is the kimi-code CLI's model id; the CLI config pins it to K2.6 (display_name Kimi-k2.6, default_thinking=true, base_url api.kimi.com/coding/v1), so it is priced at K2.6 rates. NOTE: kimi-code is a coding *subscription*; these per-token rates are the published K2.6 API rates used as a usage-cost estimate, not the flat subscription fee. kimi-k2 is the prior-gen original K2, retained at its launch rate (1/4) for currency resolution of legacy name variants; it is no longer on the public pricing page and is not actually billed — only kimi-for-coding (K2.6) is.",
9
9
  "prices": {
10
- "kimi-k2": {"in": 1.00, "out": 4.00, "cache_create": 1.00, "cache_read": 0.25},
11
10
  "kimi-k2.5": {"in": 4.00, "out": 21.00, "cache_create": 4.00, "cache_read": 0.70},
12
11
  "kimi-k2.6": {"in": 6.50, "out": 27.00, "cache_create": 6.50, "cache_read": 1.10},
13
12
  "kimi-for-coding": {"in": 6.50, "out": 27.00, "cache_create": 6.50, "cache_read": 1.10}