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
@@ -0,0 +1,234 @@
1
+ """Google Vertex AI adapter.
2
+
3
+ Streams through the official ``google-genai`` SDK (``Client(vertexai=True, ...)``), as pi
4
+ upstream does with ``@google/genai``: either with a Vertex API key (express mode) or with
5
+ Application Default Credentials (through ``google-auth``) plus project and location.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import re
12
+ import urllib.parse
13
+ from collections.abc import Mapping
14
+ from typing import Any, Literal
15
+
16
+ import google.auth
17
+ from google.auth.credentials import Credentials
18
+ from google.genai import Client
19
+ from google.genai import types as genai_types
20
+
21
+ from ..event_stream import AssistantMessageEventStream
22
+ from ..models import clamp_thinking_level
23
+ from ..types import Model, StreamOptions, TranscriptContext
24
+ from ..utils.http import get_http_client
25
+ from ..utils.misc import get_provider_env_value
26
+ from .google_shared import (
27
+ GoogleThinkingOptions,
28
+ ResolvedGoogleThinkingLevel,
29
+ get_google_client,
30
+ new_google_output,
31
+ resolve_google_thinking_level,
32
+ run_google_stream,
33
+ to_google_thinking_level,
34
+ uses_google_thinking_level,
35
+ )
36
+ from .simple_options import build_base_options
37
+
38
+ __all__ = [
39
+ "GoogleVertexOptions",
40
+ "stream",
41
+ "stream_simple",
42
+ "get_google_budget",
43
+ "resolve_api_key",
44
+ "resolve_project",
45
+ "resolve_location",
46
+ ]
47
+
48
+ API_VERSION = "v1"
49
+ GCP_VERTEX_CREDENTIALS_MARKER = "gcp-vertex-credentials"
50
+ CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform"
51
+ _GLOBAL_ENDPOINT = "https://aiplatform.googleapis.com/"
52
+ _PLACEHOLDER_API_KEY = re.compile(r"<[^>]+>")
53
+ _API_VERSION_SEGMENT = re.compile(r"v\d+(?:beta\d*)?")
54
+ _API_VERSION_IN_TEXT = re.compile(r"(?:^|/)v\d+(?:beta\d*)?(?:/|$)")
55
+
56
+
57
+ class GoogleVertexOptions(StreamOptions, total=False):
58
+ tool_choice: Literal["auto", "none", "any"]
59
+ thinking: GoogleThinkingOptions
60
+ project: str
61
+ location: str
62
+
63
+
64
+ def stream(
65
+ model: Model, context: TranscriptContext, options: Mapping[str, Any] | None = None
66
+ ) -> AssistantMessageEventStream:
67
+ """Stream a Vertex AI response."""
68
+ opts = dict(options or {})
69
+ out = AssistantMessageEventStream()
70
+ output = new_google_output(model)
71
+ out.start(
72
+ run_google_stream(
73
+ model,
74
+ context,
75
+ opts,
76
+ out,
77
+ output,
78
+ create_client=lambda: _create_client(model, opts),
79
+ missing_finish_reason_message="Google Vertex stream ended without a finish reason",
80
+ )
81
+ )
82
+ return out
83
+
84
+
85
+ def stream_simple(
86
+ model: Model, context: TranscriptContext, options: Mapping[str, Any] | None = None
87
+ ) -> AssistantMessageEventStream:
88
+ """Stream with provider-neutral options (``reasoning``, ``tool_choice``, ``thinking_budgets``)."""
89
+ options = options or {}
90
+ base: dict[str, Any] = build_base_options(model, context, options, None)
91
+ if options.get("tool_choice") is not None:
92
+ base["tool_choice"] = options["tool_choice"]
93
+ reasoning = options.get("reasoning")
94
+ if not reasoning:
95
+ return stream(model, context, {**base, "thinking": {"enabled": False}})
96
+
97
+ clamped = clamp_thinking_level(model, reasoning)
98
+ if clamped == "off":
99
+ return stream(model, context, {**base, "thinking": {"enabled": False}})
100
+ resolved = resolve_google_thinking_level(model, clamped)
101
+
102
+ if uses_google_thinking_level(model):
103
+ return stream(
104
+ model, context, {**base, "thinking": {"enabled": True, "level": to_google_thinking_level(resolved)}}
105
+ )
106
+ return stream(
107
+ model,
108
+ context,
109
+ {
110
+ **base,
111
+ "thinking": {
112
+ "enabled": True,
113
+ "budget_tokens": get_google_budget(model, resolved, options.get("thinking_budgets")),
114
+ },
115
+ },
116
+ )
117
+
118
+
119
+ # ---------------------------------------------------------------------------
120
+ # Client configuration
121
+ # ---------------------------------------------------------------------------
122
+
123
+
124
+ def resolve_api_key(options: Mapping[str, Any] | None) -> str | None:
125
+ """The Vertex API key, or ``None`` for ADC (no key, the ADC marker, or a ``<placeholder>``)."""
126
+ api_key = ((options or {}).get("api_key") or "").strip()
127
+ if not api_key or api_key == GCP_VERTEX_CREDENTIALS_MARKER or _PLACEHOLDER_API_KEY.fullmatch(api_key):
128
+ return None
129
+ return api_key
130
+
131
+
132
+ def resolve_project(options: Mapping[str, Any] | None) -> str:
133
+ options = options or {}
134
+ env = options.get("env")
135
+ project = (
136
+ options.get("project")
137
+ or get_provider_env_value("GOOGLE_CLOUD_PROJECT", env)
138
+ or get_provider_env_value("GCLOUD_PROJECT", env)
139
+ )
140
+ if not project:
141
+ raise ValueError(
142
+ "Vertex AI requires a project ID. Set GOOGLE_CLOUD_PROJECT/GCLOUD_PROJECT or pass project in options."
143
+ )
144
+ return project
145
+
146
+
147
+ def resolve_location(options: Mapping[str, Any] | None) -> str:
148
+ options = options or {}
149
+ location = options.get("location") or get_provider_env_value("GOOGLE_CLOUD_LOCATION", options.get("env"))
150
+ if not location:
151
+ raise ValueError("Vertex AI requires a location. Set GOOGLE_CLOUD_LOCATION or pass location in options.")
152
+ return location
153
+
154
+
155
+ def _resolve_custom_base_url(base_url: str) -> str | None:
156
+ """A model baseUrl to forward, skipping generated ``{location}`` placeholders."""
157
+ trimmed = base_url.strip()
158
+ if not trimmed or "{location}" in trimmed:
159
+ return None
160
+ return trimmed
161
+
162
+
163
+ def _base_url_includes_api_version(base_url: str) -> bool:
164
+ parsed = urllib.parse.urlsplit(base_url)
165
+ if parsed.scheme and parsed.netloc:
166
+ return any(_API_VERSION_SEGMENT.fullmatch(part) for part in parsed.path.split("/"))
167
+ return _API_VERSION_IN_TEXT.search(base_url) is not None
168
+
169
+
170
+ def _load_credentials_file(path: str) -> Credentials:
171
+ credentials, _project = google.auth.load_credentials_from_file(path, scopes=[CLOUD_PLATFORM_SCOPE])
172
+ return credentials
173
+
174
+
175
+ async def _create_client(model: Model, options: Mapping[str, Any]) -> Client:
176
+ env = options.get("env")
177
+ http_options: dict[str, Any] = {"api_version": API_VERSION}
178
+ custom_base_url = _resolve_custom_base_url(model.base_url)
179
+ if custom_base_url is not None:
180
+ # A model baseUrl is a collection URL: no projects/.../locations/... prefix. The JS SDK
181
+ # still appends the API version unless the URL has one; the Python SDK never does for
182
+ # collection-scoped URLs, so add it here.
183
+ if not _base_url_includes_api_version(custom_base_url):
184
+ custom_base_url = f"{custom_base_url.rstrip('/')}/{API_VERSION}"
185
+ http_options["base_url"] = custom_base_url
186
+ http_options["base_url_resource_scope"] = genai_types.ResourceScope.COLLECTION
187
+ else:
188
+ # The SDK reads GOOGLE_VERTEX_BASE_URL from os.environ itself; this also honors the provider env.
189
+ env_base_url = (get_provider_env_value("GOOGLE_VERTEX_BASE_URL", env) or "").strip()
190
+ if env_base_url:
191
+ http_options["base_url"] = env_base_url
192
+ http_client = get_http_client(http_options.get("base_url") or _GLOBAL_ENDPOINT, options)
193
+ http_options["httpx_async_client"] = http_client
194
+ base_key = (http_options.get("base_url"), http_options.get("base_url_resource_scope"))
195
+
196
+ # Use a Vertex API key if provided, otherwise ADC with project and location.
197
+ api_key = resolve_api_key(options)
198
+ if api_key:
199
+
200
+ async def api_key_client() -> Client:
201
+ return Client(vertexai=True, api_key=api_key, http_options=genai_types.HttpOptions(**http_options))
202
+
203
+ return await get_google_client(http_client, ("vertex-api-key", api_key, *base_key), api_key_client)
204
+
205
+ project = resolve_project(options)
206
+ location = resolve_location(options)
207
+ # Upstream passes GOOGLE_APPLICATION_CREDENTIALS to google-auth as keyFilename; without it the
208
+ # SDK uses google.auth.default(). The cached client keeps the credentials and refreshes the token.
209
+ key_filename = get_provider_env_value("GOOGLE_APPLICATION_CREDENTIALS", env)
210
+
211
+ async def adc_client() -> Client:
212
+ credentials = await asyncio.to_thread(_load_credentials_file, key_filename) if key_filename else None
213
+ return Client(
214
+ vertexai=True,
215
+ project=project,
216
+ location=location,
217
+ credentials=credentials,
218
+ http_options=genai_types.HttpOptions(**http_options),
219
+ )
220
+
221
+ return await get_google_client(http_client, ("vertex-adc", project, location, key_filename, *base_key), adc_client)
222
+
223
+
224
+ def get_google_budget(
225
+ model: Model, level: ResolvedGoogleThinkingLevel, custom_budgets: Mapping[str, int] | None = None
226
+ ) -> int:
227
+ """Token budget for a thinking level on budget-based Gemini models (-1 = dynamic)."""
228
+ if custom_budgets and custom_budgets.get(level) is not None:
229
+ return custom_budgets[level]
230
+ if "2.5-pro" in model.id:
231
+ return {"minimal": 128, "low": 2048, "medium": 8192, "high": 32768}[level]
232
+ if "2.5-flash" in model.id:
233
+ return {"minimal": 128, "low": 2048, "medium": 8192, "high": 24576}[level]
234
+ return -1
pi_ai/api/lazy.py ADDED
@@ -0,0 +1,156 @@
1
+ """Deferred stream setup and lazily imported API implementations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ from collections.abc import AsyncIterable, Awaitable, Callable, Mapping
7
+ from types import ModuleType
8
+ from typing import Any
9
+
10
+ from ..abort import AbortError
11
+ from ..event_stream import AssistantMessageEventStream, EventStream
12
+ from ..types import AssistantMessage, ErrorEvent, Model, TranscriptContext, Usage, now_ms
13
+ from ..utils.diagnostics import format_thrown_value
14
+
15
+ __all__ = ["lazy_stream", "error_stream", "setup_error_message", "LazyApi", "lazy_api"]
16
+
17
+
18
+ def setup_error_message(model: Any, error: Any, timestamp: int | None = None) -> AssistantMessage:
19
+ aborted = isinstance(error, AbortError)
20
+ return AssistantMessage(
21
+ [],
22
+ api=model.api,
23
+ provider=model.provider,
24
+ model=model.id,
25
+ usage=Usage(),
26
+ stop_reason="aborted" if aborted else "error",
27
+ error_message=format_thrown_value(error),
28
+ timestamp=timestamp if timestamp is not None else now_ms(),
29
+ )
30
+
31
+
32
+ def error_stream(model: Any, error: Any, timestamp: int | None = None) -> AssistantMessageEventStream:
33
+ """A stream that terminates immediately with an error event."""
34
+ stream = AssistantMessageEventStream()
35
+ message = setup_error_message(model, error, timestamp)
36
+ stream.push(ErrorEvent(message.stop_reason, message)) # type: ignore[arg-type]
37
+ stream.end(message)
38
+ return stream
39
+
40
+
41
+ def lazy_stream(
42
+ model: Any,
43
+ setup: Callable[[], Awaitable[AsyncIterable[Any]]],
44
+ ) -> AssistantMessageEventStream:
45
+ """Return a stream synchronously while async setup (auth, loading) runs behind it.
46
+
47
+ Setup failures terminate the stream with an error event (``aborted`` when the
48
+ failure is an :class:`AbortError`).
49
+ """
50
+ started_at = now_ms()
51
+ outer = AssistantMessageEventStream()
52
+
53
+ async def run() -> None:
54
+ try:
55
+ inner = await setup()
56
+ except Exception as error:
57
+ message = setup_error_message(model, error, started_at)
58
+ outer.push(ErrorEvent(message.stop_reason, message)) # type: ignore[arg-type]
59
+ outer.end(message)
60
+ return
61
+ if isinstance(inner, EventStream):
62
+ inner.forward_to(outer)
63
+ return
64
+ try:
65
+ async for event in inner:
66
+ outer.push(event)
67
+ except Exception as error:
68
+ message = setup_error_message(model, error, started_at)
69
+ outer.push(ErrorEvent(message.stop_reason, message)) # type: ignore[arg-type]
70
+ result: Callable[[], Awaitable[Any]] | None = getattr(inner, "result", None)
71
+ outer.end(await result() if result is not None else None)
72
+
73
+ outer.start(run())
74
+ return outer
75
+
76
+
77
+ class LazyApi:
78
+ """``ProviderStreams`` that imports its implementation module on first use.
79
+
80
+ The module must expose ``stream`` and ``stream_simple`` (and optionally
81
+ ``fetch_deferred``/``cancel_deferred``). Import failures, for example a missing
82
+ optional dependency, surface as stream errors.
83
+ """
84
+
85
+ def __init__(self, module: str, *, fetch_deferred: bool = False, cancel_deferred: bool = False) -> None:
86
+ self.module = module
87
+ self._loaded: ModuleType | None = None
88
+ self.fetch_deferred: Callable[..., AssistantMessageEventStream] | None = (
89
+ self._fetch_deferred if fetch_deferred else None
90
+ )
91
+ self.cancel_deferred: Callable[..., Awaitable[None]] | None = self._cancel_deferred if cancel_deferred else None
92
+
93
+ def load(self) -> ModuleType:
94
+ module = self._loaded
95
+ if module is None:
96
+ module = importlib.import_module(self.module)
97
+ self._loaded = module
98
+ return module
99
+
100
+ def stream(
101
+ self, model: Model, context: TranscriptContext, options: Mapping[str, Any] | None = None
102
+ ) -> AssistantMessageEventStream:
103
+ try:
104
+ module = self.load()
105
+ except Exception as error:
106
+ return error_stream(model, error)
107
+ return module.stream(model, context, options)
108
+
109
+ def stream_simple(
110
+ self, model: Model, context: TranscriptContext, options: Mapping[str, Any] | None = None
111
+ ) -> AssistantMessageEventStream:
112
+ try:
113
+ module = self.load()
114
+ except Exception as error:
115
+ return error_stream(model, error)
116
+ return module.stream_simple(model, context, options)
117
+
118
+ def _fetch_deferred(self, model: Model, handle: Any, options: Mapping[str, Any] | None = None) -> Any:
119
+ try:
120
+ module = self.load()
121
+ implementation = getattr(module, "fetch_deferred", None)
122
+ if implementation is None:
123
+ raise RuntimeError("API does not support deferred responses")
124
+ except Exception as error:
125
+ return error_stream(model, error)
126
+ return implementation(model, handle, options)
127
+
128
+ async def _cancel_deferred(self, model: Model, handle: Any, options: Mapping[str, Any] | None = None) -> None:
129
+ implementation = getattr(self.load(), "cancel_deferred", None)
130
+ if implementation is None:
131
+ raise RuntimeError("API cannot cancel deferred responses")
132
+ await implementation(model, handle, options)
133
+
134
+ def __repr__(self) -> str:
135
+ return f"LazyApi({self.module!r})"
136
+
137
+
138
+ def lazy_api(module: str, *, fetch_deferred: bool = False, cancel_deferred: bool = False) -> LazyApi:
139
+ return LazyApi(module, fetch_deferred=fetch_deferred, cancel_deferred=cancel_deferred)
140
+
141
+
142
+ class LazyOperation:
143
+ """Lazily imported image or classifier implementation (``generate_images``/``classify``)."""
144
+
145
+ def __init__(self, module: str, attribute: str) -> None:
146
+ self.module = module
147
+ self.attribute = attribute
148
+
149
+ def _get(self) -> Callable[..., Awaitable[Any]]:
150
+ return getattr(importlib.import_module(self.module), self.attribute)
151
+
152
+ async def generate_images(self, model: Any, context: Any, options: Mapping[str, Any] | None = None) -> Any:
153
+ return await self._get()(model, context, options)
154
+
155
+ async def classify(self, model: Any, context: Any, options: Mapping[str, Any] | None = None) -> Any:
156
+ return await self._get()(model, context, options)