mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
refactor(combo): ComboContext + extract phaseComboSetup (god-file split fase 1) (#4326)
Primeiro incremento da decomposição do god-file combo.ts (2604 LOC). Extrai o bloco de setup do handleComboChat (strategy/relay/resilience/universal-handoff/ context-cache pinning/agent middleware/config cascade/timeout) para phaseComboSetup(ctx), com ComboContext carregando o body mutável compartilhado. - combo/context.ts: ComboContext (carrier do body mutável) + createComboContext. - combo/comboSetup.ts: phaseComboSetup(ctx): ComboSetup (byte-equivalente ao bloco inline; reescreve ctx.body no pin + middleware, retorna os locals derivados). - comboConfig.ts: resolveComboSetupConfig (DRY — encapsula o ternário do config, tipo único p/ ComboContext.config sem cast). - combo.ts: -47 linhas; rebinda os locals do ctx, resto de handleComboChat intacto; remove 6 imports que ficaram órfãos. Comportamento preservado: 357 testes de caracterização combo seguem verdes (+4 novos), integração sse-correctness 5/5, typecheck 0, file-size encolhe (sem _rebaseline). Fases 2-N = sub-planos follow-up. Ver _tasks/quality/2026-06-19-DESIGN-godfiles-decomposition.md.
This commit is contained in:
committed by
GitHub
parent
5cca4ff90c
commit
fbb423f32b
@@ -18,7 +18,7 @@ import {
|
||||
isProviderExhaustedReason,
|
||||
hasPerModelQuota,
|
||||
} from "./accountFallback.ts";
|
||||
import { FETCH_TIMEOUT_MS, RateLimitReason } from "../config/constants.ts";
|
||||
import { RateLimitReason } from "../config/constants.ts";
|
||||
import { errorResponse, unavailableResponse } from "../utils/error.ts";
|
||||
import {
|
||||
recordComboIntent,
|
||||
@@ -29,14 +29,11 @@ import {
|
||||
import {
|
||||
resolveComboConfig,
|
||||
getDefaultComboConfig,
|
||||
resolveComboTargetTimeoutMs,
|
||||
} from "./comboConfig.ts";
|
||||
import {
|
||||
maybeGenerateHandoff,
|
||||
resolveContextRelayConfig,
|
||||
maybeGenerateUniversalHandoff,
|
||||
injectUniversalHandoffBody,
|
||||
resolveUniversalHandoffConfig,
|
||||
SKIP_UNIVERSAL_HANDOFF_FLAG,
|
||||
type MessageLike,
|
||||
} from "./contextHandoff.ts";
|
||||
@@ -53,7 +50,8 @@ import * as semaphore from "./rateLimitSemaphore.ts";
|
||||
import { getCircuitBreaker } from "../../src/shared/utils/circuitBreaker";
|
||||
import { fisherYatesShuffle, getNextFromDeck } from "../../src/shared/utils/shuffleDeck";
|
||||
import { parseModel } from "./model.ts";
|
||||
import { applyComboAgentMiddleware } from "./comboAgentMiddleware.ts";
|
||||
import { createComboContext } from "./combo/context.ts";
|
||||
import { phaseComboSetup } from "./combo/comboSetup.ts";
|
||||
import { checkCredentialGate, logCredentialSkip } from "./credentialGate.ts";
|
||||
import { emit } from "../../src/lib/events/eventBus";
|
||||
import { notifyWebhookEvent } from "../../src/lib/webhookDispatcher";
|
||||
@@ -76,7 +74,6 @@ import type { RoutingHint } from "./manifestAdapter";
|
||||
import { buildComplexityRoutingHint } from "./autoCombo/complexityRouter";
|
||||
import type { CompressionMode } from "./compression/types.ts";
|
||||
import { getProviderConnections } from "../../src/lib/db/providers";
|
||||
import { normalizeRoutingStrategy } from "../../src/shared/constants/routingStrategies.ts";
|
||||
import {
|
||||
isProviderInCooldown,
|
||||
recordProviderCooldown,
|
||||
@@ -148,7 +145,6 @@ import {
|
||||
applyRequestTagRouting,
|
||||
scoreAutoTargets,
|
||||
expandAutoComboCandidatePool,
|
||||
deriveComboSessionKey,
|
||||
} from "./combo/autoStrategy.ts";
|
||||
import {
|
||||
resolveResetWindowConfig,
|
||||
@@ -426,67 +422,24 @@ export async function handleComboChat({
|
||||
signal,
|
||||
apiKeyAllowedConnections = null,
|
||||
}: HandleComboChatOptions): Promise<Response> {
|
||||
const strategy = normalizeRoutingStrategy(combo.strategy || "priority");
|
||||
const relayConfig =
|
||||
strategy === "context-relay" ? resolveContextRelayConfig(relayOptions?.config || null) : null;
|
||||
|
||||
const resilienceSettings: ResilienceSettings = settings
|
||||
? resolveResilienceSettings(settings)
|
||||
: resolveResilienceSettings(null);
|
||||
|
||||
const universalHandoffConfig = resolveUniversalHandoffConfig(
|
||||
(combo.universal_handoff || combo.universalHandoff) as
|
||||
| Record<string, unknown>
|
||||
| null
|
||||
| undefined,
|
||||
relayOptions?.universalHandoffConfig as Record<string, unknown> | null | undefined
|
||||
);
|
||||
// ── Server-side context cache pinning (replaces <omniModel> tag roundtrip) ─
|
||||
// Uses session_model_history — no client-side tag injection, no visible output pollution.
|
||||
//
|
||||
// #3825: when the client sends no session id (most OpenAI-compatible clients), fall
|
||||
// back to a stable conversation fingerprint derived from the body so the combo still
|
||||
// re-pins to the same model across turns. ONLY engaged when context_cache_protection
|
||||
// is truthy — when the toggle is off, behavior is unchanged (combos rotate as before,
|
||||
// no pin read/write, no <omniModel> tag).
|
||||
const effectiveSessionId: string | null = combo.context_cache_protection
|
||||
? (relayOptions?.sessionId ?? deriveComboSessionKey(body))
|
||||
: null;
|
||||
let pinnedModel: string | null = null;
|
||||
if (
|
||||
combo.context_cache_protection &&
|
||||
effectiveSessionId &&
|
||||
!(body as Record<string, unknown>)?.[SKIP_UNIVERSAL_HANDOFF_FLAG]
|
||||
) {
|
||||
const pinned = getLastSessionModel(effectiveSessionId, combo.name);
|
||||
if (pinned) {
|
||||
body = { ...body, model: pinned };
|
||||
pinnedModel = pinned;
|
||||
log.info("COMBO", `[#401] Context cache: pinned model=${pinned} (server-side)`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Combo Agent Middleware (#399 + #401) ────────────────────────────────
|
||||
// Apply system_message override, tool_filter_regex.
|
||||
// Context cache pinning is handled above via session_model_history.
|
||||
const { body: agentBody } = applyComboAgentMiddleware(
|
||||
body,
|
||||
combo,
|
||||
"" // provider/model not yet known — resolved per-model in loop
|
||||
);
|
||||
body = agentBody;
|
||||
const clientRequestedStream = body?.stream === true;
|
||||
// Context cache pinning is handled above via server-side session_model_history.
|
||||
// No tag injection on response — use handleSingleModel directly.
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// Use config cascade before dispatch so all strategies, pinned context routes,
|
||||
// and round-robin targets share the same timeout policy.
|
||||
const config = settings
|
||||
? resolveComboConfig(combo, settings)
|
||||
: { ...getDefaultComboConfig(), ...(combo.config || {}) };
|
||||
const comboTargetTimeoutMs = resolveComboTargetTimeoutMs(config, FETCH_TIMEOUT_MS);
|
||||
const reasoningTokenBufferEnabled = config.reasoningTokenBufferEnabled !== false;
|
||||
// Combo setup phase (god-file decomposition fase 1): strategy / relay / resilience /
|
||||
// universal-handoff / context-cache pinning / agent middleware / config cascade / timeout.
|
||||
// phaseComboSetup rewrites ctx.body (pinning + middleware); rebind `body` from it so the
|
||||
// rest of handleComboChat is unchanged. See combo/comboSetup.ts + combo/context.ts.
|
||||
const comboCtx = createComboContext({ body, combo, settings, relayOptions, log });
|
||||
const {
|
||||
strategy,
|
||||
relayConfig,
|
||||
resilienceSettings,
|
||||
universalHandoffConfig,
|
||||
effectiveSessionId,
|
||||
pinnedModel,
|
||||
clientRequestedStream,
|
||||
config,
|
||||
comboTargetTimeoutMs,
|
||||
reasoningTokenBufferEnabled,
|
||||
} = phaseComboSetup(comboCtx);
|
||||
body = comboCtx.body;
|
||||
|
||||
// ── Per-model timeout wrapper ────────────────────────────────────────────
|
||||
// Combo target timeouts inherit FETCH_TIMEOUT_MS by default. Operators can
|
||||
|
||||
115
open-sse/services/combo/comboSetup.ts
Normal file
115
open-sse/services/combo/comboSetup.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* phaseComboSetup — the first extracted phase of handleComboChat (combo.ts).
|
||||
*
|
||||
* Resolves the per-request combo setup BEFORE dispatch: routing strategy, context-relay
|
||||
* config, resilience settings, universal-handoff config, server-side context-cache pinning
|
||||
* (which rewrites ctx.body), combo agent middleware (also rewrites ctx.body), the config
|
||||
* cascade, the per-target timeout, and the reasoning-token-buffer flag. Behaviour is
|
||||
* byte-identical to the inline block it replaces — it just reads/writes via ComboContext.
|
||||
*
|
||||
* See _tasks/quality/2026-06-19-DESIGN-godfiles-decomposition.md §4.
|
||||
*/
|
||||
import { normalizeRoutingStrategy } from "../../../src/shared/constants/routingStrategies.ts";
|
||||
import {
|
||||
resolveContextRelayConfig,
|
||||
resolveUniversalHandoffConfig,
|
||||
SKIP_UNIVERSAL_HANDOFF_FLAG,
|
||||
} from "../contextHandoff.ts";
|
||||
import { getLastSessionModel } from "../../../src/lib/db/contextHandoffs.ts";
|
||||
import { applyComboAgentMiddleware } from "../comboAgentMiddleware.ts";
|
||||
import {
|
||||
resolveComboSetupConfig,
|
||||
resolveComboTargetTimeoutMs,
|
||||
} from "../comboConfig.ts";
|
||||
import { resolveResilienceSettings } from "../../../src/lib/resilience/settings";
|
||||
import { FETCH_TIMEOUT_MS } from "../../config/constants.ts";
|
||||
import { deriveComboSessionKey } from "./autoStrategy.ts";
|
||||
import type { ComboContext } from "./context.ts";
|
||||
|
||||
export interface ComboSetup {
|
||||
strategy: ReturnType<typeof normalizeRoutingStrategy>;
|
||||
relayConfig: ReturnType<typeof resolveContextRelayConfig> | null;
|
||||
resilienceSettings: ReturnType<typeof resolveResilienceSettings>;
|
||||
universalHandoffConfig: ReturnType<typeof resolveUniversalHandoffConfig>;
|
||||
effectiveSessionId: string | null;
|
||||
pinnedModel: string | null;
|
||||
clientRequestedStream: boolean;
|
||||
config: ReturnType<typeof resolveComboSetupConfig>;
|
||||
comboTargetTimeoutMs: number;
|
||||
reasoningTokenBufferEnabled: boolean;
|
||||
}
|
||||
|
||||
export function phaseComboSetup(ctx: ComboContext): ComboSetup {
|
||||
const { combo, settings, relayOptions, log } = ctx;
|
||||
|
||||
const strategy = normalizeRoutingStrategy(combo.strategy || "priority");
|
||||
const relayConfig =
|
||||
strategy === "context-relay" ? resolveContextRelayConfig(relayOptions?.config || null) : null;
|
||||
|
||||
const resilienceSettings = settings
|
||||
? resolveResilienceSettings(settings)
|
||||
: resolveResilienceSettings(null);
|
||||
|
||||
const universalHandoffConfig = resolveUniversalHandoffConfig(
|
||||
(combo.universal_handoff || combo.universalHandoff) as
|
||||
| Record<string, unknown>
|
||||
| null
|
||||
| undefined,
|
||||
relayOptions?.universalHandoffConfig as Record<string, unknown> | null | undefined
|
||||
);
|
||||
|
||||
// ── Server-side context cache pinning (replaces <omniModel> tag roundtrip) ─
|
||||
// Uses session_model_history — no client-side tag injection, no visible output pollution.
|
||||
//
|
||||
// #3825: when the client sends no session id (most OpenAI-compatible clients), fall
|
||||
// back to a stable conversation fingerprint derived from the body so the combo still
|
||||
// re-pins to the same model across turns. ONLY engaged when context_cache_protection
|
||||
// is truthy — when the toggle is off, behavior is unchanged (combos rotate as before,
|
||||
// no pin read/write, no <omniModel> tag).
|
||||
const effectiveSessionId: string | null = combo.context_cache_protection
|
||||
? (relayOptions?.sessionId ?? deriveComboSessionKey(ctx.body))
|
||||
: null;
|
||||
let pinnedModel: string | null = null;
|
||||
if (
|
||||
combo.context_cache_protection &&
|
||||
effectiveSessionId &&
|
||||
!(ctx.body as Record<string, unknown>)?.[SKIP_UNIVERSAL_HANDOFF_FLAG]
|
||||
) {
|
||||
const pinned = getLastSessionModel(effectiveSessionId, combo.name);
|
||||
if (pinned) {
|
||||
ctx.body = { ...ctx.body, model: pinned };
|
||||
pinnedModel = pinned;
|
||||
log.info("COMBO", `[#401] Context cache: pinned model=${pinned} (server-side)`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Combo Agent Middleware (#399 + #401) ────────────────────────────────
|
||||
// Apply system_message override, tool_filter_regex.
|
||||
// Context cache pinning is handled above via session_model_history.
|
||||
const { body: agentBody } = applyComboAgentMiddleware(
|
||||
ctx.body,
|
||||
combo,
|
||||
"" // provider/model not yet known — resolved per-model in loop
|
||||
);
|
||||
ctx.body = agentBody;
|
||||
const clientRequestedStream = ctx.body?.stream === true;
|
||||
|
||||
// Use config cascade before dispatch so all strategies, pinned context routes,
|
||||
// and round-robin targets share the same timeout policy.
|
||||
const config = resolveComboSetupConfig(combo, settings);
|
||||
const comboTargetTimeoutMs = resolveComboTargetTimeoutMs(config, FETCH_TIMEOUT_MS);
|
||||
const reasoningTokenBufferEnabled = config.reasoningTokenBufferEnabled !== false;
|
||||
|
||||
return {
|
||||
strategy,
|
||||
relayConfig,
|
||||
resilienceSettings,
|
||||
universalHandoffConfig,
|
||||
effectiveSessionId,
|
||||
pinnedModel,
|
||||
clientRequestedStream,
|
||||
config,
|
||||
comboTargetTimeoutMs,
|
||||
reasoningTokenBufferEnabled,
|
||||
};
|
||||
}
|
||||
46
open-sse/services/combo/context.ts
Normal file
46
open-sse/services/combo/context.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* ComboContext — the per-request mutable carrier for the combo dispatch pipeline
|
||||
* (Quality Gate v2 / Fase 9 — god-file decomposition, fase 1).
|
||||
*
|
||||
* handleComboChat in combo.ts is a ~1600-LOC orchestrator whose phases share a set of
|
||||
* "dispatch frontier" locals — chief among them the request `body`, which the setup phase
|
||||
* rewrites (context-cache pinning + combo agent middleware) and later dispatch phases keep
|
||||
* sharing. Carrying that mutable state on a single ctx object lets each phase be extracted
|
||||
* into a small, independently testable/mutatable function instead of living inside the
|
||||
* monolith. This is the first extracted slice; subsequent phases will add their own shared
|
||||
* mutable fields (executor, runUpstreamStream, semaphore, retry accumulators) to ComboContext.
|
||||
*
|
||||
* See _tasks/quality/2026-06-19-DESIGN-godfiles-decomposition.md §4.
|
||||
*/
|
||||
import type { ComboLike, ComboLogger, ComboRelayOptions } from "./types.ts";
|
||||
|
||||
export interface ComboContext {
|
||||
/**
|
||||
* Request body — MUTABLE. phaseComboSetup rewrites it for server-side context-cache
|
||||
* pinning and combo agent middleware; later dispatch phases keep reading/mutating it.
|
||||
*/
|
||||
body: Record<string, unknown>;
|
||||
readonly combo: ComboLike;
|
||||
readonly settings: Record<string, unknown> | null;
|
||||
readonly relayOptions: ComboRelayOptions | null;
|
||||
readonly log: ComboLogger;
|
||||
}
|
||||
|
||||
export function createComboContext(opts: {
|
||||
body: Record<string, unknown>;
|
||||
combo: ComboLike;
|
||||
settings?: Record<string, unknown> | null;
|
||||
relayOptions?: ComboRelayOptions | null;
|
||||
log: ComboLogger;
|
||||
}): ComboContext {
|
||||
// body is carried by reference (not copied) so phaseComboSetup's reassignments are
|
||||
// byte-identical to the original inline code: it only ever replaces ctx.body with a NEW
|
||||
// object on a pin ({ ...body, model }) or via the middleware result, never mutates in place.
|
||||
return {
|
||||
body: opts.body,
|
||||
combo: opts.combo,
|
||||
settings: opts.settings ?? null,
|
||||
relayOptions: opts.relayOptions ?? null,
|
||||
log: opts.log,
|
||||
};
|
||||
}
|
||||
@@ -180,3 +180,15 @@ export function resolveComboConfig(
|
||||
export function getDefaultComboConfig() {
|
||||
return { ...DEFAULT_COMBO_CONFIG };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the effective combo config the same way handleComboChat does: cascade via
|
||||
* resolveComboConfig when settings exist, else the defaults merged with the combo's own
|
||||
* config. Encapsulated here so the ternary lives in one place (DRY) and its inferred union
|
||||
* return type is the single source of truth for ComboContext.config (combo/context.ts).
|
||||
*/
|
||||
export function resolveComboSetupConfig(combo: ComboConfigLike, settings: ComboSettingsLike) {
|
||||
return settings
|
||||
? resolveComboConfig(combo, settings)
|
||||
: { ...getDefaultComboConfig(), ...((combo?.config as Record<string, unknown>) || {}) };
|
||||
}
|
||||
|
||||
65
tests/unit/combo/combo-context.test.ts
Normal file
65
tests/unit/combo/combo-context.test.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
// tests/unit/combo/combo-context.test.ts
|
||||
// Unit tests for the first extracted combo phase (god-file decomposition fase 1):
|
||||
// createComboContext (combo/context.ts) + phaseComboSetup (combo/comboSetup.ts).
|
||||
// The pinning-ON path (getLastSessionModel) is covered end-to-end by the existing combo
|
||||
// characterization suite (combo-sessionless-pin-3825, combo-config, etc.); here we exercise
|
||||
// the pure path (context_cache_protection OFF, no settings -> no DB) and the body carrier.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createComboContext } from "../../../open-sse/services/combo/context.ts";
|
||||
import { phaseComboSetup } from "../../../open-sse/services/combo/comboSetup.ts";
|
||||
|
||||
const log = { info() {}, warn() {}, error() {}, debug() {} };
|
||||
|
||||
test("createComboContext carries inputs and the body BY REFERENCE", () => {
|
||||
const body = { model: "auto", messages: [], stream: true };
|
||||
const combo = { name: "c1", models: ["a", "b"] };
|
||||
const ctx = createComboContext({ body, combo, log });
|
||||
assert.equal(ctx.body, body, "body must be the same reference (not copied) for byte-identical pinning");
|
||||
assert.equal(ctx.combo, combo);
|
||||
assert.equal(ctx.settings, null);
|
||||
assert.equal(ctx.relayOptions, null);
|
||||
assert.equal(ctx.log, log);
|
||||
});
|
||||
|
||||
test("phaseComboSetup resolves strategy/config/stream with pinning OFF (pure, no DB)", () => {
|
||||
const body = { model: "auto", messages: [], stream: true };
|
||||
const combo = { name: "c1", models: ["a"], strategy: "priority" };
|
||||
const ctx = createComboContext({ body, combo, log });
|
||||
|
||||
const setup = phaseComboSetup(ctx);
|
||||
|
||||
assert.equal(setup.strategy, "priority");
|
||||
assert.equal(setup.pinnedModel, null, "no pin when context_cache_protection is off");
|
||||
assert.equal(setup.effectiveSessionId, null);
|
||||
assert.equal(setup.clientRequestedStream, true, "body.stream === true");
|
||||
assert.equal(typeof setup.comboTargetTimeoutMs, "number");
|
||||
assert.equal(typeof setup.reasoningTokenBufferEnabled, "boolean");
|
||||
assert.ok(setup.config && typeof setup.config === "object", "config cascade resolved");
|
||||
assert.ok(
|
||||
setup.resilienceSettings && typeof setup.resilienceSettings === "object",
|
||||
"resilience settings resolved"
|
||||
);
|
||||
});
|
||||
|
||||
test("phaseComboSetup: clientRequestedStream is false when body.stream is not true", () => {
|
||||
const ctx = createComboContext({
|
||||
body: { model: "auto", messages: [] },
|
||||
combo: { name: "c", models: ["a"] },
|
||||
log,
|
||||
});
|
||||
const setup = phaseComboSetup(ctx);
|
||||
assert.equal(setup.clientRequestedStream, false);
|
||||
});
|
||||
|
||||
test("phaseComboSetup normalizes an unknown strategy to a valid routing strategy", () => {
|
||||
const ctx = createComboContext({
|
||||
body: { model: "auto", messages: [] },
|
||||
combo: { name: "c", models: ["a"], strategy: "not-a-real-strategy" },
|
||||
log,
|
||||
});
|
||||
const setup = phaseComboSetup(ctx);
|
||||
// normalizeRoutingStrategy falls back to the default for unknown values.
|
||||
assert.equal(typeof setup.strategy, "string");
|
||||
assert.ok(setup.strategy.length > 0);
|
||||
});
|
||||
Reference in New Issue
Block a user