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/providers/azure.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Azure OpenAI provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ..api.lazy import lazy_stream
|
|
10
|
+
from ..api.registry import azure_openai_responses_api, openai_completions_api
|
|
11
|
+
from ..auth.helpers import env_api_key_auth
|
|
12
|
+
from ..auth.types import ProviderAuth
|
|
13
|
+
from ..event_stream import AssistantMessageEventStream
|
|
14
|
+
from ..models import Provider, create_provider
|
|
15
|
+
from ..types import Model, TranscriptContext
|
|
16
|
+
from ..utils.callbacks import maybe_await
|
|
17
|
+
from .catalog import catalog_models
|
|
18
|
+
|
|
19
|
+
__all__ = ["azure_provider"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _resolve_azure_model(model: Model, options: Mapping[str, Any] | None) -> Model:
|
|
23
|
+
from ..api.azure_openai_config import resolve_azure_base_url
|
|
24
|
+
|
|
25
|
+
return dataclasses.replace(model, base_url=resolve_azure_base_url(model, options))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _with_deployment_name(model: Model, options: Mapping[str, Any] | None) -> Mapping[str, Any] | None:
|
|
29
|
+
"""Send the deployment name as the request's model, keeping ``model.id`` as the catalog id."""
|
|
30
|
+
from ..api.azure_openai_config import resolve_deployment_name
|
|
31
|
+
|
|
32
|
+
deployment_name = resolve_deployment_name(model, options)
|
|
33
|
+
if deployment_name == model.id:
|
|
34
|
+
return options
|
|
35
|
+
user_on_payload = options.get("on_payload") if options else None
|
|
36
|
+
|
|
37
|
+
async def on_payload(payload: Any, payload_model: Any) -> Any:
|
|
38
|
+
params = {**payload, "model": deployment_name}
|
|
39
|
+
if user_on_payload is not None:
|
|
40
|
+
replaced = await maybe_await(user_on_payload(params, payload_model))
|
|
41
|
+
if replaced is not None:
|
|
42
|
+
return replaced
|
|
43
|
+
return params
|
|
44
|
+
|
|
45
|
+
return {**(options or {}), "on_payload": on_payload}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class _AzureStreams:
|
|
49
|
+
"""Resolve the Azure endpoint and deployment before dispatch.
|
|
50
|
+
|
|
51
|
+
Resolution runs inside ``lazy_stream`` so an unconfigured endpoint errors on the
|
|
52
|
+
stream instead of raising out of ``stream()``.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
__slots__ = ("_streams",)
|
|
56
|
+
|
|
57
|
+
def __init__(self, streams: Any) -> None:
|
|
58
|
+
self._streams = streams
|
|
59
|
+
|
|
60
|
+
def stream(
|
|
61
|
+
self, model: Model, context: TranscriptContext, options: Mapping[str, Any] | None = None
|
|
62
|
+
) -> AssistantMessageEventStream:
|
|
63
|
+
async def setup() -> AssistantMessageEventStream:
|
|
64
|
+
return self._streams.stream(
|
|
65
|
+
_resolve_azure_model(model, options), context, _with_deployment_name(model, options)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return lazy_stream(model, setup)
|
|
69
|
+
|
|
70
|
+
def stream_simple(
|
|
71
|
+
self, model: Model, context: TranscriptContext, options: Mapping[str, Any] | None = None
|
|
72
|
+
) -> AssistantMessageEventStream:
|
|
73
|
+
async def setup() -> AssistantMessageEventStream:
|
|
74
|
+
return self._streams.stream_simple(
|
|
75
|
+
_resolve_azure_model(model, options), context, _with_deployment_name(model, options)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return lazy_stream(model, setup)
|
|
79
|
+
|
|
80
|
+
def __repr__(self) -> str:
|
|
81
|
+
return f"azure_streams({self._streams!r})"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def azure_streams(streams: Any) -> Any:
|
|
85
|
+
return _AzureStreams(streams)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def azure_provider() -> Provider:
|
|
89
|
+
"""Azure OpenAI (Responses and Chat Completions), authenticated with ``AZURE_OPENAI_API_KEY``.
|
|
90
|
+
|
|
91
|
+
Catalog models ship without a base URL: the endpoint comes from ``AZURE_OPENAI_BASE_URL``,
|
|
92
|
+
``AZURE_OPENAI_RESOURCE_NAME`` or the matching request options.
|
|
93
|
+
"""
|
|
94
|
+
return create_provider(
|
|
95
|
+
id="azure",
|
|
96
|
+
name="Azure",
|
|
97
|
+
auth=ProviderAuth(api_key=env_api_key_auth("Azure OpenAI API key", ["AZURE_OPENAI_API_KEY"])),
|
|
98
|
+
models=lambda: catalog_models("azure"),
|
|
99
|
+
api={
|
|
100
|
+
"azure-openai-responses": azure_openai_responses_api(),
|
|
101
|
+
"openai-completions": azure_streams(openai_completions_api()),
|
|
102
|
+
},
|
|
103
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Baseten provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..api.registry import openai_completions_api
|
|
6
|
+
from ..auth.helpers import env_api_key_auth
|
|
7
|
+
from ..auth.types import ProviderAuth
|
|
8
|
+
from ..models import Provider, create_provider
|
|
9
|
+
from .catalog import catalog_models
|
|
10
|
+
|
|
11
|
+
__all__ = ["baseten_provider"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def baseten_provider() -> Provider:
|
|
15
|
+
"""Baseten (OpenAI-compatible Chat Completions), authenticated with ``BASETEN_API_KEY``."""
|
|
16
|
+
return create_provider(
|
|
17
|
+
id="baseten",
|
|
18
|
+
name="Baseten",
|
|
19
|
+
base_url="https://inference.baseten.co/v1",
|
|
20
|
+
auth=ProviderAuth(api_key=env_api_key_auth("Baseten API key", ["BASETEN_API_KEY"])),
|
|
21
|
+
models=lambda: catalog_models("baseten"),
|
|
22
|
+
api=openai_completions_api(),
|
|
23
|
+
)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Built-in model catalog data.
|
|
2
|
+
|
|
3
|
+
Catalog files (``data/<provider>.json``) are the generated model data shipped by
|
|
4
|
+
``@earendil-works/pi-ai``. Each file groups entries by API id and keys them as
|
|
5
|
+
``"<type>:<model id>"``. Files load on first access and are cached per process.
|
|
6
|
+
|
|
7
|
+
Returned models are shared instances, as in pi. Derive variants with
|
|
8
|
+
``dataclasses.replace(model, ...)`` instead of mutating them.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import threading
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from functools import lru_cache
|
|
17
|
+
from importlib import resources
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from ..serialization import model_from_dict
|
|
21
|
+
from ..types import AnyModel, ClassifierModel, ImageModel, Model
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"BUILTIN_PROVIDERS",
|
|
25
|
+
"load_catalog",
|
|
26
|
+
"catalog_models",
|
|
27
|
+
"get_builtin_providers",
|
|
28
|
+
"get_builtin_model",
|
|
29
|
+
"get_builtin_image_model",
|
|
30
|
+
"get_builtin_classifier_model",
|
|
31
|
+
"get_builtin_models",
|
|
32
|
+
"get_builtin_image_models",
|
|
33
|
+
"get_builtin_classifier_models",
|
|
34
|
+
"get_all_builtin_models",
|
|
35
|
+
"get_builtin_model_data_generated_at",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
#: Providers present in the generated catalog.
|
|
39
|
+
BUILTIN_PROVIDERS: tuple[str, ...] = (
|
|
40
|
+
"amazon-bedrock",
|
|
41
|
+
"ant-ling",
|
|
42
|
+
"anthropic",
|
|
43
|
+
"azure",
|
|
44
|
+
"baseten",
|
|
45
|
+
"cerebras",
|
|
46
|
+
"cloudflare-ai-gateway",
|
|
47
|
+
"cloudflare-workers-ai",
|
|
48
|
+
"deepseek",
|
|
49
|
+
"fireworks",
|
|
50
|
+
"github-copilot",
|
|
51
|
+
"google",
|
|
52
|
+
"google-vertex",
|
|
53
|
+
"groq",
|
|
54
|
+
"huggingface",
|
|
55
|
+
"kimi-coding",
|
|
56
|
+
"meta",
|
|
57
|
+
"minimax",
|
|
58
|
+
"minimax-cn",
|
|
59
|
+
"mistral",
|
|
60
|
+
"moonshotai",
|
|
61
|
+
"moonshotai-cn",
|
|
62
|
+
"nvidia",
|
|
63
|
+
"openai",
|
|
64
|
+
"openai-codex",
|
|
65
|
+
"opencode",
|
|
66
|
+
"opencode-go",
|
|
67
|
+
"openrouter",
|
|
68
|
+
"qwen-token-plan",
|
|
69
|
+
"qwen-token-plan-cn",
|
|
70
|
+
"qwen-token-plan-individual",
|
|
71
|
+
"radius",
|
|
72
|
+
"together",
|
|
73
|
+
"typesafe",
|
|
74
|
+
"vercel-ai-gateway",
|
|
75
|
+
"xai",
|
|
76
|
+
"xiaomi",
|
|
77
|
+
"xiaomi-token-plan-ams",
|
|
78
|
+
"xiaomi-token-plan-cn",
|
|
79
|
+
"xiaomi-token-plan-sgp",
|
|
80
|
+
"zai",
|
|
81
|
+
"zai-coding-cn",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
_lock = threading.Lock()
|
|
85
|
+
_cache: dict[str, dict[str, dict[str, AnyModel]]] = {}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _read_json(name: str) -> Any:
|
|
89
|
+
data_dir = resources.files("pi_ai.providers").joinpath("data")
|
|
90
|
+
with data_dir.joinpath(name).open("r", encoding="utf-8") as handle:
|
|
91
|
+
return json.load(handle)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def load_catalog(provider: str) -> dict[str, dict[str, AnyModel]]:
|
|
95
|
+
"""Catalog of ``provider`` as ``{"chat": {id: model}, "image": {...}, "classifier": {...}}``."""
|
|
96
|
+
cached = _cache.get(provider)
|
|
97
|
+
if cached is not None:
|
|
98
|
+
return cached
|
|
99
|
+
with _lock:
|
|
100
|
+
cached = _cache.get(provider)
|
|
101
|
+
if cached is not None:
|
|
102
|
+
return cached
|
|
103
|
+
groups: dict[str, dict[str, AnyModel]] = {"chat": {}, "image": {}, "classifier": {}}
|
|
104
|
+
try:
|
|
105
|
+
raw = _read_json(f"{provider}.json")
|
|
106
|
+
except FileNotFoundError:
|
|
107
|
+
raw = {}
|
|
108
|
+
for entries in raw.values():
|
|
109
|
+
for key, value in entries.items():
|
|
110
|
+
model_type, _, _ = key.partition(":")
|
|
111
|
+
group = groups.get(model_type or "chat")
|
|
112
|
+
if group is None:
|
|
113
|
+
continue
|
|
114
|
+
model = model_from_dict(value)
|
|
115
|
+
group[model.id] = model
|
|
116
|
+
_cache[provider] = groups
|
|
117
|
+
return groups
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def catalog_models(provider: str) -> list[AnyModel]:
|
|
121
|
+
"""Every built-in model of ``provider`` (chat, then image, then classifier)."""
|
|
122
|
+
groups = load_catalog(provider)
|
|
123
|
+
return [*groups["chat"].values(), *groups["image"].values(), *groups["classifier"].values()]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def get_builtin_providers() -> list[str]:
|
|
127
|
+
return list(BUILTIN_PROVIDERS)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def get_builtin_model(provider: str, model_id: str) -> Model | None:
|
|
131
|
+
"""One generated built-in chat model, independent of any ``Models`` collection."""
|
|
132
|
+
return load_catalog(provider)["chat"].get(model_id) # type: ignore[return-value]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_builtin_image_model(provider: str, model_id: str) -> ImageModel | None:
|
|
136
|
+
return load_catalog(provider)["image"].get(model_id) # type: ignore[return-value]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def get_builtin_classifier_model(provider: str, model_id: str) -> ClassifierModel | None:
|
|
140
|
+
return load_catalog(provider)["classifier"].get(model_id) # type: ignore[return-value]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_builtin_models(provider: str) -> list[Model]:
|
|
144
|
+
return list(load_catalog(provider)["chat"].values()) # type: ignore[arg-type]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def get_builtin_image_models(provider: str) -> list[ImageModel]:
|
|
148
|
+
return list(load_catalog(provider)["image"].values()) # type: ignore[arg-type]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def get_builtin_classifier_models(provider: str) -> list[ClassifierModel]:
|
|
152
|
+
return list(load_catalog(provider)["classifier"].values()) # type: ignore[arg-type]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def get_all_builtin_models(provider: str) -> list[AnyModel]:
|
|
156
|
+
return catalog_models(provider)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@lru_cache(maxsize=1)
|
|
160
|
+
def get_builtin_model_data_generated_at() -> int | None:
|
|
161
|
+
"""Generation timestamp (ms) shared by all built-in provider catalogs."""
|
|
162
|
+
try:
|
|
163
|
+
manifest = _read_json("manifest.json")
|
|
164
|
+
stamp = datetime.fromisoformat(manifest["generatedAt"].replace("Z", "+00:00"))
|
|
165
|
+
except (FileNotFoundError, KeyError, ValueError):
|
|
166
|
+
return None
|
|
167
|
+
return int(stamp.timestamp() * 1000)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Cerebras provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..api.registry import openai_completions_api
|
|
6
|
+
from ..auth.helpers import env_api_key_auth
|
|
7
|
+
from ..auth.types import ProviderAuth
|
|
8
|
+
from ..models import Provider, create_provider
|
|
9
|
+
from .catalog import catalog_models
|
|
10
|
+
|
|
11
|
+
__all__ = ["cerebras_provider"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def cerebras_provider() -> Provider:
|
|
15
|
+
"""Cerebras (OpenAI-compatible Chat Completions), authenticated with ``CEREBRAS_API_KEY``."""
|
|
16
|
+
return create_provider(
|
|
17
|
+
id="cerebras",
|
|
18
|
+
name="Cerebras",
|
|
19
|
+
base_url="https://api.cerebras.ai/v1",
|
|
20
|
+
auth=ProviderAuth(api_key=env_api_key_auth("Cerebras API key", ["CEREBRAS_API_KEY"])),
|
|
21
|
+
models=lambda: catalog_models("cerebras"),
|
|
22
|
+
api=openai_completions_api(),
|
|
23
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Cloudflare AI Gateway provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..api.registry import anthropic_messages_api, openai_completions_api, openai_responses_api
|
|
6
|
+
from ..auth.types import ProviderAuth
|
|
7
|
+
from ..models import Provider, create_provider
|
|
8
|
+
from .catalog import catalog_models
|
|
9
|
+
from .cloudflare_auth import cloudflare_ai_gateway_auth
|
|
10
|
+
from .cloudflare_stream import cloudflare_streams
|
|
11
|
+
|
|
12
|
+
__all__ = ["cloudflare_ai_gateway_provider"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def cloudflare_ai_gateway_provider() -> Provider:
|
|
16
|
+
"""Cloudflare AI Gateway: ``CLOUDFLARE_API_KEY``, ``CLOUDFLARE_ACCOUNT_ID`` and ``CLOUDFLARE_GATEWAY_ID``."""
|
|
17
|
+
# The api map is pinned to all three APIs: models.dev's gateway catalog drops and
|
|
18
|
+
# restores `workers-ai/*` (openai-completions) entries over time, so the map must
|
|
19
|
+
# not depend on which APIs the generated catalog happens to contain.
|
|
20
|
+
return create_provider(
|
|
21
|
+
id="cloudflare-ai-gateway",
|
|
22
|
+
name="Cloudflare AI Gateway",
|
|
23
|
+
auth=ProviderAuth(api_key=cloudflare_ai_gateway_auth()),
|
|
24
|
+
models=lambda: catalog_models("cloudflare-ai-gateway"),
|
|
25
|
+
api={
|
|
26
|
+
"anthropic-messages": cloudflare_streams(anthropic_messages_api()),
|
|
27
|
+
"openai-completions": cloudflare_streams(openai_completions_api()),
|
|
28
|
+
"openai-responses": cloudflare_streams(openai_responses_api()),
|
|
29
|
+
},
|
|
30
|
+
)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Cloudflare Workers AI / AI Gateway auth: API key plus account (and gateway) ids."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from ..abort import AbortSignal
|
|
8
|
+
from ..auth.types import (
|
|
9
|
+
ApiKeyAuth,
|
|
10
|
+
AuthContext,
|
|
11
|
+
AuthPrompt,
|
|
12
|
+
AuthResult,
|
|
13
|
+
Credential,
|
|
14
|
+
ModelAuth,
|
|
15
|
+
ProviderAuthInteraction,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = ["cloudflare_workers_ai_auth", "cloudflare_ai_gateway_auth"]
|
|
19
|
+
|
|
20
|
+
CLOUDFLARE_API_KEY = "CLOUDFLARE_API_KEY"
|
|
21
|
+
CLOUDFLARE_ACCOUNT_ID = "CLOUDFLARE_ACCOUNT_ID"
|
|
22
|
+
CLOUDFLARE_GATEWAY_ID = "CLOUDFLARE_GATEWAY_ID"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(slots=True)
|
|
26
|
+
class _ResolvedCloudflare:
|
|
27
|
+
api_key: str
|
|
28
|
+
env: dict[str, str]
|
|
29
|
+
source: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def _resolve_value(name: str, ctx: AuthContext, credential: Credential | None, signal: AbortSignal) -> str | None:
|
|
33
|
+
# Per-field merge: prefer the credential value, fall back to ambient env.
|
|
34
|
+
# A credential carrying only the API key must still pick up the account /
|
|
35
|
+
# gateway id from the environment.
|
|
36
|
+
from_credential: str | None = None
|
|
37
|
+
if credential is not None:
|
|
38
|
+
if name == CLOUDFLARE_API_KEY:
|
|
39
|
+
from_credential = credential.get("key")
|
|
40
|
+
else:
|
|
41
|
+
from_credential = (credential.get("env") or {}).get(name)
|
|
42
|
+
if from_credential is not None:
|
|
43
|
+
return from_credential
|
|
44
|
+
signal.throw_if_aborted()
|
|
45
|
+
value = await ctx.env(name)
|
|
46
|
+
signal.throw_if_aborted()
|
|
47
|
+
return value
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def _resolve_cloudflare_env(
|
|
51
|
+
kind: str, ctx: AuthContext, credential: Credential | None, signal: AbortSignal
|
|
52
|
+
) -> _ResolvedCloudflare | None:
|
|
53
|
+
api_key = await _resolve_value(CLOUDFLARE_API_KEY, ctx, credential, signal)
|
|
54
|
+
account_id = await _resolve_value(CLOUDFLARE_ACCOUNT_ID, ctx, credential, signal)
|
|
55
|
+
gateway_id = await _resolve_value(CLOUDFLARE_GATEWAY_ID, ctx, credential, signal) if kind == "ai-gateway" else None
|
|
56
|
+
|
|
57
|
+
if not api_key or not account_id or (kind == "ai-gateway" and not gateway_id):
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
env = {CLOUDFLARE_ACCOUNT_ID: account_id}
|
|
61
|
+
if gateway_id:
|
|
62
|
+
env[CLOUDFLARE_GATEWAY_ID] = gateway_id
|
|
63
|
+
return _ResolvedCloudflare(api_key, env, "stored credential" if credential is not None else CLOUDFLARE_API_KEY)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def cloudflare_workers_ai_auth() -> ApiKeyAuth:
|
|
67
|
+
"""API key auth that also requires ``CLOUDFLARE_ACCOUNT_ID`` (returned as provider env)."""
|
|
68
|
+
|
|
69
|
+
async def login(interaction: ProviderAuthInteraction) -> Credential:
|
|
70
|
+
key = await interaction.prompt(AuthPrompt(type="secret", message="Enter Cloudflare API key"))
|
|
71
|
+
account_id = await interaction.prompt(AuthPrompt(type="text", message="Enter Cloudflare account ID"))
|
|
72
|
+
return {"type": "api_key", "key": key, "env": {CLOUDFLARE_ACCOUNT_ID: account_id}}
|
|
73
|
+
|
|
74
|
+
async def resolve(*, ctx: AuthContext, credential: Credential | None, signal: AbortSignal) -> AuthResult | None:
|
|
75
|
+
resolved = await _resolve_cloudflare_env("workers-ai", ctx, credential, signal)
|
|
76
|
+
if resolved is None:
|
|
77
|
+
return None
|
|
78
|
+
return AuthResult(ModelAuth(api_key=resolved.api_key), env=resolved.env, source=resolved.source)
|
|
79
|
+
|
|
80
|
+
return ApiKeyAuth("Cloudflare API key", resolve, login=login)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def cloudflare_ai_gateway_auth() -> ApiKeyAuth:
|
|
84
|
+
"""AI Gateway auth: the key travels as ``cf-aig-authorization``; account and gateway ids as provider env."""
|
|
85
|
+
|
|
86
|
+
async def login(interaction: ProviderAuthInteraction) -> Credential:
|
|
87
|
+
key = await interaction.prompt(AuthPrompt(type="secret", message="Enter Cloudflare API key"))
|
|
88
|
+
account_id = await interaction.prompt(AuthPrompt(type="text", message="Enter Cloudflare account ID"))
|
|
89
|
+
gateway_id = await interaction.prompt(AuthPrompt(type="text", message="Enter Cloudflare AI Gateway ID"))
|
|
90
|
+
return {
|
|
91
|
+
"type": "api_key",
|
|
92
|
+
"key": key,
|
|
93
|
+
"env": {CLOUDFLARE_ACCOUNT_ID: account_id, CLOUDFLARE_GATEWAY_ID: gateway_id},
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async def resolve(*, ctx: AuthContext, credential: Credential | None, signal: AbortSignal) -> AuthResult | None:
|
|
97
|
+
resolved = await _resolve_cloudflare_env("ai-gateway", ctx, credential, signal)
|
|
98
|
+
if resolved is None:
|
|
99
|
+
return None
|
|
100
|
+
return AuthResult(
|
|
101
|
+
ModelAuth(
|
|
102
|
+
headers={
|
|
103
|
+
"cf-aig-authorization": f"Bearer {resolved.api_key}",
|
|
104
|
+
"Authorization": None,
|
|
105
|
+
"x-api-key": None,
|
|
106
|
+
}
|
|
107
|
+
),
|
|
108
|
+
env=resolved.env,
|
|
109
|
+
source=resolved.source,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return ApiKeyAuth("Cloudflare API key", resolve, login=login)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Materialize Cloudflare account/gateway placeholders in model base URLs before dispatch."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from typing import Any, TypeVar
|
|
8
|
+
|
|
9
|
+
from ..event_stream import AssistantMessageEventStream
|
|
10
|
+
from ..types import ClassifierContext, ClassifierModel, ClassifierResult, Model, TranscriptContext
|
|
11
|
+
|
|
12
|
+
__all__ = ["resolve_cloudflare_model", "cloudflare_streams", "cloudflare_classifier"]
|
|
13
|
+
|
|
14
|
+
CLOUDFLARE_ACCOUNT_ID = "CLOUDFLARE_ACCOUNT_ID"
|
|
15
|
+
CLOUDFLARE_GATEWAY_ID = "CLOUDFLARE_GATEWAY_ID"
|
|
16
|
+
|
|
17
|
+
_ModelT = TypeVar("_ModelT")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def resolve_cloudflare_model(model: _ModelT, env: Mapping[str, str] | None) -> _ModelT:
|
|
21
|
+
"""Replace ``{CLOUDFLARE_ACCOUNT_ID}``/``{CLOUDFLARE_GATEWAY_ID}`` in ``model.base_url`` from ``env``.
|
|
22
|
+
|
|
23
|
+
Unresolved placeholders are kept; the model is returned unchanged when nothing changes.
|
|
24
|
+
"""
|
|
25
|
+
if not env:
|
|
26
|
+
return model
|
|
27
|
+
base_url: str = model.base_url # type: ignore[attr-defined]
|
|
28
|
+
resolved = base_url
|
|
29
|
+
for name in (CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_GATEWAY_ID):
|
|
30
|
+
value = env.get(name)
|
|
31
|
+
placeholder = "{" + name + "}"
|
|
32
|
+
resolved = resolved.replace(placeholder, value if value is not None else placeholder)
|
|
33
|
+
if resolved == base_url:
|
|
34
|
+
return model
|
|
35
|
+
return dataclasses.replace(model, base_url=resolved) # type: ignore[type-var]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _env(options: Mapping[str, Any] | None) -> Mapping[str, str] | None:
|
|
39
|
+
return options.get("env") if options else None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class _CloudflareStreams:
|
|
43
|
+
__slots__ = ("_streams",)
|
|
44
|
+
|
|
45
|
+
def __init__(self, streams: Any) -> None:
|
|
46
|
+
self._streams = streams
|
|
47
|
+
|
|
48
|
+
def stream(
|
|
49
|
+
self, model: Model, context: TranscriptContext, options: Mapping[str, Any] | None = None
|
|
50
|
+
) -> AssistantMessageEventStream:
|
|
51
|
+
return self._streams.stream(resolve_cloudflare_model(model, _env(options)), context, options)
|
|
52
|
+
|
|
53
|
+
def stream_simple(
|
|
54
|
+
self, model: Model, context: TranscriptContext, options: Mapping[str, Any] | None = None
|
|
55
|
+
) -> AssistantMessageEventStream:
|
|
56
|
+
return self._streams.stream_simple(resolve_cloudflare_model(model, _env(options)), context, options)
|
|
57
|
+
|
|
58
|
+
def __repr__(self) -> str:
|
|
59
|
+
return f"cloudflare_streams({self._streams!r})"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class _CloudflareClassifier:
|
|
63
|
+
__slots__ = ("_classifier",)
|
|
64
|
+
|
|
65
|
+
def __init__(self, classifier: Any) -> None:
|
|
66
|
+
self._classifier = classifier
|
|
67
|
+
|
|
68
|
+
async def classify(
|
|
69
|
+
self, model: ClassifierModel, context: ClassifierContext, options: Mapping[str, Any] | None = None
|
|
70
|
+
) -> ClassifierResult:
|
|
71
|
+
return await self._classifier.classify(resolve_cloudflare_model(model, _env(options)), context, options)
|
|
72
|
+
|
|
73
|
+
def __repr__(self) -> str:
|
|
74
|
+
return f"cloudflare_classifier({self._classifier!r})"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def cloudflare_streams(streams: Any) -> Any:
|
|
78
|
+
"""Wrap an API implementation so Cloudflare endpoint placeholders materialize from the resolved provider env."""
|
|
79
|
+
return _CloudflareStreams(streams)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def cloudflare_classifier(classifier: Any) -> Any:
|
|
83
|
+
"""Classifier counterpart of :func:`cloudflare_streams`."""
|
|
84
|
+
return _CloudflareClassifier(classifier)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Cloudflare Workers AI provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..api.registry import cloudflare_workers_ai_system_one_api, openai_completions_api
|
|
6
|
+
from ..auth.types import ProviderAuth
|
|
7
|
+
from ..models import Provider, create_provider
|
|
8
|
+
from .catalog import catalog_models
|
|
9
|
+
from .cloudflare_auth import cloudflare_workers_ai_auth
|
|
10
|
+
from .cloudflare_stream import cloudflare_classifier, cloudflare_streams
|
|
11
|
+
|
|
12
|
+
__all__ = ["cloudflare_workers_ai_provider"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def cloudflare_workers_ai_provider() -> Provider:
|
|
16
|
+
"""Cloudflare Workers AI: ``CLOUDFLARE_API_KEY`` plus ``CLOUDFLARE_ACCOUNT_ID``."""
|
|
17
|
+
return create_provider(
|
|
18
|
+
id="cloudflare-workers-ai",
|
|
19
|
+
name="Cloudflare Workers AI",
|
|
20
|
+
auth=ProviderAuth(api_key=cloudflare_workers_ai_auth()),
|
|
21
|
+
models=lambda: catalog_models("cloudflare-workers-ai"),
|
|
22
|
+
api=cloudflare_streams(openai_completions_api()),
|
|
23
|
+
classifiers={
|
|
24
|
+
"cloudflare-workers-ai-system-one": cloudflare_classifier(cloudflare_workers_ai_system_one_api()),
|
|
25
|
+
},
|
|
26
|
+
)
|