@wacry/codeks-bridge 0.0.1 → 0.0.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/ui/detail.js CHANGED
@@ -3,11 +3,12 @@
3
3
  import { h, btn, fill, copyText, download, fmtBytes, fmtDateTime, fmtDur, fmtInt, fmtTime, kvTable, oneLine, safeName, shortId, strBlock, toast, tryJson, put } from './dom.js';
4
4
  import { jsonTree } from './tree.js';
5
5
  import { structureView, newStructState, focusPointer } from './structure.js';
6
- import { eventsView, newEventsState, finalObject, parseEvents, usageText } from './events.js';
6
+ import { eventsView, newEventsState, finalObject, inputTransformationReports, parseEvents, usageText } from './events.js';
7
7
  import { diffView } from './diff.js';
8
- import { annChip, annDetail, annIndex, callLabel, decisionClass, decisionText, END_LABEL, KIND_LABEL, KIND_TEXT, OUTCOME_LABEL, reasonText, sourceText } from './labels.js';
8
+ import { annChip, annDetail, annIndex, callLabel, decisionClass, decisionText, END_LABEL, INPUT_TRANSFORMATION_LABEL, KIND_LABEL, KIND_TEXT, OUTCOME_LABEL, reasonText, sourceText } from './labels.js';
9
9
  import { OUTCOME_CLASS, threadKey, byNewest } from './list.js';
10
10
  import { exportDoc } from './api.js';
11
+ import { responseLatency } from './latency.js';
11
12
 
12
13
  const RAW_LIMIT = 200_000;
13
14
  const VIEW_STATES = 80;
@@ -93,6 +94,28 @@ function fieldsTable(obj, hints, skip = []) {
93
94
  return kvTable(rows, 'fields-kv');
94
95
  }
95
96
 
