fix(providers): post-merge fixes for Codex OAuth pools (#450)

- fix Rules of Hooks violation in chatgpt-oauth-routing-section
- add stale-while-revalidate with atomic dedup for RouteEligibility
- move raw SQL from HTTP handler to TracingStore.ListCodexPoolSpans
- persist round-robin state in Registry shared counter
- extract duplicated frontend helpers to agent-display-utils
- split oversized frontend files (964→214 lines max)
- add GIN indexes for spans.metadata and sessions.metadata
- fix tenant-aware provider lookup in handleQuota
- separate empty-role vs error handling in resolveTenantHint
- scope pool validation to chatgpt_oauth providers only
- wrap buildEntries in useCallback for stable useMemo deps
- document OAuth concurrent auth limitation and RoleAdmin breaking change
This commit is contained in:
viettranx committed 2026-03-27 10:19:16 +07:00
1 parent b9c1731e31
commit 74dc086a80
35 files changed
+1695 -1455

No files matched your search

+6 -54
View File
@@ -2,7 +2,6 @@ package http
import (
"context"
"encoding/json"
"errors"
"net/http"
"slices"
@@ -10,7 +9,6 @@ import (
"time"
"github.com/google/uuid"
"github.com/lib/pq"
"github.com/nextlevelbuilder/goclaw/internal/i18n"
"github.com/nextlevelbuilder/goclaw/internal/providers"
@@ -183,8 +181,8 @@ func agentRoutingExtraNames(routing *store.ChatGPTOAuthRoutingConfig) []string {
func (h *AgentsHandler) handleCodexPoolActivity(w http.ResponseWriter, r *http.Request) {
locale := store.LocaleFromContext(r.Context())
if h.db == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidRequest, "database unavailable")})
if h.tracingStore == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidRequest, "tracing store unavailable")})
return
}
@@ -222,57 +220,15 @@ func (h *AgentsHandler) handleCodexPoolActivity(w http.ResponseWriter, r *http.R
return
}
const query = `
SELECT
sp.id,
sp.trace_id,
sp.start_time,
COALESCE(sp.duration_ms, 0),
sp.status,
COALESCE(sp.provider, ''),
COALESCE(sp.model, ''),
COALESCE(sp.metadata, '{}'::jsonb)
FROM spans sp
JOIN traces t ON t.id = sp.trace_id
WHERE t.agent_id = $1
AND t.tenant_id = $2
AND t.parent_trace_id IS NULL
AND sp.tenant_id = $2
AND sp.span_type = 'llm_call'
AND (
sp.provider = ANY($3)
OR COALESCE(sp.metadata->'chatgpt_oauth_routing'->>'selected_provider', '') = ANY($3)
OR COALESCE(sp.metadata->'chatgpt_oauth_routing'->>'serving_provider', '') = ANY($3)
)
ORDER BY sp.start_time DESC
LIMIT $4`
rows, err := h.db.QueryContext(r.Context(), query, agent.ID, agent.TenantID, pq.Array(poolProviders), statsLimit)
rawSpans, err := h.tracingStore.ListCodexPoolSpans(r.Context(), agent.ID, agent.TenantID, poolProviders, statsLimit)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
defer rows.Close()
spans := make([]codexPoolSpanActivity, 0, statsLimit)
for rows.Next() {
var item codexPoolSpanActivity
var metadata json.RawMessage
if err := rows.Scan(
&item.SpanID,
&item.TraceID,
&item.StartedAt,
&item.DurationMS,
&item.Status,
&item.Provider,
&item.Model,
&metadata,
); err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
item.Metadata = metadata
if evidence := providers.ExtractChatGPTOAuthRoutingEvidence(metadata); evidence.HasData() {
spans := make([]store.CodexPoolSpan, 0, len(rawSpans))
for _, item := range rawSpans {
if evidence := providers.ExtractChatGPTOAuthRoutingEvidence(item.Metadata); evidence.HasData() {
if !providerInPool(poolProviders, evidence.SelectedProvider) && !providerInPool(poolProviders, evidence.ServingProvider) {
continue
}
@@ -281,10 +237,6 @@ LIMIT $4`
}
spans = append(spans, item)
}
if err := rows.Err(); err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
providerCounts, recent := buildCodexPoolActivity(poolProviders, spans)
if len(recent) > limit {