package ai import ( "context" "fmt" "net/http" "net/http/httptest" "strings" "testing" "github.com/stretchr/testify/require" ) func TestExtractChatContent_String(t *testing.T) { raw := []byte(`{"model":"grok-3","choices":[{"finish_reason":"stop","message":{"content":"你好世界"}}]}`) text, meta, err := extractChatContent(raw) require.NoError(t, err) require.Equal(t, "你好世界", text) require.Equal(t, "stop", meta.FinishReason) } func TestCompleteContinuesLengthResponseWithoutOverlap(t *testing.T) { calls := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls++ w.Header().Set("Content-Type", "application/json") if calls == 1 { _, _ = fmt.Fprint(w, `{"model":"grok-3","choices":[{"finish_reason":"length","message":{"content":"第一段,還沒"}}]}`) return } _, _ = fmt.Fprint(w, `{"model":"grok-3","choices":[{"finish_reason":"stop","message":{"content":"還沒講完。第二段。"}}]}`) })) defer server.Close() client := NewOpenAICompatible(ProviderXAI, server.URL) text, err := client.Complete(context.Background(), "key", "grok-3", "寫完整") require.NoError(t, err) require.Equal(t, "第一段,還沒講完。第二段。", text) require.Equal(t, 2, calls) } func TestExtractStreamEventFinishReason(t *testing.T) { chunk, finish, ok := extractStreamEvent(`{"choices":[{"finish_reason":"length","delta":{}}]}`) require.True(t, ok) require.Empty(t, chunk) require.Equal(t, "length", finish) } func TestCompleteStreamContinuesLengthResponse(t *testing.T) { calls := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls++ if calls == 1 { w.Header().Set("Content-Type", "text/event-stream") _, _ = fmt.Fprint(w, "data: {\"choices\":[{\"delta\":{\"content\":\"第一段,還沒\"}}]}\n\n") _, _ = fmt.Fprint(w, "data: {\"choices\":[{\"finish_reason\":\"length\",\"delta\":{}}]}\n\n") _, _ = fmt.Fprint(w, "data: [DONE]\n\n") return } w.Header().Set("Content-Type", "application/json") _, _ = fmt.Fprint(w, `{"model":"grok-3","choices":[{"finish_reason":"stop","message":{"content":"還沒講完。第二段。"}}]}`) })) defer server.Close() client := NewOpenAICompatible(ProviderXAI, server.URL) var streamed strings.Builder text, err := client.CompleteStream(context.Background(), "key", "grok-3", "寫完整", func(chunk string) error { streamed.WriteString(chunk) return nil }) require.NoError(t, err) require.Equal(t, "第一段,還沒講完。第二段。", text) require.Equal(t, text, streamed.String()) require.Equal(t, 2, calls) } func TestExtractChatContent_ArrayParts(t *testing.T) { raw := []byte(`{"choices":[{"message":{"content":[{"type":"text","text":"第一段"},{"type":"text","text":"第二段"}]}}]}`) text, _, err := extractChatContent(raw) require.NoError(t, err) require.Contains(t, text, "第一段") require.Contains(t, text, "第二段") } func TestExtractChatContent_Empty(t *testing.T) { raw := []byte(`{"choices":[{"finish_reason":"length","message":{"content":""}}]}`) text, meta, err := extractChatContent(raw) require.NoError(t, err) require.Equal(t, "", text) require.Equal(t, "length", meta.FinishReason) } func TestExtractChatContent_IgnoresReasoningOnly(t *testing.T) { // deepseek / kimi 思考模型常只回 reasoning_content;不可當聊天回覆 raw := []byte(`{"choices":[{"finish_reason":"length","message":{"content":null,"reasoning_content":"我們需要根據最近的對話來回應…"}}]}`) text, meta, err := extractChatContent(raw) require.NoError(t, err) require.Equal(t, "", text) require.Equal(t, "length", meta.FinishReason) }