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,138 @@
|
|
|
1
|
+
"""Azure OpenAI endpoint resolution (base URL, API version, deployment names)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from typing import Any, NamedTuple
|
|
8
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
9
|
+
|
|
10
|
+
from ..types import StreamOptions
|
|
11
|
+
from ..utils.misc import get_provider_env_value
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"DEFAULT_AZURE_API_VERSION",
|
|
15
|
+
"AzureEndpointOptions",
|
|
16
|
+
"AzureConfig",
|
|
17
|
+
"resolve_deployment_name",
|
|
18
|
+
"resolve_azure_base_url",
|
|
19
|
+
"resolve_azure_config",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
DEFAULT_AZURE_API_VERSION = "v1"
|
|
23
|
+
|
|
24
|
+
_TRAILING_SLASHES = re.compile(r"/+$")
|
|
25
|
+
_URL_SCHEME = re.compile(r"^[a-zA-Z][a-zA-Z0-9+.-]*$")
|
|
26
|
+
_SPECIAL_SCHEMES = frozenset({"http", "https", "ws", "wss", "ftp", "file"})
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AzureEndpointOptions(StreamOptions, total=False):
|
|
30
|
+
"""Azure models ship without a base URL: one resource per user, resolved per request."""
|
|
31
|
+
|
|
32
|
+
azure_api_version: str
|
|
33
|
+
azure_resource_name: str
|
|
34
|
+
azure_base_url: str
|
|
35
|
+
azure_deployment_name: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AzureConfig(NamedTuple):
|
|
39
|
+
base_url: str
|
|
40
|
+
api_version: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _parse_deployment_name_map(value: str | None) -> dict[str, str]:
|
|
44
|
+
mapping: dict[str, str] = {}
|
|
45
|
+
if not value:
|
|
46
|
+
return mapping
|
|
47
|
+
for entry in value.split(","):
|
|
48
|
+
trimmed = entry.strip()
|
|
49
|
+
if not trimmed:
|
|
50
|
+
continue
|
|
51
|
+
model_id, _, deployment_name = trimmed.partition("=")
|
|
52
|
+
# JavaScript's split("=", 2) keeps only the text up to a second "=".
|
|
53
|
+
deployment_name = deployment_name.split("=", 1)[0]
|
|
54
|
+
if not model_id or not deployment_name:
|
|
55
|
+
continue
|
|
56
|
+
mapping[model_id.strip()] = deployment_name.strip()
|
|
57
|
+
return mapping
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def resolve_deployment_name(model: Any, options: Mapping[str, Any] | None = None) -> str:
|
|
61
|
+
"""Deployment for ``model.id``: ``azure_deployment_name``, ``AZURE_OPENAI_DEPLOYMENT_NAME_MAP``, or the id."""
|
|
62
|
+
options = options or {}
|
|
63
|
+
explicit = options.get("azure_deployment_name")
|
|
64
|
+
if explicit:
|
|
65
|
+
return explicit
|
|
66
|
+
mapped = _parse_deployment_name_map(
|
|
67
|
+
get_provider_env_value("AZURE_OPENAI_DEPLOYMENT_NAME_MAP", options.get("env"))
|
|
68
|
+
).get(model.id)
|
|
69
|
+
return mapped or model.id
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _normalize_azure_base_url(base_url: str) -> str:
|
|
73
|
+
trimmed = _TRAILING_SLASHES.sub("", base_url.strip())
|
|
74
|
+
try:
|
|
75
|
+
parts = urlsplit(trimmed)
|
|
76
|
+
except ValueError as error:
|
|
77
|
+
raise ValueError(f"Invalid Azure OpenAI base URL: {base_url}") from error
|
|
78
|
+
scheme = parts.scheme.lower()
|
|
79
|
+
if not scheme or not _URL_SCHEME.match(scheme) or (scheme in _SPECIAL_SCHEMES and not parts.netloc):
|
|
80
|
+
raise ValueError(f"Invalid Azure OpenAI base URL: {base_url}")
|
|
81
|
+
|
|
82
|
+
hostname = parts.hostname or ""
|
|
83
|
+
is_azure_host = (
|
|
84
|
+
hostname.endswith(".openai.azure.com")
|
|
85
|
+
or hostname.endswith(".cognitiveservices.azure.com")
|
|
86
|
+
or hostname.endswith(".ai.azure.com")
|
|
87
|
+
)
|
|
88
|
+
normalized_path = _TRAILING_SLASHES.sub("", parts.path)
|
|
89
|
+
path = parts.path
|
|
90
|
+
query = parts.query
|
|
91
|
+
|
|
92
|
+
# Ensure Azure hosts have /openai/v1 as base path so /responses and ?api-version=v1
|
|
93
|
+
# are appended correctly.
|
|
94
|
+
if is_azure_host and normalized_path in ("", "/", "/openai", "/openai/v1/responses"):
|
|
95
|
+
path = "/openai/v1"
|
|
96
|
+
query = ""
|
|
97
|
+
|
|
98
|
+
netloc = parts.netloc if "@" in parts.netloc else parts.netloc.lower()
|
|
99
|
+
if not path and scheme in _SPECIAL_SCHEMES:
|
|
100
|
+
path = "/"
|
|
101
|
+
url = urlunsplit((scheme, netloc, path, query, parts.fragment))
|
|
102
|
+
return _TRAILING_SLASHES.sub("", url)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _build_default_base_url(resource_name: str) -> str:
|
|
106
|
+
return f"https://{resource_name}.openai.azure.com/openai/v1"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def resolve_azure_base_url(model: Any, options: Mapping[str, Any] | None = None) -> str:
|
|
110
|
+
"""Base URL from ``azure_base_url``, ``AZURE_OPENAI_BASE_URL``, the resource name, or ``model.base_url``."""
|
|
111
|
+
options = options or {}
|
|
112
|
+
env = options.get("env")
|
|
113
|
+
explicit = (options.get("azure_base_url") or "").strip()
|
|
114
|
+
base_url = explicit or (get_provider_env_value("AZURE_OPENAI_BASE_URL", env) or "").strip() or None
|
|
115
|
+
resource_name = options.get("azure_resource_name") or get_provider_env_value("AZURE_OPENAI_RESOURCE_NAME", env)
|
|
116
|
+
|
|
117
|
+
resolved_base_url = base_url
|
|
118
|
+
if not resolved_base_url and resource_name:
|
|
119
|
+
resolved_base_url = _build_default_base_url(resource_name)
|
|
120
|
+
if not resolved_base_url and getattr(model, "base_url", None):
|
|
121
|
+
resolved_base_url = model.base_url
|
|
122
|
+
if not resolved_base_url:
|
|
123
|
+
raise ValueError(
|
|
124
|
+
"Azure OpenAI base URL is required. Set AZURE_OPENAI_BASE_URL or AZURE_OPENAI_RESOURCE_NAME, "
|
|
125
|
+
"or pass azure_base_url, azure_resource_name, or model.base_url."
|
|
126
|
+
)
|
|
127
|
+
return _normalize_azure_base_url(resolved_base_url)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def resolve_azure_config(model: Any, options: Mapping[str, Any] | None = None) -> AzureConfig:
|
|
131
|
+
"""Base URL and API version (``azure_api_version``, ``AZURE_OPENAI_API_VERSION``, or ``"v1"``)."""
|
|
132
|
+
options = options or {}
|
|
133
|
+
return AzureConfig(
|
|
134
|
+
base_url=resolve_azure_base_url(model, options),
|
|
135
|
+
api_version=options.get("azure_api_version")
|
|
136
|
+
or get_provider_env_value("AZURE_OPENAI_API_VERSION", options.get("env"))
|
|
137
|
+
or DEFAULT_AZURE_API_VERSION,
|
|
138
|
+
)
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# pyright: reportAttributeAccessIssue=false, reportArgumentType=false
|
|
2
|
+
# Converters branch on ``role``/``type`` discriminators held in locals, which pyright does not narrow.
|
|
3
|
+
"""Azure OpenAI Responses API (``azure-openai-responses``).
|
|
4
|
+
|
|
5
|
+
Streams ``responses.create`` through the official ``openai`` SDK's ``AsyncAzureOpenAI``
|
|
6
|
+
client (``POST {base_url}/responses?api-version=<version>`` with an ``api-key`` header).
|
|
7
|
+
The deployment name goes in the body's ``model`` field.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from typing import Any, Literal
|
|
14
|
+
|
|
15
|
+
from ..abort import abort_scope
|
|
16
|
+
from ..event_stream import AssistantMessageEventStream
|
|
17
|
+
from ..models import clamp_thinking_level
|
|
18
|
+
from ..types import (
|
|
19
|
+
AssistantMessage,
|
|
20
|
+
DoneEvent,
|
|
21
|
+
ErrorEvent,
|
|
22
|
+
Model,
|
|
23
|
+
ProviderResponse,
|
|
24
|
+
StartEvent,
|
|
25
|
+
TranscriptContext,
|
|
26
|
+
Usage,
|
|
27
|
+
)
|
|
28
|
+
from ..utils.callbacks import maybe_await
|
|
29
|
+
from ..utils.error_body import format_provider_error, normalize_provider_error
|
|
30
|
+
from ..utils.misc import get_pi_user_agent, headers_to_record
|
|
31
|
+
from ..utils.transcript import get_declared_tools, resolve_transcript, resolve_transcript_tools
|
|
32
|
+
from .azure_openai_config import AzureEndpointOptions, resolve_azure_config, resolve_deployment_name
|
|
33
|
+
from .constrained_sampling import create_grammar_tool_input_properties
|
|
34
|
+
from .openai_prompt_cache import clamp_openai_prompt_cache_key
|
|
35
|
+
from .openai_responses_shared import convert_responses_messages, convert_responses_tools, process_responses_stream
|
|
36
|
+
from .openai_sdk import (
|
|
37
|
+
create_openai_client,
|
|
38
|
+
iter_sdk_stream,
|
|
39
|
+
merge_sdk_headers,
|
|
40
|
+
open_sdk_stream,
|
|
41
|
+
openai_request,
|
|
42
|
+
sdk_body_arguments,
|
|
43
|
+
)
|
|
44
|
+
from .simple_options import build_base_options, resolve_sampling_params
|
|
45
|
+
|
|
46
|
+
__all__ = ["AzureOpenAIResponsesOptions", "AZURE_TOOL_CALL_PROVIDERS", "stream", "stream_simple"]
|
|
47
|
+
|
|
48
|
+
AZURE_TOOL_CALL_PROVIDERS = frozenset({"openai", "openai-codex", "opencode", "azure"})
|
|
49
|
+
# OpenAI Responses rejects max_output_tokens below 16: https://github.com/earendil-works/pi/issues/6265
|
|
50
|
+
OPENAI_RESPONSES_MIN_OUTPUT_TOKENS = 16
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class AzureOpenAIResponsesOptions(AzureEndpointOptions, total=False):
|
|
54
|
+
reasoning_effort: Literal["minimal", "low", "medium", "high", "xhigh", "max"]
|
|
55
|
+
tool_choice: Any
|
|
56
|
+
reasoning_summary: Literal["auto", "detailed", "concise"] | None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _format_azure_openai_error(error: Any) -> str:
|
|
60
|
+
return format_provider_error(normalize_provider_error(error), "Azure OpenAI API error")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def stream(
|
|
64
|
+
model: Model, context: TranscriptContext, options: Mapping[str, Any] | None = None
|
|
65
|
+
) -> AssistantMessageEventStream:
|
|
66
|
+
"""Stream a response from an Azure OpenAI Responses deployment."""
|
|
67
|
+
opts: dict[str, Any] = dict(options or {})
|
|
68
|
+
out = AssistantMessageEventStream()
|
|
69
|
+
normalized_context = resolve_transcript(context, (model.compat or {}).get("supportsMidConvoSystemMessages"))
|
|
70
|
+
out.start(_run(model, normalized_context, opts, out))
|
|
71
|
+
return out
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def _run(
|
|
75
|
+
model: Model, context: TranscriptContext, options: dict[str, Any], out: AssistantMessageEventStream
|
|
76
|
+
) -> None:
|
|
77
|
+
deployment_name = resolve_deployment_name(model, options)
|
|
78
|
+
output = AssistantMessage(
|
|
79
|
+
[],
|
|
80
|
+
api="azure-openai-responses",
|
|
81
|
+
provider=model.provider,
|
|
82
|
+
model=model.id,
|
|
83
|
+
usage=Usage(),
|
|
84
|
+
stop_reason="pending",
|
|
85
|
+
)
|
|
86
|
+
signal = options.get("signal")
|
|
87
|
+
try:
|
|
88
|
+
async with abort_scope(signal):
|
|
89
|
+
api_key = options.get("api_key")
|
|
90
|
+
if not api_key:
|
|
91
|
+
raise RuntimeError(f"No API key for provider: {model.provider}")
|
|
92
|
+
headers = merge_sdk_headers({"User-Agent": get_pi_user_agent()}, model.headers, options.get("headers"))
|
|
93
|
+
base_url, api_version = resolve_azure_config(model, options)
|
|
94
|
+
client = create_openai_client(
|
|
95
|
+
api_key=api_key,
|
|
96
|
+
# The SDK appends the path to the base URL and sends ``api-version`` as the query string.
|
|
97
|
+
base_url=base_url.partition("?")[0],
|
|
98
|
+
headers=headers,
|
|
99
|
+
options=options,
|
|
100
|
+
azure_api_version=api_version,
|
|
101
|
+
)
|
|
102
|
+
grammar_tool_input_properties = create_grammar_tool_input_properties(
|
|
103
|
+
get_declared_tools(context.messages), bool((model.compat or {}).get("supportsOpenAIGrammarTools"))
|
|
104
|
+
)
|
|
105
|
+
params: Any = _build_params(model, context, options, deployment_name, grammar_tool_input_properties)
|
|
106
|
+
on_payload = options.get("on_payload")
|
|
107
|
+
if on_payload is not None:
|
|
108
|
+
next_params = await maybe_await(on_payload(params, model))
|
|
109
|
+
if next_params is not None:
|
|
110
|
+
params = next_params
|
|
111
|
+
create = client.responses.with_raw_response.create
|
|
112
|
+
body_arguments = sdk_body_arguments(params, headers=headers)
|
|
113
|
+
raw = await openai_request(lambda: create(stream=True, **body_arguments), options)
|
|
114
|
+
sdk_stream = open_sdk_stream(raw)
|
|
115
|
+
try:
|
|
116
|
+
on_response = options.get("on_response")
|
|
117
|
+
if on_response is not None:
|
|
118
|
+
await maybe_await(
|
|
119
|
+
on_response(ProviderResponse(raw.status_code, headers_to_record(raw.headers)), model)
|
|
120
|
+
)
|
|
121
|
+
out.push(StartEvent(output))
|
|
122
|
+
await process_responses_stream(
|
|
123
|
+
iter_sdk_stream(sdk_stream),
|
|
124
|
+
output,
|
|
125
|
+
out,
|
|
126
|
+
model,
|
|
127
|
+
{
|
|
128
|
+
"on_provider_stream_event": options.get("on_provider_stream_event"),
|
|
129
|
+
"grammar_tool_input_properties": grammar_tool_input_properties,
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
finally:
|
|
133
|
+
await sdk_stream.close()
|
|
134
|
+
|
|
135
|
+
if signal is not None and signal.aborted:
|
|
136
|
+
raise RuntimeError("Request was aborted")
|
|
137
|
+
if output.stop_reason == "pending":
|
|
138
|
+
raise RuntimeError("Azure OpenAI Responses stream ended without a stop reason")
|
|
139
|
+
if output.stop_reason in ("aborted", "error"):
|
|
140
|
+
raise RuntimeError(output.error_message or "An unknown error occurred")
|
|
141
|
+
|
|
142
|
+
out.push(DoneEvent(output.stop_reason, output)) # type: ignore[arg-type]
|
|
143
|
+
out.end()
|
|
144
|
+
except Exception as error:
|
|
145
|
+
aborted = signal is not None and signal.aborted
|
|
146
|
+
output.stop_reason = "aborted" if aborted else "error"
|
|
147
|
+
output.error_message = "Request was aborted" if aborted else _format_azure_openai_error(error)
|
|
148
|
+
out.push(ErrorEvent(output.stop_reason, output)) # type: ignore[arg-type]
|
|
149
|
+
out.end()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def stream_simple(
|
|
153
|
+
model: Model, context: TranscriptContext, options: Mapping[str, Any] | None = None
|
|
154
|
+
) -> AssistantMessageEventStream:
|
|
155
|
+
"""Stream with provider-neutral options (``reasoning``, ``tool_choice``, ...)."""
|
|
156
|
+
options = options or {}
|
|
157
|
+
api_key = options.get("api_key")
|
|
158
|
+
if not api_key:
|
|
159
|
+
raise RuntimeError(f"No API key for provider: {model.provider}")
|
|
160
|
+
|
|
161
|
+
base = build_base_options(model, context, options, api_key)
|
|
162
|
+
if options.get("tool_choice") is not None:
|
|
163
|
+
base["tool_choice"] = options["tool_choice"]
|
|
164
|
+
reasoning = options.get("reasoning")
|
|
165
|
+
clamped = clamp_thinking_level(model, reasoning) if reasoning else None
|
|
166
|
+
if clamped is not None and clamped != "off":
|
|
167
|
+
base["reasoning_effort"] = clamped
|
|
168
|
+
# Azure endpoint options are not part of the simple options; keep them when callers pass them.
|
|
169
|
+
for key in ("azure_api_version", "azure_resource_name", "azure_base_url", "azure_deployment_name"):
|
|
170
|
+
if options.get(key) is not None:
|
|
171
|
+
base[key] = options[key]
|
|
172
|
+
return stream(model, context, base)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _build_params(
|
|
176
|
+
model: Model,
|
|
177
|
+
context: TranscriptContext,
|
|
178
|
+
options: Mapping[str, Any],
|
|
179
|
+
deployment_name: str,
|
|
180
|
+
grammar_tool_input_properties: Mapping[str, str],
|
|
181
|
+
) -> dict[str, Any]:
|
|
182
|
+
compat = model.compat or {}
|
|
183
|
+
supports_additional_tools = bool(compat.get("supportsAdditionalTools"))
|
|
184
|
+
supports_tool_search = bool(compat.get("supportsToolSearch"))
|
|
185
|
+
supports_strict_mode = compat.get("supportsStrictMode")
|
|
186
|
+
if supports_strict_mode is None:
|
|
187
|
+
supports_strict_mode = True
|
|
188
|
+
supports_openai_grammar_tools = bool(compat.get("supportsOpenAIGrammarTools"))
|
|
189
|
+
tool_options = {
|
|
190
|
+
"supports_strict_mode": supports_strict_mode,
|
|
191
|
+
"supports_openai_grammar_tools": supports_openai_grammar_tools,
|
|
192
|
+
}
|
|
193
|
+
transcript_tools = resolve_transcript_tools(context.messages, supports_additional_tools or supports_tool_search)
|
|
194
|
+
messages = convert_responses_messages(
|
|
195
|
+
model,
|
|
196
|
+
context,
|
|
197
|
+
AZURE_TOOL_CALL_PROVIDERS,
|
|
198
|
+
{
|
|
199
|
+
"grammar_tool_input_properties": grammar_tool_input_properties,
|
|
200
|
+
"supports_mid_convo_system_messages": bool(compat.get("supportsMidConvoSystemMessages")),
|
|
201
|
+
"supports_additional_tools": supports_additional_tools,
|
|
202
|
+
"supports_tool_search": supports_tool_search,
|
|
203
|
+
"tool_options": tool_options, # type: ignore[typeddict-item]
|
|
204
|
+
},
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
params: dict[str, Any] = {"model": deployment_name, "input": messages, "stream": True}
|
|
208
|
+
prompt_cache_key = clamp_openai_prompt_cache_key(options.get("session_id"))
|
|
209
|
+
if prompt_cache_key is not None:
|
|
210
|
+
params["prompt_cache_key"] = prompt_cache_key
|
|
211
|
+
params["store"] = False
|
|
212
|
+
|
|
213
|
+
max_tokens = options.get("max_tokens")
|
|
214
|
+
if max_tokens:
|
|
215
|
+
params["max_output_tokens"] = max(max_tokens, OPENAI_RESPONSES_MIN_OUTPUT_TOKENS)
|
|
216
|
+
|
|
217
|
+
if options.get("temperature") is not None:
|
|
218
|
+
params["temperature"] = options["temperature"]
|
|
219
|
+
|
|
220
|
+
if transcript_tools.request_tools:
|
|
221
|
+
params["tools"] = convert_responses_tools(transcript_tools.request_tools, tool_options) # type: ignore[arg-type]
|
|
222
|
+
if options.get("tool_choice") is not None:
|
|
223
|
+
params["tool_choice"] = options["tool_choice"]
|
|
224
|
+
|
|
225
|
+
requested = options.get("reasoning_effort")
|
|
226
|
+
reasoning_effort = requested or ("medium" if options.get("reasoning_summary") else None)
|
|
227
|
+
if model.reasoning:
|
|
228
|
+
level_map = model.thinking_level_map or {}
|
|
229
|
+
if reasoning_effort:
|
|
230
|
+
effort = reasoning_effort
|
|
231
|
+
if requested:
|
|
232
|
+
mapped = level_map.get(requested)
|
|
233
|
+
effort = mapped if mapped is not None else requested
|
|
234
|
+
params["reasoning"] = {"effort": effort, "summary": options.get("reasoning_summary") or "auto"}
|
|
235
|
+
params["include"] = ["reasoning.encrypted_content"]
|
|
236
|
+
elif not ("off" in level_map and level_map["off"] is None):
|
|
237
|
+
off = level_map.get("off")
|
|
238
|
+
params["reasoning"] = {"effort": off if off is not None else "none"}
|
|
239
|
+
|
|
240
|
+
# Last so model and request sampling parameters override named request fields.
|
|
241
|
+
sampling_params = resolve_sampling_params(model, reasoning_effort or "off", options.get("sampling_params"))
|
|
242
|
+
if sampling_params:
|
|
243
|
+
params.update(sampling_params)
|
|
244
|
+
|
|
245
|
+
return params
|