panrouter 5.0.0 → 5.0.1
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/package.json +1 -1
- package/relay_client.cjs +184 -13
package/package.json
CHANGED
package/relay_client.cjs
CHANGED
|
@@ -16,6 +16,168 @@ const WebSocket = globalThis.WebSocket;
|
|
|
16
16
|
const WS_OPEN = WebSocket.OPEN;
|
|
17
17
|
const net = require('net');
|
|
18
18
|
|
|
19
|
+
// ─── Anthropic ↔ OpenAI 格式转换(内嵌,不动服务端和管理版) ──
|
|
20
|
+
const MODEL_MAP = { "combo": "deepseek-v4-flash-free" };
|
|
21
|
+
const DEFAULT_MODEL = "deepseek-v4-flash-free";
|
|
22
|
+
|
|
23
|
+
function claudeToOpenAI(body) {
|
|
24
|
+
const result = { messages: [], stream: false };
|
|
25
|
+
const raw = body.model || "";
|
|
26
|
+
const name = raw.includes("/") ? raw.split("/").pop() : raw;
|
|
27
|
+
result.model = MODEL_MAP[name] || MODEL_MAP[raw] || DEFAULT_MODEL;
|
|
28
|
+
|
|
29
|
+
if (body.max_tokens) result.max_tokens = body.max_tokens;
|
|
30
|
+
if (body.temperature !== undefined) result.temperature = body.temperature;
|
|
31
|
+
if (body.top_p !== undefined) result.top_p = body.top_p;
|
|
32
|
+
if (body.system) {
|
|
33
|
+
const txt = Array.isArray(body.system)
|
|
34
|
+
? body.system.map(s => s.text || "").filter(Boolean).join("\n")
|
|
35
|
+
: String(body.system);
|
|
36
|
+
if (txt) result.messages.push({ role: "system", content: txt });
|
|
37
|
+
}
|
|
38
|
+
if (Array.isArray(body.messages)) {
|
|
39
|
+
for (const m of body.messages) {
|
|
40
|
+
const c = convertMsg(m);
|
|
41
|
+
if (Array.isArray(c)) result.messages.push(...c);
|
|
42
|
+
else if (c) result.messages.push(c);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(body.tools)) {
|
|
46
|
+
result.tools = body.tools.map(t => ({
|
|
47
|
+
type: "function",
|
|
48
|
+
function: { name: t.name, description: String(t.description || ""), parameters: t.input_schema || { type: "object", properties: {} } },
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
if (body.tool_choice) {
|
|
52
|
+
const tc = body.tool_choice;
|
|
53
|
+
if (tc.type === "tool") result.tool_choice = { type: "function", function: { name: tc.name } };
|
|
54
|
+
else result.tool_choice = tc.type === "any" ? "required" : tc.type;
|
|
55
|
+
}
|
|
56
|
+
if (body.stop_sequences) result.stop = Array.isArray(body.stop_sequences) ? body.stop_sequences : [body.stop_sequences];
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function convertMsg(m) {
|
|
61
|
+
if (m.role === "user") return convertUser(m);
|
|
62
|
+
if (m.role === "assistant") return convertAssistant(m);
|
|
63
|
+
if (m.role === "tool") return { role: "tool", tool_call_id: m.tool_use_id, content: flatText(m.content) };
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function convertUser(m) {
|
|
68
|
+
if (typeof m.content === "string") return { role: "user", content: m.content };
|
|
69
|
+
if (!Array.isArray(m.content)) return { role: "user", content: "" };
|
|
70
|
+
const parts = [], toolResults = [];
|
|
71
|
+
for (const b of m.content) {
|
|
72
|
+
if (b.type === "text") parts.push(b.text);
|
|
73
|
+
if (b.type === "tool_result") toolResults.push({ role: "tool", tool_call_id: b.tool_use_id, content: flatText(b.content) });
|
|
74
|
+
}
|
|
75
|
+
if (toolResults.length > 0) {
|
|
76
|
+
if (parts.length) toolResults.push({ role: "user", content: parts.join("") });
|
|
77
|
+
return toolResults;
|
|
78
|
+
}
|
|
79
|
+
return { role: "user", content: parts.join("") };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function convertAssistant(m) {
|
|
83
|
+
if (typeof m.content === "string") return m.content ? { role: "assistant", content: m.content } : null;
|
|
84
|
+
if (!Array.isArray(m.content)) return null;
|
|
85
|
+
const texts = [], calls = [];
|
|
86
|
+
for (const b of m.content) {
|
|
87
|
+
if (b.type === "text") texts.push(b.text);
|
|
88
|
+
if (b.type === "thinking") texts.push(b.thinking || "");
|
|
89
|
+
if (b.type === "tool_use") calls.push({ id: b.id, type: "function", function: { name: b.name, arguments: JSON.stringify(b.input || {}) } });
|
|
90
|
+
}
|
|
91
|
+
const r = { role: "assistant" };
|
|
92
|
+
if (texts.length && calls.length === 0) r.content = texts.join("");
|
|
93
|
+
if (calls.length > 0) r.tool_calls = calls;
|
|
94
|
+
if (calls.length === 0 && texts.length === 0) r.content = "";
|
|
95
|
+
return r;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function flatText(c) {
|
|
99
|
+
if (typeof c === "string") return c;
|
|
100
|
+
if (Array.isArray(c)) return c.filter(x => x.type === "text").map(x => x.text).join("\n");
|
|
101
|
+
return "";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// OpenAI 非流式响应 → Anthropic 格式
|
|
105
|
+
function openaiToClaudeResponse(data) {
|
|
106
|
+
if (!data || !data.choices?.[0]?.message) return null;
|
|
107
|
+
const choice = data.choices[0];
|
|
108
|
+
const msg = choice.message;
|
|
109
|
+
|
|
110
|
+
const result = {
|
|
111
|
+
id: `msg_${(data.id || "").replace("chatcmpl-", "")}`,
|
|
112
|
+
type: "message",
|
|
113
|
+
role: "assistant",
|
|
114
|
+
model: data.model || "unknown",
|
|
115
|
+
content: [],
|
|
116
|
+
stop_reason: null,
|
|
117
|
+
stop_sequence: null,
|
|
118
|
+
usage: {
|
|
119
|
+
input_tokens: data.usage?.prompt_tokens || 0,
|
|
120
|
+
output_tokens: data.usage?.completion_tokens || 0,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const finishMap = { stop: "end_turn", tool_calls: "tool_use", length: "max_tokens", content_filter: "content_filter" };
|
|
125
|
+
result.stop_reason = finishMap[choice.finish_reason] || choice.finish_reason || null;
|
|
126
|
+
|
|
127
|
+
if (msg.content) {
|
|
128
|
+
result.content.push({ type: "text", text: msg.content });
|
|
129
|
+
}
|
|
130
|
+
if (msg.tool_calls) {
|
|
131
|
+
for (const tc of msg.tool_calls) {
|
|
132
|
+
let input = {};
|
|
133
|
+
try { input = JSON.parse(tc.function?.arguments || "{}"); } catch {}
|
|
134
|
+
result.content.push({
|
|
135
|
+
type: "tool_use",
|
|
136
|
+
id: tc.id,
|
|
137
|
+
name: tc.function?.name || "",
|
|
138
|
+
input,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 判断请求是否为 Anthropic 格式(需要转换)
|
|
147
|
+
// 规则:有 messages 且至少一条 message 的 content 是数组(Anthropic 内容块)
|
|
148
|
+
function isAnthropicFormat(body) {
|
|
149
|
+
if (!body || typeof body !== "object" || !Array.isArray(body.messages)) return false;
|
|
150
|
+
// 如果第一条 message 的 content 是数组(含 type/text 等字段),是 Anthropic 格式
|
|
151
|
+
return body.messages.some(m => Array.isArray(m.content) && m.content.length > 0 && typeof m.content[0] === "object" && m.content[0].type);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 转换 Anthropic 请求并调用 OpenCode,再转回 Anthropic 格式
|
|
155
|
+
async function callOpenCodeWithConversion(body) {
|
|
156
|
+
const openAIBody = claudeToOpenAI(body);
|
|
157
|
+
log(`已转换 Anthropic→OpenAI: model=${openAIBody.model} msgs=${openAIBody.messages.length}`, "CONVERT");
|
|
158
|
+
|
|
159
|
+
const res = await fetch("https://opencode.ai/zen/v1/chat/completions", {
|
|
160
|
+
method: "POST",
|
|
161
|
+
headers: {
|
|
162
|
+
"Content-Type": "application/json",
|
|
163
|
+
Authorization: "Bearer public",
|
|
164
|
+
"x-opencode-client": "desktop",
|
|
165
|
+
},
|
|
166
|
+
body: JSON.stringify(openAIBody),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const data = await res.json();
|
|
170
|
+
|
|
171
|
+
if (!res.ok) {
|
|
172
|
+
log(`OpenCode API 错误: HTTP ${res.status}`, "ERROR");
|
|
173
|
+
return { error: { message: data.error?.message || `HTTP ${res.status}` } };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const claudeResp = openaiToClaudeResponse(data);
|
|
177
|
+
log(`已转换 OpenAI→Anthropic: stop_reason=${claudeResp?.stop_reason || "?"}`, "CONVERT");
|
|
178
|
+
return claudeResp;
|
|
179
|
+
}
|
|
180
|
+
|
|
19
181
|
// ─── 命令行参数解析 ──────────────────────────────────────────
|
|
20
182
|
function parseArgs() {
|
|
21
183
|
const args = process.argv.slice(2);
|
|
@@ -250,19 +412,28 @@ function executeAndRespond(task, body) {
|
|
|
250
412
|
function callOpenAIAndRespond(task) {
|
|
251
413
|
const body = task.body || task;
|
|
252
414
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
415
|
+
// 自动判断:如果 body 是 Anthropic 格式(有 messages 且不含 OpenAI 特征),自动转换
|
|
416
|
+
const needsConversion = isAnthropicFormat(body);
|
|
417
|
+
|
|
418
|
+
const doRequest = needsConversion
|
|
419
|
+
? callOpenCodeWithConversion(body)
|
|
420
|
+
: fetch("https://opencode.ai/zen/v1/chat/completions", {
|
|
421
|
+
method: "POST",
|
|
422
|
+
headers: {
|
|
423
|
+
"Content-Type": "application/json",
|
|
424
|
+
Authorization: "Bearer public",
|
|
425
|
+
"x-opencode-client": "desktop",
|
|
426
|
+
},
|
|
427
|
+
body: JSON.stringify(body),
|
|
428
|
+
}).then(async (r) => {
|
|
429
|
+
const data = await r.json();
|
|
430
|
+
const content = data?.choices?.[0]?.message?.content || "";
|
|
431
|
+
log(`AI ${r.status === 200 ? "成功" : "失败"}: ${content.slice(0, 60)}`, "AI");
|
|
432
|
+
return data;
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
doRequest.then(response => {
|
|
436
|
+
sendResponse(task.request_id, response);
|
|
266
437
|
}).catch(e => {
|
|
267
438
|
log(`AI 异常: ${e.message}`, "AI");
|
|
268
439
|
sendResponse(task.request_id, { error: e.message });
|