Skip to content

Add Qwen and Doubao providers - #498

Merged
steipete merged 9 commits into
steipete:mainfrom
LeoLin990405:feat/qwen-doubao
May 10, 2026
Merged

Add Qwen and Doubao providers#498
steipete merged 9 commits into
steipete:mainfrom
LeoLin990405:feat/qwen-doubao

Conversation

@LeoLin990405

Copy link
Copy Markdown
Contributor

Summary

  • Add Qwen (通义灵码) provider — Alibaba's DashScope coding plan
  • Add Doubao (豆包) provider — ByteDance's Volcengine Ark coding plan

Both are popular Chinese AI coding platforms with growing user bases.

Implementation

  • Follows the existing descriptor-driven provider architecture
  • Uses minimal API probe (POST with max_tokens: 1) to read rate-limit headers
  • Qwen: auto-detects sk-sp-* coding plan keys vs regular API keys, routes to appropriate endpoint
  • Doubao: uses /api/coding/v3/chat/completions endpoint with doubao-seed-2.0-code model
  • Graceful fallback when no rate-limit headers are returned (shows "Active" status)

Test plan

  • Build succeeds on macOS
  • Enable Qwen in settings, enter DashScope API key → usage data displays
  • Enable Doubao in settings, enter ARK API key → usage data displays
  • CLI: codexbar --provider qwen and codexbar --provider doubao show usage
  • Providers appear correctly in widget picker

@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: 20efb95c0d

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

let headers = httpResponse.allHeaderFields
let remaining = Self.intHeader(headers, "x-ratelimit-remaining-requests")
let limit = Self.intHeader(headers, "x-ratelimit-limit-requests")
let resetString = headers["x-ratelimit-reset-requests"] as? 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 Parse reset header case-insensitively

HTTPURLResponse.allHeaderFields lookups are case-sensitive, but this code only checks the lowercase key for the reset header. If the server returns X-RateLimit-Reset-Requests (or any other casing), resetString becomes nil even though the header is present, so the app loses reset-time information in normal responses. Reusing the same case-insensitive header lookup used for remaining/limit avoids this data loss.

Useful? React with 👍 / 👎.

LeoLin990405 added a commit to LeoLin990405/CodexBar that referenced this pull request Mar 9, 2026
Use a dedicated stringHeader helper (case-insensitive) for the
x-ratelimit-reset-requests header, matching how remaining/limit
headers are already parsed. Addresses Codex review feedback on PR steipete#498.
@ratulsarna

Copy link
Copy Markdown
Collaborator

Thanks for the PR @LeoLin990405 ! I think there’s one real correctness issue and one risky compatibility spot:

  1. Qwen regular keys are hardcoded to the Beijing compatible-mode endpoint. DashScope uses region-specific base URLs/keys, so valid Singapore/Virginia keys may fail here.
  2. Both probes hardcode a single model (qwen3-coder-plus / doubao-seed-2.0-code). That can turn “valid key, wrong model entitlement/region” into a false failure.

Also worth checking: on 429, apiKeyValid stays false, so if rate-limit headers are missing the UI may show “No usage data” instead of the intended graceful fallback.

1. Reset header lookup is now case-insensitive (new stringHeader helper),
   matching the existing intHeader behavior.
2. On 429 (rate-limited), apiKeyValid is set to true so the UI shows
   "Active" instead of "No usage data" when rate-limit headers are absent.
3. Probe multiple fallback models instead of hardcoding a single model,
   so keys with different entitlements or regions still work.
@LeoLin990405

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review, @ratulsarna! I've pushed a fix addressing all three points:

Changes in 3a548ce

1. Reset header case-insensitivity
Added a stringHeader helper (matching the existing intHeader pattern) so x-ratelimit-reset-requests is looked up case-insensitively. Previously only intHeader did this — reset was using a direct dictionary lookup.

2. 429 → apiKeyValid = true
A 429 means the key is valid, just rate-limited. Now apiKeyValid is set to true for both 200 and 429 responses, so the UI correctly shows "Active — check dashboard for details" instead of "No usage data" when rate-limit headers are absent.

3. Model fallback list
Instead of hardcoding a single probe model, both fetchers now try a list of models sequentially:

  • Qwen: qwen3-coder-plusqwen-turboqwen-plus
  • Doubao: doubao-seed-2.0-codedoubao-1.5-pro-32kdoubao-lite-32k

If a model returns 403/404 (not entitled or not found), the next model is tried. This handles the "valid key, wrong model entitlement/region" scenario you flagged.

Qwen regular keys are hardcoded to the Beijing compatible-mode endpoint.

Good catch — I kept the Beijing endpoint as the default since DashScope's compatible-mode API routes through Beijing for most regions. If there's demand for explicit region support, that could be a follow-up (e.g. reading a DASHSCOPE_REGION env var), but for now the model fallback should cover the most common failure mode.

