llm-cli-gateway 2.15.0 → 2.17.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/.agents/skills/least-cost-routing/SKILL.md +123 -0
- package/CHANGELOG.md +78 -1
- package/dist/acp/client.js +5 -0
- package/dist/acp/flight-redaction.d.ts +2 -0
- package/dist/acp/flight-redaction.js +2 -0
- package/dist/acp/process-manager.js +2 -1
- package/dist/acp/provider-registry.js +8 -8
- package/dist/acp/runtime.d.ts +1 -0
- package/dist/acp/runtime.js +20 -0
- package/dist/acp/types.d.ts +52 -0
- package/dist/acp/types.js +8 -0
- package/dist/api-provider.d.ts +1 -0
- package/dist/api-provider.js +1 -0
- package/dist/async-job-manager.d.ts +20 -1
- package/dist/async-job-manager.js +85 -28
- package/dist/claude-mcp-config.js +1 -1
- package/dist/compressor/estimate.d.ts +5 -0
- package/dist/compressor/estimate.js +21 -0
- package/dist/compressor/index.d.ts +23 -0
- package/dist/compressor/index.js +77 -0
- package/dist/compressor/router.d.ts +2 -0
- package/dist/compressor/router.js +57 -0
- package/dist/compressor/transforms/ansi.d.ts +3 -0
- package/dist/compressor/transforms/ansi.js +99 -0
- package/dist/compressor/transforms/json.d.ts +1 -0
- package/dist/compressor/transforms/json.js +156 -0
- package/dist/compressor/transforms/log.d.ts +12 -0
- package/dist/compressor/transforms/log.js +55 -0
- package/dist/compressor/transforms/whitespace.d.ts +8 -0
- package/dist/compressor/transforms/whitespace.js +79 -0
- package/dist/config.d.ts +39 -0
- package/dist/config.js +166 -6
- package/dist/db.js +4 -4
- package/dist/doctor.d.ts +34 -1
- package/dist/doctor.js +85 -3
- package/dist/executor.d.ts +6 -0
- package/dist/executor.js +16 -3
- package/dist/flight-recorder.d.ts +19 -0
- package/dist/flight-recorder.js +110 -2
- package/dist/http-transport.js +19 -21
- package/dist/index.d.ts +58 -2
- package/dist/index.js +929 -94
- package/dist/job-store.d.ts +14 -2
- package/dist/job-store.js +170 -43
- package/dist/lcr-priors.d.ts +60 -0
- package/dist/lcr-priors.js +190 -0
- package/dist/lcr-router-env.d.ts +20 -0
- package/dist/lcr-router-env.js +133 -0
- package/dist/lcr-telemetry.d.ts +2 -0
- package/dist/lcr-telemetry.js +17 -0
- package/dist/least-cost-router.d.ts +86 -0
- package/dist/least-cost-router.js +296 -0
- package/dist/least-cost-types.d.ts +34 -0
- package/dist/least-cost-types.js +1 -0
- package/dist/migrate-sessions.js +1 -1
- package/dist/migrate.js +1 -1
- package/dist/model-registry.js +1 -1
- package/dist/postgres-job-store-worker.js +56 -13
- package/dist/pricing.d.ts +17 -0
- package/dist/pricing.js +167 -0
- package/dist/provider-definitions.d.ts +5 -0
- package/dist/provider-definitions.js +56 -10
- package/dist/provider-tool-capabilities.js +3 -3
- package/dist/request-helpers.d.ts +2 -2
- package/dist/request-helpers.js +1 -1
- package/dist/resources.d.ts +37 -2
- package/dist/resources.js +96 -1
- package/dist/retry.d.ts +1 -0
- package/dist/retry.js +1 -1
- package/dist/spawn-env-isolation.d.ts +10 -0
- package/dist/spawn-env-isolation.js +55 -0
- package/dist/token-estimator.d.ts +6 -0
- package/dist/token-estimator.js +59 -0
- package/dist/upstream-contracts.js +48 -28
- package/dist/validation-receipt.js +1 -1
- package/dist/validation-tools.d.ts +6 -0
- package/dist/validation-tools.js +174 -54
- package/npm-shrinkwrap.json +2 -2
- package/package.json +15 -6
- package/setup/status.schema.json +68 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { CLI_TYPES } from "./provider-types.js";
|
|
2
|
+
import { getCliInfo } from "./model-registry.js";
|
|
3
|
+
import { getProviderDefinition } from "./provider-definitions.js";
|
|
4
|
+
import { cliBreakerState } from "./executor.js";
|
|
5
|
+
import { apiProviderBreakerState } from "./api-provider.js";
|
|
6
|
+
import { providerAtCapacity } from "./async-job-manager.js";
|
|
7
|
+
import { getModelCost } from "./pricing.js";
|
|
8
|
+
import { classifyContent } from "./token-estimator.js";
|
|
9
|
+
import { computeLcrPriorsFromDb, lookupCalibrationK, } from "./lcr-priors.js";
|
|
10
|
+
const CLI_TYPE_SET = new Set(CLI_TYPES);
|
|
11
|
+
function isCliType(provider) {
|
|
12
|
+
return CLI_TYPE_SET.has(provider);
|
|
13
|
+
}
|
|
14
|
+
const API_PROVIDER_CAPABILITIES = {
|
|
15
|
+
acceptsImages: false,
|
|
16
|
+
acceptsAttachments: false,
|
|
17
|
+
toolCalling: true,
|
|
18
|
+
jsonSchema: true,
|
|
19
|
+
outputFormats: ["text", "json"],
|
|
20
|
+
capabilityScope: "full",
|
|
21
|
+
effortLevels: [],
|
|
22
|
+
};
|
|
23
|
+
const EMPTY_PRIORS = {
|
|
24
|
+
outputPriors: new Map(),
|
|
25
|
+
calibration: new Map(),
|
|
26
|
+
accuracyByBasis: new Map(),
|
|
27
|
+
};
|
|
28
|
+
const PRIORS_TTL_MS = 60_000;
|
|
29
|
+
let priorsCacheEntry = null;
|
|
30
|
+
function resolvePriors(deps) {
|
|
31
|
+
if (deps.priors)
|
|
32
|
+
return deps.priors;
|
|
33
|
+
const scope = deps.priorsScope ?? "global";
|
|
34
|
+
if (scope === "off" || !deps.flightRecorder)
|
|
35
|
+
return EMPTY_PRIORS;
|
|
36
|
+
const key = `${scope}:${deps.ownerPrincipal ?? ""}`;
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
if (priorsCacheEntry &&
|
|
39
|
+
priorsCacheEntry.key === key &&
|
|
40
|
+
now - priorsCacheEntry.at < PRIORS_TTL_MS) {
|
|
41
|
+
return priorsCacheEntry.priors;
|
|
42
|
+
}
|
|
43
|
+
const priors = computeLcrPriorsFromDb(deps.flightRecorder, {
|
|
44
|
+
priorsScope: scope,
|
|
45
|
+
ownerPrincipal: deps.ownerPrincipal,
|
|
46
|
+
});
|
|
47
|
+
priorsCacheEntry = { at: now, key, priors };
|
|
48
|
+
return priors;
|
|
49
|
+
}
|
|
50
|
+
export function resetLcrPriorsCacheForTest() {
|
|
51
|
+
priorsCacheEntry = null;
|
|
52
|
+
}
|
|
53
|
+
function candidateCapabilities(provider) {
|
|
54
|
+
if (isCliType(provider)) {
|
|
55
|
+
const def = getProviderDefinition(provider);
|
|
56
|
+
return {
|
|
57
|
+
acceptsImages: def.requestSurface.acceptsImages,
|
|
58
|
+
acceptsAttachments: def.requestSurface.acceptsAttachments,
|
|
59
|
+
toolCalling: def.requestSurface.toolCalling,
|
|
60
|
+
jsonSchema: def.requestSurface.jsonSchema,
|
|
61
|
+
outputFormats: def.outputFormats,
|
|
62
|
+
capabilityScope: def.capabilityScope,
|
|
63
|
+
effortLevels: def.discovery.modelDiscovery.facts.effortLevels,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return API_PROVIDER_CAPABILITIES;
|
|
67
|
+
}
|
|
68
|
+
export function buildRouterEnv(deps) {
|
|
69
|
+
const apiProviderByName = new Map(deps.apiProviders.map(p => [p.name, p]));
|
|
70
|
+
const providerNames = [...CLI_TYPES, ...deps.apiProviders.map(p => p.name)];
|
|
71
|
+
const metricsByTool = deps.performanceMetrics.snapshot().byTool;
|
|
72
|
+
const priors = resolvePriors(deps);
|
|
73
|
+
const isAuthed = deps.isAuthed ??
|
|
74
|
+
((provider) => {
|
|
75
|
+
return isCliType(provider) || apiProviderByName.has(provider);
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
providers() {
|
|
79
|
+
return providerNames;
|
|
80
|
+
},
|
|
81
|
+
models(provider) {
|
|
82
|
+
if (isCliType(provider)) {
|
|
83
|
+
return Object.keys(getCliInfo()[provider].models);
|
|
84
|
+
}
|
|
85
|
+
const api = apiProviderByName.get(provider);
|
|
86
|
+
if (!api)
|
|
87
|
+
return [];
|
|
88
|
+
return api.models && api.models.length > 0 ? api.models : [api.defaultModel];
|
|
89
|
+
},
|
|
90
|
+
isAuthed,
|
|
91
|
+
breakerState(provider) {
|
|
92
|
+
return isCliType(provider) ? cliBreakerState(provider) : apiProviderBreakerState(provider);
|
|
93
|
+
},
|
|
94
|
+
atCapacity(provider) {
|
|
95
|
+
return providerAtCapacity(deps.limiterSnapshot, provider);
|
|
96
|
+
},
|
|
97
|
+
capabilities(provider) {
|
|
98
|
+
return candidateCapabilities(provider);
|
|
99
|
+
},
|
|
100
|
+
modelCost(provider, model) {
|
|
101
|
+
return getModelCost(provider, model, { preferCatalog: deps.preferCatalogPrice });
|
|
102
|
+
},
|
|
103
|
+
successRate(provider) {
|
|
104
|
+
return metricsByTool[provider]?.successRate ?? 0;
|
|
105
|
+
},
|
|
106
|
+
meanLatencyMs(provider) {
|
|
107
|
+
return metricsByTool[provider]?.averageResponseTimeMs ?? 0;
|
|
108
|
+
},
|
|
109
|
+
calibrationK(prompt, family) {
|
|
110
|
+
return lookupCalibrationK(priors, classifyContent(prompt), family);
|
|
111
|
+
},
|
|
112
|
+
outputPrior(provider, model) {
|
|
113
|
+
const p = priors.outputPriors.get(`${provider}:${model}`);
|
|
114
|
+
return p ? { median: p.median, p90: p.p90 } : null;
|
|
115
|
+
},
|
|
116
|
+
confidenceBand(prompt, family) {
|
|
117
|
+
const bucket = priors.calibration.get(`${classifyContent(prompt)}:${family}`);
|
|
118
|
+
return bucket?.confidence ?? "low";
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
export function toRouterConfig(cfg) {
|
|
123
|
+
return {
|
|
124
|
+
minTier: cfg.minTier,
|
|
125
|
+
maxCostUsd: cfg.maxCostUsd,
|
|
126
|
+
defaultExpectedOutputTokens: cfg.defaultExpectedOutputTokens,
|
|
127
|
+
budgetOutputSafetyFactor: cfg.budgetOutputSafetyFactor,
|
|
128
|
+
allowUnpriced: cfg.allowUnpriced,
|
|
129
|
+
tiers: cfg.tiers,
|
|
130
|
+
candidates: cfg.candidates,
|
|
131
|
+
preferenceOrder: cfg.preferenceOrder.length > 0 ? cfg.preferenceOrder : undefined,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { CLI_TYPES } from "./provider-types.js";
|
|
2
|
+
const CLI_TELEMETRY_TIERS = {
|
|
3
|
+
claude: "T1",
|
|
4
|
+
codex: "T2",
|
|
5
|
+
gemini: "T2",
|
|
6
|
+
grok: "T3",
|
|
7
|
+
mistral: "T1",
|
|
8
|
+
devin: "T4",
|
|
9
|
+
cursor: "T4",
|
|
10
|
+
};
|
|
11
|
+
const DEFAULT_TELEMETRY_TIER = "T2";
|
|
12
|
+
export function telemetryTierFor(provider) {
|
|
13
|
+
if (CLI_TYPES.includes(provider)) {
|
|
14
|
+
return CLI_TELEMETRY_TIERS[provider];
|
|
15
|
+
}
|
|
16
|
+
return DEFAULT_TELEMETRY_TIER;
|
|
17
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { ModelCost, CostBasis, Confidence, QualityTier, PriceSource } from "./least-cost-types.js";
|
|
2
|
+
export interface Candidate {
|
|
3
|
+
provider: string;
|
|
4
|
+
model: string;
|
|
5
|
+
}
|
|
6
|
+
export interface CandidateCapabilities {
|
|
7
|
+
acceptsImages: boolean;
|
|
8
|
+
acceptsAttachments: boolean;
|
|
9
|
+
toolCalling: boolean;
|
|
10
|
+
jsonSchema: boolean;
|
|
11
|
+
outputFormats: readonly string[];
|
|
12
|
+
capabilityScope: string;
|
|
13
|
+
effortLevels: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
export interface RouterEnv {
|
|
16
|
+
providers(): readonly string[];
|
|
17
|
+
models(provider: string): readonly string[];
|
|
18
|
+
isAuthed(provider: string): boolean;
|
|
19
|
+
breakerState(provider: string): string;
|
|
20
|
+
atCapacity(provider: string): boolean;
|
|
21
|
+
capabilities(provider: string): CandidateCapabilities;
|
|
22
|
+
modelCost(provider: string, model: string): ModelCost;
|
|
23
|
+
successRate(provider: string, model: string): number;
|
|
24
|
+
meanLatencyMs(provider: string, model: string): number;
|
|
25
|
+
calibrationK(prompt: string, family: string): number;
|
|
26
|
+
outputPrior(provider: string, model: string): {
|
|
27
|
+
median: number;
|
|
28
|
+
p90: number;
|
|
29
|
+
} | null;
|
|
30
|
+
confidenceBand(prompt: string, family: string): Confidence;
|
|
31
|
+
}
|
|
32
|
+
export interface RequiredCapabilities {
|
|
33
|
+
images?: boolean;
|
|
34
|
+
attachments?: boolean;
|
|
35
|
+
toolCalling?: boolean;
|
|
36
|
+
jsonSchema?: boolean;
|
|
37
|
+
outputFormat?: string;
|
|
38
|
+
effort?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface RouteRequestInput {
|
|
41
|
+
prompt: string;
|
|
42
|
+
candidates?: Candidate[];
|
|
43
|
+
minTier?: QualityTier;
|
|
44
|
+
maxCostUsd?: number;
|
|
45
|
+
expectedOutputTokens?: number;
|
|
46
|
+
maxOutputTokens?: number;
|
|
47
|
+
requiredCapabilities?: RequiredCapabilities;
|
|
48
|
+
allowUnpriced?: boolean;
|
|
49
|
+
budgetWaiver?: boolean;
|
|
50
|
+
fallback?: Candidate;
|
|
51
|
+
}
|
|
52
|
+
export interface RouterConfig {
|
|
53
|
+
minTier: QualityTier;
|
|
54
|
+
maxCostUsd: number;
|
|
55
|
+
defaultExpectedOutputTokens: number;
|
|
56
|
+
budgetOutputSafetyFactor: number;
|
|
57
|
+
allowUnpriced: boolean;
|
|
58
|
+
tiers: Record<string, QualityTier>;
|
|
59
|
+
candidates: {
|
|
60
|
+
allow: string[];
|
|
61
|
+
deny: string[];
|
|
62
|
+
};
|
|
63
|
+
preferenceOrder?: string[];
|
|
64
|
+
}
|
|
65
|
+
export interface RejectedCandidate {
|
|
66
|
+
candidate: Candidate;
|
|
67
|
+
reason: string;
|
|
68
|
+
}
|
|
69
|
+
export interface RouteDecision {
|
|
70
|
+
chosen: Candidate | null;
|
|
71
|
+
tier?: QualityTier;
|
|
72
|
+
estCostUsd?: number;
|
|
73
|
+
costBasis?: CostBasis;
|
|
74
|
+
confidence?: Confidence;
|
|
75
|
+
nearTie?: boolean;
|
|
76
|
+
estInputTokens?: number;
|
|
77
|
+
estOutputTokens?: number;
|
|
78
|
+
priceAsOf?: string;
|
|
79
|
+
priceSource?: PriceSource;
|
|
80
|
+
consideredCount: number;
|
|
81
|
+
rejected: RejectedCandidate[];
|
|
82
|
+
reroutes: number;
|
|
83
|
+
error?: "NoEligibleCandidate" | "BudgetExceeded";
|
|
84
|
+
}
|
|
85
|
+
export declare function selectCandidate(req: RouteRequestInput, env: RouterEnv, config: RouterConfig, excluded?: ReadonlySet<string>): RouteDecision;
|
|
86
|
+
export declare function selectCheapestPerTier(req: RouteRequestInput, env: RouterEnv, config: RouterConfig, excluded?: ReadonlySet<string>): RouteDecision[];
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { modelIdToFamily, composeCost } from "./pricing.js";
|
|
2
|
+
import { estimateInputTokens } from "./token-estimator.js";
|
|
3
|
+
const TIER_ORDER = {
|
|
4
|
+
economy: 0,
|
|
5
|
+
standard: 1,
|
|
6
|
+
frontier: 2,
|
|
7
|
+
};
|
|
8
|
+
function candidateKey(c) {
|
|
9
|
+
return `${c.provider}:${c.model}`;
|
|
10
|
+
}
|
|
11
|
+
function isExplicitlyListed(c, explicit) {
|
|
12
|
+
if (!explicit)
|
|
13
|
+
return false;
|
|
14
|
+
return explicit.some(e => e.provider === c.provider && e.model === c.model);
|
|
15
|
+
}
|
|
16
|
+
function passesAllowDeny(c, config) {
|
|
17
|
+
const providerKey = c.provider;
|
|
18
|
+
const pairKey = candidateKey(c);
|
|
19
|
+
const matches = (entry) => entry === providerKey || entry === pairKey;
|
|
20
|
+
if (config.candidates.deny.some(matches))
|
|
21
|
+
return false;
|
|
22
|
+
if (config.candidates.allow.length === 0)
|
|
23
|
+
return true;
|
|
24
|
+
return config.candidates.allow.some(matches);
|
|
25
|
+
}
|
|
26
|
+
function buildPool(req, env, config) {
|
|
27
|
+
const source = req.candidates && req.candidates.length > 0
|
|
28
|
+
? req.candidates
|
|
29
|
+
: env
|
|
30
|
+
.providers()
|
|
31
|
+
.flatMap(provider => env.models(provider).map(model => ({ provider, model })));
|
|
32
|
+
return source.filter(c => passesAllowDeny(c, config));
|
|
33
|
+
}
|
|
34
|
+
function checkCapabilities(caps, required) {
|
|
35
|
+
if (!required)
|
|
36
|
+
return { ok: true };
|
|
37
|
+
if (required.images === true && !caps.acceptsImages) {
|
|
38
|
+
return { ok: false, reason: "capability:images" };
|
|
39
|
+
}
|
|
40
|
+
if (required.attachments === true && !caps.acceptsAttachments) {
|
|
41
|
+
return { ok: false, reason: "capability:attachments" };
|
|
42
|
+
}
|
|
43
|
+
if (required.toolCalling === true && !caps.toolCalling) {
|
|
44
|
+
return { ok: false, reason: "capability:toolCalling" };
|
|
45
|
+
}
|
|
46
|
+
if (required.jsonSchema === true && !caps.jsonSchema) {
|
|
47
|
+
return { ok: false, reason: "capability:jsonSchema" };
|
|
48
|
+
}
|
|
49
|
+
if (required.outputFormat !== undefined && !caps.outputFormats.includes(required.outputFormat)) {
|
|
50
|
+
return { ok: false, reason: "capability:outputFormat" };
|
|
51
|
+
}
|
|
52
|
+
if (required.effort !== undefined && !caps.effortLevels.includes(required.effort)) {
|
|
53
|
+
return { ok: false, reason: "capability:effort" };
|
|
54
|
+
}
|
|
55
|
+
return { ok: true };
|
|
56
|
+
}
|
|
57
|
+
function checkTier(candidate, req, config) {
|
|
58
|
+
const family = modelIdToFamily(candidate.model);
|
|
59
|
+
const tierKey = `${candidate.provider}:${family}`;
|
|
60
|
+
const tier = config.tiers[tierKey];
|
|
61
|
+
if (tier === undefined) {
|
|
62
|
+
if (isExplicitlyListed(candidate, req.candidates)) {
|
|
63
|
+
return { ok: true, tier: undefined };
|
|
64
|
+
}
|
|
65
|
+
return { ok: false, reason: "untiered" };
|
|
66
|
+
}
|
|
67
|
+
const effectiveMinTier = req.minTier ?? config.minTier;
|
|
68
|
+
if (TIER_ORDER[tier] < TIER_ORDER[effectiveMinTier]) {
|
|
69
|
+
return { ok: false, reason: "below-min-tier" };
|
|
70
|
+
}
|
|
71
|
+
return { ok: true, tier };
|
|
72
|
+
}
|
|
73
|
+
function evaluateBaseEligibility(candidate, req, env) {
|
|
74
|
+
if (!env.isAuthed(candidate.provider)) {
|
|
75
|
+
return { ok: false, reason: "auth" };
|
|
76
|
+
}
|
|
77
|
+
if (env.breakerState(candidate.provider) !== "CLOSED") {
|
|
78
|
+
return { ok: false, reason: "breaker" };
|
|
79
|
+
}
|
|
80
|
+
if (env.atCapacity(candidate.provider)) {
|
|
81
|
+
return { ok: false, reason: "capacity" };
|
|
82
|
+
}
|
|
83
|
+
const caps = env.capabilities(candidate.provider);
|
|
84
|
+
const capResult = checkCapabilities(caps, req.requiredCapabilities);
|
|
85
|
+
if (!capResult.ok) {
|
|
86
|
+
return { ok: false, reason: capResult.reason };
|
|
87
|
+
}
|
|
88
|
+
if (caps.capabilityScope === "maintain-only" && !isExplicitlyListed(candidate, req.candidates)) {
|
|
89
|
+
return { ok: false, reason: "maintain-only" };
|
|
90
|
+
}
|
|
91
|
+
return { ok: true };
|
|
92
|
+
}
|
|
93
|
+
const CONFIDENCE_ORDER = { low: 0, medium: 1, high: 2 };
|
|
94
|
+
function minConfidence(a, b) {
|
|
95
|
+
return CONFIDENCE_ORDER[a] <= CONFIDENCE_ORDER[b] ? a : b;
|
|
96
|
+
}
|
|
97
|
+
function rankCandidate(candidate, tier, modelCost, req, config, env) {
|
|
98
|
+
const family = modelCost.family !== "unknown" ? modelCost.family : modelIdToFamily(candidate.model);
|
|
99
|
+
const calibrationK = env.calibrationK(req.prompt, family);
|
|
100
|
+
const estInputTokens = estimateInputTokens(req.prompt, { family, calibrationK });
|
|
101
|
+
const prior = env.outputPrior(candidate.provider, candidate.model);
|
|
102
|
+
const estOutputTokens = req.expectedOutputTokens ??
|
|
103
|
+
(prior ? Math.max(1, Math.round(prior.median)) : config.defaultExpectedOutputTokens);
|
|
104
|
+
const budgetOutputTokens = req.maxOutputTokens ??
|
|
105
|
+
(prior
|
|
106
|
+
? Math.max(1, Math.round(prior.p90))
|
|
107
|
+
: config.defaultExpectedOutputTokens * config.budgetOutputSafetyFactor);
|
|
108
|
+
const rankResult = composeCost(null, { estInputTokens, estOutputTokens }, modelCost);
|
|
109
|
+
const band = env.confidenceBand(req.prompt, family);
|
|
110
|
+
return {
|
|
111
|
+
candidate,
|
|
112
|
+
tier,
|
|
113
|
+
modelCost,
|
|
114
|
+
estInputTokens,
|
|
115
|
+
estOutputTokens,
|
|
116
|
+
rankCostUsd: rankResult.costUsd,
|
|
117
|
+
rankKey: modelCost.source === "unknown" ? Number.POSITIVE_INFINITY : rankResult.costUsd,
|
|
118
|
+
costBasis: rankResult.cost_basis,
|
|
119
|
+
confidence: minConfidence(rankResult.confidence, band),
|
|
120
|
+
budgetOutputTokens,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function preferenceIndex(provider, preferenceOrder) {
|
|
124
|
+
if (!preferenceOrder)
|
|
125
|
+
return Number.MAX_SAFE_INTEGER;
|
|
126
|
+
const idx = preferenceOrder.indexOf(provider);
|
|
127
|
+
return idx === -1 ? Number.MAX_SAFE_INTEGER : idx;
|
|
128
|
+
}
|
|
129
|
+
function compareCandidates(a, b, env, config) {
|
|
130
|
+
if (a.rankKey !== b.rankKey)
|
|
131
|
+
return a.rankKey - b.rankKey;
|
|
132
|
+
const successA = env.successRate(a.candidate.provider, a.candidate.model);
|
|
133
|
+
const successB = env.successRate(b.candidate.provider, b.candidate.model);
|
|
134
|
+
if (successA !== successB)
|
|
135
|
+
return successB - successA;
|
|
136
|
+
const latencyA = env.meanLatencyMs(a.candidate.provider, a.candidate.model);
|
|
137
|
+
const latencyB = env.meanLatencyMs(b.candidate.provider, b.candidate.model);
|
|
138
|
+
if (latencyA !== latencyB)
|
|
139
|
+
return latencyA - latencyB;
|
|
140
|
+
const prefA = preferenceIndex(a.candidate.provider, config.preferenceOrder);
|
|
141
|
+
const prefB = preferenceIndex(b.candidate.provider, config.preferenceOrder);
|
|
142
|
+
if (prefA !== prefB)
|
|
143
|
+
return prefA - prefB;
|
|
144
|
+
const lexA = `${a.candidate.provider}/${a.candidate.model}`;
|
|
145
|
+
const lexB = `${b.candidate.provider}/${b.candidate.model}`;
|
|
146
|
+
return lexA.localeCompare(lexB);
|
|
147
|
+
}
|
|
148
|
+
function checkBudget(ranked, req, config, isFallback) {
|
|
149
|
+
const { modelCost, estInputTokens } = ranked;
|
|
150
|
+
const allowUnpriced = req.allowUnpriced ?? config.allowUnpriced;
|
|
151
|
+
const budgetWaiver = req.budgetWaiver ?? false;
|
|
152
|
+
if (modelCost.source === "unknown") {
|
|
153
|
+
if (allowUnpriced && budgetWaiver)
|
|
154
|
+
return { ok: true };
|
|
155
|
+
return { ok: false, reason: "budget" };
|
|
156
|
+
}
|
|
157
|
+
const budgetResult = composeCost(null, { estInputTokens, estOutputTokens: ranked.budgetOutputTokens }, modelCost);
|
|
158
|
+
const budgetCost = budgetResult.costUsd;
|
|
159
|
+
const effectiveMax = req.maxCostUsd ?? config.maxCostUsd;
|
|
160
|
+
if (budgetCost > effectiveMax) {
|
|
161
|
+
if (isFallback && budgetWaiver)
|
|
162
|
+
return { ok: true };
|
|
163
|
+
return { ok: false, reason: "budget" };
|
|
164
|
+
}
|
|
165
|
+
return { ok: true };
|
|
166
|
+
}
|
|
167
|
+
function toDecisionFromRanked(ranked, consideredCount, rejected, nearTie) {
|
|
168
|
+
return {
|
|
169
|
+
chosen: ranked.candidate,
|
|
170
|
+
tier: ranked.tier,
|
|
171
|
+
estCostUsd: ranked.rankCostUsd,
|
|
172
|
+
costBasis: ranked.costBasis,
|
|
173
|
+
confidence: ranked.confidence,
|
|
174
|
+
nearTie,
|
|
175
|
+
estInputTokens: ranked.estInputTokens,
|
|
176
|
+
estOutputTokens: ranked.estOutputTokens,
|
|
177
|
+
priceAsOf: ranked.modelCost.asOf,
|
|
178
|
+
priceSource: ranked.modelCost.source,
|
|
179
|
+
consideredCount,
|
|
180
|
+
rejected,
|
|
181
|
+
reroutes: 0,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function buildRankedPool(req, env, config, excluded) {
|
|
185
|
+
const pool = buildPool(req, env, config).filter(c => !excluded?.has(candidateKey(c)));
|
|
186
|
+
const rejected = [];
|
|
187
|
+
const eligible = [];
|
|
188
|
+
for (const candidate of pool) {
|
|
189
|
+
const baseEligibility = evaluateBaseEligibility(candidate, req, env);
|
|
190
|
+
if (!baseEligibility.ok) {
|
|
191
|
+
rejected.push({ candidate, reason: baseEligibility.reason });
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const tierResult = checkTier(candidate, req, config);
|
|
195
|
+
if (!tierResult.ok) {
|
|
196
|
+
rejected.push({ candidate, reason: tierResult.reason });
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const modelCost = env.modelCost(candidate.provider, candidate.model);
|
|
200
|
+
const allowUnpriced = req.allowUnpriced ?? config.allowUnpriced;
|
|
201
|
+
if (modelCost.source === "unknown" && !allowUnpriced) {
|
|
202
|
+
rejected.push({ candidate, reason: "unpriced" });
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
eligible.push({ candidate, tier: tierResult.tier, modelCost });
|
|
206
|
+
}
|
|
207
|
+
const ranked = eligible.map(e => rankCandidate(e.candidate, e.tier, e.modelCost, req, config, env));
|
|
208
|
+
ranked.sort((a, b) => compareCandidates(a, b, env, config));
|
|
209
|
+
return { ranked, rejected };
|
|
210
|
+
}
|
|
211
|
+
export function selectCandidate(req, env, config, excluded) {
|
|
212
|
+
const { ranked, rejected } = buildRankedPool(req, env, config, excluded);
|
|
213
|
+
if (ranked.length === 0) {
|
|
214
|
+
if (req.fallback) {
|
|
215
|
+
const fallbackReq = {
|
|
216
|
+
...req,
|
|
217
|
+
candidates: [...(req.candidates ?? []), req.fallback],
|
|
218
|
+
};
|
|
219
|
+
const fbBase = evaluateBaseEligibility(req.fallback, fallbackReq, env);
|
|
220
|
+
if (!fbBase.ok) {
|
|
221
|
+
return {
|
|
222
|
+
chosen: null,
|
|
223
|
+
consideredCount: 0,
|
|
224
|
+
rejected: [...rejected, { candidate: req.fallback, reason: fbBase.reason }],
|
|
225
|
+
reroutes: 0,
|
|
226
|
+
error: "NoEligibleCandidate",
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
const fbTier = checkTier(req.fallback, fallbackReq, config);
|
|
230
|
+
if (!fbTier.ok) {
|
|
231
|
+
return {
|
|
232
|
+
chosen: null,
|
|
233
|
+
consideredCount: 0,
|
|
234
|
+
rejected: [...rejected, { candidate: req.fallback, reason: fbTier.reason }],
|
|
235
|
+
reroutes: 0,
|
|
236
|
+
error: "NoEligibleCandidate",
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const fallbackModelCost = env.modelCost(req.fallback.provider, req.fallback.model);
|
|
240
|
+
const fallbackRanked = rankCandidate(req.fallback, fbTier.tier, fallbackModelCost, req, config, env);
|
|
241
|
+
const budget = checkBudget(fallbackRanked, req, config, true);
|
|
242
|
+
if (!budget.ok) {
|
|
243
|
+
return {
|
|
244
|
+
chosen: null,
|
|
245
|
+
consideredCount: 0,
|
|
246
|
+
rejected: [...rejected, { candidate: req.fallback, reason: budget.reason }],
|
|
247
|
+
reroutes: 0,
|
|
248
|
+
error: "BudgetExceeded",
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return toDecisionFromRanked(fallbackRanked, 0, rejected, false);
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
chosen: null,
|
|
255
|
+
consideredCount: 0,
|
|
256
|
+
rejected,
|
|
257
|
+
reroutes: 0,
|
|
258
|
+
error: "NoEligibleCandidate",
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const best = ranked[0];
|
|
262
|
+
const secondBest = ranked.length > 1 ? ranked[1] : undefined;
|
|
263
|
+
const nearTie = secondBest !== undefined &&
|
|
264
|
+
Number.isFinite(best.rankKey) &&
|
|
265
|
+
best.rankKey > 0 &&
|
|
266
|
+
Math.abs(secondBest.rankKey - best.rankKey) / best.rankKey <= 0.01;
|
|
267
|
+
const budget = checkBudget(best, req, config, false);
|
|
268
|
+
if (!budget.ok) {
|
|
269
|
+
return {
|
|
270
|
+
chosen: null,
|
|
271
|
+
consideredCount: ranked.length,
|
|
272
|
+
rejected: [...rejected, { candidate: best.candidate, reason: budget.reason }],
|
|
273
|
+
reroutes: 0,
|
|
274
|
+
error: "BudgetExceeded",
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
return toDecisionFromRanked(best, ranked.length, rejected, nearTie);
|
|
278
|
+
}
|
|
279
|
+
export function selectCheapestPerTier(req, env, config, excluded) {
|
|
280
|
+
const { ranked, rejected } = buildRankedPool(req, env, config, excluded);
|
|
281
|
+
const decidedTiers = new Set();
|
|
282
|
+
const decisions = [];
|
|
283
|
+
for (const candidate of ranked) {
|
|
284
|
+
const tier = candidate.tier;
|
|
285
|
+
if (tier === undefined)
|
|
286
|
+
continue;
|
|
287
|
+
if (decidedTiers.has(tier))
|
|
288
|
+
continue;
|
|
289
|
+
decidedTiers.add(tier);
|
|
290
|
+
const budget = checkBudget(candidate, req, config, false);
|
|
291
|
+
if (!budget.ok)
|
|
292
|
+
continue;
|
|
293
|
+
decisions.push(toDecisionFromRanked(candidate, ranked.length, rejected, false));
|
|
294
|
+
}
|
|
295
|
+
return decisions.sort((a, b) => TIER_ORDER[a.tier] - TIER_ORDER[b.tier]);
|
|
296
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type AccountingMode = "inclusive" | "disjoint";
|
|
2
|
+
export type CostBasis = "provider-reported" | "derived-from-tokens" | "pre-flight-estimate";
|
|
3
|
+
export type Confidence = "high" | "medium" | "low";
|
|
4
|
+
export type QualityTier = "economy" | "standard" | "frontier";
|
|
5
|
+
export type PriceSource = "table" | "api-catalog" | "unknown";
|
|
6
|
+
export interface ModelCost {
|
|
7
|
+
inputUsdPerMTok: number;
|
|
8
|
+
outputUsdPerMTok: number;
|
|
9
|
+
cacheReadMultiplier: number;
|
|
10
|
+
cacheWriteUsdPerMTok: number;
|
|
11
|
+
accountingMode: AccountingMode;
|
|
12
|
+
family: string;
|
|
13
|
+
source: PriceSource;
|
|
14
|
+
asOf: string;
|
|
15
|
+
}
|
|
16
|
+
export interface TokenCounts {
|
|
17
|
+
inputTokens: number;
|
|
18
|
+
outputTokens: number;
|
|
19
|
+
cacheReadTokens?: number;
|
|
20
|
+
cacheCreationTokens?: number;
|
|
21
|
+
reasoningTokens?: number;
|
|
22
|
+
reportedCostUsd?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface TokenEstimate {
|
|
25
|
+
estInputTokens: number;
|
|
26
|
+
estOutputTokens: number;
|
|
27
|
+
estCacheReadTokens?: number;
|
|
28
|
+
estCacheWriteTokens?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface CostResult {
|
|
31
|
+
costUsd: number;
|
|
32
|
+
cost_basis: CostBasis;
|
|
33
|
+
confidence: Confidence;
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/migrate-sessions.js
CHANGED
|
@@ -22,7 +22,7 @@ export async function migrateFromFile(filePath, pgManager) {
|
|
|
22
22
|
fileData = JSON.parse(fileContent);
|
|
23
23
|
}
|
|
24
24
|
catch (error) {
|
|
25
|
-
throw new Error(`Failed to read sessions file: ${error instanceof Error ? error.message : String(error)}
|
|
25
|
+
throw new Error(`Failed to read sessions file: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
26
26
|
}
|
|
27
27
|
console.error(`Found ${Object.keys(fileData.sessions).length} sessions to migrate`);
|
|
28
28
|
for (const [id, session] of Object.entries(fileData.sessions)) {
|
package/dist/migrate.js
CHANGED
|
@@ -86,7 +86,7 @@ async function importOptionalPg() {
|
|
|
86
86
|
}
|
|
87
87
|
catch (error) {
|
|
88
88
|
if (error?.code === "ERR_MODULE_NOT_FOUND" || error?.code === "MODULE_NOT_FOUND") {
|
|
89
|
-
throw new Error("PostgreSQL migrations require optional peer dependency 'pg'. Install it alongside llm-cli-gateway before running migrate.");
|
|
89
|
+
throw new Error("PostgreSQL migrations require optional peer dependency 'pg'. Install it alongside llm-cli-gateway before running migrate.", { cause: error });
|
|
90
90
|
}
|
|
91
91
|
throw error;
|
|
92
92
|
}
|
package/dist/model-registry.js
CHANGED
|
@@ -192,7 +192,7 @@ function addWarning(info, warning) {
|
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
function isValidModelName(model) {
|
|
195
|
-
return model.length > 0 && !/[\
|
|
195
|
+
return model.length > 0 && !/[\s\p{Cc}]/u.test(model);
|
|
196
196
|
}
|
|
197
197
|
function addModel(info, model, description, metadata, options = {}) {
|
|
198
198
|
const normalized = model.trim();
|