归一化优化

This commit is contained in:
CJACK
2026-04-26 04:44:55 +08:00
parent 7475defeca
commit df61f06d9a
12 changed files with 248 additions and 31 deletions

View File

@@ -27,11 +27,32 @@ func TestNormalizeClaudeRequestUsesGlobalAliasMapping(t *testing.T) {
if out.Standard.ResolvedModel != "deepseek-v4-pro-search" {
t.Fatalf("resolved model mismatch: got=%q", out.Standard.ResolvedModel)
}
if !out.Standard.Thinking || !out.Standard.Search {
if out.Standard.Thinking || !out.Standard.Search {
t.Fatalf("unexpected flags: thinking=%v search=%v", out.Standard.Thinking, out.Standard.Search)
}
}
func TestNormalizeClaudeRequestEnablesThinkingWhenRequested(t *testing.T) {
req := map[string]any{
"model": "claude-opus-4-6",
"messages": []any{
map[string]any{"role": "user", "content": "hello"},
},
"thinking": map[string]any{"type": "enabled", "budget_tokens": 1024},
}
out, err := normalizeClaudeRequest(mockClaudeConfig{
aliases: map[string]string{
"claude-opus-4-6": "deepseek-v4-pro",
},
}, req)
if err != nil {
t.Fatalf("normalizeClaudeRequest error: %v", err)
}
if !out.Standard.Thinking {
t.Fatalf("expected explicit Claude thinking request to enable downstream thinking")
}
}
func TestNormalizeClaudeRequestPrefersGlobalAliasMapping(t *testing.T) {
req := map[string]any{
"model": "claude-sonnet-4-6",

View File

@@ -52,7 +52,7 @@ func (h *Handler) proxyViaOpenAI(w http.ResponseWriter, r *http.Request, store C
}
}
translatedReq := translatorcliproxy.ToOpenAI(sdktranslator.FormatClaude, translateModel, raw, stream)
translatedReq = applyExplicitThinkingOverrideToOpenAIRequest(translatedReq, req)
translatedReq = applyClaudeThinkingPolicyToOpenAIRequest(translatedReq, req)
isVercelPrepare := strings.TrimSpace(r.URL.Query().Get("__stream_prepare")) == "1"
isVercelRelease := strings.TrimSpace(r.URL.Query().Get("__stream_release")) == "1"
@@ -124,15 +124,18 @@ func (h *Handler) proxyViaOpenAI(w http.ResponseWriter, r *http.Request, store C
return true
}
func applyExplicitThinkingOverrideToOpenAIRequest(translated []byte, original map[string]any) []byte {
enabled, ok := util.ResolveThinkingOverride(original)
if !ok {
return translated
}
func applyClaudeThinkingPolicyToOpenAIRequest(translated []byte, original map[string]any) []byte {
req := map[string]any{}
if err := json.Unmarshal(translated, &req); err != nil {
return translated
}
enabled, ok := util.ResolveThinkingOverride(original)
if !ok {
if _, translatedHasOverride := util.ResolveThinkingOverride(req); translatedHasOverride {
return translated
}
enabled = false
}
typ := "disabled"
if enabled {
typ = "enabled"

View File

@@ -126,6 +126,46 @@ func TestClaudeProxyViaOpenAIPreservesThinkingOverride(t *testing.T) {
}
}
func TestClaudeProxyViaOpenAIDisablesThinkingByDefault(t *testing.T) {
openAI := &openAIProxyCaptureStub{}
h := &Handler{
Store: claudeProxyStoreStub{aliases: map[string]string{"claude-sonnet-4-6": "deepseek-v4-flash"}},
OpenAI: openAI,
}
req := httptest.NewRequest(http.MethodPost, "/anthropic/v1/messages", strings.NewReader(`{"model":"claude-sonnet-4-6","messages":[{"role":"user","content":"hi"}],"stream":false}`))
rec := httptest.NewRecorder()
h.Messages(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String())
}
thinking, _ := openAI.seenReq["thinking"].(map[string]any)
if thinking["type"] != "disabled" {
t.Fatalf("expected Claude default to disable downstream thinking, got %#v", openAI.seenReq)
}
}
func TestClaudeProxyViaOpenAIEnablesThinkingWhenRequested(t *testing.T) {
openAI := &openAIProxyCaptureStub{}
h := &Handler{
Store: claudeProxyStoreStub{aliases: map[string]string{"claude-sonnet-4-6": "deepseek-v4-flash"}},
OpenAI: openAI,
}
req := httptest.NewRequest(http.MethodPost, "/anthropic/v1/messages", strings.NewReader(`{"model":"claude-sonnet-4-6","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"enabled","budget_tokens":1024},"stream":false}`))
rec := httptest.NewRecorder()
h.Messages(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String())
}
thinking, _ := openAI.seenReq["thinking"].(map[string]any)
if thinking["type"] != "enabled" {
t.Fatalf("expected Claude explicit thinking to enable downstream thinking, got %#v", openAI.seenReq)
}
}
func TestClaudeProxyTranslatesInlineImageToOpenAIDataURL(t *testing.T) {
openAI := &openAIProxyCaptureStub{}
h := &Handler{OpenAI: openAI}

View File

@@ -31,12 +31,11 @@ func normalizeClaudeRequest(store ConfigReader, req map[string]any) (claudeNorma
dsPayload := convertClaudeToDeepSeek(payload, store)
dsModel, _ := dsPayload["model"].(string)
defaultThinkingEnabled, searchEnabled, ok := config.GetModelConfig(dsModel)
_, searchEnabled, ok := config.GetModelConfig(dsModel)
if !ok {
defaultThinkingEnabled = false
searchEnabled = false
}
thinkingEnabled := util.ResolveThinkingEnabled(req, defaultThinkingEnabled)
thinkingEnabled := util.ResolveThinkingEnabled(req, false)
finalPrompt := deepseek.MessagesPrepareWithThinking(toMessageMaps(dsPayload["messages"]), thinkingEnabled)
toolNames := extractClaudeToolNames(toolsRequested)
if len(toolNames) == 0 && len(toolsRequested) > 0 {

View File

@@ -36,6 +36,11 @@ func (h *Handler) proxyViaOpenAI(w http.ResponseWriter, r *http.Request, stream
return true
}
routeModel := strings.TrimSpace(chi.URLParam(r, "model"))
var req map[string]any
if err := json.Unmarshal(raw, &req); err != nil {
writeGeminiError(w, http.StatusBadRequest, "invalid json")
return true
}
translatedReq := translatorcliproxy.ToOpenAI(sdktranslator.FormatGemini, routeModel, raw, stream)
if !strings.Contains(string(translatedReq), `"stream"`) {
var reqMap map[string]any
@@ -46,6 +51,7 @@ func (h *Handler) proxyViaOpenAI(w http.ResponseWriter, r *http.Request, stream
}
}
}
translatedReq = applyGeminiThinkingPolicyToOpenAIRequest(translatedReq, req)
isVercelPrepare := strings.TrimSpace(r.URL.Query().Get("__stream_prepare")) == "1"
isVercelRelease := strings.TrimSpace(r.URL.Query().Get("__stream_release")) == "1"
@@ -116,6 +122,72 @@ func (h *Handler) proxyViaOpenAI(w http.ResponseWriter, r *http.Request, stream
return true
}
func applyGeminiThinkingPolicyToOpenAIRequest(translated []byte, original map[string]any) []byte {
req := map[string]any{}
if err := json.Unmarshal(translated, &req); err != nil {
return translated
}
enabled, ok := resolveGeminiThinkingOverride(original)
if !ok {
return translated
}
typ := "disabled"
if enabled {
typ = "enabled"
}
req["thinking"] = map[string]any{"type": typ}
out, err := json.Marshal(req)
if err != nil {
return translated
}
return out
}
func resolveGeminiThinkingOverride(req map[string]any) (bool, bool) {
generationConfig, ok := req["generationConfig"].(map[string]any)
if !ok {
generationConfig, ok = req["generation_config"].(map[string]any)
}
if !ok {
return false, false
}
thinkingConfig, ok := generationConfig["thinkingConfig"].(map[string]any)
if !ok {
thinkingConfig, ok = generationConfig["thinking_config"].(map[string]any)
}
if !ok {
return false, false
}
budget, ok := numericAny(thinkingConfig["thinkingBudget"])
if !ok {
budget, ok = numericAny(thinkingConfig["thinking_budget"])
}
if !ok {
return false, false
}
return budget > 0, true
}
func numericAny(raw any) (float64, bool) {
switch v := raw.(type) {
case float64:
return v, true
case float32:
return float64(v), true
case int:
return float64(v), true
case int64:
return float64(v), true
case int32:
return float64(v), true
case json.Number:
f, err := v.Float64()
return f, err == nil
default:
return 0, false
}
}
func writeGeminiErrorFromOpenAI(w http.ResponseWriter, status int, raw []byte) {
message := strings.TrimSpace(string(raw))
var parsed map[string]any

View File

@@ -290,6 +290,46 @@ func TestGeminiProxyTranslatesInlineImageToOpenAIDataURL(t *testing.T) {
}
}
func TestGeminiProxyViaOpenAIDisablesThinkingBudgetZero(t *testing.T) {
openAI := &geminiOpenAISuccessStub{}
h := &Handler{Store: testGeminiConfig{}, OpenAI: openAI}
r := chi.NewRouter()
RegisterRoutes(r, h)
body := `{"contents":[{"role":"user","parts":[{"text":"hello"}]}],"generationConfig":{"thinkingConfig":{"thinkingBudget":0}}}`
req := httptest.NewRequest(http.MethodPost, "/v1beta/models/gemini-2.5-flash:generateContent", strings.NewReader(body))
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String())
}
thinking, _ := openAI.seenReq["thinking"].(map[string]any)
if thinking["type"] != "disabled" {
t.Fatalf("expected Gemini thinkingBudget=0 to disable OpenAI thinking, got %#v", openAI.seenReq)
}
}
func TestGeminiProxyViaOpenAIEnablesPositiveThinkingBudget(t *testing.T) {
openAI := &geminiOpenAISuccessStub{}
h := &Handler{Store: testGeminiConfig{}, OpenAI: openAI}
r := chi.NewRouter()
RegisterRoutes(r, h)
body := `{"contents":[{"role":"user","parts":[{"text":"hello"}]}],"generationConfig":{"thinkingConfig":{"thinkingBudget":1024}}}`
req := httptest.NewRequest(http.MethodPost, "/v1beta/models/gemini-2.5-flash:generateContent", strings.NewReader(body))
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String())
}
thinking, _ := openAI.seenReq["thinking"].(map[string]any)
if thinking["type"] != "enabled" {
t.Fatalf("expected Gemini positive thinkingBudget to enable OpenAI thinking, got %#v", openAI.seenReq)
}
}
func TestGenerateContentOpenAIProxyErrorUsesGeminiEnvelope(t *testing.T) {
h := &Handler{
Store: testGeminiConfig{},