clams-python 1.7.1__tar.gz → 1.7.3__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 (102) hide show
  1. {clams_python-1.7.1 → clams_python-1.7.3}/CHANGELOG.md +17 -0
  2. {clams_python-1.7.1/clams_python.egg-info → clams_python-1.7.3}/PKG-INFO +2 -2
  3. {clams_python-1.7.1 → clams_python-1.7.3}/clams/app/__init__.py +176 -15
  4. {clams_python-1.7.1 → clams_python-1.7.3}/clams/develop/templates/app/cli.py.template +21 -4
  5. {clams_python-1.7.1 → clams_python-1.7.3/clams_python.egg-info}/PKG-INFO +2 -2
  6. {clams_python-1.7.1 → clams_python-1.7.3}/clams_python.egg-info/SOURCES.txt +3 -0
  7. {clams_python-1.7.1 → clams_python-1.7.3}/clams_python.egg-info/requires.txt +1 -1
  8. clams_python-1.7.3/clams_python.egg-info/scm_file_list.json +96 -0
  9. clams_python-1.7.3/clams_python.egg-info/scm_version.json +8 -0
  10. {clams_python-1.7.1 → clams_python-1.7.3}/documentation/app-baseclasses.rst +61 -1
  11. clams_python-1.7.3/documentation/autodoc/clams.backends.rst +15 -0
  12. {clams_python-1.7.1 → clams_python-1.7.3}/documentation/conf.py +3 -1
  13. {clams_python-1.7.1 → clams_python-1.7.3}/documentation/target-versions.csv +2 -0
  14. {clams_python-1.7.1 → clams_python-1.7.3}/pyproject.toml +1 -1
  15. {clams_python-1.7.1 → clams_python-1.7.3}/tests/test_promptable.py +156 -14
  16. {clams_python-1.7.1 → clams_python-1.7.3}/.github/workflows/codecov.yml +0 -0
  17. {clams_python-1.7.1 → clams_python-1.7.3}/.github/workflows/container.yml +0 -0
  18. {clams_python-1.7.1 → clams_python-1.7.3}/.github/workflows/containers.yml +0 -0
  19. {clams_python-1.7.1 → clams_python-1.7.3}/.github/workflows/issue-assign.yml +0 -0
  20. {clams_python-1.7.1 → clams_python-1.7.3}/.github/workflows/issue-close.yml +0 -0
  21. {clams_python-1.7.1 → clams_python-1.7.3}/.github/workflows/issue-infra-project.yml +0 -0
  22. {clams_python-1.7.1 → clams_python-1.7.3}/.github/workflows/publish.yml +0 -0
  23. {clams_python-1.7.1 → clams_python-1.7.3}/.github/workflows/release-check.yml +0 -0
  24. {clams_python-1.7.1 → clams_python-1.7.3}/.gitignore +0 -0
  25. {clams_python-1.7.1 → clams_python-1.7.3}/.pytype.cfg +0 -0
  26. {clams_python-1.7.1 → clams_python-1.7.3}/CONTRIBUTING.md +0 -0
  27. {clams_python-1.7.1 → clams_python-1.7.3}/LICENSE +0 -0
  28. {clams_python-1.7.1 → clams_python-1.7.3}/README.md +0 -0
  29. {clams_python-1.7.1 → clams_python-1.7.3}/build-tools/build.py +0 -0
  30. {clams_python-1.7.1 → clams_python-1.7.3}/build-tools/clean.py +0 -0
  31. {clams_python-1.7.1 → clams_python-1.7.3}/build-tools/docs.py +0 -0
  32. {clams_python-1.7.1 → clams_python-1.7.3}/build-tools/prep_release.py +0 -0
  33. {clams_python-1.7.1 → clams_python-1.7.3}/build-tools/publish.py +0 -0
  34. {clams_python-1.7.1 → clams_python-1.7.3}/build-tools/test.py +0 -0
  35. {clams_python-1.7.1 → clams_python-1.7.3}/clams/__init__.py +0 -0
  36. {clams_python-1.7.1 → clams_python-1.7.3}/clams/appmetadata/__init__.py +0 -0
  37. {clams_python-1.7.1 → clams_python-1.7.3}/clams/backends/__init__.py +0 -0
  38. {clams_python-1.7.1 → clams_python-1.7.3}/clams/backends/hf.py +0 -0
  39. {clams_python-1.7.1 → clams_python-1.7.3}/clams/develop/__init__.py +0 -0
  40. {clams_python-1.7.1 → clams_python-1.7.3}/clams/develop/templates/app/.dockerignore.template +0 -0
  41. {clams_python-1.7.1 → clams_python-1.7.3}/clams/develop/templates/app/.gitignore.template +0 -0
  42. {clams_python-1.7.1 → clams_python-1.7.3}/clams/develop/templates/app/Containerfile.template +0 -0
  43. {clams_python-1.7.1 → clams_python-1.7.3}/clams/develop/templates/app/LICENSE.template +0 -0
  44. {clams_python-1.7.1 → clams_python-1.7.3}/clams/develop/templates/app/README.md.template +0 -0
  45. {clams_python-1.7.1 → clams_python-1.7.3}/clams/develop/templates/app/app.py.template +0 -0
  46. {clams_python-1.7.1 → clams_python-1.7.3}/clams/develop/templates/app/metadata.py.template +0 -0
  47. {clams_python-1.7.1 → clams_python-1.7.3}/clams/develop/templates/app/requirements.txt.template +0 -0
  48. {clams_python-1.7.1 → clams_python-1.7.3}/clams/develop/templates/gha/for-clams-team.md.template +0 -0
  49. {clams_python-1.7.1 → clams_python-1.7.3}/clams/develop/templates/gha/workflows/issue-apps-project.yml.template +0 -0
  50. {clams_python-1.7.1 → clams_python-1.7.3}/clams/develop/templates/gha/workflows/issue-assign.yml.template +0 -0
  51. {clams_python-1.7.1 → clams_python-1.7.3}/clams/develop/templates/gha/workflows/issue-close.yml.template +0 -0
  52. {clams_python-1.7.1 → clams_python-1.7.3}/clams/develop/templates/gha/workflows/publish.yml.template +0 -0
  53. {clams_python-1.7.1 → clams_python-1.7.3}/clams/develop/templates/utl-tf/__init__.py.template +0 -0
  54. {clams_python-1.7.1 → clams_python-1.7.3}/clams/develop/templates/utl-tf/timeframe.py.template +0 -0
  55. {clams_python-1.7.1 → clams_python-1.7.3}/clams/envelop/__init__.py +0 -0
  56. {clams_python-1.7.1 → clams_python-1.7.3}/clams/restify/__init__.py +0 -0
  57. {clams_python-1.7.1 → clams_python-1.7.3}/clams/serve/__init__.py +0 -0
  58. {clams_python-1.7.1 → clams_python-1.7.3}/clams/ver/__init__.py +0 -0
  59. {clams_python-1.7.1 → clams_python-1.7.3}/clams_python.egg-info/dependency_links.txt +0 -0
  60. {clams_python-1.7.1 → clams_python-1.7.3}/clams_python.egg-info/entry_points.txt +0 -0
  61. {clams_python-1.7.1 → clams_python-1.7.3}/clams_python.egg-info/top_level.txt +0 -0
  62. {clams_python-1.7.1 → clams_python-1.7.3}/container/Containerfile +0 -0
  63. {clams_python-1.7.1 → clams_python-1.7.3}/container/ffmpeg-hf.containerfile +0 -0
  64. {clams_python-1.7.1 → clams_python-1.7.3}/container/ffmpeg-hf4.containerfile +0 -0
  65. {clams_python-1.7.1 → clams_python-1.7.3}/container/ffmpeg-hf5.containerfile +0 -0
  66. {clams_python-1.7.1 → clams_python-1.7.3}/container/ffmpeg-tf2.containerfile +0 -0
  67. {clams_python-1.7.1 → clams_python-1.7.3}/container/ffmpeg-torch2.containerfile +0 -0
  68. {clams_python-1.7.1 → clams_python-1.7.3}/container/ffmpeg.containerfile +0 -0
  69. {clams_python-1.7.1 → clams_python-1.7.3}/container/generate_containers_yml.py +0 -0
  70. {clams_python-1.7.1 → clams_python-1.7.3}/container/hf.containerfile +0 -0
  71. {clams_python-1.7.1 → clams_python-1.7.3}/container/hf4.containerfile +0 -0
  72. {clams_python-1.7.1 → clams_python-1.7.3}/container/hf5.containerfile +0 -0
  73. {clams_python-1.7.1 → clams_python-1.7.3}/container/jdk8.containerfile +0 -0
  74. {clams_python-1.7.1 → clams_python-1.7.3}/container/opencv4-hf.containerfile +0 -0
  75. {clams_python-1.7.1 → clams_python-1.7.3}/container/opencv4-hf4.containerfile +0 -0
  76. {clams_python-1.7.1 → clams_python-1.7.3}/container/opencv4-hf5.containerfile +0 -0
  77. {clams_python-1.7.1 → clams_python-1.7.3}/container/opencv4-tf2.containerfile +0 -0
  78. {clams_python-1.7.1 → clams_python-1.7.3}/container/opencv4-torch2.containerfile +0 -0
  79. {clams_python-1.7.1 → clams_python-1.7.3}/container/opencv4.containerfile +0 -0
  80. {clams_python-1.7.1 → clams_python-1.7.3}/container/tf2.containerfile +0 -0
  81. {clams_python-1.7.1 → clams_python-1.7.3}/container/torch2.containerfile +0 -0
  82. {clams_python-1.7.1 → clams_python-1.7.3}/documentation/appdirectory.rst +0 -0
  83. {clams_python-1.7.1 → clams_python-1.7.3}/documentation/appmetadata.rst +0 -0
  84. {clams_python-1.7.1 → clams_python-1.7.3}/documentation/autodoc/clams.app.rst +0 -0
  85. {clams_python-1.7.1 → clams_python-1.7.3}/documentation/autodoc/clams.appmetadata.rst +0 -0
  86. {clams_python-1.7.1 → clams_python-1.7.3}/documentation/autodoc/clams.mmif_utils.rst +0 -0
  87. {clams_python-1.7.1 → clams_python-1.7.3}/documentation/autodoc/clams.restify.rst +0 -0
  88. {clams_python-1.7.1 → clams_python-1.7.3}/documentation/clamsapp.rst +0 -0
  89. {clams_python-1.7.1 → clams_python-1.7.3}/documentation/cli.rst +0 -0
  90. {clams_python-1.7.1 → clams_python-1.7.3}/documentation/gpu-apps.rst +0 -0
  91. {clams_python-1.7.1 → clams_python-1.7.3}/documentation/index.rst +0 -0
  92. {clams_python-1.7.1 → clams_python-1.7.3}/documentation/input-output.rst +0 -0
  93. {clams_python-1.7.1 → clams_python-1.7.3}/documentation/introduction.rst +0 -0
  94. {clams_python-1.7.1 → clams_python-1.7.3}/documentation/modules.rst +0 -0
  95. {clams_python-1.7.1 → clams_python-1.7.3}/documentation/runtime-params.rst +0 -0
  96. {clams_python-1.7.1 → clams_python-1.7.3}/documentation/tutorial.md +0 -0
  97. {clams_python-1.7.1 → clams_python-1.7.3}/setup.cfg +0 -0
  98. {clams_python-1.7.1 → clams_python-1.7.3}/tests/metadata.py +0 -0
  99. {clams_python-1.7.1 → clams_python-1.7.3}/tests/pillow-logo.png +0 -0
  100. {clams_python-1.7.1 → clams_python-1.7.3}/tests/test_backends_hf.py +0 -0
  101. {clams_python-1.7.1 → clams_python-1.7.3}/tests/test_clamsapp.py +0 -0
  102. {clams_python-1.7.1 → clams_python-1.7.3}/tests/test_envelope.py +0 -0
