@hienlh/ppm 0.9.26 → 0.9.28

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 (27) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/web/assets/{browser-tab-DPvH5VhD.js → browser-tab-BXObsxU8.js} +1 -1
  3. package/dist/web/assets/{chat-tab-BF8l8a3L.js → chat-tab-Dco_O7_F.js} +1 -1
  4. package/dist/web/assets/{code-editor-C2TuWBbN.js → code-editor-C8-vI02_.js} +1 -1
  5. package/dist/web/assets/{database-viewer-DmuRsqPC.js → database-viewer-BImZ8TiM.js} +1 -1
  6. package/dist/web/assets/{diff-viewer-sSiXKfTi.js → diff-viewer-DNcUZzTD.js} +1 -1
  7. package/dist/web/assets/{extension-webview-BTY54kgc.js → extension-webview-Bod9-Tiq.js} +1 -1
  8. package/dist/web/assets/{git-graph-z16nosLs.js → git-graph-fH_QVGgm.js} +1 -1
  9. package/dist/web/assets/{index-GUlcYGYH.js → index-CMR5khQa.js} +6 -13
  10. package/dist/web/assets/keybindings-store-CCk3i0IU.js +1 -0
  11. package/dist/web/assets/{markdown-renderer-3lMjksD-.js → markdown-renderer-BEbe02NV.js} +1 -1
  12. package/dist/web/assets/{postgres-viewer-bN4ltnEE.js → postgres-viewer-DjdC-7Ac.js} +1 -1
  13. package/dist/web/assets/{settings-tab-DW3RE6Sm.js → settings-tab-ChcmQoqO.js} +1 -1
  14. package/dist/web/assets/{sqlite-viewer-3ti8mIWl.js → sqlite-viewer-CYe2il7B.js} +1 -1
  15. package/dist/web/assets/{terminal-tab-BcFzPhTl.js → terminal-tab-BdoPaAzE.js} +1 -1
  16. package/dist/web/assets/{use-monaco-theme-C39wzosv.js → use-monaco-theme-BPLGv__F.js} +1 -1
  17. package/dist/web/index.html +1 -1
  18. package/dist/web/sw.js +1 -1
  19. package/package.json +1 -1
  20. package/src/server/routes/proxy.ts +15 -0
  21. package/src/server/routes/settings.ts +2 -0
  22. package/src/services/proxy-openai-bridge.ts +213 -0
  23. package/src/services/proxy.service.ts +33 -0
  24. package/src/web/components/settings/proxy-settings-section.tsx +51 -38
  25. package/src/web/components/settings/proxy-test-section.tsx +26 -17
  26. package/src/web/lib/api-settings.ts +2 -0
  27. package/dist/web/assets/keybindings-store-Ch4LMTQO.js +0 -1
