File size: 13,695 Bytes
bc4a7e8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
/**

 * OmniRoute Copilot β€” Chat Engine

 *

 * Processes user messages, classifies intent, executes tools,

 * queries CodeGraph, invokes CLI commands, or responds with

 * knowledge from the system prompt.

 */

import { getCopilotSystemPrompt } from "./systemPrompt";
import { COPILOT_TOOLS, getCopilotTool, getCopilotToolDescriptions } from "./tools";

// ── Types ────────────────────────────────────────────────────────────────────

export interface CopilotMessage {
  role: "user" | "assistant" | "system";
  content: string;
}

export interface CopilotRequest {
  messages: CopilotMessage[];
}

export interface CopilotResponse {
  message: string;
  toolCalls?: Array<{ name: string; args: Record<string, unknown>; result: string }>;
}

// ── Tool Lookup for Dynamic Dispatch ────────────────────────────────────────

const TOOL_NAMES = COPILOT_TOOLS.map((t) => t.name);

// ── Knowledge-based responses ───────────────────────────────────────────────

function getKnowledgeResponse(query: string): string | null {
  const q = query.toLowerCase();

  // Architecture questions
  if (
    /architecture|arquitectura|pipeline/.test(q) ||
    (q.includes("request") && (q.includes("flow") || q.includes("path")))
  ) {
    return `## OmniRoute Architecture



The request pipeline flows through:

1. **API Route** β†’ CORS β†’ Zod validation β†’ Auth (optional)

2. **Guardrails** β†’ Prompt injection guard, PII masking

3. **Pre-request Middleware Hooks** (NEW) β€” mutate routing decisions

4. **Task-aware routing / Combo resolution** β€” picks the target

5. **Cache check** β€” semantic/signature cache

6. **Rate limit check**

7. **Request translation** β€” OpenAI format β†’ provider format

8. **Executor** β€” build URL + headers, fetch with retry

9. **Response translation** β€” provider format β†’ client format

10. **SSE stream or JSON response**



The data layer uses **SQLite** via 45+ domain modules in \`src/lib/db/\`.

The streaming engine lives in \`open-sse/\` (handlers, executors, translator).`;
  }

  // Combo questions
  if (/combo|routing|strategy|estrategia/.test(q)) {
    return `## Combo Routing



Combos chain multiple targets (provider+model) with a strategy:



**14 strategies available:**

- \`priority\`: Try targets in order, fall through on failure

- \`weighted\`: Distribute load by weight

- \`round-robin\`: Cycle through targets

- \`auto\`: Intelligent selection (rules, cost, latency, eco, fast, LKGP)

- \`fill-first\`: Fill capacity of first target

- \`cost-optimized\`: Minimize cost

- \`context-optimized\`: Maximize context window

- \`p2c\`: Power of Two Choices

- \`random\` / \`strict-random\`: Random selection

- \`least-used\`: Load balance by usage

- \`reset-aware\`: Account for API reset windows

- \`context-relay\`: Relay context between models

- \`lkgp\`: Last Known Good Provider



Use \`createCombo\` tool or \`runOmniRouteCli\` to create them.`;
  }

  // Provider questions
  if (/provider|proveedor/.test(q)) {
    return `## Providers (212+)



OmniRoute supports 212+ providers across categories:

- **Free**: Qoder AI, Qwen Code, Gemini CLI, Kiro AI

- **OAuth** (14): Claude Code, Antigravity, Codex, GitHub Copilot, Cursor, Kimi Coding, Windsurf, etc.

- **API Key** (120+): OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, etc.

- **Self-Hosted** (8+): LM Studio, vLLM, Ollama, Triton, etc.

- **Custom**: \`openai-compatible-*\` and \`anthropic-compatible-*\`



Use \`listProviders\` to see your configured ones.`;
  }

  // Debugging/troubleshooting
  if (/debug|error|fail|fallo|problema|issue|crash|log/.test(q)) {
    return `## Troubleshooting



**Common issues:**



1. **Provider returns errors**: Check credentials with the health API (\`/api/monitoring/health\`)

2. **Combo targeting wrong provider**: Check strategy and targets with \`listCombos\`

3. **Rate limiting**: Check circuit breaker state via health API

4. **Auth errors**: Verify API key scopes with \`listApiKeys\`

5. **DeepSeek 400 errors**: Likely \`reasoning_content\` stripping issue β€” fixed in schemaCoercion.ts



**Use CodeGraph** to investigate specific code paths with \`searchCodeGraph\`.`;
  }

  // CodeGraph questions
  if (/codigo|cΓ³digo|codebase|cΓ³mo funciona|how does|where is|dΓ³nde estΓ‘/.test(q)) {
    return `## Codebase Investigation



I can use CodeGraph to explore the OmniRoute codebase. Just ask me:

- "Busca la funciΓ³n handleChatCore"

- "QuiΓ©n llama a sanitizeMessage?"

- "QuΓ© funciones hay en combo.ts?"

- "Dame contexto del archivo chatCore.ts"

- "Lista los archivos TypeScript indexados"



Use these search terms naturally and I'll query the CodeGraph index.`;
  }

  return null;
}

