hins111 commited on
Commit
0508c90
·
verified ·
1 Parent(s): a41c1a4

Create main.ts

Browse files
Files changed (1) hide show
  1. main.ts +575 -0
main.ts ADDED
@@ -0,0 +1,575 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // file: openai_tau_proxy_confirm_newlines.ts
2
+
3
+ /**
4
+ * Deno script to proxy OpenAI chat completion requests to a Tau API.
5
+ * Handles request/response format translation, including streaming and
6
+ * mapping Tau's 'g:' chunks to OpenAI's 'reasoning_content'.
7
+ * Also supports the /v1/models endpoint.
8
+ * Includes fix for model placement, configurable API key, explicit semicolons,
9
+ * quote cleaning, and debug logging to confirm newline escaping in JSON.
10
+ */
11
+
12
+ // --- Constants ---
13
+ const TAU_API_URL = "https://tau-api.fly.dev/v1/chat";
14
+ const DEFAULT_TAU_MODEL = "anthropic-claude-4-opus"; // Default if client model isn't found or mapped
15
+ const ALLOWED_TAU_MODELS = [
16
+ "google-gemini-2.5-pro",
17
+ "anthropic-claude-4-sonnet-thinking",
18
+ "anthropic-claude-4-opus",
19
+ "anthropic-claude-4-sonnet",
20
+ "openai-gpt-4.1",
21
+ "openai-gpt-o1",
22
+ "openai-gpt-4o"
23
+ ];
24
+
25
+ // Simple mapping for common OpenAI names to Tau names if needed
26
+ const MODEL_MAP: Record<string, string> = {
27
+ "gpt-4o": "openai-gpt-4o",
28
+ "gpt-4": "openai-gpt-4.1", // Example mapping based on Tau's listed names
29
+ "gpt-3.5-turbo": "openai-gpt-o1", // Example mapping
30
+ "claude-3-opus-20240229": "anthropic-claude-4-opus",
31
+ "claude-3-sonnet-20240229": "anthropic-claude-4-sonnet",
32
+ // Add more mappings if clients use different names
33
+ };
34
+
35
+ // Read Tau API Key from environment variable
36
+ const TAU_API_KEY = Deno.env.get("TAU_API_KEY");
37
+ if (!TAU_API_KEY) {
38
+ console.warn("TAU_API_KEY environment variable is not set. Requests to Tau API might fail if authentication is required.");
39
+ }
40
+
41
+
42
+ // --- Helper Functions ---
43
+
44
+ /** Generates a UUID with a prefix for OpenAI-like IDs */
45
+ function generateId(prefix: string = ""): string {
46
+ return `${prefix}${crypto.randomUUID().replace(/-/g, '')}`;
47
+ }
48
+
49
+ /** Generates a current timestamp in seconds */
50
+ function getCurrentTimestamp(): number {
51
+ return Math.floor(Date.now() / 1000);
52
+ }
53
+
54
+ /** Parses a Tau API stream line, cleaning content from 0: and g: prefixes. */
55
+ function parseTauStreamLine(line: string): { prefix: string | null, content: string } {
56
+ console.debug(`Raw stream line received: "${line.replace(/\n/g, "\\n").replace(/\r/g, "\\r")}"`);
57
+
58
+ const colonIndex = line.indexOf(':');
59
+ if (colonIndex === -1) {
60
+ console.warn("Stream line missing prefix colon:", line);
61
+ return { prefix: null, content: line };
62
+ }
63
+ const prefix = line.substring(0, colonIndex);
64
+ let content = line.substring(colonIndex + 1);
65
+
66
+ // --- Cleaning Logic for 0: and g: content ---
67
+ if (prefix === '0' || prefix === 'g') {
68
+ // Check if content is wrapped in an extra pair of quotes, as observed
69
+ if (content.startsWith('"') && content.endsWith('"')) {
70
+ content = content.substring(1, content.length - 1);
71
+ }
72
+ // Replace any occurrences of double double quotes ("") with a single quote (")
73
+ content = content.replace(/""/g, '"');
74
+ // The content string *now* contains literal \n characters where they were in the Tau stream.
75
+ }
76
+ // --- End Cleaning Logic ---
77
+
78
+ console.debug(`Parsed line: Prefix: "${prefix}", Cleaned Content string (contains actual newlines): "${content.replace(/\n/g, "\\n").replace(/\r/g, "\\r")}"`);
79
+
80
+ return { prefix, content };
81
+ }
82
+
83
+ /** Converts Tau API stream line to OpenAI SSE chunk */
84
+ function tauLineToOpenAIChunk(
85
+ line: string,
86
+ completionId: string,
87
+ createdAt: number,
88
+ model: string
89
+ ): { sse: string | null, isDone: boolean, finishReason: string | null, usage: any | null } {
90
+ const { prefix, content } = parseTauStreamLine(line);
91
+
92
+ let delta: any = {};
93
+ let finishReason: string | null = null;
94
+ let isDone = false;
95
+ let usage: any | null = null;
96
+
97
+ if (prefix === '0') {
98
+ delta.content = content; // This is the cleaned string with actual \n characters
99
+ console.debug(`SSE Chunk (0:): Content string being put into delta: "${content.replace(/\n/g, "\\n").replace(/\r/g, "\\r")}"`);
100
+ } else if (prefix === 'g') {
101
+ delta.reasoning_content = content; // This is the cleaned string with actual \n characters
102
+ console.debug(`SSE Chunk (g:): Reasoning string being put into delta: "${content.replace(/\n/g, "\\n").replace(/\r{/g, "\\r")}"`);
103
+ } else if (prefix === 'e' || prefix === 'd') {
104
+ try {
105
+ const data = JSON.parse(content);
106
+ if (data.finishReason) {
107
+ finishReason = data.finishReason;
108
+ isDone = true;
109
+ console.debug(`SSE Chunk (e/d:): Found finish reason: ${finishReason}`);
110
+ }
111
+ if (data.usage) {
112
+ usage = {
113
+ prompt_tokens: data.usage.inputTokens || 0,
114
+ completion_tokens: data.usage.outputTokens || 0,
115
+ total_tokens: (data.usage.inputTokens || 0) + (data.usage.outputTokens || 0),
116
+ };
117
+ console.debug(`SSE Chunk (e/d:): Found usage: ${JSON.stringify(usage)}`);
118
+ }
119
+ } catch (e) {
120
+ console.error("Failed to parse JSON from e/d prefix:", content, e);
121
+ }
122
+ } else if (prefix === '8') {
123
+ try {
124
+ const data = JSON.parse(content);
125
+ if (data.usageCost && data.usageTokens) {
126
+ usage = {
127
+ prompt_tokens: data.usageTokens.inputTokens || 0,
128
+ completion_tokens: data.usageTokens.outputTokens || 0,
129
+ total_tokens: (data.usageTokens.inputTokens || 0) + (data.usageTokens.outputTokens || 0),
130
+ };
131
+ console.debug(`SSE Chunk (8:): Found usage: ${JSON.stringify(usage)}`);
132
+ }
133
+ } catch (e) {
134
+ console.error("Failed to parse JSON from 8 prefix:", content, e);
135
+ }
136
+ } else if (prefix === null) {
137
+ return { sse: null, isDone: false, finishReason: null, usage: null };
138
+ } else {
139
+ console.warn("Received unknown Tau stream prefix:", prefix, "content:", content);
140
+ return { sse: null, isDone: false, finishReason: null, usage: null };
141
+ }
142
+
143
+ if (Object.keys(delta).length === 0 && !finishReason && !usage) {
144
+ return { sse: null, isDone: false, finishReason: null, usage: null };
145
+ }
146
+
147
+ const chunk: any = {
148
+ id: completionId,
149
+ object: "chat.completion.chunk",
150
+ created: createdAt,
151
+ model: model,
152
+ choices: [{
153
+ index: 0,
154
+ delta: delta, // delta now contains the cleaned string with actual newlines
155
+ logprobs: null,
156
+ finish_reason: finishReason
157
+ }]
158
+ };
159
+
160
+ // JSON.stringify will automatically escape the actual newlines (\n) in delta strings to \\n
161
+ const sseData = JSON.stringify(chunk);
162
+ const sseString = `data: ${sseData}\n\n`;
163
+
164
+ // Log the final SSE string *exactly* as it's being sent over the wire
165
+ console.debug(`SSE Chunk: Final data line being sent: "${sseString.replace(/\n/g, "\\n").replace(/\r/g, "\\r")}"`);
166
+
167
+
168
+ return { sse: sseString, isDone: isDone, finishReason: finishReason, usage: usage };
169
+ }
170
+
171
+ // --- Endpoint Handlers ---
172
+
173
+ function handleListModels(): Response {
174
+ const models = ALLOWED_TAU_MODELS.map(modelName => ({
175
+ id: modelName,
176
+ object: "model",
177
+ created: getCurrentTimestamp(),
178
+ owned_by: "tau-proxy",
179
+ }));
180
+
181
+ const responseBody = {
182
+ object: "list",
183
+ data: models,
184
+ };
185
+
186
+ return new Response(JSON.stringify(responseBody, null, 2), {
187
+ headers: { "Content-Type": "application/json" },
188
+ status: 200,
189
+ });
190
+ }
191
+
192
+ async function handleChatCompletions(request: Request): Promise<Response> {
193
+ let reqBody: any;
194
+ try {
195
+ reqBody = await request.json();
196
+ } catch (error) {
197
+ console.error("Failed to parse request body:", error);;
198
+ return new Response(JSON.stringify({ error: { message: "Invalid JSON body", type: "invalid_request_error" } }), {
199
+ status: 400,
200
+ headers: { "Content-Type": "application/json" },
201
+ });;
202
+ }
203
+
204
+ if (!Array.isArray(reqBody.messages) || reqBody.messages.length === 0) {
205
+ return new Response(JSON.stringify({ error: { message: "Request body must contain a non-empty 'messages' array", type: "invalid_request_error" } }), {
206
+ status: 400,
207
+ headers: { "Content-Type": "application/json" },
208
+ });;
209
+ }
210
+
211
+ const clientRequestedModel = reqBody.model;
212
+ const stream = reqBody.stream === true;
213
+
214
+ let tauModel = DEFAULT_TAU_MODEL;
215
+ if (clientRequestedModel) {
216
+ const mappedModel = MODEL_MAP[clientRequestedModel];
217
+ if (mappedModel && ALLOWED_TAU_MODELS.includes(mappedModel)) {
218
+ tauModel = mappedModel;
219
+ } else if (ALLOWED_TAU_MODELS.includes(clientRequestedModel)) {
220
+ tauModel = clientRequestedModel;
221
+ } else {
222
+ console.warn(`Client requested model "${clientRequestedModel}" not found in mapping or allowed Tau models. Using default: "${DEFAULT_TAU_MODEL}"`);
223
+ }
224
+ } else {
225
+ console.log(`No model specified by client. Using default: "${DEFAULT_TAU_MODEL}"`);
226
+ }
227
+
228
+
229
+ let modelAdded = false;
230
+ const tauRequestMessages = reqBody.messages.map((msg: any) => {
231
+ const messageId = generateId("msg_");
232
+ const createdAt = new Date().toISOString();
233
+ const role = msg.role;
234
+
235
+ let parts: any[] = [];
236
+ if (typeof msg.content === 'string' && msg.content.length > 0) {
237
+ parts.push({ type: "text", text: msg.content });
238
+ } else if (Array.isArray(msg.content)) {
239
+ parts = msg.content.filter((part: any) => part.type === 'text' && part.text && part.text.length > 0).map((part: any) => ({ type: "text", text: part.text }));
240
+ if (parts.length === 0 && msg.content.length > 0) {
241
+ console.warn("Unsupported non-text multimodal content in message:", msg.content);
242
+ }
243
+ }
244
+
245
+ const tauMessage: any = {
246
+ id: messageId,
247
+ content: "", // As per example
248
+ role: role,
249
+ parts: parts,
250
+ metadata: {}, // As per example
251
+ createdAt: createdAt,
252
+ };
253
+
254
+ if (!modelAdded && role === 'user') {
255
+ tauMessage.model = tauModel;
256
+ modelAdded = true;
257
+ console.log(`Added model ${tauModel} to the first user message.`);
258
+ } else if (role === 'assistant') {
259
+ tauMessage.content = typeof msg.content === 'string' ? msg.content : '';
260
+ tauMessage.parts = [];
261
+ if (typeof msg.content !== 'string' && msg.content != null) {
262
+ console.warn(`Assistant message content is not a string and cannot be mapped to Tau's content field: ${typeof msg.content}`);
263
+ }
264
+ }
265
+
266
+ return tauMessage;
267
+ });
268
+
269
+ const tauRequestId = generateId("bld_");
270
+
271
+ const tauRequestBody = {
272
+ id: tauRequestId,
273
+ messages: tauRequestMessages,
274
+ };
275
+
276
+ console.log("Sending request to Tau API:", JSON.stringify(tauRequestBody, null, 2));
277
+
278
+ const headers: HeadersInit = {
279
+ "Content-Type": "application/json",
280
+ };
281
+ if (TAU_API_KEY) {
282
+ headers["Authorization"] = `Bearer ${TAU_API_KEY}`;
283
+ }
284
+
285
+
286
+ let tauResponse: Response;
287
+ try {
288
+ tauResponse = await fetch(TAU_API_URL, {
289
+ method: "POST",
290
+ headers: headers,
291
+ body: JSON.stringify(tauRequestBody),
292
+ });
293
+ } catch (error) {
294
+ console.error("Failed to connect to Tau API:", error);;
295
+ return new Response(JSON.stringify({ error: { message: `Failed to connect to upstream API: ${error.message}`, type: "upstream_error" } }), {
296
+ status: 500,
297
+ headers: { "Content-Type": "application/json" },
298
+ });;
299
+ }
300
+
301
+ if (!tauResponse.ok) {
302
+ const errorBody = await tauResponse.text();
303
+ console.error(`Tau API returned status ${tauResponse.status}: ${errorBody}`);;
304
+ let errorJson = null;
305
+ try {
306
+ errorJson = JSON.parse(errorBody);
307
+ } catch (e) { /* Not JSON */ }
308
+
309
+ return new Response(JSON.stringify({
310
+ error: {
311
+ message: `Upstream API error: ${tauResponse.status} - ${errorBody}`,
312
+ type: "upstream_error",
313
+ details: errorJson
314
+ }
315
+ }), {
316
+ status: tauResponse.status >= 400 && tauResponse.status < 500 ? 400 : 502,
317
+ headers: { "Content-Type": "application/json" },
318
+ });;
319
+ }
320
+
321
+ // --- Handle Tau API Response ---
322
+
323
+ const completionId = generateId("chatcmpl-");
324
+ const createdAt = getCurrentTimestamp();
325
+
326
+ if (stream) {
327
+ // --- Streaming Response ---
328
+ const reader = tauResponse.body!.getReader();
329
+ const { readable, writable } = new TransformStream();
330
+ const writer = writable.getWriter();
331
+ const encoder = new TextEncoder();
332
+ const decoder = new TextDecoder();
333
+
334
+ async function processStream() {
335
+ let buffer = "";
336
+ let finished = false;
337
+
338
+ try {
339
+ while (!finished) {
340
+ const { done, value } = await reader.read();
341
+
342
+ if (done) {
343
+ finished = true;
344
+ } else {
345
+ buffer += decoder.decode(value, { stream: true });
346
+ }
347
+
348
+ let newlineIndex;
349
+ while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
350
+ const line = buffer.substring(0, newlineIndex);
351
+ buffer = buffer.substring(newlineIndex + 1);
352
+
353
+ if (line.trim() === "") continue;
354
+ // parseTauStreamLine logs raw line and cleaned content
355
+
356
+ const { sse, isDone, finishReason, usage } = tauLineToOpenAIChunk(line, completionId, createdAt, tauModel);
357
+
358
+ if (sse) {
359
+ await writer.write(encoder.encode(sse));
360
+ }
361
+
362
+ if (isDone) {
363
+ finished = true;
364
+ }
365
+ }
366
+
367
+ if (finished && buffer.length > 0) {
368
+ console.warn("Processing leftover buffer after stream end:", buffer);;
369
+ const { sse, isDone: lastIsDone, finishReason: lastFinishReason, usage: lastChunkUsage } = tauLineToOpenAIChunk(buffer, completionId, createdAt, tauModel);
370
+ if (sse) {
371
+ await writer.write(encoder.encode(sse));
372
+ }
373
+ finished = finished || lastIsDone;
374
+ buffer = "";
375
+ }
376
+ }
377
+ } catch (error) {
378
+ console.error("Stream processing error:", error);;
379
+ try {
380
+ await writer.write(encoder.encode(`data: ${JSON.stringify({ error: { message: `Stream error: ${error.message}`, type: "stream_error" } })}\n\n`));
381
+ } catch (writeError) { console.error("Failed to write error message:", writeError);; }
382
+ } finally {
383
+ try {
384
+ await writer.write(encoder.encode("data: [DONE]\n\n"));
385
+ await writer.close();
386
+ } catch (closeError) { console.error("Failed to send DONE or close stream:", closeError);; }
387
+ }
388
+ }
389
+
390
+ processStream();
391
+
392
+ return new Response(readable, {
393
+ headers: {
394
+ "Content-Type": "text/event-stream",
395
+ "Cache-Control": "no-cache",
396
+ "Connection": "keep-alive",
397
+ },
398
+ });;
399
+
400
+ } else {
401
+ // --- Non-Streaming Response ---
402
+ let buffer = "";
403
+ const reader = tauResponse.body!.getReader();
404
+ const decoder = new TextDecoder();
405
+
406
+ let combinedContent = "";
407
+ let combinedReasoningContent = "";
408
+ let finishReason: string | null = null;
409
+ let usageData: any | null = null;
410
+
411
+ try {
412
+ while (true) {
413
+ const { done, value } = await reader.read();
414
+ buffer += decoder.decode(value, { stream: !done });
415
+
416
+ if (done) {
417
+ break;
418
+ }
419
+
420
+ let newlineIndex;
421
+ while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
422
+ const line = buffer.substring(0, newlineIndex);
423
+ buffer = buffer.substring(newlineIndex + 1);
424
+
425
+ if (line.trim() === "") continue;
426
+ // parseTauStreamLine logs raw line and cleaned content
427
+
428
+ const { prefix, content } = parseTauStreamLine(line);
429
+
430
+ if (prefix === '0') {
431
+ combinedContent += content; // Use the cleaned content
432
+ console.debug(`Non-stream: Appended to combinedContent string: "${content.replace(/\n/g, "\\n").replace(/\r/g, "\\r")}"`);
433
+ } else if (prefix === 'g') {
434
+ combinedReasoningContent += content; // Use the cleaned content
435
+ console.debug(`Non-stream: Appended to combinedReasoningContent string: "${content.replace(/\n/g, "\\n").replace(/\r/g, "\\r")}"`);
436
+ } else if (prefix === 'e' || prefix === 'd') {
437
+ try {
438
+ const data = JSON.parse(content);
439
+ if (data.finishReason) {
440
+ finishReason = data.finishReason;
441
+ console.debug(`Non-stream: Found finish reason: ${finishReason}`);
442
+ }
443
+ if (data.usage) {
444
+ usageData = {
445
+ prompt_tokens: data.usage.inputTokens || 0,
446
+ completion_tokens: data.usage.outputTokens || 0,
447
+ total_tokens: (data.usage.inputTokens || 0) + (data.usage.outputTokens || 0),
448
+ };
449
+ console.debug(`Non-stream: Found usage (e/d): ${JSON.stringify(usageData)}`);
450
+ }
451
+ } catch (e) {
452
+ console.error("Failed to parse JSON from e/d prefix (non-stream):", content, e);;
453
+ }
454
+ } else if (prefix === '8') {
455
+ try {
456
+ const data = JSON.parse(content);
457
+ if (data.usageCost && data.usageTokens) {
458
+ usageData = {
459
+ prompt_tokens: data.usageTokens.inputTokens || 0,
460
+ completion_tokens: data.usageTokens.outputTokens || 0,
461
+ total_tokens: (data.usageTokens.inputTokens || 0) + (data.usageTokens.outputTokens || 0),
462
+ };
463
+ console.debug(`Non-stream: Found usage (8): ${JSON.stringify(usageData)}`);
464
+ }
465
+ } catch (e) {
466
+ console.error("Failed to parse JSON from 8 prefix (non-stream):", content, e);;
467
+ }
468
+ } else if (prefix === null) {
469
+ console.warn("Ignoring non-stream line with no prefix:", line);
470
+ } else {
471
+ console.warn("Received unknown Tau non-stream prefix:", prefix, "content:", content);
472
+ }
473
+ }
474
+ }
475
+ } catch (error) {
476
+ console.error("Error reading Tau API response (non-stream):", error);;
477
+ return new Response(JSON.stringify({ error: { message: `Error processing upstream response: ${error.message}`, type: "upstream_error" } }), {
478
+ status: 500,
479
+ headers: { "Content-Type": "application/json" },
480
+ });;
481
+ }
482
+
483
+ // Process any remaining buffer after the loop
484
+ if (buffer.length > 0) {
485
+ console.warn("Non-stream buffer leftover:", buffer);;
486
+ const lines = buffer.split('\n');
487
+ for(const line of lines) {
488
+ if (line.trim() === "") continue;
489
+ const { prefix, content } = parseTauStreamLine(line);
490
+ if (prefix === '0') {
491
+ combinedContent += content; // Use the cleaned content
492
+ console.debug(`Non-stream: Appended leftover to combinedContent string: "${content.replace(/\n/g, "\\n").replace(/\r/g, "\\r")}"`);
493
+ } else if (prefix === 'g') {
494
+ combinedReasoningContent += content; // Use the cleaned content
495
+ console.debug(`Non-stream: Appended leftover to combinedReasoningContent string: "${content.replace(/\n/g, "\\n").replace(/\r/g, "\\r")}"`);
496
+ } else if (prefix === 'e' || prefix === 'd') {
497
+ try {
498
+ const data = JSON.parse(content);
499
+ if (data.finishReason) finishReason = data.finishReason;
500
+ if (data.usage) usageData = { prompt_tokens: data.usage.inputTokens || 0, completion_tokens: data.usage.completionTokens || 0, total_tokens: (data.usage.inputTokens || 0) + (data.usage.completionTokens || 0) };
501
+ console.debug(`Non-stream: Found leftover usage/finish (e/d): ${JSON.stringify(usageData || { finishReason })}`);
502
+ } catch (e) { console.warn("Failed to parse leftover e/d:", buffer);; }
503
+ } else if (prefix === '8') {
504
+ try {
505
+ const data = JSON.parse(content);
506
+ if (data.usageCost && data.usageTokens) { usageData = { prompt_tokens: data.usageTokens.inputTokens || 0, completion_tokens: data.usageTokens.outputTokens || 0, total_tokens: (data.usageTokens.inputTokens || 0) + (data.usageTokens.outputTokens || 0) }; }
507
+ console.debug(`Non-stream: Found leftover usage (8): ${JSON.stringify(usageData)}`);
508
+ } catch (e) { console.warn("Failed to parse leftover 8:", buffer);; }
509
+ } else if (prefix === null) {
510
+ console.warn("Ignoring leftover non-stream line with no prefix:", line);
511
+ } else {
512
+ console.warn("Received unknown Tau non-stream leftover prefix:", prefix, "content:", content);
513
+ }
514
+ }
515
+ }
516
+
517
+ // Log the final combined string content *before* it's JSON.stringify-ed
518
+ console.debug("Non-Stream: Final combinedContent string before JSON.stringify:", `"${combinedContent.replace(/\n/g, "\\n").replace(/\r/g, "\\r")}"`);
519
+ console.debug("Non-Stream: Final combinedReasoningContent string before JSON.stringify:", `"${combinedReasoningContent.replace(/\n/g, "\\n").replace(/\r/g, "\\r")}"`);
520
+
521
+
522
+ const responseJson: any = {
523
+ id: completionId,
524
+ object: "chat.completion",
525
+ created: createdAt,
526
+ model: tauModel,
527
+ choices: [{
528
+ index: 0,
529
+ message: {
530
+ role: "assistant",
531
+ content: combinedContent, // Use the combined, cleaned string
532
+ },
533
+ logprobs: null,
534
+ finish_reason: finishReason,
535
+ }],
536
+ usage: usageData ? {
537
+ prompt_tokens: usageData.prompt_tokens,
538
+ completion_tokens: usageData.completion_tokens,
539
+ total_tokens: usageData.prompt_tokens + usageData.completion_tokens,
540
+ } : { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
541
+ system_fingerprint: "tau_api_proxy",
542
+ };;
543
+
544
+ console.debug("Final Non-Stream Response Body (after JSON.stringify):", JSON.stringify(responseJson, null, 2));
545
+
546
+ return new Response(JSON.stringify(responseJson, null, 2), {
547
+ headers: { "Content-Type": "application/json" },
548
+ });;
549
+ }
550
+ }
551
+
552
+
553
+ // --- Main Request Handler Dispatcher ---
554
+
555
+ async function handler(request: Request): Promise<Response> {
556
+ const url = new URL(request.url);
557
+ const path = url.pathname;
558
+ const method = request.method;
559
+
560
+ console.log(`Received request: ${method} ${path}`);;
561
+
562
+ if (method === "GET" && path === "/v1/models") {
563
+ return handleListModels();;
564
+ } else if (method === "POST" && path === "/v1/chat/completions") {
565
+ return handleChatCompletions(request);;
566
+ } else {
567
+ return new Response("Not Found", { status: 404 });;
568
+ }
569
+ }
570
+
571
+
572
+ // --- Start Server ---
573
+ const PORT = 8000;
574
+ console.log(`Listening on http://localhost:${PORT}/`);;
575
+ Deno.serve({ port: PORT }, handler);;