Skip to content

Commit b6b9c2b

Browse files
lijunliu-ghCopilot
andcommitted
feat: add Azure OpenAI provider support
- Add AzureOpenAIProvider class using openai.AzureOpenAI client - Auto-detect Azure when AZURE_OPENAI_ENDPOINT env var is set - Add AZURE_OPENAI_API_VERSION support (default: 2024-08-01-preview) - Update .env.example with Azure config section - Update docstring with Azure setup instructions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c63867a commit b6b9c2b

2 files changed

Lines changed: 124 additions & 5 deletions

File tree

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ OPENAI_API_KEY=
44
OPENAI_MODEL=gpt-4.1-mini
55
OPENAI_BASE_URL=
66

7+
# Azure OpenAI (set AZURE_OPENAI_ENDPOINT to auto-select this provider)
8+
# AZURE_OPENAI_ENDPOINT=https://<your-resource>.openai.azure.com
9+
# OPENAI_API_KEY=<your-azure-api-key>
10+
# OPENAI_MODEL=<your-deployment-name> # e.g. gpt-4o
11+
# AZURE_OPENAI_API_VERSION=2024-08-01-preview
12+
713
# OpenRouter (optional, alternative multi-model gateway)
814
OPENROUTER_API_KEY=
915

python_backend/llm_provider.py

Lines changed: 118 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
export OPENAI_API_KEY="sk-..."
99
export OPENAI_MODEL="gpt-4.1-mini" # or o3, o4-mini, codex-mini-latest
1010
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+
1117
# Anthropic Claude
1218
export ANTHROPIC_API_KEY="sk-ant-..."
1319
export OPENAI_MODEL="claude-sonnet-4-20250514"
@@ -129,6 +135,107 @@ def chat(self, system_prompt: str, user_prompt: str) -> str:
129135
return str(content)
130136

131137

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+
132239
# ---------------------------------------------------------------------------
133240
# Anthropic Claude
134241
# ---------------------------------------------------------------------------
@@ -292,12 +399,18 @@ def get_default_provider() -> LLMProvider:
292399
"""Return the best provider based on environment variables.
293400
294401
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.)
300408
"""
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+
301414
if os.getenv("OPENROUTER_API_KEY"):
302415
model = os.getenv("OPENAI_MODEL") or "openai/gpt-4.1-mini"
303416
logger.info("Auto-detected provider OpenRouterProvider for model=%s", model)

0 commit comments

Comments
 (0)