@@ -1,4 +1,21 @@
1
1
 
2
+ ## releasing 1.7.2 (2026-07-07)
3
+ ### Overview
4
+ This patch adds experimental handling of "reasoning traces" of propmtable apps
5
+
6
+ ### Additions
7
+ * common parameter `useReasoning` (bool) is added
8
+ * when reasoning is on, in-line tagged reasoning traces (commonly within `<think>...</think>`) are separately stored in `modelReasoningTrace` of the TextDocument
9
+
10
+ > [!NOTE]
11
+ > The new names are tentative and can be changed in the near future.
12
+
13
+
14
+ ## releasing 1.7.1 (2026-06-19)
15
+ ### Overview
16
+ Updates the `mmif-python` SDK dependency to 1.5.1 to incorporate a bugfix in VDH, with some other minor documentation generation improvements.
17
+
18
+
2
19
  ## releasing 1.7.0 (2026-05-29)
3
20
  ### Overview
4
21
  This release introduces a specialized base class `ClamsPromptableApp` and its HuggingFace specialization `ClamsHFPromptableApp` for CLAMS apps that wrap LLM / VLM / ALM / LMM-based analyzers, along with other HF-related helper code in various places. Additionally, it adds `appTags` field for app categorization in app metadata, and new timeframe related local utility recipe for `clams develop --recipe utl-tf` scaffold.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clams-python
3
- Version: 1.7.1
3
+ Version: 1.7.3
4
4
  Summary: A collection of APIs to develop CLAMS app for python
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: homepage, https://clams.ai
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
13
13
  Requires-Python: >=3.10
14
14
  Description-Content-Type: text/markdown
15
15
  License-File: LICENSE
16
- Requires-Dist: mmif-python==1.5.1
16
+ Requires-Dist: mmif-python==1.5.3
17
17
  Requires-Dist: Flask>=2
18
18
  Requires-Dist: Flask-RESTful>=0.3.9
