@jsonstudio/llms 0.6.1643 → 0.6.1739
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/actions/harvest-tool-calls-from-text.d.ts +10 -0
- package/dist/conversion/compat/actions/harvest-tool-calls-from-text.js +121 -0
- package/dist/conversion/compat/actions/iflow-kimi-cli-defaults.d.ts +10 -0
- package/dist/conversion/compat/actions/iflow-kimi-cli-defaults.js +80 -0
- package/dist/conversion/compat/actions/iflow-kimi-history-media-placeholder.d.ts +7 -0
- package/dist/conversion/compat/actions/iflow-kimi-history-media-placeholder.js +161 -0
- package/dist/conversion/compat/actions/iflow-kimi-thinking-reasoning-fill.d.ts +12 -0
- package/dist/conversion/compat/actions/iflow-kimi-thinking-reasoning-fill.js +67 -0
- package/dist/conversion/compat/actions/iflow-response-body-unwrap.d.ts +9 -0
- package/dist/conversion/compat/actions/iflow-response-body-unwrap.js +140 -0
- package/dist/conversion/compat/actions/lmstudio-responses-fc-ids.d.ts +10 -0
- package/dist/conversion/compat/actions/lmstudio-responses-fc-ids.js +59 -0
- package/dist/conversion/compat/actions/lmstudio-responses-input-stringify.d.ts +14 -0
- package/dist/conversion/compat/actions/lmstudio-responses-input-stringify.js +125 -0
- package/dist/conversion/compat/actions/normalize-tool-call-ids.d.ts +11 -0
- package/dist/conversion/compat/actions/normalize-tool-call-ids.js +140 -0
- package/dist/conversion/compat/actions/strip-orphan-function-calls-tag.d.ts +2 -0
- package/dist/conversion/compat/actions/strip-orphan-function-calls-tag.js +152 -0
- package/dist/conversion/compat/antigravity-session-signature.d.ts +1 -1
- package/dist/conversion/compat/antigravity-session-signature.js +5 -4
- package/dist/conversion/compat/profiles/chat-iflow.json +6 -0
- package/dist/conversion/compat/profiles/chat-lmstudio.json +7 -1
- package/dist/conversion/hub/operation-table/operation-table-runner.js +1 -1
- package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +19 -2
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +101 -5
- package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.d.ts +2 -0
- package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.js +63 -0
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +18 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +1 -1
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +8 -5
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.js +5 -1
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +113 -0
- package/dist/conversion/hub/pipeline/target-utils.js +3 -0
- package/dist/conversion/hub/response/provider-response.js +27 -1
- package/dist/conversion/responses/responses-openai-bridge.js +32 -6
- package/dist/conversion/shared/anthropic-message-utils.js +20 -5
- package/dist/conversion/shared/bridge-id-utils.d.ts +2 -0
- package/dist/conversion/shared/bridge-id-utils.js +52 -15
- package/dist/conversion/shared/responses-conversation-store.js +40 -5
- package/dist/conversion/shared/responses-output-builder.js +23 -7
- package/dist/conversion/shared/responses-tool-utils.d.ts +1 -0
- package/dist/conversion/shared/responses-tool-utils.js +30 -13
- package/dist/conversion/shared/text-markup-normalizer.d.ts +1 -0
- package/dist/conversion/shared/text-markup-normalizer.js +269 -1
- package/dist/router/virtual-router/bootstrap.js +31 -7
- package/dist/router/virtual-router/classifier.js +1 -1
- package/dist/router/virtual-router/engine/antigravity/alias-lease.d.ts +33 -0
- package/dist/router/virtual-router/engine/antigravity/alias-lease.js +247 -0
- package/dist/router/virtual-router/engine/health/index.d.ts +23 -0
- package/dist/router/virtual-router/engine/health/index.js +720 -0
- package/dist/router/virtual-router/engine/provider-key/parse.d.ts +6 -0
- package/dist/router/virtual-router/engine/provider-key/parse.js +43 -0
- package/dist/router/virtual-router/engine/routing-pools/index.d.ts +13 -0
- package/dist/router/virtual-router/engine/routing-pools/index.js +225 -0
- package/dist/router/virtual-router/engine/routing-state/keys.d.ts +3 -0
- package/dist/router/virtual-router/engine/routing-state/keys.js +30 -0
- package/dist/router/virtual-router/engine/routing-state/metadata.d.ts +6 -0
- package/dist/router/virtual-router/engine/routing-state/metadata.js +132 -0
- package/dist/router/virtual-router/engine/routing-state/store.d.ts +11 -0
- package/dist/router/virtual-router/engine/routing-state/store.js +107 -0
- package/dist/router/virtual-router/engine-health.d.ts +1 -23
- package/dist/router/virtual-router/engine-health.js +1 -720
- package/dist/router/virtual-router/engine-selection/route-utils.js +57 -0
- package/dist/router/virtual-router/engine-selection/tier-selection-select.js +8 -48
- package/dist/router/virtual-router/engine-selection/tier-selection.js +34 -17
- package/dist/router/virtual-router/engine-selection.d.ts +1 -13
- package/dist/router/virtual-router/engine-selection.js +1 -225
- package/dist/router/virtual-router/engine.d.ts +2 -23
- package/dist/router/virtual-router/engine.js +130 -603
- package/dist/router/virtual-router/message-utils.js +15 -5
- package/dist/servertool/engine.js +4 -4
- package/dist/servertool/handlers/followup-request-builder.js +46 -0
- package/dist/servertool/handlers/gemini-empty-reply-continue.js +48 -47
- package/dist/servertool/handlers/stop-message-auto.js +64 -7
- package/dist/servertool/handlers/vision.js +10 -0
- package/dist/servertool/types.d.ts +3 -0
- package/dist/sse/sse-to-json/builders/response-builder.js +6 -0
- package/dist/sse/sse-to-json/chat-sse-to-json-converter.js +32 -2
- package/dist/sse/sse-to-json/parsers/sse-parser.js +34 -0
- package/dist/sse/sse-to-json/responses-sse-to-json-converter.d.ts +1 -0
- package/dist/sse/sse-to-json/responses-sse-to-json-converter.js +33 -1
- package/dist/tools/apply-patch/args-normalizer/default-actions.d.ts +2 -0
- package/dist/tools/apply-patch/args-normalizer/default-actions.js +12 -0
- package/dist/tools/apply-patch/args-normalizer/extract-patch.d.ts +2 -0
- package/dist/tools/apply-patch/args-normalizer/extract-patch.js +15 -0
- package/dist/tools/apply-patch/args-normalizer/index.d.ts +2 -0
- package/dist/tools/apply-patch/args-normalizer/index.js +164 -0
- package/dist/tools/apply-patch/args-normalizer/structured-builders.d.ts +7 -0
- package/dist/tools/apply-patch/args-normalizer/structured-builders.js +85 -0
- package/dist/tools/apply-patch/args-normalizer/types.d.ts +54 -0
- package/dist/tools/apply-patch/args-normalizer/types.js +1 -0
- package/dist/tools/apply-patch/patch-text/looks-like-patch.js +1 -0
- package/dist/tools/apply-patch/patch-text/normalize.js +104 -5
- package/dist/tools/apply-patch/structured/coercion.js +28 -4
- package/dist/tools/apply-patch/validator.js +7 -146
- package/package.json +1 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
const MAX_RESPONSES_ITEM_ID_LENGTH = 64;
|
|
1
3
|
function sanitizeCore(value) {
|
|
2
4
|
return value
|
|
3
5
|
.replace(/[^A-Za-z0-9_-]/g, '_')
|
|
@@ -5,7 +7,26 @@ function sanitizeCore(value) {
|
|
|
5
7
|
.replace(/^_+/, '')
|
|
6
8
|
.replace(/_+$/, '');
|
|
7
9
|
}
|
|
8
|
-
function
|
|
10
|
+
function shortHash(value) {
|
|
11
|
+
try {
|
|
12
|
+
return createHash('sha1').update(value).digest('hex').slice(0, 10);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return Math.random().toString(36).slice(2, 12);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function clampPrefixedId(prefix, core, hashSource) {
|
|
19
|
+
const sanitized = sanitizeCore(core) || Math.random().toString(36).slice(2, 10);
|
|
20
|
+
const direct = `${prefix}${sanitized}`;
|
|
21
|
+
if (direct.length <= MAX_RESPONSES_ITEM_ID_LENGTH) {
|
|
22
|
+
return direct;
|
|
23
|
+
}
|
|
24
|
+
const hash = shortHash(`${prefix}|${hashSource}|${sanitized}`);
|
|
25
|
+
const room = Math.max(1, MAX_RESPONSES_ITEM_ID_LENGTH - prefix.length - 1 - hash.length);
|
|
26
|
+
const head = sanitizeCore(sanitized.slice(0, room)) || 'id';
|
|
27
|
+
return `${prefix}${head}_${hash}`;
|
|
28
|
+
}
|
|
29
|
+
function extractCore(value) {
|
|
9
30
|
if (typeof value !== 'string')
|
|
10
31
|
return null;
|
|
11
32
|
const trimmed = value.trim();
|
|
@@ -20,23 +41,39 @@ function stripPrefix(value) {
|
|
|
20
41
|
else if (/^call[_-]/i.test(sanitized)) {
|
|
21
42
|
sanitized = sanitized.replace(/^call[_-]?/i, '');
|
|
22
43
|
}
|
|
23
|
-
|
|
24
|
-
sanitized = Math.random().toString(36).slice(2, 10);
|
|
25
|
-
}
|
|
26
|
-
return `fc_${sanitized}`;
|
|
44
|
+
return sanitizeCore(sanitized) || null;
|
|
27
45
|
}
|
|
28
|
-
function normalizeWithFallback(options) {
|
|
29
|
-
const
|
|
30
|
-
if (
|
|
31
|
-
return
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
function normalizeWithFallback(options, prefix) {
|
|
47
|
+
const callCore = extractCore(options.callId);
|
|
48
|
+
if (callCore) {
|
|
49
|
+
return clampPrefixedId(prefix, callCore, String(options.callId));
|
|
50
|
+
}
|
|
51
|
+
const fallbackCore = extractCore(options.fallback);
|
|
52
|
+
if (fallbackCore) {
|
|
53
|
+
return clampPrefixedId(prefix, fallbackCore, String(options.fallback));
|
|
54
|
+
}
|
|
55
|
+
const randomCore = Math.random().toString(36).slice(2, 10);
|
|
56
|
+
return clampPrefixedId(prefix, randomCore, randomCore);
|
|
36
57
|
}
|
|
37
58
|
export function normalizeFunctionCallId(options) {
|
|
38
|
-
return normalizeWithFallback(options);
|
|
59
|
+
return normalizeWithFallback(options, 'fc_');
|
|
39
60
|
}
|
|
40
61
|
export function normalizeFunctionCallOutputId(options) {
|
|
41
|
-
return normalizeWithFallback(options);
|
|
62
|
+
return normalizeWithFallback(options, 'fc_');
|
|
63
|
+
}
|
|
64
|
+
export function normalizeResponsesCallId(options) {
|
|
65
|
+
return normalizeWithFallback(options, 'call_');
|
|
66
|
+
}
|
|
67
|
+
export function clampResponsesInputItemId(raw) {
|
|
68
|
+
const trimmed = typeof raw === 'string' ? raw.trim() : '';
|
|
69
|
+
if (!trimmed) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
if (trimmed.length <= MAX_RESPONSES_ITEM_ID_LENGTH) {
|
|
73
|
+
return trimmed;
|
|
74
|
+
}
|
|
75
|
+
const hash = shortHash(trimmed);
|
|
76
|
+
const room = Math.max(1, MAX_RESPONSES_ITEM_ID_LENGTH - 1 - hash.length);
|
|
77
|
+
const head = trimmed.slice(0, room);
|
|
78
|
+
return `${head}_${hash}`;
|
|
42
79
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ProviderProtocolError } from './errors.js';
|
|
2
|
+
import { normalizeFunctionCallOutputId } from './bridge-id-utils.js';
|
|
2
3
|
const TTL_MS = 1000 * 60 * 30; // 30min
|
|
3
4
|
function cloneDeep(value) {
|
|
4
5
|
try {
|
|
@@ -111,7 +112,7 @@ function convertOutputToInputItems(response) {
|
|
|
111
112
|
}
|
|
112
113
|
return items;
|
|
113
114
|
}
|
|
114
|
-
function normalizeSubmittedToolOutputs(toolOutputs) {
|
|
115
|
+
function normalizeSubmittedToolOutputs(toolOutputs, callIdToFunctionItemId) {
|
|
115
116
|
const items = [];
|
|
116
117
|
const submitted = [];
|
|
117
118
|
toolOutputs.forEach((entry, index) => {
|
|
@@ -126,7 +127,17 @@ function normalizeSubmittedToolOutputs(toolOutputs) {
|
|
|
126
127
|
? rec.id
|
|
127
128
|
: undefined;
|
|
128
129
|
const trimmed = typeof rawId === 'string' ? rawId.trim() : '';
|
|
129
|
-
const callId = trimmed.length ? trimmed : `
|
|
130
|
+
const callId = trimmed.length ? trimmed : `call_resume_${index}`;
|
|
131
|
+
const mappedItemId = callIdToFunctionItemId && typeof callId === 'string' && callIdToFunctionItemId.has(callId)
|
|
132
|
+
? callIdToFunctionItemId.get(callId)
|
|
133
|
+
: undefined;
|
|
134
|
+
const mappedIdTrimmed = typeof mappedItemId === 'string' ? mappedItemId.trim() : '';
|
|
135
|
+
const outputId = mappedIdTrimmed.length
|
|
136
|
+
? normalizeFunctionCallOutputId({ callId: mappedIdTrimmed, fallback: mappedIdTrimmed })
|
|
137
|
+
: normalizeFunctionCallOutputId({
|
|
138
|
+
callId,
|
|
139
|
+
fallback: trimmed.length ? trimmed : `fc_resume_${index}`
|
|
140
|
+
});
|
|
130
141
|
const outputValue = rec.output ?? null;
|
|
131
142
|
const normalizedOutput = typeof outputValue === 'string'
|
|
132
143
|
? outputValue
|
|
@@ -140,7 +151,7 @@ function normalizeSubmittedToolOutputs(toolOutputs) {
|
|
|
140
151
|
})();
|
|
141
152
|
items.push({
|
|
142
153
|
type: 'function_call_output',
|
|
143
|
-
id:
|
|
154
|
+
id: outputId,
|
|
144
155
|
call_id: callId,
|
|
145
156
|
output: normalizedOutput
|
|
146
157
|
});
|
|
@@ -244,11 +255,35 @@ class ResponsesConversationStore {
|
|
|
244
255
|
});
|
|
245
256
|
}
|
|
246
257
|
const mergedInput = coerceInputArray(entry.input);
|
|
247
|
-
const
|
|
258
|
+
const callIdToFunctionItemId = new Map();
|
|
259
|
+
for (const item of mergedInput) {
|
|
260
|
+
if (!item || typeof item !== 'object')
|
|
261
|
+
continue;
|
|
262
|
+
const type = typeof item.type === 'string' ? String(item.type) : '';
|
|
263
|
+
if (type !== 'function_call')
|
|
264
|
+
continue;
|
|
265
|
+
const id = typeof item.id === 'string' ? String(item.id).trim() : '';
|
|
266
|
+
const callId = typeof item.call_id === 'string' ? String(item.call_id).trim() : '';
|
|
267
|
+
if (id) {
|
|
268
|
+
callIdToFunctionItemId.set(id, id);
|
|
269
|
+
}
|
|
270
|
+
if (callId) {
|
|
271
|
+
callIdToFunctionItemId.set(callId, id || callId);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const normalizedOutputs = normalizeSubmittedToolOutputs(toolOutputs, callIdToFunctionItemId);
|
|
248
275
|
mergedInput.push(...normalizedOutputs.items);
|
|
249
276
|
const payload = cloneDeep(entry.basePayload);
|
|
250
277
|
payload.input = mergedInput;
|
|
251
|
-
|
|
278
|
+
// Preserve the caller's streaming intent for the resume request.
|
|
279
|
+
// Do not force-enable streaming here: some upstreams return JSON even when we send `Accept: text/event-stream`,
|
|
280
|
+
// and forcing `stream=true` can trip SSE decoders on non-SSE bodies.
|
|
281
|
+
payload.stream =
|
|
282
|
+
typeof submitPayload.stream === 'boolean'
|
|
283
|
+
? Boolean(submitPayload.stream)
|
|
284
|
+
: typeof entry.basePayload.stream === 'boolean'
|
|
285
|
+
? Boolean(entry.basePayload.stream)
|
|
286
|
+
: false;
|
|
252
287
|
payload.previous_response_id = responseId;
|
|
253
288
|
if (Array.isArray(entry.tools) && entry.tools.length) {
|
|
254
289
|
payload.tools = cloneDeep(entry.tools);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { normalizeFunctionCallId } from './bridge-id-utils.js';
|
|
1
|
+
import { normalizeFunctionCallId, normalizeFunctionCallOutputId } from './bridge-id-utils.js';
|
|
2
2
|
import { normalizeContentPart } from './output-content-normalizer.js';
|
|
3
3
|
import { expandResponsesMessageItem } from '../../sse/shared/responses-output-normalizer.js';
|
|
4
4
|
function buildToolOutputIndex(response) {
|
|
@@ -134,10 +134,18 @@ export function buildResponsesOutputFromChat(options) {
|
|
|
134
134
|
const pendingToolCalls = normalizedToolCalls.filter((entry) => !executedIds.has(entry.id));
|
|
135
135
|
const hasNormalizedToolCalls = pendingToolCalls.length > 0;
|
|
136
136
|
if (hasNormalizedToolCalls) {
|
|
137
|
+
const pendingIds = new Set(pendingToolCalls.map((entry) => entry.id));
|
|
137
138
|
for (const item of outputItems) {
|
|
138
|
-
|
|
139
|
+
const type = item.type;
|
|
140
|
+
if (type === 'message') {
|
|
139
141
|
item.status = 'in_progress';
|
|
140
142
|
}
|
|
143
|
+
if (type === 'function_call') {
|
|
144
|
+
const callId = item.call_id;
|
|
145
|
+
if (typeof callId === 'string' && pendingIds.has(callId)) {
|
|
146
|
+
item.status = 'in_progress';
|
|
147
|
+
}
|
|
148
|
+
}
|
|
141
149
|
}
|
|
142
150
|
}
|
|
143
151
|
const requiredAction = hasNormalizedToolCalls
|
|
@@ -221,14 +229,22 @@ function buildFunctionCallOutput(call, allocateOutputId, sanitizeFunctionName, b
|
|
|
221
229
|
const originalCallId = typeof call.id === 'string' && call.id.trim().length
|
|
222
230
|
? String(call.id)
|
|
223
231
|
: (typeof call.call_id === 'string' && call.call_id.trim().length ? String(call.call_id) : undefined);
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
232
|
+
// Preserve original tool call IDs when present (OpenAI Responses contract expects stable call_id
|
|
233
|
+
// across tool_call → submit_tool_outputs). Only generate/normalize when missing.
|
|
234
|
+
const callId = typeof originalCallId === 'string' && originalCallId.trim().length
|
|
235
|
+
? originalCallId.trim()
|
|
236
|
+
: normalizeFunctionCallId({
|
|
237
|
+
callId: originalCallId,
|
|
238
|
+
fallback: `fc_call_${baseCount + offset}`
|
|
239
|
+
});
|
|
240
|
+
const outputId = normalizeFunctionCallOutputId({
|
|
241
|
+
callId,
|
|
242
|
+
fallback: allocateOutputId('fc')
|
|
227
243
|
});
|
|
228
244
|
const output = {
|
|
229
|
-
id:
|
|
245
|
+
id: outputId,
|
|
230
246
|
type: 'function_call',
|
|
231
|
-
status: '
|
|
247
|
+
status: 'completed',
|
|
232
248
|
name: sanitized,
|
|
233
249
|
call_id: callId,
|
|
234
250
|
arguments: argsStr
|
|
@@ -2,6 +2,7 @@ export type ToolCallIdStyle = 'preserve' | 'fc';
|
|
|
2
2
|
type BridgeInputItem = Record<string, unknown>;
|
|
3
3
|
export interface CallIdTransformer {
|
|
4
4
|
normalizeCallId(raw: unknown): string;
|
|
5
|
+
normalizeItemId(raw: unknown, callId: string): string;
|
|
5
6
|
normalizeOutputId(callId: string, raw: unknown): string;
|
|
6
7
|
}
|
|
7
8
|
export declare function createToolCallIdTransformer(style: ToolCallIdStyle): CallIdTransformer | null;
|
|
@@ -1,25 +1,33 @@
|
|
|
1
|
-
import { normalizeFunctionCallId, normalizeFunctionCallOutputId } from './bridge-id-utils.js';
|
|
1
|
+
import { normalizeFunctionCallId, normalizeFunctionCallOutputId, normalizeResponsesCallId } from './bridge-id-utils.js';
|
|
2
2
|
export function createToolCallIdTransformer(style) {
|
|
3
3
|
if (style !== 'fc') {
|
|
4
4
|
return null;
|
|
5
5
|
}
|
|
6
6
|
let callCounter = 0;
|
|
7
|
+
let itemCounter = 0;
|
|
7
8
|
let outputCounter = 0;
|
|
8
|
-
const
|
|
9
|
+
const callAlias = new Map();
|
|
9
10
|
const normalizeCallId = (raw) => {
|
|
10
11
|
const rawStr = typeof raw === 'string' && raw.trim().length ? raw.trim() : undefined;
|
|
11
|
-
if (rawStr &&
|
|
12
|
-
return
|
|
12
|
+
if (rawStr && callAlias.has(rawStr)) {
|
|
13
|
+
return callAlias.get(rawStr);
|
|
13
14
|
}
|
|
14
|
-
const normalized =
|
|
15
|
+
const normalized = normalizeResponsesCallId({
|
|
15
16
|
callId: rawStr,
|
|
16
|
-
fallback: `
|
|
17
|
+
fallback: `call_${++callCounter}`
|
|
17
18
|
});
|
|
18
19
|
if (rawStr) {
|
|
19
|
-
|
|
20
|
+
callAlias.set(rawStr, normalized);
|
|
20
21
|
}
|
|
21
22
|
return normalized;
|
|
22
23
|
};
|
|
24
|
+
const normalizeItemId = (raw, callId) => {
|
|
25
|
+
const rawStr = typeof raw === 'string' && raw.trim().length ? raw.trim() : undefined;
|
|
26
|
+
return normalizeFunctionCallId({
|
|
27
|
+
callId: rawStr ?? callId,
|
|
28
|
+
fallback: `fc_item_${++itemCounter}`
|
|
29
|
+
});
|
|
30
|
+
};
|
|
23
31
|
const normalizeOutputId = (callId, raw) => {
|
|
24
32
|
const rawStr = typeof raw === 'string' && raw.trim().length ? raw.trim() : undefined;
|
|
25
33
|
return normalizeFunctionCallOutputId({
|
|
@@ -29,6 +37,7 @@ export function createToolCallIdTransformer(style) {
|
|
|
29
37
|
};
|
|
30
38
|
return {
|
|
31
39
|
normalizeCallId,
|
|
40
|
+
normalizeItemId,
|
|
32
41
|
normalizeOutputId
|
|
33
42
|
};
|
|
34
43
|
}
|
|
@@ -38,19 +47,27 @@ export function enforceToolCallIdStyle(input, transformer) {
|
|
|
38
47
|
continue;
|
|
39
48
|
const type = typeof entry.type === 'string' ? entry.type.toLowerCase() : '';
|
|
40
49
|
if (type === 'function_call') {
|
|
41
|
-
const
|
|
42
|
-
entry.call_id =
|
|
43
|
-
entry.id =
|
|
50
|
+
const normalizedCallId = transformer.normalizeCallId(entry.call_id ?? entry.id);
|
|
51
|
+
entry.call_id = normalizedCallId;
|
|
52
|
+
entry.id = transformer.normalizeItemId(entry.id ?? normalizedCallId, normalizedCallId);
|
|
44
53
|
continue;
|
|
45
54
|
}
|
|
46
55
|
if (type === 'function_call_output' || type === 'tool_result' || type === 'tool_message') {
|
|
47
|
-
const
|
|
48
|
-
entry.call_id =
|
|
49
|
-
|
|
56
|
+
const normalizedCallId = transformer.normalizeCallId(entry.call_id ?? entry.tool_call_id ?? entry.id);
|
|
57
|
+
entry.call_id = normalizedCallId;
|
|
58
|
+
// OpenAI `/v1/responses` request schema uses `call_id` for tool outputs.
|
|
59
|
+
// Some internal carriers may include `tool_call_id`; strip it before sending upstream
|
|
60
|
+
// to avoid strict schema errors (e.g. "Unknown parameter: input[N].tool_call_id").
|
|
61
|
+
if (entry.tool_call_id !== undefined) {
|
|
62
|
+
delete entry.tool_call_id;
|
|
63
|
+
}
|
|
64
|
+
entry.id = transformer.normalizeOutputId(normalizedCallId, entry.id);
|
|
50
65
|
}
|
|
51
66
|
}
|
|
52
67
|
}
|
|
53
68
|
export function resolveToolCallIdStyle(metadata) {
|
|
69
|
+
// Standard OpenAI `/v1/responses` requires function_call item ids to start with `fc_`.
|
|
70
|
+
// Default to `fc` unless explicitly overridden (e.g. LM Studio compat).
|
|
54
71
|
if (!metadata)
|
|
55
72
|
return 'fc';
|
|
56
73
|
const raw = metadata.toolCallIdStyle;
|
|
@@ -21,4 +21,5 @@ export declare function extractXMLToolCallsFromText(text: string): ToolCallLite[
|
|
|
21
21
|
* 仅针对已知工具名(目前为 list_directory),避免误伤普通 XML 文本。
|
|
22
22
|
*/
|
|
23
23
|
export declare function extractSimpleXmlToolsFromText(text: string): ToolCallLite[] | null;
|
|
24
|
+
export declare function extractQwenToolCallTokensFromText(text: string): ToolCallLite[] | null;
|
|
24
25
|
export declare function normalizeAssistantTextToToolCalls(message: Record<string, any>): Record<string, any>;
|
|
@@ -117,6 +117,151 @@ function enabled() {
|
|
|
117
117
|
return true;
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
|
+
function escapeControlCharsInsideJsonStrings(raw) {
|
|
121
|
+
try {
|
|
122
|
+
if (typeof raw !== 'string' || !raw)
|
|
123
|
+
return null;
|
|
124
|
+
let out = '';
|
|
125
|
+
let changed = false;
|
|
126
|
+
let inString = false;
|
|
127
|
+
let escaped = false;
|
|
128
|
+
for (let i = 0; i < raw.length; i++) {
|
|
129
|
+
const ch = raw[i];
|
|
130
|
+
if (inString) {
|
|
131
|
+
if (!escaped && ch === '"') {
|
|
132
|
+
inString = false;
|
|
133
|
+
out += ch;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (!escaped && ch === '\\') {
|
|
137
|
+
escaped = true;
|
|
138
|
+
out += ch;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
// JSON disallows unescaped control chars inside strings; models sometimes emit raw newlines/tabs.
|
|
142
|
+
if (!escaped) {
|
|
143
|
+
if (ch === '\n') {
|
|
144
|
+
out += '\\n';
|
|
145
|
+
changed = true;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (ch === '\r') {
|
|
149
|
+
if (raw[i + 1] === '\n') {
|
|
150
|
+
i++;
|
|
151
|
+
}
|
|
152
|
+
out += '\\n';
|
|
153
|
+
changed = true;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (ch === '\t') {
|
|
157
|
+
out += '\\t';
|
|
158
|
+
changed = true;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const code = ch.charCodeAt(0);
|
|
162
|
+
if (code >= 0 && code < 0x20) {
|
|
163
|
+
out += `\\u${code.toString(16).padStart(4, '0')}`;
|
|
164
|
+
changed = true;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// fallthrough: normal character inside string
|
|
169
|
+
out += ch;
|
|
170
|
+
escaped = false;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (ch === '"') {
|
|
174
|
+
inString = true;
|
|
175
|
+
out += ch;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
out += ch;
|
|
179
|
+
}
|
|
180
|
+
return changed ? out : null;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function tryParseJsonWithModelRepairs(raw) {
|
|
187
|
+
try {
|
|
188
|
+
if (typeof raw !== 'string')
|
|
189
|
+
return null;
|
|
190
|
+
const text = raw.trim();
|
|
191
|
+
if (!text)
|
|
192
|
+
return null;
|
|
193
|
+
try {
|
|
194
|
+
return JSON.parse(text);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
const repaired = escapeControlCharsInsideJsonStrings(text);
|
|
198
|
+
if (!repaired)
|
|
199
|
+
return null;
|
|
200
|
+
try {
|
|
201
|
+
return JSON.parse(repaired);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function salvageToolArgsFromRawText(toolName, rawArgs) {
|
|
213
|
+
try {
|
|
214
|
+
const lname = String(toolName || '').toLowerCase();
|
|
215
|
+
const text = String(rawArgs || '');
|
|
216
|
+
const out = {};
|
|
217
|
+
const pickString = (re) => {
|
|
218
|
+
const m = text.match(re);
|
|
219
|
+
if (!m)
|
|
220
|
+
return undefined;
|
|
221
|
+
return String(m[1] ?? '');
|
|
222
|
+
};
|
|
223
|
+
const pickNumber = (re) => {
|
|
224
|
+
const m = text.match(re);
|
|
225
|
+
if (!m)
|
|
226
|
+
return undefined;
|
|
227
|
+
const n = Number.parseInt(String(m[1] ?? ''), 10);
|
|
228
|
+
return Number.isFinite(n) ? n : undefined;
|
|
229
|
+
};
|
|
230
|
+
if (lname === 'exec_command') {
|
|
231
|
+
const cmd = pickString(/"(?:cmd|command)"\s*:\s*"([\s\S]*?)"\s*(?:,|})/i);
|
|
232
|
+
const workdir = pickString(/"(?:workdir|cwd)"\s*:\s*"([\s\S]*?)"\s*(?:,|})/i);
|
|
233
|
+
const timeout = pickNumber(/"(?:timeout_ms|timeout)"\s*:\s*(\d+)\s*(?:,|})/i);
|
|
234
|
+
if (cmd !== undefined)
|
|
235
|
+
out.cmd = cmd;
|
|
236
|
+
if (cmd !== undefined)
|
|
237
|
+
out.command = cmd;
|
|
238
|
+
if (workdir !== undefined)
|
|
239
|
+
out.workdir = workdir;
|
|
240
|
+
if (timeout !== undefined)
|
|
241
|
+
out.timeout_ms = timeout;
|
|
242
|
+
return Object.keys(out).length ? out : null;
|
|
243
|
+
}
|
|
244
|
+
if (lname === 'write_stdin') {
|
|
245
|
+
const sessionId = pickNumber(/"(?:session_id|sessionId)"\s*:\s*(\d+)\s*(?:,|})/i);
|
|
246
|
+
const chars = pickString(/"(?:chars|text|input|data)"\s*:\s*"([\s\S]*?)"\s*(?:,|})/i);
|
|
247
|
+
if (sessionId !== undefined)
|
|
248
|
+
out.session_id = sessionId;
|
|
249
|
+
if (chars !== undefined)
|
|
250
|
+
out.chars = chars;
|
|
251
|
+
return Object.keys(out).length ? out : null;
|
|
252
|
+
}
|
|
253
|
+
if (lname === 'apply_patch') {
|
|
254
|
+
const patch = pickString(/"(?:patch|text|input|instructions)"\s*:\s*"([\s\S]*?)"\s*(?:,|})/i);
|
|
255
|
+
if (patch !== undefined)
|
|
256
|
+
out.patch = patch;
|
|
257
|
+
return Object.keys(out).length ? out : null;
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
120
265
|
// 已移除所有 rcc.tool.v1 相关处理:不再识别或剥离 rcc 封装
|
|
121
266
|
// Extract <tool:NAME>...</tool:NAME> blocks used by some tool-mode outputs.
|
|
122
267
|
// Example:
|
|
@@ -1054,6 +1199,128 @@ export function extractSimpleXmlToolsFromText(text) {
|
|
|
1054
1199
|
return null;
|
|
1055
1200
|
}
|
|
1056
1201
|
}
|
|
1202
|
+
// Extract Qwen-style tool call tokens that appear in plain text, e.g.:
|
|
1203
|
+
// <|tool_calls_section_begin|>
|
|
1204
|
+
// <|tool_call_begin|> functions.exec_command:66 <|tool_call_argument_begin|> {"cmd":"pwd"} <|tool_call_end|>
|
|
1205
|
+
// <|tool_calls_section_end|>
|
|
1206
|
+
// Some upstreams emit this even when OpenAI tool_calls are supported; we harvest it into tool_calls.
|
|
1207
|
+
export function extractQwenToolCallTokensFromText(text) {
|
|
1208
|
+
try {
|
|
1209
|
+
if (typeof text !== 'string' || !text)
|
|
1210
|
+
return null;
|
|
1211
|
+
// Upstreams (notably Qwen-family models behind OpenAI-compatible shells) may insert
|
|
1212
|
+
// whitespace/newlines inside token markers, e.g. "<| tool_call_begin|>".
|
|
1213
|
+
// Avoid brittle substring checks; do a cheap regex presence test instead.
|
|
1214
|
+
if (!/<\|\s*tool_call_begin\s*\|>/i.test(text) && !/<\|\s*tool_call_argument_begin\s*\|>/i.test(text)) {
|
|
1215
|
+
return null;
|
|
1216
|
+
}
|
|
1217
|
+
const out = [];
|
|
1218
|
+
// tolerate whitespace/newlines and missing argument_end token
|
|
1219
|
+
const re = /<\|\s*tool_call_begin\s*\|>\s*([A-Za-z0-9_.-]+)(?::(\d+))?\s*(?:<\|\s*tool_call_argument_begin\s*\|>\s*([\s\S]*?)\s*)?<\|\s*tool_call_end\s*\|>/gi;
|
|
1220
|
+
let m;
|
|
1221
|
+
while ((m = re.exec(text)) !== null) {
|
|
1222
|
+
const rawName = String(m[1] || '').trim();
|
|
1223
|
+
const numericId = typeof m[2] === 'string' && m[2].trim().length ? m[2].trim() : undefined;
|
|
1224
|
+
const rawArgs = typeof m[3] === 'string' ? String(m[3]).trim() : '';
|
|
1225
|
+
if (!rawName)
|
|
1226
|
+
continue;
|
|
1227
|
+
let name = rawName;
|
|
1228
|
+
if (name.startsWith('functions.')) {
|
|
1229
|
+
name = name.slice('functions.'.length);
|
|
1230
|
+
}
|
|
1231
|
+
const lname = name.toLowerCase();
|
|
1232
|
+
if (!lname)
|
|
1233
|
+
continue;
|
|
1234
|
+
let parsedArgs = null;
|
|
1235
|
+
if (!rawArgs) {
|
|
1236
|
+
parsedArgs = {};
|
|
1237
|
+
}
|
|
1238
|
+
else {
|
|
1239
|
+
parsedArgs = tryParseJsonWithModelRepairs(rawArgs);
|
|
1240
|
+
if (parsedArgs === null) {
|
|
1241
|
+
parsedArgs = salvageToolArgsFromRawText(lname, rawArgs) ?? { _raw: rawArgs };
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
const argsObj = parsedArgs && typeof parsedArgs === 'object' && !Array.isArray(parsedArgs)
|
|
1245
|
+
? parsedArgs
|
|
1246
|
+
: { value: parsedArgs };
|
|
1247
|
+
// Alias normalization (models often use "command" instead of "cmd", etc.)
|
|
1248
|
+
if (lname === 'exec_command') {
|
|
1249
|
+
const cmd = typeof argsObj.cmd === 'string' ? String(argsObj.cmd) : '';
|
|
1250
|
+
const command = typeof argsObj.command === 'string' ? String(argsObj.command) : '';
|
|
1251
|
+
if (!cmd && command) {
|
|
1252
|
+
argsObj.cmd = command;
|
|
1253
|
+
}
|
|
1254
|
+
const cwd = typeof argsObj.cwd === 'string' ? String(argsObj.cwd) : '';
|
|
1255
|
+
const workdir = typeof argsObj.workdir === 'string' ? String(argsObj.workdir) : '';
|
|
1256
|
+
if (!workdir && cwd) {
|
|
1257
|
+
argsObj.workdir = cwd;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
if (lname === 'write_stdin') {
|
|
1261
|
+
const chars = typeof argsObj.chars === 'string' ? String(argsObj.chars) : '';
|
|
1262
|
+
const textVal = typeof argsObj.text === 'string' ? String(argsObj.text) : '';
|
|
1263
|
+
const input = typeof argsObj.input === 'string' ? String(argsObj.input) : '';
|
|
1264
|
+
if (!chars && (textVal || input)) {
|
|
1265
|
+
argsObj.chars = textVal || input;
|
|
1266
|
+
}
|
|
1267
|
+
const sid = argsObj.session_id;
|
|
1268
|
+
const sidAlt = argsObj.sessionId;
|
|
1269
|
+
if ((sid === undefined || sid === null || String(sid).trim() === '') && (sidAlt !== undefined && sidAlt !== null)) {
|
|
1270
|
+
argsObj.session_id = sidAlt;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
if (lname === 'apply_patch') {
|
|
1274
|
+
const patch = typeof argsObj.patch === 'string' ? String(argsObj.patch) : '';
|
|
1275
|
+
const textPatch = typeof argsObj.text === 'string' ? String(argsObj.text) : '';
|
|
1276
|
+
const inputPatch = typeof argsObj.input === 'string' ? String(argsObj.input) : '';
|
|
1277
|
+
const instructions = typeof argsObj.instructions === 'string' ? String(argsObj.instructions) : '';
|
|
1278
|
+
if (!patch && (textPatch || inputPatch || instructions)) {
|
|
1279
|
+
argsObj.patch = textPatch || inputPatch || instructions;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
const filtered = filterArgsForTool(lname, argsObj);
|
|
1283
|
+
// Basic guards for known tools
|
|
1284
|
+
if (lname === 'exec_command') {
|
|
1285
|
+
const cmd = typeof filtered?.cmd === 'string' ? String(filtered.cmd).trim() : '';
|
|
1286
|
+
if (!cmd)
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
if (lname === 'write_stdin') {
|
|
1290
|
+
// Allow empty args: our servertool schema may inject session_id automatically when a session is active.
|
|
1291
|
+
// Some models emit polling writes as `{}`.
|
|
1292
|
+
}
|
|
1293
|
+
if (lname === 'apply_patch') {
|
|
1294
|
+
const patchText = typeof filtered?.patch === 'string' ? String(filtered.patch).trim() : '';
|
|
1295
|
+
if (!patchText)
|
|
1296
|
+
continue;
|
|
1297
|
+
}
|
|
1298
|
+
let argsStr = '{}';
|
|
1299
|
+
try {
|
|
1300
|
+
argsStr = JSON.stringify(filtered);
|
|
1301
|
+
}
|
|
1302
|
+
catch {
|
|
1303
|
+
argsStr = '{}';
|
|
1304
|
+
}
|
|
1305
|
+
const stableId = (() => {
|
|
1306
|
+
if (numericId && /^[0-9]{1,10}$/.test(numericId)) {
|
|
1307
|
+
const namePart = lname.replace(/[^A-Za-z0-9_-]/g, '_');
|
|
1308
|
+
const id = `call_${namePart}_${numericId}`
|
|
1309
|
+
.replace(/_{2,}/g, '_')
|
|
1310
|
+
.replace(/^_+/, '')
|
|
1311
|
+
.replace(/_+$/, '');
|
|
1312
|
+
return id || undefined;
|
|
1313
|
+
}
|
|
1314
|
+
return undefined;
|
|
1315
|
+
})();
|
|
1316
|
+
out.push({ id: stableId ?? `call_${Math.random().toString(36).slice(2, 10)}`, name: lname, args: argsStr });
|
|
1317
|
+
}
|
|
1318
|
+
return out.length ? out : null;
|
|
1319
|
+
}
|
|
1320
|
+
catch {
|
|
1321
|
+
return null;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1057
1324
|
export function normalizeAssistantTextToToolCalls(message) {
|
|
1058
1325
|
if (!enabled())
|
|
1059
1326
|
return message;
|
|
@@ -1094,7 +1361,8 @@ export function normalizeAssistantTextToToolCalls(message) {
|
|
|
1094
1361
|
for (const text of candidates) {
|
|
1095
1362
|
// Order: xml-like <tool_call> → apply_patch → execute blocks → 简单 XML 工具(list_directory 等) → bare command salvage
|
|
1096
1363
|
calls =
|
|
1097
|
-
|
|
1364
|
+
extractQwenToolCallTokensFromText(text) ||
|
|
1365
|
+
extractToolNamespaceXmlBlocksFromText(text) ||
|
|
1098
1366
|
extractParameterXmlToolsFromText(text) ||
|
|
1099
1367
|
extractInvokeToolsFromText(text) ||
|
|
1100
1368
|
extractXMLToolCallsFromText(text) ||
|
|
@@ -515,7 +515,13 @@ function normalizeProvider(providerId, raw) {
|
|
|
515
515
|
const compatibilityProfile = resolveCompatibilityProfile(providerId, provider);
|
|
516
516
|
const headers = maybeInjectClaudeCodeHeaders(providerId, providerType, compatibilityProfile, normalizeHeaders(provider.headers));
|
|
517
517
|
const responsesNode = asRecord(provider.responses);
|
|
518
|
-
const responsesConfig = normalizeResponsesConfig(
|
|
518
|
+
const responsesConfig = normalizeResponsesConfig({
|
|
519
|
+
providerId,
|
|
520
|
+
providerType,
|
|
521
|
+
compatibilityProfile,
|
|
522
|
+
provider,
|
|
523
|
+
node: responsesNode
|
|
524
|
+
});
|
|
519
525
|
const processMode = normalizeProcessMode(provider.process);
|
|
520
526
|
const streaming = resolveProviderStreamingPreference(provider, responsesNode);
|
|
521
527
|
const modelStreaming = normalizeModelStreaming(provider);
|
|
@@ -660,16 +666,23 @@ function coerceStreamingPreference(value) {
|
|
|
660
666
|
}
|
|
661
667
|
return undefined;
|
|
662
668
|
}
|
|
663
|
-
function normalizeResponsesConfig(
|
|
664
|
-
const source = node ?? asRecord(provider.responses);
|
|
665
|
-
if (!source) {
|
|
666
|
-
return undefined;
|
|
667
|
-
}
|
|
669
|
+
function normalizeResponsesConfig(options) {
|
|
670
|
+
const source = options.node ?? asRecord(options.provider.responses);
|
|
668
671
|
const rawStyle = typeof source.toolCallIdStyle === 'string' ? source.toolCallIdStyle.trim().toLowerCase() : undefined;
|
|
669
672
|
if (rawStyle === 'fc' || rawStyle === 'preserve') {
|
|
670
673
|
return { toolCallIdStyle: rawStyle };
|
|
671
674
|
}
|
|
672
|
-
|
|
675
|
+
const providerType = typeof options.providerType === 'string' ? options.providerType.trim().toLowerCase() : '';
|
|
676
|
+
if (!providerType.includes('responses')) {
|
|
677
|
+
return undefined;
|
|
678
|
+
}
|
|
679
|
+
const providerId = typeof options.providerId === 'string' ? options.providerId.trim().toLowerCase() : '';
|
|
680
|
+
const compat = typeof options.compatibilityProfile === 'string' ? options.compatibilityProfile.trim().toLowerCase() : '';
|
|
681
|
+
// Default tool-call id style:
|
|
682
|
+
// - Standard OpenAI /v1/responses requires function_call ids to start with "fc_".
|
|
683
|
+
// - LM Studio (OpenAI-compatible) often emits `call_*` ids and expects them to be preserved.
|
|
684
|
+
const isLmstudio = providerId === 'lmstudio' || compat === 'chat:lmstudio';
|
|
685
|
+
return { toolCallIdStyle: isLmstudio ? 'preserve' : 'fc' };
|
|
673
686
|
}
|
|
674
687
|
function resolveProviderStreamingPreference(provider, responsesNode) {
|
|
675
688
|
const configNode = asRecord(provider.config);
|
|
@@ -1046,6 +1059,17 @@ function extractProviderAuthEntries(providerId, raw) {
|
|
|
1046
1059
|
pushEntry(undefined, buildAuthCandidate(baseTypeSource, fallbackExtras));
|
|
1047
1060
|
}
|
|
1048
1061
|
}
|
|
1062
|
+
// Allow explicit apiKey auth with empty value (local no-auth providers like LM Studio).
|
|
1063
|
+
// If the config declares an auth node (even with an empty apiKey), treat it as an intentional no-auth setup.
|
|
1064
|
+
if (!entries.length && baseType === 'apiKey') {
|
|
1065
|
+
const authDeclared = Object.prototype.hasOwnProperty.call(provider, 'auth') ||
|
|
1066
|
+
Object.prototype.hasOwnProperty.call(provider, 'apiKey') ||
|
|
1067
|
+
Object.prototype.hasOwnProperty.call(provider, 'apiKeys') ||
|
|
1068
|
+
Object.prototype.hasOwnProperty.call(provider, 'authType');
|
|
1069
|
+
if (authDeclared) {
|
|
1070
|
+
pushEntry(undefined, buildAuthCandidate(baseTypeSource, { value: '' }));
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1049
1073
|
if (!entries.length) {
|
|
1050
1074
|
throw new VirtualRouterError(`Provider ${providerId} is missing auth configuration`, VirtualRouterErrorCode.CONFIG_ERROR);
|
|
1051
1075
|
}
|