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