responses-proxy 0.1.0
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 +56 -0
- package/cli.js +118 -0
- package/dist/anthropic-messages.js +383 -0
- package/dist/anthropic-messages.test.js +209 -0
- package/dist/audit-log.js +138 -0
- package/dist/audit-log.test.js +480 -0
- package/dist/billing-expiration.js +70 -0
- package/dist/billing-expiration.test.js +114 -0
- package/dist/billing.js +716 -0
- package/dist/billing.test.js +228 -0
- package/dist/chatgpt-oauth-store.js +240 -0
- package/dist/chatgpt-oauth-store.test.js +88 -0
- package/dist/chatgpt-oauth.js +118 -0
- package/dist/chatgpt-oauth.test.js +63 -0
- package/dist/chatgpt-provider-auth.js +60 -0
- package/dist/chatgpt-provider-auth.test.js +101 -0
- package/dist/client/app-icon.svg +17 -0
- package/dist/client/assets/index-C7Vvhst8.js +14 -0
- package/dist/client/assets/index-DpqgYK3L.css +1 -0
- package/dist/client/favicon.svg +17 -0
- package/dist/client/index.html +31 -0
- package/dist/client-config-apply.js +345 -0
- package/dist/client-config-apply.test.js +185 -0
- package/dist/client-token-limits.js +111 -0
- package/dist/client-token-limits.test.js +129 -0
- package/dist/codex-config.js +47 -0
- package/dist/codex-setup.js +87 -0
- package/dist/codex-setup.test.js +30 -0
- package/dist/config.js +314 -0
- package/dist/cost-analytics.js +31 -0
- package/dist/cost-analytics.test.js +38 -0
- package/dist/customer-key-access.js +126 -0
- package/dist/customer-key-access.test.js +178 -0
- package/dist/customer-keys.js +209 -0
- package/dist/customer-keys.test.js +68 -0
- package/dist/customer-usage.js +18 -0
- package/dist/customer-usage.test.js +55 -0
- package/dist/dashboard-auth.js +318 -0
- package/dist/dashboard-auth.test.js +133 -0
- package/dist/dashboard-serving.test.js +235 -0
- package/dist/error-response.js +174 -0
- package/dist/error-response.test.js +88 -0
- package/dist/forward.js +357 -0
- package/dist/health-websocket-manager.js +174 -0
- package/dist/http-rate-limit.js +36 -0
- package/dist/http-rate-limit.test.js +62 -0
- package/dist/kiro-auth.js +136 -0
- package/dist/kiro-auth.test.js +234 -0
- package/dist/kiro-codewhisperer.js +646 -0
- package/dist/kiro-codewhisperer.test.js +219 -0
- package/dist/kiro-device-login.js +338 -0
- package/dist/kiro-eventstream.js +219 -0
- package/dist/kiro-eventstream.test.js +79 -0
- package/dist/kiro-forward.js +401 -0
- package/dist/kiro-import-cli.js +69 -0
- package/dist/kiro-import.js +94 -0
- package/dist/kiro-import.test.js +125 -0
- package/dist/kiro-token-store.js +196 -0
- package/dist/kiro-token-store.test.js +207 -0
- package/dist/krouter-usage.js +243 -0
- package/dist/model-combo-repository.js +147 -0
- package/dist/model-routing.js +69 -0
- package/dist/model-routing.test.js +41 -0
- package/dist/normalize-request.js +531 -0
- package/dist/normalize-request.test.js +277 -0
- package/dist/omv-public-firewall.test.js +11 -0
- package/dist/package.json +17 -0
- package/dist/prompt-cache-state.js +146 -0
- package/dist/prompt-cache-state.test.js +71 -0
- package/dist/prompt-cache.js +229 -0
- package/dist/provider-health-service.js +404 -0
- package/dist/provider-request-parameters.js +107 -0
- package/dist/provider-request-parameters.test.js +26 -0
- package/dist/provider-routing.js +114 -0
- package/dist/provider-routing.test.js +64 -0
- package/dist/provider-usage.js +314 -0
- package/dist/request-timeout-policy.js +61 -0
- package/dist/request-timeout-policy.test.js +40 -0
- package/dist/response-cache.js +69 -0
- package/dist/response-cache.test.js +28 -0
- package/dist/routing-combo-repository.js +300 -0
- package/dist/routing-engine.js +377 -0
- package/dist/routing-integration.js +155 -0
- package/dist/routing-simulation-engine.js +326 -0
- package/dist/rtk-layer.js +483 -0
- package/dist/rtk-layer.test.js +198 -0
- package/dist/runtime-provider-repository.js +1742 -0
- package/dist/runtime-provider-repository.test.js +1177 -0
- package/dist/schema.js +118 -0
- package/dist/schema.test.js +16 -0
- package/dist/sepay-webhook.js +87 -0
- package/dist/sepay-webhook.test.js +142 -0
- package/dist/server-body-limit.test.js +35 -0
- package/dist/server-client-token-limits.test.js +161 -0
- package/dist/server-codex-config-setup.test.js +76 -0
- package/dist/server-http-rate-limit.test.js +80 -0
- package/dist/server-response-cache.test.js +105 -0
- package/dist/server-routes-alias.test.js +39 -0
- package/dist/server-sepay-webhook-security.test.js +59 -0
- package/dist/server.js +5906 -0
- package/dist/session-log.js +178 -0
- package/dist/tailnet-funnel-script.test.js +33 -0
- package/dist/telegram-bot/actions.js +118 -0
- package/dist/telegram-bot/admin-actions.js +103 -0
- package/dist/telegram-bot/auth.js +46 -0
- package/dist/telegram-bot/auth.test.js +1 -0
- package/dist/telegram-bot/bot-identity-repository.js +189 -0
- package/dist/telegram-bot/bot-identity-repository.test.js +78 -0
- package/dist/telegram-bot/callbacks.js +30 -0
- package/dist/telegram-bot/codex-config-delivery.js +38 -0
- package/dist/telegram-bot/codex-config-delivery.test.js +75 -0
- package/dist/telegram-bot/commands/accounts.js +140 -0
- package/dist/telegram-bot/commands/apikey.js +737 -0
- package/dist/telegram-bot/commands/apply.js +265 -0
- package/dist/telegram-bot/commands/clients.js +13 -0
- package/dist/telegram-bot/commands/customer-billing.test.js +271 -0
- package/dist/telegram-bot/commands/grant.js +138 -0
- package/dist/telegram-bot/commands/grant.test.js +217 -0
- package/dist/telegram-bot/commands/help.js +52 -0
- package/dist/telegram-bot/commands/me.js +53 -0
- package/dist/telegram-bot/commands/models.js +6 -0
- package/dist/telegram-bot/commands/oauth.js +64 -0
- package/dist/telegram-bot/commands/plans.js +96 -0
- package/dist/telegram-bot/commands/providers.js +27 -0
- package/dist/telegram-bot/commands/quota.js +10 -0
- package/dist/telegram-bot/commands/renew-user.js +139 -0
- package/dist/telegram-bot/commands/renew-user.test.js +184 -0
- package/dist/telegram-bot/commands/renew.js +1369 -0
- package/dist/telegram-bot/commands/renew.test.js +1633 -0
- package/dist/telegram-bot/commands/start.js +212 -0
- package/dist/telegram-bot/commands/start.test.js +280 -0
- package/dist/telegram-bot/commands/status.js +6 -0
- package/dist/telegram-bot/commands/tailscale.js +15 -0
- package/dist/telegram-bot/commands/tailscale.test.js +76 -0
- package/dist/telegram-bot/commands/test.js +51 -0
- package/dist/telegram-bot/commands/test.test.js +14 -0
- package/dist/telegram-bot/commands/usage.js +10 -0
- package/dist/telegram-bot/config.js +98 -0
- package/dist/telegram-bot/config.test.js +42 -0
- package/dist/telegram-bot/customer-actions.js +160 -0
- package/dist/telegram-bot/customer-api-keys.js +68 -0
- package/dist/telegram-bot/customer-billing.js +72 -0
- package/dist/telegram-bot/customer-workspace-repository.js +134 -0
- package/dist/telegram-bot/customer-workspace-repository.test.js +47 -0
- package/dist/telegram-bot/dashboard-login.js +39 -0
- package/dist/telegram-bot/format.js +140 -0
- package/dist/telegram-bot/grants.js +370 -0
- package/dist/telegram-bot/grants.test.js +290 -0
- package/dist/telegram-bot/index.js +85 -0
- package/dist/telegram-bot/message-cleanup.js +55 -0
- package/dist/telegram-bot/message-cleanup.test.js +77 -0
- package/dist/telegram-bot/message-format.js +45 -0
- package/dist/telegram-bot/message-format.test.js +10 -0
- package/dist/telegram-bot/proxy-client.js +174 -0
- package/dist/telegram-bot/rate-limit.js +95 -0
- package/dist/telegram-bot/rate-limit.test.js +58 -0
- package/dist/telegram-bot/sessions.js +171 -0
- package/dist/telegram-bot/sessions.test.js +107 -0
- package/dist/telegram-bot/telegram-adapter.js +126 -0
- package/dist/telegram-bot/worker.js +63 -0
- package/package.json +39 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
const ANSI_PATTERN = /\u001b\[[0-9;]*m/g;
|
|
2
|
+
const DEFAULT_MAX_CHARS = 4_000;
|
|
3
|
+
const DEFAULT_MAX_LINES = 120;
|
|
4
|
+
const DEFAULT_DETECT_FORMAT = "auto";
|
|
5
|
+
export function cloneRtkLayerPolicy(policy) {
|
|
6
|
+
if (!policy) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
enabled: typeof policy.enabled === "boolean" ? policy.enabled : undefined,
|
|
11
|
+
toolOutputEnabled: typeof policy.toolOutputEnabled === "boolean" ? policy.toolOutputEnabled : undefined,
|
|
12
|
+
maxChars: typeof policy.maxChars === "number" && Number.isFinite(policy.maxChars) && policy.maxChars > 0
|
|
13
|
+
? Math.round(policy.maxChars)
|
|
14
|
+
: undefined,
|
|
15
|
+
maxLines: typeof policy.maxLines === "number" && Number.isFinite(policy.maxLines) && policy.maxLines > 0
|
|
16
|
+
? Math.round(policy.maxLines)
|
|
17
|
+
: undefined,
|
|
18
|
+
tailLines: typeof policy.tailLines === "number" &&
|
|
19
|
+
Number.isFinite(policy.tailLines) &&
|
|
20
|
+
policy.tailLines >= 0
|
|
21
|
+
? Math.round(policy.tailLines)
|
|
22
|
+
: undefined,
|
|
23
|
+
tailChars: typeof policy.tailChars === "number" &&
|
|
24
|
+
Number.isFinite(policy.tailChars) &&
|
|
25
|
+
policy.tailChars >= 0
|
|
26
|
+
? Math.round(policy.tailChars)
|
|
27
|
+
: undefined,
|
|
28
|
+
detectFormat: normalizeDetectFormat(policy.detectFormat),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function parseRtkLayerPolicyInput(value) {
|
|
32
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const record = value;
|
|
36
|
+
const policy = cloneRtkLayerPolicy({
|
|
37
|
+
enabled: coerceBoolean(record.enabled),
|
|
38
|
+
toolOutputEnabled: coerceBoolean(record.toolOutputEnabled ?? record.tool_output_enabled),
|
|
39
|
+
maxChars: coercePositiveInt(record.maxChars ?? record.max_chars),
|
|
40
|
+
maxLines: coercePositiveInt(record.maxLines ?? record.max_lines),
|
|
41
|
+
tailLines: coerceNonNegativeInt(record.tailLines ?? record.tail_lines),
|
|
42
|
+
tailChars: coerceNonNegativeInt(record.tailChars ?? record.tail_chars),
|
|
43
|
+
detectFormat: normalizeDetectFormat(record.detectFormat ?? record.detect_format),
|
|
44
|
+
});
|
|
45
|
+
if (!policy) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
if (policy.enabled === undefined &&
|
|
49
|
+
policy.toolOutputEnabled === undefined &&
|
|
50
|
+
policy.maxChars === undefined &&
|
|
51
|
+
policy.maxLines === undefined &&
|
|
52
|
+
policy.tailLines === undefined &&
|
|
53
|
+
policy.tailChars === undefined &&
|
|
54
|
+
policy.detectFormat === undefined) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
return policy;
|
|
58
|
+
}
|
|
59
|
+
export function resolveRtkLayerPolicy(base, providerPolicy, clientPolicy) {
|
|
60
|
+
const merged = {
|
|
61
|
+
...base,
|
|
62
|
+
...compactRtkLayerPolicy(cloneRtkLayerPolicy(providerPolicy)),
|
|
63
|
+
...compactRtkLayerPolicy(cloneRtkLayerPolicy(clientPolicy)),
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
enabled: merged.enabled,
|
|
67
|
+
toolOutputEnabled: merged.toolOutputEnabled,
|
|
68
|
+
maxChars: merged.maxChars ?? DEFAULT_MAX_CHARS,
|
|
69
|
+
maxLines: merged.maxLines ?? DEFAULT_MAX_LINES,
|
|
70
|
+
tailLines: merged.tailLines ?? 0,
|
|
71
|
+
tailChars: merged.tailChars,
|
|
72
|
+
detectFormat: normalizeDetectFormat(merged.detectFormat) ?? DEFAULT_DETECT_FORMAT,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function compactRtkLayerPolicy(policy) {
|
|
76
|
+
if (!policy) {
|
|
77
|
+
return {};
|
|
78
|
+
}
|
|
79
|
+
return Object.fromEntries(Object.entries(policy).filter(([, value]) => value !== undefined));
|
|
80
|
+
}
|
|
81
|
+
export function mergeRtkLayerPolicies(base, providerPolicy, clientPolicy) {
|
|
82
|
+
return resolveRtkLayerPolicy(base, providerPolicy, clientPolicy);
|
|
83
|
+
}
|
|
84
|
+
export function applyRtkLayer(body, options = {}) {
|
|
85
|
+
const stats = emptyStats(options.enabled === true);
|
|
86
|
+
if (!options.enabled || !options.toolOutputEnabled) {
|
|
87
|
+
return { body, stats };
|
|
88
|
+
}
|
|
89
|
+
let applied = false;
|
|
90
|
+
const nextBody = {
|
|
91
|
+
...body,
|
|
92
|
+
};
|
|
93
|
+
if (Array.isArray(body.messages)) {
|
|
94
|
+
nextBody.messages = body.messages.map((message) => {
|
|
95
|
+
if (message.role !== "tool") {
|
|
96
|
+
return message;
|
|
97
|
+
}
|
|
98
|
+
const nextContent = transformMessageToolContent(message.content, stats, options);
|
|
99
|
+
if (nextContent === message.content) {
|
|
100
|
+
return message;
|
|
101
|
+
}
|
|
102
|
+
applied = true;
|
|
103
|
+
return {
|
|
104
|
+
...message,
|
|
105
|
+
content: nextContent,
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (Array.isArray(body.input)) {
|
|
110
|
+
nextBody.input = body.input.map((item) => {
|
|
111
|
+
if (!isFunctionCallOutputItem(item)) {
|
|
112
|
+
return item;
|
|
113
|
+
}
|
|
114
|
+
const transformed = transformToolOutput(item.output, stats, options);
|
|
115
|
+
if (transformed === item.output) {
|
|
116
|
+
return item;
|
|
117
|
+
}
|
|
118
|
+
applied = true;
|
|
119
|
+
return {
|
|
120
|
+
...item,
|
|
121
|
+
output: transformed,
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
stats.applied = applied;
|
|
126
|
+
stats.charsSaved = Math.max(0, stats.charsBefore - stats.charsAfter);
|
|
127
|
+
return {
|
|
128
|
+
body: applied ? nextBody : body,
|
|
129
|
+
stats,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function emptyStats(enabled) {
|
|
133
|
+
return {
|
|
134
|
+
enabled,
|
|
135
|
+
applied: false,
|
|
136
|
+
toolOutputsSeen: 0,
|
|
137
|
+
toolOutputsReduced: 0,
|
|
138
|
+
charsBefore: 0,
|
|
139
|
+
charsAfter: 0,
|
|
140
|
+
charsSaved: 0,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function transformMessageToolContent(content, stats, options) {
|
|
144
|
+
if (typeof content === "string") {
|
|
145
|
+
return transformToolOutput(content, stats, options);
|
|
146
|
+
}
|
|
147
|
+
if (!Array.isArray(content)) {
|
|
148
|
+
return content;
|
|
149
|
+
}
|
|
150
|
+
let changed = false;
|
|
151
|
+
const next = content.map((part) => {
|
|
152
|
+
if (!isRecord(part) || part.type !== "text" || typeof part.text !== "string") {
|
|
153
|
+
return part;
|
|
154
|
+
}
|
|
155
|
+
const text = transformToolOutput(part.text, stats, options);
|
|
156
|
+
if (text !== part.text) {
|
|
157
|
+
changed = true;
|
|
158
|
+
return {
|
|
159
|
+
...part,
|
|
160
|
+
text,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return part;
|
|
164
|
+
});
|
|
165
|
+
return changed ? next : content;
|
|
166
|
+
}
|
|
167
|
+
function transformToolOutput(raw, stats, options) {
|
|
168
|
+
stats.toolOutputsSeen += 1;
|
|
169
|
+
stats.charsBefore += raw.length;
|
|
170
|
+
const maxChars = options.maxChars ?? DEFAULT_MAX_CHARS;
|
|
171
|
+
const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
|
|
172
|
+
const normalized = canonicalizeToolOutput(raw);
|
|
173
|
+
const detectedFormat = detectToolOutputFormat(normalized, options.detectFormat);
|
|
174
|
+
const formatReady = detectedFormat === "json" ? prettifyJsonToolOutput(normalized) ?? normalized : normalized;
|
|
175
|
+
const reduced = clipToolOutput(formatReady, maxChars, maxLines, options.tailLines ?? 0, options.tailChars, detectedFormat);
|
|
176
|
+
stats.charsAfter += reduced.length;
|
|
177
|
+
if (reduced !== raw) {
|
|
178
|
+
stats.toolOutputsReduced += 1;
|
|
179
|
+
}
|
|
180
|
+
return reduced;
|
|
181
|
+
}
|
|
182
|
+
function canonicalizeToolOutput(raw) {
|
|
183
|
+
const lines = raw
|
|
184
|
+
.replace(/\r\n/g, "\n")
|
|
185
|
+
.replace(ANSI_PATTERN, "")
|
|
186
|
+
.split("\n")
|
|
187
|
+
.map((line) => line.replace(/[ \t]+$/g, ""))
|
|
188
|
+
.filter((line, index, all) => {
|
|
189
|
+
if (line.length > 0) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
return index > 0 && index < all.length - 1;
|
|
193
|
+
});
|
|
194
|
+
const deduped = [];
|
|
195
|
+
let previous;
|
|
196
|
+
for (const line of lines) {
|
|
197
|
+
if (line === previous && line.trim().length > 0) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
deduped.push(line);
|
|
201
|
+
previous = line;
|
|
202
|
+
}
|
|
203
|
+
return deduped.join("\n").trim();
|
|
204
|
+
}
|
|
205
|
+
function clipToolOutput(value, maxChars, maxLines, tailLines, tailChars, detectedFormat) {
|
|
206
|
+
const lines = value ? value.split("\n") : [];
|
|
207
|
+
if (lines.length <= maxLines && value.length <= maxChars) {
|
|
208
|
+
return value;
|
|
209
|
+
}
|
|
210
|
+
const safeTailLines = Math.max(0, Math.min(tailLines, maxLines > 1 ? maxLines - 1 : 0));
|
|
211
|
+
const initialHeadLineBudget = Math.max(1, maxLines - safeTailLines);
|
|
212
|
+
const headLines = lines.slice(0, initialHeadLineBudget);
|
|
213
|
+
const tailStartIndex = Math.max(initialHeadLineBudget, lines.length - safeTailLines);
|
|
214
|
+
const tail = safeTailLines > 0 ? lines.slice(tailStartIndex) : [];
|
|
215
|
+
const importantLines = extractImportantLines(lines, detectedFormat, initialHeadLineBudget, tailStartIndex, maxLines);
|
|
216
|
+
const marker = formatAwareTruncationMarker(detectedFormat);
|
|
217
|
+
const selectedLines = buildSelectedLines(headLines, importantLines, tail, marker);
|
|
218
|
+
let clipped = selectedLines.join("\n");
|
|
219
|
+
const visibleLines = countVisibleSourceLines(selectedLines, marker);
|
|
220
|
+
const hiddenLineCount = Math.max(0, lines.length - visibleLines);
|
|
221
|
+
const summary = ["", buildTruncationSummary(detectedFormat, hiddenLineCount, Math.max(0, value.length - Math.min(value.length, clipped.length)))].join("\n");
|
|
222
|
+
const summaryBudget = Math.max(0, maxChars - summary.length);
|
|
223
|
+
clipped = applySmartCharBudget(clipped, headLines.join("\n"), importantLines.join("\n"), tail.join("\n"), marker, summaryBudget, tailChars, detectedFormat);
|
|
224
|
+
const hiddenCharCount = Math.max(0, value.length - clipped.length);
|
|
225
|
+
const finalizedSummary = ["", buildTruncationSummary(detectedFormat, hiddenLineCount, hiddenCharCount)].join("\n");
|
|
226
|
+
const finalBudget = Math.max(0, maxChars - finalizedSummary.length);
|
|
227
|
+
const head = applySmartCharBudget(clipped, headLines.join("\n"), importantLines.join("\n"), tail.join("\n"), marker, finalBudget, tailChars, detectedFormat);
|
|
228
|
+
return `${head}${finalizedSummary}`.trim();
|
|
229
|
+
}
|
|
230
|
+
function applySmartCharBudget(fallbackClipped, headText, importantText, tailText, marker, availableChars, tailChars, detectedFormat) {
|
|
231
|
+
if (availableChars <= 0) {
|
|
232
|
+
return "";
|
|
233
|
+
}
|
|
234
|
+
if (!tailText && !importantText) {
|
|
235
|
+
return fallbackClipped.slice(0, availableChars).trimEnd();
|
|
236
|
+
}
|
|
237
|
+
const importantSeparatorBudget = importantText ? marker.length + 1 : 0;
|
|
238
|
+
const tailSeparatorBudget = tailText ? marker.length + 1 : 0;
|
|
239
|
+
const separatorBudget = importantSeparatorBudget + tailSeparatorBudget;
|
|
240
|
+
if (availableChars <= separatorBudget + 8) {
|
|
241
|
+
return fallbackClipped.slice(0, availableChars).trimEnd();
|
|
242
|
+
}
|
|
243
|
+
const autoTailRatio = detectedFormat === "stack"
|
|
244
|
+
? 0.45
|
|
245
|
+
: detectedFormat === "command"
|
|
246
|
+
? 0.38
|
|
247
|
+
: detectedFormat === "json"
|
|
248
|
+
? 0.25
|
|
249
|
+
: 0.3;
|
|
250
|
+
const minTailChars = detectedFormat === "stack" ? 24 : detectedFormat === "json" ? 20 : 16;
|
|
251
|
+
const importantBudget = importantText.length > 0
|
|
252
|
+
? Math.min(importantText.length, Math.max(24, Math.round(availableChars * (detectedFormat === "command" ? 0.34 : 0.18))))
|
|
253
|
+
: 0;
|
|
254
|
+
const desiredTailChars = typeof tailChars === "number" && tailChars >= 0
|
|
255
|
+
? tailChars
|
|
256
|
+
: Math.round(availableChars * autoTailRatio);
|
|
257
|
+
const boundedTailChars = Math.max(0, Math.min(tailText.length, Math.max(minTailChars, desiredTailChars), Math.max(0, availableChars - separatorBudget - 16)));
|
|
258
|
+
const headBudget = Math.max(0, availableChars - separatorBudget - boundedTailChars - importantBudget);
|
|
259
|
+
const boundedHead = clipStartText(headText, headBudget);
|
|
260
|
+
const boundedImportant = detectedFormat === "command"
|
|
261
|
+
? clipStartText(importantText, importantBudget)
|
|
262
|
+
: clipEndText(importantText, importantBudget);
|
|
263
|
+
const boundedTail = clipEndText(tailText, boundedTailChars);
|
|
264
|
+
let finalHead = boundedHead;
|
|
265
|
+
let finalImportant = boundedImportant;
|
|
266
|
+
let composite = buildBudgetComposite(finalHead, finalImportant, boundedTail, marker);
|
|
267
|
+
if (composite.length > availableChars && finalHead.length > 0) {
|
|
268
|
+
const overflow = composite.length - availableChars;
|
|
269
|
+
finalHead = clipStartText(finalHead, Math.max(0, finalHead.length - overflow));
|
|
270
|
+
composite = buildBudgetComposite(finalHead, finalImportant, boundedTail, marker);
|
|
271
|
+
}
|
|
272
|
+
if (composite.length > availableChars && finalImportant.length > 0) {
|
|
273
|
+
const overflow = composite.length - availableChars;
|
|
274
|
+
finalImportant =
|
|
275
|
+
detectedFormat === "command"
|
|
276
|
+
? clipStartText(finalImportant, Math.max(0, finalImportant.length - overflow))
|
|
277
|
+
: clipEndText(finalImportant, Math.max(0, finalImportant.length - overflow));
|
|
278
|
+
composite = buildBudgetComposite(finalHead, finalImportant, boundedTail, marker);
|
|
279
|
+
}
|
|
280
|
+
return composite.length <= availableChars
|
|
281
|
+
? composite
|
|
282
|
+
: composite.slice(0, availableChars).trimEnd();
|
|
283
|
+
}
|
|
284
|
+
function buildBudgetComposite(headText, importantText, tailText, marker) {
|
|
285
|
+
return [headText, importantText ? marker : "", importantText, tailText ? marker : "", tailText]
|
|
286
|
+
.filter(Boolean)
|
|
287
|
+
.join("\n");
|
|
288
|
+
}
|
|
289
|
+
function clipStartText(value, maxChars) {
|
|
290
|
+
if (maxChars <= 0) {
|
|
291
|
+
return "";
|
|
292
|
+
}
|
|
293
|
+
return value.length <= maxChars ? value : value.slice(0, maxChars).trimEnd();
|
|
294
|
+
}
|
|
295
|
+
function clipEndText(value, maxChars) {
|
|
296
|
+
if (maxChars <= 0) {
|
|
297
|
+
return "";
|
|
298
|
+
}
|
|
299
|
+
return value.length <= maxChars ? value : value.slice(Math.max(0, value.length - maxChars)).trimStart();
|
|
300
|
+
}
|
|
301
|
+
function extractImportantLines(lines, detectedFormat, headEndIndexExclusive, tailStartIndexInclusive, maxLines) {
|
|
302
|
+
if (detectedFormat !== "command" && detectedFormat !== "plain") {
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
const middle = lines.slice(headEndIndexExclusive, tailStartIndexInclusive);
|
|
306
|
+
if (middle.length === 0) {
|
|
307
|
+
return [];
|
|
308
|
+
}
|
|
309
|
+
const candidates = middle
|
|
310
|
+
.map((line, index) => ({
|
|
311
|
+
line,
|
|
312
|
+
index,
|
|
313
|
+
score: getOperationalLineImportance(line),
|
|
314
|
+
}))
|
|
315
|
+
.filter((entry) => entry.score > 0);
|
|
316
|
+
if (candidates.length === 0) {
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
const maxImportantLines = detectedFormat === "command"
|
|
320
|
+
? Math.max(1, Math.min(2, Math.floor(maxLines / 2)))
|
|
321
|
+
: Math.max(1, Math.floor(maxLines / 3));
|
|
322
|
+
const highestScore = Math.max(...candidates.map((entry) => entry.score));
|
|
323
|
+
const prioritized = highestScore >= 3 ? candidates.filter((entry) => entry.score >= 3) : candidates;
|
|
324
|
+
const selected = prioritized
|
|
325
|
+
.sort((left, right) => right.score - left.score || left.index - right.index)
|
|
326
|
+
.slice(0, maxImportantLines)
|
|
327
|
+
.sort((left, right) => left.index - right.index)
|
|
328
|
+
.map((entry) => entry.line);
|
|
329
|
+
return selected;
|
|
330
|
+
}
|
|
331
|
+
function isImportantOperationalLine(line) {
|
|
332
|
+
return getOperationalLineImportance(line) > 0;
|
|
333
|
+
}
|
|
334
|
+
function getOperationalLineImportance(line) {
|
|
335
|
+
const normalized = line.trim().toLowerCase();
|
|
336
|
+
if (!normalized) {
|
|
337
|
+
return 0;
|
|
338
|
+
}
|
|
339
|
+
if (normalized.includes("fatal") ||
|
|
340
|
+
normalized.includes("error") ||
|
|
341
|
+
normalized.includes("exception") ||
|
|
342
|
+
normalized.includes("failed") ||
|
|
343
|
+
normalized.includes("failure")) {
|
|
344
|
+
return 3;
|
|
345
|
+
}
|
|
346
|
+
if (normalized.includes("timeout") ||
|
|
347
|
+
normalized.includes("timed out") ||
|
|
348
|
+
normalized.includes("exit code") ||
|
|
349
|
+
normalized.includes("status=") ||
|
|
350
|
+
normalized.includes("status code")) {
|
|
351
|
+
return 2;
|
|
352
|
+
}
|
|
353
|
+
if (normalized.includes("warn")) {
|
|
354
|
+
return 1;
|
|
355
|
+
}
|
|
356
|
+
return 0;
|
|
357
|
+
}
|
|
358
|
+
function buildSelectedLines(headLines, importantLines, tailLines, marker) {
|
|
359
|
+
const combined = [...headLines];
|
|
360
|
+
if (importantLines.length > 0) {
|
|
361
|
+
combined.push(marker, ...importantLines);
|
|
362
|
+
}
|
|
363
|
+
if (tailLines.length > 0) {
|
|
364
|
+
combined.push(marker, ...tailLines);
|
|
365
|
+
}
|
|
366
|
+
const deduped = [];
|
|
367
|
+
for (const line of combined) {
|
|
368
|
+
if (deduped[deduped.length - 1] === line) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
deduped.push(line);
|
|
372
|
+
}
|
|
373
|
+
return deduped;
|
|
374
|
+
}
|
|
375
|
+
function countVisibleSourceLines(selectedLines, marker) {
|
|
376
|
+
let markers = 0;
|
|
377
|
+
for (const line of selectedLines) {
|
|
378
|
+
if (line === marker) {
|
|
379
|
+
markers += 1;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return Math.max(0, selectedLines.length - markers);
|
|
383
|
+
}
|
|
384
|
+
function detectToolOutputFormat(value, detectFormat) {
|
|
385
|
+
const forced = normalizeDetectFormat(detectFormat);
|
|
386
|
+
if (forced && forced !== "auto") {
|
|
387
|
+
return forced;
|
|
388
|
+
}
|
|
389
|
+
const trimmed = value.trim();
|
|
390
|
+
if (!trimmed) {
|
|
391
|
+
return "plain";
|
|
392
|
+
}
|
|
393
|
+
if (prettifyJsonToolOutput(trimmed)) {
|
|
394
|
+
return "json";
|
|
395
|
+
}
|
|
396
|
+
const lines = trimmed.split("\n");
|
|
397
|
+
const stackMatches = lines.filter((line) => /^\s*at\s+/.test(line) ||
|
|
398
|
+
/(Error|Exception|Traceback)/.test(line) ||
|
|
399
|
+
/:\d+:\d+/.test(line)).length;
|
|
400
|
+
if (stackMatches >= 2 || /^(?:\w*Error|\w*Exception|Traceback)/.test(lines[0] || "")) {
|
|
401
|
+
return "stack";
|
|
402
|
+
}
|
|
403
|
+
const commandMatches = lines.filter((line) => /^\s*(?:\$|>|\+|#)\s+/.test(line) ||
|
|
404
|
+
/^\s*(?:INFO|WARN|ERROR|DEBUG|TRACE)\b/.test(line) ||
|
|
405
|
+
/^\s*\[[A-Z]+\]/.test(line) ||
|
|
406
|
+
/^\s*\d{2}:\d{2}:\d{2}/.test(line) ||
|
|
407
|
+
/^\s*\d{4}-\d{2}-\d{2}[ T]/.test(line)).length;
|
|
408
|
+
if (commandMatches >= Math.max(2, Math.ceil(lines.length / 3))) {
|
|
409
|
+
return "command";
|
|
410
|
+
}
|
|
411
|
+
return "plain";
|
|
412
|
+
}
|
|
413
|
+
function prettifyJsonToolOutput(value) {
|
|
414
|
+
try {
|
|
415
|
+
const parsed = JSON.parse(value);
|
|
416
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
417
|
+
return undefined;
|
|
418
|
+
}
|
|
419
|
+
return JSON.stringify(parsed, null, 2);
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
return undefined;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function formatAwareTruncationMarker(detectedFormat) {
|
|
426
|
+
switch (detectedFormat) {
|
|
427
|
+
case "json":
|
|
428
|
+
return '"... truncated json ..."';
|
|
429
|
+
case "stack":
|
|
430
|
+
return "... stack frames truncated ...";
|
|
431
|
+
case "command":
|
|
432
|
+
return "... log lines truncated ...";
|
|
433
|
+
default:
|
|
434
|
+
return "...";
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function buildTruncationSummary(detectedFormat, hiddenLineCount, hiddenCharCount) {
|
|
438
|
+
return `[rtk trunc fmt=${detectedFormat} lines=${hiddenLineCount} chars=${hiddenCharCount}]`;
|
|
439
|
+
}
|
|
440
|
+
function isRecord(value) {
|
|
441
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
442
|
+
}
|
|
443
|
+
function isFunctionCallOutputItem(value) {
|
|
444
|
+
return isRecord(value) && value.type === "function_call_output" && typeof value.output === "string";
|
|
445
|
+
}
|
|
446
|
+
function coerceBoolean(value) {
|
|
447
|
+
if (value === true || value === "true" || value === 1 || value === "1") {
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
450
|
+
if (value === false || value === "false" || value === 0 || value === "0") {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
455
|
+
function coercePositiveInt(value) {
|
|
456
|
+
const numeric = typeof value === "number"
|
|
457
|
+
? value
|
|
458
|
+
: typeof value === "string" && value.trim()
|
|
459
|
+
? Number(value)
|
|
460
|
+
: NaN;
|
|
461
|
+
return Number.isInteger(numeric) && numeric > 0 ? numeric : undefined;
|
|
462
|
+
}
|
|
463
|
+
function coerceNonNegativeInt(value) {
|
|
464
|
+
const numeric = typeof value === "number"
|
|
465
|
+
? value
|
|
466
|
+
: typeof value === "string" && value.trim()
|
|
467
|
+
? Number(value)
|
|
468
|
+
: NaN;
|
|
469
|
+
return Number.isInteger(numeric) && numeric >= 0 ? numeric : undefined;
|
|
470
|
+
}
|
|
471
|
+
function normalizeDetectFormat(value) {
|
|
472
|
+
if (typeof value !== "string") {
|
|
473
|
+
return undefined;
|
|
474
|
+
}
|
|
475
|
+
const normalized = value.trim().toLowerCase();
|
|
476
|
+
return normalized === "auto" ||
|
|
477
|
+
normalized === "plain" ||
|
|
478
|
+
normalized === "json" ||
|
|
479
|
+
normalized === "stack" ||
|
|
480
|
+
normalized === "command"
|
|
481
|
+
? normalized
|
|
482
|
+
: undefined;
|
|
483
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { applyRtkLayer, resolveRtkLayerPolicy } from "./rtk-layer.js";
|
|
4
|
+
test("rtk layer is inert when disabled", () => {
|
|
5
|
+
const body = {
|
|
6
|
+
model: "gpt-5.4",
|
|
7
|
+
messages: [
|
|
8
|
+
{
|
|
9
|
+
role: "user",
|
|
10
|
+
content: "hello",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
role: "tool",
|
|
14
|
+
tool_call_id: "call_1",
|
|
15
|
+
content: "\u001b[32mok\u001b[0m\n\nsame\nsame\n",
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
};
|
|
19
|
+
const result = applyRtkLayer(body, {
|
|
20
|
+
enabled: false,
|
|
21
|
+
});
|
|
22
|
+
assert.equal(result.body, body);
|
|
23
|
+
assert.equal(result.stats.applied, false);
|
|
24
|
+
assert.equal(result.stats.toolOutputsSeen, 0);
|
|
25
|
+
});
|
|
26
|
+
test("rtk layer canonicalizes and truncates tool message output deterministically", () => {
|
|
27
|
+
const body = {
|
|
28
|
+
model: "gpt-5.4",
|
|
29
|
+
messages: [
|
|
30
|
+
{
|
|
31
|
+
role: "user",
|
|
32
|
+
content: "check logs",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
role: "tool",
|
|
36
|
+
tool_call_id: "call_1",
|
|
37
|
+
content: "\u001b[32mline-1\u001b[0m\r\n\r\nsame\r\nsame\r\nline-4\r\nline-5\r\nline-6",
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
const result = applyRtkLayer(body, {
|
|
42
|
+
enabled: true,
|
|
43
|
+
toolOutputEnabled: true,
|
|
44
|
+
maxLines: 3,
|
|
45
|
+
maxChars: 80,
|
|
46
|
+
});
|
|
47
|
+
assert.equal(result.stats.applied, true);
|
|
48
|
+
assert.equal(result.stats.toolOutputsSeen, 1);
|
|
49
|
+
assert.equal(result.stats.toolOutputsReduced, 1);
|
|
50
|
+
const toolMessage = result.body.messages?.[1];
|
|
51
|
+
assert.equal(typeof toolMessage?.content, "string");
|
|
52
|
+
assert.match(toolMessage?.content, /\[rtk trunc fmt=plain/);
|
|
53
|
+
assert.equal((toolMessage?.content).includes("\u001b["), false);
|
|
54
|
+
assert.equal((toolMessage?.content).includes("same\nsame"), false);
|
|
55
|
+
});
|
|
56
|
+
test("rtk layer transforms direct function_call_output items without touching user input", () => {
|
|
57
|
+
const body = {
|
|
58
|
+
model: "gpt-5.4",
|
|
59
|
+
input: [
|
|
60
|
+
{
|
|
61
|
+
role: "user",
|
|
62
|
+
content: "keep this intact",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
type: "function_call_output",
|
|
66
|
+
call_id: "call_1",
|
|
67
|
+
output: "a\nb\nc\nd\ne",
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
const result = applyRtkLayer(body, {
|
|
72
|
+
enabled: true,
|
|
73
|
+
toolOutputEnabled: true,
|
|
74
|
+
maxLines: 2,
|
|
75
|
+
maxChars: 40,
|
|
76
|
+
});
|
|
77
|
+
assert.equal(result.body.input?.[0], body.input?.[0]);
|
|
78
|
+
assert.notEqual(result.body.input?.[1], body.input?.[1]);
|
|
79
|
+
assert.match(String((result.body.input?.[1]).output), /lines=3/);
|
|
80
|
+
});
|
|
81
|
+
test("rtk layer can preserve tail lines when truncating long tool output", () => {
|
|
82
|
+
const body = {
|
|
83
|
+
model: "gpt-5.4",
|
|
84
|
+
messages: [
|
|
85
|
+
{
|
|
86
|
+
role: "tool",
|
|
87
|
+
tool_call_id: "call_2",
|
|
88
|
+
content: "head-1\nhead-2\nmid-1\nmid-2\nerror-1\nerror-2",
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
const result = applyRtkLayer(body, {
|
|
93
|
+
enabled: true,
|
|
94
|
+
toolOutputEnabled: true,
|
|
95
|
+
maxLines: 4,
|
|
96
|
+
maxChars: 120,
|
|
97
|
+
tailLines: 2,
|
|
98
|
+
});
|
|
99
|
+
assert.equal(result.stats.applied, true);
|
|
100
|
+
const toolMessage = result.body.messages?.[0];
|
|
101
|
+
assert.equal(typeof toolMessage?.content, "string");
|
|
102
|
+
assert.match(String(toolMessage?.content), /head-1/);
|
|
103
|
+
assert.match(String(toolMessage?.content), /\n\.\.\.\n/);
|
|
104
|
+
assert.match(String(toolMessage?.content), /error-1/);
|
|
105
|
+
assert.match(String(toolMessage?.content), /error-2/);
|
|
106
|
+
assert.equal(String(toolMessage?.content).includes("mid-1"), false);
|
|
107
|
+
});
|
|
108
|
+
test("rtk layer auto-detects and pretty-prints json before truncation", () => {
|
|
109
|
+
const body = {
|
|
110
|
+
model: "gpt-5.4",
|
|
111
|
+
messages: [
|
|
112
|
+
{
|
|
113
|
+
role: "tool",
|
|
114
|
+
tool_call_id: "call_json",
|
|
115
|
+
content: '{"root":{"items":[1,2,3],"meta":{"status":"ok","trace":"abc"}},"tail":{"done":true}}',
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
const result = applyRtkLayer(body, {
|
|
120
|
+
enabled: true,
|
|
121
|
+
toolOutputEnabled: true,
|
|
122
|
+
maxLines: 6,
|
|
123
|
+
maxChars: 190,
|
|
124
|
+
tailLines: 3,
|
|
125
|
+
tailChars: 60,
|
|
126
|
+
});
|
|
127
|
+
const content = String(result.body.messages?.[0]?.content ?? "");
|
|
128
|
+
assert.match(content, /fmt=json/);
|
|
129
|
+
assert.match(content, /"root": \{/);
|
|
130
|
+
assert.match(content, /"done": true/);
|
|
131
|
+
});
|
|
132
|
+
test("rtk layer auto-detects stack traces and preserves the tail error frames", () => {
|
|
133
|
+
const body = {
|
|
134
|
+
model: "gpt-5.4",
|
|
135
|
+
messages: [
|
|
136
|
+
{
|
|
137
|
+
role: "tool",
|
|
138
|
+
tool_call_id: "call_stack",
|
|
139
|
+
content: "TypeError: failed\n at first (/app/a.ts:1:1)\n at second (/app/b.ts:2:2)\n at third (/app/c.ts:3:3)\n at final (/app/d.ts:4:4)",
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
const result = applyRtkLayer(body, {
|
|
144
|
+
enabled: true,
|
|
145
|
+
toolOutputEnabled: true,
|
|
146
|
+
maxLines: 4,
|
|
147
|
+
maxChars: 140,
|
|
148
|
+
tailLines: 2,
|
|
149
|
+
});
|
|
150
|
+
const content = String(result.body.messages?.[0]?.content ?? "");
|
|
151
|
+
assert.match(content, /fmt=stack/);
|
|
152
|
+
assert.match(content, /stack frames truncated/);
|
|
153
|
+
assert.match(content, /final \(\/app\/d.ts:4:4/);
|
|
154
|
+
});
|
|
155
|
+
test("rtk layer preserves important command log lines from the middle segment", () => {
|
|
156
|
+
const body = {
|
|
157
|
+
model: "gpt-5.4",
|
|
158
|
+
messages: [
|
|
159
|
+
{
|
|
160
|
+
role: "tool",
|
|
161
|
+
tool_call_id: "call_cmd",
|
|
162
|
+
content: [
|
|
163
|
+
"$ npm run build",
|
|
164
|
+
"INFO compiling module a",
|
|
165
|
+
"INFO compiling module b",
|
|
166
|
+
"INFO compiling module c",
|
|
167
|
+
"WARN retrying cache fetch",
|
|
168
|
+
"INFO compiling module d",
|
|
169
|
+
"ERROR request failed with status code 502",
|
|
170
|
+
"INFO compiling module e",
|
|
171
|
+
"INFO compiling module f",
|
|
172
|
+
"build finished",
|
|
173
|
+
].join("\n"),
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
};
|
|
177
|
+
const result = applyRtkLayer(body, {
|
|
178
|
+
enabled: true,
|
|
179
|
+
toolOutputEnabled: true,
|
|
180
|
+
maxLines: 4,
|
|
181
|
+
maxChars: 180,
|
|
182
|
+
tailLines: 1,
|
|
183
|
+
detectFormat: "command",
|
|
184
|
+
});
|
|
185
|
+
const content = String(result.body.messages?.[0]?.content ?? "");
|
|
186
|
+
assert.match(content, /fmt=command/);
|
|
187
|
+
assert.match(content, /ERROR request failed with status code 502/);
|
|
188
|
+
assert.match(content, /build finished/);
|
|
189
|
+
});
|
|
190
|
+
test("rtk policy resolution prefers client override over provider default", () => {
|
|
191
|
+
const resolved = resolveRtkLayerPolicy({ enabled: true, toolOutputEnabled: true, maxChars: 4000, maxLines: 120, tailLines: 0, detectFormat: "auto" }, { maxChars: 2400, tailLines: 3 }, { maxLines: 60, detectFormat: "json" });
|
|
192
|
+
assert.equal(resolved.enabled, true);
|
|
193
|
+
assert.equal(resolved.toolOutputEnabled, true);
|
|
194
|
+
assert.equal(resolved.maxChars, 2400);
|
|
195
|
+
assert.equal(resolved.maxLines, 60);
|
|
196
|
+
assert.equal(resolved.tailLines, 3);
|
|
197
|
+
assert.equal(resolved.detectFormat, "json");
|
|
198
|
+
});
|