prompture 0.0.47.dev2__tar.gz → 0.0.47.dev3__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 (139) hide show
  1. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/PKG-INFO +1 -1
  2. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/_version.py +2 -2
  3. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/async_conversation.py +16 -0
  4. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/conversation.py +16 -0
  5. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/async_grok_driver.py +23 -9
  6. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/async_groq_driver.py +23 -9
  7. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/async_lmstudio_driver.py +10 -2
  8. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/async_moonshot_driver.py +20 -9
  9. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/async_openrouter_driver.py +43 -17
  10. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/grok_driver.py +23 -9
  11. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/groq_driver.py +23 -9
  12. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/lmstudio_driver.py +11 -2
  13. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/moonshot_driver.py +20 -9
  14. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/openrouter_driver.py +34 -10
  15. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture.egg-info/PKG-INFO +1 -1
  16. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/.claude/skills/add-driver/SKILL.md +0 -0
  17. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/.claude/skills/add-driver/references/driver-template.md +0 -0
  18. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/.claude/skills/add-example/SKILL.md +0 -0
  19. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/.claude/skills/add-field/SKILL.md +0 -0
  20. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/.claude/skills/add-persona/SKILL.md +0 -0
  21. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/.claude/skills/add-test/SKILL.md +0 -0
  22. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/.claude/skills/add-tool/SKILL.md +0 -0
  23. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/.claude/skills/run-tests/SKILL.md +0 -0
  24. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/.claude/skills/scaffold-extraction/SKILL.md +0 -0
  25. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/.claude/skills/update-pricing/SKILL.md +0 -0
  26. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/.env.copy +0 -0
  27. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/.github/FUNDING.yml +0 -0
  28. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/.github/scripts/update_docs_version.py +0 -0
  29. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/.github/scripts/update_wrapper_version.py +0 -0
  30. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/.github/workflows/dev.yml +0 -0
  31. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/.github/workflows/documentation.yml +0 -0
  32. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/.github/workflows/publish.yml +0 -0
  33. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/CLAUDE.md +0 -0
  34. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/LICENSE +0 -0
  35. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/MANIFEST.in +0 -0
  36. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/README.md +0 -0
  37. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/ROADMAP.md +0 -0
  38. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/docs/source/_static/custom.css +0 -0
  39. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/docs/source/_templates/footer.html +0 -0
  40. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/docs/source/api/core.rst +0 -0
  41. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/docs/source/api/drivers.rst +0 -0
  42. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/docs/source/api/field_definitions.rst +0 -0
  43. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/docs/source/api/index.rst +0 -0
  44. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/docs/source/api/runner.rst +0 -0
  45. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/docs/source/api/tools.rst +0 -0
  46. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/docs/source/api/validator.rst +0 -0
  47. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/docs/source/conf.py +0 -0
  48. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/docs/source/contributing.rst +0 -0
  49. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/docs/source/examples.rst +0 -0
  50. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/docs/source/field_definitions_reference.rst +0 -0
  51. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/docs/source/index.rst +0 -0
  52. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/docs/source/installation.rst +0 -0
  53. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/docs/source/quickstart.rst +0 -0
  54. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/docs/source/toon_input_guide.rst +0 -0
  55. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/packages/README.md +0 -0
  56. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/packages/llm_to_json/README.md +0 -0
  57. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/packages/llm_to_json/llm_to_json/__init__.py +0 -0
  58. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/packages/llm_to_json/pyproject.toml +0 -0
  59. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/packages/llm_to_json/test.py +0 -0
  60. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/packages/llm_to_toon/README.md +0 -0
  61. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/packages/llm_to_toon/llm_to_toon/__init__.py +0 -0
  62. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/packages/llm_to_toon/pyproject.toml +0 -0
  63. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/packages/llm_to_toon/test.py +0 -0
  64. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/__init__.py +0 -0
  65. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/agent.py +0 -0
  66. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/agent_types.py +0 -0
  67. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/aio/__init__.py +0 -0
  68. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/async_agent.py +0 -0
  69. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/async_core.py +0 -0
  70. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/async_driver.py +0 -0
  71. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/async_groups.py +0 -0
  72. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/cache.py +0 -0
  73. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/callbacks.py +0 -0
  74. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/cli.py +0 -0
  75. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/core.py +0 -0
  76. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/cost_mixin.py +0 -0
  77. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/discovery.py +0 -0
  78. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/driver.py +0 -0
  79. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/__init__.py +0 -0
  80. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/airllm_driver.py +0 -0
  81. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/async_airllm_driver.py +0 -0
  82. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/async_azure_driver.py +0 -0
  83. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/async_claude_driver.py +0 -0
  84. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/async_google_driver.py +0 -0
  85. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/async_hugging_driver.py +0 -0
  86. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/async_local_http_driver.py +0 -0
  87. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/async_modelscope_driver.py +0 -0
  88. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/async_ollama_driver.py +0 -0
  89. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/async_openai_driver.py +0 -0
  90. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/async_registry.py +0 -0
  91. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/async_zai_driver.py +0 -0
  92. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/azure_driver.py +0 -0
  93. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/claude_driver.py +0 -0
  94. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/google_driver.py +0 -0
  95. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/hugging_driver.py +0 -0
  96. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/local_http_driver.py +0 -0
  97. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/modelscope_driver.py +0 -0
  98. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/ollama_driver.py +0 -0
  99. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/openai_driver.py +0 -0
  100. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/registry.py +0 -0
  101. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/vision_helpers.py +0 -0
  102. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/drivers/zai_driver.py +0 -0
  103. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/field_definitions.py +0 -0
  104. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/group_types.py +0 -0
  105. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/groups.py +0 -0
  106. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/image.py +0 -0
  107. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/ledger.py +0 -0
  108. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/logging.py +0 -0
  109. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/model_rates.py +0 -0
  110. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/persistence.py +0 -0
  111. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/persona.py +0 -0
  112. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/runner.py +0 -0
  113. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/scaffold/__init__.py +0 -0
  114. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/scaffold/generator.py +0 -0
  115. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/scaffold/templates/Dockerfile.j2 +0 -0
  116. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/scaffold/templates/README.md.j2 +0 -0
  117. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/scaffold/templates/config.py.j2 +0 -0
  118. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/scaffold/templates/env.example.j2 +0 -0
  119. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/scaffold/templates/main.py.j2 +0 -0
  120. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/scaffold/templates/models.py.j2 +0 -0
  121. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/scaffold/templates/requirements.txt.j2 +0 -0
  122. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/serialization.py +0 -0
  123. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/server.py +0 -0
  124. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/session.py +0 -0
  125. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/settings.py +0 -0
  126. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/simulated_tools.py +0 -0
  127. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/tools.py +0 -0
  128. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/tools_schema.py +0 -0
  129. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture/validator.py +0 -0
  130. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture.egg-info/SOURCES.txt +0 -0
  131. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture.egg-info/dependency_links.txt +0 -0
  132. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture.egg-info/entry_points.txt +0 -0
  133. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture.egg-info/requires.txt +0 -0
  134. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/prompture.egg-info/top_level.txt +0 -0
  135. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/pyproject.toml +0 -0
  136. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/requirements.txt +0 -0
  137. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/setup.cfg +0 -0
  138. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/test.py +0 -0
  139. {prompture-0.0.47.dev2 → prompture-0.0.47.dev3}/test_version_diagnosis.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prompture
