@huai_xin/dsh-workbuddy 0.1.7 → 0.1.8
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 +9 -0
- package/docs/workbuddy-protocol.md +34 -0
- package/package.json +1 -1
- package/src/adapter.js +39 -2
package/README.md
CHANGED
|
@@ -306,6 +306,15 @@ node test/live/policy-probe.mjs # 两版白名单与准入结果对照
|
|
|
306
306
|
- **思考档位只认服务端声明的 `reasoning.supportedEfforts`**(两个源合并后国内 15/34、国际 21/28);只声明了默认档的模型不给档位列表 —— 与网页端行为一致。合并后仍无档位的,请求里不带 `reasoning_effort`,由服务端取默认。
|
|
307
307
|
- 实时**繁忙程度**只有大陆版有(`POST /v2/chat/busy`,海外版 404,`regions.js` 的 `hasBusy` 决定那一列是否显示)。它**不是可用性信号**:只对白名单内的活跃模型返回 `idle/normal/busy/congested`,其余一律 `unknown`(面板显示「未知」,不当成故障)。插件侧 50 秒缓存,与官方前端一致。
|
|
308
308
|
- 两版**条目数会变**:2026-10-22 实测大陆版 30 个(`ok` 22 / `dead` 8)、海外版 25 个(`ok` 13 / `denied` 12)。`gpt-5.3-codex` 只在账号名单里,两个 `/v3/config` 都没有它,倍率因此是「未知」。
|
|
309
|
+
- **长上下文下模型会"说话说一半就收尾"(上游行为,非插件问题)**:海外版 `deepseek-v4.1-flash` 在上下文很长时会不调用工具、只用一句话叙述下一步然后 `finish_reason: "stop"`(例如「我需要先读取该文件,才能准确修改。」)。实测(2026-10-22,`node test/live/long-context-probe.mjs`):
|
|
310
|
+
|
|
311
|
+
| 输入规模 | 正确调用工具 | 提前 `stop` |
|
|
312
|
+
| --- | --- | --- |
|
|
313
|
+
| 0k(1 条消息) | 2/2 | 0/2 |
|
|
314
|
+
| 30k 字符(613 条) | 2/2 | 0/2 |
|
|
315
|
+
| 89k 字符(1825 条) | 1/2 | **1/2** |
|
|
316
|
+
|
|
317
|
+
用户侧复现阈值约 **120k 上下文**起明显(其它模型未测)。判据是**换了上下文长度、其它一切不变**即可复现/消失,且流里**一个 tool-call 分片都没有**、`finish_reason` 就是 `stop` —— 所以既不是适配器丢帧(丢帧会看到 `finish_reason: "tool_calls"`),也不是采样(四档温度 16/16 正常)、不是工具 schema(70 个工具 / 单个 / 简化 schema 9/9 正常)。`stop` 是模型自己采到 EOS,属于上游质量问题;缓解手段只有换模型、把会话压短(harness 的压缩),或回一句"继续"。
|
|
309
318
|
|
|
310
319
|
## 上游行为与错误映射
|
|
311
320
|
|
|
@@ -394,6 +394,7 @@ data: [DONE]
|
|
|
394
394
|
| INTL scoped → `500`(openresty/APISIX,HTML 错误页) | 2026-10-21 起持续;`/v3/config` 正常 ✅ |
|
|
395
395
|
| `glm-5.1` 图片能力两个端点矛盾 | scoped 报 `false`、`/v3/config` 报 `true`,实测能看图(📖 参考实现据此写了覆盖白名单) |
|
|
396
396
|
| 目录包含渠道不服务的模型 | 见 `11102`(§6)✅ |
|
|
397
|
+
| 长上下文下"说话说一半就收尾"(`finish_reason: stop`,无工具调用) | 上游/模型行为,**不是插件问题**。实测:同一提示、同一套工具、只改前面的对话长度 —— 0k 2/2 正常、30k 1/2(一次 181 字后 `stop`)、89k 1/2;每次 finish 诊断块都标 `workbuddy-intl/deepseek-v4.1-flash`。判据:流里**没有任何 tool-call 分片**、`finish_reason` 就是 `stop`(若丢帧会是 `tool_calls` 且能看到分片)。已排除:采样(四档温度 16/16 正常)、工具 schema/数量(70 个 / 单个 / 简化 schema 9/9 正常)、提示词校验(海外版无此错误,请求体按原样转发)。复现:`node test/live/long-context-probe.mjs 2`;排查:`node test/live/toolcall-audit.mjs <会话前缀>`、`sampling-probe.mjs`、`tools-probe.mjs`。缓解只有换模型/压缩上下文/回一句"继续" —— 合法的 `stop` 不能当错误重试。 |
|
|
397
398
|
|
|
398
399
|
### 4.7 模型选择器里每一项元数据的来源
|
|
399
400
|
|
|
@@ -838,6 +839,39 @@ provider 与全部 HTTP 路由都没注册,设置面板只剩一个 `HTTP 404`
|
|
|
838
839
|
|
|
839
840
|
---
|
|
840
841
|
|
|
842
|
+
## 12. 与官方客户端、DSH 的请求处理对照(2026-10-22)
|
|
843
|
+
|
|
844
|
+
官方客户端不是网页版:`www.workbuddy.cn` / `www.workbuddy.ai` 的 bundle 里**没有**聊天逻辑
|
|
845
|
+
(只有站点壳,实测抓了入口 + 28 个懒加载 chunk,`chat/completions`、`finish_reason` 全部 0 命中)。
|
|
846
|
+
真正的客户端是 CLI:`@tencent-ai/codebuddy-code`(2.172.1,59 MB,`dist/codebuddy.js` 28 MB)。
|
|
847
|
+
下面这张表来自读它的 bundle(`mapFinishReasonToStopReason`、请求体组装处)。
|
|
848
|
+
|
|
849
|
+
| 方面 | 官方 CLI | 本插件 | DSH |
|
|
850
|
+
| --- | --- | --- | --- |
|
|
851
|
+
| 端点 | 自定义/默认 URL + `/chat/completions`(CN 默认即 `copilot.tencent.com` ✓ 我们早就在用) | CN `copilot.tencent.com/v2/chat/completions`,失败回退 `workbuddy.cn`;INTL `workbuddy.ai/v2/chat/completions` | 由适配器注册的 provider 决定 |
|
|
852
|
+
| `temperature` / `top_p` / `frequency_penalty` / `presence_penalty` | **总是发**(模型配置值,缺省用 `DEFAULT_TEMPERATURE`) | **不发**(仅当调用方传 `options.temperature`) | 类型里有 `LlmCallControls.temperature`,但没有设置入口 ⇒ 实际永远为空 |
|
|
853
|
+
| `max_tokens` | 总是发(模型配置) | 发(调用方 `maxTokens`,否则目录 `maxOutputTokens`) | 提供 `maxTokens` |
|
|
854
|
+
| `tool_choice` / `parallel_tool_calls` | 发 | 不发(走服务端默认 `auto`) | — |
|
|
855
|
+
| `stream_options: {include_usage:true}` | **发** | **2026-10-22 起补上**(此前不发,网关可能不回 `usage`) | — |
|
|
856
|
+
| `prompt_cache_key` | **不发** | 发(会话级,前缀缓存命中率才不为 0) | — |
|
|
857
|
+
| `finish_reason` 映射 | `content_filter`/`refusal`→`refusal`;`length`/`max_tokens`→`max_tokens`;`stop`/`end_turn`/`stop_sequence`/`tool_calls`/`tool_use`/`function_call`→`end_turn`;未知→不归类 | **2026-10-22 起对齐**:`refusal`/`content_filter`→不可重试的 `INVALID_REQUEST` 错误;`error`→`TRANSPORT`(可重试);`end_turn`/`stop_sequence`→`stop`;`tool_use`/`function_call`→`tool-calls`;未知值记日志后按 `stop` | 只有 `stop`/`tool-calls`/`max-tokens`/`aborted`/`error` 五类,**没有 refusal** |
|
|
858
|
+
| `finish_reason=error` | 抛 `provider_finish_reason_error`(`model_service`) | 现在报 `TRANSPORT`,交给重试 | — |
|
|
859
|
+
| 空流 / 占位帧 | `is_empty_stream`、placeholder-only 检测 | `EMPTY_RESPONSE` | — |
|
|
860
|
+
| 流空闲监控 | `setInterval` + `streamIdleTimeoutMs`(日志 `Stream idle monitor armed`) | **没有**(依赖 harness 自身超时) | 无 |
|
|
861
|
+
| 请求体改写 | 模型不支持工具时 `stripToolDefinitions`(连 `tool_choice` 一起去掉)、图片扁平化、自定义模型 URL 规整 | CN:系统提示词指纹改写(`Illegal API invocation`)+ 换会话标识重试一次 | — |
|
|
862
|
+
|
|
863
|
+
三条结论:
|
|
864
|
+
|
|
865
|
+
1. **我们和官方在"结束原因"上原本差一张表** —— 官方把 `refusal`/`content_filter` 单独归类、
|
|
866
|
+
把 `error` 当服务错误;我们此前只有 `stop`/`length`/`tool_calls`,其余全落进 `stop`。
|
|
867
|
+
后果就是:**上游拒绝或过滤时,表现和"模型自己说完了"完全一样** —— 已按上表对齐。
|
|
868
|
+
2. **`stream_options.include_usage` 官方一直带**,我们漏了;补上后 `usage` 更可靠(诊断里的
|
|
869
|
+
`outputTokens` 也才有据可依)。
|
|
870
|
+
3 . **`prompt_cache_key` 是反向差异**:官方不发,我们发。它只影响前缀缓存命中,不影响语义,
|
|
871
|
+
留着(会话级稳定值,命中率实测有效)。
|
|
872
|
+
|
|
873
|
+
---
|
|
874
|
+
|
|
841
875
|
## 附:本仓库可复用的抓取脚本
|
|
842
876
|
|
|
843
877
|
| 脚本 | 用途 |
|
package/package.json
CHANGED
package/src/adapter.js
CHANGED
|
@@ -643,6 +643,10 @@ export class WorkBuddyAdapter {
|
|
|
643
643
|
model,
|
|
644
644
|
messages,
|
|
645
645
|
stream: true,
|
|
646
|
+
// The official client asks for the usage frame explicitly; without it the
|
|
647
|
+
// gateway may omit `usage` on some routes, which costs the panel its token
|
|
648
|
+
// counts and the diagnostics their output-token line.
|
|
649
|
+
stream_options: { include_usage: true },
|
|
646
650
|
// Server-side prefix cache: without this field the hit rate is always 0.
|
|
647
651
|
prompt_cache_key: this.sessionId
|
|
648
652
|
}
|
|
@@ -1005,9 +1009,38 @@ export class WorkBuddyAdapter {
|
|
|
1005
1009
|
yield finishChunk({ kind: 'error', failure: { message: '模型返回了空响应', code: 'EMPTY_RESPONSE' } })
|
|
1006
1010
|
return
|
|
1007
1011
|
}
|
|
1008
|
-
|
|
1012
|
+
// `finish_reason` has more values than the three the OpenAI docs mention, and
|
|
1013
|
+
// the official CodeBuddy CLI maps the whole set (`mapFinishReasonToStopReason`
|
|
1014
|
+
// in its bundle): `refusal` / `content_filter` are their own outcome, not a
|
|
1015
|
+
// stop, and `error` means the provider gave up mid-answer. Mapping those to
|
|
1016
|
+
// `stop` is how a refusal and a real finish became indistinguishable here.
|
|
1017
|
+
const refusalReason = finishReason === 'refusal' || finishReason === 'content_filter'
|
|
1018
|
+
if (refusalReason) {
|
|
1019
|
+
this.ctx.logger.warn(`[workbuddy] ${this.region.id}: ${options.model} 的上游拒绝/过滤了本次回答(finish_reason=${finishReason}),不重试`)
|
|
1020
|
+
yield finishChunk({
|
|
1021
|
+
kind: 'error',
|
|
1022
|
+
failure: {
|
|
1023
|
+
message: `${this.region.provider}: 上游拒绝或按内容策略过滤了本次回答(finish_reason=${finishReason}),请换个说法或换个模型`,
|
|
1024
|
+
code: 'INVALID_REQUEST'
|
|
1025
|
+
}
|
|
1026
|
+
})
|
|
1027
|
+
return
|
|
1028
|
+
}
|
|
1029
|
+
if (finishReason === 'error') {
|
|
1030
|
+
this.ctx.logger.warn(`[workbuddy] ${this.region.id}: ${options.model} 的上游以 finish_reason=error 结束,按传输错误上报,可自动重试`)
|
|
1031
|
+
yield finishChunk({
|
|
1032
|
+
kind: 'error',
|
|
1033
|
+
failure: {
|
|
1034
|
+
message: `${this.region.provider}: 上游在回答中途返回了错误(finish_reason=error;可重试的传输错误)`,
|
|
1035
|
+
code: 'TRANSPORT'
|
|
1036
|
+
}
|
|
1037
|
+
})
|
|
1038
|
+
return
|
|
1039
|
+
}
|
|
1040
|
+
const toolReason = finishReason === 'tool_calls' || finishReason === 'tool_use' || finishReason === 'function_call'
|
|
1041
|
+
const reason = toolReason || toolOrder.length > 0
|
|
1009
1042
|
? { kind: 'tool-calls' }
|
|
1010
|
-
: finishReason === 'length'
|
|
1043
|
+
: finishReason === 'length' || finishReason === 'max_tokens'
|
|
1011
1044
|
? { kind: 'max-tokens' }
|
|
1012
1045
|
: { kind: 'stop' }
|
|
1013
1046
|
// Two endings are complete-looking but are not the model choosing to stop, and
|
|
@@ -1018,6 +1051,10 @@ export class WorkBuddyAdapter {
|
|
|
1018
1051
|
this.ctx.logger.warn(`[workbuddy] ${this.region.id}: ${options.model} 的输出达到上限,本步被截断(输出 ${outputTokens ?? '未知'} tokens)`)
|
|
1019
1052
|
} else if (reason.kind === 'stop' && finishReason === undefined) {
|
|
1020
1053
|
this.ctx.logger.warn(`[workbuddy] ${this.region.id}: ${options.model} 的响应以 [DONE] 收尾但没有 finish_reason,按 stop 处理(输出 ${outputTokens ?? '未知'} tokens)`)
|
|
1054
|
+
} else if (reason.kind === 'stop' && !['stop', 'end_turn', 'stop_sequence'].includes(finishReason)) {
|
|
1055
|
+
// An unknown value is worth seeing: the official client leaves the stop
|
|
1056
|
+
// reason at its default for these too, so nobody upstream has classified them.
|
|
1057
|
+
this.ctx.logger.warn(`[workbuddy] ${this.region.id}: ${options.model} 返回了未识别的 finish_reason=${finishReason},按 stop 处理`)
|
|
1021
1058
|
}
|
|
1022
1059
|
yield finishChunk(reason)
|
|
1023
1060
|
}
|