move llms to core package

This commit is contained in:
musistudio 2025-12-28 22:42:07 +08:00
parent 60a1f94878
commit 06a18c0734
46 changed files with 9055 additions and 0 deletions

48
packages/cli/src/types/inquirer.d.ts vendored Normal file
View file

@ -0,0 +1,48 @@
// Type declarations for @inquirer packages
declare module '@inquirer/input' {
import { DistinctChoice } from '@inquirer/core';
interface PromptConfig {
message: string;
default?: string;
}
export default function prompt<T = string>(config: PromptConfig): Promise<T>;
}
declare module '@inquirer/confirm' {
interface PromptConfig {
message: string;
default?: boolean;
}
export default function prompt(config: PromptConfig): Promise<boolean>;
}
declare module '@inquirer/select' {
export default function prompt<T = string>(config: {
message: string;
choices: Array<{ name: string; value: T; description?: string }>;
default?: T;
}): Promise<T>;
}
declare module '@inquirer/password' {
interface PromptConfig {
message: string;
mask?: string;
}
export default function prompt(config: PromptConfig): Promise<string>;
}
declare module '@inquirer/checkbox' {
export default function prompt<T = string>(config: {
message: string;
choices: Array<{ name: string; value: T; checked?: boolean }>;
}): Promise<T[]>;
}
declare module '@inquirer/editor' {
interface PromptConfig {
message: string;
default?: string;
}
export default function prompt(config: PromptConfig): Promise<string>;
}

17
packages/core/.npmignore Normal file
View file

@ -0,0 +1,17 @@
src
node_modules
.claude
CLAUDE.md
screenshoots
.DS_Store
.vscode
.idea
.env
.blog
docs
scripts
eslint.config.cjs
*.log
config.json
tsconfig.json
dist

View file

@ -0,0 +1,52 @@
{
"name": "@musistudio/llms",
"version": "1.0.51",
"description": "A universal LLM API transformation server",
"main": "dist/cjs/server.cjs",
"module": "dist/esm/server.mjs",
"type": "module",
"exports": {
".": {
"import": "./dist/esm/server.mjs",
"require": "./dist/cjs/server.cjs"
}
},
"scripts": {
"tsx": "tsx",
"build": "tsx scripts/build.ts",
"build:watch": "tsx scripts/build.ts --watch",
"dev": "nodemon",
"start": "node dist/cjs/server.cjs",
"start:esm": "node dist/esm/server.mjs",
"lint": "eslint src --ext .ts,.tsx"
},
"keywords": [
"llm",
"anthropic",
"openai",
"gemini",
"transformer",
"api"
],
"author": "musistudio",
"license": "MIT",
"dependencies": {
"@anthropic-ai/sdk": "^0.54.0",
"@fastify/cors": "^11.0.1",
"@google/genai": "^1.7.0",
"dotenv": "^16.5.0",
"fastify": "^5.4.0",
"google-auth-library": "^10.1.0",
"json5": "^2.2.3",
"jsonrepair": "^3.13.0",
"openai": "^5.6.0",
"undici": "^7.10.0",
"uuid": "^11.1.0"
},
"devDependencies": {
"@types/node": "^24.0.15",
"esbuild": "^0.25.1",
"tsx": "^4.20.3",
"typescript": "^5.8.2"
}
}

View file

@ -0,0 +1,62 @@
import * as esbuild from "esbuild";
const watch = process.argv.includes("--watch");
const baseConfig: esbuild.BuildOptions = {
entryPoints: ["src/server.ts"],
bundle: true,
minify: true,
sourcemap: true,
platform: "node",
target: "node18",
plugins: [],
external: ["fastify", "dotenv", "@fastify/cors", "undici"],
};
const cjsConfig: esbuild.BuildOptions = {
...baseConfig,
outdir: "dist/cjs",
format: "cjs",
outExtension: { ".js": ".cjs" },
};
const esmConfig: esbuild.BuildOptions = {
...baseConfig,
outdir: "dist/esm",
format: "esm",
outExtension: { ".js": ".mjs" },
};
async function build() {
console.log("Building CJS and ESM versions...");
const cjsCtx = await esbuild.context(cjsConfig);
const esmCtx = await esbuild.context(esmConfig);
if (watch) {
console.log("Watching for changes...");
await Promise.all([
cjsCtx.watch(),
esmCtx.watch(),
]);
} else {
await Promise.all([
cjsCtx.rebuild(),
esmCtx.rebuild(),
]);
await Promise.all([
cjsCtx.dispose(),
esmCtx.dispose(),
]);
console.log("✅ Build completed successfully!");
console.log(" - CJS: dist/cjs/server.cjs");
console.log(" - ESM: dist/esm/server.mjs");
}
}
build().catch((err) => {
console.error(err);
process.exit(1);
});

View file

@ -0,0 +1,39 @@
import { FastifyRequest, FastifyReply } from "fastify";
export interface ApiError extends Error {
statusCode?: number;
code?: string;
type?: string;
}
export function createApiError(
message: string,
statusCode: number = 500,
code: string = "internal_error",
type: string = "api_error"
): ApiError {
const error = new Error(message) as ApiError;
error.statusCode = statusCode;
error.code = code;
error.type = type;
return error;
}
export async function errorHandler(
error: ApiError,
request: FastifyRequest,
reply: FastifyReply
) {
request.log.error(error);
const statusCode = error.statusCode || 500;
const response = {
error: {
message: error.message + error.stack || "Internal Server Error",
type: error.type || "api_error",
code: error.code || "internal_error",
},
};
return reply.code(statusCode).send(response);
}

View file

@ -0,0 +1,571 @@
import {
FastifyInstance,
FastifyPluginAsync,
FastifyRequest,
FastifyReply,
} from "fastify";
import { RegisterProviderRequest, LLMProvider } from "@/types/llm";
import { sendUnifiedRequest } from "@/utils/request";
import { createApiError } from "./middleware";
import { version } from "../../package.json";
/**
* transformer端点的主函数
*
*/
async function handleTransformerEndpoint(
req: FastifyRequest,
reply: FastifyReply,
fastify: FastifyInstance,
transformer: any
) {
const body = req.body as any;
const providerName = req.provider!;
const provider = fastify._server!.providerService.getProvider(providerName);
// 验证提供者是否存在
if (!provider) {
throw createApiError(
`Provider '${providerName}' not found`,
404,
"provider_not_found"
);
}
// 处理请求转换器链
const { requestBody, config, bypass } = await processRequestTransformers(
body,
provider,
transformer,
req.headers,
{
req,
}
);
// 发送请求到LLM提供者
const response = await sendRequestToProvider(
requestBody,
config,
provider,
fastify,
bypass,
transformer,
{
req,
}
);
// 处理响应转换器链
const finalResponse = await processResponseTransformers(
requestBody,
response,
provider,
transformer,
bypass,
{
req,
}
);
// 格式化并返回响应
return formatResponse(finalResponse, reply, body);
}
/**
*
* transformRequestOutprovider transformersmodel-specific transformers
*
*/
async function processRequestTransformers(
body: any,
provider: any,
transformer: any,
headers: any,
context: any
) {
let requestBody = body;
let config: any = {};
let bypass = false;
// 检查是否应该跳过转换器(透传参数)
bypass = shouldBypassTransformers(provider, transformer, body);
if (bypass) {
if (headers instanceof Headers) {
headers.delete("content-length");
} else {
delete headers["content-length"];
}
config.headers = headers;
}
// 执行transformer的transformRequestOut方法
if (!bypass && typeof transformer.transformRequestOut === "function") {
const transformOut = await transformer.transformRequestOut(requestBody);
if (transformOut.body) {
requestBody = transformOut.body;
config = transformOut.config || {};
} else {
requestBody = transformOut;
}
}
// 执行provider级别的转换器
if (!bypass && provider.transformer?.use?.length) {
for (const providerTransformer of provider.transformer.use) {
if (
!providerTransformer ||
typeof providerTransformer.transformRequestIn !== "function"
) {
continue;
}
const transformIn = await providerTransformer.transformRequestIn(
requestBody,
provider,
context
);
if (transformIn.body) {
requestBody = transformIn.body;
config = { ...config, ...transformIn.config };
} else {
requestBody = transformIn;
}
}
}
// 执行模型特定的转换器
if (!bypass && provider.transformer?.[body.model]?.use?.length) {
for (const modelTransformer of provider.transformer[body.model].use) {
if (
!modelTransformer ||
typeof modelTransformer.transformRequestIn !== "function"
) {
continue;
}
requestBody = await modelTransformer.transformRequestIn(
requestBody,
provider,
context
);
}
}
return { requestBody, config, bypass };
}
/**
*
* provider只使用一个transformer且该transformer与当前transformer相同时
*/
function shouldBypassTransformers(
provider: any,
transformer: any,
body: any
): boolean {
return (
provider.transformer?.use?.length === 1 &&
provider.transformer.use[0].name === transformer.name &&
(!provider.transformer?.[body.model]?.use.length ||
(provider.transformer?.[body.model]?.use.length === 1 &&
provider.transformer?.[body.model]?.use[0].name === transformer.name))
);
}
/**
* LLM提供者
*
*/
async function sendRequestToProvider(
requestBody: any,
config: any,
provider: any,
fastify: FastifyInstance,
bypass: boolean,
transformer: any,
context: any
) {
const url = config.url || new URL(provider.baseUrl);
// 在透传参数下处理认证
if (bypass && typeof transformer.auth === "function") {
const auth = await transformer.auth(requestBody, provider);
if (auth.body) {
requestBody = auth.body;
let headers = config.headers || {};
if (auth.config?.headers) {
headers = {
...headers,
...auth.config.headers,
};
delete headers.host;
delete auth.config.headers;
}
config = {
...config,
...auth.config,
headers,
};
} else {
requestBody = auth;
}
}
// 发送HTTP请求
// 准备headers
const requestHeaders: Record<string, string> = {
Authorization: `Bearer ${provider.apiKey}`,
...(config?.headers || {}),
};
for (const key in requestHeaders) {
if (requestHeaders[key] === "undefined") {
delete requestHeaders[key];
} else if (
["authorization", "Authorization"].includes(key) &&
requestHeaders[key]?.includes("undefined")
) {
delete requestHeaders[key];
}
}
const response = await sendUnifiedRequest(
url,
requestBody,
{
httpsProxy: fastify._server!.configService.getHttpsProxy(),
...config,
headers: JSON.parse(JSON.stringify(requestHeaders)),
},
context,
fastify.log
);
// 处理请求错误
if (!response.ok) {
const errorText = await response.text();
fastify.log.error(
`[provider_response_error] Error from provider(${provider.name},${requestBody.model}: ${response.status}): ${errorText}`,
);
throw createApiError(
`Error from provider(${provider.name},${requestBody.model}: ${response.status}): ${errorText}`,
response.status,
"provider_response_error"
);
}
return response;
}
/**
*
* provider transformersmodel-specific transformerstransformer的transformResponseIn
*/
async function processResponseTransformers(
requestBody: any,
response: any,
provider: any,
transformer: any,
bypass: boolean,
context: any
) {
let finalResponse = response;
// 执行provider级别的响应转换器
if (!bypass && provider.transformer?.use?.length) {
for (const providerTransformer of Array.from(
provider.transformer.use
).reverse()) {
if (
!providerTransformer ||
typeof providerTransformer.transformResponseOut !== "function"
) {
continue;
}
finalResponse = await providerTransformer.transformResponseOut(
finalResponse,
context
);
}
}
// 执行模型特定的响应转换器
if (!bypass && provider.transformer?.[requestBody.model]?.use?.length) {
for (const modelTransformer of Array.from(
provider.transformer[requestBody.model].use
).reverse()) {
if (
!modelTransformer ||
typeof modelTransformer.transformResponseOut !== "function"
) {
continue;
}
finalResponse = await modelTransformer.transformResponseOut(
finalResponse,
context
);
}
}
// 执行transformer的transformResponseIn方法
if (!bypass && transformer.transformResponseIn) {
finalResponse = await transformer.transformResponseIn(
finalResponse,
context
);
}
return finalResponse;
}
/**
*
* HTTP状态码
*/
function formatResponse(response: any, reply: FastifyReply, body: any) {
// 设置HTTP状态码
if (!response.ok) {
reply.code(response.status);
}
// 处理流式响应
const isStream = body.stream === true;
if (isStream) {
reply.header("Content-Type", "text/event-stream");
reply.header("Cache-Control", "no-cache");
reply.header("Connection", "keep-alive");
return reply.send(response.body);
} else {
// 处理普通JSON响应
return response.json();
}
}
export const registerApiRoutes: FastifyPluginAsync = async (
fastify: FastifyInstance
) => {
// Health and info endpoints
fastify.get("/", async () => {
return { message: "LLMs API", version };
});
fastify.get("/health", async () => {
return { status: "ok", timestamp: new Date().toISOString() };
});
const transformersWithEndpoint =
fastify._server!.transformerService.getTransformersWithEndpoint();
for (const { transformer } of transformersWithEndpoint) {
if (transformer.endPoint) {
fastify.post(
transformer.endPoint,
async (req: FastifyRequest, reply: FastifyReply) => {
return handleTransformerEndpoint(req, reply, fastify, transformer);
}
);
}
}
fastify.post(
"/providers",
{
schema: {
body: {
type: "object",
properties: {
id: { type: "string" },
name: { type: "string" },
type: { type: "string", enum: ["openai", "anthropic"] },
baseUrl: { type: "string" },
apiKey: { type: "string" },
models: { type: "array", items: { type: "string" } },
},
required: ["id", "name", "type", "baseUrl", "apiKey", "models"],
},
},
},
async (
request: FastifyRequest<{ Body: RegisterProviderRequest }>,
reply: FastifyReply
) => {
// Validation
const { name, baseUrl, apiKey, models } = request.body;
if (!name?.trim()) {
throw createApiError(
"Provider name is required",
400,
"invalid_request"
);
}
if (!baseUrl || !isValidUrl(baseUrl)) {
throw createApiError(
"Valid base URL is required",
400,
"invalid_request"
);
}
if (!apiKey?.trim()) {
throw createApiError("API key is required", 400, "invalid_request");
}
if (!models || !Array.isArray(models) || models.length === 0) {
throw createApiError(
"At least one model is required",
400,
"invalid_request"
);
}
// Check if provider already exists
if (fastify._server!.providerService.getProvider(request.body.name)) {
throw createApiError(
`Provider with name '${request.body.name}' already exists`,
400,
"provider_exists"
);
}
return fastify._server!.providerService.registerProvider(request.body);
}
);
fastify.get("/providers", async () => {
return fastify._server!.providerService.getProviders();
});
fastify.get(
"/providers/:id",
{
schema: {
params: {
type: "object",
properties: { id: { type: "string" } },
required: ["id"],
},
},
},
async (request: FastifyRequest<{ Params: { id: string } }>) => {
const provider = fastify._server!.providerService.getProvider(
request.params.id
);
if (!provider) {
throw createApiError("Provider not found", 404, "provider_not_found");
}
return provider;
}
);
fastify.put(
"/providers/:id",
{
schema: {
params: {
type: "object",
properties: { id: { type: "string" } },
required: ["id"],
},
body: {
type: "object",
properties: {
name: { type: "string" },
type: { type: "string", enum: ["openai", "anthropic"] },
baseUrl: { type: "string" },
apiKey: { type: "string" },
models: { type: "array", items: { type: "string" } },
enabled: { type: "boolean" },
},
},
},
},
async (
request: FastifyRequest<{
Params: { id: string };
Body: Partial<LLMProvider>;
}>,
reply
) => {
const provider = fastify._server!.providerService.updateProvider(
request.params.id,
request.body
);
if (!provider) {
throw createApiError("Provider not found", 404, "provider_not_found");
}
return provider;
}
);
fastify.delete(
"/providers/:id",
{
schema: {
params: {
type: "object",
properties: { id: { type: "string" } },
required: ["id"],
},
},
},
async (request: FastifyRequest<{ Params: { id: string } }>) => {
const success = fastify._server!.providerService.deleteProvider(
request.params.id
);
if (!success) {
throw createApiError("Provider not found", 404, "provider_not_found");
}
return { message: "Provider deleted successfully" };
}
);
fastify.patch(
"/providers/:id/toggle",
{
schema: {
params: {
type: "object",
properties: { id: { type: "string" } },
required: ["id"],
},
body: {
type: "object",
properties: { enabled: { type: "boolean" } },
required: ["enabled"],
},
},
},
async (
request: FastifyRequest<{
Params: { id: string };
Body: { enabled: boolean };
}>,
reply
) => {
const success = fastify._server!.providerService.toggleProvider(
request.params.id,
request.body.enabled
);
if (!success) {
throw createApiError("Provider not found", 404, "provider_not_found");
}
return {
message: `Provider ${
request.body.enabled ? "enabled" : "disabled"
} successfully`,
};
}
);
};
// Helper function
function isValidUrl(url: string): boolean {
try {
new URL(url);
return true;
} catch {
return false;
}
}

207
packages/core/src/server.ts Normal file
View file

@ -0,0 +1,207 @@
import Fastify, {
FastifyInstance,
FastifyReply,
FastifyRequest,
FastifyPluginAsync,
FastifyPluginCallback,
FastifyPluginOptions,
FastifyRegisterOptions,
preHandlerHookHandler,
onRequestHookHandler,
preParsingHookHandler,
preValidationHookHandler,
preSerializationHookHandler,
onSendHookHandler,
onResponseHookHandler,
onTimeoutHookHandler,
onErrorHookHandler,
onRouteHookHandler,
onRegisterHookHandler,
onReadyHookHandler,
onListenHookHandler,
onCloseHookHandler,
FastifyBaseLogger,
FastifyLoggerOptions,
FastifyServerOptions,
} from "fastify";
import cors from "@fastify/cors";
import { ConfigService, AppConfig } from "./services/config";
import { errorHandler } from "./api/middleware";
import { registerApiRoutes } from "./api/routes";
import { ProviderService } from "./services/provider";
import { TransformerService } from "./services/transformer";
// Extend FastifyRequest to include custom properties
declare module "fastify" {
interface FastifyRequest {
provider?: string;
}
interface FastifyInstance {
_server?: Server;
}
}
interface ServerOptions extends FastifyServerOptions {
initialConfig?: AppConfig;
}
// Application factory
function createApp(options: FastifyServerOptions = {}): FastifyInstance {
const fastify = Fastify({
bodyLimit: 50 * 1024 * 1024,
...options,
});
// Register error handler
fastify.setErrorHandler(errorHandler);
// Register CORS
fastify.register(cors);
return fastify;
}
// Server class
class Server {
private app: FastifyInstance;
configService: ConfigService;
providerService!: ProviderService;
transformerService: TransformerService;
constructor(options: ServerOptions = {}) {
const { initialConfig, ...fastifyOptions } = options;
this.app = createApp({
...fastifyOptions,
logger: fastifyOptions.logger ?? true,
});
this.configService = new ConfigService(options);
this.transformerService = new TransformerService(
this.configService,
this.app.log
);
this.transformerService.initialize().finally(() => {
this.providerService = new ProviderService(
this.configService,
this.transformerService,
this.app.log
);
});
}
async register<Options extends FastifyPluginOptions = FastifyPluginOptions>(
plugin: FastifyPluginAsync<Options> | FastifyPluginCallback<Options>,
options?: FastifyRegisterOptions<Options>
): Promise<void> {
await (this.app as any).register(plugin, options);
}
addHook(hookName: "onRequest", hookFunction: onRequestHookHandler): void;
addHook(hookName: "preParsing", hookFunction: preParsingHookHandler): void;
addHook(
hookName: "preValidation",
hookFunction: preValidationHookHandler
): void;
addHook(hookName: "preHandler", hookFunction: preHandlerHookHandler): void;
addHook(
hookName: "preSerialization",
hookFunction: preSerializationHookHandler
): void;
addHook(hookName: "onSend", hookFunction: onSendHookHandler): void;
addHook(hookName: "onResponse", hookFunction: onResponseHookHandler): void;
addHook(hookName: "onTimeout", hookFunction: onTimeoutHookHandler): void;
addHook(hookName: "onError", hookFunction: onErrorHookHandler): void;
addHook(hookName: "onRoute", hookFunction: onRouteHookHandler): void;
addHook(hookName: "onRegister", hookFunction: onRegisterHookHandler): void;
addHook(hookName: "onReady", hookFunction: onReadyHookHandler): void;
addHook(hookName: "onListen", hookFunction: onListenHookHandler): void;
addHook(hookName: "onClose", hookFunction: onCloseHookHandler): void;
public addHook(hookName: string, hookFunction: any): void {
this.app.addHook(hookName as any, hookFunction);
}
public async registerNamespace(name: string, options: any) {
if (!name) throw new Error("name is required");
const configService = new ConfigService(options);
const transformerService = new TransformerService(
configService,
this.app.log
);
await transformerService.initialize();
const providerService = new ProviderService(
configService,
transformerService,
this.app.log
);
this.app.register((fastify) => {
fastify.decorate('configService', configService);
fastify.decorate('transformerService', transformerService);
fastify.decorate('providerService', providerService);
}, { prefix: name });
this.app.register(registerApiRoutes, { prefix: name });
}
async start(): Promise<void> {
try {
this.app._server = this;
this.app.addHook("preHandler", (req, reply, done) => {
const url = new URL(`http://127.0.0.1${req.url}`);
if (url.pathname.endsWith("/v1/messages") && req.body) {
const body = req.body as any;
req.log.info({ data: body, type: "request body" });
if (!body.stream) {
body.stream = false;
}
}
done();
});
this.app.addHook(
"preHandler",
async (req: FastifyRequest, reply: FastifyReply) => {
const url = new URL(`http://127.0.0.1${req.url}`);
if (url.pathname.endsWith("/v1/messages") && req.body) {
try {
const body = req.body as any;
if (!body || !body.model) {
return reply
.code(400)
.send({ error: "Missing model in request body" });
}
const [provider, ...model] = body.model.split(",");
body.model = model.join(",");
req.provider = provider;
return;
} catch (err) {
req.log.error({error: err}, "Error in modelProviderMiddleware:");
return reply.code(500).send({ error: "Internal server error" });
}
}
}
);
this.app.register(registerApiRoutes);
const address = await this.app.listen({
port: parseInt(this.configService.get("PORT") || "3000", 10),
host: this.configService.get("HOST") || "127.0.0.1",
});
this.app.log.info(`🚀 LLMs API server listening on ${address}`);
const shutdown = async (signal: string) => {
this.app.log.info(`Received ${signal}, shutting down gracefully...`);
await this.app.close();
process.exit(0);
};
process.on("SIGINT", () => shutdown("SIGINT"));
process.on("SIGTERM", () => shutdown("SIGTERM"));
} catch (error) {
this.app.log.error(`Error starting server: ${error}`);
process.exit(1);
}
}
}
// Export for external use
export default Server;

View file

@ -0,0 +1,179 @@
import { readFileSync, existsSync } from "fs";
import { join } from "path";
import { config } from "dotenv";
import JSON5 from 'json5';
export interface ConfigOptions {
envPath?: string;
jsonPath?: string;
useEnvFile?: boolean;
useJsonFile?: boolean;
useEnvironmentVariables?: boolean;
initialConfig?: AppConfig;
}
export interface AppConfig {
[key: string]: any;
}
export class ConfigService {
private config: AppConfig = {};
private options: ConfigOptions;
constructor(
options: ConfigOptions = {
jsonPath: "./config.json",
}
) {
this.options = {
envPath: options.envPath || ".env",
jsonPath: options.jsonPath,
useEnvFile: false,
useJsonFile: options.useJsonFile !== false,
useEnvironmentVariables: options.useEnvironmentVariables !== false,
...options,
};
this.loadConfig();
}
private loadConfig(): void {
if (this.options.useJsonFile && this.options.jsonPath) {
this.loadJsonConfig();
}
if (this.options.initialConfig) {
this.config = { ...this.config, ...this.options.initialConfig };
}
if (this.options.useEnvFile) {
this.loadEnvConfig();
}
// if (this.options.useEnvironmentVariables) {
// this.loadEnvironmentVariables();
// }
if (this.config.LOG_FILE) {
process.env.LOG_FILE = this.config.LOG_FILE;
}
if (this.config.LOG) {
process.env.LOG = this.config.LOG;
}
}
private loadJsonConfig(): void {
if (!this.options.jsonPath) return;
const jsonPath = this.isAbsolutePath(this.options.jsonPath)
? this.options.jsonPath
: join(process.cwd(), this.options.jsonPath);
if (existsSync(jsonPath)) {
try {
const jsonContent = readFileSync(jsonPath, "utf-8");
const jsonConfig = JSON5.parse(jsonContent);
this.config = { ...this.config, ...jsonConfig };
console.log(`Loaded JSON config from: ${jsonPath}`);
} catch (error) {
console.warn(`Failed to load JSON config from ${jsonPath}:`, error);
}
} else {
console.warn(`JSON config file not found: ${jsonPath}`);
}
}
private loadEnvConfig(): void {
const envPath = this.isAbsolutePath(this.options.envPath!)
? this.options.envPath!
: join(process.cwd(), this.options.envPath!);
if (existsSync(envPath)) {
try {
const result = config({ path: envPath });
if (result.parsed) {
this.config = {
...this.config,
...this.parseEnvConfig(result.parsed),
};
}
} catch (error) {
console.warn(`Failed to load .env config from ${envPath}:`, error);
}
}
}
private loadEnvironmentVariables(): void {
const envConfig = this.parseEnvConfig(process.env);
this.config = { ...this.config, ...envConfig };
}
private parseEnvConfig(
env: Record<string, string | undefined>
): Partial<AppConfig> {
const parsed: Partial<AppConfig> = {};
Object.assign(parsed, env);
return parsed;
}
private isAbsolutePath(path: string): boolean {
return path.startsWith("/") || path.includes(":");
}
public get<T = any>(key: keyof AppConfig): T | undefined;
public get<T = any>(key: keyof AppConfig, defaultValue: T): T;
public get<T = any>(key: keyof AppConfig, defaultValue?: T): T | undefined {
const value = this.config[key];
return value !== undefined ? (value as T) : defaultValue;
}
public getAll(): AppConfig {
return { ...this.config };
}
public getHttpsProxy(): string | undefined {
return (
this.get("HTTPS_PROXY") ||
this.get("https_proxy") ||
this.get("httpsProxy") ||
this.get("PROXY_URL")
);
}
public has(key: keyof AppConfig): boolean {
return this.config[key] !== undefined;
}
public set(key: keyof AppConfig, value: any): void {
this.config[key] = value;
}
public reload(): void {
this.config = {};
this.loadConfig();
}
public getConfigSummary(): string {
const summary: string[] = [];
if (this.options.initialConfig) {
summary.push("Initial Config");
}
if (this.options.useJsonFile && this.options.jsonPath) {
summary.push(`JSON: ${this.options.jsonPath}`);
}
if (this.options.useEnvFile) {
summary.push(`ENV: ${this.options.envPath}`);
}
if (this.options.useEnvironmentVariables) {
summary.push("Environment Variables");
}
return `Config sources: ${summary.join(", ")}`;
}
}

View file

@ -0,0 +1,287 @@
import { TransformerConstructor } from "@/types/transformer";
import {
LLMProvider,
RegisterProviderRequest,
ModelRoute,
RequestRouteInfo,
ConfigProvider,
} from "../types/llm";
import { ConfigService } from "./config";
import { TransformerService } from "./transformer";
export class ProviderService {
private providers: Map<string, LLMProvider> = new Map();
private modelRoutes: Map<string, ModelRoute> = new Map();
constructor(private readonly configService: ConfigService, private readonly transformerService: TransformerService, private readonly logger: any) {
this.initializeCustomProviders();
}
private initializeCustomProviders() {
const providersConfig =
this.configService.get<ConfigProvider[]>("providers");
if (providersConfig && Array.isArray(providersConfig)) {
this.initializeFromProvidersArray(providersConfig);
return;
}
}
private initializeFromProvidersArray(providersConfig: ConfigProvider[]) {
providersConfig.forEach((providerConfig: ConfigProvider) => {
try {
if (
!providerConfig.name ||
!providerConfig.api_base_url ||
!providerConfig.api_key
) {
return;
}
const transformer: LLMProvider["transformer"] = {}
if (providerConfig.transformer) {
Object.keys(providerConfig.transformer).forEach(key => {
if (key === 'use') {
if (Array.isArray(providerConfig.transformer.use)) {
transformer.use = providerConfig.transformer.use.map((transformer) => {
if (Array.isArray(transformer) && typeof transformer[0] === 'string') {
const Constructor = this.transformerService.getTransformer(transformer[0]);
if (Constructor) {
return new (Constructor as TransformerConstructor)(transformer[1]);
}
}
if (typeof transformer === 'string') {
const transformerInstance = this.transformerService.getTransformer(transformer);
if (typeof transformerInstance === 'function') {
return new transformerInstance();
}
return transformerInstance;
}
}).filter((transformer) => typeof transformer !== 'undefined');
}
} else {
if (Array.isArray(providerConfig.transformer[key]?.use)) {
transformer[key] = {
use: providerConfig.transformer[key].use.map((transformer) => {
if (Array.isArray(transformer) && typeof transformer[0] === 'string') {
const Constructor = this.transformerService.getTransformer(transformer[0]);
if (Constructor) {
return new (Constructor as TransformerConstructor)(transformer[1]);
}
}
if (typeof transformer === 'string') {
const transformerInstance = this.transformerService.getTransformer(transformer);
if (typeof transformerInstance === 'function') {
return new transformerInstance();
}
return transformerInstance;
}
}).filter((transformer) => typeof transformer !== 'undefined')
}
}
}
})
}
this.registerProvider({
name: providerConfig.name,
baseUrl: providerConfig.api_base_url,
apiKey: providerConfig.api_key,
models: providerConfig.models || [],
transformer: providerConfig.transformer ? transformer : undefined,
});
this.logger.info(`${providerConfig.name} provider registered`);
} catch (error) {
this.logger.error(`${providerConfig.name} provider registered error: ${error}`);
}
});
}
registerProvider(request: RegisterProviderRequest): LLMProvider {
const provider: LLMProvider = {
...request,
};
this.providers.set(provider.name, provider);
request.models.forEach((model) => {
const fullModel = `${provider.name},${model}`;
const route: ModelRoute = {
provider: provider.name,
model,
fullModel,
};
this.modelRoutes.set(fullModel, route);
if (!this.modelRoutes.has(model)) {
this.modelRoutes.set(model, route);
}
});
return provider;
}
getProviders(): LLMProvider[] {
return Array.from(this.providers.values());
}
getProvider(name: string): LLMProvider | undefined {
return this.providers.get(name);
}
updateProvider(
id: string,
updates: Partial<LLMProvider>
): LLMProvider | null {
const provider = this.providers.get(id);
if (!provider) {
return null;
}
const updatedProvider = {
...provider,
...updates,
updatedAt: new Date(),
};
this.providers.set(id, updatedProvider);
if (updates.models) {
provider.models.forEach((model) => {
const fullModel = `${provider.name},${model}`;
this.modelRoutes.delete(fullModel);
this.modelRoutes.delete(model);
});
updates.models.forEach((model) => {
const fullModel = `${provider.name},${model}`;
const route: ModelRoute = {
provider: provider.name,
model,
fullModel,
};
this.modelRoutes.set(fullModel, route);
if (!this.modelRoutes.has(model)) {
this.modelRoutes.set(model, route);
}
});
}
return updatedProvider;
}
deleteProvider(id: string): boolean {
const provider = this.providers.get(id);
if (!provider) {
return false;
}
provider.models.forEach((model) => {
const fullModel = `${provider.name},${model}`;
this.modelRoutes.delete(fullModel);
this.modelRoutes.delete(model);
});
this.providers.delete(id);
return true;
}
toggleProvider(name: string, enabled: boolean): boolean {
const provider = this.providers.get(name);
if (!provider) {
return false;
}
return true;
}
resolveModelRoute(modelName: string): RequestRouteInfo | null {
const route = this.modelRoutes.get(modelName);
if (!route) {
return null;
}
const provider = this.providers.get(route.provider);
if (!provider) {
return null;
}
return {
provider,
originalModel: modelName,
targetModel: route.model,
};
}
getAvailableModelNames(): string[] {
const modelNames: string[] = [];
this.providers.forEach((provider) => {
provider.models.forEach((model) => {
modelNames.push(model);
modelNames.push(`${provider.name},${model}`);
});
});
return modelNames;
}
getModelRoutes(): ModelRoute[] {
return Array.from(this.modelRoutes.values());
}
private parseTransformerConfig(transformerConfig: any): any {
if (!transformerConfig) return {};
if (Array.isArray(transformerConfig)) {
return transformerConfig.reduce((acc, item) => {
if (Array.isArray(item)) {
const [name, config = {}] = item;
acc[name] = config;
} else {
acc[item] = {};
}
return acc;
}, {});
}
return transformerConfig;
}
async getAvailableModels(): Promise<{
object: string;
data: Array<{
id: string;
object: string;
owned_by: string;
provider: string;
}>;
}> {
const models: Array<{
id: string;
object: string;
owned_by: string;
provider: string;
}> = [];
this.providers.forEach((provider) => {
provider.models.forEach((model) => {
models.push({
id: model,
object: "model",
owned_by: provider.name,
provider: provider.name,
});
models.push({
id: `${provider.name},${model}`,
object: "model",
owned_by: provider.name,
provider: provider.name,
});
});
});
return {
object: "list",
data: models,
};
}
}

View file

