@jsonstudio/llms 0.6.2 → 0.6.34
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 +2 -1
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +13 -0
- package/dist/conversion/hub/response/response-mappers.d.ts +1 -1
- package/dist/conversion/hub/response/response-mappers.js +12 -2
- package/dist/conversion/shared/responses-conversation-store.d.ts +35 -0
- package/dist/conversion/shared/responses-conversation-store.js +64 -19
- package/dist/conversion/shared/responses-output-builder.js +43 -22
- package/dist/conversion/shared/responses-response-utils.js +47 -1
- package/dist/conversion/shared/text-markup-normalizer.js +2 -2
- package/dist/conversion/shared/tool-canonicalizer.js +118 -16
- package/dist/conversion/shared/tool-mapping.js +30 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/router/virtual-router/bootstrap.js +31 -7
- package/dist/router/virtual-router/classifier.js +56 -27
- 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 +110 -8
- 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 +4 -0
- package/dist/router/virtual-router/types.js +2 -2
- package/dist/sse/sse-to-json/builders/response-builder.d.ts +2 -0
- package/dist/sse/sse-to-json/builders/response-builder.js +52 -11
- package/package.json +3 -3
|
@@ -262,6 +262,7 @@ export function buildOpenAIChatFromGeminiResponse(payload) {
|
|
|
262
262
|
catch {
|
|
263
263
|
// best-effort policy execution
|
|
264
264
|
}
|
|
265
|
+
const normalizedFinishReason = toolCalls.length ? 'tool_calls' : finish_reason;
|
|
265
266
|
const chatResp = {
|
|
266
267
|
id: payload?.id || `chatcmpl_${Date.now()}`,
|
|
267
268
|
object: 'chat.completion',
|
|
@@ -269,7 +270,7 @@ export function buildOpenAIChatFromGeminiResponse(payload) {
|
|
|
269
270
|
choices: [
|
|
270
271
|
{
|
|
271
272
|
index: 0,
|
|
272
|
-
finish_reason,
|
|
273
|
+
finish_reason: normalizedFinishReason,
|
|
273
274
|
message: chatMsg
|
|
274
275
|
}
|
|
275
276
|
]
|
package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { captureResponsesContext, buildChatRequestFromResponses } from '../../../../../responses/responses-openai-bridge.js';
|
|
2
|
+
import { captureResponsesRequestContext } from '../../../../../shared/responses-conversation-store.js';
|
|
2
3
|
import { recordStage } from '../../../stages/utils.js';
|
|
3
4
|
export async function runReqInboundStage3ContextCapture(options) {
|
|
4
5
|
let context;
|
|
@@ -18,6 +19,18 @@ export async function runReqInboundStage3ContextCapture(options) {
|
|
|
18
19
|
? augmentContextSnapshot(context, fallbackSnapshot)
|
|
19
20
|
: fallbackSnapshot;
|
|
20
21
|
recordStage(options.stageRecorder, 'req_inbound_stage3_context_capture', snapshot);
|
|
22
|
+
if (options.adapterContext.providerProtocol === 'openai-responses') {
|
|
23
|
+
try {
|
|
24
|
+
captureResponsesRequestContext({
|
|
25
|
+
requestId: options.adapterContext.requestId,
|
|
26
|
+
payload: options.rawRequest,
|
|
27
|
+
context: snapshot
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
/* ignore capture failures */
|
|
32
|
+
}
|
|
33
|
+
}
|
|
21
34
|
return context ?? snapshot;
|
|
22
35
|
}
|
|
23
36
|
export function runChatContextCapture(options) {
|
|
@@ -9,7 +9,7 @@ export declare class OpenAIChatResponseMapper implements ResponseMapper {
|
|
|
9
9
|
toChatCompletion(format: FormatEnvelope, _ctx: AdapterContext): ChatCompletionLike;
|
|
10
10
|
}
|
|
11
11
|
export declare class ResponsesResponseMapper implements ResponseMapper {
|
|
12
|
-
toChatCompletion(format: FormatEnvelope,
|
|
12
|
+
toChatCompletion(format: FormatEnvelope, ctx: AdapterContext): ChatCompletionLike;
|
|
13
13
|
}
|
|
14
14
|
export declare class AnthropicResponseMapper implements ResponseMapper {
|
|
15
15
|
toChatCompletion(format: FormatEnvelope, ctx: AdapterContext): ChatCompletionLike;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { buildOpenAIChatFromGeminiResponse } from '../../codecs/gemini-openai-codec.js';
|
|
2
2
|
import { buildChatResponseFromResponses } from '../../shared/responses-response-utils.js';
|
|
3
|
+
import { recordResponsesResponse } from '../../shared/responses-conversation-store.js';
|
|
3
4
|
import { buildOpenAIChatFromAnthropicMessage } from './response-runtime.js';
|
|
4
5
|
export class OpenAIChatResponseMapper {
|
|
5
6
|
toChatCompletion(format, _ctx) {
|
|
@@ -7,8 +8,17 @@ export class OpenAIChatResponseMapper {
|
|
|
7
8
|
}
|
|
8
9
|
}
|
|
9
10
|
export class ResponsesResponseMapper {
|
|
10
|
-
toChatCompletion(format,
|
|
11
|
-
|
|
11
|
+
toChatCompletion(format, ctx) {
|
|
12
|
+
const payload = (format.payload ?? {});
|
|
13
|
+
if (ctx?.requestId) {
|
|
14
|
+
try {
|
|
15
|
+
recordResponsesResponse({ requestId: ctx.requestId, response: payload });
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
/* ignore capture failures */
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return buildChatResponseFromResponses(payload);
|
|
12
22
|
}
|
|
13
23
|
}
|
|
14
24
|
export class AnthropicResponseMapper {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
type AnyRecord = Record<string, unknown>;
|
|
2
|
+
interface CaptureContextArgs {
|
|
3
|
+
requestId?: string;
|
|
4
|
+
payload: AnyRecord;
|
|
5
|
+
context: AnyRecord;
|
|
6
|
+
}
|
|
7
|
+
interface RecordResponseArgs {
|
|
8
|
+
requestId?: string;
|
|
9
|
+
response: AnyRecord;
|
|
10
|
+
}
|
|
11
|
+
interface ResumeOptions {
|
|
12
|
+
requestId?: string;
|
|
13
|
+
}
|
|
14
|
+
interface ResumeResult {
|
|
15
|
+
payload: AnyRecord;
|
|
16
|
+
meta: AnyRecord;
|
|
17
|
+
}
|
|
18
|
+
declare class ResponsesConversationStore {
|
|
19
|
+
private requestMap;
|
|
20
|
+
private responseIndex;
|
|
21
|
+
rebindRequestId(oldId: string | undefined, newId: string | undefined): void;
|
|
22
|
+
captureRequestContext(args: CaptureContextArgs): void;
|
|
23
|
+
recordResponse(args: RecordResponseArgs): void;
|
|
24
|
+
resumeConversation(responseId: string, submitPayload: AnyRecord, options?: ResumeOptions): ResumeResult;
|
|
25
|
+
clearRequest(requestId?: string): void;
|
|
26
|
+
private cleanupEntry;
|
|
27
|
+
private prune;
|
|
28
|
+
}
|
|
29
|
+
declare const store: ResponsesConversationStore;
|
|
30
|
+
export declare function captureResponsesRequestContext(args: CaptureContextArgs): void;
|
|
31
|
+
export declare function recordResponsesResponse(args: RecordResponseArgs): void;
|
|
32
|
+
export declare function resumeResponsesConversation(responseId: string, submitPayload: AnyRecord, options?: ResumeOptions): ResumeResult;
|
|
33
|
+
export declare function clearResponsesConversationByRequestId(requestId?: string): void;
|
|
34
|
+
export declare function rebindResponsesConversationRequestId(oldId?: string, newId?: string): void;
|
|
35
|
+
export { store as responsesConversationStore };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const TTL_MS = 1000 * 60 * 30; //
|
|
1
|
+
const TTL_MS = 1000 * 60 * 30; // 30min
|
|
2
2
|
function cloneDeep(value) {
|
|
3
3
|
try {
|
|
4
4
|
if (typeof globalThis.structuredClone === 'function') {
|
|
@@ -49,19 +49,26 @@ function pickPersistedFields(payload) {
|
|
|
49
49
|
'input_audio',
|
|
50
50
|
'output_audio'
|
|
51
51
|
];
|
|
52
|
-
const
|
|
52
|
+
const next = {};
|
|
53
53
|
for (const key of fields) {
|
|
54
54
|
if (payload[key] !== undefined) {
|
|
55
|
-
|
|
55
|
+
next[key] = cloneDeep(payload[key]);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
-
return
|
|
58
|
+
return next;
|
|
59
59
|
}
|
|
60
60
|
function normalizeOutputItemToInput(item) {
|
|
61
|
+
if (!item || typeof item !== 'object') {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
61
64
|
const type = typeof item.type === 'string' ? item.type : '';
|
|
62
65
|
if (type === 'message' || type === 'reasoning') {
|
|
63
66
|
const role = typeof item.role === 'string' ? item.role : 'assistant';
|
|
64
|
-
const content = Array.isArray(item.content)
|
|
67
|
+
const content = Array.isArray(item.content)
|
|
68
|
+
? cloneDeep(item.content)
|
|
69
|
+
: typeof item.text === 'string'
|
|
70
|
+
? [{ type: 'text', text: item.text }]
|
|
71
|
+
: [];
|
|
65
72
|
return {
|
|
66
73
|
type: 'message',
|
|
67
74
|
role,
|
|
@@ -70,7 +77,11 @@ function normalizeOutputItemToInput(item) {
|
|
|
70
77
|
};
|
|
71
78
|
}
|
|
72
79
|
if (type === 'function_call') {
|
|
73
|
-
const callId = typeof item.call_id === 'string'
|
|
80
|
+
const callId = typeof item.call_id === 'string'
|
|
81
|
+
? item.call_id
|
|
82
|
+
: typeof item.id === 'string'
|
|
83
|
+
? item.id
|
|
84
|
+
: undefined;
|
|
74
85
|
return {
|
|
75
86
|
type: 'function_call',
|
|
76
87
|
role: 'assistant',
|
|
@@ -80,9 +91,9 @@ function normalizeOutputItemToInput(item) {
|
|
|
80
91
|
arguments: item.arguments,
|
|
81
92
|
function: isRecord(item.function)
|
|
82
93
|
? cloneDeep(item.function)
|
|
83
|
-
:
|
|
94
|
+
: typeof item.name === 'string'
|
|
84
95
|
? { name: item.name, arguments: item.arguments }
|
|
85
|
-
: undefined
|
|
96
|
+
: undefined
|
|
86
97
|
};
|
|
87
98
|
}
|
|
88
99
|
return null;
|
|
@@ -139,6 +150,18 @@ function normalizeSubmittedToolOutputs(toolOutputs) {
|
|
|
139
150
|
class ResponsesConversationStore {
|
|
140
151
|
requestMap = new Map();
|
|
141
152
|
responseIndex = new Map();
|
|
153
|
+
rebindRequestId(oldId, newId) {
|
|
154
|
+
if (!oldId || !newId || oldId === newId) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const entry = this.requestMap.get(oldId);
|
|
158
|
+
if (!entry) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
this.requestMap.delete(oldId);
|
|
162
|
+
entry.requestId = newId;
|
|
163
|
+
this.requestMap.set(newId, entry);
|
|
164
|
+
}
|
|
142
165
|
captureRequestContext(args) {
|
|
143
166
|
const { requestId, payload, context } = args;
|
|
144
167
|
if (!requestId || !payload)
|
|
@@ -148,7 +171,8 @@ class ResponsesConversationStore {
|
|
|
148
171
|
requestId,
|
|
149
172
|
basePayload: pickPersistedFields(payload),
|
|
150
173
|
input: coerceInputArray(context.input),
|
|
151
|
-
tools: coerceTools(context.toolsRaw ||
|
|
174
|
+
tools: coerceTools(context.toolsRaw) ||
|
|
175
|
+
coerceTools(Array.isArray(payload.tools) ? payload.tools : undefined),
|
|
152
176
|
createdAt: Date.now(),
|
|
153
177
|
updatedAt: Date.now()
|
|
154
178
|
};
|
|
@@ -164,7 +188,7 @@ class ResponsesConversationStore {
|
|
|
164
188
|
this.requestMap.set(requestId, entry);
|
|
165
189
|
}
|
|
166
190
|
recordResponse(args) {
|
|
167
|
-
const entry = this.requestMap.get(args.requestId);
|
|
191
|
+
const entry = args.requestId ? this.requestMap.get(args.requestId) : undefined;
|
|
168
192
|
if (!entry)
|
|
169
193
|
return;
|
|
170
194
|
const response = args.response;
|
|
@@ -188,9 +212,7 @@ class ResponsesConversationStore {
|
|
|
188
212
|
if (!entry) {
|
|
189
213
|
throw new Error('Responses conversation expired or not found');
|
|
190
214
|
}
|
|
191
|
-
const toolOutputs = Array.isArray(submitPayload.tool_outputs)
|
|
192
|
-
? submitPayload.tool_outputs
|
|
193
|
-
: [];
|
|
215
|
+
const toolOutputs = Array.isArray(submitPayload.tool_outputs) ? submitPayload.tool_outputs : [];
|
|
194
216
|
if (!toolOutputs.length) {
|
|
195
217
|
throw new Error('tool_outputs array is required when submitting Responses tool results');
|
|
196
218
|
}
|
|
@@ -208,7 +230,8 @@ class ResponsesConversationStore {
|
|
|
208
230
|
payload.model = submitPayload.model.trim();
|
|
209
231
|
}
|
|
210
232
|
if (submitPayload.metadata && isRecord(submitPayload.metadata)) {
|
|
211
|
-
|
|
233
|
+
const baseMeta = isRecord(payload.metadata) ? payload.metadata : {};
|
|
234
|
+
payload.metadata = { ...baseMeta, ...cloneDeep(submitPayload.metadata) };
|
|
212
235
|
}
|
|
213
236
|
delete payload.tool_outputs;
|
|
214
237
|
delete payload.response_id;
|
|
@@ -225,6 +248,8 @@ class ResponsesConversationStore {
|
|
|
225
248
|
};
|
|
226
249
|
}
|
|
227
250
|
clearRequest(requestId) {
|
|
251
|
+
if (!requestId)
|
|
252
|
+
return;
|
|
228
253
|
const entry = this.requestMap.get(requestId);
|
|
229
254
|
if (!entry)
|
|
230
255
|
return;
|
|
@@ -254,10 +279,14 @@ class ResponsesConversationStore {
|
|
|
254
279
|
}
|
|
255
280
|
}
|
|
256
281
|
}
|
|
257
|
-
|
|
282
|
+
const store = new ResponsesConversationStore();
|
|
283
|
+
const RESPONSES_DEBUG = (process.env.ROUTECODEX_RESPONSES_DEBUG || '').trim() === '1';
|
|
258
284
|
export function captureResponsesRequestContext(args) {
|
|
259
285
|
try {
|
|
260
|
-
|
|
286
|
+
if (RESPONSES_DEBUG) {
|
|
287
|
+
console.log('[responses-store] capture', args.requestId);
|
|
288
|
+
}
|
|
289
|
+
store.captureRequestContext(args);
|
|
261
290
|
}
|
|
262
291
|
catch {
|
|
263
292
|
/* ignore capture failures */
|
|
@@ -265,15 +294,31 @@ export function captureResponsesRequestContext(args) {
|
|
|
265
294
|
}
|
|
266
295
|
export function recordResponsesResponse(args) {
|
|
267
296
|
try {
|
|
268
|
-
|
|
297
|
+
if (RESPONSES_DEBUG) {
|
|
298
|
+
console.log('[responses-store] record', args.requestId, args.response?.id);
|
|
299
|
+
}
|
|
300
|
+
store.recordResponse(args);
|
|
269
301
|
}
|
|
270
302
|
catch {
|
|
271
303
|
/* ignore */
|
|
272
304
|
}
|
|
273
305
|
}
|
|
274
306
|
export function resumeResponsesConversation(responseId, submitPayload, options) {
|
|
275
|
-
|
|
307
|
+
if (RESPONSES_DEBUG) {
|
|
308
|
+
console.log('[responses-store] resume', responseId);
|
|
309
|
+
}
|
|
310
|
+
return store.resumeConversation(responseId, submitPayload, options);
|
|
276
311
|
}
|
|
277
312
|
export function clearResponsesConversationByRequestId(requestId) {
|
|
278
|
-
|
|
313
|
+
if (RESPONSES_DEBUG && requestId) {
|
|
314
|
+
console.log('[responses-store] clear', requestId);
|
|
315
|
+
}
|
|
316
|
+
store.clearRequest(requestId);
|
|
317
|
+
}
|
|
318
|
+
export function rebindResponsesConversationRequestId(oldId, newId) {
|
|
319
|
+
if (RESPONSES_DEBUG && oldId && newId) {
|
|
320
|
+
console.log('[responses-store] rebind', oldId, '->', newId);
|
|
321
|
+
}
|
|
322
|
+
store.rebindRequestId(oldId, newId);
|
|
279
323
|
}
|
|
324
|
+
export { store as responsesConversationStore };
|
|
@@ -112,29 +112,50 @@ export function buildResponsesOutputFromChat(options) {
|
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
114
|
function normalizeUsage(usageRaw) {
|
|
115
|
-
if (usageRaw
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
115
|
+
if (!usageRaw || typeof usageRaw !== 'object') {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
const source = usageRaw;
|
|
119
|
+
const usage = {};
|
|
120
|
+
const inputValue = normalizeNumber(source.input_tokens ?? source.prompt_tokens);
|
|
121
|
+
if (inputValue !== undefined) {
|
|
122
|
+
usage.input_tokens = inputValue;
|
|
123
|
+
}
|
|
124
|
+
const outputValue = normalizeNumber(source.output_tokens ?? source.completion_tokens);
|
|
125
|
+
if (outputValue !== undefined) {
|
|
126
|
+
usage.output_tokens = outputValue;
|
|
127
|
+
}
|
|
128
|
+
const totalValue = normalizeNumber(source.total_tokens);
|
|
129
|
+
if (totalValue !== undefined) {
|
|
130
|
+
usage.total_tokens = totalValue;
|
|
131
|
+
}
|
|
132
|
+
else if (inputValue !== undefined && outputValue !== undefined) {
|
|
133
|
+
usage.total_tokens = inputValue + outputValue;
|
|
134
|
+
}
|
|
135
|
+
const inputDetails = extractTokenDetails(source.input_tokens_details ??
|
|
136
|
+
source.prompt_tokens_details);
|
|
137
|
+
if (inputDetails) {
|
|
138
|
+
usage.input_tokens_details = inputDetails;
|
|
139
|
+
}
|
|
140
|
+
const outputDetails = extractTokenDetails(source.output_tokens_details ??
|
|
141
|
+
source.completion_tokens_details);
|
|
142
|
+
if (outputDetails) {
|
|
143
|
+
usage.output_tokens_details = outputDetails;
|
|
144
|
+
}
|
|
145
|
+
return Object.keys(usage).length ? usage : undefined;
|
|
146
|
+
}
|
|
147
|
+
function normalizeNumber(value) {
|
|
148
|
+
if (value == null) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
const parsed = Number(value);
|
|
152
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
153
|
+
}
|
|
154
|
+
function extractTokenDetails(raw) {
|
|
155
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
156
|
+
return undefined;
|
|
136
157
|
}
|
|
137
|
-
return
|
|
158
|
+
return { ...raw };
|
|
138
159
|
}
|
|
139
160
|
function buildFunctionCallOutput(call, allocateOutputId, sanitizeFunctionName, baseCount, offset) {
|
|
140
161
|
try {
|
|
@@ -167,7 +167,7 @@ export function buildChatResponseFromResponses(payload) {
|
|
|
167
167
|
const created = typeof response.created_at === 'number'
|
|
168
168
|
? response.created_at
|
|
169
169
|
: (response.created ?? Math.floor(Date.now() / 1000));
|
|
170
|
-
const usage = response.usage;
|
|
170
|
+
const usage = mapResponsesUsageToChat(response.usage);
|
|
171
171
|
const toolCalls = collectToolCallsFromResponses(response);
|
|
172
172
|
const { textParts, reasoningParts } = extractOutputSegments(response);
|
|
173
173
|
const rawReasoningSegments = collectRawReasoningSegments(response);
|
|
@@ -237,3 +237,49 @@ export function buildChatResponseFromResponses(payload) {
|
|
|
237
237
|
}
|
|
238
238
|
return chat;
|
|
239
239
|
}
|
|
240
|
+
function mapResponsesUsageToChat(usageRaw) {
|
|
241
|
+
if (!usageRaw || typeof usageRaw !== 'object') {
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
const source = usageRaw;
|
|
245
|
+
const usage = {};
|
|
246
|
+
const promptValue = toNumber(source.prompt_tokens ?? source.input_tokens);
|
|
247
|
+
if (promptValue !== undefined) {
|
|
248
|
+
usage.prompt_tokens = promptValue;
|
|
249
|
+
}
|
|
250
|
+
const completionValue = toNumber(source.completion_tokens ?? source.output_tokens);
|
|
251
|
+
if (completionValue !== undefined) {
|
|
252
|
+
usage.completion_tokens = completionValue;
|
|
253
|
+
}
|
|
254
|
+
const totalValue = toNumber(source.total_tokens);
|
|
255
|
+
if (totalValue !== undefined) {
|
|
256
|
+
usage.total_tokens = totalValue;
|
|
257
|
+
}
|
|
258
|
+
else if (promptValue !== undefined && completionValue !== undefined) {
|
|
259
|
+
usage.total_tokens = promptValue + completionValue;
|
|
260
|
+
}
|
|
261
|
+
const promptDetails = extractDetailObject(source.prompt_tokens_details ??
|
|
262
|
+
source.input_tokens_details);
|
|
263
|
+
if (promptDetails) {
|
|
264
|
+
usage.prompt_tokens_details = promptDetails;
|
|
265
|
+
}
|
|
266
|
+
const completionDetails = extractDetailObject(source.completion_tokens_details ??
|
|
267
|
+
source.output_tokens_details);
|
|
268
|
+
if (completionDetails) {
|
|
269
|
+
usage.completion_tokens_details = completionDetails;
|
|
270
|
+
}
|
|
271
|
+
return Object.keys(usage).length ? usage : undefined;
|
|
272
|
+
}
|
|
273
|
+
function toNumber(value) {
|
|
274
|
+
if (value == null) {
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
const parsed = Number(value);
|
|
278
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
279
|
+
}
|
|
280
|
+
function extractDetailObject(raw) {
|
|
281
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
return { ...raw };
|
|
285
|
+
}
|
|
@@ -95,10 +95,10 @@ export function extractApplyPatchCallsFromText(text) {
|
|
|
95
95
|
continue;
|
|
96
96
|
let argsStr = '{}';
|
|
97
97
|
try {
|
|
98
|
-
argsStr = JSON.stringify({ patch });
|
|
98
|
+
argsStr = JSON.stringify({ input: patch });
|
|
99
99
|
}
|
|
100
100
|
catch {
|
|
101
|
-
argsStr = '{"
|
|
101
|
+
argsStr = '{"input":""}';
|
|
102
102
|
}
|
|
103
103
|
out.push({ id: genId(), name: 'apply_patch', args: argsStr });
|
|
104
104
|
}
|
|
@@ -12,31 +12,133 @@ 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
|
+
}
|
|
15
77
|
export function canonicalizeChatResponseTools(payload) {
|
|
16
78
|
try {
|
|
17
79
|
const out = isObject(payload) ? JSON.parse(JSON.stringify(payload)) : payload;
|
|
18
80
|
const choices = Array.isArray(out?.choices) ? out.choices : [];
|
|
19
81
|
for (const ch of choices) {
|
|
20
82
|
const msg = ch && ch.message ? ch.message : undefined;
|
|
21
|
-
const
|
|
22
|
-
|
|
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
|
+
}
|
|
23
126
|
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';
|
|
28
127
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
+
};
|
|
36
138
|
try {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
139
|
+
msg.tool_calls = [toolCall];
|
|
140
|
+
msg.content = null;
|
|
141
|
+
ch.finish_reason = 'tool_calls';
|
|
40
142
|
}
|
|
41
143
|
catch { /* ignore */ }
|
|
42
144
|
}
|
|
@@ -15,6 +15,28 @@ 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
|
+
}
|
|
18
40
|
function resolveToolName(candidate, options) {
|
|
19
41
|
const sanitized = options?.sanitizeName?.(candidate);
|
|
20
42
|
if (typeof sanitized === 'string' && sanitized.trim().length) {
|
|
@@ -80,6 +102,9 @@ export function bridgeToolToChatDefinition(rawTool, options) {
|
|
|
80
102
|
if (strict !== undefined) {
|
|
81
103
|
fnOut.strict = strict;
|
|
82
104
|
}
|
|
105
|
+
if (isApplyPatchTool(name)) {
|
|
106
|
+
applyPatchSchemaToFunction(fnOut);
|
|
107
|
+
}
|
|
83
108
|
return {
|
|
84
109
|
type: normalizedType,
|
|
85
110
|
function: fnOut
|
|
@@ -131,6 +156,11 @@ export function chatToolToBridgeDefinition(rawTool, options) {
|
|
|
131
156
|
if (strict !== undefined) {
|
|
132
157
|
fnOut.strict = strict;
|
|
133
158
|
}
|
|
159
|
+
if (isApplyPatchTool(name)) {
|
|
160
|
+
applyPatchSchemaToFunction(fnOut);
|
|
161
|
+
responseShape.parameters = buildApplyPatchParameters();
|
|
162
|
+
responseShape.strict = true;
|
|
163
|
+
}
|
|
134
164
|
responseShape.function = fnOut;
|
|
135
165
|
return responseShape;
|
|
136
166
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,4 +7,5 @@
|
|
|
7
7
|
export * from './conversion/index.js';
|
|
8
8
|
export * from './router/virtual-router/bootstrap.js';
|
|
9
9
|
export * from './router/virtual-router/types.js';
|
|
10
|
+
export { DEFAULT_THINKING_KEYWORDS } from './router/virtual-router/default-thinking-keywords.js';
|
|
10
11
|
export declare const VERSION = "0.4.0";
|
package/dist/index.js
CHANGED
|
@@ -7,4 +7,5 @@
|
|
|
7
7
|
export * from './conversion/index.js';
|
|
8
8
|
export * from './router/virtual-router/bootstrap.js';
|
|
9
9
|
export * from './router/virtual-router/types.js';
|
|
10
|
+
export { DEFAULT_THINKING_KEYWORDS } from './router/virtual-router/default-thinking-keywords.js';
|
|
10
11
|
export const VERSION = '0.4.0';
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { VirtualRouterError, VirtualRouterErrorCode } from './types.js';
|
|
2
|
+
import { DEFAULT_THINKING_KEYWORDS } from './default-thinking-keywords.js';
|
|
3
|
+
const KEYWORD_INJECTION_KEYS = ['thinking', 'background', 'vision', 'coding'];
|
|
2
4
|
const DEFAULT_CLASSIFIER = {
|
|
3
|
-
longContextThresholdTokens:
|
|
4
|
-
thinkingKeywords:
|
|
5
|
+
longContextThresholdTokens: 180000,
|
|
6
|
+
thinkingKeywords: DEFAULT_THINKING_KEYWORDS,
|
|
5
7
|
codingKeywords: ['apply_patch', 'write_file', 'create_file', 'shell', '修改文件', '写入文件'],
|
|
6
8
|
backgroundKeywords: ['background', 'context dump', '上下文'],
|
|
7
|
-
visionKeywords: ['vision', 'image', 'picture', 'photo']
|
|
9
|
+
visionKeywords: ['vision', 'image', 'picture', 'photo'],
|
|
10
|
+
keywordInjections: {}
|
|
8
11
|
};
|
|
9
12
|
const DEFAULT_LOAD_BALANCING = { strategy: 'round-robin' };
|
|
10
13
|
const DEFAULT_HEALTH = { failureThreshold: 3, cooldownMs: 30_000, fatalCooldownMs: 300_000 };
|
|
@@ -186,7 +189,8 @@ function normalizeClassifier(input) {
|
|
|
186
189
|
thinkingKeywords: normalizeStringArray(normalized.thinkingKeywords, DEFAULT_CLASSIFIER.thinkingKeywords),
|
|
187
190
|
codingKeywords: normalizeStringArray(normalized.codingKeywords, DEFAULT_CLASSIFIER.codingKeywords),
|
|
188
191
|
backgroundKeywords: normalizeStringArray(normalized.backgroundKeywords, DEFAULT_CLASSIFIER.backgroundKeywords),
|
|
189
|
-
visionKeywords: normalizeStringArray(normalized.visionKeywords, DEFAULT_CLASSIFIER.visionKeywords)
|
|
192
|
+
visionKeywords: normalizeStringArray(normalized.visionKeywords, DEFAULT_CLASSIFIER.visionKeywords),
|
|
193
|
+
keywordInjections: normalizeKeywordInjectionMap(normalized.keywordInjections)
|
|
190
194
|
};
|
|
191
195
|
return result;
|
|
192
196
|
}
|
|
@@ -197,6 +201,26 @@ function normalizeStringArray(value, fallback) {
|
|
|
197
201
|
const normalized = value.map((item) => (typeof item === 'string' ? item.trim() : '')).filter(Boolean);
|
|
198
202
|
return normalized.length ? normalized : [...fallback];
|
|
199
203
|
}
|
|
204
|
+
function normalizeKeywordInjectionMap(value) {
|
|
205
|
+
const map = {};
|
|
206
|
+
const record = asRecord(value);
|
|
207
|
+
if (!record) {
|
|
208
|
+
return map;
|
|
209
|
+
}
|
|
210
|
+
for (const key of KEYWORD_INJECTION_KEYS) {
|
|
211
|
+
const rawList = record[key];
|
|
212
|
+
if (!Array.isArray(rawList)) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const normalized = rawList
|
|
216
|
+
.map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
|
|
217
|
+
.filter(Boolean);
|
|
218
|
+
if (normalized.length) {
|
|
219
|
+
map[key] = normalized;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return map;
|
|
223
|
+
}
|
|
200
224
|
function normalizeProvider(providerId, raw) {
|
|
201
225
|
const provider = asRecord(raw);
|
|
202
226
|
const providerType = detectProviderType(provider);
|
|
@@ -234,9 +258,9 @@ function normalizeResponsesConfig(provider) {
|
|
|
234
258
|
return undefined;
|
|
235
259
|
}
|
|
236
260
|
function resolveCompatibilityProfile(provider) {
|
|
237
|
-
const
|
|
238
|
-
if (
|
|
239
|
-
return
|
|
261
|
+
const profile = readOptionalString(provider.compatibilityProfile);
|
|
262
|
+
if (profile) {
|
|
263
|
+
return profile;
|
|
240
264
|
}
|
|
241
265
|
return 'default';
|
|
242
266
|
}
|
|
@@ -1,19 +1,42 @@
|
|
|
1
1
|
import { DEFAULT_ROUTE, ROUTE_PRIORITY } from './types.js';
|
|
2
|
-
|
|
2
|
+
import { DEFAULT_THINKING_KEYWORDS } from './default-thinking-keywords.js';
|
|
3
|
+
const DEFAULT_LONG_CONTEXT_THRESHOLD = 180000;
|
|
3
4
|
export class RoutingClassifier {
|
|
4
5
|
config;
|
|
5
6
|
constructor(config) {
|
|
7
|
+
const keywordConfig = normalizeKeywordConfig(config);
|
|
6
8
|
this.config = {
|
|
7
9
|
longContextThresholdTokens: config.longContextThresholdTokens ?? DEFAULT_LONG_CONTEXT_THRESHOLD,
|
|
8
|
-
thinkingKeywords:
|
|
9
|
-
backgroundKeywords:
|
|
10
|
+
thinkingKeywords: keywordConfig.thinking,
|
|
11
|
+
backgroundKeywords: keywordConfig.background,
|
|
12
|
+
visionKeywords: keywordConfig.vision,
|
|
13
|
+
codingKeywords: keywordConfig.coding
|
|
10
14
|
};
|
|
11
15
|
}
|
|
12
16
|
classify(features) {
|
|
13
17
|
const evaluations = [
|
|
18
|
+
{
|
|
19
|
+
route: 'longcontext',
|
|
20
|
+
triggered: features.estimatedTokens >= (this.config.longContextThresholdTokens ?? DEFAULT_LONG_CONTEXT_THRESHOLD),
|
|
21
|
+
reason: 'longcontext:token-threshold'
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
route: 'thinking',
|
|
25
|
+
triggered: features.hasThinkingKeyword ||
|
|
26
|
+
containsKeywords(features.userTextSample, this.config.thinkingKeywords ?? []) ||
|
|
27
|
+
features.previousToolCategory === 'context_read' ||
|
|
28
|
+
features.previousToolCategory === 'plan',
|
|
29
|
+
reason: 'thinking:keywords'
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
route: 'background',
|
|
33
|
+
triggered: containsKeywords(features.userTextSample, this.config.backgroundKeywords ?? []),
|
|
34
|
+
reason: 'background:keywords'
|
|
35
|
+
},
|
|
14
36
|
{
|
|
15
37
|
route: 'vision',
|
|
16
|
-
triggered: features.hasVisionTool && features.hasImageAttachment
|
|
38
|
+
triggered: (features.hasVisionTool && features.hasImageAttachment) ||
|
|
39
|
+
features.previousToolCategory === 'vision',
|
|
17
40
|
reason: 'vision:requires-tool+image'
|
|
18
41
|
},
|
|
19
42
|
{
|
|
@@ -23,29 +46,15 @@ export class RoutingClassifier {
|
|
|
23
46
|
},
|
|
24
47
|
{
|
|
25
48
|
route: 'coding',
|
|
26
|
-
triggered: features.hasCodingTool
|
|
49
|
+
triggered: features.hasCodingTool ||
|
|
50
|
+
containsKeywords(features.userTextSample, this.config.codingKeywords ?? []) ||
|
|
51
|
+
features.previousToolCategory === 'coding',
|
|
27
52
|
reason: 'coding:coding-tools-detected'
|
|
28
53
|
},
|
|
29
54
|
{
|
|
30
55
|
route: 'tools',
|
|
31
56
|
triggered: features.hasTools || features.hasToolCallResponses,
|
|
32
57
|
reason: 'tools:tool-request-detected'
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
route: 'longcontext',
|
|
36
|
-
triggered: features.estimatedTokens >= (this.config.longContextThresholdTokens ?? DEFAULT_LONG_CONTEXT_THRESHOLD),
|
|
37
|
-
reason: 'longcontext:token-threshold'
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
route: 'thinking',
|
|
41
|
-
triggered: features.hasThinkingKeyword ||
|
|
42
|
-
containsKeywords(features.userTextSample, this.config.thinkingKeywords ?? []),
|
|
43
|
-
reason: 'thinking:keywords'
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
route: 'background',
|
|
47
|
-
triggered: containsKeywords(features.userTextSample, this.config.backgroundKeywords ?? []),
|
|
48
|
-
reason: 'background:keywords'
|
|
49
58
|
}
|
|
50
59
|
];
|
|
51
60
|
const triggeredEvaluations = evaluations.filter((evaluation) => evaluation.triggered);
|
|
@@ -84,12 +93,6 @@ export class RoutingClassifier {
|
|
|
84
93
|
return index >= 0 ? index : ROUTE_PRIORITY.length;
|
|
85
94
|
}
|
|
86
95
|
}
|
|
87
|
-
function normalizeList(source, fallback) {
|
|
88
|
-
if (!source || source.length === 0) {
|
|
89
|
-
return fallback;
|
|
90
|
-
}
|
|
91
|
-
return source.map((item) => item.toLowerCase());
|
|
92
|
-
}
|
|
93
96
|
function containsKeywords(text, keywords) {
|
|
94
97
|
if (!text || !keywords.length) {
|
|
95
98
|
return false;
|
|
@@ -97,3 +100,29 @@ function containsKeywords(text, keywords) {
|
|
|
97
100
|
const normalized = text.toLowerCase();
|
|
98
101
|
return keywords.some((keyword) => normalized.includes(keyword));
|
|
99
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const DEFAULT_THINKING_KEYWORDS: string[];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { countRequestTokens } from './token-counter.js';
|
|
1
2
|
const THINKING_KEYWORDS = ['let me think', 'chain of thought', 'cot', 'reason step', 'deliberate'];
|
|
2
3
|
const WEB_TOOL_KEYWORDS = ['websearch', 'web_search', 'web-search', 'webfetch', 'web_fetch', 'web_request', 'search_web', 'internet_search'];
|
|
3
4
|
export function buildRoutingFeatures(request, metadata) {
|
|
@@ -7,10 +8,11 @@ export function buildRoutingFeatures(request, metadata) {
|
|
|
7
8
|
const normalizedUserText = latestUserText.toLowerCase();
|
|
8
9
|
const hasTools = Array.isArray(request.tools) && request.tools.length > 0;
|
|
9
10
|
const hasToolCallResponses = assistantMessages.some((msg) => Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0);
|
|
10
|
-
const estimatedTokens =
|
|
11
|
+
const estimatedTokens = countRequestTokens(request);
|
|
11
12
|
const hasThinking = detectKeyword(normalizedUserText, THINKING_KEYWORDS);
|
|
12
|
-
const
|
|
13
|
-
const
|
|
13
|
+
const previousToolCategory = detectPreviousToolCategory(request.messages);
|
|
14
|
+
const hasVisionTool = detectVisionTool(request) || previousToolCategory === 'vision';
|
|
15
|
+
const hasImageAttachment = previousToolCategory === 'vision' || (hasVisionTool && detectImageAttachment(latestUserMessage));
|
|
14
16
|
const hasCodingTool = detectCodingTool(request);
|
|
15
17
|
const hasWebTool = detectWebTool(request);
|
|
16
18
|
const hasThinkingKeyword = hasThinking || detectExtendedThinkingKeyword(normalizedUserText);
|
|
@@ -28,6 +30,7 @@ export function buildRoutingFeatures(request, metadata) {
|
|
|
28
30
|
hasCodingTool,
|
|
29
31
|
hasThinkingKeyword,
|
|
30
32
|
estimatedTokens,
|
|
33
|
+
previousToolCategory,
|
|
31
34
|
metadata: {
|
|
32
35
|
...metadata
|
|
33
36
|
}
|
|
@@ -125,12 +128,111 @@ function detectExtendedThinkingKeyword(text) {
|
|
|
125
128
|
const keywords = ['仔细分析', '思考', '超级思考', '深度思考', 'careful analysis', 'deep thinking', 'deliberate'];
|
|
126
129
|
return keywords.some((keyword) => text.includes(keyword));
|
|
127
130
|
}
|
|
128
|
-
function
|
|
129
|
-
if (!
|
|
130
|
-
return
|
|
131
|
+
function detectPreviousToolCategory(messages) {
|
|
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';
|
|
131
181
|
}
|
|
132
|
-
|
|
133
|
-
|
|
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
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
return JSON.parse(raw);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const CONTEXT_READ_PATTERNS = [
|
|
200
|
+
/\brg\b/i,
|
|
201
|
+
/\bsed\b/i,
|
|
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
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return false;
|
|
134
236
|
}
|
|
135
237
|
function extractToolName(tool) {
|
|
136
238
|
if (!tool || typeof tool !== 'object') {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { encoding_for_model, get_encoding } from 'tiktoken';
|
|
2
|
+
const DEFAULT_ENCODING = 'cl100k_base';
|
|
3
|
+
const encoderCache = new Map();
|
|
4
|
+
let defaultEncoder = null;
|
|
5
|
+
function getEncoder(model) {
|
|
6
|
+
if (model) {
|
|
7
|
+
const normalized = model.trim();
|
|
8
|
+
if (encoderCache.has(normalized)) {
|
|
9
|
+
return encoderCache.get(normalized);
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const encoder = encoding_for_model(normalized);
|
|
13
|
+
encoderCache.set(normalized, encoder);
|
|
14
|
+
return encoder;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// fall back to default encoder
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (!defaultEncoder) {
|
|
21
|
+
defaultEncoder = get_encoding(DEFAULT_ENCODING);
|
|
22
|
+
}
|
|
23
|
+
return defaultEncoder;
|
|
24
|
+
}
|
|
25
|
+
export function countRequestTokens(request) {
|
|
26
|
+
const encoder = getEncoder(request.model);
|
|
27
|
+
let total = 0;
|
|
28
|
+
for (const message of request.messages || []) {
|
|
29
|
+
total += countMessageTokens(message, encoder);
|
|
30
|
+
}
|
|
31
|
+
if (Array.isArray(request.tools)) {
|
|
32
|
+
for (const tool of request.tools) {
|
|
33
|
+
total += encodeText(JSON.stringify(tool ?? {}), encoder);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (request.parameters) {
|
|
37
|
+
total += encodeText(JSON.stringify(request.parameters), encoder);
|
|
38
|
+
}
|
|
39
|
+
if (request.metadata) {
|
|
40
|
+
total += encodeText(JSON.stringify(request.metadata), encoder);
|
|
41
|
+
}
|
|
42
|
+
return total;
|
|
43
|
+
}
|
|
44
|
+
function countMessageTokens(message, encoder) {
|
|
45
|
+
let total = 0;
|
|
46
|
+
total += encodeText(message.role, encoder);
|
|
47
|
+
total += encodeContent(message.content, encoder);
|
|
48
|
+
if (Array.isArray(message.tool_calls)) {
|
|
49
|
+
for (const call of message.tool_calls) {
|
|
50
|
+
total += encodeText(JSON.stringify(call ?? {}), encoder);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (Array.isArray(message.metadata?.toolRuns)) {
|
|
54
|
+
total += encodeText(JSON.stringify(message.metadata?.toolRuns), encoder);
|
|
55
|
+
}
|
|
56
|
+
if (message.name) {
|
|
57
|
+
total += encodeText(message.name, encoder);
|
|
58
|
+
}
|
|
59
|
+
if (message.metadata) {
|
|
60
|
+
total += encodeText(JSON.stringify(message.metadata), encoder);
|
|
61
|
+
}
|
|
62
|
+
if (message.tool_call_id) {
|
|
63
|
+
total += encodeText(message.tool_call_id, encoder);
|
|
64
|
+
}
|
|
65
|
+
return total;
|
|
66
|
+
}
|
|
67
|
+
function encodeContent(content, encoder) {
|
|
68
|
+
if (content === null || content === undefined) {
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
if (typeof content === 'string') {
|
|
72
|
+
return encodeText(content, encoder);
|
|
73
|
+
}
|
|
74
|
+
if (Array.isArray(content)) {
|
|
75
|
+
let total = 0;
|
|
76
|
+
for (const part of content) {
|
|
77
|
+
if (typeof part === 'string') {
|
|
78
|
+
total += encodeText(part, encoder);
|
|
79
|
+
}
|
|
80
|
+
else if (part && typeof part === 'object') {
|
|
81
|
+
if (typeof part.text === 'string') {
|
|
82
|
+
total += encodeText(part.text, encoder);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
total += encodeText(JSON.stringify(part), encoder);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return total;
|
|
90
|
+
}
|
|
91
|
+
if (typeof content === 'object') {
|
|
92
|
+
return encodeText(JSON.stringify(content), encoder);
|
|
93
|
+
}
|
|
94
|
+
return encodeText(String(content), encoder);
|
|
95
|
+
}
|
|
96
|
+
function encodeText(value, encoder) {
|
|
97
|
+
if (value === null || value === undefined) {
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
const text = typeof value === 'string' ? value : String(value);
|
|
101
|
+
if (!text.trim()) {
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
return encoder.encode(text).length;
|
|
105
|
+
}
|
|
@@ -5,6 +5,7 @@ import type { StandardizedRequest } from '../../conversion/hub/types/standardize
|
|
|
5
5
|
export declare const DEFAULT_ROUTE = "default";
|
|
6
6
|
export declare const ROUTE_PRIORITY: string[];
|
|
7
7
|
export type RoutingPools = Record<string, string[]>;
|
|
8
|
+
export type PreviousToolCategory = 'context_read' | 'plan' | 'vision' | 'coding';
|
|
8
9
|
export interface ProviderAuthConfig {
|
|
9
10
|
type: 'apiKey' | 'oauth';
|
|
10
11
|
secretRef?: string;
|
|
@@ -47,12 +48,14 @@ export interface ProviderRuntimeProfile {
|
|
|
47
48
|
processMode?: 'chat' | 'passthrough';
|
|
48
49
|
responsesConfig?: ResponsesProviderConfig;
|
|
49
50
|
}
|
|
51
|
+
export type RouteKeywordCategory = 'thinking' | 'background' | 'vision' | 'coding';
|
|
50
52
|
export interface VirtualRouterClassifierConfig {
|
|
51
53
|
longContextThresholdTokens?: number;
|
|
52
54
|
thinkingKeywords?: string[];
|
|
53
55
|
codingKeywords?: string[];
|
|
54
56
|
backgroundKeywords?: string[];
|
|
55
57
|
visionKeywords?: string[];
|
|
58
|
+
keywordInjections?: Partial<Record<RouteKeywordCategory, string[]>>;
|
|
56
59
|
}
|
|
57
60
|
export interface LoadBalancingPolicy {
|
|
58
61
|
strategy: 'round-robin' | 'weighted' | 'sticky';
|
|
@@ -111,6 +114,7 @@ export interface RoutingFeatures {
|
|
|
111
114
|
hasCodingTool: boolean;
|
|
112
115
|
hasThinkingKeyword: boolean;
|
|
113
116
|
estimatedTokens: number;
|
|
117
|
+
previousToolCategory?: PreviousToolCategory | null;
|
|
114
118
|
metadata: RouterMetadataInput;
|
|
115
119
|
}
|
|
116
120
|
export interface ClassificationResult {
|
|
@@ -395,6 +395,36 @@ export class ResponsesResponseBuilder {
|
|
|
395
395
|
outputItemState.arguments = '';
|
|
396
396
|
outputItemState.lastEventTime = event.timestamp;
|
|
397
397
|
}
|
|
398
|
+
coerceArgumentsChunk(raw) {
|
|
399
|
+
if (typeof raw === 'string') {
|
|
400
|
+
return raw;
|
|
401
|
+
}
|
|
402
|
+
if (raw && typeof raw === 'object') {
|
|
403
|
+
try {
|
|
404
|
+
return JSON.stringify(raw);
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
return String(raw);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
shouldOverrideArguments(current, incoming) {
|
|
413
|
+
if (!incoming) {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
const trimmed = incoming.trim();
|
|
417
|
+
if (!current || !current.length) {
|
|
418
|
+
return trimmed.length > 0;
|
|
419
|
+
}
|
|
420
|
+
if (!trimmed.length) {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
if (trimmed === '{}' || trimmed.toLowerCase() === 'null') {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
398
428
|
/**
|
|
399
429
|
* 处理function_call.delta事件
|
|
400
430
|
*/
|
|
@@ -404,10 +434,10 @@ export class ResponsesResponseBuilder {
|
|
|
404
434
|
if (!outputItemState) {
|
|
405
435
|
throw new Error(`Output item not found: ${data.item_id}`);
|
|
406
436
|
}
|
|
407
|
-
const chunk = (data
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
if (
|
|
437
|
+
const chunk = this.coerceArgumentsChunk(data?.delta?.arguments) ??
|
|
438
|
+
this.coerceArgumentsChunk(data?.delta) ??
|
|
439
|
+
this.coerceArgumentsChunk(data?.arguments);
|
|
440
|
+
if (chunk) {
|
|
411
441
|
outputItemState.arguments = (outputItemState.arguments || '') + chunk;
|
|
412
442
|
}
|
|
413
443
|
outputItemState.lastEventTime = event.timestamp;
|
|
@@ -445,8 +475,16 @@ export class ResponsesResponseBuilder {
|
|
|
445
475
|
outputItemState.name = data.name;
|
|
446
476
|
if (typeof data.call_id === 'string' && data.call_id)
|
|
447
477
|
outputItemState.callId = data.call_id;
|
|
448
|
-
|
|
449
|
-
|
|
478
|
+
const finalChunk = this.coerceArgumentsChunk(data?.arguments) ??
|
|
479
|
+
this.coerceArgumentsChunk(data?.delta?.arguments) ??
|
|
480
|
+
this.coerceArgumentsChunk(data?.delta);
|
|
481
|
+
if (this.shouldOverrideArguments(outputItemState.arguments, finalChunk)) {
|
|
482
|
+
outputItemState.arguments = finalChunk;
|
|
483
|
+
}
|
|
484
|
+
else if (!outputItemState.arguments && finalChunk) {
|
|
485
|
+
// 没有任何累计增量时,保底写入 done 事件里的值
|
|
486
|
+
outputItemState.arguments = finalChunk;
|
|
487
|
+
}
|
|
450
488
|
}
|
|
451
489
|
catch { /* ignore */ }
|
|
452
490
|
outputItemState.status = 'completed';
|
|
@@ -630,14 +668,17 @@ export class ResponsesResponseBuilder {
|
|
|
630
668
|
this.state = 'error';
|
|
631
669
|
}
|
|
632
670
|
handleResponseCompleted(event) {
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
671
|
+
const payload = event.data?.response ?? event.data;
|
|
672
|
+
const usage = (payload && payload.usage)
|
|
673
|
+
? payload.usage
|
|
674
|
+
: event.data?.usage;
|
|
675
|
+
if (usage) {
|
|
676
|
+
this.response.usage = usage;
|
|
636
677
|
}
|
|
637
678
|
// 标准化完成态:部分上游的 response.completed 不包含 status 字段
|
|
638
679
|
// 若未提供明确的完成状态,统一标记为 'completed',而不是沿用之前的 in_progress。
|
|
639
|
-
this.response.status = (
|
|
640
|
-
?
|
|
680
|
+
this.response.status = (payload && payload.status != null)
|
|
681
|
+
? payload.status
|
|
641
682
|
: 'completed';
|
|
642
683
|
// 将已聚合的输出写回并标记完成(若为空数组也重建)
|
|
643
684
|
try {
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsonstudio/llms",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.034",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"scripts": {
|
|
9
|
-
"build": "tsc -p tsconfig.json",
|
|
10
|
-
"build:dev": "tsc -p tsconfig.json",
|
|
9
|
+
"build": "node scripts/bump-version.mjs && tsc -p tsconfig.json",
|
|
10
|
+
"build:dev": "node scripts/bump-version.mjs && tsc -p tsconfig.json",
|
|
11
11
|
"lint": "eslint --no-eslintrc -c .eslintrc.json src --ext .ts --no-cache",
|
|
12
12
|
"lint:fix": "eslint --no-eslintrc -c .eslintrc.json src --ext .ts --no-cache --fix",
|
|
13
13
|
"postbuild": "node scripts/tests/run-matrix-ci.mjs",
|