Files
2026-07-13 12:35:43 +08:00

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
}
}