maka-agent 0.2.0-dev.22.20260905 → 0.2.0-dev.23.20260906
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/native/runtime-host-peer/prebuilds/darwin-arm64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-peer/prebuilds/linux-arm64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-peer/prebuilds/linux-x64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-peer/prebuilds/win32-x64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
- package/node_modules/@maka/core/dist/bot-events.js +18 -63
- package/node_modules/@maka/core/dist/model-call-attempt.js +0 -82
- package/node_modules/@maka/core/dist/model-call-usage-projection.js +0 -186
- package/node_modules/@maka/core/dist/record-schema.js +13 -0
- package/node_modules/@maka/core/dist/redaction.js +41 -58
- package/node_modules/@maka/core/dist/session-trace.js +5 -1
- package/node_modules/@maka/core/dist/usage-ledger-merge.js +7 -9
- package/node_modules/@maka/core/dist/workhub-creation-intent.js +34 -20
- package/node_modules/@maka/eval/harbor/egress-proxy/entrypoint.sh +2 -1
- package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +51 -24
- package/node_modules/@maka/runtime/dist/bots/base-adapter.js +17 -0
- package/node_modules/@maka/runtime/dist/bots/bot-test.js +38 -22
- package/node_modules/@maka/runtime/dist/bots/dingtalk-bridge.js +7 -5
- package/node_modules/@maka/runtime/dist/bots/discord-bridge.js +4 -4
- package/node_modules/@maka/runtime/dist/bots/feishu-bridge.js +4 -5
- package/node_modules/@maka/runtime/dist/bots/qq-bridge.js +6 -4
- package/node_modules/@maka/runtime/dist/bots/slack-bridge.js +3 -4
- package/node_modules/@maka/runtime/dist/bots/telegram-bridge.js +4 -5
- package/node_modules/@maka/runtime/dist/bots/wechat-bridge.js +17 -19
- package/node_modules/@maka/runtime/dist/bots/wecom-bridge.js +5 -6
- package/node_modules/@maka/runtime/dist/bots/ws-bridge-base.js +2 -3
- package/node_modules/@maka/runtime/dist/session-trace-projection.js +3 -30
- package/node_modules/@maka/runtime-host/dist/candidate-entry.js +4 -1
- package/node_modules/@maka/runtime-host/dist/client/index.js +1 -1
- package/node_modules/@maka/runtime-host/dist/client/peer-client.js +13 -0
- package/node_modules/@maka/runtime-host/dist/client/registered-host-termination.js +0 -59
- package/node_modules/@maka/runtime-host/dist/peer-mesh/node.js +28 -16
- package/node_modules/@maka/runtime-host/dist/protocol/external-session.js +4 -0
- package/node_modules/@maka/runtime-host/dist/protocol/host-status.js +8 -0
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +13 -1
- package/node_modules/@maka/runtime-host/dist/protocol/operations.js +1 -0
- package/node_modules/@maka/runtime-host/dist/protocol/session-collaboration.js +28 -1
- package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +33 -0
- package/node_modules/@maka/runtime-host/dist/server/access-authority.js +26 -1
- package/node_modules/@maka/runtime-host/dist/server/access-credential-store.js +5 -1
- package/node_modules/@maka/runtime-host/dist/server/canonical-usage-reader.js +24 -11
- package/node_modules/@maka/runtime-host/dist/server/daily-review-coordinator.js +7 -4
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +70 -6
- package/node_modules/@maka/runtime-host/dist/server/external-session-coordinator.js +6 -3
- package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +12 -6
- package/node_modules/@maka/runtime-host/dist/server/message-coordinator.js +9 -2
- package/node_modules/@maka/runtime-host/dist/server/root-admission-owner.js +7 -0
- package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +21 -0
- package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +151 -5
- package/node_modules/@maka/runtime-host/dist/server/usage-pricing-coordinator.js +4 -11
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +44 -15
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +6 -1
- package/node_modules/@maka/storage/dist/agent-run-store.js +44 -0
- package/node_modules/@maka/storage/dist/conversation-operational-state.js +4 -0
- package/node_modules/@maka/storage/dist/execution-stores.js +2 -0
- package/node_modules/@maka/storage/dist/model-call-ledger.js +199 -32
- package/node_modules/@maka/storage/dist/model-call-usage-sql.js +136 -0
- package/node_modules/@maka/storage/dist/operational-state-store-public.js +1 -1
- package/node_modules/@maka/storage/dist/operational-state-store.js +5 -3
- package/node_modules/@maka/storage/dist/sqlite-core-execution-schema.js +11 -1
- package/node_modules/@maka/storage/dist/sqlite-usage-schema.js +200 -20
- package/node_modules/@maka/storage/dist/usage-stores.js +6 -2
- package/package.json +1 -1
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* different copy without diluting the core message. Exported so the
|
|
23
23
|
* handler can use it AND a contract test can pin it.
|
|
24
24
|
*/
|
|
25
|
+
// bot-channel notices follow the bot audience language; localization tracked under #2672
|
|
25
26
|
export function nonTextMessageAck(kind) {
|
|
26
27
|
switch (kind) {
|
|
27
28
|
case 'photo':
|
|
@@ -139,74 +140,28 @@ function sanitizeBotUserName(value) {
|
|
|
139
140
|
.trim() || 'unknown');
|
|
140
141
|
}
|
|
141
142
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* suitable for persistence in `BotChannelSettings.lastError`. The Settings
|
|
145
|
-
* page reads `lastError` from persisted settings (not live status), so
|
|
146
|
-
* without this persistence step the user sees stale connection-test
|
|
147
|
-
* errors instead of the actual send-path failure that happened minutes
|
|
148
|
-
* ago.
|
|
149
|
-
*
|
|
150
|
-
* Returns `undefined` for non-error reasons (disabled/stopped/missing
|
|
151
|
-
* credentials — those have their own UI surface) and for unrecognized
|
|
152
|
-
* inputs whose pass-through risks leaking unredacted payloads.
|
|
153
|
-
*
|
|
154
|
-
* Length-capped at 200 chars defensively; a real Telegram error
|
|
155
|
-
* description is typically well under 80 chars.
|
|
143
|
+
* Non-error `BotStatus.reason` values. These states have their own UI surface,
|
|
144
|
+
* so an error readout must not repeat them as failures.
|
|
156
145
|
*/
|
|
157
|
-
const
|
|
158
|
-
'
|
|
159
|
-
'
|
|
160
|
-
'
|
|
161
|
-
'
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
'missing-feishu-credentials': undefined,
|
|
168
|
-
'feishu-domain-required': undefined,
|
|
169
|
-
'feishu-events-not-connected': undefined,
|
|
170
|
-
'scaffold-only': undefined,
|
|
171
|
-
unimplemented: undefined,
|
|
172
|
-
};
|
|
146
|
+
const BOT_BENIGN_STATUS_REASONS = new Set([
|
|
147
|
+
'disabled',
|
|
148
|
+
'stopped',
|
|
149
|
+
'token_missing',
|
|
150
|
+
'feishu_credentials_missing',
|
|
151
|
+
'feishu-domain-required',
|
|
152
|
+
'feishu-events-not-connected',
|
|
153
|
+
'scaffold-only',
|
|
154
|
+
'unimplemented',
|
|
155
|
+
]);
|
|
173
156
|
/**
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* `connections-open-500`. Without these patterns the user sees the
|
|
177
|
-
* raw machine code in `lastError`; with them they get a translated
|
|
178
|
-
* description plus the diagnostic code preserved in parentheses.
|
|
179
|
-
*
|
|
180
|
-
* Each entry is a regex with one numeric capture group; the matched
|
|
181
|
-
* code is preserved verbatim so support diagnostics still survive.
|
|
157
|
+
* `BotStatus.reason` stays a stable code; presenters own the copy. Benign
|
|
158
|
+
* states yield `undefined` so they never overwrite a real error readout.
|
|
182
159
|
*/
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
{ pattern: /^gateway-closed-(\d+)$/, format: (code) => `Gateway 连接关闭(${code});正在重连` },
|
|
186
|
-
{ pattern: /^connections-open-(\d+)$/, format: (code) => `Stream 订阅打开失败(HTTP ${code})` },
|
|
187
|
-
{ pattern: /^stream-closed-(\d+)$/, format: (code) => `Stream 连接关闭(${code});正在重连` },
|
|
188
|
-
{ pattern: /^send-failed-(\d+)$/, format: (code) => `发送失败(HTTP ${code})` },
|
|
189
|
-
{
|
|
190
|
-
pattern: /^getAppAccessToken-(\d+)$/,
|
|
191
|
-
format: (code) => `获取 access_token 失败(HTTP ${code})`,
|
|
192
|
-
},
|
|
193
|
-
];
|
|
194
|
-
export function humanizeBotStatusReason(reason) {
|
|
195
|
-
if (typeof reason !== 'string' || reason.length === 0)
|
|
160
|
+
export function botStatusErrorReason(reason) {
|
|
161
|
+
if (typeof reason !== 'string')
|
|
196
162
|
return undefined;
|
|
197
|
-
if (reason in BOT_REASON_HUMANIZE) {
|
|
198
|
-
return BOT_REASON_HUMANIZE[reason];
|
|
199
|
-
}
|
|
200
|
-
for (const { pattern, format } of BOT_REASON_HUMANIZE_PATTERNS) {
|
|
201
|
-
const match = pattern.exec(reason);
|
|
202
|
-
if (match)
|
|
203
|
-
return format(match[1]);
|
|
204
|
-
}
|
|
205
|
-
// Pass-through for platform-supplied descriptions ("Bad Request:
|
|
206
|
-
// chat not found", etc.). Trim + length-cap to keep `lastError`
|
|
207
|
-
// bounded.
|
|
208
163
|
const trimmed = reason.trim();
|
|
209
|
-
if (trimmed.length === 0)
|
|
164
|
+
if (trimmed.length === 0 || BOT_BENIGN_STATUS_REASONS.has(trimmed))
|
|
210
165
|
return undefined;
|
|
211
166
|
return trimmed.length > 200 ? trimmed.slice(0, 200) : trimmed;
|
|
212
167
|
}
|
|
@@ -349,15 +349,6 @@ export function decodeModelCallAttempt(value) {
|
|
|
349
349
|
}
|
|
350
350
|
return value;
|
|
351
351
|
}
|
|
352
|
-
export function isModelCallAttempt(value) {
|
|
353
|
-
try {
|
|
354
|
-
decodeModelCallAttempt(value);
|
|
355
|
-
return true;
|
|
356
|
-
}
|
|
357
|
-
catch {
|
|
358
|
-
return false;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
352
|
/**
|
|
362
353
|
* Collapses re-appended records by `attemptId`, keeping the last occurrence.
|
|
363
354
|
*
|
|
@@ -389,76 +380,3 @@ export function groupModelCallAttempts(attempts) {
|
|
|
389
380
|
}
|
|
390
381
|
return [...groups.values()];
|
|
391
382
|
}
|
|
392
|
-
/**
|
|
393
|
-
* The attempt that settled a logical call: the highest `attempt` ordinal that
|
|
394
|
-
* reached a provider outcome. Terminality is a projection concern, not a stored
|
|
395
|
-
* field, so it is derived rather than recorded.
|
|
396
|
-
*/
|
|
397
|
-
export function settledAttempt(group) {
|
|
398
|
-
let settled;
|
|
399
|
-
for (const attempt of group.attempts) {
|
|
400
|
-
if (!settled || attempt.attempt > settled.attempt)
|
|
401
|
-
settled = attempt;
|
|
402
|
-
}
|
|
403
|
-
return settled;
|
|
404
|
-
}
|
|
405
|
-
/**
|
|
406
|
-
* Extracts the canonical attempts a run committed, from that run's AgentRun
|
|
407
|
-
* events. This is the projection the Usage read model is rebuilt through, so it
|
|
408
|
-
* has to be total: an event that cannot be decoded is counted, not thrown, or
|
|
409
|
-
* one bad record would block every later one in the same run from ever being
|
|
410
|
-
* projected.
|
|
411
|
-
*/
|
|
412
|
-
export function modelCallAttemptsFromRunEvents(events) {
|
|
413
|
-
const attempts = [];
|
|
414
|
-
let unreadableEvents = 0;
|
|
415
|
-
for (const event of events) {
|
|
416
|
-
if (event.type !== MODEL_CALL_ATTEMPT_EVENT_TYPE)
|
|
417
|
-
continue;
|
|
418
|
-
try {
|
|
419
|
-
attempts.push(decodeModelCallAttempt(event.data));
|
|
420
|
-
}
|
|
421
|
-
catch {
|
|
422
|
-
unreadableEvents += 1;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
return { attempts, unreadableEvents };
|
|
426
|
-
}
|
|
427
|
-
export function summarizeModelCallCoverage(attempts) {
|
|
428
|
-
const unique = dedupeModelCallAttempts(attempts);
|
|
429
|
-
const coverage = {
|
|
430
|
-
attempts: unique.length,
|
|
431
|
-
pricedAttempts: 0,
|
|
432
|
-
unpricedAttempts: 0,
|
|
433
|
-
usageReportedAttempts: 0,
|
|
434
|
-
usagePartialAttempts: 0,
|
|
435
|
-
usageMissingAttempts: 0,
|
|
436
|
-
};
|
|
437
|
-
for (const attempt of unique) {
|
|
438
|
-
if (attempt.costBasis === 'priced')
|
|
439
|
-
coverage.pricedAttempts += 1;
|
|
440
|
-
else
|
|
441
|
-
coverage.unpricedAttempts += 1;
|
|
442
|
-
if (attempt.usageBasis === 'reported')
|
|
443
|
-
coverage.usageReportedAttempts += 1;
|
|
444
|
-
else if (attempt.usageBasis === 'partial')
|
|
445
|
-
coverage.usagePartialAttempts += 1;
|
|
446
|
-
else
|
|
447
|
-
coverage.usageMissingAttempts += 1;
|
|
448
|
-
}
|
|
449
|
-
return coverage;
|
|
450
|
-
}
|
|
451
|
-
/**
|
|
452
|
-
* Sums cost across attempts. Returns the total alongside the coverage that
|
|
453
|
-
* qualifies it, because a bare number cannot express "plus an unknown amount
|
|
454
|
-
* from unpriced calls".
|
|
455
|
-
*/
|
|
456
|
-
export function sumModelCallCostUsd(attempts) {
|
|
457
|
-
const unique = dedupeModelCallAttempts(attempts);
|
|
458
|
-
let costUsd = 0;
|
|
459
|
-
for (const attempt of unique) {
|
|
460
|
-
if (attempt.costBasis === 'priced' && attempt.costUsd !== undefined)
|
|
461
|
-
costUsd += attempt.costUsd;
|
|
462
|
-
}
|
|
463
|
-
return { costUsd, coverage: summarizeModelCallCoverage(unique) };
|
|
464
|
-
}
|
|
@@ -16,8 +16,6 @@
|
|
|
16
16
|
* specific language governing permissions and limitations
|
|
17
17
|
* under the License.
|
|
18
18
|
*/
|
|
19
|
-
import { dedupeModelCallAttempts, summarizeModelCallCoverage, } from './model-call-attempt.js';
|
|
20
|
-
import { usageBucketKey } from './usage-stats/bucket-key.js';
|
|
21
19
|
const DAY_MS = 86_400_000;
|
|
22
20
|
export function resolveUsageRange(range, now) {
|
|
23
21
|
if (typeof range === 'object')
|
|
@@ -44,190 +42,6 @@ export function usageStatusForAttempt(status) {
|
|
|
44
42
|
return 'error';
|
|
45
43
|
return 'aborted';
|
|
46
44
|
}
|
|
47
|
-
function matchesQuery(attempt, query, range) {
|
|
48
|
-
if (attempt.completedAt < range.from || attempt.completedAt > range.to)
|
|
49
|
-
return false;
|
|
50
|
-
if (query.sessionId !== undefined && attempt.sessionId !== query.sessionId)
|
|
51
|
-
return false;
|
|
52
|
-
if (query.providerId !== undefined && attempt.providerId !== query.providerId)
|
|
53
|
-
return false;
|
|
54
|
-
if (query.modelId !== undefined && attempt.modelId !== query.modelId)
|
|
55
|
-
return false;
|
|
56
|
-
if (query.connectionSlug !== undefined && attempt.connectionSlug !== query.connectionSlug) {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
if (query.status !== undefined && query.status !== 'all') {
|
|
60
|
-
if (usageStatusForAttempt(attempt.status) !== query.status)
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Selects the attempts a query addresses, in append order, deduped by
|
|
67
|
-
* `attemptId` so a re-appended settlement counts once.
|
|
68
|
-
*/
|
|
69
|
-
export function selectModelCallAttempts(attempts, query, now) {
|
|
70
|
-
const range = resolveUsageRange(query.range, now);
|
|
71
|
-
const rows = dedupeModelCallAttempts(attempts).filter((a) => matchesQuery(a, query, range));
|
|
72
|
-
return { rows, range };
|
|
73
|
-
}
|
|
74
|
-
function tokens(attempt) {
|
|
75
|
-
const reportedCacheRead = attempt.cacheReadInputTokens ?? 0;
|
|
76
|
-
const input = attempt.inputTokens ?? 0;
|
|
77
|
-
const output = attempt.outputTokens ?? 0;
|
|
78
|
-
const cacheMiss = attempt.cacheMissInputTokens ?? 0;
|
|
79
|
-
// With no reported prompt total there is no denominator to validate against,
|
|
80
|
-
// but the provider's cache evidence remains authoritative. Presentation code
|
|
81
|
-
// must leave ratios unavailable while Usage coverage is partial.
|
|
82
|
-
const cacheRead = attempt.inputTokens === undefined
|
|
83
|
-
? reportedCacheRead
|
|
84
|
-
: clampCacheReadTokens(input, reportedCacheRead);
|
|
85
|
-
const cacheWrite = attempt.cacheWriteInputTokens ?? 0;
|
|
86
|
-
const reasoning = attempt.reasoningTokens ?? 0;
|
|
87
|
-
return { input, output, cacheMiss, cacheRead, cacheWrite, reasoning, total: input + output };
|
|
88
|
-
}
|
|
89
45
|
export function clampCacheReadTokens(inputTokens, cacheReadTokens) {
|
|
90
46
|
return Math.min(cacheReadTokens, inputTokens);
|
|
91
47
|
}
|
|
92
|
-
/** Cost contributed by an attempt. Unpriced records contribute nothing to the
|
|
93
|
-
* sum and are surfaced through coverage instead of being counted as zero. */
|
|
94
|
-
function pricedCost(attempt) {
|
|
95
|
-
return attempt.costBasis === 'priced' ? (attempt.costUsd ?? 0) : 0;
|
|
96
|
-
}
|
|
97
|
-
export function projectModelCallUsageSummary(attempts, query, now) {
|
|
98
|
-
const { rows, range } = selectModelCallAttempts(attempts, query, now);
|
|
99
|
-
const totals = {
|
|
100
|
-
input: 0,
|
|
101
|
-
output: 0,
|
|
102
|
-
cacheMiss: 0,
|
|
103
|
-
cacheRead: 0,
|
|
104
|
-
cacheWrite: 0,
|
|
105
|
-
reasoning: 0,
|
|
106
|
-
total: 0,
|
|
107
|
-
};
|
|
108
|
-
let totalCostUsd = 0;
|
|
109
|
-
let totalDurationMs = 0;
|
|
110
|
-
let cacheHitRequests = 0;
|
|
111
|
-
let cacheCreateRequests = 0;
|
|
112
|
-
let errorRequests = 0;
|
|
113
|
-
for (const attempt of rows) {
|
|
114
|
-
const t = tokens(attempt);
|
|
115
|
-
totals.input += t.input;
|
|
116
|
-
totals.output += t.output;
|
|
117
|
-
totals.cacheMiss += t.cacheMiss;
|
|
118
|
-
totals.cacheRead += t.cacheRead;
|
|
119
|
-
totals.cacheWrite += t.cacheWrite;
|
|
120
|
-
totals.reasoning += t.reasoning;
|
|
121
|
-
totals.total += t.total;
|
|
122
|
-
totalCostUsd += pricedCost(attempt);
|
|
123
|
-
totalDurationMs += attempt.latencyMs;
|
|
124
|
-
if (t.cacheRead > 0)
|
|
125
|
-
cacheHitRequests += 1;
|
|
126
|
-
if (t.cacheWrite > 0)
|
|
127
|
-
cacheCreateRequests += 1;
|
|
128
|
-
if (usageStatusForAttempt(attempt.status) === 'error')
|
|
129
|
-
errorRequests += 1;
|
|
130
|
-
}
|
|
131
|
-
return {
|
|
132
|
-
range,
|
|
133
|
-
totalRequests: rows.length,
|
|
134
|
-
totalCostUsd,
|
|
135
|
-
totalDurationMs,
|
|
136
|
-
totalTokens: totals,
|
|
137
|
-
cacheHitRequests,
|
|
138
|
-
cacheCreateRequests,
|
|
139
|
-
errorRequests,
|
|
140
|
-
coverage: summarizeModelCallCoverage(rows),
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
export function projectModelCallUsageBuckets(attempts, query, groupBy, now) {
|
|
144
|
-
const { rows } = selectModelCallAttempts(attempts, query, now);
|
|
145
|
-
const groups = new Map();
|
|
146
|
-
for (const attempt of rows) {
|
|
147
|
-
const key = usageBucketKey({ providerId: attempt.providerId, modelId: attempt.modelId, ts: attempt.completedAt }, groupBy);
|
|
148
|
-
const group = groups.get(key);
|
|
149
|
-
if (group)
|
|
150
|
-
group.push(attempt);
|
|
151
|
-
else
|
|
152
|
-
groups.set(key, [attempt]);
|
|
153
|
-
}
|
|
154
|
-
return [...groups.entries()]
|
|
155
|
-
.map(([key, group]) => {
|
|
156
|
-
const agg = {
|
|
157
|
-
input: 0,
|
|
158
|
-
output: 0,
|
|
159
|
-
cacheMiss: 0,
|
|
160
|
-
cacheRead: 0,
|
|
161
|
-
cacheWrite: 0,
|
|
162
|
-
reasoning: 0,
|
|
163
|
-
total: 0,
|
|
164
|
-
};
|
|
165
|
-
let costUsd = 0;
|
|
166
|
-
let latency = 0;
|
|
167
|
-
let errors = 0;
|
|
168
|
-
for (const attempt of group) {
|
|
169
|
-
const t = tokens(attempt);
|
|
170
|
-
agg.input += t.input;
|
|
171
|
-
agg.output += t.output;
|
|
172
|
-
agg.cacheMiss += t.cacheMiss;
|
|
173
|
-
agg.cacheRead += t.cacheRead;
|
|
174
|
-
agg.cacheWrite += t.cacheWrite;
|
|
175
|
-
agg.reasoning += t.reasoning;
|
|
176
|
-
agg.total += t.total;
|
|
177
|
-
costUsd += pricedCost(attempt);
|
|
178
|
-
latency += attempt.latencyMs;
|
|
179
|
-
if (usageStatusForAttempt(attempt.status) === 'error')
|
|
180
|
-
errors += 1;
|
|
181
|
-
}
|
|
182
|
-
return {
|
|
183
|
-
key,
|
|
184
|
-
label: key,
|
|
185
|
-
requests: group.length,
|
|
186
|
-
inputTokens: agg.input,
|
|
187
|
-
outputTokens: agg.output,
|
|
188
|
-
cacheMissTokens: agg.cacheMiss,
|
|
189
|
-
cacheReadTokens: agg.cacheRead,
|
|
190
|
-
cacheWriteTokens: agg.cacheWrite,
|
|
191
|
-
reasoningTokens: agg.reasoning,
|
|
192
|
-
totalTokens: agg.total,
|
|
193
|
-
costUsd,
|
|
194
|
-
avgLatencyMs: group.length === 0 ? 0 : latency / group.length,
|
|
195
|
-
errorRate: group.length === 0 ? 0 : errors / group.length,
|
|
196
|
-
};
|
|
197
|
-
})
|
|
198
|
-
.sort((left, right) => right.requests - left.requests);
|
|
199
|
-
}
|
|
200
|
-
export function projectModelCallUsageLogs(attempts, query, now, offset = 0, limit = 100) {
|
|
201
|
-
const { rows } = selectModelCallAttempts(attempts, query, now);
|
|
202
|
-
const ordered = [...rows].sort((left, right) => right.completedAt - left.completedAt);
|
|
203
|
-
const page = ordered.slice(offset, offset + limit).map((attempt) => {
|
|
204
|
-
const t = tokens(attempt);
|
|
205
|
-
return {
|
|
206
|
-
id: attempt.attemptId,
|
|
207
|
-
ts: attempt.completedAt,
|
|
208
|
-
callKind: attempt.callKind,
|
|
209
|
-
callId: attempt.logicalCallId,
|
|
210
|
-
...(attempt.connectionSlug !== undefined ? { connectionSlug: attempt.connectionSlug } : {}),
|
|
211
|
-
providerId: attempt.providerId,
|
|
212
|
-
modelId: attempt.modelId,
|
|
213
|
-
inputTokens: t.input,
|
|
214
|
-
outputTokens: t.output,
|
|
215
|
-
cacheMissTokens: t.cacheMiss,
|
|
216
|
-
cacheReadTokens: t.cacheRead,
|
|
217
|
-
cacheWriteTokens: t.cacheWrite,
|
|
218
|
-
reasoningTokens: t.reasoning,
|
|
219
|
-
totalTokens: t.total,
|
|
220
|
-
// A row keeps its basis, not just its number. Collapsing an unpriced call
|
|
221
|
-
// to 0 here would reproduce, per row, exactly the ambiguity the coverage
|
|
222
|
-
// breakdown removes from the totals.
|
|
223
|
-
...(attempt.costBasis === 'priced' ? { costUsd: attempt.costUsd ?? 0 } : {}),
|
|
224
|
-
costBasis: attempt.costBasis,
|
|
225
|
-
latencyMs: attempt.latencyMs,
|
|
226
|
-
status: usageStatusForAttempt(attempt.status),
|
|
227
|
-
...(attempt.errorClass !== undefined ? { errorClass: attempt.errorClass } : {}),
|
|
228
|
-
sessionId: attempt.sessionId,
|
|
229
|
-
turnId: attempt.turnId,
|
|
230
|
-
};
|
|
231
|
-
});
|
|
232
|
-
return { rows: page, total: ordered.length, coverage: summarizeModelCallCoverage(rows) };
|
|
233
|
-
}
|
|
@@ -33,6 +33,19 @@ export function hasExactShape(value, shape) {
|
|
|
33
33
|
return (shape.required.every((key) => Object.hasOwn(value, key)) &&
|
|
34
34
|
Object.keys(value).every((key) => shape.allowed.has(key)));
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Narrows a record to the keys a shape allows. `undefined` entries are dropped
|
|
38
|
+
* so the result serializes the way {@link hasExactShape} reads it back.
|
|
39
|
+
*/
|
|
40
|
+
export function pickShape(value, shape) {
|
|
41
|
+
const picked = {};
|
|
42
|
+
for (const key of shape.allowed) {
|
|
43
|
+
const entry = value[key];
|
|
44
|
+
if (entry !== undefined)
|
|
45
|
+
picked[key] = entry;
|
|
46
|
+
}
|
|
47
|
+
return picked;
|
|
48
|
+
}
|
|
36
49
|
export function isFiniteNumber(value) {
|
|
37
50
|
return typeof value === 'number' && Number.isFinite(value);
|
|
38
51
|
}
|
|
@@ -174,76 +174,59 @@ function isAssignmentSensitiveKey(key) {
|
|
|
174
174
|
const suffix = sensitiveKeySegments(key).at(-1);
|
|
175
175
|
return suffix !== 'auth' && suffix !== 'authorization';
|
|
176
176
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
provider: '模型服务返回错误',
|
|
183
|
-
network: '网络错误',
|
|
184
|
-
},
|
|
185
|
-
'zh-TW': {
|
|
186
|
-
timeout: '請求逾時',
|
|
187
|
-
rateLimit: '已達模型速率限制',
|
|
188
|
-
authentication: '驗證失敗',
|
|
189
|
-
provider: '模型服務傳回錯誤',
|
|
190
|
-
network: '網路錯誤',
|
|
191
|
-
},
|
|
192
|
-
en: {
|
|
193
|
-
timeout: 'Request timed out',
|
|
194
|
-
rateLimit: 'Rate limit exceeded',
|
|
195
|
-
authentication: 'Authentication failed',
|
|
196
|
-
provider: 'Provider returned an error',
|
|
197
|
-
network: 'Network error',
|
|
198
|
-
},
|
|
199
|
-
};
|
|
200
|
-
function classifyGeneralizedError(error) {
|
|
177
|
+
/**
|
|
178
|
+
* Keyword classification shared by the localized message helpers and by
|
|
179
|
+
* producers that emit a stable machine code instead of prose.
|
|
180
|
+
*/
|
|
181
|
+
export function classifyGeneralizedError(error) {
|
|
201
182
|
const message = error instanceof Error ? error.message : String(error);
|
|
202
|
-
const
|
|
203
|
-
const lower = redacted.toLowerCase();
|
|
183
|
+
const lower = redactSecrets(message).toLowerCase();
|
|
204
184
|
if (lower.includes('timeout'))
|
|
205
185
|
return 'timeout';
|
|
206
186
|
if (lower.includes('429') || lower.includes('rate'))
|
|
207
|
-
return '
|
|
187
|
+
return 'rate_limited';
|
|
208
188
|
if (lower.includes('401') || lower.includes('403') || isAuthenticationErrorText(lower))
|
|
209
|
-
return '
|
|
210
|
-
if (
|
|
211
|
-
return '
|
|
189
|
+
return 'auth_failed';
|
|
190
|
+
if (/\b5\d\d\b/.test(lower))
|
|
191
|
+
return 'provider_error';
|
|
212
192
|
if (lower.includes('network') ||
|
|
213
193
|
lower.includes('fetch') ||
|
|
214
194
|
lower.includes('econn') ||
|
|
215
195
|
lower.includes('enotfound'))
|
|
216
|
-
return '
|
|
217
|
-
return
|
|
196
|
+
return 'network_error';
|
|
197
|
+
return undefined;
|
|
218
198
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
199
|
+
/** Locale copy for each {@link GeneralizedErrorClass}; catalog authors spread
|
|
200
|
+
* this per-locale block instead of restating the sentences. */
|
|
201
|
+
export const GENERALIZED_ERROR_COPY = {
|
|
202
|
+
'zh-CN': {
|
|
203
|
+
timeout: '请求超时',
|
|
204
|
+
rate_limited: '触发模型速率限制',
|
|
205
|
+
auth_failed: '鉴权失败',
|
|
206
|
+
provider_error: '模型服务返回错误',
|
|
207
|
+
network_error: '网络错误',
|
|
208
|
+
},
|
|
209
|
+
'zh-TW': {
|
|
210
|
+
timeout: '請求逾時',
|
|
211
|
+
rate_limited: '已達模型速率限制',
|
|
212
|
+
auth_failed: '驗證失敗',
|
|
213
|
+
provider_error: '模型服務傳回錯誤',
|
|
214
|
+
network_error: '網路錯誤',
|
|
215
|
+
},
|
|
216
|
+
en: {
|
|
217
|
+
timeout: 'Request timed out',
|
|
218
|
+
rate_limited: 'Rate limit exceeded',
|
|
219
|
+
auth_failed: 'Authentication failed',
|
|
220
|
+
provider_error: 'Provider returned an error',
|
|
221
|
+
network_error: 'Network error',
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
export function generalizedErrorMessageForLocale(error, fallback, locale) {
|
|
225
|
+
const classified = classifyGeneralizedError(error);
|
|
226
|
+
return classified ? GENERALIZED_ERROR_COPY[locale][classified] : fallback;
|
|
222
227
|
}
|
|
223
228
|
export function generalizedErrorMessage(error, fallback = 'Operation failed') {
|
|
224
|
-
return
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Chinese-locale companion to `generalizedErrorMessage()` (PR110b
|
|
228
|
-
* follow-up). Same classification rules; returns Chinese phrasing
|
|
229
|
-
* instead of English. Used by surfaces that must enforce a
|
|
230
|
-
* Chinese-only error copy contract (session start, onboarding setup
|
|
231
|
-
* banners, etc.) — the English version would have leaked through any
|
|
232
|
-
* matched category, breaking the gate.
|
|
233
|
-
*
|
|
234
|
-
* The fallback default is also Chinese so callers that don't supply
|
|
235
|
-
* one still produce a Chinese-only result. Pass a more specific
|
|
236
|
-
* Chinese fallback (e.g. "会话已创建但发送失败,请重试。") for better
|
|
237
|
-
* UX when the classifier can't categorize.
|
|
238
|
-
*/
|
|
239
|
-
export function generalizedErrorMessageChinese(error, fallback = '操作失败') {
|
|
240
|
-
return localizedGeneralizedErrorMessage(error, fallback, 'zh-CN');
|
|
241
|
-
}
|
|
242
|
-
export function generalizedErrorMessageTraditionalChinese(error, fallback = '操作失敗') {
|
|
243
|
-
return localizedGeneralizedErrorMessage(error, fallback, 'zh-TW');
|
|
244
|
-
}
|
|
245
|
-
export function generalizedErrorMessageForLocale(error, fallback, locale) {
|
|
246
|
-
return localizedGeneralizedErrorMessage(error, fallback, locale);
|
|
229
|
+
return generalizedErrorMessageForLocale(error, fallback, 'en');
|
|
247
230
|
}
|
|
248
231
|
export function isAuthenticationErrorText(message) {
|
|
249
232
|
return message.replace(/\bauthorit\w*/g, '').includes('auth');
|
|
@@ -65,7 +65,11 @@ const MODEL_CALL_STEP_SHAPE = defineObjectShape()([
|
|
|
65
65
|
'attempts',
|
|
66
66
|
'status',
|
|
67
67
|
], ['connectionSlug', 'historyCompactRoute', 'costUsd']);
|
|
68
|
-
|
|
68
|
+
/**
|
|
69
|
+
* The Inspector's own view of an attempt. Exported so the projection narrows an
|
|
70
|
+
* authority record through this list rather than restating it by hand.
|
|
71
|
+
*/
|
|
72
|
+
export const MODEL_ATTEMPT_SHAPE = defineObjectShape()([
|
|
69
73
|
'attemptId',
|
|
70
74
|
'attempt',
|
|
71
75
|
'status',
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
* specific language governing permissions and limitations
|
|
17
17
|
* under the License.
|
|
18
18
|
*/
|
|
19
|
-
import { projectModelCallUsageBuckets, projectModelCallUsageLogs, projectModelCallUsageSummary, } from './model-call-usage-projection.js';
|
|
20
19
|
/**
|
|
21
20
|
* The cost to present for a usage total, or `undefined` when it cannot be shown
|
|
22
21
|
* honestly. Trust the total only when at least one canonical attempt was priced;
|
|
@@ -66,8 +65,8 @@ export function legacyUsageProvenance(legacyRecords) {
|
|
|
66
65
|
pendingRepairs: 0,
|
|
67
66
|
};
|
|
68
67
|
}
|
|
69
|
-
export function mergeUsageSummary(legacy, canonical
|
|
70
|
-
const projected =
|
|
68
|
+
export function mergeUsageSummary(legacy, canonical) {
|
|
69
|
+
const projected = canonical.projection;
|
|
71
70
|
return {
|
|
72
71
|
range: projected.range,
|
|
73
72
|
totalRequests: legacy.totalRequests + projected.totalRequests,
|
|
@@ -95,17 +94,16 @@ export function mergeUsageSummary(legacy, canonical, query, now) {
|
|
|
95
94
|
},
|
|
96
95
|
};
|
|
97
96
|
}
|
|
98
|
-
export function mergeUsageBuckets(legacy, canonical
|
|
99
|
-
const projected = projectModelCallUsageBuckets(canonical.attempts, query, groupBy, now);
|
|
97
|
+
export function mergeUsageBuckets(legacy, canonical) {
|
|
100
98
|
const merged = new Map();
|
|
101
|
-
for (const bucket of [...legacy, ...
|
|
99
|
+
for (const bucket of [...legacy, ...canonical.projection.buckets]) {
|
|
102
100
|
const existing = merged.get(bucket.key);
|
|
103
101
|
merged.set(bucket.key, existing ? combineBuckets(existing, bucket) : { ...bucket });
|
|
104
102
|
}
|
|
105
103
|
return {
|
|
106
104
|
buckets: [...merged.values()].sort((left, right) => right.requests - left.requests),
|
|
107
105
|
provenance: {
|
|
108
|
-
coverage:
|
|
106
|
+
coverage: canonical.projection.coverage,
|
|
109
107
|
legacyRecords: legacy.reduce((total, bucket) => total + bucket.requests, 0),
|
|
110
108
|
unreadableRecords: canonical.unreadableRecords,
|
|
111
109
|
pendingRepairs: canonical.pendingRepairs,
|
|
@@ -117,8 +115,8 @@ export function mergeUsageBuckets(legacy, canonical, query, groupBy, now) {
|
|
|
117
115
|
* the merged order can only come from the first `offset + limit` rows of each —
|
|
118
116
|
* callers may bound their reads to that prefix and still page exactly.
|
|
119
117
|
*/
|
|
120
|
-
export function mergeUsageLogs(legacy, canonical,
|
|
121
|
-
const projected =
|
|
118
|
+
export function mergeUsageLogs(legacy, canonical, offset, limit) {
|
|
119
|
+
const projected = canonical.projection;
|
|
122
120
|
const rows = [];
|
|
123
121
|
let left = 0;
|
|
124
122
|
let right = 0;
|