Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
455
backend/sdk/cliproxy/auth/conductor_stream.go
Normal file
455
backend/sdk/cliproxy/auth/conductor_stream.go
Normal file
|
|
@ -0,0 +1,455 @@
|
|||
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
|
||||
}
|
||||
Loading…
Reference in a new issue