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/cli.py ADDED
@@ -0,0 +1,265 @@
1
+ """``pi-ai`` command line: log in to the built-in OAuth providers.
2
+
3
+ ``pi-ai login [provider]`` runs a provider's OAuth flow and saves the credential to
4
+ ``auth.json`` in the current directory (the same format pi uses); ``pi-ai list``
5
+ lists the providers that support OAuth.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import contextlib
12
+ import getpass
13
+ import os
14
+ import re
15
+ import sys
16
+ import threading
17
+ import uuid
18
+ import webbrowser
19
+ from collections import deque
20
+ from collections.abc import Callable, Sequence
21
+ from typing import TYPE_CHECKING, Any, TextIO
22
+
23
+ from .abort import AbortController, AbortError, AbortSignal, abort_scope
24
+ from .auth.credential_store import FileCredentialStore
25
+ from .auth.types import AuthEvent, AuthPrompt, LoginOptions
26
+
27
+ if TYPE_CHECKING:
28
+ from .models import Models, Provider
29
+
30
+ __all__ = ["main", "run"]
31
+
32
+ AUTH_FILE = "auth.json"
33
+
34
+
35
+ class _LineReader:
36
+ """Reads stdin lines on a daemon thread so a dismissed prompt never blocks exit.
37
+
38
+ A line goes to the oldest prompt still waiting; lines typed while no prompt waits
39
+ are dropped. (``asyncio.to_thread(input)`` would keep the interpreter alive until
40
+ Enter is pressed after the browser callback already finished the login.)
41
+ """
42
+
43
+ def __init__(self, stream: Callable[[], TextIO] = lambda: sys.stdin) -> None:
44
+ self._stream = stream
45
+ self._lock = threading.Lock()
46
+ self._waiters: deque[tuple[asyncio.AbstractEventLoop, asyncio.Future[str]]] = deque()
47
+ self._reading = False
48
+
49
+ async def readline(self) -> str:
50
+ loop = asyncio.get_running_loop()
51
+ future: asyncio.Future[str] = loop.create_future()
52
+ with self._lock:
53
+ self._waiters.append((loop, future))
54
+ start = not self._reading
55
+ self._reading = True
56
+ if start:
57
+ threading.Thread(target=self._read_loop, name="pi-ai-stdin", daemon=True).start()
58
+ return await future
59
+
60
+ def _read_loop(self) -> None:
61
+ while True:
62
+ try:
63
+ line: str | None = self._stream().readline()
64
+ except Exception:
65
+ line = ""
66
+ with self._lock:
67
+ while self._waiters and self._waiters[0][1].done():
68
+ self._waiters.popleft()
69
+ if not line:
70
+ waiters = list(self._waiters)
71
+ self._waiters.clear()
72
+ self._reading = False
73
+ else:
74
+ waiters = [self._waiters.popleft()] if self._waiters else []
75
+ if not self._waiters:
76
+ self._reading = False
77
+ reading = self._reading
78
+ for loop, future in waiters:
79
+ with contextlib.suppress(RuntimeError):
80
+ loop.call_soon_threadsafe(_settle_line, future, line)
81
+ if not reading:
82
+ return
83
+
84
+
85
+ def _settle_line(future: asyncio.Future[str], line: str | None) -> None:
86
+ if future.done():
87
+ return
88
+ if not line:
89
+ future.set_exception(EOFError("No input (stdin closed)"))
90
+ else:
91
+ future.set_result(line.rstrip("\r\n"))
92
+
93
+
94
+ async def _read_secret(question: str) -> str:
95
+ loop = asyncio.get_running_loop()
96
+ future: asyncio.Future[str] = loop.create_future()
97
+
98
+ def read() -> None:
99
+ try:
100
+ value = getpass.getpass(question)
101
+ except BaseException as error:
102
+ failure = error if isinstance(error, Exception) else EOFError("No input")
103
+ loop.call_soon_threadsafe(lambda: future.done() or future.set_exception(failure))
104
+ return
105
+ loop.call_soon_threadsafe(lambda: future.done() or future.set_result(value))
106
+
107
+ threading.Thread(target=read, name="pi-ai-getpass", daemon=True).start()
108
+ return await future
109
+
110
+
111
+ def _parse_int(value: str) -> int | None:
112
+ """``Number.parseInt(value, 10)``."""
113
+ match = re.match(r"^\s*([+-]?\d+)", value)
114
+ return int(match.group(1)) if match else None
115
+
116
+
117
+ def _can_open_browser() -> bool:
118
+ # Without a display, `webbrowser` may start a text-mode browser inside this terminal.
119
+ if sys.platform.startswith("linux") or sys.platform.startswith("freebsd"):
120
+ return bool(os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY"))
121
+ return True
122
+
123
+
124
+ def _open_browser(url: str) -> None:
125
+ if not _can_open_browser():
126
+ return
127
+ with contextlib.suppress(Exception):
128
+ webbrowser.open(url)
129
+
130
+
131
+ class _ConsoleInteraction:
132
+ """Login interaction on stdin/stdout."""
133
+
134
+ def __init__(self, lines: _LineReader, *, open_browser: Callable[[str], None] = _open_browser) -> None:
135
+ self.signal: AbortSignal | None = AbortController().signal
136
+ self._lines = lines
137
+ self._open_browser = open_browser
138
+
139
+ async def _ask(self, question: str, *, secret: bool = False, signal: Any = None) -> str:
140
+ if secret:
141
+ async with abort_scope(signal):
142
+ return await _read_secret(question)
143
+ print(question, end="", flush=True)
144
+ try:
145
+ async with abort_scope(signal):
146
+ return await self._lines.readline()
147
+ except (AbortError, asyncio.CancelledError):
148
+ # The prompt was dismissed (for example the browser callback finished the login).
149
+ print()
150
+ raise
151
+
152
+ async def prompt(self, prompt: AuthPrompt) -> str:
153
+ if prompt.type == "select":
154
+ options = prompt.options or []
155
+ print(f"\n{prompt.message}")
156
+ for index, option in enumerate(options):
157
+ print(f" {index + 1}. {option.label}")
158
+ answer = await self._ask(f"Enter number (1-{len(options)}): ", signal=prompt.signal)
159
+ choice = _parse_int(answer)
160
+ if choice is None or not 1 <= choice <= len(options):
161
+ raise ValueError("Invalid selection")
162
+ return options[choice - 1].id
163
+ suffix = f" ({prompt.placeholder})" if prompt.placeholder else ""
164
+ return await self._ask(f"{prompt.message}{suffix}: ", secret=prompt.type == "secret", signal=prompt.signal)
165
+
166
+ def notify(self, event: AuthEvent) -> None:
167
+ if event.type == "auth_url":
168
+ print(f"\nOpen this URL in your browser:\n{event.url}")
169
+ if event.instructions:
170
+ print(event.instructions)
171
+ if event.url:
172
+ self._open_browser(event.url)
173
+ elif event.type == "device_code":
174
+ print(f"\nOpen this URL in your browser:\n{event.verification_uri}")
175
+ print(f"Enter code: {event.user_code}")
176
+ if event.verification_uri:
177
+ self._open_browser(event.verification_uri)
178
+ elif event.message:
179
+ print(event.message)
180
+
181
+
182
+ def _oauth_providers(models: Models) -> list[Provider]:
183
+ return [provider for provider in models.get_providers() if provider.auth.oauth is not None]
184
+
185
+
186
+ def _usage(providers: Sequence[Provider]) -> str:
187
+ provider_list = "\n".join(f" {provider.id:<20} {provider.name}" for provider in providers)
188
+ return (
189
+ "Usage: pi-ai <command> [provider]\n\n"
190
+ "Commands:\n"
191
+ " login [provider] Login to an OAuth provider\n"
192
+ " list List available providers\n\n"
193
+ f"Providers:\n{provider_list}"
194
+ )
195
+
196
+
197
+ async def _login(models: Models, provider_id: str, interaction: _ConsoleInteraction) -> None:
198
+ # This CLI does not persist an installation ID; apps should reuse one across logins.
199
+ await models.login(provider_id, "oauth", interaction, LoginOptions(get_device_id=lambda: str(uuid.uuid4())))
200
+ print(f"\nCredentials saved to {AUTH_FILE}")
201
+
202
+
203
+ async def run(
204
+ argv: Sequence[str],
205
+ *,
206
+ models: Models | None = None,
207
+ lines: _LineReader | None = None,
208
+ open_browser: Callable[[str], None] = _open_browser,
209
+ ) -> None:
210
+ """Run the CLI with ``argv`` (without the program name). Raises on errors."""
211
+ if models is None:
212
+ from .providers.all import builtin_models
213
+
214
+ models = builtin_models(credentials=FileCredentialStore(AUTH_FILE))
215
+ providers = _oauth_providers(models)
216
+ reader = lines if lines is not None else _LineReader()
217
+
218
+ command = argv[0] if argv else None
219
+ if not command or command in ("help", "--help", "-h"):
220
+ print(_usage(providers))
221
+ return
222
+ if command == "list":
223
+ for provider in providers:
224
+ print(f"{provider.id:<20} {provider.name}")
225
+ return
226
+ if command == "login":
227
+ interaction = _ConsoleInteraction(reader, open_browser=open_browser)
228
+ provider_id = argv[1] if len(argv) > 1 else None
229
+ if not provider_id:
230
+ for index, provider in enumerate(providers):
231
+ print(f" {index + 1}. {provider.name}")
232
+ print(f"Enter number (1-{len(providers)}): ", end="", flush=True)
233
+ choice = _parse_int(await reader.readline())
234
+ provider_id = providers[choice - 1].id if choice is not None and 1 <= choice <= len(providers) else None
235
+ if not provider_id or not any(provider.id == provider_id for provider in providers):
236
+ raise ValueError(f"Unknown provider: {provider_id or ''}")
237
+ await _login(models, provider_id, interaction)
238
+ return
239
+ raise ValueError(f"Unknown command: {command}")
240
+
241
+
242
+ async def _main(argv: Sequence[str]) -> None:
243
+ from .utils.http import aclose_http_clients
244
+
245
+ try:
246
+ await run(argv)
247
+ finally:
248
+ await aclose_http_clients()
249
+
250
+
251
+ def main(argv: Sequence[str] | None = None) -> None:
252
+ """Entry point of the ``pi-ai`` script."""
253
+ args = list(sys.argv[1:] if argv is None else argv)
254
+ try:
255
+ asyncio.run(_main(args))
256
+ except KeyboardInterrupt:
257
+ print("\nLogin cancelled", file=sys.stderr)
258
+ sys.exit(130)
259
+ except Exception as error:
260
+ print(f"Error: {error}", file=sys.stderr)
261
+ sys.exit(1)
262
+
263
+
264
+ if __name__ == "__main__":
265
+ main()