@huai_xin/dsh-workbuddy 0.1.8 → 0.1.10
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/docs/workbuddy-protocol.md +35 -2
- package/package.json +1 -1
- package/src/adapter.js +37 -16
|
@@ -175,8 +175,8 @@ POST <chatEndpoint>/v2/chat/completions
|
|
|
175
175
|
| `max_tokens` | 否 | 实测 `128000` 可用 ✅;未知/别名模型按目录 `maxOutputTokens` |
|
|
176
176
|
| `tools` | 否 | OpenAI function 形态,70 个工具 / 60 KB 实测通过 ✅ |
|
|
177
177
|
| `prompt_cache_key` | 否 | 服务端前缀缓存键;缺失时缓存命中恒为 0 📖 |
|
|
178
|
-
| `thinking` | 否 |
|
|
179
|
-
| `reasoning_effort` | 否 | `low` / `medium` / `high` / `xhigh` / `max`,取值须在该模型支持列表内 ✅
|
|
178
|
+
| `thinking` | 否 | **本插件不再发送**(2026-10-23 实测:带不带它都不影响 ds 是否思考,见 §12.1) |
|
|
179
|
+
| `reasoning_effort` | 否 | `low` / `medium` / `high` / `xhigh` / `max`,取值须在该模型支持列表内 ✅ 网关会校验(见下);**这是唯一的思考控制**,按目录声明统一取值(调用方选择 → `defaultEffort` → `high` → 首档) |
|
|
180
180
|
| `temperature` / `stop` | 否 | 直通 ✅ |
|
|
181
181
|
|
|
182
182
|
**`reasoning_effort` 是真实生效的参数**(2026-10-22 实测,海外版 `deepseek-v4.1-flash`,免费模型,
|
|
@@ -860,6 +860,39 @@ provider 与全部 HTTP 路由都没注册,设置面板只剩一个 `HTTP 404`
|
|
|
860
860
|
| 流空闲监控 | `setInterval` + `streamIdleTimeoutMs`(日志 `Stream idle monitor armed`) | **没有**(依赖 harness 自身超时) | 无 |
|
|
861
861
|
| 请求体改写 | 模型不支持工具时 `stripToolDefinitions`(连 `tool_choice` 一起去掉)、图片扁平化、自定义模型 URL 规整 | CN:系统提示词指纹改写(`Illegal API invocation`)+ 换会话标识重试一次 | — |
|
|
862
862
|
|
|
863
|
+
#### 12.1 请求体逐字段对照(2026-10-23 补)
|
|
864
|
+
|
|
865
|
+
官方 `providerData` 里还有 `thinking:{type:'disabled'}`(关闭思考时才发;开启时不发该字段)、
|
|
866
|
+
`reasoning_effort`(档位)。逐字段差异与影响判断:
|
|
867
|
+
|
|
868
|
+
| 字段 | 本插件 | 官方 CLI | 影响 |
|
|
869
|
+
| --- | --- | --- | --- |
|
|
870
|
+
| `model` / `messages` / `stream` / `tools` / `max_tokens` | 发 | 发 | 一致 ✓ |
|
|
871
|
+
| `stream_options.include_usage` | 发(0.1.8 起) | 发 | 已对齐 ✓ |
|
|
872
|
+
| `tool_choice` / `parallel_tool_calls` | 不发 | 发(配置值) | 服务端默认 `auto` / `true`,等价 ✓ |
|
|
873
|
+
| `temperature` / `top_p` / `frequency_penalty` / `presence_penalty` | **不发**(仅调用方给 temperature 时发) | **总是发**(模型配置值,缺省 `DEFAULT_TEMPERATURE`) | **真实差异**,但实测对"提前收尾"无影响(默认/0.2/0.5/0.7/0.3+top_p0.9 各 8 次:2/8、2/8、3/8、3/8,同一水平)→ 暂不跟随,避免擅自改变输出风格 |
|
|
874
|
+
| `reasoning_effort` | 发(目录档位,**所有模型同一套规则**:调用方选择 → `defaultEffort` → `high` → 首档;无档位不发) | 发(同左) | 一致 ✓ |
|
|
875
|
+
| `thinking` | **不发**(2026-10-23 起) | **开启时不发**;关闭时发 `{type:'disabled'}` | 已对齐 ✓。实测(两个部署各 2–3 次):带不带 `{type:'enabled'}`,`high` 档都是 0 思考字符;`max` 档不带时思考更多(INTL 384 vs 306、CN 476 vs 418)→ 该字段对"思不思考"没有作用 |
|
|
876
|
+
| `prompt_cache_key` | 发(会话级) | **不发** | 反向差异;实测对本问题无影响(去掉后 1/8 vs 2/8),保留以提高前缀缓存命中 ✓ |
|
|
877
|
+
| `stop` | 仅调用方给 | 不发 | 等价 ✓ |
|
|
878
|
+
| `store` / `prompt_cache_retention` | 不发 | 发(配置值,通常未设置) | 等价 ✓ |
|
|
879
|
+
| 请求头 | `Authorization` / `Accept` / `X-Domain` / `X-Product` / `X-Product-Code` / `User-Agent` | 同类内部头(`X-User-Id`/`X-Enterprise-Id`/`X-Tenant-Id`/`X-Department-Info`/`X-Domain`/`User-Agent`),且会剔除内部头再转发 | 我们的头是目录/聊天都验证过可用的最小集合 ✓ |
|
|
880
|
+
|
|
881
|
+
**结论:结构上不是"完全一样",但没有一处会造成"提前收尾"** —— 唯一的实质差异是官方总带采样参数,
|
|
882
|
+
而实测它对这个问题无效。`refusal` 的含义见 §12.2。
|
|
883
|
+
|
|
884
|
+
#### 12.2 `finish_reason: "refusal"` 是什么
|
|
885
|
+
|
|
886
|
+
官方把 `refusal` 与 `content_filter` 归为同一类 `stopReason:"refusal"`,它的来源(读官方 bundle):
|
|
887
|
+
|
|
888
|
+
1. **输入侧内容安全拦截**:`prepareSensitiveUserPrompt` 判定 `deny` 时直接返回
|
|
889
|
+
`stopReason:"refusal"`(日志 `Sensitive user prompt blocked before history`)——**请求根本没发出去**;
|
|
890
|
+
2. **模型/厂商主动拒绝**:`isContentPolicyRefusal(...)` 判定的内容策略拒绝;
|
|
891
|
+
3. 本地命令执行失败等"请求没能完成"的收尾,也复用 `refusal`。
|
|
892
|
+
|
|
893
|
+
所以 `refusal` ≠ 网络问题、≠ 截断,而是**"这次不回答"**。正确反应是**不重试**(重试会得到同样的拒绝),
|
|
894
|
+
把原因告诉用户 —— 本插件现在正是这样(`INVALID_REQUEST` + 明确文案,见 §4.5)。
|
|
895
|
+
|
|
863
896
|
三条结论:
|
|
864
897
|
|
|
865
898
|
1. **我们和官方在"结束原因"上原本差一张表** —— 官方把 `refusal`/`content_filter` 单独归类、
|
package/package.json
CHANGED
package/src/adapter.js
CHANGED
|
@@ -660,23 +660,30 @@ export class WorkBuddyAdapter {
|
|
|
660
660
|
const maxTokens = positiveInt(options.maxTokens) ?? entry?.maxTokens
|
|
661
661
|
if (maxTokens !== undefined) body.max_tokens = maxTokens
|
|
662
662
|
|
|
663
|
-
// Thinking is pinned on
|
|
664
|
-
//
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
// `
|
|
672
|
-
//
|
|
673
|
-
//
|
|
663
|
+
// Thinking is pinned on for every model, with no per-family branch: the effort
|
|
664
|
+
// level is the only reasoning control, and it is picked from what the catalog
|
|
665
|
+
// declares rather than from the model's name —
|
|
666
|
+
// 1. the level the caller chose, when the model declares it;
|
|
667
|
+
// 2. otherwise the model's own `defaultEffort` from the catalog;
|
|
668
|
+
// 3. otherwise `high` when declared, else the first declared level;
|
|
669
|
+
// 4. no levels declared → send nothing and let the deployment decide.
|
|
670
|
+
//
|
|
671
|
+
// The `thinking` field is deliberately **not** sent for anyone. Measured
|
|
672
|
+
// 2026-10-23 on both deployments (`test/live/thinking-field-probe.mjs`): with
|
|
673
|
+
// `reasoning_effort` present, dropping `thinking:{type:'enabled'}` changes
|
|
674
|
+
// nothing at `high` (0 reasoning chars either way) and yields slightly more
|
|
675
|
+
// reasoning at `max` (384 vs 306 on INTL, 476 vs 418 on CN). The official client
|
|
676
|
+
// only sends that field when it turns thinking *off*, so the enabled form was
|
|
677
|
+
// never part of its contract.
|
|
678
|
+
//
|
|
679
|
+
// `efforts` is optional: many catalog entries declare no level set, and reading
|
|
680
|
+
// `Object.keys` off that `undefined` used to throw before the request was built.
|
|
674
681
|
const levels = entry?.efforts === undefined ? [] : Object.keys(entry.efforts)
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
682
|
+
const preferred = levels.includes(options.reasoningEffort)
|
|
683
|
+
? options.reasoningEffort
|
|
684
|
+
: (levels.includes(entry?.defaultEffort) ? entry.defaultEffort : undefined)
|
|
685
|
+
const effort = preferred ?? (levels.includes('high') ? 'high' : levels[0])
|
|
686
|
+
if (effort !== undefined) body.reasoning_effort = effort
|
|
680
687
|
return body
|
|
681
688
|
}
|
|
682
689
|
|
|
@@ -968,6 +975,20 @@ export class WorkBuddyAdapter {
|
|
|
968
975
|
}
|
|
969
976
|
|
|
970
977
|
const argsTruncated = [...toolCalls.values()].some((block) => isTruncatedArguments(block.text))
|
|
978
|
+
// A step that ends with prose and no tool call *looks* like the model choosing to
|
|
979
|
+
// stop, so nothing else records it — and "it stopped mid-answer" arrives as a bug
|
|
980
|
+
// report with no evidence. These two shapes are the ones seen in practice:
|
|
981
|
+
// an announcement that expects a next action ("……:" / "………") and a statement of
|
|
982
|
+
// inability ("我需要先读取…" / "我无法…"). Logging them with the raw reason and the
|
|
983
|
+
// text turns the next report into a fact.
|
|
984
|
+
if (streamEnded === true && toolOrder.length === 0 && (finishReason === 'stop' || finishReason === undefined)) {
|
|
985
|
+
const prose = blocks.find((block) => block.kind === 'text')?.text ?? ''
|
|
986
|
+
const tail = prose.trim()
|
|
987
|
+
const suspicious = /[::…]$/.test(tail) || /(我需要|我没有|我无法|先读取|无法直接|不能直接)/.test(tail)
|
|
988
|
+
if (suspicious) {
|
|
989
|
+
this.ctx.logger.warn(`[workbuddy] ${this.region.id}: ${options.model} 疑似提前收尾(finish_reason=${finishReason ?? '(无)'},输出 ${outputTokens ?? '未知'} tokens,无工具调用):「${tail.slice(0, 60)}」`)
|
|
990
|
+
}
|
|
991
|
+
}
|
|
971
992
|
// A stream that stops without the `[DONE]` sentinel was cut, not finished:
|
|
972
993
|
// upstream closed the connection mid-answer. Reporting that as a normal stop
|
|
973
994
|
// ends the turn silently — the model looks like it simply chose to stop — and
|