Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
f25ac3af
Commit
f25ac3af
authored
Dec 23, 2025
by
shaw
Browse files
feat: OpenAI OAuth账号显示Codex使用量
从响应头提取x-codex-*使用量信息并保存到账号Extra字段, 前端账号列表展示5h/7d窗口的使用进度条。
parent
f6341b7f
Changes
6
Hide whitespace changes
Inline
Side-by-side
backend/internal/repository/account_repo.go
View file @
f25ac3af
...
@@ -23,14 +23,18 @@ func (r *AccountRepository) Create(ctx context.Context, account *model.Account)
...
@@ -23,14 +23,18 @@ func (r *AccountRepository) Create(ctx context.Context, account *model.Account)
func
(
r
*
AccountRepository
)
GetByID
(
ctx
context
.
Context
,
id
int64
)
(
*
model
.
Account
,
error
)
{
func
(
r
*
AccountRepository
)
GetByID
(
ctx
context
.
Context
,
id
int64
)
(
*
model
.
Account
,
error
)
{
var
account
model
.
Account
var
account
model
.
Account
err
:=
r
.
db
.
WithContext
(
ctx
)
.
Preload
(
"Proxy"
)
.
Preload
(
"AccountGroups"
)
.
First
(
&
account
,
id
)
.
Error
err
:=
r
.
db
.
WithContext
(
ctx
)
.
Preload
(
"Proxy"
)
.
Preload
(
"AccountGroups
.Group
"
)
.
First
(
&
account
,
id
)
.
Error
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
err
return
nil
,
err
}
}
// 填充 GroupIDs 虚拟字段
// 填充 GroupIDs
和 Groups
虚拟字段
account
.
GroupIDs
=
make
([]
int64
,
0
,
len
(
account
.
AccountGroups
))
account
.
GroupIDs
=
make
([]
int64
,
0
,
len
(
account
.
AccountGroups
))
account
.
Groups
=
make
([]
*
model
.
Group
,
0
,
len
(
account
.
AccountGroups
))
for
_
,
ag
:=
range
account
.
AccountGroups
{
for
_
,
ag
:=
range
account
.
AccountGroups
{
account
.
GroupIDs
=
append
(
account
.
GroupIDs
,
ag
.
GroupID
)
account
.
GroupIDs
=
append
(
account
.
GroupIDs
,
ag
.
GroupID
)
if
ag
.
Group
!=
nil
{
account
.
Groups
=
append
(
account
.
Groups
,
ag
.
Group
)
}
}
}
return
&
account
,
nil
return
&
account
,
nil
}
}
...
@@ -303,3 +307,31 @@ func (r *AccountRepository) SetSchedulable(ctx context.Context, id int64, schedu
...
@@ -303,3 +307,31 @@ func (r *AccountRepository) SetSchedulable(ctx context.Context, id int64, schedu
return
r
.
db
.
WithContext
(
ctx
)
.
Model
(
&
model
.
Account
{})
.
Where
(
"id = ?"
,
id
)
.
return
r
.
db
.
WithContext
(
ctx
)
.
Model
(
&
model
.
Account
{})
.
Where
(
"id = ?"
,
id
)
.
Update
(
"schedulable"
,
schedulable
)
.
Error
Update
(
"schedulable"
,
schedulable
)
.
Error
}
}
// UpdateExtra updates specific fields in account's Extra JSONB field
// It merges the updates into existing Extra data without overwriting other fields
func
(
r
*
AccountRepository
)
UpdateExtra
(
ctx
context
.
Context
,
id
int64
,
updates
map
[
string
]
any
)
error
{
if
len
(
updates
)
==
0
{
return
nil
}
// Get current account to preserve existing Extra data
var
account
model
.
Account
if
err
:=
r
.
db
.
WithContext
(
ctx
)
.
Select
(
"extra"
)
.
Where
(
"id = ?"
,
id
)
.
First
(
&
account
)
.
Error
;
err
!=
nil
{
return
err
}
// Initialize Extra if nil
if
account
.
Extra
==
nil
{
account
.
Extra
=
make
(
model
.
JSONB
)
}
// Merge updates into existing Extra
for
k
,
v
:=
range
updates
{
account
.
Extra
[
k
]
=
v
}
// Save updated Extra
return
r
.
db
.
WithContext
(
ctx
)
.
Model
(
&
model
.
Account
{})
.
Where
(
"id = ?"
,
id
)
.
Update
(
"extra"
,
account
.
Extra
)
.
Error
}
backend/internal/service/openai_gateway_service.go
View file @
f25ac3af
...
@@ -11,6 +11,7 @@ import (
...
@@ -11,6 +11,7 @@ import (
"fmt"
"fmt"
"io"
"io"
"net/http"
"net/http"
"strconv"
"strings"
"strings"
"time"
"time"
...
@@ -38,6 +39,18 @@ var openaiAllowedHeaders = map[string]bool{
...
@@ -38,6 +39,18 @@ var openaiAllowedHeaders = map[string]bool{
"session_id"
:
true
,
"session_id"
:
true
,
}
}
// OpenAICodexUsageSnapshot represents Codex API usage limits from response headers
type
OpenAICodexUsageSnapshot
struct
{
PrimaryUsedPercent
*
float64
`json:"primary_used_percent,omitempty"`
PrimaryResetAfterSeconds
*
int
`json:"primary_reset_after_seconds,omitempty"`
PrimaryWindowMinutes
*
int
`json:"primary_window_minutes,omitempty"`
SecondaryUsedPercent
*
float64
`json:"secondary_used_percent,omitempty"`
SecondaryResetAfterSeconds
*
int
`json:"secondary_reset_after_seconds,omitempty"`
SecondaryWindowMinutes
*
int
`json:"secondary_window_minutes,omitempty"`
PrimaryOverSecondaryPercent
*
float64
`json:"primary_over_secondary_percent,omitempty"`
UpdatedAt
string
`json:"updated_at,omitempty"`
}
// OpenAIUsage represents OpenAI API response usage
// OpenAIUsage represents OpenAI API response usage
type
OpenAIUsage
struct
{
type
OpenAIUsage
struct
{
InputTokens
int
`json:"input_tokens"`
InputTokens
int
`json:"input_tokens"`
...
@@ -284,6 +297,13 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
...
@@ -284,6 +297,13 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
}
}
}
}
// Extract and save Codex usage snapshot from response headers (for OAuth accounts)
if
account
.
Type
==
model
.
AccountTypeOAuth
{
if
snapshot
:=
extractCodexUsageHeaders
(
resp
.
Header
);
snapshot
!=
nil
{
s
.
updateCodexUsageSnapshot
(
ctx
,
account
.
ID
,
snapshot
)
}
}
return
&
OpenAIForwardResult
{
return
&
OpenAIForwardResult
{
RequestID
:
resp
.
Header
.
Get
(
"x-request-id"
),
RequestID
:
resp
.
Header
.
Get
(
"x-request-id"
),
Usage
:
*
usage
,
Usage
:
*
usage
,
...
@@ -708,3 +728,109 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
...
@@ -708,3 +728,109 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
return
nil
return
nil
}
}
// extractCodexUsageHeaders extracts Codex usage limits from response headers
func
extractCodexUsageHeaders
(
headers
http
.
Header
)
*
OpenAICodexUsageSnapshot
{
snapshot
:=
&
OpenAICodexUsageSnapshot
{}
hasData
:=
false
// Helper to parse float64 from header
parseFloat
:=
func
(
key
string
)
*
float64
{
if
v
:=
headers
.
Get
(
key
);
v
!=
""
{
if
f
,
err
:=
strconv
.
ParseFloat
(
v
,
64
);
err
==
nil
{
return
&
f
}
}
return
nil
}
// Helper to parse int from header
parseInt
:=
func
(
key
string
)
*
int
{
if
v
:=
headers
.
Get
(
key
);
v
!=
""
{
if
i
,
err
:=
strconv
.
Atoi
(
v
);
err
==
nil
{
return
&
i
}
}
return
nil
}
// Primary (weekly) limits
if
v
:=
parseFloat
(
"x-codex-primary-used-percent"
);
v
!=
nil
{
snapshot
.
PrimaryUsedPercent
=
v
hasData
=
true
}
if
v
:=
parseInt
(
"x-codex-primary-reset-after-seconds"
);
v
!=
nil
{
snapshot
.
PrimaryResetAfterSeconds
=
v
hasData
=
true
}
if
v
:=
parseInt
(
"x-codex-primary-window-minutes"
);
v
!=
nil
{
snapshot
.
PrimaryWindowMinutes
=
v
hasData
=
true
}
// Secondary (5h) limits
if
v
:=
parseFloat
(
"x-codex-secondary-used-percent"
);
v
!=
nil
{
snapshot
.
SecondaryUsedPercent
=
v
hasData
=
true
}
if
v
:=
parseInt
(
"x-codex-secondary-reset-after-seconds"
);
v
!=
nil
{
snapshot
.
SecondaryResetAfterSeconds
=
v
hasData
=
true
}
if
v
:=
parseInt
(
"x-codex-secondary-window-minutes"
);
v
!=
nil
{
snapshot
.
SecondaryWindowMinutes
=
v
hasData
=
true
}
// Overflow ratio
if
v
:=
parseFloat
(
"x-codex-primary-over-secondary-limit-percent"
);
v
!=
nil
{
snapshot
.
PrimaryOverSecondaryPercent
=
v
hasData
=
true
}
if
!
hasData
{
return
nil
}
snapshot
.
UpdatedAt
=
time
.
Now
()
.
Format
(
time
.
RFC3339
)
return
snapshot
}
// updateCodexUsageSnapshot saves the Codex usage snapshot to account's Extra field
func
(
s
*
OpenAIGatewayService
)
updateCodexUsageSnapshot
(
ctx
context
.
Context
,
accountID
int64
,
snapshot
*
OpenAICodexUsageSnapshot
)
{
if
snapshot
==
nil
{
return
}
// Convert snapshot to map for merging into Extra
updates
:=
make
(
map
[
string
]
any
)
if
snapshot
.
PrimaryUsedPercent
!=
nil
{
updates
[
"codex_primary_used_percent"
]
=
*
snapshot
.
PrimaryUsedPercent
}
if
snapshot
.
PrimaryResetAfterSeconds
!=
nil
{
updates
[
"codex_primary_reset_after_seconds"
]
=
*
snapshot
.
PrimaryResetAfterSeconds
}
if
snapshot
.
PrimaryWindowMinutes
!=
nil
{
updates
[
"codex_primary_window_minutes"
]
=
*
snapshot
.
PrimaryWindowMinutes
}
if
snapshot
.
SecondaryUsedPercent
!=
nil
{
updates
[
"codex_secondary_used_percent"
]
=
*
snapshot
.
SecondaryUsedPercent
}
if
snapshot
.
SecondaryResetAfterSeconds
!=
nil
{
updates
[
"codex_secondary_reset_after_seconds"
]
=
*
snapshot
.
SecondaryResetAfterSeconds
}
if
snapshot
.
SecondaryWindowMinutes
!=
nil
{
updates
[
"codex_secondary_window_minutes"
]
=
*
snapshot
.
SecondaryWindowMinutes
}
if
snapshot
.
PrimaryOverSecondaryPercent
!=
nil
{
updates
[
"codex_primary_over_secondary_percent"
]
=
*
snapshot
.
PrimaryOverSecondaryPercent
}
updates
[
"codex_usage_updated_at"
]
=
snapshot
.
UpdatedAt
// Update account's Extra field asynchronously
go
func
()
{
updateCtx
,
cancel
:=
context
.
WithTimeout
(
context
.
Background
(),
5
*
time
.
Second
)
defer
cancel
()
_
=
s
.
accountRepo
.
UpdateExtra
(
updateCtx
,
accountID
,
updates
)
}()
}
backend/internal/service/ports/account.go
View file @
f25ac3af
...
@@ -34,4 +34,5 @@ type AccountRepository interface {
...
@@ -34,4 +34,5 @@ type AccountRepository interface {
SetOverloaded
(
ctx
context
.
Context
,
id
int64
,
until
time
.
Time
)
error
SetOverloaded
(
ctx
context
.
Context
,
id
int64
,
until
time
.
Time
)
error
ClearRateLimit
(
ctx
context
.
Context
,
id
int64
)
error
ClearRateLimit
(
ctx
context
.
Context
,
id
int64
)
error
UpdateSessionWindow
(
ctx
context
.
Context
,
id
int64
,
start
,
end
*
time
.
Time
,
status
string
)
error
UpdateSessionWindow
(
ctx
context
.
Context
,
id
int64
,
start
,
end
*
time
.
Time
,
status
string
)
error
UpdateExtra
(
ctx
context
.
Context
,
id
int64
,
updates
map
[
string
]
any
)
error
}
}
frontend/src/components/account/AccountUsageCell.vue
View file @
f25ac3af
...
@@ -68,7 +68,31 @@
...
@@ -68,7 +68,31 @@
<SetupTokenTimeWindow
:account=
"account"
/>
<SetupTokenTimeWindow
:account=
"account"
/>
</
template
>
</
template
>
<!-- OpenAI accounts: no usage window API, show dash -->
<!-- OpenAI OAuth accounts: show Codex usage from extra field -->
<
template
v-else-if=
"account.platform === 'openai' && account.type === 'oauth'"
>
<div
v-if=
"hasCodexUsage"
class=
"space-y-1"
>
<!-- 5h Window (Secondary) -->
<UsageProgressBar
v-if=
"codexSecondaryUsedPercent !== null"
label=
"5h"
:utilization=
"codexSecondaryUsedPercent"
:resets-at=
"codexSecondaryResetAt"
color=
"indigo"
/>
<!-- Weekly Window (Primary) -->
<UsageProgressBar
v-if=
"codexPrimaryUsedPercent !== null"
label=
"7d"
:utilization=
"codexPrimaryUsedPercent"
:resets-at=
"codexPrimaryResetAt"
color=
"emerald"
/>
</div>
<div
v-else
class=
"text-xs text-gray-400"
>
-
</div>
</
template
>
<!-- Other accounts: no usage window -->
<
template
v-else
>
<
template
v-else
>
<div
class=
"text-xs text-gray-400"
>
-
</div>
<div
class=
"text-xs text-gray-400"
>
-
</div>
</
template
>
</
template
>
...
@@ -100,9 +124,44 @@ const showUsageWindows = computed(() =>
...
@@ -100,9 +124,44 @@ const showUsageWindows = computed(() =>
props
.
account
.
type
===
'
oauth
'
||
props
.
account
.
type
===
'
setup-token
'
props
.
account
.
type
===
'
oauth
'
||
props
.
account
.
type
===
'
setup-token
'
)
)
// OpenAI Codex usage computed properties
const
hasCodexUsage
=
computed
(()
=>
{
const
extra
=
props
.
account
.
extra
return
extra
&&
(
extra
.
codex_primary_used_percent
!==
undefined
||
extra
.
codex_secondary_used_percent
!==
undefined
)
})
const
codexPrimaryUsedPercent
=
computed
(()
=>
{
const
extra
=
props
.
account
.
extra
if
(
!
extra
||
extra
.
codex_primary_used_percent
===
undefined
)
return
null
return
extra
.
codex_primary_used_percent
})
const
codexSecondaryUsedPercent
=
computed
(()
=>
{
const
extra
=
props
.
account
.
extra
if
(
!
extra
||
extra
.
codex_secondary_used_percent
===
undefined
)
return
null
return
extra
.
codex_secondary_used_percent
})
const
codexPrimaryResetAt
=
computed
(()
=>
{
const
extra
=
props
.
account
.
extra
if
(
!
extra
||
extra
.
codex_primary_reset_after_seconds
===
undefined
)
return
null
const
resetTime
=
new
Date
(
Date
.
now
()
+
extra
.
codex_primary_reset_after_seconds
*
1000
)
return
resetTime
.
toISOString
()
})
const
codexSecondaryResetAt
=
computed
(()
=>
{
const
extra
=
props
.
account
.
extra
if
(
!
extra
||
extra
.
codex_secondary_reset_after_seconds
===
undefined
)
return
null
const
resetTime
=
new
Date
(
Date
.
now
()
+
extra
.
codex_secondary_reset_after_seconds
*
1000
)
return
resetTime
.
toISOString
()
})
const
loadUsage
=
async
()
=>
{
const
loadUsage
=
async
()
=>
{
// Only fetch usage for Anthropic OAuth accounts
// Only fetch usage for Anthropic OAuth accounts
// OpenAI
doesn't have a usage window API - usage is updated from response headers
during forwarding
// OpenAI
usage comes from account.extra field (updated
during forwarding
)
if
(
props
.
account
.
platform
!==
'
anthropic
'
||
props
.
account
.
type
!==
'
oauth
'
)
return
if
(
props
.
account
.
platform
!==
'
anthropic
'
||
props
.
account
.
type
!==
'
oauth
'
)
return
loading
.
value
=
true
loading
.
value
=
true
...
...
frontend/src/components/common/SubscriptionProgressMini.vue
View file @
f25ac3af
...
@@ -29,7 +29,7 @@
...
@@ -29,7 +29,7 @@
<transition
name=
"dropdown"
>
<transition
name=
"dropdown"
>
<div
<div
v-if=
"tooltipOpen"
v-if=
"tooltipOpen"
class=
"absolute right-0 mt-2 w-
80
bg-white dark:bg-dark-800 rounded-xl shadow-xl border border-gray-200 dark:border-dark-700 z-50"
class=
"absolute right-0 mt-2 w-
[340px]
bg-white dark:bg-dark-800 rounded-xl shadow-xl border border-gray-200 dark:border-dark-700 z-50
overflow-hidden
"
>
>
<div
class=
"p-3 border-b border-gray-100 dark:border-dark-700"
>
<div
class=
"p-3 border-b border-gray-100 dark:border-dark-700"
>
<h3
class=
"text-sm font-semibold text-gray-900 dark:text-white"
>
<h3
class=
"text-sm font-semibold text-gray-900 dark:text-white"
>
...
@@ -62,43 +62,43 @@
...
@@ -62,43 +62,43 @@
<!--
Progress
bars
-->
<!--
Progress
bars
-->
<
div
class
=
"
space-y-1.5
"
>
<
div
class
=
"
space-y-1.5
"
>
<
div
v
-
if
=
"
subscription.group?.daily_limit_usd
"
class
=
"
flex items-center gap-2
"
>
<
div
v
-
if
=
"
subscription.group?.daily_limit_usd
"
class
=
"
flex items-center gap-2
"
>
<
span
class
=
"
text-[10px] text-gray-500 w-8
"
>
{{
t
(
'
subscriptionProgress.daily
'
)
}}
<
/span
>
<
span
class
=
"
text-[10px] text-gray-500 w-8
flex-shrink-0
"
>
{{
t
(
'
subscriptionProgress.daily
'
)
}}
<
/span
>
<
div
class
=
"
flex-1 bg-gray-200 dark:bg-dark-600 rounded-full h-1.5
"
>
<
div
class
=
"
flex-1
min-w-0
bg-gray-200 dark:bg-dark-600 rounded-full h-1.5
"
>
<
div
<
div
class
=
"
h-1.5 rounded-full transition-all
"
class
=
"
h-1.5 rounded-full transition-all
"
:
class
=
"
getProgressBarClass(subscription.daily_usage_usd, subscription.group?.daily_limit_usd)
"
:
class
=
"
getProgressBarClass(subscription.daily_usage_usd, subscription.group?.daily_limit_usd)
"
:
style
=
"
{ width: getProgressWidth(subscription.daily_usage_usd, subscription.group?.daily_limit_usd)
}
"
:
style
=
"
{ width: getProgressWidth(subscription.daily_usage_usd, subscription.group?.daily_limit_usd)
}
"
><
/div
>
><
/div
>
<
/div
>
<
/div
>
<
span
class
=
"
text-[10px] text-gray-500 w-
16
text-right
"
>
<
span
class
=
"
text-[10px] text-gray-500 w-
24
text-right
flex-shrink-0
"
>
{{
formatUsage
(
subscription
.
daily_usage_usd
,
subscription
.
group
?.
daily_limit_usd
)
}}
{{
formatUsage
(
subscription
.
daily_usage_usd
,
subscription
.
group
?.
daily_limit_usd
)
}}
<
/span
>
<
/span
>
<
/div
>
<
/div
>
<
div
v
-
if
=
"
subscription.group?.weekly_limit_usd
"
class
=
"
flex items-center gap-2
"
>
<
div
v
-
if
=
"
subscription.group?.weekly_limit_usd
"
class
=
"
flex items-center gap-2
"
>
<
span
class
=
"
text-[10px] text-gray-500 w-8
"
>
{{
t
(
'
subscriptionProgress.weekly
'
)
}}
<
/span
>
<
span
class
=
"
text-[10px] text-gray-500 w-8
flex-shrink-0
"
>
{{
t
(
'
subscriptionProgress.weekly
'
)
}}
<
/span
>
<
div
class
=
"
flex-1 bg-gray-200 dark:bg-dark-600 rounded-full h-1.5
"
>
<
div
class
=
"
flex-1
min-w-0
bg-gray-200 dark:bg-dark-600 rounded-full h-1.5
"
>
<
div
<
div
class
=
"
h-1.5 rounded-full transition-all
"
class
=
"
h-1.5 rounded-full transition-all
"
:
class
=
"
getProgressBarClass(subscription.weekly_usage_usd, subscription.group?.weekly_limit_usd)
"
:
class
=
"
getProgressBarClass(subscription.weekly_usage_usd, subscription.group?.weekly_limit_usd)
"
:
style
=
"
{ width: getProgressWidth(subscription.weekly_usage_usd, subscription.group?.weekly_limit_usd)
}
"
:
style
=
"
{ width: getProgressWidth(subscription.weekly_usage_usd, subscription.group?.weekly_limit_usd)
}
"
><
/div
>
><
/div
>
<
/div
>
<
/div
>
<
span
class
=
"
text-[10px] text-gray-500 w-
16
text-right
"
>
<
span
class
=
"
text-[10px] text-gray-500 w-
24
text-right
flex-shrink-0
"
>
{{
formatUsage
(
subscription
.
weekly_usage_usd
,
subscription
.
group
?.
weekly_limit_usd
)
}}
{{
formatUsage
(
subscription
.
weekly_usage_usd
,
subscription
.
group
?.
weekly_limit_usd
)
}}
<
/span
>
<
/span
>
<
/div
>
<
/div
>
<
div
v
-
if
=
"
subscription.group?.monthly_limit_usd
"
class
=
"
flex items-center gap-2
"
>
<
div
v
-
if
=
"
subscription.group?.monthly_limit_usd
"
class
=
"
flex items-center gap-2
"
>
<
span
class
=
"
text-[10px] text-gray-500 w-8
"
>
{{
t
(
'
subscriptionProgress.monthly
'
)
}}
<
/span
>
<
span
class
=
"
text-[10px] text-gray-500 w-8
flex-shrink-0
"
>
{{
t
(
'
subscriptionProgress.monthly
'
)
}}
<
/span
>
<
div
class
=
"
flex-1 bg-gray-200 dark:bg-dark-600 rounded-full h-1.5
"
>
<
div
class
=
"
flex-1
min-w-0
bg-gray-200 dark:bg-dark-600 rounded-full h-1.5
"
>
<
div
<
div
class
=
"
h-1.5 rounded-full transition-all
"
class
=
"
h-1.5 rounded-full transition-all
"
:
class
=
"
getProgressBarClass(subscription.monthly_usage_usd, subscription.group?.monthly_limit_usd)
"
:
class
=
"
getProgressBarClass(subscription.monthly_usage_usd, subscription.group?.monthly_limit_usd)
"
:
style
=
"
{ width: getProgressWidth(subscription.monthly_usage_usd, subscription.group?.monthly_limit_usd)
}
"
:
style
=
"
{ width: getProgressWidth(subscription.monthly_usage_usd, subscription.group?.monthly_limit_usd)
}
"
><
/div
>
><
/div
>
<
/div
>
<
/div
>
<
span
class
=
"
text-[10px] text-gray-500 w-
16
text-right
"
>
<
span
class
=
"
text-[10px] text-gray-500 w-
24
text-right
flex-shrink-0
"
>
{{
formatUsage
(
subscription
.
monthly_usage_usd
,
subscription
.
group
?.
monthly_limit_usd
)
}}
{{
formatUsage
(
subscription
.
monthly_usage_usd
,
subscription
.
group
?.
monthly_limit_usd
)
}}
<
/span
>
<
/span
>
<
/div
>
<
/div
>
...
...
frontend/src/types/index.ts
View file @
f25ac3af
...
@@ -316,6 +316,7 @@ export interface Account {
...
@@ -316,6 +316,7 @@ export interface Account {
platform
:
AccountPlatform
;
platform
:
AccountPlatform
;
type
:
AccountType
;
type
:
AccountType
;
credentials
?:
Record
<
string
,
unknown
>
;
credentials
?:
Record
<
string
,
unknown
>
;
extra
?:
CodexUsageSnapshot
&
Record
<
string
,
unknown
>
;
// Extra fields including Codex usage
proxy_id
:
number
|
null
;
proxy_id
:
number
|
null
;
concurrency
:
number
;
concurrency
:
number
;
priority
:
number
;
priority
:
number
;
...
@@ -361,6 +362,18 @@ export interface AccountUsageInfo {
...
@@ -361,6 +362,18 @@ export interface AccountUsageInfo {
seven_day_sonnet
:
UsageProgress
|
null
;
seven_day_sonnet
:
UsageProgress
|
null
;
}
}
// OpenAI Codex usage snapshot (from response headers)
export
interface
CodexUsageSnapshot
{
codex_primary_used_percent
?:
number
;
// Weekly limit usage percentage
codex_primary_reset_after_seconds
?:
number
;
// Seconds until weekly reset
codex_primary_window_minutes
?:
number
;
// Weekly window in minutes
codex_secondary_used_percent
?:
number
;
// 5h limit usage percentage
codex_secondary_reset_after_seconds
?:
number
;
// Seconds until 5h reset
codex_secondary_window_minutes
?:
number
;
// 5h window in minutes
codex_primary_over_secondary_percent
?:
number
;
// Overflow ratio
codex_usage_updated_at
?:
string
;
// Last update timestamp
}
export
interface
CreateAccountRequest
{
export
interface
CreateAccountRequest
{
name
:
string
;
name
:
string
;
platform
:
AccountPlatform
;
platform
:
AccountPlatform
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment