@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.
- package/CHANGELOG.md +9 -0
- package/dist/web/assets/{browser-tab-DPvH5VhD.js → browser-tab-BXObsxU8.js} +1 -1
- package/dist/web/assets/{chat-tab-BF8l8a3L.js → chat-tab-Dco_O7_F.js} +1 -1
- package/dist/web/assets/{code-editor-C2TuWBbN.js → code-editor-C8-vI02_.js} +1 -1
- package/dist/web/assets/{database-viewer-DmuRsqPC.js → database-viewer-BImZ8TiM.js} +1 -1
- package/dist/web/assets/{diff-viewer-sSiXKfTi.js → diff-viewer-DNcUZzTD.js} +1 -1
- package/dist/web/assets/{extension-webview-BTY54kgc.js → extension-webview-Bod9-Tiq.js} +1 -1
- package/dist/web/assets/{git-graph-z16nosLs.js → git-graph-fH_QVGgm.js} +1 -1
- package/dist/web/assets/{index-GUlcYGYH.js → index-CMR5khQa.js} +6 -13
- package/dist/web/assets/keybindings-store-CCk3i0IU.js +1 -0
- package/dist/web/assets/{markdown-renderer-3lMjksD-.js → markdown-renderer-BEbe02NV.js} +1 -1
- package/dist/web/assets/{postgres-viewer-bN4ltnEE.js → postgres-viewer-DjdC-7Ac.js} +1 -1
- package/dist/web/assets/{settings-tab-DW3RE6Sm.js → settings-tab-ChcmQoqO.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-3ti8mIWl.js → sqlite-viewer-CYe2il7B.js} +1 -1
- package/dist/web/assets/{terminal-tab-BcFzPhTl.js → terminal-tab-BdoPaAzE.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-C39wzosv.js → use-monaco-theme-BPLGv__F.js} +1 -1
- package/dist/web/index.html +1 -1
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/server/routes/proxy.ts +15 -0
- package/src/server/routes/settings.ts +2 -0
- package/src/services/proxy-openai-bridge.ts +213 -0
- package/src/services/proxy.service.ts +33 -0
- package/src/web/components/settings/proxy-settings-section.tsx +51 -38
- package/src/web/components/settings/proxy-test-section.tsx +26 -17
- package/src/web/lib/api-settings.ts +2 -0
- 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={
|
|
134
|
+
<ProxyTestButton authKey={settings.authKey!} baseUrl={window.location.origin} />
|
|
135
135
|
</div>
|
|
136
136
|
|
|
137
|
-
{/*
|
|
137
|
+
{/* Anthropic endpoint */}
|
|
138
138
|
<div className="space-y-1">
|
|
139
|
-
<Label className="text-[10px] text-muted-foreground">
|
|
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, "
|
|
148
|
+
onClick={() => copyToClipboard(hasTunnel ? settings.proxyEndpoint! : localEndpoint, "anthropic")}
|
|
149
149
|
>
|
|
150
|
-
{copied === "
|
|
150
|
+
{copied === "anthropic" ? "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
|
+
{/* 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
|
|
181
|
+
{/* Usage examples */}
|
|
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">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
|
-
{
|
|
187
|
-
|
|
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
|
-
"
|
|
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 === "
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
73
|
-
const
|
|
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 (
|
|
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]">
|
|
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={() =>
|
|
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
|
-
<
|
|
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};
|