|
8 | 8 | export OPENAI_API_KEY="sk-..." |
9 | 9 | export OPENAI_MODEL="gpt-4.1-mini" # or o3, o4-mini, codex-mini-latest |
10 | 10 |
|
| 11 | + # Azure OpenAI |
| 12 | + export AZURE_OPENAI_ENDPOINT="https://<resource>.openai.azure.com" |
| 13 | + export OPENAI_API_KEY="<azure-api-key>" |
| 14 | + export OPENAI_MODEL="<deployment-name>" # your deployment name, e.g. gpt-4o |
| 15 | + export AZURE_OPENAI_API_VERSION="2024-08-01-preview" # optional |
| 16 | +
|
11 | 17 | # Anthropic Claude |
12 | 18 | export ANTHROPIC_API_KEY="sk-ant-..." |
13 | 19 | export OPENAI_MODEL="claude-sonnet-4-20250514" |
@@ -129,6 +135,107 @@ def chat(self, system_prompt: str, user_prompt: str) -> str: |
129 | 135 | return str(content) |
130 | 136 |
|
131 | 137 |
|
| 138 | +# --------------------------------------------------------------------------- |
| 139 | +# Azure OpenAI |
| 140 | +# --------------------------------------------------------------------------- |
| 141 | + |
| 142 | + |
| 143 | +class AzureOpenAIProvider: |
| 144 | + """Provider backed by Azure OpenAI Service. |
| 145 | +
|
| 146 | + Requires the ``openai`` package (already a dependency). |
| 147 | + Set ``AZURE_OPENAI_ENDPOINT`` to enable auto-detection. |
| 148 | +
|
| 149 | + Required env vars:: |
| 150 | +
|
| 151 | + AZURE_OPENAI_ENDPOINT https://<resource>.openai.azure.com |
| 152 | + OPENAI_API_KEY your Azure OpenAI API key |
| 153 | + OPENAI_MODEL your deployment name (e.g. gpt-4o) |
| 154 | +
|
| 155 | + Optional:: |
| 156 | +
|
| 157 | + AZURE_OPENAI_API_VERSION API version (default: 2024-08-01-preview) |
| 158 | + """ |
| 159 | + |
| 160 | + _DEFAULT_API_VERSION = "2024-08-01-preview" |
| 161 | + _REASONING_PREFIXES = ("o1", "o3", "o4", "codex") |
| 162 | + |
| 163 | + def __init__( |
| 164 | + self, |
| 165 | + api_key: str | None = None, |
| 166 | + endpoint: str | None = None, |
| 167 | + model: str | None = None, |
| 168 | + api_version: str | None = None, |
| 169 | + ) -> None: |
| 170 | + AzureOpenAI = importlib.import_module("openai").AzureOpenAI |
| 171 | + |
| 172 | + resolved_key = api_key or os.getenv("OPENAI_API_KEY") |
| 173 | + if not resolved_key: |
| 174 | + raise RuntimeError( |
| 175 | + "OPENAI_API_KEY is not set. " |
| 176 | + "Set OPENAI_API_KEY to your Azure OpenAI API key." |
| 177 | + ) |
| 178 | + resolved_endpoint = endpoint or os.getenv("AZURE_OPENAI_ENDPOINT") |
| 179 | + if not resolved_endpoint: |
| 180 | + raise RuntimeError( |
| 181 | + "AZURE_OPENAI_ENDPOINT is not set. " |
| 182 | + "Set it to https://<resource>.openai.azure.com" |
| 183 | + ) |
| 184 | + resolved_version = ( |
| 185 | + api_version |
| 186 | + or os.getenv("AZURE_OPENAI_API_VERSION") |
| 187 | + or self._DEFAULT_API_VERSION |
| 188 | + ) |
| 189 | + # Strip path/query from endpoint — AzureOpenAI expects the base URL only |
| 190 | + from urllib.parse import urlparse |
| 191 | + parsed = urlparse(resolved_endpoint) |
| 192 | + base_endpoint = f"{parsed.scheme}://{parsed.netloc}" |
| 193 | + |
| 194 | + self._client: Any = AzureOpenAI( |
| 195 | + api_key=resolved_key, |
| 196 | + azure_endpoint=base_endpoint, |
| 197 | + api_version=resolved_version, |
| 198 | + ) |
| 199 | + self._model = model or os.getenv("OPENAI_MODEL") or "gpt-4o" |
| 200 | + logger.info( |
| 201 | + "AzureOpenAIProvider: endpoint=%s model=%s api_version=%s", |
| 202 | + base_endpoint, |
| 203 | + self._model, |
| 204 | + resolved_version, |
| 205 | + ) |
| 206 | + |
| 207 | + def _is_reasoning_model(self) -> bool: |
| 208 | + return self._model.startswith(self._REASONING_PREFIXES) |
| 209 | + |
| 210 | + def chat(self, system_prompt: str, user_prompt: str) -> str: |
| 211 | + logger.info("Requesting deck JSON from Azure model=%s", self._model) |
| 212 | + reasoning = self._is_reasoning_model() |
| 213 | + |
| 214 | + instruction_role = "developer" if reasoning else "system" |
| 215 | + messages = [ |
| 216 | + {"role": instruction_role, "content": system_prompt}, |
| 217 | + {"role": "user", "content": user_prompt}, |
| 218 | + ] |
| 219 | + |
| 220 | + params: dict[str, Any] = { |
| 221 | + "model": self._model, |
| 222 | + "response_format": {"type": "json_object"}, |
| 223 | + "messages": messages, |
| 224 | + } |
| 225 | + |
| 226 | + if reasoning: |
| 227 | + effort = os.getenv("OPENAI_REASONING_EFFORT", "medium") |
| 228 | + params["reasoning_effort"] = effort |
| 229 | + else: |
| 230 | + params["temperature"] = 0.3 |
| 231 | + |
| 232 | + response = self._client.chat.completions.create(**params) |
| 233 | + content = response.choices[0].message.content if response.choices else None |
| 234 | + if not content: |
| 235 | + raise RuntimeError("Model returned no content.") |
| 236 | + return str(content) |
| 237 | + |
| 238 | + |
132 | 239 | # --------------------------------------------------------------------------- |
133 | 240 | # Anthropic Claude |
134 | 241 | # --------------------------------------------------------------------------- |
@@ -292,12 +399,18 @@ def get_default_provider() -> LLMProvider: |
292 | 399 | """Return the best provider based on environment variables. |
293 | 400 |
|
294 | 401 | Detection order: |
295 | | - 1. If ``OPENROUTER_API_KEY`` is set -> OpenRouterProvider |
296 | | - 2. Read ``OPENAI_MODEL`` (or default ``gpt-4.1-mini``) |
297 | | - 3. If model starts with ``claude`` -> AnthropicProvider |
298 | | - 4. If model starts with ``gemini`` -> GeminiProvider |
299 | | - 5. Otherwise -> OpenAIProvider (works for OpenAI, Qwen, DeepSeek, GLM, MiniMax, etc.) |
| 402 | + 1. If ``AZURE_OPENAI_ENDPOINT`` is set -> AzureOpenAIProvider |
| 403 | + 2. If ``OPENROUTER_API_KEY`` is set -> OpenRouterProvider |
| 404 | + 3. Read ``OPENAI_MODEL`` (or default ``gpt-4.1-mini``) |
| 405 | + 4. If model starts with ``claude`` -> AnthropicProvider |
| 406 | + 5. If model starts with ``gemini`` -> GeminiProvider |
| 407 | + 6. Otherwise -> OpenAIProvider (works for OpenAI, Qwen, DeepSeek, GLM, MiniMax, etc.) |
300 | 408 | """ |
| 409 | + if os.getenv("AZURE_OPENAI_ENDPOINT"): |
| 410 | + model = os.getenv("OPENAI_MODEL") or "gpt-4o" |
| 411 | + logger.info("Auto-detected provider AzureOpenAIProvider for model=%s", model) |
| 412 | + return AzureOpenAIProvider(model=model) |
| 413 | + |
301 | 414 | if os.getenv("OPENROUTER_API_KEY"): |
302 | 415 | model = os.getenv("OPENAI_MODEL") or "openai/gpt-4.1-mini" |
303 | 416 | logger.info("Auto-detected provider OpenRouterProvider for model=%s", model) |
|
0 commit comments