@jsonstudio/llms 0.6.375 → 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 (37) hide show
  1. package/dist/conversion/codecs/gemini-openai-codec.js +15 -1
  2. package/dist/conversion/compat/actions/iflow-web-search.d.ts +18 -0
  3. package/dist/conversion/compat/actions/iflow-web-search.js +87 -0
  4. package/dist/conversion/compat/profiles/chat-glm.json +4 -0
  5. package/dist/conversion/compat/profiles/chat-iflow.json +5 -1
  6. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
  7. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +2 -0
  8. package/dist/conversion/hub/pipeline/hub-pipeline.js +5 -1
  9. package/dist/conversion/hub/pipeline/session-identifiers.d.ts +9 -0
  10. package/dist/conversion/hub/pipeline/session-identifiers.js +76 -0
  11. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +31 -2
  12. package/dist/conversion/hub/process/chat-process.js +89 -25
  13. package/dist/conversion/responses/responses-openai-bridge.js +75 -4
  14. package/dist/conversion/shared/anthropic-message-utils.js +41 -6
  15. package/dist/conversion/shared/errors.d.ts +20 -0
  16. package/dist/conversion/shared/errors.js +28 -0
  17. package/dist/conversion/shared/responses-conversation-store.js +30 -3
  18. package/dist/conversion/shared/responses-output-builder.js +68 -6
  19. package/dist/filters/special/request-toolcalls-stringify.d.ts +13 -0
  20. package/dist/filters/special/request-toolcalls-stringify.js +103 -3
  21. package/dist/filters/special/response-tool-text-canonicalize.d.ts +16 -0
  22. package/dist/filters/special/response-tool-text-canonicalize.js +27 -3
  23. package/dist/router/virtual-router/classifier.js +4 -2
  24. package/dist/router/virtual-router/engine.d.ts +30 -0
  25. package/dist/router/virtual-router/engine.js +600 -42
  26. package/dist/router/virtual-router/provider-registry.d.ts +15 -0
  27. package/dist/router/virtual-router/provider-registry.js +40 -0
  28. package/dist/router/virtual-router/routing-instructions.d.ts +34 -0
  29. package/dist/router/virtual-router/routing-instructions.js +383 -0
  30. package/dist/router/virtual-router/sticky-session-store.d.ts +3 -0
  31. package/dist/router/virtual-router/sticky-session-store.js +110 -0
  32. package/dist/router/virtual-router/tool-signals.js +0 -22
  33. package/dist/router/virtual-router/types.d.ts +35 -0
  34. package/dist/servertool/engine.js +42 -1
  35. package/dist/servertool/handlers/web-search.js +157 -4
  36. package/dist/servertool/types.d.ts +6 -0
  37. package/package.json +1 -1
@@ -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);
@@ -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
+ }
@@ -85,22 +85,6 @@ const SHELL_WRITE_PATTERNS = [
85
85
  'go install',
86
86
  'make install'
87
87
  ];
88
- const SHELL_SEARCH_PATTERNS = [
89
- 'rg ',
90
- 'rg-',
91
- 'grep ',
92
- 'grep-',
93
- 'ripgrep',
94
- 'find ',
95
- 'fd ',
96
- 'locate ',
97
- 'search ',
98
- 'ack ',
99
- 'ag ',
100
- 'where ',
101
- 'which ',
102
- 'codesearch'
103
- ];
104
88
  const SHELL_READ_PATTERNS = [
105
89
  'ls',
106
90
  'dir ',
@@ -362,9 +346,6 @@ function classifyShellCommand(command) {
362
346
  if (segments.some((segment) => matchesAnyPattern(segment, SHELL_WRITE_PATTERNS))) {
363
347
  return 'write';
364
348
  }
365
- if (segments.some((segment) => matchesAnyPattern(segment, SHELL_SEARCH_PATTERNS))) {
366
- return 'search';
367
- }
368
349
  if (segments.some((segment) => matchesAnyPattern(segment, SHELL_READ_PATTERNS))) {
369
350
  return 'read';
370
351
  }
@@ -372,9 +353,6 @@ function classifyShellCommand(command) {
372
353
  if (matchesAnyPattern(stripped, SHELL_WRITE_PATTERNS)) {
373
354
  return 'write';
374
355
  }
375
- if (matchesAnyPattern(stripped, SHELL_SEARCH_PATTERNS)) {
376
- return 'search';
377
- }
378
356
  if (matchesAnyPattern(stripped, SHELL_READ_PATTERNS)) {
379
357
  return 'read';
380
358
  }
@@ -5,6 +5,7 @@ import type { StandardizedRequest } from '../../conversion/hub/types/standardize
5
5
  export declare const DEFAULT_MODEL_CONTEXT_TOKENS = 200000;
6
6
  export declare const DEFAULT_ROUTE = "default";
7
7
  export declare const ROUTE_PRIORITY: string[];
8
+ export type RoutingInstructionMode = 'force' | 'sticky' | 'none';
8
9
  export interface RoutePoolTier {
9
10
  id: string;
10
11
  targets: string[];
@@ -167,6 +168,40 @@ export interface RouterMetadataInput {
167
168
  * serverToolsDisabled when this flag is true.
168
169
  */
169
170
  serverToolRequired?: boolean;
171
+ /**
172
+ * 强制路由模式,从消息中的 <**...**> 指令解析得出
173
+ */
174
+ routingMode?: RoutingInstructionMode;
175
+ /**
176
+ * 允许的 provider 白名单
177
+ */
178
+ allowedProviders?: string[];
179
+ /**
180
+ * 强制使用的 provider model (格式: provider.model)
181
+ */
182
+ forcedProviderModel?: string;
183
+ /**
184
+ * 强制使用的 provider keyAlias
185
+ */
186
+ forcedProviderKeyAlias?: string;
187
+ /**
188
+ * 强制使用的 provider keyIndex (从 1 开始)
189
+ */
190
+ forcedProviderKeyIndex?: number;
191
+ /**
192
+ * 禁用的 provider model 列表
193
+ */
194
+ disabledProviderModels?: string[];
195
+ /**
196
+ * 禁用的 provider keyAlias 列表
197
+ */
198
+ disabledProviderKeyAliases?: string[];
199
+ /**
200
+ * 禁用的 provider keyIndex 列表 (从 1 开始)
201
+ */
202
+ disabledProviderKeyIndexes?: number[];
203
+ sessionId?: string;
204
+ conversationId?: string;
170
205
  responsesResume?: {
171
206
  previousRequestId?: string;
172
207
  restoredFromResponseId?: string;