feat: add Gemini, Qwen Code, and Mistral Vibe providers#22
Conversation
Add three new usage data providers that run in parallel during `straude push`. Each reads local session files and fails silently when not installed. Model display names added across the web UI. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
feat: add Gemini, Qwen Code, and Mistral Vibe providers
|
@jqueguiner is attempting to deploy a commit to the Pacific Systems Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughIntroduces three new AI provider integrations (Gemini, Qwen, Mistral) with data retrieval and parsing modules. Updates the CLI usage aggregation system to merge multiple provider sources. Expands web UI model display name mappings and updates documentation and tests accordingly. Changes
Sequence DiagramsequenceDiagram
participant CLI as CLI Push Command
participant Claude as Claude Provider
participant Codex as Codex Provider
participant Gemini as Gemini Provider
participant Qwen as Qwen Provider
participant Mistral as Mistral Provider
participant Merge as mergeEntries()
participant Output as Daily Usage Pipeline
CLI->>Claude: runClaudeRawAsync(dates)
Claude-->>CLI: raw usage data
CLI->>Codex: runCodexRawAsync(dates)
Codex-->>CLI: raw usage data
CLI->>Gemini: runGeminiRawAsync(dates)
Gemini-->>CLI: raw usage data
CLI->>Qwen: runQwenRawAsync(dates)
Qwen-->>CLI: raw usage data
CLI->>Mistral: runMistralRawAsync(dates)
Mistral-->>CLI: raw usage data
CLI->>Claude: parseClaudeOutput(raw)
Claude-->>CLI: CcusageDailyEntry[]
CLI->>Codex: parseCodexOutput(raw)
Codex-->>CLI: CcusageDailyEntry[]
CLI->>Gemini: parseGeminiOutput(raw)
Gemini-->>CLI: CcusageDailyEntry[]
CLI->>Qwen: parseQwenOutput(raw)
Qwen-->>CLI: CcusageDailyEntry[]
CLI->>Mistral: parseMistralOutput(raw)
Mistral-->>CLI: CcusageDailyEntry[]
CLI->>Merge: mergeEntries(claude, codex, gemini, qwen, mistral)
Merge->>Merge: Aggregate all sources by date
Merge->>Merge: Compute totals & model breakdown
Merge-->>CLI: merged CcusageDailyEntry[]
CLI->>Output: Push to daily usage pipeline
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
I guess it needs adjustments in the backend too. |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/cli/src/commands/push.ts (1)
263-303:⚠️ Potential issue | 🟠 MajorUnresolved Gemini, Qwen, and Mistral rows still get submitted.
blockedDatesis derived only fromcodexParsed.entryMeta. The new providers can also returnmode === "unresolved", so the command warns about bad normalization and then merges those rows anyway. Filter each provider'sdataagainst its ownentryMetabefore callingmergeEntries, and add a regression for at least one unresolved Gemini/Qwen/Mistral row.Possible fix
- const codexMetaByDate = new Map((codexParsed.entryMeta ?? []).map((row) => [row.date, row.meta])); - const blockedDates = new Set<string>(); - - for (const [date, meta] of codexMetaByDate) { - if (meta.mode === "unresolved") { - blockedDates.add(date); - } - } - - if (blockedDates.size > 0) { - const blocked = [...blockedDates].sort(); - const reason = "unresolved codex normalization"; - console.log(`Warning: skipping Codex rows for ${blocked.length} date(s) due to ${reason}: ${blocked.join(", ")}`); - } - - const codexEntries = codexParsed.data.filter((entry) => !blockedDates.has(entry.date)); + const dropUnresolved = ( + provider: string, + parsed: { data: CcusageDailyEntry[]; entryMeta?: Array<{ date: string; meta: { mode: string } }> }, + ) => { + const blockedDates = new Set( + (parsed.entryMeta ?? []) + .filter((row) => row.meta.mode === "unresolved") + .map((row) => row.date), + ); + if (blockedDates.size > 0) { + console.log( + `Warning: skipping ${provider} rows for ${blockedDates.size} date(s) due to unresolved normalization: ${[...blockedDates].sort().join(", ")}`, + ); + } + return parsed.data.filter((entry) => !blockedDates.has(entry.date)); + }; + + const codexEntries = dropUnresolved("Codex", codexParsed); + const geminiEntries = dropUnresolved("Gemini", geminiParsed); + const qwenEntries = dropUnresolved("Qwen", qwenParsed); + const mistralEntries = dropUnresolved("Mistral", mistralParsed); @@ - geminiParsed.data, - qwenParsed.data, - mistralParsed.data, + geminiEntries, + qwenEntries, + mistralEntries, );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cli/src/commands/push.ts` around lines 263 - 303, The current code only derives blockedDates from codexParsed.entryMeta so unresolved rows from Gemini/Qwen/Mistral still get merged; create an entryMeta->date map and blocked date Set for each provider (e.g., geminiParsed.entryMeta, qwenParsed.entryMeta, mistralParsed.entryMeta) similar to codexMetaByDate/blockedDates, filter each provider's data (geminiParsed.data, qwenParsed.data, mistralParsed.data, and codexParsed.data) to remove entries whose date is in that provider's blocked set (replace codexEntries with per-provider filtered arrays), then pass those filtered arrays into mergeEntries; also add a regression test that submits at least one unresolved Gemini or Qwen or Mistral row to ensure these are skipped.
🧹 Nitpick comments (2)
packages/cli/src/lib/qwen.ts (1)
154-178: Consider validating parsed array elements.The
parsed as CcusageDailyEntry[]assertion (Line 170) trusts that the JSON structure matches without field validation. SincerunQwenRawAsyncserializes the data itself, this is safe in practice. However, for robustness against future refactoring, consider adding a quick shape check or documenting this assumption.💡 Optional: Add minimal validation
if (!Array.isArray(parsed)) { return { data: [], anomalies: [], normalizationSummary: summarizeNormalization([]), entryMeta: [] }; } - const data = parsed as CcusageDailyEntry[]; + // Trust the shape since we serialized it ourselves in runQwenRawAsync + const data = parsed as CcusageDailyEntry[]; return {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cli/src/lib/qwen.ts` around lines 154 - 178, In parseQwenOutput, the code unconditionally casts parsed to CcusageDailyEntry[] (parsed as CcusageDailyEntry[]) which can hide malformed data; either add a minimal runtime shape check (e.g., ensure parsed is an array and each item has required keys like the CcusageDailyEntry fields) before accepting it or add a clear comment documenting the invariant coming from runQwenRawAsync; locate parseQwenOutput and the parsed/CcusageDailyEntry usage and implement the short validation or documentation change accordingly.packages/cli/src/lib/mistral.ts (1)
68-89: Helper functions are appropriate, thoughtoIsoDateis duplicated.The
extractDatefunction handles both ISO timestamps and folder naming patterns correctly. Note thattoIsoDateis identical to the one inqwen.ts— consider extracting to a shared utility if more providers are added in the future.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cli/src/lib/mistral.ts` around lines 68 - 89, The toIsoDate function is duplicated across providers (e.g., toIsoDate in this module and in qwen.ts); extract it into a shared utility module (e.g., create a util function named toIsoDate in a common helpers file), export it, replace the local toIsoDate with an import, and update any references (including extractDate which can continue to coexist) so both this module and qwen.ts import the single shared toIsoDate; ensure module exports/imports are correct and run type checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/cli/src/lib/gemini.ts`:
- Around line 33-35: The code currently treats any non-"[]" gcusage string as
valid; change the logic in runGeminiRawAsync where execGcusageAsync is called
(the block around execGcusageAsync([...]) and the duplicate at the later
occurrence) to JSON.parse the result and check Array.isArray(parsed) — only
return the raw result when parsed is an array; if parsing fails or parsed is not
an array, do not return (fall through to the session-reader fallback) and
optionally emit a low-confidence anomaly/log entry instead of letting
parseGeminiOutput silently treat non-arrays as empty.
---
Outside diff comments:
In `@packages/cli/src/commands/push.ts`:
- Around line 263-303: The current code only derives blockedDates from
codexParsed.entryMeta so unresolved rows from Gemini/Qwen/Mistral still get
merged; create an entryMeta->date map and blocked date Set for each provider
(e.g., geminiParsed.entryMeta, qwenParsed.entryMeta, mistralParsed.entryMeta)
similar to codexMetaByDate/blockedDates, filter each provider's data
(geminiParsed.data, qwenParsed.data, mistralParsed.data, and codexParsed.data)
to remove entries whose date is in that provider's blocked set (replace
codexEntries with per-provider filtered arrays), then pass those filtered arrays
into mergeEntries; also add a regression test that submits at least one
unresolved Gemini or Qwen or Mistral row to ensure these are skipped.
---
Nitpick comments:
In `@packages/cli/src/lib/mistral.ts`:
- Around line 68-89: The toIsoDate function is duplicated across providers
(e.g., toIsoDate in this module and in qwen.ts); extract it into a shared
utility module (e.g., create a util function named toIsoDate in a common helpers
file), export it, replace the local toIsoDate with an import, and update any
references (including extractDate which can continue to coexist) so both this
module and qwen.ts import the single shared toIsoDate; ensure module
exports/imports are correct and run type checks.
In `@packages/cli/src/lib/qwen.ts`:
- Around line 154-178: In parseQwenOutput, the code unconditionally casts parsed
to CcusageDailyEntry[] (parsed as CcusageDailyEntry[]) which can hide malformed
data; either add a minimal runtime shape check (e.g., ensure parsed is an array
and each item has required keys like the CcusageDailyEntry fields) before
accepting it or add a clear comment documenting the invariant coming from
runQwenRawAsync; locate parseQwenOutput and the parsed/CcusageDailyEntry usage
and implement the short validation or documentation change accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 25b6282e-ce9e-47e5-8fad-7e20ac5b08fc
📒 Files selected for processing (12)
apps/web/components/app/feed/ActivityCard.tsxapps/web/lib/utils/post-share.tsapps/web/lib/utils/recap.tsdocs/CHANGELOG.mddocs/DECISIONS.mdpackages/cli/__tests__/commands/push.test.tspackages/cli/__tests__/flows/cli-sync-flow.test.tspackages/cli/src/commands/push.tspackages/cli/src/lib/ccusage.tspackages/cli/src/lib/gemini.tspackages/cli/src/lib/mistral.tspackages/cli/src/lib/qwen.ts
| const result = await execGcusageAsync(["--json", "--period", "day", "--since", since, "--until", until]); | ||
| // gcusage returns "[]" when no telemetry.log exists — fall through to session reader | ||
| if (result.trim() !== "[]") return result; |
There was a problem hiding this comment.
Validate gcusage output before skipping the session fallback.
Only literal "[]" falls through today. If gcusage returns any other valid JSON shape, runGeminiRawAsync accepts it and parseGeminiOutput then silently turns the provider into data: [], even when ~/.gemini/tmp/*/chats has usable data. Please require an array payload here, or surface a low-confidence anomaly instead of treating non-arrays as empty.
Also applies to: 243-245
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/cli/src/lib/gemini.ts` around lines 33 - 35, The code currently
treats any non-"[]" gcusage string as valid; change the logic in
runGeminiRawAsync where execGcusageAsync is called (the block around
execGcusageAsync([...]) and the duplicate at the later occurrence) to JSON.parse
the result and check Array.isArray(parsed) — only return the raw result when
parsed is an array; if parsing fails or parsed is not an array, do not return
(fall through to the session-reader fallback) and optionally emit a
low-confidence anomaly/log entry instead of letting parseGeminiOutput silently
treat non-arrays as empty.
The function computed `normalized = model.trim()` but then used raw `model` for most regex tests and .includes() calls. Whitespace-padded model names failed anchor-based matches (^o3, ^o4, ^gpt-) and returned untrimmed strings for unknown models. All references now use `normalized`. Exported the function so tests import from source instead of copy-pasting. Added 12 unit tests covering whitespace, all provider patterns, and legacy fallbacks. Created 6 GitHub issues for larger items found during TD review: - #31 Race condition in usage/submit multi-device flow - #32 Deduplicate prettifyModel across 3 files - #33 Add rate limiting to DELETE endpoints - #34 Add missing test coverage for 17 untested API routes - #35 PR #22 multi-provider support review feedback - #36 Validate leaderboard cursor input Nightshift-Task: td-review Nightshift-Ref: https://github.com/marcus/nightshift Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: prettifyModel uses normalized (trimmed) input consistently The function computed `normalized = model.trim()` but then used raw `model` for most regex tests and .includes() calls. Whitespace-padded model names failed anchor-based matches (^o3, ^o4, ^gpt-) and returned untrimmed strings for unknown models. All references now use `normalized`. Exported the function so tests import from source instead of copy-pasting. Added 12 unit tests covering whitespace, all provider patterns, and legacy fallbacks. Created 6 GitHub issues for larger items found during TD review: - #31 Race condition in usage/submit multi-device flow - #32 Deduplicate prettifyModel across 3 files - #33 Add rate limiting to DELETE endpoints - #34 Add missing test coverage for 17 untested API routes - #35 PR #22 multi-provider support review feedback - #36 Validate leaderboard cursor input Nightshift-Task: td-review Nightshift-Ref: https://github.com/marcus/nightshift Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: update CHANGELOG issue ref from duplicate #32 to canonical #25 Closed duplicate issues #31-36 (duplicates of #24-29 from prior iteration). Nightshift-Task: td-review Nightshift-Ref: https://github.com/marcus/nightshift Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Summary
gemini.ts,qwen.ts,mistral.ts) that read local usage data from Gemini CLI, Qwen Code, and Mistral Vibe respectivelystraude pushand merge into the daily usage pipeline via a now-variadicmergeEntriesgcusageCLI first, falls back to reading~/.gemini/tmp/*/chats/session files directly~/.qwen/projects/*/chats/*.jsonl~/.vibe/logs/session/session_*/meta.jsonwith cost estimation from pricing metadataProvider JSON Output Formats
Gemini CLI (
gemini.ts)Reads from
gcusageCLI or falls back to~/.gemini/tmp/*/chats/session-*.json. Output format:[ { "date": "2026-03-04", "models": ["gemini-2.5-pro"], "input": 12500, "output": 3200, "thought": 800, "cache": 5000, "tool": 150 } ]Qwen Code (
qwen.ts)Reads JSONL from
~/.qwen/projects/*/chats/*.jsonl. Each line is a JSON object; assistant entries containusageMetadata:{ "type": "assistant", "timestamp": "2026-03-04T10:30:00Z", "model": "coder-model", "usageMetadata": { "promptTokenCount": 8500, "candidatesTokenCount": 1200, "thoughtsTokenCount": 400, "totalTokenCount": 10100, "cachedContentTokenCount": 3000 } }Aggregated output (internal, before merge):
[ { "date": "2026-03-04", "models": ["coder-model"], "inputTokens": 8500, "outputTokens": 1600, "cacheCreationTokens": 0, "cacheReadTokens": 3000, "totalTokens": 10100, "costUSD": 0 } ]Mistral Vibe (
mistral.ts)Reads
~/.vibe/logs/session/session_*/meta.json. Each session directory contains ameta.json:{ "session_id": "abc123", "start_time": "2026-02-22T10:02:44Z", "config": { "active_model": "devstral-2" }, "stats": { "session_prompt_tokens": 4874312, "session_completion_tokens": 13352, "input_price_per_million": 0.4, "output_price_per_million": 2.0, "session_cost": 4.3596708 } }Aggregated output (internal, before merge):
[ { "date": "2026-02-22", "models": ["devstral-2", "devstral-small"], "inputTokens": 34120186, "outputTokens": 93463, "cacheCreationTokens": 0, "cacheReadTokens": 0, "totalTokens": 34213649, "costUSD": 11.18 } ]Example CLI Output
Note:
coder-model= Qwen Code,gemini-2.5-pro= Gemini CLI,devstral-small= Mistral Vibe. All three new providers merge seamlessly with existing Claude and Codex data.Test plan
gemini-2.5-pro)coder-model)devstral-2,devstral-small)straude push— all providers merged correctlynpx vitest run)🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Documentation