implemented stateless ant/oai messages/responses apis
This commit is contained in:
+279
@@ -988,6 +988,285 @@ func TestFibonacciBackoff(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnthropicMessagesEndpoint(t *testing.T) {
|
||||
var receivedOpenAIBody map[string]any
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/chat/completions" {
|
||||
t.Errorf("Expected request to be routed to /v1/chat/completions, got %s", r.URL.Path)
|
||||
}
|
||||
|
||||
var body map[string]any
|
||||
_ = json.NewDecoder(r.Body).Decode(&body)
|
||||
receivedOpenAIBody = body
|
||||
|
||||
isStream, _ := body["stream"].(bool)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if isStream {
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
flusher, _ := w.(http.Flusher)
|
||||
_, _ = w.Write([]byte("data: {\"choices\":[{\"delta\":{\"content\":\"Hello \"}}]}\n\n"))
|
||||
flusher.Flush()
|
||||
_, _ = w.Write([]byte("data: {\"choices\":[{\"delta\":{\"content\":\"Anthropic!\"},\"finish_reason\":\"stop\"}]}\n\n"))
|
||||
flusher.Flush()
|
||||
_, _ = w.Write([]byte("data: [DONE]\n\n"))
|
||||
flusher.Flush()
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
resp := map[string]any{
|
||||
"id": "chatcmpl-test",
|
||||
"model": "claude-model",
|
||||
"choices": []any{
|
||||
map[string]any{
|
||||
"index": 0,
|
||||
"message": map[string]any{
|
||||
"role": "assistant",
|
||||
"content": "Hello from Anthropic proxy",
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
},
|
||||
},
|
||||
"usage": map[string]any{
|
||||
"prompt_tokens": 10,
|
||||
"completion_tokens": 15,
|
||||
"total_tokens": 25,
|
||||
},
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
configs := []ModelConfig{
|
||||
{Model: "claude-model", Key: "test-key", Endpoint: server.URL},
|
||||
}
|
||||
cm := &ConfigManager{
|
||||
configs: configs,
|
||||
uniqueModels: []string{"claude-model"},
|
||||
}
|
||||
handler := handleMessages(cm, nil, 0)
|
||||
|
||||
// 1. Non-streaming test
|
||||
t.Run("Non-streaming Anthropic message", func(t *testing.T) {
|
||||
reqObj := map[string]any{
|
||||
"model": "claude-model",
|
||||
"system": "You are a helpful bot",
|
||||
"messages": []any{
|
||||
map[string]any{
|
||||
"role": "user",
|
||||
"content": "Hello",
|
||||
},
|
||||
},
|
||||
"max_tokens": 100,
|
||||
}
|
||||
reqBytes, _ := json.Marshal(reqObj)
|
||||
req := httptest.NewRequest("POST", "/v1/messages", bytes.NewReader(reqBytes))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Result().StatusCode != http.StatusOK {
|
||||
t.Fatalf("Expected 200 OK, got %d", w.Result().StatusCode)
|
||||
}
|
||||
|
||||
var anthropicResp map[string]any
|
||||
_ = json.NewDecoder(w.Body).Decode(&anthropicResp)
|
||||
|
||||
if anthropicResp["type"] != "message" {
|
||||
t.Errorf("Expected response type 'message', got %v", anthropicResp["type"])
|
||||
}
|
||||
if anthropicResp["role"] != "assistant" {
|
||||
t.Errorf("Expected role 'assistant', got %v", anthropicResp["role"])
|
||||
}
|
||||
content, ok := anthropicResp["content"].([]any)
|
||||
if !ok || len(content) == 0 {
|
||||
t.Fatalf("Expected non-empty content array in Anthropic response, got %+v", anthropicResp)
|
||||
}
|
||||
textVal := content[0].(map[string]any)["text"].(string)
|
||||
if textVal != "Hello from Anthropic proxy" {
|
||||
t.Errorf("Unexpected text in response: %s", textVal)
|
||||
}
|
||||
|
||||
// Verify system message was prepended into OpenAI messages format
|
||||
openAIMsgs, ok := receivedOpenAIBody["messages"].([]any)
|
||||
if !ok || len(openAIMsgs) != 2 {
|
||||
t.Fatalf("Expected 2 messages (system + user) in OpenAI body, got %+v", receivedOpenAIBody)
|
||||
}
|
||||
if openAIMsgs[0].(map[string]any)["role"] != "system" {
|
||||
t.Errorf("Expected first message to be system prompt")
|
||||
}
|
||||
})
|
||||
|
||||
// 2. Streaming test
|
||||
t.Run("Streaming Anthropic message", func(t *testing.T) {
|
||||
reqObj := map[string]any{
|
||||
"model": "claude-model",
|
||||
"stream": true,
|
||||
"messages": []any{
|
||||
map[string]any{
|
||||
"role": "user",
|
||||
"content": "Stream me",
|
||||
},
|
||||
},
|
||||
}
|
||||
reqBytes, _ := json.Marshal(reqObj)
|
||||
req := httptest.NewRequest("POST", "/v1/messages", bytes.NewReader(reqBytes))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Result().StatusCode != http.StatusOK {
|
||||
t.Fatalf("Expected 200 OK, got %d", w.Result().StatusCode)
|
||||
}
|
||||
|
||||
bodyStr := w.Body.String()
|
||||
if !strings.Contains(bodyStr, "event: message_start") || !strings.Contains(bodyStr, "event: content_block_delta") || !strings.Contains(bodyStr, "event: message_stop") {
|
||||
t.Errorf("Expected Anthropic SSE stream events in response, got: %s", bodyStr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestOpenAIResponsesEndpoint(t *testing.T) {
|
||||
var receivedOpenAIBody map[string]any
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/chat/completions" {
|
||||
t.Errorf("Expected request to be routed to /v1/chat/completions, got %s", r.URL.Path)
|
||||
}
|
||||
|
||||
var body map[string]any
|
||||
_ = json.NewDecoder(r.Body).Decode(&body)
|
||||
receivedOpenAIBody = body
|
||||
|
||||
isStream, _ := body["stream"].(bool)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if isStream {
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
flusher, _ := w.(http.Flusher)
|
||||
_, _ = w.Write([]byte("data: {\"choices\":[{\"delta\":{\"content\":\"Response \"}}]}\n\n"))
|
||||
flusher.Flush()
|
||||
_, _ = w.Write([]byte("data: {\"choices\":[{\"delta\":{\"content\":\"Chunk\"},\"finish_reason\":\"stop\"}]}\n\n"))
|
||||
flusher.Flush()
|
||||
_, _ = w.Write([]byte("data: [DONE]\n\n"))
|
||||
flusher.Flush()
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
resp := map[string]any{
|
||||
"id": "chatcmpl-resp-test",
|
||||
"model": "gpt-5-model",
|
||||
"choices": []any{
|
||||
map[string]any{
|
||||
"index": 0,
|
||||
"message": map[string]any{
|
||||
"role": "assistant",
|
||||
"content": "Hello from Responses proxy",
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
},
|
||||
},
|
||||
"usage": map[string]any{
|
||||
"prompt_tokens": 12,
|
||||
"completion_tokens": 18,
|
||||
"total_tokens": 30,
|
||||
},
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
configs := []ModelConfig{
|
||||
{Model: "gpt-5-model", Key: "test-key", Endpoint: server.URL},
|
||||
}
|
||||
cm := &ConfigManager{
|
||||
configs: configs,
|
||||
uniqueModels: []string{"gpt-5-model"},
|
||||
}
|
||||
handler := handleResponses(cm, nil, 0)
|
||||
|
||||
// 1. Non-streaming test
|
||||
t.Run("Non-streaming OpenAI response", func(t *testing.T) {
|
||||
reqObj := map[string]any{
|
||||
"model": "gpt-5-model",
|
||||
"instructions": "Be accurate",
|
||||
"input": "What is 2+2?",
|
||||
}
|
||||
reqBytes, _ := json.Marshal(reqObj)
|
||||
req := httptest.NewRequest("POST", "/v1/responses", bytes.NewReader(reqBytes))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Result().StatusCode != http.StatusOK {
|
||||
t.Fatalf("Expected 200 OK, got %d", w.Result().StatusCode)
|
||||
}
|
||||
|
||||
var respObj map[string]any
|
||||
_ = json.NewDecoder(w.Body).Decode(&respObj)
|
||||
|
||||
if respObj["object"] != "response" {
|
||||
t.Errorf("Expected object 'response', got %v", respObj["object"])
|
||||
}
|
||||
if respObj["status"] != "completed" {
|
||||
t.Errorf("Expected status 'completed', got %v", respObj["status"])
|
||||
}
|
||||
output, ok := respObj["output"].([]any)
|
||||
if !ok || len(output) == 0 {
|
||||
t.Fatalf("Expected output array in Responses API response, got %+v", respObj)
|
||||
}
|
||||
firstOutput := output[0].(map[string]any)
|
||||
contentSlice, ok := firstOutput["content"].([]any)
|
||||
if !ok || len(contentSlice) == 0 {
|
||||
t.Fatalf("Expected output message content slice, got %+v", firstOutput)
|
||||
}
|
||||
textVal := contentSlice[0].(map[string]any)["text"].(string)
|
||||
if textVal != "Hello from Responses proxy" {
|
||||
t.Errorf("Unexpected text in response output: %s", textVal)
|
||||
}
|
||||
|
||||
// Verify instructions and input were parsed into OpenAI messages format
|
||||
openAIMsgs, ok := receivedOpenAIBody["messages"].([]any)
|
||||
if !ok || len(openAIMsgs) != 2 {
|
||||
t.Fatalf("Expected 2 messages in OpenAI body, got %+v", receivedOpenAIBody)
|
||||
}
|
||||
if openAIMsgs[0].(map[string]any)["role"] != "system" {
|
||||
t.Errorf("Expected system role for instructions")
|
||||
}
|
||||
})
|
||||
|
||||
// 2. Streaming test
|
||||
t.Run("Streaming OpenAI response", func(t *testing.T) {
|
||||
reqObj := map[string]any{
|
||||
"model": "gpt-5-model",
|
||||
"stream": true,
|
||||
"input": "Stream test",
|
||||
}
|
||||
reqBytes, _ := json.Marshal(reqObj)
|
||||
req := httptest.NewRequest("POST", "/v1/responses", bytes.NewReader(reqBytes))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Result().StatusCode != http.StatusOK {
|
||||
t.Fatalf("Expected 200 OK, got %d", w.Result().StatusCode)
|
||||
}
|
||||
|
||||
bodyStr := w.Body.String()
|
||||
if !strings.Contains(bodyStr, "event: response.created") || !strings.Contains(bodyStr, "event: response.output_text.delta") || !strings.Contains(bodyStr, "event: response.completed") {
|
||||
t.Errorf("Expected OpenAI Responses SSE events in response stream, got: %s", bodyStr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user