Files
goclaw/internal/providers/registry.go
T
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

165 lines
4.5 KiB
Go

package providers
import (
"context"
"fmt"
"io"
"log/slog"
"strings"
"sync"
"github.com/google/uuid"
)
// MasterTenantID is the default tenant for config-based providers.
var MasterTenantID = uuid.Must(uuid.Parse("0193a5b0-7000-7000-8000-000000000001"))
// Registry manages available LLM providers with per-tenant isolation.
// Providers are keyed by "tenantID/name". Get falls back to master tenant.
type Registry struct {
providers map[string]Provider
mu sync.RWMutex
tenantFromCtx func(context.Context) uuid.UUID // injected to avoid circular import with store
}
// NewRegistry creates a provider registry.
// tenantFromCtx extracts tenant UUID from context (pass store.TenantIDFromContext).
func NewRegistry(tenantFromCtx func(context.Context) uuid.UUID) *Registry {
return &Registry{
providers: make(map[string]Provider),
tenantFromCtx: tenantFromCtx,
}
}
// compoundKey returns "tenantID/name" for registry lookup.
func compoundKey(tenantID uuid.UUID, name string) string {
return tenantID.String() + "/" + name
}
// tenantFromContext resolves the tenant UUID from context, defaulting to MasterTenantID.
func (r *Registry) tenantFromContext(ctx context.Context) uuid.UUID {
if r.tenantFromCtx != nil {
if t := r.tenantFromCtx(ctx); t != uuid.Nil {
return t
}
}
return MasterTenantID
}
// Register adds a provider to the registry under the master tenant.
func (r *Registry) Register(provider Provider) {
r.RegisterForTenant(MasterTenantID, provider)
}
// RegisterForTenant adds a provider under a specific tenant.
// If a provider with the same tenant+name already exists, it is closed before replacement.
func (r *Registry) RegisterForTenant(tenantID uuid.UUID, provider Provider) {
r.mu.Lock()
defer r.mu.Unlock()
key := compoundKey(tenantID, provider.Name())
if old, ok := r.providers[key]; ok {
if c, ok := old.(io.Closer); ok {
c.Close()
}
}
r.providers[key] = provider
}
// Unregister removes a provider from the master tenant.
func (r *Registry) Unregister(name string) {
r.UnregisterForTenant(MasterTenantID, name)
}
// UnregisterForTenant removes a provider from a specific tenant.
func (r *Registry) UnregisterForTenant(tenantID uuid.UUID, name string) {
r.mu.Lock()
defer r.mu.Unlock()
key := compoundKey(tenantID, name)
if old, ok := r.providers[key]; ok {
if c, ok := old.(io.Closer); ok {
c.Close()
}
delete(r.providers, key)
}
}
// Get returns a provider by name, using tenant from context (falls back to master).
func (r *Registry) Get(ctx context.Context, name string) (Provider, error) {
return r.GetForTenant(r.tenantFromContext(ctx), name)
}
// GetForTenant returns a provider by name for a specific tenant.
// Falls back to master tenant if not found for the given tenant.
func (r *Registry) GetForTenant(tenantID uuid.UUID, name string) (Provider, error) {
r.mu.RLock()
defer r.mu.RUnlock()
// Try tenant-specific first
if tenantID != MasterTenantID {
if p, ok := r.providers[compoundKey(tenantID, name)]; ok {
return p, nil
}
}
// Fallback to master tenant
if p, ok := r.providers[compoundKey(MasterTenantID, name)]; ok {
return p, nil
}
return nil, fmt.Errorf("provider not found: %s", name)
}
// Close calls Close() on all providers that implement io.Closer.
func (r *Registry) Close() {
r.mu.Lock()
defer r.mu.Unlock()
for key, p := range r.providers {
if c, ok := p.(io.Closer); ok {
if err := c.Close(); err != nil {
slog.Warn("provider close error", "key", key, "error", err)
}
}
}
}
// List returns provider names visible to the tenant in context (tenant-specific + master defaults).
func (r *Registry) List(ctx context.Context) []string {
return r.ListForTenant(r.tenantFromContext(ctx))
}
// ListForTenant returns provider names available to a tenant (tenant-specific + master defaults).
func (r *Registry) ListForTenant(tenantID uuid.UUID) []string {
r.mu.RLock()
defer r.mu.RUnlock()
seen := make(map[string]bool)
var names []string
masterPrefix := MasterTenantID.String() + "/"
tenantPrefix := tenantID.String() + "/"
// Add tenant-specific providers first
if tenantID != MasterTenantID {
for key := range r.providers {
if after, ok := strings.CutPrefix(key, tenantPrefix); ok {
name := after
if !seen[name] {
seen[name] = true
names = append(names, name)
}
}
}
}
// Add master tenant providers (not already overridden)
for key := range r.providers {
if after, ok := strings.CutPrefix(key, masterPrefix); ok {
name := after
if !seen[name] {
seen[name] = true
names = append(names, name)
}
}
}
return names
}