@andrej7510/ai-router 1.0.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 (4) hide show
  1. package/README.md +105 -0
  2. package/ai.js +287 -0
  3. package/claude-load +1 -0
  4. package/package.json +24 -0
package/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # ai-router
2
+
3
+ CLI orchestrator that automatically routes tasks to **OpenAI Codex** or **Anthropic Claude** based on task complexity — and shifts more work to Codex when Claude's context window is running high.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g ai-router
9
+ ```
10
+
11
+ Requires `codex` and `claude` CLIs to be installed and authenticated:
12
+
13
+ ```bash
14
+ npm install -g @openai/codex
15
+ npm install -g @anthropic-ai/claude-code
16
+ ```
17
+
18
+ ## How it works
19
+
20
+ Every task is scored 0–100 using keyword and length heuristics. Routing happens in two layers:
21
+
22
+ **1. Intent detection (takes priority)**
23
+
24
+ The task is matched against intent patterns first. Each intent maps to the most appropriate Claude model:
25
+
26
+ | Intent | Trigger keywords | Model |
27
+ |--------|-----------------|-------|
28
+ | `plan` | design, architect, brainstorm, strategy, roadmap, scaffold | `claude-opus-4-5` |
29
+ | `security` | security, encrypt, auth, vulnerability, threat, audit, pentest | `claude-opus-4-5` |
30
+ | `debug` | why, broken, not working, crash, investigate, root cause | `claude-sonnet-4-6` |
31
+ | `explain` | explain, how does, analyse, understand, overview, summarize | `claude-sonnet-4-6` |
32
+ | `edit` | add, fix, rename, change, update, remove, refactor, extract | `claude-haiku-4-5` |
33
+ | `test` / `format` | unit test, lint, typo, format, mock, coverage | `claude-haiku-4-5` |
34
+
35
+ **2. Score-based fallback (no intent detected)**
36
+
37
+ | Score | Model |
38
+ |-------|-------|
39
+ | ≥ 80 | `claude-opus-4-5` |
40
+ | 60–79 | `claude-sonnet-4-6` |
41
+ | < 60 | `claude-haiku-4-5` |
42
+
43
+ Tasks that score below the routing threshold go to **Codex** regardless of intent.
44
+
45
+ **3. Load-shedding**
46
+
47
+ The routing threshold shifts automatically when Claude's context usage is high:
48
+
49
+ | Claude load | Threshold | Effect |
50
+ |-------------|-----------|--------|
51
+ | < 80% | 40 | Normal routing |
52
+ | ≥ 80% | 60 | Codex handles medium-complexity tasks |
53
+ | ≥ 90% | 80 | Almost everything goes to Codex |
54
+
55
+ ## Usage
56
+
57
+ ```bash
58
+ # Auto-route based on complexity
59
+ ai "add a null check to the confirm handler" # → Codex
60
+ ai "why is the serial handshake timing out" # → Claude
61
+ ai "brainstorm an encryption strategy" # → Claude
62
+
63
+ # Force a specific tool
64
+ ai --codex "write a unit test for deriveDigest"
65
+ ai --claude "design the auth flow"
66
+
67
+ # Preview routing without running
68
+ ai --dry "your task here"
69
+
70
+ # Set Claude context usage (0–100)
71
+ ai --set-load 85
72
+
73
+ # Show current load + threshold
74
+ ai --status
75
+ ```
76
+
77
+ ## Load management
78
+
79
+ When your Claude session is approaching its context limit, tell the router:
80
+
81
+ ```bash
82
+ ai --set-load 85 # Claude at 85% → threshold raises to 60
83
+ ai --set-load 92 # Claude at 92% → threshold raises to 80
84
+ ai --set-load 0 # Back to normal
85
+ ```
86
+
87
+ The load value is stored in `~/.ai-router/claude-load` and persists across sessions.
88
+
89
+ ## Routing examples
90
+
91
+ | Task | Score | Intent | Model | At 85% | At 92% |
92
+ |------|-------|--------|-------|--------|--------|
93
+ | `rename function` | 10 | edit | — | Codex | Codex |
94
+ | `add a null check` | 20 | edit | — | Codex | Codex |
95
+ | `explain the escrow flow` | 42 | explain | `sonnet-4-6` | Codex | Codex |
96
+ | `debug serial timeout` | 74 | debug | `sonnet-4-6` | `sonnet-4-6` | Codex |
97
+ | `design an encryption strategy` | 86 | security | `opus-4-5` | `opus-4-5` | `opus-4-5` |
98
+ | `architect the payment flow` | 78 | plan | `opus-4-5` | `opus-4-5` | Codex |
99
+ | `why is the handshake failing` | 62 | debug | `sonnet-4-6` | `sonnet-4-6` | Codex |
100
+
101
+ ## Future improvements
102
+
103
+ - Per-project `.ai-router.json` config (custom patterns + thresholds)
104
+ - Auto-detect Claude context usage via API
105
+ - Support additional AI CLI tools
package/ai.js ADDED
@@ -0,0 +1,287 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ai — Orchestrator that routes tasks to Codex or Claude based on complexity
4
+ * and task intent, picking the cheapest Claude model that can do the job.
5
+ *
6
+ * Intent → model mapping:
7
+ * plan / strategy / architecture → PLAN model (most powerful)
8
+ * security / crypto / audit → SECURITY model
9
+ * debug / investigate → DEBUG model
10
+ * explain / analyse → EXPLAIN model
11
+ * edit / refactor / simple fix → EDIT model
12
+ * test / format / typo → EDIT model (cheapest)
13
+ * (no intent detected, fallback) → score-based (haiku / sonnet / opus)
14
+ *
15
+ * Load-shedding thresholds:
16
+ * Claude < 80% → threshold 40 (normal)
17
+ * Claude ≥ 80% → threshold 60 (Codex picks up medium tasks)
18
+ * Claude ≥ 90% → threshold 80 (almost everything → Codex)
19
+ *
20
+ * Usage:
21
+ * ai "your task" → auto-route
22
+ * ai --codex "..." → force Codex
23
+ * ai --claude "..." → force Claude
24
+ * ai --dry "..." → show routing decision only
25
+ * ai --set-load 85 → tell router Claude is at 85% context usage
26
+ * ai --status → show current load + model table
27
+ */
28
+
29
+ // ── Model map — update IDs here when new versions drop ───────────────────────
30
+ const MODELS = {
31
+ plan: "claude-opus-4-5", // planning, architecture, strategy
32
+ security: "claude-opus-4-5", // security, crypto, audits — high stakes
33
+ debug: "claude-sonnet-4-6", // debugging, investigation
34
+ explain: "claude-sonnet-4-6", // explanations, analysis
35
+ edit: "claude-haiku-4-5", // simple edits, tests, formatting
36
+ // score-based fallbacks (when no intent detected)
37
+ high: "claude-opus-4-5", // score ≥ 80
38
+ mid: "claude-sonnet-4-6", // score 60–79
39
+ low: "claude-haiku-4-5", // score < 60 (but above threshold)
40
+ };
41
+
42
+ import { execFileSync, execSync } from "child_process";
43
+ import { readFileSync, writeFileSync, existsSync } from "fs";
44
+ import { join } from "path";
45
+ import { homedir } from "os";
46
+
47
+ const LOAD_FILE = join(homedir(), ".ai-router", "claude-load");
48
+
49
+ // ── Claude load helpers ───────────────────────────────────────────────────────
50
+
51
+ function readLoad() {
52
+ if (!existsSync(LOAD_FILE)) return 0;
53
+ const val = parseInt(readFileSync(LOAD_FILE, "utf8").trim(), 10);
54
+ return isNaN(val) ? 0 : Math.max(0, Math.min(100, val));
55
+ }
56
+
57
+ function writeLoad(pct) {
58
+ writeFileSync(LOAD_FILE, String(pct), "utf8");
59
+ }
60
+
61
+ function thresholdForLoad(load) {
62
+ if (load >= 90) return 80; // near full — only very complex tasks reach Claude
63
+ if (load >= 80) return 60; // high load — Codex handles medium tasks too
64
+ return 40; // normal
65
+ }
66
+
67
+ function loadLabel(load) {
68
+ if (load >= 90) return "CRITICAL (≥90%)";
69
+ if (load >= 80) return "HIGH (≥80%)";
70
+ return "NORMAL";
71
+ }
72
+
73
+ // ── Intent detector ───────────────────────────────────────────────────────────
74
+
75
+ const INTENT_PATTERNS = {
76
+ plan: /\b(plan|design|architect|brainstorm|strategy|roadmap|outline|structure|organiz|scaffold)\b/i,
77
+ security: /\b(security|encrypt|decrypt|auth(?:entication|oriz)?|vulnerabilit|threat|attack|ecdh|tls|aes|csrf|xss|injection|audit|pentest|harden)\b/i,
78
+ debug: /\b(debug|why (is|does|isn.t|doesn.t)|broken|not work(?:ing)?|doesn.t work|failing|crash(?:ing)?|error|investigate|trace|root cause|symptom)\b/i,
79
+ explain: /\b(explain|how does|what (is|does|are)|understand|analyse|analyze|overview|summarize|describe|clarify)\b/i,
80
+ edit: /\b(add|fix|rename|change|update|remove|delete|insert|set|replace|convert|refactor|extract|move|split|merge|cleanup|simplify)\b/i,
81
+ test: /\b(unit test|test for|write (a )?test|spec|coverage|mock|stub|format|lint|typo|comment|docstring)\b/i,
82
+ };
83
+
84
+ /**
85
+ * Detect the primary intent of a task string.
86
+ * Returns the first matching intent key, or null if none match.
87
+ * Priority order: plan > security > debug > explain > edit > test
88
+ */
89
+ function detectIntent(task) {
90
+ for (const [intent, pattern] of Object.entries(INTENT_PATTERNS)) {
91
+ if (pattern.test(task)) return intent;
92
+ }
93
+ return null;
94
+ }
95
+
96
+ /**
97
+ * Pick the Claude model for a task.
98
+ * Intent takes priority; score-based fallback when intent is ambiguous.
99
+ */
100
+ function claudeModel(task, s) {
101
+ const intent = detectIntent(task);
102
+ if (intent === "plan") return { model: MODELS.plan, intent };
103
+ if (intent === "security") return { model: MODELS.security, intent };
104
+ if (intent === "debug") return { model: MODELS.debug, intent };
105
+ if (intent === "explain") return { model: MODELS.explain, intent };
106
+ if (intent === "edit") return { model: MODELS.edit, intent };
107
+ if (intent === "test") return { model: MODELS.edit, intent }; // haiku is fine
108
+ // fallback: score bands
109
+ const model = s >= 80 ? MODELS.high : s >= 60 ? MODELS.mid : MODELS.low;
110
+ return { model, intent: null };
111
+ }
112
+
113
+ // ── Complexity scorer ─────────────────────────────────────────────────────────
114
+
115
+ const COMPLEX_PATTERNS = [
116
+ // Intent: understand, plan, analyse
117
+ /\b(why|how does|explain|understand|analyse|analyze|brainstorm|design|architect|plan|strategy)\b/i,
118
+ // Scope: whole system, many files
119
+ /\b(refactor|rewrite|migrate|reorgani[sz]e|across|system|all files?)\b/i,
120
+ // Debugging unknown issues
121
+ /\b(debug|broken|not work|doesn.t work|failing|crash|error|issue|investigate)\b/i,
122
+ // Security / crypto
123
+ /\b(security|encrypt|decrypt|auth|vulnerabilit|threat|attack|ecdh|tls|aes)\b/i,
124
+ // Open-ended / soft
125
+ /\b(best (way|practice)|should (i|we)|what (if|about)|trade.?off|compare|vs\.?|pros? and cons?)\b/i,
126
+ // Multi-step
127
+ /\b(first .* then|step[- ]by[- ]step|and (also|then)|multiple)\b/i,
128
+ ];
129
+
130
+ const SIMPLE_PATTERNS = [
131
+ // Narrow scope
132
+ /\b(add|fix|rename|change|update|remove|delete|insert|set|replace|convert)\b/i,
133
+ // File-local
134
+ /\b(function|variable|parameter|field|method|class|import|type|interface|const|let|var)\b/i,
135
+ // Tests / formatting
136
+ /\b(unit test|test for|format|lint|typo|comment|docstring|log (line|statement))\b/i,
137
+ // Short task marker
138
+ /^.{0,60}$/,
139
+ ];
140
+
141
+ function score(task) {
142
+ let s = 50;
143
+ for (const p of COMPLEX_PATTERNS) if (p.test(task)) s += 12;
144
+ for (const p of SIMPLE_PATTERNS) if (p.test(task)) s -= 10;
145
+ if (task.length > 200) s += 15;
146
+ if (task.length > 100) s += 8;
147
+ if (task.length < 60) s -= 10;
148
+ return Math.max(0, Math.min(100, s));
149
+ }
150
+
151
+ // ── CLI arg parsing ───────────────────────────────────────────────────────────
152
+
153
+ const args = process.argv.slice(2);
154
+
155
+ let forceCodex = false;
156
+ let forceClaude = false;
157
+ let dryRun = false;
158
+ let setLoad = null;
159
+ let showStatus = false;
160
+ const taskParts = [];
161
+
162
+ for (let i = 0; i < args.length; i++) {
163
+ const arg = args[i];
164
+ if (arg === "--codex") { forceCodex = true; continue; }
165
+ if (arg === "--claude") { forceClaude = true; continue; }
166
+ if (arg === "--dry") { dryRun = true; continue; }
167
+ if (arg === "--status") { showStatus = true; continue; }
168
+ if (arg === "--set-load") { setLoad = parseInt(args[++i], 10); continue; }
169
+ taskParts.push(arg);
170
+ }
171
+
172
+ // ── --set-load ────────────────────────────────────────────────────────────────
173
+
174
+ if (setLoad !== null) {
175
+ if (isNaN(setLoad) || setLoad < 0 || setLoad > 100) {
176
+ console.error("Usage: ai --set-load <0-100>");
177
+ process.exit(1);
178
+ }
179
+ writeLoad(setLoad);
180
+ const threshold = thresholdForLoad(setLoad);
181
+ console.log(`\n┌─ AI Router — Claude load updated ────────────────`);
182
+ console.log(`│ Claude usage: ${setLoad}% [${loadLabel(setLoad)}]`);
183
+ console.log(`│ Threshold: ${threshold} (score < ${threshold} → Codex)`);
184
+ console.log(`└──────────────────────────────────────────────────\n`);
185
+ process.exit(0);
186
+ }
187
+
188
+ // ── --status ──────────────────────────────────────────────────────────────────
189
+
190
+ if (showStatus) {
191
+ const load = readLoad();
192
+ const threshold = thresholdForLoad(load);
193
+ console.log(`\n┌─ AI Router — Status ──────────────────────────────`);
194
+ console.log(`│ Claude usage: ${load}% [${loadLabel(load)}]`);
195
+ console.log(`│ Threshold: ${threshold} (score < ${threshold} → Codex)`);
196
+ console.log(`│`);
197
+ console.log(`│ Intent-based routing (takes priority over score):`);
198
+ console.log(`│ plan → ${MODELS.plan}`);
199
+ console.log(`│ security → ${MODELS.security}`);
200
+ console.log(`│ debug → ${MODELS.debug}`);
201
+ console.log(`│ explain → ${MODELS.explain}`);
202
+ console.log(`│ edit → ${MODELS.edit}`);
203
+ console.log(`│ test → ${MODELS.edit}`);
204
+ console.log(`│`);
205
+ console.log(`│ Score fallback (no intent detected):`);
206
+ console.log(`│ score ≥ 80 → ${MODELS.high}`);
207
+ console.log(`│ score 60–79 → ${MODELS.mid}`);
208
+ console.log(`│ score < 60 → ${MODELS.low}`);
209
+ console.log(`│`);
210
+ console.log(`│ Update with: ai --set-load <0-100>`);
211
+ console.log(`└──────────────────────────────────────────────────\n`);
212
+ process.exit(0);
213
+ }
214
+
215
+ // ── Require a task ────────────────────────────────────────────────────────────
216
+
217
+ const task = taskParts.join(" ").trim();
218
+
219
+ if (!task) {
220
+ console.error(`
221
+ ai — AI task router (Codex vs Claude)
222
+
223
+ Usage:
224
+ ai "your task here"
225
+ ai --codex "force codex"
226
+ ai --claude "force claude"
227
+ ai --dry "show routing only, don't run"
228
+ ai --set-load 85 set Claude context usage (0-100)
229
+ ai --status show current load + threshold
230
+ `);
231
+ process.exit(1);
232
+ }
233
+
234
+ // ── Route ─────────────────────────────────────────────────────────────────────
235
+
236
+ const load = readLoad();
237
+ const THRESHOLD = thresholdForLoad(load);
238
+ const loadNote = load >= 80 ? ` ⚡ Claude at ${load}% — threshold lowered to ${THRESHOLD}` : "";
239
+
240
+ let tool;
241
+ let reason;
242
+ let model = null; // only set when routing to Claude
243
+ let intent = null;
244
+
245
+ const s = score(task);
246
+
247
+ if (forceCodex) {
248
+ tool = "codex";
249
+ reason = "forced via --codex";
250
+ } else if (forceClaude) {
251
+ tool = "claude";
252
+ ({ model, intent } = claudeModel(task, s));
253
+ reason = `forced via --claude`;
254
+ } else {
255
+ if (s >= THRESHOLD) {
256
+ tool = "claude";
257
+ ({ model, intent } = claudeModel(task, s));
258
+ reason = `score ${s}/100 ≥ ${THRESHOLD}`;
259
+ } else {
260
+ tool = "codex";
261
+ reason = `score ${s}/100 < ${THRESHOLD}`;
262
+ }
263
+ }
264
+
265
+ const intentLabel = intent ? ` [${intent}]` : "";
266
+ console.log(`\n┌─ AI Router ──────────────────────────────────────`);
267
+ console.log(`│ Task: ${task.slice(0, 72)}${task.length > 72 ? "…" : ""}`);
268
+ console.log(`│ Route: ${tool.toUpperCase()}${model ? ` → ${model}` : ""}${intentLabel} (${reason})`);
269
+ if (loadNote) console.log(`│ ${loadNote}`);
270
+ console.log(`└──────────────────────────────────────────────────\n`);
271
+
272
+ if (dryRun) process.exit(0);
273
+
274
+ // ── Execute ───────────────────────────────────────────────────────────────────
275
+
276
+ const cmd = tool === "codex" ? "codex" : "claude";
277
+ const argv = tool === "codex"
278
+ ? ["exec", "--sandbox", "workspace-write", task]
279
+ : ["--model", model, task];
280
+
281
+ try {
282
+ execFileSync(cmd, argv, { stdio: "inherit" });
283
+ } catch (e) {
284
+ if (e.status) process.exit(e.status);
285
+ console.error(e.message);
286
+ process.exit(1);
287
+ }
package/claude-load ADDED
@@ -0,0 +1 @@
1
+ 0
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@andrej7510/ai-router",
3
+ "version": "1.0.0",
4
+ "description": "CLI that routes tasks to OpenAI Codex or Claude based on complexity, with automatic load-shedding when Claude context is high",
5
+ "type": "module",
6
+ "bin": {
7
+ "ai": "ai.js"
8
+ },
9
+ "keywords": [
10
+ "ai",
11
+ "claude",
12
+ "codex",
13
+ "openai",
14
+ "anthropic",
15
+ "cli",
16
+ "orchestration",
17
+ "routing"
18
+ ],
19
+ "author": "andrej7510",
20
+ "license": "MIT",
21
+ "engines": {
22
+ "node": ">=18"
23
+ }
24
+ }