19
19
  Requires-Dist: gunicorn>=20
@@ -711,6 +711,26 @@ class ClamsPromptableApp(ClamsApp):
711
711
  'values grow the KV cache linearly and increase GPU memory '
712
712
  'usage; reduce if VRAM is constrained.',
713
713
  },
714
+ {
715
+ 'name': 'useReasoning', 'type': 'boolean', 'default': False,
716
+ 'description':
717
+ 'Request the model\'s reasoning ("thinking") mode. Off by '
718
+ 'default. Honored only by apps whose backing model has a '
719
+ 'distinct reasoning mode; apps without one ignore it. When '
720
+ 'honored and enabled, the reasoning trace is split from the '
721
+ 'answer and stored in the ``modelReasoningTrace`` property of '
722
+ 'the output ``TextDocument`` (kept out of the document text). '
723
+ 'Reasoning is markedly slower and far more token-hungry: the '
724
+ 'whole trace is generated before the answer and drawn from the '
725
+ 'same budget capped by ``maxNewTokens``, so raise '
726
+ '``maxNewTokens`` substantially (thousands of tokens, not '
727
+ 'hundreds) when enabling this, or the trace may consume the '
728
+ 'entire budget and the answer be truncated or empty. Small '
729
+ 'reasoning models (as a rule of thumb, roughly 4B parameters '
730
+ 'and under) are especially prone to non-terminating "thinking '
731
+ 'loops" that exhaust the budget without producing an answer; '
732
+ 'validate termination per model before relying on it.',
733
+ },
714
734
  {
715
735
  'name': 'temperature', 'type': 'number', 'default': 0.0,
716
736
  'description':
@@ -1005,8 +1025,10 @@ class ClamsPromptableApp(ClamsApp):
1005
1025
  """
1006
1026
  Persist a single LLM text response into a view. Writes one
1007
1027
  ``TextDocument`` (containing the response) plus possible
1008
- grounding via an ``Alignment`` annotation and ``origins`` /
1009
- ``origination`` properties on the TD.
1028
+ grounding via an ``Alignment`` annotation and ``origins`` /
1029
+ ``origination`` properties on the TD. The TD also inherits the
1030
+ ``document`` of its ``source`` annotation, so it records which
1031
+ medium the response describes.
1010
1032
 
1011
1033
  The two grounding link kinds are semantically distinct:
1012
1034
 
@@ -1028,7 +1050,12 @@ class ClamsPromptableApp(ClamsApp):
1028
1050
  ``Alignment`` first if needed.
1029
1051
  :param source: ``id`` of the annotation to record as the
1030
1052
  cross-modal anchor of the response (see above).
1031
- :param response: the text generated by the model.
1053
+ :param response: the text to store as the TextDocument's
1054
+ value. Typically the model's generated answer, but the
1055
+ caller MAY pass a post-processed form (e.g. with a
1056
+ reasoning block stripped, whitespace trimmed, or otherwise
1057
+ reformatted); this method stores whatever it is given,
1058
+ verbatim, as the ``text``.
1032
1059
  :param origins: optional list of ``id``\\s of annotations the
1033
1060
  response was *derived* from. Must be paired with
1034
1061
  ``origination``.
@@ -1039,15 +1066,24 @@ class ClamsPromptableApp(ClamsApp):
1039
1066
  ``origins``.
1040
1067
  :param reasoning_trace: optional model-side reasoning trace
1041
1068
  (a chain-of-thought / scratchpad string, NOT a Python
1042
- traceback). NOT YET SUPPORTED -- passing a non-``None``
1043
- value raises :py:class:`NotImplementedError`. Storage
1044
- convention is still being decided at
1045
- clamsproject/clams-python#263.
1069
+ traceback). When non-``None``, it is stored on the
1070
+ returned TextDocument as the ``modelReasoningTrace``
1071
+ property, kept OUT of the ``text`` (which holds whatever
1072
+ ``response`` the caller passed). The point is separation,
1073
+ not any claim about the ``text`` being verbatim: storing a
1074
+ trace adds no annotation and no new type, so the type-level
1075
+ output shape is identical whether or not a trace is stored.
1076
+ Pass ``None`` (the default) to store no trace. The property
1077
+ name is provisional pending the
1078
+ vocabulary convention (clamsproject/clams-vocabulary#11).
1046
1079
  :return: ``(TextDocument, Alignment)`` tuple of the new
1047
1080
  annotations.
1048
1081
  :raises ValueError: if exactly one of ``origins`` /
1049
- ``origination`` is set; they must be supplied together
1050
- or both omitted.
1082
+ ``origination`` is set (they must be supplied together or
1083
+ both omitted), or if the ``source`` annotation carries no
1084
+ ``document`` to ground the response to (a malformed input).
1085
+ :raises KeyError: if ``source`` does not resolve to an
1086
+ annotation in the MMIF.
1051
1087
  """
1052
1088
  if bool(origins) != bool(origination):
1053
1089
  raise ValueError(
@@ -1056,6 +1092,20 @@ class ClamsPromptableApp(ClamsApp):
1056
1092
  f"origins={origins!r}, origination={origination!r}."
1057
1093
  )
1058
1094
  td = view.new_textdocument(text=response)
1095
+ # Ground the TextDocument to the medium it describes: copy `document`
1096
+ # from the source annotation (e.g. the parent TimeFrame's video) onto
1097
+ # the TD, so it records which document the response pertains to. A
1098
+ # grounded response must be traceable to a document, so a source that
1099
+ # carries no `document` means the upstream MMIF is malformed -- raise
1100
+ # rather than silently drop the grounding.
1101
+ source_ann = view._parent_mmif[source]
1102
+ source_doc = source_ann.get_property('document')
1103
+ if not source_doc:
1104
+ raise ValueError(
1105
+ f"source annotation {source!r} has no `document` property to "
1106
+ f"ground the response to; the input MMIF is malformed."
1107
+ )
1108
+ td.add_property('document', source_doc)
1059
1109
  if origins:
1060
1110
  td.add_property('origins', origins)
1061
1111
  td.add_property('origination', origination)
@@ -1065,13 +1115,71 @@ class ClamsPromptableApp(ClamsApp):
1065
1115
  target=td.id,
1066
1116
  )
1067
1117
  if reasoning_trace is not None:
1068
- raise NotImplementedError(
1069
- "Reasoning-trace storage convention is not yet defined; "
1070
- "tracked at clamsproject/clams-python#263."
1071
- )
1118
+ # Store the trace as a property on the TD rather than a separate
1119
+ # document: keeps the type-level output shape invariant (same
1120
+ # annotation, same `contains`, same graph) whether or not a trace is
1121
+ # present -- a strict enrichment, independent of whatever `response`
1122
+ # the caller put in `text`. Property name provisional pending
1123
+ # clamsproject/clams-vocabulary#11.
1124
+ td.add_property('modelReasoningTrace', reasoning_trace)
1072
1125
  return td, align
