@oh-my-pi/pi-catalog 18.2.6 → 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 +24 -3
- package/README.md +18 -18
- package/THIRD-PARTY-NOTICES.txt +0 -37
- package/dist/types/build.d.ts +7 -1
- package/dist/types/compat/auth-ids.d.ts +1 -1
- package/dist/types/compat/axes.d.ts +1 -1
- package/dist/types/compat/cascade.d.ts +3 -2
- package/dist/types/compat/provider-ids.d.ts +1 -1
- package/dist/types/compat/resolve.d.ts +2 -0
- package/dist/types/compat/taxonomy.d.ts +2 -2
- package/dist/types/compat/types.d.ts +18 -7
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/discovery/openai-compatible.d.ts +2 -0
- package/dist/types/discovery/typesafe.d.ts +26 -0
- package/dist/types/model-manager.d.ts +1 -1
- package/dist/types/provider-models/openai-compat.d.ts +6 -4
- package/dist/types/provider-models/special.d.ts +10 -0
- package/dist/types/types.d.ts +23 -0
- package/package.json +38 -38
- package/src/build.ts +27 -5
- package/src/compat/auth-ids.ts +2 -0
- package/src/compat/axes.ts +9 -1
- package/src/compat/cascade.ts +45 -23
- package/src/compat/provider-ids.ts +3 -0
- package/src/compat/resolve.ts +24 -14
- package/src/compat/rules/README.md +38 -35
- package/src/compat/rules/auth/local.kdl +4 -0
- package/src/compat/rules/auth/typesafe.kdl +3 -6
- package/src/compat/rules/auth/web.kdl +4 -0
- package/src/compat/rules/classes/qwen.kdl +6 -5
- package/src/compat/rules/providers/anthropic.kdl +1 -0
- package/src/compat/rules/providers/deepinfra.kdl +28 -0
- package/src/compat/rules/providers/google-antigravity.kdl +17 -0
- package/src/compat/rules/providers/google.kdl +9 -0
- package/src/compat/rules/providers/llama.cpp.kdl +23 -0
- package/src/compat/rules/providers/local.kdl +110 -0
- package/src/compat/rules/providers/openai-codex.kdl +18 -0
- package/src/compat/rules/providers/openai.kdl +61 -0
- package/src/compat/rules/providers/openrouter.kdl +132 -0
- package/src/compat/rules/providers/typesafe.kdl +21 -0
- package/src/compat/rules/providers/web.kdl +153 -0
- package/src/compat/rules/providers/xai-oauth.kdl +26 -0
- package/src/compat/rules/providers/xai.kdl +22 -0
- package/src/compat/rules/runtime/behavior.kdl +2 -1
- package/src/compat/rules/taxonomy/qwen.kdl +2 -0
- package/src/compat/rules.json +12526 -1
- package/src/compat/taxonomy.ts +92 -31
- package/src/compat/types.ts +22 -7
- package/src/discovery/devin.ts +3 -1
- package/src/discovery/index.ts +1 -0
- package/src/discovery/openai-compatible.ts +4 -1
- package/src/discovery/typesafe.ts +118 -0
- package/src/model-manager.ts +44 -15
- package/src/models.json +1 -1
- package/src/provider-models/descriptors.ts +6 -0
- package/src/provider-models/openai-compat.ts +357 -78
- package/src/provider-models/special.ts +53 -0
- package/src/types.ts +51 -0
|
@@ -77,6 +77,9 @@ import {
|
|
|
77
77
|
cursorModelManagerOptions,
|
|
78
78
|
devinModelManagerOptions,
|
|
79
79
|
gitLabDuoWorkflowModelManagerOptions,
|
|
80
|
+
localModelManagerOptions,
|
|
81
|
+
typesafeModelManagerOptions,
|
|
82
|
+
webModelManagerOptions,
|
|
80
83
|
zaiModelManagerOptions,
|
|
81
84
|
} from "./special";
|
|
82
85
|
|
|
@@ -114,6 +117,7 @@ const MODEL_MANAGER_FACTORIES: Readonly<Partial<Record<KnownProvider, ModelManag
|
|
|
114
117
|
kilo: config => kiloModelManagerOptions(config),
|
|
115
118
|
"kimi-code": config => kimiCodeModelManagerOptions(config),
|
|
116
119
|
litellm: config => litellmModelManagerOptions(config),
|
|
120
|
+
local: () => localModelManagerOptions(),
|
|
117
121
|
"lm-studio": config => lmStudioModelManagerOptions(config),
|
|
118
122
|
mistral: config => mistralModelManagerOptions(config),
|
|
119
123
|
"muse-code": config => museCodeModelManagerOptions(config),
|
|
@@ -135,11 +139,13 @@ const MODEL_MANAGER_FACTORIES: Readonly<Partial<Record<KnownProvider, ModelManag
|
|
|
135
139
|
"siliconflow-cn": config => siliconflowCnModelManagerOptions(config),
|
|
136
140
|
synthetic: config => syntheticModelManagerOptions(config),
|
|
137
141
|
together: config => togetherModelManagerOptions(config),
|
|
142
|
+
typesafe: config => typesafeModelManagerOptions(config),
|
|
138
143
|
umans: config => umansModelManagerOptions(config),
|
|
139
144
|
venice: config => veniceModelManagerOptions(config),
|
|
140
145
|
"vercel-ai-gateway": config => vercelAiGatewayModelManagerOptions(config),
|
|
141
146
|
vllm: config => vllmModelManagerOptions(config),
|
|
142
147
|
"wafer-serverless": config => waferServerlessModelManagerOptions(config),
|
|
148
|
+
web: () => webModelManagerOptions(),
|
|
143
149
|
coreweave: config => coreWeaveModelManagerOptions(config),
|
|
144
150
|
xai: config => xaiModelManagerOptions(config),
|
|
145
151
|
"xai-oauth": config => xaiOAuthModelManagerOptions(config),
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import { xaiResponsesReasoningEffortMap } from "../compat/openai";
|
|
15
15
|
import { hasModelScopedEffortLadder, resolveModelPolicy } from "../compat/resolve";
|
|
16
16
|
import { compareRevision, parseRevision } from "../compat/revision";
|
|
17
|
-
import { seedModels } from "../compat/providers";
|
|
17
|
+
import { providerEntries, seedModels } from "../compat/providers";
|
|
18
18
|
import { billingVariantPlain, classifyModel, discoveryVocabulary } from "../compat/taxonomy";
|
|
19
19
|
import {
|
|
20
20
|
DEFAULT_OPENAI_COMPATIBLE_DISCOVERY_TIMEOUT_MS,
|
|
@@ -28,7 +28,18 @@ import { getBundledModelReferenceIndex } from "../identity/bundled";
|
|
|
28
28
|
import { resolveModelReference } from "../identity/reference";
|
|
29
29
|
import type { ModelManagerOptions, ModelsDevFallback } from "../model-manager";
|
|
30
30
|
import { type GeneratedProvider, getBundledModels } from "../models";
|
|
31
|
-
import
|
|
31
|
+
import {
|
|
32
|
+
KIND_API_KINDS,
|
|
33
|
+
MODEL_KINDS,
|
|
34
|
+
type Api,
|
|
35
|
+
type FetchImpl,
|
|
36
|
+
type Model,
|
|
37
|
+
type ModelKind,
|
|
38
|
+
type ModelSpec,
|
|
39
|
+
type OpenAICompat,
|
|
40
|
+
type Provider,
|
|
41
|
+
type ThinkingConfig,
|
|
42
|
+
} from "../types";
|
|
32
43
|
import { discoveryFetch, isAnthropicOAuthToken, isRecord, toBoolean, toNumber, toPositiveNumber } from "../utils";
|
|
33
44
|
import { ALIBABA_TOKEN_PLAN_BASE_URL, parseAlibabaTokenPlanCredential } from "../wire/alibaba-token-plan";
|
|
34
45
|
import { normalizeCharmHyperBaseUrl } from "../wire/charm-hyper";
|
|
@@ -96,6 +107,7 @@ const ANTHROPIC_OAUTH_BETA =
|
|
|
96
107
|
export interface ModelsDevModel {
|
|
97
108
|
id?: string;
|
|
98
109
|
name?: string;
|
|
110
|
+
kind?: string;
|
|
99
111
|
tool_call?: boolean;
|
|
100
112
|
reasoning?: boolean;
|
|
101
113
|
reasoning_options?: Array<{ type?: string; values?: string[]; min?: number; max?: number }>;
|
|
@@ -111,6 +123,7 @@ export interface ModelsDevModel {
|
|
|
111
123
|
};
|
|
112
124
|
modalities?: {
|
|
113
125
|
input?: string[];
|
|
126
|
+
output?: string[];
|
|
114
127
|
};
|
|
115
128
|
status?: string;
|
|
116
129
|
provider?: { npm?: string };
|
|
@@ -517,8 +530,11 @@ function mapWithBundledReference<TApi extends Api>(
|
|
|
517
530
|
name,
|
|
518
531
|
};
|
|
519
532
|
}
|
|
533
|
+
// Generic `/models` rows describe the chat roster. Do not make a bundled
|
|
534
|
+
// runner kind look endpoint-authored merely because its metadata is reused.
|
|
535
|
+
const { kind: _inheritedKind, ...chatReference } = reference;
|
|
520
536
|
return {
|
|
521
|
-
...
|
|
537
|
+
...chatReference,
|
|
522
538
|
id: defaults.id,
|
|
523
539
|
name,
|
|
524
540
|
api: defaults.api,
|
|
@@ -1434,8 +1450,11 @@ function mapDeepinfraModel(
|
|
|
1434
1450
|
: referenceMaxTokens !== null && contextWindow !== null
|
|
1435
1451
|
? Math.min(referenceMaxTokens, contextWindow)
|
|
1436
1452
|
: referenceMaxTokens;
|
|
1453
|
+
// This endpoint is filtered to `chat`; a same-id runner reference may lend
|
|
1454
|
+
// metadata, but its kind is not evidence that chat discovery advertised it.
|
|
1455
|
+
const { kind: _inheritedKind, ...chatReference } = reference ?? {};
|
|
1437
1456
|
return {
|
|
1438
|
-
...
|
|
1457
|
+
...chatReference,
|
|
1439
1458
|
id,
|
|
1440
1459
|
name: reference?.name ?? id,
|
|
1441
1460
|
api: "openai-completions",
|
|
@@ -1691,8 +1710,7 @@ function mergeCuratedIntoModel(
|
|
|
1691
1710
|
* window, reasoning flags, or the effort-dial allowlist.
|
|
1692
1711
|
*
|
|
1693
1712
|
* Three passes:
|
|
1694
|
-
* 1. Filter
|
|
1695
|
-
* surfaces routed through dedicated tools — generate_image, tts).
|
|
1713
|
+
* 1. Filter KDL exclusions and runner seed ids out of the chat roster.
|
|
1696
1714
|
* 2. Overlay curated metadata onto dynamic-fetch matches. xAI's /v1/models
|
|
1697
1715
|
* does not return context_window or reasoning metadata, so without
|
|
1698
1716
|
* this overlay the runtime falls back to the bundled-reference default
|
|
@@ -1708,8 +1726,13 @@ function mergeCuratedIntoModel(
|
|
|
1708
1726
|
* in original order.
|
|
1709
1727
|
*/
|
|
1710
1728
|
function applyXAIOAuthCuration(dynamic: readonly ModelSpec<"openai-responses">[]): ModelSpec<"openai-responses">[] {
|
|
1711
|
-
const
|
|
1712
|
-
const
|
|
1729
|
+
const curatedModels: ModelSpec<"openai-responses">[] = [];
|
|
1730
|
+
const runnerIds = new Set<string>();
|
|
1731
|
+
for (const seed of seedModels("xai-oauth")) {
|
|
1732
|
+
if (isResponsesSeed(seed)) curatedModels.push(seed);
|
|
1733
|
+
else runnerIds.add(seed.id);
|
|
1734
|
+
}
|
|
1735
|
+
const filtered = dynamic.filter(e => !runnerIds.has(e.id) && !isExcludedModel("xai-oauth", e.id));
|
|
1713
1736
|
|
|
1714
1737
|
const byId = new Map<string, ModelSpec<"openai-responses">>(filtered.map(e => [e.id, e]));
|
|
1715
1738
|
for (const curated of curatedModels) {
|
|
@@ -1737,13 +1760,20 @@ function applyXAIOAuthCuration(dynamic: readonly ModelSpec<"openai-responses">[]
|
|
|
1737
1760
|
return [...curatedFirst, ...rest];
|
|
1738
1761
|
}
|
|
1739
1762
|
|
|
1763
|
+
function isResponsesSeed(seed: ModelSpec<Api>): seed is ModelSpec<"openai-responses"> {
|
|
1764
|
+
return seed.api === "openai-responses";
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1740
1767
|
/**
|
|
1741
1768
|
* Render the xai-oauth KDL seed as the static runtime fallback consumed by
|
|
1742
1769
|
* {@link xaiOAuthModelManagerOptions}.
|
|
1743
1770
|
*/
|
|
1744
|
-
export function buildXaiOAuthStaticSeed(baseUrl?: string): ModelSpec<
|
|
1771
|
+
export function buildXaiOAuthStaticSeed(baseUrl?: string): ModelSpec<Api>[] {
|
|
1745
1772
|
const resolvedBaseUrl = baseUrl ?? "https://api.x.ai/v1";
|
|
1746
|
-
return seedModels
|
|
1773
|
+
return seedModels("xai-oauth").map(seed => {
|
|
1774
|
+
if (!isResponsesSeed(seed)) {
|
|
1775
|
+
return { ...seed, baseUrl: resolvedBaseUrl };
|
|
1776
|
+
}
|
|
1747
1777
|
const base: ModelSpec<"openai-responses"> = {
|
|
1748
1778
|
...seed,
|
|
1749
1779
|
baseUrl: resolvedBaseUrl,
|
|
@@ -1753,9 +1783,7 @@ export function buildXaiOAuthStaticSeed(baseUrl?: string): ModelSpec<"openai-res
|
|
|
1753
1783
|
});
|
|
1754
1784
|
}
|
|
1755
1785
|
|
|
1756
|
-
export function xaiOAuthModelManagerOptions(
|
|
1757
|
-
config?: XaiOAuthModelManagerConfig,
|
|
1758
|
-
): ModelManagerOptions<"openai-responses"> {
|
|
1786
|
+
export function xaiOAuthModelManagerOptions(config?: XaiOAuthModelManagerConfig): ModelManagerOptions<Api> {
|
|
1759
1787
|
const defaultBaseUrl = "https://api.x.ai/v1";
|
|
1760
1788
|
const resolvedBaseUrl = config?.baseUrl ?? defaultBaseUrl;
|
|
1761
1789
|
const base = createOpenAICompatibleModelManagerOptions({
|
|
@@ -3161,6 +3189,14 @@ export interface OpenRouterModelManagerConfig {
|
|
|
3161
3189
|
fetch?: FetchImpl;
|
|
3162
3190
|
}
|
|
3163
3191
|
|
|
3192
|
+
/**
|
|
3193
|
+
* OpenRouter's Decisions API lives at `/api/alpha`, a sibling of the `/api/v1`
|
|
3194
|
+
* chat root; derive it so a custom gateway base URL keeps both aligned.
|
|
3195
|
+
*/
|
|
3196
|
+
function openrouterDecisionsBaseUrl(chatBaseUrl: string): string {
|
|
3197
|
+
return chatBaseUrl.endsWith("/v1") ? `${chatBaseUrl.slice(0, -"/v1".length)}/alpha` : `${chatBaseUrl}/alpha`;
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3164
3200
|
function mapOpenRouterThinking(entry: OpenAICompatibleModelRecord): ThinkingConfig | undefined {
|
|
3165
3201
|
const reasoning = entry.reasoning;
|
|
3166
3202
|
if (!isRecord(reasoning)) return undefined;
|
|
@@ -3180,11 +3216,10 @@ function mapOpenRouterThinking(entry: OpenAICompatibleModelRecord): ThinkingConf
|
|
|
3180
3216
|
};
|
|
3181
3217
|
}
|
|
3182
3218
|
|
|
3183
|
-
export function openrouterModelManagerOptions(
|
|
3184
|
-
config?: OpenRouterModelManagerConfig,
|
|
3185
|
-
): ModelManagerOptions<"openrouter"> {
|
|
3219
|
+
export function openrouterModelManagerOptions(config?: OpenRouterModelManagerConfig): ModelManagerOptions<Api> {
|
|
3186
3220
|
const apiKey = config?.apiKey;
|
|
3187
|
-
const baseUrl = config?.baseUrl ?? "https://openrouter.ai/api/v1";
|
|
3221
|
+
const baseUrl = (config?.baseUrl ?? "https://openrouter.ai/api/v1").replace(/\/+$/g, "");
|
|
3222
|
+
const decisionsBaseUrl = openrouterDecisionsBaseUrl(baseUrl);
|
|
3188
3223
|
const references = createBundledReferenceMap<"openrouter">("openrouter");
|
|
3189
3224
|
return {
|
|
3190
3225
|
providerId: "openrouter",
|
|
@@ -3192,55 +3227,257 @@ export function openrouterModelManagerOptions(
|
|
|
3192
3227
|
// Namespace the refreshed pseudo-API cache separately so those rows cannot
|
|
3193
3228
|
// override bundled `api: "openrouter"` models during online-if-uncached startup.
|
|
3194
3229
|
cacheProviderId: resolveModelCacheProviderId("openrouter"),
|
|
3195
|
-
fetchDynamicModels: () =>
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3230
|
+
fetchDynamicModels: async () => {
|
|
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");
|
|
3219
3263
|
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
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
|
+
};
|
|
3230
3285
|
},
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
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,
|
|
3239
3310
|
}),
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
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",
|
|
3318
|
+
baseUrl,
|
|
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,
|
|
3352
|
+
}),
|
|
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"> => ({
|
|
3395
|
+
...defaults,
|
|
3396
|
+
baseUrl,
|
|
3397
|
+
kind: "video",
|
|
3398
|
+
reasoning: false,
|
|
3399
|
+
input: ["text", "image"],
|
|
3400
|
+
supportsTools: false,
|
|
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
|
+
]);
|
|
3435
|
+
|
|
3436
|
+
if (imageModels === null) {
|
|
3437
|
+
logger.warn("OpenRouter image model discovery unavailable; preserving chat model discovery", {
|
|
3438
|
+
endpoint: `${baseUrl}/images/models`,
|
|
3439
|
+
});
|
|
3440
|
+
}
|
|
3441
|
+
if (decisionModels === null) {
|
|
3442
|
+
logger.warn("OpenRouter decision model discovery unavailable; preserving chat model discovery", {
|
|
3443
|
+
endpoint: `${baseUrl}/models?output_modalities=decisions`,
|
|
3444
|
+
});
|
|
3445
|
+
}
|
|
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
|
+
}
|
|
3471
|
+
|
|
3472
|
+
const models = new Map<string, ModelSpec<Api>>();
|
|
3473
|
+
for (const model of chatModels ?? []) models.set(model.id, model);
|
|
3474
|
+
for (const model of imageModels ?? []) models.set(model.id, model);
|
|
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);
|
|
3479
|
+
return Array.from(models.values()).sort((left, right) => left.id.localeCompare(right.id));
|
|
3480
|
+
},
|
|
3244
3481
|
};
|
|
3245
3482
|
}
|
|
3246
3483
|
|
|
@@ -6174,32 +6411,70 @@ export function mapModelsDevToModels(
|
|
|
6174
6411
|
descriptors: readonly ModelsDevProviderDescriptor[],
|
|
6175
6412
|
): ModelSpec<Api>[] {
|
|
6176
6413
|
const models: ModelSpec<Api>[] = [];
|
|
6414
|
+
const providers = providerEntries();
|
|
6177
6415
|
for (const desc of descriptors) {
|
|
6178
|
-
const providerData =
|
|
6416
|
+
const providerData = data[desc.modelsDevKey];
|
|
6179
6417
|
if (!isRecord(providerData) || !isRecord(providerData.models)) continue;
|
|
6180
6418
|
|
|
6181
|
-
for (const
|
|
6419
|
+
for (const modelId in providerData.models) {
|
|
6420
|
+
const rawModel = providerData.models[modelId];
|
|
6182
6421
|
if (!isRecord(rawModel)) continue;
|
|
6183
6422
|
const m = rawModel as ModelsDevModel;
|
|
6423
|
+
const name = toModelName(m.name, modelId);
|
|
6424
|
+
let kind: ModelKind | undefined;
|
|
6425
|
+
let kindApi: Api | undefined;
|
|
6426
|
+
|
|
6427
|
+
if (m.kind !== undefined) {
|
|
6428
|
+
const policy = resolveModelPolicy({
|
|
6429
|
+
id: modelId,
|
|
6430
|
+
name,
|
|
6431
|
+
api: desc.api,
|
|
6432
|
+
provider: desc.providerId,
|
|
6433
|
+
baseUrl: desc.baseUrl,
|
|
6434
|
+
reasoning: false,
|
|
6435
|
+
input: ["text"],
|
|
6436
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
6437
|
+
contextWindow: null,
|
|
6438
|
+
maxTokens: null,
|
|
6439
|
+
});
|
|
6440
|
+
const normalizedKind = policy.catalog.kind ?? m.kind;
|
|
6441
|
+
if (typeof normalizedKind !== "string" || !MODEL_KINDS.some(value => value === normalizedKind)) continue;
|
|
6442
|
+
if (normalizedKind !== "chat") {
|
|
6443
|
+
const kindApiKind = KIND_API_KINDS.find(value => value === normalizedKind);
|
|
6444
|
+
if (kindApiKind === undefined) continue;
|
|
6445
|
+
kindApi = providers[desc.providerId]?.kindApis?.[kindApiKind];
|
|
6446
|
+
if (kindApi === undefined) continue;
|
|
6447
|
+
kind = kindApiKind;
|
|
6448
|
+
}
|
|
6449
|
+
}
|
|
6184
6450
|
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
if (
|
|
6188
|
-
|
|
6189
|
-
if (m.tool_call !== true)
|
|
6451
|
+
if (kind === undefined) {
|
|
6452
|
+
// Ordinary chat rows retain the provider-specific/default tool filter.
|
|
6453
|
+
if (desc.filterModel) {
|
|
6454
|
+
if (!desc.filterModel(modelId, m)) continue;
|
|
6455
|
+
} else if (m.tool_call !== true) {
|
|
6456
|
+
continue;
|
|
6457
|
+
}
|
|
6190
6458
|
}
|
|
6191
6459
|
|
|
6192
|
-
//
|
|
6193
|
-
|
|
6460
|
+
// Non-chat rows use the provider-authored runner API; chat rows retain
|
|
6461
|
+
// per-model API/base URL resolution (for example OpenCode route pins).
|
|
6462
|
+
let resolved: { api: Api; baseUrl: string } | null;
|
|
6463
|
+
if (kind === undefined) {
|
|
6464
|
+
resolved = desc.resolveApi?.(modelId, m) ?? { api: desc.api, baseUrl: desc.baseUrl };
|
|
6465
|
+
} else {
|
|
6466
|
+
if (kindApi === undefined) continue;
|
|
6467
|
+
resolved = { api: kindApi, baseUrl: desc.baseUrl };
|
|
6468
|
+
}
|
|
6194
6469
|
if (!resolved) continue;
|
|
6195
6470
|
|
|
6196
6471
|
const mapped: ModelSpec<Api> = {
|
|
6197
6472
|
id: modelId,
|
|
6198
|
-
name
|
|
6473
|
+
name,
|
|
6199
6474
|
api: resolved.api,
|
|
6200
|
-
provider: desc.providerId
|
|
6475
|
+
provider: desc.providerId,
|
|
6201
6476
|
baseUrl: resolved.baseUrl,
|
|
6202
|
-
reasoning: m.reasoning === true,
|
|
6477
|
+
reasoning: kind === undefined && m.reasoning === true,
|
|
6203
6478
|
input: toInputCapabilities(m.modalities?.input),
|
|
6204
6479
|
cost: {
|
|
6205
6480
|
input: toNumber(m.cost?.input) ?? 0,
|
|
@@ -6209,15 +6484,19 @@ export function mapModelsDevToModels(
|
|
|
6209
6484
|
},
|
|
6210
6485
|
contextWindow: toPositiveNumber(m.limit?.context, desc.defaultContextWindow ?? null),
|
|
6211
6486
|
maxTokens: toPositiveNumber(m.limit?.output, desc.defaultMaxTokens ?? null),
|
|
6487
|
+
...(kind !== undefined ? { kind, supportsTools: false } : {}),
|
|
6212
6488
|
...(m.int != null ? { int: m.int } : {}),
|
|
6213
6489
|
...(m.tps != null ? { tps: m.tps } : {}),
|
|
6214
|
-
...(m.tool_call === false ? { supportsTools: false } : {}),
|
|
6490
|
+
...(kind === undefined && m.tool_call === false ? { supportsTools: false } : {}),
|
|
6215
6491
|
...(desc.compat && { compat: desc.compat }),
|
|
6216
6492
|
...(desc.headers && { headers: { ...desc.headers } }),
|
|
6217
6493
|
};
|
|
6218
6494
|
|
|
6219
|
-
//
|
|
6220
|
-
|
|
6495
|
+
// Provider transforms are chat-specific. Normalized non-chat rows are
|
|
6496
|
+
// complete once their authored runner API has been assigned.
|
|
6497
|
+
if (kind !== undefined) {
|
|
6498
|
+
models.push(mapped);
|
|
6499
|
+
} else if (desc.transformModel) {
|
|
6221
6500
|
const result = desc.transformModel(mapped, modelId, m);
|
|
6222
6501
|
if (result === null) continue;
|
|
6223
6502
|
if (Array.isArray(result)) {
|
|
@@ -4,6 +4,7 @@ import { apiRouteFor } from "../compat/behavior";
|
|
|
4
4
|
import { seedModels } from "../compat/providers";
|
|
5
5
|
import { type CodexModelDiscoveryResult, fetchCodexModels } from "../discovery/codex";
|
|
6
6
|
import type { DevinModelDiscoveryOptions } from "../discovery/devin";
|
|
7
|
+
import { fetchTypeSafeModels, TYPESAFE_DEFAULT_BASE_URL } from "../discovery/typesafe";
|
|
7
8
|
import { buildGitLabDuoWorkflowFallbackModel, fetchGitLabDuoWorkflowModels } from "../discovery/gitlab-duo-workflow";
|
|
8
9
|
import type { ModelManagerOptions } from "../model-manager";
|
|
9
10
|
import { getBundledModel } from "../models";
|
|
@@ -352,6 +353,58 @@ export function devinModelManagerOptions(config: DevinModelManagerConfig = {}):
|
|
|
352
353
|
}
|
|
353
354
|
|
|
354
355
|
const devinDiscovery = once(() => import("../discovery/devin"));
|
|
356
|
+
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
// Synthetic role providers
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
export function localModelManagerOptions(): ModelManagerOptions<"local-inference"> {
|
|
362
|
+
return {
|
|
363
|
+
providerId: "local",
|
|
364
|
+
cacheProviderId: resolveModelCacheProviderId("local"),
|
|
365
|
+
staticModels: seedModels<"local-inference">("local"),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function webModelManagerOptions(): ModelManagerOptions<"web-search"> {
|
|
370
|
+
return {
|
|
371
|
+
providerId: "web",
|
|
372
|
+
cacheProviderId: resolveModelCacheProviderId("web"),
|
|
373
|
+
staticModels: seedModels<"web-search">("web"),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Credentials and endpoint overrides for the TypeSafe catalog manager. */
|
|
378
|
+
export interface TypeSafeModelManagerConfig {
|
|
379
|
+
apiKey?: string;
|
|
380
|
+
baseUrl?: string;
|
|
381
|
+
fetch?: FetchImpl;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Discover account-visible judge models while keeping the bundled offline seed. */
|
|
385
|
+
export function typesafeModelManagerOptions(config: TypeSafeModelManagerConfig = {}): ModelManagerOptions<"typesafe"> {
|
|
386
|
+
const { apiKey } = config;
|
|
387
|
+
const envBaseUrl = Bun.env.TYPESAFE_BASE_URL?.trim();
|
|
388
|
+
const baseUrl = (config.baseUrl ?? (envBaseUrl || TYPESAFE_DEFAULT_BASE_URL)).replace(/\/+$/, "");
|
|
389
|
+
const staticModels = seedModels<"typesafe">("typesafe");
|
|
390
|
+
return {
|
|
391
|
+
providerId: "typesafe",
|
|
392
|
+
cacheProviderId: resolveModelCacheProviderId("typesafe"),
|
|
393
|
+
staticModels: staticModels.map(model => ({ ...model, baseUrl })),
|
|
394
|
+
...(apiKey ? { dynamicModelsAuthoritative: true } : undefined),
|
|
395
|
+
...(apiKey
|
|
396
|
+
? {
|
|
397
|
+
fetchDynamicModels: () =>
|
|
398
|
+
fetchTypeSafeModels({
|
|
399
|
+
apiKey,
|
|
400
|
+
baseUrl,
|
|
401
|
+
fetch: config.fetch,
|
|
402
|
+
}),
|
|
403
|
+
}
|
|
404
|
+
: undefined),
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
355
408
|
// ---------------------------------------------------------------------------
|
|
356
409
|
// Zai
|
|
357
410
|
// ---------------------------------------------------------------------------
|