@jsonstudio/llms 0.6.467 → 0.6.567

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.
Files changed (45) hide show
  1. package/dist/conversion/compat/actions/claude-thinking-tools.d.ts +15 -0
  2. package/dist/conversion/compat/actions/claude-thinking-tools.js +72 -0
  3. package/dist/conversion/compat/profiles/chat-gemini.json +1 -1
  4. package/dist/conversion/compat/profiles/responses-output2choices-test.json +12 -0
  5. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
  6. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +2 -0
  7. package/dist/conversion/hub/pipeline/hub-pipeline.js +15 -0
  8. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +15 -0
  9. package/dist/conversion/hub/process/chat-process.js +44 -17
  10. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +8 -0
  11. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +13 -8
  12. package/dist/conversion/hub/tool-session-compat.d.ts +26 -0
  13. package/dist/conversion/hub/tool-session-compat.js +299 -0
  14. package/dist/conversion/responses/responses-openai-bridge.d.ts +0 -1
  15. package/dist/conversion/responses/responses-openai-bridge.js +0 -71
  16. package/dist/conversion/shared/gemini-tool-utils.js +8 -0
  17. package/dist/conversion/shared/responses-output-builder.js +6 -68
  18. package/dist/conversion/shared/tool-governor.js +75 -4
  19. package/dist/conversion/shared/tool-mapping.js +14 -8
  20. package/dist/filters/special/request-toolcalls-stringify.js +5 -55
  21. package/dist/filters/special/request-tools-normalize.js +0 -19
  22. package/dist/guidance/index.js +25 -9
  23. package/dist/router/virtual-router/engine-health.d.ts +11 -0
  24. package/dist/router/virtual-router/engine-health.js +210 -0
  25. package/dist/router/virtual-router/engine-logging.d.ts +19 -0
  26. package/dist/router/virtual-router/engine-logging.js +165 -0
  27. package/dist/router/virtual-router/engine-selection.d.ts +32 -0
  28. package/dist/router/virtual-router/engine-selection.js +649 -0
  29. package/dist/router/virtual-router/engine.d.ts +4 -13
  30. package/dist/router/virtual-router/engine.js +64 -517
  31. package/dist/router/virtual-router/health-manager.d.ts +23 -0
  32. package/dist/router/virtual-router/health-manager.js +14 -0
  33. package/dist/router/virtual-router/message-utils.js +22 -0
  34. package/dist/router/virtual-router/routing-instructions.d.ts +6 -1
  35. package/dist/router/virtual-router/routing-instructions.js +129 -3
  36. package/dist/router/virtual-router/types.d.ts +6 -0
  37. package/dist/servertool/handlers/gemini-empty-reply-continue.d.ts +1 -0
  38. package/dist/servertool/handlers/gemini-empty-reply-continue.js +120 -0
  39. package/dist/servertool/handlers/stop-message-auto.d.ts +1 -0
  40. package/dist/servertool/handlers/stop-message-auto.js +147 -0
  41. package/dist/servertool/handlers/vision.js +105 -7
  42. package/dist/servertool/server-side-tools.d.ts +2 -0
  43. package/dist/servertool/server-side-tools.js +2 -0
  44. package/dist/tools/tool-registry.js +195 -4
  45. package/package.json +1 -1
@@ -60,7 +60,26 @@ function shouldRunVisionFlow(ctx) {
60
60
  if (followupFlag) {
61
61
  return false;
62
62
  }
63
- return record.hasImageAttachment === true || record.hasImageAttachment === 'true';
63
+ const hasImageAttachment = record.hasImageAttachment === true || record.hasImageAttachment === 'true';
64
+ if (!hasImageAttachment) {
65
+ return false;
66
+ }
67
+ // 若当前已经使用具备内建多模态能力的 Provider(例如 Gemini/Claude/ChatGPT 路径),
68
+ // 且未显式 forceVision,则不再触发额外的 vision 二跳,避免同一轮请求跑两次。
69
+ const forceVision = record.forceVision === true || record.forceVision === 'true';
70
+ if (forceVision) {
71
+ return true;
72
+ }
73
+ const providerType = typeof record.providerType === 'string' ? record.providerType.toLowerCase() : '';
74
+ const providerProtocol = typeof record.providerProtocol === 'string' ? record.providerProtocol.toLowerCase() : '';
75
+ const inlineMultimodal = providerType === 'gemini' ||
76
+ providerType === 'responses' ||
77
+ providerProtocol === 'gemini-chat' ||
78
+ providerProtocol === 'openai-responses';
79
+ if (inlineMultimodal) {
80
+ return false;
81
+ }
82
+ return true;
64
83
  }
