feat(combos): wire capacity adapter into chat handler routing

- detectRequiredCapabilities: infer audioInput/videoInput from block
  type and embedded mime, not just vision/pdf
- handleChat / handleSingleModelChat: augment combo and single-model
  routing with capacity-adapter models when the target lacks a
  required capability, wrapped with history stripping for the
  adapter model's context window
This commit is contained in:
decolua
2026-08-05 16:12:55 +07:00
parent 8e59093db7
commit 6498b3122f
2 changed files with 61 additions and 12 deletions

View File

@@ -14,7 +14,8 @@ import { DEFAULT_HEADROOM_URL } from "@/lib/headroom/detect";
import { getTransform as getPxpipeTransform } from "@/lib/pxpipe/loader.js";
import { appendPxpipeEvent } from "@/lib/pxpipe/events.js";
import { errorResponse, unavailableResponse } from "open-sse/utils/error.js";
import { handleComboChat, handleFusionChat } from "open-sse/services/combo.js";
import { handleComboChat, handleFusionChat, detectRequiredCapabilities } from "open-sse/services/combo.js";
import { augmentModelsWithCapacityAdapter, withCapacityAdapterStripping, getActiveAdapterStrategy } from "open-sse/services/capacityAdapter.js";
import { handleBypassRequest } from "open-sse/utils/bypassHandler.js";
import { HTTP_STATUS } from "open-sse/config/runtimeConfig.js";
import { detectFormatByEndpoint } from "open-sse/translator/formats.js";
@@ -83,6 +84,8 @@ export async function handleChat(request, clientRawRequest = null) {
const bypassResponse = handleBypassRequest(body, modelStr, userAgent, !!settings.ccFilterNaming);
if (bypassResponse) return bypassResponse.response || bypassResponse;
const requiredCapabilities = detectRequiredCapabilities(body);
// Check if model is a combo (has multiple models with fallback)
const comboModels = await getComboModels(modelStr);
if (comboModels) {
@@ -90,6 +93,8 @@ export async function handleChat(request, clientRawRequest = null) {
const comboStrategies = settings.comboStrategies || {};
const comboSpecificStrategy = comboStrategies[modelStr]?.fallbackStrategy;
const comboStrategy = comboSpecificStrategy || settings.comboStrategy || "fallback";
const augmentedModels = augmentModelsWithCapacityAdapter(comboModels, requiredCapabilities, settings);
const adapterAdded = augmentedModels.filter((m) => !comboModels.includes(m));
if (comboStrategy === "fusion") {
log.info("CHAT", `Combo "${modelStr}" with ${comboModels.length} models (strategy: fusion)`);
@@ -112,11 +117,14 @@ export async function handleChat(request, clientRawRequest = null) {
}
const comboStickyLimit = settings.comboStickyRoundRobinLimit;
log.info("CHAT", `Combo "${modelStr}" with ${comboModels.length} models (strategy: ${comboStrategy}, sticky: ${comboStickyLimit})`);
log.info("CHAT", `Combo "${modelStr}" with ${augmentedModels.length} models (strategy: ${comboStrategy}, sticky: ${comboStickyLimit})`);
return handleComboChat({
body,
models: comboModels,
handleSingleModel: (b, m) => handleSingleModelChat(b, m, clientRawRequest, request, apiKey),
models: augmentedModels,
handleSingleModel: withCapacityAdapterStripping(
(b, m) => handleSingleModelChat(b, m, clientRawRequest, request, apiKey),
adapterAdded
),
log,
comboName: modelStr,
comboStrategy,
@@ -124,7 +132,25 @@ export async function handleChat(request, clientRawRequest = null) {
});
}
// Single model request
// Single model request — may still switch to a capacity-adapter model if the
// target lacks a capability the request needs (e.g. no vision, request has an image).
const soloAugmented = augmentModelsWithCapacityAdapter([modelStr], requiredCapabilities, settings);
if (soloAugmented.length > 1) {
const adapterAdded = soloAugmented.filter((m) => m !== modelStr);
log.info("CHAT", `Capacity adapter for [${[...requiredCapabilities].join(",")}] on "${modelStr}" → trying ${soloAugmented.join(", ")}`);
return handleComboChat({
body,
models: soloAugmented,
handleSingleModel: withCapacityAdapterStripping(
(b, m) => handleSingleModelChat(b, m, clientRawRequest, request, apiKey),
adapterAdded
),
log,
comboName: modelStr,
comboStrategy: getActiveAdapterStrategy(requiredCapabilities, settings)
});
}
return handleSingleModelChat(body, modelStr, clientRawRequest, request, apiKey);
}
@@ -143,6 +169,9 @@ async function handleSingleModelChat(body, modelStr, clientRawRequest = null, re
const comboStrategies = chatSettings.comboStrategies || {};
const comboSpecificStrategy = comboStrategies[modelStr]?.fallbackStrategy;
const comboStrategy = comboSpecificStrategy || chatSettings.comboStrategy || "fallback";
const requiredCapabilities = detectRequiredCapabilities(body);
const augmentedModels = augmentModelsWithCapacityAdapter(comboModels, requiredCapabilities, chatSettings);
const adapterAdded = augmentedModels.filter((m) => !comboModels.includes(m));
if (comboStrategy === "fusion") {
log.info("CHAT", `Combo "${modelStr}" with ${comboModels.length} models (strategy: fusion)`);
@@ -165,11 +194,14 @@ async function handleSingleModelChat(body, modelStr, clientRawRequest = null, re
}
const comboStickyLimit = chatSettings.comboStickyRoundRobinLimit;
log.info("CHAT", `Combo "${modelStr}" with ${comboModels.length} models (strategy: ${comboStrategy}, sticky: ${comboStickyLimit})`);
log.info("CHAT", `Combo "${modelStr}" with ${augmentedModels.length} models (strategy: ${comboStrategy}, sticky: ${comboStickyLimit})`);
return handleComboChat({
body,
models: comboModels,
handleSingleModel: (b, m) => handleSingleModelChat(b, m, clientRawRequest, request, apiKey),
models: augmentedModels,
handleSingleModel: withCapacityAdapterStripping(
(b, m) => handleSingleModelChat(b, m, clientRawRequest, request, apiKey),
adapterAdded
),
log,
comboName: modelStr,
comboStrategy,