# prime-charm-hyper

A [Prime Agent](https://github.com/PrimeIntellect-ai/prime-agent) provider extension for
[Charm Hyper](https://hyper.charm.land), Charm's inference service for coding models.

Registers the `hyper` provider with the whole Hyper catalog, device-code subscription
login, API-key auth, and the thinking controls each model advertises.

## Install

```
ln -s /Users/dawn/proj/prime-charm-hyper ~/.prime/agent/extensions/prime-charm-hyper
```

Edits are picked up by `/reload`; new sessions load it at startup. For a one-off run
without installing:

```
prime-agent -e /Users/dawn/proj/prime-charm-hyper --model hyper/glm-5.3-flash
```

## Auth

`/login hyper` offers the subscription device-code flow first (`POST /device/auth`, then
`GET /device/auth/<code>` until approved, then `POST /token/exchange`); the link and
verification code are printed in the login dialog and the token is refreshed
automatically. `HYPER_API_KEY` covers throwaway shells and the extension's own config
resolves to it (`!printf %s "$HYPER_API_KEY"`), so when neither exists the provider reports
an unresolved key rather than sending a placeholder as the credential. There is no key file.

Credentials live in Prime Agent's `auth.json` and are tried in list order, so the stored
order is the precedence: subscription first, API key as the fallback that gets used when
the subscription is missing, stale, or blocked. Verified both ways: a turn still completes
with the API key replaced by a bogus value (subscription used), and with the subscription
replaced by an expired one (API key used).

The access token lasts an hour. Every `POST /token/exchange` returns a **rotated** refresh
token and invalidates the previous one, so the extension returns the exchanged credential
and Prime Agent persists it; a rejected refresh is reported as "run /login hyper again"
rather than as a generic HTTP error. Removing a single account happens in the `/login`
provider list; `prime-agent logout hyper` drops every credential for the provider,
including the API key.

## Why Chat Completions

Hyper exposes three chat endpoints. This extension uses `POST /v1/chat/completions` because
it is the only one that carries the behaviour an agent needs, measured on the live service:

| | `/v1/chat/completions` | `/v1/messages` | `/v1/responses` |
| --- | --- | --- | --- |
| streaming text | yes | yes | yes |
| tool calls | yes | yes | yes |
| reasoning visible | `reasoning_content` | `thinking` block | not emitted |
| can stop thinking | `reasoning_effort: "none"` | no | n/a |
| prompt-cache accounting | `prompt_tokens_details.cached_tokens` | none | none |
| usage in stream | yes, with cost | yes | yes |

`/v1/messages` returns a thinking block even when the request omits `thinking`, and reports
no cache fields, so cost accounting would be wrong. `/v1/responses` never emitted a
reasoning item at any effort level. Both are usable; neither is better for this client.

One endpoint quirk worth knowing: `tool_choice: "required"` and the object form both return
HTTP 400, while `auto` and `none` work. Prime Agent never sends `tool_choice`, so ordinary
tool use is unaffected.

## Models

Metadata comes from [models.dev](https://models.dev)'s `hyper` entry, which carries what a
coding agent needs and Hyper's own endpoints only partly report: the reasoning effort levels
this deployment takes, vision support, context and output limits, and prices.
`GET /v1/models` - public and documented - supplies membership and this deployment's own
prices, 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 registers
the 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 if
the model list changed. Startup never waits on the network, and a fresh clone has models
before the first request completes.

Regenerate after a model is added or repriced:

```
npm run regen              # two requests: models.dev and /v1/models
npm run regen -- --dry     # print the table without writing
```

## Thinking

Hyper's effort levels are per model, and Prime Agent's are not, so each advertised level maps
to the level of the same name; a level the model does not advertise is hidden rather than
sent on the chance that Hyper ignores it, because two settings that send the same request are
worse than one setting. Models that reason without naming levels (`glm-5`, `kimi-k2-thinking`,
`gemma-4-26b`) get a single on state, Prime Agent's `max`; a model that does not reason at all
gets no thinking controls.

The off switch is only offered where Hyper advertises one, which arrives as a `none` level and
goes out as `reasoning_effort: "none"`. Everything else sends plain OpenAI fields:
`reasoning_effort` for a level, nothing for off. Hyper also accepts a non-standard
`thinking: {type: "disabled"}` on many models that do not advertise `none` - measured working
on `deepseek-v4-*` and `glm-5.x`, and a hard 400 on `glm-5.3` and `kimi-k2-thinking` - but
that needs per-model evidence, and the advertised level list is the contract this extension
ships against.

## Caching and cost

Prompt caching is implicit and automatic: Hyper bills a repeated prefix as a cache hit
(measured at 4x lower cost on the second call) and reports it in
`prompt_tokens_details.cached_tokens`, which the openai-completions provider turns into Prime
Agent's `cacheRead` usage. No cache-control markers are sent; `/v1/messages` is where those
would go, and it does not report cache usage at all.

`/hyper` prints the account's hypercredit balance (`GET /v1/credits`) and the registered model
count. Hyper extends the standard usage object with `cost.usd`, `cost.hypercredits`, and
`remaining.hypercredits`; Prime Agent reads only token counts and the catalog's published
prices, so the balance in `/hyper` is the live figure.

## Verifying

```
npm test           # offline: key precedence, registration, catalog -> model config derivation
npm run verify     # live: catalog vs /v1/models and models.dev, streaming, usage, cache, tools, vision
npm run smoke      # one real turn through prime-agent with a hyper model
npm run check      # tsc --noEmit against a neighbouring prime-agent checkout
```

`verify` costs about twenty requests and takes ten seconds. It checks the derivation, compares
every catalog model against both live sources, and exercises streaming, usage, cache
accounting, tool calls, reasoning output, image acceptance and rejection, and the credit
endpoint.

The device login was checked as far as it can be without a human: `POST /device/auth` returns
the code and verification URL this extension renders, and an unapproved code polls as
`authorization_pending`. The final token exchange follows the shape the official client uses
and needs one approved login to exercise.

## Relationship to the official extension

Charm publishes `@charmland/pi-hyper-provider` for upstream Pi. This one is independent and
targets this fork's extension API. Notable differences: this extension refreshes the catalog in
the background instead of during startup, takes model metadata from models.dev, and has no
status-line integration.

## Layout

| file | role |
| --- | --- |
| `index.ts` | extension entry: registration, background refresh, `/hyper` command |
| `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 resolution, credit balance |
| `http.ts` | JSON transport with timeouts and error extraction |
| `catalog.json` | committed model list |
| `regen.ts` | rebuilds `catalog.json` |
| `test.ts` | offline checks |
| `verify.ts` | live verification |
