package claude import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" ) type claudeProxyStoreStub struct { aliases map[string]string } func (s claudeProxyStoreStub) ModelAliases() map[string]string { return s.aliases } func (claudeProxyStoreStub) CurrentInputFileEnabled() bool { return true } func (claudeProxyStoreStub) CurrentInputFileMinChars() int { return 0 } type openAIProxyStub struct { status int body string } func TestClaudeProxyViaOpenAIPrefersGlobalAliasMapping(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()) } if got := strings.TrimSpace(openAI.seenModel); got != "deepseek-v4-flash" { t.Fatalf("expected global alias mapped proxy model deepseek-v4-flash, got %q", got) } } func (s openAIProxyStub) ChatCompletions(w http.ResponseWriter, _ *http.Request) { if s.status == 0 { s.status = http.StatusOK } w.Header().Set("Content-Type", "application/json") w.WriteHeader(s.status) _, _ = w.Write([]byte(s.body)) } type openAIProxyCaptureStub struct { seenModel string seenReq map[string]any } func (s *openAIProxyCaptureStub) ChatCompletions(w http.ResponseWriter, r *http.Request) { var req map[string]any _ = json.NewDecoder(r.Body).Decode(&req) s.seenReq = req if m, ok := req["model"].(string); ok { s.seenModel = m } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"id":"ok","choices":[{"message":{"role":"assistant","content":"ok"}}]}`)) } func TestClaudeProxyViaOpenAIVercelPreparePassthrough(t *testing.T) { h := &Handler{OpenAI: openAIProxyStub{status: 200, body: `{"lease_id":"lease_123","payload":{"a":1}}`}} req := httptest.NewRequest(http.MethodPost, "/anthropic/v1/messages?__stream_prepare=1", strings.NewReader(`{"model":"claude-sonnet-4-5","messages":[{"role":"user","content":"hi"}],"stream":true}`)) rec := httptest.NewRecorder() h.Messages(rec, req) if rec.Code != http.StatusOK { t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String()) } var out map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil { t.Fatalf("expected json response, got err=%v body=%s", err, rec.Body.String()) } if _, ok := out["lease_id"]; !ok { t.Fatalf("expected lease_id in prepare passthrough, got=%v", out) } } func TestClaudeProxyViaOpenAIUsesGlobalAliasMapping(t *testing.T) { openAI := &openAIProxyCaptureStub{} h := &Handler{ Store: claudeProxyStoreStub{aliases: map[string]string{"claude-3-opus": "deepseek-v4-pro"}}, OpenAI: openAI, } req := httptest.NewRequest(http.MethodPost, "/anthropic/v1/messages", strings.NewReader(`{"model":"claude-3-opus","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()) } if got := strings.TrimSpace(openAI.seenModel); got != "deepseek-v4-pro" { t.Fatalf("expected mapped proxy model deepseek-v4-pro, got %q", got) } } func TestClaudeProxyViaOpenAIPreservesThinkingOverride(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":"disabled"},"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 translated OpenAI request to preserve disabled thinking, got %#v", openAI.seenReq) } } func TestClaudeProxyViaOpenAIEnablesThinkingInternallyByDefaultForNonStream(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"] != "enabled" { t.Fatalf("expected Claude non-stream default to enable downstream thinking internally, 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 TestClaudeProxyViaOpenAIEnablesStreamThinkingByDefault(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":true}`)) rec := httptest.NewRecorder() h.Messages(rec, req) thinking, _ := openAI.seenReq["thinking"].(map[string]any) if thinking["type"] != "enabled" { t.Fatalf("expected Claude stream default to enable downstream thinking, got %#v", openAI.seenReq) } } func TestClaudeProxyViaOpenAIExposesThinkingBlocksByDefault(t *testing.T) { body := `{"id":"chatcmpl_1","object":"chat.completion","created":1,"model":"claude-sonnet-4-5","choices":[{"index":0,"message":{"role":"assistant","content":null,"reasoning_content":"internal reasoning","tool_calls":[{"id":"call_1","type":"function","function":{"name":"search","arguments":"{\"q\":\"x\"}"}}]},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}` h := &Handler{OpenAI: openAIProxyStub{status: 200, body: body}} req := httptest.NewRequest(http.MethodPost, "/anthropic/v1/messages", strings.NewReader(`{"model":"claude-sonnet-4-5","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()) } got := rec.Body.String() if !strings.Contains(got, `"type":"thinking"`) { t.Fatalf("expected converted Claude response to expose thinking block, got %s", got) } if !strings.Contains(got, `"tool_use"`) { t.Fatalf("expected converted Claude response to preserve tool_use, got %s", got) } } func TestClaudeProxyViaOpenAIStripsThinkingBlocksWhenDisabled(t *testing.T) { body := `{"id":"chatcmpl_1","object":"chat.completion","created":1,"model":"claude-sonnet-4-5","choices":[{"index":0,"message":{"role":"assistant","content":"ok","reasoning_content":"internal reasoning"},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}` h := &Handler{OpenAI: openAIProxyStub{status: 200, body: body}} req := httptest.NewRequest(http.MethodPost, "/anthropic/v1/messages", strings.NewReader(`{"model":"claude-sonnet-4-5","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"disabled"},"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()) } got := rec.Body.String() if strings.Contains(got, `"type":"thinking"`) { t.Fatalf("expected disabled thinking to strip thinking block, got %s", got) } } func TestClaudeProxyTranslatesInlineImageToOpenAIDataURL(t *testing.T) { openAI := &openAIProxyCaptureStub{} h := &Handler{OpenAI: openAI} req := httptest.NewRequest(http.MethodPost, "/anthropic/v1/messages", strings.NewReader(`{"model":"claude-sonnet-4-5","messages":[{"role":"user","content":[{"type":"text","text":"hello"},{"type":"image","source":{"type":"base64","media_type":"image/png","data":"QUJDRA=="}}]}],"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()) } messages, _ := openAI.seenReq["messages"].([]any) if len(messages) != 1 { t.Fatalf("expected one translated message, got %#v", openAI.seenReq) } msg, _ := messages[0].(map[string]any) content, _ := msg["content"].([]any) if len(content) != 2 { t.Fatalf("expected translated content blocks, got %#v", msg) } imageBlock, _ := content[1].(map[string]any) if strings.TrimSpace(asString(imageBlock["type"])) != "image_url" { t.Fatalf("expected image_url block, got %#v", imageBlock) } imageURL, _ := imageBlock["image_url"].(map[string]any) if !strings.HasPrefix(strings.TrimSpace(asString(imageURL["url"])), "data:image/png;base64,") { t.Fatalf("expected translated data url, got %#v", imageBlock) } }