teleportation-cli 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.
- package/.claude/hooks/config-loader.mjs +93 -0
- package/.claude/hooks/heartbeat.mjs +331 -0
- package/.claude/hooks/notification.mjs +35 -0
- package/.claude/hooks/permission_request.mjs +307 -0
- package/.claude/hooks/post_tool_use.mjs +137 -0
- package/.claude/hooks/pre_tool_use.mjs +451 -0
- package/.claude/hooks/session-register.mjs +274 -0
- package/.claude/hooks/session_end.mjs +256 -0
- package/.claude/hooks/session_start.mjs +308 -0
- package/.claude/hooks/stop.mjs +277 -0
- package/.claude/hooks/user_prompt_submit.mjs +91 -0
- package/LICENSE +21 -0
- package/README.md +243 -0
- package/lib/auth/api-key.js +110 -0
- package/lib/auth/credentials.js +341 -0
- package/lib/backup/manager.js +461 -0
- package/lib/cli/daemon-commands.js +299 -0
- package/lib/cli/index.js +303 -0
- package/lib/cli/session-commands.js +294 -0
- package/lib/cli/snapshot-commands.js +223 -0
- package/lib/cli/worktree-commands.js +291 -0
- package/lib/config/manager.js +306 -0
- package/lib/daemon/lifecycle.js +336 -0
- package/lib/daemon/pid-manager.js +160 -0
- package/lib/daemon/teleportation-daemon.js +2009 -0
- package/lib/handoff/config.js +102 -0
- package/lib/handoff/example.js +152 -0
- package/lib/handoff/git-handoff.js +351 -0
- package/lib/handoff/handoff.js +277 -0
- package/lib/handoff/index.js +25 -0
- package/lib/handoff/session-state.js +238 -0
- package/lib/install/installer.js +555 -0
- package/lib/machine-coders/claude-code-adapter.js +329 -0
- package/lib/machine-coders/example.js +239 -0
- package/lib/machine-coders/gemini-cli-adapter.js +406 -0
- package/lib/machine-coders/index.js +103 -0
- package/lib/machine-coders/interface.js +168 -0
- package/lib/router/classifier.js +251 -0
- package/lib/router/example.js +92 -0
- package/lib/router/index.js +69 -0
- package/lib/router/mech-llms-client.js +277 -0
- package/lib/router/models.js +188 -0
- package/lib/router/router.js +382 -0
- package/lib/session/cleanup.js +100 -0
- package/lib/session/metadata.js +258 -0
- package/lib/session/mute-checker.js +114 -0
- package/lib/session-registry/manager.js +302 -0
- package/lib/snapshot/manager.js +390 -0
- package/lib/utils/errors.js +166 -0
- package/lib/utils/logger.js +148 -0
- package/lib/utils/retry.js +155 -0
- package/lib/worktree/manager.js +301 -0
- package/package.json +66 -0
- package/teleportation-cli.cjs +2987 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Classifier
|
|
3
|
+
*
|
|
4
|
+
* Classifies coding tasks into tiers (cheap/mid/best) based on:
|
|
5
|
+
* - Prompt content (keywords, complexity indicators)
|
|
6
|
+
* - Context metadata (file count, diff size, etc.)
|
|
7
|
+
*
|
|
8
|
+
* This is a heuristic-based classifier. For more sophisticated classification,
|
|
9
|
+
* use the /v1/models/recommend endpoint from llms.mechdna.net.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Tier constants
|
|
13
|
+
export const TIERS = {
|
|
14
|
+
CHEAP: 'cheap',
|
|
15
|
+
MID: 'mid',
|
|
16
|
+
BEST: 'best',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Tier ordering for escalation
|
|
20
|
+
export const TIER_ORDER = ['cheap', 'mid', 'best'];
|
|
21
|
+
|
|
22
|
+
// Keywords that suggest different complexity levels
|
|
23
|
+
const COMPLEXITY_KEYWORDS = {
|
|
24
|
+
// Best tier - complex reasoning, planning, architecture
|
|
25
|
+
best: [
|
|
26
|
+
'architect', 'architecture', 'design', 'redesign',
|
|
27
|
+
'plan', 'planning', 'strategy', 'roadmap',
|
|
28
|
+
'migrate', 'migration', 'rewrite', 'rebuild',
|
|
29
|
+
'from scratch', 'greenfield', 'new system',
|
|
30
|
+
'complex', 'complicated', 'tricky', 'difficult',
|
|
31
|
+
'optimize', 'performance', 'scalability',
|
|
32
|
+
'security', 'audit', 'review thoroughly',
|
|
33
|
+
'think through', 'reason about', 'analyze deeply'
|
|
34
|
+
],
|
|
35
|
+
|
|
36
|
+
// Mid tier - substantial but bounded work
|
|
37
|
+
mid: [
|
|
38
|
+
'refactor', 'reorganize', 'restructure',
|
|
39
|
+
'implement', 'build', 'create', 'develop',
|
|
40
|
+
'fix bug', 'debug', 'troubleshoot',
|
|
41
|
+
'add feature', 'new feature', 'enhance',
|
|
42
|
+
'test', 'testing', 'write tests',
|
|
43
|
+
'multiple files', 'across files', 'several',
|
|
44
|
+
'integrate', 'integration', 'connect',
|
|
45
|
+
'update', 'upgrade', 'modify'
|
|
46
|
+
],
|
|
47
|
+
|
|
48
|
+
// Cheap tier - simple, bounded tasks (default)
|
|
49
|
+
cheap: [
|
|
50
|
+
'explain', 'what is', 'how does', 'why',
|
|
51
|
+
'simple', 'quick', 'small', 'minor',
|
|
52
|
+
'typo', 'rename', 'format', 'lint',
|
|
53
|
+
'comment', 'document', 'readme',
|
|
54
|
+
'list', 'show', 'find', 'search', 'grep',
|
|
55
|
+
'one file', 'single file', 'this file',
|
|
56
|
+
'variable', 'function name', 'import'
|
|
57
|
+
]
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Context thresholds
|
|
61
|
+
const THRESHOLDS = {
|
|
62
|
+
// File count thresholds
|
|
63
|
+
MANY_FILES: 5, // > 5 files = likely mid or best
|
|
64
|
+
FEW_FILES: 2, // <= 2 files = likely cheap
|
|
65
|
+
|
|
66
|
+
// Diff/change size thresholds (lines)
|
|
67
|
+
LARGE_DIFF: 500, // > 500 lines = likely best
|
|
68
|
+
MEDIUM_DIFF: 100, // > 100 lines = likely mid
|
|
69
|
+
SMALL_DIFF: 30, // <= 30 lines = likely cheap
|
|
70
|
+
|
|
71
|
+
// Prompt length thresholds (characters)
|
|
72
|
+
LONG_PROMPT: 2000, // Long prompts often indicate complex tasks
|
|
73
|
+
SHORT_PROMPT: 200 // Short prompts often indicate simple tasks
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Classify a task based on prompt and context
|
|
78
|
+
*
|
|
79
|
+
* @param {string} prompt - The user's prompt/request
|
|
80
|
+
* @param {Object} context - Optional context metadata
|
|
81
|
+
* @param {number} context.fileCount - Number of files involved
|
|
82
|
+
* @param {number} context.diffLines - Number of lines changed/affected
|
|
83
|
+
* @param {string[]} context.fileTypes - Types of files involved
|
|
84
|
+
* @param {boolean} context.isNewProject - Whether this is a new project
|
|
85
|
+
* @param {string} context.previousAttemptFailed - If a previous attempt failed
|
|
86
|
+
* @returns {{ tier: string, reason: string, confidence: number }}
|
|
87
|
+
*/
|
|
88
|
+
export function classifyTask(prompt, context = {}) {
|
|
89
|
+
const text = prompt.toLowerCase();
|
|
90
|
+
const {
|
|
91
|
+
fileCount = 1,
|
|
92
|
+
diffLines = 0,
|
|
93
|
+
isNewProject = false,
|
|
94
|
+
previousAttemptFailed = false
|
|
95
|
+
} = context;
|
|
96
|
+
|
|
97
|
+
// Track scoring
|
|
98
|
+
let scores = { cheap: 0, mid: 0, best: 0 };
|
|
99
|
+
let reasons = [];
|
|
100
|
+
|
|
101
|
+
// 1. Check for explicit tier keywords
|
|
102
|
+
for (const [tier, keywords] of Object.entries(COMPLEXITY_KEYWORDS)) {
|
|
103
|
+
for (const keyword of keywords) {
|
|
104
|
+
if (text.includes(keyword)) {
|
|
105
|
+
scores[tier] += 2;
|
|
106
|
+
if (scores[tier] === 2) { // First match for this tier
|
|
107
|
+
reasons.push(`keyword "${keyword}"`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 2. Context-based scoring
|
|
114
|
+
|
|
115
|
+
// File count
|
|
116
|
+
if (fileCount > THRESHOLDS.MANY_FILES) {
|
|
117
|
+
scores.best += 2;
|
|
118
|
+
scores.mid += 1;
|
|
119
|
+
reasons.push(`${fileCount} files involved`);
|
|
120
|
+
} else if (fileCount > THRESHOLDS.FEW_FILES) {
|
|
121
|
+
scores.mid += 2;
|
|
122
|
+
reasons.push(`${fileCount} files involved`);
|
|
123
|
+
} else {
|
|
124
|
+
scores.cheap += 1;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Diff size
|
|
128
|
+
if (diffLines > THRESHOLDS.LARGE_DIFF) {
|
|
129
|
+
scores.best += 3;
|
|
130
|
+
reasons.push(`large diff (${diffLines} lines)`);
|
|
131
|
+
} else if (diffLines > THRESHOLDS.MEDIUM_DIFF) {
|
|
132
|
+
scores.mid += 2;
|
|
133
|
+
reasons.push(`medium diff (${diffLines} lines)`);
|
|
134
|
+
} else if (diffLines > THRESHOLDS.SMALL_DIFF) {
|
|
135
|
+
scores.mid += 1;
|
|
136
|
+
} else {
|
|
137
|
+
scores.cheap += 1;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// New project = more complex
|
|
141
|
+
if (isNewProject) {
|
|
142
|
+
scores.best += 2;
|
|
143
|
+
scores.mid += 1;
|
|
144
|
+
reasons.push('new project');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Previous attempt failed = escalate
|
|
148
|
+
if (previousAttemptFailed) {
|
|
149
|
+
scores.best += 2;
|
|
150
|
+
scores.mid += 1;
|
|
151
|
+
reasons.push('previous attempt failed');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Prompt length heuristic
|
|
155
|
+
if (prompt.length > THRESHOLDS.LONG_PROMPT) {
|
|
156
|
+
scores.mid += 1;
|
|
157
|
+
scores.best += 1;
|
|
158
|
+
} else if (prompt.length < THRESHOLDS.SHORT_PROMPT) {
|
|
159
|
+
scores.cheap += 1;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 3. Determine winner
|
|
163
|
+
const maxScore = Math.max(scores.cheap, scores.mid, scores.best);
|
|
164
|
+
let tier;
|
|
165
|
+
|
|
166
|
+
if (scores.best === maxScore && scores.best > 0) {
|
|
167
|
+
tier = 'best';
|
|
168
|
+
} else if (scores.mid === maxScore && scores.mid > 0) {
|
|
169
|
+
tier = 'mid';
|
|
170
|
+
} else {
|
|
171
|
+
tier = 'cheap';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Calculate confidence (0-1)
|
|
175
|
+
const totalScore = scores.cheap + scores.mid + scores.best;
|
|
176
|
+
const confidence = totalScore > 0 ? maxScore / totalScore : 0.5;
|
|
177
|
+
|
|
178
|
+
// Build reason string
|
|
179
|
+
const reasonStr = reasons.length > 0
|
|
180
|
+
? reasons.slice(0, 3).join(', ')
|
|
181
|
+
: 'default (no strong signals)';
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
tier,
|
|
185
|
+
reason: reasonStr,
|
|
186
|
+
confidence: Math.round(confidence * 100) / 100,
|
|
187
|
+
scores // Include raw scores for debugging
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Build a use-case string for the /v1/models/recommend API
|
|
193
|
+
*
|
|
194
|
+
* @param {string} prompt - The user's prompt
|
|
195
|
+
* @param {Object} context - Context metadata
|
|
196
|
+
* @returns {string} A concise use-case description
|
|
197
|
+
*/
|
|
198
|
+
export function buildUseCaseString(prompt, context = {}) {
|
|
199
|
+
const { tier, reason } = classifyTask(prompt, context);
|
|
200
|
+
|
|
201
|
+
// Truncate prompt for API
|
|
202
|
+
const truncatedPrompt = prompt.length > 300
|
|
203
|
+
? prompt.slice(0, 300) + '...'
|
|
204
|
+
: prompt;
|
|
205
|
+
|
|
206
|
+
// Build context description
|
|
207
|
+
const contextParts = [];
|
|
208
|
+
if (context.fileCount) contextParts.push(`${context.fileCount} files`);
|
|
209
|
+
if (context.diffLines) contextParts.push(`${context.diffLines} lines changed`);
|
|
210
|
+
if (context.isNewProject) contextParts.push('new project');
|
|
211
|
+
|
|
212
|
+
const contextStr = contextParts.length > 0
|
|
213
|
+
? ` Context: ${contextParts.join(', ')}.`
|
|
214
|
+
: '';
|
|
215
|
+
|
|
216
|
+
return `Coding task (${tier} complexity): ${truncatedPrompt}${contextStr}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Extract context from a coding environment
|
|
221
|
+
* Helper to build context object from various sources
|
|
222
|
+
*
|
|
223
|
+
* @param {Object} options
|
|
224
|
+
* @returns {Object} Context object for classifyTask
|
|
225
|
+
*/
|
|
226
|
+
export function extractContext(options = {}) {
|
|
227
|
+
const {
|
|
228
|
+
files = [], // Array of file paths
|
|
229
|
+
diff = '', // Git diff or similar
|
|
230
|
+
isNew = false, // New project flag
|
|
231
|
+
failedBefore = false // Previous attempt failed
|
|
232
|
+
} = options;
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
fileCount: files.length,
|
|
236
|
+
diffLines: diff ? diff.split('\n').length : 0,
|
|
237
|
+
fileTypes: [...new Set(files.map(f => f.split('.').pop()))],
|
|
238
|
+
isNewProject: isNew,
|
|
239
|
+
previousAttemptFailed: failedBefore
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export default {
|
|
244
|
+
classifyTask,
|
|
245
|
+
buildUseCaseString,
|
|
246
|
+
extractContext,
|
|
247
|
+
TIERS,
|
|
248
|
+
TIER_ORDER,
|
|
249
|
+
COMPLEXITY_KEYWORDS,
|
|
250
|
+
THRESHOLDS
|
|
251
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Router Example / Test Script
|
|
4
|
+
*
|
|
5
|
+
* Run: node lib/router/example.js
|
|
6
|
+
*
|
|
7
|
+
* Requires: MECH_API_KEY environment variable
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createRouter, classifyTask } from './index.js';
|
|
11
|
+
|
|
12
|
+
async function main() {
|
|
13
|
+
console.log('=== Teleportation Router Example ===\n');
|
|
14
|
+
|
|
15
|
+
// Check for API key
|
|
16
|
+
if (!process.env.MECH_API_KEY) {
|
|
17
|
+
console.log('⚠️ MECH_API_KEY not set. Classification will work but completions will fail.\n');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ============================================
|
|
21
|
+
// 1. Test Task Classification
|
|
22
|
+
// ============================================
|
|
23
|
+
console.log('--- Task Classification Examples ---\n');
|
|
24
|
+
|
|
25
|
+
const testCases = [
|
|
26
|
+
{ prompt: 'What is a variable?', context: {} },
|
|
27
|
+
{ prompt: 'Rename this function to getUserById', context: { fileCount: 1 } },
|
|
28
|
+
{ prompt: 'Refactor the authentication module to use JWT', context: { fileCount: 5, diffLines: 200 } },
|
|
29
|
+
{ prompt: 'Design the architecture for a new microservices system', context: { isNewProject: true } },
|
|
30
|
+
{ prompt: 'Fix the bug where users can\'t login', context: { fileCount: 3, diffLines: 50 } },
|
|
31
|
+
{ prompt: 'Migrate the database from MySQL to PostgreSQL', context: { fileCount: 20, diffLines: 1000 } },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
for (const { prompt, context } of testCases) {
|
|
35
|
+
const result = classifyTask(prompt, context);
|
|
36
|
+
console.log(`Prompt: "${prompt.slice(0, 50)}${prompt.length > 50 ? '...' : ''}"`);
|
|
37
|
+
console.log(` → Tier: ${result.tier} (confidence: ${result.confidence})`);
|
|
38
|
+
console.log(` → Reason: ${result.reason}`);
|
|
39
|
+
console.log();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ============================================
|
|
43
|
+
// 2. Test Router (if API key available)
|
|
44
|
+
// ============================================
|
|
45
|
+
if (process.env.MECH_API_KEY) {
|
|
46
|
+
console.log('--- Router Test ---\n');
|
|
47
|
+
|
|
48
|
+
const router = createRouter({ verbose: true });
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
// Simple question (should use cheap model)
|
|
52
|
+
console.log('Test 1: Simple question\n');
|
|
53
|
+
const result1 = await router.route({
|
|
54
|
+
prompt: 'What is the capital of France?',
|
|
55
|
+
messages: [{ role: 'user', content: 'What is the capital of France?' }]
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
console.log(`Response: ${result1.content}`);
|
|
59
|
+
console.log(`Model: ${result1.model}`);
|
|
60
|
+
console.log(`Cost: $${result1.cost?.toFixed(6) || 'unknown'}`);
|
|
61
|
+
console.log(`Escalations: ${result1.escalations}`);
|
|
62
|
+
console.log();
|
|
63
|
+
|
|
64
|
+
// Coding task (should use mid model)
|
|
65
|
+
console.log('Test 2: Coding task\n');
|
|
66
|
+
const result2 = await router.route({
|
|
67
|
+
prompt: 'Write a function to reverse a string in JavaScript',
|
|
68
|
+
messages: [{ role: 'user', content: 'Write a function to reverse a string in JavaScript' }],
|
|
69
|
+
context: { fileCount: 1 }
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
console.log(`Response: ${result2.content.slice(0, 200)}...`);
|
|
73
|
+
console.log(`Model: ${result2.model}`);
|
|
74
|
+
console.log(`Cost: $${result2.cost?.toFixed(6) || 'unknown'}`);
|
|
75
|
+
console.log(`Escalations: ${result2.escalations}`);
|
|
76
|
+
console.log();
|
|
77
|
+
|
|
78
|
+
// Get usage summary
|
|
79
|
+
console.log('--- Usage Summary ---\n');
|
|
80
|
+
const usage = await router.getUsageSummary();
|
|
81
|
+
console.log(`Today: ${usage.today?.totalRequests || 0} requests, $${usage.today?.totalCost?.toFixed(4) || '0.0000'}`);
|
|
82
|
+
console.log(`This month: ${usage.thisMonth?.totalRequests || 0} requests, $${usage.thisMonth?.totalCost?.toFixed(4) || '0.0000'}`);
|
|
83
|
+
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.error('Router test failed:', err.message);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log('\n=== Done ===');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Teleportation Model Router
|
|
3
|
+
*
|
|
4
|
+
* Cost-aware routing for LLM requests. Automatically selects the cheapest
|
|
5
|
+
* capable model for each task, with escalation on failure.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
*
|
|
9
|
+
* ```js
|
|
10
|
+
* import { createRouter, route } from './lib/router/index.js';
|
|
11
|
+
*
|
|
12
|
+
* // Option 1: Create a router instance
|
|
13
|
+
* const router = createRouter({ verbose: true });
|
|
14
|
+
* const result = await router.route({
|
|
15
|
+
* prompt: 'Fix the bug in this function',
|
|
16
|
+
* messages: [{ role: 'user', content: 'Fix the bug in this function...' }],
|
|
17
|
+
* context: { fileCount: 1, diffLines: 20 }
|
|
18
|
+
* });
|
|
19
|
+
* console.log(result.content);
|
|
20
|
+
* console.log(`Cost: $${result.cost.toFixed(4)}, Model: ${result.model}`);
|
|
21
|
+
*
|
|
22
|
+
* // Option 2: Quick one-off routing
|
|
23
|
+
* const result = await route('Explain this code', messages);
|
|
24
|
+
*
|
|
25
|
+
* // Option 3: Direct completion (no routing)
|
|
26
|
+
* const result = await router.complete({
|
|
27
|
+
* model: 'gpt-4o-mini',
|
|
28
|
+
* messages: [{ role: 'user', content: 'Hello' }]
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* Environment Variables:
|
|
33
|
+
* - MECH_API_KEY: API key for llms.mechdna.net
|
|
34
|
+
* - MECH_LLMS_URL: Override API URL (default: https://llms.mechdna.net)
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
// Main router
|
|
38
|
+
export { Router, createRouter, route } from './router.js';
|
|
39
|
+
|
|
40
|
+
// Task classifier
|
|
41
|
+
export {
|
|
42
|
+
classifyTask,
|
|
43
|
+
buildUseCaseString,
|
|
44
|
+
extractContext
|
|
45
|
+
} from './classifier.js';
|
|
46
|
+
|
|
47
|
+
// Re-export constants from default export
|
|
48
|
+
import classifierModule from './classifier.js';
|
|
49
|
+
export const COMPLEXITY_KEYWORDS = classifierModule.COMPLEXITY_KEYWORDS;
|
|
50
|
+
export const THRESHOLDS = classifierModule.THRESHOLDS;
|
|
51
|
+
|
|
52
|
+
// Model registry
|
|
53
|
+
export {
|
|
54
|
+
QUALITY_TIERS,
|
|
55
|
+
TIER_TO_QUALITY,
|
|
56
|
+
FALLBACK_MODELS,
|
|
57
|
+
getModelsByQuality,
|
|
58
|
+
getModelsByTier,
|
|
59
|
+
getCheapestForTier,
|
|
60
|
+
getEscalationOrder,
|
|
61
|
+
getNextTier
|
|
62
|
+
} from './models.js';
|
|
63
|
+
|
|
64
|
+
// Mech LLMs client
|
|
65
|
+
export { MechLLMsClient, createClient } from './mech-llms-client.js';
|
|
66
|
+
|
|
67
|
+
// Default export
|
|
68
|
+
import { Router } from './router.js';
|
|
69
|
+
export default Router;
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mech LLMs API Client
|
|
3
|
+
*
|
|
4
|
+
* Client for llms.mechdna.net - the unified LLM gateway.
|
|
5
|
+
* Provides access to:
|
|
6
|
+
* - Chat completions (multi-provider)
|
|
7
|
+
* - Model registry with cost/capability data
|
|
8
|
+
* - Model recommendations
|
|
9
|
+
* - Usage tracking
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const DEFAULT_BASE_URL = 'https://llms.mechdna.net';
|
|
13
|
+
const DEFAULT_TIMEOUT = 120000; // 2 minutes for LLM calls
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Mech LLMs API Client
|
|
17
|
+
*/
|
|
18
|
+
export class MechLLMsClient {
|
|
19
|
+
/**
|
|
20
|
+
* @param {Object} options
|
|
21
|
+
* @param {string} options.apiKey - Mech API key (X-API-Key header)
|
|
22
|
+
* @param {string} [options.baseUrl] - API base URL
|
|
23
|
+
* @param {number} [options.timeout] - Request timeout in ms
|
|
24
|
+
*/
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
this.apiKey = options.apiKey || process.env.MECH_API_KEY;
|
|
27
|
+
this.baseUrl = options.baseUrl || process.env.MECH_LLMS_URL || DEFAULT_BASE_URL;
|
|
28
|
+
this.timeout = options.timeout || DEFAULT_TIMEOUT;
|
|
29
|
+
|
|
30
|
+
if (!this.apiKey) {
|
|
31
|
+
console.warn('[MechLLMsClient] No API key provided. Set MECH_API_KEY env var or pass apiKey option.');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Make an authenticated request to the API
|
|
37
|
+
*/
|
|
38
|
+
async _request(method, path, body = null) {
|
|
39
|
+
const url = `${this.baseUrl}${path}`;
|
|
40
|
+
const headers = {
|
|
41
|
+
'Content-Type': 'application/json',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if (this.apiKey) {
|
|
45
|
+
headers['X-API-Key'] = this.apiKey;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const controller = new AbortController();
|
|
49
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const response = await fetch(url, {
|
|
53
|
+
method,
|
|
54
|
+
headers,
|
|
55
|
+
body: body ? JSON.stringify(body) : null,
|
|
56
|
+
signal: controller.signal
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
clearTimeout(timeoutId);
|
|
60
|
+
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
const error = await response.json().catch(() => ({ error: { message: response.statusText } }));
|
|
63
|
+
throw new Error(error.error?.message || `HTTP ${response.status}: ${response.statusText}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return response.json();
|
|
67
|
+
} catch (err) {
|
|
68
|
+
clearTimeout(timeoutId);
|
|
69
|
+
if (err.name === 'AbortError') {
|
|
70
|
+
throw new Error(`Request timeout after ${this.timeout}ms`);
|
|
71
|
+
}
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ============================================
|
|
77
|
+
// Chat Completions
|
|
78
|
+
// ============================================
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create a chat completion
|
|
82
|
+
*
|
|
83
|
+
* @param {Object} options
|
|
84
|
+
* @param {string} options.model - Model ID (e.g., 'gpt-4o', 'claude-sonnet-4')
|
|
85
|
+
* @param {Array} options.messages - Chat messages
|
|
86
|
+
* @param {number} [options.maxTokens] - Max tokens to generate
|
|
87
|
+
* @param {number} [options.temperature] - Sampling temperature
|
|
88
|
+
* @param {boolean} [options.stream] - Enable streaming (not supported in this client yet)
|
|
89
|
+
* @param {Array} [options.tools] - Tools/functions for function calling
|
|
90
|
+
* @param {Object} [options.reasoning] - Reasoning/thinking config
|
|
91
|
+
* @returns {Promise<Object>} Completion response with x_llm_cost
|
|
92
|
+
*/
|
|
93
|
+
async createCompletion(options) {
|
|
94
|
+
const {
|
|
95
|
+
model,
|
|
96
|
+
messages,
|
|
97
|
+
maxTokens = 4096,
|
|
98
|
+
temperature,
|
|
99
|
+
tools,
|
|
100
|
+
reasoning,
|
|
101
|
+
responseFormat,
|
|
102
|
+
...rest
|
|
103
|
+
} = options;
|
|
104
|
+
|
|
105
|
+
const body = {
|
|
106
|
+
model,
|
|
107
|
+
messages,
|
|
108
|
+
max_tokens: maxTokens,
|
|
109
|
+
...rest
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (temperature !== undefined) body.temperature = temperature;
|
|
113
|
+
if (tools) body.tools = tools;
|
|
114
|
+
if (reasoning) body.reasoning = reasoning;
|
|
115
|
+
if (responseFormat) body.response_format = responseFormat;
|
|
116
|
+
|
|
117
|
+
return this._request('POST', '/v1/chat/completions', body);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ============================================
|
|
121
|
+
// Model Registry
|
|
122
|
+
// ============================================
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* List available models
|
|
126
|
+
*
|
|
127
|
+
* @returns {Promise<Object>} List of models
|
|
128
|
+
*/
|
|
129
|
+
async listModels() {
|
|
130
|
+
return this._request('GET', '/v1/models');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get model registry with filtering and sorting
|
|
135
|
+
*
|
|
136
|
+
* @param {Object} [filters]
|
|
137
|
+
* @param {string} [filters.provider] - Filter by provider (anthropic, openai, google)
|
|
138
|
+
* @param {string} [filters.capability] - Filter by capability
|
|
139
|
+
* @param {string} [filters.quality] - Filter by quality tier
|
|
140
|
+
* @param {number} [filters.minCost] - Min cost per 1M tokens
|
|
141
|
+
* @param {number} [filters.maxCost] - Max cost per 1M tokens
|
|
142
|
+
* @param {string} [filters.sortBy] - Sort by (cost, speed, quality)
|
|
143
|
+
* @param {string} [filters.sortOrder] - Sort order (asc, desc)
|
|
144
|
+
* @returns {Promise<Object>} Model registry response
|
|
145
|
+
*/
|
|
146
|
+
async getModelRegistry(filters = {}) {
|
|
147
|
+
const params = new URLSearchParams();
|
|
148
|
+
|
|
149
|
+
if (filters.provider) params.set('provider', filters.provider);
|
|
150
|
+
if (filters.capability) params.set('capability', filters.capability);
|
|
151
|
+
if (filters.quality) params.set('quality', filters.quality);
|
|
152
|
+
if (filters.minCost) params.set('min_cost', filters.minCost);
|
|
153
|
+
if (filters.maxCost) params.set('max_cost', filters.maxCost);
|
|
154
|
+
if (filters.sortBy) params.set('sort_by', filters.sortBy);
|
|
155
|
+
if (filters.sortOrder) params.set('sort_order', filters.sortOrder);
|
|
156
|
+
|
|
157
|
+
const query = params.toString();
|
|
158
|
+
const path = query ? `/v1/models/registry?${query}` : '/v1/models/registry';
|
|
159
|
+
|
|
160
|
+
return this._request('GET', path);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get cheapest model for a quality tier
|
|
165
|
+
*
|
|
166
|
+
* @param {string} quality - Quality tier (flagship, high, standard, fast, budget)
|
|
167
|
+
* @returns {Promise<Object|null>} Cheapest model or null
|
|
168
|
+
*/
|
|
169
|
+
async getCheapestModel(quality) {
|
|
170
|
+
const result = await this.getModelRegistry({
|
|
171
|
+
quality,
|
|
172
|
+
sortBy: 'cost',
|
|
173
|
+
sortOrder: 'asc'
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return result.models?.[0] || null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Compare multiple models
|
|
181
|
+
*
|
|
182
|
+
* @param {string[]} models - Array of model IDs to compare
|
|
183
|
+
* @returns {Promise<Object>} Comparison response
|
|
184
|
+
*/
|
|
185
|
+
async compareModels(models) {
|
|
186
|
+
return this._request('POST', '/v1/models/compare', { models });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get model recommendations based on use case
|
|
191
|
+
*
|
|
192
|
+
* @param {Object} options
|
|
193
|
+
* @param {string} options.useCase - Use case description
|
|
194
|
+
* @param {Object} [options.requirements] - Requirements
|
|
195
|
+
* @param {number} [options.requirements.maxCost] - Max cost per 1M tokens
|
|
196
|
+
* @param {string} [options.requirements.minSpeed] - Min speed (very_fast, fast, medium, slow)
|
|
197
|
+
* @param {string[]} [options.requirements.capabilities] - Required capabilities
|
|
198
|
+
* @returns {Promise<Object>} Recommendations response
|
|
199
|
+
*/
|
|
200
|
+
async getRecommendations(options) {
|
|
201
|
+
const { useCase, requirements } = options;
|
|
202
|
+
|
|
203
|
+
const body = {
|
|
204
|
+
use_case: useCase
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
if (requirements) {
|
|
208
|
+
body.requirements = {};
|
|
209
|
+
if (requirements.maxCost) body.requirements.max_cost = requirements.maxCost;
|
|
210
|
+
if (requirements.minSpeed) body.requirements.min_speed = requirements.minSpeed;
|
|
211
|
+
if (requirements.capabilities) body.requirements.capabilities = requirements.capabilities;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return this._request('POST', '/v1/models/recommend', body);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ============================================
|
|
218
|
+
// Usage Tracking
|
|
219
|
+
// ============================================
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get usage statistics
|
|
223
|
+
*
|
|
224
|
+
* @param {Object} [filters]
|
|
225
|
+
* @param {string} [filters.userId] - Filter by user ID
|
|
226
|
+
* @param {string} [filters.projectId] - Filter by project ID
|
|
227
|
+
* @param {string} [filters.model] - Filter by model
|
|
228
|
+
* @param {string} [filters.startDate] - Start date (ISO 8601)
|
|
229
|
+
* @param {string} [filters.endDate] - End date (ISO 8601)
|
|
230
|
+
* @param {string} [filters.groupBy] - Group by (hour, day, week, month, model, project)
|
|
231
|
+
* @returns {Promise<Object>} Usage statistics
|
|
232
|
+
*/
|
|
233
|
+
async getUsage(filters = {}) {
|
|
234
|
+
const params = new URLSearchParams();
|
|
235
|
+
|
|
236
|
+
if (filters.userId) params.set('userId', filters.userId);
|
|
237
|
+
if (filters.projectId) params.set('projectId', filters.projectId);
|
|
238
|
+
if (filters.model) params.set('model', filters.model);
|
|
239
|
+
if (filters.startDate) params.set('startDate', filters.startDate);
|
|
240
|
+
if (filters.endDate) params.set('endDate', filters.endDate);
|
|
241
|
+
if (filters.groupBy) params.set('groupBy', filters.groupBy);
|
|
242
|
+
|
|
243
|
+
const query = params.toString();
|
|
244
|
+
const path = query ? `/v1/usage?${query}` : '/v1/usage';
|
|
245
|
+
|
|
246
|
+
return this._request('GET', path);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Get usage summary (today, this month, all time)
|
|
251
|
+
*
|
|
252
|
+
* @param {Object} [filters]
|
|
253
|
+
* @param {string} [filters.userId] - Filter by user ID
|
|
254
|
+
* @param {string} [filters.projectId] - Filter by project ID
|
|
255
|
+
* @returns {Promise<Object>} Usage summary
|
|
256
|
+
*/
|
|
257
|
+
async getUsageSummary(filters = {}) {
|
|
258
|
+
const params = new URLSearchParams();
|
|
259
|
+
|
|
260
|
+
if (filters.userId) params.set('userId', filters.userId);
|
|
261
|
+
if (filters.projectId) params.set('projectId', filters.projectId);
|
|
262
|
+
|
|
263
|
+
const query = params.toString();
|
|
264
|
+
const path = query ? `/v1/usage/summary?${query}` : '/v1/usage/summary';
|
|
265
|
+
|
|
266
|
+
return this._request('GET', path);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Create a client instance with default configuration
|
|
272
|
+
*/
|
|
273
|
+
export function createClient(options = {}) {
|
|
274
|
+
return new MechLLMsClient(options);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export default MechLLMsClient;
|