@jsonstudio/llms 0.6.230 → 0.6.467

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.
Files changed (81) hide show
  1. package/README.md +2 -0
  2. package/dist/conversion/codecs/gemini-openai-codec.js +24 -2
  3. package/dist/conversion/compat/actions/gemini-web-search.d.ts +17 -0
  4. package/dist/conversion/compat/actions/gemini-web-search.js +68 -0
  5. package/dist/conversion/compat/actions/glm-image-content.d.ts +2 -0
  6. package/dist/conversion/compat/actions/glm-image-content.js +83 -0
  7. package/dist/conversion/compat/actions/glm-vision-prompt.d.ts +11 -0
  8. package/dist/conversion/compat/actions/glm-vision-prompt.js +177 -0
  9. package/dist/conversion/compat/actions/glm-web-search.js +25 -28
  10. package/dist/conversion/compat/actions/iflow-web-search.d.ts +18 -0
  11. package/dist/conversion/compat/actions/iflow-web-search.js +87 -0
  12. package/dist/conversion/compat/actions/universal-shape-filter.js +11 -0
  13. package/dist/conversion/compat/profiles/chat-gemini.json +17 -0
  14. package/dist/conversion/compat/profiles/chat-glm.json +194 -184
  15. package/dist/conversion/compat/profiles/chat-iflow.json +199 -195
  16. package/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
  17. package/dist/conversion/compat/profiles/chat-qwen.json +20 -20
  18. package/dist/conversion/compat/profiles/responses-c4m.json +42 -42
  19. package/dist/conversion/config/sample-config.json +1 -1
  20. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +24 -0
  21. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +8 -0
  22. package/dist/conversion/hub/pipeline/hub-pipeline.js +32 -1
  23. package/dist/conversion/hub/pipeline/session-identifiers.d.ts +9 -0
  24. package/dist/conversion/hub/pipeline/session-identifiers.js +76 -0
  25. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +31 -2
  26. package/dist/conversion/hub/pipeline/target-utils.js +6 -0
  27. package/dist/conversion/hub/process/chat-process.js +186 -40
  28. package/dist/conversion/hub/response/provider-response.d.ts +13 -1
  29. package/dist/conversion/hub/response/provider-response.js +84 -35
  30. package/dist/conversion/hub/response/server-side-tools.js +61 -4
  31. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +123 -3
  32. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +17 -1
  33. package/dist/conversion/hub/standardized-bridge.js +14 -0
  34. package/dist/conversion/responses/responses-openai-bridge.js +110 -6
  35. package/dist/conversion/shared/anthropic-message-utils.js +133 -9
  36. package/dist/conversion/shared/bridge-message-utils.js +137 -10
  37. package/dist/conversion/shared/errors.d.ts +20 -0
  38. package/dist/conversion/shared/errors.js +28 -0
  39. package/dist/conversion/shared/responses-conversation-store.js +30 -3
  40. package/dist/conversion/shared/responses-output-builder.js +111 -8
  41. package/dist/conversion/shared/tool-filter-pipeline.js +1 -0
  42. package/dist/filters/special/request-toolcalls-stringify.d.ts +13 -0
  43. package/dist/filters/special/request-toolcalls-stringify.js +103 -3
  44. package/dist/filters/special/response-tool-text-canonicalize.d.ts +16 -0
  45. package/dist/filters/special/response-tool-text-canonicalize.js +27 -3
  46. package/dist/router/virtual-router/bootstrap.js +44 -12
  47. package/dist/router/virtual-router/classifier.js +13 -17
  48. package/dist/router/virtual-router/engine.d.ts +39 -0
  49. package/dist/router/virtual-router/engine.js +755 -55
  50. package/dist/router/virtual-router/features.js +1 -1
  51. package/dist/router/virtual-router/message-utils.js +36 -24
  52. package/dist/router/virtual-router/provider-registry.d.ts +15 -0
  53. package/dist/router/virtual-router/provider-registry.js +42 -1
  54. package/dist/router/virtual-router/routing-instructions.d.ts +34 -0
  55. package/dist/router/virtual-router/routing-instructions.js +383 -0
  56. package/dist/router/virtual-router/sticky-session-store.d.ts +3 -0
  57. package/dist/router/virtual-router/sticky-session-store.js +110 -0
  58. package/dist/router/virtual-router/token-counter.js +14 -3
  59. package/dist/router/virtual-router/tool-signals.js +0 -22
  60. package/dist/router/virtual-router/types.d.ts +80 -0
  61. package/dist/router/virtual-router/types.js +2 -1
  62. package/dist/servertool/engine.d.ts +27 -0
  63. package/dist/servertool/engine.js +101 -0
  64. package/dist/servertool/flow-types.d.ts +40 -0
  65. package/dist/servertool/flow-types.js +1 -0
  66. package/dist/servertool/handlers/vision.d.ts +1 -0
  67. package/dist/servertool/handlers/vision.js +194 -0
  68. package/dist/servertool/handlers/web-search.d.ts +1 -0
  69. package/dist/servertool/handlers/web-search.js +791 -0
  70. package/dist/servertool/orchestration-types.d.ts +33 -0
  71. package/dist/servertool/orchestration-types.js +1 -0
  72. package/dist/servertool/registry.d.ts +18 -0
  73. package/dist/servertool/registry.js +27 -0
  74. package/dist/servertool/server-side-tools.d.ts +8 -0
  75. package/dist/servertool/server-side-tools.js +208 -0
  76. package/dist/servertool/types.d.ts +94 -0
  77. package/dist/servertool/types.js +1 -0
  78. package/dist/servertool/vision-tool.d.ts +2 -0
  79. package/dist/servertool/vision-tool.js +185 -0
  80. package/dist/sse/sse-to-json/builders/response-builder.js +6 -3
  81. package/package.json +1 -1
@@ -14,7 +14,7 @@ export function buildRoutingFeatures(request, metadata) {
14
14
  const estimatedTokens = computeRequestTokens(request, latestUserText);
15
15
  const hasThinking = detectKeyword(normalizedUserText, THINKING_KEYWORDS);
16
16
  const hasVisionTool = detectVisionTool(request);
17
- const hasImageAttachment = hasVisionTool && detectImageAttachment(latestUserMessage);
17
+ const hasImageAttachment = detectImageAttachment(latestUserMessage);
18
18
  const hasCodingTool = detectCodingTool(request);
19
19
  const hasWebTool = detectWebTool(request);
20
20
  const hasThinkingKeyword = hasThinking || detectExtendedThinkingKeyword(normalizedUserText);
@@ -37,30 +37,42 @@ export function detectExtendedThinkingKeyword(text) {
37
37
  export function detectImageAttachment(message) {
38
38
  if (!message)
39
39
  return false;
40
- if (!message.metadata || typeof message.metadata !== 'object') {
41
- return false;
42
- }
43
- const meta = message.metadata;
44
- const attachments = (meta.attachments ?? null);
45
- if (Array.isArray(attachments)) {
46
- return attachments.some((attachment) => {
47
- const candidate = attachment;
48
- const typeValue = typeof candidate.type === 'string' ? candidate.type.toLowerCase() : '';
49
- const urlValue = typeof candidate.url === 'string'
50
- ? candidate.url
51
- : typeof candidate.src === 'string'
52
- ? candidate.src
53
- : typeof candidate.image_url === 'string'
54
- ? candidate.image_url
55
- : typeof candidate.image_url?.url === 'string'
56
- ? candidate.image_url.url
57
- : '';
58
- return typeValue.includes('image') && urlValue.trim().length > 0;
59
- });
60
- }
61
- if (typeof meta.attachmentType === 'string' && meta.attachmentType.toLowerCase().includes('image')) {
62
- const urlCandidate = typeof meta.attachmentUrl === 'string' ? meta.attachmentUrl : '';
63
- return urlCandidate.trim().length > 0;
40
+ // 仅基于标准 Chat 语义判断是否携带图片:
41
+ // - content 为数组时查找 { type: 'image' | 'image_url' | 'input_image', ... } 块;
42
+ // - 不再依赖 metadata.attachments,也不再用纯文本关键字或剪贴板标记作为信号。
43
+ if (Array.isArray(message.content)) {
44
+ for (const part of message.content) {
45
+ if (!part || typeof part !== 'object') {
46
+ continue;
47
+ }
48
+ const record = part;
49
+ const typeValue = typeof record.type === 'string' ? record.type.toLowerCase() : '';
50
+ // For chat/standardized content, images may appear as:
51
+ // - { type: "image_url", image_url: { url } }
52
+ // - { type: "image", uri: "...", data: "...", url: "..." }
53
+ // - { type: "input_image", image_url: "data:..." }
54
+ // Treat any non-empty URL/URI/data on an image-* block as a signal.
55
+ let imageCandidate = '';
56
+ if (typeof record.image_url === 'string') {
57
+ imageCandidate = record.image_url ?? '';
58
+ }
59
+ else if (record.image_url &&
60
+ typeof record.image_url?.url === 'string') {
61
+ imageCandidate = record.image_url?.url ?? '';
62
+ }
63
+ else if (typeof record.url === 'string') {
64
+ imageCandidate = record.url ?? '';
65
+ }
66
+ else if (typeof record.uri === 'string') {
67
+ imageCandidate = record.uri ?? '';
68
+ }
69
+ else if (typeof record.data === 'string') {
70
+ imageCandidate = record.data ?? '';
71
+ }
72
+ if (typeValue.includes('image') && imageCandidate.trim().length > 0) {
73
+ return true;
74
+ }
75
+ }
64
76
  }
65
77
  return false;
66
78
  }
@@ -0,0 +1,15 @@
1
+ import type { ProviderProfile, TargetMetadata } from './types.js';
2
+ export declare class ProviderRegistry {
3
+ private readonly providers;
4
+ constructor(profiles?: Record<string, ProviderProfile>);
5
+ load(profiles: Record<string, ProviderProfile>): void;
6
+ get(providerKey: string): ProviderProfile;
7
+ has(providerKey: string): boolean;
8
+ listKeys(): string[];
9
+ resolveRuntimeKeyByAlias(providerId: string, keyAlias: string): string | null;
10
+ resolveRuntimeKeyByIndex(providerId: string, keyIndex: number): string | null;
11
+ listProviderKeys(providerId: string): string[];
12
+ resolveRuntimeKeyByModel(providerId: string, modelId: string): string | null;
13
+ buildTarget(providerKey: string): TargetMetadata;
14
+ private static normalizeProfile;
15
+ }
@@ -28,6 +28,46 @@ export class ProviderRegistry {
28
28
  listKeys() {
29
29
  return Array.from(this.providers.keys());
30
30
  }
31
+ resolveRuntimeKeyByAlias(providerId, keyAlias) {
32
+ const pattern = new RegExp(`^${providerId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.${keyAlias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\.|$)`);
33
+ for (const key of this.providers.keys()) {
34
+ if (pattern.test(key)) {
35
+ return key;
36
+ }
37
+ }
38
+ return null;
39
+ }
40
+ resolveRuntimeKeyByIndex(providerId, keyIndex) {
41
+ const index = keyIndex - 1;
42
+ if (index < 0)
43
+ return null;
44
+ const keys = this.listProviderKeys(providerId);
45
+ if (index >= keys.length)
46
+ return null;
47
+ return keys[index];
48
+ }
49
+ listProviderKeys(providerId) {
50
+ const pattern = new RegExp(`^${providerId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.`);
51
+ return this.listKeys().filter(key => pattern.test(key));
52
+ }
53
+ resolveRuntimeKeyByModel(providerId, modelId) {
54
+ if (!providerId || !modelId) {
55
+ return null;
56
+ }
57
+ const normalizedModel = modelId.trim();
58
+ if (!normalizedModel) {
59
+ return null;
60
+ }
61
+ const providerKeys = this.listProviderKeys(providerId);
62
+ for (const key of providerKeys) {
63
+ const profile = this.providers.get(key);
64
+ const candidate = profile?.modelId ?? deriveModelId(key);
65
+ if (candidate === normalizedModel) {
66
+ return key;
67
+ }
68
+ }
69
+ return null;
70
+ }
31
71
  buildTarget(providerKey) {
32
72
  const profile = this.get(providerKey);
33
73
  const modelId = profile.modelId ?? deriveModelId(profile.providerKey);
@@ -62,7 +102,8 @@ export class ProviderRegistry {
62
102
  processMode: profile.processMode || 'chat',
63
103
  responsesConfig: profile.responsesConfig,
64
104
  streaming: profile.streaming,
65
- maxContextTokens: profile.maxContextTokens
105
+ maxContextTokens: profile.maxContextTokens,
106
+ ...(profile.serverToolsDisabled ? { serverToolsDisabled: true } : {})
66
107
  };
67
108
  }
68
109
  }
@@ -0,0 +1,34 @@
1
+ import type { StandardizedMessage } from '../../conversion/hub/types/standardized.js';
2
+ export interface RoutingInstruction {
3
+ type: 'force' | 'sticky' | 'disable' | 'enable' | 'clear' | 'allow';
4
+ provider?: string;
5
+ keyAlias?: string;
6
+ keyIndex?: number;
7
+ model?: string;
8
+ pathLength?: number;
9
+ }
10
+ export interface RoutingInstructionState {
11
+ forcedTarget?: {
12
+ provider?: string;
13
+ keyAlias?: string;
14
+ keyIndex?: number;
15
+ model?: string;
16
+ pathLength?: number;
17
+ };
18
+ stickyTarget?: {
19
+ provider?: string;
20
+ keyAlias?: string;
21
+ keyIndex?: number;
22
+ model?: string;
23
+ pathLength?: number;
24
+ };
25
+ allowedProviders: Set<string>;
26
+ disabledProviders: Set<string>;
27
+ disabledKeys: Map<string, Set<string | number>>;
28
+ disabledModels: Map<string, Set<string>>;
29
+ }
30
+ export declare function parseRoutingInstructions(messages: StandardizedMessage[]): RoutingInstruction[];
31
+ export declare function applyRoutingInstructions(instructions: RoutingInstruction[], currentState: RoutingInstructionState): RoutingInstructionState;
32
+ export declare function cleanMessagesFromRoutingInstructions(messages: StandardizedMessage[]): StandardizedMessage[];
33
+ export declare function serializeRoutingInstructionState(state: RoutingInstructionState): Record<string, unknown>;
34
+ export declare function deserializeRoutingInstructionState(data: Record<string, unknown>): RoutingInstructionState;
@@ -0,0 +1,383 @@
1
+ import { extractMessageText } from './message-utils.js';
2
+ export function parseRoutingInstructions(messages) {
3
+ const instructions = [];
4
+ // 从最新一条携带路由指令标记(<** ... **>)的 user 消息中解析指令,
5
+ // 而不是简单地取“最后一条 user 消息”。这样可以在服务重启后,通过完整
6
+ // 会话历史恢复 sticky/黑名单状态,同时保持“最后一次指令生效”的语义。
7
+ let sanitized = null;
8
+ for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
9
+ const message = messages[idx];
10
+ if (!message || message.role !== 'user') {
11
+ continue;
12
+ }
13
+ const content = extractMessageText(message);
14
+ if (!content) {
15
+ continue;
16
+ }
17
+ const candidate = stripCodeSegments(content);
18
+ if (!candidate) {
19
+ continue;
20
+ }
21
+ if (!/<\*\*[^*]+\*\*>/.test(candidate)) {
22
+ continue;
23
+ }
24
+ sanitized = candidate;
25
+ break;
26
+ }
27
+ if (!sanitized) {
28
+ return instructions;
29
+ }
30
+ const regex = /<\*\*([^*]+)\*\*>/g;
31
+ let match;
32
+ while ((match = regex.exec(sanitized)) !== null) {
33
+ const instruction = match[1].trim();
34
+ if (!instruction) {
35
+ continue;
36
+ }
37
+ const segments = expandInstructionSegments(instruction);
38
+ for (const segment of segments) {
39
+ const parsed = parseSingleInstruction(segment);
40
+ if (parsed) {
41
+ instructions.push(parsed);
42
+ }
43
+ }
44
+ }
45
+ return instructions;
46
+ }
47
+ function expandInstructionSegments(instruction) {
48
+ const trimmed = instruction.trim();
49
+ if (!trimmed) {
50
+ return [];
51
+ }
52
+ const prefix = trimmed[0];
53
+ if (prefix === '!' || prefix === '#' || prefix === '@') {
54
+ const tokens = splitInstructionTargets(trimmed.substring(1));
55
+ return tokens
56
+ .map((token) => token.replace(/^[!#@]+/, '').trim())
57
+ .filter((token) => token.length > 0)
58
+ .map((token) => `${prefix}${token}`);
59
+ }
60
+ return splitInstructionTargets(trimmed);
61
+ }
62
+ function splitInstructionTargets(content) {
63
+ return content
64
+ .split(',')
65
+ .map((segment) => segment.trim())
66
+ .filter((segment) => segment.length > 0);
67
+ }
68
+ function parseSingleInstruction(instruction) {
69
+ if (instruction === 'clear') {
70
+ return { type: 'clear' };
71
+ }
72
+ if (instruction.startsWith('!')) {
73
+ const target = instruction.substring(1).trim();
74
+ if (!target) {
75
+ return null;
76
+ }
77
+ const parsed = parseTarget(target);
78
+ if (!parsed) {
79
+ return null;
80
+ }
81
+ // 约定:
82
+ // - "!providerA,providerB":允许列表(whitelist),用于快速限制可用 provider 集合;
83
+ // - "!provider.model" / "!provider.alias.model" / "!provider.2":sticky 语义,按 provider / alias / model 过滤当前路由池。
84
+ //
85
+ // 这样可以在不破坏既有 "!glm,openai" 语义的前提下,引入基于模型 / alias 的 sticky 行为。
86
+ if (!target.includes('.')) {
87
+ if (parsed.provider) {
88
+ return { type: 'allow', provider: parsed.provider, pathLength: parsed.pathLength };
89
+ }
90
+ return null;
91
+ }
92
+ const normalized = normalizeStickyOrForceTarget(parsed);
93
+ return { type: 'sticky', ...normalized };
94
+ }
95
+ else if (instruction.startsWith('#')) {
96
+ const target = instruction.substring(1).trim();
97
+ const parsed = parseTarget(target);
98
+ if (parsed) {
99
+ return { type: 'disable', ...parsed };
100
+ }
101
+ }
102
+ else if (instruction.startsWith('@')) {
103
+ const target = instruction.substring(1).trim();
104
+ const parsed = parseTarget(target);
105
+ if (parsed) {
106
+ return { type: 'enable', ...parsed };
107
+ }
108
+ }
109
+ else if (isValidProviderModel(instruction)) {
110
+ const parsed = parseTarget(instruction);
111
+ if (parsed) {
112
+ const normalized = normalizeStickyOrForceTarget(parsed);
113
+ return { type: 'force', ...normalized };
114
+ }
115
+ }
116
+ return null;
117
+ }
118
+ function parseTarget(target) {
119
+ if (!target) {
120
+ return null;
121
+ }
122
+ const parts = target.split('.');
123
+ const pathLength = parts.length;
124
+ if (parts.length === 0) {
125
+ return null;
126
+ }
127
+ const provider = parts[0];
128
+ if (!provider || !isValidIdentifier(provider)) {
129
+ return null;
130
+ }
131
+ if (parts.length === 1) {
132
+ return { provider, pathLength };
133
+ }
134
+ if (parts.length === 2) {
135
+ const second = parts[1];
136
+ const keyIndex = parseInt(second, 10);
137
+ if (!isNaN(keyIndex) && keyIndex > 0) {
138
+ return { provider, keyIndex, pathLength };
139
+ }
140
+ if (isValidIdentifier(second)) {
141
+ return { provider, model: second, keyAlias: second, pathLength };
142
+ }
143
+ return null;
144
+ }
145
+ if (parts.length === 3) {
146
+ const keyAlias = parts[1];
147
+ const model = parts[2];
148
+ if (isValidIdentifier(keyAlias) && isValidIdentifier(model)) {
149
+ return { provider, keyAlias, model, pathLength };
150
+ }
151
+ return null;
152
+ }
153
+ return null;
154
+ }
155
+ function normalizeStickyOrForceTarget(target) {
156
+ if (target &&
157
+ target.pathLength === 2 &&
158
+ typeof target.model === 'string' &&
159
+ typeof target.keyAlias === 'string' &&
160
+ target.model === target.keyAlias) {
161
+ const clone = { ...target };
162
+ delete clone.keyAlias;
163
+ return clone;
164
+ }
165
+ return target;
166
+ }
167
+ function isValidIdentifier(id) {
168
+ return /^[a-zA-Z0-9_-]+$/.test(id);
169
+ }
170
+ function isValidProviderModel(providerModel) {
171
+ const pattern = /^[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)+$/;
172
+ return pattern.test(providerModel);
173
+ }
174
+ function stripCodeSegments(text) {
175
+ if (!text) {
176
+ return '';
177
+ }
178
+ // Remove fenced code blocks ```...``` or ~~~...~~~
179
+ let sanitized = text.replace(/```[\s\S]*?```/g, ' ');
180
+ sanitized = sanitized.replace(/~~~[\s\S]*?~~~/g, ' ');
181
+ // Remove inline code `...`
182
+ sanitized = sanitized.replace(/`[^`]*`/g, ' ');
183
+ return sanitized;
184
+ }
185
+ export function applyRoutingInstructions(instructions, currentState) {
186
+ const newState = {
187
+ forcedTarget: currentState.forcedTarget ? { ...currentState.forcedTarget } : undefined,
188
+ stickyTarget: currentState.stickyTarget ? { ...currentState.stickyTarget } : undefined,
189
+ allowedProviders: new Set(currentState.allowedProviders),
190
+ disabledProviders: new Set(currentState.disabledProviders),
191
+ disabledKeys: new Map(Array.from(currentState.disabledKeys.entries()).map(([k, v]) => [k, new Set(v)])),
192
+ disabledModels: new Map(Array.from(currentState.disabledModels.entries()).map(([k, v]) => [k, new Set(v)]))
193
+ };
194
+ let allowReset = false;
195
+ let disableReset = false;
196
+ for (const instruction of instructions) {
197
+ switch (instruction.type) {
198
+ case 'force':
199
+ newState.forcedTarget = {
200
+ provider: instruction.provider,
201
+ keyAlias: instruction.keyAlias,
202
+ keyIndex: instruction.keyIndex,
203
+ model: instruction.model,
204
+ pathLength: instruction.pathLength
205
+ };
206
+ // 保留 stickyTarget,允许单次 force 覆盖但不清除持久 sticky
207
+ // newState.stickyTarget = undefined;
208
+ break;
209
+ case 'sticky':
210
+ newState.stickyTarget = {
211
+ provider: instruction.provider,
212
+ keyAlias: instruction.keyAlias,
213
+ keyIndex: instruction.keyIndex,
214
+ model: instruction.model,
215
+ pathLength: instruction.pathLength
216
+ };
217
+ newState.forcedTarget = undefined;
218
+ break;
219
+ case 'allow':
220
+ if (!allowReset) {
221
+ newState.allowedProviders.clear();
222
+ allowReset = true;
223
+ }
224
+ if (instruction.provider) {
225
+ newState.allowedProviders.add(instruction.provider);
226
+ }
227
+ break;
228
+ case 'disable': {
229
+ if (!disableReset) {
230
+ newState.disabledProviders.clear();
231
+ newState.disabledKeys.clear();
232
+ newState.disabledModels.clear();
233
+ disableReset = true;
234
+ }
235
+ if (instruction.provider) {
236
+ const hasKeySpecifier = instruction.keyAlias || instruction.keyIndex !== undefined;
237
+ const hasModelSpecifier = typeof instruction.model === 'string' && instruction.model.length > 0;
238
+ if (hasKeySpecifier) {
239
+ if (!newState.disabledKeys.has(instruction.provider)) {
240
+ newState.disabledKeys.set(instruction.provider, new Set());
241
+ }
242
+ const keySet = newState.disabledKeys.get(instruction.provider);
243
+ if (instruction.keyAlias) {
244
+ keySet.add(instruction.keyAlias);
245
+ }
246
+ if (instruction.keyIndex !== undefined) {
247
+ keySet.add(instruction.keyIndex);
248
+ }
249
+ }
250
+ if (hasModelSpecifier) {
251
+ if (!newState.disabledModels.has(instruction.provider)) {
252
+ newState.disabledModels.set(instruction.provider, new Set());
253
+ }
254
+ newState.disabledModels.get(instruction.provider).add(instruction.model);
255
+ }
256
+ if (!hasKeySpecifier && !hasModelSpecifier) {
257
+ newState.disabledProviders.add(instruction.provider);
258
+ }
259
+ }
260
+ break;
261
+ }
262
+ case 'enable': {
263
+ if (instruction.provider) {
264
+ const hasKeySpecifier = instruction.keyAlias || instruction.keyIndex !== undefined;
265
+ const hasModelSpecifier = typeof instruction.model === 'string' && instruction.model.length > 0;
266
+ if (hasKeySpecifier) {
267
+ const keySet = newState.disabledKeys.get(instruction.provider);
268
+ if (keySet) {
269
+ if (instruction.keyAlias) {
270
+ keySet.delete(instruction.keyAlias);
271
+ }
272
+ if (instruction.keyIndex !== undefined) {
273
+ keySet.delete(instruction.keyIndex);
274
+ }
275
+ if (keySet.size === 0) {
276
+ newState.disabledKeys.delete(instruction.provider);
277
+ }
278
+ }
279
+ }
280
+ if (hasModelSpecifier) {
281
+ const modelSet = newState.disabledModels.get(instruction.provider);
282
+ if (modelSet) {
283
+ modelSet.delete(instruction.model);
284
+ if (modelSet.size === 0) {
285
+ newState.disabledModels.delete(instruction.provider);
286
+ }
287
+ }
288
+ }
289
+ if (!hasKeySpecifier && !hasModelSpecifier) {
290
+ newState.disabledProviders.delete(instruction.provider);
291
+ newState.disabledKeys.delete(instruction.provider);
292
+ newState.disabledModels.delete(instruction.provider);
293
+ }
294
+ }
295
+ break;
296
+ }
297
+ case 'clear':
298
+ newState.forcedTarget = undefined;
299
+ newState.stickyTarget = undefined;
300
+ newState.allowedProviders.clear();
301
+ newState.disabledProviders.clear();
302
+ newState.disabledKeys.clear();
303
+ newState.disabledModels.clear();
304
+ break;
305
+ }
306
+ }
307
+ return newState;
308
+ }
309
+ export function cleanMessagesFromRoutingInstructions(messages) {
310
+ return messages
311
+ .map((message) => {
312
+ if (message.role !== 'user' || typeof message.content !== 'string') {
313
+ return message;
314
+ }
315
+ const cleanedContent = message.content.replace(/<\*\*[^*]+\*\*>/g, '').trim();
316
+ return {
317
+ ...message,
318
+ content: cleanedContent
319
+ };
320
+ })
321
+ .filter((message) => {
322
+ if (message.role !== 'user') {
323
+ return true;
324
+ }
325
+ if (typeof message.content !== 'string') {
326
+ return true;
327
+ }
328
+ return message.content.trim().length > 0;
329
+ });
330
+ }
331
+ export function serializeRoutingInstructionState(state) {
332
+ return {
333
+ forcedTarget: state.forcedTarget,
334
+ stickyTarget: state.stickyTarget,
335
+ allowedProviders: Array.from(state.allowedProviders),
336
+ disabledProviders: Array.from(state.disabledProviders),
337
+ disabledKeys: Array.from(state.disabledKeys.entries()).map(([provider, keys]) => ({
338
+ provider,
339
+ keys: Array.from(keys)
340
+ })),
341
+ disabledModels: Array.from(state.disabledModels.entries()).map(([provider, models]) => ({
342
+ provider,
343
+ models: Array.from(models)
344
+ }))
345
+ };
346
+ }
347
+ export function deserializeRoutingInstructionState(data) {
348
+ const state = {
349
+ forcedTarget: undefined,
350
+ stickyTarget: undefined,
351
+ allowedProviders: new Set(),
352
+ disabledProviders: new Set(),
353
+ disabledKeys: new Map(),
354
+ disabledModels: new Map()
355
+ };
356
+ if (data.forcedTarget && typeof data.forcedTarget === 'object') {
357
+ state.forcedTarget = data.forcedTarget;
358
+ }
359
+ if (data.stickyTarget && typeof data.stickyTarget === 'object') {
360
+ state.stickyTarget = data.stickyTarget;
361
+ }
362
+ if (Array.isArray(data.allowedProviders)) {
363
+ state.allowedProviders = new Set(data.allowedProviders);
364
+ }
365
+ if (Array.isArray(data.disabledProviders)) {
366
+ state.disabledProviders = new Set(data.disabledProviders);
367
+ }
368
+ if (Array.isArray(data.disabledKeys)) {
369
+ for (const entry of data.disabledKeys) {
370
+ if (entry.provider && Array.isArray(entry.keys)) {
371
+ state.disabledKeys.set(entry.provider, new Set(entry.keys));
372
+ }
373
+ }
374
+ }
375
+ if (Array.isArray(data.disabledModels)) {
376
+ for (const entry of data.disabledModels) {
377
+ if (entry.provider && Array.isArray(entry.models)) {
378
+ state.disabledModels.set(entry.provider, new Set(entry.models));
379
+ }
380
+ }
381
+ }
382
+ return state;
383
+ }
@@ -0,0 +1,3 @@
1
+ import type { RoutingInstructionState } from './routing-instructions.js';
2
+ export declare function loadRoutingInstructionStateSync(key: string | undefined): RoutingInstructionState | null;
3
+ export declare function saveRoutingInstructionStateAsync(key: string | undefined, state: RoutingInstructionState | null): void;
@@ -0,0 +1,110 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import os from 'node:os';
4
+ import { serializeRoutingInstructionState, deserializeRoutingInstructionState } from './routing-instructions.js';
5
+ function isPersistentKey(key) {
6
+ if (!key)
7
+ return false;
8
+ return key.startsWith('session:') || key.startsWith('conversation:');
9
+ }
10
+ function resolveSessionDir() {
11
+ try {
12
+ const override = process.env.ROUTECODEX_SESSION_DIR;
13
+ if (override && override.trim()) {
14
+ return override.trim();
15
+ }
16
+ const home = os.homedir();
17
+ if (!home) {
18
+ return null;
19
+ }
20
+ return path.join(home, '.routecodex', 'sessions');
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ function keyToFilename(key) {
27
+ if (!isPersistentKey(key)) {
28
+ return null;
29
+ }
30
+ const idx = key.indexOf(':');
31
+ if (idx <= 0 || idx === key.length - 1) {
32
+ return null;
33
+ }
34
+ const scope = key.substring(0, idx); // "session" | "conversation"
35
+ const rawId = key.substring(idx + 1);
36
+ const safeId = rawId.replace(/[^a-zA-Z0-9_.-]/g, '_');
37
+ return `${scope}-${safeId}.json`;
38
+ }
39
+ export function loadRoutingInstructionStateSync(key) {
40
+ if (!isPersistentKey(key)) {
41
+ return null;
42
+ }
43
+ const dir = resolveSessionDir();
44
+ const filename = keyToFilename(key);
45
+ if (!dir || !filename) {
46
+ return null;
47
+ }
48
+ const filepath = path.join(dir, filename);
49
+ try {
50
+ if (!fs.existsSync(filepath)) {
51
+ return null;
52
+ }
53
+ const raw = fs.readFileSync(filepath, 'utf8');
54
+ if (!raw) {
55
+ return null;
56
+ }
57
+ const parsed = JSON.parse(raw);
58
+ const payload = parsed && typeof parsed.version === 'number'
59
+ ? parsed.state
60
+ : parsed;
61
+ if (!payload || typeof payload !== 'object') {
62
+ return null;
63
+ }
64
+ return deserializeRoutingInstructionState(payload);
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ export function saveRoutingInstructionStateAsync(key, state) {
71
+ if (!isPersistentKey(key)) {
72
+ return;
73
+ }
74
+ const dir = resolveSessionDir();
75
+ const filename = keyToFilename(key);
76
+ if (!dir || !filename) {
77
+ return;
78
+ }
79
+ const filepath = path.join(dir, filename);
80
+ // 空状态意味着清除持久化文件
81
+ if (!state) {
82
+ try {
83
+ fs.unlink(filepath, () => {
84
+ // ignore errors (e.g. ENOENT)
85
+ });
86
+ }
87
+ catch {
88
+ // ignore sync unlink failures
89
+ }
90
+ return;
91
+ }
92
+ const payload = {
93
+ version: 1,
94
+ state: serializeRoutingInstructionState(state)
95
+ };
96
+ try {
97
+ fs.mkdirSync(dir, { recursive: true });
98
+ }
99
+ catch {
100
+ // ignore mkdir errors; write below will fail silently
101
+ }
102
+ try {
103
+ fs.writeFile(filepath, JSON.stringify(payload), { encoding: 'utf8' }, () => {
104
+ // ignore async write errors
105
+ });
106
+ }
107
+ catch {
108
+ // ignore sync write failures
109
+ }
110
+ }