mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-11 03:13:24 +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
169 lines
4.7 KiB
Go
169 lines
4.7 KiB
Go
package http
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
func marshalJSONRaw(value any) (json.RawMessage, error) {
|
|
if value == nil {
|
|
return nil, nil
|
|
}
|
|
data, err := json.Marshal(value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return json.RawMessage(data), nil
|
|
}
|
|
|
|
func normalizedProviderNamesForValidation(names []string) []string {
|
|
if len(names) == 0 {
|
|
return nil
|
|
}
|
|
seen := make(map[string]bool, len(names))
|
|
result := make([]string, 0, len(names))
|
|
for _, name := range names {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" || seen[name] {
|
|
continue
|
|
}
|
|
seen[name] = true
|
|
result = append(result, name)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func validateChatGPTOAuthPoolGraph(providers []store.LLMProviderData) error {
|
|
providersByName := make(map[string]store.LLMProviderData, len(providers))
|
|
for _, provider := range providers {
|
|
providersByName[provider.Name] = provider
|
|
}
|
|
|
|
ownerByMember := map[string]string{}
|
|
hasPoolConfig := map[string]bool{}
|
|
|
|
for _, provider := range providers {
|
|
settings := store.ParseChatGPTOAuthProviderSettings(provider.Settings)
|
|
if settings == nil {
|
|
continue
|
|
}
|
|
if provider.ProviderType != store.ProviderChatGPTOAuth {
|
|
return fmt.Errorf("provider %q must be chatgpt_oauth to manage an OpenAI Codex pool", provider.Name)
|
|
}
|
|
|
|
hasPoolConfig[provider.Name] = true
|
|
for _, memberName := range normalizedProviderNamesForValidation(settings.CodexPool.ExtraProviderNames) {
|
|
if memberName == provider.Name {
|
|
return fmt.Errorf("provider %q cannot include itself in its OpenAI Codex pool", provider.Name)
|
|
}
|
|
|
|
member, ok := providersByName[memberName]
|
|
if !ok {
|
|
return fmt.Errorf("provider %q references unknown OpenAI Codex pool member %q", provider.Name, memberName)
|
|
}
|
|
if member.ProviderType != store.ProviderChatGPTOAuth {
|
|
return fmt.Errorf("provider %q can only add chatgpt_oauth members; %q is %s", provider.Name, memberName, member.ProviderType)
|
|
}
|
|
|
|
if existingOwner, ok := ownerByMember[memberName]; ok && existingOwner != provider.Name {
|
|
return fmt.Errorf("provider %q already belongs to pool %q", memberName, existingOwner)
|
|
}
|
|
ownerByMember[memberName] = provider.Name
|
|
}
|
|
}
|
|
|
|
for ownerName := range hasPoolConfig {
|
|
if existingOwner, ok := ownerByMember[ownerName]; ok {
|
|
return fmt.Errorf("provider %q already belongs to pool %q and cannot manage its own pool", ownerName, existingOwner)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateChatGPTOAuthProviderCandidate(
|
|
ctx context.Context,
|
|
providerStore store.ProviderStore,
|
|
currentID uuid.UUID,
|
|
candidate *store.LLMProviderData,
|
|
) error {
|
|
if providerStore == nil || candidate == nil {
|
|
return nil
|
|
}
|
|
|
|
existingProviders, err := providerStore.ListProviders(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
finalProviders := make([]store.LLMProviderData, 0, len(existingProviders)+1)
|
|
replaced := false
|
|
for _, provider := range existingProviders {
|
|
if currentID != uuid.Nil && provider.ID == currentID {
|
|
finalProviders = append(finalProviders, *candidate)
|
|
replaced = true
|
|
continue
|
|
}
|
|
finalProviders = append(finalProviders, provider)
|
|
}
|
|
if currentID == uuid.Nil || !replaced {
|
|
finalProviders = append(finalProviders, *candidate)
|
|
}
|
|
|
|
return validateChatGPTOAuthPoolGraph(finalProviders)
|
|
}
|
|
|
|
func validateChatGPTOAuthAgentRouting(
|
|
ctx context.Context,
|
|
providerStore store.ProviderStore,
|
|
providerName string,
|
|
routing *store.ChatGPTOAuthRoutingConfig,
|
|
) error {
|
|
if providerStore == nil || providerName == "" || routing == nil {
|
|
return nil
|
|
}
|
|
|
|
tenantID := store.TenantIDFromContext(ctx)
|
|
baseProvider, err := lookupProviderByNameWithMasterFallback(ctx, providerStore, tenantID, providerName)
|
|
if err != nil || baseProvider == nil || baseProvider.ProviderType != store.ProviderChatGPTOAuth {
|
|
return nil
|
|
}
|
|
|
|
defaultSettings := store.ParseChatGPTOAuthProviderSettings(baseProvider.Settings)
|
|
defaultMembers := []string{}
|
|
if defaultSettings != nil {
|
|
defaultMembers = normalizedProviderNamesForValidation(defaultSettings.CodexPool.ExtraProviderNames)
|
|
}
|
|
|
|
if len(defaultMembers) == 0 {
|
|
if routing.Strategy != store.ChatGPTOAuthStrategyPrimaryFirst || len(routing.ExtraProviderNames) > 0 {
|
|
return fmt.Errorf("configure OpenAI Codex pool members on provider %q before enabling agent-level routing", providerName)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if routing.OverrideMode == store.ChatGPTOAuthOverrideInherit {
|
|
return nil
|
|
}
|
|
|
|
if len(routing.ExtraProviderNames) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if !slices.Equal(
|
|
normalizedProviderNamesForValidation(routing.ExtraProviderNames),
|
|
defaultMembers,
|
|
) {
|
|
return fmt.Errorf("agent routing cannot change pool membership for provider %q; manage members on the provider instead", providerName)
|
|
}
|
|
|
|
return nil
|
|
}
|