reactifact 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. reactifact/__init__.py +96 -0
  2. reactifact/__main__.py +10 -0
  3. reactifact/_extras.py +36 -0
  4. reactifact/agents.py +173 -0
  5. reactifact/artifacts.py +130 -0
  6. reactifact/branching.py +255 -0
  7. reactifact/budget.py +41 -0
  8. reactifact/chat.py +373 -0
  9. reactifact/checkpoints.py +329 -0
  10. reactifact/cli/__init__.py +73 -0
  11. reactifact/cli/branch.py +77 -0
  12. reactifact/cli/common.py +67 -0
  13. reactifact/cli/context.py +53 -0
  14. reactifact/cli/graph.py +21 -0
  15. reactifact/cli/replay.py +69 -0
  16. reactifact/cli/scenario.py +94 -0
  17. reactifact/cli/trace.py +45 -0
  18. reactifact/commit.py +97 -0
  19. reactifact/commit_log.py +235 -0
  20. reactifact/consume.py +96 -0
  21. reactifact/context.py +599 -0
  22. reactifact/effects.py +232 -0
  23. reactifact/eval.py +319 -0
  24. reactifact/events.py +34 -0
  25. reactifact/interrupt.py +22 -0
  26. reactifact/llm_agent.py +172 -0
  27. reactifact/operations.py +192 -0
  28. reactifact/patches.py +112 -0
  29. reactifact/produce.py +226 -0
  30. reactifact/prompts.py +111 -0
  31. reactifact/providers/__init__.py +153 -0
  32. reactifact/providers/_retry.py +61 -0
  33. reactifact/providers/anthropic.py +182 -0
  34. reactifact/providers/azure.py +31 -0
  35. reactifact/providers/cerebras.py +11 -0
  36. reactifact/providers/chat.py +417 -0
  37. reactifact/providers/contracts.py +105 -0
  38. reactifact/providers/deepseek.py +11 -0
  39. reactifact/providers/fake.py +40 -0
  40. reactifact/providers/fireworks.py +17 -0
  41. reactifact/providers/gemini.py +284 -0
  42. reactifact/providers/github_models.py +13 -0
  43. reactifact/providers/groq.py +18 -0
  44. reactifact/providers/image.py +157 -0
  45. reactifact/providers/mistral.py +17 -0
  46. reactifact/providers/nvidia.py +18 -0
  47. reactifact/providers/ollama.py +18 -0
  48. reactifact/providers/openai.py +44 -0
  49. reactifact/providers/openrouter.py +70 -0
  50. reactifact/providers/perplexity.py +11 -0
  51. reactifact/providers/qwen.py +17 -0
  52. reactifact/providers/speech.py +347 -0
  53. reactifact/providers/together.py +17 -0
  54. reactifact/providers/video.py +407 -0
  55. reactifact/providers/xai.py +11 -0
  56. reactifact/providers/zai.py +11 -0
  57. reactifact/py.typed +0 -0
  58. reactifact/recipes/__init__.py +63 -0
  59. reactifact/recipes/inputs.py +34 -0
  60. reactifact/recipes/memory.py +166 -0
  61. reactifact/recipes/resolve.py +51 -0
  62. reactifact/recipes/rollback.py +87 -0
  63. reactifact/recipes/search.py +81 -0
  64. reactifact/recipes/skills.py +108 -0
  65. reactifact/recipes/status.py +79 -0
  66. reactifact/recipes/text.py +202 -0
  67. reactifact/relations.py +104 -0
  68. reactifact/replay.py +187 -0
  69. reactifact/resources.py +45 -0
  70. reactifact/runtime.py +498 -0
  71. reactifact/scheduler.py +188 -0
  72. reactifact/session.py +75 -0
  73. reactifact/sources.py +498 -0
  74. reactifact/streaming.py +58 -0
  75. reactifact/structured.py +245 -0
  76. reactifact/testing/__init__.py +48 -0
  77. reactifact/testing/assertions.py +326 -0
  78. reactifact/testing/exceptions.py +27 -0
  79. reactifact/testing/fault.py +164 -0
  80. reactifact/testing/lab.py +350 -0
  81. reactifact/testing/mock.py +166 -0
  82. reactifact/testing/record.py +50 -0
  83. reactifact/testing/registry.py +87 -0
  84. reactifact/tool_use.py +528 -0
  85. reactifact/tools.py +111 -0
  86. reactifact/tracing/__init__.py +29 -0
  87. reactifact/tracing/langfuse.py +125 -0
  88. reactifact/tracing/models.py +93 -0
  89. reactifact/tracing/postgres.py +220 -0
  90. reactifact/tracing/store.py +254 -0
  91. reactifact/tracing/templates/ui.html +196 -0
  92. reactifact/tracing/templates/ui_run.html +264 -0
  93. reactifact/tracing/tracer.py +370 -0
  94. reactifact/tracing/web.py +117 -0
  95. reactifact/triggers.py +41 -0
  96. reactifact/viz.py +248 -0
  97. reactifact/web.py +117 -0
  98. reactifact-0.6.0.dist-info/METADATA +226 -0
  99. reactifact-0.6.0.dist-info/RECORD +103 -0
  100. reactifact-0.6.0.dist-info/WHEEL +5 -0
  101. reactifact-0.6.0.dist-info/entry_points.txt +2 -0
  102. reactifact-0.6.0.dist-info/licenses/LICENSE +21 -0
  103. reactifact-0.6.0.dist-info/top_level.txt +1 -0
