mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-21 14:19:18 +08:00
@@ -185,6 +185,10 @@ func patchGrokResponsesBody(body []byte, upstreamModel string) ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out, err = sanitizeGrokReasoningNullContent(out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out, err = sanitizeGrokResponsesTools(out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -304,6 +308,35 @@ func sanitizeGrokResponsesInput(body []byte) ([]byte, error) {
|
||||
return sjson.SetRawBytes(body, "input", encoded)
|
||||
}
|
||||
|
||||
// sanitizeGrokReasoningNullContent 删除 reasoning 项中的 "content": null。
|
||||
// xAI 的 untagged enum 反序列化器拒收该字段,返回 422。
|
||||
func sanitizeGrokReasoningNullContent(body []byte) ([]byte, error) {
|
||||
input := gjson.GetBytes(body, "input")
|
||||
if !input.Exists() || !input.IsArray() {
|
||||
return body, nil
|
||||
}
|
||||
|
||||
items := input.Array()
|
||||
changed := false
|
||||
for i := len(items) - 1; i >= 0; i-- {
|
||||
item := items[i]
|
||||
if strings.TrimSpace(item.Get("type").String()) != "reasoning" {
|
||||
continue
|
||||
}
|
||||
contentResult := item.Get("content")
|
||||
if contentResult.Exists() && contentResult.Type == gjson.Null {
|
||||
var err error
|
||||
body, err = sjson.DeleteBytes(body, fmt.Sprintf("input.%d.content", i))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
_ = changed
|
||||
return body, nil
|
||||
}
|
||||
|
||||
var grokResponsesSupportedToolTypes = map[string]struct{}{
|
||||
"code_execution": {},
|
||||
"code_interpreter": {},
|
||||
|
||||
@@ -1771,3 +1771,70 @@ func TestFailoverOpenAIUpstreamHTTPErrorUsesOnlyGrokRateLimitPolicy(t *testing.T
|
||||
require.Equal(t, 1, repo.rateLimitedCalls)
|
||||
require.Zero(t, repo.tempUnschedCalls)
|
||||
}
|
||||
|
||||
func TestPatchGrokResponsesBody_StripsReasoningContentNull(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
body := []byte(`{
|
||||
"model": "grok-latest",
|
||||
"input": [
|
||||
{"type":"message","role":"user","content":[{"type":"input_text","text":"hi"}]},
|
||||
{"type":"reasoning","summary":[{"type":"summary_text","text":"thinking..."}],"content":null,"encrypted_content":null},
|
||||
{"type":"message","role":"assistant","content":[{"type":"output_text","text":"Hello!"}]}
|
||||
]
|
||||
}`)
|
||||
|
||||
patched, err := patchGrokResponsesBody(body, "grok-4.5")
|
||||
require.NoError(t, err)
|
||||
require.True(t, json.Valid(patched))
|
||||
|
||||
input := gjson.GetBytes(patched, "input")
|
||||
require.True(t, input.IsArray())
|
||||
|
||||
items := input.Array()
|
||||
require.Len(t, items, 3)
|
||||
|
||||
reasoning := items[1]
|
||||
require.Equal(t, "reasoning", reasoning.Get("type").String())
|
||||
require.True(t, reasoning.Get("summary").Exists(), "summary should be preserved")
|
||||
require.False(t, reasoning.Get("content").Exists(), "content: null should be stripped")
|
||||
}
|
||||
|
||||
func TestPatchGrokResponsesBody_KeepsReasoningContentNonNull(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
body := []byte(`{
|
||||
"model": "grok-latest",
|
||||
"input": [
|
||||
{"type":"reasoning","summary":[{"type":"summary_text","text":"ok"}],"content":"real content"}
|
||||
]
|
||||
}`)
|
||||
|
||||
patched, err := patchGrokResponsesBody(body, "grok-4.5")
|
||||
require.NoError(t, err)
|
||||
|
||||
reasoning := gjson.GetBytes(patched, "input.0")
|
||||
require.Equal(t, "real content", reasoning.Get("content").String(), "non-null content must not be stripped")
|
||||
}
|
||||
|
||||
func TestPatchGrokResponsesBody_MultipleReasoningContentNull(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
body := []byte(`{
|
||||
"model": "grok-latest",
|
||||
"input": [
|
||||
{"type":"reasoning","summary":[{"type":"summary_text","text":"r1"}],"content":null},
|
||||
{"type":"message","role":"user","content":"hi"},
|
||||
{"type":"reasoning","summary":[{"type":"summary_text","text":"r2"}],"content":null}
|
||||
]
|
||||
}`)
|
||||
|
||||
patched, err := patchGrokResponsesBody(body, "grok-4.5")
|
||||
require.NoError(t, err)
|
||||
|
||||
items := gjson.GetBytes(patched, "input").Array()
|
||||
require.Len(t, items, 3)
|
||||
|
||||
require.False(t, items[0].Get("content").Exists())
|
||||
require.False(t, items[2].Get("content").Exists())
|
||||
}
|
||||
|
||||
@@ -136,12 +136,20 @@ func sanitizeEncryptedReasoningInputItem(item any) (next any, changed bool, keep
|
||||
return item, false, true
|
||||
}
|
||||
|
||||
_, hasEncryptedContent := inputItem["encrypted_content"]
|
||||
if !hasEncryptedContent {
|
||||
return item, false, true
|
||||
if _, has := inputItem["encrypted_content"]; has {
|
||||
delete(inputItem, "encrypted_content")
|
||||
changed = true
|
||||
}
|
||||
|
||||
delete(inputItem, "encrypted_content")
|
||||
// xAI 422: "content": null 导致 untagged enum 反序列化失败
|
||||
if v, has := inputItem["content"]; has && v == nil {
|
||||
delete(inputItem, "content")
|
||||
changed = true
|
||||
}
|
||||
|
||||
if !changed {
|
||||
return item, false, true
|
||||
}
|
||||
if len(inputItem) == 1 {
|
||||
return nil, true, false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTrimOpenAIEncryptedReasoningItems_ContentNull(t *testing.T) {
|
||||
reqBody := map[string]any{
|
||||
"model": "grok-4.5",
|
||||
"input": []any{
|
||||
map[string]any{"type": "message", "role": "user", "content": "hi"},
|
||||
map[string]any{
|
||||
"type": "reasoning",
|
||||
"summary": []any{map[string]any{"type": "summary_text", "text": "thinking..."}},
|
||||
"content": nil,
|
||||
"encrypted_content": nil,
|
||||
},
|
||||
map[string]any{"type": "message", "role": "assistant", "content": "Hello!"},
|
||||
},
|
||||
}
|
||||
|
||||
changed := trimOpenAIEncryptedReasoningItems(reqBody)
|
||||
require.True(t, changed)
|
||||
|
||||
input := reqBody["input"].([]any)
|
||||
require.Len(t, input, 3)
|
||||
|
||||
reasoning := input[1].(map[string]any)
|
||||
assert.Equal(t, "reasoning", reasoning["type"])
|
||||
assert.NotNil(t, reasoning["summary"])
|
||||
_, hasContent := reasoning["content"]
|
||||
assert.False(t, hasContent, "content: null should be stripped")
|
||||
_, hasEncrypted := reasoning["encrypted_content"]
|
||||
assert.False(t, hasEncrypted, "encrypted_content should be stripped")
|
||||
}
|
||||
|
||||
func TestTrimOpenAIEncryptedReasoningItems_ContentNullOnly(t *testing.T) {
|
||||
reqBody := map[string]any{
|
||||
"model": "grok-4.5",
|
||||
"input": []any{
|
||||
map[string]any{
|
||||
"type": "reasoning",
|
||||
"summary": []any{map[string]any{"type": "summary_text", "text": "ok"}},
|
||||
"content": nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
changed := trimOpenAIEncryptedReasoningItems(reqBody)
|
||||
require.True(t, changed)
|
||||
|
||||
input := reqBody["input"].([]any)
|
||||
require.Len(t, input, 1)
|
||||
|
||||
reasoning := input[0].(map[string]any)
|
||||
_, hasContent := reasoning["content"]
|
||||
assert.False(t, hasContent, "content: null should be stripped even without encrypted_content")
|
||||
}
|
||||
|
||||
func TestTrimOpenAIEncryptedReasoningItems_ContentNonNull(t *testing.T) {
|
||||
reqBody := map[string]any{
|
||||
"model": "grok-4.5",
|
||||
"input": []any{
|
||||
map[string]any{
|
||||
"type": "reasoning",
|
||||
"summary": []any{map[string]any{"type": "summary_text", "text": "ok"}},
|
||||
"content": "some actual content",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
changed := trimOpenAIEncryptedReasoningItems(reqBody)
|
||||
assert.False(t, changed, "non-null content should not be stripped")
|
||||
|
||||
input := reqBody["input"].([]any)
|
||||
reasoning := input[0].(map[string]any)
|
||||
assert.Equal(t, "some actual content", reasoning["content"])
|
||||
}
|
||||
|
||||
func TestTrimOpenAIEncryptedReasoningItems_NoReasoningItems(t *testing.T) {
|
||||
reqBody := map[string]any{
|
||||
"model": "grok-4.5",
|
||||
"input": []any{
|
||||
map[string]any{"type": "message", "role": "user", "content": "hi"},
|
||||
},
|
||||
}
|
||||
|
||||
changed := trimOpenAIEncryptedReasoningItems(reqBody)
|
||||
assert.False(t, changed)
|
||||
}
|
||||
|
||||
func TestTrimOpenAIEncryptedReasoningItems_ContentNullDropsBareSkeleton(t *testing.T) {
|
||||
reqBody := map[string]any{
|
||||
"input": []any{
|
||||
map[string]any{"type": "reasoning", "content": nil},
|
||||
},
|
||||
}
|
||||
|
||||
changed := trimOpenAIEncryptedReasoningItems(reqBody)
|
||||
require.True(t, changed)
|
||||
_, hasInput := reqBody["input"]
|
||||
assert.False(t, hasInput, "bare reasoning skeleton should be dropped, emptying input")
|
||||
}
|
||||
Reference in New Issue
Block a user