mirror of
https://github.com/katanemo/plano.git
synced 2026-05-21 13:55:15 +02:00
address review feedback on #890: default provider, --listener-port, top-level imports
This commit is contained in:
parent
cf8d297600
commit
c054523ecf
3 changed files with 81 additions and 15 deletions
|
|
@ -27,6 +27,10 @@ class ProviderDefault:
|
||||||
env_var: str
|
env_var: str
|
||||||
base_url: str
|
base_url: str
|
||||||
model_pattern: str
|
model_pattern: str
|
||||||
|
# Concrete, small, cheap model to promote to `default: true` in the
|
||||||
|
# synthesized config when this provider has an env key. Lets clients send
|
||||||
|
# bare (unprefixed) model names that Plano otherwise rejects.
|
||||||
|
default_model: str | None = None
|
||||||
# Only set for providers whose prefix in the model pattern is NOT one of the
|
# Only set for providers whose prefix in the model pattern is NOT one of the
|
||||||
# built-in SUPPORTED_PROVIDERS in cli/planoai/config_generator.py. For
|
# built-in SUPPORTED_PROVIDERS in cli/planoai/config_generator.py. For
|
||||||
# built-ins, the validator infers the interface from the model prefix and
|
# built-ins, the validator infers the interface from the model prefix and
|
||||||
|
|
@ -41,18 +45,21 @@ PROVIDER_DEFAULTS: list[ProviderDefault] = [
|
||||||
env_var="OPENAI_API_KEY",
|
env_var="OPENAI_API_KEY",
|
||||||
base_url="https://api.openai.com/v1",
|
base_url="https://api.openai.com/v1",
|
||||||
model_pattern="openai/*",
|
model_pattern="openai/*",
|
||||||
|
default_model="gpt-4o-mini",
|
||||||
),
|
),
|
||||||
ProviderDefault(
|
ProviderDefault(
|
||||||
name="anthropic",
|
name="anthropic",
|
||||||
env_var="ANTHROPIC_API_KEY",
|
env_var="ANTHROPIC_API_KEY",
|
||||||
base_url="https://api.anthropic.com/v1",
|
base_url="https://api.anthropic.com/v1",
|
||||||
model_pattern="anthropic/*",
|
model_pattern="anthropic/*",
|
||||||
|
default_model="claude-haiku-4-5",
|
||||||
),
|
),
|
||||||
ProviderDefault(
|
ProviderDefault(
|
||||||
name="gemini",
|
name="gemini",
|
||||||
env_var="GEMINI_API_KEY",
|
env_var="GEMINI_API_KEY",
|
||||||
base_url="https://generativelanguage.googleapis.com/v1beta",
|
base_url="https://generativelanguage.googleapis.com/v1beta",
|
||||||
model_pattern="gemini/*",
|
model_pattern="gemini/*",
|
||||||
|
default_model="gemini-2.5-flash",
|
||||||
),
|
),
|
||||||
ProviderDefault(
|
ProviderDefault(
|
||||||
name="groq",
|
name="groq",
|
||||||
|
|
@ -79,6 +86,7 @@ PROVIDER_DEFAULTS: list[ProviderDefault] = [
|
||||||
env_var="DO_API_KEY",
|
env_var="DO_API_KEY",
|
||||||
base_url="https://inference.do-ai.run/v1",
|
base_url="https://inference.do-ai.run/v1",
|
||||||
model_pattern="digitalocean/*",
|
model_pattern="digitalocean/*",
|
||||||
|
default_model="openai-gpt-5.4-mini",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -137,11 +145,27 @@ def synthesize_default_config(
|
||||||
for p in detection.passthrough:
|
for p in detection.passthrough:
|
||||||
model_providers.append(_entry(p, {"passthrough_auth": True}))
|
model_providers.append(_entry(p, {"passthrough_auth": True}))
|
||||||
|
|
||||||
# We intentionally don't mark any provider as `default: true` here:
|
# Promote the first env-keyed provider that ships a `default_model` to a
|
||||||
# the validator rejects wildcard models (e.g. `openai/*`) as defaults, and
|
# concrete `default: true` entry. This lets clients send bare (unprefixed)
|
||||||
# all our entries are wildcards. Clients should prefix model names (e.g.
|
# model names and still route somewhere sensible. Wildcards can't be marked
|
||||||
# `openai/gpt-4o-mini`, `do/router:software-engineering`); the wildcard
|
# default (the validator rejects it), so we add a second concrete row.
|
||||||
# matcher routes them to the correct provider.
|
#
|
||||||
|
# When no env keys are present, we leave `default` unset: the user is fully
|
||||||
|
# in pass-through mode and must prefix model names so Plano knows which
|
||||||
|
# upstream to route to.
|
||||||
|
default_picked = next(
|
||||||
|
(p for p in detection.with_keys if p.default_model is not None), None
|
||||||
|
)
|
||||||
|
if default_picked is not None:
|
||||||
|
model_providers.append(
|
||||||
|
{
|
||||||
|
"name": f"{default_picked.name}/{default_picked.default_model}",
|
||||||
|
"model": f"{default_picked.name}/{default_picked.default_model}",
|
||||||
|
"base_url": default_picked.base_url,
|
||||||
|
"access_key": f"${default_picked.env_var}",
|
||||||
|
"default": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"version": "v0.4.0",
|
"version": "v0.4.0",
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,13 @@ import sys
|
||||||
import contextlib
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
import rich_click as click
|
import rich_click as click
|
||||||
|
import yaml
|
||||||
from planoai import targets
|
from planoai import targets
|
||||||
|
from planoai.defaults import (
|
||||||
|
DEFAULT_LLM_LISTENER_PORT,
|
||||||
|
detect_providers,
|
||||||
|
synthesize_default_config,
|
||||||
|
)
|
||||||
|
|
||||||
# Brand color - Plano purple
|
# Brand color - Plano purple
|
||||||
PLANO_COLOR = "#969FF4"
|
PLANO_COLOR = "#969FF4"
|
||||||
|
|
@ -317,7 +323,23 @@ def build(docker):
|
||||||
help="Show detailed startup logs with timestamps.",
|
help="Show detailed startup logs with timestamps.",
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
)
|
)
|
||||||
def up(file, path, foreground, with_tracing, tracing_port, docker, verbose):
|
@click.option(
|
||||||
|
"--listener-port",
|
||||||
|
default=DEFAULT_LLM_LISTENER_PORT,
|
||||||
|
type=int,
|
||||||
|
show_default=True,
|
||||||
|
help="Override the LLM listener port when running without a config file. Ignored when a config file is present.",
|
||||||
|
)
|
||||||
|
def up(
|
||||||
|
file,
|
||||||
|
path,
|
||||||
|
foreground,
|
||||||
|
with_tracing,
|
||||||
|
tracing_port,
|
||||||
|
docker,
|
||||||
|
verbose,
|
||||||
|
listener_port,
|
||||||
|
):
|
||||||
"""Starts Plano."""
|
"""Starts Plano."""
|
||||||
from rich.status import Status
|
from rich.status import Status
|
||||||
|
|
||||||
|
|
@ -332,15 +354,8 @@ def up(file, path, foreground, with_tracing, tracing_port, docker, verbose):
|
||||||
# pass-through config that covers the common LLM providers and
|
# pass-through config that covers the common LLM providers and
|
||||||
# auto-wires OTel export to ``planoai obs``. See cli/planoai/defaults.py.
|
# auto-wires OTel export to ``planoai obs``. See cli/planoai/defaults.py.
|
||||||
if not os.path.exists(plano_config_file):
|
if not os.path.exists(plano_config_file):
|
||||||
import yaml
|
|
||||||
|
|
||||||
from planoai.defaults import (
|
|
||||||
detect_providers,
|
|
||||||
synthesize_default_config,
|
|
||||||
)
|
|
||||||
|
|
||||||
detection = detect_providers()
|
detection = detect_providers()
|
||||||
cfg_dict = synthesize_default_config()
|
cfg_dict = synthesize_default_config(listener_port=listener_port)
|
||||||
|
|
||||||
default_dir = os.path.expanduser("~/.plano")
|
default_dir = os.path.expanduser("~/.plano")
|
||||||
os.makedirs(default_dir, exist_ok=True)
|
os.makedirs(default_dir, exist_ok=True)
|
||||||
|
|
@ -350,7 +365,7 @@ def up(file, path, foreground, with_tracing, tracing_port, docker, verbose):
|
||||||
plano_config_file = synthesized_path
|
plano_config_file = synthesized_path
|
||||||
console.print(
|
console.print(
|
||||||
f"[dim]No plano config found; using defaults ({detection.summary}). "
|
f"[dim]No plano config found; using defaults ({detection.summary}). "
|
||||||
f"Listening on :12000, tracing -> http://localhost:4317.[/dim]"
|
f"Listening on :{listener_port}, tracing -> http://localhost:4317.[/dim]"
|
||||||
)
|
)
|
||||||
|
|
||||||
if not docker:
|
if not docker:
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ def test_zero_env_vars_produces_pure_passthrough():
|
||||||
for provider in cfg["model_providers"]:
|
for provider in cfg["model_providers"]:
|
||||||
assert provider.get("passthrough_auth") is True
|
assert provider.get("passthrough_auth") is True
|
||||||
assert "access_key" not in provider
|
assert "access_key" not in provider
|
||||||
|
# No provider should be marked default in pure pass-through mode.
|
||||||
|
assert provider.get("default") is not True
|
||||||
# All known providers should be listed.
|
# All known providers should be listed.
|
||||||
names = {p["name"] for p in cfg["model_providers"]}
|
names = {p["name"] for p in cfg["model_providers"]}
|
||||||
assert "digitalocean" in names
|
assert "digitalocean" in names
|
||||||
|
|
@ -42,6 +44,31 @@ def test_env_keys_promote_providers_to_env_keyed():
|
||||||
assert by_name["anthropic"].get("passthrough_auth") is True
|
assert by_name["anthropic"].get("passthrough_auth") is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_first_env_keyed_provider_becomes_default():
|
||||||
|
cfg = synthesize_default_config(
|
||||||
|
env={"OPENAI_API_KEY": "sk-1", "ANTHROPIC_API_KEY": "a-1"}
|
||||||
|
)
|
||||||
|
defaults = [p for p in cfg["model_providers"] if p.get("default") is True]
|
||||||
|
assert len(defaults) == 1
|
||||||
|
# openai appears first in PROVIDER_DEFAULTS so it wins.
|
||||||
|
assert defaults[0]["model"] == "openai/gpt-4o-mini"
|
||||||
|
assert defaults[0]["access_key"] == "$OPENAI_API_KEY"
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_skips_providers_without_default_model():
|
||||||
|
# Groq has no default_model wired up — the next env-keyed provider with one
|
||||||
|
# should be picked instead.
|
||||||
|
cfg = synthesize_default_config(env={"GROQ_API_KEY": "g", "ANTHROPIC_API_KEY": "a"})
|
||||||
|
defaults = [p for p in cfg["model_providers"] if p.get("default") is True]
|
||||||
|
assert len(defaults) == 1
|
||||||
|
assert defaults[0]["model"].startswith("anthropic/")
|
||||||
|
|
||||||
|
|
||||||
|
def test_listener_port_is_configurable():
|
||||||
|
cfg = synthesize_default_config(env={}, listener_port=11000)
|
||||||
|
assert cfg["listeners"][0]["port"] == 11000
|
||||||
|
|
||||||
|
|
||||||
def test_detection_summary_strings():
|
def test_detection_summary_strings():
|
||||||
det = detect_providers(env={"OPENAI_API_KEY": "sk", "DO_API_KEY": "d"})
|
det = detect_providers(env={"OPENAI_API_KEY": "sk", "DO_API_KEY": "d"})
|
||||||
summary = det.summary
|
summary = det.summary
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue