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/compat.py ADDED
@@ -0,0 +1,651 @@
1
+ """Compatibility entry point preserving the old global pi-ai API surface.
2
+
3
+ API-dispatch :func:`stream`/:func:`complete` with env API key injection, the API
4
+ registry, generated catalog reads (:func:`get_model`/:func:`get_models`/
5
+ :func:`get_providers`), per-API lazy stream wrappers, and image generation.
6
+
7
+ Existing apps switch imports from ``pi_ai`` to ``pi_ai.compat`` unchanged (this
8
+ module re-exports the whole root API); new code uses :func:`pi_ai.create_models`
9
+ and the provider factories in :mod:`pi_ai.providers`.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import random
15
+ import string
16
+ from collections.abc import Awaitable, Callable, Mapping, Sequence
17
+ from dataclasses import dataclass
18
+ from typing import Any, TypeAlias
19
+
20
+ from . import * # noqa: F403 (the compat entry point re-exports the root API)
21
+ from . import __all__ as _root_all
22
+ from .api.lazy import LazyApi
23
+ from .api.registry import (
24
+ anthropic_messages_api,
25
+ azure_openai_responses_api,
26
+ bedrock_converse_stream_api,
27
+ google_generative_ai_api,
28
+ google_vertex_api,
29
+ mistral_conversations_api,
30
+ openai_codex_responses_api,
31
+ openai_completions_api,
32
+ openai_responses_api,
33
+ openrouter_images_api,
34
+ pi_messages_api,
35
+ )
36
+ from .env_api_keys import (
37
+ ANTHROPIC_API_KEY_ENV,
38
+ ANTHROPIC_AUTH_TOKEN_ENV,
39
+ ANTHROPIC_FEDERATION_RULE_ID_ENV,
40
+ ANTHROPIC_IDENTITY_TOKEN_FILE_ENV,
41
+ ANTHROPIC_OAUTH_TOKEN_ENV,
42
+ ANTHROPIC_ORGANIZATION_ID_ENV,
43
+ ANTHROPIC_SERVICE_ACCOUNT_ID_ENV,
44
+ ANTHROPIC_WORKSPACE_ID_ENV,
45
+ PROVIDER_API_KEY_ENV_VARS,
46
+ find_env_keys,
47
+ get_env_api_key,
48
+ )
49
+ from .event_stream import AssistantMessageEventStream
50
+ from .models import Models, Provider
51
+ from .providers.all import (
52
+ BuiltinProvider,
53
+ builtin_models,
54
+ get_builtin_image_model,
55
+ get_builtin_image_models,
56
+ get_builtin_model,
57
+ get_builtin_models,
58
+ get_builtin_providers,
59
+ )
60
+ from .providers.catalog import BUILTIN_PROVIDERS, load_catalog
61
+ from .providers.faux import FauxModelDefinition, FauxProviderState, FauxResponseStep, create_faux_core
62
+ from .types import (
63
+ AssistantImages,
64
+ AssistantMessage,
65
+ Context,
66
+ ImageModel,
67
+ ImagesContext,
68
+ Model,
69
+ TranscriptContext,
70
+ now_ms,
71
+ )
72
+ from .utils.diagnostics import format_thrown_value
73
+ from .utils.transcript import normalize_context
74
+
75
+ __all__ = [
76
+ *_root_all,
77
+ # lazy API wrappers
78
+ "anthropic_messages_api",
79
+ "azure_openai_responses_api",
80
+ "bedrock_converse_stream_api",
81
+ "google_generative_ai_api",
82
+ "google_vertex_api",
83
+ "mistral_conversations_api",
84
+ "openai_codex_responses_api",
85
+ "openai_completions_api",
86
+ "openai_responses_api",
87
+ "pi_messages_api",
88
+ # env API keys
89
+ "ANTHROPIC_AUTH_TOKEN_ENV",
90
+ "ANTHROPIC_OAUTH_TOKEN_ENV",
91
+ "ANTHROPIC_API_KEY_ENV",
92
+ "ANTHROPIC_FEDERATION_RULE_ID_ENV",
93
+ "ANTHROPIC_ORGANIZATION_ID_ENV",
94
+ "ANTHROPIC_SERVICE_ACCOUNT_ID_ENV",
95
+ "ANTHROPIC_IDENTITY_TOKEN_FILE_ENV",
96
+ "ANTHROPIC_WORKSPACE_ID_ENV",
97
+ "PROVIDER_API_KEY_ENV_VARS",
98
+ "find_env_keys",
99
+ "get_env_api_key",
100
+ # static catalog reads
101
+ "BuiltinProvider",
102
+ "BuiltinImageProvider",
103
+ "get_model",
104
+ "get_models",
105
+ "get_providers",
106
+ "get_image_model",
107
+ "get_image_models",
108
+ "get_image_providers",
109
+ # API registry
110
+ "ApiStreamFunction",
111
+ "ApiStreamSimpleFunction",
112
+ "ApiProvider",
113
+ "register_api_provider",
114
+ "get_api_provider",
115
+ "get_api_providers",
116
+ "unregister_api_providers",
117
+ "register_builtin_api_providers",
118
+ "reset_api_providers",
119
+ "FauxProviderRegistration",
120
+ "register_faux_provider",
121
+ # api dispatch
122
+ "stream",
123
+ "complete",
124
+ "stream_simple",
125
+ "complete_simple",
126
+ # images
127
+ "ImagesApiFunction",
128
+ "ImagesApiProvider",
129
+ "register_images_api_provider",
130
+ "get_images_api_provider",
131
+ "generate_images",
132
+ "generate_images_openrouter",
133
+ "register_builtin_images_api_providers",
134
+ # legacy per-API aliases
135
+ "stream_anthropic",
136
+ "stream_simple_anthropic",
137
+ "stream_azure_openai_responses",
138
+ "stream_simple_azure_openai_responses",
139
+ "stream_google",
140
+ "stream_simple_google",
141
+ "stream_google_vertex",
142
+ "stream_simple_google_vertex",
143
+ "stream_mistral",
144
+ "stream_simple_mistral",
145
+ "stream_openai_codex_responses",
146
+ "stream_simple_openai_codex_responses",
147
+ "stream_openai_completions",
148
+ "stream_simple_openai_completions",
149
+ "stream_openai_responses",
150
+ "stream_simple_openai_responses",
151
+ ]
152
+
153
+
154
+ def _merge_options(options: Mapping[str, Any] | None, kwargs: Mapping[str, Any]) -> dict[str, Any] | None:
155
+ if not kwargs:
156
+ return dict(options) if options is not None else None
157
+ return {**(options or {}), **kwargs}
158
+
159
+
160
+ # ---------------------------------------------------------------------------
161
+ # Static catalog reads
162
+ # ---------------------------------------------------------------------------
163
+
164
+ #: Deprecated static catalog read. Use ``pi_ai.providers.all.get_builtin_model`` or ``Models.get_model()``.
165
+ get_model = get_builtin_model
166
+
167
+ #: Deprecated static catalog read. Use ``pi_ai.providers.all.get_builtin_models`` or ``Models.get_models()``.
168
+ get_models = get_builtin_models
169
+
170
+ #: Deprecated static catalog read. Use ``pi_ai.providers.all.get_builtin_providers`` or ``Models.get_providers()``.
171
+ get_providers = get_builtin_providers
172
+
173
+ #: Built-in providers with at least one image model in the generated catalog.
174
+ BuiltinImageProvider: TypeAlias = str
175
+
176
+ _image_providers: list[str] | None = None
177
+
178
+
179
+ def get_image_model(provider: str, model_id: str) -> ImageModel | None:
180
+ """Deprecated static catalog read. Use ``get_builtin_image_model`` or ``Models.get_model_of_type("image", ...)``."""
181
+ return get_builtin_image_model(provider, model_id)
182
+
183
+
184
+ def get_image_providers() -> list[str]:
185
+ """Deprecated static catalog read. Use ``Models.get_providers()``."""
186
+ global _image_providers
187
+ if _image_providers is None:
188
+ _image_providers = [provider for provider in BUILTIN_PROVIDERS if load_catalog(provider)["image"]]
189
+ return list(_image_providers)
190
+
191
+
192
+ def get_image_models(provider: str) -> list[ImageModel]:
193
+ """Deprecated static catalog read. Use ``get_builtin_image_models`` or ``Models.get_models_of_type("image")``."""
194
+ return get_builtin_image_models(provider)
195
+
196
+
197
+ # ---------------------------------------------------------------------------
198
+ # API registry
199
+ # ---------------------------------------------------------------------------
200
+
201
+ ApiStreamFunction: TypeAlias = Callable[
202
+ [Model, TranscriptContext, "Mapping[str, Any] | None"], AssistantMessageEventStream
203
+ ]
204
+ ApiStreamSimpleFunction: TypeAlias = ApiStreamFunction
205
+
206
+
207
+ @dataclass(slots=True)
208
+ class ApiProvider:
209
+ """A chat API implementation registered under its ``api`` id."""
210
+
211
+ api: str
212
+ stream: ApiStreamFunction
213
+ stream_simple: ApiStreamSimpleFunction
214
+
215
+
216
+ @dataclass(slots=True)
217
+ class _RegisteredApiProvider:
218
+ provider: ApiProvider
219
+ source_id: str | None
220
+
221
+
222
+ _api_provider_registry: dict[str, _RegisteredApiProvider] = {}
223
+
224
+
225
+ def _field(provider: Any, name: str) -> Any:
226
+ return provider[name] if isinstance(provider, Mapping) else getattr(provider, name)
227
+
228
+
229
+ def _wrap_stream(api: str, stream_fn: Callable[..., AssistantMessageEventStream]) -> ApiStreamFunction:
230
+ def wrapped(
231
+ model: Model, context: TranscriptContext, options: Mapping[str, Any] | None = None
232
+ ) -> AssistantMessageEventStream:
233
+ if model.api != api:
234
+ raise ValueError(f"Mismatched api: {model.api} expected {api}")
235
+ return stream_fn(model, context, options)
236
+
237
+ return wrapped
238
+
239
+
240
+ def register_api_provider(provider: ApiProvider | Mapping[str, Any] | Any, source_id: str | None = None) -> None:
241
+ """Register (or replace) the implementation for ``provider.api``.
242
+
243
+ ``provider`` is an :class:`ApiProvider`, any object with ``api``/``stream``/``stream_simple``
244
+ attributes, or a mapping with those keys. ``source_id`` groups registrations for
245
+ :func:`unregister_api_providers`.
246
+ """
247
+ api = _field(provider, "api")
248
+ _api_provider_registry[api] = _RegisteredApiProvider(
249
+ ApiProvider(
250
+ api=api,
251
+ stream=_wrap_stream(api, _field(provider, "stream")),
252
+ stream_simple=_wrap_stream(api, _field(provider, "stream_simple")),
253
+ ),
254
+ source_id,
255
+ )
256
+
257
+
258
+ def get_api_provider(api: str) -> ApiProvider | None:
259
+ entry = _api_provider_registry.get(api)
260
+ return entry.provider if entry is not None else None
261
+
262
+
263
+ def get_api_providers() -> list[ApiProvider]:
264
+ return [entry.provider for entry in _api_provider_registry.values()]
265
+
266
+
267
+ def unregister_api_providers(source_id: str) -> None:
268
+ """Remove every registration made with ``source_id``."""
269
+ for api in [api for api, entry in _api_provider_registry.items() if entry.source_id == source_id]:
270
+ del _api_provider_registry[api]
271
+
272
+
273
+ def _clear_api_providers() -> None:
274
+ _api_provider_registry.clear()
275
+
276
+
277
+ class FauxProviderRegistration:
278
+ """Handle returned by :func:`register_faux_provider`."""
279
+
280
+ def __init__(self, core: Any, source_id: str) -> None:
281
+ self._core = core
282
+ self._source_id = source_id
283
+ self.api: str = core.api
284
+ self.models: list[Model] = core.models
285
+ self.state: FauxProviderState = core.state
286
+
287
+ def get_model(self, model_id: str | None = None) -> Model | None:
288
+ """The first faux model, or the one with ``model_id`` (``None`` if unknown)."""
289
+ return self._core.get_model(model_id)
290
+
291
+ def set_responses(self, responses: Sequence[FauxResponseStep]) -> None:
292
+ self._core.set_responses(responses)
293
+
294
+ def append_responses(self, responses: Sequence[FauxResponseStep]) -> None:
295
+ self._core.append_responses(responses)
296
+
297
+ def get_pending_response_count(self) -> int:
298
+ return self._core.get_pending_response_count()
299
+
300
+ def unregister(self) -> None:
301
+ unregister_api_providers(self._source_id)
302
+
303
+
304
+ def _random_base36(length: int) -> str:
305
+ return "".join(random.choices(string.digits + string.ascii_lowercase, k=length))
306
+
307
+
308
+ def register_faux_provider(
309
+ *,
310
+ api: str | None = None,
311
+ provider: str | None = None,
312
+ models: Sequence[FauxModelDefinition | Mapping[str, Any]] | None = None,
313
+ deferred: Mapping[str, Any] | None = None,
314
+ tokens_per_second: float | None = None,
315
+ token_size: Mapping[str, int] | None = None,
316
+ ) -> FauxProviderRegistration:
317
+ """Register a scripted in-memory API implementation in the global API registry."""
318
+ core = create_faux_core(
319
+ api=api,
320
+ provider=provider,
321
+ models=models,
322
+ deferred=deferred,
323
+ tokens_per_second=tokens_per_second,
324
+ token_size=token_size,
325
+ )
326
+ source_id = f"faux-provider-{_random_base36(8)}"
327
+ register_api_provider(ApiProvider(api=core.api, stream=core.stream, stream_simple=core.stream_simple), source_id)
328
+ return FauxProviderRegistration(core, source_id)
329
+
330
+
331
+ _BUILTIN_APIS: tuple[tuple[str, LazyApi], ...] = (
332
+ ("anthropic-messages", anthropic_messages_api()),
333
+ ("openai-completions", openai_completions_api()),
334
+ ("openai-responses", openai_responses_api()),
335
+ ("openai-codex-responses", openai_codex_responses_api()),
336
+ ("azure-openai-responses", azure_openai_responses_api()),
337
+ ("google-generative-ai", google_generative_ai_api()),
338
+ ("google-vertex", google_vertex_api()),
339
+ ("mistral-conversations", mistral_conversations_api()),
340
+ ("bedrock-converse-stream", bedrock_converse_stream_api()),
341
+ ("pi-messages", pi_messages_api()),
342
+ )
343
+
344
+ _builtin_api_provider_instances: dict[str, ApiProvider | None] = {}
345
+
346
+
347
+ def register_builtin_api_providers() -> None:
348
+ """Register the built-in API implementations without clobbering existing entries.
349
+
350
+ Compat may load after a test or extension has already registered an override for
351
+ a built-in api id.
352
+ """
353
+ for api, streams in _BUILTIN_APIS:
354
+ if get_api_provider(api) is None:
355
+ register_api_provider(ApiProvider(api=api, stream=streams.stream, stream_simple=streams.stream_simple))
356
+ _builtin_api_provider_instances[api] = get_api_provider(api)
357
+
358
+
359
+ def reset_api_providers() -> None:
360
+ """Drop every registration and re-register the built-in API implementations."""
361
+ _clear_api_providers()
362
+ _builtin_api_provider_instances.clear()
363
+ register_builtin_api_providers()
364
+
365
+
366
+ register_builtin_api_providers()
367
+
368
+
369
+ # ---------------------------------------------------------------------------
370
+ # API dispatch with env API key injection
371
+ # ---------------------------------------------------------------------------
372
+
373
+ _AMBIENT_AUTH_MARKER = "<authenticated>"
374
+ _compat_models_instance: Models | None = None
375
+
376
+
377
+ def _compat_models() -> Models:
378
+ global _compat_models_instance
379
+ if _compat_models_instance is None:
380
+ _compat_models_instance = builtin_models()
381
+ return _compat_models_instance
382
+
383
+
384
+ def _has_explicit_api_key(api_key: Any) -> bool:
385
+ return isinstance(api_key, str) and len(api_key.strip()) > 0
386
+
387
+
388
+ def _with_env_api_key(model: Model, options: dict[str, Any] | None) -> dict[str, Any] | None:
389
+ if options is not None and _has_explicit_api_key(options.get("api_key")):
390
+ return options
391
+ api_key = get_env_api_key(model.provider, options.get("env") if options else None)
392
+ if not api_key or api_key == _AMBIENT_AUTH_MARKER:
393
+ return options
394
+ return {**(options or {}), "api_key": api_key}
395
+
396
+
397
+ def _has_resolved_cloudflare_auth(options: Mapping[str, Any] | None) -> bool:
398
+ if not options:
399
+ return False
400
+ headers = options.get("headers") or {}
401
+ return _has_explicit_api_key(options.get("api_key")) or isinstance(headers.get("cf-aig-authorization"), str)
402
+
403
+
404
+ def _get_builtin_provider_for_model(model: Model) -> Provider | None:
405
+ if get_api_provider(model.api) is not _builtin_api_provider_instances.get(model.api):
406
+ return None
407
+ provider = _compat_models().get_provider(model.provider)
408
+ if provider is None:
409
+ return None
410
+ return provider if any(candidate.api == model.api for candidate in provider.get_models()) else None
411
+
412
+
413
+ def _resolve_api_provider(api: str) -> ApiProvider:
414
+ provider = get_api_provider(api)
415
+ if provider is None:
416
+ raise ValueError(f"No API provider registered for api: {api}")
417
+ return provider
418
+
419
+
420
+ def stream(
421
+ model: Model,
422
+ context: Context | Mapping[str, Any] | TranscriptContext,
423
+ options: Mapping[str, Any] | None = None,
424
+ /,
425
+ **kwargs: Any,
426
+ ) -> AssistantMessageEventStream:
427
+ """Stream through the implementation registered for ``model.api``.
428
+
429
+ Built-in providers' models go through their provider (so provider request shaping
430
+ applies); the API key comes from ``options["api_key"]`` or the provider's env var.
431
+ Raises :class:`ValueError` when no implementation is registered for ``model.api``.
432
+ """
433
+ transcript = normalize_context(context)
434
+ opts = _merge_options(options, kwargs)
435
+ builtin_provider = _get_builtin_provider_for_model(model)
436
+ if builtin_provider is not None:
437
+ if model.provider.startswith("cloudflare-") and not _has_resolved_cloudflare_auth(opts):
438
+ return _compat_models().stream(model, transcript, opts)
439
+ return builtin_provider.stream(model, transcript, _with_env_api_key(model, opts))
440
+ provider = _resolve_api_provider(model.api)
441
+ return provider.stream(model, transcript, _with_env_api_key(model, opts))
442
+
443
+
444
+ async def complete(
445
+ model: Model,
446
+ context: Context | Mapping[str, Any] | TranscriptContext,
447
+ options: Mapping[str, Any] | None = None,
448
+ /,
449
+ **kwargs: Any,
450
+ ) -> AssistantMessage:
451
+ return await stream(model, context, options, **kwargs).result()
452
+
453
+
454
+ def stream_simple(
455
+ model: Model,
456
+ context: Context | Mapping[str, Any] | TranscriptContext,
457
+ options: Mapping[str, Any] | None = None,
458
+ /,
459
+ **kwargs: Any,
460
+ ) -> AssistantMessageEventStream:
461
+ """Like :func:`stream` with provider-neutral options (``reasoning="medium"``, ``tool_choice``...)."""
462
+ transcript = normalize_context(context)
463
+ opts = _merge_options(options, kwargs)
464
+ builtin_provider = _get_builtin_provider_for_model(model)
465
+ if builtin_provider is not None:
466
+ if model.provider.startswith("cloudflare-") and not _has_resolved_cloudflare_auth(opts):
467
+ return _compat_models().stream_simple(model, transcript, opts)
468
+ return builtin_provider.stream_simple(model, transcript, _with_env_api_key(model, opts))
469
+ provider = _resolve_api_provider(model.api)
470
+ return provider.stream_simple(model, transcript, _with_env_api_key(model, opts))
471
+
472
+
473
+ async def complete_simple(
474
+ model: Model,
475
+ context: Context | Mapping[str, Any] | TranscriptContext,
476
+ options: Mapping[str, Any] | None = None,
477
+ /,
478
+ **kwargs: Any,
479
+ ) -> AssistantMessage:
480
+ return await stream_simple(model, context, options, **kwargs).result()
481
+
482
+
483
+ # ---------------------------------------------------------------------------
484
+ # Image generation registry
485
+ # ---------------------------------------------------------------------------
486
+
487
+ ImagesApiFunction: TypeAlias = Callable[
488
+ [ImageModel, ImagesContext, "Mapping[str, Any] | None"], Awaitable[AssistantImages]
489
+ ]
490
+
491
+
492
+ @dataclass(slots=True)
493
+ class ImagesApiProvider:
494
+ """An image-generation implementation registered under its ``api`` id."""
495
+
496
+ api: str
497
+ generate_images: ImagesApiFunction
498
+
499
+
500
+ @dataclass(slots=True)
501
+ class _RegisteredImagesApiProvider:
502
+ provider: ImagesApiProvider
503
+ source_id: str | None
504
+
505
+
506
+ _images_api_provider_registry: dict[str, _RegisteredImagesApiProvider] = {}
507
+
508
+
509
+ def _wrap_generate_images(api: str, generate: Callable[..., Awaitable[AssistantImages]]) -> ImagesApiFunction:
510
+ async def wrapped(
511
+ model: ImageModel, context: ImagesContext, options: Mapping[str, Any] | None = None
512
+ ) -> AssistantImages:
513
+ if model.api != api:
514
+ raise ValueError(f"Mismatched api: {model.api} expected {api}")
515
+ return await generate(model, context, options)
516
+
517
+ return wrapped
518
+
519
+
520
+ def register_images_api_provider(
521
+ provider: ImagesApiProvider | Mapping[str, Any] | Any, source_id: str | None = None
522
+ ) -> None:
523
+ """Register (or replace) the image implementation for ``provider.api``."""
524
+ api = _field(provider, "api")
525
+ _images_api_provider_registry[api] = _RegisteredImagesApiProvider(
526
+ ImagesApiProvider(api=api, generate_images=_wrap_generate_images(api, _field(provider, "generate_images"))),
527
+ source_id,
528
+ )
529
+
530
+
531
+ def get_images_api_provider(api: str) -> ImagesApiProvider | None:
532
+ entry = _images_api_provider_registry.get(api)
533
+ return entry.provider if entry is not None else None
534
+
535
+
536
+ _openrouter_images = openrouter_images_api()
537
+
538
+
539
+ async def generate_images_openrouter(
540
+ model: ImageModel, context: ImagesContext, options: Mapping[str, Any] | None = None
541
+ ) -> AssistantImages:
542
+ """OpenRouter image generation, loaded on first use. Load and request failures become error results."""
543
+ try:
544
+ return await _openrouter_images.generate_images(model, context, options)
545
+ except Exception as error:
546
+ return AssistantImages(
547
+ api=model.api,
548
+ provider=model.provider,
549
+ model=model.id,
550
+ output=[],
551
+ stop_reason="error",
552
+ error_message=format_thrown_value(error),
553
+ timestamp=now_ms(),
554
+ )
555
+
556
+
557
+ def register_builtin_images_api_providers() -> None:
558
+ register_images_api_provider(ImagesApiProvider(api="openrouter-images", generate_images=generate_images_openrouter))
559
+
560
+
561
+ register_builtin_images_api_providers()
562
+
563
+
564
+ async def generate_images(
565
+ model: ImageModel,
566
+ context: ImagesContext,
567
+ options: Mapping[str, Any] | None = None,
568
+ /,
569
+ **kwargs: Any,
570
+ ) -> AssistantImages:
571
+ """Global image generation dispatched on ``model.api`` through the images API registry.
572
+
573
+ Auth must be passed explicitly via ``api_key``; prefer ``Models.generate_images()``,
574
+ which resolves provider auth. Raises :class:`ValueError` for an unregistered api.
575
+ """
576
+ provider = get_images_api_provider(model.api)
577
+ if provider is None:
578
+ raise ValueError(f"No API provider registered for api: {model.api}")
579
+ return await provider.generate_images(model, context, _merge_options(options, kwargs))
580
+
581
+
582
+ # ---------------------------------------------------------------------------
583
+ # Legacy per-API stream aliases
584
+ # ---------------------------------------------------------------------------
585
+
586
+
587
+ def _legacy_alias(streams: LazyApi, simple: bool, name: str, module: str) -> ApiStreamFunction:
588
+ method = streams.stream_simple if simple else streams.stream
589
+ target = "stream_simple" if simple else "stream"
590
+
591
+ def alias(
592
+ model: Model,
593
+ context: Context | Mapping[str, Any] | TranscriptContext,
594
+ options: Mapping[str, Any] | None = None,
595
+ /,
596
+ **kwargs: Any,
597
+ ) -> AssistantMessageEventStream:
598
+ return method(model, normalize_context(context), _merge_options(options, kwargs))
599
+
600
+ alias.__name__ = alias.__qualname__ = name
601
+ alias.__doc__ = f"Deprecated: use ``pi_ai.api.{module}.{target}`` or ``{module}_api().{target}``."
602
+ return alias
603
+
604
+
605
+ _anthropic_messages_streams = anthropic_messages_api()
606
+ _azure_openai_responses_streams = azure_openai_responses_api()
607
+ _google_generative_ai_streams = google_generative_ai_api()
608
+ _google_vertex_streams = google_vertex_api()
609
+ _mistral_conversations_streams = mistral_conversations_api()
610
+ _openai_codex_responses_streams = openai_codex_responses_api()
611
+ _openai_completions_streams = openai_completions_api()
612
+ _openai_responses_streams = openai_responses_api()
613
+
614
+ stream_anthropic = _legacy_alias(_anthropic_messages_streams, False, "stream_anthropic", "anthropic_messages")
615
+ stream_simple_anthropic = _legacy_alias(
616
+ _anthropic_messages_streams, True, "stream_simple_anthropic", "anthropic_messages"
617
+ )
618
+ stream_azure_openai_responses = _legacy_alias(
619
+ _azure_openai_responses_streams, False, "stream_azure_openai_responses", "azure_openai_responses"
620
+ )
621
+ stream_simple_azure_openai_responses = _legacy_alias(
622
+ _azure_openai_responses_streams, True, "stream_simple_azure_openai_responses", "azure_openai_responses"
623
+ )
624
+ stream_google = _legacy_alias(_google_generative_ai_streams, False, "stream_google", "google_generative_ai")
625
+ stream_simple_google = _legacy_alias(
626
+ _google_generative_ai_streams, True, "stream_simple_google", "google_generative_ai"
627
+ )
628
+ stream_google_vertex = _legacy_alias(_google_vertex_streams, False, "stream_google_vertex", "google_vertex")
629
+ stream_simple_google_vertex = _legacy_alias(
630
+ _google_vertex_streams, True, "stream_simple_google_vertex", "google_vertex"
631
+ )
632
+ stream_mistral = _legacy_alias(_mistral_conversations_streams, False, "stream_mistral", "mistral_conversations")
633
+ stream_simple_mistral = _legacy_alias(
634
+ _mistral_conversations_streams, True, "stream_simple_mistral", "mistral_conversations"
635
+ )
636
+ stream_openai_codex_responses = _legacy_alias(
637
+ _openai_codex_responses_streams, False, "stream_openai_codex_responses", "openai_codex_responses"
638
+ )
639
+ stream_simple_openai_codex_responses = _legacy_alias(
640
+ _openai_codex_responses_streams, True, "stream_simple_openai_codex_responses", "openai_codex_responses"
641
+ )
642
+ stream_openai_completions = _legacy_alias(
643
+ _openai_completions_streams, False, "stream_openai_completions", "openai_completions"
644
+ )
645
+ stream_simple_openai_completions = _legacy_alias(
646
+ _openai_completions_streams, True, "stream_simple_openai_completions", "openai_completions"
647
+ )
648
+ stream_openai_responses = _legacy_alias(_openai_responses_streams, False, "stream_openai_responses", "openai_responses")
649
+ stream_simple_openai_responses = _legacy_alias(
650
+ _openai_responses_streams, True, "stream_simple_openai_responses", "openai_responses"
651
+ )