@vymalo/opencode-models-info 0.12.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/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 -109
- 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 -315
- 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 -67
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/package.json +3 -3
package/dist/plugin.js
CHANGED
|
@@ -4,344 +4,371 @@ import { fetchOpenRouterModels } from "./fetcher.js";
|
|
|
4
4
|
import { isTextOnlyModality, mapOpenRouterEntry, mergeIntoModel } from "./mapping.js";
|
|
5
5
|
import { startScheduler } from "./scheduler.js";
|
|
6
6
|
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
* Walk every provider in the assembled OpenCode config, fetch its
|
|
8
|
+
* `meta.modelsInfoUrl` (if any) — honoring the cache — and merge derived
|
|
9
|
+
* metadata onto each matching model entry. Runs providers in parallel; one
|
|
10
|
+
* failure never blocks others.
|
|
11
|
+
*/
|
|
12
12
|
export async function enrichConfig(input, deps) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
});
|
|
21
|
-
await Promise.allSettled(Object.entries(providers).map(([providerId, providerConfig]) => enrichProvider(providerId, providerConfig, deps)));
|
|
13
|
+
const providers = input.provider;
|
|
14
|
+
if (!providers) {
|
|
15
|
+
deps.logger.trace("models_info_enrich_no_providers", {});
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
deps.logger.trace("models_info_enrich_start", { providerCount: Object.keys(providers).length });
|
|
19
|
+
await Promise.allSettled(Object.entries(providers).map(([providerId, providerConfig]) => enrichProvider(providerId, providerConfig, deps)));
|
|
22
20
|
}
|
|
23
21
|
/**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
22
|
+
* Start one background refresh scheduler per opted-in provider, keeping the
|
|
23
|
+
* on-disk cache warm at `meta.modelsInfoTtlSeconds` cadence (the same knob
|
|
24
|
+
* that already governs cache freshness — no separate interval to configure).
|
|
25
|
+
*
|
|
26
|
+
* `config` only runs at plugin load and on rare config-signature changes
|
|
27
|
+
* (see docs/models-info.md#periodic-refresh), so a long-lived OpenCode
|
|
28
|
+
* process would otherwise keep serving whatever the catalog looked like at
|
|
29
|
+
* boot until it happens to restart. This closes that gap for the *next*
|
|
30
|
+
* `config` run — it cannot push a live update into an already-open session,
|
|
31
|
+
* since a config hook has no way to hot-patch a running one. Callers own the
|
|
32
|
+
* returned handles' lifecycle: stop them when the config that produced them
|
|
33
|
+
* is superseded (a rebuilt `config` hook) or the plugin is disposed.
|
|
34
|
+
*/
|
|
37
35
|
export function startCacheRefreshSchedulers(input, deps) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
36
|
+
const providers = input.provider;
|
|
37
|
+
if (!providers) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
const handles = [];
|
|
41
|
+
for (const [providerId, providerConfig] of Object.entries(providers)) {
|
|
42
|
+
const opts = parseMetaOptions(providerConfig?.options);
|
|
43
|
+
if (!opts) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const providerHeaders = asHeaderMap(providerConfig?.options?.headers);
|
|
47
|
+
const intervalMs = opts.modelsInfoTtlSeconds * 1e3;
|
|
48
|
+
deps.logger.trace("models_info_refresh_scheduler_started", {
|
|
49
|
+
providerId,
|
|
50
|
+
intervalMs
|
|
51
|
+
});
|
|
52
|
+
handles.push(startScheduler({
|
|
53
|
+
intervalMs,
|
|
54
|
+
logger: deps.logger,
|
|
55
|
+
taskName: `models-info-refresh:${providerId}`,
|
|
56
|
+
run: () => refreshProviderCache(providerId, opts, providerHeaders, deps)
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
return handles;
|
|
59
60
|
}
|
|
60
61
|
async function enrichProvider(providerId, providerConfig, deps) {
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
62
|
+
try {
|
|
63
|
+
if (!providerConfig) {
|
|
64
|
+
deps.logger.trace("models_info_provider_no_config", { providerId });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const opts = parseMetaOptions(providerConfig.options);
|
|
68
|
+
deps.logger.trace("models_info_provider_meta_parsed", {
|
|
69
|
+
providerId,
|
|
70
|
+
hasModelsInfoUrl: Boolean(opts)
|
|
71
|
+
});
|
|
72
|
+
if (!opts) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const models = providerConfig.models;
|
|
76
|
+
const modelCount = models ? Object.keys(models).length : 0;
|
|
77
|
+
deps.logger.trace("models_info_provider_models_present", {
|
|
78
|
+
providerId,
|
|
79
|
+
hasModels: modelCount > 0,
|
|
80
|
+
modelCount
|
|
81
|
+
});
|
|
82
|
+
if (!models || Object.keys(models).length === 0) {
|
|
83
|
+
deps.logger.debug("models_info_provider_skipped_no_models", { providerId });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// Pull whatever headers the upstream config (oauth2 plugin, static API
|
|
87
|
+
// key, etc.) has already attached to the provider; the meta-specific
|
|
88
|
+
// `modelsInfoHeaders` win on conflict. This is what makes the plugin
|
|
89
|
+
// truly auth-agnostic — we never need to know how the token was acquired.
|
|
90
|
+
const providerHeaders = asHeaderMap(providerConfig.options?.headers);
|
|
91
|
+
deps.logger.trace("models_info_provider_headers_resolved", {
|
|
92
|
+
providerId,
|
|
93
|
+
hasProviderHeaders: Boolean(providerHeaders),
|
|
94
|
+
hasMetaHeaders: Boolean(opts.modelsInfoHeaders)
|
|
95
|
+
});
|
|
96
|
+
const record = await loadRecord(providerId, opts, providerHeaders, deps);
|
|
97
|
+
if (!record) {
|
|
98
|
+
deps.logger.trace("models_info_no_record", { providerId });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const byId = new Map(record.models.map((m) => [m.id, m]));
|
|
102
|
+
const overwrite = opts.modelsInfoOverwrite ? new Set(opts.modelsInfoOverwrite) : undefined;
|
|
103
|
+
deps.logger.trace("models_info_match_table_built", {
|
|
104
|
+
providerId,
|
|
105
|
+
sourceModels: record.models.length,
|
|
106
|
+
overwriteFields: overwrite ? [...overwrite] : []
|
|
107
|
+
});
|
|
108
|
+
const totalModels = Object.keys(models).length;
|
|
109
|
+
const tally = {
|
|
110
|
+
enriched: 0,
|
|
111
|
+
hidden: 0
|
|
112
|
+
};
|
|
113
|
+
const reconcileCtx = {
|
|
114
|
+
providerId,
|
|
115
|
+
models,
|
|
116
|
+
byId,
|
|
117
|
+
opts,
|
|
118
|
+
overwrite,
|
|
119
|
+
deps
|
|
120
|
+
};
|
|
121
|
+
for (const [modelId, modelConfig] of Object.entries(models)) {
|
|
122
|
+
const outcome = reconcileModel(modelId, modelConfig, reconcileCtx);
|
|
123
|
+
if (outcome !== "skipped") {
|
|
124
|
+
tally[outcome] += 1;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const { enriched: enrichedCount, hidden: hiddenCount } = tally;
|
|
128
|
+
deps.logger.trace("models_info_provider_done", {
|
|
129
|
+
providerId,
|
|
130
|
+
enrichedCount,
|
|
131
|
+
hiddenCount,
|
|
132
|
+
totalModels
|
|
133
|
+
});
|
|
134
|
+
deps.logger.debug("models_info_enriched", {
|
|
135
|
+
providerId,
|
|
136
|
+
enrichedCount,
|
|
137
|
+
hiddenCount,
|
|
138
|
+
totalModels,
|
|
139
|
+
sourceModels: record.models.length
|
|
140
|
+
});
|
|
141
|
+
} catch (error) {
|
|
142
|
+
// Promise.allSettled would otherwise swallow this — surface it loudly so
|
|
143
|
+
// a broken cache disk or mapping bug isn't silently no-op'd per provider.
|
|
144
|
+
deps.logger.error("models_info_enrichment_failed", {
|
|
145
|
+
providerId,
|
|
146
|
+
error: error instanceof Error ? error.message : String(error)
|
|
147
|
+
});
|
|
148
|
+
}
|
|
139
149
|
}
|
|
140
150
|
/**
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
151
|
+
* Decide one model's fate against the catalog and apply it in place —
|
|
152
|
+
* merge, delete, or leave untouched — returning what happened for the
|
|
153
|
+
* caller's tally. Three independent gates:
|
|
154
|
+
* - `modelsInfoHideTextOnly` OR `modelsInfoHideUnmatched` — a model absent
|
|
155
|
+
* from the catalog entirely gets deleted. Either flag alone is enough;
|
|
156
|
+
* `modelsInfoHideTextOnly` has triggered this since 0.10.0 (unchanged,
|
|
157
|
+
* for backward compat with existing adopters) — `modelsInfoHideUnmatched`
|
|
158
|
+
* exists for a consumer that wants catalog-authoritative *membership*
|
|
159
|
+
* WITHOUT also pulling in modality-based hiding.
|
|
160
|
+
* - `modelsInfoHideInternal` — a matched model the catalog flags
|
|
161
|
+
* `internal: true` gets deleted. Independent of the above: modality and
|
|
162
|
+
* internal/restricted status are unrelated signals, and conflating them
|
|
163
|
+
* (routing "hide internal" through `modelsInfoHideTextOnly`) hides
|
|
164
|
+
* legitimate text-only external models.
|
|
165
|
+
* - `modelsInfoHideTextOnly` — a matched model the catalog reports as
|
|
166
|
+
* text-in/text-out only gets deleted. Unchanged from 0.10.0.
|
|
167
|
+
*/
|
|
158
168
|
function reconcileModel(modelId, modelConfig, ctx) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
169
|
+
const { providerId, models, byId, opts, overwrite, deps } = ctx;
|
|
170
|
+
const declaredId = typeof modelConfig.id === "string" ? modelConfig.id : undefined;
|
|
171
|
+
const matchById = byId.has(modelId);
|
|
172
|
+
const match = byId.get(modelId) ?? (declaredId ? byId.get(declaredId) : undefined);
|
|
173
|
+
if (!match) {
|
|
174
|
+
if (!opts.modelsInfoHideTextOnly && !opts.modelsInfoHideUnmatched) {
|
|
175
|
+
deps.logger.trace("models_info_model_unmatched", {
|
|
176
|
+
providerId,
|
|
177
|
+
modelId,
|
|
178
|
+
declaredId
|
|
179
|
+
});
|
|
180
|
+
return "skipped";
|
|
181
|
+
}
|
|
182
|
+
delete models[modelId];
|
|
183
|
+
deps.logger.debug("models_info_model_hidden_unmatched", {
|
|
184
|
+
providerId,
|
|
185
|
+
modelId,
|
|
186
|
+
declaredId
|
|
187
|
+
});
|
|
188
|
+
return "hidden";
|
|
189
|
+
}
|
|
190
|
+
deps.logger.trace("models_info_model_matched", {
|
|
191
|
+
providerId,
|
|
192
|
+
modelId,
|
|
193
|
+
matchedBy: matchById ? "id" : "declaredId"
|
|
194
|
+
});
|
|
195
|
+
if (opts.modelsInfoHideInternal && match.internal === true) {
|
|
196
|
+
delete models[modelId];
|
|
197
|
+
deps.logger.debug("models_info_model_hidden_internal", {
|
|
198
|
+
providerId,
|
|
199
|
+
modelId
|
|
200
|
+
});
|
|
201
|
+
return "hidden";
|
|
202
|
+
}
|
|
203
|
+
const derived = mapOpenRouterEntry(match, overwrite);
|
|
204
|
+
if (opts.modelsInfoHideTextOnly && isTextOnlyModality(derived.modalities)) {
|
|
205
|
+
delete models[modelId];
|
|
206
|
+
deps.logger.debug("models_info_model_hidden_text_only", {
|
|
207
|
+
providerId,
|
|
208
|
+
modelId
|
|
209
|
+
});
|
|
210
|
+
return "hidden";
|
|
211
|
+
}
|
|
212
|
+
const derivedFields = Object.keys(derived);
|
|
213
|
+
const appliedFields = derivedFields.filter((f) => modelConfig[f] === undefined || overwrite?.has(f));
|
|
214
|
+
const skippedFields = derivedFields.filter((f) => !appliedFields.includes(f));
|
|
215
|
+
deps.logger.trace("models_info_model_merge", {
|
|
216
|
+
providerId,
|
|
217
|
+
modelId,
|
|
218
|
+
derivedFields,
|
|
219
|
+
appliedFields,
|
|
220
|
+
skippedFields
|
|
221
|
+
});
|
|
222
|
+
mergeIntoModel(modelConfig, derived, overwrite);
|
|
223
|
+
return "enriched";
|
|
200
224
|
}
|
|
201
225
|
async function loadRecord(providerId, opts, providerHeaders, deps) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
226
|
+
// Cache key is keyed on the user-specified `modelsInfoHeaders` (NOT the
|
|
227
|
+
// provider's rotating auth header) — so switching tenants busts the cache,
|
|
228
|
+
// but an OAuth2 token rotation does not thrash it. See cacheKey() docstring.
|
|
229
|
+
const key = cacheKey(providerId, opts.modelsInfoUrl, opts.modelsInfoHeaders);
|
|
230
|
+
deps.logger.trace("models_info_cache_key_computed", {
|
|
231
|
+
providerId,
|
|
232
|
+
key
|
|
233
|
+
});
|
|
234
|
+
const now = deps.now ? deps.now() : Date.now();
|
|
235
|
+
const cached = await deps.cache.get(key);
|
|
236
|
+
deps.logger.trace("models_info_cache_lookup", {
|
|
237
|
+
providerId,
|
|
238
|
+
found: Boolean(cached),
|
|
239
|
+
expired: cached ? isExpired(cached, now) : undefined
|
|
240
|
+
});
|
|
241
|
+
if (cached && !isExpired(cached, now)) {
|
|
242
|
+
deps.logger.debug("models_info_cache_hit", {
|
|
243
|
+
providerId,
|
|
244
|
+
url: opts.modelsInfoUrl,
|
|
245
|
+
ageMs: now - cached.fetchedAt
|
|
246
|
+
});
|
|
247
|
+
return cached;
|
|
248
|
+
}
|
|
249
|
+
return fetchAndCache(providerId, opts, providerHeaders, deps, key, cached, now);
|
|
223
250
|
}
|
|
224
251
|
/**
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
252
|
+
* Unconditionally revalidates a provider's catalog against the network
|
|
253
|
+
* (still honoring the `ETag`, so an unchanged catalog is a cheap `304`) and
|
|
254
|
+
* writes the result back to the cache — regardless of whether the current
|
|
255
|
+
* entry is still within its TTL. This is the periodic-refresh path (see
|
|
256
|
+
* {@link startCacheRefreshSchedulers}): `loadRecord`'s TTL fast-path exists
|
|
257
|
+
* to avoid a network call from the `config` hook on every launch, but a
|
|
258
|
+
* background scheduler's entire point is to go check anyway.
|
|
259
|
+
*/
|
|
233
260
|
async function refreshProviderCache(providerId, opts, providerHeaders, deps) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
261
|
+
const key = cacheKey(providerId, opts.modelsInfoUrl, opts.modelsInfoHeaders);
|
|
262
|
+
const now = deps.now ? deps.now() : Date.now();
|
|
263
|
+
const cached = await deps.cache.get(key);
|
|
264
|
+
await fetchAndCache(providerId, opts, providerHeaders, deps, key, cached, now);
|
|
238
265
|
}
|
|
239
266
|
async function fetchAndCache(providerId, opts, providerHeaders, deps, key, cached, now) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
267
|
+
const headers = buildFetchHeaders(opts, providerHeaders);
|
|
268
|
+
deps.logger.trace("models_info_fetch_start", {
|
|
269
|
+
providerId,
|
|
270
|
+
url: opts.modelsInfoUrl,
|
|
271
|
+
hasHeaders: Boolean(headers),
|
|
272
|
+
hasConditional: Boolean(cached?.etag)
|
|
273
|
+
});
|
|
274
|
+
const result = await fetchOpenRouterModels({
|
|
275
|
+
url: opts.modelsInfoUrl,
|
|
276
|
+
headers,
|
|
277
|
+
timeoutMs: opts.modelsInfoTimeoutMs,
|
|
278
|
+
etag: cached?.etag,
|
|
279
|
+
fetchImpl: deps.fetchImpl
|
|
280
|
+
});
|
|
281
|
+
deps.logger.trace("models_info_fetch_result", {
|
|
282
|
+
providerId,
|
|
283
|
+
status: result.status,
|
|
284
|
+
count: result.models?.length
|
|
285
|
+
});
|
|
286
|
+
if (result.status === "ok" && result.models) {
|
|
287
|
+
const next = {
|
|
288
|
+
fetchedAt: now,
|
|
289
|
+
ttlSeconds: opts.modelsInfoTtlSeconds,
|
|
290
|
+
etag: result.etag,
|
|
291
|
+
models: result.models
|
|
292
|
+
};
|
|
293
|
+
// Disk write is best-effort — a read-only $HOME / cache dir shouldn't
|
|
294
|
+
// make us throw away a perfectly good fresh response.
|
|
295
|
+
await safePut(deps, key, next, providerId, opts.modelsInfoUrl);
|
|
296
|
+
deps.logger.info("models_info_fetched", {
|
|
297
|
+
providerId,
|
|
298
|
+
url: opts.modelsInfoUrl,
|
|
299
|
+
count: result.models.length
|
|
300
|
+
});
|
|
301
|
+
return next;
|
|
302
|
+
}
|
|
303
|
+
if (result.status === "not-modified" && cached) {
|
|
304
|
+
// Apply the CURRENT TTL from config — a tightened TTL in opencode.json
|
|
305
|
+
// should take effect on the next revalidation, not on the next full
|
|
306
|
+
// 200 fetch (which might be 24h away).
|
|
307
|
+
const refreshed = {
|
|
308
|
+
...cached,
|
|
309
|
+
fetchedAt: now,
|
|
310
|
+
ttlSeconds: opts.modelsInfoTtlSeconds
|
|
311
|
+
};
|
|
312
|
+
await safePut(deps, key, refreshed, providerId, opts.modelsInfoUrl);
|
|
313
|
+
deps.logger.debug("models_info_not_modified", {
|
|
314
|
+
providerId,
|
|
315
|
+
url: opts.modelsInfoUrl
|
|
316
|
+
});
|
|
317
|
+
return refreshed;
|
|
318
|
+
}
|
|
319
|
+
if (cached) {
|
|
320
|
+
deps.logger.warn("models_info_fetch_failed_using_stale", {
|
|
321
|
+
providerId,
|
|
322
|
+
url: opts.modelsInfoUrl,
|
|
323
|
+
error: result.error,
|
|
324
|
+
ageMs: now - cached.fetchedAt
|
|
325
|
+
});
|
|
326
|
+
return cached;
|
|
327
|
+
}
|
|
328
|
+
deps.logger.warn("models_info_fetch_failed_no_cache", {
|
|
329
|
+
providerId,
|
|
330
|
+
url: opts.modelsInfoUrl,
|
|
331
|
+
error: result.error
|
|
332
|
+
});
|
|
333
|
+
return undefined;
|
|
307
334
|
}
|
|
308
335
|
/**
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
336
|
+
* Merge the provider's resolved request headers with the meta-specific
|
|
337
|
+
* `modelsInfoHeaders`. Meta wins on conflict so a user can override e.g. a
|
|
338
|
+
* dynamic `Authorization` header for the metadata endpoint specifically.
|
|
339
|
+
*/
|
|
313
340
|
function buildFetchHeaders(opts, providerHeaders) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
341
|
+
if (!providerHeaders && !opts.modelsInfoHeaders) {
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
...providerHeaders ?? {},
|
|
346
|
+
...opts.modelsInfoHeaders ?? {}
|
|
347
|
+
};
|
|
321
348
|
}
|
|
322
349
|
async function safePut(deps, key, record, providerId, url) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
350
|
+
try {
|
|
351
|
+
await deps.cache.put(key, record);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
deps.logger.warn("models_info_cache_write_failed", {
|
|
354
|
+
providerId,
|
|
355
|
+
url,
|
|
356
|
+
error: error instanceof Error ? error.message : String(error)
|
|
357
|
+
});
|
|
358
|
+
}
|
|
333
359
|
}
|
|
334
360
|
function asHeaderMap(value) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
361
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
const out = {};
|
|
365
|
+
for (const [k, v] of Object.entries(value)) {
|
|
366
|
+
if (typeof v === "string" && v.length > 0) {
|
|
367
|
+
out[k] = v;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
345
371
|
}
|
|
346
372
|
export { FileCacheStore };
|
|
373
|
+
|
|
347
374
|
//# sourceMappingURL=plugin.js.map
|