@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 +21 -0
- package/README.md +165 -0
- package/dist/cache.d.ts +33 -0
- package/dist/cache.js +104 -0
- package/dist/cache.js.map +1 -0
- package/dist/config.d.ts +27 -0
- package/dist/config.js +97 -0
- package/dist/config.js.map +1 -0
- package/dist/fetcher.d.ts +14 -0
- package/dist/fetcher.js +81 -0
- package/dist/fetcher.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/lib.d.ts +8 -0
- package/dist/lib.js +8 -0
- package/dist/lib.js.map +1 -0
- package/dist/logging.d.ts +15 -0
- package/dist/logging.js +69 -0
- package/dist/logging.js.map +1 -0
- package/dist/mapping.d.ts +36 -0
- package/dist/mapping.js +109 -0
- package/dist/mapping.js.map +1 -0
- package/dist/opencode.d.ts +12 -0
- package/dist/opencode.js +56 -0
- package/dist/opencode.js.map +1 -0
- package/dist/plugin.d.ts +24 -0
- package/dist/plugin.js +180 -0
- package/dist/plugin.js.map +1 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +64 -0
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
|
package/dist/cache.d.ts
ADDED
|
@@ -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":"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"}
|
package/dist/config.d.ts
ADDED
|
@@ -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>;
|
package/dist/fetcher.js
ADDED
|
@@ -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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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
package/dist/lib.js.map
ADDED
|
@@ -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;
|
package/dist/logging.js
ADDED
|
@@ -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 {};
|
package/dist/mapping.js
ADDED
|
@@ -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;
|
package/dist/opencode.js
ADDED
|
@@ -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"}
|
package/dist/plugin.d.ts
ADDED
|
@@ -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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"}
|
package/dist/types.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
+
}
|