@jsonstudio/llms 0.6.1892 → 0.6.2172
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/deepseek-web-request.js +16 -2
- package/dist/conversion/compat/actions/deepseek-web-response.d.ts +7 -1
- package/dist/conversion/compat/actions/deepseek-web-response.js +302 -40
- package/dist/conversion/compat/actions/harvest-tool-calls-from-text.d.ts +5 -0
- package/dist/conversion/compat/actions/harvest-tool-calls-from-text.js +7 -4
- package/dist/conversion/compat/actions/iflow-tool-text-fallback.d.ts +1 -0
- package/dist/conversion/compat/actions/iflow-tool-text-fallback.js +12 -0
- package/dist/conversion/compat/actions/strip-orphan-function-calls-tag.js +1 -1
- package/dist/conversion/compat/actions/tool-text-request-guidance.d.ts +9 -0
- package/dist/conversion/compat/actions/tool-text-request-guidance.js +177 -0
- package/dist/conversion/compat/antigravity-session-signature.d.ts +6 -0
- package/dist/conversion/compat/antigravity-session-signature.js +15 -0
- package/dist/conversion/compat/profiles/chat-deepseek-web.json +52 -1
- package/dist/conversion/compat/profiles/chat-glm.json +22 -0
- package/dist/conversion/compat/profiles/chat-iflow.json +4 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +13 -27
- package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +10 -1
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +13 -4
- package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.js +1 -53
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +8 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +8 -4
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +191 -9
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +118 -15
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +65 -2
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage3_servertool_orchestration/index.d.ts +34 -0
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage3_servertool_orchestration/index.js +75 -0
- package/dist/conversion/hub/process/chat-process.js +85 -18
- package/dist/conversion/hub/response/provider-response.js +21 -50
- package/dist/conversion/hub/response/response-runtime.js +71 -10
- package/dist/conversion/responses/responses-openai-bridge/response-payload.d.ts +3 -0
- package/dist/conversion/responses/responses-openai-bridge/response-payload.js +576 -0
- package/dist/conversion/responses/responses-openai-bridge/types.d.ts +42 -0
- package/dist/conversion/responses/responses-openai-bridge/types.js +1 -0
- package/dist/conversion/responses/responses-openai-bridge.d.ts +3 -44
- package/dist/conversion/responses/responses-openai-bridge.js +193 -504
- package/dist/conversion/shared/anthropic-message-utils.js +82 -2
- package/dist/conversion/shared/bridge-message-utils.js +92 -39
- package/dist/conversion/shared/snapshot-hooks.js +8 -13
- package/dist/conversion/shared/text-markup-normalizer/extractors-apply-patch.d.ts +2 -0
- package/dist/conversion/shared/text-markup-normalizer/extractors-apply-patch.js +129 -0
- package/dist/conversion/shared/text-markup-normalizer/extractors-json.d.ts +4 -0
- package/dist/conversion/shared/text-markup-normalizer/extractors-json.js +637 -0
- package/dist/conversion/shared/text-markup-normalizer/extractors-shared.d.ts +21 -0
- package/dist/conversion/shared/text-markup-normalizer/extractors-shared.js +177 -0
- package/dist/conversion/shared/text-markup-normalizer/extractors-transcript.d.ts +5 -0
- package/dist/conversion/shared/text-markup-normalizer/extractors-transcript.js +385 -0
- package/dist/conversion/shared/text-markup-normalizer/extractors-xml.d.ts +10 -0
- package/dist/conversion/shared/text-markup-normalizer/extractors-xml.js +602 -0
- package/dist/conversion/shared/text-markup-normalizer/extractors.d.ts +5 -0
- package/dist/conversion/shared/text-markup-normalizer/extractors.js +4 -0
- package/dist/conversion/shared/text-markup-normalizer/normalize.d.ts +2 -0
- package/dist/conversion/shared/text-markup-normalizer/normalize.js +76 -0
- package/dist/conversion/shared/text-markup-normalizer.d.ts +3 -25
- package/dist/conversion/shared/text-markup-normalizer.js +2 -1386
- package/dist/conversion/shared/tool-governor.js +136 -10
- package/dist/filters/utils/snapshot-writer.js +3 -3
- package/dist/router/virtual-router/bootstrap/auth-utils.d.ts +6 -0
- package/dist/router/virtual-router/bootstrap/auth-utils.js +288 -0
- package/dist/router/virtual-router/bootstrap/claude-code-helpers.d.ts +11 -0
- package/dist/router/virtual-router/bootstrap/claude-code-helpers.js +18 -0
- package/dist/router/virtual-router/bootstrap/config-defaults.d.ts +5 -0
- package/dist/router/virtual-router/bootstrap/config-defaults.js +13 -0
- package/dist/router/virtual-router/bootstrap/config-normalizers.d.ts +4 -0
- package/dist/router/virtual-router/bootstrap/config-normalizers.js +106 -0
- package/dist/router/virtual-router/bootstrap/profile-builder.d.ts +7 -0
- package/dist/router/virtual-router/bootstrap/profile-builder.js +68 -0
- package/dist/router/virtual-router/bootstrap/provider-normalization.d.ts +40 -0
- package/dist/router/virtual-router/bootstrap/provider-normalization.js +212 -0
- package/dist/router/virtual-router/bootstrap/responses-helpers.d.ts +15 -0
- package/dist/router/virtual-router/bootstrap/responses-helpers.js +65 -0
- package/dist/router/virtual-router/bootstrap/routing-config.d.ts +23 -0
- package/dist/router/virtual-router/bootstrap/routing-config.js +293 -0
- package/dist/router/virtual-router/bootstrap/streaming-helpers.d.ts +12 -0
- package/dist/router/virtual-router/bootstrap/streaming-helpers.js +128 -0
- package/dist/router/virtual-router/bootstrap/utils.d.ts +5 -0
- package/dist/router/virtual-router/bootstrap/utils.js +41 -0
- package/dist/router/virtual-router/bootstrap/web-search-config.d.ts +4 -0
- package/dist/router/virtual-router/bootstrap/web-search-config.js +131 -0
- package/dist/router/virtual-router/bootstrap.d.ts +0 -4
- package/dist/router/virtual-router/bootstrap.js +31 -1275
- package/dist/router/virtual-router/classifier.js +32 -14
- package/dist/router/virtual-router/engine/antigravity/alias-lease.js +2 -2
- package/dist/router/virtual-router/engine/cooldown-manager.d.ts +34 -0
- package/dist/router/virtual-router/engine/cooldown-manager.js +118 -0
- package/dist/router/virtual-router/engine/route-analytics.d.ts +28 -0
- package/dist/router/virtual-router/engine/route-analytics.js +44 -0
- package/dist/router/virtual-router/engine/routing-pools/index.js +165 -4
- package/dist/router/virtual-router/engine/sticky-session-manager.d.ts +29 -0
- package/dist/router/virtual-router/engine/sticky-session-manager.js +55 -0
- package/dist/router/virtual-router/engine-logging.d.ts +42 -1
- package/dist/router/virtual-router/engine-logging.js +82 -15
- package/dist/router/virtual-router/engine-selection/multimodal-capability.d.ts +3 -0
- package/dist/router/virtual-router/engine-selection/multimodal-capability.js +26 -0
- package/dist/router/virtual-router/engine-selection/route-utils.js +6 -2
- package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +1 -0
- package/dist/router/virtual-router/engine-selection/tier-selection.js +31 -1
- package/dist/router/virtual-router/engine.d.ts +21 -7
- package/dist/router/virtual-router/engine.js +198 -194
- package/dist/router/virtual-router/features.js +12 -4
- package/dist/router/virtual-router/message-utils.d.ts +8 -0
- package/dist/router/virtual-router/message-utils.js +170 -45
- package/dist/router/virtual-router/pre-command-file-resolver.js +40 -2
- package/dist/router/virtual-router/routing-instructions.d.ts +8 -0
- package/dist/router/virtual-router/routing-instructions.js +18 -2
- package/dist/router/virtual-router/routing-stop-message-actions.js +34 -10
- package/dist/router/virtual-router/routing-stop-message-state-codec.d.ts +2 -0
- package/dist/router/virtual-router/routing-stop-message-state-codec.js +50 -1
- package/dist/router/virtual-router/stop-message-state-sync.d.ts +1 -1
- package/dist/router/virtual-router/stop-message-state-sync.js +3 -0
- package/dist/router/virtual-router/token-counter.js +51 -10
- package/dist/router/virtual-router/tool-signals.js +4 -0
- package/dist/router/virtual-router/types.d.ts +15 -0
- package/dist/servertool/clock/session-scope.d.ts +3 -0
- package/dist/servertool/clock/session-scope.js +52 -0
- package/dist/servertool/clock/state.js +9 -0
- package/dist/servertool/clock/tasks.js +12 -1
- package/dist/servertool/clock/types.d.ts +3 -0
- package/dist/servertool/engine.js +177 -31
- package/dist/servertool/handlers/clock-auto.js +2 -8
- package/dist/servertool/handlers/clock.js +6 -9
- package/dist/servertool/handlers/recursive-detection-guard.js +53 -14
- package/dist/servertool/handlers/stop-message-auto/blocked-report.d.ts +16 -0
- package/dist/servertool/handlers/stop-message-auto/blocked-report.js +349 -0
- package/dist/servertool/handlers/stop-message-auto/iflow-followup.d.ts +23 -0
- package/dist/servertool/handlers/stop-message-auto/iflow-followup.js +503 -0
- package/dist/servertool/handlers/stop-message-auto/routing-state.d.ts +38 -0
- package/dist/servertool/handlers/stop-message-auto/routing-state.js +149 -0
- package/dist/servertool/handlers/stop-message-auto/runtime-utils.d.ts +67 -0
- package/dist/servertool/handlers/stop-message-auto/runtime-utils.js +387 -0
- package/dist/servertool/handlers/stop-message-auto.d.ts +1 -1
- package/dist/servertool/handlers/stop-message-auto.js +80 -556
- package/dist/servertool/handlers/stop-message-stage-policy/bd-runtime.d.ts +18 -0
- package/dist/servertool/handlers/stop-message-stage-policy/bd-runtime.js +398 -0
- package/dist/servertool/handlers/stop-message-stage-policy/decision.d.ts +9 -0
- package/dist/servertool/handlers/stop-message-stage-policy/decision.js +127 -0
- package/dist/servertool/handlers/stop-message-stage-policy/observation.d.ts +2 -0
- package/dist/servertool/handlers/stop-message-stage-policy/observation.js +179 -0
- package/dist/servertool/handlers/stop-message-stage-policy/templates.d.ts +4 -0
- package/dist/servertool/handlers/stop-message-stage-policy/templates.js +96 -0
- package/dist/servertool/handlers/stop-message-stage-policy/text-utils.d.ts +9 -0
- package/dist/servertool/handlers/stop-message-stage-policy/text-utils.js +89 -0
- package/dist/servertool/handlers/stop-message-stage-policy/types.d.ts +59 -0
- package/dist/servertool/handlers/stop-message-stage-policy/types.js +1 -0
- package/dist/servertool/handlers/stop-message-stage-policy.d.ts +3 -43
- package/dist/servertool/handlers/stop-message-stage-policy.js +2 -684
- package/dist/servertool/handlers/web-search.js +117 -0
- package/dist/servertool/server-side-tools.d.ts +0 -1
- package/dist/servertool/server-side-tools.js +4 -3
- package/dist/sse/sse-to-json/builders/response-builder.js +16 -0
- package/dist/sse/sse-to-json/chat-sse-to-json-converter.d.ts +1 -0
- package/dist/sse/sse-to-json/chat-sse-to-json-converter.js +110 -37
- package/dist/telemetry/stats-center.d.ts +9 -0
- package/dist/telemetry/stats-center.js +29 -1
- package/dist/tools/apply-patch/structured/coercion.js +3 -11
- package/dist/tools/exec-command/validator.d.ts +1 -0
- package/dist/tools/exec-command/validator.js +132 -0
- package/dist/tools/tool-registry.d.ts +1 -0
- package/dist/tools/tool-registry.js +1 -1
- package/package.json +1 -1
|
@@ -217,6 +217,10 @@ function isIflowWebSearchEngine(engine) {
|
|
|
217
217
|
const key = engine.providerKey.toLowerCase();
|
|
218
218
|
return key.startsWith('iflow.');
|
|
219
219
|
}
|
|
220
|
+
function isQwenWebSearchEngine(engine) {
|
|
221
|
+
const key = engine.providerKey.toLowerCase();
|
|
222
|
+
return key.startsWith('qwen.');
|
|
223
|
+
}
|
|
220
224
|
function normalizeResultCount(value) {
|
|
221
225
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
222
226
|
const normalized = Math.trunc(value);
|
|
@@ -250,6 +254,18 @@ async function executeWebSearchBackend(args) {
|
|
|
250
254
|
hits = backendResult.hits;
|
|
251
255
|
ok = backendResult.ok;
|
|
252
256
|
}
|
|
257
|
+
else if (isQwenWebSearchEngine(engine) && options.providerInvoker) {
|
|
258
|
+
const backendResult = await executeQwenWebSearchViaProvider({
|
|
259
|
+
options,
|
|
260
|
+
engine,
|
|
261
|
+
query,
|
|
262
|
+
count: args.resultCount,
|
|
263
|
+
requestSuffix
|
|
264
|
+
});
|
|
265
|
+
summary = backendResult.summary;
|
|
266
|
+
hits = backendResult.hits;
|
|
267
|
+
ok = backendResult.ok;
|
|
268
|
+
}
|
|
253
269
|
else if (options.reenterPipeline) {
|
|
254
270
|
const payload = buildWebSearchReenterPayload(engine, query, recency, args.resultCount);
|
|
255
271
|
const followup = await reenterServerToolBackend({
|
|
@@ -586,6 +602,107 @@ async function executeIflowWebSearchViaProvider(args) {
|
|
|
586
602
|
ok
|
|
587
603
|
};
|
|
588
604
|
}
|
|
605
|
+
async function executeQwenWebSearchViaProvider(args) {
|
|
606
|
+
const { options, engine, query, count, requestSuffix } = args;
|
|
607
|
+
if (!options.providerInvoker) {
|
|
608
|
+
return {
|
|
609
|
+
summary: '',
|
|
610
|
+
hits: [],
|
|
611
|
+
ok: false
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
const payload = {
|
|
615
|
+
data: {
|
|
616
|
+
model: engine.id,
|
|
617
|
+
uq: query,
|
|
618
|
+
page: 1,
|
|
619
|
+
rows: count
|
|
620
|
+
},
|
|
621
|
+
metadata: {
|
|
622
|
+
entryEndpoint: '/api/v1/indices/plugin/web_search',
|
|
623
|
+
qwenWebSearch: true,
|
|
624
|
+
routeName: 'web_search'
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
const backend = await options.providerInvoker({
|
|
628
|
+
providerKey: engine.providerKey,
|
|
629
|
+
providerType: undefined,
|
|
630
|
+
modelId: undefined,
|
|
631
|
+
providerProtocol: options.providerProtocol,
|
|
632
|
+
payload,
|
|
633
|
+
entryEndpoint: '/api/v1/indices/plugin/web_search',
|
|
634
|
+
requestId: `${options.requestId}${requestSuffix}`,
|
|
635
|
+
routeHint: 'web_search'
|
|
636
|
+
});
|
|
637
|
+
const providerResponse = backend.providerResponse && typeof backend.providerResponse === 'object'
|
|
638
|
+
? backend.providerResponse
|
|
639
|
+
: null;
|
|
640
|
+
if (!providerResponse) {
|
|
641
|
+
return {
|
|
642
|
+
summary: '',
|
|
643
|
+
hits: [],
|
|
644
|
+
ok: false
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
const container = providerResponse;
|
|
648
|
+
const status = typeof container.status === 'number' ? container.status : undefined;
|
|
649
|
+
const message = typeof container.message === 'string' && container.message.trim()
|
|
650
|
+
? container.message.trim()
|
|
651
|
+
: typeof container.msg === 'string' && container.msg.trim()
|
|
652
|
+
? container.msg.trim()
|
|
653
|
+
: '';
|
|
654
|
+
if (status !== undefined && status !== 0) {
|
|
655
|
+
throw new Error(message || `qwen web_search failed with status=${status}`);
|
|
656
|
+
}
|
|
657
|
+
const dataNode = container.data && typeof container.data === 'object' && !Array.isArray(container.data)
|
|
658
|
+
? container.data
|
|
659
|
+
: undefined;
|
|
660
|
+
const rawDocs = Array.isArray(dataNode?.docs) ? dataNode?.docs : [];
|
|
661
|
+
const hits = [];
|
|
662
|
+
for (const item of rawDocs) {
|
|
663
|
+
if (!item || typeof item !== 'object' || Array.isArray(item))
|
|
664
|
+
continue;
|
|
665
|
+
const record = item;
|
|
666
|
+
const linkCandidate = typeof record.url === 'string' && record.url.trim()
|
|
667
|
+
? record.url.trim()
|
|
668
|
+
: typeof record.link === 'string' && record.link.trim()
|
|
669
|
+
? record.link.trim()
|
|
670
|
+
: '';
|
|
671
|
+
if (!linkCandidate)
|
|
672
|
+
continue;
|
|
673
|
+
const title = typeof record.title === 'string' && record.title.trim() ? record.title.trim() : undefined;
|
|
674
|
+
const content = typeof record.snippet === 'string' && record.snippet.trim()
|
|
675
|
+
? record.snippet.trim()
|
|
676
|
+
: typeof record.content === 'string' && record.content.trim()
|
|
677
|
+
? record.content.trim()
|
|
678
|
+
: undefined;
|
|
679
|
+
const publishDate = typeof record.timestamp_format === 'string' && record.timestamp_format.trim()
|
|
680
|
+
? record.timestamp_format.trim()
|
|
681
|
+
: typeof record.timestamp === 'string' && record.timestamp.trim()
|
|
682
|
+
? record.timestamp.trim()
|
|
683
|
+
: typeof record.time === 'string' && record.time.trim()
|
|
684
|
+
? record.time.trim()
|
|
685
|
+
: undefined;
|
|
686
|
+
const media = typeof record.source === 'string' && record.source.trim() ? record.source.trim() : undefined;
|
|
687
|
+
hits.push({
|
|
688
|
+
title,
|
|
689
|
+
link: linkCandidate,
|
|
690
|
+
content,
|
|
691
|
+
publish_date: publishDate,
|
|
692
|
+
media
|
|
693
|
+
});
|
|
694
|
+
if (hits.length >= count) {
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
const summary = message || (hits.length ? formatHitsSummary(hits) : '');
|
|
699
|
+
const ok = status === 0 || hits.length > 0;
|
|
700
|
+
return {
|
|
701
|
+
summary,
|
|
702
|
+
hits,
|
|
703
|
+
ok
|
|
704
|
+
};
|
|
705
|
+
}
|
|
589
706
|
function injectWebSearchToolResult(base, toolCall, engine, query, backendResult) {
|
|
590
707
|
const cloned = cloneJson(base);
|
|
591
708
|
const existingOutputs = Array.isArray(cloned.tool_outputs)
|
|
@@ -8,7 +8,6 @@ import './handlers/clock.js';
|
|
|
8
8
|
import './handlers/clock-auto.js';
|
|
9
9
|
import './handlers/exec-command-guard.js';
|
|
10
10
|
import './handlers/apply-patch-guard.js';
|
|
11
|
-
import './handlers/recursive-detection-guard.js';
|
|
12
11
|
import './handlers/continue-execution.js';
|
|
13
12
|
export declare function runServerSideToolEngine(options: ServerSideToolEngineOptions): Promise<ServerSideToolEngineResult>;
|
|
14
13
|
export declare function extractToolCalls(chatResponse: JsonObject): ToolCall[];
|
|
@@ -10,7 +10,6 @@ import './handlers/clock.js';
|
|
|
10
10
|
import './handlers/clock-auto.js';
|
|
11
11
|
import './handlers/exec-command-guard.js';
|
|
12
12
|
import './handlers/apply-patch-guard.js';
|
|
13
|
-
import './handlers/recursive-detection-guard.js';
|
|
14
13
|
import './handlers/continue-execution.js';
|
|
15
14
|
import { runPreCommandHooks } from './pre-command-hooks.js';
|
|
16
15
|
import { readRuntimeMetadata } from '../conversion/shared/runtime-metadata.js';
|
|
@@ -242,10 +241,12 @@ export async function runServerSideToolEngine(options) {
|
|
|
242
241
|
if (!base) {
|
|
243
242
|
return { mode: 'passthrough', finalChatResponse: options.chatResponse };
|
|
244
243
|
}
|
|
245
|
-
|
|
244
|
+
const toolCalls = extractToolCalls(base);
|
|
245
|
+
if (isClientDisconnected(options.adapterContext) && toolCalls.length > 0) {
|
|
246
|
+
// When client is already disconnected, skip executing explicit tool_call servertools.
|
|
247
|
+
// Auto hooks (e.g. stop_message_auto) still need to run to keep session state consistent.
|
|
246
248
|
return { mode: 'passthrough', finalChatResponse: base };
|
|
247
249
|
}
|
|
248
|
-
const toolCalls = extractToolCalls(base);
|
|
249
250
|
const contextBase = {
|
|
250
251
|
base,
|
|
251
252
|
toolCalls,
|
|
@@ -831,6 +831,22 @@ export class ResponsesResponseBuilder {
|
|
|
831
831
|
}
|
|
832
832
|
return { success: true, response: this.response };
|
|
833
833
|
}
|
|
834
|
+
// Further salvage: some upstreams end the stream without any terminal events
|
|
835
|
+
// (no output_item.done/response.completed/response.done) but still materialize
|
|
836
|
+
// output_item.added + content deltas. Use current aggregated items as completed.
|
|
837
|
+
if (this.outputItemBuilders.size > 0) {
|
|
838
|
+
this.response.status = 'completed';
|
|
839
|
+
try {
|
|
840
|
+
const cur = this.response.output;
|
|
841
|
+
if (!Array.isArray(cur) || cur.length === 0) {
|
|
842
|
+
this.response.output = this.buildOutputItems();
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
catch {
|
|
846
|
+
this.response.output = this.buildOutputItems();
|
|
847
|
+
}
|
|
848
|
+
return { success: true, response: this.response };
|
|
849
|
+
}
|
|
834
850
|
}
|
|
835
851
|
catch { /* ignore */ }
|
|
836
852
|
return { success: false, error: new Error('Building not completed') };
|
|
@@ -110,24 +110,40 @@ export class ChatSseToJsonConverter {
|
|
|
110
110
|
parseSseChunk(chunk) {
|
|
111
111
|
const lines = chunk.trim().split('\n');
|
|
112
112
|
let rawEventType;
|
|
113
|
-
|
|
113
|
+
const dataLines = [];
|
|
114
114
|
for (const line of lines) {
|
|
115
115
|
if (line.startsWith('event:')) {
|
|
116
116
|
rawEventType = line.substring(6).trim();
|
|
117
117
|
}
|
|
118
118
|
else if (line.startsWith('data:')) {
|
|
119
|
-
|
|
119
|
+
dataLines.push(line.substring(5).trim());
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
-
const
|
|
122
|
+
const dataValue = dataLines.join('\n');
|
|
123
|
+
const parsedData = (() => {
|
|
124
|
+
if (!dataValue) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const parsed = JSON.parse(dataValue);
|
|
129
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
130
|
+
return parsed;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// best effort
|
|
135
|
+
}
|
|
136
|
+
return undefined;
|
|
137
|
+
})();
|
|
138
|
+
const normalizeEventType = (candidate, payload) => {
|
|
123
139
|
if (!candidate)
|
|
124
140
|
return undefined;
|
|
125
|
-
const v = candidate.trim();
|
|
141
|
+
const v = candidate.trim().toLowerCase();
|
|
126
142
|
if (!v)
|
|
127
143
|
return undefined;
|
|
128
144
|
// OpenAI Chat Completions SSE does not include `event:` lines; we infer types elsewhere.
|
|
129
145
|
// When upstream does include `event:`, accept common aliases for compatibility.
|
|
130
|
-
if (v === 'chat_chunk'
|
|
146
|
+
if (v === 'chat_chunk')
|
|
131
147
|
return 'chat_chunk';
|
|
132
148
|
if (v === 'chat.done' || v === 'chat_done')
|
|
133
149
|
return 'chat.done';
|
|
@@ -140,6 +156,16 @@ export class ChatSseToJsonConverter {
|
|
|
140
156
|
return 'ping';
|
|
141
157
|
if (v === 'finish' || v === 'close')
|
|
142
158
|
return 'chat.done';
|
|
159
|
+
if (v === 'toast') {
|
|
160
|
+
const toastType = typeof payload?.type === 'string' ? payload.type.trim().toLowerCase() : '';
|
|
161
|
+
const finishReason = typeof payload?.finish_reason === 'string'
|
|
162
|
+
? payload.finish_reason.trim().toLowerCase()
|
|
163
|
+
: '';
|
|
164
|
+
if (toastType === 'error' || finishReason === 'context_length_exceeded' || finishReason === 'rate_limit_exceeded') {
|
|
165
|
+
return 'error';
|
|
166
|
+
}
|
|
167
|
+
return 'ping';
|
|
168
|
+
}
|
|
143
169
|
// Legacy aliases
|
|
144
170
|
if (v === 'chunk')
|
|
145
171
|
return 'chat_chunk';
|
|
@@ -147,7 +173,7 @@ export class ChatSseToJsonConverter {
|
|
|
147
173
|
return 'chat.done';
|
|
148
174
|
return undefined;
|
|
149
175
|
};
|
|
150
|
-
let eventType = normalizeEventType(rawEventType);
|
|
176
|
+
let eventType = normalizeEventType(rawEventType, parsedData);
|
|
151
177
|
if (!eventType) {
|
|
152
178
|
// OpenAI-compatible streams often omit `event:`; use `[DONE]` sentinel to mark completion.
|
|
153
179
|
if (dataValue) {
|
|
@@ -240,27 +266,29 @@ export class ChatSseToJsonConverter {
|
|
|
240
266
|
async processChatChunk(event, context) {
|
|
241
267
|
try {
|
|
242
268
|
const payload = typeof event.data === 'string' ? event.data : JSON.stringify(event.data ?? {});
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
context.currentResponse.id
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
269
|
+
const parsedEntries = this.parseChatChunkPayload(payload);
|
|
270
|
+
for (const parsed of parsedEntries) {
|
|
271
|
+
if (this.tryProcessDeepSeekWebPatchEvent(parsed, context)) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const chunk = parsed;
|
|
275
|
+
// 验证chunk格式
|
|
276
|
+
if (context.options.validateChunks) {
|
|
277
|
+
this.validateChatChunk(chunk);
|
|
278
|
+
}
|
|
279
|
+
context.aggregatedChunks.push(chunk);
|
|
280
|
+
// 初始化响应结构(如果是第一个chunk)
|
|
281
|
+
if (!context.currentResponse.id && chunk.id) {
|
|
282
|
+
context.currentResponse.id = chunk.id;
|
|
283
|
+
context.currentResponse.object = 'chat.completion';
|
|
284
|
+
context.currentResponse.created = chunk.created;
|
|
285
|
+
context.currentResponse.model = chunk.model;
|
|
286
|
+
}
|
|
287
|
+
// 处理choices
|
|
288
|
+
if (chunk.choices && Array.isArray(chunk.choices)) {
|
|
289
|
+
for (const choice of chunk.choices) {
|
|
290
|
+
await this.processChoice(choice, context);
|
|
291
|
+
}
|
|
264
292
|
}
|
|
265
293
|
}
|
|
266
294
|
}
|
|
@@ -268,6 +296,33 @@ export class ChatSseToJsonConverter {
|
|
|
268
296
|
throw ErrorUtils.wrapError(error, 'Failed to parse chat_chunk');
|
|
269
297
|
}
|
|
270
298
|
}
|
|
299
|
+
parseChatChunkPayload(payload) {
|
|
300
|
+
try {
|
|
301
|
+
return [JSON.parse(payload)];
|
|
302
|
+
}
|
|
303
|
+
catch (primaryError) {
|
|
304
|
+
const segments = payload
|
|
305
|
+
.split('\n')
|
|
306
|
+
.map((entry) => entry.trim())
|
|
307
|
+
.filter((entry) => entry.length > 0);
|
|
308
|
+
if (segments.length <= 1) {
|
|
309
|
+
throw primaryError;
|
|
310
|
+
}
|
|
311
|
+
const parsed = [];
|
|
312
|
+
for (const segment of segments) {
|
|
313
|
+
try {
|
|
314
|
+
parsed.push(JSON.parse(segment));
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
// Ignore non-JSON lines so valid partial frames can still be recovered.
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (!parsed.length) {
|
|
321
|
+
throw primaryError;
|
|
322
|
+
}
|
|
323
|
+
return parsed;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
271
326
|
ensureChoiceBuilder(context, choiceIndex) {
|
|
272
327
|
let choiceBuilder = context.choiceIndexMap.get(choiceIndex);
|
|
273
328
|
if (!choiceBuilder) {
|
|
@@ -651,18 +706,36 @@ export class ChatSseToJsonConverter {
|
|
|
651
706
|
* 处理error事件
|
|
652
707
|
*/
|
|
653
708
|
async processErrorEvent(event, _context) {
|
|
709
|
+
const rawPayload = typeof event.data === 'string' ? event.data : JSON.stringify(event.data ?? {});
|
|
710
|
+
let errorData;
|
|
654
711
|
try {
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
const typedError = new Error(errorMessage);
|
|
660
|
-
typedError.code = code;
|
|
661
|
-
throw ErrorUtils.createError(typedError.message, CHAT_CONVERSION_ERROR_CODES.STREAM_ERROR, { errorData, event });
|
|
662
|
-
}
|
|
663
|
-
catch (parseError) {
|
|
664
|
-
throw ErrorUtils.createError(`SSE error event: ${event.data}`, CHAT_CONVERSION_ERROR_CODES.STREAM_ERROR, { event });
|
|
712
|
+
const parsed = JSON.parse(rawPayload);
|
|
713
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
714
|
+
errorData = parsed;
|
|
715
|
+
}
|
|
665
716
|
}
|
|
717
|
+
catch {
|
|
718
|
+
// keep raw payload only
|
|
719
|
+
}
|
|
720
|
+
const errorMessage = errorData
|
|
721
|
+
? (typeof errorData.error === 'string'
|
|
722
|
+
? errorData.error
|
|
723
|
+
: typeof errorData.message === 'string'
|
|
724
|
+
? errorData.message
|
|
725
|
+
: typeof errorData.content === 'string'
|
|
726
|
+
? errorData.content
|
|
727
|
+
: 'Unknown SSE error')
|
|
728
|
+
: `SSE error event: ${event.data}`;
|
|
729
|
+
const code = errorData
|
|
730
|
+
? (typeof errorData.code === 'string'
|
|
731
|
+
? errorData.code
|
|
732
|
+
: typeof errorData.finish_reason === 'string'
|
|
733
|
+
? errorData.finish_reason
|
|
734
|
+
: 'SSE_ERROR')
|
|
735
|
+
: 'SSE_ERROR';
|
|
736
|
+
const typedError = new Error(errorMessage);
|
|
737
|
+
typedError.code = code;
|
|
738
|
+
throw ErrorUtils.createError(typedError.message, CHAT_CONVERSION_ERROR_CODES.STREAM_ERROR, { errorData, event });
|
|
666
739
|
}
|
|
667
740
|
/**
|
|
668
741
|
* 构建部分响应
|
|
@@ -8,6 +8,12 @@ export interface VirtualRouterHitEvent {
|
|
|
8
8
|
runtimeKey?: string;
|
|
9
9
|
providerType?: string;
|
|
10
10
|
modelId?: string;
|
|
11
|
+
reason?: string;
|
|
12
|
+
requestTokens?: number;
|
|
13
|
+
selectionPenalty?: number;
|
|
14
|
+
stopMessageActive?: boolean;
|
|
15
|
+
stopMessageMode?: 'on' | 'off' | 'auto' | 'unset';
|
|
16
|
+
stopMessageRemaining?: number;
|
|
11
17
|
}
|
|
12
18
|
export interface ProviderUsageEvent {
|
|
13
19
|
requestId: string;
|
|
@@ -29,6 +35,9 @@ export interface RouterStatsBucket {
|
|
|
29
35
|
poolHitCount: Record<string, number>;
|
|
30
36
|
routeHitCount: Record<string, number>;
|
|
31
37
|
providerHitCount: Record<string, number>;
|
|
38
|
+
reasonHitCount: Record<string, number>;
|
|
39
|
+
penaltyHitCount: Record<string, number>;
|
|
40
|
+
stopMessageActiveCount: number;
|
|
32
41
|
}
|
|
33
42
|
export interface RouterStatsSnapshot {
|
|
34
43
|
global: RouterStatsBucket;
|
|
@@ -6,7 +6,10 @@ function createEmptyRouterBucket() {
|
|
|
6
6
|
requestCount: 0,
|
|
7
7
|
poolHitCount: {},
|
|
8
8
|
routeHitCount: {},
|
|
9
|
-
providerHitCount: {}
|
|
9
|
+
providerHitCount: {},
|
|
10
|
+
reasonHitCount: {},
|
|
11
|
+
penaltyHitCount: {},
|
|
12
|
+
stopMessageActiveCount: 0
|
|
10
13
|
};
|
|
11
14
|
}
|
|
12
15
|
function createEmptyProviderBucket() {
|
|
@@ -135,6 +138,17 @@ class DefaultStatsCenter {
|
|
|
135
138
|
if (ev.providerKey) {
|
|
136
139
|
bucket.providerHitCount[ev.providerKey] = (bucket.providerHitCount[ev.providerKey] || 0) + 1;
|
|
137
140
|
}
|
|
141
|
+
if (typeof ev.reason === 'string' && ev.reason.trim()) {
|
|
142
|
+
const reason = ev.reason.trim();
|
|
143
|
+
bucket.reasonHitCount[reason] = (bucket.reasonHitCount[reason] || 0) + 1;
|
|
144
|
+
}
|
|
145
|
+
if (typeof ev.selectionPenalty === 'number' && Number.isFinite(ev.selectionPenalty) && ev.selectionPenalty > 0) {
|
|
146
|
+
const key = String(Math.floor(ev.selectionPenalty));
|
|
147
|
+
bucket.penaltyHitCount[key] = (bucket.penaltyHitCount[key] || 0) + 1;
|
|
148
|
+
}
|
|
149
|
+
if (ev.stopMessageActive === true) {
|
|
150
|
+
bucket.stopMessageActiveCount += 1;
|
|
151
|
+
}
|
|
138
152
|
}
|
|
139
153
|
applyProviderUsageToBucket(bucket, ev) {
|
|
140
154
|
bucket.requestCount += 1;
|
|
@@ -187,6 +201,7 @@ function printStatsToConsole(snapshot) {
|
|
|
187
201
|
const totalRequests = router.global.requestCount;
|
|
188
202
|
const poolEntries = Object.entries(router.global.poolHitCount);
|
|
189
203
|
const providerEntries = Object.entries(router.global.providerHitCount);
|
|
204
|
+
const reasonEntries = Object.entries(router.global.reasonHitCount);
|
|
190
205
|
// Router summary
|
|
191
206
|
// eslint-disable-next-line no-console
|
|
192
207
|
console.log('[stats] Virtual Router:');
|
|
@@ -210,6 +225,19 @@ function printStatsToConsole(snapshot) {
|
|
|
210
225
|
console.log(` ${providerKey}: ${count}`);
|
|
211
226
|
}
|
|
212
227
|
}
|
|
228
|
+
if (reasonEntries.length) {
|
|
229
|
+
// eslint-disable-next-line no-console
|
|
230
|
+
console.log(' top reasons:');
|
|
231
|
+
const sortedReasons = reasonEntries.sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
232
|
+
for (const [reason, count] of sortedReasons) {
|
|
233
|
+
// eslint-disable-next-line no-console
|
|
234
|
+
console.log(` ${reason}: ${count}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (router.global.stopMessageActiveCount > 0) {
|
|
238
|
+
// eslint-disable-next-line no-console
|
|
239
|
+
console.log(` stopMessage-active hits: ${router.global.stopMessageActiveCount}`);
|
|
240
|
+
}
|
|
213
241
|
const globalProvider = providers.global;
|
|
214
242
|
const totalProviderRequests = globalProvider.requestCount;
|
|
215
243
|
const avgLatency = globalProvider.successCount > 0 ? globalProvider.latencySumMs / globalProvider.successCount : 0;
|
|
@@ -8,14 +8,7 @@ const resolveTopLevelFile = (record) => {
|
|
|
8
8
|
asString(record.filename);
|
|
9
9
|
if (direct)
|
|
10
10
|
return direct;
|
|
11
|
-
|
|
12
|
-
if (!targetAlias)
|
|
13
|
-
return undefined;
|
|
14
|
-
if (targetAlias.includes('\n') || targetAlias.includes('\r'))
|
|
15
|
-
return undefined;
|
|
16
|
-
if (!/[./\\]/.test(targetAlias))
|
|
17
|
-
return undefined;
|
|
18
|
-
return targetAlias;
|
|
11
|
+
return undefined;
|
|
19
12
|
};
|
|
20
13
|
const buildSingleChangePayload = (record) => {
|
|
21
14
|
const kindRaw = asString(record.kind);
|
|
@@ -33,12 +26,11 @@ const buildSingleChangePayload = (record) => {
|
|
|
33
26
|
const changeFile = asString(record.file) ??
|
|
34
27
|
asString(record.path) ??
|
|
35
28
|
asString(record.filepath) ??
|
|
36
|
-
asString(record.filename)
|
|
37
|
-
resolveTopLevelFile(record);
|
|
29
|
+
asString(record.filename);
|
|
38
30
|
if (changeFile) {
|
|
39
31
|
change.file = changeFile;
|
|
40
32
|
}
|
|
41
|
-
return { file: changeFile, changes: [change] };
|
|
33
|
+
return { ...(changeFile ? { file: changeFile } : {}), changes: [change] };
|
|
42
34
|
};
|
|
43
35
|
const coerceChangesArray = (value) => {
|
|
44
36
|
const parsed = tryParseJson(value);
|
|
@@ -9,6 +9,133 @@ const toJson = (value) => {
|
|
|
9
9
|
return '{}';
|
|
10
10
|
}
|
|
11
11
|
};
|
|
12
|
+
const GIT_RESET_HARD_PATTERN = /\bgit\s+reset\s+--hard(?:\s|$)/i;
|
|
13
|
+
const GIT_CHECKOUT_PATTERN = /\bgit\s+checkout\b/i;
|
|
14
|
+
const SHELL_SEPARATORS = new Set([';', '&&', '||', '|', '&']);
|
|
15
|
+
function splitShellTokens(command) {
|
|
16
|
+
const tokens = [];
|
|
17
|
+
let current = '';
|
|
18
|
+
let quote = null;
|
|
19
|
+
let escaped = false;
|
|
20
|
+
const pushCurrent = () => {
|
|
21
|
+
if (current.length > 0) {
|
|
22
|
+
tokens.push(current);
|
|
23
|
+
current = '';
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
for (let i = 0; i < command.length; i += 1) {
|
|
27
|
+
const ch = command[i];
|
|
28
|
+
if (escaped) {
|
|
29
|
+
current += ch;
|
|
30
|
+
escaped = false;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (ch === '\\') {
|
|
34
|
+
escaped = true;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (quote) {
|
|
38
|
+
if (ch === quote) {
|
|
39
|
+
quote = null;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
current += ch;
|
|
43
|
+
}
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (ch === '"' || ch === "'") {
|
|
47
|
+
quote = ch;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (/\s/.test(ch)) {
|
|
51
|
+
pushCurrent();
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (ch === ';') {
|
|
55
|
+
pushCurrent();
|
|
56
|
+
tokens.push(';');
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (ch === '|' || ch === '&') {
|
|
60
|
+
pushCurrent();
|
|
61
|
+
const next = command[i + 1];
|
|
62
|
+
if ((ch === '|' || ch === '&') && next === ch) {
|
|
63
|
+
tokens.push(ch + next);
|
|
64
|
+
i += 1;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
tokens.push(ch);
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
current += ch;
|
|
72
|
+
}
|
|
73
|
+
pushCurrent();
|
|
74
|
+
return tokens;
|
|
75
|
+
}
|
|
76
|
+
function evaluateGitCheckoutScope(command) {
|
|
77
|
+
const match = GIT_CHECKOUT_PATTERN.exec(command);
|
|
78
|
+
if (!match) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const checkoutText = command.slice(match.index);
|
|
82
|
+
const tokens = splitShellTokens(checkoutText);
|
|
83
|
+
if (tokens.length < 3 || tokens[0]?.toLowerCase() !== 'git' || tokens[1]?.toLowerCase() !== 'checkout') {
|
|
84
|
+
return {
|
|
85
|
+
reason: 'forbidden_git_checkout_scope',
|
|
86
|
+
message: 'Command blocked: git checkout is restricted to a single file path. Use `git checkout -- <file>` or `git checkout <ref> -- <file>`.'
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const separatorIdx = tokens.findIndex((token, idx) => idx >= 2 && SHELL_SEPARATORS.has(token));
|
|
90
|
+
if (separatorIdx >= 0) {
|
|
91
|
+
return {
|
|
92
|
+
reason: 'forbidden_git_checkout_scope',
|
|
93
|
+
message: 'Command blocked: git checkout must be a standalone single-file command (no chained commands).'
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const checkoutTokens = separatorIdx >= 0 ? tokens.slice(0, separatorIdx) : tokens;
|
|
97
|
+
const dashDashIdx = checkoutTokens.indexOf('--', 2);
|
|
98
|
+
if (dashDashIdx < 0) {
|
|
99
|
+
return {
|
|
100
|
+
reason: 'forbidden_git_checkout_scope',
|
|
101
|
+
message: 'Command blocked: git checkout is restricted to a single file path. Use `git checkout -- <file>` or `git checkout <ref> -- <file>`.'
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const beforeDashDash = checkoutTokens.slice(2, dashDashIdx);
|
|
105
|
+
if (beforeDashDash.length > 1 || beforeDashDash.some((token) => token.startsWith('-'))) {
|
|
106
|
+
return {
|
|
107
|
+
reason: 'forbidden_git_checkout_scope',
|
|
108
|
+
message: 'Command blocked: git checkout is restricted to a single file path. Use `git checkout -- <file>` or `git checkout <ref> -- <file>`.'
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const paths = checkoutTokens.slice(dashDashIdx + 1);
|
|
112
|
+
if (paths.length !== 1) {
|
|
113
|
+
return {
|
|
114
|
+
reason: 'forbidden_git_checkout_scope',
|
|
115
|
+
message: 'Command blocked: git checkout is restricted to a single file path. Use `git checkout -- <file>` or `git checkout <ref> -- <file>`.'
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const path = paths[0];
|
|
119
|
+
if (!path || path === '.' || path === '/' || path === '*' || path.endsWith('/')) {
|
|
120
|
+
return {
|
|
121
|
+
reason: 'forbidden_git_checkout_scope',
|
|
122
|
+
message: 'Command blocked: git checkout is restricted to one concrete file path (directory/pathset restore is not allowed).'
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
function detectPolicyViolation(command) {
|
|
128
|
+
if (!command || !command.trim()) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
if (GIT_RESET_HARD_PATTERN.test(command)) {
|
|
132
|
+
return {
|
|
133
|
+
reason: 'forbidden_git_reset_hard',
|
|
134
|
+
message: 'Command blocked: `git reset --hard` is destructive. Use `git reset --mixed <ref>` or file-scoped restore commands instead.'
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return evaluateGitCheckoutScope(command);
|
|
138
|
+
}
|
|
12
139
|
export function validateExecCommandArgs(argsString, rawArgs) {
|
|
13
140
|
const raw = typeof argsString === 'string' ? argsString : String(argsString ?? '');
|
|
14
141
|
const parsed = isRecord(rawArgs) && Object.keys(rawArgs).length > 0
|
|
@@ -18,5 +145,10 @@ export function validateExecCommandArgs(argsString, rawArgs) {
|
|
|
18
145
|
if (normalized.ok === false) {
|
|
19
146
|
return { ok: false, reason: normalized.reason };
|
|
20
147
|
}
|
|
148
|
+
const command = typeof normalized.normalized.cmd === 'string' ? normalized.normalized.cmd : '';
|
|
149
|
+
const violation = detectPolicyViolation(command);
|
|
150
|
+
if (violation) {
|
|
151
|
+
return { ok: false, reason: violation.reason, message: violation.message };
|
|
152
|
+
}
|
|
21
153
|
return { ok: true, normalizedArgs: toJson(normalized.normalized) };
|
|
22
154
|
}
|