the stack · a field guide

Steering + guardrails. One mental model.

The same ideas behind TokenOps MCP — exact cost, graduated budget guard, cheapest model — plug into any agentic framework with four OSS packages. TypeScript or Python.

the 4 layers

It's never one package. It's four, doing one job each.

tokenops-mcpnpm · @finoptix/tokenops-mcp

ledger · budgets · local agents

The product you already saw: exact cost ledger (SQLite), 4-state budget guard and route_llm for your local agents — OpenCode, Claude Code, Cursor, Windsurf.

track_usageget_budgetset_budgetroute_llmsession_reportrun_audit
styrrnpm · @carloscortezcloud/styrr-llm

steering · which model

Model routing brain. Picks the cheapest / most reliable model for a task, falls back when one is down or rate-limited. The engine behind route_llm.

route_llm
sayaynpm · @carloscortezcloud/sayay-guard

guard · enforcement

The graduated circuit breaker: allow → warn (80%) → degrade (95%) → block (100%). Runs in-process, before and after every LLM call. No proxy, zero extra hops.

set_budget (enforced)get_budget
qhawaynpm · @carloscortezcloud/qhaway

observability · audit

Trace wrapper that captures latency, tokens and exact cost per call, user and session. Feeds Grafana / OTel / MLflow. The dashboard side of run_audit.

session_reportrun_audit
same mental model as tokenops mcp
tokenops mcp toolcloud component
route_llmstyrr — which model is cheapest for this task
set_budget + guard (warn→degrade→block)sayay — enforcement at the call site
track_usage · get_budget · session_reportqhaway spans + sayay storage
run_auditqhaway dashboards · OTel / MLflow / Grafana exporters

tokenops-mcp = local, single-user (your machine, ~/.tokenops/usage.db). sayay + styrr + qhaway = in the process of your deployed, multi-tenant agents.

the pattern

Styrr decides. Sayay enforces. Qhaway watches.

Every framework reduces to the same shape: a routing layer that picks the model, a guard that checks before and records after, and a trace wrapper. Here it is, generic:

generic · ts
// 1 · decide — styrr (steering)
const router = new StyrRouter({ models, apiKey })

// 2 · enforce — sayay (guard)
const guard = new SayayGuard({ storage, budget: { dailyUsd: 5 } })

// 3 · wire it together
async function call(userId, prompt) {
  const d = await guard.check(userId, estimateCost(prompt))
  // → allow | warn | degrade | block
  if (d.action === 'block') throw new Error(d.reason)
  const res = await router.prompt(
    prompt,
    d.action === 'degrade' ? { models: [d.suggestedModel] } : undefined,
  )
  await guard.record(userId, costFromUsage(res.usage))
  // → exact tokens from the response, never an estimate
  return res
}

// 4 · observe — qhaway
const traced = trace.wrap(call) // latency, tokens, cost, user, session

framework recipes

Strands, AgentCore, LangChain, Vertex, Azure... same recipe.

Strands AgentsTS
  • steeringstyrr-strands → StyrModelProvider (fallback chain as the model)
  • guardsayay-guard → SayayPlugin (Before/AfterModelCall lifecycle)
  • storageDynamoDB / S3
  • obsqhaway wrap
Strands AgentsPY
  • steeringpip install styrr → StyrRouter as the model provider
  • guardsayay (PyPI) — check antes, record después
  • storageD1 / Postgres / Redis
  • obsqhaway (PyPI) — spans + exporters
LangChainTS · PY
  • steeringstyrr-langchain → StyrLLM (custom LLM / callback)
  • guardsayay-langchain → SayayCallbackHandler (on_llm_start / on_llm_end)
  • storagecualquiera — Redis, D1, Postgres
  • obsqhaway callback → spans por tool/chain
AWS Bedrock AgentCoreTS
  • guardmiddleware en el harness → guard.check/record + DynamoDBStorage
  • steeringstyrr provider adapter bedrock (anthropic → llama fallback)
  • storageDynamoDB (TTL daily)
  • obsAgentCore ya manda OTel/CloudWatch; qhaway opcional
Google Vertex / GeminiTS · PY
  • guardwrapper de generateContent — usage viene en usageMetadata
  • storageFirestoreStorage / Redis
  • steeringstyrr adapter vertex
  • obsqhaway wrap
Azure FoundryTS · PY
  • guardmismo patrón wrapper — responses traen usage
  • storageAzure Table / Redis
  • steeringstyrr adapter azure
  • obsqhaway wrap
Cloudflare Workers / Agents SDKTS
  • guardhooks beforeModelCall / afterModelCall + KVStorage / D1Storage
  • techoCF AI Gateway spend limits = hard cap global (429)
  • obsqhaway con D1Storage
  • storageKV (TTL) o D1
Cualquier frameworkany
  • patrónANTES → guard.check(userId, estCost) · DESPUÉS → guard.record(userId, realCost)
  • steeringstyrr para elegir el modelo (o el propio route_llm del MCP)
  • storagepluggable — KV, D1, Redis, DynamoDB, Firestore
  • obsqhaway.wrap(call) y listo

the one that ties it — strands agents, ts, in one block:

import { Agent } from '@strands-agents/sdk'
import { StyrModelProvider } from '@carloscortezcloud/styrr-strands'
import { SayayPlugin } from '@carloscortezcloud/sayay-guard'

const agent = new Agent({
  model: new StyrModelProvider({               // steering
    models: [
      'anthropic.claude-opus-4-7',
      'meta-llama/llama-3.3-70b-instruct:free',
    ],
    apiKey: process.env.OPENROUTER_API_KEY,
  }),
  plugins: [new SayayPlugin({                    // guard
    budget: { dailyUsd: 5.0 },
    userId: 'user-123', onExceeded: 'degrade',
    degradeModel: 'meta-llama/llama-3.3-70b-instruct:free',
  })],
  systemPrompt: 'You are a helpful assistant.',
})

ts vs python

Same packages, two ecosystems.

concerntypescriptpython
steering (routing)@carloscortezcloud/styrr-llmstyrr (PyPI)
strands adapter@carloscortezcloud/styrr-strandsStyrRouter como model
langchain adapter@carloscortezcloud/styrr-langchainstyrr.langchain.StyrLLM
budget guard@carloscortezcloud/sayay-guardsayay (PyPI)
observabilidad@carloscortezcloud/qhawayqhaway (PyPI)

¿styrr, sayay o qhaway? No es "o" — es "y", según el caso:

routing / elegir modelo → styrr · enforcement de budget → sayay · tracing / audit / dashboards → qhaway

en python: pip install styrr + sayay, y qhawaycuando el caso es observabilidad. El guard y el router son ortogonales — se combinan, no compiten.

Start with the ledger. Add the guard when it hurts.