mocode-ai 1.6.6 → 1.6.8
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 +39 -30
- package/README.zh-CN.md +50 -29
- package/dist/agent/stages/history-manager.js +3 -0
- package/dist/config/index.js +11 -2
- package/dist/config/presets.js +13 -2
- package/dist/config/profiles.js +1 -1
- package/dist/llm/index.js +7 -2
- package/dist/llm/providers/sse.js +38 -0
- package/dist/llm/providers/vertex.js +397 -0
- package/dist/llm/proxy.js +101 -0
- package/dist/llm/reasoning.js +6 -0
- package/dist/models/map-preset.js +23 -4
- package/dist/models/model-panel.js +4 -2
- package/dist/models/protocol.js +49 -4
- package/dist/models/search.js +2 -2
- package/dist/repl/commands/effort.js +1 -1
- package/dist/repl/commands/model-actions.js +2 -2
- package/dist/repl/commands/model.js +1 -1
- package/dist/session/scheduler.js +0 -3
- package/dist/session/store.js +1 -1
- package/dist/tools/builtins/glob.js +64 -16
- package/dist/tools/builtins/grep.js +176 -122
- package/dist/tools/builtins/index.js +9 -2
- package/dist/tools/builtins/task.js +1 -0
- package/dist/tools/constants.js +11 -0
- package/dist/tools/policy.js +3 -0
- package/dist/tools/ripgrep-client.js +180 -0
- package/dist/tools/router.js +4 -2
- package/dist/ui/fuzzy-picker.js +1 -3
- package/package.json +4 -2
package/dist/llm/index.js
CHANGED
|
@@ -7,6 +7,8 @@ import { ThinkTagFilter } from './think-filter.js';
|
|
|
7
7
|
import { sanitizeToolSchemas } from './tool-schema.js';
|
|
8
8
|
import { isMarkedStreamInterrupted } from './stream-interrupt.js';
|
|
9
9
|
import { defaultAnthropicFetch, anthropicChatOnce } from './providers/anthropic.js';
|
|
10
|
+
import { vertexChatOnce } from './providers/vertex.js';
|
|
11
|
+
import { installEnvProxy } from './proxy.js';
|
|
10
12
|
import { registerModelProvider, getModelProvider, listModelProviders } from './provider.js';
|
|
11
13
|
import { effectiveReasoningEffort } from '../config/index.js';
|
|
12
14
|
import { resolveReasoningParams } from './reasoning.js';
|
|
@@ -53,6 +55,7 @@ function newOpenAIClient(runtimeConfig) {
|
|
|
53
55
|
}
|
|
54
56
|
/** Build mutable client state owned by one runtime; no module-global overrides are consulted. */
|
|
55
57
|
export function createChatClientState(runtimeConfig, overrides = {}) {
|
|
58
|
+
installEnvProxy();
|
|
56
59
|
return {
|
|
57
60
|
openAI: newOpenAIClient(runtimeConfig),
|
|
58
61
|
openAICreateImpl: overrides.openAICreateImpl ?? null,
|
|
@@ -403,6 +406,7 @@ registerModelProvider({
|
|
|
403
406
|
name: 'anthropic',
|
|
404
407
|
chatOnce: (messages, handlers, signal, tools, runtime, overrides) => anthropicChatOnce(messages, handlers, signal, tools ?? chatTools, runtime, overrides),
|
|
405
408
|
});
|
|
409
|
+
registerModelProvider({ name: 'google', chatOnce: vertexChatOnce });
|
|
406
410
|
/**
|
|
407
411
|
* 多 provider 兼容的 cache / reasoning 字段提取。
|
|
408
412
|
* 不同后端报 cached 字段名差异巨大,这里按"最常见的几种"顺序 probe,
|
|
@@ -509,7 +513,8 @@ async function chatWithRuntime(runtime, messages, handlers, signal, toolsOverrid
|
|
|
509
513
|
}
|
|
510
514
|
producedOutput = false;
|
|
511
515
|
try {
|
|
512
|
-
const
|
|
516
|
+
const providerName = runtime === defaultProviderRuntime && runtime.clientState.openAICreateImpl ? 'openai' : runtime.config.provider;
|
|
517
|
+
const provider = getModelProvider(providerName);
|
|
513
518
|
if (!provider) {
|
|
514
519
|
// 未注册的 provider:给出可用名单,避免悄悄落到错误实现。
|
|
515
520
|
throw new Error(`未知的 LLM provider "${runtime.config.provider}";已注册:${listModelProviders().join(', ') || '(空)'}`);
|
|
@@ -623,7 +628,7 @@ async function chatOnce(messages, handlers, signal, toolsOverride, runtime = def
|
|
|
623
628
|
const reasoningParams = effort === 'auto'
|
|
624
629
|
? {}
|
|
625
630
|
: resolveReasoningParams(effort, {
|
|
626
|
-
provider: runtimeConfig.provider
|
|
631
|
+
provider: runtimeConfig.provider,
|
|
627
632
|
model: runtime.getModel(),
|
|
628
633
|
maxTokens: runtimeConfig.maxTokens,
|
|
629
634
|
// 仅当激活预设来自目录且对应当前模型时携带,否则走正则兜底。
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function decodeSseRecord(record) {
|
|
2
|
+
let event = '';
|
|
3
|
+
const data = [];
|
|
4
|
+
for (const line of record.split(/\r?\n/)) {
|
|
5
|
+
if (line.startsWith('event:'))
|
|
6
|
+
event = line.slice(6).trim();
|
|
7
|
+
else if (line.startsWith('data:'))
|
|
8
|
+
data.push(line.slice(5).trimStart());
|
|
9
|
+
}
|
|
10
|
+
if (data.length === 0)
|
|
11
|
+
return null;
|
|
12
|
+
return { event, data: data.join('\n') };
|
|
13
|
+
}
|
|
14
|
+
/** 按 SSE 事件边界(空行)切分流式响应体;响应结束时冲刷尾段。 */
|
|
15
|
+
export async function* readSse(response) {
|
|
16
|
+
if (!response.body)
|
|
17
|
+
throw new Error('流式响应缺少 body');
|
|
18
|
+
const reader = response.body.getReader();
|
|
19
|
+
const decoder = new TextDecoder();
|
|
20
|
+
let buffer = '';
|
|
21
|
+
while (true) {
|
|
22
|
+
const { value, done } = await reader.read();
|
|
23
|
+
buffer += decoder.decode(value, { stream: !done });
|
|
24
|
+
let match;
|
|
25
|
+
while ((match = /\r?\n\r?\n/.exec(buffer)) !== null) {
|
|
26
|
+
const record = buffer.slice(0, match.index);
|
|
27
|
+
buffer = buffer.slice(match.index + match[0].length);
|
|
28
|
+
const parsed = decodeSseRecord(record);
|
|
29
|
+
if (parsed)
|
|
30
|
+
yield parsed;
|
|
31
|
+
}
|
|
32
|
+
if (done)
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
const parsed = decodeSseRecord(buffer);
|
|
36
|
+
if (parsed)
|
|
37
|
+
yield parsed;
|
|
38
|
+
}
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vertex AI / Gemini 原生协议 provider。
|
|
3
|
+
*
|
|
4
|
+
* 端点形态:
|
|
5
|
+
* <baseURL>/<model>:streamGenerateContent?alt=sse
|
|
6
|
+
* 其中 baseURL 可为:
|
|
7
|
+
* https://asia-northeast1-aiplatform.googleapis.com/v1/publishers/google/models(Vertex 日本)
|
|
8
|
+
* https://generativelanguage.googleapis.com/v1beta/models(Gemini Developer API)
|
|
9
|
+
*
|
|
10
|
+
* 认证:Vertex 的 Express API key 用 `x-goog-api-key`;Gemini Developer API 也兼容该头。
|
|
11
|
+
*/
|
|
12
|
+
import { randomUUID } from 'node:crypto';
|
|
13
|
+
import { config, effectiveReasoningEffort, getActiveModel } from '../../config/index.js';
|
|
14
|
+
import { readSse } from './sse.js';
|
|
15
|
+
function isRecord(value) {
|
|
16
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
function textFromContent(content) {
|
|
19
|
+
if (typeof content === 'string')
|
|
20
|
+
return content;
|
|
21
|
+
if (content == null)
|
|
22
|
+
return '';
|
|
23
|
+
if (Array.isArray(content)) {
|
|
24
|
+
return content.map((part) => (isRecord(part) && typeof part.text === 'string' ? part.text : '')).join('');
|
|
25
|
+
}
|
|
26
|
+
return String(content);
|
|
27
|
+
}
|
|
28
|
+
function imagePart(content) {
|
|
29
|
+
if (!isRecord(content) || content.type !== 'image_url')
|
|
30
|
+
return null;
|
|
31
|
+
const imageUrl = content.image_url;
|
|
32
|
+
const url = typeof imageUrl === 'string'
|
|
33
|
+
? imageUrl
|
|
34
|
+
: isRecord(imageUrl) && typeof imageUrl.url === 'string'
|
|
35
|
+
? imageUrl.url
|
|
36
|
+
: '';
|
|
37
|
+
const match = /^data:(image\/(?:jpeg|png|gif|webp));base64,(.+)$/s.exec(url);
|
|
38
|
+
if (!match)
|
|
39
|
+
return null;
|
|
40
|
+
return { inlineData: { mimeType: match[1], data: match[2] } };
|
|
41
|
+
}
|
|
42
|
+
function appendContent(contents, role, parts) {
|
|
43
|
+
if (parts.length === 0)
|
|
44
|
+
return;
|
|
45
|
+
const previous = contents[contents.length - 1];
|
|
46
|
+
if (previous?.role === role)
|
|
47
|
+
previous.parts.push(...parts);
|
|
48
|
+
else
|
|
49
|
+
contents.push({ role, parts });
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 追加 system reminder(转成 user 文本)。
|
|
53
|
+
* Vertex 要求含 functionResponse 的 user 轮保持纯净:把普通文本合并进该轮会报
|
|
54
|
+
* "Requests ending with a model turn are not supported."(实测)。
|
|
55
|
+
* 因此上一个 user 轮含 functionResponse 时,reminder 必须独立成一个 user 轮。
|
|
56
|
+
*/
|
|
57
|
+
function appendReminder(contents, text) {
|
|
58
|
+
const part = { text: `[System reminder]\n${text}` };
|
|
59
|
+
const previous = contents[contents.length - 1];
|
|
60
|
+
if (previous?.role === 'user' && previous.parts.some((item) => !!item.functionResponse)) {
|
|
61
|
+
contents.push({ role: 'user', parts: [part] });
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
appendContent(contents, 'user', [part]);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function functionResponsePart(toolMessage) {
|
|
68
|
+
let response = { result: textFromContent(toolMessage.content) };
|
|
69
|
+
const raw = textFromContent(toolMessage.content).trim();
|
|
70
|
+
if (raw) {
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(raw);
|
|
73
|
+
if (isRecord(parsed))
|
|
74
|
+
response = parsed;
|
|
75
|
+
else
|
|
76
|
+
response = { result: parsed };
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// 保留默认 { result }。
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { functionResponse: { name: '', response } };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 把 OpenAI Chat 形态转成 Gemini contents。
|
|
86
|
+
* - system 不进 contents(见 buildGeminiRequest 的 systemInstruction)
|
|
87
|
+
* - assistant.tool_calls → model parts[].functionCall
|
|
88
|
+
* - role:tool → 紧随 model 之后的 functionResponse;Gemini 要求该轮 role 为 user
|
|
89
|
+
*/
|
|
90
|
+
export function encodeGeminiMessages(messages) {
|
|
91
|
+
const systemParts = [];
|
|
92
|
+
const contents = [];
|
|
93
|
+
let dialogStarted = false;
|
|
94
|
+
for (const rawMessage of messages) {
|
|
95
|
+
const message = rawMessage;
|
|
96
|
+
if (message.role === 'system' && !dialogStarted) {
|
|
97
|
+
const text = textFromContent(message.content);
|
|
98
|
+
if (text)
|
|
99
|
+
systemParts.push(text);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
dialogStarted = true;
|
|
103
|
+
if (message.role === 'system') {
|
|
104
|
+
appendReminder(contents, textFromContent(message.content));
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (message.role === 'tool') {
|
|
108
|
+
// OpenAI 的 tool 消息只有 tool_call_id,name 不一定存在;name 在相邻 assistant tool_calls 中补齐。
|
|
109
|
+
const part = functionResponsePart(message);
|
|
110
|
+
appendContent(contents, 'user', [part]);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (message.role === 'assistant') {
|
|
114
|
+
const parts = [];
|
|
115
|
+
if (Array.isArray(message.content)) {
|
|
116
|
+
for (const contentPart of message.content) {
|
|
117
|
+
const image = imagePart(contentPart);
|
|
118
|
+
if (image)
|
|
119
|
+
parts.push(image);
|
|
120
|
+
else {
|
|
121
|
+
const text = textFromContent(contentPart);
|
|
122
|
+
if (text)
|
|
123
|
+
parts.push({ text });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
const text = textFromContent(message.content);
|
|
129
|
+
if (text)
|
|
130
|
+
parts.push({ text });
|
|
131
|
+
}
|
|
132
|
+
for (const toolCall of message.tool_calls ?? []) {
|
|
133
|
+
const name = toolCall.function?.name ?? '';
|
|
134
|
+
let args = {};
|
|
135
|
+
const rawArgs = toolCall.function?.arguments?.trim();
|
|
136
|
+
if (rawArgs) {
|
|
137
|
+
try {
|
|
138
|
+
const parsed = JSON.parse(rawArgs);
|
|
139
|
+
if (isRecord(parsed))
|
|
140
|
+
args = parsed;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// 保留空对象,避免把坏参数直接抛成非 JSON。
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
parts.push({
|
|
147
|
+
functionCall: { name, args },
|
|
148
|
+
// 关键:上一轮 functionCall part 上的 thought signature 必须原样回灌,
|
|
149
|
+
// 否则带思考的 Vertex 模型会拒绝整个请求。
|
|
150
|
+
...(toolCall.thoughtSignature ? { thoughtSignature: toolCall.thoughtSignature } : {}),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
appendContent(contents, 'model', parts);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const parts = [];
|
|
157
|
+
if (Array.isArray(message.content)) {
|
|
158
|
+
for (const contentPart of message.content) {
|
|
159
|
+
const image = imagePart(contentPart);
|
|
160
|
+
if (image)
|
|
161
|
+
parts.push(image);
|
|
162
|
+
else {
|
|
163
|
+
const text = textFromContent(contentPart);
|
|
164
|
+
if (text)
|
|
165
|
+
parts.push({ text });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const text = textFromContent(message.content);
|
|
171
|
+
if (text)
|
|
172
|
+
parts.push({ text });
|
|
173
|
+
}
|
|
174
|
+
appendContent(contents, 'user', parts);
|
|
175
|
+
}
|
|
176
|
+
// Gemini 要求 functionResponse 与对应 functionCall 配对。OpenAI 工具消息按 id 关联,
|
|
177
|
+
// 这里用相邻 model functionCall 的名字回填无名 response。
|
|
178
|
+
for (let i = 0; i < contents.length; i++) {
|
|
179
|
+
const content = contents[i];
|
|
180
|
+
if (content.role !== 'user')
|
|
181
|
+
continue;
|
|
182
|
+
const previousModel = [...contents.slice(0, i)].reverse().find((item) => item.role === 'model');
|
|
183
|
+
const callNames = (previousModel?.parts ?? [])
|
|
184
|
+
.map((part) => part.functionCall?.name)
|
|
185
|
+
.filter((name) => !!name);
|
|
186
|
+
let callIndex = 0;
|
|
187
|
+
for (const part of content.parts) {
|
|
188
|
+
if (!part.functionResponse)
|
|
189
|
+
continue;
|
|
190
|
+
part.functionResponse.name = callNames[callIndex] ?? callNames[callNames.length - 1] ?? 'tool';
|
|
191
|
+
callIndex++;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return { system: systemParts.join('\n\n'), contents };
|
|
195
|
+
}
|
|
196
|
+
/** Gemini Schema 不接受 OpenAI 工具 schema 中常见的部分注解字段;递归转成其 Schema 子集。 */
|
|
197
|
+
export function sanitizeGeminiSchema(value) {
|
|
198
|
+
if (Array.isArray(value))
|
|
199
|
+
return value.map(sanitizeGeminiSchema);
|
|
200
|
+
if (!isRecord(value))
|
|
201
|
+
return value;
|
|
202
|
+
const out = {};
|
|
203
|
+
for (const [key, child] of Object.entries(value)) {
|
|
204
|
+
if (key === '$schema' || key === 'additionalProperties' || key === '$comment')
|
|
205
|
+
continue;
|
|
206
|
+
out[key] = sanitizeGeminiSchema(child);
|
|
207
|
+
}
|
|
208
|
+
// Gemini Schema.type 是单值枚举,不接受 OpenAPI 3.1 的 ["string", "null"]。
|
|
209
|
+
if (Array.isArray(out.type)) {
|
|
210
|
+
const types = out.type.filter((type) => typeof type === 'string');
|
|
211
|
+
const nullable = types.includes('null');
|
|
212
|
+
out.type = types.find((type) => type !== 'null') ?? 'string';
|
|
213
|
+
if (nullable)
|
|
214
|
+
out.nullable = true;
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
export function encodeGeminiTools(tools) {
|
|
219
|
+
if (tools.length === 0)
|
|
220
|
+
return [];
|
|
221
|
+
return [
|
|
222
|
+
{
|
|
223
|
+
functionDeclarations: tools.map((tool) => ({
|
|
224
|
+
name: tool.function.name,
|
|
225
|
+
description: tool.function.description,
|
|
226
|
+
parameters: sanitizeGeminiSchema(tool.function.parameters ?? { type: 'object', properties: {} }),
|
|
227
|
+
})),
|
|
228
|
+
},
|
|
229
|
+
];
|
|
230
|
+
}
|
|
231
|
+
function thinkingBudget(effort) {
|
|
232
|
+
if (effort === 'auto')
|
|
233
|
+
return null;
|
|
234
|
+
if (effort === 'off')
|
|
235
|
+
return 0;
|
|
236
|
+
if (effort === 'low')
|
|
237
|
+
return 1024;
|
|
238
|
+
if (effort === 'medium')
|
|
239
|
+
return 4096;
|
|
240
|
+
return 24576;
|
|
241
|
+
}
|
|
242
|
+
function endpoint(baseURL, model) {
|
|
243
|
+
const base = baseURL.replace(/\/+$/, '');
|
|
244
|
+
const escapedModel = encodeURIComponent(model).replace('%40', '@');
|
|
245
|
+
const suffix = `/${escapedModel}`;
|
|
246
|
+
if (base.endsWith(suffix) || base.includes(`${suffix}:`)) {
|
|
247
|
+
return `${base}:streamGenerateContent?alt=sse`;
|
|
248
|
+
}
|
|
249
|
+
return `${base}${suffix}:streamGenerateContent?alt=sse`;
|
|
250
|
+
}
|
|
251
|
+
export function buildGeminiRequest(messages, tools, runtime, overrides) {
|
|
252
|
+
const runtimeConfig = runtime?.config ?? config;
|
|
253
|
+
const encoded = encodeGeminiMessages(messages);
|
|
254
|
+
const geminiTools = encodeGeminiTools(tools);
|
|
255
|
+
const effort = effectiveReasoningEffort(overrides?.reasoningEffort);
|
|
256
|
+
const budget = thinkingBudget(effort);
|
|
257
|
+
const generationConfig = {
|
|
258
|
+
maxOutputTokens: runtimeConfig.maxTokens ?? 8192,
|
|
259
|
+
temperature: 0.2,
|
|
260
|
+
...(budget === null ? {} : { thinkingConfig: { thinkingBudget: budget } }),
|
|
261
|
+
};
|
|
262
|
+
return {
|
|
263
|
+
...(encoded.system ? { systemInstruction: { parts: [{ text: encoded.system }] } } : {}),
|
|
264
|
+
contents: encoded.contents,
|
|
265
|
+
...(geminiTools.length > 0 ? { tools: geminiTools } : {}),
|
|
266
|
+
generationConfig,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
class GeminiHttpError extends Error {
|
|
270
|
+
status;
|
|
271
|
+
code;
|
|
272
|
+
headers;
|
|
273
|
+
constructor(status, message, headers, code) {
|
|
274
|
+
super(message);
|
|
275
|
+
this.name = 'GeminiAPIError';
|
|
276
|
+
this.status = status;
|
|
277
|
+
this.code = code;
|
|
278
|
+
this.headers = headers;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async function throwResponseError(response) {
|
|
282
|
+
let message = `Gemini API HTTP ${response.status}`;
|
|
283
|
+
let code;
|
|
284
|
+
try {
|
|
285
|
+
const body = JSON.parse(await response.text());
|
|
286
|
+
if (typeof body.error?.message === 'string')
|
|
287
|
+
message = body.error.message;
|
|
288
|
+
if (typeof body.error?.code === 'string')
|
|
289
|
+
code = body.error.code;
|
|
290
|
+
else if (typeof body.error?.status === 'string')
|
|
291
|
+
code = body.error.status;
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
// 非 JSON 错误页只保留状态码。
|
|
295
|
+
}
|
|
296
|
+
throw new GeminiHttpError(response.status, message, response.headers, code);
|
|
297
|
+
}
|
|
298
|
+
function usageFromGemini(raw) {
|
|
299
|
+
const promptTokens = typeof raw.promptTokenCount === 'number' ? raw.promptTokenCount : 0;
|
|
300
|
+
const completionTokens = typeof raw.candidatesTokenCount === 'number' ? raw.candidatesTokenCount : 0;
|
|
301
|
+
const thoughts = typeof raw.thoughtsTokenCount === 'number' ? raw.thoughtsTokenCount : 0;
|
|
302
|
+
const totalTokens = typeof raw.totalTokenCount === 'number' ? raw.totalTokenCount : promptTokens + completionTokens;
|
|
303
|
+
return {
|
|
304
|
+
promptTokens,
|
|
305
|
+
completionTokens,
|
|
306
|
+
totalTokens,
|
|
307
|
+
cachedTokens: 0,
|
|
308
|
+
reasoningTokens: thoughts,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/** Vertex/Gemini 单次流式请求;重试仍由外层 chatWithRuntime 统一处理。 */
|
|
312
|
+
export async function vertexChatOnce(messages, handlers, signal, tools, runtime, overrides) {
|
|
313
|
+
const runtimeConfig = runtime?.config ?? config;
|
|
314
|
+
const model = runtime?.getModel() ?? getActiveModel();
|
|
315
|
+
const activeTools = tools ?? [];
|
|
316
|
+
const response = await fetch(endpoint(runtimeConfig.baseURL, model), {
|
|
317
|
+
method: 'POST',
|
|
318
|
+
headers: {
|
|
319
|
+
'content-type': 'application/json',
|
|
320
|
+
accept: 'text/event-stream',
|
|
321
|
+
'x-goog-api-key': runtimeConfig.apiKey,
|
|
322
|
+
},
|
|
323
|
+
body: JSON.stringify(buildGeminiRequest(messages, activeTools, runtime ? { config: runtime.config, getModel: runtime.getModel } : undefined, overrides)),
|
|
324
|
+
signal,
|
|
325
|
+
});
|
|
326
|
+
if (!response.ok)
|
|
327
|
+
await throwResponseError(response);
|
|
328
|
+
let content = '';
|
|
329
|
+
let usage;
|
|
330
|
+
const toolCalls = [];
|
|
331
|
+
let reportedTool = false;
|
|
332
|
+
const live = { cjk: 0, other: 0 };
|
|
333
|
+
const countLive = (text) => {
|
|
334
|
+
for (const ch of text) {
|
|
335
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
336
|
+
if ((cp >= 0x4e00 && cp <= 0x9fff) || (cp >= 0x3040 && cp <= 0x30ff) || (cp >= 0xac00 && cp <= 0xd7a3))
|
|
337
|
+
live.cjk++;
|
|
338
|
+
else
|
|
339
|
+
live.other++;
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
const reportLive = () => {
|
|
343
|
+
handlers.onProgress?.({ completionTokens: Math.ceil(live.cjk + live.other / 4) });
|
|
344
|
+
};
|
|
345
|
+
for await (const record of readSse(response)) {
|
|
346
|
+
let event;
|
|
347
|
+
try {
|
|
348
|
+
event = JSON.parse(record.data);
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
const usageMetadata = event.usageMetadata;
|
|
354
|
+
if (isRecord(usageMetadata))
|
|
355
|
+
usage = usageFromGemini(usageMetadata);
|
|
356
|
+
const candidates = Array.isArray(event.candidates) ? event.candidates : [];
|
|
357
|
+
const candidate = isRecord(candidates[0]) ? candidates[0] : undefined;
|
|
358
|
+
const candidateContent = isRecord(candidate?.content) ? candidate.content : undefined;
|
|
359
|
+
const parts = Array.isArray(candidateContent?.parts) ? candidateContent.parts : [];
|
|
360
|
+
for (const rawPart of parts) {
|
|
361
|
+
if (!isRecord(rawPart))
|
|
362
|
+
continue;
|
|
363
|
+
// thought:true 的部分是模型思考(仅 includeThoughts 时才下发),不是正文,不能 onText/入 history。
|
|
364
|
+
if (typeof rawPart.text === 'string' && rawPart.text && rawPart.thought !== true) {
|
|
365
|
+
content += rawPart.text;
|
|
366
|
+
countLive(rawPart.text);
|
|
367
|
+
handlers.onText?.(rawPart.text);
|
|
368
|
+
reportLive();
|
|
369
|
+
}
|
|
370
|
+
if (isRecord(rawPart.functionCall)) {
|
|
371
|
+
const name = typeof rawPart.functionCall.name === 'string' ? rawPart.functionCall.name : '';
|
|
372
|
+
const args = isRecord(rawPart.functionCall.args) ? rawPart.functionCall.args : {};
|
|
373
|
+
// Vertex/Gemini 的 functionCall 不返回 OpenAI 风格 id;HistoryManager 要求
|
|
374
|
+
// 非空且唯一的 tool_call id,故在 provider 边界生成本会话唯一 id。
|
|
375
|
+
// 同 part 上的 thoughtSignature 一并捕获,供下一轮回灌。
|
|
376
|
+
toolCalls.push({
|
|
377
|
+
id: `gemini-${randomUUID()}`,
|
|
378
|
+
name,
|
|
379
|
+
arguments: JSON.stringify(args),
|
|
380
|
+
...(typeof rawPart.thoughtSignature === 'string' ? { thoughtSignature: rawPart.thoughtSignature } : {}),
|
|
381
|
+
});
|
|
382
|
+
if (!reportedTool && name) {
|
|
383
|
+
handlers.onToolCall?.(name);
|
|
384
|
+
reportedTool = true;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (usage) {
|
|
390
|
+
handlers.onProgress?.({
|
|
391
|
+
completionTokens: usage.completionTokens,
|
|
392
|
+
promptTokens: usage.promptTokens,
|
|
393
|
+
cachedTokens: usage.cachedTokens,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
return { content: content || null, toolCalls, usage };
|
|
397
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 进程级 HTTP(S) 代理安装。
|
|
3
|
+
*
|
|
4
|
+
* Node 的全局 fetch(undici)默认不读 HTTP_PROXY/HTTPS_PROXY/NO_PROXY(Node 24 需
|
|
5
|
+
* --use-env-proxy 或 NODE_USE_ENV_PROXY=1)。这里在首个 client state 构建时显式安装
|
|
6
|
+
* undici 的 EnvHttpProxyAgent:Clash/V2Ray 等本地代理通过环境变量或 Windows 系统代理
|
|
7
|
+
* (WinINET 注册表)均可识别;本地 localhost/127.0.0.1 始终绕过。
|
|
8
|
+
*
|
|
9
|
+
* 幂等:进程内只安装一次。MOCODE_NO_ENV_PROXY=1 可完全关闭。
|
|
10
|
+
*/
|
|
11
|
+
import { execSync } from 'node:child_process';
|
|
12
|
+
import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici';
|
|
13
|
+
let installed = false;
|
|
14
|
+
function withProtocol(value) {
|
|
15
|
+
return /^https?:\/\//i.test(value) ? value : `http://${value}`;
|
|
16
|
+
}
|
|
17
|
+
export function parseWindowsProxyServer(raw) {
|
|
18
|
+
const entries = raw
|
|
19
|
+
.split(';')
|
|
20
|
+
.map((entry) => entry.trim())
|
|
21
|
+
.filter(Boolean);
|
|
22
|
+
const result = {};
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
const match = /^(https?|socks)=(.+)$/i.exec(entry);
|
|
25
|
+
if (!match)
|
|
26
|
+
continue;
|
|
27
|
+
const scheme = match[1].toLowerCase();
|
|
28
|
+
const value = withProtocol(match[2].trim());
|
|
29
|
+
if (scheme === 'https')
|
|
30
|
+
result.https = value;
|
|
31
|
+
else if (scheme === 'http')
|
|
32
|
+
result.http = value;
|
|
33
|
+
}
|
|
34
|
+
// Clash Verge 常见形态:ProxyServer=127.0.0.1:7897(mixed port)。
|
|
35
|
+
if (!result.http && !result.https && entries[0]?.includes(':')) {
|
|
36
|
+
return { http: withProtocol(entries[0]), https: withProtocol(entries[0]) };
|
|
37
|
+
}
|
|
38
|
+
if (result.http && !result.https)
|
|
39
|
+
result.https = result.http;
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
function registryValue(name) {
|
|
43
|
+
try {
|
|
44
|
+
const output = execSync(`reg query "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings" /v ${name}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
45
|
+
const line = output.split(/\r?\n/).find((item) => item.includes(name));
|
|
46
|
+
return (line
|
|
47
|
+
?.trim()
|
|
48
|
+
.split(/\s{2,}/)
|
|
49
|
+
.pop()
|
|
50
|
+
?.trim() ?? '');
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return '';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function windowsProxyOptions() {
|
|
57
|
+
if (process.platform !== 'win32')
|
|
58
|
+
return null;
|
|
59
|
+
if (!/1$/.test(registryValue('ProxyEnable')))
|
|
60
|
+
return null;
|
|
61
|
+
const parsed = parseWindowsProxyServer(registryValue('ProxyServer'));
|
|
62
|
+
if (!parsed.http && !parsed.https)
|
|
63
|
+
return null;
|
|
64
|
+
const overrides = registryValue('ProxyOverride')
|
|
65
|
+
.split(';')
|
|
66
|
+
.map((entry) => entry.trim())
|
|
67
|
+
.filter((entry) => entry && entry !== '<local>');
|
|
68
|
+
const noProxy = [...new Set([...overrides, 'localhost', '127.0.0.1'])].join(',');
|
|
69
|
+
return {
|
|
70
|
+
...(parsed.http ? { httpProxy: parsed.http } : {}),
|
|
71
|
+
httpsProxy: parsed.https ?? parsed.http,
|
|
72
|
+
noProxy,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function installEnvProxy() {
|
|
76
|
+
if (installed)
|
|
77
|
+
return;
|
|
78
|
+
installed = true;
|
|
79
|
+
if (process.env.MOCODE_NO_ENV_PROXY === '1')
|
|
80
|
+
return;
|
|
81
|
+
const envProxy = process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy;
|
|
82
|
+
const windowsOptions = windowsProxyOptions();
|
|
83
|
+
const options = envProxy ? undefined : (windowsOptions ?? undefined);
|
|
84
|
+
if (!envProxy && !windowsOptions)
|
|
85
|
+
return;
|
|
86
|
+
// EnvHttpProxyAgent 构造时会通过 process.emitWarning 打一条 UNDICI-EHPA 实验性警告,
|
|
87
|
+
// 在 TUI 里可能落成脏行;屏蔽这一条特定警告,其余警告照常。
|
|
88
|
+
const originalEmitWarning = process.emitWarning.bind(process);
|
|
89
|
+
process.emitWarning = ((...args) => {
|
|
90
|
+
const first = args[0];
|
|
91
|
+
if (typeof first === 'string' && first.includes('EnvHttpProxyAgent is experimental'))
|
|
92
|
+
return;
|
|
93
|
+
return originalEmitWarning(...args);
|
|
94
|
+
});
|
|
95
|
+
try {
|
|
96
|
+
setGlobalDispatcher(new EnvHttpProxyAgent(options));
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
process.emitWarning = originalEmitWarning;
|
|
100
|
+
}
|
|
101
|
+
}
|
package/dist/llm/reasoning.js
CHANGED
|
@@ -54,6 +54,9 @@ const ANTHROPIC_DEFAULT_MAX = 8192;
|
|
|
54
54
|
* 供 /model show 给出「参数不会下发」提示,避免用户设了 high 却静默无效。
|
|
55
55
|
*/
|
|
56
56
|
export function recognizesReasoning(target) {
|
|
57
|
+
// Google Gemini 的 thinkingConfig 由 vertex provider 自行转换。
|
|
58
|
+
if (target.provider === 'google')
|
|
59
|
+
return /gemini/.test(target.model.toLowerCase());
|
|
57
60
|
// 目录来源显式声明了思考能力 → 直接采纳(含声明 false 时不识别)。
|
|
58
61
|
if (target.catalogProviderId && typeof target.catalogReasoning === 'boolean') {
|
|
59
62
|
return target.catalogReasoning;
|
|
@@ -75,6 +78,9 @@ export function recognizesReasoning(target) {
|
|
|
75
78
|
export function resolveReasoningParams(effort, target) {
|
|
76
79
|
if (effort === 'auto')
|
|
77
80
|
return {};
|
|
81
|
+
// Gemini/Vertex provider 在 buildGeminiRequest 内直接生成 thinkingConfig;这里不应再注入 OpenAI 方言。
|
|
82
|
+
if (target.provider === 'google')
|
|
83
|
+
return {};
|
|
78
84
|
// 目录优先:带 catalogProviderId 时按目录能力+厂商方言产出。
|
|
79
85
|
// 注意 anthropic 路径的思考在 anthropic provider 内仍走它自己的 target.provider,
|
|
80
86
|
// 目录只对 OpenAI 兼容路径有意义;Anthropic 官方协议继续走下方既有分支。
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { classifyModel, resolveModelBaseURL, toPresetProvider } from './protocol.js';
|
|
2
2
|
/** 上下文窗口缺失/为 0 时的保守默认(宁可小,避免超长发出去被拒)。 */
|
|
3
3
|
export const FALLBACK_CONTEXT_WINDOW = 128000;
|
|
4
4
|
/**
|
|
@@ -6,7 +6,14 @@ export const FALLBACK_CONTEXT_WINDOW = 128000;
|
|
|
6
6
|
* 读 process.env 是本模块唯一触碰环境的地方,调用方可传 env 覆盖以便测试。
|
|
7
7
|
*/
|
|
8
8
|
export function resolveApiKey(provider, env = process.env) {
|
|
9
|
-
|
|
9
|
+
// models.dev 的 google-vertex.env 是 project/location/ADC,不是 Express API key;不能误当密钥。
|
|
10
|
+
const googleKeyEnvs = ['GOOGLE_CLOUD_API_KEY', 'GOOGLE_API_KEY', 'GOOGLE_GENERATIVE_AI_API_KEY', 'GEMINI_API_KEY'];
|
|
11
|
+
const keyNames = isGeminiProvider(provider)
|
|
12
|
+
? provider.npm === '@ai-sdk/google-vertex'
|
|
13
|
+
? googleKeyEnvs
|
|
14
|
+
: [...new Set([...googleKeyEnvs, ...(provider.env ?? [])])]
|
|
15
|
+
: (provider.env ?? []);
|
|
16
|
+
for (const keyName of keyNames) {
|
|
10
17
|
const v = env[keyName];
|
|
11
18
|
if (typeof v === 'string' && v.trim())
|
|
12
19
|
return v.trim();
|
|
@@ -14,6 +21,18 @@ export function resolveApiKey(provider, env = process.env) {
|
|
|
14
21
|
return null;
|
|
15
22
|
}
|
|
16
23
|
/** 把目录来源拼成合法预设名:`<provider>-<model>` 去非法字符。 */
|
|
24
|
+
export function isGeminiProvider(provider) {
|
|
25
|
+
return (provider.id === 'google' ||
|
|
26
|
+
provider.id === 'google-vertex' ||
|
|
27
|
+
provider.npm === '@ai-sdk/google' ||
|
|
28
|
+
provider.npm === '@ai-sdk/google-vertex');
|
|
29
|
+
}
|
|
30
|
+
/** Vertex 目录的首个 env 是 project 变量;API key 接入时应优先提示实际使用的 key。 */
|
|
31
|
+
export function preferredKeyEnvName(provider) {
|
|
32
|
+
if (isGeminiProvider(provider))
|
|
33
|
+
return 'GOOGLE_CLOUD_API_KEY';
|
|
34
|
+
return provider.env?.[0] ?? 'API_KEY';
|
|
35
|
+
}
|
|
17
36
|
export function defaultPresetName(providerId, modelId) {
|
|
18
37
|
const raw = `${providerId}-${modelId}`;
|
|
19
38
|
const sanitized = raw
|
|
@@ -28,11 +47,11 @@ export function defaultPresetName(providerId, modelId) {
|
|
|
28
47
|
*/
|
|
29
48
|
export function buildPreset(input) {
|
|
30
49
|
const { provider, model } = input;
|
|
31
|
-
const proto =
|
|
50
|
+
const proto = classifyModel(provider, model);
|
|
32
51
|
const presetProvider = toPresetProvider(proto);
|
|
33
52
|
if (!presetProvider)
|
|
34
53
|
return null;
|
|
35
|
-
const baseURL =
|
|
54
|
+
const baseURL = resolveModelBaseURL(provider, model);
|
|
36
55
|
if (!baseURL)
|
|
37
56
|
return null;
|
|
38
57
|
const contextWindow = typeof model.limit?.context === 'number' && model.limit.context > 0
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { classifyProvider, resolveBaseURL } from './protocol.js';
|
|
1
|
+
import { classifyProvider, classifyModel, resolveBaseURL } from './protocol.js';
|
|
2
2
|
function fmtK(n) {
|
|
3
3
|
if (!n || n <= 0)
|
|
4
4
|
return '-';
|
|
@@ -38,7 +38,7 @@ export function buildPanelEntries(input) {
|
|
|
38
38
|
continue;
|
|
39
39
|
if (classifyProvider(provider) === 'unsupported')
|
|
40
40
|
continue;
|
|
41
|
-
if (presetKeys.has(`${provider
|
|
41
|
+
if (presetKeys.has(`${resolveBaseURL(provider)}|${e.modelId}`))
|
|
42
42
|
continue;
|
|
43
43
|
const c = e.model.cost;
|
|
44
44
|
const caps = [e.model.reasoning ? '思考' : '', e.model.tool_call ? '工具' : '', e.model.attachment ? '多模态' : '']
|
|
@@ -165,6 +165,8 @@ export function buildModelTree(input) {
|
|
|
165
165
|
for (const e of input.catalog) {
|
|
166
166
|
if (!e.supported || e.providerId !== providerId)
|
|
167
167
|
continue;
|
|
168
|
+
if (classifyModel(provider, e.model) === 'unsupported')
|
|
169
|
+
continue;
|
|
168
170
|
if (g.seenModels.has(e.modelId))
|
|
169
171
|
continue;
|
|
170
172
|
g.seenModels.add(e.modelId);
|