mycode-sdk 0.9.0__tar.gz → 0.9.2__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 (23) hide show
  1. {mycode_sdk-0.9.0 → mycode_sdk-0.9.2}/PKG-INFO +6 -6
  2. {mycode_sdk-0.9.0 → mycode_sdk-0.9.2}/pyproject.toml +6 -6
  3. {mycode_sdk-0.9.0 → mycode_sdk-0.9.2}/src/mycode/__init__.py +1 -8
  4. {mycode_sdk-0.9.0 → mycode_sdk-0.9.2}/src/mycode/agent.py +79 -87
  5. {mycode_sdk-0.9.0 → mycode_sdk-0.9.2}/src/mycode/attachments.py +3 -2
  6. {mycode_sdk-0.9.0 → mycode_sdk-0.9.2}/src/mycode/models_catalog.json +56 -112
  7. {mycode_sdk-0.9.0 → mycode_sdk-0.9.2}/src/mycode/providers/anthropic_like.py +47 -49
  8. {mycode_sdk-0.9.0 → mycode_sdk-0.9.2}/src/mycode/providers/base.py +6 -0
  9. {mycode_sdk-0.9.0 → mycode_sdk-0.9.2}/src/mycode/providers/gemini.py +2 -3
  10. {mycode_sdk-0.9.0 → mycode_sdk-0.9.2}/src/mycode/providers/openai_chat.py +15 -5
  11. {mycode_sdk-0.9.0 → mycode_sdk-0.9.2}/src/mycode/providers/openai_responses.py +4 -3
  12. {mycode_sdk-0.9.0 → mycode_sdk-0.9.2}/src/mycode/session.py +11 -10
  13. {mycode_sdk-0.9.0 → mycode_sdk-0.9.2}/src/mycode/tools.py +10 -12
  14. {mycode_sdk-0.9.0 → mycode_sdk-0.9.2}/.gitignore +0 -0
  15. {mycode_sdk-0.9.0 → mycode_sdk-0.9.2}/LICENSE +0 -0
  16. {mycode_sdk-0.9.0 → mycode_sdk-0.9.2}/README.md +0 -0
  17. {mycode_sdk-0.9.0 → mycode_sdk-0.9.2}/src/mycode/compact.py +0 -0
  18. {mycode_sdk-0.9.0 → mycode_sdk-0.9.2}/src/mycode/hooks.py +0 -0
  19. {mycode_sdk-0.9.0 → mycode_sdk-0.9.2}/src/mycode/messages.py +0 -0
  20. {mycode_sdk-0.9.0 → mycode_sdk-0.9.2}/src/mycode/models.py +0 -0
  21. {mycode_sdk-0.9.0 → mycode_sdk-0.9.2}/src/mycode/providers/__init__.py +0 -0
  22. {mycode_sdk-0.9.0 → mycode_sdk-0.9.2}/src/mycode/py.typed +0 -0
  23. {mycode_sdk-0.9.0 → mycode_sdk-0.9.2}/src/mycode/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mycode-sdk
3
- Version: 0.9.0
3
+ Version: 0.9.2
4
4
  Summary: Lightweight Python SDK for building AI agents.
5
5
  Project-URL: Homepage, https://github.com/legibet/mycode
6
6
  Project-URL: Repository, https://github.com/legibet/mycode
@@ -18,11 +18,11 @@ Classifier: Programming Language :: Python :: 3.12
18
18
  Classifier: Programming Language :: Python :: 3.13
19
19
  Classifier: Topic :: Software Development
20
20
  Requires-Python: >=3.12
21
- Requires-Dist: anthropic>=0.102.0
22
- Requires-Dist: google-genai>=2.3.0
23
- Requires-Dist: griffelib>=2.0.2
24
- Requires-Dist: openai>=2.36.0
25
- Requires-Dist: pydantic>=2.13.4
21
+ Requires-Dist: anthropic>=0.106.0
22
+ Requires-Dist: google-genai>=2.8.0
23
+ Requires-Dist: griffelib>=2.0.0
24
+ Requires-Dist: openai>=2.41.0
25
+ Requires-Dist: pydantic>=2.13.0
26
26
  Description-Content-Type: text/markdown
27
27
 
28
28
  # mycode-sdk
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "mycode-sdk"
7
- version = "0.9.0"
7
+ version = "0.9.2"
8
8
  description = "Lightweight Python SDK for building AI agents."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -23,11 +23,11 @@ classifiers = [
23
23
  ]
24
24
  keywords = ["agent", "llm", "anthropic", "openai", "gemini", "sdk"]
25
25
  dependencies = [
26
- "anthropic>=0.102.0",
27
- "google-genai>=2.3.0",
28
- "griffelib>=2.0.2",
29
- "openai>=2.36.0",
30
- "pydantic>=2.13.4",
26
+ "anthropic>=0.106.0",
27
+ "google-genai>=2.8.0",
28
+ "griffelib>=2.0.0",
29
+ "openai>=2.41.0",
30
+ "pydantic>=2.13.0",
31
31
  ]
32
32
 
33
33
  [project.urls]
@@ -1,11 +1,4 @@
1
- """mycode — multi-turn tool-calling agent runtime.
2
-
3
- Public API for embedding the agent loop in other Python applications. The
4
- runtime ships four built-in coding tools (``read``, ``write``, ``edit``,
5
- ``bash``) exposed as :data:`read_tool`, :data:`write_tool`, :data:`edit_tool`,
6
- :data:`bash_tool` — pick the ones you want via ``tools=[...]`` rather than
7
- silently exposing file system and shell access.
8
- """
1
+ """mycode — multi-turn tool-calling agent runtime."""
9
2
 
10
3
  from importlib import metadata
11
4
 
@@ -13,6 +13,7 @@ import os
13
13
  import tempfile
14
14
  import time
15
15
  from collections.abc import AsyncIterator, Awaitable, Callable, Sequence
16
+ from contextlib import suppress
16
17
  from dataclasses import dataclass, field
17
18
  from pathlib import Path
18
19
  from typing import Any, cast
@@ -232,18 +233,12 @@ class Agent:
232
233
  yield Event("tool_start", {"tool_call": {"id": tool_id, "name": name, "input": args}})
233
234
 
234
235
  if self._cancel_event.is_set():
235
- yield self._tool_done_event(
236
- tool_id,
237
- ToolExecutionResult(output="error: cancelled", is_error=True),
238
- )
236
+ yield self._error_done(tool_id, "error: cancelled")
239
237
  return
240
238
 
241
239
  spec = self.tools.get(name)
242
240
  if spec is None:
243
- yield self._tool_done_event(
244
- tool_id,
245
- ToolExecutionResult(output=f"error: unknown tool: {name}", is_error=True),
246
- )
241
+ yield self._error_done(tool_id, f"error: unknown tool: {name}")
247
242
  return
248
243
 
