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.
Files changed (54) hide show
  1. package/.claude/hooks/config-loader.mjs +93 -0
  2. package/.claude/hooks/heartbeat.mjs +331 -0
  3. package/.claude/hooks/notification.mjs +35 -0
  4. package/.claude/hooks/permission_request.mjs +307 -0
  5. package/.claude/hooks/post_tool_use.mjs +137 -0
  6. package/.claude/hooks/pre_tool_use.mjs +451 -0
  7. package/.claude/hooks/session-register.mjs +274 -0
  8. package/.claude/hooks/session_end.mjs +256 -0
  9. package/.claude/hooks/session_start.mjs +308 -0
  10. package/.claude/hooks/stop.mjs +277 -0
  11. package/.claude/hooks/user_prompt_submit.mjs +91 -0
  12. package/LICENSE +21 -0
  13. package/README.md +243 -0
  14. package/lib/auth/api-key.js +110 -0
  15. package/lib/auth/credentials.js +341 -0
  16. package/lib/backup/manager.js +461 -0
  17. package/lib/cli/daemon-commands.js +299 -0
  18. package/lib/cli/index.js +303 -0
  19. package/lib/cli/session-commands.js +294 -0
  20. package/lib/cli/snapshot-commands.js +223 -0
  21. package/lib/cli/worktree-commands.js +291 -0
  22. package/lib/config/manager.js +306 -0
  23. package/lib/daemon/lifecycle.js +336 -0
  24. package/lib/daemon/pid-manager.js +160 -0
  25. package/lib/daemon/teleportation-daemon.js +2009 -0
  26. package/lib/handoff/config.js +102 -0
  27. package/lib/handoff/example.js +152 -0
  28. package/lib/handoff/git-handoff.js +351 -0
  29. package/lib/handoff/handoff.js +277 -0
  30. package/lib/handoff/index.js +25 -0
  31. package/lib/handoff/session-state.js +238 -0
  32. package/lib/install/installer.js +555 -0
  33. package/lib/machine-coders/claude-code-adapter.js +329 -0
  34. package/lib/machine-coders/example.js +239 -0
  35. package/lib/machine-coders/gemini-cli-adapter.js +406 -0
  36. package/lib/machine-coders/index.js +103 -0
  37. package/lib/machine-coders/interface.js +168 -0
  38. package/lib/router/classifier.js +251 -0
  39. package/lib/router/example.js +92 -0
  40. package/lib/router/index.js +69 -0
  41. package/lib/router/mech-llms-client.js +277 -0
  42. package/lib/router/models.js +188 -0
  43. package/lib/router/router.js +382 -0
  44. package/lib/session/cleanup.js +100 -0
  45. package/lib/session/metadata.js +258 -0
  46. package/lib/session/mute-checker.js +114 -0
  47. package/lib/session-registry/manager.js +302 -0
  48. package/lib/snapshot/manager.js +390 -0
  49. package/lib/utils/errors.js +166 -0
  50. package/lib/utils/logger.js +148 -0
  51. package/lib/utils/retry.js +155 -0
  52. package/lib/worktree/manager.js +301 -0
  53. package/package.json +66 -0
  54. 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;