Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
7ff4281
fix(grok): normalize non-object tool parameter schema roots
ifThink404 Aug 26, 2026
c284771
fix(errors): surface Grok string-form upstream error bodies
ifThink404 Aug 26, 2026
d2cd0ee
Merge pull request #2 from ifThink404/codex/fix-grok-tool-schema-root
ifThink404 Aug 26, 2026
ab83539
fix(grok): normalize union roots even when type is object
ifThink404 Aug 26, 2026
c467378
merge: sync official v2.8.4 into production-main
ifThink404 Aug 27, 2026
036dc61
feat(claude): add Claude Code OAuth provider with fingerprint stabili…
ifThink404 Aug 28, 2026
a016d3d
feat(security): optional at-rest encryption for credential tokens
ifThink404 Aug 28, 2026
3870dac
fix(claude): make OAuth login robust with primary/fallback HTTP clients
ifThink404 Aug 28, 2026
1530681
Merge remote-tracking branch 'origin/main' into codex/production-main
ifThink404 Aug 28, 2026
983d494
fix(claude): route native claude-* models to Claude accounts
ifThink404 Aug 28, 2026
abbad6a
feat(claude): account-scoped model catalog + correct 4.5 pricing
ifThink404 Aug 28, 2026
239a55e
feat(pricing): include Claude models + per-provider channel in pricin…
ifThink404 Aug 28, 2026
131519e
feat(pricing-ui): group model pricing by provider (Codex/Grok/Antigra…
ifThink404 Aug 28, 2026
df641f4
feat(claude): dynamic per-account model discovery + Anthropic officia…
ifThink404 Aug 28, 2026
3faf327
feat(pricing-ui): Anthropic official-price toggle + per-account claud…
ifThink404 Aug 28, 2026
d85fd7c
feat(pricing-ui): model catalog with quick-jump, refresh, and NEW badges
ifThink404 Aug 28, 2026
4beb402
feat(claude-ui): stats/filters/usage on the Claude accounts page
ifThink404 Aug 28, 2026
659b610
feat(claude): add experimental Claude Code (Anthropic) OAuth provider
ifThink404 Aug 28, 2026
86a2a74
Merge remote-tracking branch 'fork/codex/production-main' into feat/c…
ifThink404 Aug 28, 2026
3b2d426
docs: define Claude provider parity design
ifThink404 Aug 29, 2026
9e8af6b
docs: plan Claude parity implementation
ifThink404 Aug 29, 2026
40996f2
fix(claude): isolate provider sampling and channel routing
ifThink404 Aug 29, 2026
ea37bd4
feat(frontend): add Claude provider parity
ifThink404 Aug 29, 2026
57f12ba
fix(claude): keep provider out of Codex probes
ifThink404 Aug 29, 2026
a198599
fix(claude): isolate provider models and usage semantics
ifThink404 Aug 29, 2026
f6f3d94
feat: complete Claude provider parity
ifThink404 Aug 29, 2026
f32a609
merge: complete Claude provider parity into production
ifThink404 Aug 29, 2026
ad9e1ea
merge: sync official main into production
ifThink404 Aug 29, 2026
d40f363
test: stop scheduler before database cleanup
ifThink404 Aug 29, 2026
8fc8092
feat(claude): add portable credentials and stable UA audit
ifThink404 Aug 29, 2026
dd4ac96
fix(claude): skip warmup for disabled imports
ifThink404 Aug 29, 2026
f69d130
fix(claude): validate imported metadata
ifThink404 Aug 29, 2026
4539946
fix(claude): expose upstream user agent in account rows
ifThink404 Aug 29, 2026
a1fca0d
fix(claude): deduplicate rotated refresh tokens
ifThink404 Aug 29, 2026
10639b0
feat(claude): harden provider security and parity
ifThink404 Aug 30, 2026
90b6ee9
fix(claude): align request limits with Sub2API
ifThink404 Aug 30, 2026
2edc157
feat(proxy): embed IP risk scoring in proxy management
ifThink404 Aug 31, 2026
827a4ee
Merge remote-tracking branch 'origin/main' into codex/production-main
ifThink404 Aug 31, 2026
5d617a0
fix(admin): drop removed subscription upgrade initializer
ifThink404 Aug 31, 2026
970045e
ci: rerun official checks after merge cleanup
ifThink404 Aug 31, 2026
0383de8
Merge remote-tracking branch 'origin/main' into codex/production-main
ifThink404 Aug 31, 2026
ea0f95f
Merge remote-tracking branch 'origin/main' into codex/production-main
ifThink404 Aug 31, 2026
32d60c3
fix(auth): clarify Claude unauthorized state and cooldown
ifThink404 Aug 31, 2026
e1da7f0
Merge remote-tracking branch 'origin/main' into codex/production-main
ifThink404 Aug 31, 2026
eb5842b
test(ci): isolate Grok import probe and split database race
ifThink404 Aug 31, 2026
803c217
merge(main): resolve production branch conflicts
ifThink404 Aug 31, 2026
77aa7dd
merge(main): resolve latest export conflict
ifThink404 Sep 1, 2026
718a8af
merge(main): resolve account proxy badge conflict
ifThink404 Sep 1, 2026
5eb2cfc
fix Claude official pricing and quota sync
ifThink404 Sep 2, 2026
f5a24d6
docs: specify Claude client version policy
ifThink404 Sep 2, 2026
57a53ec
feat: enforce Claude Code client platform and versions
ifThink404 Sep 2, 2026
5b4f5ad
fix: gate Claude client versions before scheduling
ifThink404 Sep 2, 2026
54c64ff
fix(claude): backfill legacy usage windows on account page
ifThink404 Sep 2, 2026
6981c77
docs: specify Claude CLI version sync and fingerprint alignment
ifThink404 Sep 2, 2026
9092dde
docs: implementation plan for Claude CLI version sync
ifThink404 Sep 2, 2026
0e1ef0a
feat(auth): add effective Claude CLI version and UA version rewrite
ifThink404 Sep 2, 2026
9c99d8b
feat(auth): pin generated Claude fingerprint UA to effective CLI version
ifThink404 Sep 2, 2026
3372b2a
feat(auth): refresh Claude fingerprint UA versions to effective CLI v…
ifThink404 Sep 2, 2026
c23cdf8
fix(auth): guard fingerprint refresh in-memory write against concurre…
ifThink404 Sep 2, 2026
6992f5b
feat(auth): add Claude CLI version sync toggle and interval to Claude…
ifThink404 Sep 2, 2026
a33082c
style(auth): gofmt ClaudeConfig and Store field alignment
ifThink404 Sep 2, 2026
c6d19bc
feat(database): persist Claude synced CLI version and account custom …
ifThink404 Sep 2, 2026
d13b2e2
feat(proxy): sync latest Claude Code CLI version and refresh fingerpr…
ifThink404 Sep 2, 2026
fe0259b
test(proxy): cover never-downgrade, DB persistence, and env kill swit…
ifThink404 Sep 2, 2026
56e475c
fix(proxy): align forced Claude fingerprint UA version with model flo…
ifThink404 Sep 2, 2026
af59fb1
test(proxy): cover outbound UA alignment wiring and fail closed on re…
ifThink404 Sep 2, 2026
27ba39a
feat(admin): expose Claude CLI version sync settings and manual sync …
ifThink404 Sep 2, 2026
423acab
feat(frontend): add Claude CLI version sync types, API, and copy
ifThink404 Sep 2, 2026
090a57e
feat(frontend): Claude settings use shared Select and expose CLI vers…
ifThink404 Sep 2, 2026
d77202e
refactor(frontend): replace raw selects with shared Select and guard …
ifThink404 Sep 2, 2026
8821562
fix(frontend): keep Proxies risk selects content-sized after shared S…
ifThink404 Sep 2, 2026
741d8bd
docs: add frontend UI constraints (DESIGN.md) and enforce in CLAUDE.md
ifThink404 Sep 2, 2026
e97383e
chore: keep CLAUDE.md untracked (local GitNexus + UI constraints)
ifThink404 Sep 2, 2026
92135a9
fix: harden Claude fingerprint refresh, sync error reporting, and UA …
ifThink404 Sep 2, 2026
29147da
fix(proxy): recover Claude requests carrying invalid thinking signatu…
ifThink404 Sep 2, 2026
de1df33
fix(billing): bill Claude prompt-cache reads and writes with Anthropi…
ifThink404 Sep 2, 2026
d4c0821
fix(billing): persist Claude cache-write tokens from native usage int…
ifThink404 Sep 2, 2026
7a4abc1
fix(billing): do not double count streamed Claude cache writes
ifThink404 Sep 2, 2026
6dc71ce
fix(claude): stop routing models an account's plan does not include
ifThink404 Sep 2, 2026
df065cd
fix(claude): drop thinking.type=disabled for always-on thinking models
ifThink404 Sep 2, 2026
ef04932
fix(claude): also rectify effort-vs-disabled-thinking rejections
ifThink404 Sep 2, 2026
e5c027a
feat(claude): first-token timeout, pre-first-token SSE keepalive and …
ifThink404 Sep 3, 2026
ac72a77
fix(admin): keep canonical-cased Claude identity headers on same-time…
ifThink404 Sep 3, 2026
a60d633
fix(registry): allow major-only GPT versions such as gpt-6-astra
ifThink404 Sep 4, 2026
2dac769
fix(registry): only extract model ids from quoted/codex -m contexts o…
ifThink404 Sep 4, 2026
982c661
feat(models): refresh every channel from the model catalog button
ifThink404 Sep 4, 2026
820bcb8
feat(models): plan-group sampling with live progress for model refres…
ifThink404 Sep 4, 2026
93b4452
feat(prompt): retention policy for audit logs with CY-linked rows pro…
ifThink404 Sep 4, 2026
e1ecc5a
feat(prompt): show linked risk profiles on CY detail and allow contex…
ifThink404 Sep 4, 2026
2c5aba6
fix(prompt): accept natural-language security boundaries in identity …
ifThink404 Sep 4, 2026
6722c5c
fix(prompt): accept negated classification boundaries in identity cla…
ifThink404 Sep 4, 2026
e469c4a
feat(prompt): show linked risk profiles on CY learning-review evidence
ifThink404 Sep 4, 2026
b6d0d40
feat(prompt): let AI draft the rule from CY evidence in the learning …
ifThink404 Sep 4, 2026
c0f467e
fix(prompt): validate AI rule drafts like manual drafts and check the…
ifThink404 Sep 4, 2026
5b9add5
fix(prompt): own provider selection for AI rule drafts and show candi…
ifThink404 Sep 4, 2026
05015d1
fix(prompt): keep learning-review rule text inside its column
ifThink404 Sep 4, 2026
b851ed8
fix(prompt): widen learning-review source and action columns and wrap…
ifThink404 Sep 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions .github/scripts/go-race-packages.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
#!/usr/bin/env bash
# Print the `go test` selector args for one test-race shard so heavy packages
# do not share a 2-core runner. The admin package alone takes ~12min under
# -race on a 2-core runner (long tail of 1.5-5s tests, no single hot spot),
# which collided with the 12m go-test timeout — so it is further split in two
# by test-name initial (^Test[A-L] ≈ 45% of measured runtime, ^Test[^A-L] the
# rest; the two regexes are complementary, no test can be silently skipped).
# do not share a 2-core runner. Admin and database both have long tails under
# -race, so each is split in two by test-name initial. The complementary
# regexes cover every Test* function without silently skipping tests.
# The `rest` shard is everything except the dedicated shards.
set -euo pipefail

Expand All @@ -16,8 +14,11 @@ case "$shard" in
admin-m-z)
echo "-run ^Test[^A-L] ./admin"
;;
database)
echo ./database
database-a-l)
echo "-run ^Test[A-L] ./database"
;;
database-m-z)
echo "-run ^Test[^A-L] ./database"
;;
proxy)
echo ./proxy/...
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ jobs:
include:
- name: admin-a-l
- name: admin-m-z
- name: database
- name: database-a-l
- name: database-m-z
- name: proxy
- name: promptfilter
- name: rest
Expand Down
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,8 @@ grok-build-main/
.superpowers/
CLAUDE.md
.cursor
diagrams/
diagrams/
# local run artifacts
/data/
/codex2api_local
/server.log
40 changes: 40 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# DESIGN.md — 前端 UI 约束

