From 7400941ae8f14fea54d7696c28a1a12162732dfe Mon Sep 17 00:00:00 2001 From: musistudio Date: Tue, 30 Dec 2025 18:23:44 +0800 Subject: [PATCH] fix preset error --- packages/core/package.json | 3 + packages/core/scripts/build.ts | 2 +- packages/core/src/server.ts | 36 ++++++-- packages/{server => core}/src/utils/cache.ts | 0 packages/{server => core}/src/utils/router.ts | 88 ++++++++++--------- packages/server/src/index.ts | 34 +++---- packages/server/src/server.ts | 3 +- packages/server/src/types.d.ts | 43 ++++++++- pnpm-lock.yaml | 9 ++ 9 files changed, 152 insertions(+), 66 deletions(-) rename packages/{server => core}/src/utils/cache.ts (100%) rename packages/{server => core}/src/utils/router.ts (80%) diff --git a/packages/core/package.json b/packages/core/package.json index 8efa6d9..d24db76 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -39,11 +39,14 @@ "google-auth-library": "^10.1.0", "json5": "^2.2.3", "jsonrepair": "^3.13.0", + "lru-cache": "^11.2.2", "openai": "^5.6.0", + "tiktoken": "^1.0.21", "undici": "^7.10.0", "uuid": "^11.1.0" }, "devDependencies": { + "@CCR/shared": "workspace:*", "@types/node": "^24.0.15", "esbuild": "^0.25.1", "tsx": "^4.20.3", diff --git a/packages/core/scripts/build.ts b/packages/core/scripts/build.ts index 5681482..e3de252 100644 --- a/packages/core/scripts/build.ts +++ b/packages/core/scripts/build.ts @@ -10,7 +10,7 @@ const baseConfig: esbuild.BuildOptions = { platform: "node", target: "node18", plugins: [], - external: ["fastify", "dotenv", "@fastify/cors", "undici"], + external: ["fastify", "dotenv", "@fastify/cors", "undici", "tiktoken", "@CCR/shared", "lru-cache"], }; const cjsConfig: esbuild.BuildOptions = { diff --git a/packages/core/src/server.ts b/packages/core/src/server.ts index 660db69..bbbc7e3 100644 --- a/packages/core/src/server.ts +++ b/packages/core/src/server.ts @@ -30,6 +30,8 @@ import { errorHandler } from "./api/middleware"; import { registerApiRoutes } from "./api/routes"; import { ProviderService } from "./services/provider"; import { TransformerService } from "./services/transformer"; +import { router, calculateTokenCount, searchProjectBySession } from "./utils/router"; +import { sessionUsageCache } from "./utils/cache"; // Extend FastifyRequest to include custom properties declare module "fastify" { @@ -125,6 +127,15 @@ class Server { fastify.decorate('configService', this.configService); fastify.decorate('transformerService', this.transformerService); fastify.decorate('providerService', this.providerService); + // Add router hook for main namespace + fastify.addHook('preHandler', async (req: any, reply: any) => { + const url = new URL(`http://127.0.0.1${req.url}`); + if (url.pathname.endsWith("/v1/messages")) { + await router(req, reply, { + configService: this.configService, + }); + } + }); await registerApiRoutes(fastify); }); return @@ -133,6 +144,7 @@ class Server { const configService = new ConfigService({ initialConfig: { providers: options.Providers, + Router: options.Router, } }); const transformerService = new TransformerService( @@ -145,15 +157,19 @@ class Server { transformerService, this.app.log ); - // await this.app.register((fastify) => { - // fastify.decorate('configService', configService); - // fastify.decorate('transformerService', transformerService); - // fastify.decorate('providerService', providerService); - // }, { prefix: name }); await this.app.register(async (fastify) => { fastify.decorate('configService', configService); fastify.decorate('transformerService', transformerService); fastify.decorate('providerService', providerService); + // Add router hook for namespace + fastify.addHook('preHandler', async (req: any, reply: any) => { + const url = new URL(`http://127.0.0.1${req.url}`); + if (url.pathname.endsWith("/v1/messages")) { + await router(req, reply, { + configService, + }); + } + }); await registerApiRoutes(fastify); }, { prefix: name }); } @@ -174,6 +190,8 @@ class Server { done(); }); + await this.registerNamespace('/') + this.app.addHook( "preHandler", async (req: FastifyRequest, reply: FastifyReply) => { @@ -198,7 +216,6 @@ class Server { } ); - await this.registerNamespace('/') const address = await this.app.listen({ port: parseInt(this.configService.get("PORT") || "3000", 10), @@ -224,3 +241,10 @@ class Server { // Export for external use export default Server; +export { sessionUsageCache }; +export { router }; +export { calculateTokenCount }; +export { searchProjectBySession }; +export { ConfigService } from "./services/config"; +export { ProviderService } from "./services/provider"; +export { TransformerService } from "./services/transformer"; diff --git a/packages/server/src/utils/cache.ts b/packages/core/src/utils/cache.ts similarity index 100% rename from packages/server/src/utils/cache.ts rename to packages/core/src/utils/cache.ts diff --git a/packages/server/src/utils/router.ts b/packages/core/src/utils/router.ts similarity index 80% rename from packages/server/src/utils/router.ts rename to packages/core/src/utils/router.ts index 5de5a9f..dcf59cd 100644 --- a/packages/server/src/utils/router.ts +++ b/packages/core/src/utils/router.ts @@ -1,10 +1,11 @@ import { get_encoding } from "tiktoken"; import { sessionUsageCache, Usage } from "./cache"; -import { readFile, access } from "fs/promises"; +import { readFile } from "fs/promises"; import { opendir, stat } from "fs/promises"; import { join } from "path"; import { CLAUDE_PROJECTS_DIR, HOME_DIR } from "@CCR/shared"; import { LRUCache } from "lru-cache"; +import { ConfigService } from "../services/config"; // Types from @anthropic-ai/sdk interface Tool { @@ -86,17 +87,10 @@ export const calculateTokenCount = ( return tokenCount; }; -const readConfigFile = async (filePath: string) => { - try { - await access(filePath); - const content = await readFile(filePath, "utf8"); - return JSON.parse(content); - } catch (error) { - return null; // 文件不存在或读取失败时返回null - } -}; - -const getProjectSpecificRouter = async (req: any) => { +const getProjectSpecificRouter = async ( + req: any, + configService: ConfigService +) => { // 检查是否有项目特定的配置 if (req.sessionId) { const project = await searchProjectBySession(req.sessionId); @@ -109,14 +103,18 @@ const getProjectSpecificRouter = async (req: any) => { ); // 首先尝试读取sessionConfig文件 - const sessionConfig = await readConfigFile(sessionConfigPath); - if (sessionConfig && sessionConfig.Router) { - return sessionConfig.Router; - } - const projectConfig = await readConfigFile(projectConfigPath); - if (projectConfig && projectConfig.Router) { - return projectConfig.Router; - } + try { + const sessionConfig = JSON.parse(await readFile(sessionConfigPath, "utf8")); + if (sessionConfig && sessionConfig.Router) { + return sessionConfig.Router; + } + } catch {} + try { + const projectConfig = JSON.parse(await readFile(projectConfigPath, "utf8")); + if (projectConfig && projectConfig.Router) { + return projectConfig.Router; + } + } catch {} } } return undefined; // 返回undefined表示使用原始配置 @@ -125,15 +123,16 @@ const getProjectSpecificRouter = async (req: any) => { const getUseModel = async ( req: any, tokenCount: number, - config: any, + configService: ConfigService, lastUsage?: Usage | undefined ) => { - const projectSpecificRouter = await getProjectSpecificRouter(req); - const Router = projectSpecificRouter || config.Router; + const projectSpecificRouter = await getProjectSpecificRouter(req, configService); + const providers = configService.get("providers") || []; + const Router = projectSpecificRouter || configService.get("Router"); if (req.body.model.includes(",")) { const [provider, model] = req.body.model.split(","); - const finalProvider = config.Providers.find( + const finalProvider = providers.find( (p: any) => p.name.toLowerCase() === provider ); const finalModel = finalProvider?.models?.find( @@ -146,13 +145,13 @@ const getUseModel = async ( } // if tokenCount is greater than the configured threshold, use the long context model - const longContextThreshold = Router.longContextThreshold || 60000; + const longContextThreshold = Router?.longContextThreshold || 60000; const lastUsageThreshold = lastUsage && lastUsage.input_tokens > longContextThreshold && tokenCount > 20000; const tokenCountThreshold = tokenCount > longContextThreshold; - if ((lastUsageThreshold || tokenCountThreshold) && Router.longContext) { + if ((lastUsageThreshold || tokenCountThreshold) && Router?.longContext) { req.log.info( `Using long context model due to token count: ${tokenCount}, threshold: ${longContextThreshold}` ); @@ -174,32 +173,38 @@ const getUseModel = async ( } } // Use the background model for any Claude Haiku variant + const globalRouter = configService.get("Router"); if ( req.body.model?.includes("claude") && req.body.model?.includes("haiku") && - config.Router.background + globalRouter?.background ) { req.log.info(`Using background model for ${req.body.model}`); - return config.Router.background; + return globalRouter.background; } // The priority of websearch must be higher than thinking. if ( Array.isArray(req.body.tools) && req.body.tools.some((tool: any) => tool.type?.startsWith("web_search")) && - Router.webSearch + Router?.webSearch ) { return Router.webSearch; } // if exits thinking, use the think model - if (req.body.thinking && Router.think) { + if (req.body.thinking && Router?.think) { req.log.info(`Using think model for ${req.body.thinking}`); return Router.think; } - return Router!.default; + return Router?.default; }; -export const router = async (req: any, _res: any, context: any) => { - const { config, event } = context; +export interface RouterContext { + configService: ConfigService; + event?: any; +} + +export const router = async (req: any, _res: any, context: RouterContext) => { + const { configService, event } = context; // Parse sessionId from metadata.user_id if (req.body.metadata?.user_id) { const parts = req.body.metadata.user_id.split("_session_"); @@ -209,12 +214,13 @@ export const router = async (req: any, _res: any, context: any) => { } const lastMessageUsage = sessionUsageCache.get(req.sessionId); const { messages, system = [], tools }: MessageCreateParamsBase = req.body; + const rewritePrompt = configService.get("REWRITE_SYSTEM_PROMPT"); if ( - config.REWRITE_SYSTEM_PROMPT && + rewritePrompt && system.length > 1 && system[1]?.text?.includes("") ) { - const prompt = await readFile(config.REWRITE_SYSTEM_PROMPT, "utf-8"); + const prompt = await readFile(rewritePrompt, "utf-8"); system[1].text = `${prompt}${system[1].text.split("").pop()}`; } @@ -226,11 +232,12 @@ export const router = async (req: any, _res: any, context: any) => { ); let model; - if (config.CUSTOM_ROUTER_PATH) { + const customRouterPath = configService.get("CUSTOM_ROUTER_PATH"); + if (customRouterPath) { try { - const customRouter = require(config.CUSTOM_ROUTER_PATH); + const customRouter = require(customRouterPath); req.tokenCount = tokenCount; // Pass token count to custom router - model = await customRouter(req, config, { + model = await customRouter(req, configService.getAll(), { event, }); } catch (e: any) { @@ -238,12 +245,13 @@ export const router = async (req: any, _res: any, context: any) => { } } if (!model) { - model = await getUseModel(req, tokenCount, config, lastMessageUsage); + model = await getUseModel(req, tokenCount, configService, lastMessageUsage); } req.body.model = model; } catch (error: any) { req.log.error(`Error in router middleware: ${error.message}`); - req.body.model = config.Router!.default; + const Router = configService.get("Router"); + req.body.model = Router?.default; } return; }; diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index de15c1a..28daf32 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -4,14 +4,13 @@ import { homedir } from "os"; import { join } from "path"; import { initConfig, initDir } from "./utils"; import { createServer } from "./server"; -import { router } from "./utils/router"; import { apiKeyAuth } from "./middleware/auth"; -import {CONFIG_FILE, HOME_DIR, listPresets} from "@CCR/shared"; +import { CONFIG_FILE, HOME_DIR, listPresets } from "@CCR/shared"; import { createStream } from 'rotating-file-stream'; -import { sessionUsageCache } from "./utils/cache"; -import {SSEParserTransform} from "./utils/SSEParser.transform"; -import {SSESerializerTransform} from "./utils/SSESerializer.transform"; -import {rewriteStream} from "./utils/rewriteStream"; +import { sessionUsageCache } from "@musistudio/llms"; +import { SSEParserTransform } from "./utils/SSEParser.transform"; +import { SSESerializerTransform } from "./utils/SSESerializer.transform"; +import { rewriteStream } from "./utils/rewriteStream"; import JSON5 from "json5"; import { IAgent, ITool } from "./agents/type"; import agentsManager from "./agents"; @@ -138,10 +137,9 @@ async function getServer(options: RunOptions = {}) { logger: loggerConfig, }); - presets.forEach(preset => { - console.log(preset.name, preset.config); - serverInstance.registerNamespace(preset.name, preset.config); - }) + await Promise.allSettled( + presets.map(async preset => await serverInstance.registerNamespace(preset.name, preset.config)) + ) // Add async preHandler hook for authentication serverInstance.addHook("preHandler", async (req: any, reply: any) => { @@ -155,7 +153,15 @@ async function getServer(options: RunOptions = {}) { }); }); serverInstance.addHook("preHandler", async (req: any, reply: any) => { - if (req.url.startsWith("/v1/messages") && !req.url.startsWith("/v1/messages/count_tokens")) { + const url = new URL(`http://127.0.0.1${req.url}`); + req.pathname = url.pathname; + if (req.pathname.endsWith("/v1/messages") && req.pathname !== "/v1/messages") { + req.preset = req.pathname.replace("/v1/messages", "").replace("/", ""); + } + }) + + serverInstance.addHook("preHandler", async (req: any, reply: any) => { + if (req.pathname.endsWith("/v1/messages")) { const useAgents = [] for (const agent of agentsManager.getAllAgents()) { @@ -185,17 +191,13 @@ async function getServer(options: RunOptions = {}) { if (useAgents.length) { req.agents = useAgents; } - await router(req, reply, { - config, - event - }); } }); serverInstance.addHook("onError", async (request: any, reply: any, error: any) => { event.emit('onError', request, reply, error); }) serverInstance.addHook("onSend", (req: any, reply: any, payload: any, done: any) => { - if (req.sessionId && req.url.startsWith("/v1/messages") && !req.url.startsWith("/v1/messages/count_tokens")) { + if (req.sessionId && req.pathname.endsWith("/v1/messages")) { if (payload instanceof ReadableStream) { if (req.agents) { const abortController = new AbortController(); diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index 76fc5cc..8fcb488 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -1,10 +1,9 @@ -import Server from "@musistudio/llms"; +import Server, { calculateTokenCount } from "@musistudio/llms"; import { readConfigFile, writeConfigFile, backupConfigFile } from "./utils"; import { join } from "path"; import fastifyStatic from "@fastify/static"; import { readdirSync, statSync, readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, rmSync } from "fs"; import { homedir } from "os"; -import { calculateTokenCount } from "./utils/router"; import { getPresetDir, readManifestFromDir, diff --git a/packages/server/src/types.d.ts b/packages/server/src/types.d.ts index b37c84b..ad50686 100644 --- a/packages/server/src/types.d.ts +++ b/packages/server/src/types.d.ts @@ -1,5 +1,6 @@ declare module "@musistudio/llms" { import { FastifyInstance } from "fastify"; + import { FastifyBaseLogger } from "fastify"; export interface ServerConfig { jsonPath?: string; @@ -9,7 +10,7 @@ declare module "@musistudio/llms" { export interface Server { app: FastifyInstance; - logger: any; + logger: FastifyBaseLogger; start(): Promise; } @@ -18,4 +19,44 @@ declare module "@musistudio/llms" { }; export default Server; + + // Export cache + export interface Usage { + input_tokens: number; + output_tokens: number; + } + + export const sessionUsageCache: any; + + // Export router + export interface RouterContext { + configService: any; + event?: any; + } + + export const router: (req: any, res: any, context: RouterContext) => Promise; + + // Export utilities + export const calculateTokenCount: (messages: any[], system: any, tools: any[]) => number; + export const searchProjectBySession: (sessionId: string) => Promise; + + // Export services + export class ConfigService { + constructor(options?: any); + get(key: string): T | undefined; + get(key: string, defaultValue: T): T; + getAll(): any; + has(key: string): boolean; + set(key: string, value: any): void; + reload(): void; + } + + export class ProviderService { + constructor(configService: any, transformerService: any, logger: any); + } + + export class TransformerService { + constructor(configService: any, logger: any); + initialize(): Promise; + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e8feef..6951edf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -133,9 +133,15 @@ importers: jsonrepair: specifier: ^3.13.0 version: 3.13.1 + lru-cache: + specifier: ^11.2.2 + version: 11.2.2 openai: specifier: ^5.6.0 version: 5.23.2(ws@8.18.3) + tiktoken: + specifier: ^1.0.21 + version: 1.0.22 undici: specifier: ^7.10.0 version: 7.16.0 @@ -143,6 +149,9 @@ importers: specifier: ^11.1.0 version: 11.1.0 devDependencies: + '@CCR/shared': + specifier: workspace:* + version: link:../shared '@types/node': specifier: ^24.0.15 version: 24.7.0