@@ -0,0 +1,213 @@
1
+ /**
2
+ * OpenAI-compatible proxy bridge — converts OpenAI Chat Completions
3
+ * requests into SDK query() calls and returns OpenAI-format responses.
4
+ *
5
+ * Endpoint: POST /proxy/v1/chat/completions
6
+ * Reference: https://github.com/fuergaosi233/claude-code-proxy
7
+ */
8
+ import { query } from "@anthropic-ai/claude-agent-sdk";
9
+ import { accountSelector } from "./account-selector.service.ts";
10
+
11
+ // ── Helpers ──────────────────────────────────────────────────────────
12
+
13
+ function mapModelToSdkModel(model: string): "sonnet" | "opus" | "haiku" {
14
+ if (model.includes("opus")) return "opus";
15
+ if (model.includes("haiku")) return "haiku";
16
+ return "sonnet";
17
+ }
18
+
19
+ function buildSdkEnv(accessToken: string): Record<string, string | undefined> {
20
+ const isOAuth = accessToken.startsWith("sk-ant-oat");
21
+ return {
22
+ ...process.env,
23
+ ANTHROPIC_API_KEY: isOAuth ? "" : accessToken,
24
+ CLAUDE_CODE_OAUTH_TOKEN: isOAuth ? accessToken : "",
25
+ ANTHROPIC_BASE_URL: "",
26
+ };
27
+ }
28
+
29
+ /** Extract system prompt and build text prompt from OpenAI messages format */
30
+ function buildPromptFromOpenAiMessages(body: any): { prompt: string; systemPrompt?: string } {
31
+ const messages: any[] = body.messages ?? [];
32
+ let systemPrompt: string | undefined;
33
+ const conversationParts: string[] = [];
34
+
35
+ for (const m of messages) {
36
+ const text = typeof m.content === "string"
37
+ ? m.content
38
+ : Array.isArray(m.content)
39
+ ? m.content.filter((b: any) => b.type === "text").map((b: any) => b.text).join("\n")
40
+ : String(m.content ?? "");
41
+
42
+ if (m.role === "system") {
43
+ systemPrompt = systemPrompt ? `${systemPrompt}\n${text}` : text;
44
+ } else {
45
+ const role = m.role === "assistant" ? "Assistant" : "Human";
46
+ conversationParts.push(`${role}: ${text}`);
47
+ }
48
+ }
49
+
50
+ return { prompt: conversationParts.join("\n\n"), systemPrompt };
51
+ }
52
+
53
+ function openAiError(status: number, message: string): Response {
54
+ return new Response(JSON.stringify({
55
+ error: { message, type: "server_error", code: String(status) },
56
+ }), { status, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } });
57
+ }
58
+
59
+ // ── Public API ───────────────────────────────────────────────────────
60
+
61
+ interface SdkAccount {
62
+ id: string;
63
+ email?: string | null;
64
+ accessToken: string;
65
+ }
66
+
67
+ /** Forward an OpenAI-format chat completions request via SDK query() */
68
+ export async function forwardOpenAiViaSdk(body: any, account: SdkAccount): Promise<Response> {
69
+ const model = mapModelToSdkModel(body.model || "sonnet");
70
+ const stream = body.stream ?? false;
71
+ const { prompt, systemPrompt } = buildPromptFromOpenAiMessages(body);
72
+ const env = buildSdkEnv(account.accessToken);
73
+
74
+ console.log(`[proxy-openai] ${stream ? "stream" : "non-stream"} → ${model} via ${account.email ?? account.id}`);
75
+
76
+ if (!stream) return handleNonStreaming(prompt, systemPrompt, model, env, body, account);
77
+ return handleStreaming(prompt, systemPrompt, model, env, body, account);
78
+ }
79
+
80
+ // ── Non-streaming ────────────────────────────────────────────────────
81
+
82
+ async function handleNonStreaming(
83
+ prompt: string, systemPrompt: string | undefined,
84
+ model: "sonnet" | "opus" | "haiku",
85
+ env: Record<string, string | undefined>,
86
+ body: any, account: SdkAccount,
87
+ ): Promise<Response> {
88
+ try {
89
+ let fullContent = "";
90
+ const response = query({
91
+ prompt,
92
+ options: { maxTurns: 1, model, env, ...(systemPrompt && { systemPrompt }) },
93
+ });
94
+
95
+ for await (const message of response) {
96
+ if (message.type === "assistant") {
97
+ for (const block of (message as any).message?.content ?? []) {
98
+ if (block.type === "text") fullContent += block.text;
99
+ }
100
+ }
101
+ }
102
+
103
+ accountSelector.onSuccess(account.id);
104
+
105
+ return new Response(JSON.stringify({
106
+ id: `chatcmpl-${Date.now()}`,
107
+ object: "chat.completion",
108
+ created: Math.floor(Date.now() / 1000),
109
+ model: body.model || "claude-sonnet-4-6",
110
+ choices: [{
111
+ index: 0,
112
+ message: { role: "assistant", content: fullContent },
113
+ finish_reason: "stop",
114
+ }],
115
+ usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
116
+ }), {
117
+ status: 200,
118
+ headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
119
+ });
120
+ } catch (error) {
121
+ console.error(`[proxy-openai] Non-stream error:`, (error as Error).message);
122
+ accountSelector.onRateLimit(account.id);
123
+ return openAiError(502, (error as Error).message);
124
+ }
125
+ }
126
+
127
+ // ── Streaming ────────────────────────────────────────────────────────
128
+
129
+ async function handleStreaming(
130
+ prompt: string, systemPrompt: string | undefined,
131
+ model: "sonnet" | "opus" | "haiku",
132
+ env: Record<string, string | undefined>,
133
+ body: any, account: SdkAccount,
134
+ ): Promise<Response> {
135
+ const encoder = new TextEncoder();
136
+ const chatId = `chatcmpl-${Date.now()}`;
137
+ const created = Math.floor(Date.now() / 1000);
138
+ const modelName = body.model || "claude-sonnet-4-6";
139
+
140
+ const chunk = (delta: any, finishReason: string | null) => ({
141
+ id: chatId, object: "chat.completion.chunk", created, model: modelName,
142
+ choices: [{ index: 0, delta, finish_reason: finishReason }],
143
+ });
144
+
145
+ const readable = new ReadableStream({
146
+ async start(controller) {
147
+ const send = (data: any) => {
148
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
149
+ };
150
+
151
+ try {
152
+ const response = query({
153
+ prompt,
154
+ options: {
155
+ maxTurns: 1, model, env,
156
+ ...(systemPrompt && { systemPrompt }),
157
+ includePartialMessages: true,
158
+ },
159
+ });
160
+
161
+ // Initial chunk with role
162
+ send(chunk({ role: "assistant", content: "" }, null));
163
+
164
+ const skipBlockIndices = new Set<number>();
165
+
166
+ for await (const message of response) {
167
+ if (message.type !== "stream_event") continue;
168
+
169
+ const event = (message as any).event;
170
+ const eventType = event.type as string;
171
+ const eventIndex = event.index as number | undefined;
172
+
173
+ // Track and skip tool_use blocks
174
+ if (eventType === "content_block_start" && event.content_block?.type === "tool_use") {
175
+ if (eventIndex !== undefined) skipBlockIndices.add(eventIndex);
176
+ continue;
177
+ }
178
+ if (eventIndex !== undefined && skipBlockIndices.has(eventIndex)) continue;
179
+
180
+ // Text deltas → OpenAI content chunks
181
+ if (eventType === "content_block_delta" && event.delta?.type === "text_delta") {
182
+ const text = event.delta.text ?? "";
183
+ if (text) send(chunk({ content: text }, null));
184
+ }
185
+
186
+ // Message complete → finish chunk
187
+ if (eventType === "message_stop") {
188
+ send(chunk({}, "stop"));
189
+ }
190
+ }
191
+
192
+ accountSelector.onSuccess(account.id);
193
+ controller.enqueue(encoder.encode("data: [DONE]\n\n"));
194
+ controller.close();
195
+ } catch (error) {
196
+ console.error(`[proxy-openai] Stream error:`, (error as Error).message);
197
+ accountSelector.onRateLimit(account.id);
198
+ send(chunk({ content: `\n\nError: ${(error as Error).message}` }, "stop"));
199
+ controller.enqueue(encoder.encode("data: [DONE]\n\n"));
200
+ controller.close();
201
+ }
202
+ },
203
+ });
204
+
205
+ return new Response(readable, {
206
+ headers: {
207
+ "Content-Type": "text/event-stream",
208
+ "Cache-Control": "no-cache",
209
+ "Connection": "keep-alive",
210
+ "Access-Control-Allow-Origin": "*",
211
+ },
212
+ });
213
+ }
@@ -2,6 +2,7 @@ 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";
5
6
  import { randomBytes } from "node:crypto";