249
244
  hook_ctx = ToolHookContext(
@@ -259,10 +254,7 @@ class Agent:
259
254
  try:
260
255
  result = await self.hooks.run_before_tool(hook_ctx)
261
256
  except Exception as exc:
262
- yield self._tool_done_event(
263
- tool_id,
264
- ToolExecutionResult(output=f"error: tool hook failed: {exc}", is_error=True),
265
- )
257
+ yield self._error_done(tool_id, f"error: tool hook failed: {exc}")
266
258
  return
267
259
 
268
260
  if result is not None:
@@ -270,10 +262,7 @@ class Agent:
270
262
  return
271
263
 
272
264
  if self._cancel_event.is_set():
273
- yield self._tool_done_event(
274
- tool_id,
275
- ToolExecutionResult(output="error: cancelled", is_error=True),
276
- )
265
+ yield self._error_done(tool_id, "error: cancelled")
277
266
  return
278
267
 
279
268
  if spec.streams_output:
@@ -336,13 +325,10 @@ class Agent:
336
325
  yield Event("tool_output", {"tool_use_id": tool_id, "output": output})
337
326
 
338
327
  if was_cancelled:
339
- try:
328
+ with suppress(Exception):
340
329
  await task
341
- except Exception:
342
- pass
343
330
  output = "\n".join([*output_parts, "error: cancelled"]) if output_parts else "error: cancelled"
344
- result = ToolExecutionResult(output=output, is_error=True)
345
- yield self._tool_done_event(tool_id, result)
331
+ yield self._error_done(tool_id, output)
346
332
  return
347
333
  else:
348
334
  try:
@@ -398,6 +384,11 @@ class Agent:
398
384
  data["content"] = result.content
399
385
  return Event("tool_done", data)
400
386
 
387
+ def _error_done(self, tool_id: str, message: str) -> Event:
388
+ """Build a tool_done event carrying an error result."""
389
+
390
+ return self._tool_done_event(tool_id, ToolExecutionResult(output=message, is_error=True))
391
+
401
392
  # ------------------------------------------------------------------
402
393
  # Provider streaming
403
394
  # ------------------------------------------------------------------
@@ -429,10 +420,52 @@ class Agent:
429
420
  finally:
430
421
  close = cast(Callable[[], Awaitable[None]] | None, getattr(provider_stream, "aclose", None))
431
422
  if close is not None:
432
- try:
423
+ with suppress(Exception):
433
424
  await close()
434
- except Exception:
435
- pass
425
+
426
+ def _build_request(
427
+ self,
428
+ *,
429
+ tools: list[dict[str, Any]] | None = None,
430
+ max_tokens: int | None = None,
431
+ reasoning_effort: str | None = None,
432
+ append_messages: Sequence[ConversationMessage] = (),
433
+ ) -> ProviderRequest:
434
+ """Build a ProviderRequest from the agent's current state."""
435
+
436
+ return ProviderRequest(
437
+ provider=self.provider,
438
+ model=self.model,
439
+ session_id=self.session_id,
440
+ messages=self.messages,
441
+ system=self.system,
442
+ tools=self.tools.definitions if tools is None else tools,
443
+ max_tokens=self.max_tokens if max_tokens is None else max_tokens,
444
+ temperature=self.temperature,
445
+ api_key=self.api_key,
446
+ api_base=self.api_base,
447
+ reasoning_effort=reasoning_effort,
448
+ supports_image_input=self.supports_image_input,
449
+ supports_pdf_input=self.supports_pdf_input,
450
+ transcript_path=self.transcript_path,
451
+ append_messages=list(append_messages),
452
+ )
453
+
454
+ @staticmethod
455
+ def _elapsed_ms(start: float) -> int:
456
+ return max(0, int((time.monotonic() - start) * 1000))
457
+
458
+ @staticmethod
459
+ def _stamp_thinking_duration(content: list[Any], duration_ms: int) -> None:
460
+ """Merge duration_ms onto the last thinking block in content, if any."""
461
+
462
+ for block in reversed(content):
463
+ if not isinstance(block, dict) or block.get("type") != "thinking":
464
+ continue
465
+ raw_meta = block.get("meta")
466
+ meta = raw_meta if isinstance(raw_meta, dict) else {}
467
+ block["meta"] = {**meta, "duration_ms": duration_ms}
468
+ return
436
469
 
437
470
  # ------------------------------------------------------------------
438
471
  # Public entry points
@@ -460,8 +493,9 @@ class Agent:
460
493
 
461
494
  self._cancel_event.clear()
462
495
 
496
+ user_message: ConversationMessage
463
497
  if isinstance(user_input, str):
464
- user_message: ConversationMessage = user_text_message(user_input)
498
+ user_message = user_text_message(user_input)
465
499
  else:
466
500
  if (user_input.get("role") or "user") != "user":
467
501
  yield Event("error", {"message": "user input must be a user message"})
@@ -479,16 +513,15 @@ class Agent:
479
513
  user_message["content"] = list(user_message.get("content") or []) + blocks
480
514
 
481
515
  content_blocks = user_message.get("content") or []
482
- if not self.supports_image_input and any(
483
- isinstance(block, dict) and block.get("type") == "image" for block in content_blocks
484
- ):
485
- yield Event("error", {"message": "current model does not support image input"})
486
- return
487
- if not self.supports_pdf_input and any(
488
- isinstance(block, dict) and block.get("type") == "document" for block in content_blocks
516
+ for block_type, supported, label in (
517
+ ("image", self.supports_image_input, "image input"),
518
+ ("document", self.supports_pdf_input, "PDF input"),
489
519
  ):
490
- yield Event("error", {"message": "current model does not support PDF input"})
491
- return
520
+ if not supported and any(
521
+ isinstance(block, dict) and block.get("type") == block_type for block in content_blocks
522
+ ):
523
+ yield Event("error", {"message": f"current model does not support {label}"})
524
+ return
492
525
 
493
526
  self.messages.append(user_message)
494
527
  await persist(user_message)
@@ -507,22 +540,7 @@ class Agent:
507
540
  thinking_started_at: float | None = None
508
541
  thinking_duration_ms: int | None = None
509
542
  provider_cancelled = False
510
- request = ProviderRequest(
511
- provider=self.provider,
512
- model=self.model,
513
- session_id=self.session_id,
514
- messages=self.messages,
515
- system=self.system,
516
- tools=self.tools.definitions,
517
- max_tokens=self.max_tokens,
518
- temperature=self.temperature,
519
- api_key=self.api_key,
520
- api_base=self.api_base,
521
- reasoning_effort=self.reasoning_effort,
522
- supports_image_input=self.supports_image_input,
523
- supports_pdf_input=self.supports_pdf_input,
524
- transcript_path=self.transcript_path,
525
- )
543
+ request = self._build_request(reasoning_effort=self.reasoning_effort)
526
544
 
527
545
  try:
528
546
  async for provider_event in self._stream_provider_turn(adapter, request):
@@ -546,7 +564,7 @@ class Agent:
546
564
  delta_text = str(provider_event.data.get("text") or "")
547
565
  if delta_text:
548
566
  if thinking_started_at is not None and thinking_duration_ms is None:
549
- thinking_duration_ms = max(0, int((time.monotonic() - thinking_started_at) * 1000))
567
+ thinking_duration_ms = self._elapsed_ms(thinking_started_at)
550
568
  yield Event("reasoning_done", {"duration_ms": thinking_duration_ms})
551
569
  if partial_content and partial_content[-1].get("type") == "text":
552
570
  partial_content[-1]["text"] = f"{partial_content[-1].get('text') or ''}{delta_text}"
@@ -562,7 +580,7 @@ class Agent:
562
580
  continue
563
581
 
564
582
  if thinking_started_at is not None and thinking_duration_ms is None:
565
- thinking_duration_ms = max(0, int((time.monotonic() - thinking_started_at) * 1000))
583
+ thinking_duration_ms = self._elapsed_ms(thinking_started_at)
566
584
  yield Event("reasoning_done", {"duration_ms": thinking_duration_ms})
567
585
 
568
586
  message = provider_event.data.get("message")
@@ -579,15 +597,9 @@ class Agent:
579
597
  if provider_cancelled:
580
598
  if partial_content:
581
599
  if thinking_started_at is not None and thinking_duration_ms is None:
582
- thinking_duration_ms = max(0, int((time.monotonic() - thinking_started_at) * 1000))
600
+ thinking_duration_ms = self._elapsed_ms(thinking_started_at)
583
601
  if thinking_duration_ms is not None:
584
- for block in reversed(partial_content):
585
- if block.get("type") != "thinking":
586
- continue
587
- raw_meta = block.get("meta")
588
- meta = cast(dict[str, Any], raw_meta) if isinstance(raw_meta, dict) else {}
589
- block["meta"] = {**meta, "duration_ms": thinking_duration_ms}
590
- break
602
+ self._stamp_thinking_duration(partial_content, thinking_duration_ms)
591
603
  cancelled_message = build_message(
592
604
  "assistant",
593
605
  [dict(block) for block in partial_content],
@@ -607,13 +619,7 @@ class Agent:
607
619
  return
608
620
 
609
621
  if thinking_duration_ms is not None:
610
- for block in reversed(assistant_message.get("content") or []):
611
- if not isinstance(block, dict) or block.get("type") != "thinking":
612
- continue
613
- raw_meta = block.get("meta")
614
- meta = cast(dict[str, Any], raw_meta) if isinstance(raw_meta, dict) else {}
615
- block["meta"] = {**meta, "duration_ms": thinking_duration_ms}
616
- break
622
+ self._stamp_thinking_duration(assistant_message.get("content") or [], thinking_duration_ms)
617
623
 
618
624
  # Stamp context_window onto the persisted assistant message so
619
625
  # rewinds and refreshed clients can render token-usage % without
@@ -649,16 +655,13 @@ class Agent:
649
655
  continue
650
656
 
651
657
  d = event.data
652
- output = str(d.get("output") or "")
653
- metadata = d.get("metadata") if isinstance(d.get("metadata"), dict) else None
654
- content = d.get("content")
655
658
  tool_results.append(
656
659
  tool_result_block(
657
- tool_use_id=str(d.get("tool_use_id") or ""),
658
- output=output,
659
- metadata=metadata,
660
- is_error=bool(d.get("is_error")),
661
- content=content if isinstance(content, list) else None,
660
+ tool_use_id=d["tool_use_id"],
661
+ output=d["output"],
662
+ metadata=d.get("metadata"),
663
+ is_error=d["is_error"],
664
+ content=d.get("content"),
662
665
  )
663
666
  )
664
667
 
@@ -735,20 +738,9 @@ class Agent:
735
738
  ) -> None:
736
739
  """Ask the provider for a summary, persist the compact event, append it."""
737
740
 
738
- request = ProviderRequest(
739
- provider=self.provider,
740
- model=self.model,
741
- session_id=self.session_id,
742
- messages=self.messages,
743
- system=self.system,
741
+ request = self._build_request(
744
742
  tools=[],
745
743
  max_tokens=min(self.max_tokens, 8192),
746
- temperature=self.temperature,
747
- api_key=self.api_key,
748
- api_base=self.api_base,
749
- supports_image_input=self.supports_image_input,
750
- supports_pdf_input=self.supports_pdf_input,
751
- transcript_path=self.transcript_path,
752
744
  append_messages=[user_text_message(COMPACT_SUMMARY_PROMPT)],
753
745
  )
754
746
 
@@ -6,6 +6,7 @@ same content blocks for the same input.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import builtins
9
10
  import html
10
11
  from base64 import b64encode
11
12
  from collections.abc import Sequence
@@ -29,7 +30,7 @@ class Attachment:
29
30
  bare ``Attachment("notes.txt")`` from silently becoming text instead of a path.
30
31
  """
31
32
 
32
- source: Path | bytes | str
33
+ source: Path | builtins.bytes | str
33
34
  media_type: str | None = None
34
35
  name: str | None = None
35
36
 
@@ -40,7 +41,7 @@ class Attachment:
40
41
  return cls(source=Path(path), name=name)
41
42
 
42
43
  @classmethod
43
- def bytes(cls, data: bytes, *, media_type: str, name: str | None = None) -> Self:
44
+ def bytes(cls, data: builtins.bytes, *, media_type: str, name: str | None = None) -> Self:
44
45
  """``media_type`` must be a supported image type or ``application/pdf``."""
45
46
 
46
47
  if not media_type:
@@ -56,6 +56,13 @@
56
56
  "supports_pdf_input": true,
57
57
  "supports_reasoning": false
58
58
  },
59
+ "claude-fable-5": {
60
+ "context_window": 1000000,
61
+ "max_output_tokens": 128000,
62
+ "supports_image_input": true,
63
+ "supports_pdf_input": true,
64
+ "supports_reasoning": true
65
+ },
59
66
  "claude-haiku-4-5": {
60
67
  "context_window": 200000,
61
68
  "max_output_tokens": 64000,
@@ -263,6 +270,13 @@
263
270
  "supports_pdf_input": true,
264
271
  "supports_reasoning": true
265
272
  },
273
+ "gemini-3-pro-image-preview": {
274
+ "context_window": 131072,
275
+ "max_output_tokens": 32768,
276
+ "supports_image_input": true,
277
+ "supports_pdf_input": false,
278
+ "supports_reasoning": true
279
+ },
266
280
  "gemini-3-pro-preview": {
267
281
  "context_window": 1048576,
268
282
  "max_output_tokens": 65536,
@@ -448,6 +462,13 @@
448
462
  "supports_image_input": true,
449
463
  "supports_pdf_input": false,
450
464
  "supports_reasoning": true
465
+ },
466
+ "kimi-k2.7-code": {
467
+ "context_window": 262144,
468
+ "max_output_tokens": 262144,
469
+ "supports_image_input": true,
470
+ "supports_pdf_input": false,
471
+ "supports_reasoning": true
451
472
  }
452
473
  },
453
474
  "openai": {
@@ -731,20 +752,6 @@
731
752
  "supports_pdf_input": true,
732
753
  "supports_reasoning": true
733
754
  },
734
- "o1-mini": {
735
- "context_window": 128000,
736
- "max_output_tokens": 65536,
737
- "supports_image_input": false,
738
- "supports_pdf_input": false,
739
- "supports_reasoning": true
740
- },
741
- "o1-preview": {
742
- "context_window": 128000,
743
- "max_output_tokens": 32768,
744
- "supports_image_input": false,
745
- "supports_pdf_input": false,
746
- "supports_reasoning": true
747
- },
748
755
  "o1-pro": {
749
756
  "context_window": 200000,
750
757
  "max_output_tokens": 100000,
@@ -915,6 +922,13 @@
915
922
  "supports_pdf_input": false,
916
923
  "supports_reasoning": false
917
924
  },
925
+ "anthropic/claude-fable-5": {
926
+ "context_window": 1000000,
927
+ "max_output_tokens": 128000,
928
+ "supports_image_input": true,
929
+ "supports_pdf_input": true,
930
+ "supports_reasoning": true
931
+ },
918
932
  "anthropic/claude-haiku-4.5": {
919
933
  "context_window": 200000,
920
934
  "max_output_tokens": 64000,
@@ -1013,20 +1027,6 @@
1013
1027
  "supports_pdf_input": false,
1014
1028
  "supports_reasoning": false
1015
1029
  },
1016
- "arcee-ai/maestro-reasoning": {
1017
- "context_window": 131072,
1018
- "max_output_tokens": 32000,
1019
- "supports_image_input": false,
1020
- "supports_pdf_input": false,
1021
- "supports_reasoning": false
1022
- },
1023
- "arcee-ai/spotlight": {
1024
- "context_window": 131072,
1025
- "max_output_tokens": 65537,
1026
- "supports_image_input": true,
1027
- "supports_pdf_input": false,
1028
- "supports_reasoning": false
1029
- },
1030
1030
  "arcee-ai/trinity-large-thinking": {
1031
1031
  "context_window": 262144,
1032
1032
  "max_output_tokens": 262144,
@@ -1048,13 +1048,6 @@
1048
1048
  "supports_pdf_input": false,
1049
1049
  "supports_reasoning": false
1050
1050
  },
1051
- "baidu/ernie-4.5-vl-28b-a3b": {
1052
- "context_window": 30000,
1053
- "max_output_tokens": 8000,
1054
- "supports_image_input": true,
1055
- "supports_pdf_input": false,
1056
- "supports_reasoning": true
1057
- },
1058
1051
  "baidu/ernie-4.5-vl-424b-a47b": {
1059
1052
  "context_window": 123000,
1060
1053
  "max_output_tokens": 16000,
@@ -1175,8 +1168,8 @@
1175
1168
  "supports_reasoning": true
1176
1169
  },
1177
1170
  "deepseek/deepseek-r1-distill-llama-70b": {
1178
- "context_window": 131072,
1179
- "max_output_tokens": 16384,
1171
+ "context_window": 8192,
1172
+ "max_output_tokens": 8192,
1180
1173
  "supports_image_input": false,
1181
1174
  "supports_pdf_input": false,
1182
1175
  "supports_reasoning": true
@@ -1210,7 +1203,7 @@
1210
1203
  "supports_reasoning": true
1211
1204
  },
1212
1205
  "deepseek/deepseek-v4-flash": {
1213
- "context_window": 1048576,
1206
+ "context_window": 1048575,
1214
1207
  "max_output_tokens": 131072,
1215
1208
  "supports_image_input": false,
1216
1209
  "supports_pdf_input": false,
@@ -1386,7 +1379,7 @@
1386
1379
  },
1387
1380
  "google/gemma-4-31b-it": {
1388
1381
  "context_window": 262144,
1389
- "max_output_tokens": 16384,
1382
+ "max_output_tokens": 262144,
1390
1383
  "supports_image_input": true,
1391
1384
  "supports_pdf_input": false,
1392
1385
  "supports_reasoning": true
@@ -1532,7 +1525,7 @@
1532
1525
  "supports_reasoning": false
1533
1526
  },
1534
1527
  "meta-llama/llama-3.1-8b-instruct": {
1535
- "context_window": 16384,
1528
+ "context_window": 131072,
1536
1529
  "max_output_tokens": 16384,
1537
1530
  "supports_image_input": false,
1538
1531
  "supports_pdf_input": false,
@@ -1594,13 +1587,6 @@
1594
1587
  "supports_pdf_input": false,
1595
1588
  "supports_reasoning": false
1596
1589
  },
1597
- "meta-llama/llama-guard-3-8b": {
1598
- "context_window": 131072,
1599
- "max_output_tokens": 131072,
1600
- "supports_image_input": false,
1601
- "supports_pdf_input": false,
1602
- "supports_reasoning": false
1603
- },
1604
1590
  "meta-llama/llama-guard-4-12b": {
1605
1591
  "context_window": 163840,
1606
1592
  "max_output_tokens": 16384,
@@ -1840,20 +1826,20 @@
1840
1826
  "supports_reasoning": true
1841
1827
  },
1842
1828
  "moonshotai/kimi-k2.5": {
1843
- "context_window": 262144,
1829
+ "context_window": 256000,
1844
1830
  "max_output_tokens": 262144,
1845
1831
  "supports_image_input": true,
1846
1832
  "supports_pdf_input": false,
1847
1833
  "supports_reasoning": true
1848
1834
  },
1849
1835
  "moonshotai/kimi-k2.6": {
1850
- "context_window": 262144,
1851
- "max_output_tokens": 262144,
1836
+ "context_window": 262142,
1837
+ "max_output_tokens": 262142,
1852
1838
  "supports_image_input": true,
1853
1839
  "supports_pdf_input": false,
1854
1840
  "supports_reasoning": true
1855
1841
  },
1856
- "moonshotai/kimi-k2.6:free": {
1842
+ "moonshotai/kimi-k2.7-code": {
1857
1843
  "context_window": 262144,
1858
1844
  "max_output_tokens": 262144,
1859
1845
  "supports_image_input": true,
@@ -1874,19 +1860,12 @@
1874
1860
  "supports_pdf_input": false,
1875
1861
  "supports_reasoning": false
1876
1862
  },
1877
- "nex-agi/deepseek-v3.1-nex-n1": {
1878
- "context_window": 131072,
1879
- "max_output_tokens": 163840,
1880
- "supports_image_input": false,
1881
- "supports_pdf_input": false,
1882
- "supports_reasoning": false
1883
- },
1884
- "nousresearch/hermes-2-pro-llama-3-8b": {
1885
- "context_window": 8192,
1886
- "max_output_tokens": 8192,
1887
- "supports_image_input": false,
1863
+ "nex-agi/nex-n2-pro:free": {
1864
+ "context_window": 262144,
1865
+ "max_output_tokens": 262144,
1866
+ "supports_image_input": true,
1888
1867
  "supports_pdf_input": false,
1889
- "supports_reasoning": false
1868
+ "supports_reasoning": true
1890
1869
  },
1891
1870
  "nousresearch/hermes-3-llama-3.1-405b": {
1892
1871
  "context_window": 131072,
@@ -1993,13 +1972,6 @@
1993
1972
  "supports_pdf_input": false,
1994
1973
  "supports_reasoning": true
1995
1974
  },
1996
- "nvidia/nemotron-nano-9b-v2": {
1997
- "context_window": 131072,
1998
- "max_output_tokens": 16384,
1999
- "supports_image_input": false,
2000
- "supports_pdf_input": false,
2001
- "supports_reasoning": true
2002
- },
2003
1975
  "nvidia/nemotron-nano-9b-v2:free": {
2004
1976
  "context_window": 128000,
2005
1977
  "max_output_tokens": 128000,
@@ -2042,13 +2014,6 @@
2042
2014
  "supports_pdf_input": false,
2043
2015
  "supports_reasoning": false
2044
2016
  },
2045
- "openai/gpt-4-1106-preview": {
2046
- "context_window": 128000,
2047
- "max_output_tokens": 4096,
2048
- "supports_image_input": false,
2049
- "supports_pdf_input": false,
2050
- "supports_reasoning": false
2051
- },
2052
2017
  "openai/gpt-4-turbo": {
2053
2018
  "context_window": 128000,
2054
2019
  "max_output_tokens": 4096,
@@ -2596,8 +2561,8 @@
2596
2561
  "supports_reasoning": true
2597
2562
  },
2598
2563
  "qwen/qwen2.5-vl-72b-instruct": {
2599
- "context_window": 32000,
2600
- "max_output_tokens": 8192,
2564
+ "context_window": 128000,
2565
+ "max_output_tokens": 128000,
2601
2566
  "supports_image_input": true,
2602
2567
  "supports_pdf_input": false,
2603
2568
  "supports_reasoning": false
@@ -2632,7 +2597,7 @@
2632
2597
  },
2633
2598
  "qwen/qwen3-30b-a3b": {
2634
2599
  "context_window": 40960,
2635
- "max_output_tokens": 20000,
2600
+ "max_output_tokens": 16384,
2636
2601
  "supports_image_input": false,
2637
2602
  "supports_pdf_input": false,
2638
2603
  "supports_reasoning": true
@@ -2821,7 +2786,7 @@
2821
2786
  },
2822
2787
  "qwen/qwen3.5-9b": {
2823
2788
  "context_window": 262144,
2824
- "max_output_tokens": 81920,
2789
+ "max_output_tokens": 262144,
2825
2790
  "supports_image_input": true,
2826
2791
  "supports_pdf_input": false,
2827
2792
  "supports_reasoning": true
@@ -2855,8 +2820,8 @@
2855
2820
  "supports_reasoning": true
2856
2821
  },
2857
2822
  "qwen/qwen3.6-35b-a3b": {
2858
- "context_window": 262140,
2859
- "max_output_tokens": 262140,
2823
+ "context_window": 262144,
2824
+ "max_output_tokens": 262144,
2860
2825
  "supports_image_input": true,
2861
2826
  "supports_pdf_input": false,
2862
2827
  "supports_reasoning": true
@@ -2924,13 +2889,6 @@
2924
2889
  "supports_pdf_input": false,
2925
2890
  "supports_reasoning": false
2926
2891
  },
2927
- "sao10k/l3-euryale-70b": {
2928
- "context_window": 8192,
2929
- "max_output_tokens": 8192,
2930
- "supports_image_input": false,
2931
- "supports_pdf_input": false,
2932
- "supports_reasoning": false
2933
- },
2934
2892
  "sao10k/l3-lunaris-8b": {
2935
2893
  "context_window": 8192,
2936
2894
  "max_output_tokens": 16384,
@@ -3092,13 +3050,6 @@
3092
3050
  "supports_pdf_input": false,
3093
3051
  "supports_reasoning": true
3094
3052
  },
3095
- "z-ai/glm-4-32b": {
3096
- "context_window": 128000,
3097
- "max_output_tokens": 128000,
3098
- "supports_image_input": false,
3099
- "supports_pdf_input": false,
3100
- "supports_reasoning": false
3101
- },
3102
3053
  "z-ai/glm-4.5": {
3103
3054
  "context_window": 131072,
3104
3055
  "max_output_tokens": 98304,
@@ -3113,13 +3064,6 @@
3113
3064
  "supports_pdf_input": false,
3114
3065
  "supports_reasoning": true
3115
3066
  },
3116
- "z-ai/glm-4.5-air:free": {
3117
- "context_window": 131072,
3118
- "max_output_tokens": 96000,
3119
- "supports_image_input": false,
3120
- "supports_pdf_input": false,
3121
- "supports_reasoning": true
3122
- },
3123
3067
  "z-ai/glm-4.5v": {
3124
3068
  "context_window": 65536,
3125
3069
  "max_output_tokens": 16384,
@@ -3136,7 +3080,7 @@
3136
3080
  },
3137
3081
  "z-ai/glm-4.6v": {
3138
3082
  "context_window": 131072,
3139
- "max_output_tokens": 24000,
3083
+ "max_output_tokens": 32768,
3140
3084
  "supports_image_input": true,
3141
3085
  "supports_pdf_input": false,
3142
3086
  "supports_reasoning": true
@@ -3163,7 +3107,7 @@
3163
3107
  "supports_reasoning": true
3164
3108
  },
3165
3109
  "z-ai/glm-5-turbo": {
3166
- "context_window": 202752,
3110
+ "context_window": 262144,
3167
3111
  "max_output_tokens": 131072,
3168
3112
  "supports_image_input": false,
3169
3113
  "supports_pdf_input": false,
@@ -3176,11 +3120,11 @@
3176
3120
  "supports_pdf_input": false,
3177
3121
  "supports_reasoning": true
3178
3122
  },
3179
- "z-ai/glm-5v-turbo": {
3180
- "context_window": 202752,
3181
- "max_output_tokens": 131072,
3123
+ "~anthropic/claude-fable-latest": {
3124
+ "context_window": 1000000,
3125
+ "max_output_tokens": 128000,
3182
3126
  "supports_image_input": true,
3183
- "supports_pdf_input": false,
3127
+ "supports_pdf_input": true,
3184
3128
  "supports_reasoning": true
3185
3129
  },
3186
3130
  "~anthropic/claude-haiku-latest": {
@@ -3219,8 +3163,8 @@
3219
3163
  "supports_reasoning": true
3220
3164
  },
3221
3165
  "~moonshotai/kimi-latest": {
3222
- "context_window": 262144,
3223
- "max_output_tokens": 262144,
3166
+ "context_window": 262142,
3167
+ "max_output_tokens": 262142,
3224
3168
  "supports_image_input": true,
3225
3169
  "supports_pdf_input": false,
3226
3170
  "supports_reasoning": true
@@ -24,12 +24,10 @@ from mycode.providers.base import (
24
24
  get_native_meta,
25
25
  load_document_block_payload,
26
26
  load_image_block_payload,
27
+ native_block_meta,
27
28
  tool_result_content_blocks,
28
29
  )
29
30
 
30
- # Maps reasoning_effort values to extended thinking budget_tokens.
31
- _THINKING_BUDGETS: dict[str, int] = {"low": 2048, "medium": 8192, "high": 24576, "xhigh": 32768}
32
-
33
31
 
34
32
  class AnthropicLikeAdapter(ProviderAdapter):
35
33
  """Shared Messages adapter for Anthropic-compatible providers."""
@@ -42,14 +40,6 @@ class AnthropicLikeAdapter(ProviderAdapter):
42
40
  del request
43
41
  return None
44
42
 
45
- def manual_thinking_config(self, effort: str | None) -> dict[str, Any] | None:
46
- if not effort:
47
- return None
48
- if effort == "none":
49
- return {"type": "disabled"}
50
- budget = _THINKING_BUDGETS.get(effort)
51
- return {"type": "enabled", "budget_tokens": budget} if budget else None
52
-
53
43
  def _build_request_payload(self, request: ProviderRequest) -> dict[str, Any]:
54
44
  messages = [self._serialize_message(message) for message in self.prepare_messages(request)]
55
45
  self._apply_cache_control(messages)
@@ -58,9 +48,9 @@ class AnthropicLikeAdapter(ProviderAdapter):
58
48
  payload: dict[str, Any] = {
59
49
  "model": request.model,
60
50
  "max_tokens": request.max_tokens,
61
- "temperature": request.temperature,
62
51
  "messages": messages,
63
52
  }
53
+ # Anthropic-compatible providers use provider-default sampling; temperature is intentionally omitted.
64
54
  if request.system:
65
55
  payload["system"] = [
66
56
  {
@@ -133,25 +123,27 @@ class AnthropicLikeAdapter(ProviderAdapter):
133
123
  api_key = self.require_api_key(request.api_key)
134
124
 
135
125
  try:
136
- async with AsyncAnthropic(
137
- api_key=api_key,
138
- base_url=self.resolve_base_url(request.api_base),
139
- timeout=DEFAULT_REQUEST_TIMEOUT,
140
- ) as client:
141
- async with client.messages.stream(**self._build_request_payload(request)) as stream:
142
- async for event in stream:
143
- event_type = getattr(event, "type", None)
144
- if event_type == "thinking":
145
- thinking = cast(str | None, getattr(event, "thinking", None))
146
- if thinking:
147
- yield ProviderStreamEvent("thinking_delta", {"text": thinking})
148
- continue
149
- if event_type == "text":
150
- text = cast(str | None, getattr(event, "text", None))
151
- if text:
152
- yield ProviderStreamEvent("text_delta", {"text": text})
153
-
154
- final_message = await stream.get_final_message()
126
+ async with (
127
+ AsyncAnthropic(
128
+ api_key=api_key,
129
+ base_url=self.resolve_base_url(request.api_base),
130
+ timeout=DEFAULT_REQUEST_TIMEOUT,
131
+ ) as client,
132
+ client.messages.stream(**self._build_request_payload(request)) as stream,
133
+ ):
134
+ async for event in stream:
135
+ event_type = getattr(event, "type", None)
136
+ if event_type == "thinking":
137
+ thinking = cast(str | None, getattr(event, "thinking", None))
138
+ if thinking:
139
+ yield ProviderStreamEvent("thinking_delta", {"text": thinking})
140
+ continue
141
+ if event_type == "text":
142
+ text = cast(str | None, getattr(event, "text", None))
143
+ if text:
144
+ yield ProviderStreamEvent("text_delta", {"text": text})
145
+
146
+ final_message = await stream.get_final_message()
155
147
  except APIError as exc:
156
148
  raise ValueError(str(exc)) from exc
157
149
 
@@ -175,7 +167,7 @@ class AnthropicLikeAdapter(ProviderAdapter):
175
167
  blocks.append(
176
168
  thinking_block(
177
169
  getattr(block, "thinking", ""),
178
- meta={"native": native_meta} if native_meta else None,
170
+ meta=native_block_meta(native_meta),
179
171
  )
180
172
  )
181
173
  continue
@@ -185,9 +177,7 @@ class AnthropicLikeAdapter(ProviderAdapter):
185
177
  citations = getattr(block, "citations", None)
186
178
  if citations:
187
179
  native_meta["citations"] = dump_model(citations)
188
- blocks.append(
189
- text_block(getattr(block, "text", ""), meta={"native": native_meta} if native_meta else None)
190
- )
180
+ blocks.append(text_block(getattr(block, "text", ""), meta=native_block_meta(native_meta)))
191
181
  continue
192
182
 
193
183
  if block_type == "tool_use":
@@ -200,7 +190,7 @@ class AnthropicLikeAdapter(ProviderAdapter):
200
190
  tool_id=getattr(block, "id", ""),
201
191
  name=getattr(block, "name", ""),
202
192
  input=getattr(block, "input", None),
203
- meta={"native": native_meta} if native_meta else None,
193
+ meta=native_block_meta(native_meta),
204
194
  )
205
195
  )
206
196
  continue
@@ -314,7 +304,7 @@ class AnthropicAdapter(AnthropicLikeAdapter):
314
304
  label = "Anthropic"
315
305
  default_base_url = "https://api.anthropic.com"
316
306
  env_api_key_names = ("ANTHROPIC_API_KEY",)
317
- default_models = ("claude-sonnet-4-6", "claude-opus-4-7")
307
+ default_models = ("claude-sonnet-4-6", "claude-opus-4-8")
318
308
  supports_reasoning_effort = True
319
309
 
320
310
  @override
@@ -325,12 +315,10 @@ class AnthropicAdapter(AnthropicLikeAdapter):
325
315
  if effort == "none":
326
316
  return {"type": "disabled"}
327
317
  normalized = request.model.lower()
328
- if normalized.startswith(("claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4-6")):
329
- thinking: dict[str, Any] = {"type": "adaptive"}
330
- if normalized.startswith("claude-opus-4-7"):
331
- thinking["display"] = "summarized"
332
- return thinking
333
- return self.manual_thinking_config(effort)
318
+ thinking: dict[str, Any] = {"type": "adaptive"}
319
+ if normalized.startswith(("claude-opus-4-7", "claude-opus-4-8")):
320
+ thinking["display"] = "summarized"
321
+ return thinking
334
322
 
335
323
  @override
336
324
  def output_config(self, request: ProviderRequest) -> dict[str, Any] | None:
@@ -339,7 +327,7 @@ class AnthropicAdapter(AnthropicLikeAdapter):
339
327
  return None
340
328
 
341
329
  normalized = request.model.lower()
342
- if normalized.startswith("claude-opus-4-7"):
330
+ if normalized.startswith(("claude-opus-4-7", "claude-opus-4-8")):
343
331
  return {"effort": effort}
344
332
 
345
333
  if normalized.startswith("claude-sonnet-4-6"):
@@ -364,12 +352,22 @@ class MoonshotAIAdapter(AnthropicLikeAdapter):
364
352
  label = "Moonshot"
365
353
  default_base_url = "https://api.moonshot.ai/anthropic"
366
354
  env_api_key_names = ("MOONSHOT_API_KEY",)
367
- default_models = ("kimi-k2.6",)
355
+ default_models = ("kimi-k2.7-code", "kimi-k2.6")
368
356
  supports_reasoning_effort = True
369
357
 
370
358
  @override
371
359
  def thinking_config(self, request: ProviderRequest) -> dict[str, Any] | None:
372
- return self.manual_thinking_config(request.reasoning_effort)
360
+ if not request.reasoning_effort:
361
+ return None
362
+ if request.reasoning_effort == "none" and request.model.lower() != "kimi-k2.7-code":
363
+ return {"type": "disabled"}
364
+ return {"type": "adaptive"}
365
+
366
+ @override
367
+ def output_config(self, request: ProviderRequest) -> dict[str, Any] | None:
368
+ if not request.reasoning_effort or request.reasoning_effort == "none":
369
+ return None
370
+ return {"effort": request.reasoning_effort}
373
371
 
374
372
 
375
373
  class MiniMaxAdapter(AnthropicLikeAdapter):
@@ -384,9 +382,9 @@ class MiniMaxAdapter(AnthropicLikeAdapter):
384
382
  label = "MiniMax"
385
383
  default_base_url = "https://api.minimax.io/anthropic"
386
384
  env_api_key_names = ("MINIMAX_API_KEY",)
387
- default_models = ("MiniMax-M2.7", "MiniMax-M2.7-highspeed")
388
- supports_reasoning_effort = True
385
+ default_models = ("MiniMax-M3",)
386
+ supports_reasoning_effort = False
389
387
 
390
388
  @override
391
389
  def thinking_config(self, request: ProviderRequest) -> dict[str, Any] | None:
392
- return self.manual_thinking_config(request.reasoning_effort)
390
+ return {"type": "adaptive"}
@@ -68,6 +68,12 @@ def get_native_meta(block: dict[str, Any]) -> dict[str, Any]:
68
68
  return {}
69
69
 
70
70
 
71
+ def native_block_meta(native: dict[str, Any]) -> dict[str, Any] | None:
72
+ """Wrap accumulated native values as block meta, or None when empty."""
73
+
74
+ return {"native": native} if native else None
75
+
76
+
71
77
  class ProviderAdapter(ABC):
72
78
  """Base class for provider adapters.
73
79
 
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from collections.abc import AsyncIterator
6
+ from contextlib import suppress
6
7
  from typing import Any, override
7
8
  from urllib.parse import urlparse
8
9
 
@@ -88,10 +89,8 @@ class GoogleGeminiAdapter(ProviderAdapter):
88
89
  except APIError as exc:
89
90
  raise ValueError(str(exc)) from exc
90
91
  finally:
91
- try:
92
+ with suppress(Exception):
92
93
  await client.aio.aclose()
93
- except Exception:
94
- pass
95
94
 
96
95
  raw_usage = usage or {}
97
96
  total_tokens = raw_usage.get("total_token_count") or None
@@ -19,6 +19,7 @@ from mycode.providers.base import (
19
19
  get_native_meta,
20
20
  load_document_block_payload,
21
21
  load_image_block_payload,
22
+ native_block_meta,
22
23
  )
23
24
  from mycode.utils import omit_none, parse_tool_arguments
24
25
 
@@ -109,7 +110,7 @@ class OpenAIChatAdapter(ProviderAdapter):
109
110
  blocks.append(
110
111
  thinking_block(
111
112
  "".join(thinking_parts),
112
- meta={"native": thinking_native_meta} if thinking_native_meta else None,
113
+ meta=native_block_meta(thinking_native_meta),
113
114
  )
114
115
  )
115
116
  if text_parts:
@@ -248,13 +249,15 @@ class OpenAIChatAdapter(ProviderAdapter):
248
249
  if role != "assistant":
249
250
  return []
250
251
 
251
- text_parts = [str(block.get("text") or "") for block in blocks if block.get("type") == "text"]
252
+ text_parts = [
253
+ str(block.get("text") or "") for block in blocks if block.get("type") == "text" and block.get("text")
254
+ ]
252
255
  thinking_blocks = [block for block in blocks if block.get("type") == "thinking"]
253
256
  tool_use_blocks = [block for block in blocks if block.get("type") == "tool_use"]
254
257
 
255
258
  payload: dict[str, Any] = {
256
259
  "role": "assistant",
257
- "content": "\n".join(part for part in text_parts if part),
260
+ "content": "\n".join(text_parts),
258
261
  }
259
262
 
260
263
  if tool_use_blocks:
@@ -363,12 +366,19 @@ class ZAIAdapter(OpenAIChatAdapter):
363
366
  label = "Z.AI"
364
367
  default_base_url = "https://api.z.ai/api/paas/v4/"
365
368
  env_api_key_names = ("ZAI_API_KEY",)
366
- default_models = ("glm-5.1", "glm-5-turbo")
369
+ default_models = ("glm-5.2",)
367
370
  auto_discoverable = True
371
+ supports_reasoning_effort = True
368
372
 
369
373
  @override
370
374
  def _build_provider_payload_overrides(self, request: ProviderRequest) -> dict[str, Any]:
371
- return {"extra_body": {"thinking": {"type": "enabled", "clear_thinking": False}}}
375
+ payload: dict[str, Any] = {"extra_body": {"thinking": {"type": "enabled", "clear_thinking": False}}}
376
+ if request.model.lower().startswith("glm-5.2"):
377
+ if request.reasoning_effort in {"low", "medium", "high"}:
378
+ payload["reasoning_effort"] = "high"
379
+ elif request.reasoning_effort == "xhigh":
380
+ payload["reasoning_effort"] = "max"
381
+ return payload
372
382
 
373
383
 
374
384
  class OpenRouterAdapter(OpenAIChatAdapter):
@@ -18,6 +18,7 @@ from mycode.providers.base import (
18
18
  dump_model,
19
19
  load_document_block_payload,
20
20
  load_image_block_payload,
21
+ native_block_meta,
21
22
  tool_result_content_blocks,
22
23
  )
23
24
  from mycode.utils import omit_none, parse_tool_arguments
@@ -285,7 +286,7 @@ class OpenAIResponsesAdapter(ProviderAdapter):
285
286
  blocks.append(
286
287
  thinking_block(
287
288
  "".join(text_parts),
288
- meta={"native": item_meta} if item_meta else None,
289
+ meta=native_block_meta(item_meta),
289
290
  )
290
291
  )
291
292
  continue
@@ -301,7 +302,7 @@ class OpenAIResponsesAdapter(ProviderAdapter):
301
302
  blocks.append(
302
303
  text_block(
303
304
  getattr(part, "text", ""),
304
- meta={"native": native_meta} if native_meta else None,
305
+ meta=native_block_meta(native_meta),
305
306
  )
306
307
  )
307
308
  continue
@@ -327,7 +328,7 @@ class OpenAIResponsesAdapter(ProviderAdapter):
327
328
  tool_id=getattr(item, "call_id", ""),
328
329
  name=getattr(item, "name", ""),
329
330
  input=tool_input,
330
- meta={"native": item_meta} if item_meta else None,
331
+ meta=native_block_meta(item_meta),
331
332
  )
332
333
  )
333
334
 
@@ -85,6 +85,12 @@ SessionMetaDict = dict[str, object]
85
85
  SessionIndex = dict[str, SessionMetaDict]
86
86
 
87
87
 
88
+ def _project_meta(raw: dict[str, object]) -> SessionMetaDict:
89
+ """Keep only the known session-meta keys."""
90
+
91
+ return {key: raw[key] for key in META_KEYS if key in raw}
92
+
93
+
88
94
  class SessionData(TypedDict):
89
95
  session: SessionMetaDict
90
96
  messages: list[ConversationMessage]
@@ -138,10 +144,10 @@ class SessionStore:
138
144
  return None
139
145
  if not isinstance(raw, dict):
140
146
  return None
141
- return {key: raw[key] for key in META_KEYS if key in raw}
147
+ return _project_meta(raw)
142
148
 
143
149
  def _write_meta(self, session_id: str, meta: SessionMetaDict) -> None:
144
- meta = {key: meta[key] for key in META_KEYS if key in meta}
150
+ meta = _project_meta(meta)
145
151
  self.meta_path(session_id).write_text(json.dumps(meta, indent=2, ensure_ascii=False), encoding="utf-8")
146
152
  index = self._read_index()
147
153
  index[session_id] = dict(meta)
@@ -154,11 +160,7 @@ class SessionStore:
154
160
  return self._rebuild_index()
155
161
  if not isinstance(raw, dict):
156
162
  return self._rebuild_index()
157
- return {
158
- str(session_id): {key: meta[key] for key in META_KEYS if key in meta}
159
- for session_id, meta in raw.items()
160
- if isinstance(meta, dict)
161
- }
163
+ return {str(session_id): _project_meta(meta) for session_id, meta in raw.items() if isinstance(meta, dict)}
162
164
 
163
165
  def _write_index(self, index: SessionIndex) -> None:
164
166
  self.index_path().write_text(json.dumps(index, indent=2, ensure_ascii=False), encoding="utf-8")
@@ -228,7 +230,6 @@ class SessionStore:
228
230
  if meta is None:
229
231
  return None
230
232
 
231
- # Read the raw append-only log first. Replay happens after that.
232
233
  raw_messages: list[ConversationMessage] = []
233
234
  try:
234
235
  with self.messages_path(session_id).open("r", encoding="utf-8") as handle:
@@ -238,10 +239,10 @@ class SessionStore:
238
239
  continue
239
240
  try:
240
241
  msg = json.loads(line)
241
- if isinstance(msg, dict):
242
- raw_messages.append(cast(ConversationMessage, msg))
243
242
  except ValueError:
244
243
  continue
244
+ if isinstance(msg, dict):
245
+ raw_messages.append(cast(ConversationMessage, msg))
245
246
  except FileNotFoundError:
246
247
  pass
247
248
 
@@ -26,9 +26,11 @@ import typing
26
26
  from base64 import b64encode
27
27
  from collections import deque
28
28
  from collections.abc import Callable, Mapping, Sequence
29
+ from contextlib import suppress
29
30
  from dataclasses import dataclass
30
31
  from difflib import SequenceMatcher, unified_diff
31
32
  from pathlib import Path
33
+ from types import FunctionType
32
34
  from typing import Any, TextIO, cast, overload
33
35
 
34
36
  from griffe import Docstring, DocstringSectionKind, Parser
@@ -222,10 +224,8 @@ def _kill_proc_tree(proc: subprocess.Popen[str]) -> None:
222
224
  else:
223
225
  proc.kill()
224
226
  except Exception:
225
- try:
227
+ with suppress(Exception):
226
228
  proc.kill()
227
- except Exception:
228
- pass
229
229
 
230
230
 
231
231
  # ---------------------------------------------------------------------------
@@ -235,7 +235,7 @@ def _kill_proc_tree(proc: subprocess.Popen[str]) -> None:
235
235
 
236
236
  @overload
237
237
  def tool(
238
- function: Callable[..., Any],
238
+ function: FunctionType,
239
239
  *,
240
240
  name: str | None = None,
241
241
  description: str | None = None,
@@ -252,17 +252,17 @@ def tool(
252
252
  description: str | None = None,
253
253
  parameters: Mapping[str, str] | None = None,
254
254
  streams_output: bool = False,
255
- ) -> Callable[[Callable[..., Any]], ToolSpec]: ...
255
+ ) -> Callable[[FunctionType], ToolSpec]: ...
256
256
 
257
257
 
258
258
  def tool(
259
- function: Callable[..., Any] | None = None,
259
+ function: FunctionType | None = None,
260
260
  *,
261
261
  name: str | None = None,
262
262
  description: str | None = None,
263
263
  parameters: Mapping[str, str] | None = None,
264
264
  streams_output: bool = False,
265
- ) -> ToolSpec | Callable[[Callable[..., Any]], ToolSpec]:
265
+ ) -> ToolSpec | Callable[[FunctionType], ToolSpec]:
266
266
  """Wrap a sync or async Python function as a :class:`ToolSpec`."""