// ── Intent Classification ────────────────────────────────────────────────────

const INTENT_PATTERNS: Array<{
  pattern: RegExp;
  tool: string;
  extractArgs: (match: RegExpMatchArray) => Record<string, unknown>;
}> = [
  // ── Provider tools ──
  {
    pattern: /list.*(?:providers?|connections?|accounts)/i,
    tool: "listProviders",
    extractArgs: () => ({}),
  },
  {
    pattern: /list.*(oauth|api.?key|free|local).*provider/i,
    tool: "listProviders",
    extractArgs: (m) => ({ type: (m[1] || "").toLowerCase().replace(/[^a-z]/g, "") }),
  },

  // ── Combo tools ──
  { pattern: /list.*(?:combo|route)/i, tool: "listCombos", extractArgs: () => ({}) },
  { pattern: /show.*(?:combo|route)/i, tool: "listCombos", extractArgs: () => ({}) },
  { pattern: /qu[eΓ©].*combo/i, tool: "listCombos", extractArgs: () => ({}) },

  // Create combo
  {
    pattern: /crea(?:te|r?)\s*(?:un\s*)?combo/i,
    tool: "createCombo",
    extractArgs: () => ({}),
  },

  // ── API Key tools ──
  { pattern: /list.*(?:api.?key|key)/i, tool: "listApiKeys", extractArgs: () => ({}) },
  { pattern: /show.*(?:api.?key|key)/i, tool: "listApiKeys", extractArgs: () => ({}) },
  {
    pattern: /crea(?:te|r?)\s*(?:un\s*)?(?:api.?)?key/i,
    tool: "createApiKey",
    extractArgs: () => ({}),
  },
  { pattern: /revoke|revocar|borrar.*key/i, tool: "revokeApiKey", extractArgs: () => ({}) },

  // ── Key Group tools ──
  { pattern: /list.*(?:group|grupo)/i, tool: "listKeyGroups", extractArgs: () => ({}) },
  { pattern: /show.*(?:group|grupo)/i, tool: "listKeyGroups", extractArgs: () => ({}) },

  // ── CodeGraph tools ──
  // Search symbols
  {
    pattern:
      /(?:busca|search|find|dΓ³nde estΓ‘|where is)\s*(?:el\s*)?(?:sΓ­mbolo|symbol|function|funciΓ³n|class|clase)?\s*[`"']?([a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*)[`"']?/i,
    tool: "searchCodeGraph",
    extractArgs: (m) => ({ query: m[1] }),
  },
  // Callers
  {
    pattern:
      /(?:qui[Γ©e]n|who|what)\s*(?:llama|call|usa|use|referenc).*[`"']?([a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*)[`"']?/i,
    tool: "findCallers",
    extractArgs: (m) => ({ symbol: m[1] }),
  },
  // Callees
  {
    pattern:
      /(?:quΓ©|what|que)\s*(?:llama|call|usa)\s*[`"']?([a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*)[`"']?/i,
    tool: "findCallees",
    extractArgs: (m) => ({ symbol: m[1] }),
  },
  // File context
  {
    pattern:
      /(?:contexto|context|sΓ­mbolos|symbols|funciones|functions)\s*(?:de|in|del|en)\s*[`"']?([a-zA-Z0-9_/.-]+(?:\.\w+)?)[`"']?/i,
    tool: "getFileContext",
    extractArgs: (m) => ({ filePath: m[1] }),
  },
  // Files
  {
    pattern: /list.*(?:archivos|files|index)/i,
    tool: "listCodeGraphFiles",
    extractArgs: () => ({}),
  },
  { pattern: /codegraph.*(?:stats|stat|status)/i, tool: "codeGraphStats", extractArgs: () => ({}) },

  // ── CLI executor ──
  {
    pattern: /^(?:cli|terminal|ejecuta|run|exec)\s+(.+)/i,
    tool: "runOmniRouteCli",
    extractArgs: (m) => ({ command: m[1].trim() }),
  },

  // ── Health / status ──
  {
    pattern: /^(?:health|status|salud|estado)$/i,
    tool: "runOmniRouteCli",
    extractArgs: () => ({ command: "health" }),
  },

  // ── Help ──
  {
    pattern: /^(?:help|ayuda|que puedes hacer|quΓ© puedes hacer|\?)$/i,
    tool: "help",
    extractArgs: () => ({}),
  },
];

function classifyIntent(text: string): { tool: string; args: Record<string, unknown> } | null {
  for (const intent of INTENT_PATTERNS) {
    const match = text.match(intent.pattern);
    if (match) {
      return { tool: intent.tool, args: intent.extractArgs(match) };
    }
  }
  return null;
}

// ── Help Response ────────────────────────────────────────────────────────────

function getHelpResponse(): string {
  return `## OmniRoute Copilot β€” Comandos disponibles



### ConfiguraciΓ³n

- "Lista los providers" β†’ \`listProviders\`

- "Lista mis combos" β†’ \`listCombos\`

- "Crea un combo..." β†’ \`createCombo\` (te pedirΓ© detalles)

- "Lista las API keys" β†’ \`listApiKeys\`

- "Crea una API key para desarrollo" β†’ \`createApiKey\`

- "Revoca la key abc123" β†’ \`revokeApiKey\`

- "Lista los grupos" β†’ \`listKeyGroups\`



### CodeGraph (investigar el cΓ³digo)

- "Busca la funciΓ³n handleChatCore" β†’ \`searchCodeGraph\`

- "QuiΓ©n llama a sanitizeMessage?" β†’ \`findCallers\`

- "QuΓ© funciones hay en combo.ts?" β†’ \`getFileContext\`

- "Lista los archivos indexados" β†’ \`listCodeGraphFiles\`



### CLI

- "CLI health" β†’ ejecuta \`omniroute health\`

- "CLI list-combos" β†’ ejecuta \`omniroute list-combos\`

- "CLI set-budget 10" β†’ ejecuta \`omniroute set-budget 10\`



### Conocimiento

- "CΓ³mo funciona OmniRoute?" β†’ explica la arquitectura

- "QuΓ© son los combos?" β†’ explica routing

- "CΓ³mo debuggeo un error?" β†’ troubleshooting



### Tools disponibles:\n\n${getCopilotToolDescriptions()}`;
}

// ── Chat Engine ──────────────────────────────────────────────────────────────

export async function processCopilotChat(request: CopilotRequest): Promise<CopilotResponse> {
  const lastMessage = request.messages[request.messages.length - 1];
  if (!lastMessage || lastMessage.role !== "user") {
    return { message: "No user message found." };
  }

  const userText = lastMessage.content.trim();
  if (!userText) {
    return { message: "Please provide a message." };
  }

  // Classify intent
  const intent = classifyIntent(userText);

  if (!intent) {
    // No tool match β€” check knowledge base
    const knowledge = getKnowledgeResponse(userText);
    if (knowledge) {
      return { message: knowledge };
    }
    // Fallback: respond with help
    return {
      message: `I understand you want help with OmniRoute.\n\n${getHelpResponse()}`,
    };
  }

  // Handle help separately
  if (intent.tool === "help") {
    return { message: getHelpResponse() };
  }

  // Handle tools that need more info from the user
  if (intent.tool === "createCombo" && !userText.includes("{") && !userText.includes("target")) {
    return {
      message: `Para crear un combo, necesito algunos detalles:



1. **Nombre** del combo (ej: "mi-combo-fallback")

2. **Estrategia** (priority, weighted, round-robin, cost-optimized, auto)

3. **Targets** β€” los proveedores/modelos en orden



Puedes decirme algo como:

> Crea un combo llamado "fallback-claude" con estrategia priority y targets: [{"provider":"claude-code","model":"claude-sonnet-4"},{"provider":"openai","model":"gpt-4o"}]`,
    };
  }

  // Handle createApiKey β€” extract name from sentence
  if (intent.tool === "createApiKey") {
    const nameMatch = userText.match(
      /(?:llamad[oa]|named?|par[ae]?)\s*["'']?([a-zA-Z0-9_-]+)["'']?/i
    );
    const name = nameMatch ? nameMatch[1] : "copilot-key";
    const scopeMatch = userText.match(/(?:con\s*)?scope?s?\s*:?\s*["'']?([a-zA-Z,]+)["'']?/i);
    const scopes = scopeMatch ? scopeMatch[1] : undefined;

    const tool = getCopilotTool("createApiKey");
    if (!tool) return { message: "Error: createApiKey tool not found." };

    const result = await tool.handler({
      name,
      machineId: "copilot",
      scopes,
    });

    return {
      message: result,
      toolCalls: [{ name: "createApiKey", args: { name, scopes }, result }],
    };
  }

  // Handle CLI executor β€” pass the full command
  if (intent.tool === "runOmniRouteCli") {
    const tool = getCopilotTool("runOmniRouteCli");
    if (!tool) return { message: "Error: CLI executor not found." };

    const result = await tool.handler(intent.args);
    return {
      message: result,
      toolCalls: [{ name: "runOmniRouteCli", args: intent.args, result }],
    };
  }

  // For all other tools, dispatch directly
  const tool = getCopilotTool(intent.tool);
  if (!tool)
    return {
      message: `I don't have a tool for that yet. Try asking in a different way.\n\n${getHelpResponse()}`,
    };

  const result = await tool.handler(intent.args);
  return {
    message: result,
    toolCalls: [{ name: intent.tool, args: intent.args, result }],
  };
}