@starlink-awaken/agentmesh 1.3.0 → 1.3.2

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.0';
10
+ const VERSION = '1.3.2';
11
11
  const BANNER = `
12
12
  █████╗ ██████╗ ███████╗███╗ ██╗████████╗
13
13
  ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝
@@ -1,4 +1,3 @@
1
1
  import type { ChatCompletionRequest, ResolvedProvider } from './types.js';
2
2
  export declare function callChatCompletions(provider: ResolvedProvider, request: ChatCompletionRequest): Promise<Response>;
3
3
  export declare function callResponsesApi(provider: ResolvedProvider, body: Record<string, any>): Promise<Response>;
4
- export declare function buildStreamingResponse(upstreamResp: Response): Response;
@@ -1,6 +1,5 @@
1
1
  import { circuitBreakerRegistry } from './circuit-breaker.js';
2
2
  import { withRetry, isRetryable } from './retry.js';
3
- // 所有目标 Provider 都兼容 OpenAI API 格式,统一客户端即可
4
3
  export async function callChatCompletions(provider, request) {
5
4
  const { base_url, api_key, name: providerName } = provider;
6
5
  const { model, messages, stream, temperature, max_tokens, tools, tool_choice } = request;
@@ -19,22 +18,18 @@ export async function callChatCompletions(provider, request) {
19
18
  'Content-Type': 'application/json',
20
19
  Authorization: `Bearer ${api_key}`,
21
20
  };
22
- // OpenRouter 需要额外的头部
23
21
  if (providerName === 'openrouter') {
24
22
  headers['HTTP-Referer'] = 'http://127.0.0.1:3000';
25
23
  headers['X-Title'] = 'Agent Mesh Gateway';
26
24
  }
27
25
  const url = `${base_url.replace(/\/$/, '')}/chat/completions`;
28
- // 熔断器检查
29
26
  if (!circuitBreakerRegistry.canRequest(providerName)) {
30
27
  throw new Error(`Circuit breaker open for ${providerName}`);
31
28
  }
32
29
  try {
33
30
  const resp = await withRetry(providerName, async () => {
34
31
  const r = await fetch(url, {
35
- method: 'POST',
36
- headers,
37
- body: JSON.stringify(body),
32
+ method: 'POST', headers, body: JSON.stringify(body),
38
33
  signal: AbortSignal.timeout(120_000),
39
34
  });
40
35
  return r;
@@ -42,7 +37,6 @@ export async function callChatCompletions(provider, request) {
42
37
  console.warn(`[Retry] ${providerName} attempt ${attempt} after ${status} — retrying in ${delayMs}ms`);
43
38
  });
44
39
  if (!resp.ok && isRetryable(resp.status)) {
45
- // Retry logic already handled in withRetry, but if we get here after max retries:
46
40
  circuitBreakerRegistry.recordFailure(providerName);
47
41
  }
48
42
  else if (resp.ok) {
@@ -58,38 +52,260 @@ export async function callChatCompletions(provider, request) {
58
52
  throw err;
59
53
  }
60
54
  }
55
+ // ============================================================================
56
+ // Responses API → Chat Completions 双向转换(含 tool_calls 往返)
57
+ // ============================================================================
61
58
  export async function callResponsesApi(provider, body) {
62
- // Responses API Chat Completions 转换
63
- const messages = convertResponsesInputToMessages(body.input || []);
59
+ const messages = convertInputToMessages(body.input || []);
64
60
  if (body.instructions) {
65
61
  messages.unshift({ role: 'system', content: body.instructions });
66
62
  }
67
- return callChatCompletions(provider, {
63
+ // 转换 tools 定义(Codex 的 tool schema → OpenAI format)
64
+ const tools = convertToolSchemas(body.tools);
65
+ const chatResp = await callChatCompletions(provider, {
68
66
  model: body.model,
69
67
  messages,
70
68
  stream: body.stream,
71
- tools: body.tools,
69
+ tools,
70
+ tool_choice: body.tool_choice,
71
+ });
72
+ // 非流式:直接转换响应
73
+ if (!body.stream) {
74
+ const ccData = (await chatResp.json());
75
+ return new Response(JSON.stringify(convertChatToResponses(ccData)), {
76
+ status: 200,
77
+ headers: { 'Content-Type': 'application/json' },
78
+ });
79
+ }
80
+ // 流式:解析 SSE → 转换 → 重新打包 SSE
81
+ const transformed = transformSSEStream(chatResp.body);
82
+ return new Response(transformed, {
83
+ status: 200,
84
+ headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' },
72
85
  });
73
86
  }
74
- function convertResponsesInputToMessages(input) {
87
+ // ============================================================================
88
+ // 输入转换: Responses input[] → Chat messages[]
89
+ // ============================================================================
90
+ function convertInputToMessages(input) {
75
91
  const messages = [];
76
92
  for (const item of input) {
77
- if (item.role === 'system') {
78
- messages.push({ role: 'system', content: extractTextContent(item.content) });
79
- }
80
- else if (item.role === 'user') {
81
- messages.push({ role: 'user', content: extractTextContent(item.content) });
82
- }
83
- else if (item.role === 'assistant') {
84
- messages.push({ role: 'assistant', content: extractTextContent(item.content) });
85
- }
86
- else if (item.type === 'message') {
87
- const role = item.role || 'user';
88
- messages.push({ role, content: extractTextContent(item.content) });
93
+ switch (item.type || item.role) {
94
+ // 标准消息类型
95
+ case 'message':
96
+ messages.push({ role: item.role || 'user', content: extractTextContent(item.content) });
97
+ break;
98
+ // Function call(Assistant 侧发起工具调用)
99
+ case 'function_call': {
100
+ messages.push({
101
+ role: 'assistant',
102
+ content: null,
103
+ tool_calls: [{
104
+ id: item.call_id,
105
+ type: 'function',
106
+ function: {
107
+ name: item.name,
108
+ arguments: typeof item.arguments === 'string' ? item.arguments : JSON.stringify(item.arguments),
109
+ },
110
+ }],
111
+ });
112
+ break;
113
+ }
114
+ // Function call output(Tool 返回结果)
115
+ case 'function_call_output':
116
+ messages.push({
117
+ role: 'tool',
118
+ tool_call_id: item.call_id,
119
+ content: typeof item.output === 'string' ? item.output : JSON.stringify(item.output),
120
+ });
121
+ break;
122
+ // 简单角色
123
+ case 'system':
124
+ messages.push({ role: 'system', content: extractTextContent(item.content) });
125
+ break;
126
+ case 'user':
127
+ messages.push({ role: 'user', content: extractTextContent(item.content) });
128
+ break;
129
+ case 'assistant':
130
+ messages.push({ role: 'assistant', content: extractTextContent(item.content) });
131
+ break;
132
+ default:
133
+ // 回退: role 字段
134
+ if (item.role) {
135
+ messages.push({ role: item.role, content: extractTextContent(item.content) });
136
+ }
89
137
  }
90
138
  }
91
139
  return messages;
92
140
  }
141
+ // ============================================================================
142
+ // 输出转换: Chat completions response → Responses API response
143
+ // ============================================================================
144
+ function convertChatToResponses(ccData) {
145
+ const choice = ccData.choices?.[0];
146
+ if (!choice) {
147
+ return { id: ccData.id, object: 'response', model: ccData.model, output: [], usage: ccData.usage };
148
+ }
149
+ const output = [];
150
+ // 文本回复
151
+ if (choice.message?.content) {
152
+ output.push({
153
+ type: 'message',
154
+ role: 'assistant',
155
+ content: [{ type: 'output_text', text: choice.message.content }],
156
+ });
157
+ }
158
+ // 工具调用
159
+ if (choice.message?.tool_calls) {
160
+ for (const tc of choice.message.tool_calls) {
161
+ output.push({
162
+ type: 'function_call',
163
+ call_id: tc.id,
164
+ name: tc.function?.name,
165
+ arguments: tc.function?.arguments,
166
+ });
167
+ }
168
+ }
169
+ // finish_reason = 'tool_calls' 表示等待 tool 结果(Codex 需要此信息)
170
+ const status = choice.finish_reason === 'tool_calls' ? 'requires_action' : 'completed';
171
+ return {
172
+ id: ccData.id,
173
+ object: 'response',
174
+ model: ccData.model,
175
+ status,
176
+ output,
177
+ usage: ccData.usage,
178
+ };
179
+ }
180
+ // ============================================================================
181
+ // SSE 流式转换: Chat SSE → Responses SSE
182
+ // ============================================================================
183
+ function transformSSEStream(upstreamBody) {
184
+ const encoder = new TextEncoder();
185
+ let buffer = '';
186
+ let responseId = '';
187
+ let modelName = '';
188
+ let contentBuffer = '';
189
+ let toolCallAccum = {};
190
+ return new ReadableStream({
191
+ async start(controller) {
192
+ const reader = upstreamBody.getReader();
193
+ const decoder = new TextDecoder();
194
+ try {
195
+ while (true) {
196
+ const { done, value } = await reader.read();
197
+ if (done)
198
+ break;
199
+ buffer += decoder.decode(value, { stream: true });
200
+ const lines = buffer.split('\n');
201
+ buffer = lines.pop() || '';
202
+ for (const line of lines) {
203
+ if (!line.startsWith('data: '))
204
+ continue;
205
+ const data = line.slice(6).trim();
206
+ if (data === '[DONE]') {
207
+ // 发送最终事件
208
+ const finalEvt = buildResponseEvent(responseId, modelName, contentBuffer, toolCallAccum, true);
209
+ controller.enqueue(encoder.encode(finalEvt));
210
+ controller.enqueue(encoder.encode('data: [DONE]\n\n'));
211
+ continue;
212
+ }
213
+ try {
214
+ const chunk = JSON.parse(data);
215
+ responseId = chunk.id || responseId;
216
+ modelName = chunk.model || modelName;
217
+ const delta = chunk.choices?.[0]?.delta;
218
+ if (!delta)
219
+ continue;
220
+ // 文本增量
221
+ if (delta.content) {
222
+ contentBuffer += delta.content;
223
+ const evt = `data: ${JSON.stringify({
224
+ type: 'response.output_text.delta',
225
+ delta: delta.content,
226
+ })}\n\n`;
227
+ controller.enqueue(encoder.encode(evt));
228
+ }
229
+ // 工具调用增量
230
+ if (delta.tool_calls) {
231
+ for (const tc of delta.tool_calls) {
232
+ if (!toolCallAccum[tc.index]) {
233
+ toolCallAccum[tc.index] = {
234
+ id: tc.id || '',
235
+ type: 'function',
236
+ function: { name: tc.function?.name || '', arguments: '' },
237
+ };
238
+ }
239
+ if (tc.function?.arguments) {
240
+ toolCallAccum[tc.index].function.arguments += tc.function.arguments;
241
+ }
242
+ if (tc.id)
243
+ toolCallAccum[tc.index].id = tc.id;
244
+ if (tc.function?.name)
245
+ toolCallAccum[tc.index].function.name = tc.function.name;
246
+ }
247
+ // 发送 tool_call delta 事件
248
+ const tcEvt = `data: ${JSON.stringify({
249
+ type: 'response.function_call_arguments.delta',
250
+ tool_calls: Object.values(toolCallAccum),
251
+ })}\n\n`;
252
+ controller.enqueue(encoder.encode(tcEvt));
253
+ }
254
+ }
255
+ catch {
256
+ // 非 JSON 行直接透传
257
+ controller.enqueue(encoder.encode(line + '\n'));
258
+ }
259
+ }
260
+ }
261
+ }
262
+ catch (err) {
263
+ controller.error(err);
264
+ }
265
+ finally {
266
+ reader.releaseLock();
267
+ controller.close();
268
+ }
269
+ },
270
+ });
271
+ }
272
+ function buildResponseEvent(id, model, content, toolCalls, isFinal) {
273
+ const output = [];
274
+ if (content) {
275
+ output.push({ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: content }] });
276
+ }
277
+ for (const tc of Object.values(toolCalls)) {
278
+ output.push({
279
+ type: 'function_call',
280
+ call_id: tc.id,
281
+ name: tc.function.name,
282
+ arguments: tc.function.arguments,
283
+ });
284
+ }
285
+ return `data: ${JSON.stringify({
286
+ type: isFinal ? 'response.completed' : 'response.output_text.delta',
287
+ response: isFinal ? { id, model, object: 'response', status: 'completed', output } : undefined,
288
+ delta: isFinal ? undefined : content,
289
+ })}\n\n`;
290
+ }
291
+ // ============================================================================
292
+ // 工具定义转换
293
+ // ============================================================================
294
+ function convertToolSchemas(tools) {
295
+ if (!tools || !Array.isArray(tools))
296
+ return undefined;
297
+ return tools.map(t => ({
298
+ type: 'function',
299
+ function: {
300
+ name: t.name,
301
+ description: t.description,
302
+ parameters: t.parameters || t.input_schema,
303
+ },
304
+ }));
305
+ }
306
+ // ============================================================================
307
+ // 辅助函数
308
+ // ============================================================================
93
309
  function extractTextContent(content) {
94
310
  if (typeof content === 'string')
95
311
  return content;
@@ -101,13 +317,3 @@ function extractTextContent(content) {
101
317
  }
102
318
  return String(content || '');
103
319
  }
104
- export function buildStreamingResponse(upstreamResp) {
105
- return new Response(upstreamResp.body, {
106
- status: upstreamResp.status,
107
- headers: {
108
- 'Content-Type': 'text/event-stream',
109
- 'Cache-Control': 'no-cache',
110
- Connection: 'keep-alive',
111
- },
112
- });
113
- }
@@ -1,7 +1,7 @@
1
1
  import { isProviderAvailable } from './quota.js';
2
2
  import { circuitBreakerRegistry } from './circuit-breaker.js';
3
- // 模型名重映射:对外模型名 → 实际 Provider 的模型名
4
- const MODEL_ALIASES = {
3
+ // 模型名重映射:对外模型名 → 实际 Provider 的模型名(可从 config 覆盖)
4
+ let modelAliases = {
5
5
  deepseek: {
6
6
  'gpt-5.3-codex': 'deepseek-v4-pro',
7
7
  'gpt-5.4': 'deepseek-v4-pro',
@@ -13,6 +13,13 @@ const MODEL_ALIASES = {
13
13
  let config;
14
14
  export function initModelRouter(cfg) {
15
15
  config = cfg;
16
+ // 从 config 加载模型别名(覆盖默认)
17
+ if (cfg.model_aliases) {
18
+ for (const [key, val] of Object.entries(cfg.model_aliases)) {
19
+ modelAliases.deepseek = modelAliases.deepseek || {};
20
+ modelAliases.deepseek[key] = val;
21
+ }
22
+ }
16
23
  }
17
24
  export function getConfig() {
18
25
  return config;
@@ -24,7 +31,7 @@ export function resolveProvider(model) {
24
31
  // 1. 按 model_routing 配置查找
25
32
  const routingEntries = Object.entries(config.model_routing);
26
33
  for (const [pattern, providers] of routingEntries) {
27
- if (model.includes(pattern)) {
34
+ if (model.startsWith(pattern)) {
28
35
  for (const providerName of providers) {
29
36
  const providerCfg = config.providers[providerName];
30
37
  if (!providerCfg)
@@ -79,7 +86,7 @@ export function resolveProvider(model) {
79
86
  return null;
80
87
  }
81
88
  export function remapModel(model, providerName) {
82
- return MODEL_ALIASES[providerName]?.[model] || model;
89
+ return modelAliases[providerName]?.[model] || model;
83
90
  }
84
91
  function resolveApiKey(_name, providerCfg) {
85
92
  if (providerCfg.api_key && providerCfg.api_key !== '') {
@@ -109,44 +109,28 @@ export async function modelGatewayRoutes(fastify) {
109
109
  const model = remapModel(originalModel, provider.name);
110
110
  body.model = model;
111
111
  console.log(`[ModelGW:Responses] ${originalModel} → ${provider.name}/${model}`);
112
+ const reqStart2 = Date.now();
112
113
  try {
113
114
  const upstreamResp = await callResponsesApi(provider, body);
114
115
  if (!upstreamResp.ok) {
115
116
  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 });
116
118
  return reply.code(upstreamResp.status).send({
117
119
  error: { message: `${provider.name}: ${errText.slice(0, 500)}` },
118
120
  });
119
121
  }
122
+ recordRequest({ timestamp: Date.now(), model: originalModel, provider: provider.name, actualModel: model, latencyMs: Date.now() - reqStart2, status: 200, streaming: !!body.stream });
120
123
  if (body.stream) {
121
124
  return reply.headers({
122
125
  'Content-Type': 'text/event-stream',
123
126
  'Cache-Control': 'no-cache',
124
127
  }).send(upstreamResp.body);
125
128
  }
126
- // Chat Completions 响应转回 Responses API 格式
127
- const ccData = (await upstreamResp.json());
128
- const choice = ccData.choices?.[0];
129
- const responsesData = {
130
- id: ccData.id,
131
- object: 'response',
132
- model: ccData.model,
133
- output: [
134
- {
135
- type: 'message',
136
- role: 'assistant',
137
- content: [
138
- {
139
- type: 'output_text',
140
- text: choice?.message?.content || '',
141
- },
142
- ],
143
- },
144
- ],
145
- usage: ccData.usage,
146
- };
147
- reply.send(responsesData);
129
+ const data = await upstreamResp.json();
130
+ reply.send(data);
148
131
  }
149
132
  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 });
150
134
  console.error(`[ModelGW:Responses] Error:`, err.message);
151
135
  reply.code(502).send({
152
136
  error: { message: `Provider error: ${err.message}` },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starlink-awaken/agentmesh",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Unified Agent Gateway - Multi-Agent Scheduler and Router",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",