mycode-sdk 0.9.4__tar.gz → 0.9.5__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.4 → mycode_sdk-0.9.5}/PKG-INFO +6 -6
  2. {mycode_sdk-0.9.4 → mycode_sdk-0.9.5}/pyproject.toml +7 -7
  3. {mycode_sdk-0.9.4 → mycode_sdk-0.9.5}/src/mycode/agent.py +29 -41
  4. {mycode_sdk-0.9.4 → mycode_sdk-0.9.5}/src/mycode/models.py +14 -4
  5. {mycode_sdk-0.9.4 → mycode_sdk-0.9.5}/src/mycode/models_catalog.json +142 -114
  6. {mycode_sdk-0.9.4 → mycode_sdk-0.9.5}/src/mycode/providers/base.py +10 -5
  7. {mycode_sdk-0.9.4 → mycode_sdk-0.9.5}/src/mycode/providers/openai_chat.py +11 -16
  8. {mycode_sdk-0.9.4 → mycode_sdk-0.9.5}/src/mycode/providers/openai_responses.py +1 -1
  9. {mycode_sdk-0.9.4 → mycode_sdk-0.9.5}/src/mycode/session.py +10 -23
  10. {mycode_sdk-0.9.4 → mycode_sdk-0.9.5}/src/mycode/tools.py +10 -24
  11. {mycode_sdk-0.9.4 → mycode_sdk-0.9.5}/src/mycode/utils.py +0 -16
  12. {mycode_sdk-0.9.4 → mycode_sdk-0.9.5}/.gitignore +0 -0
  13. {mycode_sdk-0.9.4 → mycode_sdk-0.9.5}/LICENSE +0 -0
  14. {mycode_sdk-0.9.4 → mycode_sdk-0.9.5}/README.md +0 -0
  15. {mycode_sdk-0.9.4 → mycode_sdk-0.9.5}/src/mycode/__init__.py +0 -0
  16. {mycode_sdk-0.9.4 → mycode_sdk-0.9.5}/src/mycode/attachments.py +0 -0
  17. {mycode_sdk-0.9.4 → mycode_sdk-0.9.5}/src/mycode/compact.py +0 -0
  18. {mycode_sdk-0.9.4 → mycode_sdk-0.9.5}/src/mycode/hooks.py +0 -0
  19. {mycode_sdk-0.9.4 → mycode_sdk-0.9.5}/src/mycode/messages.py +0 -0
  20. {mycode_sdk-0.9.4 → mycode_sdk-0.9.5}/src/mycode/providers/__init__.py +0 -0
  21. {mycode_sdk-0.9.4 → mycode_sdk-0.9.5}/src/mycode/providers/anthropic_like.py +0 -0
  22. {mycode_sdk-0.9.4 → mycode_sdk-0.9.5}/src/mycode/providers/gemini.py +0 -0
  23. {mycode_sdk-0.9.4 → mycode_sdk-0.9.5}/src/mycode/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mycode-sdk
3
- Version: 0.9.4
3
+ Version: 0.9.5
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.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
21
+ Requires-Dist: anthropic>=0.116.0
22
+ Requires-Dist: google-genai>=2.11.0
23
+ Requires-Dist: griffelib>=2.1.0
24
+ Requires-Dist: openai>=2.45.0
25
+ Requires-Dist: pydantic>=2.13.4
26
26
  Description-Content-Type: text/markdown
27
27
 
28
28
  # mycode-sdk
@@ -1,10 +1,10 @@
1
1
  [build-system]
2
- requires = ["hatchling"]
2
+ requires = ["hatchling>=1.31.0"]
3
3
  build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "mycode-sdk"
7
- version = "0.9.4"
7
+ version = "0.9.5"
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.106.0",
27
- "google-genai>=2.8.0",
28
- "griffelib>=2.0.0",
29
- "openai>=2.41.0",
30
- "pydantic>=2.13.0",
26
+ "anthropic>=0.116.0",
27
+ "google-genai>=2.11.0",
28
+ "griffelib>=2.1.0",
29
+ "openai>=2.45.0",
30
+ "pydantic>=2.13.4",
31
31
  ]
32
32
 
33
33
  [project.urls]
@@ -152,16 +152,10 @@ class Agent:
152
152
  # (e.g. bash spill files) live next to the session JSONL; without a
153
153
  # session, fall back to a tempdir scoped to ``session_id``.
154
154
  if session_dir is not None:
155
- tool_output_dir = session_dir / self.session_id / "tool-output"
155
+ self.tool_output_dir = session_dir / self.session_id / "tool-output"
156
156
  else:
157
- tool_output_dir = Path(tempfile.gettempdir()) / "mycode" / self.session_id / "tool-output"
157
+ self.tool_output_dir = Path(tempfile.gettempdir()) / "mycode" / self.session_id / "tool-output"
158
158
  self.tools = ToolExecutor(tools)
159
- self.tool_ctx = ToolContext(
160
- executor=self.tools,
161
- cwd=self.cwd,
162
- tool_output_dir=tool_output_dir,
163
- supports_image_input=False,
164
- )
165
159
 
