diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-09 14:14:38 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-09 14:14:38 +0200 |
| commit | 67e1badee2e087bc673f252a5463c55e4ff4f9b1 (patch) | |
| tree | d9b465ca46be65abe2a1970303d4199c4235e80a /docs | |
| parent | 7246d07dda18c07278cc9df122071f523f697be3 (diff) | |
| download | llamachat-67e1badee2e087bc673f252a5463c55e4ff4f9b1.tar.gz llamachat-67e1badee2e087bc673f252a5463c55e4ff4f9b1.zip | |
fix: stop a partial [providers.local] from deleting the local provider
The synthesis guard tested key membership, so a [providers.local] that set
only an api_key claimed the slot, blocked the bare base_url from filling
it, then failed the URL check and vanished. Losing the local provider is
the one outcome this feature cannot have. The guard now tests the URL and
merges, so the table adds detail to the local provider rather than
replacing it.
A filter given as a bare string was iterated character-wise, turning
filter = "qwen" into four needles that match almost every model id. Both
failures were silent, which is what made them worth fixing now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/superpowers/plans/2026-08-09-external-providers.md | 40 |
1 files changed, 35 insertions, 5 deletions
diff --git a/docs/superpowers/plans/2026-08-09-external-providers.md b/docs/superpowers/plans/2026-08-09-external-providers.md index a17c1d9..0c8fc19 100644 --- a/docs/superpowers/plans/2026-08-09-external-providers.md +++ b/docs/superpowers/plans/2026-08-09-external-providers.md @@ -137,6 +137,26 @@ def test_provider_parsing(): # Unset numbers stay None so "unknown" is distinguishable from zero. assert parsed["local"].ctx_size is None assert parsed["local"].price_in is None + + # A [providers.local] that omits base_url inherits the bare one rather + # than shadowing the local provider out of existence. + partial = providers.parse( + { + "base_url": "http://localhost:8181", + "providers": {"local": {"api_key": "env:SOME_VAR"}}, + } + ) + assert set(partial) == {"local"} + assert partial["local"].base_url == "http://localhost:8181" + # The explicit entry's own fields survive the merge. + assert partial["local"].api_key == "env:SOME_VAR" + + # A filter given as a bare string is one needle, not four. + stringy = providers.parse( + {"providers": {"p": {"base_url": "http://x.example.org", + "filter": "qwen"}}} + ) + assert stringy["p"].filter == ["qwen"] print("ok provider config parsing") ``` @@ -210,11 +230,15 @@ def parse(values: dict) -> dict[str, Provider]: """ table = dict(values.get("providers") or {}) - # An old config has only a bare base_url. Synthesize the local provider - # from it so nothing needs migrating, but never override an explicit one. + # An old config has only a bare base_url. Fill it in as the local + # provider's URL so nothing needs migrating, but never override an + # explicit one. The test is the URL rather than the key: a + # [providers.local] that only sets an api_key is adding detail to the + # provider the user already has, not replacing it, and treating it as a + # replacement would silently delete local entirely. bare = values.get("base_url") - if bare and LOCAL not in table: - table[LOCAL] = {"base_url": bare} + if bare and not (table.get(LOCAL) or {}).get("base_url"): + table[LOCAL] = {**(table.get(LOCAL) or {}), "base_url": bare} out: dict[str, Provider] = {} for name, entry in table.items(): @@ -224,12 +248,18 @@ def parse(values: dict) -> dict[str, Provider]: # ponytail: a provider with no URL is misconfigured, not a # partial one. Skipping beats inventing a default endpoint. continue + # filter = "qwen" is an easy TOML slip for filter = ["qwen"], and + # iterating the string would turn it into four single-character + # needles that match nearly every model id. + needles = entry.get("filter") or [] + if isinstance(needles, str): + needles = [needles] vision = entry.get("vision") out[str(name)] = Provider( name=str(name), base_url=base_url, api_key=str(entry.get("api_key") or ""), - filter=[str(f) for f in (entry.get("filter") or [])], + filter=[str(f) for f in needles], ctx_size=_number(entry.get("ctx_size"), int), vision=None if vision is None else bool(vision), price_in=_number(entry.get("price_in"), float), |
