Skip to content
Merged
3 changes: 2 additions & 1 deletion file-size-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"open-sse/services/batchProcessor.ts": 828,
"open-sse/services/browserBackedChat.ts": 850,
"open-sse/services/claudeCodeCompatible.ts": 1202,
"open-sse/services/combo.ts": 5054,
"open-sse/services/combo.ts": 5131,
"open-sse/services/rateLimitManager.ts": 1017,
"open-sse/services/tokenRefresh.ts": 1997,
"open-sse/services/usage.ts": 3394,
Expand Down Expand Up @@ -111,5 +111,6 @@
"_rebaseline_2026_06_12_phase1n1s": "Phase 1n-1s (#3501): ProviderDetailPageClient.tsx 2554→1376 (extraídos ConnectionsListPanel, ConnectionsHeaderToolbar, ZedImportCard, BatchTestResultsModal, AdaptaTutorialModal + hooks/useApiKeySave + 4 helper closures→providerPageHelpers.ts). providerPageHelpers.ts 822→897 justificado: recebe 4 closures do god-component (getApiLabel/getApiDefaultPath/getApiPath/getHeaderIconProviderId), zero lógica nova, cliente encolhe mais do que helpers crescem.",
"_rebaseline_2026_06_12_phase1t": "Phase 1t (#3501): ProviderDetailPageClient.tsx 1377→782 — META ≤800 ATINGIDA (extraídos ProviderPageHeader, CompatibleNodeCard, ProviderModalsPanel, EmptyConnectionsPlaceholder, UpstreamProxyCard, SearchProviderCard + hooks useConnectionGate/useProviderNodeActions). Drift concorrente reconciliado: ResilienceTab/sse-chat/sse-auth/accountFallback/combo (merges #3629 model-lockout etc.).",
"_rebaseline_2026_06_12_v3823_new_features": "Re-baseline v3.8.23 pós-merge de #3742 (cost drilldown: ApiManagerPageClient.tsx +21, CostOverviewTab.tsx +14, providerLimits.ts +2, usage.ts +53) + #3743 (provider display modes: ProviderDetailPageClient.tsx +2, providerPageHelpers.ts +42, providers.ts +2) + #3740 (semantic cache key isolation: chat.ts +3). Crescimento justificado por features novas mergeadas no ciclo.",
"_rebaseline_2026_06_13_combo_quota_audit": "Re-baseline consciente do audit combo+quota (PR #3779): combo.ts 5054→5131 (+77). Crescimento = 5 fixes TDD + estratégia complexity-aware 2026 (W1 clampComboDepth + threading de maxDepth em 6 assinaturas/dispatch/DAG; W2 extração shouldSkipForPredictedTtft; W4 scoreAutoTargets exportado + param manifestHint). A parte limpa-extraível do W4 (construção do hint inline, ~30 linhas) FOI extraída para autoCombo/complexityRouter.ts (buildComplexityRoutingHint) — este +76 é o resíduo irredutível (edição de assinaturas/threading, não bloco movível). Shrink estrutural de combo.ts segue com #3501.",
"_rebaseline_2026_06_13_v3824_3776": "Re-baseline v3.8.24 pós #3776 (strict-mode controls Claude Code default models: ApiManagerPageClient.tsx 2701→2909 = UI de famílias bloqueáveis cc/* + chips; apiKeys.ts 1490→1633 = blocked_models deny-list + candidatos de permissão claude-code; schemas.ts 2515→2519 = reformatação Prettier + reasoningTokenBufferEnabled restaurado) + carry-over base.ts 1205→1218 do #3780 (enforceThinkingTemperature no chokepoint, drift de baseline não bumpado no merge). Crescimento de feature; sem god-component novo."
}
41 changes: 17 additions & 24 deletions open-sse/handlers/chatCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5227,21 +5227,15 @@ export async function handleChatCore({
// === Quota Share POST-hook (B/F7) — fire-and-forget, fail-open ===
if (apiKeyInfo?.id && credentials?.connectionId) {
try {
const { scheduleRecordConsumption } = await import("@/lib/quota/spendRecorder");
const { scheduleRecordConsumption, buildConsumptionCost } = await import(
"@/lib/quota/spendRecorder"
);
scheduleRecordConsumption(
{
apiKeyId: apiKeyInfo.id,
connectionId: credentials.connectionId,
provider: provider ?? "unknown",
cost: {
tokens:
usage && typeof usage === "object"
? (((usage as Record<string, unknown>).prompt_tokens as number) ?? 0) +
(((usage as Record<string, unknown>).completion_tokens as number) ?? 0)
: 0,
usd: estimatedCost > 0 ? estimatedCost : 0,
requests: 1,
},
cost: buildConsumptionCost(usage, estimatedCost),
},
log
);
Expand Down Expand Up @@ -5522,29 +5516,28 @@ export async function handleChatCore({
}

// === Quota Share POST-hook streaming (B/F7) — fire-and-forget, fail-open ===
// Resolve the real per-request cost (calculateCost) so USD-unit pools accrue
// on streaming traffic too; this previously recorded usd:0 hardcoded, which
// meant DeepSeek-style `usd/monthly` shared pools never blocked on streams.
if (apiKeyInfo?.id && credentials?.connectionId && streamStatus === 200) {
const su = streamUsage as Record<string, unknown> | null;
const quotaApiKeyId = apiKeyInfo.id;
const quotaConnectionId = credentials.connectionId;
// onStreamComplete is sync — use .then() (fire-and-forget, fail-open) instead of await
import("@/lib/quota/spendRecorder")
.then(({ scheduleRecordConsumption }) => {
scheduleRecordConsumption(
.then(({ recordStreamingConsumption }) =>
recordStreamingConsumption(
{
apiKeyId: quotaApiKeyId,
connectionId: quotaConnectionId,
provider: provider ?? "unknown",
cost: {
tokens: su
? (Number(su.prompt_tokens ?? 0) || 0) + (Number(su.completion_tokens ?? 0) || 0)
: 0,
usd: 0, // estimatedCost resolved async above; omit to avoid dependency
requests: 1,
},
provider,
model,
streamUsage,
streamStatus,
serviceTier: effectiveServiceTier,
},
log
);
})
{ calculateCost, log }
)
)
.catch(() => {
// Outer fail-open — never throws to caller
});
Expand Down
111 changes: 111 additions & 0 deletions open-sse/services/autoCombo/complexityRouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* complexityRouter.ts — Request-complexity classification for tier-aware routing.
*
* 2026 strategy: route by the intrinsic difficulty of the *request* (not only by
* provider stats), so trivial prompts can use cheap models and hard/reasoning
* prompts escalate to capable ones. Built on the existing specificity detector
* (codeComplexity / mathComplexity / reasoningDepth / contextSize / toolCalling
* / domainSpecificity), adding an explicit tool-use → minimum-tier escalation:
* a request carrying tool/function schemas (or agentic tool-calling signals)
* should not be routed below the "cheap" tier even when the prose looks
* trivial, because function-calling reliability matters more than raw cost.
*
* The classification maps to a `recommendedTier` that feeds the auto-router's
* tier-affinity / specificity-match scoring factors (see scoreAutoTargets,
* gated by config.complexityAwareRouting).
*/
import {
analyzeSpecificity,
getSpecificityLevel,
getRecommendedMinTier,
} from "../specificityDetector";
import type { RuleInput, SpecificityLevel } from "../specificityTypes";
import { generateRoutingHints, type RoutingHint } from "../manifestAdapter";

export type ComplexityTier = "free" | "cheap" | "premium";

export interface ComplexityClassification {
/** 0..100 specificity / difficulty score. */
score: number;
/** trivial | simple | moderate | complex | expert */
level: SpecificityLevel;
/** Minimum provider tier recommended for this request. */
recommendedTier: ComplexityTier;
/** True when the request carries tool/function schemas or agentic tool signals. */
hasToolUse: boolean;
/** Names of the specificity rules that fired (for the inspector / dashboard). */
signals: string[];
}

const TIER_ORDER: ComplexityTier[] = ["free", "cheap", "premium"];

/** Raise `tier` to at least `floor`; never lowers it. */
export function escalateTier(tier: ComplexityTier, floor: ComplexityTier): ComplexityTier {
return TIER_ORDER.indexOf(tier) >= TIER_ORDER.indexOf(floor) ? tier : floor;
}

/**
* Classify a request's complexity and recommend a minimum provider tier.
* Pure + dependency-light (no DB / network); safe on the hot path.
*/
export function classifyRequestComplexity(input: RuleInput): ComplexityClassification {
const result = analyzeSpecificity(input);
const level = getSpecificityLevel(result.score);

const explicitTools = Array.isArray(input.tools) && input.tools.length > 0;
const hasToolUse = explicitTools || result.breakdown.toolCalling > 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To prevent potential runtime TypeError exceptions if result.breakdown is undefined, use optional chaining and nullish coalescing to safely access toolCalling.

Suggested change
const hasToolUse = explicitTools || result.breakdown.toolCalling > 0;
const hasToolUse = explicitTools || (result.breakdown?.toolCalling ?? 0) > 0;


let recommendedTier = getRecommendedMinTier(level) as ComplexityTier;
// Tool-using / agentic requests need reliable function calling — floor at "cheap".
if (hasToolUse) recommendedTier = escalateTier(recommendedTier, "cheap");

return {
score: result.score,
level,
recommendedTier,
hasToolUse,
signals: result.rulesTriggered,
};
}

/**
* Build the opt-in complexity-aware routing hint for the auto-router. Returns a
* RoutingHint whose `recommendedMinTier` is escalated to the request's intrinsic
* complexity (and floored at "cheap" for tool-using requests), or `null` on any
* failure — fail-open, so scoring stays tier-neutral. Extracted from combo.ts to
* keep the complexity-routing logic in one module.
*/
export function buildComplexityRoutingHint(
modelTargets: Parameters<typeof generateRoutingHints>[0],
body: { messages?: unknown; tools?: unknown; model?: unknown } | null | undefined,
log: { info: (tag: string, message: string) => void }
): RoutingHint | null {
try {
const ruleInput = {
messages: Array.isArray(body?.messages)
? (body.messages as Array<{ role?: string; content?: string | unknown }>)
: [],
tools: Array.isArray(body?.tools)
? (body.tools as Array<{
function?: { name: string; description?: string; parameters?: unknown };
}>)
: undefined,
model: typeof body?.model === "string" ? body.model : undefined,
};
const hint = generateRoutingHints(modelTargets, ruleInput);
// Tool-use escalation: floor the recommended tier at "cheap" so scoring
// favors function-calling-reliable models for agentic requests.
const classification = classifyRequestComplexity(ruleInput);
hint.recommendedMinTier = escalateTier(
hint.recommendedMinTier as ComplexityTier,
classification.recommendedTier
) as typeof hint.recommendedMinTier;
log.info(
"COMBO",
`Complexity-aware routing: level=${classification.level} score=${classification.score} minTier=${hint.recommendedMinTier} tools=${classification.hasToolUse}`
);
return hint;
} catch {
return null; // fail-open: scoring stays tier-neutral
}
}
51 changes: 29 additions & 22 deletions open-sse/services/autoCombo/scoring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
*/

import type { RoutingHint } from "../manifestAdapter";
import { clamp01 } from "../../utils/number";
import { classifyTier } from "../tierResolver";

export interface ScoringFactors {
quota: number;
Expand Down Expand Up @@ -86,19 +88,22 @@ export interface ScoredProvider {
* Supports tierAffinity + specificityMatch weights when manifest routing is enabled.
*/
export function calculateScore(factors: ScoringFactors, weights: ScoringWeights): number {
return (
// clamp01 bounds the result to [0,1] and maps a non-finite sum (a NaN factor)
// to 0, so a single bad input can't yield NaN (which sorts nondeterministically)
// or a score >1 from float drift in weights that nominally sum to 1.
return clamp01(
weights.quota * factors.quota +
weights.health * factors.health +
weights.costInv * factors.costInv +
weights.latencyInv * factors.latencyInv +
weights.taskFit * factors.taskFit +
weights.stability * factors.stability +
weights.tierPriority * factors.tierPriority +
(weights.tierAffinity ?? 0) * factors.tierAffinity +
(weights.specificityMatch ?? 0) * factors.specificityMatch +
(weights.contextAffinity ?? 0) * factors.contextAffinity +
(weights.resetWindowAffinity ?? 0) * factors.resetWindowAffinity +
(weights.connectionDensity ?? 0) * factors.connectionDensity
weights.health * factors.health +
weights.costInv * factors.costInv +
weights.latencyInv * factors.latencyInv +
weights.taskFit * factors.taskFit +
weights.stability * factors.stability +
weights.tierPriority * factors.tierPriority +
(weights.tierAffinity ?? 0) * factors.tierAffinity +
(weights.specificityMatch ?? 0) * factors.specificityMatch +
(weights.contextAffinity ?? 0) * factors.contextAffinity +
(weights.resetWindowAffinity ?? 0) * factors.resetWindowAffinity +
(weights.connectionDensity ?? 0) * factors.connectionDensity
);
}

Expand Down Expand Up @@ -131,7 +136,6 @@ function calculateTierAffinity(
): number {
if (!hint) return 0.5;
try {
const { classifyTier } = require("../tierResolver");
const assignment = classifyTier(candidate.provider, candidate.model);
const tierOrder = ["free", "cheap", "premium"];
const providerTierIdx = tierOrder.indexOf(assignment.tier);
Expand All @@ -151,7 +155,6 @@ function calculateSpecificityMatch(
): number {
if (!hint) return 0.5;
try {
const { classifyTier } = require("../tierResolver");
const assignment = classifyTier(candidate.provider, candidate.model);
const specificityScore = hint.specificity.score;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Use optional chaining to safely access hint.specificity.score to prevent a potential TypeError if hint.specificity is undefined.

Suggested change
const specificityScore = hint.specificity.score;
const specificityScore = hint.specificity?.score;
if (specificityScore === undefined) return 0.5;


Expand All @@ -176,24 +179,28 @@ export function calculateFactors(
const maxLatency = Math.max(...pool.map((p) => p.p95LatencyMs), 1);
const maxStdDev = Math.max(...pool.map((p) => p.latencyStdDev), 0.001);

// Every factor is contractually [0,1]. clamp01 guards against bad telemetry
// (negative quota / cost / latency, NaN, out-of-range candidate-supplied
// affinities) so a single bad input can't produce a negative or >1 factor
// that distorts the weighted score.
return {
quota: Math.min(1, candidate.quotaRemaining / 100),
quota: clamp01(candidate.quotaRemaining / 100),
health:
candidate.circuitBreakerState === "CLOSED"
? 1.0
: candidate.circuitBreakerState === "HALF_OPEN"
? 0.5
: 0.0,
costInv: 1 - candidate.costPer1MTokens / maxCost,
latencyInv: 1 - candidate.p95LatencyMs / maxLatency,
taskFit: getTaskFitness(candidate.model, taskType),
stability: 1 - candidate.latencyStdDev / maxStdDev,
costInv: clamp01(1 - candidate.costPer1MTokens / maxCost),
latencyInv: clamp01(1 - candidate.p95LatencyMs / maxLatency),
taskFit: clamp01(getTaskFitness(candidate.model, taskType)),
stability: clamp01(1 - candidate.latencyStdDev / maxStdDev),
tierPriority: calculateTierScore(candidate.accountTier, candidate.quotaResetIntervalSecs),
tierAffinity: calculateTierAffinity(candidate, manifestHint),
specificityMatch: calculateSpecificityMatch(candidate, manifestHint),
contextAffinity: candidate.contextAffinity ?? 0.5,
resetWindowAffinity: candidate.resetWindowAffinity ?? 0.5,
connectionDensity: Math.min(1, Math.max(0, ((candidate.connectionPoolSize ?? 1) - 1) / 10)),
contextAffinity: clamp01(candidate.contextAffinity ?? 0.5),
resetWindowAffinity: clamp01(candidate.resetWindowAffinity ?? 0.5),
connectionDensity: clamp01(((candidate.connectionPoolSize ?? 1) - 1) / 10),
};
}

Expand Down
Loading