Skip to content

fix(scorecard): bind costs to provider routes#4351

Open
nightt5879 wants to merge 1 commit into
Hmbown:mainfrom
nightt5879:nightt5879/issue-4335-provider-aware-scorecard
Open

fix(scorecard): bind costs to provider routes#4351
nightt5879 wants to merge 1 commit into
Hmbown:mainfrom
nightt5879:nightt5879/issue-4335-provider-aware-scorecard

Conversation

@nightt5879

Copy link
Copy Markdown
Contributor

Summary

  • accept optional provider / effective_provider provenance in offline scorecard records while keeping legacy model-only JSON readable
  • resolve USD costs from exact (provider, wire_model_id) catalog offerings so Codex OAuth, local/self-hosted, custom, unknown, and unpriced gateway routes fail closed instead of borrowing a model-only price
  • retain DeepSeek's hand-sourced USD/CNY pricing only after an exact DeepSeek offering match, and expose USD/CNY availability separately in text and JSON
  • skip USD cost regression comparisons when either scorecard is incomplete, including legacy baselines that predate provider provenance
  • preserve and surface @findshan's original v0.8.66 EPIC: Token, cache, and context discipline release gate #3388 scorecard credit

Root cause

The offline scorecard accepted only model + usage and called a model-only pricing helper. A model id such as gpt-5.5 could therefore inherit OpenAI API pricing even when the recorded route was ChatGPT/Codex OAuth, Ollama, or another provider where that dollar price is unavailable.

User impact

Token/cache metrics remain available for every record. Dollar and CNY values are now reported only when the recorded provider/model route has authoritative pricing; a real zero-cost turn is distinct from unavailable pricing in both the human summary and JSON output.

Validation

  • cargo fmt --all -- --check — passed
  • git diff --check upstream/main...HEAD — passed
  • cargo test -p codewhale-tui --bin codewhale-tui --locked scorecard — 13 passed
  • cargo test -p codewhale-config --locked — 360 passed
  • real CLI text + --json smoke with OpenAI API, Codex OAuth, and legacy records — passed; API subtotal was $0.0200, while OAuth/legacy and unavailable CNY were explicitly marked unavailable
  • python scripts/check-coauthor-trailers.py --range upstream/main..HEAD --check-authors — passed

The full Windows TUI suite completed with 6,329 passed, 2 ignored, and 5 failures. Two failures passed in isolation. The remaining three setup-matrix failures reproduce unchanged in a detached clean upstream/main@3e97b278 worktree, so they are not caused by this diff:

  • doctor_setup_state_tests::doctor_setup_provider_model_json_covers_cn_codex_and_local_matrix
  • tui::provider_picker::tests::setup_provider_key_entry_matrix_keeps_hosted_codex_and_local_hints_distinct
  • tui::setup::tests::provider_model_detail_lines_keep_codex_oauth_url_free

Local Rust 1.95 clippy --all-targets -D warnings is also blocked by five pre-existing new lints in unchanged compaction.rs, tui/ui.rs, tui/widgets/footer.rs, and two core/engine/tests.rs sites. No clippy warning pointed to this PR's files.

Fixes #4335.

Accept optional provider provenance in offline scorecard records and fail legacy, custom, local, and unknown routes closed instead of guessing price from a model id.

Resolve dollar costs from provider-scoped catalog offerings, retain exact-route DeepSeek USD/CNY pricing, expose per-currency completeness, and skip cost regressions against legacy incomplete baselines.

Fixes Hmbown#4335. Builds on the original Hmbown#3388 scorecard by @findshan.

Co-authored-by: Wenshan Deng <224246733+findshan@users.noreply.github.com>
Signed-off-by: Nightt <87569709+nightt5879@users.noreply.github.com>
@nightt5879 nightt5879 requested a review from Hmbown as a code owner July 12, 2026 02:21
@nightt5879

Copy link
Copy Markdown
Contributor Author

@Hmbown Could you please request a Copilot code review for this PR? GitHub rejects the fork author's request with RequestReviewsByLogin permission. I'll address and resolve every actionable AI review thread before finalizing.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8b76652e06

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/tui/src/main.rs
.iter()
.map(|r| TurnInput {
turn_id: r.turn_id.clone(),
provider: r.provider.as_deref(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Emit provider provenance from turn-end records

When the scorecard input is generated from the current turn_end hook payloads, this field is always None: turn_end_payload still emits model and usage but no provider/effective_provider (crates/tui/src/hooks.rs:1288-1305). Because Scorecard::from_turns now treats missing provider as unpriced and skips USD cost regressions unless cost_complete, existing release-gate recordings silently lose all cost totals/comparisons; wire the effective provider into the hook/export payload or derive it before building TurnInput.

Useful? React with 👍 / 👎.

pub turn_id: String,
#[serde(default, alias = "effective_provider")]
pub provider: Option<String>,
pub model: String,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept effective_model in scorecard records

When users feed persisted runtime TurnRecord exports such as the turns returned by the runtime thread detail API, those rows carry effective_provider and effective_model rather than provider and model (crates/tui/src/runtime_threads.rs:250-256). This deserializer aliases only effective_provider and still requires model, so those records fail to parse even though they contain the provider provenance this change is meant to use; add an effective_model alias before requiring provider-aware pricing.

Useful? React with 👍 / 👎.

let Some(pricing) = OfferingPricing::from_catalog_offering(&offering) else {
return AvailableCost::default();
};
let Some(amount) = pricing.estimate_cost(token_usage) else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Price cache hits when catalog omits a discount

For OpenAI API runs on gpt-5.5-pro (or any priced catalog model with no cache-read discount), a turn with prompt_cache_hit_tokens > 0 reaches estimate_cost, which returns None because cache_read_per_million is absent; the whole turn becomes unpriced even though the route has authoritative input/output prices and the existing pricing table treated gpt-5.5-pro cache hits at the input rate. This makes USD totals and cost regressions disappear for otherwise priced API routes whenever cache telemetry is present.

Useful? React with 👍 / 👎.

);
}

let Some(pricing) = OfferingPricing::from_catalog_offering(&offering) else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve time-aware Anthropic pricing

For Anthropic claude-sonnet-5 records before September 1, 2026, this path uses the bundled catalog's standard 3.00/15.00 rates instead of the existing claude_sonnet_5_pricing intro window, which pricing.rs explicitly keeps at 2.00/10.00 through August 31, 2026. That overstates scorecard cost and can create false cost regressions for this model during the active intro period; keep the existing time-aware pricing override after verifying the provider/model route.

Useful? React with 👍 / 👎.

Comment on lines +132 to +133
let Some(offering) = catalog_offering_for_model(provider, model) else {
return AvailableCost::default();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize supported DeepSeek aliases before lookup

For direct DeepSeek turns recorded with the still-supported compatibility aliases deepseek-chat or deepseek-reasoner (the config marks them valid until July 24, 2026), this exact catalog lookup returns None because the catalog only has the v4 ids, so the scorecard marks those priced DeepSeek calls as unavailable before the pricing layer can map them to the flash rate. Normalize these direct DeepSeek aliases before the catalog check, or otherwise preserve pricing for them until their configured retirement.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make offline scorecard pricing provider-aware

1 participant