337 lines
11 KiB
Go
337 lines
11 KiB
Go
package handlers
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"net/url"
|
|
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces"
|
|
"golang.org/x/net/context"
|
|
)
|
|
|
|
const (
|
|
modelExecutionMetadataSourceKey = "source"
|
|
modelExecutionInternalSource = "plugin_host_model_callback"
|
|
)
|
|
|
|
type modelExecutionOptions struct {
|
|
Headers http.Header
|
|
Query url.Values
|
|
InternalSource bool
|
|
SkipInterceptorPluginID string
|
|
SkipRouterPluginID string
|
|
ForcedProvider string
|
|
AuthSelectionModel string
|
|
}
|
|
|
|
// ProtocolExecutionRequest describes a route-level model execution request with explicit protocols.
|
|
type ProtocolExecutionRequest struct {
|
|
EntryProtocol string
|
|
ExitProtocol string
|
|
ForcedProvider string
|
|
AuthSelectionModel string
|
|
Model string
|
|
Stream bool
|
|
Body []byte
|
|
Headers http.Header
|
|
Query url.Values
|
|
Alt string
|
|
}
|
|
|
|
// ModelExecutionRequest describes an internal model execution request.
|
|
type ModelExecutionRequest struct {
|
|
EntryProtocol string
|
|
ExitProtocol string
|
|
Model string
|
|
Stream bool
|
|
Body []byte
|
|
Headers http.Header
|
|
Query url.Values
|
|
Alt string
|
|
SkipInterceptorPluginID string
|
|
SkipRouterPluginID string
|
|
}
|
|
|
|
// ModelExecutionResponse describes a non-streaming internal model execution response.
|
|
type ModelExecutionResponse struct {
|
|
StatusCode int
|
|
Headers http.Header
|
|
Body []byte
|
|
}
|
|
|
|
// ModelExecutionStream describes a streaming internal model execution response.
|
|
type ModelExecutionStream struct {
|
|
StatusCode int
|
|
Headers http.Header
|
|
Chunks <-chan ModelExecutionChunk
|
|
}
|
|
|
|
// ModelExecutionChunk carries either a streaming payload or a terminal stream error.
|
|
type ModelExecutionChunk struct {
|
|
Payload []byte
|
|
Err *ModelExecutionStreamError
|
|
}
|
|
|
|
// ModelExecutionStreamError carries a JSON-friendly terminal stream error.
|
|
type ModelExecutionStreamError struct {
|
|
StatusCode int `json:"status_code"`
|
|
Message string `json:"message"`
|
|
Headers http.Header `json:"headers"`
|
|
}
|
|
|
|
// Error returns the stream error message or the HTTP status text.
|
|
func (e *ModelExecutionStreamError) Error() string {
|
|
if e == nil {
|
|
return ""
|
|
}
|
|
if e.Message != "" {
|
|
return e.Message
|
|
}
|
|
return http.StatusText(e.StatusCode)
|
|
}
|
|
|
|
// ExecuteModel executes an internal non-streaming model request.
|
|
// Host model callbacks are non-recursive for their caller: when
|
|
// skip plugin IDs are set, that plugin's interceptors and router are skipped
|
|
// for the nested model execution while other plugins may still run.
|
|
func (h *BaseAPIHandler) ExecuteModel(ctx context.Context, req ModelExecutionRequest) (ModelExecutionResponse, *interfaces.ErrorMessage) {
|
|
if req.Stream {
|
|
return ModelExecutionResponse{}, modelExecutionModeError("ExecuteModel requires Stream=false")
|
|
}
|
|
body, headers, errMsg := h.executeWithAuthManagerFormats(ctx, req.EntryProtocol, req.ExitProtocol, req.Model, cloneBytes(req.Body), req.Alt, false, modelExecutionOptions{
|
|
Headers: req.Headers,
|
|
Query: req.Query,
|
|
InternalSource: true,
|
|
SkipInterceptorPluginID: req.SkipInterceptorPluginID,
|
|
SkipRouterPluginID: req.SkipRouterPluginID,
|
|
})
|
|
if errMsg != nil {
|
|
return ModelExecutionResponse{}, errMsg
|
|
}
|
|
return ModelExecutionResponse{
|
|
StatusCode: http.StatusOK,
|
|
Headers: cloneHeader(headers),
|
|
Body: cloneBytes(body),
|
|
}, nil
|
|
}
|
|
|
|
// ExecuteModelStream executes an internal streaming model request.
|
|
// Host model callbacks are non-recursive for their caller: when
|
|
// skip plugin IDs are set, that plugin's interceptors and router are skipped
|
|
// for the nested model execution while other plugins may still run.
|
|
func (h *BaseAPIHandler) ExecuteModelStream(ctx context.Context, req ModelExecutionRequest) (ModelExecutionStream, *interfaces.ErrorMessage) {
|
|
if !req.Stream {
|
|
return ModelExecutionStream{}, modelExecutionModeError("ExecuteModelStream requires Stream=true")
|
|
}
|
|
dataChan, headers, errChan := h.executeStreamWithAuthManagerFormats(ctx, req.EntryProtocol, req.ExitProtocol, req.Model, cloneBytes(req.Body), req.Alt, false, modelExecutionOptions{
|
|
Headers: req.Headers,
|
|
Query: req.Query,
|
|
InternalSource: true,
|
|
SkipInterceptorPluginID: req.SkipInterceptorPluginID,
|
|
SkipRouterPluginID: req.SkipRouterPluginID,
|
|
})
|
|
chunks, errMsg := prepareModelExecutionStream(ctx, dataChan, errChan)
|
|
if errMsg != nil {
|
|
return ModelExecutionStream{}, errMsg
|
|
}
|
|
return ModelExecutionStream{
|
|
StatusCode: http.StatusOK,
|
|
Headers: cloneHeader(headers),
|
|
Chunks: chunks,
|
|
}, nil
|
|
}
|
|
|
|
// ExecuteProtocolWithAuthManager executes a route-level non-streaming request with explicit protocols.
|
|
func (h *BaseAPIHandler) ExecuteProtocolWithAuthManager(ctx context.Context, req ProtocolExecutionRequest) (ModelExecutionResponse, *interfaces.ErrorMessage) {
|
|
if req.Stream {
|
|
return ModelExecutionResponse{}, modelExecutionModeError("ExecuteProtocolWithAuthManager requires Stream=false")
|
|
}
|
|
body, headers, errMsg := h.executeWithAuthManagerFormats(ctx, req.EntryProtocol, req.ExitProtocol, req.Model, cloneBytes(req.Body), req.Alt, false, modelExecutionOptions{
|
|
Headers: req.Headers,
|
|
Query: req.Query,
|
|
ForcedProvider: req.ForcedProvider,
|
|
AuthSelectionModel: req.AuthSelectionModel,
|
|
})
|
|
if errMsg != nil {
|
|
return ModelExecutionResponse{}, errMsg
|
|
}
|
|
return ModelExecutionResponse{
|
|
StatusCode: http.StatusOK,
|
|
Headers: cloneHeader(headers),
|
|
Body: cloneBytes(body),
|
|
}, nil
|
|
}
|
|
|
|
// ExecuteProtocolStreamWithAuthManager executes a route-level streaming request with explicit protocols.
|
|
func (h *BaseAPIHandler) ExecuteProtocolStreamWithAuthManager(ctx context.Context, req ProtocolExecutionRequest) (ModelExecutionStream, *interfaces.ErrorMessage) {
|
|
if !req.Stream {
|
|
return ModelExecutionStream{}, modelExecutionModeError("ExecuteProtocolStreamWithAuthManager requires Stream=true")
|
|
}
|
|
dataChan, headers, errChan := h.executeStreamWithAuthManagerFormats(ctx, req.EntryProtocol, req.ExitProtocol, req.Model, cloneBytes(req.Body), req.Alt, false, modelExecutionOptions{
|
|
Headers: req.Headers,
|
|
Query: req.Query,
|
|
ForcedProvider: req.ForcedProvider,
|
|
AuthSelectionModel: req.AuthSelectionModel,
|
|
})
|
|
chunks, errMsg := prepareModelExecutionStream(ctx, dataChan, errChan)
|
|
if errMsg != nil {
|
|
return ModelExecutionStream{}, errMsg
|
|
}
|
|
return ModelExecutionStream{
|
|
StatusCode: http.StatusOK,
|
|
Headers: cloneHeader(headers),
|
|
Chunks: chunks,
|
|
}, nil
|
|
}
|
|
|
|
func modelExecutionModeError(message string) *interfaces.ErrorMessage {
|
|
return &interfaces.ErrorMessage{StatusCode: http.StatusBadRequest, Error: errors.New(message)}
|
|
}
|
|
|
|
func modelExecutionResponseProtocol(entryProtocol, exitProtocol string) string {
|
|
if exitProtocol == "" {
|
|
return entryProtocol
|
|
}
|
|
return exitProtocol
|
|
}
|
|
|
|
func modelExecutionHeaders(ctx context.Context, headers http.Header) http.Header {
|
|
if len(headers) > 0 {
|
|
return cloneHeader(headers)
|
|
}
|
|
return headersFromContext(ctx)
|
|
}
|
|
|
|
// modelExecutionQuery prefers an explicitly provided query and otherwise falls
|
|
// back to the inbound query embedded in the request context. This lets model
|
|
// routers observe query parameters for plain HTTP requests even when callers
|
|
// do not populate execOptions.Query (mirrors modelExecutionHeaders).
|
|
func modelExecutionQuery(ctx context.Context, query url.Values) url.Values {
|
|
if len(query) > 0 {
|
|
return cloneURLValues(query)
|
|
}
|
|
return queryFromContext(ctx)
|
|
}
|
|
|
|
func cloneURLValues(src url.Values) url.Values {
|
|
if src == nil {
|
|
return nil
|
|
}
|
|
dst := make(url.Values, len(src))
|
|
for key, values := range src {
|
|
dst[key] = append([]string(nil), values...)
|
|
}
|
|
return dst
|
|
}
|
|
|
|
func addModelExecutionSourceMetadata(meta map[string]any, internalSource bool) {
|
|
if !internalSource || meta == nil {
|
|
return
|
|
}
|
|
meta[modelExecutionMetadataSourceKey] = modelExecutionInternalSource
|
|
}
|
|
|
|
func prepareModelExecutionStream(ctx context.Context, dataChan <-chan []byte, errChan <-chan *interfaces.ErrorMessage) (<-chan ModelExecutionChunk, *interfaces.ErrorMessage) {
|
|
pending, nextDataChan, nextErrChan, errMsg := receiveInitialModelExecutionChunk(ctx, dataChan, errChan)
|
|
if errMsg != nil {
|
|
return nil, errMsg
|
|
}
|
|
return wrapModelExecutionChunks(ctx, nextDataChan, nextErrChan, pending), nil
|
|
}
|
|
|
|
func receiveInitialModelExecutionChunk(ctx context.Context, dataChan <-chan []byte, errChan <-chan *interfaces.ErrorMessage) ([]ModelExecutionChunk, <-chan []byte, <-chan *interfaces.ErrorMessage, *interfaces.ErrorMessage) {
|
|
var done <-chan struct{}
|
|
if ctx != nil {
|
|
done = ctx.Done()
|
|
}
|
|
for dataChan != nil || errChan != nil {
|
|
select {
|
|
case payload, ok := <-dataChan:
|
|
if !ok {
|
|
dataChan = nil
|
|
continue
|
|
}
|
|
return []ModelExecutionChunk{{Payload: cloneBytes(payload)}}, dataChan, errChan, nil
|
|
case errMsg, ok := <-errChan:
|
|
if !ok {
|
|
errChan = nil
|
|
continue
|
|
}
|
|
if errMsg != nil {
|
|
return nil, dataChan, errChan, errMsg
|
|
}
|
|
case <-done:
|
|
return nil, dataChan, errChan, nil
|
|
}
|
|
}
|
|
return nil, dataChan, errChan, nil
|
|
}
|
|
|
|
func wrapModelExecutionChunks(ctx context.Context, dataChan <-chan []byte, errChan <-chan *interfaces.ErrorMessage, pending []ModelExecutionChunk) <-chan ModelExecutionChunk {
|
|
chunks := make(chan ModelExecutionChunk)
|
|
go func() {
|
|
defer close(chunks)
|
|
var done <-chan struct{}
|
|
if ctx != nil {
|
|
done = ctx.Done()
|
|
}
|
|
for _, chunk := range pending {
|
|
if !sendModelExecutionChunk(ctx, chunks, chunk) {
|
|
return
|
|
}
|
|
}
|
|
for dataChan != nil || errChan != nil {
|
|
select {
|
|
case <-done:
|
|
return
|
|
case payload, ok := <-dataChan:
|
|
if !ok {
|
|
dataChan = nil
|
|
continue
|
|
}
|
|
if !sendModelExecutionChunk(ctx, chunks, ModelExecutionChunk{Payload: cloneBytes(payload)}) {
|
|
return
|
|
}
|
|
case errMsg, ok := <-errChan:
|
|
if !ok {
|
|
errChan = nil
|
|
continue
|
|
}
|
|
if errMsg != nil {
|
|
_ = sendModelExecutionChunk(ctx, chunks, ModelExecutionChunk{Err: modelExecutionStreamErrorFromMessage(errMsg)})
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
return chunks
|
|
}
|
|
|
|
func modelExecutionStreamErrorFromMessage(errMsg *interfaces.ErrorMessage) *ModelExecutionStreamError {
|
|
if errMsg == nil {
|
|
return nil
|
|
}
|
|
message := ""
|
|
if errMsg.Error != nil {
|
|
message = errMsg.Error.Error()
|
|
}
|
|
return &ModelExecutionStreamError{
|
|
StatusCode: errMsg.StatusCode,
|
|
Message: message,
|
|
Headers: cloneHeader(errMsg.Addon),
|
|
}
|
|
}
|
|
|
|
func sendModelExecutionChunk(ctx context.Context, chunks chan<- ModelExecutionChunk, chunk ModelExecutionChunk) bool {
|
|
if ctx == nil {
|
|
chunks <- chunk
|
|
return true
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return false
|
|
case chunks <- chunk:
|
|
return true
|
|
}
|
|
}
|