@jsonstudio/llms 0.6.753 → 0.6.802
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/conversion/compat/actions/apply-patch-fixer.d.ts +1 -0
- package/dist/conversion/compat/actions/apply-patch-fixer.js +30 -0
- package/dist/conversion/compat/actions/apply-patch-format-fixer.d.ts +1 -0
- package/dist/conversion/compat/actions/apply-patch-format-fixer.js +233 -0
- package/dist/conversion/compat/actions/index.d.ts +2 -0
- package/dist/conversion/compat/actions/index.js +2 -0
- package/dist/conversion/compat/profiles/chat-gemini.json +15 -15
- package/dist/conversion/compat/profiles/chat-glm.json +194 -194
- package/dist/conversion/compat/profiles/chat-iflow.json +199 -199
- 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/compat/profiles/responses-output2choices-test.json +10 -9
- package/dist/conversion/hub/pipeline/context-limit.d.ts +13 -0
- package/dist/conversion/hub/pipeline/context-limit.js +55 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +6 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +35 -0
- package/dist/conversion/shared/bridge-message-utils.d.ts +1 -0
- package/dist/conversion/shared/bridge-message-utils.js +7 -0
- package/dist/conversion/shared/bridge-policies.js +8 -8
- package/dist/conversion/shared/snapshot-hooks.js +54 -1
- package/dist/conversion/shared/tool-governor.js +18 -23
- package/dist/filters/special/response-tool-arguments-stringify.js +3 -22
- package/dist/router/virtual-router/engine-selection.js +49 -4
- package/dist/router/virtual-router/engine.d.ts +5 -0
- package/dist/router/virtual-router/engine.js +21 -0
- package/dist/tools/apply-patch/regression-capturer.d.ts +12 -0
- package/dist/tools/apply-patch/regression-capturer.js +112 -0
- package/dist/tools/apply-patch/structured.d.ts +20 -0
- package/dist/tools/apply-patch/structured.js +441 -0
- package/dist/tools/apply-patch/validator.d.ts +8 -0
- package/dist/tools/apply-patch/validator.js +616 -0
- package/dist/tools/apply-patch-structured.d.ts +1 -20
- package/dist/tools/apply-patch-structured.js +1 -277
- package/dist/tools/args-json.d.ts +1 -0
- package/dist/tools/args-json.js +175 -0
- package/dist/tools/exec-command/normalize.d.ts +17 -0
- package/dist/tools/exec-command/normalize.js +112 -0
- package/dist/tools/exec-command/regression-capturer.d.ts +11 -0
- package/dist/tools/exec-command/regression-capturer.js +144 -0
- package/dist/tools/exec-command/validator.d.ts +6 -0
- package/dist/tools/exec-command/validator.js +22 -0
- package/dist/tools/patch-args-normalizer.d.ts +15 -0
- package/dist/tools/patch-args-normalizer.js +472 -0
- package/dist/tools/patch-regression-capturer.d.ts +1 -0
- package/dist/tools/patch-regression-capturer.js +1 -0
- package/dist/tools/tool-registry.js +36 -541
- package/package.json +1 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
"id": "responses:output2choices-test",
|
|
3
|
+
"protocol": "openai-responses",
|
|
4
|
+
"response": {
|
|
5
|
+
"mappings": [
|
|
6
|
+
{
|
|
7
|
+
"action": "convert_responses_output_to_choices"
|
|
8
|
+
}
|
|
9
|
+
]
|
|
10
|
+
}
|
|
11
11
|
}
|
|
12
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ProcessedRequest, StandardizedRequest } from '../types/standardized.js';
|
|
2
|
+
import type { TargetMetadata } from '../../../router/virtual-router/types.js';
|
|
3
|
+
export declare function enforceTargetContextLimitOrThrow(options: {
|
|
4
|
+
requestId: string;
|
|
5
|
+
routeName?: string;
|
|
6
|
+
target: TargetMetadata;
|
|
7
|
+
request: StandardizedRequest | ProcessedRequest;
|
|
8
|
+
}): {
|
|
9
|
+
estimatedInputTokens?: number;
|
|
10
|
+
maxContextTokens?: number;
|
|
11
|
+
allowedTokens?: number;
|
|
12
|
+
safetyRatio?: number;
|
|
13
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { computeRequestTokens } from '../../../router/virtual-router/token-estimator.js';
|
|
2
|
+
function readSafetyRatioFromEnv() {
|
|
3
|
+
const raw = process?.env?.RCC_CONTEXT_TOKEN_SAFETY_RATIO ??
|
|
4
|
+
process?.env?.ROUTECODEX_CONTEXT_TOKEN_SAFETY_RATIO ??
|
|
5
|
+
'';
|
|
6
|
+
const value = Number(raw);
|
|
7
|
+
if (!Number.isFinite(value)) {
|
|
8
|
+
return 0;
|
|
9
|
+
}
|
|
10
|
+
// Keep within sane bounds; default is 0 (exact limit).
|
|
11
|
+
return Math.max(0, Math.min(0.5, value));
|
|
12
|
+
}
|
|
13
|
+
export function enforceTargetContextLimitOrThrow(options) {
|
|
14
|
+
const maxContextTokensRaw = options.target?.maxContextTokens;
|
|
15
|
+
const maxContextTokens = typeof maxContextTokensRaw === 'number' && Number.isFinite(maxContextTokensRaw) && maxContextTokensRaw > 0
|
|
16
|
+
? Math.floor(maxContextTokensRaw)
|
|
17
|
+
: undefined;
|
|
18
|
+
if (!maxContextTokens) {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
const estimatedInputTokens = computeRequestTokens(options.request, '');
|
|
22
|
+
if (!(typeof estimatedInputTokens === 'number' && Number.isFinite(estimatedInputTokens) && estimatedInputTokens > 0)) {
|
|
23
|
+
return { maxContextTokens };
|
|
24
|
+
}
|
|
25
|
+
const safetyRatio = readSafetyRatioFromEnv();
|
|
26
|
+
const allowedTokens = Math.max(1, Math.floor(maxContextTokens * (1 - safetyRatio)));
|
|
27
|
+
if (estimatedInputTokens >= allowedTokens) {
|
|
28
|
+
const providerKey = options.target?.providerKey || 'unknown';
|
|
29
|
+
const modelId = options.target?.modelId || options.request?.model || 'unknown';
|
|
30
|
+
const routeName = options.routeName || 'unknown';
|
|
31
|
+
const message = `Context too long for ${providerKey}.${modelId}: ` +
|
|
32
|
+
`estimatedInputTokens=${estimatedInputTokens} exceeds allowed=${allowedTokens} ` +
|
|
33
|
+
`(maxContextTokens=${maxContextTokens}, safetyRatio=${safetyRatio}, route=${routeName})`;
|
|
34
|
+
const err = Object.assign(new Error(message), {
|
|
35
|
+
name: 'ContextLimitError',
|
|
36
|
+
code: 'CONTEXT_TOO_LONG',
|
|
37
|
+
status: 400,
|
|
38
|
+
requestId: options.requestId,
|
|
39
|
+
providerKey: options.target?.providerKey,
|
|
40
|
+
providerType: options.target?.providerType,
|
|
41
|
+
routeName: options.routeName,
|
|
42
|
+
details: {
|
|
43
|
+
estimatedInputTokens,
|
|
44
|
+
allowedTokens,
|
|
45
|
+
maxContextTokens,
|
|
46
|
+
safetyRatio,
|
|
47
|
+
providerKey: options.target?.providerKey,
|
|
48
|
+
modelId,
|
|
49
|
+
routeName
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
54
|
+
return { estimatedInputTokens, maxContextTokens, allowedTokens, safetyRatio };
|
|
55
|
+
}
|
|
@@ -64,7 +64,13 @@ export declare class HubPipeline {
|
|
|
64
64
|
private config;
|
|
65
65
|
private unsubscribeProviderErrors?;
|
|
66
66
|
constructor(config: HubPipelineConfig);
|
|
67
|
+
updateRuntimeDeps(deps: {
|
|
68
|
+
healthStore?: HubPipelineConfig['healthStore'] | null;
|
|
69
|
+
routingStateStore?: HubPipelineConfig['routingStateStore'] | null;
|
|
70
|
+
quotaView?: HubPipelineConfig['quotaView'] | null;
|
|
71
|
+
}): void;
|
|
67
72
|
updateVirtualRouterConfig(nextConfig: VirtualRouterConfig): void;
|
|
73
|
+
dispose(): void;
|
|
68
74
|
private executeRequestStagePipeline;
|
|
69
75
|
execute(request: HubPipelineRequest): Promise<HubPipelineResult>;
|
|
70
76
|
private captureAnthropicAliasMap;
|
|
@@ -51,6 +51,30 @@ export class HubPipeline {
|
|
|
51
51
|
this.unsubscribeProviderErrors = undefined;
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
+
updateRuntimeDeps(deps) {
|
|
55
|
+
if (!deps || typeof deps !== 'object') {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if ('healthStore' in deps) {
|
|
59
|
+
this.config.healthStore = deps.healthStore ?? undefined;
|
|
60
|
+
}
|
|
61
|
+
if ('routingStateStore' in deps) {
|
|
62
|
+
this.config.routingStateStore = (deps.routingStateStore ?? undefined);
|
|
63
|
+
}
|
|
64
|
+
if ('quotaView' in deps) {
|
|
65
|
+
this.config.quotaView = deps.quotaView ?? undefined;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
this.routerEngine.updateDeps({
|
|
69
|
+
healthStore: this.config.healthStore ?? null,
|
|
70
|
+
routingStateStore: (this.config.routingStateStore ?? null),
|
|
71
|
+
quotaView: this.config.quotaView ?? null
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// best-effort: runtime deps updates must never break routing
|
|
76
|
+
}
|
|
77
|
+
}
|
|
54
78
|
updateVirtualRouterConfig(nextConfig) {
|
|
55
79
|
if (!nextConfig || typeof nextConfig !== 'object') {
|
|
56
80
|
throw new Error('HubPipeline updateVirtualRouterConfig requires VirtualRouterConfig payload');
|
|
@@ -58,6 +82,17 @@ export class HubPipeline {
|
|
|
58
82
|
this.config.virtualRouter = nextConfig;
|
|
59
83
|
this.routerEngine.initialize(nextConfig);
|
|
60
84
|
}
|
|
85
|
+
dispose() {
|
|
86
|
+
if (this.unsubscribeProviderErrors) {
|
|
87
|
+
try {
|
|
88
|
+
this.unsubscribeProviderErrors();
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// ignore dispose failures
|
|
92
|
+
}
|
|
93
|
+
this.unsubscribeProviderErrors = undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
61
96
|
async executeRequestStagePipeline(normalized, hooks) {
|
|
62
97
|
const formatAdapter = hooks.createFormatAdapter();
|
|
63
98
|
const semanticMapper = hooks.createSemanticMapper();
|
|
@@ -20,3 +20,4 @@ export interface BridgeInputToChatOptions {
|
|
|
20
20
|
toolResultFallbackText?: string;
|
|
21
21
|
}
|
|
22
22
|
export declare function convertBridgeInputToChatMessages(options: BridgeInputToChatOptions): Array<Record<string, unknown>>;
|
|
23
|
+
export declare function ensureMessagesArray(state: any): Array<Record<string, unknown>>;
|
|
@@ -644,3 +644,10 @@ export function convertBridgeInputToChatMessages(options) {
|
|
|
644
644
|
}
|
|
645
645
|
return messages;
|
|
646
646
|
}
|
|
647
|
+
export function ensureMessagesArray(state) {
|
|
648
|
+
if (Array.isArray(state.messages))
|
|
649
|
+
return state.messages;
|
|
650
|
+
if (!state.messages)
|
|
651
|
+
state.messages = [];
|
|
652
|
+
return state.messages;
|
|
653
|
+
}
|
|
@@ -171,7 +171,7 @@ const OPENAI_CHAT_POLICY = {
|
|
|
171
171
|
request: {
|
|
172
172
|
inbound: [
|
|
173
173
|
reasoningAction('openai_chat_reasoning'),
|
|
174
|
-
toolCallNormalizationAction('openai_chat_tool_call'),
|
|
174
|
+
toolCallNormalizationAction('openai_chat_tool_call'), { name: 'compat.fix-apply-patch' },
|
|
175
175
|
{ name: 'messages.ensure-system-instruction' },
|
|
176
176
|
{ name: 'metadata.extra-fields', options: { allowedKeys: OPENAI_CHAT_ALLOWED_FIELDS } },
|
|
177
177
|
{ name: 'metadata.provider-field', options: { field: 'metadata', target: 'providerMetadata' } },
|
|
@@ -180,7 +180,7 @@ const OPENAI_CHAT_POLICY = {
|
|
|
180
180
|
outbound: [
|
|
181
181
|
{ name: 'messages.normalize-history' },
|
|
182
182
|
{ name: 'tools.capture-results' },
|
|
183
|
-
toolCallNormalizationAction('openai_chat_tool_call'),
|
|
183
|
+
toolCallNormalizationAction('openai_chat_tool_call'), { name: 'compat.fix-apply-patch' },
|
|
184
184
|
{ name: 'tools.ensure-placeholders' },
|
|
185
185
|
{ name: 'messages.ensure-output-fields', options: { toolFallback: 'Tool call completed (no output).' } },
|
|
186
186
|
{ name: 'messages.ensure-system-instruction' },
|
|
@@ -193,12 +193,12 @@ const OPENAI_CHAT_POLICY = {
|
|
|
193
193
|
response: {
|
|
194
194
|
inbound: [
|
|
195
195
|
reasoningAction('openai_chat_reasoning'),
|
|
196
|
-
toolCallNormalizationAction('openai_chat_tool_call'),
|
|
196
|
+
toolCallNormalizationAction('openai_chat_tool_call'), { name: 'compat.fix-apply-patch' },
|
|
197
197
|
{ name: 'metadata.extra-fields', options: { allowedKeys: OPENAI_CHAT_ALLOWED_FIELDS } }
|
|
198
198
|
],
|
|
199
199
|
outbound: [
|
|
200
200
|
reasoningAction('openai_chat_reasoning'),
|
|
201
|
-
toolCallNormalizationAction('openai_chat_tool_call'),
|
|
201
|
+
toolCallNormalizationAction('openai_chat_tool_call'), { name: 'compat.fix-apply-patch' },
|
|
202
202
|
{ name: 'metadata.extra-fields', options: { allowedKeys: OPENAI_CHAT_ALLOWED_FIELDS } }
|
|
203
203
|
]
|
|
204
204
|
}
|
|
@@ -242,7 +242,7 @@ const GEMINI_POLICY = {
|
|
|
242
242
|
protocol: 'gemini-chat',
|
|
243
243
|
request: {
|
|
244
244
|
inbound: [
|
|
245
|
-
reasoningAction('gemini_reasoning'),
|
|
245
|
+
reasoningAction('gemini_reasoning'), { name: 'compat.fix-apply-patch' },
|
|
246
246
|
{ name: 'messages.ensure-system-instruction' },
|
|
247
247
|
{ name: 'metadata.extra-fields', options: { allowedKeys: GEMINI_ALLOWED_FIELDS } }
|
|
248
248
|
],
|
|
@@ -252,17 +252,17 @@ const GEMINI_POLICY = {
|
|
|
252
252
|
{ name: 'tools.ensure-placeholders' },
|
|
253
253
|
{ name: 'messages.ensure-output-fields', options: { toolFallback: 'Tool call completed (no output).' } },
|
|
254
254
|
{ name: 'messages.ensure-system-instruction' },
|
|
255
|
-
reasoningAction('gemini_reasoning'),
|
|
255
|
+
reasoningAction('gemini_reasoning'), { name: 'compat.fix-apply-patch' },
|
|
256
256
|
{ name: 'metadata.extra-fields', options: { allowedKeys: GEMINI_ALLOWED_FIELDS } }
|
|
257
257
|
]
|
|
258
258
|
},
|
|
259
259
|
response: {
|
|
260
260
|
inbound: [
|
|
261
|
-
reasoningAction('gemini_reasoning'),
|
|
261
|
+
reasoningAction('gemini_reasoning'), { name: 'compat.fix-apply-patch' },
|
|
262
262
|
{ name: 'metadata.extra-fields', options: { allowedKeys: GEMINI_ALLOWED_FIELDS } }
|
|
263
263
|
],
|
|
264
264
|
outbound: [
|
|
265
|
-
reasoningAction('gemini_reasoning'),
|
|
265
|
+
reasoningAction('gemini_reasoning'), { name: 'compat.fix-apply-patch' },
|
|
266
266
|
{ name: 'metadata.extra-fields', options: { allowedKeys: GEMINI_ALLOWED_FIELDS } }
|
|
267
267
|
]
|
|
268
268
|
}
|
|
@@ -110,6 +110,59 @@ function extractNestedGroupRequestId(value) {
|
|
|
110
110
|
}
|
|
111
111
|
return undefined;
|
|
112
112
|
}
|
|
113
|
+
function extractNestedEntryEndpoint(value) {
|
|
114
|
+
if (!value || typeof value !== 'object') {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
const obj = value;
|
|
118
|
+
const direct = readStringField(obj.entryEndpoint) ||
|
|
119
|
+
readStringField(obj.entry_endpoint) ||
|
|
120
|
+
readStringField(obj.endpoint);
|
|
121
|
+
if (direct) {
|
|
122
|
+
return direct;
|
|
123
|
+
}
|
|
124
|
+
const meta = obj.meta;
|
|
125
|
+
if (meta && typeof meta === 'object') {
|
|
126
|
+
const m = meta;
|
|
127
|
+
const fromMeta = readStringField(m.entryEndpoint) ||
|
|
128
|
+
readStringField(m.entry_endpoint) ||
|
|
129
|
+
readStringField(m.endpoint);
|
|
130
|
+
if (fromMeta) {
|
|
131
|
+
return fromMeta;
|
|
132
|
+
}
|
|
133
|
+
const ctx = m.context;
|
|
134
|
+
if (ctx && typeof ctx === 'object') {
|
|
135
|
+
const c = ctx;
|
|
136
|
+
const fromCtx = readStringField(c.entryEndpoint) ||
|
|
137
|
+
readStringField(c.entry_endpoint) ||
|
|
138
|
+
readStringField(c.endpoint);
|
|
139
|
+
if (fromCtx) {
|
|
140
|
+
return fromCtx;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const metadata = obj.metadata;
|
|
145
|
+
if (metadata && typeof metadata === 'object') {
|
|
146
|
+
const md = metadata;
|
|
147
|
+
const fromMetadata = readStringField(md.entryEndpoint) ||
|
|
148
|
+
readStringField(md.entry_endpoint) ||
|
|
149
|
+
readStringField(md.endpoint);
|
|
150
|
+
if (fromMetadata) {
|
|
151
|
+
return fromMetadata;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const runtime = obj.runtime;
|
|
155
|
+
if (runtime && typeof runtime === 'object') {
|
|
156
|
+
const r = runtime;
|
|
157
|
+
const fromRuntime = readStringField(r.entryEndpoint) ||
|
|
158
|
+
readStringField(r.entry_endpoint) ||
|
|
159
|
+
readStringField(r.endpoint);
|
|
160
|
+
if (fromRuntime) {
|
|
161
|
+
return fromRuntime;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
113
166
|
function toErrorCode(error) {
|
|
114
167
|
if (!error || typeof error !== 'object') {
|
|
115
168
|
return undefined;
|
|
@@ -205,7 +258,7 @@ async function promotePendingDir(options) {
|
|
|
205
258
|
}
|
|
206
259
|
async function writeSnapshotFile(options) {
|
|
207
260
|
const root = resolveSnapshotRoot();
|
|
208
|
-
const folder = resolveSnapshotFolder(options.endpoint);
|
|
261
|
+
const folder = resolveSnapshotFolder(extractNestedEntryEndpoint(options.data) || options.endpoint);
|
|
209
262
|
const stageToken = sanitizeToken(options.stage, 'snapshot');
|
|
210
263
|
const groupRequestToken = sanitizeToken(options.groupRequestId ||
|
|
211
264
|
extractNestedGroupRequestId(options.data) ||
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// enforceChatBudget: 为避免在请求侧引入多余依赖,这里提供最小实现(保留形状,不裁剪)。
|
|
5
5
|
import { augmentOpenAITools } from '../../guidance/index.js';
|
|
6
6
|
import { validateToolCall } from '../../tools/tool-registry.js';
|
|
7
|
-
import {
|
|
7
|
+
import { captureApplyPatchRegression } from '../../tools/patch-regression-capturer.js';
|
|
8
|
+
import { normalizeExecCommandArgs } from '../../tools/exec-command/normalize.js';
|
|
8
9
|
function isObject(v) { return !!v && typeof v === 'object' && !Array.isArray(v); }
|
|
9
10
|
// Note: tool schema strict augmentation removed per alignment
|
|
10
11
|
function enforceChatBudget(chat, _modelId) { return chat; }
|
|
@@ -245,6 +246,13 @@ export function normalizeApplyPatchToolCallsOnResponse(chat) {
|
|
|
245
246
|
else if (validation && !validation.ok) {
|
|
246
247
|
try {
|
|
247
248
|
const reason = validation.reason ?? 'unknown';
|
|
249
|
+
captureApplyPatchRegression({
|
|
250
|
+
errorType: reason,
|
|
251
|
+
originalArgs: rawArgs,
|
|
252
|
+
normalizedArgs: argsStr,
|
|
253
|
+
validationError: reason,
|
|
254
|
+
source: 'tool-governor.response'
|
|
255
|
+
});
|
|
248
256
|
const snippet = typeof argsStr === 'string' && argsStr.trim().length
|
|
249
257
|
? argsStr.trim().slice(0, 200).replace(/\s+/g, ' ')
|
|
250
258
|
: '';
|
|
@@ -312,6 +320,13 @@ function normalizeSpecialToolCallsOnRequest(request) {
|
|
|
312
320
|
else if (validation && !validation.ok) {
|
|
313
321
|
try {
|
|
314
322
|
const reason = validation.reason ?? 'unknown';
|
|
323
|
+
captureApplyPatchRegression({
|
|
324
|
+
errorType: reason,
|
|
325
|
+
originalArgs: rawArgs,
|
|
326
|
+
normalizedArgs: argsStr,
|
|
327
|
+
validationError: reason,
|
|
328
|
+
source: 'tool-governor.request'
|
|
329
|
+
});
|
|
315
330
|
const snippet = typeof argsStr === 'string' && argsStr.trim().length
|
|
316
331
|
? argsStr.trim().slice(0, 200).replace(/\s+/g, ' ')
|
|
317
332
|
: '';
|
|
@@ -336,8 +351,9 @@ function normalizeSpecialToolCallsOnRequest(request) {
|
|
|
336
351
|
}
|
|
337
352
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
338
353
|
const normalized = normalizeExecCommandArgs(parsed);
|
|
354
|
+
const next = normalized.ok ? normalized.normalized : parsed;
|
|
339
355
|
try {
|
|
340
|
-
fn.arguments = JSON.stringify(
|
|
356
|
+
fn.arguments = JSON.stringify(next ?? {});
|
|
341
357
|
}
|
|
342
358
|
catch {
|
|
343
359
|
fn.arguments = '{}';
|
|
@@ -356,27 +372,6 @@ function normalizeSpecialToolCallsOnRequest(request) {
|
|
|
356
372
|
return request;
|
|
357
373
|
}
|
|
358
374
|
}
|
|
359
|
-
function normalizeExecCommandArgs(args) {
|
|
360
|
-
try {
|
|
361
|
-
const out = { ...args };
|
|
362
|
-
const rawCmd = typeof out.cmd === 'string' && out.cmd.trim().length
|
|
363
|
-
? String(out.cmd)
|
|
364
|
-
: typeof out.command === 'string' && out.command.trim().length
|
|
365
|
-
? String(out.command)
|
|
366
|
-
: undefined;
|
|
367
|
-
if (rawCmd) {
|
|
368
|
-
const fixed = repairFindMeta(rawCmd);
|
|
369
|
-
out.cmd = fixed;
|
|
370
|
-
if (typeof out.command === 'string') {
|
|
371
|
-
out.command = fixed;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
return out;
|
|
375
|
-
}
|
|
376
|
-
catch {
|
|
377
|
-
return args;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
375
|
function enhanceResponseToolArguments(chat) {
|
|
381
376
|
try {
|
|
382
377
|
const enable = String(process?.env?.RCC_TOOL_ENHANCE ?? '1').trim() !== '0';
|
|
@@ -1,28 +1,8 @@
|
|
|
1
1
|
import { repairFindMeta } from '../../conversion/shared/tooling.js';
|
|
2
|
+
import { normalizeExecCommandArgs } from '../../tools/exec-command/normalize.js';
|
|
2
3
|
function isObject(v) {
|
|
3
4
|
return !!v && typeof v === 'object' && !Array.isArray(v);
|
|
4
5
|
}
|
|
5
|
-
function normalizeExecCommandArgs(args) {
|
|
6
|
-
try {
|
|
7
|
-
const out = { ...args };
|
|
8
|
-
const rawCmd = typeof out.cmd === 'string' && out.cmd.trim().length
|
|
9
|
-
? String(out.cmd)
|
|
10
|
-
: typeof out.command === 'string' && out.command.trim().length
|
|
11
|
-
? String(out.command)
|
|
12
|
-
: undefined;
|
|
13
|
-
if (rawCmd) {
|
|
14
|
-
const fixed = repairFindMeta(rawCmd);
|
|
15
|
-
out.cmd = fixed;
|
|
16
|
-
if (typeof out.command === 'string') {
|
|
17
|
-
out.command = fixed;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
return out;
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
return args;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
6
|
function packShellCommand(cmd) {
|
|
27
7
|
// Normalize into ["bash","-lc","<single string>"] to support pipes, parens, -exec, etc.
|
|
28
8
|
const normalizeArray = (argv) => {
|
|
@@ -104,8 +84,9 @@ export class ResponseToolArgumentsStringifyFilter {
|
|
|
104
84
|
}
|
|
105
85
|
else if ((name === 'exec_command' || name === 'shell_command' || name === 'bash') && isObject(parsed)) {
|
|
106
86
|
const normalized = normalizeExecCommandArgs(parsed);
|
|
87
|
+
const next = normalized.ok ? normalized.normalized : parsed;
|
|
107
88
|
try {
|
|
108
|
-
fn.arguments = JSON.stringify(
|
|
89
|
+
fn.arguments = JSON.stringify(next ?? {});
|
|
109
90
|
}
|
|
110
91
|
catch {
|
|
111
92
|
fn.arguments = '{}';
|
|
@@ -220,7 +220,11 @@ function trySelectFromTier(routeName, tier, stickyKey, estimatedTokens, features
|
|
|
220
220
|
targets = targets.filter((key) => !excludedKeys.has(key));
|
|
221
221
|
}
|
|
222
222
|
if (targets.length > 0) {
|
|
223
|
-
|
|
223
|
+
const cooled = targets.filter((key) => !deps.isProviderCoolingDown(key));
|
|
224
|
+
// 单 provider 兜底:当一个 tier 只有一个候选 key 时,不因 cooldown 造成路由池为空。
|
|
225
|
+
if (cooled.length > 0 || targets.length !== 1) {
|
|
226
|
+
targets = cooled;
|
|
227
|
+
}
|
|
224
228
|
}
|
|
225
229
|
if (allowedProviders && allowedProviders.size > 0) {
|
|
226
230
|
targets = targets.filter((key) => {
|
|
@@ -331,14 +335,22 @@ function trySelectFromTier(routeName, tier, stickyKey, estimatedTokens, features
|
|
|
331
335
|
const selectWithQuota = (candidates) => {
|
|
332
336
|
if (!quotaView) {
|
|
333
337
|
if (tier.mode === 'priority') {
|
|
334
|
-
|
|
338
|
+
const selected = selectFirstAvailable(candidates);
|
|
339
|
+
if (!selected && candidates.length === 1) {
|
|
340
|
+
return candidates[0];
|
|
341
|
+
}
|
|
342
|
+
return selected;
|
|
335
343
|
}
|
|
336
|
-
|
|
344
|
+
const selected = deps.loadBalancer.select({
|
|
337
345
|
routeName: `${routeName}:${tier.id}`,
|
|
338
346
|
candidates,
|
|
339
347
|
stickyKey: options.allowAliasRotation ? undefined : stickyKey,
|
|
340
348
|
availabilityCheck: (key) => deps.healthManager.isAvailable(key)
|
|
341
349
|
}, tier.mode === 'round-robin' ? 'round-robin' : undefined);
|
|
350
|
+
if (!selected && candidates.length === 1) {
|
|
351
|
+
return candidates[0];
|
|
352
|
+
}
|
|
353
|
+
return selected;
|
|
342
354
|
}
|
|
343
355
|
const buckets = new Map();
|
|
344
356
|
for (const key of candidates) {
|
|
@@ -389,6 +401,33 @@ function trySelectFromTier(routeName, tier, stickyKey, estimatedTokens, features
|
|
|
389
401
|
}
|
|
390
402
|
}
|
|
391
403
|
}
|
|
404
|
+
// default 路由永不因 quota gating 而“空池”:
|
|
405
|
+
// 当 quotaView 过滤后没有任何可用候选时,默认路由允许忽略 quotaView,
|
|
406
|
+
// 继续按健康/负载均衡选择一个 providerKey(但不覆盖 forced/required 约束)。
|
|
407
|
+
const quotaBypassAllowed = routeName === DEFAULT_ROUTE && (!requiredProviderKeys || requiredProviderKeys.size === 0);
|
|
408
|
+
if (quotaBypassAllowed) {
|
|
409
|
+
if (tier.mode === 'priority') {
|
|
410
|
+
const selected = selectFirstAvailable(candidates);
|
|
411
|
+
if (selected) {
|
|
412
|
+
return selected;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
const selected = deps.loadBalancer.select({
|
|
417
|
+
routeName: `${routeName}:${tier.id}:quota-bypass`,
|
|
418
|
+
candidates,
|
|
419
|
+
stickyKey: options.allowAliasRotation ? undefined : stickyKey,
|
|
420
|
+
availabilityCheck: (key) => deps.healthManager.isAvailable(key)
|
|
421
|
+
}, tier.mode === 'round-robin' ? 'round-robin' : undefined);
|
|
422
|
+
if (selected) {
|
|
423
|
+
return selected;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
// 单 provider 兜底:当只剩一个候选 key 时,不因 quota/blacklist/cooldown 或健康状态过滤导致无 provider。
|
|
428
|
+
if (candidates.length === 1) {
|
|
429
|
+
return candidates[0];
|
|
430
|
+
}
|
|
392
431
|
return null;
|
|
393
432
|
};
|
|
394
433
|
for (const candidatePool of prioritizedPools) {
|
|
@@ -416,10 +455,13 @@ export function selectFromStickyPool(stickyKeySet, metadata, features, state, de
|
|
|
416
455
|
]));
|
|
417
456
|
const disabledModels = new Map(Array.from(state.disabledModels.entries()).map(([provider, models]) => [provider, new Set(models)]));
|
|
418
457
|
let candidates = Array.from(stickyKeySet).filter((key) => !deps.isProviderCoolingDown(key));
|
|
458
|
+
if (!candidates.length && stickyKeySet.size === 1) {
|
|
459
|
+
candidates = Array.from(stickyKeySet);
|
|
460
|
+
}
|
|
419
461
|
const quotaView = deps.quotaView;
|
|
420
462
|
const now = quotaView ? Date.now() : 0;
|
|
421
463
|
if (quotaView) {
|
|
422
|
-
|
|
464
|
+
const filtered = candidates.filter((key) => {
|
|
423
465
|
const entry = quotaView(key);
|
|
424
466
|
if (!entry) {
|
|
425
467
|
return true;
|
|
@@ -435,6 +477,9 @@ export function selectFromStickyPool(stickyKeySet, metadata, features, state, de
|
|
|
435
477
|
}
|
|
436
478
|
return true;
|
|
437
479
|
});
|
|
480
|
+
if (filtered.length > 0 || candidates.length !== 1) {
|
|
481
|
+
candidates = filtered;
|
|
482
|
+
}
|
|
438
483
|
}
|
|
439
484
|
if (allowedProviders.size > 0) {
|
|
440
485
|
candidates = candidates.filter((key) => {
|
|
@@ -29,6 +29,11 @@ export declare class VirtualRouterEngine {
|
|
|
29
29
|
routingStateStore?: RoutingInstructionStateStore;
|
|
30
30
|
quotaView?: ProviderQuotaView;
|
|
31
31
|
});
|
|
32
|
+
updateDeps(deps: {
|
|
33
|
+
healthStore?: VirtualRouterHealthStore | null;
|
|
34
|
+
routingStateStore?: RoutingInstructionStateStore | null;
|
|
35
|
+
quotaView?: ProviderQuotaView | null;
|
|
36
|
+
}): void;
|
|
32
37
|
private parseDirectProviderModel;
|
|
33
38
|
initialize(config: VirtualRouterConfig): void;
|
|
34
39
|
route(request: StandardizedRequest | ProcessedRequest, metadata: RouterMetadataInput): {
|
|
@@ -44,6 +44,27 @@ export class VirtualRouterEngine {
|
|
|
44
44
|
this.quotaView = deps.quotaView;
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
updateDeps(deps) {
|
|
48
|
+
if (!deps || typeof deps !== 'object') {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if ('healthStore' in deps) {
|
|
52
|
+
this.healthStore = deps.healthStore ?? undefined;
|
|
53
|
+
}
|
|
54
|
+
if ('routingStateStore' in deps) {
|
|
55
|
+
this.routingStateStore =
|
|
56
|
+
deps.routingStateStore ??
|
|
57
|
+
{
|
|
58
|
+
loadSync: loadRoutingInstructionStateSync,
|
|
59
|
+
saveAsync: saveRoutingInstructionStateAsync
|
|
60
|
+
};
|
|
61
|
+
// Routing state store changes require clearing in-memory cache to avoid stale reads.
|
|
62
|
+
this.routingInstructionState.clear();
|
|
63
|
+
}
|
|
64
|
+
if ('quotaView' in deps) {
|
|
65
|
+
this.quotaView = deps.quotaView ?? undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
47
68
|
parseDirectProviderModel(model) {
|
|
48
69
|
const raw = typeof model === 'string' ? model.trim() : '';
|
|
49
70
|
if (!raw) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface RegressionSample {
|
|
2
|
+
id: string;
|
|
3
|
+
timestamp: string;
|
|
4
|
+
errorType: string;
|
|
5
|
+
originalArgs: string;
|
|
6
|
+
normalizedArgs?: string;
|
|
7
|
+
fixerResult?: string;
|
|
8
|
+
validationError?: string;
|
|
9
|
+
source?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function captureApplyPatchRegression(sample: Omit<RegressionSample, 'id' | 'timestamp'>): void;
|
|
12
|
+
export {};
|