fix(llm): normalize and validate apiBase with descriptive errors

RO
5mo ago
+188-91

Fixes #11872

Summary

A bare apiBase like localhost:11434 (no protocol) or an unresolved template like ${{ secrets.MY_KEY }} surfaces as an opaque TypeError: Invalid URL deep inside provider code at request time, with no hint of which model or value is at fault. This PR normalizes protocol-less values and validates apiBase at construction with an error naming the provider and the bad value. It covers two of the three root causes RomneyDa identified in the issue; the third (the getMaxStopWords() non-null assertion) is already addressed by #12518 and is deliberately not duplicated here.

Root cause

BaseLLM's constructor (core/llm/index.ts) stores apiBase with only trailing-slash normalization; nothing validates the value, so new URL(...) fails later wherever a provider first builds a request. Separately, the JSON config path constructs models inside uncaught Promise.all calls in core/config/load.ts (chat models, tab-autocomplete, embeddings, reranker), so any single throwing constructor used to reject the entire config load, unlike the YAML path which catches per model (core/config/yaml/loadYaml.ts).

Changes

  • core/llm/index.ts: after trailing-slash normalization, prepend http:// when apiBase has no protocol, then validate with new URL(); on failure throw Invalid apiBase for provider "<name>": "<value>"... including a hint about unresolved secret references
  • core/config/load.ts: all four JSON-path construction sites (chat models, tab-autocomplete models, embeddings provider, reranker) now catch constructor errors per model, report them as non-fatal ConfigValidationErrors, and continue loading the remaining models — matching the YAML path's behavior
  • core/llm/index.test.ts: 4 new tests under "BaseLLM apiBase normalization"

What this doesn't change

  • OpenAI.getMaxStopWords() — the non-null assertion there is fixed by #12518; this PR intentionally does not touch it
  • The YAML config path, which already catches per-model construction errors
  • Provider-specific URL construction downstream of the validated apiBase
  • Empty-string apiBase GUI placeholders remain falsy and skip validation

Checklist

  • I've read the contributing guide
  • The relevant docs, if any, have been updated or created
  • The relevant tests, if any, have been updated or created

Screen recording or screenshot

Before: Uncaught TypeError: Invalid URL at request time, no indication of source.
After (config load): Invalid apiBase for provider "ollama": "localhost:11434"... reported as a config error while other models keep loading. Bare hosts like localhost:11434 now work via http:// prepending.

Tests

  • cd core && npm test -- llm/index.test.ts — 190/190 passing (186 existing + 4 new). Covers: unresolved template strings throw with provider name and value, bare hosts normalize to http://... with trailing slash, undefined apiBase skips validation, well-formed URLs pass through unchanged.
  • cd core && npm run tsc:check — clean.

Summary by cubic

Normalize and validate LLM apiBase at construction to give clear errors and support bare hosts. JSON config now treats model-construction errors as non-fatal so other models still load.

  • Bug Fixes
    • Prepend http:// when apiBase has no protocol and keep trailing slash normalization.
    • Validate apiBase early and throw a clear error with provider name and value; includes a hint for unresolved secret references.
    • JSON config path now catches constructor errors per model (chat, tab-autocomplete, embeddings, reranker), reports them as non-fatal, and continues loading.
    • Added tests for apiBase normalization and validation.