#1286·jcode

Direct OpenAI-compatible providers are reported as `openrouter` in logs and in `/usage` spend

Author: ningfangbinCreated Sep 17, 2026Updated Sep 17, 2026

Summary

The openrouter provider is a multiplexing slot: it serves the public OpenRouter aggregator and every direct OpenAI-compatible profile (deepseek, nvidia-nim, zai, ...). The slot's transport name is openrouter / OpenRouter, while the profile-aware name is display_name() (runtime_display_name()).

Several code paths use the transport name instead of the profile-aware name, so sessions that talk to DeepSeek through a direct profile are reported as openrouter:

  1. Log prefix – every line of a DeepSeek session is tagged prv:openrouter|mod:deepseek.
  2. /usage spend ledger – spend is attributed to an openrouter bucket on hosts where OpenRouter is not configured and was never used.
  3. KV cache telemetryprovider=OpenRouter while model=deepseek-flash.

Environment

  • jcode v0.84.0 (57d587899) — release build, Ubuntu 20.04, x86_64
  • Provider: DeepSeek via API key (~/.config/jcode/deepseek.env), openrouter reports not_configured
  • Also reproduced on master (5f33d6239); the offending lines are unchanged there

Steps to reproduce

bash
mkdir /tmp/jcode-repro
cat > /tmp/jcode-repro/config.toml <<'EOF'
[[providers.deepseek.models]]
id = "deepseek-flash"
context_window = 1048576
EOF

# API key: export DEEPSEEK_API_KEY=... or place it at
# $JCODE_HOME/config/jcode/deepseek.env
JCODE_HOME=/tmp/jcode-repro jcode run -p deepseek -m deepseek-flash "hi"
grep 'prv:' /tmp/jcode-repro/logs/*.log

Actual

A single run of the release binary in a clean JCODE_HOME:

[2026-09-17 10:18:33.787] [INFO] [ses:session_macaque_1789|prv:openrouter|mod:deepseek] TURN_CANCEL_REGISTERED ...
[2026-09-17 10:18:33.788] [INFO] [ses:session_macaque_1789|prv:openrouter|mod:deepseek] Locking tool list at 30 tools ...

The same host's existing release-version logs contain prv:OpenRouter|mod:deepseek 503,047 times (2026-09-09) and 115,349 times (2026-09-10). The sessions' own metadata says the provider is DeepSeek ("provider_key": "deepseek" / "openai-compatible:deepseek").

KV cache telemetry on the same host, same release:

[2026-09-16 09:51:35] [INFO] KV_CACHE_USAGE: turn=65 call=1 provider=OpenRouter upstream=None model=deepseek-flash input=799739 cache_read=2432 ...
[2026-09-16 09:50:41] [AUTH] AUTH event=auth_status_check_fast provider=all ... openrouter=not_configured ...

~/.jcode/provider_activity.json on that host (what /usage renders):

json
"openrouter":                 { "month_usd": 629.99, "all_time_usd": 774.80 },
"openai-compatible:deepseek": { "month_usd": 155.73, "all_time_usd": 192.34 }

while jcode auth status reports openrouter not_configured, there is no auth.json and no OpenRouter API key anywhere, and no OpenRouter model was ever selected (the model picker usage contains only DeepSeek, Copilot, NVIDIA NIM and generic OpenAI-compatible entries). The openrouter bucket therefore cannot be genuine OpenRouter spend.

Expected

  • A session using the deepseek profile is reported as deepseek / DeepSeek, with the session's provider key (openai-compatible:deepseek) used wherever a stable identity is needed.
  • /usage attributes spend to the provider that actually served the request.

Root cause

  • crates/jcode-base/src/provider/mod.rs:1776-1800name() returns the transport slot label (ActiveProvider::OpenRouter => "OpenRouter"), while display_name() resolves the active profile through runtime_display_name(). Callers that need "which provider is billing me" must use the latter (or the session provider key), but several use name().
  • crates/jcode-app-core/src/agent/environment.rs:35logging::set_provider_info(self.provider.name(), &self.provider.model()) puts the slot name into every log line.
  • crates/jcode-app-core/src/server/client_state.rs:500 – the History event's provider name falls back to provider.name() (the slot) when the session has no provider_key; the TUI stores that verbatim and reuses it as the billing identity.
  • crates/jcode-tui/src/tui/app/misc_ui.rs:327-341record_api_key_spend derives the ledger key from the TUI's display name plus JCODE_RUNTIME_PROVIDER; a name of OpenRouter with the env var unset maps straight to the openrouter bucket (source_key_for_provider_label, crates/jcode-base/src/provider_activity.rs:310).
  • crates/jcode-base/src/session.rs:282derive_session_provider_key falls back to env vars and finally to the raw name, so a direct profile can be recorded as openrouter.

Suggested fix

Per the contract clarified in #691, Provider::name() should stay the stable machine-facing id. The fix belongs in the surfaces that read it while needing a profile identity:

  • Agent::set_log_context – log display_name() (profile-aware) instead of name().
  • the History provider-name fallback in client_state.rs – same.
  • record_api_key_spend / /usage ledger attribution – use an authoritative provider key rather than re-deriving it from a display label plus process env.
  • derive_session_provider_key – fall back to the profile identity, not the raw transport name.

Related issues

  • #691 – this is the concrete surface report that issue asked for. name() stays the stable machine id; these are the surfaces still reading it where the profile identity is needed.
  • #380 – a specific case of the runtime-identity divergence described there (the History fallback is quoted in that issue).
  • #1116 – same family: provider identity re-derived from a process-global env var, different trigger.
  • #795 / PR #863 – fixed the auth/route half of "OpenRouter reported while not configured". The log prefix and /usage ledger half is still reproducible on v0.84.0 and on master.

Additional context

Related but not yet released: provider-keyed [pricing.providers."<profile>"] rate cards (in development after v0.84.0) are looked up with the same derived provider key. Because that key can degrade to openrouter, the user's card can be silently skipped and the call priced by the generic USD fallback instead. Details available on request.

I am happy to prepare a PR for the surfaces above if that helps.