ai_api / src /lib /copilot /engine.ts
Yogesh
initial deploy
cd8bd0a
Raw
History Blame Contribute Delete
13.3 kB
/**
* 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 }],
};
}