@askalf/dario 1.2.1 → 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/README.md +22 -4
- package/dist/cli.js +5 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/proxy.d.ts +1 -0
- package/dist/proxy.js +217 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
|
|
17
17
|
<p align="center">
|
|
18
18
|
<a href="#quick-start">Quick Start</a> •
|
|
19
|
-
<a href="#
|
|
20
|
-
<a href="#usage-examples">Examples</a> •
|
|
19
|
+
<a href="#openai-compatibility">OpenAI Compat</a> •
|
|
21
20
|
<a href="#cli-backend">CLI Backend</a> •
|
|
21
|
+
<a href="#usage-examples">Examples</a> •
|
|
22
22
|
<a href="#faq">FAQ</a>
|
|
23
23
|
</p>
|
|
24
24
|
|
|
@@ -157,6 +157,21 @@ Combine with `--cli` for rate-limit-proof Opus:
|
|
|
157
157
|
dario proxy --cli --model=opus
|
|
158
158
|
```
|
|
159
159
|
|
|
160
|
+
## OpenAI Compatibility
|
|
161
|
+
|
|
162
|
+
Dario implements `/v1/chat/completions` — any tool built for the OpenAI API works with your Claude subscription. No code changes needed.
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
dario proxy --model=opus
|
|
166
|
+
|
|
167
|
+
export OPENAI_BASE_URL=http://localhost:3456/v1
|
|
168
|
+
export OPENAI_API_KEY=dario
|
|
169
|
+
|
|
170
|
+
# Cursor, Continue, LiteLLM, any OpenAI SDK — all work
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Use `--model=opus` to force the model regardless of what the client sends. Or pass `claude-opus-4-6` as the model name directly — Claude model names work as-is.
|
|
174
|
+
|
|
160
175
|
## Usage Examples
|
|
161
176
|
|
|
162
177
|
### curl
|
|
@@ -290,7 +305,8 @@ ANTHROPIC_BASE_URL=http://localhost:3456 ANTHROPIC_API_KEY=dario your-tool-here
|
|
|
290
305
|
|
|
291
306
|
### Direct API Mode
|
|
292
307
|
- All Claude models (Opus 4.6, Sonnet 4.6, Haiku 4.5)
|
|
293
|
-
-
|
|
308
|
+
- **OpenAI-compatible** (`/v1/chat/completions`) — works with any OpenAI SDK or tool
|
|
309
|
+
- Streaming and non-streaming (both Anthropic and OpenAI SSE formats)
|
|
294
310
|
- Tool use / function calling
|
|
295
311
|
- System prompts and multi-turn conversations
|
|
296
312
|
- Prompt caching and extended thinking
|
|
@@ -307,7 +323,9 @@ ANTHROPIC_BASE_URL=http://localhost:3456 ANTHROPIC_API_KEY=dario your-tool-here
|
|
|
307
323
|
|
|
308
324
|
| Path | Description |
|
|
309
325
|
|------|-------------|
|
|
310
|
-
| `POST /v1/messages` | Anthropic Messages API
|
|
326
|
+
| `POST /v1/messages` | Anthropic Messages API |
|
|
327
|
+
| `POST /v1/chat/completions` | OpenAI-compatible Chat API |
|
|
328
|
+
| `GET /v1/models` | Model list (works with both SDKs) |
|
|
311
329
|
| `GET /health` | Proxy health + OAuth status + request count |
|
|
312
330
|
| `GET /status` | Detailed OAuth token status |
|
|
313
331
|
|
package/dist/cli.js
CHANGED
|
@@ -13,9 +13,9 @@ import { readFile, unlink } from 'node:fs/promises';
|
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { homedir } from 'node:os';
|
|
15
15
|
import { startAutoOAuthFlow, getStatus, refreshTokens } from './oauth.js';
|
|
16
|
-
import { startProxy } from './proxy.js';
|
|
16
|
+
import { startProxy, sanitizeError } from './proxy.js';
|
|
17
17
|
const args = process.argv.slice(2);
|
|
18
|
-
const command = args[0] ??
|
|
18
|
+
const command = args[0] ?? 'proxy';
|
|
19
19
|
async function login() {
|
|
20
20
|
console.log('');
|
|
21
21
|
console.log(' dario — Claude Login');
|
|
@@ -50,7 +50,7 @@ async function login() {
|
|
|
50
50
|
}
|
|
51
51
|
catch (err) {
|
|
52
52
|
console.error('');
|
|
53
|
-
console.error(` Login failed: ${err
|
|
53
|
+
console.error(` Login failed: ${sanitizeError(err)}`);
|
|
54
54
|
console.error(' Try again with `dario login`.');
|
|
55
55
|
process.exit(1);
|
|
56
56
|
}
|
|
@@ -89,7 +89,7 @@ async function refresh() {
|
|
|
89
89
|
console.log(`[dario] Token refreshed. Expires in ${expiresIn} minutes.`);
|
|
90
90
|
}
|
|
91
91
|
catch (err) {
|
|
92
|
-
console.error(`[dario] Refresh failed: ${err
|
|
92
|
+
console.error(`[dario] Refresh failed: ${sanitizeError(err)}`);
|
|
93
93
|
process.exit(1);
|
|
94
94
|
}
|
|
95
95
|
}
|
|
@@ -172,7 +172,6 @@ if (!handler) {
|
|
|
172
172
|
process.exit(1);
|
|
173
173
|
}
|
|
174
174
|
handler().catch(err => {
|
|
175
|
-
|
|
176
|
-
console.error('Fatal error:', msg.replace(/sk-ant-[a-zA-Z0-9_-]+/g, '[REDACTED]'));
|
|
175
|
+
console.error('Fatal error:', sanitizeError(err));
|
|
177
176
|
process.exit(1);
|
|
178
177
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -6,4 +6,4 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { startAutoOAuthFlow, refreshTokens, getAccessToken, getStatus, loadCredentials } from './oauth.js';
|
|
8
8
|
export type { OAuthTokens, CredentialsFile } from './oauth.js';
|
|
9
|
-
export { startProxy } from './proxy.js';
|
|
9
|
+
export { startProxy, sanitizeError } from './proxy.js';
|
package/dist/index.js
CHANGED
package/dist/proxy.d.ts
CHANGED
package/dist/proxy.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* No API key needed — your Claude subscription pays for it.
|
|
9
9
|
*/
|
|
10
10
|
import { createServer } from 'node:http';
|
|
11
|
-
import { randomUUID } from 'node:crypto';
|
|
11
|
+
import { randomUUID, timingSafeEqual } from 'node:crypto';
|
|
12
12
|
import { execSync, spawn } from 'node:child_process';
|
|
13
13
|
import { arch, platform, version as nodeVersion } from 'node:process';
|
|
14
14
|
import { getAccessToken, getStatus } from './oauth.js';
|
|
@@ -17,7 +17,6 @@ const DEFAULT_PORT = 3456;
|
|
|
17
17
|
const MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB — generous for large prompts, prevents abuse
|
|
18
18
|
const UPSTREAM_TIMEOUT_MS = 300_000; // 5 min — matches Anthropic SDK default
|
|
19
19
|
const LOCALHOST = '127.0.0.1';
|
|
20
|
-
const CORS_ORIGIN = 'http://localhost';
|
|
21
20
|
// Detect installed Claude Code version at startup
|
|
22
21
|
function detectClaudeVersion() {
|
|
23
22
|
try {
|
|
@@ -55,10 +54,139 @@ const MODEL_ALIASES = {
|
|
|
55
54
|
'sonnet': 'claude-sonnet-4-6',
|
|
56
55
|
'haiku': 'claude-haiku-4-5',
|
|
57
56
|
};
|
|
58
|
-
|
|
57
|
+
// OpenAI model name → Anthropic model name
|
|
58
|
+
const OPENAI_MODEL_MAP = {
|
|
59
|
+
'gpt-4.1': 'claude-opus-4-6',
|
|
60
|
+
'gpt-4.1-mini': 'claude-sonnet-4-6',
|
|
61
|
+
'gpt-4.1-nano': 'claude-haiku-4-5',
|
|
62
|
+
'gpt-4o': 'claude-opus-4-6',
|
|
63
|
+
'gpt-4o-mini': 'claude-haiku-4-5',
|
|
64
|
+
'gpt-4-turbo': 'claude-opus-4-6',
|
|
65
|
+
'gpt-4': 'claude-opus-4-6',
|
|
66
|
+
'gpt-3.5-turbo': 'claude-haiku-4-5',
|
|
67
|
+
'o3': 'claude-opus-4-6',
|
|
68
|
+
'o3-mini': 'claude-sonnet-4-6',
|
|
69
|
+
'o4-mini': 'claude-sonnet-4-6',
|
|
70
|
+
'o1': 'claude-opus-4-6',
|
|
71
|
+
'o1-mini': 'claude-sonnet-4-6',
|
|
72
|
+
'o1-pro': 'claude-opus-4-6',
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Translate OpenAI chat completion request → Anthropic Messages request.
|
|
76
|
+
*/
|
|
77
|
+
function openaiToAnthropic(body, modelOverride) {
|
|
78
|
+
const messages = body.messages;
|
|
79
|
+
if (!messages)
|
|
80
|
+
return body;
|
|
81
|
+
// Extract system messages
|
|
82
|
+
const systemMessages = messages.filter(m => m.role === 'system');
|
|
83
|
+
const nonSystemMessages = messages.filter(m => m.role !== 'system');
|
|
84
|
+
// Map model name
|
|
85
|
+
const requestModel = String(body.model || '');
|
|
86
|
+
const model = modelOverride || OPENAI_MODEL_MAP[requestModel] || requestModel;
|
|
87
|
+
const result = {
|
|
88
|
+
model,
|
|
89
|
+
messages: nonSystemMessages.map(m => ({
|
|
90
|
+
role: m.role === 'assistant' ? 'assistant' : 'user',
|
|
91
|
+
content: m.content,
|
|
92
|
+
})),
|
|
93
|
+
max_tokens: body.max_tokens ?? body.max_completion_tokens ?? 8192,
|
|
94
|
+
};
|
|
95
|
+
if (systemMessages.length > 0) {
|
|
96
|
+
result.system = systemMessages.map(m => typeof m.content === 'string' ? m.content : JSON.stringify(m.content)).join('\n');
|
|
97
|
+
}
|
|
98
|
+
if (body.stream)
|
|
99
|
+
result.stream = true;
|
|
100
|
+
if (body.temperature != null)
|
|
101
|
+
result.temperature = body.temperature;
|
|
102
|
+
if (body.top_p != null)
|
|
103
|
+
result.top_p = body.top_p;
|
|
104
|
+
if (body.stop)
|
|
105
|
+
result.stop_sequences = Array.isArray(body.stop) ? body.stop : [body.stop];
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Translate Anthropic Messages response → OpenAI chat completion response.
|
|
110
|
+
*/
|
|
111
|
+
function anthropicToOpenai(body) {
|
|
112
|
+
const content = body.content;
|
|
113
|
+
const text = content?.find(c => c.type === 'text')?.text ?? '';
|
|
114
|
+
const usage = body.usage;
|
|
115
|
+
return {
|
|
116
|
+
id: `chatcmpl-${(body.id || '').replace('msg_', '')}`,
|
|
117
|
+
object: 'chat.completion',
|
|
118
|
+
created: Math.floor(Date.now() / 1000),
|
|
119
|
+
model: body.model,
|
|
120
|
+
choices: [{
|
|
121
|
+
index: 0,
|
|
122
|
+
message: { role: 'assistant', content: text },
|
|
123
|
+
finish_reason: body.stop_reason === 'end_turn' ? 'stop' : body.stop_reason === 'max_tokens' ? 'length' : 'stop',
|
|
124
|
+
}],
|
|
125
|
+
usage: {
|
|
126
|
+
prompt_tokens: usage?.input_tokens ?? 0,
|
|
127
|
+
completion_tokens: usage?.output_tokens ?? 0,
|
|
128
|
+
total_tokens: (usage?.input_tokens ?? 0) + (usage?.output_tokens ?? 0),
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Translate Anthropic SSE stream → OpenAI SSE stream.
|
|
134
|
+
*/
|
|
135
|
+
function translateStreamChunk(line) {
|
|
136
|
+
if (!line.startsWith('data: '))
|
|
137
|
+
return null;
|
|
138
|
+
const json = line.slice(6).trim();
|
|
139
|
+
if (json === '[DONE]')
|
|
140
|
+
return 'data: [DONE]\n\n';
|
|
141
|
+
try {
|
|
142
|
+
const event = JSON.parse(json);
|
|
143
|
+
if (event.type === 'content_block_delta') {
|
|
144
|
+
const delta = event.delta;
|
|
145
|
+
if (delta?.type === 'text_delta' && delta.text) {
|
|
146
|
+
return `data: ${JSON.stringify({
|
|
147
|
+
id: 'chatcmpl-dario',
|
|
148
|
+
object: 'chat.completion.chunk',
|
|
149
|
+
created: Math.floor(Date.now() / 1000),
|
|
150
|
+
model: 'claude',
|
|
151
|
+
choices: [{ index: 0, delta: { content: delta.text }, finish_reason: null }],
|
|
152
|
+
})}\n\n`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (event.type === 'message_stop') {
|
|
156
|
+
return `data: ${JSON.stringify({
|
|
157
|
+
id: 'chatcmpl-dario',
|
|
158
|
+
object: 'chat.completion.chunk',
|
|
159
|
+
created: Math.floor(Date.now() / 1000),
|
|
160
|
+
model: 'claude',
|
|
161
|
+
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
|
162
|
+
})}\n\ndata: [DONE]\n\n`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch { /* skip unparseable */ }
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* OpenAI-compatible models list.
|
|
170
|
+
*/
|
|
171
|
+
function openaiModelsList() {
|
|
172
|
+
const models = ['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-haiku-4-5'];
|
|
173
|
+
return {
|
|
174
|
+
object: 'list',
|
|
175
|
+
data: models.map(id => ({
|
|
176
|
+
id,
|
|
177
|
+
object: 'model',
|
|
178
|
+
created: 1700000000,
|
|
179
|
+
owned_by: 'anthropic',
|
|
180
|
+
})),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
export function sanitizeError(err) {
|
|
59
184
|
const msg = err instanceof Error ? err.message : String(err);
|
|
60
|
-
// Never leak tokens in error messages
|
|
61
|
-
return msg
|
|
185
|
+
// Never leak tokens, JWTs, or bearer values in error messages
|
|
186
|
+
return msg
|
|
187
|
+
.replace(/sk-ant-[a-zA-Z0-9_-]+/g, '[REDACTED]')
|
|
188
|
+
.replace(/eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g, '[REDACTED_JWT]')
|
|
189
|
+
.replace(/Bearer\s+[a-zA-Z0-9_-]+/gi, 'Bearer [REDACTED]');
|
|
62
190
|
}
|
|
63
191
|
/**
|
|
64
192
|
* CLI Backend: route requests through `claude --print` instead of direct API.
|
|
@@ -164,11 +292,28 @@ export async function startProxy(opts = {}) {
|
|
|
164
292
|
const useCli = opts.cliBackend ?? false;
|
|
165
293
|
let requestCount = 0;
|
|
166
294
|
let tokenCostEstimate = 0;
|
|
295
|
+
// Optional proxy authentication
|
|
296
|
+
const apiKey = process.env.DARIO_API_KEY;
|
|
297
|
+
const corsOrigin = `http://localhost:${port}`;
|
|
298
|
+
function checkAuth(req) {
|
|
299
|
+
if (!apiKey)
|
|
300
|
+
return true; // no key set = open access
|
|
301
|
+
const provided = req.headers['x-api-key']
|
|
302
|
+
|| req.headers.authorization?.replace(/^Bearer\s+/i, '');
|
|
303
|
+
if (!provided)
|
|
304
|
+
return false;
|
|
305
|
+
try {
|
|
306
|
+
return timingSafeEqual(Buffer.from(provided), Buffer.from(apiKey));
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
167
312
|
const server = createServer(async (req, res) => {
|
|
168
313
|
// CORS preflight
|
|
169
314
|
if (req.method === 'OPTIONS') {
|
|
170
315
|
res.writeHead(204, {
|
|
171
|
-
'Access-Control-Allow-Origin':
|
|
316
|
+
'Access-Control-Allow-Origin': corsOrigin,
|
|
172
317
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
173
318
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization, x-api-key, anthropic-version, anthropic-beta',
|
|
174
319
|
'Access-Control-Max-Age': '86400',
|
|
@@ -190,6 +335,12 @@ export async function startProxy(opts = {}) {
|
|
|
190
335
|
}));
|
|
191
336
|
return;
|
|
192
337
|
}
|
|
338
|
+
// Auth gate — everything below health requires auth when DARIO_API_KEY is set
|
|
339
|
+
if (!checkAuth(req)) {
|
|
340
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
341
|
+
res.end(JSON.stringify({ error: 'Unauthorized', message: 'Invalid or missing API key' }));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
193
344
|
// Status endpoint
|
|
194
345
|
if (urlPath === '/status') {
|
|
195
346
|
const s = await getStatus();
|
|
@@ -197,20 +348,28 @@ export async function startProxy(opts = {}) {
|
|
|
197
348
|
res.end(JSON.stringify(s));
|
|
198
349
|
return;
|
|
199
350
|
}
|
|
351
|
+
// OpenAI-compatible models list
|
|
352
|
+
if (urlPath === '/v1/models' && req.method === 'GET') {
|
|
353
|
+
requestCount++;
|
|
354
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': corsOrigin });
|
|
355
|
+
res.end(JSON.stringify(openaiModelsList()));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
// Detect OpenAI-format requests
|
|
359
|
+
const isOpenAI = urlPath === '/v1/chat/completions';
|
|
200
360
|
// Allowlisted API paths — only these are proxied (prevents SSRF)
|
|
201
361
|
const allowedPaths = {
|
|
202
362
|
'/v1/messages': `${ANTHROPIC_API}/v1/messages`,
|
|
203
|
-
'/v1/models': `${ANTHROPIC_API}/v1/models`,
|
|
204
363
|
'/v1/complete': `${ANTHROPIC_API}/v1/complete`,
|
|
205
364
|
};
|
|
206
|
-
const targetBase = allowedPaths[urlPath];
|
|
365
|
+
const targetBase = isOpenAI ? `${ANTHROPIC_API}/v1/messages` : allowedPaths[urlPath];
|
|
207
366
|
if (!targetBase) {
|
|
208
367
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
209
368
|
res.end(JSON.stringify({ error: 'Forbidden', message: 'Path not allowed' }));
|
|
210
369
|
return;
|
|
211
370
|
}
|
|
212
|
-
// Only allow POST (Messages API) and GET (models)
|
|
213
|
-
if (req.method !== 'POST'
|
|
371
|
+
// Only allow POST (Messages/Chat API) and GET (models)
|
|
372
|
+
if (req.method !== 'POST') {
|
|
214
373
|
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
215
374
|
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
216
375
|
return;
|
|
@@ -238,14 +397,23 @@ export async function startProxy(opts = {}) {
|
|
|
238
397
|
requestCount++;
|
|
239
398
|
res.writeHead(cliResult.status, {
|
|
240
399
|
'Content-Type': cliResult.contentType,
|
|
241
|
-
'Access-Control-Allow-Origin':
|
|
400
|
+
'Access-Control-Allow-Origin': corsOrigin,
|
|
242
401
|
});
|
|
243
402
|
res.end(cliResult.body);
|
|
244
403
|
return;
|
|
245
404
|
}
|
|
246
|
-
//
|
|
405
|
+
// Translate OpenAI → Anthropic format if needed
|
|
247
406
|
let finalBody = body.length > 0 ? body : undefined;
|
|
248
|
-
if (
|
|
407
|
+
if (isOpenAI && body.length > 0) {
|
|
408
|
+
try {
|
|
409
|
+
const parsed = JSON.parse(body.toString());
|
|
410
|
+
const translated = openaiToAnthropic(parsed, modelOverride);
|
|
411
|
+
finalBody = Buffer.from(JSON.stringify(translated));
|
|
412
|
+
}
|
|
413
|
+
catch { /* not JSON, send as-is */ }
|
|
414
|
+
}
|
|
415
|
+
else if (modelOverride && body.length > 0) {
|
|
416
|
+
// Override model in request body if --model flag was set
|
|
249
417
|
try {
|
|
250
418
|
const parsed = JSON.parse(body.toString());
|
|
251
419
|
parsed.model = modelOverride;
|
|
@@ -308,7 +476,7 @@ export async function startProxy(opts = {}) {
|
|
|
308
476
|
// Forward response headers
|
|
309
477
|
const responseHeaders = {
|
|
310
478
|
'Content-Type': contentType || 'application/json',
|
|
311
|
-
'Access-Control-Allow-Origin':
|
|
479
|
+
'Access-Control-Allow-Origin': corsOrigin,
|
|
312
480
|
};
|
|
313
481
|
// Forward rate limit headers (including unified subscription headers)
|
|
314
482
|
for (const [key, value] of upstream.headers.entries()) {
|
|
@@ -321,12 +489,33 @@ export async function startProxy(opts = {}) {
|
|
|
321
489
|
if (isStream && upstream.body) {
|
|
322
490
|
// Stream SSE chunks through
|
|
323
491
|
const reader = upstream.body.getReader();
|
|
492
|
+
const decoder = new TextDecoder();
|
|
324
493
|
try {
|
|
494
|
+
let buffer = '';
|
|
325
495
|
while (true) {
|
|
326
496
|
const { done, value } = await reader.read();
|
|
327
497
|
if (done)
|
|
328
498
|
break;
|
|
329
|
-
|
|
499
|
+
if (isOpenAI) {
|
|
500
|
+
// Translate Anthropic SSE → OpenAI SSE
|
|
501
|
+
buffer += decoder.decode(value, { stream: true });
|
|
502
|
+
const lines = buffer.split('\n');
|
|
503
|
+
buffer = lines.pop() ?? '';
|
|
504
|
+
for (const line of lines) {
|
|
505
|
+
const translated = translateStreamChunk(line);
|
|
506
|
+
if (translated)
|
|
507
|
+
res.write(translated);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
res.write(value);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// Flush remaining buffer
|
|
515
|
+
if (isOpenAI && buffer.trim()) {
|
|
516
|
+
const translated = translateStreamChunk(buffer);
|
|
517
|
+
if (translated)
|
|
518
|
+
res.write(translated);
|
|
330
519
|
}
|
|
331
520
|
}
|
|
332
521
|
catch (err) {
|
|
@@ -338,7 +527,19 @@ export async function startProxy(opts = {}) {
|
|
|
338
527
|
else {
|
|
339
528
|
// Buffer and forward
|
|
340
529
|
const responseBody = await upstream.text();
|
|
341
|
-
|
|
530
|
+
if (isOpenAI && upstream.status >= 200 && upstream.status < 300) {
|
|
531
|
+
// Translate Anthropic response → OpenAI format
|
|
532
|
+
try {
|
|
533
|
+
const parsed = JSON.parse(responseBody);
|
|
534
|
+
res.end(JSON.stringify(anthropicToOpenai(parsed)));
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
res.end(responseBody);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
res.end(responseBody);
|
|
542
|
+
}
|
|
342
543
|
// Quick token estimate for logging
|
|
343
544
|
if (verbose && responseBody) {
|
|
344
545
|
try {
|