package provider import ( "context" "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" "haixun-backend/internal/model/ai/domain/enum" domai "haixun-backend/internal/model/ai/domain/usecase" ) func TestOpenAICompatible_ListModels(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet || r.URL.Path != "/models" { t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) } if got := r.Header.Get("Authorization"); got != "Bearer test-token" { t.Fatalf("unexpected auth header: %s", got) } _, _ = w.Write([]byte(`{"data":[{"id":"grok-3-fast"},{"id":"grok-3"},{"id":""}]}`)) })) defer server.Close() p := NewOpenAICompatible(enum.ProviderXAI, server.URL) models, err := p.ListModels(context.Background(), domai.Credential{APIKey: "test-token"}) if err != nil { t.Fatalf("ListModels() error = %v", err) } want := []string{"grok-3", "grok-3-fast"} if len(models) != len(want) { t.Fatalf("models = %v, want %v", models, want) } for i, id := range want { if models[i] != id { t.Fatalf("models[%d] = %s, want %s", i, models[i], id) } } } func TestOpenAICompatible_ListModels_MissingToken(t *testing.T) { p := NewOpenAICompatible(enum.ProviderXAI, "https://example.com") _, err := p.ListModels(context.Background(), domai.Credential{}) if err == nil { t.Fatal("expected error for missing token") } } func TestOpenAICompatible_GenerateText_UsesContent(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost || r.URL.Path != "/chat/completions" { t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) } body, _ := io.ReadAll(r.Body) var req chatCompletionRequest if err := json.Unmarshal(body, &req); err != nil { t.Fatalf("unmarshal request: %v", err) } if req.Stream { t.Fatal("expected non-stream request") } if req.Thinking == nil || req.Thinking.Type != "disabled" { t.Fatalf("thinking = %+v, want disabled for deepseek on opencode", req.Thinking) } if req.MaxTokens == nil || *req.MaxTokens < 2048 { t.Fatalf("max_tokens = %+v, want >= 2048 for deepseek on opencode", req.MaxTokens) } _, _ = w.Write([]byte(`{"choices":[{"message":{"content":"hello"},"finish_reason":"stop"}]}`)) })) defer server.Close() p := NewOpenAICompatible(enum.ProviderOpenCode, server.URL) maxTokens := 256 result, err := p.GenerateText(context.Background(), domai.GenerateRequest{ Provider: enum.ProviderOpenCode, Model: "deepseek-v4-flash", Credential: domai.Credential{APIKey: "test-token"}, Messages: []domai.Message{{Role: "user", Content: "hi"}}, MaxTokens: &maxTokens, }) if err != nil { t.Fatalf("GenerateText() error = %v", err) } if result.Text != "hello" || result.FinishReason != "stop" { t.Fatalf("result = %+v, want text=hello finish_reason=stop", result) } } func TestOpenAICompatible_GenerateText_FallsBackToReasoningContent(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"choices":[{"message":{"content":"","reasoning_content":"thinking only"},"finish_reason":"length"}]}`)) })) defer server.Close() p := NewOpenAICompatible(enum.ProviderXAI, server.URL) result, err := p.GenerateText(context.Background(), domai.GenerateRequest{ Model: "deepseek-test", Credential: domai.Credential{APIKey: "test-token"}, Messages: []domai.Message{{Role: "user", Content: "hi"}}, }) if err != nil { t.Fatalf("GenerateText() error = %v", err) } if result.Text != "thinking only" { t.Fatalf("result.Text = %q, want thinking only", result.Text) } } func TestOpenAICompatible_StreamText_ParsesReasoningContent(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte("data: {\"choices\":[{\"delta\":{\"reasoning_content\":\"think\"}}]}\n\n")) _, _ = w.Write([]byte("data: {\"choices\":[{\"delta\":{\"content\":\"answer\"},\"finish_reason\":\"stop\"}]}\n\n")) _, _ = w.Write([]byte("data: [DONE]\n\n")) })) defer server.Close() p := NewOpenAICompatible(enum.ProviderXAI, server.URL) stream, err := p.StreamText(context.Background(), domai.GenerateRequest{ Model: "deepseek-test", Credential: domai.Credential{APIKey: "test-token"}, Messages: []domai.Message{{Role: "user", Content: "hi"}}, }) if err != nil { t.Fatalf("StreamText() error = %v", err) } var deltas []string var finishReason string for event := range stream { switch event.Type { case "delta": deltas = append(deltas, event.Text) case "done": finishReason = event.FinishReason case "error": t.Fatalf("stream error: %s", event.Error) } } if strings.Join(deltas, "") != "thinkanswer" { t.Fatalf("deltas = %v, want thinkanswer", deltas) } if finishReason != "stop" { t.Fatalf("finishReason = %q, want stop", finishReason) } } func TestBuildChatCompletionRequest_DisablesThinkingForOpenCodeDeepSeek(t *testing.T) { p := NewOpenAICompatible(enum.ProviderOpenCode, "https://example.com") maxTokens := 128 body := p.buildChatCompletionRequest(domai.GenerateRequest{ Provider: enum.ProviderOpenCode, Model: "deepseek-v4-flash", Messages: []domai.Message{{Role: "user", Content: "hi"}}, MaxTokens: &maxTokens, }, true) if body.Thinking == nil || body.Thinking.Type != "disabled" { t.Fatalf("thinking = %+v, want disabled", body.Thinking) } if body.MaxTokens == nil || *body.MaxTokens < 2048 { t.Fatalf("max_tokens = %+v, want >= 2048", body.MaxTokens) } } func TestExtractAssistantText_ContentArray(t *testing.T) { msg := assistantMessage{ Content: json.RawMessage(`[{"type":"text","text":"array content"}]`), } if got := extractAssistantText(msg); got != "array content" { t.Fatalf("extractAssistantText() = %q, want array content", got) } }