feat: sync deployed v1.0.0-rc.38-custom.42
Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
This commit is contained in:
1 parent
aa6c3f50f4
commit
ceddfd3d6b
828 files changed
+122253
-13702
No files matched your search
@@ -1,6 +1,7 @@
|
||||
package claudemessages
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -23,7 +24,7 @@ type openRouterRequestReasoning struct {
|
||||
Exclude bool `json:"exclude,omitempty"`
|
||||
}
|
||||
|
||||
func ClaudeMessagesRequestToOpenAIChat(claudeRequest dto.ClaudeRequest, info convmeta.Meta) (*dto.GeneralOpenAIRequest, error) {
|
||||
func ClaudeMessagesRequestToOpenAIChat(ctx context.Context, claudeRequest dto.ClaudeRequest, info convmeta.Meta) (*dto.GeneralOpenAIRequest, error) {
|
||||
openAIRequest := dto.GeneralOpenAIRequest{
|
||||
Model: claudeRequest.Model,
|
||||
Temperature: claudeRequest.Temperature,
|
||||
@@ -40,7 +41,7 @@ func ClaudeMessagesRequestToOpenAIChat(claudeRequest dto.ClaudeRequest, info con
|
||||
if claudeRequest.Stream != nil {
|
||||
openAIRequest.Stream = kitutil.GetPointer(*claudeRequest.Stream)
|
||||
}
|
||||
reasoningIntent, effectiveEffort, err := claudeRequestReasoningIntent(&claudeRequest, info)
|
||||
reasoningIntent, effectiveEffort, err := claudeRequestReasoningIntent(ctx, &claudeRequest, info)
|
||||
if err != nil {
|
||||
return nil, reasoning.AsClientError(err)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package claudemessages
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -8,11 +9,12 @@ import (
|
||||
|
||||
"github.com/QuantumNous/new-api/relaykit/dto"
|
||||
"github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta"
|
||||
"github.com/QuantumNous/new-api/relaykit/relayconvert/internal/convdiag"
|
||||
kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil"
|
||||
"github.com/QuantumNous/new-api/relaykit/relayconvert/reasoning"
|
||||
)
|
||||
|
||||
func ClaudeMessagesRequestToOpenAIResponses(claudeRequest dto.ClaudeRequest, info convmeta.Meta) (*dto.OpenAIResponsesRequest, error) {
|
||||
func ClaudeMessagesRequestToOpenAIResponses(ctx context.Context, claudeRequest dto.ClaudeRequest, info convmeta.Meta) (*dto.OpenAIResponsesRequest, error) {
|
||||
if strings.TrimSpace(claudeRequest.Model) == "" {
|
||||
return nil, errors.New("model is required")
|
||||
}
|
||||
@@ -67,7 +69,7 @@ func ClaudeMessagesRequestToOpenAIResponses(claudeRequest dto.ClaudeRequest, inf
|
||||
responsesRequest.MaxOutputTokens = &maxOutputTokens
|
||||
}
|
||||
|
||||
reasoningIntent, effectiveEffort, err := claudeRequestReasoningIntent(&claudeRequest, info)
|
||||
reasoningIntent, effectiveEffort, err := claudeRequestReasoningIntent(ctx, &claudeRequest, info)
|
||||
if err != nil {
|
||||
return nil, reasoning.AsClientError(err)
|
||||
}
|
||||
@@ -81,20 +83,22 @@ func ClaudeMessagesRequestToOpenAIResponses(claudeRequest dto.ClaudeRequest, inf
|
||||
return responsesRequest, nil
|
||||
}
|
||||
|
||||
func claudeRequestReasoningIntent(claudeRequest *dto.ClaudeRequest, info convmeta.Meta) (reasoning.Intent, reasoning.Effort, error) {
|
||||
reasoningIntent, err := reasoning.FromClaude(claudeRequest)
|
||||
func claudeRequestReasoningIntent(ctx context.Context, claudeRequest *dto.ClaudeRequest, info convmeta.Meta) (reasoning.Intent, reasoning.Effort, error) {
|
||||
reasoningIntent, diagnostics, err := reasoning.FromClaude(claudeRequest)
|
||||
if err != nil {
|
||||
return reasoning.Intent{}, "", err
|
||||
}
|
||||
convdiag.Add(ctx, diagnostics...)
|
||||
sourceModel := claudeRequest.Model
|
||||
if info != nil && info.GetOriginModelName() != "" {
|
||||
sourceModel = info.GetOriginModelName()
|
||||
}
|
||||
if suffix := reasoning.IntentFromState(convmeta.ReasoningStateOf(info)); !suffix.IsEmpty() {
|
||||
reasoningIntent, err = reasoning.MergeExplicitAndSuffix(reasoningIntent, suffix, sourceModel)
|
||||
reasoningIntent, diagnostics, err = reasoning.MergeExplicitAndSuffix(reasoningIntent, suffix, sourceModel)
|
||||
if err != nil {
|
||||
return reasoning.Intent{}, "", err
|
||||
}
|
||||
convdiag.Add(ctx, diagnostics...)
|
||||
}
|
||||
reasoningIntent = reasoning.ResolveClaudeDefault(sourceModel, reasoningIntent)
|
||||
return reasoningIntent, reasoning.EffectiveEffort(reasoningIntent), nil
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
package geminichat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/relaykit/dto"
|
||||
"github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta"
|
||||
"github.com/QuantumNous/new-api/relaykit/relayconvert/internal/convdiag"
|
||||
"github.com/QuantumNous/new-api/relaykit/relayconvert/internal/jsonutil"
|
||||
kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil"
|
||||
"github.com/QuantumNous/new-api/relaykit/relayconvert/reasoning"
|
||||
)
|
||||
|
||||
func GeminiGenerateContentRequestToOpenAIChat(geminiRequest *dto.GeminiChatRequest, info convmeta.Meta) (*dto.GeneralOpenAIRequest, error) {
|
||||
func GeminiGenerateContentRequestToOpenAIChat(ctx context.Context, geminiRequest *dto.GeminiChatRequest, info convmeta.Meta) (*dto.GeneralOpenAIRequest, error) {
|
||||
modelName := ""
|
||||
isStream := false
|
||||
if info != nil {
|
||||
@@ -22,30 +24,42 @@ func GeminiGenerateContentRequestToOpenAIChat(geminiRequest *dto.GeminiChatReque
|
||||
Model: modelName,
|
||||
Stream: kitutil.GetPointer(isStream),
|
||||
}
|
||||
reasoningIntent, err := reasoning.FromGemini(geminiRequest)
|
||||
if err != nil {
|
||||
return nil, reasoning.AsClientError(err)
|
||||
}
|
||||
sourceModelName := modelName
|
||||
if info != nil && info.GetOriginModelName() != "" {
|
||||
sourceModelName = info.GetOriginModelName()
|
||||
}
|
||||
baseSourceModel := sourceModelName
|
||||
opts := convmeta.OptionsOf(info)
|
||||
preserveSuffix := opts.ShouldPreserveThinkingSuffix(sourceModelName)
|
||||
// Capability lookups need the unsuffixed Gemini model the client addressed.
|
||||
// The host already folded any alias into ReasoningState; mirror its parser
|
||||
// so gemini-pro-latest-thinking still resolves to gemini-pro-latest.
|
||||
baseSourceModel := sourceModelName
|
||||
if !preserveSuffix {
|
||||
if suffix := reasoning.IntentFromState(convmeta.ReasoningStateOf(info)); !suffix.IsEmpty() {
|
||||
reasoningIntent, err = reasoning.MergeExplicitAndSuffix(reasoningIntent, suffix, sourceModelName)
|
||||
if err != nil {
|
||||
return nil, reasoning.AsClientError(err)
|
||||
}
|
||||
baseSourceModel = reasoning.ParseModelModifiers(sourceModelName).Base
|
||||
if base, _, found, err := reasoning.ParseGeminiModelSuffix(baseSourceModel, opts.Gemini.ThinkingAdapterEnabled); err == nil && found {
|
||||
baseSourceModel = base
|
||||
}
|
||||
}
|
||||
if baseSourceModel != "" && geminiRequest.GenerationConfig.ThinkingConfig != nil {
|
||||
_, err = reasoning.ValidateGeminiThinkingConfig(baseSourceModel, geminiRequest.GenerationConfig.ThinkingConfig)
|
||||
_, diagnostics, err := reasoning.NormalizeGeminiThinkingConfig(baseSourceModel, &geminiRequest.GenerationConfig)
|
||||
if err != nil {
|
||||
return nil, reasoning.AsClientError(err)
|
||||
}
|
||||
convdiag.Add(ctx, diagnostics...)
|
||||
}
|
||||
reasoningIntent, diagnostics, err := reasoning.FromGemini(geminiRequest)
|
||||
if err != nil {
|
||||
return nil, reasoning.AsClientError(err)
|
||||
}
|
||||
convdiag.Add(ctx, diagnostics...)
|
||||
if !preserveSuffix {
|
||||
if suffix := reasoning.IntentFromState(convmeta.ReasoningStateOf(info)); !suffix.IsEmpty() {
|
||||
reasoningIntent, diagnostics, err = reasoning.MergeExplicitAndSuffix(reasoningIntent, suffix, sourceModelName)
|
||||
if err != nil {
|
||||
return nil, reasoning.AsClientError(err)
|
||||
}
|
||||
convdiag.Add(ctx, diagnostics...)
|
||||
}
|
||||
}
|
||||
reasoningIntent = reasoning.ResolveGeminiDefault(baseSourceModel, reasoningIntent)
|
||||
effectiveEffort := reasoning.EffectiveEffort(reasoningIntent)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/QuantumNous/new-api/relaykit/dto"
|
||||
"github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta"
|
||||
"github.com/QuantumNous/new-api/relaykit/relayconvert/internal/convdiag"
|
||||
relaymedia "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/media"
|
||||
sharedclaude "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/shared/claude"
|
||||
kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil"
|
||||
@@ -94,10 +95,11 @@ func OpenAIChatRequestToClaudeMessages(c context.Context, info convmeta.Meta, te
|
||||
}
|
||||
}
|
||||
|
||||
sourceReasoning, err := reasoning.FromOpenAIChat(&textRequest)
|
||||
sourceReasoning, diagnostics, err := reasoning.FromOpenAIChat(&textRequest)
|
||||
if err != nil {
|
||||
return nil, reasoning.AsClientError(err)
|
||||
}
|
||||
convdiag.Add(c, diagnostics...)
|
||||
if err := sharedclaude.ApplyReasoning(c, &claudeRequest, info, sourceReasoning, true); err != nil {
|
||||
return nil, reasoning.AsClientError(err)
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ func OpenAIChatRequestToGeminiGenerateContent(c context.Context, textRequest dto
|
||||
}
|
||||
}
|
||||
|
||||
if err := sharedgemini.ApplyThinkingConfig(&geminiRequest, info, textRequest); err != nil {
|
||||
if err := sharedgemini.ApplyThinkingConfig(c, &geminiRequest, info, textRequest); err != nil {
|
||||
return nil, reasoning.AsClientError(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package oaichat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/relaykit/dto"
|
||||
"github.com/QuantumNous/new-api/relaykit/relayconvert/internal/convdiag"
|
||||
kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil"
|
||||
"github.com/QuantumNous/new-api/relaykit/relayconvert/reasoning"
|
||||
"github.com/samber/lo"
|
||||
@@ -74,7 +76,7 @@ func convertChatResponseFormatToResponsesText(reqFormat *dto.ResponseFormat) jso
|
||||
return textRaw
|
||||
}
|
||||
|
||||
func ChatCompletionsRequestToResponsesRequest(req *dto.GeneralOpenAIRequest) (*dto.OpenAIResponsesRequest, error) {
|
||||
func ChatCompletionsRequestToResponsesRequest(ctx context.Context, req *dto.GeneralOpenAIRequest) (*dto.OpenAIResponsesRequest, error) {
|
||||
if req == nil {
|
||||
return nil, errors.New("request is nil")
|
||||
}
|
||||
@@ -412,10 +414,11 @@ func ChatCompletionsRequestToResponsesRequest(req *dto.GeneralOpenAIRequest) (*d
|
||||
out.MaxOutputTokens = lo.ToPtr(maxOutputTokens)
|
||||
}
|
||||
|
||||
reasoningIntent, err := reasoning.FromOpenAIChat(req)
|
||||
reasoningIntent, diagnostics, err := reasoning.FromOpenAIChat(req)
|
||||
if err != nil {
|
||||
return nil, reasoning.AsClientError(err)
|
||||
}
|
||||
convdiag.Add(ctx, diagnostics...)
|
||||
if err := reasoning.ApplyToOpenAIResponses(out, reasoningIntent); err != nil {
|
||||
return nil, reasoning.AsClientError(err)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package oaichat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
@@ -28,7 +29,7 @@ func TestChatCompletionsRequestToResponsesRequestInstructionsAndTools(t *testing
|
||||
},
|
||||
}
|
||||
|
||||
got, err := ChatCompletionsRequestToResponsesRequest(req)
|
||||
got, err := ChatCompletionsRequestToResponsesRequest(context.Background(), req)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "gpt-test", got.Model)
|
||||
@@ -42,7 +43,7 @@ func TestChatCompletionsRequestToResponsesRequestInstructionsAndTools(t *testing
|
||||
func TestChatCompletionsRequestToResponsesRequestPreservesPromptCacheKey(t *testing.T) {
|
||||
t.Run("present", func(t *testing.T) {
|
||||
key := "session-\"quoted\"\\path\n世界"
|
||||
got, err := ChatCompletionsRequestToResponsesRequest(&dto.GeneralOpenAIRequest{
|
||||
got, err := ChatCompletionsRequestToResponsesRequest(context.Background(), &dto.GeneralOpenAIRequest{
|
||||
Model: "gpt-test",
|
||||
Messages: []dto.Message{{Role: "user", Content: "hello"}},
|
||||
PromptCacheKey: key,
|
||||
@@ -59,7 +60,7 @@ func TestChatCompletionsRequestToResponsesRequestPreservesPromptCacheKey(t *test
|
||||
})
|
||||
|
||||
t.Run("absent", func(t *testing.T) {
|
||||
got, err := ChatCompletionsRequestToResponsesRequest(&dto.GeneralOpenAIRequest{
|
||||
got, err := ChatCompletionsRequestToResponsesRequest(context.Background(), &dto.GeneralOpenAIRequest{
|
||||
Model: "gpt-test",
|
||||
Messages: []dto.Message{{Role: "user", Content: "hello"}},
|
||||
})
|
||||
@@ -92,7 +93,7 @@ func TestChatCompletionsRequestToResponsesRequestPreservesQwenThinkingBudget(t *
|
||||
},
|
||||
}
|
||||
|
||||
got, err := ChatCompletionsRequestToResponsesRequest(req)
|
||||
got, err := ChatCompletionsRequestToResponsesRequest(context.Background(), req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.budget, got.ThinkingBudget)
|
||||
|
||||
@@ -108,7 +109,7 @@ func TestChatCompletionsRequestToResponsesRequestPreservesQwenThinkingBudget(t *
|
||||
}
|
||||
|
||||
func TestChatCompletionsRequestToResponsesRequestRejectsMultipleChoices(t *testing.T) {
|
||||
_, err := ChatCompletionsRequestToResponsesRequest(&dto.GeneralOpenAIRequest{
|
||||
_, err := ChatCompletionsRequestToResponsesRequest(context.Background(), &dto.GeneralOpenAIRequest{
|
||||
Model: "gpt-test",
|
||||
N: lo.ToPtr(2),
|
||||
})
|
||||
@@ -147,7 +148,7 @@ func TestChatCompletionsRequestToResponsesRequestPreservesPenalties(t *testing.T
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ChatCompletionsRequestToResponsesRequest(&dto.GeneralOpenAIRequest{
|
||||
got, err := ChatCompletionsRequestToResponsesRequest(context.Background(), &dto.GeneralOpenAIRequest{
|
||||
Model: "gpt-test",
|
||||
Messages: []dto.Message{{Role: "user", Content: "hello"}},
|
||||
FrequencyPenalty: tt.frequency,
|
||||
|
||||
@@ -42,7 +42,7 @@ func ChatCompletionsResponseToResponsesResponse(resp *dto.OpenAITextResponse, id
|
||||
out := &dto.OpenAIResponsesResponse{
|
||||
ID: id,
|
||||
Object: "response",
|
||||
CreatedAt: chatCreatedAt(resp.Created),
|
||||
CreatedAt: dto.IntValue(chatCreatedAt(resp.Created)),
|
||||
Status: []byte(`"completed"`),
|
||||
Model: resp.Model,
|
||||
Output: make([]dto.ResponsesOutput, 0),
|
||||
|
||||
@@ -665,7 +665,7 @@ func (s *ChatToResponsesStreamState) finalResponse() *dto.OpenAIResponsesRespons
|
||||
return &dto.OpenAIResponsesResponse{
|
||||
ID: s.ID,
|
||||
Object: "response",
|
||||
CreatedAt: int(s.Created),
|
||||
CreatedAt: dto.IntValue(s.Created),
|
||||
Status: []byte(fmt.Sprintf("%q", s.status)),
|
||||
IncompleteDetails: s.incompleteDetails,
|
||||
Model: s.Model,
|
||||
@@ -678,7 +678,7 @@ func (s *ChatToResponsesStreamState) createdResponse() *dto.OpenAIResponsesRespo
|
||||
return &dto.OpenAIResponsesResponse{
|
||||
ID: s.ID,
|
||||
Object: "response",
|
||||
CreatedAt: int(s.Created),
|
||||
CreatedAt: dto.IntValue(s.Created),
|
||||
Status: []byte(`"in_progress"`),
|
||||
Model: s.Model,
|
||||
Output: []dto.ResponsesOutput{},
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/QuantumNous/new-api/relaykit/dto"
|
||||
"github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta"
|
||||
"github.com/QuantumNous/new-api/relaykit/relayconvert/internal/convdiag"
|
||||
relaymedia "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/media"
|
||||
sharedclaude "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/shared/claude"
|
||||
kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil"
|
||||
@@ -56,10 +57,11 @@ func OpenAIResponsesRequestToClaudeMessages(c context.Context, info convmeta.Met
|
||||
if toolChoice != nil || RawJSONPresent(req.ParallelToolCalls) {
|
||||
claudeRequest.ToolChoice = sharedclaude.MapOpenAIToolChoice(toolChoice, ParallelToolCalls(req.ParallelToolCalls))
|
||||
}
|
||||
sourceReasoning, err := reasoning.FromOpenAIResponses(req)
|
||||
sourceReasoning, diagnostics, err := reasoning.FromOpenAIResponses(req)
|
||||
if err != nil {
|
||||
return nil, reasoning.AsClientError(err)
|
||||
}
|
||||
convdiag.Add(c, diagnostics...)
|
||||
if err := sharedclaude.ApplyReasoning(c, claudeRequest, info, sourceReasoning, true); err != nil {
|
||||
return nil, reasoning.AsClientError(err)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/QuantumNous/new-api/relaykit/dto"
|
||||
"github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta"
|
||||
"github.com/QuantumNous/new-api/relaykit/relayconvert/internal/convdiag"
|
||||
relaymedia "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/media"
|
||||
sharedgemini "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/shared/gemini"
|
||||
kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil"
|
||||
@@ -61,17 +62,18 @@ func OpenAIResponsesRequestToGeminiChat(c context.Context, req *dto.OpenAIRespon
|
||||
if err := applyResponsesTextToGemini(req.Text, geminiRequest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reasoningIntent, err := reasoning.FromOpenAIResponses(req)
|
||||
reasoningIntent, diagnostics, err := reasoning.FromOpenAIResponses(req)
|
||||
if err != nil {
|
||||
return nil, reasoning.AsClientError(err)
|
||||
}
|
||||
convdiag.Add(c, diagnostics...)
|
||||
var reasoningPivot dto.GeneralOpenAIRequest
|
||||
if err := reasoning.ApplyToOpenAIChat(&reasoningPivot, reasoningIntent); err != nil {
|
||||
return nil, reasoning.AsClientError(err)
|
||||
}
|
||||
reasoningPivot.Model = req.Model
|
||||
reasoningPivot.MaxCompletionTokens = req.MaxOutputTokens
|
||||
if err := sharedgemini.ApplyThinkingConfig(geminiRequest, info, reasoningPivot); err != nil {
|
||||
if err := sharedgemini.ApplyThinkingConfig(c, geminiRequest, info, reasoningPivot); err != nil {
|
||||
return nil, reasoning.AsClientError(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package oairesponses
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/relaykit/dto"
|
||||
"github.com/QuantumNous/new-api/relaykit/relayconvert/internal/convdiag"
|
||||
kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil"
|
||||
"github.com/QuantumNous/new-api/relaykit/relayconvert/reasoning"
|
||||
)
|
||||
@@ -25,7 +27,7 @@ const (
|
||||
ResponsesInputTypeCustomToolOutput = responsesInputTypeCustomToolOutput
|
||||
)
|
||||
|
||||
func ResponsesRequestToChatCompletionsRequest(req *dto.OpenAIResponsesRequest) (*dto.GeneralOpenAIRequest, error) {
|
||||
func ResponsesRequestToChatCompletionsRequest(ctx context.Context, req *dto.OpenAIResponsesRequest) (*dto.GeneralOpenAIRequest, error) {
|
||||
if req == nil {
|
||||
return nil, errors.New("request is nil")
|
||||
}
|
||||
@@ -86,9 +88,12 @@ func ResponsesRequestToChatCompletionsRequest(req *dto.OpenAIResponsesRequest) (
|
||||
return nil, fmt.Errorf("invalid presence_penalty: %w", err)
|
||||
}
|
||||
|
||||
if reasoningIntent, err := reasoning.FromOpenAIResponses(req); err != nil {
|
||||
reasoningIntent, diagnostics, err := reasoning.FromOpenAIResponses(req)
|
||||
if err != nil {
|
||||
return nil, reasoning.AsClientError(err)
|
||||
} else if err := reasoning.ApplyToOpenAIChat(out, reasoningIntent); err != nil {
|
||||
}
|
||||
convdiag.Add(ctx, diagnostics...)
|
||||
if err := reasoning.ApplyToOpenAIChat(out, reasoningIntent); err != nil {
|
||||
return nil, reasoning.AsClientError(err)
|
||||
}
|
||||
if req.ServiceTier != "" {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package oairesponses
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
@@ -19,7 +20,7 @@ func TestResponsesRequestToChatCompletionsRequestInstructionsAndScalarInput(t *t
|
||||
maxOutputTokens := uint(128)
|
||||
parallelToolCalls := true
|
||||
|
||||
got, err := ResponsesRequestToChatCompletionsRequest(&dto.OpenAIResponsesRequest{
|
||||
got, err := ResponsesRequestToChatCompletionsRequest(context.Background(), &dto.OpenAIResponsesRequest{
|
||||
Model: "gpt-test",
|
||||
Instructions: mustRawMessage(t, "system rules"),
|
||||
Input: mustRawMessage(t, "hello"),
|
||||
@@ -68,7 +69,7 @@ func TestResponsesRequestToChatCompletionsRequestPreservesQwenThinkingBudget(t *
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ResponsesRequestToChatCompletionsRequest(&dto.OpenAIResponsesRequest{
|
||||
got, err := ResponsesRequestToChatCompletionsRequest(context.Background(), &dto.OpenAIResponsesRequest{
|
||||
Model: "qwen-plus",
|
||||
Input: mustRawMessage(t, "hello"),
|
||||
EnableThinking: json.RawMessage(`true`),
|
||||
@@ -89,7 +90,7 @@ func TestResponsesRequestToChatCompletionsRequestPreservesQwenThinkingBudget(t *
|
||||
}
|
||||
|
||||
func TestResponsesRequestToChatCompletionsRequestMultimodalInput(t *testing.T) {
|
||||
got, err := ResponsesRequestToChatCompletionsRequest(&dto.OpenAIResponsesRequest{
|
||||
got, err := ResponsesRequestToChatCompletionsRequest(context.Background(), &dto.OpenAIResponsesRequest{
|
||||
Model: "gpt-test",
|
||||
Input: mustRawMessage(t, []map[string]any{
|
||||
{
|
||||
@@ -123,7 +124,7 @@ func TestResponsesRequestToChatCompletionsRequestMultimodalInput(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResponsesRequestToChatCompletionsRequestAssistantTextAndFunctionCallCoexist(t *testing.T) {
|
||||
got, err := ResponsesRequestToChatCompletionsRequest(&dto.OpenAIResponsesRequest{
|
||||
got, err := ResponsesRequestToChatCompletionsRequest(context.Background(), &dto.OpenAIResponsesRequest{
|
||||
Model: "gpt-test",
|
||||
Input: mustRawMessage(t, []map[string]any{
|
||||
{
|
||||
@@ -162,7 +163,7 @@ func TestResponsesRequestToChatCompletionsRequestAssistantTextAndFunctionCallCoe
|
||||
}
|
||||
|
||||
func TestResponsesRequestToChatCompletionsRequestOnlyFunctionCallCreatesAssistant(t *testing.T) {
|
||||
got, err := ResponsesRequestToChatCompletionsRequest(&dto.OpenAIResponsesRequest{
|
||||
got, err := ResponsesRequestToChatCompletionsRequest(context.Background(), &dto.OpenAIResponsesRequest{
|
||||
Model: "gpt-test",
|
||||
Input: mustRawMessage(t, []map[string]any{
|
||||
{
|
||||
@@ -184,7 +185,7 @@ func TestResponsesRequestToChatCompletionsRequestOnlyFunctionCallCreatesAssistan
|
||||
}
|
||||
|
||||
func TestResponsesRequestToChatCompletionsRequestToolsToolChoiceAndTextFormat(t *testing.T) {
|
||||
got, err := ResponsesRequestToChatCompletionsRequest(&dto.OpenAIResponsesRequest{
|
||||
got, err := ResponsesRequestToChatCompletionsRequest(context.Background(), &dto.OpenAIResponsesRequest{
|
||||
Model: "gpt-test",
|
||||
Input: mustRawMessage(t, "hello"),
|
||||
Tools: mustRawMessage(t, []map[string]any{
|
||||
@@ -233,7 +234,7 @@ func TestResponsesRequestToChatCompletionsRequestToolsToolChoiceAndTextFormat(t
|
||||
}
|
||||
|
||||
func TestResponsesRequestToChatCompletionsRequestCustomToolCallPreservesRawShape(t *testing.T) {
|
||||
got, err := ResponsesRequestToChatCompletionsRequest(&dto.OpenAIResponsesRequest{
|
||||
got, err := ResponsesRequestToChatCompletionsRequest(context.Background(), &dto.OpenAIResponsesRequest{
|
||||
Model: "gpt-test",
|
||||
Input: mustRawMessage(t, []map[string]any{
|
||||
{
|
||||
@@ -287,7 +288,7 @@ func TestResponsesRequestToChatCompletionsRequestRejectsStatefulFields(t *testin
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := ResponsesRequestToChatCompletionsRequest(tt.req)
|
||||
_, err := ResponsesRequestToChatCompletionsRequest(context.Background(), tt.req)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.want)
|
||||
assert.Contains(t, err.Error(), "stateful fields")
|
||||
@@ -326,7 +327,7 @@ func TestResponsesRequestToChatCompletionsRequestPreservesPenalties(t *testing.T
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ResponsesRequestToChatCompletionsRequest(&dto.OpenAIResponsesRequest{
|
||||
got, err := ResponsesRequestToChatCompletionsRequest(context.Background(), &dto.OpenAIResponsesRequest{
|
||||
Model: "gpt-test",
|
||||
Input: mustRawMessage(t, "hello"),
|
||||
FrequencyPenalty: tt.frequencyRaw,
|
||||
@@ -341,7 +342,7 @@ func TestResponsesRequestToChatCompletionsRequestPreservesPenalties(t *testing.T
|
||||
}
|
||||
|
||||
func TestResponsesRequestToChatCompletionsRequestRejectsMalformedPenalty(t *testing.T) {
|
||||
_, err := ResponsesRequestToChatCompletionsRequest(&dto.OpenAIResponsesRequest{
|
||||
_, err := ResponsesRequestToChatCompletionsRequest(context.Background(), &dto.OpenAIResponsesRequest{
|
||||
Model: "gpt-test",
|
||||
Input: mustRawMessage(t, "hello"),
|
||||
FrequencyPenalty: json.RawMessage(`"not-a-number"`),
|
||||
|
||||
@@ -66,7 +66,7 @@ func ResponsesResponseToChatCompletionsResponse(resp *dto.OpenAIResponsesRespons
|
||||
|
||||
usage := UsageFromResponsesUsage(resp.Usage)
|
||||
|
||||
created := resp.CreatedAt
|
||||
created := int64(resp.CreatedAt)
|
||||
|
||||
var toolCalls []dto.ToolCallResponse
|
||||
if len(resp.Output) > 0 {
|
||||
@@ -218,12 +218,7 @@ func usageFromResponsesUsage(src *dto.Usage, createBillingSnapshot bool) *dto.Us
|
||||
usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens
|
||||
}
|
||||
if src.InputTokensDetails != nil {
|
||||
usage.PromptTokensDetails.CachedTokens = src.InputTokensDetails.CachedTokens
|
||||
usage.PromptTokensDetails.CachedCreationTokens = src.InputTokensDetails.CachedCreationTokens
|
||||
usage.PromptTokensDetails.CacheWriteTokens = src.InputTokensDetails.CacheWriteTokens
|
||||
usage.PromptTokensDetails.TextTokens = src.InputTokensDetails.TextTokens
|
||||
usage.PromptTokensDetails.ImageTokens = src.InputTokensDetails.ImageTokens
|
||||
usage.PromptTokensDetails.AudioTokens = src.InputTokensDetails.AudioTokens
|
||||
usage.PromptTokensDetails = src.InputTokensDetails.Clone()
|
||||
}
|
||||
if src.CompletionTokenDetails.ReasoningTokens != 0 ||
|
||||
src.CompletionTokenDetails.TextTokens != 0 ||
|
||||
|
||||
@@ -9,6 +9,49 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNormalizeResponsesUsagePreservesImageCacheDetails(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
imageTokens int
|
||||
}{
|
||||
{name: "image cache", imageTokens: 200},
|
||||
{name: "explicit zero image cache", imageTokens: 0},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
imageTokens, audioTokens := tc.imageTokens, 0
|
||||
source := &dto.Usage{
|
||||
InputTokens: 1000,
|
||||
OutputTokens: 100,
|
||||
InputTokensDetails: &dto.InputTokenDetails{
|
||||
CachedTokens: 300,
|
||||
CachedCreationTokens: 20,
|
||||
CacheWriteTokens: 10,
|
||||
TextTokens: 400,
|
||||
ImageTokens: 600,
|
||||
CachedTokensDetails: &dto.CachedTokenDetails{
|
||||
ImageTokens: &imageTokens,
|
||||
AudioTokens: &audioTokens,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
usage := NormalizeResponsesUsage(source)
|
||||
assert.Equal(t, *source.InputTokensDetails, usage.PromptTokensDetails)
|
||||
details := usage.PromptTokensDetails.CachedTokensDetails
|
||||
require.NotNil(t, details)
|
||||
require.NotNil(t, details.ImageTokens)
|
||||
require.NotNil(t, details.AudioTokens)
|
||||
assert.Nil(t, details.TextTokens, "missing modalities must remain absent")
|
||||
|
||||
imageTokens, audioTokens = 999, 888
|
||||
source.InputTokensDetails.ImageTokens = 9999
|
||||
assert.Equal(t, tc.imageTokens, *details.ImageTokens, "later source updates must not change billable image cache")
|
||||
assert.Zero(t, *details.AudioTokens, "explicit zero must survive normalization and source mutation")
|
||||
assert.Equal(t, 600, usage.PromptTokensDetails.ImageTokens)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponsesResponseToChatCompletionsPreservesTextAndToolCalls(t *testing.T) {
|
||||
resp := &dto.OpenAIResponsesResponse{
|
||||
ID: "resp_1",
|
||||
|
||||
@@ -52,21 +52,24 @@ func ApplyReasoning(ctx context.Context, req *dto.ClaudeRequest, info convmeta.M
|
||||
return nil
|
||||
}
|
||||
|
||||
native, err := reasoning.FromClaude(req)
|
||||
native, diagnostics, err := reasoning.FromClaude(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
explicit, err := reasoning.MergeExplicit(native, source, req.Model)
|
||||
convdiag.Add(ctx, diagnostics...)
|
||||
explicit, diagnostics, err := reasoning.MergeExplicit(native, source, req.Model)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
convdiag.Add(ctx, diagnostics...)
|
||||
if info != nil && !reasoning.IsKnownClaudeModel(capabilityModel) && reasoning.IsKnownClaudeModel(info.GetOriginModelName()) {
|
||||
capabilityModel = info.GetOriginModelName()
|
||||
}
|
||||
intent, err := reasoning.MergeExplicitAndSuffix(explicit, suffix, req.Model)
|
||||
intent, diagnostics, err := reasoning.MergeExplicitAndSuffix(explicit, suffix, req.Model)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
convdiag.Add(ctx, diagnostics...)
|
||||
knownClaudeModel := reasoning.IsKnownClaudeModel(capabilityModel)
|
||||
if !knownClaudeModel && intent.Mode == reasoning.ModeAdaptive {
|
||||
// Cross-protocol pivots cannot safely assume that an unknown
|
||||
@@ -80,8 +83,8 @@ func ApplyReasoning(ctx context.Context, req *dto.ClaudeRequest, info convmeta.M
|
||||
}
|
||||
if req.MaxTokens == nil && intent.HasStrength() {
|
||||
// Adapter-provided defaults may be raised to accommodate an exact
|
||||
// cross-protocol budget. Explicit client max_tokens values are never
|
||||
// expanded and remain subject to the renderer's strict validation.
|
||||
// cross-protocol budget. Values too small for manual thinking are
|
||||
// raised again by the renderer, which reports the change.
|
||||
minimum := uint(1280)
|
||||
if configuredDefault, configured := opts.Claude.DefaultMaxTokensFor(capabilityModel); configured && configuredDefault > 0 {
|
||||
minimum = uint(configuredDefault)
|
||||
@@ -108,6 +111,9 @@ func ApplyReasoning(ctx context.Context, req *dto.ClaudeRequest, info convmeta.M
|
||||
}
|
||||
convdiag.Add(ctx, rendered.Diagnostics...)
|
||||
req.Model = baseModel
|
||||
if rendered.MaxTokens != nil {
|
||||
req.MaxTokens = rendered.MaxTokens
|
||||
}
|
||||
if rendered.Thinking != nil {
|
||||
req.Thinking = rendered.Thinking
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
package gemini
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/relaykit/dto"
|
||||
"github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta"
|
||||
"github.com/QuantumNous/new-api/relaykit/relayconvert/internal/convdiag"
|
||||
"github.com/QuantumNous/new-api/relaykit/relayconvert/reasoning"
|
||||
"github.com/QuantumNous/new-api/relaykit/types"
|
||||
)
|
||||
|
||||
var SupportedMimeTypes = map[string]bool{
|
||||
@@ -73,7 +76,12 @@ func AttachFirstTextThoughtSignature(opts *convmeta.Options, parts []dto.GeminiP
|
||||
return false
|
||||
}
|
||||
|
||||
func ApplyThinkingConfig(geminiRequest *dto.GeminiChatRequest, info convmeta.Meta, oaiRequest ...dto.GeneralOpenAIRequest) error {
|
||||
// ApplyThinkingConfig resolves every reasoning control that can reach a
|
||||
// Gemini request (provider-native thinkingConfig, generic OpenAI fields, and
|
||||
// host model-name aliases) into the target model's dialect. Precedence is
|
||||
// model name, then provider-native config, then generic fields; each override
|
||||
// and each capability coercion is reported through convdiag on ctx.
|
||||
func ApplyThinkingConfig(ctx context.Context, geminiRequest *dto.GeminiChatRequest, info convmeta.Meta, oaiRequest ...dto.GeneralOpenAIRequest) error {
|
||||
opts := convmeta.OptionsOf(info)
|
||||
if geminiRequest == nil {
|
||||
return nil
|
||||
@@ -86,11 +94,13 @@ func ApplyThinkingConfig(geminiRequest *dto.GeminiChatRequest, info convmeta.Met
|
||||
if modelName == "" {
|
||||
modelName = oaiRequest[0].Model
|
||||
}
|
||||
var diagnostics []types.ConversionDiagnostic
|
||||
var err error
|
||||
source, err = reasoning.FromOpenAIChat(&oaiRequest[0])
|
||||
source, diagnostics, err = reasoning.FromOpenAIChat(&oaiRequest[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
convdiag.Add(ctx, diagnostics...)
|
||||
}
|
||||
|
||||
baseModel := modelName
|
||||
@@ -110,6 +120,12 @@ func ApplyThinkingConfig(geminiRequest *dto.GeminiChatRequest, info convmeta.Met
|
||||
effort := ""
|
||||
if config := geminiRequest.GenerationConfig.ThinkingConfig; config != nil {
|
||||
effort = config.ThinkingLevel
|
||||
// Gemini accepts thinkingLevel case-insensitively; record the
|
||||
// canonical effort so logs match other protocols. Unknown values
|
||||
// stay as sent because this path does not validate.
|
||||
if canonical, err := reasoning.ParseEffort(effort); err == nil {
|
||||
effort = string(canonical)
|
||||
}
|
||||
if effort == "" && config.ThinkingBudget != nil {
|
||||
effort = string(reasoning.EffortFromBudget(*config.ThinkingBudget))
|
||||
}
|
||||
@@ -118,33 +134,47 @@ func ApplyThinkingConfig(geminiRequest *dto.GeminiChatRequest, info convmeta.Met
|
||||
}
|
||||
return nil
|
||||
}
|
||||
native, err := reasoning.FromGemini(geminiRequest)
|
||||
// Rewrite the provider-native config into the target model's dialect
|
||||
// first, so a budget on Gemini 3 or a level on Gemini 2.5 is converted
|
||||
// rather than rejected and later comparisons see what will be sent.
|
||||
nativeEffort, diagnostics, err := reasoning.NormalizeGeminiThinkingConfig(baseModel, &geminiRequest.GenerationConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
source = reasoning.ResolveGeminiEnabledDefault(baseModel, source, geminiRequest.GenerationConfig.MaxOutputTokens)
|
||||
convdiag.Add(ctx, diagnostics...)
|
||||
native, diagnostics, err := reasoning.FromGemini(geminiRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
convdiag.Add(ctx, diagnostics...)
|
||||
if native.HasStrength() && source.HasStrength() {
|
||||
equivalent, compareErr := reasoning.EquivalentGeminiStrength(baseModel, native, source)
|
||||
if compareErr != nil {
|
||||
return compareErr
|
||||
// Native Gemini configuration is the lossless representation and wins
|
||||
// over the generic OpenAI fields. Only a generic effort or budget can
|
||||
// disagree with it; a bare enable states no strength to compare.
|
||||
if source.Effort != "" || source.BudgetTokens != nil {
|
||||
equivalent, compareErr := reasoning.EquivalentGeminiStrength(baseModel, native, source)
|
||||
if compareErr != nil {
|
||||
return compareErr
|
||||
}
|
||||
if !equivalent {
|
||||
convdiag.Add(ctx, types.ConversionDiagnostic{
|
||||
Code: "native_overrode_standard",
|
||||
Path: "generationConfig.thinkingConfig",
|
||||
Message: fmt.Sprintf("model %q: Gemini thinking_config effort %q overrides the standard reasoning effort %q", modelName, reasoning.EffectiveEffort(native), reasoning.EffectiveEffort(source)),
|
||||
Severity: types.ConversionDiagnosticWarning,
|
||||
})
|
||||
}
|
||||
}
|
||||
if !equivalent {
|
||||
nativeEffort := reasoning.EffectiveEffort(native)
|
||||
sourceEffort := reasoning.EffectiveEffort(source)
|
||||
return fmt.Errorf("%w for model %q: Gemini thinking_config effort %q differs from standard effort %q", reasoning.ErrEffortConflict, modelName, nativeEffort, sourceEffort)
|
||||
}
|
||||
// Native Gemini configuration is the lossless representation. Once the
|
||||
// two controls are equivalent, retain only portable visibility metadata
|
||||
// from the standard representation.
|
||||
if native.IncludeThoughts == nil {
|
||||
native.IncludeThoughts = source.IncludeThoughts
|
||||
}
|
||||
source = reasoning.Intent{}
|
||||
}
|
||||
explicit, err := reasoning.MergeExplicit(native, source, modelName)
|
||||
explicit, diagnostics, err := reasoning.MergeExplicit(native, source, modelName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
convdiag.Add(ctx, diagnostics...)
|
||||
if explicit.HasStrength() && suffix.HasStrength() {
|
||||
equivalent, compareErr := reasoning.EquivalentGeminiStrength(baseModel, explicit, suffix)
|
||||
if compareErr != nil {
|
||||
@@ -157,22 +187,21 @@ func ApplyThinkingConfig(geminiRequest *dto.GeminiChatRequest, info convmeta.Met
|
||||
suffix = reasoning.Intent{}
|
||||
}
|
||||
}
|
||||
requested, err := reasoning.MergeExplicitAndSuffix(explicit, suffix, modelName)
|
||||
requested, diagnostics, err := reasoning.MergeExplicitAndSuffix(explicit, suffix, modelName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
convdiag.Add(ctx, diagnostics...)
|
||||
requested = reasoning.ResolveGeminiEnabledDefault(baseModel, requested, geminiRequest.GenerationConfig.MaxOutputTokens)
|
||||
|
||||
if native.HasStrength() && !suffix.HasStrength() {
|
||||
// The normalized native config is what goes upstream; only portable
|
||||
// visibility metadata is taken from the standard representation.
|
||||
if explicit.IncludeThoughts != nil {
|
||||
geminiRequest.GenerationConfig.ThinkingConfig.IncludeThoughts = explicit.IncludeThoughts
|
||||
}
|
||||
effort, err := reasoning.ValidateGeminiThinkingConfig(baseModel, geminiRequest.GenerationConfig.ThinkingConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info != nil && effort != "" {
|
||||
info.SetReasoningEffort(string(effort))
|
||||
if info != nil && nativeEffort != "" {
|
||||
info.SetReasoningEffort(string(nativeEffort))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -188,6 +217,7 @@ func ApplyThinkingConfig(geminiRequest *dto.GeminiChatRequest, info convmeta.Met
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
convdiag.Add(ctx, rendered.Diagnostics...)
|
||||
geminiRequest.GenerationConfig.ThinkingConfig = rendered.Config
|
||||
if info != nil && rendered.EffectiveEffort != "" {
|
||||
info.SetReasoningEffort(string(rendered.EffectiveEffort))
|
||||
|
||||
Reference in new issue
Block a user