1073
1126
 
1074
-
1127
+ @staticmethod
1128
+ def split_tagged_reasoning_trace(
1129
+ text: str,
1130
+ open_tag: str = '<think>',
1131
+ close_tag: str = '</think>',
1132
+ assume_open: bool = False,
1133
+ ) -> Tuple[str, Optional[str]]:
1134
+ """
1135
+ Split a reasoning model's output into ``(answer, trace)`` for the
1136
+ inline XML-tag style: a reasoning block delimited by ``open_tag`` /
1137
+ ``close_tag`` (default ``<think>`` / ``</think>``), e.g. as emitted
1138
+ by DeepSeek-R1 or Qwen3.5.
1139
+
1140
+ Prefilled opening tags: some chat templates inject the OPENING tag
1141
+ into the prompt when reasoning is enabled (Qwen3.5 does this), so the
1142
+ decoded output carries only the CLOSING tag -- the model never emits
1143
+ the opener. Pass ``assume_open=True`` in that situation (typically
1144
+ wired to the app's reasoning toggle) so the trace is still recovered.
1145
+ The cases:
1146
+
1147
+ * close tag present: ``answer`` is the text after it; ``trace`` is the
1148
+ text between the tags, or everything before the close tag when the
1149
+ opening tag is absent (prefilled).
1150
+ * close tag absent, ``assume_open=True``: the output is unterminated
1151
+ reasoning (e.g. the trace overran ``maxNewTokens`` before closing) --
1152
+ there is no answer, so returns ``('', text.strip())``.
1153
+ * close tag absent, ``assume_open=False`` (default): treated as a
1154
+ plain, non-reasoning answer -- returns ``(text.strip(), None)``.
1155
+
1156
+ Only the inline-tag style is handled; models that isolate reasoning in
1157
+ a separate token-delimited channel (e.g. gpt-oss, Gemma) need their own
1158
+ parsing in the app.
1159
+
1160
+ :param text: raw decoded model output.
1161
+ :param open_tag: opening marker of the inline reasoning block.
1162
+ :param close_tag: closing marker of the inline reasoning block.
1163
+ :param assume_open: treat the text as the body of a reasoning block
1164
+ whose opening tag was prefilled into the prompt, so a missing close
1165
+ tag means unterminated reasoning (empty answer) rather than a plain
1166
+ answer.
1167
+ :return: ``(answer, trace_or_None)``.
1168
+ """
1169
+ ci = text.rfind(close_tag)
1170
+ if ci == -1:
1171
+ return ('', text.strip()) if assume_open else (text.strip(), None)
1172
+ answer = text[ci + len(close_tag):].strip()
1173
+ oi = text.find(open_tag)
1174
+ if oi != -1 and oi < ci:
1175
+ trace = text[oi + len(open_tag):ci].strip()
1176
+ elif assume_open:
1177
+ # opening tag was prefilled into the prompt (not generated), so the
1178
+ # trace is everything up to the close tag
1179
+ trace = text[:ci].strip()
1180
+ else:
1181
+ trace = None
1182
+ return answer, trace
1075
1183
  class ClamsHFPromptableApp(ClamsPromptableApp):
1076
1184
  """
1077
1185
  Base class for promptable CLAMS apps backed by a local
@@ -1289,7 +1397,7 @@ class ClamsHFPromptableApp(ClamsPromptableApp):
1289
1397
  dtype=self.DTYPE,
1290
1398
  padding_side=self.PADDING_SIDE,
1291
1399
  revision=revision,
1292
- model_kwargs=self.MODEL_KWARGS,
1400
+ model_kwargs=self.model_load_kwargs(model_id, revision),
1293
1401
  processor_kwargs=self.PROCESSOR_KWARGS,
1294
1402
  )
1295
1403
  self.logger.info(f"HF model loaded on {triple[2]}")
@@ -1297,6 +1405,32 @@ class ClamsHFPromptableApp(ClamsPromptableApp):
1297
1405
  self.processor, self.model, self.device = triple
1298
1406
  return triple
1299
1407
 
1408
+ def model_load_kwargs(self, model_id: str, revision: str) -> dict:
1409
+ """
1410
+ The ``model_kwargs`` forwarded to
1411
+ :func:`~clams.backends.hf.load_hf_model` (and on to
1412
+ ``from_pretrained``) for a SPECIFIC family member. The base
1413
+ implementation returns a copy of the class-level
1414
+ :py:attr:`MODEL_KWARGS`, applied uniformly to every variant.
1415
+
1416
+ Override when a family mixes per-variant load requirements that
1417
+ a single class-level ``MODEL_KWARGS`` cannot express -- e.g. a
1418
+ ``analyzer_versions`` family whose members span full-precision,
1419
+ FP8, and GPTQ-Int4 builds, where only the quantized members need
1420
+ a ``quantization_config``::
1421
+
1422
+ def model_load_kwargs(self, model_id, revision):
1423
+ kw = super().model_load_kwargs(model_id, revision)
1424
+ if model_id.endswith('-GPTQ-Int4'):
1425
+ kw['quantization_config'] = ... # gptq config
1426
+ return kw
1427
+
1428
+ :param model_id: resolved HF model id (no ``@revision`` suffix).
1429
+ :param revision: resolved commit/revision being loaded.
1430
+ :return: ``model_kwargs`` dict for this variant.
1431
+ """
1432
+ return dict(self.MODEL_KWARGS or {})
1433
+
1300
1434
  def generate(
1301
1435
  self,
1302
1436
  prompt: List[str],
@@ -1341,6 +1475,7 @@ class ClamsHFPromptableApp(ClamsPromptableApp):
1341
1475
  if n == 0:
1342
1476
  return []
1343
1477
  gen_kwargs = self.build_gen_kwargs(**generation_params)
1478
+ template_kwargs = self.build_template_kwargs(**generation_params)
1344
1479
  try:
1345
1480
  conversations = [
1346
1481
  self.build_conversation(
@@ -1357,6 +1492,7 @@ class ClamsHFPromptableApp(ClamsPromptableApp):
1357
1492
  return_dict=True,
1358
1493
  padding=True,
1359
1494
  return_tensors="pt",
1495
+ **template_kwargs,
1360
1496
  )
1361
1497
  inputs = inputs.to(self.device)
1362
1498
  if (self.DTYPE is not None
@@ -1405,6 +1541,31 @@ class ClamsHFPromptableApp(ClamsPromptableApp):
1405
1541
  })
1406
1542
  return gen_kwargs
1407
1543
 
1544
+ def build_template_kwargs(self, **generation_params) -> dict:
1545
+ """
1546
+ Extra keyword arguments to pass to
1547
+ ``processor.apply_chat_template`` in :py:meth:`generate`, beyond
1548
+ the SDK-fixed ones (``add_generation_prompt``, ``tokenize``,
1549
+ ``return_dict``, ``padding``, ``return_tensors``). The base
1550
+ implementation returns ``{}``.
1551
+
1552
+ Override to inject model-specific chat-template controls without
1553
+ having to reimplement :py:meth:`generate`. This is where an app
1554
+ honors the SDK ``useReasoning`` parameter, mapping it onto the
1555
+ backend's reasoning switch. Common cases:
1556
+
1557
+ * ``{'enable_thinking': bool(generation_params.get('use_reasoning'))}``
1558
+ to bind ``useReasoning`` to a reasoning ("thinking") model's
1559
+ template switch;
1560
+ * ``{'tools': [...]}`` or ``{'documents': [...]}`` for tool-use /
1561
+ RAG chat templates.
1562
+
1563
+ Keys MUST NOT collide with the SDK-fixed kwargs above. Unknown
1564
+ keys are passed through to the template's render context, where
1565
+ a template that does not reference them simply ignores them.
1566
+ """
1567
+ return {}
1568
+
1408
1569
 
1409
1570
  class ParameterCaster(object):
1410
1571
 
@@ -6,13 +6,17 @@ DO NOT CHANGE the name of the file
6
6
  """
