Manifest maintains a database of LLM model pricing to enable accurate cost tracking. The Model Prices page lets you view pricing for all supported models, sync updates, and identify models without pricing data.
Model Pricing Database
The model_pricing table stores per-token pricing for all supported models:
// From packages/backend/src/entities/model-pricing.entity.ts
{
model_name: string, // Canonical model identifier
provider: string, // Provider name (OpenAI, Anthropic, etc.)
input_price_per_token: number, // Cost per input token (USD)
output_price_per_token: number, // Cost per output token (USD)
context_window: number, // Max context size
capability_reasoning: number, // Reasoning capability score (0-1)
capability_code: number, // Code capability score (0-1)
quality_score: number, // Overall quality score (0-1)
updated_at: string // Last sync timestamp
}
Supported Providers
Manifest tracks pricing for:
- OpenAI: GPT-4o, GPT-4 Turbo, GPT-3.5, O1, O3
- Anthropic: Claude 3.5 Sonnet, Claude 3 Opus/Haiku
- Google: Gemini 2.0, Gemini 1.5 Pro/Flash
- DeepSeek: DeepSeek-V3, DeepSeek-R1
- Meta: Llama 3.3, Llama 3.2, Llama 3.1
- xAI: Grok 2, Grok Beta
- Mistral: Mistral Large, Codestral
- Ollama: Local models (zero cost)
Model Prices UI
The Model Prices page (/model-prices) displays all models in a sortable, filterable table:
// From packages/frontend/src/pages/ModelPrices.tsx:198-235
<table class="data-table">
<thead>
<tr>
<th onClick={() => handleSort('model_name')}>Model{indicator('model_name')}</th>
<th onClick={() => handleSort('provider')}>Provider{indicator('provider')}</th>
<th onClick={() => handleSort('input_price_per_million')}>
Cost to send / 1M tokens{indicator('input_price_per_million')}
</th>
<th onClick={() => handleSort('output_price_per_million')}>
Cost to receive / 1M tokens{indicator('output_price_per_million')}
</th>
</tr>
</thead>
<tbody>
<For each={sortedModels()}>
{(model) => (
<tr>
<td>{model.model_name}</td>
<td>{model.provider}</td>
<td>{formatPrice(model.input_price_per_million)}</td>
<td>{formatPrice(model.output_price_per_million)}</td>
</tr>
)}
</For>
</tbody>
</table>
Sorting
Click any column header to sort by that field. Clicking again reverses the sort direction (ascending ↔ descending).
// From packages/frontend/src/pages/ModelPrices.tsx:36-43
const handleSort = (key: SortKey) => {
if (sortKey() === key) {
setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'));
} else {
setSortKey(key);
setSortDir('asc');
}
};
Filtering
Filter by model name or provider using the filter bar:
// From packages/frontend/src/pages/ModelPrices.tsx:59-70
const filteredModels = createMemo(() => {
const models = data()?.models;
if (!models) return [];
const selModels = selectedModels();
const selProviders = selectedProviders();
return models.filter((m) => {
if (selModels.size > 0 && !selModels.has(m.model_name)) return false;
if (selProviders.size > 0 && !selProviders.has(m.provider)) return false;
return true;
});
});
Pricing Sync
Manifest syncs model pricing from external sources:
Automatic Sync
Pricing syncs automatically:
- On startup: When the backend starts
- Every 24 hours: Via cron job (configurable)
// From packages/backend/src/database/pricing-sync.service.ts
@Injectable()
export class PricingSyncService {
constructor(private readonly ds: DataSource) {}
async syncPricing(): Promise<number> {
// Fetch latest pricing from OpenRouter
const response = await fetch('https://openrouter.ai/api/v1/models');
const data = await response.json();
let updated = 0;
for (const model of data.data) {
const existing = await this.ds.query(
`SELECT * FROM model_pricing WHERE model_name = $1`,
[model.id]
);
if (existing.length === 0) {
// Insert new model
await this.ds.query(
`INSERT INTO model_pricing (...) VALUES (...)`,
[...]
);
updated++;
} else {
// Update if pricing changed
const current = existing[0];
if (current.input_price_per_token !== model.pricing.prompt ||
current.output_price_per_token !== model.pricing.completion) {
await this.ds.query(
`UPDATE model_pricing SET input_price_per_token = $1, output_price_per_token = $2, updated_at = $3 WHERE model_name = $4`,
[model.pricing.prompt, model.pricing.completion, new Date().toISOString(), model.id]
);
updated++;
}
}
}
return updated;
}
}
Manual Sync
Trigger a manual sync via API:
POST /api/v1/model-prices/sync
Response:
The UI can expose this as a “Refresh Prices” button.
Pricing History
When pricing changes, the old price is archived in pricing_history:
// From packages/backend/src/database/pricing-history.service.ts
async archiveChange(
modelName: string,
oldInputPrice: number,
oldOutputPrice: number,
newInputPrice: number,
newOutputPrice: number
) {
await this.ds.query(
`INSERT INTO pricing_history (id, model_name, old_input_price, old_output_price, new_input_price, new_output_price, changed_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
[uuid(), modelName, oldInputPrice, oldOutputPrice, newInputPrice, newOutputPrice, new Date().toISOString()]
);
}
View History
GET /api/v1/model-prices/gpt-4o/history
Response:
[
{
"changed_at": "2024-03-04T12:00:00Z",
"old_input_price_per_million": 5.00,
"old_output_price_per_million": 15.00,
"new_input_price_per_million": 2.50,
"new_output_price_per_million": 10.00
}
]
Model Name Normalization
Manifest normalizes model names to handle provider-specific variations:
// From packages/backend/src/model-prices/model-name-normalizer.ts
export function normalizeModelName(name: string): string {
// Remove common prefixes
let normalized = name.toLowerCase();
normalized = normalized.replace(/^(openai|anthropic|google|meta|mistral)[\/\-]/, '');
// Handle date suffixes (e.g., claude-3-5-sonnet-20241022 → claude-3-5-sonnet)
normalized = normalized.replace(/-\d{8}$/, '');
// Remove :latest, :beta tags
normalized = normalized.replace(/:(latest|beta|preview)$/, '');
return normalized;
}
This ensures pricing lookups work even when clients send non-canonical model names.
Unresolved Models
When Manifest encounters a model without pricing data, it logs it to unresolved_models:
// From packages/backend/src/model-prices/unresolved-model-tracker.service.ts
async trackUnresolved(modelName: string, provider: string, contextHint?: string) {
const existing = await this.ds.query(
`SELECT id FROM unresolved_models WHERE model_name = $1`,
[modelName]
);
if (existing.length === 0) {
await this.ds.query(
`INSERT INTO unresolved_models (id, model_name, provider, first_seen, last_seen, occurrence_count, context_hint)
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
[uuid(), modelName, provider, now, now, 1, contextHint]
);
} else {
await this.ds.query(
`UPDATE unresolved_models SET last_seen = $1, occurrence_count = occurrence_count + 1 WHERE model_name = $2`,
[now, modelName]
);
}
}
View Unresolved Models
GET /api/v1/model-prices/unresolved
Response:
[
{
"model_name": "my-custom-model",
"provider": "ollama",
"first_seen": "2024-03-04T10:00:00Z",
"last_seen": "2024-03-04T12:30:00Z",
"occurrence_count": 147,
"context_hint": "proxy"
}
]
Use this to identify models that need manual pricing entries.
Provider Configuration
When configuring routing, Manifest only allows selecting models from active providers:
// From packages/backend/src/routing/routing.controller.ts:160-181
@Get(':agentName/available-models')
async getAvailableModels(@CurrentUser() user: AuthUser, @Param() params: AgentNameParamDto) {
const agent = await this.resolveAgent(user.id, params.agentName);
const providers = await this.routingService.getProviders(agent.id);
const activeProviders = expandProviderNames(
providers.filter((p) => p.is_active).map((p) => p.provider),
);
const models = this.pricingCache.getAll();
return models
.filter((m) => activeProviders.has(m.provider.toLowerCase()))
.map((m) => ({
model_name: m.model_name,
provider: m.provider,
input_price_per_token: m.input_price_per_token,
output_price_per_token: m.output_price_per_token,
context_window: m.context_window,
capability_reasoning: m.capability_reasoning,
capability_code: m.capability_code,
quality_score: m.quality_score,
}));
}
This ensures users only see models they can actually use.
Pricing Cache
Model pricing is cached in-memory for fast lookups:
// From packages/backend/src/model-prices/model-pricing-cache.service.ts
@Injectable()
export class ModelPricingCacheService implements OnModuleInit {
private cache: Map<string, ModelPricing> = new Map();
async onModuleInit() {
await this.refresh();
}
async refresh() {
const rows = await this.ds.query(`SELECT * FROM model_pricing`);
this.cache.clear();
for (const row of rows) {
this.cache.set(row.model_name, row);
}
}
getByModel(modelName: string): ModelPricing | undefined {
return this.cache.get(normalizeModelName(modelName));
}
getAll(): ModelPricing[] {
return Array.from(this.cache.values());
}
}
The cache refreshes:
- On app startup (
onModuleInit)
- After pricing sync
- On manual refresh
API Endpoints
Get All Model Prices
Response:
{
"models": [
{
"model_name": "gpt-4o",
"provider": "OpenAI",
"input_price_per_million": 2.50,
"output_price_per_million": 10.00
},
{
"model_name": "claude-3-5-sonnet-20241022",
"provider": "Anthropic",
"input_price_per_million": 3.00,
"output_price_per_million": 15.00
}
],
"lastSyncedAt": "2024-03-04T12:00:00Z"
}
Sync Pricing
POST /api/v1/model-prices/sync
Get Unresolved Models
GET /api/v1/model-prices/unresolved
Get Pricing History
GET /api/v1/model-prices/{modelName}/history
Model pricing is cached for 1 hour on the client side (@CacheTTL(MODEL_PRICES_CACHE_TTL_MS)). Sync changes may take up to an hour to appear in the UI.
Custom Model Pricing
For self-hosted or custom models, manually insert pricing:
INSERT INTO model_pricing (
model_name, provider, input_price_per_token, output_price_per_token,
context_window, capability_reasoning, capability_code, quality_score, updated_at
)
VALUES (
'my-custom-model', 'ollama', 0.0, 0.0,
128000, 0.8, 0.9, 0.85, NOW()
);
Or use the API (future feature):
POST /api/v1/model-prices/custom
Content-Type: application/json
{
"model_name": "my-custom-model",
"provider": "ollama",
"input_price_per_million": 0.0,
"output_price_per_million": 0.0
}