{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://pi.dev/schemas/bifrost.json",
  "title": "Bifrost Model Router Config",
  "description": "Configuration for the pi-bifrost query-aware model router. Maps user prompts to model tiers via regex rules and an optional LLM classifier. Merged from extension defaults → global → project-local → root.",
  "type": "object",
  "properties": {
    "enabled": {
      "type": "boolean",
      "description": "Enable or disable the model router. When disabled, all requests use the default model.",
      "default": true
    },
    "silent": {
      "type": "boolean",
      "description": "Suppress Bifrost console output and UI notifications without disabling routing.",
      "default": false
    },
    "default": {
      "type": "string",
      "description": "Model tier to use when no rule matches and the classifier returns no category. Must match a key in 'models'.",
      "default": "general",
      "examples": ["quick", "general", "frontier"]
    },
    "strategy": {
      "type": "string",
      "enum": ["first", "cheapest", "cheapest_input", "cheapest_output", "largest_context", "random", "fastest", "subscription_balance"],
      "description": "Model selection strategy. 'first'/'fastest' picks first candidate (list should be pre-sorted). 'cheapest' sorts by input+output cost. 'cheapest_input' by input cost. 'cheapest_output' by output cost. 'largest_context' by context window. 'random' picks randomly. 'subscription_balance' weighs subscription-provider candidates by weekly usage remaining (see quotaRouting).",
      "default": "first"
    },
    "categoryStrategies": {
      "type": "object",
      "description": "Per-tier strategy overrides. Keys must match tier names in 'models'. If a tier is not listed here, 'strategy' is used.",
      "default": { "quick": "first", "general": "first", "frontier": "first" },
      "additionalProperties": {
        "type": "string",
        "enum": ["first", "cheapest", "cheapest_input", "cheapest_output", "largest_context", "random", "fastest", "subscription_balance"]
      }
    },
    "models": {
      "type": "object",
      "description": "Model tiers. Each key is a tier name (e.g. 'quick', 'general', 'frontier'). Values are provider/model patterns — either a single string or an ordered array of candidate patterns.",
      "default": {},
      "additionalProperties": {
        "oneOf": [
          { "type": "string" },
          {
            "type": "array",
            "items": { "type": "string" }
          }
        ]
      },
      "examples": [
        {
          "quick": ["opencode/deepseek-v4-flash-free"],
          "general": ["opencode-go/deepseek-v4-pro", "openai-codex/gpt-5.4-mini"],
          "frontier": ["openai-codex/gpt-5.6-sol", "opencode-go/glm-5.2"]
        }
      ]
    },
    "rules": {
      "type": "array",
      "description": "Regex-based routing rules applied in order. First matching rule determines the model tier. Case-insensitive matching. If no rule matches, the 'default' tier is used. Prefer using bifrost-routes.json for project-specific rules — it overrides this field.",
      "x-aiHints": {
        "danger": "Wrong regex can misroute all requests. Test with /bifrost preview <prompt> before committing.",
        "atomic": true
      },
      "items": {
        "$ref": "#/definitions/RouteRule"
      }
    },
    "classifier": {
      "$ref": "#/definitions/ClassifierConfig"
    },
    "thinking": {
      "$ref": "#/definitions/ThinkingConfig"
    },
    "cache": {
      "$ref": "#/definitions/CacheConfig"
    },
    "debug": {
      "$ref": "#/definitions/DebugConfig"
    },
    "reliability": {
      "$ref": "#/definitions/ReliabilityConfig"
    },
    "discovery": {
      "$ref": "#/definitions/DiscoveryConfig"
    },
    "quotaRouting": {
      "$ref": "#/definitions/QuotaRoutingConfig"
    }
  },
  "definitions": {
    "ThinkingConfig": {
      "type": "object",
      "description": "Prompt-derived thinking recommendations. Defaults to off; apply requires explicit opt-in.",
      "properties": {
        "mode": { "type": "string", "enum": ["off", "advisory", "apply"], "default": "off" },
        "defaultLevel": { "type": "string", "enum": ["off", "minimal", "low", "medium", "high", "xhigh", "max"], "default": "medium" },
        "maxLevel": { "type": "string", "enum": ["off", "minimal", "low", "medium", "high", "xhigh", "max"], "default": "high" },
        "byTier": {
          "type": "object",
          "additionalProperties": { "type": "string", "enum": ["off", "minimal", "low", "medium", "high", "xhigh", "max"] }
        }
      },
      "additionalProperties": false
    },
    "QuotaRoutingConfig": {
      "type": "object",
      "description": "Subscription-aware frontier weighting. Only meaningful when a tier uses the 'subscription_balance' strategy. Reads weekly quota telemetry for Codex and Antigravity, biases toward the subscription provider with more remaining allowance, and keeps paid OpenRouter candidates out of the running while any subscription allowance remains above reservePercent.",
      "additionalProperties": false,
      "properties": {
        "reservePercent": {
          "type": "number",
          "minimum": 0,
          "maximum": 1,
          "description": "Fraction of weekly allowance treated as exhausted (default 0.03). Paid OpenRouter candidates stay blocked while any subscription is above this.",
          "default": 0.03
        },
        "gamma": {
          "type": "number",
          "minimum": 0,
          "description": "Exponent shaping quota bias (default 3). Higher favors the provider with more remaining allowance harder.",
          "default": 3
        },
        "staleMinutes": {
          "type": "number",
          "minimum": 1,
          "description": "Snapshot older than this is treated as no data (minutes, default 15).",
          "default": 15
        },
        "refreshMinutes": {
          "type": "number",
          "minimum": 1,
          "description": "Minimum interval between background quota refreshes (minutes, default 30).",
          "default": 30
        },
        "providers": {
          "type": "object",
          "description": "Static per-provider quota overrides; pinned values always win over live telemetry. Keys are provider names.",
          "additionalProperties": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "weeklyRemainingFraction": { "type": "number", "minimum": 0, "maximum": 1 },
              "hoursToReset": { "type": "number", "minimum": 0 }
            }
          }
        }
      }
    },
    "DiscoveryConfig": {
      "type": "object",
      "description": "Bifrost-owned discovery metadata. Update commands use it to avoid changing manual model entries.",
      "required": ["managed"],
      "additionalProperties": false,
      "properties": {
        "managed": {
          "type": "object",
          "description": "Canonical provider/model keys created by discovery and their owning sources.",
          "additionalProperties": {
            "type": "array",
            "uniqueItems": true,
            "items": {
              "type": "string",
              "enum": ["scoped", "free"]
            }
          }
        }
      }
    },
    "RouteRule": {
      "type": "object",
      "required": ["pattern", "model"],
      "additionalProperties": false,
      "properties": {
        "pattern": {
          "type": "string",
          "description": "JavaScript regex pattern (without flags, double-escaped). Matched case-insensitively against the user's prompt. Example: '\\\\b(debug|fix|refactor)\\\\b'"
        },
        "model": {
          "type": "string",
          "description": "Model tier name OR a direct model reference (e.g. 'opencode-go/glm-5.1'). If the value contains '/', it's treated as an exact provider/id lookup and bypasses tier selection. Otherwise, it must match a key in the 'models' map."
        }
      }
    },
    "ClassifierConfig": {
      "type": "object",
      "description": "LLM-based classifier that inspects user prompts and selects a tier before falling back to regex rules. Set 'enabled: false' to use regex-only routing.",
      "required": ["model"],
      "additionalProperties": false,
      "properties": {
        "enabled": {
          "type": "boolean",
          "description": "Enable the LLM classifier. When disabled, only regex rules are used.",
          "default": true
        },
        "model": {
          "type": "string",
          "description": "Model pattern for the classifier. Use the cheapest model you trust — each classification costs tokens. Example: 'ollama/qwen2.5-coder:latest'"
        },
        "endpoint": {
          "type": "string",
          "description": "Optional base URL for the classifier when the model is not in pi's model registry. Example: 'http://127.0.0.1:1234/v1'. When set, only method='direct' works for that endpoint."
        },
        "method": {
          "type": "string",
          "enum": ["direct", "subprocess", "auto"],
          "description": "How to invoke the classifier. 'direct' uses a fast OpenAI-compatible HTTP call (needs compatible API). 'subprocess' spawns a fresh pi CLI (slower but supports any provider). 'auto' tries direct first, falls back to subprocess.",
          "default": "auto"
        },
        "maxTokens": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "description": "Max output tokens for the classifier. Keep it low — only need the category name back.",
          "default": 20
        },
        "temperature": {
          "type": "number",
          "minimum": 0,
          "maximum": 2,
          "description": "Temperature for classifier sampling. 0 for deterministic category selection.",
          "default": 0
        },
        "fallbackToRegex": {
          "type": "boolean",
          "description": "Fall back to regex rules when the classifier fails or returns an unknown category.",
          "default": true
        },
        "systemPrompt": {
          "type": "string",
          "description": "Custom system prompt for the classifier. Override to change how categories are presented to the LLM.",
          "default": "You are a routing classifier. Pick the category that best matches the task complexity and type. Respond with only the exact category name. No explanation, no punctuation."
        }
      }
    },
    "CacheConfig": {
      "type": "object",
      "description": "Fuzzy classification cache. Repeated or similar prompts reuse the cached category instead of calling the classifier again.",
      "additionalProperties": false,
      "properties": {
        "enabled": {
          "type": "boolean",
          "description": "Enable the classification cache.",
          "default": true
        },
        "maxEntries": {
          "type": "integer",
          "minimum": 1,
          "maximum": 10000,
          "description": "Maximum number of cached entries. Oldest entries (by last use) are evicted first.",
          "default": 500
        },
        "threshold": {
          "type": "number",
          "minimum": 0,
          "maximum": 1,
          "description": "Jaccard similarity threshold for fuzzy cache matches. 0 = any prompt matches. 1 = only exact token matches.",
          "default": 0.85
        },
        "path": {
          "type": "string",
          "description": "Custom path for the cache file. Absolute, ~-prefixed, or relative to project root. Default: .pi/bifrost-cache.jsonl"
        }
      }
    },
    "ReliabilityConfig": {
      "type": "object",
      "description": "Health-aware model selection and circuit breaker. Tracks repeated probe/runtime failures, opens circuits for unhealthy models, and skips them before routing.",
      "additionalProperties": false,
      "properties": {
        "enabled": {
          "type": "boolean",
          "description": "Enable circuit-breaker based model health filtering.",
          "default": true
        },
        "failureThreshold": {
          "type": "integer",
          "minimum": 1,
          "description": "Open model circuit after this many failures within windowMinutes.",
          "default": 3
        },
        "windowMinutes": {
          "type": "integer",
          "minimum": 1,
          "description": "Rolling failure window, in minutes.",
          "default": 5
        },
        "cooldownMinutes": {
          "type": "integer",
          "minimum": 1,
          "description": "How long an opened circuit stays open before model becomes eligible again.",
          "default": 60
        },
        "path": {
          "type": "string",
          "description": "Custom path for persisted reliability state. Default: .pi/bifrost-reliability.json"
        }
      }
    },
    "DebugConfig": {
      "type": "object",
      "description": "Structured debug logging. Appends JSONL lines to a file for performance analysis and troubleshooting. Disabled by default.",
      "additionalProperties": false,
      "properties": {
        "enabled": {
          "type": "boolean",
          "description": "Enable debug logging.",
          "default": false
        },
        "path": {
          "type": "string",
          "description": "Custom path for the debug log file. Default: .pi/bifrost-debug.jsonl"
        }
      }
    }
  }
}
