mycode-sdk 0.9.4__tar.gz → 0.10.0__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.
- {mycode_sdk-0.9.4 → mycode_sdk-0.10.0}/PKG-INFO +6 -6
- {mycode_sdk-0.9.4 → mycode_sdk-0.10.0}/pyproject.toml +7 -7
- {mycode_sdk-0.9.4 → mycode_sdk-0.10.0}/src/mycode/__init__.py +2 -0
- {mycode_sdk-0.9.4 → mycode_sdk-0.10.0}/src/mycode/agent.py +93 -55
- {mycode_sdk-0.9.4 → mycode_sdk-0.10.0}/src/mycode/compact.py +19 -0
- {mycode_sdk-0.9.4 → mycode_sdk-0.10.0}/src/mycode/messages.py +2 -2
- {mycode_sdk-0.9.4 → mycode_sdk-0.10.0}/src/mycode/models.py +18 -8
- {mycode_sdk-0.9.4 → mycode_sdk-0.10.0}/src/mycode/models_catalog.json +142 -114
- {mycode_sdk-0.9.4 → mycode_sdk-0.10.0}/src/mycode/providers/anthropic_like.py +3 -2
- {mycode_sdk-0.9.4 → mycode_sdk-0.10.0}/src/mycode/providers/base.py +9 -4
- {mycode_sdk-0.9.4 → mycode_sdk-0.10.0}/src/mycode/providers/gemini.py +8 -8
- {mycode_sdk-0.9.4 → mycode_sdk-0.10.0}/src/mycode/providers/openai_chat.py +13 -18
- {mycode_sdk-0.9.4 → mycode_sdk-0.10.0}/src/mycode/providers/openai_responses.py +5 -6
- {mycode_sdk-0.9.4 → mycode_sdk-0.10.0}/src/mycode/session.py +14 -30
- {mycode_sdk-0.9.4 → mycode_sdk-0.10.0}/src/mycode/tools.py +18 -39
- {mycode_sdk-0.9.4 → mycode_sdk-0.10.0}/src/mycode/utils.py +0 -16
- {mycode_sdk-0.9.4 → mycode_sdk-0.10.0}/.gitignore +0 -0
- {mycode_sdk-0.9.4 → mycode_sdk-0.10.0}/LICENSE +0 -0
- {mycode_sdk-0.9.4 → mycode_sdk-0.10.0}/README.md +0 -0
- {mycode_sdk-0.9.4 → mycode_sdk-0.10.0}/src/mycode/attachments.py +0 -0
- {mycode_sdk-0.9.4 → mycode_sdk-0.10.0}/src/mycode/hooks.py +0 -0
- {mycode_sdk-0.9.4 → mycode_sdk-0.10.0}/src/mycode/providers/__init__.py +0 -0
- {mycode_sdk-0.9.4 → mycode_sdk-0.10.0}/src/mycode/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mycode-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
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.
|
|
22
|
-
Requires-Dist: google-genai>=2.
|
|
23
|
-
Requires-Dist: griffelib>=2.
|
|
24
|
-
Requires-Dist: openai>=2.
|
|
25
|
-
Requires-Dist: pydantic>=2.13.
|
|
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.
|
|
7
|
+
version = "0.10.0"
|
|
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.
|
|
27
|
-
"google-genai>=2.
|
|
28
|
-
"griffelib>=2.
|
|
29
|
-
"openai>=2.
|
|
30
|
-
"pydantic>=2.13.
|
|
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]
|
|
@@ -4,6 +4,7 @@ from importlib import metadata
|
|
|
4
4
|
|
|
5
5
|
from mycode.agent import Agent, Event, PersistCallback, RunResult
|
|
6
6
|
from mycode.attachments import Attachment
|
|
7
|
+
from mycode.compact import NothingToCompactError
|
|
7
8
|
from mycode.hooks import AfterToolHook, BeforeToolHook, HookResult, Hooks, ToolHookContext
|
|
8
9
|
from mycode.messages import (
|
|
9
10
|
ContentBlock,
|
|
@@ -45,6 +46,7 @@ __all__ = [
|
|
|
45
46
|
"BeforeToolHook",
|
|
46
47
|
"HookResult",
|
|
47
48
|
"Hooks",
|
|
49
|
+
"NothingToCompactError",
|
|
48
50
|
"PersistCallback",
|
|
49
51
|
"RunResult",
|
|
50
52
|
"SessionStore",
|
|
@@ -23,7 +23,9 @@ from mycode.attachments import AttachmentLike, build_attachment_blocks
|
|
|
23
23
|
from mycode.compact import (
|
|
24
24
|
COMPACT_SUMMARY_PROMPT,
|
|
25
25
|
DEFAULT_COMPACT_THRESHOLD,
|
|
26
|
+
NothingToCompactError,
|
|
26
27
|
build_compact_event,
|
|
28
|
+
has_compactable_history,
|
|
27
29
|
should_compact,
|
|
28
30
|
)
|
|
29
31
|
from mycode.hooks import Hooks, ToolHookContext
|
|
@@ -152,16 +154,10 @@ class Agent:
|
|
|
152
154
|
# (e.g. bash spill files) live next to the session JSONL; without a
|
|
153
155
|
# session, fall back to a tempdir scoped to ``session_id``.
|
|
154
156
|
if session_dir is not None:
|
|
155
|
-
tool_output_dir = session_dir / self.session_id / "tool-output"
|
|
157
|
+
self.tool_output_dir = session_dir / self.session_id / "tool-output"
|
|
156
158
|
else:
|
|
157
|
-
tool_output_dir = Path(tempfile.gettempdir()) / "mycode" / self.session_id / "tool-output"
|
|
159
|
+
self.tool_output_dir = Path(tempfile.gettempdir()) / "mycode" / self.session_id / "tool-output"
|
|
158
160
|
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
161
|
|
|
166
162
|
self.refresh_capabilities(
|
|
167
163
|
max_tokens=max_tokens,
|
|
@@ -201,7 +197,6 @@ class Agent:
|
|
|
201
197
|
self.supports_reasoning: bool | None = meta.supports_reasoning
|
|
202
198
|
self.supports_image_input: bool = bool(meta.supports_image_input)
|
|
203
199
|
self.supports_pdf_input: bool = bool(meta.supports_pdf_input)
|
|
204
|
-
self.tool_ctx.supports_image_input = self.supports_image_input
|
|
205
200
|
|
|
206
201
|
def cancel(self) -> None:
|
|
207
202
|
"""Request cancellation of the in-flight turn."""
|
|
@@ -330,11 +325,11 @@ class Agent:
|
|
|
330
325
|
output = "\n".join([*output_parts, "error: cancelled"]) if output_parts else "error: cancelled"
|
|
331
326
|
yield self._error_done(tool_id, output)
|
|
332
327
|
return
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
result = await task
|
|
331
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
332
|
+
result = ToolExecutionResult(output=f"error: {exc}", is_error=True)
|
|
338
333
|
|
|
339
334
|
yield await self._finish_tool_call(tool_id, hook_ctx, result)
|
|
340
335
|
|
|
@@ -360,13 +355,11 @@ class Agent:
|
|
|
360
355
|
*,
|
|
361
356
|
emit: Callable[[str], None] | None = None,
|
|
362
357
|
) -> ToolContext:
|
|
363
|
-
"""Build a per-call ToolContext from the base context."""
|
|
364
|
-
|
|
365
358
|
return ToolContext(
|
|
366
359
|
executor=self.tools,
|
|
367
|
-
cwd=self.
|
|
368
|
-
tool_output_dir=self.
|
|
369
|
-
supports_image_input=self.
|
|
360
|
+
cwd=self.cwd,
|
|
361
|
+
tool_output_dir=self.tool_output_dir,
|
|
362
|
+
supports_image_input=self.supports_image_input,
|
|
370
363
|
tool_call_id=tool_id,
|
|
371
364
|
emit=emit,
|
|
372
365
|
)
|
|
@@ -402,15 +395,12 @@ class Agent:
|
|
|
402
395
|
|
|
403
396
|
provider_stream: AsyncIterator[ProviderStreamEvent] = adapter.stream_turn(request)
|
|
404
397
|
|
|
405
|
-
async def next_provider_event() -> ProviderStreamEvent:
|
|
406
|
-
return await anext(provider_stream)
|
|
407
|
-
|
|
408
398
|
try:
|
|
409
399
|
while True:
|
|
410
400
|
if self._cancel_event.is_set():
|
|
411
401
|
raise asyncio.CancelledError
|
|
412
402
|
|
|
413
|
-
self._provider_event_task = asyncio.
|
|
403
|
+
self._provider_event_task = asyncio.ensure_future(anext(provider_stream))
|
|
414
404
|
try:
|
|
415
405
|
yield await self._provider_event_task
|
|
416
406
|
except StopAsyncIteration:
|
|
@@ -467,6 +457,23 @@ class Agent:
|
|
|
467
457
|
block["meta"] = {**meta, "duration_ms": duration_ms}
|
|
468
458
|
return
|
|
469
459
|
|
|
460
|
+
async def _persist_message(
|
|
461
|
+
self,
|
|
462
|
+
message: ConversationMessage,
|
|
463
|
+
on_persist: PersistCallback | None,
|
|
464
|
+
) -> None:
|
|
465
|
+
"""Persist one message: caller callback first, then the SDK session store."""
|
|
466
|
+
|
|
467
|
+
if on_persist is not None:
|
|
468
|
+
# Callers may need to write related records before the SDK
|
|
469
|
+
# appends this message to its own session log.
|
|
470
|
+
await on_persist(message)
|
|
471
|
+
if self._store is None:
|
|
472
|
+
return
|
|
473
|
+
if not self._store.session_exists(self.session_id):
|
|
474
|
+
await self._store.create_session(self.session_id, cwd=self.cwd)
|
|
475
|
+
await self._store.append_message(self.session_id, message)
|
|
476
|
+
|
|
470
477
|
# ------------------------------------------------------------------
|
|
471
478
|
# Public entry points
|
|
472
479
|
# ------------------------------------------------------------------
|
|
@@ -481,15 +488,7 @@ class Agent:
|
|
|
481
488
|
"""Run the full agent loop for one user message."""
|
|
482
489
|
|
|
483
490
|
async def persist(message: ConversationMessage) -> None:
|
|
484
|
-
|
|
485
|
-
# Callers may need to write related records before the SDK
|
|
486
|
-
# appends this message to its own session log.
|
|
487
|
-
await on_persist(message)
|
|
488
|
-
if self._store is None:
|
|
489
|
-
return
|
|
490
|
-
if not self._store.session_exists(self.session_id):
|
|
491
|
-
await self._store.create_session(self.session_id, cwd=self.cwd)
|
|
492
|
-
await self._store.append_message(self.session_id, message)
|
|
491
|
+
await self._persist_message(message, on_persist)
|
|
493
492
|
|
|
494
493
|
self._cancel_event.clear()
|
|
495
494
|
|
|
@@ -529,7 +528,10 @@ class Agent:
|
|
|
529
528
|
adapter = get_provider_adapter(self.provider)
|
|
530
529
|
|
|
531
530
|
turn_number = 0
|
|
532
|
-
while
|
|
531
|
+
while True:
|
|
532
|
+
if self.max_turns is not None and turn_number >= self.max_turns:
|
|
533
|
+
yield Event("error", {"message": "max_turns reached"})
|
|
534
|
+
return
|
|
533
535
|
turn_number += 1
|
|
534
536
|
if self._cancel_event.is_set():
|
|
535
537
|
yield Event("error", {"message": "cancelled"})
|
|
@@ -632,13 +634,15 @@ class Agent:
|
|
|
632
634
|
|
|
633
635
|
total_tokens = meta.get("total_tokens")
|
|
634
636
|
if total_tokens:
|
|
635
|
-
|
|
636
|
-
"
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
637
|
+
yield Event(
|
|
638
|
+
"usage",
|
|
639
|
+
{
|
|
640
|
+
"total_tokens": total_tokens,
|
|
641
|
+
"model": meta.get("model") or self.model,
|
|
642
|
+
"provider": meta.get("provider") or self.provider,
|
|
643
|
+
"context_window": meta["context_window"],
|
|
644
|
+
},
|
|
645
|
+
)
|
|
642
646
|
|
|
643
647
|
tool_calls = [
|
|
644
648
|
block
|
|
@@ -665,11 +669,10 @@ class Agent:
|
|
|
665
669
|
)
|
|
666
670
|
)
|
|
667
671
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
return
|
|
672
|
+
if self._cancel_event.is_set():
|
|
673
|
+
# Skip remaining tool calls; the results collected so
|
|
674
|
+
# far are still persisted below.
|
|
675
|
+
break
|
|
673
676
|
|
|
674
677
|
tool_result_message = build_message("user", tool_results)
|
|
675
678
|
self.messages.append(tool_result_message)
|
|
@@ -679,7 +682,7 @@ class Agent:
|
|
|
679
682
|
return
|
|
680
683
|
if should_compact(total_tokens, self.context_window, self.compact_threshold):
|
|
681
684
|
try:
|
|
682
|
-
await self._compact(adapter,
|
|
685
|
+
await self._compact(adapter, on_persist)
|
|
683
686
|
yield Event("compact", {})
|
|
684
687
|
except asyncio.CancelledError:
|
|
685
688
|
yield Event("error", {"message": "cancelled"})
|
|
@@ -693,11 +696,7 @@ class Agent:
|
|
|
693
696
|
)
|
|
694
697
|
|
|
695
698
|
if not tool_calls:
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
else:
|
|
699
|
-
yield Event("error", {"message": "max_turns reached"})
|
|
700
|
-
return
|
|
699
|
+
return
|
|
701
700
|
|
|
702
701
|
def run(
|
|
703
702
|
self,
|
|
@@ -731,12 +730,47 @@ class Agent:
|
|
|
731
730
|
# Context compaction
|
|
732
731
|
# ------------------------------------------------------------------
|
|
733
732
|
|
|
733
|
+
async def acompact(
|
|
734
|
+
self,
|
|
735
|
+
*,
|
|
736
|
+
on_persist: PersistCallback | None = None,
|
|
737
|
+
) -> ConversationMessage:
|
|
738
|
+
"""Compact the conversation now and return the persisted compact marker.
|
|
739
|
+
|
|
740
|
+
Raises :class:`NothingToCompactError` when no new context follows the
|
|
741
|
+
latest compact marker, and :class:`asyncio.CancelledError` when
|
|
742
|
+
:meth:`cancel` stops the summary request.
|
|
743
|
+
"""
|
|
744
|
+
|
|
745
|
+
self._cancel_event.clear()
|
|
746
|
+
adapter = get_provider_adapter(self.provider)
|
|
747
|
+
return await self._compact(adapter, on_persist)
|
|
748
|
+
|
|
749
|
+
def compact(
|
|
750
|
+
self,
|
|
751
|
+
*,
|
|
752
|
+
on_persist: PersistCallback | None = None,
|
|
753
|
+
) -> ConversationMessage:
|
|
754
|
+
"""Compact the conversation synchronously; see :meth:`acompact`."""
|
|
755
|
+
|
|
756
|
+
try:
|
|
757
|
+
asyncio.get_running_loop()
|
|
758
|
+
except RuntimeError:
|
|
759
|
+
pass
|
|
760
|
+
else:
|
|
761
|
+
raise RuntimeError("Agent.compact() cannot run inside an active event loop; use Agent.acompact() instead")
|
|
762
|
+
|
|
763
|
+
return asyncio.run(self.acompact(on_persist=on_persist))
|
|
764
|
+
|
|
734
765
|
async def _compact(
|
|
735
766
|
self,
|
|
736
767
|
adapter: ProviderAdapter,
|
|
737
|
-
|
|
738
|
-
) ->
|
|
739
|
-
"""Ask the provider for a summary, persist the compact
|
|
768
|
+
on_persist: PersistCallback | None,
|
|
769
|
+
) -> ConversationMessage:
|
|
770
|
+
"""Ask the provider for a summary, persist and append the compact marker."""
|
|
771
|
+
|
|
772
|
+
if not has_compactable_history(self.messages):
|
|
773
|
+
raise NothingToCompactError("nothing to compact")
|
|
740
774
|
|
|
741
775
|
request = self._build_request(
|
|
742
776
|
tools=[],
|
|
@@ -758,6 +792,9 @@ class Agent:
|
|
|
758
792
|
if not summary_text:
|
|
759
793
|
raise ValueError("compaction produced empty summary")
|
|
760
794
|
|
|
795
|
+
if self._cancel_event.is_set():
|
|
796
|
+
raise asyncio.CancelledError
|
|
797
|
+
|
|
761
798
|
summary_total_tokens = (summary_message.get("meta") or {}).get("total_tokens")
|
|
762
799
|
compact_event = build_compact_event(
|
|
763
800
|
summary_text,
|
|
@@ -766,5 +803,6 @@ class Agent:
|
|
|
766
803
|
total_tokens=summary_total_tokens,
|
|
767
804
|
)
|
|
768
805
|
|
|
769
|
-
await
|
|
806
|
+
await self._persist_message(compact_event, on_persist)
|
|
770
807
|
self.messages.append(compact_event)
|
|
808
|
+
return compact_event
|
|
@@ -8,6 +8,11 @@ from mycode.messages import ConversationMessage, build_message, text_block
|
|
|
8
8
|
|
|
9
9
|
DEFAULT_COMPACT_THRESHOLD = 0.8
|
|
10
10
|
|
|
11
|
+
|
|
12
|
+
class NothingToCompactError(ValueError):
|
|
13
|
+
"""Raised when there is no new context to compact past the latest marker."""
|
|
14
|
+
|
|
15
|
+
|
|
11
16
|
COMPACT_SUMMARY_PROMPT = """\
|
|
12
17
|
Summarize this conversation to create a continuation document. \
|
|
13
18
|
This summary will replace the full conversation history, so it must \
|
|
@@ -65,6 +70,20 @@ def should_compact(
|
|
|
65
70
|
return last_total_tokens >= context_window * threshold
|
|
66
71
|
|
|
67
72
|
|
|
73
|
+
def has_compactable_history(messages: list[ConversationMessage]) -> bool:
|
|
74
|
+
"""True when at least one non-empty user/assistant message follows the latest compact marker."""
|
|
75
|
+
|
|
76
|
+
last_compact = -1
|
|
77
|
+
for i, message in enumerate(messages):
|
|
78
|
+
if message.get("role") == "compact":
|
|
79
|
+
last_compact = i
|
|
80
|
+
|
|
81
|
+
return any(
|
|
82
|
+
message.get("role") in ("user", "assistant") and message.get("content")
|
|
83
|
+
for message in messages[last_compact + 1 :]
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
68
87
|
def build_compact_event(
|
|
69
88
|
summary_text: str,
|
|
70
89
|
*,
|
|
@@ -165,8 +165,8 @@ def flatten_message_text(message: ConversationMessage, *, include_thinking: bool
|
|
|
165
165
|
continue
|
|
166
166
|
raw_meta = block.get("meta")
|
|
167
167
|
meta = raw_meta if isinstance(raw_meta, dict) else {}
|
|
168
|
-
#
|
|
169
|
-
if meta.get("attachment"):
|
|
168
|
+
# Local payload blocks should not become session titles or history labels.
|
|
169
|
+
if meta.get("attachment") or meta.get("skill_snapshot"):
|
|
170
170
|
continue
|
|
171
171
|
btype = block.get("type")
|
|
172
172
|
if btype == "text" or (include_thinking and btype == "thinking"):
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import functools
|
|
6
5
|
import json
|
|
7
6
|
from dataclasses import dataclass, replace
|
|
7
|
+
from functools import cache
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
@@ -30,14 +30,14 @@ class ModelMetadata:
|
|
|
30
30
|
supports_pdf_input: bool | None = None
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
@
|
|
33
|
+
@cache
|
|
34
34
|
def load_models_catalog() -> dict[str, Any] | None:
|
|
35
35
|
"""Load the bundled model catalog from disk once per process."""
|
|
36
36
|
|
|
37
37
|
try:
|
|
38
38
|
data = json.loads(_MODELS_CATALOG_PATH.read_text(encoding="utf-8"))
|
|
39
|
-
except
|
|
40
|
-
|
|
39
|
+
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
40
|
+
return None
|
|
41
41
|
return data if isinstance(data, dict) else None
|
|
42
42
|
|
|
43
43
|
|
|
@@ -66,16 +66,26 @@ def resolve_model_metadata(
|
|
|
66
66
|
*,
|
|
67
67
|
provider: str,
|
|
68
68
|
model: str,
|
|
69
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
|