97
+ /** Renders server-reported input changes separately from local replay annotations. */
98
+ function serverTransformations(resp) {
99
+ const reports = inputTransformationReports(resp);
100
+ if (!reports.length) return null;
101
+ return h('section', { class: 'server-transformations' },
102
+ h('div', { class: 'note' },
103
+ '上游服务端报告 · input_transformations。与 bridge 本地回放过滤分开记录;path 指向本次上游请求。丢弃 thinking 可能损失推理上下文,允许失配不代表已丢弃。'),
104
+ reports.map(report => {
105
+ const where = `${report.event === null ? 'JSON 正文' : `SSE 事件 #${report.event}`} ${report.pointer}`;
106
+ const entries = Array.isArray(report.value) ? report.value : null;
107
+ return h('div', { class: 'transformation-report' },
108
+ h('div', { class: 'sub-title mono' }, where),
109
+ entries?.length === 0 ? h('div', { class: 'dim' }, '服务端报告为空 []') : null,
110
+ entries ? entries.map(entry => h('details', { class: 'transformation-entry' },
111
+ h('summary', null,
112
+ h('span', { class: `chip ${entry?.type === 'thinking_dropped' ? 'warn' : 'info'}` },
113
+ INPUT_TRANSFORMATION_LABEL[entry?.type] ?? entry?.type ?? '未知变换'),
114
+ ' ', h('span', { class: 'mono' }, [entry?.path, entry?.reason].filter(x => x != null).map(x => typeof x === 'string' ? x : JSON.stringify(x)).join(' · '))),
115
+ jsonTree(entry, { auto: 2 }))) : jsonTree(report.value, { auto: 2 }));
116
+ }));
117
+ }
118
+
96
119
  const ROUTE_HINTS = {
97
120
  alias: '别名', catalog: '目录模型', adapter: '适配器', api: '上游 API', upstream: '上游配置', base_url: 'base_url', upstream_model: '上游模型',
98
121
  origin_id: '来源指纹', reasoning_format: 'reasoning 格式', native_compaction_format: '原生压缩格式', compaction_mode: '压缩模式',
@@ -380,7 +403,7 @@ export class DetailView {
380
403
  else {
381
404
  put(out, h('span', { class: `chip ${respClass(r)}` }, `${r.status ?? '–'} ${r.status_text ?? ''}`.trim()),
382
405
  h('span', { class: `chip ${r.end === 'active' ? 'info' : 'dim'}`, title: `end: ${r.end}` }, END_LABEL[r.end] ?? r.end ?? '–'),
383
- r.headers_ms != null ? h('span', { class: 'dim', title: '自请求开始到响应头的时间' }, `首包 ${fmtDur(r.headers_ms)}`) : null,
406
+ r.headers_ms != null ? h('span', { class: 'dim', title: '自请求开始到响应头的时间' }, `响应头 ${fmtDur(r.headers_ms)}`) : null,
384
407
  r.ended_ms != null ? h('span', { class: 'dim', title: '自请求开始到结束的时间' }, `结束 ${fmtDur(r.ended_ms)}`) : null,
385
408
  r.events ? h('span', { class: 'dim' }, `${fmtInt(r.events.length)} 事件`) : h('span', { class: 'dim' }, '非 SSE'),
386
409
  r.error ? h('span', { class: 'chip bad', title: r.error }, oneLine(`错误:${r.error}`, 80)) : null);
@@ -442,7 +465,9 @@ export class DetailView {
442
465
  return structureView(r.value, { state: st, ann, annChip: chip });
443
466
  }
444
467
  const fo = finalObject(resp);
445
- return structureView(fo.value, { state: st, ann, annChip: chip, note: fo.note });
468
+ return h('div', null,
469
+ stage.call ? serverTransformations(resp) : null,
470
+ structureView(fo.value, { state: st, ann, annChip: chip, note: fo.note }));
446
471
  }
447
472
 
448
473
  // ---------- overview ----------
@@ -452,10 +477,11 @@ export class DetailView {
452
477
  const box = h('div', { class: 'overview' });
453
478
  put(box, h('h4', null, '路由'), d.route ? fieldsTable(d.route, ROUTE_HINTS) : h('div', { class: 'dim' }, '没有路由信息(请求无法解析或路由失败)'));
454
479
  put(box, h('h4', null, '时间线'), this.timeline());
480
+ put(box, h('h4', null, '流式内容延迟'), this.latency());
455
481
  const keys = callKeys(d.upstream);
456
482
  if (d.upstream?.length) {
457
483
  put(box, h('h4', null, `上游调用(${d.upstream.length})`), h('table', { class: 'calls' },
458
- h('thead', null, h('tr', null, ['标签', '请求', '状态', '结束', '开始', '首包', '结束于', '请求大小', '响应大小', '错误'].map((x) => h('th', null, x)))),
484
+ h('thead', null, h('tr', null, ['标签', '请求', '状态', '结束', '开始', '响应头于', '结束于', '请求大小', '响应大小', '错误'].map((x) => h('th', null, x)))),
459
485
  h('tbody', null, d.upstream.map((c, i) => {
460
486
  const r = c.response;
461
487
  const tr = h('tr', { class: 'clickable', title: '打开该调用的上游响应', onclick: () => this.open(`upres:${keys[i]}`) },
@@ -475,6 +501,13 @@ export class DetailView {
475
501
  if (d.annotations?.length) {
476
502
  put(box, h('h4', null, '标注'), h('div', null, h('span', { class: 'chip info' }, annSummary(d.annotations)), ' ', btn('查看全部标注', () => this.open('ann'))));
477
503
  }
504
+ for (const [i, call] of (d.upstream ?? []).entries()) {
505
+ const reports = serverTransformations(call.response);
506
+ if (reports) {
507
+ put(box, h('h4', null, `服务端输入变换 · ${callLabel(call.label)} (${keys[i]})`),
508
+ btn('查看上游响应', () => this.open(`upres:${keys[i]}`)), reports);
509
+ }
510
+ }
478
511
  put(box, h('h4', null, '记录字段'), fieldsTable(d, SUMMARY_HINTS, ['route', 'client', 'upstream', 'annotations']));
479
512
  return box;
480
513
  }
@@ -494,13 +527,30 @@ export class DetailView {
494
527
  const bar = h('span', { class: `tl-bar ${r.cls}` });
495
528
  bar.style.left = pct(r.start);
496
529
  bar.style.width = pct(Math.max(end - r.start, total / 400));
497
- const mark = r.head != null ? h('span', { class: 'tl-head', title: `首包 ${fmtDur(r.head)}` }) : null;
530
+ const mark = r.head != null ? h('span', { class: 'tl-head', title: `响应头 ${fmtDur(r.head)}` }) : null;
498
531
  if (mark) mark.style.left = pct(r.head);
499
532
  return h('div', { class: 'tl-row clickable', onclick: () => this.open(r.key) },
500
533
  h('span', { class: 'tl-label' }, r.label),
501
534
  h('span', { class: 'tl-track' }, bar, mark),
502
535
  h('span', { class: 'tl-num mono' }, `${fmtDur(r.start)} → ${r.head != null ? fmtDur(r.head) : '–'} → ${r.end != null ? fmtDur(r.end) : d.active ? '…' : '–'}`));
503
- }), h('div', { class: 'dim tl-axis' }, `0 → ${fmtDur(total)}(开始 → 首包 → 结束,相对请求开始)`));
536
+ }), h('div', { class: 'dim tl-axis' }, `0 → ${fmtDur(total)}(开始 → 响应头 → 结束,相对请求开始)`));
537
+ }
538
+
539
+ latency() {
540
+ const d = this.detail;
541
+ const rows = (d.upstream ?? []).map(c => ({
542
+ label: `${callLabel(c.label)} → bridge`,
543
+ value: responseLatency(c.response, c.started_ms),
544
+ }));
545
+ rows.push({ label: 'bridge → codeks', value: responseLatency(d.client?.response, 0, true) });
546
+ return h('div', null,
547
+ h('div', { class: 'note' }, '上游从各自调用开始计时;写回 codeks 从客户端请求开始计时,包含前置 handoff。只计文字、reasoning 或工具内容,响应头和 keepalive 不计。这里是到达或写出时间,不是模型生成或 TUI 渲染时间。'),
548
+ h('table', { class: 'calls' },
549
+ h('thead', null, h('tr', null, ['方向', '首内容', '首文字', '首 reasoning', '首工具'].map(x => h('th', null, x)))),
550
+ h('tbody', null, rows.map(({ label, value }) => h('tr', null,
551
+ h('td', null, label),
552
+ ['content_ms', 'text_ms', 'reasoning_ms', 'tool_ms'].map(key => h('td', { class: 'mono' }, value?.[key] != null ? fmtDur(value[key]) : '—')))))),
553
+ h('div', { class: 'dim' }, '— 表示未观测到或无法测量;JSON 响应和缺少时间戳的导入不估算。上游 reasoning 可包含不透明载荷;写回 codeks 仅计可见摘要。工具以首次名称或参数为准,压缩密文不计入这些指标。'));
504
554
  }
505
555
 
506
556
  // ---------- annotations ----------
package/ui/events.js CHANGED
@@ -22,6 +22,30 @@ export function parseEvents(resp) {
22
22
  return list;
23
23
  }
24
24
 
25
+ /** Keeps each server report with its source; repeated SSE reports are not totals. */
26
+ export function inputTransformationReports(resp) {
27
+ const reports = [];
28
+ const add = (value, pointer, event = null) => {
29
+ if (value != null && Object.hasOwn(value, 'input_transformations')) {
30
+ reports.push({ event, pointer: `${pointer}/input_transformations`, value: value.input_transformations });
31
+ }
32
+ };
33
+ const events = parseEvents(resp);
34
+ if (events) {
35
+ for (const ev of events) {
36
+ if (ev.json?.type === 'message_start') add(ev.json.message, '/message', ev.i);
37
+ else if (ev.json?.type === 'message_delta') {
38
+ add(ev.json, '', ev.i);
39
+ add(ev.json.delta, '/delta', ev.i);
40
+ }
41
+ }
42
+ } else {
43
+ const parsed = tryJson(resp?.body?.text);
44
+ if (parsed.ok) add(parsed.value, '');
45
+ }
46
+ return reports;
47
+ }
48
+
25
49
  /** Returns {key, text, label} for streaming delta events of Responses, Messages and Chat. */
26
50
  function deltaOf(ev) {
27
51
  const j = ev.json;
@@ -311,6 +335,7 @@ function fromMessages(data) {
311
335
  } else if (j.type === 'message_delta') {
312
336
  Object.assign(msg, j.delta ?? {});
313
337
  if (j.usage) msg.usage = { ...(msg.usage ?? {}), ...j.usage };
338
+ if (Object.hasOwn(j, 'input_transformations')) msg.input_transformations = j.input_transformations;
314
339
  } else if (!['message_stop', 'ping'].includes(j.type)) extra.push(j);
315
340
  }
316
341
  for (const [i, s] of partial) if (blocks[i] && blocks[i].input === undefined) blocks[i].input = s;
package/ui/labels.js CHANGED
@@ -5,6 +5,11 @@ import { jsonTree } from './tree.js';
5
5
  export const CALL_LABEL = { main: '主请求', summary: '压缩·文字摘要', native: '压缩·原生', handoff: '压缩·交接复述' };
6
6
  export const callLabel = (l) => CALL_LABEL[l] ?? l;
7
7
 
8
+ export const INPUT_TRANSFORMATION_LABEL = {
9
+ thinking_dropped: '已丢弃 thinking',
10
+ thinking_mismatch_allowed: '允许失配,保留 thinking',
11
+ };
12
+
8
13
  export const KIND_LABEL = { turn: '轮次', compaction: '压缩', invalid: '无效' };
9
14
  export const OUTCOME_LABEL = { active: '进行中', completed: '完成', incomplete: '未完成', failed: '失败', error: '错误', aborted: '中止' };
10
15
  export const END_LABEL = { active: '传输中', complete: '完整', cancelled: '已取消', error: '出错', unread: '终止事件后停止读取' };
package/ui/latency.js ADDED
@@ -0,0 +1,88 @@
1
+ // Delivery times derived from captured SSE events, never from headers or usage.
2
+ const present = (value) => typeof value === 'string' && value.length > 0;
3
+ const array = (value) => Array.isArray(value) ? value : [];
4
+
5
+ function contentKinds(block, visibleOnly) {
6
+ if (!block || typeof block !== 'object') return [];
7
+ switch (block.type) {
8
+ case 'text': case 'output_text': case 'refusal':
9
+ return present(block.text ?? block.refusal) ? ['text'] : [];
10
+ case 'thinking':
11
+ return present(block.thinking) ? ['reasoning'] : [];
12
+ case 'summary_text': case 'reasoning_text':
13
+ return present(block.text) ? ['reasoning'] : [];
14
+ case 'redacted_thinking':
15
+ return !visibleOnly && present(block.data) ? ['reasoning'] : [];
16
+ case 'reasoning':
17
+ return [
18
+ ...array(block.summary).flatMap(part => contentKinds(part, visibleOnly)),
19
+ ...array(block.content).flatMap(part => contentKinds(part, visibleOnly)),
20
+ ...(!visibleOnly && present(block.encrypted_content) ? ['reasoning'] : []),
21
+ ];
22
+ case 'message':
23
+ return Array.isArray(block.content) ? block.content.flatMap(part => contentKinds(part, visibleOnly)) : [];
24
+ case 'function_call': case 'custom_tool_call': case 'tool_use':
25
+ return present(block.name) || present(block.arguments) || present(block.input) ? ['tool'] : [];
26
+ default: return [];
27
+ }
28
+ }
29
+
30
+ function eventKinds(data, visibleOnly) {
31
+ switch (data?.type) {
32
+ case 'response.output_text.delta': case 'response.output_text.done':
33
+ return present(data.delta ?? data.text) ? ['text'] : [];
34
+ case 'response.refusal.delta': case 'response.refusal.done':
35
+ return present(data.delta ?? data.refusal) ? ['text'] : [];
36
+ case 'response.reasoning_summary_text.delta': case 'response.reasoning_summary_text.done':
37
+ case 'response.reasoning_text.delta': case 'response.reasoning_text.done':
38
+ return present(data.delta ?? data.text) ? ['reasoning'] : [];
39
+ case 'response.function_call_arguments.delta': case 'response.function_call_arguments.done':
40
+ case 'response.custom_tool_call_input.delta': case 'response.custom_tool_call_input.done':
41
+ return present(data.delta ?? data.arguments ?? data.input) ? ['tool'] : [];
42
+ case 'response.output_item.added': case 'response.output_item.done':
43
+ return contentKinds(data.item, visibleOnly);
44
+ case 'response.content_part.added': case 'response.content_part.done':
45
+ case 'response.reasoning_summary_part.added': case 'response.reasoning_summary_part.done':
46
+ return contentKinds(data.part, visibleOnly);
47
+ case 'response.completed': case 'response.incomplete': case 'response.failed':
48
+ return array(data.response?.output).flatMap(item => contentKinds(item, visibleOnly));
49
+ case 'message_start':
50
+ return array(data.message?.content).flatMap(item => contentKinds(item, visibleOnly));
51
+ case 'content_block_start':
52
+ return contentKinds(data.content_block, visibleOnly);
53
+ case 'content_block_delta':
54
+ switch (data.delta?.type) {
55
+ case 'text_delta': return present(data.delta.text) ? ['text'] : [];
56
+ case 'thinking_delta': return present(data.delta.thinking) ? ['reasoning'] : [];
57
+ case 'input_json_delta': return present(data.delta.partial_json) ? ['tool'] : [];
58
+ default: return [];
59
+ }
60
+ default:
61
+ return array(data?.choices).flatMap(choice => {
62
+ const delta = choice?.delta ?? choice?.message;
63
+ if (!delta) return [];
64
+ return [
65
+ ...(present(delta.content) || present(delta.refusal) ? ['text'] : []),
66
+ ...(present(delta.reasoning_content) ? ['reasoning'] : []),
67
+ ...(array(delta.tool_calls).some(call => present(call.function?.name) || present(call.function?.arguments)) ? ['tool'] : []),
68
+ ];
69
+ });
70
+ }
71
+ }
72
+
73
+ /** Null means no timed SSE capture; individual null fields mean not observed. */
74
+ export function responseLatency(response, startedMs, visibleOnly = false) {
75
+ if (!Array.isArray(response?.events) || !Number.isFinite(startedMs)) return null;
76
+ const first = { content_ms: null, text_ms: null, reasoning_ms: null, tool_ms: null };
77
+ for (const event of response.events) {
78
+ if (!Number.isFinite(event.t) || event.t < startedMs) continue;
79
+ let data;
80
+ try { data = JSON.parse(event.data); } catch { continue; }
81
+ for (const kind of eventKinds(data, visibleOnly)) {
82
+ const elapsed = Math.round((event.t - startedMs) * 10) / 10;
83
+ first.content_ms ??= elapsed;
84
+ first[`${kind}_ms`] ??= elapsed;
85
+ }
86
+ }
87
+ return first;
88
+ }