mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
feat(integration): integrate ClawRouter services into active pipeline
- 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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user