dsh-neartoken 0.17.0 → 0.17.1
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 +2 -0
- package/lib/client.js +94 -94
- package/lib/index.js +212 -40
- package/lib/types/host/anthropic-wire.d.ts +7 -0
- package/lib/types/host/request-body.d.ts +48 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -228,6 +228,8 @@ Gemini 系走原生 surface 而不是 openai 兼容层:同模型同提示词
|
|
|
228
228
|
|
|
229
229
|
- 四套 wire(chat / anthropic / genai / responses)各自解析 SSE;**图片按各协议发送** —— chat 用 `image_url` part、anthropic 用 `source.base64`、genai 用 `inlineData`、responses 用 `input_image`。发送前先按 `imageMaxEdge` / `imageMaxBytes` 重编码(见「图片压缩」),所以线上字节与媒体类型未必等于附件里的原图。**工具结果里的图片也发**(`read_image` 的结果就是一段文本信封 + 一张图):anthropic 把图塞回同一个 `tool_result` 的 content 块数组;genai 在同一 user turn 的 parts 里补 `inlineData`;chat 的 tool 消息只能放字符串、responses 的 `function_call_output` 同理,所以图跟在**整段连续工具结果之后**的一条 user 消息 / 条目里(并行调用时不会把 tool 消息切断)。字节由 `attachments.readImage` 在请求前内联;没有 attachments 服务或读不出来会**让请求失败**,不再静默降级成纯文本(曾经就是这样:贴的截图模型根本收不到)。会话里被 image-offload 标记的图片(`offloaded: true`)不发字节;插件也不替它生成占位文本,所以那一次图片对模型完全不可见。文件块(非图片附件)仍不发;`/model` 弹窗仍是内置平铺列表;箭头键盘导航未接。
|
|
230
230
|
- **genai 线的并行工具调用必须收在同一个 user turn 里**:Gemini 校验「一个 functionCall turn ↔ 紧随其后那条 user turn 里等量的 functionResponse parts」,每条工具结果各开一条 user turn 会被上游拒成 400 `Please ensure that the number of function response parts is equal to the number of function call parts of the function call turn`。它是**确定性**错误:历史不变就必然复现,重试、换分组都无用,只能新开会话(0.16.1 及以前踩中过一次:一轮发了 6 个并行 `read`,整轮就此作废)。0.16.2 起把整段连续的工具结果折进同一个 user turn;Gemini ≥ 3 另给 `functionCall` / `functionResponse` 带上 `id`(同一个函数被并行调多次时才分得清谁答谁),`gemma*` 与更早的 Gemini 不发该字段,只按函数名匹配。
|
|
231
|
+
- **anthropic 线的工具结果同样必须合批**:messages API 校验「一条 assistant turn 的 `tool_use` ↔ 紧随其后那条 user turn 的 `tool_result`」一一对应。并行调用时若每条结果各开一条 user 消息,网关报 400 `Invalid message sequence: tool_use and tool_result blocks must be correctly paired and ordered`(0.17.1 以前 anthropic 线就是这个形状 —— genai 线 0.16.2 修了同类问题,anthropic 线漏了)。现在整段连续结果折进紧邻 assistant 的那一条 user 消息;结果没记录(中断、fork、压缩裁剪)的调用补一句占位结果;对不上任何调用的结果丢弃;批内混进来的 user 文本移到结果之后;相邻同角色轮次合并、`tool_result` 固定排在 user 消息首位。这条是**请求体形状**的错,与历史记录无关:升级后此前必挂的会话下一轮就能继续。
|
|
232
|
+
- **请求体按需 brotli 压缩**:每次请求都带全量工具定义,body 常在几十到几百 KB,带截图时上 MB。上游(New API 的 `DecompressRequestMiddleware`)在解析前解压 `content-encoding` 为 gzip / br / zstd 的 body,实测三条 wire(chat / messages / responses)全通;deflate 不支持,且不带该头就按明文解析。所以 **≥16 KB 的 body 以 brotli quality 5 发送**:实测 57 KB 的工具密集请求压到 468 B、2 MB 真实源码压到 418 KB(19%)。四条聊天 wire 与出图的三条通道(`images` / `genai` / `chat`)共用同一个出口 `postJson`,阈值与回落只有一份 —— 带参考图的出图请求是另一个 base64 大户。选型依据:同 CPU 下 brotli 比 gzip 小 8–14%;zstd level 3 与 gzip 等大但快 5 倍,只在 CPU 吃紧时更优;而 brotli 的**运行时默认 quality 是 11**,2 MB 上要 3.7 秒,必须显式钉 5。低于阈值走明文 —— 那么小的上行不值一次压缩加解压。图片主导的请求收益有天花板(base64 编码的已压缩图只能压到 75%,那 25% 只是还原 base64 膨胀),真正的杠杆仍是 `imageMaxEdge` / `imageMaxBytes`。若上游某天不认该编码(400 且 body 解析失败),同一请求立刻以明文重发一次:被拒的请求从未计费,代价是一个往返而不是整次调用。
|
|
231
233
|
- 四套 wire 的 SSE 解析都有「帧样本 → 期望 chunk 序列」回归测试(`src/host/wire-parse.spec.ts`),帧样本取自生产网关的真实响应。网关在内容帧上发 `"usage": null`,只有末帧带真值:任何一处把 `usage` 当对象直接读都会让整条流中途崩掉(曾导致 claude 模型全不可用)。
|
|
232
234
|
- **每个请求都带会话亲和键**:chat 与 responses 发 `prompt_cache_key`,anthropic 发 `metadata.user_id`,取值都是 DSH 的 `GenerateOptions.sessionId`。站点就是靠这个值挑上游渠道的(New API 的「codex cli trace」规则:`^gpt-.*$` + `/v1/responses`,键取自 body 的 `prompt_cache_key`,取不到就把整条规则跳过),而提示词缓存按**上游账号**存 —— 不带键时每个请求随机换账号,缓存永远不中。实测同一 6k 前缀:不带键连发 4 次 `cached_tokens` 全 0;带键后第 2 次起 5888/6011 ≈ 98%。`/v1/chat/completions` 上目前没有对应规则,chat 线即使带键也只是偶发命中,要彻底解决需要站点加一条匹配该路径的亲和规则;anthropic 线的 `metadata.user_id` 同理(规则只作用于 `/v1/messages`,这一条已经匹配)。
|
|
233
235
|
- **anthropic 线显式打缓存断点**:Anthropic 只在 `cache_control` 断点处写缓存,没有断点就整段 prompt 按新输入计价(亲和键只保证落在同一上游账号,不产生缓存)。每个请求打 4 个断点:`tools` 末项、`system` 末块、最后两条消息各自末块 —— harness 每轮全量重放历史,尾部双断点保证下一轮几乎全命中,每轮只写新增片段。实测(Claude Max 超惠分组,~6.3k token 前缀):无断点连发两次 `cache_read` 全 0;打点后第 2 次起 `in=4, cr=6316`,三轮增长历史 `cr` 累积到 10811、`in` 恒为个位数。断点也覆盖 `tool_result` 结尾的轮次(DSH 会话的常态形态)。
|
package/lib/client.js
CHANGED
|
@@ -401,23 +401,23 @@ window.__ModuleLoader__.load({
|
|
|
401
401
|
document.head.appendChild(tag);
|
|
402
402
|
}
|
|
403
403
|
var group_tags_module_css_default = {
|
|
404
|
-
"
|
|
405
|
-
"
|
|
404
|
+
"toneIndigo": "FotuUq_toneIndigo",
|
|
405
|
+
"toneLime": "FotuUq_toneLime",
|
|
406
406
|
"toneAmber": "FotuUq_toneAmber",
|
|
407
|
+
"ratioPricey": "FotuUq_ratioPricey",
|
|
408
|
+
"toneCyan": "FotuUq_toneCyan",
|
|
409
|
+
"toneTeal": "FotuUq_toneTeal",
|
|
410
|
+
"tonePink": "FotuUq_tonePink",
|
|
411
|
+
"tag": "FotuUq_tag",
|
|
412
|
+
"toneFuchsia": "FotuUq_toneFuchsia",
|
|
413
|
+
"ratioBase": "FotuUq_ratioBase",
|
|
407
414
|
"toneRose": "FotuUq_toneRose",
|
|
408
|
-
"
|
|
409
|
-
"toneLime": "FotuUq_toneLime",
|
|
415
|
+
"toneViolet": "FotuUq_toneViolet",
|
|
410
416
|
"ratioChip": "FotuUq_ratioChip",
|
|
411
417
|
"toneEmerald": "FotuUq_toneEmerald",
|
|
412
|
-
"toneViolet": "FotuUq_toneViolet",
|
|
413
|
-
"toneIndigo": "FotuUq_toneIndigo",
|
|
414
|
-
"toneTeal": "FotuUq_toneTeal",
|
|
415
|
-
"ratioPricey": "FotuUq_ratioPricey",
|
|
416
|
-
"ratioBase": "FotuUq_ratioBase",
|
|
417
418
|
"toneSky": "FotuUq_toneSky",
|
|
418
|
-
"
|
|
419
|
-
"
|
|
420
|
-
"tonePink": "FotuUq_tonePink"
|
|
419
|
+
"ratioCheap": "FotuUq_ratioCheap",
|
|
420
|
+
"toneOrange": "FotuUq_toneOrange"
|
|
421
421
|
};
|
|
422
422
|
//#endregion
|
|
423
423
|
//#region src/client/group-tags.tsx
|
|
@@ -472,39 +472,39 @@ window.__ModuleLoader__.load({
|
|
|
472
472
|
document.head.appendChild(tag);
|
|
473
473
|
}
|
|
474
474
|
var ModelGroupSelect_module_css_default = {
|
|
475
|
-
"root": "_2pt-bW_root",
|
|
476
|
-
"warning": "_2pt-bW_warning",
|
|
477
|
-
"optionTrailing": "_2pt-bW_optionTrailing",
|
|
478
|
-
"cellLabel": "_2pt-bW_cellLabel",
|
|
479
|
-
"cellValue": "_2pt-bW_cellValue",
|
|
480
|
-
"optionCopy": "_2pt-bW_optionCopy",
|
|
481
475
|
"groups": "_2pt-bW_groups",
|
|
482
|
-
"
|
|
483
|
-
"searchRow": "_2pt-bW_searchRow",
|
|
484
|
-
"status": "_2pt-bW_status",
|
|
485
|
-
"triggerIcon": "_2pt-bW_triggerIcon",
|
|
476
|
+
"groupOptionLabel": "_2pt-bW_groupOptionLabel",
|
|
486
477
|
"menu": "_2pt-bW_menu",
|
|
478
|
+
"empty": "_2pt-bW_empty",
|
|
479
|
+
"optionCopy": "_2pt-bW_optionCopy",
|
|
480
|
+
"root": "_2pt-bW_root",
|
|
481
|
+
"cellLabel": "_2pt-bW_cellLabel",
|
|
487
482
|
"searchClear": "_2pt-bW_searchClear",
|
|
488
|
-
"
|
|
489
|
-
"trigger": "_2pt-bW_trigger",
|
|
490
|
-
"groupInline": "_2pt-bW_groupInline",
|
|
491
|
-
"optionActive": "_2pt-bW_optionActive",
|
|
492
|
-
"chevron": "_2pt-bW_chevron",
|
|
493
|
-
"modelName": "_2pt-bW_modelName",
|
|
494
|
-
"triggerEffort": "_2pt-bW_triggerEffort",
|
|
495
|
-
"triggerLabel": "_2pt-bW_triggerLabel",
|
|
483
|
+
"toast": "_2pt-bW_toast",
|
|
496
484
|
"retry": "_2pt-bW_retry",
|
|
485
|
+
"groupName": "_2pt-bW_groupName",
|
|
486
|
+
"trigger": "_2pt-bW_trigger",
|
|
487
|
+
"modelOption": "_2pt-bW_modelOption",
|
|
488
|
+
"triggerIcon": "_2pt-bW_triggerIcon",
|
|
497
489
|
"error": "_2pt-bW_error",
|
|
490
|
+
"triggerEffort": "_2pt-bW_triggerEffort",
|
|
491
|
+
"modelName": "_2pt-bW_modelName",
|
|
498
492
|
"variantCount": "_2pt-bW_variantCount",
|
|
493
|
+
"option": "_2pt-bW_option",
|
|
494
|
+
"cellValue": "_2pt-bW_cellValue",
|
|
495
|
+
"warning": "_2pt-bW_warning",
|
|
496
|
+
"optionTrailing": "_2pt-bW_optionTrailing",
|
|
499
497
|
"selected": "_2pt-bW_selected",
|
|
500
|
-
"
|
|
498
|
+
"cellChevron": "_2pt-bW_cellChevron",
|
|
499
|
+
"triggerLabel": "_2pt-bW_triggerLabel",
|
|
501
500
|
"search": "_2pt-bW_search",
|
|
502
|
-
"
|
|
503
|
-
"
|
|
501
|
+
"chevron": "_2pt-bW_chevron",
|
|
502
|
+
"optionActive": "_2pt-bW_optionActive",
|
|
503
|
+
"groupInline": "_2pt-bW_groupInline",
|
|
504
|
+
"check": "_2pt-bW_check",
|
|
505
|
+
"searchRow": "_2pt-bW_searchRow",
|
|
504
506
|
"cell": "_2pt-bW_cell",
|
|
505
|
-
"
|
|
506
|
-
"modelOption": "_2pt-bW_modelOption",
|
|
507
|
-
"empty": "_2pt-bW_empty"
|
|
507
|
+
"status": "_2pt-bW_status"
|
|
508
508
|
};
|
|
509
509
|
//#endregion
|
|
510
510
|
//#region src/client/ModelGroupSelect.tsx
|
|
@@ -1070,77 +1070,77 @@ window.__ModuleLoader__.load({
|
|
|
1070
1070
|
document.head.appendChild(tag);
|
|
1071
1071
|
}
|
|
1072
1072
|
var NearTokenSection_module_css_default = {
|
|
1073
|
-
"
|
|
1073
|
+
"metaLabel": "_02HI2q_metaLabel",
|
|
1074
|
+
"codeRow": "_02HI2q_codeRow",
|
|
1075
|
+
"tipTitle": "_02HI2q_tipTitle",
|
|
1076
|
+
"name": "_02HI2q_name",
|
|
1077
|
+
"linkLine": "_02HI2q_linkLine",
|
|
1078
|
+
"metaEditor": "_02HI2q_metaEditor",
|
|
1079
|
+
"fact": "_02HI2q_fact",
|
|
1080
|
+
"chevronOpen": "_02HI2q_chevronOpen",
|
|
1081
|
+
"valueRow": "_02HI2q_valueRow",
|
|
1082
|
+
"form": "_02HI2q_form",
|
|
1083
|
+
"link": "_02HI2q_link",
|
|
1084
|
+
"count": "_02HI2q_count",
|
|
1085
|
+
"code": "_02HI2q_code",
|
|
1086
|
+
"metaTitle": "_02HI2q_metaTitle",
|
|
1087
|
+
"metaIntro": "_02HI2q_metaIntro",
|
|
1088
|
+
"countPicked": "_02HI2q_countPicked",
|
|
1089
|
+
"linkRow": "_02HI2q_linkRow",
|
|
1090
|
+
"rowName": "_02HI2q_rowName",
|
|
1091
|
+
"list": "_02HI2q_list",
|
|
1092
|
+
"voucherEmpty": "_02HI2q_voucherEmpty",
|
|
1093
|
+
"title": "_02HI2q_title",
|
|
1094
|
+
"metaField": "_02HI2q_metaField",
|
|
1074
1095
|
"factValue": "_02HI2q_factValue",
|
|
1096
|
+
"cardToggle": "_02HI2q_cardToggle",
|
|
1097
|
+
"topupBar": "_02HI2q_topupBar",
|
|
1098
|
+
"label": "_02HI2q_label",
|
|
1099
|
+
"metaBtn": "_02HI2q_metaBtn",
|
|
1100
|
+
"search": "_02HI2q_search",
|
|
1101
|
+
"voucherAmount": "_02HI2q_voucherAmount",
|
|
1102
|
+
"topupInput": "_02HI2q_topupInput",
|
|
1075
1103
|
"notice": "_02HI2q_notice",
|
|
1076
1104
|
"tipRow": "_02HI2q_tipRow",
|
|
1077
|
-
"
|
|
1078
|
-
"tipGroup": "_02HI2q_tipGroup",
|
|
1079
|
-
"count": "_02HI2q_count",
|
|
1080
|
-
"voucherGroup": "_02HI2q_voucherGroup",
|
|
1081
|
-
"select": "_02HI2q_select",
|
|
1105
|
+
"autosave": "_02HI2q_autosave",
|
|
1082
1106
|
"metaHint": "_02HI2q_metaHint",
|
|
1083
|
-
"
|
|
1084
|
-
"
|
|
1085
|
-
"voucherEmpty": "_02HI2q_voucherEmpty",
|
|
1086
|
-
"topup": "_02HI2q_topup",
|
|
1107
|
+
"chevron": "_02HI2q_chevron",
|
|
1108
|
+
"invite": "_02HI2q_invite",
|
|
1087
1109
|
"facts": "_02HI2q_facts",
|
|
1088
|
-
"fact": "_02HI2q_fact",
|
|
1089
1110
|
"badge": "_02HI2q_badge",
|
|
1090
|
-
"
|
|
1091
|
-
"
|
|
1092
|
-
"
|
|
1093
|
-
"
|
|
1094
|
-
"
|
|
1095
|
-
"
|
|
1096
|
-
"
|
|
1097
|
-
"
|
|
1098
|
-
"
|
|
1111
|
+
"rowMeta": "_02HI2q_rowMeta",
|
|
1112
|
+
"select": "_02HI2q_select",
|
|
1113
|
+
"voucherGroup": "_02HI2q_voucherGroup",
|
|
1114
|
+
"hint": "_02HI2q_hint",
|
|
1115
|
+
"rowRatio": "_02HI2q_rowRatio",
|
|
1116
|
+
"tipGroup": "_02HI2q_tipGroup",
|
|
1117
|
+
"row": "_02HI2q_row",
|
|
1118
|
+
"toolbar": "_02HI2q_toolbar",
|
|
1119
|
+
"metaBtnOn": "_02HI2q_metaBtnOn",
|
|
1120
|
+
"topupChips": "_02HI2q_topupChips",
|
|
1121
|
+
"cardHead": "_02HI2q_cardHead",
|
|
1122
|
+
"voucherList": "_02HI2q_voucherList",
|
|
1123
|
+
"topup": "_02HI2q_topup",
|
|
1099
1124
|
"intro": "_02HI2q_intro",
|
|
1100
|
-
"
|
|
1125
|
+
"metaInput": "_02HI2q_metaInput",
|
|
1126
|
+
"badgeOn": "_02HI2q_badgeOn",
|
|
1101
1127
|
"handle": "_02HI2q_handle",
|
|
1102
|
-
"
|
|
1103
|
-
"
|
|
1104
|
-
"
|
|
1105
|
-
"
|
|
1106
|
-
"
|
|
1107
|
-
"
|
|
1108
|
-
"metaBtnOn": "_02HI2q_metaBtnOn",
|
|
1109
|
-
"row": "_02HI2q_row",
|
|
1110
|
-
"voucherAmount": "_02HI2q_voucherAmount",
|
|
1111
|
-
"topupChipActive": "_02HI2q_topupChipActive",
|
|
1128
|
+
"refresh": "_02HI2q_refresh",
|
|
1129
|
+
"factLabel": "_02HI2q_factLabel",
|
|
1130
|
+
"countDot": "_02HI2q_countDot",
|
|
1131
|
+
"buttonPrimary": "_02HI2q_buttonPrimary",
|
|
1132
|
+
"tip": "_02HI2q_tip",
|
|
1133
|
+
"card": "_02HI2q_card",
|
|
1112
1134
|
"button": "_02HI2q_button",
|
|
1113
|
-
"hint": "_02HI2q_hint",
|
|
1114
|
-
"autosave": "_02HI2q_autosave",
|
|
1115
|
-
"metaEditor": "_02HI2q_metaEditor",
|
|
1116
|
-
"metaIntro": "_02HI2q_metaIntro",
|
|
1117
|
-
"link": "_02HI2q_link",
|
|
1118
1135
|
"section": "_02HI2q_section",
|
|
1119
|
-
"
|
|
1120
|
-
"title": "_02HI2q_title",
|
|
1136
|
+
"voucherFact": "_02HI2q_voucherFact",
|
|
1121
1137
|
"topupChip": "_02HI2q_topupChip",
|
|
1122
|
-
"
|
|
1123
|
-
"
|
|
1124
|
-
"valueRow": "_02HI2q_valueRow",
|
|
1125
|
-
"toolbar": "_02HI2q_toolbar",
|
|
1138
|
+
"tipGroupName": "_02HI2q_tipGroupName",
|
|
1139
|
+
"topupChipActive": "_02HI2q_topupChipActive",
|
|
1126
1140
|
"actions": "_02HI2q_actions",
|
|
1127
|
-
"
|
|
1128
|
-
"code": "_02HI2q_code",
|
|
1141
|
+
"metaCheck": "_02HI2q_metaCheck",
|
|
1129
1142
|
"voucherRow": "_02HI2q_voucherRow",
|
|
1130
|
-
"
|
|
1131
|
-
"metaTitle": "_02HI2q_metaTitle",
|
|
1132
|
-
"metaField": "_02HI2q_metaField",
|
|
1133
|
-
"refresh": "_02HI2q_refresh",
|
|
1134
|
-
"countPicked": "_02HI2q_countPicked",
|
|
1135
|
-
"rowRatio": "_02HI2q_rowRatio",
|
|
1136
|
-
"metaInput": "_02HI2q_metaInput",
|
|
1137
|
-
"invite": "_02HI2q_invite",
|
|
1138
|
-
"topupInput": "_02HI2q_topupInput",
|
|
1139
|
-
"metaLabel": "_02HI2q_metaLabel",
|
|
1140
|
-
"voucherFact": "_02HI2q_voucherFact",
|
|
1141
|
-
"metaBtn": "_02HI2q_metaBtn",
|
|
1142
|
-
"cardHead": "_02HI2q_cardHead",
|
|
1143
|
-
"countDot": "_02HI2q_countDot"
|
|
1143
|
+
"metaActions": "_02HI2q_metaActions"
|
|
1144
1144
|
};
|
|
1145
1145
|
//#endregion
|
|
1146
1146
|
//#region src/client/NearTokenSection.tsx
|
package/lib/index.js
CHANGED
|
@@ -3,6 +3,8 @@ import { credentialRef, isCredentialRefName } from "@deepseek-ai/dsh-credentials
|
|
|
3
3
|
import { HarnessError, LlmAdapter, LlmError, ProviderRequestId, attributionHeaders, errorChain, isContextWindowExceededError, isQuotaExceededError } from "@deepseek-ai/dsh-llm";
|
|
4
4
|
import { readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
+
import { brotliCompress, constants } from "node:zlib";
|
|
7
|
+
import { promisify } from "node:util";
|
|
6
8
|
import { AttachmentId } from "@deepseek-ai/dsh-attachment";
|
|
7
9
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
8
10
|
import { brandString } from "@deepseek-ai/dsh-brand";
|
|
@@ -970,6 +972,77 @@ var NearTokenAccount = class {
|
|
|
970
972
|
return info;
|
|
971
973
|
}
|
|
972
974
|
};
|
|
975
|
+
/** Brotli quality to encode at: past this a smaller body costs real CPU. */
|
|
976
|
+
const QUALITY = 5;
|
|
977
|
+
/** The header pair that announces a brotli body. */
|
|
978
|
+
const BROTLI_HEADERS = { "content-encoding": "br" };
|
|
979
|
+
const compress = promisify(brotliCompress);
|
|
980
|
+
/** One body in the clear: the shape every bail-out below returns. */
|
|
981
|
+
const CLEAR = {
|
|
982
|
+
headers: {},
|
|
983
|
+
compressed: false
|
|
984
|
+
};
|
|
985
|
+
/**
|
|
986
|
+
* POST one JSON body, compressed when that pays for itself.
|
|
987
|
+
*
|
|
988
|
+
* The one place a call leaves this plugin, so the threshold, the encoding, and
|
|
989
|
+
* the fallback below cannot drift between the chat wires and the picture
|
|
990
|
+
* channel.
|
|
991
|
+
* @param send - the HTTP carrier, so a test or a caller can substitute one.
|
|
992
|
+
* @param url - absolute endpoint.
|
|
993
|
+
* @param request - headers, body, and cancellation.
|
|
994
|
+
* @returns the response, compressed or not; never a response the caller has to discard.
|
|
995
|
+
*/
|
|
996
|
+
async function postJson(send, url, request) {
|
|
997
|
+
const encoded = await encodeBody(request.json);
|
|
998
|
+
const post = (payload, encoding) => send(url, {
|
|
999
|
+
method: "POST",
|
|
1000
|
+
headers: {
|
|
1001
|
+
...request.headers,
|
|
1002
|
+
...encoding
|
|
1003
|
+
},
|
|
1004
|
+
body: payload,
|
|
1005
|
+
signal: request.signal
|
|
1006
|
+
});
|
|
1007
|
+
const response = await post(encoded.body, encoded.headers);
|
|
1008
|
+
if (response.status !== 400 || !encoded.compressed) return response;
|
|
1009
|
+
await response.body?.cancel().catch(() => {});
|
|
1010
|
+
return post(request.json, {});
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Compress one request body, when that pays for itself.
|
|
1014
|
+
*
|
|
1015
|
+
* Compression is an optimization on top of a request that must go out either
|
|
1016
|
+
* way, so every failure here — a runtime without brotli, a body that does not
|
|
1017
|
+
* shrink, an encoder error — sends the body in the clear rather than failing
|
|
1018
|
+
* the call.
|
|
1019
|
+
* @param json - the serialized request body.
|
|
1020
|
+
* @returns the bytes to send, the headers they need, and whether they are packed.
|
|
1021
|
+
*/
|
|
1022
|
+
async function encodeBody(json) {
|
|
1023
|
+
const bytes = Buffer.byteLength(json, "utf8");
|
|
1024
|
+
if (bytes < 16384) return {
|
|
1025
|
+
body: json,
|
|
1026
|
+
...CLEAR
|
|
1027
|
+
};
|
|
1028
|
+
try {
|
|
1029
|
+
const packed = await compress(Buffer.from(json, "utf8"), { params: { [constants.BROTLI_PARAM_QUALITY]: QUALITY } });
|
|
1030
|
+
if (packed.byteLength >= bytes) return {
|
|
1031
|
+
body: json,
|
|
1032
|
+
...CLEAR
|
|
1033
|
+
};
|
|
1034
|
+
return {
|
|
1035
|
+
body: new Uint8Array(packed),
|
|
1036
|
+
headers: BROTLI_HEADERS,
|
|
1037
|
+
compressed: true
|
|
1038
|
+
};
|
|
1039
|
+
} catch {
|
|
1040
|
+
return {
|
|
1041
|
+
body: json,
|
|
1042
|
+
...CLEAR
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
973
1046
|
//#endregion
|
|
974
1047
|
//#region src/host/images.ts
|
|
975
1048
|
/** Model names that generate pictures on this kind of site, in or out of the catalog's endpoint list. */
|
|
@@ -1228,15 +1301,14 @@ var NearTokenImageClient = class {
|
|
|
1228
1301
|
async attempt(request) {
|
|
1229
1302
|
const pictures = request.images ?? [];
|
|
1230
1303
|
const path = request.channel === "images" ? "/v1/images/generations" : request.channel === "genai" ? `/v1beta/models/${request.model}:generateContent` : "/v1/chat/completions";
|
|
1231
|
-
const response = await request.send
|
|
1232
|
-
method: "POST",
|
|
1304
|
+
const response = await postJson(request.send, `${this.deps.baseURL()}${path}`, {
|
|
1233
1305
|
headers: {
|
|
1234
1306
|
...attributionHeaders(),
|
|
1235
1307
|
"content-type": "application/json",
|
|
1236
1308
|
...request.channel === "genai" ? { "x-goog-api-key": request.key } : { authorization: `Bearer ${request.key}` },
|
|
1237
1309
|
...request.route?.groupRef === void 0 ? {} : { "x-neartoken-group": request.route.groupRef }
|
|
1238
1310
|
},
|
|
1239
|
-
|
|
1311
|
+
json: JSON.stringify(request.channel === "images" ? {
|
|
1240
1312
|
model: request.model,
|
|
1241
1313
|
prompt: request.prompt,
|
|
1242
1314
|
n: 1,
|
|
@@ -2397,40 +2469,95 @@ function serializeMessage$3(message, images = /* @__PURE__ */ new Map()) {
|
|
|
2397
2469
|
content: blocks
|
|
2398
2470
|
};
|
|
2399
2471
|
}
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2472
|
+
default: return;
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
/**
|
|
2476
|
+
* What a tool call whose result is missing reads as.
|
|
2477
|
+
*
|
|
2478
|
+
* Every call the harness sent is answered, but a replayed log, a fork seed, or a
|
|
2479
|
+
* compacted history can lack the result. The messages API refuses a request whose
|
|
2480
|
+
* `tool_use` has no answer, so the call is answered here instead of dropped.
|
|
2481
|
+
*/
|
|
2482
|
+
const MISSING_TOOL_RESULT = "No result was recorded for this tool call.";
|
|
2483
|
+
/**
|
|
2484
|
+
* The block one tool message contributes to the turn that answers its call.
|
|
2485
|
+
*
|
|
2486
|
+
* A tool result is itself a block list, so the pictures a tool handed back
|
|
2487
|
+
* (read_image does exactly that) travel inside the result rather than being
|
|
2488
|
+
* flattened into its text.
|
|
2489
|
+
* @param message - the harness tool message.
|
|
2490
|
+
* @param images - the request's inlined images.
|
|
2491
|
+
* @returns the `tool_result` block.
|
|
2492
|
+
*/
|
|
2493
|
+
function toolResultBlock(message, images = /* @__PURE__ */ new Map()) {
|
|
2494
|
+
const content = message.content;
|
|
2495
|
+
const text = textOf$3(content);
|
|
2496
|
+
const pictures = imagesIn(content, images);
|
|
2497
|
+
if (pictures.length === 0) return {
|
|
2498
|
+
type: "tool_result",
|
|
2499
|
+
tool_use_id: message.toolCallId,
|
|
2500
|
+
content: text
|
|
2501
|
+
};
|
|
2502
|
+
const blocks = text === "" ? [] : [{
|
|
2503
|
+
type: "text",
|
|
2504
|
+
text
|
|
2505
|
+
}];
|
|
2506
|
+
for (const image of pictures) blocks.push({
|
|
2507
|
+
type: "image",
|
|
2508
|
+
source: {
|
|
2509
|
+
type: "base64",
|
|
2510
|
+
media_type: image.mediaType,
|
|
2511
|
+
data: image.data
|
|
2512
|
+
}
|
|
2513
|
+
});
|
|
2514
|
+
return {
|
|
2515
|
+
type: "tool_result",
|
|
2516
|
+
tool_use_id: message.toolCallId,
|
|
2517
|
+
content: blocks
|
|
2518
|
+
};
|
|
2519
|
+
}
|
|
2520
|
+
/**
|
|
2521
|
+
* The call ids one assistant turn asks the next turn to answer.
|
|
2522
|
+
* @param turn - the serialized assistant turn.
|
|
2523
|
+
* @returns the ids, in block order.
|
|
2524
|
+
*/
|
|
2525
|
+
function toolUseIds(turn) {
|
|
2526
|
+
const ids = [];
|
|
2527
|
+
for (const block of turn.content) if (block.type === "tool_use") ids.push(String(block.id));
|
|
2528
|
+
return ids;
|
|
2529
|
+
}
|
|
2530
|
+
/**
|
|
2531
|
+
* Fold adjacent same-role turns into one.
|
|
2532
|
+
*
|
|
2533
|
+
* The messages API reads the conversation as one turn per role, and a gateway in
|
|
2534
|
+
* front of it may not merge a repeated role itself. Inside a user turn the
|
|
2535
|
+
* `tool_result` blocks come first: a result that follows text is refused.
|
|
2536
|
+
* @param turns - the settled turns, in send order.
|
|
2537
|
+
* @returns the merged messages, ready to send.
|
|
2538
|
+
*/
|
|
2539
|
+
function mergeAdjacent(turns) {
|
|
2540
|
+
const merged = [];
|
|
2541
|
+
for (const turn of turns) {
|
|
2542
|
+
const last = merged.at(-1);
|
|
2543
|
+
if (last === void 0 || last.role !== turn.role) {
|
|
2544
|
+
merged.push({
|
|
2545
|
+
role: turn.role,
|
|
2546
|
+
content: [...turn.content]
|
|
2422
2547
|
});
|
|
2423
|
-
|
|
2424
|
-
role: "user",
|
|
2425
|
-
content: [{
|
|
2426
|
-
type: "tool_result",
|
|
2427
|
-
tool_use_id: message.toolCallId,
|
|
2428
|
-
content: blocks
|
|
2429
|
-
}]
|
|
2430
|
-
};
|
|
2548
|
+
continue;
|
|
2431
2549
|
}
|
|
2432
|
-
|
|
2550
|
+
if (last.role === "assistant") {
|
|
2551
|
+
last.content.push(...turn.content);
|
|
2552
|
+
continue;
|
|
2553
|
+
}
|
|
2554
|
+
last.content.unshift(...turn.content.filter((block) => block.type === "tool_result"));
|
|
2555
|
+
last.content.push(...turn.content.filter((block) => block.type !== "tool_result"));
|
|
2433
2556
|
}
|
|
2557
|
+
return merged.map((turn) => ({
|
|
2558
|
+
role: turn.role,
|
|
2559
|
+
content: turn.content
|
|
2560
|
+
}));
|
|
2434
2561
|
}
|
|
2435
2562
|
/**
|
|
2436
2563
|
* The breakpoint that marks a cacheable prefix end.
|
|
@@ -2472,6 +2599,13 @@ function claudeEffort(effort) {
|
|
|
2472
2599
|
/**
|
|
2473
2600
|
* Serialize one harness request into an Anthropic messages body.
|
|
2474
2601
|
*
|
|
2602
|
+
* The turn list is settled into the pairing the messages API checks: every
|
|
2603
|
+
* `tool_use` of an assistant turn is answered by a `tool_result` in the single
|
|
2604
|
+
* user turn that follows it, calls the harness never recorded a result for are
|
|
2605
|
+
* answered with a placeholder, results are collected into one turn rather than
|
|
2606
|
+
* one turn each (a parallel batch of calls and a split batch of results are
|
|
2607
|
+
* refused as unpaired), and adjacent same-role turns are folded into one.
|
|
2608
|
+
*
|
|
2475
2609
|
* Reasoning is requested as `thinking.type = "adaptive"` with the effort in
|
|
2476
2610
|
* `output_config`, which every Claude generation behind this gateway accepts.
|
|
2477
2611
|
* The older `thinking.type = "enabled"` budget form is not portable: models of
|
|
@@ -2485,9 +2619,35 @@ function claudeEffort(effort) {
|
|
|
2485
2619
|
* @returns the ready-to-JSON body object.
|
|
2486
2620
|
*/
|
|
2487
2621
|
function serializeRequest$3(options, model, images = /* @__PURE__ */ new Map()) {
|
|
2488
|
-
const
|
|
2622
|
+
const turns = [];
|
|
2623
|
+
const results = [];
|
|
2624
|
+
let open = [];
|
|
2625
|
+
let deferred = [];
|
|
2626
|
+
const settle = () => {
|
|
2627
|
+
if (results.length === 0 && open.length === 0) return;
|
|
2628
|
+
for (const id of open) results.push({
|
|
2629
|
+
type: "tool_result",
|
|
2630
|
+
tool_use_id: id,
|
|
2631
|
+
content: MISSING_TOOL_RESULT
|
|
2632
|
+
});
|
|
2633
|
+
for (const turn of deferred) results.push(...turn.content);
|
|
2634
|
+
turns.push({
|
|
2635
|
+
role: "user",
|
|
2636
|
+
content: results.splice(0, results.length)
|
|
2637
|
+
});
|
|
2638
|
+
open = [];
|
|
2639
|
+
deferred = [];
|
|
2640
|
+
};
|
|
2489
2641
|
let system = options.system;
|
|
2490
2642
|
for (const message of options.messages) {
|
|
2643
|
+
if (message.role === "tool") {
|
|
2644
|
+
const callId = String(message.toolCallId);
|
|
2645
|
+
if (!open.includes(callId)) continue;
|
|
2646
|
+
results.push(toolResultBlock(message, images));
|
|
2647
|
+
open = open.filter((pending) => pending !== callId);
|
|
2648
|
+
if (open.length === 0) settle();
|
|
2649
|
+
continue;
|
|
2650
|
+
}
|
|
2491
2651
|
if (message.role === "system" && system === void 0) {
|
|
2492
2652
|
const text = textOf$3(message.content);
|
|
2493
2653
|
if (text !== "") {
|
|
@@ -2495,9 +2655,22 @@ function serializeRequest$3(options, model, images = /* @__PURE__ */ new Map())
|
|
|
2495
2655
|
continue;
|
|
2496
2656
|
}
|
|
2497
2657
|
}
|
|
2498
|
-
const
|
|
2499
|
-
if (
|
|
2658
|
+
const turn = serializeMessage$3(message, images);
|
|
2659
|
+
if (turn === void 0) continue;
|
|
2660
|
+
if (turn.role === "assistant") {
|
|
2661
|
+
settle();
|
|
2662
|
+
turns.push(turn);
|
|
2663
|
+
open = toolUseIds(turn);
|
|
2664
|
+
continue;
|
|
2665
|
+
}
|
|
2666
|
+
if (open.length > 0) {
|
|
2667
|
+
deferred.push(turn);
|
|
2668
|
+
continue;
|
|
2669
|
+
}
|
|
2670
|
+
turns.push(turn);
|
|
2500
2671
|
}
|
|
2672
|
+
settle();
|
|
2673
|
+
const messages = mergeAdjacent(turns);
|
|
2501
2674
|
const body = {
|
|
2502
2675
|
model,
|
|
2503
2676
|
messages,
|
|
@@ -4290,13 +4463,12 @@ var NearTokenAdapter = class extends LlmAdapter {
|
|
|
4290
4463
|
const images = await inlineImages(this.deps.attachments(), options.messages, options.signal, this.deps.imagePolicy());
|
|
4291
4464
|
let response;
|
|
4292
4465
|
try {
|
|
4293
|
-
response = await fetch
|
|
4294
|
-
method: "POST",
|
|
4466
|
+
response = await postJson(fetch, wire.url(this.deps.baseURL(), model), {
|
|
4295
4467
|
headers: {
|
|
4296
4468
|
...attributionHeaders(),
|
|
4297
4469
|
...wire.headers(key, groupRef)
|
|
4298
4470
|
},
|
|
4299
|
-
|
|
4471
|
+
json: JSON.stringify(wire.serialize(options, model, images)),
|
|
4300
4472
|
signal: options.signal
|
|
4301
4473
|
});
|
|
4302
4474
|
} catch (error) {
|
|
@@ -3,6 +3,13 @@ import type { WireImages } from './wire-images.ts';
|
|
|
3
3
|
/**
|
|
4
4
|
* Serialize one harness request into an Anthropic messages body.
|
|
5
5
|
*
|
|
6
|
+
* The turn list is settled into the pairing the messages API checks: every
|
|
7
|
+
* `tool_use` of an assistant turn is answered by a `tool_result` in the single
|
|
8
|
+
* user turn that follows it, calls the harness never recorded a result for are
|
|
9
|
+
* answered with a placeholder, results are collected into one turn rather than
|
|
10
|
+
* one turn each (a parallel batch of calls and a split batch of results are
|
|
11
|
+
* refused as unpaired), and adjacent same-role turns are folded into one.
|
|
12
|
+
*
|
|
6
13
|
* Reasoning is requested as `thinking.type = "adaptive"` with the effort in
|
|
7
14
|
* `output_config`, which every Claude generation behind this gateway accepts.
|
|
8
15
|
* The older `thinking.type = "enabled"` budget form is not portable: models of
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smallest body, in bytes, that is worth a compression round trip.
|
|
3
|
+
*
|
|
4
|
+
* Below this the saved upload is a few milliseconds even on a slow link, which
|
|
5
|
+
* the fixed cost of encoding and decoding eats.
|
|
6
|
+
*/
|
|
7
|
+
export declare const MIN_COMPRESS_BYTES: number;
|
|
8
|
+
/** One request body ready to send, with the headers its encoding requires. */
|
|
9
|
+
export interface EncodedBody {
|
|
10
|
+
/** The bytes to send: the JSON text itself, or its brotli form. */
|
|
11
|
+
readonly body: Uint8Array<ArrayBuffer> | string;
|
|
12
|
+
/** Encoding headers; empty when the body travels in the clear. */
|
|
13
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
14
|
+
/** Whether {@link EncodedBody.body} is compressed, so a caller can retry in the clear. */
|
|
15
|
+
readonly compressed: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** One JSON POST the encoding layer shapes: where it goes, what it sends, how it authenticates. */
|
|
18
|
+
export interface JsonPost {
|
|
19
|
+
/** Headers the call needs regardless of encoding. */
|
|
20
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
21
|
+
/** The serialized JSON body. */
|
|
22
|
+
readonly json: string;
|
|
23
|
+
/** Cancellation passthrough. */
|
|
24
|
+
readonly signal?: AbortSignal | undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* POST one JSON body, compressed when that pays for itself.
|
|
28
|
+
*
|
|
29
|
+
* The one place a call leaves this plugin, so the threshold, the encoding, and
|
|
30
|
+
* the fallback below cannot drift between the chat wires and the picture
|
|
31
|
+
* channel.
|
|
32
|
+
* @param send - the HTTP carrier, so a test or a caller can substitute one.
|
|
33
|
+
* @param url - absolute endpoint.
|
|
34
|
+
* @param request - headers, body, and cancellation.
|
|
35
|
+
* @returns the response, compressed or not; never a response the caller has to discard.
|
|
36
|
+
*/
|
|
37
|
+
export declare function postJson(send: typeof fetch, url: string, request: JsonPost): Promise<Response>;
|
|
38
|
+
/**
|
|
39
|
+
* Compress one request body, when that pays for itself.
|
|
40
|
+
*
|
|
41
|
+
* Compression is an optimization on top of a request that must go out either
|
|
42
|
+
* way, so every failure here — a runtime without brotli, a body that does not
|
|
43
|
+
* shrink, an encoder error — sends the body in the clear rather than failing
|
|
44
|
+
* the call.
|
|
45
|
+
* @param json - the serialized request body.
|
|
46
|
+
* @returns the bytes to send, the headers they need, and whether they are packed.
|
|
47
|
+
*/
|
|
48
|
+
export declare function encodeBody(json: string): Promise<EncodedBody>;
|