reactifact/prompts.py ADDED
@@ -0,0 +1,111 @@
1
+ """reactifact.prompts — minimal, strict prompt templating (§68).
2
+
3
+ `PromptTemplate` renders a `{var}`-style template with *declared* variables:
4
+ at construction time the placeholders are parsed, at render time a missing
5
+ variable is a `KeyError` (never a silent `format` leak), and `{{`/`}}` stay
6
+ literal braces. Domain model attributes are supported, so a template can take
7
+ a whole artifact: `"Research {question.text} in {topic}"` and be rendered with
8
+ `template.render(question=…, topic=…)`.
9
+
10
+ `MessagesPrompt` is the same idea for a chat sequence of `(role, template)`
11
+ rows — it renders to `list[Message]` ready for an LLM request.
12
+
13
+ This is deliberately small and dependency-free: it sits between the app's
14
+ "domain strings" and `structured_llm`/`LLMAgent.system`, without claiming to be
15
+ a general prompting framework.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import re
21
+ import string
22
+ from collections.abc import Mapping, Sequence
23
+ from typing import Any, cast
24
+
25
+ from .providers import Message, Role
26
+
27
+ _FIELD = re.compile(r"[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*\Z")
28
+
29
+ _formatter = string.Formatter()
30
+
31
+
32
+ def _root_fields(template: str) -> frozenset[str]:
33
+ """The top-level variable names referenced by the template."""
34
+ roots: set[str] = set()
35
+ for _, field_name, _, _ in _formatter.parse(template):
36
+ if field_name is None or field_name == "":
37
+ continue
38
+ if _FIELD.match(field_name):
39
+ roots.add(field_name.split(".")[0])
40
+ return frozenset(roots)
41
+
42
+
43
+ class PromptTemplate:
44
+ """A strict `{var}` template over the values passed to `render`."""
45
+
46
+ def __init__(
47
+ self,
48
+ template: str,
49
+ *,
50
+ defaults: Mapping[str, Any] | None = None,
51
+ ):
52
+ if not isinstance(template, str) or not template.strip():
53
+ raise ValueError("prompt template must be a non-empty string")
54
+ self._template = template
55
+ self._defaults = dict(defaults or {})
56
+ self.variables = _root_fields(template)
57
+
58
+ @property
59
+ def template(self) -> str:
60
+ return self._template
61
+
62
+ def render(self, **values: Any) -> str:
63
+ """Fills the placeholders; a missing declared variable is a `KeyError`."""
64
+ merged = {**self._defaults, **values}
65
+ missing = self.variables - merged.keys()
66
+ if missing:
67
+ raise KeyError(f"missing prompt variables: {', '.join(sorted(missing))}")
68
+ try:
69
+ return self._template.format(**merged)
70
+ except (AttributeError, IndexError, KeyError) as exc:
71
+ raise ValueError(
72
+ f"failed to render prompt (template {self._template!r}): {exc}"
73
+ ) from exc
74
+
75
+ def __repr__(self) -> str:
76
+ return f"PromptTemplate(variables={sorted(self.variables)})"
77
+
78
+
79
+ class MessagesPrompt:
80
+ """A chat prompt: an ordered set of `(role, template)` rows.
81
+
82
+ Renders to `list[Message]`; every row sees the same variables and a missing
83
+ variable anywhere is a `KeyError`.
84
+ """
85
+
86
+ def __init__(self, messages: Sequence[tuple[str, str]]):
87
+ if not messages:
88
+ raise ValueError(
89
+ "MessagesPrompt requires at least one (role, template) row"
90
+ )
91
+ self._rows: list[tuple[Role, PromptTemplate]] = []
92
+ for role, template in messages:
93
+ if role not in ("system", "user", "assistant", "tool"):
94
+ raise ValueError(f"unknown message role in MessagesPrompt: {role!r}")
95
+ self._rows.append((cast(Role, role), PromptTemplate(template)))
96
+ variables: set[str] = set()
97
+ for _, row in self._rows:
98
+ variables |= set(row.variables)
99
+ self.variables = frozenset(variables)
100
+
101
+ def render(self, **values: Any) -> list[Message]:
102
+ return [
103
+ Message(role=role, content=template.render(**values))
104
+ for role, template in self._rows
105
+ ]
106
+
107
+ def __repr__(self) -> str:
108
+ return f"MessagesPrompt(roles={[r for r, _ in self._rows]})"
109
+
110
+
111
+ __all__ = ["MessagesPrompt", "PromptTemplate"]
@@ -0,0 +1,153 @@
1
+ """Providers package: contracts + implementations (chat, embeddings, images, fakes).
2
+
3
+ The app configures the needed providers when initializing resources;
4
+ the core public API (`reactifact`) exports only contracts.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from .anthropic import AnthropicProvider, anthropic_llm
12
+ from .azure import azure_llm
13
+ from .cerebras import cerebras_llm
14
+ from .chat import (
15
+ OpenAICompatEmbedder,
16
+ OpenAICompatProvider,
17
+ embedder_from_env,
18
+ llm_from_env,
19
+ )
20
+ from .contracts import (
21
+ EmbeddingProvider,
22
+ LLMProvider,
23
+ LLMRequest,
24
+ LLMResponse,
25
+ LLMResponseChunk,
26
+ Message,
27
+ Role,
28
+ )
29
+ from .deepseek import deepseek_llm
30
+ from .fake import FakeEmbedder, FakeLLM
31
+ from .fireworks import fireworks_embedder, fireworks_llm
32
+ from .gemini import GeminiImageProvider, GeminiProvider, gemini_image, gemini_llm
33
+ from .github_models import github_models_llm
34
+ from .groq import groq_llm, groq_transcriber
35
+ from .image import (
36
+ ImageProvider,
37
+ OpenAICompatImageProvider,
38
+ OpenRouterImageProvider,
39
+ image_from_env,
40
+ )
41
+ from .mistral import mistral_embedder, mistral_llm
42
+ from .nvidia import nvidia_embedder, nvidia_nim_llm
43
+ from .ollama import ollama_llm
44
+ from .openai import openai_embedder, openai_llm
45
+ from .openrouter import (
46
+ openrouter_embedder,
47
+ openrouter_image,
48
+ openrouter_llm,
49
+ openrouter_speech,
50
+ )
51
+ from .perplexity import perplexity_llm
52
+ from .qwen import qwen_embedder, qwen_llm
53
+ from .speech import (
54
+ OpenAICompatSpeech,
55
+ OpenAICompatTranscriber,
56
+ SpeechProvider,
57
+ TranscriberProvider,
58
+ speech_from_env,
59
+ transcriber_from_env,
60
+ )
61
+ from .together import together_embedder, together_llm
62
+ from .video import (
63
+ LumaVideoProvider,
64
+ OpenRouterVideoProvider,
65
+ RunwayVideoProvider,
66
+ SoraVideoProvider,
67
+ VideoProvider,
68
+ VideoResult,
69
+ video_from_env,
70
+ )
71
+ from .xai import xai_llm
72
+ from .zai import zai_llm
73
+
74
+
75
+ def from_env(**overrides: Any) -> LLMProvider | None:
76
+ """One-call provider selection: `OPENROUTER_API_KEY` first, else a local
77
+ OpenAI-compatible endpoint via `OPENAI_BASE_URL`, else `None` (offline).
78
+
79
+ This is the exact two-branch selection every example in this repo hand-
80
+ rolls as a local `build_llm()` (deliberately, per example, so a reader
81
+ sees the wiring — see `examples/*/main.py`); use this instead when you
82
+ just want the common default in your own app without copying that block.
83
+ Overrides (`model`, `max_tokens`, `temperature`, ...) are forwarded to
84
+ whichever provider ends up selected.
85
+ """
86
+ return openrouter_llm(**overrides) or llm_from_env(**overrides)
87
+
88
+
89
+ __all__ = [
90
+ "AnthropicProvider",
91
+ "EmbeddingProvider",
92
+ "FakeEmbedder",
93
+ "FakeLLM",
94
+ "GeminiImageProvider",
95
+ "GeminiProvider",
96
+ "ImageProvider",
97
+ "LLMProvider",
98
+ "LLMRequest",
99
+ "LLMResponse",
100
+ "LLMResponseChunk",
101
+ "LumaVideoProvider",
102
+ "Message",
103
+ "OpenAICompatEmbedder",
104
+ "OpenAICompatImageProvider",
105
+ "OpenAICompatProvider",
106
+ "OpenAICompatSpeech",
107
+ "OpenAICompatTranscriber",
108
+ "OpenRouterImageProvider",
109
+ "OpenRouterVideoProvider",
110
+ "Role",
111
+ "RunwayVideoProvider",
112
+ "SoraVideoProvider",
113
+ "SpeechProvider",
114
+ "TranscriberProvider",
115
+ "VideoProvider",
116
+ "VideoResult",
117
+ "anthropic_llm",
118
+ "azure_llm",
119
+ "cerebras_llm",
120
+ "deepseek_llm",
121
+ "embedder_from_env",
122
+ "fireworks_embedder",
123
+ "fireworks_llm",
124
+ "from_env",
125
+ "gemini_image",
126
+ "gemini_llm",
127
+ "github_models_llm",
128
+ "groq_llm",
129
+ "groq_transcriber",
130
+ "image_from_env",
131
+ "llm_from_env",
132
+ "mistral_embedder",
133
+ "mistral_llm",
134
+ "nvidia_embedder",
135
+ "nvidia_nim_llm",
136
+ "ollama_llm",
137
+ "openai_embedder",
138
+ "openai_llm",
139
+ "openrouter_embedder",
140
+ "openrouter_image",
141
+ "openrouter_llm",
142
+ "openrouter_speech",
143
+ "perplexity_llm",
144
+ "qwen_embedder",
145
+ "qwen_llm",
146
+ "speech_from_env",
147
+ "together_embedder",
148
+ "together_llm",
149
+ "transcriber_from_env",
150
+ "video_from_env",
151
+ "xai_llm",
152
+ "zai_llm",
153
+ ]
@@ -0,0 +1,61 @@
1
+ """Shared network-retry helper for HTTP-based LLM providers.
2
+
3
+ Every `complete()` implementation in this package hits the same class of
4
+ transient failures (429 rate limits, 5xx, connection resets/timeouts) and,
5
+ before this module, retried none of them — a single blip failed the call
6
+ outright. Not in `contracts.py`: the core stays free of the httpx dependency
7
+ (see that module's docstring); this lives in the `providers` package instead
8
+ and is imported by the concrete implementations that need it.
9
+
10
+ Scoped to `complete()` only, deliberately not `stream()`: a streaming call
11
+ can fail after already yielding chunks to the caller, and retrying by
12
+ restarting the request would silently duplicate what was already streamed.
13
+ `complete()` fails atomically (nothing to un-yield), so it's safe to retry
14
+ in full.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import asyncio
20
+ from collections.abc import Awaitable, Callable
21
+ from typing import TypeVar
22
+
23
+ import httpx
24
+
25
+ T = TypeVar("T")
26
+
27
+ #: HTTP statuses worth retrying: rate limit + server-side errors. Anything
28
+ #: else (400/401/403/404, ...) is a request/auth problem retrying won't fix.
29
+ RETRYABLE_STATUS = frozenset({429, 500, 502, 503, 504})
30
+
31
+
32
+ async def with_retry(
33
+ call: Callable[[], Awaitable[T]],
34
+ *,
35
+ attempts: int = 3,
36
+ base_delay: float = 0.5,
37
+ ) -> T:
38
+ """Retries `call` on transient HTTP failures with exponential backoff.
39
+
40
+ Retries `httpx.TransportError` (connection reset, timeout, DNS...) and
41
+ `httpx.HTTPStatusError` whose status is in `RETRYABLE_STATUS`; any other
42
+ exception — including a non-retryable status — propagates on the first
43
+ attempt. `attempts=1` disables retrying entirely.
44
+ """
45
+ for attempt in range(attempts):
46
+ try:
47
+ return await call()
48
+ except httpx.HTTPStatusError as exc:
49
+ if (
50
+ exc.response.status_code not in RETRYABLE_STATUS
51
+ or attempt + 1 >= attempts
52
+ ):
53
+ raise
54
+ except httpx.TransportError:
55
+ if attempt + 1 >= attempts:
56
+ raise
57
+ await asyncio.sleep(base_delay * (2**attempt))
58
+ raise AssertionError("unreachable: loop always returns or raises")
59
+
60
+
61
+ __all__ = ["RETRYABLE_STATUS", "with_retry"]
@@ -0,0 +1,182 @@
1
+ """Anthropic — Messages API (not OpenAI-compatible: a separate contract)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import AsyncIterator
7
+ from typing import Any
8
+
9
+ import httpx
10
+
11
+ from ._retry import with_retry
12
+ from .contracts import (
13
+ LLMProvider,
14
+ LLMRequest,
15
+ LLMResponse,
16
+ LLMResponseChunk,
17
+ auth_value,
18
+ )
19
+
20
+
21
+ class AnthropicProvider(LLMProvider):
22
+ """Provider for the Anthropic Messages API (/v1/messages, x-api-key).
23
+
24
+ Auth defaults to the Anthropic convention (`x-api-key` header, raw key) but
25
+ is configurable like the other providers (auth_header/auth_scheme/proxy).
26
+ `response_format` does not exist in Anthropic — structured output is obtained
27
+ via plain JSON in the text (the tolerant parse_structured covers this).
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ api_key: str,
33
+ model: str = "claude-3-5-sonnet-latest",
34
+ base_url: str = "https://api.anthropic.com/v1",
35
+ timeout: float = 90.0,
36
+ transport: Any | None = None,
37
+ extra_headers: dict[str, str] | None = None,
38
+ max_tokens: int | None = None,
39
+ temperature: float | None = None,
40
+ proxy: str | None = None,
41
+ auth_header: str = "x-api-key",
42
+ auth_scheme: str | None = None,
43
+ retry_attempts: int = 3,
44
+ ):
45
+ self.api_key = api_key
46
+ self.model = model
47
+ self.base_url = base_url.rstrip("/")
48
+ self._timeout = timeout
49
+ # Anthropic's Messages API requires `max_tokens`; 4096 is the library
50
+ # default. Pass an explicit value to change it, or override per call.
51
+ self.max_tokens = max_tokens if max_tokens is not None else 4096
52
+ self.temperature = temperature
53
+ self.retry_attempts = retry_attempts
54
+ self._headers = {
55
+ "anthropic-version": "2023-06-01",
56
+ "content-type": "application/json",
57
+ **(extra_headers or {}),
58
+ }
59
+ self._headers.setdefault(auth_header.lower(), auth_value(api_key, auth_scheme))
60
+ self._transport = transport
61
+ self._proxy = proxy
62
+ self._client: httpx.AsyncClient | None = None
63
+
64
+ def _get_client(self) -> httpx.AsyncClient:
65
+ if self._client is None:
66
+ self._client = httpx.AsyncClient(
67
+ timeout=self._timeout,
68
+ transport=self._transport,
69
+ headers=self._headers,
70
+ proxy=self._proxy,
71
+ )
72
+ return self._client
73
+
74
+ def _payload(self, request: LLMRequest, stream: bool) -> dict[str, Any]:
75
+ system = "\n\n".join(m.content for m in request.messages if m.role == "system")
76
+ messages = [
77
+ {"role": m.role, "content": m.content}
78
+ for m in request.messages
79
+ if m.role in ("user", "assistant")
80
+ ]
81
+ if not messages:
82
+ messages = [{"role": "user", "content": ""}]
83
+ temperature = (
84
+ request.temperature if request.temperature is not None else self.temperature
85
+ )
86
+ payload: dict[str, Any] = {
87
+ "model": request.extra.get("model") or self.model,
88
+ "max_tokens": (
89
+ request.max_tokens
90
+ if request.max_tokens is not None
91
+ else self.max_tokens
92
+ ),
93
+ "messages": messages,
94
+ "stream": stream,
95
+ }
96
+ if temperature is not None:
97
+ payload["temperature"] = temperature
98
+ if system:
99
+ payload["system"] = system
100
+ if request.stop:
101
+ payload["stop_sequences"] = request.stop
102
+ for key, value in request.extra.items():
103
+ if key != "model":
104
+ payload[key] = value
105
+ return payload
106
+
107
+ async def complete(self, request: LLMRequest) -> LLMResponse:
108
+ async def _call() -> LLMResponse:
109
+ response = await self._get_client().post(
110
+ f"{self.base_url}/messages",
111
+ json=self._payload(request, stream=False),
112
+ )
113
+ response.raise_for_status()
114
+ data = response.json()
115
+ text = "".join(
116
+ block.get("text", "")
117
+ for block in data.get("content", [])
118
+ if block.get("type") == "text"
119
+ )
120
+ return LLMResponse(
121
+ text=text,
122
+ raw=data,
123
+ finish_reason=data.get("stop_reason"),
124
+ usage=data.get("usage", {}),
125
+ )
126
+
127
+ return await with_retry(_call, attempts=self.retry_attempts)
128
+
129
+ async def stream(self, request: LLMRequest) -> AsyncIterator[LLMResponseChunk]:
130
+ async with self._get_client().stream(
131
+ "POST",
132
+ f"{self.base_url}/messages",
133
+ json=self._payload(request, stream=True),
134
+ ) as response:
135
+ response.raise_for_status()
136
+ async for line in response.aiter_lines():
137
+ if not line.startswith("data:"):
138
+ continue
139
+ data = line[len("data:") :].strip()
140
+ if not data or data == "[DONE]":
141
+ continue
142
+ try:
143
+ blob = json.loads(data)
144
+ except ValueError:
145
+ continue
146
+ if blob.get("type") == "content_block_delta":
147
+ delta = blob.get("delta", {})
148
+ text = delta.get("text")
149
+ if text:
150
+ yield LLMResponseChunk(text=text)
151
+
152
+ async def aclose(self) -> None:
153
+ if self._client is not None:
154
+ await self._client.aclose()
155
+ self._client = None
156
+
157
+
158
+ def anthropic_llm(
159
+ api_key: str | None = None,
160
+ model: str = "claude-3-5-sonnet-latest",
161
+ **kwargs: Any,
162
+ ) -> AnthropicProvider | None:
163
+ """Builds an Anthropic provider (key from ANTHROPIC_API_KEY).
164
+
165
+ Optional knobs in env or kwargs: ANTHROPIC_PROXY / proxy,
166
+ ANTHROPIC_AUTH_HEADER / auth_header (default x-api-key),
167
+ ANTHROPIC_AUTH_SCHEME / auth_scheme (default raw key).
168
+ """
169
+ import os
170
+
171
+ if api_key is None:
172
+ api_key = kwargs.get("api_key") or os.getenv("ANTHROPIC_API_KEY")
173
+ if not api_key:
174
+ return None
175
+ kwargs.setdefault("proxy", os.getenv("ANTHROPIC_PROXY") or None)
176
+ kwargs.setdefault(
177
+ "auth_header",
178
+ os.getenv("ANTHROPIC_AUTH_HEADER") or "x-api-key",
179
+ )
180
+ scheme = kwargs.get("auth_scheme") or os.getenv("ANTHROPIC_AUTH_SCHEME")
181
+ kwargs["auth_scheme"] = scheme if scheme else None
182
+ return AnthropicProvider(api_key=api_key, model=model, **kwargs)
@@ -0,0 +1,31 @@
1
+ """Azure OpenAI — chat (OpenAI-compatible, deployment-as-model)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from .chat import OpenAICompatProvider, _network_knobs
8
+
9
+
10
+ def azure_llm(
11
+ endpoint: str,
12
+ api_key: str | None = None,
13
+ deployment: str | None = None,
14
+ **kwargs: Any,
15
+ ) -> OpenAICompatProvider:
16
+ """OpenAI-compatible Azure provider.
17
+
18
+ `endpoint` is the resource URL, e.g.
19
+ https://my-resource.openai.azure.com/openai/deployments/my-deployment;
20
+ pass `deployment` or set AZURE_OPENAI_API_KEY / AZURE_OPENAI_DEPLOYMENT.
21
+ """
22
+ import os
23
+
24
+ if api_key is None:
25
+ api_key = kwargs.get("api_key") or os.getenv("AZURE_OPENAI_API_KEY")
26
+ if deployment is None:
27
+ deployment = os.getenv("AZURE_OPENAI_DEPLOYMENT")
28
+ merged = {**_network_knobs("AZURE", kwargs), **kwargs}
29
+ return OpenAICompatProvider(
30
+ base_url=endpoint, api_key=api_key, model=deployment, **merged
31
+ )
@@ -0,0 +1,11 @@
1
+ """Cerebras — ultra-fast inference (OpenAI-compatible)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .chat import _openai_compat_llm
6
+
7
+ cerebras_llm = _openai_compat_llm(
8
+ env_prefix="CEREBRAS",
9
+ default_model="llama-3.3-70b",
10
+ default_base_url="https://api.cerebras.ai/v1",
11
+ )