# Models: listing them and choosing one

vid2context does not have one model. It has a chain of rungs (one provider, one model, one credential each), tried free-first: local, then free tiers with your own key, then keyless hosts, then paid. This guide covers how the chain is shown to people as a list of models, and how a request picks one.

## Listing models

```ts
import { groupModels, listModels } from '@vid2context/core';

const models = listModels(registry, env, { kind: 'llm' }); // kind: 'llm' | 'stt' | 'embed' | 'video' | 'visual'; omit for all
const groups = groupModels(models); // [{ tier, services: [{ service, models }] }], for a picker
```

`listModels` reads env only (no network) and returns every rung the installed providers know, enabled or not, so people can see what a free key would unlock. Each entry is a `ModelInfo`:

| Field | Meaning |
|---|---|
| `id` | Rung id, e.g. `llm/groq/openai/gpt-oss-120b`. Send it back to choose this model. |
| `label` | The model id without vendor prefixes: `gpt-oss-120b`. |
| `service`, `provider`, `model` | Upstream service (`groq`, `ovh`; a pooled extra key is `groq#2`), the provider package, the exact model id. |
| `tier` | `local`, `free`, `keyless` or `paid`. |
| `quality` | 1 (rough) to 5 (best); the `quality` policy orders by it. |
| `capabilities` | Badges, below. |
| `contextTokens` | Context window on that tier, when known. |
| `enabled` | Whether the chain would use it now: its key is set, keyless lanes are allowed (`VID2CONTEXT_KEYLESS` is not `0`), paid is allowed (`VID2CONTEXT_ALLOW_PAID` is not `0`). |
| `requires` | Names of the env vars that enable it. Never their values. |

The list is sorted the way Auto tries rungs: tier (local, free, keyless, paid), then priority, then quality. Over HTTP the same list is `GET /api/v1/models?kind=llm`.

## Capability badges

| Badge | Meaning |
|---|---|
| `stream` | Answers stream token by token. Models without it still work in a chat UI; their answer arrives in one piece. |
| `json` | Can be asked for a JSON object (`response_format: json_object`). |
| `json-schema` | Strict structured output: the answer is held to a JSON Schema. |
| `tools` | Function calling. |
| `vision` | Accepts images. |

Speech-to-text rungs use their own words (`segments`, `words`, `diarization`, `translate`, ...); see [docs/contracts/providers.md](https://vid2context.com/docs/contracts/providers.md).

## Choosing a model: Auto, pinned, strict

```ts
import { chatStream, resolveModelChoice } from '@vid2context/core';

const chain = resolveModelChoice(choice, registry, env);
for await (const ev of chatStream(registry, req, chain, ctx)) { /* meta, reasoning, delta, done */ }
```

| Choice | What happens |
|---|---|
| nothing (Auto) | The server's policy (`VID2CONTEXT_POLICY`, `free` by default) over the whole chain. This is the default and the cheapest. |
| `{ policy: 'free' \| 'quality' \| 'local-only' }` | That policy over the whole chain. `local-only` never leaves the machine. |
| `{ rung: '<id>' }` (pinned) | Start with that model. If it fails before the first token (rate limit, outage, not enabled), the chain carries on in its normal order. |
| `{ rung: '<id>', strict: true }` | That model or an error. Choosing a strict model that is not enabled is rejected up front, with the env var names that would enable it. |

An unknown id or policy is a `bad-request` error whose hint points at `/models`.

## What the stream looks like

`chatStream` yields `meta` (the rung that is answering), then `reasoning` (only when the model shows its thinking), then `delta` events with the answer, then one `done` with the full text, usage and every attempt. If the first rung fails before answering, the next one is tried and `meta` names the one that answered. Once answer text has started, the stream never switches models: a failure ends it with an error listing the attempts, so a reply never mixes two models.
