take model metadata from models.dev and membership from /v1/models
56 unmodified lines5758596061626162636465662 unmodified lines6970717273737475767729 unmodified lines10710810911011111111211311411518 unmodified lines1341351361371381381391401411423 unmodified lines56 unmodified lines
Metadata comes from [models.dev](https://models.dev)'s `hyper` entry, which carries what acoding agent needs and Hyper's own endpoints only partly report: the reasoning effort levelsthis deployment takes, vision support, context and output limits, and prices.`GET /v1/provider` supplies membership and this deployment's own prices, and it agrees withmodels.dev on everything except display names.`GET /v1/models` - public and documented - supplies membership and this deployment's ownprices, and it agrees with models.dev on everything except display names.
`catalog.json` is that join, committed, and read at startup. On load the extension registersthe newest available catalog - the committed one, or the last refresh cached in`~/.prime/agent/hyper-catalog.json` - and then refreshes in the background, re-registering if2 unmodified lines
Regenerate after a model is added or repriced:
```npm run regen # two requests: models.dev and /v1/providernpm run regen # two requests: models.dev and /v1/modelsnpm run regen -- --dry # print the table without writing```
## Thinking29 unmodified lines## Verifying
```npm test # offline: key precedence, registration, catalog -> model config derivationnpm run verify # live: catalog vs /v1/provider and models.dev, streaming, usage, cache, tools, visionnpm run verify # live: catalog vs /v1/models and models.dev, streaming, usage, cache, tools, visionnpm run smoke # one real turn through prime-agent with a hyper modelnpm run check # tsc --noEmit against a neighbouring prime-agent checkout```
18 unmodified lines
| file | role || --- | --- || `index.ts` | extension entry: registration, background refresh, `/hyper` command || `catalog.ts` | `/v1/provider` + models.dev parsing, catalog files, model-config derivation || `catalog.ts` | `/v1/models` + models.dev parsing, catalog files, model-config derivation || `oauth.ts` | device-code login and refresh-token exchange || `hyper.ts` | provider identity, paths, key precedence, credit balance || `http.ts` | JSON transport with timeouts and error extraction || `catalog.json` | committed model list |3 unmodified lines12334567619 unmodified linesExpand all627628629630631631632{ "version": 1, "generatedAt": "2026-09-15T23:14:21.121Z", "generatedAt": "2026-09-15T23:16:38.722Z", "models": [ { "id": "deepseek-v4-flash", "name": "DeepSeek V4 Flash",619 unmodified linesExpand all "cacheWrite": 0 } } ], "source": "https://models.dev/api.json + GET /v1/provider" "source": "https://models.dev/api.json + GET /v1/models"}2 unmodified lines3456787891011126 unmodified lines1920212223232425262788 unmodified lines11611711811912012012112212312412512512612712812913013113213313013113213313413413513613713813913713813914014114214314414514614414514614714814915014814915015115115215315415515615715815915715816016116216316416512 unmodified lines178179180181179182183184185186100 unmodified lines2 unmodified lines * * Metadata comes from models.dev's `hyper` provider entry, which carries what a coding * agent needs and what Hyper's own endpoints only partly report: the reasoning effort * levels the deployment takes, vision support, context and output limits, and prices. * `GET /v1/provider` supplies membership and this deployment's own prices, and decides * which models exist right now. * `GET /v1/models` - public, documented - supplies membership and this deployment's own * prices, and decides which models exist right now. * * `catalog.json` is that join, committed. The runtime loads the newest of it and the last * refresh cached in ~/.prime/agent/hyper-catalog.json, registers immediately, and * refreshes in the background. Parsing is strict: a shape we do not understand leaves the6 unmodified linesimport { API_BASE_URL, cachePath } from "./hyper.js";import { getJson } from "./http.js";
export const CATALOG_VERSION = 1;export const CATALOG_SOURCE = "https://models.dev/api.json + GET /v1/provider";export const CATALOG_SOURCE = "https://models.dev/api.json + GET /v1/models";
/** Prime Agent's thinking levels, ascending. Hyper's effort levels use the same names. */export const PI_LEVELS = ["minimal", "low", "medium", "high", "xhigh", "max"] as const;
88 unmodified lines return optionalBoolean(model, "attachment") || input.includes("image");};
/** * One model of `GET /v1/provider`, enriched from models.dev. Prices and limits come from * One model of `GET /v1/models`, enriched from models.dev. Prices and limits come from * whichever source states them; Hyper's own answer wins where the two disagree, since * those are the prices this key is billed at. */function parseFacts(raw: unknown, index: number, dev: Record<string, unknown> | undefined): ModelFacts { const where = `Hyper /provider model ${index}`; const where = `Hyper /models model ${index}`; if (!isRecord(raw)) throw new Error(`${where} is not an object`); const id = requireString(raw, "id", where); const pricing = isRecord(optional(raw, "pricing")) ? (optional(raw, "pricing") as Record<string, unknown>) : {}; const capabilities = isRecord(optional(raw, "capabilities")) ? (optional(raw, "capabilities") as Record<string, unknown>) : {}; const reasoning = isRecord(optional(raw, "reasoning")) ? (optional(raw, "reasoning") as Record<string, unknown>) : undefined; const levels = reasoning ? strings(optional(reasoning, "effort_levels"), where, "effort_levels") : []; const devCost = dev && isRecord(optional(dev, "cost")) ? (optional(dev, "cost") as Record<string, unknown>) : undefined; const devLimit = dev && isRecord(optional(dev, "limit")) ? (optional(dev, "limit") as Record<string, unknown>) : undefined; const cost = (name: string, fallback: number): number => { const live = optionalNumber(raw, name); if (live !== 0 || fallback === 0) return live; return fallback; }; // /v1/models reports effort levels as objects; keep the names. const namedLevels = levels .map((level) => (isRecord(level) ? optional(level, "value") : level)) .filter((level): level is string => typeof level === "string"); return { id, name: dev ? requireString(dev, "name", `models.dev ${id}`) : requireString(raw, "name", where), reasoning: dev ? requireBoolean(dev, "reasoning", `models.dev ${id}`) : requireBoolean(raw, "can_reason", where), vision: dev ? devVision(dev) : requireBoolean(raw, "supports_attachments", where), name: dev ? requireString(dev, "name", `models.dev ${id}`) : requireString(raw, "display_name", where), reasoning: dev ? requireBoolean(dev, "reasoning", `models.dev ${id}`) : reasoning !== undefined, vision: dev ? devVision(dev) : optionalBoolean(capabilities, "vision"), contextWindow: devLimit ? (optionalNumber(devLimit, "context") || requireNumber(raw, "context_window", where)) : requireNumber(raw, "context_window", where), maxTokens: devLimit ? (optionalNumber(devLimit, "output") || requireNumber(raw, "default_max_tokens", where)) : requireNumber(raw, "default_max_tokens", where), levels: dev ? devLevels(dev) : strings(optional(raw, "reasoning_levels"), where, "reasoning_levels"), ? (optionalNumber(devLimit, "output") || requireNumber(raw, "max_output_tokens", where)) : requireNumber(raw, "max_output_tokens", where), levels: dev ? devLevels(dev) : namedLevels, cost: { input: cost("cost_per_1m_in", devCost ? optionalNumber(devCost, "input") : 0), output: cost("cost_per_1m_out", devCost ? optionalNumber(devCost, "output") : 0), cacheRead: cost("cost_per_1m_out_cached", devCost ? optionalNumber(devCost, "cache_read") : 0), cacheWrite: cost("cost_per_1m_in_cached", devCost ? optionalNumber(devCost, "cache_write") : 0), input: optionalNumber(pricing, "input") || (devCost ? optionalNumber(devCost, "input") : 0), output: optionalNumber(pricing, "output") || (devCost ? optionalNumber(devCost, "output") : 0), cacheRead: optionalNumber(pricing, "cache_hit") || (devCost ? optionalNumber(devCost, "cache_read") : 0), cacheWrite: optionalNumber(pricing, "cache_create") || (devCost ? optionalNumber(devCost, "cache_write") : 0), }, };}
export function parseFactsResponse(payload: unknown, devModels: Record<string, unknown>): ModelFacts[] { const models = isRecord(payload) ? at(payload, "models") : undefined; if (!Array.isArray(models) || models.length === 0) throw new Error("Hyper /provider returned no models"); const models = isRecord(payload) ? at(payload, "data") : undefined; if (!Array.isArray(models) || models.length === 0) throw new Error("Hyper /v1/models returned no models"); return models.map((model, index) => { const id = isRecord(model) ? optional(model, "id") : undefined; const dev = typeof id === "string" ? devModels[id] : undefined; return parseFacts(model, index, isRecord(dev) ? dev : undefined);12 unmodified lines }}
export const fetchFacts = async (): Promise<ModelFacts[]> => parseFactsResponse(await getJson(`${API_BASE_URL}/provider`), await fetchDevModels()); parseFactsResponse(await getJson(`${API_BASE_URL}/models`), await fetchDevModels());
/** One model row of a catalog file. */function parseCatalogModel(raw: unknown, index: number): ModelFacts { const where = `Catalog model ${index}`;100 unmodified lines9 unmodified lines10111213141415161718105 unmodified linesExpand all9 unmodified lines * 1. `/login hyper` -> device-code subscription login * 2. HYPER_API_KEY * 3. ~/.charm.key * * Models come from `GET /v1/provider` via the committed `catalog.json` and the last * Models come from `GET /v1/models` and models.dev via the committed `catalog.json` and the last * successful refresh in ~/.prime/agent/hyper-catalog.json. The catalog that is newest is * registered immediately, so startup never waits on the network, and a background refresh * re-registers when Hyper's list changed. */105 unmodified linesExpand all123345678991011121328 unmodified lines/** * Regenerate `catalog.json`: metadata from models.dev's `hyper` entry, membership and this * deployment's prices from `GET /v1/provider`. * deployment's prices from `GET /v1/models`. * * npm run regen write the catalog * npm run regen -- --dry print it without writing * * Two requests, no probing. Models.dev carries what Hyper's endpoints only partly report * (the reasoning effort levels the deployment takes, vision, limits), and `/v1/provider` * (the reasoning effort levels the deployment takes, vision, limits), and `/v1/models` * is authoritative for which models exist and what this key is billed. */
import { writeFileSync } from "node:fs";28 unmodified lines4 unmodified lines56789910111213101 unmodified linesExpand all11511611711811911912012112212311 unmodified lines13513613713813913914014114214388 unmodified lines2322332342352362362372382392407 unmodified lines4 unmodified lines * * Three claims are checked: * * 1. mapping the catalog row -> Prime Agent model config derivation, pure code * 2. catalog catalog.json still agrees with /v1/provider and models.dev's hyper entry * 2. catalog catalog.json still agrees with /v1/models and models.dev's hyper entry * 3. wire streaming, usage, prompt-cache accounting, tool calls, reasoning output, * image handling, and the credit balance the /hyper command reads * * Roughly twenty requests. Exits non-zero on any failure.101 unmodified linesExpand all
const close = (a: number, b: number): boolean => Math.abs(a - b) < 1e-4;
async function checkCatalog(): Promise<void> { const live = parseFactsResponse(await getJson(`${API_BASE_URL}/provider`), await fetchDevModels()); const live = parseFactsResponse(await getJson(`${API_BASE_URL}/models`), await fetchDevModels()); const liveIds = new Set(live.map((model) => model.id)); for (const model of live) { const frozen = byId.get(model.id); expect(frozen !== undefined, `${model.id} is missing from catalog.json (run npm run regen)`);11 unmodified lines `${model.id} effort levels changed (${frozen.levels.join(",")} -> ${model.levels.join(",")})`, ); } for (const model of catalog.models) { expect(liveIds.has(model.id), `${model.id} is in catalog.json but not in /v1/provider (run npm run regen)`); expect(liveIds.has(model.id), `${model.id} is in catalog.json but not in /v1/models (run npm run regen)`); }}
// 3. wire -----------------------------------------------------------------------------88 unmodified linesasync function main(): Promise<void> { checkMapping(); process.stdout.write("mapping checked\n"); await checkCatalog(); process.stdout.write("catalog checked against /v1/provider and models.dev\n"); process.stdout.write("catalog checked against /v1/models and models.dev\n"); await checkWire(); process.stdout.write("wire behaviour checked\n"); if (failures.length > 0) { for (const failure of failures) process.stdout.write(`FAIL ${failure}\n`);7 unmodified lines