Files
Genarrative/server-rs/crates/platform-llm
lhk229 fc53ee5c9c
Project CI / Frontend tests (pull_request) Failing after 27s
Project CI / Repository checks (pull_request) Failing after 54s
Project CI / Backend tests (pull_request) Successful in 4m55s
Project CI / Native shell tests (pull_request) Failing after 8m17s
补齐 Responses 的 incomplete 终态
Responses 的整体收尾信号有两个,此前只处理了 completed,incomplete 落入
通配分支被整个忽略。真实端点抓包确认:撞到 max_output_tokens 时上游只发
response.incomplete、不发 completed,载荷与 completed 同构,带完整 output[],
item 标 status=incomplete,incomplete_details.reason 给出原因。

忽略它造成三件事:不终止读取循环,网关不主动关连接就等到调用方超时;流式
Responses 永远产生不出 incomplete 这个 finish_reason,上一轮加的截断拒绝规则
对它形同虚设;completed-only 型网关的工具调用被静默丢掉。

与 completed 合并到同一分支,只在 finish_reason 上区分。incomplete 由此自动
走 reject_incomplete_tool_calls:工具调用拒绝,正文按降级结果返回,与 Chat 的
length 口径一致。

三个用例的事件序列转录自真实抓包,退回通配分支时三条全部失败。
2026-07-27 08:51:53 +00:00
..
2026-07-27 04:06:26 +00:00

platform-llm 平台适配 crate

日期:2026-07-27

1. crate 职责

platform-llm 是 Rust 工作区里的大模型平台适配 crate,当前已经落地以下能力:

  1. 统一 Ark / DashScope / Anthropic / 其他兼容网关的文本模型配置结构
  2. 统一 OpenAI Chat Completions、OpenAI Responses 和 Anthropic Messages 的文本 / 原生 function tool 请求、非流式响应与 SSE 流式解析
  3. 统一超时、连接失败、上游错误、空响应与重试策略
  4. 为后续 module-ai、module-story、module-npc、module-custom-world 提供可直接复用的基础 client

2. 当前能力边界

当前实现只覆盖“文本 run”主链,不提前混入媒体生成和业务编排:

  1. 对外抽象固定为 LlmRunRequest / LlmRunResponse,不再保留旧 LlmTextRequest / LlmTextResponse 类型。
  2. OpenAiChat、OpenAiResponses 与 Anthropic 三类 API kind 都支持 JSON 请求、非流式响应和 SSE 流式响应;默认 API kind 仍为 OpenAiResponses。
  3. 三类协议都使用统一的 function_tools / tool_choice 输入和 LlmRunResponse.tool_calls 输出。Anthropic 请求使用顶层 tools[].input_schema 与对象形态 tool_choice;Anthropic URL 默认在 base URL 后拼 /v1/messages,如果 base URL 已以 /v1 结尾则只拼 /messages。
  4. Anthropic 当前仍不支持 web_search、图片内容和纯 system 消息;至少需要一条非 system 文本消息。角色动画、图片、视频、资产轮询仍留在其他平台适配和业务模块任务里。
  5. 流式 on_delta 只发送文本增量与完成原因;工具调用增量在 crate 内按 slot 聚合,完整调用只从最终 LlmRunResponse.tool_calls 读取。上下文管理、后台执行和业务状态不写回本 crate。
  6. 支持按 provider 打标签,但不把业务 prompt、SSE 转发和模块状态写回本 crate。
  7. DashScope 当前只通过“调用方显式提供兼容文本网关 base url”的方式接入,不复用图像 API。
  8. 角色动画、图片、视频、资产轮询仍留在后续 platform-llm / platform-oss / 业务模块任务里另行实现。

3. 三协议工具支持矩阵

API kind 请求工具形态 tool_choice 非流式工具响应 流式工具聚合
OpenAiChat tools[].type=function,函数内为 name / description / parameters / strict "auto" / "required" choices[0].message.tool_calls delta.tool_calls[].index;首片提供 id/name,后续拼接 arguments
OpenAiResponses tools[].type=function,函数内为 name / description / parameters / strict "auto" / "required" output[].type=function_call output_index;output_item.added 提供身份,function_call_arguments.delta 拼接,.done 覆盖完整参数
Anthropic 顶层 tools[] 为 name / description / input_schema,没有 function 包装层和 strict { "type": "auto" } / { "type": "any" };Required 映射为 any content[].type=tool_use,input 序列化为 arguments content block index;content_block_start 提供身份,input_json_delta 拼接参数

Responses 如果只发送 response.completed,解析器会从其中的 response.output[] 恢复 function_call;恢复时使用 output 数组下标作为 slot。三种协议的并行工具调用只在平台层做 slot 聚合,不代表工具会在平台层并发执行。

4. 流式与参数契约

  1. LlmStreamDelta 只包含 accumulated_text、delta_text 和 finish_reason,工具调用不会进入 on_delta;纯工具响应允许 text 为空。
  2. 工具片段按协议索引聚合:Chat 使用 delta.tool_calls[].index,Responses 使用 output_index,Anthropic 使用 content block index。Responses 的 .done 和 response.completed 完整 arguments 是权威值,可以覆盖之前的分片拼接。
  3. 流结束固化工具调用时,缺少 id 或函数名返回 Deserialize;空参数默认保存为 {};非空参数必须能反序列化为完整 JSON,截断或半截 JSON 不会交给业务层。这里是 JSON 语法完整性检查,不是针对 parameters 的 JSON Schema 业务校验。
  4. 非流式工具调用采用不同的参数边界:缺失或空白 arguments 统一归一为 {};Chat / Responses 的非空畸形 arguments 不在平台层做 JSON 校验、修复或静默丢弃,而是保留参数内容(仅按统一归一策略去除首尾空白),连同 call id 和函数名交给调用方的 repair 循环。Anthropic tool_use.input 缺失时同样按 {} 归一;调用方不能把非流式参数自动假定为统一 schema 校验通过。

5. 错误边界

  1. Deserialize:上游 JSON、SSE 或 UTF-8 无法解析,Chat 非流式缺少 choices[0],流式工具调用缺少 id / name,或流式工具参数不是完整 JSON。
  2. StreamUnavailable:仅用于流式响应已声明 tool_use / tool_calls,但一个工具 slot 都没有聚合出来的协议兼容失败;调用方可以据此回退一次非流式请求,不能把已收到的解说文本当成最终回复。
  3. EmptyResponse:最终文本为空且工具调用也为空。只有文本为空但存在有效工具调用时,响应才是合法的纯工具响应。
  4. 流尾部出现 Timeout、Connectivity、Transport 或 Deserialize 时,只有已形成非空文本或至少一个工具调用、观察到协议完成信号、已有完成原因且工具参数完整的响应才会保留;纯工具响应即使 text 为空也可以保留。工具参数半截或既没有文本也没有工具调用时继续返回错误。

6. 核心导出

当前对外导出以下公共类型:

  1. LlmProvider
  2. LlmConfig
  3. LlmMessageRole
  4. LlmMessage
  5. LlmRunRequest
  6. LlmApiKind
  7. LlmStreamDelta
  8. LlmFunctionTool
  9. LlmToolChoice
  10. LlmToolCall
  11. LlmRunResponse
  12. LlmTokenUsage
  13. LlmClient
  14. LlmError

7. 设计文档

当前文档入口

当前长期工程口径已融合到:

  1. ../../../docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md
  2. ../../../docs/【玩法创作】平台入口与玩法链路-2026-05-15.md
  3. ../../../docs/【开发运维】本地开发验证与生产运维-2026-05-15.md

旧阶段设计文档不再作为实现依据。

8. 边界约束

  1. platform-llm 只承接模型平台适配,不承接业务模块状态真相与业务规则。
  2. 业务模块只能依赖这里的统一 client / DTO / 错误模型,不能再把上游请求细节散落回各 crate。
  3. api-server 后续如果需要做 REST/SSE façade,只允许在协议层调用 platform-llm,不能复制一份私有实现。

9. 验收证据边界

  1. cargo test -p platform-llm 的确定性用例把 checked-in SSE fixture 交给 parser,验证归一后的文本、工具调用、slot 聚合、Responses completed-only 恢复、参数 JSON 完整性和错误边界。fixture 可以来源于真实端点抓包,但测试不保存原始 SSE,也不逐事件与端点报文比较,因此不能证明抓包转录无偏差。
  2. tests/live_stream_tool_calls.rs 是默认 #[ignore] 的真实端点工具调用 smoke。它只验证最终归一结果中的工具名、id 和完整参数 JSON;文本增量字符数仅用于打印观测,工具调用不进入 on_delta,也没有原始 SSE 录制或逐事件对比能力。
  3. 因此验收应分别称为“固定 SSE fixture parser 覆盖”和“真实端点归一工具调用 smoke”,不能把后者描述为原始 SSE fidelity 或转录一致性证明。