@jsonstudio/llms 0.6.199 → 0.6.215
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 +45 -3
- package/dist/conversion/compat/actions/glm-tool-extraction.d.ts +2 -0
- package/dist/conversion/compat/actions/glm-tool-extraction.js +264 -0
- package/dist/conversion/compat/profiles/chat-glm.json +181 -181
- package/dist/conversion/compat/profiles/chat-iflow.json +195 -195
- package/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
- package/dist/conversion/compat/profiles/chat-qwen.json +20 -20
- package/dist/conversion/compat/profiles/responses-c4m.json +42 -42
- package/dist/conversion/hub/pipeline/compat/compat-engine.js +3 -1087
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.d.ts +9 -0
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +845 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +2 -2
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +118 -11
- package/dist/conversion/shared/snapshot-utils.js +17 -47
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/router/virtual-router/engine.d.ts +1 -0
- package/dist/router/virtual-router/engine.js +16 -0
- package/dist/telemetry/stats-center.d.ts +73 -0
- package/dist/telemetry/stats-center.js +280 -0
- package/package.json +1 -1
|
@@ -11,6 +11,7 @@ import { GeminiSemanticMapper } from '../semantic-mappers/gemini-mapper.js';
|
|
|
11
11
|
import { ChatFormatAdapter } from '../format-adapters/chat-format-adapter.js';
|
|
12
12
|
import { ChatSemanticMapper } from '../semantic-mappers/chat-mapper.js';
|
|
13
13
|
import { createSnapshotRecorder } from '../snapshot-recorder.js';
|
|
14
|
+
import { shouldRecordSnapshots } from '../../shared/snapshot-utils.js';
|
|
14
15
|
import { runReqInboundStage1FormatParse } from './stages/req_inbound/req_inbound_stage1_format_parse/index.js';
|
|
15
16
|
import { runReqInboundStage2SemanticMap } from './stages/req_inbound/req_inbound_stage2_semantic_map/index.js';
|
|
16
17
|
import { runChatContextCapture, captureResponsesContextSnapshot } from './stages/req_inbound/req_inbound_stage3_context_capture/index.js';
|
|
@@ -348,8 +349,7 @@ export class HubPipeline {
|
|
|
348
349
|
return adapterContext;
|
|
349
350
|
}
|
|
350
351
|
maybeCreateStageRecorder(context, endpoint) {
|
|
351
|
-
|
|
352
|
-
if (flag === '0') {
|
|
352
|
+
if (!shouldRecordSnapshots()) {
|
|
353
353
|
return undefined;
|
|
354
354
|
}
|
|
355
355
|
const effectiveEndpoint = endpoint || context.entryEndpoint || '/v1/chat/completions';
|
|
@@ -17,6 +17,31 @@ const GENERATION_CONFIG_KEYS = [
|
|
|
17
17
|
];
|
|
18
18
|
const PASSTHROUGH_METADATA_PREFIX = 'rcc_passthrough_';
|
|
19
19
|
const PASSTHROUGH_PARAMETERS = ['tool_choice'];
|
|
20
|
+
const DUMMY_THOUGHT_SIGNATURE = 'skip_thought_signature_validator';
|
|
21
|
+
function coerceThoughtSignature(value) {
|
|
22
|
+
if (typeof value === 'string' && value.trim().length) {
|
|
23
|
+
return value.trim();
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
function extractThoughtSignatureFromToolCall(tc) {
|
|
28
|
+
if (!tc || typeof tc !== 'object') {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
const node = tc;
|
|
32
|
+
const direct = coerceThoughtSignature(node.thought_signature ?? node.thoughtSignature);
|
|
33
|
+
if (direct) {
|
|
34
|
+
return direct;
|
|
35
|
+
}
|
|
36
|
+
const extra = node.extra_content ?? node.extraContent;
|
|
37
|
+
if (extra && typeof extra === 'object') {
|
|
38
|
+
const googleNode = extra.google ?? extra.Google;
|
|
39
|
+
if (googleNode && typeof googleNode === 'object') {
|
|
40
|
+
return coerceThoughtSignature(googleNode.thought_signature ?? googleNode.thoughtSignature);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
20
45
|
function normalizeToolOutputs(messages, missing) {
|
|
21
46
|
const outputs = [];
|
|
22
47
|
messages.forEach((msg, index) => {
|
|
@@ -35,6 +60,37 @@ function normalizeToolOutputs(messages, missing) {
|
|
|
35
60
|
});
|
|
36
61
|
return outputs.length ? outputs : undefined;
|
|
37
62
|
}
|
|
63
|
+
function synthesizeToolOutputsFromMessages(messages) {
|
|
64
|
+
if (!Array.isArray(messages)) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
const outputs = [];
|
|
68
|
+
for (const message of messages) {
|
|
69
|
+
if (!message || typeof message !== 'object')
|
|
70
|
+
continue;
|
|
71
|
+
if (message.role !== 'assistant')
|
|
72
|
+
continue;
|
|
73
|
+
const toolCalls = Array.isArray(message.tool_calls)
|
|
74
|
+
? message.tool_calls
|
|
75
|
+
: [];
|
|
76
|
+
for (const call of toolCalls) {
|
|
77
|
+
const callId = typeof call.id === 'string' ? call.id : undefined;
|
|
78
|
+
if (!callId) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const existing = outputs.find((entry) => entry.tool_call_id === callId);
|
|
82
|
+
if (existing) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
outputs.push({
|
|
86
|
+
tool_call_id: callId,
|
|
87
|
+
content: '',
|
|
88
|
+
name: (call.function && call.function.name) || undefined
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return outputs;
|
|
93
|
+
}
|
|
38
94
|
function normalizeToolContent(value) {
|
|
39
95
|
if (typeof value === 'string')
|
|
40
96
|
return value;
|
|
@@ -47,6 +103,30 @@ function normalizeToolContent(value) {
|
|
|
47
103
|
return String(value ?? '');
|
|
48
104
|
}
|
|
49
105
|
}
|
|
106
|
+
function convertToolMessageToOutput(message) {
|
|
107
|
+
const rawId = (message.tool_call_id ?? message.id);
|
|
108
|
+
const callId = typeof rawId === 'string' && rawId.trim().length ? rawId.trim() : undefined;
|
|
109
|
+
if (!callId) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
tool_call_id: callId,
|
|
114
|
+
content: normalizeToolContent(message.content),
|
|
115
|
+
name: typeof message.name === 'string' ? message.name : undefined
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function buildFunctionResponseEntry(output) {
|
|
119
|
+
const parsedPayload = safeParseJson(output.content);
|
|
120
|
+
const normalizedPayload = ensureFunctionResponsePayload(cloneAsJsonValue(parsedPayload));
|
|
121
|
+
const part = {
|
|
122
|
+
functionResponse: {
|
|
123
|
+
name: output.name || 'tool',
|
|
124
|
+
id: output.tool_call_id,
|
|
125
|
+
response: normalizedPayload
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
return { role: 'user', parts: [part] };
|
|
129
|
+
}
|
|
50
130
|
function collectSystemSegments(systemInstruction) {
|
|
51
131
|
if (!systemInstruction)
|
|
52
132
|
return [];
|
|
@@ -93,13 +173,20 @@ function collectParameters(payload) {
|
|
|
93
173
|
}
|
|
94
174
|
function buildGeminiRequestFromChat(chat, metadata) {
|
|
95
175
|
const contents = [];
|
|
176
|
+
const emittedToolOutputs = new Set();
|
|
96
177
|
for (const message of chat.messages) {
|
|
97
178
|
if (!message || typeof message !== 'object')
|
|
98
179
|
continue;
|
|
99
180
|
if (message.role === 'system')
|
|
100
181
|
continue;
|
|
101
|
-
if (message.role === 'tool')
|
|
182
|
+
if (message.role === 'tool') {
|
|
183
|
+
const toolOutput = convertToolMessageToOutput(message);
|
|
184
|
+
if (toolOutput) {
|
|
185
|
+
contents.push(buildFunctionResponseEntry(toolOutput));
|
|
186
|
+
emittedToolOutputs.add(toolOutput.tool_call_id);
|
|
187
|
+
}
|
|
102
188
|
continue;
|
|
189
|
+
}
|
|
103
190
|
const entry = {
|
|
104
191
|
role: mapChatRoleToGemini(message.role),
|
|
105
192
|
parts: []
|
|
@@ -131,24 +218,36 @@ function buildGeminiRequestFromChat(chat, metadata) {
|
|
|
131
218
|
if (typeof tc.id === 'string') {
|
|
132
219
|
part.functionCall.id = tc.id;
|
|
133
220
|
}
|
|
221
|
+
const thoughtSignature = extractThoughtSignatureFromToolCall(tc) ?? DUMMY_THOUGHT_SIGNATURE;
|
|
222
|
+
if (thoughtSignature) {
|
|
223
|
+
part.thoughtSignature = thoughtSignature;
|
|
224
|
+
}
|
|
134
225
|
entry.parts.push(part);
|
|
135
226
|
}
|
|
136
227
|
if (entry.parts.length) {
|
|
137
228
|
contents.push(entry);
|
|
138
229
|
}
|
|
139
230
|
}
|
|
231
|
+
const toolOutputMap = new Map();
|
|
140
232
|
if (Array.isArray(chat.toolOutputs)) {
|
|
141
|
-
for (const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
233
|
+
for (const entry of chat.toolOutputs) {
|
|
234
|
+
if (entry && typeof entry.tool_call_id === 'string' && entry.tool_call_id.trim().length) {
|
|
235
|
+
toolOutputMap.set(entry.tool_call_id.trim(), entry);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (toolOutputMap.size === 0) {
|
|
240
|
+
const syntheticOutputs = synthesizeToolOutputsFromMessages(chat.messages);
|
|
241
|
+
for (const output of syntheticOutputs) {
|
|
242
|
+
toolOutputMap.set(output.tool_call_id, output);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
for (const output of toolOutputMap.values()) {
|
|
246
|
+
if (emittedToolOutputs.has(output.tool_call_id)) {
|
|
247
|
+
continue;
|
|
151
248
|
}
|
|
249
|
+
contents.push(buildFunctionResponseEntry(output));
|
|
250
|
+
emittedToolOutputs.add(output.tool_call_id);
|
|
152
251
|
}
|
|
153
252
|
const request = {
|
|
154
253
|
model: chat.parameters?.model || 'models/gemini-pro',
|
|
@@ -235,6 +334,14 @@ function safeParseJson(value) {
|
|
|
235
334
|
return value;
|
|
236
335
|
}
|
|
237
336
|
}
|
|
337
|
+
function ensureFunctionResponsePayload(value) {
|
|
338
|
+
if (value && typeof value === 'object') {
|
|
339
|
+
return value;
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
result: value === undefined ? null : value
|
|
343
|
+
};
|
|
344
|
+
}
|
|
238
345
|
function cloneAsJsonValue(value) {
|
|
239
346
|
try {
|
|
240
347
|
return JSON.parse(JSON.stringify(value ?? null));
|
|
@@ -1,35 +1,25 @@
|
|
|
1
|
-
import os from 'node:os';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import fs from 'node:fs/promises';
|
|
4
1
|
import { writeSnapshotViaHooks } from './snapshot-hooks.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
return hint;
|
|
9
|
-
const ep = String(endpoint || '').toLowerCase();
|
|
10
|
-
if (ep.includes('/responses'))
|
|
11
|
-
return 'openai-responses';
|
|
12
|
-
if (ep.includes('/messages'))
|
|
13
|
-
return 'anthropic-messages';
|
|
14
|
-
if (ep.includes('/gemini'))
|
|
15
|
-
return 'gemini-chat';
|
|
16
|
-
return 'openai-chat';
|
|
17
|
-
}
|
|
18
|
-
async function ensureDir(dir) {
|
|
19
|
-
try {
|
|
20
|
-
await fs.mkdir(dir, { recursive: true });
|
|
2
|
+
function resolveBoolFromEnv(value, fallback) {
|
|
3
|
+
if (!value) {
|
|
4
|
+
return fallback;
|
|
21
5
|
}
|
|
22
|
-
|
|
23
|
-
|
|
6
|
+
const normalized = value.trim().toLowerCase();
|
|
7
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized)) {
|
|
8
|
+
return true;
|
|
24
9
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
10
|
+
if (['0', 'false', 'no', 'off'].includes(normalized)) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
return fallback;
|
|
28
14
|
}
|
|
29
15
|
export function shouldRecordSnapshots() {
|
|
30
|
-
const
|
|
31
|
-
if (
|
|
32
|
-
return
|
|
16
|
+
const hubFlag = process.env.ROUTECODEX_HUB_SNAPSHOTS;
|
|
17
|
+
if (hubFlag && hubFlag.trim().length) {
|
|
18
|
+
return resolveBoolFromEnv(hubFlag, true);
|
|
19
|
+
}
|
|
20
|
+
const sharedFlag = process.env.ROUTECODEX_SNAPSHOT ?? process.env.ROUTECODEX_SNAPSHOTS;
|
|
21
|
+
if (sharedFlag && sharedFlag.trim().length) {
|
|
22
|
+
return resolveBoolFromEnv(sharedFlag, true);
|
|
33
23
|
}
|
|
34
24
|
return true;
|
|
35
25
|
}
|
|
@@ -37,26 +27,6 @@ export async function recordSnapshot(options) {
|
|
|
37
27
|
if (!shouldRecordSnapshots())
|
|
38
28
|
return;
|
|
39
29
|
const endpoint = options.endpoint || '/v1/chat/completions';
|
|
40
|
-
const folder = mapEndpointToFolder(endpoint, options.folderHint);
|
|
41
|
-
const dir = path.join(SNAPSHOT_BASE, folder);
|
|
42
|
-
try {
|
|
43
|
-
await ensureDir(dir);
|
|
44
|
-
const safeStage = sanitize(options.stage);
|
|
45
|
-
const safeRequestId = sanitize(options.requestId);
|
|
46
|
-
const file = path.join(dir, `${safeRequestId}_${safeStage}.json`);
|
|
47
|
-
const payload = {
|
|
48
|
-
meta: {
|
|
49
|
-
stage: options.stage,
|
|
50
|
-
timestamp: Date.now(),
|
|
51
|
-
endpoint
|
|
52
|
-
},
|
|
53
|
-
body: options.data
|
|
54
|
-
};
|
|
55
|
-
await fs.writeFile(file, JSON.stringify(payload, null, 2), 'utf-8');
|
|
56
|
-
}
|
|
57
|
-
catch (error) {
|
|
58
|
-
console.warn('[snapshot-utils] failed to write snapshot', error);
|
|
59
|
-
}
|
|
60
30
|
void writeSnapshotViaHooks({
|
|
61
31
|
endpoint,
|
|
62
32
|
stage: options.stage,
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ export declare class VirtualRouterEngine {
|
|
|
11
11
|
private routeStats;
|
|
12
12
|
private readonly debug;
|
|
13
13
|
private healthConfig;
|
|
14
|
+
private readonly statsCenter;
|
|
14
15
|
initialize(config: VirtualRouterConfig): void;
|
|
15
16
|
route(request: StandardizedRequest | ProcessedRequest, metadata: RouterMetadataInput): {
|
|
16
17
|
target: TargetMetadata;
|
|
@@ -5,6 +5,7 @@ import { RoutingClassifier } from './classifier.js';
|
|
|
5
5
|
import { buildRoutingFeatures } from './features.js';
|
|
6
6
|
import { ContextAdvisor } from './context-advisor.js';
|
|
7
7
|
import { DEFAULT_MODEL_CONTEXT_TOKENS, DEFAULT_ROUTE, ROUTE_PRIORITY, VirtualRouterError, VirtualRouterErrorCode } from './types.js';
|
|
8
|
+
import { getStatsCenter } from '../../telemetry/stats-center.js';
|
|
8
9
|
export class VirtualRouterEngine {
|
|
9
10
|
routing = {};
|
|
10
11
|
providerRegistry = new ProviderRegistry();
|
|
@@ -16,6 +17,7 @@ export class VirtualRouterEngine {
|
|
|
16
17
|
routeStats = new Map();
|
|
17
18
|
debug = console; // thin hook; host may monkey-patch for colored logging
|
|
18
19
|
healthConfig = null;
|
|
20
|
+
statsCenter = getStatsCenter();
|
|
19
21
|
initialize(config) {
|
|
20
22
|
this.validateConfig(config);
|
|
21
23
|
this.routing = config.routing;
|
|
@@ -40,6 +42,20 @@ export class VirtualRouterEngine {
|
|
|
40
42
|
const target = this.providerRegistry.buildTarget(selection.providerKey);
|
|
41
43
|
this.healthManager.recordSuccess(selection.providerKey);
|
|
42
44
|
this.incrementRouteStat(selection.routeUsed, selection.providerKey);
|
|
45
|
+
try {
|
|
46
|
+
this.statsCenter.recordVirtualRouterHit({
|
|
47
|
+
requestId: metadata.requestId,
|
|
48
|
+
timestamp: Date.now(),
|
|
49
|
+
entryEndpoint: metadata.entryEndpoint || '/v1/chat/completions',
|
|
50
|
+
routeName: selection.routeUsed,
|
|
51
|
+
pool: selection.routeUsed,
|
|
52
|
+
providerKey: selection.providerKey,
|
|
53
|
+
modelId: target.modelId || undefined
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// stats must never break routing
|
|
58
|
+
}
|
|
43
59
|
const hitReason = this.buildHitReason(selection.routeUsed, selection.providerKey, classification, features);
|
|
44
60
|
const formatted = this.formatVirtualRouterHit(selection.routeUsed, selection.providerKey, target.modelId || '', hitReason);
|
|
45
61
|
if (formatted) {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export interface VirtualRouterHitEvent {
|
|
2
|
+
requestId: string;
|
|
3
|
+
timestamp: number;
|
|
4
|
+
entryEndpoint: string;
|
|
5
|
+
routeName: string;
|
|
6
|
+
pool: string;
|
|
7
|
+
providerKey: string;
|
|
8
|
+
runtimeKey?: string;
|
|
9
|
+
providerType?: string;
|
|
10
|
+
modelId?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ProviderUsageEvent {
|
|
13
|
+
requestId: string;
|
|
14
|
+
timestamp: number;
|
|
15
|
+
providerKey: string;
|
|
16
|
+
runtimeKey?: string;
|
|
17
|
+
providerType: string;
|
|
18
|
+
modelId?: string;
|
|
19
|
+
routeName?: string;
|
|
20
|
+
entryEndpoint?: string;
|
|
21
|
+
success: boolean;
|
|
22
|
+
latencyMs: number;
|
|
23
|
+
promptTokens?: number;
|
|
24
|
+
completionTokens?: number;
|
|
25
|
+
totalTokens?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface RouterStatsBucket {
|
|
28
|
+
requestCount: number;
|
|
29
|
+
poolHitCount: Record<string, number>;
|
|
30
|
+
routeHitCount: Record<string, number>;
|
|
31
|
+
providerHitCount: Record<string, number>;
|
|
32
|
+
}
|
|
33
|
+
export interface RouterStatsSnapshot {
|
|
34
|
+
global: RouterStatsBucket;
|
|
35
|
+
byEntryEndpoint: Record<string, RouterStatsBucket>;
|
|
36
|
+
}
|
|
37
|
+
export interface ProviderStatsBucket {
|
|
38
|
+
requestCount: number;
|
|
39
|
+
successCount: number;
|
|
40
|
+
errorCount: number;
|
|
41
|
+
latencySumMs: number;
|
|
42
|
+
minLatencyMs: number;
|
|
43
|
+
maxLatencyMs: number;
|
|
44
|
+
usage: {
|
|
45
|
+
promptTokens: number;
|
|
46
|
+
completionTokens: number;
|
|
47
|
+
totalTokens: number;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export interface ProviderStatsSnapshot {
|
|
51
|
+
global: ProviderStatsBucket;
|
|
52
|
+
byProviderKey: Record<string, ProviderStatsBucket>;
|
|
53
|
+
byRoute: Record<string, ProviderStatsBucket>;
|
|
54
|
+
byEntryEndpoint: Record<string, ProviderStatsBucket>;
|
|
55
|
+
}
|
|
56
|
+
export interface StatsSnapshot {
|
|
57
|
+
router: RouterStatsSnapshot;
|
|
58
|
+
providers: ProviderStatsSnapshot;
|
|
59
|
+
}
|
|
60
|
+
export interface StatsCenterOptions {
|
|
61
|
+
enable?: boolean;
|
|
62
|
+
autoPrintOnExit?: boolean;
|
|
63
|
+
persistPath?: string | null;
|
|
64
|
+
}
|
|
65
|
+
export interface StatsCenter {
|
|
66
|
+
recordVirtualRouterHit(ev: VirtualRouterHitEvent): void;
|
|
67
|
+
recordProviderUsage(ev: ProviderUsageEvent): void;
|
|
68
|
+
getSnapshot(): StatsSnapshot;
|
|
69
|
+
flushToDisk(): Promise<void>;
|
|
70
|
+
reset(): void;
|
|
71
|
+
}
|
|
72
|
+
export declare function initStatsCenter(options?: StatsCenterOptions): StatsCenter;
|
|
73
|
+
export declare function getStatsCenter(): StatsCenter;
|