@LeoLin990405

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review, @ratulsarna! Really appreciate you catching these — all three are valid issues. I've pushed a fix in 3a548ce that addresses each point. Here's a detailed walkthrough:


Fix 1: Reset header case-insensitivity

The problem: resetString used a direct dictionary lookup (headers["x-ratelimit-reset-requests"] as? String), which is case-sensitive on HTTPURLResponse.allHeaderFields. Meanwhile, intHeader (used for remaining and limit) already had a case-insensitive fallback loop — so this was an inconsistency. If the server returned X-RateLimit-Reset-Requests with different casing, the reset time would silently become nil and the app would lose reset-time information.

The fix: Added a stringHeader helper that mirrors intHeader's case-insensitive search pattern:

// Before (case-sensitive — could miss headers with different casing):
let resetString = headers["x-ratelimit-reset-requests"] as? String

// After (case-insensitive — consistent with intHeader):
let resetString = Self.stringHeader(headers, "x-ratelimit-reset-requests")

Applied to both QwenUsageFetcher and DoubaoUsageFetcher.


Fix 2: Treat 429 as "key is valid"

The problem: On HTTP 429 (rate-limited), apiKeyValid stayed false because it only checked statusCode == 200. When rate-limit headers were also absent from the 429 response, toUsageSnapshot() fell through to the else branch and displayed "No usage data" — which is misleading, since the key is valid, it's just being throttled.

The fix: Treat both 200 and 429 as valid-key responses:

// Before:
apiKeyValid: httpResponse.statusCode == 200

// After:
let keyValid = httpResponse.statusCode == 200 || httpResponse.statusCode == 429
apiKeyValid: keyValid

Now when a 429 comes back without rate-limit headers, the UI correctly shows "Active — check dashboard for details" instead of the confusing "No usage data".


Fix 3: Multi-model probe with fallback

The problem: Both fetchers hardcoded a single probe model (qwen3-coder-plus / doubao-seed-2.0-code). If a user has a valid API key but lacks entitlement for that specific model (different subscription tier, region, or plan), the probe returns 403/404 and the whole fetch is treated as a failure — even though the key itself is perfectly fine.

The fix: Try a list of models sequentially, falling back on 403/404:

Provider Probe order (most → least specific)
Qwen qwen3-coder-plusqwen-turboqwen-plus
Doubao doubao-seed-2.0-codedoubao-1.5-pro-32kdoubao-lite-32k
for model in self.probeModels {
    do {
        return try await self.probe(apiKey: apiKey, model: model)
    } catch let error as QwenUsageError {
        // Model not entitled or not found → try the next one
        if case let .apiError(code, _) = error, code == 404 || code == 403 {
            Self.log.debug("Qwen probe model \(model) unavailable (\(code)), trying next")
            lastError = error
            continue
        }
        throw error  // Non-model errors (network, 401 auth) propagate immediately
    }
}

This ensures we only retry on model-specific failures, while auth errors and network issues are surfaced right away without wasting retries.


A note on the region concern

Qwen regular keys are hardcoded to the Beijing compatible-mode endpoint.

I kept the Beijing dashscope.aliyuncs.com endpoint as the default for now — DashScope's OpenAI-compatible API routes through this host for most regions and key types in practice. The model fallback list above should cover the most common failure scenario (valid key, wrong model entitlement). If we see real-world cases where region-specific endpoints are needed, we could add a DASHSCOPE_BASE_URL environment variable override as a follow-up — but I'd prefer to keep this PR focused on the correctness fixes you flagged.


Build verification

CI build passes on macOS with Xcode (full Xcode toolchain, not just command-line tools):

Build succeeded — Run #23040849961

Step Status Duration
Resolve dependencies ✅ Pass
Build release ✅ Pass ~2m
Fix rpath and package ✅ Pass
Upload build artifact ✅ Pass

All steps green. Let me know if you'd like any further changes!

steipete added 3 commits May 10, 2026 09:41
# Conflicts:
#	Sources/CodexBar/Providers/Shared/ProviderImplementationRegistry.swift
#	Sources/CodexBar/UsageStore.swift
#	Sources/CodexBarCLI/TokenAccountCLI.swift
#	Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift
#	Sources/CodexBarCore/Logging/LogCategories.swift
#	Sources/CodexBarCore/Providers/ProviderDescriptor.swift
#	Sources/CodexBarCore/Providers/ProviderTokenResolver.swift
#	Sources/CodexBarCore/Providers/Providers.swift
#	Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift
#	Sources/CodexBarWidget/CodexBarWidgetProvider.swift
#	Sources/CodexBarWidget/CodexBarWidgetViews.swift
@steipete

Copy link
Copy Markdown
Owner

