pi-ai-python 1.3.0__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.
Files changed (189) hide show
  1. pi_agent/__init__.py +127 -0
  2. pi_agent/agent.py +617 -0
  3. pi_agent/agent_loop.py +820 -0
  4. pi_agent/proxy.py +269 -0
  5. pi_agent/py.typed +0 -0
  6. pi_agent/stream_fn.py +27 -0
  7. pi_agent/tools.py +327 -0
  8. pi_agent/types.py +394 -0
  9. pi_ai/__init__.py +263 -0
  10. pi_ai/abort.py +315 -0
  11. pi_ai/api/__init__.py +1 -0
  12. pi_ai/api/anthropic_messages.py +1782 -0
  13. pi_ai/api/azure_openai_config.py +138 -0
  14. pi_ai/api/azure_openai_responses.py +245 -0
  15. pi_ai/api/bedrock_converse_stream.py +1598 -0
  16. pi_ai/api/classifier_shared.py +211 -0
  17. pi_ai/api/cloudflare.py +36 -0
  18. pi_ai/api/cloudflare_workers_ai_system_one.py +80 -0
  19. pi_ai/api/constrained_sampling.py +279 -0
  20. pi_ai/api/github_copilot_headers.py +30 -0
  21. pi_ai/api/google_generative_ai.py +138 -0
  22. pi_ai/api/google_shared.py +927 -0
  23. pi_ai/api/google_vertex.py +234 -0
  24. pi_ai/api/lazy.py +156 -0
  25. pi_ai/api/llama_cpp_classify.py +449 -0
  26. pi_ai/api/mistral_conversations.py +756 -0
  27. pi_ai/api/openai_codex_responses.py +1545 -0
  28. pi_ai/api/openai_completions.py +1594 -0
  29. pi_ai/api/openai_decisions.py +210 -0
  30. pi_ai/api/openai_prompt_cache.py +19 -0
  31. pi_ai/api/openai_responses.py +418 -0
  32. pi_ai/api/openai_responses_shared.py +840 -0
  33. pi_ai/api/openai_sdk.py +204 -0
  34. pi_ai/api/openrouter_images.py +153 -0
  35. pi_ai/api/pi_messages.py +449 -0
  36. pi_ai/api/registry.py +103 -0
  37. pi_ai/api/simple_options.py +119 -0
  38. pi_ai/api/system_one_shared.py +148 -0
  39. pi_ai/api/transform_messages.py +211 -0
  40. pi_ai/api/typesafe_system_one.py +43 -0
  41. pi_ai/auth/__init__.py +1 -0
  42. pi_ai/auth/context.py +47 -0
  43. pi_ai/auth/credential_store.py +208 -0
  44. pi_ai/auth/helpers.py +123 -0
  45. pi_ai/auth/oauth/__init__.py +5 -0
  46. pi_ai/auth/oauth/_common.py +454 -0
  47. pi_ai/auth/oauth/anthropic.py +283 -0
  48. pi_ai/auth/oauth/callback_server.py +339 -0
  49. pi_ai/auth/oauth/device_code.py +127 -0
  50. pi_ai/auth/oauth/github_copilot.py +508 -0
  51. pi_ai/auth/oauth/kimi_coding.py +283 -0
  52. pi_ai/auth/oauth/load.py +64 -0
  53. pi_ai/auth/oauth/meta.py +238 -0
  54. pi_ai/auth/oauth/openai_chatgpt.py +321 -0
  55. pi_ai/auth/oauth/openai_codex.py +391 -0
  56. pi_ai/auth/oauth/openrouter.py +173 -0
  57. pi_ai/auth/oauth/pkce.py +23 -0
  58. pi_ai/auth/oauth/radius.py +311 -0
  59. pi_ai/auth/oauth/xai.py +220 -0
  60. pi_ai/auth/resolve.py +190 -0
  61. pi_ai/auth/types.py +292 -0
  62. pi_ai/cli.py +265 -0
  63. pi_ai/compat.py +651 -0
  64. pi_ai/env_api_keys.py +137 -0
  65. pi_ai/event_stream.py +329 -0
  66. pi_ai/models.py +1191 -0
  67. pi_ai/models_store.py +121 -0
  68. pi_ai/oauth.py +90 -0
  69. pi_ai/providers/__init__.py +1 -0
  70. pi_ai/providers/all.py +154 -0
  71. pi_ai/providers/amazon_bedrock.py +116 -0
  72. pi_ai/providers/ant_ling.py +23 -0
  73. pi_ai/providers/anthropic.py +105 -0
  74. pi_ai/providers/azure.py +103 -0
  75. pi_ai/providers/baseten.py +23 -0
  76. pi_ai/providers/catalog.py +167 -0
  77. pi_ai/providers/cerebras.py +23 -0
  78. pi_ai/providers/cloudflare_ai_gateway.py +30 -0
  79. pi_ai/providers/cloudflare_auth.py +112 -0
  80. pi_ai/providers/cloudflare_stream.py +84 -0
  81. pi_ai/providers/cloudflare_workers_ai.py +26 -0
  82. pi_ai/providers/data/amazon-bedrock.json +1 -0
  83. pi_ai/providers/data/ant-ling.json +1 -0
  84. pi_ai/providers/data/anthropic.json +1 -0
  85. pi_ai/providers/data/azure.json +1 -0
  86. pi_ai/providers/data/baseten.json +1 -0
  87. pi_ai/providers/data/cerebras.json +1 -0
  88. pi_ai/providers/data/cloudflare-ai-gateway.json +1 -0
  89. pi_ai/providers/data/cloudflare-workers-ai.json +1 -0
  90. pi_ai/providers/data/deepseek.json +1 -0
  91. pi_ai/providers/data/fireworks.json +1 -0
  92. pi_ai/providers/data/github-copilot.json +1 -0
  93. pi_ai/providers/data/google-vertex.json +1 -0
  94. pi_ai/providers/data/google.json +1 -0
  95. pi_ai/providers/data/groq.json +1 -0
  96. pi_ai/providers/data/huggingface.json +1 -0
  97. pi_ai/providers/data/kimi-coding.json +1 -0
  98. pi_ai/providers/data/manifest.json +1 -0
  99. pi_ai/providers/data/meta.json +1 -0
  100. pi_ai/providers/data/minimax-cn.json +1 -0
  101. pi_ai/providers/data/minimax.json +1 -0
  102. pi_ai/providers/data/mistral.json +1 -0
  103. pi_ai/providers/data/moonshotai-cn.json +1 -0
  104. pi_ai/providers/data/moonshotai.json +1 -0
  105. pi_ai/providers/data/nvidia.json +1 -0
  106. pi_ai/providers/data/openai-codex.json +1 -0
  107. pi_ai/providers/data/openai.json +1 -0
  108. pi_ai/providers/data/opencode-go.json +1 -0
  109. pi_ai/providers/data/opencode.json +1 -0
  110. pi_ai/providers/data/openrouter.json +1 -0
  111. pi_ai/providers/data/qwen-token-plan-cn.json +1 -0
  112. pi_ai/providers/data/qwen-token-plan-individual.json +1 -0
  113. pi_ai/providers/data/qwen-token-plan.json +1 -0
  114. pi_ai/providers/data/radius.json +1 -0
  115. pi_ai/providers/data/together.json +1 -0
  116. pi_ai/providers/data/typesafe.json +1 -0
  117. pi_ai/providers/data/vercel-ai-gateway.json +1 -0
  118. pi_ai/providers/data/xai.json +1 -0
  119. pi_ai/providers/data/xiaomi-token-plan-ams.json +1 -0
  120. pi_ai/providers/data/xiaomi-token-plan-cn.json +1 -0
  121. pi_ai/providers/data/xiaomi-token-plan-sgp.json +1 -0
  122. pi_ai/providers/data/xiaomi.json +1 -0
  123. pi_ai/providers/data/zai-coding-cn.json +1 -0
  124. pi_ai/providers/data/zai.json +1 -0
  125. pi_ai/providers/deepseek.py +23 -0
  126. pi_ai/providers/faux.py +680 -0
  127. pi_ai/providers/fireworks.py +26 -0
  128. pi_ai/providers/github_copilot.py +46 -0
  129. pi_ai/providers/google.py +23 -0
  130. pi_ai/providers/google_vertex.py +131 -0
  131. pi_ai/providers/groq.py +23 -0
  132. pi_ai/providers/huggingface.py +23 -0
  133. pi_ai/providers/kimi_coding.py +32 -0
  134. pi_ai/providers/meta.py +32 -0
  135. pi_ai/providers/minimax.py +23 -0
  136. pi_ai/providers/minimax_cn.py +23 -0
  137. pi_ai/providers/mistral.py +23 -0
  138. pi_ai/providers/moonshotai.py +23 -0
  139. pi_ai/providers/moonshotai_cn.py +23 -0
  140. pi_ai/providers/nvidia.py +23 -0
  141. pi_ai/providers/openai.py +44 -0
  142. pi_ai/providers/openai_codex.py +26 -0
  143. pi_ai/providers/opencode.py +36 -0
  144. pi_ai/providers/opencode_go.py +33 -0
  145. pi_ai/providers/opencode_headers.py +60 -0
  146. pi_ai/providers/openrouter.py +145 -0
  147. pi_ai/providers/openrouter_api.py +122 -0
  148. pi_ai/providers/qwen_token_plan.py +23 -0
  149. pi_ai/providers/qwen_token_plan_cn.py +23 -0
  150. pi_ai/providers/qwen_token_plan_individual.py +23 -0
  151. pi_ai/providers/radius.py +146 -0
  152. pi_ai/providers/radius_config.py +136 -0
  153. pi_ai/providers/together.py +23 -0
  154. pi_ai/providers/typesafe.py +22 -0
  155. pi_ai/providers/vercel_ai_gateway.py +25 -0
  156. pi_ai/providers/xai.py +32 -0
  157. pi_ai/providers/xiaomi.py +23 -0
  158. pi_ai/providers/xiaomi_token_plan_ams.py +23 -0
  159. pi_ai/providers/xiaomi_token_plan_cn.py +23 -0
  160. pi_ai/providers/xiaomi_token_plan_sgp.py +23 -0
  161. pi_ai/providers/zai.py +23 -0
  162. pi_ai/providers/zai_coding_cn.py +23 -0
  163. pi_ai/py.typed +0 -0
  164. pi_ai/serialization.py +477 -0
  165. pi_ai/session_resources.py +33 -0
  166. pi_ai/types.py +1003 -0
  167. pi_ai/utils/__init__.py +1 -0
  168. pi_ai/utils/callbacks.py +15 -0
  169. pi_ai/utils/diagnostics.py +70 -0
  170. pi_ai/utils/error_body.py +89 -0
  171. pi_ai/utils/estimate.py +125 -0
  172. pi_ai/utils/fastjson.py +63 -0
  173. pi_ai/utils/http.py +409 -0
  174. pi_ai/utils/json_parse.py +424 -0
  175. pi_ai/utils/misc.py +193 -0
  176. pi_ai/utils/model_operations.py +73 -0
  177. pi_ai/utils/models_error.py +35 -0
  178. pi_ai/utils/oauth_page.py +121 -0
  179. pi_ai/utils/overflow.py +87 -0
  180. pi_ai/utils/retry.py +193 -0
  181. pi_ai/utils/schema.py +283 -0
  182. pi_ai/utils/text.py +65 -0
  183. pi_ai/utils/transcript.py +271 -0
  184. pi_ai/utils/validation.py +813 -0
  185. pi_ai_python-1.3.0.dist-info/METADATA +167 -0
  186. pi_ai_python-1.3.0.dist-info/RECORD +189 -0
  187. pi_ai_python-1.3.0.dist-info/WHEEL +4 -0
  188. pi_ai_python-1.3.0.dist-info/entry_points.txt +2 -0
  189. pi_ai_python-1.3.0.dist-info/licenses/LICENSE +22 -0
pi_ai/types.py ADDED
@@ -0,0 +1,1003 @@
1
+ """Core data types for pi-ai.
2
+
3
+ Every type mirrors its counterpart in ``@earendil-works/pi-ai`` (``src/types.ts``).
4
+ Python attribute names are snake_case; :mod:`pi_ai.serialization` converts to and
5
+ from pi's camelCase JSON so transcripts, models and contexts stay interchangeable
6
+ with the TypeScript implementation.
7
+
8
+ All data classes use ``__slots__`` for low memory use and fast attribute access.
9
+ Discriminators (``type`` on content blocks, events and models, ``role`` on
10
+ messages) are class-level constants, so they can be read as ``block.type`` and
11
+ matched structurally (``match event: case TextDeltaEvent(delta=d): ...``).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import time
17
+ from collections.abc import Awaitable, Callable, Mapping
18
+ from dataclasses import KW_ONLY, dataclass, field
19
+ from typing import TYPE_CHECKING, Any, ClassVar, Literal, Protocol, TypeAlias, TypedDict
20
+
21
+ if TYPE_CHECKING:
22
+ import httpx
23
+
24
+ from .abort import AbortSignal
25
+ from .event_stream import AssistantMessageEventStream
26
+ from .utils.diagnostics import AssistantMessageDiagnostic
27
+
28
+ __all__ = [
29
+ # literal aliases
30
+ "KnownApi",
31
+ "Api",
32
+ "KnownImageApi",
33
+ "ImageApi",
34
+ "KnownClassifierApi",
35
+ "ClassifierApi",
36
+ "KnownProvider",
37
+ "ProviderId",
38
+ "StopReason",
39
+ "ThinkingLevel",
40
+ "ModelThinkingLevel",
41
+ "THINKING_LEVELS",
42
+ "MODEL_THINKING_LEVELS",
43
+ "CacheRetention",
44
+ "Transport",
45
+ "ToolChoice",
46
+ "ModelType",
47
+ "ModelInputModality",
48
+ "JsonValue",
49
+ "JsonObject",
50
+ "ProviderEnv",
51
+ "ProviderHeaders",
52
+ "SamplingParams",
53
+ # content
54
+ "TextContent",
55
+ "ThinkingContent",
56
+ "ImageContent",
57
+ "ToolCall",
58
+ "TextSignatureV1",
59
+ "AssistantContent",
60
+ "UserContent",
61
+ "ToolResultContent",
62
+ # usage
63
+ "UsageCost",
64
+ "Usage",
65
+ # tools
66
+ "GrammarFormat",
67
+ "GrammarVariants",
68
+ "ConstrainedSamplingConfig",
69
+ "Tool",
70
+ "ToolReference",
71
+ # messages
72
+ "DeferredHandle",
73
+ "SystemMessage",
74
+ "UserMessage",
75
+ "AssistantMessage",
76
+ "NestedToolCallRecord",
77
+ "NestedToolCalls",
78
+ "ToolResultMessage",
79
+ "Message",
80
+ "Context",
81
+ "TranscriptContext",
82
+ # events
83
+ "StartEvent",
84
+ "TextStartEvent",
85
+ "TextDeltaEvent",
86
+ "TextEndEvent",
87
+ "ThinkingStartEvent",
88
+ "ThinkingDeltaEvent",
89
+ "ThinkingEndEvent",
90
+ "ToolCallStartEvent",
91
+ "ToolCallDeltaEvent",
92
+ "ToolCallEndEvent",
93
+ "DoneEvent",
94
+ "ErrorEvent",
95
+ "AssistantMessageEvent",
96
+ # models
97
+ "ModelCostRates",
98
+ "ModelCostTier",
99
+ "ModelCost",
100
+ "ModelImageResizeOptions",
101
+ "ModelImageInputLimits",
102
+ "ModelInputLimits",
103
+ "BaseModel",
104
+ "Model",
105
+ "ImageModel",
106
+ "ClassifierModel",
107
+ "AnyModel",
108
+ # images
109
+ "ImagesContext",
110
+ "AssistantImages",
111
+ # classifier
112
+ "ClassifierChoiceQuestion",
113
+ "ClassifierScoreQuestion",
114
+ "ClassifierBoolQuestion",
115
+ "ClassifierQuestion",
116
+ "ClassifierContext",
117
+ "ClassifierChoiceAnswer",
118
+ "ClassifierScoreAnswer",
119
+ "ClassifierBoolAnswer",
120
+ "ClassifierAnswer",
121
+ "ClassifierResult",
122
+ # options
123
+ "ProviderResponse",
124
+ "ThinkingBudgets",
125
+ "ProviderRequestOptions",
126
+ "StreamOptions",
127
+ "SimpleStreamOptions",
128
+ "DeferredFetchOptions",
129
+ "DeferredCancelOptions",
130
+ "ImagesOptions",
131
+ "ClassifierOptions",
132
+ # implementation contracts
133
+ "ProviderStreams",
134
+ "ProviderImages",
135
+ "ProviderClassifier",
136
+ "StreamFunction",
137
+ "now_ms",
138
+ ]
139
+
140
+
141
+ def now_ms() -> int:
142
+ """Current Unix time in milliseconds (the timestamp unit used throughout pi)."""
143
+ return time.time_ns() // 1_000_000
144
+
145
+
146
+ # ---------------------------------------------------------------------------
147
+ # Literal aliases
148
+ # ---------------------------------------------------------------------------
149
+
150
+ KnownApi: TypeAlias = Literal[
151
+ "openai-completions",
152
+ "mistral-conversations",
153
+ "openai-responses",
154
+ "azure-openai-responses",
155
+ "openai-codex-responses",
156
+ "anthropic-messages",
157
+ "bedrock-converse-stream",
158
+ "google-generative-ai",
159
+ "google-vertex",
160
+ "pi-messages",
161
+ ]
162
+ #: Any API id. Known ids are listed in :data:`KnownApi`; custom providers may use their own.
163
+ Api: TypeAlias = str
164
+
165
+ KnownImageApi: TypeAlias = Literal["openrouter-images"]
166
+ ImageApi: TypeAlias = str
167
+
168
+ KnownClassifierApi: TypeAlias = Literal[
169
+ "typesafe-system-one",
170
+ "cloudflare-workers-ai-system-one",
171
+ "llama-cpp-classify",
172
+ "openai-decisions",
173
+ ]
174
+ ClassifierApi: TypeAlias = str
175
+
176
+ KnownProvider: TypeAlias = Literal[
177
+ "amazon-bedrock",
178
+ "ant-ling",
179
+ "anthropic",
180
+ "google",
181
+ "google-vertex",
182
+ "openai",
183
+ "azure",
184
+ "openai-codex",
185
+ "radius",
186
+ "typesafe",
187
+ "nvidia",
188
+ "deepseek",
189
+ "github-copilot",
190
+ "xai",
191
+ "groq",
192
+ "cerebras",
193
+ "openrouter",
194
+ "vercel-ai-gateway",
195
+ "zai",
196
+ "zai-coding-cn",
197
+ "mistral",
198
+ "minimax",
199
+ "minimax-cn",
200
+ "moonshotai",
201
+ "moonshotai-cn",
202
+ "huggingface",
203
+ "fireworks",
204
+ "together",
205
+ "baseten",
206
+ "opencode",
207
+ "opencode-go",
208
+ "kimi-coding",
209
+ "meta",
210
+ "cloudflare-workers-ai",
211
+ "cloudflare-ai-gateway",
212
+ "qwen-token-plan",
213
+ "qwen-token-plan-cn",
214
+ "qwen-token-plan-individual",
215
+ "xiaomi",
216
+ "xiaomi-token-plan-cn",
217
+ "xiaomi-token-plan-ams",
218
+ "xiaomi-token-plan-sgp",
219
+ ]
220
+ ProviderId: TypeAlias = str
221
+
222
+ StopReason: TypeAlias = Literal["pending", "stop", "length", "toolUse", "error", "aborted", "deferred"]
223
+
224
+ #: Thinking levels accepted by ``stream_simple(reasoning=...)``.
225
+ ThinkingLevel: TypeAlias = Literal["minimal", "low", "medium", "high", "xhigh", "max"]
226
+ #: Thinking levels including ``"off"``.
227
+ ModelThinkingLevel: TypeAlias = Literal["off", "minimal", "low", "medium", "high", "xhigh", "max"]
228
+ THINKING_LEVELS: tuple[ThinkingLevel, ...] = ("minimal", "low", "medium", "high", "xhigh", "max")
229
+ MODEL_THINKING_LEVELS: tuple[ModelThinkingLevel, ...] = ("off", *THINKING_LEVELS)
230
+
231
+ CacheRetention: TypeAlias = Literal["none", "short", "long"]
232
+ Transport: TypeAlias = Literal["sse", "websocket", "websocket-cached", "auto"]
233
+ ToolChoice: TypeAlias = Literal["auto", "none"]
234
+ ModelType: TypeAlias = Literal["chat", "image", "classifier"]
235
+ ModelInputModality: TypeAlias = Literal["text", "image"]
236
+
237
+ JsonValue: TypeAlias = Any
238
+ JsonObject: TypeAlias = dict[str, Any]
239
+ #: Provider-scoped environment overrides. Values take precedence over ``os.environ``.
240
+ ProviderEnv: TypeAlias = dict[str, str]
241
+ #: Request headers. A ``None`` value suppresses a provider/API default header with the same name.
242
+ ProviderHeaders: TypeAlias = dict[str, "str | None"]
243
+ SamplingParams: TypeAlias = dict[str, Any]
244
+
245
+
246
+ # ---------------------------------------------------------------------------
247
+ # Content blocks
248
+ # ---------------------------------------------------------------------------
249
+
250
+
251
+ class TextSignatureV1(TypedDict, total=False):
252
+ v: Literal[1]
253
+ id: str
254
+ phase: Literal["commentary", "final_answer"]
255
+
256
+
257
+ @dataclass(slots=True)
258
+ class TextContent:
259
+ """Plain text."""
260
+
261
+ type: ClassVar[Literal["text"]] = "text"
262
+ text: str = ""
263
+ _: KW_ONLY
264
+ #: Provider message metadata, e.g. OpenAI Responses message ids (legacy id string or TextSignatureV1 JSON).
265
+ text_signature: str | None = None
266
+
267
+
268
+ @dataclass(slots=True)
269
+ class ThinkingContent:
270
+ """Model reasoning. ``thinking_signature`` carries provider replay data."""
271
+
272
+ type: ClassVar[Literal["thinking"]] = "thinking"
273
+ thinking: str = ""
274
+ _: KW_ONLY
275
+ thinking_signature: str | None = None
276
+ #: True when the reasoning was redacted by safety filters; the encrypted payload is in ``thinking_signature``.
277
+ redacted: bool | None = None
278
+
279
+
280
+ @dataclass(slots=True)
281
+ class ImageContent:
282
+ """Base64-encoded image data."""
283
+
284
+ type: ClassVar[Literal["image"]] = "image"
285
+ data: str
286
+ mime_type: str
287
+
288
+
289
+ @dataclass(slots=True)
290
+ class ToolCall:
291
+ """A tool call requested by the model."""
292
+
293
+ type: ClassVar[Literal["toolCall"]] = "toolCall"
294
+ id: str
295
+ name: str
296
+ arguments: dict[str, Any] = field(default_factory=dict)
297
+ _: KW_ONLY
298
+ #: Google-specific opaque signature for reusing thought context.
299
+ thought_signature: str | None = None
300
+ #: OpenAI Responses namespace for calls to dynamically loaded or namespaced tools.
301
+ namespace: str | None = None
302
+ #: Set when the model's complete arguments text was not a valid JSON object; ``arguments`` is then ``{}``.
303
+ arguments_error: str | None = None
304
+
305
+
306
+ AssistantContent: TypeAlias = TextContent | ThinkingContent | ToolCall
307
+ UserContent: TypeAlias = TextContent | ImageContent
308
+ ToolResultContent: TypeAlias = TextContent | ImageContent
309
+
310
+
311
+ # ---------------------------------------------------------------------------
312
+ # Usage
313
+ # ---------------------------------------------------------------------------
314
+
315
+
316
+ @dataclass(slots=True)
317
+ class UsageCost:
318
+ input: float = 0.0
319
+ output: float = 0.0
320
+ cache_read: float = 0.0
321
+ cache_write: float = 0.0
322
+ total: float = 0.0
323
+
324
+
325
+ @dataclass(slots=True)
326
+ class Usage:
327
+ """Token counts and their cost in USD."""
328
+
329
+ input: int = 0
330
+ output: int = 0
331
+ cache_read: int = 0
332
+ cache_write: int = 0
333
+ total_tokens: int = 0
334
+ cost: UsageCost = field(default_factory=UsageCost)
335
+ _: KW_ONLY
336
+ #: Subset of ``cache_write`` written with 1h retention. Only Anthropic reports this split.
337
+ cache_write_1h: int | None = None
338
+ #: Reasoning tokens, a subset of ``output``, when the provider reports them.
339
+ reasoning: int | None = None
340
+
341
+
342
+ # ---------------------------------------------------------------------------
343
+ # Tools
344
+ # ---------------------------------------------------------------------------
345
+
346
+ GrammarFormat: TypeAlias = Literal["openai_lark", "openai_regex"]
347
+ GrammarVariants: TypeAlias = dict[str, str]
348
+
349
+
350
+ class ConstrainedSamplingConfig(TypedDict, total=False):
351
+ """Provider-side constrained sampling for a tool.
352
+
353
+ ``{"type": "json_schema", "strict": "prefer" | "require"}`` or
354
+ ``{"type": "grammar", "variants": {"openai_lark": "..."}}``.
355
+ """
356
+
357
+ type: Literal["json_schema", "grammar"]
358
+ strict: Literal["prefer", "require"]
359
+ variants: GrammarVariants
360
+
361
+
362
+ @dataclass(slots=True)
363
+ class Tool:
364
+ """A tool the model may call.
365
+
366
+ ``parameters`` is a JSON Schema object (build it with :class:`pi_ai.Type`) or a
367
+ Pydantic model class. Pydantic models are converted to JSON Schema for the
368
+ provider and validated arguments are returned as model instances.
369
+ """
370
+
371
+ name: str
372
+ description: str = ""
373
+ parameters: Any = field(default_factory=lambda: {"type": "object", "properties": {}})
374
+ _: KW_ONLY
375
+ constrained_sampling: ConstrainedSamplingConfig | Literal[False] | None = None
376
+
377
+
378
+ @dataclass(slots=True)
379
+ class ToolReference:
380
+ name: str
381
+
382
+
383
+ # ---------------------------------------------------------------------------
384
+ # Messages
385
+ # ---------------------------------------------------------------------------
386
+
387
+
388
+ @dataclass(slots=True)
389
+ class DeferredHandle:
390
+ provider: str
391
+ model_id: str
392
+ api: str
393
+ #: Provider token, such as a response id or batch id plus row id.
394
+ id: str
395
+ _: KW_ONLY
396
+ expires_at: int | None = None
397
+ poll_after_ms: int | None = None
398
+ #: Provider conversion data required to reconstruct the final assistant message.
399
+ data: Any = None
400
+
401
+
402
+ @dataclass(slots=True)
403
+ class SystemMessage:
404
+ """System instructions and tool declarations at one point in the transcript.
405
+
406
+ The leading system message is the system prompt. Later system messages change it:
407
+ ``content`` adds instructions from that point on, ``sections`` replace or remove
408
+ named prompt sections (``None`` removes one), and ``tools_added``/``tools_removed``
409
+ change the tool set. Replaying every system message in order yields the current
410
+ prompt and tools.
411
+ """
412
+
413
+ role: ClassVar[Literal["system"]] = "system"
414
+ content: str | list[TextContent] = ""
415
+ _: KW_ONLY
416
+ sections: dict[str, str | None] | None = None
417
+ tools_added: list[Tool] | None = None
418
+ tools_removed: list[ToolReference] | None = None
419
+ timestamp: int = field(default_factory=now_ms)
420
+
421
+
422
+ @dataclass(slots=True)
423
+ class UserMessage:
424
+ role: ClassVar[Literal["user"]] = "user"
425
+ content: str | list[TextContent | ImageContent] = ""
426
+ _: KW_ONLY
427
+ timestamp: int = field(default_factory=now_ms)
428
+
429
+
430
+ @dataclass(slots=True)
431
+ class AssistantMessage:
432
+ role: ClassVar[Literal["assistant"]] = "assistant"
433
+ content: list[TextContent | ThinkingContent | ToolCall] = field(default_factory=list)
434
+ _: KW_ONLY
435
+ api: str
436
+ provider: str
437
+ model: str
438
+ usage: Usage = field(default_factory=Usage)
439
+ stop_reason: StopReason = "stop"
440
+ #: Unix timestamp in milliseconds when the request started.
441
+ timestamp: int = field(default_factory=now_ms)
442
+ #: Concrete model reported by the provider when different from the requested ``model``.
443
+ response_model: str | None = None
444
+ #: Provider-specific response/message identifier when the upstream API exposes one.
445
+ response_id: str | None = None
446
+ #: Exact provider-native effort level used for this response.
447
+ provider_thinking_level: str | None = None
448
+ #: Pi thinking level the agent loop requested for this response.
449
+ thinking_level: ModelThinkingLevel | None = None
450
+ diagnostics: list[AssistantMessageDiagnostic] | None = None
451
+ deferred: DeferredHandle | None = None
452
+ error_message: str | None = None
453
+ raw_stop_reason: str | None = None
454
+ #: Provider indication of whether the model explicitly ended its turn.
455
+ end_turn: bool | None = None
456
+ #: Milliseconds from ``timestamp`` until the response ended (monotonic clock).
457
+ duration_ms: int | None = None
458
+
459
+ def shallow_copy(self) -> AssistantMessage:
460
+ """Copy the message object (not its content blocks), like ``{ ...message }`` in TypeScript."""
461
+ copied = object.__new__(AssistantMessage)
462
+ for name in _ASSISTANT_SLOTS:
463
+ object.__setattr__(copied, name, getattr(self, name))
464
+ copied.content = list(self.content)
465
+ return copied
466
+
467
+
468
+ _ASSISTANT_SLOTS: tuple[str, ...] = AssistantMessage.__slots__ # type: ignore[attr-defined]
469
+
470
+
471
+ @dataclass(slots=True)
472
+ class NestedToolCallRecord:
473
+ """A tool call that another tool made while it ran (for example from a codemode script)."""
474
+
475
+ id: str
476
+ name: str
477
+ status: Literal["ok", "error", "unfinished"]
478
+ _: KW_ONLY
479
+ arguments: dict[str, Any] | None = None
480
+ arguments_bytes: int | None = None
481
+ duration_ms: int | None = None
482
+ error: str | None = None
483
+
484
+
485
+ @dataclass(slots=True)
486
+ class NestedToolCalls:
487
+ calls: list[NestedToolCallRecord] = field(default_factory=list)
488
+ complete: bool = True
489
+
490
+
491
+ @dataclass(slots=True)
492
+ class ToolResultMessage:
493
+ role: ClassVar[Literal["toolResult"]] = "toolResult"
494
+ tool_call_id: str
495
+ tool_name: str
496
+ content: list[TextContent | ImageContent] = field(default_factory=list)
497
+ is_error: bool = False
498
+ _: KW_ONLY
499
+ details: Any = None
500
+ #: Usage from the tool execution itself. Not part of the main LLM context accounting.
501
+ usage: Usage | None = None
502
+ #: Calls this tool made to other tools. Kept for the session record; not sent to the model.
503
+ nested_calls: NestedToolCalls | None = None
504
+ timestamp: int = field(default_factory=now_ms)
505
+ #: Milliseconds the tool's execution took.
506
+ duration_ms: int | None = None
507
+
508
+
509
+ Message: TypeAlias = SystemMessage | UserMessage | AssistantMessage | ToolResultMessage
510
+
511
+
512
+ @dataclass(slots=True)
513
+ class Context:
514
+ """Request input accepted by the public stream entry points.
515
+
516
+ ``system_prompt`` and ``tools`` are shorthand for a leading system message;
517
+ :func:`pi_ai.normalize_context` folds them into one before the request reaches
518
+ a provider.
519
+ """
520
+
521
+ messages: list[Message] = field(default_factory=list)
522
+ system_prompt: str | None = None
523
+ tools: list[Tool] | None = None
524
+
525
+
526
+ class TranscriptContext:
527
+ """Normalized request context passed to providers and API implementations.
528
+
529
+ The prompt and tool declarations are carried by the transcript's system
530
+ messages. Only :func:`pi_ai.normalize_context` (and the transcript helpers)
531
+ produce this type, so a raw :class:`Context` cannot reach provider code by
532
+ accident.
533
+ """
534
+
535
+ __slots__ = ("messages",)
536
+
537
+ def __init__(self, messages: list[Message]) -> None:
538
+ self.messages = messages
539
+
540
+ def __repr__(self) -> str:
541
+ return f"TranscriptContext(messages={self.messages!r})"
542
+
543
+
544
+ # ---------------------------------------------------------------------------
545
+ # Stream events
546
+ # ---------------------------------------------------------------------------
547
+
548
+
549
+ @dataclass(slots=True)
550
+ class StartEvent:
551
+ type: ClassVar[Literal["start"]] = "start"
552
+ partial: AssistantMessage
553
+
554
+
555
+ @dataclass(slots=True)
556
+ class TextStartEvent:
557
+ type: ClassVar[Literal["text_start"]] = "text_start"
558
+ content_index: int
559
+ partial: AssistantMessage
560
+
561
+
562
+ @dataclass(slots=True)
563
+ class TextDeltaEvent:
564
+ type: ClassVar[Literal["text_delta"]] = "text_delta"
565
+ content_index: int
566
+ delta: str
567
+ partial: AssistantMessage
568
+
569
+
570
+ @dataclass(slots=True)
571
+ class TextEndEvent:
572
+ type: ClassVar[Literal["text_end"]] = "text_end"
573
+ content_index: int
574
+ content: str
575
+ partial: AssistantMessage
576
+
577
+
578
+ @dataclass(slots=True)
579
+ class ThinkingStartEvent:
580
+ type: ClassVar[Literal["thinking_start"]] = "thinking_start"
581
+ content_index: int
582
+ partial: AssistantMessage
583
+
584
+
585
+ @dataclass(slots=True)
586
+ class ThinkingDeltaEvent:
587
+ type: ClassVar[Literal["thinking_delta"]] = "thinking_delta"
588
+ content_index: int
589
+ delta: str
590
+ partial: AssistantMessage
591
+
592
+
593
+ @dataclass(slots=True)
594
+ class ThinkingEndEvent:
595
+ type: ClassVar[Literal["thinking_end"]] = "thinking_end"
596
+ content_index: int
597
+ content: str
598
+ partial: AssistantMessage
599
+
600
+
601
+ @dataclass(slots=True)
602
+ class ToolCallStartEvent:
603
+ type: ClassVar[Literal["toolcall_start"]] = "toolcall_start"
604
+ content_index: int
605
+ partial: AssistantMessage
606
+
607
+
608
+ @dataclass(slots=True)
609
+ class ToolCallDeltaEvent:
610
+ type: ClassVar[Literal["toolcall_delta"]] = "toolcall_delta"
611
+ content_index: int
612
+ delta: str
613
+ partial: AssistantMessage
614
+
615
+
616
+ @dataclass(slots=True)
617
+ class ToolCallEndEvent:
618
+ type: ClassVar[Literal["toolcall_end"]] = "toolcall_end"
619
+ content_index: int
620
+ tool_call: ToolCall
621
+ partial: AssistantMessage
622
+
623
+
624
+ @dataclass(slots=True)
625
+ class DoneEvent:
626
+ type: ClassVar[Literal["done"]] = "done"
627
+ reason: Literal["stop", "length", "toolUse", "deferred"]
628
+ message: AssistantMessage
629
+
630
+
631
+ @dataclass(slots=True)
632
+ class ErrorEvent:
633
+ type: ClassVar[Literal["error"]] = "error"
634
+ reason: Literal["aborted", "error"]
635
+ error: AssistantMessage
636
+
637
+
638
+ AssistantMessageEvent: TypeAlias = (
639
+ StartEvent
640
+ | TextStartEvent
641
+ | TextDeltaEvent
642
+ | TextEndEvent
643
+ | ThinkingStartEvent
644
+ | ThinkingDeltaEvent
645
+ | ThinkingEndEvent
646
+ | ToolCallStartEvent
647
+ | ToolCallDeltaEvent
648
+ | ToolCallEndEvent
649
+ | DoneEvent
650
+ | ErrorEvent
651
+ )
652
+
653
+
654
+ # ---------------------------------------------------------------------------
655
+ # Models
656
+ # ---------------------------------------------------------------------------
657
+
658
+
659
+ @dataclass(slots=True)
660
+ class ModelCostRates:
661
+ """USD per million tokens."""
662
+
663
+ input: float = 0.0
664
+ output: float = 0.0
665
+ cache_read: float = 0.0
666
+ cache_write: float = 0.0
667
+
668
+
669
+ @dataclass(slots=True)
670
+ class ModelCostTier:
671
+ """Request-wide pricing tier used when total request input exceeds ``input_tokens_above``."""
672
+
673
+ input_tokens_above: int
674
+ input: float = 0.0
675
+ output: float = 0.0
676
+ cache_read: float = 0.0
677
+ cache_write: float = 0.0
678
+
679
+
680
+ @dataclass(slots=True)
681
+ class ModelCost:
682
+ """USD per million tokens, with optional request-wide pricing tiers."""
683
+
684
+ input: float = 0.0
685
+ output: float = 0.0
686
+ cache_read: float = 0.0
687
+ cache_write: float = 0.0
688
+ tiers: list[ModelCostTier] | None = None
689
+
690
+
691
+ @dataclass(slots=True)
692
+ class ModelImageResizeOptions:
693
+ max_width: int | None = None
694
+ max_height: int | None = None
695
+ #: Maximum base64-encoded payload size in bytes.
696
+ max_bytes: int | None = None
697
+ jpeg_quality: int | None = None
698
+
699
+
700
+ @dataclass(slots=True)
701
+ class ModelImageInputLimits:
702
+ resize: ModelImageResizeOptions | None = None
703
+ max_per_message: int | None = None
704
+ max_per_request: int | None = None
705
+
706
+
707
+ @dataclass(slots=True)
708
+ class ModelInputLimits:
709
+ #: Maximum serialized provider request size in bytes.
710
+ max_request_bytes: int | None = None
711
+ images: ModelImageInputLimits | None = None
712
+
713
+
714
+ @dataclass(slots=True, kw_only=True)
715
+ class BaseModel:
716
+ """Fields shared by every catalog entry, regardless of what you can do with it."""
717
+
718
+ type: ClassVar[ModelType] = "chat"
719
+ id: str
720
+ name: str = ""
721
+ api: str
722
+ provider: str
723
+ base_url: str = ""
724
+ input: list[str] = field(default_factory=lambda: ["text"])
725
+ cost: ModelCost = field(default_factory=ModelCost)
726
+ input_limits: ModelInputLimits | None = None
727
+ headers: dict[str, str] | None = None
728
+
729
+ def __post_init__(self) -> None:
730
+ if not self.name:
731
+ self.name = self.id
732
+
733
+
734
+ @dataclass(slots=True, kw_only=True)
735
+ class Model(BaseModel):
736
+ """Chat model: usable with ``stream()`` and friends.
737
+
738
+ ``compat`` holds API-specific compatibility overrides as a plain dict using pi's
739
+ JSON keys (``{"supportsDeveloperRole": False}``), so ``models.json`` configuration
740
+ is interchangeable with the TypeScript implementation.
741
+ """
742
+
743
+ type: ClassVar[Literal["chat"]] = "chat"
744
+ reasoning: bool = False
745
+ context_window: int = 128_000
746
+ max_tokens: int = 16_384
747
+ #: Maps pi thinking levels to provider/model-specific values. ``None`` marks a level unsupported.
748
+ thinking_level_map: dict[str, str | None] | None = None
749
+ #: Prompt cache lifetimes in seconds per retention tier (``{"short": 300, "long": 3600}``).
750
+ prompt_cache: dict[str, float] | None = None
751
+ #: Default sampling parameters; per-request ``sampling_params`` override these per key.
752
+ sampling_params: SamplingParams | None = None
753
+ #: Sampling parameter overrides selected by the effective pi thinking level.
754
+ sampling_params_by_thinking_level: dict[str, SamplingParams] | None = None
755
+ #: Compatibility overrides for the model's API. Unset fields are auto-detected.
756
+ compat: dict[str, Any] | None = None
757
+
758
+
759
+ @dataclass(slots=True, kw_only=True)
760
+ class ImageModel(BaseModel):
761
+ """Image-generation model: usable with ``generate_images()`` only."""
762
+
763
+ type: ClassVar[Literal["image"]] = "image"
764
+ #: Output modalities. Always includes ``"image"``; ``"text"`` means the model can also return text blocks.
765
+ output: list[str] = field(default_factory=lambda: ["image"])
766
+
767
+
768
+ @dataclass(slots=True, kw_only=True)
769
+ class ClassifierModel(BaseModel):
770
+ """Structured classifier model: usable with ``classify()`` only."""
771
+
772
+ type: ClassVar[Literal["classifier"]] = "classifier"
773
+ context_window: int = 0
774
+
775
+
776
+ AnyModel: TypeAlias = Model | ImageModel | ClassifierModel
777
+
778
+
779
+ # ---------------------------------------------------------------------------
780
+ # Image generation
781
+ # ---------------------------------------------------------------------------
782
+
783
+
784
+ @dataclass(slots=True)
785
+ class ImagesContext:
786
+ input: list[TextContent | ImageContent] = field(default_factory=list)
787
+
788
+
789
+ @dataclass(slots=True, kw_only=True)
790
+ class AssistantImages:
791
+ api: str
792
+ provider: str
793
+ model: str
794
+ output: list[TextContent | ImageContent] = field(default_factory=list)
795
+ response_id: str | None = None
796
+ usage: Usage | None = None
797
+ stop_reason: Literal["stop", "error", "aborted"] = "stop"
798
+ error_message: str | None = None
799
+ timestamp: int = field(default_factory=now_ms)
800
+
801
+
802
+ # ---------------------------------------------------------------------------
803
+ # Classification
804
+ # ---------------------------------------------------------------------------
805
+
806
+
807
+ @dataclass(slots=True)
808
+ class ClassifierChoiceQuestion:
809
+ type: ClassVar[Literal["choice"]] = "choice"
810
+ instructions: str
811
+ criteria: dict[str, str]
812
+
813
+
814
+ @dataclass(slots=True)
815
+ class ClassifierScoreQuestion:
816
+ type: ClassVar[Literal["score"]] = "score"
817
+ instructions: str
818
+ criteria: list[str]
819
+
820
+
821
+ @dataclass(slots=True)
822
+ class ClassifierBoolQuestion:
823
+ type: ClassVar[Literal["bool"]] = "bool"
824
+ instructions: str
825
+ #: ``{"true": "...", "false": "..."}``
826
+ criteria: dict[str, str]
827
+
828
+
829
+ ClassifierQuestion: TypeAlias = ClassifierChoiceQuestion | ClassifierScoreQuestion | ClassifierBoolQuestion
830
+
831
+
832
+ @dataclass(slots=True)
833
+ class ClassifierContext:
834
+ state: dict[str, Any]
835
+ questions: dict[str, ClassifierQuestion]
836
+ _: KW_ONLY
837
+ #: Images judged together with ``state``. Only models whose ``input`` includes ``"image"`` accept them.
838
+ images: list[ImageContent] | None = None
839
+
840
+
841
+ @dataclass(slots=True)
842
+ class ClassifierChoiceAnswer:
843
+ type: ClassVar[Literal["choice"]] = "choice"
844
+ choice: str
845
+ probabilities: dict[str, float]
846
+ confidence: float
847
+
848
+
849
+ @dataclass(slots=True)
850
+ class ClassifierScoreAnswer:
851
+ type: ClassVar[Literal["score"]] = "score"
852
+ score: float
853
+ confidence: float
854
+
855
+
856
+ @dataclass(slots=True)
857
+ class ClassifierBoolAnswer:
858
+ type: ClassVar[Literal["bool"]] = "bool"
859
+ probability: float
860
+
861
+
862
+ ClassifierAnswer: TypeAlias = ClassifierChoiceAnswer | ClassifierScoreAnswer | ClassifierBoolAnswer
863
+
864
+
865
+ @dataclass(slots=True, kw_only=True)
866
+ class ClassifierResult:
867
+ api: str
868
+ provider: str
869
+ model: str
870
+ answers: dict[str, ClassifierAnswer] = field(default_factory=dict)
871
+ usage: Usage | None = None
872
+ stop_reason: Literal["stop", "error", "aborted"] = "stop"
873
+ error_message: str | None = None
874
+ timestamp: int = field(default_factory=now_ms)
875
+
876
+
877
+ # ---------------------------------------------------------------------------
878
+ # Options
879
+ # ---------------------------------------------------------------------------
880
+
881
+
882
+ @dataclass(slots=True)
883
+ class ProviderResponse:
884
+ status: int
885
+ headers: dict[str, str]
886
+
887
+
888
+ class ThinkingBudgets(TypedDict, total=False):
889
+ """Token budgets for each thinking level (token-based providers only)."""
890
+
891
+ minimal: int
892
+ low: int
893
+ medium: int
894
+ high: int
895
+
896
+
897
+ MaybeAwaitable: TypeAlias = "Any | Awaitable[Any]"
898
+
899
+
900
+ class ProviderRequestOptions(TypedDict, total=False):
901
+ """Authentication, HTTP transport, and lifecycle callbacks shared by provider requests.
902
+
903
+ Every public entry point also accepts these as keyword arguments.
904
+ """
905
+
906
+ signal: AbortSignal
907
+ api_key: str
908
+ #: Custom ``httpx.AsyncClient`` for provider HTTP requests (the analog of pi's ``fetch`` option).
909
+ http_client: httpx.AsyncClient
910
+ #: Provider-scoped environment values; take precedence over ``os.environ``.
911
+ env: ProviderEnv
912
+ #: Inspect or replace the provider payload before sending. Return ``None`` to keep it unchanged.
913
+ on_payload: Callable[[Any, Any], Any]
914
+ #: Called after an HTTP response is received, before its body is consumed.
915
+ on_response: Callable[[ProviderResponse, Any], Any]
916
+ #: Extra headers merged over provider defaults. ``None`` values suppress a default header.
917
+ headers: ProviderHeaders
918
+ #: HTTP request timeout in milliseconds.
919
+ timeout_ms: float
920
+ #: Maximum client-side retry attempts for retryable HTTP failures (default 0 at this layer).
921
+ max_retries: int
922
+ #: Fail instead of waiting when the server requests a retry delay above this (default 60000; 0 disables).
923
+ max_retry_delay_ms: float
924
+
925
+
926
+ class StreamOptions(ProviderRequestOptions, total=False):
927
+ #: Observe each parsed provider stream event before pi normalizes it. Treat as read-only.
928
+ on_provider_stream_event: Callable[[Any, Any], Any]
929
+ temperature: float
930
+ #: Extra sampling parameters merged into OpenAI-compatible request bodies (``top_p``, ``top_k``, ...).
931
+ sampling_params: SamplingParams
932
+ max_tokens: int
933
+ transport: Transport
934
+ #: Prompt cache retention preference. Default ``"short"``.
935
+ cache_retention: CacheRetention
936
+ #: Session identifier for session-based caching and routing.
937
+ session_id: str
938
+ websocket_connect_timeout_ms: float
939
+ #: Request metadata. Providers extract the fields they understand (e.g. Anthropic ``user_id``).
940
+ metadata: dict[str, Any]
941
+
942
+
943
+ class SimpleStreamOptions(StreamOptions, total=False):
944
+ #: Provider-neutral tool selection.
945
+ tool_choice: ToolChoice
946
+ reasoning: ThinkingLevel
947
+ #: Ask a capable provider to return a durable handle and continue asynchronously.
948
+ deferred: bool | dict[str, str]
949
+ thinking_budgets: ThinkingBudgets
950
+
951
+
952
+ class DeferredFetchOptions(ProviderRequestOptions, total=False):
953
+ #: Maximum provider long-poll duration in milliseconds. 0 performs one status check.
954
+ wait: float
955
+
956
+
957
+ DeferredCancelOptions: TypeAlias = ProviderRequestOptions
958
+
959
+
960
+ class ImagesOptions(ProviderRequestOptions, total=False):
961
+ metadata: dict[str, Any]
962
+
963
+
964
+ class ClassifierOptions(ProviderRequestOptions, total=False):
965
+ #: Divides answer logits before normalization. Values above 1 soften the distribution.
966
+ temperature: float
967
+
968
+
969
+ # ---------------------------------------------------------------------------
970
+ # Implementation contracts
971
+ # ---------------------------------------------------------------------------
972
+
973
+ StreamFunction: TypeAlias = Callable[
974
+ [Model, TranscriptContext, "Mapping[str, Any] | None"], "AssistantMessageEventStream"
975
+ ]
976
+
977
+
978
+ class ProviderStreams(Protocol):
979
+ """The stream contract of an API implementation module.
980
+
981
+ Every module under ``pi_ai.api`` exposes ``stream`` and ``stream_simple``; capable
982
+ modules also expose ``fetch_deferred`` and ``cancel_deferred``.
983
+ """
984
+
985
+ def stream(
986
+ self, model: Model, context: TranscriptContext, options: Mapping[str, Any] | None = None
987
+ ) -> AssistantMessageEventStream: ...
988
+
989
+ def stream_simple(
990
+ self, model: Model, context: TranscriptContext, options: Mapping[str, Any] | None = None
991
+ ) -> AssistantMessageEventStream: ...
992
+
993
+
994
+ class ProviderImages(Protocol):
995
+ async def generate_images(
996
+ self, model: ImageModel, context: ImagesContext, options: Mapping[str, Any] | None = None
997
+ ) -> AssistantImages: ...
998
+
999
+
1000
+ class ProviderClassifier(Protocol):
1001
+ async def classify(
1002
+ self, model: ClassifierModel, context: ClassifierContext, options: Mapping[str, Any] | None = None
1003
+ ) -> ClassifierResult: ...