@oh-my-pi/pi-catalog 18.14.2 → 18.16.0
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 +30 -0
- package/dist/types/compat/auth-ids.d.ts +2 -2
- package/dist/types/compat/catalog-policy.d.ts +6 -4
- package/dist/types/compat/factory-droid.d.ts +7 -1
- package/dist/types/compat/provider-ids.d.ts +1 -1
- package/dist/types/compat/resolve.d.ts +1 -1
- package/dist/types/compat/revision.d.ts +2 -0
- package/dist/types/compat/server-side-fallback.d.ts +6 -4
- package/dist/types/compat/types.d.ts +3 -1
- package/dist/types/discovery/openai-compatible.d.ts +6 -0
- package/dist/types/models.d.ts +16 -0
- package/dist/types/pricing.d.ts +66 -6
- package/dist/types/provider-models/openai-compat.d.ts +18 -0
- package/dist/types/types.d.ts +11 -5
- package/dist/types/utils.d.ts +8 -0
- package/package.json +4 -4
- package/src/compat/auth-ids.ts +5 -0
- package/src/compat/axes.ts +5 -2
- package/src/compat/catalog-policy.ts +14 -10
- package/src/compat/collapse.ts +1 -1
- package/src/compat/factory-droid.ts +9 -1
- package/src/compat/provider-ids.ts +2 -0
- package/src/compat/resolve.ts +4 -2
- package/src/compat/revision.ts +10 -0
- package/src/compat/rules/README.md +2 -0
- package/src/compat/rules/auth/_order.kdl +1 -1
- package/src/compat/rules/auth/mistral.kdl +4 -0
- package/src/compat/rules/auth/modular.kdl +9 -0
- package/src/compat/rules/auth/scaleway.kdl +17 -0
- package/src/compat/rules/providers/factory-droid.kdl +219 -13
- package/src/compat/rules/providers/modular.kdl +35 -0
- package/src/compat/rules/providers/sambanova.kdl +2 -0
- package/src/compat/rules/providers/scaleway.kdl +179 -0
- package/src/compat/rules/runtime/behavior.kdl +13 -0
- package/src/compat/rules.json +1 -1
- package/src/compat/server-side-fallback.ts +7 -5
- package/src/compat/types.ts +3 -1
- package/src/discovery/codex.ts +2 -9
- package/src/discovery/factory-droid.ts +35 -11
- package/src/discovery/openai-compatible.ts +1 -1
- package/src/model-cache.ts +32 -58
- package/src/model-catalog-format.ts +5 -8
- package/src/model-manager.ts +27 -121
- package/src/model-tokenizer.ts +13 -12
- package/src/models.json +1 -1
- package/src/models.ts +32 -1
- package/src/pricing.ts +59 -94
- package/src/provider-models/descriptors.ts +2 -0
- package/src/provider-models/openai-compat.ts +60 -59
- package/src/types.ts +11 -4
- package/src/utils.ts +18 -0
- package/src/wire/factory-droid.ts +4 -3
package/src/models.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { classifyModel } from "./compat/taxonomy";
|
|
2
2
|
import { getCatalogProvider, getCatalogProviders, openModelCatalog } from "./model-catalog-format";
|
|
3
3
|
import modelsData from "./models.json" with { type: "json" };
|
|
4
|
-
import { dueEffectiveRate } from "./pricing";
|
|
4
|
+
import { dueDatedCard, dueEffectiveRate } from "./pricing";
|
|
5
5
|
import type {
|
|
6
6
|
Api,
|
|
7
7
|
EffectiveTokenCost,
|
|
@@ -77,6 +77,37 @@ export function getModelPricingStatus(model: Pick<Model<Api>, "cost" | "pricingS
|
|
|
77
77
|
return model.pricingStatus ?? "unknown";
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/** Factory Droid credit rate in force at a time, and the next dated change to it. */
|
|
81
|
+
export interface FactoryDroidCreditRate {
|
|
82
|
+
rate: number;
|
|
83
|
+
/** The earliest later `credit-rates` card that changes `rate`: the rate it sets and when (Unix ms). */
|
|
84
|
+
next?: { rate: number; at: number };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Factory Droid Standard Credits rate at `timestamp` (Unix ms, default now):
|
|
89
|
+
* the `input` of the `credit-rates` card due then (the latest at or before
|
|
90
|
+
* it, as for cost cards), else the base rate. Undefined for models without a
|
|
91
|
+
* credit rate.
|
|
92
|
+
*/
|
|
93
|
+
export function getFactoryDroidCreditRate(
|
|
94
|
+
model: Pick<Model, "factoryDroidCredits" | "factoryDroidCreditCards">,
|
|
95
|
+
timestamp = Date.now(),
|
|
96
|
+
): FactoryDroidCreditRate | undefined {
|
|
97
|
+
const base = model.factoryDroidCredits;
|
|
98
|
+
if (base === undefined) return undefined;
|
|
99
|
+
const cards = model.factoryDroidCreditCards ?? [];
|
|
100
|
+
const rate = dueDatedCard(cards, timestamp)?.input ?? base;
|
|
101
|
+
let next: FactoryDroidCreditRate["next"];
|
|
102
|
+
for (const card of cards) {
|
|
103
|
+
const cardRate = card.input ?? base;
|
|
104
|
+
if (card.effectiveFrom > timestamp && cardRate !== rate && !(next && next.at <= card.effectiveFrom)) {
|
|
105
|
+
next = { rate: cardRate, at: card.effectiveFrom };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return next ? { rate, next } : { rate };
|
|
109
|
+
}
|
|
110
|
+
|
|
80
111
|
/**
|
|
81
112
|
* The base token card in force at `timestamp` (Unix ms, default now): the
|
|
82
113
|
* latest due `timeBased` effective rate, else the row's own legs. Price
|
package/src/pricing.ts
CHANGED
|
@@ -1,84 +1,35 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
3
|
-
LongContextTokenCost,
|
|
4
|
-
ModelCost,
|
|
5
|
-
PeakPricingWindow,
|
|
6
|
-
TimeBasedCost,
|
|
7
|
-
TokenCost,
|
|
8
|
-
} from "./types";
|
|
1
|
+
import { type } from "@oh-my-pi/omptype";
|
|
2
|
+
import type { EffectiveTokenCost, ModelCost, TimeBasedCost, TokenCost } from "./types";
|
|
9
3
|
import { isRecord } from "./utils";
|
|
10
4
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
function isPeakWindow(value: unknown): value is PeakPricingWindow {
|
|
37
|
-
return (
|
|
38
|
-
isRecord(value) &&
|
|
39
|
-
Array.isArray(value.weekdays) &&
|
|
40
|
-
value.weekdays.length > 0 &&
|
|
41
|
-
value.weekdays.every(day => Number.isInteger(day) && day >= 0 && day <= 6) &&
|
|
42
|
-
new Set(value.weekdays).size === value.weekdays.length &&
|
|
43
|
-
typeof value.startMinute === "number" &&
|
|
44
|
-
Number.isInteger(value.startMinute) &&
|
|
45
|
-
typeof value.endMinute === "number" &&
|
|
46
|
-
Number.isInteger(value.endMinute) &&
|
|
47
|
-
value.startMinute >= 0 &&
|
|
48
|
-
value.endMinute <= 1440 &&
|
|
49
|
-
value.startMinute < value.endMinute
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function isEffectiveRate(value: unknown): value is EffectiveTokenCost {
|
|
54
|
-
return (
|
|
55
|
-
isRecord(value) &&
|
|
56
|
-
isRates(value) &&
|
|
57
|
-
typeof value.effectiveFrom === "number" &&
|
|
58
|
-
Number.isSafeInteger(value.effectiveFrom) &&
|
|
59
|
-
Math.abs(value.effectiveFrom) <= 8_640_000_000_000_000 &&
|
|
60
|
-
(value.longContext === undefined || isLongContext(value.longContext))
|
|
61
|
-
);
|
|
62
|
-
}
|
|
5
|
+
// Schedules are also checked on in-memory discovery output, where absent optionals may be explicit `undefined`.
|
|
6
|
+
const Rate = type("number >= 0");
|
|
7
|
+
const Rates = type({ input: Rate, output: Rate, cacheRead: Rate, cacheWrite: Rate });
|
|
8
|
+
const LongContext = Rates.and({ inputThreshold: "number > 0", "inputThresholdInclusive?": "boolean | undefined" });
|
|
9
|
+
const PeakWindow = type({
|
|
10
|
+
weekdays: type("0 <= number.integer <= 6").array().atLeastLength(1),
|
|
11
|
+
startMinute: "number.integer >= 0",
|
|
12
|
+
endMinute: "number.integer <= 1440",
|
|
13
|
+
}).narrow(window => new Set(window.weekdays).size === window.weekdays.length && window.startMinute < window.endMinute);
|
|
14
|
+
const EffectiveRate = Rates.and({
|
|
15
|
+
// The `Date` range.
|
|
16
|
+
effectiveFrom: "-8640000000000000 <= number.integer <= 8640000000000000",
|
|
17
|
+
"longContext?": LongContext.or("undefined"),
|
|
18
|
+
});
|
|
19
|
+
/** Serialized time-based pricing schedule, as cached model rows carry it. */
|
|
20
|
+
export const TimeBasedCostShape = type({
|
|
21
|
+
offPeakMultiplier: Rate,
|
|
22
|
+
peakWindows: PeakWindow.array(),
|
|
23
|
+
"effectiveRates?": EffectiveRate.array().or("undefined"),
|
|
24
|
+
}).narrow(
|
|
25
|
+
cost =>
|
|
26
|
+
cost.effectiveRates === undefined ||
|
|
27
|
+
new Set(cost.effectiveRates.map(rate => rate.effectiveFrom)).size === cost.effectiveRates.length,
|
|
28
|
+
);
|
|
63
29
|
|
|
64
30
|
/** Validate serialized schedules before admitting cached model rows. */
|
|
65
31
|
export function isTimeBasedCost(value: unknown): value is TimeBasedCost {
|
|
66
|
-
|
|
67
|
-
!isRecord(value) ||
|
|
68
|
-
!nonnegative(value.offPeakMultiplier) ||
|
|
69
|
-
!Array.isArray(value.peakWindows) ||
|
|
70
|
-
!value.peakWindows.every(isPeakWindow)
|
|
71
|
-
) {
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
if (value.effectiveRates === undefined) return true;
|
|
75
|
-
if (!Array.isArray(value.effectiveRates) || !value.effectiveRates.every(isEffectiveRate)) return false;
|
|
76
|
-
const dates = new Set<number>();
|
|
77
|
-
for (const rate of value.effectiveRates) {
|
|
78
|
-
if (dates.has(rate.effectiveFrom)) return false;
|
|
79
|
-
dates.add(rate.effectiveFrom);
|
|
80
|
-
}
|
|
81
|
-
return true;
|
|
32
|
+
return TimeBasedCostShape.allows(value);
|
|
82
33
|
}
|
|
83
34
|
|
|
84
35
|
function payload(value: unknown, keys: readonly string[], field: string): Record<string, unknown> {
|
|
@@ -148,15 +99,15 @@ export function materializeTimeBasedCost(value: unknown): TimeBasedCost {
|
|
|
148
99
|
}
|
|
149
100
|
|
|
150
101
|
/**
|
|
151
|
-
* The
|
|
152
|
-
*
|
|
102
|
+
* The dated card due at `timestamp`: the one with the latest `effectiveFrom`
|
|
103
|
+
* at or before it, in any order, or undefined before the first card.
|
|
153
104
|
*/
|
|
154
|
-
export function
|
|
155
|
-
|
|
105
|
+
export function dueDatedCard<T extends { effectiveFrom: number }>(
|
|
106
|
+
cards: readonly T[],
|
|
156
107
|
timestamp: number,
|
|
157
|
-
):
|
|
158
|
-
let due:
|
|
159
|
-
for (const candidate of
|
|
108
|
+
): T | undefined {
|
|
109
|
+
let due: T | undefined;
|
|
110
|
+
for (const candidate of cards) {
|
|
160
111
|
if (candidate.effectiveFrom <= timestamp && (due === undefined || candidate.effectiveFrom > due.effectiveFrom)) {
|
|
161
112
|
due = candidate;
|
|
162
113
|
}
|
|
@@ -164,21 +115,38 @@ export function dueEffectiveRate(
|
|
|
164
115
|
return due;
|
|
165
116
|
}
|
|
166
117
|
|
|
118
|
+
/**
|
|
119
|
+
* The latest `effectiveRates` card of a materialized schedule due at
|
|
120
|
+
* `timestamp`, or undefined before the first one (or without a schedule).
|
|
121
|
+
*/
|
|
122
|
+
export function dueEffectiveRate(
|
|
123
|
+
schedule: TimeBasedCost | undefined,
|
|
124
|
+
timestamp: number,
|
|
125
|
+
): EffectiveTokenCost | undefined {
|
|
126
|
+
return dueDatedCard(schedule?.effectiveRates ?? [], timestamp);
|
|
127
|
+
}
|
|
128
|
+
|
|
167
129
|
const TOKEN_COST_LEGS = ["input", "output", "cacheRead", "cacheWrite"] as const;
|
|
168
130
|
const DATED_CARD_KEYS: readonly string[] = ["effectiveFrom", ...TOKEN_COST_LEGS];
|
|
169
131
|
|
|
170
|
-
/**
|
|
132
|
+
/**
|
|
133
|
+
* A dated `effective-rates` card of a `cost-patch`, `cost-fallback` or
|
|
134
|
+
* `credit-rates` rule: the legs it overwrites from `effectiveFrom` (Unix ms).
|
|
135
|
+
*/
|
|
171
136
|
export interface DatedCostCard extends Partial<TokenCost> {
|
|
172
137
|
effectiveFrom: number;
|
|
173
138
|
}
|
|
174
139
|
|
|
175
140
|
/**
|
|
176
|
-
* Parse the compiled `effective-rates` block of a `cost-patch
|
|
177
|
-
* `cost-fallback` payload into dated cards,
|
|
178
|
-
* malformed card, so a typo in a rule fails the
|
|
179
|
-
* leaving the old price in force.
|
|
141
|
+
* Parse the compiled `effective-rates` block of a `cost-patch`,
|
|
142
|
+
* `cost-fallback` or Factory Droid `credit-rates` payload into dated cards,
|
|
143
|
+
* ascending by date. Throws on a malformed card, so a typo in a rule fails the
|
|
144
|
+
* build instead of silently leaving the old price in force.
|
|
180
145
|
*/
|
|
181
|
-
export function parseDatedCostCards(
|
|
146
|
+
export function parseDatedCostCards(
|
|
147
|
+
costAxis: unknown,
|
|
148
|
+
axis: "cost-patch" | "cost-fallback" | "credit-rates",
|
|
149
|
+
): DatedCostCard[] {
|
|
182
150
|
const effectiveRates = isRecord(costAxis) ? costAxis.effectiveRates : undefined;
|
|
183
151
|
if (effectiveRates === undefined) return [];
|
|
184
152
|
if (!isRecord(effectiveRates)) throw new Error(`Invalid ${axis} effective-rates: expected named objects`);
|
|
@@ -190,7 +158,7 @@ export function parseDatedCostCards(costAxis: unknown, axis: "cost-patch" | "cos
|
|
|
190
158
|
for (const leg of TOKEN_COST_LEGS) {
|
|
191
159
|
const value = entry[leg];
|
|
192
160
|
if (value === undefined) continue;
|
|
193
|
-
if (!
|
|
161
|
+
if (!Rate.allows(value))
|
|
194
162
|
throw new Error(`Invalid ${axis} effective-rate ${leg}: expected a nonnegative number`);
|
|
195
163
|
card[leg] = value;
|
|
196
164
|
}
|
|
@@ -235,10 +203,7 @@ export function datedCost(base: TokenCost, cards: readonly DatedCostCard[], now
|
|
|
235
203
|
* `cost` unchanged before the first card.
|
|
236
204
|
*/
|
|
237
205
|
export function applyDueDatedCostCard(cost: TokenCost, cards: readonly DatedCostCard[], now = Date.now()): void {
|
|
238
|
-
|
|
239
|
-
for (const card of cards) {
|
|
240
|
-
if (card.effectiveFrom <= now) due = card;
|
|
241
|
-
}
|
|
206
|
+
const due = dueDatedCard(cards, now);
|
|
242
207
|
if (due === undefined) return;
|
|
243
208
|
for (const leg of TOKEN_COST_LEGS) {
|
|
244
209
|
const value = due[leg];
|
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
qwenPortalModelManagerOptions,
|
|
62
62
|
sakanaModelManagerOptions,
|
|
63
63
|
sambanovaModelManagerOptions,
|
|
64
|
+
scalewayModelManagerOptions,
|
|
64
65
|
siliconflowCnModelManagerOptions,
|
|
65
66
|
siliconflowModelManagerOptions,
|
|
66
67
|
singularityApiDevModelManagerOptions,
|
|
@@ -150,6 +151,7 @@ const MODEL_MANAGER_FACTORIES: Readonly<Partial<Record<KnownProvider, ModelManag
|
|
|
150
151
|
"qwen-portal": config => qwenPortalModelManagerOptions(config),
|
|
151
152
|
sakana: config => sakanaModelManagerOptions(config),
|
|
152
153
|
sambanova: config => sambanovaModelManagerOptions(config),
|
|
154
|
+
scaleway: config => scalewayModelManagerOptions(config),
|
|
153
155
|
siliconflow: config => siliconflowModelManagerOptions(config),
|
|
154
156
|
"siliconflow-cn": config => siliconflowCnModelManagerOptions(config),
|
|
155
157
|
"singularityapi-dev": config => singularityApiDevModelManagerOptions(config),
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
import { resolveAnthropicApiVersion } from "../compat/anthropic";
|
|
14
14
|
import { xaiResponsesReasoningEffortMap } from "../compat/openai";
|
|
15
15
|
import { hasModelScopedEffortLadder, resolveModelPolicy } from "../compat/resolve";
|
|
16
|
-
import { compareRevision, parseRevision } from "../compat/revision";
|
|
16
|
+
import { compareRevision, parseRevision, revisionAtLeast } from "../compat/revision";
|
|
17
17
|
import { providerEntries, seedModels } from "../compat/providers";
|
|
18
18
|
import { billingVariantPlain, classifyModel, discoveryVocabulary } from "../compat/taxonomy";
|
|
19
19
|
import {
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
fetchOpenAICompatibleModels,
|
|
22
22
|
type OpenAICompatibleModelMapperContext,
|
|
23
23
|
type OpenAICompatibleModelRecord,
|
|
24
|
+
withOpenAICompatibleDiscoveryTimeout,
|
|
24
25
|
} from "../discovery/openai-compatible";
|
|
25
26
|
import { Effort, THINKING_EFFORTS } from "../effort";
|
|
26
27
|
import { FIREWORKS_FAST_SUFFIX, toFireworksPublicModelId } from "../fireworks-model-id";
|
|
@@ -43,15 +44,7 @@ import {
|
|
|
43
44
|
type TokenCost,
|
|
44
45
|
runnerApiKind,
|
|
45
46
|
} from "../types";
|
|
46
|
-
import {
|
|
47
|
-
discoveryFetch,
|
|
48
|
-
isAnthropicOAuthToken,
|
|
49
|
-
isRecord,
|
|
50
|
-
toBoolean,
|
|
51
|
-
toNumber,
|
|
52
|
-
toPositiveNumber,
|
|
53
|
-
toPositiveNumberOrNull,
|
|
54
|
-
} from "../utils";
|
|
47
|
+
import { discoveryFetch, isAnthropicOAuthToken, isRecord, toBoolean, toNumber, toPositiveNumber } from "../utils";
|
|
55
48
|
import { ALIBABA_TOKEN_PLAN_BASE_URL, parseAlibabaTokenPlanCredential } from "../wire/alibaba-token-plan";
|
|
56
49
|
import { normalizeCharmHyperBaseUrl } from "../wire/charm-hyper";
|
|
57
50
|
import { CLINEPASS_API_BASE_URL, clinePassClientHeaders } from "../wire/cline-pass";
|
|
@@ -78,15 +71,6 @@ import { getClinePassModelMetadata } from "./cline-pass";
|
|
|
78
71
|
import type { ModelManagerConfig } from "./descriptor-types";
|
|
79
72
|
import { filterModelsDevCatalogRows } from "./models-dev-policies";
|
|
80
73
|
|
|
81
|
-
function revisionAtLeast(revision: string | undefined, floor: string): boolean {
|
|
82
|
-
if (revision === undefined) return false;
|
|
83
|
-
const parsedRevision = parseRevision(revision);
|
|
84
|
-
const parsedFloor = parseRevision(floor);
|
|
85
|
-
return (
|
|
86
|
-
parsedRevision !== undefined && parsedFloor !== undefined && compareRevision(parsedRevision, parsedFloor) >= 0
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
74
|
function isGlmReasoningIdentity(provider: string, modelId: string, floor: string): boolean {
|
|
91
75
|
const identity = classifyModel(provider, modelId, { lenient: true });
|
|
92
76
|
if (identity.class !== "glm" || !revisionAtLeast(identity.revision, floor)) return false;
|
|
@@ -99,24 +83,6 @@ const MODELS_DEV_URL = "https://catalog.stencil.so/models.json.zstd";
|
|
|
99
83
|
/** Little-endian magic number opening every zstd frame (RFC 8878). */
|
|
100
84
|
const ZSTD_MAGIC = 0xfd2fb528;
|
|
101
85
|
|
|
102
|
-
/**
|
|
103
|
-
* Uses a cancellable timer rather than the native abort-timeout helper so
|
|
104
|
-
* successful fast discovery requests do not leave armed timeout signals for
|
|
105
|
-
* concurrent GC to trip over later.
|
|
106
|
-
*/
|
|
107
|
-
async function withCatalogDiscoveryTimeout<T>(timeoutMs: number, run: (signal: AbortSignal) => Promise<T>): Promise<T> {
|
|
108
|
-
const controller = new AbortController();
|
|
109
|
-
const timer = setTimeout(
|
|
110
|
-
() => controller.abort(new DOMException("The operation timed out.", "TimeoutError")),
|
|
111
|
-
timeoutMs,
|
|
112
|
-
);
|
|
113
|
-
try {
|
|
114
|
-
return await run(controller.signal);
|
|
115
|
-
} finally {
|
|
116
|
-
clearTimeout(timer);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
86
|
const ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
|
|
121
87
|
const ANTHROPIC_OAUTH_BETA =
|
|
122
88
|
"claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,context-management-2025-06-27,prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07,advanced-tool-use-2025-11-20,effort-2025-11-24,extended-cache-ttl-2025-04-11";
|
|
@@ -226,8 +192,9 @@ export function fetchWellKnownModels(fetchImpl?: FetchImpl, signal?: AbortSignal
|
|
|
226
192
|
function fetchRevalidatedWellKnownModels(fetchImpl?: FetchImpl, signal?: AbortSignal): Promise<unknown> {
|
|
227
193
|
const session = getCatalogSession(fetchImpl);
|
|
228
194
|
if (!session.inflight) {
|
|
229
|
-
session.inflight =
|
|
230
|
-
|
|
195
|
+
session.inflight = withOpenAICompatibleDiscoveryTimeout(
|
|
196
|
+
DEFAULT_OPENAI_COMPATIBLE_DISCOVERY_TIMEOUT_MS,
|
|
197
|
+
transportSignal => fetchCatalogPayload(fetchImpl ?? discoveryFetch(), session, transportSignal),
|
|
231
198
|
).finally(() => {
|
|
232
199
|
session.inflight = null;
|
|
233
200
|
});
|
|
@@ -239,7 +206,7 @@ function fetchRevalidatedWellKnownModelsWithTimeout(
|
|
|
239
206
|
fetchImpl?: FetchImpl,
|
|
240
207
|
timeoutMs = DEFAULT_OPENAI_COMPATIBLE_DISCOVERY_TIMEOUT_MS,
|
|
241
208
|
): Promise<unknown> {
|
|
242
|
-
return
|
|
209
|
+
return withOpenAICompatibleDiscoveryTimeout(timeoutMs, signal => fetchRevalidatedWellKnownModels(fetchImpl, signal));
|
|
243
210
|
}
|
|
244
211
|
|
|
245
212
|
async function fetchCatalogPayload(
|
|
@@ -545,6 +512,8 @@ function mapWithBundledReference<TApi extends Api>(
|
|
|
545
512
|
return {
|
|
546
513
|
...defaults,
|
|
547
514
|
name,
|
|
515
|
+
contextWindow: toPositiveNumber(entry.context_length, defaults.contextWindow),
|
|
516
|
+
maxTokens: toPositiveNumber(entry.max_completion_tokens, defaults.maxTokens),
|
|
548
517
|
};
|
|
549
518
|
}
|
|
550
519
|
// Generic `/models` rows describe the chat roster. Do not make a bundled
|
|
@@ -1725,8 +1694,9 @@ async function fetchDeepinfraModels(options: {
|
|
|
1725
1694
|
const fetchImpl = discoveryFetch(options.fetch);
|
|
1726
1695
|
let payload: unknown;
|
|
1727
1696
|
try {
|
|
1728
|
-
const response = await
|
|
1729
|
-
|
|
1697
|
+
const response = await withOpenAICompatibleDiscoveryTimeout(
|
|
1698
|
+
DEFAULT_OPENAI_COMPATIBLE_DISCOVERY_TIMEOUT_MS,
|
|
1699
|
+
signal => fetchImpl(`${options.baseUrl}/models${DEEPINFRA_MODELS_QUERY}`, { method: "GET", headers, signal }),
|
|
1730
1700
|
);
|
|
1731
1701
|
if (!response.ok) {
|
|
1732
1702
|
return null;
|
|
@@ -2284,7 +2254,7 @@ async function loadSiliconFlowModelsDevReferences(
|
|
|
2284
2254
|
try {
|
|
2285
2255
|
// Bounded: this enrichment is optional, so a stalled models.dev must not
|
|
2286
2256
|
// hold back the authoritative endpoint request that runs after it.
|
|
2287
|
-
const payload = await
|
|
2257
|
+
const payload = await withOpenAICompatibleDiscoveryTimeout(SILICONFLOW_MODELS_DEV_REFERENCE_TIMEOUT_MS, signal =>
|
|
2288
2258
|
fetchWellKnownModels(fetchImpl, signal),
|
|
2289
2259
|
);
|
|
2290
2260
|
return createModelsDevReferenceMap<"openai-completions">(
|
|
@@ -2828,7 +2798,7 @@ const CLINE_PASS_LIVE_CATALOG_URL = "https://openrouter.ai/api/v1/models";
|
|
|
2828
2798
|
/** Enrichment tier, never authoritative: any failure degrades to the bundled path. */
|
|
2829
2799
|
async function fetchClinePassLiveCatalog(fetchImpl: FetchImpl): Promise<ClinePassLiveCatalog | undefined> {
|
|
2830
2800
|
try {
|
|
2831
|
-
const response = await
|
|
2801
|
+
const response = await withOpenAICompatibleDiscoveryTimeout(5_000, signal =>
|
|
2832
2802
|
fetchImpl(CLINE_PASS_LIVE_CATALOG_URL, { signal }),
|
|
2833
2803
|
);
|
|
2834
2804
|
if (!response.ok) return undefined;
|
|
@@ -3007,7 +2977,7 @@ async function fetchClinePassModels(
|
|
|
3007
2977
|
// The roster is authoritative for ids; the live reference catalog is a pure
|
|
3008
2978
|
// enrichment tier fetched alongside and tolerated away (offline → bundle).
|
|
3009
2979
|
const [response, liveCatalog] = await Promise.all([
|
|
3010
|
-
|
|
2980
|
+
withOpenAICompatibleDiscoveryTimeout(5_000, signal =>
|
|
3011
2981
|
fetchImpl(CLINEPASS_MODELS_URL, { signal, headers: clinePassClientHeaders() }),
|
|
3012
2982
|
),
|
|
3013
2983
|
fetchClinePassLiveCatalog(fetchImpl),
|
|
@@ -4422,7 +4392,7 @@ export async function fetchLmStudioNativeModelMetadata(
|
|
|
4422
4392
|
if (options?.signal !== undefined) {
|
|
4423
4393
|
return fetchMetadata(options.signal);
|
|
4424
4394
|
}
|
|
4425
|
-
return
|
|
4395
|
+
return withOpenAICompatibleDiscoveryTimeout(LM_STUDIO_NATIVE_METADATA_TIMEOUT_MS, fetchMetadata);
|
|
4426
4396
|
}
|
|
4427
4397
|
|
|
4428
4398
|
export interface LmStudioModelManagerConfig {
|
|
@@ -4770,8 +4740,6 @@ export function basetenModelManagerOptions(
|
|
|
4770
4740
|
cacheWrite: 0,
|
|
4771
4741
|
};
|
|
4772
4742
|
|
|
4773
|
-
const contextWindow = toPositiveNumber(raw.context_length, reference?.contextWindow ?? defaults.contextWindow);
|
|
4774
|
-
const maxTokens = toPositiveNumber(raw.max_completion_tokens, reference?.maxTokens ?? defaults.maxTokens);
|
|
4775
4743
|
const baseModel = mapWithBundledReference(entry, defaults, reference);
|
|
4776
4744
|
|
|
4777
4745
|
return {
|
|
@@ -4779,8 +4747,6 @@ export function basetenModelManagerOptions(
|
|
|
4779
4747
|
reasoning,
|
|
4780
4748
|
input: vision ? ["text", "image"] : ["text"],
|
|
4781
4749
|
cost,
|
|
4782
|
-
contextWindow,
|
|
4783
|
-
maxTokens,
|
|
4784
4750
|
...(supportsTools === false ? { supportsTools } : {}),
|
|
4785
4751
|
};
|
|
4786
4752
|
},
|
|
@@ -6393,7 +6359,9 @@ async function fetchLiteLLMRichModelsInternal<TApi extends Api>(
|
|
|
6393
6359
|
if (options.signal !== undefined) {
|
|
6394
6360
|
return fetchModels(options.signal);
|
|
6395
6361
|
}
|
|
6396
|
-
return options.timeoutMs !== undefined
|
|
6362
|
+
return options.timeoutMs !== undefined
|
|
6363
|
+
? withOpenAICompatibleDiscoveryTimeout(options.timeoutMs, fetchModels)
|
|
6364
|
+
: fetchModels();
|
|
6397
6365
|
}
|
|
6398
6366
|
|
|
6399
6367
|
export async function fetchLiteLLMRichModels<TApi extends Api>(
|
|
@@ -6544,13 +6512,7 @@ export function nanoGptModelManagerOptions(
|
|
|
6544
6512
|
const maxOutputTokens = entry.max_output_tokens ?? entry.max_completion_tokens;
|
|
6545
6513
|
const mapped = mapWithBundledReference(
|
|
6546
6514
|
{ ...entry, max_completion_tokens: maxOutputTokens },
|
|
6547
|
-
|
|
6548
|
-
? defaults
|
|
6549
|
-
: {
|
|
6550
|
-
...defaults,
|
|
6551
|
-
contextWindow: toPositiveNumberOrNull(entry.context_length),
|
|
6552
|
-
maxTokens: toPositiveNumberOrNull(maxOutputTokens),
|
|
6553
|
-
},
|
|
6515
|
+
defaults,
|
|
6554
6516
|
reference,
|
|
6555
6517
|
);
|
|
6556
6518
|
return {
|
|
@@ -6783,7 +6745,7 @@ export function githubCopilotModelManagerOptions(config?: GithubCopilotModelMana
|
|
|
6783
6745
|
fetchDynamicModels: async () => {
|
|
6784
6746
|
const fetchImpl = discoveryFetch(config?.fetch);
|
|
6785
6747
|
const requestBaseUrl = isPersonalGitHubCopilotBaseUrl(baseUrl)
|
|
6786
|
-
? ((await
|
|
6748
|
+
? ((await withOpenAICompatibleDiscoveryTimeout(DEFAULT_OPENAI_COMPATIBLE_DISCOVERY_TIMEOUT_MS, signal =>
|
|
6787
6749
|
discoverGitHubCopilotApiEndpoint(apiKey, fetchImpl, signal),
|
|
6788
6750
|
)) ?? baseUrl)
|
|
6789
6751
|
: baseUrl;
|
|
@@ -7794,6 +7756,8 @@ const MODELS_DEV_PROVIDER_DESCRIPTORS_SPECIALIZED: readonly ModelsDevProviderDes
|
|
|
7794
7756
|
return { api: "openai-completions" as const, baseUrl: ZENMUX_OPENAI_BASE_URL };
|
|
7795
7757
|
},
|
|
7796
7758
|
}),
|
|
7759
|
+
// --- Modular ---
|
|
7760
|
+
openAiCompletionsDescriptor("modular", "modular", "https://api.modular.com/v1"),
|
|
7797
7761
|
];
|
|
7798
7762
|
/** All provider descriptors for models.dev data mapping in generate-models.ts. */
|
|
7799
7763
|
export const MODELS_DEV_PROVIDER_DESCRIPTORS: readonly ModelsDevProviderDescriptor[] = [
|
|
@@ -8238,3 +8202,40 @@ export function singularityApiTechModelManagerOptions(
|
|
|
8238
8202
|
): ModelManagerOptions<Api> {
|
|
8239
8203
|
return singularityApiModelManagerOptions("singularityapi-tech", SINGULARITYAPI_TECH_API_BASE_URL, config);
|
|
8240
8204
|
}
|
|
8205
|
+
|
|
8206
|
+
// ---------------------------------------------------------------------------
|
|
8207
|
+
// Scaleway
|
|
8208
|
+
// ---------------------------------------------------------------------------
|
|
8209
|
+
|
|
8210
|
+
/**
|
|
8211
|
+
* Scaleway discovery configuration: the API key plus optional base-URL and
|
|
8212
|
+
* fetch overrides. Consumed by {@link scalewayModelManagerOptions}, and
|
|
8213
|
+
* exported for extensions and tests that construct the manager directly.
|
|
8214
|
+
*/
|
|
8215
|
+
export interface ScalewayModelManagerConfig {
|
|
8216
|
+
apiKey?: string;
|
|
8217
|
+
baseUrl?: string;
|
|
8218
|
+
fetch?: FetchImpl;
|
|
8219
|
+
}
|
|
8220
|
+
|
|
8221
|
+
/**
|
|
8222
|
+
* Scaleway model manager: plain OpenAI-compatible chat completions at
|
|
8223
|
+
* `api.scaleway.ai/v1` (fr-par serverless Generative APIs). `/v1/models`
|
|
8224
|
+
* returns bare ids, so the authored seed in `providers/scaleway.kdl` is the
|
|
8225
|
+
* reference each live row merges names, modalities, prices, and limits from;
|
|
8226
|
+
* a successful fetch is the complete roster and prunes retired rows.
|
|
8227
|
+
*/
|
|
8228
|
+
export function scalewayModelManagerOptions(
|
|
8229
|
+
config?: ScalewayModelManagerConfig,
|
|
8230
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
8231
|
+
return createOpenAICompatibleModelManagerOptions({
|
|
8232
|
+
api: "openai-completions",
|
|
8233
|
+
providerId: "scaleway",
|
|
8234
|
+
defaultBaseUrl: "https://api.scaleway.ai/v1",
|
|
8235
|
+
config,
|
|
8236
|
+
dynamicModelsAuthoritative: true,
|
|
8237
|
+
requireApiKey: true,
|
|
8238
|
+
filterModel: (_entry, model) => !isExcludedModel("scaleway", model.id),
|
|
8239
|
+
mapModel: mapWithBundledReference,
|
|
8240
|
+
});
|
|
8241
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ModelIdentity } from "./compat/types";
|
|
2
2
|
import type { Effort } from "./effort";
|
|
3
|
+
import type { DatedCostCard } from "./pricing";
|
|
3
4
|
|
|
4
5
|
// Re-exported from @oh-my-pi/pi-utils so the whole workspace shares one
|
|
5
6
|
// `fetch`-compatible signature (tls-fetch's wrappers produce/accept it).
|
|
@@ -1477,8 +1478,8 @@ export interface ModelCost extends TokenCost {
|
|
|
1477
1478
|
/**
|
|
1478
1479
|
* Exact local content tokenizer family for a model.
|
|
1479
1480
|
*
|
|
1480
|
-
* Absent means no first-party local tokenizer is known and consumers
|
|
1481
|
-
*
|
|
1481
|
+
* Absent means no first-party local tokenizer is known and consumers count
|
|
1482
|
+
* in o200k_base. The values name tokenizer
|
|
1482
1483
|
* generations rather than providers: DeepSeek V3 through V4 share
|
|
1483
1484
|
* `"deepseek-v3"`; Kimi K2 through K3 share `"kimi-k2"`.
|
|
1484
1485
|
*/
|
|
@@ -1491,6 +1492,7 @@ export type ModelTokenizer =
|
|
|
1491
1492
|
| "deepseek-v3"
|
|
1492
1493
|
| "kimi-k2"
|
|
1493
1494
|
| "glm5"
|
|
1495
|
+
| "cl100k"
|
|
1494
1496
|
| "o200k";
|
|
1495
1497
|
|
|
1496
1498
|
/** One account's discovered entitlements on a model; see {@link Model.accountAccess}. */
|
|
@@ -1686,8 +1688,12 @@ export interface Model<TApi extends Api = Api> {
|
|
|
1686
1688
|
factoryDroidApiProviders?: string[];
|
|
1687
1689
|
/** Factory Droid: `configured_order` when live routing chose the rotation; absent means registry order. */
|
|
1688
1690
|
factoryDroidRoutingSource?: "configured_order";
|
|
1691
|
+
/** Factory Droid: server-side refusal fallbacks the account can run; absent when none are named. */
|
|
1692
|
+
factoryDroidRefusalFallbacks?: string[];
|
|
1689
1693
|
/** Factory Droid Standard Credits base rate (relative per-token weight, not dollars). */
|
|
1690
1694
|
factoryDroidCredits?: number;
|
|
1695
|
+
/** Factory Droid dated `credit-rates` cards, ascending; `getFactoryDroidCreditRate` picks the one due now. */
|
|
1696
|
+
factoryDroidCreditCards?: readonly DatedCostCard[];
|
|
1691
1697
|
/** Canonical organization resolved during Factory discovery. */
|
|
1692
1698
|
factoryDroidOrgId?: string;
|
|
1693
1699
|
cost: ModelCost;
|
|
@@ -1760,8 +1766,9 @@ export interface Model<TApi extends Api = Api> {
|
|
|
1760
1766
|
useResponsesLite?: boolean;
|
|
1761
1767
|
/**
|
|
1762
1768
|
* Service-tier ids the provider advertises for this model (Codex discovery
|
|
1763
|
-
* `service_tiers[].id`,
|
|
1764
|
-
*
|
|
1769
|
+
* `service_tiers[].id`, or an OpenAI-compatible `/v1/models` row's
|
|
1770
|
+
* `service_tiers`, e.g. `priority`, `ultrafast`). Absent when the provider
|
|
1771
|
+
* publishes no per-model tier list.
|
|
1765
1772
|
*/
|
|
1766
1773
|
serviceTiers?: readonly string[];
|
|
1767
1774
|
/** Preferred model to switch to when context promotion is triggered (model id or provider/id). */
|
package/src/utils.ts
CHANGED
|
@@ -40,6 +40,24 @@ export function toBoolean(value: unknown): boolean | undefined {
|
|
|
40
40
|
return typeof value === "boolean" ? value : undefined;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Service-tier ids from a discovery row's `service_tiers` array, deduplicated
|
|
45
|
+
* in advertisement order. Entries may be codex-rs `{ id, name, description }`
|
|
46
|
+
* objects or bare id strings (OpenAI-compatible relays). Returns `undefined`
|
|
47
|
+
* when the value is not an array; an empty array is kept, since it is a
|
|
48
|
+
* statement that the model offers no optional tier.
|
|
49
|
+
*/
|
|
50
|
+
export function parseServiceTierIds(value: unknown): string[] | undefined {
|
|
51
|
+
if (!Array.isArray(value)) return undefined;
|
|
52
|
+
const ids: string[] = [];
|
|
53
|
+
for (const tier of value) {
|
|
54
|
+
const raw = tier !== null && typeof tier === "object" && "id" in tier ? tier.id : tier;
|
|
55
|
+
const id = typeof raw === "string" ? raw.trim() : "";
|
|
56
|
+
if (id && !ids.includes(id)) ids.push(id);
|
|
57
|
+
}
|
|
58
|
+
return ids;
|
|
59
|
+
}
|
|
60
|
+
|
|
43
61
|
export function isAnthropicOAuthToken(key: string): boolean {
|
|
44
62
|
return key.includes("sk-ant-oat");
|
|
45
63
|
}
|
|
@@ -124,7 +124,8 @@ export function factoryDroidModelCacheProviderId(options: { apiKey?: string } &
|
|
|
124
124
|
}
|
|
125
125
|
const region = options.region === "eu" ? "eu" : "global";
|
|
126
126
|
const scope = `${credentialScope}\u0000${options.orgId ?? ""}\u0000${region}\u0000${options.inferenceRegion ?? region}`;
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
|
|
127
|
+
// v5: rows cached before discovery recorded the account's refusal fallbacks
|
|
128
|
+
// would let the request path send every fallback the rules name, so they
|
|
129
|
+
// must be refetched.
|
|
130
|
+
return `factory-droid:models-v5:${Bun.hash(scope).toString(36)}`;
|
|
130
131
|
}
|