166
160
  self.refresh_capabilities(
167
161
  max_tokens=max_tokens,
@@ -201,7 +195,6 @@ class Agent:
201
195
  self.supports_reasoning: bool | None = meta.supports_reasoning
202
196
  self.supports_image_input: bool = bool(meta.supports_image_input)
203
197
  self.supports_pdf_input: bool = bool(meta.supports_pdf_input)
204
- self.tool_ctx.supports_image_input = self.supports_image_input
205
198
 
206
199
  def cancel(self) -> None:
207
200
  """Request cancellation of the in-flight turn."""
@@ -330,11 +323,11 @@ class Agent:
330
323
  output = "\n".join([*output_parts, "error: cancelled"]) if output_parts else "error: cancelled"
331
324
  yield self._error_done(tool_id, output)
332
325
  return
333
- else:
334
- try:
335
- result = await task
336
- except Exception as exc: # pragma: no cover - defensive
337
- result = ToolExecutionResult(output=f"error: {exc}", is_error=True)
326
+
327
+ try:
328
+ result = await task
329
+ except Exception as exc: # pragma: no cover - defensive
330
+ result = ToolExecutionResult(output=f"error: {exc}", is_error=True)
338
331
 
339
332
  yield await self._finish_tool_call(tool_id, hook_ctx, result)
340
333
 
@@ -360,13 +353,11 @@ class Agent:
360
353
  *,
361
354
  emit: Callable[[str], None] | None = None,
362
355
  ) -> ToolContext:
363
- """Build a per-call ToolContext from the base context."""
364
-
365
356
  return ToolContext(
366
357
  executor=self.tools,
367
- cwd=self.tool_ctx.cwd,
368
- tool_output_dir=self.tool_ctx.tool_output_dir,
369
- supports_image_input=self.tool_ctx.supports_image_input,
358
+ cwd=self.cwd,
359
+ tool_output_dir=self.tool_output_dir,
360
+ supports_image_input=self.supports_image_input,
370
361
  tool_call_id=tool_id,
371
362
  emit=emit,
372
363
  )
@@ -402,15 +393,12 @@ class Agent:
402
393
 
403
394
  provider_stream: AsyncIterator[ProviderStreamEvent] = adapter.stream_turn(request)
404
395
 
405
- async def next_provider_event() -> ProviderStreamEvent:
406
- return await anext(provider_stream)
407
-
408
396
  try:
409
397
  while True:
410
398
  if self._cancel_event.is_set():
411
399
  raise asyncio.CancelledError
412
400
 
413
- self._provider_event_task = asyncio.create_task(next_provider_event())
401
+ self._provider_event_task = asyncio.ensure_future(anext(provider_stream))
414
402
  try:
415
403
  yield await self._provider_event_task
416
404
  except StopAsyncIteration:
@@ -529,7 +517,10 @@ class Agent:
529
517
  adapter = get_provider_adapter(self.provider)
530
518
 
531
519
  turn_number = 0
532
- while self.max_turns is None or turn_number < self.max_turns:
520
+ while True:
521
+ if self.max_turns is not None and turn_number >= self.max_turns:
522
+ yield Event("error", {"message": "max_turns reached"})
523
+ return
533
524
  turn_number += 1
534
525
  if self._cancel_event.is_set():
535
526
  yield Event("error", {"message": "cancelled"})
@@ -632,13 +623,15 @@ class Agent:
632
623
 
633
624
  total_tokens = meta.get("total_tokens")
634
625
  if total_tokens:
635
- payload: dict[str, Any] = {
636
- "total_tokens": total_tokens,
637
- "model": meta.get("model") or self.model,
638
- "provider": meta.get("provider") or self.provider,
639
- "context_window": meta["context_window"],
640
- }
641
- yield Event("usage", payload)
626
+ yield Event(
627
+ "usage",
628
+ {
629
+ "total_tokens": total_tokens,
630
+ "model": meta.get("model") or self.model,
631
+ "provider": meta.get("provider") or self.provider,
632
+ "context_window": meta["context_window"],
633
+ },
634
+ )
642
635
 
643
636
  tool_calls = [
644
637
  block
@@ -665,11 +658,10 @@ class Agent:
665
658
  )
666
659
  )
667
660
 
668
- if self._cancel_event.is_set():
669
- tool_result_message = build_message("user", tool_results)
670
- self.messages.append(tool_result_message)
671
- await persist(tool_result_message)
672
- return
661
+ if self._cancel_event.is_set():
662
+ # Skip remaining tool calls; the results collected so
663
+ # far are still persisted below.
664
+ break
673
665
 
674
666
  tool_result_message = build_message("user", tool_results)
675
667
  self.messages.append(tool_result_message)
@@ -693,11 +685,7 @@ class Agent:
693
685
  )
694
686
 
695
687
  if not tool_calls:
696
- break
697
-
698
- else:
699
- yield Event("error", {"message": "max_turns reached"})
700
- return
688
+ return
701
689
 
