Files
HomeAgent/internal/lua/openai_family_stream_test.go
JianFeeeee cff8e10ad5 fix(lua): 补齐 6 个适配器的流式 tool_calls 支持
体检判据(TestAllBundledAdaptersStreamToolCallStatus)报出的三类问题,
本提交解决其中两类;第三类(gemini)未动,原因见下。

## ① OpenAI 兼容族:github / groq / mistral(3 个)

它们的 transform_stream_chunk 与修复前的 deepseek **逐字相同** ——
只透 content/done,tool_calls 处理只存在于 transform_response(非流式)。

后果与 deepseek 相同:流式模式下工具调用全部丢失,模型调不动任何工具,
且**没有任何报错**。生产当前未启用这三个源,但按预设配置的用户会踩到。

照 deepseek 的修法补上(含 reasoning_content 透传)。

## ② 嵌套形态 + 键名错:server / kimicode / anthropic / ollama(4 个)

这四个**有** tool_calls 处理,但发的是:

    { index = N, id = ..., ["function"] = { name = ..., arguments = ... } }

而 homed 的 `agentAPI.ToolCall` 是**扁平**结构,json tag 为:

    id / type / name / arguments / raw_arguments / stream_index

两处都是**静默**失效(Go 侧按 json tag 反序列化,取不到就是零值,无报错):
- **嵌套** `["function"]` ⇒ `name` / `raw_arguments` 取零值
  ⇒ flush 时判「无 name」丢弃,或参数为空
- **键名 `index`** ⇒ `StreamIndex` 取零值
  ⇒ 多个分片并到同一个桶,argsRaw 混拼 ⇒ 每个工具报「参数不是合法 JSON」
  而**一个都没真跑**

已逐项对齐为扁平 + `stream_index`。协议差异都保留:
- anthropic:`content_block_start` / `input_json_delta`,续传片 name 留空
  (内核按 stream_index 累积,补齐 name 后才 flush)
- ollama:tool_calls **整条一次发完**(不分片),故 stream_index 取数组下标

## ③ gemini 未动

它的流式函数处理 `candidates[].content.parts`,**全文件没有任何
tool_calls / functionCall 处理** —— 连非流式路径也没有。补它不是"对齐"
而是新实现,且 gemini 的 functionCall 形态(`functionCall: {name, args}`,
args 是对象而非 JSON 字符串)与 OpenAI 族不同,需要单独判据。

生产三个源(llmsproxy / visionllm / justworker)全部用 `openai.lua`,
不阻塞。留作独立项。

## 判据

- TestOpenAICompatibleFamilyHandlesStreamToolCalls  5 个 OpenAI 族适配器,
  逐个验证 tool_calls 未丢 + stream_index 正确
- TestAnthropicAdapterEmitsFlatToolCallsWithStreamIndex  用 **Anthropic 协议**
  的 fixture(不用 OpenAI 的,否则会因"不适用该 chunk"跳过 —— 看着绿,
  实则没测)
- TestOllamaAdapterEmitsFlatToolCallsWithStreamIndex  用 Ollama 协议形态
- TestDeepSeekAdapterHandlesStreamToolCalls  单列,因它有源预设指向

★ 三个判据按**协议**分文件而非逐适配器:这几个文件的流式函数逐字相同,
共用一个 fixture 会因协议不适用而静默跳过 —— 那等于没测。

## 体检分类

    修前: ✓ [openai]        ⚠ [kimicode server]  ✗ [anthropic deepseek gemini github groq mistral ollama]
    修后: ✓ [openai deepseek github groq mistral]  ⚠ []  ✗ [gemini]
2026-09-27 18:34:33 +08:00

65 lines
2.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package lua
import (
"encoding/json"
"testing"
)
// openAICompatibleStreamAdapters 是「OpenAI 兼容流式协议」那一族适配器。
//
// 它们的 transform_stream_chunk 曾只透 content/done,**完全不处理
// tool_calls**(那部分只存在于 transform_response 即非流式路径)。后果:
// 流式模式下工具调用全部丢失,模型调不动任何工具,且没有任何报错。
//
// 为什么难发现:非流式路径是好的 ⇒ 手工端到端测试也过;内核的 tool call
// 循环默认走流式 ⇒ 实际不可用;core 包的批内判据直接构造 Go 结构体,
// 绕过适配器 ⇒ 测不到这一层。
//
// ★ 本判据按**协议族**组织而不是逐个适配器:这几个文件的流式函数逐字相同,
//
// 逐个写判据只是复制粘贴,且漏掉一个就少一个保护。
var openAICompatibleStreamAdapters = []string{"openai", "deepseek", "github", "groq", "mistral"}
func TestOpenAICompatibleFamilyHandlesStreamToolCalls(t *testing.T) {
// 两个 tool_call,各一片:首片带 name、续传片只带 arguments
frags := []string{
`{"id":"c","choices":[{"index":0,"delta":{"tool_calls":[` +
`{"index":0,"id":"t0","type":"function","function":{"name":"cmd_run","arguments":""}}]}}]}`,
`{"id":"c","choices":[{"index":0,"delta":{"tool_calls":[` +
`{"index":1,"function":{"arguments":"{\"command\":\"x\"}"}}]}}]}`,
}
for _, name := range openAICompatibleStreamAdapters {
t.Run(name, func(t *testing.T) {
vm := NewVM(t.TempDir())
loadBundled(t, vm, name)
for i, f := range frags {
out, err := vm.CallTransformStreamChunk(name, f)
if err != nil {
t.Fatalf("第 %d 片: %v", i, err)
}
var u struct {
ToolCalls []struct {
StreamIndex int `json:"stream_index"`
Name string `json:"name"`
RawArguments string `json:"raw_arguments"`
} `json:"tool_calls"`
}
if json.Unmarshal([]byte(out), &u) != nil {
t.Fatalf("第 %d 片输出非法: %s", i, out)
}
if len(u.ToolCalls) == 0 {
t.Errorf("第 %d 片:%s 的流式路径**丢掉了 tool_call**\n"+
" 输出:%s\n"+
" ⇒ 该源在流式模式下无法调用任何工具,且无任何报错。",
i, name, out)
continue
}
if u.ToolCalls[0].StreamIndex != i {
t.Errorf("第 %d 片 stream_index = %d,应为 %d —— 缺它会让内核"+
"把多个分片并到同一个桶,参数混拼成非法 JSON", i, u.ToolCalls[0].StreamIndex, i)
}
}
})
}
}