Files
OmniRoute/src/lib/copilot/engine.ts
Diego Rodrigues de Sa e Souza 7c23dab64d Release v3.8.40
v3.8.40 cycle integration → main. All test gates green (Unit/Integration/Coverage/Node-compat/Quality-Ratchet). The only red check, 'PR Test Policy', is the test-masking heuristic firing on the cumulative ~57-commit release diff (legitimate assert consolidations already reviewed per-PR — Gemini CLI removal #5246, retired GPT models #5280, provider catalog refreshes); overridden with --admin per the documented release-PR convention. CodeQL/SonarQube advisory scans non-blocking; #5278's code already passed CodeQL on main. Homologated on VPS 192.168.0.15 (v3.8.40 healthy).
2026-06-29 08:40:06 -03:00

373 lines
13 KiB
TypeScript

/**
* 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, 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 }],
};
}