Skip to content

feat: add bulk Codex usage viewer - #3272

Closed
smallwhiteman wants to merge 1 commit into
QuantumNous:mainfrom
smallwhiteman:feat/codex-bulk-usage-view
Closed

feat: add bulk Codex usage viewer#3272
smallwhiteman wants to merge 1 commit into
QuantumNous:mainfrom
smallwhiteman:feat/codex-bulk-usage-view

Conversation

@smallwhiteman

@smallwhiteman smallwhiteman commented Mar 16, 2026

Copy link
Copy Markdown

Summary

  • add an admin action to view all Codex OAuth channel usage in one modal
  • add a backend API to aggregate Codex usage results and sort accounts by usage
  • show each account's 5-hour and weekly windows inline instead of requiring an extra details click

Test plan

  • go test ./...
  • cd web && bun run build
  • rebuild and restart the embedded frontend binary locally
  • verify the bulk Codex usage modal is served from the rebuilt app

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added ability to view aggregated Codex usage metrics across all channels with summary totals.
    • New button provides quick access to bulk channel usage reporting with progress indicators.
    • Enhanced usage value formatting and automatic sorting by usage volume.
    • Streamlined modal workflow for viewing detailed channel usage data.

Add a dedicated Codex channel action to inspect all OAuth account usage in one place, with backend aggregation and an inline modal showing sorted rate-limit windows and raw payloads.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Mar 16, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This PR extends Codex usage tracking by introducing bulk aggregation across all channels. The backend adds a new endpoint and refactors usage fetching with token refresh and upstream error handling. The frontend adds UI components and modal flows to display aggregated usage data.

Changes

Cohort / File(s) Summary
Backend API & Routing
controller/codex_usage.go, router/api-router.go
Introduced new internal types (codexUsageFetchResult, codexBulkUsageItem, codexBulkUsageSummary) and refactored GetCodexChannelUsage. Added GetAllCodexChannelUsage with aggregation, sorting, and summary accumulation. Implemented fetchCodexChannelUsage with token validation, OAuth parsing, upstream request handling with optional token refresh on 401/403 responses, and extractCodexUsageValue for flexible payload parsing. New endpoint GET /api/channel/codex/usage/all wired to GetAllCodexChannelUsage.
Frontend UI Components
web/src/components/table/channels/ChannelsActions.jsx
Added openAllCodexUsage prop and conditionally rendered new button when activeTypeKey === '57' and enableTagMode is false to trigger bulk usage modal.
Frontend Modal & Helpers
web/src/components/table/channels/modals/CodexUsageModal.jsx
Introduced formatUsageValue helper and new public components BulkCodexUsageList and BulkCodexUsageLoader for displaying aggregated Codex usage. Added openBulkCodexUsageModal entry point for bulk modal flow with data fetching, progress handling, and error messaging.
Frontend Hook
web/src/hooks/channels/useChannelsData.jsx
Extended useChannelsData hook with new openAllCodexUsage method that invokes the bulk modal with onCopy callback for text copying and success/error notifications.

Sequence Diagram

sequenceDiagram
    actor User
    participant Frontend as Frontend UI
    participant Backend as Controller
    participant Upstream as Upstream API

    User->>Frontend: Click "View All Codex Usage"
    Frontend->>Backend: GET /api/channel/codex/usage/all
    
    loop For each channel
        Backend->>Backend: Validate channel & OAuth key
        Backend->>Upstream: Request usage (with current token)
        Upstream-->>Backend: 401/403 response
        Backend->>Backend: Refresh OAuth token
        Backend->>Upstream: Retry request (with new token)
        Upstream-->>Backend: Usage data
        Backend->>Backend: Extract usage value & build item
    end
    
    Backend->>Backend: Accumulate summary (Total, Finished, Success, Failed)
    Backend->>Backend: Sort items by UsageValue descending
    Backend-->>Frontend: Return bulk usage list + summary
    Frontend->>User: Display aggregated usage table with summary
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • feat: codex channel #2652: Both PRs modify Codex usage endpoints and controller code (GetCodexChannelUsage, OAuth token handling, /api/channel/codex/usage routes) with direct code-level overlaps.

Poem

🐰 Hoppy times for Codex metrics flow,
Bulk usage now puts on a show,
All channels gathered in one place,
Token refresh keeps the pace,
Summary sorted, fast and lean!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add bulk Codex usage viewer' accurately summarizes the main change: introducing a bulk usage viewer for Codex channels with aggregated usage display and sorting.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can suggest fixes for GitHub Check annotations.

Configure the reviews.tools.github-checks setting to adjust the time to wait for GitHub Checks to complete.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
controller/codex_usage.go (1)

205-210: ⚠️ Potential issue | 🟠 Major

Don’t swallow refreshed-token persistence failures.

If the key update fails here, this request can still succeed once with the in-memory token, but the next read falls back to the stale credential from DB/cache and refreshes again. Please surface the write error and only invalidate caches after the update succeeds.

Proposed fix
 			encoded, encErr := common.Marshal(oauthKey)
-			if encErr == nil {
-				_ = model.DB.Model(&model.Channel{}).Where("id = ?", ch.Id).Update("key", string(encoded)).Error
-				model.InitChannelCache()
-				service.ResetProxyClientCache()
-			}
+			if encErr != nil {
+				return 0, nil, encErr
+			}
+			if dbErr := model.DB.Model(&model.Channel{}).Where("id = ?", ch.Id).Update("key", string(encoded)).Error; dbErr != nil {
+				return 0, nil, dbErr
+			}
+			model.InitChannelCache()
+			service.ResetProxyClientCache()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/codex_usage.go` around lines 205 - 210, The update to persist the
refreshed token is currently swallowed; change the block that marshals oauthKey
and calls Model(&model.Channel{}).Where("id = ?", ch.Id).Update("key",
string(encoded)).Error so you capture the update error (e.g., updErr := ...
.Error), return or surface that error instead of ignoring it, and only call
model.InitChannelCache() and service.ResetProxyClientCache() when updErr == nil;
if the update fails, log/return the error to the caller so the failure is not
silently ignored and caches are not invalidated prematurely.
🧹 Nitpick comments (1)
web/src/components/table/channels/modals/CodexUsageModal.jsx (1)

281-283: Use useTranslation() in the new bulk modal components.

BulkCodexUsageList and BulkCodexUsageLoader keep extending the t prop chain instead of reading from react-i18next locally. Since these are new components, this is a good place to stop the prop drilling.

As per coding guidelines, "Frontend i18n: Use i18next + react-i18next + i18next-browser-languagedetector. Translation files in web/src/i18n/locales/{lang}.json must be flat JSON with Chinese source strings as keys. Use useTranslation() hook and call t('中文key') in components."

Also applies to: 403-405

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/table/channels/modals/CodexUsageModal.jsx` around lines
281 - 283, Replace the manual t prop usage in the new modal components by using
react-i18next's hook: import useTranslation from 'react-i18next' and inside
BulkCodexUsageList (and BulkCodexUsageLoader) call const { t } =
useTranslation(); remove the tt fallback and the t parameter from the component
props/signature, update all t(...) references to the local t, and remove t from
any parent prop drilling for these components so translations come from
useTranslation() locally.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@controller/codex_usage.go`:
- Around line 83-105: The loop over channels currently calls
fetchCodexChannelUsage serially, letting slow upstream requests in
requestCodexUsageWithRefresh stall the whole operation; change this to a bounded
parallel fetch (e.g., use an errgroup with a semaphore or worker pool) to run a
limited number (N) of concurrent calls to fetchCodexChannelUsage, collect
results into codexBulkUsageItem and append to items safely (protect items and
summary with a mutex or send results on a channel), and update
summary.Finished/Success/Failed atomically; ensure you preserve the
ChannelID/Name/Status mapping when aggregating concurrent results.
- Line 12: Remove the direct encoding/json import and the unreachable
json.Number case in the type switch: delete the "encoding/json" import from the
imports list and remove the switch case that handles json.Number in the function
that processes unmarshaled values (reference common.Unmarshal() and the case for
json.Number) so the file only relies on common/json.go wrappers and no dead-case
remains.

In `@web/src/components/table/channels/modals/CodexUsageModal.jsx`:
- Around line 468-483: The progress handler (onDownloadProgress) is treating the
response as NDJSON and looking for parsed.channels, but GetAllCodexChannelUsage
returns a single JSON object { summary, data }, so parsed?.channels is always
empty; change onDownloadProgress to parse the full responseText once (not
line-by-line), read parsed.data (the channels array) instead of parsed.channels,
and seed placeholders via buildPlaceholderItems(parsed.data) -> sortItems ->
setItems and setSummary(normalizeSummary(...)). To avoid reparsing on every
progress event, add a small parsedOnceRef (similar to mountedRef) or compare
responseText length and only run the parsing/setting logic once per new full
payload; keep the same guards using mountedRef when setting state.

---

Outside diff comments:
In `@controller/codex_usage.go`:
- Around line 205-210: The update to persist the refreshed token is currently
swallowed; change the block that marshals oauthKey and calls
Model(&model.Channel{}).Where("id = ?", ch.Id).Update("key",
string(encoded)).Error so you capture the update error (e.g., updErr := ...
.Error), return or surface that error instead of ignoring it, and only call
model.InitChannelCache() and service.ResetProxyClientCache() when updErr == nil;
if the update fails, log/return the error to the caller so the failure is not
silently ignored and caches are not invalidated prematurely.

---

Nitpick comments:
In `@web/src/components/table/channels/modals/CodexUsageModal.jsx`:
- Around line 281-283: Replace the manual t prop usage in the new modal
components by using react-i18next's hook: import useTranslation from
'react-i18next' and inside BulkCodexUsageList (and BulkCodexUsageLoader) call
const { t } = useTranslation(); remove the tt fallback and the t parameter from
the component props/signature, update all t(...) references to the local t, and
remove t from any parent prop drilling for these components so translations come
from useTranslation() locally.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9febb4ca-b0f4-4d72-aa31-0e0e4c802512

📥 Commits

Reviewing files that changed from the base of the PR and between 0eee341 and 0db84df.

📒 Files selected for processing (5)
  • controller/codex_usage.go
  • router/api-router.go
  • web/src/components/table/channels/ChannelsActions.jsx
  • web/src/components/table/channels/modals/CodexUsageModal.jsx
  • web/src/hooks/channels/useChannelsData.jsx

Comment thread controller/codex_usage.go
"strings"
"time"

"encoding/json"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the file structure and locate the file
find . -name "codex_usage.go" -type f 2>/dev/null | head -5

Repository: QuantumNous/new-api

Length of output: 90


🏁 Script executed:

# Get the file size to determine how much we can read
wc -l controller/codex_usage.go 2>/dev/null || echo "File not found"

Repository: QuantumNous/new-api

Length of output: 92


🏁 Script executed:

# Read the import section (around line 12) and the problematic section (248-275)
echo "=== Lines 1-25 (import section) ===" && sed -n '1,25p' controller/codex_usage.go && echo -e "\n=== Lines 245-280 (problematic section) ===" && sed -n '245,280p' controller/codex_usage.go

Repository: QuantumNous/new-api

Length of output: 1207


🏁 Script executed:

# Check if encoding/json is directly used in the file
rg "encoding/json|json\\.Number" controller/codex_usage.go -n

Repository: QuantumNous/new-api

Length of output: 106


🏁 Script executed:

# Check what's available in common/json.go
cat -n common/json.go | head -100

Repository: QuantumNous/new-api

Length of output: 1132


🏁 Script executed:

# Check all usages of json.Number in the file
rg "json\\.Number" controller/codex_usage.go -n -B 2 -A 2

Repository: QuantumNous/new-api

Length of output: 187


🏁 Script executed:

# Check if json.Number or UseNumber is used anywhere in the codebase
rg "json\\.Number|UseNumber" --type go | head -20

Repository: QuantumNous/new-api

Length of output: 451


🏁 Script executed:

# Check the full lookupFloat function context and where it's called
sed -n '248,277p' controller/codex_usage.go

Repository: QuantumNous/new-api

Length of output: 663


🏁 Script executed:

# Check if there are any alternatives or patterns for handling json.Number in common/json.go
rg "UseNumber|json\.Number|RawMessage|Number" common/json.go -n

Repository: QuantumNous/new-api

Length of output: 113


🏁 Script executed:

# Check all uses of the json package (from encoding/json import) in the file
rg "json\." controller/codex_usage.go -n | grep -v "gjson\." | grep -v "json\""

Repository: QuantumNous/new-api

Length of output: 86


🏁 Script executed:

# Check the context where lookupFloat is called to understand the data source
rg "lookupFloat" controller/codex_usage.go -n -B 3 -A 3

Repository: QuantumNous/new-api

Length of output: 662


🏁 Script executed:

# Check if service/error.go also imports encoding/json
head -20 service/error.go

Repository: QuantumNous/new-api

Length of output: 422


🏁 Script executed:

# Check how error.go uses json.Number to see if there's a pattern we should follow
rg "json\.Number" service/error.go -n -B 3 -A 3

Repository: QuantumNous/new-api

Length of output: 242


🏁 Script executed:

# Find where the maps being passed to lookupFloat are created
sed -n '220,246p' controller/codex_usage.go

Repository: QuantumNous/new-api

Length of output: 716


🏁 Script executed:

# Search for UseNumber in the codebase to understand the source of json.Number values
rg "UseNumber" --type go

Repository: QuantumNous/new-api

Length of output: 45


🏁 Script executed:

# Check if there's any comment or documentation about why json.Number handling is needed
sed -n '200,248p' controller/codex_usage.go | cat -n

Repository: QuantumNous/new-api

Length of output: 1775


🏁 Script executed:

# Find where FetchCodexWhamUsage is defined and how it unmarshals
rg "FetchCodexWhamUsage" --type go -A 30 | head -50

Repository: QuantumNous/new-api

Length of output: 3109


🏁 Script executed:

# Search more specifically in service for codex usage fetching
rg "extractCodexUsageValue" controller/codex_usage.go -B 10 | head -30

Repository: QuantumNous/new-api

Length of output: 606


🏁 Script executed:

# Check if any decoder.UseNumber() calls exist in the codebase for codex handling
rg "Decode|Unmarshal" controller/codex_usage.go -n -B 2 -A 2

Repository: QuantumNous/new-api

Length of output: 174


🏁 Script executed:

# Let's see the full context of how the payload arrives at extractCodexUsageValue
sed -n '150,225p' controller/codex_usage.go

Repository: QuantumNous/new-api

Length of output: 2585


🏁 Script executed:

# Check how common.Unmarshal is implemented to see if it uses UseNumber
cat -n common/json.go

Repository: QuantumNous/new-api

Length of output: 1132


🏁 Script executed:

# Check if there's any UseNumber usage in codex_wham_usage.go
cat service/codex_wham_usage.go | head -50

Repository: QuantumNous/new-api

Length of output: 1171


🏁 Script executed:

# Verify the actual behavior: check if json.Number is possible without UseNumber()
cat > /tmp/test_unmarshal.go << 'EOF'
package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	// Standard Unmarshal without UseNumber
	var payload any
	err := json.Unmarshal([]byte(`{"value": 123.45}`), &payload)
	if err != nil {
		return
	}
	m := payload.(map[string]any)
	v := m["value"]
	fmt.Printf("Type of value: %T\n", v)
	
	// With UseNumber via Decoder
	var payload2 any
	decoder := json.NewDecoder(nil)
	decoder.UseNumber()
	// Would produce json.Number, but not with plain Unmarshal
}
EOF
cat /tmp/test_unmarshal.go

Repository: QuantumNous/new-api

Length of output: 528


🏁 Script executed:

# Search for UseNumber in the codebase to see if any decoder is using it
rg "UseNumber" --type go

Repository: QuantumNous/new-api

Length of output: 45


🏁 Script executed:

# Double-check: is json.Number actually reachable in this code flow?
# Look at what types can be returned from the Codex API unmarshaling
sed -n '160,170p' controller/codex_usage.go

Repository: QuantumNous/new-api

Length of output: 287


🏁 Script executed:

# Create a simple test to confirm what types json.Unmarshal produces
python3 << 'EOF'
# Standard json.Unmarshal without UseNumber produces float64 for JSON numbers
# json.Number is ONLY produced when using json.NewDecoder().UseNumber()
print("Standard json.Unmarshal behavior:")
print("- JSON numbers -> Go float64 (not json.Number)")
print("- json.Number is ONLY available via json.Decoder with UseNumber()")
print("")
print("Since common.Unmarshal uses json.Unmarshal directly (no UseNumber),")
print("the case json.Number branch in lookupFloat is UNREACHABLE dead code.")
EOF

Repository: QuantumNous/new-api

Length of output: 347


🏁 Script executed:

# Verify there are no decoder.UseNumber() calls related to codex usage parsing
rg "DecodeJson|UseNumber|json.NewDecoder" service/codex_wham_usage.go

Repository: QuantumNous/new-api

Length of output: 45


🏁 Script executed:

# Final check: confirm the import is used ONLY for json.Number
rg "json\\..*" controller/codex_usage.go -n | grep -v "gjson"

Repository: QuantumNous/new-api

Length of output: 86


Remove the direct encoding/json import — it violates the wrapper-only JSON rule and is unreachable.

The encoding/json import at line 12 breaks the codebase's requirement to use only common/json.go wrappers for JSON operations. The json.Number case at line 265 is dead code: common.Unmarshal() uses standard json.Unmarshal without UseNumber(), which produces float64 for JSON numbers, never json.Number. Delete the import and remove the unreachable case statement.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/codex_usage.go` at line 12, Remove the direct encoding/json import
and the unreachable json.Number case in the type switch: delete the
"encoding/json" import from the imports list and remove the switch case that
handles json.Number in the function that processes unmarshaled values (reference
common.Unmarshal() and the case for json.Number) so the file only relies on
common/json.go wrappers and no dead-case remains.