@ -0,0 +1,165 @@
import { Transformer, TransformerConstructor } from "@/types/transformer";
import { ConfigService } from "./config";
import Transformers from "@/transformer";
import Module from "node:module";
interface TransformerConfig {
transformers: Array<{
name: string;
type: "class" | "module";
path?: string;
options?: any;
}>;
}
export class TransformerService {
private transformers: Map<string, Transformer | TransformerConstructor> =
new Map();
constructor(
private readonly configService: ConfigService,
private readonly logger: any
) {}
registerTransformer(name: string, transformer: Transformer): void {
this.transformers.set(name, transformer);
this.logger.info(
`register transformer: ${name}${
transformer.endPoint
? ` (endpoint: ${transformer.endPoint})`
: " (no endpoint)"
}`
);
}
getTransformer(
name: string
): Transformer | TransformerConstructor | undefined {
return this.transformers.get(name);
}
getAllTransformers(): Map<string, Transformer | TransformerConstructor> {
return new Map(this.transformers);
}
getTransformersWithEndpoint(): { name: string; transformer: Transformer }[] {
const result: { name: string; transformer: Transformer }[] = [];
this.transformers.forEach((transformer, name) => {
// Check if it's an instance with endPoint
if (typeof transformer === 'object' && transformer.endPoint) {
result.push({ name, transformer });
}
});
return result;
}
getTransformersWithoutEndpoint(): {
name: string;
transformer: Transformer;
}[] {
const result: { name: string; transformer: Transformer }[] = [];
this.transformers.forEach((transformer, name) => {
// Check if it's an instance without endPoint
if (typeof transformer === 'object' && !transformer.endPoint) {
result.push({ name, transformer });
}
});
return result;
}
removeTransformer(name: string): boolean {
return this.transformers.delete(name);
}
hasTransformer(name: string): boolean {
return this.transformers.has(name);
}
async registerTransformerFromConfig(config: {
path?: string;
options?: any;
}): Promise<boolean> {
try {
if (config.path) {
const module = require(require.resolve(config.path));
if (module) {
const instance = new module(config.options);
// Set logger for transformer instance
if (instance && typeof instance === "object") {
(instance as any).logger = this.logger;
}
if (!instance.name) {
throw new Error(
`Transformer instance from ${config.path} does not have a name property.`
);
}
this.registerTransformer(instance.name, instance);
return true;
}
}
return false;
} catch (error: any) {
this.logger.error(
`load transformer (${config.path}) \nerror: ${error.message}\nstack: ${error.stack}`
);
return false;
}
}
async initialize(): Promise<void> {
try {
await this.registerDefaultTransformersInternal();
await this.loadFromConfig();
} catch (error: any) {
this.logger.error(
`TransformerService init error: ${error.message}\nStack: ${error.stack}`
);
}
}
private async registerDefaultTransformersInternal(): Promise<void> {
try {
Object.values(Transformers).forEach(
(TransformerStatic: any) => {
if (
"TransformerName" in TransformerStatic &&
typeof TransformerStatic.TransformerName === "string"
) {
this.registerTransformer(
TransformerStatic.TransformerName,
TransformerStatic
);
} else {
const transformerInstance = new TransformerStatic();
// Set logger for transformer instance
if (
transformerInstance &&
typeof transformerInstance === "object"
) {
(transformerInstance as any).logger = this.logger;
}
this.registerTransformer(
transformerInstance.name!,
transformerInstance
);
}
}
);
} catch (error) {
this.logger.error({ error }, "transformer regist error:");
}
}
private async loadFromConfig(): Promise<void> {
const transformers = this.configService.get<
TransformerConfig["transformers"]
>("transformers", []);
for (const transformer of transformers) {
await this.registerTransformerFromConfig(transformer);
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,45 @@
import { LLMProvider, UnifiedChatRequest, UnifiedMessage } from "@/types/llm";
import { Transformer } from "@/types/transformer";
/**
* Transformer class for Cerebras
*/
export class CerebrasTransformer implements Transformer {
name = "cerebras";
/**
* Transform the request from Claude Code format to Cerebras format
* @param request - The incoming request
* @param provider - The LLM provider information
* @returns The transformed request
*/
async transformRequestIn(
request: UnifiedChatRequest,
provider: LLMProvider
): Promise<Record<string, unknown>> {
// Deep clone the request to avoid modifying the original
const transformedRequest = JSON.parse(JSON.stringify(request));
if (transformedRequest.reasoning) {
delete transformedRequest.reasoning;
} else {
transformedRequest.disable_reasoning = false
}
return {
body: transformedRequest,
config: {
headers: {
'Authorization': `Bearer ${provider.apiKey}`,
'Content-Type': 'application/json'
}
}
};
}
async transformResponseOut(response: Response): Promise<Response> {
return response;
}
}

View file

@ -0,0 +1,23 @@
import { MessageContent, TextContent, UnifiedChatRequest } from "@/types/llm";
import { Transformer } from "../types/transformer";
export class CleancacheTransformer implements Transformer {
name = "cleancache";
async transformRequestIn(request: UnifiedChatRequest): Promise<UnifiedChatRequest> {
if (Array.isArray(request.messages)) {
request.messages.forEach((msg) => {
if (Array.isArray(msg.content)) {
(msg.content as MessageContent[]).forEach((item) => {
if ((item as TextContent).cache_control) {
delete (item as TextContent).cache_control;
}
});
} else if (msg.cache_control) {
delete msg.cache_control;
}
});
}
return request;
}
}

View file

@ -0,0 +1,108 @@
import { UnifiedChatRequest } from "../types/llm";
import { Transformer, TransformerOptions } from "../types/transformer";
export interface CustomParamsOptions extends TransformerOptions {
/**
* Custom parameters to inject into the request body
* Any key-value pairs will be added to the request
* Supports: string, number, boolean, object, array
*/
[key: string]: any;
}
/**
* Transformer for injecting dynamic custom parameters into LLM requests
* Allows runtime configuration of arbitrary parameters that get merged
* into the request body using deep merge strategy
*/
export class CustomParamsTransformer implements Transformer {
static TransformerName = "customparams";
private options: CustomParamsOptions;
constructor(options: CustomParamsOptions = {}) {
this.options = options;
}
async transformRequestIn(
request: UnifiedChatRequest
): Promise<UnifiedChatRequest> {
// Create a copy of the request to avoid mutating the original
const modifiedRequest = { ...request } as any;
// Inject custom parameters with deep merge
const parametersToInject = Object.entries(this.options);
for (const [key, value] of parametersToInject) {
if (key in modifiedRequest) {
// Deep merge with existing parameter
if (typeof modifiedRequest[key] === 'object' &&
typeof value === 'object' &&
!Array.isArray(modifiedRequest[key]) &&
!Array.isArray(value) &&
modifiedRequest[key] !== null &&
value !== null) {
// Deep merge objects
modifiedRequest[key] = this.deepMergeObjects(modifiedRequest[key], value);
} else {
// For non-objects, keep existing value (preserve original)
continue;
}
} else {
// Add new parameter
modifiedRequest[key] = this.cloneValue(value);
}
}
return modifiedRequest;
}
async transformResponseOut(response: Response): Promise<Response> {
// Pass through response unchanged
return response;
}
/**
* Deep merge two objects recursively
*/
private deepMergeObjects(target: any, source: any): any {
const result = { ...target };
for (const [key, value] of Object.entries(source)) {
if (key in result &&
typeof result[key] === 'object' &&
typeof value === 'object' &&
!Array.isArray(result[key]) &&
!Array.isArray(value) &&
result[key] !== null &&
value !== null) {
result[key] = this.deepMergeObjects(result[key], value);
} else {
result[key] = this.cloneValue(value);
}
}
return result;
}
/**
* Clone a value to prevent reference issues
*/
private cloneValue(value: any): any {
if (value === null || typeof value !== 'object') {
return value;
}
if (Array.isArray(value)) {
return value.map(item => this.cloneValue(item));
}
const cloned: any = {};
for (const [key, val] of Object.entries(value)) {
cloned[key] = this.cloneValue(val);
}
return cloned;
}
}

View file

@ -0,0 +1,221 @@
import { UnifiedChatRequest } from "../types/llm";
import { Transformer } from "../types/transformer";
export class DeepseekTransformer implements Transformer {
name = "deepseek";
async transformRequestIn(request: UnifiedChatRequest): Promise<UnifiedChatRequest> {
if (request.max_tokens && request.max_tokens > 8192) {
request.max_tokens = 8192; // DeepSeek has a max token limit of 8192
}
return request;
}
async transformResponseOut(response: Response): Promise<Response> {
if (response.headers.get("Content-Type")?.includes("application/json")) {
const jsonResponse = await response.json();
// Handle non-streaming response if needed
return new Response(JSON.stringify(jsonResponse), {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
} else if (response.headers.get("Content-Type")?.includes("stream")) {
if (!response.body) {
return response;
}
const decoder = new TextDecoder();
const encoder = new TextEncoder();
let reasoningContent = "";
let isReasoningComplete = false;
let buffer = ""; // 用于缓冲不完整的数据
const stream = new ReadableStream({
async start(controller) {
const reader = response.body!.getReader();
const processBuffer = (
buffer: string,
controller: ReadableStreamDefaultController,
encoder: TextEncoder
) => {
const lines = buffer.split("\n");
for (const line of lines) {
if (line.trim()) {
controller.enqueue(encoder.encode(line + "\n"));
}
}
};
const processLine = (
line: string,
context: {
controller: ReadableStreamDefaultController;
encoder: TextEncoder;
reasoningContent: () => string;
appendReasoningContent: (content: string) => void;
isReasoningComplete: () => boolean;
setReasoningComplete: (val: boolean) => void;
}
) => {
const { controller, encoder } = context;
if (
line.startsWith("data: ") &&
line.trim() !== "data: [DONE]"
) {
try {
const data = JSON.parse(line.slice(6));
// Extract reasoning_content from delta
if (data.choices?.[0]?.delta?.reasoning_content) {
context.appendReasoningContent(
data.choices[0].delta.reasoning_content
);
const thinkingChunk = {
...data,
choices: [
{
...data.choices[0],
delta: {
...data.choices[0].delta,
thinking: {
content: data.choices[0].delta.reasoning_content,
},
},
},
],
};
delete thinkingChunk.choices[0].delta.reasoning_content;
const thinkingLine = `data: ${JSON.stringify(
thinkingChunk
)}\n\n`;
controller.enqueue(encoder.encode(thinkingLine));
return;
}
// Check if reasoning is complete (when delta has content but no reasoning_content)
if (
data.choices?.[0]?.delta?.content &&
context.reasoningContent() &&
!context.isReasoningComplete()
) {
context.setReasoningComplete(true);
const signature = Date.now().toString();
// Create a new chunk with thinking block
const thinkingChunk = {
...data,
choices: [
{
...data.choices[0],
delta: {
...data.choices[0].delta,
content: null,
thinking: {
content: context.reasoningContent(),
signature: signature,
},
},
},
],
};
delete thinkingChunk.choices[0].delta.reasoning_content;
// Send the thinking chunk
const thinkingLine = `data: ${JSON.stringify(
thinkingChunk
)}\n\n`;
controller.enqueue(encoder.encode(thinkingLine));
}
if (data.choices[0]?.delta?.reasoning_content) {
delete data.choices[0].delta.reasoning_content;
}
// Send the modified chunk
if (
data.choices?.[0]?.delta &&
Object.keys(data.choices[0].delta).length > 0
) {
if (context.isReasoningComplete()) {
data.choices[0].index++;
}
const modifiedLine = `data: ${JSON.stringify(data)}\n\n`;
controller.enqueue(encoder.encode(modifiedLine));
}
} catch (e) {
// If JSON parsing fails, pass through the original line
controller.enqueue(encoder.encode(line + "\n"));
}
} else {
// Pass through non-data lines (like [DONE])
controller.enqueue(encoder.encode(line + "\n"));
}
};
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
// 处理缓冲区中剩余的数据
if (buffer.trim()) {
processBuffer(buffer, controller, encoder);
}
break;
}
const chunk = decoder.decode(value, { stream: true });
buffer += chunk;
// 处理缓冲区中完整的数据行
const lines = buffer.split("\n");
buffer = lines.pop() || ""; // 最后一行可能不完整,保留在缓冲区
for (const line of lines) {
if (!line.trim()) continue;
try {
processLine(line, {
controller,
encoder,
reasoningContent: () => reasoningContent,
appendReasoningContent: (content) =>
(reasoningContent += content),
isReasoningComplete: () => isReasoningComplete,
setReasoningComplete: (val) => (isReasoningComplete = val),
});
} catch (error) {
console.error("Error processing line:", line, error);
// 如果解析失败,直接传递原始行
controller.enqueue(encoder.encode(line + "\n"));
}
}
}
} catch (error) {
console.error("Stream error:", error);
controller.error(error);
} finally {
try {
reader.releaseLock();
} catch (e) {
console.error("Error releasing reader lock:", e);
}
controller.close();
}
},
});
return new Response(stream, {
status: response.status,
statusText: response.statusText,
headers: {
"Content-Type": response.headers.get("Content-Type") || "text/plain",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}
return response;
}
}

View file

@ -0,0 +1,334 @@
import { Transformer } from "@/types/transformer";
import { parseToolArguments } from "@/utils/toolArgumentsParser";
export class EnhanceToolTransformer implements Transformer {
name = "enhancetool";
async transformResponseOut(response: Response): Promise<Response> {
if (response.headers.get("Content-Type")?.includes("application/json")) {
const jsonResponse = await response.json();
if (jsonResponse?.choices?.[0]?.message?.tool_calls?.length) {
// 处理非流式的工具调用参数解析
for (const toolCall of jsonResponse.choices[0].message.tool_calls) {
if (toolCall.function?.arguments) {
toolCall.function.arguments = parseToolArguments(
toolCall.function.arguments,
this.logger
);
}
}
}
return new Response(JSON.stringify(jsonResponse), {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
} else if (response.headers.get("Content-Type")?.includes("stream")) {
if (!response.body) {
return response;
}
const decoder = new TextDecoder();
const encoder = new TextEncoder();
// Define interface for tool call tracking
interface ToolCall {
index?: number;
name?: string;
id?: string;
arguments?: string;
}
let currentToolCall: ToolCall = {};
let hasTextContent = false;
let reasoningContent = "";
let isReasoningComplete = false;
let hasToolCall = false;
let buffer = ""; // 用于缓冲不完整的数据
const stream = new ReadableStream({
async start(controller) {
const reader = response.body!.getReader();
const processBuffer = (
buffer: string,
controller: ReadableStreamDefaultController,
encoder: TextEncoder
) => {
const lines = buffer.split("\n");
for (const line of lines) {
if (line.trim()) {
controller.enqueue(encoder.encode(line + "\n"));
}
}
};
// Helper function to process completed tool calls
const processCompletedToolCall = (
data: any,
controller: ReadableStreamDefaultController,
encoder: TextEncoder
) => {
let finalArgs = "";
try {
finalArgs = parseToolArguments(currentToolCall.arguments || "", this.logger);
} catch (e: any) {
console.error(
`${e.message} ${
e.stack
} 工具调用参数解析失败: ${JSON.stringify(
currentToolCall
)}`
);
// Use original arguments if parsing fails
finalArgs = currentToolCall.arguments || "";
}
const delta = {
role: "assistant",
tool_calls: [
{
function: {
name: currentToolCall.name,
arguments: finalArgs,
},
id: currentToolCall.id,
index: currentToolCall.index,
type: "function",
},
],
};
// Remove content field entirely to prevent extra null values
const modifiedData = {
...data,
choices: [
{
...data.choices[0],
delta,
},
],
};
// Remove content field if it exists
if (modifiedData.choices[0].delta.content !== undefined) {
delete modifiedData.choices[0].delta.content;
}
const modifiedLine = `data: ${JSON.stringify(modifiedData)}\n\n`;
controller.enqueue(encoder.encode(modifiedLine));
};
const processLine = (
line: string,
context: {
controller: ReadableStreamDefaultController;
encoder: TextEncoder;
hasTextContent: () => boolean;
setHasTextContent: (val: boolean) => void;
reasoningContent: () => string;
appendReasoningContent: (content: string) => void;
isReasoningComplete: () => boolean;
setReasoningComplete: (val: boolean) => void;
}
) => {
const { controller, encoder } = context;
if (line.startsWith("data: ") && line.trim() !== "data: [DONE]") {
const jsonStr = line.slice(6);
try {
const data = JSON.parse(jsonStr);
// Handle tool calls in streaming mode
if (data.choices?.[0]?.delta?.tool_calls?.length) {
const toolCallDelta = data.choices[0].delta.tool_calls[0];
// Initialize currentToolCall if this is the first chunk for this tool call
if (typeof currentToolCall.index === "undefined") {
currentToolCall = {
index: toolCallDelta.index,
name: toolCallDelta.function?.name || "",
id: toolCallDelta.id || "",
arguments: toolCallDelta.function?.arguments || ""
};
if (toolCallDelta.function?.arguments) {
toolCallDelta.function.arguments = ''
}
// Send the first chunk as-is
const modifiedLine = `data: ${JSON.stringify(data)}\n\n`;
controller.enqueue(encoder.encode(modifiedLine));
return;
}
// Accumulate arguments if this is a continuation of the current tool call
else if (currentToolCall.index === toolCallDelta.index) {
if (toolCallDelta.function?.arguments) {
currentToolCall.arguments += toolCallDelta.function.arguments;
}
// Don't send intermediate chunks that only contain arguments
return;
}
// If we have a different tool call index, process the previous one and start a new one
else {
// Process the completed tool call using helper function
processCompletedToolCall(data, controller, encoder);
// Start tracking the new tool call
currentToolCall = {
index: toolCallDelta.index,
name: toolCallDelta.function?.name || "",
id: toolCallDelta.id || "",
arguments: toolCallDelta.function?.arguments || ""
};
return;
}
}
// Handle finish_reason for tool_calls
if (data.choices?.[0]?.finish_reason === "tool_calls" && currentToolCall.index !== undefined) {
// Process the final tool call using helper function
processCompletedToolCall(data, controller, encoder);
currentToolCall = {};
return;
}
// Handle text content alongside tool calls
if (
data.choices?.[0]?.delta?.tool_calls?.length &&
context.hasTextContent()
) {
if (typeof data.choices[0].index === "number") {
data.choices[0].index += 1;
} else {
data.choices[0].index = 1;
}
}
const modifiedLine = `data: ${JSON.stringify(data)}\n\n`;
controller.enqueue(encoder.encode(modifiedLine));
} catch (e) {
// 如果JSON解析失败可能是数据不完整将原始行传递下去
controller.enqueue(encoder.encode(line + "\n"));
}
} else {
// Pass through non-data lines (like [DONE])
controller.enqueue(encoder.encode(line + "\n"));
}
};
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
// 处理缓冲区中剩余的数据
if (buffer.trim()) {
processBuffer(buffer, controller, encoder);
}
break;
}
// 检查value是否有效
if (!value || value.length === 0) {
continue;
}
let chunk;
try {
chunk = decoder.decode(value, { stream: true });
} catch (decodeError) {
console.warn("Failed to decode chunk", decodeError);
continue;
}
if (chunk.length === 0) {
continue;
}
buffer += chunk;
// 如果缓冲区过大,进行处理避免内存泄漏
if (buffer.length > 1000000) {
// 1MB 限制
console.warn(
"Buffer size exceeds limit, processing partial data"
);
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.trim()) {
try {
processLine(line, {
controller,
encoder,
hasTextContent: () => hasTextContent,
setHasTextContent: (val) => (hasTextContent = val),
reasoningContent: () => reasoningContent,
appendReasoningContent: (content) =>
(reasoningContent += content),
isReasoningComplete: () => isReasoningComplete,
setReasoningComplete: (val) =>
(isReasoningComplete = val),
});
} catch (error) {
console.error("Error processing line:", line, error);
// 如果解析失败,直接传递原始行
controller.enqueue(encoder.encode(line + "\n"));
}
}
}
continue;
}
// 处理缓冲区中完整的数据行
const lines = buffer.split("\n");
buffer = lines.pop() || ""; // 最后一行可能不完整,保留在缓冲区
for (const line of lines) {
if (!line.trim()) continue;
try {
processLine(line, {
controller,
encoder,
hasTextContent: () => hasTextContent,
setHasTextContent: (val) => (hasTextContent = val),
reasoningContent: () => reasoningContent,
appendReasoningContent: (content) =>
(reasoningContent += content),
isReasoningComplete: () => isReasoningComplete,
setReasoningComplete: (val) => (isReasoningComplete = val),
});
} catch (error) {
console.error("Error processing line:", line, error);
// 如果解析失败,直接传递原始行
controller.enqueue(encoder.encode(line + "\n"));
}
}
}
} catch (error) {
console.error("Stream error:", error);
controller.error(error);
} finally {
try {
reader.releaseLock();
} catch (e) {
console.error("Error releasing reader lock:", e);
}
controller.close();
}
},
});
return new Response(stream, {
status: response.status,
statusText: response.statusText,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}
return response;
}
}

View file

@ -0,0 +1,342 @@
import { UnifiedChatRequest } from "../types/llm";
import { Transformer } from "../types/transformer";
const PROMPT = `Always think before answering. Even if the problem seems simple, always write down your reasoning process explicitly.
Output format:
<reasoning_content>
Your detailed thinking process goes here
</reasoning_content>
Your final answer must follow after the closing tag above.`;
const MAX_INTERLEAVED_TIMES = 10;
export class ForceReasoningTransformer implements Transformer {
name = "forcereasoning";
async transformRequestIn(
request: UnifiedChatRequest
): Promise<UnifiedChatRequest> {
let times = 0
request.messages
.filter((msg) => msg.role === "assistant")
.reverse()
.forEach((message) => {
if (message.thinking) {
if (message.thinking.content) {
if (!message.content || times < MAX_INTERLEAVED_TIMES) {
times++;
message.content = `<reasoning_content>${message.thinking.content}</reasoning_content>\n${message.content}`;
}
}
delete message.thinking;
}
});
const lastMessage = request.messages[request.messages.length - 1];
if (lastMessage.role === "user") {
if (Array.isArray(lastMessage.content)) {
lastMessage.content.push({
type: "text",
text: PROMPT,
});
} else {
lastMessage.content = [
{
type: "text",
text: PROMPT,
},
{
type: "text",
text: lastMessage.content || '',
},
];
}
}
if (lastMessage.role === "tool") {
request.messages.push({
role: "user",
content: [
{
type: "text",
text: PROMPT,
},
],
});
}
return request;
}
async transformResponseOut(response: Response): Promise<Response> {
const reasonStartTag = "<reasoning_content>";
const reasonStopTag = "</reasoning_content>";
if (response.headers.get("Content-Type")?.includes("application/json")) {
const jsonResponse: any = await response.json();
if (jsonResponse.choices[0]?.message.content) {
const regex = /<reasoning_content>(.*?)<\/reasoning_content>/s;
const match = jsonResponse.choices[0]?.message.content.match(regex);
if (match && match[1]) {
jsonResponse.thinking = {
content: match[1],
};
}
}
return new Response(JSON.stringify(jsonResponse), {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
} else if (response.headers.get("Content-Type")?.includes("stream")) {
if (!response.body) {
return response;
}
let contentIndex = 0;
const decoder = new TextDecoder();
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
const reader = response.body!.getReader();
let lineBuffer = "";
let fsmState: "SEARCHING" | "REASONING" | "FINAL" = "SEARCHING";
let tagBuffer = "";
let finalBuffer = "";
const processAndEnqueue = (
originalData: any,
content: string | null | undefined
) => {
if (typeof content !== "string") {
if (
originalData.choices?.[0]?.delta &&
Object.keys(originalData.choices[0].delta).length > 0 &&
!originalData.choices[0].delta.content
) {
originalData.choices[0].index = contentIndex
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(originalData)}\n\n`)
);
}
return;
}
let currentContent = tagBuffer + content;
tagBuffer = "";
while (currentContent.length > 0) {
if (fsmState === "SEARCHING") {
const startTagIndex = currentContent.indexOf(reasonStartTag);
if (startTagIndex !== -1) {
currentContent = currentContent.substring(
startTagIndex + reasonStartTag.length
);
fsmState = "REASONING";
} else {
for (let i = reasonStartTag.length - 1; i > 0; i--) {
if (
currentContent.endsWith(reasonStartTag.substring(0, i))
) {
tagBuffer = currentContent.substring(
currentContent.length - i
);
break;
}
}
currentContent = "";
}
} else if (fsmState === "REASONING") {
const endTagIndex = currentContent.indexOf(reasonStopTag);
if (endTagIndex !== -1) {
const reasoningPart = currentContent.substring(
0,
endTagIndex
);
if (reasoningPart.length > 0) {
const newDelta = {
...originalData.choices[0].delta,
thinking: {
content: reasoningPart,
},
};
delete newDelta.content;
const thinkingChunk = {
...originalData,
choices: [
{ ...originalData.choices[0], delta: newDelta, index: contentIndex },
],
};
controller.enqueue(
encoder.encode(
`data: ${JSON.stringify(thinkingChunk)}\n\n`
)
);
}
// Send signature message
const signatureDelta = {
...originalData.choices[0].delta,
thinking: { signature: new Date().getTime().toString() },
};
delete signatureDelta.content;
const signatureChunk = {
...originalData,
choices: [
{ ...originalData.choices[0], delta: signatureDelta, index: contentIndex },
],
};
controller.enqueue(
encoder.encode(
`data: ${JSON.stringify(signatureChunk)}\n\n`
)
);
contentIndex++;
currentContent = currentContent.substring(
endTagIndex + reasonStopTag.length
);
fsmState = "FINAL";
} else {
let reasoningPart = currentContent;
for (let i = reasonStopTag.length - 1; i > 0; i--) {
if (
currentContent.endsWith(reasonStopTag.substring(0, i))
) {
tagBuffer = currentContent.substring(
currentContent.length - i
);
reasoningPart = currentContent.substring(
0,
currentContent.length - i
);
break;
}
}
if (reasoningPart.length > 0) {
const newDelta = {
...originalData.choices[0].delta,
thinking: { content: reasoningPart },
};
delete newDelta.content;
const thinkingChunk = {
...originalData,
choices: [
{ ...originalData.choices[0], delta: newDelta, index: contentIndex },
],
};
controller.enqueue(
encoder.encode(
`data: ${JSON.stringify(thinkingChunk)}\n\n`
)
);
}
currentContent = "";
}
} else if (fsmState === "FINAL") {
if (currentContent.length > 0) {
// 检查内容是否只包含换行符
const isOnlyNewlines = /^\s*$/.test(currentContent);
if (isOnlyNewlines) {
// 如果只有换行符,添加到缓冲区但不发送
finalBuffer += currentContent;
} else {
// 如果有非换行符内容,将缓冲区和新内容一起发送
const finalPart = finalBuffer + currentContent;
const newDelta = {
...originalData.choices[0].delta,
content: finalPart,
};
if (newDelta.thinking) delete newDelta.thinking;
const finalChunk = {
...originalData,
choices: [
{ ...originalData.choices[0], delta: newDelta },
],
};
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(finalChunk)}\n\n`)
);
// 发送后清空缓冲区
finalBuffer = "";
}
}
contentIndex++
currentContent = "";
}
}
};
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
const chunk = decoder.decode(value, { stream: true });
lineBuffer += chunk;
const lines = lineBuffer.split("\n");
lineBuffer = lines.pop() || "";
for (const line of lines) {
if (!line.trim()) continue;
if (line.trim() === "data: [DONE]") {
controller.enqueue(encoder.encode(line + "\n\n"));
break;
}
if (line.startsWith("data:")) {
try {
const data = JSON.parse(line.slice(5));
processAndEnqueue(data, data.choices?.[0]?.delta?.content);
} catch (e) {
controller.enqueue(encoder.encode(line + "\n"));
}
} else {
controller.enqueue(encoder.encode(line + "\n"));
}
}
}
} catch (error) {
console.error("Stream error:", error);
controller.error(error);
} finally {
try {
reader.releaseLock();
} catch (e) {
console.error("Error releasing reader lock:", e);
}
if (fsmState === "REASONING") {
const signatureDelta = {
thinking: { signature: new Date().getTime().toString() },
};
const signatureChunk = {
choices: [{ delta: signatureDelta }],
};
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(signatureChunk)}\n\n`)
);
}
controller.close();
}
},
});
return new Response(stream, {
status: response.status,
statusText: response.statusText,
headers: {
"Content-Type": response.headers.get("Content-Type") || "text/plain",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}
return response;
}
}

View file

@ -0,0 +1,40 @@
import { LLMProvider, UnifiedChatRequest } from "../types/llm";
import { Transformer } from "../types/transformer";
import {
buildRequestBody,
transformRequestOut,
transformResponseOut,
} from "../utils/gemini.util";
export class GeminiTransformer implements Transformer {
name = "gemini";
endPoint = "/v1beta/models/:modelAndAction";
async transformRequestIn(
request: UnifiedChatRequest,
provider: LLMProvider
): Promise<Record<string, any>> {
return {
body: buildRequestBody(request),
config: {
url: new URL(
`./${request.model}:${
request.stream ? "streamGenerateContent?alt=sse" : "generateContent"
}`,
provider.baseUrl
),
headers: {
"x-goog-api-key": provider.apiKey,
Authorization: undefined,
},
},
};
}
transformRequestOut = transformRequestOut;
async transformResponseOut(response: Response): Promise<Response> {
return transformResponseOut(response, this.name, this.logger);
}
}

View file

@ -0,0 +1,228 @@
import { MessageContent, TextContent, UnifiedChatRequest } from "@/types/llm";
import { Transformer } from "../types/transformer";
import { v4 as uuidv4 } from "uuid"
export class GroqTransformer implements Transformer {
name = "groq";
async transformRequestIn(request: UnifiedChatRequest): Promise<UnifiedChatRequest> {
request.messages.forEach(msg => {
if (Array.isArray(msg.content)) {
(msg.content as MessageContent[]).forEach((item) => {
if ((item as TextContent).cache_control) {
delete (item as TextContent).cache_control;
}
});
} else if (msg.cache_control) {
delete msg.cache_control;
}
})
if (Array.isArray(request.tools)) {
request.tools.forEach(tool => {
delete tool.function.parameters.$schema;
})
}
return request
}
async transformResponseOut(response: Response): Promise<Response> {
if (response.headers.get("Content-Type")?.includes("application/json")) {
const jsonResponse = await response.json();
return new Response(JSON.stringify(jsonResponse), {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
} else if (response.headers.get("Content-Type")?.includes("stream")) {
if (!response.body) {
return response;
}
const decoder = new TextDecoder();
const encoder = new TextEncoder();
let hasTextContent = false;
let reasoningContent = "";
let isReasoningComplete = false;
let buffer = ""; // 用于缓冲不完整的数据
const stream = new ReadableStream({
async start(controller) {
const reader = response.body!.getReader();
const processBuffer = (buffer: string, controller: ReadableStreamDefaultController, encoder: InstanceType<typeof TextEncoder>) => {
const lines = buffer.split("\n");
for (const line of lines) {
if (line.trim()) {
controller.enqueue(encoder.encode(line + "\n"));
}
}
};
const processLine = (line: string, context: {
controller: ReadableStreamDefaultController;
encoder: typeof TextEncoder;
hasTextContent: () => boolean;
setHasTextContent: (val: boolean) => void;
reasoningContent: () => string;
appendReasoningContent: (content: string) => void;
isReasoningComplete: () => boolean;
setReasoningComplete: (val: boolean) => void;
}) => {
const { controller, encoder } = context;
if (line.startsWith("data: ") && line.trim() !== "data: [DONE]") {
const jsonStr = line.slice(6);
try {
const data = JSON.parse(jsonStr);
if (data.error) {
throw new Error(JSON.stringify(data));
}
if (data.choices?.[0]?.delta?.content && !context.hasTextContent()) {
context.setHasTextContent(true);
}
if (
data.choices?.[0]?.delta?.tool_calls?.length
) {
data.choices?.[0]?.delta?.tool_calls.forEach((tool: any) => {
tool.id = `call_${uuidv4()}`;
})
}
if (
data.choices?.[0]?.delta?.tool_calls?.length &&
context.hasTextContent()
) {
if (typeof data.choices[0].index === 'number') {
data.choices[0].index += 1;
} else {
data.choices[0].index = 1;
}
}
const modifiedLine = `data: ${JSON.stringify(data)}\n\n`;
controller.enqueue(encoder.encode(modifiedLine));
} catch (e) {
// 如果JSON解析失败可能是数据不完整将原始行传递下去
controller.enqueue(encoder.encode(line + "\n"));
}
} else {
// Pass through non-data lines (like [DONE])
controller.enqueue(encoder.encode(line + "\n"));
}
};
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
// 处理缓冲区中剩余的数据
if (buffer.trim()) {
processBuffer(buffer, controller, encoder);
}
break;
}
// 检查value是否有效
if (!value || value.length === 0) {
continue;
}
let chunk;
try {
chunk = decoder.decode(value, { stream: true });
} catch (decodeError) {
console.warn("Failed to decode chunk", decodeError);
continue;
}
if (chunk.length === 0) {
continue;
}
buffer += chunk;
// 如果缓冲区过大,进行处理避免内存泄漏
if (buffer.length > 1000000) { // 1MB 限制
console.warn("Buffer size exceeds limit, processing partial data");
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.trim()) {
try {
processLine(line, {
controller,
encoder,
hasTextContent: () => hasTextContent,
setHasTextContent: (val) => hasTextContent = val,
reasoningContent: () => reasoningContent,
appendReasoningContent: (content) => reasoningContent += content,
isReasoningComplete: () => isReasoningComplete,
setReasoningComplete: (val) => isReasoningComplete = val
});
} catch (error) {
console.error("Error processing line:", line, error);
// 如果解析失败,直接传递原始行
controller.enqueue(encoder.encode(line + "\n"));
}
}
}
continue;
}
// 处理缓冲区中完整的数据行
const lines = buffer.split("\n");
buffer = lines.pop() || ""; // 最后一行可能不完整,保留在缓冲区
for (const line of lines) {
if (!line.trim()) continue;
try {
processLine(line, {
controller,
encoder,
hasTextContent: () => hasTextContent,
setHasTextContent: (val) => hasTextContent = val,
reasoningContent: () => reasoningContent,
appendReasoningContent: (content) => reasoningContent += content,
isReasoningComplete: () => isReasoningComplete,
setReasoningComplete: (val) => isReasoningComplete = val
});
} catch (error) {
console.error("Error processing line:", line, error);
// 如果解析失败,直接传递原始行
controller.enqueue(encoder.encode(line + "\n"));
}
}
}
} catch (error) {
console.error("Stream error:", error);
controller.error(error);
} finally {
try {
reader.releaseLock();
} catch (e) {
console.error("Error releasing reader lock:", e);
}
controller.close();
}
},
});
return new Response(stream, {
status: response.status,
statusText: response.statusText,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}
return response;
}
}

View file

@ -0,0 +1,45 @@
import { AnthropicTransformer } from "./anthropic.transformer";
import { GeminiTransformer } from "./gemini.transformer";
import { VertexGeminiTransformer } from "./vertex-gemini.transformer";
import { DeepseekTransformer } from "./deepseek.transformer";
import { TooluseTransformer } from "./tooluse.transformer";
import { OpenrouterTransformer } from "./openrouter.transformer";
import { MaxTokenTransformer } from "./maxtoken.transformer";
import { GroqTransformer } from "./groq.transformer";
import { CleancacheTransformer } from "./cleancache.transformer";
import { EnhanceToolTransformer } from "./enhancetool.transformer";
import { ReasoningTransformer } from "./reasoning.transformer";
import { SamplingTransformer } from "./sampling.transformer";
import { MaxCompletionTokens } from "./maxcompletiontokens.transformer";
import { VertexClaudeTransformer } from "./vertex-claude.transformer";
import { CerebrasTransformer } from "./cerebras.transformer";
import { StreamOptionsTransformer } from "./streamoptions.transformer";
import { OpenAITransformer } from "./openai.transformer";
import { CustomParamsTransformer } from "./customparams.transformer";
import { VercelTransformer } from "./vercel.transformer";
import { OpenAIResponsesTransformer } from "./openai.responses.transformer";
import { ForceReasoningTransformer } from "./forcereasoning.transformer"
export default {
AnthropicTransformer,
GeminiTransformer,
VertexGeminiTransformer,
VertexClaudeTransformer,
DeepseekTransformer,
TooluseTransformer,
OpenrouterTransformer,
OpenAITransformer,
MaxTokenTransformer,
GroqTransformer,
CleancacheTransformer,
EnhanceToolTransformer,
ReasoningTransformer,
SamplingTransformer,
MaxCompletionTokens,
CerebrasTransformer,
StreamOptionsTransformer,
CustomParamsTransformer,
VercelTransformer,
OpenAIResponsesTransformer,
ForceReasoningTransformer
};

View file

@ -0,0 +1,16 @@
import { UnifiedChatRequest } from "../types/llm";
import { Transformer } from "../types/transformer";
export class MaxCompletionTokens implements Transformer {
static TransformerName = "maxcompletiontokens";
async transformRequestIn(
request: UnifiedChatRequest
): Promise<UnifiedChatRequest> {
if (request.max_tokens) {
request.max_completion_tokens = request.max_tokens;
delete request.max_tokens;
}
return request;
}
}

View file

@ -0,0 +1,18 @@
import { UnifiedChatRequest } from "../types/llm";
import { Transformer, TransformerOptions } from "../types/transformer";
export class MaxTokenTransformer implements Transformer {
static TransformerName = "maxtoken";
max_tokens: number;
constructor(private readonly options?: TransformerOptions) {
this.max_tokens = this.options?.max_tokens;
}
async transformRequestIn(request: UnifiedChatRequest): Promise<UnifiedChatRequest> {
if (request.max_tokens && request.max_tokens > this.max_tokens) {
request.max_tokens = this.max_tokens;
}
return request;
}
}

View file

@ -0,0 +1,792 @@
import { UnifiedChatRequest, MessageContent } from "@/types/llm";
import { Transformer } from "@/types/transformer";
interface ResponsesAPIOutputItem {
type: string;
id?: string;
call_id?: string;
name?: string;
arguments?: string;
content?: Array<{
type: string;
text?: string;
image_url?: string;
mime_type?: string;
image_base64?: string;
}>;
reasoning?: string;
}
interface ResponsesAPIPayload {
id: string;
object: string;
model: string;
created_at: number;
output: ResponsesAPIOutputItem[];
usage?: {
input_tokens: number;
output_tokens: number;
total_tokens: number;
};
}
interface ResponsesStreamEvent {
type: string;
item_id?: string;
output_index?: number;
delta?:
| string
| {
url?: string;
b64_json?: string;
mime_type?: string;
};
item?: {
id?: string;
type?: string;
call_id?: string;
name?: string;
content?: Array<{
type: string;
text?: string;
image_url?: string;
mime_type?: string;
}>;
reasoning?: string; // 添加 reasoning 字段支持
};
response?: {
id?: string;
model?: string;
output?: Array<{
type: string;
}>;
};
reasoning_summary?: string; // 添加推理摘要支持
}
export class OpenAIResponsesTransformer implements Transformer {
name = "openai-responses";
endPoint = "/v1/responses";
async transformRequestIn(
request: UnifiedChatRequest
): Promise<UnifiedChatRequest> {
delete request.temperature;
delete request.max_tokens;
// 处理 reasoning 参数
if (request.reasoning) {
(request as any).reasoning = {
effort: request.reasoning.effort,
summary: "detailed",
};
}
const input: any[] = [];
const systemMessages = request.messages.filter(
(msg) => msg.role === "system"
);
if (systemMessages.length > 0) {
const firstSystem = systemMessages[0];
if (Array.isArray(firstSystem.content)) {
firstSystem.content.forEach((item) => {
let text = "";
if (typeof item === "string") {
text = item;
} else if (item && typeof item === "object" && "text" in item) {
text = (item as { text: string }).text;
}
input.push({
role: "system",
content: text,
});
});
} else {
(request as any).instructions = firstSystem.content;
}
}
request.messages.forEach((message) => {
if (message.role === "system") return;
if (Array.isArray(message.content)) {
const convertedContent = message.content
.map((content) => this.normalizeRequestContent(content, message.role))
.filter(
(content): content is Record<string, unknown> => content !== null
);
if (convertedContent.length > 0) {
(message as any).content = convertedContent;
} else {
delete (message as any).content;
}
}
if (message.role === "tool") {
const toolMessage: any = { ...message };
toolMessage.type = "function_call_output";
toolMessage.call_id = message.tool_call_id;
toolMessage.output = message.content;
delete toolMessage.cache_control;
delete toolMessage.role;
delete toolMessage.tool_call_id;
delete toolMessage.content;
input.push(toolMessage);
return;
}
if (message.role === "assistant" && Array.isArray(message.tool_calls)) {
message.tool_calls.forEach((tool) => {
input.push({
type: "function_call",
arguments: tool.function.arguments,
name: tool.function.name,
call_id: tool.id,
});
});
return;
}
input.push(message);
});
(request as any).input = input;
delete (request as any).messages;
if (Array.isArray(request.tools)) {
const webSearch = request.tools.find(
(tool) => tool.function.name === "web_search"
);
(request as any).tools = request.tools
.filter((tool) => tool.function.name !== "web_search")
.map((tool) => {
if (tool.function.name === "WebSearch") {
delete tool.function.parameters.properties.allowed_domains;
}
if (tool.function.name === "Edit") {
return {
type: tool.type,
name: tool.function.name,
description: tool.function.description,
parameters: {
...tool.function.parameters,
required: [
"file_path",
"old_string",
"new_string",
"replace_all",
],
},
strict: true,
};
}
return {
type: tool.type,
name: tool.function.name,
description: tool.function.description,
parameters: tool.function.parameters,
};
});
if (webSearch) {
(request as any).tools.push({
type: "web_search",
});
}
}
request.parallel_tool_calls = false;
return request;
}
async transformResponseOut(response: Response): Promise<Response> {
const contentType = response.headers.get("Content-Type") || "";
if (contentType.includes("application/json")) {
const jsonResponse: any = await response.json();
// 检查是否为responses API格式的JSON响应
if (jsonResponse.object === "response" && jsonResponse.output) {
// 将responses格式转换为chat格式
const chatResponse = this.convertResponseToChat(jsonResponse);
return new Response(JSON.stringify(chatResponse), {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
}
// 不是responses API格式保持原样
return new Response(JSON.stringify(jsonResponse), {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
} else if (contentType.includes("text/event-stream")) {
if (!response.body) {
return response;
}
const decoder = new TextDecoder();
const encoder = new TextEncoder();
let buffer = ""; // 用于缓冲不完整的数据
let isStreamEnded = false;
const transformer = this;
const stream = new ReadableStream({
async start(controller) {
const reader = response.body!.getReader();
// 索引跟踪变量,只有在事件类型切换时才增加索引
let currentIndex = -1;
let lastEventType = "";
// 获取当前应该使用的索引的函数
const getCurrentIndex = (eventType: string) => {
if (eventType !== lastEventType) {
currentIndex++;
lastEventType = eventType;
}
return currentIndex;
};
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
if (!isStreamEnded) {
// 发送结束标记
const doneChunk = `data: [DONE]\n\n`;
controller.enqueue(encoder.encode(doneChunk));
}
break;
}
const chunk = decoder.decode(value, { stream: true });
buffer += chunk;
// 处理缓冲区中完整的数据行
let lines = buffer.split(/\r?\n/);
buffer = lines.pop() || ""; // 最后一行可能不完整,保留在缓冲区
for (const line of lines) {
if (!line.trim()) continue;
try {
if (line.startsWith("event: ")) {
// 处理事件行,暂存以便与下一行数据配对
continue;
} else if (line.startsWith("data: ")) {
const dataStr = line.slice(5).trim(); // 移除 "data: " 前缀
if (dataStr === "[DONE]") {
isStreamEnded = true;
controller.enqueue(encoder.encode(`data: [DONE]\n\n`));
continue;
}
try {
const data: ResponsesStreamEvent = JSON.parse(dataStr);
// 根据不同的事件类型转换为chat格式
if (data.type === "response.output_text.delta") {
// 将output_text.delta转换为chat格式
const chatChunk = {
id: data.item_id || "chatcmpl-" + Date.now(),
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: data.response?.model,
choices: [
{
index: getCurrentIndex(data.type),
delta: {
content: data.delta || "",
},
finish_reason: null,
},
],
};
controller.enqueue(
encoder.encode(
`data: ${JSON.stringify(chatChunk)}\n\n`
)
);
} else if (
data.type === "response.output_item.added" &&
data.item?.type === "function_call"
) {
// 处理function call开始 - 创建初始的tool call chunk
const functionCallChunk = {
id:
data.item.call_id ||
data.item.id ||
"chatcmpl-" + Date.now(),
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: data.response?.model || "gpt-5-codex-",
choices: [
{
index: getCurrentIndex(data.type),
delta: {
role: "assistant",
tool_calls: [
{
index: 0,
id: data.item.call_id || data.item.id,
function: {
name: data.item.name || "",
arguments: "",
},
type: "function",
},
],
},
finish_reason: null,
},
],
};
controller.enqueue(
encoder.encode(
`data: ${JSON.stringify(functionCallChunk)}\n\n`
)
);
} else if (
data.type === "response.output_item.added" &&
data.item?.type === "message"
) {
// 处理message item added事件
const contentItems: MessageContent[] = [];
(data.item.content || []).forEach((item: any) => {
if (item.type === "output_text") {
contentItems.push({
type: "text",
text: item.text || "",
});
}
});
const delta: any = { role: "assistant" };
if (
contentItems.length === 1 &&
contentItems[0].type === "text"
) {
delta.content = contentItems[0].text;
} else if (contentItems.length > 0) {
delta.content = contentItems;
}
if (delta.content) {
const messageChunk = {
id: data.item.id || "chatcmpl-" + Date.now(),
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: data.response?.model,
choices: [
{
index: getCurrentIndex(data.type),
delta,
finish_reason: null,
},
],
};
controller.enqueue(
encoder.encode(
`data: ${JSON.stringify(messageChunk)}\n\n`
)
);
}
} else if (
data.type === "response.output_text.annotation.added"
) {
const annotationChunk = {
id: data.item_id || "chatcmpl-" + Date.now(),
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: data.response?.model || "gpt-5-codex",
choices: [
{
index: getCurrentIndex(data.type),
delta: {
annotations: [
{
type: "url_citation",
url_citation: {
url: data.annotation?.url || "",
title: data.annotation?.title || "",
content: "",
start_index:
data.annotation?.start_index || 0,
end_index:
data.annotation?.end_index || 0,
},
},
],
},
finish_reason: null,
},
],
};
controller.enqueue(
encoder.encode(
`data: ${JSON.stringify(annotationChunk)}\n\n`
)
);
} else if (
data.type === "response.function_call_arguments.delta"
) {
// 处理function call参数增量
const functionCallChunk = {
id: data.item_id || "chatcmpl-" + Date.now(),
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: data.response?.model || "gpt-5-codex-",
choices: [
{
index: getCurrentIndex(data.type),
delta: {
tool_calls: [
{
index: 0,
function: {
arguments: data.delta || "",
},
},
],
},
finish_reason: null,
},
],
};
controller.enqueue(
encoder.encode(
`data: ${JSON.stringify(functionCallChunk)}\n\n`
)
);
} else if (data.type === "response.completed") {
// 发送结束标记 - 检查是否是tool_calls完成
const finishReason = data.response?.output?.some(
(item: any) => item.type === "function_call"
)
? "tool_calls"
: "stop";
const endChunk = {
id: data.response?.id || "chatcmpl-" + Date.now(),
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: data.response?.model || "gpt-5-codex-",
choices: [
{
index: 0,
delta: {},
finish_reason: finishReason,
},
],
};
controller.enqueue(
encoder.encode(
`data: ${JSON.stringify(endChunk)}\n\n`
)
);
isStreamEnded = true;
} else if (
data.type === "response.reasoning_summary_text.delta"
) {
// 处理推理文本,将其转换为 thinking delta 格式
const thinkingChunk = {
id: data.item_id || "chatcmpl-" + Date.now(),
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: data.response?.model,
choices: [
{
index: getCurrentIndex(data.type),
delta: {
thinking: {
content: data.delta || "",
},
},
finish_reason: null,
},
],
};
controller.enqueue(
encoder.encode(
`data: ${JSON.stringify(thinkingChunk)}\n\n`
)
);
} else if (
data.type === "response.reasoning_summary_part.done" &&
data.part
) {
const thinkingChunk = {
id: data.item_id || "chatcmpl-" + Date.now(),
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: data.response?.model,
choices: [
{
index: currentIndex,
delta: {
thinking: {
signature: data.item_id,
},
},
finish_reason: null,
},
],
};
controller.enqueue(
encoder.encode(
`data: ${JSON.stringify(thinkingChunk)}\n\n`
)
);
}
} catch (e) {
// 如果JSON解析失败传递原始行
controller.enqueue(encoder.encode(line + "\n"));
}
} else {
// 传递其他行
controller.enqueue(encoder.encode(line + "\n"));
}
} catch (error) {
console.error("Error processing line:", line, error);
// 如果解析失败,直接传递原始行
controller.enqueue(encoder.encode(line + "\n"));
}
}
}
// 处理缓冲区中剩余的数据
if (buffer.trim()) {
controller.enqueue(encoder.encode(buffer + "\n"));
}
// 确保流结束时发送结束标记
if (!isStreamEnded) {
const doneChunk = `data: [DONE]\n\n`;
controller.enqueue(encoder.encode(doneChunk));
}
} catch (error) {
console.error("Stream error:", error);
controller.error(error);
} finally {
try {
reader.releaseLock();
} catch (e) {
console.error("Error releasing reader lock:", e);
}
controller.close();
}
},
});
return new Response(stream, {
status: response.status,
statusText: response.statusText,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
"Access-Control-Allow-Origin": "*",
},
});
}
return response;
}
private normalizeRequestContent(content: any, role: string | undefined) {
// 克隆内容对象并删除缓存控制字段
const clone = { ...content };
delete clone.cache_control;
if (content.type === "text") {
return {
type: role === "assistant" ? "output_text" : "input_text",
text: content.text,
};
}
if (content.type === "image_url") {
console.log(content);
const imagePayload: Record<string, unknown> = {
type: role === "assistant" ? "output_image" : "input_image",
};
if (typeof content.image_url?.url === "string") {
imagePayload.image_url = content.image_url.url;
}
return imagePayload;
}
return null;
}
private convertResponseToChat(responseData: ResponsesAPIPayload): any {
// 从output数组中提取不同类型的输出
const messageOutput = responseData.output?.find(
(item) => item.type === "message"
);
const functionCallOutput = responseData.output?.find(
(item) => item.type === "function_call"
);
let annotations;
if (
messageOutput?.content?.length &&
messageOutput?.content[0].annotations
) {
annotations = messageOutput.content[0].annotations.map((item) => {
return {
type: "url_citation",
url_citation: {
url: item.url || "",
title: item.title || "",
content: "",
start_index: item.start_index || 0,
end_index: item.end_index || 0,
},
};
});
}
this.logger.debug({
data: annotations,
type: "url_citation",
});
let messageContent: string | MessageContent[] | null = null;
let toolCalls = null;
let thinking = null;
// 处理推理内容
if (messageOutput && messageOutput.reasoning) {
thinking = {
content: messageOutput.reasoning,
};
}
if (messageOutput && messageOutput.content) {
// 分离文本和图片内容
const textParts: string[] = [];
const imageParts: MessageContent[] = [];
messageOutput.content.forEach((item: any) => {
if (item.type === "output_text") {
textParts.push(item.text || "");
} else if (item.type === "output_image") {
const imageContent = this.buildImageContent({
url: item.image_url,
mime_type: item.mime_type,
});
if (imageContent) {
imageParts.push(imageContent);
}
} else if (item.type === "output_image_base64") {
const imageContent = this.buildImageContent({
b64_json: item.image_base64,
mime_type: item.mime_type,
});
if (imageContent) {
imageParts.push(imageContent);
}
}
});
// 构建最终内容
if (imageParts.length > 0) {
// 如果有图片,将所有内容组合成数组
const contentArray: MessageContent[] = [];
if (textParts.length > 0) {
contentArray.push({
type: "text",
text: textParts.join(""),
});
}
contentArray.push(...imageParts);
messageContent = contentArray;
} else {
// 如果只有文本,返回字符串
messageContent = textParts.join("");
}
}
if (functionCallOutput) {
// 处理function_call类型的输出
toolCalls = [
{
id: functionCallOutput.call_id || functionCallOutput.id,
function: {
name: functionCallOutput.name,
arguments: functionCallOutput.arguments,
},
type: "function",
},
];
}
// 构建chat格式的响应
const chatResponse = {
id: responseData.id || "chatcmpl-" + Date.now(),
object: "chat.completion",
created: responseData.created_at,
model: responseData.model,
choices: [
{
index: 0,
message: {
role: "assistant",
content: messageContent || null,
tool_calls: toolCalls,
thinking: thinking,
annotations: annotations,
},
logprobs: null,
finish_reason: toolCalls ? "tool_calls" : "stop",
},
],
usage: responseData.usage
? {
prompt_tokens: responseData.usage.input_tokens || 0,
completion_tokens: responseData.usage.output_tokens || 0,
total_tokens: responseData.usage.total_tokens || 0,
}
: null,
};
return chatResponse;
}
private buildImageContent(source: {
url?: string;
b64_json?: string;
mime_type?: string;
}): MessageContent | null {
if (!source) return null;
if (source.url || source.b64_json) {
return {
type: "image_url",
image_url: {
url: source.url || "",
b64_json: source.b64_json,
},
media_type: source.mime_type,
} as MessageContent;
}
return null;
}
}

View file

@ -0,0 +1,6 @@
import { Transformer } from "@/types/transformer";
export class OpenAITransformer implements Transformer {
name = "OpenAI";
endPoint = "/v1/chat/completions";
}

View file

@ -0,0 +1,357 @@
import { UnifiedChatRequest } from "@/types/llm";
import { Transformer, TransformerOptions } from "../types/transformer";
import { v4 as uuidv4 } from "uuid";
export class OpenrouterTransformer implements Transformer {
static TransformerName = "openrouter";
constructor(private readonly options?: TransformerOptions) {}
async transformRequestIn(
request: UnifiedChatRequest
): Promise<UnifiedChatRequest> {
if (!request.model.includes("claude")) {
request.messages.forEach((msg) => {
if (Array.isArray(msg.content)) {
msg.content.forEach((item: any) => {
if (item.cache_control) {
delete item.cache_control;
}
if (item.type === "image_url") {
if (!item.image_url.url.startsWith("http")) {
item.image_url.url = `${item.image_url.url}`;
}
delete item.media_type;
}
});
} else if (msg.cache_control) {
delete msg.cache_control;
}
});
} else {
request.messages.forEach((msg) => {
if (Array.isArray(msg.content)) {
msg.content.forEach((item: any) => {
if (item.type === "image_url") {
if (!item.image_url.url.startsWith("http")) {
item.image_url.url = `data:${item.media_type};base64,${item.image_url.url}`;
}
delete item.media_type;
}
});
}
});
}
Object.assign(request, this.options || {});
return request;
}
async transformResponseOut(response: Response): Promise<Response> {
if (response.headers.get("Content-Type")?.includes("application/json")) {
const jsonResponse = await response.json();
return new Response(JSON.stringify(jsonResponse), {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
} else if (response.headers.get("Content-Type")?.includes("stream")) {
if (!response.body) {
return response;
}
const decoder = new TextDecoder();
const encoder = new TextEncoder();
let hasTextContent = false;
let reasoningContent = "";
let isReasoningComplete = false;
let hasToolCall = false;
let buffer = ""; // 用于缓冲不完整的数据
const stream = new ReadableStream({
async start(controller) {
const reader = response.body!.getReader();
const processBuffer = (
buffer: string,
controller: ReadableStreamDefaultController,
encoder: TextEncoder
) => {
const lines = buffer.split("\n");
for (const line of lines) {
if (line.trim()) {
controller.enqueue(encoder.encode(line + "\n"));
}
}
};
const processLine = (
line: string,
context: {
controller: ReadableStreamDefaultController;
encoder: TextEncoder;
hasTextContent: () => boolean;
setHasTextContent: (val: boolean) => void;
reasoningContent: () => string;
appendReasoningContent: (content: string) => void;
isReasoningComplete: () => boolean;
setReasoningComplete: (val: boolean) => void;
}
) => {
const { controller, encoder } = context;
if (line.startsWith("data: ") && line.trim() !== "data: [DONE]") {
const jsonStr = line.slice(6);
try {
const data = JSON.parse(jsonStr);
if (data.usage) {
this.logger?.debug(
{ usage: data.usage, hasToolCall },
"usage"
);
data.choices[0].finish_reason = hasToolCall
? "tool_calls"
: "stop";
}
if (data.choices?.[0]?.finish_reason === "error") {
controller.enqueue(
encoder.encode(
`data: ${JSON.stringify({
error: data.choices?.[0].error,
})}\n\n`
)
);
}
if (
data.choices?.[0]?.delta?.content &&
!context.hasTextContent()
) {
context.setHasTextContent(true);
}
// Extract reasoning_content from delta
if (data.choices?.[0]?.delta?.reasoning) {
context.appendReasoningContent(
data.choices[0].delta.reasoning
);
const thinkingChunk = {
...data,
choices: [
{
...data.choices?.[0],
delta: {
...data.choices[0].delta,
thinking: {
content: data.choices[0].delta.reasoning,
},
},
},
],
};
if (thinkingChunk.choices?.[0]?.delta) {
delete thinkingChunk.choices[0].delta.reasoning;
}
const thinkingLine = `data: ${JSON.stringify(
thinkingChunk
)}\n\n`;
controller.enqueue(encoder.encode(thinkingLine));
return;
}
// Check if reasoning is complete
if (
data.choices?.[0]?.delta?.content &&
context.reasoningContent() &&
!context.isReasoningComplete()
) {
context.setReasoningComplete(true);
const signature = Date.now().toString();
const thinkingChunk = {
...data,
choices: [
{
...data.choices?.[0],
delta: {
...data.choices[0].delta,
content: null,
thinking: {
content: context.reasoningContent(),
signature: signature,
},
},
},
],
};
if (thinkingChunk.choices?.[0]?.delta) {
delete thinkingChunk.choices[0].delta.reasoning;
}
const thinkingLine = `data: ${JSON.stringify(
thinkingChunk
)}\n\n`;
controller.enqueue(encoder.encode(thinkingLine));
}
if (data.choices?.[0]?.delta?.reasoning) {
delete data.choices[0].delta.reasoning;
}
if (
data.choices?.[0]?.delta?.tool_calls?.length &&
!Number.isNaN(
parseInt(data.choices?.[0]?.delta?.tool_calls[0].id, 10)
)
) {
data.choices?.[0]?.delta?.tool_calls.forEach((tool: any) => {
tool.id = `call_${uuidv4()}`;
});
}
if (
data.choices?.[0]?.delta?.tool_calls?.length &&
!hasToolCall
) {
hasToolCall = true;
}
if (
data.choices?.[0]?.delta?.tool_calls?.length &&
context.hasTextContent()
) {
if (typeof data.choices[0].index === "number") {
data.choices[0].index += 1;
} else {
data.choices[0].index = 1;
}
}
const modifiedLine = `data: ${JSON.stringify(data)}\n\n`;
controller.enqueue(encoder.encode(modifiedLine));
} catch (e) {
// 如果JSON解析失败可能是数据不完整将原始行传递下去
controller.enqueue(encoder.encode(line + "\n"));
}
} else {
// Pass through non-data lines (like [DONE])
controller.enqueue(encoder.encode(line + "\n"));
}
};
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
// 处理缓冲区中剩余的数据
if (buffer.trim()) {
processBuffer(buffer, controller, encoder);
}
break;
}
// 检查value是否有效
if (!value || value.length === 0) {
continue;
}
let chunk;
try {
chunk = decoder.decode(value, { stream: true });
} catch (decodeError) {
console.warn("Failed to decode chunk", decodeError);
continue;
}
if (chunk.length === 0) {
continue;
}
buffer += chunk;
// 如果缓冲区过大,进行处理避免内存泄漏
if (buffer.length > 1000000) {
// 1MB 限制
console.warn(
"Buffer size exceeds limit, processing partial data"
);
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.trim()) {
try {
processLine(line, {
controller,
encoder,
hasTextContent: () => hasTextContent,
setHasTextContent: (val) => (hasTextContent = val),
reasoningContent: () => reasoningContent,
appendReasoningContent: (content) =>
(reasoningContent += content),
isReasoningComplete: () => isReasoningComplete,
setReasoningComplete: (val) =>
(isReasoningComplete = val),
});
} catch (error) {
console.error("Error processing line:", line, error);
// 如果解析失败,直接传递原始行
controller.enqueue(encoder.encode(line + "\n"));
}
}
}
continue;
}
// 处理缓冲区中完整的数据行
const lines = buffer.split("\n");
buffer = lines.pop() || ""; // 最后一行可能不完整,保留在缓冲区
for (const line of lines) {
if (!line.trim()) continue;
try {
processLine(line, {
controller,
encoder,
hasTextContent: () => hasTextContent,
setHasTextContent: (val) => (hasTextContent = val),
reasoningContent: () => reasoningContent,
appendReasoningContent: (content) =>
(reasoningContent += content),
isReasoningComplete: () => isReasoningComplete,
setReasoningComplete: (val) => (isReasoningComplete = val),
});
} catch (error) {
console.error("Error processing line:", line, error);
// 如果解析失败,直接传递原始行
controller.enqueue(encoder.encode(line + "\n"));
}
}
}
} catch (error) {
console.error("Stream error:", error);
controller.error(error);
} finally {
try {
reader.releaseLock();
} catch (e) {
console.error("Error releasing reader lock:", e);
}
controller.close();
}
},
});
return new Response(stream, {
status: response.status,
statusText: response.statusText,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}
return response;
}
}

View file

@ -0,0 +1,250 @@
import { UnifiedChatRequest } from "@/types/llm";
import { Transformer, TransformerOptions } from "../types/transformer";
export class ReasoningTransformer implements Transformer {
static TransformerName = "reasoning";
enable: any;
constructor(private readonly options?: TransformerOptions) {
this.enable = this.options?.enable ?? true;
}
async transformRequestIn(
request: UnifiedChatRequest
): Promise<UnifiedChatRequest> {
if (!this.enable) {
request.thinking = {
type: "disabled",
budget_tokens: -1,
};
request.enable_thinking = false;
return request;
}
if (request.reasoning) {
request.thinking = {
type: "enabled",
budget_tokens: request.reasoning.max_tokens,
};
request.enable_thinking = true;
}
return request;
}
async transformResponseOut(response: Response): Promise<Response> {
if (!this.enable) return response;
if (response.headers.get("Content-Type")?.includes("application/json")) {
const jsonResponse = await response.json();
if (jsonResponse.choices[0]?.message.reasoning_content) {
jsonResponse.thinking = {
content: jsonResponse.choices[0]?.message.reasoning_content
}
}
// Handle non-streaming response if needed
return new Response(JSON.stringify(jsonResponse), {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
} else if (response.headers.get("Content-Type")?.includes("stream")) {
if (!response.body) {
return response;
}
const decoder = new TextDecoder();
const encoder = new TextEncoder();
let reasoningContent = "";
let isReasoningComplete = false;
let buffer = ""; // Buffer for incomplete data
const stream = new ReadableStream({
async start(controller) {
const reader = response.body!.getReader();
// Process buffer function
const processBuffer = (
buffer: string,
controller: ReadableStreamDefaultController,
encoder: TextEncoder
) => {
const lines = buffer.split("\n");
for (const line of lines) {
if (line.trim()) {
controller.enqueue(encoder.encode(line + "\n"));
}
}
};
// Process line function
const processLine = (
line: string,
context: {
controller: ReadableStreamDefaultController;
encoder: typeof TextEncoder;
reasoningContent: () => string;
appendReasoningContent: (content: string) => void;
isReasoningComplete: () => boolean;
setReasoningComplete: (val: boolean) => void;
}
) => {
const { controller, encoder } = context;
this.logger?.debug({ line }, `Processing reason line`);
if (line.startsWith("data: ") && line.trim() !== "data: [DONE]") {
try {
const data = JSON.parse(line.slice(6));
console.log(JSON.stringify(data))
// Extract reasoning_content from delta
if (data.choices?.[0]?.delta?.reasoning_content) {
context.appendReasoningContent(
data.choices[0].delta.reasoning_content
);
const thinkingChunk = {
...data,
choices: [
{
...data.choices[0],
delta: {
...data.choices[0].delta,
thinking: {
content: data.choices[0].delta.reasoning_content,
},
},
},
],
};
delete thinkingChunk.choices[0].delta.reasoning_content;
const thinkingLine = `data: ${JSON.stringify(
thinkingChunk
)}\n\n`;
controller.enqueue(encoder.encode(thinkingLine));
return;
}
// Check if reasoning is complete (when delta has content but no reasoning_content)
if (
(data.choices?.[0]?.delta?.content ||
data.choices?.[0]?.delta?.tool_calls) &&
context.reasoningContent() &&
!context.isReasoningComplete()
) {
context.setReasoningComplete(true);
const signature = Date.now().toString();
// Create a new chunk with thinking block
const thinkingChunk = {
...data,
choices: [
{
...data.choices[0],
delta: {
...data.choices[0].delta,
content: null,
thinking: {
content: context.reasoningContent(),
signature: signature,
},
},
},
],
};
delete thinkingChunk.choices[0].delta.reasoning_content;
// Send the thinking chunk
const thinkingLine = `data: ${JSON.stringify(
thinkingChunk
)}\n\n`;
controller.enqueue(encoder.encode(thinkingLine));
}
if (data.choices?.[0]?.delta?.reasoning_content) {
delete data.choices[0].delta.reasoning_content;
}
// Send the modified chunk
if (
data.choices?.[0]?.delta &&
Object.keys(data.choices[0].delta).length > 0
) {
if (context.isReasoningComplete()) {
data.choices[0].index++;
}
const modifiedLine = `data: ${JSON.stringify(data)}\n\n`;
controller.enqueue(encoder.encode(modifiedLine));
}
} catch (e) {
// If JSON parsing fails, pass through the original line
controller.enqueue(encoder.encode(line + "\n"));
}
} else {
// Pass through non-data lines (like [DONE])
controller.enqueue(encoder.encode(line + "\n"));
}
};
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
// Process remaining data in buffer
if (buffer.trim()) {
processBuffer(buffer, controller, encoder);
}
break;
}
const chunk = decoder.decode(value, { stream: true });
buffer += chunk;
// Process complete lines from buffer
const lines = buffer.split("\n");
buffer = lines.pop() || ""; // Keep incomplete line in buffer
for (const line of lines) {
if (!line.trim()) continue;
try {
processLine(line, {
controller,
encoder: encoder,
reasoningContent: () => reasoningContent,
appendReasoningContent: (content) =>
(reasoningContent += content),
isReasoningComplete: () => isReasoningComplete,
setReasoningComplete: (val) => (isReasoningComplete = val),
});
} catch (error) {
console.error("Error processing line:", line, error);
// Pass through original line if parsing fails
controller.enqueue(encoder.encode(line + "\n"));
}
}
}
} catch (error) {
console.error("Stream error:", error);
controller.error(error);
} finally {
try {
reader.releaseLock();
} catch (e) {
console.error("Error releasing reader lock:", e);
}
controller.close();
}
},
});
return new Response(stream, {
status: response.status,
statusText: response.statusText,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}
return response;
}
}

View file

@ -0,0 +1,41 @@
import { UnifiedChatRequest } from "../types/llm";
import { Transformer, TransformerOptions } from "../types/transformer";
export class SamplingTransformer implements Transformer {
static TransformerName = "sampling";
max_tokens: number;
temperature: number;
top_p: number;
top_k: number;
repetition_penalty: number;
constructor(private readonly options?: TransformerOptions) {
this.max_tokens = this.options?.max_tokens;
this.temperature = this.options?.temperature;
this.top_p = this.options?.top_p;
this.top_k = this.options?.top_k;
this.repetition_penalty = this.options?.repetition_penalty;
}
async transformRequestIn(
request: UnifiedChatRequest
): Promise<UnifiedChatRequest> {
if (request.max_tokens && request.max_tokens > this.max_tokens) {
request.max_tokens = this.max_tokens;
}
if (typeof this.temperature !== "undefined") {
request.temperature = this.temperature;
}
if (typeof this.top_p !== "undefined") {
request.top_p = this.top_p;
}
if (typeof this.top_k !== "undefined") {
request.top_k = this.top_k;
}
if (typeof this.repetition_penalty !== "undefined") {
request.repetition_penalty = this.repetition_penalty;
}
return request;
}
}

View file

@ -0,0 +1,16 @@
import { UnifiedChatRequest } from "../types/llm";
import { Transformer, TransformerOptions } from "../types/transformer";
export class StreamOptionsTransformer implements Transformer {
name = "streamoptions";
async transformRequestIn(
request: UnifiedChatRequest
): Promise<UnifiedChatRequest> {
if (!request.stream) return request;
request.stream_options = {
include_usage: true,
};
return request;
}
}

View file

@ -0,0 +1,223 @@
import { UnifiedChatRequest } from "../types/llm";
import { Transformer } from "../types/transformer";
export class TooluseTransformer implements Transformer {
name = "tooluse";
transformRequestIn(request: UnifiedChatRequest): UnifiedChatRequest {
request.messages.push({
role: "system",
content: `<system-reminder>Tool mode is active. The user expects you to proactively execute the most suitable tool to help complete the task.
Before invoking a tool, you must carefully evaluate whether it matches the current task. If no available tool is appropriate for the task, you MUST call the \`ExitTool\` to exit tool mode — this is the only valid way to terminate tool mode.
Always prioritize completing the user's task effectively and efficiently by using tools whenever appropriate.</system-reminder>`,
});
if (request.tools?.length) {
request.tool_choice = "required";
request.tools.push({
type: "function",
function: {
name: "ExitTool",
description: `Use this tool when you are in tool mode and have completed the task. This is the only valid way to exit tool mode.
IMPORTANT: Before using this tool, ensure that none of the available tools are applicable to the current task. You must evaluate all available options only if no suitable tool can help you complete the task should you use ExitTool to terminate tool mode.
Examples:
1. Task: "Use a tool to summarize this document" Do not use ExitTool if a summarization tool is available.
2. Task: "Whats the weather today?" If no tool is available to answer, use ExitTool after reasoning that none can fulfill the task.`,
parameters: {
type: "object",
properties: {
response: {
type: "string",
description:
"Your response will be forwarded to the user exactly as returned — the tool will not modify or post-process it in any way.",
},
},
required: ["response"],
},
},
});
}
return request;
}
async transformResponseOut(response: Response): Promise<Response> {
if (response.headers.get("Content-Type")?.includes("application/json")) {
const jsonResponse = await response.json();
if (
jsonResponse?.choices?.[0]?.message.tool_calls?.length &&
jsonResponse?.choices?.[0]?.message.tool_calls[0]?.function?.name ===
"ExitTool"
) {
const toolCall = jsonResponse?.choices[0]?.message.tool_calls[0];
const toolArguments = JSON.parse(toolCall.function.arguments || "{}");
jsonResponse.choices[0].message.content = toolArguments.response || "";
delete jsonResponse.choices[0].message.tool_calls;
}
// Handle non-streaming response if needed
return new Response(JSON.stringify(jsonResponse), {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
} else if (response.headers.get("Content-Type")?.includes("stream")) {
if (!response.body) {
return response;
}
const decoder = new TextDecoder();
const encoder = new TextEncoder();
let exitToolIndex = -1;
let exitToolResponse = "";
let buffer = ""; // 用于缓冲不完整的数据
const stream = new ReadableStream({
async start(controller) {
const reader = response.body!.getReader();
const processBuffer = (
buffer: string,
controller: ReadableStreamDefaultController,
encoder: TextEncoder
) => {
const lines = buffer.split("\n");
for (const line of lines) {
if (line.trim()) {
controller.enqueue(encoder.encode(line + "\n"));
}
}
};
const processLine = (
line: string,
context: {
controller: ReadableStreamDefaultController;
encoder: TextEncoder;
exitToolIndex: () => number;
setExitToolIndex: (val: number) => void;
exitToolResponse: () => string;
appendExitToolResponse: (content: string) => void;
}
) => {
const {
controller,
encoder,
exitToolIndex,
setExitToolIndex,
appendExitToolResponse,
} = context;
if (
line.startsWith("data: ") &&
line.trim() !== "data: [DONE]"
) {
try {
const data = JSON.parse(line.slice(6));
if (data.choices[0]?.delta?.tool_calls?.length) {
const toolCall = data.choices[0].delta.tool_calls[0];
if (toolCall.function?.name === "ExitTool") {
setExitToolIndex(toolCall.index);
return;
} else if (
exitToolIndex() > -1 &&
toolCall.index === exitToolIndex() &&
toolCall.function.arguments
) {
appendExitToolResponse(toolCall.function.arguments);
try {
const response = JSON.parse(context.exitToolResponse());
data.choices = [
{
delta: {
role: "assistant",
content: response.response || "",
},
},
];
const modifiedLine = `data: ${JSON.stringify(
data
)}\n\n`;
controller.enqueue(encoder.encode(modifiedLine));
} catch (e) {}
return;
}
}
if (
data.choices?.[0]?.delta &&
Object.keys(data.choices[0].delta).length > 0
) {
const modifiedLine = `data: ${JSON.stringify(data)}\n\n`;
controller.enqueue(encoder.encode(modifiedLine));
}
} catch (e) {
// If JSON parsing fails, pass through the original line
controller.enqueue(encoder.encode(line + "\n"));
}
} else {
// Pass through non-data lines (like [DONE])
controller.enqueue(encoder.encode(line + "\n"));
}
};
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
if (buffer.trim()) {
processBuffer(buffer, controller, encoder);
}
break;
}
const chunk = decoder.decode(value, { stream: true });
buffer += chunk;
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (!line.trim()) continue;
try {
processLine(line, {
controller,
encoder,
exitToolIndex: () => exitToolIndex,
setExitToolIndex: (val) => (exitToolIndex = val),
exitToolResponse: () => exitToolResponse,
appendExitToolResponse: (content) =>
(exitToolResponse += content),
});
} catch (error) {
console.error("Error processing line:", line, error);
// 如果解析失败,直接传递原始行
controller.enqueue(encoder.encode(line + "\n"));
}
}
}
} catch (error) {
console.error("Stream error:", error);
controller.error(error);
} finally {
try {
reader.releaseLock();
} catch (e) {
console.error("Error releasing reader lock:", e);
}
controller.close();
}
},
});
return new Response(stream, {
status: response.status,
statusText: response.statusText,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}
return response;
}
}

