Files
HomeAgent/internal/lua/ollama_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

56 lines
1.9 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"
)
// TestOllamaAdapterEmitsFlatToolCallsWithStreamIndex 用 **Ollama 协议**
// 的形态喂分片,验证输出是扁平结构且键名是 stream_index。
//
// ollama 的 tool_calls 是**整条一次发完**(不分片),所以 stream_index 取
// 数组下标。它此前发的是嵌套 ["function"]={...} + index 键名 —— 两种都是
// **静默**失效:Go 侧 agentAPI.ToolCall 没有 function 字段(取零值),
// 键名 index 也不会映射到 StreamIndex(取零值)。
func TestOllamaAdapterEmitsFlatToolCallsWithStreamIndex(t *testing.T) {
vm := NewVM(t.TempDir())
loadBundled(t, vm, "ollama")
chunk := `{"message":{"content":"","tool_calls":[` +
`{"id":"c0","function":{"name":"Read","arguments":"{\"p\":1}"}},` +
`{"id":"c1","function":{"name":"Write","arguments":"{\"p\":2}"}}]},` +
`"done":false}`
out, err := vm.CallTransformStreamChunk("ollama", chunk)
if err != nil {
t.Fatalf("transform_stream_chunk: %v", err)
}
var u struct {
ToolCalls []map[string]interface{} `json:"tool_calls"`
}
if json.Unmarshal([]byte(out), &u) != nil {
t.Fatalf("输出非法: %s", out)
}
if len(u.ToolCalls) != 2 {
t.Fatalf("应有 2 个 tool_call,实际 %d: %s", len(u.ToolCalls), out)
}
for i, tc := range u.ToolCalls {
if _, nested := tc["function"]; nested {
t.Errorf("[%d] 仍是**嵌套**形态 %v —— Go 侧没有 function 字段,"+
"name/raw_arguments 取零值(静默)", i, tc)
}
if _, has := tc["name"]; !has {
t.Errorf("[%d] 缺顶层 name: %v", i, tc)
}
if _, has := tc["raw_arguments"]; !has {
t.Errorf("[%d] 缺顶层 raw_arguments: %v", i, tc)
}
si, ok := tc["stream_index"]
if !ok {
t.Errorf("[%d] 缺 stream_index: %v —— 键名写成 index 的话内核取零值,"+
"多分片并桶、参数混拼(静默)", i, tc)
} else if int(si.(float64)) != i {
t.Errorf("[%d] stream_index = %v,应为 %d", i, si, i)
}
}
}