M0: compilable skeleton — Kigi 0.1.0 fork surgery
Hard fork of xai-org/grok-build (Apache-2.0) re-targeted as Kigi, an
unofficial Kimi Code CLI community build.
Rename & identity
- 72 xai-*/xai-grok-* crates -> kigi-* (explicit: xai-grok-pager-bin ->
kigi-bin [binary `kigi`], xai-grok-pager -> kigi-tui; rest mechanical);
ptyctl, ptyctl-cli, third_party/ unchanged; proto package
xai.grok.tools.v1 -> kigi.tools.v1
- Config home ~/.kigi (KIGI_SHARE_DIR override), env prefix GROK_* ->
KIGI_*, `kigi --version` carries the unofficial-community-build notice
- clap identity, help text, startup banner, prompt templates rebranded
(templates re-encrypted)
Deletions (PRD removal list #5/#6/#7/#9/#10)
- voice input (xai-grok-voice) and all TUI wiring
- telemetry: Mixpanel client, external OTel stream, Sentry, OTLP layers,
trace/GCS/S3 upload queues (kigi-file-utils halved), workspace upload
module & dc_log, heap-profile uploader, auth-diagnostics uploader,
session-analytics halves of feedback; local zero-egress observability
preserved in new kigi-log crate (unified log, --debug firehose,
subsystem file logs, opt-in instrumentation)
- announcements (crate, remote-settings fields, TUI surfaces)
- plugin marketplace (crate, sources/browse/CTA/extensions-modal tab);
direct plugin install/uninstall/update via kigi-agent git_install kept
- relay/gateway/assets endpoints and features (agent relay, headless
relay transport, gateway bridge, LeaderEnvUrls); leader IPC socket now
~/.kigi/leader.sock + KIGI_LEADER_SOCKET, no ws-url derivation
- functional types rehomed instead of deleted: PermissionMode ->
kigi-config-types, McpInitStrategy -> kigi-mcp, PrCreationSource ->
session signals, TerminalDiagnostics -> kigi-pager-render, agent_id ->
shell util
Endpoints
- kigi-env rewritten: single production KigiEndpoints {coding_api_base_url
https://api.kimi.com/coding/v1 (KIGI_CODE_BASE_URL), oauth_host
https://auth.kimi.com (KIGI_OAUTH_HOST), update_base_url (GitHub
Releases API), upgrade_page_url}; GrokBuildEnvironment enum deleted
Toolchain & workspace hygiene
- Rust 1.97.0 pinned; edition 2024; full cargo update; git2 hoisted to
workspace at 0.21 (Option->Result API migration), quick-xml 0.41
- Root Cargo.toml hand-maintained (PRD §8.1): version 0.1.0 inherited by
all members, members sorted, unused deps pruned
- cargo-deny advisories gate (deny.toml with documented transitive
exceptions); CI workflow (check/clippy/fmt/deny/test, macOS+Linux)
- cross-crate test seams re-gated behind `test-support` cargo feature;
insta snapshot baselines renamed to the kigi_tui prefix
- clippy --workspace --all-targets: zero warnings; fmt clean
Fixes surfaced by the port
- updater probe/installer divergence (bin/kigi vs bin/grok symlink set)
- idle model-metadata refresh dead under KIGI_CODE_BASE_URL override
(new is_effective_coding_endpoint_url, loopback+override aware)
- macOS symlinked-TMPDIR fixture canonicalization (foreign_sessions,
fast-worktree); RSS measurement tests serialized via serial_test
Docs & legal (Apache §4)
- NOTICE added (upstream attribution + change statement); THIRD-PARTY
notices sustained; kigi-tools ported-code notices extended; README,
CONTRIBUTING, SECURITY, AGENTS.md rewritten
Out of scope for M0 (tracked): Kimi auth/inference (M1), search/fetch,
command parity, config import (M2), Computer Hub excision & final
brand-token sweep (M2), distribution & self-update rewrite (M3).
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
//! Per-prompt and per-session billing ledgers (not serialized).
|
||||
//!
|
||||
//! `total_tokens()` is input + output: Responses wire `total` is live context
|
||||
//! length. Compaction and other side calls never call `record_main_loop_call`.
|
||||
//!
|
||||
//! # Completeness ownership
|
||||
//!
|
||||
//! Wire incomplete is the OR of these stores (each has a distinct role):
|
||||
//!
|
||||
//! - **`UsageLedger.incomplete`** — durable on the bill snapshot. Set by nested
|
||||
//! subagent incomplete fold, drain timeout, true apply-miss, and
|
||||
//! `mark_usage_incomplete`. Monotonic for a ledger instance.
|
||||
//! - **Sticky (`subagent_usage_not_applied` on the coordinator)** — pin-scoped
|
||||
//! **report** signal (session-only attribution or apply-miss report). Not a
|
||||
//! second token sink; does not stain ledgers by itself.
|
||||
//! - **Foreground live IDs** — fold may still land; freeze drains ≤120s or fails
|
||||
//! closed. Cancel skips multi-second drain (actor-loop safety).
|
||||
//! - **Background live** — never waits; prompt report incomplete immediately;
|
||||
//! spend still folds into the session ledger at completion (no session-ledger
|
||||
//! incomplete).
|
||||
//!
|
||||
//! Freeze and cancel share one outcome policy: ledger marks only on fail-closed;
|
||||
//! sticky and background_live are report-level only.
|
||||
//!
|
||||
//! Projection (`PromptUsage`) never invents tokens; it only ORs completeness
|
||||
//! and scrubs costs when partial or incomplete.
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use kigi_sampling_types::TokenUsage;
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct UsageTotals {
|
||||
pub input_tokens: u64,
|
||||
pub output_tokens: u64,
|
||||
pub cached_read_tokens: u64,
|
||||
pub reasoning_tokens: u64,
|
||||
pub model_calls: u64,
|
||||
pub api_duration_ms: u64,
|
||||
/// USD ticks (1e10 per USD). Absent when no call reported cost.
|
||||
pub cost_usd_ticks: Option<i64>,
|
||||
pub cost_missing_calls: u64,
|
||||
}
|
||||
|
||||
impl UsageTotals {
|
||||
fn from_call(
|
||||
usage: &TokenUsage,
|
||||
api_duration_ms: Option<u64>,
|
||||
cost_usd_ticks: Option<i64>,
|
||||
) -> Self {
|
||||
let cost_usd_ticks = kigi_sampling_types::reported_cost_ticks(cost_usd_ticks);
|
||||
Self {
|
||||
input_tokens: u64::from(usage.prompt_tokens),
|
||||
output_tokens: u64::from(usage.completion_tokens),
|
||||
cached_read_tokens: u64::from(usage.cached_prompt_tokens),
|
||||
reasoning_tokens: u64::from(usage.reasoning_tokens),
|
||||
model_calls: 1,
|
||||
api_duration_ms: api_duration_ms.unwrap_or(0),
|
||||
cost_usd_ticks,
|
||||
cost_missing_calls: u64::from(cost_usd_ticks.is_none()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn total_tokens(&self) -> u64 {
|
||||
self.input_tokens.saturating_add(self.output_tokens)
|
||||
}
|
||||
|
||||
pub fn cost_is_partial(&self) -> bool {
|
||||
self.cost_usd_ticks.is_some() && self.cost_missing_calls > 0
|
||||
}
|
||||
|
||||
fn fold_totals(&mut self, other: &UsageTotals) {
|
||||
let Self {
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
cached_read_tokens,
|
||||
reasoning_tokens,
|
||||
model_calls,
|
||||
api_duration_ms,
|
||||
cost_usd_ticks,
|
||||
cost_missing_calls,
|
||||
} = other;
|
||||
self.input_tokens = self.input_tokens.saturating_add(*input_tokens);
|
||||
self.output_tokens = self.output_tokens.saturating_add(*output_tokens);
|
||||
self.cached_read_tokens = self.cached_read_tokens.saturating_add(*cached_read_tokens);
|
||||
self.reasoning_tokens = self.reasoning_tokens.saturating_add(*reasoning_tokens);
|
||||
self.model_calls = self.model_calls.saturating_add(*model_calls);
|
||||
self.api_duration_ms = self.api_duration_ms.saturating_add(*api_duration_ms);
|
||||
self.cost_missing_calls = self.cost_missing_calls.saturating_add(*cost_missing_calls);
|
||||
self.cost_usd_ticks = merge_cost_ticks(self.cost_usd_ticks, *cost_usd_ticks);
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_cost_ticks(a: Option<i64>, b: Option<i64>) -> Option<i64> {
|
||||
match (a, b) {
|
||||
(None, None) => None,
|
||||
(a, b) => Some(a.unwrap_or(0).saturating_add(b.unwrap_or(0))),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct UsageLedger {
|
||||
pub totals: UsageTotals,
|
||||
pub by_model: IndexMap<String, UsageTotals>,
|
||||
/// Main-agent loop rounds for `num_turns` (subagents excluded).
|
||||
pub main_loop_model_calls: u64,
|
||||
/// Bill may under-count (drain timeout, nested subagent incomplete, apply failure).
|
||||
pub incomplete: bool,
|
||||
}
|
||||
|
||||
impl UsageLedger {
|
||||
/// Fold one main-agent-loop model call. This is the only writer of
|
||||
/// `main_loop_model_calls` (the wire `numTurns`); side calls such as
|
||||
/// compaction must not use it.
|
||||
pub fn record_main_loop_call(
|
||||
&mut self,
|
||||
model_id: &str,
|
||||
usage: &TokenUsage,
|
||||
api_duration_ms: Option<u64>,
|
||||
cost_usd_ticks: Option<i64>,
|
||||
) {
|
||||
let call = UsageTotals::from_call(usage, api_duration_ms, cost_usd_ticks);
|
||||
self.main_loop_model_calls = self.main_loop_model_calls.saturating_add(1);
|
||||
self.fold_entry(model_id, &call);
|
||||
}
|
||||
|
||||
/// Fold subagent usage without incrementing `main_loop_model_calls`.
|
||||
pub fn record_subagent(&mut self, by_model: &[(String, UsageTotals)], incomplete: bool) {
|
||||
for (model_id, totals) in by_model {
|
||||
self.fold_entry(model_id, totals);
|
||||
}
|
||||
if incomplete {
|
||||
self.incomplete = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark_incomplete(&mut self) {
|
||||
self.incomplete = true;
|
||||
}
|
||||
|
||||
fn fold_entry(&mut self, model_id: &str, totals: &UsageTotals) {
|
||||
self.totals.fold_totals(totals);
|
||||
self.by_model
|
||||
.entry(model_id.to_owned())
|
||||
.or_default()
|
||||
.fold_totals(totals);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn tu(prompt: u32, completion: u32) -> TokenUsage {
|
||||
TokenUsage {
|
||||
prompt_tokens: prompt,
|
||||
completion_tokens: completion,
|
||||
total_tokens: 999_999,
|
||||
reasoning_tokens: 0,
|
||||
cached_prompt_tokens: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ledger_sums_partial_subagent_and_zero_cost() {
|
||||
let mut ledger = UsageLedger::default();
|
||||
ledger.record_main_loop_call("m", &tu(1, 1), None, Some(0));
|
||||
assert_eq!(ledger.totals.cost_usd_ticks, None);
|
||||
assert_eq!(ledger.totals.cost_missing_calls, 1);
|
||||
|
||||
ledger.record_main_loop_call("a", &tu(100, 10), Some(100), None);
|
||||
ledger.record_main_loop_call("a", &tu(50, 5), Some(50), Some(70));
|
||||
assert_eq!(ledger.totals.cost_usd_ticks, Some(70));
|
||||
assert!(ledger.totals.cost_is_partial());
|
||||
assert_eq!(ledger.main_loop_model_calls, 3);
|
||||
|
||||
ledger.record_subagent(
|
||||
&[(
|
||||
"b".into(),
|
||||
UsageTotals {
|
||||
input_tokens: 5,
|
||||
model_calls: 1,
|
||||
..Default::default()
|
||||
},
|
||||
)],
|
||||
false,
|
||||
);
|
||||
assert_eq!(ledger.by_model["b"].input_tokens, 5);
|
||||
assert_eq!(ledger.main_loop_model_calls, 3);
|
||||
assert_eq!(ledger.totals.model_calls, 4);
|
||||
assert!(!ledger.incomplete);
|
||||
|
||||
ledger.record_subagent(&[], true);
|
||||
assert!(ledger.incomplete);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user