prompture 0.0.47.dev2__py3-none-any.whl → 0.0.48__py3-none-any.whl

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.
prompture/_version.py CHANGED
@@ -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.48'
32
+ __version_tuple__ = version_tuple = (0, 0, 48)
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":
prompture/conversation.py CHANGED
@@ -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":
@@ -99,6 +99,13 @@ class AsyncClaudeDriver(CostMixin, AsyncDriver):
99
99
  resp = await client.messages.create(**common_kwargs)
100
100
  text = resp.content[0].text
101
101
 
102
+ # Extract reasoning/thinking content from content blocks
103
+ reasoning_content = ClaudeDriver._extract_thinking(resp.content)
104
+
105
+ # Fallback: use reasoning as text if content is empty
106
+ if not text and reasoning_content:
107
+ text = reasoning_content
108
+
102
109
  prompt_tokens = resp.usage.input_tokens
103
110
  completion_tokens = resp.usage.output_tokens
104
111
  total_tokens = prompt_tokens + completion_tokens
@@ -114,7 +121,10 @@ class AsyncClaudeDriver(CostMixin, AsyncDriver):
114
121
  "model_name": model,
115
122
  }
116
123
 
117
- return {"text": text, "meta": meta}
124
+ result: dict[str, Any] = {"text": text, "meta": meta}
125
+ if reasoning_content is not None:
126
+ result["reasoning_content"] = reasoning_content
127
+ return result
118
128
 
119
129
  # ------------------------------------------------------------------
120
130
  # Helpers
@@ -211,12 +221,17 @@ class AsyncClaudeDriver(CostMixin, AsyncDriver):
211
221
  "arguments": block.input,
212
222
  })
213
223
 
214
- return {
224
+ reasoning_content = ClaudeDriver._extract_thinking(resp.content)
225
+
226
+ result: dict[str, Any] = {
215
227
  "text": text,
216
228
  "meta": meta,
217
229
  "tool_calls": tool_calls_out,
218
230
  "stop_reason": resp.stop_reason,
219
231
  }
232
+ if reasoning_content is not None:
233
+ result["reasoning_content"] = reasoning_content
234
+ return result
220
235
 
221
236
  # ------------------------------------------------------------------
222
237
  # Streaming
@@ -247,6 +262,7 @@ class AsyncClaudeDriver(CostMixin, AsyncDriver):
247
262
  kwargs["system"] = system_content
248
263
 
249
264
  full_text = ""
265
+ full_reasoning = ""
250
266
  prompt_tokens = 0
251
267
  completion_tokens = 0
252
268
 
@@ -254,10 +270,16 @@ class AsyncClaudeDriver(CostMixin, AsyncDriver):
254
270
  async for event in stream:
255
271
  if hasattr(event, "type"):
256
272
  if event.type == "content_block_delta" and hasattr(event, "delta"):
257
- delta_text = getattr(event.delta, "text", "")
258
- if delta_text:
259
- full_text += delta_text
260
- yield {"type": "delta", "text": delta_text}
273
+ delta_type = getattr(event.delta, "type", "")
274
+ if delta_type == "thinking_delta":
275
+ thinking_text = getattr(event.delta, "thinking", "")
276
+ if thinking_text:
277
+ full_reasoning += thinking_text
278
+ else:
279
+ delta_text = getattr(event.delta, "text", "")
280
+ if delta_text:
281
+ full_text += delta_text
282
+ yield {"type": "delta", "text": delta_text}
261
283
  elif event.type == "message_delta" and hasattr(event, "usage"):
262
284
  completion_tokens = getattr(event.usage, "output_tokens", 0)
263
285
  elif event.type == "message_start" and hasattr(event, "message"):
@@ -268,7 +290,7 @@ class AsyncClaudeDriver(CostMixin, AsyncDriver):
268
290
  total_tokens = prompt_tokens + completion_tokens
269
291
  total_cost = self._calculate_cost("claude", model, prompt_tokens, completion_tokens)
270
292
 
271
- yield {
293
+ done_chunk: dict[str, Any] = {
272
294
  "type": "done",
273
295
  "text": full_text,
274
296
  "meta": {
@@ -280,3 +302,6 @@ class AsyncClaudeDriver(CostMixin, AsyncDriver):
280
302
  "model_name": model,
281
303
  },
282
304
  }
305
+ if full_reasoning:
306
+ done_chunk["reasoning_content"] = full_reasoning
307
+ yield done_chunk
@@ -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
@@ -81,7 +81,16 @@ class AsyncOllamaDriver(AsyncDriver):
81
81
  "model_name": merged_options.get("model", self.model),
82
82
  }
83
83
 
84
- return {"text": response_data.get("response", ""), "meta": meta}
84
+ text = response_data.get("response", "")
85
+ reasoning_content = response_data.get("thinking") or None
86
+
87
+ if not text and reasoning_content:
88
+ text = reasoning_content
89
+
90
+ result: dict[str, Any] = {"text": text, "meta": meta}
91
+ if reasoning_content is not None:
92
+ result["reasoning_content"] = reasoning_content
93
+ return result
85
94
 
86
95
  # ------------------------------------------------------------------
87
96
  # Tool use
@@ -139,8 +148,12 @@ class AsyncOllamaDriver(AsyncDriver):
139
148
 
140
149
  message = response_data.get("message", {})
141
150
  text = message.get("content") or ""
151
+ reasoning_content = message.get("thinking") or None
142
152
  stop_reason = response_data.get("done_reason", "stop")
143
153
 
154
+ if not text and reasoning_content:
155
+ text = reasoning_content
156
+
144
157
  tool_calls_out: list[dict[str, Any]] = []
145
158
  for tc in message.get("tool_calls", []):
146
159
  func = tc.get("function", {})
@@ -158,12 +171,15 @@ class AsyncOllamaDriver(AsyncDriver):
158
171
  "arguments": args,
159
172
  })
160
173
 
161
- return {
174
+ result: dict[str, Any] = {
162
175
  "text": text,
163
176
  "meta": meta,
164
177
  "tool_calls": tool_calls_out,
165
178
  "stop_reason": stop_reason,
166
179
  }
180
+ if reasoning_content is not None:
181
+ result["reasoning_content"] = reasoning_content
182
+ return result
167
183
 
168
184
  async def generate_messages(self, messages: list[dict[str, str]], options: dict[str, Any]) -> dict[str, Any]:
169
185
  """Use Ollama's /api/chat endpoint for multi-turn conversations."""
@@ -217,4 +233,12 @@ class AsyncOllamaDriver(AsyncDriver):
217
233
 
218
234
  message = response_data.get("message", {})
219
235
  text = message.get("content", "")
220
- return {"text": text, "meta": meta}
236
+ reasoning_content = message.get("thinking") or None
237
+
238
+ if not text and reasoning_content:
239
+ text = reasoning_content
240
+
241
+ result: dict[str, Any] = {"text": text, "meta": meta}
242
+ if reasoning_content is not None:
243
+ result["reasoning_content"] = reasoning_content
244
+ return result
@@ -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