# dsh-llm-fidelity

[English](README.md) | [简体中文](README.zh.md)

**你的 Agent 干活干到一半，模型突然拒绝了一个可选参数，整轮对话当场中断；你的用量看板上的数字，和账单对不上。dsh-llm-fidelity 把这两件事都解决——自动、无感、而且诚实。**

[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 插件。装上即可，其他什么都不用改。

## 装了之后会发生什么

一个真实场景，前后对比：

> 你让 Agent 修一个 bug，改到第三步，模型服务突然返回：
>
> ```text
> 400 — 'stop' is not supported with this model.
> ```
>
> **没装插件**——对话当场死亡。你翻供应商文档、改配置、重开会话，把任务从头再讲一遍。下周换一家供应商？同一堵墙，换了个参数再撞一次。
>
> **装了插件**——这个 400 在几毫秒内被吸收：出问题的参数被自动去掉，同一个请求原样重发。你看到的只是回答继续输出，仿佛什么都没发生。而且插件**记住了**这家服务的脾气——下一个请求连这次失败都不会发生。**你甚至不会知道这个错误存在过。**

同样的承诺适用于 `'max_tokens' is not supported…`、`'temperature' is not supported…`、`reasoning effort is not supported…` 等一整个家族——也包括更隐蔽的痛点：网关把你的用量统计清零、重复记账。全部在插件这里终结。

- 🔧 **对话不再中断** —— 被拒绝的参数在同一个请求内被修正重发，你永远看不到那个报错。
- 💯 **数字值得信任** —— 用量统计如实进入账目：不清零、不重复、不编造。
- 🚀 **零配置** —— 默认即生产可用；不注册任何模型路由，与官方和社区适配器零冲突。

---

## 这些报错，你一定见过

只要你让 Agent 接第三方模型或网关，几乎必然撞上过其中一条：

```text
Unsupported parameter: 'max_tokens' is not supported with this model.
Use 'max_completion_tokens' instead.

Unsupported parameter: 'stop' is not supported with this model.

'temperature' is not supported with this model.

reasoning effort is not supported by this model
```

这些报错真实、普遍、且仍在发生。OpenAI 的推理模型（GPT-5、GPT-6 系列）拒绝 `max_tokens`、`stop` 和 `temperature`——受害名单贯穿 [LiteLLM](https://github.com/BerriAI/litellm/issues/7292)、[Azure OpenAI](https://learn.microsoft.com/en-us/answers/questions/5840554/sudden-openai-errors-on-gpt-4o)、[crewAI](https://github.com/crewAIInc/crewAI/issues/2661) 和所有 OpenAI 兼容网关。把 `stop` 传 `null` 也没用（[crewAI #1908](https://github.com/crewAIInc/crewAI/issues/1908)）。`max_tokens` 改名 `max_completion_tokens` 还埋了个坑：新字段包含隐藏的推理 token，照旧额度换算会拿到[空回答——而且照样计费](https://kunavo.com/guides/openai-api-max-tokens-not-supported)。

**你实际经历的是**：Agent 干活干到第三个工具调用，撞上这条报错，整轮任务需要你手动纠错——翻文档、改配置、重启，祈祷别再撞上别的参数。

还有更安静的问题——用量统计。网关在流结束时重发一个**全零统计快照**，把真实数字抹掉；同一份快照发两遍，被**算成双倍**；缓存命中的字段干脆缺失。看板说 320 万 token，账单说 510 万，没人说得清差在哪。

## dsh-llm-fidelity 怎么解决

**当模型明确拒绝一个可选参数时，插件在同一个请求内把它修好并重发——毫秒级、完全无感。**

```text
你发出:   { temperature: 0.7, stop: ["END"], ... }
模型返回:  400 — 'temperature' is not supported with this model.
插件:     （去掉 temperature，立即重发）
模型返回:  200 — 这是你的回答。
你看到的:  回答。仅此而已。永远如此。
```

修正清单刻意很短：`temperature`、`reasoningEffort`、`stop`、`maxTokens`（省略，或在错误明确给出输出上限时收敛到该值）。被拒绝的字段是从供应商自己的错误消息里读出来的——不猜测、不盲试。插件还会**记住**哪家服务接受哪种组合（进程内缓存，1 小时有效），所以同一条路由的失败**至多发生一次**——第二个请求已经会说这家服务商的方言了。

**对用量数字**，插件逐帧守护统计流：后到的帧缺字段，抹不掉先前的真实统计；全零占位帧，冲不掉真实总量；重复帧，不会被算两遍。以及最高的一条原则：**服务从没上报过的数字，插件绝不编造。**少一条参考数据，也好过一条假数据。

## 好在哪

把 400 糊弄过去的办法有十几种，这个插件刻意选择最"笨"但最对的那些：

| | dsh-llm-fidelity |
|---|---|
| **只修供应商明确拒绝的** | ✅ 从错误消息本身读出被拒字段，绝不猜测。超长上下文、余额不足、认证失败、网络故障一律如实透传——真问题就该让你看见。 |
| **永不碰你的内容** | ✅ 消息、system、工具、约束一律不可触碰。只有可选标量（`temperature` / `reasoningEffort` / `stop` / `maxTokens`）允许被省略或收敛。 |
| **有界，不玩魔法** | ✅ 每个请求至多 4 次修正，每次必须实际改变请求。没有无限重试，没有悄悄换协议。 |
| **会学习** | ✅ 按"服务商 + 模型"记忆（进程内，1 小时 TTL）。失败一次，同路由永不再犯。 |
| **与所有人和睦相处** | ✅ 不注册任何模型路由、不替换任何服务。官方适配器、社区适配器、官方 [`dsh-llm-retry`](https://github.com/deepseek-ai/deepseek-harness) 照常工作——429 限流和服务器错误是 retry 的本职，插件主动让路。 |

## 安装

> **兼容性**：支持 DSH `0.1.0-rc.6` 及以上、`0.2.0-rc` 整条版本线（已在 DeepSeek 桌面版 0.2.0-rc.2 上实测通过）。若安装时提示"不兼容"，请先确认你的 DSH 版本。

```bash
dsh plugin add dsh-llm-fidelity
```

或任何基于 npm 的流程：

```bash
pnpm add dsh-llm-fidelity
```

然后在插件设置中启用。默认即生产可用，所有开关可选：

| 配置项 | 默认 | 说明 |
|---|---|---|
| `usageFidelity` | `true` | 用量统计守护（去重、防清零、防缺字段） |
| `rectify` | `true` | 参数报错自动修正 |
| `rectifyStatuses` | `[400, 422]` | 哪些状态码视为"参数报错" |
| `maxAdaptations` | `4` | 每个请求最多自动重发几次 |
| `strategyTtlMs` | `3600000` | 记住的服务参数偏好保留多久（毫秒） |
| `strategyCacheSize` | `256` | 最多记住多少条服务偏好 |
| `rectifyAuxiliary` | `false` | 后台任务（如自动取名）是否也参与修正 |

### 怎么修改配置

插件通过 profile 的 bundle 层安装，配置也在 profile 的 patch 文件里改（桌面版默认是 `~/.dsh/profiles/desktop/cordis.patch.yml`）。按行 id `llm-fidelity` 覆盖即可：

```yaml
- id: llm-fidelity
  config:
    maxAdaptations: 2
    rectifyAuxiliary: true
```

注意：patch 的 `config` 是**整体替换**不是合并——覆盖时请把想保留的键一并写全（未写出的键回落到内置默认值）。

## 工作原理（给想深入了解的人）

插件挂在 DeepSeek Harness 的 `llm/stream` 事件上——所有模型请求的必经之路。它不注册任何模型路由、不替换任何服务，只是在路边加了两道检查：

- **参数修正**：监视"明确拒绝某个参数"的报错，修正出问题的可选字段，通过公开的 `ctx.llm.stream()` 入口重新入队（和正常请求完全同一条路，内部状态保持一致）。**只有还没向下游交付任何内容时才允许重发**——内容一旦到达你手上，错误原样透传。不重放，不伪造成功。
- **用量守护**：逐帧检查统计流，补齐缺失字段、去掉重复和占位符，在回合结束前把最终数字交给记账层。

## 边界（诚实声明）

- 插件工作在统一数据层，只能看到标准化的数据帧。服务**从来没上报**的数字（比如缓存命中数）找不回来——它保证数据干净，不负责无中生有。
- OAuth 登录类服务、超长上下文、余额不足，设计上不在修正范围内。
- 会话日志记录原始请求参数，修正在链路上透明；内容重放跟随最终成功的那次尝试。

## License

MIT
