use super::utils::{open_readonly_sqlite_opt, sqlite_for_each_row_on};
use super::UnifiedMessage;
use crate::TokenBreakdown;
use std::path::Path;
pub fn is_synthetic_model(model_id: &str) -> bool {
let lower = model_id.to_lowercase();
if lower.starts_with("hf:") {
return true;
}
if lower.starts_with("accounts/fireworks/") {
return true;
}
if lower.starts_with("accounts/together/") {
return true;
}
false
}
pub fn is_synthetic_provider(provider_id: &str) -> bool {
let lower = provider_id.to_lowercase();
matches!(
lower.as_str(),
"synthetic" | "glhf" | "synthetic.new" | "octofriend"
)
}
pub fn is_synthetic_gateway(model_id: &str, provider_id: &str) -> bool {
is_synthetic_model(model_id) || is_synthetic_provider(provider_id)
}
pub fn normalize_synthetic_model(model_id: &str) -> String {
let lower = model_id.to_lowercase();
if let Some(rest) = lower.strip_prefix("hf:") {
if let Some((_org, model)) = rest.split_once('/') {
return model.to_string();
}
return rest.to_string();
}
if let Some(rest) = lower.strip_prefix("accounts/") {
if let Some(models_rest) = rest.split_once("/models/") {
return models_rest.1.to_string();
}
}
lower
}
pub fn normalize_synthetic_gateway_fields(model_id: &mut String, provider_id: &mut String) -> bool {
if !is_synthetic_gateway(model_id, provider_id) {
return false;
}
*model_id = normalize_synthetic_model(model_id);
if provider_id.is_empty() || provider_id.eq_ignore_ascii_case("unknown") {
*provider_id = "synthetic".to_string();
}
true
}
pub fn matches_synthetic_filter(client: &str, model_id: &str, provider_id: &str) -> bool {
client.eq_ignore_ascii_case("synthetic") || is_synthetic_gateway(model_id, provider_id)
}
pub fn parse_octofriend_sqlite(db_path: &Path) -> Vec<UnifiedMessage> {
let Some(conn) = open_readonly_sqlite_opt(db_path) else {
return Vec::new();
};
let has_messages_table: bool = conn
.query_row(
"SELECT count(*) FROM sqlite_master WHERE type='table' AND name IN ('messages', 'sessions', 'token_usage')",
[],
|row| row.get::<_, i64>(0),
)
.map(|count| count > 0)
.unwrap_or(false);
if !has_messages_table {
return Vec::new();
}
let mut messages = Vec::new();
sqlite_for_each_row_on(
&conn,
db_path,
"SELECT id, model, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, reasoning_tokens, cost, timestamp, session_id, provider FROM messages WHERE input_tokens IS NOT NULL OR output_tokens IS NOT NULL",
None,
&mut |row| {
let id: String = row.get(0)?;
let model_id: String = row.get::<_, String>(1).unwrap_or_default();
let input: i64 = row.get::<_, i64>(2).unwrap_or(0);
let output: i64 = row.get::<_, i64>(3).unwrap_or(0);
let cache_read: i64 = row.get::<_, i64>(4).unwrap_or(0);
let cache_write: i64 = row.get::<_, i64>(5).unwrap_or(0);
let reasoning: i64 = row.get::<_, i64>(6).unwrap_or(0);
let cost: f64 = row.get::<_, f64>(7).unwrap_or(0.0);
let timestamp: f64 = row.get::<_, f64>(8).unwrap_or(0.0);
let session_id: String = row.get::<_, String>(9).unwrap_or_else(|_| "unknown".to_string());
let provider: String = row.get::<_, String>(10).unwrap_or_else(|_| "synthetic".to_string());
let total = input + output + cache_read + cache_write + reasoning;
if total == 0 {
return Ok(());
}
let ts_ms = if timestamp > 1e12 {
timestamp as i64
} else {
(timestamp * 1000.0) as i64
};
let mut msg = UnifiedMessage::new(
"synthetic",
normalize_synthetic_model(&model_id),
provider,
session_id,
ts_ms,
TokenBreakdown {
input: input.max(0),
output: output.max(0),
cache_read: cache_read.max(0),
cache_write: cache_write.max(0),
reasoning: reasoning.max(0),
},
cost.max(0.0),
);
msg.dedup_key = Some(id);
messages.push(msg);
Ok(())
},
);
if messages.is_empty() {
sqlite_for_each_row_on(
&conn,
db_path,
"SELECT id, model, input_tokens, output_tokens, timestamp, session_id FROM token_usage WHERE input_tokens > 0 OR output_tokens > 0",
None,
&mut |row| {
let id: String = row.get(0)?;
let model_id: String = row.get::<_, String>(1).unwrap_or_default();
let input: i64 = row.get::<_, i64>(2).unwrap_or(0);
let output: i64 = row.get::<_, i64>(3).unwrap_or(0);
let timestamp: f64 = row.get::<_, f64>(4).unwrap_or(0.0);
let session_id: String = row.get::<_, String>(5).unwrap_or_else(|_| "unknown".to_string());
let ts_ms = if timestamp > 1e12 {
timestamp as i64
} else {
(timestamp * 1000.0) as i64
};
let mut msg = UnifiedMessage::new(
"synthetic",
normalize_synthetic_model(&model_id),
"synthetic",
session_id,
ts_ms,
TokenBreakdown {
input: input.max(0),
output: output.max(0),
cache_read: 0,
cache_write: 0,
reasoning: 0,
},
0.0,
);
msg.dedup_key = Some(id);
messages.push(msg);
Ok(())
},
);
}
messages
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_synthetic_model_hf_prefix() {
assert!(is_synthetic_model("hf:deepseek-ai/DeepSeek-V3-0324"));
assert!(is_synthetic_model("hf:zai-org/GLM-4.7"));
assert!(is_synthetic_model("hf:moonshotai/Kimi-K2.5"));
assert!(is_synthetic_model("hf:MiniMaxAI/MiniMax-M2.1"));
}
#[test]
fn test_is_synthetic_model_fireworks_prefix() {
assert!(is_synthetic_model(
"accounts/fireworks/models/deepseek-v3-0324"
));
assert!(is_synthetic_model("accounts/fireworks/models/glm-4.7"));
}
#[test]
fn test_is_synthetic_model_together_prefix() {
assert!(is_synthetic_model("accounts/together/models/qwen3-235b"));
}
#[test]
fn test_is_synthetic_model_negative() {
assert!(!is_synthetic_model("claude-sonnet-4-5"));
assert!(!is_synthetic_model("gpt-5.2-codex"));
assert!(!is_synthetic_model("deepseek-v3"));
assert!(!is_synthetic_model("gemini-2.5-pro"));
}
#[test]
fn test_is_synthetic_provider() {
assert!(is_synthetic_provider("synthetic"));
assert!(is_synthetic_provider("glhf"));
assert!(is_synthetic_provider("Synthetic"));
assert!(is_synthetic_provider("GLHF"));
assert!(is_synthetic_provider("synthetic.new"));
assert!(is_synthetic_provider("octofriend"));
}
#[test]
fn test_is_synthetic_provider_negative() {
assert!(!is_synthetic_provider("anthropic"));
assert!(!is_synthetic_provider("openai"));
assert!(!is_synthetic_provider("moonshot"));
assert!(!is_synthetic_provider("fireworks"));
}
#[test]
fn test_normalize_synthetic_model_hf() {
assert_eq!(
normalize_synthetic_model("hf:deepseek-ai/DeepSeek-V3-0324"),
"deepseek-v3-0324"
);
assert_eq!(normalize_synthetic_model("hf:zai-org/GLM-4.7"), "glm-4.7");
assert_eq!(
normalize_synthetic_model("hf:moonshotai/Kimi-K2.5"),
"kimi-k2.5"
);
}
#[test]
fn test_normalize_synthetic_model_fireworks() {
assert_eq!(
normalize_synthetic_model("accounts/fireworks/models/deepseek-v3-0324"),
"deepseek-v3-0324"
);
}
#[test]
fn test_normalize_synthetic_model_passthrough() {
assert_eq!(
normalize_synthetic_model("claude-sonnet-4-5"),
"claude-sonnet-4-5"
);
assert_eq!(normalize_synthetic_model("gpt-4o"), "gpt-4o");
}
#[test]
fn test_normalize_synthetic_gateway_fields_sets_provider_when_unknown() {
let mut model_id = "hf:deepseek-ai/DeepSeek-V3-0324".to_string();
let mut provider_id = "unknown".to_string();
let matched = normalize_synthetic_gateway_fields(&mut model_id, &mut provider_id);
assert!(matched);
assert_eq!(model_id, "deepseek-v3-0324");
assert_eq!(provider_id, "synthetic");
}
#[test]
fn test_normalize_synthetic_gateway_fields_preserves_existing_provider() {
let mut model_id = "accounts/fireworks/models/deepseek-v3-0324".to_string();
let mut provider_id = "fireworks".to_string();
let matched = normalize_synthetic_gateway_fields(&mut model_id, &mut provider_id);
assert!(matched);
assert_eq!(model_id, "deepseek-v3-0324");
assert_eq!(provider_id, "fireworks");
}
#[test]
fn test_matches_synthetic_filter_accepts_gateway_traffic_without_rewriting_client() {
assert!(matches_synthetic_filter(
"opencode",
"hf:deepseek-ai/DeepSeek-V3-0324",
"unknown"
));
assert!(matches_synthetic_filter(
"claude",
"claude-sonnet-4-5",
"glhf"
));
assert!(!matches_synthetic_filter("opencode", "gpt-4o", "anthropic"));
}
#[test]
fn test_parse_octofriend_sqlite_nonexistent() {
let result = parse_octofriend_sqlite(Path::new("/nonexistent/path/sqlite.db"));
assert!(result.is_empty());
}
}