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.
- pi_agent/__init__.py +127 -0
- pi_agent/agent.py +617 -0
- pi_agent/agent_loop.py +820 -0
- pi_agent/proxy.py +269 -0
- pi_agent/py.typed +0 -0
- pi_agent/stream_fn.py +27 -0
- pi_agent/tools.py +327 -0
- pi_agent/types.py +394 -0
- pi_ai/__init__.py +263 -0
- pi_ai/abort.py +315 -0
- pi_ai/api/__init__.py +1 -0
- pi_ai/api/anthropic_messages.py +1782 -0
- pi_ai/api/azure_openai_config.py +138 -0
- pi_ai/api/azure_openai_responses.py +245 -0
- pi_ai/api/bedrock_converse_stream.py +1598 -0
- pi_ai/api/classifier_shared.py +211 -0
- pi_ai/api/cloudflare.py +36 -0
- pi_ai/api/cloudflare_workers_ai_system_one.py +80 -0
- pi_ai/api/constrained_sampling.py +279 -0
- pi_ai/api/github_copilot_headers.py +30 -0
- pi_ai/api/google_generative_ai.py +138 -0
- pi_ai/api/google_shared.py +927 -0
- pi_ai/api/google_vertex.py +234 -0
- pi_ai/api/lazy.py +156 -0
- pi_ai/api/llama_cpp_classify.py +449 -0
- pi_ai/api/mistral_conversations.py +756 -0
- pi_ai/api/openai_codex_responses.py +1545 -0
- pi_ai/api/openai_completions.py +1594 -0
- pi_ai/api/openai_decisions.py +210 -0
- pi_ai/api/openai_prompt_cache.py +19 -0
- pi_ai/api/openai_responses.py +418 -0
- pi_ai/api/openai_responses_shared.py +840 -0
- pi_ai/api/openai_sdk.py +204 -0
- pi_ai/api/openrouter_images.py +153 -0
- pi_ai/api/pi_messages.py +449 -0
- pi_ai/api/registry.py +103 -0
- pi_ai/api/simple_options.py +119 -0
- pi_ai/api/system_one_shared.py +148 -0
- pi_ai/api/transform_messages.py +211 -0
- pi_ai/api/typesafe_system_one.py +43 -0
- pi_ai/auth/__init__.py +1 -0
- pi_ai/auth/context.py +47 -0
- pi_ai/auth/credential_store.py +208 -0
- pi_ai/auth/helpers.py +123 -0
- pi_ai/auth/oauth/__init__.py +5 -0
- pi_ai/auth/oauth/_common.py +454 -0
- pi_ai/auth/oauth/anthropic.py +283 -0
- pi_ai/auth/oauth/callback_server.py +339 -0
- pi_ai/auth/oauth/device_code.py +127 -0
- pi_ai/auth/oauth/github_copilot.py +508 -0
- pi_ai/auth/oauth/kimi_coding.py +283 -0
- pi_ai/auth/oauth/load.py +64 -0
- pi_ai/auth/oauth/meta.py +238 -0
- pi_ai/auth/oauth/openai_chatgpt.py +321 -0
- pi_ai/auth/oauth/openai_codex.py +391 -0
- pi_ai/auth/oauth/openrouter.py +173 -0
- pi_ai/auth/oauth/pkce.py +23 -0
- pi_ai/auth/oauth/radius.py +311 -0
- pi_ai/auth/oauth/xai.py +220 -0
- pi_ai/auth/resolve.py +190 -0
- pi_ai/auth/types.py +292 -0
- pi_ai/cli.py +265 -0
- pi_ai/compat.py +651 -0
- pi_ai/env_api_keys.py +137 -0
- pi_ai/event_stream.py +329 -0
- pi_ai/models.py +1191 -0
- pi_ai/models_store.py +121 -0
- pi_ai/oauth.py +90 -0
- pi_ai/providers/__init__.py +1 -0
- pi_ai/providers/all.py +154 -0
- pi_ai/providers/amazon_bedrock.py +116 -0
- pi_ai/providers/ant_ling.py +23 -0
- pi_ai/providers/anthropic.py +105 -0
- pi_ai/providers/azure.py +103 -0
- pi_ai/providers/baseten.py +23 -0
- pi_ai/providers/catalog.py +167 -0
- pi_ai/providers/cerebras.py +23 -0
- pi_ai/providers/cloudflare_ai_gateway.py +30 -0
- pi_ai/providers/cloudflare_auth.py +112 -0
- pi_ai/providers/cloudflare_stream.py +84 -0
- pi_ai/providers/cloudflare_workers_ai.py +26 -0
- pi_ai/providers/data/amazon-bedrock.json +1 -0
- pi_ai/providers/data/ant-ling.json +1 -0
- pi_ai/providers/data/anthropic.json +1 -0
- pi_ai/providers/data/azure.json +1 -0
- pi_ai/providers/data/baseten.json +1 -0
- pi_ai/providers/data/cerebras.json +1 -0
- pi_ai/providers/data/cloudflare-ai-gateway.json +1 -0
- pi_ai/providers/data/cloudflare-workers-ai.json +1 -0
- pi_ai/providers/data/deepseek.json +1 -0
- pi_ai/providers/data/fireworks.json +1 -0
- pi_ai/providers/data/github-copilot.json +1 -0
- pi_ai/providers/data/google-vertex.json +1 -0
- pi_ai/providers/data/google.json +1 -0
- pi_ai/providers/data/groq.json +1 -0
- pi_ai/providers/data/huggingface.json +1 -0
- pi_ai/providers/data/kimi-coding.json +1 -0
- pi_ai/providers/data/manifest.json +1 -0
- pi_ai/providers/data/meta.json +1 -0
- pi_ai/providers/data/minimax-cn.json +1 -0
- pi_ai/providers/data/minimax.json +1 -0
- pi_ai/providers/data/mistral.json +1 -0
- pi_ai/providers/data/moonshotai-cn.json +1 -0
- pi_ai/providers/data/moonshotai.json +1 -0
- pi_ai/providers/data/nvidia.json +1 -0
- pi_ai/providers/data/openai-codex.json +1 -0
- pi_ai/providers/data/openai.json +1 -0
- pi_ai/providers/data/opencode-go.json +1 -0
- pi_ai/providers/data/opencode.json +1 -0
- pi_ai/providers/data/openrouter.json +1 -0
- pi_ai/providers/data/qwen-token-plan-cn.json +1 -0
- pi_ai/providers/data/qwen-token-plan-individual.json +1 -0
- pi_ai/providers/data/qwen-token-plan.json +1 -0
- pi_ai/providers/data/radius.json +1 -0
- pi_ai/providers/data/together.json +1 -0
- pi_ai/providers/data/typesafe.json +1 -0
- pi_ai/providers/data/vercel-ai-gateway.json +1 -0
- pi_ai/providers/data/xai.json +1 -0
- pi_ai/providers/data/xiaomi-token-plan-ams.json +1 -0
- pi_ai/providers/data/xiaomi-token-plan-cn.json +1 -0
- pi_ai/providers/data/xiaomi-token-plan-sgp.json +1 -0
- pi_ai/providers/data/xiaomi.json +1 -0
- pi_ai/providers/data/zai-coding-cn.json +1 -0
- pi_ai/providers/data/zai.json +1 -0
- pi_ai/providers/deepseek.py +23 -0
- pi_ai/providers/faux.py +680 -0
- pi_ai/providers/fireworks.py +26 -0
- pi_ai/providers/github_copilot.py +46 -0
- pi_ai/providers/google.py +23 -0
- pi_ai/providers/google_vertex.py +131 -0
- pi_ai/providers/groq.py +23 -0
- pi_ai/providers/huggingface.py +23 -0
- pi_ai/providers/kimi_coding.py +32 -0
- pi_ai/providers/meta.py +32 -0
- pi_ai/providers/minimax.py +23 -0
- pi_ai/providers/minimax_cn.py +23 -0
- pi_ai/providers/mistral.py +23 -0
- pi_ai/providers/moonshotai.py +23 -0
- pi_ai/providers/moonshotai_cn.py +23 -0
- pi_ai/providers/nvidia.py +23 -0
- pi_ai/providers/openai.py +44 -0
- pi_ai/providers/openai_codex.py +26 -0
- pi_ai/providers/opencode.py +36 -0
- pi_ai/providers/opencode_go.py +33 -0
- pi_ai/providers/opencode_headers.py +60 -0
- pi_ai/providers/openrouter.py +145 -0
- pi_ai/providers/openrouter_api.py +122 -0
- pi_ai/providers/qwen_token_plan.py +23 -0
- pi_ai/providers/qwen_token_plan_cn.py +23 -0
- pi_ai/providers/qwen_token_plan_individual.py +23 -0
- pi_ai/providers/radius.py +146 -0
- pi_ai/providers/radius_config.py +136 -0
- pi_ai/providers/together.py +23 -0
- pi_ai/providers/typesafe.py +22 -0
- pi_ai/providers/vercel_ai_gateway.py +25 -0
- pi_ai/providers/xai.py +32 -0
- pi_ai/providers/xiaomi.py +23 -0
- pi_ai/providers/xiaomi_token_plan_ams.py +23 -0
- pi_ai/providers/xiaomi_token_plan_cn.py +23 -0
- pi_ai/providers/xiaomi_token_plan_sgp.py +23 -0
- pi_ai/providers/zai.py +23 -0
- pi_ai/providers/zai_coding_cn.py +23 -0
- pi_ai/py.typed +0 -0
- pi_ai/serialization.py +477 -0
- pi_ai/session_resources.py +33 -0
- pi_ai/types.py +1003 -0
- pi_ai/utils/__init__.py +1 -0
- pi_ai/utils/callbacks.py +15 -0
- pi_ai/utils/diagnostics.py +70 -0
- pi_ai/utils/error_body.py +89 -0
- pi_ai/utils/estimate.py +125 -0
- pi_ai/utils/fastjson.py +63 -0
- pi_ai/utils/http.py +409 -0
- pi_ai/utils/json_parse.py +424 -0
- pi_ai/utils/misc.py +193 -0
- pi_ai/utils/model_operations.py +73 -0
- pi_ai/utils/models_error.py +35 -0
- pi_ai/utils/oauth_page.py +121 -0
- pi_ai/utils/overflow.py +87 -0
- pi_ai/utils/retry.py +193 -0
- pi_ai/utils/schema.py +283 -0
- pi_ai/utils/text.py +65 -0
- pi_ai/utils/transcript.py +271 -0
- pi_ai/utils/validation.py +813 -0
- pi_ai_python-1.3.0.dist-info/METADATA +167 -0
- pi_ai_python-1.3.0.dist-info/RECORD +189 -0
- pi_ai_python-1.3.0.dist-info/WHEEL +4 -0
- pi_ai_python-1.3.0.dist-info/entry_points.txt +2 -0
- 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
|