Maintainer pass pushed.

  • Split Qwen back out; Qwen path is covered by the Alibaba/Qwen aliases on main.
  • Kept/rebased Doubao provider only, wired provider registry/config/token resolver/widget/CLI/docs/changelog.
  • Added Doubao parser/config/token tests.
  • Fixed a reproducible Codex RPC timeout-test flake found during full-suite validation by making the hung app-server stub deterministic.

Local validation:

  • swift test --filter Doubao
  • swift test --filter ProviderConfigEnvironmentTests
  • swift test --filter ProviderTokenResolverTests
  • swift test --filter SettingsStoreTests
  • swift test --filter CodexUsageFetcherFallbackTests
  • ./Scripts/lint.sh lint
  • swift test (2260 tests)
  • CLI smoke: ARK_API_KEY=codexbar-invalid-smoke swift run CodexBarCLI usage --provider doubao --source api --format json --pretty reached the Doubao API and returned the expected 401 invalid-key error.

CI workflow was approved and is running: https://github.com/steipete/CodexBar/actions/runs/25624507999

@steipete
steipete merged commit 6eb3699 into steipete:main May 10, 2026
4 checks passed
LeoLin990405 added a commit to LeoLin990405/CodexBar that referenced this pull request May 12, 2026
The 5/10 upstream merges (PR steipete#498 Doubao, the StepFun first-party port,
and PR steipete#651 MiMo token plan) all introduced descriptors with English
display names, which overwrote the 5/6 53648ee `Localize Chinese
provider names` pass. Bring them back in line with the fork's stated
naming policy (README: 国内 AI provider 使用中文名称, e.g. 豆包 / 阶跃星辰 /
小米 Mimo):

- Doubao  → 豆包          + toggle "显示豆包用量"
- StepFun → 阶跃星辰      + toggle "显示阶跃星辰用量"
- MiMo    → 小米 Mimo     + toggle "显示小米 Mimo Token Plan 与余额"

Same pass also localizes the associated tokenCost.noDataMessage strings
that hit user-visible cost popovers. Doubao additionally has its
sessionLabel/weeklyLabel translated ("请求" / "速率限制"); StepFun and
MiMo keep their English window labels because StepFunUsageFetcherTests
asserts `sessionLabel == "5h Window"` and `weeklyLabel == "Weekly Window"`,
and MiMo's "Credits" is a product term shipped to multiple regions.

Brand names left untouched: Claude / Codex / Cursor / Gemini / Copilot
/ DeepSeek / Trae / MiniMax (per README's brand-term-verbatim rule).
@foobra

foobra commented Jun 10, 2026

Copy link
Copy Markdown

@LeoLin990405 Doubao rate show error, it always show 100%

LeoLin990405 added a commit to LeoLin990405/CodexBar that referenced this pull request Jun 10, 2026
… not 100%

Volcano Ark returns HTTP 200 with `x-ratelimit-limit-requests > 0` and
`x-ratelimit-remaining-requests = 0` on some account tiers (notably
unverified personal keys) without actually rate-limiting the request — a
genuine throttle would return 429. The previous math computed
`used = limit` and clamped to 100%, so the Doubao card always showed
100% used for affected users.

Tighten the normal-math guard to `limitRequests > 0 && remainingRequests > 0`
so the unreliable-headers state falls through to the existing
"Active - check dashboard for details" fallback (which was already used
when both headers are missing). Also emit a `log.warning` when the
pattern is observed so users hitting this can attach evidence from
`~/Library/Logs/CodexBar/CodexBar.log` to bug reports.

Adds `Tests/CodexBarTests/DoubaoUsageFetcherTests.swift` covering the
normal path, the boundary near-full path, the unreliable-headers path,
the both-headers-missing path, the invalid-key path, and provider
identity tagging.

Fixes steipete#1382. Reported by @foobra on PR steipete#498.
@LeoLin990405

Copy link
Copy Markdown
Contributor Author

@foobra Thanks for the report! Confirmed — root cause is that Volcano Ark returns HTTP 200 with x-ratelimit-limit-requests > 0 and x-ratelimit-remaining-requests = 0 on certain account tiers without actually rate-limiting the request (a real throttle would be 429), and the existing math reads that as 100% used.

Tracked at #1382, fix in flight at #1383 — the card will fall back to the existing "Active — check dashboard for details" hint when the unreliable-headers pattern is seen, plus a log.warning so the case is greppable from ~/Library/Logs/CodexBar/CodexBar.log for further debugging.

If you have a moment after the fix lands, grabbing that warning line from your Codexbar log would help confirm the exact Volcano account-tier conditions and any related provider configuration.

steipete pushed a commit to LeoLin990405/CodexBar that referenced this pull request Jun 10, 2026
… not 100%

Volcano Ark returns HTTP 200 with `x-ratelimit-limit-requests > 0` and
`x-ratelimit-remaining-requests = 0` on some account tiers (notably
unverified personal keys) without actually rate-limiting the request — a
genuine throttle would return 429. The previous math computed
`used = limit` and clamped to 100%, so the Doubao card always showed
100% used for affected users.

Tighten the normal-math guard to `limitRequests > 0 && remainingRequests > 0`
so the unreliable-headers state falls through to the existing
"Active - check dashboard for details" fallback (which was already used
when both headers are missing). Also emit a `log.warning` when the
pattern is observed so users hitting this can attach evidence from
`~/Library/Logs/CodexBar/CodexBar.log` to bug reports.

Adds `Tests/CodexBarTests/DoubaoUsageFetcherTests.swift` covering the
normal path, the boundary near-full path, the unreliable-headers path,
the both-headers-missing path, the invalid-key path, and provider
identity tagging.

Fixes steipete#1382. Reported by @foobra on PR steipete#498.
steipete added a commit that referenced this pull request Jun 10, 2026
* fix(doubao): treat 200 + limit>0 + remaining=0 as unreliable headers, not 100%

Volcano Ark returns HTTP 200 with `x-ratelimit-limit-requests > 0` and
`x-ratelimit-remaining-requests = 0` on some account tiers (notably
unverified personal keys) without actually rate-limiting the request — a
genuine throttle would return 429. The previous math computed
`used = limit` and clamped to 100%, so the Doubao card always showed
100% used for affected users.

Tighten the normal-math guard to `limitRequests > 0 && remainingRequests > 0`
so the unreliable-headers state falls through to the existing
"Active - check dashboard for details" fallback (which was already used
when both headers are missing). Also emit a `log.warning` when the
pattern is observed so users hitting this can attach evidence from
`~/Library/Logs/CodexBar/CodexBar.log` to bug reports.

Adds `Tests/CodexBarTests/DoubaoUsageFetcherTests.swift` covering the
normal path, the boundary near-full path, the unreliable-headers path,
the both-headers-missing path, the invalid-key path, and provider
identity tagging.

Fixes #1382. Reported by @foobra on PR #498.

* fix: preserve Doubao throttle state

* fix: confirm ambiguous Doubao request limits

* fix: preserve Doubao confirmation semantics

* fix: require complete Doubao request limits

* fix: classify Doubao request throttles

* fix: preserve confirmed Doubao exhaustion

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
LeoLin990405 added a commit to LeoLin990405/CodexBar that referenced this pull request Aug 6, 2026
Use a dedicated stringHeader helper (case-insensitive) for the
x-ratelimit-reset-requests header, matching how remaining/limit
headers are already parsed. Addresses Codex review feedback on PR steipete#498.
LeoLin990405 added a commit to LeoLin990405/CodexBar that referenced this pull request Aug 6, 2026
* Add Qwen and Doubao (通义灵码 & 豆包) providers

* fix: address review feedback for Qwen & Doubao providers

1. Reset header lookup is now case-insensitive (new stringHeader helper),
   matching the existing intHeader behavior.
2. On 429 (rate-limited), apiKeyValid is set to true so the UI shows
   "Active" instead of "No usage data" when rate-limit headers are absent.
3. Probe multiple fallback models instead of hardcoding a single model,
   so keys with different entitlements or regions still work.

* ci: add workflow_dispatch trigger to enable manual CI runs

* ci: trigger CI build

* ci: re-trigger CI after main sync

* chore: revert ci.yml workflow_dispatch (not needed for this PR)

* refactor: split Qwen from Doubao provider PR

* test: stabilize Codex RPC timeout stub

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
LeoLin990405 added a commit to LeoLin990405/CodexBar that referenced this pull request Aug 7, 2026
… not 100%

Volcano Ark returns HTTP 200 with `x-ratelimit-limit-requests > 0` and
`x-ratelimit-remaining-requests = 0` on some account tiers (notably
unverified personal keys) without actually rate-limiting the request — a
genuine throttle would return 429. The previous math computed
`used = limit` and clamped to 100%, so the Doubao card always showed
100% used for affected users.

Tighten the normal-math guard to `limitRequests > 0 && remainingRequests > 0`
so the unreliable-headers state falls through to the existing
"Active - check dashboard for details" fallback (which was already used
when both headers are missing). Also emit a `log.warning` when the
pattern is observed so users hitting this can attach evidence from
`~/Library/Logs/CodexBar/CodexBar.log` to bug reports.

Adds `Tests/CodexBarTests/DoubaoUsageFetcherTests.swift` covering the
normal path, the boundary near-full path, the unreliable-headers path,
the both-headers-missing path, the invalid-key path, and provider
identity tagging.

Fixes steipete#1382. Reported by @foobra on PR steipete#498.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants