Skip to content

Commit 7a18c29

Browse files
committed
🐛 fix(agent): honor base URL overrides in Groq/OpenRouter validation + list local LLM providers in API keys settings
Two small follow-ups on the local-LLM registration work in the preceding commit: 1. Groq and OpenRouter key validation were hard-coded to hit the public hostnames (api.groq.com, openrouter.ai) even when GROQ_BASE_URL or OPENROUTER_BASE_URL redirected the runtime chat path at a local runner. That produced a false negative on the settings page for operators pointing Groq at a local Ollama or an internal corporate gateway. The validator now resolves the validation URL from the same base URL resolution helper the Chat method uses, so self-hosted and enterprise gateways validate correctly. 2. The APIKeySettings modal's PROVIDER_INFO map and providerToIconMap now include entries for the six local LLM runners (ollama, llamacpp, localai, vllm, lm-studio, rhaiis). The placeholder copy tells the operator which URL env var to set, since local runners typically run unauthenticated. A dedicated Base URL input field per provider is tracked as a follow-up issue. Signed-off-by: Andrew Anderson <[email protected]>
1 parent 8f6500a commit 7a18c29

3 files changed

Lines changed: 81 additions & 10 deletions

File tree

pkg/agent/provider_groq.go

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,35 @@ const (
3333
// via the GROQ_MODEL env var or the settings UI.
3434
groqDefaultModel = "llama-3.3-70b-versatile"
3535

36-
// groqValidationURL is the Groq models listing endpoint. It returns
37-
// 200 for any valid API key and 401 otherwise, so it's a cheap way to
38-
// check credentials without spending tokens on a chat completion.
39-
groqValidationURL = "https://api.groq.com/openai/v1/models"
36+
// groqModelsPath is appended to the base URL to form the validation
37+
// endpoint. Together with groqResolveBaseURL it lets the validator
38+
// honor GROQ_BASE_URL so an operator pointing Groq at a local Ollama
39+
// or an internal gateway does not get a false negative from a hit
40+
// against the real Groq hostname (see groqValidationURL below).
41+
groqModelsPath = "/models"
4042
)
4143

