tarsk 0.3.3 → 0.3.16
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/dist/cli.d.ts +3 -0
- package/dist/cli.js +22 -0
- package/dist/index.js +3 -2
- package/dist/public/assets/{index-DJC-p914.js → index-CLr9LKtA.js} +1679 -1682
- package/dist/public/index.html +1 -1
- package/node_modules/@neovate/code/LICENSE +21 -0
- package/node_modules/@neovate/code/README.md +56 -0
- package/node_modules/@neovate/code/dist/cli.mjs +714 -716
- package/node_modules/@neovate/code/dist/index.d.ts +0 -373
- package/node_modules/@neovate/code/dist/index.mjs +790 -792
- package/node_modules/@neovate/code/package.json +138 -2
- package/node_modules/@neovate/code/vendor/ripgrep/COPYING +3 -0
- package/node_modules/@neovate/code/vendor/ripgrep/arm64-darwin/rg +0 -0
- package/node_modules/@neovate/code/vendor/ripgrep/arm64-linux/rg +0 -0
- package/node_modules/@neovate/code/vendor/ripgrep/x64-darwin/rg +0 -0
- package/node_modules/@neovate/code/vendor/ripgrep/x64-linux/rg +0 -0
- package/node_modules/@neovate/code/vendor/ripgrep/x64-win32/rg.exe +0 -0
- package/package.json +2 -2
- package/dist/managers/ConversationManager.d.ts +0 -83
- package/dist/managers/ConversationManager.js +0 -129
- package/dist/managers/GitManager.d.ts +0 -133
- package/dist/managers/GitManager.js +0 -330
- package/dist/managers/MetadataManager.d.ts +0 -139
- package/dist/managers/MetadataManager.js +0 -309
- package/dist/managers/ModelManager.d.ts +0 -57
- package/dist/managers/ModelManager.js +0 -129
- package/dist/managers/NeovateExecutor.d.ts +0 -40
- package/dist/managers/NeovateExecutor.js +0 -138
- package/dist/managers/ProjectManager.d.ts +0 -162
- package/dist/managers/ProjectManager.js +0 -353
- package/dist/managers/ThreadManager.d.ts +0 -181
- package/dist/managers/ThreadManager.js +0 -325
- package/dist/model-info-openai.d.ts +0 -17
- package/dist/model-info-openai.js +0 -59
- package/dist/public/assets/index-B443aj9k.js +0 -8506
- package/dist/routes/chat-old.d.ts +0 -21
- package/dist/routes/chat-old.js +0 -251
- package/dist/routes/projects-old.d.ts +0 -20
- package/dist/routes/projects-old.js +0 -297
- package/dist/routes/threads-old.d.ts +0 -14
- package/dist/routes/threads-old.js +0 -393
- package/dist/utils/openai-pricing-scraper.d.ts +0 -17
- package/dist/utils/openai-pricing-scraper.js +0 -185
|
@@ -1,393 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Thread routes for the REST API
|
|
3
|
-
*
|
|
4
|
-
* Handles all thread-related operations:
|
|
5
|
-
* - POST /api/threads - Create a new thread
|
|
6
|
-
* - GET /api/threads?projectId=xxx - List threads for a project
|
|
7
|
-
* - DELETE /api/threads/:id - Delete a thread
|
|
8
|
-
*/
|
|
9
|
-
import { Hono } from 'hono';
|
|
10
|
-
import { validateThreadName } from '../utils/validation.js';
|
|
11
|
-
import { streamAsyncGenerator } from '../lib/stream-helper.js';
|
|
12
|
-
const extractTextBlocksFromContent = (content) => {
|
|
13
|
-
const trimmed = content.trim();
|
|
14
|
-
if (!trimmed.startsWith('[') && !trimmed.startsWith('{')) {
|
|
15
|
-
return [];
|
|
16
|
-
}
|
|
17
|
-
try {
|
|
18
|
-
const parsed = JSON.parse(trimmed);
|
|
19
|
-
if (Array.isArray(parsed)) {
|
|
20
|
-
return parsed
|
|
21
|
-
.filter((block) => block && typeof block === 'object' && block.type === 'text')
|
|
22
|
-
.map((block) => (typeof block.text === 'string' ? block.text : ''))
|
|
23
|
-
.filter((text) => text.length > 0);
|
|
24
|
-
}
|
|
25
|
-
if (parsed && typeof parsed === 'object' && typeof parsed.text === 'string') {
|
|
26
|
-
return [parsed.text];
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
return [];
|
|
31
|
-
}
|
|
32
|
-
return [];
|
|
33
|
-
};
|
|
34
|
-
const extractAssistantContent = (events, fallback) => {
|
|
35
|
-
if (!events || events.length === 0) {
|
|
36
|
-
return fallback;
|
|
37
|
-
}
|
|
38
|
-
const resultEvent = [...events].reverse().find((event) => event.type === 'result');
|
|
39
|
-
if (resultEvent && typeof resultEvent.content === 'string' && resultEvent.content.trim().length > 0) {
|
|
40
|
-
return resultEvent.content;
|
|
41
|
-
}
|
|
42
|
-
const assistantMessages = events.filter((event) => event.type === 'message' && event.role === 'assistant');
|
|
43
|
-
let lastTextBlock = null;
|
|
44
|
-
const assistantChunks = [];
|
|
45
|
-
for (const event of assistantMessages) {
|
|
46
|
-
if (typeof event.content !== 'string') {
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
assistantChunks.push(event.content);
|
|
50
|
-
const textBlocks = extractTextBlocksFromContent(event.content);
|
|
51
|
-
if (textBlocks.length > 0) {
|
|
52
|
-
lastTextBlock = textBlocks[textBlocks.length - 1];
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
if (lastTextBlock) {
|
|
56
|
-
return lastTextBlock;
|
|
57
|
-
}
|
|
58
|
-
if (assistantChunks.length === 0) {
|
|
59
|
-
return fallback;
|
|
60
|
-
}
|
|
61
|
-
const combined = assistantChunks.join('');
|
|
62
|
-
return combined.trim().length > 0 ? combined : fallback;
|
|
63
|
-
};
|
|
64
|
-
export function createThreadRoutes(threadManager, gitManager, conversationManager) {
|
|
65
|
-
const router = new Hono();
|
|
66
|
-
/**
|
|
67
|
-
* POST /api/threads
|
|
68
|
-
* Create a new thread for a project
|
|
69
|
-
*
|
|
70
|
-
* Request body:
|
|
71
|
-
* {
|
|
72
|
-
* "projectId": "uuid",
|
|
73
|
-
* "title": "Optional thread title"
|
|
74
|
-
* }
|
|
75
|
-
*
|
|
76
|
-
* Response: Newline-delimited JSON stream of ThreadEvent objects
|
|
77
|
-
*
|
|
78
|
-
* Requirements:
|
|
79
|
-
* - 6.3 - THE CLI SHALL expose REST API endpoints for Thread operations
|
|
80
|
-
* - 8.3 - WHEN creating a Thread, THE App SHALL send a POST request with the Project identifier to the CLI
|
|
81
|
-
* - 8.4 - THE CLI SHALL support streaming responses to the client
|
|
82
|
-
*/
|
|
83
|
-
router.post('/', async (c) => {
|
|
84
|
-
try {
|
|
85
|
-
const body = await c.req.json();
|
|
86
|
-
const { projectId, title } = body;
|
|
87
|
-
// Validate projectId is provided
|
|
88
|
-
if (!projectId || typeof projectId !== 'string') {
|
|
89
|
-
const errorResponse = {
|
|
90
|
-
error: {
|
|
91
|
-
code: 'INVALID_REQUEST',
|
|
92
|
-
message: 'projectId is required and must be a string',
|
|
93
|
-
timestamp: new Date().toISOString()
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
return c.json(errorResponse, 400);
|
|
97
|
-
}
|
|
98
|
-
// Validate title if provided
|
|
99
|
-
if (title !== undefined && typeof title !== 'string') {
|
|
100
|
-
const errorResponse = {
|
|
101
|
-
error: {
|
|
102
|
-
code: 'INVALID_REQUEST',
|
|
103
|
-
message: 'title must be a string if provided',
|
|
104
|
-
timestamp: new Date().toISOString()
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
return c.json(errorResponse, 400);
|
|
108
|
-
}
|
|
109
|
-
// Validate thread name (empty/whitespace is allowed, will auto-generate)
|
|
110
|
-
if (title && title.trim() !== '') {
|
|
111
|
-
const nameError = validateThreadName(title);
|
|
112
|
-
if (nameError) {
|
|
113
|
-
const errorResponse = {
|
|
114
|
-
error: {
|
|
115
|
-
code: 'INVALID_REQUEST',
|
|
116
|
-
message: nameError,
|
|
117
|
-
timestamp: new Date().toISOString()
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
return c.json(errorResponse, 400);
|
|
121
|
-
}
|
|
122
|
-
// Check for duplicate branch names
|
|
123
|
-
const existingThreads = await threadManager.listThreads(projectId);
|
|
124
|
-
const sanitizedNewName = gitManager.sanitizeBranchName(title);
|
|
125
|
-
const existingSanitized = existingThreads.map(t => gitManager.sanitizeBranchName(t.title));
|
|
126
|
-
if (existingSanitized.includes(sanitizedNewName)) {
|
|
127
|
-
const errorResponse = {
|
|
128
|
-
error: {
|
|
129
|
-
code: 'DUPLICATE_BRANCH',
|
|
130
|
-
message: `A thread with branch name "${sanitizedNewName}" already exists`,
|
|
131
|
-
timestamp: new Date().toISOString()
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
return c.json(errorResponse, 409);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
// Stream the thread creation events
|
|
138
|
-
return streamAsyncGenerator(c, threadManager.createThread(projectId, title));
|
|
139
|
-
}
|
|
140
|
-
catch (error) {
|
|
141
|
-
const errorResponse = {
|
|
142
|
-
error: {
|
|
143
|
-
code: 'REQUEST_PARSE_ERROR',
|
|
144
|
-
message: 'Failed to parse request body',
|
|
145
|
-
details: error instanceof Error ? error.message : String(error),
|
|
146
|
-
timestamp: new Date().toISOString()
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
return c.json(errorResponse, 400);
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
/**
|
|
153
|
-
* GET /api/threads?projectId=xxx
|
|
154
|
-
* List all threads for a specific project
|
|
155
|
-
*
|
|
156
|
-
* Query parameters:
|
|
157
|
-
* - projectId (required): The project ID to filter threads
|
|
158
|
-
*
|
|
159
|
-
* Response:
|
|
160
|
-
* [
|
|
161
|
-
* {
|
|
162
|
-
* "id": "uuid",
|
|
163
|
-
* "projectId": "uuid",
|
|
164
|
-
* "title": "Thread title",
|
|
165
|
-
* "path": "/path/to/thread",
|
|
166
|
-
* "currentBranch": "branch-name",
|
|
167
|
-
* "createdAt": "2024-01-01T00:00:00Z"
|
|
168
|
-
* }
|
|
169
|
-
* ]
|
|
170
|
-
*
|
|
171
|
-
* Requirements:
|
|
172
|
-
* - 6.3 - THE CLI SHALL expose REST API endpoints for Thread operations
|
|
173
|
-
*/
|
|
174
|
-
router.get('/', async (c) => {
|
|
175
|
-
try {
|
|
176
|
-
const projectId = c.req.query('projectId');
|
|
177
|
-
// Validate projectId is provided
|
|
178
|
-
if (!projectId) {
|
|
179
|
-
const errorResponse = {
|
|
180
|
-
error: {
|
|
181
|
-
code: 'INVALID_REQUEST',
|
|
182
|
-
message: 'projectId query parameter is required',
|
|
183
|
-
timestamp: new Date().toISOString()
|
|
184
|
-
}
|
|
185
|
-
};
|
|
186
|
-
return c.json(errorResponse, 400);
|
|
187
|
-
}
|
|
188
|
-
const threads = await threadManager.listThreads(projectId);
|
|
189
|
-
return c.json(threads);
|
|
190
|
-
}
|
|
191
|
-
catch (error) {
|
|
192
|
-
const errorResponse = {
|
|
193
|
-
error: {
|
|
194
|
-
code: 'LIST_THREADS_ERROR',
|
|
195
|
-
message: 'Failed to list threads',
|
|
196
|
-
details: error instanceof Error ? error.message : String(error),
|
|
197
|
-
timestamp: new Date().toISOString()
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
return c.json(errorResponse, 500);
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
/**
|
|
204
|
-
* DELETE /api/threads/:id
|
|
205
|
-
* Delete a thread
|
|
206
|
-
*
|
|
207
|
-
* Response:
|
|
208
|
-
* {
|
|
209
|
-
* "success": true,
|
|
210
|
-
* "message": "Thread deleted successfully"
|
|
211
|
-
* }
|
|
212
|
-
*
|
|
213
|
-
* Requirements:
|
|
214
|
-
* - 6.3 - THE CLI SHALL expose REST API endpoints for Thread operations
|
|
215
|
-
* - 8.4 - WHEN deleting a Thread, THE App SHALL send a DELETE request with the Thread identifier to the CLI
|
|
216
|
-
*/
|
|
217
|
-
router.delete('/:id', async (c) => {
|
|
218
|
-
try {
|
|
219
|
-
const threadId = c.req.param('id');
|
|
220
|
-
if (!threadId) {
|
|
221
|
-
const errorResponse = {
|
|
222
|
-
error: {
|
|
223
|
-
code: 'INVALID_REQUEST',
|
|
224
|
-
message: 'Thread ID is required',
|
|
225
|
-
timestamp: new Date().toISOString()
|
|
226
|
-
}
|
|
227
|
-
};
|
|
228
|
-
return c.json(errorResponse, 400);
|
|
229
|
-
}
|
|
230
|
-
await threadManager.deleteThread(threadId);
|
|
231
|
-
return c.json({
|
|
232
|
-
success: true,
|
|
233
|
-
message: 'Thread deleted successfully'
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
catch (error) {
|
|
237
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
238
|
-
// Check if it's a "not found" error
|
|
239
|
-
if (errorMessage.includes('not found')) {
|
|
240
|
-
const errorResponse = {
|
|
241
|
-
error: {
|
|
242
|
-
code: 'THREAD_NOT_FOUND',
|
|
243
|
-
message: errorMessage,
|
|
244
|
-
timestamp: new Date().toISOString()
|
|
245
|
-
}
|
|
246
|
-
};
|
|
247
|
-
return c.json(errorResponse, 404);
|
|
248
|
-
}
|
|
249
|
-
const errorResponse = {
|
|
250
|
-
error: {
|
|
251
|
-
code: 'DELETE_THREAD_ERROR',
|
|
252
|
-
message: 'Failed to delete thread',
|
|
253
|
-
details: errorMessage,
|
|
254
|
-
timestamp: new Date().toISOString()
|
|
255
|
-
}
|
|
256
|
-
};
|
|
257
|
-
return c.json(errorResponse, 500);
|
|
258
|
-
}
|
|
259
|
-
});
|
|
260
|
-
/**
|
|
261
|
-
* POST /api/threads/:id/select
|
|
262
|
-
* Select a thread as the active thread
|
|
263
|
-
*
|
|
264
|
-
* Response:
|
|
265
|
-
* {
|
|
266
|
-
* "success": true,
|
|
267
|
-
* "message": "Thread selected successfully",
|
|
268
|
-
* "threadId": "uuid"
|
|
269
|
-
* }
|
|
270
|
-
*
|
|
271
|
-
* Requirements:
|
|
272
|
-
* - 6.3 - THE CLI SHALL expose REST API endpoints for Thread operations
|
|
273
|
-
*/
|
|
274
|
-
router.post('/:id/select', async (c) => {
|
|
275
|
-
try {
|
|
276
|
-
const threadId = c.req.param('id');
|
|
277
|
-
if (!threadId) {
|
|
278
|
-
const errorResponse = {
|
|
279
|
-
error: {
|
|
280
|
-
code: 'INVALID_REQUEST',
|
|
281
|
-
message: 'Thread ID is required',
|
|
282
|
-
timestamp: new Date().toISOString()
|
|
283
|
-
}
|
|
284
|
-
};
|
|
285
|
-
return c.json(errorResponse, 400);
|
|
286
|
-
}
|
|
287
|
-
// Verify thread exists
|
|
288
|
-
const thread = await threadManager.getThread(threadId);
|
|
289
|
-
if (!thread) {
|
|
290
|
-
const errorResponse = {
|
|
291
|
-
error: {
|
|
292
|
-
code: 'THREAD_NOT_FOUND',
|
|
293
|
-
message: `Thread not found: ${threadId}`,
|
|
294
|
-
timestamp: new Date().toISOString()
|
|
295
|
-
}
|
|
296
|
-
};
|
|
297
|
-
return c.json(errorResponse, 404);
|
|
298
|
-
}
|
|
299
|
-
await threadManager.selectThread(threadId);
|
|
300
|
-
return c.json({
|
|
301
|
-
success: true,
|
|
302
|
-
message: 'Thread selected successfully',
|
|
303
|
-
threadId
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
catch (error) {
|
|
307
|
-
const errorResponse = {
|
|
308
|
-
error: {
|
|
309
|
-
code: 'SELECT_THREAD_ERROR',
|
|
310
|
-
message: 'Failed to select thread',
|
|
311
|
-
details: error instanceof Error ? error.message : String(error),
|
|
312
|
-
timestamp: new Date().toISOString()
|
|
313
|
-
}
|
|
314
|
-
};
|
|
315
|
-
return c.json(errorResponse, 500);
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
/**
|
|
319
|
-
* GET /api/threads/:id/messages
|
|
320
|
-
* Get chat history for a thread
|
|
321
|
-
*/
|
|
322
|
-
router.get('/:id/messages', async (c) => {
|
|
323
|
-
try {
|
|
324
|
-
const threadId = c.req.param('id');
|
|
325
|
-
if (!threadId) {
|
|
326
|
-
const errorResponse = {
|
|
327
|
-
error: {
|
|
328
|
-
code: 'INVALID_REQUEST',
|
|
329
|
-
message: 'Thread ID is required',
|
|
330
|
-
timestamp: new Date().toISOString()
|
|
331
|
-
}
|
|
332
|
-
};
|
|
333
|
-
return c.json(errorResponse, 400);
|
|
334
|
-
}
|
|
335
|
-
const thread = await threadManager.getThread(threadId);
|
|
336
|
-
if (!thread) {
|
|
337
|
-
const errorResponse = {
|
|
338
|
-
error: {
|
|
339
|
-
code: 'THREAD_NOT_FOUND',
|
|
340
|
-
message: `Thread not found: ${threadId}`,
|
|
341
|
-
timestamp: new Date().toISOString()
|
|
342
|
-
}
|
|
343
|
-
};
|
|
344
|
-
return c.json(errorResponse, 404);
|
|
345
|
-
}
|
|
346
|
-
const history = await conversationManager.getConversationHistory(thread.path);
|
|
347
|
-
if (!history || history.messages.length === 0) {
|
|
348
|
-
return c.json([]);
|
|
349
|
-
}
|
|
350
|
-
const messages = history.messages.flatMap((message) => {
|
|
351
|
-
const userMessage = {
|
|
352
|
-
messageId: `user-${message.id}`,
|
|
353
|
-
sender: 'user',
|
|
354
|
-
content: message.request.message,
|
|
355
|
-
contentType: 'text',
|
|
356
|
-
timestamp: message.timestamp,
|
|
357
|
-
model: message.request.model,
|
|
358
|
-
attachments: message.request.attachments?.map((attachment) => ({
|
|
359
|
-
fileName: attachment.name,
|
|
360
|
-
content: attachment.content,
|
|
361
|
-
size: attachment.size,
|
|
362
|
-
}))
|
|
363
|
-
};
|
|
364
|
-
if (!message.response) {
|
|
365
|
-
return [userMessage];
|
|
366
|
-
}
|
|
367
|
-
const agentMessage = {
|
|
368
|
-
messageId: `agent-${message.id}`,
|
|
369
|
-
sender: 'agent',
|
|
370
|
-
content: extractAssistantContent(message.response.events, message.response.content),
|
|
371
|
-
contentType: 'markdown',
|
|
372
|
-
timestamp: message.response.completedAt,
|
|
373
|
-
model: message.request.model,
|
|
374
|
-
};
|
|
375
|
-
return [userMessage, agentMessage];
|
|
376
|
-
});
|
|
377
|
-
return c.json(messages);
|
|
378
|
-
}
|
|
379
|
-
catch (error) {
|
|
380
|
-
const errorResponse = {
|
|
381
|
-
error: {
|
|
382
|
-
code: 'GET_THREAD_MESSAGES_ERROR',
|
|
383
|
-
message: 'Failed to load thread messages',
|
|
384
|
-
details: error instanceof Error ? error.message : String(error),
|
|
385
|
-
timestamp: new Date().toISOString()
|
|
386
|
-
}
|
|
387
|
-
};
|
|
388
|
-
return c.json(errorResponse, 500);
|
|
389
|
-
}
|
|
390
|
-
});
|
|
391
|
-
return router;
|
|
392
|
-
}
|
|
393
|
-
//# sourceMappingURL=threads-old.js.map
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenAI Pricing Scraper
|
|
3
|
-
*
|
|
4
|
-
* Scrapes pricing information from OpenAI's pricing page and caches it locally for 24 hours.
|
|
5
|
-
* This is necessary because the OpenAI API doesn't return pricing in the models list.
|
|
6
|
-
*/
|
|
7
|
-
import type { ModelInfoPricing } from '../model-info.js';
|
|
8
|
-
/**
|
|
9
|
-
* Get OpenAI pricing with caching.
|
|
10
|
-
* Returns cached pricing if available and fresh, otherwise attempts to scrape and cache new data.
|
|
11
|
-
* Falls back to hardcoded pricing if scraping fails (due to Cloudflare or other blocks).
|
|
12
|
-
*
|
|
13
|
-
* @param cacheDir - Directory to store cache file (typically .metadata)
|
|
14
|
-
* @returns Map of model name -> ModelInfoPricing
|
|
15
|
-
*/
|
|
16
|
-
export declare function getOpenAIPricing(cacheDir: string): Promise<Map<string, ModelInfoPricing>>;
|
|
17
|
-
//# sourceMappingURL=openai-pricing-scraper.d.ts.map
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenAI Pricing Scraper
|
|
3
|
-
*
|
|
4
|
-
* Scrapes pricing information from OpenAI's pricing page and caches it locally for 24 hours.
|
|
5
|
-
* This is necessary because the OpenAI API doesn't return pricing in the models list.
|
|
6
|
-
*/
|
|
7
|
-
import { promises as fs } from 'fs';
|
|
8
|
-
import { join } from 'path';
|
|
9
|
-
const PRICING_URL = 'https://openai.com/api/pricing';
|
|
10
|
-
const CACHE_DURATION_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
11
|
-
// Fallback pricing data in case scraping fails or Cloudflare blocks access
|
|
12
|
-
// Source: OpenAI's public pricing page (manually curated)
|
|
13
|
-
// Updated: February 2026
|
|
14
|
-
const FALLBACK_PRICING = {
|
|
15
|
-
'gpt-4-turbo': { input: 0.000010, output: 0.000030 },
|
|
16
|
-
'gpt-4': { input: 0.000030, output: 0.000060 },
|
|
17
|
-
'gpt-4.1': { input: 0.000003, output: 0.000012 },
|
|
18
|
-
'gpt-4o': { input: 0.000005, output: 0.000015 },
|
|
19
|
-
'gpt-4o-mini': { input: 0.00000015, output: 0.0000006 },
|
|
20
|
-
'gpt-3.5-turbo': { input: 0.0000005, output: 0.0000015 },
|
|
21
|
-
'o1': { input: 0.000015, output: 0.000060 },
|
|
22
|
-
'o1-mini': { input: 0.000003, output: 0.000012 },
|
|
23
|
-
};
|
|
24
|
-
/** Strip HTML tags and normalize whitespace */
|
|
25
|
-
function stripTags(html) {
|
|
26
|
-
return html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
|
|
27
|
-
}
|
|
28
|
-
/** Parse price from text (e.g., "$0.005 / 1M tokens" -> 0.000005) */
|
|
29
|
-
function parsePrice(text) {
|
|
30
|
-
if (!text)
|
|
31
|
-
return undefined;
|
|
32
|
-
const match = text.match(/[\d.]+/);
|
|
33
|
-
if (!match)
|
|
34
|
-
return undefined;
|
|
35
|
-
const price = Number(match[0]);
|
|
36
|
-
// Convert from price per 1M tokens to price per token
|
|
37
|
-
return price / 1_000_000;
|
|
38
|
-
}
|
|
39
|
-
/** Fetch and parse pricing from OpenAI's pricing page */
|
|
40
|
-
async function scrapePricingFromWeb() {
|
|
41
|
-
console.log('[OpenAI Pricing] Fetching pricing from', PRICING_URL);
|
|
42
|
-
const response = await fetch(PRICING_URL, {
|
|
43
|
-
headers: {
|
|
44
|
-
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
45
|
-
},
|
|
46
|
-
});
|
|
47
|
-
if (!response.ok) {
|
|
48
|
-
throw new Error(`Failed to fetch pricing page: ${response.status}`);
|
|
49
|
-
}
|
|
50
|
-
const html = await response.text();
|
|
51
|
-
const pricing = {
|
|
52
|
-
currency: 'USD',
|
|
53
|
-
unit: 'per_token',
|
|
54
|
-
last_updated: new Date().toISOString(),
|
|
55
|
-
text_models: {},
|
|
56
|
-
};
|
|
57
|
-
// Extract all tables
|
|
58
|
-
const tables = [...html.matchAll(/<table[\s\S]*?<\/table>/gi)];
|
|
59
|
-
console.log(`[OpenAI Pricing] Found ${tables.length} tables`);
|
|
60
|
-
for (const tableMatch of tables) {
|
|
61
|
-
const table = tableMatch[0];
|
|
62
|
-
// Extract rows
|
|
63
|
-
const rows = [...table.matchAll(/<tr[\s\S]*?<\/tr>/gi)];
|
|
64
|
-
for (const rowMatch of rows) {
|
|
65
|
-
const cells = [...rowMatch[0].matchAll(/<t[hd][\s\S]*?<\/t[hd]>/gi)].map((c) => stripTags(c[0]));
|
|
66
|
-
if (cells.length < 2)
|
|
67
|
-
continue;
|
|
68
|
-
const [model, input, cached, output] = cells;
|
|
69
|
-
console.log(`[OpenAI Pricing] Parsed row - model: "${model}", input: "${input}", cached: "${cached}", output: "${output}"`);
|
|
70
|
-
const inputPrice = parsePrice(input);
|
|
71
|
-
const cachedPrice = parsePrice(cached);
|
|
72
|
-
const outputPrice = parsePrice(output);
|
|
73
|
-
if (!model || (!inputPrice && !cachedPrice && !outputPrice))
|
|
74
|
-
continue;
|
|
75
|
-
pricing.text_models[model] = {
|
|
76
|
-
...(inputPrice !== undefined && { input: inputPrice }),
|
|
77
|
-
...(cachedPrice !== undefined && { cached_input: cachedPrice }),
|
|
78
|
-
...(outputPrice !== undefined && { output: outputPrice }),
|
|
79
|
-
};
|
|
80
|
-
if (Object.keys(pricing.text_models).length % 5 === 0) {
|
|
81
|
-
console.log(`[OpenAI Pricing] Parsed ${Object.keys(pricing.text_models).length} models...`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
console.log(`[OpenAI Pricing] Successfully scraped ${Object.keys(pricing.text_models).length} models`);
|
|
86
|
-
return pricing;
|
|
87
|
-
}
|
|
88
|
-
/** Check if cache file exists and is fresh */
|
|
89
|
-
async function isCacheFresh(cacheFile) {
|
|
90
|
-
try {
|
|
91
|
-
const stats = await fs.stat(cacheFile);
|
|
92
|
-
const age = Date.now() - stats.mtime.getTime();
|
|
93
|
-
const isFresh = age < CACHE_DURATION_MS;
|
|
94
|
-
console.log(`[OpenAI Pricing] Cache age: ${Math.round(age / 1000 / 60)} minutes, fresh: ${isFresh}`);
|
|
95
|
-
return isFresh;
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
console.log('[OpenAI Pricing] No cache file found');
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
/** Read cached pricing from file */
|
|
103
|
-
async function readCachedPricing(cacheFile) {
|
|
104
|
-
const content = await fs.readFile(cacheFile, 'utf-8');
|
|
105
|
-
return JSON.parse(content);
|
|
106
|
-
}
|
|
107
|
-
/** Write pricing cache to file */
|
|
108
|
-
async function writeCachedPricing(cacheFile, pricing) {
|
|
109
|
-
const dir = cacheFile.substring(0, cacheFile.lastIndexOf('/'));
|
|
110
|
-
await fs.mkdir(dir, { recursive: true });
|
|
111
|
-
await fs.writeFile(cacheFile, JSON.stringify(pricing, null, 2));
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Get OpenAI pricing with caching.
|
|
115
|
-
* Returns cached pricing if available and fresh, otherwise attempts to scrape and cache new data.
|
|
116
|
-
* Falls back to hardcoded pricing if scraping fails (due to Cloudflare or other blocks).
|
|
117
|
-
*
|
|
118
|
-
* @param cacheDir - Directory to store cache file (typically .metadata)
|
|
119
|
-
* @returns Map of model name -> ModelInfoPricing
|
|
120
|
-
*/
|
|
121
|
-
export async function getOpenAIPricing(cacheDir) {
|
|
122
|
-
const cacheFile = join(cacheDir, 'openai-pricing.json');
|
|
123
|
-
const result = new Map();
|
|
124
|
-
try {
|
|
125
|
-
let pricing = null;
|
|
126
|
-
// Try to use cache if it's fresh
|
|
127
|
-
if (await isCacheFresh(cacheFile)) {
|
|
128
|
-
try {
|
|
129
|
-
pricing = await readCachedPricing(cacheFile);
|
|
130
|
-
console.log('[OpenAI Pricing] Using cached pricing');
|
|
131
|
-
}
|
|
132
|
-
catch (error) {
|
|
133
|
-
console.error('[OpenAI Pricing] Error reading cache:', error);
|
|
134
|
-
pricing = null;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
// Scrape if we don't have fresh cache
|
|
138
|
-
if (!pricing) {
|
|
139
|
-
try {
|
|
140
|
-
pricing = await scrapePricingFromWeb();
|
|
141
|
-
try {
|
|
142
|
-
await writeCachedPricing(cacheFile, pricing);
|
|
143
|
-
console.log('[OpenAI Pricing] Cached pricing to', cacheFile);
|
|
144
|
-
}
|
|
145
|
-
catch (error) {
|
|
146
|
-
console.error('[OpenAI Pricing] Error writing cache:', error);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
console.error('[OpenAI Pricing] Error scraping pricing, using fallback data:', error);
|
|
151
|
-
// Use fallback pricing
|
|
152
|
-
for (const [model, prices] of Object.entries(FALLBACK_PRICING)) {
|
|
153
|
-
result.set(model, {
|
|
154
|
-
prompt: prices.input,
|
|
155
|
-
completion: prices.output,
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
console.log(`[OpenAI Pricing] Using fallback pricing for ${result.size} models`);
|
|
159
|
-
return result;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
// Convert to ModelInfoPricing format
|
|
163
|
-
for (const [model, prices] of Object.entries(pricing.text_models)) {
|
|
164
|
-
if (prices.input !== undefined && prices.output !== undefined) {
|
|
165
|
-
result.set(model, {
|
|
166
|
-
prompt: prices.input,
|
|
167
|
-
completion: prices.output,
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
console.log(`[OpenAI Pricing] Loaded pricing for ${result.size} models`);
|
|
172
|
-
}
|
|
173
|
-
catch (error) {
|
|
174
|
-
console.error('[OpenAI Pricing] Unexpected error getting pricing, using fallback:', error);
|
|
175
|
-
// Use fallback pricing as last resort
|
|
176
|
-
for (const [model, prices] of Object.entries(FALLBACK_PRICING)) {
|
|
177
|
-
result.set(model, {
|
|
178
|
-
prompt: prices.input,
|
|
179
|
-
completion: prices.output,
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
return result;
|
|
184
|
-
}
|
|
185
|
-
//# sourceMappingURL=openai-pricing-scraper.js.map
|