267
267
 
268
268
  current_frame = inspect.currentframe()
@@ -271,7 +271,7 @@ def tool(
271
271
  del current_frame
272
272
  del caller_frame
273
273
 
274
- def wrap(fn: Callable[..., Any]) -> ToolSpec:
274
+ def wrap(fn: FunctionType) -> ToolSpec:
275
275
  tool_name = name or fn.__name__
276
276
  signature = inspect.signature(fn)
277
277
  signature_parameters = list(signature.parameters.values())
@@ -421,7 +421,7 @@ def _coerce_tool_result(value: Any) -> ToolExecutionResult:
421
421
  if isinstance(value, str):
422
422
  return ToolExecutionResult(output=value)
423
423
  try:
424
- text = json.dumps(value, ensure_ascii=False)
424
+ text = json.dumps(value, ensure_ascii=False, default=str)
425
425
  except TypeError:
426
426
  text = str(value)
427
427
  return ToolExecutionResult(output=text)
@@ -901,10 +901,8 @@ def bash_tool(ctx: ToolContext, command: str, timeout: int | None = None) -> Too
901
901
  return ToolExecutionResult(output=f"error: {exc}", is_error=True)
902
902
  finally:
903
903
  if log_file is not None:
904
- try:
904
+ with suppress(Exception):
905
905
  log_file.close()
906
- except Exception:
907
- pass
908
906
  if proc is not None:
909
907
  ctx.untrack_proc(proc)
910
908
  if proc.poll() is None:
File without changes
File without changes
File without changes