@userdaoo/iflow-api-bridge 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,102 @@
1
+ /**
2
+ * OpenAI API 兼容类型定义
3
+ */
4
+
5
+ export interface ChatCompletionMessage {
6
+ role: 'system' | 'user' | 'assistant' | 'tool';
7
+ content: string | null;
8
+ name?: string;
9
+ tool_calls?: ToolCall[];
10
+ tool_call_id?: string;
11
+ }
12
+
13
+ export interface ToolCall {
14
+ id: string;
15
+ type: 'function';
16
+ function: {
17
+ name: string;
18
+ arguments: string;
19
+ };
20
+ }
21
+
22
+ export interface ChatCompletionRequest {
23
+ model: string;
24
+ messages: ChatCompletionMessage[];
25
+ stream?: boolean;
26
+ temperature?: number;
27
+ top_p?: number;
28
+ max_tokens?: number;
29
+ presence_penalty?: number;
30
+ frequency_penalty?: number;
31
+ stop?: string | string[];
32
+ tools?: ToolDefinition[];
33
+ tool_choice?: 'none' | 'auto' | { type: 'function'; function: { name: string } };
34
+ }
35
+
36
+ export interface ToolDefinition {
37
+ type: 'function';
38
+ function: {
39
+ name: string;
40
+ description?: string;
41
+ parameters?: Record<string, unknown>;
42
+ };
43
+ }
44
+
45
+ export interface ChatCompletionResponse {
46
+ id: string;
47
+ object: 'chat.completion';
48
+ created: number;
49
+ model: string;
50
+ choices: ChatCompletionChoice[];
51
+ usage: UsageInfo;
52
+ }
53
+
54
+ export interface ChatCompletionChoice {
55
+ index: number;
56
+ message: ChatCompletionMessage;
57
+ finish_reason: 'stop' | 'length' | 'tool_calls' | null;
58
+ }
59
+
60
+ export interface ChatCompletionStreamResponse {
61
+ id: string;
62
+ object: 'chat.completion.chunk';
63
+ created: number;
64
+ model: string;
65
+ choices: ChatCompletionStreamChoice[];
66
+ }
67
+
68
+ export interface ChatCompletionStreamChoice {
69
+ index: number;
70
+ delta: {
71
+ role?: string;
72
+ content?: string;
73
+ tool_calls?: ToolCall[];
74
+ };
75
+ finish_reason: 'stop' | 'length' | 'tool_calls' | null;
76
+ }
77
+
78
+ export interface UsageInfo {
79
+ prompt_tokens: number;
80
+ completion_tokens: number;
81
+ total_tokens: number;
82
+ }
83
+
84
+ export interface ModelInfo {
85
+ id: string;
86
+ object: 'model';
87
+ created: number;
88
+ owned_by: string;
89
+ }
90
+
91
+ export interface ModelsResponse {
92
+ object: 'list';
93
+ data: ModelInfo[];
94
+ }
95
+
96
+ export interface ErrorResponse {
97
+ error: {
98
+ message: string;
99
+ type: string;
100
+ code: string;
101
+ };
102
+ }
package/src/server.ts ADDED
@@ -0,0 +1,341 @@
1
+ /**
2
+ * HTTP 服务器和 OpenAI API 路由
3
+ */
4
+
5
+ import express, { type Request, type Response, type NextFunction } from 'express';
6
+ import cors from 'cors';
7
+ import { IFlowAdapter } from './adapter.js';
8
+ import {
9
+ type ChatCompletionRequest,
10
+ type ChatCompletionResponse,
11
+ type ChatCompletionStreamResponse,
12
+ type ModelsResponse,
13
+ type ErrorResponse,
14
+ } from './openai/types.js';
15
+ import {
16
+ generateId,
17
+ getTimestamp,
18
+ messagesToIFlowPrompt,
19
+ createCompletionResponse,
20
+ createStreamChunk,
21
+ calculateUsage,
22
+ formatSSE,
23
+ SSE_DONE,
24
+ AVAILABLE_MODELS,
25
+ getDefaultModel,
26
+ } from './openai/transformer.js';
27
+
28
+ export interface ServerOptions {
29
+ port: number;
30
+ host?: string;
31
+ cors?: boolean;
32
+ apiKey?: string;
33
+ model?: string;
34
+ }
35
+
36
+ export class IFlowAPIServer {
37
+ private app: express.Application;
38
+ private adapter: IFlowAdapter;
39
+ private options: ServerOptions;
40
+
41
+ constructor(options: ServerOptions) {
42
+ this.options = options;
43
+ this.app = express();
44
+ this.adapter = new IFlowAdapter({
45
+ model: options.model,
46
+ });
47
+
48
+ this.setupMiddleware();
49
+ this.setupRoutes();
50
+ this.setupErrorHandler();
51
+ }
52
+
53
+ /**
54
+ * 设置中间件
55
+ */
56
+ private setupMiddleware(): void {
57
+ // JSON 解析
58
+ this.app.use(express.json({ limit: '10mb' }));
59
+
60
+ // CORS
61
+ if (this.options.cors !== false) {
62
+ this.app.use(cors({
63
+ origin: '*',
64
+ methods: ['GET', 'POST', 'OPTIONS'],
65
+ allowedHeaders: ['Content-Type', 'Authorization'],
66
+ }));
67
+ }
68
+
69
+ // API Key 验证(如果配置了)
70
+ if (this.options.apiKey) {
71
+ this.app.use((req: Request, res: Response, next: NextFunction) => {
72
+ // 健康检查端点不需要认证
73
+ if (req.path === '/health') {
74
+ return next();
75
+ }
76
+
77
+ const authHeader = req.headers.authorization;
78
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
79
+ return res.status(401).json(this.createError('未提供 API Key', 'authentication_error'));
80
+ }
81
+
82
+ const token = authHeader.substring(7);
83
+ if (token !== this.options.apiKey) {
84
+ return res.status(401).json(this.createError('API Key 无效', 'authentication_error'));
85
+ }
86
+
87
+ next();
88
+ });
89
+ }
90
+ }
91
+
92
+ /**
93
+ * 设置路由
94
+ */
95
+ private setupRoutes(): void {
96
+ // 健康检查
97
+ this.app.get('/health', (req: Request, res: Response) => {
98
+ res.json({
99
+ status: 'ok',
100
+ connected: this.adapter.isConnected(),
101
+ timestamp: new Date().toISOString(),
102
+ });
103
+ });
104
+
105
+ // 模型列表
106
+ this.app.get('/v1/models', async (req: Request, res: Response) => {
107
+ const response: ModelsResponse = {
108
+ object: 'list',
109
+ data: AVAILABLE_MODELS.map(model => ({
110
+ id: model.id,
111
+ object: 'model',
112
+ created: getTimestamp(),
113
+ owned_by: 'iflow',
114
+ })),
115
+ };
116
+ res.json(response);
117
+ });
118
+
119
+ // 聊天完成
120
+ this.app.post('/v1/chat/completions', async (req: Request, res: Response) => {
121
+ console.log(`[${new Date().toISOString()}] 收到聊天请求`);
122
+ console.log('请求体:', JSON.stringify(req.body, null, 2));
123
+
124
+ try {
125
+ const body = req.body as ChatCompletionRequest;
126
+
127
+ // 验证请求
128
+ if (!body.messages || !Array.isArray(body.messages) || body.messages.length === 0) {
129
+ console.log('请求验证失败: messages 为空');
130
+ return res.status(400).json(this.createError('messages 不能为空', 'invalid_request_error'));
131
+ }
132
+
133
+ const isStream = body.stream === true;
134
+ const model = body.model || getDefaultModel();
135
+
136
+ console.log(`流式: ${isStream}, 模型: ${model}`);
137
+
138
+ // 转换消息为 iFlow 提示词
139
+ const prompt = messagesToIFlowPrompt(body.messages);
140
+ console.log('提示词:', prompt.substring(0, 100) + '...');
141
+
142
+ if (isStream) {
143
+ console.log('处理流式响应...');
144
+ await this.handleStreamResponse(res, prompt, model);
145
+ } else {
146
+ console.log('处理非流式响应...');
147
+ await this.handleNonStreamResponse(res, prompt, model);
148
+ }
149
+ } catch (error) {
150
+ console.error('处理请求错误:', error);
151
+ res.status(500).json(this.createError('内部服务器错误', 'internal_error'));
152
+ }
153
+ });
154
+ }
155
+
156
+ /**
157
+ * 处理流式响应
158
+ */
159
+ private async handleStreamResponse(res: Response, prompt: string, model: string): Promise<void> {
160
+ const id = generateId();
161
+
162
+ res.setHeader('Content-Type', 'text/event-stream');
163
+ res.setHeader('Cache-Control', 'no-cache');
164
+ res.setHeader('Connection', 'keep-alive');
165
+
166
+ // 设置 60 秒超时
167
+ const timeout = setTimeout(() => {
168
+ res.write(formatSSE(this.createError('请求超时', 'timeout_error')));
169
+ res.end();
170
+ }, 60000);
171
+
172
+ try {
173
+ // 发送开始标记(角色)
174
+ const startChunk: ChatCompletionStreamResponse = {
175
+ id,
176
+ object: 'chat.completion.chunk',
177
+ created: getTimestamp(),
178
+ model,
179
+ choices: [{
180
+ index: 0,
181
+ delta: { role: 'assistant' },
182
+ finish_reason: null,
183
+ }],
184
+ };
185
+ res.write(formatSSE(startChunk));
186
+
187
+ // 流式发送内容
188
+ let fullContent = '';
189
+ let isDone = false;
190
+ for await (const chunk of this.adapter.sendMessageStream(prompt)) {
191
+ if (isDone) break;
192
+
193
+ switch (chunk.type) {
194
+ case 'content':
195
+ if (chunk.content) {
196
+ fullContent += chunk.content;
197
+ const streamChunk = createStreamChunk(id, model, chunk.content);
198
+ res.write(formatSSE(streamChunk));
199
+ }
200
+ break;
201
+
202
+ case 'tool_call':
203
+ // 工具调用信息可以记录日志,但不发送到客户端
204
+ console.log(`工具调用: ${chunk.toolName} - ${chunk.toolStatus}`);
205
+ break;
206
+
207
+ case 'done':
208
+ // 收到完成信号,标记结束
209
+ isDone = true;
210
+ break;
211
+
212
+ case 'error':
213
+ console.error('流式处理错误:', chunk.error);
214
+ res.write(formatSSE(this.createError(
215
+ chunk.error || '流式响应错误',
216
+ 'streaming_error',
217
+ 'streaming_error'
218
+ )));
219
+ isDone = true;
220
+ break;
221
+ }
222
+ }
223
+
224
+ // 发送结束标记
225
+ const endChunk = createStreamChunk(id, model, '', 'stop');
226
+ res.write(formatSSE(endChunk));
227
+ res.write(SSE_DONE);
228
+ res.end();
229
+ } catch (error) {
230
+ console.error('流式响应错误:', error);
231
+ res.write(formatSSE(this.createError('流式响应失败', 'streaming_error')));
232
+ res.end();
233
+ } finally {
234
+ clearTimeout(timeout);
235
+ }
236
+ }
237
+
238
+ /**
239
+ * 处理非流式响应
240
+ */
241
+ private async handleNonStreamResponse(res: Response, prompt: string, model: string): Promise<void> {
242
+ const id = generateId();
243
+
244
+ // 设置 60 秒超时
245
+ const timeout = setTimeout(() => {
246
+ res.status(504).json(this.createError('请求超时', 'timeout_error'));
247
+ }, 60000);
248
+
249
+ try {
250
+ const response = await this.adapter.sendMessage(prompt);
251
+ clearTimeout(timeout);
252
+
253
+ const usage = calculateUsage(prompt, response.content);
254
+
255
+ const completionResponse = createCompletionResponse(
256
+ id,
257
+ model,
258
+ response.content,
259
+ usage,
260
+ response.stopReason === 'max_tokens' ? 'length' : 'stop'
261
+ );
262
+
263
+ res.json(completionResponse);
264
+ } catch (error) {
265
+ clearTimeout(timeout);
266
+ const errorMsg = error instanceof Error ? error.message : String(error);
267
+ console.error('处理请求错误:', errorMsg);
268
+
269
+ // 区分超时错误和其他错误
270
+ if (errorMsg.includes('超时') || errorMsg.includes('timeout')) {
271
+ res.status(504).json(this.createError(errorMsg, 'timeout_error', 'timeout'));
272
+ } else {
273
+ res.status(500).json(this.createError(errorMsg, 'internal_error', 'internal_error'));
274
+ }
275
+ }
276
+ }
277
+
278
+ /**
279
+ * 设置错误处理
280
+ */
281
+ private setupErrorHandler(): void {
282
+ this.app.use((err: Error, req: Request, res: Response, _next: NextFunction) => {
283
+ console.error('服务器错误:', err);
284
+ res.status(500).json(this.createError('服务器内部错误', 'internal_error'));
285
+ });
286
+ }
287
+
288
+ /**
289
+ * 创建错误响应
290
+ */
291
+ private createError(message: string, type: string, code: string = 'unknown_error'): ErrorResponse {
292
+ return {
293
+ error: {
294
+ message,
295
+ type,
296
+ code,
297
+ },
298
+ };
299
+ }
300
+
301
+ /**
302
+ * 启动服务器
303
+ */
304
+ async start(): Promise<void> {
305
+ // 连接 iFlow
306
+ console.log('正在连接 iFlow...');
307
+ await this.adapter.connect();
308
+ console.log('iFlow 连接成功');
309
+
310
+ // 启动 HTTP 服务器
311
+ const host = this.options.host || '0.0.0.0';
312
+ const port = this.options.port;
313
+
314
+ return new Promise((resolve, reject) => {
315
+ this.app.listen(port, host, () => {
316
+ const model = this.options.model || 'iFlow 默认';
317
+ console.log(`\n🚀 iFlow API 桥接服务已启动`);
318
+ console.log(`📍 服务地址: http://${host}:${port}`);
319
+ console.log(`🤖 使用模型: ${model}`);
320
+ console.log(`🔗 OpenAI API: http://${host}:${port}/v1/chat/completions`);
321
+ console.log(`📋 模型列表: http://${host}:${port}/v1/models`);
322
+ console.log(`❤️ 健康检查: http://${host}:${port}/health`);
323
+ console.log(`\n💡 使用示例:`);
324
+ console.log(` export OPENAI_BASE_URL=http://${host}:${port}/v1`);
325
+ console.log(` export OPENAI_API_KEY=sk-iflow`);
326
+ console.log(` claude`);
327
+ console.log();
328
+ resolve();
329
+ }).on('error', (err) => {
330
+ reject(err);
331
+ });
332
+ });
333
+ }
334
+
335
+ /**
336
+ * 停止服务器
337
+ */
338
+ async stop(): Promise<void> {
339
+ await this.adapter.disconnect();
340
+ }
341
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true,
14
+ "declaration": true,
15
+ "declarationMap": true,
16
+ "sourceMap": true
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist"]
20
+ }