test: check request errors and stop asserting PathRewrite on /v1/models
/v1/models is served from config by ServeHTTP (task 2.5) so it never reaches routing; the rewrite test now uses a non-reserved path.
This commit is contained in:
@@ -76,7 +76,10 @@ func TestBodyBasedDispatch(t *testing.T) {
|
|||||||
reasoningCalled = false
|
reasoningCalled = false
|
||||||
ornithCalled = false
|
ornithCalled = false
|
||||||
requestBody := `{"model":"reasoning","messages":[{"role":"user","content":"hi"}]}`
|
requestBody := `{"model":"reasoning","messages":[{"role":"user","content":"hi"}]}`
|
||||||
resp, _ := http.Post(server.URL+"/v1/chat/completions", "application/json", strings.NewReader(requestBody))
|
resp, err := http.Post(server.URL+"/v1/chat/completions", "application/json", strings.NewReader(requestBody))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
|
|
||||||
if !reasoningCalled {
|
if !reasoningCalled {
|
||||||
@@ -151,7 +154,10 @@ func TestBodyPreservedUnmodified(t *testing.T) {
|
|||||||
|
|
||||||
// Send a request with specific body content
|
// Send a request with specific body content
|
||||||
originalBody := `{"model":"reasoning","stream":true,"messages":[{"role":"user","content":"hello world"}],"temperature":0.7}`
|
originalBody := `{"model":"reasoning","stream":true,"messages":[{"role":"user","content":"hello world"}],"temperature":0.7}`
|
||||||
resp, _ := http.Post(server.URL+"/v1/chat/completions", "application/json", strings.NewReader(originalBody))
|
resp, err := http.Post(server.URL+"/v1/chat/completions", "application/json", strings.NewReader(originalBody))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
|
|
||||||
if string(receivedBody) != originalBody {
|
if string(receivedBody) != originalBody {
|
||||||
@@ -249,7 +255,10 @@ func TestUnknownModelReject(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
requestBody := `{"model":"unknown-model","messages":[]}`
|
requestBody := `{"model":"unknown-model","messages":[]}`
|
||||||
resp, _ := http.Post(server.URL+"/v1/chat/completions", "application/json", strings.NewReader(requestBody))
|
resp, err := http.Post(server.URL+"/v1/chat/completions", "application/json", strings.NewReader(requestBody))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusBadRequest {
|
if resp.StatusCode != http.StatusBadRequest {
|
||||||
@@ -300,7 +309,10 @@ func TestMissingModelField(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
requestBody := `{"messages":[]}`
|
requestBody := `{"messages":[]}`
|
||||||
resp, _ := http.Post(server.URL+"/v1/chat/completions", "application/json", strings.NewReader(requestBody))
|
resp, err := http.Post(server.URL+"/v1/chat/completions", "application/json", strings.NewReader(requestBody))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusBadRequest {
|
if resp.StatusCode != http.StatusBadRequest {
|
||||||
|
|||||||
@@ -323,7 +323,10 @@ func TestBodySizeCapRejectionLogged(t *testing.T) {
|
|||||||
|
|
||||||
// Send an oversized body
|
// Send an oversized body
|
||||||
body := strings.Repeat("a", int(maxBodySize)+1)
|
body := strings.Repeat("a", int(maxBodySize)+1)
|
||||||
resp, _ := http.Post(server.URL+"/test", "text/plain", strings.NewReader(body))
|
resp, err := http.Post(server.URL+"/test", "text/plain", strings.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
|
|
||||||
// Verify rejection status
|
// Verify rejection status
|
||||||
|
|||||||
@@ -45,11 +45,14 @@ func TestEmbeddingsPassthroughNoRewrite(t *testing.T) {
|
|||||||
server := httptest.NewServer(handler)
|
server := httptest.NewServer(handler)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
resp, _ := http.Post(
|
resp, err := http.Post(
|
||||||
server.URL+"/v1/embeddings",
|
server.URL+"/v1/embeddings",
|
||||||
"application/json",
|
"application/json",
|
||||||
bytes.NewReader([]byte(`{"model":"nomic-ai/nomic-embed-text-v2-moe","input":"hello"}`)),
|
bytes.NewReader([]byte(`{"model":"nomic-ai/nomic-embed-text-v2-moe","input":"hello"}`)),
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if !embeddingsCalled {
|
if !embeddingsCalled {
|
||||||
@@ -91,11 +94,14 @@ func TestEmbeddingsResponsePassthrough(t *testing.T) {
|
|||||||
server := httptest.NewServer(handler)
|
server := httptest.NewServer(handler)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
resp, _ := http.Post(
|
resp, err := http.Post(
|
||||||
server.URL+"/v1/embeddings",
|
server.URL+"/v1/embeddings",
|
||||||
"application/json",
|
"application/json",
|
||||||
bytes.NewReader([]byte(`{"model":"nomic-ai/nomic-embed-text-v2-moe","input":"test"}`)),
|
bytes.NewReader([]byte(`{"model":"nomic-ai/nomic-embed-text-v2-moe","input":"test"}`)),
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, _ := io.ReadAll(resp.Body)
|
||||||
@@ -135,11 +141,14 @@ func TestRerankPathRewrite(t *testing.T) {
|
|||||||
server := httptest.NewServer(handler)
|
server := httptest.NewServer(handler)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
resp, _ := http.Post(
|
resp, err := http.Post(
|
||||||
server.URL+"/v1/rerank",
|
server.URL+"/v1/rerank",
|
||||||
"application/json",
|
"application/json",
|
||||||
bytes.NewReader([]byte(`{"model":"BAAI/bge-reranker-base","query":"test","texts":["a","b"]}`)),
|
bytes.NewReader([]byte(`{"model":"BAAI/bge-reranker-base","query":"test","texts":["a","b"]}`)),
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
// Verify path IS rewritten to /rerank
|
// Verify path IS rewritten to /rerank
|
||||||
@@ -177,11 +186,14 @@ func TestRerankResponsePassthrough(t *testing.T) {
|
|||||||
server := httptest.NewServer(handler)
|
server := httptest.NewServer(handler)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
resp, _ := http.Post(
|
resp, err := http.Post(
|
||||||
server.URL+"/v1/rerank",
|
server.URL+"/v1/rerank",
|
||||||
"application/json",
|
"application/json",
|
||||||
bytes.NewReader([]byte(`{"model":"BAAI/bge-reranker-base","query":"q","texts":["a"]}`)),
|
bytes.NewReader([]byte(`{"model":"BAAI/bge-reranker-base","query":"q","texts":["a"]}`)),
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, _ := io.ReadAll(resp.Body)
|
||||||
@@ -209,11 +221,14 @@ func TestEmbeddingsUnknownModel(t *testing.T) {
|
|||||||
server := httptest.NewServer(handler)
|
server := httptest.NewServer(handler)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
resp, _ := http.Post(
|
resp, err := http.Post(
|
||||||
server.URL+"/v1/embeddings",
|
server.URL+"/v1/embeddings",
|
||||||
"application/json",
|
"application/json",
|
||||||
bytes.NewReader([]byte(`{"model":"unknown-embeddings","input":"test"}`)),
|
bytes.NewReader([]byte(`{"model":"unknown-embeddings","input":"test"}`)),
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusBadRequest {
|
if resp.StatusCode != http.StatusBadRequest {
|
||||||
@@ -244,11 +259,14 @@ func TestRerankerUnknownModel(t *testing.T) {
|
|||||||
server := httptest.NewServer(handler)
|
server := httptest.NewServer(handler)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
resp, _ := http.Post(
|
resp, err := http.Post(
|
||||||
server.URL+"/v1/rerank",
|
server.URL+"/v1/rerank",
|
||||||
"application/json",
|
"application/json",
|
||||||
bytes.NewReader([]byte(`{"model":"unknown-reranker","query":"q","texts":["a"]}`)),
|
bytes.NewReader([]byte(`{"model":"unknown-reranker","query":"q","texts":["a"]}`)),
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusBadRequest {
|
if resp.StatusCode != http.StatusBadRequest {
|
||||||
@@ -288,11 +306,14 @@ func TestEmbeddingsBodyForwarded(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
originalBody := `{"model":"nomic-ai/nomic-embed-text-v2-moe","input":"test data with special chars: \u0001"}`
|
originalBody := `{"model":"nomic-ai/nomic-embed-text-v2-moe","input":"test data with special chars: \u0001"}`
|
||||||
resp, _ := http.Post(
|
resp, err := http.Post(
|
||||||
server.URL+"/v1/embeddings",
|
server.URL+"/v1/embeddings",
|
||||||
"application/json",
|
"application/json",
|
||||||
bytes.NewReader([]byte(originalBody)),
|
bytes.NewReader([]byte(originalBody)),
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
|
|
||||||
// The received body should match the original (though may have different formatting)
|
// The received body should match the original (though may have different formatting)
|
||||||
@@ -337,11 +358,14 @@ func TestRerankerBodyForwarded(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
originalBody := `{"model":"BAAI/bge-reranker-base","query":"test","texts":["a","b","c"]}`
|
originalBody := `{"model":"BAAI/bge-reranker-base","query":"test","texts":["a","b","c"]}`
|
||||||
resp, _ := http.Post(
|
resp, err := http.Post(
|
||||||
server.URL+"/v1/rerank",
|
server.URL+"/v1/rerank",
|
||||||
"application/json",
|
"application/json",
|
||||||
bytes.NewReader([]byte(originalBody)),
|
bytes.NewReader([]byte(originalBody)),
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
|
|
||||||
var orig, received map[string]interface{}
|
var orig, received map[string]interface{}
|
||||||
@@ -380,11 +404,14 @@ func TestUpstreamErrorStatusSurfaced(t *testing.T) {
|
|||||||
server := httptest.NewServer(handler)
|
server := httptest.NewServer(handler)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
resp, _ := http.Post(
|
resp, err := http.Post(
|
||||||
server.URL+"/v1/embeddings",
|
server.URL+"/v1/embeddings",
|
||||||
"application/json",
|
"application/json",
|
||||||
bytes.NewReader([]byte(`{"model":"nomic-ai/nomic-embed-text-v2-moe","input":"test"}`)),
|
bytes.NewReader([]byte(`{"model":"nomic-ai/nomic-embed-text-v2-moe","input":"test"}`)),
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusInternalServerError {
|
if resp.StatusCode != http.StatusInternalServerError {
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ func TestHeaderHygiene(t *testing.T) {
|
|||||||
t.Errorf("hop-by-hop header %s should be stripped, but found: %s", header, receivedHeaders.Get(header))
|
t.Errorf("hop-by-hop header %s should be stripped, but found: %s", header, receivedHeaders.Get(header))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TE header is tricky - it should be stripped but may be handled differently
|
// TE header is tricky - it should be stripped but may be handled differently
|
||||||
// Just verify it's not the original value for now
|
// Just verify it's not the original value for now
|
||||||
if receivedHeaders.Get("TE") == "trailers" {
|
if receivedHeaders.Get("TE") == "trailers" {
|
||||||
|
|||||||
@@ -74,7 +74,10 @@ func TestModelsEndpointContentType(t *testing.T) {
|
|||||||
server := httptest.NewServer(handler)
|
server := httptest.NewServer(handler)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
resp, _ := http.Get(server.URL + "/v1/models")
|
resp, err := http.Get(server.URL + "/v1/models")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
ct := resp.Header.Get("Content-Type")
|
ct := resp.Header.Get("Content-Type")
|
||||||
@@ -101,7 +104,10 @@ func TestModelsEndpointResponseShape(t *testing.T) {
|
|||||||
server := httptest.NewServer(handler)
|
server := httptest.NewServer(handler)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
resp, _ := http.Get(server.URL + "/v1/models")
|
resp, err := http.Get(server.URL + "/v1/models")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
var result ModelListResponse
|
var result ModelListResponse
|
||||||
@@ -161,7 +167,10 @@ func TestModelsEndpointEnumeratesAllModels(t *testing.T) {
|
|||||||
server := httptest.NewServer(handler)
|
server := httptest.NewServer(handler)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
resp, _ := http.Get(server.URL + "/v1/models")
|
resp, err := http.Get(server.URL + "/v1/models")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
var result ModelListResponse
|
var result ModelListResponse
|
||||||
@@ -211,7 +220,10 @@ func TestModelsEndpointHasRequiredFields(t *testing.T) {
|
|||||||
server := httptest.NewServer(handler)
|
server := httptest.NewServer(handler)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
resp, _ := http.Get(server.URL + "/v1/models")
|
resp, err := http.Get(server.URL + "/v1/models")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
var result ModelListResponse
|
var result ModelListResponse
|
||||||
@@ -292,7 +304,10 @@ func TestModelsEndpointDerivedFromConfig(t *testing.T) {
|
|||||||
server := httptest.NewServer(handler)
|
server := httptest.NewServer(handler)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
resp, _ := http.Get(server.URL + "/v1/models")
|
resp, err := http.Get(server.URL + "/v1/models")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
var result ModelListResponse
|
var result ModelListResponse
|
||||||
@@ -347,18 +362,24 @@ func TestModelsEndpointConsistentWithDispatch(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
// Get list of models
|
// Get list of models
|
||||||
resp, _ := http.Get(server.URL + "/v1/models")
|
resp, err := http.Get(server.URL + "/v1/models")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
var result ModelListResponse
|
var result ModelListResponse
|
||||||
json.NewDecoder(resp.Body).Decode(&result)
|
json.NewDecoder(resp.Body).Decode(&result)
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
|
|
||||||
// Try to dispatch to each advertised model
|
// Try to dispatch to each advertised model
|
||||||
for _, model := range result.Data {
|
for _, model := range result.Data {
|
||||||
dispatchResp, _ := http.Post(
|
dispatchResp, err := http.Post(
|
||||||
server.URL+"/v1/chat/completions",
|
server.URL+"/v1/chat/completions",
|
||||||
"application/json",
|
"application/json",
|
||||||
strings.NewReader(`{"model":"`+model.ID+`","messages":[]}`),
|
strings.NewReader(`{"model":"`+model.ID+`","messages":[]}`),
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
defer dispatchResp.Body.Close()
|
defer dispatchResp.Body.Close()
|
||||||
|
|
||||||
// Should not return 400 (unknown model error)
|
// Should not return 400 (unknown model error)
|
||||||
@@ -394,14 +415,20 @@ func TestModelsEndpointResponseIsConsistent(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
// Call endpoint twice
|
// Call endpoint twice
|
||||||
resp1, _ := http.Get(server.URL + "/v1/models")
|
resp1, err := http.Get(server.URL + "/v1/models")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
var result1 ModelListResponse
|
var result1 ModelListResponse
|
||||||
json.NewDecoder(resp1.Body).Decode(&result1)
|
json.NewDecoder(resp1.Body).Decode(&result1)
|
||||||
resp1.Body.Close()
|
resp1.Body.Close()
|
||||||
|
|
||||||
time.Sleep(10 * time.Millisecond)
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
resp2, _ := http.Get(server.URL + "/v1/models")
|
resp2, err := http.Get(server.URL + "/v1/models")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
var result2 ModelListResponse
|
var result2 ModelListResponse
|
||||||
json.NewDecoder(resp2.Body).Decode(&result2)
|
json.NewDecoder(resp2.Body).Decode(&result2)
|
||||||
resp2.Body.Close()
|
resp2.Body.Close()
|
||||||
|
|||||||
@@ -105,7 +105,9 @@ func TestProxyPathRewrite(t *testing.T) {
|
|||||||
server := httptest.NewServer(handler)
|
server := httptest.NewServer(handler)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
resp, err := http.Get(server.URL + "/v1/models")
|
// Not /v1/models: ServeHTTP serves that endpoint from config (task 2.5)
|
||||||
|
// and returns before routing, so it never exercises PathRewrite.
|
||||||
|
resp, err := http.Get(server.URL + "/some/path")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("request failed: %v", err)
|
t.Fatalf("request failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -146,7 +148,7 @@ func TestProxyConnectionReuse(t *testing.T) {
|
|||||||
defer handler.Close()
|
defer handler.Close()
|
||||||
|
|
||||||
// Verify connection reuse by checking that the same transport is used
|
// Verify connection reuse by checking that the same transport is used
|
||||||
|
|
||||||
// We can't easily count raw TCP connections in this test setup,
|
// We can't easily count raw TCP connections in this test setup,
|
||||||
// but we can verify that the transport is being reused by checking
|
// but we can verify that the transport is being reused by checking
|
||||||
// that the same transport handles both requests
|
// that the same transport handles both requests
|
||||||
@@ -402,7 +404,10 @@ func TestProxyPreservesBody(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
testBody := `{"model": "test", "messages": []}`
|
testBody := `{"model": "test", "messages": []}`
|
||||||
resp, _ := http.Post(server.URL+"/test", "application/json", strings.NewReader(testBody))
|
resp, err := http.Post(server.URL+"/test", "application/json", strings.NewReader(testBody))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
|
|
||||||
if receivedBody != testBody {
|
if receivedBody != testBody {
|
||||||
|
|||||||
@@ -102,7 +102,10 @@ func TestReadTimeout(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
resp, _ := http.Get(server.URL + "/test")
|
resp, err := http.Get(server.URL + "/test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
elapsed := time.Since(start)
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
// Should timeout around the read timeout (with some tolerance)
|
// Should timeout around the read timeout (with some tolerance)
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import (
|
|||||||
|
|
||||||
// OpenAI-style tool definition
|
// OpenAI-style tool definition
|
||||||
type Tool struct {
|
type Tool struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Function ToolFunction `json:"function"`
|
Function ToolFunction `json:"function"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ToolFunction struct {
|
type ToolFunction struct {
|
||||||
@@ -28,21 +28,21 @@ type ToolFunction struct {
|
|||||||
|
|
||||||
// OpenAI chat completion with tools request
|
// OpenAI chat completion with tools request
|
||||||
type ChatCompletionRequest struct {
|
type ChatCompletionRequest struct {
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
Messages []Message `json:"messages"`
|
Messages []Message `json:"messages"`
|
||||||
Tools []Tool `json:"tools,omitempty"`
|
Tools []Tool `json:"tools,omitempty"`
|
||||||
Stream bool `json:"stream,omitempty"`
|
Stream bool `json:"stream,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Message struct {
|
type Message struct {
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Content interface{} `json:"content"`
|
Content interface{} `json:"content"`
|
||||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ToolCall struct {
|
type ToolCall struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Function FunctionCall `json:"function"`
|
Function FunctionCall `json:"function"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,21 +64,21 @@ func TestToolCallOpenAIStyle(t *testing.T) {
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
response := map[string]interface{}{
|
response := map[string]interface{}{
|
||||||
"id": "chatcmpl-123",
|
"id": "chatcmpl-123",
|
||||||
"object": "chat.completion",
|
"object": "chat.completion",
|
||||||
"model": "reasoning",
|
"model": "reasoning",
|
||||||
"choices": []map[string]interface{}{
|
"choices": []map[string]interface{}{
|
||||||
{
|
{
|
||||||
"index": 0,
|
"index": 0,
|
||||||
"message": map[string]interface{}{
|
"message": map[string]interface{}{
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"content": nil,
|
"content": nil,
|
||||||
"tool_calls": []map[string]interface{}{
|
"tool_calls": []map[string]interface{}{
|
||||||
{
|
{
|
||||||
"id": "call_abc123",
|
"id": "call_abc123",
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": map[string]interface{}{
|
"function": map[string]interface{}{
|
||||||
"name": "get_weather",
|
"name": "get_weather",
|
||||||
"arguments": `{"location":"San Francisco","unit":"celsius"}`,
|
"arguments": `{"location":"San Francisco","unit":"celsius"}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -372,10 +372,10 @@ func TestToolCallMultiTurn(t *testing.T) {
|
|||||||
"content": nil,
|
"content": nil,
|
||||||
"tool_calls": []map[string]interface{}{
|
"tool_calls": []map[string]interface{}{
|
||||||
{
|
{
|
||||||
"id": "call_abc123",
|
"id": "call_abc123",
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": map[string]interface{}{
|
"function": map[string]interface{}{
|
||||||
"name": "get_weather",
|
"name": "get_weather",
|
||||||
"arguments": `{"location":"San Francisco"}`,
|
"arguments": `{"location":"San Francisco"}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -430,7 +430,10 @@ func TestToolCallMultiTurn(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body1, _ := json.Marshal(turn1)
|
body1, _ := json.Marshal(turn1)
|
||||||
resp1, _ := http.Post(server.URL+"/v1/chat/completions", "application/json", bytes.NewReader(body1))
|
resp1, err := http.Post(server.URL+"/v1/chat/completions", "application/json", bytes.NewReader(body1))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
var turn1Resp map[string]interface{}
|
var turn1Resp map[string]interface{}
|
||||||
json.NewDecoder(resp1.Body).Decode(&turn1Resp)
|
json.NewDecoder(resp1.Body).Decode(&turn1Resp)
|
||||||
resp1.Body.Close()
|
resp1.Body.Close()
|
||||||
@@ -472,7 +475,10 @@ func TestToolCallMultiTurn(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body2, _ := json.Marshal(turn2)
|
body2, _ := json.Marshal(turn2)
|
||||||
resp2, _ := http.Post(server.URL+"/v1/chat/completions", "application/json", bytes.NewReader(body2))
|
resp2, err := http.Post(server.URL+"/v1/chat/completions", "application/json", bytes.NewReader(body2))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
var turn2Resp map[string]interface{}
|
var turn2Resp map[string]interface{}
|
||||||
json.NewDecoder(resp2.Body).Decode(&turn2Resp)
|
json.NewDecoder(resp2.Body).Decode(&turn2Resp)
|
||||||
resp2.Body.Close()
|
resp2.Body.Close()
|
||||||
@@ -503,7 +509,7 @@ func TestParallelToolCalls(t *testing.T) {
|
|||||||
"content": nil,
|
"content": nil,
|
||||||
"tool_calls": []map[string]interface{}{
|
"tool_calls": []map[string]interface{}{
|
||||||
{
|
{
|
||||||
"id": "call_1",
|
"id": "call_1",
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": map[string]interface{}{
|
"function": map[string]interface{}{
|
||||||
"name": "get_weather",
|
"name": "get_weather",
|
||||||
@@ -511,7 +517,7 @@ func TestParallelToolCalls(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "call_2",
|
"id": "call_2",
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": map[string]interface{}{
|
"function": map[string]interface{}{
|
||||||
"name": "get_weather",
|
"name": "get_weather",
|
||||||
@@ -519,7 +525,7 @@ func TestParallelToolCalls(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "call_3",
|
"id": "call_3",
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": map[string]interface{}{
|
"function": map[string]interface{}{
|
||||||
"name": "get_weather",
|
"name": "get_weather",
|
||||||
@@ -576,7 +582,10 @@ func TestParallelToolCalls(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body, _ := json.Marshal(request)
|
body, _ := json.Marshal(request)
|
||||||
resp, _ := http.Post(server.URL+"/v1/chat/completions", "application/json", bytes.NewReader(body))
|
resp, err := http.Post(server.URL+"/v1/chat/completions", "application/json", bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
var respData map[string]interface{}
|
var respData map[string]interface{}
|
||||||
json.NewDecoder(resp.Body).Decode(&respData)
|
json.NewDecoder(resp.Body).Decode(&respData)
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
@@ -627,9 +636,9 @@ func TestAnthropicToolUse(t *testing.T) {
|
|||||||
|
|
||||||
// Anthropic response format with tool_use block
|
// Anthropic response format with tool_use block
|
||||||
response := map[string]interface{}{
|
response := map[string]interface{}{
|
||||||
"id": "msg_123",
|
"id": "msg_123",
|
||||||
"type": "message",
|
"type": "message",
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"content": []map[string]interface{}{
|
"content": []map[string]interface{}{
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@@ -714,7 +723,10 @@ func TestAnthropicToolUse(t *testing.T) {
|
|||||||
body, _ := json.Marshal(anthropicRequest)
|
body, _ := json.Marshal(anthropicRequest)
|
||||||
// Note: For now we route through a generic path
|
// Note: For now we route through a generic path
|
||||||
// In Phase 2.9+, this would be integrated with the Anthropic dialect handler
|
// In Phase 2.9+, this would be integrated with the Anthropic dialect handler
|
||||||
resp, _ := http.Post(server.URL+"/v1/messages", "application/json", bytes.NewReader(body))
|
resp, err := http.Post(server.URL+"/v1/messages", "application/json", bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
var respData map[string]interface{}
|
var respData map[string]interface{}
|
||||||
json.NewDecoder(resp.Body).Decode(&respData)
|
json.NewDecoder(resp.Body).Decode(&respData)
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
@@ -762,7 +774,7 @@ func TestComplexToolArguments(t *testing.T) {
|
|||||||
"content": nil,
|
"content": nil,
|
||||||
"tool_calls": []map[string]interface{}{
|
"tool_calls": []map[string]interface{}{
|
||||||
{
|
{
|
||||||
"id": "call_complex",
|
"id": "call_complex",
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": map[string]interface{}{
|
"function": map[string]interface{}{
|
||||||
"name": "create_event",
|
"name": "create_event",
|
||||||
@@ -832,7 +844,10 @@ func TestComplexToolArguments(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body, _ := json.Marshal(request)
|
body, _ := json.Marshal(request)
|
||||||
resp, _ := http.Post(server.URL+"/v1/chat/completions", "application/json", bytes.NewReader(body))
|
resp, err := http.Post(server.URL+"/v1/chat/completions", "application/json", bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
var respData map[string]interface{}
|
var respData map[string]interface{}
|
||||||
json.NewDecoder(resp.Body).Decode(&respData)
|
json.NewDecoder(resp.Body).Decode(&respData)
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
|
|||||||
@@ -134,11 +134,14 @@ func TestUnknownModelEnumeratesValidModels(t *testing.T) {
|
|||||||
server := httptest.NewServer(handler)
|
server := httptest.NewServer(handler)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
resp, _ := http.Post(
|
resp, err := http.Post(
|
||||||
server.URL+"/v1/chat/completions",
|
server.URL+"/v1/chat/completions",
|
||||||
"application/json",
|
"application/json",
|
||||||
bytes.NewReader([]byte(`{"model":"unknown","messages":[]}`)),
|
bytes.NewReader([]byte(`{"model":"unknown","messages":[]}`)),
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
var prob map[string]interface{}
|
var prob map[string]interface{}
|
||||||
@@ -188,11 +191,14 @@ func TestMissingModelFieldReturns4xx(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
// Request with no model field
|
// Request with no model field
|
||||||
resp, _ := http.Post(
|
resp, err := http.Post(
|
||||||
server.URL+"/v1/chat/completions",
|
server.URL+"/v1/chat/completions",
|
||||||
"application/json",
|
"application/json",
|
||||||
bytes.NewReader([]byte(`{"messages":[]}`)),
|
bytes.NewReader([]byte(`{"messages":[]}`)),
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode < 400 || resp.StatusCode >= 500 {
|
if resp.StatusCode < 400 || resp.StatusCode >= 500 {
|
||||||
@@ -230,11 +236,14 @@ func TestNullModelFieldReturns4xx(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
// Request with null model
|
// Request with null model
|
||||||
resp, _ := http.Post(
|
resp, err := http.Post(
|
||||||
server.URL+"/v1/chat/completions",
|
server.URL+"/v1/chat/completions",
|
||||||
"application/json",
|
"application/json",
|
||||||
bytes.NewReader([]byte(`{"model":null,"messages":[]}`)),
|
bytes.NewReader([]byte(`{"model":null,"messages":[]}`)),
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode < 400 || resp.StatusCode >= 500 {
|
if resp.StatusCode < 400 || resp.StatusCode >= 500 {
|
||||||
@@ -266,11 +275,14 @@ func TestEmptyModelFieldReturns4xx(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
// Request with empty model string
|
// Request with empty model string
|
||||||
resp, _ := http.Post(
|
resp, err := http.Post(
|
||||||
server.URL+"/v1/chat/completions",
|
server.URL+"/v1/chat/completions",
|
||||||
"application/json",
|
"application/json",
|
||||||
bytes.NewReader([]byte(`{"model":"","messages":[]}`)),
|
bytes.NewReader([]byte(`{"model":"","messages":[]}`)),
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode < 400 || resp.StatusCode >= 500 {
|
if resp.StatusCode < 400 || resp.StatusCode >= 500 {
|
||||||
@@ -297,11 +309,14 @@ func TestInvalidJSONIsDistinguishableError(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
// Request with invalid JSON
|
// Request with invalid JSON
|
||||||
resp, _ := http.Post(
|
resp, err := http.Post(
|
||||||
server.URL+"/v1/chat/completions",
|
server.URL+"/v1/chat/completions",
|
||||||
"application/json",
|
"application/json",
|
||||||
bytes.NewReader([]byte(`not json`)),
|
bytes.NewReader([]byte(`not json`)),
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode < 400 || resp.StatusCode >= 500 {
|
if resp.StatusCode < 400 || resp.StatusCode >= 500 {
|
||||||
@@ -405,19 +420,25 @@ func TestMissingModelAndUnknownModelBothReturn4xx(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
// Test missing model
|
// Test missing model
|
||||||
resp1, _ := http.Post(
|
resp1, err := http.Post(
|
||||||
server.URL+"/v1/chat/completions",
|
server.URL+"/v1/chat/completions",
|
||||||
"application/json",
|
"application/json",
|
||||||
bytes.NewReader([]byte(`{"messages":[]}`)),
|
bytes.NewReader([]byte(`{"messages":[]}`)),
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
resp1.Body.Close()
|
resp1.Body.Close()
|
||||||
|
|
||||||
// Test unknown model
|
// Test unknown model
|
||||||
resp2, _ := http.Post(
|
resp2, err := http.Post(
|
||||||
server.URL+"/v1/chat/completions",
|
server.URL+"/v1/chat/completions",
|
||||||
"application/json",
|
"application/json",
|
||||||
bytes.NewReader([]byte(`{"model":"unknown","messages":[]}`)),
|
bytes.NewReader([]byte(`{"model":"unknown","messages":[]}`)),
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
resp2.Body.Close()
|
resp2.Body.Close()
|
||||||
|
|
||||||
// Both should be in 4xx range
|
// Both should be in 4xx range
|
||||||
@@ -460,11 +481,14 @@ func TestProblemDetailHasRequiredFields(t *testing.T) {
|
|||||||
server := httptest.NewServer(handler)
|
server := httptest.NewServer(handler)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
resp, _ := http.Post(
|
resp, err := http.Post(
|
||||||
server.URL+"/v1/chat/completions",
|
server.URL+"/v1/chat/completions",
|
||||||
"application/json",
|
"application/json",
|
||||||
bytes.NewReader([]byte(`{"model":"unknown","messages":[]}`)),
|
bytes.NewReader([]byte(`{"model":"unknown","messages":[]}`)),
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
var prob map[string]interface{}
|
var prob map[string]interface{}
|
||||||
|
|||||||
Reference in New Issue
Block a user