View file

@ -0,0 +1,358 @@
import { UnifiedChatRequest } from "@/types/llm";
import { Transformer, TransformerOptions } from "../types/transformer";
import { v4 as uuidv4 } from "uuid";
export class VercelTransformer implements Transformer {
static TransformerName = "vercel";
endPoint = "/v1/chat/completions";
constructor(private readonly options?: TransformerOptions) {}
async transformRequestIn(
request: UnifiedChatRequest
): Promise<UnifiedChatRequest> {
if (!request.model.includes("claude")) {
request.messages.forEach((msg) => {
if (Array.isArray(msg.content)) {
msg.content.forEach((item: any) => {
if (item.cache_control) {
delete item.cache_control;
}
if (item.type === "image_url") {
if (!item.image_url.url.startsWith("http")) {
item.image_url.url = `data:${item.media_type};base64,${item.image_url.url}`;
}
delete item.media_type;
}
});
} else if (msg.cache_control) {
delete msg.cache_control;
}
});
} else {
request.messages.forEach((msg) => {
if (Array.isArray(msg.content)) {
msg.content.forEach((item: any) => {
if (item.type === "image_url") {
if (!item.image_url.url.startsWith("http")) {
item.image_url.url = `data:${item.media_type};base64,${item.image_url.url}`;
}
delete item.media_type;
}
});
}
});
}
Object.assign(request, this.options || {});
return request;
}
async transformResponseOut(response: Response): Promise<Response> {
if (response.headers.get("Content-Type")?.includes("application/json")) {
const jsonResponse = await response.json();
return new Response(JSON.stringify(jsonResponse), {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
} else if (response.headers.get("Content-Type")?.includes("stream")) {
if (!response.body) {
return response;
}
const decoder = new TextDecoder();
const encoder = new TextEncoder();
let hasTextContent = false;
let reasoningContent = "";
let isReasoningComplete = false;
let hasToolCall = false;
let buffer = ""; // Buffer for incomplete data
const stream = new ReadableStream({
async start(controller) {
const reader = response.body!.getReader();
const processBuffer = (
buffer: string,
controller: ReadableStreamDefaultController,
encoder: TextEncoder
) => {
const lines = buffer.split("\n");
for (const line of lines) {
if (line.trim()) {
controller.enqueue(encoder.encode(line + "\n"));
}
}
};
const processLine = (
line: string,
context: {
controller: ReadableStreamDefaultController;
encoder: TextEncoder;
hasTextContent: () => boolean;
setHasTextContent: (val: boolean) => void;
reasoningContent: () => string;
appendReasoningContent: (content: string) => void;
isReasoningComplete: () => boolean;
setReasoningComplete: (val: boolean) => void;
}
) => {
const { controller, encoder } = context;
if (line.startsWith("data: ") && line.trim() !== "data: [DONE]") {
const jsonStr = line.slice(6);
try {
const data = JSON.parse(jsonStr);
if (data.usage) {
this.logger?.debug(
{ usage: data.usage, hasToolCall },
"usage"
);
data.choices[0].finish_reason = hasToolCall
? "tool_calls"
: "stop";
}
if (data.choices?.[0]?.finish_reason === "error") {
controller.enqueue(
encoder.encode(
`data: ${JSON.stringify({
error: data.choices?.[0].error,
})}\n\n`
)
);
}
if (
data.choices?.[0]?.delta?.content &&
!context.hasTextContent()
) {
context.setHasTextContent(true);
}
// Extract reasoning_content from delta
if (data.choices?.[0]?.delta?.reasoning) {
context.appendReasoningContent(
data.choices[0].delta.reasoning
);
const thinkingChunk = {
...data,
choices: [
{
...data.choices?.[0],
delta: {
...data.choices[0].delta,
thinking: {
content: data.choices[0].delta.reasoning,
},
},
},
],
};
if (thinkingChunk.choices?.[0]?.delta) {
delete thinkingChunk.choices[0].delta.reasoning;
}
const thinkingLine = `data: ${JSON.stringify(
thinkingChunk
)}\n\n`;
controller.enqueue(encoder.encode(thinkingLine));
return;
}
// Check if reasoning is complete
if (
data.choices?.[0]?.delta?.content &&
context.reasoningContent() &&
!context.isReasoningComplete()
) {
context.setReasoningComplete(true);
const signature = Date.now().toString();
const thinkingChunk = {
...data,
choices: [
{
...data.choices?.[0],
delta: {
...data.choices[0].delta,
content: null,
thinking: {
content: context.reasoningContent(),
signature: signature,
},
},
},
],
};
if (thinkingChunk.choices?.[0]?.delta) {
delete thinkingChunk.choices[0].delta.reasoning;
}
const thinkingLine = `data: ${JSON.stringify(
thinkingChunk
)}\n\n`;
controller.enqueue(encoder.encode(thinkingLine));
}
if (data.choices?.[0]?.delta?.reasoning) {
delete data.choices[0].delta.reasoning;
}
if (
data.choices?.[0]?.delta?.tool_calls?.length &&
!Number.isNaN(
parseInt(data.choices?.[0]?.delta?.tool_calls[0].id, 10)
)
) {
data.choices?.[0]?.delta?.tool_calls.forEach((tool: any) => {
tool.id = `call_${uuidv4()}`;
});
}
if (
data.choices?.[0]?.delta?.tool_calls?.length &&
!hasToolCall
) {
hasToolCall = true;
}
if (
data.choices?.[0]?.delta?.tool_calls?.length &&
context.hasTextContent()
) {
if (typeof data.choices[0].index === "number") {
data.choices[0].index += 1;
} else {
data.choices[0].index = 1;
}
}
const modifiedLine = `data: ${JSON.stringify(data)}\n\n`;
controller.enqueue(encoder.encode(modifiedLine));
} catch (e) {
// If JSON parsing fails, data might be incomplete, pass through the original line
controller.enqueue(encoder.encode(line + "\n"));
}
} else {
// Pass through non-data lines (like [DONE])
controller.enqueue(encoder.encode(line + "\n"));
}
};
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
// Process remaining data in buffer
if (buffer.trim()) {
processBuffer(buffer, controller, encoder);
}
break;
}
// Check if value is valid
if (!value || value.length === 0) {
continue;
}
let chunk;
try {
chunk = decoder.decode(value, { stream: true });
} catch (decodeError) {
console.warn("Failed to decode chunk", decodeError);
continue;
}
if (chunk.length === 0) {
continue;
}
buffer += chunk;
// Process buffer if it gets too large to avoid memory leaks
if (buffer.length > 1000000) {
// 1MB limit
console.warn(
"Buffer size exceeds limit, processing partial data"
);
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.trim()) {
try {
processLine(line, {
controller,
encoder,
hasTextContent: () => hasTextContent,
setHasTextContent: (val) => (hasTextContent = val),
reasoningContent: () => reasoningContent,
appendReasoningContent: (content) =>
(reasoningContent += content),
isReasoningComplete: () => isReasoningComplete,
setReasoningComplete: (val) =>
(isReasoningComplete = val),
});
} catch (error) {
console.error("Error processing line:", line, error);
// If parsing fails, pass through the original line
controller.enqueue(encoder.encode(line + "\n"));
}
}
}
continue;
}
// Process complete lines in buffer
const lines = buffer.split("\n");
buffer = lines.pop() || ""; // Last line might be incomplete, keep in buffer
for (const line of lines) {
if (!line.trim()) continue;
try {
processLine(line, {
controller,
encoder,
hasTextContent: () => hasTextContent,
setHasTextContent: (val) => (hasTextContent = val),
reasoningContent: () => reasoningContent,
appendReasoningContent: (content) =>
(reasoningContent += content),
isReasoningComplete: () => isReasoningComplete,
setReasoningComplete: (val) => (isReasoningComplete = val),
});
} catch (error) {
console.error("Error processing line:", line, error);
// If parsing fails, pass through the original line
controller.enqueue(encoder.encode(line + "\n"));
}
}
}
} catch (error) {
console.error("Stream error:", error);
controller.error(error);
} finally {
try {
reader.releaseLock();
} catch (e) {
console.error("Error releasing reader lock:", e);
}
controller.close();
}
},
});
return new Response(stream, {
status: response.status,
statusText: response.statusText,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}
return response;
}
}

