@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +4 -7
  2. package/dist/web/assets/{browser-tab-D0o6oSlt.js → browser-tab-DmDrxklj.js} +1 -1
  3. package/dist/web/assets/{chat-tab-Boo_H1k9.js → chat-tab-CMwOy57v.js} +1 -1
  4. package/dist/web/assets/{code-editor-DayGetAZ.js → code-editor-jsL0PK8A.js} +1 -1
  5. package/dist/web/assets/{database-viewer-CaxAp1qK.js → database-viewer-CBo5yPV-.js} +1 -1
  6. package/dist/web/assets/{diff-viewer-BvEXe_B4.js → diff-viewer-Dk-plEOm.js} +1 -1
  7. package/dist/web/assets/{extension-webview-6XProGzB.js → extension-webview-B0tE14-C.js} +1 -1
  8. package/dist/web/assets/{git-graph-CvgIIt2x.js → git-graph-BsYuai5I.js} +1 -1
  9. package/dist/web/assets/{index-DocPzjV6.js → index-DMiaze7L.js} +13 -6
  10. package/dist/web/assets/keybindings-store-B01E0k20.js +1 -0
  11. package/dist/web/assets/{markdown-renderer-UCGYJpI-.js → markdown-renderer-lUfZhpU0.js} +1 -1
  12. package/dist/web/assets/{postgres-viewer-TV6kyo6B.js → postgres-viewer-sZclUhuS.js} +1 -1
  13. package/dist/web/assets/{settings-tab-EziN5Pco.js → settings-tab-CvbLGbR6.js} +1 -1
  14. package/dist/web/assets/{sqlite-viewer-D7LPvSkU.js → sqlite-viewer-BAjul3Ct.js} +1 -1
  15. package/dist/web/assets/{terminal-tab-C7Hdv1nq.js → terminal-tab-Ds9ymO7D.js} +1 -1
  16. package/dist/web/assets/{use-monaco-theme-CI4vTUsh.js → use-monaco-theme-D9bFLaXR.js} +1 -1
  17. package/dist/web/index.html +1 -1
  18. package/dist/web/sw.js +1 -1
  19. package/docs/streaming-input-guide.md +267 -0
  20. package/package.json +1 -1
  21. package/snapshot-state.md +1526 -0
  22. package/src/server/index.ts +1 -1
  23. package/src/server/routes/proxy.ts +0 -15
  24. package/src/server/routes/settings.ts +0 -2
  25. package/src/services/proxy-sdk-bridge.ts +21 -63
  26. package/src/services/proxy.service.ts +0 -33
  27. package/src/services/supervisor.ts +10 -0
  28. package/src/web/components/settings/proxy-settings-section.tsx +37 -50
  29. package/src/web/components/settings/proxy-test-section.tsx +25 -48
  30. package/src/web/lib/api-settings.ts +0 -2
  31. package/test-session-ops.mjs +444 -0
  32. package/test-tokens.mjs +212 -0
  33. package/.claude.bak/agent-memory/tester/MEMORY.md +0 -3
  34. package/.claude.bak/agent-memory/tester/project-ppm-test-conventions.md +0 -32
  35. package/dist/web/assets/keybindings-store-2KURy8S3.js +0 -1
  36. package/src/services/proxy-openai-bridge.ts +0 -241
@@ -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
- const msgType = (message as any).type;
178
+ if (message.type !== "stream_event") continue;
181
179
 
182
- // ── stream_event: raw Anthropic SSE events (best quality) ──
183
- if (msgType === "stream_event") {
184
- const event = (message as any).event;
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
- if (eventType === "content_block_start" && event.content_block?.type === "tool_use") {
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
- // ── partial: incremental content (fallback if no stream_event) ──
209
- if (msgType === "partial" && !streamed) {
210
- const content = (message as any).message?.content ?? [];
211
- let fullText = "";
212
- for (const block of content) {
213
- if (block.type === "text") fullText += block.text ?? "";
214
- }
215
- const delta = fullText.slice(lastContentLen);
216
- if (delta) {
217
- // Emit Anthropic SSE envelope on first partial
218
- if (lastContentLen === 0) {
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
- // ── assistant: final complete message (fallback if nothing streamed) ──
230
- if (msgType === "assistant" && !streamed && lastContentLen === 0) {
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
- {/* Anthropic endpoint */}
137
+ {/* Local endpoint */}
138
138
  <div className="space-y-1">
139
- <Label className="text-[10px] text-muted-foreground">Anthropic Endpoint</Label>
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
- {hasTunnel ? settings.proxyEndpoint : localEndpoint}
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(hasTunnel ? settings.proxyEndpoint! : localEndpoint, "anthropic")}
148
+ onClick={() => copyToClipboard(localEndpoint, "local")}
149
149
  >
150
- {copied === "anthropic" ? "Copied!" : <Copy className="size-3" />}
150
+ {copied === "local" ? "Copied!" : <Copy className="size-3" />}
151
151
  </Button>
152
152
  </div>
153
153
  </div>
154
154
 
155
- {/* OpenAI endpoint */}
156
- <div className="space-y-1">
157
- <Label className="text-[10px] text-muted-foreground">OpenAI-Compatible Endpoint</Label>
158
- <div className="flex gap-1.5 items-center">
159
- <code className="text-[10px] font-mono bg-muted px-1.5 py-0.5 rounded flex-1 truncate">
160
- {hasTunnel ? settings.openAiEndpoint : settings.localOpenAiEndpoint}
161
- </code>
162
- <Button
163
- variant="ghost"
164
- size="sm"
165
- className="h-6 px-1.5 cursor-pointer shrink-0"
166
- onClick={() => copyToClipboard(
167
- hasTunnel ? settings.openAiEndpoint! : settings.localOpenAiEndpoint, "openai",
168
- )}
169
- >
170
- {copied === "openai" ? "Copied!" : <Copy className="size-3" />}
171
- </Button>
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
- </div>
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 examples */}
181
+ {/* Usage example */}
182
182
  <div className="space-y-1 pt-1">
183
- <Label className="text-[10px] text-muted-foreground">Anthropic Format</Label>
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
- {`ANTHROPIC_BASE_URL=${hasTunnel ? settings.tunnelUrl + "/proxy" : localBaseUrl + "/proxy"}
187
- ANTHROPIC_API_KEY=${settings.authKey}`}
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
- "anthropic-env",
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 === "openai-env" ? "Copied!" : <Copy className="size-2.5" />}
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 isOpenAi = format === "openai";
77
- const endpoint = isOpenAi
78
- ? `${baseUrl}/proxy/v1/chat/completions`
79
- : `${baseUrl}/proxy/v1/messages`;
80
-
81
- const body = JSON.stringify({ model, max_tokens: 256, stream: streaming, messages: [{ role: "user", content: message }] });
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 (isOpenAi) {
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
- if (!streaming) {
103
- // Non-streaming: read full JSON and pretty-print
104
- const text = await res.text();
105
- try { setOutput(JSON.stringify(JSON.parse(text), null, 2)); } catch { setOutput(text); }
106
- setElapsed(Date.now() - start);
107
- } else {
108
- // Streaming: read SSE chunks progressively
109
- const reader = res.body?.getReader();
110
- if (!reader) { setError("No response body"); setTesting(false); return; }
111
- const decoder = new TextDecoder();
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> {