@jsonstudio/llms 0.6.34 → 0.6.74
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/codecs/gemini-openai-codec.js +1 -2
- package/dist/conversion/codecs/responses-openai-codec.js +16 -1
- 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 +0 -13
- 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/responses/responses-openai-bridge.d.ts +1 -0
- package/dist/conversion/responses/responses-openai-bridge.js +71 -0
- 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-filter-pipeline.js +63 -21
- package/dist/conversion/shared/tool-mapping.js +52 -32
- package/dist/filters/config/openai-openai.fieldmap.json +18 -0
- package/dist/filters/special/request-tools-normalize.js +20 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/router/virtual-router/bootstrap.js +18 -33
- package/dist/router/virtual-router/classifier.js +51 -77
- package/dist/router/virtual-router/features.js +338 -111
- package/dist/router/virtual-router/types.d.ts +2 -4
- package/dist/router/virtual-router/types.js +2 -2
- package/dist/sse/sse-to-json/builders/response-builder.js +1 -0
- package/dist/tools/tool-registry.js +4 -3
- package/package.json +3 -3
|
@@ -1,73 +1,67 @@
|
|
|
1
1
|
import { DEFAULT_ROUTE, ROUTE_PRIORITY } from './types.js';
|
|
2
|
-
import { DEFAULT_THINKING_KEYWORDS } from './default-thinking-keywords.js';
|
|
3
2
|
const DEFAULT_LONG_CONTEXT_THRESHOLD = 180000;
|
|
4
3
|
export class RoutingClassifier {
|
|
5
4
|
config;
|
|
6
5
|
constructor(config) {
|
|
7
|
-
const keywordConfig = normalizeKeywordConfig(config);
|
|
8
6
|
this.config = {
|
|
9
7
|
longContextThresholdTokens: config.longContextThresholdTokens ?? DEFAULT_LONG_CONTEXT_THRESHOLD,
|
|
10
|
-
thinkingKeywords:
|
|
11
|
-
backgroundKeywords:
|
|
12
|
-
visionKeywords: keywordConfig.vision,
|
|
13
|
-
codingKeywords: keywordConfig.coding
|
|
8
|
+
thinkingKeywords: normalizeList(config.thinkingKeywords, ['think step', 'analysis', 'reasoning']),
|
|
9
|
+
backgroundKeywords: normalizeList(config.backgroundKeywords, ['background', 'context dump'])
|
|
14
10
|
};
|
|
15
11
|
}
|
|
16
12
|
classify(features) {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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: {
|
|
23
|
+
triggered: features.hasVisionTool && features.hasImageAttachment,
|
|
24
|
+
reason: 'vision:requires-tool+image'
|
|
22
25
|
},
|
|
23
|
-
{
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
containsKeywords(features.userTextSample, this.config.thinkingKeywords ?? []) ||
|
|
27
|
-
features.previousToolCategory === 'context_read' ||
|
|
28
|
-
features.previousToolCategory === 'plan',
|
|
29
|
-
reason: 'thinking:keywords'
|
|
26
|
+
longcontext: {
|
|
27
|
+
triggered: reachedLongContext,
|
|
28
|
+
reason: 'longcontext:token-threshold'
|
|
30
29
|
},
|
|
31
|
-
{
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
reason: 'background:keywords'
|
|
30
|
+
websearch: {
|
|
31
|
+
triggered: features.hasWebTool || searchContinuation,
|
|
32
|
+
reason: searchContinuation ? 'websearch:last-tool-search' : 'websearch:web-tools-detected'
|
|
35
33
|
},
|
|
36
|
-
{
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
features.previousToolCategory === 'vision',
|
|
40
|
-
reason: 'vision:requires-tool+image'
|
|
34
|
+
coding: {
|
|
35
|
+
triggered: codingContinuation,
|
|
36
|
+
reason: 'coding:last-tool-write'
|
|
41
37
|
},
|
|
42
|
-
{
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
reason: 'websearch:web-tools-detected'
|
|
38
|
+
thinking: {
|
|
39
|
+
triggered: thinkingContinuation || thinkingKeywordHit,
|
|
40
|
+
reason: thinkingContinuation ? 'thinking:last-tool-read' : 'thinking:keywords'
|
|
46
41
|
},
|
|
47
|
-
{
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
containsKeywords(features.userTextSample, this.config.codingKeywords ?? []) ||
|
|
51
|
-
features.previousToolCategory === 'coding',
|
|
52
|
-
reason: 'coding:coding-tools-detected'
|
|
42
|
+
tools: {
|
|
43
|
+
triggered: toolsContinuation || features.hasTools || features.hasToolCallResponses,
|
|
44
|
+
reason: toolsContinuation ? 'tools:last-tool-other' : 'tools:tool-request-detected'
|
|
53
45
|
},
|
|
54
|
-
{
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
46
|
+
background: {
|
|
47
|
+
triggered: containsKeywords(features.userTextSample, this.config.backgroundKeywords ?? []),
|
|
48
|
+
reason: 'background:keywords'
|
|
49
|
+
}
|
|
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);
|
|
58
56
|
}
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
const chosenRoute = orderedRoutes.length ? orderedRoutes[0] : DEFAULT_ROUTE;
|
|
63
|
-
const chosenReason = triggeredEvaluations.find((entry) => entry.route === chosenRoute)?.reason || 'fallback:default';
|
|
64
|
-
const candidates = this.ensureDefaultCandidate(orderedRoutes);
|
|
65
|
-
return this.buildResult(chosenRoute, chosenReason, evaluations, candidates);
|
|
57
|
+
}
|
|
58
|
+
const candidates = this.ensureDefaultCandidate([DEFAULT_ROUTE]);
|
|
59
|
+
return this.buildResult(DEFAULT_ROUTE, 'fallback:default', evaluationMap, candidates);
|
|
66
60
|
}
|
|
67
61
|
buildResult(routeName, chosenReason, evaluations, candidates) {
|
|
68
|
-
const diagnostics = evaluations
|
|
69
|
-
.filter((evaluation) => evaluation.triggered)
|
|
70
|
-
.map((evaluation) => evaluation.reason);
|
|
62
|
+
const diagnostics = Object.entries(evaluations)
|
|
63
|
+
.filter(([_, evaluation]) => evaluation.triggered)
|
|
64
|
+
.map(([_, evaluation]) => evaluation.reason);
|
|
71
65
|
const reasoningParts = [chosenReason, ...diagnostics.filter((reason) => reason !== chosenReason)];
|
|
72
66
|
return {
|
|
73
67
|
routeName,
|
|
@@ -93,6 +87,12 @@ export class RoutingClassifier {
|
|
|
93
87
|
return index >= 0 ? index : ROUTE_PRIORITY.length;
|
|
94
88
|
}
|
|
95
89
|
}
|
|
90
|
+
function normalizeList(source, fallback) {
|
|
91
|
+
if (!source || source.length === 0) {
|
|
92
|
+
return fallback;
|
|
93
|
+
}
|
|
94
|
+
return source.map((item) => item.toLowerCase());
|
|
95
|
+
}
|
|
96
96
|
function containsKeywords(text, keywords) {
|
|
97
97
|
if (!text || !keywords.length) {
|
|
98
98
|
return false;
|
|
@@ -100,29 +100,3 @@ function containsKeywords(text, keywords) {
|
|
|
100
100
|
const normalized = text.toLowerCase();
|
|
101
101
|
return keywords.some((keyword) => normalized.includes(keyword));
|
|
102
102
|
}
|
|
103
|
-
function normalizeKeywordConfig(config) {
|
|
104
|
-
const injections = config.keywordInjections ?? {};
|
|
105
|
-
return {
|
|
106
|
-
thinking: mergeKeywordLists(DEFAULT_THINKING_KEYWORDS, config.thinkingKeywords, injections.thinking),
|
|
107
|
-
background: mergeKeywordLists(['background', 'context dump', '上下文'], config.backgroundKeywords, injections.background),
|
|
108
|
-
vision: mergeKeywordLists(['vision', 'image', 'picture', 'photo'], config.visionKeywords, injections.vision),
|
|
109
|
-
coding: mergeKeywordLists(config.codingKeywords, injections.coding)
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
function mergeKeywordLists(...lists) {
|
|
113
|
-
const set = new Set();
|
|
114
|
-
for (const list of lists) {
|
|
115
|
-
if (!Array.isArray(list)) {
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
for (const item of list) {
|
|
119
|
-
if (!item)
|
|
120
|
-
continue;
|
|
121
|
-
const normalized = String(item).toLowerCase();
|
|
122
|
-
if (normalized) {
|
|
123
|
-
set.add(normalized);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return Array.from(set);
|
|
128
|
-
}
|
|
@@ -1,6 +1,125 @@
|
|
|
1
1
|
import { countRequestTokens } from './token-counter.js';
|
|
2
2
|
const THINKING_KEYWORDS = ['let me think', 'chain of thought', 'cot', 'reason step', 'deliberate'];
|
|
3
3
|
const WEB_TOOL_KEYWORDS = ['websearch', 'web_search', 'web-search', 'webfetch', 'web_fetch', 'web_request', 'search_web', 'internet_search'];
|
|
4
|
+
const READ_TOOL_EXACT = new Set([
|
|
5
|
+
'read_file',
|
|
6
|
+
'read_text',
|
|
7
|
+
'view_file',
|
|
8
|
+
'view_code',
|
|
9
|
+
'view_document',
|
|
10
|
+
'open_file',
|
|
11
|
+
'get_file',
|
|
12
|
+
'download_file',
|
|
13
|
+
'describe_current_request',
|
|
14
|
+
'list_dir',
|
|
15
|
+
'list_directory',
|
|
16
|
+
'list_files',
|
|
17
|
+
'list_documents',
|
|
18
|
+
'list_resources',
|
|
19
|
+
'search_files',
|
|
20
|
+
'find_files'
|
|
21
|
+
]);
|
|
22
|
+
const WRITE_TOOL_EXACT = new Set([
|
|
23
|
+
'apply_patch',
|
|
24
|
+
'write_file',
|
|
25
|
+
'create_file',
|
|
26
|
+
'modify_file',
|
|
27
|
+
'edit_file',
|
|
28
|
+
'update_file',
|
|
29
|
+
'save_file',
|
|
30
|
+
'append_file',
|
|
31
|
+
'replace_file',
|
|
32
|
+
'delete_file',
|
|
33
|
+
'remove_file',
|
|
34
|
+
'rename_file',
|
|
35
|
+
'move_file',
|
|
36
|
+
'copy_file',
|
|
37
|
+
'mkdir',
|
|
38
|
+
'rmdir'
|
|
39
|
+
]);
|
|
40
|
+
const SEARCH_TOOL_EXACT = new Set(['websearch', 'web_search', 'search_web', 'internet_search', 'webfetch', 'web_fetch']);
|
|
41
|
+
const READ_TOOL_KEYWORDS = ['read', 'list', 'view', 'download', 'open', 'show', 'fetch', 'inspect'];
|
|
42
|
+
const WRITE_TOOL_KEYWORDS = ['write', 'patch', 'modify', 'edit', 'create', 'update', 'append', 'replace', 'delete', 'remove'];
|
|
43
|
+
const SEARCH_TOOL_KEYWORDS = ['search', 'websearch', 'web_fetch', 'webfetch', 'web-request', 'web_request', 'internet'];
|
|
44
|
+
const SHELL_TOOL_NAMES = new Set(['shell_command', 'shell', 'bash']);
|
|
45
|
+
const SHELL_HEREDOC_PATTERN = /<<\s*['"]?[a-z0-9_-]+/i;
|
|
46
|
+
const SHELL_WRITE_PATTERNS = [
|
|
47
|
+
'apply_patch',
|
|
48
|
+
'sed -i',
|
|
49
|
+
'perl -pi',
|
|
50
|
+
'tee ',
|
|
51
|
+
'cat <<',
|
|
52
|
+
'cat >',
|
|
53
|
+
'printf >',
|
|
54
|
+
'touch ',
|
|
55
|
+
'truncate',
|
|
56
|
+
'mkdir',
|
|
57
|
+
'mktemp',
|
|
58
|
+
'rmdir',
|
|
59
|
+
'rm ',
|
|
60
|
+
'rm-',
|
|
61
|
+
'unlink',
|
|
62
|
+
'mv ',
|
|
63
|
+
'cp ',
|
|
64
|
+
'ln -',
|
|
65
|
+
'chmod',
|
|
66
|
+
'chown',
|
|
67
|
+
'chgrp',
|
|
68
|
+
'tar ',
|
|
69
|
+
'git add',
|
|
70
|
+
'git commit',
|
|
71
|
+
'git apply',
|
|
72
|
+
'git am',
|
|
73
|
+
'git rebase',
|
|
74
|
+
'git checkout',
|
|
75
|
+
'git merge',
|
|
76
|
+
'patch <<',
|
|
77
|
+
'npm install',
|
|
78
|
+
'pnpm install',
|
|
79
|
+
'yarn add',
|
|
80
|
+
'yarn install',
|
|
81
|
+
'pip install',
|
|
82
|
+
'pip3 install',
|
|
83
|
+
'brew install',
|
|
84
|
+
'cargo add',
|
|
85
|
+
'cargo install',
|
|
86
|
+
'go install',
|
|
87
|
+
'make install'
|
|
88
|
+
];
|
|
89
|
+
const SHELL_SEARCH_PATTERNS = [
|
|
90
|
+
'rg ',
|
|
91
|
+
'rg-',
|
|
92
|
+
'grep ',
|
|
93
|
+
'grep-',
|
|
94
|
+
'ripgrep',
|
|
95
|
+
'find ',
|
|
96
|
+
'fd ',
|
|
97
|
+
'locate ',
|
|
98
|
+
'search ',
|
|
99
|
+
'ack ',
|
|
100
|
+
'ag ',
|
|
101
|
+
'where ',
|
|
102
|
+
'which ',
|
|
103
|
+
'codesearch'
|
|
104
|
+
];
|
|
105
|
+
const SHELL_READ_PATTERNS = [
|
|
106
|
+
'ls',
|
|
107
|
+
'dir ',
|
|
108
|
+
'pwd',
|
|
109
|
+
'cat ',
|
|
110
|
+
'type ',
|
|
111
|
+
'head ',
|
|
112
|
+
'tail ',
|
|
113
|
+
'stat',
|
|
114
|
+
'tree',
|
|
115
|
+
'wc ',
|
|
116
|
+
'du ',
|
|
117
|
+
'printf "',
|
|
118
|
+
'python - <<',
|
|
119
|
+
'python -c',
|
|
120
|
+
'node - <<',
|
|
121
|
+
'node -e'
|
|
122
|
+
];
|
|
4
123
|
export function buildRoutingFeatures(request, metadata) {
|
|
5
124
|
const latestUserMessage = getLatestUserMessage(request.messages);
|
|
6
125
|
const assistantMessages = request.messages.filter((msg) => msg.role === 'assistant');
|
|
@@ -8,18 +127,18 @@ export function buildRoutingFeatures(request, metadata) {
|
|
|
8
127
|
const normalizedUserText = latestUserText.toLowerCase();
|
|
9
128
|
const hasTools = Array.isArray(request.tools) && request.tools.length > 0;
|
|
10
129
|
const hasToolCallResponses = assistantMessages.some((msg) => Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0);
|
|
11
|
-
const estimatedTokens =
|
|
130
|
+
const estimatedTokens = computeRequestTokens(request, latestUserText);
|
|
12
131
|
const hasThinking = detectKeyword(normalizedUserText, THINKING_KEYWORDS);
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const hasImageAttachment = previousToolCategory === 'vision' || (hasVisionTool && detectImageAttachment(latestUserMessage));
|
|
132
|
+
const hasVisionTool = detectVisionTool(request);
|
|
133
|
+
const hasImageAttachment = hasVisionTool && detectImageAttachment(latestUserMessage);
|
|
16
134
|
const hasCodingTool = detectCodingTool(request);
|
|
17
135
|
const hasWebTool = detectWebTool(request);
|
|
18
136
|
const hasThinkingKeyword = hasThinking || detectExtendedThinkingKeyword(normalizedUserText);
|
|
137
|
+
const lastAssistantTool = detectLastAssistantToolCategory(assistantMessages);
|
|
19
138
|
return {
|
|
20
139
|
requestId: metadata.requestId,
|
|
21
140
|
model: request.model,
|
|
22
|
-
totalMessages:
|
|
141
|
+
totalMessages: request.messages?.length ?? 0,
|
|
23
142
|
userTextSample: latestUserText.slice(0, 2000),
|
|
24
143
|
toolCount: request.tools?.length ?? 0,
|
|
25
144
|
hasTools,
|
|
@@ -30,7 +149,8 @@ export function buildRoutingFeatures(request, metadata) {
|
|
|
30
149
|
hasCodingTool,
|
|
31
150
|
hasThinkingKeyword,
|
|
32
151
|
estimatedTokens,
|
|
33
|
-
|
|
152
|
+
lastAssistantToolCategory: lastAssistantTool?.category,
|
|
153
|
+
lastAssistantToolName: lastAssistantTool?.name,
|
|
34
154
|
metadata: {
|
|
35
155
|
...metadata
|
|
36
156
|
}
|
|
@@ -100,12 +220,15 @@ function detectCodingTool(request) {
|
|
|
100
220
|
return false;
|
|
101
221
|
}
|
|
102
222
|
return request.tools.some((tool) => {
|
|
103
|
-
const functionName = extractToolName(tool);
|
|
104
|
-
const description = extractToolDescription(tool);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
223
|
+
const functionName = extractToolName(tool).toLowerCase();
|
|
224
|
+
const description = (extractToolDescription(tool) || '').toLowerCase();
|
|
225
|
+
if (!functionName && !description) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
if (WRITE_TOOL_EXACT.has(functionName)) {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
return WRITE_TOOL_KEYWORDS.some((keyword) => functionName.includes(keyword.toLowerCase()) || description.includes(keyword.toLowerCase()));
|
|
109
232
|
});
|
|
110
233
|
}
|
|
111
234
|
function detectWebTool(request) {
|
|
@@ -128,111 +251,20 @@ function detectExtendedThinkingKeyword(text) {
|
|
|
128
251
|
const keywords = ['仔细分析', '思考', '超级思考', '深度思考', 'careful analysis', 'deep thinking', 'deliberate'];
|
|
129
252
|
return keywords.some((keyword) => text.includes(keyword));
|
|
130
253
|
}
|
|
131
|
-
function
|
|
132
|
-
if (!Array.isArray(messages) || !messages.length) {
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
let lastUserIndex = messages.length;
|
|
136
|
-
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
137
|
-
if (messages[idx]?.role === 'user') {
|
|
138
|
-
lastUserIndex = idx;
|
|
139
|
-
break;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
for (let idx = lastUserIndex - 1; idx >= 0; idx -= 1) {
|
|
143
|
-
const candidate = messages[idx];
|
|
144
|
-
if (!candidate || candidate.role !== 'assistant') {
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
const messageRecord = candidate;
|
|
148
|
-
const rawCalls = Array.isArray(messageRecord.tool_calls)
|
|
149
|
-
? messageRecord.tool_calls
|
|
150
|
-
: [];
|
|
151
|
-
if (!rawCalls.length) {
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
for (const call of rawCalls) {
|
|
155
|
-
const categorized = categorizeToolCall(call);
|
|
156
|
-
if (categorized) {
|
|
157
|
-
return categorized;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return null;
|
|
162
|
-
}
|
|
163
|
-
function categorizeToolCall(call) {
|
|
164
|
-
if (!call) {
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
167
|
-
const fn = (call.function ?? call);
|
|
168
|
-
const name = typeof (fn?.name ?? call.name) === 'string' ? String(fn?.name ?? call.name) : '';
|
|
169
|
-
if (!name) {
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
const args = parseFunctionArguments(typeof fn?.arguments === 'string' ? fn.arguments : undefined);
|
|
173
|
-
if (name === 'update_plan') {
|
|
174
|
-
return 'plan';
|
|
175
|
-
}
|
|
176
|
-
if (name === 'view_image') {
|
|
177
|
-
return hasImageLink(args) ? 'vision' : null;
|
|
178
|
-
}
|
|
179
|
-
if (name === 'apply_patch') {
|
|
180
|
-
return 'coding';
|
|
181
|
-
}
|
|
182
|
-
if (name === 'shell_command') {
|
|
183
|
-
const command = typeof args?.command === 'string' ? args.command : '';
|
|
184
|
-
return classifyShellCommand(command);
|
|
185
|
-
}
|
|
186
|
-
return null;
|
|
187
|
-
}
|
|
188
|
-
function parseFunctionArguments(raw) {
|
|
189
|
-
if (!raw) {
|
|
190
|
-
return null;
|
|
191
|
-
}
|
|
254
|
+
function computeRequestTokens(request, fallbackText) {
|
|
192
255
|
try {
|
|
193
|
-
return
|
|
256
|
+
return countRequestTokens(request);
|
|
194
257
|
}
|
|
195
258
|
catch {
|
|
196
|
-
return
|
|
259
|
+
return fallbackEstimateTokens(fallbackText, request.messages?.length ?? 0);
|
|
197
260
|
}
|
|
198
261
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
/\bcat\b/i,
|
|
203
|
-
/\btail\b/i,
|
|
204
|
-
/\bhead\b/i,
|
|
205
|
-
/\bls\b/i,
|
|
206
|
-
/\bwc\b/i,
|
|
207
|
-
/\bgrep\b/i,
|
|
208
|
-
/node\s+-\s*<<['"]/i,
|
|
209
|
-
/python\s+-\s*<<['"]/i
|
|
210
|
-
];
|
|
211
|
-
function classifyShellCommand(command) {
|
|
212
|
-
if (!command) {
|
|
213
|
-
return null;
|
|
214
|
-
}
|
|
215
|
-
const normalized = command.trim();
|
|
216
|
-
if (!normalized) {
|
|
217
|
-
return null;
|
|
218
|
-
}
|
|
219
|
-
if (CONTEXT_READ_PATTERNS.some((pattern) => pattern.test(normalized))) {
|
|
220
|
-
return 'context_read';
|
|
221
|
-
}
|
|
222
|
-
return null;
|
|
223
|
-
}
|
|
224
|
-
const IMAGE_LINK_KEYS = ['imagePath', 'path', 'filepath', 'file', 'url', 'image', 'src'];
|
|
225
|
-
function hasImageLink(args) {
|
|
226
|
-
if (!args || typeof args !== 'object') {
|
|
227
|
-
return false;
|
|
228
|
-
}
|
|
229
|
-
for (const key of IMAGE_LINK_KEYS) {
|
|
230
|
-
const value = args[key];
|
|
231
|
-
if (typeof value === 'string' && value.trim()) {
|
|
232
|
-
return true;
|
|
233
|
-
}
|
|
262
|
+
function fallbackEstimateTokens(text, messageCount) {
|
|
263
|
+
if (!text) {
|
|
264
|
+
return Math.max(32, Math.max(messageCount, 1) * 16);
|
|
234
265
|
}
|
|
235
|
-
|
|
266
|
+
const rough = Math.ceil(text.length / 4);
|
|
267
|
+
return Math.max(rough, Math.max(messageCount, 1) * 32);
|
|
236
268
|
}
|
|
237
269
|
function extractToolName(tool) {
|
|
238
270
|
if (!tool || typeof tool !== 'object') {
|
|
@@ -262,3 +294,198 @@ function extractToolDescription(tool) {
|
|
|
262
294
|
}
|
|
263
295
|
return '';
|
|
264
296
|
}
|
|
297
|
+
function detectLastAssistantToolCategory(messages) {
|
|
298
|
+
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
299
|
+
const msg = messages[idx];
|
|
300
|
+
if (!msg || !Array.isArray(msg.tool_calls) || msg.tool_calls.length === 0) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
let fallback;
|
|
304
|
+
for (const call of msg.tool_calls) {
|
|
305
|
+
const classification = classifyToolCall(call);
|
|
306
|
+
if (!classification) {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (!fallback) {
|
|
310
|
+
fallback = classification;
|
|
311
|
+
}
|
|
312
|
+
if (classification.category !== 'other') {
|
|
313
|
+
return classification;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (fallback) {
|
|
317
|
+
return fallback;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
function classifyToolCall(call) {
|
|
323
|
+
if (!call || typeof call !== 'object') {
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
326
|
+
const functionName = typeof call?.function?.name === 'string' && call.function.name.trim()
|
|
327
|
+
? canonicalizeToolName(call.function.name)
|
|
328
|
+
: '';
|
|
329
|
+
if (!functionName) {
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
const argsObject = parseToolArguments(call?.function?.arguments);
|
|
333
|
+
const commandText = extractCommandText(argsObject);
|
|
334
|
+
const nameCategory = categorizeToolName(functionName);
|
|
335
|
+
if (nameCategory === 'write' || nameCategory === 'read' || nameCategory === 'search') {
|
|
336
|
+
return { category: nameCategory, name: functionName };
|
|
337
|
+
}
|
|
338
|
+
if (SHELL_TOOL_NAMES.has(functionName)) {
|
|
339
|
+
const shellCategory = classifyShellCommand(commandText);
|
|
340
|
+
return { category: shellCategory, name: functionName };
|
|
341
|
+
}
|
|
342
|
+
if (commandText) {
|
|
343
|
+
const derivedCategory = classifyShellCommand(commandText);
|
|
344
|
+
if (derivedCategory !== 'other') {
|
|
345
|
+
return { category: derivedCategory, name: functionName };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return { category: 'other', name: functionName };
|
|
349
|
+
}
|
|
350
|
+
function canonicalizeToolName(rawName) {
|
|
351
|
+
const trimmed = rawName.trim();
|
|
352
|
+
const markerIndex = trimmed.indexOf('arg_');
|
|
353
|
+
if (markerIndex > 0) {
|
|
354
|
+
return trimmed.slice(0, markerIndex);
|
|
355
|
+
}
|
|
356
|
+
return trimmed;
|
|
357
|
+
}
|
|
358
|
+
function parseToolArguments(rawArguments) {
|
|
359
|
+
if (!rawArguments) {
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
if (typeof rawArguments === 'string') {
|
|
363
|
+
try {
|
|
364
|
+
return JSON.parse(rawArguments);
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
return rawArguments;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (typeof rawArguments === 'object') {
|
|
371
|
+
return rawArguments;
|
|
372
|
+
}
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
function extractCommandText(args) {
|
|
376
|
+
if (!args) {
|
|
377
|
+
return '';
|
|
378
|
+
}
|
|
379
|
+
if (typeof args === 'string') {
|
|
380
|
+
return args;
|
|
381
|
+
}
|
|
382
|
+
if (Array.isArray(args)) {
|
|
383
|
+
return args.map((item) => (typeof item === 'string' ? item : '')).filter(Boolean).join(' ');
|
|
384
|
+
}
|
|
385
|
+
if (typeof args === 'object') {
|
|
386
|
+
const record = args;
|
|
387
|
+
const command = record.command;
|
|
388
|
+
const input = record.input;
|
|
389
|
+
const nestedArgs = record.args;
|
|
390
|
+
if (typeof command === 'string') {
|
|
391
|
+
return command;
|
|
392
|
+
}
|
|
393
|
+
if (Array.isArray(command)) {
|
|
394
|
+
return command.map((item) => (typeof item === 'string' ? item : '')).filter(Boolean).join(' ');
|
|
395
|
+
}
|
|
396
|
+
if (typeof input === 'string') {
|
|
397
|
+
return input;
|
|
398
|
+
}
|
|
399
|
+
if (typeof nestedArgs === 'string') {
|
|
400
|
+
return nestedArgs;
|
|
401
|
+
}
|
|
402
|
+
if (Array.isArray(nestedArgs)) {
|
|
403
|
+
return nestedArgs.map((item) => (typeof item === 'string' ? item : '')).filter(Boolean).join(' ');
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return '';
|
|
407
|
+
}
|
|
408
|
+
function categorizeToolName(name) {
|
|
409
|
+
const normalized = name.toLowerCase();
|
|
410
|
+
if (SEARCH_TOOL_EXACT.has(normalized) ||
|
|
411
|
+
SEARCH_TOOL_KEYWORDS.some((keyword) => normalized.includes(keyword.toLowerCase()))) {
|
|
412
|
+
return 'search';
|
|
413
|
+
}
|
|
414
|
+
if (READ_TOOL_EXACT.has(normalized) ||
|
|
415
|
+
READ_TOOL_KEYWORDS.some((keyword) => normalized.includes(keyword.toLowerCase()))) {
|
|
416
|
+
return 'read';
|
|
417
|
+
}
|
|
418
|
+
if (WRITE_TOOL_EXACT.has(normalized) ||
|
|
419
|
+
WRITE_TOOL_KEYWORDS.some((keyword) => normalized.includes(keyword.toLowerCase()))) {
|
|
420
|
+
return 'write';
|
|
421
|
+
}
|
|
422
|
+
return 'other';
|
|
423
|
+
}
|
|
424
|
+
function classifyShellCommand(command) {
|
|
425
|
+
if (!command) {
|
|
426
|
+
return 'other';
|
|
427
|
+
}
|
|
428
|
+
if (SHELL_HEREDOC_PATTERN.test(command)) {
|
|
429
|
+
return 'write';
|
|
430
|
+
}
|
|
431
|
+
const segments = splitCommandSegments(command).map(stripShellWrapper);
|
|
432
|
+
if (segments.some((segment) => matchesAnyPattern(segment, SHELL_WRITE_PATTERNS))) {
|
|
433
|
+
return 'write';
|
|
434
|
+
}
|
|
435
|
+
if (segments.some((segment) => matchesAnyPattern(segment, SHELL_SEARCH_PATTERNS))) {
|
|
436
|
+
return 'search';
|
|
437
|
+
}
|
|
438
|
+
if (segments.some((segment) => matchesAnyPattern(segment, SHELL_READ_PATTERNS))) {
|
|
439
|
+
return 'read';
|
|
440
|
+
}
|
|
441
|
+
const stripped = stripShellWrapper(command);
|
|
442
|
+
if (matchesAnyPattern(stripped, SHELL_WRITE_PATTERNS)) {
|
|
443
|
+
return 'write';
|
|
444
|
+
}
|
|
445
|
+
if (matchesAnyPattern(stripped, SHELL_SEARCH_PATTERNS)) {
|
|
446
|
+
return 'search';
|
|
447
|
+
}
|
|
448
|
+
if (matchesAnyPattern(stripped, SHELL_READ_PATTERNS)) {
|
|
449
|
+
return 'read';
|
|
450
|
+
}
|
|
451
|
+
return 'other';
|
|
452
|
+
}
|
|
453
|
+
function splitCommandSegments(command) {
|
|
454
|
+
return command
|
|
455
|
+
.split(/(?:\r?\n|&&|\|\||;)/)
|
|
456
|
+
.map((segment) => segment.trim())
|
|
457
|
+
.filter(Boolean);
|
|
458
|
+
}
|
|
459
|
+
function matchesAnyPattern(text, patterns) {
|
|
460
|
+
if (!text) {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
const trimmed = text.trim().toLowerCase();
|
|
464
|
+
const normalized = trimmed.startsWith('sudo ') ? trimmed.slice(5).trim() : trimmed;
|
|
465
|
+
return patterns.some((pattern) => {
|
|
466
|
+
const lowered = pattern.toLowerCase().trim();
|
|
467
|
+
return normalized.startsWith(lowered);
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
function stripShellWrapper(segment) {
|
|
471
|
+
let normalized = segment.trim();
|
|
472
|
+
const wrappers = ['bash -lc ', 'bash -lc', 'sh -c ', 'sh -c', '/bin/sh -c ', '/bin/sh -c', 'env -i bash -lc ', 'env -i bash -lc'];
|
|
473
|
+
for (const wrapper of wrappers) {
|
|
474
|
+
if (normalized.toLowerCase().startsWith(wrapper)) {
|
|
475
|
+
normalized = normalized.slice(wrapper.length).trim();
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
normalized = trimEnclosingQuotes(normalized);
|
|
480
|
+
if (normalized.startsWith('sudo ')) {
|
|
481
|
+
normalized = normalized.slice(5).trim();
|
|
482
|
+
}
|
|
483
|
+
return normalized;
|
|
484
|
+
}
|
|
485
|
+
function trimEnclosingQuotes(value) {
|
|
486
|
+
if ((value.startsWith('"') && value.endsWith('"') && value.length > 1) ||
|
|
487
|
+
(value.startsWith("'") && value.endsWith("'") && value.length > 1)) {
|
|
488
|
+
return value.slice(1, -1).trim();
|
|
489
|
+
}
|
|
490
|
+
return value;
|
|
491
|
+
}
|