opencode-jev-model-router 0.1.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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +104 -0
  3. package/dist/adapter.d.ts +23 -0
  4. package/dist/adapter.d.ts.map +1 -0
  5. package/dist/adapter.js +509 -0
  6. package/dist/adapter.js.map +1 -0
  7. package/dist/config.d.ts +46 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +398 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/index.d.ts +7 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +36 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/jev.d.ts +45 -0
  16. package/dist/jev.d.ts.map +1 -0
  17. package/dist/jev.js +135 -0
  18. package/dist/jev.js.map +1 -0
  19. package/dist/models.d.ts +47 -0
  20. package/dist/models.d.ts.map +1 -0
  21. package/dist/models.js +132 -0
  22. package/dist/models.js.map +1 -0
  23. package/dist/policy.d.ts +33 -0
  24. package/dist/policy.d.ts.map +1 -0
  25. package/dist/policy.js +100 -0
  26. package/dist/policy.js.map +1 -0
  27. package/dist/prompt.d.ts +12 -0
  28. package/dist/prompt.d.ts.map +1 -0
  29. package/dist/prompt.js +32 -0
  30. package/dist/prompt.js.map +1 -0
  31. package/dist/quota.d.ts +31 -0
  32. package/dist/quota.d.ts.map +1 -0
  33. package/dist/quota.js +149 -0
  34. package/dist/quota.js.map +1 -0
  35. package/dist/schedule.d.ts +16 -0
  36. package/dist/schedule.d.ts.map +1 -0
  37. package/dist/schedule.js +44 -0
  38. package/dist/schedule.js.map +1 -0
  39. package/dist/sessions.d.ts +37 -0
  40. package/dist/sessions.d.ts.map +1 -0
  41. package/dist/sessions.js +119 -0
  42. package/dist/sessions.js.map +1 -0
  43. package/dist/types.d.ts +82 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +9 -0
  46. package/dist/types.js.map +1 -0
  47. package/opencode-jev-router.example.json +58 -0
  48. package/package.json +61 -0
  49. package/schemas/config.schema.json +83 -0
