AI Agent Cost Guardrails
Budget enforcement middleware for LLM calls. Prevent runaway AI costs.
Quick Start · Actions · Storage · Ecosystem
Sayay (Quechua: "to stop/detain") stops your AI costs from running away. Set per-user daily/monthly budgets or credit systems. Before every LLM call, Sayay decides: allow, warn, degrade, or block.
import{SayayGuard,MemoryStorage}from'sayay';constguard=newSayayGuard({storage: newMemoryStorage(),budget: {dailyUsd: 5.00,monthlyUsd: 50.00},onExceeded: 'block',degradeToModel: 'meta-llama/llama-3.3-70b-instruct:free',});// Before LLM call:constdecision=awaitguard.check('user-123',0.003);if(decision.action==='block'){thrownewError(`Budget exceeded: ${decision.reason}`);}if(decision.action==='degrade'){// Use decision.suggestedModel instead of expensive model}// After LLM call:awaitguard.record('user-123',0.0025);npm install github:breakingthecloud/sayayimport{SayayGuard,MemoryStorage}from'sayay';constguard=newSayayGuard({storage: newMemoryStorage(),budget: {dailyUsd: 5.00,monthlyUsd: 50.00},onExceeded: 'block',degradeToModel: 'meta-llama/llama-3.3-70b-instruct:free',});constdecision=awaitguard.check('user-123',0.003);console.log(decision.action);// 'allow' | 'warn' | 'degrade' | 'block'| Action | What happens |
|---|---|
allow | Call proceeds normally |
warn | Call proceeds, but threshold reached (log it) |
degrade | Call proceeds with cheaper model (decision.suggestedModel) |
block | Call rejected, return error to user |
0%────────80%──────95%──────100%
allow │ warn │degrade│ block
Configurable via warnThreshold and degradeThreshold.
constguard=newSayayGuard({storage: newMemoryStorage(),budget: {credits: 50,creditsPerCall: 1},onExceeded: 'block',warnThreshold: 80,});awaitguard.record('user-123',0,1);constusage=awaitguard.getUsage('user-123');console.log(`Credits used: ${usage.credits}/50`);Sayay needs a storage backend to track usage. Built-in: MemoryStorage (testing) and DynamoStorage (DynamoDB, optional AWS dependency).
For production, implement SayayStorage:
// Cloudflare KV example:classKVStorageimplementsSayayStorage{constructor(privatekv: KVNamespace){}asyncget(key: string){returnparseFloat(awaitthis.kv.get(key)||'0');}asyncincrement(key: string,amount: number,ttl?: number){constcurrent=awaitthis.get(key);constnewVal=current+amount;awaitthis.kv.put(key,String(newVal),ttl ? {expirationTtl: ttl} : undefined);returnnewVal;}asyncreset(key: string){awaitthis.kv.delete(key);}}Real-time token/cost ledger per customer/session in DynamoDB — survives Lambda warm
starts and is the "Sayay = Cost Guardrail in Lambda + DynamoDB" pattern. Requires
@aws-sdk/lib-dynamodb + @aws-sdk/client-dynamodb (lazy-imported, so the package
keeps zero hard dependencies).
import{SayayGuard,DynamoStorage}from'@carloscortezcloud/sayay-guard';// Table: partition key `pk` (S), attribute `value` (N), TTL on `ttl` (N)constguard=newSayayGuard({storage: newDynamoStorage({tableName: 'sayay-ledger',region: 'us-east-1'}),budget: {dailyUsd: 10},});Use checkOrThrow() to raise a native exception when the budget is exhausted.
In AWS Step Functions, matching ErrorEquals: ["TokenBudgetExceededException"]
in a Catch block instantly jumps to the error handler — stopping the workflow
before retries rack up more cost.
import{SayayGuard,MemoryStorage}from'@carloscortezcloud/sayay-guard';constguard=newSayayGuard({storage: newMemoryStorage(),budget: {dailyUsd: 10}});// Throws TokenBudgetExceededException on block; returns decision otherwiseconstdecision=awaitguard.checkOrThrow('user-42',0.005);TokenBudgetExceededException extends BudgetExceededError, so existing
instanceof BudgetExceededError checks keep working (backward compatible).
Pass cloudWatch in config to emit a metric per decision. Requires
@aws-sdk/client-cloudwatch (lazy-imported). Emits Decision, RemainingBudget,
and UsagePercent metrics under the Sayay namespace (configurable).
constguard=newSayayGuard({
storage,budget: {dailyUsd: 10},cloudWatch: {metricNamespace: 'MyApp',region: 'us-east-1'},});Pipe every budget decision (allow/warn/degrade/block) into Qhaway
as a sayay.check span. The Qhaway metrics pipeline then exposes
qhaway_sayay_decisions_total{action,user} and the Grafana "Budget Guardrails"
panel visualizes blocks vs degradations vs allows.
import{SayayGuard,MemoryStorage,SayayQhawayPlugin}from'@carloscortezcloud/sayay-guard';import{MemoryStorageasQhawayStorage}from'@carloscortezcloud/qhaway';constqhawayStorage=newQhawayStorage();constqhaway=newSayayQhawayPlugin({storage: qhawayStorage,agentId: 'finops-agent',sessionId: 'session-demo-001',budgetKey: 'team-budget',});constguard=newSayayGuard({storage: newMemoryStorage(),budget: {dailyUsd: 10},onDecision: qhaway.onDecision,});onDecision writes a span with tool_name: "sayay.check", model: "budget-guard",
cost_usd: 0, and metadata: { action, budgetKey, spent, limit, usagePercent, suggestedModel }.
A failed write only logs — observability never breaks the guard path.
Works with any Tinkuy agent via its guard option (see
examples/README.md for the run guide and
examples/sayay-qhaway.ts for the full wiring):
constagent=newAgent({ router, guard,tools: [...],systemPrompt: '...'});import{StyrRouter}from'styrr';import{SayayGuard,MemoryStorage}from'sayay';constguard=newSayayGuard({storage: newMemoryStorage(),budget: {dailyUsd: 10}});constrouter=newStyrRouter({apiKey: '...',models: [...]});asyncfunctionsafeLLMCall(userId: string,prompt: string){constdecision=awaitguard.check(userId,0.005);if(decision.action==='block')thrownewError(decision.reason);constresult=awaitrouter.prompt(prompt);awaitguard.record(userId,result.usage?.totalTokens||0.003);returnresult;}| Package | Role | npm |
|---|---|---|
| Sayay | Cost guardrails (this) | GitHub |
| Styrr | LLM router | styrr |
| Tinkuy | Agent framework | @carloscortezcloud/tinkuy-agent |
| Qhaway | Agent observability | @carloscortezcloud/qhaway |
| TideRAG | Edge RAG pipeline | @carloscortezcloud/tiderag |
Apache 2.0 — see LICENSE.
Built by engineers who got tired of surprise AWS bills.
Tinkuy Labs · finoptix.dev
Your AI costs should have a stop button. Sayay is that button.