diff --git a/README.md b/README.md index 30a24da..0112a1d 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ prefix is sugar for the common cases: |---------------------------------|---------------------------------------------------------| | `policy_ir` term in the body | run that Σ_pol policy (the primary path) | | `""` / unprefixed `model` | the `default` policy | -| `model = "profile:NAME"` | a named profile from the catalog (only `default` ships) | +| `model = "profile:NAME"` | a named profile from the catalog (`default` and `agent` ship) | | `model = "family:FAMILY"` | default, pinned to a model family | | `model = "pin:PROVIDER/FAMILY"` | default, pinned to one (provider, family) | @@ -135,7 +135,7 @@ curl -s http://127.0.0.1:8080/x/policy/templates/cheapest-family \ }' ``` -The three templates are: +The four templates are: - `cheapest-family` — stay in one exact family and minimize expected token cost; `provider_strategy: "ordered"` instead enforces Codex → AntSeed → @@ -143,6 +143,13 @@ The three templates are: breaker-open routes only as final fallbacks. - `smart-value` — minimize cost among the current top five intelligence models (the shortlist size and price/reliability rails are configurable). +- `agent` — the reusable `profile:agent` policy for autonomous tool users: + require tools, 128k context, top-ten measured intelligence and reliable, + priced routes; prefer healthy Codex/direct providers before gateways and + trusted AntSeed peers; cap the cascade at eight candidates. The policy also + sets a 10s first-token and 22s per-attempt timeout and moves immediately to a + different candidate on provider failures, so a stalled first route cannot + consume the complete request deadline. - `default` — the actual policy used by OpenAI-compatible callers that send no policy: prefer a top-five intelligence model, then enforce Codex → AntSeed → Bedrock → OpenRouter and use a 75% cost / 25% intelligence value score inside diff --git a/SKILL.md b/SKILL.md index f8d15e1..2010bac 100644 --- a/SKILL.md +++ b/SKILL.md @@ -49,10 +49,14 @@ Every step is a request with the **same** `Authorization: Bearer ` you used lists the blessed choices. Compile one with `POST /x/policy/templates/{id}` (for example `{"family":"glm-5.2","provider_strategy":"ordered"}` against - `cheapest-family`). Author raw `policy_ir` only when the templates cannot + `cheapest-family`, or `agent` for a tool-running autonomous client). Author + raw `policy_ir` only when the templates cannot express the intent. The published `default` template is byte-for-byte equivalent after normalization to the policy used when an OpenAI-compatible request sends no `policy_ir`. + The published `agent` template is likewise identical to `profile:agent` and + owns its first-token/per-attempt budgets, trust gate, and fast-fallback plan; + callers should select the profile instead of copying its raw term. 2. **Admit & identify — no spend.** `POST /x/policy/normalize` `{policy_ir}` → `{policy_ir, fingerprint, version}`. A `400` here pinpoints what's invalid (unknown op, undeclared field, …) so you fix the term before paying. 3. **Preview the ranking — no spend.** `POST /x/rank` `{policy_ir}` → `{ranked, rejected}`: the candidates this host would admit and how it orders them, plus the ones it filtered out, each with the `reason` it failed. This is how you see *what your policy does* without a single call. 4. **Run it for real.** `POST /v1/chat/completions` with `policy_ir` (or `flow_ir`) + `messages` (the example above). A real call — real spend. diff --git a/auth_proxy.py b/auth_proxy.py index e72e0a0..2c3289e 100644 --- a/auth_proxy.py +++ b/auth_proxy.py @@ -34,6 +34,12 @@ UPSTREAM = os.getenv("ROUTER_UPSTREAM", "http://router:18080").rstrip("/") CALLER_KEYS_JSON = os.getenv("CALLER_KEYS_JSON", "{}") CALLER_KEYS_SHA256_JSON = os.getenv("CALLER_KEYS_SHA256_JSON", "{}") +# GitOps/bootstrap keys arrive from a reconciled workload Secret. Keep them in +# a separate channel because load_env_secrets() intentionally lets the +# dashboard-managed PVC override CALLER_KEYS_JSON; replacing the whole map +# would otherwise make a reconciled workload key disappear after the first +# dashboard-issued key is persisted. +CALLER_KEYS_BOOTSTRAP_JSON = os.getenv("CALLER_KEYS_BOOTSTRAP_JSON", "{}") RATE_PER_MIN = int(os.getenv("RATE_PER_MIN", "600")) BURST = int(os.getenv("BURST", "200")) RECENT_LIMIT = int(os.getenv("DASHBOARD_RECENT_LIMIT", "200")) @@ -79,8 +85,21 @@ def _load_caller_map(raw: str, name: str) -> Dict[str, str]: raise RuntimeError(f"invalid {name}: {exc}") from exc -CALLER_KEYS: Dict[str, str] = _load_caller_map(CALLER_KEYS_JSON, "CALLER_KEYS_JSON") -CALLER_KEY_HASHES: Dict[str, str] = _load_caller_map(CALLER_KEYS_SHA256_JSON, "CALLER_KEYS_SHA256_JSON") +def _bootstrap_caller_key_hashes(raw: str) -> Dict[str, str]: + """Load GitOps keys as hashes so the dashboard can never reveal them.""" + return { + hashlib.sha256(token.encode()).hexdigest(): owner + for token, owner in _load_caller_map( + raw, "CALLER_KEYS_BOOTSTRAP_JSON").items() + } + + +CALLER_KEYS: Dict[str, str] = _load_caller_map( + CALLER_KEYS_JSON, "CALLER_KEYS_JSON") +CALLER_KEY_HASHES: Dict[str, str] = _bootstrap_caller_key_hashes( + CALLER_KEYS_BOOTSTRAP_JSON) +CALLER_KEY_HASHES.update(_load_caller_map( + CALLER_KEYS_SHA256_JSON, "CALLER_KEYS_SHA256_JSON")) logging.basicConfig(level=os.getenv("LOG_LEVEL", "INFO"), format="%(message)s") log = logging.getLogger("llm-router-auth-proxy") @@ -4940,6 +4959,7 @@ def _dashboard_html() -> str: @@ -5215,7 +5235,7 @@ def _dashboard_html() -> str: async function bTest(){$('bError').style.display='none';$('bTestResult').innerHTML='
Running…
';try{const term=bCurrentTerm();const prompt=$('bTestPrompt').value.trim()||'Reply exactly: pong';const r=await fetch('/dashboard/api/policy/test',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json'},body:JSON.stringify({policy_ir:term,prompt})});if(r.status===401){showLogin();return}const d=await r.json();if(!r.ok)throw new Error(d.error?.message||(typeof d.error==='string'?d.error:'test '+r.status));const okc=d.ok?'':'bad';const out=d.text?`
${esc(d.text)}
`:`
${esc(d.error||'no output')}
`;$('bTestResult').innerHTML=`
status ${esc(d.status)}${esc(d.provider||'—')}${d.served_model_id?' · '+esc(d.served_model_id):''}
How it routed — executed live, real spend
${actDetail(d)}
Answer
${out}
Also recorded in Activity.
`;toast('Test call done')}catch(e){bFail(e.message)}} async function loadBuilderFamilies(){try{const r=await fetch('/dashboard/api/policies',{credentials:'same-origin'});if(!r.ok)return;const d=await r.json();const fams=new Set();(d.profiles||[]).forEach(p=>(p.models||[]).forEach(m=>{if(m.name)fams.add(m.name)}));$('familyOptions').innerHTML=[...fams].sort().map(f=>`