fix(llm): normalize and validate apiBase with descriptive errors
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, prependhttp://whenapiBasehas no protocol, then validate withnew URL(); on failure throwInvalid apiBase for provider "<name>": "<value>"...including a hint about unresolved secret referencescore/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-fatalConfigValidationErrors, and continue loading the remaining models — matching the YAML path's behaviorcore/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
apiBaseGUI 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 tohttp://...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://whenapiBasehas no protocol and keep trailing slash normalization. - Validate
apiBaseearly 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
apiBasenormalization and validation.
- Prepend