$schema: 'http://json-schema.org/draft-07/schema#' type: object properties: version: type: string enum: - v0.1 - v0.1.0 - 0.1-beta - 0.2.0 - v0.3.0 - v0.4.0 agents: type: array items: type: object properties: id: type: string url: type: string additionalProperties: false required: - id - url filters: type: array items: type: object properties: id: type: string url: type: string type: type: string enum: - mcp - http transport: type: string enum: - streamable-http tool: type: string additionalProperties: false required: - id - url listeners: oneOf: - type: array additionalProperties: false items: type: object properties: name: type: string port: type: integer address: type: string timeout: type: string router: type: string enum: - plano_orchestrator_v1 max_retries: type: integer type: type: string enum: - model - prompt - agent agents: type: array items: type: object properties: id: type: string description: type: string default: type: boolean input_filters: type: array items: type: string additionalProperties: false required: - id - description input_filters: type: array items: type: string output_filters: type: array items: type: string additionalProperties: false required: - type - name - type: object # deprecated legacy format, use list format instead additionalProperties: false properties: ingress_traffic: type: object properties: address: type: string port: type: integer message_format: type: string enum: - openai timeout: type: string additionalProperties: false egress_traffic: type: object properties: address: type: string port: type: integer message_format: type: string enum: - openai timeout: type: string additionalProperties: false endpoints: type: object patternProperties: '^[a-zA-Z][a-zA-Z0-9_]*$': type: object properties: endpoint: type: string pattern: '^.*$' connect_timeout: type: string protocol: type: string enum: - http - https http_host: type: string prefix_affinity: type: boolean description: > For self-hosted multi-replica backends (e.g. vLLM): consistent-hash requests to replicas by the x-plano-prefix-hash header so the same prompt prefix lands on the same replica and reuses its KV cache. Uses ring-hash load balancing across all resolved endpoint addresses. Default false. additionalProperties: false required: - endpoint model_providers: type: array items: type: object properties: name: type: string access_key: type: string model: type: string default: type: boolean base_url: type: string passthrough_auth: type: boolean description: "When true, forwards the client's Authorization header to upstream instead of using the configured access_key. Useful for routing to services like LiteLLM that validate their own virtual keys." http_host: type: string provider_interface: type: string enum: - plano - claude - deepseek - groq - mistral - openai - xiaomi - gemini - chatgpt - digitalocean - vercel - openrouter - moonshotai headers: type: object additionalProperties: type: string description: "Additional headers to send with upstream requests (e.g., ChatGPT-Account-Id, originator)." routing_preferences: type: array description: "[DEPRECATED] Inline routing_preferences under a model_provider are auto-migrated to the top-level routing_preferences list by the config generator. New configs should declare routing_preferences at the top level with an explicit models: [...] list. See docs/routing-api.md." items: type: object properties: name: type: string description: type: string additionalProperties: false required: - name - description additionalProperties: false required: - model llm_providers: # deprecated for legacy support, use model_providers instead type: array items: type: object properties: name: type: string access_key: type: string model: type: string default: type: boolean base_url: type: string passthrough_auth: type: boolean description: "When true, forwards the client's Authorization header to upstream instead of using the configured access_key. Useful for routing to services like LiteLLM that validate their own virtual keys." http_host: type: string provider_interface: type: string enum: - plano - claude - deepseek - groq - mistral - openai - xiaomi - gemini - chatgpt - digitalocean - vercel - openrouter - moonshotai headers: type: object additionalProperties: type: string description: "Additional headers to send with upstream requests (e.g., ChatGPT-Account-Id, originator)." routing_preferences: type: array description: "[DEPRECATED] Inline routing_preferences under an llm_provider are auto-migrated to the top-level routing_preferences list by the config generator. New configs should declare routing_preferences at the top level with an explicit models: [...] list. See docs/routing-api.md." items: type: object properties: name: type: string description: type: string additionalProperties: false required: - name - description additionalProperties: false required: - model model_aliases: type: object patternProperties: '^.*$': type: object properties: target: type: string additionalProperties: false required: - target overrides: type: object properties: prompt_target_intent_matching_threshold: type: number optimize_context_window: type: boolean use_agent_orchestrator: type: boolean disable_signals: type: boolean description: "Disable agentic signal analysis (frustration, repetition, escalation, etc.) on LLM responses to save CPU. Default false." upstream_connect_timeout: type: string description: "Connect timeout for upstream provider clusters (e.g., '5s', '10s'). Default is '5s'." upstream_tls_ca_path: type: string description: "Path to the trusted CA bundle for upstream TLS verification. Default is '/etc/ssl/certs/ca-certificates.crt'." llm_routing_model: type: string description: "Model name for the LLM router (e.g., 'Plano-Orchestrator'). Must match a model in model_providers." agent_orchestration_model: type: string description: "Model name for the agent orchestrator (e.g., 'Plano-Orchestrator'). Must match a model in model_providers." orchestrator_model_context_length: type: integer description: "Maximum token length for the orchestrator/routing model context window. Default is 8192." prompt_caching: type: object description: > Automatic provider prompt caching, configured once for the whole Plano instance. Disabled by default; set enabled: true to opt in. Prompt caching never changes which model routing selects — it only keeps a conversation on the same model/provider so the upstream prompt cache stays warm across turns, and auto-injects provider cache-control markers where supported. properties: enabled: type: boolean description: "Master switch. Default false (opt-in). Applies across the entire instance." session_affinity: type: boolean description: "Auto-derive a session key from the prompt prefix (system + tools + first user message) when X-Model-Affinity is absent, so follow-up turns reuse the same warm cache. Default true when enabled." inject_cache_control: type: boolean description: "Auto-insert ephemeral cache_control breakpoints for providers that need explicit markers (Anthropic). Default true when enabled." min_prefix_tokens: type: integer minimum: 1 description: "Skip breakpoint injection when the estimated stable prefix is below this many tokens. Default 1024." session_ttl_seconds: type: integer minimum: 1 description: "Pin lifetime for implicit/explicit sessions; align with the provider cache window (e.g. 3600 for Anthropic 1h caching). Defaults to routing.session_ttl_seconds." additionalProperties: false system_prompt: type: string prompt_targets: type: array items: type: object properties: name: type: string default: type: boolean description: type: string auto_llm_dispatch_on_response: type: boolean parameters: type: array items: type: object properties: name: type: string additionalProperties: false required: type: boolean default: anyOf: - type: string - type: integer - type: boolean description: type: string type: type: string enum: type: array items: anyOf: - type: string - type: integer - type: boolean in_path: type: boolean format: type: string additionalProperties: false required: - name - description - type endpoint: type: object properties: name: type: string path: type: string http_method: type: string enum: - GET - POST http_headers: type: object additionalProperties: type: string additionalProperties: false required: - name - path system_prompt: type: string additionalProperties: false required: - name - description ratelimits: type: array items: type: object properties: model: type: string selector: type: object properties: key: type: string value: type: string additionalProperties: false required: - key - value limit: type: object properties: tokens: type: integer unit: type: string additionalProperties: false required: - tokens - unit additionalProperties: false required: - model - selector - limit tracing: type: object properties: random_sampling: type: integer trace_arch_internal: type: boolean opentracing_grpc_endpoint: type: string span_attributes: type: object properties: header_prefixes: type: array items: type: string static: type: object additionalProperties: type: string additionalProperties: false exporters: type: array items: oneOf: - type: object properties: type: type: string const: posthog url: type: string api_key: type: string distinct_id_header: type: string capture_messages: type: boolean additionalProperties: false required: - type - url - api_key additionalProperties: false mode: type: string enum: - llm - prompt routing: type: object properties: llm_provider: type: string model: type: string session_ttl_seconds: type: integer minimum: 1 description: TTL in seconds for session-pinned routing cache entries. Default 600 (10 minutes). session_max_entries: type: integer minimum: 1 maximum: 10000 description: Maximum number of session-pinned routing cache entries. Default 10000. session_cache: type: object properties: type: type: string enum: - memory - redis default: memory description: Session cache backend. "memory" (default) is in-process; "redis" is shared across replicas. url: type: string description: Redis URL, e.g. redis://localhost:6379. Required when type is redis. tenant_header: type: string description: > Optional HTTP header name whose value is used as a tenant prefix in the cache key. When set, keys are scoped as plano:affinity:{tenant_id}:{session_id}. additionalProperties: false routing_budget: type: object description: > Per-session cost gate on model switching. Self-sufficient and independent of prompt_caching — it applies whenever configured (presence of this block turns it on), derives implicit sessions on its own, and always prices warm anchors at their cached input rate (provider caches are assumed real). The default posture is to stick to the model a session is warm on. When routing proposes a different model while that session's provider cache is plausibly still warm (inferred from the idle gap vs. the provider's cache window), the actual input-token cost of abandoning the cache — context_tokens x (candidate_uncached_input_rate - anchor_cached_input_rate), output cost deliberately excluded — accrues into the session's cumulative switch spend. A paid switch is allowed only while that spend stays within max_overhead_pct percent of the session's running never-switch baseline (what staying on the anchor would have cost). An outright-cheaper switch is free but never reduces the spend. Requires a cost source in model_metrics_sources. properties: max_overhead_pct: type: number minimum: 0 description: > Cap on cumulative switching overhead, as a percentage of what the session would have cost by never switching (a whole number: 20 = 20%). The promise is "this conversation bills at most max_overhead_pct% above never-switching." 0 means never pay to switch (only outright-cheaper switches are allowed); larger values buy more quality-driven switches. Typical range 10-30. replenish_on_rebind: type: boolean description: "Reset the running baseline/spend totals when a cold session re-binds. Default true." cache_read_discount: type: number minimum: 0 maximum: 1 description: > Fallback used to estimate a model's cached input rate when the pricing feed doesn't publish one (cached_rate = input_rate x discount). A pricing detail, not a cost policy. Default 0.1. record_counterfactual: type: boolean description: > When true, a vetoed switch records the route the gate would have taken had the switch been allowed, as the plano.switch.counterfactual_route span attribute. Telemetry only — the counterfactual model is never dispatched. Useful for evals/benchmarks. Default false. required: ["max_overhead_pct"] additionalProperties: false additionalProperties: false state_storage: type: object properties: type: type: string enum: - memory - postgres connection_string: type: string description: Required when type is postgres. Supports environment variable substitution using $VAR or ${VAR} syntax. additionalProperties: false required: - type # Note: connection_string is conditionally required based on type # If type is 'postgres', connection_string must be provided # If type is 'memory', connection_string is not needed allOf: - if: properties: type: const: postgres then: required: - connection_string prompt_guards: type: object properties: input_guards: type: object properties: jailbreak: type: object properties: on_exception: type: object properties: message: type: string additionalProperties: false required: - message additionalProperties: false required: - on_exception additionalProperties: false required: - jailbreak routing_preferences: type: array items: type: object properties: name: type: string description: type: string models: type: array items: type: string minItems: 1 selection_policy: type: object properties: prefer: type: string enum: - cheapest - fastest - none additionalProperties: false required: - prefer additionalProperties: false required: - name - description - models model_metrics_sources: type: array items: oneOf: - type: object properties: type: type: string const: cost provider: type: string enum: - digitalocean - models.dev url: type: string description: "Optional override for the pricing catalog endpoint. Defaults per provider (digitalocean: DO GenAI catalog; models.dev: https://models.dev/api.json)." refresh_interval: type: integer minimum: 1 description: "Refresh interval in seconds" model_aliases: type: object description: "Map catalog keys to Plano model names used in routing_preferences. DigitalOcean keys are 'lowercase(creator)/model_id'; models.dev keys are 'creator/model_id'. Example: 'openai/openai-gpt-oss-120b: openai/gpt-4o'" additionalProperties: type: string required: - type - provider additionalProperties: false - type: object properties: type: type: string const: latency provider: type: string enum: - prometheus url: type: string query: type: string refresh_interval: type: integer minimum: 1 description: "Refresh interval in seconds" required: - type - provider - url - query additionalProperties: false additionalProperties: false required: - version - listeners