View file

@ -0,0 +1,81 @@
import { LLMProvider, UnifiedChatRequest } from "../types/llm";
import { Transformer } from "../types/transformer";
import {
buildRequestBody,
transformRequestOut,
transformResponseOut,
} from "../utils/vertex-claude.util";
async function getAccessToken(): Promise<string> {
try {
const { GoogleAuth } = await import('google-auth-library');
const auth = new GoogleAuth({
scopes: ['https://www.googleapis.com/auth/cloud-platform']
});
const client = await auth.getClient();
const accessToken = await client.getAccessToken();
return accessToken.token || '';
} catch (error) {
console.error('Error getting access token:', error);
throw new Error('Failed to get access token for Vertex AI. Please ensure you have set up authentication using one of these methods:\n' +
'1. Set GOOGLE_APPLICATION_CREDENTIALS to point to service account key file\n' +
'2. Run "gcloud auth application-default login"\n' +
'3. Use Google Cloud environment with default service account');
}
}
export class VertexClaudeTransformer implements Transformer {
name = "vertex-claude";
async transformRequestIn(
request: UnifiedChatRequest,
provider: LLMProvider
): Promise<Record<string, any>> {
let projectId = process.env.GOOGLE_CLOUD_PROJECT;
const location = process.env.GOOGLE_CLOUD_LOCATION || 'us-east5';
if (!projectId && process.env.GOOGLE_APPLICATION_CREDENTIALS) {
try {
const fs = await import('fs');
const keyContent = fs.readFileSync(process.env.GOOGLE_APPLICATION_CREDENTIALS, 'utf8');
const credentials = JSON.parse(keyContent);
if (credentials && credentials.project_id) {
projectId = credentials.project_id;
}
} catch (error) {
console.error('Error extracting project_id from GOOGLE_APPLICATION_CREDENTIALS:', error);
}
}
if (!projectId) {
throw new Error('Project ID is required for Vertex AI. Set GOOGLE_CLOUD_PROJECT environment variable or ensure project_id is in GOOGLE_APPLICATION_CREDENTIALS file.');
}
const accessToken = await getAccessToken();
return {
body: buildRequestBody(request),
config: {
url: new URL(
`/v1/projects/${projectId}/locations/${location}/publishers/anthropic/models/${request.model}:${request.stream ? "streamRawPredict" : "rawPredict"}`,
`https://${location}-aiplatform.googleapis.com`
).toString(),
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
},
};
}
async transformRequestOut(request: Record<string, any>): Promise<UnifiedChatRequest> {
return transformRequestOut(request);
}
async transformResponseOut(response: Response): Promise<Response> {
return transformResponseOut(response, this.name, this.logger);
}
}

