teapot-coding-agent 0.37.77 → 0.37.79

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.
@@ -173,13 +173,91 @@ export function metaFrom(m) {
173
173
  ...(m.reasoning === true ? { reasoning: true } : {}),
174
174
  };
175
175
  }
176
- /** every model models.dev lists for this baseUrl's provider, or [] when the provider is unknown */
177
- export async function fetchModelList(baseUrl, _apiKey) {
178
- const snap = await loadDb();
176
+ /**
177
+ * The model list for a provider: the PROVIDER's own catalogue, enriched by models.dev.
178
+ *
179
+ * The provider is the authority on which models exist — models.dev lags and omits entries. MEASURED:
180
+ * `reflection/beam:free` is served by Command Code (94 models) and absent from models.dev's
181
+ * `commandcode` record (72), so a models.dev-only list silently drops models the operator can
182
+ * actually call.
183
+ *
184
+ * models.dev is still needed for CAPABILITY, because most providers publish none: Command Code's
185
+ * `/models` returns only `{context_length, created, id, name, object, owned_by, supported_endpoints}`
186
+ * while OpenRouter publishes `architecture` (modalities) and `supported_parameters`. So:
187
+ *
188
+ * - provider list wins for MEMBERSHIP;
189
+ * - a provider field, when present, wins for that field;
190
+ * - models.dev fills only what the provider did not say.
191
+ *
192
+ * When the provider cannot be reached at all we fall back to the models.dev list rather than showing
193
+ * nothing.
194
+ */
195
+ export async function fetchModelList(baseUrl, apiKey) {
196
+ const [snap, upstream] = await Promise.all([
197
+ loadDb(),
198
+ fetchProviderModels(baseUrl, apiKey).catch(() => null),
199
+ ]);
179
200
  const provider = providerFor(snap.providers, baseUrl);
180
- if (!provider?.models)
181
- return [];
182
- return Object.values(provider.models).map(metaFrom).filter((m) => m.id);
201
+ const dbModels = provider?.models ? Object.values(provider.models).map(metaFrom).filter((m) => m.id) : [];
202
+ if (!upstream)
203
+ return dbModels;
204
+ const byId = new Map(dbModels.map((m) => [m.id, m]));
205
+ return upstream
206
+ .map((m) => mergeMeta(m, byId.get(m.id)))
207
+ .sort((a, b) => a.id.localeCompare(b.id));
208
+ }
209
+ /** a provider's own `/models`, normalised, or null when it does not answer usefully */
210
+ async function fetchProviderModels(baseUrl, apiKey) {
211
+ const url = `${String(baseUrl).replace(/\/+$/, "")}/models`;
212
+ const res = await fetch(url, {
213
+ headers: { ...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}), ...providerHeaders(baseUrl) },
214
+ signal: AbortSignal.timeout(15_000),
215
+ });
216
+ if (!res.ok)
217
+ return null;
218
+ const j = (await res.json());
219
+ const out = [];
220
+ for (const m of j.data ?? []) {
221
+ if (typeof m.id !== "string" || !m.id)
222
+ continue;
223
+ const input = strings(m.architecture?.input_modalities);
224
+ const output = strings(m.architecture?.output_modalities);
225
+ const ctx = typeof m.context_length === "number" ? m.context_length : undefined;
226
+ // `reasoning` in supported_parameters means the model accepts a reasoning_effort-style control
227
+ const params = strings(m.supported_parameters);
228
+ out.push({
229
+ id: m.id,
230
+ ...(typeof m.name === "string" ? { name: m.name } : {}),
231
+ ...(ctx !== undefined ? { contextLength: ctx } : {}),
232
+ ...(input ? { modalities: { input, output: output ?? ["text"] } } : {}),
233
+ ...(params?.some((p) => p === "reasoning" || p === "reasoning_effort") ? { reasoning: true } : {}),
234
+ ...(params ? { supportedParameters: params } : {}),
235
+ });
236
+ }
237
+ return out.length ? out : null;
238
+ }
239
+ /** provider fields win; models.dev fills only the gaps */
240
+ function mergeMeta(own, db) {
241
+ if (!db)
242
+ return own;
243
+ return {
244
+ id: own.id,
245
+ ...(own.name ?? db.name ? { name: own.name ?? db.name } : {}),
246
+ ...(own.contextLength ?? db.contextLength ? { contextLength: own.contextLength ?? db.contextLength } : {}),
247
+ ...(own.modalities ?? db.modalities
248
+ ? { modalities: own.modalities ?? db.modalities }
249
+ : {}),
250
+ /**
251
+ * Efforts come from models.dev even when the provider listed the model: no provider publishes the
252
+ * effort LEVELS, only that reasoning exists. `own.reasoning` still wins for the yes/no question.
253
+ */
254
+ ...(db.supportedEfforts ? { supportedEfforts: db.supportedEfforts } : {}),
255
+ ...(own.supportedParameters ?? db.supportedParameters
256
+ ? { supportedParameters: own.supportedParameters ?? db.supportedParameters }
257
+ : {}),
258
+ ...(own.reasoning ?? db.reasoning ? { reasoning: own.reasoning ?? db.reasoning } : {}),
259
+ ...(db.pricing ? { pricing: db.pricing } : {}),
260
+ };
183
261
  }
184
262
  export function peekModelList(baseUrl) {
185
263
  if (!fresh(db))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teapot-coding-agent",
3
- "version": "0.37.77",
3
+ "version": "0.37.79",
4
4
  "description": "A lightweight, always-on multi-agent harness for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-or-later",