char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
42f0225fe
char/aliceai-t5-moe
1#!/usr/bin/env python3 2import pytest 3import base64 4import requests 5 6from utils import * 7 8server :ServerProcess 9 10 11def get_test_image_base64 ()-> str : 12"""Get a test image in base64 format""" 13# Use the same test image as test_vision_api.py 14IMG_URL = "https://huggingface.co/ggml-org/tinygemma3-GGUF/resolve/main/test/11_truck.png" 15response = requests .get (IMG_URL ) 16response .raise_for_status () 17return base64 .b64encode (response .content ).decode ("utf-8" ) 18 19@ pytest . fixture ( autouse = True ) 20def create_server (): 21global server 22server = ServerPreset .tinyllama2 () 23server .model_alias = "tinyllama-2-anthropic" 24server .n_slots = 1 25server .n_ctx = 8192 26server .n_batch = 2048 27 28 29@ pytest . fixture 30def vision_server (): 31"""Separate fixture for vision tests that require multimodal support""" 32global server 33server = ServerPreset .tinygemma3 () 34server .offline = False # Allow downloading the model 35server .model_alias = "tinygemma3-anthropic" 36server .n_slots = 1 37return server 38 39 40# Basic message tests 41 42def test_anthropic_messages_basic (): 43"""Test basic Anthropic messages endpoint""" 44server .start () 45 46res = server .make_request ("POST" ,"/v1/messages" ,data = { 47"model" :"test" , 48"max_tokens" :50 , 49"messages" : [ 50 {"role" :"user" ,"content" :"Say hello" } 51 ] 52 }) 53 54assert res .status_code == 200 ,f"Expected 200, got { res . status_code } " 55assert res .body ["type" ]== "message" ,f"Expected type 'message', got { res . body . get ( 'type' ) } " 56assert res .body ["role" ]== "assistant" ,f"Expected role 'assistant', got { res . body . get ( 'role' ) } " 57assert "content" in res .body ,"Missing 'content' field" 58assert isinstance (res .body ["content" ],list ),"Content should be an array" 59assert len (res .body ["content" ])> 0 ,"Content array should not be empty" 60assert res .body ["content" ][0 ]["type" ]== "text" ,"First content block should be text" 61assert "text" in res .body ["content" ][0 ],"Text content block missing 'text' field" 62assert res .body ["stop_reason" ]in ["end_turn" ,"max_tokens" ],f"Invalid stop_reason: { res . body . get ( 'stop_reason' ) } " 63assert "usage" in res .body ,"Missing 'usage' field" 64assert "cache_read_input_tokens" in res .body ["usage" ],"Missing usage.cache_read_input_tokens" 65assert "input_tokens" in res .body ["usage" ],"Missing usage.input_tokens" 66assert "output_tokens" in res .body ["usage" ],"Missing usage.output_tokens" 67assert isinstance (res .body ["usage" ]["cache_read_input_tokens" ],int ),"cache_read_input_tokens should be integer" 68assert isinstance (res .body ["usage" ]["input_tokens" ],int ),"input_tokens should be integer" 69assert isinstance (res .body ["usage" ]["output_tokens" ],int ),"output_tokens should be integer" 70assert res .body ["usage" ]["output_tokens" ]> 0 ,"Should have generated some tokens" 71# Anthropic API should NOT include timings 72assert "timings" not in res .body ,"Anthropic API should not include timings field" 73 74 75def test_anthropic_messages_with_system (): 76"""Test messages with system prompt""" 77server .start () 78 79res = server .make_request ("POST" ,"/v1/messages" ,data = { 80"model" :"test" , 81"max_tokens" :50 , 82"system" :"You are a helpful assistant." , 83"messages" : [ 84 {"role" :"user" ,"content" :"Hello" } 85 ] 86 }) 87 88assert res .status_code == 200 89assert res .body ["type" ]== "message" 90assert len (res .body ["content" ])> 0 91 92 93def test_anthropic_messages_multipart_content (): 94"""Test messages with multipart content blocks""" 95server .start () 96 97res = server .make_request ("POST" ,"/v1/messages" ,data = { 98"model" :"test" , 99"max_tokens" :50 , 100"messages" : [ 101 { 102"role" :"user" , 103"content" : [ 104 {"type" :"text" ,"text" :"What is" }, 105 {"type" :"text" ,"text" :" the answer?" } 106 ] 107 } 108 ] 109 }) 110 111assert res .status_code == 200 112assert res .body ["type" ]== "message" 113 114 115def test_anthropic_messages_conversation (): 116"""Test multi-turn conversation""" 117server .start () 118 119res = server .make_request ("POST" ,"/v1/messages" ,data = { 120"model" :"test" , 121"max_tokens" :50 , 122"messages" : [ 123 {"role" :"user" ,"content" :"Hello" }, 124 {"role" :"assistant" ,"content" :"Hi there!" }, 125 {"role" :"user" ,"content" :"How are you?" } 126 ] 127 }) 128 129assert res .status_code == 200 130assert res .body ["type" ]== "message" 131 132 133# Streaming tests 134 135def test_anthropic_messages_streaming (): 136"""Test streaming messages""" 137server .start () 138 139res = server .make_stream_request ("POST" ,"/v1/messages" ,data = { 140"model" :"test" , 141"max_tokens" :30 , 142"messages" : [ 143 {"role" :"user" ,"content" :"Say hello" } 144 ], 145"stream" :True 146 }) 147 148events = [] 149for data in res : 150# Each event should have type and other fields 151assert "type" in data ,f"Missing 'type' in event: { data } " 152events .append (data ) 153 154# Verify event sequence 155event_types = [e ["type" ]for e in events ] 156assert "message_start" in event_types ,"Missing message_start event" 157assert "content_block_start" in event_types ,"Missing content_block_start event" 158assert "content_block_delta" in event_types ,"Missing content_block_delta event" 159assert "content_block_stop" in event_types ,"Missing content_block_stop event" 160assert "message_delta" in event_types ,"Missing message_delta event" 161assert "message_stop" in event_types ,"Missing message_stop event" 162 163# Check message_start structure 164message_start = next (e for e in events if e ["type" ]== "message_start" ) 165assert "message" in message_start ,"message_start missing 'message' field" 166assert message_start ["message" ]["type" ]== "message" 167assert message_start ["message" ]["role" ]== "assistant" 168assert message_start ["message" ]["content" ]== [] 169assert "usage" in message_start ["message" ] 170assert message_start ["message" ]["usage" ]["input_tokens" ]> 0 171 172# Check content_block_start 173block_start = next (e for e in events if e ["type" ]== "content_block_start" ) 174assert "index" in block_start ,"content_block_start missing 'index'" 175assert block_start ["index" ]== 0 ,"First content block should be at index 0" 176assert "content_block" in block_start 177assert block_start ["content_block" ]["type" ]== "text" 178 179# Check content_block_delta 180deltas = [e for e in events if e ["type" ]== "content_block_delta" ] 181assert len (deltas )> 0 ,"Should have at least one content_block_delta" 182for delta in deltas : 183assert "index" in delta 184assert "delta" in delta 185assert delta ["delta" ]["type" ]== "text_delta" 186assert "text" in delta ["delta" ] 187 188# Check content_block_stop 189block_stop = next (e for e in events if e ["type" ]== "content_block_stop" ) 190assert "index" in block_stop 191assert block_stop ["index" ]== 0 192 193# Check message_delta 194message_delta = next (e for e in events if e ["type" ]== "message_delta" ) 195assert "delta" in message_delta 196assert "stop_reason" in message_delta ["delta" ] 197assert message_delta ["delta" ]["stop_reason" ]in ["end_turn" ,"max_tokens" ] 198assert "usage" in message_delta 199assert message_delta ["usage" ]["output_tokens" ]> 0 200 201# Check message_stop 202message_stop = next (e for e in events if e ["type" ]== "message_stop" ) 203# message_stop should NOT have timings for Anthropic API 204assert "timings" not in message_stop ,"Anthropic streaming should not include timings" 205 206 207# Token counting tests 208 209def test_anthropic_count_tokens (): 210"""Test token counting endpoint""" 211server .start () 212 213res = server .make_request ("POST" ,"/v1/messages/count_tokens" ,data = { 214"model" :"test" , 215"messages" : [ 216 {"role" :"user" ,"content" :"Hello world" } 217 ] 218 }) 219 220assert res .status_code == 200 221assert "input_tokens" in res .body 222assert isinstance (res .body ["input_tokens" ],int ) 223assert res .body ["input_tokens" ]> 0 224# Should only have input_tokens, no other fields 225assert "output_tokens" not in res .body 226 227 228def test_anthropic_count_tokens_with_system (): 229"""Test token counting with system prompt""" 230server .start () 231 232res = server .make_request ("POST" ,"/v1/messages/count_tokens" ,data = { 233"model" :"test" , 234"system" :"You are a helpful assistant." , 235"messages" : [ 236 {"role" :"user" ,"content" :"Hello" } 237 ] 238 }) 239 240assert res .status_code == 200 241assert res .body ["input_tokens" ]> 0 242 243 244def test_anthropic_count_tokens_no_max_tokens (): 245"""Test that count_tokens doesn't require max_tokens""" 246server .start () 247 248# max_tokens is NOT required for count_tokens 249res = server .make_request ("POST" ,"/v1/messages/count_tokens" ,data = { 250"model" :"test" , 251"messages" : [ 252 {"role" :"user" ,"content" :"Hello" } 253 ] 254 }) 255 256assert res .status_code == 200 257assert "input_tokens" in res .body 258 259 260# Tool use tests 261 262def test_anthropic_tool_use_basic (): 263"""Test basic tool use""" 264server .jinja = True 265server .start () 266 267res = server .make_request ("POST" ,"/v1/messages" ,data = { 268"model" :"test" , 269"max_tokens" :200 , 270"tools" : [{ 271"name" :"get_weather" , 272"description" :"Get the current weather in a location" , 273"input_schema" : { 274"type" :"object" , 275"properties" : { 276"location" : { 277"type" :"string" , 278"description" :"City name" 279 } 280 }, 281"required" : ["location" ] 282 } 283 }], 284"messages" : [ 285 {"role" :"user" ,"content" :"What's the weather in Paris?" } 286 ] 287 }) 288 289assert res .status_code == 200 290assert res .body ["type" ]== "message" 291assert len (res .body ["content" ])> 0 292 293# Check if model used the tool (it might not always, depending on the model) 294content_types = [block .get ("type" )for block in res .body ["content" ]] 295 296if "tool_use" in content_types : 297# Model used the tool 298assert res .body ["stop_reason" ]== "tool_use" 299 300# Find the tool_use block 301tool_block = next (b for b in res .body ["content" ]if b .get ("type" )== "tool_use" ) 302assert "id" in tool_block 303assert "name" in tool_block 304assert tool_block ["name" ]== "get_weather" 305assert "input" in tool_block 306assert isinstance (tool_block ["input" ],dict ) 307 308 309def test_anthropic_tool_result (): 310"""Test sending tool results back 311 312This test verifies that tool_result blocks are properly converted to 313role="tool" messages internally. Without proper conversion, this would 314fail with a 500 error: "unsupported content[].type" because tool_result 315blocks would remain in the user message content array. 316""" 317server .jinja = True 318server .start () 319 320res = server .make_request ("POST" ,"/v1/messages" ,data = { 321"model" :"test" , 322"max_tokens" :100 , 323"messages" : [ 324 {"role" :"user" ,"content" :"What's the weather?" }, 325 { 326"role" :"assistant" , 327"content" : [ 328 { 329"type" :"tool_use" , 330"id" :"test123" , 331"name" :"get_weather" , 332"input" : {"location" :"Paris" } 333 } 334 ] 335 }, 336 { 337"role" :"user" , 338"content" : [ 339 { 340"type" :"tool_result" , 341"tool_use_id" :"test123" , 342"content" :"The weather is sunny, 25°C" 343 } 344 ] 345 } 346 ] 347 }) 348 349# This would be 500 with the old bug where tool_result blocks weren't converted 350assert res .status_code == 200 351assert res .body ["type" ]== "message" 352# Model should respond to the tool result 353assert len (res .body ["content" ])> 0 354assert res .body ["content" ][0 ]["type" ]== "text" 355 356 357def test_anthropic_tool_result_with_text (): 358"""Test tool result mixed with text content 359 360This tests the edge case where a user message contains both text and 361tool_result blocks. The server must properly split these into separate 362messages: a user message with text, followed by tool messages. 363Without proper handling, this would fail with 500: "unsupported content[].type" 364""" 365server .jinja = True 366server .start () 367 368res = server .make_request ("POST" ,"/v1/messages" ,data = { 369"model" :"test" , 370"max_tokens" :100 , 371"messages" : [ 372 {"role" :"user" ,"content" :"What's the weather?" }, 373 { 374"role" :"assistant" , 375"content" : [ 376 { 377"type" :"tool_use" , 378"id" :"tool_1" , 379"name" :"get_weather" , 380"input" : {"location" :"Paris" } 381 } 382 ] 383 }, 384 { 385"role" :"user" , 386"content" : [ 387 {"type" :"text" ,"text" :"Here are the results:" }, 388 { 389"type" :"tool_result" , 390"tool_use_id" :"tool_1" , 391"content" :"Sunny, 25°C" 392 } 393 ] 394 } 395 ] 396 }) 397 398assert res .status_code == 200 399assert res .body ["type" ]== "message" 400assert len (res .body ["content" ])> 0 401 402 403def test_anthropic_tool_result_with_image (): 404"""Test tool result containing mixed text and image blocks 405 406Verifies that image blocks inside Anthropic tool_result content are 407properly converted to OpenAI image_url format rather than being 408silently dropped. With a non-multimodal model, the converted image 409triggers a clear error message instead of being ignored. 410""" 411server .jinja = True 412server .start () 413 414# Small 1x1 red PNG image in base64 (same as vision tests) 415red_pixel_png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==" 416 417res = server .make_request ("POST" ,"/v1/messages" ,data = { 418"model" :"test" , 419"max_tokens" :100 , 420"messages" : [ 421 {"role" :"user" ,"content" :"What is in this image?" }, 422 { 423"role" :"assistant" , 424"content" : [ 425 { 426"type" :"tool_use" , 427"id" :"tool_1" , 428"name" :"read" , 429"input" : {"file" :"test.png" } 430 } 431 ] 432 }, 433 { 434"role" :"user" , 435"content" : [ 436 { 437"type" :"tool_result" , 438"tool_use_id" :"tool_1" , 439"content" : [ 440 {"type" :"text" ,"text" :"File: test.png" }, 441 { 442"type" :"image" , 443"source" : { 444"type" :"base64" , 445"media_type" :"image/png" , 446"data" :red_pixel_png 447 } 448 } 449 ] 450 } 451 ] 452 } 453 ] 454 }) 455 456# Without the fix, image block would cause "unsupported content[].type" 457# With the fix, image is converted to image_url but tinyllama doesn't support images 458assert res .status_code == 500 459assert "image input is not supported" in res .body .get ("error" , {}).get ("message" ,"" ).lower () 460 461 462def test_anthropic_tool_result_error (): 463"""Test tool result with error flag""" 464server .jinja = True 465server .start () 466 467res = server .make_request ("POST" ,"/v1/messages" ,data = { 468"model" :"test" , 469"max_tokens" :100 , 470"messages" : [ 471 {"role" :"user" ,"content" :"Get the weather" }, 472 { 473"role" :"assistant" , 474"content" : [ 475 { 476"type" :"tool_use" , 477"id" :"test123" , 478"name" :"get_weather" , 479"input" : {"location" :"InvalidCity" } 480 } 481 ] 482 }, 483 { 484"role" :"user" , 485"content" : [ 486 { 487"type" :"tool_result" , 488"tool_use_id" :"test123" , 489"is_error" :True , 490"content" :"City not found" 491 } 492 ] 493 } 494 ] 495 }) 496 497assert res .status_code == 200 498assert res .body ["type" ]== "message" 499 500 501def test_anthropic_tool_streaming (): 502"""Test streaming with tool use""" 503server .jinja = True 504server .start () 505 506res = server .make_stream_request ("POST" ,"/v1/messages" ,data = { 507"model" :"test" , 508"max_tokens" :200 , 509"stream" :True , 510"tools" : [{ 511"name" :"calculator" , 512"description" :"Calculate math" , 513"input_schema" : { 514"type" :"object" , 515"properties" : { 516"expression" : {"type" :"string" } 517 }, 518"required" : ["expression" ] 519 } 520 }], 521"messages" : [ 522 {"role" :"user" ,"content" :"Calculate 2+2" } 523 ] 524 }) 525 526events = [] 527for data in res : 528events .append (data ) 529 530event_types = [e ["type" ]for e in events ] 531 532# Should have basic events 533assert "message_start" in event_types 534assert "message_stop" in event_types 535 536# If tool was used, check for proper tool streaming 537if any (e .get ("type" )== "content_block_start" and 538e .get ("content_block" , {}).get ("type" )== "tool_use" 539for e in events ): 540# Find tool use block start 541tool_starts = [e for e in events if 542e .get ("type" )== "content_block_start" and 543e .get ("content_block" , {}).get ("type" )== "tool_use" ] 544 545assert len (tool_starts )> 0 ,"Should have tool_use content_block_start" 546 547# Check index is correct (should be 0 if no text, 1 if there's text) 548tool_start = tool_starts [0 ] 549assert "index" in tool_start 550assert tool_start ["content_block" ]["type" ]== "tool_use" 551assert "name" in tool_start ["content_block" ] 552 553 554# Vision/multimodal tests 555 556def test_anthropic_vision_format_accepted (): 557"""Test that Anthropic vision format is accepted (format validation only)""" 558server .start () 559 560# Small 1x1 red PNG image in base64 561red_pixel_png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==" 562 563res = server .make_request ("POST" ,"/v1/messages" ,data = { 564"model" :"test" , 565"max_tokens" :10 , 566"messages" : [ 567 { 568"role" :"user" , 569"content" : [ 570 { 571"type" :"image" , 572"source" : { 573"type" :"base64" , 574"media_type" :"image/png" , 575"data" :red_pixel_png 576 } 577 }, 578 { 579"type" :"text" , 580"text" :"What is this?" 581 } 582 ] 583 } 584 ] 585 }) 586 587# Server accepts the format but tinyllama doesn't support images 588# So it should return 500 with clear error message about missing mmproj 589assert res .status_code == 500 590assert "image input is not supported" in res .body .get ("error" , {}).get ("message" ,"" ).lower () 591 592 593def test_anthropic_vision_base64_with_multimodal_model (vision_server ): 594"""Test vision with base64 image using Anthropic format with multimodal model""" 595global server 596server = vision_server 597server .start () 598 599# Get test image in base64 format 600image_base64 = get_test_image_base64 () 601 602res = server .make_request ("POST" ,"/v1/messages" ,data = { 603"model" :"test" , 604"max_tokens" :10 , 605"messages" : [ 606 { 607"role" :"user" , 608"content" : [ 609 { 610"type" :"image" , 611"source" : { 612"type" :"base64" , 613"media_type" :"image/png" , 614"data" :image_base64 615 } 616 }, 617 { 618"type" :"text" , 619"text" :"What is this:\n" 620 } 621 ] 622 } 623 ] 624 }) 625 626assert res .status_code == 200 ,f"Expected 200, got { res . status_code } : { res . body } " 627assert res .body ["type" ]== "message" 628assert len (res .body ["content" ])> 0 629assert res .body ["content" ][0 ]["type" ]== "text" 630# The model should generate some response about the image 631assert len (res .body ["content" ][0 ]["text" ])> 0 632 633 634# Parameter tests 635 636def test_anthropic_stop_sequences (): 637"""Test stop_sequences parameter""" 638server .start () 639 640res = server .make_request ("POST" ,"/v1/messages" ,data = { 641"model" :"test" , 642"max_tokens" :100 , 643"stop_sequences" : ["\n" ,"END" ], 644"messages" : [ 645 {"role" :"user" ,"content" :"Count to 10" } 646 ] 647 }) 648 649assert res .status_code == 200 650assert res .body ["type" ]== "message" 651 652 653def test_anthropic_temperature (): 654"""Test temperature parameter""" 655server .start () 656 657res = server .make_request ("POST" ,"/v1/messages" ,data = { 658"model" :"test" , 659"max_tokens" :50 , 660"temperature" :0.5 , 661"messages" : [ 662 {"role" :"user" ,"content" :"Hello" } 663 ] 664 }) 665 666assert res .status_code == 200 667assert res .body ["type" ]== "message" 668 669 670def test_anthropic_top_p (): 671"""Test top_p parameter""" 672server .start () 673 674res = server .make_request ("POST" ,"/v1/messages" ,data = { 675"model" :"test" , 676"max_tokens" :50 , 677"top_p" :0.9 , 678"messages" : [ 679 {"role" :"user" ,"content" :"Hello" } 680 ] 681 }) 682 683assert res .status_code == 200 684assert res .body ["type" ]== "message" 685 686 687def test_anthropic_top_k (): 688"""Test top_k parameter (llama.cpp specific)""" 689server .start () 690 691res = server .make_request ("POST" ,"/v1/messages" ,data = { 692"model" :"test" , 693"max_tokens" :50 , 694"top_k" :40 , 695"messages" : [ 696 {"role" :"user" ,"content" :"Hello" } 697 ] 698 }) 699 700assert res .status_code == 200 701assert res .body ["type" ]== "message" 702 703 704# Error handling tests 705 706def test_anthropic_missing_messages (): 707"""Test error when messages are missing""" 708server .start () 709 710res = server .make_request ("POST" ,"/v1/messages" ,data = { 711"model" :"test" , 712"max_tokens" :50 713# missing "messages" field 714 }) 715 716# Should return an error (400 or 500) 717assert res .status_code >= 400 718 719 720def test_anthropic_empty_messages (): 721"""Test permissive handling of empty messages array""" 722server .start () 723 724res = server .make_request ("POST" ,"/v1/messages" ,data = { 725"model" :"test" , 726"max_tokens" :50 , 727"messages" : [] 728 }) 729 730# Server is permissive and accepts empty messages (provides defaults) 731# This matches the permissive validation design choice 732assert res .status_code == 200 733assert res .body ["type" ]== "message" 734 735 736# Content block index tests 737 738def test_anthropic_streaming_content_block_indices (): 739"""Test that content block indices are correct in streaming""" 740server .jinja = True 741server .start () 742 743# Request that might produce both text and tool use 744res = server .make_stream_request ("POST" ,"/v1/messages" ,data = { 745"model" :"test" , 746"max_tokens" :400 , 747"stream" :True , 748"tools" : [{ 749"name" :"test_tool" , 750"description" :"A test tool" , 751"input_schema" : { 752"type" :"object" , 753"properties" : { 754"param" : {"type" :"string" } 755 }, 756"required" : ["param" ] 757 } 758 }], 759"messages" : [ 760 {"role" :"user" ,"content" :"Use the test tool" } 761 ] 762 }) 763 764events = [] 765for data in res : 766events .append (data ) 767 768# Check content_block_start events have sequential indices 769block_starts = [e for e in events if e .get ("type" )== "content_block_start" ] 770if len (block_starts )> 1 : 771# If there are multiple blocks, indices should be sequential 772indices = [e ["index" ]for e in block_starts ] 773expected_indices = list (range (len (block_starts ))) 774assert indices == expected_indices ,f"Expected indices { expected_indices } , got { indices } " 775 776# Check content_block_stop events match the starts 777block_stops = [e for e in events if e .get ("type" )== "content_block_stop" ] 778start_indices = set (e ["index" ]for e in block_starts ) 779stop_indices = set (e ["index" ]for e in block_stops ) 780assert start_indices == stop_indices ,"content_block_stop indices should match content_block_start indices" 781 782 783# Extended features tests 784 785def test_anthropic_thinking (): 786"""Test extended thinking parameter""" 787server .jinja = True 788server .start () 789 790res = server .make_request ("POST" ,"/v1/messages" ,data = { 791"model" :"test" , 792"max_tokens" :100 , 793"thinking" : { 794"type" :"enabled" , 795"budget_tokens" :50 796 }, 797"messages" : [ 798 {"role" :"user" ,"content" :"What is 2+2?" } 799 ] 800 }) 801 802assert res .status_code == 200 803assert res .body ["type" ]== "message" 804 805 806def test_anthropic_metadata (): 807"""Test metadata parameter""" 808server .start () 809 810res = server .make_request ("POST" ,"/v1/messages" ,data = { 811"model" :"test" , 812"max_tokens" :50 , 813"metadata" : { 814"user_id" :"test_user_123" 815 }, 816"messages" : [ 817 {"role" :"user" ,"content" :"Hello" } 818 ] 819 }) 820 821assert res .status_code == 200 822assert res .body ["type" ]== "message" 823 824 825# Compatibility tests 826 827def test_anthropic_vs_openai_different_response_format (): 828"""Verify Anthropic format is different from OpenAI format""" 829server .start () 830 831# Make OpenAI request 832openai_res = server .make_request ("POST" ,"/v1/chat/completions" ,data = { 833"model" :"test" , 834"max_tokens" :50 , 835"messages" : [ 836 {"role" :"user" ,"content" :"Hello" } 837 ] 838 }) 839 840# Make Anthropic request 841anthropic_res = server .make_request ("POST" ,"/v1/messages" ,data = { 842"model" :"test" , 843"max_tokens" :50 , 844"messages" : [ 845 {"role" :"user" ,"content" :"Hello" } 846 ] 847 }) 848 849assert openai_res .status_code == 200 850assert anthropic_res .status_code == 200 851 852# OpenAI has "object", Anthropic has "type" 853assert "object" in openai_res .body 854assert "type" in anthropic_res .body 855assert openai_res .body ["object" ]== "chat.completion" 856assert anthropic_res .body ["type" ]== "message" 857 858# OpenAI has "choices", Anthropic has "content" 859assert "choices" in openai_res .body 860assert "content" in anthropic_res .body 861 862# Different usage field names 863assert "prompt_tokens" in openai_res .body ["usage" ] 864assert "input_tokens" in anthropic_res .body ["usage" ] 865assert "completion_tokens" in openai_res .body ["usage" ] 866assert "output_tokens" in anthropic_res .body ["usage" ] 867 868 869# Extended thinking tests with reasoning models 870 871# The next two tests cover the input path (conversation history): 872# Client sends thinking blocks -> convert_anthropic_to_oai -> reasoning_content -> template 873 874def test_anthropic_thinking_history_in_count_tokens (): 875"""Test that interleaved thinking blocks in conversation history are not dropped during conversion.""" 876global server 877server .jinja = True 878server .chat_template_file = '../../../models/templates/Qwen-Qwen3-0.6B.jinja' 879server .start () 880 881tool = { 882"name" :"list_files" , 883"description" :"List files" , 884"input_schema" : { 885"type" :"object" , 886"properties" : {"path" : {"type" :"string" }}, 887"required" : ["path" ] 888 } 889 } 890 891messages_without_thinking = [ 892 {"role" :"user" ,"content" :"Fix the bug" }, 893 { 894"role" :"assistant" , 895"content" : [ 896 {"type" :"tool_use" ,"id" :"call_1" ,"name" :"list_files" ,"input" : {"path" :"." }} 897 ] 898 }, 899 { 900"role" :"user" , 901"content" : [ 902 {"type" :"tool_result" ,"tool_use_id" :"call_1" ,"content" :"main.py" } 903 ] 904 }, 905 ] 906 907messages_with_thinking = [ 908 {"role" :"user" ,"content" :"Fix the bug" }, 909 { 910"role" :"assistant" , 911"content" : [ 912 {"type" :"thinking" ,"thinking" :"I should check the project structure first to understand the codebase layout." }, 913 {"type" :"tool_use" ,"id" :"call_1" ,"name" :"list_files" ,"input" : {"path" :"." }} 914 ] 915 }, 916 { 917"role" :"user" , 918"content" : [ 919 {"type" :"tool_result" ,"tool_use_id" :"call_1" ,"content" :"main.py" } 920 ] 921 }, 922 ] 923 924res_without = server .make_request ("POST" ,"/v1/messages/count_tokens" ,data = { 925"model" :"test" , 926"messages" :messages_without_thinking , 927"tools" : [tool ], 928 }) 929assert res_without .status_code == 200 ,f"Expected 200: { res_without . body } " 930 931res_with = server .make_request ("POST" ,"/v1/messages/count_tokens" ,data = { 932"model" :"test" , 933"messages" :messages_with_thinking , 934"tools" : [tool ], 935 }) 936assert res_with .status_code == 200 ,f"Expected 200: { res_with . body } " 937 938# Thinking blocks should increase the token count 939assert res_with .body ["input_tokens" ]> res_without .body ["input_tokens" ], \ 940f"Expected more tokens with thinking ( { res_with . body [ 'input_tokens' ] } ) than without ( { res_without . body [ 'input_tokens' ] } )" 941 942 943def test_anthropic_thinking_history_in_template (): 944"""Test that reasoning_content from converted interleaved thinking blocks renders in the prompt.""" 945global server 946server .jinja = True 947server .chat_template_file = '../../../models/templates/Qwen-Qwen3-0.6B.jinja' 948server .start () 949 950reasoning_1 = "I should check the project structure first." 951reasoning_2 = "Now I need to read the main file." 952 953res = server .make_request ("POST" ,"/apply-template" ,data = { 954"messages" : [ 955 {"role" :"user" ,"content" :"Fix the bug in main.py" }, 956 { 957"role" :"assistant" , 958"content" :"" , 959"reasoning_content" :reasoning_1 , 960"tool_calls" : [{ 961"id" :"call_1" , 962"type" :"function" , 963"function" : {"name" :"list_files" ,"arguments" :"{\"path\": \".\"}" } 964 }] 965 }, 966 {"role" :"tool" ,"tool_call_id" :"call_1" ,"content" :"main.py\nutils.py" }, 967 { 968"role" :"assistant" , 969"content" :"" , 970"reasoning_content" :reasoning_2 , 971"tool_calls" : [{ 972"id" :"call_2" , 973"type" :"function" , 974"function" : {"name" :"read_file" ,"arguments" :"{\"path\": \"main.py\"}" } 975 }] 976 }, 977 {"role" :"tool" ,"tool_call_id" :"call_2" ,"content" :"print('hello')" }, 978 ], 979"tools" : [{ 980"type" :"function" , 981"function" : { 982"name" :"list_files" , 983"description" :"List files" , 984"parameters" : {"type" :"object" ,"properties" : {"path" : {"type" :"string" }},"required" : ["path" ]} 985 } 986 }, { 987"type" :"function" , 988"function" : { 989"name" :"read_file" , 990"description" :"Read a file" , 991"parameters" : {"type" :"object" ,"properties" : {"path" : {"type" :"string" }},"required" : ["path" ]} 992 } 993 }], 994 }) 995assert res .status_code == 200 ,f"Expected 200, got { res . status_code } : { res . body } " 996prompt = res .body ["prompt" ] 997 998# Both reasoning_content values should be rendered in <think> tags 999assert reasoning_1 in prompt ,f"Expected first reasoning text in prompt: { prompt } " 1000assert reasoning_2 in prompt ,f"Expected second reasoning text in prompt: { prompt } " 1001assert prompt .count ("<think>" )>= 2 ,f"Expected at least 2 <think> blocks in prompt: { prompt } " 1002 1003 1004@ pytest . mark . slow 1005@ pytest . mark . parametrize ( "stream" , [ False , True ]) 1006def test_anthropic_thinking_with_reasoning_model (stream ): 1007"""Test that thinking content blocks are properly returned for reasoning models""" 1008global server 1009server = ServerProcess () 1010server .model_hf_repo = "bartowski/DeepSeek-R1-Distill-Qwen-7B-GGUF" 1011server .model_hf_file = "DeepSeek-R1-Distill-Qwen-7B-Q4_K_M.gguf" 1012server .reasoning_format = "deepseek" 1013server .jinja = True 1014server .n_ctx = 8192 1015server .n_predict = 1024 1016server .start (timeout_seconds = 600 )# large model needs time to download 1017 1018if stream : 1019res = server .make_stream_request ("POST" ,"/v1/messages" ,data = { 1020"model" :"test" , 1021"max_tokens" :1024 , 1022"thinking" : { 1023"type" :"enabled" , 1024"budget_tokens" :500 1025 }, 1026"messages" : [ 1027 {"role" :"user" ,"content" :"What is 2+2?" } 1028 ], 1029"stream" :True 1030 }) 1031 1032events = list (res ) 1033 1034# should have thinking content block events 1035thinking_starts = [e for e in events if 1036e .get ("type" )== "content_block_start" and 1037e .get ("content_block" , {}).get ("type" )== "thinking" ] 1038assert len (thinking_starts )> 0 ,"Should have thinking content_block_start event" 1039assert thinking_starts [0 ]["index" ]== 0 ,"Thinking block should be at index 0" 1040 1041# should have thinking_delta events 1042thinking_deltas = [e for e in events if 1043e .get ("type" )== "content_block_delta" and 1044e .get ("delta" , {}).get ("type" )== "thinking_delta" ] 1045assert len (thinking_deltas )> 0 ,"Should have thinking_delta events" 1046 1047# should have signature_delta event before thinking block closes (Anthropic API requirement) 1048signature_deltas = [e for e in events if 1049e .get ("type" )== "content_block_delta" and 1050e .get ("delta" , {}).get ("type" )== "signature_delta" ] 1051assert len (signature_deltas )> 0 ,"Should have signature_delta event for thinking block" 1052 1053# should have text block after thinking 1054text_starts = [e for e in events if 1055e .get ("type" )== "content_block_start" and 1056e .get ("content_block" , {}).get ("type" )== "text" ] 1057assert len (text_starts )> 0 ,"Should have text content_block_start event" 1058assert text_starts [0 ]["index" ]== 1 ,"Text block should be at index 1 (after thinking)" 1059else : 1060res = server .make_request ("POST" ,"/v1/messages" ,data = { 1061"model" :"test" , 1062"max_tokens" :1024 , 1063"thinking" : { 1064"type" :"enabled" , 1065"budget_tokens" :500 1066 }, 1067"messages" : [ 1068 {"role" :"user" ,"content" :"What is 2+2?" } 1069 ] 1070 }) 1071 1072assert res .status_code == 200 1073assert res .body ["type" ]== "message" 1074 1075content = res .body ["content" ] 1076assert len (content )>= 2 ,"Should have at least thinking and text blocks" 1077 1078# first block should be thinking 1079thinking_blocks = [b for b in content if b .get ("type" )== "thinking" ] 1080assert len (thinking_blocks )> 0 ,"Should have thinking content block" 1081assert "thinking" in thinking_blocks [0 ],"Thinking block should have 'thinking' field" 1082assert len (thinking_blocks [0 ]["thinking" ])> 0 ,"Thinking content should not be empty" 1083assert "signature" in thinking_blocks [0 ],"Thinking block should have 'signature' field (Anthropic API requirement)" 1084 1085# should also have text block 1086text_blocks = [b for b in content if b .get ("type" )== "text" ] 1087assert len (text_blocks )> 0 ,"Should have text content block"