diff --git a/README.md b/README.md index ad4e05d..880ec77 100644 --- a/README.md +++ b/README.md @@ -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" } + }, + "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) diff --git a/docs/models-info.md b/docs/models-info.md index 0b420d1..fbe1e2c 100644 --- a/docs/models-info.md +++ b/docs/models-info.md @@ -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 diff --git a/packages/opencode-models-info/README.md b/packages/opencode-models-info/README.md index 6d9eba3..835d5c2 100644 --- a/packages/opencode-models-info/README.md +++ b/packages/opencode-models-info/README.md @@ -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`.