6
7
 
7
8
  const PROXY_ENABLED_KEY = "proxy_enabled";
@@ -86,6 +87,38 @@ class ProxyService {
86
87
  return this.forwardDirect(path, method, headers, body, token, account);
87
88
  }
88
89
 
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
+
89
122
  /** Direct HTTP forward for API key accounts */
90
123
  private async forwardDirect(
91
124
  path: string,
@@ -131,46 +131,46 @@ export function ProxySettingsSection() {
131
131
  <div className="space-y-2 rounded-md border p-3 bg-muted/30">
132
132
  <div className="flex items-center justify-between">
133
133
  <h4 className="text-[11px] font-medium">Connection Info</h4>
134
- <ProxyTestButton authKey={settings.authKey!} baseUrl={localBaseUrl} />
134
+ <ProxyTestButton authKey={settings.authKey!} baseUrl={window.location.origin} />
135
135
  </div>
136
136
 
137
- {/* Local endpoint */}
137
+ {/* Anthropic endpoint */}
138
138
  <div className="space-y-1">
139
- <Label className="text-[10px] text-muted-foreground">Local Endpoint</Label>
139
+ <Label className="text-[10px] text-muted-foreground">Anthropic 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
- {localEndpoint}
142
+ {hasTunnel ? settings.proxyEndpoint : 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(localEndpoint, "local")}
148
+ onClick={() => copyToClipboard(hasTunnel ? settings.proxyEndpoint! : localEndpoint, "anthropic")}
149
149
  >
150
- {copied === "local" ? "Copied!" : <Copy className="size-3" />}
150
+ {copied === "anthropic" ? "Copied!" : <Copy className="size-3" />}
151
151
  </Button>
152
152
  </div>
153
153
  </div>
154
154
 
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>
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>
172
172
  </div>
173
- )}
173
+ </div>
174
174
 