7
7
 
8
8
  import argparse
9
+ import json
9
10
  import sys
10
11
  from contextlib import redirect_stdout
11
12
 
13
+ import jsonschema
14
+
12
15
  import app
13
16
 
14
17
  import clams.app
15
18
  from clams import AppMetadata
19
+ from clams.envelop import EnvelopeError
16
20
 
17
21
 
18
22
  def metadata_to_argparser(app_metadata: AppMetadata) -> argparse.ArgumentParser:
@@ -81,11 +85,24 @@ if __name__ == "__main__":
81
85
  params[pname] = pvalue
82
86
  else:
83
87
  params[pname] = [pvalue]
84
- if args.OUT_MMIF_FILE.name == '<stdout>':
85
- with redirect_stdout(sys.stderr):
88
+ # Mirror the HTTP server's error handling (see clams.restify): an invalid
89
+ # input is reported and exits non-zero, while an app-level failure is
90
+ # recorded as an error view instead of crashing with a raw traceback.
91
+ try:
92
+ if args.OUT_MMIF_FILE.name == '<stdout>':
93
+ with redirect_stdout(sys.stderr):
94
+ out_mmif = clamsapp.annotate(in_data, **params)
95
+ else:
86
96
  out_mmif = clamsapp.annotate(in_data, **params)
87
- else:
88
- out_mmif = clamsapp.annotate(in_data, **params)
97
+ except (jsonschema.exceptions.ValidationError, json.JSONDecodeError, EnvelopeError) as e:
98
+ detail = e.message if isinstance(e, jsonschema.exceptions.ValidationError) else str(e)
99
+ print(f"Invalid input data. See below for validation error.\n\n{detail}", file=sys.stderr)
100
+ sys.exit(1)
101
+ except Exception:
102
+ clamsapp.logger.exception("Error in annotation")
103
+ out_mmif = clamsapp.record_error(in_data, **params).serialize(pretty=True)
104
+ args.OUT_MMIF_FILE.write(out_mmif)
105
+ sys.exit(1)
89
106
  args.OUT_MMIF_FILE.write(out_mmif)
90
107
  else:
91
108
  arg_parser.print_help()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clams-python
3
- Version: 1.7.1
3
+ Version: 1.7.3
4
4
  Summary: A collection of APIs to develop CLAMS app for python
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: homepage, https://clams.ai
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
13
13
  Requires-Python: >=3.10
14
14
  Description-Content-Type: text/markdown
15
15
  License-File: LICENSE
16
- Requires-Dist: mmif-python==1.5.1
16
+ Requires-Dist: mmif-python==1.5.3
17
17
  Requires-Dist: Flask>=2
18
18
  Requires-Dist: Flask-RESTful>=0.3.9
19
19
  Requires-Dist: gunicorn>=20
@@ -50,6 +50,8 @@ clams_python.egg-info/SOURCES.txt
50
50
  clams_python.egg-info/dependency_links.txt
51
51
  clams_python.egg-info/entry_points.txt
52
52
  clams_python.egg-info/requires.txt
53
+ clams_python.egg-info/scm_file_list.json
54
+ clams_python.egg-info/scm_version.json
53
55
  clams_python.egg-info/top_level.txt
54
56
  container/Containerfile
55
57
  container/ffmpeg-hf.containerfile
@@ -87,6 +89,7 @@ documentation/target-versions.csv
87
89
  documentation/tutorial.md
88
90
  documentation/autodoc/clams.app.rst
89
91
  documentation/autodoc/clams.appmetadata.rst
92
+ documentation/autodoc/clams.backends.rst
90
93
  documentation/autodoc/clams.mmif_utils.rst
91
94
  documentation/autodoc/clams.restify.rst
92
95
  tests/metadata.py
@@ -1,4 +1,4 @@
1
- mmif-python==1.5.1
1
+ mmif-python==1.5.3
2
2
  Flask>=2
3
3
  Flask-RESTful>=0.3.9
4
4
  gunicorn>=20
