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);
}
/**
* 处理请求转换器链
* 依次执行transformRequestOut、provider transformers、model-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 transformers、model-specific transformers、transformer的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);
}