This commit is contained in:
musistudio
2026-01-01 22:11:21 +08:00
parent e7608ada4a
commit 4c5a84e028
17 changed files with 2092 additions and 388 deletions

View File

@@ -50,44 +50,147 @@ async function handleTransformerEndpoint(
);
}
// Process request transformer chain
const { requestBody, config, bypass } = await processRequestTransformers(
body,
provider,
transformer,
req.headers,
{
req,
}
);
try {
// Process request transformer chain
const { requestBody, config, bypass } = await processRequestTransformers(
body,
provider,
transformer,
req.headers,
{
req,
}
);
// Send request to LLM provider
const response = await sendRequestToProvider(
requestBody,
config,
provider,
fastify,
bypass,
transformer,
{
req,
}
);
// Send request to LLM provider
const response = await sendRequestToProvider(
requestBody,
config,
provider,
fastify,
bypass,
transformer,
{
req,
}
);
// Process response transformer chain
const finalResponse = await processResponseTransformers(
requestBody,
response,
provider,
transformer,
bypass,
{
req,
}
);
// Process response transformer chain
const finalResponse = await processResponseTransformers(
requestBody,
response,
provider,
transformer,
bypass,
{
req,
}
);
// Format and return response
return formatResponse(finalResponse, reply, body);
// Format and return response
return formatResponse(finalResponse, reply, body);
} catch (error: any) {
// Handle fallback if error occurs
if (error.code === 'provider_response_error') {
const fallbackResult = await handleFallback(req, reply, fastify, transformer, error);
if (fallbackResult) {
return fallbackResult;
}
}
throw error;
}
}
/**
* Handle fallback logic when request fails
* Tries each fallback model in sequence until one succeeds
*/
async function handleFallback(
req: FastifyRequest,
reply: FastifyReply,
fastify: FastifyInstance,
transformer: any,
error: any
): Promise<any> {
const scenarioType = (req as any).scenarioType || 'default';
const fallbackConfig = fastify.configService.get<any>('fallback');
if (!fallbackConfig || !fallbackConfig[scenarioType]) {
return null;
}
const fallbackList = fallbackConfig[scenarioType] as string[];
if (!Array.isArray(fallbackList) || fallbackList.length === 0) {
return null;
}
req.log.warn(`Request failed for ${(req as any).scenarioType}, trying ${fallbackList.length} fallback models`);
// Try each fallback model in sequence
for (const fallbackModel of fallbackList) {
try {
req.log.info(`Trying fallback model: ${fallbackModel}`);
// Update request with fallback model
const newBody = { ...(req.body as any) };
const [fallbackProvider, ...fallbackModelName] = fallbackModel.split(',');
newBody.model = fallbackModelName.join(',');
// Create new request object with updated provider and body
const newReq = {
...req,
provider: fallbackProvider,
body: newBody,
};
const provider = fastify.providerService.getProvider(fallbackProvider);
if (!provider) {
req.log.warn(`Fallback provider '${fallbackProvider}' not found, skipping`);
continue;
}
// Process request transformer chain
const { requestBody, config, bypass } = await processRequestTransformers(
newBody,
provider,
transformer,
req.headers,
{ req: newReq }
);
// Send request to LLM provider
const response = await sendRequestToProvider(
requestBody,
config,
provider,
fastify,
bypass,
transformer,
{ req: newReq }
);
// Process response transformer chain
const finalResponse = await processResponseTransformers(
requestBody,
response,
provider,
transformer,
bypass,
{ req: newReq }
);
req.log.info(`Fallback model ${fallbackModel} succeeded`);
// Format and return response
return formatResponse(finalResponse, reply, newBody);
} catch (fallbackError: any) {
req.log.warn(`Fallback model ${fallbackModel} failed: ${fallbackError.message}`);
continue;
}
}
req.log.error(`All fallback models failed for yichu ${scenarioType}`);
return null;
}
/**

View File

@@ -39,6 +39,7 @@ declare module "fastify" {
interface FastifyRequest {
provider?: string;
model?: string;
scenarioType?: string;
}
interface FastifyInstance {
_server?: Server;
@@ -266,6 +267,7 @@ export { sessionUsageCache };
export { router };
export { calculateTokenCount };
export { searchProjectBySession };
export type { RouterScenarioType, RouterFallbackConfig } from "./utils/router";
export { ConfigService } from "./services/config";
export { ProviderService } from "./services/provider";
export { TransformerService } from "./services/transformer";

View File

@@ -126,7 +126,7 @@ const getUseModel = async (
tokenCount: number,
configService: ConfigService,
lastUsage?: Usage | undefined
) => {
): Promise<{ model: string; scenarioType: RouterScenarioType }> => {
const projectSpecificRouter = await getProjectSpecificRouter(req, configService);
const providers = configService.get<any[]>("providers") || [];
const Router = projectSpecificRouter || configService.get("Router");
@@ -140,9 +140,9 @@ const getUseModel = async (
(m: any) => m.toLowerCase() === model
);
if (finalProvider && finalModel) {
return `${finalProvider.name},${finalModel}`;
return { model: `${finalProvider.name},${finalModel}`, scenarioType: 'default' };
}
return req.body.model;
return { model: req.body.model, scenarioType: 'default' };
}
// if tokenCount is greater than the configured threshold, use the long context model
@@ -156,7 +156,7 @@ const getUseModel = async (
req.log.info(
`Using long context model due to token count: ${tokenCount}, threshold: ${longContextThreshold}`
);
return Router.longContext;
return { model: Router.longContext, scenarioType: 'longContext' };
}
if (
req.body?.system?.length > 1 &&
@@ -170,7 +170,7 @@ const getUseModel = async (
`<CCR-SUBAGENT-MODEL>${model[1]}</CCR-SUBAGENT-MODEL>`,
""
);
return model[1];
return { model: model[1], scenarioType: 'default' };
}
}
// Use the background model for any Claude Haiku variant
@@ -181,7 +181,7 @@ const getUseModel = async (
globalRouter?.background
) {
req.log.info(`Using background model for ${req.body.model}`);
return globalRouter.background;
return { model: globalRouter.background, scenarioType: 'background' };
}
// The priority of websearch must be higher than thinking.
if (
@@ -189,14 +189,14 @@ const getUseModel = async (
req.body.tools.some((tool: any) => tool.type?.startsWith("web_search")) &&
Router?.webSearch
) {
return Router.webSearch;
return { model: Router.webSearch, scenarioType: 'webSearch' };
}
// if exits thinking, use the think model
if (req.body.thinking && Router?.think) {
req.log.info(`Using think model for ${req.body.thinking}`);
return Router.think;
return { model: Router.think, scenarioType: 'think' };
}
return Router?.default;
return { model: Router?.default, scenarioType: 'default' };
};
export interface RouterContext {
@@ -205,6 +205,16 @@ export interface RouterContext {
event?: any;
}
export type RouterScenarioType = 'default' | 'background' | 'think' | 'longContext' | 'webSearch';
export interface RouterFallbackConfig {
default?: string[];
background?: string[];
think?: string[];
longContext?: string[];
webSearch?: string[];
}
export const router = async (req: any, _res: any, context: RouterContext) => {
const { configService, event } = context;
// Parse sessionId from metadata.user_id
@@ -247,9 +257,6 @@ export const router = async (req: any, _res: any, context: RouterContext) => {
tokenizerConfig
);
tokenCount = result.tokenCount;
req.log.debug(
`Token count: ${tokenCount} (tokenizer: ${result.tokenizerUsed}, cached: ${result.cached})`
);
} else {
// Legacy fallback
tokenCount = calculateTokenCount(
@@ -273,13 +280,19 @@ export const router = async (req: any, _res: any, context: RouterContext) => {
}
}
if (!model) {
model = await getUseModel(req, tokenCount, configService, lastMessageUsage);
const result = await getUseModel(req, tokenCount, configService, lastMessageUsage);
model = result.model;
req.scenarioType = result.scenarioType;
} else {
// Custom router doesn't provide scenario type, default to 'default'
req.scenarioType = 'default';
}
req.body.model = model;
} catch (error: any) {
req.log.error(`Error in router middleware: ${error.message}`);
const Router = configService.get("Router");
req.body.model = Router?.default;
req.scenarioType = 'default';
}
return;
};