| |
| |
| |
| |
| |
| |
|
|
|
|
| import { getCopilotSystemPrompt } from "./systemPrompt";
|
| import { COPILOT_TOOLS, getCopilotTool, getCopilotToolDescriptions } from "./tools";
|
|
|
|
|
|
|
| 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 }>;
|
| }
|
|
|
|
|
|
|
| const TOOL_NAMES = COPILOT_TOOLS.map((t) => t.name);
|
|
|
|
|
|
|
| function getKnowledgeResponse(query: string): string | null {
|
| const q = query.toLowerCase();
|
|
|
|
|
| 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).`;
|
| }
|
|
|
|
|
| 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.`;
|
| }
|
|
|
|
|
| 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.`;
|
| }
|
|
|
|
|
| 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\`.`;
|
| }
|
|
|
|
|
| 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;
|
| }
|
|
|
|
|
|
|
| const INTENT_PATTERNS: Array<{
|
| pattern: RegExp;
|
| tool: string;
|
| extractArgs: (match: RegExpMatchArray) => Record<string, unknown>;
|
| }> = [
|
|
|
| {
|
| 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, "") }),
|
| },
|
|
|
|
|
| { pattern: /list.*(?:combo|route)/i, tool: "listCombos", extractArgs: () => ({}) },
|
| { pattern: /show.*(?:combo|route)/i, tool: "listCombos", extractArgs: () => ({}) },
|
| { pattern: /qu[eΓ©].*combo/i, tool: "listCombos", extractArgs: () => ({}) },
|
|
|
|
|
| {
|
| pattern: /crea(?:te|r?)\s*(?:un\s*)?combo/i,
|
| tool: "createCombo",
|
| extractArgs: () => ({}),
|
| },
|
|
|
|
|
| { 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: () => ({}) },
|
|
|
|
|
| { pattern: /list.*(?:group|grupo)/i, tool: "listKeyGroups", extractArgs: () => ({}) },
|
| { pattern: /show.*(?:group|grupo)/i, tool: "listKeyGroups", extractArgs: () => ({}) },
|
|
|
|
|
|
|
| {
|
| 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] }),
|
| },
|
|
|
| {
|
| 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] }),
|
| },
|
|
|
| {
|
| 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] }),
|
| },
|
|
|
| {
|
| 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] }),
|
| },
|
|
|
| {
|
| pattern: /list.*(?:archivos|files|index)/i,
|
| tool: "listCodeGraphFiles",
|
| extractArgs: () => ({}),
|
| },
|
| { pattern: /codegraph.*(?:stats|stat|status)/i, tool: "codeGraphStats", extractArgs: () => ({}) },
|
|
|
|
|
| {
|
| pattern: /^(?:cli|terminal|ejecuta|run|exec)\s+(.+)/i,
|
| tool: "runOmniRouteCli",
|
| extractArgs: (m) => ({ command: m[1].trim() }),
|
| },
|
|
|
|
|
| {
|
| pattern: /^(?:health|status|salud|estado)$/i,
|
| tool: "runOmniRouteCli",
|
| extractArgs: () => ({ command: "health" }),
|
| },
|
|
|
|
|
| {
|
| 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;
|
| }
|
|
|
|
|
|
|
| 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()}`;
|
| }
|
|
|
|
|
|
|
| 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." };
|
| }
|
|
|
|
|
| const intent = classifyIntent(userText);
|
|
|
| if (!intent) {
|
|
|
| const knowledge = getKnowledgeResponse(userText);
|
| if (knowledge) {
|
| return { message: knowledge };
|
| }
|
|
|
| return {
|
| message: `I understand you want help with OmniRoute.\n\n${getHelpResponse()}`,
|
| };
|
| }
|
|
|
|
|
| if (intent.tool === "help") {
|
| return { message: getHelpResponse() };
|
| }
|
|
|
|
|
| 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"}]`,
|
| };
|
| }
|
|
|
|
|
| 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 }],
|
| };
|
| }
|
|
|
|
|
| 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 }],
|
| };
|
| }
|
|
|
|
|
| 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 }],
|
| };
|
| }
|
|
|