@hienlh/ppm 0.9.30 → 0.9.31
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/CHANGELOG.md +4 -7
- package/dist/web/assets/{browser-tab-D0o6oSlt.js → browser-tab-DmDrxklj.js} +1 -1
- package/dist/web/assets/{chat-tab-Boo_H1k9.js → chat-tab-CMwOy57v.js} +1 -1
- package/dist/web/assets/{code-editor-DayGetAZ.js → code-editor-jsL0PK8A.js} +1 -1
- package/dist/web/assets/{database-viewer-CaxAp1qK.js → database-viewer-CBo5yPV-.js} +1 -1
- package/dist/web/assets/{diff-viewer-BvEXe_B4.js → diff-viewer-Dk-plEOm.js} +1 -1
- package/dist/web/assets/{extension-webview-6XProGzB.js → extension-webview-B0tE14-C.js} +1 -1
- package/dist/web/assets/{git-graph-CvgIIt2x.js → git-graph-BsYuai5I.js} +1 -1
- package/dist/web/assets/{index-DocPzjV6.js → index-DMiaze7L.js} +13 -6
- package/dist/web/assets/keybindings-store-B01E0k20.js +1 -0
- package/dist/web/assets/{markdown-renderer-UCGYJpI-.js → markdown-renderer-lUfZhpU0.js} +1 -1
- package/dist/web/assets/{postgres-viewer-TV6kyo6B.js → postgres-viewer-sZclUhuS.js} +1 -1
- package/dist/web/assets/{settings-tab-EziN5Pco.js → settings-tab-CvbLGbR6.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-D7LPvSkU.js → sqlite-viewer-BAjul3Ct.js} +1 -1
- package/dist/web/assets/{terminal-tab-C7Hdv1nq.js → terminal-tab-Ds9ymO7D.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-CI4vTUsh.js → use-monaco-theme-D9bFLaXR.js} +1 -1
- package/dist/web/index.html +1 -1
- package/dist/web/sw.js +1 -1
- package/docs/streaming-input-guide.md +267 -0
- package/package.json +1 -1
- package/snapshot-state.md +1526 -0
- package/src/server/index.ts +1 -1
- package/src/server/routes/proxy.ts +0 -15
- package/src/server/routes/settings.ts +0 -2
- package/src/services/proxy-sdk-bridge.ts +21 -63
- package/src/services/proxy.service.ts +0 -33
- package/src/services/supervisor.ts +10 -0
- package/src/web/components/settings/proxy-settings-section.tsx +37 -50
- package/src/web/components/settings/proxy-test-section.tsx +25 -48
- package/src/web/lib/api-settings.ts +0 -2
- package/test-session-ops.mjs +444 -0
- package/test-tokens.mjs +212 -0
- package/.claude.bak/agent-memory/tester/MEMORY.md +0 -3
- package/.claude.bak/agent-memory/tester/project-ppm-test-conventions.md +0 -32
- package/dist/web/assets/keybindings-store-2KURy8S3.js +0 -1
- package/src/services/proxy-openai-bridge.ts +0 -241
package/src/server/index.ts
CHANGED
|
@@ -387,8 +387,8 @@ if (process.argv.includes("__serve__")) {
|
|
|
387
387
|
wf(statusFile, JSON.stringify(status));
|
|
388
388
|
if (status.shareUrl) {
|
|
389
389
|
const { tunnelService } = await import("../services/tunnel.service.ts");
|
|
390
|
-
tunnelService.setExternalUrl(status.shareUrl);
|
|
391
390
|
if (status.tunnelPid) tunnelService.setExternalPid(status.tunnelPid);
|
|
391
|
+
tunnelService.setExternalUrl(status.shareUrl);
|
|
392
392
|
}
|
|
393
393
|
} catch { /* status.json missing or no shareUrl — normal */ }
|
|
394
394
|
|
|
@@ -57,21 +57,6 @@ proxyRoutes.post("/v1/messages", async (c) => {
|
|
|
57
57
|
return proxyService.forward("/v1/messages", "POST", headers, body);
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
/** POST /proxy/v1/chat/completions — OpenAI-compatible chat completions proxy */
|
|
61
|
-
proxyRoutes.post("/v1/chat/completions", async (c) => {
|
|
62
|
-
if (!proxyService.isEnabled()) {
|
|
63
|
-
return c.json({ error: { message: "Proxy is disabled", type: "server_error" } }, 503);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const authHeader = c.req.header("authorization") || c.req.header("x-api-key");
|
|
67
|
-
if (!validateProxyAuth(authHeader)) {
|
|
68
|
-
return c.json({ error: { message: "Invalid proxy auth key", type: "authentication_error" } }, 401);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const body = await c.req.text();
|
|
72
|
-
return proxyService.forwardOpenAi(body);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
60
|
/** POST /proxy/v1/messages/count_tokens — token counting proxy */
|
|
76
61
|
proxyRoutes.post("/v1/messages/count_tokens", async (c) => {
|
|
77
62
|
if (!proxyService.isEnabled()) {
|
|
@@ -292,10 +292,8 @@ async function buildProxyResponse() {
|
|
|
292
292
|
authKey: proxyService.getAuthKey() ?? null,
|
|
293
293
|
requestCount: proxyService.getRequestCount(),
|
|
294
294
|
localEndpoint: `${localOrigin}/proxy/v1/messages`,
|
|
295
|
-
localOpenAiEndpoint: `${localOrigin}/proxy/v1/chat/completions`,
|
|
296
295
|
tunnelUrl: tunnelUrl ?? null,
|
|
297
296
|
proxyEndpoint: tunnelUrl ? `${tunnelUrl}/proxy/v1/messages` : null,
|
|
298
|
-
openAiEndpoint: tunnelUrl ? `${tunnelUrl}/proxy/v1/chat/completions` : null,
|
|
299
297
|
};
|
|
300
298
|
}
|
|
301
299
|
|
|
@@ -172,82 +172,40 @@ async function handleStreaming(
|
|
|
172
172
|
|
|
173
173
|
// Track tool_use block indices to filter them out
|
|
174
174
|
const skipBlockIndices = new Set<number>();
|
|
175
|
-
let streamed = false; // track if we sent any SSE events
|
|
176
|
-
let lastContentLen = 0; // for partial message diff
|
|
177
175
|
|
|
178
176
|
try {
|
|
179
177
|
for await (const message of response) {
|
|
180
|
-
|
|
178
|
+
if (message.type !== "stream_event") continue;
|
|
181
179
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const eventType = event.type as string;
|
|
186
|
-
const eventIndex = event.index as number | undefined;
|
|
180
|
+
const event = (message as any).event;
|
|
181
|
+
const eventType = event.type as string;
|
|
182
|
+
const eventIndex = event.index as number | undefined;
|
|
187
183
|
|
|
188
|
-
|
|
184
|
+
// Filter tool_use content blocks — external tools expect text only
|
|
185
|
+
if (eventType === "content_block_start") {
|
|
186
|
+
const block = event.content_block;
|
|
187
|
+
if (block?.type === "tool_use") {
|
|
189
188
|
if (eventIndex !== undefined) skipBlockIndices.add(eventIndex);
|
|
190
189
|
continue;
|
|
191
190
|
}
|
|
192
|
-
if (eventIndex !== undefined && skipBlockIndices.has(eventIndex)) continue;
|
|
193
|
-
|
|
194
|
-
if (eventType === "message_delta") {
|
|
195
|
-
const patched = {
|
|
196
|
-
...event,
|
|
197
|
-
delta: { ...(event.delta || {}), stop_reason: "end_turn" },
|
|
198
|
-
usage: event.usage || { output_tokens: 0 },
|
|
199
|
-
};
|
|
200
|
-
controller.enqueue(encoder.encode(`event: ${eventType}\ndata: ${JSON.stringify(patched)}\n\n`));
|
|
201
|
-
} else {
|
|
202
|
-
controller.enqueue(encoder.encode(`event: ${eventType}\ndata: ${JSON.stringify(event)}\n\n`));
|
|
203
|
-
}
|
|
204
|
-
streamed = true;
|
|
205
|
-
continue;
|
|
206
191
|
}
|
|
207
192
|
|
|
208
|
-
//
|
|
209
|
-
if (
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const msgStart = { type: "message_start", message: { id: `msg_${Date.now()}`, type: "message", role: "assistant", model: body.model, content: [], stop_reason: null, usage: { input_tokens: 0, output_tokens: 0 } } };
|
|
220
|
-
controller.enqueue(encoder.encode(`event: message_start\ndata: ${JSON.stringify(msgStart)}\n\n`));
|
|
221
|
-
controller.enqueue(encoder.encode(`event: content_block_start\ndata: ${JSON.stringify({ type: "content_block_start", index: 0, content_block: { type: "text", text: "" } })}\n\n`));
|
|
222
|
-
}
|
|
223
|
-
controller.enqueue(encoder.encode(`event: content_block_delta\ndata: ${JSON.stringify({ type: "content_block_delta", index: 0, delta: { type: "text_delta", text: delta } })}\n\n`));
|
|
224
|
-
lastContentLen = fullText.length;
|
|
225
|
-
}
|
|
193
|
+
// Skip deltas and stops for tool_use blocks
|
|
194
|
+
if (eventIndex !== undefined && skipBlockIndices.has(eventIndex)) continue;
|
|
195
|
+
|
|
196
|
+
// Override message_delta to always show end_turn
|
|
197
|
+
if (eventType === "message_delta") {
|
|
198
|
+
const patched = {
|
|
199
|
+
...event,
|
|
200
|
+
delta: { ...(event.delta || {}), stop_reason: "end_turn" },
|
|
201
|
+
usage: event.usage || { output_tokens: 0 },
|
|
202
|
+
};
|
|
203
|
+
controller.enqueue(encoder.encode(`event: ${eventType}\ndata: ${JSON.stringify(patched)}\n\n`));
|
|
226
204
|
continue;
|
|
227
205
|
}
|
|
228
206
|
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
const content = (message as any).message?.content ?? [];
|
|
232
|
-
let fullText = "";
|
|
233
|
-
for (const block of content) {
|
|
234
|
-
if (block.type === "text") fullText += block.text ?? "";
|
|
235
|
-
}
|
|
236
|
-
if (fullText) {
|
|
237
|
-
const msgStart = { type: "message_start", message: { id: `msg_${Date.now()}`, type: "message", role: "assistant", model: body.model, content: [], stop_reason: null, usage: { input_tokens: 0, output_tokens: 0 } } };
|
|
238
|
-
controller.enqueue(encoder.encode(`event: message_start\ndata: ${JSON.stringify(msgStart)}\n\n`));
|
|
239
|
-
controller.enqueue(encoder.encode(`event: content_block_start\ndata: ${JSON.stringify({ type: "content_block_start", index: 0, content_block: { type: "text", text: "" } })}\n\n`));
|
|
240
|
-
controller.enqueue(encoder.encode(`event: content_block_delta\ndata: ${JSON.stringify({ type: "content_block_delta", index: 0, delta: { type: "text_delta", text: fullText } })}\n\n`));
|
|
241
|
-
lastContentLen = fullText.length;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Close SSE envelope if we used partial/assistant fallback
|
|
247
|
-
if (!streamed && lastContentLen > 0) {
|
|
248
|
-
controller.enqueue(encoder.encode(`event: content_block_stop\ndata: ${JSON.stringify({ type: "content_block_stop", index: 0 })}\n\n`));
|
|
249
|
-
controller.enqueue(encoder.encode(`event: message_delta\ndata: ${JSON.stringify({ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 0 } })}\n\n`));
|
|
250
|
-
controller.enqueue(encoder.encode(`event: message_stop\ndata: ${JSON.stringify({ type: "message_stop" })}\n\n`));
|
|
207
|
+
// Forward all other events (message_start, text deltas, content_block_start/stop, message_stop)
|
|
208
|
+
controller.enqueue(encoder.encode(`event: ${eventType}\ndata: ${JSON.stringify(event)}\n\n`));
|
|
251
209
|
}
|
|
252
210
|
|
|
253
211
|
accountSelector.onSuccess(account.id);
|
|
@@ -2,7 +2,6 @@ import { getConfigValue, setConfigValue } from "./db.service.ts";
|
|
|
2
2
|
import { accountSelector } from "./account-selector.service.ts";
|
|
3
3
|
import { accountService } from "./account.service.ts";
|
|
4
4
|
import { forwardViaSdk } from "./proxy-sdk-bridge.ts";
|
|
5
|
-
import { forwardOpenAiViaSdk } from "./proxy-openai-bridge.ts";
|
|
6
5
|
import { randomBytes } from "node:crypto";
|
|
7
6
|
|
|
8
7
|
const PROXY_ENABLED_KEY = "proxy_enabled";
|
|
@@ -87,38 +86,6 @@ class ProxyService {
|
|
|
87
86
|
return this.forwardDirect(path, method, headers, body, token, account);
|
|
88
87
|
}
|
|
89
88
|
|
|
90
|
-
/**
|
|
91
|
-
* Forward an OpenAI-format chat completions request via SDK query().
|
|
92
|
-
* Always uses SDK bridge (works for both OAuth and API key accounts).
|
|
93
|
-
*/
|
|
94
|
-
async forwardOpenAi(body: string): Promise<Response> {
|
|
95
|
-
const account = accountSelector.next();
|
|
96
|
-
if (!account) {
|
|
97
|
-
return new Response(
|
|
98
|
-
JSON.stringify({ error: { message: "No active accounts available", type: "server_error" } }),
|
|
99
|
-
{ status: 401, headers: { "Content-Type": "application/json" } },
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
let token = account.accessToken;
|
|
104
|
-
if (token.startsWith("sk-ant-oat")) {
|
|
105
|
-
const fresh = await accountService.ensureFreshToken(account.id);
|
|
106
|
-
if (fresh) token = fresh.accessToken;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
const parsed = JSON.parse(body);
|
|
111
|
-
this.requestCount++;
|
|
112
|
-
return await forwardOpenAiViaSdk(parsed, { id: account.id, email: account.email, accessToken: token });
|
|
113
|
-
} catch (e) {
|
|
114
|
-
console.error(`[proxy] OpenAI bridge error:`, (e as Error).message);
|
|
115
|
-
return new Response(
|
|
116
|
-
JSON.stringify({ error: { message: (e as Error).message, type: "server_error" } }),
|
|
117
|
-
{ status: 502, headers: { "Content-Type": "application/json" } },
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
89
|
/** Direct HTTP forward for API key accounts */
|
|
123
90
|
private async forwardDirect(
|
|
124
91
|
path: string,
|
|
@@ -9,6 +9,7 @@ import { resolve } from "node:path";
|
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import {
|
|
11
11
|
readFileSync, writeFileSync, existsSync, mkdirSync, openSync, appendFileSync,
|
|
12
|
+
unlinkSync,
|
|
12
13
|
} from "node:fs";
|
|
13
14
|
import { isCompiledBinary } from "./autostart-generator.ts";
|
|
14
15
|
|
|
@@ -30,6 +31,7 @@ const PPM_DIR = resolve(process.env.PPM_HOME || resolve(homedir(), ".ppm"));
|
|
|
30
31
|
const STATUS_FILE = resolve(PPM_DIR, "status.json");
|
|
31
32
|
const PID_FILE = resolve(PPM_DIR, "ppm.pid");
|
|
32
33
|
const LOG_FILE = resolve(PPM_DIR, "ppm.log");
|
|
34
|
+
const RESTARTING_FLAG = resolve(PPM_DIR, ".restarting");
|
|
33
35
|
|
|
34
36
|
// ─── State ─────────────────────────────────────────────────────────────
|
|
35
37
|
let serverChild: Subprocess | null = null;
|
|
@@ -417,6 +419,9 @@ async function selfReplace(): Promise<{ success: boolean; error?: string }> {
|
|
|
417
419
|
supervisorState = "upgrading";
|
|
418
420
|
updateStatus({ state: "upgrading" });
|
|
419
421
|
|
|
422
|
+
// Set restarting flag so server child's stopTunnel() skips killing the tunnel
|
|
423
|
+
try { writeFileSync(RESTARTING_FLAG, ""); } catch {}
|
|
424
|
+
|
|
420
425
|
// Kill server child to free the port; keep tunnel alive for domain continuity
|
|
421
426
|
log("INFO", "Stopping server before spawning new supervisor (tunnel kept alive)");
|
|
422
427
|
if (serverChild) { try { serverChild.kill(); } catch {} serverChild = null; }
|
|
@@ -456,6 +461,7 @@ async function selfReplace(): Promise<{ success: boolean; error?: string }> {
|
|
|
456
461
|
// Timeout — new supervisor didn't start, restore old supervisor
|
|
457
462
|
log("ERROR", "Self-replace timeout: new supervisor did not start");
|
|
458
463
|
try { child.kill(); } catch {}
|
|
464
|
+
try { unlinkSync(RESTARTING_FLAG); } catch {}
|
|
459
465
|
shuttingDown = false;
|
|
460
466
|
notifyStateChange("upgrading", "running", "upgrade_failed");
|
|
461
467
|
supervisorState = "running";
|
|
@@ -463,6 +469,7 @@ async function selfReplace(): Promise<{ success: boolean; error?: string }> {
|
|
|
463
469
|
return { success: false, error: "New supervisor failed to start within 30s" };
|
|
464
470
|
} catch (e) {
|
|
465
471
|
log("ERROR", `Self-replace error: ${e}`);
|
|
472
|
+
try { unlinkSync(RESTARTING_FLAG); } catch {}
|
|
466
473
|
shuttingDown = false;
|
|
467
474
|
notifyStateChange("upgrading", "running", "upgrade_failed");
|
|
468
475
|
supervisorState = "running";
|
|
@@ -631,6 +638,9 @@ export async function runSupervisor(opts: {
|
|
|
631
638
|
}) {
|
|
632
639
|
if (!existsSync(PPM_DIR)) mkdirSync(PPM_DIR, { recursive: true });
|
|
633
640
|
|
|
641
|
+
// Clean up restarting flag from previous upgrade/restart
|
|
642
|
+
try { unlinkSync(RESTARTING_FLAG); } catch {}
|
|
643
|
+
|
|
634
644
|
// Save original argv for self-replace
|
|
635
645
|
originalArgv = [...process.argv];
|
|
636
646
|
|
|
@@ -134,43 +134,43 @@ export function ProxySettingsSection() {
|
|
|
134
134
|
<ProxyTestButton authKey={settings.authKey!} baseUrl={window.location.origin} />
|
|
135
135
|
</div>
|
|
136
136
|
|
|
137
|
-
{/*
|
|
137
|
+
{/* Local endpoint */}
|
|
138
138
|
<div className="space-y-1">
|
|
139
|
-
<Label className="text-[10px] text-muted-foreground">
|
|
139
|
+
<Label className="text-[10px] text-muted-foreground">Local Endpoint</Label>
|
|
140
140
|
<div className="flex gap-1.5 items-center">
|
|
141
141
|
<code className="text-[10px] font-mono bg-muted px-1.5 py-0.5 rounded flex-1 truncate">
|
|
142
|
-
{
|
|
142
|
+
{localEndpoint}
|
|
143
143
|
</code>
|
|
144
144
|
<Button
|
|
145
145
|
variant="ghost"
|
|
146
146
|
size="sm"
|
|
147
147
|
className="h-6 px-1.5 cursor-pointer shrink-0"
|
|
148
|
-
onClick={() => copyToClipboard(
|
|
148
|
+
onClick={() => copyToClipboard(localEndpoint, "local")}
|
|
149
149
|
>
|
|
150
|
-
{copied === "
|
|
150
|
+
{copied === "local" ? "Copied!" : <Copy className="size-3" />}
|
|
151
151
|
</Button>
|
|
152
152
|
</div>
|
|
153
153
|
</div>
|
|
154
154
|
|
|
155
|
-
{/*
|
|
156
|
-
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
<
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
</
|
|
155
|
+
{/* Tunnel endpoint */}
|
|
156
|
+
{hasTunnel && settings.proxyEndpoint && (
|
|
157
|
+
<div className="space-y-1">
|
|
158
|
+
<Label className="text-[10px] text-muted-foreground">Public Endpoint (Tunnel)</Label>
|
|
159
|
+
<div className="flex gap-1.5 items-center">
|
|
160
|
+
<code className="text-[10px] font-mono bg-muted px-1.5 py-0.5 rounded flex-1 truncate">
|
|
161
|
+
{settings.proxyEndpoint}
|
|
162
|
+
</code>
|
|
163
|
+
<Button
|
|
164
|
+
variant="ghost"
|
|
165
|
+
size="sm"
|
|
166
|
+
className="h-6 px-1.5 cursor-pointer shrink-0"
|
|
167
|
+
onClick={() => copyToClipboard(settings.proxyEndpoint!, "tunnel")}
|
|
168
|
+
>
|
|
169
|
+
{copied === "tunnel" ? "Copied!" : <Copy className="size-3" />}
|
|
170
|
+
</Button>
|
|
171
|
+
</div>
|
|
172
172
|
</div>
|
|
173
|
-
|
|
173
|
+
)}
|
|
174
174
|
|
|
175
175
|
{!hasTunnel && (
|
|
176
176
|
<p className="text-[10px] text-muted-foreground">
|
|
@@ -178,13 +178,21 @@ export function ProxySettingsSection() {
|
|
|
178
178
|
</p>
|
|
179
179
|
)}
|
|
180
180
|
|
|
181
|
-
{/* Usage
|
|
181
|
+
{/* Usage example */}
|
|
182
182
|
<div className="space-y-1 pt-1">
|
|
183
|
-
<Label className="text-[10px] text-muted-foreground">
|
|
183
|
+
<Label className="text-[10px] text-muted-foreground">Usage Example</Label>
|
|
184
184
|
<div className="relative">
|
|
185
185
|
<pre className="text-[9px] font-mono bg-muted p-2 rounded overflow-x-auto whitespace-pre">
|
|
186
|
-
{
|
|
187
|
-
|
|
186
|
+
{`# Set as base URL in your tool
|
|
187
|
+
ANTHROPIC_BASE_URL=${hasTunnel ? settings.tunnelUrl + "/proxy" : localBaseUrl + "/proxy"}
|
|
188
|
+
ANTHROPIC_API_KEY=${settings.authKey}
|
|
189
|
+
|
|
190
|
+
# Or use curl
|
|
191
|
+
curl ${hasTunnel ? settings.proxyEndpoint : localEndpoint} \\
|
|
192
|
+
-H "x-api-key: ${settings.authKey}" \\
|
|
193
|
+
-H "content-type: application/json" \\
|
|
194
|
+
-H "anthropic-version: 2023-06-01" \\
|
|
195
|
+
-d '{"model":"claude-sonnet-4-6","max_tokens":1024,"messages":[{"role":"user","content":"Hello"}]}'`}
|
|
188
196
|
</pre>
|
|
189
197
|
<Button
|
|
190
198
|
variant="ghost"
|
|
@@ -192,31 +200,10 @@ ANTHROPIC_API_KEY=${settings.authKey}`}
|
|
|
192
200
|
className="absolute top-1 right-1 h-5 px-1 cursor-pointer"
|
|
193
201
|
onClick={() => copyToClipboard(
|
|
194
202
|
`ANTHROPIC_BASE_URL=${hasTunnel ? settings.tunnelUrl + "/proxy" : localBaseUrl + "/proxy"}\nANTHROPIC_API_KEY=${settings.authKey}`,
|
|
195
|
-
"
|
|
196
|
-
)}
|
|
197
|
-
>
|
|
198
|
-
{copied === "anthropic-env" ? "Copied!" : <Copy className="size-2.5" />}
|
|
199
|
-
</Button>
|
|
200
|
-
</div>
|
|
201
|
-
</div>
|
|
202
|
-
|
|
203
|
-
<div className="space-y-1">
|
|
204
|
-
<Label className="text-[10px] text-muted-foreground">OpenAI Format</Label>
|
|
205
|
-
<div className="relative">
|
|
206
|
-
<pre className="text-[9px] font-mono bg-muted p-2 rounded overflow-x-auto whitespace-pre">
|
|
207
|
-
{`OPENAI_BASE_URL=${hasTunnel ? settings.tunnelUrl + "/proxy/v1" : localBaseUrl + "/proxy/v1"}
|
|
208
|
-
OPENAI_API_KEY=${settings.authKey}`}
|
|
209
|
-
</pre>
|
|
210
|
-
<Button
|
|
211
|
-
variant="ghost"
|
|
212
|
-
size="sm"
|
|
213
|
-
className="absolute top-1 right-1 h-5 px-1 cursor-pointer"
|
|
214
|
-
onClick={() => copyToClipboard(
|
|
215
|
-
`OPENAI_BASE_URL=${hasTunnel ? settings.tunnelUrl + "/proxy/v1" : localBaseUrl + "/proxy/v1"}\nOPENAI_API_KEY=${settings.authKey}`,
|
|
216
|
-
"openai-env",
|
|
203
|
+
"example",
|
|
217
204
|
)}
|
|
218
205
|
>
|
|
219
|
-
{copied === "
|
|
206
|
+
{copied === "example" ? "Copied!" : <Copy className="size-2.5" />}
|
|
220
207
|
</Button>
|
|
221
208
|
</div>
|
|
222
209
|
</div>
|
|
@@ -45,7 +45,6 @@ function ProxyTestForm({ authKey, baseUrl }: ProxyTestDialogProps) {
|
|
|
45
45
|
const [format, setFormat] = useState<EndpointFormat>("anthropic");
|
|
46
46
|
const [message, setMessage] = useState(DEFAULT_MESSAGE);
|
|
47
47
|
const [model, setModel] = useState(DEFAULT_MODEL);
|
|
48
|
-
const [streaming, setStreaming] = useState(true);
|
|
49
48
|
const [testing, setTesting] = useState(false);
|
|
50
49
|
const [output, setOutput] = useState<string | null>(null);
|
|
51
50
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -66,6 +65,9 @@ function ProxyTestForm({ authKey, baseUrl }: ProxyTestDialogProps) {
|
|
|
66
65
|
setElapsed(null);
|
|
67
66
|
};
|
|
68
67
|
|
|
68
|
+
// Both formats hit the same proxy endpoint — only request/response shape differs
|
|
69
|
+
const endpoint = `${baseUrl}/proxy/v1/messages`;
|
|
70
|
+
|
|
69
71
|
const runTest = async () => {
|
|
70
72
|
setTesting(true);
|
|
71
73
|
setOutput(null);
|
|
@@ -73,19 +75,21 @@ function ProxyTestForm({ authKey, baseUrl }: ProxyTestDialogProps) {
|
|
|
73
75
|
setElapsed(null);
|
|
74
76
|
const start = Date.now();
|
|
75
77
|
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
:
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
const body = JSON.stringify({
|
|
79
|
+
model,
|
|
80
|
+
max_tokens: 256,
|
|
81
|
+
stream: true,
|
|
82
|
+
messages: [{ role: "user", content: message }],
|
|
83
|
+
});
|
|
82
84
|
|
|
83
85
|
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
84
|
-
if (
|
|
85
|
-
headers["Authorization"] = `Bearer ${authKey}`;
|
|
86
|
-
} else {
|
|
86
|
+
if (format === "anthropic") {
|
|
87
87
|
headers["x-api-key"] = authKey;
|
|
88
88
|
headers["anthropic-version"] = "2023-06-01";
|
|
89
|
+
} else {
|
|
90
|
+
// OpenAI-style: use Authorization Bearer
|
|
91
|
+
headers["Authorization"] = `Bearer ${authKey}`;
|
|
92
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
try {
|
|
@@ -99,25 +103,18 @@ function ProxyTestForm({ authKey, baseUrl }: ProxyTestDialogProps) {
|
|
|
99
103
|
return;
|
|
100
104
|
}
|
|
101
105
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
let raw = "";
|
|
113
|
-
while (true) {
|
|
114
|
-
const { done, value } = await reader.read();
|
|
115
|
-
if (done) break;
|
|
116
|
-
raw += decoder.decode(value, { stream: true });
|
|
117
|
-
setOutput(raw);
|
|
118
|
-
}
|
|
119
|
-
setElapsed(Date.now() - start);
|
|
106
|
+
const reader = res.body?.getReader();
|
|
107
|
+
if (!reader) { setError("No response body"); setTesting(false); return; }
|
|
108
|
+
|
|
109
|
+
const decoder = new TextDecoder();
|
|
110
|
+
let raw = "";
|
|
111
|
+
while (true) {
|
|
112
|
+
const { done, value } = await reader.read();
|
|
113
|
+
if (done) break;
|
|
114
|
+
raw += decoder.decode(value, { stream: true });
|
|
115
|
+
setOutput(raw);
|
|
120
116
|
}
|
|
117
|
+
setElapsed(Date.now() - start);
|
|
121
118
|
} catch (e) {
|
|
122
119
|
setError((e as Error).message);
|
|
123
120
|
} finally {
|
|
@@ -165,26 +162,6 @@ function ProxyTestForm({ authKey, baseUrl }: ProxyTestDialogProps) {
|
|
|
165
162
|
</select>
|
|
166
163
|
</div>
|
|
167
164
|
|
|
168
|
-
{/* Streaming toggle */}
|
|
169
|
-
<div className="flex items-center justify-between">
|
|
170
|
-
<Label className="text-[11px]">Streaming</Label>
|
|
171
|
-
<div className="flex gap-1">
|
|
172
|
-
{([true, false] as const).map((s) => (
|
|
173
|
-
<button
|
|
174
|
-
key={String(s)}
|
|
175
|
-
onClick={() => setStreaming(s)}
|
|
176
|
-
className={`h-7 px-3 rounded-md text-[11px] font-medium border transition-colors cursor-pointer ${
|
|
177
|
-
streaming === s
|
|
178
|
-
? "bg-primary text-primary-foreground border-primary"
|
|
179
|
-
: "bg-muted text-muted-foreground border-transparent hover:bg-muted/80"
|
|
180
|
-
}`}
|
|
181
|
-
>
|
|
182
|
-
{s ? "Stream" : "JSON"}
|
|
183
|
-
</button>
|
|
184
|
-
))}
|
|
185
|
-
</div>
|
|
186
|
-
</div>
|
|
187
|
-
|
|
188
165
|
{/* Message + Test button */}
|
|
189
166
|
<div className="space-y-1.5">
|
|
190
167
|
<Label className="text-[11px]">Message</Label>
|
|
@@ -191,10 +191,8 @@ export interface ProxySettings {
|
|
|
191
191
|
authKey: string | null;
|
|
192
192
|
requestCount: number;
|
|
193
193
|
localEndpoint: string;
|
|
194
|
-
localOpenAiEndpoint: string;
|
|
195
194
|
tunnelUrl: string | null;
|
|
196
195
|
proxyEndpoint: string | null;
|
|
197
|
-
openAiEndpoint: string | null;
|
|
198
196
|
}
|
|
199
197
|
|
|
200
198
|
export function getProxySettings(): Promise<ProxySettings> {
|