44+
// groqResolveBaseURL returns the effective base URL for the Groq provider,
45+
// honoring the GROQ_BASE_URL override. Kept separate from NewGroqProvider so
46+
// package-level helpers (validation, tests) can consult the same resolution
47+
// without constructing a provider.
48+
func groqResolveBaseURL() string {
49+
if v := os.Getenv("GROQ_BASE_URL"); v != "" {
50+
return v
51+
}
52+
return groqDefaultBaseURL
53+
}
54+
55+
// groqValidationURL returns the models listing endpoint relative to whatever
56+
// base URL the operator has configured. It returns 200 for any valid API key
57+
// and 401 otherwise, so it's a cheap way to check credentials without spending
58+
// tokens on a chat completion. When GROQ_BASE_URL points at a local runner,
59+
// the validator hits that runner's /models endpoint instead — so self-hosted
60+
// and enterprise gateways validate correctly (#tracking-validation-urls).
61+
func groqValidationURL() string {
62+
return groqResolveBaseURL() + groqModelsPath
63+
}
64+
4265
// GroqProvider implements AIProvider for Groq (https://groq.com).
4366
type GroqProvider struct {
4467
baseURL string

pkg/agent/server_operations.go

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"io"
88
"log/slog"
99
"net/http"
10+
"os"
1011
"os/exec"
1112
"strings"
1213
"sync"
@@ -525,17 +526,29 @@ func validateOpenAIKey(ctx context.Context, apiKey string) (bool, error) {
525526
return false, fmt.Errorf("API error: %s", string(body))
526527
}
527528

528-
// openRouterValidationURL is the OpenRouter models listing endpoint. It
529-
// returns 200 for any valid API key and 401 otherwise, so it's a cheap way
530-
// to check credentials without spending tokens on a chat completion.
531-
const openRouterValidationURL = "https://openrouter.ai/api/v1/models"
529+
// openRouterDefaultValidationURL is the public OpenRouter models listing
530+
// endpoint. It returns 200 for any valid API key and 401 otherwise, so it's a
531+
// cheap way to check credentials without spending tokens on a chat completion.
532+
// When OPENROUTER_BASE_URL is set, the validation request is redirected to
533+
// that base URL's /models endpoint so operators with a self-hosted or corporate
534+
// OpenRouter proxy validate against their own endpoint, not the public one.
535+
const openRouterDefaultValidationURL = "https://openrouter.ai/api/v1/models"
536+
537+
// openRouterValidationURL resolves the validation URL at call time so a
538+
// runtime OPENROUTER_BASE_URL override is honored.
539+
func openRouterValidationURL() string {
540+
if base := os.Getenv("OPENROUTER_BASE_URL"); base != "" {
541+
return strings.TrimRight(base, "/") + "/models"
542+
}
543+
return openRouterDefaultValidationURL
544+
}
532545

533546
// validateOpenRouterKey tests an OpenRouter API key by hitting the models
534547
// listing endpoint. Mirrors validateOpenAIKey semantics: a 200 means valid,
535548
// 401 means invalid (cached as (false, nil) so we don't re-fire on every
536549
// startup — see #7923).
537550
func validateOpenRouterKey(ctx context.Context, apiKey string) (bool, error) {
538-
req, err := http.NewRequestWithContext(ctx, "GET", openRouterValidationURL, nil)
551+
req, err := http.NewRequestWithContext(ctx, "GET", openRouterValidationURL(), nil)
539552
if err != nil {
540553
return false, err
541554
}
@@ -565,7 +578,7 @@ func validateOpenRouterKey(ctx context.Context, apiKey string) (bool, error) {
565578
// valid, 401 means invalid (cached as (false, nil) so we don't re-fire on
566579
// every startup — see #7923).
567580
func validateGroqKey(ctx context.Context, apiKey string) (bool, error) {
568-
req, err := http.NewRequestWithContext(ctx, "GET", groqValidationURL, nil)
581+
req, err := http.NewRequestWithContext(ctx, "GET", groqValidationURL(), nil)
569582
if err != nil {
570583
return false, err
571584
}

web/src/components/agent/APIKeySettings.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,34 @@ const PROVIDER_INFO: Record<string, { docsUrl: string; placeholder: string }> =
9090
docsUrl: AI_PROVIDER_DOCS.groq,
9191
placeholder: 'gsk_...',
9292
},
93+
// Local LLM runners. Most do not require an API key — set the
94+
// corresponding URL env var instead (see SECURITY-MODEL.md §3). The
95+
// placeholder advises the operator how to configure the runner today;
96+
// full UI support for base URL overrides is tracked as a follow-up.
97+
ollama: {
98+
docsUrl: 'https://ollama.com',
99+
placeholder: 'Set OLLAMA_URL env var (no key needed)',
100+
},
101+
llamacpp: {
102+
docsUrl: 'https://github.com/ggml-org/llama.cpp',
103+
placeholder: 'Set LLAMACPP_URL env var (no key needed)',
104+
},
105+
localai: {
106+
docsUrl: 'https://localai.io',
107+
placeholder: 'Set LOCALAI_URL env var (no key needed)',
108+
},
109+
vllm: {
110+
docsUrl: 'https://docs.vllm.ai',
111+
placeholder: 'Set VLLM_URL env var (no key needed)',
112+
},
113+
'lm-studio': {
114+
docsUrl: 'https://lmstudio.ai',
115+
placeholder: 'Set LM_STUDIO_URL env var (no key needed)',
116+
},
117+
rhaiis: {
118+
docsUrl: 'https://docs.redhat.com/en/documentation/red_hat_ai_inference_server/',
119+
placeholder: 'Set RHAIIS_URL env var (no key needed)',
120+
},
93121
}
94122

95123
// Map backend provider key names to AgentIcon provider values
@@ -109,6 +137,13 @@ function providerToIconMap(provider: string): string {
109137
'open-webui': 'open-webui',
110138
openrouter: 'openrouter',
111139
groq: 'groq',
140+
// Local LLM runners — provider key matches the icon key 1:1
141+
ollama: 'ollama',
142+
llamacpp: 'llamacpp',
143+
localai: 'localai',
144+
vllm: 'vllm',
145+
'lm-studio': 'lm-studio',
146+
rhaiis: 'rhaiis',
112147
}
113148
return map[provider] || provider
114149
}

0 commit comments

Comments
 (0)