Files
odidere/internal/llm/llm_integration_test.go

211 lines
4.5 KiB
Go
Raw Permalink Normal View History

package llm
import (
"context"
"log/slog"
"os"
"testing"
"code.chimeric.al/chimerical/odidere/internal/config"
)
// 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),
Timeout: "30s",
}
client, err := NewClient(cfg, nil, slog.Default())
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),
Timeout: "60s",
}
client, err := NewClient(cfg, nil, slog.Default())
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{
Model: testModel(t),
SystemMessage: "",
MaxIterations: 0,
Messages: []Message{{
Role: RoleUser,
ContentParts: []ContentPart{{
Type: "text",
Text: "Say hello in three words.",
}},
}},
2026-07-07 15:14:07 +00:00
},
)
if err != nil {
t.Fatalf("Completions: %v", err)
}
2026-06-27 00:13:16 +00:00
if len(res.Messages) == 0 {
t.Fatal("expected at least one message")
}
2026-06-27 00:13:16 +00:00
text := res.Messages[len(res.Messages)-1].Text()
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),
Timeout: "60s",
}
client, err := NewClient(cfg, nil, slog.Default())
if err != nil {
t.Fatalf("NewClient: %v", err)
}
events := make(chan StreamEvent)
errCh := make(chan error, 1)
go func() {
errCh <- client.CompletionsStream(
2026-07-07 15:14:07 +00:00
context.Background(), CompletionsParams{
Model: testModel(t),
SystemMessage: "",
MaxIterations: 0,
Messages: []Message{{
Role: RoleUser,
ContentParts: []ContentPart{{
Type: "text",
Text: "Say hello in three words.",
}},
}},
2026-07-07 15:14:07 +00:00
}, events,
)
}()
var messages []Message
for evt := range events {
messages = append(messages, evt.Message)
}
if err := <-errCh; err != nil {
t.Fatalf("CompletionsStream: %v", err)
}
if len(messages) == 0 {
t.Fatal("expected at least one message")
}
text := messages[len(messages)-1].Text()
if text == "" {
t.Error("final message has no text")
}
}
func TestIntegration_ToolCalling(t *testing.T) {
_ = testURL(t)
// Build a minimal tool registry with a single echo tool.
reg, err := NewRegistry([]config.ToolConfig{
{
Name: "echo",
Description: "Echo back the input string",
Command: "echo",
Arguments: []string{"{{.input}}"},
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"input": map[string]any{"type": "string"},
},
"required": []string{"input"},
},
},
})
if err != nil {
t.Fatalf("NewRegistry: %v", err)
}
cfg := Config{
URL: testURL(t),
Key: testKey(t),
Timeout: "60s",
}
client, err := NewClient(cfg, reg, slog.Default())
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{
Model: testModel(t),
SystemMessage: "",
MaxIterations: 10,
Messages: []Message{{
Role: RoleUser,
ContentParts: []ContentPart{{
Type: "text",
Text: "Use the echo tool to say 'hello'",
}},
}},
2026-07-07 15:14:07 +00:00
},
)
if err != nil {
t.Fatalf("Completions: %v", err)
}
2026-06-27 00:13:16 +00:00
if len(res.Messages) == 0 {
t.Fatal("expected at least one message")
}
// The final message should be a non-tool-call assistant message.
2026-06-27 00:13:16 +00:00
final := res.Messages[len(res.Messages)-1]
if len(final.ToolCalls) > 0 {
t.Error("final message still has tool calls")
}
}