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/quota.js ADDED
@@ -0,0 +1,149 @@
1
+ import { mkdirSync, readFileSync, writeFileSync, appendFileSync, statSync, renameSync, unlinkSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ const ERRORS_MAX_BYTES = 1_000_000;
5
+ /** Allowance / subscription exhaustion — not context-length or bare 429 rate limits. */
6
+ export function isQuotaError(err) {
7
+ if (err == null)
8
+ return false;
9
+ if (typeof err === "number")
10
+ return false;
11
+ if (typeof err === "string") {
12
+ return /quota|allowance|usage[\s_-]?limit/i.test(err);
13
+ }
14
+ if (typeof err === "object") {
15
+ const o = err;
16
+ const data = o.data;
17
+ const message = String(o.message ?? data?.message ?? o.name ?? "");
18
+ return /quota|allowance|usage[\s_-]?limit/i.test(message);
19
+ }
20
+ return false;
21
+ }
22
+ function safeErrorSummary(err) {
23
+ if (err == null)
24
+ return { message: "null" };
25
+ if (typeof err !== "object") {
26
+ return { message: String(err).slice(0, 240) };
27
+ }
28
+ const o = err;
29
+ const data = o.data;
30
+ return {
31
+ status: o.status ?? o.statusCode,
32
+ name: typeof o.name === "string" ? o.name : undefined,
33
+ message: String(o.message ?? data?.message ?? "").slice(0, 240),
34
+ };
35
+ }
36
+ export function defaultStateDir() {
37
+ const xdg = process.env.XDG_STATE_HOME;
38
+ const root = xdg && xdg.length > 0 ? xdg : join(homedir(), ".local", "state");
39
+ return join(root, "opencode-jev-router");
40
+ }
41
+ export class QuotaStore {
42
+ state = {};
43
+ path;
44
+ errorsPath;
45
+ now;
46
+ constructor(opts) {
47
+ const dir = opts?.dir ?? defaultStateDir();
48
+ this.path = join(dir, "quota.json");
49
+ this.errorsPath = join(dir, "errors.jsonl");
50
+ this.now = opts?.now ?? Date.now;
51
+ this.load();
52
+ }
53
+ load() {
54
+ try {
55
+ const raw = readFileSync(this.path, "utf8");
56
+ const parsed = JSON.parse(raw);
57
+ if (parsed && typeof parsed === "object")
58
+ this.state = parsed;
59
+ }
60
+ catch (err) {
61
+ const code = err.code;
62
+ if (code !== "ENOENT") {
63
+ this.state = {};
64
+ }
65
+ }
66
+ }
67
+ persist() {
68
+ try {
69
+ mkdirSync(dirname(this.path), { recursive: true });
70
+ const tmp = `${this.path}.${process.pid}.tmp`;
71
+ writeFileSync(tmp, JSON.stringify(this.state, null, 2) + "\n");
72
+ renameSync(tmp, this.path);
73
+ }
74
+ catch {
75
+ try {
76
+ unlinkSync(`${this.path}.${process.pid}.tmp`);
77
+ }
78
+ catch {
79
+ // ignore
80
+ }
81
+ // fail open
82
+ }
83
+ }
84
+ isExhausted(ref, now = this.now()) {
85
+ const entry = this.state[ref];
86
+ if (!entry)
87
+ return false;
88
+ if (!Number.isFinite(entry.exhaustedUntil) || entry.exhaustedUntil <= now) {
89
+ delete this.state[ref];
90
+ this.persist();
91
+ return false;
92
+ }
93
+ return true;
94
+ }
95
+ mark(ref, untilMs, reason, now = this.now()) {
96
+ if (!Number.isFinite(untilMs))
97
+ return;
98
+ this.state[ref] = {
99
+ exhaustedUntil: untilMs,
100
+ reason,
101
+ markedAt: now,
102
+ };
103
+ this.persist();
104
+ }
105
+ clear(ref) {
106
+ if (!ref) {
107
+ this.state = {};
108
+ }
109
+ else {
110
+ delete this.state[ref];
111
+ }
112
+ this.persist();
113
+ }
114
+ list(now = this.now()) {
115
+ const out = [];
116
+ for (const [ref, entry] of Object.entries(this.state)) {
117
+ if (!entry || typeof entry !== "object")
118
+ continue;
119
+ if (Number.isFinite(entry.exhaustedUntil) && entry.exhaustedUntil > now) {
120
+ out.push({ ref, ...entry });
121
+ }
122
+ }
123
+ return out.sort((a, b) => a.exhaustedUntil - b.exhaustedUntil);
124
+ }
125
+ appendError(record) {
126
+ try {
127
+ mkdirSync(dirname(this.errorsPath), { recursive: true });
128
+ try {
129
+ if (statSync(this.errorsPath).size > ERRORS_MAX_BYTES) {
130
+ writeFileSync(this.errorsPath, "");
131
+ }
132
+ }
133
+ catch (err) {
134
+ if (err.code !== "ENOENT")
135
+ throw err;
136
+ }
137
+ const { error, ...rest } = record;
138
+ appendFileSync(this.errorsPath, JSON.stringify({
139
+ t: this.now(),
140
+ ...rest,
141
+ error: safeErrorSummary(error),
142
+ }) + "\n");
143
+ }
144
+ catch {
145
+ // optional
146
+ }
147
+ }
148
+ }
149
+ //# sourceMappingURL=quota.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quota.js","sourceRoot":"","sources":["../src/quota.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * DeepSeek peak/off-peak on OpenCode Go.
3
+ * @see https://opencode.ai/docs/go/#usage-limits
4
+ *
5
+ * Peak (≈2× token $): Mon–Fri UTC 01:00–04:00 and 06:00–10:00.
6
+ * Else off-peak, including weekends. Monthly $ cap is unchanged.
7
+ */
8
+ export declare function isDeepSeekScheduledModel(ref: string): boolean;
9
+ /** Half-open UTC hour ranges; weekends always off-peak. */
10
+ export declare function isDeepSeekPeak(now?: Date): boolean;
11
+ export type SchedulePeriod = "peak" | "off-peak";
12
+ export declare function deepSeekPeriod(now?: Date): SchedulePeriod;
13
+ /** During peak, put non-DeepSeek candidates first. */
14
+ export declare function orderCandidatesForSchedule(candidates: string[], now?: Date): string[];
15
+ export declare function describeDeepSeekPeakWindows(): string;
16
+ //# sourceMappingURL=schedule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedule.d.ts","sourceRoot":"","sources":["../src/schedule.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAQ7D;AAED,2DAA2D;AAC3D,wBAAgB,cAAc,CAAC,GAAG,GAAE,IAAiB,GAAG,OAAO,CAK9D;AAED,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,UAAU,CAAC;AAEjD,wBAAgB,cAAc,CAAC,GAAG,GAAE,IAAiB,GAAG,cAAc,CAErE;AAED,sDAAsD;AACtD,wBAAgB,0BAA0B,CACxC,UAAU,EAAE,MAAM,EAAE,EACpB,GAAG,GAAE,IAAiB,GACrB,MAAM,EAAE,CAQV;AAED,wBAAgB,2BAA2B,IAAI,MAAM,CAEpD"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * DeepSeek peak/off-peak on OpenCode Go.
3
+ * @see https://opencode.ai/docs/go/#usage-limits
4
+ *
5
+ * Peak (≈2× token $): Mon–Fri UTC 01:00–04:00 and 06:00–10:00.
6
+ * Else off-peak, including weekends. Monthly $ cap is unchanged.
7
+ */
8
+ const PEAK_WINDOWS = [
9
+ { startHour: 1, endHour: 4 },
10
+ { startHour: 6, endHour: 10 },
11
+ ];
12
+ export function isDeepSeekScheduledModel(ref) {
13
+ const id = ref.includes("/") ? ref.slice(ref.indexOf("/") + 1) : ref;
14
+ return (id === "deepseek-v4.1-flash" ||
15
+ id === "deepseek-v4-pro" ||
16
+ id === "deepseek-v4-flash" ||
17
+ id === "deepseek-v4-flash-vision-exp");
18
+ }
19
+ /** Half-open UTC hour ranges; weekends always off-peak. */
20
+ export function isDeepSeekPeak(now = new Date()) {
21
+ const day = now.getUTCDay();
22
+ if (day === 0 || day === 6)
23
+ return false;
24
+ const hour = now.getUTCHours();
25
+ return PEAK_WINDOWS.some((w) => hour >= w.startHour && hour < w.endHour);
26
+ }
27
+ export function deepSeekPeriod(now = new Date()) {
28
+ return isDeepSeekPeak(now) ? "peak" : "off-peak";
29
+ }
30
+ /** During peak, put non-DeepSeek candidates first. */
31
+ export function orderCandidatesForSchedule(candidates, now = new Date()) {
32
+ if (!isDeepSeekPeak(now))
33
+ return [...candidates];
34
+ const preferred = [];
35
+ const deferred = [];
36
+ for (const ref of candidates) {
37
+ (isDeepSeekScheduledModel(ref) ? deferred : preferred).push(ref);
38
+ }
39
+ return [...preferred, ...deferred];
40
+ }
41
+ export function describeDeepSeekPeakWindows() {
42
+ return "Mon–Fri UTC 01:00–04:00 and 06:00–10:00 (else off-peak, incl. weekends)";
43
+ }
44
+ //# sourceMappingURL=schedule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedule.js","sourceRoot":"","sources":["../src/schedule.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,YAAY,GAAG;IACnB,EAAE,SAAS,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE;IAC5B,EAAE,SAAS,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE;CACrB,CAAC;AAEX,MAAM,UAAU,wBAAwB,CAAC,GAAW;IAClD,MAAM,EAAE,GAAG,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IACrE,OAAO,CACL,EAAE,KAAK,qBAAqB;QAC5B,EAAE,KAAK,iBAAiB;QACxB,EAAE,KAAK,mBAAmB;QAC1B,EAAE,KAAK,8BAA8B,CACtC,CAAC;AACJ,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,cAAc,CAAC,MAAY,IAAI,IAAI,EAAE;IACnD,MAAM,GAAG,GAAG,GAAG,CAAC,SAAS,EAAE,CAAC;IAC5B,IAAI,GAAG,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACzC,MAAM,IAAI,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;IAC/B,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,CAAC,SAAS,IAAI,IAAI,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC;AAC3E,CAAC;AAID,MAAM,UAAU,cAAc,CAAC,MAAY,IAAI,IAAI,EAAE;IACnD,OAAO,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC;AACnD,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,0BAA0B,CACxC,UAAoB,EACpB,MAAY,IAAI,IAAI,EAAE;IAEtB,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC;QAAE,OAAO,CAAC,GAAG,UAAU,CAAC,CAAC;IACjD,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,CAAC,wBAAwB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACnE,CAAC;IACD,OAAO,CAAC,GAAG,SAAS,EAAE,GAAG,QAAQ,CAAC,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,2BAA2B;IACzC,OAAO,yEAAyE,CAAC;AACnF,CAAC"}
@@ -0,0 +1,37 @@
1
+ import type { DecisionRecord, ModelRef, Tier } from "./types.js";
2
+ /**
3
+ * Per-session routing state. Kept in-process; OpenCode reloads plugins rarely.
4
+ */
5
+ export declare class SessionStore {
6
+ private readonly modes;
7
+ private readonly internal;
8
+ private readonly lastServed;
9
+ private readonly lastTier;
10
+ private readonly contextTokens;
11
+ private readonly pinToast;
12
+ private readonly history;
13
+ private readonly maxEntries;
14
+ private readonly retainPrompt;
15
+ private readonly historyEnabled;
16
+ constructor(opts: {
17
+ maxEntries: number;
18
+ retainPrompt: boolean;
19
+ historyEnabled: boolean;
20
+ });
21
+ isAutomatic(sessionID: string, defaultEnabled: boolean): boolean;
22
+ setAutomatic(sessionID: string, enabled: boolean): void;
23
+ markInternal(sessionID: string): void;
24
+ isInternal(sessionID: string): boolean;
25
+ setLastServed(sessionID: string, model: ModelRef): void;
26
+ getLastServed(sessionID: string): ModelRef | undefined;
27
+ setLastTier(sessionID: string, tier: Tier): void;
28
+ getLastTier(sessionID: string): Tier | undefined;
29
+ setContextTokens(sessionID: string, tokens: number): void;
30
+ getContextTokens(sessionID: string): number;
31
+ shouldToastPin(sessionID: string): boolean;
32
+ forget(sessionID: string): void;
33
+ record(entry: DecisionRecord): void;
34
+ last(sessionID?: string): DecisionRecord | undefined;
35
+ explain(sessionID?: string): string;
36
+ }
37
+ //# sourceMappingURL=sessions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAGjE;;GAEG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA8B;IACpD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;IAC9C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA+B;IAC1D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA2B;IACpD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA6B;IAC3D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;IAC9C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAU;IACvC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAU;gBAE7B,IAAI,EAAE;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,OAAO,CAAC;QACtB,cAAc,EAAE,OAAO,CAAC;KACzB;IAMD,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,GAAG,OAAO;IAMhE,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IAIvD,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAIrC,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAItC,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,IAAI;IAIvD,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS;IAItD,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI;IAIhD,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS;IAIhD,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAIzD,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAI3C,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAM1C,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAS/B,MAAM,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI;IASnC,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS;IAQpD,OAAO,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM;CAyCpC"}
@@ -0,0 +1,119 @@
1
+ import { formatModelRef } from "./models.js";
2
+ /**
3
+ * Per-session routing state. Kept in-process; OpenCode reloads plugins rarely.
4
+ */
5
+ export class SessionStore {
6
+ modes = new Map();
7
+ internal = new Set();
8
+ lastServed = new Map();
9
+ lastTier = new Map();
10
+ contextTokens = new Map();
11
+ pinToast = new Set();
12
+ history = [];
13
+ maxEntries;
14
+ retainPrompt;
15
+ historyEnabled;
16
+ constructor(opts) {
17
+ this.maxEntries = opts.maxEntries;
18
+ this.retainPrompt = opts.retainPrompt;
19
+ this.historyEnabled = opts.historyEnabled;
20
+ }
21
+ isAutomatic(sessionID, defaultEnabled) {
22
+ return this.modes.has(sessionID)
23
+ ? this.modes.get(sessionID)
24
+ : defaultEnabled;
25
+ }
26
+ setAutomatic(sessionID, enabled) {
27
+ this.modes.set(sessionID, enabled);
28
+ }
29
+ markInternal(sessionID) {
30
+ this.internal.add(sessionID);
31
+ }
32
+ isInternal(sessionID) {
33
+ return this.internal.has(sessionID);
34
+ }
35
+ setLastServed(sessionID, model) {
36
+ this.lastServed.set(sessionID, model);
37
+ }
38
+ getLastServed(sessionID) {
39
+ return this.lastServed.get(sessionID);
40
+ }
41
+ setLastTier(sessionID, tier) {
42
+ this.lastTier.set(sessionID, tier);
43
+ }
44
+ getLastTier(sessionID) {
45
+ return this.lastTier.get(sessionID);
46
+ }
47
+ setContextTokens(sessionID, tokens) {
48
+ this.contextTokens.set(sessionID, Math.max(0, tokens));
49
+ }
50
+ getContextTokens(sessionID) {
51
+ return this.contextTokens.get(sessionID) ?? 0;
52
+ }
53
+ shouldToastPin(sessionID) {
54
+ if (this.pinToast.has(sessionID))
55
+ return false;
56
+ this.pinToast.add(sessionID);
57
+ return true;
58
+ }
59
+ forget(sessionID) {
60
+ this.modes.delete(sessionID);
61
+ this.internal.delete(sessionID);
62
+ this.lastServed.delete(sessionID);
63
+ this.lastTier.delete(sessionID);
64
+ this.contextTokens.delete(sessionID);
65
+ this.pinToast.delete(sessionID);
66
+ }
67
+ record(entry) {
68
+ if (!this.historyEnabled)
69
+ return;
70
+ const stored = this.retainPrompt
71
+ ? entry
72
+ : { ...entry, prompt: undefined };
73
+ this.history.push(stored);
74
+ while (this.history.length > this.maxEntries)
75
+ this.history.shift();
76
+ }
77
+ last(sessionID) {
78
+ if (!sessionID)
79
+ return this.history[this.history.length - 1];
80
+ for (let i = this.history.length - 1; i >= 0; i -= 1) {
81
+ if (this.history[i]?.sessionID === sessionID)
82
+ return this.history[i];
83
+ }
84
+ return undefined;
85
+ }
86
+ explain(sessionID) {
87
+ const last = this.last(sessionID);
88
+ if (!last)
89
+ return "No routing decisions recorded yet.";
90
+ const lines = [
91
+ "Jev Router",
92
+ "",
93
+ `Current tier: ${last.currentTier}`,
94
+ `Recommended tier: ${last.decision.tier}`,
95
+ `Selected model: ${last.model ?? "(unchanged)"}`,
96
+ `Used fallback: ${last.usedFallback ? "yes" : "no"}`,
97
+ `Confidence: ${last.jev ? `${Math.round(last.jev.confidence * 100)}%` : "n/a"}`,
98
+ `Decision: ${last.decision.reason}`,
99
+ ];
100
+ const served = sessionID ? this.getLastServed(sessionID) : undefined;
101
+ if (served) {
102
+ lines.push(`Last served: ${formatModelRef(served)}`);
103
+ }
104
+ if (sessionID) {
105
+ const tier = this.getLastTier(sessionID);
106
+ if (tier)
107
+ lines.push(`Last routed tier: ${tier}`);
108
+ lines.push(`Context tokens: ${this.getContextTokens(sessionID)}`);
109
+ }
110
+ if (last.metrics) {
111
+ lines.push("", `Task complexity: ${last.metrics.taskComplexity.toFixed(2)}`, `Reasoning required: ${last.metrics.reasoningRequired.toFixed(2)}`, `Tool complexity: ${last.metrics.toolComplexity.toFixed(2)}`, `Context size: ${last.metrics.contextSize.toFixed(2)}`);
112
+ }
113
+ if (typeof last.latencyMs === "number") {
114
+ lines.push("", `Latency: ${last.latencyMs} ms`);
115
+ }
116
+ return lines.join("\n");
117
+ }
118
+ }
119
+ //# sourceMappingURL=sessions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.js","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,82 @@
1
+ /** Abstract capability tiers, cheapest first. */
2
+ export declare const TIER_NAMES: readonly ["fast", "balanced", "strong", "long"];
3
+ export type Tier = (typeof TIER_NAMES)[number];
4
+ export type ModelRef = {
5
+ providerID: string;
6
+ modelID: string;
7
+ variant?: string;
8
+ };
9
+ export type TierConfig = {
10
+ model: string;
11
+ variant?: string;
12
+ enabled?: boolean;
13
+ /** Alternate models when primary is invalid or quota-exhausted. */
14
+ fallbacks?: string[];
15
+ /** Prompt override aliases, e.g. ["luna"] for strong. */
16
+ aliases?: string[];
17
+ };
18
+ export type RoutingConfig = {
19
+ failOpen: boolean;
20
+ maxPromptBytes: number;
21
+ timeoutMs: number;
22
+ deadlineMs: number;
23
+ maxRetries: number;
24
+ minimumConfidence: number;
25
+ uncertainCeiling: Tier;
26
+ downgradeMaxContextTokens: number;
27
+ };
28
+ export type QuotaConfig = {
29
+ /** Hours to skip a model after a detected quota error (default 5). */
30
+ cooldownHours: number;
31
+ };
32
+ export type HistoryConfig = {
33
+ enabled: boolean;
34
+ retainPrompt: boolean;
35
+ maxEntries: number;
36
+ };
37
+ export type RouterConfig = {
38
+ enabled: boolean;
39
+ /**
40
+ * When true (global config only), project-level files may override tier
41
+ * models/fallbacks. Default false so a cloned repo cannot remap prompts.
42
+ */
43
+ allowProjectModels: boolean;
44
+ tiers: Record<Tier, TierConfig>;
45
+ routing: RoutingConfig;
46
+ quota: QuotaConfig;
47
+ history: HistoryConfig;
48
+ echoRouting: boolean;
49
+ };
50
+ export type JevAnswer = {
51
+ choice: string;
52
+ confidence: number;
53
+ };
54
+ export type JevResult = JevAnswer & {
55
+ metrics: {
56
+ taskComplexity: number;
57
+ reasoningRequired: number;
58
+ toolComplexity: number;
59
+ contextSize: number;
60
+ };
61
+ ms: number;
62
+ };
63
+ export type PolicyDecision = {
64
+ tier: Tier;
65
+ reason: string;
66
+ changed: boolean;
67
+ };
68
+ export type DecisionRecord = {
69
+ at: number;
70
+ sessionID: string;
71
+ prompt?: string;
72
+ currentTier: Tier;
73
+ jev: JevAnswer | null;
74
+ metrics?: JevResult["metrics"];
75
+ decision: PolicyDecision;
76
+ model?: string;
77
+ usedFallback?: boolean;
78
+ latencyMs?: number;
79
+ };
80
+ export declare function isTier(value: string): value is Tier;
81
+ export declare function rankOf(name: string): number;
82
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,9 @@
1
+ /** Abstract capability tiers, cheapest first. */
2
+ export const TIER_NAMES = ["fast", "balanced", "strong", "long"];
3
+ export function isTier(value) {
4
+ return TIER_NAMES.includes(value);
5
+ }
6
+ export function rankOf(name) {
7
+ return TIER_NAMES.indexOf(name);
8
+ }
9
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,iDAAiD;AACjD,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,CAAU,CAAC;AA0F1E,MAAM,UAAU,MAAM,CAAC,KAAa;IAClC,OAAQ,UAAgC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC3D,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,IAAY;IACjC,OAAQ,UAAgC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;AACzD,CAAC"}
@@ -0,0 +1,58 @@
1
+ {
2
+ "$schema": "https://github.com/aaronshaf/opencode-jev-model-router/schemas/config.schema.json",
3
+ "enabled": true,
4
+ "allowProjectModels": false,
5
+ "tiers": {
6
+ "fast": {
7
+ "model": "opencode-go/muse-spark-1.3-contributor",
8
+ "aliases": ["fast", "flash", "muse"],
9
+ "fallbacks": [
10
+ "opencode-go/mimo-v2.5",
11
+ "opencode-go/deepseek-v4.1-flash",
12
+ "opencode-go/glm-5.3-flash"
13
+ ]
14
+ },
15
+ "balanced": {
16
+ "model": "opencode-go/deepseek-v4.1-flash",
17
+ "aliases": ["balanced"],
18
+ "fallbacks": [
19
+ "opencode-go/muse-spark-1.3-contributor",
20
+ "opencode-go/mimo-v2.5",
21
+ "opencode-go/qwen3.7-plus"
22
+ ]
23
+ },
24
+ "strong": {
25
+ "model": "opencode-go/gpt-5.6-luna",
26
+ "aliases": ["strong", "luna"],
27
+ "fallbacks": [
28
+ "opencode-go/kimi-k2.7-code",
29
+ "opencode-go/qwen3.7-plus"
30
+ ]
31
+ },
32
+ "long": {
33
+ "model": "opencode-go/kimi-k3",
34
+ "enabled": false,
35
+ "aliases": ["long", "kimi", "k3"],
36
+ "fallbacks": ["opencode-go/kimi-k2.7-code"]
37
+ }
38
+ },
39
+ "routing": {
40
+ "failOpen": true,
41
+ "maxPromptBytes": 16384,
42
+ "timeoutMs": 1500,
43
+ "deadlineMs": 3000,
44
+ "maxRetries": 1,
45
+ "minimumConfidence": 0.3,
46
+ "uncertainCeiling": "balanced",
47
+ "downgradeMaxContextTokens": 20000
48
+ },
49
+ "quota": {
50
+ "cooldownHours": 5
51
+ },
52
+ "history": {
53
+ "enabled": true,
54
+ "retainPrompt": false,
55
+ "maxEntries": 20
56
+ },
57
+ "echoRouting": true
58
+ }
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "opencode-jev-model-router",
3
+ "version": "0.1.0",
4
+ "description": "Jev-based automatic per-turn model routing for OpenCode",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/aaronshaf/opencode-jev-model-router.git"
10
+ },
11
+ "homepage": "https://github.com/aaronshaf/opencode-jev-model-router#readme",
12
+ "bugs": "https://github.com/aaronshaf/opencode-jev-model-router/issues",
13
+ "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "schemas",
24
+ "opencode-jev-router.example.json",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "engines": {
29
+ "node": ">=22"
30
+ },
31
+ "scripts": {
32
+ "build": "tsc -p tsconfig.json",
33
+ "typecheck": "tsc -p tsconfig.json --noEmit",
34
+ "test": "npm run build && node --test test/*.test.mjs",
35
+ "check": "npm run typecheck && npm test",
36
+ "prepublishOnly": "npm run check"
37
+ },
38
+ "keywords": [
39
+ "opencode",
40
+ "jev",
41
+ "typesafe",
42
+ "model-routing",
43
+ "plugin"
44
+ ],
45
+ "dependencies": {
46
+ "@typesafe-ai/sdk": "^0.6.0"
47
+ },
48
+ "devDependencies": {
49
+ "@opencode-ai/plugin": "1.18.31",
50
+ "@types/node": "^26.5.1",
51
+ "typescript": "~5.9.3"
52
+ },
53
+ "peerDependencies": {
54
+ "@opencode-ai/plugin": ">=1.0.0"
55
+ },
56
+ "peerDependenciesMeta": {
57
+ "@opencode-ai/plugin": {
58
+ "optional": true
59
+ }
60
+ }
61
+ }