@vymalo/opencode-models-info 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 vymalo contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,165 @@
1
+ # @vymalo/opencode-models-info
2
+
3
+ OpenCode plugin that **enriches** model entries already contributed by other plugins (or by your `opencode.json`) with full metadata — context length, output limit, pricing, modalities, and capability flags (`tool_call`, `reasoning`, `attachment`) — by fetching from a provider-supplied **OpenRouter-shaped** endpoint.
4
+
5
+ Auth-agnostic by design: the plugin runs as an OpenCode `config` hook *after* other plugins have populated providers and headers, so it composes with `@vymalo/opencode-oauth2`, static API keys, or any other auth scheme without depending on any of them.
6
+
7
+ ## Why use this
8
+
9
+ OpenCode supports rich per-model metadata (context window, USD/M-token cost, tool-call/reasoning/attachment flags) but you usually have to handwrite it in `opencode.json`. If your provider exposes a JSON endpoint with this info (OpenRouter, LiteLLM with the OpenRouter-compat extension, your own gateway), this plugin fetches it once, merges it onto every model, caches the result, and stays out of the way.
10
+
11
+ ## Installation
12
+
13
+ ```sh
14
+ npm install @vymalo/opencode-models-info
15
+ ```
16
+
17
+ Add it to your `opencode.json` plugin list:
18
+
19
+ ```json
20
+ {
21
+ "plugin": ["@vymalo/opencode-models-info"]
22
+ }
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ For every provider you want enriched, add `options.meta.modelsInfoUrl`:
28
+
29
+ ```json
30
+ {
31
+ "plugin": ["@vymalo/opencode-models-info"],
32
+ "provider": {
33
+ "my-gateway": {
34
+ "npm": "@ai-sdk/openai-compatible",
35
+ "options": {
36
+ "baseURL": "https://gateway.example.com/v1",
37
+ "meta": {
38
+ "modelsInfoUrl": "models/info",
39
+ "modelsInfoTtlSeconds": 86400,
40
+ "modelsInfoTimeoutMs": 5000
41
+ }
42
+ },
43
+ "models": {
44
+ "gpt-x-large": {}
45
+ }
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ That's it. After OpenCode starts:
52
+
53
+ 1. The hook picks up every provider with a `meta.modelsInfoUrl`.
54
+ 2. It `GET`s that URL once, sending whatever `options.headers` the provider already has (so it composes with any auth plugin — see [Auth composition](#auth-composition)).
55
+ 3. Each model entry whose `id` matches an entry in the response gets `limit`, `cost`, `modalities`, `tool_call`, `reasoning`, `attachment`, etc. filled in — **only where they were not already set** (upstream wins).
56
+ 4. The response is cached on disk for `modelsInfoTtlSeconds` (default 24h), keyed by `(providerId, url, modelsInfoHeaders)`. ETags are honored.
57
+ 5. On fetch error with a valid cache, the stale snapshot is served — the plugin never blocks OpenCode startup on a network failure.
58
+
59
+ ### URL resolution
60
+
61
+ `meta.modelsInfoUrl` resolves against `options.baseURL` using standard WHATWG URL semantics:
62
+
63
+ | `baseURL` | `modelsInfoUrl` | Resolved URL |
64
+ | -------------------------- | ---------------------- | ------------------------------------- |
65
+ | `https://x.test/v1` | `models/info` | `https://x.test/v1/models/info` |
66
+ | `https://x.test/v1` | `/models/info` | `https://x.test/models/info` |
67
+ | `https://x.test/v1` | `https://o.test/m` | `https://o.test/m` |
68
+
69
+ Two practical rules: drop the leading `/` to keep the metadata path under your inference API path; keep the leading `/` to escape to a different path under the same host.
70
+
71
+ ### Options
72
+
73
+ | Option | Default | Notes |
74
+ | ------------------------------- | ------------------ | --------------------------------------------------------------------- |
75
+ | `meta.modelsInfoUrl` | _(required)_ | Absolute URL or path resolved against `options.baseURL` (see above). |
76
+ | `meta.modelsInfoTtlSeconds` | `86400` (24h) | Cache TTL. |
77
+ | `meta.modelsInfoTimeoutMs` | `5000` | Per-fetch HTTP timeout. |
78
+ | `meta.modelsInfoHeaders` | _(none)_ | Extra request headers. Override `options.headers` on conflict. Included in the cache key, so a tenant switch busts the cache. |
79
+
80
+ ### Auth composition
81
+
82
+ The plugin sends the union of `options.headers` and `meta.modelsInfoHeaders` (meta wins on conflict). This makes three common setups work without configuration:
83
+
84
+ 1. **Public metadata endpoint** (e.g. OpenRouter's `/models`) — no auth needed.
85
+ 2. **Static API key** — drop a `Bearer` into `options.headers` once, both inference and metadata use it.
86
+ 3. **OAuth2 via [`@vymalo/opencode-oauth2`](../opencode-oauth2/README.md) ≥ 0.4.0** — that plugin stamps the cached bearer into `options.headers.Authorization` at config time so the metadata fetch inherits it automatically. The chat-time path still uses freshly-refreshed tokens.
87
+
88
+ If you need a different token for the metadata endpoint than for inference (e.g. a service-account bearer), set it explicitly under `meta.modelsInfoHeaders.Authorization` — it'll override whatever the provider has set.
89
+
90
+ ### Expected response shape (OpenRouter)
91
+
92
+ ```json
93
+ {
94
+ "data": [
95
+ {
96
+ "id": "model-a",
97
+ "name": "Model A",
98
+ "context_length": 128000,
99
+ "pricing": { "prompt": "0.000003", "completion": "0.000015" },
100
+ "architecture": { "input_modalities": ["text", "image"], "output_modalities": ["text"] },
101
+ "top_provider": { "max_completion_tokens": 4096 },
102
+ "supported_parameters": ["tools", "temperature", "reasoning"]
103
+ }
104
+ ]
105
+ }
106
+ ```
107
+
108
+ A bare top-level array (no `data` wrapper) is also accepted.
109
+
110
+ ### Field mapping
111
+
112
+ | OpenRouter | OpenCode |
113
+ | ------------------------------------------------------- | ------------------------- |
114
+ | `context_length` + `top_provider.max_completion_tokens` | `limit.context` / `limit.output` |
115
+ | `pricing.prompt` / `.completion` (USD/token) | `cost.input` / `cost.output` (USD per 1M tokens — converted) |
116
+ | `pricing.input_cache_read` / `.input_cache_write` | `cost.cache_read` / `cost.cache_write` |
117
+ | `architecture.input_modalities` / `.output_modalities` | `modalities.input` / `modalities.output` (filtered to OpenCode's enum) |
118
+ | `supported_parameters: ["tools" or "tool_choice"]` | `tool_call: true` |
119
+ | `supported_parameters: ["reasoning" / "thinking" / …]` | `reasoning: true` |
120
+ | `supported_parameters: ["temperature"]` | `temperature: true` |
121
+ | Non-text input modality present | `attachment: true` |
122
+ | `name` | `name` (if absent) |
123
+
124
+ ## Cache location
125
+
126
+ | OS | Path |
127
+ | ------- | -------------------------------------------------------------------- |
128
+ | macOS | `~/Library/Caches/opencode-models-info/` |
129
+ | Linux | `${XDG_CACHE_HOME:-~/.cache}/opencode-models-info/` |
130
+ | Windows | `%LOCALAPPDATA%\opencode-models-info\` |
131
+
132
+ Files are named by `sha256(providerId::url)`, `0o600`, atomic-rename-on-write.
133
+
134
+ ## Testing
135
+
136
+ Unit tests run against mocked `fetch`:
137
+
138
+ ```sh
139
+ pnpm --filter @vymalo/opencode-models-info test
140
+ ```
141
+
142
+ Integration tests run against a real HTTP server (WireMock) from the workspace's shared [`test-env/`](../../test-env/) compose stack. They skip themselves when `INTEGRATION_MODELS_INFO_URL` is unset:
143
+
144
+ ```sh
145
+ pnpm test:env:up # from repo root
146
+ pnpm --filter @vymalo/opencode-models-info test:integration
147
+ pnpm test:env:down
148
+
149
+ # Or one-shot from repo root: spin up, run all integration suites, tear down.
150
+ pnpm test:integration
151
+ ```
152
+
153
+ The integration suite exercises real network round-trips, ETag handling (`304 Not Modified`), `modelsInfoHeaders` propagation, and the disk cache — all against a fixed catalog fixture under [`test-env/wiremock/__files/openrouter-catalog.json`](../../test-env/wiremock/__files/openrouter-catalog.json).
154
+
155
+ ## Library API
156
+
157
+ For embedding the enrichment logic outside an OpenCode hook (e.g. tests or custom tooling), import from the `/lib` subpath:
158
+
159
+ ```ts
160
+ import { enrichConfig, FileCacheStore, createJsonConsoleLogger } from "@vymalo/opencode-models-info/lib";
161
+ ```
162
+
163
+ ## License
164
+
165
+ MIT
@@ -0,0 +1,33 @@
1
+ import type { CachedModelsRecord } from "./types.js";
2
+ export declare function resolveCacheDir(namespace?: string): string;
3
+ /**
4
+ * Cache key = sha256(providerId :: url :: stableJSON(headers)).
5
+ *
6
+ * Only the **caller-specified** headers (i.e. `meta.modelsInfoHeaders`) go
7
+ * into the key — NOT the provider's other request headers. Rationale: if a
8
+ * rotating bearer (e.g. from `@vymalo/opencode-oauth2`) were keyed in, the
9
+ * cache would thrash on every token refresh. Headers the user explicitly
10
+ * configures for the metadata fetch (tenant selectors, static auth, etc.)
11
+ * are exactly the ones that should bust the cache when they change.
12
+ */
13
+ export declare function cacheKey(providerId: string, url: string, headers?: Record<string, string>): string;
14
+ export interface CacheStore {
15
+ get(key: string): Promise<CachedModelsRecord | undefined>;
16
+ put(key: string, record: CachedModelsRecord): Promise<void>;
17
+ }
18
+ /**
19
+ * Two-layer cache: an in-memory map for the process lifetime, backed by JSON
20
+ * files on disk so cold starts reuse the last good snapshot. Disk writes are
21
+ * atomic via rename-after-write so a crashed process can't leave a torn file.
22
+ */
23
+ export declare class FileCacheStore implements CacheStore {
24
+ private readonly baseDir;
25
+ private readonly memory;
26
+ private ready;
27
+ constructor(baseDir?: string);
28
+ private ensureReady;
29
+ private filePath;
30
+ get(key: string): Promise<CachedModelsRecord | undefined>;
31
+ put(key: string, record: CachedModelsRecord): Promise<void>;
32
+ }
33
+ export declare function isExpired(record: CachedModelsRecord, now?: number): boolean;
package/dist/cache.js ADDED
@@ -0,0 +1,104 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ function resolveDefaultCacheRoot() {
6
+ if (process.platform === "win32") {
7
+ return process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local");
8
+ }
9
+ if (process.platform === "darwin") {
10
+ return join(homedir(), "Library", "Caches");
11
+ }
12
+ return process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache");
13
+ }
14
+ export function resolveCacheDir(namespace = "opencode-models-info") {
15
+ return join(resolveDefaultCacheRoot(), namespace);
16
+ }
17
+ /**
18
+ * Cache key = sha256(providerId :: url :: stableJSON(headers)).
19
+ *
20
+ * Only the **caller-specified** headers (i.e. `meta.modelsInfoHeaders`) go
21
+ * into the key — NOT the provider's other request headers. Rationale: if a
22
+ * rotating bearer (e.g. from `@vymalo/opencode-oauth2`) were keyed in, the
23
+ * cache would thrash on every token refresh. Headers the user explicitly
24
+ * configures for the metadata fetch (tenant selectors, static auth, etc.)
25
+ * are exactly the ones that should bust the cache when they change.
26
+ */
27
+ export function cacheKey(providerId, url, headers) {
28
+ const headerPart = headers ? stableStringify(headers) : "";
29
+ return createHash("sha256").update(`${providerId}::${url}::${headerPart}`).digest("hex");
30
+ }
31
+ function stableStringify(headers) {
32
+ const sorted = Object.keys(headers)
33
+ .sort()
34
+ .map((k) => [k.toLowerCase(), headers[k]]);
35
+ return JSON.stringify(sorted);
36
+ }
37
+ /**
38
+ * Two-layer cache: an in-memory map for the process lifetime, backed by JSON
39
+ * files on disk so cold starts reuse the last good snapshot. Disk writes are
40
+ * atomic via rename-after-write so a crashed process can't leave a torn file.
41
+ */
42
+ export class FileCacheStore {
43
+ baseDir;
44
+ memory = new Map();
45
+ ready;
46
+ constructor(baseDir = resolveCacheDir()) {
47
+ this.baseDir = baseDir;
48
+ }
49
+ async ensureReady() {
50
+ if (!this.ready) {
51
+ this.ready = mkdir(this.baseDir, { recursive: true, mode: 0o700 }).then(() => undefined);
52
+ }
53
+ await this.ready;
54
+ }
55
+ filePath(key) {
56
+ return join(this.baseDir, `${key}.json`);
57
+ }
58
+ async get(key) {
59
+ const memHit = this.memory.get(key);
60
+ if (memHit) {
61
+ return memHit;
62
+ }
63
+ try {
64
+ await this.ensureReady();
65
+ const raw = await readFile(this.filePath(key), "utf8");
66
+ const parsed = JSON.parse(raw);
67
+ if (!isValidRecord(parsed)) {
68
+ return undefined;
69
+ }
70
+ this.memory.set(key, parsed);
71
+ return parsed;
72
+ }
73
+ catch (error) {
74
+ if (isFileNotFound(error)) {
75
+ return undefined;
76
+ }
77
+ return undefined;
78
+ }
79
+ }
80
+ async put(key, record) {
81
+ this.memory.set(key, record);
82
+ await this.ensureReady();
83
+ const target = this.filePath(key);
84
+ const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
85
+ await writeFile(tmp, JSON.stringify(record), { mode: 0o600 });
86
+ await rename(tmp, target);
87
+ }
88
+ }
89
+ export function isExpired(record, now = Date.now()) {
90
+ return now - record.fetchedAt > record.ttlSeconds * 1000;
91
+ }
92
+ function isValidRecord(value) {
93
+ if (!value || typeof value !== "object") {
94
+ return false;
95
+ }
96
+ const record = value;
97
+ return (typeof record.fetchedAt === "number" &&
98
+ typeof record.ttlSeconds === "number" &&
99
+ Array.isArray(record.models));
100
+ }
101
+ function isFileNotFound(error) {
102
+ return Boolean(error && typeof error === "object" && error.code === "ENOENT");
103
+ }
104
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAIjC,SAAS,uBAAuB;IAC9B,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,OAAO,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;IACzE,CAAC;IACD,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAClC,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;IAC9C,CAAC;IACD,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,CAAC,CAAC;AACjE,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,SAAS,GAAG,sBAAsB;IAChE,OAAO,IAAI,CAAC,uBAAuB,EAAE,EAAE,SAAS,CAAC,CAAC;AACpD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,QAAQ,CACtB,UAAkB,EAClB,GAAW,EACX,OAAgC;IAEhC,MAAM,UAAU,GAAG,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC3D,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,UAAU,KAAK,GAAG,KAAK,UAAU,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC3F,CAAC;AAED,SAAS,eAAe,CAAC,OAA+B;IACtD,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC;SAChC,IAAI,EAAE;SACN,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC,CAAU,CAAC,CAAC;IACtD,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;AAChC,CAAC;AAOD;;;;GAIG;AACH,MAAM,OAAO,cAAc;IAII;IAHZ,MAAM,GAAG,IAAI,GAAG,EAA8B,CAAC;IACxD,KAAK,CAA4B;IAEzC,YAA6B,UAAkB,eAAe,EAAE;QAAnC,YAAO,GAAP,OAAO,CAA4B;IAAG,CAAC;IAE5D,KAAK,CAAC,WAAW;QACvB,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC3F,CAAC;QACD,MAAM,IAAI,CAAC,KAAK,CAAC;IACnB,CAAC;IAEO,QAAQ,CAAC,GAAW;QAC1B,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,CAAC;IAC3C,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW;QACnB,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACpC,IAAI,MAAM,EAAE,CAAC;YACX,OAAO,MAAM,CAAC;QAChB,CAAC;QACD,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;YACzB,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;YACvD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAuB,CAAC;YACrD,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC3B,OAAO,SAAS,CAAC;YACnB,CAAC;YACD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;YAC7B,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC1B,OAAO,SAAS,CAAC;YACnB,CAAC;YACD,OAAO,SAAS,CAAC;QACnB,CAAC;IACH,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,MAA0B;QAC/C,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAC7B,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QACzB,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAClC,MAAM,GAAG,GAAG,GAAG,MAAM,IAAI,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC;QACzD,MAAM,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC9D,MAAM,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAC5B,CAAC;CACF;AAED,MAAM,UAAU,SAAS,CAAC,MAA0B,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;IAC5E,OAAO,GAAG,GAAG,MAAM,CAAC,SAAS,GAAG,MAAM,CAAC,UAAU,GAAG,IAAI,CAAC;AAC3D,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACxC,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,MAAM,GAAG,KAAgC,CAAC;IAChD,OAAO,CACL,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ;QACpC,OAAO,MAAM,CAAC,UAAU,KAAK,QAAQ;QACrC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAC7B,CAAC;AACJ,CAAC;AAED,SAAS,cAAc,CAAC,KAAc;IACpC,OAAO,OAAO,CACZ,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAK,KAA2B,CAAC,IAAI,KAAK,QAAQ,CACrF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,27 @@
1
+ import type { MetaProviderOptions } from "./types.js";
2
+ export declare const DEFAULT_TTL_SECONDS = 86400;
3
+ export declare const DEFAULT_TIMEOUT_MS = 5000;
4
+ /**
5
+ * Parse a provider's `options.meta` for opt-in model-info fields. Returns
6
+ * `null` if the provider has not opted in (no `meta.modelsInfoUrl`).
7
+ *
8
+ * URL resolution follows the WHATWG URL spec when `modelsInfoUrl` is not
9
+ * absolute:
10
+ * - Absolute URL (`https://…`) → used as-is.
11
+ * - Path starting with `/` → resolves from the **origin**
12
+ * of `baseURL`. So with
13
+ * `baseURL: "https://x.test/v1"`
14
+ * and `modelsInfoUrl: "/models"`,
15
+ * you get `https://x.test/models`.
16
+ * Useful when your metadata
17
+ * endpoint sits at a different
18
+ * path than the inference API.
19
+ * - Path without leading `/` → resolves **relative to**
20
+ * `baseURL`. So with
21
+ * `baseURL: "https://x.test/v1"`
22
+ * and `modelsInfoUrl: "models"`,
23
+ * you get `https://x.test/v1/models`.
24
+ * Useful when metadata sits under
25
+ * the same path as inference.
26
+ */
27
+ export declare function parseMetaOptions(providerOptions: Record<string, unknown> | undefined): MetaProviderOptions | null;
package/dist/config.js ADDED
@@ -0,0 +1,97 @@
1
+ export const DEFAULT_TTL_SECONDS = 86_400;
2
+ export const DEFAULT_TIMEOUT_MS = 5_000;
3
+ const META_KEY = "meta";
4
+ function asRecord(value) {
5
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
6
+ return undefined;
7
+ }
8
+ return value;
9
+ }
10
+ function asString(value) {
11
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
12
+ }
13
+ function asStringMap(value) {
14
+ const record = asRecord(value);
15
+ if (!record) {
16
+ return undefined;
17
+ }
18
+ const out = {};
19
+ for (const [key, raw] of Object.entries(record)) {
20
+ if (typeof raw === "string" && raw.length > 0) {
21
+ out[key] = raw;
22
+ }
23
+ }
24
+ return Object.keys(out).length > 0 ? out : undefined;
25
+ }
26
+ function asPositiveInt(value, fallback) {
27
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
28
+ return Math.floor(value);
29
+ }
30
+ return fallback;
31
+ }
32
+ /**
33
+ * Parse a provider's `options.meta` for opt-in model-info fields. Returns
34
+ * `null` if the provider has not opted in (no `meta.modelsInfoUrl`).
35
+ *
36
+ * URL resolution follows the WHATWG URL spec when `modelsInfoUrl` is not
37
+ * absolute:
38
+ * - Absolute URL (`https://…`) → used as-is.
39
+ * - Path starting with `/` → resolves from the **origin**
40
+ * of `baseURL`. So with
41
+ * `baseURL: "https://x.test/v1"`
42
+ * and `modelsInfoUrl: "/models"`,
43
+ * you get `https://x.test/models`.
44
+ * Useful when your metadata
45
+ * endpoint sits at a different
46
+ * path than the inference API.
47
+ * - Path without leading `/` → resolves **relative to**
48
+ * `baseURL`. So with
49
+ * `baseURL: "https://x.test/v1"`
50
+ * and `modelsInfoUrl: "models"`,
51
+ * you get `https://x.test/v1/models`.
52
+ * Useful when metadata sits under
53
+ * the same path as inference.
54
+ */
55
+ export function parseMetaOptions(providerOptions) {
56
+ if (!providerOptions) {
57
+ return null;
58
+ }
59
+ const meta = asRecord(providerOptions[META_KEY]);
60
+ if (!meta) {
61
+ return null;
62
+ }
63
+ const rawUrl = asString(meta.modelsInfoUrl);
64
+ if (!rawUrl) {
65
+ return null;
66
+ }
67
+ const baseURL = asString(providerOptions.baseURL);
68
+ const modelsInfoUrl = resolveUrl(rawUrl, baseURL);
69
+ return {
70
+ modelsInfoUrl,
71
+ modelsInfoTtlSeconds: asPositiveInt(meta.modelsInfoTtlSeconds, DEFAULT_TTL_SECONDS),
72
+ modelsInfoTimeoutMs: asPositiveInt(meta.modelsInfoTimeoutMs, DEFAULT_TIMEOUT_MS),
73
+ modelsInfoHeaders: asStringMap(meta.modelsInfoHeaders),
74
+ modelsInfoFormat: "openrouter"
75
+ };
76
+ }
77
+ function resolveUrl(candidate, baseURL) {
78
+ if (/^https?:\/\//i.test(candidate)) {
79
+ return candidate;
80
+ }
81
+ if (!baseURL) {
82
+ return candidate;
83
+ }
84
+ // Always treat the baseURL as a directory by appending a trailing slash if
85
+ // it's missing. This way a path-relative `modelsInfoUrl` ("models/info")
86
+ // resolves under the baseURL's path instead of replacing its last segment
87
+ // (the WHATWG default). A leading-slash candidate ("/models/info") still
88
+ // resolves from the origin per spec.
89
+ const base = baseURL.endsWith("/") ? baseURL : `${baseURL}/`;
90
+ try {
91
+ return new URL(candidate, base).toString();
92
+ }
93
+ catch {
94
+ return candidate;
95
+ }
96
+ }
97
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { FetchModelsResult } from "./types.js";
2
+ export interface FetchOptions {
3
+ url: string;
4
+ headers?: Record<string, string>;
5
+ timeoutMs: number;
6
+ etag?: string;
7
+ fetchImpl?: typeof fetch;
8
+ }
9
+ /**
10
+ * GET the models-info endpoint and return either the parsed entries, a
11
+ * not-modified marker (when the server respects the supplied `If-None-Match`),
12
+ * or an error result. Never throws — the plugin must remain non-fatal.
13
+ */
14
+ export declare function fetchOpenRouterModels(opts: FetchOptions): Promise<FetchModelsResult>;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * GET the models-info endpoint and return either the parsed entries, a
3
+ * not-modified marker (when the server respects the supplied `If-None-Match`),
4
+ * or an error result. Never throws — the plugin must remain non-fatal.
5
+ */
6
+ export async function fetchOpenRouterModels(opts) {
7
+ const impl = opts.fetchImpl ?? fetch;
8
+ const controller = new AbortController();
9
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs);
10
+ try {
11
+ const headers = {
12
+ accept: "application/json",
13
+ ...(opts.headers ?? {})
14
+ };
15
+ if (opts.etag) {
16
+ headers["if-none-match"] = opts.etag;
17
+ }
18
+ const response = await impl(opts.url, {
19
+ method: "GET",
20
+ headers,
21
+ signal: controller.signal
22
+ });
23
+ if (response.status === 304) {
24
+ return { status: "not-modified", etag: opts.etag };
25
+ }
26
+ if (!response.ok) {
27
+ return { status: "error", error: `HTTP ${response.status}` };
28
+ }
29
+ const body = (await response.json());
30
+ const models = normalizeResponse(body);
31
+ if (!models) {
32
+ return { status: "error", error: "unexpected response shape" };
33
+ }
34
+ return {
35
+ status: "ok",
36
+ etag: response.headers.get("etag") ?? undefined,
37
+ models
38
+ };
39
+ }
40
+ catch (error) {
41
+ return {
42
+ status: "error",
43
+ error: error instanceof Error ? error.message : String(error)
44
+ };
45
+ }
46
+ finally {
47
+ clearTimeout(timer);
48
+ }
49
+ }
50
+ function normalizeResponse(body) {
51
+ if (Array.isArray(body)) {
52
+ return validateFiltered(body);
53
+ }
54
+ if (body && typeof body === "object") {
55
+ const data = body.data;
56
+ if (Array.isArray(data)) {
57
+ return validateFiltered(data);
58
+ }
59
+ }
60
+ return undefined;
61
+ }
62
+ /**
63
+ * Filter to entries with a string `id`, but reject the whole response if a
64
+ * non-empty input came in and every entry was filtered out — that's a parse
65
+ * error, not a legitimate empty catalog, and we don't want to overwrite a
66
+ * previously-good cache with []. An input that was empty to begin with is
67
+ * still a valid (if unusual) response.
68
+ */
69
+ function validateFiltered(input) {
70
+ const filtered = input.filter(isOpenRouterModel);
71
+ if (input.length > 0 && filtered.length === 0) {
72
+ return undefined;
73
+ }
74
+ return filtered;
75
+ }
76
+ function isOpenRouterModel(value) {
77
+ return (Boolean(value) &&
78
+ typeof value === "object" &&
79
+ typeof value.id === "string");
80
+ }
81
+ //# sourceMappingURL=fetcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetcher.js","sourceRoot":"","sources":["../src/fetcher.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export { default } from "./opencode.js";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ // OpenCode plugin entry. The host iterates every named export of this module
2
+ // and rejects any export that isn't a Plugin function (or { server: Plugin }).
3
+ // Library API is exposed via the "./lib" subpath in package.json.
4
+ export { default } from "./opencode.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,+EAA+E;AAC/E,kEAAkE;AAClE,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC"}
package/dist/lib.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ export { createOpencodeModelsInfoPlugin, OpencodeModelsInfoPlugin, type OpenCodePluginFactoryOptions } from "./opencode.js";
2
+ export { cacheKey, type CacheStore, FileCacheStore, isExpired, resolveCacheDir } from "./cache.js";
3
+ export { DEFAULT_TIMEOUT_MS, DEFAULT_TTL_SECONDS, parseMetaOptions } from "./config.js";
4
+ export { fetchOpenRouterModels, type FetchOptions } from "./fetcher.js";
5
+ export { createJsonConsoleLogger, DEFAULT_LOG_LEVEL, fromOpenCodeLogLevel, type LogFields, type Logger, type LogLevel } from "./logging.js";
6
+ export { mapOpenRouterEntry, mergeIntoModel, type ModelMetadata } from "./mapping.js";
7
+ export { type EnrichConfigInput, type EnrichDeps, enrichConfig, type ProviderConfigLike } from "./plugin.js";
8
+ export type { CachedModelsRecord, FetchModelsResult, MetaProviderOptions, OpenRouterArchitecture, OpenRouterModality, OpenRouterModel, OpenRouterModelsResponse, OpenRouterPricing, OpenRouterTopProvider } from "./types.js";
package/dist/lib.js ADDED
@@ -0,0 +1,8 @@
1
+ export { createOpencodeModelsInfoPlugin, OpencodeModelsInfoPlugin } from "./opencode.js";
2
+ export { cacheKey, FileCacheStore, isExpired, resolveCacheDir } from "./cache.js";
3
+ export { DEFAULT_TIMEOUT_MS, DEFAULT_TTL_SECONDS, parseMetaOptions } from "./config.js";
4
+ export { fetchOpenRouterModels } from "./fetcher.js";
5
+ export { createJsonConsoleLogger, DEFAULT_LOG_LEVEL, fromOpenCodeLogLevel } from "./logging.js";
6
+ export { mapOpenRouterEntry, mergeIntoModel } from "./mapping.js";
7
+ export { enrichConfig } from "./plugin.js";
8
+ //# sourceMappingURL=lib.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lib.js","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,8BAA8B,EAC9B,wBAAwB,EAEzB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,QAAQ,EAER,cAAc,EACd,SAAS,EACT,eAAe,EAChB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EACjB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,qBAAqB,EAAqB,MAAM,cAAc,CAAC;AAExE,OAAO,EACL,uBAAuB,EACvB,iBAAiB,EACjB,oBAAoB,EAIrB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,kBAAkB,EAClB,cAAc,EAEf,MAAM,cAAc,CAAC;AAEtB,OAAO,EAGL,YAAY,EAEb,MAAM,aAAa,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { LogLevel } from "./types.js";
2
+ export type { LogLevel } from "./types.js";
3
+ export declare const LOG_LEVEL_PRIORITY: Record<LogLevel, number>;
4
+ export declare const DEFAULT_LOG_LEVEL: LogLevel;
5
+ export interface LogFields {
6
+ [key: string]: unknown;
7
+ }
8
+ export interface Logger {
9
+ debug(event: string, fields?: LogFields): void;
10
+ info(event: string, fields?: LogFields): void;
11
+ warn(event: string, fields?: LogFields): void;
12
+ error(event: string, fields?: LogFields): void;
13
+ }
14
+ export declare function createJsonConsoleLogger(minLevel?: LogLevel): Logger;
15
+ export declare function fromOpenCodeLogLevel(value: unknown): LogLevel | undefined;
@@ -0,0 +1,69 @@
1
+ export const LOG_LEVEL_PRIORITY = {
2
+ debug: 10,
3
+ info: 20,
4
+ warn: 30,
5
+ error: 40
6
+ };
7
+ export const DEFAULT_LOG_LEVEL = "info";
8
+ function redactFields(fields) {
9
+ if (!fields) {
10
+ return undefined;
11
+ }
12
+ const redacted = {};
13
+ for (const [key, value] of Object.entries(fields)) {
14
+ if (/token|secret|password|authorization/i.test(key)) {
15
+ redacted[key] = "[redacted]";
16
+ continue;
17
+ }
18
+ redacted[key] = value;
19
+ }
20
+ return redacted;
21
+ }
22
+ export function createJsonConsoleLogger(minLevel = DEFAULT_LOG_LEVEL) {
23
+ const minPriority = LOG_LEVEL_PRIORITY[minLevel];
24
+ const write = (level, event, fields) => {
25
+ if (LOG_LEVEL_PRIORITY[level] < minPriority) {
26
+ return;
27
+ }
28
+ const payload = {
29
+ ts: new Date().toISOString(),
30
+ level,
31
+ event,
32
+ ...(redactFields(fields) ?? {})
33
+ };
34
+ const line = JSON.stringify(payload);
35
+ if (level === "error") {
36
+ console.error(line);
37
+ return;
38
+ }
39
+ if (level === "warn") {
40
+ console.warn(line);
41
+ return;
42
+ }
43
+ console.log(line);
44
+ };
45
+ return {
46
+ debug: (event, fields) => write("debug", event, fields),
47
+ info: (event, fields) => write("info", event, fields),
48
+ warn: (event, fields) => write("warn", event, fields),
49
+ error: (event, fields) => write("error", event, fields)
50
+ };
51
+ }
52
+ export function fromOpenCodeLogLevel(value) {
53
+ if (typeof value !== "string") {
54
+ return undefined;
55
+ }
56
+ switch (value.toUpperCase()) {
57
+ case "DEBUG":
58
+ return "debug";
59
+ case "INFO":
60
+ return "info";
61
+ case "WARN":
62
+ return "warn";
63
+ case "ERROR":
64
+ return "error";
65
+ default:
66
+ return undefined;
67
+ }
68
+ }
69
+ //# sourceMappingURL=logging.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logging.js","sourceRoot":"","sources":["../src/logging.ts"],"names":[],"mappings":"AAIA,MAAM,CAAC,MAAM,kBAAkB,GAA6B;IAC1D,KAAK,EAAE,EAAE;IACT,IAAI,EAAE,EAAE;IACR,IAAI,EAAE,EAAE;IACR,KAAK,EAAE,EAAE;CACV,CAAC;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAa,MAAM,CAAC;AAalD,SAAS,YAAY,CAAC,MAAkB;IACtC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,IAAI,sCAAsC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACrD,QAAQ,CAAC,GAAG,CAAC,GAAG,YAAY,CAAC;YAC7B,SAAS;QACX,CAAC;QACD,QAAQ,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IACxB,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,WAAqB,iBAAiB;IAC5E,MAAM,WAAW,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAEjD,MAAM,KAAK,GAAG,CAAC,KAAe,EAAE,KAAa,EAAE,MAAkB,EAAQ,EAAE;QACzE,IAAI,kBAAkB,CAAC,KAAK,CAAC,GAAG,WAAW,EAAE,CAAC;YAC5C,OAAO;QACT,CAAC;QACD,MAAM,OAAO,GAAG;YACd,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YAC5B,KAAK;YACL,KAAK;YACL,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;SAChC,CAAC;QACF,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QACrC,IAAI,KAAK,KAAK,OAAO,EAAE,CAAC;YACtB,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACpB,OAAO;QACT,CAAC;QACD,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;YACrB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACnB,OAAO;QACT,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACpB,CAAC,CAAC;IAEF,OAAO;QACL,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC;QACvD,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC;QACrD,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC;QACrD,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC;KACxD,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,KAAc;IACjD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,QAAQ,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;QAC5B,KAAK,OAAO;YACV,OAAO,OAAO,CAAC;QACjB,KAAK,MAAM;YACT,OAAO,MAAM,CAAC;QAChB,KAAK,MAAM;YACT,OAAO,MAAM,CAAC;QAChB,KAAK,OAAO;YACV,OAAO,OAAO,CAAC;QACjB;YACE,OAAO,SAAS,CAAC;IACrB,CAAC;AACH,CAAC"}
@@ -0,0 +1,36 @@
1
+ import type { OpenRouterModel } from "./types.js";
2
+ type OpenCodeModality = "text" | "audio" | "image" | "video" | "pdf";
3
+ export interface ModelMetadata {
4
+ name?: string;
5
+ attachment?: boolean;
6
+ reasoning?: boolean;
7
+ temperature?: boolean;
8
+ tool_call?: boolean;
9
+ cost?: {
10
+ input: number;
11
+ output: number;
12
+ cache_read?: number;
13
+ cache_write?: number;
14
+ };
15
+ limit?: {
16
+ context: number;
17
+ output: number;
18
+ };
19
+ modalities?: {
20
+ input: OpenCodeModality[];
21
+ output: OpenCodeModality[];
22
+ };
23
+ }
24
+ /**
25
+ * Pure transformation from an OpenRouter model entry to the subset of
26
+ * OpenCode `ModelConfig` fields we know how to populate. Returns only the
27
+ * fields we can derive; callers do the upstream-wins merge.
28
+ */
29
+ export declare function mapOpenRouterEntry(entry: OpenRouterModel): ModelMetadata;
30
+ /**
31
+ * Merge a derived metadata snapshot onto an existing OpenCode model entry.
32
+ * Upstream wins: any field already present is left untouched. Returns the
33
+ * same object reference (mutated) for ergonomic chaining.
34
+ */
35
+ export declare function mergeIntoModel<T extends Record<string, unknown>>(existing: T, derived: ModelMetadata): T;
36
+ export {};
@@ -0,0 +1,109 @@
1
+ const OPENCODE_MODALITIES = new Set(["text", "audio", "image", "video", "pdf"]);
2
+ /**
3
+ * Pure transformation from an OpenRouter model entry to the subset of
4
+ * OpenCode `ModelConfig` fields we know how to populate. Returns only the
5
+ * fields we can derive; callers do the upstream-wins merge.
6
+ */
7
+ export function mapOpenRouterEntry(entry) {
8
+ const out = {};
9
+ if (entry.name) {
10
+ out.name = entry.name;
11
+ }
12
+ const context = entry.top_provider?.context_length ?? entry.context_length;
13
+ const output = entry.top_provider?.max_completion_tokens;
14
+ if (typeof context === "number" && typeof output === "number") {
15
+ out.limit = { context, output };
16
+ }
17
+ else if (typeof context === "number") {
18
+ // OpenCode requires both fields when `limit` is set. Skip rather than fake.
19
+ }
20
+ const cost = mapPricing(entry.pricing);
21
+ if (cost) {
22
+ out.cost = cost;
23
+ }
24
+ const inputMods = filterModalities(entry.architecture?.input_modalities);
25
+ const outputMods = filterModalities(entry.architecture?.output_modalities);
26
+ if (inputMods.length > 0 && outputMods.length > 0) {
27
+ out.modalities = { input: inputMods, output: outputMods };
28
+ }
29
+ const params = entry.supported_parameters ?? [];
30
+ const paramSet = new Set(params.map((p) => p.toLowerCase()));
31
+ if (paramSet.has("tools") || paramSet.has("tool_choice")) {
32
+ out.tool_call = true;
33
+ }
34
+ if (paramSet.has("reasoning") || paramSet.has("reasoning_effort") || paramSet.has("thinking")) {
35
+ out.reasoning = true;
36
+ }
37
+ if (paramSet.has("temperature")) {
38
+ out.temperature = true;
39
+ }
40
+ if (inputMods.some((m) => m !== "text")) {
41
+ out.attachment = true;
42
+ }
43
+ return out;
44
+ }
45
+ function mapPricing(pricing) {
46
+ if (!pricing) {
47
+ return undefined;
48
+ }
49
+ const input = perMillion(pricing.prompt);
50
+ const output = perMillion(pricing.completion);
51
+ if (input === undefined || output === undefined) {
52
+ return undefined;
53
+ }
54
+ const cost = { input, output };
55
+ const cacheRead = perMillion(pricing.input_cache_read);
56
+ if (cacheRead !== undefined) {
57
+ cost.cache_read = cacheRead;
58
+ }
59
+ const cacheWrite = perMillion(pricing.input_cache_write);
60
+ if (cacheWrite !== undefined) {
61
+ cost.cache_write = cacheWrite;
62
+ }
63
+ return cost;
64
+ }
65
+ /** OpenRouter pricing is a string per-token in USD; OpenCode stores per-1M-token. */
66
+ function perMillion(raw) {
67
+ if (raw === undefined || raw === null || raw === "") {
68
+ return undefined;
69
+ }
70
+ const parsed = Number.parseFloat(raw);
71
+ if (!Number.isFinite(parsed)) {
72
+ return undefined;
73
+ }
74
+ return roundTo(parsed * 1_000_000, 6);
75
+ }
76
+ function roundTo(value, decimals) {
77
+ const factor = 10 ** decimals;
78
+ return Math.round(value * factor) / factor;
79
+ }
80
+ function filterModalities(values) {
81
+ if (!values) {
82
+ return [];
83
+ }
84
+ const out = [];
85
+ for (const value of values) {
86
+ if (OPENCODE_MODALITIES.has(value) &&
87
+ !out.includes(value)) {
88
+ out.push(value);
89
+ }
90
+ }
91
+ return out;
92
+ }
93
+ /**
94
+ * Merge a derived metadata snapshot onto an existing OpenCode model entry.
95
+ * Upstream wins: any field already present is left untouched. Returns the
96
+ * same object reference (mutated) for ergonomic chaining.
97
+ */
98
+ export function mergeIntoModel(existing, derived) {
99
+ for (const [key, value] of Object.entries(derived)) {
100
+ if (value === undefined) {
101
+ continue;
102
+ }
103
+ if (existing[key] === undefined) {
104
+ existing[key] = value;
105
+ }
106
+ }
107
+ return existing;
108
+ }
109
+ //# sourceMappingURL=mapping.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mapping.js","sourceRoot":"","sources":["../src/mapping.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ import { type CacheStore } from "./cache.js";
3
+ import { type Logger } from "./logging.js";
4
+ export interface OpenCodePluginFactoryOptions {
5
+ logger?: Logger;
6
+ fetchImpl?: typeof fetch;
7
+ cache?: CacheStore;
8
+ cacheDir?: string;
9
+ }
10
+ export declare function createOpencodeModelsInfoPlugin(factoryOptions?: OpenCodePluginFactoryOptions): Plugin;
11
+ export declare const OpencodeModelsInfoPlugin: Plugin;
12
+ export default OpencodeModelsInfoPlugin;
@@ -0,0 +1,56 @@
1
+ import { FileCacheStore } from "./cache.js";
2
+ import { createJsonConsoleLogger, DEFAULT_LOG_LEVEL, fromOpenCodeLogLevel, LOG_LEVEL_PRIORITY } from "./logging.js";
3
+ import { enrichConfig } from "./plugin.js";
4
+ const PLUGIN_SERVICE_NAME = "opencode-models-info-plugin";
5
+ /**
6
+ * Pipe plugin logs through OpenCode's `client.app.log` so they show up in the
7
+ * host's structured log stream, with the JSON console as a reliable fallback.
8
+ * Mirrors the pattern used by `@vymalo/opencode-oauth2`.
9
+ */
10
+ function createOpenCodeLogger(client, getMinLevel) {
11
+ const fallback = createJsonConsoleLogger("debug");
12
+ const write = (level, event, fields) => {
13
+ if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[getMinLevel()]) {
14
+ return;
15
+ }
16
+ fallback[level](event, fields);
17
+ void client.app
18
+ .log({
19
+ body: {
20
+ service: PLUGIN_SERVICE_NAME,
21
+ level,
22
+ message: event,
23
+ extra: fields
24
+ }
25
+ })
26
+ .catch(() => {
27
+ /* best-effort */
28
+ });
29
+ };
30
+ return {
31
+ debug: (event, fields) => write("debug", event, fields),
32
+ info: (event, fields) => write("info", event, fields),
33
+ warn: (event, fields) => write("warn", event, fields),
34
+ error: (event, fields) => write("error", event, fields)
35
+ };
36
+ }
37
+ export function createOpencodeModelsInfoPlugin(factoryOptions = {}) {
38
+ return async ({ client }) => {
39
+ let currentLogLevel = DEFAULT_LOG_LEVEL;
40
+ const logger = factoryOptions.logger ?? createOpenCodeLogger(client, () => currentLogLevel);
41
+ const cache = factoryOptions.cache ?? new FileCacheStore(factoryOptions.cacheDir);
42
+ return {
43
+ config: async (config) => {
44
+ currentLogLevel = fromOpenCodeLogLevel(config.logLevel) ?? DEFAULT_LOG_LEVEL;
45
+ await enrichConfig(config, {
46
+ cache,
47
+ logger,
48
+ fetchImpl: factoryOptions.fetchImpl
49
+ });
50
+ }
51
+ };
52
+ };
53
+ }
54
+ export const OpencodeModelsInfoPlugin = createOpencodeModelsInfoPlugin();
55
+ export default OpencodeModelsInfoPlugin;
56
+ //# sourceMappingURL=opencode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencode.js","sourceRoot":"","sources":["../src/opencode.ts"],"names":[],"mappings":"AAEA,OAAO,EAAmB,cAAc,EAAE,MAAM,YAAY,CAAC;AAC7D,OAAO,EACL,uBAAuB,EACvB,iBAAiB,EACjB,oBAAoB,EAEpB,kBAAkB,EAGnB,MAAM,cAAc,CAAC;AACtB,OAAO,EAA0B,YAAY,EAAE,MAAM,aAAa,CAAC;AAEnE,MAAM,mBAAmB,GAAG,6BAA6B,CAAC;AAW1D;;;;GAIG;AACH,SAAS,oBAAoB,CAAC,MAA6B,EAAE,WAA2B;IACtF,MAAM,QAAQ,GAAG,uBAAuB,CAAC,OAAO,CAAC,CAAC;IAElD,MAAM,KAAK,GAAG,CAAC,KAAe,EAAE,KAAa,EAAE,MAAkB,EAAE,EAAE;QACnE,IAAI,kBAAkB,CAAC,KAAK,CAAC,GAAG,kBAAkB,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;YAClE,OAAO;QACT,CAAC;QACD,QAAQ,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QAC/B,KAAK,MAAM,CAAC,GAAG;aACZ,GAAG,CAAC;YACH,IAAI,EAAE;gBACJ,OAAO,EAAE,mBAAmB;gBAC5B,KAAK;gBACL,OAAO,EAAE,KAAK;gBACd,KAAK,EAAE,MAAM;aACd;SACF,CAAC;aACD,KAAK,CAAC,GAAG,EAAE;YACV,iBAAiB;QACnB,CAAC,CAAC,CAAC;IACP,CAAC,CAAC;IAEF,OAAO;QACL,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC;QACvD,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC;QACrD,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC;QACrD,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC;KACxD,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,8BAA8B,CAC5C,iBAA+C,EAAE;IAEjD,OAAO,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE;QAC1B,IAAI,eAAe,GAAa,iBAAiB,CAAC;QAClD,MAAM,MAAM,GAAG,cAAc,CAAC,MAAM,IAAI,oBAAoB,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,eAAe,CAAC,CAAC;QAC5F,MAAM,KAAK,GAAG,cAAc,CAAC,KAAK,IAAI,IAAI,cAAc,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;QAElF,OAAO;YACL,MAAM,EAAE,KAAK,EAAE,MAAsB,EAAE,EAAE;gBACvC,eAAe,GAAG,oBAAoB,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,iBAAiB,CAAC;gBAC7E,MAAM,YAAY,CAAC,MAA2B,EAAE;oBAC9C,KAAK;oBACL,MAAM;oBACN,SAAS,EAAE,cAAc,CAAC,SAAS;iBACpC,CAAC,CAAC;YACL,CAAC;SACF,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,wBAAwB,GAAG,8BAA8B,EAAE,CAAC;AAEzE,eAAe,wBAAwB,CAAC"}
@@ -0,0 +1,24 @@
1
+ import { type CacheStore, FileCacheStore } from "./cache.js";
2
+ import type { Logger } from "./logging.js";
3
+ export type ProviderOptions = Record<string, unknown> | undefined;
4
+ export interface ProviderConfigLike {
5
+ options?: Record<string, unknown>;
6
+ models?: Record<string, Record<string, unknown>>;
7
+ }
8
+ export interface EnrichConfigInput {
9
+ provider?: Record<string, ProviderConfigLike>;
10
+ }
11
+ export interface EnrichDeps {
12
+ cache: CacheStore;
13
+ logger: Logger;
14
+ fetchImpl?: typeof fetch;
15
+ now?: () => number;
16
+ }
17
+ /**
18
+ * Walk every provider in the assembled OpenCode config, fetch its
19
+ * `meta.modelsInfoUrl` (if any) — honoring the cache — and merge derived
20
+ * metadata onto each matching model entry. Runs providers in parallel; one
21
+ * failure never blocks others.
22
+ */
23
+ export declare function enrichConfig(input: EnrichConfigInput, deps: EnrichDeps): Promise<void>;
24
+ export { FileCacheStore };
package/dist/plugin.js ADDED
@@ -0,0 +1,180 @@
1
+ import { cacheKey, FileCacheStore, isExpired } from "./cache.js";
2
+ import { parseMetaOptions } from "./config.js";
3
+ import { fetchOpenRouterModels } from "./fetcher.js";
4
+ import { mapOpenRouterEntry, mergeIntoModel } from "./mapping.js";
5
+ /**
6
+ * Walk every provider in the assembled OpenCode config, fetch its
7
+ * `meta.modelsInfoUrl` (if any) — honoring the cache — and merge derived
8
+ * metadata onto each matching model entry. Runs providers in parallel; one
9
+ * failure never blocks others.
10
+ */
11
+ export async function enrichConfig(input, deps) {
12
+ const providers = input.provider;
13
+ if (!providers) {
14
+ return;
15
+ }
16
+ await Promise.allSettled(Object.entries(providers).map(([providerId, providerConfig]) => enrichProvider(providerId, providerConfig, deps)));
17
+ }
18
+ async function enrichProvider(providerId, providerConfig, deps) {
19
+ try {
20
+ if (!providerConfig) {
21
+ return;
22
+ }
23
+ const opts = parseMetaOptions(providerConfig.options);
24
+ if (!opts) {
25
+ return;
26
+ }
27
+ const models = providerConfig.models;
28
+ if (!models || Object.keys(models).length === 0) {
29
+ deps.logger.debug("models_info_provider_skipped_no_models", { providerId });
30
+ return;
31
+ }
32
+ // Pull whatever headers the upstream config (oauth2 plugin, static API
33
+ // key, etc.) has already attached to the provider; the meta-specific
34
+ // `modelsInfoHeaders` win on conflict. This is what makes the plugin
35
+ // truly auth-agnostic — we never need to know how the token was acquired.
36
+ const providerHeaders = asHeaderMap(providerConfig.options?.headers);
37
+ const record = await loadRecord(providerId, opts, providerHeaders, deps);
38
+ if (!record) {
39
+ return;
40
+ }
41
+ const byId = new Map(record.models.map((m) => [m.id, m]));
42
+ let enrichedCount = 0;
43
+ for (const [modelId, modelConfig] of Object.entries(models)) {
44
+ const declaredId = typeof modelConfig.id === "string" ? modelConfig.id : undefined;
45
+ const match = byId.get(modelId) ?? (declaredId ? byId.get(declaredId) : undefined);
46
+ if (!match) {
47
+ continue;
48
+ }
49
+ const derived = mapOpenRouterEntry(match);
50
+ mergeIntoModel(modelConfig, derived);
51
+ enrichedCount += 1;
52
+ }
53
+ deps.logger.info("models_info_enriched", {
54
+ providerId,
55
+ enrichedCount,
56
+ totalModels: Object.keys(models).length,
57
+ sourceModels: record.models.length
58
+ });
59
+ }
60
+ catch (error) {
61
+ // Promise.allSettled would otherwise swallow this — surface it loudly so
62
+ // a broken cache disk or mapping bug isn't silently no-op'd per provider.
63
+ deps.logger.error("models_info_enrichment_failed", {
64
+ providerId,
65
+ error: error instanceof Error ? error.message : String(error)
66
+ });
67
+ }
68
+ }
69
+ async function loadRecord(providerId, opts, providerHeaders, deps) {
70
+ // Cache key is keyed on the user-specified `modelsInfoHeaders` (NOT the
71
+ // provider's rotating auth header) — so switching tenants busts the cache,
72
+ // but an OAuth2 token rotation does not thrash it. See cacheKey() docstring.
73
+ const key = cacheKey(providerId, opts.modelsInfoUrl, opts.modelsInfoHeaders);
74
+ const now = deps.now ? deps.now() : Date.now();
75
+ const cached = await deps.cache.get(key);
76
+ if (cached && !isExpired(cached, now)) {
77
+ deps.logger.debug("models_info_cache_hit", {
78
+ providerId,
79
+ url: opts.modelsInfoUrl,
80
+ ageMs: now - cached.fetchedAt
81
+ });
82
+ return cached;
83
+ }
84
+ const headers = buildFetchHeaders(opts, providerHeaders);
85
+ const result = await fetchOpenRouterModels({
86
+ url: opts.modelsInfoUrl,
87
+ headers,
88
+ timeoutMs: opts.modelsInfoTimeoutMs,
89
+ etag: cached?.etag,
90
+ fetchImpl: deps.fetchImpl
91
+ });
92
+ if (result.status === "ok" && result.models) {
93
+ const next = {
94
+ fetchedAt: now,
95
+ ttlSeconds: opts.modelsInfoTtlSeconds,
96
+ etag: result.etag,
97
+ models: result.models
98
+ };
99
+ // Disk write is best-effort — a read-only $HOME / cache dir shouldn't
100
+ // make us throw away a perfectly good fresh response.
101
+ await safePut(deps, key, next, providerId, opts.modelsInfoUrl);
102
+ deps.logger.info("models_info_fetched", {
103
+ providerId,
104
+ url: opts.modelsInfoUrl,
105
+ count: result.models.length
106
+ });
107
+ return next;
108
+ }
109
+ if (result.status === "not-modified" && cached) {
110
+ // Apply the CURRENT TTL from config — a tightened TTL in opencode.json
111
+ // should take effect on the next revalidation, not on the next full
112
+ // 200 fetch (which might be 24h away).
113
+ const refreshed = {
114
+ ...cached,
115
+ fetchedAt: now,
116
+ ttlSeconds: opts.modelsInfoTtlSeconds
117
+ };
118
+ await safePut(deps, key, refreshed, providerId, opts.modelsInfoUrl);
119
+ deps.logger.debug("models_info_not_modified", {
120
+ providerId,
121
+ url: opts.modelsInfoUrl
122
+ });
123
+ return refreshed;
124
+ }
125
+ if (cached) {
126
+ deps.logger.warn("models_info_fetch_failed_using_stale", {
127
+ providerId,
128
+ url: opts.modelsInfoUrl,
129
+ error: result.error,
130
+ ageMs: now - cached.fetchedAt
131
+ });
132
+ return cached;
133
+ }
134
+ deps.logger.warn("models_info_fetch_failed_no_cache", {
135
+ providerId,
136
+ url: opts.modelsInfoUrl,
137
+ error: result.error
138
+ });
139
+ return undefined;
140
+ }
141
+ /**
142
+ * Merge the provider's resolved request headers with the meta-specific
143
+ * `modelsInfoHeaders`. Meta wins on conflict so a user can override e.g. a
144
+ * dynamic `Authorization` header for the metadata endpoint specifically.
145
+ */
146
+ function buildFetchHeaders(opts, providerHeaders) {
147
+ if (!providerHeaders && !opts.modelsInfoHeaders) {
148
+ return undefined;
149
+ }
150
+ return {
151
+ ...(providerHeaders ?? {}),
152
+ ...(opts.modelsInfoHeaders ?? {})
153
+ };
154
+ }
155
+ async function safePut(deps, key, record, providerId, url) {
156
+ try {
157
+ await deps.cache.put(key, record);
158
+ }
159
+ catch (error) {
160
+ deps.logger.warn("models_info_cache_write_failed", {
161
+ providerId,
162
+ url,
163
+ error: error instanceof Error ? error.message : String(error)
164
+ });
165
+ }
166
+ }
167
+ function asHeaderMap(value) {
168
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
169
+ return undefined;
170
+ }
171
+ const out = {};
172
+ for (const [k, v] of Object.entries(value)) {
173
+ if (typeof v === "string" && v.length > 0) {
174
+ out[k] = v;
175
+ }
176
+ }
177
+ return Object.keys(out).length > 0 ? out : undefined;
178
+ }
179
+ export { FileCacheStore };
180
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ export type LogLevel = "debug" | "info" | "warn" | "error";
2
+ export interface OpenRouterPricing {
3
+ prompt?: string;
4
+ completion?: string;
5
+ request?: string;
6
+ image?: string;
7
+ input_cache_read?: string;
8
+ input_cache_write?: string;
9
+ }
10
+ export type OpenRouterModality = "text" | "image" | "audio" | "video" | "pdf" | "file";
11
+ export interface OpenRouterArchitecture {
12
+ input_modalities?: OpenRouterModality[];
13
+ output_modalities?: OpenRouterModality[];
14
+ modality?: string;
15
+ tokenizer?: string;
16
+ }
17
+ export interface OpenRouterTopProvider {
18
+ max_completion_tokens?: number;
19
+ context_length?: number;
20
+ }
21
+ export interface OpenRouterModel {
22
+ id: string;
23
+ name?: string;
24
+ context_length?: number;
25
+ pricing?: OpenRouterPricing;
26
+ architecture?: OpenRouterArchitecture;
27
+ top_provider?: OpenRouterTopProvider;
28
+ supported_parameters?: string[];
29
+ }
30
+ export interface OpenRouterModelsResponse {
31
+ data: OpenRouterModel[];
32
+ }
33
+ export interface MetaProviderOptions {
34
+ modelsInfoUrl: string;
35
+ modelsInfoTtlSeconds: number;
36
+ modelsInfoTimeoutMs: number;
37
+ modelsInfoHeaders?: Record<string, string>;
38
+ modelsInfoFormat: "openrouter";
39
+ }
40
+ export interface CachedModelsRecord {
41
+ fetchedAt: number;
42
+ ttlSeconds: number;
43
+ etag?: string;
44
+ models: OpenRouterModel[];
45
+ }
46
+ export interface FetchModelsResult {
47
+ status: "ok" | "not-modified" | "error";
48
+ etag?: string;
49
+ models?: OpenRouterModel[];
50
+ error?: string;
51
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@vymalo/opencode-models-info",
3
+ "version": "0.1.0",
4
+ "description": "OpenCode plugin that enriches model entries with full metadata (context length, pricing, modalities, capability flags) fetched from a provider-supplied OpenRouter-shaped endpoint.",
5
+ "license": "MIT",
6
+ "author": "vymalo contributors",
7
+ "homepage": "https://github.com/vymalo/opencode-oauth2#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/vymalo/opencode-oauth2.git",
11
+ "directory": "packages/opencode-models-info"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/vymalo/opencode-oauth2/issues"
15
+ },
16
+ "keywords": [
17
+ "opencode",
18
+ "opencode-plugin",
19
+ "models",
20
+ "metadata",
21
+ "openrouter",
22
+ "ai-sdk"
23
+ ],
24
+ "type": "module",
25
+ "main": "dist/index.js",
26
+ "types": "dist/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js"
31
+ },
32
+ "./lib": {
33
+ "types": "./dist/lib.d.ts",
34
+ "import": "./dist/lib.js"
35
+ },
36
+ "./package.json": "./package.json"
37
+ },
38
+ "sideEffects": false,
39
+ "files": [
40
+ "dist"
41
+ ],
42
+ "engines": {
43
+ "node": ">=22"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "dependencies": {
49
+ "@opencode-ai/plugin": "1.15.10"
50
+ },
51
+ "devDependencies": {
52
+ "vite": "^8.0.14",
53
+ "vitest": "^4.1.7"
54
+ },
55
+ "scripts": {
56
+ "build": "tsc -p tsconfig.json",
57
+ "lint": "biome lint .",
58
+ "typecheck": "tsc -p tsconfig.json --noEmit",
59
+ "test": "vitest run",
60
+ "test:integration": "vitest run --config vitest.integration.config.ts",
61
+ "format": "biome format --write .",
62
+ "format:check": "biome format ."
63
+ }
64
+ }