175
175
  {!hasTunnel && (
176
176
  <p className="text-[10px] text-muted-foreground">
@@ -178,21 +178,13 @@ export function ProxySettingsSection() {
178
178
  </p>
179
179
  )}
180
180
 
181
- {/* Usage example */}
181
+ {/* Usage examples */}
182
182
  <div className="space-y-1 pt-1">
183
- <Label className="text-[10px] text-muted-foreground">Usage Example</Label>
183
+ <Label className="text-[10px] text-muted-foreground">Anthropic Format</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
- {`# 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"}]}'`}
186
+ {`ANTHROPIC_BASE_URL=${hasTunnel ? settings.tunnelUrl + "/proxy" : localBaseUrl + "/proxy"}
187
+ ANTHROPIC_API_KEY=${settings.authKey}`}
196
188
  </pre>
197
189
  <Button
198
190
  variant="ghost"
@@ -200,10 +192,31 @@ curl ${hasTunnel ? settings.proxyEndpoint : localEndpoint} \\
200
192
  className="absolute top-1 right-1 h-5 px-1 cursor-pointer"
201
193
  onClick={() => copyToClipboard(
202
194
  `ANTHROPIC_BASE_URL=${hasTunnel ? settings.tunnelUrl + "/proxy" : localBaseUrl + "/proxy"}\nANTHROPIC_API_KEY=${settings.authKey}`,
203
- "example",
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",
204
217
  )}
205
218
  >
206
- {copied === "example" ? "Copied!" : <Copy className="size-2.5" />}
219
+ {copied === "openai-env" ? "Copied!" : <Copy className="size-2.5" />}
207
220
  </Button>
208
221
  </div>
209
222
  </div>
@@ -14,7 +14,6 @@ type EndpointFormat = "anthropic" | "openai";
14
14
 
15
15
  interface ProxyTestDialogProps {
16
16
  authKey: string;
17
- /** Current page origin, e.g. http://localhost:3210 */
18
17
  baseUrl: string;
19
18
  }
20
19
 
@@ -36,7 +35,7 @@ export function ProxyTestButton(props: ProxyTestDialogProps) {
36
35
  Send a test request and inspect the raw response.
37
36
  </DialogDescription>
38
37
  </DialogHeader>
39
- <ProxyTestForm {...props} />
38
+ {open && <ProxyTestForm {...props} />}
40
39
  </DialogContent>
41
40
  </Dialog>
42
41
  );
@@ -58,9 +57,13 @@ function ProxyTestForm({ authKey, baseUrl }: ProxyTestDialogProps) {
58
57
  if (outputRef.current) outputRef.current.scrollTop = outputRef.current.scrollHeight;
59
58
  }, [output]);
60
59
 
61
- const endpoint = format === "anthropic"
62
- ? `${baseUrl}/proxy/v1/messages`
63
- : `${baseUrl}/proxy/v1/chat/completions`;
60
+ // Clear output when switching format
61
+ const switchFormat = (f: EndpointFormat) => {
62
+ setFormat(f);
63
+ setOutput(null);
64
+ setError(null);
65
+ setElapsed(null);
66
+ };
64
67
 