View file

@ -0,0 +1,79 @@
import { LLMProvider, UnifiedChatRequest } from "../types/llm";
import { Transformer } from "../types/transformer";
import {
buildRequestBody,
transformRequestOut,
transformResponseOut,
} from "../utils/gemini.util";
async function getAccessToken(): Promise<string> {
try {
const { GoogleAuth } = await import('google-auth-library');
const auth = new GoogleAuth({
scopes: ['https://www.googleapis.com/auth/cloud-platform']
});
const client = await auth.getClient();
const accessToken = await client.getAccessToken();
return accessToken.token || '';
} catch (error) {
console.error('Error getting access token:', error);
throw new Error('Failed to get access token for Vertex AI. Please ensure you have set up authentication using one of these methods:\n' +
'1. Set GOOGLE_APPLICATION_CREDENTIALS to point to service account key file\n' +
'2. Run "gcloud auth application-default login"\n' +
'3. Use Google Cloud environment with default service account');
}
}
export class VertexGeminiTransformer implements Transformer {
name = "vertex-gemini";
async transformRequestIn(
request: UnifiedChatRequest,
provider: LLMProvider
): Promise<Record<string, any>> {
let projectId = process.env.GOOGLE_CLOUD_PROJECT;
const location = process.env.GOOGLE_CLOUD_LOCATION || 'us-central1';
if (!projectId && process.env.GOOGLE_APPLICATION_CREDENTIALS) {
try {
const fs = await import('fs');
const keyContent = fs.readFileSync(process.env.GOOGLE_APPLICATION_CREDENTIALS, 'utf8');
const credentials = JSON.parse(keyContent);
if (credentials && credentials.project_id) {
projectId = credentials.project_id;
}
} catch (error) {
console.error('Error extracting project_id from GOOGLE_APPLICATION_CREDENTIALS:', error);
}
}
if (!projectId) {
throw new Error('Project ID is required for Vertex AI. Set GOOGLE_CLOUD_PROJECT environment variable or ensure project_id is in GOOGLE_APPLICATION_CREDENTIALS file.');
}
const accessToken = await getAccessToken();
return {
body: buildRequestBody(request),
config: {
url: new URL(
`./v1beta1/projects/${projectId}/locations/${location}/publishers/google/models/${request.model}:${request.stream ? "streamGenerateContent" : "generateContent"}`,
provider.baseUrl.endsWith('/') ? provider.baseUrl : provider.baseUrl + '/' || `https://${location}-aiplatform.googleapis.com`
),
headers: {
"Authorization": `Bearer ${accessToken}`,
"x-goog-api-key": undefined,
},
},
};
}
async transformRequestOut(request: Record<string, any>): Promise<UnifiedChatRequest> {
return transformRequestOut(request);
}
async transformResponseOut(response: Response): Promise<Response> {
return transformResponseOut(response, this.name);
}
}

