Cost Tracking & Pricing¶
AIForge automatically tracks the USD cost and token usage of every LLM and embedding call. Cost data is returned per-call, aggregated per-session, and exposed via the REST API — giving you complete visibility into spend without any extra instrumentation.
Table of Contents¶
- Why Cost Tracking?
- How It Works
- Per-Call Cost
- Session-Level Aggregation
- CostTracker API
- Properties
- Summary
- Reset
- Cost Warning Threshold
- REST API — GET /stats
- Built-in Pricing Table
- OpenAI Models
- Anthropic Models
- Voyage Embeddings
- Google Gemini Models
- Amazon Bedrock Models
- Mistral AI Models
- Ollama / Local Models
- Context Windows
- Custom Pricing Overrides
- Custom Context Windows
- Fuzzy Model Matching
- Cost Calculation Formula
- Prompt Caching — Cost Savings
- Request Cache — Cost Savings
- Rate Limiting — Budget Protection
- Batch Operations
- Evaluation Runs
- Cost Estimation (Monthly, Per Developer)
- Integration Examples
- Python SDK
- REST API (curl)
- Node.js
- Go
- Java
- Troubleshooting
- Configuration Reference
- See Also
Why Cost Tracking?¶
| Without tracking | With AIForge cost tracking |
|---|---|
| Discover surprise bills weeks later | See per-call cost in every ModuleResponse |
| No breakdown by feature or model | Aggregation by module and by model |
| No budget guardrails | cost_warn_threshold logs a WARNING for expensive calls |
| Manual token counting | Input/output tokens tracked automatically |
| No caching awareness | Cache hits return cost=0, and provider prompt-cache reads are billed at the discounted rate |
Cost tracking is enabled by default. Set AIFORGE_COST_TRACKING_ENABLED=false to disable.
How It Works¶
Module.run(text)
│
├── RateLimiter.acquire() ← block if RPM/TPM exceeded
│
├── RequestCache.get(key) ← return cached result (cost=0) if hit
│
├── Provider.complete(messages) ← actual LLM call
│ returns CompletionResult { text, input_tokens, output_tokens, model,
│ cache_read_tokens, cache_write_tokens }
│
├── CompletionResult.cost
│ → calculate_cost(model, input_tokens, output_tokens,
│ cache_read_tokens=..., cache_write_tokens=...)
│ looks up PRICING table → USD amount
│
├── CostTracker.record(module, model, tokens, cost)
│ accumulates session-level stats
│
└── return ModuleResponse { text, cost, input_tokens, output_tokens, model }
Every module (Summarizer, Classifier, DocQA, etc.) follows this exact
flow via the BaseModule._call_llm() / _acall_llm() methods. No manual
instrumentation is required.
Per-Call Cost¶
Every ModuleResponse includes cost and token fields:
from aiforge import Summarizer
s = Summarizer()
result = s.run("Long document text here...")
print(result.cost) # 0.00285 (USD)
print(result.input_tokens) # 1200
print(result.output_tokens) # 150
print(result.model) # "gpt-4o"
print(result.latency_ms) # 820.5
ModuleResponse cost fields:
| Field | Type | Description |
|---|---|---|
cost |
float |
USD cost of this call (0.0 for cache hits or unknown models) |
input_tokens |
int |
Tokens sent to the model |
output_tokens |
int |
Tokens received from the model |
model |
str |
Model name used for the call |
latency_ms |
float |
Wall-clock time in milliseconds |
trace_id |
str |
Trace ID (if tracing enabled) |
Session-Level Aggregation¶
The CostTracker singleton accumulates costs across all module calls in the
current process. It is thread-safe and requires no setup.
CostTracker API¶
from aiforge.infra.cost_tracker import CostTracker
# Get the process-level singleton
tracker = CostTracker.get_session()
| Method | Signature | Description |
|---|---|---|
get_session() |
@classmethod (warn_threshold=1.0) → CostTracker |
Return or create the process-level singleton |
reset_session() |
@classmethod () → None |
Destroy the singleton (next get_session() creates a new one) |
record() |
(*, module, model, input_tokens, output_tokens, cost) → None |
Record a completed LLM call (called automatically by modules) |
summary() |
() → dict |
Return a snapshot of all tracked metrics |
reset() |
() → None |
Reset all counters on the current instance |
Properties¶
All properties are thread-safe (acquire an internal lock before reading):
tracker = CostTracker.get_session()
tracker.total_cost # float — cumulative USD cost
tracker.total_tokens # int — cumulative input + output tokens
tracker.call_count # int — number of LLM/embedding calls
tracker.by_module # dict[str, float] — cost grouped by module name
tracker.by_model # dict[str, float] — cost grouped by model name
Summary¶
summary() returns all metrics in a single dict — ideal for logging,
dashboards, or API responses:
tracker.summary()
# {
# "total_cost_usd": 0.04215,
# "total_tokens": 8421,
# "call_count": 12,
# "by_module": {"Summarizer": 0.012, "DocQA": 0.025, "Classifier": 0.005},
# "by_model": {"gpt-4o": 0.040, "text-embedding-3-small": 0.002}
# }
Reset¶
# Reset counters on the current instance (keeps the singleton alive)
tracker.reset()
# OR fully destroy and recreate
CostTracker.reset_session()
tracker = CostTracker.get_session()
Cost Warning Threshold¶
When a single LLM call exceeds the configured threshold, a structured WARNING log is emitted:
# aiforge.yaml
cost_tracking_enabled: true
cost_warn_threshold: 1.00 # USD — warn when any single call exceeds $1
The warning includes full context for investigation:
{
"event": "High-cost LLM call",
"cost_usd": "$1.2500",
"threshold_usd": "$1.0000",
"module": "Summarizer",
"model": "gpt-4-turbo",
"input_tokens": 95000,
"output_tokens": 2000
}
The threshold is a warning only — calls are never blocked. Adjust the
threshold to match your budget: 0.10 for strict monitoring, 5.00 for
batch workloads.
REST API — GET /stats¶
The API server exposes session-level cost metrics at the /stats endpoint:
{
"total_cost_usd": 0.04215,
"total_tokens": 8421,
"call_count": 12,
"by_module": {
"Summarizer": 0.012,
"DocQA": 0.025,
"Classifier": 0.005
},
"by_model": {
"gpt-4o": 0.040,
"text-embedding-3-small": 0.002
}
}
This endpoint reads from CostTracker.get_session() — the same singleton
used by all modules. No authentication is required for /stats (it is a
public path by default).
Built-in Pricing Table¶
All prices are in USD per 1,000,000 tokens (1M tokens). The table is
defined in aiforge/core/pricing.py and kept as a best-effort snapshot.
Where a table has no cache columns, cached tokens use the default multipliers
in Prompt Caching. Ollama's mistral and
mistral-nemo entries are local models and cost nothing.
OpenAI Models¶
| Model | Input ($/1M) | Output ($/1M) | Context Window |
|---|---|---|---|
gpt-4o |
2.50 | 10.00 | 128,000 |
gpt-4o-mini |
0.15 | 0.60 | 128,000 |
gpt-4-turbo |
10.00 | 30.00 | 128,000 |
gpt-4.5 / gpt-4.5-preview |
75.00 | 150.00 | 128,000 |
gpt-3.5-turbo |
0.50 | 1.50 | 16,385 |
o1 |
15.00 | 60.00 | 200,000 |
o1-preview |
15.00 | 60.00 | 128,000 |
o1-mini |
3.00 | 12.00 | 128,000 |
o3 |
10.00 | 40.00 | 200,000 |
o3-mini |
1.10 | 4.40 | 200,000 |
text-embedding-3-small |
0.02 | — | — |
text-embedding-3-large |
0.13 | — | — |
text-embedding-ada-002 |
0.10 | — | — |
Anthropic Models¶
| Model | Input ($/1M) | Output ($/1M) | Context Window |
|---|---|---|---|
claude-fable-5-1 |
10.00 | 50.00 | 1,000,000 |
claude-opus-5-5 |
4.00 | 20.00 | 1,000,000 |
claude-opus-5 |
5.00 | 25.00 | 1,000,000 |
claude-opus-4-8 / 4-7 / 4-6 |
5.00 | 25.00 | 1,000,000 |
claude-sonnet-5-5 |
2.00 | 10.00 | 1,000,000 |
claude-sonnet-5 |
2.00 | 10.00 | 1,000,000 |
claude-sonnet-4-6 |
3.00 | 15.00 | 1,000,000 |
claude-haiku-5-5 |
0.10 | 0.50 | 1,000,000 |
claude-haiku-4-5 |
1.00 | 5.00 | 200,000 |
claude-haiku-5-5 is listed at its rate for prompts up to 100K tokens. Retired Claude 3.x
models stay in the table so historical cost reports still resolve.
Voyage Embeddings¶
Used with the Anthropic provider for embedding calls:
| Model | Input ($/1M) | Output ($/1M) |
|---|---|---|
voyage-3 |
0.06 | — |
voyage-3-lite |
0.02 | — |
voyage-large-2 |
0.12 | — |
voyage-code-2 |
0.12 | — |
Google Gemini Models¶
| Model | Input ($/1M) | Output ($/1M) | Cache read ($/1M) | Context Window |
|---|---|---|---|---|
gemini-4-argon |
2.00 | 10.00 | 0.10 | 1,048,576 |
gemini-3.8-flash |
0.75 | 3.75 | 0.075 | 1,048,576 |
gemini-3.6-flash |
0.75 | 3.75 | 0.075 | 1,048,576 |
gemini-3.5-flash-lite |
0.30 | 2.50 | 0.03 | 1,048,576 |
gemini-3.1-flash-lite |
0.25 | 1.50 | 0.025 | 1,048,576 |
gemini-3.1-pro-preview |
2.00 | 12.00 | 0.20 | 1,048,576 |
gemini-2.5-pro |
1.25 | 10.00 | 0.125 | 1,048,576 |
gemini-2.5-flash |
0.30 | 2.50 | 0.03 | 1,048,576 |
gemini-2.5-flash-lite |
0.10 | 0.40 | 0.01 | 1,048,576 |
gemini-embedding-2 |
0.20 | — | — | — |
Gemini Developer API, Standard tier. Pro models are listed at their rate for
prompts up to 200K tokens; longer prompts cost more. gemini-embedding-001
has no built-in price, so add it under custom_pricing if you use it.
Amazon Bedrock Models¶
| Model | Input ($/1M) | Output ($/1M) | Cache read ($/1M) | Cache write ($/1M) | Context Window |
|---|---|---|---|---|---|
global.anthropic.claude-fable-5-1 |
10.00 | 50.00 | 0.25 | 12.50 | 1,000,000 |
anthropic.claude-fable-5-1 |
11.00 | 55.00 | 0.275 | 13.75 | 1,000,000 |
global.anthropic.claude-opus-5-5 |
4.00 | 20.00 | 0.20 | 5.00 | 1,000,000 |
anthropic.claude-opus-5-5 |
4.40 | 22.00 | 0.22 | 5.50 | 1,000,000 |
global.anthropic.claude-sonnet-5-5 |
2.00 | 10.00 | 0.10 | 2.50 | 1,000,000 |
anthropic.claude-sonnet-5-5 |
2.20 | 11.00 | 0.11 | 2.75 | 1,000,000 |
global.anthropic.claude-haiku-5-5 |
0.10 | 0.50 | 0.01 | 0.125 | 1,000,000 |
anthropic.claude-haiku-5-5 |
0.11 | 0.55 | 0.011 | 0.1375 | 1,000,000 |
global.anthropic.claude-sonnet-4-6 |
3.00 | 15.00 | 0.30 | 3.75 | 1,000,000 |
anthropic.claude-sonnet-4-6 |
3.30 | 16.50 | 0.33 | 4.125 | 1,000,000 |
global.anthropic.claude-haiku-4-5 |
1.00 | 5.00 | 0.10 | 1.25 | 200,000 |
anthropic.claude-haiku-4-5 |
1.10 | 5.50 | 0.11 | 1.375 | 200,000 |
global.amazon.nova-2-lite-v1 |
0.30 | 2.50 | 0.075 | 0.00 | 1,000,000 |
amazon.nova-2-lite-v1 |
0.33 | 2.75 | 0.0825 | 0.00 | 1,000,000 |
amazon.nova-pro-v1 |
0.80 | 3.20 | 0.20 | 0.00 | 300,000 |
amazon.nova-lite-v1 |
0.06 | 0.24 | 0.015 | 0.00 | 300,000 |
amazon.nova-micro-v1 |
0.035 | 0.14 | 0.00875 | 0.00 | 128,000 |
amazon.titan-embed-text-v2 |
0.02 | — | — | — | — |
amazon.titan-embed-text-v1 |
0.10 | — | — | — | — |
US East (N. Virginia) on-demand prices. Keys are Bedrock model IDs without the
version suffix: us.anthropic.claude-haiku-4-5-20251001-v1:0 resolves to
anthropic.claude-haiku-4-5. Geographic inference profiles (us., eu.,
apac., …) share the in-region price, while global. profiles have their own,
cheaper entry. Amazon Nova does not charge for cache writes. Claude Haiku 5.5 is
listed at its rate for prompts up to 100K tokens.
Mistral AI Models¶
| Model | Input ($/1M) | Output ($/1M) | Cache read ($/1M) | Context Window |
|---|---|---|---|---|
mistral-large-4 |
0.68 | 2.09 | 0.07 | 1,000,000 |
mistral-large-2512 |
0.50 | 1.50 | 0.05 | 256,000 |
mistral-medium-3-5 |
1.50 | 7.50 | 0.15 | 256,000 |
mistral-small-2603 |
0.15 | 0.60 | 0.015 | 256,000 |
codestral-2508 |
0.30 | 0.90 | 0.03 | 128,000 |
mistral-embed |
0.10 | — | — | — |
mistral-embed-2312 |
0.10 | — | — | — |
codestral-embed |
0.15 | — | — | — |
codestral-embed-2505 |
0.15 | — | — | — |
Cached input is billed at 10% of the input price. mistral-large-4 is listed at
its preview sale price.
Ollama / Local Models¶
All local models are priced at $0.00 (no cost):
llama3, llama3.1, llama3.2, llama3.3, mistral, mistral-nemo, codellama, phi-3, phi-4, gemma2, qwen2.5, nomic-embed-text, mxbai-embed-large, all-minilm
Context Windows¶
Context windows determine the maximum token length per call. The Summarizer
uses this to decide between stuff (single-shot) and map_reduce (chunked)
strategies.
Default fallback for unknown models: 8,192 tokens.
See the pricing tables above for per-model context windows. Override with
custom_context_windows in your config.
Custom Pricing Overrides¶
Override built-in prices or add pricing for new/fine-tuned models:
# aiforge.yaml
custom_pricing:
gpt-4o: # override built-in price
input: 2.50
output: 10.00
my-finetuned-model: # add new model
input: 6.00
output: 18.00
claude-sonnet-5-5: # optional explicit prompt-cache prices
input: 2.00
output: 10.00
cache_read: 0.20
cache_write: 2.50
cache_read and cache_write are optional. When they are left out, the
default multipliers in Prompt Caching apply.
Overrides are applied at startup via apply_pricing_overrides() and take
precedence over the built-in PRICING table. The lookup order is:
_pricing_overrides(fromcustom_pricingconfig) — checked firstPRICING(built-in table) — fallback- Not found → cost =
0.0+ WARNING log
You can also apply overrides at runtime:
from aiforge.core.pricing import apply_pricing_overrides
apply_pricing_overrides({
"my-new-model": {"input": 3.00, "output": 12.00}
})
Custom Context Windows¶
Or at runtime:
from aiforge.core.pricing import apply_context_window_overrides
apply_context_window_overrides({"my-new-model": 64000})
Fuzzy Model Matching¶
When looking up pricing or context windows, AIForge uses fuzzy matching to handle date-suffixed model variants:
claude-3-7-sonnet-20250219 → strips "-20250219" → matches "claude-3-7-sonnet"
gpt-4o-2024-11-20 → strips "-20" → matches "gpt-4o-2024-11"
The algorithm: if the exact name isn't found, split on the last - and retry
with the prefix. This handles the common pattern of providers appending release
dates to model names.
Cost Calculation Formula¶
uncached = input_tokens − cache_read_tokens − cache_write_tokens
cost = (input_price × uncached
+ cache_read_price × cache_read_tokens
+ cache_write_price × cache_write_tokens
+ output_price × output_tokens) / 1,000,000
Without prompt caching both cache counts are zero, so this reduces to
input_price × input_tokens + output_price × output_tokens.
Example: A gpt-4o call with 2,000 input tokens and 500 output tokens:
$$ \text{cost} = \frac{2.50 \times 2{,}000}{1{,}000{,}000} + \frac{10.00 \times 500}{1{,}000{,}000} = 0.005 + 0.005 = \$0.01 $$
API function:
from aiforge.core.pricing import calculate_cost
cost = calculate_cost("gpt-4o", input_tokens=2000, output_tokens=500)
# 0.01
# With prompt caching: 1,800 of the 2,000 input tokens were cache reads
cost = calculate_cost("gpt-4o", 2000, 500, cache_read_tokens=1800)
# 0.00775 (2.50 × 200 + 1.25 × 1,800 + 10.00 × 500) / 1M
CompletionResult.cost applies this formula to a provider result directly.
Prompt Caching — Cost Savings¶
Providers can reuse a long, repeated prompt prefix (system prompt, tool
definitions, a document) across calls and bill those input tokens at a
discount. AIForge reports them on every CompletionResult:
| Field | Meaning |
|---|---|
cache_read_tokens |
Input tokens served from the provider's prompt cache |
cache_write_tokens |
Input tokens written to the cache on this call (Anthropic, Bedrock) |
cost |
USD cost with cache reads and writes priced at their own rates |
Default cache prices, as multiples of the model's input price:
| Provider | Cache read | Cache write |
|---|---|---|
| Anthropic (Claude) | 0.1× | 1.25× |
| OpenAI / Azure / others | 0.5× | 1.0× (no surcharge) |
A pricing entry with explicit cache_read / cache_write prices overrides
these multipliers; the built-in Gemini, Bedrock and Mistral entries all have
them (see the tables above).
OpenAI, Azure, Gemini and Mistral cache long prompts automatically. For
Anthropic and Bedrock, caching is controlled by prompt_cache (system by
default, or auto / off); see
Configuration and Providers → Prompt caching.
Example from a live run of claude-haiku-5-5 with a ~13.8K-token system
prompt sent twice:
| Call | Cache write | Cache read | Cost |
|---|---|---|---|
| 1st | 13,821 | 0 | $0.00173 |
| 2nd | 0 | 13,821 | $0.00014 |
Request Cache — Cost Savings¶
The request cache eliminates cost for repeated identical calls. Cache hits
return the stored result with cost=0.0, input_tokens=0, output_tokens=0.
# aiforge.yaml
cache_enabled: true
cache_ttl: 3600 # 1 hour
cache_backend: memory # or "redis" for shared caching
cache_max_entries: 1000 # LRU eviction for memory backend
Cache keys are SHA-256 hashes of: model + messages + temperature + max_tokens + module + provider. Two identical calls within the TTL window return the same result at zero cost.
Check cache effectiveness:
from aiforge.infra.cache import RequestCache
cache = RequestCache.get_instance()
print(cache.stats())
# {"enabled": true, "size": 42, "hits": 120, "misses": 38, "hit_rate": 0.76, "ttl_seconds": 3600}
A high hit rate directly translates to cost savings.
Rate Limiting — Budget Protection¶
Rate limiting prevents runaway costs by capping throughput:
When limits are hit, requests block (sleep) until capacity is available — they do not fail with an error. This provides automatic backpressure without losing work.
from aiforge.infra.rate_limiter import RateLimiter
# Update limits at runtime
RateLimiter.get_instance().update_limits(rpm=100, tpm=50000)
Combined with cost tracking, rate limiting ensures you stay within both API provider limits and your budget.
Batch Operations¶
BatchResult aggregates cost across multiple calls:
from aiforge import Classifier
c = Classifier(labels=["positive", "negative", "neutral"])
batch = c.run_batch(["Great product!", "Terrible service.", "It's okay."])
print(batch.total_cost) # 0.0045 (sum of all calls)
print(batch.total_tokens) # 450
print(len(batch.results)) # 3
print(len(batch.failed)) # 0
for r in batch.results:
print(f"{r.label}: ${r.cost:.4f}")
BatchResult fields:
| Field | Type | Description |
|---|---|---|
results |
list[ModuleResponse] |
Successful call results |
total_cost |
float |
Sum of all result.cost values |
total_tokens |
int |
Sum of all input + output tokens |
failed |
list[dict] |
Failed items with error details |
Evaluation Runs¶
aiforge.evals reports the cost of an evaluation run, read from
each case's ModuleResponse. LLM-judge scorers are counted separately, so you
can see what the judging itself costs:
from aiforge import Classifier
from aiforge.evals import evaluate, label_match
report = evaluate(
Classifier(labels=["bug", "feature", "question"]),
[{"input": "The app crashes on upload", "expected": "bug"}],
scorers=[label_match()],
)
print(report.total_cost) # USD spent by the module across all cases
print(report.scoring_cost) # USD spent by llm_judge scorers
print(report.total_tokens)
aiforge eval run prints the same totals, and the saved JSON report includes
them in its summary block. Module calls made during an eval are also recorded
in the session CostTracker.
Cost Estimation (Monthly, Per Developer)¶
| Component | Unit Cost | Estimated Dev Usage | Monthly Cost |
|---|---|---|---|
| OpenAI GPT-4o | $2.50/1M input, $10/1M output | ~2M input, ~500K output | ~$10.00 |
| OpenAI text-embedding-3-small | $0.02/1M tokens | ~5M tokens | ~$0.10 |
| Anthropic Claude Sonnet 5.5 | $2/1M input, $10/1M output | ~1M input, ~250K output | ~$4.50 |
| Ollama (local) | Free | Unlimited | $0.00 |
| ChromaDB | Free (local) | Unlimited | $0.00 |
| Total (OpenAI only) | ~$10.10/mo | ||
| Total (mixed: Ollama dev + cloud prod) | ~$5.00/mo |
Tip: Use Ollama during development (
provider: ollama) and switch to cloud providers for staging/production. All Ollama models are priced at $0.
Integration Examples¶
Python SDK¶
from aiforge import Summarizer, Classifier, DocQA
from aiforge.infra.cost_tracker import CostTracker
# Make some calls
s = Summarizer()
c = Classifier(labels=["bug", "feature", "question"])
qa = DocQA()
summary = s.run("Long document text...")
label = c.run("The app crashes on startup")
qa.ingest(["doc1.pdf", "doc2.md"])
answer = qa.ask("How does authentication work?")
# Check per-call costs
print(f"Summary cost: ${summary.cost:.4f}")
print(f"Classify cost: ${label.cost:.4f}")
print(f"QA cost: ${answer.cost:.4f}")
# Session-level totals
tracker = CostTracker.get_session()
metrics = tracker.summary()
print(f"Total spend: ${metrics['total_cost_usd']:.4f}")
print(f"Total tokens: {metrics['total_tokens']:,}")
print(f"Calls: {metrics['call_count']}")
print(f"By module: {metrics['by_module']}")
print(f"By model: {metrics['by_model']}")
REST API (curl)¶
# Make an API call — cost is in the response
curl -s http://localhost:8000/summarize \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $AIFORGE_API_KEY" \
-d '{"text": "Long document text..."}' | jq '{cost, input_tokens, output_tokens, model}'
# {
# "cost": 0.00285,
# "input_tokens": 1200,
# "output_tokens": 150,
# "model": "gpt-4o"
# }
# Check session-level stats
curl -s http://localhost:8000/stats | jq .
# {
# "total_cost_usd": 0.04215,
# "total_tokens": 8421,
# "call_count": 12,
# "by_module": { "Summarizer": 0.012, "DocQA": 0.025, "Classifier": 0.005 },
# "by_model": { "gpt-4o": 0.040, "text-embedding-3-small": 0.002 }
# }
Node.js¶
const BASE = "http://localhost:8000";
const HEADERS = {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.AIFORGE_API_KEY}`,
};
// Make a call and read cost from the response
async function summarizeWithCost(text) {
const res = await fetch(`${BASE}/summarize`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({ text }),
});
const data = await res.json();
console.log(`Cost: $${data.cost.toFixed(4)}`);
console.log(`Tokens: ${data.input_tokens} in / ${data.output_tokens} out`);
return data;
}
// Poll session stats
async function getStats() {
const res = await fetch(`${BASE}/stats`);
const stats = await res.json();
console.log(`Total spend: $${stats.total_cost_usd.toFixed(4)}`);
console.log(`Calls: ${stats.call_count}`);
console.log("By module:", stats.by_module);
console.log("By model:", stats.by_model);
return stats;
}
Go¶
package main
import (
"encoding/json"
"fmt"
"net/http"
"strings"
)
type ModuleResponse struct {
Text string `json:"text"`
Cost float64 `json:"cost"`
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
Model string `json:"model"`
}
type Stats struct {
TotalCostUSD float64 `json:"total_cost_usd"`
TotalTokens int `json:"total_tokens"`
CallCount int `json:"call_count"`
ByModule map[string]float64 `json:"by_module"`
ByModel map[string]float64 `json:"by_model"`
}
func summarize(text string) (*ModuleResponse, error) {
body := fmt.Sprintf(`{"text": %q}`, text)
req, _ := http.NewRequest("POST", "http://localhost:8000/summarize",
strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result ModuleResponse
json.NewDecoder(resp.Body).Decode(&result)
fmt.Printf("Cost: $%.4f | Tokens: %d in, %d out\n",
result.Cost, result.InputTokens, result.OutputTokens)
return &result, nil
}
func getStats() (*Stats, error) {
resp, err := http.Get("http://localhost:8000/stats")
if err != nil {
return nil, err
}
defer resp.Body.Close()
var stats Stats
json.NewDecoder(resp.Body).Decode(&stats)
fmt.Printf("Total: $%.4f across %d calls\n", stats.TotalCostUSD, stats.CallCount)
return &stats, nil
}
Java¶
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.*;
import java.util.Map;
public class AiForgeCostExample {
private static final String BASE = "http://localhost:8000";
private static final String API_KEY = System.getenv("AIFORGE_API_KEY");
private static final ObjectMapper mapper = new ObjectMapper();
private static final HttpClient client = HttpClient.newHttpClient();
/** Call /summarize and print cost info. */
public static Map<String, Object> summarize(String text) throws Exception {
String json = mapper.writeValueAsString(Map.of("text", text));
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/summarize"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + API_KEY)
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());
Map<String, Object> result = mapper.readValue(resp.body(), Map.class);
System.out.printf("Cost: $%.4f | Tokens: %s in, %s out%n",
((Number) result.get("cost")).doubleValue(),
result.get("input_tokens"),
result.get("output_tokens"));
return result;
}
/** Fetch session-level cost stats. */
public static Map<String, Object> getStats() throws Exception {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/stats"))
.GET()
.build();
HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());
Map<String, Object> stats = mapper.readValue(resp.body(), Map.class);
System.out.printf("Total: $%.4f across %s calls%n",
((Number) stats.get("total_cost_usd")).doubleValue(),
stats.get("call_count"));
return stats;
}
}
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
cost=0.0 for a known model |
Model name has an unexpected suffix | Check get_model_pricing("your-model") — if None, add to custom_pricing |
cost=0.0 for all calls |
COST_TRACKING_ENABLED=false |
Set AIFORGE_COST_TRACKING_ENABLED=true |
| WARNING: "Unknown model X" | Model not in built-in table | Add to custom_pricing in aiforge.yaml |
by_module is empty |
No calls have been made yet | Make at least one module call, then check summary() |
/stats returns all zeros |
Server just started / session was reset | Stats accumulate from first call; they reset when the process restarts |
| Warn threshold fires too often | Threshold too low for your workload | Increase AIFORGE_COST_WARN_THRESHOLD (e.g., 5.0 for batch jobs) |
| Cache hits not showing in stats | Cache is disabled | Set AIFORGE_CACHE_ENABLED=true — cache hits bypass cost recording entirely |
| Rate limiter blocking too long | RPM/TPM caps too low | Increase rate_limit_rpm / rate_limit_tpm in config |
Configuration Reference¶
| Variable | Type | Default | Description |
|---|---|---|---|
AIFORGE_COST_TRACKING_ENABLED |
bool |
true |
Enable/disable cost tracking |
AIFORGE_COST_WARN_THRESHOLD |
float |
1.0 |
USD threshold for per-call WARNING log |
AIFORGE_CUSTOM_PRICING |
dict |
{} |
Override or add model pricing (USD per 1M tokens) |
AIFORGE_CUSTOM_CONTEXT_WINDOWS |
dict |
{} |
Override model context window sizes |
AIFORGE_CACHE_ENABLED |
bool |
true |
Enable request cache (cache hits = $0) |
AIFORGE_CACHE_TTL |
int |
3600 |
Cache time-to-live in seconds |
AIFORGE_CACHE_BACKEND |
str |
memory |
Cache backend: memory or redis |
AIFORGE_CACHE_MAX_ENTRIES |
int |
1000 |
Max in-memory cache entries (LRU eviction) |
AIFORGE_CACHE_REDIS_URL |
str |
redis://localhost:6379/0 |
Redis connection URL |
AIFORGE_RATE_LIMIT_RPM |
int |
500 |
Requests per minute cap |
AIFORGE_RATE_LIMIT_TPM |
int |
150000 |
Tokens per minute cap |
See Also¶
- Configuration — Full configuration reference and settings hierarchy
- API Server — REST API endpoints including
/stats - API Authentication — Securing the API server
- Providers — Provider-specific details and model lists
- Evaluations — Per-run cost, tokens and latency for test sets
- Modules — Per-module SDK documentation