@@ -0,0 +1,96 @@
1
+ {
2
+ "files": [
3
+ "README.md",
4
+ "LICENSE",
5
+ "pyproject.toml",
6
+ ".pytype.cfg",
7
+ "CHANGELOG.md",
8
+ "CONTRIBUTING.md",
9
+ ".gitignore",
10
+ "build-tools/publish.py",
11
+ "build-tools/prep_release.py",
12
+ "build-tools/build.py",
13
+ "build-tools/docs.py",
14
+ "build-tools/clean.py",
15
+ "build-tools/test.py",
16
+ "clams/__init__.py",
17
+ "clams/develop/__init__.py",
18
+ "clams/develop/templates/gha/for-clams-team.md.template",
19
+ "clams/develop/templates/gha/workflows/issue-assign.yml.template",
20
+ "clams/develop/templates/gha/workflows/publish.yml.template",
21
+ "clams/develop/templates/gha/workflows/issue-apps-project.yml.template",
22
+ "clams/develop/templates/gha/workflows/issue-close.yml.template",
23
+ "clams/develop/templates/utl-tf/__init__.py.template",
24
+ "clams/develop/templates/utl-tf/timeframe.py.template",
25
+ "clams/develop/templates/app/Containerfile.template",
26
+ "clams/develop/templates/app/metadata.py.template",
27
+ "clams/develop/templates/app/README.md.template",
28
+ "clams/develop/templates/app/.dockerignore.template",
29
+ "clams/develop/templates/app/requirements.txt.template",
30
+ "clams/develop/templates/app/LICENSE.template",
31
+ "clams/develop/templates/app/.gitignore.template",
32
+ "clams/develop/templates/app/app.py.template",
33
+ "clams/develop/templates/app/cli.py.template",
34
+ "clams/restify/__init__.py",
35
+ "clams/envelop/__init__.py",
36
+ "clams/appmetadata/__init__.py",
37
+ "clams/app/__init__.py",
38
+ "clams/serve/__init__.py",
39
+ "clams/ver/__init__.py",
40
+ "clams/backends/__init__.py",
41
+ "clams/backends/hf.py",
42
+ "documentation/modules.rst",
43
+ "documentation/target-versions.csv",
44
+ "documentation/app-baseclasses.rst",
45
+ "documentation/gpu-apps.rst",
46
+ "documentation/clamsapp.rst",
47
+ "documentation/appdirectory.rst",
48
+ "documentation/index.rst",
49
+ "documentation/tutorial.md",
50
+ "documentation/introduction.rst",
51
+ "documentation/appmetadata.rst",
52
+ "documentation/cli.rst",
53
+ "documentation/runtime-params.rst",
54
+ "documentation/input-output.rst",
55
+ "documentation/conf.py",
56
+ "documentation/autodoc/clams.restify.rst",
57
+ "documentation/autodoc/clams.app.rst",
58
+ "documentation/autodoc/clams.backends.rst",
59
+ "documentation/autodoc/clams.mmif_utils.rst",
60
+ "documentation/autodoc/clams.appmetadata.rst",
61
+ "tests/test_promptable.py",
62
+ "tests/pillow-logo.png",
63
+ "tests/test_backends_hf.py",
64
+ "tests/test_envelope.py",
65
+ "tests/metadata.py",
66
+ "tests/test_clamsapp.py",
67
+ "container/tf2.containerfile",
68
+ "container/opencv4-hf.containerfile",
69
+ "container/torch2.containerfile",
70
+ "container/opencv4-hf4.containerfile",
71
+ "container/Containerfile",
72
+ "container/hf4.containerfile",
73
+ "container/ffmpeg-hf.containerfile",
74
+ "container/ffmpeg-tf2.containerfile",
75
+ "container/generate_containers_yml.py",
76
+ "container/opencv4-tf2.containerfile",
77
+ "container/opencv4-torch2.containerfile",
78
+ "container/hf.containerfile",
79
+ "container/jdk8.containerfile",
80
+ "container/hf5.containerfile",
81
+ "container/ffmpeg.containerfile",
82
+ "container/ffmpeg-hf5.containerfile",
83
+ "container/ffmpeg-hf4.containerfile",
84
+ "container/opencv4-hf5.containerfile",
85
+ "container/ffmpeg-torch2.containerfile",
86
+ "container/opencv4.containerfile",
87
+ ".github/workflows/container.yml",
88
+ ".github/workflows/containers.yml",
89
+ ".github/workflows/issue-close.yml",
90
+ ".github/workflows/codecov.yml",
91
+ ".github/workflows/release-check.yml",
92
+ ".github/workflows/issue-infra-project.yml",
93
+ ".github/workflows/issue-assign.yml",
94
+ ".github/workflows/publish.yml"
95
+ ]
96
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "1.7.3",
3
+ "distance": 0,
4
+ "node": "gea1bca511e70720b1f46476fbb5f41ff2d9329cd",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-07-09"
8
+ }
@@ -174,6 +174,17 @@ from :class:`~clams.app.ClamsApp`. These names are reserved; see
174
174
  - Maximum number of new tokens generated per inference call. Larger values
175
175
  grow the KV cache linearly and add to GPU memory usage; reduce if VRAM
176
176
  is constrained.
177
+ * - ``useReasoning``
178
+ - boolean
179
+ - ``false``
180
+ - no
181
+ - Request the model's reasoning ("thinking") mode. Off by default;
182
+ honored only by apps whose model has a distinct reasoning mode
183
+ (others ignore it). Much slower and more token-hungry -- the trace is
184
+ generated before the answer and drawn from the ``maxNewTokens`` budget,
185
+ and small models can loop without terminating. When honored, the trace
186
+ is stored in the ``modelReasoningTrace`` property of the output
187
+ ``TextDocument``. See :ref:`promptable-reasoning`.
177
188
  * - ``temperature``
178
189
  - number
179
190
  - ``0.0``
@@ -320,6 +331,52 @@ inferences, final reply returned.
320
331
  ``turn-taking`` is the default because it costs a single inference call
321
332
  and is the more common multi-element pattern.
322
333
 
