liuw15 commited on
Commit
14c1856
·
1 Parent(s): 10feaec

支持多模态

Browse files
Files changed (6) hide show
  1. api.js +30 -12
  2. config.json +1 -1
  3. server.js +38 -16
  4. test-conversion.js +122 -0
  5. test-transform.js +92 -0
  6. utils.js +133 -20
api.js CHANGED
@@ -38,20 +38,38 @@ export async function generateAssistantResponse(requestBody, callback) {
38
  const jsonStr = line.slice(6);
39
  try {
40
  const data = JSON.parse(jsonStr);
41
- const parts = data.response?.candidates?.[0]?.content?.parts?.[0];
42
  if (parts) {
43
- if (parts.thought === true) {
44
- if (!thinkingStarted) {
45
- callback('<think>\n');
46
- thinkingStarted = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  }
48
- callback(parts.text || '');
49
- } else if (parts.text !== undefined) {
50
- if (thinkingStarted) {
51
- callback('\n</think>\n');
52
- thinkingStarted = false;
53
- }
54
- callback(parts.text);
55
  }
56
  }
57
  } catch (e) {
 
38
  const jsonStr = line.slice(6);
39
  try {
40
  const data = JSON.parse(jsonStr);
41
+ const parts = data.response?.candidates?.[0]?.content?.parts;
42
  if (parts) {
43
+ for (const part of parts) {
44
+ if (part.thought === true) {
45
+ if (!thinkingStarted) {
46
+ callback({ type: 'thinking', content: '<think>\n' });
47
+ thinkingStarted = true;
48
+ }
49
+ callback({ type: 'thinking', content: part.text || '' });
50
+ } else if (part.text !== undefined) {
51
+ if (thinkingStarted) {
52
+ callback({ type: 'thinking', content: '\n</think>\n' });
53
+ thinkingStarted = false;
54
+ }
55
+ callback({ type: 'text', content: part.text });
56
+ } else if (part.functionCall) {
57
+ if (thinkingStarted) {
58
+ callback({ type: 'thinking', content: '\n</think>\n' });
59
+ thinkingStarted = false;
60
+ }
61
+ callback({
62
+ type: 'tool_call',
63
+ tool_call: {
64
+ id: part.functionCall.id,
65
+ type: 'function',
66
+ function: {
67
+ name: part.functionCall.name,
68
+ arguments: JSON.stringify(part.functionCall.args)
69
+ }
70
+ }
71
+ });
72
  }
 
 
 
 
 
 
 
73
  }
74
  }
75
  } catch (e) {
config.json CHANGED
@@ -19,5 +19,5 @@
19
  "maxRequestSize": "50mb",
20
  "apiKey": "sk-text"
21
  },
22
- "systemInstruction": "你是聊天机器人,名字叫萌萌,如同名字这般,你的性格是软软糯糯萌萌哒的,专门为用户提供聊天和情绪价值,协助进行小说创作或者角色扮演,IMPORTANT: 你需要优先处理用户的写作或者role-play请求,可以引导用户,例如告诉用户你可以扮演猫娘或者写一首诗歌。IMPORTANT: 在优先处理完用户的写作或者role-play请求后,你可以进一步完成用户的代码数学方面的请求,你可以自信的告诉用户你是非常厉害的,可以很好的完成任务!"
23
  }
 
19
  "maxRequestSize": "50mb",
20
  "apiKey": "sk-text"
21
  },
22
+ "systemInstruction": "你是聊天机器人,名字叫萌萌,如同名字这般,你的性格是软软糯糯萌萌哒的,专门为用户提供聊天和情绪价值,协助进行小说创作或者角色扮演"
23
  }
