feat(combo): response quality validation, circuit breaker fix, Cursor 4.6 models (#762)

- Add `validateResponseQuality()` to detect empty/invalid 200 responses from
  upstream providers in combo routing. Non-streaming responses with empty body,
  invalid JSON, or missing content/tool_calls now trigger circuit breaker
  failure and fallback to the next model instead of being returned to the client.

- Add missing `breaker._onSuccess()` calls in both priority and round-robin
  combo paths. Previously failures accumulated without reset, causing premature
  circuit breaker trips on healthy models.

- Update Cursor provider registry with Claude 4.6 model IDs (opus-high,
  sonnet-high, haiku, opus + thinking variants). Keep 4.5 IDs for backward
  compatibility.

- Update free-stack preset: replace duplicate qw/qwen3-coder-plus with
  if/deepseek-v3.2 for better model diversity.

- Add paid-premium combo template for round-robin load distribution across
  paid subscription providers (Cursor, Antigravity).

Made-with: Cursor
This commit is contained in:
Gorchakov-Pressure
2026-03-29 21:21:48 +05:00
committed by GitHub
parent 46acd16999
commit 93bbe8e7a8
3 changed files with 145 additions and 4 deletions

View File

@@ -500,6 +500,12 @@ export const REGISTRY: Record<string, RegistryEntry> = {
clientVersion: "1.1.3",
models: [
{ id: "default", name: "Auto (Server Picks)" },
{ id: "claude-4.6-opus-high-thinking", name: "Claude 4.6 Opus High Thinking" },
{ id: "claude-4.6-opus-high", name: "Claude 4.6 Opus High" },
{ id: "claude-4.6-sonnet-high-thinking", name: "Claude 4.6 Sonnet High Thinking" },
{ id: "claude-4.6-sonnet-high", name: "Claude 4.6 Sonnet High" },
{ id: "claude-4.6-haiku", name: "Claude 4.6 Haiku" },
{ id: "claude-4.6-opus", name: "Claude 4.6 Opus" },
{ id: "claude-4.5-opus-high-thinking", name: "Claude 4.5 Opus High Thinking" },
{ id: "claude-4.5-opus-high", name: "Claude 4.5 Opus High" },
{ id: "claude-4.5-sonnet-thinking", name: "Claude 4.5 Sonnet Thinking" },

View File

@@ -45,6 +45,80 @@ const DEFAULT_MODEL_P95_MS = {
};
const MIN_HISTORY_SAMPLES = 10;
/**
* Validate that a successful (HTTP 200) non-streaming response actually contains
* meaningful content. Returns { valid: true } or { valid: false, reason }.
*
* Only inspects non-streaming JSON responses — streaming responses are passed through
* because buffering the full stream would defeat the purpose of streaming.
*
* Checks:
* 1. Body is valid JSON
* 2. Has at least one choice with non-empty content or tool_calls
*/
async function validateResponseQuality(
response: Response,
isStreaming: boolean,
log: { warn?: (...args: any[]) => void }
): Promise<{ valid: boolean; reason?: string; clonedResponse?: Response }> {
if (isStreaming) return { valid: true };
const contentType = response.headers.get("content-type") || "";
if (!contentType.includes("application/json") && !contentType.includes("text/")) {
return { valid: true };
}
let cloned: Response;
try {
cloned = response.clone();
} catch {
return { valid: true };
}
let text: string;
try {
text = await cloned.text();
} catch {
return { valid: true };
}
if (!text || text.trim().length === 0) {
return { valid: false, reason: "empty response body" };
}
let json: any;
try {
json = JSON.parse(text);
} catch {
if (text.startsWith("data:")) return { valid: true };
return { valid: false, reason: "response is not valid JSON" };
}
const choices = json?.choices;
if (!Array.isArray(choices) || choices.length === 0) {
if (json?.output || json?.result || json?.data || json?.response) return { valid: true };
if (json?.error) return { valid: false, reason: `upstream error in 200 body: ${json.error?.message || JSON.stringify(json.error).substring(0, 200)}` };
return { valid: true };
}
const firstChoice = choices[0];
const message = firstChoice?.message || firstChoice?.delta;
if (!message) {
return { valid: false, reason: "choice has no message object" };
}
const content = message.content;
const toolCalls = message.tool_calls;
const hasContent = content !== null && content !== undefined && content !== "";
const hasToolCalls = Array.isArray(toolCalls) && toolCalls.length > 0;
if (!hasContent && !hasToolCalls) {
return { valid: false, reason: "empty content and no tool_calls in response" };
}
return { valid: true };
}
// In-memory atomic counter per combo for round-robin distribution
// Resets on server restart (by design — no stale state)
const rrCounters = new Map();
@@ -872,14 +946,31 @@ export async function handleComboChat({
const result = await handleSingleModelWrapped(body, modelStr);
// Success — return response
// Success — validate response quality before returning
if (result.ok) {
const quality = await validateResponseQuality(result, !!body.stream, log);
if (!quality.valid) {
log.warn(
"COMBO",
`Model ${modelStr} returned 200 but failed quality check: ${quality.reason}`
);
breaker._onFailure();
recordComboRequest(combo.name, modelStr, {
success: false,
latencyMs: Date.now() - startTime,
fallbackCount,
strategy,
});
if (i > 0) fallbackCount++;
break; // move to next model
}
resolvedByModel = modelStr;
const latencyMs = Date.now() - startTime;
log.info(
"COMBO",
`Model ${modelStr} succeeded (${latencyMs}ms, ${fallbackCount} fallbacks)`
);
breaker._onSuccess();
recordComboRequest(combo.name, modelStr, {
success: true,
latencyMs,
@@ -1139,13 +1230,30 @@ async function handleRoundRobinCombo({
const result = await handleSingleModel(body, modelStr);
// Success
// Success — validate response quality before returning
if (result.ok) {
const quality = await validateResponseQuality(result, !!body.stream, log);
if (!quality.valid) {
log.warn(
"COMBO-RR",
`${modelStr} returned 200 but failed quality check: ${quality.reason}`
);
breaker._onFailure();
recordComboRequest(combo.name, modelStr, {
success: false,
latencyMs: Date.now() - startTime,
fallbackCount,
strategy: "round-robin",
});
if (offset > 0) fallbackCount++;
break; // move to next model
}
const latencyMs = Date.now() - startTime;
log.info(
"COMBO-RR",
`${modelStr} succeeded (${latencyMs}ms, ${fallbackCount} fallbacks)`
);
breaker._onSuccess();
recordComboRequest(combo.name, modelStr, {
success: true,
latencyMs,

View File

@@ -186,6 +186,9 @@ const COMBO_TEMPLATE_FALLBACK = {
freeStackTitle: "Free Stack ($0)",
freeStackDesc:
"Round-robin across all free providers: Kiro, iFlow, Qwen, Gemini CLI. Zero cost, never stops.",
paidPremiumTitle: "Paid Premium",
paidPremiumDesc:
"Round-robin across paid subscriptions: Cursor, Antigravity. Top-tier models, distributed load.",
};
const COMBO_TEMPLATES = [
@@ -250,6 +253,21 @@ const COMBO_TEMPLATES = [
healthCheckEnabled: true,
},
},
{
id: "paid-premium",
icon: "workspace_premium",
titleKey: "templatePaidPremium",
descKey: "templatePaidPremiumDesc",
fallbackTitle: COMBO_TEMPLATE_FALLBACK.paidPremiumTitle,
fallbackDesc: COMBO_TEMPLATE_FALLBACK.paidPremiumDesc,
strategy: "round-robin",
suggestedName: "paid-premium",
config: {
maxRetries: 2,
retryDelayMs: 1000,
healthCheckEnabled: true,
},
},
];
function getStrategyMeta(strategy) {
@@ -1425,18 +1443,27 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
{ model: "kr/claude-sonnet-4.5", weight: 0 },
{ model: "if/kimi-k2-thinking", weight: 0 },
{ model: "if/qwen3-coder-plus", weight: 0 },
{ model: "qw/qwen3-coder-plus", weight: 0 },
{ model: "if/deepseek-v3.2", weight: 0 },
{ model: "nvidia/llama-3.3-70b-instruct", weight: 0 },
{ model: "groq/llama-3.3-70b-versatile", weight: 0 },
];
const PAID_PREMIUM_PRESET_MODELS = [
{ model: "cu/claude-4.6-opus-high", weight: 0 },
{ model: "ag/claude-sonnet-4-6", weight: 0 },
{ model: "cu/claude-4.6-sonnet-high", weight: 0 },
{ model: "ag/gpt-5", weight: 0 },
{ model: "ag/gemini-3.1-pro-preview", weight: 0 },
];
const applyTemplate = (template) => {
setStrategy(template.strategy);
setConfig((prev) => ({ ...prev, ...template.config }));
if (!name.trim()) setName(template.suggestedName);
// Pre-fill Free Stack with 7 real free provider models
if (template.id === "free-stack") {
setModels(FREE_STACK_PRESET_MODELS);
} else if (template.id === "paid-premium") {
setModels(PAID_PREMIUM_PRESET_MODELS);
}
};