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) } msgs, err := client.Completions( 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) } if len(msgs) == 0 { t.Fatal("expected at least one message") } text := msgs[len(msgs)-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( 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) } msgs, err := client.Completions( 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) } if len(msgs) == 0 { t.Fatal("expected at least one message") } // The final message should be a non-tool-call assistant message. final := msgs[len(msgs)-1] if len(final.ToolCalls) > 0 { t.Error("final message still has tool calls") } }