renderers 0.1.13.dev13__tar.gz → 0.1.13.dev15__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/PKG-INFO +1 -1
  2. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/pyproject.toml +2 -0
  3. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/_version.py +2 -2
  4. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/muse_glimmer.py +44 -18
  5. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_muse_glimmer.py +30 -2
  6. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_tool_arg_type_preservation.py +14 -5
  7. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/uv.lock +4 -3
  8. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/.github/workflows/publish-dev.yml +0 -0
  9. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/.github/workflows/publish.yml +0 -0
  10. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/.github/workflows/style.yml +0 -0
  11. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/.github/workflows/test.yml +0 -0
  12. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/.gitignore +0 -0
  13. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/.pre-commit-config.yaml +0 -0
  14. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/LICENSE +0 -0
  15. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/README.md +0 -0
  16. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/docs/renderer-config.md +0 -0
  17. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/examples/README.md +0 -0
  18. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/examples/sglang/multiturn_generate_sglang.py +0 -0
  19. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/examples/sglang/online_multiturn_sglang.py +0 -0
  20. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/examples/tinker/multiturn_generate_tinker.py +0 -0
  21. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/examples/transformers/multiturn_generate_transformers.py +0 -0
  22. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/examples/vllm/multiturn_generate_vllm.py +0 -0
  23. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/__init__.py +0 -0
  24. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/base.py +0 -0
  25. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/client.py +0 -0
  26. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/configs.py +0 -0
  27. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/custom.py +0 -0
  28. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/deepseek_r1.py +0 -0
  29. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/deepseek_v3.py +0 -0
  30. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/deepseek_v4.py +0 -0
  31. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/default.py +0 -0
  32. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/gemma4.py +0 -0
  33. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/glm45.py +0 -0
  34. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/glm5.py +0 -0
  35. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/gpt_oss.py +0 -0
  36. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/hy3.py +0 -0
  37. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/inkling.py +0 -0
  38. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/kimi_k2.py +0 -0
  39. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/kimi_k25.py +0 -0
  40. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/laguna_s21.py +0 -0
  41. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/laguna_xs2.py +0 -0
  42. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/llama_3.py +0 -0
  43. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/minimax_m2.py +0 -0
  44. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/nemotron3.py +0 -0
  45. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/parsers.py +0 -0
  46. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/parsing.py +0 -0
  47. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/prime_qwen3.py +0 -0
  48. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/qwen3.py +0 -0
  49. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/qwen35.py +0 -0
  50. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/qwen36.py +0 -0
  51. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/qwen38.py +0 -0
  52. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/qwen3_vl.py +0 -0
  53. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/reasoning.py +0 -0
  54. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/renderers/tagged.py +0 -0
  55. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/audit_prefix_stability.py +0 -0
  56. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/conftest.py +0 -0
  57. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/fixtures/prefix_stability.json +0 -0
  58. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/parity.py +0 -0
  59. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/reference_continuations.py +0 -0
  60. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/reference_rendering.py +0 -0
  61. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/replay.py +0 -0
  62. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_bridge.py +0 -0
  63. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_build_helpers.py +0 -0
  64. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_client.py +0 -0
  65. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_custom.py +0 -0
  66. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_deepseek_r1.py +0 -0
  67. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_deepseek_v4.py +0 -0
  68. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_disabled_thinking_stability.py +0 -0
  69. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_gemma4.py +0 -0
  70. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_glm53_flash.py +0 -0
  71. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_glm_tool_name_validation.py +0 -0
  72. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_hy3.py +0 -0
  73. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_incremental.py +0 -0
  74. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_inkling.py +0 -0
  75. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_is_content.py +0 -0
  76. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_kimi_k25_tool_schema.py +0 -0
  77. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_laguna_m1.py +0 -0
  78. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_laguna_xs21.py +0 -0
  79. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_llama_3.py +0 -0
  80. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_load_tokenizer.py +0 -0
  81. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_message_indices.py +0 -0
  82. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_message_tool_names.py +0 -0
  83. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_multimodal.py +0 -0
  84. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_nemotron35_vlm.py +0 -0
  85. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_nemotron3_ultra.py +0 -0
  86. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_offsetless_tokenizers.py +0 -0
  87. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_parity.py +0 -0
  88. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_parse_response.py +0 -0
  89. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_parse_response_robustness.py +0 -0
  90. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_parsers.py +0 -0
  91. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_preserve_thinking.py +0 -0
  92. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_qwen35_size_coverage.py +0 -0
  93. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_qwen38.py +0 -0
  94. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_reasoning_boundaries.py +0 -0
  95. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_renderer_config.py +0 -0
  96. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_roundtrip.py +0 -0
  97. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_sampled_mask.py +0 -0
  98. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_tagged_rollout.py +0 -0
  99. {renderers-0.1.13.dev13 → renderers-0.1.13.dev15}/tests/test_tokens_per_message.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: renderers
3
- Version: 0.1.13.dev13
3
+ Version: 0.1.13.dev15
4
4
  Summary: Chat template renderers — deterministic message-to-token conversion for LLM training
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -121,6 +121,8 @@ exclude-newer = "7 days"
121
121
  # fast-track so first-party releases can land same-day. Only packages that
122
122
  # appear in `uv tree` are listed.
123
123
  prime-pydantic-config = false
124
+ # CA bundle: security fixes (e.g. 2026.10.19 for the eMudhra compromise) must not wait out the cooldown
125
+ certifi = false
124
126
 
125
127
  [tool.ty.environment]
126
128
  python-version = "3.13"
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.1.13.dev13'
22
- __version_tuple__ = version_tuple = (0, 1, 13, 'dev13')
21
+ __version__ = version = '0.1.13.dev15'
22
+ __version_tuple__ = version_tuple = (0, 1, 13, 'dev15')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -51,7 +51,6 @@ from renderers.base import (
51
51
  should_rerender_for_thinking_retention,
52
52
  )
53
53
  from renderers.configs import MuseGlimmerRendererConfig
54
- from renderers.parsing import _build_param_type_index, _coerce_arg_value
55
54
 
56
55
  _IMAGE_TYPES = ("image", "image_url")
57
56
  _UNSUPPORTED_MEDIA_TYPES = ("video", "video_url", "audio", "audio_url", "input_audio")
@@ -104,6 +103,15 @@ _PARAM_RE = re.compile(
104
103
  r'<atem:parameter\b[^>]*?\bname="(?P<key>[^"]+)"[^>]*?>(?P<value>.*?)</atem:parameter>',
105
104
  re.DOTALL,
106
105
  )
106
+ _STRIP_REASONING_RE = re.compile(
107
+ r"(?:<\|start\|>assistant\s*)?to=self<\|message\|>.*?<\|eom\|>", re.DOTALL
108
+ )
109
+ _STRIP_OPEN_REASONING_RE = re.compile(
110
+ r"(?:<\|start\|>assistant\s*)?to=self<\|message\|>"
111
+ r"(?:(?!<\|eom\|>)(?!to=[^\s<]+<\|message\|>).)*"
112
+ r"(?=to=[^\s<]+<\|message\|>|$)",
113
+ re.DOTALL,
114
+ )
107
115
 
108
116
 
109
117
  def _json(value: Any) -> str:
@@ -723,23 +731,42 @@ class MuseGlimmerRenderer:
723
731
  reasoning = (
724
732
  "\n".join(p for p in reasoning_parts if p) if reasoning_parts else None
725
733
  )
726
- param_types = _build_param_type_index(tools)
734
+ registered = {_function(tool)["name"] for tool in tools or []}
727
735
  calls = [
728
736
  c
729
737
  for recipient, body, closed in tool_bodies
730
- for c in self._parse_calls(recipient, body, param_types)
738
+ for c in self._parse_calls(body, registered)
731
739
  ]
740
+ content = "".join(content_parts)
741
+ if (
742
+ messages
743
+ and messages[-1][0] == "user"
744
+ and not messages[-1][2]
745
+ and not any(call.name for call in calls)
746
+ and content.endswith("\n")
747
+ and "<atem:invoke" not in content
748
+ and "<atem:function_calls>" not in content
749
+ ):
750
+ # vLLM's non-streaming content regex ends at `$`, before one final LF.
751
+ content = content[:-1]
752
+ if tool_bodies and not any(call.name for call in calls):
753
+ # Stock vLLM preserves malformed tool markup as content when no
754
+ # complete invocation can be dispatched; it does not invent a call.
755
+ remainder = _STRIP_REASONING_RE.sub("", text)
756
+ remainder = _STRIP_OPEN_REASONING_RE.sub("", remainder)
757
+ if "<atem:invoke" in remainder or "<atem:function_calls>" in remainder:
758
+ content = remainder
759
+ elif not content:
760
+ content = "" if "to=self<|message|>" in text else text
732
761
  return ParsedResponse(
733
- content="".join(content_parts),
762
+ content=content,
734
763
  reasoning_content=reasoning,
735
764
  tool_calls=calls,
736
765
  reasoning_complete=reasoning_complete,
737
766
  )
738
767
 
739
768
  @staticmethod
740
- def _parse_calls(
741
- recipient: str, body: str, param_types: dict[str, dict[str, dict[str, Any]]]
742
- ) -> list[ParsedToolCall]:
769
+ def _parse_calls(body: str, registered: set[str]) -> list[ParsedToolCall]:
743
770
  """Every ``<atem:invoke>`` in one tool-channel message."""
744
771
  calls: list[ParsedToolCall] = []
745
772
  last_end = 0
@@ -754,23 +781,24 @@ class MuseGlimmerRenderer:
754
781
  )
755
782
  )
756
783
  continue
757
- schema = param_types.get(name, {})
784
+ if name not in registered:
785
+ head, sep, tail = name.partition(".")
786
+ if sep and head == tail and head in registered:
787
+ name = head
758
788
  arguments: dict[str, Any] = {}
759
- fallback = False
760
789
  for p in _PARAM_RE.finditer(m.group("body")):
761
- value, used = _coerce_arg_value(
762
- p.group("value"), schema.get(p.group("key"))
763
- )
790
+ value = p.group("value")
791
+ try:
792
+ value = json.loads(value)
793
+ except (json.JSONDecodeError, ValueError):
794
+ pass
764
795
  arguments[p.group("key")] = value
765
- fallback = fallback or used
766
796
  calls.append(
767
797
  ParsedToolCall(
768
798
  raw=m.group(0),
769
799
  name=name,
770
800
  arguments=arguments,
771
- status=ToolCallParseStatus.INVALID_JSON
772
- if fallback
773
- else ToolCallParseStatus.OK,
801
+ status=ToolCallParseStatus.OK,
774
802
  )
775
803
  )
776
804
  rest = body[last_end:]
@@ -779,7 +807,6 @@ class MuseGlimmerRenderer:
779
807
  calls.append(
780
808
  ParsedToolCall(
781
809
  raw=rest.strip(),
782
- name=recipient,
783
810
  status=ToolCallParseStatus.UNCLOSED_BLOCK,
784
811
  )
785
812
  )
@@ -787,7 +814,6 @@ class MuseGlimmerRenderer:
787
814
  calls.append(
788
815
  ParsedToolCall(
789
816
  raw=body,
790
- name=recipient,
791
817
  status=ToolCallParseStatus.MALFORMED_STRUCTURE,
792
818
  )
793
819
  )
@@ -17,7 +17,6 @@ from datetime import datetime
17
17
  from functools import lru_cache
18
18
 
19
19
  import pytest
20
-
21
20
  from renderers.base import ToolCallParseStatus
22
21
 
23
22
  MODEL = "meta-models/Muse-Glimmer-30B"
@@ -365,6 +364,22 @@ _PARSE_CASES = {
365
364
  ("Let me think.", "The answer is 4.", []),
366
365
  ),
367
366
  "answer-only": (" to=user<|message|>Hello there!", (None, "Hello there!", [])),
367
+ "answer-final-newline": (" to=user<|message|>Hello\n", (None, "Hello", [])),
368
+ "answer-final-two-newlines": (
369
+ " to=user<|message|>Hello\n\n",
370
+ (None, "Hello\n", []),
371
+ ),
372
+ "answer-final-space-newline": (" to=user<|message|>Hello \n", (None, "Hello ", [])),
373
+ "closed-answer-keeps-newline": (
374
+ " to=user<|message|>Hello\n<|eom|>",
375
+ (None, "Hello\n", []),
376
+ ),
377
+ "tool-and-answer-keeps-newline": (
378
+ " to=get_weather<|message|>"
379
+ + _invoke("get_weather", ("city", "Paris"))
380
+ + "<|eom|><|start|>assistant to=user<|message|>Hello\n",
381
+ (None, "Hello\n", [("get_weather", {"city": "Paris"})]),
382
+ ),
368
383
  "tool-call": (
369
384
  " to=self<|message|>Need weather.<|eom|><|start|>assistant to=get_weather<|message|>"
370
385
  + _invoke(
@@ -413,6 +428,16 @@ _PARSE_CASES = {
413
428
  "<|start|>assistant to=user<|message|>Done.",
414
429
  ("First.\nSecond.", "Done.", []),
415
430
  ),
431
+ "doubled-bare-name": (
432
+ " to=get_weather<|message|>"
433
+ + _invoke("get_weather.get_weather", ("city", "Paris")),
434
+ (None, "", [("get_weather", {"city": "Paris"})]),
435
+ ),
436
+ "string-schema-does-not-override-scalar-decoding": (
437
+ " to=get_weather<|message|>"
438
+ + _invoke("get_weather", ("city", "123"), ("code", "false")),
439
+ (None, "", [("get_weather", {"city": 123, "code": False})]),
440
+ ),
416
441
  }
417
442
 
418
443
 
@@ -520,14 +545,17 @@ def test_unfinished_tool_call_is_unclosed():
520
545
  )
521
546
  )
522
547
  assert [c.status for c in parsed.tool_calls] == [ToolCallParseStatus.UNCLOSED_BLOCK]
548
+ assert parsed.tool_calls[0].name is None
549
+ assert '<atem:invoke name="get_weather">' in parsed.content
523
550
 
524
551
 
525
552
  def test_tool_channel_without_invoke_is_malformed():
526
553
  _, renderer = _load()
527
554
  parsed = renderer.parse_response(_ids(" to=get_weather<|message|>Paris please"))
528
555
  assert [(c.name, c.status) for c in parsed.tool_calls] == [
529
- ("get_weather", ToolCallParseStatus.MALFORMED_STRUCTURE)
556
+ (None, ToolCallParseStatus.MALFORMED_STRUCTURE)
530
557
  ]
558
+ assert parsed.content == " to=get_weather<|message|>Paris please"
531
559
 
532
560
 
533
561
  def test_prefilled_reasoning_prompt_continues_reasoning():
@@ -13,6 +13,9 @@ reference parsers (e.g. ``vllm/glm45_tool_parser.py``). The hermes-JSON
13
13
  (Qwen3) and section-JSON (Kimi K2) parsers sidestep the bug because
14
14
  their wire format quotes strings; both serve as controls.
15
15
 
16
+ Muse Glimmer follows vLLM's schema-independent JSON decoding instead, so
17
+ its cases assert decoded values even when the schema declares a string.
18
+
16
19
  Originally raised by Robin (Poolside) on PR #21.
17
20
  """
18
21
 
@@ -25,6 +28,8 @@ from typing import Any
25
28
  import pytest
26
29
  from parity import models_for
27
30
 
31
+ from renderers.muse_glimmer import MuseGlimmerRenderer
32
+
28
33
 
29
34
  # (HuggingFace model name, renderer name). Two JSON-shaped controls
30
35
  # (string types already preserved by the wire format) + five XML-style
@@ -110,9 +115,8 @@ def _extract_assistant_tokens(renderer, prompt, assistant_msg, *, tools=None):
110
115
 
111
116
 
112
117
  @pytest.mark.parametrize("args", JSON_LOOKING_STRING_ARGS)
113
- def test_string_arg_preserves_type(model, renderer_name, renderer, args):
114
- """Tool-call args of declared type ``str`` must round-trip as ``str``,
115
- not get re-parsed as bool/int/null/list/dict by the parser."""
118
+ def test_string_arg_follows_parser_contract(model, renderer_name, renderer, args):
119
+ """Preserve declared strings unless the reference parser decodes JSON."""
116
120
  msg = {
117
121
  "role": "assistant",
118
122
  "content": "",
@@ -128,8 +132,13 @@ def test_string_arg_preserves_type(model, renderer_name, renderer, args):
128
132
 
129
133
  assert parsed.tool_calls, f"{model}: parser returned no tool_calls"
130
134
  got = _normalize_args(parsed.tool_calls[0].arguments)
131
- assert got == args, (
132
- f"{model}: tool-arg type drift — sent {args!r}, parser returned {got!r}"
135
+ expected = (
136
+ {key: json.loads(value) for key, value in args.items()}
137
+ if isinstance(renderer, MuseGlimmerRenderer)
138
+ else args
139
+ )
140
+ assert got == expected, (
141
+ f"{model}: tool-arg type drift — expected {expected!r}, parser returned {got!r}"
133
142
  )
134
143
 
135
144
 
@@ -7,6 +7,7 @@ exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for
7
7
  exclude-newer-span = "P7D"
8
8
 
9
9
  [options.exclude-newer-package]
10
+ certifi = false
10
11
  prime-pydantic-config = false
11
12
 
12
13
  [[package]]
@@ -42,11 +43,11 @@ wheels = [
42
43
 
43
44
  [[package]]
44
45
  name = "certifi"
45
- version = "2026.4.22"
46
+ version = "2026.10.19"
46
47
  source = { registry = "https://pypi.org/simple" }
47
- sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" }
48
+ sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/1436c50cc46699de0230fbaacc1d1de7bfec413176099949e9d5112e88f2/certifi-2026.10.19.tar.gz", hash = "sha256:035c1e85572b141bf1c3a3eff0e33a8eb74917522f2559075ff8d2fea780a1ae", size = 137598, upload-time = "2026-10-20T02:24:16.604Z" }
48
49
  wheels = [
49
- { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
50
+ { url = "https://files.pythonhosted.org/packages/c4/51/29046e48f693c54e5b7412216d447a1d8362c9653148d98c02cdf65fa733/certifi-2026.10.19-py3-none-any.whl", hash = "sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b", size = 136464, upload-time = "2026-10-20T02:24:15.072Z" },
50
51
  ]
51
52
 
52
53
  [[package]]