diff options
| -rw-r--r-- | docs/superpowers/plans/2026-08-09-external-providers.md | 40 | ||||
| -rw-r--r-- | llamachat/providers.py | 20 | ||||
| -rwxr-xr-x | test_llamachat.py | 20 |
3 files changed, 70 insertions, 10 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), diff --git a/llamachat/providers.py b/llamachat/providers.py index 48b4883..d955436 100644 --- a/llamachat/providers.py +++ b/llamachat/providers.py @@ -63,11 +63,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(): @@ -77,12 +81,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), diff --git a/test_llamachat.py b/test_llamachat.py index 6a5c0e6..09d6203 100755 --- a/test_llamachat.py +++ b/test_llamachat.py @@ -958,6 +958,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") |
