mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-16 15:43:13 +08:00
feat(composite): enforce reasoning effort policy
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func ensureCompositeTargetPlatform(c *gin.Context, apiKey *service.APIKey, model string) {
|
||||
@@ -55,3 +58,42 @@ func effectiveAPIKeyPlatform(c *gin.Context, apiKey *service.APIKey) string {
|
||||
}
|
||||
return apiKey.Group.Platform
|
||||
}
|
||||
|
||||
func openAIReasoningEffortPolicyForRequest(c *gin.Context, apiKey *service.APIKey) (string, []service.ReasoningEffortMapping, bool) {
|
||||
if apiKey == nil || apiKey.Group == nil {
|
||||
return "", nil, false
|
||||
}
|
||||
if apiKey.Group.Platform != service.PlatformOpenAI && apiKey.Group.Platform != service.PlatformComposite {
|
||||
return "", nil, false
|
||||
}
|
||||
if effectiveAPIKeyPlatform(c, apiKey) != service.PlatformOpenAI {
|
||||
return "", nil, false
|
||||
}
|
||||
return apiKey.Group.MaxReasoningEffort, apiKey.Group.ReasoningEffortMappings, true
|
||||
}
|
||||
|
||||
func applyOpenAIReasoningEffortPolicyForRequest(c *gin.Context, apiKey *service.APIKey, body []byte) ([]byte, bool) {
|
||||
maxEffort, mappings, ok := openAIReasoningEffortPolicyForRequest(c, apiKey)
|
||||
if !ok {
|
||||
return body, false
|
||||
}
|
||||
return service.ApplyOpenAIReasoningEffortPolicy(body, maxEffort, mappings)
|
||||
}
|
||||
|
||||
func bindOpenAIReasoningEffortPolicyForMessagesRequest(c *gin.Context, apiKey *service.APIKey, body []byte) {
|
||||
if c == nil || c.Request == nil {
|
||||
return
|
||||
}
|
||||
// The Messages bridge synthesizes a default OpenAI effort when
|
||||
// output_config.effort is omitted. Bind the group policy only for an
|
||||
// explicit client value so the ceiling does not alter that default.
|
||||
effort := gjson.GetBytes(body, "output_config.effort")
|
||||
if !effort.Exists() || effort.Type != gjson.String || strings.TrimSpace(effort.String()) == "" {
|
||||
return
|
||||
}
|
||||
maxEffort, mappings, ok := openAIReasoningEffortPolicyForRequest(c, apiKey)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
c.Request = c.Request.WithContext(service.WithOpenAIReasoningEffortPolicy(c.Request.Context(), maxEffort, mappings))
|
||||
}
|
||||
|
||||
@@ -77,6 +77,46 @@ func TestCompositeTargetPlatformResolvedAllowsConcreteGroupWithoutResolution(t *
|
||||
require.True(t, compositeTargetPlatformResolved(c, apiKey, "llama-4-maverick"))
|
||||
}
|
||||
|
||||
func TestOpenAIReasoningEffortPolicyForCompositeTarget(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
group := &service.Group{
|
||||
Platform: service.PlatformComposite,
|
||||
MaxReasoningEffort: "medium",
|
||||
ReasoningEffortMappings: []service.ReasoningEffortMapping{
|
||||
{From: "max", To: "xhigh"},
|
||||
},
|
||||
}
|
||||
apiKey := &service.APIKey{Group: group}
|
||||
body := []byte(`{"reasoning":{"effort":"max"}}`)
|
||||
|
||||
openAICtx, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
openAICtx.Request = httptest.NewRequest("POST", "/v1/responses", nil)
|
||||
openAICtx.Request = openAICtx.Request.WithContext(service.WithResolvedTargetPlatform(openAICtx.Request.Context(), service.PlatformOpenAI))
|
||||
got, changed := applyOpenAIReasoningEffortPolicyForRequest(openAICtx, apiKey, body)
|
||||
require.True(t, changed)
|
||||
require.JSONEq(t, `{"reasoning":{"effort":"medium"}}`, string(got))
|
||||
|
||||
bindOpenAIReasoningEffortPolicyForMessagesRequest(openAICtx, apiKey, []byte(`{"output_config":{"effort":"max"}}`))
|
||||
bound, changed := service.ApplyOpenAIReasoningEffortPolicyFromContext(openAICtx.Request.Context(), body)
|
||||
require.True(t, changed)
|
||||
require.JSONEq(t, `{"reasoning":{"effort":"medium"}}`, string(bound))
|
||||
|
||||
omittedCtx, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
omittedCtx.Request = httptest.NewRequest("POST", "/v1/messages", nil)
|
||||
omittedCtx.Request = omittedCtx.Request.WithContext(service.WithResolvedTargetPlatform(omittedCtx.Request.Context(), service.PlatformOpenAI))
|
||||
bindOpenAIReasoningEffortPolicyForMessagesRequest(omittedCtx, apiKey, []byte(`{"model":"gpt-5"}`))
|
||||
omitted, changed := service.ApplyOpenAIReasoningEffortPolicyFromContext(omittedCtx.Request.Context(), body)
|
||||
require.False(t, changed)
|
||||
require.Equal(t, body, omitted)
|
||||
|
||||
grokCtx, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
grokCtx.Request = httptest.NewRequest("POST", "/v1/responses", nil)
|
||||
grokCtx.Request = grokCtx.Request.WithContext(service.WithResolvedTargetPlatform(grokCtx.Request.Context(), service.PlatformGrok))
|
||||
got, changed = applyOpenAIReasoningEffortPolicyForRequest(grokCtx, apiKey, body)
|
||||
require.False(t, changed)
|
||||
require.Equal(t, body, got)
|
||||
}
|
||||
|
||||
func TestClientRequestedModelUsesCompositePublicModel(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
|
||||
@@ -80,10 +80,8 @@ func (h *OpenAIGatewayHandler) ChatCompletions(c *gin.Context) {
|
||||
h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Model is not supported by this OpenAI-compatible endpoint for composite groups")
|
||||
return
|
||||
}
|
||||
if apiKey.Group != nil && apiKey.Group.Platform == service.PlatformOpenAI {
|
||||
if cappedBody, changed := service.ApplyOpenAIReasoningEffortPolicy(body, apiKey.Group.MaxReasoningEffort, apiKey.Group.ReasoningEffortMappings); changed {
|
||||
body = cappedBody
|
||||
}
|
||||
if cappedBody, changed := applyOpenAIReasoningEffortPolicyForRequest(c, apiKey, body); changed {
|
||||
body = cappedBody
|
||||
}
|
||||
reqStream, ok := parseOpenAICompatibleStream(body)
|
||||
if !ok {
|
||||
|
||||
@@ -312,10 +312,8 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
|
||||
h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Model is not supported by this OpenAI-compatible endpoint for composite groups")
|
||||
return
|
||||
}
|
||||
if apiKey.Group != nil && apiKey.Group.Platform == service.PlatformOpenAI {
|
||||
if cappedBody, changed := service.ApplyOpenAIReasoningEffortPolicy(body, apiKey.Group.MaxReasoningEffort, apiKey.Group.ReasoningEffortMappings); changed {
|
||||
body = cappedBody
|
||||
}
|
||||
if cappedBody, changed := applyOpenAIReasoningEffortPolicyForRequest(c, apiKey, body); changed {
|
||||
body = cappedBody
|
||||
}
|
||||
|
||||
reqStream, ok := parseOpenAICompatibleStream(body)
|
||||
@@ -932,6 +930,7 @@ func (h *OpenAIGatewayHandler) Messages(c *gin.Context) {
|
||||
h.anthropicErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "Model is not supported by this OpenAI-compatible endpoint for composite groups")
|
||||
return
|
||||
}
|
||||
bindOpenAIReasoningEffortPolicyForMessagesRequest(c, apiKey, body)
|
||||
routingModel := service.NormalizeOpenAICompatRequestedModel(reqModel)
|
||||
preferredMappedModel := resolveOpenAIMessagesDispatchMappedModel(apiKey, reqModel)
|
||||
reqStream := gjson.GetBytes(body, "stream").Bool()
|
||||
@@ -1813,12 +1812,7 @@ func (h *OpenAIGatewayHandler) ResponsesWebSocket(c *gin.Context) {
|
||||
zap.Int("candidate_count", scheduleDecision.CandidateCount),
|
||||
)
|
||||
|
||||
maxReasoningEffort := ""
|
||||
var reasoningEffortMappings []service.ReasoningEffortMapping
|
||||
if apiKey.Group != nil && apiKey.Group.Platform == service.PlatformOpenAI {
|
||||
maxReasoningEffort = apiKey.Group.MaxReasoningEffort
|
||||
reasoningEffortMappings = apiKey.Group.ReasoningEffortMappings
|
||||
}
|
||||
maxReasoningEffort, reasoningEffortMappings, _ := openAIReasoningEffortPolicyForRequest(c, apiKey)
|
||||
var requestPayloadHash string
|
||||
// Passthrough rejects overlapping response.create frames, so one immutable
|
||||
// turn-tagged slot preserves the exact mapping used for the in-flight request.
|
||||
|
||||
Reference in New Issue
Block a user