The docs read /v1/models rates from its per-million keys and say that pricing.prompt and completion are per token.

This commit is contained in:
2026-09-30 14:55:49 -07:00
parent d4e0db1673
commit 3b77105813
7 changed files with 28 additions and 15 deletions
+6 -4
View File
@@ -15,7 +15,9 @@ import { PROVIDER_ICONS, providerKey } from '@/components/provider-icons';
const ENDPOINT = 'https://api.hanzo.ai/v1/models';
type Pricing = { input: number | null; output: number | null; cacheRead: number | null; cacheWrite: number | null };
// `prompt`/`completion` are USD per token (OpenRouter's keys); the per-million
// rates sit under keys that name the unit.
type Pricing = { prompt?: string; completion?: string; input_per_million?: number; output_per_million?: number };
type Model = {
id: string;
name: string;
@@ -41,7 +43,7 @@ const FAMILY_ICON: Record<string, typeof Sparkle> = { Sparkle, Zap, Cpu, Box };
// catalogue's raw key; these are the names we use for them.
const OURS: Record<string, string> = { hanzo: 'Hanzo', zenlm: 'Zen' };
// Prices come back as USD per 1M tokens.
// Prices shown are USD per 1M tokens.
function price(v: number | null | undefined): string {
if (v == null) return '—';
if (v === 0) return 'Free';
@@ -148,8 +150,8 @@ function ModelRow({ m }: { m: Model }) {
</YStack>
</Td>
<Td align="right">{ctx(m.context_window)}</Td>
<Td align="right">{price(p?.input)}</Td>
<Td align="right">{price(p?.output)}</Td>
<Td align="right">{price(p?.input_per_million)}</Td>
<Td align="right">{price(p?.output_per_million)}</Td>
<Td align="right">
<XStack gap={4} flexWrap="wrap" justify="flex-end">
{capabilities(m).slice(0, 4).map((f) => (
+3 -2
View File
@@ -195,8 +195,9 @@ and each call is a row in the org's usage (`GET /v1/billing/usage`, model
| `typesafe/jev-1.13`, `~typesafe/jev-latest` | $0.03 per call |
Kai's rate is its row in the model catalogue every model is billed from:
`GET https://api.hanzo.ai/v1/models` lists `kai` with `pricing.input` 0.021 and
`pricing.output` 0. A refused or failed call is not billed.
`GET https://api.hanzo.ai/v1/models` lists `kai` with `pricing.input_per_million`
0.021 (`pricing.prompt` `"0.000000021"` per token) and `pricing.output_per_million`
0. A refused or failed call is not billed.
## Errors
@@ -34,8 +34,9 @@ curl -sS https://api.hanzo.ai/v1/models | jq '.data[] | select(.id == "kai")'
Step 2 needs an org whose plan covers paid models; without one it answers
`402` with `"code": "plan_required"` and a link to pick a plan. Step 3 answers
`"outputs": ["decision"]` and `"pricing": {"input": 0.021, "output": 0}`: the
dollars per million input tokens every `kai` call is billed at.
`"outputs": ["decision"]` and `"pricing"` with `"input_per_million": 0.021` and
`"output_per_million": 0`: the dollars per million input tokens every `kai` call
is billed at (`"prompt": "0.000000021"` is the same rate per token).
## What changes
+1 -1
View File
@@ -103,7 +103,7 @@ The managed ids, from the gateway:
```bash
curl -s https://api.hanzo.ai/v1/models \
| jq -r '.data[] | select(.id | startswith("enso")) | "\(.id)\t\(.context_window)\t\(.pricing.input)/\(.pricing.output)"'
| jq -r '.data[] | select(.id | startswith("enso")) | "\(.id)\t\(.context_window)\t\(.pricing.input_per_million)/\(.pricing.output_per_million)"'
```
| Id | Reasoning | Note |
+4 -2
View File
@@ -54,14 +54,16 @@ needs no key:
```bash
curl -s https://api.hanzo.ai/v1/models \
| jq -r '.data[] | select(.id | test("^(zen|enso)")) | "\(.id)\t\(.owned_by)\t\(.pricing.input)/\(.pricing.output)"'
| jq -r '.data[] | select(.id | test("^(zen|enso)")) | "\(.id)\t\(.owned_by)\t\(.pricing.input_per_million)/\(.pricing.output_per_million)"'
```
<ModelsCatalog />
## What a rate means
Rates are per million tokens, input and output priced separately, and they are
Rates are per million tokens (`pricing.input_per_million`, `output_per_million`;
`pricing.prompt` and `completion` state the same rates per token, as OpenRouter
does), input and output priced separately, and they are
the same whether you call from the CLI, an SDK, HTTP or an MCP tool — the price
is a property of the model, not of the surface you reach it through.
+1 -1
View File
@@ -75,7 +75,7 @@ needs no key:
```bash
curl -s https://api.hanzo.ai/v1/models \
| jq -r '.data[] | select(.owned_by == "zenlm") | "\(.id)\t\(.context_window)\t\(.pricing.input)/\(.pricing.output)"'
| jq -r '.data[] | select(.owned_by == "zenlm") | "\(.id)\t\(.context_window)\t\(.pricing.input_per_million)/\(.pricing.output_per_million)"'
```
The gateway is the source of truth for ids, windows and rates. If this page and
+10 -3
View File
@@ -177,12 +177,19 @@ async function fetchCatalogue(): Promise<Map<string, Rate & { context: number |
console.warn(`[pricing] catalogue answered ${r.status}`);
return out;
}
// `pricing.prompt`/`completion` are USD per token; the per-million rates this
// page prints sit under `input_per_million`/`output_per_million`.
const d = (await r.json()) as {
data?: Array<{ id: string; context_window?: number | null; pricing?: Rate }>;
data?: Array<{
id: string;
context_window?: number | null;
pricing?: { input_per_million?: number; output_per_million?: number };
}>;
};
for (const m of d.data ?? []) {
if (m.pricing?.input == null) continue;
out.set(m.id, { ...m.pricing, context: m.context_window ?? null });
const input = m.pricing?.input_per_million;
if (input == null) continue;
out.set(m.id, { input, output: m.pricing?.output_per_million ?? null, context: m.context_window ?? null });
}
} catch (e) {
console.warn(`[pricing] catalogue unreachable: ${(e as Error).message}`);