@oh-my-pi/pi-catalog 18.2.7 → 18.2.8

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.
@@ -29,6 +29,7 @@ import { resolveModelReference } from "../identity/reference";
29
29
  import type { ModelManagerOptions, ModelsDevFallback } from "../model-manager";
30
30
  import { type GeneratedProvider, getBundledModels } from "../models";
31
31
  import {
32
+ KIND_API_KINDS,
32
33
  MODEL_KINDS,
33
34
  type Api,
34
35
  type FetchImpl,
@@ -3227,127 +3228,210 @@ export function openrouterModelManagerOptions(config?: OpenRouterModelManagerCon
3227
3228
  // override bundled `api: "openrouter"` models during online-if-uncached startup.
3228
3229
  cacheProviderId: resolveModelCacheProviderId("openrouter"),
3229
3230
  fetchDynamicModels: async () => {
3230
- const [chatModels, imageModels, decisionModels] = await Promise.all([
3231
- fetchOpenAICompatibleModels({
3232
- api: "openrouter",
3233
- provider: "openrouter",
3234
- baseUrl,
3235
- apiKey,
3236
- filterModel: (entry: OpenAICompatibleModelRecord) => {
3237
- const params = entry.supported_parameters;
3238
- return Array.isArray(params) && params.includes("tools");
3239
- },
3240
- mapModel: (
3241
- entry: OpenAICompatibleModelRecord,
3242
- defaults: ModelSpec<"openrouter">,
3243
- _context: OpenAICompatibleModelMapperContext<"openrouter">,
3244
- ): ModelSpec<"openrouter"> => {
3245
- const reference = references.get(defaults.id);
3246
- const baseModel = mapWithBundledReference(entry, defaults, reference);
3247
- const pricing = isRecord(entry.pricing) ? entry.pricing : undefined;
3248
- const params = Array.isArray(entry.supported_parameters)
3249
- ? entry.supported_parameters.filter((value): value is string => typeof value === "string")
3250
- : [];
3251
- const thinking = mapOpenRouterThinking(entry);
3252
- const architecture = isRecord(entry.architecture) ? entry.architecture : undefined;
3253
- const input: ("text" | "image")[] = Array.isArray(architecture?.input_modalities)
3254
- ? toInputCapabilities(architecture.input_modalities)
3255
- : String(architecture?.modality ?? "").includes("image")
3256
- ? ["text", "image"]
3257
- : ["text"];
3258
- const topProvider = isRecord(entry.top_provider) ? entry.top_provider : undefined;
3259
-
3260
- const supportsToolChoice = params.includes("tool_choice");
3231
+ const [chatModels, imageModels, decisionModels, rerankModels, videoModels, embeddingModels] =
3232
+ await Promise.all([
3233
+ fetchOpenAICompatibleModels({
3234
+ api: "openrouter",
3235
+ provider: "openrouter",
3236
+ baseUrl,
3237
+ apiKey,
3238
+ filterModel: (entry: OpenAICompatibleModelRecord) => {
3239
+ const params = entry.supported_parameters;
3240
+ return Array.isArray(params) && params.includes("tools");
3241
+ },
3242
+ mapModel: (
3243
+ entry: OpenAICompatibleModelRecord,
3244
+ defaults: ModelSpec<"openrouter">,
3245
+ _context: OpenAICompatibleModelMapperContext<"openrouter">,
3246
+ ): ModelSpec<"openrouter"> => {
3247
+ const reference = references.get(defaults.id);
3248
+ const baseModel = mapWithBundledReference(entry, defaults, reference);
3249
+ const pricing = isRecord(entry.pricing) ? entry.pricing : undefined;
3250
+ const params = Array.isArray(entry.supported_parameters)
3251
+ ? entry.supported_parameters.filter((value): value is string => typeof value === "string")
3252
+ : [];
3253
+ const thinking = mapOpenRouterThinking(entry);
3254
+ const architecture = isRecord(entry.architecture) ? entry.architecture : undefined;
3255
+ const input: ("text" | "image")[] = Array.isArray(architecture?.input_modalities)
3256
+ ? toInputCapabilities(architecture.input_modalities)
3257
+ : String(architecture?.modality ?? "").includes("image")
3258
+ ? ["text", "image"]
3259
+ : ["text"];
3260
+ const topProvider = isRecord(entry.top_provider) ? entry.top_provider : undefined;
3261
+
3262
+ const supportsToolChoice = params.includes("tool_choice");
3261
3263
 
3262
- return {
3263
- ...baseModel,
3264
- reasoning: params.includes("reasoning"),
3265
- ...(thinking !== undefined ? { thinking } : {}),
3266
- input,
3267
- cost: {
3268
- input: parseFloat(String(pricing?.prompt ?? "0")) * 1_000_000,
3269
- output: parseFloat(String(pricing?.completion ?? "0")) * 1_000_000,
3270
- cacheRead: parseFloat(String(pricing?.input_cache_read ?? "0")) * 1_000_000,
3271
- cacheWrite: parseFloat(String(pricing?.input_cache_write ?? "0")) * 1_000_000,
3272
- },
3273
- contextWindow:
3274
- typeof entry.context_length === "number" ? entry.context_length : baseModel.contextWindow,
3275
- maxTokens:
3276
- typeof topProvider?.max_completion_tokens === "number"
3277
- ? topProvider.max_completion_tokens
3278
- : baseModel.maxTokens,
3279
- ...(!supportsToolChoice && {
3280
- compat: { ...baseModel.compat, supportsToolChoice: false },
3281
- }),
3282
- };
3283
- },
3284
- fetch: config?.fetch,
3285
- }),
3286
- fetchOpenAICompatibleModels({
3287
- api: "openrouter-images",
3288
- provider: "openrouter",
3289
- baseUrl: `${baseUrl}/images`,
3290
- apiKey,
3291
- filterModel: entry => {
3292
- const architecture = isRecord(entry.architecture) ? entry.architecture : undefined;
3293
- return (
3294
- Array.isArray(architecture?.output_modalities) && architecture.output_modalities.includes("image")
3295
- );
3296
- },
3297
- mapModel: (_entry, defaults): ModelSpec<"openrouter-images"> => ({
3298
- ...defaults,
3264
+ return {
3265
+ ...baseModel,
3266
+ reasoning: params.includes("reasoning"),
3267
+ ...(thinking !== undefined ? { thinking } : {}),
3268
+ input,
3269
+ cost: {
3270
+ input: parseFloat(String(pricing?.prompt ?? "0")) * 1_000_000,
3271
+ output: parseFloat(String(pricing?.completion ?? "0")) * 1_000_000,
3272
+ cacheRead: parseFloat(String(pricing?.input_cache_read ?? "0")) * 1_000_000,
3273
+ cacheWrite: parseFloat(String(pricing?.input_cache_write ?? "0")) * 1_000_000,
3274
+ },
3275
+ contextWindow:
3276
+ typeof entry.context_length === "number" ? entry.context_length : baseModel.contextWindow,
3277
+ maxTokens:
3278
+ typeof topProvider?.max_completion_tokens === "number"
3279
+ ? topProvider.max_completion_tokens
3280
+ : baseModel.maxTokens,
3281
+ ...(!supportsToolChoice && {
3282
+ compat: { ...baseModel.compat, supportsToolChoice: false },
3283
+ }),
3284
+ };
3285
+ },
3286
+ fetch: config?.fetch,
3287
+ }),
3288
+ fetchOpenAICompatibleModels({
3289
+ api: "openrouter-images",
3290
+ provider: "openrouter",
3291
+ baseUrl: `${baseUrl}/images`,
3292
+ apiKey,
3293
+ filterModel: entry => {
3294
+ const architecture = isRecord(entry.architecture) ? entry.architecture : undefined;
3295
+ return (
3296
+ Array.isArray(architecture?.output_modalities) &&
3297
+ architecture.output_modalities.includes("image")
3298
+ );
3299
+ },
3300
+ mapModel: (_entry, defaults): ModelSpec<"openrouter-images"> => ({
3301
+ ...defaults,
3302
+ baseUrl,
3303
+ kind: "image",
3304
+ reasoning: false,
3305
+ input: ["text", "image"],
3306
+ supportsTools: false,
3307
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
3308
+ contextWindow: null,
3309
+ maxTokens: null,
3310
+ }),
3311
+ fetch: config?.fetch,
3312
+ }),
3313
+ // Decision models (`text->decisions`) are absent from the default roster
3314
+ // and answer only through the Decisions API, outside the `/v1` prefix.
3315
+ fetchOpenAICompatibleModels({
3316
+ api: "openrouter-decisions",
3317
+ provider: "openrouter",
3299
3318
  baseUrl,
3300
- kind: "image",
3301
- reasoning: false,
3302
- input: ["text", "image"],
3303
- supportsTools: false,
3304
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
3305
- contextWindow: null,
3306
- maxTokens: null,
3319
+ apiKey,
3320
+ query: { output_modalities: "decisions" },
3321
+ filterModel: entry => {
3322
+ const architecture = isRecord(entry.architecture) ? entry.architecture : undefined;
3323
+ return (
3324
+ Array.isArray(architecture?.output_modalities) &&
3325
+ architecture.output_modalities.includes("decisions")
3326
+ );
3327
+ },
3328
+ mapModel: (entry, defaults): ModelSpec<"openrouter-decisions"> => {
3329
+ const pricing = isRecord(entry.pricing) ? entry.pricing : undefined;
3330
+ const topProvider = isRecord(entry.top_provider) ? entry.top_provider : undefined;
3331
+ return {
3332
+ ...defaults,
3333
+ baseUrl: decisionsBaseUrl,
3334
+ kind: "judge",
3335
+ reasoning: false,
3336
+ input: ["text"],
3337
+ supportsTools: false,
3338
+ cost: {
3339
+ input: parseFloat(String(pricing?.prompt ?? "0")) * 1_000_000,
3340
+ output: parseFloat(String(pricing?.completion ?? "0")) * 1_000_000,
3341
+ cacheRead: 0,
3342
+ cacheWrite: 0,
3343
+ },
3344
+ contextWindow: typeof entry.context_length === "number" ? entry.context_length : null,
3345
+ maxTokens:
3346
+ typeof topProvider?.max_completion_tokens === "number"
3347
+ ? topProvider.max_completion_tokens
3348
+ : null,
3349
+ };
3350
+ },
3351
+ fetch: config?.fetch,
3307
3352
  }),
3308
- fetch: config?.fetch,
3309
- }),
3310
- // Decision models (`text->decisions`) are absent from the default roster
3311
- // and answer only through the Decisions API, outside the `/v1` prefix.
3312
- fetchOpenAICompatibleModels({
3313
- api: "openrouter-decisions",
3314
- provider: "openrouter",
3315
- baseUrl,
3316
- apiKey,
3317
- query: { output_modalities: "decisions" },
3318
- filterModel: entry => {
3319
- const architecture = isRecord(entry.architecture) ? entry.architecture : undefined;
3320
- return (
3321
- Array.isArray(architecture?.output_modalities) &&
3322
- architecture.output_modalities.includes("decisions")
3323
- );
3324
- },
3325
- mapModel: (entry, defaults): ModelSpec<"openrouter-decisions"> => {
3326
- const pricing = isRecord(entry.pricing) ? entry.pricing : undefined;
3327
- const topProvider = isRecord(entry.top_provider) ? entry.top_provider : undefined;
3328
- return {
3353
+ fetchOpenAICompatibleModels({
3354
+ api: "openrouter-rerank",
3355
+ provider: "openrouter",
3356
+ baseUrl,
3357
+ apiKey,
3358
+ query: { output_modalities: "rerank" },
3359
+ filterModel: entry => {
3360
+ const architecture = isRecord(entry.architecture) ? entry.architecture : undefined;
3361
+ return (
3362
+ Array.isArray(architecture?.output_modalities) &&
3363
+ architecture.output_modalities.includes("rerank")
3364
+ );
3365
+ },
3366
+ mapModel: (entry, defaults): ModelSpec<"openrouter-rerank"> => {
3367
+ const architecture = isRecord(entry.architecture) ? entry.architecture : undefined;
3368
+ const topProvider = isRecord(entry.top_provider) ? entry.top_provider : undefined;
3369
+ return {
3370
+ ...defaults,
3371
+ baseUrl,
3372
+ kind: "rerank",
3373
+ reasoning: false,
3374
+ input: Array.isArray(architecture?.input_modalities)
3375
+ ? toInputCapabilities(architecture.input_modalities)
3376
+ : ["text"],
3377
+ supportsTools: false,
3378
+ // OpenRouter bills reranking per search; ModelCost has no search-unit axis.
3379
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
3380
+ contextWindow: typeof entry.context_length === "number" ? entry.context_length : null,
3381
+ maxTokens:
3382
+ typeof topProvider?.max_completion_tokens === "number"
3383
+ ? topProvider.max_completion_tokens
3384
+ : null,
3385
+ };
3386
+ },
3387
+ fetch: config?.fetch,
3388
+ }),
3389
+ fetchOpenAICompatibleModels({
3390
+ api: "openrouter-video",
3391
+ provider: "openrouter",
3392
+ baseUrl: `${baseUrl}/videos`,
3393
+ apiKey,
3394
+ mapModel: (_entry, defaults): ModelSpec<"openrouter-video"> => ({
3329
3395
  ...defaults,
3330
- baseUrl: decisionsBaseUrl,
3331
- kind: "judge",
3396
+ baseUrl,
3397
+ kind: "video",
3332
3398
  reasoning: false,
3333
- input: ["text"],
3399
+ input: ["text", "image"],
3334
3400
  supportsTools: false,
3335
- cost: {
3336
- input: parseFloat(String(pricing?.prompt ?? "0")) * 1_000_000,
3337
- output: parseFloat(String(pricing?.completion ?? "0")) * 1_000_000,
3338
- cacheRead: 0,
3339
- cacheWrite: 0,
3340
- },
3341
- contextWindow: typeof entry.context_length === "number" ? entry.context_length : null,
3342
- maxTokens:
3343
- typeof topProvider?.max_completion_tokens === "number"
3344
- ? topProvider.max_completion_tokens
3345
- : null,
3346
- };
3347
- },
3348
- fetch: config?.fetch,
3349
- }),
3350
- ]);
3401
+ // OpenRouter bills video by output second/SKU; ModelCost has no duration axis.
3402
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
3403
+ contextWindow: null,
3404
+ maxTokens: null,
3405
+ }),
3406
+ fetch: config?.fetch,
3407
+ }),
3408
+ fetchOpenAICompatibleModels({
3409
+ api: "openai-embeddings",
3410
+ provider: "openrouter",
3411
+ baseUrl: `${baseUrl}/embeddings`,
3412
+ apiKey,
3413
+ mapModel: (entry, defaults): ModelSpec<"openai-embeddings"> => {
3414
+ const pricing = isRecord(entry.pricing) ? entry.pricing : undefined;
3415
+ return {
3416
+ ...defaults,
3417
+ baseUrl,
3418
+ kind: "embedding",
3419
+ reasoning: false,
3420
+ input: ["text"],
3421
+ supportsTools: false,
3422
+ cost: {
3423
+ input: parseFloat(String(pricing?.prompt ?? "0")) * 1_000_000,
3424
+ output: 0,
3425
+ cacheRead: 0,
3426
+ cacheWrite: 0,
3427
+ },
3428
+ contextWindow: typeof entry.context_length === "number" ? entry.context_length : null,
3429
+ maxTokens: null,
3430
+ };
3431
+ },
3432
+ fetch: config?.fetch,
3433
+ }),
3434
+ ]);
3351
3435
 
3352
3436
  if (imageModels === null) {
3353
3437
  logger.warn("OpenRouter image model discovery unavailable; preserving chat model discovery", {
@@ -3359,12 +3443,39 @@ export function openrouterModelManagerOptions(config?: OpenRouterModelManagerCon
3359
3443
  endpoint: `${baseUrl}/models?output_modalities=decisions`,
3360
3444
  });
3361
3445
  }
3362
- if (chatModels === null && imageModels === null && decisionModels === null) return null;
3446
+ if (rerankModels === null) {
3447
+ logger.warn("OpenRouter rerank model discovery unavailable; preserving other model discovery", {
3448
+ endpoint: `${baseUrl}/models?output_modalities=rerank`,
3449
+ });
3450
+ }
3451
+ if (videoModels === null) {
3452
+ logger.warn("OpenRouter video model discovery unavailable; preserving other model discovery", {
3453
+ endpoint: `${baseUrl}/videos/models`,
3454
+ });
3455
+ }
3456
+ if (embeddingModels === null) {
3457
+ logger.warn("OpenRouter embedding model discovery unavailable; preserving other model discovery", {
3458
+ endpoint: `${baseUrl}/embeddings/models`,
3459
+ });
3460
+ }
3461
+ if (
3462
+ chatModels === null &&
3463
+ imageModels === null &&
3464
+ decisionModels === null &&
3465
+ rerankModels === null &&
3466
+ videoModels === null &&
3467
+ embeddingModels === null
3468
+ ) {
3469
+ return null;
3470
+ }
3363
3471
 
3364
3472
  const models = new Map<string, ModelSpec<Api>>();
3365
3473
  for (const model of chatModels ?? []) models.set(model.id, model);
3366
3474
  for (const model of imageModels ?? []) models.set(model.id, model);
3367
3475
  for (const model of decisionModels ?? []) models.set(model.id, model);
3476
+ for (const model of rerankModels ?? []) models.set(model.id, model);
3477
+ for (const model of videoModels ?? []) models.set(model.id, model);
3478
+ for (const model of embeddingModels ?? []) models.set(model.id, model);
3368
3479
  return Array.from(models.values()).sort((left, right) => left.id.localeCompare(right.id));
3369
3480
  },
3370
3481
  };
@@ -6329,10 +6440,11 @@ export function mapModelsDevToModels(
6329
6440
  const normalizedKind = policy.catalog.kind ?? m.kind;
6330
6441
  if (typeof normalizedKind !== "string" || !MODEL_KINDS.some(value => value === normalizedKind)) continue;
6331
6442
  if (normalizedKind !== "chat") {
6332
- if (normalizedKind !== "image" && normalizedKind !== "tts" && normalizedKind !== "stt") continue;
6333
- kindApi = providers[desc.providerId]?.kindApis?.[normalizedKind];
6443
+ const kindApiKind = KIND_API_KINDS.find(value => value === normalizedKind);
6444
+ if (kindApiKind === undefined) continue;
6445
+ kindApi = providers[desc.providerId]?.kindApis?.[kindApiKind];
6334
6446
  if (kindApi === undefined) continue;
6335
- kind = normalizedKind;
6447
+ kind = kindApiKind;
6336
6448
  }
6337
6449
  }
6338
6450
 
package/src/types.ts CHANGED
@@ -24,9 +24,23 @@ export type KnownApi =
24
24
  export type Api = KnownApi | (string & {});
25
25
 
26
26
  /** Catalog kinds used to isolate role-specific runners from session chat models. */
27
- export const MODEL_KINDS = ["chat", "tiny", "image", "tts", "stt", "search", "judge"] as const;
27
+ export const MODEL_KINDS = [
28
+ "chat",
29
+ "tiny",
30
+ "image",
31
+ "tts",
32
+ "stt",
33
+ "search",
34
+ "judge",
35
+ "embedding",
36
+ "rerank",
37
+ "video",
38
+ ] as const;
28
39
  /** Technical capability of a catalog model; absent model kinds mean chat. */
29
40
  export type ModelKind = (typeof MODEL_KINDS)[number];
41
+ /** Kinds a provider maps to a runner transport through `kind-apis` in its KDL; discovery drops rows of these kinds when the provider declares no API. */
42
+ export const KIND_API_KINDS = ["image", "tts", "stt", "embedding", "rerank", "video"] as const;
43
+ export type KindApiKind = (typeof KIND_API_KINDS)[number];
30
44
  /** Grounding transport available to chat models selected by the web role. */
31
45
  export type WebSearchGrounding = "gemini" | "anthropic" | "codex" | "xai" | "openrouter";
32
46
  /** Non-chat runner protocols accepted by catalog seeds, outside the chat dispatch union. */
@@ -39,6 +53,10 @@ export const RUNNER_APIS = [
39
53
  "openrouter-images",
40
54
  "xai-tts",
41
55
  "openai-speech",
56
+ "openai-embeddings",
57
+ "openrouter-rerank",
58
+ "openrouter-video",
59
+ "openai-transcriptions",
42
60
  ] as const;
43
61
 
44
62
  /** Resolve a model's kind while preserving chat semantics for existing catalog rows. */