@oh-my-pi/pi-catalog 18.2.7 → 18.2.9
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 +26 -3
- package/README.md +18 -18
- package/dist/types/compat/auth-ids.d.ts +2 -2
- package/dist/types/compat/axes.d.ts +2 -2
- package/dist/types/compat/catalog-policy.d.ts +7 -0
- package/dist/types/compat/output-limits.d.ts +3 -0
- package/dist/types/compat/provider-ids.d.ts +1 -1
- package/dist/types/compat/tools.d.ts +5 -0
- package/dist/types/compat/types.d.ts +2 -2
- package/dist/types/provider-models/openai-compat.d.ts +26 -0
- package/dist/types/types.d.ts +5 -2
- package/dist/types/wire/singularityapi.d.ts +29 -0
- package/package.json +38 -38
- package/src/compat/auth-ids.ts +4 -0
- package/src/compat/axes.ts +21 -2
- package/src/compat/catalog-policy.ts +28 -0
- package/src/compat/output-limits.ts +12 -0
- package/src/compat/provider-ids.ts +2 -0
- package/src/compat/resolve.ts +4 -2
- package/src/compat/rules/README.md +34 -33
- package/src/compat/rules/auth/_order.kdl +1 -1
- package/src/compat/rules/auth/singularityapi-dev.kdl +22 -0
- package/src/compat/rules/auth/singularityapi-tech.kdl +23 -0
- package/src/compat/rules/classes/gpt-oss.kdl +3 -0
- package/src/compat/rules/classes/xai.kdl +3 -3
- package/src/compat/rules/providers/amazon-bedrock.kdl +1 -1
- package/src/compat/rules/providers/anthropic.kdl +1 -1
- package/src/compat/rules/providers/cloudflare-ai-gateway.kdl +1 -1
- package/src/compat/rules/providers/commandcode.kdl +1 -1
- package/src/compat/rules/providers/cursor.kdl +1 -1
- package/src/compat/rules/providers/google-antigravity.kdl +5 -0
- package/src/compat/rules/providers/kilo.kdl +1 -1
- package/src/compat/rules/providers/litellm.kdl +1 -1
- package/src/compat/rules/providers/native-tools.kdl +4 -0
- package/src/compat/rules/providers/openai.kdl +58 -0
- package/src/compat/rules/providers/opencode-zen.kdl +1 -1
- package/src/compat/rules/providers/openrouter.kdl +108 -0
- package/src/compat/rules/providers/output-limits.kdl +4 -0
- package/src/compat/rules/providers/singularityapi-dev.kdl +92 -0
- package/src/compat/rules/providers/singularityapi-tech.kdl +72 -0
- package/src/compat/rules/providers/tool-free-history.kdl +6 -0
- package/src/compat/rules/providers/vercel-ai-gateway.kdl +1 -1
- package/src/compat/rules/providers/xai-oauth.kdl +8 -1
- package/src/compat/rules/providers/xai.kdl +4 -4
- package/src/compat/rules/providers/xiaomi-token-plan-cn.kdl +23 -0
- package/src/compat/rules/providers/zenmux.kdl +1 -1
- package/src/compat/rules/runtime/behavior.kdl +2 -0
- package/src/compat/rules.json +1 -1
- package/src/compat/tools.ts +12 -0
- package/src/compat/types.ts +2 -2
- package/src/discovery/devin.ts +59 -34
- package/src/models.json +1 -1
- package/src/provider-models/cache-provider-id.ts +31 -0
- package/src/provider-models/descriptors.ts +4 -0
- package/src/provider-models/openai-compat.ts +390 -119
- package/src/types.ts +19 -1
- package/src/wire/singularityapi.ts +34 -0
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { CHARM_HYPER_API_BASE_URL, normalizeCharmHyperBaseUrl } from "../wire/charm-hyper";
|
|
2
2
|
import { PERSONAL_GITHUB_COPILOT_BASE_URL } from "../wire/github-copilot";
|
|
3
|
+
import {
|
|
4
|
+
SINGULARITYAPI_DEV_API_BASE_URL,
|
|
5
|
+
SINGULARITYAPI_TECH_API_BASE_URL,
|
|
6
|
+
normalizeSingularityApiBaseUrl,
|
|
7
|
+
} from "../wire/singularityapi";
|
|
3
8
|
|
|
4
9
|
export interface ModelCacheProviderIdOptions {
|
|
5
10
|
apiKey?: string;
|
|
@@ -11,6 +16,11 @@ const CREDENTIAL_SCOPED_MODEL_CACHE_PROVIDERS: Readonly<Record<string, true>> =
|
|
|
11
16
|
"opencode-zen": true,
|
|
12
17
|
"github-copilot": true,
|
|
13
18
|
"muse-code": true,
|
|
19
|
+
// Both SingularityAPI rosters are issued per key, so the namespace must be
|
|
20
|
+
// resolved with the credential (`hydrateCredentialScopedModelCaches`) rather
|
|
21
|
+
// than from the synchronous, credential-less startup read.
|
|
22
|
+
"singularityapi-dev": true,
|
|
23
|
+
"singularityapi-tech": true,
|
|
14
24
|
};
|
|
15
25
|
|
|
16
26
|
/** Whether a provider's model-cache namespace requires its resolved credential. */
|
|
@@ -87,6 +97,27 @@ export function resolveModelCacheProviderId(providerId: string, options: ModelCa
|
|
|
87
97
|
const scope = `${options.apiKey ?? ""}\u0000${baseUrl}`;
|
|
88
98
|
return `muse-code:models-v1:${Bun.hash(scope).toString(36)}`;
|
|
89
99
|
}
|
|
100
|
+
case "singularityapi-dev":
|
|
101
|
+
case "singularityapi-tech": {
|
|
102
|
+
// Both products issue their roster per key, and a configured proxy
|
|
103
|
+
// publishes its own. Discovery is authoritative, so a shared namespace
|
|
104
|
+
// would serve the previous key's roster for the full 24h TTL — including
|
|
105
|
+
// ids the current key cannot call. Hashing the pair means switching
|
|
106
|
+
// either re-runs discovery instead, and the provider-id prefix keeps the
|
|
107
|
+
// two products from ever reading each other's rows behind one proxy.
|
|
108
|
+
//
|
|
109
|
+
// Both call paths must land on one namespace: `ModelRegistry` resolves
|
|
110
|
+
// this provider through the credential-scoped hydration pass (it is in
|
|
111
|
+
// CREDENTIAL_SCOPED_MODEL_CACHE_PROVIDERS), while discovery hashes the
|
|
112
|
+
// `/v1`-suffixed endpoint the matching `singularityApi*ModelManagerOptions`
|
|
113
|
+
// passes — which is why both normalize through
|
|
114
|
+
// `normalizeSingularityApiBaseUrl` against their own canonical host.
|
|
115
|
+
const canonical =
|
|
116
|
+
providerId === "singularityapi-tech" ? SINGULARITYAPI_TECH_API_BASE_URL : SINGULARITYAPI_DEV_API_BASE_URL;
|
|
117
|
+
const baseUrl = normalizeSingularityApiBaseUrl(options.baseUrl, canonical);
|
|
118
|
+
const scope = `${options.apiKey ?? ""}\u0000${baseUrl}`;
|
|
119
|
+
return `${providerId}:models-v1:${Bun.hash(scope).toString(36)}`;
|
|
120
|
+
}
|
|
90
121
|
case "litellm": {
|
|
91
122
|
const baseUrl = options.baseUrl ?? getDefaultModelDiscoveryBaseUrl(providerId)!;
|
|
92
123
|
// rich-v11 invalidates rows that inherited ClinePass gateway metadata
|
|
@@ -59,6 +59,8 @@ import {
|
|
|
59
59
|
sakanaModelManagerOptions,
|
|
60
60
|
siliconflowCnModelManagerOptions,
|
|
61
61
|
siliconflowModelManagerOptions,
|
|
62
|
+
singularityApiDevModelManagerOptions,
|
|
63
|
+
singularityApiTechModelManagerOptions,
|
|
62
64
|
syntheticModelManagerOptions,
|
|
63
65
|
togetherModelManagerOptions,
|
|
64
66
|
umansModelManagerOptions,
|
|
@@ -137,6 +139,8 @@ const MODEL_MANAGER_FACTORIES: Readonly<Partial<Record<KnownProvider, ModelManag
|
|
|
137
139
|
sakana: config => sakanaModelManagerOptions(config),
|
|
138
140
|
siliconflow: config => siliconflowModelManagerOptions(config),
|
|
139
141
|
"siliconflow-cn": config => siliconflowCnModelManagerOptions(config),
|
|
142
|
+
"singularityapi-dev": config => singularityApiDevModelManagerOptions(config),
|
|
143
|
+
"singularityapi-tech": config => singularityApiTechModelManagerOptions(config),
|
|
140
144
|
synthetic: config => syntheticModelManagerOptions(config),
|
|
141
145
|
together: config => togetherModelManagerOptions(config),
|
|
142
146
|
typesafe: config => typesafeModelManagerOptions(config),
|
|
@@ -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,
|
|
@@ -54,6 +55,11 @@ import {
|
|
|
54
55
|
mergeCopilotApiHeaders,
|
|
55
56
|
parseGitHubCopilotApiKey,
|
|
56
57
|
} from "../wire/github-copilot";
|
|
58
|
+
import {
|
|
59
|
+
SINGULARITYAPI_DEV_API_BASE_URL,
|
|
60
|
+
SINGULARITYAPI_TECH_API_BASE_URL,
|
|
61
|
+
normalizeSingularityApiBaseUrl,
|
|
62
|
+
} from "../wire/singularityapi";
|
|
57
63
|
import { createBundledReferenceMap, createReferenceResolver, toModelSpec } from "./bundled-references";
|
|
58
64
|
import { getDefaultModelDiscoveryBaseUrl, resolveModelCacheProviderId } from "./cache-provider-id";
|
|
59
65
|
import { getClinePassModelMetadata } from "./cline-pass";
|
|
@@ -3227,127 +3233,210 @@ export function openrouterModelManagerOptions(config?: OpenRouterModelManagerCon
|
|
|
3227
3233
|
// override bundled `api: "openrouter"` models during online-if-uncached startup.
|
|
3228
3234
|
cacheProviderId: resolveModelCacheProviderId("openrouter"),
|
|
3229
3235
|
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
|
-
|
|
3236
|
+
const [chatModels, imageModels, decisionModels, rerankModels, videoModels, embeddingModels] =
|
|
3237
|
+
await Promise.all([
|
|
3238
|
+
fetchOpenAICompatibleModels({
|
|
3239
|
+
api: "openrouter",
|
|
3240
|
+
provider: "openrouter",
|
|
3241
|
+
baseUrl,
|
|
3242
|
+
apiKey,
|
|
3243
|
+
filterModel: (entry: OpenAICompatibleModelRecord) => {
|
|
3244
|
+
const params = entry.supported_parameters;
|
|
3245
|
+
return Array.isArray(params) && params.includes("tools");
|
|
3246
|
+
},
|
|
3247
|
+
mapModel: (
|
|
3248
|
+
entry: OpenAICompatibleModelRecord,
|
|
3249
|
+
defaults: ModelSpec<"openrouter">,
|
|
3250
|
+
_context: OpenAICompatibleModelMapperContext<"openrouter">,
|
|
3251
|
+
): ModelSpec<"openrouter"> => {
|
|
3252
|
+
const reference = references.get(defaults.id);
|
|
3253
|
+
const baseModel = mapWithBundledReference(entry, defaults, reference);
|
|
3254
|
+
const pricing = isRecord(entry.pricing) ? entry.pricing : undefined;
|
|
3255
|
+
const params = Array.isArray(entry.supported_parameters)
|
|
3256
|
+
? entry.supported_parameters.filter((value): value is string => typeof value === "string")
|
|
3257
|
+
: [];
|
|
3258
|
+
const thinking = mapOpenRouterThinking(entry);
|
|
3259
|
+
const architecture = isRecord(entry.architecture) ? entry.architecture : undefined;
|
|
3260
|
+
const input: ("text" | "image")[] = Array.isArray(architecture?.input_modalities)
|
|
3261
|
+
? toInputCapabilities(architecture.input_modalities)
|
|
3262
|
+
: String(architecture?.modality ?? "").includes("image")
|
|
3263
|
+
? ["text", "image"]
|
|
3264
|
+
: ["text"];
|
|
3265
|
+
const topProvider = isRecord(entry.top_provider) ? entry.top_provider : undefined;
|
|
3266
|
+
|
|
3267
|
+
const supportsToolChoice = params.includes("tool_choice");
|
|
3261
3268
|
|
|
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
|
-
|
|
3269
|
+
return {
|
|
3270
|
+
...baseModel,
|
|
3271
|
+
reasoning: params.includes("reasoning"),
|
|
3272
|
+
...(thinking !== undefined ? { thinking } : {}),
|
|
3273
|
+
input,
|
|
3274
|
+
cost: {
|
|
3275
|
+
input: parseFloat(String(pricing?.prompt ?? "0")) * 1_000_000,
|
|
3276
|
+
output: parseFloat(String(pricing?.completion ?? "0")) * 1_000_000,
|
|
3277
|
+
cacheRead: parseFloat(String(pricing?.input_cache_read ?? "0")) * 1_000_000,
|
|
3278
|
+
cacheWrite: parseFloat(String(pricing?.input_cache_write ?? "0")) * 1_000_000,
|
|
3279
|
+
},
|
|
3280
|
+
contextWindow:
|
|
3281
|
+
typeof entry.context_length === "number" ? entry.context_length : baseModel.contextWindow,
|
|
3282
|
+
maxTokens:
|
|
3283
|
+
typeof topProvider?.max_completion_tokens === "number"
|
|
3284
|
+
? topProvider.max_completion_tokens
|
|
3285
|
+
: baseModel.maxTokens,
|
|
3286
|
+
...(!supportsToolChoice && {
|
|
3287
|
+
compat: { ...baseModel.compat, supportsToolChoice: false },
|
|
3288
|
+
}),
|
|
3289
|
+
};
|
|
3290
|
+
},
|
|
3291
|
+
fetch: config?.fetch,
|
|
3292
|
+
}),
|
|
3293
|
+
fetchOpenAICompatibleModels({
|
|
3294
|
+
api: "openrouter-images",
|
|
3295
|
+
provider: "openrouter",
|
|
3296
|
+
baseUrl: `${baseUrl}/images`,
|
|
3297
|
+
apiKey,
|
|
3298
|
+
filterModel: entry => {
|
|
3299
|
+
const architecture = isRecord(entry.architecture) ? entry.architecture : undefined;
|
|
3300
|
+
return (
|
|
3301
|
+
Array.isArray(architecture?.output_modalities) &&
|
|
3302
|
+
architecture.output_modalities.includes("image")
|
|
3303
|
+
);
|
|
3304
|
+
},
|
|
3305
|
+
mapModel: (_entry, defaults): ModelSpec<"openrouter-images"> => ({
|
|
3306
|
+
...defaults,
|
|
3307
|
+
baseUrl,
|
|
3308
|
+
kind: "image",
|
|
3309
|
+
reasoning: false,
|
|
3310
|
+
input: ["text", "image"],
|
|
3311
|
+
supportsTools: false,
|
|
3312
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
3313
|
+
contextWindow: null,
|
|
3314
|
+
maxTokens: null,
|
|
3315
|
+
}),
|
|
3316
|
+
fetch: config?.fetch,
|
|
3317
|
+
}),
|
|
3318
|
+
// Decision models (`text->decisions`) are absent from the default roster
|
|
3319
|
+
// and answer only through the Decisions API, outside the `/v1` prefix.
|
|
3320
|
+
fetchOpenAICompatibleModels({
|
|
3321
|
+
api: "openrouter-decisions",
|
|
3322
|
+
provider: "openrouter",
|
|
3299
3323
|
baseUrl,
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3324
|
+
apiKey,
|
|
3325
|
+
query: { output_modalities: "decisions" },
|
|
3326
|
+
filterModel: entry => {
|
|
3327
|
+
const architecture = isRecord(entry.architecture) ? entry.architecture : undefined;
|
|
3328
|
+
return (
|
|
3329
|
+
Array.isArray(architecture?.output_modalities) &&
|
|
3330
|
+
architecture.output_modalities.includes("decisions")
|
|
3331
|
+
);
|
|
3332
|
+
},
|
|
3333
|
+
mapModel: (entry, defaults): ModelSpec<"openrouter-decisions"> => {
|
|
3334
|
+
const pricing = isRecord(entry.pricing) ? entry.pricing : undefined;
|
|
3335
|
+
const topProvider = isRecord(entry.top_provider) ? entry.top_provider : undefined;
|
|
3336
|
+
return {
|
|
3337
|
+
...defaults,
|
|
3338
|
+
baseUrl: decisionsBaseUrl,
|
|
3339
|
+
kind: "judge",
|
|
3340
|
+
reasoning: false,
|
|
3341
|
+
input: ["text"],
|
|
3342
|
+
supportsTools: false,
|
|
3343
|
+
cost: {
|
|
3344
|
+
input: parseFloat(String(pricing?.prompt ?? "0")) * 1_000_000,
|
|
3345
|
+
output: parseFloat(String(pricing?.completion ?? "0")) * 1_000_000,
|
|
3346
|
+
cacheRead: 0,
|
|
3347
|
+
cacheWrite: 0,
|
|
3348
|
+
},
|
|
3349
|
+
contextWindow: typeof entry.context_length === "number" ? entry.context_length : null,
|
|
3350
|
+
maxTokens:
|
|
3351
|
+
typeof topProvider?.max_completion_tokens === "number"
|
|
3352
|
+
? topProvider.max_completion_tokens
|
|
3353
|
+
: null,
|
|
3354
|
+
};
|
|
3355
|
+
},
|
|
3356
|
+
fetch: config?.fetch,
|
|
3307
3357
|
}),
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
architecture.
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3358
|
+
fetchOpenAICompatibleModels({
|
|
3359
|
+
api: "openrouter-rerank",
|
|
3360
|
+
provider: "openrouter",
|
|
3361
|
+
baseUrl,
|
|
3362
|
+
apiKey,
|
|
3363
|
+
query: { output_modalities: "rerank" },
|
|
3364
|
+
filterModel: entry => {
|
|
3365
|
+
const architecture = isRecord(entry.architecture) ? entry.architecture : undefined;
|
|
3366
|
+
return (
|
|
3367
|
+
Array.isArray(architecture?.output_modalities) &&
|
|
3368
|
+
architecture.output_modalities.includes("rerank")
|
|
3369
|
+
);
|
|
3370
|
+
},
|
|
3371
|
+
mapModel: (entry, defaults): ModelSpec<"openrouter-rerank"> => {
|
|
3372
|
+
const architecture = isRecord(entry.architecture) ? entry.architecture : undefined;
|
|
3373
|
+
const topProvider = isRecord(entry.top_provider) ? entry.top_provider : undefined;
|
|
3374
|
+
return {
|
|
3375
|
+
...defaults,
|
|
3376
|
+
baseUrl,
|
|
3377
|
+
kind: "rerank",
|
|
3378
|
+
reasoning: false,
|
|
3379
|
+
input: Array.isArray(architecture?.input_modalities)
|
|
3380
|
+
? toInputCapabilities(architecture.input_modalities)
|
|
3381
|
+
: ["text"],
|
|
3382
|
+
supportsTools: false,
|
|
3383
|
+
// OpenRouter bills reranking per search; ModelCost has no search-unit axis.
|
|
3384
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
3385
|
+
contextWindow: typeof entry.context_length === "number" ? entry.context_length : null,
|
|
3386
|
+
maxTokens:
|
|
3387
|
+
typeof topProvider?.max_completion_tokens === "number"
|
|
3388
|
+
? topProvider.max_completion_tokens
|
|
3389
|
+
: null,
|
|
3390
|
+
};
|
|
3391
|
+
},
|
|
3392
|
+
fetch: config?.fetch,
|
|
3393
|
+
}),
|
|
3394
|
+
fetchOpenAICompatibleModels({
|
|
3395
|
+
api: "openrouter-video",
|
|
3396
|
+
provider: "openrouter",
|
|
3397
|
+
baseUrl: `${baseUrl}/videos`,
|
|
3398
|
+
apiKey,
|
|
3399
|
+
mapModel: (_entry, defaults): ModelSpec<"openrouter-video"> => ({
|
|
3329
3400
|
...defaults,
|
|
3330
|
-
baseUrl
|
|
3331
|
-
kind: "
|
|
3401
|
+
baseUrl,
|
|
3402
|
+
kind: "video",
|
|
3332
3403
|
reasoning: false,
|
|
3333
|
-
input: ["text"],
|
|
3404
|
+
input: ["text", "image"],
|
|
3334
3405
|
supportsTools: false,
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3406
|
+
// OpenRouter bills video by output second/SKU; ModelCost has no duration axis.
|
|
3407
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
3408
|
+
contextWindow: null,
|
|
3409
|
+
maxTokens: null,
|
|
3410
|
+
}),
|
|
3411
|
+
fetch: config?.fetch,
|
|
3412
|
+
}),
|
|
3413
|
+
fetchOpenAICompatibleModels({
|
|
3414
|
+
api: "openai-embeddings",
|
|
3415
|
+
provider: "openrouter",
|
|
3416
|
+
baseUrl: `${baseUrl}/embeddings`,
|
|
3417
|
+
apiKey,
|
|
3418
|
+
mapModel: (entry, defaults): ModelSpec<"openai-embeddings"> => {
|
|
3419
|
+
const pricing = isRecord(entry.pricing) ? entry.pricing : undefined;
|
|
3420
|
+
return {
|
|
3421
|
+
...defaults,
|
|
3422
|
+
baseUrl,
|
|
3423
|
+
kind: "embedding",
|
|
3424
|
+
reasoning: false,
|
|
3425
|
+
input: ["text"],
|
|
3426
|
+
supportsTools: false,
|
|
3427
|
+
cost: {
|
|
3428
|
+
input: parseFloat(String(pricing?.prompt ?? "0")) * 1_000_000,
|
|
3429
|
+
output: 0,
|
|
3430
|
+
cacheRead: 0,
|
|
3431
|
+
cacheWrite: 0,
|
|
3432
|
+
},
|
|
3433
|
+
contextWindow: typeof entry.context_length === "number" ? entry.context_length : null,
|
|
3434
|
+
maxTokens: null,
|
|
3435
|
+
};
|
|
3436
|
+
},
|
|
3437
|
+
fetch: config?.fetch,
|
|
3438
|
+
}),
|
|
3439
|
+
]);
|
|
3351
3440
|
|
|
3352
3441
|
if (imageModels === null) {
|
|
3353
3442
|
logger.warn("OpenRouter image model discovery unavailable; preserving chat model discovery", {
|
|
@@ -3359,12 +3448,39 @@ export function openrouterModelManagerOptions(config?: OpenRouterModelManagerCon
|
|
|
3359
3448
|
endpoint: `${baseUrl}/models?output_modalities=decisions`,
|
|
3360
3449
|
});
|
|
3361
3450
|
}
|
|
3362
|
-
if (
|
|
3451
|
+
if (rerankModels === null) {
|
|
3452
|
+
logger.warn("OpenRouter rerank model discovery unavailable; preserving other model discovery", {
|
|
3453
|
+
endpoint: `${baseUrl}/models?output_modalities=rerank`,
|
|
3454
|
+
});
|
|
3455
|
+
}
|
|
3456
|
+
if (videoModels === null) {
|
|
3457
|
+
logger.warn("OpenRouter video model discovery unavailable; preserving other model discovery", {
|
|
3458
|
+
endpoint: `${baseUrl}/videos/models`,
|
|
3459
|
+
});
|
|
3460
|
+
}
|
|
3461
|
+
if (embeddingModels === null) {
|
|
3462
|
+
logger.warn("OpenRouter embedding model discovery unavailable; preserving other model discovery", {
|
|
3463
|
+
endpoint: `${baseUrl}/embeddings/models`,
|
|
3464
|
+
});
|
|
3465
|
+
}
|
|
3466
|
+
if (
|
|
3467
|
+
chatModels === null &&
|
|
3468
|
+
imageModels === null &&
|
|
3469
|
+
decisionModels === null &&
|
|
3470
|
+
rerankModels === null &&
|
|
3471
|
+
videoModels === null &&
|
|
3472
|
+
embeddingModels === null
|
|
3473
|
+
) {
|
|
3474
|
+
return null;
|
|
3475
|
+
}
|
|
3363
3476
|
|
|
3364
3477
|
const models = new Map<string, ModelSpec<Api>>();
|
|
3365
3478
|
for (const model of chatModels ?? []) models.set(model.id, model);
|
|
3366
3479
|
for (const model of imageModels ?? []) models.set(model.id, model);
|
|
3367
3480
|
for (const model of decisionModels ?? []) models.set(model.id, model);
|
|
3481
|
+
for (const model of rerankModels ?? []) models.set(model.id, model);
|
|
3482
|
+
for (const model of videoModels ?? []) models.set(model.id, model);
|
|
3483
|
+
for (const model of embeddingModels ?? []) models.set(model.id, model);
|
|
3368
3484
|
return Array.from(models.values()).sort((left, right) => left.id.localeCompare(right.id));
|
|
3369
3485
|
},
|
|
3370
3486
|
};
|
|
@@ -4937,6 +5053,9 @@ export function xiaomiModelManagerOptions(
|
|
|
4937
5053
|
// would incorrectly pin to the standard endpoint (api.xiaomimimo.com).
|
|
4938
5054
|
const baseUrl = isTokenPlanKey ? tokenPlanBaseUrls[0] : (config?.baseUrl ?? XIAOMI_STANDARD_BASE_URL);
|
|
4939
5055
|
const references = createBundledReferenceMap<"openai-completions">("xiaomi");
|
|
5056
|
+
for (const seed of seedModels<"openai-completions">(providerId)) {
|
|
5057
|
+
references.set(seed.id, seed);
|
|
5058
|
+
}
|
|
4940
5059
|
const fetchModels = (url: string) =>
|
|
4941
5060
|
fetchOpenAICompatibleModels({
|
|
4942
5061
|
api: "openai-completions",
|
|
@@ -6329,10 +6448,11 @@ export function mapModelsDevToModels(
|
|
|
6329
6448
|
const normalizedKind = policy.catalog.kind ?? m.kind;
|
|
6330
6449
|
if (typeof normalizedKind !== "string" || !MODEL_KINDS.some(value => value === normalizedKind)) continue;
|
|
6331
6450
|
if (normalizedKind !== "chat") {
|
|
6332
|
-
|
|
6333
|
-
|
|
6451
|
+
const kindApiKind = KIND_API_KINDS.find(value => value === normalizedKind);
|
|
6452
|
+
if (kindApiKind === undefined) continue;
|
|
6453
|
+
kindApi = providers[desc.providerId]?.kindApis?.[kindApiKind];
|
|
6334
6454
|
if (kindApi === undefined) continue;
|
|
6335
|
-
kind =
|
|
6455
|
+
kind = kindApiKind;
|
|
6336
6456
|
}
|
|
6337
6457
|
}
|
|
6338
6458
|
|
|
@@ -7288,3 +7408,154 @@ export function charmHyperModelManagerOptions(
|
|
|
7288
7408
|
}),
|
|
7289
7409
|
};
|
|
7290
7410
|
}
|
|
7411
|
+
|
|
7412
|
+
// ---------------------------------------------------------------------------
|
|
7413
|
+
// SingularityAPI
|
|
7414
|
+
// ---------------------------------------------------------------------------
|
|
7415
|
+
|
|
7416
|
+
export interface SingularityApiModelManagerConfig {
|
|
7417
|
+
apiKey?: string;
|
|
7418
|
+
baseUrl?: string;
|
|
7419
|
+
fetch?: FetchImpl;
|
|
7420
|
+
}
|
|
7421
|
+
|
|
7422
|
+
interface SingularityApiCapability extends Record<string, unknown> {
|
|
7423
|
+
endpoint?: unknown;
|
|
7424
|
+
context_window_tokens?: unknown;
|
|
7425
|
+
maximum_output_tokens?: unknown;
|
|
7426
|
+
default_output_tokens?: unknown;
|
|
7427
|
+
pricing?: unknown;
|
|
7428
|
+
}
|
|
7429
|
+
|
|
7430
|
+
/** Endpoints that decide which transport serves a `/v1/models` row. */
|
|
7431
|
+
const SINGULARITYAPI_CHAT_ENDPOINT = "/v1/chat/completions";
|
|
7432
|
+
const SINGULARITYAPI_IMAGE_ENDPOINT = "/v1/images/generations";
|
|
7433
|
+
|
|
7434
|
+
function singularityApiCapabilities(entry: OpenAICompatibleModelRecord): readonly SingularityApiCapability[] {
|
|
7435
|
+
const capabilities = entry.capabilities;
|
|
7436
|
+
if (!Array.isArray(capabilities)) return [];
|
|
7437
|
+
return capabilities.filter((capability): capability is SingularityApiCapability => isRecord(capability));
|
|
7438
|
+
}
|
|
7439
|
+
|
|
7440
|
+
function toSingularityApiRate(value: unknown): number {
|
|
7441
|
+
const parsed = toNumber(value);
|
|
7442
|
+
return parsed !== undefined && parsed >= 0 ? parsed : 0;
|
|
7443
|
+
}
|
|
7444
|
+
|
|
7445
|
+
function resolveSingularityApiCost(capability: SingularityApiCapability | undefined): ModelSpec<Api>["cost"] {
|
|
7446
|
+
const pricing = capability !== undefined && isRecord(capability.pricing) ? capability.pricing : undefined;
|
|
7447
|
+
if (!pricing) return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
7448
|
+
return {
|
|
7449
|
+
input: toSingularityApiRate(pricing.input_per_million_usd),
|
|
7450
|
+
output: toSingularityApiRate(pricing.output_per_million_usd),
|
|
7451
|
+
cacheRead: 0,
|
|
7452
|
+
cacheWrite: 0,
|
|
7453
|
+
};
|
|
7454
|
+
}
|
|
7455
|
+
|
|
7456
|
+
/**
|
|
7457
|
+
* Map one `/v1/models` row onto its serving transport.
|
|
7458
|
+
*
|
|
7459
|
+
* The wire's own `capabilities` list decides the transport: a row that serves
|
|
7460
|
+
* chat completions is a chat model, and a row whose only surface is
|
|
7461
|
+
* `/v1/images/generations` is routed to `openai-images` so
|
|
7462
|
+
* `generateImage`-style dispatch can reach it. Without that assignment the row
|
|
7463
|
+
* kept the discovery default (`openai-completions`) while still being marked
|
|
7464
|
+
* as an image model, so it was offered as an image target and then rejected by
|
|
7465
|
+
* every image client. The gateway bills image requests per request, never by
|
|
7466
|
+
* tokens, so those rows carry no token tariff.
|
|
7467
|
+
*/
|
|
7468
|
+
function mapSingularityApiModel(entry: OpenAICompatibleModelRecord, defaults: ModelSpec<Api>): ModelSpec<Api> {
|
|
7469
|
+
const capabilities = singularityApiCapabilities(entry);
|
|
7470
|
+
const capability = capabilities.find(candidate => candidate.endpoint === SINGULARITYAPI_CHAT_ENDPOINT);
|
|
7471
|
+
if (
|
|
7472
|
+
capability === undefined &&
|
|
7473
|
+
capabilities.some(candidate => candidate.endpoint === SINGULARITYAPI_IMAGE_ENDPOINT)
|
|
7474
|
+
) {
|
|
7475
|
+
return {
|
|
7476
|
+
...defaults,
|
|
7477
|
+
api: "openai-images",
|
|
7478
|
+
name: toModelName(entry.name, defaults.name),
|
|
7479
|
+
kind: "image",
|
|
7480
|
+
reasoning: false,
|
|
7481
|
+
input: ["text", "image"],
|
|
7482
|
+
supportsTools: false,
|
|
7483
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
7484
|
+
contextWindow: null,
|
|
7485
|
+
maxTokens: null,
|
|
7486
|
+
};
|
|
7487
|
+
}
|
|
7488
|
+
return {
|
|
7489
|
+
...defaults,
|
|
7490
|
+
name: toModelName(entry.name, defaults.name),
|
|
7491
|
+
contextWindow: toPositiveNumber(capability?.context_window_tokens, defaults.contextWindow),
|
|
7492
|
+
maxTokens: toPositiveNumber(capability?.maximum_output_tokens, defaults.maxTokens),
|
|
7493
|
+
cost: resolveSingularityApiCost(capability),
|
|
7494
|
+
};
|
|
7495
|
+
}
|
|
7496
|
+
/**
|
|
7497
|
+
* Core options shared by both SingularityAPI products. `mapModel` is supplied
|
|
7498
|
+
* only by the universal gateway, whose rows publish capability metadata; the
|
|
7499
|
+
* lane roster answers with bare ids and keeps the discovery defaults.
|
|
7500
|
+
*/
|
|
7501
|
+
function singularityApiModelManagerOptions(
|
|
7502
|
+
providerId: "singularityapi-dev" | "singularityapi-tech",
|
|
7503
|
+
canonical: string,
|
|
7504
|
+
config: SingularityApiModelManagerConfig | undefined,
|
|
7505
|
+
mapModel?: (entry: OpenAICompatibleModelRecord, defaults: ModelSpec<Api>) => ModelSpec<Api>,
|
|
7506
|
+
): ModelManagerOptions<Api> {
|
|
7507
|
+
const apiKey = config?.apiKey;
|
|
7508
|
+
const baseUrl = normalizeSingularityApiBaseUrl(config?.baseUrl, canonical);
|
|
7509
|
+
return {
|
|
7510
|
+
providerId,
|
|
7511
|
+
cacheProviderId: resolveModelCacheProviderId(providerId, { apiKey, baseUrl }),
|
|
7512
|
+
dynamicModelsAuthoritative: true,
|
|
7513
|
+
...(apiKey && {
|
|
7514
|
+
fetchDynamicModels: () =>
|
|
7515
|
+
fetchOpenAICompatibleModels<Api>({
|
|
7516
|
+
api: "openai-completions",
|
|
7517
|
+
provider: providerId,
|
|
7518
|
+
baseUrl,
|
|
7519
|
+
apiKey,
|
|
7520
|
+
...(mapModel && { mapModel }),
|
|
7521
|
+
fetch: config?.fetch,
|
|
7522
|
+
}),
|
|
7523
|
+
}),
|
|
7524
|
+
};
|
|
7525
|
+
}
|
|
7526
|
+
|
|
7527
|
+
/**
|
|
7528
|
+
* `singularityapi-dev` — SingularityAPI's pay-as-you-go universal gateway
|
|
7529
|
+
* (`api.singularityapi.dev`): chat completions over a 300+ model catalog,
|
|
7530
|
+
* plus image generation for the rows that advertise it.
|
|
7531
|
+
* `GET /v1/models` publishes each row's per-endpoint capabilities — context
|
|
7532
|
+
* window, max output tokens, and per-million pricing as 12-decimal strings —
|
|
7533
|
+
* with `cache-control: no-store`, so discovery reads limits and tariffs
|
|
7534
|
+
* straight off the wire and the endpoint list picks each row's transport.
|
|
7535
|
+
* Rows without a reasoning vocabulary stay non-reasoning; reviewed KDL rules
|
|
7536
|
+
* own the ladders the gateway leaves implicit (DeepSeek Flash/Pro, GPT-5.6
|
|
7537
|
+
* flagships), because a model discovered as non-reasoning never sends a
|
|
7538
|
+
* `reasoning_effort` and the gateway requires one alongside tools.
|
|
7539
|
+
*/
|
|
7540
|
+
export function singularityApiDevModelManagerOptions(
|
|
7541
|
+
config?: SingularityApiModelManagerConfig,
|
|
7542
|
+
): ModelManagerOptions<Api> {
|
|
7543
|
+
return singularityApiModelManagerOptions(
|
|
7544
|
+
"singularityapi-dev",
|
|
7545
|
+
SINGULARITYAPI_DEV_API_BASE_URL,
|
|
7546
|
+
config,
|
|
7547
|
+
mapSingularityApiModel,
|
|
7548
|
+
);
|
|
7549
|
+
}
|
|
7550
|
+
|
|
7551
|
+
/**
|
|
7552
|
+
* `singularityapi-tech` — SingularityAPI's slot-reserved DeepSeek lanes
|
|
7553
|
+
* (`api.singularityapi.tech`). `/v1/models` answers with bare `{id}` rows and
|
|
7554
|
+
* no capability metadata, so rows keep the discovery defaults and the
|
|
7555
|
+
* reviewed KDL rules own the wire shape, limits patch, and effort ladder.
|
|
7556
|
+
*/
|
|
7557
|
+
export function singularityApiTechModelManagerOptions(
|
|
7558
|
+
config?: SingularityApiModelManagerConfig,
|
|
7559
|
+
): ModelManagerOptions<Api> {
|
|
7560
|
+
return singularityApiModelManagerOptions("singularityapi-tech", SINGULARITYAPI_TECH_API_BASE_URL, config);
|
|
7561
|
+
}
|