package auth import ( "context" "net/http" "strings" "time" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" ) func discardStreamChunks(ch <-chan cliproxyexecutor.StreamChunk) { if ch == nil { return } go func() { for range ch { } }() } type streamBootstrapError struct { cause error headers http.Header } func cloneHTTPHeader(headers http.Header) http.Header { if headers == nil { return nil } return headers.Clone() } func newStreamBootstrapError(err error, headers http.Header) error { if err == nil { return nil } return &streamBootstrapError{ cause: err, headers: cloneHTTPHeader(headers), } } func (e *streamBootstrapError) Error() string { if e == nil || e.cause == nil { return "" } return e.cause.Error() } func (e *streamBootstrapError) Unwrap() error { if e == nil { return nil } return e.cause } func (e *streamBootstrapError) Headers() http.Header { if e == nil { return nil } return cloneHTTPHeader(e.headers) } func streamErrorResult(headers http.Header, err error) *cliproxyexecutor.StreamResult { ch := make(chan cliproxyexecutor.StreamChunk, 1) ch <- cliproxyexecutor.StreamChunk{Err: err} close(ch) return &cliproxyexecutor.StreamResult{ Headers: cloneHTTPHeader(headers), Chunks: ch, } } func validateStreamResult(result *cliproxyexecutor.StreamResult, err error) (*cliproxyexecutor.StreamResult, error) { if err != nil { return result, err } if result == nil || result.Chunks == nil { return result, &Error{Code: "empty_stream", Message: "upstream stream has no source", Retryable: true} } return result, nil } func readStreamBootstrap(ctx context.Context, ch <-chan cliproxyexecutor.StreamChunk) ([]cliproxyexecutor.StreamChunk, bool, error) { if ch == nil { return nil, true, nil } buffered := make([]cliproxyexecutor.StreamChunk, 0, 1) for { var ( chunk cliproxyexecutor.StreamChunk ok bool ) if ctx != nil { select { case <-ctx.Done(): return nil, false, ctx.Err() case chunk, ok = <-ch: } } else { chunk, ok = <-ch } if !ok { return buffered, true, nil } if chunk.Err != nil { return nil, false, chunk.Err } buffered = append(buffered, chunk) if len(chunk.Payload) > 0 { return buffered, false, nil } } } func (m *Manager) wrapStreamResult(ctx context.Context, auth *Auth, provider, resultModel string, headers http.Header, buffered []cliproxyexecutor.StreamChunk, remaining <-chan cliproxyexecutor.StreamChunk, aliasResult OAuthModelAliasResult, ephemeralResult bool, opts cliproxyexecutor.Options) *cliproxyexecutor.StreamResult { out := make(chan cliproxyexecutor.StreamChunk) streamStart := time.Now() go func() { defer close(out) var failed bool forward := true var rewriter *StreamRewriter if aliasResult.ForceMapping && strings.TrimSpace(aliasResult.OriginalAlias) != "" { rewriter = NewStreamRewriter(StreamRewriteOptions{RewriteModel: aliasResult.OriginalAlias}) } emit := func(chunk cliproxyexecutor.StreamChunk) bool { if chunk.Err != nil && !failed { failed = true entry := logEntryWithRequestID(ctx) warnLogUpstreamFailure(ctx, entry, provider, resultModel, auth, time.Since(streamStart), chunk.Err) rerr := resultErrorFromError(chunk.Err) action, okAction := matchRequestScopedErrorAction(auth, chunk.Err, m.runtimeConfigSnapshot()) result := Result{AuthID: auth.ID, Provider: provider, Model: resultModel, Success: false, Error: rerr, Options: opts} applyRequestScopedActionToResult(action, okAction, &result) m.recordExecutionResult(ctx, result, auth, ephemeralResult) } if !forward { return false } if chunk.Err != nil { if ctx == nil { out <- chunk return true } select { case <-ctx.Done(): forward = false return false case out <- chunk: return true } } if len(chunk.Payload) == 0 { return true } payload := rewriteForceMappedStreamChunk(rewriter, chunk.Payload) if len(payload) == 0 { return true } chunk.Payload = payload if ctx == nil { out <- chunk return true } select { case <-ctx.Done(): forward = false return false case out <- chunk: return true } } for _, chunk := range buffered { if ok := emit(chunk); !ok { discardStreamChunks(remaining) return } } for chunk := range remaining { if ok := emit(chunk); !ok { discardStreamChunks(remaining) return } } if tail := finishForceMappedStreamChunks(rewriter); len(tail) > 0 { tailChunk := cliproxyexecutor.StreamChunk{Payload: tail} if !emit(tailChunk) { return } } if !failed && (ephemeralResult || claudeOAuthRequestCancellation(ctx, auth, nil) == nil) { m.recordExecutionResult(ctx, Result{AuthID: auth.ID, Provider: provider, Model: resultModel, Success: true, Options: opts}, auth, ephemeralResult) } }() return &cliproxyexecutor.StreamResult{Headers: headers, Chunks: out} } func (m *Manager) replaceHomeExecutionLifecycleAuth(lifecycle cliproxyexecutor.ExecutionLifecycle, auth *Auth) { selection, ok := lifecycle.(*HomeDispatchSelection) if !ok || selection == nil { return } m.replaceHomeSelectionAuth(selection, auth) } func (m *Manager) executeStreamWithModelPool(ctx context.Context, executor ProviderExecutor, auth *Auth, provider string, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, routeModel, executionModel string, execModels []string, pooled bool, aliasResult OAuthModelAliasResult, routing *apiKeyModelRoutingSnapshot, allowRetry bool, ephemeralResult bool, unauthorizedRefreshTried map[string]struct{}) (*cliproxyexecutor.StreamResult, error) { if executor == nil { return nil, &Error{Code: "executor_not_found", Message: "executor not registered"} } ctx = contextWithRequestedModelAlias(ctx, opts, routeModel) var lastErr error didRefreshOnUnauthorized := false if auth != nil && unauthorizedRefreshTried != nil { _, didRefreshOnUnauthorized = unauthorizedRefreshTried[auth.ID] } for idx, execModel := range execModels { resultModel := m.stateModelForExecution(auth, routeModel, execModel, pooled) execReq := req execReq.Model = execModel if executionModel != "" { execReq.Model = executionModel } execOpts := opts var errIntercept error execReq, execOpts, errIntercept = applyRequestAfterAuthInterceptor(ctx, executor, provider, execReq, execOpts, requestedModelAliasFromOptions(execOpts, routeModel)) if errIntercept != nil { return nil, errIntercept } if executionModel == "" { execReq = attachResolvedAPIKeyModelInfo(routing, execReq, auth, routeModel, execModel) } if errCtx := ctx.Err(); errCtx != nil { return nil, errCtx } entry := logEntryWithRequestID(ctx) startStream := time.Now() streamResult, errStream := executor.ExecuteStream(ctx, auth, execReq, execOpts) durationStream := time.Since(startStream) if errStream != nil { if errCtx := ctx.Err(); errCtx != nil { return nil, errCtx } if allowRetry { alreadyTried := didRefreshOnUnauthorized willAttemptHomeRefresh := ephemeralResult && !alreadyTried && auth != nil && auth.AuthKind() == AuthKindOAuth && isUnauthorizedError(errStream) refreshed, okRefresh, errRefresh := m.tryRefreshExecutionAuthAfterUnauthorized(ctx, executor, auth, errStream, alreadyTried, ephemeralResult) if willAttemptHomeRefresh { didRefreshOnUnauthorized = true if unauthorizedRefreshTried != nil { unauthorizedRefreshTried[auth.ID] = struct{}{} } } if errRefresh != nil { errStream = errRefresh warnLogUpstreamFailure(ctx, entry, provider, execModel, auth, durationStream, errStream) } else if okRefresh { auth = refreshed m.replaceHomeExecutionLifecycleAuth(execOpts.ExecutionLifecycle, auth) publishSelectedAuthMetadata(execOpts.Metadata, auth) didRefreshOnUnauthorized = true startRetry := time.Now() streamResult, errStream = executor.ExecuteStream(ctx, auth, execReq, execOpts) durationRetry := time.Since(startRetry) if errStream != nil { warnLogUpstreamFailure(ctx, entry, provider, execModel, auth, durationRetry, errStream) if errCtx := ctx.Err(); errCtx != nil { return nil, errCtx } } } else { warnLogUpstreamFailure(ctx, entry, provider, execModel, auth, durationStream, errStream) } } else { warnLogUpstreamFailure(ctx, entry, provider, execModel, auth, durationStream, errStream) } } if !ephemeralResult { if errCancel := claudeOAuthRequestCancellation(ctx, auth, errStream); errCancel != nil { return nil, errCancel } } streamResult, errStream = validateStreamResult(streamResult, errStream) if errStream != nil { rerr := resultErrorFromError(errStream) action, okAction := matchRequestScopedErrorAction(auth, errStream, m.runtimeConfigSnapshot()) result := Result{AuthID: auth.ID, Provider: provider, Model: resultModel, Success: false, Error: rerr, Options: execOpts} result.RetryAfter = retryAfterFromError(errStream) if isCredentialScopedError(errStream) { result.CredentialScope = true } applyRequestScopedActionToResult(action, okAction, &result) m.recordExecutionResult(ctx, result, auth, ephemeralResult) if okAction { if isRequestScopedStop(action, okAction) { return nil, wrapRequestStopError(errStream) } lastErr = errStream if result.CredentialScope { return nil, errStream } continue } if isRequestInvalidError(errStream) { return nil, errStream } lastErr = errStream if result.CredentialScope { return nil, errStream } continue } buffered, closed, bootstrapErr := readStreamBootstrap(ctx, streamResult.Chunks) if bootstrapErr != nil { if errCtx := ctx.Err(); errCtx != nil { discardStreamChunks(streamResult.Chunks) return nil, errCtx } if allowRetry { alreadyTried := didRefreshOnUnauthorized willAttemptHomeRefresh := ephemeralResult && !alreadyTried && auth != nil && auth.AuthKind() == AuthKindOAuth && isUnauthorizedError(bootstrapErr) refreshed, okRefresh, errRefresh := m.tryRefreshExecutionAuthAfterUnauthorized(ctx, executor, auth, bootstrapErr, alreadyTried, ephemeralResult) if willAttemptHomeRefresh { didRefreshOnUnauthorized = true if unauthorizedRefreshTried != nil { unauthorizedRefreshTried[auth.ID] = struct{}{} } } if errRefresh != nil { discardStreamChunks(streamResult.Chunks) bootstrapErr = errRefresh warnLogUpstreamFailure(ctx, entry, provider, execModel, auth, time.Since(startStream), bootstrapErr) streamResult = &cliproxyexecutor.StreamResult{} } else if okRefresh { discardStreamChunks(streamResult.Chunks) auth = refreshed m.replaceHomeExecutionLifecycleAuth(execOpts.ExecutionLifecycle, auth) publishSelectedAuthMetadata(execOpts.Metadata, auth) didRefreshOnUnauthorized = true startRetry := time.Now() retryStream, retryErr := executor.ExecuteStream(ctx, auth, execReq, execOpts) retryStream, retryErr = validateStreamResult(retryStream, retryErr) if retryErr != nil { if errCtx := ctx.Err(); errCtx != nil { return nil, errCtx } bootstrapErr = retryErr warnLogUpstreamFailure(ctx, entry, provider, execModel, auth, time.Since(startRetry), bootstrapErr) streamResult = &cliproxyexecutor.StreamResult{} } else { streamResult = retryStream buffered, closed, bootstrapErr = readStreamBootstrap(ctx, streamResult.Chunks) if bootstrapErr != nil { warnLogUpstreamFailure(ctx, entry, provider, execModel, auth, time.Since(startRetry), bootstrapErr) } } } else { warnLogUpstreamFailure(ctx, entry, provider, execModel, auth, time.Since(startStream), bootstrapErr) } } else { warnLogUpstreamFailure(ctx, entry, provider, execModel, auth, time.Since(startStream), bootstrapErr) } } if !ephemeralResult { if errCancel := claudeOAuthRequestCancellation(ctx, auth, bootstrapErr); errCancel != nil { discardStreamChunks(streamResult.Chunks) return nil, errCancel } } if bootstrapErr != nil { action, okAction := matchRequestScopedErrorAction(auth, bootstrapErr, m.runtimeConfigSnapshot()) if okAction { rerr := resultErrorFromError(bootstrapErr) result := Result{AuthID: auth.ID, Provider: provider, Model: resultModel, Success: false, Error: rerr, Options: execOpts} result.RetryAfter = retryAfterFromError(bootstrapErr) if isCredentialScopedError(bootstrapErr) { result.CredentialScope = true } applyRequestScopedActionToResult(action, okAction, &result) m.recordExecutionResult(ctx, result, auth, ephemeralResult) discardStreamChunks(streamResult.Chunks) if isRequestScopedStop(action, okAction) { return nil, wrapRequestStopError(bootstrapErr) } lastErr = bootstrapErr if result.CredentialScope { return nil, newStreamBootstrapError(bootstrapErr, streamResult.Headers) } continue } if isRequestInvalidError(bootstrapErr) { rerr := resultErrorFromError(bootstrapErr) result := Result{AuthID: auth.ID, Provider: provider, Model: resultModel, Success: false, Error: rerr, Options: execOpts} result.RetryAfter = retryAfterFromError(bootstrapErr) if isCredentialScopedError(bootstrapErr) { result.CredentialScope = true } m.recordExecutionResult(ctx, result, auth, ephemeralResult) discardStreamChunks(streamResult.Chunks) return nil, bootstrapErr } if idx < len(execModels)-1 { rerr := resultErrorFromError(bootstrapErr) result := Result{AuthID: auth.ID, Provider: provider, Model: resultModel, Success: false, Error: rerr, Options: execOpts} result.RetryAfter = retryAfterFromError(bootstrapErr) if isCredentialScopedError(bootstrapErr) { result.CredentialScope = true } m.recordExecutionResult(ctx, result, auth, ephemeralResult) discardStreamChunks(streamResult.Chunks) lastErr = bootstrapErr if result.CredentialScope { return nil, newStreamBootstrapError(bootstrapErr, streamResult.Headers) } continue } rerr := resultErrorFromError(bootstrapErr) result := Result{AuthID: auth.ID, Provider: provider, Model: resultModel, Success: false, Error: rerr, Options: execOpts} result.RetryAfter = retryAfterFromError(bootstrapErr) if isCredentialScopedError(bootstrapErr) { result.CredentialScope = true } m.recordExecutionResult(ctx, result, auth, ephemeralResult) discardStreamChunks(streamResult.Chunks) return nil, newStreamBootstrapError(bootstrapErr, streamResult.Headers) } if closed && len(buffered) == 0 { emptyErr := &Error{Code: "empty_stream", Message: "upstream stream closed before first payload", Retryable: true} warnLogUpstreamFailure(ctx, entry, provider, execModel, auth, time.Since(startStream), emptyErr) result := Result{AuthID: auth.ID, Provider: provider, Model: resultModel, Success: false, Error: emptyErr, Options: execOpts} m.recordExecutionResult(ctx, result, auth, ephemeralResult) if idx < len(execModels)-1 { lastErr = emptyErr continue } return nil, newStreamBootstrapError(emptyErr, streamResult.Headers) } remaining := streamResult.Chunks if closed { closedCh := make(chan cliproxyexecutor.StreamChunk) close(closedCh) remaining = closedCh } attemptAliasResult := resolveAttemptAliasResult(routing, auth, routeModel, execModel, aliasResult) return m.wrapStreamResult(ctx, auth.Clone(), provider, resultModel, streamResult.Headers, buffered, remaining, attemptAliasResult, ephemeralResult, execOpts), nil } if lastErr == nil { lastErr = &Error{Code: "auth_not_found", Message: "no upstream model available"} } return nil, lastErr }