2026-06-26 14:22:05 +00:00
|
|
|
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-06-26 14:22:05 +00:00
|
|
|
context.Background(),
|
|
|
|
|
testModel(t),
|
|
|
|
|
"",
|
|
|
|
|
0,
|
|
|
|
|
[]Message{{
|
|
|
|
|
Role: RoleUser,
|
|
|
|
|
ContentParts: []ContentPart{{
|
|
|
|
|
Type: "text",
|
|
|
|
|
Text: "Say hello in three words.",
|
|
|
|
|
}},
|
|
|
|
|
}},
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Completions: %v", err)
|
|
|
|
|
}
|
2026-06-27 00:13:16 +00:00
|
|
|
if len(res.Messages) == 0 {
|
2026-06-26 14:22:05 +00:00
|
|
|
t.Fatal("expected at least one message")
|
|
|
|
|
}
|
2026-06-27 00:13:16 +00:00
|
|
|
text := res.Messages[len(res.Messages)-1].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),
|
|
|
|
|
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(
|
|
|
|
|
context.Background(),
|
|
|
|
|
testModel(t),
|
|
|
|
|
"",
|
|
|
|
|
0,
|
|
|
|
|
[]Message{{
|
|
|
|
|
Role: RoleUser,
|
|
|
|
|
ContentParts: []ContentPart{{
|
|
|
|
|
Type: "text",
|
|
|
|
|
Text: "Say hello in three words.",
|
|
|
|
|
}},
|
|
|
|
|
}},
|
|
|
|
|
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-06-26 14:22:05 +00:00
|
|
|
context.Background(),
|
|
|
|
|
testModel(t),
|
|
|
|
|
"",
|
|
|
|
|
10,
|
|
|
|
|
[]Message{{
|
|
|
|
|
Role: RoleUser,
|
|
|
|
|
ContentParts: []ContentPart{{
|
|
|
|
|
Type: "text",
|
|
|
|
|
Text: "Use the echo tool to say 'hello'",
|
|
|
|
|
}},
|
|
|
|
|
}},
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Completions: %v", err)
|
|
|
|
|
}
|
2026-06-27 00:13:16 +00:00
|
|
|
if len(res.Messages) == 0 {
|
2026-06-26 14:22:05 +00:00
|
|
|
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]
|
2026-06-26 14:22:05 +00:00
|
|
|
if len(final.ToolCalls) > 0 {
|
|
|
|
|
t.Error("final message still has tool calls")
|
|
|
|
|
}
|
|
|
|
|
}
|