@8-/gemini-web-api 1.0.0 → 1.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/README.md +77 -24
- package/index.js +62 -876
- package/package.json +4 -2
- package/src/constant.js +89 -0
- package/src/cookieRead.js +65 -0
- package/src/modelDiscover.js +278 -0
- package/src/payloadBuild.js +115 -0
- package/src/router.js +302 -0
- package/src/serverStart.js +120 -0
- package/src/sessionState.js +70 -0
- package/src/sseResponse.js +166 -0
- package/src/streamExtract.js +75 -0
- package/src/toolHandle.js +332 -0
- package/test/modelDiscover.test.js +105 -0
- package/test/toolHandle.test.js +243 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
jsonFormat,
|
|
5
|
+
jsonParseSafe,
|
|
6
|
+
toolCallExtract,
|
|
7
|
+
toolFenceGateCreate,
|
|
8
|
+
toolPromptBuild,
|
|
9
|
+
toolResultFormat,
|
|
10
|
+
} from "../src/toolHandle.js";
|
|
11
|
+
import { conversationFormat } from "../src/payloadBuild.js";
|
|
12
|
+
|
|
13
|
+
test("toolPromptBuild generates prompt with function definitions", () => {
|
|
14
|
+
const tool_li = [
|
|
15
|
+
{
|
|
16
|
+
type: "function",
|
|
17
|
+
function: {
|
|
18
|
+
name: "get_weather",
|
|
19
|
+
description: "Get current weather",
|
|
20
|
+
parameters: {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
city: { type: "string" },
|
|
24
|
+
},
|
|
25
|
+
required: ["city"],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
prompt = toolPromptBuild(tool_li);
|
|
31
|
+
|
|
32
|
+
assert.ok(prompt.includes("# TOOLS"));
|
|
33
|
+
assert.ok(prompt.includes("get_weather"));
|
|
34
|
+
assert.ok(prompt.includes("```tool_call"));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("toolPromptBuild respects tool_choice constraints", () => {
|
|
38
|
+
const tool_li = [
|
|
39
|
+
{
|
|
40
|
+
type: "function",
|
|
41
|
+
function: { name: "get_weather", description: "Get weather" },
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
type: "function",
|
|
45
|
+
function: { name: "search", description: "Search web" },
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
assert.equal(toolPromptBuild(tool_li, "none"), "");
|
|
50
|
+
|
|
51
|
+
const required_prompt = toolPromptBuild(tool_li, "required");
|
|
52
|
+
assert.ok(required_prompt.includes("You MUST call at least one of the tools above"));
|
|
53
|
+
|
|
54
|
+
const forced_prompt = toolPromptBuild(tool_li, {
|
|
55
|
+
type: "function",
|
|
56
|
+
function: { name: "search" },
|
|
57
|
+
});
|
|
58
|
+
assert.ok(forced_prompt.includes('You MUST call the tool "search"'));
|
|
59
|
+
assert.ok(!forced_prompt.includes("get_weather"));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("toolResultFormat handles exit codes and empty results cleanly", () => {
|
|
63
|
+
const empty_res = toolResultFormat("exec", ""),
|
|
64
|
+
exit_zero_empty = toolResultFormat("exec", "Process exited with code 0\nOutput:\n"),
|
|
65
|
+
exit_zero_output = toolResultFormat("exec", "Process exited with code 0\nOutput:\nhello world"),
|
|
66
|
+
exit_err = toolResultFormat("exec", "Process exited with code 127\nOutput:\ncommand not found");
|
|
67
|
+
|
|
68
|
+
assert.ok(empty_res.includes("✅ 完成,无输出"));
|
|
69
|
+
assert.ok(exit_zero_empty.includes("✅ 命令成功(exit 0),无文本输出"));
|
|
70
|
+
assert.ok(exit_zero_output.includes("✅ 命令成功(exit 0)。输出:\nhello world"));
|
|
71
|
+
assert.ok(exit_err.includes("❌ 命令失败(exit 127)"));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("toolCallExtract parses fenced tool calls accurately", () => {
|
|
75
|
+
const text =
|
|
76
|
+
"Here is the data:\n" +
|
|
77
|
+
"```tool_call\n" +
|
|
78
|
+
'{"name": "get_weather", "arguments": {"city": "Tokyo"}}\n' +
|
|
79
|
+
"```\n" +
|
|
80
|
+
"Have a nice day!",
|
|
81
|
+
[clean_text, call_li] = toolCallExtract(text);
|
|
82
|
+
|
|
83
|
+
assert.equal(call_li.length, 1);
|
|
84
|
+
assert.equal(call_li[0].function.name, "get_weather");
|
|
85
|
+
assert.equal(call_li[0].function.arguments, '{"city":"Tokyo"}');
|
|
86
|
+
assert.ok(!clean_text.includes("```tool_call"));
|
|
87
|
+
assert.ok(clean_text.includes("Here is the data:"));
|
|
88
|
+
assert.ok(clean_text.includes("Have a nice day!"));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("toolFenceGate prevents tool fence from leaking during streaming", () => {
|
|
92
|
+
const emitted_li = [],
|
|
93
|
+
gate = toolFenceGateCreate((chunk) => {
|
|
94
|
+
emitted_li.push(chunk);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
gate.push("Hello! ");
|
|
98
|
+
gate.push("I will check the weather. ");
|
|
99
|
+
gate.push("```");
|
|
100
|
+
gate.push("tool_call\n");
|
|
101
|
+
gate.push('{"name": "get_weather", "arguments": {"city": "Paris"}}\n');
|
|
102
|
+
gate.push("```");
|
|
103
|
+
gate.push(" Hope that helps!");
|
|
104
|
+
gate.flush();
|
|
105
|
+
|
|
106
|
+
const joined = emitted_li.join("");
|
|
107
|
+
assert.ok(!joined.includes("```tool_call"));
|
|
108
|
+
assert.ok(!joined.includes("get_weather"));
|
|
109
|
+
assert.ok(joined.includes("Hello! I will check the weather. "));
|
|
110
|
+
assert.ok(joined.includes(" Hope that helps!"));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("conversationFormat formats multi-turn tool history", () => {
|
|
114
|
+
const msg_li = [
|
|
115
|
+
{ role: "user", content: "What is the weather in Paris?" },
|
|
116
|
+
{
|
|
117
|
+
role: "assistant",
|
|
118
|
+
content: "Let me check.",
|
|
119
|
+
tool_calls: [
|
|
120
|
+
{
|
|
121
|
+
id: "call_1",
|
|
122
|
+
type: "function",
|
|
123
|
+
function: { name: "get_weather", arguments: '{"city":"Paris"}' },
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
{ role: "tool", name: "get_weather", content: '{"temp": "22C"}' },
|
|
128
|
+
],
|
|
129
|
+
tool_li = [
|
|
130
|
+
{
|
|
131
|
+
type: "function",
|
|
132
|
+
function: { name: "get_weather", description: "Get weather" },
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
conversation = conversationFormat(msg_li, tool_li);
|
|
136
|
+
|
|
137
|
+
assert.ok(conversation.includes("[System instruction]:"));
|
|
138
|
+
assert.ok(conversation.includes("Human: What is the weather in Paris?"));
|
|
139
|
+
assert.ok(conversation.includes("Assistant: Let me check."));
|
|
140
|
+
assert.ok(conversation.includes('```tool_call\n{"name":"get_weather","arguments":{"city":"Paris"}}\n```'));
|
|
141
|
+
assert.ok(conversation.includes("[Tool result for get_weather]:"));
|
|
142
|
+
assert.ok(conversation.includes("[System instruction — highest priority]:"));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("toolCallExtract parses ZCode actual failed output (missing opening backticks)", () => {
|
|
146
|
+
const text =
|
|
147
|
+
'tool_call\n{"name": "Agent", "arguments": {"description": "Search bftree in garnet", "prompt": "Search ./garnet code.", "subagent_type": "Explore"}}\n```',
|
|
148
|
+
[clean_text, call_li] = toolCallExtract(text);
|
|
149
|
+
|
|
150
|
+
assert.equal(call_li.length, 1);
|
|
151
|
+
assert.equal(call_li[0].function.name, "Agent");
|
|
152
|
+
const args = JSON.parse(call_li[0].function.arguments);
|
|
153
|
+
assert.equal(args.description, "Search bftree in garnet");
|
|
154
|
+
assert.equal(args.subagent_type, "Explore");
|
|
155
|
+
assert.equal(clean_text, "");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("toolCallExtract handles markdown escaped underscores and json tool_calls array", () => {
|
|
159
|
+
const escaped_text =
|
|
160
|
+
'```tool_call\n{"name": "Agent", "arguments": {"subagent\\_type": "Explore"}}\n```',
|
|
161
|
+
[, escaped_call_li] = toolCallExtract(escaped_text);
|
|
162
|
+
|
|
163
|
+
assert.equal(escaped_call_li.length, 1);
|
|
164
|
+
const escaped_args = JSON.parse(escaped_call_li[0].function.arguments);
|
|
165
|
+
assert.equal(escaped_args.subagent_type, "Explore");
|
|
166
|
+
|
|
167
|
+
const json_array_text =
|
|
168
|
+
'```json\n{"tool_calls": [{"name": "read", "arguments": {"path": "a.txt"}}]}\n```',
|
|
169
|
+
[, arr_call_li] = toolCallExtract(json_array_text);
|
|
170
|
+
|
|
171
|
+
assert.equal(arr_call_li.length, 1);
|
|
172
|
+
assert.equal(arr_call_li[0].function.name, "read");
|
|
173
|
+
assert.equal(JSON.parse(arr_call_li[0].function.arguments).path, "a.txt");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("toolCallExtract parses bare JSON tool call", () => {
|
|
177
|
+
const bare_text = '{"name": "Agent", "arguments": {"task": "inspect"}}',
|
|
178
|
+
[, call_li] = toolCallExtract(bare_text);
|
|
179
|
+
|
|
180
|
+
assert.equal(call_li.length, 1);
|
|
181
|
+
assert.equal(call_li[0].function.name, "Agent");
|
|
182
|
+
assert.equal(JSON.parse(call_li[0].function.arguments).task, "inspect");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("conversationFormat supports Vercel AI-SDK / ZCode message format (toolCalls, input, toolName)", () => {
|
|
186
|
+
const msg_li = [
|
|
187
|
+
{
|
|
188
|
+
role: "assistant",
|
|
189
|
+
content: "",
|
|
190
|
+
toolCalls: [
|
|
191
|
+
{
|
|
192
|
+
id: "tool_123",
|
|
193
|
+
name: "Bash",
|
|
194
|
+
input: { command: "ls -la" },
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
role: "tool",
|
|
200
|
+
toolName: "Bash",
|
|
201
|
+
toolCallId: "tool_123",
|
|
202
|
+
content: "Exit code 0\nOutput:\nfile1.txt",
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
conversation = conversationFormat(msg_li);
|
|
206
|
+
|
|
207
|
+
assert.ok(conversation.includes('```tool_call\n{"name":"Bash","arguments":{"command":"ls -la"}}\n```'));
|
|
208
|
+
assert.ok(conversation.includes("[Tool result for Bash]:"));
|
|
209
|
+
assert.ok(conversation.includes("file1.txt"));
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("toolCallExtract parses restarted / duplicate fence output", () => {
|
|
213
|
+
const bad_text =
|
|
214
|
+
'```tool_call\n{"name```tool_call\n{"name": "get_weather", "arguments": {"city": "Tokyo"}}\n```',
|
|
215
|
+
[clean_text, call_li] = toolCallExtract(bad_text);
|
|
216
|
+
|
|
217
|
+
assert.equal(call_li.length, 1);
|
|
218
|
+
assert.equal(call_li[0].function.name, "get_weather");
|
|
219
|
+
assert.equal(JSON.parse(call_li[0].function.arguments).city, "Tokyo");
|
|
220
|
+
assert.equal(clean_text, "");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("toolCallExtract parses multiple bare JSON tool calls", () => {
|
|
224
|
+
const multi_bare_text =
|
|
225
|
+
'Calling: {"name": "func_a", "arguments": {"x": 1}}, and {"name": "func_b", "arguments": {"y": 2}}',
|
|
226
|
+
[, call_li] = toolCallExtract(multi_bare_text);
|
|
227
|
+
|
|
228
|
+
assert.equal(call_li.length, 2);
|
|
229
|
+
assert.equal(call_li[0].function.name, "func_a");
|
|
230
|
+
assert.equal(call_li[1].function.name, "func_b");
|
|
231
|
+
assert.equal(JSON.parse(call_li[0].function.arguments).x, 1);
|
|
232
|
+
assert.equal(JSON.parse(call_li[1].function.arguments).y, 2);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("jsonFormat formats objects and strings cleanly", () => {
|
|
236
|
+
const formatted = jsonFormat({ a: 1, b: "test" });
|
|
237
|
+
assert.ok(formatted.includes('"a"') && formatted.includes("1"));
|
|
238
|
+
assert.ok(formatted.includes('"b"') && formatted.includes("test"));
|
|
239
|
+
|
|
240
|
+
const raw_str = jsonFormat("simple text");
|
|
241
|
+
assert.equal(raw_str, "simple text");
|
|
242
|
+
});
|
|
243
|
+
|