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/auth/resolve.py ADDED
@@ -0,0 +1,190 @@
1
+ """Auth resolution shared by every operation of a ``Models`` collection.
2
+
3
+ A stored credential owns its provider: ambient sources (env vars, profiles) are
4
+ consulted only when nothing is stored, and a failed OAuth refresh never silently
5
+ falls back to an env key.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import time
12
+ from collections.abc import Callable
13
+ from dataclasses import dataclass
14
+ from typing import Any
15
+
16
+ from ..abort import AbortController, AbortError, AbortSignal, operation_signal, race_with_abort_signal
17
+ from ..utils.models_error import ModelsError
18
+ from .context import overlay_env_auth_context
19
+ from .types import ApiKeyAuth, AuthContext, AuthResult, Credential, CredentialStore, OAuthAuth, ProviderAuth
20
+
21
+ __all__ = [
22
+ "AuthResolutionOverrides",
23
+ "resolve_provider_auth",
24
+ "refresh_stored_oauth_credential",
25
+ "DEFAULT_OAUTH_MINIMUM_VALIDITY_MS",
26
+ "DEFAULT_OAUTH_REFRESH_TIMEOUT_MS",
27
+ ]
28
+
29
+ DEFAULT_OAUTH_MINIMUM_VALIDITY_MS = 5 * 60 * 1000
30
+ DEFAULT_OAUTH_REFRESH_TIMEOUT_MS = 15_000
31
+
32
+
33
+ @dataclass(slots=True)
34
+ class AuthResolutionOverrides:
35
+ api_key: str | None = None
36
+ env: dict[str, str] | None = None
37
+ #: Require this much remaining OAuth-token validity; defaults to five minutes.
38
+ min_oauth_validity_ms: int | None = None
39
+ signal: AbortSignal | None = None
40
+
41
+
42
+ def _now_ms() -> int:
43
+ return time.time_ns() // 1_000_000
44
+
45
+
46
+ async def resolve_provider_auth(
47
+ provider: Any,
48
+ credentials: CredentialStore,
49
+ auth_context: AuthContext,
50
+ overrides: AuthResolutionOverrides | None = None,
51
+ ) -> AuthResult | None:
52
+ signal = operation_signal(overrides.signal if overrides else None)
53
+ return await race_with_abort_signal(
54
+ _resolve(provider, credentials, auth_context, overrides or AuthResolutionOverrides(), signal), signal
55
+ )
56
+
57
+
58
+ async def _resolve(
59
+ provider: Any,
60
+ credentials: CredentialStore,
61
+ auth_context: AuthContext,
62
+ overrides: AuthResolutionOverrides,
63
+ signal: AbortSignal,
64
+ ) -> AuthResult | None:
65
+ signal.throw_if_aborted()
66
+ auth: ProviderAuth = provider.auth
67
+ request_context = overlay_env_auth_context(auth_context, overrides.env) if overrides.env else auth_context
68
+
69
+ if overrides.api_key is not None and auth.api_key is not None:
70
+ credential: Credential = {"type": "api_key", "key": overrides.api_key}
71
+ if overrides.env:
72
+ credential["env"] = dict(overrides.env)
73
+ return await _resolve_api_key(request_context, auth.api_key, provider.id, credential, signal)
74
+
75
+ stored = await _read_credential(credentials, provider.id, signal)
76
+ if stored:
77
+ if stored.get("type") == "oauth" and auth.oauth is not None:
78
+ return await _resolve_stored_oauth(
79
+ credentials, provider.id, auth.oauth, stored, signal, overrides.min_oauth_validity_ms
80
+ )
81
+ if stored.get("type") == "api_key" and auth.api_key is not None:
82
+ if overrides.env:
83
+ stored = {**stored, "env": {**(stored.get("env") or {}), **overrides.env}}
84
+ return await _resolve_api_key(request_context, auth.api_key, provider.id, stored, signal)
85
+ return None
86
+
87
+ if auth.api_key is not None:
88
+ return await _resolve_api_key(request_context, auth.api_key, provider.id, None, signal)
89
+ return None
90
+
91
+
92
+ async def refresh_stored_oauth_credential(
93
+ credentials: CredentialStore,
94
+ provider_id: str,
95
+ oauth: OAuthAuth,
96
+ needs_refresh: Callable[[Credential], bool],
97
+ signal: AbortSignal,
98
+ ) -> Credential | None:
99
+ """Refresh a stored OAuth credential under the credential-store lock and persist it.
100
+
101
+ ``needs_refresh`` is re-checked under the lock, so concurrent callers refresh once.
102
+ ``signal`` cancels only the wait for the lock: once a refresh starts the provider
103
+ may already have rotated the refresh token, so the refresh and its persistence
104
+ are bounded only by a timeout.
105
+ """
106
+ lock_wait = AbortController()
107
+ remove = signal.add_listener(lambda: lock_wait.abort(signal.reason))
108
+
109
+ async def refresh(current: Credential | None) -> Credential | None:
110
+ remove()
111
+ signal.throw_if_aborted()
112
+ if not current or current.get("type") != "oauth":
113
+ return None # logged out meanwhile
114
+ if not needs_refresh(current):
115
+ return None # another request/process refreshed
116
+ refresh_signal = AbortSignal.timeout(DEFAULT_OAUTH_REFRESH_TIMEOUT_MS / 1000)
117
+ try:
118
+ refreshed = await asyncio.wait_for(
119
+ oauth.refresh(current, refresh_signal), DEFAULT_OAUTH_REFRESH_TIMEOUT_MS / 1000 + 1
120
+ )
121
+ except Exception as error:
122
+ raise ModelsError("oauth", f"OAuth refresh failed for {provider_id}", cause=error) from error
123
+ result = dict(refreshed)
124
+ result.setdefault("type", "oauth")
125
+ return result
126
+
127
+ try:
128
+ post = await credentials.modify(provider_id, refresh, signal=lock_wait.signal)
129
+ except ModelsError:
130
+ raise
131
+ except AbortError:
132
+ signal.throw_if_aborted()
133
+ raise
134
+ except Exception as error:
135
+ signal.throw_if_aborted()
136
+ raise ModelsError("auth", f"Credential store modify failed for {provider_id}", cause=error) from error
137
+ finally:
138
+ remove()
139
+ return post if post and post.get("type") == "oauth" else None
140
+
141
+
142
+ async def _resolve_stored_oauth(
143
+ credentials: CredentialStore,
144
+ provider_id: str,
145
+ oauth: OAuthAuth,
146
+ stored: Credential,
147
+ signal: AbortSignal,
148
+ min_oauth_validity_ms: int | None,
149
+ ) -> AuthResult | None:
150
+ minimum = max(DEFAULT_OAUTH_MINIMUM_VALIDITY_MS, min_oauth_validity_ms or 0)
151
+
152
+ def expires_soon(credential: Credential) -> bool:
153
+ return _now_ms() + minimum >= int(credential.get("expires") or 0)
154
+
155
+ credential = stored
156
+ if expires_soon(credential):
157
+ post = await refresh_stored_oauth_credential(credentials, provider_id, oauth, expires_soon, signal)
158
+ if post is None:
159
+ return None
160
+ credential = post
161
+ if min_oauth_validity_ms is not None and expires_soon(credential):
162
+ raise ModelsError("oauth", f"OAuth refresh returned a token that expires too soon for {provider_id}")
163
+ try:
164
+ return AuthResult(await oauth.to_auth(credential), source="OAuth")
165
+ except Exception as error:
166
+ raise ModelsError("oauth", f"OAuth auth derivation failed for {provider_id}", cause=error) from error
167
+
168
+
169
+ async def _resolve_api_key(
170
+ auth_context: AuthContext,
171
+ api_key: ApiKeyAuth,
172
+ provider_id: str,
173
+ credential: Credential | None,
174
+ signal: AbortSignal,
175
+ ) -> AuthResult | None:
176
+ try:
177
+ return await api_key.resolve(ctx=auth_context, credential=credential, signal=signal)
178
+ except AbortError:
179
+ raise
180
+ except Exception as error:
181
+ raise ModelsError("auth", f"API key auth failed for provider {provider_id}", cause=error) from error
182
+
183
+
184
+ async def _read_credential(credentials: CredentialStore, provider_id: str, signal: AbortSignal) -> Credential | None:
185
+ try:
186
+ return await credentials.read(provider_id, signal=signal)
187
+ except AbortError:
188
+ raise
189
+ except Exception as error:
190
+ raise ModelsError("auth", f"Credential store read failed for {provider_id}", cause=error) from error
pi_ai/auth/types.py ADDED
@@ -0,0 +1,292 @@
1
+ """Auth types: credentials, auth methods, login interaction protocol.
2
+
3
+ Credentials are plain dicts in exactly the shape of pi's ``auth.json``::
4
+
5
+ {"type": "api_key", "key": "sk-...", "env": {"CLOUDFLARE_ACCOUNT_ID": "..."}}
6
+ {"type": "oauth", "access": "...", "refresh": "...", "expires": 1767225600000, ...}
7
+
8
+ so credential files are shared with the TypeScript tools.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from collections.abc import Awaitable, Callable, Sequence
14
+ from dataclasses import dataclass
15
+ from typing import TYPE_CHECKING, Any, Literal, Protocol, TypeAlias, TypedDict
16
+
17
+ if TYPE_CHECKING:
18
+ from ..abort import AbortSignal
19
+
20
+ __all__ = [
21
+ "ModelAuth",
22
+ "ApiKeyCredential",
23
+ "OAuthCredential",
24
+ "OAuthCredentials",
25
+ "Credential",
26
+ "CredentialInfo",
27
+ "CredentialStore",
28
+ "AuthContext",
29
+ "AuthResult",
30
+ "AuthCheck",
31
+ "AuthType",
32
+ "AuthSelectOption",
33
+ "AuthPrompt",
34
+ "AuthInfoLink",
35
+ "AuthEvent",
36
+ "AuthInteraction",
37
+ "ProviderAuthInteraction",
38
+ "ApiKeyAuth",
39
+ "OAuthAuth",
40
+ "LoginOptions",
41
+ "ProviderAuth",
42
+ ]
43
+
44
+ AuthType: TypeAlias = Literal["api_key", "oauth"]
45
+
46
+
47
+ @dataclass(slots=True)
48
+ class ModelAuth:
49
+ """Request auth for a single model request: an API key, headers, and/or a base URL."""
50
+
51
+ api_key: str | None = None
52
+ headers: dict[str, str | None] | None = None
53
+ base_url: str | None = None
54
+
55
+
56
+ class ApiKeyCredential(TypedDict, total=False):
57
+ type: Literal["api_key"]
58
+ key: str
59
+ #: Provider-scoped environment/config values such as Cloudflare account ids.
60
+ env: dict[str, str]
61
+
62
+
63
+ class OAuthCredentials(TypedDict, total=False):
64
+ refresh: str
65
+ access: str
66
+ #: Expiry as a Unix timestamp in milliseconds.
67
+ expires: int
68
+
69
+
70
+ class OAuthCredential(OAuthCredentials, total=False):
71
+ type: Literal["oauth"]
72
+
73
+
74
+ Credential: TypeAlias = dict[str, Any]
75
+
76
+
77
+ @dataclass(slots=True)
78
+ class CredentialInfo:
79
+ """Non-secret credential metadata for account/status enumeration."""
80
+
81
+ provider_id: str
82
+ type: AuthType
83
+
84
+
85
+ class CredentialStore(Protocol):
86
+ """App-owned credential storage keyed by provider id, one credential per provider.
87
+
88
+ ``modify`` is the only write path: a serialized read-modify-write, so OAuth refresh
89
+ under the lock cannot double-refresh a rotated token. Methods raise only on storage
90
+ failure; ``read`` returns ``None`` for missing entries.
91
+ """
92
+
93
+ async def read(self, provider_id: str, *, signal: AbortSignal | None = None) -> Credential | None: ...
94
+
95
+ async def list(self, *, signal: AbortSignal | None = None) -> Sequence[CredentialInfo]: ...
96
+
97
+ async def modify(
98
+ self,
99
+ provider_id: str,
100
+ fn: Callable[[Credential | None], Awaitable[Credential | None]],
101
+ *,
102
+ signal: AbortSignal | None = None,
103
+ ) -> Credential | None: ...
104
+
105
+ async def delete(self, provider_id: str, *, signal: AbortSignal | None = None) -> None: ...
106
+
107
+
108
+ class AuthContext(Protocol):
109
+ """Environment access for auth resolution. Injectable for tests and sandboxes."""
110
+
111
+ async def env(self, name: str) -> str | None: ...
112
+
113
+ async def file_exists(self, path: str) -> bool: ...
114
+
115
+
116
+ @dataclass(slots=True)
117
+ class AuthResult:
118
+ auth: ModelAuth
119
+ #: Provider-scoped environment/config values resolved from credentials and ambient context.
120
+ env: dict[str, str] | None = None
121
+ #: Human-readable source for status UI: ``"ANTHROPIC_API_KEY"``, ``"OAuth"``, ``"~/.aws/credentials"``.
122
+ source: str | None = None
123
+
124
+
125
+ @dataclass(slots=True)
126
+ class AuthCheck:
127
+ type: AuthType
128
+ source: str | None = None
129
+
130
+
131
+ @dataclass(slots=True)
132
+ class AuthSelectOption:
133
+ id: str
134
+ label: str
135
+ description: str | None = None
136
+
137
+
138
+ @dataclass(slots=True)
139
+ class AuthPrompt:
140
+ """A prompt shown during login. ``signal`` aborts when an out-of-band event resolves the step."""
141
+
142
+ type: Literal["text", "secret", "select", "manual_code"]
143
+ message: str
144
+ placeholder: str | None = None
145
+ options: list[AuthSelectOption] | None = None
146
+ signal: AbortSignal | None = None
147
+
148
+
149
+ @dataclass(slots=True)
150
+ class AuthInfoLink:
151
+ url: str
152
+ label: str | None = None
153
+
154
+
155
+ @dataclass(slots=True)
156
+ class AuthEvent:
157
+ """Login notification: ``info``, ``auth_url``, ``device_code`` or ``progress``."""
158
+
159
+ type: Literal["info", "auth_url", "device_code", "progress"]
160
+ message: str | None = None
161
+ links: list[AuthInfoLink] | None = None
162
+ url: str | None = None
163
+ instructions: str | None = None
164
+ user_code: str | None = None
165
+ verification_uri: str | None = None
166
+ interval_seconds: float | None = None
167
+ expires_in_seconds: float | None = None
168
+
169
+
170
+ class AuthInteraction(Protocol):
171
+ """Login callbacks for API-key and OAuth flows.
172
+
173
+ ``prompt()`` returns the entered string (``select`` returns the option id) and
174
+ raises on cancel. ``signal`` aborts the whole login flow.
175
+ """
176
+
177
+ signal: AbortSignal | None
178
+
179
+ async def prompt(self, prompt: AuthPrompt) -> str: ...
180
+
181
+ def notify(self, event: AuthEvent) -> None: ...
182
+
183
+
184
+ class ProviderAuthInteraction:
185
+ """Normalized interaction passed to provider login implementations (signal always present)."""
186
+
187
+ __slots__ = ("_inner", "signal")
188
+
189
+ def __init__(self, inner: AuthInteraction, signal: AbortSignal) -> None:
190
+ self._inner = inner
191
+ self.signal = signal
192
+
193
+ async def prompt(self, prompt: AuthPrompt) -> str:
194
+ return await self._inner.prompt(prompt)
195
+
196
+ def notify(self, event: AuthEvent) -> None:
197
+ self._inner.notify(event)
198
+
199
+
200
+ @dataclass(slots=True)
201
+ class LoginOptions:
202
+ #: Stable id of this app installation (e.g. OpenAI's agent host id).
203
+ get_device_id: Callable[[], str] | None = None
204
+ #: Name this app introduces itself with during login. Defaults to pi's own name.
205
+ agent_name: str | None = None
206
+
207
+
208
+ ResolveFn: TypeAlias = Callable[..., Awaitable["AuthResult | None"]]
209
+ CheckFn: TypeAlias = Callable[..., Awaitable["AuthCheck | None"]]
210
+ LoginFn: TypeAlias = Callable[..., Awaitable[Credential]]
211
+
212
+
213
+ class ApiKeyAuth:
214
+ """API-key auth: stored key/provider env plus ambient sources (env vars, profiles, files).
215
+
216
+ Either pass callables or subclass and override :meth:`resolve` (and optionally
217
+ ``login``/``check``)::
218
+
219
+ ApiKeyAuth("llama.cpp", resolve=lambda **_: AuthResult(ModelAuth()))
220
+
221
+ ``resolve`` receives keyword arguments ``ctx``, ``credential`` and ``signal`` and
222
+ returns ``None`` when the provider is not configured.
223
+ """
224
+
225
+ #: Interactive setup returning an ``ApiKeyCredential``. ``None`` means ambient-only.
226
+ login: Callable[[ProviderAuthInteraction], Awaitable[Credential]] | None = None
227
+ #: Optional side-effect-free availability check (use when ``resolve`` runs commands).
228
+ check: CheckFn | None = None
229
+
230
+ def __init__(
231
+ self,
232
+ name: str,
233
+ resolve: ResolveFn | None = None,
234
+ *,
235
+ login: Callable[[ProviderAuthInteraction], Awaitable[Credential]] | None = None,
236
+ check: CheckFn | None = None,
237
+ ) -> None:
238
+ self.name = name
239
+ if resolve is not None:
240
+ self._resolve_fn: ResolveFn | None = resolve
241
+ else:
242
+ self._resolve_fn = None
243
+ if login is not None:
244
+ self.login = login
245
+ if check is not None:
246
+ self.check = check
247
+
248
+ async def resolve(
249
+ self, *, ctx: AuthContext, credential: Credential | None, signal: AbortSignal
250
+ ) -> AuthResult | None:
251
+ if self._resolve_fn is None:
252
+ raise NotImplementedError(f"{type(self).__name__}.resolve() is not implemented")
253
+ from ..utils.callbacks import maybe_await
254
+
255
+ return await maybe_await(self._resolve_fn(ctx=ctx, credential=credential, signal=signal))
256
+
257
+ def __repr__(self) -> str:
258
+ return f"ApiKeyAuth(name={self.name!r})"
259
+
260
+
261
+ class OAuthAuth:
262
+ """OAuth auth. ``refresh`` produces a credential and ``to_auth`` derives request auth from it.
263
+
264
+ ``Models`` runs ``refresh`` under the credential-store lock, so concurrent
265
+ requests and processes refresh only once.
266
+ """
267
+
268
+ name: str = "OAuth"
269
+ #: Whether access through this method is backed by a provider subscription.
270
+ is_subscription: bool = False
271
+ #: Selector label for the login option, e.g. "Sign in with ChatGPT".
272
+ login_label: str | None = None
273
+
274
+ async def login(self, interaction: ProviderAuthInteraction, options: LoginOptions | None = None) -> Credential:
275
+ raise NotImplementedError
276
+
277
+ async def refresh(self, credential: Credential, signal: AbortSignal) -> Credential:
278
+ raise NotImplementedError
279
+
280
+ async def to_auth(self, credential: Credential) -> ModelAuth:
281
+ raise NotImplementedError
282
+
283
+ def __repr__(self) -> str:
284
+ return f"{type(self).__name__}(name={self.name!r})"
285
+
286
+
287
+ @dataclass(slots=True)
288
+ class ProviderAuth:
289
+ """Provider auth. At least one of ``api_key``/``oauth`` must be present."""
290
+
291
+ api_key: ApiKeyAuth | None = None
292
+ oauth: OAuthAuth | None = None