feat: OpenAI OAuth账号显示Codex使用量
从响应头提取x-codex-*使用量信息并保存到账号Extra字段, 前端账号列表展示5h/7d窗口的使用进度条。
This commit is contained in:
@@ -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
|
|||||||
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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{
|
|||||||
"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
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
|||||||
|
|
||||||
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)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(() =>
|
|||||||
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
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
<!-- 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>
|
||||||
|
|||||||
@@ -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 {
|
|||||||
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;
|
||||||
|
|||||||
Reference in New Issue
Block a user