Comment thread controller/codex_usage.go
Comment on lines +83 to +105
for _, ch := range channels {
if ch == nil {
continue
}
result := fetchCodexChannelUsage(c.Request.Context(), ch)
item := codexBulkUsageItem{
ChannelID: ch.Id,
ChannelName: ch.Name,
ChannelStatus: ch.Status,
Success: result.Success,
Message: result.Message,
UpstreamStatus: result.UpstreamStatus,
UsageValue: result.UsageValue,
Data: result.Payload,
}
items = append(items, item)
summary.Finished++
if item.Success {
summary.Success++
} else {
summary.Failed++
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Bound the bulk fetch with concurrency instead of serial calls.

This loop does the upstream request, optional token refresh, and retry one channel at a time. With the 15s/10s/15s timeouts in requestCodexUsageWithRefresh, a few slow accounts can push this endpoint past common proxy timeouts and make the bulk modal unusable. A small worker pool or errgroup limit here would keep one bad channel from stalling the whole admin action.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/codex_usage.go` around lines 83 - 105, The loop over channels
currently calls fetchCodexChannelUsage serially, letting slow upstream requests
in requestCodexUsageWithRefresh stall the whole operation; change this to a
bounded parallel fetch (e.g., use an errgroup with a semaphore or worker pool)
to run a limited number (N) of concurrent calls to fetchCodexChannelUsage,
collect results into codexBulkUsageItem and append to items safely (protect
items and summary with a mutex or send results on a channel), and update
summary.Finished/Success/Failed atomically; ensure you preserve the
ChannelID/Name/Status mapping when aggregating concurrent results.

Comment on lines +468 to +483
onDownloadProgress: (progressEvent) => {
const xhr = progressEvent?.event?.target;
const responseText = xhr?.responseText;
if (!mountedRef.current || !responseText) return;
const lastLine = responseText
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
.pop();
if (!lastLine) return;
try {
const parsed = JSON.parse(lastLine);
const nextItems = sortItems(buildPlaceholderItems(parsed?.channels || []));
setItems(nextItems);
setSummary(normalizeSummary(nextItems));
} catch (error) {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

The progress parser is out of sync with /api/channel/codex/usage/all.

GetAllCodexChannelUsage returns { summary, data } in one JSON document, not a channels field or an NDJSON stream. parsed?.channels is therefore always empty here, so this handler never seeds placeholder rows and just keeps reparsing the growing response buffer for no UI gain.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/table/channels/modals/CodexUsageModal.jsx` around lines
468 - 483, The progress handler (onDownloadProgress) is treating the response as
NDJSON and looking for parsed.channels, but GetAllCodexChannelUsage returns a
single JSON object { summary, data }, so parsed?.channels is always empty;
change onDownloadProgress to parse the full responseText once (not
line-by-line), read parsed.data (the channels array) instead of parsed.channels,
and seed placeholders via buildPlaceholderItems(parsed.data) -> sortItems ->
setItems and setSummary(normalizeSummary(...)). To avoid reparsing on every
progress event, add a small parsedOnceRef (similar to mountedRef) or compare
responseText length and only run the parsing/setting logic once per new full
payload; keep the same guards using mountedRef when setting state.

@seefs001

Copy link
Copy Markdown
Collaborator

不考虑增加对codex渠道做批量操作的功能,这个渠道是仅供个人使用,不向号池发展。

@seefs001 seefs001 closed this Mar 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants