run.sh 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. #!/usr/bin/env bash
  2. # Smoketests for glm53. See lib.sh for the conventions.
  3. #
  4. # These test what clients rely on, not raw capability. The one that matters
  5. # most here is fluency-vs-correctness: a TP/shard/quantisation mismatch on this
  6. # model loads cleanly, reports healthy and serves fluent nonsense, so every
  7. # case asks for an answer that can be checked.
  8. source "$(dirname "$0")/lib.sh" "$@"
  9. get() { # PATH -> STATUS, BODY, SECS
  10. local out
  11. out=$(curl -s --max-time "$TIMEOUT" -w '\n%{http_code} %{time_total}' "$BASE$1")
  12. BODY="${out%$'\n'*}"; read -r STATUS SECS <<<"${out##*$'\n'}"
  13. }
  14. chat() { # JSON-extra MAX_TOKENS USER-TEXT -> request body
  15. jq -n --arg m "$NAME" --argjson x "$1" --argjson n "$2" --arg q "$3" \
  16. '{model:$m, max_tokens:$n, temperature:0, messages:[{role:"user", content:$q}]} + $x'
  17. }
  18. OFF='{"chat_template_kwargs":{"enable_thinking":false}}'
  19. # The served name alone proves nothing: any server can be told to answer to
  20. # glm53. The root is the path the engine loaded from, where this compose mounts
  21. # the checkpoint, so this fails against another model even when every other
  22. # case would pass. It passes through mentat-serve unchanged.
  23. t_models_lists_glm_checkpoint() {
  24. get /v1/models
  25. check "should list $NAME" "[.data[].id] | index(\"$NAME\") != null" || return
  26. check "$NAME should be a GLM-5.3 checkpoint" \
  27. ".data[] | select(.id == \"$NAME\") | .root | test(\"glm-5\\\\.3\"; \"i\")" || return
  28. ok "$(jqb --arg n "$NAME" '.data[] | select(.id == $n) | .root')"
  29. }
  30. # Thinking is on by default: a correct answer, with the trace in the reasoning
  31. # field rather than in the content.
  32. t_thinking_on_by_default() {
  33. post /v1/chat/completions "$(chat '{}' 2048 'What is 17*23? Reply with the number only.')"
  34. check "should finish" '.choices[0].finish_reason == "stop"' || return
  35. check "content should be 391" '.choices[0].message.content | test("391")' || return
  36. check "reasoning should be present" '(.choices[0].message.reasoning // .choices[0].message.reasoning_content // "") | length > 0' || return
  37. ok "$(jqb '.usage.completion_tokens') tok"
  38. }
  39. # Off per request. The model has no real non-thinking mode (an empty
  40. # <think></think> breaks long output), so off means `Reasoning Effort: Low`: a
  41. # short trace in the reasoning field and a bare answer. Before the parser patch
  42. # that trace landed in the content, so the content must be short and clean.
  43. t_thinking_off_per_request() {
  44. post /v1/chat/completions "$(chat "$OFF" 512 'What is 17*23? Reply with the number only.')"
  45. check "content should be 391" '.choices[0].message.content | test("391")' || return
  46. check "content should be short, not a trace" '.choices[0].message.content | length < 40' || return
  47. check "content should hold no think tags" '.choices[0].message.content | test("think>") | not' || return
  48. check "reasoning should be short (low effort)" '(.choices[0].message.reasoning // .choices[0].message.reasoning_content // "") | length < 600' || return
  49. ok "$(jqb '.usage.completion_tokens') tok"
  50. }
  51. # A known fact, as the in-image self-test asks. Corrupted weights answer
  52. # fluently and wrongly.
  53. t_fact() {
  54. post /v1/chat/completions "$(chat "$OFF" 512 'What is the capital of Australia? One word.')"
  55. check "should answer Canberra" '.choices[0].message.content | test("Canberra")' || return
  56. ok
  57. }
  58. # The tool parser: a well-formed call to an offered tool must come back parsed.
  59. # The fail-closed parser (TOOL_PARSER=glm47_failclosed) was once silently
  60. # dropped from production by a compose override.
  61. t_tool_call_parsed() {
  62. post /v1/chat/completions "$(jq -n --arg m "$NAME" '{
  63. model:$m, max_tokens:1024, temperature:0, tool_choice:"auto",
  64. chat_template_kwargs:{enable_thinking:false},
  65. tools:[{type:"function", function:{name:"get_weather", description:"Get the current weather for a city.",
  66. parameters:{type:"object", properties:{city:{type:"string"}}, required:["city"]}}}],
  67. messages:[{role:"user", content:"What is the weather in Paris right now? Use the tool."}]}')"
  68. check "should call get_weather" '.choices[0].message.tool_calls[0].function.name == "get_weather"' || return
  69. check "arguments should name Paris" '.choices[0].message.tool_calls[0].function.arguments | fromjson | .city | test("Paris")' || return
  70. ok
  71. }
  72. # Multimodal. A text-only chat template once made images fail here while text
  73. # kept working.
  74. t_image_question() {
  75. post /v1/chat/completions "$(jq -n --arg m "$NAME" --rawfile b64 <(base64 < "$HERE/page-table.png" | tr -d '\n') '{
  76. model:$m, max_tokens:64, temperature:0, chat_template_kwargs:{enable_thinking:false},
  77. messages:[{role:"user", content:[
  78. {type:"image_url", image_url:{url:("data:image/png;base64," + $b64)}},
  79. {type:"text", text:"In the table on this page, what is the Q4 value for the North region? Answer with the number only."}]}]}')"
  80. check "should read 1450 from the table" '.choices[0].message.content | test("1450")' || return
  81. ok
  82. }
  83. t_max_tokens_exact() {
  84. post /v1/chat/completions "$(chat "$OFF" 5 'Count from one to fifty in words.')"
  85. check "should stop on length" '.choices[0].finish_reason == "length"' || return
  86. check "should emit exactly 5 tokens" '.usage.completion_tokens == 5' || return
  87. ok
  88. }
  89. # The limit comes from the server, so this follows a MAX_MODEL_LEN change.
  90. t_context_limit_refused() {
  91. post /tokenize "$(jq -n --arg m "$NAME" '{model:$m, prompt:"x"}')"
  92. local max; max=$(jqb '.max_model_len')
  93. post /v1/chat/completions "$(chat "$OFF" "$max" 'Say hi.')"
  94. [ "$STATUS" = 400 ] || { BODY="{\"status\": $STATUS}"; check "status should be 400" 'false'; return; }
  95. check "error should name the context length" '(.error.message // .message) | test("context length")' || return
  96. ok "limit $max"
  97. }
  98. run_all