@huai_xin/dsh-workbuddy 0.1.8 → 0.1.9

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.
@@ -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` | 否 | `{"type":"enabled"}`,deepseek 系使用 📖✅ |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huai_xin/dsh-workbuddy",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "WorkBuddy model provider for DeepSeek Harness (international + China deployments)",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
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: there is no switch anywhere in the plugin. Upstream
664
- // publishes `reasoning.canDisableThinking` without naming a request parameter
665
- // that would act on it (protocol §4.3.1), so the effort level is the only
666
- // reasoning control, and the explicit `thinking` field is sent where the
667
- // official client is known to send it (deepseek family, §4.5) — the other
668
- // families ignore it, whatever the value.
669
- const isDeepSeek = model.startsWith('deepseek-')
670
- if (isDeepSeek) body.thinking = { type: 'enabled' }
671
- // `efforts` is optional: many catalog entries declare no level set, and
672
- // reading `Object.keys` off that `undefined` used to throw before the
673
- // request was even built.
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
- if (options.reasoningEffort !== undefined && levels.includes(options.reasoningEffort)) {
676
- body.reasoning_effort = options.reasoningEffort
677
- } else if (isDeepSeek && levels.length > 0) {
678
- body.reasoning_effort = levels.includes('high') ? 'high' : levels[0]
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