mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-15 23:22:59 +08:00
feat(apicompat): configure minimum output token floor
This commit is contained in:
@@ -979,6 +979,9 @@ const (
|
||||
|
||||
// GatewayConfig API网关相关配置
|
||||
type GatewayConfig struct {
|
||||
// MinOutputTokens is the minimum max_output_tokens used when
|
||||
// converting Chat Completions or Anthropic requests to Responses.
|
||||
MinOutputTokens int `mapstructure:"min_output_tokens"`
|
||||
// 等待上游响应头的超时时间(秒),0表示无超时
|
||||
// 注意:这不影响流式数据传输,只控制等待响应头的时间
|
||||
ResponseHeaderTimeout int `mapstructure:"response_header_timeout"`
|
||||
@@ -2035,6 +2038,7 @@ func configureConfigSource(setConfigFile, addConfigPath func(string)) {
|
||||
|
||||
func setDefaults() {
|
||||
viper.SetDefault("run_mode", RunModeStandard)
|
||||
viper.SetDefault("gateway.min_output_tokens", 128)
|
||||
viper.SetDefault("simple_mode.auto_create_default_groups", true)
|
||||
viper.SetDefault("simple_mode_key_rate_limit_enabled", false)
|
||||
|
||||
@@ -2707,6 +2711,9 @@ func setEnvReachableDefaults() {
|
||||
}
|
||||
|
||||
func (c *Config) Validate() error {
|
||||
if c.Gateway.MinOutputTokens < 16 {
|
||||
return fmt.Errorf("gateway.min_output_tokens must be at least 16")
|
||||
}
|
||||
forwardedClientIPHeaders, err := NormalizeForwardedClientIPHeaders(c.Security.ForwardedClientIPHeaders)
|
||||
if err != nil {
|
||||
return fmt.Errorf("security.forwarded_client_ip_headers: %w", err)
|
||||
|
||||
@@ -31,6 +31,29 @@ func TestLoadDefaultModelsListReadMaxBytes(t *testing.T) {
|
||||
require.Equal(t, DefaultModelsListReadMaxBytes, cfg.Gateway.ModelsListReadMaxBytes)
|
||||
}
|
||||
|
||||
func TestLoadDefaultMinOutputTokens(t *testing.T) {
|
||||
resetViperWithJWTSecret(t)
|
||||
cfg, err := Load()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 128, cfg.Gateway.MinOutputTokens)
|
||||
}
|
||||
|
||||
func TestLoadMinOutputTokensFromEnv(t *testing.T) {
|
||||
resetViperWithJWTSecret(t)
|
||||
t.Setenv("GATEWAY_MIN_OUTPUT_TOKENS", "16")
|
||||
cfg, err := Load()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 16, cfg.Gateway.MinOutputTokens)
|
||||
}
|
||||
|
||||
func TestValidateMinOutputTokensMinimum(t *testing.T) {
|
||||
resetViperWithJWTSecret(t)
|
||||
cfg, err := Load()
|
||||
require.NoError(t, err)
|
||||
cfg.Gateway.MinOutputTokens = 15
|
||||
require.EqualError(t, cfg.Validate(), "gateway.min_output_tokens must be at least 16")
|
||||
}
|
||||
|
||||
func TestLoadTimezonePrecedence(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -13,6 +13,12 @@ import (
|
||||
// Chat Completions intermediary round-trip (e.g. thinking, cache_control,
|
||||
// structured system prompts).
|
||||
func AnthropicToResponses(req *AnthropicRequest) (*ResponsesRequest, error) {
|
||||
return AnthropicToResponsesWithMinOutputTokens(req, DefaultMinMaxOutputTokens)
|
||||
}
|
||||
|
||||
// AnthropicToResponsesWithMinOutputTokens converts an Anthropic request using
|
||||
// the supplied max_output_tokens floor.
|
||||
func AnthropicToResponsesWithMinOutputTokens(req *AnthropicRequest, minOutputTokens int) (*ResponsesRequest, error) {
|
||||
if err := openai.ValidateGPT61SolReasoningEffort(req.Model, anthropicReasoningEffort(req)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -50,8 +56,8 @@ func AnthropicToResponses(req *AnthropicRequest) (*ResponsesRequest, error) {
|
||||
|
||||
if req.MaxTokens > 0 {
|
||||
v := req.MaxTokens
|
||||
if v < minMaxOutputTokens {
|
||||
v = minMaxOutputTokens
|
||||
if v < minOutputTokens {
|
||||
v = minOutputTokens
|
||||
}
|
||||
out.MaxOutputTokens = &v
|
||||
}
|
||||
|
||||
@@ -40,6 +40,12 @@ import (
|
||||
// ResponsesToChatCompletionsRequest but avoids materializing the intermediate
|
||||
// ResponsesRequest and the extra marshal/unmarshal cycle.
|
||||
func AnthropicToChatCompletionsRequest(req *AnthropicRequest) (*ChatCompletionsRequest, error) {
|
||||
return AnthropicToChatCompletionsRequestWithMinOutputTokens(req, DefaultMinMaxOutputTokens)
|
||||
}
|
||||
|
||||
// AnthropicToChatCompletionsRequestWithMinOutputTokens converts an Anthropic
|
||||
// Messages request using the supplied max_completion_tokens floor.
|
||||
func AnthropicToChatCompletionsRequestWithMinOutputTokens(req *AnthropicRequest, minOutputTokens int) (*ChatCompletionsRequest, error) {
|
||||
if req == nil {
|
||||
return nil, fmt.Errorf("anthropic request is nil")
|
||||
}
|
||||
@@ -63,8 +69,8 @@ func AnthropicToChatCompletionsRequest(req *AnthropicRequest) (*ChatCompletionsR
|
||||
|
||||
if req.MaxTokens > 0 {
|
||||
v := req.MaxTokens
|
||||
if v < minMaxOutputTokens {
|
||||
v = minMaxOutputTokens
|
||||
if v < minOutputTokens {
|
||||
v = minOutputTokens
|
||||
}
|
||||
out.MaxCompletionTokens = &v
|
||||
}
|
||||
|
||||
@@ -267,6 +267,43 @@ func TestAnthropicToChatCompletionsRequest_MaxTokensFloor(t *testing.T) {
|
||||
require.Equal(t, minMaxOutputTokens, *out.MaxCompletionTokens)
|
||||
}
|
||||
|
||||
func TestAnthropicToChatCompletionsRequestWithMinOutputTokens_ConfiguredFloor(t *testing.T) {
|
||||
req := &AnthropicRequest{
|
||||
Model: "claude-sonnet-4-20250514",
|
||||
MaxTokens: 16,
|
||||
Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"hi"`)}},
|
||||
}
|
||||
|
||||
out, err := AnthropicToChatCompletionsRequestWithMinOutputTokens(req, 64)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, out.MaxCompletionTokens)
|
||||
require.Equal(t, 64, *out.MaxCompletionTokens)
|
||||
}
|
||||
|
||||
func TestAnthropicToResponses_ConfiguredMaxTokensFloor(t *testing.T) {
|
||||
for _, input := range []int{16, 64, 127} {
|
||||
req := &AnthropicRequest{
|
||||
Model: "claude-sonnet-4-20250514",
|
||||
MaxTokens: input,
|
||||
Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"hi"`)}},
|
||||
}
|
||||
out, err := AnthropicToResponsesWithMinOutputTokens(req, 16)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, out.MaxOutputTokens)
|
||||
require.Equal(t, input, *out.MaxOutputTokens)
|
||||
}
|
||||
|
||||
req := &AnthropicRequest{
|
||||
Model: "claude-sonnet-4-20250514",
|
||||
MaxTokens: 1,
|
||||
Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"hi"`)}},
|
||||
}
|
||||
out, err := AnthropicToResponsesWithMinOutputTokens(req, 16)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, out.MaxOutputTokens)
|
||||
require.Equal(t, 16, *out.MaxOutputTokens)
|
||||
}
|
||||
|
||||
func TestAnthropicToChatCompletionsRequest_ReasoningEffortMapping(t *testing.T) {
|
||||
req := &AnthropicRequest{
|
||||
Model: "gpt-5.4",
|
||||
|
||||
@@ -2,6 +2,7 @@ package apicompat
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -290,6 +291,46 @@ func mustMarshalJSON(t *testing.T, value any) []byte {
|
||||
}
|
||||
|
||||
func TestChatCompletionsToResponses_MaxTokens(t *testing.T) {
|
||||
t.Run("default floor", func(t *testing.T) {
|
||||
maxTokens := 1
|
||||
req := &ChatCompletionsRequest{
|
||||
Model: "gpt-4o",
|
||||
MaxTokens: &maxTokens,
|
||||
Messages: []ChatMessage{{Role: "user", Content: json.RawMessage(`"Hi"`)}},
|
||||
}
|
||||
resp, err := ChatCompletionsToResponses(req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp.MaxOutputTokens)
|
||||
assert.Equal(t, DefaultMinMaxOutputTokens, *resp.MaxOutputTokens)
|
||||
})
|
||||
|
||||
t.Run("configured floor", func(t *testing.T) {
|
||||
for _, input := range []int{16, 64, 127, 128} {
|
||||
input := input
|
||||
t.Run(fmt.Sprintf("input_%d", input), func(t *testing.T) {
|
||||
req := &ChatCompletionsRequest{
|
||||
Model: "gpt-4o",
|
||||
MaxTokens: &input,
|
||||
Messages: []ChatMessage{{Role: "user", Content: json.RawMessage(`"Hi"`)}},
|
||||
}
|
||||
resp, err := ChatCompletionsToResponsesWithMinOutputTokens(req, 16)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp.MaxOutputTokens)
|
||||
assert.Equal(t, input, *resp.MaxOutputTokens)
|
||||
})
|
||||
}
|
||||
input := 1
|
||||
req := &ChatCompletionsRequest{
|
||||
Model: "gpt-4o",
|
||||
MaxTokens: &input,
|
||||
Messages: []ChatMessage{{Role: "user", Content: json.RawMessage(`"Hi"`)}},
|
||||
}
|
||||
resp, err := ChatCompletionsToResponsesWithMinOutputTokens(req, 16)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp.MaxOutputTokens)
|
||||
assert.Equal(t, 16, *resp.MaxOutputTokens)
|
||||
})
|
||||
|
||||
t.Run("max_tokens", func(t *testing.T) {
|
||||
maxTokens := 100
|
||||
req := &ChatCompletionsRequest{
|
||||
@@ -318,6 +359,21 @@ func TestChatCompletionsToResponses_MaxTokens(t *testing.T) {
|
||||
require.NotNil(t, resp.MaxOutputTokens)
|
||||
assert.Equal(t, 500, *resp.MaxOutputTokens)
|
||||
})
|
||||
|
||||
t.Run("max_completion_tokens_preferred before configured floor", func(t *testing.T) {
|
||||
maxTokens := 500
|
||||
maxCompletion := 1
|
||||
req := &ChatCompletionsRequest{
|
||||
Model: "gpt-4o",
|
||||
MaxTokens: &maxTokens,
|
||||
MaxCompletionTokens: &maxCompletion,
|
||||
Messages: []ChatMessage{{Role: "user", Content: json.RawMessage(`"Hi"`)}},
|
||||
}
|
||||
resp, err := ChatCompletionsToResponsesWithMinOutputTokens(req, 16)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp.MaxOutputTokens)
|
||||
assert.Equal(t, 16, *resp.MaxOutputTokens)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChatCompletionsToResponses_ReasoningEffort(t *testing.T) {
|
||||
|
||||
@@ -18,6 +18,12 @@ type chatMessageContent struct {
|
||||
// true. store is always false and reasoning.encrypted_content is always
|
||||
// included so that the response translator has full context.
|
||||
func ChatCompletionsToResponses(req *ChatCompletionsRequest) (*ResponsesRequest, error) {
|
||||
return ChatCompletionsToResponsesWithMinOutputTokens(req, DefaultMinMaxOutputTokens)
|
||||
}
|
||||
|
||||
// ChatCompletionsToResponsesWithMinOutputTokens converts a Chat Completions
|
||||
// request using the supplied max_output_tokens floor.
|
||||
func ChatCompletionsToResponsesWithMinOutputTokens(req *ChatCompletionsRequest, minOutputTokens int) (*ResponsesRequest, error) {
|
||||
if err := openai.ValidateGPT61SolReasoningEffort(req.Model, req.ReasoningEffort); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -62,8 +68,8 @@ func ChatCompletionsToResponses(req *ChatCompletionsRequest) (*ResponsesRequest,
|
||||
}
|
||||
if maxTokens > 0 {
|
||||
v := maxTokens
|
||||
if v < minMaxOutputTokens {
|
||||
v = minMaxOutputTokens
|
||||
if v < minOutputTokens {
|
||||
v = minOutputTokens
|
||||
}
|
||||
out.MaxOutputTokens = &v
|
||||
}
|
||||
|
||||
@@ -860,6 +860,9 @@ func (d ChatDelta) reasoningText() *string {
|
||||
// Shared constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// minMaxOutputTokens is the floor for max_output_tokens in a Responses request.
|
||||
// Very small values may cause upstream API errors, so we enforce a minimum.
|
||||
const minMaxOutputTokens = 128
|
||||
// DefaultMinMaxOutputTokens is the default floor for max_output_tokens in a
|
||||
// Responses request. Very small values may cause upstream API errors, so we
|
||||
// enforce a minimum.
|
||||
const DefaultMinMaxOutputTokens = 128
|
||||
|
||||
const minMaxOutputTokens = DefaultMinMaxOutputTokens
|
||||
|
||||
@@ -71,7 +71,7 @@ func (s *AntigravityGatewayService) ForwardAsChatCompletions(
|
||||
return nil, s.writeAntigravityCompatError(c, http.StatusBadRequest, "invalid_request_error", "model is required")
|
||||
}
|
||||
|
||||
responsesRequest, err := apicompat.ChatCompletionsToResponses(&request)
|
||||
responsesRequest, err := chatCompletionsToResponses(settingServiceConfig(s.settingService), &request)
|
||||
if err != nil {
|
||||
return nil, s.writeAntigravityCompatError(c, http.StatusBadRequest, "invalid_request_error", err.Error())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
|
||||
)
|
||||
|
||||
func configuredMinOutputTokens(cfg *config.Config) int {
|
||||
if cfg != nil && cfg.Gateway.MinOutputTokens >= 16 {
|
||||
return cfg.Gateway.MinOutputTokens
|
||||
}
|
||||
return apicompat.DefaultMinMaxOutputTokens
|
||||
}
|
||||
|
||||
func chatCompletionsToResponses(cfg *config.Config, req *apicompat.ChatCompletionsRequest) (*apicompat.ResponsesRequest, error) {
|
||||
return apicompat.ChatCompletionsToResponsesWithMinOutputTokens(req, configuredMinOutputTokens(cfg))
|
||||
}
|
||||
|
||||
func anthropicToResponses(cfg *config.Config, req *apicompat.AnthropicRequest) (*apicompat.ResponsesRequest, error) {
|
||||
return apicompat.AnthropicToResponsesWithMinOutputTokens(req, configuredMinOutputTokens(cfg))
|
||||
}
|
||||
|
||||
func settingServiceConfig(settingService *SettingService) *config.Config {
|
||||
if settingService == nil {
|
||||
return nil
|
||||
}
|
||||
return settingService.cfg
|
||||
}
|
||||
@@ -44,7 +44,7 @@ func (s *GatewayService) ForwardAsChatCompletions(
|
||||
clientStream := ccReq.Stream
|
||||
|
||||
// 2. Convert CC → Responses → Anthropic (chained conversion)
|
||||
responsesReq, err := apicompat.ChatCompletionsToResponses(&ccReq)
|
||||
responsesReq, err := chatCompletionsToResponses(s.cfg, &ccReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("convert chat completions to responses: %w", err)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func (s *GatewayService) ForwardAsResponses(
|
||||
) (*ForwardResult, error) {
|
||||
startTime := time.Now()
|
||||
|
||||
normalizedBody, normalized, err := normalizeOpenAIResponsesLegacyIngress(body)
|
||||
normalizedBody, normalized, err := normalizeOpenAIResponsesLegacyIngressWithConfig(s.cfg, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func (s *GeminiMessagesCompatService) ForwardAsChatCompletions(
|
||||
clientStream := ccReq.Stream
|
||||
includeUsage := ccReq.StreamOptions != nil && ccReq.StreamOptions.IncludeUsage
|
||||
|
||||
responsesReq, err := apicompat.ChatCompletionsToResponses(&ccReq)
|
||||
responsesReq, err := chatCompletionsToResponses(s.cfg, &ccReq)
|
||||
if err != nil {
|
||||
return nil, s.writeChatCompletionsError(c, http.StatusBadRequest, "invalid_request_error", err.Error())
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ func (s *OpenAIGatewayService) forwardAsChatCompletions(
|
||||
// Normal path: convert Chat Completions → Responses.
|
||||
// ChatCompletionsToResponses always sets Stream=true (upstream always streams).
|
||||
chatReq.Model = upstreamModel
|
||||
responsesReq, err = apicompat.ChatCompletionsToResponses(&chatReq)
|
||||
responsesReq, err = chatCompletionsToResponses(s.cfg, &chatReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("convert chat completions to responses: %w", err)
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ func (s *OpenAIGatewayService) forwardChatCompletionsViaNativeAnthropic(
|
||||
clientStream := ccReq.Stream
|
||||
|
||||
// 2. Convert CC → Responses → Anthropic (chained conversion)
|
||||
responsesReq, err := apicompat.ChatCompletionsToResponses(&ccReq)
|
||||
responsesReq, err := chatCompletionsToResponses(s.cfg, &ccReq)
|
||||
if err != nil {
|
||||
writeChatCompletionsError(c, http.StatusBadRequest, "invalid_request_error", "Failed to convert request")
|
||||
return nil, fmt.Errorf("convert chat completions to responses: %w", err)
|
||||
|
||||
@@ -65,7 +65,7 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
|
||||
if normalized {
|
||||
body = normalizedBody
|
||||
}
|
||||
legacyIngressBody, legacyIngressChanged, legacyIngressErr := normalizeOpenAIResponsesLegacyIngress(body)
|
||||
legacyIngressBody, legacyIngressChanged, legacyIngressErr := normalizeOpenAIResponsesLegacyIngressWithConfig(s.cfg, body)
|
||||
if legacyIngressErr != nil {
|
||||
return nil, legacyIngressErr
|
||||
}
|
||||
|
||||
@@ -541,7 +541,7 @@ func (s *OpenAIGatewayService) forwardGrokChatCompletionsViaResponses(
|
||||
return s.forwardAsRawChatCompletions(ctx, c, account, body, defaultMappedModel)
|
||||
}
|
||||
|
||||
responsesReq, err := apicompat.ChatCompletionsToResponses(&chatReq)
|
||||
responsesReq, err := chatCompletionsToResponses(s.cfg, &chatReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("convert grok chat completions to responses: %w", err)
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ func (s *OpenAIGatewayService) ForwardAsAnthropic(
|
||||
|
||||
// 3. Convert Anthropic → Responses after compatibility-only replay guard.
|
||||
anthropicReq.Model = upstreamModel
|
||||
responsesReq, err := apicompat.AnthropicToResponses(&anthropicReq)
|
||||
responsesReq, err := anthropicToResponses(s.cfg, &anthropicReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("convert anthropic to responses: %w", err)
|
||||
}
|
||||
|
||||
@@ -52,7 +52,10 @@ func (s *OpenAIGatewayService) forwardAnthropicViaRawChatCompletions(
|
||||
clientStream := anthropicReq.Stream
|
||||
|
||||
// 2. Anthropic → Chat Completions (direct, no Responses intermediary)
|
||||
chatReq, err := apicompat.AnthropicToChatCompletionsRequest(&anthropicReq)
|
||||
chatReq, err := apicompat.AnthropicToChatCompletionsRequestWithMinOutputTokens(
|
||||
&anthropicReq,
|
||||
configuredMinOutputTokens(s.cfg),
|
||||
)
|
||||
if err != nil {
|
||||
writeAnthropicError(c, http.StatusBadRequest, "invalid_request_error", err.Error())
|
||||
return nil, fmt.Errorf("convert anthropic to chat completions: %w", err)
|
||||
|
||||
@@ -214,6 +214,7 @@ func (s *OpenAIGatewayService) forwardOpenAIPassthrough(
|
||||
if account != nil && account.IsOpenAI() {
|
||||
responsesLite := isOpenAIResponsesLiteHeader(c.GetHeader(responsesLiteHeader)) || isOpenAIResponsesLiteWebSocketPayload(body)
|
||||
normalizedBody, normalized, normalizeErr := normalizeOpenAIResponsesCompatibilityBodyWithOptions(body, account, openAIResponsesCompatibilityOptions{
|
||||
Config: s.cfg,
|
||||
ResponsesLite: responsesLite,
|
||||
Compact: isOpenAIResponsesCompactPath(c),
|
||||
})
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
|
||||
"github.com/Wei-Shaw/sub2api/internal/util/urlvalidator"
|
||||
@@ -1433,7 +1434,12 @@ func normalizeOpenAIResponsesWebSocketCompatibilityBody(body []byte, account *Ac
|
||||
return normalizeOpenAIResponsesCompatibilityBodyWithOptions(body, account, openAIResponsesCompatibilityOptions{ResponsesLite: responsesLite})
|
||||
}
|
||||
|
||||
func normalizeOpenAIResponsesWebSocketCompatibilityBodyWithConfig(cfg *config.Config, body []byte, account *Account, responsesLite bool) ([]byte, bool, error) {
|
||||
return normalizeOpenAIResponsesCompatibilityBodyWithOptions(body, account, openAIResponsesCompatibilityOptions{Config: cfg, ResponsesLite: responsesLite})
|
||||
}
|
||||
|
||||
type openAIResponsesCompatibilityOptions struct {
|
||||
Config *config.Config
|
||||
ResponsesLite bool
|
||||
// Compact marks the /responses/compact wire shape, which is left as-is
|
||||
// by request-shape compatibility rewrites such as web_search history.
|
||||
@@ -1449,7 +1455,7 @@ func normalizeOpenAIResponsesCompatibilityBodyWithOptions(body []byte, account *
|
||||
changed := false
|
||||
if account.IsOpenAIOAuthLike() {
|
||||
var err error
|
||||
normalized, changed, err = normalizeOpenAIResponsesLegacyIngress(body)
|
||||
normalized, changed, err = normalizeOpenAIResponsesLegacyIngressWithConfig(opts.Config, body)
|
||||
if err != nil {
|
||||
return body, false, err
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
@@ -13,6 +14,10 @@ import (
|
||||
// payloads that a few Responses clients still emit. Native Responses input is
|
||||
// always authoritative because this path has no separate full-replay attempt.
|
||||
func normalizeOpenAIResponsesLegacyIngress(body []byte) ([]byte, bool, error) {
|
||||
return normalizeOpenAIResponsesLegacyIngressWithConfig(nil, body)
|
||||
}
|
||||
|
||||
func normalizeOpenAIResponsesLegacyIngressWithConfig(cfg *config.Config, body []byte) ([]byte, bool, error) {
|
||||
if len(body) == 0 {
|
||||
return body, false, nil
|
||||
}
|
||||
@@ -43,7 +48,7 @@ func normalizeOpenAIResponsesLegacyIngress(body []byte) ([]byte, bool, error) {
|
||||
if hasMessages {
|
||||
messages, messagesAreArray := messagesValue.([]any)
|
||||
if messagesAreArray && len(messages) > 0 {
|
||||
legacy, err := convertLegacyResponsesMessages(body)
|
||||
legacy, err := convertLegacyResponsesMessages(cfg, body)
|
||||
if err != nil {
|
||||
return body, false, err
|
||||
}
|
||||
@@ -105,13 +110,13 @@ type convertedLegacyResponsesMessages struct {
|
||||
hasReasoningEffort bool
|
||||
}
|
||||
|
||||
func convertLegacyResponsesMessages(body []byte) (convertedLegacyResponsesMessages, error) {
|
||||
func convertLegacyResponsesMessages(cfg *config.Config, body []byte) (convertedLegacyResponsesMessages, error) {
|
||||
var converted convertedLegacyResponsesMessages
|
||||
var chatRequest apicompat.ChatCompletionsRequest
|
||||
if err := json.Unmarshal(body, &chatRequest); err != nil {
|
||||
return converted, fmt.Errorf("normalize legacy Responses messages: %w", err)
|
||||
}
|
||||
responsesRequest, err := apicompat.ChatCompletionsToResponses(&chatRequest)
|
||||
responsesRequest, err := chatCompletionsToResponses(cfg, &chatRequest)
|
||||
if err != nil {
|
||||
return converted, fmt.Errorf("normalize legacy Responses messages: %w", err)
|
||||
}
|
||||
|
||||
@@ -269,7 +269,7 @@ func (s *OpenAIGatewayService) ProxyResponsesWebSocketFromClient(
|
||||
normalized = next
|
||||
}
|
||||
responsesLite := isOpenAIResponsesLiteWebSocketPayload(normalized)
|
||||
if compatibilityBody, compatibilityChanged, compatibilityErr := normalizeOpenAIResponsesWebSocketCompatibilityBody(normalized, account, responsesLite); compatibilityErr != nil {
|
||||
if compatibilityBody, compatibilityChanged, compatibilityErr := normalizeOpenAIResponsesWebSocketCompatibilityBodyWithConfig(s.cfg, normalized, account, responsesLite); compatibilityErr != nil {
|
||||
return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "invalid websocket request payload", compatibilityErr)
|
||||
} else if compatibilityChanged {
|
||||
normalized = compatibilityBody
|
||||
|
||||
@@ -746,7 +746,7 @@ func (s *OpenAIGatewayService) proxyResponsesWebSocketV2Passthrough(
|
||||
firstClientMessage = s.ReplaceModelInBody(firstClientMessage, capturedSessionModel)
|
||||
}
|
||||
firstMessageResponsesLite := isOpenAIResponsesLiteWebSocketPayload(firstClientMessage)
|
||||
if normalized, compatibilityChanged, normalizeErr := normalizeOpenAIResponsesWebSocketCompatibilityBody(firstClientMessage, account, firstMessageResponsesLite); normalizeErr != nil {
|
||||
if normalized, compatibilityChanged, normalizeErr := normalizeOpenAIResponsesWebSocketCompatibilityBodyWithConfig(s.cfg, firstClientMessage, account, firstMessageResponsesLite); normalizeErr != nil {
|
||||
return fmt.Errorf("normalize first websocket response.create: %w", normalizeErr)
|
||||
} else if compatibilityChanged {
|
||||
firstClientMessage = normalized
|
||||
@@ -998,7 +998,7 @@ func (s *OpenAIGatewayService) proxyResponsesWebSocketV2Passthrough(
|
||||
}
|
||||
responsesLite := isResponseCreate && isOpenAIResponsesLiteWebSocketPayload(payload)
|
||||
if isResponseCreate {
|
||||
if normalized, compatibilityChanged, normalizeErr := normalizeOpenAIResponsesWebSocketCompatibilityBody(payload, account, responsesLite); normalizeErr != nil {
|
||||
if normalized, compatibilityChanged, normalizeErr := normalizeOpenAIResponsesWebSocketCompatibilityBodyWithConfig(s.cfg, payload, account, responsesLite); normalizeErr != nil {
|
||||
return payload, nil, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "invalid websocket request payload", normalizeErr)
|
||||
} else if compatibilityChanged {
|
||||
payload = normalized
|
||||
|
||||
@@ -217,6 +217,9 @@ security:
|
||||
# 网关配置
|
||||
# =============================================================================
|
||||
gateway:
|
||||
# Minimum max_output_tokens when converting Chat Completions or Anthropic requests to Responses (minimum 16, default 128)
|
||||
# Chat Completions 或 Anthropic 请求转换为 Responses 时的最小 max_output_tokens(最小 16,默认 128)
|
||||
min_output_tokens: 128
|
||||
# Timeout for waiting upstream response headers (seconds)
|
||||
# 等待上游响应头超时时间(秒)
|
||||
response_header_timeout: 600
|
||||
|
||||
Reference in New Issue
Block a user