mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-18 08:14:39 +00:00
* 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
149 lines
4.6 KiB
Go
149 lines
4.6 KiB
Go
package providers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"slices"
|
|
"sync"
|
|
)
|
|
|
|
const ChatGPTOAuthRoutingMetadataKey = "chatgpt_oauth_routing"
|
|
|
|
type chatGPTOAuthRoutingObservationKey struct{}
|
|
|
|
type ChatGPTOAuthRoutingEvidence struct {
|
|
PoolOwnerProvider string `json:"pool_owner_provider,omitempty"`
|
|
Strategy string `json:"strategy,omitempty"`
|
|
PoolProviders []string `json:"pool_providers,omitempty"`
|
|
SelectedProvider string `json:"selected_provider,omitempty"`
|
|
ServingProvider string `json:"serving_provider,omitempty"`
|
|
AttemptedProviders []string `json:"attempted_providers,omitempty"`
|
|
FailoverProviders []string `json:"failover_providers,omitempty"`
|
|
AttemptCount int `json:"attempt_count,omitempty"`
|
|
}
|
|
|
|
func (e ChatGPTOAuthRoutingEvidence) HasData() bool {
|
|
return e.SelectedProvider != "" || e.ServingProvider != "" || e.AttemptCount > 0 || len(e.PoolProviders) > 0
|
|
}
|
|
|
|
type ChatGPTOAuthRoutingObservation struct {
|
|
mu sync.Mutex
|
|
evidence ChatGPTOAuthRoutingEvidence
|
|
}
|
|
|
|
func NewChatGPTOAuthRoutingObservation() *ChatGPTOAuthRoutingObservation {
|
|
return &ChatGPTOAuthRoutingObservation{}
|
|
}
|
|
|
|
func WithChatGPTOAuthRoutingObservation(ctx context.Context, observation *ChatGPTOAuthRoutingObservation) context.Context {
|
|
return context.WithValue(ctx, chatGPTOAuthRoutingObservationKey{}, observation)
|
|
}
|
|
|
|
func ChatGPTOAuthRoutingObservationFromContext(ctx context.Context) *ChatGPTOAuthRoutingObservation {
|
|
observation, _ := ctx.Value(chatGPTOAuthRoutingObservationKey{}).(*ChatGPTOAuthRoutingObservation)
|
|
return observation
|
|
}
|
|
|
|
func (o *ChatGPTOAuthRoutingObservation) SetPool(poolOwnerProvider, strategy string, poolProviders []string) {
|
|
if o == nil {
|
|
return
|
|
}
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
o.evidence.PoolOwnerProvider = poolOwnerProvider
|
|
o.evidence.Strategy = strategy
|
|
o.evidence.PoolProviders = append([]string(nil), poolProviders...)
|
|
}
|
|
|
|
func (o *ChatGPTOAuthRoutingObservation) RecordAttempt(providerName string) {
|
|
if o == nil || providerName == "" {
|
|
return
|
|
}
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
o.evidence.AttemptCount++
|
|
if o.evidence.SelectedProvider == "" {
|
|
o.evidence.SelectedProvider = providerName
|
|
}
|
|
if !slices.Contains(o.evidence.AttemptedProviders, providerName) {
|
|
o.evidence.AttemptedProviders = append(o.evidence.AttemptedProviders, providerName)
|
|
}
|
|
if o.evidence.SelectedProvider != providerName && !slices.Contains(o.evidence.FailoverProviders, providerName) {
|
|
o.evidence.FailoverProviders = append(o.evidence.FailoverProviders, providerName)
|
|
}
|
|
}
|
|
|
|
func (o *ChatGPTOAuthRoutingObservation) RecordSuccess(providerName string) {
|
|
if o == nil || providerName == "" {
|
|
return
|
|
}
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
o.evidence.ServingProvider = providerName
|
|
}
|
|
|
|
func (o *ChatGPTOAuthRoutingObservation) Snapshot() ChatGPTOAuthRoutingEvidence {
|
|
if o == nil {
|
|
return ChatGPTOAuthRoutingEvidence{}
|
|
}
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
evidence := o.evidence
|
|
evidence.PoolProviders = append([]string(nil), o.evidence.PoolProviders...)
|
|
evidence.AttemptedProviders = append([]string(nil), o.evidence.AttemptedProviders...)
|
|
evidence.FailoverProviders = append([]string(nil), o.evidence.FailoverProviders...)
|
|
return evidence
|
|
}
|
|
|
|
func MergeChatGPTOAuthRoutingMetadata(existing json.RawMessage, evidence ChatGPTOAuthRoutingEvidence) json.RawMessage {
|
|
if !evidence.HasData() {
|
|
return existing
|
|
}
|
|
payload := map[string]any{}
|
|
if len(existing) > 0 {
|
|
_ = json.Unmarshal(existing, &payload)
|
|
}
|
|
payload[ChatGPTOAuthRoutingMetadataKey] = evidence
|
|
data, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return existing
|
|
}
|
|
return json.RawMessage(data)
|
|
}
|
|
|
|
func ExtractChatGPTOAuthRoutingEvidence(raw json.RawMessage) ChatGPTOAuthRoutingEvidence {
|
|
if len(raw) == 0 {
|
|
return ChatGPTOAuthRoutingEvidence{}
|
|
}
|
|
var payload map[string]json.RawMessage
|
|
if err := json.Unmarshal(raw, &payload); err != nil {
|
|
return ChatGPTOAuthRoutingEvidence{}
|
|
}
|
|
section := payload[ChatGPTOAuthRoutingMetadataKey]
|
|
if len(section) == 0 {
|
|
return ChatGPTOAuthRoutingEvidence{}
|
|
}
|
|
var evidence ChatGPTOAuthRoutingEvidence
|
|
if err := json.Unmarshal(section, &evidence); err != nil {
|
|
return ChatGPTOAuthRoutingEvidence{}
|
|
}
|
|
evidence.PoolProviders = uniqueStrings(evidence.PoolProviders)
|
|
evidence.AttemptedProviders = uniqueStrings(evidence.AttemptedProviders)
|
|
evidence.FailoverProviders = uniqueStrings(evidence.FailoverProviders)
|
|
return evidence
|
|
}
|
|
|
|
func uniqueStrings(values []string) []string {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
result := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
if value == "" || slices.Contains(result, value) {
|
|
continue
|
|
}
|
|
result = append(result, value)
|
|
}
|
|
return result
|
|
}
|