Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Cross-package release notes for relayburn. Package changelogs contain package-le
## [Unreleased]

- Pricing recognizes Claude 5 and GPT-5.6 models, prefers first-party tariffs over reseller duplicates, and applies long-context price tiers.
- Pricing reports retired first-party models, including the `codex-auto-review` alias, as unpriced instead of inheriting reseller tariffs; summary buckets expose their unpriced-turn count, while a ledger-home `models.dev.json` override can explicitly restore pricing.
- `burn hotspots --findings` surfaces unknown model pricing explicitly and ranks unpriced sessions by token volume instead of treating them as $0.00.

## [4.0.0] - 2026-06-23
Expand Down
2 changes: 1 addition & 1 deletion crates/relayburn-cli/src/commands/summary/human.rs
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,7 @@ pub(super) fn emit_human(
report.unpriced_turns, models,
);
eprintln!(
" Update the snapshot (pnpm run pricing:update) or add an override at $RELAYBURN_HOME/models.dev.json.",
" Update the snapshot (pnpm run pricing:update) or add an override at <ledger-home>/models.dev.json.",
);
}
}
Expand Down
4 changes: 4 additions & 0 deletions crates/relayburn-cli/src/commands/summary/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ pub(super) fn grouped_json_value(
"stopReasons".into(),
stop_reasons_to_json(&report.stop_reasons),
);
payload.insert("unpricedTurns".into(), json!(report.unpriced_turns));
if !report.unpriced_models.is_empty() {
payload.insert("unpricedModels".into(), json!(report.unpriced_models));
}
if !report.subagents.is_empty() {
// `subagents: {paired, orphan, total}` (issue #435). Skipped
// when both buckets are zero so the JSON shape stays compact
Expand Down
53 changes: 24 additions & 29 deletions crates/relayburn-cli/src/commands/summary/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -457,9 +457,8 @@ mod tests {
assert!(format!("{duplicate}").contains("duplicate --tag filter"));
}

#[test]
fn grouped_json_includes_quality_when_report_has_it() {
let report = SummaryGroupedReport {
fn empty_grouped_report() -> SummaryGroupedReport {
SummaryGroupedReport {
group_by: SummaryGroupBy::Model,
tag_key: None,
tag_values: Vec::new(),
Expand All @@ -479,16 +478,35 @@ mod tests {
replacement_savings: relayburn_sdk::ReplacementSavingsSummary::default(),
stop_reasons: relayburn_sdk::StopReasonCounts::default(),
subagents: SubagentCounts::default(),
quality: Some(QualityResult::default()),
quality: None,
unpriced_turns: 0,
unpriced_models: Vec::new(),
};
}
}

#[test]
fn grouped_json_includes_quality_when_report_has_it() {
let mut report = empty_grouped_report();
report.quality = Some(QualityResult::default());

let value = grouped_json_value(&report, &relayburn_sdk::IngestReport::empty());

assert_eq!(value["quality"], json!({"outcomes": [], "oneShot": []}));
}

#[test]
fn grouped_json_surfaces_unpriced_turns_and_models() {
let mut report = empty_grouped_report();
report.turn_count = 1;
report.unpriced_turns = 1;
report.unpriced_models = vec!["gpt-5-codex".into()];

let value = grouped_json_value(&report, &relayburn_sdk::IngestReport::empty());

assert_eq!(value["unpricedTurns"], 1);
assert_eq!(value["unpricedModels"], json!(["gpt-5-codex"]));
}

#[test]
fn subagents_line_renders_only_when_counts_nonzero() {
// Empty bucket → skipped, line absent (so old summaries keep
Expand All @@ -514,30 +532,7 @@ mod tests {
fn subagents_json_payload_includes_total_and_omits_when_empty() {
// Empty bucket → key absent in JSON so `summary.json | jq` for
// pre-#435 callers still passes without a `?.` guard.
let mut report = SummaryGroupedReport {
group_by: SummaryGroupBy::Model,
tag_key: None,
tag_values: Vec::new(),
turn_count: 0,
rows: Vec::new(),
total_cost: CostBreakdown {
model: String::new().into(),
total: 0.0,
input: 0.0,
output: 0.0,
reasoning: 0.0,
cache_read: 0.0,
cache_create: 0.0,
},
fidelity: relayburn_sdk::summarize_fidelity(&[]),
per_cell_fidelity: json!({"groupBy": "model"}),
replacement_savings: relayburn_sdk::ReplacementSavingsSummary::default(),
stop_reasons: relayburn_sdk::StopReasonCounts::default(),
subagents: SubagentCounts::default(),
quality: None,
unpriced_turns: 0,
unpriced_models: Vec::new(),
};
let mut report = empty_grouped_report();
let value = grouped_json_value(&report, &relayburn_sdk::IngestReport::empty());
assert!(
value.get("subagents").is_none(),
Expand Down
206 changes: 206 additions & 0 deletions crates/relayburn-sdk/data/primary-model-ids.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
[
"chatgpt-image-latest",
"claude-3-5-haiku-20241022",
"claude-3-5-haiku-latest",
"claude-3-5-haiku@20241022",
"claude-3-5-sonnet-20240620",
"claude-3-5-sonnet-20241022",
"claude-3-5-sonnet@20241022",
"claude-3-7-sonnet-20250219",
"claude-3-7-sonnet@20250219",
"claude-3-haiku-20240307",
"claude-3-opus-20240229",
"claude-3-sonnet-20240229",
"claude-fable-5",
"claude-haiku-4-5",
"claude-haiku-4-5-20251001",
"claude-haiku-4-5@20251001",
"claude-opus-4-0",
"claude-opus-4-1",
"claude-opus-4-1-20250805",
"claude-opus-4-1@20250805",
"claude-opus-4-20250514",
"claude-opus-4-5",
"claude-opus-4-5-20251101",
"claude-opus-4-5@20251101",
"claude-opus-4-6",
"claude-opus-4-6@default",
"claude-opus-4-7",
"claude-opus-4-7@default",
"claude-opus-4-8",
"claude-opus-4-8@default",
"claude-opus-4@20250514",
"claude-opus-5",
"claude-opus-5@default",
"claude-sonnet-4-0",
"claude-sonnet-4-20250514",
"claude-sonnet-4-5",
"claude-sonnet-4-5-20250929",
"claude-sonnet-4-5@20250929",
"claude-sonnet-4-6",
"claude-sonnet-4-6@default",
"claude-sonnet-4@20250514",
"claude-sonnet-5",
"claude-sonnet-5@default",
"codex-auto-review",
"deep-research-max-preview-04-2026",
"deep-research-preview-04-2026",
"deepseek-ai/deepseek-v3.1-maas",
"deepseek-ai/deepseek-v3.2-maas",
"gemini-1.5-flash",
"gemini-1.5-flash-8b",
"gemini-1.5-pro",
"gemini-2.0-flash",
"gemini-2.0-flash-lite",
"gemini-2.5-computer-use-preview-10-2025",
"gemini-2.5-flash",
"gemini-2.5-flash-image",
"gemini-2.5-flash-image-preview",
"gemini-2.5-flash-lite",
"gemini-2.5-flash-lite-preview-06-17",
"gemini-2.5-flash-lite-preview-09-2025",
"gemini-2.5-flash-preview-04-17",
"gemini-2.5-flash-preview-05-20",
"gemini-2.5-flash-preview-09-2025",
"gemini-2.5-flash-preview-tts",
"gemini-2.5-flash-tts",
"gemini-2.5-pro",
"gemini-2.5-pro-preview-05-06",
"gemini-2.5-pro-preview-06-05",
"gemini-2.5-pro-preview-tts",
"gemini-2.5-pro-tts",
"gemini-3-flash-preview",
"gemini-3-pro-image",
"gemini-3-pro-image-preview",
"gemini-3-pro-preview",
"gemini-3.1-flash-image",
"gemini-3.1-flash-image-preview",
"gemini-3.1-flash-lite",
"gemini-3.1-flash-lite-image",
"gemini-3.1-flash-lite-preview",
"gemini-3.1-flash-live-preview",
"gemini-3.1-flash-tts-preview",
"gemini-3.1-pro-preview",
"gemini-3.1-pro-preview-customtools",
"gemini-3.5-flash",
"gemini-3.5-flash-lite",
"gemini-3.5-live-translate-preview",
"gemini-3.6-flash",
"gemini-embedding-001",
"gemini-embedding-2",
"gemini-flash-latest",
"gemini-flash-lite-latest",
"gemini-live-2.5-flash",
"gemini-live-2.5-flash-preview-native-audio",
"gemini-omni-flash-preview",
"gemini-robotics-er-1.6-preview",
"gemma-3-12b-it",
"gemma-3-27b-it",
"gemma-3-4b-it",
"gemma-3n-e2b-it",
"gemma-3n-e4b-it",
"gemma-4-26b-a4b-it",
"gemma-4-31b-it",
"gpt-3.5-turbo",
"gpt-4",
"gpt-4-turbo",
"gpt-4.1",
"gpt-4.1-mini",
"gpt-4.1-nano",
"gpt-4o",
"gpt-4o-2024-05-13",
"gpt-4o-2024-08-06",
"gpt-4o-2024-11-20",
"gpt-4o-mini",
"gpt-5",
"gpt-5-chat-latest",
"gpt-5-codex",
"gpt-5-mini",
"gpt-5-nano",
"gpt-5-pro",
"gpt-5.1",
"gpt-5.1-chat-latest",
"gpt-5.1-codex",
"gpt-5.1-codex-max",
"gpt-5.1-codex-mini",
"gpt-5.2",
"gpt-5.2-chat-latest",
"gpt-5.2-codex",
Comment thread
willwashburn marked this conversation as resolved.
"gpt-5.2-pro",
"gpt-5.3-chat-latest",
"gpt-5.3-codex",
"gpt-5.3-codex-spark",
"gpt-5.4",
"gpt-5.4-mini",
"gpt-5.4-nano",
"gpt-5.4-pro",
"gpt-5.5",
"gpt-5.5-pro",
"gpt-5.6",
"gpt-5.6-luna",
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-image-1",
"gpt-image-1-mini",
"gpt-image-1.5",
"gpt-image-2",
"gpt-realtime-2.1",
"grok-2",
"grok-2-1212",
"grok-2-latest",
"grok-2-vision",
"grok-2-vision-1212",
"grok-2-vision-latest",
"grok-3",
"grok-3-fast",
"grok-3-fast-latest",
"grok-3-latest",
"grok-3-mini",
"grok-3-mini-fast",
"grok-3-mini-fast-latest",
"grok-3-mini-latest",
"grok-4",
"grok-4-1-fast",
"grok-4-1-fast-non-reasoning",
"grok-4-fast",
"grok-4-fast-non-reasoning",
"grok-4.20-0309-non-reasoning",
"grok-4.20-0309-reasoning",
"grok-4.20-multi-agent-0309",
"grok-4.3",
"grok-4.5",
"grok-beta",
"grok-build-0.1",
"grok-code-fast-1",
"grok-imagine-image",
"grok-imagine-image-quality",
"grok-imagine-video",
"grok-imagine-video-1.5",
"grok-vision-beta",
"lyria-3-clip-preview",
"lyria-3-pro-preview",
"meta/llama-3.3-70b-instruct-maas",
"meta/llama-4-maverick-17b-128e-instruct-maas",
"moonshotai/kimi-k2-thinking-maas",
"o1",
"o1-mini",
"o1-preview",
"o1-pro",
"o3",
"o3-deep-research",
"o3-mini",
"o3-pro",
"o4-mini",
"o4-mini-deep-research",
"openai/gpt-oss-120b-maas",
"openai/gpt-oss-20b-maas",
"qwen/qwen3-235b-a22b-instruct-2507-maas",
"text-embedding-3-large",
"text-embedding-3-small",
"text-embedding-ada-002",
"veo-3.1-fast-generate-preview",
"veo-3.1-generate-preview",
"veo-3.1-lite-generate-preview",
"zai-org/glm-4.7-maas",
"zai-org/glm-5-maas"
]
Loading
Loading