package server import ( "encoding/json" "io" "net/http" "net/http/httptest" "path/filepath" "strings" "testing" "git.misaka.ren/M1saka/zhanlu_proxy/internal/auth" "git.misaka.ren/M1saka/zhanlu_proxy/internal/config" "git.misaka.ren/M1saka/zhanlu_proxy/internal/store" ) // TestResponsesNonStreaming verifies POST /v1/responses with stream:false // returns a properly formatted Responses API JSON object. func TestResponsesNonStreaming(t *testing.T) { upstream, proxy, st := setupTestServer(t) defer upstream.Close() defer proxy.Close() creds := auth.Credentials{ AccessKey: "AK", SecretKey: "SK", Token: "TOKEN", APIKey: "sk-test-456", ModelBaseURL: upstream.URL, Email: "dev@example.com", } if err := st.SaveCredentials(creds); err != nil { t.Fatal(err) } body := `{"model":"GLM-4.7","input":"hi","stream":false}` resp, err := http.Post(proxy.URL+"/v1/responses", "application/json", strings.NewReader(body)) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != 200 { b, _ := io.ReadAll(resp.Body) t.Fatalf("status %d: %s", resp.StatusCode, string(b)) } var result map[string]any if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { t.Fatal(err) } if result["object"] != "response" { t.Fatalf("object = %v", result["object"]) } if result["status"] != "completed" { t.Fatalf("status = %v", result["status"]) } output, ok := result["output"].([]any) if !ok || len(output) == 0 { t.Fatalf("output = %v", result["output"]) } msg, ok := output[0].(map[string]any) if !ok { t.Fatalf("output[0] type = %T", output[0]) } if msg["type"] != "message" { t.Fatalf("output[0] type = %v", msg["type"]) } if msg["role"] != "assistant" { t.Fatalf("output[0] role = %v", msg["role"]) } content, ok := msg["content"].([]any) if !ok || len(content) == 0 { t.Fatalf("content = %v", msg["content"]) } part, ok := content[0].(map[string]any) if !ok { t.Fatalf("content[0] type = %T", content[0]) } if part["type"] != "output_text" { t.Fatalf("content[0] type = %v", part["type"]) } if part["text"] != "hello" { t.Fatalf("content[0] text = %v", part["text"]) } // Verify required SDK fields are present. if _, ok := result["parallel_tool_calls"]; !ok { t.Fatal("missing parallel_tool_calls in response") } if _, ok := result["tool_choice"]; !ok { t.Fatal("missing tool_choice in response") } if _, ok := result["tools"]; !ok { t.Fatal("missing tools in response") } // Verify output_text has logprobs field. if _, ok := part["logprobs"]; !ok { t.Fatal("missing logprobs in output_text content part") } usage, ok := result["usage"].(map[string]any) if !ok { t.Fatalf("usage = %v", result["usage"]) } if totNum(usage["input_tokens"]) != 10 { t.Fatalf("input_tokens = %v", usage["input_tokens"]) } if totNum(usage["output_tokens"]) != 20 { t.Fatalf("output_tokens = %v", usage["output_tokens"]) } if totNum(usage["total_tokens"]) != 30 { t.Fatalf("total_tokens = %v", usage["total_tokens"]) } } // TestResponsesStreaming verifies POST /v1/responses with stream:true // emits proper Responses API SSE events. func TestResponsesStreaming(t *testing.T) { upstream, proxy, st := setupTestServer(t) defer upstream.Close() defer proxy.Close() creds := auth.Credentials{ AccessKey: "AK", SecretKey: "SK", Token: "TOKEN", APIKey: "sk-test-456", ModelBaseURL: upstream.URL, Email: "dev@example.com", } if err := st.SaveCredentials(creds); err != nil { t.Fatal(err) } body := `{"model":"GLM-4.7","input":"hi","stream":true}` resp, err := http.Post(proxy.URL+"/v1/responses", "application/json", strings.NewReader(body)) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != 200 { b, _ := io.ReadAll(resp.Body) t.Fatalf("status %d: %s", resp.StatusCode, string(b)) } raw, _ := io.ReadAll(resp.Body) s := string(raw) // Check key lifecycle events are present. checks := []string{ "event: response.created", "event: response.in_progress", "event: response.output_item.added", "event: response.content_part.added", "event: response.output_text.delta", "event: response.output_text.done", "event: response.content_part.done", "event: response.output_item.done", "event: response.completed", } for _, c := range checks { if !strings.Contains(s, c) { t.Fatalf("missing %q in SSE body:\n%s", c, s) } } // Verify the text delta contains "hello". if !strings.Contains(s, `"delta":"hello"`) { t.Fatalf("text delta missing 'hello' in SSE body:\n%s", s) } // Verify required SDK fields are present in completed event. if !strings.Contains(s, `"parallel_tool_calls"`) { t.Fatalf("missing parallel_tool_calls in SSE body:\n%s", s) } if !strings.Contains(s, `"tool_choice"`) { t.Fatalf("missing tool_choice in SSE body:\n%s", s) } // Verify logprobs field in output_text. if !strings.Contains(s, `"logprobs"`) { t.Fatalf("missing logprobs in SSE body:\n%s", s) } // Verify usage in the completed event has input_tokens. if !strings.Contains(s, `"input_tokens":`) { t.Fatalf("usage missing input_tokens in SSE body:\n%s", s) } } // TestResponsesStringInput verifies the proxy correctly translates a string // "input" to a chat-completions "messages" array before forwarding upstream. func TestResponsesStringInput(t *testing.T) { var capturedBody []byte upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/chat/completions" { capturedBody, _ = io.ReadAll(r.Body) w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte("data: {\"id\":\"x\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"ok\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"x\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n\n")) return } http.NotFound(w, r) })) defer upstream.Close() st, err := store.Open(filepath.Join(t.TempDir(), "test.db")) if err != nil { t.Fatal(err) } defer st.Close() cfg := config.Config{ MobileLoginBaseURL: upstream.URL, MobileModelBaseURL: upstream.URL, UpstreamPath: "/chat/completions", DBPath: filepath.Join(t.TempDir(), "zhanlu.db"), PublicKeyPEM: defaultTestPublicKey, PhonePublicKeyPEM: defaultTestPublicKey, SM2PrivateKey: testSM2Key, PluginVersion: "1.4.2", } h := New(cfg, st) proxy := httptest.NewServer(h) defer proxy.Close() creds := auth.Credentials{ APIKey: "sk-test-456", ModelBaseURL: upstream.URL, Email: "dev@example.com", } _ = st.SaveCredentials(creds) body := `{"model":"GLM-4.7","input":"hello world","stream":false}` resp, err := http.Post(proxy.URL+"/v1/responses", "application/json", strings.NewReader(body)) if err != nil { t.Fatal(err) } defer resp.Body.Close() io.Copy(io.Discard, resp.Body) if len(capturedBody) == 0 { t.Fatal("no upstream request body captured") } var upstreamReq map[string]any if err := json.Unmarshal(capturedBody, &upstreamReq); err != nil { t.Fatal(err) } messages, ok := upstreamReq["messages"].([]any) if !ok || len(messages) != 1 { t.Fatalf("messages = %v", upstreamReq["messages"]) } msg, _ := messages[0].(map[string]any) if msg["role"] != "user" || msg["content"] != "hello world" { t.Fatalf("upstream message = %v", msg) } } // TestResponsesInstructions verifies instructions are prepended as a system // message in the upstream request. func TestResponsesInstructions(t *testing.T) { var capturedBody []byte upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/chat/completions" { capturedBody, _ = io.ReadAll(r.Body) w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte("data: {\"id\":\"x\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"ok\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"x\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n\n")) return } http.NotFound(w, r) })) defer upstream.Close() st, err := store.Open(filepath.Join(t.TempDir(), "test.db")) if err != nil { t.Fatal(err) } defer st.Close() cfg := config.Config{ MobileLoginBaseURL: upstream.URL, MobileModelBaseURL: upstream.URL, UpstreamPath: "/chat/completions", DBPath: filepath.Join(t.TempDir(), "zhanlu.db"), PublicKeyPEM: defaultTestPublicKey, PhonePublicKeyPEM: defaultTestPublicKey, SM2PrivateKey: testSM2Key, PluginVersion: "1.4.2", } h := New(cfg, st) proxy := httptest.NewServer(h) defer proxy.Close() creds := auth.Credentials{ APIKey: "sk-test-456", ModelBaseURL: upstream.URL, Email: "dev@example.com", } _ = st.SaveCredentials(creds) body := `{"model":"GLM-4.7","instructions":"be concise","input":"hello","stream":false}` resp, err := http.Post(proxy.URL+"/v1/responses", "application/json", strings.NewReader(body)) if err != nil { t.Fatal(err) } defer resp.Body.Close() io.Copy(io.Discard, resp.Body) if len(capturedBody) == 0 { t.Fatal("no upstream request body captured") } var upstreamReq map[string]any _ = json.Unmarshal(capturedBody, &upstreamReq) messages, _ := upstreamReq["messages"].([]any) if len(messages) != 2 { t.Fatalf("messages = %d items", len(messages)) } sys, _ := messages[0].(map[string]any) if sys["role"] != "system" || sys["content"] != "be concise" { t.Fatalf("system message = %v", sys) } }