mirror of
https://github.com/katanemo/plano.git
synced 2026-04-25 00:36:34 +02:00
Wires `vercel/*` and `openrouter/*` into the synthesized default config so `planoai up` with no user config exposes both providers out of the box (env-keyed via AI_GATEWAY_API_KEY / OPENROUTER_API_KEY, pass-through otherwise). Registers both in SUPPORTED_PROVIDERS_WITHOUT_BASE_URL so wildcard model entries validate without an explicit provider_interface.
178 lines
5.9 KiB
Python
178 lines
5.9 KiB
Python
"""Default config synthesizer for zero-config ``planoai up``.
|
|
|
|
When the user runs ``planoai up`` in a directory with no ``config.yaml`` /
|
|
``plano_config.yaml``, we synthesize a pass-through config that covers the
|
|
common LLM providers and auto-wires OTel export to ``localhost:4317`` so
|
|
``planoai obs`` works out of the box.
|
|
|
|
Auth handling:
|
|
- If the provider's env var is set, bind ``access_key: $ENV_VAR``.
|
|
- Otherwise set ``passthrough_auth: true`` so the client's own Authorization
|
|
header is forwarded. No env var is required to start the proxy.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from dataclasses import dataclass
|
|
|
|
DEFAULT_LLM_LISTENER_PORT = 12000
|
|
# plano_config validation requires an http:// scheme on the OTLP endpoint.
|
|
DEFAULT_OTLP_ENDPOINT = "http://localhost:4317"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ProviderDefault:
|
|
name: str
|
|
env_var: str
|
|
base_url: str
|
|
model_pattern: str
|
|
# 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-ins, the validator infers the interface from the model prefix and
|
|
# rejects configs that set this field explicitly.
|
|
provider_interface: str | None = None
|
|
|
|
|
|
# Keep ordering stable so synthesized configs diff cleanly across runs.
|
|
PROVIDER_DEFAULTS: list[ProviderDefault] = [
|
|
ProviderDefault(
|
|
name="openai",
|
|
env_var="OPENAI_API_KEY",
|
|
base_url="https://api.openai.com/v1",
|
|
model_pattern="openai/*",
|
|
),
|
|
ProviderDefault(
|
|
name="anthropic",
|
|
env_var="ANTHROPIC_API_KEY",
|
|
base_url="https://api.anthropic.com/v1",
|
|
model_pattern="anthropic/*",
|
|
),
|
|
ProviderDefault(
|
|
name="gemini",
|
|
env_var="GEMINI_API_KEY",
|
|
base_url="https://generativelanguage.googleapis.com/v1beta",
|
|
model_pattern="gemini/*",
|
|
),
|
|
ProviderDefault(
|
|
name="groq",
|
|
env_var="GROQ_API_KEY",
|
|
base_url="https://api.groq.com/openai/v1",
|
|
model_pattern="groq/*",
|
|
),
|
|
ProviderDefault(
|
|
name="deepseek",
|
|
env_var="DEEPSEEK_API_KEY",
|
|
base_url="https://api.deepseek.com/v1",
|
|
model_pattern="deepseek/*",
|
|
),
|
|
ProviderDefault(
|
|
name="mistral",
|
|
env_var="MISTRAL_API_KEY",
|
|
base_url="https://api.mistral.ai/v1",
|
|
model_pattern="mistral/*",
|
|
),
|
|
# DigitalOcean Gradient is a first-class provider post-#889 — the
|
|
# `digitalocean/` model prefix routes to the built-in Envoy cluster, no
|
|
# base_url needed at runtime.
|
|
ProviderDefault(
|
|
name="digitalocean",
|
|
env_var="DO_API_KEY",
|
|
base_url="https://inference.do-ai.run/v1",
|
|
model_pattern="digitalocean/*",
|
|
),
|
|
ProviderDefault(
|
|
name="vercel",
|
|
env_var="AI_GATEWAY_API_KEY",
|
|
base_url="https://ai-gateway.vercel.sh/v1",
|
|
model_pattern="vercel/*",
|
|
),
|
|
# OpenRouter is a first-class provider — the `openrouter/` model prefix is
|
|
# accepted by the schema and brightstaff's ProviderId parser, so no
|
|
# provider_interface override is needed.
|
|
ProviderDefault(
|
|
name="openrouter",
|
|
env_var="OPENROUTER_API_KEY",
|
|
base_url="https://openrouter.ai/api/v1",
|
|
model_pattern="openrouter/*",
|
|
),
|
|
]
|
|
|
|
|
|
@dataclass
|
|
class DetectionResult:
|
|
with_keys: list[ProviderDefault]
|
|
passthrough: list[ProviderDefault]
|
|
|
|
@property
|
|
def summary(self) -> str:
|
|
parts = []
|
|
if self.with_keys:
|
|
parts.append("env-keyed: " + ", ".join(p.name for p in self.with_keys))
|
|
if self.passthrough:
|
|
parts.append("pass-through: " + ", ".join(p.name for p in self.passthrough))
|
|
return " | ".join(parts) if parts else "no providers"
|
|
|
|
|
|
def detect_providers(env: dict[str, str] | None = None) -> DetectionResult:
|
|
env = env if env is not None else dict(os.environ)
|
|
with_keys: list[ProviderDefault] = []
|
|
passthrough: list[ProviderDefault] = []
|
|
for p in PROVIDER_DEFAULTS:
|
|
val = env.get(p.env_var)
|
|
if val:
|
|
with_keys.append(p)
|
|
else:
|
|
passthrough.append(p)
|
|
return DetectionResult(with_keys=with_keys, passthrough=passthrough)
|
|
|
|
|
|
def synthesize_default_config(
|
|
env: dict[str, str] | None = None,
|
|
*,
|
|
listener_port: int = DEFAULT_LLM_LISTENER_PORT,
|
|
otel_endpoint: str = DEFAULT_OTLP_ENDPOINT,
|
|
) -> dict:
|
|
"""Build a pass-through config dict suitable for validation + envoy rendering.
|
|
|
|
The returned dict can be dumped to YAML and handed to the existing `planoai up`
|
|
pipeline unchanged.
|
|
"""
|
|
detection = detect_providers(env)
|
|
|
|
def _entry(p: ProviderDefault, base: dict) -> dict:
|
|
row: dict = {"name": p.name, "model": p.model_pattern, "base_url": p.base_url}
|
|
if p.provider_interface is not None:
|
|
row["provider_interface"] = p.provider_interface
|
|
row.update(base)
|
|
return row
|
|
|
|
model_providers: list[dict] = []
|
|
for p in detection.with_keys:
|
|
model_providers.append(_entry(p, {"access_key": f"${p.env_var}"}))
|
|
for p in detection.passthrough:
|
|
model_providers.append(_entry(p, {"passthrough_auth": True}))
|
|
|
|
# No explicit `default: true` entry is synthesized: the plano config
|
|
# validator rejects wildcard models as defaults, and brightstaff already
|
|
# registers bare model names as lookup keys during wildcard expansion
|
|
# (crates/common/src/llm_providers.rs), so `{"model": "gpt-4o-mini"}`
|
|
# without a prefix resolves via the openai wildcard without needing
|
|
# `default: true`. See discussion on #890.
|
|
|
|
return {
|
|
"version": "v0.4.0",
|
|
"listeners": [
|
|
{
|
|
"name": "llm",
|
|
"type": "model",
|
|
"port": listener_port,
|
|
"address": "0.0.0.0",
|
|
}
|
|
],
|
|
"model_providers": model_providers,
|
|
"tracing": {
|
|
"random_sampling": 100,
|
|
"opentracing_grpc_endpoint": otel_endpoint,
|
|
},
|
|
}
|