65
84
  function getCapturedRequest(adapterContext) {
66
85
  if (!adapterContext || typeof adapterContext !== 'object') {
@@ -80,15 +99,15 @@ function buildVisionAnalysisPayload(source) {
80
99
  if (typeof source.model === 'string' && source.model.trim()) {
81
100
  payload.model = source.model.trim();
82
101
  }
83
- if (Array.isArray(source.messages)) {
84
- payload.messages = cloneJson(source.messages);
85
- }
86
- else {
102
+ const rawMessages = source.messages;
103
+ if (!Array.isArray(rawMessages) || !rawMessages.length) {
87
104
  return null;
88
105
  }
89
- if (Array.isArray(source.tools) && source.tools.length) {
90
- payload.tools = cloneJson(source.tools);
106
+ const visionMessages = buildVisionAnalysisMessages(rawMessages);
107
+ if (!visionMessages.length) {
108
+ return null;
91
109
  }
110
+ payload.messages = visionMessages;
92
111
  const parameters = source.parameters;
93
112
  if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) {
94
113
  const params = cloneJson(parameters);
@@ -96,6 +115,85 @@ function buildVisionAnalysisPayload(source) {
96
115
  }
97
116
  return payload;
98
117
  }
118
+ function buildVisionAnalysisMessages(sourceMessages) {
119
+ const latestUser = extractLatestUserMessageForVision(sourceMessages);
120
+ if (!latestUser) {
121
+ return [];
122
+ }
123
+ const userMessage = buildVisionUserMessage(latestUser);
124
+ if (!userMessage) {
125
+ return [];
126
+ }
127
+ const messages = [];
128
+ const systemMessage = buildVisionSystemMessage();
129
+ if (systemMessage) {
130
+ messages.push(systemMessage);
131
+ }
132
+ messages.push(userMessage);
133
+ return messages;
134
+ }
135
+ function extractLatestUserMessageForVision(sourceMessages) {
136
+ for (let idx = sourceMessages.length - 1; idx >= 0; idx -= 1) {
137
+ const msg = sourceMessages[idx];
138
+ if (!msg || typeof msg !== 'object' || Array.isArray(msg)) {
139
+ continue;
140
+ }
141
+ const role = msg.role;
142
+ if (typeof role === 'string' && role.trim().toLowerCase() === 'user') {
143
+ return cloneJson(msg);
144
+ }
145
+ }
146
+ return null;
147
+ }
148
+ function buildVisionSystemMessage() {
149
+ const content = '你是一名专业的图像分析助手。无论输入是界面截图、文档、图表、代码编辑器、应用窗口还是普通照片,都需要先用结构化、详细的自然语言完整描述画面内容(关键区域、文字信息、布局层次、颜色与对比度、元素之间的关系等),然后总结出与用户任务最相关的关键信息和潜在问题,最后给出具体、可执行的改进建议或结论,避免泛泛而谈。';
150
+ return {
151
+ role: 'system',
152
+ content
153
+ };
154
+ }
155
+ function buildVisionUserMessage(source) {
156
+ const roleRaw = source.role;
157
+ const role = typeof roleRaw === 'string' && roleRaw.trim().length
158
+ ? roleRaw.trim()
159
+ : 'user';
160
+ const rawContent = source.content;
161
+ const message = { role };
162
+ if (Array.isArray(rawContent)) {
163
+ const textParts = [];
164
+ const imageParts = [];
165
+ for (const part of rawContent) {
166
+ if (!part || typeof part !== 'object' || Array.isArray(part)) {
167
+ textParts.push(part);
168
+ continue;
169
+ }
170
+ const record = part;
171
+ const typeValue = typeof record.type === 'string' ? record.type.toLowerCase() : '';
172
+ if (typeValue.includes('image')) {
173
+ imageParts.push(part);
174
+ }
175
+ else {
176
+ textParts.push(part);
177
+ }
178
+ }
179
+ const combined = [];
180
+ if (textParts.length)
181
+ combined.push(...textParts);
182
+ if (imageParts.length)
183
+ combined.push(...imageParts);
184
+ if (!combined.length) {
185
+ return null;
186
+ }
187
+ message.content = combined;
188
+ }
189
+ else if (typeof rawContent === 'string' && rawContent.trim().length) {
190
+ message.content = rawContent.trim();
191
+ }
192
+ else {
193
+ return null;
194
+ }
195
+ return message;
196
+ }
99
197
  function buildVisionFollowupPayload(source, summary) {
100
198
  if (!source || typeof source !== 'object') {
101
199
  return null;
@@ -2,6 +2,8 @@ import type { JsonObject } from '../conversion/hub/types/json.js';
2
2
  import type { ServerSideToolEngineOptions, ServerSideToolEngineResult, ToolCall } from './types.js';
3
3
  import './handlers/web-search.js';
4
4
  import './handlers/vision.js';
5
+ import './handlers/gemini-empty-reply-continue.js';
6
+ import './handlers/stop-message-auto.js';
5
7
  export declare function runServerSideToolEngine(options: ServerSideToolEngineOptions): Promise<ServerSideToolEngineResult>;
6
8
  export declare function extractToolCalls(chatResponse: JsonObject): ToolCall[];
7
9
  export declare function cloneJson<T>(value: T): T;
@@ -1,6 +1,8 @@
1
1
  import { getServerToolHandler, listAutoServerToolHandlers } from './registry.js';
2
2
  import './handlers/web-search.js';
3
3
  import './handlers/vision.js';
4
+ import './handlers/gemini-empty-reply-continue.js';
5
+ import './handlers/stop-message-auto.js';
4
6
  export async function runServerSideToolEngine(options) {
5
7
  const base = asObject(options.chatResponse);
6
8
  if (!base) {
@@ -43,6 +43,164 @@ const toJson = (value) => {
43
43
  return '{}';
44
44
  }
45
45
  };
46
+ const APPLY_PATCH_SECTION_RE = /^\*\*\*\s+(Add|Update|Delete|Copy|Move)\s+File:\s*(.+)$/;
47
+ const sanitizeRelativePath = (value) => {
48
+ if (!value)
49
+ return null;
50
+ const trimmed = value.trim().replace(/\\/g, '/');
51
+ if (!trimmed)
52
+ return null;
53
+ if (trimmed.startsWith('/'))
54
+ return null;
55
+ if (/^[a-zA-Z]:/.test(trimmed))
56
+ return null;
57
+ if (trimmed.includes('\0'))
58
+ return null;
59
+ if (trimmed.includes('..')) {
60
+ const segments = trimmed.split('/');
61
+ if (segments.some((seg) => seg === '..')) {
62
+ return null;
63
+ }
64
+ }
65
+ return trimmed.replace(/\/{2,}/g, '/');
66
+ };
67
+ const collectExplicitPaths = (record) => {
68
+ const bucket = [];
69
+ const invalid = [];
70
+ const pushPath = (candidate) => {
71
+ const str = asString(candidate);
72
+ const normalized = sanitizeRelativePath(str);
73
+ if (normalized)
74
+ bucket.push(normalized);
75
+ else if (typeof candidate === 'string')
76
+ invalid.push(candidate);
77
+ };
78
+ const entries = [
79
+ record.paths,
80
+ record.path,
81
+ record.files,
82
+ record.file,
83
+ record.targets,
84
+ record.target_path
85
+ ];
86
+ for (const entry of entries) {
87
+ if (!entry)
88
+ continue;
89
+ if (typeof entry === 'string') {
90
+ pushPath(entry);
91
+ }
92
+ else if (Array.isArray(entry)) {
93
+ entry.forEach((item) => {
94
+ if (typeof item === 'string')
95
+ pushPath(item);
96
+ else if (isRecord(item))
97
+ pushPath(item.path);
98
+ });
99
+ }
100
+ else if (isRecord(entry)) {
101
+ pushPath(entry.path ?? entry.value);
102
+ }
103
+ }
104
+ return { paths: bucket, invalid };
105
+ };
106
+ const looksLikePatchBody = (text) => {
107
+ const trimmed = text.trim();
108
+ if (!trimmed)
109
+ return false;
110
+ const hunkRe = /^@@/m;
111
+ const diffLineRe = /^[ +-]/m;
112
+ return hunkRe.test(trimmed) || diffLineRe.test(trimmed);
113
+ };
114
+ const applyPathOverrides = (lines, overridePaths) => {
115
+ if (!overridePaths.length)
116
+ return;
117
+ const sections = [];
118
+ for (let i = 0; i < lines.length; i += 1) {
119
+ const match = lines[i].match(APPLY_PATCH_SECTION_RE);
120
+ if (match) {
121
+ sections.push({ index: i, action: match[1] });
122
+ }
123
+ }
124
+ if (!sections.length) {
125
+ const beginIdx = lines.findIndex((line) => line.trim() === '*** Begin Patch');
126
+ if (beginIdx >= 0) {
127
+ lines.splice(beginIdx + 1, 0, `*** Update File: ${overridePaths[0]}`);
128
+ }
129
+ return;
130
+ }
131
+ const limit = Math.min(sections.length, overridePaths.length);
132
+ for (let i = 0; i < limit; i += 1) {
133
+ lines[sections[i].index] = `*** ${sections[i].action} File: ${overridePaths[i]}`;
134
+ }
135
+ };
136
+ const normalizeApplyPatchInput = (rawPatch, options) => {
137
+ if (typeof rawPatch !== 'string') {
138
+ return null;
139
+ }
140
+ const text = rawPatch.replace(/\r\n/g, '\n');
141
+ const beginIdx = text.toLowerCase().indexOf('*** begin patch');
142
+ const endIdx = text.toLowerCase().lastIndexOf('*** end patch');
143
+ let candidate = '';
144
+ if (beginIdx >= 0 && endIdx >= 0 && endIdx > beginIdx) {
145
+ candidate = text.slice(beginIdx, endIdx + '*** End Patch'.length);
146
+ }
147
+ else if (options?.overridePaths && options.overridePaths.length) {
148
+ if (!looksLikePatchBody(text)) {
149
+ return null;
150
+ }
151
+ const headerPath = options.overridePaths[0];
152
+ candidate = ['*** Begin Patch', `*** Update File: ${headerPath}`, text.trim(), '*** End Patch'].join('\n');
153
+ }
154
+ else {
155
+ return null;
156
+ }
157
+ const lines = candidate.split(/\r?\n/);
158
+ if (!lines.length) {
159
+ return null;
160
+ }
161
+ let firstIdx = 0;
162
+ while (firstIdx < lines.length && lines[firstIdx].trim().length === 0) {
163
+ firstIdx += 1;
164
+ }
165
+ if (firstIdx >= lines.length) {
166
+ return null;
167
+ }
168
+ const firstLineRaw = lines[firstIdx];
169
+ const firstLineTrimmed = firstLineRaw.trim();
170
+ if (!firstLineTrimmed.toLowerCase().startsWith('*** begin patch')) {
171
+ return null;
172
+ }
173
+ if (firstLineTrimmed !== '*** Begin Patch') {
174
+ lines[firstIdx] = '*** Begin Patch';
175
+ }
176
+ let lastIdx = lines.length - 1;
177
+ while (lastIdx > firstIdx && lines[lastIdx].trim().length === 0) {
178
+ lastIdx -= 1;
179
+ }
180
+ if (lastIdx > firstIdx) {
181
+ const lastLineTrimmed = lines[lastIdx].trim();
182
+ if (lastLineTrimmed.toLowerCase().startsWith('*** end patch') && lastLineTrimmed !== '*** End Patch') {
183
+ lines[lastIdx] = '*** End Patch';
184
+ }
185
+ }
186
+ if (options?.overridePaths && options.overridePaths.length) {
187
+ applyPathOverrides(lines, options.overridePaths);
188
+ }
189
+ const normalized = lines.join('\n');
190
+ if (!normalized.includes('*** Begin Patch') || !normalized.includes('*** End Patch')) {
191
+ return null;
192
+ }
193
+ return normalized;
194
+ };
195
+ const logApplyPatchValidatorError = (message) => {
196
+ try {
197
+ // eslint-disable-next-line no-console
198
+ console.error(`\x1b[31m[apply_patch][validator] ${message}\x1b[0m`);
199
+ }
200
+ catch {
201
+ /* ignore */
202
+ }
203
+ };
46
204
  const detectForbiddenWrite = (script) => {
47
205
  const normalized = script.toLowerCase();
48
206
  if (!normalized) {
@@ -95,12 +253,45 @@ export function validateToolCall(name, argsString) {
95
253
  const rawArgs = tryParseJson(typeof argsString === 'string' ? argsString : '{}');
96
254
  switch (normalizedName) {
97
255
  case 'apply_patch': {
98
- const input = asString(rawArgs.input);
99
- const patch = asString(rawArgs.patch) ?? input;
100
- if (!patch) {
256
+ const record = rawArgs;
257
+ const inputField = asString(record.input);
258
+ const patchField = asString(record.patch);
259
+ const rawStr = typeof argsString === 'string' ? argsString : '';
260
+ let patchRaw = null;
261
+ if (patchField) {
262
+ patchRaw = patchField;
263
+ }
264
+ else if (inputField) {
265
+ patchRaw = inputField;
266
+ }
267
+ else if (rawStr && /\*\*\*\s+Begin Patch/i.test(rawStr)) {
268
+ patchRaw = rawStr;
269
+ }
270
+ if (!patchRaw) {
271
+ logApplyPatchValidatorError('missing apply_patch patch/input payload');
101
272
  return { ok: false, reason: 'missing_input' };
102
273
  }
103
- return { ok: true, normalizedArgs: toJson({ input: patch, patch }) };
274
+ const { paths: explicitPaths, invalid } = collectExplicitPaths(record);
275
+ if (invalid.length) {
276
+ invalid.forEach((value) => logApplyPatchValidatorError(`invalid path argument: ${value}`));
277
+ }
278
+ const normalizedPatch = normalizeApplyPatchInput(patchRaw, { overridePaths: explicitPaths });
279
+ if (!normalizedPatch) {
280
+ logApplyPatchValidatorError('patch text missing *** Begin Patch/*** End Patch or failed to auto-wrap with provided paths');
281
+ return { ok: false, reason: 'invalid_patch_header' };
282
+ }
283
+ // Canonical, parameterized shape; keep input for backwards compatibility.
284
+ const payload = {
285
+ patch: normalizedPatch,
286
+ input: normalizedPatch
287
+ };
288
+ if (explicitPaths.length) {
289
+ payload.paths = explicitPaths;
290
+ }
291
+ return {
292
+ ok: true,
293
+ normalizedArgs: toJson(payload)
294
+ };
104
295
  }
105
296
  case 'shell': {
106
297
  const rawCommand = rawArgs.command;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsonstudio/llms",
3
- "version": "0.6.467",
3
+ "version": "0.6.567",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",