package/dist/jev.js ADDED
@@ -0,0 +1,135 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { TypeSafeClient } from "@typesafe-ai/sdk";
5
+ import { COMPLEXITY_MAX_SCORE, CONTEXT_WINDOW_TOKENS, QUESTIONS, } from "./config.js";
6
+ let client;
7
+ let cachedKey;
8
+ /** Env vars first, then ~/.config/opencode key files (OpenCode often lacks shell exports). */
9
+ export function resolveApiKey(env = process.env, opts = {}) {
10
+ const fromEnv = env.JEV_API_KEY || env.JEV_KEY || env.TYPESAFE_API_KEY;
11
+ if (fromEnv?.trim())
12
+ return fromEnv.trim();
13
+ if (opts.skipFiles)
14
+ return undefined;
15
+ if (cachedKey && !opts.configDir)
16
+ return cachedKey;
17
+ const configDir = opts.configDir ?? join(homedir(), ".config", "opencode");
18
+ const keyFile = join(configDir, "opencode-jev-router.key");
19
+ if (existsSync(keyFile)) {
20
+ try {
21
+ const raw = readFileSync(keyFile, "utf8").trim();
22
+ if (raw && !raw.includes("\n")) {
23
+ if (!opts.configDir)
24
+ cachedKey = raw;
25
+ return raw;
26
+ }
27
+ }
28
+ catch {
29
+ // ignore
30
+ }
31
+ }
32
+ const envFile = join(configDir, ".env");
33
+ if (existsSync(envFile)) {
34
+ try {
35
+ for (const line of readFileSync(envFile, "utf8").split("\n")) {
36
+ const m = /^(?:JEV_API_KEY|JEV_KEY|TYPESAFE_API_KEY)\s*=\s*(.*)$/.exec(line.trim());
37
+ if (!m)
38
+ continue;
39
+ let value = m[1].trim();
40
+ if ((value.startsWith('"') && value.endsWith('"')) ||
41
+ (value.startsWith("'") && value.endsWith("'"))) {
42
+ value = value.slice(1, -1);
43
+ }
44
+ if (value) {
45
+ if (!opts.configDir)
46
+ cachedKey = value;
47
+ return value;
48
+ }
49
+ }
50
+ }
51
+ catch {
52
+ // ignore
53
+ }
54
+ }
55
+ return undefined;
56
+ }
57
+ /** Tests only. */
58
+ export function resetJevClient() {
59
+ client = undefined;
60
+ cachedKey = undefined;
61
+ }
62
+ function getClient(config, apiKey) {
63
+ client ??= new TypeSafeClient({
64
+ apiKey,
65
+ timeout: config.timeoutMs,
66
+ retry: {
67
+ maxRetries: config.maxRetries,
68
+ backoffInitialMs: 150,
69
+ backoffMaxMs: 400,
70
+ },
71
+ logLevel: "warn",
72
+ });
73
+ return client;
74
+ }
75
+ /**
76
+ * Ask Jev which tier fits this prompt. Returns null on any failure — never
77
+ * blocks the user turn.
78
+ */
79
+ export async function askJev(input, deps = {}) {
80
+ const { prompt, current, contextTokens, available, routing } = input;
81
+ const started = Date.now();
82
+ const abort = new AbortController();
83
+ const deadline = setTimeout(() => abort.abort(), routing.deadlineMs);
84
+ const request = {
85
+ state: {
86
+ request: prompt,
87
+ session: { current_model: current, context_tokens: contextTokens },
88
+ environment: { available_models: available },
89
+ },
90
+ questions: QUESTIONS,
91
+ };
92
+ const fail = (message) => {
93
+ deps.onError?.(message);
94
+ return null;
95
+ };
96
+ try {
97
+ const apiKey = (deps.resolveKey ?? (() => resolveApiKey()))();
98
+ if (!apiKey) {
99
+ return fail("Jev API key missing (set JEV_KEY or ~/.config/opencode/opencode-jev-router.key)");
100
+ }
101
+ const systemOne = deps.systemOne ??
102
+ ((req, options) => getClient(routing, apiKey).systemOne(req, options));
103
+ const result = await systemOne(request, { signal: abort.signal });
104
+ const answers = result.answers;
105
+ const answer = answers.model_tier;
106
+ const task_complexity = answers.task_complexity;
107
+ const reasoning_required = answers.reasoning_required;
108
+ const tool_complexity = answers.tool_complexity;
109
+ if (!answer ||
110
+ typeof answer.choice !== "string" ||
111
+ typeof answer.confidence !== "number" ||
112
+ !Number.isFinite(answer.confidence)) {
113
+ return fail("Jev returned a malformed model_tier answer");
114
+ }
115
+ return {
116
+ choice: answer.choice,
117
+ confidence: answer.confidence,
118
+ metrics: {
119
+ taskComplexity: (task_complexity?.score ?? 0) / COMPLEXITY_MAX_SCORE,
120
+ reasoningRequired: (reasoning_required?.score ?? 0) / COMPLEXITY_MAX_SCORE,
121
+ toolComplexity: (tool_complexity?.score ?? 0) / COMPLEXITY_MAX_SCORE,
122
+ contextSize: Math.min(contextTokens / CONTEXT_WINDOW_TOKENS, 1),
123
+ },
124
+ ms: Date.now() - started,
125
+ };
126
+ }
127
+ catch (err) {
128
+ const message = err instanceof Error ? err.message : String(err);
129
+ return fail(`Jev call failed: ${message}`);
130
+ }
131
+ finally {
132
+ clearTimeout(deadline);
133
+ }
134
+ }
135
+ //# sourceMappingURL=jev.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jev.js","sourceRoot":"","sources":["../src/jev.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ import type { ModelRef, RouterConfig, Tier } from "./types.js";
2
+ import type { QuotaStore } from "./quota.js";
3
+ /**
4
+ * Parse an OpenCode model ref. Split only on the first slash so OpenRouter-style
5
+ * IDs like `openrouter/deepseek/deepseek-v3.2` keep the rest intact.
6
+ */
7
+ export declare function parseModelRef(ref: string): ModelRef | undefined;
8
+ export declare function formatModelRef(model: ModelRef): string;
9
+ export type KnownModels = Set<string> | undefined;
10
+ /** All configured model refs for a tier (primary + fallbacks). */
11
+ export declare function tierCandidates(config: RouterConfig, tier: Tier): string[];
12
+ /** Every model the router manages (for pin detection). */
13
+ export declare function managedModelSet(config: RouterConfig): Set<string>;
14
+ export declare function isManagedModel(config: RouterConfig, model: {
15
+ providerID: string;
16
+ modelID: string;
17
+ } | undefined): boolean;
18
+ export type SelectOptions = {
19
+ known?: KnownModels;
20
+ quota?: QuotaStore;
21
+ now?: number;
22
+ /** Wall clock for peak/off-peak reordering (defaults to new Date(now)). */
23
+ at?: Date;
24
+ };
25
+ /**
26
+ * Resolve a tier to the first eligible concrete model (primary then fallbacks).
27
+ * During DeepSeek peak hours, non-DeepSeek candidates are tried first.
28
+ */
29
+ export declare function modelForTier(config: RouterConfig, tier: Tier, opts?: SelectOptions): {
30
+ model: ModelRef;
31
+ usedFallback: boolean;
32
+ } | undefined;
33
+ /** Infer which configured tier a model belongs to (primary preferred over fallback). */
34
+ export declare function tierOfModel(config: RouterConfig, model: {
35
+ providerID: string;
36
+ modelID: string;
37
+ } | undefined, fallback?: Tier): Tier;
38
+ /** Tiers that are enabled and have a parseable primary (ignores quota). */
39
+ export declare function configAvailableTiers(config: RouterConfig, known?: KnownModels): Tier[];
40
+ /** Tiers that currently have at least one eligible (non-exhausted) model. */
41
+ export declare function resolveAvailableTiers(config: RouterConfig, opts?: SelectOptions): Tier[];
42
+ /** True when `model` is still a non-exhausted, known candidate for `tier`. */
43
+ export declare function isEligibleCandidate(config: RouterConfig, tier: Tier, model: {
44
+ providerID: string;
45
+ modelID: string;
46
+ }, opts?: SelectOptions): boolean;
47
+ //# sourceMappingURL=models.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAE/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAO/D;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAEtD;AAED,MAAM,MAAM,WAAW,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC;AAMlD,kEAAkE;AAClE,wBAAgB,cAAc,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,CASzE;AAED,0DAA0D;AAC1D,wBAAgB,eAAe,CAAC,MAAM,EAAE,YAAY,GAAG,GAAG,CAAC,MAAM,CAAC,CAMjE;AAED,wBAAgB,cAAc,CAC5B,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,GACzD,OAAO,CAGT;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,EAAE,CAAC,EAAE,IAAI,CAAC;CACX,CAAC;AAEF;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,YAAY,EACpB,IAAI,EAAE,IAAI,EACV,IAAI,GAAE,aAAkB,GACvB;IAAE,KAAK,EAAE,QAAQ,CAAC;IAAC,YAAY,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,CAoBxD;AAED,wFAAwF;AACxF,wBAAgB,WAAW,CACzB,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,EAC1D,QAAQ,GAAE,IAAiB,GAC1B,IAAI,CAkBN;AAED,2EAA2E;AAC3E,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,YAAY,EACpB,KAAK,CAAC,EAAE,WAAW,GAClB,IAAI,EAAE,CAYR;AAED,6EAA6E;AAC7E,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,YAAY,EACpB,IAAI,GAAE,aAAkB,GACvB,IAAI,EAAE,CAIR;AAED,8EAA8E;AAC9E,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,YAAY,EACpB,IAAI,EAAE,IAAI,EACV,KAAK,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EAC9C,IAAI,GAAE,aAAkB,GACvB,OAAO,CAMT"}
package/dist/models.js ADDED
@@ -0,0 +1,132 @@
1
+ import { TIER_NAMES } from "./types.js";
2
+ import { orderCandidatesForSchedule } from "./schedule.js";
3
+ /**
4
+ * Parse an OpenCode model ref. Split only on the first slash so OpenRouter-style
5
+ * IDs like `openrouter/deepseek/deepseek-v3.2` keep the rest intact.
6
+ */
7
+ export function parseModelRef(ref) {
8
+ const slash = ref.indexOf("/");
9
+ if (slash <= 0 || slash === ref.length - 1)
10
+ return undefined;
11
+ return {
12
+ providerID: ref.slice(0, slash),
13
+ modelID: ref.slice(slash + 1),
14
+ };
15
+ }
16
+ export function formatModelRef(model) {
17
+ return `${model.providerID}/${model.modelID}`;
18
+ }
19
+ function isKnown(ref, known) {
20
+ return !known || known.has(ref);
21
+ }
22
+ /** All configured model refs for a tier (primary + fallbacks). */
23
+ export function tierCandidates(config, tier) {
24
+ const tierConfig = config.tiers[tier];
25
+ if (!tierConfig || tierConfig.enabled === false)
26
+ return [];
27
+ const out = [];
28
+ if (tierConfig.model)
29
+ out.push(tierConfig.model);
30
+ for (const fb of tierConfig.fallbacks ?? []) {
31
+ if (fb && !out.includes(fb))
32
+ out.push(fb);
33
+ }
34
+ return out;
35
+ }
36
+ /** Every model the router manages (for pin detection). */
37
+ export function managedModelSet(config) {
38
+ const set = new Set();
39
+ for (const name of TIER_NAMES) {
40
+ for (const ref of tierCandidates(config, name))
41
+ set.add(ref);
42
+ }
43
+ return set;
44
+ }
45
+ export function isManagedModel(config, model) {
46
+ if (!model)
47
+ return false;
48
+ return managedModelSet(config).has(formatModelRef(model));
49
+ }
50
+ /**
51
+ * Resolve a tier to the first eligible concrete model (primary then fallbacks).
52
+ * During DeepSeek peak hours, non-DeepSeek candidates are tried first.
53
+ */
54
+ export function modelForTier(config, tier, opts = {}) {
55
+ const tierConfig = config.tiers[tier];
56
+ if (!tierConfig || tierConfig.enabled === false)
57
+ return undefined;
58
+ const at = opts.at ??
59
+ (typeof opts.now === "number" ? new Date(opts.now) : new Date());
60
+ const candidates = orderCandidatesForSchedule(tierCandidates(config, tier), at);
61
+ const primary = tierConfig.model;
62
+ for (let i = 0; i < candidates.length; i += 1) {
63
+ const raw = candidates[i];
64
+ const parsed = parseModelRef(raw);
65
+ if (!parsed)
66
+ continue;
67
+ if (!isKnown(raw, opts.known))
68
+ continue;
69
+ if (opts.quota?.isExhausted(raw, opts.now))
70
+ continue;
71
+ const model = tierConfig.variant
72
+ ? { ...parsed, variant: tierConfig.variant }
73
+ : parsed;
74
+ return { model, usedFallback: raw !== primary };
75
+ }
76
+ return undefined;
77
+ }
78
+ /** Infer which configured tier a model belongs to (primary preferred over fallback). */
79
+ export function tierOfModel(config, model, fallback = "balanced") {
80
+ if (!model)
81
+ return fallback;
82
+ const ref = formatModelRef(model);
83
+ for (const name of TIER_NAMES) {
84
+ if (config.tiers[name]?.model === ref)
85
+ return name;
86
+ }
87
+ for (const name of TIER_NAMES) {
88
+ const fallbacks = config.tiers[name]?.fallbacks ?? [];
89
+ if (fallbacks.includes(ref))
90
+ return name;
91
+ }
92
+ for (const name of TIER_NAMES) {
93
+ for (const candidate of tierCandidates(config, name)) {
94
+ const parsed = parseModelRef(candidate);
95
+ if (parsed && parsed.modelID === model.modelID)
96
+ return name;
97
+ }
98
+ }
99
+ return fallback;
100
+ }
101
+ /** Tiers that are enabled and have a parseable primary (ignores quota). */
102
+ export function configAvailableTiers(config, known) {
103
+ return TIER_NAMES.filter((name) => {
104
+ const tier = config.tiers[name];
105
+ if (!tier || tier.enabled === false)
106
+ return false;
107
+ const primary = parseModelRef(tier.model);
108
+ if (!primary)
109
+ return false;
110
+ if (known && !known.has(tier.model)) {
111
+ // Primary unknown — still config-available if a fallback is known
112
+ return (tier.fallbacks ?? []).some((fb) => known.has(fb));
113
+ }
114
+ return true;
115
+ });
116
+ }
117
+ /** Tiers that currently have at least one eligible (non-exhausted) model. */
118
+ export function resolveAvailableTiers(config, opts = {}) {
119
+ return TIER_NAMES.filter((name) => modelForTier(config, name, opts) !== undefined);
120
+ }
121
+ /** True when `model` is still a non-exhausted, known candidate for `tier`. */
122
+ export function isEligibleCandidate(config, tier, model, opts = {}) {
123
+ const ref = formatModelRef(model);
124
+ if (!tierCandidates(config, tier).includes(ref))
125
+ return false;
126
+ if (!isKnown(ref, opts.known))
127
+ return false;
128
+ if (opts.quota?.isExhausted(ref, opts.now))
129
+ return false;
130
+ return true;
131
+ }
132
+ //# sourceMappingURL=models.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"models.js","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import type { JevAnswer, PolicyDecision, RouterConfig, Tier } from "./types.js";
2
+ export type OverridePattern = {
3
+ tier: Tier;
4
+ re: RegExp;
5
+ };
6
+ /** Build override matchers from enabled tiers' aliases. */
7
+ export declare function buildOverridePatterns(tiers: RouterConfig["tiers"]): OverridePattern[];
8
+ /** The tier the user named explicitly in the prompt, or null. */
9
+ export declare function detectOverride(prompt: string | undefined | null, patterns: OverridePattern[]): Tier | null;
10
+ /**
11
+ * Nearest runnable tier.
12
+ * - If the tier is config-available but not eligible (quota), step **down**.
13
+ * - If the tier is not config-available, step **up** (never into `long` unless asked).
14
+ */
15
+ export declare function clampToAvailable(tier: Tier, eligible: Tier[], configAvailable: Tier[]): Tier | null;
16
+ export type DecideInput = {
17
+ prompt: string;
18
+ jev: JevAnswer | null;
19
+ current: Tier;
20
+ /** Tiers with at least one non-exhausted model. */
21
+ available: Tier[];
22
+ /** Tiers enabled/valid ignoring quota. */
23
+ configAvailable: Tier[];
24
+ contextTokens?: number;
25
+ overridePatterns: OverridePattern[];
26
+ thresholds: Pick<RouterConfig["routing"], "minimumConfidence" | "uncertainCeiling" | "downgradeMaxContextTokens">;
27
+ };
28
+ /**
29
+ * Turns a Jev answer into the tier we will actually run. Pure and total: any
30
+ * missing, malformed, or unavailable input falls back to the model already in use.
31
+ */
32
+ export declare function decide(input: DecideInput): PolicyDecision;
33
+ //# sourceMappingURL=policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EACT,cAAc,EACd,YAAY,EACZ,IAAI,EACL,MAAM,YAAY,CAAC;AAGpB,MAAM,MAAM,eAAe,GAAG;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzD,2DAA2D;AAC3D,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,GAC3B,eAAe,EAAE,CAoBnB;AAMD,iEAAiE;AACjE,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EACjC,QAAQ,EAAE,eAAe,EAAE,GAC1B,IAAI,GAAG,IAAI,CAGb;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,IAAI,EAAE,EAChB,eAAe,EAAE,IAAI,EAAE,GACtB,IAAI,GAAG,IAAI,CAeb;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,SAAS,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE,IAAI,CAAC;IACd,mDAAmD;IACnD,SAAS,EAAE,IAAI,EAAE,CAAC;IAClB,0CAA0C;IAC1C,eAAe,EAAE,IAAI,EAAE,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,eAAe,EAAE,CAAC;IACpC,UAAU,EAAE,IAAI,CACd,YAAY,CAAC,SAAS,CAAC,EACvB,mBAAmB,GAAG,kBAAkB,GAAG,2BAA2B,CACvE,CAAC;CACH,CAAC;AAEF;;;GAGG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,cAAc,CAiEzD"}
package/dist/policy.js ADDED
@@ -0,0 +1,100 @@
1
+ import { TIER_NAMES, isTier, rankOf } from "./types.js";
2
+ /** Build override matchers from enabled tiers' aliases. */
3
+ export function buildOverridePatterns(tiers) {
4
+ const patterns = [];
5
+ for (const name of TIER_NAMES) {
6
+ if (tiers[name]?.enabled === false)
7
+ continue;
8
+ const aliases = tiers[name]?.aliases ?? [name];
9
+ const unique = [...new Set(aliases.map((a) => a.trim()).filter(Boolean))];
10
+ if (!unique.length)
11
+ continue;
12
+ const alt = unique.map(escapeRegExp).join("|");
13
+ // "use|switch to" only — avoid "focus on long" / "with fast refresh".
14
+ // Require end, punctuation, or "to/for …" after the alias so
15
+ // "use strong typing" and "use fast-glob" do not match.
16
+ patterns.push({
17
+ tier: name,
18
+ re: new RegExp(`\\b(?:use|switch to)\\s+(?:${alt})(?![\\w-])(?:\\s+model)?(?:\\s+please)?(?=\\s+to\\b|\\s+for\\b|[.,;:!?]|\\s*$)`, "i"),
19
+ });
20
+ }
21
+ return patterns;
22
+ }
23
+ function escapeRegExp(value) {
24
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
25
+ }
26
+ /** The tier the user named explicitly in the prompt, or null. */
27
+ export function detectOverride(prompt, patterns) {
28
+ const hit = patterns.find((p) => p.re.test(prompt ?? ""));
29
+ return hit ? hit.tier : null;
30
+ }
31
+ /**
32
+ * Nearest runnable tier.
33
+ * - If the tier is config-available but not eligible (quota), step **down**.
34
+ * - If the tier is not config-available, step **up** (never into `long` unless asked).
35
+ */
36
+ export function clampToAvailable(tier, eligible, configAvailable) {
37
+ if (eligible.includes(tier))
38
+ return tier;
39
+ const rank = rankOf(tier);
40
+ if (configAvailable.includes(tier)) {
41
+ const down = TIER_NAMES.filter((t, i) => i < rank && eligible.includes(t));
42
+ return down.length ? down[down.length - 1] : null;
43
+ }
44
+ const up = TIER_NAMES.filter((t, i) => i > rank && eligible.includes(t) && (t !== "long" || tier === "long"));
45
+ if (up.length)
46
+ return up[0];
47
+ const down = TIER_NAMES.filter((t, i) => i < rank && eligible.includes(t));
48
+ return down.length ? down[down.length - 1] : null;
49
+ }
50
+ /**
51
+ * Turns a Jev answer into the tier we will actually run. Pure and total: any
52
+ * missing, malformed, or unavailable input falls back to the model already in use.
53
+ */
54
+ export function decide(input) {
55
+ const { prompt, jev, current, available, configAvailable, contextTokens = 0, overridePatterns, thresholds, } = input;
56
+ const settle = (tier, reason) => {
57
+ const final = clampToAvailable(tier, available, configAvailable) ?? current;
58
+ let why = reason;
59
+ if (final !== tier) {
60
+ why = configAvailable.includes(tier)
61
+ ? `${reason}+quota-down`
62
+ : `${reason}+unavailable`;
63
+ }
64
+ if (final === current && available.length === 0) {
65
+ why = "quota-exhausted/no-change";
66
+ }
67
+ else if (final === current) {
68
+ why = `${why}/no-change`;
69
+ }
70
+ return {
71
+ tier: final,
72
+ reason: why,
73
+ changed: final !== current,
74
+ };
75
+ };
76
+ const override = detectOverride(prompt, overridePatterns);
77
+ if (override)
78
+ return settle(override, "override");
79
+ if (!available.length) {
80
+ return { tier: current, reason: "quota-exhausted/no-change", changed: false };
81
+ }
82
+ if (!jev || !isTier(jev.choice))
83
+ return settle(current, "jev-unavailable");
84
+ let target = jev.choice;
85
+ if (jev.confidence < thresholds.minimumConfidence) {
86
+ if (rankOf(target) < rankOf(current)) {
87
+ return settle(current, "low-confidence-no-downgrade");
88
+ }
89
+ const ceiling = Math.max(rankOf(current), rankOf(thresholds.uncertainCeiling));
90
+ if (rankOf(target) > ceiling) {
91
+ return settle(TIER_NAMES[ceiling], "low-confidence-capped");
92
+ }
93
+ }
94
+ if (rankOf(target) < rankOf(current) &&
95
+ contextTokens > thresholds.downgradeMaxContextTokens) {
96
+ return settle(current, "downgrade-not-worth-cache-rebuild");
97
+ }
98
+ return settle(target, "jev");
99
+ }
100
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.js","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ export type TextPartLike = {
2
+ type?: string;
3
+ text?: string;
4
+ synthetic?: boolean;
5
+ ignored?: boolean;
6
+ };
7
+ /**
8
+ * Extract bounded non-synthetic user text for routing.
9
+ * Returns empty string when there is nothing usable or the payload is too large.
10
+ */
11
+ export declare function extractPromptText(parts: TextPartLike[] | undefined, maxBytes: number): string;
12
+ //# sourceMappingURL=prompt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,YAAY,EAAE,GAAG,SAAS,EACjC,QAAQ,EAAE,MAAM,GACf,MAAM,CA6BR"}
package/dist/prompt.js ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Extract bounded non-synthetic user text for routing.
3
+ * Returns empty string when there is nothing usable or the payload is too large.
4
+ */
5
+ export function extractPromptText(parts, maxBytes) {
6
+ const text = (parts || [])
7
+ .filter((part) => part &&
8
+ part.type === "text" &&
9
+ part.synthetic !== true &&
10
+ part.ignored !== true &&
11
+ typeof part.text === "string")
12
+ .map((part) => (part.text ?? "").trim())
13
+ .filter(Boolean)
14
+ .join("\n");
15
+ if (!text)
16
+ return "";
17
+ const bytes = new TextEncoder().encode(text);
18
+ if (bytes.byteLength <= maxBytes)
19
+ return text;
20
+ // Cut on a UTF-8 code-point boundary (never emit U+FFFD).
21
+ let end = Math.min(maxBytes, bytes.byteLength);
22
+ while (end > 0 && (bytes[end - 1] & 0xc0) === 0x80)
23
+ end -= 1;
24
+ if (end > 0) {
25
+ const lead = bytes[end - 1];
26
+ const need = lead >= 0xf0 ? 4 : lead >= 0xe0 ? 3 : lead >= 0xc0 ? 2 : 1;
27
+ if (need > 1 && end - 1 + need > maxBytes)
28
+ end -= 1;
29
+ }
30
+ return new TextDecoder().decode(bytes.slice(0, end));
31
+ }
32
+ //# sourceMappingURL=prompt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.js","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":"AAOA;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAC/B,KAAiC,EACjC,QAAgB;IAEhB,MAAM,IAAI,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC;SACvB,MAAM,CACL,CAAC,IAAI,EAAE,EAAE,CACP,IAAI;QACJ,IAAI,CAAC,IAAI,KAAK,MAAM;QACpB,IAAI,CAAC,SAAS,KAAK,IAAI;QACvB,IAAI,CAAC,OAAO,KAAK,IAAI;QACrB,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,CAChC;SACA,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;SACvC,MAAM,CAAC,OAAO,CAAC;SACf,IAAI,CAAC,IAAI,CAAC,CAAC;IAEd,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IAErB,MAAM,KAAK,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC7C,IAAI,KAAK,CAAC,UAAU,IAAI,QAAQ;QAAE,OAAO,IAAI,CAAC;IAE9C,0DAA0D;IAC1D,IAAI,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;IAC/C,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAE,GAAG,IAAI,CAAC,KAAK,IAAI;QAAE,GAAG,IAAI,CAAC,CAAC;IAC9D,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;QACZ,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,GAAG,CAAC,CAAE,CAAC;QAC7B,MAAM,IAAI,GACR,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7D,IAAI,IAAI,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,GAAG,IAAI,GAAG,QAAQ;YAAE,GAAG,IAAI,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;AACvD,CAAC"}
@@ -0,0 +1,31 @@
1
+ export type ExhaustionEntry = {
2
+ exhaustedUntil: number;
3
+ reason: string;
4
+ markedAt: number;
5
+ };
6
+ export type QuotaState = {
7
+ [modelRef: string]: ExhaustionEntry;
8
+ };
9
+ /** Allowance / subscription exhaustion — not context-length or bare 429 rate limits. */
10
+ export declare function isQuotaError(err: unknown): boolean;
11
+ export declare function defaultStateDir(): string;
12
+ export declare class QuotaStore {
13
+ private state;
14
+ private readonly path;
15
+ private readonly errorsPath;
16
+ private readonly now;
17
+ constructor(opts?: {
18
+ dir?: string;
19
+ now?: () => number;
20
+ });
21
+ private load;
22
+ private persist;
23
+ isExhausted(ref: string, now?: number): boolean;
24
+ mark(ref: string, untilMs: number, reason: string, now?: number): void;
25
+ clear(ref?: string): void;
26
+ list(now?: number): Array<{
27
+ ref: string;
28
+ } & ExhaustionEntry>;
29
+ appendError(record: Record<string, unknown>): void;
30
+ }
31
+ //# sourceMappingURL=quota.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quota.d.ts","sourceRoot":"","sources":["../src/quota.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,eAAe,GAAG;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,CAAC;CACrC,CAAC;AAIF,wFAAwF;AACxF,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAalD;AAgBD,wBAAgB,eAAe,IAAI,MAAM,CAIxC;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,KAAK,CAAkB;IAC/B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAEvB,IAAI,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;KAAE;IAQvD,OAAO,CAAC,IAAI;IAaZ,OAAO,CAAC,OAAO;IAgBf,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,SAAa,GAAG,OAAO;IAWnD,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,SAAa,GAAG,IAAI;IAU1E,KAAK,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI;IASzB,IAAI,CAAC,GAAG,SAAa,GAAG,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,eAAe,CAAC;IAWhE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;CAuBnD"}