Skip to content

Add Qwen and Doubao providers#498

Open
LeoLin990405 wants to merge 6 commits intosteipete:mainfrom
LeoLin990405:feat/qwen-doubao
Open

Add Qwen and Doubao providers#498
LeoLin990405 wants to merge 6 commits intosteipete:mainfrom
LeoLin990405:feat/qwen-doubao

Conversation

@LeoLin990405
Copy link

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

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

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

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
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
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
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!

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.

2 participants