@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.
- package/dist/conversion/compat/profiles/chat-glm.json +17 -0
- package/dist/conversion/compat/profiles/chat-iflow.json +36 -0
- package/dist/conversion/compat/profiles/chat-lmstudio.json +37 -0
- package/dist/conversion/compat/profiles/chat-qwen.json +18 -0
- package/dist/conversion/compat/profiles/responses-c4m.json +45 -0
- package/dist/conversion/config/compat-profiles.json +38 -0
- package/dist/conversion/config/sample-config.json +314 -0
- package/dist/conversion/config/version-switch.json +150 -0
- package/dist/conversion/hub/pipeline/compat/compat-engine.d.ts +4 -0
- package/dist/conversion/hub/pipeline/compat/compat-engine.js +667 -0
- package/dist/conversion/hub/pipeline/compat/compat-profile-store.d.ts +2 -0
- package/dist/conversion/hub/pipeline/compat/compat-profile-store.js +76 -0
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +62 -0
- package/dist/conversion/hub/pipeline/compat/compat-types.js +1 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +1 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +76 -28
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +10 -12
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.d.ts +14 -0
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.js +23 -0
- package/dist/conversion/hub/response/provider-response.js +18 -0
- package/dist/conversion/hub/response/response-mappers.d.ts +1 -1
- package/dist/conversion/hub/response/response-mappers.js +2 -12
- package/dist/conversion/shared/responses-output-builder.js +22 -43
- package/dist/conversion/shared/responses-response-utils.js +1 -47
- package/dist/conversion/shared/text-markup-normalizer.js +2 -2
- package/dist/conversion/shared/tool-canonicalizer.js +16 -118
- package/dist/conversion/shared/tool-mapping.js +0 -30
- package/dist/filters/config/openai-openai.fieldmap.json +18 -0
- package/dist/router/virtual-router/bootstrap.js +16 -7
- package/dist/router/virtual-router/classifier.js +40 -37
- package/dist/router/virtual-router/default-thinking-keywords.d.ts +1 -0
- package/dist/router/virtual-router/default-thinking-keywords.js +13 -0
- package/dist/router/virtual-router/features.js +340 -11
- package/dist/router/virtual-router/token-counter.d.ts +2 -0
- package/dist/router/virtual-router/token-counter.js +105 -0
- package/dist/router/virtual-router/types.d.ts +2 -0
- package/dist/router/virtual-router/types.js +2 -2
- package/dist/sse/sse-to-json/builders/response-builder.js +1 -0
- 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
|
|
84
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
238
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
reason: 'websearch:web-tools-detected'
|
|
26
|
+
longcontext: {
|
|
27
|
+
triggered: reachedLongContext,
|
|
28
|
+
reason: 'longcontext:token-threshold'
|
|
23
29
|
},
|
|
24
|
-
{
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
reason: 'tools:tool-request-detected'
|
|
34
|
+
coding: {
|
|
35
|
+
triggered: codingContinuation,
|
|
36
|
+
reason: 'coding:last-tool-write'
|
|
33
37
|
},
|
|
34
|
-
{
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
reason: 'longcontext:token-threshold'
|
|
38
|
+
thinking: {
|
|
39
|
+
triggered: thinkingContinuation || thinkingKeywordHit,
|
|
40
|
+
reason: thinkingContinuation ? 'thinking:last-tool-read' : 'thinking:keywords'
|
|
38
41
|
},
|
|
39
|
-
{
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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[];
|