@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 +25 -1
- package/dist/cache.d.ts +23 -23
- package/dist/cache.js +89 -91
- package/dist/cache.js.map +1 -1
- package/dist/config.d.ts +23 -23
- package/dist/config.js +109 -108
- package/dist/config.js.map +1 -1
- package/dist/fetcher.d.ts +9 -9
- package/dist/fetcher.js +76 -70
- package/dist/fetcher.js.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib.js +1 -0
- package/dist/lib.js.map +1 -1
- package/dist/logging.d.ts +6 -6
- package/dist/logging.js +58 -62
- package/dist/logging.js.map +1 -1
- package/dist/mapping.d.ts +39 -39
- package/dist/mapping.js +144 -130
- package/dist/mapping.js.map +1 -1
- package/dist/opencode.d.ts +4 -4
- package/dist/opencode.js +70 -69
- package/dist/opencode.js.map +1 -1
- package/dist/plugin.d.ts +25 -25
- package/dist/plugin.js +342 -311
- package/dist/plugin.js.map +1 -1
- package/dist/scheduler.d.ts +16 -16
- package/dist/scheduler.js +55 -57
- package/dist/scheduler.js.map +1 -1
- package/dist/types.d.ts +67 -54
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/package.json +3 -3
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
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
16
|
-
|
|
15
|
+
get(key: string): Promise<CachedModelsRecord | undefined>;
|
|
16
|
+
put(key: string, record: CachedModelsRecord): Promise<void>;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
15
|
+
return join(resolveDefaultCacheRoot(), namespace);
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
29
|
-
|
|
28
|
+
const headerPart = headers ? stableStringify(headers) : "";
|
|
29
|
+
return createHash("sha256").update(`${providerId}::${url}::${headerPart}`).digest("hex");
|
|
30
30
|
}
|
|
31
31
|
function stableStringify(headers) {
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
99
|
+
return now - record.fetchedAt > record.ttlSeconds * 1e3;
|
|
101
100
|
}
|
|
102
101
|
function isValidRecord(value) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
{"
|
|
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 =
|
|
3
|
+
export declare const DEFAULT_TIMEOUT_MS = 5e3;
|
|
4
4
|
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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 =
|
|
2
|
-
export const DEFAULT_TIMEOUT_MS =
|
|
1
|
+
export const DEFAULT_TTL_SECONDS = 86400;
|
|
2
|
+
export const DEFAULT_TIMEOUT_MS = 5e3;
|
|
3
3
|
const META_KEY = "meta";
|
|
4
4
|
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
24
|
}
|
|
25
25
|
function asString(value) {
|
|
26
|
-
|
|
26
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
27
27
|
}
|
|
28
28
|
function asStringMap(value) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
+
return value === true;
|
|
61
61
|
}
|
|
62
62
|
/**
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|