@vymalo/opencode-models-info 0.11.0 → 0.14.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/README.md CHANGED
@@ -90,6 +90,7 @@ This warms the cache for the **next** `config` run (the next launch, the next wi
90
90
  | `meta.modelsInfoOverwrite` | _(none)_ | Array of field names (`name`, `attachment`, `reasoning`, `temperature`, `tool_call`, `cost`, `limit`, `modalities`) that the endpoint is allowed to overwrite even when already set. Opts those fields out of upstream-wins — see [Forcing endpoint values to win](#forcing-endpoint-values-to-win). Unknown names are ignored. |
91
91
  | `meta.modelsInfoHideTextOnly` | `false` | When `true`, makes the `modelsInfoUrl` catalog authoritative for which models are shown: drops a model from `provider.models` entirely if the catalog reports it as text-in/text-out only, **or** if the catalog doesn't mention it at all — see [Hiding text-only models](#hiding-text-only-models). |
92
92
  | `meta.modelsInfoHideInternal` | `false` | When `true`, drops a model from `provider.models` if the catalog reports a non-standard `internal: true` field for it. Independent of `modelsInfoHideTextOnly` — modality and internal/restricted status are unrelated signals — see [Hiding internal models](#hiding-internal-models). |
93
+ | `meta.modelsInfoHideUnmatched` | `false` | When `true`, drops a model from `provider.models` if the catalog has no entry for it at all — the membership half of `modelsInfoHideTextOnly`, without its modality filtering. Either flag alone triggers the same deletion — see [Requiring a catalog entry](#requiring-a-catalog-entry). |
93
94
 
94
95
  ### Auth composition
95
96
 
@@ -174,6 +175,29 @@ This is a hard delete, not a flag — a hidden model can't be selected at all, t
174
175
 
175
176
  > **Composes with `modelsInfoOverwrite`.** Turning on `modelsInfoHideTextOnly` doesn't change how surviving models are merged — upstream-wins (or your `modelsInfoOverwrite` list) still governs individual fields. It only changes which model entries survive to be merged at all.
176
177
 
178
+ ### Requiring a catalog entry
179
+
180
+ Case 2 above — dropping a model the catalog doesn't mention at all — is useful on its own, without the modality filtering that comes bundled into `modelsInfoHideTextOnly`. Set `meta.modelsInfoHideUnmatched: true` to get just that:
181
+
182
+ ```jsonc
183
+ {
184
+ "options": {
185
+ "meta": {
186
+ "modelsInfoUrl": "models/info",
187
+ "modelsInfoHideUnmatched": true
188
+ }
189
+ }
190
+ }
191
+ ```
192
+
193
+ This exists because a real adopter needed catalog-authoritative *membership* — a client's `provider.models` can carry ids the catalog has since dropped (renamed, removed, access-scoped, or just stale local state a client picked up a while ago) — but `modelsInfoHideTextOnly`'s modality filtering was hiding legitimate text-only external models right along with the stale ones.
194
+
195
+ - A model with no catalog entry is hidden, regardless of modality.
196
+ - A matched text-only model is left alone by this flag on its own (it's still subject to `modelsInfoHideTextOnly`'s modality check if that flag is *also* set).
197
+ - `modelsInfoHideTextOnly` and `modelsInfoHideUnmatched` reach the same deletion for an unmatched model — either alone is enough, setting both is redundant, not additive.
198
+
199
+ Don't reach for `modelsInfoHideTextOnly` just to get this behavior if you don't also want modality filtering — that's exactly the trap `modelsInfoHideInternal` was built to avoid for the internal/access-scope axis (see below). Logs at debug (`models_info_model_hidden_unmatched`, shared with `modelsInfoHideTextOnly`'s own unmatched-hiding).
200
+
177
201
  ### Hiding internal models
178
202
 
179
203
  If your catalog serves both externally-usable and internal-only/restricted models — models that exist on the backend but shouldn't be selectable in OpenCode — flag them explicitly and set `meta.modelsInfoHideInternal: true`:
@@ -201,7 +225,7 @@ Your catalog entry adds a non-standard `internal: true` field:
201
225
  - `internal` absent or `false` → never hidden by this flag, regardless of modality.
202
226
  - Unknown (`internal` field absent from the catalog entry) is left alone, not treated as `false` — same "known before we assert" rule as everywhere else in this plugin.
203
227
 
204
- The two flags compose freely; set whichever your catalog's semantics call for, or both. Note that `modelsInfoHideInternal` does **not** extend case 2 above (dropping a model your catalog has no entry for at all) — that stays governed solely by `modelsInfoHideTextOnly`, since an unmatched model's status is unknown, not "internal." Logs at debug (`models_info_model_hidden_internal`).
228
+ All three flags compose freely; set whichever your catalog's semantics call for. Note that `modelsInfoHideInternal` does **not** extend case 2 above (dropping a model your catalog has no entry for at all) — that's governed by `modelsInfoHideTextOnly` or `modelsInfoHideUnmatched` (see above), since an unmatched model's status is unknown, not "internal." Logs at debug (`models_info_model_hidden_internal`).
205
229
 
206
230
  ### Expected response shape (OpenRouter)
207
231
 
package/dist/cache.d.ts CHANGED
@@ -1,33 +1,33 @@
1
1
  import type { CachedModelsRecord } from "./types.js";
2
2
  export declare function resolveCacheDir(namespace?: string): string;
3
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
- */
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
13
  export declare function cacheKey(providerId: string, url: string, headers?: Record<string, string>): string;
14
14
  export interface CacheStore {
15
- get(key: string): Promise<CachedModelsRecord | undefined>;
16
- put(key: string, record: CachedModelsRecord): Promise<void>;
15
+ get(key: string): Promise<CachedModelsRecord | undefined>;
16
+ put(key: string, record: CachedModelsRecord): Promise<void>;
17
17
  }
18
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
- */
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
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>;
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
32
  }
33
33
  export declare function isExpired(record: CachedModelsRecord, now?: number): boolean;
package/dist/cache.js CHANGED
@@ -3,112 +3,110 @@ import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
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");
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
13
  }
14
14
  export function resolveCacheDir(namespace = "opencode-models-info") {
15
- return join(resolveDefaultCacheRoot(), namespace);
15
+ return join(resolveDefaultCacheRoot(), namespace);
16
16
  }
17
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
- */
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
27
  export function cacheKey(providerId, url, headers) {
28
- const headerPart = headers ? stableStringify(headers) : "";
29
- return createHash("sha256").update(`${providerId}::${url}::${headerPart}`).digest("hex");
28
+ const headerPart = headers ? stableStringify(headers) : "";
29
+ return createHash("sha256").update(`${providerId}::${url}::${headerPart}`).digest("hex");
30
30
  }
31
31
  function stableStringify(headers) {
32
- const sorted = Object.keys(headers)
33
- .sort()
34
- .map((k) => [k.toLowerCase(), headers[k]]);
35
- return JSON.stringify(sorted);
32
+ const sorted = Object.keys(headers).sort().map((k) => [k.toLowerCase(), headers[k]]);
33
+ return JSON.stringify(sorted);
36
34
  }
37
35
  /**
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
- */
36
+ * Two-layer cache: an in-memory map for the process lifetime, backed by JSON
37
+ * files on disk so cold starts reuse the last good snapshot. Disk writes are
38
+ * atomic via rename-after-write so a crashed process can't leave a torn file.
39
+ */
42
40
  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
- // Unique per-write temp name (pid + uuid) so concurrent opencode instances
85
- // or two enrich passes in one process racing the same key — never collide
86
- // on the temp file and trip an ENOENT on rename. See opencode-oauth2's
87
- // saveServerState for the full rationale.
88
- const tmp = `${target}.${process.pid}.${randomUUID()}.tmp`;
89
- try {
90
- await writeFile(tmp, JSON.stringify(record), { mode: 0o600 });
91
- await rename(tmp, target);
92
- }
93
- catch (error) {
94
- await unlink(tmp).catch(() => { });
95
- throw error;
96
- }
97
- }
41
+ baseDir;
42
+ memory = new Map();
43
+ ready;
44
+ constructor(baseDir = resolveCacheDir()) {
45
+ this.baseDir = baseDir;
46
+ }
47
+ async ensureReady() {
48
+ if (!this.ready) {
49
+ this.ready = mkdir(this.baseDir, {
50
+ recursive: true,
51
+ mode: 448
52
+ }).then(() => undefined);
53
+ }
54
+ await this.ready;
55
+ }
56
+ filePath(key) {
57
+ return join(this.baseDir, `${key}.json`);
58
+ }
59
+ async get(key) {
60
+ const memHit = this.memory.get(key);
61
+ if (memHit) {
62
+ return memHit;
63
+ }
64
+ try {
65
+ await this.ensureReady();
66
+ const raw = await readFile(this.filePath(key), "utf8");
67
+ const parsed = JSON.parse(raw);
68
+ if (!isValidRecord(parsed)) {
69
+ return undefined;
70
+ }
71
+ this.memory.set(key, parsed);
72
+ return parsed;
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
+ // Unique per-write temp name (pid + uuid) so concurrent opencode instances
85
+ // or two enrich passes in one process racing the same key — never collide
86
+ // on the temp file and trip an ENOENT on rename. See opencode-oauth2's
87
+ // saveServerState for the full rationale.
88
+ const tmp = `${target}.${process.pid}.${randomUUID()}.tmp`;
89
+ try {
90
+ await writeFile(tmp, JSON.stringify(record), { mode: 384 });
91
+ await rename(tmp, target);
92
+ } catch (error) {
93
+ await unlink(tmp).catch(() => {});
94
+ throw error;
95
+ }
96
+ }
98
97
  }
