1098 lines
34 KiB
Go
1098 lines
34 KiB
Go
package relayconvert
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/QuantumNous/new-api/relaykit/dto"
|
|
"github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta"
|
|
geminichat "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/gemini_chat"
|
|
oaichat "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/oai_chat"
|
|
kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil"
|
|
"github.com/QuantumNous/new-api/relaykit/types"
|
|
)
|
|
|
|
type ResponseConverterFunc func(c context.Context, info convmeta.Meta, response any) (any, *dto.Usage, error)
|
|
|
|
type ResponseStreamConverterFunc func(c context.Context, info convmeta.Meta, response any) (any, *dto.Usage, error)
|
|
|
|
type ResponseStreamStateFactory func(options ResponseStreamOptions) any
|
|
|
|
type ResponseStreamChunkConverterFunc func(c context.Context, info convmeta.Meta, response any, state any) ([]any, *dto.Usage, error)
|
|
|
|
type ResponseStreamFinalizerFunc func(c context.Context, info convmeta.Meta, state any) ([]any, *dto.Usage, error)
|
|
|
|
type ResponseConverterQuality string
|
|
|
|
const (
|
|
ResponseConverterQualityGood ResponseConverterQuality = "good"
|
|
ResponseConverterQualityFair ResponseConverterQuality = "fair"
|
|
ResponseConverterQualityDiscouraged ResponseConverterQuality = "discouraged"
|
|
)
|
|
|
|
type ResponseStep struct {
|
|
Converter string
|
|
From types.RelayFormat
|
|
To types.RelayFormat
|
|
}
|
|
|
|
type ResponseResult struct {
|
|
Value any
|
|
Usage *dto.Usage
|
|
From types.RelayFormat
|
|
To types.RelayFormat
|
|
Converter string
|
|
Quality ResponseConverterQuality
|
|
Steps []ResponseStep
|
|
Stream bool
|
|
}
|
|
|
|
type ResponseConverterSpec struct {
|
|
ID string
|
|
From types.RelayFormat
|
|
To types.RelayFormat
|
|
Quality ResponseConverterQuality
|
|
Convert ResponseConverterFunc
|
|
ConvertStream ResponseStreamConverterFunc
|
|
NewStreamState ResponseStreamStateFactory
|
|
ConvertStreamChunk ResponseStreamChunkConverterFunc
|
|
FinalizeStream ResponseStreamFinalizerFunc
|
|
StepConverters []string
|
|
}
|
|
|
|
type responseConverterRoute struct {
|
|
from types.RelayFormat
|
|
to types.RelayFormat
|
|
}
|
|
|
|
type ResponseStreamOptions struct {
|
|
ID string
|
|
Model string
|
|
Created int64
|
|
IncludeUsage bool
|
|
}
|
|
|
|
type ResponseStreamState struct {
|
|
From types.RelayFormat
|
|
To types.RelayFormat
|
|
Converter string
|
|
Quality ResponseConverterQuality
|
|
Steps []ResponseStep
|
|
|
|
specs []ResponseConverterSpec
|
|
stepStates []any
|
|
usage *dto.Usage
|
|
}
|
|
|
|
const (
|
|
ResponseConverterOAIChatToOAIResponses = "oai_chat_to_oai_responses_resp"
|
|
ResponseConverterOAIResponsesToOAIChat = "oai_responses_to_oai_chat_resp"
|
|
ResponseConverterOAIChatToClaudeMessages = "oai_chat_to_claude_messages_resp"
|
|
ResponseConverterOAIChatToGeminiChat = "oai_chat_to_gemini_chat_resp"
|
|
ResponseConverterClaudeMessagesToOAIChat = "claude_messages_to_oai_chat_resp"
|
|
ResponseConverterGeminiChatToOAIChat = "gemini_chat_to_oai_chat_resp"
|
|
|
|
responseConverterClaudeToGemini = "claude_messages_to_gemini_chat_resp"
|
|
responseConverterClaudeToResponses = "claude_messages_to_oai_responses_resp"
|
|
responseConverterGeminiToClaude = "gemini_chat_to_claude_messages_resp"
|
|
responseConverterGeminiToResponses = "gemini_chat_to_oai_responses_resp"
|
|
responseConverterResponsesToClaude = "oai_responses_to_claude_messages_resp"
|
|
responseConverterResponsesToGemini = "oai_responses_to_gemini_chat_resp"
|
|
)
|
|
|
|
var (
|
|
responseConverterMu sync.RWMutex
|
|
responseConverters = make(map[string]ResponseConverterSpec)
|
|
responseConverterAliases = make(map[string]string)
|
|
responseConverterRoutes = make(map[responseConverterRoute]string)
|
|
)
|
|
|
|
func registerBuiltinResponseConverter(spec ResponseConverterSpec) {
|
|
spec.ID = strings.TrimSpace(spec.ID)
|
|
if spec.ID == "" {
|
|
panic("response converter ID is required")
|
|
}
|
|
if spec.From == "" || spec.To == "" {
|
|
panic(fmt.Sprintf("response converter %q must declare from and to formats", spec.ID))
|
|
}
|
|
if spec.Quality == "" {
|
|
panic(fmt.Sprintf("response converter %q must declare quality", spec.ID))
|
|
}
|
|
if spec.Convert == nil &&
|
|
spec.ConvertStream == nil &&
|
|
spec.ConvertStreamChunk == nil &&
|
|
len(spec.StepConverters) == 0 {
|
|
panic(fmt.Sprintf("response converter %q must declare convert, stream convert, or step converters", spec.ID))
|
|
}
|
|
if len(spec.StepConverters) > 0 &&
|
|
(spec.Convert != nil || spec.ConvertStream != nil || spec.NewStreamState != nil || spec.ConvertStreamChunk != nil || spec.FinalizeStream != nil) {
|
|
panic(fmt.Sprintf("response converter %q cannot declare direct implementations and step converters together", spec.ID))
|
|
}
|
|
if _, exists := responseConverters[spec.ID]; exists {
|
|
panic(fmt.Sprintf("response converter %q is already registered", spec.ID))
|
|
}
|
|
route := responseConverterRoute{from: spec.From, to: spec.To}
|
|
if existingID, exists := responseConverterRoutes[route]; exists {
|
|
panic(fmt.Sprintf("response converter route from %s to %s is already registered by %q", spec.From, spec.To, existingID))
|
|
}
|
|
|
|
if len(spec.StepConverters) > 0 {
|
|
stepConverters := make([]string, 0, len(spec.StepConverters))
|
|
current := spec.From
|
|
for _, converterID := range spec.StepConverters {
|
|
step, ok := responseConverters[converterID]
|
|
if !ok {
|
|
panic(fmt.Sprintf("response converter %q references unknown step converter %q", spec.ID, converterID))
|
|
}
|
|
if len(step.StepConverters) > 0 {
|
|
panic(fmt.Sprintf("response converter %q step %q must be a direct converter", spec.ID, converterID))
|
|
}
|
|
if step.From != current {
|
|
panic(fmt.Sprintf("response converter %q step %q expects %s after %s", spec.ID, converterID, step.From, current))
|
|
}
|
|
stepConverters = append(stepConverters, converterID)
|
|
current = step.To
|
|
}
|
|
if current != spec.To {
|
|
panic(fmt.Sprintf("response converter %q ends at %s, expected %s", spec.ID, current, spec.To))
|
|
}
|
|
spec.StepConverters = stepConverters
|
|
}
|
|
|
|
responseConverters[spec.ID] = spec
|
|
responseConverterRoutes[route] = spec.ID
|
|
}
|
|
|
|
func registerResponseConverterAlias(alias string, converter string) {
|
|
alias = strings.TrimSpace(alias)
|
|
converter = strings.TrimSpace(converter)
|
|
if alias == "" {
|
|
panic("response converter alias is required")
|
|
}
|
|
if converter == "" {
|
|
panic(fmt.Sprintf("response converter alias %q target is required", alias))
|
|
}
|
|
if alias == converter {
|
|
return
|
|
}
|
|
if _, exists := responseConverters[alias]; exists {
|
|
panic(fmt.Sprintf("response converter alias %q conflicts with registered converter", alias))
|
|
}
|
|
if _, exists := responseConverters[converter]; !exists {
|
|
panic(fmt.Sprintf("response converter alias %q references unknown converter %q", alias, converter))
|
|
}
|
|
if existing, exists := responseConverterAliases[alias]; exists && existing != converter {
|
|
panic(fmt.Sprintf("response converter alias %q is already registered for %q", alias, existing))
|
|
}
|
|
responseConverterAliases[alias] = converter
|
|
}
|
|
|
|
func LookupResponseConverter(converter string) (ResponseConverterSpec, bool) {
|
|
responseConverterMu.RLock()
|
|
defer responseConverterMu.RUnlock()
|
|
|
|
converterID := resolveResponseConverterID(converter)
|
|
spec, ok := responseConverters[converterID]
|
|
if !ok {
|
|
return ResponseConverterSpec{}, false
|
|
}
|
|
return cloneResponseConverterSpec(spec), true
|
|
}
|
|
|
|
func ConvertResponse(c context.Context, info convmeta.Meta, target types.RelayFormat, response any) (*ResponseResult, error) {
|
|
from, err := inferResponseRelayFormat(response)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if target == "" {
|
|
return nil, errors.New("target relay format is required")
|
|
}
|
|
if from == target {
|
|
return &ResponseResult{
|
|
Value: response,
|
|
Usage: canonicalUsageFromResponse(response),
|
|
From: from,
|
|
To: target,
|
|
Stream: false,
|
|
}, nil
|
|
}
|
|
|
|
spec, ok := lookupResponseRoute(from, target)
|
|
if !ok {
|
|
return nil, fmt.Errorf("response converter from %s to %s is not registered", from, target)
|
|
}
|
|
return executeResponseSpec(c, info, from, target, response, spec)
|
|
}
|
|
|
|
func ConvertResponseByID(c context.Context, info convmeta.Meta, converter string, response any) (*ResponseResult, error) {
|
|
from, err := inferResponseRelayFormat(response)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
spec, ok := LookupResponseConverter(converter)
|
|
if !ok {
|
|
return nil, fmt.Errorf("response converter %q is not registered", strings.TrimSpace(converter))
|
|
}
|
|
if spec.From != "" && spec.From != from {
|
|
return nil, fmt.Errorf("response converter %q expects %s response, got %s", spec.ID, spec.From, from)
|
|
}
|
|
return executeResponseSpec(c, info, from, spec.To, response, spec)
|
|
}
|
|
|
|
func ConvertStreamResponse(c context.Context, info convmeta.Meta, target types.RelayFormat, response any) (*ResponseResult, error) {
|
|
from, err := inferResponseRelayFormat(response)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if target == "" {
|
|
return nil, errors.New("target relay format is required")
|
|
}
|
|
if from == target {
|
|
return &ResponseResult{
|
|
Value: response,
|
|
Usage: canonicalUsageFromResponse(response),
|
|
From: from,
|
|
To: target,
|
|
Stream: true,
|
|
}, nil
|
|
}
|
|
|
|
spec, ok := lookupResponseRoute(from, target)
|
|
if !ok {
|
|
return nil, fmt.Errorf("response converter from %s to %s is not registered", from, target)
|
|
}
|
|
return executeStatelessStreamResponseSpec(c, info, from, target, response, spec)
|
|
}
|
|
|
|
func NewResponseStreamState(from types.RelayFormat, target types.RelayFormat, options ResponseStreamOptions) (*ResponseStreamState, error) {
|
|
if from == "" {
|
|
return nil, errors.New("source relay format is required")
|
|
}
|
|
if target == "" {
|
|
return nil, errors.New("target relay format is required")
|
|
}
|
|
if from == target {
|
|
return &ResponseStreamState{
|
|
From: from,
|
|
To: target,
|
|
}, nil
|
|
}
|
|
|
|
spec, ok := lookupResponseRoute(from, target)
|
|
if !ok {
|
|
return nil, fmt.Errorf("response converter from %s to %s is not registered", from, target)
|
|
}
|
|
return newResponseStreamStateFromSpec(from, target, options, spec)
|
|
}
|
|
|
|
func NewResponseStreamStateByID(converter string, options ResponseStreamOptions) (*ResponseStreamState, error) {
|
|
spec, ok := LookupResponseConverter(converter)
|
|
if !ok {
|
|
return nil, fmt.Errorf("response converter %q is not registered", strings.TrimSpace(converter))
|
|
}
|
|
return newResponseStreamStateFromSpec(spec.From, spec.To, options, spec)
|
|
}
|
|
|
|
func ConvertStreamResponseChunk(c context.Context, info convmeta.Meta, state *ResponseStreamState, response any) ([]ResponseResult, error) {
|
|
if state == nil {
|
|
return nil, errors.New("response stream state is required")
|
|
}
|
|
from, err := inferResponseRelayFormat(response)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if from != state.From {
|
|
return nil, fmt.Errorf("response stream converter %q expects %s response, got %s", state.Converter, state.From, from)
|
|
}
|
|
if state.From == state.To {
|
|
usage := canonicalUsageFromResponse(response)
|
|
state.rememberUsage(usage)
|
|
return responseStreamResults(state, streamValuesFromAny(response), usage), nil
|
|
}
|
|
|
|
values, usage, err := executeResponseStreamSteps(c, info, state, []any{response}, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
state.rememberUsage(usage)
|
|
return responseStreamResults(state, values, usage), nil
|
|
}
|
|
|
|
func FinalizeStreamResponse(c context.Context, info convmeta.Meta, state *ResponseStreamState) ([]ResponseResult, error) {
|
|
if state == nil {
|
|
return nil, errors.New("response stream state is required")
|
|
}
|
|
if state.From == state.To {
|
|
return nil, nil
|
|
}
|
|
|
|
if state.To == types.RelayFormatClaude && info != nil {
|
|
claudeInfo := info.EnsureClaudeConvertInfo()
|
|
if claudeInfo.Usage == nil {
|
|
claudeInfo.Usage = state.Usage()
|
|
}
|
|
}
|
|
|
|
values := make([]any, 0)
|
|
var usage *dto.Usage
|
|
for i, spec := range state.specs {
|
|
finalValues, stepUsage, err := finalizeResponseStreamStep(c, info, spec, state.stepStates[i])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if stepUsage != nil {
|
|
usage = stepUsage
|
|
state.rememberUsage(stepUsage)
|
|
}
|
|
if len(finalValues) == 0 {
|
|
continue
|
|
}
|
|
current, currentUsage, err := executeResponseStreamSteps(c, info, state, finalValues, i+1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if currentUsage != nil {
|
|
usage = currentUsage
|
|
state.rememberUsage(currentUsage)
|
|
}
|
|
values = append(values, current...)
|
|
}
|
|
return responseStreamResults(state, values, usage), nil
|
|
}
|
|
|
|
func (s *ResponseStreamState) Usage() *dto.Usage {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
if s.usage != nil {
|
|
return s.usage
|
|
}
|
|
for _, state := range s.stepStates {
|
|
switch typed := state.(type) {
|
|
case *ChatToResponsesStreamState:
|
|
if typed.Usage != nil {
|
|
return typed.Usage
|
|
}
|
|
case *ResponsesToChatStreamState:
|
|
if typed.Usage != nil {
|
|
return typed.Usage
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *ResponseStreamState) SetUsage(usage *dto.Usage) {
|
|
if s == nil || usage == nil {
|
|
return
|
|
}
|
|
s.usage = usage
|
|
for _, state := range s.stepStates {
|
|
switch typed := state.(type) {
|
|
case *ChatToResponsesStreamState:
|
|
typed.Usage = UsageFromChatUsage(usage)
|
|
case *ResponsesToChatStreamState:
|
|
typed.Usage = usage
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *ResponseStreamState) UsageText() string {
|
|
if s == nil {
|
|
return ""
|
|
}
|
|
for _, state := range s.stepStates {
|
|
switch typed := state.(type) {
|
|
case interface{ UsageText() string }:
|
|
if text := typed.UsageText(); text != "" {
|
|
return text
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func executeResponseSpec(c context.Context, info convmeta.Meta, from types.RelayFormat, target types.RelayFormat, response any, spec ResponseConverterSpec) (*ResponseResult, error) {
|
|
steps, err := expandResponseConverterSteps(spec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return executeResponseSteps(c, info, from, target, response, spec.ID, spec.Quality, steps)
|
|
}
|
|
|
|
func executeResponseSteps(c context.Context, info convmeta.Meta, from types.RelayFormat, target types.RelayFormat, response any, converter string, quality ResponseConverterQuality, specs []ResponseConverterSpec) (*ResponseResult, error) {
|
|
current := response
|
|
var usage *dto.Usage
|
|
steps := make([]ResponseStep, 0, len(specs))
|
|
for _, spec := range specs {
|
|
var step ResponseStep
|
|
var err error
|
|
current, usage, step, err = executeResponseStep(c, info, spec, current)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
steps = append(steps, step)
|
|
}
|
|
|
|
converters := make([]string, 0, len(steps))
|
|
for _, step := range steps {
|
|
converters = append(converters, step.Converter)
|
|
}
|
|
if converter == "" {
|
|
converter = strings.Join(converters, ",")
|
|
}
|
|
return &ResponseResult{
|
|
Value: current,
|
|
Usage: usage,
|
|
From: from,
|
|
To: target,
|
|
Converter: converter,
|
|
Quality: quality,
|
|
Steps: steps,
|
|
Stream: false,
|
|
}, nil
|
|
}
|
|
|
|
func executeResponseStep(c context.Context, info convmeta.Meta, spec ResponseConverterSpec, response any) (any, *dto.Usage, ResponseStep, error) {
|
|
if spec.Convert == nil {
|
|
return nil, nil, ResponseStep{}, fmt.Errorf("response converter %q has no non-stream implementation", spec.ID)
|
|
}
|
|
|
|
value, usage, err := spec.Convert(c, info, response)
|
|
if err != nil {
|
|
return nil, nil, ResponseStep{}, err
|
|
}
|
|
return value, usage, ResponseStep{
|
|
Converter: spec.ID,
|
|
From: spec.From,
|
|
To: spec.To,
|
|
}, nil
|
|
}
|
|
|
|
func executeStatelessStreamResponseSpec(c context.Context, info convmeta.Meta, from types.RelayFormat, target types.RelayFormat, response any, spec ResponseConverterSpec) (*ResponseResult, error) {
|
|
steps, err := expandResponseConverterSteps(spec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
current := response
|
|
var usage *dto.Usage
|
|
resultSteps := make([]ResponseStep, 0, len(steps))
|
|
for _, step := range steps {
|
|
if step.ConvertStream == nil {
|
|
return nil, fmt.Errorf("response converter %q has no stream implementation", step.ID)
|
|
}
|
|
var err error
|
|
current, usage, err = step.ConvertStream(c, info, current)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resultSteps = append(resultSteps, ResponseStep{
|
|
Converter: step.ID,
|
|
From: step.From,
|
|
To: step.To,
|
|
})
|
|
}
|
|
return &ResponseResult{
|
|
Value: current,
|
|
Usage: usage,
|
|
From: from,
|
|
To: target,
|
|
Converter: spec.ID,
|
|
Quality: spec.Quality,
|
|
Steps: resultSteps,
|
|
Stream: true,
|
|
}, nil
|
|
}
|
|
|
|
func newResponseStreamStateFromSpec(from types.RelayFormat, target types.RelayFormat, options ResponseStreamOptions, spec ResponseConverterSpec) (*ResponseStreamState, error) {
|
|
steps, err := expandResponseConverterSteps(spec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stepStates := make([]any, len(steps))
|
|
resultSteps := make([]ResponseStep, 0, len(steps))
|
|
for i, step := range steps {
|
|
if step.NewStreamState != nil {
|
|
stepStates[i] = step.NewStreamState(options)
|
|
}
|
|
resultSteps = append(resultSteps, ResponseStep{
|
|
Converter: step.ID,
|
|
From: step.From,
|
|
To: step.To,
|
|
})
|
|
}
|
|
return &ResponseStreamState{
|
|
From: from,
|
|
To: target,
|
|
Converter: spec.ID,
|
|
Quality: spec.Quality,
|
|
Steps: resultSteps,
|
|
specs: steps,
|
|
stepStates: stepStates,
|
|
}, nil
|
|
}
|
|
|
|
func executeResponseStreamSteps(c context.Context, info convmeta.Meta, state *ResponseStreamState, values []any, start int) ([]any, *dto.Usage, error) {
|
|
current := values
|
|
var usage *dto.Usage
|
|
for i := start; i < len(state.specs); i++ {
|
|
spec := state.specs[i]
|
|
next := make([]any, 0)
|
|
for _, value := range current {
|
|
prepareResponseStreamInfo(info, spec)
|
|
stepValues, stepUsage, err := executeResponseStreamStep(c, info, spec, state.stepStates[i], value)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if stepUsage != nil {
|
|
usage = stepUsage
|
|
state.rememberUsage(stepUsage)
|
|
}
|
|
next = append(next, stepValues...)
|
|
}
|
|
current = next
|
|
if len(current) == 0 {
|
|
return nil, usage, nil
|
|
}
|
|
}
|
|
return current, usage, nil
|
|
}
|
|
|
|
func prepareResponseStreamInfo(info convmeta.Meta, spec ResponseConverterSpec) {
|
|
if info == nil {
|
|
return
|
|
}
|
|
if spec.From != types.RelayFormatOpenAI {
|
|
return
|
|
}
|
|
if spec.To != types.RelayFormatClaude && spec.To != types.RelayFormatGemini {
|
|
return
|
|
}
|
|
info.IncrSendResponseCount()
|
|
}
|
|
|
|
func executeResponseStreamStep(c context.Context, info convmeta.Meta, spec ResponseConverterSpec, state any, response any) ([]any, *dto.Usage, error) {
|
|
if spec.ConvertStreamChunk != nil {
|
|
return spec.ConvertStreamChunk(c, info, response, state)
|
|
}
|
|
if spec.ConvertStream == nil {
|
|
return nil, nil, fmt.Errorf("response converter %q has no stream implementation", spec.ID)
|
|
}
|
|
value, usage, err := spec.ConvertStream(c, info, response)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return streamValuesFromAny(value), usage, nil
|
|
}
|
|
|
|
func finalizeResponseStreamStep(c context.Context, info convmeta.Meta, spec ResponseConverterSpec, state any) ([]any, *dto.Usage, error) {
|
|
if spec.FinalizeStream == nil {
|
|
return nil, nil, nil
|
|
}
|
|
return spec.FinalizeStream(c, info, state)
|
|
}
|
|
|
|
func (s *ResponseStreamState) rememberUsage(usage *dto.Usage) {
|
|
if s != nil && usage != nil {
|
|
s.usage = usage
|
|
}
|
|
}
|
|
|
|
func responseStreamResults(state *ResponseStreamState, values []any, usage *dto.Usage) []ResponseResult {
|
|
if state == nil || len(values) == 0 {
|
|
return nil
|
|
}
|
|
results := make([]ResponseResult, 0, len(values))
|
|
for _, value := range values {
|
|
results = append(results, ResponseResult{
|
|
Value: value,
|
|
Usage: usage,
|
|
From: state.From,
|
|
To: state.To,
|
|
Converter: state.Converter,
|
|
Quality: state.Quality,
|
|
Steps: append([]ResponseStep{}, state.Steps...),
|
|
Stream: true,
|
|
})
|
|
}
|
|
return results
|
|
}
|
|
|
|
func streamValuesFromAny(value any) []any {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
rv := reflect.ValueOf(value)
|
|
if rv.Kind() == reflect.Pointer && rv.IsNil() {
|
|
return nil
|
|
}
|
|
if rv.Kind() != reflect.Slice && rv.Kind() != reflect.Array {
|
|
return []any{value}
|
|
}
|
|
if rv.Type().Elem().Kind() == reflect.Uint8 {
|
|
return []any{value}
|
|
}
|
|
values := make([]any, 0, rv.Len())
|
|
for i := 0; i < rv.Len(); i++ {
|
|
item := rv.Index(i)
|
|
if item.Kind() == reflect.Pointer && item.IsNil() {
|
|
continue
|
|
}
|
|
values = append(values, item.Interface())
|
|
}
|
|
return values
|
|
}
|
|
|
|
func expandResponseConverterSteps(spec ResponseConverterSpec) ([]ResponseConverterSpec, error) {
|
|
if len(spec.StepConverters) == 0 {
|
|
if spec.Convert == nil && spec.ConvertStream == nil && spec.ConvertStreamChunk == nil {
|
|
return nil, fmt.Errorf("response converter %q has no registered implementation", spec.ID)
|
|
}
|
|
return []ResponseConverterSpec{spec}, nil
|
|
}
|
|
|
|
steps := make([]ResponseConverterSpec, 0, len(spec.StepConverters))
|
|
current := spec.From
|
|
for _, converterID := range spec.StepConverters {
|
|
step, ok := LookupResponseConverter(converterID)
|
|
if !ok {
|
|
return nil, fmt.Errorf("response converter %q references missing step converter %q", spec.ID, converterID)
|
|
}
|
|
if len(step.StepConverters) > 0 {
|
|
return nil, fmt.Errorf("response converter %q step %q is not a direct converter", spec.ID, converterID)
|
|
}
|
|
if step.From != current {
|
|
return nil, fmt.Errorf("response converter %q step %q expects %s response, got %s", spec.ID, converterID, step.From, current)
|
|
}
|
|
steps = append(steps, step)
|
|
current = step.To
|
|
}
|
|
if current != spec.To {
|
|
return nil, fmt.Errorf("response converter %q ends at %s, expected %s", spec.ID, current, spec.To)
|
|
}
|
|
return steps, nil
|
|
}
|
|
|
|
func lookupResponseRoute(from types.RelayFormat, to types.RelayFormat) (ResponseConverterSpec, bool) {
|
|
responseConverterMu.RLock()
|
|
defer responseConverterMu.RUnlock()
|
|
|
|
converterID, ok := responseConverterRoutes[responseConverterRoute{from: from, to: to}]
|
|
if !ok {
|
|
return ResponseConverterSpec{}, false
|
|
}
|
|
spec, ok := responseConverters[converterID]
|
|
return cloneResponseConverterSpec(spec), ok
|
|
}
|
|
|
|
func resolveResponseConverterID(converter string) string {
|
|
converter = strings.TrimSpace(converter)
|
|
if canonical, ok := responseConverterAliases[converter]; ok {
|
|
return canonical
|
|
}
|
|
return converter
|
|
}
|
|
|
|
func cloneResponseConverterSpec(spec ResponseConverterSpec) ResponseConverterSpec {
|
|
if len(spec.StepConverters) > 0 {
|
|
spec.StepConverters = append([]string{}, spec.StepConverters...)
|
|
}
|
|
return spec
|
|
}
|
|
|
|
func inferResponseRelayFormat(response any) (types.RelayFormat, error) {
|
|
if isNilResponse(response) {
|
|
return "", errors.New("response is nil")
|
|
}
|
|
switch response.(type) {
|
|
case *dto.OpenAITextResponse, dto.OpenAITextResponse, *dto.ChatCompletionsStreamResponse, dto.ChatCompletionsStreamResponse:
|
|
return types.RelayFormatOpenAI, nil
|
|
case *dto.OpenAIResponsesResponse, dto.OpenAIResponsesResponse, *dto.ResponsesStreamResponse, dto.ResponsesStreamResponse:
|
|
return types.RelayFormatOpenAIResponses, nil
|
|
case *dto.ClaudeResponse, dto.ClaudeResponse:
|
|
return types.RelayFormatClaude, nil
|
|
case *dto.GeminiChatResponse, dto.GeminiChatResponse:
|
|
return types.RelayFormatGemini, nil
|
|
default:
|
|
return "", fmt.Errorf("unsupported response type %T", response)
|
|
}
|
|
}
|
|
|
|
func isNilResponse(response any) bool {
|
|
if response == nil {
|
|
return true
|
|
}
|
|
value := reflect.ValueOf(response)
|
|
switch value.Kind() {
|
|
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
|
|
return value.IsNil()
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func canonicalUsageFromResponse(response any) *dto.Usage {
|
|
switch resp := response.(type) {
|
|
case *dto.OpenAITextResponse:
|
|
return UsageFromChatUsage(&resp.Usage)
|
|
case dto.OpenAITextResponse:
|
|
return UsageFromChatUsage(&resp.Usage)
|
|
case *dto.ChatCompletionsStreamResponse:
|
|
if resp.Usage == nil {
|
|
return nil
|
|
}
|
|
return UsageFromChatUsage(resp.Usage)
|
|
case dto.ChatCompletionsStreamResponse:
|
|
if resp.Usage == nil {
|
|
return nil
|
|
}
|
|
return UsageFromChatUsage(resp.Usage)
|
|
case *dto.OpenAIResponsesResponse:
|
|
return UsageFromResponsesUsage(resp.Usage)
|
|
case dto.OpenAIResponsesResponse:
|
|
return UsageFromResponsesUsage(resp.Usage)
|
|
case *dto.ResponsesStreamResponse:
|
|
if resp.Response == nil {
|
|
return nil
|
|
}
|
|
return UsageFromResponsesUsage(resp.Response.Usage)
|
|
case dto.ResponsesStreamResponse:
|
|
if resp.Response == nil {
|
|
return nil
|
|
}
|
|
return UsageFromResponsesUsage(resp.Response.Usage)
|
|
case *dto.ClaudeResponse:
|
|
return usageFromClaudeResponse(resp)
|
|
case dto.ClaudeResponse:
|
|
return usageFromClaudeResponse(&resp)
|
|
case *dto.GeminiChatResponse:
|
|
return UsageFromGeminiMetadata(resp.GetUsageMetadata(), 0)
|
|
case dto.GeminiChatResponse:
|
|
return UsageFromGeminiMetadata(resp.GetUsageMetadata(), 0)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func usageFromClaudeResponse(resp *dto.ClaudeResponse) *dto.Usage {
|
|
if resp == nil {
|
|
return nil
|
|
}
|
|
if resp.Usage != nil {
|
|
return UsageFromClaudeAPIUsage(resp.Usage)
|
|
}
|
|
if resp.Message != nil && resp.Message.Usage != nil {
|
|
return UsageFromClaudeAPIUsage(resp.Message.Usage)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func convertOAIChatResponseToOAIResponses(_ context.Context, _ convmeta.Meta, response any) (any, *dto.Usage, error) {
|
|
chatResponse, err := asOAIChatResponse(response)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
id := strings.TrimSpace(chatResponse.Id)
|
|
if id == "" {
|
|
id = fmt.Sprintf("resp_%s", kitutil.GetUUID())
|
|
}
|
|
return ChatCompletionsResponseToResponsesResponse(chatResponse, id)
|
|
}
|
|
|
|
func convertOAIResponsesResponseToOAIChat(_ context.Context, _ convmeta.Meta, response any) (any, *dto.Usage, error) {
|
|
responsesResponse, err := asOAIResponsesResponse(response)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
id := strings.TrimSpace(responsesResponse.ID)
|
|
if id == "" {
|
|
id = fmt.Sprintf("chatcmpl-%s", kitutil.GetUUID())
|
|
}
|
|
return ResponsesResponseToChatCompletionsResponse(responsesResponse, id)
|
|
}
|
|
|
|
func newOAIChatToOAIResponsesStreamState(options ResponseStreamOptions) any {
|
|
id := strings.TrimSpace(options.ID)
|
|
if id == "" {
|
|
id = fmt.Sprintf("resp_%s", kitutil.GetUUID())
|
|
}
|
|
state := NewChatToResponsesStreamState(id, strings.TrimSpace(options.Model))
|
|
if options.Created != 0 {
|
|
state.Created = options.Created
|
|
}
|
|
return state
|
|
}
|
|
|
|
func convertOAIChatStreamResponseToOAIResponses(_ context.Context, _ convmeta.Meta, response any, state any) ([]any, *dto.Usage, error) {
|
|
chatResponse, err := asOAIChatStreamResponse(response)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
streamState, ok := state.(*ChatToResponsesStreamState)
|
|
if !ok || streamState == nil {
|
|
return nil, nil, errors.New("OAI chat to OAI responses stream state is required")
|
|
}
|
|
events, err := ChatCompletionsStreamChunkToResponsesEvents(chatResponse, streamState)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return streamValuesFromAny(events), streamState.Usage, nil
|
|
}
|
|
|
|
func finalizeOAIChatStreamResponseToOAIResponses(_ context.Context, _ convmeta.Meta, state any) ([]any, *dto.Usage, error) {
|
|
streamState, ok := state.(*ChatToResponsesStreamState)
|
|
if !ok || streamState == nil {
|
|
return nil, nil, errors.New("OAI chat to OAI responses stream state is required")
|
|
}
|
|
events := FinalizeChatCompletionsStreamToResponses(streamState)
|
|
return streamValuesFromAny(events), streamState.Usage, nil
|
|
}
|
|
|
|
func newOAIResponsesToOAIChatStreamState(options ResponseStreamOptions) any {
|
|
state := NewResponsesToChatStreamState(strings.TrimSpace(options.Model), options.IncludeUsage)
|
|
state.ID = strings.TrimSpace(options.ID)
|
|
if options.Created != 0 {
|
|
state.Created = options.Created
|
|
}
|
|
return state
|
|
}
|
|
|
|
func convertOAIResponsesStreamResponseToOAIChat(_ context.Context, _ convmeta.Meta, response any, state any) ([]any, *dto.Usage, error) {
|
|
responsesResponse, err := asOAIResponsesStreamResponse(response)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
streamState, ok := state.(*ResponsesToChatStreamState)
|
|
if !ok || streamState == nil {
|
|
return nil, nil, errors.New("OAI responses to OAI chat stream state is required")
|
|
}
|
|
chunks, err := ResponsesStreamEventToChatChunks(responsesResponse, streamState)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return streamValuesFromAny(chunks), streamState.Usage, nil
|
|
}
|
|
|
|
func finalizeOAIResponsesStreamResponseToOAIChat(_ context.Context, _ convmeta.Meta, state any) ([]any, *dto.Usage, error) {
|
|
streamState, ok := state.(*ResponsesToChatStreamState)
|
|
if !ok || streamState == nil {
|
|
return nil, nil, errors.New("OAI responses to OAI chat stream state is required")
|
|
}
|
|
chunks := FinalizeResponsesToChatStream(streamState)
|
|
return streamValuesFromAny(chunks), streamState.Usage, nil
|
|
}
|
|
|
|
func convertOAIChatResponseToClaudeMessages(_ context.Context, info convmeta.Meta, response any) (any, *dto.Usage, error) {
|
|
chatResponse, err := asOAIChatResponse(response)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return ResponseOpenAI2Claude(chatResponse, info), UsageFromChatUsage(&chatResponse.Usage), nil
|
|
}
|
|
|
|
func convertOAIChatStreamResponseToClaudeMessages(_ context.Context, info convmeta.Meta, response any) (any, *dto.Usage, error) {
|
|
chatResponse, err := asOAIChatStreamResponse(response)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return StreamResponseOpenAI2Claude(chatResponse, info), canonicalUsageFromResponse(chatResponse), nil
|
|
}
|
|
|
|
func finalizeOAIChatStreamResponseToClaudeMessages(_ context.Context, info convmeta.Meta, _ any) ([]any, *dto.Usage, error) {
|
|
if info == nil {
|
|
info = &convmeta.Values{}
|
|
}
|
|
usage := info.EnsureClaudeConvertInfo().Usage
|
|
responses := oaichat.FinalizeStreamResponseOpenAI2Claude(info)
|
|
return streamValuesFromAny(responses), usage, nil
|
|
}
|
|
|
|
func convertOAIChatResponseToGeminiChat(_ context.Context, info convmeta.Meta, response any) (any, *dto.Usage, error) {
|
|
chatResponse, err := asOAIChatResponse(response)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return ResponseOpenAI2Gemini(chatResponse, info), UsageFromChatUsage(&chatResponse.Usage), nil
|
|
}
|
|
|
|
func convertOAIChatStreamResponseToGeminiChat(_ context.Context, info convmeta.Meta, response any) (any, *dto.Usage, error) {
|
|
chatResponse, err := asOAIChatStreamResponse(response)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return StreamResponseOpenAI2Gemini(chatResponse, info), canonicalUsageFromResponse(chatResponse), nil
|
|
}
|
|
|
|
func convertClaudeMessagesResponseToOAIChat(_ context.Context, _ convmeta.Meta, response any) (any, *dto.Usage, error) {
|
|
claudeResponse, err := asClaudeResponse(response)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
usage := usageFromClaudeResponse(claudeResponse)
|
|
openAIResponse := ResponseClaude2OpenAI(claudeResponse)
|
|
if usage != nil {
|
|
openAIResponse.Usage = *usage
|
|
}
|
|
return openAIResponse, usage, nil
|
|
}
|
|
|
|
func convertClaudeMessagesStreamResponseToOAIChat(_ context.Context, _ convmeta.Meta, response any) (any, *dto.Usage, error) {
|
|
claudeResponse, err := asClaudeResponse(response)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
openAIResponse := StreamResponseClaude2OpenAI(claudeResponse)
|
|
usage := usageFromClaudeResponse(claudeResponse)
|
|
if openAIResponse != nil && usage != nil {
|
|
openAIResponse.Usage = usage
|
|
}
|
|
return openAIResponse, usage, nil
|
|
}
|
|
|
|
func convertGeminiChatResponseToOAIChat(_ context.Context, info convmeta.Meta, response any) (any, *dto.Usage, error) {
|
|
geminiResponse, err := asGeminiChatResponse(response)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
usage := UsageFromGeminiMetadata(geminiResponse.GetUsageMetadata(), fallbackPromptTokens(info))
|
|
openAIResponse := ResponseGeminiChat2OpenAI(fmt.Sprintf("chatcmpl-%s", kitutil.GetUUID()), kitutil.GetTimestamp(), geminiResponse)
|
|
if info != nil && info.HasChannelMeta() {
|
|
openAIResponse.Model = info.GetUpstreamModelName()
|
|
}
|
|
if usage != nil {
|
|
openAIResponse.Usage = *usage
|
|
}
|
|
return openAIResponse, usage, nil
|
|
}
|
|
|
|
func newGeminiChatToOAIChatStreamState(options ResponseStreamOptions) any {
|
|
return geminichat.NewGeminiToChatStreamState(options.ID, options.Created)
|
|
}
|
|
|
|
func convertGeminiChatStreamResponseChunkToOAIChat(_ context.Context, info convmeta.Meta, response any, state any) ([]any, *dto.Usage, error) {
|
|
geminiResponse, err := asGeminiChatResponse(response)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
streamState, ok := state.(*geminichat.GeminiToChatStreamState)
|
|
if !ok || streamState == nil {
|
|
return nil, nil, errors.New("Gemini chat to OAI chat stream state is required")
|
|
}
|
|
usage := UsageFromGeminiMetadata(geminiResponse.GetUsageMetadata(), fallbackPromptTokens(info))
|
|
model := ""
|
|
if info != nil && info.HasChannelMeta() {
|
|
model = info.GetUpstreamModelName()
|
|
}
|
|
responses := streamState.ConvertChunk(geminiResponse, model, usage)
|
|
return streamValuesFromAny(responses), usage, nil
|
|
}
|
|
|
|
func finalizeGeminiChatStreamResponseToOAIChat(_ context.Context, info convmeta.Meta, state any) ([]any, *dto.Usage, error) {
|
|
streamState, ok := state.(*geminichat.GeminiToChatStreamState)
|
|
if !ok || streamState == nil {
|
|
return nil, nil, errors.New("Gemini chat to OAI chat stream state is required")
|
|
}
|
|
model := ""
|
|
if info != nil && info.HasChannelMeta() {
|
|
model = info.GetUpstreamModelName()
|
|
}
|
|
responses := streamState.Finalize(model)
|
|
return streamValuesFromAny(responses), streamState.Usage(), nil
|
|
}
|
|
|
|
func convertGeminiChatStreamResponseToOAIChat(_ context.Context, info convmeta.Meta, response any) (any, *dto.Usage, error) {
|
|
geminiResponse, err := asGeminiChatResponse(response)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
openAIResponse, _ := StreamResponseGeminiChat2OpenAI(geminiResponse)
|
|
usage := UsageFromGeminiMetadata(geminiResponse.GetUsageMetadata(), fallbackPromptTokens(info))
|
|
if openAIResponse != nil {
|
|
openAIResponse.Id = fmt.Sprintf("chatcmpl-%s", kitutil.GetUUID())
|
|
openAIResponse.Created = kitutil.GetTimestamp()
|
|
if info != nil && info.HasChannelMeta() {
|
|
openAIResponse.Model = info.GetUpstreamModelName()
|
|
}
|
|
openAIResponse.Usage = usage
|
|
}
|
|
return openAIResponse, usage, nil
|
|
}
|
|
|
|
func fallbackPromptTokens(info convmeta.Meta) int {
|
|
if info == nil {
|
|
return 0
|
|
}
|
|
return info.GetEstimatePromptTokens()
|
|
}
|
|
|
|
func asOAIChatResponse(response any) (*dto.OpenAITextResponse, error) {
|
|
switch resp := response.(type) {
|
|
case *dto.OpenAITextResponse:
|
|
return resp, nil
|
|
case dto.OpenAITextResponse:
|
|
return &resp, nil
|
|
default:
|
|
return nil, fmt.Errorf("expected OAI chat response, got %T", response)
|
|
}
|
|
}
|
|
|
|
func asOAIChatStreamResponse(response any) (*dto.ChatCompletionsStreamResponse, error) {
|
|
switch resp := response.(type) {
|
|
case *dto.ChatCompletionsStreamResponse:
|
|
return resp, nil
|
|
case dto.ChatCompletionsStreamResponse:
|
|
return &resp, nil
|
|
default:
|
|
return nil, fmt.Errorf("expected OAI chat stream response, got %T", response)
|
|
}
|
|
}
|
|
|
|
func asOAIResponsesResponse(response any) (*dto.OpenAIResponsesResponse, error) {
|
|
switch resp := response.(type) {
|
|
case *dto.OpenAIResponsesResponse:
|
|
return resp, nil
|
|
case dto.OpenAIResponsesResponse:
|
|
return &resp, nil
|
|
default:
|
|
return nil, fmt.Errorf("expected OAI responses response, got %T", response)
|
|
}
|
|
}
|
|
|
|
func asOAIResponsesStreamResponse(response any) (*dto.ResponsesStreamResponse, error) {
|
|
switch resp := response.(type) {
|
|
case *dto.ResponsesStreamResponse:
|
|
return resp, nil
|
|
case dto.ResponsesStreamResponse:
|
|
return &resp, nil
|
|
default:
|
|
return nil, fmt.Errorf("expected OAI responses stream response, got %T", response)
|
|
}
|
|
}
|
|
|
|
func asClaudeResponse(response any) (*dto.ClaudeResponse, error) {
|
|
switch resp := response.(type) {
|
|
case *dto.ClaudeResponse:
|
|
return resp, nil
|
|
case dto.ClaudeResponse:
|
|
return &resp, nil
|
|
default:
|
|
return nil, fmt.Errorf("expected Claude messages response, got %T", response)
|
|
}
|
|
}
|
|
|
|
func asGeminiChatResponse(response any) (*dto.GeminiChatResponse, error) {
|
|
switch resp := response.(type) {
|
|
case *dto.GeminiChatResponse:
|
|
return resp, nil
|
|
case dto.GeminiChatResponse:
|
|
return &resp, nil
|
|
default:
|
|
return nil, fmt.Errorf("expected Gemini chat response, got %T", response)
|
|
}
|
|
}
|