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.
Files changed (162) hide show
  1. package/.env.example +40 -0
  2. package/Dockerfile +17 -0
  3. package/dist/config.d.ts +48 -0
  4. package/dist/config.js +98 -0
  5. package/dist/config.js.map +1 -0
  6. package/dist/converter/request.d.ts +6 -0
  7. package/dist/converter/request.js +184 -0
  8. package/dist/converter/request.js.map +1 -0
  9. package/dist/converter/response.d.ts +6 -0
  10. package/dist/converter/response.js +76 -0
  11. package/dist/converter/response.js.map +1 -0
  12. package/dist/converter/stream.d.ts +54 -0
  13. package/dist/converter/stream.js +318 -0
  14. package/dist/converter/stream.js.map +1 -0
  15. package/dist/converter/types.d.ts +239 -0
  16. package/dist/converter/types.js +6 -0
  17. package/dist/converter/types.js.map +1 -0
  18. package/dist/data/posts.d.ts +19 -0
  19. package/dist/data/posts.js +462 -0
  20. package/dist/data/posts.js.map +1 -0
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.js +233 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/middleware/api-key-auth.d.ts +6 -0
  25. package/dist/middleware/api-key-auth.js +76 -0
  26. package/dist/middleware/api-key-auth.js.map +1 -0
  27. package/dist/middleware/quota-guard.d.ts +10 -0
  28. package/dist/middleware/quota-guard.js +27 -0
  29. package/dist/middleware/quota-guard.js.map +1 -0
  30. package/dist/middleware/rate-limiter.d.ts +5 -0
  31. package/dist/middleware/rate-limiter.js +50 -0
  32. package/dist/middleware/rate-limiter.js.map +1 -0
  33. package/dist/middleware/request-logger.d.ts +6 -0
  34. package/dist/middleware/request-logger.js +37 -0
  35. package/dist/middleware/request-logger.js.map +1 -0
  36. package/dist/middleware/session-auth.d.ts +19 -0
  37. package/dist/middleware/session-auth.js +99 -0
  38. package/dist/middleware/session-auth.js.map +1 -0
  39. package/dist/providers/aliyun.d.ts +13 -0
  40. package/dist/providers/aliyun.js +20 -0
  41. package/dist/providers/aliyun.js.map +1 -0
  42. package/dist/providers/base-provider.d.ts +36 -0
  43. package/dist/providers/base-provider.js +133 -0
  44. package/dist/providers/base-provider.js.map +1 -0
  45. package/dist/providers/deepseek.d.ts +11 -0
  46. package/dist/providers/deepseek.js +18 -0
  47. package/dist/providers/deepseek.js.map +1 -0
  48. package/dist/providers/registry.d.ts +18 -0
  49. package/dist/providers/registry.js +98 -0
  50. package/dist/providers/registry.js.map +1 -0
  51. package/dist/providers/types.d.ts +17 -0
  52. package/dist/providers/types.js +3 -0
  53. package/dist/providers/types.js.map +1 -0
  54. package/dist/routes/admin.d.ts +1 -0
  55. package/dist/routes/admin.js +153 -0
  56. package/dist/routes/admin.js.map +1 -0
  57. package/dist/routes/auth.d.ts +2 -0
  58. package/dist/routes/auth.js +318 -0
  59. package/dist/routes/auth.js.map +1 -0
  60. package/dist/routes/blog.d.ts +1 -0
  61. package/dist/routes/blog.js +29 -0
  62. package/dist/routes/blog.js.map +1 -0
  63. package/dist/routes/dashboard.d.ts +1 -0
  64. package/dist/routes/dashboard.js +184 -0
  65. package/dist/routes/dashboard.js.map +1 -0
  66. package/dist/routes/messages.d.ts +1 -0
  67. package/dist/routes/messages.js +309 -0
  68. package/dist/routes/messages.js.map +1 -0
  69. package/dist/routes/models.d.ts +1 -0
  70. package/dist/routes/models.js +39 -0
  71. package/dist/routes/models.js.map +1 -0
  72. package/dist/routes/payment.d.ts +1 -0
  73. package/dist/routes/payment.js +150 -0
  74. package/dist/routes/payment.js.map +1 -0
  75. package/dist/routes/sitemap.d.ts +1 -0
  76. package/dist/routes/sitemap.js +38 -0
  77. package/dist/routes/sitemap.js.map +1 -0
  78. package/dist/services/alipay.d.ts +27 -0
  79. package/dist/services/alipay.js +106 -0
  80. package/dist/services/alipay.js.map +1 -0
  81. package/dist/services/database.d.ts +4 -0
  82. package/dist/services/database.js +170 -0
  83. package/dist/services/database.js.map +1 -0
  84. package/dist/services/health-checker.d.ts +13 -0
  85. package/dist/services/health-checker.js +95 -0
  86. package/dist/services/health-checker.js.map +1 -0
  87. package/dist/services/mailer.d.ts +3 -0
  88. package/dist/services/mailer.js +91 -0
  89. package/dist/services/mailer.js.map +1 -0
  90. package/dist/services/metrics.d.ts +56 -0
  91. package/dist/services/metrics.js +94 -0
  92. package/dist/services/metrics.js.map +1 -0
  93. package/dist/services/remote-control.d.ts +20 -0
  94. package/dist/services/remote-control.js +209 -0
  95. package/dist/services/remote-control.js.map +1 -0
  96. package/dist/services/remote-ws.d.ts +5 -0
  97. package/dist/services/remote-ws.js +143 -0
  98. package/dist/services/remote-ws.js.map +1 -0
  99. package/dist/services/usage.d.ts +13 -0
  100. package/dist/services/usage.js +39 -0
  101. package/dist/services/usage.js.map +1 -0
  102. package/dist/utils/errors.d.ts +27 -0
  103. package/dist/utils/errors.js +48 -0
  104. package/dist/utils/errors.js.map +1 -0
  105. package/dist/utils/logger.d.ts +2 -0
  106. package/dist/utils/logger.js +14 -0
  107. package/dist/utils/logger.js.map +1 -0
  108. package/docker-compose.yml +19 -0
  109. package/package.json +39 -0
  110. package/public/robots.txt +8 -0
  111. package/src/config.ts +140 -0
  112. package/src/converter/request.ts +207 -0
  113. package/src/converter/response.ts +85 -0
  114. package/src/converter/stream.ts +373 -0
  115. package/src/converter/types.ts +257 -0
  116. package/src/data/posts.ts +474 -0
  117. package/src/index.ts +219 -0
  118. package/src/middleware/api-key-auth.ts +82 -0
  119. package/src/middleware/quota-guard.ts +28 -0
  120. package/src/middleware/rate-limiter.ts +61 -0
  121. package/src/middleware/request-logger.ts +36 -0
  122. package/src/middleware/session-auth.ts +91 -0
  123. package/src/providers/aliyun.ts +16 -0
  124. package/src/providers/base-provider.ts +148 -0
  125. package/src/providers/deepseek.ts +14 -0
  126. package/src/providers/registry.ts +111 -0
  127. package/src/providers/types.ts +26 -0
  128. package/src/routes/admin.ts +169 -0
  129. package/src/routes/auth.ts +369 -0
  130. package/src/routes/blog.ts +28 -0
  131. package/src/routes/dashboard.ts +208 -0
  132. package/src/routes/messages.ts +346 -0
  133. package/src/routes/models.ts +37 -0
  134. package/src/routes/payment.ts +189 -0
  135. package/src/routes/sitemap.ts +40 -0
  136. package/src/services/alipay.ts +116 -0
  137. package/src/services/database.ts +187 -0
  138. package/src/services/health-checker.ts +115 -0
  139. package/src/services/mailer.ts +90 -0
  140. package/src/services/metrics.ts +104 -0
  141. package/src/services/remote-control.ts +226 -0
  142. package/src/services/remote-ws.ts +145 -0
  143. package/src/services/usage.ts +57 -0
  144. package/src/types/express.d.ts +46 -0
  145. package/src/utils/errors.ts +44 -0
  146. package/src/utils/logger.ts +8 -0
  147. package/tsconfig.json +17 -0
  148. package/views/pages/404.ejs +14 -0
  149. package/views/pages/admin.ejs +307 -0
  150. package/views/pages/blog-post.ejs +378 -0
  151. package/views/pages/blog.ejs +148 -0
  152. package/views/pages/dashboard.ejs +441 -0
  153. package/views/pages/docs.ejs +807 -0
  154. package/views/pages/index.ejs +416 -0
  155. package/views/pages/login.ejs +170 -0
  156. package/views/pages/orders.ejs +111 -0
  157. package/views/pages/pricing.ejs +379 -0
  158. package/views/pages/register.ejs +397 -0
  159. package/views/pages/remote.ejs +334 -0
  160. package/views/pages/settings.ejs +373 -0
  161. package/views/partials/header.ejs +70 -0
  162. 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
+ }