Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,25 +90,30 @@ See [packages/opencode-oauth2/README.md](packages/opencode-oauth2/README.md) for

## Companion plugin: model metadata

This workspace also ships [`@vymalo/opencode-models-info`](packages/opencode-models-info) — a separate, **auth-agnostic** plugin that enriches your model entries with full metadata (context length, output limit, USD/M-token cost, modalities, and `tool_call` / `reasoning` / `attachment` flags) by fetching from an OpenRouter-shaped `/models` endpoint.
This workspace also ships [`@vymalo/opencode-models-info`](packages/opencode-models-info) — a separate, **auth-agnostic** plugin that enriches your model entries with full metadata (context length, output limit, USD/M-token cost, modalities, and `tool_call` / `reasoning` / `attachment` flags).

It doesn't depend on this plugin: it runs as a `config` hook *after* other plugins, so it composes with oauth2, static API keys, or no auth at all. When paired with `@vymalo/opencode-oauth2` ≥ 0.4.0, OAuth2-protected metadata endpoints work with zero extra config — this plugin stamps the cached bearer onto the provider's headers at config time, and the metadata fetch inherits it.
`meta.modelsInfoUrl` is **the HTTP(S) endpoint that returns the metadata JSON** — `{ "data": [ { "id", "context_length", "pricing", … } ] }`. Point it at your provider's metadata endpoint (an absolute URL, or a path resolved against `baseURL`):

```jsonc
{
"plugin": ["@vymalo/opencode-oauth2", "@vymalo/opencode-models-info"],
"plugin": ["@vymalo/opencode-models-info"],
"provider": {
"example-ai": {
"my-provider": {
"npm": "@ai-sdk/openai-compatible",
"options": {
"baseURL": "https://api.example.com/v1",
"oauth2": { "issuer": "https://auth.example.com", "clientId": "opencode-client", "scopes": ["openid", "offline_access"] },
"meta": { "modelsInfoUrl": "models" }
}
"meta": { "modelsInfoUrl": "https://api.example.com/v1/models" }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Point the example at rich metadata

In this top-level copy-paste example, a normal @ai-sdk/openai-compatible provider will usually expose a sparse OpenAI /v1/models response at this URL, and the new note below explicitly says that shape lacks the context_length/pricing fields the plugin maps. In that common setup the fetch succeeds but enriches nothing, so the example should use an OpenRouter-shaped metadata route (or an absolute OpenRouter catalog URL) rather than https://api.example.com/v1/models.

Useful? React with 👍 / 👎.

},
"models": { "my-model-large": {} }
}
}
}
```

The expected JSON is commonly called the **OpenRouter shape** (it's what OpenRouter's `/models` returns), but the plugin has no dependency on OpenRouter — any endpoint serving that shape works. A plain OpenAI-compatible `/v1/models` returns sparse data (`id`, `object`, `owned_by`) — *not* `context_length` / `pricing` — so the endpoint must actually carry the richer fields.

It doesn't depend on the oauth2 plugin — it runs as a `config` hook *after* other plugins, composing with oauth2, static API keys, or no auth. When paired with `@vymalo/opencode-oauth2` ≥ 0.4.0, an OAuth2-protected metadata endpoint works with zero extra config: the oauth2 plugin stamps the cached bearer onto the provider's headers at config time and the metadata fetch inherits it.

Full reference: [`packages/opencode-models-info/README.md`](packages/opencode-models-info/README.md). Behavior, caching, and composition details: [`docs/models-info.md`](docs/models-info.md).

## Federated identity (CI / Kubernetes)
Expand Down
4 changes: 4 additions & 0 deletions docs/models-info.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ For the copy-paste config reference (every option, the full OpenRouter→OpenCod

OpenCode supports rich per-model metadata — context window, output limit, USD-per-1M-token cost, and `tool_call` / `reasoning` / `attachment` capability flags — but you normally hand-write it in `opencode.json`. If your provider exposes an OpenRouter-shaped `/models` endpoint, this plugin fetches it once, merges the metadata onto your model entries, caches the result, and stays out of the way.

You point the plugin at that endpoint with `options.meta.modelsInfoUrl` — **the HTTP(S) URL (absolute, or a path resolved against `options.baseURL`) that returns the metadata JSON**: `{ "data": [ { "id", "context_length", "pricing", … } ] }`. This JSON is commonly called the **OpenRouter shape** (it's what OpenRouter's `/models` returns), but the plugin has no dependency on OpenRouter and never contacts it — any endpoint that returns the shape works: a self-hosted gateway, a LiteLLM proxy, or a custom metadata route. The compatibility bar is low: a bare top-level array (no `data` wrapper) is accepted, and the field mapping is partial, so an endpoint only needs to emit the fields you want enriched.

> **Not the vanilla `/v1/models`.** A standard OpenAI-compatible `/v1/models` returns only `id` / `object` / `owned_by` — none of the fields this plugin maps. Pointing `modelsInfoUrl` at it fetches successfully and enriches nothing. The URL must return the richer OpenRouter shape.

It is **auth-agnostic** and does **not** depend on `@vymalo/opencode-oauth2`. It only mutates the already-assembled OpenCode config, so it works with static API keys, oauth2, or no auth at all.

## The one hook
Expand Down
16 changes: 9 additions & 7 deletions packages/opencode-models-info/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,30 +24,32 @@ Add it to your `opencode.json` plugin list:

## Usage

For every provider you want enriched, add `options.meta.modelsInfoUrl`:
`meta.modelsInfoUrl` is **the HTTP(S) endpoint that returns the metadata JSON** — an absolute URL or a path resolved against `options.baseURL`. Point it at your own provider's metadata endpoint:

```json
{
"plugin": ["@vymalo/opencode-models-info"],
"provider": {
"my-gateway": {
"my-provider": {
"npm": "@ai-sdk/openai-compatible",
"options": {
"baseURL": "https://gateway.example.com/v1",
"baseURL": "https://api.example.com/v1",
"meta": {
"modelsInfoUrl": "models/info",
"modelsInfoUrl": "https://api.example.com/v1/models",
"modelsInfoTtlSeconds": 86400,
"modelsInfoTimeoutMs": 5000
}
},
"models": {
"gpt-x-large": {}
}
"models": { "my-model-large": {} }
}
}
}
```

An absolute URL is clearest. A relative path is also accepted — it resolves against `baseURL` (e.g. `"models"` → `https://api.example.com/v1/models`); see [URL resolution](#url-resolution).

> **What shape must that endpoint return?** The JSON described in [Expected response shape](#expected-response-shape-openrouter) below — commonly called the **OpenRouter shape** because OpenRouter's `/models` endpoint returns it, but the plugin has no dependency on OpenRouter and never contacts it. The compatibility bar is low: a **bare top-level array** (no `data` wrapper) is accepted, and the mapping is **partial**, so your endpoint only needs to emit the fields you want enriched (e.g. just `id` + `context_length` + `pricing`). **But note:** a vanilla OpenAI-compatible `/v1/models` returns only `id` / `object` / `owned_by` — *none* of the fields this plugin maps — so pointing `modelsInfoUrl` there fetches successfully and enriches nothing. The endpoint has to actually carry the richer data.

That's it. After OpenCode starts:

1. The hook picks up every provider with a `meta.modelsInfoUrl`.
Expand Down