Skip to content

fix(agent): 兼容 reasoning model#1357

Merged
CodFrm merged 6 commits intorelease/v1.4-agentfrom
fix(agent)-兼容-reasoning-model
Apr 22, 2026

Hidden character warning

The head ref may contain hidden characters: "fix(agent)-\u517c\u5bb9-reasoning-model"
Merged

fix(agent): 兼容 reasoning model#1357
CodFrm merged 6 commits intorelease/v1.4-agentfrom
fix(agent)-兼容-reasoning-model

Conversation

@cyfung1031
Copy link
Copy Markdown
Collaborator

@cyfung1031 cyfung1031 commented Apr 19, 2026

Checklist / 检查清单

  • Fixes mentioned issues / 修复已提及的问题
  • Code reviewed by human / 代码通过人工检查
  • Changes tested / 已完成测试

Description / 描述

  • streaming response 在 reasoning model 出现
  • <think>...</think> 在 reasoning model 出现 (gemma4)

測試: Unsloth Studio 安裝 unsloth/gemma-4-E2B-it-GGUF

Screenshots / 截图

@cyfung1031 cyfung1031 changed the title Fix(agent) 兼容 reasoning model fix(agent): 兼容 reasoning model Apr 19, 2026
@CodFrm CodFrm requested a review from Copilot April 22, 2026 16:01
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR improves compatibility with “reasoning” models by preventing streamed responses/thinking tags from leaking into user-visible content, and adds coverage for resource loading behavior.

Changes:

  • Add <think>...</think> filtering in OpenAI-stream parsing to route thought text into thinking_delta.
  • Adjust AgentProvider’s test request to use explicit system/user prompts and disable streaming.
  • Refactor ResourceService.loadByUrl to use Uint8Array consistently and add unit tests for resource loading.

Reviewed changes

Copilot reviewed 7 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/pkg/utils/monaco-editor/utils.test.ts Updates test fixtures for global comment parsing/updating.
src/pages/options/routes/AgentProvider.tsx Changes provider “probe” request to be non-streaming and include system/user prompts.
src/app/service/service_worker/resource.ts Simplifies blob-to-bytes handling by removing redundant Uint8Array conversion.
src/app/service/service_worker/resource.test.ts Adds tests for ResourceService.loadByUrl across text/binary/error cases.
src/app/service/agent/core/providers/openai.ts Adds <think> tag handling in SSE stream content deltas.
pnpm-workspace.yaml Adds workspace-level configuration.
package.json Bumps Rspack and pnpm versions.
.gitignore Ignores .omc.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines +142 to +143
// 跨 chunk 追踪 <think>...</think> 块状态(用于把思考混在 content 里的模型)
let inThinkBlock = false;
Comment on lines +202 to +238
// 处理 <think>...</think> 内联标签(reasoning 模型)
// 思考内容路由为 thinking_delta,避免裸露标签出现在对话里
let remaining: string = delta.content;

while (remaining.length > 0) {
if (inThinkBlock) {
// 已在 think 块内,找结束标签
const endIdx = remaining.indexOf("</think>");
if (endIdx === -1) {
// 整段都是思考内容
onEvent({ type: "thinking_delta", delta: remaining });
remaining = "";
} else {
// 结束标签之前是思考内容,之后是正文
if (endIdx > 0) {
onEvent({ type: "thinking_delta", delta: remaining.slice(0, endIdx) });
}
inThinkBlock = false;
remaining = remaining.slice(endIdx + "</think>".length);
}
} else {
// 不在 think 块内,找开始标签
const startIdx = remaining.indexOf("<think>");
if (startIdx === -1) {
// 整段都是正文
onEvent({ type: "content_delta", delta: remaining });
remaining = "";
} else {
// 开始标签之前是正文,之后进入思考块
if (startIdx > 0) {
onEvent({ type: "content_delta", delta: remaining.slice(0, startIdx) });
}
inThinkBlock = true;
remaining = remaining.slice(startIdx + "<think>".length);
}
}
}
Comment thread pnpm-workspace.yaml
@@ -0,0 +1 @@
minimumReleaseAge: 10080
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

该 PR 主要为 Agent 体系补齐对“推理模型(reasoning model)”的兼容:一方面在 OpenAI 兼容的流式解析中将 <think>...</think> 这类内联思考标签路由为 thinking_delta,另一方面在 Options 里的“测试连接”请求中显式关闭流式返回;同时顺带修正了 ResourceService 的二进制读取实现并补充对应单测,并更新了 pnpm/rspack 相关依赖与锁文件。

Changes:

  • OpenAI SSE 流解析:识别并拆分 <think>...</think>,将思考内容输出为 thinking_delta
  • Options/AgentProvider 测试连接请求:增加 system/user 提示词并设置 stream: false
  • ResourceService:直接使用 blobToUint8Array,新增 loadByUrl 单测;并更新 pnpm/rspack/pnpm-lock 等工程依赖