99
98
  export function isExpired(record, now = Date.now()) {
100
- return now - record.fetchedAt > record.ttlSeconds * 1000;
99
+ return now - record.fetchedAt > record.ttlSeconds * 1e3;
101
100
  }
102
101
  function isValidRecord(value) {
103
- if (!value || typeof value !== "object") {
104
- return false;
105
- }
106
- const record = value;
107
- return (typeof record.fetchedAt === "number" &&
108
- typeof record.ttlSeconds === "number" &&
109
- Array.isArray(record.models));
102
+ if (!value || typeof value !== "object") {
103
+ return false;
104
+ }
105
+ const record = value;
106
+ return typeof record.fetchedAt === "number" && typeof record.ttlSeconds === "number" && Array.isArray(record.models);
110
107
  }
111
108
  function isFileNotFound(error) {
112
- return Boolean(error && typeof error === "object" && error.code === "ENOENT");
109
+ return Boolean(error && typeof error === "object" && error.code === "ENOENT");
113
110
  }
111
+
114
112
  //# sourceMappingURL=cache.js.map
package/dist/cache.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}
1
+ {"mappings":"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","names":[],"sources":["../src/cache.ts"],"version":3,"file":"cache.js","sourceRoot":""}
package/dist/config.d.ts CHANGED
@@ -1,27 +1,27 @@
1
1
  import type { MetaProviderOptions } from "./types.js";