本文件是 `frontend/` 的组件与布局约束。所有前端改动(含 AI 代理生成的代码)必须遵守;
`frontend/src/lib/uiConventions.test.mjs` 与 `claudeParity.test.mjs` 会在 CI 中强制其中可机检的部分。

## 1. 表单控件:只用共享组件,不手写

| 需求 | 必须使用 | 禁止 |
|---|---|---|
| 下拉选择 | `components/ui/select.tsx` 的 `Select`(`options` 数组,`value` / `onValueChange`) | 原生 `<select>` / 自定义 className 字符串(如 `selectCls`) |
| 开关 | `components/ui/switch` 的 `Switch` | `<input type="checkbox">` |
| 数字输入 | `components/ui/draft-number-input` 的 `DraftNumberInput`(带 `min` / `max`) | `<Input type="number">`(仅历史遗留允许) |
| 文本输入 | `components/ui/input` 的 `Input` | 原生 `<input>` |
| 少量互斥选项 | `Settings.tsx` 的 `SegmentedPillGroup` | 手写按钮组 |
| 按钮 | `components/ui/button` 的 `Button`,图标用 lucide,加载态用 `RefreshCw` + `animate-spin` | 原生 `<button>` |

需要新的表单控件时,先在 `components/ui/` 新增共享组件,再在页面使用;不在页面内部就地实现。

