symphony-core 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
core_ai/__init__.py ADDED
@@ -0,0 +1,81 @@
1
+ from core_ai.content import (
2
+ Content,
3
+ IMAGE_MIME_BY_SUFFIX,
4
+ estimate_content_tokens,
5
+ image_part,
6
+ image_part_from_bytes,
7
+ normalize_content,
8
+ sniff_image_media_type,
9
+ split_text_and_images,
10
+ text_from_content,
11
+ to_anthropic_blocks,
12
+ to_gemini_parts,
13
+ to_openai_chat_content,
14
+ to_openai_responses_content,
15
+ )
16
+ from core_ai.models import ModelAPI, ModelCatalog, ModelInfo, get_model, list_models
17
+ from core_ai.providers.anthropic import AnthropicProvider
18
+ from core_ai.providers.base import BaseProvider
19
+ from core_ai.providers.catalog import (
20
+ PROVIDERS,
21
+ MissingProviderCredentials,
22
+ ProviderSpec,
23
+ configured_provider_ids,
24
+ find_provider,
25
+ get_provider,
26
+ has_configured_provider,
27
+ provider_is_configured,
28
+ )
29
+ from core_ai.providers.defaults import build_default_registry, default_model_id
30
+ from core_ai.providers.gemini import GeminiProvider
31
+ from core_ai.providers.grok import GrokProvider
32
+ from core_ai.providers.local import LocalProvider
33
+ from core_ai.providers.ollama import OllamaProvider
34
+ from core_ai.providers.openai import OpenAIProvider
35
+ from core_ai.providers.openrouter import OpenRouterProvider
36
+ from core_ai.providers.vercel import VercelProvider
37
+ from core_ai.registry import ModelRegistry
38
+ from core_ai.types import Message, StreamEvent
39
+
40
+ __all__ = [
41
+ "AnthropicProvider",
42
+ "BaseProvider",
43
+ "Content",
44
+ "GeminiProvider",
45
+ "GrokProvider",
46
+ "IMAGE_MIME_BY_SUFFIX",
47
+ "LocalProvider",
48
+ "Message",
49
+ "MissingProviderCredentials",
50
+ "ModelAPI",
51
+ "ModelCatalog",
52
+ "ModelInfo",
53
+ "ModelRegistry",
54
+ "OllamaProvider",
55
+ "OpenAIProvider",
56
+ "OpenRouterProvider",
57
+ "PROVIDERS",
58
+ "ProviderSpec",
59
+ "StreamEvent",
60
+ "VercelProvider",
61
+ "build_default_registry",
62
+ "configured_provider_ids",
63
+ "default_model_id",
64
+ "estimate_content_tokens",
65
+ "find_provider",
66
+ "get_model",
67
+ "get_provider",
68
+ "has_configured_provider",
69
+ "image_part",
70
+ "image_part_from_bytes",
71
+ "list_models",
72
+ "normalize_content",
73
+ "provider_is_configured",
74
+ "sniff_image_media_type",
75
+ "split_text_and_images",
76
+ "text_from_content",
77
+ "to_anthropic_blocks",
78
+ "to_gemini_parts",
79
+ "to_openai_chat_content",
80
+ "to_openai_responses_content",
81
+ ]
core_ai/content.py ADDED
@@ -0,0 +1,384 @@
1
+ """Provider-neutral multimodal content helpers.
2
+
3
+ Canonical user/assistant parts look like:
4
+
5
+ {"type": "text", "text": "hello"}
6
+ {"type": "image", "media_type": "image/png", "data": "<base64>", "filename": "shot.png"}
7
+ {"type": "image", "media_type": "image/jpeg", "url": "https://example.com/a.jpg"}
8
+
9
+ Providers translate this shape into their native request payloads. Incoming
10
+ OpenAI / Anthropic / Gemini image parts are normalized back to the canonical
11
+ form so mixed histories keep working.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import base64
17
+ import re
18
+ from typing import Any, Dict, Iterable, List, Optional, Sequence
19
+ from urllib.parse import unquote, urlparse
20
+
21
+ from core_ai.types import Content
22
+
23
+ ContentPart = Dict[str, Any]
24
+
25
+ IMAGE_TOKEN_ESTIMATE = 768
26
+ IMAGE_MIME_BY_SUFFIX = {
27
+ ".png": "image/png",
28
+ ".jpg": "image/jpeg",
29
+ ".jpeg": "image/jpeg",
30
+ ".gif": "image/gif",
31
+ ".webp": "image/webp",
32
+ ".bmp": "image/bmp",
33
+ ".tif": "image/tiff",
34
+ ".tiff": "image/tiff",
35
+ }
36
+ _DATA_URL_RE = re.compile(
37
+ r"^data:(?P<media>image/[A-Za-z0-9.+-]+);base64,(?P<data>[A-Za-z0-9+/=\s]+)$",
38
+ re.IGNORECASE,
39
+ )
40
+ _MAGIC_TYPES: tuple[tuple[bytes, str], ...] = (
41
+ (b"\x89PNG\r\n\x1a\n", "image/png"),
42
+ (b"\xff\xd8\xff", "image/jpeg"),
43
+ (b"GIF87a", "image/gif"),
44
+ (b"GIF89a", "image/gif"),
45
+ (b"BM", "image/bmp"),
46
+ )
47
+
48
+
49
+ def text_from_content(content: Optional[Content]) -> str:
50
+ """Return the human-readable text from a message body."""
51
+ if content is None:
52
+ return ""
53
+ if isinstance(content, str):
54
+ return content
55
+ chunks: List[str] = []
56
+ for part in _iter_parts(content):
57
+ kind = _part_type(part)
58
+ if kind == "text":
59
+ text = str(part.get("text") or "")
60
+ if text:
61
+ chunks.append(text)
62
+ elif kind == "image":
63
+ chunks.append(f"[image:{_image_filename(part)}]")
64
+ return "\n".join(chunks)
65
+
66
+
67
+ def normalize_content(content: Optional[Content]) -> List[ContentPart]:
68
+ """Expand a message body into canonical text/image parts."""
69
+ if content is None or content == "":
70
+ return []
71
+ if isinstance(content, str):
72
+ return [{"type": "text", "text": content}]
73
+ parts: List[ContentPart] = []
74
+ for item in content:
75
+ part = _normalize_part(item)
76
+ if part is not None:
77
+ parts.append(part)
78
+ return parts
79
+
80
+
81
+ def to_openai_chat_content(content: Optional[Content]) -> Content:
82
+ """Chat Completions content: string or text / image_url parts."""
83
+ if isinstance(content, str) or content is None:
84
+ return content or ""
85
+ parts: List[ContentPart] = []
86
+ for part in normalize_content(content):
87
+ if part["type"] == "text":
88
+ parts.append({"type": "text", "text": part.get("text") or ""})
89
+ continue
90
+ url = _openai_image_url(part)
91
+ if url:
92
+ parts.append({"type": "image_url", "image_url": {"url": url}})
93
+ return parts
94
+
95
+
96
+ def to_openai_responses_content(
97
+ content: Optional[Content],
98
+ *,
99
+ role: str = "user",
100
+ typed_parts: bool = False,
101
+ ) -> Content:
102
+ """Responses API content: string, or input_text / input_image parts.
103
+
104
+ Codex rejects bare strings, so callers on that path pass `typed_parts=True`.
105
+ """
106
+ text_type = "output_text" if role == "assistant" else "input_text"
107
+ if isinstance(content, str) or content is None:
108
+ text = content or ""
109
+ if typed_parts:
110
+ return [{"type": text_type, "text": text}] if text else []
111
+ return text
112
+ parts: List[ContentPart] = []
113
+ for part in normalize_content(content):
114
+ if part["type"] == "text":
115
+ parts.append({"type": text_type, "text": part.get("text") or ""})
116
+ continue
117
+ url = _openai_image_url(part)
118
+ if url:
119
+ parts.append({"type": "input_image", "image_url": url})
120
+ return parts
121
+
122
+
123
+ def to_anthropic_blocks(content: Optional[Content]) -> List[ContentPart]:
124
+ """Anthropic Messages content blocks."""
125
+ blocks: List[ContentPart] = []
126
+ for part in normalize_content(content):
127
+ if part["type"] == "text":
128
+ text = part.get("text") or ""
129
+ if text:
130
+ blocks.append({"type": "text", "text": text})
131
+ continue
132
+ source = _anthropic_image_source(part)
133
+ if source is not None:
134
+ blocks.append({"type": "image", "source": source})
135
+ return blocks
136
+
137
+
138
+ def to_gemini_parts(content: Optional[Content]) -> List[ContentPart]:
139
+ """Gemini generateContent parts."""
140
+ parts: List[ContentPart] = []
141
+ for part in normalize_content(content):
142
+ if part["type"] == "text":
143
+ text = part.get("text") or ""
144
+ if text:
145
+ parts.append({"text": text})
146
+ continue
147
+ media_type = str(part.get("media_type") or "image/png")
148
+ data = part.get("data")
149
+ url = str(part.get("url") or "")
150
+ if data:
151
+ parts.append({"inlineData": {"mimeType": media_type, "data": data}})
152
+ elif url.startswith(("http://", "https://")):
153
+ parts.append({"fileData": {"mimeType": media_type, "fileUri": url}})
154
+ elif url:
155
+ parsed = _parse_data_url(url)
156
+ if parsed is not None:
157
+ parts.append(
158
+ {
159
+ "inlineData": {
160
+ "mimeType": parsed[0],
161
+ "data": parsed[1],
162
+ }
163
+ }
164
+ )
165
+ return parts
166
+
167
+
168
+ def image_part(
169
+ *,
170
+ media_type: str,
171
+ data: Optional[str] = None,
172
+ url: Optional[str] = None,
173
+ filename: Optional[str] = None,
174
+ ) -> ContentPart:
175
+ part: ContentPart = {"type": "image", "media_type": media_type}
176
+ if data:
177
+ part["data"] = data
178
+ if url:
179
+ part["url"] = url
180
+ if filename:
181
+ part["filename"] = filename
182
+ return part
183
+
184
+
185
+ def image_part_from_bytes(
186
+ payload: bytes,
187
+ *,
188
+ media_type: str,
189
+ filename: Optional[str] = None,
190
+ ) -> ContentPart:
191
+ return image_part(
192
+ media_type=media_type,
193
+ data=base64.b64encode(payload).decode("ascii"),
194
+ filename=filename,
195
+ )
196
+
197
+
198
+ def sniff_image_media_type(data: bytes, *, filename: str = "") -> Optional[str]:
199
+ """Return an image media type from a filename suffix or magic bytes."""
200
+ name = filename.rsplit("/", 1)[-1].rsplit("\\", 1)[-1]
201
+ suffix = ""
202
+ if "." in name:
203
+ suffix = "." + name.rsplit(".", 1)[-1].lower()
204
+ if suffix in IMAGE_MIME_BY_SUFFIX:
205
+ return IMAGE_MIME_BY_SUFFIX[suffix]
206
+ if data.startswith(b"RIFF") and data[8:12] == b"WEBP":
207
+ return "image/webp"
208
+ if data[:4] in {b"II*\x00", b"MM\x00*"}:
209
+ return "image/tiff"
210
+ for magic, media_type in _MAGIC_TYPES:
211
+ if data.startswith(magic):
212
+ return media_type
213
+ return None
214
+
215
+
216
+ def split_text_and_images(content: Optional[Content]) -> tuple[str, List[ContentPart]]:
217
+ """Split a message/tool body into text and canonical image parts."""
218
+ if content is None:
219
+ return "", []
220
+ if isinstance(content, str):
221
+ return content, []
222
+ texts: List[str] = []
223
+ images: List[ContentPart] = []
224
+ for part in normalize_content(content):
225
+ if part["type"] == "text":
226
+ text = str(part.get("text") or "")
227
+ if text:
228
+ texts.append(text)
229
+ elif part["type"] == "image":
230
+ images.append(part)
231
+ text = "\n".join(texts)
232
+ if not text and images:
233
+ text = text_from_content(images)
234
+ return text, images
235
+
236
+
237
+ def estimate_content_tokens(content: Optional[Content]) -> int:
238
+ """Heuristic tokens for a message body, without dumping image bytes."""
239
+ if content is None:
240
+ return 0
241
+ if isinstance(content, str):
242
+ return _text_tokens(content)
243
+ total = 0
244
+ for part in normalize_content(content):
245
+ if part["type"] == "image":
246
+ total += IMAGE_TOKEN_ESTIMATE
247
+ else:
248
+ total += _text_tokens(str(part.get("text") or ""))
249
+ return total
250
+
251
+
252
+ def _text_tokens(text: str) -> int:
253
+ if not text:
254
+ return 0
255
+ return max(1, (len(text) + 3) // 4)
256
+
257
+
258
+ def _iter_parts(content: Sequence[Any]) -> Iterable[Dict[str, Any]]:
259
+ for item in content:
260
+ if isinstance(item, dict):
261
+ yield item
262
+
263
+
264
+ def _part_type(part: Dict[str, Any]) -> str:
265
+ declared = str(part.get("type") or "")
266
+ if declared in {"text", "input_text", "output_text"}:
267
+ return "text"
268
+ if declared in {"image", "image_url", "input_image"}:
269
+ return "image"
270
+ if "inlineData" in part or "fileData" in part or "source" in part:
271
+ return "image"
272
+ if "text" in part:
273
+ return "text"
274
+ return declared or "unknown"
275
+
276
+
277
+ def _normalize_part(item: Any) -> Optional[ContentPart]:
278
+ if not isinstance(item, dict):
279
+ if item is None or item == "":
280
+ return None
281
+ return {"type": "text", "text": str(item)}
282
+
283
+ kind = _part_type(item)
284
+ if kind == "text":
285
+ text = item.get("text")
286
+ return {"type": "text", "text": "" if text is None else str(text)}
287
+
288
+ if kind != "image":
289
+ return None
290
+
291
+ media_type = str(item.get("media_type") or item.get("mime_type") or "")
292
+ data = item.get("data")
293
+ url = item.get("url")
294
+ filename = item.get("filename") or item.get("name")
295
+
296
+ nested = item.get("image_url")
297
+ if isinstance(nested, dict):
298
+ url = nested.get("url") or url
299
+ elif isinstance(nested, str) and nested:
300
+ url = nested
301
+
302
+ source = item.get("source")
303
+ if isinstance(source, dict):
304
+ media_type = str(source.get("media_type") or media_type)
305
+ if source.get("type") == "base64" and source.get("data"):
306
+ data = source.get("data")
307
+ elif source.get("url"):
308
+ url = source.get("url")
309
+
310
+ inline = item.get("inlineData") or item.get("inline_data")
311
+ if isinstance(inline, dict):
312
+ media_type = str(inline.get("mimeType") or inline.get("mime_type") or media_type)
313
+ data = inline.get("data") or data
314
+
315
+ file_data = item.get("fileData") or item.get("file_data")
316
+ if isinstance(file_data, dict):
317
+ media_type = str(file_data.get("mimeType") or file_data.get("mime_type") or media_type)
318
+ url = file_data.get("fileUri") or file_data.get("file_uri") or url
319
+
320
+ if isinstance(url, str):
321
+ parsed = _parse_data_url(url)
322
+ if parsed is not None:
323
+ media_type = media_type or parsed[0]
324
+ data = data or parsed[1]
325
+ url = None
326
+
327
+ if not media_type:
328
+ media_type = "image/png"
329
+
330
+ part = image_part(
331
+ media_type=media_type,
332
+ data=str(data) if data else None,
333
+ url=str(url) if url else None,
334
+ filename=str(filename) if filename else None,
335
+ )
336
+ if "data" not in part and "url" not in part:
337
+ return None
338
+ return part
339
+
340
+
341
+ def _image_filename(part: Dict[str, Any]) -> str:
342
+ name = part.get("filename")
343
+ if name:
344
+ return str(name)
345
+ url = str(part.get("url") or "")
346
+ if url:
347
+ path = unquote(urlparse(url).path)
348
+ leaf = path.rsplit("/", 1)[-1]
349
+ if leaf:
350
+ return leaf
351
+ media = str(part.get("media_type") or "image")
352
+ suffix = media.split("/", 1)[-1] or "png"
353
+ return f"image.{suffix}"
354
+
355
+
356
+ def _openai_image_url(part: Dict[str, Any]) -> Optional[str]:
357
+ data = part.get("data")
358
+ media_type = str(part.get("media_type") or "image/png")
359
+ if data:
360
+ return f"data:{media_type};base64,{data}"
361
+ url = str(part.get("url") or "")
362
+ return url or None
363
+
364
+
365
+ def _anthropic_image_source(part: Dict[str, Any]) -> Optional[Dict[str, Any]]:
366
+ data = part.get("data")
367
+ media_type = str(part.get("media_type") or "image/png")
368
+ if data:
369
+ return {"type": "base64", "media_type": media_type, "data": data}
370
+ url = str(part.get("url") or "")
371
+ if url.startswith(("http://", "https://")):
372
+ return {"type": "url", "url": url}
373
+ parsed = _parse_data_url(url) if url else None
374
+ if parsed is not None:
375
+ return {"type": "base64", "media_type": parsed[0], "data": parsed[1]}
376
+ return None
377
+
378
+
379
+ def _parse_data_url(value: str) -> Optional[tuple[str, str]]:
380
+ match = _DATA_URL_RE.match(value.strip())
381
+ if match is None:
382
+ return None
383
+ data = re.sub(r"\s+", "", match.group("data"))
384
+ return match.group("media").lower(), data
@@ -0,0 +1,18 @@
1
+ from core_ai.models.registry import (
2
+ ModelCatalog,
3
+ get_model,
4
+ list_models,
5
+ register_model,
6
+ unregister_model,
7
+ )
8
+ from core_ai.models.types import ModelAPI, ModelInfo
9
+
10
+ __all__ = [
11
+ "ModelAPI",
12
+ "ModelCatalog",
13
+ "ModelInfo",
14
+ "get_model",
15
+ "list_models",
16
+ "register_model",
17
+ "unregister_model",
18
+ ]
@@ -0,0 +1,110 @@
1
+ # Generated by scripts/generate_models.py. Do not edit by hand.
2
+
3
+ from core_ai.models.types import ModelInfo
4
+
5
+ OPENAI_MODELS = (
6
+ ModelInfo(id="gpt-4", provider="openai", api="responses", context_limit=8192),
7
+ ModelInfo(id="gpt-4-turbo", provider="openai", api="responses", context_limit=128000),
8
+ ModelInfo(id="gpt-4.1", provider="openai", api="responses", context_limit=1047576),
9
+ ModelInfo(id="gpt-4.1-mini", provider="openai", api="responses", context_limit=1047576),
10
+ ModelInfo(id="gpt-4.1-nano", provider="openai", api="responses", context_limit=1047576),
11
+ ModelInfo(id="gpt-4o", provider="openai", api="responses", context_limit=128000),
12
+ ModelInfo(id="gpt-4o-2024-05-13", provider="openai", api="responses", context_limit=128000),
13
+ ModelInfo(id="gpt-4o-2024-08-06", provider="openai", api="responses", context_limit=128000),
14
+ ModelInfo(id="gpt-4o-2024-11-20", provider="openai", api="responses", context_limit=128000),
15
+ ModelInfo(id="gpt-4o-mini", provider="openai", api="responses", context_limit=128000),
16
+ ModelInfo(id="gpt-5", provider="openai", api="responses", reasoning=True, thinking_level_map=(('off', None), ('minimal', 'minimal'), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=400000),
17
+ ModelInfo(id="gpt-5-mini", provider="openai", api="responses", reasoning=True, thinking_level_map=(('off', None), ('minimal', 'minimal'), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=400000),
18
+ ModelInfo(id="gpt-5-nano", provider="openai", api="responses", reasoning=True, thinking_level_map=(('off', None), ('minimal', 'minimal'), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=400000),
19
+ ModelInfo(id="gpt-5-pro", provider="openai", api="responses", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', None), ('medium', None), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=400000),
20
+ ModelInfo(id="gpt-5.1", provider="openai", api="responses", reasoning=True, thinking_level_map=(('off', 'none'), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=400000),
21
+ ModelInfo(id="gpt-5.2", provider="openai", api="responses", reasoning=True, thinking_level_map=(('off', 'none'), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', None)), context_limit=400000),
22
+ ModelInfo(id="gpt-5.2-chat-latest", provider="openai", api="responses", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', None), ('medium', 'medium'), ('high', None), ('xhigh', None), ('max', None)), context_limit=128000),
23
+ ModelInfo(id="gpt-5.2-pro", provider="openai", api="responses", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', None), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', None)), context_limit=400000),
24
+ ModelInfo(id="gpt-5.3-chat-latest", provider="openai", api="responses", context_limit=128000),
25
+ ModelInfo(id="gpt-5.3-codex", provider="openai", api="responses", reasoning=True, thinking_level_map=(('off', 'none'), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', None)), context_limit=400000),
26
+ ModelInfo(id="gpt-5.3-codex-spark", provider="openai", api="responses", reasoning=True, thinking_level_map=(('off', 'none'), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', None)), context_limit=128000),
27
+ ModelInfo(id="gpt-5.4", provider="openai", api="responses", reasoning=True, thinking_level_map=(('off', 'none'), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', None)), context_limit=1050000),
28
+ ModelInfo(id="gpt-5.4-mini", provider="openai", api="responses", reasoning=True, thinking_level_map=(('off', 'none'), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', None)), context_limit=400000),
29
+ ModelInfo(id="gpt-5.4-nano", provider="openai", api="responses", reasoning=True, thinking_level_map=(('off', 'none'), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', None)), context_limit=400000),
30
+ ModelInfo(id="gpt-5.4-pro", provider="openai", api="responses", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', None), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', None)), context_limit=1050000),
31
+ ModelInfo(id="gpt-5.5", provider="openai", api="responses", reasoning=True, thinking_level_map=(('off', 'none'), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', None)), context_limit=1050000),
32
+ ModelInfo(id="gpt-5.5-pro", provider="openai", api="responses", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', None), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', None)), context_limit=1050000),
33
+ ModelInfo(id="gpt-5.6", provider="openai", api="responses", reasoning=True, thinking_level_map=(('off', 'none'), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', 'max')), context_limit=1050000),
34
+ ModelInfo(id="gpt-5.6-luna", provider="openai", api="responses", reasoning=True, thinking_level_map=(('off', 'none'), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', 'max')), context_limit=1050000),
35
+ ModelInfo(id="gpt-5.6-sol", provider="openai", api="responses", reasoning=True, thinking_level_map=(('off', 'none'), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', 'max')), context_limit=1050000),
36
+ ModelInfo(id="gpt-5.6-terra", provider="openai", api="responses", reasoning=True, thinking_level_map=(('off', 'none'), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', 'max')), context_limit=1050000),
37
+ ModelInfo(id="gpt-6-astra", provider="openai", api="responses", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', 'max')), context_limit=1050000),
38
+ ModelInfo(id="gpt-6-luna", provider="openai", api="responses", reasoning=True, thinking_level_map=(('off', 'none'), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', 'max')), context_limit=1050000),
39
+ ModelInfo(id="gpt-6-sol", provider="openai", api="responses", reasoning=True, thinking_level_map=(('off', 'none'), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', 'max')), context_limit=1050000),
40
+ ModelInfo(id="gpt-6.1-sol", provider="openai", api="responses", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', 'max')), context_limit=1050000),
41
+ ModelInfo(id="gpt-daybreak-blue-latest", provider="openai", api="responses", reasoning=True, thinking_level_map=(('off', 'none'), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', 'max')), context_limit=1050000),
42
+ ModelInfo(id="gpt-daybreak-red-latest", provider="openai", api="responses", reasoning=True, thinking_level_map=(('off', 'none'), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', 'max')), context_limit=400000),
43
+ ModelInfo(id="gpt-image-1", provider="openai", api="responses", context_limit=0),
44
+ ModelInfo(id="gpt-image-1-mini", provider="openai", api="responses", context_limit=0),
45
+ ModelInfo(id="gpt-image-1.5", provider="openai", api="responses", context_limit=0),
46
+ ModelInfo(id="gpt-image-2", provider="openai", api="responses", context_limit=0),
47
+ ModelInfo(id="gpt-realtime-2.1", provider="openai", api="responses", reasoning=True, thinking_level_map=(('off', None), ('minimal', 'minimal'), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', None)), context_limit=128000),
48
+ ModelInfo(id="o1", provider="openai", api="chat_completions", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=200000),
49
+ ModelInfo(id="o1-pro", provider="openai", api="chat_completions", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=200000),
50
+ ModelInfo(id="o3", provider="openai", api="chat_completions", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=200000),
51
+ ModelInfo(id="o3-mini", provider="openai", api="chat_completions", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=200000),
52
+ ModelInfo(id="o3-pro", provider="openai", api="chat_completions", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=200000),
53
+ ModelInfo(id="o4-mini", provider="openai", api="chat_completions", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=200000),
54
+ )
55
+
56
+ ANTHROPIC_MODELS = (
57
+ ModelInfo(id="claude-fable-5", provider="anthropic", api="messages", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', 'max')), context_limit=1000000),
58
+ ModelInfo(id="claude-fable-5-1", provider="anthropic", api="messages", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', 'max')), context_limit=1000000),
59
+ ModelInfo(id="claude-haiku-4-5", provider="anthropic", api="messages", reasoning=True, context_limit=200000),
60
+ ModelInfo(id="claude-haiku-4-5-20251001", provider="anthropic", api="messages", reasoning=True, context_limit=200000),
61
+ ModelInfo(id="claude-opus-4-5", provider="anthropic", api="messages", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=200000),
62
+ ModelInfo(id="claude-opus-4-5-20251101", provider="anthropic", api="messages", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=200000),
63
+ ModelInfo(id="claude-opus-4-6", provider="anthropic", api="messages", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', 'max')), context_limit=1000000),
64
+ ModelInfo(id="claude-opus-4-7", provider="anthropic", api="messages", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', 'max')), context_limit=1000000),
65
+ ModelInfo(id="claude-opus-4-8", provider="anthropic", api="messages", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', 'max')), context_limit=1000000),
66
+ ModelInfo(id="claude-opus-5", provider="anthropic", api="messages", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', 'max')), context_limit=1000000),
67
+ ModelInfo(id="claude-opus-5-5", provider="anthropic", api="messages", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', 'max')), context_limit=1000000),
68
+ ModelInfo(id="claude-sonnet-4-5", provider="anthropic", api="messages", reasoning=True, context_limit=1000000),
69
+ ModelInfo(id="claude-sonnet-4-5-20250929", provider="anthropic", api="messages", reasoning=True, context_limit=1000000),
70
+ ModelInfo(id="claude-sonnet-4-6", provider="anthropic", api="messages", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', 'max')), context_limit=1000000),
71
+ ModelInfo(id="claude-sonnet-5", provider="anthropic", api="messages", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', 'max')), context_limit=1000000),
72
+ ModelInfo(id="claude-sonnet-5-5", provider="anthropic", api="messages", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', 'max')), context_limit=1000000),
73
+ )
74
+
75
+ GEMINI_MODELS = (
76
+ ModelInfo(id="deep-research-max-preview-04-2026", provider="gemini", api="generate_content", reasoning=True, context_limit=131072),
77
+ ModelInfo(id="deep-research-preview-04-2026", provider="gemini", api="generate_content", reasoning=True, context_limit=131072),
78
+ ModelInfo(id="gemini-2.5-computer-use-preview-10-2025", provider="gemini", api="generate_content", reasoning=True, context_limit=128000),
79
+ ModelInfo(id="gemini-2.5-flash", provider="gemini", api="generate_content", reasoning=True, context_limit=1048576),
80
+ ModelInfo(id="gemini-2.5-flash-lite", provider="gemini", api="generate_content", reasoning=True, context_limit=1048576),
81
+ ModelInfo(id="gemini-2.5-pro", provider="gemini", api="generate_content", reasoning=True, context_limit=1048576),
82
+ ModelInfo(id="gemini-3-flash-preview", provider="gemini", api="generate_content", reasoning=True, thinking_level_map=(('off', None), ('minimal', 'minimal'), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=1048576),
83
+ ModelInfo(id="gemini-3.1-flash-lite", provider="gemini", api="generate_content", reasoning=True, thinking_level_map=(('off', None), ('minimal', 'minimal'), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=1048576),
84
+ ModelInfo(id="gemini-3.1-flash-lite-image", provider="gemini", api="generate_content", reasoning=True, thinking_level_map=(('off', None), ('minimal', 'minimal'), ('low', None), ('medium', None), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=65536),
85
+ ModelInfo(id="gemini-3.1-flash-lite-preview", provider="gemini", api="generate_content", reasoning=True, thinking_level_map=(('off', None), ('minimal', 'minimal'), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=1048576),
86
+ ModelInfo(id="gemini-3.1-flash-live-preview", provider="gemini", api="generate_content", reasoning=True, thinking_level_map=(('off', None), ('minimal', 'minimal'), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=131072),
87
+ ModelInfo(id="gemini-3.1-pro-preview", provider="gemini", api="generate_content", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=1048576),
88
+ ModelInfo(id="gemini-3.1-pro-preview-customtools", provider="gemini", api="generate_content", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=1048576),
89
+ ModelInfo(id="gemini-3.5-flash", provider="gemini", api="generate_content", reasoning=True, thinking_level_map=(('off', None), ('minimal', 'minimal'), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=1048576),
90
+ ModelInfo(id="gemini-3.5-flash-lite", provider="gemini", api="generate_content", reasoning=True, thinking_level_map=(('off', None), ('minimal', 'minimal'), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=1048576),
91
+ ModelInfo(id="gemini-3.6-flash", provider="gemini", api="generate_content", reasoning=True, thinking_level_map=(('off', None), ('minimal', 'minimal'), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=1048576),
92
+ ModelInfo(id="gemini-3.7-flash", provider="gemini", api="generate_content", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=1048576),
93
+ ModelInfo(id="gemini-3.8-flash", provider="gemini", api="generate_content", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=1048576),
94
+ ModelInfo(id="gemini-flash-latest", provider="gemini", api="generate_content", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=1048576),
95
+ ModelInfo(id="gemini-flash-lite-latest", provider="gemini", api="generate_content", reasoning=True, thinking_level_map=(('off', None), ('minimal', 'minimal'), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=1048576),
96
+ ModelInfo(id="gemma-4-26b-a4b-it", provider="gemini", api="generate_content", reasoning=True, context_limit=262144),
97
+ ModelInfo(id="gemma-4-31b-it", provider="gemini", api="generate_content", reasoning=True, context_limit=262144),
98
+ )
99
+
100
+ GROK_MODELS = (
101
+ ModelInfo(id="grok-4.20-0309-non-reasoning", provider="grok", api="chat_completions", context_limit=1000000),
102
+ ModelInfo(id="grok-4.20-0309-reasoning", provider="grok", api="chat_completions", reasoning=True, context_limit=1000000),
103
+ ModelInfo(id="grok-4.3", provider="grok", api="chat_completions", reasoning=True, thinking_level_map=(('off', 'none'), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=1000000),
104
+ ModelInfo(id="grok-4.5", provider="grok", api="chat_completions", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', None), ('max', None)), context_limit=500000),
105
+ ModelInfo(id="grok-4.6", provider="grok", api="chat_completions", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', None)), context_limit=500000),
106
+ ModelInfo(id="grok-4.7", provider="grok", api="chat_completions", reasoning=True, thinking_level_map=(('off', None), ('minimal', None), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('xhigh', 'xhigh'), ('max', None)), context_limit=500000),
107
+ ModelInfo(id="grok-build-0.1", provider="grok", api="chat_completions", reasoning=True, context_limit=256000),
108
+ )
109
+
110
+ ALL_MODELS = OPENAI_MODELS + ANTHROPIC_MODELS + GEMINI_MODELS + GROK_MODELS