mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-20 05:10:52 +08:00
feat(logging, executor): add request logging tests and WebSocket-based Codex executor
- Introduced unit tests for request logging middleware to enhance coverage. - Added WebSocket-based Codex executor to support Responses API upgrade. - Updated middleware logic to selectively capture request bodies for memory efficiency. - Enhanced Codex configuration handling with new WebSocket attributes.
This commit is contained in:
@@ -41,6 +41,17 @@ type ProviderExecutor interface {
|
||||
HttpRequest(ctx context.Context, auth *Auth, req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
// ExecutionSessionCloser allows executors to release per-session runtime resources.
|
||||
type ExecutionSessionCloser interface {
|
||||
CloseExecutionSession(sessionID string)
|
||||
}
|
||||
|
||||
const (
|
||||
// CloseAllExecutionSessionsID asks an executor to release all active execution sessions.
|
||||
// Executors that do not support this marker may ignore it.
|
||||
CloseAllExecutionSessionsID = "__all_execution_sessions__"
|
||||
)
|
||||
|
||||
// RefreshEvaluator allows runtime state to override refresh decisions.
|
||||
type RefreshEvaluator interface {
|
||||
ShouldRefresh(now time.Time, auth *Auth) bool
|
||||
@@ -389,9 +400,23 @@ func (m *Manager) RegisterExecutor(executor ProviderExecutor) {
|
||||
if executor == nil {
|
||||
return
|
||||
}
|
||||
provider := strings.TrimSpace(executor.Identifier())
|
||||
if provider == "" {
|
||||
return
|
||||
}
|
||||
|
||||
var replaced ProviderExecutor
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.executors[executor.Identifier()] = executor
|
||||
replaced = m.executors[provider]
|
||||
m.executors[provider] = executor
|
||||
m.mu.Unlock()
|
||||
|
||||
if replaced == nil || replaced == executor {
|
||||
return
|
||||
}
|
||||
if closer, ok := replaced.(ExecutionSessionCloser); ok && closer != nil {
|
||||
closer.CloseExecutionSession(CloseAllExecutionSessionsID)
|
||||
}
|
||||
}
|
||||
|
||||
// UnregisterExecutor removes the executor associated with the provider key.
|
||||
@@ -581,6 +606,7 @@ func (m *Manager) executeMixedOnce(ctx context.Context, providers []string, req
|
||||
|
||||
entry := logEntryWithRequestID(ctx)
|
||||
debugLogAuthSelection(entry, auth, provider, req.Model)
|
||||
publishSelectedAuthMetadata(opts.Metadata, auth.ID)
|
||||
|
||||
tried[auth.ID] = struct{}{}
|
||||
execCtx := ctx
|
||||
@@ -636,6 +662,7 @@ func (m *Manager) executeCountMixedOnce(ctx context.Context, providers []string,
|
||||
|
||||
entry := logEntryWithRequestID(ctx)
|
||||
debugLogAuthSelection(entry, auth, provider, req.Model)
|
||||
publishSelectedAuthMetadata(opts.Metadata, auth.ID)
|
||||
|
||||
tried[auth.ID] = struct{}{}
|
||||
execCtx := ctx
|
||||
@@ -691,6 +718,7 @@ func (m *Manager) executeStreamMixedOnce(ctx context.Context, providers []string
|
||||
|
||||
entry := logEntryWithRequestID(ctx)
|
||||
debugLogAuthSelection(entry, auth, provider, req.Model)
|
||||
publishSelectedAuthMetadata(opts.Metadata, auth.ID)
|
||||
|
||||
tried[auth.ID] = struct{}{}
|
||||
execCtx := ctx
|
||||
@@ -794,6 +822,38 @@ func hasRequestedModelMetadata(meta map[string]any) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func pinnedAuthIDFromMetadata(meta map[string]any) string {
|
||||
if len(meta) == 0 {
|
||||
return ""
|
||||
}
|
||||
raw, ok := meta[cliproxyexecutor.PinnedAuthMetadataKey]
|
||||
if !ok || raw == nil {
|
||||
return ""
|
||||
}
|
||||
switch val := raw.(type) {
|
||||
case string:
|
||||
return strings.TrimSpace(val)
|
||||
case []byte:
|
||||
return strings.TrimSpace(string(val))
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func publishSelectedAuthMetadata(meta map[string]any, authID string) {
|
||||
if len(meta) == 0 {
|
||||
return
|
||||
}
|
||||
authID = strings.TrimSpace(authID)
|
||||
if authID == "" {
|
||||
return
|
||||
}
|
||||
meta[cliproxyexecutor.SelectedAuthMetadataKey] = authID
|
||||
if callback, ok := meta[cliproxyexecutor.SelectedAuthCallbackMetadataKey].(func(string)); ok && callback != nil {
|
||||
callback(authID)
|
||||
}
|
||||
}
|
||||
|
||||
func rewriteModelForAuth(model string, auth *Auth) string {
|
||||
if auth == nil || model == "" {
|
||||
return model
|
||||
@@ -1550,7 +1610,56 @@ func (m *Manager) GetByID(id string) (*Auth, bool) {
|
||||
return auth.Clone(), true
|
||||
}
|
||||
|
||||
// Executor returns the registered provider executor for a provider key.
|
||||
func (m *Manager) Executor(provider string) (ProviderExecutor, bool) {
|
||||
if m == nil {
|
||||
return nil, false
|
||||
}
|
||||
provider = strings.TrimSpace(provider)
|
||||
if provider == "" {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
m.mu.RLock()
|
||||
executor, okExecutor := m.executors[provider]
|
||||
if !okExecutor {
|
||||
lowerProvider := strings.ToLower(provider)
|
||||
if lowerProvider != provider {
|
||||
executor, okExecutor = m.executors[lowerProvider]
|
||||
}
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
if !okExecutor || executor == nil {
|
||||
return nil, false
|
||||
}
|
||||
return executor, true
|
||||
}
|
||||
|
||||
// CloseExecutionSession asks all registered executors to release the supplied execution session.
|
||||
func (m *Manager) CloseExecutionSession(sessionID string) {
|
||||
sessionID = strings.TrimSpace(sessionID)
|
||||
if m == nil || sessionID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
m.mu.RLock()
|
||||
executors := make([]ProviderExecutor, 0, len(m.executors))
|
||||
for _, exec := range m.executors {
|
||||
executors = append(executors, exec)
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
for i := range executors {
|
||||
if closer, ok := executors[i].(ExecutionSessionCloser); ok && closer != nil {
|
||||
closer.CloseExecutionSession(sessionID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) pickNext(ctx context.Context, provider, model string, opts cliproxyexecutor.Options, tried map[string]struct{}) (*Auth, ProviderExecutor, error) {
|
||||
pinnedAuthID := pinnedAuthIDFromMetadata(opts.Metadata)
|
||||
|
||||
m.mu.RLock()
|
||||
executor, okExecutor := m.executors[provider]
|
||||
if !okExecutor {
|
||||
@@ -1571,6 +1680,9 @@ func (m *Manager) pickNext(ctx context.Context, provider, model string, opts cli
|
||||
if candidate.Provider != provider || candidate.Disabled {
|
||||
continue
|
||||
}
|
||||
if pinnedAuthID != "" && candidate.ID != pinnedAuthID {
|
||||
continue
|
||||
}
|
||||
if _, used := tried[candidate.ID]; used {
|
||||
continue
|
||||
}
|
||||
@@ -1606,6 +1718,8 @@ func (m *Manager) pickNext(ctx context.Context, provider, model string, opts cli
|
||||
}
|
||||
|
||||
func (m *Manager) pickNextMixed(ctx context.Context, providers []string, model string, opts cliproxyexecutor.Options, tried map[string]struct{}) (*Auth, ProviderExecutor, string, error) {
|
||||
pinnedAuthID := pinnedAuthIDFromMetadata(opts.Metadata)
|
||||
|
||||
providerSet := make(map[string]struct{}, len(providers))
|
||||
for _, provider := range providers {
|
||||
p := strings.TrimSpace(strings.ToLower(provider))
|
||||
@@ -1633,6 +1747,9 @@ func (m *Manager) pickNextMixed(ctx context.Context, providers []string, model s
|
||||
if candidate == nil || candidate.Disabled {
|
||||
continue
|
||||
}
|
||||
if pinnedAuthID != "" && candidate.ID != pinnedAuthID {
|
||||
continue
|
||||
}
|
||||
providerKey := strings.TrimSpace(strings.ToLower(candidate.Provider))
|
||||
if providerKey == "" {
|
||||
continue
|
||||
|
||||
100
sdk/cliproxy/auth/conductor_executor_replace_test.go
Normal file
100
sdk/cliproxy/auth/conductor_executor_replace_test.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
||||
)
|
||||
|
||||
type replaceAwareExecutor struct {
|
||||
id string
|
||||
|
||||
mu sync.Mutex
|
||||
closedSessionIDs []string
|
||||
}
|
||||
|
||||
func (e *replaceAwareExecutor) Identifier() string {
|
||||
return e.id
|
||||
}
|
||||
|
||||
func (e *replaceAwareExecutor) Execute(context.Context, *Auth, cliproxyexecutor.Request, cliproxyexecutor.Options) (cliproxyexecutor.Response, error) {
|
||||
return cliproxyexecutor.Response{}, nil
|
||||
}
|
||||
|
||||
func (e *replaceAwareExecutor) ExecuteStream(context.Context, *Auth, cliproxyexecutor.Request, cliproxyexecutor.Options) (<-chan cliproxyexecutor.StreamChunk, error) {
|
||||
ch := make(chan cliproxyexecutor.StreamChunk)
|
||||
close(ch)
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func (e *replaceAwareExecutor) Refresh(_ context.Context, auth *Auth) (*Auth, error) {
|
||||
return auth, nil
|
||||
}
|
||||
|
||||
func (e *replaceAwareExecutor) CountTokens(context.Context, *Auth, cliproxyexecutor.Request, cliproxyexecutor.Options) (cliproxyexecutor.Response, error) {
|
||||
return cliproxyexecutor.Response{}, nil
|
||||
}
|
||||
|
||||
func (e *replaceAwareExecutor) HttpRequest(context.Context, *Auth, *http.Request) (*http.Response, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (e *replaceAwareExecutor) CloseExecutionSession(sessionID string) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
e.closedSessionIDs = append(e.closedSessionIDs, sessionID)
|
||||
}
|
||||
|
||||
func (e *replaceAwareExecutor) ClosedSessionIDs() []string {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
out := make([]string, len(e.closedSessionIDs))
|
||||
copy(out, e.closedSessionIDs)
|
||||
return out
|
||||
}
|
||||
|
||||
func TestManagerRegisterExecutorClosesReplacedExecutionSessions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
manager := NewManager(nil, nil, nil)
|
||||
replaced := &replaceAwareExecutor{id: "codex"}
|
||||
current := &replaceAwareExecutor{id: "codex"}
|
||||
|
||||
manager.RegisterExecutor(replaced)
|
||||
manager.RegisterExecutor(current)
|
||||
|
||||
closed := replaced.ClosedSessionIDs()
|
||||
if len(closed) != 1 {
|
||||
t.Fatalf("expected replaced executor close calls = 1, got %d", len(closed))
|
||||
}
|
||||
if closed[0] != CloseAllExecutionSessionsID {
|
||||
t.Fatalf("expected close marker %q, got %q", CloseAllExecutionSessionsID, closed[0])
|
||||
}
|
||||
if len(current.ClosedSessionIDs()) != 0 {
|
||||
t.Fatalf("expected current executor to stay open")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerExecutorReturnsRegisteredExecutor(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
manager := NewManager(nil, nil, nil)
|
||||
current := &replaceAwareExecutor{id: "codex"}
|
||||
manager.RegisterExecutor(current)
|
||||
|
||||
resolved, okResolved := manager.Executor("CODEX")
|
||||
if !okResolved {
|
||||
t.Fatal("expected registered executor to be found")
|
||||
}
|
||||
if resolved != current {
|
||||
t.Fatal("expected resolved executor to match registered executor")
|
||||
}
|
||||
|
||||
_, okMissing := manager.Executor("unknown")
|
||||
if okMissing {
|
||||
t.Fatal("expected unknown provider lookup to fail")
|
||||
}
|
||||
}
|
||||
@@ -134,6 +134,62 @@ func canonicalModelKey(model string) string {
|
||||
return modelName
|
||||
}
|
||||
|
||||
func authWebsocketsEnabled(auth *Auth) bool {
|
||||
if auth == nil {
|
||||
return false
|
||||
}
|
||||
if len(auth.Attributes) > 0 {
|
||||
if raw := strings.TrimSpace(auth.Attributes["websockets"]); raw != "" {
|
||||
parsed, errParse := strconv.ParseBool(raw)
|
||||
if errParse == nil {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(auth.Metadata) == 0 {
|
||||
return false
|
||||
}
|
||||
raw, ok := auth.Metadata["websockets"]
|
||||
if !ok || raw == nil {
|
||||
return false
|
||||
}
|
||||
switch v := raw.(type) {
|
||||
case bool:
|
||||
return v
|
||||
case string:
|
||||
parsed, errParse := strconv.ParseBool(strings.TrimSpace(v))
|
||||
if errParse == nil {
|
||||
return parsed
|
||||
}
|
||||
default:
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func preferCodexWebsocketAuths(ctx context.Context, provider string, available []*Auth) []*Auth {
|
||||
if len(available) == 0 {
|
||||
return available
|
||||
}
|
||||
if !cliproxyexecutor.DownstreamWebsocket(ctx) {
|
||||
return available
|
||||
}
|
||||
if !strings.EqualFold(strings.TrimSpace(provider), "codex") {
|
||||
return available
|
||||
}
|
||||
|
||||
wsEnabled := make([]*Auth, 0, len(available))
|
||||
for i := 0; i < len(available); i++ {
|
||||
candidate := available[i]
|
||||
if authWebsocketsEnabled(candidate) {
|
||||
wsEnabled = append(wsEnabled, candidate)
|
||||
}
|
||||
}
|
||||
if len(wsEnabled) > 0 {
|
||||
return wsEnabled
|
||||
}
|
||||
return available
|
||||
}
|
||||
|
||||
func collectAvailableByPriority(auths []*Auth, model string, now time.Time) (available map[int][]*Auth, cooldownCount int, earliest time.Time) {
|
||||
available = make(map[int][]*Auth)
|
||||
for i := 0; i < len(auths); i++ {
|
||||
@@ -193,13 +249,13 @@ func getAvailableAuths(auths []*Auth, provider, model string, now time.Time) ([]
|
||||
|
||||
// Pick selects the next available auth for the provider in a round-robin manner.
|
||||
func (s *RoundRobinSelector) Pick(ctx context.Context, provider, model string, opts cliproxyexecutor.Options, auths []*Auth) (*Auth, error) {
|
||||
_ = ctx
|
||||
_ = opts
|
||||
now := time.Now()
|
||||
available, err := getAvailableAuths(auths, provider, model, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
available = preferCodexWebsocketAuths(ctx, provider, available)
|
||||
key := provider + ":" + canonicalModelKey(model)
|
||||
s.mu.Lock()
|
||||
if s.cursors == nil {
|
||||
@@ -226,13 +282,13 @@ func (s *RoundRobinSelector) Pick(ctx context.Context, provider, model string, o
|
||||
|
||||
// Pick selects the first available auth for the provider in a deterministic manner.
|
||||
func (s *FillFirstSelector) Pick(ctx context.Context, provider, model string, opts cliproxyexecutor.Options, auths []*Auth) (*Auth, error) {
|
||||
_ = ctx
|
||||
_ = opts
|
||||
now := time.Now()
|
||||
available, err := getAvailableAuths(auths, provider, model, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
available = preferCodexWebsocketAuths(ctx, provider, available)
|
||||
return available[0], nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user