@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.
- package/README.md +2 -0
- package/dist/conversion/codecs/gemini-openai-codec.js +24 -2
- package/dist/conversion/compat/actions/gemini-web-search.d.ts +17 -0
- package/dist/conversion/compat/actions/gemini-web-search.js +68 -0
- package/dist/conversion/compat/actions/glm-image-content.d.ts +2 -0
- package/dist/conversion/compat/actions/glm-image-content.js +83 -0
- package/dist/conversion/compat/actions/glm-vision-prompt.d.ts +11 -0
- package/dist/conversion/compat/actions/glm-vision-prompt.js +177 -0
- package/dist/conversion/compat/actions/glm-web-search.js +25 -28
- package/dist/conversion/compat/actions/iflow-web-search.d.ts +18 -0
- package/dist/conversion/compat/actions/iflow-web-search.js +87 -0
- package/dist/conversion/compat/actions/universal-shape-filter.js +11 -0
- package/dist/conversion/compat/profiles/chat-gemini.json +17 -0
- package/dist/conversion/compat/profiles/chat-glm.json +194 -184
- package/dist/conversion/compat/profiles/chat-iflow.json +199 -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/config/sample-config.json +1 -1
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +24 -0
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +8 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +32 -1
- package/dist/conversion/hub/pipeline/session-identifiers.d.ts +9 -0
- package/dist/conversion/hub/pipeline/session-identifiers.js +76 -0
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +31 -2
- package/dist/conversion/hub/pipeline/target-utils.js +6 -0
- package/dist/conversion/hub/process/chat-process.js +186 -40
- package/dist/conversion/hub/response/provider-response.d.ts +13 -1
- package/dist/conversion/hub/response/provider-response.js +84 -35
- package/dist/conversion/hub/response/server-side-tools.js +61 -4
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +123 -3
- package/dist/conversion/hub/semantic-mappers/responses-mapper.js +17 -1
- package/dist/conversion/hub/standardized-bridge.js +14 -0
- package/dist/conversion/responses/responses-openai-bridge.js +110 -6
- package/dist/conversion/shared/anthropic-message-utils.js +133 -9
- package/dist/conversion/shared/bridge-message-utils.js +137 -10
- package/dist/conversion/shared/errors.d.ts +20 -0
- package/dist/conversion/shared/errors.js +28 -0
- package/dist/conversion/shared/responses-conversation-store.js +30 -3
- package/dist/conversion/shared/responses-output-builder.js +111 -8
- package/dist/conversion/shared/tool-filter-pipeline.js +1 -0
- package/dist/filters/special/request-toolcalls-stringify.d.ts +13 -0
- package/dist/filters/special/request-toolcalls-stringify.js +103 -3
- package/dist/filters/special/response-tool-text-canonicalize.d.ts +16 -0
- package/dist/filters/special/response-tool-text-canonicalize.js +27 -3
- package/dist/router/virtual-router/bootstrap.js +44 -12
- package/dist/router/virtual-router/classifier.js +13 -17
- package/dist/router/virtual-router/engine.d.ts +39 -0
- package/dist/router/virtual-router/engine.js +755 -55
- package/dist/router/virtual-router/features.js +1 -1
- package/dist/router/virtual-router/message-utils.js +36 -24
- package/dist/router/virtual-router/provider-registry.d.ts +15 -0
- package/dist/router/virtual-router/provider-registry.js +42 -1
- package/dist/router/virtual-router/routing-instructions.d.ts +34 -0
- package/dist/router/virtual-router/routing-instructions.js +383 -0
- package/dist/router/virtual-router/sticky-session-store.d.ts +3 -0
- package/dist/router/virtual-router/sticky-session-store.js +110 -0
- package/dist/router/virtual-router/token-counter.js +14 -3
- package/dist/router/virtual-router/tool-signals.js +0 -22
- package/dist/router/virtual-router/types.d.ts +80 -0
- package/dist/router/virtual-router/types.js +2 -1
- package/dist/servertool/engine.d.ts +27 -0
- package/dist/servertool/engine.js +101 -0
- package/dist/servertool/flow-types.d.ts +40 -0
- package/dist/servertool/flow-types.js +1 -0
- package/dist/servertool/handlers/vision.d.ts +1 -0
- package/dist/servertool/handlers/vision.js +194 -0
- package/dist/servertool/handlers/web-search.d.ts +1 -0
- package/dist/servertool/handlers/web-search.js +791 -0
- package/dist/servertool/orchestration-types.d.ts +33 -0
- package/dist/servertool/orchestration-types.js +1 -0
- package/dist/servertool/registry.d.ts +18 -0
- package/dist/servertool/registry.js +27 -0
- package/dist/servertool/server-side-tools.d.ts +8 -0
- package/dist/servertool/server-side-tools.js +208 -0
- package/dist/servertool/types.d.ts +94 -0
- package/dist/servertool/types.js +1 -0
- package/dist/servertool/vision-tool.d.ts +2 -0
- package/dist/servertool/vision-tool.js +185 -0
- package/dist/sse/sse-to-json/builders/response-builder.js +6 -3
- 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 =
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
+
}
|