Configuration files
These are the packaged defaults exactly as shipped in costhelm v0.1.0 — synced from the release, never retyped. Resolution order and override mechanics are covered in Configuration. The files are heavily annotated on purpose; the comments are part of the documentation.
# costhelm — routing policy.## v3 classified every prompt into TINY / LARGE / HUGE with a router LLM and# then looked the tier up in a hardcoded TIER_TO_ORDER dict. Three gaps this# file closes:## 1. `role` was logged and then ignored. A "decision" call and a "perception"# call with the same token count got the same model. Roles now carry tier# floors and ceilings, so the role changes the model.# 2. HUGE returned 503 with a note about an unimplemented summariser. It now# routes to whichever providers can actually hold the context.# 3. Provider order per tier was a fixed list. It can now be sorted by cost,# by quality, or by a tradeoff dial between them, using the prices and# quality scores in pricing.yaml.## Adding a role, a tier, or a provider preference is an edit here and nothing# else. No Python knows the name of any role in this file.
version: 1
# ── Tiers ───────────────────────────────────────────────────────────────────# `order` is the base failover ring. Rebuilt 2026-08 on the paid-only provider# set (ollama, gemini, openrouter, openai) when the free tiers (github, groq,# cerebras, nvidia) were retired; chat.py's TIER_TO_ORDER fallback mirrors# TINY and LARGE verbatim and the two must move together. `min_ctx` filters# candidates whose max_ctx cannot hold the estimate — that is what makes HUGE# routable instead of a 503.tiers: TINY: order: [openrouter, gemini, ollama] description: short, simple, factual LARGE: order: [gemini, openrouter, ollama] description: dense or long-context work HUGE: order: [gemini, ollama, openrouter, openai] min_ctx: 8000 description: > Above the 8k classifier boundary. v3 refused these outright; v4 keeps only providers whose max_ctx actually covers the prompt (Gemini's 1M window is the reason this tier is servable at all, with OpenAI's 922k as the paid backstop) and refuses with a context-specific error if none qualify.
# ── The cross-model capability ladder ────────────────────────────────────── # TINY/LARGE/HUGE above are the CLASSIFIER's ladder: they answer "how big is # this prompt", and their rings are v3's failover rings verbatim, so default # routing has not moved. What they were never able to answer is "how good does # this answer need to be", because a ring is a failover order and not a price # ladder — every rung of it could end up served by the same model. # # These three rungs are the price ladder. Each one names a DIFFERENT model, on # a different provider, and the rungs are ordered by the blended $/Mtok in # pricing.yaml. `strict: true` is what makes a rung a rung: without it # `order_for` appends every other configured provider as a last-resort tail, # and a CHEAP request could be answered by the frontier model. `objective: # order` pins the declared preference against the global cost/quality dial, # which would otherwise re-sort a one-model rung into a different model. # # The rungs were rebuilt 2026-08 on the paid-only provider set. The gemma4 # number below was MEASURED against this gateway on 2026-07-30 (identical # 3-sentence prompt, max_tokens 512, temperature 0, `reasoning: "off"`). # gpt-oss-120b, the model that served STANDARD from Groq, did NOT survive # the host move: OpenRouter's gpt-oss endpoint answers `reasoning: "off"` # with HTTP 400 "Reasoning is mandatory for this endpoint" (measured # 2026-08-02), so the rung runs on llama-3.3 instead. See pricing.yaml for # the per-model rows. CHEAP: order: [ollama, openrouter] strict: true objective: order description: > Local weights first (gemma4:31b, $0.00/Mtok, measured 39.6 s for a 379-token answer), then OpenRouter meta-llama/llama-3.3-70b-instruct ($0.13/$0.40 per Mtok) as the network failover. Nearly free in dollars, expensive in wall-clock on the local rung — which is the real tradeoff a cheap rung buys, and the reason latency belongs next to price. STANDARD: order: [openrouter, gemini] strict: true objective: order description: > OpenRouter meta-llama/llama-3.3-70b-instruct ($0.13/$0.40 per Mtok, verified live 2026-08-02) with Gemini gemini-3.1-flash-lite ($0.25/$1.50, measured 1.2 s) as the same-rung failover. Both are genuinely different models from the FRONTIER rung; openrouter also anchors CHEAP, so only a gemma4 answer makes the two rungs differ in practice. FRONTIER: order: [openai] strict: true objective: order description: > OpenAI gpt-5.6-terra ($2.00/$12.00 per Mtok), the dearest model this gateway reaches, alone on the rung now that the free frontier hosts (GitHub Models, Cerebras) are retired. Unlike its 8k-capped predecessors it takes 922k of input, but a prompt that size still belongs on HUGE — this ladder prices capability, the other one sizes context.
# The escalation ladder, cheapest first. Also defines tier ordering for the# role floors and ceilings below, so there is one source of "which tier is# bigger". Unchanged from v3's classifier vocabulary.ladder: [TINY, LARGE, HUGE]
# ── Named secondary ladders ─────────────────────────────────────────────────# `ladder` above stays the classifier's. A named ladder orders a different set# of tiers, and rank / next_tier / prev_tier resolve within whichever ladder a# tier belongs to — so escalation on the capability ladder walks CHEAP -># STANDARD -> FRONTIER and budget pressure walks it back down, while the size# ladder keeps working exactly as it did. A tier may sit on only one ladder.ladders: capability: [CHEAP, STANDARD, FRONTIER]
# ── Benched providers ───────────────────────────────────────────────────────# Dropped from every tier, including the non-strict last-resort tail, and# reported as a per-candidate rejection so the refusal names the reason. This is# how a broken endpoint is taken out of rotation without a Python edit; delete# the line to bring it back.unavailable: {} # RESOLVED 2026-07-30. nvidia was benched here, and the entry is worth reading # even though it is now empty, because the diagnosis is the lesson. # # The symptom: every nvidia call sat for the client's full 180 s timeout and # failed with an EMPTY error string, three times out of three. That looked # like a dead provider. It was not. The deployment's NVIDIA_MODEL selected # deepseek-ai/deepseek-v4-pro, which accepts the connection and never answers; # nvidia/llama-3.1-nemotron-nano-8b-v1 behaved identically. The endpoint and # the key were fine the whole time — meta/llama-3.1-8b-instruct on the same # key answered in 1.37 s. # # So it was a bad model pin, not a dead provider, and the fix was one # NVIDIA_MODEL edit. Re-measured after that edit: 1 205 ms, 227 chars, and # nvidia is back in every ring it belongs to. Bench by name here when an # endpoint is genuinely broken; delete the line to bring it back.
# ── Cooldowns ───────────────────────────────────────────────────────────────# Seconds a provider stays benched after each class of failure. Overrides the# defaults in costhelm/routes/chat.py by name.## `timeout` is the one that mattered: an httpx ReadTimeout stringifies to the# EMPTY string, so the old matcher recognised nothing and handed back a ZERO# cooldown — NVIDIA burned 180 s, was benched for no time at all, and was a# candidate again on the very next request. Ten minutes is deliberately much# longer than the 180 s it takes to notice.backoff: timeout: 600 queue: 15 rate_limited: 30 rpm: 60 rpd: 3600 upstream_5xx: 20 auth: 600
# ── Roles ───────────────────────────────────────────────────────────────────# The role a caller declares (`auto_route: <role>`) now shapes the tier:# default_tier used when no classifier ran# min_tier floor — clamp UP to at least this tier# max_tier ceiling — clamp DOWN to at most this tier# escalate may this role spend more to retry## Role names are free-form. Add one here and it works; the gateway validates# against this list and falls back to `default_role` for anything unknown.default_role: perception
roles: perception: # Cheap, high-volume observation. Never worth a frontier model, so it is # capped: a wrong tier classification cannot escalate spend here. default_tier: TINY max_tier: LARGE escalate: true memory: # Retrieval and summarisation of stored context. Long inputs are normal, # so no ceiling, but no floor either. default_tier: TINY escalate: true decision: # The step whose mistakes are expensive. Floored at LARGE: a TINY # classification is overridden, which is the single clearest demonstration # that role changes routing. min_tier: LARGE default_tier: LARGE escalate: true # Example of adding a role with no code change: a cheap classifier role that # must never leave the smallest tier and must never escalate. triage: default_tier: TINY max_tier: TINY escalate: false
# ── Roles on the capability ladder ──────────────────────────────────────── # A role's `default_tier` is what selects its ladder, so these three route # across MODELS rather than across prompt sizes. The classifier still runs and # its verdict is still honoured — carried across by position, so a TINY # classification becomes CHEAP — and the floor/ceiling then override it, which # is the whole demonstration: the role, not the prompt length, decides which # model answers. bulk: #: High-volume grunt work. Capped at the free rung: a wrong classification #: cannot spend money here, and it may not escalate off it. default_tier: CHEAP max_tier: CHEAP escalate: false worker: #: The default working rung. May escalate one rung to FRONTIER when the #: cheap answer fails a structural check. default_tier: STANDARD escalate: true adjudicator: #: The step whose mistakes are expensive. Floored at FRONTIER, so even a #: TINY classification is answered by the frontier MODEL. min_tier: FRONTIER default_tier: FRONTIER escalate: false
# ── Cost / quality selection ────────────────────────────────────────────────# Reorders the tier's candidate ring. Rates and quality scores are read from# pricing.yaml, so a price change moves routing without touching this file.## objective: order | cost | quality | cost_quality# order keep the tier's declared ring (v3 behaviour)# cost cheapest blended $/Mtok first# quality highest quality score first# cost_quality score = tradeoff*quality - (1-tradeoff)*normalised_cost## tradeoff: 0.0 = pure cost, 1.0 = pure quality.selection: objective: cost_quality tradeoff: 0.5 #: Assumed output:input ratio when blending the two rates into one number. output_weight: 0.35 #: Providers listed here are never reordered away from the front. Use for a #: local model you want tried first because it is free. pin_first: []
# ── Cascade escalation (FrugalGPT-style) ────────────────────────────────────# Try the cheap tier; escalate only when the cheap answer looks bad. The point# of the confidence gate is that escalating on *every* call is strictly worse# than starting at the expensive tier — you pay for both.escalation: enabled: true max_escalations: 1 #: Answers scoring below this escalate one rung up the ladder. confidence_threshold: 0.5 #: Which observations may trigger an escalation. triggers: [empty_response, provider_failure, low_confidence, schema_failure] #: Confidence is scored from structural signals only — no second LLM call, #: because paying for a judge to check a cheap answer often costs more than #: the expensive answer would have. confidence: empty_response: 0.0 schema_failure: 0.1 #: stop_reason == max_tokens. Scored just ABOVE the threshold on purpose: #: truncation almost always means the caller under-budgeted max_tokens, and #: a bigger model will hit the same cap. Escalating here would pay twice for #: the same truncated answer. truncated_response: 0.55 #: Shorter than this many characters counts as a non-answer. DISABLED (0) #: on purpose. It was set to 8 during the build and immediately caused the #: failure this session is about: "Paris" — a correct one-word answer to a #: one-word question — scored 0.3, escalated to a bigger model, and made #: cost-per-resolved-task worse while cost-per-call looked unchanged. #: Emptiness is evidence that an answer is bad. Brevity is not. Set this #: only for a task class whose answers are known to be long. min_response_chars: 0 short_response: 0.3 #: Case-insensitive substrings that mark a hedge or refusal. EMPTY by #: default: a marker list is a use-case-specific guess, and a wrong one #: escalates good answers. Populate it for your own task class. hedge_markers: [] hedge_penalty: 0.35 default: 1.0
# ── Budget interaction ──────────────────────────────────────────────────────# The budget controller is the authority on refusal; this is only a preference.# When `prefer_affordable` is on, a tier whose cheapest provider is projected# to breach the remaining allowance is deprioritised, so a run near its ceiling# drifts down the ladder rather than slamming into a 402.budget: prefer_affordable: true #: Refuse to escalate when the escalated tier would breach the allowance. block_escalation_on_breach: true
# ── Provider limits ─────────────────────────────────────────────────────────# Per-provider pacing and context caps, merged over the built-in defaults in# costhelm/routing/core.py at boot. Partial entries are allowed: raising one# provider's rpm does not require restating the rest. The values below ARE the# built-in defaults, restated as data so the numbers are editable without a# code change (as of 2026-08: deliberate under-estimates of paid tiers — too# low only throttles locally, too high buys 429s).limits: ollama: {rpm: 9999, rpd: 9999999, tpm: 99999999, cooldown: 0, max_ctx: 32000} cerebras: {rpm: 30, rpd: 9999, tpm: 60000, cooldown: 2, max_ctx: 8000, tokens_per_day: 1000000} groq: {rpm: 30, rpd: 1000, tpm: 6000, cooldown: 2, max_ctx: 100000} nvidia: {rpm: 40, rpd: 9999, tpm: 100000, cooldown: 2, max_ctx: 100000} gemini: {rpm: 150, rpd: 10000, tpm: 1000000, cooldown: 1, max_ctx: 1000000} openrouter: {rpm: 60, rpd: 5000, tpm: 500000, cooldown: 1, max_ctx: 100000} github: {rpm: 10, rpd: 50, tpm: 99999999, cooldown: 6, max_ctx: 8000} # max_ctx is checked against the *prompt* estimate, so it is the model's # maximum input, not its full context window. openai: {rpm: 60, rpd: 9999, tpm: 200000, cooldown: 1, max_ctx: 922000}
# ── Agent pins ──────────────────────────────────────────────────────────────# Map an agent name (the `agent` field on /v1/chat) to a preferred provider.# A pin is a preference, not a hard binding: an explicit per-call `provider`# still wins, and a pin to an unregistered provider is ignored. Ships empty.## planner: gemini# bulk_classifier: ollamaagent_pins: {}# costhelm — per-MODEL pricing.## v3 priced per *provider*, which is wrong the moment a provider serves more# than one model (every provider here does). This table is keyed by model, with# the provider table kept only as a last-resort fallback so an unpriced model# reports exactly what v3 reported for it.## Everything in this file is data. Adding a model is a new entry here and no# Python edit anywhere.## Resolution order for (provider, model):# 1. models[<model>] exact string# 2. models[<model with vendor/ prefix stripped>]# 3. first matching patterns[] glob, in file order# 4. providers[<provider base name>] v3 behaviour# 5. unpriced_default (0/0 — never invent a price)## Units: USD per 1,000,000 tokens.# Multipliers are applied to the *input* rate for cached tokens, and to both# rates for batch. They are data because the mechanics differ per vendor:# Anthropic charges 1.25x/2.0x to write a cache and 0.1x to read it; OpenAI's# implicit cache is a free write and a 0.1x read; Gemini explicit caching is a# 0.25x read plus a storage fee this table does not model.
version: 1
# Last full verification sweep: 2026-08-02 (rates cross-checked against provider# pricing pages; rows marked MEASURED were measured against this gateway).# CI runs scripts/check_pricing.py so a configured default model can never# silently fall to unpriced_default.
defaults: currency: USD unit: usd_per_mtok cache_read_multiplier: 1.0 # no discount unless the model declares one cache_write_multiplier: 1.0 batch_multiplier: 1.0
unpriced_default: input: 0.0 output: 0.0
# `quality` is a ROUTING WEIGHT, not a benchmark score: it only ever gets# compared with the other weights in this file to break a cost tie. The rungs of# the capability ladder in routing.yaml are ordered so their weights increase# with their blended price, because a rung that costs more and scores lower would# never be chosen and the ladder would collapse to one model.## Rows marked MEASURED carry numbers taken from this gateway on 2026-07-30: one# identical 3-sentence prompt, max_tokens 512, temperature 0, `reasoning: "off"`.# `measured_*` keys are documentation — the loader reads input/output/quality and# the multipliers, and ignores everything else — so a re-measurement is a config# edit with no code consequence.
# ── Models actually reachable from this gateway's default .env ───────────────models: # Google AI Studio. Free under the course's AI-Studio quota; list rates are # recorded so a paid deployment reports real dollars. gemini-3.1-flash-lite: provider: gemini #: STANDARD rung, second in the ring. MEASURED: 31 in / 89 out, #: $0.00014125, 1236 ms. input: 0.25 output: 1.50 cache_read_multiplier: 0.25 cache_write_multiplier: 1.0 batch_multiplier: 0.5 quality: 0.62 measured_latency_ms: 1236 measured_reference_usd: 0.00014125 measured_non_empty: true gemini-3.1-flash: provider: gemini input: 0.50 output: 3.00 cache_read_multiplier: 0.25 batch_multiplier: 0.5 quality: 0.70 gemini-3.1-pro: provider: gemini input: 2.00 output: 12.00 cache_read_multiplier: 0.25 batch_multiplier: 0.5 quality: 0.78 gemini-2.5-flash: provider: gemini input: 0.30 output: 2.50 cache_read_multiplier: 0.25 batch_multiplier: 0.5 quality: 0.58
# Groq openai/gpt-oss-120b: provider: groq #: STANDARD rung, first in the ring. MEASURED: 101 in / 117 out, #: $0.0001029, 744 ms. Thinks by default: the same call with the reasoning #: dial left alone spent all 128 output tokens in its reasoning channel and #: returned `content: ""` for $0.00011115 — a fully billed non-answer. input: 0.15 output: 0.75 cache_read_multiplier: 0.1 quality: 0.60 measured_latency_ms: 744 measured_reference_usd: 0.0001029 measured_non_empty: true measured_needs_reasoning_off: true llama-3.3-70b-versatile: provider: groq input: 0.59 output: 0.79 quality: 0.48
# Cerebras zai-glm-4.7: provider: cerebras #: FRONTIER rung, second in the ring. MEASURED: 35 in / 86 out, $0.0000605, #: 1033 ms. The worst offender for reasoning: with the dial left alone it #: burned all 512 output tokens thinking and returned `content: ""` for #: $0.0002735 — 4.5x the price of the answer, for no answer. input: 0.50 output: 0.50 quality: 0.61 measured_latency_ms: 1033 measured_reference_usd: 0.0000605 measured_non_empty: true measured_needs_reasoning_off: true llama3.1-8b: provider: cerebras input: 0.10 output: 0.10 quality: 0.30
# NVIDIA NIM — free build tier for the course. # # MEASURED 2026-07-30: deepseek-v4-pro and llama-3.1-nemotron-nano-8b-v1 both # accept the connection on this key and never answer — 180 s, then an empty # error. meta/llama-3.1-8b-instruct on the same key answered in 1.37 s. The # provider is benched in routing.yaml's `unavailable:` with that reason; the # rows stay here so the numbers come back correct if NVIDIA_MODEL is repointed. deepseek-ai/deepseek-v4-pro: provider: nvidia input: 0.0 output: 0.0 quality: 0.70 measured_non_empty: false measured_note: "no response in 180 s, 3/3 attempts" meta/llama-3.1-8b-instruct: provider: nvidia #: The one NVIDIA model measured working. MEASURED: 65 in / 114 out, #: 1365 ms, free on the build tier. input: 0.0 output: 0.0 quality: 0.30 measured_latency_ms: 1365 measured_non_empty: true deepseek-ai/deepseek-v3.2: provider: nvidia input: 0.0 output: 0.0 quality: 0.66 nvidia/llama-3.1-nemotron-nano-8b-v1: provider: nvidia input: 0.0 output: 0.0 quality: 0.28
# GitHub Models — free quota; the rates below are what the same models cost # on Azure OpenAI once the quota is gone. openai/gpt-4.1: provider: github #: FRONTIER rung, first in the ring, and the most expensive model this #: gateway can reach. MEASURED: 37 in / 76 out, $0.000682, 2883 ms. Answers #: without any reasoning dial. Context caps at 8k on this surface. input: 2.00 output: 8.00 cache_read_multiplier: 0.25 #: Raised from 0.55. At 0.55 the frontier rung scored BELOW gpt-oss-120b #: (0.60) at a quarter of its price, so any cost/quality objective demoted #: it out of its own rung and the ladder collapsed to one model. quality: 0.72 measured_latency_ms: 2883 measured_reference_usd: 0.000682 measured_non_empty: true openai/gpt-4.1-mini: provider: github input: 0.40 output: 1.60 cache_read_multiplier: 0.25 quality: 0.52
# OpenRouter. Paid credits as of 2026-08. # # The default and STANDARD rung. NOT gpt-oss-120b, which would have kept the # rung's model when Groq retired: OpenRouter's gpt-oss endpoint answers # `reasoning: "off"` with HTTP 400 "Reasoning is mandatory for this # endpoint" (measured 2026-08-02), and gpt-oss left thinking burns small # output budgets. Catalog rate 2026-08-02; quality mirrors the Groq-hosted # llama-3.3-70b-versatile row above. meta-llama/llama-3.3-70b-instruct: provider: openrouter input: 0.13 output: 0.40 quality: 0.48 measured_non_empty: true # # The former free-tier default, kept priced by name so old ledger rows and a # re-pinned OPENROUTER_MODEL still report exact dollars. MEASURED: 47 in / # 108 out, $0.00, 1849 ms. Thinks by default and leaks the scratchpad # straight into `content` when it runs out of tokens. nvidia/nemotron-3-super-120b-a12b:free: provider: openrouter input: 0.0 output: 0.0 quality: 0.38 measured_latency_ms: 1849 measured_reference_usd: 0.0 measured_non_empty: true measured_needs_reasoning_off: true # S15Code's judge_b. OpenRouter catalog rate 2026-08-02. The `z-ai/` prefix # does not suffix-resolve to the bare cerebras `zai-glm-4.7` row above, so # without this row the judges would bill at the openrouter provider floor. z-ai/glm-4.7: provider: openrouter input: 0.40 output: 1.75 quality: 0.61 measured_needs_reasoning_off: true
# Local via Ollama — never charged, but priced at 0 explicitly so the report # says "free because local" rather than "unpriced". # # CHEAP rung, first in the ring. MEASURED: 46 in / 379 out, $0.00, 39570 ms. # The dollar cost is genuinely zero and the wall-clock cost is 53x the frontier # rung's — the cheapest rung on a price ladder is not the cheapest rung on a # latency ladder, and a router that only reads prices cannot see that. gemma4:31b: provider: ollama input: 0.0 output: 0.0 quality: 0.42 measured_latency_ms: 39570 measured_reference_usd: 0.0 measured_non_empty: true microsoft/Phi-4-mini-instruct: provider: github input: 0.0 output: 0.0 quality: 0.26
# ── Reference frontier rates ────────────────────────────────────────────────# The Anthropic rows are still reference-only. The three OpenAI rows are NOT:# `openai` is a real provider here now, so these are the rates the ledger bills.## gpt-5.6 prices are as of the 2026-07-30 cut, which took Luna down 80% and# Terra down 20%; Sol was not cut. Two wrong numbers are in wide circulation and# both will pass review unnoticed — the launch prices (Luna 1.00/6.00, Terra# 2.50/15.00) that this file used to carry, and OpenRouter's 0.10/0.60 Luna,# which is a reseller promo rather than OpenAI list. Check the date before# "restoring" either. claude-haiku-4.5: provider: anthropic input: 1.00 output: 5.00 cache_read_multiplier: 0.1 cache_write_multiplier: 1.25 batch_multiplier: 0.5 quality: 0.50 claude-sonnet-5: provider: anthropic input: 2.00 output: 10.00 cache_read_multiplier: 0.1 cache_write_multiplier: 1.25 batch_multiplier: 0.5 quality: 0.57 claude-opus-5: provider: anthropic input: 5.00 output: 25.00 cache_read_multiplier: 0.1 cache_write_multiplier: 1.25 batch_multiplier: 0.5 quality: 0.61 gpt-5.6-luna: provider: openai input: 0.20 output: 1.20 cache_read_multiplier: 0.1 batch_multiplier: 0.5 quality: 0.50 # The frontier rung of S15Code's ladder, after GitHub Models started 410ing and # took openai/gpt-4.1 with it. gpt-5.6-terra: provider: openai input: 2.00 output: 12.00 cache_read_multiplier: 0.1 batch_multiplier: 0.5 quality: 0.55 gpt-5.6-sol: provider: openai input: 5.00 output: 30.00 cache_read_multiplier: 0.1 batch_multiplier: 0.5 quality: 0.59
# ── Glob fallbacks, matched in order ────────────────────────────────────────# A new point release (gemini-3.2-flash-lite) is priced sanely before anyone# gets round to adding it above.patterns: - match: "*:free" input: 0.0 output: 0.0 quality: 0.35 - match: "gemini-*-flash-lite*" input: 0.25 output: 1.50 cache_read_multiplier: 0.25 batch_multiplier: 0.5 quality: 0.60 - match: "gemini-*-flash*" input: 0.50 output: 3.00 cache_read_multiplier: 0.25 batch_multiplier: 0.5 quality: 0.68 - match: "gemini-*-pro*" input: 2.00 output: 12.00 cache_read_multiplier: 0.25 batch_multiplier: 0.5 quality: 0.76 - match: "claude-*" input: 2.00 output: 10.00 cache_read_multiplier: 0.1 cache_write_multiplier: 1.25 batch_multiplier: 0.5 quality: 0.57 - match: "*nemotron*" input: 0.0 output: 0.0 quality: 0.40 # Deliberately `gpt-5*` and not `gpt-*`: globs are matched against the FULL # model string (pricing.py), so `openai/gpt-4.1-mini` — GitHub's free default — # is out of reach either way, but a bare `gpt-oss-120b` would be caught by the # wider glob and billed 13x its real rate. Rates are Terra's, so an unpinned # gpt-5 model over-bills against Luna rather than under-billing against Sol; # pin it in `models:` above if you need exact dollars. - match: "gpt-5*" input: 2.00 output: 12.00 cache_read_multiplier: 0.1 batch_multiplier: 0.5 quality: 0.55
# ── v3 per-provider table, the backward-compatible floor ───────────────────# Reached only when nothing above matched. These are the numbers v3's# pricing.PRICING_USD_PER_MTOK carried, except where a provider moved to a# paid tier (openrouter) — a paid provider must never keep a $0 floor.providers: gemini: input: 0.0 output: 0.0 nvidia: input: 0.0 output: 0.0 groq: input: 0.15 output: 0.75 cerebras: input: 0.50 output: 0.50 # Raised from v3's 0.0/0.0 when openrouter moved from the free tier to paid # credits: a $0.00 floor lets an unrecognised PAID model through the budget # controller as free (see the openai note below). Genuinely free models still # price at zero first via the `*:free` glob above, so this floor only # over-bills unknown paid models — the safe direction. openrouter: input: 1.00 output: 4.00 github: input: 0.0 output: 0.0 # This row was never v3 history either. Every remaining $0 provider on this # floor bills zero because it genuinely is free at the tier we use; OpenAI # never is, and a $0.00 floor would let an unrecognised model through the # budget controller as if it cost nothing — the failure that makes metering # useless. openai: input: 2.00 output: 12.00 ollama: input: 0.0 output: 0.0# costhelm — spend budgets.## This is the spend kill-switch. Read the enforcement model before editing:## * A call is admitted only if EVERY policy matching its principal has room# for the call's *projected worst-case* cost. One breach refuses the call.# * Refusal is an HTTP 402 with a structured envelope. The gateway never# silently truncates, downgrades, or drops a request to fit a budget —# the caller is told, with numbers, and decides what to do.# * Spend is read back out of the `calls` ledger, so it survives restarts and# cannot drift from what was actually billed.# * Enforcement is code, not a prompt. TALE (arXiv 2026) named the reason:# "token elasticity" — models blow straight past a budget stated in their# context. A budget an agent can talk itself out of is not a budget.## Ships EMPTY. A fresh costhelm behaves exactly like v3: nothing is refused.# Add a policy below (or POST /v1/budget at runtime) to arm the controller.
version: 1
defaults: enabled: true # master switch for the whole controller period: day # minute | hour | day | month | lifetime currency: USD on_breach: refuse # `refuse` is the only hard behaviour; see below include_errors: true # a failed call still burned tokens, so it still counts #: unmatched principals are unlimited. Set a `principal: "tenant:*"` policy #: to make the default deny-ish for a whole dimension. unmatched: allow
projection: # How much a call *could* cost, computed before it runs. # max_tokens -> charge output at the caller's full max_tokens (worst case) # ratio -> charge output at output_ratio x estimated input tokens output_tokens: max_tokens output_ratio: 1.0 #: multiply the projection by this before comparing to the remaining #: allowance. >1 reserves headroom for provider token-count disagreement. safety_factor: 1.0 #: floor on the projection, so a $0-priced model still trips a call-count #: ceiling if one is set. 0 disables. min_usd: 0.0
# Per-principal ceilings. `principal` is "<dimension>:<value>"; dimensions are# tenant, project, user, agent, session. `*` is a glob over the value, so# "tenant:*" means "each tenant, separately" — not "all tenants pooled".## Most specific match wins per dimension: an exact value beats a glob. All# matched policies (one per dimension) are checked, and the tightest refusal# is reported.## Example — copy, uncomment, edit. No Python changes needed for any of it.## policies:# - principal: "tenant:*"# limit_usd: 100.0# period: month# - principal: "user:*"# limit_usd: 5.0# period: day# - principal: "agent:summarizer"# limit_usd: 0.25# period: hour# - principal: "session:*"# limit_usd: 0.50 # the denial-of-wallet circuit breaker# period: lifetimepolicies: []
# Optional: warn (in the response, in the span, in the log) before refusing.# Purely advisory — it changes no routing decision.alerts: warn_at_fraction: 0.8# costhelm — response / semantic cache.## The distinction that matters: prompt caching (costhelm/cache/gemini.py, plus the# implicit prefix caching every OpenAI-compatible provider does) discounts the# *input* tokens of a call that still happens. A semantic cache answers from a# previous response and the call does not happen at all — so the saving is 100%# of the tokens, not 90% of the input tokens.## The cost of that: matching is fuzzy. Two prompts 0.93 cosine apart can want# different answers, and near the threshold you serve a confidently wrong# response. That is why the threshold is high, configurable, and why the cache# ships opt-in rather than on.
version: 1
semantic: #: Master switch. When false the module is inert and never embeds anything. enabled: true
#: Whether requests are cached by default. Left false deliberately: a #: gateway that starts silently returning stored answers to every caller is #: a surprise, and the embedding call itself costs something. Callers opt in #: per request with `semantic_cache: true`; flip this to true to invert that. default_on: false
#: Cosine similarity required for a hit. The 2026 production range is #: 0.92-0.97; below ~0.92 wrong-answer collisions become common. threshold: 0.95
#: Entry lifetime. 0 = never expires. ttl_seconds: 3600
#: Evict oldest beyond this count. 0 = unbounded. max_entries: 5000
#: Similarity is only consulted among entries whose namespace matches #: exactly. These are the request fields that must be byte-identical before #: two prompts are even comparable — a cached answer from a different model, #: system prompt or tool set is not the same answer. namespace_fields: [model, system, tools, response_format, temperature]
#: Never serve a cached answer to a request that is not deterministic-ish. #: A high temperature means the caller asked for variety. max_temperature: 0.3
#: Requests carrying tool definitions are not cached: a tool call's value is #: in executing it now, against current state. skip_when_tools: true
#: Skip caching for streaming responses (nothing to replay from the ledger). skip_when_stream: true
#: Never store a generation the model did not finish (stop_reason max_tokens #: or error). A hit replaces the provider call outright, so one reply cut off #: mid-sentence would otherwise be served as the answer for the whole TTL. #: Thinking models hit this easily: reasoning tokens consume max_tokens #: without appearing in output_tokens, so a budget that looks generous can #: still return truncated — or entirely empty — text. An empty completion is #: refused regardless of this setting. skip_when_truncated: true
#: Cache scope. Which principal dimensions must match for a hit to be #: allowed. Empty list = the cache is shared across all callers. #: Set to e.g. [tenant] to stop one tenant's answers reaching another's. scope_dimensions: [tenant]
#: Embedding task type passed to the embedder. task_type: retrieval_queryEnvironment variables
Section titled “Environment variables”| Variable | Meaning |
|---|---|
COSTHELM_PORT / COSTHELM_HOST |
bind address (default 0.0.0.0:8111) |
COSTHELM_CONFIG_DIR |
user-config directory (default ~/.costhelm) |
COSTHELM_GATEWAY_DB |
ledger path (default <config dir>/gateway.sqlite) |
COSTHELM_PRICING_YAML etc. |
per-file path overrides (_BUDGETS_, _ROUTING_, _CACHE_) |
COSTHELM_ORDER |
logical provider failover order for non-routed calls |
COSTHELM_ROUTER_ORDER |
router-pool order for the tier classifier |
COSTHELM_API_KEYS |
comma-separated keys; setting it turns auth on for /v1/* |
OTEL_EXPORTER_OTLP_ENDPOINT |
OTLP/HTTP collector base URL |
COSTHELM_TRACE_UI |
trace UI base for dashboard deep links |
COSTHELM_OTEL_CAPTURE_CONTENT |
opt-in prompt/completion capture on spans |
| provider keys | OPENAI_API_KEY, GEMINI_API_KEY_1..16, OPEN_ROUTER_API_KEY, GROQ_API_KEY, CEREBRAS_API_KEY, NVIDIA_API_KEY, GITHUB_ACCESS_TOKEN, OLLAMA_MODEL + OLLAMA_URL |
| model overrides | <PROVIDER>_MODEL, ROUTER_<PROVIDER>_MODEL |