Files
Kai (Tam Nhu) Tran 30708ae79d feat(providers): support Codex OAuth pools with inherited routing defaults
* feat(auth): support named chatgpt oauth providers

- add provider-scoped ChatGPT OAuth routes and CLI support

- persist refresh tokens per provider and reject provider-type collisions

- wire provider OAuth setup flows in the dashboard and setup UI

Refs #448

* feat(agent): add chatgpt oauth account routing

- add agent other_config routing for manual and round-robin selection

- reuse routed provider resolution across resolver and pending loaders

- add router, parser, and agent advanced dialog coverage for multi-account use

Refs #448

* docs(api): describe chatgpt oauth routing

- document named-provider ChatGPT OAuth auth routes

- describe agent-side account routing and round-robin behavior

- update OpenAPI agent config schema and provider type enum

Refs #448

* fix(store): add missing agent key context helpers

* feat(ui): clarify chatgpt oauth account setup and routing

* docs(providers): align chatgpt oauth alias examples

* feat(agent): add codex pool activity dashboard

* fix(providers): harden codex oauth alias setup

* feat(codex-pool): improve routing dashboard UX

- redesign the Codex/OpenAI pool page around saved-pool checkpoints and live evidence

- add clearer selection, attention, and recent-proof states for pool members

- make the lower panels fill the remaining desktop viewport while staying responsive

* fix(store): resolve context helper merge duplication

* feat(oauth): add codex pool quota and observation APIs

- add quota inspection and observation endpoints for ChatGPT Subscription (OAuth) providers

- teach codex routing to surface pool activity, observation metadata, and quota-aware readiness

- extend tests and HTTP docs/OpenAPI for the new pool monitoring flows

* feat(web): add codex pool quota monitor and controls

- add provider quota fetching, readiness badges, and live routing evidence on the account pool page

- redesign pool setup and activity panels for multi-account management with localized copy updates

- keep the live monitor internally scrollable and compact the account cards for better viewport fit

* fix(web): clarify pool routing labels

- rename the recent request badge from Direct to Selected

- restore compact quota bars in the live pool cards

* feat(codex-pool): add runtime health dashboard

- derive per-provider success and failure health from routed Codex traces

- surface routing, quota, and recent request evidence in the pool UI

- align provider alias guidance and owner access with the dashboard role model

* docs(auth): document tenant scoping and key roles

* fix(auth): harden tenant and codex pool access control

* fix(providers): align codex pool runtime defaults

* feat(ui): tighten codex pool responsive layout

* feat(chatgpt-oauth): refine codex pool management UX

* feat(chatgpt-oauth): surface quota bars on provider pages

- add compact quota bars to Codex provider rows and provider detail

- fetch quota only for ready visible provider rows and ready detail aliases

- fix managed-member detail visibility and tighten provider locale copy
2026-03-27 09:35:57 +07:00

127 lines
2.8 KiB
Go

package oauth
import (
"encoding/base64"
"encoding/json"
"strings"
"time"
)
type openAITokenMetadata struct {
AccountID string
PlanType string
}
type openAIJWTClaims struct {
Auth *openAIAuthClaims `json:"https://api.openai.com/auth"`
AccountID string `json:"https://api.openai.com/auth.chatgpt_account_id"`
PlanType string `json:"https://api.openai.com/auth.chatgpt_plan_type"`
}
type openAIAuthClaims struct {
AccountID string `json:"chatgpt_account_id"`
PlanType string `json:"chatgpt_plan_type"`
}
func parseOAuthSettings(raw json.RawMessage) OAuthSettings {
if len(raw) == 0 {
return OAuthSettings{}
}
var settings OAuthSettings
if err := json.Unmarshal(raw, &settings); err != nil {
return OAuthSettings{}
}
return settings
}
func mergeOAuthSettings(existing OAuthSettings, tokenResp *OpenAITokenResponse, expiresAt time.Time) OAuthSettings {
settings := existing
settings.ExpiresAt = expiresAt.Unix()
if scope := strings.TrimSpace(tokenResp.Scope); scope != "" {
settings.Scopes = scope
}
metadata := openAIMetadataFromTokenResponse(tokenResp)
if metadata.AccountID != "" {
settings.AccountID = metadata.AccountID
}
if metadata.PlanType != "" {
settings.PlanType = metadata.PlanType
}
return settings
}
func openAIMetadataFromTokenResponse(tokenResp *OpenAITokenResponse) openAITokenMetadata {
for _, token := range []string{tokenResp.IDToken, tokenResp.AccessToken} {
metadata, ok := parseOpenAIJWTMetadata(token)
if ok {
return metadata
}
}
return openAITokenMetadata{}
}
func parseOpenAIJWTMetadata(token string) (openAITokenMetadata, bool) {
token = strings.TrimSpace(token)
if token == "" {
return openAITokenMetadata{}, false
}
parts := strings.Split(token, ".")
if len(parts) < 2 {
return openAITokenMetadata{}, false
}
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return openAITokenMetadata{}, false
}
var claims openAIJWTClaims
if err := json.Unmarshal(payload, &claims); err != nil {
return openAITokenMetadata{}, false
}
metadata := openAITokenMetadata{
AccountID: firstNonEmpty(
strings.TrimSpace(claims.AccountID),
strings.TrimSpace(claims.NestedAccountID()),
),
PlanType: firstNonEmpty(
strings.TrimSpace(claims.PlanType),
strings.TrimSpace(claims.NestedPlanType()),
),
}
if metadata.AccountID == "" && metadata.PlanType == "" {
return openAITokenMetadata{}, false
}
return metadata, true
}
func (c openAIJWTClaims) NestedAccountID() string {
if c.Auth == nil {
return ""
}
return c.Auth.AccountID
}
func (c openAIJWTClaims) NestedPlanType() string {
if c.Auth == nil {
return ""
}
return c.Auth.PlanType
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if trimmed := strings.TrimSpace(value); trimmed != "" {
return trimmed
}
}
return ""
}