Skip to content

fix(usage): resolve per-app credentials for native balance/coding-plan queries#3355

Merged
farion1231 merged 3 commits into
farion1231:mainfrom
SiskonEmilia:fix/usage-per-app-credentials
Jun 1, 2026
Merged

fix(usage): resolve per-app credentials for native balance/coding-plan queries#3355
farion1231 merged 3 commits into
farion1231:mainfrom
SiskonEmilia:fix/usage-per-app-credentials

Conversation

@SiskonEmilia

@SiskonEmilia SiskonEmilia commented May 30, 2026

Copy link
Copy Markdown
Contributor

Summary / 概述

The native usage-query paths (official balance + coding-plan templates) resolved provider credentials only from env.ANTHROPIC_*. That matches Claude providers, but every other app stores credentials in a different shape:

  • Codex — key in auth.OPENAI_API_KEY, base URL inside the TOML config string
  • Hermes / OpenClaw — flattened at the top level (base_url/api_key, baseUrl/apiKey)
  • OpenCode — nested under options.{baseURL, apiKey}

So the card "refresh usage" / auto-query returned empty credentials and failed (查询失败) for those apps, even though the config-page "Test" button worked — because the frontend getProviderCredentials already extracts per-app. This PR removes that front/back asymmetry.

Related Issue / 关联 Issue

Fixes #2625
Fixes #3100
Fixed #3358
Likely also resolves #3158 (Codex 查询失败; the report lacks reproduction detail, but the Codex native query was broken for the same root cause).

#3100 additionally mentions a Hermes provider is missing api_mode field health-check warning. That is unrelated to credential resolution and is not addressed here.

Screenshots / 截图

Codex → DeepSeek provider card, native balance query (the "refresh usage" button). The Hermes / OpenClaw reports in #3100 / #2625 share the identical root cause and fix.

Before / 修改前 (查询失败) After / 修改后 (剩余: 48.12 CNY)
before after

