From 0d7486fcb57e0117cfff4ca5d3ff57f3653f2d61 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Mon, 3 Aug 2026 17:02:30 -0700 Subject: [PATCH] fix: fall back to OPENROUTER_API_KEY for Anthropic base URL When ANTHROPIC_BASE_URL is set (e.g. OpenRouter's Anthropic-compatible endpoint at https://openrouter.ai/api) but ANTHROPIC_API_KEY isn't, use OPENROUTER_API_KEY for the x-api-key header instead of erroring out. OpenRouter's Anthropic skin accepts an OpenRouter key over that header. Requires MEAT_MODEL/-model to name a Claude model so meat takes the Anthropic code path rather than its OpenAI default. --- meat/anthropic.go | 7 +++++++ meat/gateway_test.go | 48 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/meat/anthropic.go b/meat/anthropic.go index 035f062..41da72d 100644 --- a/meat/anthropic.go +++ b/meat/anthropic.go @@ -40,6 +40,10 @@ const implicitGatewayKey = "implicit" // // Resolution order: // 1. If ANTHROPIC_API_KEY (or ANTHROPIC_BASE_URL) is set, use it directly. +// If only ANTHROPIC_BASE_URL is set (no key), fall back to +// OPENROUTER_API_KEY — OpenRouter's Anthropic-compatible endpoint +// (https://openrouter.ai/api) accepts an OpenRouter key over the same +// x-api-key header Anthropic itself expects. // 2. Otherwise, on an exe.dev VM with an attached "llm" integration, route // through the gateway at https:///anthropic with managed creds. // 3. Otherwise, error. @@ -50,6 +54,9 @@ func NewAnthropicFromEnv(ctx context.Context, model string) (*AnthropicModel, er key := os.Getenv("ANTHROPIC_API_KEY") baseURL := os.Getenv("ANTHROPIC_BASE_URL") + if key == "" && baseURL != "" { + key = os.Getenv("OPENROUTER_API_KEY") + } if key != "" || baseURL != "" { return &AnthropicModel{APIKey: key, Model: model, BaseURL: baseURL}, nil } diff --git a/meat/gateway_test.go b/meat/gateway_test.go index 8ac7322..5d3d8fa 100644 --- a/meat/gateway_test.go +++ b/meat/gateway_test.go @@ -93,6 +93,54 @@ func TestNewAnthropicFromEnv_PrefersExplicitKey(t *testing.T) { } } +func TestNewAnthropicFromEnv_BaseURLFallsBackToOpenRouterKey(t *testing.T) { + t.Setenv("ANTHROPIC_API_KEY", "") + t.Setenv("ANTHROPIC_BASE_URL", "https://openrouter.ai/api") + t.Setenv("OPENROUTER_API_KEY", "sk-or-explicit") + + m, err := NewAnthropicFromEnv(context.Background(), "") + if err != nil { + t.Fatal(err) + } + if m.APIKey != "sk-or-explicit" { + t.Errorf("APIKey = %q, want sk-or-explicit", m.APIKey) + } + if m.BaseURL != "https://openrouter.ai/api" { + t.Errorf("BaseURL = %q, want https://openrouter.ai/api", m.BaseURL) + } +} + +func TestNewAnthropicFromEnv_ExplicitKeyBeatsOpenRouterFallback(t *testing.T) { + t.Setenv("ANTHROPIC_API_KEY", "sk-explicit") + t.Setenv("ANTHROPIC_BASE_URL", "https://openrouter.ai/api") + t.Setenv("OPENROUTER_API_KEY", "sk-or-explicit") + + m, err := NewAnthropicFromEnv(context.Background(), "") + if err != nil { + t.Fatal(err) + } + if m.APIKey != "sk-explicit" { + t.Errorf("APIKey = %q, want sk-explicit (explicit ANTHROPIC_API_KEY wins)", m.APIKey) + } +} + +func TestNewAnthropicFromEnv_OpenRouterKeyIgnoredWithoutBaseURL(t *testing.T) { + t.Setenv("ANTHROPIC_API_KEY", "") + t.Setenv("ANTHROPIC_BASE_URL", "") + t.Setenv("OPENROUTER_API_KEY", "sk-or-explicit") + t.Setenv("MEAT_MODEL", "") + old := exeDevMarkerPath + exeDevMarkerPath = filepath.Join(t.TempDir(), "nope") + t.Cleanup(func() { exeDevMarkerPath = old }) + + // No ANTHROPIC_BASE_URL means no signal the caller wants an + // Anthropic-compatible third-party endpoint, so OPENROUTER_API_KEY must + // not be used implicitly. + if _, err := NewAnthropicFromEnv(context.Background(), ""); err == nil { + t.Errorf("want error: OPENROUTER_API_KEY alone should not satisfy Anthropic credentials") + } +} + func TestNewAnthropicFromEnv_FallsBackToGateway(t *testing.T) { t.Setenv("ANTHROPIC_API_KEY", "") t.Setenv("ANTHROPIC_BASE_URL", "")