65
68
  const runTest = async () => {
66
69
  setTesting(true);
@@ -69,17 +72,21 @@ function ProxyTestForm({ authKey, baseUrl }: ProxyTestDialogProps) {
69
72
  setElapsed(null);
70
73
  const start = Date.now();
71
74
 
72
- const isAnthropic = format === "anthropic";
73
- const body = isAnthropic
75
+ const isOpenAi = format === "openai";
76
+ const endpoint = isOpenAi
77
+ ? `${baseUrl}/proxy/v1/chat/completions`
78
+ : `${baseUrl}/proxy/v1/messages`;
79
+
80
+ const body = isOpenAi
74
81
  ? JSON.stringify({ model, max_tokens: 256, stream: true, messages: [{ role: "user", content: message }] })
75
82
  : JSON.stringify({ model, max_tokens: 256, stream: true, messages: [{ role: "user", content: message }] });
76
83
 
77
84
  const headers: Record<string, string> = { "Content-Type": "application/json" };
78
- if (isAnthropic) {
85
+ if (isOpenAi) {
86
+ headers["Authorization"] = `Bearer ${authKey}`;
87
+ } else {
79
88
  headers["x-api-key"] = authKey;
80
89
  headers["anthropic-version"] = "2023-06-01";
81
- } else {
82
- headers["Authorization"] = `Bearer ${authKey}`;
83
90
  }
84
91
 
85
92
  try {
@@ -114,15 +121,15 @@ function ProxyTestForm({ authKey, baseUrl }: ProxyTestDialogProps) {
114
121
  };
115
122
 
116
123
  return (
117
- <div className="space-y-3">
124
+ <div className="space-y-3 min-w-0">
118
125
  {/* Endpoint format toggle */}
119
- <div className="space-y-1.5">
120
- <Label className="text-[11px]">Endpoint Format</Label>
126
+ <div className="space-y-1.5 min-w-0">
127
+ <Label className="text-[11px]">Auth Style</Label>
121
128
  <div className="flex gap-1">
122
129
  {(["anthropic", "openai"] as const).map((f) => (
123
130
  <button
124
131
  key={f}
125
- onClick={() => setFormat(f)}
132
+ onClick={() => switchFormat(f)}
126
133
  className={`flex-1 h-8 rounded-md text-[11px] font-medium border transition-colors cursor-pointer ${
127
134
  format === f
128
135
  ? "bg-primary text-primary-foreground border-primary"
@@ -133,7 +140,9 @@ function ProxyTestForm({ authKey, baseUrl }: ProxyTestDialogProps) {
133
140
  </button>
134
141
  ))}
135
142
  </div>
136
- <code className="block text-[9px] font-mono text-muted-foreground truncate">{endpoint}</code>
143
+ <p className="text-[9px] text-muted-foreground">
144
+ {format === "anthropic" ? "x-api-key header" : "Authorization: Bearer header"}
145
+ </p>
137
146
  </div>
138
147
 
139
148
  {/* Model */}
@@ -159,7 +168,7 @@ function ProxyTestForm({ authKey, baseUrl }: ProxyTestDialogProps) {
159
168
  value={message}
160
169
  onChange={(e) => setMessage(e.target.value)}
161
170
  placeholder="Type a test message..."
162
- className="h-9 text-[11px] flex-1"
171
+ className="h-9 text-[11px] flex-1 min-w-0"
163
172
  onKeyDown={(e) => { if (e.key === "Enter" && !testing) runTest(); }}
164
173
  />
165
174
  <Button
@@ -176,7 +185,7 @@ function ProxyTestForm({ authKey, baseUrl }: ProxyTestDialogProps) {
176
185
 
177
186
  {/* Raw output */}
178
187
  {(output || error) && (
179
- <div className="space-y-1">
188
+ <div className="space-y-1 min-w-0">
180
189
  <div className="flex items-center justify-between">
181
190
  <Label className="text-[10px] text-muted-foreground">Raw Response</Label>
182
191
  {elapsed != null && (
@@ -191,8 +191,10 @@ export interface ProxySettings {
191
191
  authKey: string | null;
192
192
  requestCount: number;
193
193
  localEndpoint: string;
194
+ localOpenAiEndpoint: string;
194
195
  tunnelUrl: string | null;
195
196
  proxyEndpoint: string | null;
197
+ openAiEndpoint: string | null;
196
198
  }
197
199
 
198
200
  export function getProxySettings(): Promise<ProxySettings> {
@@ -1 +0,0 @@
1
- import"./react-nm2Ru1Pt.js";import"./api-client-BKIT_Qeg.js";import{W as e}from"./index-GUlcYGYH.js";export{e as useKeybindingsStore};