Approach

  • Add a single per-app resolver Provider::resolve_usage_credentials(app_type) -> (base_url, api_key) that mirrors the frontend getProviderCredentials, and route both native branches (balance, token_plan) in query_provider_usage_inner through it.
  • Add codex_config::extract_codex_base_url as the canonical Codex config.toml base-URL parser (next to the existing extract_codex_api_key), and make the proxy adapter's private extract_codex_base_url_from_toml delegate to it — removing a byte-for-byte duplicate instead of adding a third copy.
  • Trim a trailing / from the base URL (matching the JS-script path's extract_base_url_from_provider) so the resolver is a behavior-preserving superset.
  • Align the frontend getProviderCredentials to also cover OpenCode and Claude Desktop, and to use the same OPENROUTER_API_KEY / GOOGLE_API_KEY key fallbacks as the backend, so the "Test" button and the live query stay in lockstep across all apps.

Scope

This PR fixes the native query path (balance / coding-plan) and its matching frontend extraction. The JS-script usage path (services/provider/usage.rs) keeps its own env-only copy of this logic (extract_api_key_from_provider / extract_base_url_from_provider) and is intentionally left untouched to keep the change single-purpose and to avoid colliding with the in-flight #1479 / #2031, which already target that path. resolve_usage_credentials is written so those helpers can delegate to it later (a TODO marks the seam). Happy to send that consolidation as a separate PR if you'd like it.

Tests

New unit tests in src-tauri/src/provider.rs cover the resolver for every app shape (Claude, Claude Desktop, Codex, Gemini, Hermes, OpenClaw, OpenCode), the OpenRouter/Google key fallbacks, trailing-slash trimming, empty-primary-field fallback, and the all-missing case; src-tauri/src/commands/provider.rs covers the None-provider path and delegation.

cargo test  --manifest-path src-tauri/Cargo.toml --lib            -> all pass*
cargo fmt   --manifest-path src-tauri/Cargo.toml --check          -> clean
cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warnings  -> clean for changed files
pnpm typecheck     -> pass
pnpm format:check  -> pass
pnpm test:unit     -> 280 pass*

* Built and tested on Windows. One pre-existing OpenClaw session test and two App.test.tsx integration tests fail only under the full local run on Windows (temp-path JSON escaping / parallel-load flakiness); they are unrelated to this change and reproduce identically on a clean checkout (verified via git stash). They pass in isolation and on Linux CI.

Manual verification

Built a local release bundle and verified on Windows 11:

  • Codex -> DeepSeek: card "refresh usage" now returns the balance (previously 查询失败); the config-page "Test" button still works.
  • Confirmed the front/back credential extraction now matches for OpenCode and Claude Desktop.

Notes for review

  • resolve_usage_credentials lives on Provider because Provider owns settings_config. Every AppType has an explicit match arm — Claude and Claude Desktop share one (both Anthropic env shape), and OpenCode/Codex/Gemini/Hermes/OpenClaw each get their own; there is no _ catch-all, so a new AppType fails to compile here until it's handled. The earlier comment that called Claude Desktop "unsupported" was wrong and is corrected — it resolves via the env branch.
  • The api-key fallback chains use first-present-non-empty semantics (a first_non_empty helper), matching the frontend's a || b (JS || skips empty strings). Presets seed fields like ANTHROPIC_AUTH_TOKEN as present-but-empty placeholders, so skipping only absent keys (a plain .get().or_else() chain) would still resolve to an empty key on the native path while the frontend found the real one (e.g. OpenRouter-on-Claude) — the same Test-works / refresh-fails divergence this PR removes. Tests cover empty-primary + populated-fallback for Claude and Gemini.
  • This deliberately preserves the OPENROUTER_API_KEY / GOOGLE_API_KEY key fallbacks (and adds GOOGLE_API_KEY to Gemini) on both sides, so it does not regress OpenRouter-on-Claude / legacy-Gemini configs.
  • The trailing-slash trim is new for the native path but inert there (balance.rs / coding_plan.rs detect providers via substring contains and never concatenate base_url); it is included so a future usage.rs delegation is behavior-preserving.
  • One residual, pre-existing asymmetry is left as-is: the JS extractCodexBaseUrl has a malformed-TOML recovery fallback that the strict toml-parse backend does not. For well-formed config.toml they agree; only broken TOML could diverge. Out of scope here.

Checklist / 检查清单

  • pnpm typecheck passes / 通过 TypeScript 类型检查
  • pnpm format:check passes / 通过代码格式检查
  • cargo clippy passes (if Rust code changed) / 通过 Clippy 检查(如修改了 Rust 代码)
  • Updated i18n files if user-facing text changed / 如修改了用户可见文本,已更新国际化文件 (N/A: no user-facing text changes)

…n queries

The native usage-query paths (balance + coding_plan) in
`query_provider_usage_inner` read credentials only from `env.ANTHROPIC_*`.
That matches Claude providers, but Codex stores its key in
`auth.OPENAI_API_KEY` with the base URL inside a TOML `config` string,
Hermes/OpenClaw flatten them at the top level, and OpenCode nests them under
`options`. So the card "refresh usage" / auto-query returned empty
credentials and failed ("查询失败") for those apps, even though the
config-page "Test" button worked (the frontend extracts per-app correctly).

Introduce a single per-app resolver `Provider::resolve_usage_credentials`
that mirrors the frontend `getProviderCredentials`, and route both native
branches through it. Add `extract_codex_base_url` to `codex_config` as the
canonical Codex TOML base-URL parser and make the proxy adapter delegate to
it (removing a duplicate copy). Align the frontend `getProviderCredentials`
to cover OpenCode and Claude Desktop and to use the same OpenRouter/Google
key fallbacks as the backend.

Fixes farion1231#3158
Fixes farion1231#3100
Fixes farion1231#2625
Address two review nits on the per-app credential resolver:

- provider.rs: replace the catch-all `_` arm in resolve_usage_credentials with an explicit `AppType::Claude | AppType::ClaudeDesktop` arm, so a new AppType variant fails to compile here instead of silently defaulting to the Anthropic env shape.
- UsageScriptModal.tsx: normalize getProviderCredentials baseUrl through a single trailing-slash trim so the frontend 'Test' path matches the backend resolver (trim_end_matches), keeping front/back truly in lockstep.

No behavior change for well-formed configs; tests/typecheck/fmt/clippy clean.
`obj.get(key)` returns `Some` for a present-but-empty field, so the
`.or_else()` fallback chains for the Claude/ClaudeDesktop and Gemini api-key
lookups only skipped *absent* keys, not empty ones. Presets seed fields like
`ANTHROPIC_AUTH_TOKEN` as present-but-empty placeholders, so a provider whose
real key lives in a fallback field (`ANTHROPIC_API_KEY` / `OPENROUTER_API_KEY`
/ `GOOGLE_API_KEY`, or `GOOGLE_API_KEY` for Gemini) resolved to an empty key on
the native balance/coding-plan path — while the frontend `a || b` (which skips
empty strings) still found it, reproducing the same Test-works / refresh-fails
divergence this PR removes.

Add a `first_non_empty` helper that skips present-but-empty values, matching
the frontend `||` semantics, and use it for both fallback chains. Tests cover
empty primary + populated fallback for Claude and Gemini.
@farion1231

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

ℹ️ 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".

@farion1231 farion1231 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thank you for your contribution!

@farion1231
farion1231 merged commit afa09e1 into farion1231:main Jun 1, 2026
2 checks passed
@farion1231 farion1231 mentioned this pull request Jun 9, 2026
2 tasks
gfunc pushed a commit to gfunc/cc-switch that referenced this pull request Jun 19, 2026
…n queries (farion1231#3355)

* fix(usage): resolve per-app credentials for native balance/coding-plan queries

The native usage-query paths (balance + coding_plan) in
`query_provider_usage_inner` read credentials only from `env.ANTHROPIC_*`.
That matches Claude providers, but Codex stores its key in
`auth.OPENAI_API_KEY` with the base URL inside a TOML `config` string,
Hermes/OpenClaw flatten them at the top level, and OpenCode nests them under
`options`. So the card "refresh usage" / auto-query returned empty
credentials and failed ("查询失败") for those apps, even though the
config-page "Test" button worked (the frontend extracts per-app correctly).

Introduce a single per-app resolver `Provider::resolve_usage_credentials`
that mirrors the frontend `getProviderCredentials`, and route both native
branches through it. Add `extract_codex_base_url` to `codex_config` as the
canonical Codex TOML base-URL parser and make the proxy adapter delegate to
it (removing a duplicate copy). Align the frontend `getProviderCredentials`
to cover OpenCode and Claude Desktop and to use the same OpenRouter/Google
key fallbacks as the backend.

Fixes farion1231#3158
Fixes farion1231#3100
Fixes farion1231#2625

* refactor(usage): explicit AppType arms + frontend trailing-slash trim

Address two review nits on the per-app credential resolver:

- provider.rs: replace the catch-all `_` arm in resolve_usage_credentials with an explicit `AppType::Claude | AppType::ClaudeDesktop` arm, so a new AppType variant fails to compile here instead of silently defaulting to the Anthropic env shape.
- UsageScriptModal.tsx: normalize getProviderCredentials baseUrl through a single trailing-slash trim so the frontend 'Test' path matches the backend resolver (trim_end_matches), keeping front/back truly in lockstep.

No behavior change for well-formed configs; tests/typecheck/fmt/clippy clean.

* fix(usage): skip empty primary credential fields in fallback chain

`obj.get(key)` returns `Some` for a present-but-empty field, so the
`.or_else()` fallback chains for the Claude/ClaudeDesktop and Gemini api-key
lookups only skipped *absent* keys, not empty ones. Presets seed fields like
`ANTHROPIC_AUTH_TOKEN` as present-but-empty placeholders, so a provider whose
real key lives in a fallback field (`ANTHROPIC_API_KEY` / `OPENROUTER_API_KEY`
/ `GOOGLE_API_KEY`, or `GOOGLE_API_KEY` for Gemini) resolved to an empty key on
the native balance/coding-plan path — while the frontend `a || b` (which skips
empty strings) still found it, reproducing the same Test-works / refresh-fails
divergence this PR removes.

Add a `first_non_empty` helper that skips present-but-empty values, matching
the frontend `||` semantics, and use it for both fallback chains. Tests cover
empty primary + populated fallback for Claude and Gemini.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants