2026-06-26 14:22:05 +00:00
|
|
|
package llm
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"log/slog"
|
|
|
|
|
"os"
|
|
|
|
|
"testing"
|
2026-10-08 01:00:33 +00:00
|
|
|
"time"
|
2026-06-26 14:22:05 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// testURL returns the LLM endpoint for integration tests.
|
|
|
|
|
// Set ODIDERE_LLM_TEST_URL to enable; tests skip if empty.
|
|
|
|
|
func testURL(t *testing.T) string {
|
|
|
|
|
t.Helper()
|
|
|
|
|
url := os.Getenv("ODIDERE_LLM_TEST_URL")
|
|
|
|
|
if url == "" {
|
|
|
|
|
t.Skip("ODIDERE_LLM_TEST_URL not set")
|
|
|
|
|
}
|
|
|
|
|
return url
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// testKey returns the API key for integration tests.
|
|
|
|
|
// Reads ODIDERE_LLM_TEST_KEY; empty string is valid (no auth).
|
|
|
|
|
func testKey(t *testing.T) string {
|
|
|
|
|
t.Helper()
|
|
|
|
|
return os.Getenv("ODIDERE_LLM_TEST_KEY")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// testModel returns the model ID for integration tests.
|
|
|
|
|
// Reads ODIDERE_LLM_TEST_MODEL; defaults to "test" if empty.
|
|
|
|
|
func testModel(t *testing.T) string {
|
|
|
|
|
t.Helper()
|
|
|
|
|
model := os.Getenv("ODIDERE_LLM_TEST_MODEL")
|
|
|
|
|
if model == "" {
|
|
|
|
|
return "test"
|
|
|
|
|
}
|
|
|
|
|
return model
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestIntegration_ListModels(t *testing.T) {
|
|
|
|
|
_ = testURL(t)
|
|
|
|
|
cfg := Config{
|
|
|
|
|
URL: testURL(t),
|
|
|
|
|
Key: testKey(t),
|
2026-10-08 01:00:33 +00:00
|
|
|
Timeout: 30 * time.Second,
|
2026-06-26 14:22:05 +00:00
|
|
|
}
|
2026-09-20 12:49:42 +00:00
|
|
|
client, err := NewClient(cfg, slog.Default())
|
2026-06-26 14:22:05 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("NewClient: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
models, err := client.ListModels(context.Background())
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("ListModels: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(models) == 0 {
|
|
|
|
|
t.Error("expected at least one model")
|
|
|
|
|
}
|
|
|
|
|
for _, m := range models {
|
|
|
|
|
if m.ID == "" {
|
|
|
|
|
t.Error("model has empty ID")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestIntegration_Completions(t *testing.T) {
|
|
|
|
|
_ = testURL(t)
|
|
|
|
|
cfg := Config{
|
|
|
|
|
URL: testURL(t),
|
|
|
|
|
Key: testKey(t),
|
2026-10-08 01:00:33 +00:00
|
|
|
Timeout: 60 * time.Second,
|
2026-06-26 14:22:05 +00:00
|
|
|
}
|
2026-09-20 12:49:42 +00:00
|
|
|
client, err := NewClient(cfg, slog.Default())
|
2026-06-26 14:22:05 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("NewClient: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-27 00:13:16 +00:00
|
|
|
res, err := client.Completions(
|
2026-07-07 15:14:07 +00:00
|
|
|
context.Background(), CompletionsParams{
|
2026-09-20 12:49:42 +00:00
|
|
|
Model: testModel(t),
|
2026-07-07 15:14:07 +00:00
|
|
|
Messages: []Message{{
|
|
|
|
|
Role: RoleUser,
|
|
|
|
|
ContentParts: []ContentPart{{
|
|
|
|
|
Type: "text",
|
|
|
|
|
Text: "Say hello in three words.",
|
|
|
|
|
}},
|
2026-06-26 14:22:05 +00:00
|
|
|
}},
|
2026-07-07 15:14:07 +00:00
|
|
|
},
|
2026-06-26 14:22:05 +00:00
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Completions: %v", err)
|
|
|
|
|
}
|
2026-09-20 12:49:42 +00:00
|
|
|
text := res.Message.Text()
|
2026-06-26 14:22:05 +00:00
|
|
|
if text == "" {
|
|
|
|
|
t.Error("final message has no text")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestIntegration_CompletionsStream(t *testing.T) {
|
|
|
|
|
_ = testURL(t)
|
|
|
|
|
cfg := Config{
|
|
|
|
|
URL: testURL(t),
|
|
|
|
|
Key: testKey(t),
|
2026-10-08 01:00:33 +00:00
|
|
|
Timeout: 60 * time.Second,
|
2026-06-26 14:22:05 +00:00
|
|
|
}
|
2026-09-20 12:49:42 +00:00
|
|
|
client, err := NewClient(cfg, slog.Default())
|
2026-06-26 14:22:05 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("NewClient: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-20 12:49:42 +00:00
|
|
|
stream, err := client.CompletionsStream(
|
|
|
|
|
context.Background(),
|
|
|
|
|
CompletionsParams{
|
|
|
|
|
Model: testModel(t),
|
|
|
|
|
Messages: []Message{{
|
|
|
|
|
Role: RoleUser,
|
|
|
|
|
ContentParts: []ContentPart{{
|
|
|
|
|
Type: "text",
|
|
|
|
|
Text: "Say hello in three words.",
|
2026-06-26 14:22:05 +00:00
|
|
|
}},
|
2026-09-20 12:49:42 +00:00
|
|
|
}},
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
2026-06-26 14:22:05 +00:00
|
|
|
t.Fatalf("CompletionsStream: %v", err)
|
|
|
|
|
}
|
2026-09-20 12:49:42 +00:00
|
|
|
defer stream.Close()
|
|
|
|
|
for stream.Next() {
|
2026-06-26 14:22:05 +00:00
|
|
|
}
|
2026-09-20 12:49:42 +00:00
|
|
|
if err := stream.Err(); err != nil {
|
|
|
|
|
t.Fatalf("stream.Err: %v", err)
|
|
|
|
|
}
|
|
|
|
|
result := stream.Result()
|
|
|
|
|
if result.Message.Text() == "" {
|
2026-06-26 14:22:05 +00:00
|
|
|
t.Error("final message has no text")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestIntegration_ToolCalling(t *testing.T) {
|
|
|
|
|
_ = testURL(t)
|
2026-09-20 12:49:42 +00:00
|
|
|
client, err := NewClient(Config{
|
2026-06-26 14:22:05 +00:00
|
|
|
URL: testURL(t),
|
|
|
|
|
Key: testKey(t),
|
2026-10-08 01:00:33 +00:00
|
|
|
Timeout: 60 * time.Second,
|
2026-09-20 12:49:42 +00:00
|
|
|
}, slog.Default())
|
2026-06-26 14:22:05 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("NewClient: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-27 00:13:16 +00:00
|
|
|
res, err := client.Completions(
|
2026-09-20 12:49:42 +00:00
|
|
|
context.Background(),
|
|
|
|
|
CompletionsParams{
|
|
|
|
|
Model: testModel(t),
|
2026-07-07 15:14:07 +00:00
|
|
|
Messages: []Message{{
|
|
|
|
|
Role: RoleUser,
|
|
|
|
|
ContentParts: []ContentPart{{
|
|
|
|
|
Type: "text",
|
|
|
|
|
Text: "Use the echo tool to say 'hello'",
|
|
|
|
|
}},
|
2026-06-26 14:22:05 +00:00
|
|
|
}},
|
2026-09-20 12:49:42 +00:00
|
|
|
Tools: []APITool{{
|
|
|
|
|
Type: "function",
|
|
|
|
|
Function: &FunctionDef{
|
|
|
|
|
Name: "echo",
|
|
|
|
|
Description: "Echo back the input string",
|
|
|
|
|
Parameters: map[string]any{
|
|
|
|
|
"type": "object",
|
|
|
|
|
"properties": map[string]any{
|
|
|
|
|
"input": map[string]any{"type": "string"},
|
|
|
|
|
},
|
|
|
|
|
"required": []string{"input"},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}},
|
2026-07-07 15:14:07 +00:00
|
|
|
},
|
2026-06-26 14:22:05 +00:00
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Completions: %v", err)
|
|
|
|
|
}
|
2026-09-20 12:49:42 +00:00
|
|
|
if len(res.Message.ToolCalls) == 0 {
|
|
|
|
|
t.Fatal("expected a tool call")
|
2026-06-26 14:22:05 +00:00
|
|
|
}
|
2026-09-20 12:49:42 +00:00
|
|
|
if res.Message.ToolCalls[0].Function.Name != "echo" {
|
|
|
|
|
t.Errorf(
|
|
|
|
|
"tool name = %q, want echo",
|
|
|
|
|
res.Message.ToolCalls[0].Function.Name,
|
|
|
|
|
)
|
2026-06-26 14:22:05 +00:00
|
|
|
}
|
|
|
|
|
}
|