Skip to content

[Bug] HTTP API 入口首段截断 + 工具调用在 <tool_ 处随机丢失 #1375

Open

Description

@badaixue

提交前确认 / Before submitting

  • 我已经搜索过现有 Issue,确认这不是重复问题。
  • 我已经移除日志、截图和配置中的密钥及隐私信息。

Operit 版本 / Operit version

1.12.2

运行形态 / Distribution

Release APK

设备与运行环境 / Environment

设备型号:realme RMX2072
Android / 操作系统版本:Android 12 (SDK 31)
其他相关环境:

  • 功能绑定:CHAT → 配置「大脑deepseek」(configId 已隐去)
  • 模型:deepseek-v4-flash(也测过 deepseek-v4-pro)
  • 供应商:DeepSeek(也测过智谱 API)
  • 触发方式:MacroDroid → HTTP(POST 到 Operit 对话,端口 8094)

问题领域 / Area

Chat / 对话与消息

主要问题描述 / Main problem description

现象一(入口侧):MacroDroid 通过 HTTP POST 发送触发文本到 Operit,服务端返回 200。但该文本转成对话消息喂给模型时,固定长度的首段被削掉(如收到 <tool、TEST 等残片)。手动在输入框打字发送则完整无损。说明 HTTP 路径与手动路径进入模型的路径不同。

现象二(出口侧):AI 每轮生成工具调用时,在 <tool_xxx 标签开头处随机被截断,整个调用丢失。实测残片:<tool_L6Gi、<、<tool_4rx2、<tool_Z9qK、<tool_Z9q。

已排除的因素:

  1. max_tokens 调至 8192,仍断。
  2. 模型 flash/pro 互换,仍断。
  3. 供应商换智谱,仍断。
  4. 网络稳定,HTTP 200,已排除。
  5. 关闭流式输出,仍断。
  6. 杀进程+清缓存后连续 3 轮均断。

核心判断:手动消息路径正常,HTTP 路径和出口工具调用必断。问题疑似在 Operit 内部的「HTTP → 对话消息」转换环节及流式/分块解析层。

复现步骤 / Steps to reproduce

  1. 用 MacroDroid 通过 HTTP POST 发送任意文本到 Operit 对话。
  2. 观察 AI 收到的消息是否首段残缺 → 复现问题一。
  3. 让 AI 执行任意工具调用,多轮观察 → 复现问题二(在 <tool_xxx 处断)。
  4. 把手动输入与 HTTP 输入做对比,手动路径正常。

复现频率 / Reproducibility

Always / 每次发生

相关配置 / Related context

Provider / 模型:DeepSeek (deepseek-v4-flash, deepseek-v4-pro) / 智谱
接口类型或相关开关:HTTP API (端口 8094),已尝试关闭流式输出
工具、工作流、插件或 Skill 名称:MacroDroid 触发
其他相关配置:详见环境说明

截图或录屏 / Screenshot or recording

Image

日志或错误信息 / Logs or error output

AI 实际生成截断残片示例(多轮):

  • <tool_L6Gi
  • <
  • <tool_4rx2
  • <tool_Z9qK
  • <tool_Z9q

TESTLINE-完整 → 时好时坏
TEST(4 字符) → 断
OK(2 字符) → 过
带 tool_call 的输出 → 几乎必在首个 function_calls / 工具标签处断

Activity

  1. yoruuuchan commented on Oct 23, 2026

    @yoruuuchan
    Contributor

    I'd like to work on this. I've reproduced the two failure modes in the code paths for the external HTTP entry and the streaming tool-call parser, and I'm preparing a focused fix with regression tests for arbitrary chunk boundaries.

    Plan:

    • trace HTTP request body -> chat message -> provider request, and add repeatable tests for each stage;
    • fix the truncation at its actual source (no fallback paths);
    • cover tool tag boundaries and arbitrary chunk splits with unit tests;
    • verify with a build plus an executable end-to-end check.

    Will link the PR here once it's ready.

  2. badaixue commented on Oct 23, 2026

    @badaixue
    Author

    Thank you so much for looking into this! Your plan looks perfect. I'm using a non-root Android device (ColorOS) and your fix will completely solve my background automation blockers. Looking forward to the PR!

  3. yoruuuchan commented on Oct 24, 2026

    @yoruuuchan
    Contributor

    Opened PR #1414 against dev: #1414

    The two symptoms share one root cause. TextStreamEvent only carried (eventType, id), so each consumer derived the rollback offset from its own buffer. Because content chunks and revision events are collected by separate coroutines from separate replayable channels, the consumer's offset had usually already advanced past the aborted attempt's prefix by the time it dequeued SAVEPOINT. ROLLBACK then truncated to that stale offset and left the fragment behind. The HTTP entry's extra hops (NanoHTTPD worker → runBlocking → serviceScope → shareRevisable) widen that scheduling gap, which is why the API path reproduced far more often than manual input.

    I also probed the entry side directly: POST /api/external-chat with bodies well beyond NanoHTTPD's 8192-byte buffer arrives byte-exact (verified by length and SHA-256), so the text that reached the model was an aborted-attempt prefix rather than a truncated body.

    The fix makes the producer record the authoritative offset and the discarded range on every event, keeps the raw chunk sequence as the coordinate space, and rebuilds the final text from chunk plus event replay in every consumer. 1365 unit tests pass, including new coverage for arbitrary chunk splits, tool-tag boundaries, and consecutive aborted retries. End-to-end on an emulator: a 981-character HTTP message keeps the same SHA-256 through every stage, the first attempt emitting <tool_L6Gi is rolled back to length 0, and the retry's complete <tool_AB12 name="device_info"> call is parsed and executed.

  4. badaixue commented on Oct 24, 2026

    @badaixue
    Author

    Thank you so much! I saw my exact log fragment <tool_L6Gi in your E2E test description. This is amazing. Your fix completely explains the scheduling gap issue. Can't wait to see it merged!

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions