twinclaw 1.0.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 +66 -0
- package/bin/npm-twinclaw.js +17 -0
- package/bin/run-twinbot-cli.js +36 -0
- package/bin/twinbot.js +4 -0
- package/bin/twinclaw.js +4 -0
- package/dist/api/handlers/browser.js +160 -0
- package/dist/api/handlers/callback.js +80 -0
- package/dist/api/handlers/config-validate.js +19 -0
- package/dist/api/handlers/health.js +117 -0
- package/dist/api/handlers/local-state-backup.js +118 -0
- package/dist/api/handlers/persona-state.js +59 -0
- package/dist/api/handlers/skill-packages.js +94 -0
- package/dist/api/router.js +278 -0
- package/dist/api/runtime-event-producer.js +99 -0
- package/dist/api/shared.js +82 -0
- package/dist/api/websocket-hub.js +305 -0
- package/dist/config/config-loader.js +2 -0
- package/dist/config/env-schema.js +202 -0
- package/dist/config/env-validator.js +223 -0
- package/dist/config/identity-bootstrap.js +115 -0
- package/dist/config/json-config.js +344 -0
- package/dist/config/workspace.js +186 -0
- package/dist/core/channels-cli.js +77 -0
- package/dist/core/cli.js +119 -0
- package/dist/core/context-assembly.js +33 -0
- package/dist/core/doctor.js +365 -0
- package/dist/core/gateway-cli.js +323 -0
- package/dist/core/gateway.js +416 -0
- package/dist/core/heartbeat.js +54 -0
- package/dist/core/install-cli.js +320 -0
- package/dist/core/lane-executor.js +134 -0
- package/dist/core/logs-cli.js +70 -0
- package/dist/core/onboarding.js +760 -0
- package/dist/core/pairing-cli.js +78 -0
- package/dist/core/secret-vault-cli.js +204 -0
- package/dist/core/types.js +1 -0
- package/dist/index.js +404 -0
- package/dist/interfaces/dispatcher.js +214 -0
- package/dist/interfaces/telegram_handler.js +82 -0
- package/dist/interfaces/tui-dashboard.js +53 -0
- package/dist/interfaces/whatsapp_handler.js +94 -0
- package/dist/release/cli.js +97 -0
- package/dist/release/mvp-gate-cli.js +118 -0
- package/dist/release/twinbot-config-schema.js +162 -0
- package/dist/release/twinclaw-config-schema.js +162 -0
- package/dist/services/block-chunker.js +174 -0
- package/dist/services/browser-service.js +334 -0
- package/dist/services/context-lifecycle.js +314 -0
- package/dist/services/db.js +1055 -0
- package/dist/services/delivery-tracker.js +110 -0
- package/dist/services/dm-pairing.js +245 -0
- package/dist/services/embedding-service.js +125 -0
- package/dist/services/file-watcher.js +125 -0
- package/dist/services/inbound-debounce.js +92 -0
- package/dist/services/incident-manager.js +516 -0
- package/dist/services/job-scheduler.js +176 -0
- package/dist/services/local-state-backup.js +682 -0
- package/dist/services/mcp-client-adapter.js +291 -0
- package/dist/services/mcp-server-manager.js +143 -0
- package/dist/services/model-router.js +927 -0
- package/dist/services/mvp-gate.js +845 -0
- package/dist/services/orchestration-service.js +422 -0
- package/dist/services/persona-state.js +256 -0
- package/dist/services/policy-engine.js +92 -0
- package/dist/services/proactive-notifier.js +94 -0
- package/dist/services/queue-service.js +146 -0
- package/dist/services/release-pipeline.js +652 -0
- package/dist/services/runtime-budget-governor.js +415 -0
- package/dist/services/secret-vault.js +704 -0
- package/dist/services/semantic-memory.js +249 -0
- package/dist/services/skill-package-manager.js +806 -0
- package/dist/services/skill-registry.js +122 -0
- package/dist/services/streaming-output.js +75 -0
- package/dist/services/stt-service.js +39 -0
- package/dist/services/tts-service.js +44 -0
- package/dist/skills/builtin.js +250 -0
- package/dist/skills/shell.js +87 -0
- package/dist/skills/types.js +1 -0
- package/dist/types/api.js +1 -0
- package/dist/types/context-budget.js +1 -0
- package/dist/types/doctor.js +1 -0
- package/dist/types/file-watcher.js +1 -0
- package/dist/types/incident.js +1 -0
- package/dist/types/local-state-backup.js +1 -0
- package/dist/types/mcp.js +1 -0
- package/dist/types/messaging.js +1 -0
- package/dist/types/model-routing.js +1 -0
- package/dist/types/mvp-gate.js +2 -0
- package/dist/types/orchestration.js +1 -0
- package/dist/types/persona-state.js +22 -0
- package/dist/types/policy.js +1 -0
- package/dist/types/reasoning-graph.js +1 -0
- package/dist/types/release.js +1 -0
- package/dist/types/reliability.js +1 -0
- package/dist/types/runtime-budget.js +1 -0
- package/dist/types/scheduler.js +1 -0
- package/dist/types/secret-vault.js +1 -0
- package/dist/types/skill-packages.js +1 -0
- package/dist/types/websocket.js +14 -0
- package/dist/utils/logger.js +57 -0
- package/dist/utils/retry.js +61 -0
- package/dist/utils/secret-scan.js +208 -0
- package/mcp-servers.json +179 -0
- package/package.json +81 -0
- package/skill-packages.json +92 -0
- package/skill-packages.lock.json +5 -0
- package/src/skills/builtin.ts +275 -0
- package/src/skills/shell.ts +118 -0
- package/src/skills/types.ts +30 -0
- package/src/types/api.ts +252 -0
- package/src/types/blessed-contrib.d.ts +4 -0
- package/src/types/context-budget.ts +76 -0
- package/src/types/doctor.ts +29 -0
- package/src/types/file-watcher.ts +26 -0
- package/src/types/incident.ts +57 -0
- package/src/types/local-state-backup.ts +121 -0
- package/src/types/mcp.ts +106 -0
- package/src/types/messaging.ts +35 -0
- package/src/types/model-routing.ts +61 -0
- package/src/types/mvp-gate.ts +99 -0
- package/src/types/orchestration.ts +65 -0
- package/src/types/persona-state.ts +61 -0
- package/src/types/policy.ts +27 -0
- package/src/types/reasoning-graph.ts +58 -0
- package/src/types/release.ts +115 -0
- package/src/types/reliability.ts +43 -0
- package/src/types/runtime-budget.ts +85 -0
- package/src/types/scheduler.ts +47 -0
- package/src/types/secret-vault.ts +62 -0
- package/src/types/skill-packages.ts +81 -0
- package/src/types/sqlite-vec.d.ts +5 -0
- package/src/types/websocket.ts +122 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { clearRuntimeBudgetState, getRuntimeBudgetState, getRuntimeDailyUsageAggregate, getRuntimeSessionUsageAggregate, listRuntimeBudgetEvents, listRuntimeProviderUsageAggregates, recordRuntimeBudgetEvent, recordRuntimeUsageEvent, setRuntimeBudgetState, } from './db.js';
|
|
3
|
+
import { logThought } from '../utils/logger.js';
|
|
4
|
+
import { getConfigValue } from '../config/config-loader.js';
|
|
5
|
+
const MANUAL_PROFILE_KEY = 'manual_profile';
|
|
6
|
+
const DEFAULT_SESSION_ID = 'global';
|
|
7
|
+
const DEFAULT_LOCAL_MODEL_ID = 'local';
|
|
8
|
+
const DEFAULT_LIMITS = {
|
|
9
|
+
dailyRequestLimit: 2_400,
|
|
10
|
+
dailyTokenLimit: 5_000_000,
|
|
11
|
+
sessionRequestLimit: 300,
|
|
12
|
+
sessionTokenLimit: 700_000,
|
|
13
|
+
providerRequestLimit: 1_000,
|
|
14
|
+
providerTokenLimit: 2_200_000,
|
|
15
|
+
warningRatio: 0.8,
|
|
16
|
+
warningPacingMs: 250,
|
|
17
|
+
hardLimitPacingMs: 1_250,
|
|
18
|
+
providerCooldownMs: 60_000,
|
|
19
|
+
};
|
|
20
|
+
const PROFILE_ORDER = ['economy', 'balanced', 'performance'];
|
|
21
|
+
export class RuntimeBudgetGovernor {
|
|
22
|
+
#limits;
|
|
23
|
+
#defaultProfile;
|
|
24
|
+
#preferLocalModel;
|
|
25
|
+
#localModelId;
|
|
26
|
+
#now;
|
|
27
|
+
#providerCooldowns = new Map();
|
|
28
|
+
#signatures = new Map();
|
|
29
|
+
#manualProfile;
|
|
30
|
+
constructor(config = {}) {
|
|
31
|
+
this.#limits = resolveLimits(config.limits);
|
|
32
|
+
this.#defaultProfile =
|
|
33
|
+
parseProfile(getConfigValue('RUNTIME_BUDGET_DEFAULT_PROFILE')) ??
|
|
34
|
+
config.defaultProfile ??
|
|
35
|
+
'performance';
|
|
36
|
+
this.#preferLocalModel =
|
|
37
|
+
config.preferLocalModel ??
|
|
38
|
+
parseBoolean(getConfigValue('RUNTIME_BUDGET_PREFER_LOCAL_MODEL')) ??
|
|
39
|
+
false;
|
|
40
|
+
this.#localModelId =
|
|
41
|
+
getConfigValue('RUNTIME_BUDGET_LOCAL_MODEL_ID')?.trim() || config.localModelId || DEFAULT_LOCAL_MODEL_ID;
|
|
42
|
+
this.#now = config.now ?? (() => Date.now());
|
|
43
|
+
this.#manualProfile = parseProfile(getRuntimeBudgetState(MANUAL_PROFILE_KEY));
|
|
44
|
+
}
|
|
45
|
+
get limits() {
|
|
46
|
+
return this.#limits;
|
|
47
|
+
}
|
|
48
|
+
getRoutingDirective(sessionId) {
|
|
49
|
+
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
50
|
+
const evaluation = this.#evaluate(normalizedSessionId);
|
|
51
|
+
this.#recordTransition(normalizedSessionId, evaluation);
|
|
52
|
+
return evaluation.directive;
|
|
53
|
+
}
|
|
54
|
+
recordUsage(input) {
|
|
55
|
+
const normalizedSessionId = normalizeSessionId(input.sessionId);
|
|
56
|
+
const event = {
|
|
57
|
+
id: randomUUID(),
|
|
58
|
+
sessionId: normalizedSessionId,
|
|
59
|
+
modelId: input.modelId,
|
|
60
|
+
providerId: input.providerId,
|
|
61
|
+
profile: input.profile,
|
|
62
|
+
stage: input.stage,
|
|
63
|
+
requestTokens: Math.max(0, Math.floor(input.requestTokens)),
|
|
64
|
+
responseTokens: Math.max(0, Math.floor(input.responseTokens)),
|
|
65
|
+
latencyMs: Math.max(0, Math.floor(input.latencyMs)),
|
|
66
|
+
statusCode: input.statusCode ?? null,
|
|
67
|
+
error: input.error ?? null,
|
|
68
|
+
};
|
|
69
|
+
recordRuntimeUsageEvent(event);
|
|
70
|
+
if (input.stage === 'failure' && input.statusCode === 429) {
|
|
71
|
+
this.applyProviderCooldown(input.providerId, normalizedSessionId, 'Rate-limit response detected.');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
applyProviderCooldown(providerId, sessionId, reason = 'Provider cooldown applied.') {
|
|
75
|
+
const normalizedProvider = providerId.trim().toLowerCase();
|
|
76
|
+
if (!normalizedProvider) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const now = this.#now();
|
|
80
|
+
const current = this.#providerCooldowns.get(normalizedProvider) ?? 0;
|
|
81
|
+
const next = Math.max(current, now + this.#limits.providerCooldownMs);
|
|
82
|
+
this.#providerCooldowns.set(normalizedProvider, next);
|
|
83
|
+
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
84
|
+
this.#recordBudgetEvent({
|
|
85
|
+
sessionId: normalizedSessionId,
|
|
86
|
+
severity: 'warning',
|
|
87
|
+
profile: this.#manualProfile ?? this.#defaultProfile,
|
|
88
|
+
action: 'provider_cooldown',
|
|
89
|
+
reason,
|
|
90
|
+
detail: {
|
|
91
|
+
providerId: normalizedProvider,
|
|
92
|
+
cooldownUntil: new Date(next).toISOString(),
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
setManualProfile(profile, sessionId) {
|
|
97
|
+
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
98
|
+
this.#manualProfile = profile;
|
|
99
|
+
if (profile) {
|
|
100
|
+
setRuntimeBudgetState(MANUAL_PROFILE_KEY, profile);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
clearRuntimeBudgetState(MANUAL_PROFILE_KEY);
|
|
104
|
+
}
|
|
105
|
+
this.#recordBudgetEvent({
|
|
106
|
+
sessionId: normalizedSessionId,
|
|
107
|
+
severity: 'ok',
|
|
108
|
+
profile: profile ?? this.#defaultProfile,
|
|
109
|
+
action: 'none',
|
|
110
|
+
reason: profile
|
|
111
|
+
? `Manual budget profile override set to '${profile}'.`
|
|
112
|
+
: 'Manual budget profile override cleared.',
|
|
113
|
+
detail: { manualProfile: profile },
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
resetPolicyState(sessionId) {
|
|
117
|
+
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
118
|
+
this.#providerCooldowns.clear();
|
|
119
|
+
this.#signatures.clear();
|
|
120
|
+
this.setManualProfile(null, normalizedSessionId);
|
|
121
|
+
this.#recordBudgetEvent({
|
|
122
|
+
sessionId: normalizedSessionId,
|
|
123
|
+
severity: 'ok',
|
|
124
|
+
profile: this.#defaultProfile,
|
|
125
|
+
action: 'none',
|
|
126
|
+
reason: 'Runtime budget policy state reset.',
|
|
127
|
+
detail: {},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
getSnapshot(sessionId, eventLimit = 50) {
|
|
131
|
+
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
132
|
+
const evaluation = this.#evaluate(normalizedSessionId);
|
|
133
|
+
return {
|
|
134
|
+
sessionId: normalizedSessionId,
|
|
135
|
+
manualProfile: evaluation.manualProfile,
|
|
136
|
+
limits: evaluation.limits,
|
|
137
|
+
daily: evaluation.daily,
|
|
138
|
+
session: evaluation.session,
|
|
139
|
+
providers: evaluation.providers,
|
|
140
|
+
directive: evaluation.directive,
|
|
141
|
+
recentEvents: this.getRecentEvents(eventLimit),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
getRecentEvents(limit = 100) {
|
|
145
|
+
return listRuntimeBudgetEvents(limit).map((row) => this.#toEvent(row));
|
|
146
|
+
}
|
|
147
|
+
#evaluate(sessionId) {
|
|
148
|
+
this.#pruneCooldowns();
|
|
149
|
+
this.#manualProfile = parseProfile(getRuntimeBudgetState(MANUAL_PROFILE_KEY));
|
|
150
|
+
const daily = toUsageAggregate(getRuntimeDailyUsageAggregate());
|
|
151
|
+
const session = toUsageAggregate(getRuntimeSessionUsageAggregate(sessionId));
|
|
152
|
+
const providers = listRuntimeProviderUsageAggregates().map((row) => ({
|
|
153
|
+
providerId: row.provider_id,
|
|
154
|
+
...toUsageAggregate(row),
|
|
155
|
+
}));
|
|
156
|
+
const severity = resolveSeverity(this.#limits, daily, session, providers);
|
|
157
|
+
const actions = resolveActions(severity, this.#preferLocalModel);
|
|
158
|
+
const blockedProviders = providers
|
|
159
|
+
.filter((provider) => provider.requestCount >= this.#limits.providerRequestLimit ||
|
|
160
|
+
provider.requestTokens >= this.#limits.providerTokenLimit)
|
|
161
|
+
.map((provider) => provider.providerId);
|
|
162
|
+
for (const providerId of blockedProviders) {
|
|
163
|
+
if (!this.#providerCooldowns.has(providerId)) {
|
|
164
|
+
this.#providerCooldowns.set(providerId, this.#now() + this.#limits.providerCooldownMs);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const cooldownProviders = [...this.#providerCooldowns.entries()]
|
|
168
|
+
.filter(([, until]) => until > this.#now())
|
|
169
|
+
.map(([providerId]) => providerId);
|
|
170
|
+
const finalBlockedProviders = [...new Set([...blockedProviders, ...cooldownProviders])];
|
|
171
|
+
if (finalBlockedProviders.length > 0 && !actions.includes('provider_cooldown')) {
|
|
172
|
+
actions.push('provider_cooldown');
|
|
173
|
+
}
|
|
174
|
+
const profile = this.#manualProfile ?? resolveProfile(this.#defaultProfile, severity);
|
|
175
|
+
const blockedModelIds = severity === 'hard_limit'
|
|
176
|
+
? ['primary']
|
|
177
|
+
: this.#preferLocalModel && profile === 'economy'
|
|
178
|
+
? ['primary', 'fallback_1', 'fallback_2'].filter((id) => id !== this.#localModelId)
|
|
179
|
+
: [];
|
|
180
|
+
const pacingDelayMs = severity === 'hard_limit'
|
|
181
|
+
? this.#limits.hardLimitPacingMs
|
|
182
|
+
: severity === 'warning'
|
|
183
|
+
? this.#limits.warningPacingMs
|
|
184
|
+
: 0;
|
|
185
|
+
const directive = {
|
|
186
|
+
profile,
|
|
187
|
+
severity,
|
|
188
|
+
actions,
|
|
189
|
+
pacingDelayMs,
|
|
190
|
+
blockedModelIds,
|
|
191
|
+
blockedProviders: finalBlockedProviders,
|
|
192
|
+
reason: buildReason(this.#limits, severity, daily, session, providers),
|
|
193
|
+
evaluatedAt: new Date(this.#now()).toISOString(),
|
|
194
|
+
};
|
|
195
|
+
return {
|
|
196
|
+
manualProfile: this.#manualProfile,
|
|
197
|
+
limits: this.#limits,
|
|
198
|
+
daily,
|
|
199
|
+
session,
|
|
200
|
+
providers,
|
|
201
|
+
directive,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
#recordTransition(sessionId, evaluation) {
|
|
205
|
+
const signature = [
|
|
206
|
+
evaluation.directive.severity,
|
|
207
|
+
evaluation.directive.profile,
|
|
208
|
+
evaluation.directive.actions.join(','),
|
|
209
|
+
evaluation.directive.blockedProviders.join(','),
|
|
210
|
+
evaluation.directive.blockedModelIds.join(','),
|
|
211
|
+
].join('|');
|
|
212
|
+
const previous = this.#signatures.get(sessionId);
|
|
213
|
+
if (previous === signature) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
this.#signatures.set(sessionId, signature);
|
|
217
|
+
this.#recordBudgetEvent({
|
|
218
|
+
sessionId,
|
|
219
|
+
severity: evaluation.directive.severity,
|
|
220
|
+
profile: evaluation.directive.profile,
|
|
221
|
+
action: evaluation.directive.actions[0] ?? 'none',
|
|
222
|
+
reason: evaluation.directive.reason,
|
|
223
|
+
detail: {
|
|
224
|
+
actions: evaluation.directive.actions,
|
|
225
|
+
blockedProviders: evaluation.directive.blockedProviders,
|
|
226
|
+
blockedModelIds: evaluation.directive.blockedModelIds,
|
|
227
|
+
pacingDelayMs: evaluation.directive.pacingDelayMs,
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
#recordBudgetEvent(input) {
|
|
232
|
+
recordRuntimeBudgetEvent({
|
|
233
|
+
id: randomUUID(),
|
|
234
|
+
sessionId: input.sessionId,
|
|
235
|
+
severity: input.severity,
|
|
236
|
+
profile: input.profile,
|
|
237
|
+
action: input.action,
|
|
238
|
+
reason: input.reason,
|
|
239
|
+
detailJson: JSON.stringify(input.detail),
|
|
240
|
+
});
|
|
241
|
+
void logThought(`[RuntimeBudget] severity=${input.severity} profile=${input.profile} action=${input.action} reason=${input.reason}`);
|
|
242
|
+
}
|
|
243
|
+
#toEvent(row) {
|
|
244
|
+
return {
|
|
245
|
+
id: row.id,
|
|
246
|
+
sessionId: row.session_id,
|
|
247
|
+
severity: row.severity,
|
|
248
|
+
profile: row.profile,
|
|
249
|
+
action: row.action,
|
|
250
|
+
reason: row.reason,
|
|
251
|
+
detail: parseDetail(row.detail_json),
|
|
252
|
+
createdAt: row.created_at,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
#pruneCooldowns() {
|
|
256
|
+
const now = this.#now();
|
|
257
|
+
for (const [providerId, cooldownUntil] of this.#providerCooldowns.entries()) {
|
|
258
|
+
if (cooldownUntil <= now) {
|
|
259
|
+
this.#providerCooldowns.delete(providerId);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function normalizeSessionId(value) {
|
|
265
|
+
const normalized = value?.trim();
|
|
266
|
+
return normalized || DEFAULT_SESSION_ID;
|
|
267
|
+
}
|
|
268
|
+
function toUsageAggregate(row) {
|
|
269
|
+
return {
|
|
270
|
+
requestCount: Number(row.request_count ?? 0),
|
|
271
|
+
requestTokens: Number(row.request_tokens ?? 0),
|
|
272
|
+
responseTokens: Number(row.response_tokens ?? 0),
|
|
273
|
+
failureCount: Number(row.failure_count ?? 0),
|
|
274
|
+
skippedCount: Number(row.skipped_count ?? 0),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function resolveProfile(defaultProfile, severity) {
|
|
278
|
+
if (severity === 'hard_limit') {
|
|
279
|
+
return 'economy';
|
|
280
|
+
}
|
|
281
|
+
if (severity === 'warning' && defaultProfile === 'performance') {
|
|
282
|
+
return 'balanced';
|
|
283
|
+
}
|
|
284
|
+
return defaultProfile;
|
|
285
|
+
}
|
|
286
|
+
function resolveSeverity(limits, daily, session, providers) {
|
|
287
|
+
const hardBreached = daily.requestCount >= limits.dailyRequestLimit ||
|
|
288
|
+
daily.requestTokens >= limits.dailyTokenLimit ||
|
|
289
|
+
session.requestCount >= limits.sessionRequestLimit ||
|
|
290
|
+
session.requestTokens >= limits.sessionTokenLimit ||
|
|
291
|
+
providers.some((provider) => provider.requestCount >= limits.providerRequestLimit ||
|
|
292
|
+
provider.requestTokens >= limits.providerTokenLimit);
|
|
293
|
+
if (hardBreached) {
|
|
294
|
+
return 'hard_limit';
|
|
295
|
+
}
|
|
296
|
+
const ratios = [
|
|
297
|
+
safeRatio(daily.requestCount, limits.dailyRequestLimit),
|
|
298
|
+
safeRatio(daily.requestTokens, limits.dailyTokenLimit),
|
|
299
|
+
safeRatio(session.requestCount, limits.sessionRequestLimit),
|
|
300
|
+
safeRatio(session.requestTokens, limits.sessionTokenLimit),
|
|
301
|
+
...providers.flatMap((provider) => [
|
|
302
|
+
safeRatio(provider.requestCount, limits.providerRequestLimit),
|
|
303
|
+
safeRatio(provider.requestTokens, limits.providerTokenLimit),
|
|
304
|
+
]),
|
|
305
|
+
];
|
|
306
|
+
if (ratios.some((ratio) => ratio >= limits.warningRatio)) {
|
|
307
|
+
return 'warning';
|
|
308
|
+
}
|
|
309
|
+
return 'ok';
|
|
310
|
+
}
|
|
311
|
+
function resolveActions(severity, preferLocalModel) {
|
|
312
|
+
if (severity === 'ok') {
|
|
313
|
+
return ['none'];
|
|
314
|
+
}
|
|
315
|
+
const actions = ['intelligent_pacing'];
|
|
316
|
+
if (severity === 'hard_limit') {
|
|
317
|
+
actions.push('fallback_tightening');
|
|
318
|
+
if (preferLocalModel) {
|
|
319
|
+
actions.push('prefer_local_model');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return actions;
|
|
323
|
+
}
|
|
324
|
+
function buildReason(limits, severity, daily, session, providers) {
|
|
325
|
+
if (severity === 'ok') {
|
|
326
|
+
return 'Budget utilization is below warning thresholds.';
|
|
327
|
+
}
|
|
328
|
+
const providerHeavy = providers
|
|
329
|
+
.filter((provider) => provider.requestCount >= limits.providerRequestLimit * limits.warningRatio ||
|
|
330
|
+
provider.requestTokens >= limits.providerTokenLimit * limits.warningRatio)
|
|
331
|
+
.map((provider) => provider.providerId);
|
|
332
|
+
const parts = [
|
|
333
|
+
`dailyRequests=${daily.requestCount}/${limits.dailyRequestLimit}`,
|
|
334
|
+
`dailyTokens=${daily.requestTokens}/${limits.dailyTokenLimit}`,
|
|
335
|
+
`sessionRequests=${session.requestCount}/${limits.sessionRequestLimit}`,
|
|
336
|
+
`sessionTokens=${session.requestTokens}/${limits.sessionTokenLimit}`,
|
|
337
|
+
];
|
|
338
|
+
if (providerHeavy.length > 0) {
|
|
339
|
+
parts.push(`providers=${providerHeavy.join(',')}`);
|
|
340
|
+
}
|
|
341
|
+
return severity === 'hard_limit'
|
|
342
|
+
? `Hard budget threshold reached (${parts.join(' | ')}).`
|
|
343
|
+
: `Warning budget threshold reached (${parts.join(' | ')}).`;
|
|
344
|
+
}
|
|
345
|
+
function safeRatio(value, limit) {
|
|
346
|
+
if (!Number.isFinite(limit) || limit <= 0) {
|
|
347
|
+
return 0;
|
|
348
|
+
}
|
|
349
|
+
return value / limit;
|
|
350
|
+
}
|
|
351
|
+
function parseDetail(value) {
|
|
352
|
+
try {
|
|
353
|
+
const parsed = JSON.parse(value);
|
|
354
|
+
return parsed ?? {};
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
return {};
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function resolveLimits(overrides = {}) {
|
|
361
|
+
return {
|
|
362
|
+
dailyRequestLimit: readIntEnv('RUNTIME_BUDGET_DAILY_REQUEST_LIMIT', overrides.dailyRequestLimit ?? DEFAULT_LIMITS.dailyRequestLimit),
|
|
363
|
+
dailyTokenLimit: readIntEnv('RUNTIME_BUDGET_DAILY_TOKEN_LIMIT', overrides.dailyTokenLimit ?? DEFAULT_LIMITS.dailyTokenLimit),
|
|
364
|
+
sessionRequestLimit: readIntEnv('RUNTIME_BUDGET_SESSION_REQUEST_LIMIT', overrides.sessionRequestLimit ?? DEFAULT_LIMITS.sessionRequestLimit),
|
|
365
|
+
sessionTokenLimit: readIntEnv('RUNTIME_BUDGET_SESSION_TOKEN_LIMIT', overrides.sessionTokenLimit ?? DEFAULT_LIMITS.sessionTokenLimit),
|
|
366
|
+
providerRequestLimit: readIntEnv('RUNTIME_BUDGET_PROVIDER_REQUEST_LIMIT', overrides.providerRequestLimit ?? DEFAULT_LIMITS.providerRequestLimit),
|
|
367
|
+
providerTokenLimit: readIntEnv('RUNTIME_BUDGET_PROVIDER_TOKEN_LIMIT', overrides.providerTokenLimit ?? DEFAULT_LIMITS.providerTokenLimit),
|
|
368
|
+
warningRatio: readFloatEnv('RUNTIME_BUDGET_WARNING_RATIO', overrides.warningRatio ?? DEFAULT_LIMITS.warningRatio),
|
|
369
|
+
warningPacingMs: readIntEnv('RUNTIME_BUDGET_WARNING_PACING_MS', overrides.warningPacingMs ?? DEFAULT_LIMITS.warningPacingMs),
|
|
370
|
+
hardLimitPacingMs: readIntEnv('RUNTIME_BUDGET_HARD_PACING_MS', overrides.hardLimitPacingMs ?? DEFAULT_LIMITS.hardLimitPacingMs),
|
|
371
|
+
providerCooldownMs: readIntEnv('RUNTIME_BUDGET_PROVIDER_COOLDOWN_MS', overrides.providerCooldownMs ?? DEFAULT_LIMITS.providerCooldownMs),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function readIntEnv(name, fallback) {
|
|
375
|
+
const raw = getConfigValue(name);
|
|
376
|
+
if (!raw) {
|
|
377
|
+
return fallback;
|
|
378
|
+
}
|
|
379
|
+
const parsed = Number(raw);
|
|
380
|
+
if (!Number.isFinite(parsed)) {
|
|
381
|
+
return fallback;
|
|
382
|
+
}
|
|
383
|
+
return Math.max(1, Math.floor(parsed));
|
|
384
|
+
}
|
|
385
|
+
function readFloatEnv(name, fallback) {
|
|
386
|
+
const raw = getConfigValue(name);
|
|
387
|
+
if (!raw) {
|
|
388
|
+
return fallback;
|
|
389
|
+
}
|
|
390
|
+
const parsed = Number(raw);
|
|
391
|
+
if (!Number.isFinite(parsed)) {
|
|
392
|
+
return fallback;
|
|
393
|
+
}
|
|
394
|
+
return Math.min(0.99, Math.max(0.01, parsed));
|
|
395
|
+
}
|
|
396
|
+
function parseBoolean(raw) {
|
|
397
|
+
if (!raw) {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
const normalized = raw.trim().toLowerCase();
|
|
401
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized)) {
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
if (['0', 'false', 'no', 'off'].includes(normalized)) {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
function parseProfile(raw) {
|
|
410
|
+
if (!raw) {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
const normalized = raw.trim().toLowerCase();
|
|
414
|
+
return PROFILE_ORDER.find((profile) => profile === normalized) ?? null;
|
|
415
|
+
}
|