@oh-my-pi/pi-catalog 18.2.6 → 18.2.7
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 +14 -0
- 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 +20 -0
- package/package.json +4 -4
- 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 +5 -3
- 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 +3 -0
- package/src/compat/rules/providers/openrouter.kdl +24 -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 +1 -1
- package/src/compat/taxonomy.ts +92 -31
- package/src/compat/types.ts +22 -7
- 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 +246 -79
- package/src/provider-models/special.ts +53 -0
- package/src/types.ts +33 -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,17 @@ 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
|
+
MODEL_KINDS,
|
|
33
|
+
type Api,
|
|
34
|
+
type FetchImpl,
|
|
35
|
+
type Model,
|
|
36
|
+
type ModelKind,
|
|
37
|
+
type ModelSpec,
|
|
38
|
+
type OpenAICompat,
|
|
39
|
+
type Provider,
|
|
40
|
+
type ThinkingConfig,
|
|
41
|
+
} from "../types";
|
|
32
42
|
import { discoveryFetch, isAnthropicOAuthToken, isRecord, toBoolean, toNumber, toPositiveNumber } from "../utils";
|
|
33
43
|
import { ALIBABA_TOKEN_PLAN_BASE_URL, parseAlibabaTokenPlanCredential } from "../wire/alibaba-token-plan";
|
|
34
44
|
import { normalizeCharmHyperBaseUrl } from "../wire/charm-hyper";
|
|
@@ -96,6 +106,7 @@ const ANTHROPIC_OAUTH_BETA =
|
|
|
96
106
|
export interface ModelsDevModel {
|
|
97
107
|
id?: string;
|
|
98
108
|
name?: string;
|
|
109
|
+
kind?: string;
|
|
99
110
|
tool_call?: boolean;
|
|
100
111
|
reasoning?: boolean;
|
|
101
112
|
reasoning_options?: Array<{ type?: string; values?: string[]; min?: number; max?: number }>;
|
|
@@ -111,6 +122,7 @@ export interface ModelsDevModel {
|
|
|
111
122
|
};
|
|
112
123
|
modalities?: {
|
|
113
124
|
input?: string[];
|
|
125
|
+
output?: string[];
|
|
114
126
|
};
|
|
115
127
|
status?: string;
|
|
116
128
|
provider?: { npm?: string };
|
|
@@ -517,8 +529,11 @@ function mapWithBundledReference<TApi extends Api>(
|
|
|
517
529
|
name,
|
|
518
530
|
};
|
|
519
531
|
}
|
|
532
|
+
// Generic `/models` rows describe the chat roster. Do not make a bundled
|
|
533
|
+
// runner kind look endpoint-authored merely because its metadata is reused.
|
|
534
|
+
const { kind: _inheritedKind, ...chatReference } = reference;
|
|
520
535
|
return {
|
|
521
|
-
...
|
|
536
|
+
...chatReference,
|
|
522
537
|
id: defaults.id,
|
|
523
538
|
name,
|
|
524
539
|
api: defaults.api,
|
|
@@ -1434,8 +1449,11 @@ function mapDeepinfraModel(
|
|
|
1434
1449
|
: referenceMaxTokens !== null && contextWindow !== null
|
|
1435
1450
|
? Math.min(referenceMaxTokens, contextWindow)
|
|
1436
1451
|
: referenceMaxTokens;
|
|
1452
|
+
// This endpoint is filtered to `chat`; a same-id runner reference may lend
|
|
1453
|
+
// metadata, but its kind is not evidence that chat discovery advertised it.
|
|
1454
|
+
const { kind: _inheritedKind, ...chatReference } = reference ?? {};
|
|
1437
1455
|
return {
|
|
1438
|
-
...
|
|
1456
|
+
...chatReference,
|
|
1439
1457
|
id,
|
|
1440
1458
|
name: reference?.name ?? id,
|
|
1441
1459
|
api: "openai-completions",
|
|
@@ -1691,8 +1709,7 @@ function mergeCuratedIntoModel(
|
|
|
1691
1709
|
* window, reasoning flags, or the effort-dial allowlist.
|
|
1692
1710
|
*
|
|
1693
1711
|
* Three passes:
|
|
1694
|
-
* 1. Filter
|
|
1695
|
-
* surfaces routed through dedicated tools — generate_image, tts).
|
|
1712
|
+
* 1. Filter KDL exclusions and runner seed ids out of the chat roster.
|
|
1696
1713
|
* 2. Overlay curated metadata onto dynamic-fetch matches. xAI's /v1/models
|
|
1697
1714
|
* does not return context_window or reasoning metadata, so without
|
|
1698
1715
|
* this overlay the runtime falls back to the bundled-reference default
|
|
@@ -1708,8 +1725,13 @@ function mergeCuratedIntoModel(
|
|
|
1708
1725
|
* in original order.
|
|
1709
1726
|
*/
|
|
1710
1727
|
function applyXAIOAuthCuration(dynamic: readonly ModelSpec<"openai-responses">[]): ModelSpec<"openai-responses">[] {
|
|
1711
|
-
const
|
|
1712
|
-
const
|
|
1728
|
+
const curatedModels: ModelSpec<"openai-responses">[] = [];
|
|
1729
|
+
const runnerIds = new Set<string>();
|
|
1730
|
+
for (const seed of seedModels("xai-oauth")) {
|
|
1731
|
+
if (isResponsesSeed(seed)) curatedModels.push(seed);
|
|
1732
|
+
else runnerIds.add(seed.id);
|
|
1733
|
+
}
|
|
1734
|
+
const filtered = dynamic.filter(e => !runnerIds.has(e.id) && !isExcludedModel("xai-oauth", e.id));
|
|
1713
1735
|
|
|
1714
1736
|
const byId = new Map<string, ModelSpec<"openai-responses">>(filtered.map(e => [e.id, e]));
|
|
1715
1737
|
for (const curated of curatedModels) {
|
|
@@ -1737,13 +1759,20 @@ function applyXAIOAuthCuration(dynamic: readonly ModelSpec<"openai-responses">[]
|
|
|
1737
1759
|
return [...curatedFirst, ...rest];
|
|
1738
1760
|
}
|
|
1739
1761
|
|
|
1762
|
+
function isResponsesSeed(seed: ModelSpec<Api>): seed is ModelSpec<"openai-responses"> {
|
|
1763
|
+
return seed.api === "openai-responses";
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1740
1766
|
/**
|
|
1741
1767
|
* Render the xai-oauth KDL seed as the static runtime fallback consumed by
|
|
1742
1768
|
* {@link xaiOAuthModelManagerOptions}.
|
|
1743
1769
|
*/
|
|
1744
|
-
export function buildXaiOAuthStaticSeed(baseUrl?: string): ModelSpec<
|
|
1770
|
+
export function buildXaiOAuthStaticSeed(baseUrl?: string): ModelSpec<Api>[] {
|
|
1745
1771
|
const resolvedBaseUrl = baseUrl ?? "https://api.x.ai/v1";
|
|
1746
|
-
return seedModels
|
|
1772
|
+
return seedModels("xai-oauth").map(seed => {
|
|
1773
|
+
if (!isResponsesSeed(seed)) {
|
|
1774
|
+
return { ...seed, baseUrl: resolvedBaseUrl };
|
|
1775
|
+
}
|
|
1747
1776
|
const base: ModelSpec<"openai-responses"> = {
|
|
1748
1777
|
...seed,
|
|
1749
1778
|
baseUrl: resolvedBaseUrl,
|
|
@@ -1753,9 +1782,7 @@ export function buildXaiOAuthStaticSeed(baseUrl?: string): ModelSpec<"openai-res
|
|
|
1753
1782
|
});
|
|
1754
1783
|
}
|
|
1755
1784
|
|
|
1756
|
-
export function xaiOAuthModelManagerOptions(
|
|
1757
|
-
config?: XaiOAuthModelManagerConfig,
|
|
1758
|
-
): ModelManagerOptions<"openai-responses"> {
|
|
1785
|
+
export function xaiOAuthModelManagerOptions(config?: XaiOAuthModelManagerConfig): ModelManagerOptions<Api> {
|
|
1759
1786
|
const defaultBaseUrl = "https://api.x.ai/v1";
|
|
1760
1787
|
const resolvedBaseUrl = config?.baseUrl ?? defaultBaseUrl;
|
|
1761
1788
|
const base = createOpenAICompatibleModelManagerOptions({
|
|
@@ -3161,6 +3188,14 @@ export interface OpenRouterModelManagerConfig {
|
|
|
3161
3188
|
fetch?: FetchImpl;
|
|
3162
3189
|
}
|
|
3163
3190
|
|
|
3191
|
+
/**
|
|
3192
|
+
* OpenRouter's Decisions API lives at `/api/alpha`, a sibling of the `/api/v1`
|
|
3193
|
+
* chat root; derive it so a custom gateway base URL keeps both aligned.
|
|
3194
|
+
*/
|
|
3195
|
+
function openrouterDecisionsBaseUrl(chatBaseUrl: string): string {
|
|
3196
|
+
return chatBaseUrl.endsWith("/v1") ? `${chatBaseUrl.slice(0, -"/v1".length)}/alpha` : `${chatBaseUrl}/alpha`;
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3164
3199
|
function mapOpenRouterThinking(entry: OpenAICompatibleModelRecord): ThinkingConfig | undefined {
|
|
3165
3200
|
const reasoning = entry.reasoning;
|
|
3166
3201
|
if (!isRecord(reasoning)) return undefined;
|
|
@@ -3180,11 +3215,10 @@ function mapOpenRouterThinking(entry: OpenAICompatibleModelRecord): ThinkingConf
|
|
|
3180
3215
|
};
|
|
3181
3216
|
}
|
|
3182
3217
|
|
|
3183
|
-
export function openrouterModelManagerOptions(
|
|
3184
|
-
config?: OpenRouterModelManagerConfig,
|
|
3185
|
-
): ModelManagerOptions<"openrouter"> {
|
|
3218
|
+
export function openrouterModelManagerOptions(config?: OpenRouterModelManagerConfig): ModelManagerOptions<Api> {
|
|
3186
3219
|
const apiKey = config?.apiKey;
|
|
3187
|
-
const baseUrl = config?.baseUrl ?? "https://openrouter.ai/api/v1";
|
|
3220
|
+
const baseUrl = (config?.baseUrl ?? "https://openrouter.ai/api/v1").replace(/\/+$/g, "");
|
|
3221
|
+
const decisionsBaseUrl = openrouterDecisionsBaseUrl(baseUrl);
|
|
3188
3222
|
const references = createBundledReferenceMap<"openrouter">("openrouter");
|
|
3189
3223
|
return {
|
|
3190
3224
|
providerId: "openrouter",
|
|
@@ -3192,55 +3226,147 @@ export function openrouterModelManagerOptions(
|
|
|
3192
3226
|
// Namespace the refreshed pseudo-API cache separately so those rows cannot
|
|
3193
3227
|
// override bundled `api: "openrouter"` models during online-if-uncached startup.
|
|
3194
3228
|
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
|
-
|
|
3229
|
+
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;
|
|
3217
3259
|
|
|
3218
|
-
|
|
3260
|
+
const supportsToolChoice = params.includes("tool_choice");
|
|
3219
3261
|
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
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,
|
|
3299
|
+
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,
|
|
3307
|
+
}),
|
|
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 {
|
|
3329
|
+
...defaults,
|
|
3330
|
+
baseUrl: decisionsBaseUrl,
|
|
3331
|
+
kind: "judge",
|
|
3332
|
+
reasoning: false,
|
|
3333
|
+
input: ["text"],
|
|
3334
|
+
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
|
+
]);
|
|
3351
|
+
|
|
3352
|
+
if (imageModels === null) {
|
|
3353
|
+
logger.warn("OpenRouter image model discovery unavailable; preserving chat model discovery", {
|
|
3354
|
+
endpoint: `${baseUrl}/images/models`,
|
|
3355
|
+
});
|
|
3356
|
+
}
|
|
3357
|
+
if (decisionModels === null) {
|
|
3358
|
+
logger.warn("OpenRouter decision model discovery unavailable; preserving chat model discovery", {
|
|
3359
|
+
endpoint: `${baseUrl}/models?output_modalities=decisions`,
|
|
3360
|
+
});
|
|
3361
|
+
}
|
|
3362
|
+
if (chatModels === null && imageModels === null && decisionModels === null) return null;
|
|
3363
|
+
|
|
3364
|
+
const models = new Map<string, ModelSpec<Api>>();
|
|
3365
|
+
for (const model of chatModels ?? []) models.set(model.id, model);
|
|
3366
|
+
for (const model of imageModels ?? []) models.set(model.id, model);
|
|
3367
|
+
for (const model of decisionModels ?? []) models.set(model.id, model);
|
|
3368
|
+
return Array.from(models.values()).sort((left, right) => left.id.localeCompare(right.id));
|
|
3369
|
+
},
|
|
3244
3370
|
};
|
|
3245
3371
|
}
|
|
3246
3372
|
|
|
@@ -6174,32 +6300,69 @@ export function mapModelsDevToModels(
|
|
|
6174
6300
|
descriptors: readonly ModelsDevProviderDescriptor[],
|
|
6175
6301
|
): ModelSpec<Api>[] {
|
|
6176
6302
|
const models: ModelSpec<Api>[] = [];
|
|
6303
|
+
const providers = providerEntries();
|
|
6177
6304
|
for (const desc of descriptors) {
|
|
6178
|
-
const providerData =
|
|
6305
|
+
const providerData = data[desc.modelsDevKey];
|
|
6179
6306
|
if (!isRecord(providerData) || !isRecord(providerData.models)) continue;
|
|
6180
6307
|
|
|
6181
|
-
for (const
|
|
6308
|
+
for (const modelId in providerData.models) {
|
|
6309
|
+
const rawModel = providerData.models[modelId];
|
|
6182
6310
|
if (!isRecord(rawModel)) continue;
|
|
6183
6311
|
const m = rawModel as ModelsDevModel;
|
|
6312
|
+
const name = toModelName(m.name, modelId);
|
|
6313
|
+
let kind: ModelKind | undefined;
|
|
6314
|
+
let kindApi: Api | undefined;
|
|
6315
|
+
|
|
6316
|
+
if (m.kind !== undefined) {
|
|
6317
|
+
const policy = resolveModelPolicy({
|
|
6318
|
+
id: modelId,
|
|
6319
|
+
name,
|
|
6320
|
+
api: desc.api,
|
|
6321
|
+
provider: desc.providerId,
|
|
6322
|
+
baseUrl: desc.baseUrl,
|
|
6323
|
+
reasoning: false,
|
|
6324
|
+
input: ["text"],
|
|
6325
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
6326
|
+
contextWindow: null,
|
|
6327
|
+
maxTokens: null,
|
|
6328
|
+
});
|
|
6329
|
+
const normalizedKind = policy.catalog.kind ?? m.kind;
|
|
6330
|
+
if (typeof normalizedKind !== "string" || !MODEL_KINDS.some(value => value === normalizedKind)) continue;
|
|
6331
|
+
if (normalizedKind !== "chat") {
|
|
6332
|
+
if (normalizedKind !== "image" && normalizedKind !== "tts" && normalizedKind !== "stt") continue;
|
|
6333
|
+
kindApi = providers[desc.providerId]?.kindApis?.[normalizedKind];
|
|
6334
|
+
if (kindApi === undefined) continue;
|
|
6335
|
+
kind = normalizedKind;
|
|
6336
|
+
}
|
|
6337
|
+
}
|
|
6184
6338
|
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
if (
|
|
6188
|
-
|
|
6189
|
-
if (m.tool_call !== true)
|
|
6339
|
+
if (kind === undefined) {
|
|
6340
|
+
// Ordinary chat rows retain the provider-specific/default tool filter.
|
|
6341
|
+
if (desc.filterModel) {
|
|
6342
|
+
if (!desc.filterModel(modelId, m)) continue;
|
|
6343
|
+
} else if (m.tool_call !== true) {
|
|
6344
|
+
continue;
|
|
6345
|
+
}
|
|
6190
6346
|
}
|
|
6191
6347
|
|
|
6192
|
-
//
|
|
6193
|
-
|
|
6348
|
+
// Non-chat rows use the provider-authored runner API; chat rows retain
|
|
6349
|
+
// per-model API/base URL resolution (for example OpenCode route pins).
|
|
6350
|
+
let resolved: { api: Api; baseUrl: string } | null;
|
|
6351
|
+
if (kind === undefined) {
|
|
6352
|
+
resolved = desc.resolveApi?.(modelId, m) ?? { api: desc.api, baseUrl: desc.baseUrl };
|
|
6353
|
+
} else {
|
|
6354
|
+
if (kindApi === undefined) continue;
|
|
6355
|
+
resolved = { api: kindApi, baseUrl: desc.baseUrl };
|
|
6356
|
+
}
|
|
6194
6357
|
if (!resolved) continue;
|
|
6195
6358
|
|
|
6196
6359
|
const mapped: ModelSpec<Api> = {
|
|
6197
6360
|
id: modelId,
|
|
6198
|
-
name
|
|
6361
|
+
name,
|
|
6199
6362
|
api: resolved.api,
|
|
6200
|
-
provider: desc.providerId
|
|
6363
|
+
provider: desc.providerId,
|
|
6201
6364
|
baseUrl: resolved.baseUrl,
|
|
6202
|
-
reasoning: m.reasoning === true,
|
|
6365
|
+
reasoning: kind === undefined && m.reasoning === true,
|
|
6203
6366
|
input: toInputCapabilities(m.modalities?.input),
|
|
6204
6367
|
cost: {
|
|
6205
6368
|
input: toNumber(m.cost?.input) ?? 0,
|
|
@@ -6209,15 +6372,19 @@ export function mapModelsDevToModels(
|
|
|
6209
6372
|
},
|
|
6210
6373
|
contextWindow: toPositiveNumber(m.limit?.context, desc.defaultContextWindow ?? null),
|
|
6211
6374
|
maxTokens: toPositiveNumber(m.limit?.output, desc.defaultMaxTokens ?? null),
|
|
6375
|
+
...(kind !== undefined ? { kind, supportsTools: false } : {}),
|
|
6212
6376
|
...(m.int != null ? { int: m.int } : {}),
|
|
6213
6377
|
...(m.tps != null ? { tps: m.tps } : {}),
|
|
6214
|
-
...(m.tool_call === false ? { supportsTools: false } : {}),
|
|
6378
|
+
...(kind === undefined && m.tool_call === false ? { supportsTools: false } : {}),
|
|
6215
6379
|
...(desc.compat && { compat: desc.compat }),
|
|
6216
6380
|
...(desc.headers && { headers: { ...desc.headers } }),
|
|
6217
6381
|
};
|
|
6218
6382
|
|
|
6219
|
-
//
|
|
6220
|
-
|
|
6383
|
+
// Provider transforms are chat-specific. Normalized non-chat rows are
|
|
6384
|
+
// complete once their authored runner API has been assigned.
|
|
6385
|
+
if (kind !== undefined) {
|
|
6386
|
+
models.push(mapped);
|
|
6387
|
+
} else if (desc.transformModel) {
|
|
6221
6388
|
const result = desc.transformModel(mapped, modelId, m);
|
|
6222
6389
|
if (result === null) continue;
|
|
6223
6390
|
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
|
// ---------------------------------------------------------------------------
|
package/src/types.ts
CHANGED
|
@@ -23,6 +23,29 @@ export type KnownApi =
|
|
|
23
23
|
| "devin-agent";
|
|
24
24
|
export type Api = KnownApi | (string & {});
|
|
25
25
|
|
|
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;
|
|
28
|
+
/** Technical capability of a catalog model; absent model kinds mean chat. */
|
|
29
|
+
export type ModelKind = (typeof MODEL_KINDS)[number];
|
|
30
|
+
/** Grounding transport available to chat models selected by the web role. */
|
|
31
|
+
export type WebSearchGrounding = "gemini" | "anthropic" | "codex" | "xai" | "openrouter";
|
|
32
|
+
/** Non-chat runner protocols accepted by catalog seeds, outside the chat dispatch union. */
|
|
33
|
+
export const RUNNER_APIS = [
|
|
34
|
+
"local-inference",
|
|
35
|
+
"web-search",
|
|
36
|
+
"typesafe",
|
|
37
|
+
"openrouter-decisions",
|
|
38
|
+
"openai-images",
|
|
39
|
+
"openrouter-images",
|
|
40
|
+
"xai-tts",
|
|
41
|
+
"openai-speech",
|
|
42
|
+
] as const;
|
|
43
|
+
|
|
44
|
+
/** Resolve a model's kind while preserving chat semantics for existing catalog rows. */
|
|
45
|
+
export function modelKind(model: Pick<Model, "kind">): ModelKind {
|
|
46
|
+
return model.kind ?? "chat";
|
|
47
|
+
}
|
|
48
|
+
|
|
26
49
|
/** Canonical thinking transport used by a model. */
|
|
27
50
|
export type ThinkingControlMode =
|
|
28
51
|
| "effort"
|
|
@@ -1125,6 +1148,10 @@ export type ModelTokenizer =
|
|
|
1125
1148
|
// Model interface for the unified model system
|
|
1126
1149
|
export interface Model<TApi extends Api = Api> {
|
|
1127
1150
|
id: string;
|
|
1151
|
+
/** Role-specific runner capability; omitted for ordinary chat models. */
|
|
1152
|
+
kind?: ModelKind;
|
|
1153
|
+
/** Grounding transport supported by this chat model. */
|
|
1154
|
+
webSearch?: WebSearchGrounding;
|
|
1128
1155
|
/**
|
|
1129
1156
|
* Structured model identity resolved by the compat engine: vendor lineage
|
|
1130
1157
|
* class, product family, and revision. Baked into models.json rows and
|
|
@@ -1167,6 +1194,12 @@ export interface Model<TApi extends Api = Api> {
|
|
|
1167
1194
|
name: string;
|
|
1168
1195
|
api: TApi;
|
|
1169
1196
|
provider: Provider;
|
|
1197
|
+
/**
|
|
1198
|
+
* Discovery backend whose catalog policy applies when it differs from the
|
|
1199
|
+
* credential-bearing provider id. Persisted so cached and rebuilt custom
|
|
1200
|
+
* providers retain their transport backend's policy.
|
|
1201
|
+
*/
|
|
1202
|
+
providerType?: string;
|
|
1170
1203
|
baseUrl: string;
|
|
1171
1204
|
reasoning: boolean;
|
|
1172
1205
|
/**
|