334
+ .. _promptable-reasoning:
335
+
336
+ Reasoning traces (``useReasoning``)
337
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
338
+
339
+ Some models expose a distinct reasoning ("thinking") mode that emits an
340
+ intermediate reasoning trace before the answer. ``useReasoning`` is the
341
+ standard toggle for that mode. It is inert unless the app honors it, and
342
+ honoring is a per-app choice with three wiring points:
343
+
344
+ - map ``useReasoning`` onto the backend's reasoning switch (for HF chat
345
+ templates, typically ``enable_thinking``) by overriding
346
+ :meth:`~clams.app.ClamsHFPromptableApp.build_template_kwargs`;
347
+ - split the trace from the answer with
348
+ :meth:`~clams.app.ClamsPromptableApp.split_tagged_reasoning_trace`
349
+ (inline ``<think>...</think>``-style traces only; channel-delimited
350
+ formats need app-specific parsing). Many chat templates prefill the
351
+ opening ``<think>`` into the prompt, so only the closing tag appears in
352
+ the output; pass ``assume_open=True`` (wired to the reasoning toggle) so
353
+ the trace is still recovered and a missing close tag is read as
354
+ unterminated reasoning (empty answer) rather than a plain answer;
355
+ - pass the trace to
356
+ :meth:`~clams.app.ClamsPromptableApp.response_to_grounded_textdocument`
357
+ through its ``reasoning_trace`` argument, which stores it in the
358
+ ``modelReasoningTrace`` property of the produced ``TextDocument``,
359
+ separate from the document text.
360
+
361
+ An app whose model has no reasoning mode leaves ``useReasoning`` unwired;
362
+ callers may still set it and it has no effect, like ``topK`` under greedy
363
+ decoding.
364
+
365
+ Cost and failure modes
366
+ """"""""""""""""""""""
367
+
368
+ Reasoning is much slower and far more token-hungry than a direct answer:
369
+ the entire trace is generated first, from the same budget capped by
370
+ ``maxNewTokens``. Enabling ``useReasoning`` without raising ``maxNewTokens``
371
+ substantially (thousands of tokens, not hundreds) risks the trace consuming
372
+ the whole budget, leaving the answer truncated or empty. Small reasoning
373
+ models (as a rule of thumb, roughly 4B parameters and under) are especially
374
+ prone to non-terminating "thinking loops" that never reach an answer; budget
375
+ generously and validate termination per model. When the trace overruns the
376
+ budget before closing, ``assume_open=True`` yields an empty answer plus the
377
+ unterminated trace -- an app should surface that (e.g. a warning and an empty
378
+ ``TextDocument``) rather than emitting the raw reasoning as the answer.
379
+
323
380
  Helpers
324
381
  ^^^^^^^
325
382
 
@@ -512,7 +569,10 @@ supplies:
512
569
  * a default
513
570
  :py:meth:`~clams.app.ClamsHFPromptableApp.build_gen_kwargs` that
514
571
  maps the SDK promptable parameters to HF ``model.generate()``
515
- kwargs.
572
+ kwargs;
573
+ * a :py:meth:`~clams.app.ClamsHFPromptableApp.build_template_kwargs`
574
+ hook for chat-template controls, the override point for honoring
575
+ ``useReasoning`` (see :ref:`promptable-reasoning`).
516
576
 
517
577
  See each method's docstring for full details.
518
578
 
@@ -0,0 +1,15 @@
1
+ clams.backends package
2
+ ======================
3
+
4
+ .. automodule:: clams.backends
5
+
6
+ Submodules
7
+ ----------
8
+
9
+ clams.backends.hf
10
+ ^^^^^^^^^^^^^^^^^
11
+
12
+ .. automodule:: clams.backends.hf
13
+ :members:
14
+ :undoc-members:
15
+ :show-inheritance:
@@ -26,7 +26,9 @@ sys.path.insert(0, str(proj_root_dir))
26
26
 
27
27
  # -- Project information -----------------------------------------------------
28
28
 
29
- project = proj_root_dir.name
29
+ # The canonical package name — NOT derived from the checkout directory,
30
+ # which is `clone-repo` when the shared docs-publish workflow builds here.
31
+ project = 'clams-python'
30
32
  blob_base_url = f'https://github.com/clamsproject/{project}/blob'
31
33
  copyright = f'{datetime.date.today().year}, Brandeis LLC'
32
34
  author = 'Brandeis LLC'
@@ -1,4 +1,6 @@
1
1
  "``clams-python`` version","``mmif-python`` version","Target MMIF Specification"
2
+ `1.7.3 <https://pypi.org/project/clams-python/1.7.3/>`__,`1.5.1 <https://pypi.org/project/mmif-python/1.5.1/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
3
+ `1.7.2 <https://pypi.org/project/clams-python/1.7.2/>`__,`1.5.1 <https://pypi.org/project/mmif-python/1.5.1/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
2
4
  `1.7.1 <https://pypi.org/project/clams-python/1.7.1/>`__,`1.5.1 <https://pypi.org/project/mmif-python/1.5.1/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
3
5
  `1.7.0 <https://pypi.org/project/clams-python/1.7.0/>`__,`1.5.0 <https://pypi.org/project/mmif-python/1.5.0/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
4
6
  `1.6.0 <https://pypi.org/project/clams-python/1.6.0/>`__,`1.4.0 <https://pypi.org/project/mmif-python/1.4.0/>`__,`1.1.1 <https://mmif.clams.ai/1.1.1/>`__
@@ -19,7 +19,7 @@ classifiers = [
19
19
  "Programming Language :: Python :: 3 :: Only",
20
20
  ]
21
21
  dependencies = [
22
- "mmif-python==1.5.1",
22
+ "mmif-python==1.5.3",
23
23
  "Flask>=2",
24
24
  "Flask-RESTful>=0.3.9",
25
25
  "gunicorn>=20",
@@ -10,7 +10,7 @@ single-turn / turn-taking / user-only modes, and the
10
10
  """
11
11
  import unittest
12
12
 
13
- from mmif import AnnotationTypes, DocumentTypes, Mmif
13
+ from mmif import AnnotationTypes, Document, DocumentTypes, Mmif
14
14
 
15
15
  from clams import AppMetadata, ClamsPromptableApp
16
16
 
@@ -326,6 +326,19 @@ class TestStoreResponse(unittest.TestCase):
326
326
  def setUp(self):
327
327
  self.app = make_test_app(make_metadata(call_helper=True))
328
328
  self.mmif = Mmif(validate=False)
329
+ # upstream: a media document and an annotation anchored to it, which
330
+ # serves as the response's `source`.
331
+ vdoc = Document()
332
+ vdoc.at_type = DocumentTypes.VideoDocument
333
+ vdoc.id = 'v1'
334
+ vdoc.location = 'file:///video.mp4'
335
+ self.mmif.add_document(vdoc)
336
+ self.doc_id = vdoc.id
337
+ src_view = self.mmif.new_view()
338
+ src_view.metadata.app = 'http://upstream/1'
339
+ self.src = src_view.new_annotation(
340
+ AnnotationTypes.TimeFrame, document=vdoc.id, label='scene')
341
+ # the current app's view
329
342
  self.view = self.mmif.new_view()
330
343
  self.app.sign_view(self.view, {})
331
344
  self.view.new_contain(DocumentTypes.TextDocument)
@@ -333,22 +346,34 @@ class TestStoreResponse(unittest.TestCase):
333
346
 
334
347
  def test_happy_path_creates_textdocument_and_alignment(self):
335
348
  td, align = self.app.response_to_grounded_textdocument(
336
- self.view, source='src1', response='generated text')
349
+ self.view, source=self.src.id, response='generated text')
337
350
  self.assertEqual(td.text_value, 'generated text')
338
- self.assertEqual(align.get_property('source'), 'src1')
351
+ # the TD inherits the source annotation's document
352
+ self.assertEqual(td.get_property('document'), self.doc_id)
353
+ self.assertEqual(align.get_property('source'), self.src.id)
339
354
  self.assertEqual(align.get_property('target'), td.id)
340
355
 
341
- def test_reasoning_trace_none_does_not_raise(self):
342
- # no exception
343
- self.app.response_to_grounded_textdocument(
344
- self.view, source='src1', response='text',
356
+ def test_source_without_document_raises(self):
357
+ # a source annotation carrying no `document` means a malformed input
358
+ ungrounded = self.view.new_annotation(AnnotationTypes.TimeFrame)
359
+ with self.assertRaises(ValueError):
360
+ self.app.response_to_grounded_textdocument(
361
+ self.view, source=ungrounded.id, response='text')
362
+
363
+ def test_reasoning_trace_none_stores_no_property(self):
364
+ td, _ = self.app.response_to_grounded_textdocument(
365
+ self.view, source=self.src.id, response='text',
345
366
  reasoning_trace=None)
367
+ self.assertNotIn('modelReasoningTrace', td.properties)
346
368
 
347
- def test_reasoning_trace_not_none_raises_not_implemented(self):
348
- with self.assertRaises(NotImplementedError):
349
- self.app.response_to_grounded_textdocument(
350
- self.view, source='src1', response='text',
351
- reasoning_trace='intermediate reasoning')
369
+ def test_reasoning_trace_stored_on_textdocument(self):
370
+ td, _ = self.app.response_to_grounded_textdocument(
371
+ self.view, source=self.src.id, response='the answer',
372
+ reasoning_trace='step 1 ... step 2 ...')
373
+ # trace lives in the property; the TD text stays answer-only
374
+ self.assertEqual(
375
+ td.get_property('modelReasoningTrace'), 'step 1 ... step 2 ...')
376
+ self.assertEqual(td.text_value, 'the answer')
352
377
 
353
378
  # TODO (krim @ 05/28/26): this test case belongs upstream in the
354
379
  # vocabulary type definition (the `origins`/`origination` pairing
@@ -359,11 +384,11 @@ class TestStoreResponse(unittest.TestCase):
359
384
  # underlying TD.
360
385
  def test_origins_and_origination_written_together(self):
361
386
  td, align = self.app.response_to_grounded_textdocument(
362
- self.view, source='tf1', response='caption text',
387
+ self.view, source=self.src.id, response='caption text',
363
388
  origins=['tp1'], origination='derived')
364
389
  self.assertEqual(td.get_property('origins'), ['tp1'])
365
390
  self.assertEqual(td.get_property('origination'), 'derived')
366
- self.assertEqual(align.get_property('source'), 'tf1')
391
+ self.assertEqual(align.get_property('source'), self.src.id)
367
392
  self.assertEqual(align.get_property('target'), td.id)
368
393
 
369
394
  def test_unpaired_origins_or_origination_raises(self):
@@ -377,6 +402,73 @@ class TestStoreResponse(unittest.TestCase):
377
402
  self.view, source='src1', response='text', **kwargs)
