@linxiraos/pi-catalog 1.2.1 → 1.3.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 +79 -0
- package/README.md +4 -2
- package/THIRD-PARTY-NOTICES.txt +55 -5
- package/dist/types/build.d.ts +3 -2
- package/dist/types/compat/auth-ids.d.ts +2 -2
- package/dist/types/compat/axes.d.ts +1 -1
- package/dist/types/compat/behavior.d.ts +5 -0
- package/dist/types/compat/catalog-endpoint.d.ts +8 -0
- package/dist/types/compat/image-tokenization.d.ts +9 -1
- package/dist/types/compat/judgment-images.d.ts +25 -0
- package/dist/types/compat/provider-ids.d.ts +1 -1
- package/dist/types/compat/request-compression.d.ts +23 -0
- package/dist/types/compat/request-size.d.ts +12 -3
- package/dist/types/compat/thinking-replay.d.ts +9 -0
- package/dist/types/compat/types.d.ts +4 -0
- package/dist/types/discovery/antigravity.d.ts +7 -0
- package/dist/types/model-cache.d.ts +7 -0
- package/dist/types/model-manager.d.ts +6 -0
- package/dist/types/model-thinking.d.ts +11 -1
- package/dist/types/models.d.ts +16 -1
- package/dist/types/pricing.d.ts +37 -1
- package/dist/types/provider-models/openai-compat.d.ts +18 -46
- package/dist/types/types.d.ts +63 -12
- package/dist/types/wire/codex.d.ts +2 -0
- package/package.json +5 -5
- package/src/build.d.ts +3 -2
- package/src/build.ts +159 -80
- package/src/compat/auth-ids.ts +2 -0
- package/src/compat/axes.d.ts +1 -1
- package/src/compat/axes.ts +129 -4
- package/src/compat/behavior.d.ts +5 -0
- package/src/compat/behavior.ts +9 -1
- package/src/compat/cascade.ts +4 -1
- package/src/compat/catalog-endpoint.ts +43 -0
- package/src/compat/image-tokenization.ts +28 -10
- package/src/compat/judgment-images.ts +83 -0
- package/src/compat/provider-ids.d.ts +1 -1
- package/src/compat/provider-ids.ts +1 -0
- package/src/compat/request-compression.ts +43 -0
- package/src/compat/request-size.ts +20 -5
- package/src/compat/resolve.ts +90 -53
- package/src/compat/rules/README.md +32 -3
- package/src/compat/rules/auth/_order.kdl +1 -1
- package/src/compat/rules/auth/google-antigravity.kdl +1 -1
- package/src/compat/rules/auth/sambanova.kdl +9 -0
- package/src/compat/rules/auth/stencil.kdl +1 -1
- package/src/compat/rules/classes/anthropic.kdl +42 -2
- package/src/compat/rules/classes/deepseek.kdl +4 -0
- package/src/compat/rules/classes/gemini.kdl +46 -2
- package/src/compat/rules/classes/meta.kdl +7 -0
- package/src/compat/rules/classes/minimax.kdl +11 -1
- package/src/compat/rules/classes/openai.kdl +34 -3
- package/src/compat/rules/classes/xai.kdl +26 -0
- package/src/compat/rules/providers/amazon-bedrock.kdl +1 -0
- package/src/compat/rules/providers/anthropic.kdl +23 -0
- package/src/compat/rules/providers/azure.kdl +2 -0
- package/src/compat/rules/providers/cerebras.kdl +3 -0
- package/src/compat/rules/providers/cline-pass.kdl +16 -0
- package/src/compat/rules/providers/commandcode.kdl +10 -0
- package/src/compat/rules/providers/cursor.kdl +50 -1
- package/src/compat/rules/providers/deepinfra.kdl +3 -1
- package/src/compat/rules/providers/devin.kdl +117 -6
- package/src/compat/rules/providers/factory-droid.kdl +6 -0
- package/src/compat/rules/providers/firepass.kdl +15 -0
- package/src/compat/rules/providers/fireworks.kdl +26 -0
- package/src/compat/rules/providers/github-copilot.kdl +24 -0
- package/src/compat/rules/providers/google-antigravity.kdl +37 -0
- package/src/compat/rules/providers/google-gemini-cli.kdl +1 -0
- package/src/compat/rules/providers/google-vertex.kdl +5 -0
- package/src/compat/rules/providers/google.kdl +1 -0
- package/src/compat/rules/providers/image-tokenization.kdl +2 -1
- package/src/compat/rules/providers/local.kdl +8 -1
- package/src/compat/rules/providers/meta.kdl +16 -0
- package/src/compat/rules/providers/moonshot.kdl +2 -0
- package/src/compat/rules/providers/muse-code.kdl +49 -0
- package/src/compat/rules/providers/native-tools.kdl +3 -0
- package/src/compat/rules/providers/ollama-cloud.kdl +10 -0
- package/src/compat/rules/providers/openai-codex.kdl +29 -5
- package/src/compat/rules/providers/openai.kdl +1 -0
- package/src/compat/rules/providers/opencode-go.kdl +35 -0
- package/src/compat/rules/providers/opencode-zen.kdl +4 -0
- package/src/compat/rules/providers/openrouter.kdl +19 -0
- package/src/compat/rules/providers/sambanova.kdl +60 -0
- package/src/compat/rules/providers/tool-call-names.kdl +37 -0
- package/src/compat/rules/providers/typesafe.kdl +11 -0
- package/src/compat/rules/providers/umans.kdl +7 -0
- package/src/compat/rules/providers/venice.kdl +7 -0
- package/src/compat/rules/providers/xai-oauth.kdl +16 -6
- package/src/compat/rules/providers/xai.kdl +3 -0
- package/src/compat/rules/providers/zai.kdl +5 -0
- package/src/compat/rules/runtime/behavior.kdl +128 -8
- package/src/compat/rules/taxonomy/_collapse.kdl +11 -0
- package/src/compat/rules/taxonomy/gemma.kdl +1 -0
- package/src/compat/rules/taxonomy/meta.kdl +7 -0
- package/src/compat/rules/taxonomy/openai.kdl +2 -0
- package/src/compat/rules.json +1 -1
- package/src/compat/thinking-replay.ts +33 -0
- package/src/compat/types.d.ts +4 -0
- package/src/compat/types.ts +4 -0
- package/src/discovery/antigravity.d.ts +7 -0
- package/src/discovery/antigravity.ts +13 -0
- package/src/discovery/codex.ts +0 -4
- package/src/discovery/cursor.ts +28 -20
- package/src/discovery/devin.ts +7 -8
- package/src/model-cache.d.ts +7 -0
- package/src/model-cache.ts +8 -0
- package/src/model-manager.d.ts +6 -0
- package/src/model-manager.ts +41 -21
- package/src/model-thinking.d.ts +80 -0
- package/src/model-thinking.ts +13 -1
- package/src/model-tokenizer.ts +7 -1
- package/src/models.d.ts +16 -1
- package/src/models.json +1 -1
- package/src/models.ts +26 -12
- package/src/pricing.d.ts +37 -1
- package/src/pricing.ts +123 -12
- package/src/provider-models/bundled-references.ts +5 -2
- package/src/provider-models/cache-provider-id.ts +1 -1
- package/src/provider-models/descriptors.ts +2 -0
- package/src/provider-models/google.ts +7 -1
- package/src/provider-models/openai-compat.d.ts +18 -46
- package/src/provider-models/openai-compat.ts +188 -104
- package/src/provider-models/special.ts +1 -0
- package/src/types.d.ts +63 -12
- package/src/types.ts +71 -9
- package/src/wire/codex.d.ts +2 -0
- package/src/wire/codex.ts +17 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a reader bills a replayed signed thinking block as input, from the
|
|
3
|
+
* `thinking-replay-tokens` rules in the compat cascade. A model line's lineage
|
|
4
|
+
* rule (`rules/classes/*.kdl`) applies on every host; readers without one are
|
|
5
|
+
* estimated from the signature's bytes.
|
|
6
|
+
*/
|
|
7
|
+
import { classifyModel } from "../identity";
|
|
8
|
+
import { resolveCascade } from "./cascade";
|
|
9
|
+
import type { ImageTokenizationTarget } from "./image-tokenization";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `output`: the reasoning the signature carries, at the size its turn billed
|
|
13
|
+
* as output; the signature's own bytes are free. `signature`: the signature's
|
|
14
|
+
* bytes stand in for that reasoning.
|
|
15
|
+
*/
|
|
16
|
+
export type ThinkingReplayTokens = "output" | "signature";
|
|
17
|
+
|
|
18
|
+
/** How `reader` bills replayed thinking; `signature` unless its lineage declares otherwise. */
|
|
19
|
+
export function resolveThinkingReplayTokens(reader: ImageTokenizationTarget): ThinkingReplayTokens {
|
|
20
|
+
const provider = reader.provider ?? "";
|
|
21
|
+
const model = reader.id ?? "";
|
|
22
|
+
const identity = reader.identity ?? (model ? classifyModel(provider, model, { lenient: true }) : undefined);
|
|
23
|
+
const tokens = resolveCascade({
|
|
24
|
+
provider,
|
|
25
|
+
api: reader.api ?? "",
|
|
26
|
+
class: identity?.class ?? "unknown",
|
|
27
|
+
model,
|
|
28
|
+
reasoning: false,
|
|
29
|
+
...(identity?.family !== undefined && { family: identity.family }),
|
|
30
|
+
...(identity?.revision !== undefined && { revision: identity.revision }),
|
|
31
|
+
}).catalog.thinkingReplayTokens;
|
|
32
|
+
return tokens === "output" ? "output" : "signature";
|
|
33
|
+
}
|
package/src/compat/types.d.ts
CHANGED
|
@@ -328,6 +328,8 @@ export interface CompiledBehavior {
|
|
|
328
328
|
retryResetTimezones: CompiledRetryResetTimezone[];
|
|
329
329
|
retiredProviders: string[];
|
|
330
330
|
referenceIsolatedProviders: string[];
|
|
331
|
+
/** Built-in model selector chains per agent role, keyed by role in declaration order. */
|
|
332
|
+
roleDefaults: Record<string, string[]>;
|
|
331
333
|
}
|
|
332
334
|
/**
|
|
333
335
|
* A string setting from `auth/*.kdl` that may be overridden by environment
|
|
@@ -634,6 +636,8 @@ export interface CompiledProvider {
|
|
|
634
636
|
dynamicModelsAuthoritative?: boolean;
|
|
635
637
|
/** Generator backfills never copy reasoning/input/limits from same-id rows on other hosts. */
|
|
636
638
|
skipCrossProviderReferenceFills?: boolean;
|
|
639
|
+
/** Runtime discovery keeps a bundled row's display name instead of the live one. */
|
|
640
|
+
keepBundledNames?: boolean;
|
|
637
641
|
/** Present only for providers enrolled in `generate-models.ts` discovery. */
|
|
638
642
|
discovery?: CompiledProviderDiscovery;
|
|
639
643
|
/** Non-chat model kinds mapped to their runtime transport APIs. */
|
package/src/compat/types.ts
CHANGED
|
@@ -352,6 +352,8 @@ export interface CompiledBehavior {
|
|
|
352
352
|
retryResetTimezones: CompiledRetryResetTimezone[];
|
|
353
353
|
retiredProviders: string[];
|
|
354
354
|
referenceIsolatedProviders: string[];
|
|
355
|
+
/** Built-in model selector chains per agent role, keyed by role in declaration order. */
|
|
356
|
+
roleDefaults: Record<string, string[]>;
|
|
355
357
|
}
|
|
356
358
|
|
|
357
359
|
/**
|
|
@@ -668,6 +670,8 @@ export interface CompiledProvider {
|
|
|
668
670
|
dynamicModelsAuthoritative?: boolean;
|
|
669
671
|
/** Generator backfills never copy reasoning/input/limits from same-id rows on other hosts. */
|
|
670
672
|
skipCrossProviderReferenceFills?: boolean;
|
|
673
|
+
/** Runtime discovery keeps a bundled row's display name instead of the live one. */
|
|
674
|
+
keepBundledNames?: boolean;
|
|
671
675
|
/** Present only for providers enrolled in `generate-models.ts` discovery. */
|
|
672
676
|
discovery?: CompiledProviderDiscovery;
|
|
673
677
|
/** Non-chat model kinds mapped to their runtime transport APIs. */
|
|
@@ -18,6 +18,13 @@ export interface AntigravityDiscoveryApiModel {
|
|
|
18
18
|
apiProvider?: string;
|
|
19
19
|
modelProvider?: string;
|
|
20
20
|
isInternal?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Set for roster rows the backend lists but refuses to serve on this
|
|
23
|
+
* account (e.g. `gemini-4-argon-disabled`, `tagTitle: "Unavailable"`,
|
|
24
|
+
* `model: MODEL_PLACEHOLDER_M421`); a request against such an id answers
|
|
25
|
+
* `Cloud Code Assist API error (404): Requested entity was not found.`
|
|
26
|
+
*/
|
|
27
|
+
disabled?: boolean;
|
|
21
28
|
supportsVideo?: boolean;
|
|
22
29
|
}
|
|
23
30
|
/**
|
|
@@ -29,6 +29,13 @@ export interface AntigravityDiscoveryApiModel {
|
|
|
29
29
|
apiProvider?: string;
|
|
30
30
|
modelProvider?: string;
|
|
31
31
|
isInternal?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Set for roster rows the backend lists but refuses to serve on this
|
|
34
|
+
* account (e.g. `gemini-4-argon-disabled`, `tagTitle: "Unavailable"`,
|
|
35
|
+
* `model: MODEL_PLACEHOLDER_M421`); a request against such an id answers
|
|
36
|
+
* `Cloud Code Assist API error (404): Requested entity was not found.`
|
|
37
|
+
*/
|
|
38
|
+
disabled?: boolean;
|
|
32
39
|
supportsVideo?: boolean;
|
|
33
40
|
}
|
|
34
41
|
|
|
@@ -72,6 +79,7 @@ const AntigravityDiscoveryApiModelSchema = type({
|
|
|
72
79
|
"apiProvider?": type("unknown").pipe(value => (typeof value === "string" ? value : undefined)),
|
|
73
80
|
"modelProvider?": type("unknown").pipe(value => (typeof value === "string" ? value : undefined)),
|
|
74
81
|
"isInternal?": type("unknown").pipe(value => (typeof value === "boolean" ? value : undefined)),
|
|
82
|
+
"disabled?": type("unknown").pipe(value => (typeof value === "boolean" ? value : undefined)),
|
|
75
83
|
"supportsVideo?": type("unknown").pipe(value => (typeof value === "boolean" ? value : undefined)),
|
|
76
84
|
});
|
|
77
85
|
|
|
@@ -188,6 +196,11 @@ export async function fetchAntigravityDiscoveryModels(
|
|
|
188
196
|
if (model.isInternal === true) {
|
|
189
197
|
continue;
|
|
190
198
|
}
|
|
199
|
+
// Rows the backend marks unavailable are listed but 404 on inference;
|
|
200
|
+
// discovery is authoritative, so drop them instead of offering a dead id.
|
|
201
|
+
if (model.disabled === true) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
191
204
|
|
|
192
205
|
const supportsImages = model.supportsImages === true;
|
|
193
206
|
models.push({
|
package/src/discovery/codex.ts
CHANGED
|
@@ -69,7 +69,6 @@ const codexModelEntrySchema = type({
|
|
|
69
69
|
"priority?": "unknown",
|
|
70
70
|
"prefer_websockets?": "unknown",
|
|
71
71
|
"use_responses_lite?": "unknown",
|
|
72
|
-
"tool_mode?": "unknown",
|
|
73
72
|
"available_access_programs?": "unknown",
|
|
74
73
|
"service_tiers?": "unknown",
|
|
75
74
|
});
|
|
@@ -302,7 +301,6 @@ interface ParsedCodexModelEntry {
|
|
|
302
301
|
input: ("text" | "image")[];
|
|
303
302
|
preferWebsockets: boolean;
|
|
304
303
|
useResponsesLite: boolean;
|
|
305
|
-
toolMode: boolean;
|
|
306
304
|
priority: number;
|
|
307
305
|
/** Advertised tier ids; `undefined` when the entry has no `service_tiers` array. */
|
|
308
306
|
serviceTiers: string[] | undefined;
|
|
@@ -356,7 +354,6 @@ function parseCodexModelEntry(entry: unknown): ParsedCodexModelEntry | null {
|
|
|
356
354
|
input: normalizeInputModalities(payload.input_modalities),
|
|
357
355
|
preferWebsockets: toBoolean(payload.prefer_websockets) === true,
|
|
358
356
|
useResponsesLite: toBoolean(payload.use_responses_lite) === true,
|
|
359
|
-
toolMode: payload.tool_mode === "code_mode_only",
|
|
360
357
|
priority: toFiniteNumber(payload.priority) ?? Number.MAX_SAFE_INTEGER,
|
|
361
358
|
serviceTiers,
|
|
362
359
|
};
|
|
@@ -422,7 +419,6 @@ function buildNormalizedCodexModel(
|
|
|
422
419
|
maxTokens,
|
|
423
420
|
...(parsed.preferWebsockets ? { preferWebsockets: true } : {}),
|
|
424
421
|
...(parsed.useResponsesLite ? { useResponsesLite: true } : {}),
|
|
425
|
-
...(parsed.toolMode ? { toolMode: "code_mode_only" as const } : {}),
|
|
426
422
|
...(parsed.serviceTiers !== undefined ? { serviceTiers: parsed.serviceTiers } : {}),
|
|
427
423
|
...(parsed.priority !== Number.MAX_SAFE_INTEGER ? { priority: parsed.priority } : {}),
|
|
428
424
|
},
|
package/src/discovery/cursor.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import * as http2 from "node:http2";
|
|
2
2
|
import { type } from "@linxiraos/pi-omptype";
|
|
3
|
+
import { slugify } from "@linxiraos/pi-utils";
|
|
3
4
|
import { collapseVariants, type EffortVariantFamily, reviewedVariantFamilyId } from "../compat/collapse";
|
|
4
5
|
import { compareRevision, parseRevision } from "../compat/revision";
|
|
5
6
|
import { resolveCatalogAxes, resolveModelPolicy } from "../compat/resolve";
|
|
6
7
|
import { classifyModel } from "../compat/taxonomy";
|
|
7
8
|
import { Effort, THINKING_EFFORTS } from "../effort";
|
|
8
9
|
import { getBundledModels } from "../models";
|
|
10
|
+
import { datedCost, parseDatedCostCards } from "../pricing";
|
|
9
11
|
import { toModelSpec } from "../provider-models/bundled-references";
|
|
10
|
-
import type { CursorModelRoute, Model, ModelSpec, TokenCost } from "../types";
|
|
12
|
+
import type { CursorModelRoute, Model, ModelCost, ModelSpec, TokenCost } from "../types";
|
|
11
13
|
import {
|
|
12
14
|
CURSOR_AVAILABLE_MODELS_PATH,
|
|
13
15
|
CURSOR_DEFAULT_BASE_URL,
|
|
@@ -435,13 +437,21 @@ function cursorVariantCostMultiplier(
|
|
|
435
437
|
return multiplier;
|
|
436
438
|
}
|
|
437
439
|
|
|
438
|
-
function scaleTokenCost(cost:
|
|
440
|
+
function scaleTokenCost(cost: ModelCost, multiplier: number): ModelCost {
|
|
439
441
|
if (multiplier === 1) return cost;
|
|
442
|
+
const scale = <T extends TokenCost>(card: T): T => ({
|
|
443
|
+
...card,
|
|
444
|
+
input: card.input * multiplier,
|
|
445
|
+
output: card.output * multiplier,
|
|
446
|
+
cacheRead: card.cacheRead * multiplier,
|
|
447
|
+
cacheWrite: card.cacheWrite * multiplier,
|
|
448
|
+
});
|
|
449
|
+
const effectiveRates = cost.timeBased?.effectiveRates;
|
|
440
450
|
return {
|
|
441
|
-
input: cost.input
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
451
|
+
...scale({ input: cost.input, output: cost.output, cacheRead: cost.cacheRead, cacheWrite: cost.cacheWrite }),
|
|
452
|
+
...(cost.timeBased && {
|
|
453
|
+
timeBased: { ...cost.timeBased, ...(effectiveRates && { effectiveRates: effectiveRates.map(scale) }) },
|
|
454
|
+
}),
|
|
445
455
|
};
|
|
446
456
|
}
|
|
447
457
|
|
|
@@ -462,7 +472,7 @@ function resolveRichCursorCardCost(
|
|
|
462
472
|
details: AvailableModelsResponse_ModelDetails,
|
|
463
473
|
entry: NormalizedRichVariant,
|
|
464
474
|
laneId: string,
|
|
465
|
-
):
|
|
475
|
+
): ModelCost | undefined {
|
|
466
476
|
const costAxes = details.parameterDefinitions
|
|
467
477
|
.filter(definition =>
|
|
468
478
|
[
|
|
@@ -503,7 +513,7 @@ function resolveRichCursorCardCost(
|
|
|
503
513
|
return base && scaleTokenCost(base, cursorVariantCostMultiplier(details, entry));
|
|
504
514
|
}
|
|
505
515
|
|
|
506
|
-
function resolveCursorCardAxes(slug: string): { patch?:
|
|
516
|
+
function resolveCursorCardAxes(slug: string): { patch?: ModelCost; fallback?: ModelCost } {
|
|
507
517
|
const axes = resolveCatalogAxes({
|
|
508
518
|
id: slug,
|
|
509
519
|
name: slug,
|
|
@@ -516,7 +526,9 @@ function resolveCursorCardAxes(slug: string): { patch?: TokenCost; fallback?: To
|
|
|
516
526
|
contextWindow: DEFAULT_CONTEXT_WINDOW,
|
|
517
527
|
maxTokens: DEFAULT_MAX_TOKENS,
|
|
518
528
|
});
|
|
519
|
-
|
|
529
|
+
// Dated cards ride along as a schedule, so a lane priced from a sibling
|
|
530
|
+
// slug's card (whose own id no rule matches) still changes price on time.
|
|
531
|
+
const card = (payload: unknown, axis: "cost-patch" | "cost-fallback"): ModelCost | undefined => {
|
|
520
532
|
if (typeof payload !== "object" || payload === null) return undefined;
|
|
521
533
|
const field = (key: string): number | undefined => {
|
|
522
534
|
const value = Reflect.get(payload, key);
|
|
@@ -525,9 +537,12 @@ function resolveCursorCardAxes(slug: string): { patch?: TokenCost; fallback?: To
|
|
|
525
537
|
const input = field("input");
|
|
526
538
|
const output = field("output");
|
|
527
539
|
if (input === undefined || output === undefined) return undefined;
|
|
528
|
-
return
|
|
540
|
+
return datedCost(
|
|
541
|
+
{ input, output, cacheRead: field("cacheRead") ?? 0, cacheWrite: field("cacheWrite") ?? 0 },
|
|
542
|
+
parseDatedCostCards(payload, axis),
|
|
543
|
+
);
|
|
529
544
|
};
|
|
530
|
-
return { patch: card(axes.costPatch), fallback: card(axes.costFallback) };
|
|
545
|
+
return { patch: card(axes.costPatch, "cost-patch"), fallback: card(axes.costFallback, "cost-fallback") };
|
|
531
546
|
}
|
|
532
547
|
|
|
533
548
|
function normalizeRichCursorModels(
|
|
@@ -679,8 +694,8 @@ function cursorLaneId(
|
|
|
679
694
|
): string {
|
|
680
695
|
const suffixes: string[] = [];
|
|
681
696
|
for (const parameter of parameters) {
|
|
682
|
-
const id =
|
|
683
|
-
const value =
|
|
697
|
+
const id = slugify(parameter.id);
|
|
698
|
+
const value = slugify(parameter.value);
|
|
684
699
|
// Boolean lanes are named by their `true` side regardless of variant
|
|
685
700
|
// order: Cursor lists `composer-2.5`'s Fast variant first, and treating
|
|
686
701
|
// that as the default would hand the bare id to the Fast route.
|
|
@@ -729,13 +744,6 @@ function reviewedCursorLaneId(entries: readonly NormalizedRichVariant[], claimed
|
|
|
729
744
|
return familyId;
|
|
730
745
|
}
|
|
731
746
|
|
|
732
|
-
function sanitizeCursorLanePart(value: string): string {
|
|
733
|
-
return value
|
|
734
|
-
.toLowerCase()
|
|
735
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
736
|
-
.replace(/^-+|-+$/g, "");
|
|
737
|
-
}
|
|
738
|
-
|
|
739
747
|
function cursorLaneName(details: AvailableModelsResponse_ModelDetails, laneId: string, baseId: string): string {
|
|
740
748
|
const baseName =
|
|
741
749
|
details.clientDisplayName?.trim() || details.serverModelName?.trim() || details.name.trim() || baseId;
|
package/src/discovery/devin.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { logger } from "@linxiraos/pi-utils";
|
|
1
|
+
import { logger, slugify } from "@linxiraos/pi-utils";
|
|
2
2
|
import { collapseVariants, type EffortVariantFamily } from "../compat/collapse";
|
|
3
3
|
import { Effort, THINKING_EFFORTS } from "../effort";
|
|
4
4
|
import type { DevinCompat, FetchImpl, ModelCost, ModelSpec } from "../types";
|
|
@@ -106,8 +106,8 @@ function devinCostDenominatorTokens(denominator: string): number {
|
|
|
106
106
|
/**
|
|
107
107
|
* Per-million-token rates from the config's cost dimensions. `COST_FUZZY` marks
|
|
108
108
|
* an estimated rate, not a different unit, so both kinds are read. `cacheWrite`
|
|
109
|
-
* has no Cascade dimension
|
|
110
|
-
*
|
|
109
|
+
* has no Cascade dimension and stays 0; the provider's `cache-write-rate` rules
|
|
110
|
+
* supply Devin's published rate.
|
|
111
111
|
*
|
|
112
112
|
* Composite configs (`fusion`) flatten their own rate card plus every
|
|
113
113
|
* dispatched component's card into one `modelDimensions` list. A `Sidekick`
|
|
@@ -229,10 +229,7 @@ function collectDevinFamilyLane(lanes: Map<string, DevinFamilyLane>, config: Cli
|
|
|
229
229
|
if (thinking === false) effort = "off";
|
|
230
230
|
|
|
231
231
|
// Family label as an ZETA id: "GPT-5.6 Sol" -> "gpt-5-6-sol".
|
|
232
|
-
const baseId = label
|
|
233
|
-
.toLowerCase()
|
|
234
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
235
|
-
.replace(/^-+|-+$/g, "");
|
|
232
|
+
const baseId = slugify(label);
|
|
236
233
|
if (!baseId) return;
|
|
237
234
|
const laneId = `${baseId}${oneMillionContext ? "-1m" : ""}${fast ? "-fast" : ""}`;
|
|
238
235
|
let lane = lanes.get(laneId);
|
|
@@ -478,7 +475,9 @@ function devinFusionLeadUid(uid: string, liveUids: ReadonlyMap<string, unknown>)
|
|
|
478
475
|
/**
|
|
479
476
|
* Point a Fusion pairing at its lead. zeta runs only the lead (the sidekick is
|
|
480
477
|
* paired by the native client), so the limits and pricing a caller budgets
|
|
481
|
-
* against are the lead's, not the composite card's.
|
|
478
|
+
* against are the lead's, not the composite card's. The composite uid names no
|
|
479
|
+
* model, so `buildModel` resolves the row under its `requestModelId` and the
|
|
480
|
+
* lead's catalog rules (cache-write rate, long-context tier) apply there.
|
|
482
481
|
*/
|
|
483
482
|
function routeDevinFusionLead(spec: ModelSpec<"devin-agent">, lead: ModelSpec<"devin-agent">): void {
|
|
484
483
|
spec.requestModelId = lead.id;
|
package/src/model-cache.d.ts
CHANGED
|
@@ -20,6 +20,13 @@ export interface CacheEntry<TApi extends Api = Api> {
|
|
|
20
20
|
models: Model<TApi>[];
|
|
21
21
|
fresh: boolean;
|
|
22
22
|
authoritative: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* `authoritative` of the payload row itself, ignoring the
|
|
25
|
+
* `model_cache_refresh` side row. A failed refresh flips only the side row
|
|
26
|
+
* (freshness plus retry backoff), so this keeps answering "were `models`
|
|
27
|
+
* written by an authoritative discovery result?" after such a write.
|
|
28
|
+
*/
|
|
29
|
+
payloadAuthoritative: boolean;
|
|
23
30
|
updatedAt: number;
|
|
24
31
|
/** Model ids whose live headers were intentionally omitted from disk. */
|
|
25
32
|
headerOmittedModelIds: readonly string[];
|
package/src/model-cache.ts
CHANGED
|
@@ -145,6 +145,13 @@ export interface CacheEntry<TApi extends Api = Api> {
|
|
|
145
145
|
models: Model<TApi>[];
|
|
146
146
|
fresh: boolean;
|
|
147
147
|
authoritative: boolean;
|
|
148
|
+
/**
|
|
149
|
+
* `authoritative` of the payload row itself, ignoring the
|
|
150
|
+
* `model_cache_refresh` side row. A failed refresh flips only the side row
|
|
151
|
+
* (freshness plus retry backoff), so this keeps answering "were `models`
|
|
152
|
+
* written by an authoritative discovery result?" after such a write.
|
|
153
|
+
*/
|
|
154
|
+
payloadAuthoritative: boolean;
|
|
148
155
|
updatedAt: number;
|
|
149
156
|
/** Model ids whose live headers were intentionally omitted from disk. */
|
|
150
157
|
headerOmittedModelIds: readonly string[];
|
|
@@ -596,6 +603,7 @@ function parseCacheRow<TApi extends Api>(
|
|
|
596
603
|
models,
|
|
597
604
|
fresh,
|
|
598
605
|
authoritative,
|
|
606
|
+
payloadAuthoritative: row.authoritative === 1,
|
|
599
607
|
updatedAt,
|
|
600
608
|
headerOmittedModelIds,
|
|
601
609
|
unrestorableHeaderModelIds,
|
package/src/model-manager.d.ts
CHANGED
|
@@ -51,6 +51,12 @@ export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload =
|
|
|
51
51
|
* a zero discovery price is treated as unknown and the bundled rate is kept.
|
|
52
52
|
*/
|
|
53
53
|
dynamicCostAuthoritative?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* When true, a static row's display name wins over the cached or live row
|
|
56
|
+
* for the same id, so a model keeps one name before and after discovery.
|
|
57
|
+
* Set from the provider's `keep-bundled-names` catalog entry.
|
|
58
|
+
*/
|
|
59
|
+
keepStaticNames?: boolean;
|
|
54
60
|
/**
|
|
55
61
|
* When true, a fresh cache never satisfies an online-eligible refresh: the
|
|
56
62
|
* dynamic fetch always runs (an explicit `"offline"` strategy is still
|
package/src/model-manager.ts
CHANGED
|
@@ -73,6 +73,12 @@ export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload =
|
|
|
73
73
|
* a zero discovery price is treated as unknown and the bundled rate is kept.
|
|
74
74
|
*/
|
|
75
75
|
dynamicCostAuthoritative?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* When true, a static row's display name wins over the cached or live row
|
|
78
|
+
* for the same id, so a model keeps one name before and after discovery.
|
|
79
|
+
* Set from the provider's `keep-bundled-names` catalog entry.
|
|
80
|
+
*/
|
|
81
|
+
keepStaticNames?: boolean;
|
|
76
82
|
/**
|
|
77
83
|
* When true, a fresh cache never satisfies an online-eligible refresh: the
|
|
78
84
|
* dynamic fetch always runs (an explicit `"offline"` strategy is still
|
|
@@ -254,11 +260,14 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
254
260
|
const staticModels = options.staticModels
|
|
255
261
|
? materializeStaticModels<TApi>(options.staticModels)
|
|
256
262
|
: (getBundledModels(options.providerId as GeneratedProvider) as Model<TApi>[]);
|
|
257
|
-
const
|
|
263
|
+
const modelsDevAdditiveStaticModelIds =
|
|
258
264
|
options.modelsDev?.additiveOnly && staticModels.length > 0
|
|
259
265
|
? new Set(staticModels.map(model => model.id))
|
|
260
266
|
: undefined;
|
|
261
267
|
const cache = readModelCache<TApi>(cacheProviderId, ttlMs, now, dbPath);
|
|
268
|
+
const dynamicModelsAuthoritative = options.dynamicModelsAuthoritative ?? false;
|
|
269
|
+
const isAuthoritativeDynamicCache = dynamicModelsAuthoritative && (cache?.payloadAuthoritative ?? false);
|
|
270
|
+
const cacheAdditiveStaticModelIds = isAuthoritativeDynamicCache ? undefined : modelsDevAdditiveStaticModelIds;
|
|
262
271
|
const restoredCache = restoreCachedModelHeaders(
|
|
263
272
|
cache?.models ?? [],
|
|
264
273
|
staticModels,
|
|
@@ -270,11 +279,11 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
270
279
|
);
|
|
271
280
|
const usableCachedModels = restoredCache.models.filter(model => !restoredCache.unresolvedModelIds.has(model.id));
|
|
272
281
|
const cacheHasUnresolvedHeaders = restoredCache.unresolvedModelIds.size > 0;
|
|
273
|
-
const dynamicModelsAuthoritative = options.dynamicModelsAuthoritative ?? false;
|
|
274
282
|
const mergeAuthority: DynamicMergeAuthority = {
|
|
275
283
|
input: options.dynamicInputAuthoritative ?? false,
|
|
276
284
|
reasoning: options.dynamicReasoningAuthoritative ?? false,
|
|
277
285
|
cost: options.dynamicCostAuthoritative ?? false,
|
|
286
|
+
staticNames: options.keepStaticNames ? new Map(staticModels.map(model => [model.id, model.name])) : undefined,
|
|
278
287
|
};
|
|
279
288
|
const cacheDropIds = options.dropCachedModelIdsOnStaticMismatch;
|
|
280
289
|
const staticCatalogFingerprint = fingerprintStaticModels(staticModels, dynamicModelsAuthoritative);
|
|
@@ -315,10 +324,10 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
315
324
|
cacheFingerprintMatches &&
|
|
316
325
|
!cacheHasUnresolvedHeaders
|
|
317
326
|
) {
|
|
318
|
-
const cacheContribution =
|
|
319
|
-
? restoredCache.models.filter(model => !
|
|
327
|
+
const cacheContribution = cacheAdditiveStaticModelIds
|
|
328
|
+
? restoredCache.models.filter(model => !cacheAdditiveStaticModelIds.has(model.id))
|
|
320
329
|
: restoredCache.models;
|
|
321
|
-
const cachedModels =
|
|
330
|
+
const cachedModels = cacheAdditiveStaticModelIds
|
|
322
331
|
? mergeCatalogMetrics(
|
|
323
332
|
mergeDynamicModels(staticModels, cacheContribution, mergeAuthority),
|
|
324
333
|
restoredCache.models,
|
|
@@ -338,8 +347,8 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
338
347
|
: [null, null];
|
|
339
348
|
const modelsDevFetchSucceeded = fetchedModelsDevModels !== null;
|
|
340
349
|
const normalizedModelsDevModels = fetchedModelsDevModels?.models ?? [];
|
|
341
|
-
const modelsDevModels =
|
|
342
|
-
? normalizedModelsDevModels.filter(model => !
|
|
350
|
+
const modelsDevModels = modelsDevAdditiveStaticModelIds
|
|
351
|
+
? normalizedModelsDevModels.filter(model => !modelsDevAdditiveStaticModelIds.has(model.id))
|
|
343
352
|
: normalizedModelsDevModels;
|
|
344
353
|
const shouldUseFreshCacheAsAuthoritative =
|
|
345
354
|
strategy === "online-if-uncached" && hasUsableFreshCache && hasAuthoritativeCache;
|
|
@@ -359,10 +368,8 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
359
368
|
cacheFingerprintMatches,
|
|
360
369
|
options.dropCachedModelIdsOnStaticMismatch,
|
|
361
370
|
);
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const cacheModels = additiveStaticModelIds
|
|
365
|
-
? preparedCacheModels.filter(model => !additiveStaticModelIds.has(model.id))
|
|
371
|
+
const cacheModels = cacheAdditiveStaticModelIds
|
|
372
|
+
? preparedCacheModels.filter(model => !cacheAdditiveStaticModelIds.has(model.id))
|
|
366
373
|
: preparedCacheModels;
|
|
367
374
|
const dynamicModels = fetchedDynamicModels?.models ?? [];
|
|
368
375
|
// A successful empty endpoint result stays authoritative for THIS cycle (so an
|
|
@@ -383,7 +390,9 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
383
390
|
fetchedModelsDevModels?.explicitKindModels,
|
|
384
391
|
);
|
|
385
392
|
const catalogMetricsSource = modelsDevFetchSucceeded ? normalizedModelsDevModels : preparedCacheModels;
|
|
386
|
-
const mergedWithCatalogMetrics =
|
|
393
|
+
const mergedWithCatalogMetrics = (
|
|
394
|
+
modelsDevFetchSucceeded ? modelsDevAdditiveStaticModelIds : cacheAdditiveStaticModelIds
|
|
395
|
+
)
|
|
387
396
|
? mergeCatalogMetrics(mergedWithModelsDev, catalogMetricsSource)
|
|
388
397
|
: mergedWithModelsDev;
|
|
389
398
|
const mergedModels = mergeDynamicModels(
|
|
@@ -393,7 +402,11 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
393
402
|
fetchedDynamicModels?.explicitKindModels,
|
|
394
403
|
);
|
|
395
404
|
const models = collapseBuiltVariants(
|
|
396
|
-
authoritativeDynamicFetchSucceeded
|
|
405
|
+
authoritativeDynamicFetchSucceeded
|
|
406
|
+
? retainModelIds(mergedModels, dynamicModels)
|
|
407
|
+
: !dynamicFetchSucceeded && isAuthoritativeDynamicCache && usableCachedModels.length > 0
|
|
408
|
+
? retainModelIds(mergedModels, usableCachedModels)
|
|
409
|
+
: mergedModels,
|
|
397
410
|
);
|
|
398
411
|
const resolutionAuthoritative = !hasRemoteFetcher || remoteResolutionComplete || shouldUseFreshCacheAsAuthoritative;
|
|
399
412
|
const remoteUpdatedAt = anyRemoteFetchSucceeded ? now() : undefined;
|
|
@@ -438,15 +451,18 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
438
451
|
cacheFingerprintMatches,
|
|
439
452
|
options.dropCachedModelIdsOnStaticMismatch,
|
|
440
453
|
);
|
|
441
|
-
const latestCacheModels =
|
|
442
|
-
? preparedLatestCacheModels.filter(model => !
|
|
454
|
+
const latestCacheModels = cacheAdditiveStaticModelIds
|
|
455
|
+
? preparedLatestCacheModels.filter(model => !cacheAdditiveStaticModelIds.has(model.id))
|
|
443
456
|
: preparedLatestCacheModels;
|
|
457
|
+
const mergedFallbackModels = mergeDynamicModels(
|
|
458
|
+
mergeDynamicModels(staticModels, latestCacheModels, mergeAuthority),
|
|
459
|
+
modelsDevModels,
|
|
460
|
+
mergeAuthority,
|
|
461
|
+
);
|
|
444
462
|
const fallbackSnapshotModels = collapseBuiltVariants(
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
mergeAuthority,
|
|
449
|
-
),
|
|
463
|
+
isAuthoritativeDynamicCache && latestUsableCacheModels.length > 0
|
|
464
|
+
? retainModelIds(mergedFallbackModels, latestUsableCacheModels)
|
|
465
|
+
: mergedFallbackModels,
|
|
450
466
|
);
|
|
451
467
|
if (fallbackSnapshotModels.length > 0 || latestCache !== null || cache !== null) {
|
|
452
468
|
writeModelCache(
|
|
@@ -582,6 +598,8 @@ interface DynamicMergeAuthority {
|
|
|
582
598
|
readonly input: boolean;
|
|
583
599
|
readonly reasoning: boolean;
|
|
584
600
|
readonly cost: boolean;
|
|
601
|
+
/** Static display names by id that override dynamic rows (`keepStaticNames`). */
|
|
602
|
+
readonly staticNames: ReadonlyMap<string, string> | undefined;
|
|
585
603
|
}
|
|
586
604
|
|
|
587
605
|
function mergeDynamicModels<TApi extends Api>(
|
|
@@ -694,7 +712,9 @@ function mergeDynamicModel<TApi extends Api>(
|
|
|
694
712
|
return buildModel({
|
|
695
713
|
...existingModel,
|
|
696
714
|
...dynamicModel,
|
|
697
|
-
name:
|
|
715
|
+
name:
|
|
716
|
+
authority.staticNames?.get(dynamicModel.id) ??
|
|
717
|
+
preferDiscoveryName(dynamicModel.name, existingModel.name, dynamicModel.id),
|
|
698
718
|
reasoning,
|
|
699
719
|
...(trustsOwnThinking ? { thinking: dynamicModel.thinking } : {}),
|
|
700
720
|
input: supportsImage ? ["text", "image"] : ["text"],
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thinking metadata: runtime field-read helpers.
|
|
3
|
+
*
|
|
4
|
+
* Derivation lives in the compat engine (`compat/resolve`) and runs exactly
|
|
5
|
+
* once per model — from `buildModel` for dynamic specs and from the catalog
|
|
6
|
+
* generator for bundled entries. Everything here reads baked fields only: no
|
|
7
|
+
* id parsing, no host matching, no compat detection per request.
|
|
8
|
+
*/
|
|
9
|
+
import { Effort } from "./effort";
|
|
10
|
+
import type { Api, Model, ModelSpec, ThinkingConfig } from "./types";
|
|
11
|
+
/**
|
|
12
|
+
* Runtime helpers read baked metadata only, so they accept both pre-build
|
|
13
|
+
* specs and built models.
|
|
14
|
+
*/
|
|
15
|
+
type ApiModel<TApi extends Api = Api> = ModelSpec<TApi> | Model<TApi>;
|
|
16
|
+
/**
|
|
17
|
+
* Returns the supported thinking efforts declared on the model metadata.
|
|
18
|
+
* Empty for non-reasoning models and for reasoning models without a
|
|
19
|
+
* controllable effort surface (`thinking: undefined`).
|
|
20
|
+
*/
|
|
21
|
+
export declare function getSupportedEfforts<TApi extends Api>(model: ApiModel<TApi>): readonly Effort[];
|
|
22
|
+
/**
|
|
23
|
+
* Neutral view of a model whose thinking ladder is unverified: `reasoning: false`
|
|
24
|
+
* and no ladder. Re-entering policy resolution starts from it (the ladder is
|
|
25
|
+
* derived output, not authored input), and requests without an effort send it so
|
|
26
|
+
* no reasoning or disable fields reach an upstream that may reject them.
|
|
27
|
+
*/
|
|
28
|
+
export declare function withoutUnverifiedThinking<T extends {
|
|
29
|
+
reasoning: boolean;
|
|
30
|
+
thinking?: ThinkingConfig;
|
|
31
|
+
}>(model: T): T;
|
|
32
|
+
/**
|
|
33
|
+
* Clamps a requested thinking level against explicit model metadata.
|
|
34
|
+
*
|
|
35
|
+
* Non-reasoning models always resolve to `undefined`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function clampThinkingLevelForModel<TApi extends Api>(model: ApiModel<TApi> | undefined, requested: Effort | undefined): Effort | undefined;
|
|
38
|
+
export declare function requireSupportedEffort<TApi extends Api>(model: ApiModel<TApi>, effort: Effort): Effort;
|
|
39
|
+
/** Maps a normalized thinking effort to Google's `thinkingLevel` enum values.
|
|
40
|
+
* When a collapsed family routes `minimal` onto the same wire id as `low`
|
|
41
|
+
* (Antigravity Gemini 3.6/3.7 Flash), emit `LOW` — Cloud Code Assist rejects
|
|
42
|
+
* `MINIMAL` on those `-low` SKUs.
|
|
43
|
+
*/
|
|
44
|
+
export declare function mapEffortToGoogleThinkingLevel<TApi extends Api>(effort: Effort, model?: ApiModel<TApi>): "MINIMAL" | "LOW" | "MEDIUM" | "HIGH";
|
|
45
|
+
/**
|
|
46
|
+
* Maps a normalized thinking effort to Anthropic adaptive effort values via
|
|
47
|
+
* the model's baked `thinking.effortMap` (identity for unmapped efforts).
|
|
48
|
+
*
|
|
49
|
+
* The Anthropic adaptive wire vocabulary has no `minimal` tier (valid values:
|
|
50
|
+
* `low`, `medium`, `high`, `xhigh`, `max`); a model ladder that exposes
|
|
51
|
+
* `minimal` — custom `anthropic-messages` providers do, unlike the built-in
|
|
52
|
+
* Claude ladders — would otherwise serialize `output_config.effort: "minimal"`
|
|
53
|
+
* and 400 (`level "minimal" not supported`). Clamp it to the lowest real tier,
|
|
54
|
+
* mirroring {@link mapEffortToGoogleThinkingLevel}'s `minimal` handling.
|
|
55
|
+
*/
|
|
56
|
+
export declare function mapEffortToAnthropicAdaptiveEffort<TApi extends Api>(model: ApiModel<TApi>, effort: Effort): "low" | "medium" | "high" | "xhigh" | "max" | "adaptive";
|
|
57
|
+
/**
|
|
58
|
+
* Resolves the upstream wire model id for a request at the given effort
|
|
59
|
+
* (`undefined` = thinking off). Collapsed effort-tier variants route through
|
|
60
|
+
* `thinking.effortRouting`; everything else falls back to
|
|
61
|
+
* `requestModelId ?? id`.
|
|
62
|
+
*/
|
|
63
|
+
export declare function resolveWireModelId<TApi extends Api>(model: ApiModel<TApi>, effort: Effort | undefined): string;
|
|
64
|
+
/**
|
|
65
|
+
* Lowest supported effort in canonical order — the clamp target for
|
|
66
|
+
* thinking-off requests on `thinking.requiresEffort` models.
|
|
67
|
+
*/
|
|
68
|
+
export declare function minimumSupportedEffort<TApi extends Api>(model: ApiModel<TApi>): Effort | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* Clamp target for effort-less requests on `thinking.requiresEffort` models:
|
|
71
|
+
* the effort whose wire route equals the model's default wire id
|
|
72
|
+
* (`requestModelId`), so a collapsed row clamps to the tier it already
|
|
73
|
+
* advertises as its default rather than the numerically lowest supported tier
|
|
74
|
+
* (e.g. Cursor Grok 4.5/4.6 default to `medium`, the only tier the Start plan
|
|
75
|
+
* serves). Falls back to {@link minimumSupportedEffort} when no route matches
|
|
76
|
+
* the default id — families whose default already is the minimum, or that
|
|
77
|
+
* expose no routing, are unaffected.
|
|
78
|
+
*/
|
|
79
|
+
export declare function defaultSupportedEffort<TApi extends Api>(model: ApiModel<TApi>): Effort | undefined;
|
|
80
|
+
export {};
|
package/src/model-thinking.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* id parsing, no host matching, no compat detection per request.
|
|
8
8
|
*/
|
|
9
9
|
import { Effort, THINKING_EFFORTS } from "./effort";
|
|
10
|
-
import type { Api, Model, ModelSpec } from "./types";
|
|
10
|
+
import type { Api, Model, ModelSpec, ThinkingConfig } from "./types";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Runtime helpers read baked metadata only, so they accept both pre-build
|
|
@@ -27,6 +27,18 @@ export function getSupportedEfforts<TApi extends Api>(model: ApiModel<TApi>): re
|
|
|
27
27
|
return model.thinking?.efforts ?? [];
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Neutral view of a model whose thinking ladder is unverified: `reasoning: false`
|
|
32
|
+
* and no ladder. Re-entering policy resolution starts from it (the ladder is
|
|
33
|
+
* derived output, not authored input), and requests without an effort send it so
|
|
34
|
+
* no reasoning or disable fields reach an upstream that may reject them.
|
|
35
|
+
*/
|
|
36
|
+
export function withoutUnverifiedThinking<T extends { reasoning: boolean; thinking?: ThinkingConfig }>(model: T): T {
|
|
37
|
+
if (!model.thinking?.unverified) return model;
|
|
38
|
+
const { thinking: _derived, ...rest } = model;
|
|
39
|
+
return { ...rest, reasoning: false } as T;
|
|
40
|
+
}
|
|
41
|
+
|
|
30
42
|
/**
|
|
31
43
|
* Clamps a requested thinking level against explicit model metadata.
|
|
32
44
|
*
|
package/src/model-tokenizer.ts
CHANGED
|
@@ -49,6 +49,11 @@ function glmTokenizer(identity: ModelIdentity): ModelTokenizer | undefined {
|
|
|
49
49
|
return identity.class === "glm" && revisionAtLeast(identity.revision, "5") ? "glm5" : undefined;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// GPT-5 and later bill text in o200k_base; older generations (cl100k GPT-4, unparsed gpt-4o) keep the estimate.
|
|
53
|
+
function openAITokenizer(identity: ModelIdentity): ModelTokenizer | undefined {
|
|
54
|
+
return identity.class === "openai" && revisionAtLeast(identity.revision, "5") ? "o200k" : undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
52
57
|
/**
|
|
53
58
|
* Resolve the exact locally embedded tokenizer for a canonical model id.
|
|
54
59
|
*
|
|
@@ -68,7 +73,8 @@ export function resolveModelTokenizer(modelId: string, provider = ""): ModelToke
|
|
|
68
73
|
qwenTokenizer(identity) ??
|
|
69
74
|
deepSeekTokenizer(identity) ??
|
|
70
75
|
kimiTokenizer(identity) ??
|
|
71
|
-
glmTokenizer(identity)
|
|
76
|
+
glmTokenizer(identity) ??
|
|
77
|
+
openAITokenizer(identity);
|
|
72
78
|
if (modelTokenizerCache.size === MAX_TOKENIZER_CACHE_ENTRIES) modelTokenizerCache.clear();
|
|
73
79
|
modelTokenizerCache.set(cacheKey, tokenizer ?? null);
|
|
74
80
|
return tokenizer;
|