@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.
- package/CHANGELOG.md +10 -3
- package/README.md +18 -18
- package/dist/types/compat/types.d.ts +2 -2
- package/dist/types/types.d.ts +5 -2
- package/package.json +38 -38
- package/src/compat/rules/README.md +33 -32
- package/src/compat/rules/providers/openai.kdl +58 -0
- package/src/compat/rules/providers/openrouter.kdl +108 -0
- package/src/compat/rules.json +12526 -1
- package/src/compat/types.ts +2 -2
- package/src/discovery/devin.ts +3 -1
- package/src/models.json +1 -1
- package/src/provider-models/openai-compat.ts +231 -119
- package/src/types.ts +19 -1
|
@@ -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] =
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
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
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
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
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
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
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
architecture.
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
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
|
|
3331
|
-
kind: "
|
|
3396
|
+
baseUrl,
|
|
3397
|
+
kind: "video",
|
|
3332
3398
|
reasoning: false,
|
|
3333
|
-
input: ["text"],
|
|
3399
|
+
input: ["text", "image"],
|
|
3334
3400
|
supportsTools: false,
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
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 (
|
|
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
|
-
|
|
6333
|
-
|
|
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 =
|
|
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 = [
|
|
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. */
|