@jsonstudio/llms 0.6.6 → 0.6.54

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 (39) hide show
  1. package/dist/conversion/compat/profiles/chat-glm.json +17 -0
  2. package/dist/conversion/compat/profiles/chat-iflow.json +36 -0
  3. package/dist/conversion/compat/profiles/chat-lmstudio.json +37 -0
  4. package/dist/conversion/compat/profiles/chat-qwen.json +18 -0
  5. package/dist/conversion/compat/profiles/responses-c4m.json +45 -0
  6. package/dist/conversion/config/compat-profiles.json +38 -0
  7. package/dist/conversion/config/sample-config.json +314 -0
  8. package/dist/conversion/config/version-switch.json +150 -0
  9. package/dist/conversion/hub/pipeline/compat/compat-engine.d.ts +4 -0
  10. package/dist/conversion/hub/pipeline/compat/compat-engine.js +667 -0
  11. package/dist/conversion/hub/pipeline/compat/compat-profile-store.d.ts +2 -0
  12. package/dist/conversion/hub/pipeline/compat/compat-profile-store.js +76 -0
  13. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +62 -0
  14. package/dist/conversion/hub/pipeline/compat/compat-types.js +1 -0
  15. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +1 -0
  16. package/dist/conversion/hub/pipeline/hub-pipeline.js +76 -28
  17. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +10 -12
  18. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.d.ts +14 -0
  19. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.js +23 -0
  20. package/dist/conversion/hub/response/provider-response.js +18 -0
  21. package/dist/conversion/hub/response/response-mappers.d.ts +1 -1
  22. package/dist/conversion/hub/response/response-mappers.js +2 -12
  23. package/dist/conversion/shared/responses-output-builder.js +22 -43
  24. package/dist/conversion/shared/responses-response-utils.js +1 -47
  25. package/dist/conversion/shared/text-markup-normalizer.js +2 -2
  26. package/dist/conversion/shared/tool-canonicalizer.js +16 -118
  27. package/dist/conversion/shared/tool-mapping.js +0 -30
  28. package/dist/filters/config/openai-openai.fieldmap.json +18 -0
  29. package/dist/router/virtual-router/bootstrap.js +16 -7
  30. package/dist/router/virtual-router/classifier.js +40 -37
  31. package/dist/router/virtual-router/default-thinking-keywords.d.ts +1 -0
  32. package/dist/router/virtual-router/default-thinking-keywords.js +13 -0
  33. package/dist/router/virtual-router/features.js +340 -11
  34. package/dist/router/virtual-router/token-counter.d.ts +2 -0
  35. package/dist/router/virtual-router/token-counter.js +105 -0
  36. package/dist/router/virtual-router/types.d.ts +2 -0
  37. package/dist/router/virtual-router/types.js +2 -2
  38. package/dist/sse/sse-to-json/builders/response-builder.js +1 -0
  39. package/package.json +3 -3
@@ -12,133 +12,31 @@ function repairArgumentsToString(args) {
12
12
  return String(args);
13
13
  }
14
14
  }