2
2
  export declare const DEFAULT_TTL_SECONDS = 86400;
3
- export declare const DEFAULT_TIMEOUT_MS = 5000;
3
+ export declare const DEFAULT_TIMEOUT_MS = 5e3;
4
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
- */
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
27
  export declare function parseMetaOptions(providerOptions: Record<string, unknown> | undefined): MetaProviderOptions | null;
package/dist/config.js CHANGED
@@ -1,130 +1,131 @@
1
- export const DEFAULT_TTL_SECONDS = 86_400;
2
- export const DEFAULT_TIMEOUT_MS = 5_000;
1
+ export const DEFAULT_TTL_SECONDS = 86400;
2
+ export const DEFAULT_TIMEOUT_MS = 5e3;
3
3
  const META_KEY = "meta";
4
4
  /**
5
- * Fields a user may opt out of upstream-wins via `meta.modelsInfoOverwrite`.
6
- * Mirrors the keys of `ModelMetadata` — anything outside this set is ignored
7
- * so a typo never silently clobbers an unrelated field.
8
- */
5
+ * Fields a user may opt out of upstream-wins via `meta.modelsInfoOverwrite`.
6
+ * Mirrors the keys of `ModelMetadata` — anything outside this set is ignored
7
+ * so a typo never silently clobbers an unrelated field.
8
+ */
9
9
  const OVERWRITABLE_FIELDS = new Set([
10
- "name",
11
- "attachment",
12
- "reasoning",
13
- "temperature",
14
- "tool_call",
15
- "cost",
16
- "limit",
17
- "modalities"
10
+ "name",
11
+ "attachment",
12
+ "reasoning",
13
+ "temperature",
14
+ "tool_call",
15
+ "cost",
16
+ "limit",
17
+ "modalities"
18
18
  ]);
19
19
  function asRecord(value) {
20
- if (!value || typeof value !== "object" || Array.isArray(value)) {
21
- return undefined;
22
- }
23
- return value;
20
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
21
+ return undefined;
22
+ }
23
+ return value;
24
24
  }
