2026-07-13 01:15:30 +00:00
|
|
|
|
package ai
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-07-20 06:33:14 +00:00
|
|
|
|
"context"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"net/http"
|
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
|
"strings"
|
2026-07-13 01:15:30 +00:00
|
|
|
|
"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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-20 06:33:14 +00:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-13 01:15:30 +00:00
|
|
|
|
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)
|
|
|
|
|
|
}
|