15
- function extractStringContent(value) {
16
- if (typeof value === 'string') {
17
- return value;
18
- }
19
- if (Array.isArray(value)) {
20
- const parts = [];
21
- for (const entry of value) {
22
- if (typeof entry === 'string') {
23
- parts.push(entry);
24
- }
25
- else if (entry && typeof entry === 'object') {
26
- const text = entry.text;
27
- if (typeof text === 'string') {
28
- parts.push(text);
29
- }
30
- }
31
- }
32
- if (parts.length) {
33
- return parts.join('\n');
34
- }
35
- }
36
- return null;
37
- }
38
- function readApplyPatchArgument(args) {
39
- if (!args || typeof args !== 'object')
40
- return undefined;
41
- const input = args.input;
42
- if (typeof input === 'string' && input.trim().length > 0) {
43
- return input;
44
- }
45
- const legacy = args.patch;
46
- if (typeof legacy === 'string' && legacy.trim().length > 0) {
47
- return legacy;
48
- }
49
- return undefined;
50
- }
51
- function hasApplyPatchInput(argText) {
52
- if (!argText) {
53
- return false;
54
- }
55
- try {
56
- const parsed = JSON.parse(argText);
57
- const content = readApplyPatchArgument(parsed);
58
- return typeof content === 'string' && content.trim().length > 0;
59
- }
60
- catch {
61
- return false;
62
- }
63
- }
64
- function extractUnifiedDiff(content) {
65
- if (!content)
66
- return undefined;
67
- const begin = content.indexOf('*** Begin Patch');
68
- const end = content.indexOf('*** End Patch');
69
- if (begin >= 0 && end > begin) {
70
- return content.slice(begin, end + '*** End Patch'.length).trim();
71
- }
72
- return undefined;
73
- }
74
- function createToolCallId() {
75
- return `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
76
- }
77
15
  export function canonicalizeChatResponseTools(payload) {
78
16
  try {
79
17
  const out = isObject(payload) ? JSON.parse(JSON.stringify(payload)) : payload;
80
18
  const choices = Array.isArray(out?.choices) ? out.choices : [];
81
19
  for (const ch of choices) {
82
20
  const msg = ch && ch.message ? ch.message : undefined;
83
- const toolCalls = Array.isArray(msg?.tool_calls) ? msg.tool_calls : undefined;
84
- const hasToolCalls = Array.isArray(toolCalls) && toolCalls.length > 0;
85
- const originalContentText = extractStringContent(msg?.content);
86
- const harvestedPatch = typeof originalContentText === 'string' ? extractUnifiedDiff(originalContentText) : undefined;
87
- if (hasToolCalls) {
88
- // ensure arguments is string and content is null when tool_calls present
89
- try {
90
- if (!ch.finish_reason)
91
- ch.finish_reason = 'tool_calls';
92
- }
93
- catch { /* ignore */ }
94
- try {
95
- if (msg && typeof msg === 'object')
96
- msg.content = null;
97
- }
98
- catch { /* ignore */ }
99
- for (const tc of toolCalls) {
100
- try {
101
- const fn = tc && tc.function ? tc.function : undefined;
102
- if (fn) {
103
- const repaired = repairArgumentsToString(fn.arguments);
104
- let nextArgs = repaired;
105
- try {
106
- const parsed = JSON.parse(repaired);
107
- const diffText = readApplyPatchArgument(parsed);
108
- if (typeof diffText === 'string' && diffText.trim().length > 0) {
109
- nextArgs = JSON.stringify({ input: diffText });
110
- }
111
- }
112
- catch {
113
- // fallback to repaired string
114
- }
115
- fn.arguments = nextArgs;
116
- if (typeof fn.name === 'string' &&
117
- fn.name === 'apply_patch' &&
118
- !hasApplyPatchInput(fn.arguments) &&
119
- harvestedPatch) {
120
- fn.arguments = JSON.stringify({ input: harvestedPatch });
121
- }
122
- }
123
- }
124
- catch { /* ignore */ }
125
- }
21
+ const tcs = Array.isArray(msg?.tool_calls) ? msg.tool_calls : [];
22
+ if (!tcs || !tcs.length)
126
23
  continue;
24
+ // ensure arguments is string and content is null when tool_calls present
25
+ try {
26
+ if (!ch.finish_reason)
27
+ ch.finish_reason = 'tool_calls';
127
28
  }
128
- if (harvestedPatch) {
129
- const id = createToolCallId();
130
- const toolCall = {
131
- id,
132
- type: 'function',
133
- function: {
134
- name: 'apply_patch',
135
- arguments: JSON.stringify({ input: harvestedPatch })
136
- }
137
- };
138
- try {
139
- msg.tool_calls = [toolCall];
29
+ catch { /* ignore */ }
30
+ try {
31
+ if (msg && typeof msg === 'object')
140
32
  msg.content = null;
141
- ch.finish_reason = 'tool_calls';
33
+ }
34
+ catch { /* ignore */ }
35
+ for (const tc of tcs) {
36
+ try {
37
+ const fn = tc && tc.function ? tc.function : undefined;
38
+ if (fn)
39
+ fn.arguments = repairArgumentsToString(fn.arguments);
142
40
  }
143
41
  catch { /* ignore */ }
144
42
  }
@@ -15,28 +15,6 @@ const DEFAULT_SANITIZER = (value) => {
15
15
  }
16
16
  return undefined;
17
17
  };
18
- const APPLY_PATCH_TOOL_NAME = 'apply_patch';
19
- const APPLY_PATCH_ARG_KEY = 'input';
20
- function isApplyPatchTool(name) {
21
- return typeof name === 'string' && name.trim() === APPLY_PATCH_TOOL_NAME;
22
- }
23
- function buildApplyPatchParameters() {
24
- return {
25
- type: 'object',
26
- properties: {
27
- [APPLY_PATCH_ARG_KEY]: {
28
- type: 'string',
29
- description: 'Unified diff patch content (*** Begin Patch ... *** End Patch)'
30
- }
31
- },
32
- required: [APPLY_PATCH_ARG_KEY],
33
- additionalProperties: false
34
- };
35
- }
36
- function applyPatchSchemaToFunction(fnNode) {
37
- fnNode.parameters = buildApplyPatchParameters();
38
- fnNode.strict = true;
39
- }
40
18
  function resolveToolName(candidate, options) {
41
19
  const sanitized = options?.sanitizeName?.(candidate);
42
20
  if (typeof sanitized === 'string' && sanitized.trim().length) {
@@ -102,9 +80,6 @@ export function bridgeToolToChatDefinition(rawTool, options) {
102
80
  if (strict !== undefined) {
103
81
  fnOut.strict = strict;
104
82
  }
105
- if (isApplyPatchTool(name)) {
106
- applyPatchSchemaToFunction(fnOut);
107
- }
108
83
  return {
109
84
  type: normalizedType,
110
85
  function: fnOut
@@ -156,11 +131,6 @@ export function chatToolToBridgeDefinition(rawTool, options) {
156
131
  if (strict !== undefined) {
157
132
  fnOut.strict = strict;
158
133
  }
159
- if (isApplyPatchTool(name)) {
160
- applyPatchSchemaToFunction(fnOut);
161
- responseShape.parameters = buildApplyPatchParameters();
162
- responseShape.strict = true;
163
- }
164
134
  responseShape.function = fnOut;
165
135
  return responseShape;
166
136
  }
@@ -0,0 +1,18 @@
1
+ {
2
+ "request": [
3
+ {
4
+ "sourcePath": "messages[*].tool_calls[*].function.arguments",
5
+ "targetPath": "messages[*].tool_calls[*].function.arguments",
6
+ "type": "string",
7
+ "transform": "stringifyJson"
8
+ }
9
+ ],
10
+ "response": [
11
+ {
12
+ "sourcePath": "choices[*].message.tool_calls[*].function.arguments",
13
+ "targetPath": "choices[*].message.tool_calls[*].function.arguments",
14
+ "type": "string",
15
+ "transform": "stringifyJson"
16
+ }
17
+ ]
18
+ }
@@ -1,6 +1,6 @@
1
1
  import { VirtualRouterError, VirtualRouterErrorCode } from './types.js';
2
2
  const DEFAULT_CLASSIFIER = {
3
- longContextThresholdTokens: 60000,
3
+ longContextThresholdTokens: 180000,
4
4
  thinkingKeywords: ['think step', 'analysis', 'reasoning', '仔细分析', '深度思考'],
5
5
  codingKeywords: ['apply_patch', 'write_file', 'create_file', 'shell', '修改文件', '写入文件'],
6
6
  backgroundKeywords: ['background', 'context dump', '上下文'],
@@ -208,7 +208,7 @@ function normalizeProvider(providerId, raw) {
208
208
  ? provider.baseUrl.trim()
209
209
  : '';
210
210
  const headers = normalizeHeaders(provider.headers);
211
- const compatibilityProfile = resolveCompatibilityProfile(provider);
211
+ const compatibilityProfile = resolveCompatibilityProfile(providerId, provider);
212
212
  const responsesConfig = normalizeResponsesConfig(provider);
213
213
  const processMode = normalizeProcessMode(provider.process);
214
214
  return {
@@ -233,12 +233,21 @@ function normalizeResponsesConfig(provider) {
233
233
  }
234
234
  return undefined;
235
235
  }
236
- function resolveCompatibilityProfile(provider) {
237
- const compat = provider.compat;
238
- if (typeof compat === 'string' && compat.trim().length > 0) {
239
- return compat.trim();
236
+ function resolveCompatibilityProfile(providerId, provider) {
237
+ if (typeof provider.compatibilityProfile === 'string' && provider.compatibilityProfile.trim()) {
238
+ return provider.compatibilityProfile.trim();
240
239
  }
241
- return 'default';
240
+ const legacyFields = [];
241
+ if (typeof provider.compat === 'string') {
242
+ legacyFields.push('compat');
243
+ }
244
+ if (typeof provider.compatibility_profile === 'string') {
245
+ legacyFields.push('compatibility_profile');
246
+ }
247
+ if (legacyFields.length > 0) {
248
+ throw new VirtualRouterError(`Provider "${providerId}" uses legacy compatibility field(s): ${legacyFields.join(', ')}. Rename to "compatibilityProfile".`, VirtualRouterErrorCode.CONFIG_ERROR);
249
+ }
250
+ return 'compat:passthrough';
242
251
  }
243
252
  function normalizeProcessMode(value) {
244
253
  if (typeof value !== 'string') {
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_ROUTE, ROUTE_PRIORITY } from './types.js';
2
- const DEFAULT_LONG_CONTEXT_THRESHOLD = 60000;
2
+ const DEFAULT_LONG_CONTEXT_THRESHOLD = 180000;
3
3
  export class RoutingClassifier {
4
4
  config;
5
5
  constructor(config) {
@@ -10,55 +10,58 @@ export class RoutingClassifier {
10
10
  };
11
11
  }
12
12
  classify(features) {
13
- const evaluations = [
14
- {
15
- route: 'vision',
13
+ const lastToolCategory = features.lastAssistantToolCategory;
14
+ const reachedLongContext = features.estimatedTokens >= (this.config.longContextThresholdTokens ?? DEFAULT_LONG_CONTEXT_THRESHOLD);
15
+ const thinkingKeywordHit = features.hasThinkingKeyword ||
16
+ containsKeywords(features.userTextSample, this.config.thinkingKeywords ?? []);
17
+ const codingContinuation = lastToolCategory === 'write';
18
+ const thinkingContinuation = lastToolCategory === 'read';
19
+ const searchContinuation = lastToolCategory === 'search';
20
+ const toolsContinuation = lastToolCategory === 'other';
21
+ const evaluationMap = {
22
+ vision: {
16
23
  triggered: features.hasVisionTool && features.hasImageAttachment,
17
24
  reason: 'vision:requires-tool+image'
18
25
  },
19
- {
20
- route: 'websearch',
21
- triggered: features.hasWebTool,
22
- reason: 'websearch:web-tools-detected'
26
+ longcontext: {
27
+ triggered: reachedLongContext,
28
+ reason: 'longcontext:token-threshold'
23
29
  },
24
- {
25
- route: 'coding',
26
- triggered: features.hasCodingTool,
27
- reason: 'coding:coding-tools-detected'
30
+ websearch: {
31
+ triggered: features.hasWebTool || searchContinuation,
32
+ reason: searchContinuation ? 'websearch:last-tool-search' : 'websearch:web-tools-detected'
28
33
  },
29
- {
30
- route: 'tools',
31
- triggered: features.hasTools || features.hasToolCallResponses,
32
- reason: 'tools:tool-request-detected'
34
+ coding: {
35
+ triggered: codingContinuation,
36
+ reason: 'coding:last-tool-write'
33
37
  },
34
- {
35
- route: 'longcontext',
36
- triggered: features.estimatedTokens >= (this.config.longContextThresholdTokens ?? DEFAULT_LONG_CONTEXT_THRESHOLD),
37
- reason: 'longcontext:token-threshold'
38
+ thinking: {
39
+ triggered: thinkingContinuation || thinkingKeywordHit,
40
+ reason: thinkingContinuation ? 'thinking:last-tool-read' : 'thinking:keywords'
38
41
  },
39
- {
40
- route: 'thinking',
41
- triggered: features.hasThinkingKeyword ||
42
- containsKeywords(features.userTextSample, this.config.thinkingKeywords ?? []),
43
- reason: 'thinking:keywords'
42
+ tools: {
43
+ triggered: toolsContinuation || features.hasTools || features.hasToolCallResponses,
44
+ reason: toolsContinuation ? 'tools:last-tool-other' : 'tools:tool-request-detected'
44
45
  },
45
- {
46
- route: 'background',
46
+ background: {
47
47
  triggered: containsKeywords(features.userTextSample, this.config.backgroundKeywords ?? []),
48
48
  reason: 'background:keywords'
49
49
  }
50
- ];
51
- const triggeredEvaluations = evaluations.filter((evaluation) => evaluation.triggered);
52
- const orderedRoutes = this.orderRoutes(triggeredEvaluations.map((entry) => entry.route));
53
- const chosenRoute = orderedRoutes.length ? orderedRoutes[0] : DEFAULT_ROUTE;
54
- const chosenReason = triggeredEvaluations.find((entry) => entry.route === chosenRoute)?.reason || 'fallback:default';
55
- const candidates = this.ensureDefaultCandidate(orderedRoutes);
56
- return this.buildResult(chosenRoute, chosenReason, evaluations, candidates);
50
+ };
51
+ for (const routeName of ROUTE_PRIORITY) {
52
+ const evaluation = evaluationMap[routeName];
53
+ if (evaluation && evaluation.triggered) {
54
+ const candidates = this.ensureDefaultCandidate([routeName]);
55
+ return this.buildResult(routeName, evaluation.reason, evaluationMap, candidates);
56
+ }
57
+ }
58
+ const candidates = this.ensureDefaultCandidate([DEFAULT_ROUTE]);
59
+ return this.buildResult(DEFAULT_ROUTE, 'fallback:default', evaluationMap, candidates);
57
60
  }
58
61
  buildResult(routeName, chosenReason, evaluations, candidates) {
59
- const diagnostics = evaluations
60
- .filter((evaluation) => evaluation.triggered)
61
- .map((evaluation) => evaluation.reason);
62
+ const diagnostics = Object.entries(evaluations)
63
+ .filter(([_, evaluation]) => evaluation.triggered)
64
+ .map(([_, evaluation]) => evaluation.reason);
62
65
  const reasoningParts = [chosenReason, ...diagnostics.filter((reason) => reason !== chosenReason)];
63
66
  return {
64
67
  routeName,
@@ -0,0 +1 @@
1
+ export declare const DEFAULT_THINKING_KEYWORDS: string[];
@@ -0,0 +1,13 @@
1
+ export const DEFAULT_THINKING_KEYWORDS = [
2
+ '思考',
3
+ '深度思考',
4
+ '分析',
5
+ '推理',
6
+ '思路',
7
+ '一步一步',
8
+ '慢慢',
9
+ 'think',
10
+ 'thinking',
11
+ 'step by step',
12
+ 'reason'
13
+ ];