package tests import ( "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" "github.com/gofiber/fiber/v3" "optoant/config" "optoant/handlers" "optoant/internal/transform" ) // ────────────────────────────────────────────── // Transform Unit Tests — AnthropicToBifrost // ────────────────────────────────────────────── func TestAnthropicToBifrost_Basic(t *testing.T) { antBody := `{"model":"claude-3-5-sonnet","max_tokens":1024,"messages":[{"role":"user","content":"Hello!"}]}` out, err := transform.AnthropicToBifrost([]byte(antBody)) if err != nil { t.Fatalf("unexpected error: %v", err) } var bfReq struct { Model string `json:"model"` MaxTokens int `json:"max_tokens"` Messages []struct { Role string `json:"role"` Content string `json:"content"` } `json:"messages"` } if err := json.Unmarshal(out, &bfReq); err != nil { t.Fatalf("invalid bifrost json: %v", err) } if bfReq.Model != "Anthropic/claude-3-5-sonnet" { t.Errorf("expected Anthropic/claude-3-5-sonnet, got %s", bfReq.Model) } if bfReq.MaxTokens != 1024 { t.Errorf("expected 1024, got %d", bfReq.MaxTokens) } if len(bfReq.Messages) != 1 || bfReq.Messages[0].Role != "user" { t.Errorf("unexpected messages: %+v", bfReq.Messages) } } func TestAnthropicToBifrost_WithSystem(t *testing.T) { antBody := `{"model":"claude-3","system":"You are helpful.","messages":[{"role":"user","content":"Hi"}]}` out, err := transform.AnthropicToBifrost([]byte(antBody)) if err != nil { t.Fatalf("unexpected error: %v", err) } var bfReq struct { Messages []struct { Role string `json:"role"` Content string `json:"content"` } `json:"messages"` } json.Unmarshal(out, &bfReq) if len(bfReq.Messages) != 2 { t.Fatalf("expected 2 messages (system+user), got %d", len(bfReq.Messages)) } if bfReq.Messages[0].Role != "system" || bfReq.Messages[0].Content != "You are helpful." { t.Errorf("system message missing or wrong: %+v", bfReq.Messages[0]) } if bfReq.Messages[1].Role != "user" || bfReq.Messages[1].Content != "Hi" { t.Errorf("user message wrong: %+v", bfReq.Messages[1]) } } func TestAnthropicToBifrost_ModelPrefix(t *testing.T) { tests := []struct { input string expected string }{ {"claude-3-5-sonnet", "Anthropic/claude-3-5-sonnet"}, {"deepseek-v4-flash", "DeepSeek/deepseek-v4-flash"}, {"gpt-4", "OpenAI/gpt-4"}, {"gemini-pro", "Google/gemini-pro"}, {"unknown-model", "DeepSeek/unknown-model"}, {"Anthropic/claude-3", "Anthropic/claude-3"}, {"DeepSeek/deepseek-v4", "DeepSeek/deepseek-v4"}, } for _, tc := range tests { body := `{"model":"` + tc.input + `","messages":[{"role":"user","content":"Hi"}]}` out, err := transform.AnthropicToBifrost([]byte(body)) if err != nil { t.Fatalf("error for model %s: %v", tc.input, err) } var bfReq struct { Model string `json:"model"` } json.Unmarshal(out, &bfReq) if bfReq.Model != tc.expected { t.Errorf("model %s: expected %s, got %s", tc.input, tc.expected, bfReq.Model) } } } func TestAnthropicToBifrost_StreamPassthrough(t *testing.T) { body := `{"model":"claude-3","stream":true,"messages":[{"role":"user","content":"Hi"}]}` out, err := transform.AnthropicToBifrost([]byte(body)) if err != nil { t.Fatalf("unexpected error: %v", err) } var bfReq struct { Stream bool `json:"stream"` } json.Unmarshal(out, &bfReq) if !bfReq.Stream { t.Error("expected stream=true to be preserved") } } func TestAnthropicToBifrost_InvalidJSON(t *testing.T) { _, err := transform.AnthropicToBifrost([]byte(`not json`)) if err == nil { t.Fatal("expected error for invalid json") } } // ────────────────────────────────────────────── // Transform Unit Tests — BifrostToAnthropic // ────────────────────────────────────────────── func TestBifrostToAnthropic_Basic(t *testing.T) { bfBody := `{"id":"chatcmpl-abc","object":"chat.completion","created":1700000000,"model":"Anthropic/claude-3","choices":[{"message":{"role":"assistant","content":"Hello!"},"finish_reason":"stop","index":0}]}` out, err := transform.BifrostToAnthropic([]byte(bfBody)) if err != nil { t.Fatalf("unexpected error: %v", err) } var antResp struct { ID string `json:"id"` Type string `json:"type"` Role string `json:"role"` Content []struct { Type string `json:"type"` Text string `json:"text"` } `json:"content"` Model string `json:"model"` StopReason string `json:"stop_reason"` } json.Unmarshal(out, &antResp) if antResp.Type != "message" { t.Errorf("expected type=message, got %s", antResp.Type) } if antResp.Role != "assistant" { t.Errorf("expected role=assistant, got %s", antResp.Role) } if len(antResp.Content) != 1 || antResp.Content[0].Text != "Hello!" { t.Errorf("unexpected content: %+v", antResp.Content) } if antResp.StopReason != "stop" { t.Errorf("expected stop_reason=stop, got %s", antResp.StopReason) } if antResp.ID != "chatcmpl-abc" { t.Errorf("expected id to pass through, got %s", antResp.ID) } } func TestBifrostToAnthropic_EmptyFinishReason(t *testing.T) { bfBody := `{"id":"x","object":"chat.completion","created":1,"model":"DeepSeek/deepseek-v4","choices":[{"message":{"role":"assistant","content":"Done"},"finish_reason":"","index":0}]}` out, err := transform.BifrostToAnthropic([]byte(bfBody)) if err != nil { t.Fatalf("unexpected error: %v", err) } var antResp struct { StopReason string `json:"stop_reason"` } json.Unmarshal(out, &antResp) if antResp.StopReason != "end_turn" { t.Errorf("expected end_turn for empty finish_reason, got %s", antResp.StopReason) } } func TestBifrostToAnthropic_MultipleChoices(t *testing.T) { bfBody := `{"id":"x","object":"chat.completion","created":1,"model":"DeepSeek/deepseek-v4","choices":[ {"message":{"role":"assistant","content":"First"},"finish_reason":"stop","index":0}, {"message":{"role":"assistant","content":"Second"},"finish_reason":"stop","index":1} ]}` out, err := transform.BifrostToAnthropic([]byte(bfBody)) if err != nil { t.Fatalf("unexpected error: %v", err) } var antResp struct { Content []struct { Text string `json:"text"` } `json:"content"` } json.Unmarshal(out, &antResp) if len(antResp.Content) != 2 { t.Fatalf("expected 2 content blocks, got %d", len(antResp.Content)) } if antResp.Content[0].Text != "First" || antResp.Content[1].Text != "Second" { t.Errorf("unexpected content order: %+v", antResp.Content) } } func TestBifrostToAnthropic_InvalidJSON(t *testing.T) { _, err := transform.BifrostToAnthropic([]byte(`not json`)) if err == nil { t.Fatal("expected error for invalid json") } } // ────────────────────────────────────────────── // AnthropicHandler Integration Tests // ────────────────────────────────────────────── func newAnthropicTestApp(cfg *config.Config) *fiber.App { app := fiber.New() app.All("/anthropic", handlers.AnthropicHandler(cfg, nil)) app.All("/anthropic/*", handlers.AnthropicHandler(cfg, nil)) return app } func TestAnthropicProxy_Success(t *testing.T) { upstream := mockUpstream(t, http.StatusOK, `{"id":"chatcmpl-abc","object":"chat.completion","created":1700000000,"model":"DeepSeek/deepseek-v4","choices":[{"message":{"role":"assistant","content":"Hello from Bifrost!"},"finish_reason":"stop","index":0}]}`) cfg := &config.Config{ OpenAIBackend: upstream.URL, RequestTimeoutSeconds: 5, } app := newAnthropicTestApp(cfg) payload := `{"model":"claude-3-5-sonnet","max_tokens":1024,"messages":[{"role":"user","content":"Hi"}]}` req := httptest.NewRequest(http.MethodPost, "/anthropic/v1/messages", strings.NewReader(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer test-key") resp, err := app.Test(req, fiber.TestConfig{Timeout: -1}) if err != nil { t.Fatalf("app.Test error: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } var result map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { t.Fatalf("decode response: %v", err) } if result["type"] != "message" { t.Errorf("expected type=message, got %v", result["type"]) } if result["role"] != "assistant" { t.Errorf("expected role=assistant, got %v", result["role"]) } content, ok := result["content"].([]interface{}) if !ok || len(content) == 0 { t.Fatalf("expected content array, got: %v", result["content"]) } firstBlock := content[0].(map[string]interface{}) if firstBlock["type"] != "text" { t.Errorf("expected text content type, got %v", firstBlock["type"]) } if firstBlock["text"] != "Hello from Bifrost!" { t.Errorf("expected Hello from Bifrost!, got %v", firstBlock["text"]) } } func TestAnthropicProxy_InvalidFormatPassthrough(t *testing.T) { upstream := mockUpstream(t, http.StatusOK, `{"raw":"response"}`) cfg := &config.Config{ OpenAIBackend: upstream.URL, RequestTimeoutSeconds: 5, } app := newAnthropicTestApp(cfg) payload := `this is not valid json` req := httptest.NewRequest(http.MethodPost, "/anthropic/v1/messages", strings.NewReader(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer test-key") resp, err := app.Test(req, fiber.TestConfig{Timeout: -1}) if err != nil { t.Fatalf("app.Test error: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200 (passthrough), got %d", resp.StatusCode) } body, _ := io.ReadAll(resp.Body) if !strings.Contains(string(body), "raw") { t.Errorf("expected raw response body, got: %s", string(body)) } } func TestAnthropicProxy_UpstreamError(t *testing.T) { cfg := &config.Config{ OpenAIBackend: "http://127.0.0.1:19999", RequestTimeoutSeconds: 2, } app := newAnthropicTestApp(cfg) payload := `{"model":"claude-3","messages":[{"role":"user","content":"Hi"}]}` req := httptest.NewRequest(http.MethodPost, "/anthropic/v1/messages", strings.NewReader(payload)) req.Header.Set("Content-Type", "application/json") resp, err := app.Test(req, fiber.TestConfig{Timeout: -1}) if err != nil { t.Fatalf("app.Test error: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusBadGateway { t.Errorf("expected 502, got %d", resp.StatusCode) } } func TestAnthropicProxy_ModelsList(t *testing.T) { cfg := &config.Config{OpenAIBackend: "http://127.0.0.1:19999", RequestTimeoutSeconds: 1} app := newAnthropicTestApp(cfg) req := httptest.NewRequest(http.MethodGet, "/anthropic/v1/models", nil) resp, err := app.Test(req, fiber.TestConfig{Timeout: -1}) if err != nil { t.Fatalf("app.Test error: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) data, ok := result["data"].([]interface{}) if !ok || len(data) == 0 { t.Fatalf("expected data array, got: %v", result["data"]) } first := data[0].(map[string]interface{}) if first["id"] != "deepseek-v4-flash" { t.Errorf("expected deepseek-v4-flash as first model, got %v", first["id"]) } } func TestAnthropicProxy_EmptyBody(t *testing.T) { cfg := &config.Config{OpenAIBackend: "http://127.0.0.1:19999", RequestTimeoutSeconds: 1} app := newAnthropicTestApp(cfg) req := httptest.NewRequest(http.MethodPost, "/anthropic/v1/messages", nil) req.Header.Set("Authorization", "Bearer test-key") resp, err := app.Test(req, fiber.TestConfig{Timeout: -1}) if err != nil { t.Fatalf("app.Test error: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) if result["type"] != "message" { t.Errorf("expected type=message, got %v", result["type"]) } } func TestAnthropicProxy_HEAD(t *testing.T) { cfg := &config.Config{OpenAIBackend: "http://127.0.0.1:19999", RequestTimeoutSeconds: 1} app := newAnthropicTestApp(cfg) req := httptest.NewRequest(http.MethodHead, "/anthropic/v1/messages", nil) resp, err := app.Test(req, fiber.TestConfig{Timeout: -1}) if err != nil { t.Fatalf("app.Test error: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusNotFound { t.Errorf("expected 404 for HEAD, got %d", resp.StatusCode) } } func TestAnthropicProxy_xApiKeyConversion(t *testing.T) { upstream := mockUpstream(t, http.StatusOK, `{"id":"chatcmpl-test","object":"chat.completion","created":1,"model":"DeepSeek/deepseek-v4","choices":[{"message":{"role":"assistant","content":"Auth ok"},"finish_reason":"stop","index":0}]}`) cfg := &config.Config{ OpenAIBackend: upstream.URL, RequestTimeoutSeconds: 5, } app := newAnthropicTestApp(cfg) payload := `{"model":"claude-3","messages":[{"role":"user","content":"Hi"}]}` req := httptest.NewRequest(http.MethodPost, "/anthropic/v1/messages", strings.NewReader(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Api-Key", "my-secret-key") resp, err := app.Test(req, fiber.TestConfig{Timeout: -1}) if err != nil { t.Fatalf("app.Test error: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) content := result["content"].([]interface{}) first := content[0].(map[string]interface{}) if first["text"] != "Auth ok" { t.Errorf("expected response with text, got %v", first["text"]) } } // ────────────────────────────────────────────── // Claude Code Simulation Tests // ────────────────────────────────────────────── // TestClaudeCode_ExactRequestFormat simulates what Claude Code CLI sends: // - POST /v1/messages (via /anthropic/v1/messages) // - x-api-key header (NOT Authorization) // - anthropic-version header // - Exact Anthropic Messages API format with model: "claude-sonnet-4-20250514" func TestClaudeCode_ExactRequestFormat(t *testing.T) { upstream := mockUpstream(t, http.StatusOK, `{"id":"chatcmpl-cc","object":"chat.completion","created":1700000001,"model":"DeepSeek/deepseek-v4","choices":[{"message":{"role":"assistant","content":"Hello from Bifrost! I am connected through the LLM Gateway."},"finish_reason":"stop","index":0}]}`) cfg := &config.Config{ OpenAIBackend: upstream.URL, RequestTimeoutSeconds: 5, } app := newAnthropicTestApp(cfg) // Exact format Claude Code sends: // https://docs.anthropic.com/en/api/messages payload := `{"model":"claude-sonnet-4-20250514","max_tokens":8192,"stream":false,"messages":[{"role":"user","content":"Hello, can you help me with a coding task?"}]}` req := httptest.NewRequest(http.MethodPost, "/anthropic/v1/messages", strings.NewReader(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Api-Key", "sk-ant-my-claude-code-key") req.Header.Set("Anthropic-Version", "2023-06-01") resp, err := app.Test(req, fiber.TestConfig{Timeout: -1}) if err != nil { t.Fatalf("app.Test error: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } var result map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { t.Fatalf("decode response: %v", err) } // Verify Anthropic Messages API response shape if result["type"] != "message" { t.Errorf("Claude Code expects type=message, got %v", result["type"]) } if result["role"] != "assistant" { t.Errorf("Claude Code expects role=assistant, got %v", result["role"]) } content, ok := result["content"].([]interface{}) if !ok || len(content) == 0 { t.Fatalf("Claude Code expects content array, got: %v", result["content"]) } firstBlock := content[0].(map[string]interface{}) if firstBlock["type"] != "text" { t.Errorf("expected text block, got %v", firstBlock["type"]) } if !strings.Contains(firstBlock["text"].(string), "Hello from Bifrost!") { t.Errorf("expected Bifrost greeting, got: %v", firstBlock["text"]) } // Verify model was auto-prefixed correctly upstreamReqBody := upstreamResponseBody(t, req, app) if strings.Contains(upstreamReqBody, `"model":"Anthropic/claude-sonnet-4-20250514"`) { t.Logf("✓ Model auto-prefixed to Anthropic/claude-sonnet-4-20250514") } } // upstreamResponseBody captures what the mock upstream received. // Only used in TestClaudeCode_ExactRequestFormat for verifying the transformed request. func upstreamResponseBody(t *testing.T, originalReq *http.Request, app *fiber.App) string { t.Helper() resp, err := app.Test(originalReq, fiber.TestConfig{Timeout: -1}) if err != nil { t.Fatalf("app.Test error: %v", err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) return string(body) } func TestClaudeCode_MaxTokensDefault(t *testing.T) { // Claude Code doesn't always send max_tokens (SDK has defaults) upstream := mockUpstream(t, http.StatusOK, `{"id":"chatcmpl-cc2","object":"chat.completion","created":1700000002,"model":"DeepSeek/deepseek-v4","choices":[{"message":{"role":"assistant","content":"Response without max_tokens"},"finish_reason":"end_turn","index":0}]}`) cfg := &config.Config{ OpenAIBackend: upstream.URL, RequestTimeoutSeconds: 5, } app := newAnthropicTestApp(cfg) // No max_tokens - Claude Code doesn't always send it payload := `{"model":"claude-sonnet-4-20250514","messages":[{"role":"user","content":"Hi"}]}` req := httptest.NewRequest(http.MethodPost, "/anthropic/v1/messages", strings.NewReader(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Api-Key", "sk-ant-key") resp, err := app.Test(req, fiber.TestConfig{Timeout: -1}) if err != nil { t.Fatalf("app.Test error: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } } // ────────────────────────────────────────────── // Default Model Injection Tests // ────────────────────────────────────────────── func TestAnthropicProxy_DefaultModelInjection(t *testing.T) { var capturedBody string upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) capturedBody = string(body) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _, _ = io.WriteString(w, `{"id":"x","object":"chat.completion","created":1,"model":"DeepSeek/deepseek-v4","choices":[{"message":{"role":"assistant","content":"ok"},"finish_reason":"stop","index":0}]}`) })) t.Cleanup(upstream.Close) cfg := &config.Config{ OpenAIBackend: upstream.URL, RequestTimeoutSeconds: 5, OpenAIModel: "deepseek/deepseek-v4-pro", } app := newAnthropicTestApp(cfg) // No model in payload — should be injected payload := `{"max_tokens":256,"messages":[{"role":"user","content":"Hi"}]}` req := httptest.NewRequest(http.MethodPost, "/anthropic/v1/messages", strings.NewReader(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer test-key") resp, err := app.Test(req, fiber.TestConfig{Timeout: -1}) if err != nil { t.Fatalf("app.Test error: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } // Verify the injected model was passed through as-is (already has "/") var bfReq struct { Model string `json:"model"` } json.Unmarshal([]byte(capturedBody), &bfReq) expected := "deepseek/deepseek-v4-pro" if bfReq.Model != expected { t.Errorf("expected model=%q (injected as-is, no prefix needed), got %q", expected, bfReq.Model) } } func TestAnthropicProxy_DefaultModelInjection_NopWhenModelExists(t *testing.T) { var capturedBody string upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) capturedBody = string(body) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _, _ = io.WriteString(w, `{"id":"x","object":"chat.completion","created":1,"model":"DeepSeek/deepseek-v4","choices":[{"message":{"role":"assistant","content":"ok"},"finish_reason":"stop","index":0}]}`) })) t.Cleanup(upstream.Close) cfg := &config.Config{ OpenAIBackend: upstream.URL, RequestTimeoutSeconds: 5, OpenAIModel: "deepseek/deepseek-v4-pro", } app := newAnthropicTestApp(cfg) // Model already set — should NOT be overridden payload := `{"model":"claude-3","max_tokens":256,"messages":[{"role":"user","content":"Hi"}]}` req := httptest.NewRequest(http.MethodPost, "/anthropic/v1/messages", strings.NewReader(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer test-key") resp, err := app.Test(req, fiber.TestConfig{Timeout: -1}) if err != nil { t.Fatalf("app.Test error: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } var bfReq struct { Model string `json:"model"` } json.Unmarshal([]byte(capturedBody), &bfReq) expected := "Anthropic/claude-3" if bfReq.Model != expected { t.Errorf("expected model=%q (preserved + prefixed), got %q", expected, bfReq.Model) } } func TestAnthropicProxy_TransformErrorFallsbackToPassthrough(t *testing.T) { bfBody := `this is not a valid JSON response either` upstream := mockUpstream(t, http.StatusOK, bfBody) cfg := &config.Config{ OpenAIBackend: upstream.URL, RequestTimeoutSeconds: 5, } app := newAnthropicTestApp(cfg) payload := `{"model":"claude-3","messages":[{"role":"user","content":"Hi"}]}` req := httptest.NewRequest(http.MethodPost, "/anthropic/v1/messages", strings.NewReader(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer test-key") resp, err := app.Test(req, fiber.TestConfig{Timeout: -1}) if err != nil { t.Fatalf("app.Test error: %v", err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if !strings.Contains(string(body), "not a valid JSON") { t.Errorf("expected raw fallback body, got: %s", string(body)) } }