3
- Version: 0.0.47.dev2
3
+ Version: 0.0.47.dev3
4
4
  Summary: Ask LLMs to return structured JSON and run cross-model tests. API-first.
5
5
  Author-email: Juan Denis <juan@vene.co>
6
6
  License-Expression: MIT
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.0.47.dev2'
32
- __version_tuple__ = version_tuple = (0, 0, 47, 'dev2')
31
+ __version__ = version = '0.0.47.dev3'
32
+ __version_tuple__ = version_tuple = (0, 0, 47, 'dev3')
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -109,6 +109,9 @@ class AsyncConversation:
109
109
  self._max_tool_rounds = max_tool_rounds
110
110
  self._simulated_tools = simulated_tools
111
111
 
112
+ # Reasoning content from last response
113
+ self._last_reasoning: str | None = None
114
+
112
115
  # Persistence
113
116
  self._conversation_id = conversation_id or str(uuid.uuid4())
114
117
  self._auto_save = Path(auto_save) if auto_save else None
@@ -121,6 +124,11 @@ class AsyncConversation:
121
124
  # Public helpers
122
125
  # ------------------------------------------------------------------
123
126
 
127
+ @property
128
+ def last_reasoning(self) -> str | None:
129
+ """The reasoning/thinking content from the last LLM response, if any."""
130
+ return self._last_reasoning
131
+
124
132
  @property