25
25
  function asString(value) {
26
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
26
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
27
27
  }
28
28
  function asStringMap(value) {
29
- const record = asRecord(value);
30
- if (!record) {
31
- return undefined;
32
- }
33
- const out = {};
34
- for (const [key, raw] of Object.entries(record)) {
35
- if (typeof raw === "string" && raw.length > 0) {
36
- out[key] = raw;
37
- }
38
- }
39
- return Object.keys(out).length > 0 ? out : undefined;
29
+ const record = asRecord(value);
30
+ if (!record) {
31
+ return undefined;
32
+ }
33
+ const out = {};
34
+ for (const [key, raw] of Object.entries(record)) {
35
+ if (typeof raw === "string" && raw.length > 0) {
36
+ out[key] = raw;
37
+ }
38
+ }
39
+ return Object.keys(out).length > 0 ? out : undefined;
40
40
  }
41
41
  function asOverwriteList(value) {
42
- if (!Array.isArray(value)) {
43
- return undefined;
44
- }
45
- const out = [];
46
- for (const raw of value) {
47
- if (typeof raw === "string" && OVERWRITABLE_FIELDS.has(raw) && !out.includes(raw)) {
48
- out.push(raw);
49
- }
50
- }
51
- return out.length > 0 ? out : undefined;
42
+ if (!Array.isArray(value)) {
43
+ return undefined;
44
+ }
45
+ const out = [];
46
+ for (const raw of value) {
47
+ if (typeof raw === "string" && OVERWRITABLE_FIELDS.has(raw) && !out.includes(raw)) {
48
+ out.push(raw);
49
+ }
50
+ }
51
+ return out.length > 0 ? out : undefined;
52
52
  }
53
53
  function asPositiveInt(value, fallback) {
54
- if (typeof value === "number" && Number.isFinite(value) && value > 0) {
55
- return Math.floor(value);
56
- }
57
- return fallback;
54
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
55
+ return Math.floor(value);
56
+ }
57
+ return fallback;
58
58
  }
59
59
  function asBoolean(value) {
60
- return value === true;
60
+ return value === true;
61
61
  }