View file

@ -0,0 +1,239 @@
import type { ChatCompletionMessageParam as OpenAIMessage } from "openai/resources/chat/completions";
import type { MessageParam as AnthropicMessage } from "@anthropic-ai/sdk/resources/messages";
import type {
ChatCompletion,
ChatCompletionChunk,
} from "openai/resources/chat/completions";
import type {
Message,
MessageStreamEvent,
} from "@anthropic-ai/sdk/resources/messages";
import type { ChatCompletionTool } from "openai/resources/chat/completions";
import type { Tool as AnthropicTool } from "@anthropic-ai/sdk/resources/messages";
import { Transformer } from "./transformer";
export interface UrlCitation {
url: string;
title: string;
content: string;
start_index: number;
end_index: number;
}
export interface Annotation {
type: "url_citation";
url_citation?: UrlCitation;
}
// 内容类型定义
export interface TextContent {
type: "text";
text: string;
cache_control?: {
type?: string;
};
}
export interface ImageContent {
type: "image_url";
image_url: {
url: string;
};
media_type: string;
}
export type MessageContent = TextContent | ImageContent;
// 统一的消息接口
export interface UnifiedMessage {
role: "user" | "assistant" | "system" | "tool";
content: string | null | MessageContent[];
tool_calls?: Array<{
id: string;
type: "function";
function: {
name: string;
arguments: string;
};
}>;
tool_call_id?: string;
cache_control?: {
type?: string;
};
thinking?: {
content: string;
signature?: string;
};
}
// 统一的工具定义接口
export interface UnifiedTool {
type: "function";
function: {
name: string;
description: string;
parameters: {
type: "object";
properties: Record<string, any>;
required?: string[];
additionalProperties?: boolean;
$schema?: string;
};
};
}
export type ThinkLevel = "none" | "low" | "medium" | "high";
// 统一的请求接口
export interface UnifiedChatRequest {
messages: UnifiedMessage[];
model: string;
max_tokens?: number;
temperature?: number;
stream?: boolean;
tools?: UnifiedTool[];
tool_choice?:
| "auto"
| "none"
| "required"
| string
| { type: "function"; function: { name: string } };
reasoning?: {
// OpenAI-style
effort?: ThinkLevel;
// Anthropic-style
max_tokens?: number;
enabled?: boolean;
};
}
// 统一的响应接口
export interface UnifiedChatResponse {
id: string;
model: string;
content: string | null;
usage?: {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};
tool_calls?: Array<{
id: string;
type: "function";
function: {
name: string;
arguments: string;
};
}>;
annotations?: Annotation[];
}
// 流式响应相关类型
export interface StreamChunk {
id: string;
object: string;
created: number;
model: string;
choices?: Array<{
index: number;
delta: {
role?: string;
content?: string;
thinking?: {
content?: string;
signature?: string;
};
tool_calls?: Array<{
id?: string;
type?: "function";
function?: {
name?: string;
arguments?: string;
};
}>;
annotations?: Annotation[];
};
finish_reason?: string | null;
}>;
}
// Anthropic 流式事件类型
export type AnthropicStreamEvent = MessageStreamEvent;
// OpenAI 流式块类型
export type OpenAIStreamChunk = ChatCompletionChunk;
// OpenAI 特定类型
export interface OpenAIChatRequest {
messages: OpenAIMessage[];
model: string;
max_tokens?: number;
temperature?: number;
stream?: boolean;
tools?: ChatCompletionTool[];
tool_choice?:
| "auto"
| "none"
| { type: "function"; function: { name: string } };
}
// Anthropic 特定类型
export interface AnthropicChatRequest {
messages: AnthropicMessage[];
model: string;
max_tokens: number;
temperature?: number;
stream?: boolean;
system?: string;
tools?: AnthropicTool[];
tool_choice?: { type: "auto" } | { type: "tool"; name: string };
}
// 转换选项
export interface ConversionOptions {
targetProvider: "openai" | "anthropic";
sourceProvider: "openai" | "anthropic";
}
export interface LLMProvider {
name: string;
baseUrl: string;
apiKey: string;
models: string[];
transformer?: {
[key: string]: {
use?: Transformer[];
};
} & {
use?: Transformer[];
};
}
export type RegisterProviderRequest = LLMProvider;
export interface ModelRoute {
provider: string;
model: string;
fullModel: string;
}
export interface RequestRouteInfo {
provider: LLMProvider;
originalModel: string;
targetModel: string;
}
export interface ConfigProvider {
name: string;
api_base_url: string;
api_key: string;
models: string[];
transformer: {
use?: string[] | Array<any>[];
} & {
[key: string]: {
use?: string[] | Array<any>[];
};
};
}

View file

@ -0,0 +1,43 @@
import { LLMProvider, UnifiedChatRequest } from "./llm";
export interface TransformerOptions {
[key: string]: any;
}
interface TransformerWithStaticName {
new (options?: TransformerOptions): Transformer;
TransformerName?: string;
}
interface TransformerWithInstanceName {
new (): Transformer;
name?: never;
}
export type TransformerConstructor = TransformerWithStaticName;
export interface TransformerContext {
[key: string]: any;
}
export type Transformer = {
transformRequestIn?: (
request: UnifiedChatRequest,
provider: LLMProvider,
context: TransformerContext,
) => Promise<Record<string, any>>;
transformResponseIn?: (response: Response, context?: TransformerContext) => Promise<Response>;
// 将请求格式转换为通用的格式
transformRequestOut?: (request: any, context: TransformerContext) => Promise<UnifiedChatRequest>;
// 将相应格式转换为通用的格式
transformResponseOut?: (response: Response, context: TransformerContext) => Promise<Response>;
endPoint?: string;
name?: string;
auth?: (request: any, provider: LLMProvider, context: TransformerContext) => Promise<any>;
// Logger for transformer
logger?: any;
};

View file

@ -0,0 +1,478 @@
import type { ChatCompletionMessageParam as OpenAIMessage } from "openai/resources/chat/completions";
import type { MessageParam as AnthropicMessage } from "@anthropic-ai/sdk/resources/messages";
import type { ChatCompletionTool } from "openai/resources/chat/completions";
import type { Tool as AnthropicTool } from "@anthropic-ai/sdk/resources/messages";
import {
UnifiedMessage,
UnifiedChatRequest,
UnifiedTool,
OpenAIChatRequest,
AnthropicChatRequest,
ConversionOptions,
} from "../types/llm";
// Simple logger function
function log(...args: any[]) {
// Can be extended to use a proper logger
console.log(...args);
}
export function convertToolsToOpenAI(
tools: UnifiedTool[]
): ChatCompletionTool[] {
return tools.map((tool) => ({
type: "function" as const,
function: {
name: tool.function.name,
description: tool.function.description,
parameters: tool.function.parameters,
},
}));
}
export function convertToolsToAnthropic(tools: UnifiedTool[]): AnthropicTool[] {
return tools.map((tool) => ({
name: tool.function.name,
description: tool.function.description,
input_schema: tool.function.parameters,
}));
}
export function convertToolsFromOpenAI(
tools: ChatCompletionTool[]
): UnifiedTool[] {
return tools.map((tool) => ({
type: "function" as const,
function: {
name: tool.function.name,
description: tool.function.description || "",
parameters: tool.function.parameters as any,
},
}));
}
export function convertToolsFromAnthropic(
tools: AnthropicTool[]
): UnifiedTool[] {
return tools.map((tool) => ({
type: "function" as const,
function: {
name: tool.name,
description: tool.description || "",
parameters: tool.input_schema as any,
},
}));
}
export function convertToOpenAI(
request: UnifiedChatRequest
): OpenAIChatRequest {
const messages: OpenAIMessage[] = [];
const toolResponsesQueue: Map<string, any> = new Map(); // 用于存储工具响应
request.messages.forEach((msg) => {
if (msg.role === "tool" && msg.tool_call_id) {
if (!toolResponsesQueue.has(msg.tool_call_id)) {
toolResponsesQueue.set(msg.tool_call_id, []);
}
toolResponsesQueue.get(msg.tool_call_id).push({
role: "tool",
content: msg.content,
tool_call_id: msg.tool_call_id,
});
}
});
for (let i = 0; i < request.messages.length; i++) {
const msg = request.messages[i];
if (msg.role === "tool") {
continue;
}
const message: any = {
role: msg.role,
content: msg.content,
};
if (msg.tool_calls && msg.tool_calls.length > 0) {
message.tool_calls = msg.tool_calls;
if (message.content === null) {
message.content = null;
}
}
messages.push(message);
if (
msg.role === "assistant" &&
msg.tool_calls &&
msg.tool_calls.length > 0
) {
for (const toolCall of msg.tool_calls) {
if (toolResponsesQueue.has(toolCall.id)) {
const responses = toolResponsesQueue.get(toolCall.id);
responses.forEach((response) => {
messages.push(response);
});
toolResponsesQueue.delete(toolCall.id);
} else {
messages.push({
role: "tool",
content: JSON.stringify({
success: true,
message: "Tool call executed successfully",
tool_call_id: toolCall.id,
}),
tool_call_id: toolCall.id,
} as any);
}
}
}
}
if (toolResponsesQueue.size > 0) {
for (const [id, responses] of toolResponsesQueue.entries()) {
responses.forEach((response) => {
messages.push(response);
});
}
}
const result: any = {
messages,
model: request.model,
max_tokens: request.max_tokens,
temperature: request.temperature,
stream: request.stream,
};
if (request.tools && request.tools.length > 0) {
result.tools = convertToolsToOpenAI(request.tools);
if (request.tool_choice) {
if (request.tool_choice === "auto" || request.tool_choice === "none") {
result.tool_choice = request.tool_choice;
} else {
result.tool_choice = {
type: "function",
function: { name: request.tool_choice },
};
}
}
}
return result;
}
function isToolCallContent(content: string): boolean {
try {
const parsed = JSON.parse(content);
return (
Array.isArray(parsed) &&
parsed.some((item) => item.type === "tool_use" && item.id && item.name)
);
} catch {
return false;
}
}
export function convertFromOpenAI(
request: OpenAIChatRequest
): UnifiedChatRequest {
const messages: UnifiedMessage[] = request.messages.map((msg) => {
if (
msg.role === "assistant" &&
typeof msg.content === "string" &&
isToolCallContent(msg.content)
) {
try {
const toolCalls = JSON.parse(msg.content);
const convertedToolCalls = toolCalls.map((call: any) => ({
id: call.id,
type: "function" as const,
function: {
name: call.name,
arguments: JSON.stringify(call.input || {}),
},
}));
return {
role: msg.role as "user" | "assistant" | "system",
content: null,
tool_calls: convertedToolCalls,
};
} catch (error) {
return {
role: msg.role as "user" | "assistant" | "system",
content: msg.content,
};
}
}
if (msg.role === "tool") {
return {
role: msg.role as "tool",
content:
typeof msg.content === "string"
? msg.content
: JSON.stringify(msg.content),
tool_call_id: (msg as any).tool_call_id,
};
}
return {
role: msg.role as "user" | "assistant" | "system",
content:
typeof msg.content === "string"
? msg.content
: JSON.stringify(msg.content),
...((msg as any).tool_calls && { tool_calls: (msg as any).tool_calls }),
};
});
const result: UnifiedChatRequest = {
messages,
model: request.model,
max_tokens: request.max_tokens,
temperature: request.temperature,
stream: request.stream,
};
if (request.tools && request.tools.length > 0) {
result.tools = convertToolsFromOpenAI(request.tools);
if (request.tool_choice) {
if (typeof request.tool_choice === "string") {
result.tool_choice = request.tool_choice;
} else if (request.tool_choice.type === "function") {
result.tool_choice = request.tool_choice.function.name;
}
}
}
return result;
}
export function convertFromAnthropic(
request: AnthropicChatRequest
): UnifiedChatRequest {
const messages: UnifiedMessage[] = [];
if (request.system) {
messages.push({
role: "system",
content: request.system,
});
}
const pendingToolCalls: any[] = [];
const pendingTextContent: string[] = [];
let lastRole: string | null = null;
for (let i = 0; i < request.messages.length; i++) {
const msg = request.messages[i];
if (typeof msg.content === "string") {
if (
lastRole === "assistant" &&
pendingToolCalls.length > 0 &&
msg.role !== "assistant"
) {
const assistantMessage: UnifiedMessage = {
role: "assistant",
content: pendingTextContent.join("") || null,
tool_calls:
pendingToolCalls.length > 0 ? pendingToolCalls : undefined,
};
if (assistantMessage.tool_calls && pendingTextContent.length === 0) {
assistantMessage.content = null;
}
messages.push(assistantMessage);
pendingToolCalls.length = 0;
pendingTextContent.length = 0;
}
messages.push({
role: msg.role,
content: msg.content,
});
} else if (Array.isArray(msg.content)) {
const textBlocks: string[] = [];
const toolCalls: any[] = [];
const toolResults: any[] = [];
msg.content.forEach((block) => {
if (block.type === "text") {
textBlocks.push(block.text);
} else if (block.type === "tool_use") {
toolCalls.push({
id: block.id,
type: "function" as const,
function: {
name: block.name,
arguments: JSON.stringify(block.input || {}),
},
});
} else if (block.type === "tool_result") {
toolResults.push(block);
}
});
if (toolResults.length > 0) {
if (lastRole === "assistant" && pendingToolCalls.length > 0) {
const assistantMessage: UnifiedMessage = {
role: "assistant",
content: pendingTextContent.join("") || null,
tool_calls: pendingToolCalls,
};
if (pendingTextContent.length === 0) {
assistantMessage.content = null;
}
messages.push(assistantMessage);
pendingToolCalls.length = 0;
pendingTextContent.length = 0;
}
toolResults.forEach((toolResult) => {
messages.push({
role: "tool",
content:
typeof toolResult.content === "string"
? toolResult.content
: JSON.stringify(toolResult.content),
tool_call_id: toolResult.tool_use_id,
});
});
} else if (msg.role === "assistant") {
if (lastRole === "assistant") {
pendingToolCalls.push(...toolCalls);
pendingTextContent.push(...textBlocks);
} else {
if (pendingToolCalls.length > 0) {
const prevAssistantMessage: UnifiedMessage = {
role: "assistant",
content: pendingTextContent.join("") || null,
tool_calls: pendingToolCalls,
};
if (pendingTextContent.length === 0) {
prevAssistantMessage.content = null;
}
messages.push(prevAssistantMessage);
}
pendingToolCalls.length = 0;
pendingTextContent.length = 0;
pendingToolCalls.push(...toolCalls);
pendingTextContent.push(...textBlocks);
}
} else {
if (lastRole === "assistant" && pendingToolCalls.length > 0) {
const assistantMessage: UnifiedMessage = {
role: "assistant",
content: pendingTextContent.join("") || null,
tool_calls: pendingToolCalls,
};
if (pendingTextContent.length === 0) {
assistantMessage.content = null;
}
messages.push(assistantMessage);
pendingToolCalls.length = 0;
pendingTextContent.length = 0;
}
const message: UnifiedMessage = {
role: msg.role,
content: textBlocks.join("") || null,
};
if (toolCalls.length > 0) {
message.tool_calls = toolCalls;
if (textBlocks.length === 0) {
message.content = null;
}
}
messages.push(message);
}
} else {
if (lastRole === "assistant" && pendingToolCalls.length > 0) {
const assistantMessage: UnifiedMessage = {
role: "assistant",
content: pendingTextContent.join("") || null,
tool_calls: pendingToolCalls,
};
if (pendingTextContent.length === 0) {
assistantMessage.content = null;
}
messages.push(assistantMessage);
pendingToolCalls.length = 0;
pendingTextContent.length = 0;
}
messages.push({
role: msg.role,
content: JSON.stringify(msg.content),
});
}
lastRole = msg.role;
}
if (lastRole === "assistant" && pendingToolCalls.length > 0) {
const assistantMessage: UnifiedMessage = {
role: "assistant",
content: pendingTextContent.join("") || null,
tool_calls: pendingToolCalls,
};
if (pendingTextContent.length === 0) {
assistantMessage.content = null;
}
messages.push(assistantMessage);
}
const result: UnifiedChatRequest = {
messages,
model: request.model,
max_tokens: request.max_tokens,
temperature: request.temperature,
stream: request.stream,
};
if (request.tools && request.tools.length > 0) {
result.tools = convertToolsFromAnthropic(request.tools);
if (request.tool_choice) {
if (request.tool_choice.type === "auto") {
result.tool_choice = "auto";
} else if (request.tool_choice.type === "tool") {
result.tool_choice = request.tool_choice.name;
}
}
}
return result;
}
export function convertRequest(
request: OpenAIChatRequest | AnthropicChatRequest | UnifiedChatRequest,
options: ConversionOptions
): OpenAIChatRequest | AnthropicChatRequest {
let unifiedRequest: UnifiedChatRequest;
if (options.sourceProvider === "openai") {
unifiedRequest = convertFromOpenAI(request as OpenAIChatRequest);
} else if (options.sourceProvider === "anthropic") {
unifiedRequest = convertFromAnthropic(request as AnthropicChatRequest);
} else {
unifiedRequest = request as UnifiedChatRequest;
}
if (options.targetProvider === "openai") {
return convertToOpenAI(unifiedRequest);
} else {
// For now, return unified request since Anthropic format is similar
return unifiedRequest as any;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,9 @@
export const formatBase64 = (data: string, media_type: string) => {
if (data.includes("base64")) {
data = data.split("base64").pop() as string;
if (data.startsWith(",")) {
data = data.slice(1);
}
}
return `data:${media_type};base64,${data}`;
};

View file

@ -0,0 +1,57 @@
import { ProxyAgent } from "undici";
import { UnifiedChatRequest } from "../types/llm";
export function sendUnifiedRequest(
url: URL | string,
request: UnifiedChatRequest,
config: any,
context: any,
logger?: any
): Promise<Response> {
const headers = new Headers({
"Content-Type": "application/json",
});
if (config.headers) {
Object.entries(config.headers).forEach(([key, value]) => {
if (value) {
headers.set(key, value as string);
}
});
}
let combinedSignal: AbortSignal;
const timeoutSignal = AbortSignal.timeout(config.TIMEOUT ?? 60 * 1000 * 60);
if (config.signal) {
const controller = new AbortController();
const abortHandler = () => controller.abort();
config.signal.addEventListener("abort", abortHandler);
timeoutSignal.addEventListener("abort", abortHandler);
combinedSignal = controller.signal;
} else {
combinedSignal = timeoutSignal;
}
const fetchOptions: RequestInit = {
method: "POST",
headers: headers,
body: JSON.stringify(request),
signal: combinedSignal,
};
if (config.httpsProxy) {
(fetchOptions as any).dispatcher = new ProxyAgent(
new URL(config.httpsProxy).toString()
);
}
logger?.debug(
{
reqId: context.req.id,
request: fetchOptions,
headers: Object.fromEntries(headers.entries()),
requestUrl: typeof url === "string" ? url : url.toString(),
useProxy: config.httpsProxy,
},
"final request"
);
return fetch(typeof url === "string" ? url : url.toString(), fetchOptions);
}

View file

@ -0,0 +1,8 @@
import { ThinkLevel } from "@/types/llm";
export const getThinkLevel = (thinking_budget: number): ThinkLevel => {
if (thinking_budget <= 0) return "none";
if (thinking_budget <= 1024) return "low";
if (thinking_budget <= 8192) return "medium";
return "high";
};

View file

@ -0,0 +1,51 @@
import JSON5 from "json5";
import { jsonrepair } from "jsonrepair";
/**
*
* Parse tool call arguments function
* JSON解析JSON5解析使jsonrepair进行安全修复
* First try standard JSON parsing, then JSON5 parsing, finally use jsonrepair for safe repair
*
* @param argsString - / Parameter string to parse
* @returns / Parsed parameter object or safe empty object
*/
export function parseToolArguments(argsString: string, logger?: any): string {
// Handle empty or null input
if (!argsString || argsString.trim() === "" || argsString === "{}") {
return "{}";
}
try {
// First attempt: Standard JSON parsing
JSON.parse(argsString);
logger?.debug(`工具调用参数标准JSON解析成功 / Tool arguments standard JSON parsing successful`);
return argsString;
} catch (jsonError: any) {
try {
// Second attempt: JSON5 parsing for relaxed syntax
const args = JSON5.parse(argsString);
logger?.debug(`工具调用参数JSON5解析成功 / Tool arguments JSON5 parsing successful`);
return JSON.stringify(args);
} catch (json5Error: any) {
try {
// Third attempt: Safe JSON repair without code execution
const repairedJson = jsonrepair(argsString);
logger?.debug(`工具调用参数安全修复成功 / Tool arguments safely repaired`);
return repairedJson;
} catch (repairError: any) {
// All parsing attempts failed - log errors and return safe fallback
logger?.error(
`JSON解析失败 / JSON parsing failed: ${jsonError.message}. ` +
`JSON5解析失败 / JSON5 parsing failed: ${json5Error.message}. ` +
`JSON修复失败 / JSON repair failed: ${repairError.message}. ` +
`输入数据 / Input data: ${JSON.stringify(argsString)}`
);
// Return safe empty object as fallback instead of potentially malformed input
logger?.debug(`返回安全的空对象作为后备方案 / Returning safe empty object as fallback`);
return "{}";
}
}
}
}

View file

@ -0,0 +1,542 @@
import { UnifiedChatRequest, UnifiedMessage, UnifiedTool } from "../types/llm";
// Vertex Claude消息接口
interface ClaudeMessage {
role: "user" | "assistant";
content: Array<{
type: "text" | "image";
text?: string;
source?: {
type: "base64";
media_type: string;
data: string;
};
}>;
}
// Vertex Claude工具接口
interface ClaudeTool {
name: string;
description: string;
input_schema: {
type: string;
properties: Record<string, any>;
required?: string[];
additionalProperties?: boolean;
$schema?: string;
};
}
// Vertex Claude请求接口
interface VertexClaudeRequest {
anthropic_version: "vertex-2023-10-16";
messages: ClaudeMessage[];
max_tokens: number;
stream?: boolean;
temperature?: number;
top_p?: number;
top_k?: number;
tools?: ClaudeTool[];
tool_choice?: "auto" | "none" | { type: "tool"; name: string };
}
// Vertex Claude响应接口
interface VertexClaudeResponse {
content: Array<{
type: "text";
text: string;
}>;
id: string;
model: string;
role: "assistant";
stop_reason: string;
stop_sequence: null;
type: "message";
usage: {
input_tokens: number;
output_tokens: number;
};
tool_use?: Array<{
id: string;
name: string;
input: Record<string, any>;
}>;
}
export function buildRequestBody(
request: UnifiedChatRequest
): VertexClaudeRequest {
const messages: ClaudeMessage[] = [];
for (let i = 0; i < request.messages.length; i++) {
const message = request.messages[i];
const isLastMessage = i === request.messages.length - 1;
const isAssistantMessage = message.role === "assistant";
const content: ClaudeMessage["content"] = [];
if (typeof message.content === "string") {
// 保留所有字符串内容,即使是空字符串,因为可能包含重要信息
content.push({
type: "text",
text: message.content,
});
} else if (Array.isArray(message.content)) {
message.content.forEach((item) => {
if (item.type === "text") {
// 保留所有文本内容,即使是空字符串
content.push({
type: "text",
text: item.text || "",
});
} else if (item.type === "image_url") {
// 处理图片内容
content.push({
type: "image",
source: {
type: "base64",
media_type: item.media_type || "image/jpeg",
data: item.image_url.url,
},
});
}
});
}
// 只跳过完全空的非最后一条消息(没有内容和工具调用)
if (
!isLastMessage &&
content.length === 0 &&
!message.tool_calls &&
!message.content
) {
continue;
}
// 对于最后一条 assistant 消息,如果没有内容但有工具调用,则添加空内容
if (
isLastMessage &&
isAssistantMessage &&
content.length === 0 &&
message.tool_calls
) {
content.push({
type: "text",
text: "",
});
}
messages.push({
role: message.role === "assistant" ? "assistant" : "user",
content,
});
}
const requestBody: VertexClaudeRequest = {
anthropic_version: "vertex-2023-10-16",
messages,
max_tokens: request.max_tokens || 1000,
stream: request.stream || false,
...(request.temperature && { temperature: request.temperature }),
};
// 处理工具定义
if (request.tools && request.tools.length > 0) {
requestBody.tools = request.tools.map((tool: UnifiedTool) => ({
name: tool.function.name,
description: tool.function.description,
input_schema: tool.function.parameters,
}));
}
// 处理工具选择
if (request.tool_choice) {
if (request.tool_choice === "auto" || request.tool_choice === "none") {
requestBody.tool_choice = request.tool_choice;
} else if (typeof request.tool_choice === "string") {
// 如果 tool_choice 是字符串,假设是工具名称
requestBody.tool_choice = {
type: "tool",
name: request.tool_choice,
};
}
}
return requestBody;
}
export function transformRequestOut(
request: Record<string, any>
): UnifiedChatRequest {
const vertexRequest = request as VertexClaudeRequest;
const messages: UnifiedMessage[] = vertexRequest.messages.map((msg) => {
const content = msg.content.map((item) => {
if (item.type === "text") {
return {
type: "text" as const,
text: item.text || "",
};
} else if (item.type === "image" && item.source) {
return {
type: "image_url" as const,
image_url: {
url: item.source.data,
},
media_type: item.source.media_type,
};
}
return {
type: "text" as const,
text: "",
};
});
return {
role: msg.role,
content,
};
});
const result: UnifiedChatRequest = {
messages,
model: request.model || "claude-sonnet-4@20250514",
max_tokens: vertexRequest.max_tokens,
temperature: vertexRequest.temperature,
stream: vertexRequest.stream,
};
// 处理工具定义
if (vertexRequest.tools && vertexRequest.tools.length > 0) {
result.tools = vertexRequest.tools.map((tool) => ({
type: "function" as const,
function: {
name: tool.name,
description: tool.description,
parameters: {
type: "object" as const,
properties: tool.input_schema.properties,
required: tool.input_schema.required,
additionalProperties: tool.input_schema.additionalProperties,
$schema: tool.input_schema.$schema,
},
},
}));
}
// 处理工具选择
if (vertexRequest.tool_choice) {
if (typeof vertexRequest.tool_choice === "string") {
result.tool_choice = vertexRequest.tool_choice;
} else if (vertexRequest.tool_choice.type === "tool") {
result.tool_choice = vertexRequest.tool_choice.name;
}
}
return result;
}
export async function transformResponseOut(
response: Response,
providerName: string,
logger?: any
): Promise<Response> {
if (response.headers.get("Content-Type")?.includes("application/json")) {
const jsonResponse = (await response.json()) as VertexClaudeResponse;
// 处理工具调用
let tool_calls = undefined;
if (jsonResponse.tool_use && jsonResponse.tool_use.length > 0) {
tool_calls = jsonResponse.tool_use.map((tool) => ({
id: tool.id,
type: "function" as const,
function: {
name: tool.name,
arguments: JSON.stringify(tool.input),
},
}));
}
// 转换为OpenAI格式的响应
const res = {
id: jsonResponse.id,
choices: [
{
finish_reason: jsonResponse.stop_reason || null,
index: 0,
message: {
content: jsonResponse.content[0]?.text || "",
role: "assistant",
...(tool_calls && { tool_calls }),
},
},
],
created: parseInt(new Date().getTime() / 1000 + "", 10),
model: jsonResponse.model,
object: "chat.completion",
usage: {
completion_tokens: jsonResponse.usage.output_tokens,
prompt_tokens: jsonResponse.usage.input_tokens,
total_tokens:
jsonResponse.usage.input_tokens + jsonResponse.usage.output_tokens,
},
};
return new Response(JSON.stringify(res), {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
} else if (response.headers.get("Content-Type")?.includes("stream")) {
// 处理流式响应
if (!response.body) {
return response;
}
const decoder = new TextDecoder();
const encoder = new TextEncoder();
const processLine = (
line: string,
controller: ReadableStreamDefaultController
) => {
if (line.startsWith("data: ")) {
const chunkStr = line.slice(6).trim();
if (chunkStr) {
logger?.debug({ chunkStr }, `${providerName} chunk:`);
try {
const chunk = JSON.parse(chunkStr);
// 处理 Anthropic 原生格式的流式响应
if (
chunk.type === "content_block_delta" &&
chunk.delta?.type === "text_delta"
) {
// 这是 Anthropic 原生格式,需要转换为 OpenAI 格式
const res = {
choices: [
{
delta: {
role: "assistant",
content: chunk.delta.text || "",
},
finish_reason: null,
index: 0,
logprobs: null,
},
],
created: parseInt(new Date().getTime() / 1000 + "", 10),
id: chunk.id || "",
model: chunk.model || "",
object: "chat.completion.chunk",
system_fingerprint: "fp_a49d71b8a1",
usage: {
completion_tokens: chunk.usage?.output_tokens || 0,
prompt_tokens: chunk.usage?.input_tokens || 0,
total_tokens:
(chunk.usage?.input_tokens || 0) +
(chunk.usage?.output_tokens || 0),
},
};
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(res)}\n\n`)
);
} else if (
chunk.type === "content_block_delta" &&
chunk.delta?.type === "input_json_delta"
) {
// 处理工具调用的参数增量
const res = {
choices: [
{
delta: {
tool_calls: [
{
index: chunk.index || 0,
function: {
arguments: chunk.delta.partial_json || "",
},
},
],
},
finish_reason: null,
index: 0,
logprobs: null,
},
],
created: parseInt(new Date().getTime() / 1000 + "", 10),
id: chunk.id || "",
model: chunk.model || "",
object: "chat.completion.chunk",
system_fingerprint: "fp_a49d71b8a1",
usage: {
completion_tokens: chunk.usage?.output_tokens || 0,
prompt_tokens: chunk.usage?.input_tokens || 0,
total_tokens:
(chunk.usage?.input_tokens || 0) +
(chunk.usage?.output_tokens || 0),
},
};
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(res)}\n\n`)
);
} else if (
chunk.type === "content_block_start" &&
chunk.content_block?.type === "tool_use"
) {
// 处理工具调用开始
const res = {
choices: [
{
delta: {
tool_calls: [
{
index: chunk.index || 0,
id: chunk.content_block.id,
type: "function",
function: {
name: chunk.content_block.name,
arguments: "",
},
},
],
},
finish_reason: null,
index: 0,
logprobs: null,
},
],
created: parseInt(new Date().getTime() / 1000 + "", 10),
id: chunk.id || "",
model: chunk.model || "",
object: "chat.completion.chunk",
system_fingerprint: "fp_a49d71b8a1",
usage: {
completion_tokens: chunk.usage?.output_tokens || 0,
prompt_tokens: chunk.usage?.input_tokens || 0,
total_tokens:
(chunk.usage?.input_tokens || 0) +
(chunk.usage?.output_tokens || 0),
},
};
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(res)}\n\n`)
);
} else if (chunk.type === "message_delta") {
// 处理消息结束
const res = {
choices: [
{
delta: {},
finish_reason:
chunk.delta?.stop_reason === "tool_use"
? "tool_calls"
: chunk.delta?.stop_reason === "max_tokens"
? "length"
: chunk.delta?.stop_reason === "stop_sequence"
? "content_filter"
: "stop",
index: 0,
logprobs: null,
},
],
created: parseInt(new Date().getTime() / 1000 + "", 10),
id: chunk.id || "",
model: chunk.model || "",
object: "chat.completion.chunk",
system_fingerprint: "fp_a49d71b8a1",
usage: {
completion_tokens: chunk.usage?.output_tokens || 0,
prompt_tokens: chunk.usage?.input_tokens || 0,
total_tokens:
(chunk.usage?.input_tokens || 0) +
(chunk.usage?.output_tokens || 0),
},
};
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(res)}\n\n`)
);
} else if (chunk.type === "message_stop") {
// 发送结束标记
controller.enqueue(encoder.encode(`data: [DONE]\n\n`));
} else {
// 处理其他格式的响应(保持原有逻辑作为后备)
const res = {
choices: [
{
delta: {
role: "assistant",
content: chunk.content?.[0]?.text || "",
},
finish_reason: chunk.stop_reason?.toLowerCase() || null,
index: 0,
logprobs: null,
},
],
created: parseInt(new Date().getTime() / 1000 + "", 10),
id: chunk.id || "",
model: chunk.model || "",
object: "chat.completion.chunk",
system_fingerprint: "fp_a49d71b8a1",
usage: {
completion_tokens: chunk.usage?.output_tokens || 0,
prompt_tokens: chunk.usage?.input_tokens || 0,
total_tokens:
(chunk.usage?.input_tokens || 0) +
(chunk.usage?.output_tokens || 0),
},
};
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(res)}\n\n`)
);
}
} catch (error: any) {
logger?.error(
`Error parsing ${providerName} stream chunk`,
chunkStr,
error.message
);
}
}
}
};
const stream = new ReadableStream({
async start(controller) {
const reader = response.body!.getReader();
let buffer = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
if (buffer) {
processLine(buffer, controller);
}
break;
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
processLine(line, controller);
}
}
} catch (error) {
controller.error(error);
} finally {
controller.close();
}
},
});
return new Response(stream, {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
}
return response;
}

View file

@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"moduleResolution": "node",
"declaration": true,
"sourceMap": true,
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}

View file

@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitives from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitives.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitives.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitives.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitives.Indicator>
</CheckboxPrimitives.Root>
))
Checkbox.displayName = CheckboxPrimitives.Root.displayName
export { Checkbox }

View file

@ -0,0 +1,160 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View file

@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }

23
scripts/build-core.js Normal file
View file

@ -0,0 +1,23 @@
#!/usr/bin/env node
const { execSync } = require('child_process');
const path = require('path');
const fs = require('fs');
console.log('Building Core package (@musistudio/llms)...');
try {
const coreDir = path.join(__dirname, '../packages/core');
// Build using the core package's build script
console.log('Building core package (CJS and ESM)...');
execSync('pnpm build', {
stdio: 'inherit',
cwd: coreDir
});
console.log('Core package build completed successfully!');
} catch (error) {
console.error('Core package build failed:', error.message);
process.exit(1);
}