use super::utils::file_modified_timestamp_ms;
use super::{normalize_workspace_key, workspace_label_from_key, UnifiedMessage};
use crate::TokenBreakdown;
use serde::Deserialize;
use std::io::{BufRead, BufReader};
use std::path::Path;
const CLIENT_ID: &str = "commandcode";
const PROVIDER_ID: &str = "command-code";
const UNKNOWN_MODEL: &str = "unknown";
#[derive(Debug, Deserialize)]
struct CommandCodeEntry {
role: Option<String>,
content: Option<serde_json::Value>,
timestamp: Option<String>,
#[serde(rename = "sessionId")]
session_id: Option<String>,
}
#[derive(Debug, Deserialize)]
struct CommandCodeConfig {
model: Option<String>,
}
pub fn parse_commandcode_file(path: &Path) -> Vec<UnifiedMessage> {
if path
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.ends_with(".checkpoints.jsonl"))
{
return Vec::new();
}
let file = match std::fs::File::open(path) {
Ok(file) => file,
Err(_) => return Vec::new(),
};
let fallback_timestamp = file_modified_timestamp_ms(path);
let raw_model = model_from_config(path);
let provider_id = raw_model
.as_deref()
.and_then(provider_hint_for_model)
.unwrap_or(PROVIDER_ID);
let model_id = raw_model
.map(|model| canonicalize_model(&model))
.unwrap_or_else(|| UNKNOWN_MODEL.to_string());
let session_id_from_path = session_id_from_path(path);
let workspace_key = workspace_key_from_path(path);
let workspace_label = workspace_key.as_deref().and_then(workspace_label_from_key);
let mut messages = Vec::new();
let mut session_id: Option<String> = None;
let mut turn_input_chars: usize = 0;
let mut pending_turn_start = false;
let mut assistant_index = 0usize;
let reader = BufReader::new(file);
for line in reader.lines() {
let line = match line {
Ok(line) => line,
Err(_) => continue,
};
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let entry = match serde_json::from_str::<CommandCodeEntry>(trimmed) {
Ok(entry) => entry,
Err(_) => continue,
};
if session_id.is_none() {
if let Some(id) = entry.session_id.as_deref().filter(|id| !id.is_empty()) {
session_id = Some(id.to_string());
}
}
let chars = entry.content.as_ref().map(content_chars).unwrap_or(0);
match entry.role.as_deref() {
Some("assistant") => {
let input = estimate_tokens(turn_input_chars);
let output = estimate_tokens(chars);
turn_input_chars = 0;
if input + output == 0 {
pending_turn_start = false;
continue;
}
let resolved_session = session_id
.clone()
.unwrap_or_else(|| session_id_from_path.clone());
let timestamp = entry
.timestamp
.as_deref()
.and_then(parse_rfc3339_ms)
.unwrap_or(fallback_timestamp);
let mut message = UnifiedMessage::new_with_dedup(
CLIENT_ID,
model_id.clone(),
provider_id,
resolved_session.clone(),
timestamp,
TokenBreakdown {
input,
output,
cache_read: 0,
cache_write: 0,
reasoning: 0,
},
0.0,
Some(format!("{}:{}", resolved_session, assistant_index)),
);
message.message_count = 1;
message.is_turn_start = pending_turn_start;
message.set_workspace(workspace_key.clone(), workspace_label.clone());
messages.push(message);
assistant_index += 1;
pending_turn_start = false;
}
Some("user") => {
pending_turn_start = true;
turn_input_chars += chars;
}
_ => {
turn_input_chars += chars;
}
}
}
messages
}
fn content_chars(content: &serde_json::Value) -> usize {
match content {
serde_json::Value::Null => 0,
serde_json::Value::Array(items) if items.is_empty() => 0,
serde_json::Value::Object(map) if map.is_empty() => 0,
_ => serde_json::to_string(content)
.map(|serialized| serialized.chars().count())
.unwrap_or(0),
}
}
fn estimate_tokens(chars: usize) -> i64 {
chars.div_ceil(4) as i64
}
fn canonicalize_model(model: &str) -> String {
let base = model.rsplit('/').next().unwrap_or(model);
const PROMO_SUFFIX: &str = "-free";
if base.len() > PROMO_SUFFIX.len()
&& base
.get(base.len() - PROMO_SUFFIX.len()..)
.is_some_and(|tail| tail.eq_ignore_ascii_case(PROMO_SUFFIX))
{
base[..base.len() - PROMO_SUFFIX.len()].to_string()
} else {
base.to_string()
}
}
fn provider_hint_for_model(model: &str) -> Option<&'static str> {
crate::provider_identity::inferred_provider_from_model(model)
}
fn parse_rfc3339_ms(timestamp: &str) -> Option<i64> {
chrono::DateTime::parse_from_rfc3339(timestamp)
.ok()
.map(|dt| dt.timestamp_millis())
}
fn model_from_config(session_path: &Path) -> Option<String> {
let commandcode_root = session_path.parent()?.parent()?.parent()?;
let config_path = commandcode_root.join("config.json");
let bytes = std::fs::read(config_path).ok()?;
let config: CommandCodeConfig = serde_json::from_slice(&bytes).ok()?;
config.model.filter(|model| !model.trim().is_empty())
}
fn session_id_from_path(path: &Path) -> String {
path.file_stem()
.and_then(|stem| stem.to_str())
.unwrap_or("unknown")
.to_string()
}
fn workspace_key_from_path(path: &Path) -> Option<String> {
path.parent()
.and_then(|dir| dir.file_name())
.and_then(|name| name.to_str())
.and_then(normalize_workspace_key)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::io::Write;
use tempfile::TempDir;
fn write_session(dir: &TempDir, slug: &str, session: &str, jsonl: &str) -> std::path::PathBuf {
let project_dir = dir.path().join("projects").join(slug);
std::fs::create_dir_all(&project_dir).unwrap();
let path = project_dir.join(format!("{session}.jsonl"));
let mut file = std::fs::File::create(&path).unwrap();
file.write_all(jsonl.as_bytes()).unwrap();
path
}
fn write_config(dir: &TempDir, model: &str) {
let path = dir.path().join("config.json");
let mut file = std::fs::File::create(&path).unwrap();
write!(file, r#"{{"provider":"command-code","model":"{model}"}}"#).unwrap();
}
#[test]
fn test_canonicalize_model_strips_org_prefix_and_free_promo_suffix() {
assert_eq!(
canonicalize_model("MiniMaxAI/MiniMax-M3-Free"),
"MiniMax-M3"
);
assert_eq!(
canonicalize_model("minimaxai/minimax-m3-free"),
"minimax-m3"
);
assert_eq!(canonicalize_model("MiniMaxAI/MiniMax-M2.5"), "MiniMax-M2.5");
assert_eq!(canonicalize_model("taste-1"), "taste-1");
assert_eq!(canonicalize_model("MiniMax-M3-FrEe"), "MiniMax-M3");
}
#[test]
fn test_canonicalize_model_does_not_panic_on_non_ascii() {
assert_eq!(canonicalize_model("vendor/modèle"), "modèle");
assert_eq!(canonicalize_model("café-🚀"), "café-🚀");
assert_eq!(canonicalize_model("café-free"), "café");
}
#[test]
fn test_content_chars_counts_keys_numbers_and_nested_payloads() {
assert!(content_chars(&json!([{"value": 12345}])) > 0);
let small = content_chars(&json!([{"a": "x"}]));
let large = content_chars(&json!([{"command": "run", "args": ["a", "b"], "n": 42}]));
assert!(large > small);
}
#[test]
fn test_parse_canonicalizes_model_and_estimates_tokens() {
let dir = TempDir::new().unwrap();
write_config(&dir, "MiniMaxAI/MiniMax-M3-Free");
let user = json!([{"type": "text", "text": "12345678"}]);
let assistant = json!([{"type": "text", "text": "abcd"}]);
let jsonl = format!(
"{}\n{}",
json!({"role": "user", "sessionId": "sess-1", "timestamp": "2026-06-16T05:58:15.580Z", "content": user.clone()}),
json!({"role": "assistant", "sessionId": "sess-1", "timestamp": "2026-06-16T05:58:20.332Z", "content": assistant.clone()}),
);
let path = write_session(&dir, "users-alice-repo", "sess-1", &jsonl);
let messages = parse_commandcode_file(&path);
assert_eq!(messages.len(), 1);
let msg = &messages[0];
assert_eq!(msg.client, "commandcode");
assert_eq!(msg.provider_id, "minimax");
assert_eq!(msg.model_id, "MiniMax-M3");
assert_eq!(msg.session_id, "sess-1");
assert_eq!(msg.tokens.input, estimate_tokens(content_chars(&user)));
assert_eq!(
msg.tokens.output,
estimate_tokens(content_chars(&assistant))
);
assert!(msg.tokens.input > 0 && msg.tokens.output > 0);
assert_eq!(msg.message_count, 1);
assert!(msg.is_turn_start);
assert_eq!(msg.timestamp, 1781589500332); assert_eq!(msg.workspace_key.as_deref(), Some("users-alice-repo"));
assert_eq!(msg.workspace_label.as_deref(), Some("users-alice-repo"));
}
#[test]
fn test_input_is_per_turn_delta_not_cumulative() {
let dir = TempDir::new().unwrap();
write_config(&dir, "model-x");
let jsonl = concat!(
r#"{"role":"user","sessionId":"s","content":[{"type":"text","text":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}]}"#,
"\n",
r#"{"role":"assistant","sessionId":"s","content":[{"type":"text","text":"bbbb"}]}"#,
"\n",
r#"{"role":"user","sessionId":"s","content":[{"type":"text","text":"d"}]}"#,
"\n",
r#"{"role":"assistant","sessionId":"s","content":[{"type":"text","text":"e"}]}"#,
);
let path = write_session(&dir, "proj", "s", jsonl);
let messages = parse_commandcode_file(&path);
assert_eq!(messages.len(), 2);
assert!(messages[0].tokens.input > 0);
assert!(messages[0].is_turn_start);
assert!(messages[1].is_turn_start);
assert!(
messages[1].tokens.input < messages[0].tokens.input,
"turn 2 input ({}) must reflect only its own delta, not the cumulative \
history that included turn 1 ({})",
messages[1].tokens.input,
messages[0].tokens.input
);
}
#[test]
fn test_commandcode_input_is_per_turn_delta() {
let dir = TempDir::new().unwrap();
write_config(&dir, "model-x");
let user1_content = json!([{"type": "text", "text": "aaaa"}]);
let asst1_content = json!([{"type": "text", "text": "bbbb"}]);
let user2_content = json!([{"type": "text", "text": "cccc"}]);
let asst2_content = json!([{"type": "text", "text": "dddd"}]);
let user1_chars = content_chars(&user1_content);
let asst1_chars = content_chars(&asst1_content);
let user2_chars = content_chars(&user2_content);
let asst2_chars = content_chars(&asst2_content);
let expected_input_turn1 = estimate_tokens(user1_chars);
let expected_input_turn2 = estimate_tokens(user2_chars);
let jsonl = format!(
"{}\n{}\n{}\n{}",
json!({"role": "user", "sessionId": "s", "content": user1_content}),
json!({"role": "assistant", "sessionId": "s", "content": asst1_content}),
json!({"role": "user", "sessionId": "s", "content": user2_content}),
json!({"role": "assistant", "sessionId": "s", "content": asst2_content}),
);
let path = write_session(&dir, "proj", "s", &jsonl);
let messages = parse_commandcode_file(&path);
assert_eq!(messages.len(), 2, "expected exactly 2 assistant turns");
let turn1 = &messages[0];
let turn2 = &messages[1];
assert!(
expected_input_turn1 > 0,
"turn 1 input must be positive (user1 context non-empty)"
);
assert!(
expected_input_turn2 > 0,
"turn 2 input must be positive (user2 context non-empty)"
);
assert_eq!(
turn1.tokens.input, expected_input_turn1,
"turn 1 input pinned to its own per-turn delta (user1)"
);
assert_eq!(
turn2.tokens.input, expected_input_turn2,
"turn 2 input pinned to its own per-turn delta (user2), not cumulative"
);
assert_eq!(
turn1.tokens.output,
estimate_tokens(asst1_chars),
"turn 1 output pinned to assistant message estimate"
);
assert_eq!(
turn2.tokens.output,
estimate_tokens(asst2_chars),
"turn 2 output pinned to assistant message estimate"
);
assert_eq!(
turn1.tokens.cache_read, 0,
"cache_read must be 0 (no cache attribution)"
);
assert_eq!(
turn2.tokens.cache_read, 0,
"cache_read must be 0 (no cache attribution)"
);
assert_eq!(turn1.tokens.cache_write, 0, "cache_write must be 0");
assert_eq!(turn2.tokens.cache_write, 0, "cache_write must be 0");
}
#[test]
fn test_minimax_model_resolves_nonzero_pricing() {
use crate::pricing::{ModelPricing, PricingService};
use std::collections::HashMap;
let dir = TempDir::new().unwrap();
write_config(&dir, "MiniMaxAI/MiniMax-M3-Free");
let jsonl = concat!(
r#"{"role":"user","sessionId":"s","content":[{"type":"text","text":"hello there how are you"}]}"#,
"\n",
r#"{"role":"assistant","sessionId":"s","content":[{"type":"text","text":"doing great thanks"}]}"#,
);
let path = write_session(&dir, "proj", "s", jsonl);
let messages = parse_commandcode_file(&path);
assert_eq!(messages.len(), 1);
let msg = &messages[0];
assert_eq!(msg.model_id, "MiniMax-M3");
assert_eq!(msg.provider_id, "minimax");
let mut litellm = HashMap::new();
litellm.insert(
"minimax/minimax-m3".to_string(),
ModelPricing {
input_cost_per_token: Some(0.01),
output_cost_per_token: Some(0.02),
..Default::default()
},
);
let pricing = PricingService::new(litellm, HashMap::new());
let cost = pricing.calculate_cost_with_provider(
&msg.model_id,
Some(&msg.provider_id),
&msg.tokens,
);
assert!(
cost > 0.0,
"MiniMax model must price non-zero (got {cost}); provider hint or \
model canonicalization is dropping the pricing key"
);
}
#[test]
fn test_checkpoint_files_are_skipped() {
let dir = TempDir::new().unwrap();
write_config(&dir, "model-x");
let project_dir = dir.path().join("projects").join("proj");
std::fs::create_dir_all(&project_dir).unwrap();
let path = project_dir.join("s.checkpoints.jsonl");
let mut file = std::fs::File::create(&path).unwrap();
file.write_all(
br#"{"type":"checkpoint","messageId":"m","snapshot":"snap","isSnapshotUpdate":false}"#,
)
.unwrap();
let messages = parse_commandcode_file(&path);
assert!(messages.is_empty());
}
#[test]
fn test_missing_config_falls_back_to_unknown_model() {
let dir = TempDir::new().unwrap();
let jsonl = concat!(
r#"{"role":"user","sessionId":"s","content":[{"type":"text","text":"hello"}]}"#,
"\n",
r#"{"role":"assistant","sessionId":"s","content":[{"type":"text","text":"world"}]}"#,
);
let path = write_session(&dir, "proj", "s", jsonl);
let messages = parse_commandcode_file(&path);
assert_eq!(messages.len(), 1);
assert_eq!(messages[0].model_id, "unknown");
}
#[test]
fn test_skips_malformed_lines_without_panicking() {
let dir = TempDir::new().unwrap();
write_config(&dir, "model-x");
let jsonl = concat!(
r#"{"role":"user","sessionId":"s","content":[{"type":"text","text":"hello"}]}"#,
"\n",
"not valid json at all",
"\n",
r#"{"role":"assistant","sessionId":"s","content":[{"type":"text","text":"response"}]}"#,
);
let path = write_session(&dir, "proj", "s", jsonl);
let messages = parse_commandcode_file(&path);
assert_eq!(messages.len(), 1);
assert!(messages[0].tokens.input > 0 || messages[0].tokens.output > 0);
}
#[test]
fn test_empty_assistant_with_no_context_is_skipped() {
let dir = TempDir::new().unwrap();
write_config(&dir, "model-x");
let jsonl = r#"{"role":"assistant","sessionId":"s","content":[]}"#;
let path = write_session(&dir, "proj", "s", jsonl);
let messages = parse_commandcode_file(&path);
assert!(messages.is_empty());
}
}