## 2. 设置页布局

- 每个配置模块用 `SettingsCard`(`title` / `description` / `icon` / `footer`)。
- 单个配置项用 `SettingField`(`label` / `description` / `layout="switch"` 可选),说明性提示用 `SettingHelp`。
- 栅格只用 `SETTINGS_FIELD_GRID` / `SETTINGS_FIELD_GRID_3` / `SETTINGS_SWITCH_GRID` 常量,不手写 `grid-cols-*`。
- "开关 + 数值"成对的行(例如自动同步 + 间隔)沿用 Codex 运行时优化区块的两列边框布局;新增同类区块直接复制该结构。
- 版本号、ID 等等宽内容用 `font-mono text-xs text-muted-foreground`。

## 3. 文案

- 所有可见文案走 `t('namespace.key')`;新增 key 必须同时写入 `locales/zh.json`、`en.json`、`zh-TW.json` 三个文件的同一位置。
- 占位符用 `{{name}}`,不用字符串拼接。

## 4. 守卫测试

- 新增或改动设置区块时,在 `frontend/src/lib/claudeParity.test.mjs`(Claude 相关)或对应的源码守卫测试里加断言,覆盖:使用了哪个共享组件、调用了哪个 API 方法、i18n key 存在。
- `uiConventions.test.mjs` 会扫描 `pages/` 与 `components/`(排除 `components/ui/select.tsx`)拒绝任何原生 `<select>`。

