llmapi-v2 2.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.
- package/.env.example +40 -0
- package/Dockerfile +17 -0
- package/dist/config.d.ts +48 -0
- package/dist/config.js +98 -0
- package/dist/config.js.map +1 -0
- package/dist/converter/request.d.ts +6 -0
- package/dist/converter/request.js +184 -0
- package/dist/converter/request.js.map +1 -0
- package/dist/converter/response.d.ts +6 -0
- package/dist/converter/response.js +76 -0
- package/dist/converter/response.js.map +1 -0
- package/dist/converter/stream.d.ts +54 -0
- package/dist/converter/stream.js +318 -0
- package/dist/converter/stream.js.map +1 -0
- package/dist/converter/types.d.ts +239 -0
- package/dist/converter/types.js +6 -0
- package/dist/converter/types.js.map +1 -0
- package/dist/data/posts.d.ts +19 -0
- package/dist/data/posts.js +462 -0
- package/dist/data/posts.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +233 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/api-key-auth.d.ts +6 -0
- package/dist/middleware/api-key-auth.js +76 -0
- package/dist/middleware/api-key-auth.js.map +1 -0
- package/dist/middleware/quota-guard.d.ts +10 -0
- package/dist/middleware/quota-guard.js +27 -0
- package/dist/middleware/quota-guard.js.map +1 -0
- package/dist/middleware/rate-limiter.d.ts +5 -0
- package/dist/middleware/rate-limiter.js +50 -0
- package/dist/middleware/rate-limiter.js.map +1 -0
- package/dist/middleware/request-logger.d.ts +6 -0
- package/dist/middleware/request-logger.js +37 -0
- package/dist/middleware/request-logger.js.map +1 -0
- package/dist/middleware/session-auth.d.ts +19 -0
- package/dist/middleware/session-auth.js +99 -0
- package/dist/middleware/session-auth.js.map +1 -0
- package/dist/providers/aliyun.d.ts +13 -0
- package/dist/providers/aliyun.js +20 -0
- package/dist/providers/aliyun.js.map +1 -0
- package/dist/providers/base-provider.d.ts +36 -0
- package/dist/providers/base-provider.js +133 -0
- package/dist/providers/base-provider.js.map +1 -0
- package/dist/providers/deepseek.d.ts +11 -0
- package/dist/providers/deepseek.js +18 -0
- package/dist/providers/deepseek.js.map +1 -0
- package/dist/providers/registry.d.ts +18 -0
- package/dist/providers/registry.js +98 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/providers/types.d.ts +17 -0
- package/dist/providers/types.js +3 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/routes/admin.d.ts +1 -0
- package/dist/routes/admin.js +153 -0
- package/dist/routes/admin.js.map +1 -0
- package/dist/routes/auth.d.ts +2 -0
- package/dist/routes/auth.js +318 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/blog.d.ts +1 -0
- package/dist/routes/blog.js +29 -0
- package/dist/routes/blog.js.map +1 -0
- package/dist/routes/dashboard.d.ts +1 -0
- package/dist/routes/dashboard.js +184 -0
- package/dist/routes/dashboard.js.map +1 -0
- package/dist/routes/messages.d.ts +1 -0
- package/dist/routes/messages.js +309 -0
- package/dist/routes/messages.js.map +1 -0
- package/dist/routes/models.d.ts +1 -0
- package/dist/routes/models.js +39 -0
- package/dist/routes/models.js.map +1 -0
- package/dist/routes/payment.d.ts +1 -0
- package/dist/routes/payment.js +150 -0
- package/dist/routes/payment.js.map +1 -0
- package/dist/routes/sitemap.d.ts +1 -0
- package/dist/routes/sitemap.js +38 -0
- package/dist/routes/sitemap.js.map +1 -0
- package/dist/services/alipay.d.ts +27 -0
- package/dist/services/alipay.js +106 -0
- package/dist/services/alipay.js.map +1 -0
- package/dist/services/database.d.ts +4 -0
- package/dist/services/database.js +170 -0
- package/dist/services/database.js.map +1 -0
- package/dist/services/health-checker.d.ts +13 -0
- package/dist/services/health-checker.js +95 -0
- package/dist/services/health-checker.js.map +1 -0
- package/dist/services/mailer.d.ts +3 -0
- package/dist/services/mailer.js +91 -0
- package/dist/services/mailer.js.map +1 -0
- package/dist/services/metrics.d.ts +56 -0
- package/dist/services/metrics.js +94 -0
- package/dist/services/metrics.js.map +1 -0
- package/dist/services/remote-control.d.ts +20 -0
- package/dist/services/remote-control.js +209 -0
- package/dist/services/remote-control.js.map +1 -0
- package/dist/services/remote-ws.d.ts +5 -0
- package/dist/services/remote-ws.js +143 -0
- package/dist/services/remote-ws.js.map +1 -0
- package/dist/services/usage.d.ts +13 -0
- package/dist/services/usage.js +39 -0
- package/dist/services/usage.js.map +1 -0
- package/dist/utils/errors.d.ts +27 -0
- package/dist/utils/errors.js +48 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.js +14 -0
- package/dist/utils/logger.js.map +1 -0
- package/docker-compose.yml +19 -0
- package/package.json +39 -0
- package/public/robots.txt +8 -0
- package/src/config.ts +140 -0
- package/src/converter/request.ts +207 -0
- package/src/converter/response.ts +85 -0
- package/src/converter/stream.ts +373 -0
- package/src/converter/types.ts +257 -0
- package/src/data/posts.ts +474 -0
- package/src/index.ts +219 -0
- package/src/middleware/api-key-auth.ts +82 -0
- package/src/middleware/quota-guard.ts +28 -0
- package/src/middleware/rate-limiter.ts +61 -0
- package/src/middleware/request-logger.ts +36 -0
- package/src/middleware/session-auth.ts +91 -0
- package/src/providers/aliyun.ts +16 -0
- package/src/providers/base-provider.ts +148 -0
- package/src/providers/deepseek.ts +14 -0
- package/src/providers/registry.ts +111 -0
- package/src/providers/types.ts +26 -0
- package/src/routes/admin.ts +169 -0
- package/src/routes/auth.ts +369 -0
- package/src/routes/blog.ts +28 -0
- package/src/routes/dashboard.ts +208 -0
- package/src/routes/messages.ts +346 -0
- package/src/routes/models.ts +37 -0
- package/src/routes/payment.ts +189 -0
- package/src/routes/sitemap.ts +40 -0
- package/src/services/alipay.ts +116 -0
- package/src/services/database.ts +187 -0
- package/src/services/health-checker.ts +115 -0
- package/src/services/mailer.ts +90 -0
- package/src/services/metrics.ts +104 -0
- package/src/services/remote-control.ts +226 -0
- package/src/services/remote-ws.ts +145 -0
- package/src/services/usage.ts +57 -0
- package/src/types/express.d.ts +46 -0
- package/src/utils/errors.ts +44 -0
- package/src/utils/logger.ts +8 -0
- package/tsconfig.json +17 -0
- package/views/pages/404.ejs +14 -0
- package/views/pages/admin.ejs +307 -0
- package/views/pages/blog-post.ejs +378 -0
- package/views/pages/blog.ejs +148 -0
- package/views/pages/dashboard.ejs +441 -0
- package/views/pages/docs.ejs +807 -0
- package/views/pages/index.ejs +416 -0
- package/views/pages/login.ejs +170 -0
- package/views/pages/orders.ejs +111 -0
- package/views/pages/pricing.ejs +379 -0
- package/views/pages/register.ejs +397 -0
- package/views/pages/remote.ejs +334 -0
- package/views/pages/settings.ejs +373 -0
- package/views/partials/header.ejs +70 -0
- package/views/partials/nav.ejs +140 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import type { Response } from 'express';
|
|
2
|
+
import type { IncomingMessage } from 'http';
|
|
3
|
+
import type { OpenAIStreamChunk, OpenAIStreamToolCall } from './types';
|
|
4
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
5
|
+
import { logger } from '../utils/logger';
|
|
6
|
+
|
|
7
|
+
interface ToolCallAccumulator {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
arguments: string;
|
|
11
|
+
blockIndex: number;
|
|
12
|
+
started: boolean; // Whether content_block_start has been sent
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface StreamResult {
|
|
16
|
+
inputTokens: number;
|
|
17
|
+
outputTokens: number;
|
|
18
|
+
thinkingTokens: number;
|
|
19
|
+
ttftMs: number;
|
|
20
|
+
tokensPerSec: number;
|
|
21
|
+
durationMs: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Transforms an OpenAI SSE stream into Anthropic SSE stream format.
|
|
26
|
+
*
|
|
27
|
+
* This is the most critical component of the relay service.
|
|
28
|
+
* Claude Code expects Anthropic's streaming format exactly:
|
|
29
|
+
* message_start -> content_block_start/delta/stop (repeated) -> message_delta -> message_stop
|
|
30
|
+
*/
|
|
31
|
+
export class AnthropicStreamTransformer {
|
|
32
|
+
// Block tracking
|
|
33
|
+
private blockIndex = 0;
|
|
34
|
+
private thinkingBlockOpen = false;
|
|
35
|
+
private textBlockOpen = false;
|
|
36
|
+
private toolCalls = new Map<number, ToolCallAccumulator>();
|
|
37
|
+
|
|
38
|
+
// State
|
|
39
|
+
private hasToolUse = false;
|
|
40
|
+
private finished = false;
|
|
41
|
+
private buffer = '';
|
|
42
|
+
|
|
43
|
+
// Metrics
|
|
44
|
+
private usage = { inputTokens: 0, outputTokens: 0 };
|
|
45
|
+
private thinkingCharCount = 0;
|
|
46
|
+
private outputCharCount = 0;
|
|
47
|
+
private firstTokenTime = 0;
|
|
48
|
+
private startTime = Date.now();
|
|
49
|
+
|
|
50
|
+
// Message identity
|
|
51
|
+
private messageId: string;
|
|
52
|
+
|
|
53
|
+
constructor(
|
|
54
|
+
private res: Response,
|
|
55
|
+
private displayModel: string,
|
|
56
|
+
) {
|
|
57
|
+
this.messageId = `msg_${uuidv4().replace(/-/g, '')}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Pipe an OpenAI streaming response through this transformer.
|
|
62
|
+
* Returns usage stats when complete.
|
|
63
|
+
*/
|
|
64
|
+
async pipe(backendRes: IncomingMessage): Promise<StreamResult> {
|
|
65
|
+
// Set SSE headers
|
|
66
|
+
this.res.setHeader('Content-Type', 'text/event-stream');
|
|
67
|
+
this.res.setHeader('Cache-Control', 'no-cache');
|
|
68
|
+
this.res.setHeader('Connection', 'keep-alive');
|
|
69
|
+
this.res.flushHeaders();
|
|
70
|
+
|
|
71
|
+
// Handle client disconnect
|
|
72
|
+
let clientDisconnected = false;
|
|
73
|
+
this.res.on('close', () => {
|
|
74
|
+
clientDisconnected = true;
|
|
75
|
+
backendRes.destroy();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Send message_start
|
|
79
|
+
this.sendSSE('message_start', {
|
|
80
|
+
type: 'message_start',
|
|
81
|
+
message: {
|
|
82
|
+
id: this.messageId,
|
|
83
|
+
type: 'message',
|
|
84
|
+
role: 'assistant',
|
|
85
|
+
model: this.displayModel,
|
|
86
|
+
content: [],
|
|
87
|
+
stop_reason: null,
|
|
88
|
+
stop_sequence: null,
|
|
89
|
+
usage: { input_tokens: 0, output_tokens: 0 },
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Send initial ping
|
|
94
|
+
this.sendSSE('ping', { type: 'ping' });
|
|
95
|
+
|
|
96
|
+
return new Promise<StreamResult>((resolve, reject) => {
|
|
97
|
+
backendRes.setEncoding('utf8');
|
|
98
|
+
|
|
99
|
+
backendRes.on('data', (chunk: string) => {
|
|
100
|
+
if (clientDisconnected) return;
|
|
101
|
+
this.buffer += chunk;
|
|
102
|
+
this.processBuffer();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
backendRes.on('end', () => {
|
|
106
|
+
// Process any remaining buffer
|
|
107
|
+
if (this.buffer.trim()) {
|
|
108
|
+
this.processBuffer();
|
|
109
|
+
}
|
|
110
|
+
if (!this.finished) {
|
|
111
|
+
this.finish();
|
|
112
|
+
}
|
|
113
|
+
resolve(this.getResult());
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
backendRes.on('error', (err) => {
|
|
117
|
+
logger.error({ err }, 'Backend stream error');
|
|
118
|
+
if (!this.finished) {
|
|
119
|
+
this.finish();
|
|
120
|
+
}
|
|
121
|
+
resolve(this.getResult());
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private processBuffer(): void {
|
|
127
|
+
const lines = this.buffer.split('\n');
|
|
128
|
+
this.buffer = lines.pop() || ''; // Keep incomplete last line
|
|
129
|
+
|
|
130
|
+
for (const line of lines) {
|
|
131
|
+
if (!line.startsWith('data: ')) continue;
|
|
132
|
+
const payload = line.slice(6).trim();
|
|
133
|
+
|
|
134
|
+
if (payload === '[DONE]') {
|
|
135
|
+
this.finish();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let chunk: OpenAIStreamChunk;
|
|
140
|
+
try {
|
|
141
|
+
chunk = JSON.parse(payload);
|
|
142
|
+
} catch {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this.processChunk(chunk);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private processChunk(chunk: OpenAIStreamChunk): void {
|
|
151
|
+
// Track usage from provider
|
|
152
|
+
if (chunk.usage) {
|
|
153
|
+
if (chunk.usage.prompt_tokens) this.usage.inputTokens = chunk.usage.prompt_tokens;
|
|
154
|
+
if (chunk.usage.completion_tokens) this.usage.outputTokens = chunk.usage.completion_tokens;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const choice = chunk.choices?.[0];
|
|
158
|
+
if (!choice) return;
|
|
159
|
+
|
|
160
|
+
const delta = choice.delta;
|
|
161
|
+
|
|
162
|
+
// Handle reasoning/thinking content
|
|
163
|
+
if (delta.reasoning_content) {
|
|
164
|
+
this.handleThinking(delta.reasoning_content);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Handle text content
|
|
168
|
+
if (delta.content) {
|
|
169
|
+
this.handleText(delta.content);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Handle tool calls
|
|
173
|
+
if (delta.tool_calls) {
|
|
174
|
+
this.handleToolCalls(delta.tool_calls);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Handle finish_reason (some providers send this instead of [DONE])
|
|
178
|
+
if (choice.finish_reason && !this.finished) {
|
|
179
|
+
this.finish();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private handleThinking(text: string): void {
|
|
184
|
+
this.thinkingCharCount += text.length;
|
|
185
|
+
|
|
186
|
+
if (!this.thinkingBlockOpen) {
|
|
187
|
+
this.sendSSE('content_block_start', {
|
|
188
|
+
type: 'content_block_start',
|
|
189
|
+
index: this.blockIndex,
|
|
190
|
+
content_block: { type: 'thinking', thinking: '' },
|
|
191
|
+
});
|
|
192
|
+
this.thinkingBlockOpen = true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.sendSSE('content_block_delta', {
|
|
196
|
+
type: 'content_block_delta',
|
|
197
|
+
index: this.blockIndex,
|
|
198
|
+
delta: { type: 'thinking_delta', thinking: text },
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private handleText(text: string): void {
|
|
203
|
+
if (!this.firstTokenTime) this.firstTokenTime = Date.now();
|
|
204
|
+
this.outputCharCount += text.length;
|
|
205
|
+
|
|
206
|
+
// Close thinking block if open (thinking always comes before text)
|
|
207
|
+
this.closeThinkingBlock();
|
|
208
|
+
|
|
209
|
+
if (!this.textBlockOpen) {
|
|
210
|
+
this.sendSSE('content_block_start', {
|
|
211
|
+
type: 'content_block_start',
|
|
212
|
+
index: this.blockIndex,
|
|
213
|
+
content_block: { type: 'text', text: '' },
|
|
214
|
+
});
|
|
215
|
+
this.textBlockOpen = true;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
this.sendSSE('content_block_delta', {
|
|
219
|
+
type: 'content_block_delta',
|
|
220
|
+
index: this.blockIndex,
|
|
221
|
+
delta: { type: 'text_delta', text },
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private handleToolCalls(toolCalls: OpenAIStreamToolCall[]): void {
|
|
226
|
+
// Close thinking and text blocks before tool use
|
|
227
|
+
this.closeThinkingBlock();
|
|
228
|
+
this.closeTextBlock();
|
|
229
|
+
this.hasToolUse = true;
|
|
230
|
+
|
|
231
|
+
for (const tc of toolCalls) {
|
|
232
|
+
const tcIndex = tc.index;
|
|
233
|
+
let acc = this.toolCalls.get(tcIndex);
|
|
234
|
+
|
|
235
|
+
// New tool call: create accumulator
|
|
236
|
+
if (!acc && tc.function?.name) {
|
|
237
|
+
acc = {
|
|
238
|
+
id: tc.id || `toolu_${uuidv4().replace(/-/g, '').slice(0, 24)}`,
|
|
239
|
+
name: tc.function.name,
|
|
240
|
+
arguments: '',
|
|
241
|
+
blockIndex: this.blockIndex,
|
|
242
|
+
started: false,
|
|
243
|
+
};
|
|
244
|
+
this.toolCalls.set(tcIndex, acc);
|
|
245
|
+
this.blockIndex++; // Reserve a block index for this tool
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!acc) continue;
|
|
249
|
+
|
|
250
|
+
// Start the content block if not yet started
|
|
251
|
+
if (!acc.started) {
|
|
252
|
+
this.sendSSE('content_block_start', {
|
|
253
|
+
type: 'content_block_start',
|
|
254
|
+
index: acc.blockIndex,
|
|
255
|
+
content_block: {
|
|
256
|
+
type: 'tool_use',
|
|
257
|
+
id: acc.id,
|
|
258
|
+
name: acc.name,
|
|
259
|
+
input: {},
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
acc.started = true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Stream argument chunks
|
|
266
|
+
if (tc.function?.arguments) {
|
|
267
|
+
acc.arguments += tc.function.arguments;
|
|
268
|
+
this.sendSSE('content_block_delta', {
|
|
269
|
+
type: 'content_block_delta',
|
|
270
|
+
index: acc.blockIndex,
|
|
271
|
+
delta: { type: 'input_json_delta', partial_json: tc.function.arguments },
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private closeThinkingBlock(): void {
|
|
278
|
+
if (this.thinkingBlockOpen) {
|
|
279
|
+
this.sendSSE('content_block_stop', {
|
|
280
|
+
type: 'content_block_stop',
|
|
281
|
+
index: this.blockIndex,
|
|
282
|
+
});
|
|
283
|
+
this.blockIndex++;
|
|
284
|
+
this.thinkingBlockOpen = false;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private closeTextBlock(): void {
|
|
289
|
+
if (this.textBlockOpen) {
|
|
290
|
+
this.sendSSE('content_block_stop', {
|
|
291
|
+
type: 'content_block_stop',
|
|
292
|
+
index: this.blockIndex,
|
|
293
|
+
});
|
|
294
|
+
this.blockIndex++;
|
|
295
|
+
this.textBlockOpen = false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private closeAllToolBlocks(): void {
|
|
300
|
+
for (const [, acc] of this.toolCalls) {
|
|
301
|
+
if (acc.started) {
|
|
302
|
+
this.sendSSE('content_block_stop', {
|
|
303
|
+
type: 'content_block_stop',
|
|
304
|
+
index: acc.blockIndex,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
this.toolCalls.clear();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Finalize the stream. Called once when [DONE] or finish_reason is received.
|
|
313
|
+
*/
|
|
314
|
+
private finish(): void {
|
|
315
|
+
if (this.finished) return;
|
|
316
|
+
this.finished = true;
|
|
317
|
+
|
|
318
|
+
// Determine stop_reason BEFORE closing blocks
|
|
319
|
+
const stopReason = this.hasToolUse ? 'tool_use' : 'end_turn';
|
|
320
|
+
|
|
321
|
+
// Close all open blocks
|
|
322
|
+
this.closeThinkingBlock();
|
|
323
|
+
this.closeTextBlock();
|
|
324
|
+
this.closeAllToolBlocks();
|
|
325
|
+
|
|
326
|
+
// Send message_delta with final stop_reason
|
|
327
|
+
this.sendSSE('message_delta', {
|
|
328
|
+
type: 'message_delta',
|
|
329
|
+
delta: { stop_reason: stopReason, stop_sequence: null },
|
|
330
|
+
usage: { output_tokens: this.usage.outputTokens },
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Send message_stop
|
|
334
|
+
this.sendSSE('message_stop', { type: 'message_stop' });
|
|
335
|
+
|
|
336
|
+
// End the response
|
|
337
|
+
if (!this.res.writableEnded) {
|
|
338
|
+
this.res.end();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private sendSSE(event: string, data: object): void {
|
|
343
|
+
if (this.res.writableEnded || this.res.destroyed) return;
|
|
344
|
+
try {
|
|
345
|
+
this.res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
346
|
+
} catch {
|
|
347
|
+
// Client disconnected, ignore
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private getResult(): StreamResult {
|
|
352
|
+
const elapsed = Date.now() - this.startTime;
|
|
353
|
+
const ttft = this.firstTokenTime ? this.firstTokenTime - this.startTime : elapsed;
|
|
354
|
+
|
|
355
|
+
// Estimate tokens from char count if provider didn't report usage
|
|
356
|
+
if (this.usage.outputTokens === 0 && this.outputCharCount > 0) {
|
|
357
|
+
this.usage.outputTokens = Math.ceil(this.outputCharCount / 3);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const tokensPerSec = elapsed > 0
|
|
361
|
+
? Math.round((this.usage.outputTokens / (elapsed / 1000)) * 100) / 100
|
|
362
|
+
: 0;
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
inputTokens: this.usage.inputTokens,
|
|
366
|
+
outputTokens: this.usage.outputTokens,
|
|
367
|
+
thinkingTokens: Math.ceil(this.thinkingCharCount / 3),
|
|
368
|
+
ttftMs: ttft,
|
|
369
|
+
tokensPerSec,
|
|
370
|
+
durationMs: elapsed,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Anthropic Messages API Types (what Claude Code sends/expects)
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
// --- Request ---
|
|
6
|
+
|
|
7
|
+
export interface AnthropicRequest {
|
|
8
|
+
model: string;
|
|
9
|
+
messages: AnthropicMessage[];
|
|
10
|
+
system?: string | AnthropicSystemBlock[];
|
|
11
|
+
max_tokens: number;
|
|
12
|
+
temperature?: number;
|
|
13
|
+
top_p?: number;
|
|
14
|
+
tools?: AnthropicToolDefinition[];
|
|
15
|
+
tool_choice?: AnthropicToolChoice;
|
|
16
|
+
stream?: boolean;
|
|
17
|
+
stop_sequences?: string[];
|
|
18
|
+
metadata?: { user_id?: string };
|
|
19
|
+
thinking?: { type: 'enabled'; budget_tokens: number };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface AnthropicMessage {
|
|
23
|
+
role: 'user' | 'assistant';
|
|
24
|
+
content: string | AnthropicContentBlock[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type AnthropicContentBlock =
|
|
28
|
+
| AnthropicTextBlock
|
|
29
|
+
| AnthropicThinkingBlock
|
|
30
|
+
| AnthropicToolUseBlock
|
|
31
|
+
| AnthropicToolResultBlock
|
|
32
|
+
| AnthropicImageBlock;
|
|
33
|
+
|
|
34
|
+
export interface AnthropicTextBlock {
|
|
35
|
+
type: 'text';
|
|
36
|
+
text: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface AnthropicThinkingBlock {
|
|
40
|
+
type: 'thinking';
|
|
41
|
+
thinking: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface AnthropicToolUseBlock {
|
|
45
|
+
type: 'tool_use';
|
|
46
|
+
id: string;
|
|
47
|
+
name: string;
|
|
48
|
+
input: Record<string, unknown>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface AnthropicToolResultBlock {
|
|
52
|
+
type: 'tool_result';
|
|
53
|
+
tool_use_id: string;
|
|
54
|
+
content: string | AnthropicContentBlock[];
|
|
55
|
+
is_error?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface AnthropicImageBlock {
|
|
59
|
+
type: 'image';
|
|
60
|
+
source: {
|
|
61
|
+
type: 'base64';
|
|
62
|
+
media_type: string;
|
|
63
|
+
data: string;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface AnthropicSystemBlock {
|
|
68
|
+
type: 'text';
|
|
69
|
+
text: string;
|
|
70
|
+
cache_control?: { type: 'ephemeral' };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface AnthropicToolDefinition {
|
|
74
|
+
name: string;
|
|
75
|
+
description?: string;
|
|
76
|
+
input_schema: Record<string, unknown>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type AnthropicToolChoice =
|
|
80
|
+
| { type: 'auto' }
|
|
81
|
+
| { type: 'any' }
|
|
82
|
+
| { type: 'tool'; name: string };
|
|
83
|
+
|
|
84
|
+
// --- Response ---
|
|
85
|
+
|
|
86
|
+
export interface AnthropicResponse {
|
|
87
|
+
id: string;
|
|
88
|
+
type: 'message';
|
|
89
|
+
role: 'assistant';
|
|
90
|
+
model: string;
|
|
91
|
+
content: AnthropicResponseBlock[];
|
|
92
|
+
stop_reason: 'end_turn' | 'max_tokens' | 'stop_sequence' | 'tool_use';
|
|
93
|
+
stop_sequence: string | null;
|
|
94
|
+
usage: AnthropicUsage;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type AnthropicResponseBlock =
|
|
98
|
+
| AnthropicTextBlock
|
|
99
|
+
| AnthropicThinkingBlock
|
|
100
|
+
| AnthropicToolUseBlock;
|
|
101
|
+
|
|
102
|
+
export interface AnthropicUsage {
|
|
103
|
+
input_tokens: number;
|
|
104
|
+
output_tokens: number;
|
|
105
|
+
cache_creation_input_tokens?: number;
|
|
106
|
+
cache_read_input_tokens?: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// --- Streaming Events ---
|
|
110
|
+
|
|
111
|
+
export type AnthropicStreamEvent =
|
|
112
|
+
| { type: 'message_start'; message: AnthropicResponse }
|
|
113
|
+
| { type: 'content_block_start'; index: number; content_block: AnthropicResponseBlock }
|
|
114
|
+
| { type: 'content_block_delta'; index: number; delta: AnthropicDelta }
|
|
115
|
+
| { type: 'content_block_stop'; index: number }
|
|
116
|
+
| { type: 'message_delta'; delta: { stop_reason: string; stop_sequence: string | null }; usage: { output_tokens: number } }
|
|
117
|
+
| { type: 'message_stop' }
|
|
118
|
+
| { type: 'ping' };
|
|
119
|
+
|
|
120
|
+
export type AnthropicDelta =
|
|
121
|
+
| { type: 'text_delta'; text: string }
|
|
122
|
+
| { type: 'thinking_delta'; thinking: string }
|
|
123
|
+
| { type: 'input_json_delta'; partial_json: string };
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
// ============================================================================
|
|
127
|
+
// OpenAI Chat Completions API Types (what providers accept/return)
|
|
128
|
+
// ============================================================================
|
|
129
|
+
|
|
130
|
+
// --- Request ---
|
|
131
|
+
|
|
132
|
+
export interface OpenAIRequest {
|
|
133
|
+
model: string;
|
|
134
|
+
messages: OpenAIMessage[];
|
|
135
|
+
max_tokens?: number;
|
|
136
|
+
temperature?: number;
|
|
137
|
+
top_p?: number;
|
|
138
|
+
tools?: OpenAITool[];
|
|
139
|
+
tool_choice?: string | OpenAIToolChoiceObject;
|
|
140
|
+
stream: boolean;
|
|
141
|
+
stream_options?: { include_usage: boolean };
|
|
142
|
+
stop?: string[];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export type OpenAIMessage =
|
|
146
|
+
| OpenAISystemMessage
|
|
147
|
+
| OpenAIUserMessage
|
|
148
|
+
| OpenAIAssistantMessage
|
|
149
|
+
| OpenAIToolMessage;
|
|
150
|
+
|
|
151
|
+
export interface OpenAISystemMessage {
|
|
152
|
+
role: 'system';
|
|
153
|
+
content: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface OpenAIUserMessage {
|
|
157
|
+
role: 'user';
|
|
158
|
+
content: string | OpenAIContentPart[];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export type OpenAIContentPart =
|
|
162
|
+
| { type: 'text'; text: string }
|
|
163
|
+
| { type: 'image_url'; image_url: { url: string } };
|
|
164
|
+
|
|
165
|
+
export interface OpenAIAssistantMessage {
|
|
166
|
+
role: 'assistant';
|
|
167
|
+
content: string | null;
|
|
168
|
+
tool_calls?: OpenAIToolCall[];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface OpenAIToolMessage {
|
|
172
|
+
role: 'tool';
|
|
173
|
+
tool_call_id: string;
|
|
174
|
+
content: string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface OpenAITool {
|
|
178
|
+
type: 'function';
|
|
179
|
+
function: {
|
|
180
|
+
name: string;
|
|
181
|
+
description: string;
|
|
182
|
+
parameters: Record<string, unknown>;
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface OpenAIToolChoiceObject {
|
|
187
|
+
type: 'function';
|
|
188
|
+
function: { name: string };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface OpenAIToolCall {
|
|
192
|
+
id: string;
|
|
193
|
+
type: 'function';
|
|
194
|
+
function: {
|
|
195
|
+
name: string;
|
|
196
|
+
arguments: string;
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// --- Response ---
|
|
201
|
+
|
|
202
|
+
export interface OpenAIResponse {
|
|
203
|
+
id: string;
|
|
204
|
+
object: string;
|
|
205
|
+
created: number;
|
|
206
|
+
model: string;
|
|
207
|
+
choices: OpenAIChoice[];
|
|
208
|
+
usage?: OpenAIUsage;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface OpenAIChoice {
|
|
212
|
+
index: number;
|
|
213
|
+
message: OpenAIAssistantMessage & {
|
|
214
|
+
reasoning_content?: string;
|
|
215
|
+
};
|
|
216
|
+
finish_reason: 'stop' | 'length' | 'tool_calls' | null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface OpenAIUsage {
|
|
220
|
+
prompt_tokens: number;
|
|
221
|
+
completion_tokens: number;
|
|
222
|
+
total_tokens: number;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// --- Streaming ---
|
|
226
|
+
|
|
227
|
+
export interface OpenAIStreamChunk {
|
|
228
|
+
id: string;
|
|
229
|
+
object: string;
|
|
230
|
+
created: number;
|
|
231
|
+
model: string;
|
|
232
|
+
choices: OpenAIStreamChoice[];
|
|
233
|
+
usage?: OpenAIUsage;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface OpenAIStreamChoice {
|
|
237
|
+
index: number;
|
|
238
|
+
delta: OpenAIStreamDelta;
|
|
239
|
+
finish_reason: 'stop' | 'length' | 'tool_calls' | null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface OpenAIStreamDelta {
|
|
243
|
+
role?: string;
|
|
244
|
+
content?: string | null;
|
|
245
|
+
reasoning_content?: string;
|
|
246
|
+
tool_calls?: OpenAIStreamToolCall[];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export interface OpenAIStreamToolCall {
|
|
250
|
+
index: number;
|
|
251
|
+
id?: string;
|
|
252
|
+
type?: string;
|
|
253
|
+
function?: {
|
|
254
|
+
name?: string;
|
|
255
|
+
arguments?: string;
|
|
256
|
+
};
|
|
257
|
+
}
|