62
62
  /**
63
- * Parse a provider's `options.meta` for opt-in model-info fields. Returns
64
- * `null` if the provider has not opted in (no `meta.modelsInfoUrl`).
65
- *
66
- * URL resolution follows the WHATWG URL spec when `modelsInfoUrl` is not
67
- * absolute:
68
- * - Absolute URL (`https://…`) → used as-is.
69
- * - Path starting with `/` → resolves from the **origin**
70
- * of `baseURL`. So with
71
- * `baseURL: "https://x.test/v1"`
72
- * and `modelsInfoUrl: "/models"`,
73
- * you get `https://x.test/models`.
74
- * Useful when your metadata
75
- * endpoint sits at a different
76
- * path than the inference API.
77
- * - Path without leading `/` → resolves **relative to**
78
- * `baseURL`. So with
79
- * `baseURL: "https://x.test/v1"`
80
- * and `modelsInfoUrl: "models"`,
81
- * you get `https://x.test/v1/models`.
82
- * Useful when metadata sits under
83
- * the same path as inference.
84
- */
63
+ * Parse a provider's `options.meta` for opt-in model-info fields. Returns
64
+ * `null` if the provider has not opted in (no `meta.modelsInfoUrl`).
65
+ *
66
+ * URL resolution follows the WHATWG URL spec when `modelsInfoUrl` is not
67
+ * absolute:
68
+ * - Absolute URL (`https://…`) → used as-is.
69
+ * - Path starting with `/` → resolves from the **origin**
70
+ * of `baseURL`. So with
71
+ * `baseURL: "https://x.test/v1"`
72
+ * and `modelsInfoUrl: "/models"`,
73
+ * you get `https://x.test/models`.
74
+ * Useful when your metadata
75
+ * endpoint sits at a different
76
+ * path than the inference API.
77
+ * - Path without leading `/` → resolves **relative to**
78
+ * `baseURL`. So with
79
+ * `baseURL: "https://x.test/v1"`
80
+ * and `modelsInfoUrl: "models"`,
81
+ * you get `https://x.test/v1/models`.
82
+ * Useful when metadata sits under
83
+ * the same path as inference.
84
+ */
85
85
  export function parseMetaOptions(providerOptions) {
86
- if (!providerOptions) {
87
- return null;
88
- }
89
- const meta = asRecord(providerOptions[META_KEY]);
90
- if (!meta) {
91
- return null;
92
- }
93
- const rawUrl = asString(meta.modelsInfoUrl);
94
- if (!rawUrl) {
95
- return null;
96
- }
97
- const baseURL = asString(providerOptions.baseURL);
98
- const modelsInfoUrl = resolveUrl(rawUrl, baseURL);
99
- return {
100
- modelsInfoUrl,
101
- modelsInfoTtlSeconds: asPositiveInt(meta.modelsInfoTtlSeconds, DEFAULT_TTL_SECONDS),
102
- modelsInfoTimeoutMs: asPositiveInt(meta.modelsInfoTimeoutMs, DEFAULT_TIMEOUT_MS),
103
- modelsInfoHeaders: asStringMap(meta.modelsInfoHeaders),
104
- modelsInfoOverwrite: asOverwriteList(meta.modelsInfoOverwrite),
105
- modelsInfoHideTextOnly: asBoolean(meta.modelsInfoHideTextOnly),
106
- modelsInfoHideInternal: asBoolean(meta.modelsInfoHideInternal),
107
- modelsInfoFormat: "openrouter"
108
- };
86
+ if (!providerOptions) {
87
+ return null;
88
+ }
89
+ const meta = asRecord(providerOptions[META_KEY]);
90
+ if (!meta) {
91
+ return null;
92
+ }
93
+ const rawUrl = asString(meta.modelsInfoUrl);
94
+ if (!rawUrl) {
95
+ return null;
96
+ }
97
+ const baseURL = asString(providerOptions.baseURL);
98
+ const modelsInfoUrl = resolveUrl(rawUrl, baseURL);
99
+ return {
100
+ modelsInfoUrl,
101
+ modelsInfoTtlSeconds: asPositiveInt(meta.modelsInfoTtlSeconds, DEFAULT_TTL_SECONDS),
102
+ modelsInfoTimeoutMs: asPositiveInt(meta.modelsInfoTimeoutMs, DEFAULT_TIMEOUT_MS),
103
+ modelsInfoHeaders: asStringMap(meta.modelsInfoHeaders),
104
+ modelsInfoOverwrite: asOverwriteList(meta.modelsInfoOverwrite),
105
+ modelsInfoHideTextOnly: asBoolean(meta.modelsInfoHideTextOnly),
106
+ modelsInfoHideInternal: asBoolean(meta.modelsInfoHideInternal),
107
+ modelsInfoHideUnmatched: asBoolean(meta.modelsInfoHideUnmatched),
108
+ modelsInfoFormat: "openrouter"
109
+ };
109
110
  }
110
111
  function resolveUrl(candidate, baseURL) {
111
- if (/^https?:\/\//i.test(candidate)) {
112
- return candidate;
113
- }
114
- if (!baseURL) {
115
- return candidate;
116
- }
117
- // Always treat the baseURL as a directory by appending a trailing slash if
118
- // it's missing. This way a path-relative `modelsInfoUrl` ("models/info")
119
- // resolves under the baseURL's path instead of replacing its last segment
120
- // (the WHATWG default). A leading-slash candidate ("/models/info") still
121
- // resolves from the origin per spec.
122
- const base = baseURL.endsWith("/") ? baseURL : `${baseURL}/`;
123
- try {
124
- return new URL(candidate, base).toString();
125
- }
126
- catch {
127
- return candidate;
128
- }
112
+ if (/^https?:\/\//i.test(candidate)) {
113
+ return candidate;
114
+ }
115
+ if (!baseURL) {
116
+ return candidate;
117
+ }
118
+ // Always treat the baseURL as a directory by appending a trailing slash if
119
+ // it's missing. This way a path-relative `modelsInfoUrl` ("models/info")
120
+ // resolves under the baseURL's path instead of replacing its last segment
121
+ // (the WHATWG default). A leading-slash candidate ("/models/info") still
122
+ // resolves from the origin per spec.
123
+ const base = baseURL.endsWith("/") ? baseURL : `${baseURL}/`;
124
+ try {
125
+ return new URL(candidate, base).toString();
126
+ } catch {
127
+ return candidate;
128
+ }
129
129
  }
130
+
130
131
  //# sourceMappingURL=config.js.map