## 5. 参照实现

- 共享下拉:`frontend/src/pages/Settings.tsx` ClaudeCode 卡片的时区 / 指纹模式 / 平台 / 版本策略字段。
- 同步按钮 + 自动同步开关 + 间隔:Settings.tsx 中 Codex "运行时优化" 与 ClaudeCode "CLI 版本同步" 区块。
8 changes: 4 additions & 4 deletions admin/account_analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ func buildAccountQuotaAnalysis(items []*accountListSnapshotItem, window string)
}
totalUsed := 0.0
for _, item := range items {
if item.Status == "unauthorized" || item.Status == "error" || item.OpenAIResponses || (window == "5h" && !accountListSubscriptionPlan(item.PlanType)) {
if item.Status == "unauthorized" || item.Status == "error" || item.OpenAIResponses || (window == "5h" && !accountList5hQuotaEligible(item)) {
continue
}
result.Total++
Expand Down Expand Up @@ -431,7 +431,7 @@ func buildAccountResetAnalysis(items []*accountListSnapshotItem, now time.Time)

func accountRecoveryAt(item *accountListSnapshotItem, window string, now time.Time) (time.Time, bool) {
if window == "5h" {
if accountListSubscriptionPlan(item.PlanType) && item.UsagePercent5hOK && item.UsagePercent5h >= 100 && item.Reset5hAt.After(now) {
if accountList5hQuotaEligible(item) && item.UsagePercent5hOK && item.UsagePercent5h >= 100 && item.Reset5hAt.After(now) {
return item.Reset5hAt, false
}
if item.CooldownUntil.After(now) && accountAnalysisShortRateLimited(item) {
Expand Down Expand Up @@ -463,7 +463,7 @@ func accountAnalysisShortRateLimited(item *accountListSnapshotItem) bool {

func accountAnalysisWindowRateLimited(item *accountListSnapshotItem, window string) bool {
if window == "5h" {
return (accountListSubscriptionPlan(item.PlanType) && item.UsagePercent5hOK && item.UsagePercent5h >= 100) || accountAnalysisShortRateLimited(item)
return (accountList5hQuotaEligible(item) && item.UsagePercent5hOK && item.UsagePercent5h >= 100) || accountAnalysisShortRateLimited(item)
}
status := strings.ToLower(item.Status)
reason := strings.ToLower(item.CooldownReason)
Expand All @@ -476,7 +476,7 @@ func accountAnalysisHasBurnPrediction(item *accountListSnapshotItem, window stri
return false
}
if window == "5h" {
return accountListSubscriptionPlan(item.PlanType) && item.UsagePercent5hOK
return accountList5hQuotaEligible(item) && item.UsagePercent5hOK
}
return true
}
Expand Down
5 changes: 5 additions & 0 deletions admin/account_groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,9 @@ func accountRowGroupChannel(row *database.AccountRow) string {
if row != nil && strings.EqualFold(strings.TrimSpace(row.GetCredential("upstream_type")), auth.UpstreamAntigravity) {
return database.AccountGroupChannelAntigravity
}
if row != nil && strings.EqualFold(strings.TrimSpace(row.GetCredential("upstream_type")), auth.UpstreamClaude) {
return database.AccountGroupChannelClaude
}
if isGrokAccountRow(row) {
return database.AccountGroupChannelGrok
}
Expand Down Expand Up @@ -521,6 +524,8 @@ func groupChannelDisplayName(channel string) string {
return "Grok"
case database.AccountGroupChannelAntigravity:
return "Antigravity"
case database.AccountGroupChannelClaude:
return "Claude"
default:
return "Codex"
}
Expand Down
187 changes: 129 additions & 58 deletions admin/account_response_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ func (h *Handler) buildAccountResponse(
isOpenAIResponsesAccount := strings.EqualFold(upstreamType, auth.UpstreamOpenAIResponses)
isGrokAccount := strings.EqualFold(upstreamType, auth.UpstreamGrok)
isAntigravityAccount := strings.EqualFold(upstreamType, auth.UpstreamAntigravity)
isClaudeAccount := strings.EqualFold(upstreamType, auth.UpstreamClaude)
antigravityAuthKind := ""
if isAntigravityAccount {
if strings.TrimSpace(row.GetCredential("api_key")) != "" {
Expand Down Expand Up @@ -128,9 +129,39 @@ func (h *Handler) buildAccountResponse(
}
// 指纹收敛只作用于 Codex 官方出站路径,中转/Grok 账号不暴露该字段。
codexFingerprintMode := ""
if !isOpenAIResponsesAccount && !isGrokAccount && !isAntigravityAccount {
if !isOpenAIResponsesAccount && !isGrokAccount && !isAntigravityAccount && !isClaudeAccount {
codexFingerprintMode = auth.NormalizeCodexFingerprintMode(row.GetCredential(auth.CodexFingerprintModeCredentialKey))
}
// Claude Code 指纹收敛模式 + 绑定时区,仅 Claude OAuth 账号暴露。
claudeFingerprintMode := ""
accountTimezone := ""
claudeClientPlatformOverride := ""
claudeVersionPolicyOverride := ""
claudeClientVersionOverride := ""
claudeClientPolicy := auth.ClaudeClientPolicy{}
if strings.EqualFold(strings.TrimSpace(row.GetCredential("upstream_type")), auth.UpstreamClaude) {
claudeClientPolicy = auth.ClaudeClientPolicy{Platform: auth.ClaudeClientPlatformAny, VersionPolicy: auth.ClaudeVersionPolicyPassthrough}
claudeFingerprintMode = auth.NormalizeClaudeFingerprintMode(row.GetCredential(auth.ClaudeFingerprintModeCredentialKey))
accountTimezone = strings.TrimSpace(row.GetCredential("timezone"))
claudeClientPlatformOverride = strings.ToLower(strings.TrimSpace(row.GetCredential(auth.ClaudeClientPlatformCredentialKey)))
claudeVersionPolicyOverride = strings.ToLower(strings.TrimSpace(row.GetCredential(auth.ClaudeVersionPolicyCredentialKey)))
claudeClientVersionOverride = strings.TrimSpace(row.GetCredential(auth.ClaudeClientVersionCredentialKey))
if h.store != nil {
claudeClientPolicy = h.store.ClaudeClientPolicy()
}
if claudeClientPlatformOverride != "" {
claudeClientPolicy.Platform = auth.ClaudeClientPlatform(claudeClientPlatformOverride)
}
if claudeVersionPolicyOverride != "" {
claudeClientPolicy.VersionPolicy = auth.ClaudeVersionPolicy(claudeVersionPolicyOverride)
}
if claudeClientVersionOverride != "" {
claudeClientPolicy.ClientVersion = claudeClientVersionOverride
}
if normalized, err := auth.NormalizeClaudeClientPolicy(claudeClientPolicy); err == nil {
claudeClientPolicy = normalized
}
}
ignoreUsageLimitStatusOverride := row.GetCredentialOptionalBool("ignore_usage_limit_status_override")
ignoreUsageLimitStatusEffective := h.store.IgnoreUsageLimitStatus()
if ignoreUsageLimitStatusOverride != nil {
Expand All @@ -139,6 +170,7 @@ func (h *Handler) buildAccountResponse(
modelMapping := ""
var customHeaders map[string]string
var allowedAPIKeyIDs []int64
claudeUserAgent := ""
// 工作区 ID 不是密钥:Team/K12 徽章悬停要显示空间 ID。当前页
// ListActiveByIDs 已带完整凭据;custom_headers 只用来算生效空间,
// 摘要响应仍会剥掉原文。
Expand All @@ -148,66 +180,93 @@ func (h *Handler) buildAccountResponse(
effectiveWorkspaceID := openaiidentity.EffectiveWorkspaceID(tokenWorkspaceID, headers)
if includeDetails {
modelMapping = row.GetCredential("model_mapping")
customHeaders = headers
if isClaudeAccount {
// Claude detail responses may be consumed by admin tooling, but must
// never expose arbitrary historical custom headers such as
// Authorization/Cookie/x-api-key. Keep only the provider identity
// headers needed to inspect the stable fingerprint.
customHeaders = claudeExportFingerprintHeaders(headers)
for name, value := range customHeaders {
if strings.EqualFold(strings.TrimSpace(name), "user-agent") {
claudeUserAgent = strings.TrimSpace(value)
break
}
}
} else {
customHeaders = headers
}
allowedAPIKeyIDs = row.GetCredentialInt64Slice("allowed_api_key_ids")
}
resp := accountResponse{
DetailLoaded: includeDetails,
ID: row.ID,
Name: row.Name,
Email: email,
EmailDomain: accountEmailDomain(email),
ChatGPTAccountID: row.GetCredential("account_id"),
TokenWorkspaceID: tokenWorkspaceID,
WorkspaceIDOverride: workspaceIDOverride,
EffectiveWorkspaceID: effectiveWorkspaceID,
PlanType: planType,
SubscriptionExpiresAt: row.GetCredential("subscription_expires_at"),
Status: row.Status,
ErrorMessage: row.ErrorMessage,
ATOnly: !isOpenAIResponsesAccount && !isGrokAccount && !isAntigravityAccount && row.GetCredential("refresh_token") == "" && row.GetCredential("access_token") != "",
CreditEnabled: row.CreditEnabled,
CreditSkipUsageWindow: row.CreditSkipUsageWindow,
SkipWarmTier: row.SkipWarmTier,
AccountType: row.Type,
AccessTokenType: accountAccessTokenType(row),
OpenAIResponsesAPI: isOpenAIResponsesAccount,
GrokAPI: isGrokAccount,
AntigravityAPI: isAntigravityAccount,
AntigravityAuthKind: antigravityAuthKind,
AgentIdentity: isAgentIdentityCredentialRow(row),
GrokAuthKind: grokAuthKind,
GrokPlan: grokPlan,
GrokBilling: grokBilling,
AvatarURL: row.GetCredential("avatar_url"),
VerifiedEmail: row.GetCredentialBool("verified_email"),
ProjectID: row.GetCredential("project_id"),
AntigravityQuota: antigravityQuota,
AntigravityPermissions: antigravityPermissions,
AntigravitySyncWarning: row.GetCredential("antigravity_sync_warning"),
BaseURL: baseURL,
BalanceQueryURL: balanceQueryURL,
Models: row.GetCredentialStringSlice("models"),
ModelMapping: modelMapping,
CodexClientMetadataMode: codexClientMetadataMode,
CodexFingerprintMode: codexFingerprintMode,
CustomHeaders: customHeaders,
ProxyURL: row.ProxyURL,
Enabled: row.Enabled,
Locked: row.Locked,
AllowedAPIKeyIDs: allowedAPIKeyIDs,
Tags: append([]string(nil), row.Tags...),
Note: row.Note,
ScoreBiasOverride: nullableInt64Pointer(row.ScoreBiasOverride),
ScoreBiasEffective: effectiveScoreBias(planType, row.ScoreBiasOverride),
BaseConcurrencyOverride: nullableInt64Pointer(row.BaseConcurrencyOverride),
BaseConcurrencyEffective: effectiveBaseConcurrency(row.BaseConcurrencyOverride, int64(h.store.GetMaxConcurrency())),
CreatedAt: row.CreatedAt.Format(time.RFC3339),
UpdatedAt: row.UpdatedAt.Format(time.RFC3339),
CodexUsageUpdatedAt: row.GetCredential("codex_usage_updated_at"),
Codex5HUsageUpdatedAt: row.GetCredential("codex_5h_usage_updated_at"),
UsageLimitOverride: ignoreUsageLimitStatusOverride,
UsageLimitEffective: ignoreUsageLimitStatusEffective,
DetailLoaded: includeDetails,
ID: row.ID,
Name: row.Name,
Email: email,
EmailDomain: accountEmailDomain(email),
ChatGPTAccountID: row.GetCredential("account_id"),
TokenWorkspaceID: tokenWorkspaceID,
WorkspaceIDOverride: workspaceIDOverride,
EffectiveWorkspaceID: effectiveWorkspaceID,
PlanType: planType,
SubscriptionExpiresAt: row.GetCredential("subscription_expires_at"),
Status: row.Status,
ErrorMessage: row.ErrorMessage,
ATOnly: !isOpenAIResponsesAccount && !isGrokAccount && !isAntigravityAccount && !isClaudeAccount && row.GetCredential("refresh_token") == "" && row.GetCredential("access_token") != "",
CreditEnabled: row.CreditEnabled,
CreditSkipUsageWindow: row.CreditSkipUsageWindow,
SkipWarmTier: row.SkipWarmTier,
AccountType: row.Type,
AccessTokenType: accountAccessTokenType(row),
OpenAIResponsesAPI: isOpenAIResponsesAccount,
GrokAPI: isGrokAccount,
AntigravityAPI: isAntigravityAccount,
ClaudeAPI: isClaudeAccount,
AntigravityAuthKind: antigravityAuthKind,
AgentIdentity: isAgentIdentityCredentialRow(row),
GrokAuthKind: grokAuthKind,
GrokPlan: grokPlan,
GrokBilling: grokBilling,
AvatarURL: row.GetCredential("avatar_url"),
VerifiedEmail: row.GetCredentialBool("verified_email"),
ProjectID: row.GetCredential("project_id"),
AntigravityQuota: antigravityQuota,
AntigravityPermissions: antigravityPermissions,
AntigravitySyncWarning: row.GetCredential("antigravity_sync_warning"),
BaseURL: baseURL,
BalanceQueryURL: balanceQueryURL,
Models: row.GetCredentialStringSlice("models"),
ModelMapping: modelMapping,
CodexClientMetadataMode: codexClientMetadataMode,
CodexFingerprintMode: codexFingerprintMode,
ClaudeFingerprintMode: claudeFingerprintMode,
ClaudeUserAgent: claudeUserAgent,
ClaudeClientPlatform: string(claudeClientPolicy.Platform),
ClaudeVersionPolicy: string(claudeClientPolicy.VersionPolicy),
ClaudeClientVersion: claudeClientPolicy.ClientVersion,
ClaudeClientPlatformOverride: claudeClientPlatformOverride,
ClaudeVersionPolicyOverride: claudeVersionPolicyOverride,
ClaudeClientVersionOverride: claudeClientVersionOverride,
Timezone: accountTimezone,
CustomHeaders: customHeaders,
ProxyURL: row.ProxyURL,
Enabled: row.Enabled,
Locked: row.Locked,
AllowedAPIKeyIDs: allowedAPIKeyIDs,
Tags: append([]string(nil), row.Tags...),
Note: row.Note,
ScoreBiasOverride: nullableInt64Pointer(row.ScoreBiasOverride),
ScoreBiasEffective: effectiveScoreBias(planType, row.ScoreBiasOverride),
BaseConcurrencyOverride: nullableInt64Pointer(row.BaseConcurrencyOverride),
BaseConcurrencyEffective: effectiveBaseConcurrency(row.BaseConcurrencyOverride, int64(h.store.GetMaxConcurrency())),
CreatedAt: row.CreatedAt.Format(time.RFC3339),
UpdatedAt: row.UpdatedAt.Format(time.RFC3339),
CodexUsageUpdatedAt: row.GetCredential("codex_usage_updated_at"),
Codex5HUsageUpdatedAt: row.GetCredential("codex_5h_usage_updated_at"),
ClaudeUsageProbeAt: row.GetCredential(auth.ClaudeUsageProbeAtCredentialKey),
ClaudeUsageProbeError: row.GetCredential(auth.ClaudeUsageProbeErrorCredentialKey),
ClaudeUsageWindows: parseClaudeUsageWindows(row.GetCredential(auth.ClaudeUsageWindowsCredentialKey)),
UsageLimitOverride: ignoreUsageLimitStatusOverride,
UsageLimitEffective: ignoreUsageLimitStatusEffective,
}
if isAntigravityAccount {
resp.Models = antigravityPublishedModelsOrDefault(row.GetCredentialStringSlice("models"))
Expand Down Expand Up @@ -407,6 +466,18 @@ func (h *Handler) buildAccountResponse(
return resp
}

func parseClaudeUsageWindows(raw string) []auth.ClaudeUsageWindow {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil
}
var windows []auth.ClaudeUsageWindow
if err := json.Unmarshal([]byte(raw), &windows); err != nil {
return nil
}
return windows
}

func stripAccountDetailFields(resp *accountResponse) {
if resp == nil {
return
Expand Down
Loading