@starlink-awaken/agentmesh 1.3.2 → 1.4.0
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/dist/src/cli.js +1 -1
- package/dist/src/model-gateway/metrics.js +3 -3
- package/dist/src/model-gateway/providers.js +17 -40
- package/dist/src/model-gateway/router.js +15 -2
- package/dist/src/model-gateway/routes.js +27 -9
- package/dist/src/model-gateway/types.d.ts +1 -0
- package/package.json +1 -1
package/dist/src/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
7
7
|
import { resolve, dirname, join } from 'node:path';
|
|
8
8
|
import { initLogger } from './core/logger.js';
|
|
9
9
|
const PROJECT_ROOT = resolve(dirname(import.meta.dir), '..');
|
|
10
|
-
const VERSION = '1.
|
|
10
|
+
const VERSION = '1.4.0';
|
|
11
11
|
const BANNER = `
|
|
12
12
|
█████╗ ██████╗ ███████╗███╗ ██╗████████╗
|
|
13
13
|
██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝
|
|
@@ -21,9 +21,9 @@ export function recordRequest(log) {
|
|
|
21
21
|
m.lastError = log.error;
|
|
22
22
|
m.lastErrorTime = log.timestamp;
|
|
23
23
|
}
|
|
24
|
-
recentRequests.
|
|
24
|
+
recentRequests.push(log);
|
|
25
25
|
if (recentRequests.length > MAX_RECENT)
|
|
26
|
-
recentRequests.
|
|
26
|
+
recentRequests.shift();
|
|
27
27
|
}
|
|
28
28
|
export function getMetrics() {
|
|
29
29
|
const providers = {};
|
|
@@ -46,7 +46,7 @@ export function getMetrics() {
|
|
|
46
46
|
total_requests: totalRequests,
|
|
47
47
|
total_failures: totalFailures,
|
|
48
48
|
providers,
|
|
49
|
-
recent: recentRequests.slice(
|
|
49
|
+
recent: recentRequests.slice(-20).reverse().map(r => ({
|
|
50
50
|
time: new Date(r.timestamp).toISOString(),
|
|
51
51
|
model: r.model,
|
|
52
52
|
provider: r.provider,
|
|
@@ -52,9 +52,6 @@ export async function callChatCompletions(provider, request) {
|
|
|
52
52
|
throw err;
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
// ============================================================================
|
|
56
|
-
// Responses API → Chat Completions 双向转换(含 tool_calls 往返)
|
|
57
|
-
// ============================================================================
|
|
58
55
|
export async function callResponsesApi(provider, body) {
|
|
59
56
|
const messages = convertInputToMessages(body.input || []);
|
|
60
57
|
if (body.instructions) {
|
|
@@ -84,9 +81,6 @@ export async function callResponsesApi(provider, body) {
|
|
|
84
81
|
headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' },
|
|
85
82
|
});
|
|
86
83
|
}
|
|
87
|
-
// ============================================================================
|
|
88
|
-
// 输入转换: Responses input[] → Chat messages[]
|
|
89
|
-
// ============================================================================
|
|
90
84
|
function convertInputToMessages(input) {
|
|
91
85
|
const messages = [];
|
|
92
86
|
for (const item of input) {
|
|
@@ -95,7 +89,6 @@ function convertInputToMessages(input) {
|
|
|
95
89
|
case 'message':
|
|
96
90
|
messages.push({ role: item.role || 'user', content: extractTextContent(item.content) });
|
|
97
91
|
break;
|
|
98
|
-
// Function call(Assistant 侧发起工具调用)
|
|
99
92
|
case 'function_call': {
|
|
100
93
|
messages.push({
|
|
101
94
|
role: 'assistant',
|
|
@@ -111,7 +104,6 @@ function convertInputToMessages(input) {
|
|
|
111
104
|
});
|
|
112
105
|
break;
|
|
113
106
|
}
|
|
114
|
-
// Function call output(Tool 返回结果)
|
|
115
107
|
case 'function_call_output':
|
|
116
108
|
messages.push({
|
|
117
109
|
role: 'tool',
|
|
@@ -119,7 +111,6 @@ function convertInputToMessages(input) {
|
|
|
119
111
|
content: typeof item.output === 'string' ? item.output : JSON.stringify(item.output),
|
|
120
112
|
});
|
|
121
113
|
break;
|
|
122
|
-
// 简单角色
|
|
123
114
|
case 'system':
|
|
124
115
|
messages.push({ role: 'system', content: extractTextContent(item.content) });
|
|
125
116
|
break;
|
|
@@ -130,7 +121,6 @@ function convertInputToMessages(input) {
|
|
|
130
121
|
messages.push({ role: 'assistant', content: extractTextContent(item.content) });
|
|
131
122
|
break;
|
|
132
123
|
default:
|
|
133
|
-
// 回退: role 字段
|
|
134
124
|
if (item.role) {
|
|
135
125
|
messages.push({ role: item.role, content: extractTextContent(item.content) });
|
|
136
126
|
}
|
|
@@ -138,16 +128,12 @@ function convertInputToMessages(input) {
|
|
|
138
128
|
}
|
|
139
129
|
return messages;
|
|
140
130
|
}
|
|
141
|
-
// ============================================================================
|
|
142
|
-
// 输出转换: Chat completions response → Responses API response
|
|
143
|
-
// ============================================================================
|
|
144
131
|
function convertChatToResponses(ccData) {
|
|
145
132
|
const choice = ccData.choices?.[0];
|
|
146
133
|
if (!choice) {
|
|
147
134
|
return { id: ccData.id, object: 'response', model: ccData.model, output: [], usage: ccData.usage };
|
|
148
135
|
}
|
|
149
136
|
const output = [];
|
|
150
|
-
// 文本回复
|
|
151
137
|
if (choice.message?.content) {
|
|
152
138
|
output.push({
|
|
153
139
|
type: 'message',
|
|
@@ -155,7 +141,6 @@ function convertChatToResponses(ccData) {
|
|
|
155
141
|
content: [{ type: 'output_text', text: choice.message.content }],
|
|
156
142
|
});
|
|
157
143
|
}
|
|
158
|
-
// 工具调用
|
|
159
144
|
if (choice.message?.tool_calls) {
|
|
160
145
|
for (const tc of choice.message.tool_calls) {
|
|
161
146
|
output.push({
|
|
@@ -177,16 +162,16 @@ function convertChatToResponses(ccData) {
|
|
|
177
162
|
usage: ccData.usage,
|
|
178
163
|
};
|
|
179
164
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
165
|
+
const EVENT_DELTA = 'response.output_text.delta';
|
|
166
|
+
const EVENT_TOOL_DELTA = 'response.function_call_arguments.delta';
|
|
167
|
+
const EVENT_COMPLETED = 'response.completed';
|
|
168
|
+
const sseEncoder = new TextEncoder();
|
|
183
169
|
function transformSSEStream(upstreamBody) {
|
|
184
|
-
const encoder = new TextEncoder();
|
|
185
170
|
let buffer = '';
|
|
186
171
|
let responseId = '';
|
|
187
172
|
let modelName = '';
|
|
188
173
|
let contentBuffer = '';
|
|
189
|
-
|
|
174
|
+
const toolCallAccum = {};
|
|
190
175
|
return new ReadableStream({
|
|
191
176
|
async start(controller) {
|
|
192
177
|
const reader = upstreamBody.getReader();
|
|
@@ -197,17 +182,17 @@ function transformSSEStream(upstreamBody) {
|
|
|
197
182
|
if (done)
|
|
198
183
|
break;
|
|
199
184
|
buffer += decoder.decode(value, { stream: true });
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
185
|
+
let newlineIdx;
|
|
186
|
+
while ((newlineIdx = buffer.indexOf('\n')) !== -1) {
|
|
187
|
+
const line = buffer.slice(0, newlineIdx);
|
|
188
|
+
buffer = buffer.slice(newlineIdx + 1);
|
|
203
189
|
if (!line.startsWith('data: '))
|
|
204
190
|
continue;
|
|
205
191
|
const data = line.slice(6).trim();
|
|
206
192
|
if (data === '[DONE]') {
|
|
207
|
-
// 发送最终事件
|
|
208
193
|
const finalEvt = buildResponseEvent(responseId, modelName, contentBuffer, toolCallAccum, true);
|
|
209
|
-
controller.enqueue(
|
|
210
|
-
controller.enqueue(
|
|
194
|
+
controller.enqueue(sseEncoder.encode(finalEvt));
|
|
195
|
+
controller.enqueue(sseEncoder.encode('data: [DONE]\n\n'));
|
|
211
196
|
continue;
|
|
212
197
|
}
|
|
213
198
|
try {
|
|
@@ -217,16 +202,14 @@ function transformSSEStream(upstreamBody) {
|
|
|
217
202
|
const delta = chunk.choices?.[0]?.delta;
|
|
218
203
|
if (!delta)
|
|
219
204
|
continue;
|
|
220
|
-
// 文本增量
|
|
221
205
|
if (delta.content) {
|
|
222
206
|
contentBuffer += delta.content;
|
|
223
207
|
const evt = `data: ${JSON.stringify({
|
|
224
|
-
type:
|
|
208
|
+
type: EVENT_DELTA,
|
|
225
209
|
delta: delta.content,
|
|
226
210
|
})}\n\n`;
|
|
227
|
-
controller.enqueue(
|
|
211
|
+
controller.enqueue(sseEncoder.encode(evt));
|
|
228
212
|
}
|
|
229
|
-
// 工具调用增量
|
|
230
213
|
if (delta.tool_calls) {
|
|
231
214
|
for (const tc of delta.tool_calls) {
|
|
232
215
|
if (!toolCallAccum[tc.index]) {
|
|
@@ -246,15 +229,15 @@ function transformSSEStream(upstreamBody) {
|
|
|
246
229
|
}
|
|
247
230
|
// 发送 tool_call delta 事件
|
|
248
231
|
const tcEvt = `data: ${JSON.stringify({
|
|
249
|
-
type:
|
|
232
|
+
type: EVENT_TOOL_DELTA,
|
|
250
233
|
tool_calls: Object.values(toolCallAccum),
|
|
251
234
|
})}\n\n`;
|
|
252
|
-
controller.enqueue(
|
|
235
|
+
controller.enqueue(sseEncoder.encode(tcEvt));
|
|
253
236
|
}
|
|
254
237
|
}
|
|
255
238
|
catch {
|
|
256
239
|
// 非 JSON 行直接透传
|
|
257
|
-
controller.enqueue(
|
|
240
|
+
controller.enqueue(sseEncoder.encode(line + '\n'));
|
|
258
241
|
}
|
|
259
242
|
}
|
|
260
243
|
}
|
|
@@ -283,14 +266,11 @@ function buildResponseEvent(id, model, content, toolCalls, isFinal) {
|
|
|
283
266
|
});
|
|
284
267
|
}
|
|
285
268
|
return `data: ${JSON.stringify({
|
|
286
|
-
type: isFinal ?
|
|
269
|
+
type: isFinal ? EVENT_COMPLETED : EVENT_DELTA,
|
|
287
270
|
response: isFinal ? { id, model, object: 'response', status: 'completed', output } : undefined,
|
|
288
271
|
delta: isFinal ? undefined : content,
|
|
289
272
|
})}\n\n`;
|
|
290
273
|
}
|
|
291
|
-
// ============================================================================
|
|
292
|
-
// 工具定义转换
|
|
293
|
-
// ============================================================================
|
|
294
274
|
function convertToolSchemas(tools) {
|
|
295
275
|
if (!tools || !Array.isArray(tools))
|
|
296
276
|
return undefined;
|
|
@@ -303,9 +283,6 @@ function convertToolSchemas(tools) {
|
|
|
303
283
|
},
|
|
304
284
|
}));
|
|
305
285
|
}
|
|
306
|
-
// ============================================================================
|
|
307
|
-
// 辅助函数
|
|
308
|
-
// ============================================================================
|
|
309
286
|
function extractTextContent(content) {
|
|
310
287
|
if (typeof content === 'string')
|
|
311
288
|
return content;
|
|
@@ -15,9 +15,22 @@ export function initModelRouter(cfg) {
|
|
|
15
15
|
config = cfg;
|
|
16
16
|
// 从 config 加载模型别名(覆盖默认)
|
|
17
17
|
if (cfg.model_aliases) {
|
|
18
|
-
|
|
18
|
+
// cfg.model_aliases: { "gpt-x": "deepseek-v4-pro", "claude-y": "claude-opus" }
|
|
19
|
+
// 按目标模型名反查对应的 provider → 写入对应 provider 的别名表
|
|
20
|
+
for (const [aliasKey, realModel] of Object.entries(cfg.model_aliases)) {
|
|
21
|
+
for (const [providerName, providerCfg] of Object.entries(config.providers)) {
|
|
22
|
+
const providerModels = providerCfg.models || [];
|
|
23
|
+
if (providerModels.includes(realModel)) {
|
|
24
|
+
modelAliases[providerName] = modelAliases[providerName] || {};
|
|
25
|
+
modelAliases[providerName][aliasKey] = realModel;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// 兜底:如果没匹配到任何 provider,放入 deepseek
|
|
19
30
|
modelAliases.deepseek = modelAliases.deepseek || {};
|
|
20
|
-
modelAliases.deepseek[
|
|
31
|
+
if (!modelAliases.deepseek[aliasKey]) {
|
|
32
|
+
modelAliases.deepseek[aliasKey] = realModel;
|
|
33
|
+
}
|
|
21
34
|
}
|
|
22
35
|
}
|
|
23
36
|
}
|
|
@@ -4,6 +4,14 @@ import { getQuotaSummary, probeQuota } from './quota.js';
|
|
|
4
4
|
import { circuitBreakerRegistry } from './circuit-breaker.js';
|
|
5
5
|
import { checkAllProviders } from './health.js';
|
|
6
6
|
import { getMetrics, recordRequest } from './metrics.js';
|
|
7
|
+
import { checkRateLimit } from './rate-limit.js';
|
|
8
|
+
function logReq(originalModel, providerName, actualModel, reqStart, status, streaming, error) {
|
|
9
|
+
recordRequest({
|
|
10
|
+
timestamp: Date.now(), model: originalModel,
|
|
11
|
+
provider: providerName, actualModel, latencyMs: Date.now() - reqStart,
|
|
12
|
+
status, streaming, error,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
7
15
|
export async function modelGatewayRoutes(fastify) {
|
|
8
16
|
// 健康检查 + 配额总览
|
|
9
17
|
fastify.get('/model-gateway/health', async (_req, _reply) => {
|
|
@@ -41,6 +49,11 @@ export async function modelGatewayRoutes(fastify) {
|
|
|
41
49
|
});
|
|
42
50
|
// POST /v1/chat/completions — 标准 OpenAI 兼容端点
|
|
43
51
|
fastify.post('/v1/chat/completions', async (request, reply) => {
|
|
52
|
+
const ip = request.ip || '127.0.0.1';
|
|
53
|
+
const rl = checkRateLimit('/v1/chat/completions', ip);
|
|
54
|
+
if (!rl.allowed) {
|
|
55
|
+
return reply.code(429).header('Retry-After', String(rl.resetSeconds)).header('X-RateLimit-Limit', rl.limit).header('X-RateLimit-Remaining', '0').send({ error: { message: 'Rate limit exceeded' } });
|
|
56
|
+
}
|
|
44
57
|
const body = request.body;
|
|
45
58
|
if (!body || !body.messages) {
|
|
46
59
|
return reply.code(400).send({ error: { message: 'messages is required' } });
|
|
@@ -65,16 +78,16 @@ export async function modelGatewayRoutes(fastify) {
|
|
|
65
78
|
tools: body.tools,
|
|
66
79
|
tool_choice: body.tool_choice,
|
|
67
80
|
});
|
|
68
|
-
if (!upstreamResp.ok
|
|
81
|
+
if (!upstreamResp.ok) {
|
|
69
82
|
const errText = await upstreamResp.text();
|
|
70
|
-
|
|
83
|
+
logReq(originalModel, provider.name, model, reqStart, upstreamResp.status, !!body.stream, errText.slice(0, 200));
|
|
71
84
|
console.error(`[ModelGW] ${provider.name} error ${upstreamResp.status}: ${errText.slice(0, 200)}`);
|
|
72
85
|
return reply.code(upstreamResp.status).send({
|
|
73
86
|
error: { message: `${provider.name}: ${errText.slice(0, 500)}` },
|
|
74
87
|
});
|
|
75
88
|
}
|
|
76
89
|
if (body.stream) {
|
|
77
|
-
|
|
90
|
+
logReq(originalModel, provider.name, model, reqStart, 200, true);
|
|
78
91
|
return reply.headers({
|
|
79
92
|
'Content-Type': 'text/event-stream',
|
|
80
93
|
'Cache-Control': 'no-cache',
|
|
@@ -82,11 +95,11 @@ export async function modelGatewayRoutes(fastify) {
|
|
|
82
95
|
}).send(upstreamResp.body);
|
|
83
96
|
}
|
|
84
97
|
const data = await upstreamResp.json();
|
|
85
|
-
|
|
98
|
+
logReq(originalModel, provider.name, model, reqStart, 200, false);
|
|
86
99
|
reply.send(data);
|
|
87
100
|
}
|
|
88
101
|
catch (err) {
|
|
89
|
-
|
|
102
|
+
logReq(originalModel, provider.name, model, reqStart, 502, !!body.stream, err.message);
|
|
90
103
|
console.error(`[ModelGW] Error calling ${provider.name}:`, err.message);
|
|
91
104
|
reply.code(502).send({
|
|
92
105
|
error: { message: `Provider error: ${err.message}` },
|
|
@@ -95,6 +108,11 @@ export async function modelGatewayRoutes(fastify) {
|
|
|
95
108
|
});
|
|
96
109
|
// POST /v1/responses — Codex Desktop Responses API 适配
|
|
97
110
|
fastify.post('/v1/responses', async (request, reply) => {
|
|
111
|
+
const ip = request.ip || '127.0.0.1';
|
|
112
|
+
const rl = checkRateLimit('/v1/responses', ip);
|
|
113
|
+
if (!rl.allowed) {
|
|
114
|
+
return reply.code(429).header('Retry-After', String(rl.resetSeconds)).send({ error: { message: 'Rate limit exceeded' } });
|
|
115
|
+
}
|
|
98
116
|
const body = request.body;
|
|
99
117
|
if (!body || !body.input) {
|
|
100
118
|
return reply.code(400).send({ error: { message: 'input is required' } });
|
|
@@ -109,17 +127,17 @@ export async function modelGatewayRoutes(fastify) {
|
|
|
109
127
|
const model = remapModel(originalModel, provider.name);
|
|
110
128
|
body.model = model;
|
|
111
129
|
console.log(`[ModelGW:Responses] ${originalModel} → ${provider.name}/${model}`);
|
|
112
|
-
const
|
|
130
|
+
const reqStart = Date.now();
|
|
113
131
|
try {
|
|
114
132
|
const upstreamResp = await callResponsesApi(provider, body);
|
|
115
133
|
if (!upstreamResp.ok) {
|
|
116
134
|
const errText = await upstreamResp.text();
|
|
117
|
-
|
|
135
|
+
logReq(originalModel, provider.name, model, reqStart, upstreamResp.status, !!body.stream, errText.slice(0, 200));
|
|
118
136
|
return reply.code(upstreamResp.status).send({
|
|
119
137
|
error: { message: `${provider.name}: ${errText.slice(0, 500)}` },
|
|
120
138
|
});
|
|
121
139
|
}
|
|
122
|
-
|
|
140
|
+
logReq(originalModel, provider.name, model, reqStart, 200, !!body.stream);
|
|
123
141
|
if (body.stream) {
|
|
124
142
|
return reply.headers({
|
|
125
143
|
'Content-Type': 'text/event-stream',
|
|
@@ -130,7 +148,7 @@ export async function modelGatewayRoutes(fastify) {
|
|
|
130
148
|
reply.send(data);
|
|
131
149
|
}
|
|
132
150
|
catch (err) {
|
|
133
|
-
|
|
151
|
+
logReq(originalModel, provider.name, model, reqStart, 502, !!body.stream, err.message);
|
|
134
152
|
console.error(`[ModelGW:Responses] Error:`, err.message);
|
|
135
153
|
reply.code(502).send({
|
|
136
154
|
error: { message: `Provider error: ${err.message}` },
|