fix(usage): resolve per-app credentials for native balance/coding-plan queries#3355
Merged
farion1231 merged 3 commits intoJun 1, 2026
Merged
Conversation
…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.
Owner
|
@codex review |
|
Codex Review: Didn't find any major issues. Breezy! ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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
approved these changes
Jun 1, 2026
farion1231
left a comment
Owner
There was a problem hiding this comment.
Thank you for your contribution!
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:auth.OPENAI_API_KEY, base URL inside the TOMLconfigstringbase_url/api_key,baseUrl/apiKey)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 frontendgetProviderCredentialsalready 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 fieldhealth-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.
查询失败)剩余: 48.12 CNY)Approach
Provider::resolve_usage_credentials(app_type) -> (base_url, api_key)that mirrors the frontendgetProviderCredentials, and route both native branches (balance,token_plan) inquery_provider_usage_innerthrough it.codex_config::extract_codex_base_urlas the canonical Codexconfig.tomlbase-URL parser (next to the existingextract_codex_api_key), and make the proxy adapter's privateextract_codex_base_url_from_tomldelegate to it — removing a byte-for-byte duplicate instead of adding a third copy./from the base URL (matching the JS-script path'sextract_base_url_from_provider) so the resolver is a behavior-preserving superset.getProviderCredentialsto also cover OpenCode and Claude Desktop, and to use the sameOPENROUTER_API_KEY/GOOGLE_API_KEYkey 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_credentialsis written so those helpers can delegate to it later (aTODOmarks 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.rscover 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.rscovers theNone-provider path and delegation.* Built and tested on Windows. One pre-existing OpenClaw session test and two
App.test.tsxintegration 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 viagit stash). They pass in isolation and on Linux CI.Manual verification
Built a local release bundle and verified on Windows 11:
查询失败); the config-page "Test" button still works.Notes for review
resolve_usage_credentialslives onProviderbecauseProviderownssettings_config. EveryAppTypehas 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 newAppTypefails 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.first_non_emptyhelper), matching the frontend'sa || b(JS||skips empty strings). Presets seed fields likeANTHROPIC_AUTH_TOKENas 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.OPENROUTER_API_KEY/GOOGLE_API_KEYkey fallbacks (and addsGOOGLE_API_KEYto Gemini) on both sides, so it does not regress OpenRouter-on-Claude / legacy-Gemini configs.balance.rs/coding_plan.rsdetect providers via substringcontainsand never concatenatebase_url); it is included so a futureusage.rsdelegation is behavior-preserving.extractCodexBaseUrlhas a malformed-TOML recovery fallback that the stricttoml-parse backend does not. For well-formedconfig.tomlthey agree; only broken TOML could diverge. Out of scope here.Checklist / 检查清单
pnpm typecheckpasses / 通过 TypeScript 类型检查pnpm format:checkpasses / 通过代码格式检查cargo clippypasses (if Rust code changed) / 通过 Clippy 检查(如修改了 Rust 代码)