feat: improve request pricing, model diagnostics, and client discovery
- Align response model handling with CPA-Manager-Plus and flag routing mismatches - Add per-request costs and CSV export; fix mixed-context cost aggregation - Display API-reported access types and move providers next to timestamps - Use white table backgrounds and simplify cost and status displays - Expand client discovery paths, validate executables, and prioritize installed clients - Add model, pricing, client discovery, and UI regression tests
This commit is contained in:
1 parent
0e7437493d
commit
dfba9b019f
20 files changed
+652
-152
No files matched your search
@@ -22,7 +22,7 @@ pub(crate) fn find_antigravity_cli(home: &Path) -> Option<PathBuf> {
|
||||
.join("agy/bin/agy.exe");
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let native = home.join(".local/bin/agy");
|
||||
if native.is_file() {
|
||||
if agent_cli_executable(&native) {
|
||||
Some(native)
|
||||
} else {
|
||||
find_named_agent_executable(home, &["agy"])
|
||||
|
||||
@@ -1121,12 +1121,9 @@ pub(crate) fn agent_installation_detected(
|
||||
executable_found: bool,
|
||||
app_installed: bool,
|
||||
) -> bool {
|
||||
version.is_some()
|
||||
|| (matches!(
|
||||
client,
|
||||
AgentClient::ClaudeDesktop | AgentClient::OpenCode | AgentClient::ZCode | AgentClient::WorkBuddy
|
||||
) && executable_found)
|
||||
executable_found
|
||||
|| app_installed
|
||||
|| (client == AgentClient::ClaudeDesktop && version.is_some())
|
||||
}
|
||||
|
||||
pub(crate) fn should_probe_primary_agent_executable_version(client: AgentClient) -> bool {
|
||||
@@ -2476,12 +2473,11 @@ pub(crate) fn find_opencode_managed_executable(home: &Path) -> Option<PathBuf> {
|
||||
home.join("Applications/OpenCode.app/Contents/MacOS/opencode-cli"),
|
||||
]
|
||||
.into_iter()
|
||||
.find(|path| path.is_file())
|
||||
.find(|path| agent_cli_executable(path))
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
let path = home.join(".opencode/bin/opencode");
|
||||
path.is_file().then_some(path)
|
||||
find_named_agent_executable_in_directories(&[home.join(".opencode/bin")], &["opencode"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2493,7 +2489,7 @@ pub(crate) fn find_kimi_code_executable(home: &Path) -> Option<PathBuf> {
|
||||
let managed = [directory.join("kimi")];
|
||||
managed
|
||||
.into_iter()
|
||||
.find(|path| path.is_file())
|
||||
.find(|path| agent_cli_executable(path))
|
||||
.or_else(|| find_named_agent_executable(home, &["kimi"]))
|
||||
}
|
||||
|
||||
@@ -2505,7 +2501,7 @@ pub(crate) fn find_grok_build_executable(home: &Path) -> Option<PathBuf> {
|
||||
let managed = [directory.join("grok")];
|
||||
managed
|
||||
.into_iter()
|
||||
.find(|path| path.is_file())
|
||||
.find(|path| agent_cli_executable(path))
|
||||
.or_else(|| find_named_agent_executable(home, &["grok"]))
|
||||
}
|
||||
|
||||
@@ -2514,18 +2510,54 @@ pub(crate) fn find_pi_executable(home: &Path) -> Option<PathBuf> {
|
||||
}
|
||||
|
||||
pub(crate) fn find_named_agent_executable(home: &Path, names: &[&str]) -> Option<PathBuf> {
|
||||
for directory in agent_executable_directories(home) {
|
||||
find_named_agent_executable_in_directories(&agent_executable_directories(home), names)
|
||||
}
|
||||
|
||||
pub(crate) fn agent_cli_executable(path: &Path) -> bool {
|
||||
let Ok(metadata) = fs::metadata(path) else {
|
||||
return false;
|
||||
};
|
||||
if !metadata.is_file() {
|
||||
return false;
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
metadata.permissions().mode() & 0o111 != 0
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
path.extension()
|
||||
.and_then(|extension| extension.to_str())
|
||||
.is_some_and(|extension| {
|
||||
["exe", "cmd", "bat", "com"]
|
||||
.iter()
|
||||
.any(|expected| extension.eq_ignore_ascii_case(expected))
|
||||
})
|
||||
}
|
||||
#[cfg(not(any(unix, target_os = "windows")))]
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn find_named_agent_executable_in_directories(
|
||||
directories: &[PathBuf],
|
||||
names: &[&str],
|
||||
) -> Option<PathBuf> {
|
||||
for directory in directories {
|
||||
for name in names {
|
||||
#[cfg(target_os = "windows")]
|
||||
let candidates = [
|
||||
directory.join(format!("{name}.exe")),
|
||||
directory.join(format!("{name}.cmd")),
|
||||
directory.join(format!("{name}.bat")),
|
||||
directory.join(name),
|
||||
directory.join(format!("{name}.com")),
|
||||
];
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let candidates = [directory.join(name)];
|
||||
if let Some(candidate) = candidates.into_iter().find(|path| path.is_file()) {
|
||||
if let Some(candidate) = candidates
|
||||
.into_iter()
|
||||
.find(|path| agent_cli_executable(path))
|
||||
{
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
@@ -2534,6 +2566,18 @@ pub(crate) fn find_named_agent_executable(home: &Path, names: &[&str]) -> Option
|
||||
}
|
||||
|
||||
pub(crate) fn agent_executable_directories(home: &Path) -> Vec<PathBuf> {
|
||||
#[cfg(target_os = "windows")]
|
||||
let registered = windows_registered_path_directories();
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let registered = Vec::new();
|
||||
agent_executable_directories_from_environment(home, |name| env::var_os(name), ®istered)
|
||||
}
|
||||
|
||||
pub(crate) fn agent_executable_directories_from_environment(
|
||||
home: &Path,
|
||||
environment: impl Fn(&str) -> Option<std::ffi::OsString>,
|
||||
registered: &[PathBuf],
|
||||
) -> Vec<PathBuf> {
|
||||
let mut directories = Vec::new();
|
||||
let mut push = |path: PathBuf| {
|
||||
if !path.as_os_str().is_empty() && !directories.iter().any(|item| item == &path) {
|
||||
@@ -2541,28 +2585,46 @@ pub(crate) fn agent_executable_directories(home: &Path) -> Vec<PathBuf> {
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(path) = env::var_os("PATH") {
|
||||
if let Some(path) = environment("PATH") {
|
||||
env::split_paths(&path).for_each(&mut push);
|
||||
}
|
||||
registered.iter().cloned().for_each(&mut push);
|
||||
[
|
||||
home.join(".local/bin"),
|
||||
home.join(".npm-global/bin"),
|
||||
home.join(".bun/bin"),
|
||||
home.join(".cargo/bin"),
|
||||
home.join(".volta/bin"),
|
||||
home.join(".local/share/pnpm"),
|
||||
home.join(".pyenv/shims"),
|
||||
home.join("bin"),
|
||||
]
|
||||
.into_iter()
|
||||
.for_each(&mut push);
|
||||
for variable in ["PNPM_HOME", "BUN_INSTALL", "NPM_CONFIG_PREFIX"] {
|
||||
if let Some(path) = env::var_os(variable) {
|
||||
for variable in [
|
||||
"PNPM_HOME",
|
||||
"BUN_INSTALL",
|
||||
"NPM_CONFIG_PREFIX",
|
||||
"npm_config_prefix",
|
||||
"VOLTA_HOME",
|
||||
"UV_TOOL_BIN_DIR",
|
||||
"NVM_SYMLINK",
|
||||
] {
|
||||
if let Some(path) = environment(variable).filter(|path| !path.is_empty()) {
|
||||
let path = PathBuf::from(path);
|
||||
push(
|
||||
if variable == "BUN_INSTALL" || variable == "NPM_CONFIG_PREFIX" {
|
||||
if matches!(variable, "NPM_CONFIG_PREFIX" | "npm_config_prefix") {
|
||||
push(if cfg!(target_os = "windows") {
|
||||
path
|
||||
} else {
|
||||
path.join("bin")
|
||||
});
|
||||
} else {
|
||||
push(if matches!(variable, "BUN_INSTALL" | "VOLTA_HOME") {
|
||||
path.join("bin")
|
||||
} else {
|
||||
path
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
for root in [
|
||||
@@ -2570,8 +2632,14 @@ pub(crate) fn agent_executable_directories(home: &Path) -> Vec<PathBuf> {
|
||||
home.join(".local/state/fnm_multishells"),
|
||||
] {
|
||||
if let Ok(entries) = fs::read_dir(root) {
|
||||
for entry in entries.flatten() {
|
||||
push(entry.path().join("bin"));
|
||||
let mut entries = entries
|
||||
.flatten()
|
||||
.map(|entry| entry.path())
|
||||
.filter(|path| path.is_dir())
|
||||
.collect::<Vec<_>>();
|
||||
entries.sort();
|
||||
for entry in entries.into_iter().rev() {
|
||||
push(entry.join("bin"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2586,11 +2654,31 @@ pub(crate) fn agent_executable_directories(home: &Path) -> Vec<PathBuf> {
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if let Some(app_data) = env::var_os("APPDATA") {
|
||||
push(PathBuf::from(app_data).join("npm"));
|
||||
}
|
||||
if let Some(local_app_data) = env::var_os("LOCALAPPDATA") {
|
||||
push(PathBuf::from(local_app_data).join("Microsoft/WindowsApps"));
|
||||
let app_data = environment("APPDATA")
|
||||
.filter(|path| !path.is_empty())
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| home.join("AppData/Roaming"));
|
||||
let local = environment("LOCALAPPDATA")
|
||||
.filter(|path| !path.is_empty())
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| home.join("AppData/Local"));
|
||||
push(app_data.join("npm"));
|
||||
push(home.join(".npm-global"));
|
||||
push(local.join("Microsoft/WindowsApps"));
|
||||
push(local.join("pnpm"));
|
||||
push(local.join("Volta/bin"));
|
||||
for root in [local.join("Programs/Python"), local.join("Python")] {
|
||||
if let Ok(entries) = fs::read_dir(root) {
|
||||
let mut entries = entries
|
||||
.flatten()
|
||||
.map(|entry| entry.path())
|
||||
.filter(|path| path.is_dir())
|
||||
.collect::<Vec<_>>();
|
||||
entries.sort();
|
||||
for entry in entries.into_iter().rev() {
|
||||
push(entry.join("Scripts"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -212,6 +212,23 @@ fn current_version_roots() -> [RegistryRoot; 4] {
|
||||
]
|
||||
}
|
||||
|
||||
pub(crate) fn windows_registered_path_directories() -> Vec<PathBuf> {
|
||||
let mut paths = Vec::new();
|
||||
for (hive, key) in [
|
||||
(HKEY_CURRENT_USER, r"Environment"),
|
||||
(
|
||||
HKEY_LOCAL_MACHINE,
|
||||
r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment",
|
||||
),
|
||||
] {
|
||||
if let Some(value) = RegistryKey::open(hive, key).and_then(|key| key.string("Path")) {
|
||||
let expanded = expand_environment_strings(&value).unwrap_or(value);
|
||||
paths.extend(env::split_paths(&expanded).filter(|path| path.is_absolute()));
|
||||
}
|
||||
}
|
||||
paths
|
||||
}
|
||||
|
||||
fn join_registry_path(prefix: &str, suffix: &str) -> String {
|
||||
format!("{prefix}\\{suffix}")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,128 @@
|
||||
use super::support::*;
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn installation_detection_separates_executables_from_version_metadata() {
|
||||
for client in [
|
||||
AgentClient::ClaudeCode,
|
||||
AgentClient::Codex,
|
||||
AgentClient::OpenCode,
|
||||
AgentClient::OpenClaw,
|
||||
AgentClient::Hermes,
|
||||
AgentClient::DeepSeekHarness,
|
||||
AgentClient::KimiCode,
|
||||
AgentClient::GrokBuild,
|
||||
] {
|
||||
assert!(agent_installation_detected(client, None, true, false));
|
||||
assert!(!agent_installation_detected(client, None, false, false));
|
||||
assert!(!agent_installation_detected(
|
||||
client,
|
||||
Some("1.0.0"),
|
||||
false,
|
||||
false
|
||||
));
|
||||
}
|
||||
assert!(agent_installation_detected(
|
||||
AgentClient::Codex,
|
||||
None,
|
||||
false,
|
||||
true
|
||||
));
|
||||
assert!(agent_installation_detected(
|
||||
AgentClient::ClaudeDesktop,
|
||||
Some("1.0.0"),
|
||||
false,
|
||||
false,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn executable_directories_preserve_path_precedence_and_package_manager_locations() {
|
||||
let home = agent_test_home("agent-search-environment");
|
||||
let inherited = home.join("inherited");
|
||||
let registered = home.join("registered");
|
||||
let npm = home.join("custom-npm");
|
||||
let uv = home.join("custom-uv");
|
||||
let volta = home.join("custom-volta");
|
||||
let directories = agent_executable_directories_from_environment(
|
||||
&home,
|
||||
|name| match name {
|
||||
"PATH" => Some(env::join_paths([&inherited, &inherited]).unwrap()),
|
||||
"NPM_CONFIG_PREFIX" => Some(npm.clone().into_os_string()),
|
||||
"UV_TOOL_BIN_DIR" => Some(uv.clone().into_os_string()),
|
||||
"VOLTA_HOME" => Some(volta.clone().into_os_string()),
|
||||
"BUN_INSTALL" => Some(std::ffi::OsString::new()),
|
||||
_ => None,
|
||||
},
|
||||
&[registered.clone(), inherited.clone()],
|
||||
);
|
||||
assert_eq!(&directories[..2], &[inherited.clone(), registered]);
|
||||
assert_eq!(
|
||||
directories
|
||||
.iter()
|
||||
.filter(|path| **path == inherited)
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
let npm_bin = if cfg!(target_os = "windows") {
|
||||
npm
|
||||
} else {
|
||||
npm.join("bin")
|
||||
};
|
||||
assert!(directories.contains(&npm_bin));
|
||||
assert!(directories.contains(&uv));
|
||||
assert!(directories.contains(&volta.join("bin")));
|
||||
assert!(directories.iter().all(|path| path.is_absolute()));
|
||||
fs::remove_dir_all(home).unwrap();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn windows_cli_search_skips_shell_stubs_and_finds_runnable_shims() {
|
||||
let home = agent_test_home("agent-search-shims");
|
||||
let first = home.join("first");
|
||||
let second = home.join("second");
|
||||
fs::create_dir_all(first.join("agent.exe")).unwrap();
|
||||
fs::create_dir_all(&second).unwrap();
|
||||
fs::write(first.join("agent"), "shell stub").unwrap();
|
||||
fs::write(first.join("agent.ps1"), "powershell stub").unwrap();
|
||||
let executable = second.join("agent.cmd");
|
||||
fs::write(&executable, "@exit /b 1\r\n").unwrap();
|
||||
assert_eq!(
|
||||
find_named_agent_executable_in_directories(&[first, second], &["agent"]),
|
||||
Some(executable.clone()),
|
||||
);
|
||||
let version = read_agent_version(&executable, &home);
|
||||
assert_eq!(version, None);
|
||||
assert!(agent_installation_detected(
|
||||
AgentClient::ClaudeCode,
|
||||
version.as_deref(),
|
||||
agent_cli_executable(&executable),
|
||||
false,
|
||||
));
|
||||
fs::remove_dir_all(home).unwrap();
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn unix_cli_search_requires_executable_permissions() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let home = agent_test_home("agent-search-permissions");
|
||||
let executable = home.join("agent");
|
||||
fs::write(&executable, "exit 1\n").unwrap();
|
||||
fs::set_permissions(&executable, fs::Permissions::from_mode(0o644)).unwrap();
|
||||
assert_eq!(
|
||||
find_named_agent_executable_in_directories(&[home.clone()], &["agent"]),
|
||||
None
|
||||
);
|
||||
fs::set_permissions(&executable, fs::Permissions::from_mode(0o755)).unwrap();
|
||||
assert_eq!(
|
||||
find_named_agent_executable_in_directories(&[home.clone()], &["agent"]),
|
||||
Some(executable)
|
||||
);
|
||||
fs::remove_dir_all(home).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configuration_paths_ignore_inherited_environment() {
|
||||
let home = agent_test_home("isolated-paths");
|
||||
|
||||
+116
-12
@@ -224,8 +224,11 @@ pub(crate) struct UsageRecord {
|
||||
api_group_key: String,
|
||||
#[serde(default)]
|
||||
model: String,
|
||||
#[serde(default)]
|
||||
#[serde(default, alias = "responseModel")]
|
||||
response_model: String,
|
||||
// Derived from the active price catalog when reading a page; never trust imported estimates.
|
||||
#[serde(default, skip_deserializing)]
|
||||
cost: Option<UsageRecordCost>,
|
||||
#[serde(default)]
|
||||
alias: String,
|
||||
#[serde(default, skip_serializing)]
|
||||
@@ -264,6 +267,12 @@ pub(crate) struct UsageRecord {
|
||||
tokens: UsageTokenStats,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
struct UsageRecordCost {
|
||||
total: f64,
|
||||
pricing_model: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct LegacyUsageHourFile {
|
||||
@@ -2765,9 +2774,16 @@ fn normalize_usage_record(value: Value, config: &GuiConfigFile) -> Result<UsageR
|
||||
failure_body,
|
||||
provider,
|
||||
api_group_key,
|
||||
model: string_field(object, "model").unwrap_or_else(|| "unknown".to_string()),
|
||||
response_model: string_field(object, "response_model").unwrap_or_default(),
|
||||
alias: string_field(object, "alias").unwrap_or_default(),
|
||||
model: string_field(object, "resolved_model")
|
||||
.or_else(|| string_field(object, "resolvedModel"))
|
||||
.or_else(|| string_field(object, "model"))
|
||||
.unwrap_or_else(|| "unknown".to_string()),
|
||||
response_model: string_field(object, "response_model")
|
||||
.or_else(|| string_field(object, "responseModel")).unwrap_or_default(),
|
||||
cost: None,
|
||||
alias: string_field(object, "alias")
|
||||
.or_else(|| string_field(object, "requested_model"))
|
||||
.or_else(|| string_field(object, "requestedModel")).unwrap_or_default(),
|
||||
client_ip: string_field(object, "client_ip"),
|
||||
x_forwarded_for: string_field(object, "x_forwarded_for"),
|
||||
user_agent: string_field(object, "user_agent"),
|
||||
@@ -3234,19 +3250,49 @@ fn cost_for_price(model: &str, service_tier: &str, tokens: &CostTokens, price: &
|
||||
long_output_multiplier,
|
||||
);
|
||||
let tier = service_tier.trim().to_ascii_lowercase();
|
||||
let multiplier = if tokens.long_input > 0
|
||||
&& matches!(tier.as_str(), "priority" | "fast")
|
||||
let multiplier = match tier.as_str() {
|
||||
"flex" | "batch" => 0.5,
|
||||
"priority" | "fast" => service_tier_multiplier(model),
|
||||
_ => 1.0,
|
||||
};
|
||||
// A grouped total can contain both context lengths. Long-context overrides
|
||||
// must not suppress the service-tier multiplier on its short requests.
|
||||
let long_multiplier = if matches!(tier.as_str(), "priority" | "fast")
|
||||
&& !is_model_family(model, "gpt-6")
|
||||
{
|
||||
1.0
|
||||
} else {
|
||||
match tier.as_str() {
|
||||
"flex" | "batch" => 0.5,
|
||||
"priority" | "fast" => service_tier_multiplier(model),
|
||||
_ => 1.0,
|
||||
}
|
||||
multiplier
|
||||
};
|
||||
(short_cost + long_cost) * multiplier
|
||||
short_cost * multiplier + long_cost * long_multiplier
|
||||
}
|
||||
|
||||
fn usage_record_cost(record: &UsageRecord, prices: &HashMap<String, ModelPrice>) -> Option<UsageRecordCost> {
|
||||
let (model, price) = resolve_model_price(&record.model, &record.alias, prices)?;
|
||||
let input = record.tokens.input_tokens;
|
||||
// Keep the same context buckets and tier selection as the aggregate pricing query.
|
||||
let long = input > LONG_CONTEXT_INPUT_TOKEN_THRESHOLD
|
||||
|| (input > GROK_47_LONG_CONTEXT_INPUT_TOKEN_THRESHOLD && is_model_family(model, "grok-4.7"));
|
||||
let tokens = CostTokens {
|
||||
input,
|
||||
output: record.tokens.output_tokens,
|
||||
cache_read: record.tokens.cache_read_tokens,
|
||||
cache_creation: record.tokens.cache_creation_tokens,
|
||||
long_input: if long { input } else { 0 },
|
||||
long_output: if long { record.tokens.output_tokens } else { 0 },
|
||||
long_cache_read: if long { record.tokens.cache_read_tokens } else { 0 },
|
||||
long_cache_creation: if long { record.tokens.cache_creation_tokens } else { 0 },
|
||||
};
|
||||
let identity = format!("{} {} {}", record.executor_type, record.provider, record.auth_type).to_ascii_lowercase();
|
||||
let tier = if identity.contains("codex") || record.response_service_tier.trim().is_empty() {
|
||||
&record.service_tier
|
||||
} else {
|
||||
&record.response_service_tier
|
||||
};
|
||||
Some(UsageRecordCost {
|
||||
total: cost_for_price(model, tier, &tokens, &price),
|
||||
pricing_model: price.model,
|
||||
})
|
||||
}
|
||||
|
||||
fn cost_for_token_segment(
|
||||
@@ -4243,8 +4289,10 @@ fn load_usage_events(
|
||||
.map_err(|error| format!("Failed to query SQLite usage events: {error}"))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|error| format!("Failed to read SQLite usage events: {error}"))?;
|
||||
let prices = load_model_prices(connection)?;
|
||||
for item in &mut items {
|
||||
item.source_display = usage_source_display(config, &item.provider, &item.source);
|
||||
item.cost = usage_record_cost(item, &prices);
|
||||
}
|
||||
Ok(UsageEventPage {
|
||||
items,
|
||||
@@ -4273,6 +4321,7 @@ fn usage_record_from_row(row: &Row<'_>) -> rusqlite::Result<UsageRecord> {
|
||||
api_group_key: row.get(20)?,
|
||||
model: row.get(8)?,
|
||||
response_model: row.get(37)?,
|
||||
cost: None,
|
||||
alias: row.get(9)?,
|
||||
client_ip: row.get(21)?,
|
||||
x_forwarded_for: row.get(22)?,
|
||||
@@ -5044,6 +5093,7 @@ mod tests {
|
||||
api_group_key: "hash".to_string(),
|
||||
model: model.to_string(),
|
||||
response_model: String::new(),
|
||||
cost: None,
|
||||
alias: String::new(),
|
||||
client_ip: None,
|
||||
x_forwarded_for: None,
|
||||
@@ -6586,6 +6636,60 @@ mod tests {
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_model_accepts_manager_plus_field_names() {
|
||||
let record = normalize_usage_record(serde_json::json!({
|
||||
"request_id": "compatible-models", "requestedModel": "client-alias",
|
||||
"resolvedModel": "gpt-6-sol", "model": "display-model",
|
||||
"responseModel": " reported-model "
|
||||
}), &GuiConfigFile::default()).unwrap();
|
||||
assert_eq!(record.alias, "client-alias");
|
||||
assert_eq!(record.model, "gpt-6-sol");
|
||||
assert_eq!(record.response_model, "reported-model");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_cost_matches_aggregate_and_recalculates_current_prices() {
|
||||
let mut connection = Connection::open_in_memory().unwrap();
|
||||
initialize_usage_schema(&connection).unwrap();
|
||||
let mut records = Vec::new();
|
||||
for (index, model) in ["gpt-6-sol", "gpt-5.6-terra", "xai/grok-4.7"].iter().enumerate() {
|
||||
for input in [200_000, 250_000, 300_000] {
|
||||
let mut record = sample_record(&format!("{index}-{input}"), "2026-10-04T00:00:00Z", model);
|
||||
record.provider = if index < 2 { "codex" } else { "openai" }.into();
|
||||
record.service_tier = "fast".into();
|
||||
record.response_service_tier = "flex".into();
|
||||
record.tokens.input_tokens = input;
|
||||
record.tokens.output_tokens = 500;
|
||||
record.tokens.reasoning_tokens = 100;
|
||||
record.tokens.cache_read_tokens = 100_000;
|
||||
record.tokens.cache_creation_tokens = 1_000;
|
||||
record.tokens.total_tokens = input + 500;
|
||||
record.response_model = "unpriced-response-must-not-change-billing".into();
|
||||
records.push(record);
|
||||
}
|
||||
}
|
||||
records.push(sample_record("unknown-price", "2026-10-04T00:00:00Z", "unpriced-model"));
|
||||
insert_usage_records(&mut connection, &records).unwrap();
|
||||
let query = UsageQuery::default();
|
||||
let page = load_usage_events(&connection, &query, &GuiConfigFile::default()).unwrap();
|
||||
let (aggregate, priced) = load_estimated_cost(&connection, &build_usage_filter(&query)).unwrap();
|
||||
let total: f64 = page.items.iter().filter_map(|r| r.cost.as_ref().map(|c| c.total)).sum();
|
||||
assert!((aggregate - total).abs() < 1e-9);
|
||||
assert_eq!(priced, 9);
|
||||
assert!(page.items.iter().find(|r| r.id == "unknown-price").unwrap().cost.is_none());
|
||||
|
||||
let free_price = ModelPrice { model: "unpriced-model".into(), prompt_configured: true,
|
||||
completion_configured: true, cache_read_configured: true, cache_creation_configured: true,
|
||||
source: "manual".into(), ..ModelPrice::default() };
|
||||
upsert_model_price(&connection, &free_price).unwrap();
|
||||
let updated = load_usage_events(&connection, &query, &GuiConfigFile::default()).unwrap();
|
||||
let free = updated.items.iter().find(|r| r.id == "unknown-price").unwrap();
|
||||
assert_eq!(free.cost.as_ref().unwrap().total, 0.0);
|
||||
let imported: UsageRecord = serde_json::from_value(serde_json::to_value(free).unwrap()).unwrap();
|
||||
assert!(imported.cost.is_none(), "Imported estimates must be recomputed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn estimates_gpt56_cost_with_cache_and_service_tier_rules() {
|
||||
let terra = official_model_price("openai/gpt-5.6-terra").unwrap();
|
||||
|
||||
Reference in new issue
Block a user