Keep base URL out of config errors and always keep a bounded error excerpt
Independent review Codex gpt-6-sol high: CHANGES_REQUIRED (1 high, 1 medium)
CI / ci (push) Successful in 1m27s
CI / ci (pull_request) Successful in 1m29s

New no longer echoes the supplied base URL in any configuration error, so
a credential in a rejected URL's query or fragment cannot leak.

llm.NewProviderError now takes the single credential sent with the
request ("" when none) and always keeps a body excerpt bounded to
MaxBodyExcerpt bytes, redacting the credential first. Keyless Ollama
errors therefore keep the vendor's explanation, e.g. model not found.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 16:28:21 -04:00
co-authored by Claude Opus 5.5
parent 6d0bea10b5
commit 1f4356cd17
4 changed files with 95 additions and 43 deletions
+6 -6
View File
@@ -74,15 +74,17 @@ var _ llm.Provider = (*Provider)(nil)
// New validates cfg and returns a Provider.
func New(cfg Config) (*Provider, error) {
// Base URL errors never echo the URL: a rejected one may carry a
// credential.
u, err := url.Parse(cfg.BaseURL)
if err != nil {
return nil, errors.New("openai: base URL does not parse")
}
if (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
return nil, fmt.Errorf("openai: base URL %q must be an absolute http or https URL", u.Redacted())
return nil, errors.New("openai: base URL must be an absolute http or https URL")
}
if u.User != nil || u.RawQuery != "" || u.Fragment != "" {
return nil, fmt.Errorf("openai: base URL %q must not carry credentials, a query or a fragment", u.Redacted())
return nil, errors.New("openai: base URL must not carry credentials, a query or a fragment")
}
switch cfg.StructuredOutput {
case JSONSchema, JSONObject, PromptOnly:
@@ -217,7 +219,7 @@ func (p *Provider) Generate(ctx context.Context, req llm.Request) (llm.Response,
}
if len(cr.Error) > 0 && string(cr.Error) != "null" {
// OpenRouter can report an upstream failure in a 2xx body.
return llm.Response{}, fmt.Errorf("openai: generate: %w", llm.NewProviderError(status, raw, p.secrets()))
return llm.Response{}, fmt.Errorf("openai: generate: %w", llm.NewProviderError(status, raw, p.apiKey))
}
if len(cr.Choices) == 0 {
return llm.Response{}, errors.New("openai: generate: response has no choices")
@@ -319,8 +321,6 @@ func (p *Provider) Verify(ctx context.Context) error {
return err
}
func (p *Provider) secrets() []string { return []string{p.apiKey} }
// do sends one request and returns the body and status of a 2xx response.
// Any other status becomes the matching llm typed error.
func (p *Provider) do(ctx context.Context, method, path string, payload []byte) ([]byte, int, error) {
@@ -376,7 +376,7 @@ func (p *Provider) statusError(resp *http.Response, body []byte) error {
case http.StatusRequestTimeout:
return fmt.Errorf("HTTP %d: %w", resp.StatusCode, llm.ErrTimeout)
default:
return llm.NewProviderError(resp.StatusCode, body, p.secrets())
return llm.NewProviderError(resp.StatusCode, body, p.apiKey)
}
}