TestChatDispatchesToolCallsOnFinishStop: Ollama's /v1 shim sometimes emits finish_reason="stop" even after streaming tool_calls. The client must still emit EventToolCall, or the call vanishes and the agent faces an empty turn.
(t *testing.T)
| 235 | // finish_reason="stop" even after streaming tool_calls. The client must still |
| 236 | // emit EventToolCall, or the call vanishes and the agent faces an empty turn. |
| 237 | func TestChatDispatchesToolCallsOnFinishStop(t *testing.T) { |
| 238 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { |
| 239 | sseOK(w, []string{ |
| 240 | `{"choices":[{"delta":{"tool_calls":[{"index":0,"id":"c1","function":{"name":"bash","arguments":"{\"cmd\":\"ls\"}"}}]}}]}`, |
| 241 | `{"choices":[{"delta":{},"finish_reason":"stop"}],"usage":{"completion_tokens":5}}`, |
| 242 | }) |
| 243 | })) |
| 244 | defer srv.Close() |
| 245 | c := New(srv.URL, "m", "") |
| 246 | var got *chmctx.ToolCall |
| 247 | var final *chmctx.Message |
| 248 | for _, e := range collect(c.Chat(context.Background(), nil, nil)) { |
| 249 | switch e.Kind { |
| 250 | case EventToolCall: |
| 251 | got = e.ToolCall |
| 252 | case EventDone: |
| 253 | final = e.Final |
| 254 | } |
| 255 | } |
| 256 | if got == nil || got.Name != "bash" { |
| 257 | t.Fatalf("tool call must emit on finish_reason=stop: %+v", got) |
| 258 | } |
| 259 | if cmd, _ := got.Arguments["cmd"].(string); cmd != "ls" { |
| 260 | t.Fatalf("args not carried through: %+v", got.Arguments) |
| 261 | } |
| 262 | if final == nil || len(final.ToolCalls) != 1 { |
| 263 | t.Fatalf("final should still carry the tool call: %+v", final) |
| 264 | } |
| 265 | } |
| 266 | |
| 267 | // TestChatToolCallLateIDPreserved: spec ships the tool_call `id` in the first |
| 268 | // fragment, but a sloppy provider may delay it. The client must update slot.id |