|
1 | 1 | import json |
2 | 2 | import logging |
| 3 | +import re |
3 | 4 | import unittest.mock |
4 | 5 |
|
5 | 6 | import pydantic |
@@ -127,7 +128,12 @@ def test_format_request_with_image(model, model_id): |
127 | 128 |
|
128 | 129 | def test_format_request_with_tool_use(model, model_id): |
129 | 130 | messages = [ |
130 | | - {"role": "assistant", "content": [{"toolUse": {"toolUseId": "calculator", "input": '{"expression": "2+2"}'}}]} |
| 131 | + { |
| 132 | + "role": "assistant", |
| 133 | + "content": [ |
| 134 | + {"toolUse": {"toolUseId": "tool-use-id-123", "name": "calculator", "input": '{"expression": "2+2"}'}} |
| 135 | + ], |
| 136 | + } |
131 | 137 | ] |
132 | 138 |
|
133 | 139 | tru_request = model.format_request(messages) |
@@ -321,9 +327,11 @@ def test_format_chunk_content_start_tool(model): |
321 | 327 | event = {"chunk_type": "content_start", "data_type": "tool", "data": mock_function} |
322 | 328 |
|
323 | 329 | tru_chunk = model.format_chunk(event) |
324 | | - exp_chunk = {"contentBlockStart": {"start": {"toolUse": {"name": "calculator", "toolUseId": "calculator"}}}} |
| 330 | + tool_use = tru_chunk["contentBlockStart"]["start"]["toolUse"] |
325 | 331 |
|
326 | | - assert tru_chunk == exp_chunk |
| 332 | + assert tool_use["name"] == "calculator" |
| 333 | + assert tool_use["toolUseId"] != "calculator" |
| 334 | + assert len(tool_use["toolUseId"]) > 0 |
327 | 335 |
|
328 | 336 |
|
329 | 337 | def test_format_chunk_content_delta_text(model): |
@@ -499,24 +507,27 @@ async def test_stream_with_tool_calls(ollama_client, model, agenerator, alist): |
499 | 507 | response = model.stream(messages) |
500 | 508 |
|
501 | 509 | tru_events = await alist(response) |
502 | | - exp_events = [ |
503 | | - {"messageStart": {"role": "assistant"}}, |
504 | | - {"contentBlockStart": {"start": {}}}, |
505 | | - {"contentBlockStart": {"start": {"toolUse": {"name": "calculator", "toolUseId": "calculator"}}}}, |
506 | | - {"contentBlockDelta": {"delta": {"toolUse": {"input": '{"expression": "2+2"}'}}}}, |
507 | | - {"contentBlockStop": {}}, |
508 | | - {"contentBlockDelta": {"delta": {"text": "I'll calculate that for you"}}}, |
509 | | - {"contentBlockStop": {}}, |
510 | | - {"messageStop": {"stopReason": "tool_use"}}, |
511 | | - { |
512 | | - "metadata": { |
513 | | - "usage": {"inputTokens": 8, "outputTokens": 15, "totalTokens": 23}, |
514 | | - "metrics": {"latencyMs": 2.0}, |
515 | | - } |
516 | | - }, |
517 | | - ] |
518 | 510 |
|
519 | | - assert tru_events == exp_events |
| 511 | + # Verify the tool use event has a unique ID (not equal to the tool name) |
| 512 | + tool_start_event = tru_events[2] |
| 513 | + tool_use = tool_start_event["contentBlockStart"]["start"]["toolUse"] |
| 514 | + assert tool_use["name"] == "calculator" |
| 515 | + assert tool_use["toolUseId"] != "calculator" |
| 516 | + |
| 517 | + # Verify all other events |
| 518 | + assert tru_events[0] == {"messageStart": {"role": "assistant"}} |
| 519 | + assert tru_events[1] == {"contentBlockStart": {"start": {}}} |
| 520 | + assert tru_events[3] == {"contentBlockDelta": {"delta": {"toolUse": {"input": '{"expression": "2+2"}'}}}} |
| 521 | + assert tru_events[4] == {"contentBlockStop": {}} |
| 522 | + assert tru_events[5] == {"contentBlockDelta": {"delta": {"text": "I'll calculate that for you"}}} |
| 523 | + assert tru_events[6] == {"contentBlockStop": {}} |
| 524 | + assert tru_events[7] == {"messageStop": {"stopReason": "tool_use"}} |
| 525 | + assert tru_events[8] == { |
| 526 | + "metadata": { |
| 527 | + "usage": {"inputTokens": 8, "outputTokens": 15, "totalTokens": 23}, |
| 528 | + "metrics": {"latencyMs": 2.0}, |
| 529 | + } |
| 530 | + } |
520 | 531 | expected_request = { |
521 | 532 | "model": "m1", |
522 | 533 | "messages": [{"role": "user", "content": "Calculate 2+2"}], |
@@ -625,3 +636,57 @@ def test_format_request_filters_location_source_document(model, caplog): |
625 | 636 | user_message = formatted_messages[0] |
626 | 637 | assert user_message["content"] == "analyze this document" |
627 | 638 | assert "Location sources are not supported by Ollama" in caplog.text |
| 639 | + |
| 640 | + |
| 641 | +def test_tool_use_id_is_unique_and_not_tool_name(model): |
| 642 | + """Test that toolUseId is a unique UUID, not the tool name.""" |
| 643 | + mock_function = unittest.mock.Mock() |
| 644 | + mock_function.function.name = "calculator" |
| 645 | + |
| 646 | + event = {"chunk_type": "content_start", "data_type": "tool", "data": mock_function} |
| 647 | + |
| 648 | + chunk1 = model.format_chunk(event) |
| 649 | + chunk2 = model.format_chunk(event) |
| 650 | + |
| 651 | + tool_use1 = chunk1["contentBlockStart"]["start"]["toolUse"] |
| 652 | + tool_use2 = chunk2["contentBlockStart"]["start"]["toolUse"] |
| 653 | + |
| 654 | + # toolUseId should not equal the tool name |
| 655 | + assert tool_use1["toolUseId"] != "calculator" |
| 656 | + assert tool_use2["toolUseId"] != "calculator" |
| 657 | + |
| 658 | + # toolUseId should be unique across calls |
| 659 | + assert tool_use1["toolUseId"] != tool_use2["toolUseId"] |
| 660 | + |
| 661 | + # toolUseId should follow the tooluse_<24-hex> convention used by other providers |
| 662 | + assert re.fullmatch(r"tooluse_[0-9a-f]{24}", tool_use1["toolUseId"]) |
| 663 | + assert re.fullmatch(r"tooluse_[0-9a-f]{24}", tool_use2["toolUseId"]) |
| 664 | + |
| 665 | + # name should still be correct |
| 666 | + assert tool_use1["name"] == "calculator" |
| 667 | + assert tool_use2["name"] == "calculator" |
| 668 | + |
| 669 | + |
| 670 | +def test_format_request_uses_tool_name_not_tool_use_id(model, model_id): |
| 671 | + """Test that format_request uses the 'name' field, not 'toolUseId', for the function name.""" |
| 672 | + messages = [ |
| 673 | + { |
| 674 | + "role": "assistant", |
| 675 | + "content": [ |
| 676 | + { |
| 677 | + "toolUse": { |
| 678 | + "toolUseId": "unique-id-abc-123", |
| 679 | + "name": "calculator", |
| 680 | + "input": '{"expression": "1+1"}', |
| 681 | + } |
| 682 | + } |
| 683 | + ], |
| 684 | + } |
| 685 | + ] |
| 686 | + |
| 687 | + request = model.format_request(messages) |
| 688 | + tool_call = request["messages"][0]["tool_calls"][0] |
| 689 | + |
| 690 | + # The function name in the request must come from "name", not "toolUseId" |
| 691 | + assert tool_call["function"]["name"] == "calculator" |
| 692 | + assert tool_call["function"]["name"] != "unique-id-abc-123" |
0 commit comments