lark-remote 0.5.3 → 0.5.4
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/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.5.4] - 2026-10-12
|
|
9
|
+
|
|
10
|
+
### 修复
|
|
11
|
+
|
|
12
|
+
- **codex「本 run」token 少算(多步运行只统计了最后一步)**:codex 每发起一次模型调用就记一条 token 用量,一个回合里有几趟工具往返就有几条,而运行结束卡片与 `/resume` 末尾统计此前只取最后一条。于是一个跑了 7 趟工具往返的回合,实际输出 3,648 个 token 只显示 679、未命中缓存的输入 3,022 只显示 212。现在按回合汇总该回合内所有调用的用量,与 codex 自己按回合统计的结果逐字段一致;「Context」仍显示最后一次调用的上下文窗口占用,累计用量与 `Total` 的取法不变。恢复会话时服务端可能重放上回合的用量,已按回合标识丢弃。更早的、不带回合标识的记录保持原读法
|
|
13
|
+
|
|
14
|
+
### 变更
|
|
15
|
+
|
|
16
|
+
- **测试注释与用例类型对齐**:一处用例注释改为不点名具体消息;两处 app-server 用例的类型写法与协议定义对齐(这些文件不在既有类型检查覆盖范围内,改动不影响运行时行为)。无行为变化
|
|
17
|
+
|
|
8
18
|
## [0.5.3] - 2026-10-12
|
|
9
19
|
|
|
10
20
|
### 修复
|
|
@@ -222,6 +222,11 @@ export class CodexAppServerTranslator {
|
|
|
222
222
|
this.threadId = params.threadId;
|
|
223
223
|
this.turnId = params.turn.id;
|
|
224
224
|
this.turnStatus = params.turn.status ?? 'inProgress';
|
|
225
|
+
// 单 turn 用量从零开始累计:turn 内每次采样都会发一条
|
|
226
|
+
// `thread/tokenUsage/updated`,本 run 的用量是这些增量之和(不是最后一条)。
|
|
227
|
+
// resume 后服务端重放的上一 turn 用量若在此之后到达,由 handleTokenUsageUpdated
|
|
228
|
+
// 的 turnId 闸门挡掉。
|
|
229
|
+
this.usage = { inputTokens: 0, outputTokens: 0 };
|
|
225
230
|
this.textByItem.clear();
|
|
226
231
|
this.reasoningByItem.clear();
|
|
227
232
|
this.toolOutputByItem.clear();
|
|
@@ -455,22 +460,31 @@ export class CodexAppServerTranslator {
|
|
|
455
460
|
];
|
|
456
461
|
}
|
|
457
462
|
handleTokenUsageUpdated(params) {
|
|
463
|
+
// 归属闸门:resume 之后服务端可能重放上一 turn 的 tokenUsage(turnId 不同),
|
|
464
|
+
// 计入会把上一 turn 的消耗算成本 run。本轮 turnId 由 turn/started 确立。
|
|
465
|
+
if (params.turnId && this.turnId && params.turnId !== this.turnId) {
|
|
466
|
+
return [];
|
|
467
|
+
}
|
|
458
468
|
const last = params.tokenUsage.last;
|
|
459
469
|
// 协议原生 inputTokens 含 cached(total = input + output),而 lark-remote 的
|
|
460
470
|
// 卡片契约是 ccusage 口径「input_tokens 表示未命中缓存的输入」(design §9.21)。
|
|
461
471
|
// 在入口归一,否则 done 卡 Input 会等于 Context、cache% 分母重复计数、
|
|
462
472
|
// Total 被 max(total, input+output+cache) 抬高一倍(实测 364K → 727K)。
|
|
463
473
|
// jsonl 链路有同一处减法(session/codex/rollout-reader.ts buildUsageFromScan)。
|
|
474
|
+
// 单 turn 口径:一个 turn 内每次采样各发一条通知(实测 7 次工具往返 = 7 条),
|
|
475
|
+
// 本 run 是这些增量之和——只取最后一条会把多步 run 低报(实测 3,648 output 显示成 679)。
|
|
464
476
|
const cachedInputTokens = last.cachedInputTokens === undefined
|
|
465
477
|
? undefined
|
|
466
478
|
: Math.min(last.cachedInputTokens, last.inputTokens);
|
|
467
|
-
this.usage
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
cachedInputTokens
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
479
|
+
this.usage.inputTokens += last.inputTokens - (cachedInputTokens ?? 0);
|
|
480
|
+
this.usage.outputTokens += last.outputTokens;
|
|
481
|
+
if (cachedInputTokens !== undefined) {
|
|
482
|
+
this.usage.cachedInputTokens = (this.usage.cachedInputTokens ?? 0) + cachedInputTokens;
|
|
483
|
+
}
|
|
484
|
+
if (last.totalTokens !== undefined) {
|
|
485
|
+
this.usage.totalTokens = (this.usage.totalTokens ?? 0) + last.totalTokens;
|
|
486
|
+
}
|
|
487
|
+
this.usage.contextLimit = params.tokenUsage.modelContextWindow ?? this.usage.contextLimit;
|
|
474
488
|
return [];
|
|
475
489
|
}
|
|
476
490
|
/**
|
|
@@ -42,6 +42,9 @@ function subtractRawUsage(a, b) {
|
|
|
42
42
|
total_tokens: a.total_tokens - b.total_tokens,
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
+
function emptyTurnUsage() {
|
|
46
|
+
return { input: 0, cached: 0, output: 0, total: 0 };
|
|
47
|
+
}
|
|
45
48
|
/**
|
|
46
49
|
* `compacted` 事件自带的摘要调用 usage(`latest_token_usage_record.usage`)。
|
|
47
50
|
* 用于把「压缩前窗口」与「摘要调用自身的消耗」区分开——两者都写在 compacted 前。
|
|
@@ -141,12 +144,19 @@ function scanCodexRollout(filePath, includeEvents) {
|
|
|
141
144
|
// exactly the 0.153 compatibility incident shape.
|
|
142
145
|
let userResponseItemCount = 0;
|
|
143
146
|
// token_count tracking: codex emits cumulative `total_token_usage` and an
|
|
144
|
-
// incremental `last_token_usage
|
|
145
|
-
//
|
|
147
|
+
// incremental `last_token_usage` **per sampling call** — one turn with tool
|
|
148
|
+
// round-trips logs several `token_count` lines. We want the LAST turn's usage
|
|
149
|
+
// = the sum of that turn's samplings (matches pi/opencode /resume "last turn"
|
|
150
|
+
// display semantics). Turn boundaries come from `task_started`. When
|
|
146
151
|
// last_token_usage is absent we derive it as total - previous_total.
|
|
147
152
|
let lastRawUsage;
|
|
148
153
|
/** 上一条被接受的 raw usage;compacted 分支回退一条时用(见下方注释)。 */
|
|
149
154
|
let previousRawUsage;
|
|
155
|
+
/**
|
|
156
|
+
* 末个 turn 的累计用量;`null` = 文件里没有 `task_started`(旧数据),
|
|
157
|
+
* 此时 flow 字段退回「末条 token_count」语义(见 buildUsageFromScan)。
|
|
158
|
+
*/
|
|
159
|
+
let turnUsage = null;
|
|
150
160
|
// Context window limit from the same token_count event as lastRawUsage
|
|
151
161
|
// (codex reports info.model_context_window per turn; absent on old data).
|
|
152
162
|
let lastContextLimit;
|
|
@@ -194,6 +204,12 @@ function scanCodexRollout(filePath, includeEvents) {
|
|
|
194
204
|
}
|
|
195
205
|
lastUserMessageSource = human.source;
|
|
196
206
|
}
|
|
207
|
+
else if (payload.type === 'task_started') {
|
|
208
|
+
// turn 边界:一个 turn 一条 task_started(与 task_complete 成对,
|
|
209
|
+
// turn_id 见 session_meta 之后的 `turn_context`)。从这里开始重新
|
|
210
|
+
// 累计本 turn 的用量。
|
|
211
|
+
turnUsage = emptyTurnUsage();
|
|
212
|
+
}
|
|
197
213
|
else if (payload.type === 'token_count') {
|
|
198
214
|
const info = payload.info;
|
|
199
215
|
const last = info?.last_token_usage;
|
|
@@ -217,6 +233,13 @@ function scanCodexRollout(filePath, includeEvents) {
|
|
|
217
233
|
lastRawUsage = raw;
|
|
218
234
|
const ctxWindow = info?.model_context_window;
|
|
219
235
|
lastContextLimit = typeof ctxWindow === 'number' ? ctxWindow : undefined;
|
|
236
|
+
if (turnUsage) {
|
|
237
|
+
const cached = Math.min(raw.cached_input_tokens, raw.input_tokens);
|
|
238
|
+
turnUsage.input += raw.input_tokens - cached;
|
|
239
|
+
turnUsage.cached += cached;
|
|
240
|
+
turnUsage.output += raw.output_tokens;
|
|
241
|
+
turnUsage.total += raw.total_tokens;
|
|
242
|
+
}
|
|
220
243
|
}
|
|
221
244
|
// 压缩后的水位:压缩收尾 token_count 全 0(raw 不满足上面 zero-filter),
|
|
222
245
|
// 但 total_tokens 表达真实窗口;之后若出现普通 turn(input>0),
|
|
@@ -288,6 +311,7 @@ function scanCodexRollout(filePath, includeEvents) {
|
|
|
288
311
|
lastContextLimit,
|
|
289
312
|
lastTotalUsage,
|
|
290
313
|
previousTotals,
|
|
314
|
+
turnUsage,
|
|
291
315
|
compactCount,
|
|
292
316
|
compactPreContextLength,
|
|
293
317
|
compactPostContextLength,
|
|
@@ -298,25 +322,38 @@ function scanCodexRollout(filePath, includeEvents) {
|
|
|
298
322
|
* input = raw - cached (codex reports cached input separately), cache
|
|
299
323
|
* creation is never reported by codex, reasoning is a subset of output
|
|
300
324
|
* (display-only, never added to total).
|
|
325
|
+
*
|
|
326
|
+
* Flow fields (input/output/cache/total) are the **sum over the last turn's
|
|
327
|
+
* samplings** when the file marks turn boundaries (`task_started`) — one codex
|
|
328
|
+
* turn logs one `token_count` per sampling, so the last line alone understates
|
|
329
|
+
* a tool-looping run (measured: 212 / 679 displayed vs 3,022 / 3,648 consumed).
|
|
330
|
+
* Legacy files without `task_started` keep the last-`token_count` semantics.
|
|
301
331
|
* contextLength = input_tokens (= (input_tokens-cached) + cached + 0),
|
|
302
332
|
* i.e. input + cacheRead + cacheCreation — the unified context-window
|
|
303
333
|
* occupancy contract across all five readers (excludes
|
|
304
334
|
* output/reasoning).
|
|
305
335
|
*/
|
|
306
336
|
function buildUsageFromScan(scan) {
|
|
307
|
-
const { lastRawUsage, lastContextLimit, lastTotalUsage, compactCount, compactPreContextLength, compactPostContextLength, } = scan;
|
|
337
|
+
const { lastRawUsage, lastContextLimit, lastTotalUsage, turnUsage, compactCount, compactPreContextLength, compactPostContextLength, } = scan;
|
|
308
338
|
if (!lastRawUsage)
|
|
309
339
|
return undefined;
|
|
310
340
|
const r = lastRawUsage;
|
|
311
341
|
const cached = Math.min(r.cached_input_tokens, r.input_tokens);
|
|
342
|
+
// 本 run = 末个 turn 全部采样之和;旧数据(无 task_started)退回末条。
|
|
343
|
+
const flow = turnUsage ?? {
|
|
344
|
+
input: r.input_tokens - cached,
|
|
345
|
+
cached,
|
|
346
|
+
output: r.output_tokens,
|
|
347
|
+
total: r.total_tokens,
|
|
348
|
+
};
|
|
312
349
|
const usage = {
|
|
313
|
-
inputTokens:
|
|
314
|
-
outputTokens:
|
|
350
|
+
inputTokens: flow.input,
|
|
351
|
+
outputTokens: flow.output,
|
|
315
352
|
contextLength: r.input_tokens,
|
|
316
353
|
contextLimit: lastContextLimit,
|
|
317
|
-
cacheReadTokens: cached,
|
|
354
|
+
cacheReadTokens: flow.cached,
|
|
318
355
|
cacheCreationTokens: 0,
|
|
319
|
-
totalTokens:
|
|
356
|
+
totalTokens: flow.total,
|
|
320
357
|
};
|
|
321
358
|
// Cumulative from the final total_token_usage (non-cached input +
|
|
322
359
|
// output), summed across all turns in the session.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lark-remote",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "Feishu (Lark) private-chat bridge to local coding agents — Claude Code / Codex / opencode / pi / kimi, with live-streaming CardKit 2.0 cards",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|