378
403
 
379
404
 
405
+ # ---------------------------------------------------------------------------
406
+ # Reasoning-trace split helper
407
+ # ---------------------------------------------------------------------------
408
+
409
+ class TestSplitReasoningTrace(unittest.TestCase):
410
+ """:meth:`ClamsPromptableApp.split_tagged_reasoning_trace` (static)."""
411
+
412
+ split = staticmethod(ClamsPromptableApp.split_tagged_reasoning_trace)
413
+
414
+ def test_closed_block_splits_answer_and_trace(self):
415
+ answer, trace = self.split('<think>reasoning</think>The answer.')
416
+ self.assertEqual(answer, 'The answer.')
417
+ self.assertEqual(trace, 'reasoning')
418
+
419
+ def test_no_tags_returns_text_as_answer_none_trace(self):
420
+ answer, trace = self.split('just an answer')
421
+ self.assertEqual(answer, 'just an answer')
422
+ self.assertIsNone(trace)
423
+
424
+ def test_unterminated_block_not_overstripped(self):
425
+ # No closing tag (e.g. ran out of tokens mid-thought): return the
426
+ # raw text rather than discard a possibly-real answer.
427
+ raw = '<think>thinking with no close'
428
+ answer, trace = self.split(raw)
429
+ self.assertEqual(answer, raw)
430
+ self.assertIsNone(trace)
431
+
432
+ def test_uses_final_close_tag(self):
433
+ answer, trace = self.split(
434
+ '<think>a</think>mid<think>b</think>final')
435
+ self.assertEqual(answer, 'final')
436
+
437
+ def test_custom_tags(self):
438
+ answer, trace = self.split(
439
+ '[R]why[/R]done', open_tag='[R]', close_tag='[/R]')
440
+ self.assertEqual(answer, 'done')
441
+ self.assertEqual(trace, 'why')
442
+
443
+ def test_non_thinking_output_safe_to_call(self):
444
+ # Safe to call unconditionally even when thinking is disabled.
445
+ answer, trace = self.split(' plain caption ')
446
+ self.assertEqual(answer, 'plain caption')
447
+ self.assertIsNone(trace)
448
+
449
+ def test_assume_open_close_present_open_absent(self):
450
+ # Prefilled opening tag (e.g. Qwen3.5): only the close tag is emitted.
451
+ answer, trace = self.split(
452
+ 'reasoning body</think>The answer.', assume_open=True)
453
+ self.assertEqual(answer, 'The answer.')
454
+ self.assertEqual(trace, 'reasoning body')
455
+
456
+ def test_assume_open_no_close_is_unterminated_reasoning(self):
457
+ # Trace overran the budget before closing: empty answer, whole text is
458
+ # the unterminated trace.
459
+ raw = 'still thinking, never closed'
460
+ answer, trace = self.split(raw, assume_open=True)
461
+ self.assertEqual(answer, '')
462
+ self.assertEqual(trace, raw)
463
+
464
+ def test_open_absent_default_keeps_backward_compatible(self):
465
+ # Without assume_open, a lone close tag is not treated as a prefilled
466
+ # block: answer after the close, no trace.
467
+ answer, trace = self.split('reasoning body</think>The answer.')
468
+ self.assertEqual(answer, 'The answer.')
469
+ self.assertIsNone(trace)
470
+
471
+
380
472
  # ---------------------------------------------------------------------------
381
473
  # Transport-neutral parameter casting
382
474
  # ---------------------------------------------------------------------------
@@ -580,6 +672,56 @@ class TestHFPromptableAppClassAttrs(unittest.TestCase):
580
672
  finally:
581
673
  restore()
582
674
 
675
+ def test_build_template_kwargs_default_empty(self):
676
+ restore, _ = self._patch_load()
677
+ try:
678
+ app = self._make_subclass(analyzer_versions=self.SINGLETON_AV)()
679
+ self.assertEqual(app.build_template_kwargs(), {})
680
+ finally:
681
+ restore()
682
+
683
+ def test_model_load_kwargs_default_is_model_kwargs_copy(self):
684
+ restore, calls = self._patch_load()
685
+ try:
686
+ app = self._make_subclass(
687
+ analyzer_versions=self.SINGLETON_AV,
688
+ MODEL_KWARGS={'trust_remote_code': True},
689
+ )()
690
+ # eager singleton load forwarded the class-level MODEL_KWARGS
691
+ self.assertEqual(
692
+ calls[0]['model_kwargs'], {'trust_remote_code': True})
693
+ # and it's a copy, not the same object
694
+ self.assertIsNot(
695
+ app.model_load_kwargs('org/fake-model', 'deadbee'),
696
+ app.MODEL_KWARGS)
697
+ finally:
698
+ restore()
699
+
700
+ def test_model_load_kwargs_override_is_per_variant(self):
701
+ restore, calls = self._patch_load()
702
+ try:
703
+ def per_variant(self, model_id, revision):
704
+ kw = {'base': True}
705
+ if model_id.endswith('small-model'):
706
+ kw['quantized'] = True
707
+ return kw
708
+
709
+ cls = self._make_subclass(
710
+ analyzer_versions=self.MULTI_AV,
711
+ model_load_kwargs=per_variant,
712
+ )
713
+ app = cls() # multi-member: no eager load
714
+ self.assertEqual(len(calls), 0)
715
+ app.load_model('org/large-model')
716
+ app.load_model('org/small-model')
717
+ large = next(c for c in calls if c['model_id'] == 'org/large-model')
718
+ small = next(c for c in calls if c['model_id'] == 'org/small-model')
719
+ self.assertEqual(large['model_kwargs'], {'base': True})
720
+ self.assertEqual(
721
+ small['model_kwargs'], {'base': True, 'quantized': True})
722
+ finally:
723
+ restore()
724
+
583
725
 
584
726
  if __name__ == '__main__':
585
727
  unittest.main()
File without changes
File without changes
File without changes
File without changes
File without changes