@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 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.3.2';
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.unshift(log);
24
+ recentRequests.push(log);
25
25
  if (recentRequests.length > MAX_RECENT)
26
- recentRequests.pop();
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(0, 20).map(r => ({
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
- // SSE 流式转换: Chat SSE → Responses SSE
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
- let toolCallAccum = {};
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
- const lines = buffer.split('\n');
201
- buffer = lines.pop() || '';
202
- for (const line of lines) {
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(encoder.encode(finalEvt));
210
- controller.enqueue(encoder.encode('data: [DONE]\n\n'));
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: 'response.output_text.delta',
208
+ type: EVENT_DELTA,
225
209
  delta: delta.content,
226
210
  })}\n\n`;
227
- controller.enqueue(encoder.encode(evt));
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: 'response.function_call_arguments.delta',
232
+ type: EVENT_TOOL_DELTA,
250
233
  tool_calls: Object.values(toolCallAccum),
251
234
  })}\n\n`;
252
- controller.enqueue(encoder.encode(tcEvt));
235
+ controller.enqueue(sseEncoder.encode(tcEvt));
253
236
  }
254
237
  }
255
238
  catch {
256
239
  // 非 JSON 行直接透传
257
- controller.enqueue(encoder.encode(line + '\n'));
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 ? 'response.completed' : 'response.output_text.delta',
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
- for (const [key, val] of Object.entries(cfg.model_aliases)) {
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[key] = val;
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 && upstreamResp.status !== 200) {
81
+ if (!upstreamResp.ok) {
69
82
  const errText = await upstreamResp.text();
70
- recordRequest({ timestamp: Date.now(), model: originalModel, provider: provider.name, actualModel: model, latencyMs: Date.now() - reqStart, status: upstreamResp.status, error: errText.slice(0, 200), streaming: !!body.stream });
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
- recordRequest({ timestamp: Date.now(), model: originalModel, provider: provider.name, actualModel: model, latencyMs: Date.now() - reqStart, status: 200, streaming: true });
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
- recordRequest({ timestamp: Date.now(), model: originalModel, provider: provider.name, actualModel: model, latencyMs: Date.now() - reqStart, status: 200, streaming: false });
98
+ logReq(originalModel, provider.name, model, reqStart, 200, false);
86
99
  reply.send(data);
87
100
  }
88
101
  catch (err) {
89
- recordRequest({ timestamp: Date.now(), model: originalModel, provider: provider.name, actualModel: model, latencyMs: Date.now() - reqStart, status: 502, error: err.message, streaming: !!body.stream });
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 reqStart2 = Date.now();
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
- recordRequest({ timestamp: Date.now(), model: originalModel, provider: provider.name, actualModel: model, latencyMs: Date.now() - reqStart2, status: upstreamResp.status, error: errText.slice(0, 200), streaming: !!body.stream });
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
- recordRequest({ timestamp: Date.now(), model: originalModel, provider: provider.name, actualModel: model, latencyMs: Date.now() - reqStart2, status: 200, streaming: !!body.stream });
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
- recordRequest({ timestamp: Date.now(), model: originalModel, provider: provider.name, actualModel: model, latencyMs: Date.now() - reqStart2, status: 502, error: err.message, streaming: !!body.stream });
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}` },
@@ -9,6 +9,7 @@ export interface ModelGatewayConfig {
9
9
  providers: Record<string, ModelProviderConfig>;
10
10
  fallback_chain: string[];
11
11
  model_routing: Record<string, string[]>;
12
+ model_aliases?: Record<string, string>;
12
13
  }
13
14
  export interface ResolvedProvider {
14
15
  name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starlink-awaken/agentmesh",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "Unified Agent Gateway - Multi-Agent Scheduler and Router",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",