702
690
  def run(
703
691
  self,
@@ -66,16 +66,26 @@ def resolve_model_metadata(
66
66
  *,
67
67
  provider: str,
68
68
  model: str,
69
- **overrides: Any,
69
+ context_window: int | None = None,
70
+ max_output_tokens: int | None = None,
71
+ supports_reasoning: bool | None = None,
72
+ supports_image_input: bool | None = None,
73
+ supports_pdf_input: bool | None = None,
70
74
  ) -> ModelMetadata:
71
75
  """Return catalog metadata for ``(provider, model)`` with non-None overrides layered on top.
72
76
 
73
- Override keys must match :class:`ModelMetadata` fields. Missing overrides
74
- and an absent catalog entry both leave the corresponding fields at ``None``
75
- so callers can apply their own fallback defaults.
77
+ Missing overrides and an absent catalog entry both leave the corresponding
78
+ fields at ``None`` so callers can apply their own fallback defaults.
76
79
  """
77
80
 
78
81
  base = lookup_model_metadata(provider_type=provider, model=model) or ModelMetadata(provider=provider, model=model)
82
+ overrides = {
83
+ "context_window": context_window,
84
+ "max_output_tokens": max_output_tokens,
85
+ "supports_reasoning": supports_reasoning,
86
+ "supports_image_input": supports_image_input,
87
+ "supports_pdf_input": supports_pdf_input,
88
+ }
79
89
  return replace(base, **{k: v for k, v in overrides.items() if v is not None})
80
90
 
81
91
 
@@ -1,47 +1,5 @@
1
1
  {
2
2
  "anthropic": {
3
- "claude-3-5-sonnet-20240620": {
4
- "context_window": 200000,
5
- "max_output_tokens": 8192,
6
- "supports_image_input": true,
7
- "supports_pdf_input": true,
8
- "supports_reasoning": false
9
- },
10
- "claude-3-5-sonnet-20241022": {
11
- "context_window": 200000,
12
- "max_output_tokens": 8192,
13
- "supports_image_input": true,
14
- "supports_pdf_input": true,
15
- "supports_reasoning": false
16
- },
17
- "claude-3-7-sonnet-20250219": {
18
- "context_window": 200000,
19
- "max_output_tokens": 64000,
20
- "supports_image_input": true,
21
- "supports_pdf_input": true,
22
- "supports_reasoning": true
23
- },
24
- "claude-3-haiku-20240307": {
25
- "context_window": 200000,
26
- "max_output_tokens": 4096,
27
- "supports_image_input": true,
28
- "supports_pdf_input": true,
29
- "supports_reasoning": false
30
- },
31
- "claude-3-opus-20240229": {
32
- "context_window": 200000,
33
- "max_output_tokens": 4096,
34
- "supports_image_input": true,
35
- "supports_pdf_input": true,
36
- "supports_reasoning": false
37
- },
38
- "claude-3-sonnet-20240229": {
39
- "context_window": 200000,
40
- "max_output_tokens": 4096,
41
- "supports_image_input": true,
42
- "supports_pdf_input": true,
43
- "supports_reasoning": false
44
- },
45
3
  "claude-fable-5": {
46
4
  "context_window": 1000000,
47
5
  "max_output_tokens": 128000,
@@ -63,13 +21,6 @@
63
21
  "supports_pdf_input": true,
64
22
  "supports_reasoning": true
65
23
  },
66
- "claude-opus-4-0": {
67
- "context_window": 200000,
68
- "max_output_tokens": 32000,
69
- "supports_image_input": true,
70
- "supports_pdf_input": true,
71
- "supports_reasoning": true
72
- },
73
24
  "claude-opus-4-1": {
74
25
  "context_window": 200000,
75
26
  "max_output_tokens": 32000,
@@ -84,13 +35,6 @@
84
35
  "supports_pdf_input": true,
85
36
  "supports_reasoning": true
86
37
  },
87
- "claude-opus-4-20250514": {
88
- "context_window": 200000,
89
- "max_output_tokens": 32000,
90
- "supports_image_input": true,
91
- "supports_pdf_input": true,
92
- "supports_reasoning": true
93
- },
94
38
  "claude-opus-4-5": {
95
39
  "context_window": 200000,
96
40
  "max_output_tokens": 64000,
@@ -126,29 +70,15 @@
126
70
  "supports_pdf_input": true,
127
71
  "supports_reasoning": true
128
72
  },
129
- "claude-sonnet-4-0": {
130
- "context_window": 200000,
131
- "max_output_tokens": 64000,
132
- "supports_image_input": true,
133
- "supports_pdf_input": true,
134
- "supports_reasoning": true
135
- },
136
- "claude-sonnet-4-20250514": {
137
- "context_window": 200000,
138
- "max_output_tokens": 64000,
139
- "supports_image_input": true,
140
- "supports_pdf_input": true,
141
- "supports_reasoning": true
142
- },
143
73
  "claude-sonnet-4-5": {
144
- "context_window": 200000,
74
+ "context_window": 1000000,
145
75
  "max_output_tokens": 64000,
146
76
  "supports_image_input": true,
147
77
  "supports_pdf_input": true,
148
78
  "supports_reasoning": true
149
79
  },
150
80
  "claude-sonnet-4-5-20250929": {
151
- "context_window": 200000,
81
+ "context_window": 1000000,
152
82
  "max_output_tokens": 64000,
153
83
  "supports_image_input": true,
154
84
  "supports_pdf_input": true,
@@ -156,7 +86,7 @@
156
86
  },
157
87
  "claude-sonnet-4-6": {
158
88
  "context_window": 1000000,
159
- "max_output_tokens": 64000,
89
+ "max_output_tokens": 128000,
160
90
  "supports_image_input": true,
161
91
  "supports_pdf_input": true,
162
92
  "supports_reasoning": true
@@ -340,6 +270,13 @@
340
270
  "supports_pdf_input": true,
341
271
  "supports_reasoning": true
342
272
  },
273
+ "gemini-omni-flash-preview": {
274
+ "context_window": 131072,
275
+ "max_output_tokens": 65536,
276
+ "supports_image_input": true,
277
+ "supports_pdf_input": false,
278
+ "supports_reasoning": true
279
+ },
343
280
  "gemma-4-26b-a4b-it": {
344
281
  "context_window": 262144,
345
282
  "max_output_tokens": 32768,
@@ -724,6 +661,34 @@
724
661
  "supports_pdf_input": true,
725
662
  "supports_reasoning": true
726
663
  },
664
+ "gpt-5.6": {
665
+ "context_window": 1050000,
666
+ "max_output_tokens": 128000,
667
+ "supports_image_input": true,
668
+ "supports_pdf_input": true,
669
+ "supports_reasoning": true
670
+ },
671
+ "gpt-5.6-luna": {
672
+ "context_window": 1050000,
673
+ "max_output_tokens": 128000,
674
+ "supports_image_input": true,
675
+ "supports_pdf_input": true,
676
+ "supports_reasoning": true
677
+ },
678
+ "gpt-5.6-sol": {
679
+ "context_window": 1050000,
680
+ "max_output_tokens": 128000,
681
+ "supports_image_input": true,
682
+ "supports_pdf_input": true,
683
+ "supports_reasoning": true
684
+ },
685
+ "gpt-5.6-terra": {
686
+ "context_window": 1050000,
687
+ "max_output_tokens": 128000,
688
+ "supports_image_input": true,
689
+ "supports_pdf_input": true,
690
+ "supports_reasoning": true
691
+ },
727
692
  "gpt-image-1": {
728
693
  "context_window": 0,
729
694
  "max_output_tokens": 0,
@@ -838,21 +803,21 @@
838
803
  "supports_pdf_input": false,
839
804
  "supports_reasoning": false
840
805
  },
841
- "aion-labs/aion-1.0": {
806
+ "aion-labs/aion-2.0": {
842
807
  "context_window": 131072,
843
808
  "max_output_tokens": 32768,
844
809
  "supports_image_input": false,
845
810
  "supports_pdf_input": false,
846
811
  "supports_reasoning": true
847
812
  },
848
- "aion-labs/aion-1.0-mini": {
813
+ "aion-labs/aion-3.0": {
849
814
  "context_window": 131072,
850
815
  "max_output_tokens": 32768,
851
816
  "supports_image_input": false,
852
817
  "supports_pdf_input": false,
853
818
  "supports_reasoning": true
854
819
  },
855
- "aion-labs/aion-2.0": {
820
+ "aion-labs/aion-3.0-mini": {
856
821
  "context_window": 131072,
857
822
  "max_output_tokens": 32768,
858
823
  "supports_image_input": false,
@@ -922,6 +887,13 @@
922
887
  "supports_pdf_input": false,
923
888
  "supports_reasoning": false
924
889
  },
890
+ "anthropic/claude-fable-5": {
891
+ "context_window": 1000000,
892
+ "max_output_tokens": 128000,
893
+ "supports_image_input": true,
894
+ "supports_pdf_input": true,
895
+ "supports_reasoning": true
896
+ },
925
897
  "anthropic/claude-haiku-4.5": {
926
898
  "context_window": 200000,
927
899
  "max_output_tokens": 64000,
@@ -1196,7 +1168,7 @@
1196
1168
  "supports_reasoning": true
1197
1169
  },
1198
1170
  "deepseek/deepseek-v4-flash": {
1199
- "context_window": 1048575,
1171
+ "context_window": 1048576,
1200
1172
  "max_output_tokens": 65536,
1201
1173
  "supports_image_input": false,
1202
1174
  "supports_pdf_input": false,
@@ -1230,13 +1202,6 @@
1230
1202
  "supports_pdf_input": true,
1231
1203
  "supports_reasoning": true
1232
1204
  },
1233
- "google/gemini-2.5-flash-lite-preview-09-2025": {
1234
- "context_window": 1048576,
1235
- "max_output_tokens": 65535,
1236
- "supports_image_input": true,
1237
- "supports_pdf_input": true,
1238
- "supports_reasoning": true
1239
- },
1240
1205
  "google/gemini-2.5-pro": {
1241
1206
  "context_window": 1048576,
1242
1207
  "max_output_tokens": 65536,
@@ -1482,13 +1447,6 @@
1482
1447
  "supports_pdf_input": false,
1483
1448
  "supports_reasoning": false
1484
1449
  },
1485
- "liquid/lfm-2-24b-a2b": {
1486
- "context_window": 32768,
1487
- "max_output_tokens": 32768,
1488
- "supports_image_input": false,
1489
- "supports_pdf_input": false,
1490
- "supports_reasoning": false
1491
- },
1492
1450
  "liquid/lfm-2.5-1.2b-instruct:free": {
1493
1451
  "context_window": 32768,
1494
1452
  "max_output_tokens": 32768,
@@ -1546,8 +1504,8 @@
1546
1504
  "supports_reasoning": false
1547
1505
  },
1548
1506
  "meta-llama/llama-3.2-3b-instruct": {
1549
- "context_window": 80000,
1550
- "max_output_tokens": 80000,
1507
+ "context_window": 131072,
1508
+ "max_output_tokens": 131072,
1551
1509
  "supports_image_input": false,
1552
1510
  "supports_pdf_input": false,
1553
1511
  "supports_reasoning": false
@@ -1623,8 +1581,8 @@
1623
1581
  "supports_reasoning": true
1624
1582
  },
1625
1583
  "minimax/minimax-m2": {
1626
- "context_window": 196608,
1627
- "max_output_tokens": 196608,
1584
+ "context_window": 204800,
1585
+ "max_output_tokens": 131072,
1628
1586
  "supports_image_input": false,
1629
1587
  "supports_pdf_input": false,
1630
1588
  "supports_reasoning": true
@@ -1637,8 +1595,8 @@
1637
1595
  "supports_reasoning": false
1638
1596
  },
1639
1597
  "minimax/minimax-m2.1": {
1640
- "context_window": 196608,
1641
- "max_output_tokens": 196608,
1598
+ "context_window": 204800,
1599
+ "max_output_tokens": 131072,
1642
1600
  "supports_image_input": false,
1643
1601
  "supports_pdf_input": false,
1644
1602
  "supports_reasoning": true
@@ -1813,7 +1771,7 @@
1813
1771
  },
1814
1772
  "moonshotai/kimi-k2-thinking": {
1815
1773
  "context_window": 262144,
1816
- "max_output_tokens": 262144,
1774
+ "max_output_tokens": 100352,
1817
1775
  "supports_image_input": false,
1818
1776
  "supports_pdf_input": false,
1819
1777
  "supports_reasoning": true
@@ -1834,7 +1792,7 @@
1834
1792
  },
1835
1793
  "moonshotai/kimi-k2.7-code": {
1836
1794
  "context_window": 262144,
1837
- "max_output_tokens": 16384,
1795
+ "max_output_tokens": 262144,
1838
1796
  "supports_image_input": true,
1839
1797
  "supports_pdf_input": false,
1840
1798
  "supports_reasoning": true
@@ -1853,6 +1811,13 @@
1853
1811
  "supports_pdf_input": false,
1854
1812
  "supports_reasoning": false
1855
1813
  },
1814
+ "nex-agi/nex-n2-mini": {
1815
+ "context_window": 262144,
1816
+ "max_output_tokens": 262144,
1817
+ "supports_image_input": true,
1818
+ "supports_pdf_input": false,
1819
+ "supports_reasoning": true
1820
+ },
1856
1821
  "nex-agi/nex-n2-pro": {
1857
1822
  "context_window": 262144,
1858
1823
  "max_output_tokens": 262144,
@@ -2280,6 +2245,48 @@
2280
2245
  "supports_pdf_input": true,
2281
2246
  "supports_reasoning": true
2282
2247
  },
2248
+ "openai/gpt-5.6-luna": {
2249
+ "context_window": 1050000,
2250
+ "max_output_tokens": 128000,
2251
+ "supports_image_input": true,
2252
+ "supports_pdf_input": true,
2253
+ "supports_reasoning": true
2254
+ },
2255
+ "openai/gpt-5.6-luna-pro": {
2256
+ "context_window": 1050000,
2257
+ "max_output_tokens": 128000,
2258
+ "supports_image_input": true,
2259
+ "supports_pdf_input": true,
2260
+ "supports_reasoning": true
2261
+ },
2262
+ "openai/gpt-5.6-sol": {
2263
+ "context_window": 1050000,
2264
+ "max_output_tokens": 128000,
2265
+ "supports_image_input": true,
2266
+ "supports_pdf_input": true,
2267
+ "supports_reasoning": true
2268
+ },
2269
+ "openai/gpt-5.6-sol-pro": {
2270
+ "context_window": 1050000,
2271
+ "max_output_tokens": 128000,
2272
+ "supports_image_input": true,
2273
+ "supports_pdf_input": true,
2274
+ "supports_reasoning": true
2275
+ },
2276
+ "openai/gpt-5.6-terra": {
2277
+ "context_window": 1050000,
2278
+ "max_output_tokens": 128000,
2279
+ "supports_image_input": true,
2280
+ "supports_pdf_input": true,
2281
+ "supports_reasoning": true
2282
+ },
2283
+ "openai/gpt-5.6-terra-pro": {
2284
+ "context_window": 1050000,
2285
+ "max_output_tokens": 128000,
2286
+ "supports_image_input": true,
2287
+ "supports_pdf_input": true,
2288
+ "supports_reasoning": true
2289
+ },
2283
2290
  "openai/gpt-audio": {
2284
2291
  "context_window": 128000,
2285
2292
  "max_output_tokens": 16384,
@@ -2497,14 +2504,14 @@
2497
2504
  "supports_pdf_input": false,
2498
2505
  "supports_reasoning": true
2499
2506
  },
2500
- "poolside/laguna-xs.2": {
2507
+ "poolside/laguna-xs-2.1": {
2501
2508
  "context_window": 262144,
2502
2509
  "max_output_tokens": 32768,
2503
2510
  "supports_image_input": false,
2504
2511
  "supports_pdf_input": false,
2505
2512
  "supports_reasoning": true
2506
2513
  },
2507
- "poolside/laguna-xs.2:free": {
2514
+ "poolside/laguna-xs-2.1:free": {
2508
2515
  "context_window": 262144,
2509
2516
  "max_output_tokens": 32768,
2510
2517
  "supports_image_input": false,
@@ -2603,8 +2610,8 @@
2603
2610
  "supports_reasoning": false
2604
2611
  },
2605
2612
  "qwen/qwen3-30b-a3b-thinking-2507": {
2606
- "context_window": 131072,
2607
- "max_output_tokens": 131072,
2613
+ "context_window": 81920,
2614
+ "max_output_tokens": 32768,
2608
2615
  "supports_image_input": false,
2609
2616
  "supports_pdf_input": false,
2610
2617
  "supports_reasoning": true
@@ -2617,7 +2624,7 @@
2617
2624
  "supports_reasoning": true
2618
2625
  },
2619
2626
  "qwen/qwen3-8b": {
2620
- "context_window": 40960,
2627
+ "context_window": 131072,
2621
2628
  "max_output_tokens": 8192,
2622
2629
  "supports_image_input": false,
2623
2630
  "supports_pdf_input": false,
@@ -2931,15 +2938,15 @@
2931
2938
  "supports_pdf_input": false,
2932
2939
  "supports_reasoning": true
2933
2940
  },
2934
- "switchpoint/router": {
2941
+ "tencent/hunyuan-a13b-instruct": {
2935
2942
  "context_window": 131072,
2936
2943
  "max_output_tokens": 131072,
2937
2944
  "supports_image_input": false,
2938
2945
  "supports_pdf_input": false,
2939
2946
  "supports_reasoning": true
2940
2947
  },
2941
- "tencent/hunyuan-a13b-instruct": {
2942
- "context_window": 131072,
2948
+ "tencent/hy3": {
2949
+ "context_window": 262144,
2943
2950
  "max_output_tokens": 131072,
2944
2951
  "supports_image_input": false,
2945
2952
  "supports_pdf_input": false,
@@ -2952,6 +2959,13 @@
2952
2959
  "supports_pdf_input": false,
2953
2960
  "supports_reasoning": true
2954
2961
  },
2962
+ "tencent/hy3:free": {
2963
+ "context_window": 262144,
2964
+ "max_output_tokens": 262144,
2965
+ "supports_image_input": false,
2966
+ "supports_pdf_input": false,
2967
+ "supports_reasoning": true
2968
+ },
2955
2969
  "thedrummer/cydonia-24b-v4.1": {
2956
2970
  "context_window": 131072,
2957
2971
  "max_output_tokens": 131072,
@@ -2960,8 +2974,8 @@
2960
2974
  "supports_reasoning": false
2961
2975
  },
2962
2976
  "thedrummer/rocinante-12b": {
2963
- "context_window": 32768,
2964
- "max_output_tokens": 32768,
2977
+ "context_window": 65536,
2978
+ "max_output_tokens": 65536,
2965
2979
  "supports_image_input": false,
2966
2980
  "supports_pdf_input": false,
2967
2981
  "supports_reasoning": false
@@ -3022,15 +3036,22 @@
3022
3036
  "supports_pdf_input": true,
3023
3037
  "supports_reasoning": true
3024
3038
  },
3039
+ "x-ai/grok-4.5": {
3040
+ "context_window": 500000,
3041
+ "max_output_tokens": 500000,
3042
+ "supports_image_input": true,
3043
+ "supports_pdf_input": true,
3044
+ "supports_reasoning": true
3045
+ },
3025
3046
  "x-ai/grok-build-0.1": {
3026
3047
  "context_window": 256000,
3027
3048
  "max_output_tokens": 256000,
3028
3049
  "supports_image_input": true,
3029
- "supports_pdf_input": false,
3050
+ "supports_pdf_input": true,
3030
3051
  "supports_reasoning": true
3031
3052
  },
3032
3053
  "xiaomi/mimo-v2.5": {
3033
- "context_window": 32000,
3054
+ "context_window": 1048576,
3034
3055
  "max_output_tokens": 131072,
3035
3056
  "supports_image_input": true,
3036
3057
  "supports_pdf_input": false,
@@ -3107,15 +3128,15 @@
3107
3128
  "supports_reasoning": true
3108
3129
  },
3109
3130
  "z-ai/glm-5.1": {
3110
- "context_window": 65536,
3131
+ "context_window": 200000,
3111
3132
  "max_output_tokens": 128000,
3112
3133
  "supports_image_input": false,
3113
3134
  "supports_pdf_input": false,
3114
3135
  "supports_reasoning": true
3115
3136
  },
3116
3137
  "z-ai/glm-5.2": {
3117
- "context_window": 1048576,
3118
- "max_output_tokens": 32768,
3138
+ "context_window": 101376,
3139
+ "max_output_tokens": 101376,
3119
3140
  "supports_image_input": false,
3120
3141
  "supports_pdf_input": false,
3121
3142
  "supports_reasoning": true
@@ -3189,6 +3210,13 @@
3189
3210
  "supports_image_input": true,
3190
3211
  "supports_pdf_input": true,
3191
3212
  "supports_reasoning": true
3213
+ },
3214
+ "~x-ai/grok-latest": {
3215
+ "context_window": 500000,
3216
+ "max_output_tokens": 1000000,
3217
+ "supports_image_input": true,
3218
+ "supports_pdf_input": true,
3219
+ "supports_reasoning": true
3192
3220
  }
3193
3221
  },
3194
3222
  "zai": {
@@ -7,6 +7,7 @@ before sending a request upstream.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import json
10
11
  import os
11
12
  from abc import ABC, abstractmethod
12
13
  from collections.abc import AsyncIterator
@@ -16,7 +17,6 @@ from typing import Any
16
17
  from mycode.attachments import unsupported_attachment_block
17
18
  from mycode.compact import apply_compact_replay
18
19
  from mycode.messages import ConversationMessage, build_message, text_block, tool_result_block
19
- from mycode.utils import parse_tool_arguments
20
20
 
21
21
  DEFAULT_REQUEST_TIMEOUT = 300.0
22
22
 
@@ -83,10 +83,15 @@ def parse_tool_call_input(raw_arguments: str) -> tuple[dict[str, Any], dict[str,
83
83
  ``raw_arguments`` so it stays inspectable in ``block.meta.native``.
84
84
  """
85
85
 
86
- parsed = parse_tool_arguments(raw_arguments)
87
- if parsed is None:
88
- return {}, {"raw_arguments": raw_arguments}
89
- return parsed, {}
86
+ if not raw_arguments.strip():
87
+ return {}, {}
88
+ try:
89
+ parsed = json.loads(raw_arguments)
90
+ except json.JSONDecodeError:
91
+ parsed = None
92
+ if isinstance(parsed, dict):
93
+ return parsed, {}
94
+ return {}, {"raw_arguments": raw_arguments}
90
95
 
91
96
 
92
97
  class ProviderAdapter(ABC):
@@ -29,7 +29,6 @@ from mycode.utils import omit_none
29
29
  class _ChatToolCallState:
30
30
  """Accumulate one streamed tool call from chat-completions deltas."""
31
31
 
32
- index: int
33
32
  tool_id: str | None = None
34
33
  name: str = ""
35
34
  arguments_text: str = ""
@@ -93,7 +92,7 @@ class OpenAIChatAdapter(ProviderAdapter):
93
92
 
94
93
  for tool_call in delta.tool_calls or []:
95
94
  index = tool_call.index or 0
96
- state = tool_calls.setdefault(index, _ChatToolCallState(index=index))
95
+ state = tool_calls.setdefault(index, _ChatToolCallState())
97
96
  if tool_call.id:
98
97
  state.tool_id = tool_call.id
99
98
  function = tool_call.function
@@ -184,27 +183,26 @@ class OpenAIChatAdapter(ProviderAdapter):
184
183
  if role == "user":
185
184
  payload_messages: list[dict[str, Any]] = []
186
185
  has_media = any(block.get("type") in {"image", "document"} for block in blocks)
186
+ user_content: str | list[dict[str, Any]]
187
187
  if has_media:
188
- user_content: str | list[dict[str, Any]] | None = []
188
+ media_parts: list[dict[str, Any]] = []
189
189
  for block in blocks:
190
190
  block_type = block.get("type")
191
191
  if block_type == "text":
192
192
  text = str(block.get("text") or "")
193
193
  if text:
194
- user_content.append({"type": "text", "text": text})
195
- continue
196
- if block_type == "image":
194
+ media_parts.append({"type": "text", "text": text})
195
+ elif block_type == "image":
197
196
  mime_type, data = load_image_block_payload(block)
198
- user_content.append(
197
+ media_parts.append(
199
198
  {
200
199
  "type": "image_url",
201
200
  "image_url": {"url": f"data:{mime_type};base64,{data}"},
202
201
  }
203
202
  )
204
- continue
205
- if block_type == "document":
203
+ elif block_type == "document":
206
204
  mime_type, data, name = load_document_block_payload(block)
207
- user_content.append(
205
+ media_parts.append(
208
206
  {
209
207
  "type": "file",
210
208
  "file": {
@@ -213,16 +211,13 @@ class OpenAIChatAdapter(ProviderAdapter):
213
211
  },
214
212
  }
215
213
  )
216
- if not user_content:
217
- user_content = None
214
+ user_content = media_parts
218
215
  else:
219
- text_parts = [
216
+ user_content = "\n".join(
220
217
  str(block.get("text") or "")
221
218
  for block in blocks
222
219
  if block.get("type") == "text" and block.get("text")
223
- ]
224
- text = "\n".join(text_parts)
225
- user_content = text or None
220
+ )
226
221
 
227
222
  if user_content:
228
223
  payload_messages.append({"role": "user", "content": user_content})
@@ -32,7 +32,7 @@ class OpenAIResponsesAdapter(ProviderAdapter):
32
32
  label = "OpenAI Responses"
33
33
  default_base_url = "https://api.openai.com/v1"
34
34
  env_api_key_names = ("OPENAI_API_KEY",)
35
- default_models = ("gpt-5.5", "gpt-5.4-mini")
35
+ default_models = ("gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna")
36
36
  supports_reasoning_effort = True
37
37
 
38
38
  @override
@@ -15,7 +15,7 @@ import asyncio
15
15
  import json
16
16
  import os
17
17
  import shutil
18
- from dataclasses import asdict, dataclass
18
+ from dataclasses import dataclass
19
19
  from datetime import UTC, datetime
20
20
  from pathlib import Path
21
21
  from typing import TypedDict, cast
@@ -23,6 +23,9 @@ from typing import TypedDict, cast
23
23
  from mycode.messages import ConversationMessage, flatten_message_text
24
24
 
25
25
  DEFAULT_SESSION_TITLE = "New chat"
26
+ # Session meta keeps only these stable fields. Per-turn state like provider /
27
+ # model / api_base lives on each ConversationMessage and would drift after
28
+ # ``/model`` switches.
26
29
  META_KEYS = ("cwd", "title", "created_at", "updated_at")
27
30
 
28
31
 
@@ -67,20 +70,6 @@ def apply_rewind(messages: list[ConversationMessage]) -> list[ConversationMessag
67
70
  # ---------------------------------------------------------------------
68
71
 
69
72
 
70
- @dataclass
71
- class SessionMeta:
72
- """Session metadata persisted to meta.json.
73
-
74
- Excludes per-turn state like provider / model / api_base — those live on
75
- each ConversationMessage and would drift after ``/model`` switches.
76
- """
77
-
78
- cwd: str
79
- title: str
80
- created_at: str
81
- updated_at: str
82
-
83
-
84
73
  SessionMetaDict = dict[str, object]
85
74
  SessionIndex = dict[str, SessionMetaDict]
86
75
 
@@ -184,14 +173,12 @@ class SessionStore:
184
173
  """Create the on-disk session directory with a fresh meta.json."""
185
174
 
186
175
  now = _now()
187
- meta = asdict(
188
- SessionMeta(
189
- cwd=os.path.abspath(cwd),
190
- title=DEFAULT_SESSION_TITLE,
191
- created_at=now,
192
- updated_at=now,
193
- )
194
- )
176
+ meta: SessionMetaDict = {
177
+ "cwd": os.path.abspath(cwd),
178
+ "title": DEFAULT_SESSION_TITLE,
179
+ "created_at": now,
180
+ "updated_at": now,
181
+ }
195
182
 
196
183
  def write_files() -> None:
197
184
  session_dir = self.session_dir(session_id)
@@ -785,7 +785,6 @@ def bash_tool(ctx: ToolContext, command: str, timeout: int | None = None) -> Too
785
785
  stderr=subprocess.STDOUT,
786
786
  text=True,
787
787
  bufsize=1,
788
- universal_newlines=True,
789
788
  start_new_session=os.name == "posix",
790
789
  )
791
790
  ctx.track_proc(proc)
@@ -823,10 +822,10 @@ def bash_tool(ctx: ToolContext, command: str, timeout: int | None = None) -> Too
823
822
 
824
823
  line = line.rstrip("\n")
825
824
  total_line_count += 1
826
- kept_bytes += len(line.encode("utf-8")) + 1
827
825
 
828
826
  if log_file is None:
829
827
  kept_lines.append(line)
828
+ kept_bytes += len(line.encode("utf-8")) + 1
830
829
  if kept_bytes > _BASH_MAX_IN_MEMORY_BYTES:
831
830
  # Spill to disk. Create the output dir lazily on first
832
831
  # spill so runs that stay in memory never touch the FS.
@@ -864,7 +863,7 @@ def bash_tool(ctx: ToolContext, command: str, timeout: int | None = None) -> Too
864
863
 
865
864
  # If truncation happened but we never spilled, save the full output
866
865
  # now so the user can inspect it via the "Full output" hint.
867
- if log_file is None and trunc.truncated:
866
+ if log_file is None and trunc.truncated_by:
868
867
  try:
869
868
  ctx.tool_output_dir.mkdir(parents=True, exist_ok=True)
870
869
  log_path.write_text(raw_output, encoding="utf-8")
@@ -874,7 +873,7 @@ def bash_tool(ctx: ToolContext, command: str, timeout: int | None = None) -> Too
874
873
 
875
874
  result = content
876
875
  shown_lines = trunc.output_lines
877
- was_truncated = log_file is not None or trunc.truncated
876
+ was_truncated = log_file is not None or trunc.truncated_by is not None
878
877
  if was_truncated:
879
878
  if trunc.truncated_by == "bytes":
880
879
  if total_line_count <= 1:
@@ -980,10 +979,9 @@ def _atomic_write_text(path: Path, content: str, *, newline: str | None = None)
980
979
 
981
980
  @dataclass(frozen=True)
982
981
  class Truncation:
983
- truncated: bool
982
+ # "lines" or "bytes"; None when nothing was cut.
984
983
  truncated_by: str | None
985
984
  output_lines: int
986
- output_bytes: int
987
985
 
988
986
 
989
987
  def truncate_text(
@@ -1020,26 +1018,14 @@ def truncate_text(
1020
1018
  encoded = target.encode("utf-8")
1021
1019
  sliced = encoded[-max_bytes:] if tail else encoded[:max_bytes]
1022
1020
  content = sliced.decode("utf-8", errors="ignore")
1023
- return content, Truncation(
1024
- truncated=True,
1025
- truncated_by="bytes",
1026
- output_lines=1,
1027
- output_bytes=len(sliced),
1028
- )
1021
+ return content, Truncation(truncated_by="bytes", output_lines=1)
1029
1022
 
1030
1023
  content = "\n".join(out_lines)
1031
- truncated = len(out_lines) < len(lines) or out_bytes < total_bytes
1032
1024
 
1033
1025
  truncated_by: str | None = None
1034
- if truncated:
1035
- if len(out_lines) < len(lines):
1036
- truncated_by = "lines" if len(out_lines) == max_lines else "bytes"
1037
- else:
1038
- truncated_by = "bytes"
1026
+ if len(out_lines) < len(lines):
1027
+ truncated_by = "lines" if len(out_lines) == max_lines else "bytes"
1028
+ elif out_bytes < total_bytes:
1029
+ truncated_by = "bytes"
1039
1030
 
1040
- return content, Truncation(
1041
- truncated=truncated,
1042
- truncated_by=truncated_by,
1043
- output_lines=len(out_lines),
1044
- output_bytes=out_bytes,
1045
- )
1031
+ return content, Truncation(truncated_by=truncated_by, output_lines=len(out_lines))
@@ -2,7 +2,6 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import json
6
5
  from pathlib import Path
7
6
  from typing import Any
8
7
 
@@ -29,18 +28,3 @@ def as_bool(value: Any) -> bool | None:
29
28
  def omit_none(d: dict[str, Any]) -> dict[str, Any]:
30
29
  """Return a shallow copy of d with None values removed."""
31
30
  return {k: v for k, v in d.items() if v is not None}
32
-
33
-
34
- def parse_tool_arguments(raw: str | None) -> dict[str, Any] | None:
35
- """Parse a JSON tool-arguments string.
36
-
37
- Returns the parsed dict, or None when the input is missing/invalid/non-object.
38
- Empty / None input is treated as an empty argument set.
39
- """
40
- if not raw or not raw.strip():
41
- return {}
42
- try:
43
- parsed = json.loads(raw)
44
- except json.JSONDecodeError:
45
- return None
46
- return parsed if isinstance(parsed, dict) else None
File without changes
File without changes
File without changes