vibe-proxy/backend/sdk/cliproxy/auth/conductor_execution.go
2026-08-24 00:10:41 +02:00

1703 lines
54 KiB
Go

package auth
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
cliproxysession "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/session"
coreusage "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/usage"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
log "github.com/sirupsen/logrus"
)
func claudeOAuthRequestCancellation(ctx context.Context, auth *Auth, err error) error {
if auth == nil || !strings.EqualFold(strings.TrimSpace(auth.Provider), "claude") || !strings.EqualFold(strings.TrimSpace(auth.Attributes["auth_kind"]), "oauth") {
return nil
}
if ctx != nil && errors.Is(ctx.Err(), context.Canceled) {
return ctx.Err()
}
if errors.Is(err, context.Canceled) {
return err
}
return nil
}
// Execute performs a non-streaming execution using the configured selector and executor.
// It supports multiple providers for the same model and round-robins the starting provider per model.
func (m *Manager) Execute(ctx context.Context, providers []string, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) {
req, opts = cliproxysession.Enrich(req, opts)
normalized := m.normalizeProviders(providers)
if len(normalized) == 0 {
return cliproxyexecutor.Response{}, &Error{Code: "provider_not_found", Message: "no provider supplied"}
}
if m.HomeEnabled() {
resp, errHome := m.executeHome(ctx, normalized, req, opts, false)
return resp, unwrapRequestStopError(errHome)
}
defaultRequestRetry, maxRetryCredentials, maxWait := m.retrySettings()
var lastErr error
retryModel := authSelectionModelFromOptions(opts, req.Model)
for attempt := 0; ; attempt++ {
resp, errExec := m.executeMixedOnce(ctx, normalized, req, opts, maxRetryCredentials, attempt, defaultRequestRetry)
if errExec == nil {
return resp, nil
}
if isRequestTerminatedError(errExec) || isRequestStopError(errExec) {
return cliproxyexecutor.Response{}, unwrapRequestStopError(errExec)
}
lastErr = errExec
wait, shouldRetry := m.shouldRetryAfterErrorWithHomeRetryLimit(ctx, opts, errExec, attempt, normalized, retryModel, maxWait, -1, defaultRequestRetry)
if !shouldRetry {
break
}
if errWait := waitForCooldown(ctx, wait, maxWait); errWait != nil {
return cliproxyexecutor.Response{}, errWait
}
}
if lastErr != nil {
lastErr = unwrapRequestStopError(lastErr)
if hasAntigravityProvider(normalized) && shouldAttemptAntigravityCreditsFallback(m, lastErr, normalized) {
if resp, ok, errCredits := m.tryAntigravityCreditsExecute(ctx, req, opts); errCredits != nil {
return cliproxyexecutor.Response{}, errCredits
} else if ok {
return resp, nil
}
}
return cliproxyexecutor.Response{}, lastErr
}
return cliproxyexecutor.Response{}, &Error{Code: "auth_not_found", Message: "no auth available"}
}
// It supports multiple providers for the same model and round-robins the starting provider per model.
func (m *Manager) ExecuteCount(ctx context.Context, providers []string, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) {
req, opts = cliproxysession.Enrich(req, opts)
normalized := m.normalizeProviders(providers)
if len(normalized) == 0 {
return cliproxyexecutor.Response{}, &Error{Code: "provider_not_found", Message: "no provider supplied"}
}
if m.HomeEnabled() {
resp, errHome := m.executeHome(ctx, normalized, req, opts, true)
return resp, unwrapRequestStopError(errHome)
}
defaultRequestRetry, maxRetryCredentials, maxWait := m.retrySettings()
var lastErr error
retryModel := authSelectionModelFromOptions(opts, req.Model)
for attempt := 0; ; attempt++ {
resp, errExec := m.executeCountMixedOnce(ctx, normalized, req, opts, maxRetryCredentials, attempt, defaultRequestRetry)
if errExec == nil {
return resp, nil
}
if isRequestTerminatedError(errExec) || isRequestStopError(errExec) {
return cliproxyexecutor.Response{}, unwrapRequestStopError(errExec)
}
lastErr = errExec
wait, shouldRetry := m.shouldRetryAfterErrorWithHomeRetryLimit(ctx, opts, errExec, attempt, normalized, retryModel, maxWait, -1, defaultRequestRetry)
if !shouldRetry {
break
}
if errWait := waitForCooldown(ctx, wait, maxWait); errWait != nil {
return cliproxyexecutor.Response{}, errWait
}
}
if lastErr != nil {
return cliproxyexecutor.Response{}, unwrapRequestStopError(lastErr)
}
return cliproxyexecutor.Response{}, &Error{Code: "auth_not_found", Message: "no auth available"}
}
// ExecuteStream performs a streaming execution using the configured selector and executor.
// It supports multiple providers for the same model and round-robins the starting provider per model.
func (m *Manager) ExecuteStream(ctx context.Context, providers []string, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (*cliproxyexecutor.StreamResult, error) {
req, opts = cliproxysession.Enrich(req, opts)
if m.HomeEnabled() {
if unlockSession := m.lockHomeWebsocketSession(ctx, opts); unlockSession != nil {
defer unlockSession()
}
}
normalized := m.normalizeProviders(providers)
if len(normalized) == 0 {
return nil, &Error{Code: "provider_not_found", Message: "no provider supplied"}
}
defaultRequestRetry, maxRetryCredentials, maxWait := m.retrySettings()
var lastErr error
homeRetryLimit := -1
retryModel := authSelectionModelFromOptions(opts, req.Model)
attempt := 0
retryRoundPending := false
retryRoundWaited := false
for {
result, errStream := m.executeStreamMixedOnce(ctx, normalized, req, opts, maxRetryCredentials, &homeRetryLimit, attempt, defaultRequestRetry)
if errStream == nil {
return result, nil
}
if m.HomeEnabled() && retryRoundPending {
if wait, okWait := pendingHomeRetryRoundDelay(errStream, maxWait, &homeRetryLimit, pinnedAuthIDFromMetadata(opts.Metadata) == ""); okWait && m.homeRetryAllowed(attempt-1, homeRetryLimit) {
if retryRoundWaited {
return nil, errStream
}
if errWait := waitForCooldown(ctx, wait, maxWait); errWait != nil {
return nil, errWait
}
retryRoundWaited = true
continue
}
}
retryRoundPending = false
retryRoundWaited = false
if isRequestTerminatedError(errStream) || isRequestStopError(errStream) {
return nil, unwrapRequestStopError(errStream)
}
lastErr = errStream
wait, shouldRetry := m.shouldRetryAfterErrorWithHomeRetryLimit(ctx, opts, errStream, attempt, normalized, retryModel, maxWait, homeRetryLimit, defaultRequestRetry)
if !shouldRetry {
break
}
if errWait := waitForCooldown(ctx, wait, maxWait); errWait != nil {
return nil, errWait
}
attempt++
retryRoundPending = m.HomeEnabled()
retryRoundWaited = false
}
if lastErr != nil {
lastErr = unwrapRequestStopError(lastErr)
if hasAntigravityProvider(normalized) && shouldAttemptAntigravityCreditsFallback(m, lastErr, normalized) {
if result, ok, errCredits := m.tryAntigravityCreditsExecuteStream(ctx, req, opts); errCredits != nil {
return nil, errCredits
} else if ok {
return result, nil
}
}
var bootstrapErr *streamBootstrapError
if errors.As(lastErr, &bootstrapErr) && bootstrapErr != nil {
return streamErrorResult(bootstrapErr.Headers(), lastErr), nil
}
return nil, lastErr
}
return nil, &Error{Code: "auth_not_found", Message: "no auth available"}
}
type requestToFormatResolver interface {
RequestToFormat(req cliproxyexecutor.Request, opts cliproxyexecutor.Options) sdktranslator.Format
}
func isRequestTerminatedError(err error) bool {
var terminated *cliproxyexecutor.RequestTerminatedError
return errors.As(err, &terminated) && terminated != nil
}
func applyRequestAfterAuthInterceptor(ctx context.Context, executor ProviderExecutor, provider string, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, requestedModel string) (cliproxyexecutor.Request, cliproxyexecutor.Options, error) {
if opts.RequestAfterAuthInterceptor == nil {
return req, opts, nil
}
toFormat := requestToFormat(provider, executor, req, opts)
resp := opts.RequestAfterAuthInterceptor(ctx, cliproxyexecutor.RequestAfterAuthInterceptRequest{
SourceFormat: opts.SourceFormat,
ToFormat: toFormat,
Model: req.Model,
RequestedModel: requestedModel,
Stream: opts.Stream,
Headers: cloneRequestHeaders(opts.Headers),
Body: bytes.Clone(req.Payload),
Metadata: opts.Metadata,
})
opts.Headers = mergeRequestHeaders(opts.Headers, resp.Headers, resp.ClearHeaders)
if len(resp.Body) > 0 {
req.Payload = bytes.Clone(resp.Body)
opts.OriginalRequest = bytes.Clone(resp.Body)
}
if resp.Terminate {
return req, opts, &cliproxyexecutor.RequestTerminatedError{
HTTPStatus: resp.StatusCode,
Header: cloneRequestHeaders(resp.ResponseHeaders),
Body: bytes.Clone(resp.ResponseBody),
}
}
return req, opts, nil
}
func requestToFormat(provider string, executor ProviderExecutor, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) sdktranslator.Format {
resolver, ok := executor.(requestToFormatResolver)
if ok && resolver != nil {
formatRequestTo := resolver.RequestToFormat(req, opts)
if formatRequestTo != "" {
return formatRequestTo
}
}
source := opts.SourceFormat.String()
if source == "openai-image" || source == "openai-video" {
return opts.SourceFormat
}
if opts.Alt == "responses/compact" && !opts.Stream {
return sdktranslator.FormatOpenAIResponse
}
switch strings.ToLower(strings.TrimSpace(provider)) {
case "codex":
return sdktranslator.FormatCodex
case "xai":
return sdktranslator.FormatCodex
case "claude":
return sdktranslator.FormatClaude
case "gemini", "vertex", "aistudio":
return sdktranslator.FormatGemini
case "kimi":
return sdktranslator.FormatOpenAI
case "antigravity":
return sdktranslator.FormatAntigravity
default:
return sdktranslator.FormatOpenAI
}
}
func cloneRequestHeaders(src http.Header) http.Header {
if src == nil {
return nil
}
dst := make(http.Header, len(src))
for key, values := range src {
dst[key] = append([]string(nil), values...)
}
return dst
}
func mergeRequestHeaders(current, updates http.Header, clear []string) http.Header {
if updates == nil && len(clear) == 0 {
return current
}
out := cloneRequestHeaders(current)
if out == nil && (len(updates) > 0 || len(clear) > 0) {
out = make(http.Header)
}
for _, key := range clear {
out.Del(key)
}
for key, values := range updates {
out.Del(key)
for _, value := range values {
out.Add(key, value)
}
}
return out
}
func (m *Manager) executeMixedOnce(ctx context.Context, providers []string, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, maxRetryCredentials int, retryRound int, defaultRequestRetry int) (cliproxyexecutor.Response, error) {
if len(providers) == 0 {
return cliproxyexecutor.Response{}, &Error{Code: "provider_not_found", Message: "no provider supplied"}
}
routeModel := authSelectionModelFromOptions(opts, req.Model)
executionModel, restoreExecutionModel := executionModelForAuthSelection(opts, req.Model)
opts = ensureRequestedModelMetadata(opts, routeModel)
homeMode := m.HomeEnabled()
homeAuthCount := 1
tried := make(map[string]struct{})
if !homeMode {
for authID := range m.requestRetryRoundExclusions(retryRound, defaultRequestRetry) {
tried[authID] = struct{}{}
}
}
attempted := make(map[string]struct{})
var lastErr error
for {
if maxRetryCredentials > 0 && len(attempted) >= maxRetryCredentials {
if lastErr != nil {
return cliproxyexecutor.Response{}, lastErr
}
return cliproxyexecutor.Response{}, &Error{Code: "auth_not_found", Message: "no auth available"}
}
pickOpts := opts
if homeMode {
pickOpts = withHomeRetryRound(pickOpts, retryRound)
pickOpts = withHomeAuthCount(pickOpts, homeAuthCount)
pickOpts = withHomeExcludedAuthIDs(pickOpts, tried)
}
auth, executor, provider, errPick := m.pickNextMixed(ctx, providers, routeModel, pickOpts, tried)
if errPick != nil {
if shouldReturnLastErrorOnPickFailure(homeMode, lastErr, errPick) {
return cliproxyexecutor.Response{}, lastErr
}
return cliproxyexecutor.Response{}, errPick
}
entry := logEntryWithRequestID(ctx)
debugLogAuthSelection(entry, auth, provider, routeModel)
publishSelectedAuthMetadata(opts.Metadata, auth)
tried[auth.ID] = struct{}{}
execCtx := ctx
if rt := m.roundTripperFor(auth); rt != nil {
execCtx = context.WithValue(execCtx, roundTripperContextKey{}, rt)
execCtx = context.WithValue(execCtx, "cliproxy.roundtripper", rt)
}
execCtx = contextWithRequestedModelAlias(execCtx, opts, routeModel)
models, pooled, aliasResult, routing := m.preparedExecutionModelsWithAlias(auth, routeModel)
if len(models) == 0 {
continue
}
attempted[auth.ID] = struct{}{}
var errPrepare error
auth, errPrepare = m.prepareRequestAuth(execCtx, executor, auth)
if errPrepare != nil {
if errCancel := claudeOAuthRequestCancellation(execCtx, auth, errPrepare); errCancel != nil {
return cliproxyexecutor.Response{}, errCancel
}
result := Result{AuthID: auth.ID, Provider: provider, Model: routeModel, Success: false, Error: resultErrorFromError(errPrepare), Options: pickOpts}
m.MarkResult(execCtx, result)
lastErr = errPrepare
continue
}
var authErr error
didRefreshOnUnauthorized := false
for _, upstreamModel := range models {
resultModel := m.stateModelForExecution(auth, routeModel, upstreamModel, pooled)
execReq := req
execReq.Model = upstreamModel
if restoreExecutionModel {
execReq.Model = executionModel
}
execOpts := opts
var errIntercept error
execReq, execOpts, errIntercept = applyRequestAfterAuthInterceptor(execCtx, executor, provider, execReq, execOpts, requestedModelAliasFromOptions(execOpts, routeModel))
if errIntercept != nil {
return cliproxyexecutor.Response{}, errIntercept
}
if !restoreExecutionModel {
execReq = attachResolvedAPIKeyModelInfo(routing, execReq, auth, routeModel, upstreamModel)
}
startExec := time.Now()
resp, errExec := executor.Execute(execCtx, auth, execReq, execOpts)
durationExec := time.Since(startExec)
if errExec != nil {
if errCtx := execCtx.Err(); errCtx != nil {
return cliproxyexecutor.Response{}, errCtx
}
if refreshed, okRefresh := m.tryRefreshAfterUnauthorized(execCtx, auth, errExec, didRefreshOnUnauthorized); okRefresh {
auth = refreshed
didRefreshOnUnauthorized = true
startRetry := time.Now()
resp, errExec = executor.Execute(execCtx, auth, execReq, execOpts)
durationRetry := time.Since(startRetry)
if errExec != nil {
warnLogUpstreamFailure(execCtx, entry, provider, upstreamModel, auth, durationRetry, errExec)
if errCtx := execCtx.Err(); errCtx != nil {
return cliproxyexecutor.Response{}, errCtx
}
}
} else {
warnLogUpstreamFailure(execCtx, entry, provider, upstreamModel, auth, durationExec, errExec)
}
}
if errCancel := claudeOAuthRequestCancellation(execCtx, auth, errExec); errCancel != nil {
return cliproxyexecutor.Response{}, errCancel
}
result := Result{AuthID: auth.ID, Provider: provider, Model: resultModel, Success: errExec == nil, Options: execOpts}
if errExec != nil {
result.Error = resultErrorFromError(errExec)
if ra := retryAfterFromError(errExec); ra != nil {
result.RetryAfter = ra
}
if isCredentialScopedError(errExec) {
result.CredentialScope = true
}
action, okAction := matchRequestScopedErrorAction(auth, errExec, m.runtimeConfigSnapshot())
applyRequestScopedActionToResult(action, okAction, &result)
if isResponsesCompactAvailabilityNeutralError(execOpts, errExec, result.Error) {
m.recordAvailabilityNeutralResult(execCtx, result)
} else {
m.MarkResult(execCtx, result)
}
if okAction {
if isRequestScopedStop(action, okAction) {
return cliproxyexecutor.Response{}, wrapRequestStopError(errExec)
}
authErr = errExec
if result.CredentialScope {
break
}
continue
}
if isResponsesCompactRequestFaultError(execOpts, errExec) || isRequestInvalidError(errExec) {
return cliproxyexecutor.Response{}, errExec
}
authErr = errExec
if result.CredentialScope {
break
}
continue
}
m.MarkResult(execCtx, result)
attemptAliasResult := resolveAttemptAliasResult(routing, auth, routeModel, upstreamModel, aliasResult)
rewriteForceMappedResponse(&resp, attemptAliasResult)
return resp, nil
}
if authErr != nil {
action, okAction := matchRequestScopedErrorAction(auth, authErr, m.runtimeConfigSnapshot())
if okAction {
if isRequestScopedStop(action, okAction) {
return cliproxyexecutor.Response{}, wrapRequestStopError(authErr)
}
lastErr = authErr
if homeMode {
homeAuthCount++
}
continue
}
if isResponsesCompactRequestFaultError(opts, authErr) || isRequestInvalidError(authErr) {
return cliproxyexecutor.Response{}, authErr
}
lastErr = authErr
if homeMode {
homeAuthCount++
}
continue
}
}
}
func (m *Manager) executeCountMixedOnce(ctx context.Context, providers []string, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, maxRetryCredentials int, retryRound int, defaultRequestRetry int) (cliproxyexecutor.Response, error) {
if len(providers) == 0 {
return cliproxyexecutor.Response{}, &Error{Code: "provider_not_found", Message: "no provider supplied"}
}
routeModel := authSelectionModelFromOptions(opts, req.Model)
executionModel, restoreExecutionModel := executionModelForAuthSelection(opts, req.Model)
opts = ensureRequestedModelMetadata(opts, routeModel)
homeMode := m.HomeEnabled()
homeAuthCount := 1
tried := make(map[string]struct{})
if !homeMode {
for authID := range m.requestRetryRoundExclusions(retryRound, defaultRequestRetry) {
tried[authID] = struct{}{}
}
}
attempted := make(map[string]struct{})
var lastErr error
for {
if maxRetryCredentials > 0 && len(attempted) >= maxRetryCredentials {
if lastErr != nil {
return cliproxyexecutor.Response{}, lastErr
}
return cliproxyexecutor.Response{}, &Error{Code: "auth_not_found", Message: "no auth available"}
}
pickOpts := opts
if homeMode {
pickOpts = withHomeRetryRound(pickOpts, retryRound)
pickOpts = withHomeAuthCount(pickOpts, homeAuthCount)
pickOpts = withHomeExcludedAuthIDs(pickOpts, tried)
}
auth, executor, provider, errPick := m.pickNextMixed(ctx, providers, routeModel, pickOpts, tried)
if errPick != nil {
if shouldReturnLastErrorOnPickFailure(homeMode, lastErr, errPick) {
return cliproxyexecutor.Response{}, lastErr
}
return cliproxyexecutor.Response{}, errPick
}
entry := logEntryWithRequestID(ctx)
debugLogAuthSelection(entry, auth, provider, routeModel)
publishSelectedAuthMetadata(opts.Metadata, auth)
tried[auth.ID] = struct{}{}
execCtx := ctx
if rt := m.roundTripperFor(auth); rt != nil {
execCtx = context.WithValue(execCtx, roundTripperContextKey{}, rt)
execCtx = context.WithValue(execCtx, "cliproxy.roundtripper", rt)
}
execCtx = contextWithRequestedModelAlias(execCtx, opts, routeModel)
models, pooled, aliasResult, routing := m.preparedExecutionModelsWithAlias(auth, routeModel)
if len(models) == 0 {
continue
}
attempted[auth.ID] = struct{}{}
var errPrepare error
auth, errPrepare = m.prepareRequestAuth(execCtx, executor, auth)
if errPrepare != nil {
if errCancel := claudeOAuthRequestCancellation(execCtx, auth, errPrepare); errCancel != nil {
return cliproxyexecutor.Response{}, errCancel
}
result := Result{AuthID: auth.ID, Provider: provider, Model: routeModel, Success: false, Error: resultErrorFromError(errPrepare), Options: pickOpts}
m.MarkResult(execCtx, result)
lastErr = errPrepare
continue
}
var authErr error
didRefreshOnUnauthorized := false
for _, upstreamModel := range models {
resultModel := m.stateModelForExecution(auth, routeModel, upstreamModel, pooled)
execReq := req
execReq.Model = upstreamModel
if restoreExecutionModel {
execReq.Model = executionModel
}
execOpts := opts
var errIntercept error
execReq, execOpts, errIntercept = applyRequestAfterAuthInterceptor(execCtx, executor, provider, execReq, execOpts, requestedModelAliasFromOptions(execOpts, routeModel))
if errIntercept != nil {
return cliproxyexecutor.Response{}, errIntercept
}
if !restoreExecutionModel {
execReq = attachResolvedAPIKeyModelInfo(routing, execReq, auth, routeModel, upstreamModel)
}
startExec := time.Now()
resp, errExec := executor.CountTokens(execCtx, auth, execReq, execOpts)
durationExec := time.Since(startExec)
if errExec != nil {
if errCtx := execCtx.Err(); errCtx != nil {
return cliproxyexecutor.Response{}, errCtx
}
if refreshed, okRefresh := m.tryRefreshAfterUnauthorized(execCtx, auth, errExec, didRefreshOnUnauthorized); okRefresh {
auth = refreshed
didRefreshOnUnauthorized = true
startRetry := time.Now()
resp, errExec = executor.CountTokens(execCtx, auth, execReq, execOpts)
durationRetry := time.Since(startRetry)
if errExec != nil {
warnLogUpstreamFailure(execCtx, entry, provider, upstreamModel, auth, durationRetry, errExec)
if errCtx := execCtx.Err(); errCtx != nil {
return cliproxyexecutor.Response{}, errCtx
}
}
} else {
warnLogUpstreamFailure(execCtx, entry, provider, upstreamModel, auth, durationExec, errExec)
}
}
if errCancel := claudeOAuthRequestCancellation(execCtx, auth, errExec); errCancel != nil {
return cliproxyexecutor.Response{}, errCancel
}
result := Result{AuthID: auth.ID, Provider: provider, Model: resultModel, Success: errExec == nil, Options: execOpts}
if errExec != nil {
result.Error = resultErrorFromError(errExec)
if ra := retryAfterFromError(errExec); ra != nil {
result.RetryAfter = ra
}
action, okAction := matchRequestScopedErrorAction(auth, errExec, m.runtimeConfigSnapshot())
applyRequestScopedActionToResult(action, okAction, &result)
// Some Anthropic-compatible upstreams do not implement the
// count_tokens route and return a generic endpoint 404. Record
// the failure for hooks and metrics without suspending a model
// that remains usable through the messages endpoint.
if isCountTokensEndpointNotFoundError(errExec, execReq.Model) && (result.Error == nil || result.Error.Code != ErrorCodeForceCooldown) {
m.recordAvailabilityNeutralResult(execCtx, result)
} else {
if isCredentialScopedError(errExec) {
result.CredentialScope = true
}
m.MarkResult(execCtx, result)
}
if okAction {
if isRequestScopedStop(action, okAction) {
return cliproxyexecutor.Response{}, wrapRequestStopError(errExec)
}
authErr = errExec
if result.CredentialScope {
break
}
continue
}
if isRequestInvalidError(errExec) {
return cliproxyexecutor.Response{}, errExec
}
authErr = errExec
if result.CredentialScope {
break
}
continue
}
m.MarkResult(execCtx, result)
attemptAliasResult := resolveAttemptAliasResult(routing, auth, routeModel, upstreamModel, aliasResult)
rewriteForceMappedResponse(&resp, attemptAliasResult)
return resp, nil
}
if authErr != nil {
action, okAction := matchRequestScopedErrorAction(auth, authErr, m.runtimeConfigSnapshot())
if okAction {
if isRequestScopedStop(action, okAction) {
return cliproxyexecutor.Response{}, wrapRequestStopError(authErr)
}
lastErr = authErr
if homeMode {
homeAuthCount++
}
continue
}
if isRequestInvalidError(authErr) {
return cliproxyexecutor.Response{}, authErr
}
lastErr = authErr
if homeMode {
homeAuthCount++
}
continue
}
}
}
func (m *Manager) executeStreamMixedOnce(ctx context.Context, providers []string, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, maxRetryCredentials int, homeRetryLimit *int, retryRound int, defaultRequestRetry int) (*cliproxyexecutor.StreamResult, error) {
if len(providers) == 0 {
return nil, &Error{Code: "provider_not_found", Message: "no provider supplied"}
}
routeModel := authSelectionModelFromOptions(opts, req.Model)
responseAlias := requestedModelAliasFromOptions(opts, routeModel)
executionModel, restoreExecutionModel := executionModelForAuthSelection(opts, req.Model)
opts = ensureRequestedModelMetadata(opts, routeModel)
homeMode := m.HomeEnabled()
homeAuthCount := 1
tried := make(map[string]struct{})
if !homeMode {
for authID := range m.requestRetryRoundExclusions(retryRound, defaultRequestRetry) {
tried[authID] = struct{}{}
}
}
homeExcludedAuthIDs := make(map[string]struct{})
homeSameAuthRetries := make(map[string]int)
lastHomeAuthID := ""
homeSameAuthRetryPending := false
attempted := make(map[string]struct{})
unauthorizedRefreshTried := make(map[string]struct{})
var lastErr error
var roundTiming homeRetryRoundTiming
for {
allowSameAuthRetry := homeMode && homeSameAuthRetryPending && lastHomeAuthID != "" && homeSameAuthRetries[lastHomeAuthID] == 0
if maxRetryCredentials > 0 && len(attempted) >= maxRetryCredentials && !allowSameAuthRetry {
if lastErr != nil {
if homeMode {
return nil, markHomeRetryRoundExhausted(lastErr, roundTiming.RetryAfter(), true)
}
return nil, lastErr
}
return nil, &Error{Code: "auth_not_found", Message: "no auth available"}
}
pickOpts := opts
if homeMode {
pickOpts = withHomeRetryRound(pickOpts, retryRound)
pickOpts = withHomeAuthCount(pickOpts, homeAuthCount)
pickOpts = withHomeExcludedAuthIDs(pickOpts, homeExcludedAuthIDs)
}
var selection *HomeDispatchSelection
var auth *Auth
var executor ProviderExecutor
var provider string
var errPick error
if homeMode {
selection, errPick = m.pickHomeDispatchSelection(ctx, routeModel, pickOpts)
if selection != nil {
auth = selection.CloneAuthForRoute(routeModel)
executor = selection.Executor
provider = selection.Provider
}
} else {
auth, executor, provider, errPick = m.pickNextMixed(ctx, providers, routeModel, pickOpts, tried)
}
if errPick != nil {
var homeCooldown *homeDispatchRetryAfterError
if homeMode && lastErr != nil && errors.As(errPick, &homeCooldown) && homeCooldown != nil {
observeHomeCooldownRetryLimit(homeCooldown, homeRetryLimit, pinnedAuthIDFromMetadata(opts.Metadata) == "")
return nil, markHomeRetryRoundExhausted(lastErr, homeCooldown.RetryAfter(), false)
}
if shouldReturnLastErrorOnPickFailure(homeMode, lastErr, errPick) {
if homeMode {
return nil, markHomeRetryRoundExhausted(lastErr, roundTiming.RetryAfter(), isHomeNextRoundImmediatelyAvailable(errPick))
}
return nil, lastErr
}
return nil, errPick
}
if auth == nil || executor == nil {
if selection != nil {
selection.End("missing_execution_target")
}
return nil, &Error{Code: "executor_not_found", Message: "executor not registered"}
}
if homeMode {
m.observeHomeRetryLimit(auth, selection, homeRetryLimit)
}
if selection != nil && allowSameAuthRetry && maxRetryCredentials > 0 && len(attempted) >= maxRetryCredentials && auth.ID != lastHomeAuthID {
if errEnd := m.endHomeSelectionBeforeRedispatch(ctx, selection, "max_retry_credentials"); errEnd != nil {
return nil, errEnd
}
if lastErr != nil {
return nil, markHomeRetryRoundExhausted(lastErr, roundTiming.RetryAfter(), true)
}
return nil, &Error{Code: "auth_not_found", Message: "no auth available"}
}
if homeMode && lastHomeAuthID != "" && auth.ID != lastHomeAuthID {
homeSameAuthRetryPending = false
}
if selection != nil {
// A legacy Home may ignore excluded_auth_ids and return the same
// credential again. Reject credentials explicitly excluded from this
// round while retaining the explicit same-auth retry path, which
// intentionally leaves the credential out of homeExcludedAuthIDs.
if _, alreadyTried := tried[auth.ID]; alreadyTried {
if _, excluded := homeExcludedAuthIDs[auth.ID]; excluded {
if errEnd := m.endHomeSelectionBeforeRedispatch(ctx, selection, "repeated_excluded_auth"); errEnd != nil {
return nil, errEnd
}
if lastErr != nil {
return nil, markHomeRetryRoundExhausted(lastErr, roundTiming.RetryAfter(), false)
}
return nil, repeatedHomeAuthError()
} else {
homeSameAuthRetries[auth.ID]++
if homeSameAuthRetries[auth.ID] > 1 {
// A fresh Home selection may retry the same auth once for
// connection lifecycle or authorization recovery. Repeated
// failures must still rotate away from this credential.
homeExcludedAuthIDs[auth.ID] = struct{}{}
if errEnd := m.endHomeSelectionBeforeRedispatch(ctx, selection, "repeated_same_auth"); errEnd != nil {
return nil, errEnd
}
continue
}
}
}
if _, refreshedAlready := unauthorizedRefreshTried[auth.ID]; refreshedAlready {
homeExcludedAuthIDs[auth.ID] = struct{}{}
homeSameAuthRetryPending = false
if errEnd := m.endHomeSelectionBeforeRedispatch(ctx, selection, "repeated_refresh_auth"); errEnd != nil {
return nil, errEnd
}
continue
}
}
entry := logEntryWithRequestID(ctx)
debugLogAuthSelection(entry, auth, provider, routeModel)
if selection != nil {
if errRuntimeAuth := m.bindHomeSelectionRuntimeAuth(ctx, opts, selection); errRuntimeAuth != nil {
selection.End("runtime_auth_bind_failed")
return nil, errRuntimeAuth
}
}
publishSelectedAuthMetadata(opts.Metadata, auth)
tried[auth.ID] = struct{}{}
execCtx := ctx
releaseAttempt := func() {}
if selection != nil {
var errBind error
execCtx, releaseAttempt, errBind = homeExecutionAttemptContext(ctx, selection)
if errBind != nil {
selection.End("attempt_bind_failed")
return nil, errBind
}
}
if rt := m.roundTripperFor(auth); rt != nil {
execCtx = context.WithValue(execCtx, roundTripperContextKey{}, rt)
execCtx = context.WithValue(execCtx, "cliproxy.roundtripper", rt)
}
// Enrich before auth preparation so prepare-stage usage records observe the client request.
execCtx = contextWithRequestedModelAlias(execCtx, opts, routeModel)
models, pooled, aliasResult, routing := m.preparedExecutionModelsWithAlias(auth, routeModel)
if selection != nil && aliasResult.ForceMapping && responseAlias != "" {
aliasResult.OriginalAlias = responseAlias
}
if len(models) == 0 {
if selection != nil {
homeExcludedAuthIDs[auth.ID] = struct{}{}
lastHomeAuthID = auth.ID
homeSameAuthRetryPending = false
releaseAttempt()
if errEnd := m.endHomeSelectionBeforeRedispatch(ctx, selection, "no_execution_models"); errEnd != nil {
return nil, errEnd
}
}
continue
}
attempted[auth.ID] = struct{}{}
var errPrepare error
if selection != nil {
auth, errPrepare = m.prepareHomeRequestAuth(execCtx, executor, selection)
} else {
auth, errPrepare = m.prepareRequestAuth(execCtx, executor, auth)
}
if errPrepare != nil {
if selection != nil {
excludeAuth := shouldExcludeHomeAuthAfterStreamError(execCtx, auth, errPrepare)
if _, refreshedAlready := unauthorizedRefreshTried[auth.ID]; refreshedAlready || homeSameAuthRetries[auth.ID] > 0 {
excludeAuth = true
}
if excludeAuth {
homeExcludedAuthIDs[auth.ID] = struct{}{}
}
lastHomeAuthID = auth.ID
homeSameAuthRetryPending = !excludeAuth
}
if selection == nil {
if errCancel := claudeOAuthRequestCancellation(execCtx, auth, errPrepare); errCancel != nil {
return nil, errCancel
}
}
result := Result{AuthID: auth.ID, Provider: provider, Model: routeModel, Success: false, Error: resultErrorFromError(errPrepare), Options: pickOpts}
if selection != nil {
m.reportHomeResult(execCtx, result, auth)
releaseAttempt()
} else {
m.MarkResult(execCtx, result)
}
lastErr = errPrepare
if homeMode {
roundTiming.Observe(lastErr)
}
if selection != nil {
if errEnd := m.endHomeSelectionBeforeRedispatch(ctx, selection, "prepare_failed"); errEnd != nil {
return nil, errEnd
}
}
continue
}
execReq := sanitizeDownstreamWebsocketFallbackRequest(execCtx, auth, req)
streamExecutionModel := ""
if restoreExecutionModel {
streamExecutionModel = executionModel
}
execOpts := opts
if selection != nil {
execOpts.ExecutionLifecycle = selection
}
if homeMode && len(models) > 1 {
models = models[:1]
pooled = false
}
streamResult, errStream := m.executeStreamWithModelPool(execCtx, executor, auth, provider, execReq, execOpts, routeModel, streamExecutionModel, models, pooled, aliasResult, routing, !homeMode || selection != nil, selection != nil, unauthorizedRefreshTried)
if errStream != nil {
if selection != nil {
excludeAuth := shouldExcludeHomeAuthAfterStreamError(execCtx, auth, errStream)
if _, refreshedAlready := unauthorizedRefreshTried[auth.ID]; refreshedAlready || homeSameAuthRetries[auth.ID] > 0 {
excludeAuth = true
}
if excludeAuth {
homeExcludedAuthIDs[auth.ID] = struct{}{}
}
lastHomeAuthID = auth.ID
homeSameAuthRetryPending = !excludeAuth
}
if selection != nil {
releaseAttempt()
if errEnd := m.endHomeSelectionBeforeRedispatch(ctx, selection, "stream_start_failed"); errEnd != nil {
return nil, errEnd
}
}
if errCtx := execCtx.Err(); errCtx != nil && ctx != nil && ctx.Err() != nil {
return nil, errCtx
}
action, okAction := matchRequestScopedErrorAction(auth, errStream, m.runtimeConfigSnapshot())
if okAction {
if isRequestScopedStop(action, okAction) {
return nil, wrapRequestStopError(errStream)
}
lastErr = errStream
if homeMode {
roundTiming.Observe(lastErr)
}
if homeMode {
homeAuthCount++
}
continue
}
if isRequestInvalidError(errStream) {
return nil, errStream
}
lastErr = errStream
if homeMode {
roundTiming.Observe(lastErr)
}
if homeMode {
homeAuthCount++
}
continue
}
if selection != nil {
if m.retainHomeWebsocketSelection(ctx, opts, routeModel, selection) {
return wrapHomeStream(ctx, streamResult, nil, releaseAttempt), nil
}
return wrapHomeStream(ctx, streamResult, selection, releaseAttempt), nil
}
return streamResult, nil
}
}
func shouldExcludeHomeAuthAfterStreamError(ctx context.Context, auth *Auth, err error) bool {
if err == nil || isConnectionLifecycleError(err) {
return false
}
// A 426 during a downstream websocket attempt is a transport fallback
// signal. OAuth authorization failures may also recover after a refresh.
// Both paths may retry the same credential once.
if cliproxyexecutor.DownstreamWebsocket(ctx) && statusCodeFromError(err) == http.StatusUpgradeRequired {
return false
}
return !isUnauthorizedError(err) || auth == nil || auth.AuthKind() != AuthKindOAuth
}
func cloneRequestMetadata(src map[string]any) map[string]any {
if len(src) == 0 {
return make(map[string]any, 4)
}
dst := make(map[string]any, len(src)+4)
for k, v := range src {
dst[k] = v
}
return dst
}
func ensureRequestedModelMetadata(opts cliproxyexecutor.Options, requestedModel string) cliproxyexecutor.Options {
opts.Metadata = cloneRequestMetadata(opts.Metadata)
requestedModel = strings.TrimSpace(requestedModel)
if requestedModel == "" {
return opts
}
if hasRequestedModelMetadata(opts.Metadata) {
return opts
}
opts.Metadata[cliproxyexecutor.RequestedModelMetadataKey] = requestedModel
return opts
}
func authSelectionModelFromOptions(opts cliproxyexecutor.Options, fallback string) string {
fallback = strings.TrimSpace(fallback)
if len(opts.Metadata) == 0 {
return fallback
}
raw, ok := opts.Metadata[cliproxyexecutor.AuthSelectionModelMetadataKey]
if !ok || raw == nil {
return fallback
}
switch value := raw.(type) {
case string:
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
case []byte:
if strings.TrimSpace(string(value)) != "" {
return strings.TrimSpace(string(value))
}
}
return fallback
}
func executionModelForAuthSelection(opts cliproxyexecutor.Options, model string) (string, bool) {
model = strings.TrimSpace(model)
if model == "" {
return "", false
}
selectionModel := authSelectionModelFromOptions(opts, model)
if selectionModel == model {
return "", false
}
return model, true
}
func withHomeAuthCount(opts cliproxyexecutor.Options, count int) cliproxyexecutor.Options {
if count <= 0 {
count = 1
}
meta := make(map[string]any, len(opts.Metadata)+1)
for k, v := range opts.Metadata {
meta[k] = v
}
meta[homeAuthCountMetadataKey] = count
opts.Metadata = meta
return opts
}
func withHomeRetryRound(opts cliproxyexecutor.Options, retryRound int) cliproxyexecutor.Options {
meta := make(map[string]any, len(opts.Metadata)+1)
for key, value := range opts.Metadata {
meta[key] = value
}
if retryRound > 0 {
meta[homeRetryRoundMetadataKey] = retryRound
} else {
delete(meta, homeRetryRoundMetadataKey)
}
opts.Metadata = meta
return opts
}
func withHomeExcludedAuthIDs(opts cliproxyexecutor.Options, tried map[string]struct{}) cliproxyexecutor.Options {
meta := make(map[string]any, len(opts.Metadata)+1)
for key, value := range opts.Metadata {
meta[key] = value
}
excluded := make(map[string]struct{})
for _, authID := range homeExcludedAuthIDsFromMetadata(meta) {
excluded[authID] = struct{}{}
}
for authID := range tried {
if authID = strings.TrimSpace(authID); authID != "" {
excluded[authID] = struct{}{}
}
}
if len(excluded) == 0 {
delete(meta, ExcludedAuthIDsMetadataKey)
} else {
ids := make([]string, 0, len(excluded))
for authID := range excluded {
ids = append(ids, authID)
}
sort.Strings(ids)
meta[ExcludedAuthIDsMetadataKey] = ids
}
opts.Metadata = meta
return opts
}
func homeAuthCountFromMetadata(meta map[string]any) int {
if len(meta) == 0 {
return 1
}
switch value := meta[homeAuthCountMetadataKey].(type) {
case int:
if value > 0 {
return value
}
case int64:
if value > 0 {
return int(value)
}
case float64:
if value > 0 {
return int(value)
}
}
return 1
}
func homeExcludedAuthIDsFromMetadata(meta map[string]any) []string {
if len(meta) == 0 {
return nil
}
raw, ok := meta[ExcludedAuthIDsMetadataKey]
if !ok {
return nil
}
seen := make(map[string]struct{})
ids := make([]string, 0)
appendID := func(value string) {
value = strings.TrimSpace(value)
if value == "" {
return
}
if _, exists := seen[value]; exists {
return
}
seen[value] = struct{}{}
ids = append(ids, value)
}
switch values := raw.(type) {
case []string:
for _, value := range values {
appendID(value)
}
case []any:
for _, value := range values {
if text, okText := value.(string); okText {
appendID(text)
}
}
case map[string]struct{}:
for value := range values {
appendID(value)
}
case map[string]bool:
for value, enabled := range values {
if enabled {
appendID(value)
}
}
}
if len(ids) == 0 {
return nil
}
sort.Strings(ids)
return ids
}
func hasRequestedModelMetadata(meta map[string]any) bool {
if len(meta) == 0 {
return false
}
raw, ok := meta[cliproxyexecutor.RequestedModelMetadataKey]
if !ok || raw == nil {
return false
}
switch v := raw.(type) {
case string:
return strings.TrimSpace(v) != ""
case []byte:
return strings.TrimSpace(string(v)) != ""
default:
return false
}
}
type requestAuthPrepareLock struct {
mu sync.Mutex
}
// prepareHomeRequestAuth prepares a dispatch auth without reading or updating local auth state.
func (m *Manager) prepareHomeRequestAuth(ctx context.Context, executor ProviderExecutor, selection *HomeDispatchSelection) (*Auth, error) {
if selection == nil {
return nil, nil
}
return m.prepareHomeAuthSnapshot(ctx, executor, selection.CloneAuth())
}
func (m *Manager) prepareHomeAuthSnapshot(ctx context.Context, executor ProviderExecutor, auth *Auth) (*Auth, error) {
if m == nil || executor == nil || auth == nil {
return auth, nil
}
preparer, ok := executor.(RequestAuthPreparer)
if !ok || preparer == nil || !preparer.ShouldPrepareRequestAuth(auth) {
return auth, nil
}
prepare := func() (*Auth, error) {
target := auth.Clone()
if !preparer.ShouldPrepareRequestAuth(target) {
return target, nil
}
updated, errPrepare := preparer.PrepareRequestAuth(ctx, target)
if errPrepare != nil {
return auth, errPrepare
}
if updated == nil {
return target, nil
}
return updated, nil
}
id := strings.TrimSpace(auth.ID)
if id == "" {
return prepare()
}
lockValue, _ := m.requestPrepareLocks.LoadOrStore(id, &requestAuthPrepareLock{})
lock, ok := lockValue.(*requestAuthPrepareLock)
if !ok || lock == nil {
return prepare()
}
lock.mu.Lock()
defer lock.mu.Unlock()
return prepare()
}
func (m *Manager) prepareRequestAuth(ctx context.Context, executor ProviderExecutor, auth *Auth) (*Auth, error) {
if m == nil || executor == nil || auth == nil {
return auth, nil
}
preparer, ok := executor.(RequestAuthPreparer)
if !ok || preparer == nil || !preparer.ShouldPrepareRequestAuth(auth) {
return auth, nil
}
id := strings.TrimSpace(auth.ID)
if id == "" {
return preparer.PrepareRequestAuth(ctx, auth.Clone())
}
lockValue, _ := m.requestPrepareLocks.LoadOrStore(id, &requestAuthPrepareLock{})
lock, ok := lockValue.(*requestAuthPrepareLock)
if !ok || lock == nil {
return preparer.PrepareRequestAuth(ctx, auth.Clone())
}
lock.mu.Lock()
defer lock.mu.Unlock()
target := auth.Clone()
m.mu.RLock()
if current := m.auths[id]; current != nil {
target = current.Clone()
}
m.mu.RUnlock()
if !preparer.ShouldPrepareRequestAuth(target) {
return target, nil
}
updated, errPrepare := preparer.PrepareRequestAuth(ctx, target)
if errPrepare != nil {
return auth, errPrepare
}
if updated == nil {
return target, nil
}
saved, errUpdate := m.Update(ctx, updated)
if errUpdate != nil {
return updated, errUpdate
}
if saved != nil {
return saved, nil
}
return updated, nil
}
func contextWithRequestedModelAlias(ctx context.Context, opts cliproxyexecutor.Options, fallback string) context.Context {
alias := requestedModelAliasFromOptions(opts, fallback)
ctx = coreusage.WithRequestedModelAlias(ctx, alias)
effort := reasoningEffortFromOptions(opts)
if effort != "" {
ctx = coreusage.WithReasoningEffort(ctx, effort)
}
serviceTier := serviceTierFromOptions(opts)
if serviceTier != "" {
ctx = coreusage.WithServiceTier(ctx, serviceTier)
}
if generate, ok := generateFromOptions(opts); ok {
ctx = coreusage.WithGenerate(ctx, generate)
}
return ctx
}
func requestedModelAliasFromOptions(opts cliproxyexecutor.Options, fallback string) string {
fallback = strings.TrimSpace(fallback)
if len(opts.Metadata) == 0 {
return fallback
}
raw, ok := opts.Metadata[cliproxyexecutor.RequestedModelMetadataKey]
if !ok || raw == nil {
return fallback
}
switch value := raw.(type) {
case string:
if strings.TrimSpace(value) == "" {
return fallback
}
return strings.TrimSpace(value)
case []byte:
if len(value) == 0 {
return fallback
}
return strings.TrimSpace(string(value))
default:
return fallback
}
}
func reasoningEffortFromOptions(opts cliproxyexecutor.Options) string {
if len(opts.Metadata) == 0 {
return ""
}
raw, ok := opts.Metadata[cliproxyexecutor.ReasoningEffortMetadataKey]
if !ok || raw == nil {
return ""
}
switch value := raw.(type) {
case string:
return strings.TrimSpace(value)
case []byte:
return strings.TrimSpace(string(value))
default:
return ""
}
}
func serviceTierFromOptions(opts cliproxyexecutor.Options) string {
return stringMetadataValue(opts.Metadata, cliproxyexecutor.ServiceTierMetadataKey)
}
func generateFromOptions(opts cliproxyexecutor.Options) (bool, bool) {
if len(opts.Metadata) == 0 {
return false, false
}
raw, ok := opts.Metadata[cliproxyexecutor.GenerateMetadataKey]
if !ok || raw == nil {
return false, false
}
switch value := raw.(type) {
case bool:
return value, true
default:
return false, false
}
}
func stringMetadataValue(metadata map[string]any, key string) string {
if len(metadata) == 0 {
return ""
}
raw, ok := metadata[key]
if !ok || raw == nil {
return ""
}
switch value := raw.(type) {
case string:
return strings.TrimSpace(value)
case []byte:
return strings.TrimSpace(string(value))
default:
return ""
}
}
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 disallowFreeAuthFromMetadata(meta map[string]any) bool {
if len(meta) == 0 {
return false
}
raw, ok := meta[cliproxyexecutor.DisallowFreeAuthMetadataKey]
if !ok || raw == nil {
return false
}
switch val := raw.(type) {
case bool:
return val
case string:
parsed, err := strconv.ParseBool(strings.TrimSpace(val))
return err == nil && parsed
case []byte:
parsed, err := strconv.ParseBool(strings.TrimSpace(string(val)))
return err == nil && parsed
default:
return false
}
}
func isFreeCodexAuth(auth *Auth) bool {
if auth == nil || auth.Attributes == nil {
return false
}
if !strings.EqualFold(strings.TrimSpace(auth.Provider), "codex") {
return false
}
return strings.EqualFold(strings.TrimSpace(auth.Attributes["plan_type"]), "free")
}
func publishSelectedAuthMetadata(meta map[string]any, auth *Auth) {
if len(meta) == 0 || auth == nil {
return
}
if authID := strings.TrimSpace(auth.ID); authID != "" {
meta[cliproxyexecutor.SelectedAuthMetadataKey] = authID
if callback, ok := meta[cliproxyexecutor.SelectedAuthCallbackMetadataKey].(func(string)); ok && callback != nil {
callback(authID)
}
}
if authIndex := strings.TrimSpace(auth.EnsureIndex()); authIndex != "" {
meta[cliproxyexecutor.SelectedAuthIndexMetadataKey] = authIndex
if callback, ok := meta[cliproxyexecutor.SelectedAuthIndexCallbackMetadataKey].(func(string)); ok && callback != nil {
callback(authIndex)
}
}
}
func (m *Manager) executorFor(provider string) ProviderExecutor {
m.mu.RLock()
defer m.mu.RUnlock()
return m.executors[provider]
}
// roundTripperContextKey is an unexported context key type to avoid collisions.
type roundTripperContextKey struct{}
// roundTripperFor retrieves an HTTP RoundTripper for the given auth if a provider is registered.
func (m *Manager) roundTripperFor(auth *Auth) http.RoundTripper {
m.mu.RLock()
p := m.rtProvider
m.mu.RUnlock()
if p == nil || auth == nil {
return nil
}
return p.RoundTripperFor(auth)
}
// RoundTripperProvider defines a minimal provider of per-auth HTTP transports.
type RoundTripperProvider interface {
RoundTripperFor(auth *Auth) http.RoundTripper
}
// RequestPreparer is an optional interface that provider executors can implement
// to mutate outbound HTTP requests with provider credentials.
type RequestPreparer interface {
PrepareRequest(req *http.Request, auth *Auth) error
}
func executorKeyFromAuth(auth *Auth) string {
if auth == nil {
return ""
}
if auth.Attributes != nil {
providerKey := strings.TrimSpace(auth.Attributes["provider_key"])
compatName := strings.TrimSpace(auth.Attributes["compat_name"])
if compatName != "" {
if providerKey == "" {
providerKey = compatName
}
return util.OpenAICompatibleProviderKey(providerKey)
}
}
if strings.EqualFold(strings.TrimSpace(auth.Provider), "openai-compatibility") {
providerKey := strings.TrimSpace(auth.Label)
if providerKey == "" {
providerKey = "openai-compatibility"
}
return util.OpenAICompatibleProviderKey(providerKey)
}
return strings.ToLower(strings.TrimSpace(auth.Provider))
}
// logEntryWithRequestID returns a logrus entry with request_id field if available in context.
func logEntryWithRequestID(ctx context.Context) *log.Entry {
if ctx == nil {
return log.NewEntry(log.StandardLogger())
}
if reqID := logging.GetRequestID(ctx); reqID != "" {
return log.WithField("request_id", reqID)
}
return log.NewEntry(log.StandardLogger())
}
func debugLogAuthSelection(entry *log.Entry, auth *Auth, provider string, model string) {
if !log.IsLevelEnabled(log.DebugLevel) {
return
}
if entry == nil || auth == nil {
return
}
accountType, accountInfo := auth.AccountInfo()
proxyInfo := auth.ProxyInfo()
suffix := ""
if proxyInfo != "" {
suffix = " " + proxyInfo
}
switch accountType {
case "api_key":
entry.Debugf("Use API key %s for model %s%s", util.HideAPIKey(accountInfo), model, suffix)
case "oauth":
ident := formatOauthIdentity(auth, provider, accountInfo)
entry.Debugf("Use OAuth %s for model %s%s", ident, model, suffix)
}
}
func formatOauthIdentity(auth *Auth, provider string, accountInfo string) string {
if auth == nil {
return ""
}
// Prefer the auth's provider when available.
providerName := strings.TrimSpace(auth.Provider)
if providerName == "" {
providerName = strings.TrimSpace(provider)
}
// Only log the basename to avoid leaking host paths.
// FileName may be unset for some auth backends; fall back to ID.
authFile := strings.TrimSpace(auth.FileName)
if authFile == "" {
authFile = strings.TrimSpace(auth.ID)
}
if authFile != "" {
authFile = filepath.Base(authFile)
}
parts := make([]string, 0, 3)
if providerName != "" {
parts = append(parts, "provider="+providerName)
}
if authFile != "" {
parts = append(parts, "auth_file="+authFile)
}
if len(parts) == 0 {
return accountInfo
}
return strings.Join(parts, " ")
}
func formatAuthIdentity(auth *Auth, provider string) string {
if auth == nil {
return "auth=nil"
}
accountType, accountInfo := auth.AccountInfo()
switch accountType {
case "api_key":
return fmt.Sprintf("api_key=%s", util.HideAPIKey(accountInfo))
case "oauth":
return formatOauthIdentity(auth, provider, accountInfo)
default:
if auth.FileName != "" {
return fmt.Sprintf("auth_file=%s", filepath.Base(auth.FileName))
}
if auth.ID != "" {
return fmt.Sprintf("auth_id=%s", auth.ID)
}
if accountInfo != "" {
return accountInfo
}
return "unknown"
}
}
func summarizeErrorForLog(err error) string {
if err == nil {
return ""
}
msg := strings.TrimSpace(err.Error())
const maxRunes = 300
runes := []rune(msg)
if len(runes) > maxRunes {
return string(runes[:maxRunes]) + "..."
}
return msg
}
func warnLogUpstreamFailure(ctx context.Context, entry *log.Entry, provider, model string, auth *Auth, duration time.Duration, err error) {
if err == nil {
return
}
if ctx != nil && errors.Is(ctx.Err(), context.Canceled) {
return
}
if errors.Is(err, context.Canceled) {
return
}
if isRequestInvalidError(err) {
return
}
if entry == nil {
if ctx != nil {
entry = logEntryWithRequestID(ctx)
} else {
entry = log.NewEntry(log.StandardLogger())
}
}
authIdent := formatAuthIdentity(auth, provider)
errSummary := summarizeErrorForLog(err)
entry.Warnf("upstream execution failed: provider=%s model=%s auth=%s duration=%s err=%s", provider, model, authIdent, duration.Round(time.Millisecond), errSummary)
}
// InjectCredentials delegates per-provider HTTP request preparation when supported.
// If the registered executor for the auth provider implements RequestPreparer,
// it will be invoked to modify the request (e.g., add headers).
func (m *Manager) InjectCredentials(req *http.Request, authID string) error {
if req == nil || authID == "" {
return nil
}
m.mu.RLock()
a := m.auths[authID]
var exec ProviderExecutor
if a != nil {
exec = m.executors[executorKeyFromAuth(a)]
}
m.mu.RUnlock()
if a == nil || exec == nil {
return nil
}
if p, ok := exec.(RequestPreparer); ok && p != nil {
return p.PrepareRequest(req, a)
}
return nil
}
// PrepareHttpRequest injects provider credentials into the supplied HTTP request.
func (m *Manager) PrepareHttpRequest(ctx context.Context, auth *Auth, req *http.Request) error {
if m == nil {
return &Error{Code: "provider_not_found", Message: "manager is nil"}
}
if auth == nil {
return &Error{Code: "auth_not_found", Message: "auth is nil"}
}
if req == nil {
return &Error{Code: "invalid_request", Message: "http request is nil"}
}
if ctx != nil {
*req = *req.WithContext(ctx)
}
providerKey := executorKeyFromAuth(auth)
if providerKey == "" {
return &Error{Code: "provider_not_found", Message: "auth provider is empty"}
}
exec := m.executorFor(providerKey)
if exec == nil {
return &Error{Code: "provider_not_found", Message: "executor not registered for provider: " + providerKey}
}
preparer, ok := exec.(RequestPreparer)
if !ok || preparer == nil {
return &Error{Code: "not_supported", Message: "executor does not support http request preparation"}
}
return preparer.PrepareRequest(req, auth)
}
// NewHttpRequest constructs a new HTTP request and injects provider credentials into it.
func (m *Manager) NewHttpRequest(ctx context.Context, auth *Auth, method, targetURL string, body []byte, headers http.Header) (*http.Request, error) {
if ctx == nil {
ctx = context.Background()
}
method = strings.TrimSpace(method)
if method == "" {
method = http.MethodGet
}
var reader io.Reader
if body != nil {
reader = bytes.NewReader(body)
}
httpReq, err := http.NewRequestWithContext(ctx, method, targetURL, reader)
if err != nil {
return nil, err
}
if headers != nil {
httpReq.Header = headers.Clone()
}
if errPrepare := m.PrepareHttpRequest(ctx, auth, httpReq); errPrepare != nil {
return nil, errPrepare
}
return httpReq, nil
}
// HttpRequest injects provider credentials into the supplied HTTP request and executes it.
func (m *Manager) HttpRequest(ctx context.Context, auth *Auth, req *http.Request) (*http.Response, error) {
if m == nil {
return nil, &Error{Code: "provider_not_found", Message: "manager is nil"}
}
if auth == nil {
return nil, &Error{Code: "auth_not_found", Message: "auth is nil"}
}
if req == nil {
return nil, &Error{Code: "invalid_request", Message: "http request is nil"}
}
providerKey := executorKeyFromAuth(auth)
if providerKey == "" {
return nil, &Error{Code: "provider_not_found", Message: "auth provider is empty"}
}
exec := m.executorFor(providerKey)
if exec == nil {
return nil, &Error{Code: "provider_not_found", Message: "executor not registered for provider: " + providerKey}
}
return exec.HttpRequest(ctx, auth, req)
}