125
133
  def messages(self) -> list[dict[str, Any]]:
126
134
  """Read-only view of the conversation history."""
@@ -326,6 +334,8 @@ class AsyncConversation:
326
334
  If tools are registered and the driver supports tool use,
327
335
  dispatches to the async tool execution loop.
328
336
  """
337
+ self._last_reasoning = None
338
+
329
339
  # Route to appropriate tool handling
330
340
  if self._tools:
331
341
  use_native = getattr(self._driver, "supports_tool_use", False)
@@ -340,6 +350,7 @@ class AsyncConversation:
340
350
 
341
351
  text = resp.get("text", "")
342
352
  meta = resp.get("meta", {})
353
+ self._last_reasoning = resp.get("reasoning_content")
343
354
 
344
355
  user_content = self._build_content_with_images(content, images)
345
356
  self._messages.append({"role": "user", "content": user_content})
@@ -372,6 +383,7 @@ class AsyncConversation:
372
383
  text = resp.get("text", "")
373
384
 
374
385
  if not tool_calls:
386
+ self._last_reasoning = resp.get("reasoning_content")
375
387
  self._messages.append({"role": "assistant", "content": text})
376
388
  return text
377
389
 
@@ -526,6 +538,8 @@ class AsyncConversation:
526
538
  images: list[ImageInput] | None = None,
527
539
  ) -> dict[str, Any]:
528
540
  """Send a message with schema enforcement and get structured JSON back (async)."""
541
+ self._last_reasoning = None
542
+
529
543
  merged = {**self._options, **(options or {})}
530
544
 
531
545
  schema_string = json.dumps(json_schema, indent=2)
@@ -563,6 +577,7 @@ class AsyncConversation:
563
577
 
564
578
  text = resp.get("text", "")
565
579
  meta = resp.get("meta", {})
580
+ self._last_reasoning = resp.get("reasoning_content")
566
581
 
567
582
  user_content = self._build_content_with_images(content, images)
568
583
  self._messages.append({"role": "user", "content": user_content})
@@ -597,6 +612,7 @@ class AsyncConversation:
597
612
  "json_object": json_obj,
598
613
  "usage": usage,
599
614
  "output_format": output_format,
615
+ "reasoning": self._last_reasoning,
600
616
  }
601
617
 
602
618
  if output_format == "toon":
@@ -112,6 +112,9 @@ class Conversation:
112
112
  self._max_tool_rounds = max_tool_rounds
113
113
  self._simulated_tools = simulated_tools
114
114
 
115
+ # Reasoning content from last response
116
+ self._last_reasoning: str | None = None
117
+
115
118
  # Persistence
116
119
  self._conversation_id = conversation_id or str(uuid.uuid4())
117
120
  self._auto_save = Path(auto_save) if auto_save else None
@@ -124,6 +127,11 @@ class Conversation:
124
127
  # Public helpers
125
128
  # ------------------------------------------------------------------
126
129
 
130
+ @property
131
+ def last_reasoning(self) -> str | None:
132
+ """The reasoning/thinking content from the last LLM response, if any."""
133
+ return self._last_reasoning
134
+
127
135
  @property
128
136
  def messages(self) -> list[dict[str, Any]]:
129
137
  """Read-only view of the conversation history."""
@@ -340,6 +348,8 @@ class Conversation:
340
348
  images: Optional list of images to include (bytes, path, URL,
341
349
  base64 string, or :class:`ImageContent`).
342
350
  """
