Skip to content

TUI cost widget shows zero dollar cost for OpenAI-compatible API-key providers (DeepSeek): remote billing gate uses a hard-coded provider allow-list #831

Description

@ainoob2025

Summary

The TUI cost widget shows no dollar cost (stays at $0) for OpenAI-compatible API-key providers such as DeepSeek, even though the models.dev pricing catalog has correct prices and the local cost path resolves them.

Version

  • jcode v0.68.0 (fcf5390), Linux x86_64, DeepSeek provider (OpenAI-compatible)
  • Remote/TUI mode (the default interactive client talks to the daemon)

Root cause

resolve_remote_cost_pricing() in crates/jcode-tui/src/tui/app/misc_ui.rs decides whether a remote session is billed per token using a hard-coded provider-name allow-list:

let billed = if is_anthropic || is_openai {
    api_key_billed
} else {
    // Providers that are inherently cost-based when proxied remotely.
    provider_name.contains("opencode")
        || provider_name.contains("openrouter")
        || provider_name.contains("bedrock")
        || provider_name.contains("cerebras")
        || provider_name.contains("compatible")
};

The daemon reports the active provider's display name via provider_name() -> runtime_display_name(). For DeepSeek (an OpenAI-compatible profile) that is "DeepSeek", which matches none of the allow-list strings. So billed == false, the function returns None, and accrue_remote_call_cost never adds anything — the cost stays $0.

The local cost path (update_cost_impl, same file) does this correctly:

crate::provider_catalog::openai_compatible_profile_by_id(provider_name.trim())
    .is_some_and(|profile| profile.requires_api_key)

So the asymmetry is: local sessions bill correctly, remote (default TUI) sessions don't.

Verified

  • The models.dev catalog resolves DeepSeek correctly (deepseek-v4-flash = $0.14 input / $0.28 output / $0.0028 cache-read) via model_pricing::lookup.
  • The problem is purely the billed gate above returning false for the display-name "DeepSeek".

Proposed fix

Apply the same profile-aware requires_api_key check the local path uses to the remote path. Resolve by id OR display name, because the daemon sends the display name (no openai-compatible: prefix):

let billed = if is_anthropic || is_openai {
    api_key_billed
} else {
    // Providers that are inherently cost-based when proxied remotely.
    provider_name.contains("opencode")
        || provider_name.contains("openrouter")
        || provider_name.contains("bedrock")
        || provider_name.contains("cerebras")
        || provider_name.contains("compatible")
        // Any OpenAI-compatible profile that requires an API key meters
        // per token when proxied remotely (DeepSeek, Z.AI, NVIDIA NIM,
        // Moonshot, ...). The daemon reports the profile's display name,
        // which does not carry the `openai-compatible:` prefix, so resolve
        // by id OR display name and honor `requires_api_key` exactly like
        // the local cost path does.
        || crate::provider_catalog::openai_compatible_profile_id_for_display_name(
            &provider_name,
        )
        .is_some_and(|profile_id| {
            crate::provider_catalog::openai_compatible_profile_by_id(profile_id)
                .is_some_and(|profile| profile.requires_api_key)
        })
};

Both helpers already exist and are pub in crates/jcode-base/src/provider_catalog.rs (openai_compatible_profile_id_for_display_name matches id OR display name; openai_compatible_profile_by_id(...).requires_api_key matches the local path's semantics).

Verification of the fix

Verified against the real profile catalog via an e2e probe:

provider_name before after
DeepSeek / deepseek false true
Z.AI, Moonshot AI false true
ollama (no API key) false false
OpenRouter, OpenAI, Claude true true

Environment

  • jcode v0.68.0, Linux x86_64, DeepSeek provider.
  • Full cargo build on this machine is blocked by a missing libssl-dev (unrelated to this change); the change only adds pub helper calls already used elsewhere.

Metadata

Metadata

Assignees

No one assigned

    Labels

    autonomous: likelyProbably hands-off: clearly worth fixing, agent can do it, minor judgment needed.bugSomething isn't workingtriage: fixed-pending-releaseFixed in code/committed; will close automatically on next releasetriage: reproducibleClear repro + clear fix path

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions