voracode 0.0.1

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.
@@ -0,0 +1,118 @@
1
+ /**
2
+ * VORACODE Session Manager — Create, resume, fork sessions
3
+ *
4
+ * Integrates with SQLite database for persistence.
5
+ */
6
+
7
+ import { VoraDatabase } from "../storage/database";
8
+ import { ModelRouter } from "../models/router";
9
+ import { Agent } from "../engine/agent";
10
+
11
+ export interface SessionInfo {
12
+ id: string;
13
+ title: string;
14
+ modelProvider: string;
15
+ modelName: string;
16
+ totalTokens: number;
17
+ totalTurns: number;
18
+ status: string;
19
+ createdAt: string;
20
+ updatedAt: string;
21
+ }
22
+
23
+ export class SessionManager {
24
+ private db: VoraDatabase;
25
+ private router: ModelRouter;
26
+ private agent: Agent;
27
+
28
+ constructor() {
29
+ this.db = new VoraDatabase();
30
+ this.router = new ModelRouter();
31
+ this.agent = new Agent();
32
+ }
33
+
34
+ /**
35
+ * Create a new session
36
+ */
37
+ create(modelRef?: string): string {
38
+ const provider = modelRef ? this.router.detectProvider(modelRef) : "auto";
39
+ const modelName = modelRef || "auto";
40
+ return this.db.createSession(provider, modelName);
41
+ }
42
+
43
+ /**
44
+ * Resume an existing session
45
+ */
46
+ getSession(id: string): SessionInfo | null {
47
+ const session = this.db.getSession(id) as Record<string, unknown> | null;
48
+ if (!session) return null;
49
+
50
+ return {
51
+ id: session.id as string,
52
+ title: session.title as string,
53
+ modelProvider: session.model_provider as string,
54
+ modelName: session.model_name as string,
55
+ totalTokens: (session.total_tokens as number) || 0,
56
+ totalTurns: (session.total_turns as number) || 0,
57
+ status: session.status as string,
58
+ createdAt: session.created_at as string,
59
+ updatedAt: session.updated_at as string,
60
+ };
61
+ }
62
+
63
+ /**
64
+ * List all sessions
65
+ */
66
+ list(limit = 10): SessionInfo[] {
67
+ return (this.db.listSessions(limit) as Record<string, unknown>[]).map((s) => ({
68
+ id: s.id as string,
69
+ title: s.title as string,
70
+ modelProvider: s.model_provider as string,
71
+ modelName: s.model_name as string,
72
+ totalTokens: (s.total_tokens as number) || 0,
73
+ totalTurns: (s.total_turns as number) || 0,
74
+ status: s.status as string,
75
+ createdAt: s.created_at as string,
76
+ updatedAt: s.updated_at as string,
77
+ }));
78
+ }
79
+
80
+ /**
81
+ * Delete a session
82
+ */
83
+ delete(id: string): void {
84
+ this.db.deleteSession(id);
85
+ }
86
+
87
+ /**
88
+ * Run a task in a session
89
+ */
90
+ async run(sessionId: string, message: string, modelRef: string, options?: { maxTurns?: number }) {
91
+ return this.agent.runTurn(sessionId, message, modelRef, options);
92
+ }
93
+
94
+ /**
95
+ * Fork a session (create new from existing)
96
+ */
97
+ fork(id: string): string | null {
98
+ const original = this.getSession(id);
99
+ if (!original) return null;
100
+
101
+ const newId = this.db.createSession(original.modelProvider, original.modelName, `${original.title} (fork)`);
102
+ return newId;
103
+ }
104
+
105
+ /**
106
+ * Get messages for a session
107
+ */
108
+ getMessages(sessionId: string) {
109
+ return this.db.getMessages(sessionId);
110
+ }
111
+
112
+ /**
113
+ * Get database path
114
+ */
115
+ getDbPath(): string {
116
+ return this.db.getDbPath();
117
+ }
118
+ }
@@ -0,0 +1,336 @@
1
+ /**
2
+ * VORACODE Self-Improvement Engine
3
+ *
4
+ * Learns from user patterns to suggest reusable skills.
5
+ * Privacy-first: all learning is local, no data sent anywhere.
6
+ *
7
+ * 3-Gate Learning System:
8
+ * Gate 1: Repetition (3+ similar tasks)
9
+ * Gate 2: Success rate (80%+ to auto-learn)
10
+ * Gate 3: User confirmation (always ask before creating skill)
11
+ */
12
+
13
+ import { VoraDatabase } from "../../storage/database";
14
+ import { writeFileSync, existsSync, mkdirSync } from "fs";
15
+ import { join } from "path";
16
+ import { homedir } from "os";
17
+
18
+ export interface TaskPattern {
19
+ id: number;
20
+ signature: string; // hash of task type + tools used
21
+ description: string; // human-readable description
22
+ toolSequence: string[]; // tools used in order
23
+ filesChanged: string[]; // file paths touched
24
+ commandsRun: string[]; // bash commands run
25
+ successCount: number;
26
+ failureCount: number;
27
+ lastSeen: string;
28
+ suggestedSkill: boolean; // has user been asked about this?
29
+ userDecision: "pending" | "accepted" | "rejected" | "never";
30
+ skillPath?: string; // path to generated SKILL.md
31
+ }
32
+
33
+ export class SelfImprovementEngine {
34
+ private db: VoraDatabase;
35
+ private skillsDir: string;
36
+
37
+ constructor(db: VoraDatabase) {
38
+ this.db = db;
39
+ this.skillsDir = join(homedir(), ".config", "voracode", "skills");
40
+ if (!existsSync(this.skillsDir)) {
41
+ mkdirSync(this.skillsDir, { recursive: true });
42
+ }
43
+ this.ensureTables();
44
+ }
45
+
46
+ private ensureTables(): void {
47
+ this.db.exec(`
48
+ CREATE TABLE IF NOT EXISTS task_patterns (
49
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
50
+ signature TEXT UNIQUE NOT NULL,
51
+ description TEXT NOT NULL,
52
+ tool_sequence TEXT NOT NULL,
53
+ files_changed TEXT,
54
+ commands_run TEXT,
55
+ success_count INTEGER DEFAULT 0,
56
+ failure_count INTEGER DEFAULT 0,
57
+ last_seen TEXT DEFAULT (datetime('now')),
58
+ suggested_skill INTEGER DEFAULT 0,
59
+ user_decision TEXT DEFAULT 'pending',
60
+ skill_path TEXT
61
+ )
62
+ `);
63
+
64
+ this.db.exec(`
65
+ CREATE TABLE IF NOT EXISTS user_preferences (
66
+ key TEXT PRIMARY KEY,
67
+ value TEXT NOT NULL,
68
+ category TEXT,
69
+ confidence REAL DEFAULT 0.5,
70
+ created_at TEXT DEFAULT (datetime('now')),
71
+ updated_at TEXT DEFAULT (datetime('now'))
72
+ )
73
+ `);
74
+
75
+ this.db.exec(`
76
+ CREATE TABLE IF NOT EXISTS skill_history (
77
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
78
+ skill_name TEXT NOT NULL,
79
+ version INTEGER DEFAULT 1,
80
+ action TEXT NOT NULL,
81
+ content TEXT NOT NULL,
82
+ created_at TEXT DEFAULT (datetime('now'))
83
+ )
84
+ `);
85
+ }
86
+
87
+ /**
88
+ * Record a completed task and check for patterns
89
+ */
90
+ recordTask(
91
+ description: string,
92
+ toolsUsed: string[],
93
+ filesChanged: string[],
94
+ commandsRun: string[],
95
+ success: boolean,
96
+ ): TaskPattern | null {
97
+ // Create a signature from tools used (not file paths — those are project specific)
98
+ const signature = this.createSignature(toolsUsed, description);
99
+
100
+ // Try to find existing pattern
101
+ const existing = this.db.query(
102
+ "SELECT * FROM task_patterns WHERE signature = ?",
103
+ ).get(signature) as Record<string, unknown> | null;
104
+
105
+ if (existing) {
106
+ // Update existing pattern
107
+ const newSuccess = (existing.success_count as number) + (success ? 1 : 0);
108
+ const newFailure = (existing.failure_count as number) + (success ? 0 : 1);
109
+ const total = newSuccess + newFailure;
110
+ const successRate = total > 0 ? newSuccess / total : 0;
111
+
112
+ this.db.run(
113
+ `UPDATE task_patterns SET
114
+ success_count = ?, failure_count = ?, last_seen = datetime('now')
115
+ WHERE signature = ?`,
116
+ [newSuccess, newFailure, signature],
117
+ );
118
+
119
+ // Check if we should suggest a skill
120
+ if (total >= 3 && successRate >= 0.8 && !existing.suggested_skill) {
121
+ const pattern: TaskPattern = {
122
+ id: existing.id as number,
123
+ signature,
124
+ description: existing.description as string,
125
+ toolSequence: JSON.parse(existing.tool_sequence as string),
126
+ filesChanged: JSON.parse((existing.files_changed as string) || "[]"),
127
+ commandsRun: JSON.parse((existing.commands_run as string) || "[]"),
128
+ successCount: newSuccess,
129
+ failureCount: newFailure,
130
+ lastSeen: new Date().toISOString(),
131
+ suggestedSkill: true,
132
+ userDecision: "pending",
133
+ };
134
+
135
+ // Mark as suggested
136
+ this.db.run("UPDATE task_patterns SET suggested_skill = 1 WHERE signature = ?", [signature]);
137
+
138
+ return pattern;
139
+ }
140
+
141
+ return null;
142
+ }
143
+
144
+ // Create new pattern
145
+ this.db.run(
146
+ `INSERT INTO task_patterns
147
+ (signature, description, tool_sequence, files_changed, commands_run, success_count, failure_count)
148
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
149
+ [
150
+ signature,
151
+ description,
152
+ JSON.stringify(toolsUsed),
153
+ JSON.stringify(filesChanged),
154
+ JSON.stringify(commandsRun),
155
+ success ? 1 : 0,
156
+ success ? 0 : 1,
157
+ ],
158
+ );
159
+
160
+ return null;
161
+ }
162
+
163
+ /**
164
+ * Create a pattern signature from tools and task description
165
+ */
166
+ private createSignature(toolsUsed: string[], description: string): string {
167
+ // Normalize: lowercase, remove file paths, keep tool sequence
168
+ const tools = toolsUsed.map((t) => t.toLowerCase()).sort().join(",");
169
+ const taskType = this.classifyTask(description);
170
+ return `${taskType}|${tools}`;
171
+ }
172
+
173
+ /**
174
+ * Classify a task into a category
175
+ */
176
+ private classifyTask(description: string): string {
177
+ const d = description.toLowerCase();
178
+ if (d.includes("create") || d.includes("new") || d.includes("make")) return "create";
179
+ if (d.includes("fix") || d.includes("bug") || d.includes("error")) return "fix";
180
+ if (d.includes("refactor") || d.includes("optimize") || d.includes("improve")) return "refactor";
181
+ if (d.includes("test") || d.includes("spec")) return "test";
182
+ if (d.includes("deploy") || d.includes("publish")) return "deploy";
183
+ if (d.includes("explain") || d.includes("understand") || d.includes("how")) return "explain";
184
+ return "general";
185
+ }
186
+
187
+ /**
188
+ * Generate a SKILL.md from a pattern
189
+ */
190
+ generateSkill(pattern: TaskPattern): string {
191
+ const skillName = pattern.description
192
+ .toLowerCase()
193
+ .replace(/[^a-z0-9]+/g, "-")
194
+ .replace(/^-|-$/g, "")
195
+ .slice(0, 40) || "learned-skill";
196
+
197
+ const skillDir = join(this.skillsDir, skillName);
198
+ mkdirSync(skillDir, { recursive: true });
199
+
200
+ const skillPath = join(skillDir, "SKILL.md");
201
+ const content = `---
202
+ name: ${skillName}
203
+ description: Auto-learned skill for: ${pattern.description}
204
+ version: 1.0.0
205
+ source: learned
206
+ ---
207
+
208
+ ## When to use
209
+ Use this skill when performing: ${pattern.description}
210
+
211
+ ## Pattern discovered
212
+ - Tools used: ${pattern.toolSequence.join(", ")}
213
+ - Success rate: ${((pattern.successCount / (pattern.successCount + pattern.failureCount)) * 100).toFixed(0)}%
214
+ - Times seen: ${pattern.successCount + pattern.failureCount}
215
+
216
+ ## Instructions
217
+ 1. Follow the same tool sequence that worked before
218
+ 2. Apply to the specific project structure
219
+ 3. Verify the result matches expectations
220
+
221
+ ## Notes
222
+ - This skill was auto-generated by VORACODE's self-improvement engine
223
+ - You can edit or delete it: voracode skill remove ${skillName}
224
+ - Rollback: voracode skill history ${skillName}
225
+ `;
226
+
227
+ writeFileSync(skillPath, content, "utf-8");
228
+
229
+ // Save to history
230
+ this.db.run(
231
+ "INSERT INTO skill_history (skill_name, version, action, content) VALUES (?, ?, ?, ?)",
232
+ [skillName, 1, "created", content],
233
+ );
234
+
235
+ // Update pattern with skill path
236
+ this.db.run(
237
+ "UPDATE task_patterns SET skill_path = ?, user_decision = 'accepted' WHERE signature = ?",
238
+ [skillPath, pattern.signature],
239
+ );
240
+
241
+ return skillName;
242
+ }
243
+
244
+ /**
245
+ * User rejects a suggested skill
246
+ */
247
+ rejectPattern(signature: string, permanently: boolean = false): void {
248
+ this.db.run(
249
+ "UPDATE task_patterns SET user_decision = ? WHERE signature = ?",
250
+ [permanently ? "never" : "rejected", signature],
251
+ );
252
+ }
253
+
254
+ /**
255
+ * Record a user preference
256
+ */
257
+ recordPreference(key: string, value: string, category = "general"): void {
258
+ this.db.run(
259
+ `INSERT OR REPLACE INTO user_preferences
260
+ (key, value, category, updated_at) VALUES (?, ?, ?, datetime('now'))`,
261
+ [key, value, category],
262
+ );
263
+ }
264
+
265
+ /**
266
+ * Get all learned patterns
267
+ */
268
+ getPatterns(): TaskPattern[] {
269
+ const rows = this.db.query("SELECT * FROM task_patterns ORDER BY last_seen DESC").all() as Record<string, unknown>[];
270
+ return rows.map((r) => ({
271
+ id: r.id as number,
272
+ signature: r.signature as string,
273
+ description: r.description as string,
274
+ toolSequence: JSON.parse(r.tool_sequence as string),
275
+ filesChanged: JSON.parse((r.files_changed as string) || "[]"),
276
+ commandsRun: JSON.parse((r.commands_run as string) || "[]"),
277
+ successCount: (r.success_count as number) || 0,
278
+ failureCount: (r.failure_count as number) || 0,
279
+ lastSeen: (r.last_seen as string) || new Date().toISOString(),
280
+ suggestedSkill: Boolean(r.suggested_skill),
281
+ userDecision: (r.user_decision as "pending" | "accepted" | "rejected" | "never") || "pending",
282
+ skillPath: r.skill_path as string | undefined,
283
+ }));
284
+ }
285
+
286
+ /**
287
+ * Get user preferences
288
+ */
289
+ getPreferences(): Record<string, string> {
290
+ const rows = this.db.query("SELECT key, value FROM user_preferences").all() as Record<string, unknown>[];
291
+ const prefs: Record<string, string> = {};
292
+ for (const r of rows) {
293
+ prefs[r.key as string] = r.value as string;
294
+ }
295
+ return prefs;
296
+ }
297
+
298
+ /**
299
+ * Rollback a skill to a previous version
300
+ */
301
+ rollbackSkill(skillName: string): boolean {
302
+ const history = this.db.query(
303
+ "SELECT * FROM skill_history WHERE skill_name = ? ORDER BY created_at DESC LIMIT 2",
304
+ ).all(skillName) as Record<string, unknown>[];
305
+
306
+ if (history.length < 2) return false;
307
+
308
+ const previousVersion = history[1];
309
+ const skillDir = join(this.skillsDir, skillName);
310
+ const skillPath = join(skillDir, "SKILL.md");
311
+
312
+ if (existsSync(skillPath)) {
313
+ writeFileSync(skillPath, previousVersion.content as string, "utf-8");
314
+ this.db.run(
315
+ "INSERT INTO skill_history (skill_name, version, action, content) VALUES (?, ?, ?, ?)",
316
+ [skillName, (previousVersion.version as number) + 1, "rollback", previousVersion.content as string],
317
+ );
318
+ return true;
319
+ }
320
+ return false;
321
+ }
322
+
323
+ /**
324
+ * Get storage statistics
325
+ */
326
+ getStats(): { patterns: number; preferences: number; skills: number; storageKB: number } {
327
+ const patterns = (this.db.query("SELECT COUNT(*) as c FROM task_patterns").get() as { c: number }).c;
328
+ const preferences = (this.db.query("SELECT COUNT(*) as c FROM user_preferences").get() as { c: number }).c;
329
+ const skills = (this.db.query("SELECT COUNT(*) as c FROM task_patterns WHERE skill_path IS NOT NULL").get() as { c: number }).c;
330
+
331
+ // Estimate storage: ~2KB per pattern, ~100B per preference
332
+ const storageKB = Math.round((patterns * 2 + preferences * 0.1) * 10) / 10;
333
+
334
+ return { patterns, preferences, skills, storageKB };
335
+ }
336
+ }
@@ -0,0 +1,213 @@
1
+ /**
2
+ * VORACODE Config — Configuration loader and manager
3
+ *
4
+ * Config loaded from (in priority order):
5
+ * 1. CLI flags (highest)
6
+ * 2. .voracode/config.json (project-level)
7
+ * 3. ~/.config/voracode/config.json (global, lowest)
8
+ */
9
+
10
+ import { existsSync, readFileSync, mkdirSync, writeFileSync } from "fs";
11
+ import { join, resolve } from "path";
12
+ import { homedir } from "os";
13
+
14
+ export interface VoraConfig {
15
+ version: string;
16
+ project?: {
17
+ name: string;
18
+ path: string;
19
+ };
20
+ model: {
21
+ provider: string;
22
+ name: string;
23
+ fallback: string[];
24
+ };
25
+ context: {
26
+ maxTokens: number;
27
+ compression: "auto" | "off" | "aggressive";
28
+ smartLoading: boolean;
29
+ cacheEnabled: boolean;
30
+ };
31
+ security: {
32
+ sandbox: {
33
+ enabled: boolean;
34
+ timeout: number;
35
+ maxOutput: number;
36
+ };
37
+ network: {
38
+ blocked: boolean;
39
+ allowedDomains: string[];
40
+ };
41
+ rateLimit: {
42
+ callsPerMinute: number;
43
+ tokensPerHour: number;
44
+ };
45
+ auditLog: {
46
+ enabled: boolean;
47
+ retentionDays: number;
48
+ };
49
+ };
50
+ skills: {
51
+ dirs: string[];
52
+ autoLearn: boolean;
53
+ };
54
+ plugins: string[];
55
+ telemetry: {
56
+ enabled: boolean;
57
+ localOnly: boolean;
58
+ };
59
+ }
60
+
61
+ const DEFAULT_CONFIG: VoraConfig = {
62
+ version: "1.0",
63
+ model: {
64
+ provider: "auto",
65
+ name: "auto",
66
+ fallback: ["deepseek", "groq"],
67
+ },
68
+ context: {
69
+ maxTokens: 128_000,
70
+ compression: "auto",
71
+ smartLoading: true,
72
+ cacheEnabled: true,
73
+ },
74
+ security: {
75
+ sandbox: {
76
+ enabled: true,
77
+ timeout: 30_000,
78
+ maxOutput: 1_048_576,
79
+ },
80
+ network: {
81
+ blocked: false,
82
+ allowedDomains: [
83
+ "api.openai.com",
84
+ "api.anthropic.com",
85
+ "api.deepseek.com",
86
+ "api.groq.com",
87
+ "api.openrouter.ai",
88
+ "api.github.com",
89
+ "api.huggingface.co",
90
+ "router.huggingface.co",
91
+ ],
92
+ },
93
+ rateLimit: {
94
+ callsPerMinute: 10,
95
+ tokensPerHour: 1_000_000,
96
+ },
97
+ auditLog: {
98
+ enabled: true,
99
+ retentionDays: 30,
100
+ },
101
+ },
102
+ skills: {
103
+ dirs: [
104
+ "~/.config/voracode/skills",
105
+ ".voracode/skills",
106
+ ],
107
+ autoLearn: false,
108
+ },
109
+ plugins: [],
110
+ telemetry: {
111
+ enabled: false,
112
+ localOnly: true,
113
+ },
114
+ };
115
+
116
+ export class ConfigManager {
117
+ private config: VoraConfig;
118
+ private globalPath: string;
119
+
120
+ constructor() {
121
+ this.globalPath = join(homedir(), ".config", "voracode", "config.json");
122
+ this.config = this.load();
123
+ }
124
+
125
+ private load(): VoraConfig {
126
+ // Start with defaults
127
+ const base = { ...DEFAULT_CONFIG };
128
+
129
+ // Load global config
130
+ const globalPath = join(homedir(), ".config", "voracode", "config.json");
131
+ if (existsSync(globalPath)) {
132
+ try {
133
+ const global = JSON.parse(readFileSync(globalPath, "utf-8"));
134
+ Object.assign(base, global);
135
+ } catch {
136
+ // Ignore invalid global config
137
+ }
138
+ }
139
+
140
+ // Load project config (cwd + .voracode)
141
+ const projectPath = join(process.cwd(), ".voracode", "config.json");
142
+ if (existsSync(projectPath)) {
143
+ try {
144
+ const project = JSON.parse(readFileSync(projectPath, "utf-8"));
145
+ Object.assign(base, project);
146
+ } catch {
147
+ // Ignore invalid project config
148
+ }
149
+ }
150
+
151
+ return base;
152
+ }
153
+
154
+ get(): VoraConfig {
155
+ return { ...this.config };
156
+ }
157
+
158
+ getGlobalPath(): string {
159
+ return this.globalPath;
160
+ }
161
+
162
+ ensureDefaults(): void {
163
+ const dir = join(homedir(), ".config", "voracode");
164
+ if (!existsSync(dir)) {
165
+ mkdirSync(dir, { recursive: true });
166
+ }
167
+ if (!existsSync(this.globalPath)) {
168
+ writeFileSync(this.globalPath, JSON.stringify(DEFAULT_CONFIG, null, 2));
169
+ }
170
+ }
171
+
172
+ set(key: string, value: unknown): void {
173
+ // Simple dot-path setter (e.g., "model.provider" = "deepseek")
174
+ const keys = key.split(".");
175
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
176
+ let target: any = this.config;
177
+ for (let i = 0; i < keys.length - 1; i++) {
178
+ if (!(keys[i] in target)) target[keys[i]] = {};
179
+ target = target[keys[i]];
180
+ }
181
+ target[keys[keys.length - 1]] = value;
182
+
183
+ // Persist
184
+ writeFileSync(this.globalPath, JSON.stringify(this.config, null, 2));
185
+ }
186
+
187
+ getProviderBaseUrl(provider: string): string {
188
+ const urls: Record<string, string> = {
189
+ openai: "https://api.openai.com/v1",
190
+ deepseek: "https://api.deepseek.com/v1",
191
+ groq: "https://api.groq.com/openai/v1",
192
+ together: "https://api.together.xyz/v1",
193
+ openrouter: "https://openrouter.ai/api/v1",
194
+ ollama: "http://localhost:11434/v1",
195
+ huggingface: "https://router.huggingface.co/v1",
196
+ fireworks: "https://api.fireworks.ai/inference/v1",
197
+ cerebras: "https://api.cerebras.ai/v1",
198
+ sambanova: "https://api.sambanova.ai/v1",
199
+ cloudflare: "https://api.cloudflare.com/client/v4/ai",
200
+ };
201
+ return urls[provider] || "";
202
+ }
203
+
204
+ detectProvider(modelName: string): string {
205
+ if (modelName.startsWith("gpt-") || modelName.startsWith("o3") || modelName.startsWith("o1")) return "openai";
206
+ if (modelName.startsWith("claude-") || modelName.startsWith("opus") || modelName.startsWith("sonnet")) return "anthropic";
207
+ if (modelName.startsWith("gemini-")) return "google";
208
+ if (modelName.startsWith("deepseek-")) return "deepseek";
209
+ if (modelName.startsWith("llama-") || modelName.startsWith("mixtral") || modelName.startsWith("qwen")) return "groq";
210
+ if (modelName.startsWith("command-")) return "cohere";
211
+ return "openai"; // default
212
+ }
213
+ }