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

455 lines
16 KiB
Go

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
}