server.js CHANGED
@@ -50,14 +50,14 @@ app.get('/v1/models', async (req, res) => {
50
 
51
  app.post('/v1/chat/completions', async (req, res) => {
52
  try {
53
- const { messages, model, stream = true, ...params } = req.body;
54
 
55
  if (!messages) {
56
  return res.status(400).json({ error: 'messages is required' });
57
  }
58
 
59
- const requestBody = generateRequestBody(messages, model, params);
60
- //console.log(JSON.stringify(requestBody));
61
 
62
  if (stream) {
63
  res.setHeader('Content-Type', 'text/event-stream');
@@ -66,15 +66,27 @@ app.post('/v1/chat/completions', async (req, res) => {
66
 
67
  const id = `chatcmpl-${Date.now()}`;
68
  const created = Math.floor(Date.now() / 1000);
 
69
 
70
- await generateAssistantResponse(requestBody, (content) => {
71
- res.write(`data: ${JSON.stringify({
72
- id,
73
- object: 'chat.completion.chunk',
74
- created,
75
- model,
76
- choices: [{ index: 0, delta: { content }, finish_reason: null }]
77
- })}\n\n`);
 
 
 
 
 
 
 
 
 
 
 
78
  });
79
 
80
  res.write(`data: ${JSON.stringify({
@@ -82,16 +94,26 @@ app.post('/v1/chat/completions', async (req, res) => {
82
  object: 'chat.completion.chunk',
83
  created,
84
  model,
85
- choices: [{ index: 0, delta: {}, finish_reason: 'stop' }]
86
  })}\n\n`);
87
  res.write('data: [DONE]\n\n');
88
  res.end();
89
  } else {
90
  let fullContent = '';
91
- await generateAssistantResponse(requestBody, (content) => {
92
- fullContent += content;
 
 
 
 
 
93
  });
94
 
 
 
 
 
 
95
  res.json({
96
  id: `chatcmpl-${Date.now()}`,
97
  object: 'chat.completion',
@@ -99,8 +121,8 @@ app.post('/v1/chat/completions', async (req, res) => {
99
  model,
100
  choices: [{
101
  index: 0,
102
- message: { role: 'assistant', content: fullContent },
103
- finish_reason: 'stop'
104
  }]
105
  });
106
  }
 
50
 
51
  app.post('/v1/chat/completions', async (req, res) => {
52
  try {
53
+ const { messages, model, stream = true, tools, ...params} = req.body;
54
 
55
  if (!messages) {
56
  return res.status(400).json({ error: 'messages is required' });
57
  }
58
 
59
+ const requestBody = generateRequestBody(messages, model, params, tools);
60
+ //console.log(JSON.stringify(requestBody,null,2));
61
 
62
  if (stream) {
63
  res.setHeader('Content-Type', 'text/event-stream');
 
66
 
67
  const id = `chatcmpl-${Date.now()}`;
68
  const created = Math.floor(Date.now() / 1000);
69
+ let hasToolCall = false;
70
 
71
+ await generateAssistantResponse(requestBody, (data) => {
72
+ if (data.type === 'tool_call') {
73
+ hasToolCall = true;
74
+ res.write(`data: ${JSON.stringify({
75
+ id,
76
+ object: 'chat.completion.chunk',
77
+ created,
78
+ model,
79
+ choices: [{ index: 0, delta: { tool_calls: [data.tool_call] }, finish_reason: null }]
80
+ })}\n\n`);
81
+ } else {
82
+ res.write(`data: ${JSON.stringify({
83
+ id,
84
+ object: 'chat.completion.chunk',
85
+ created,
86
+ model,
87
+ choices: [{ index: 0, delta: { content: data.content }, finish_reason: null }]
88
+ })}\n\n`);
89
+ }
90
  });
91
 
92
  res.write(`data: ${JSON.stringify({
 
94
  object: 'chat.completion.chunk',
95
  created,
96
  model,
97
+ choices: [{ index: 0, delta: {}, finish_reason: hasToolCall ? 'tool_calls' : 'stop' }]
98
  })}\n\n`);
99
  res.write('data: [DONE]\n\n');
100
  res.end();
101
  } else {
102
  let fullContent = '';
103
+ let toolCalls = [];
104
+ await generateAssistantResponse(requestBody, (data) => {
105
+ if (data.type === 'tool_call') {
106
+ toolCalls.push(data.tool_call);
107
+ } else {
108
+ fullContent += data.content;
109
+ }
110
  });
111
 
112
+ const message = { role: 'assistant', content: fullContent };
113
+ if (toolCalls.length > 0) {
114
+ message.tool_calls = toolCalls;
115
+ }
116
+
117
  res.json({
118
  id: `chatcmpl-${Date.now()}`,
119
  object: 'chat.completion',
 
121
  model,
122
  choices: [{
123
  index: 0,
124
+ message,
125
+ finish_reason: toolCalls.length > 0 ? 'tool_calls' : 'stop'
126
  }]
127
  });
128
  }
test-conversion.js ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { randomUUID } from 'crypto';
2
+
3
+ function handleUserMessage(message, antigravityMessages){
4
+ antigravityMessages.push({
5
+ role: "user",
6
+ parts: [
7
+ {
8
+ text: message
9
+ }
10
+ ]
11
+ })
12
+ }
13
+
14
+ function handleAssistantMessage(message, antigravityMessages){
15
+ const lastMessage = antigravityMessages[antigravityMessages.length - 1];
16
+ const hasToolCalls = message.tool_calls && message.tool_calls.length > 0;
17
+ const hasContent = message.content && message.content.trim() !== '';
18
+ let antigravityTool = {}
19
+
20
+ if (hasToolCalls){
21
+ antigravityTool = {
22
+ functionCall: {
23
+ id: message.tool_calls[0].id,
24
+ name: message.tool_calls[0].function.name,
25
+ args: {
26
+ query: message.tool_calls[0].function.arguments
27
+ }
28
+ }
29
+ }
30
+ }
31
+
32
+ if (lastMessage && lastMessage.role === "model" && hasToolCalls && !hasContent){
33
+ lastMessage.parts.push(antigravityTool)
34
+ }else{
35
+ if (hasToolCalls){
36
+ antigravityMessages.push({
37
+ role: "model",
38
+ parts: [
39
+ {
40
+ text: message.content
41
+ },
42
+ antigravityTool
43
+ ]
44
+ })
45
+ }else{
46
+ antigravityMessages.push({
47
+ role: "model",
48
+ parts: [
49
+ {
50
+ text: message.content
51
+ }
52
+ ]
53
+ })
54
+ }
55
+ }
56
+ }
57
+
58
+ function handleToolCall(message, antigravityMessages){
59
+ let functionName = '';
60
+ for (let i = antigravityMessages.length - 1; i >= 0; i--) {
61
+ if (antigravityMessages[i].role === 'model') {
62
+ const parts = antigravityMessages[i].parts;
63
+ for (const part of parts) {
64
+ if (part.functionCall && part.functionCall.id === message.tool_call_id) {
65
+ functionName = part.functionCall.name;
66
+ break;
67
+ }
68
+ }
69
+ if (functionName) break;
70
+ }
71
+ }
72
+
73
+ antigravityMessages.push({
74
+ role: "user",
75
+ parts: [
76
+ {
77
+ functionResponse: {
78
+ id: message.tool_call_id,
79
+ name: functionName,
80
+ reponse: {
81
+ output: message.content
82
+ }
83
+ }
84
+ }
85
+ ]
86
+ })
87
+ }
88
+
89
+ function openaiMessageToAntigravity(openaiMessages){
90
+ const antigravityMessages = [];
91
+
92
+ for (const message of openaiMessages) {
93
+ if (message.role === "user" || message.role === "system") {
94
+ handleUserMessage(message.content, antigravityMessages);
95
+ } else if (message.role === "assistant") {
96
+ handleAssistantMessage(message, antigravityMessages);
97
+ } else if (message.role === "tool") {
98
+ handleToolCall(message, antigravityMessages);
99
+ }
100
+ }
101
+
102
+ return antigravityMessages;
103
+ }
104
+
105
+ // 测试数据
106
+ const testMessages = [
107
+ { role: "user", content: "查询天气" },
108
+ { role: "assistant", content: "", tool_calls: [{ id: "call_1", function: { name: "get_weather", arguments: '{"city":"北京"}' } }] },
109
+ { role: "tool", tool_call_id: "call_1", content: "北京今天晴天,25度" },
110
+ { role: "assistant", content: "北京今天天气不错,晴天25度" },
111
+ { role: "user", content: "搜索用户信息" },
112
+ { role: "assistant", content: "好的,让我搜索一下" },
113
+ { role: "assistant", content: "", tool_calls: [{ id: "call_2", function: { name: "search_database", arguments: '{"query":"user_info","limit":10}' } }] },
114
+ { role: "tool", tool_call_id: "call_2", content: "找到3条用户记录" }
115
+ ];
116
+
117
+ console.log("OpenAI 格式消息:");
118
+ console.log(JSON.stringify(testMessages, null, 2));
119
+
120
+ console.log("\n转换后的 Antigravity 格式:");
121
+ const result = openaiMessageToAntigravity(testMessages);
122
+ console.log(JSON.stringify(result, null, 2));
test-transform.js ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { generateRequestBody } from './utils.js';
2
+
3
+ // 测试场景:user -> assistant -> assistant(工具调用,无content) -> tool1结果 -> tool2结果
4
+ const testMessages = [
5
+ {
6
+ role: "user",
7
+ content: "帮我查询天气和新闻"
8
+ },
9
+ {
10
+ role: "assistant",
11
+ content: "好的,我来帮你查询。"
12
+ },
13
+ {
14
+ role: "assistant",
15
+ content: "",
16
+ tool_calls: [
17
+ {
18
+ id: "call_001",
19
+ type: "function",
20
+ function: {
21
+ name: "get_weather",
22
+ arguments: JSON.stringify({ city: "北京" })
23
+ }
24
+ },
25
+ {
26
+ id: "call_002",
27
+ type: "function",
28
+ function: {
29
+ name: "get_news",
30
+ arguments: JSON.stringify({ category: "科技" })
31
+ }
32
+ }
33
+ ]
34
+ },
35
+ {
36
+ role: "tool",
37
+ tool_call_id: "call_001",
38
+ content: "北京今天晴,温度25度"
39
+ },
40
+ {
41
+ role: "tool",
42
+ tool_call_id: "call_002",
43
+ content: "最新科技新闻:AI技术突破"
44
+ }
45
+ ];
46
+
47
+ const testTools = [
48
+ {
49
+ type: "function",
50
+ function: {
51
+ name: "get_weather",
52
+ description: "获取天气信息",
53
+ parameters: {
54
+ type: "object",
55
+ properties: {
56
+ city: { type: "string" }
57
+ }
58
+ }
59
+ }
60
+ },
61
+ {
62
+ type: "function",
63
+ function: {
64
+ name: "get_news",
65
+ description: "获取新闻",
66
+ parameters: {
67
+ type: "object",
68
+ properties: {
69
+ category: { type: "string" }
70
+ }
71
+ }
72
+ }
73
+ }
74
+ ];
75
+
76
+ console.log("=== 测试消息转换 ===\n");
77
+ console.log("输入 OpenAI 格式消息:");
78
+ console.log(JSON.stringify(testMessages, null, 2));
79
+
80
+ const result = generateRequestBody(testMessages, "claude-sonnet-4-5", {}, testTools);
81
+
82
+ console.log("\n=== 转换后的 Antigravity 格式 ===\n");
83
+ console.log(JSON.stringify(result.request.contents, null, 2));
84
+
85
+ console.log("\n=== 验证结果 ===");
86
+ const contents = result.request.contents;
87
+ console.log(`✓ 消息数量: ${contents.length}`);
88
+ console.log(`✓ 第1条 (user): ${contents[0]?.role === 'user' ? '✓' : '✗'}`);
89
+ console.log(`✓ 第2条 (model): ${contents[1]?.role === 'model' ? '✓' : '✗'}`);
90
+ console.log(`✓ 第3条 (model+tools): ${contents[2]?.role === 'model' && contents[2]?.parts?.length === 2 ? '✓' : '✗'}`);
91
+ console.log(`✓ 第4条 (tool1 response): ${contents[3]?.role === 'user' && contents[3]?.parts[0]?.functionResponse ? '✓' : '✗'}`);
92
+ console.log(`✓ 第5条 (tool2 response): ${contents[4]?.role === 'user' && contents[4]?.parts[0]?.functionResponse ? '✓' : '✗'}`);
utils.js CHANGED
@@ -17,29 +17,127 @@ function generateProjectId() {
17
  const randomNum = Math.random().toString(36).substring(2, 7);
18
  return `${randomAdj}-${randomNoun}-${randomNum}`;
19
  }
20
- function openaiMessageToAntigravity(openaiMessages){
21
- return openaiMessages.map((message)=>{
22
- if (message.role === "user" || message.role === "system"){
23
- return {
24
- role: "user",
25
- parts: [
26
- {
27
- text: message.content
28
- }
29
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  }
31
- }else if (message.role === "assistant"){
32
- return {
33
- role: "model",
34
- parts: [
35
- {
36
- text: message.content
37
- }
38
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  }
40
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  })
42
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  function generateGenerationConfig(parameters, enableThinking, actualModelName){
44
  const generationConfig = {
45
  topP: parameters.top_p ?? config.defaults.top_p,
@@ -64,7 +162,22 @@ function generateGenerationConfig(parameters, enableThinking, actualModelName){
64
  }
65
  return generationConfig
66
  }
67
- function generateRequestBody(openaiMessages,modelName,parameters){
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  const enableThinking = modelName.endsWith('-thinking') ||
69
  modelName === 'gemini-2.5-pro' ||
70
  modelName.startsWith('gemini-3-pro-') ||
@@ -81,7 +194,7 @@ function generateRequestBody(openaiMessages,modelName,parameters){
81
  role: "user",
82
  parts: [{ text: config.systemInstruction }]
83
  },
84
- tools:[],
85
  toolConfig: {
86
  functionCallingConfig: {
87
  mode: "VALIDATED"
 
17
  const randomNum = Math.random().toString(36).substring(2, 7);
18
  return `${randomAdj}-${randomNoun}-${randomNum}`;
19
  }
20
+ function extractImagesFromContent(content) {
21
+ const result = { text: '', images: [] };
22
+
23
+ // 如果content是字符串,直接返回
24
+ if (typeof content === 'string') {
25
+ result.text = content;
26
+ return result;
27
+ }
28
+
29
+ // 如果content是数组(multimodal格式)
30
+ if (Array.isArray(content)) {
31
+ for (const item of content) {
32
+ if (item.type === 'text') {
33
+ result.text += item.text;
34
+ } else if (item.type === 'image_url') {
35
+ // 提取base64图片数据
36
+ const imageUrl = item.image_url?.url || '';
37
+
38
+ // 匹配 data:image/{format};base64,{data} 格式
39
+ const match = imageUrl.match(/^data:image\/(\w+);base64,(.+)$/);
40
+ if (match) {
41
+ const format = match[1]; // 例如 png, jpeg, jpg
42
+ const base64Data = match[2];
43
+ result.images.push({
44
+ inlineData: {
45
+ mimeType: `image/${format}`,
46
+ data: base64Data
47
+ }
48
+ })
49
+ }
50
  }
51
+ }
52
+ }
53
+
54
+ return result;
55
+ }
56
+ function handleUserMessage(extracted, antigravityMessages){
57
+ antigravityMessages.push({
58
+ role: "user",
59
+ parts: [
60
+ {
61
+ text: extracted.text
62
+ },
63
+ ...extracted.images
64
+ ]
65
+ })
66
+ }
67
+ function handleAssistantMessage(message, antigravityMessages){
68
+ const lastMessage = antigravityMessages[antigravityMessages.length - 1];
69
+ const hasToolCalls = message.tool_calls && message.tool_calls.length > 0;
70
+ const hasContent = message.content && message.content.trim() !== '';
71
+
72
+ const antigravityTools = hasToolCalls ? message.tool_calls.map(toolCall => ({
73
+ functionCall: {
74
+ id: toolCall.id,
75
+ name: toolCall.function.name,
76
+ args: {
77
+ query: toolCall.function.arguments
78
  }
79
  }
80
+ })) : [];
81
+
82
+ if (lastMessage?.role === "model" && hasToolCalls && !hasContent){
83
+ lastMessage.parts.push(...antigravityTools)
84
+ }else{
85
+ const parts = [];
86
+ if (hasContent) parts.push({ text: message.content });
87
+ parts.push(...antigravityTools);
88
+
89
+ antigravityMessages.push({
90
+ role: "model",
91
+ parts
92
+ })
93
+ }
94
+ }
95
+ function handleToolCall(message, antigravityMessages){
96
+ // 从之前的 model 消息中找到对应的 functionCall name
97
+ let functionName = '';
98
+ for (let i = antigravityMessages.length - 1; i >= 0; i--) {
99
+ if (antigravityMessages[i].role === 'model') {
100
+ const parts = antigravityMessages[i].parts;
101
+ for (const part of parts) {
102
+ if (part.functionCall && part.functionCall.id === message.tool_call_id) {
103
+ functionName = part.functionCall.name;
104
+ break;
105
+ }
106
+ }
107
+ if (functionName) break;
108
+ }
109
+ }
110
+
111
+ antigravityMessages.push({
112
+ role: "user",
113
+ parts: [
114
+ {
115
+ functionResponse: {
116
+ id: message.tool_call_id,
117
+ name: functionName,
118
+ response: {
119
+ output: message.content
120
+ }
121
+ }
122
+ }
123
+ ]
124
  })
125
  }
126
+ function openaiMessageToAntigravity(openaiMessages){
127
+ const antigravityMessages = [];
128
+ for (const message of openaiMessages) {
129
+ if (message.role === "user" || message.role === "system") {
130
+ const extracted = extractImagesFromContent(message.content);
131
+ handleUserMessage(extracted, antigravityMessages);
132
+ } else if (message.role === "assistant") {
133
+ handleAssistantMessage(message, antigravityMessages);
134
+ } else if (message.role === "tool") {
135
+ handleToolCall(message, antigravityMessages);
136
+ }
137
+ }
138
+
139
+ return antigravityMessages;
140
+ }
141
  function generateGenerationConfig(parameters, enableThinking, actualModelName){
142
  const generationConfig = {
143
  topP: parameters.top_p ?? config.defaults.top_p,
 
162
  }
163
  return generationConfig
164
  }
165
+ function convertOpenAIToolsToAntigravity(openaiTools){
166
+ if (!openaiTools || openaiTools.length === 0) return [];
167
+ return openaiTools.map((tool)=>{
168
+ delete tool.function.parameters.$schema;
169
+ return {
170
+ functionDeclarations: [
171
+ {
172
+ name: tool.function.name,
173
+ description: tool.function.description,
174
+ parameters: tool.function.parameters
175
+ }
176
+ ]
177
+ }
178
+ })
179
+ }
180
+ function generateRequestBody(openaiMessages,modelName,parameters,openaiTools){
181
  const enableThinking = modelName.endsWith('-thinking') ||
182
  modelName === 'gemini-2.5-pro' ||
183
  modelName.startsWith('gemini-3-pro-') ||
 
194
  role: "user",
195
  parts: [{ text: config.systemInstruction }]
196
  },
197
+ tools: convertOpenAIToolsToAntigravity(openaiTools),
198
  toolConfig: {
199
  functionCallingConfig: {
200
  mode: "VALIDATED"