From a3bc7620b115d52eb10712b2d3379a606282e7c1 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Tue, 17 Mar 2026 15:22:12 -0300 Subject: [PATCH] feat(integration): integrate ClawRouter services into active pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - intentClassifier → engine.ts selectProvider() When taskType is 'default', classifies prompt via multilingual keyword detection (9 langs) and uses detected intent (code/reasoning/simple/medium) for 6-factor task fitness scoring. - emergencyFallback → chatCore.ts error path (after T5 intra-family fallback) On HTTP 402 or budget-exhaustion keywords, attempts one redirect to nvidia/gpt-oss-120b ($0.00/M) before returning error to combo router. Skipped for streaming requests and tool-calling requests. - AutoComboConfig.routerStrategy field added Allows per-combo strategy override ('rules' | 'cost' | 'latency') Note: requestDedup was already integrated in chatCore.ts (line 387-430) Branch: feat/clawrouter-improvements --- open-sse/handlers/chatCore.ts | 62 +++++++++++++++++++++++++++ open-sse/services/autoCombo/engine.ts | 39 +++++++++++++++-- 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 879e28ec74..fd795ceb1f 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -43,6 +43,11 @@ import { getIdempotencyKey, checkIdempotency, saveIdempotency } from "@/lib/idem import { createProgressTransform, wantsProgress } from "../utils/progressTracker.ts"; import { isModelUnavailableError, getNextFamilyFallback } from "../services/modelFamilyFallback.ts"; import { computeRequestHash, deduplicate, shouldDeduplicate } from "../services/requestDedup.ts"; +import { + shouldUseFallback, + isFallbackDecision, + EMERGENCY_FALLBACK_CONFIG, +} from "../services/emergencyFallback.ts"; export function shouldUseNativeCodexPassthrough({ provider, @@ -641,6 +646,63 @@ export async function handleChatCore({ return createErrorResult(statusCode, errMsg, retryAfterMs); } // ── End T5 ─────────────────────────────────────────────────────────────── + + // ── Emergency Fallback (ClawRouter Feature #09/017) ──────────────────── + // When a non-streaming request fails with a budget-related error (402 or + // budget keywords), redirect to nvidia/gpt-oss-120b ($0.00/M) before + // returning the error to the combo router. This gives one last free-tier + // attempt so the user's session stays alive. + const requestHasTools = Array.isArray(translatedBody.tools) && translatedBody.tools.length > 0; + if (!stream) { + const fbDecision = shouldUseFallback( + statusCode, + message, + requestHasTools, + EMERGENCY_FALLBACK_CONFIG + ); + if (isFallbackDecision(fbDecision)) { + log?.info?.("EMERGENCY_FALLBACK", fbDecision.reason); + try { + // Build a minimal fallback request using the original body but with + // the NVIDIA free-tier model and max_tokens capped to avoid overuse. + const fbExecutor = getExecutor(fbDecision.provider); + const fbResult = await fbExecutor.execute({ + model: fbDecision.model, + body: { + ...translatedBody, + model: fbDecision.model, + max_tokens: Math.min( + typeof translatedBody.max_tokens === "number" + ? translatedBody.max_tokens + : fbDecision.maxOutputTokens, + fbDecision.maxOutputTokens + ), + }, + stream: false, + credentials: credentials, + signal: streamController.signal, + log, + extendedContext, + }); + if (fbResult.response.ok) { + providerResponse = fbResult.response; + log?.info?.( + "EMERGENCY_FALLBACK", + `Serving ${fbDecision.provider}/${fbDecision.model} as budget fallback for ${provider}/${model}` + ); + // Fall through to non-streaming handler — providerResponse is now OK + } else { + log?.warn?.( + "EMERGENCY_FALLBACK", + `Emergency fallback also failed (${fbResult.response.status})` + ); + } + } catch (fbErr) { + log?.warn?.("EMERGENCY_FALLBACK", `Emergency fallback error: ${fbErr?.message}`); + } + } + } + // ── End Emergency Fallback ──────────────────────────────────────────── } // Non-streaming response diff --git a/open-sse/services/autoCombo/engine.ts b/open-sse/services/autoCombo/engine.ts index c6cdde5428..48d316c86f 100644 --- a/open-sse/services/autoCombo/engine.ts +++ b/open-sse/services/autoCombo/engine.ts @@ -20,6 +20,7 @@ import { import { getTaskFitness } from "./taskFitness"; import { getModePack } from "./modePacks"; import { getSelfHealingManager } from "./selfHealing"; +import { classifyPromptIntent } from "../intentClassifier"; export interface AutoComboConfig { id: string; @@ -30,6 +31,8 @@ export interface AutoComboConfig { modePack?: string; budgetCap?: number; // max cost per request in USD explorationRate: number; // 0.05 = 5% exploratory + /** If set, RouterStrategy name to use for selection ('rules' | 'cost' | 'latency') */ + routerStrategy?: string; } export interface SelectionResult { @@ -43,14 +46,44 @@ export interface SelectionResult { /** * Select the best provider from an auto-combo pool. + * + * @param config - AutoCombo configuration + * @param candidates - Provider candidates to score + * @param taskType - Task type hint. When "default" or omitted, the engine will attempt + * to infer the intent from `promptMessages` using multilingual classification. + * @param promptMessages - Optional raw messages for intent classification */ export function selectProvider( config: AutoComboConfig, candidates: ProviderCandidate[], - taskType: string = "default" + taskType: string = "default", + promptMessages?: Array<{ role: string; content: unknown }> ): SelectionResult { const healer = getSelfHealingManager(); + // ── Intent classification (ClawRouter Feature #10/11) ──────────────────── + // When taskType is generic ('default'), attempt to classify the prompt intent + // using the multilingual intentClassifier for better task fitness scoring. + let effectiveTaskType = taskType; + if ((taskType === "default" || taskType === "") && promptMessages?.length) { + // Extract text from last user message for classification + const lastUserMsg = [...promptMessages].reverse().find((m) => m.role === "user"); + if (lastUserMsg) { + const text = + typeof lastUserMsg.content === "string" + ? lastUserMsg.content + : Array.isArray(lastUserMsg.content) + ? (lastUserMsg.content as Array<{ type: string; text?: string }>) + .filter((b) => b.type === "text") + .map((b) => b.text || "") + .join(" ") + : ""; + if (text.length > 10) { + const intent = classifyPromptIntent(text); + effectiveTaskType = intent; // 'code' | 'reasoning' | 'simple' | 'medium' + } + } + } // Resolve weights from mode pack or config let weights = config.weights; if (config.modePack) { @@ -80,8 +113,8 @@ export function selectProvider( excluded.length = 0; } - // Score all providers - const scored = scorePool(pool, taskType, weights, getTaskFitness); + // Score all providers (using classified intent if available) + const scored = scorePool(pool, effectiveTaskType, weights, getTaskFitness); // Apply self-healing re-evaluation with actual scores const finalCandidates = scored.filter((s) => {