351
+ self._last_reasoning = None
352
+
343
353
  # Route to appropriate tool handling
344
354
  if self._tools:
345
355
  use_native = getattr(self._driver, "supports_tool_use", False)
@@ -354,6 +364,7 @@ class Conversation:
354
364
 
355
365
  text = resp.get("text", "")
356
366
  meta = resp.get("meta", {})
367
+ self._last_reasoning = resp.get("reasoning_content")
357
368
 
358
369
  # Record in history — store content with images for context
359
370
  user_content = self._build_content_with_images(content, images)
@@ -389,6 +400,7 @@ class Conversation:
389
400
 
390
401
  if not tool_calls:
391
402
  # No tool calls -> final response
403
+ self._last_reasoning = resp.get("reasoning_content")
392
404
  self._messages.append({"role": "assistant", "content": text})
393
405
  return text
394
406
 
@@ -553,6 +565,8 @@ class Conversation:
553
565
  context clean for subsequent turns.
554
566
  """
555
567
 
568
+ self._last_reasoning = None
569
+
556
570
  merged = {**self._options, **(options or {})}
557
571
 
558
572
  # Build the full prompt with schema instructions inline (handled by ask_for_json)
@@ -594,6 +608,7 @@ class Conversation:
594
608
 
595
609
  text = resp.get("text", "")
596
610
  meta = resp.get("meta", {})
611
+ self._last_reasoning = resp.get("reasoning_content")
597
612
 
598
613
  # Store original content (without schema boilerplate) for cleaner context
599
614
  # Include images in history so subsequent turns can reference them
@@ -632,6 +647,7 @@ class Conversation:
632
647
  "json_object": json_obj,
633
648
  "usage": usage,
634
649
  "output_format": output_format,
650
+ "reasoning": self._last_reasoning,
635
651
  }
636
652
 
637
653
  if output_format == "toon":
@@ -95,8 +95,17 @@ class AsyncGrokDriver(CostMixin, AsyncDriver):
95
95
  "model_name": model,
96
96
  }
97
97
 
98
- text = resp["choices"][0]["message"]["content"]
99
- return {"text": text, "meta": meta}
98
+ message = resp["choices"][0]["message"]
99
+ text = message.get("content") or ""
100
+ reasoning_content = message.get("reasoning_content")
101
+
102
+ if not text and reasoning_content:
103
+ text = reasoning_content
104
+
105
+ result: dict[str, Any] = {"text": text, "meta": meta}
106
+ if reasoning_content is not None:
107
+ result["reasoning_content"] = reasoning_content
108
+ return result
100
109
 
101
110
  # ------------------------------------------------------------------
102
111
  # Tool use
@@ -173,15 +182,20 @@ class AsyncGrokDriver(CostMixin, AsyncDriver):
173
182
  args = json.loads(tc["function"]["arguments"])
174
183
  except (json.JSONDecodeError, TypeError):
175
184
  args = {}
176
- tool_calls_out.append({
177
- "id": tc["id"],
178
- "name": tc["function"]["name"],
179
- "arguments": args,
180
- })
181
-
182
- return {
185
+ tool_calls_out.append(
186
+ {
187
+ "id": tc["id"],
188
+ "name": tc["function"]["name"],
189
+ "arguments": args,
190
+ }
191
+ )
192
+
193
+ result: dict[str, Any] = {
183
194
  "text": text,
184
195
  "meta": meta,
185
196
  "tool_calls": tool_calls_out,
186
197
  "stop_reason": stop_reason,
187
198
  }
199
+ if choice["message"].get("reasoning_content") is not None:
200
+ result["reasoning_content"] = choice["message"]["reasoning_content"]
201
+ return result
@@ -88,8 +88,16 @@ class AsyncGroqDriver(CostMixin, AsyncDriver):
88
88
  "model_name": model,
89
89
  }
90
90
 
91
- text = resp.choices[0].message.content
92
- return {"text": text, "meta": meta}
91
+ text = resp.choices[0].message.content or ""
92
+ reasoning_content = getattr(resp.choices[0].message, "reasoning_content", None)
93
+
94
+ if not text and reasoning_content:
95
+ text = reasoning_content
96
+
97
+ result: dict[str, Any] = {"text": text, "meta": meta}
98
+ if reasoning_content is not None:
99
+ result["reasoning_content"] = reasoning_content
100
+ return result
93
101
 
94
102
  # ------------------------------------------------------------------
95
103
  # Tool use
@@ -152,15 +160,21 @@ class AsyncGroqDriver(CostMixin, AsyncDriver):
152
160
  args = json.loads(tc.function.arguments)
153
161
  except (json.JSONDecodeError, TypeError):
154
162
  args = {}
155
- tool_calls_out.append({
156
- "id": tc.id,
157
- "name": tc.function.name,
158
- "arguments": args,
159
- })
160
-
161
- return {
163
+ tool_calls_out.append(
164
+ {
165
+ "id": tc.id,
166
+ "name": tc.function.name,
167
+ "arguments": args,
168
+ }
169
+ )
170
+
171
+ result: dict[str, Any] = {
162
172
  "text": text,
163
173
  "meta": meta,
164
174
  "tool_calls": tool_calls_out,
165
175
  "stop_reason": stop_reason,
166
176
  }
177
+ reasoning_content = getattr(choice.message, "reasoning_content", None)
178
+ if reasoning_content is not None:
179
+ result["reasoning_content"] = reasoning_content
180
+ return result
@@ -98,7 +98,12 @@ class AsyncLMStudioDriver(AsyncDriver):
98
98
  if "choices" not in response_data or not response_data["choices"]:
99
99
  raise ValueError(f"Unexpected response format: {response_data}")
100
100
 
101
- text = response_data["choices"][0]["message"]["content"]
101
+ message = response_data["choices"][0]["message"]
102
+ text = message.get("content") or ""
103
+ reasoning_content = message.get("reasoning_content")
104
+
105
+ if not text and reasoning_content:
106
+ text = reasoning_content
102
107
 
103
108
  usage = response_data.get("usage", {})
104
109
  prompt_tokens = usage.get("prompt_tokens", 0)
@@ -114,7 +119,10 @@ class AsyncLMStudioDriver(AsyncDriver):
114
119
  "model_name": merged_options.get("model", self.model),
115
120
  }
116
121
 
117
- return {"text": text, "meta": meta}
122
+ result: dict[str, Any] = {"text": text, "meta": meta}
123
+ if reasoning_content is not None:
124
+ result["reasoning_content"] = reasoning_content
125
+ return result
118
126
 
119
127
  # -- Model management (LM Studio 0.4.0+) ----------------------------------
120
128
 
@@ -138,10 +138,11 @@ class AsyncMoonshotDriver(CostMixin, AsyncDriver):
138
138
 
139
139
  message = resp["choices"][0]["message"]
140
140
  text = message.get("content") or ""
141
+ reasoning_content = message.get("reasoning_content")
141
142
 
142
143
  # Reasoning models may return content in reasoning_content when content is empty
143
- if not text and message.get("reasoning_content"):
144
- text = message["reasoning_content"]
144
+ if not text and reasoning_content:
145
+ text = reasoning_content
145
146
 
146
147
  # Structured output fallback: if we used json_schema mode and got an
147
148
  # empty response, retry with json_object mode and schema in the prompt.
@@ -184,8 +185,9 @@ class AsyncMoonshotDriver(CostMixin, AsyncDriver):
184
185
  resp = fb_resp
185
186
  fb_message = fb_resp["choices"][0]["message"]
186
187
  text = fb_message.get("content") or ""
187
- if not text and fb_message.get("reasoning_content"):
188
- text = fb_message["reasoning_content"]
188
+ reasoning_content = fb_message.get("reasoning_content")
189
+ if not text and reasoning_content:
190
+ text = reasoning_content
189
191
 
190
192
  total_cost = self._calculate_cost("moonshot", model, prompt_tokens, completion_tokens)
191
193
 
@@ -198,7 +200,10 @@ class AsyncMoonshotDriver(CostMixin, AsyncDriver):
198
200
  "model_name": model,
199
201
  }
200
202
 
201
- return {"text": text, "meta": meta}
203
+ result: dict[str, Any] = {"text": text, "meta": meta}
204
+ if reasoning_content is not None:
205
+ result["reasoning_content"] = reasoning_content
206
+ return result
202
207
 
203
208
  # ------------------------------------------------------------------
204
209
  # Tool use
@@ -334,6 +339,7 @@ class AsyncMoonshotDriver(CostMixin, AsyncDriver):
334
339
  data["temperature"] = opts["temperature"]
335
340
 
336
341
  full_text = ""
342
+ full_reasoning = ""
337
343
  prompt_tokens = 0
338
344
  completion_tokens = 0
339
345
 
@@ -368,9 +374,11 @@ class AsyncMoonshotDriver(CostMixin, AsyncDriver):
368
374
  if choices:
369
375
  delta = choices[0].get("delta", {})
370
376
  content = delta.get("content") or ""
371
- # Reasoning models stream thinking via reasoning_content
372
- if not content:
373
- content = delta.get("reasoning_content") or ""
377
+ reasoning_chunk = delta.get("reasoning_content") or ""
378
+ if reasoning_chunk:
379
+ full_reasoning += reasoning_chunk
380
+ if not content and reasoning_chunk:
381
+ content = reasoning_chunk
374
382
  if content:
375
383
  full_text += content
376
384
  yield {"type": "delta", "text": content}
@@ -378,7 +386,7 @@ class AsyncMoonshotDriver(CostMixin, AsyncDriver):
378
386
  total_tokens = prompt_tokens + completion_tokens
379
387
  total_cost = self._calculate_cost("moonshot", model, prompt_tokens, completion_tokens)
380
388
 
381
- yield {
389
+ done_chunk: dict[str, Any] = {
382
390
  "type": "done",
383
391
  "text": full_text,
384
392
  "meta": {
@@ -390,3 +398,6 @@ class AsyncMoonshotDriver(CostMixin, AsyncDriver):
390
398
  "model_name": model,
391
399
  },
392
400
  }
401
+ if full_reasoning:
402
+ done_chunk["reasoning_content"] = full_reasoning
403
+ yield done_chunk
@@ -122,8 +122,17 @@ class AsyncOpenRouterDriver(CostMixin, AsyncDriver):
122
122
  "model_name": model,
123
123
  }
124
124
 
125
- text = resp["choices"][0]["message"]["content"]
126
- return {"text": text, "meta": meta}
125
+ message = resp["choices"][0]["message"]
126
+ text = message.get("content") or ""
127
+ reasoning_content = message.get("reasoning_content")
128
+
129
+ if not text and reasoning_content:
130
+ text = reasoning_content
131
+
132
+ result: dict[str, Any] = {"text": text, "meta": meta}
133
+ if reasoning_content is not None:
134
+ result["reasoning_content"] = reasoning_content
135
+ return result
127
136
 
128
137
  # ------------------------------------------------------------------
129
138
  # Tool use
@@ -196,18 +205,23 @@ class AsyncOpenRouterDriver(CostMixin, AsyncDriver):
196
205
  args = json.loads(tc["function"]["arguments"])
197
206
  except (json.JSONDecodeError, TypeError):
198
207
  args = {}
199
- tool_calls_out.append({
200
- "id": tc["id"],
201
- "name": tc["function"]["name"],
202
- "arguments": args,
203
- })
208
+ tool_calls_out.append(
209
+ {
210
+ "id": tc["id"],
211
+ "name": tc["function"]["name"],
212
+ "arguments": args,
213
+ }
214
+ )
204
215
 
205
- return {
216
+ result: dict[str, Any] = {
206
217
  "text": text,
207
218
  "meta": meta,
208
219
  "tool_calls": tool_calls_out,
209
220
  "stop_reason": stop_reason,
210
221
  }
222
+ if choice["message"].get("reasoning_content") is not None:
223
+ result["reasoning_content"] = choice["message"]["reasoning_content"]
224
+ return result
211
225
 
212
226
  # ------------------------------------------------------------------
213
227
  # Streaming
@@ -238,21 +252,25 @@ class AsyncOpenRouterDriver(CostMixin, AsyncDriver):
238
252
  data["temperature"] = opts["temperature"]
239
253
 
240
254
  full_text = ""
255
+ full_reasoning = ""
241
256
  prompt_tokens = 0
242
257
  completion_tokens = 0
243
258
 
244
- async with httpx.AsyncClient() as client, client.stream(
245
- "POST",
246
- f"{self.base_url}/chat/completions",
247
- headers=self.headers,
248
- json=data,
249
- timeout=120,
250
- ) as response:
259
+ async with (
260
+ httpx.AsyncClient() as client,
261
+ client.stream(
262
+ "POST",
263
+ f"{self.base_url}/chat/completions",
264
+ headers=self.headers,
265
+ json=data,
266
+ timeout=120,
267
+ ) as response,
268
+ ):
251
269
  response.raise_for_status()
252
270
  async for line in response.aiter_lines():
253
271
  if not line or not line.startswith("data: "):
254
272
  continue
255
- payload = line[len("data: "):]
273
+ payload = line[len("data: ") :]
256
274
  if payload.strip() == "[DONE]":
257
275
  break
258
276
  try:
@@ -270,6 +288,11 @@ class AsyncOpenRouterDriver(CostMixin, AsyncDriver):
270
288
  if choices:
271
289
  delta = choices[0].get("delta", {})
272
290
  content = delta.get("content", "")
291
+ reasoning_chunk = delta.get("reasoning_content") or ""
292
+ if reasoning_chunk:
293
+ full_reasoning += reasoning_chunk
294
+ if not content and reasoning_chunk:
295
+ content = reasoning_chunk
273
296
  if content:
274
297
  full_text += content
275
298
  yield {"type": "delta", "text": content}
@@ -277,7 +300,7 @@ class AsyncOpenRouterDriver(CostMixin, AsyncDriver):
277
300
  total_tokens = prompt_tokens + completion_tokens
278
301
  total_cost = self._calculate_cost("openrouter", model, prompt_tokens, completion_tokens)
279
302
 
280
- yield {
303
+ done_chunk: dict[str, Any] = {
281
304
  "type": "done",
282
305
  "text": full_text,
283
306
  "meta": {
@@ -289,3 +312,6 @@ class AsyncOpenRouterDriver(CostMixin, AsyncDriver):
289
312
  "model_name": model,
290
313
  },
291
314
  }
315
+ if full_reasoning:
316
+ done_chunk["reasoning_content"] = full_reasoning
317
+ yield done_chunk
@@ -154,8 +154,17 @@ class GrokDriver(CostMixin, Driver):
154
154
  "model_name": model,
155
155
  }
156
156
 
157
- text = resp["choices"][0]["message"]["content"]
158
- return {"text": text, "meta": meta}
157
+ message = resp["choices"][0]["message"]
158
+ text = message.get("content") or ""
159
+ reasoning_content = message.get("reasoning_content")
160
+
161
+ if not text and reasoning_content:
162
+ text = reasoning_content
163
+
164
+ result: dict[str, Any] = {"text": text, "meta": meta}
165
+ if reasoning_content is not None:
166
+ result["reasoning_content"] = reasoning_content
167
+ return result
159
168
 
160
169
  # ------------------------------------------------------------------
161
170
  # Tool use
@@ -227,15 +236,20 @@ class GrokDriver(CostMixin, Driver):
227
236
  args = json.loads(tc["function"]["arguments"])
228
237
  except (json.JSONDecodeError, TypeError):
229
238
  args = {}
230
- tool_calls_out.append({
231
- "id": tc["id"],
232
- "name": tc["function"]["name"],
233
- "arguments": args,
234
- })
235
-
236
- return {
239
+ tool_calls_out.append(
240
+ {
241
+ "id": tc["id"],
242
+ "name": tc["function"]["name"],
243
+ "arguments": args,
244
+ }
245
+ )
246
+
247
+ result: dict[str, Any] = {
237
248
  "text": text,
238
249
  "meta": meta,
239
250
  "tool_calls": tool_calls_out,
240
251
  "stop_reason": stop_reason,
241
252
  }
253
+ if choice["message"].get("reasoning_content") is not None:
254
+ result["reasoning_content"] = choice["message"]["reasoning_content"]
255
+ return result
@@ -122,8 +122,16 @@ class GroqDriver(CostMixin, Driver):
122
122
  }
123
123
 
124
124
  # Extract generated text
125
- text = resp.choices[0].message.content
126
- return {"text": text, "meta": meta}
125
+ text = resp.choices[0].message.content or ""
126
+ reasoning_content = getattr(resp.choices[0].message, "reasoning_content", None)
127
+
128
+ if not text and reasoning_content:
129
+ text = reasoning_content
130
+
131
+ result: dict[str, Any] = {"text": text, "meta": meta}
132
+ if reasoning_content is not None:
133
+ result["reasoning_content"] = reasoning_content
134
+ return result
127
135
 
128
136
  # ------------------------------------------------------------------
129
137
  # Tool use
@@ -186,15 +194,21 @@ class GroqDriver(CostMixin, Driver):
186
194
  args = json.loads(tc.function.arguments)
187
195
  except (json.JSONDecodeError, TypeError):
188
196
  args = {}
189
- tool_calls_out.append({
190
- "id": tc.id,
191
- "name": tc.function.name,
192
- "arguments": args,
193
- })
194
-
195
- return {
197
+ tool_calls_out.append(
198
+ {
199
+ "id": tc.id,
200
+ "name": tc.function.name,
201
+ "arguments": args,
202
+ }
203
+ )
204
+
205
+ result: dict[str, Any] = {
196
206
  "text": text,
197
207
  "meta": meta,
198
208
  "tool_calls": tool_calls_out,
199
209
  "stop_reason": stop_reason,
200
210
  }
211
+ reasoning_content = getattr(choice.message, "reasoning_content", None)
212
+ if reasoning_content is not None:
213
+ result["reasoning_content"] = reasoning_content
214
+ return result
@@ -123,7 +123,13 @@ class LMStudioDriver(Driver):
123
123
  raise RuntimeError(f"LM Studio request failed: {e}") from e
124
124
 
125
125
  # Extract text
126
- text = response_data["choices"][0]["message"]["content"]
126
+ message = response_data["choices"][0]["message"]
127
+ text = message.get("content") or ""
128
+ reasoning_content = message.get("reasoning_content")
129
+
130
+ # Reasoning models (e.g. DeepSeek R1) may return content in reasoning_content
131
+ if not text and reasoning_content:
132
+ text = reasoning_content
127
133
 
128
134
  # Meta info
129
135
  usage = response_data.get("usage", {})
@@ -140,7 +146,10 @@ class LMStudioDriver(Driver):
140
146
  "model_name": merged_options.get("model", self.model),
141
147
  }
142
148
 
143
- return {"text": text, "meta": meta}
149
+ result: dict[str, Any] = {"text": text, "meta": meta}
150
+ if reasoning_content is not None:
151
+ result["reasoning_content"] = reasoning_content
152
+ return result
144
153
 
145
154
  # -- Model management (LM Studio 0.4.0+) ----------------------------------
146
155