651 lines
23 KiB
Go
651 lines
23 KiB
Go
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))
|
|
}
|
|
}
|