Reviewed changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/app/service/agent/core/providers/openai.ts 在 content 增量中解析 <think>...</think> 并转发为 thinking_delta
src/pages/options/routes/AgentProvider.tsx 测试连接请求增加 system/user 消息并显式 stream: false
src/app/service/service_worker/resource.ts 资源加载改用 blobToUint8Array,去掉多余的 ArrayBuffer → Uint8Array 转换
src/app/service/service_worker/resource.test.ts 新增 ResourceService.loadByUrl 的单测覆盖文本/二进制/异常场景
src/pkg/utils/monaco-editor/utils.test.ts 测试用例数据微调(axios → moment)
pnpm-workspace.yaml 新增 pnpm workspace 配置(仅包含 minimumReleaseAge
package.json 升级 rspack 相关依赖版本与 pnpm 版本声明
pnpm-lock.yaml 锁文件随依赖升级发生大规模更新
.gitignore 忽略新增的 .omc 目录/文件
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines 401 to 409
body = JSON.stringify({
model: editingModel.model || defaultModel,
max_tokens: 256,
messages: [{ role: "user", content: "hi" }],
messages: [
{ role: "system", content: systemMessage },
{ role: "user", content: userMessage },
],
stream: false,
});
Comment on lines 142 to +238
@@ -196,7 +199,43 @@ export function parseOpenAIStream(
}
}
} else {
onEvent({ type: "content_delta", delta: delta.content });
// 处理 <think>...</think> 内联标签(reasoning 模型)
// 思考内容路由为 thinking_delta,避免裸露标签出现在对话里
let remaining: string = delta.content;

while (remaining.length > 0) {
if (inThinkBlock) {
// 已在 think 块内,找结束标签
const endIdx = remaining.indexOf("</think>");
if (endIdx === -1) {
// 整段都是思考内容
onEvent({ type: "thinking_delta", delta: remaining });
remaining = "";
} else {
// 结束标签之前是思考内容,之后是正文
if (endIdx > 0) {
onEvent({ type: "thinking_delta", delta: remaining.slice(0, endIdx) });
}
inThinkBlock = false;
remaining = remaining.slice(endIdx + "</think>".length);
}
} else {
// 不在 think 块内,找开始标签
const startIdx = remaining.indexOf("<think>");
if (startIdx === -1) {
// 整段都是正文
onEvent({ type: "content_delta", delta: remaining });
remaining = "";
} else {
// 开始标签之前是正文,之后进入思考块
if (startIdx > 0) {
onEvent({ type: "content_delta", delta: remaining.slice(0, startIdx) });
}
inThinkBlock = true;
remaining = remaining.slice(startIdx + "<think>".length);
}
}
}
Comment on lines +202 to +238
// 处理 <think>...</think> 内联标签(reasoning 模型)
// 思考内容路由为 thinking_delta,避免裸露标签出现在对话里
let remaining: string = delta.content;

while (remaining.length > 0) {
if (inThinkBlock) {
// 已在 think 块内,找结束标签
const endIdx = remaining.indexOf("</think>");
if (endIdx === -1) {
// 整段都是思考内容
onEvent({ type: "thinking_delta", delta: remaining });
remaining = "";
} else {
// 结束标签之前是思考内容,之后是正文
if (endIdx > 0) {
onEvent({ type: "thinking_delta", delta: remaining.slice(0, endIdx) });
}
inThinkBlock = false;
remaining = remaining.slice(endIdx + "</think>".length);
}
} else {
// 不在 think 块内,找开始标签
const startIdx = remaining.indexOf("<think>");
if (startIdx === -1) {
// 整段都是正文
onEvent({ type: "content_delta", delta: remaining });
remaining = "";
} else {
// 开始标签之前是正文,之后进入思考块
if (startIdx > 0) {
onEvent({ type: "content_delta", delta: remaining.slice(0, startIdx) });
}
inThinkBlock = true;
remaining = remaining.slice(startIdx + "<think>".length);
}
}
}
@CodFrm
Copy link
Copy Markdown
Member

CodFrm commented Apr 22, 2026

说起来,现在还有什么模型用 <think>...</think> ?感觉都是去年/前年的模型了

@CodFrm
Copy link
Copy Markdown
Member

CodFrm commented Apr 22, 2026

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

reasoning 模型通过 content 输出 <think>...</think> 时,token 级拆分可能
让标签跨多个 SSE event 到达(例如上一 chunk 末尾 "<th",下一 chunk 开头
"ink>")。之前的 indexOf 仅在单个 delta 内查找,会把残片当作正文输出。

- 新增 thinkTagCarry 缓冲末尾可能匹配标签前缀的残片
- 流结束([DONE] 或无 [DONE] 自然结束)时 flush carry,避免丢内容
- 补充 5 个单测覆盖单 chunk / 跨 chunk / 逐字符 / 残片 flush 场景
@CodFrm CodFrm merged commit b0a45f6 into release/v1.4-agent Apr 22, 2026
4 checks passed
@CodFrm CodFrm deleted the fix(agent)-兼容-reasoning-model branch April 22, 2026 17:00
@cyfung1031
Copy link
Copy Markdown
Collaborator Author

说起来,现在还有什么模型用 <think>...</think> ?感觉都是去年/前年的模型了

gemma4 吧
我是用 Unsloth Studio 跑 unsloth/gemma-4-E2B-it-GGUF

https://ai.google.dev/gemma/docs/core/model_card_4?hl=en

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants