From 2b2f4110da53a03742faff89cdec17c55e091a90 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Thu, 11 Jun 2026 00:21:50 +0300 Subject: llm: return concrete client types from provider constructors Follow the Go idiom "return concrete types, accept interfaces": the provider constructors newOpenAI, newAnthropic, newOpenRouter, newOllama and their *WithTimeout variants now return their concrete *Client value types instead of the Client interface. The provider factories registered in the registry still return Client, so NewFromConfig and the registry keep working unchanged. Updated comments to explain the reasoning, dropped the now-redundant type assertions in tests, and switched the anthropic Streamer-capability tests to assert via an explicit Client interface value. Co-Authored-By: Claude Opus 4.8 --- internal/llm/anthropic.go | 11 +++++++---- internal/llm/anthropic_test.go | 20 +++++++++++--------- internal/llm/ollama.go | 7 +++++-- internal/llm/ollama_test.go | 26 +++++++++++++------------- internal/llm/openai.go | 7 +++++-- internal/llm/openai_http_test.go | 18 +++++++++--------- internal/llm/openai_sse_negative_test.go | 2 +- internal/llm/openrouter.go | 8 ++++++-- internal/llm/openrouter_test.go | 8 ++++---- 9 files changed, 61 insertions(+), 46 deletions(-) (limited to 'internal') diff --git a/internal/llm/anthropic.go b/internal/llm/anthropic.go index 17c40ad..3f7a616 100644 --- a/internal/llm/anthropic.go +++ b/internal/llm/anthropic.go @@ -81,8 +81,8 @@ type anthropicStreamError struct { // Ensure anthropicClient implements Client and Streamer. var ( - _ Client = (*anthropicClient)(nil) - _ Streamer = (*anthropicClient)(nil) + _ Client = anthropicClient{} + _ Streamer = anthropicClient{} ) func anthropicProviderFactory(cfg Config, keys ProviderKeys) (Client, error) { @@ -101,11 +101,14 @@ func anthropicProviderFactory(cfg Config, keys ProviderKeys) (Client, error) { // Constructor // newAnthropic constructs an Anthropic client using explicit configuration values. // The apiKey may be empty; calls will fail until a valid key is supplied. -func newAnthropic(baseURL, model, apiKey string, defaultTemp *float64) Client { +// Following the Go idiom "return concrete types, accept interfaces", this +// returns anthropicClient directly; the provider registry wraps it back into a +// Client at registration time. +func newAnthropic(baseURL, model, apiKey string, defaultTemp *float64) anthropicClient { return newAnthropicWithTimeout(baseURL, model, apiKey, defaultTemp, 0) } -func newAnthropicWithTimeout(baseURL, model, apiKey string, defaultTemp *float64, timeoutSec int) Client { +func newAnthropicWithTimeout(baseURL, model, apiKey string, defaultTemp *float64, timeoutSec int) anthropicClient { if strings.TrimSpace(baseURL) == "" { baseURL = "https://api.anthropic.com/v1" } diff --git a/internal/llm/anthropic_test.go b/internal/llm/anthropic_test.go index 2459064..4e66d48 100644 --- a/internal/llm/anthropic_test.go +++ b/internal/llm/anthropic_test.go @@ -59,7 +59,7 @@ func TestAnthropicChat_Success(t *testing.T) { })) defer srv.Close() - c := newAnthropic(srv.URL, "claude-3-5-sonnet-20241022", "test-key", nil).(anthropicClient) + c := newAnthropic(srv.URL, "claude-3-5-sonnet-20241022", "test-key", nil) response, err := c.Chat(context.Background(), []Message{ {Role: "user", Content: "Hello"}, }) @@ -100,7 +100,7 @@ func TestAnthropicChat_APIError(t *testing.T) { })) defer srv.Close() - c := newAnthropic(srv.URL, "claude-3-5-sonnet-20241022", "invalid-key", nil).(anthropicClient) + c := newAnthropic(srv.URL, "claude-3-5-sonnet-20241022", "invalid-key", nil) _, err := c.Chat(context.Background(), []Message{ {Role: "user", Content: "Hello"}, }) @@ -128,7 +128,7 @@ func TestAnthropicChat_EmptyResponse(t *testing.T) { })) defer srv.Close() - c := newAnthropic(srv.URL, "claude-3-5-sonnet-20241022", "test-key", nil).(anthropicClient) + c := newAnthropic(srv.URL, "claude-3-5-sonnet-20241022", "test-key", nil) _, err := c.Chat(context.Background(), []Message{ {Role: "user", Content: "Hello"}, }) @@ -163,7 +163,7 @@ func TestAnthropicChat_WithTemperature(t *testing.T) { })) defer srv.Close() - c := newAnthropic(srv.URL, "claude-3-5-sonnet-20241022", "test-key", nil).(anthropicClient) + c := newAnthropic(srv.URL, "claude-3-5-sonnet-20241022", "test-key", nil) _, err := c.Chat(context.Background(), []Message{ {Role: "user", Content: "Hello"}, }, WithTemperature(0.5)) @@ -190,7 +190,9 @@ func TestAnthropicStream_Success(t *testing.T) { })) defer srv.Close() - c := newAnthropic(srv.URL, "claude-3-5-sonnet-20241022", "test-key", nil) + // newAnthropic now returns the concrete anthropicClient; assert via a Client + // interface value to keep verifying the optional Streamer capability. + var c Client = newAnthropic(srv.URL, "claude-3-5-sonnet-20241022", "test-key", nil) streamer, ok := c.(Streamer) if !ok { t.Fatalf("Anthropic client does not implement Streamer interface") @@ -213,7 +215,7 @@ func TestAnthropicStream_Success(t *testing.T) { } func TestAnthropicStream_NoAPIKey(t *testing.T) { - c := newAnthropic("https://api.anthropic.com/v1", "claude-3-5-sonnet-20241022", "", nil) + var c Client = newAnthropic("https://api.anthropic.com/v1", "claude-3-5-sonnet-20241022", "", nil) streamer, ok := c.(Streamer) if !ok { t.Fatalf("Anthropic client does not implement Streamer interface") @@ -238,21 +240,21 @@ func TestAnthropicClient_Name(t *testing.T) { func TestAnthropicClient_DefaultModel(t *testing.T) { model := "claude-3-opus-20250219" - c := newAnthropic("https://api.anthropic.com/v1", model, "test-key", nil).(anthropicClient) + c := newAnthropic("https://api.anthropic.com/v1", model, "test-key", nil) if c.DefaultModel() != model { t.Fatalf("expected '%s', got '%s'", model, c.DefaultModel()) } } func TestAnthropicClient_DefaultBaseURL(t *testing.T) { - c := newAnthropic("", "claude-3-5-sonnet-20241022", "test-key", nil).(anthropicClient) + c := newAnthropic("", "claude-3-5-sonnet-20241022", "test-key", nil) if c.baseURL != "https://api.anthropic.com/v1" { t.Fatalf("expected default base URL, got '%s'", c.baseURL) } } func TestAnthropicClient_DefaultModel_Empty(t *testing.T) { - c := newAnthropic("https://api.anthropic.com/v1", "", "test-key", nil).(anthropicClient) + c := newAnthropic("https://api.anthropic.com/v1", "", "test-key", nil) if c.defaultModel != "claude-3-5-sonnet-20240620" { t.Fatalf("expected default model, got '%s'", c.defaultModel) } diff --git a/internal/llm/ollama.go b/internal/llm/ollama.go index 98e5dce..d8f911f 100644 --- a/internal/llm/ollama.go +++ b/internal/llm/ollama.go @@ -61,11 +61,14 @@ func ollamaProviderFactory(cfg Config, keys ProviderKeys) (Client, error) { // Constructor (kept among the first functions by convention). // apiKey may be empty for local Ollama; pass a non-empty key for Ollama Cloud. -func newOllama(baseURL, model string, defaultTemp *float64, apiKey string) Client { +// Following the Go idiom "return concrete types, accept interfaces", this +// returns ollamaClient directly; the provider registry wraps it back into a +// Client at registration time. +func newOllama(baseURL, model string, defaultTemp *float64, apiKey string) ollamaClient { return newOllamaWithTimeout(baseURL, model, apiKey, defaultTemp, 0) } -func newOllamaWithTimeout(baseURL, model, apiKey string, defaultTemp *float64, timeoutSec int) Client { +func newOllamaWithTimeout(baseURL, model, apiKey string, defaultTemp *float64, timeoutSec int) ollamaClient { // Defaults target Ollama Cloud (ollama.ai); a local server is opted into // by setting base_url = "http://localhost:11434" (or HEXAI_OLLAMA_BASE_URL) // and an appropriate model. diff --git a/internal/llm/ollama_test.go b/internal/llm/ollama_test.go index 2216e21..e319617 100644 --- a/internal/llm/ollama_test.go +++ b/internal/llm/ollama_test.go @@ -49,7 +49,7 @@ func TestBuildOllamaRequest_TempOverride(t *testing.T) { } func TestOllama_NameAndModel(t *testing.T) { - c := newOllama("http://x", "model-x", nil, "").(ollamaClient) + c := newOllama("http://x", "model-x", nil, "") if c.Name() != "ollama" { t.Fatalf("name: %q", c.Name()) } @@ -71,7 +71,7 @@ func TestOllamaChat_NoAuthHeaderWhenKeyEmpty(t *testing.T) { _ = json.NewEncoder(w).Encode(map[string]any{"message": map[string]string{"role": "assistant", "content": "ok"}, "done": true}) })) defer ts.Close() - c := newOllama(ts.URL, "m", nil, "").(ollamaClient) + c := newOllama(ts.URL, "m", nil, "") c.httpClient = ts.Client() if _, err := c.Chat(context.Background(), []Message{{Role: "user", Content: "hi"}}); err != nil { t.Fatalf("unexpected: %v", err) @@ -95,7 +95,7 @@ func TestOllamaChat_AuthHeaderWhenKeySet(t *testing.T) { _ = json.NewEncoder(w).Encode(map[string]any{"message": map[string]string{"role": "assistant", "content": "ok"}, "done": true}) })) defer ts.Close() - c := newOllama(ts.URL, "m", f64p(0.1), key).(ollamaClient) + c := newOllama(ts.URL, "m", f64p(0.1), key) c.httpClient = ts.Client() if _, err := c.Chat(context.Background(), []Message{{Role: "user", Content: "hi"}}); err != nil { t.Fatalf("unexpected: %v", err) @@ -110,7 +110,7 @@ func TestOllamaChat_AuthHeaderWhenKeySet(t *testing.T) { _, _ = w.Write([]byte(`{"message":{"role":"assistant","content":"ok"},"done":true}`)) })) defer ts.Close() - c := newOllama(ts.URL, "m", nil, key).(ollamaClient) + c := newOllama(ts.URL, "m", nil, key) c.httpClient = ts.Client() if err := c.ChatStream(context.Background(), []Message{{Role: "user", Content: "hi"}}, func(string) {}); err != nil { t.Fatalf("unexpected: %v", err) @@ -130,7 +130,7 @@ func TestOllamaChat_Success(t *testing.T) { _ = json.NewEncoder(w).Encode(map[string]any{"message": map[string]string{"role": "assistant", "content": "Hello"}, "done": true}) })) defer ts.Close() - c := newOllama(ts.URL, "m", f64p(0.1), "").(ollamaClient) + c := newOllama(ts.URL, "m", f64p(0.1), "") c.httpClient = ts.Client() out, err := c.Chat(context.Background(), []Message{{Role: "user", Content: "hi"}}) if err != nil { @@ -149,7 +149,7 @@ func TestOllamaChat_EmptyContent(t *testing.T) { _ = json.NewEncoder(w).Encode(map[string]any{"message": map[string]string{"role": "assistant", "content": ""}, "done": true}) })) defer ts.Close() - c := newOllama(ts.URL, "m", nil, "").(ollamaClient) + c := newOllama(ts.URL, "m", nil, "") c.httpClient = ts.Client() if _, err := c.Chat(context.Background(), []Message{{Role: "user", Content: "x"}}); err == nil { t.Fatalf("expected error for empty content") @@ -166,7 +166,7 @@ func TestOllamaChat_Non2xx(t *testing.T) { _ = json.NewEncoder(w).Encode(map[string]any{"error": "bad"}) })) defer ts1.Close() - c1 := newOllama(ts1.URL, "m", nil, "").(ollamaClient) + c1 := newOllama(ts1.URL, "m", nil, "") c1.httpClient = ts1.Client() if _, err := c1.Chat(context.Background(), []Message{{Role: "user", Content: "x"}}); err == nil { t.Fatalf("expected error for 400 with api body") @@ -177,7 +177,7 @@ func TestOllamaChat_Non2xx(t *testing.T) { _, _ = w.Write([]byte("{}")) })) defer ts2.Close() - c2 := newOllama(ts2.URL, "m", nil, "").(ollamaClient) + c2 := newOllama(ts2.URL, "m", nil, "") c2.httpClient = ts2.Client() if _, err := c2.Chat(context.Background(), []Message{{Role: "user", Content: "x"}}); err == nil { t.Fatalf("expected error for 500") @@ -189,7 +189,7 @@ type rtFunc func(*http.Request) (*http.Response, error) func (f rtFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } func TestOllamaChat_HTTPError(t *testing.T) { - c := newOllama("http://127.0.0.1:0", "m", nil, "").(ollamaClient) + c := newOllama("http://127.0.0.1:0", "m", nil, "") c.httpClient = &http.Client{Transport: rtFunc(func(*http.Request) (*http.Response, error) { return nil, fmt.Errorf("boom") })} if _, err := c.Chat(context.Background(), []Message{{Role: "user", Content: "x"}}); err == nil { t.Fatalf("expected http error path") @@ -204,7 +204,7 @@ func TestOllamaChat_DecodeError(t *testing.T) { _, _ = w.Write([]byte("{bad json}")) })) defer ts.Close() - c := newOllama(ts.URL, "m", nil, "").(ollamaClient) + c := newOllama(ts.URL, "m", nil, "") c.httpClient = ts.Client() if _, err := c.Chat(context.Background(), []Message{{Role: "user", Content: "x"}}); err == nil { t.Fatalf("expected decode error") @@ -229,7 +229,7 @@ func TestOllamaChatStream_Success(t *testing.T) { _, _ = w.Write([]byte(`{"message":{"role":"assistant","content":"!"},"done":true}`)) })) defer ts.Close() - c := newOllama(ts.URL, "m", nil, "").(ollamaClient) + c := newOllama(ts.URL, "m", nil, "") c.httpClient = ts.Client() var got strings.Builder if err := c.ChatStream(context.Background(), []Message{{Role: "user", Content: "x"}}, func(s string) { got.WriteString(s) }); err != nil { @@ -248,7 +248,7 @@ func TestOllamaChatStream_ErrorEvent(t *testing.T) { _ = json.NewEncoder(w).Encode(map[string]any{"error": "oops"}) })) defer ts.Close() - c := newOllama(ts.URL, "m", nil, "").(ollamaClient) + c := newOllama(ts.URL, "m", nil, "") c.httpClient = ts.Client() if err := c.ChatStream(context.Background(), []Message{{Role: "user", Content: "x"}}, func(string) {}); err == nil { t.Fatalf("expected stream error") @@ -263,7 +263,7 @@ func TestOllamaChatStream_DecodeError(t *testing.T) { _, _ = w.Write([]byte("{not json}")) })) defer ts.Close() - c := newOllama(ts.URL, "m", nil, "").(ollamaClient) + c := newOllama(ts.URL, "m", nil, "") c.httpClient = ts.Client() if err := c.ChatStream(context.Background(), []Message{{Role: "user", Content: "x"}}, func(string) {}); err == nil { t.Fatalf("expected decode error") diff --git a/internal/llm/openai.go b/internal/llm/openai.go index a119fe7..d475de0 100644 --- a/internal/llm/openai.go +++ b/internal/llm/openai.go @@ -110,11 +110,14 @@ func resolveOpenAITemperature(model string, configured *float64) *float64 { // Constructor (kept among the first functions by convention) // newOpenAI constructs an OpenAI client using explicit configuration values. // The apiKey may be empty; calls will fail until a valid key is supplied. -func newOpenAI(baseURL, model, apiKey string, defaultTemp *float64) Client { +// Following the Go idiom "return concrete types, accept interfaces", this +// returns openAIClient directly so callers keep full type information; the +// provider registry wraps it back into a Client at registration time. +func newOpenAI(baseURL, model, apiKey string, defaultTemp *float64) openAIClient { return newOpenAIWithTimeout(baseURL, model, apiKey, defaultTemp, 0) } -func newOpenAIWithTimeout(baseURL, model, apiKey string, defaultTemp *float64, timeoutSec int) Client { +func newOpenAIWithTimeout(baseURL, model, apiKey string, defaultTemp *float64, timeoutSec int) openAIClient { if strings.TrimSpace(baseURL) == "" { baseURL = "https://api.openai.com/v1" } diff --git a/internal/llm/openai_http_test.go b/internal/llm/openai_http_test.go index d0fc828..400ac18 100644 --- a/internal/llm/openai_http_test.go +++ b/internal/llm/openai_http_test.go @@ -23,7 +23,7 @@ func TestOpenAI_Chat_Success(t *testing.T) { _ = json.NewEncoder(w).Encode(map[string]any{"choices": []map[string]any{{"index": 0, "message": map[string]string{"role": "assistant", "content": "OK"}}}}) })) defer srv.Close() - c := newOpenAI(srv.URL, "g", "KEY", f64p(0.2)).(openAIClient) + c := newOpenAI(srv.URL, "g", "KEY", f64p(0.2)) c.httpClient = srv.Client() out, err := c.Chat(context.Background(), []Message{{Role: "user", Content: "hi"}}) if err != nil || out != "OK" { @@ -32,7 +32,7 @@ func TestOpenAI_Chat_Success(t *testing.T) { } func TestOpenAI_Chat_MissingKey(t *testing.T) { - c := newOpenAI("http://x", "g", "", f64p(0.2)).(openAIClient) + c := newOpenAI("http://x", "g", "", f64p(0.2)) if _, err := c.Chat(context.Background(), []Message{{Role: "user", Content: "hi"}}); err == nil { t.Fatalf("expected error for missing key") } @@ -49,7 +49,7 @@ func TestOpenAI_ChatStream_SSE(t *testing.T) { _, _ = io.WriteString(w, "data: [DONE]\n") })) defer srv.Close() - c := newOpenAI(srv.URL, "g", "KEY", f64p(0.2)).(openAIClient) + c := newOpenAI(srv.URL, "g", "KEY", f64p(0.2)) c.httpClient = srv.Client() var got string err := c.ChatStream(context.Background(), []Message{{Role: "user", Content: "hi"}}, func(s string) { got += s }) @@ -75,7 +75,7 @@ func TestOpenAI_ChatStream_SSE_ErrorChunk(t *testing.T) { _, _ = io.WriteString(w, "data: [DONE]\n") })) defer srv.Close() - c := newOpenAI(srv.URL, "g", "KEY", f64p(0.2)).(openAIClient) + c := newOpenAI(srv.URL, "g", "KEY", f64p(0.2)) c.httpClient = srv.Client() var got string if err := c.ChatStream(context.Background(), []Message{{Role: "user", Content: "hi"}}, func(s string) { got += s }); err == nil { @@ -91,7 +91,7 @@ func TestOpenAI_Chat_NoChoices_Error(t *testing.T) { _ = json.NewEncoder(w).Encode(map[string]any{"choices": []any{}}) })) defer srv.Close() - c := newOpenAI(srv.URL, "g", "KEY", f64p(0.2)).(openAIClient) + c := newOpenAI(srv.URL, "g", "KEY", f64p(0.2)) c.httpClient = srv.Client() if _, err := c.Chat(context.Background(), []Message{{Role: "user", Content: "hi"}}); err == nil { t.Fatalf("expected error when choices empty") @@ -108,7 +108,7 @@ func TestOpenAI_ChatStream_SSE_EmptyDelta_NoError(t *testing.T) { _, _ = io.WriteString(w, "data: [DONE]\\n") })) defer srv.Close() - c := newOpenAI(srv.URL, "g", "KEY", f64p(0.2)).(openAIClient) + c := newOpenAI(srv.URL, "g", "KEY", f64p(0.2)) c.httpClient = srv.Client() var got string if err := c.ChatStream(context.Background(), []Message{{Role: "user", Content: "hi"}}, func(s string) { got += s }); err != nil { @@ -129,7 +129,7 @@ func TestOpenAI_Chat_DecodeError_StatusOK(t *testing.T) { _, _ = io.WriteString(w, "{invalid") })) defer srv.Close() - c := newOpenAI(srv.URL, "g", "KEY", f64p(0.2)).(openAIClient) + c := newOpenAI(srv.URL, "g", "KEY", f64p(0.2)) c.httpClient = srv.Client() if _, err := c.Chat(context.Background(), []Message{{Role: "user", Content: "hi"}}); err == nil { t.Fatalf("expected decode error for invalid JSON body") @@ -150,7 +150,7 @@ func TestOpenAI_Chat_MultiChoiceAndErrorBody(t *testing.T) { }) })) defer srv.Close() - c := newOpenAI(srv.URL, "g", "KEY", f64p(0.2)).(openAIClient) + c := newOpenAI(srv.URL, "g", "KEY", f64p(0.2)) c.httpClient = srv.Client() out, err := c.Chat(context.Background(), []Message{{Role: "user", Content: "hi"}}) if err != nil || out != "FIRST" { @@ -163,7 +163,7 @@ func TestOpenAI_Chat_MultiChoiceAndErrorBody(t *testing.T) { _ = json.NewEncoder(w).Encode(map[string]any{"error": map[string]any{"message": "bad", "type": "invalid"}}) })) defer srv2.Close() - c2 := newOpenAI(srv2.URL, "g", "KEY", f64p(0.2)).(openAIClient) + c2 := newOpenAI(srv2.URL, "g", "KEY", f64p(0.2)) c2.httpClient = srv2.Client() if _, err := c2.Chat(context.Background(), []Message{{Role: "user", Content: "hi"}}); err == nil { t.Fatalf("expected error from non-2xx with error body") diff --git a/internal/llm/openai_sse_negative_test.go b/internal/llm/openai_sse_negative_test.go index 7f4f7db..3ef9320 100644 --- a/internal/llm/openai_sse_negative_test.go +++ b/internal/llm/openai_sse_negative_test.go @@ -20,7 +20,7 @@ func TestOpenAI_ChatStream_SSE_MalformedChunk(t *testing.T) { _, _ = io.WriteString(w, "data: [DONE]\n") })) defer srv.Close() - c := newOpenAI(srv.URL, "g", "KEY", f64p(0.2)).(openAIClient) + c := newOpenAI(srv.URL, "g", "KEY", f64p(0.2)) c.httpClient = srv.Client() var got string if err := c.ChatStream(context.Background(), []Message{{Role: "user", Content: "hi"}}, func(s string) { got += s }); err != nil { diff --git a/internal/llm/openrouter.go b/internal/llm/openrouter.go index aa8a4d4..d728275 100644 --- a/internal/llm/openrouter.go +++ b/internal/llm/openrouter.go @@ -40,11 +40,15 @@ func openRouterProviderFactory(cfg Config, keys ProviderKeys) (Client, error) { ), nil } -func newOpenRouter(baseURL, model, apiKey string, defaultTemp *float64) Client { +// newOpenRouter constructs an OpenRouter client using explicit configuration +// values. Following the Go idiom "return concrete types, accept interfaces", +// this returns openRouterClient directly; the provider registry wraps it back +// into a Client at registration time. +func newOpenRouter(baseURL, model, apiKey string, defaultTemp *float64) openRouterClient { return newOpenRouterWithTimeout(baseURL, model, apiKey, defaultTemp, 0) } -func newOpenRouterWithTimeout(baseURL, model, apiKey string, defaultTemp *float64, timeoutSec int) Client { +func newOpenRouterWithTimeout(baseURL, model, apiKey string, defaultTemp *float64, timeoutSec int) openRouterClient { if strings.TrimSpace(baseURL) == "" { baseURL = "https://openrouter.ai/api/v1" } diff --git a/internal/llm/openrouter_test.go b/internal/llm/openrouter_test.go index 07d6e0f..f42459e 100644 --- a/internal/llm/openrouter_test.go +++ b/internal/llm/openrouter_test.go @@ -35,7 +35,7 @@ func TestOpenRouter_Chat_SendsHeadersAndBody(t *testing.T) { })) defer srv.Close() - c := newOpenRouter(srv.URL, "anthropic/claude-test", "KEY", f64p(0.2)).(openRouterClient) + c := newOpenRouter(srv.URL, "anthropic/claude-test", "KEY", f64p(0.2)) c.httpClient = srv.Client() out, err := c.Chat(context.Background(), []Message{{Role: "user", Content: "ping"}}) if err != nil { @@ -81,7 +81,7 @@ func TestOpenRouter_ChatStream_SendsHeaders(t *testing.T) { })) defer srv.Close() - c := newOpenRouter(srv.URL, "anthropic/claude-test", "KEY", f64p(0.2)).(openRouterClient) + c := newOpenRouter(srv.URL, "anthropic/claude-test", "KEY", f64p(0.2)) c.httpClient = srv.Client() var got string err := c.ChatStream(context.Background(), []Message{{Role: "user", Content: "ping"}}, func(s string) { got += s }) @@ -100,7 +100,7 @@ func TestOpenRouter_ChatStream_SendsHeaders(t *testing.T) { } func TestOpenRouter_Chat_MissingKey(t *testing.T) { - c := newOpenRouter("http://example", "anthropic/claude-test", "", f64p(0.2)).(openRouterClient) + c := newOpenRouter("http://example", "anthropic/claude-test", "", f64p(0.2)) if _, err := c.Chat(context.Background(), []Message{{Role: "user", Content: "ping"}}); err == nil { t.Fatalf("expected error for missing api key") } else if !strings.Contains(err.Error(), "OPENROUTER_API_KEY") || !strings.Contains(err.Error(), "HEXAI_OPENROUTER_API_KEY") { @@ -111,7 +111,7 @@ func TestOpenRouter_Chat_MissingKey(t *testing.T) { func TestOpenRouter_DefaultsAndMetadata(t *testing.T) { logger := log.New(io.Discard, "", 0) logging.Bind(logger) - c := newOpenRouter("", "", "KEY", nil).(openRouterClient) + c := newOpenRouter("", "", "KEY", nil) if c.baseURL != "https://openrouter.ai/api/v1" { t.Fatalf("default baseURL mismatch: %s", c.baseURL) } -- cgit v1.2.3