Skip to content

Commit 1e3fa5a

Browse files
authored
Add Azure OpenAI backend support (#20)
- Create AzureOpenAIClient class in rlm/clients/azure_openai.py - Register azure_openai backend in rlm/clients/__init__.py - Add azure_openai to ClientBackend type in rlm/core/types.py This allows users to use Azure OpenAI as a backend by providing azure_endpoint, api_key, api_version, and optionally azure_deployment.
1 parent 3efda5c commit 1e3fa5a

3 files changed

Lines changed: 148 additions & 2 deletions

File tree

‎rlm/clients/__init__.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ def get_client(
4444
from rlm.clients.anthropic import AnthropicClient
4545

4646
return AnthropicClient(**backend_kwargs)
47+
elif backend == "azure_openai":
48+
from rlm.clients.azure_openai import AzureOpenAIClient
49+
50+
return AzureOpenAIClient(**backend_kwargs)
4751
else:
4852
raise ValueError(
49-
f"Unknown backend: {backend}. Supported backends: ['openai', 'vllm', 'portkey', 'openrouter', 'litellm', 'anthropic']"
53+
f"Unknown backend: {backend}. Supported backends: ['openai', 'vllm', 'portkey', 'openrouter', 'litellm', 'anthropic', 'azure_openai']"
5054
)

‎rlm/clients/azure_openai.py‎

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import os
2+
from collections import defaultdict
3+
from typing import Any
4+
5+
import openai
6+
from dotenv import load_dotenv
7+
8+
from rlm.clients.base_lm import BaseLM
9+
from rlm.core.types import ModelUsageSummary, UsageSummary
10+
11+
load_dotenv()
12+
13+
# Load API key from environment variable
14+
DEFAULT_AZURE_OPENAI_API_KEY = os.getenv("AZURE_OPENAI_API_KEY")
15+
16+
17+
class AzureOpenAIClient(BaseLM):
18+
"""
19+
LM Client for running models with the Azure OpenAI API.
20+
"""
21+
22+
def __init__(
23+
self,
24+
api_key: str | None = None,
25+
model_name: str | None = None,
26+
azure_endpoint: str | None = None,
27+
api_version: str | None = None,
28+
azure_deployment: str | None = None,
29+
**kwargs,
30+
):
31+
super().__init__(model_name=model_name, **kwargs)
32+
33+
if api_key is None:
34+
api_key = DEFAULT_AZURE_OPENAI_API_KEY
35+
36+
if azure_endpoint is None:
37+
azure_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
38+
39+
if api_version is None:
40+
api_version = os.getenv("AZURE_OPENAI_API_VERSION", "2024-02-01")
41+
42+
if azure_endpoint is None:
43+
raise ValueError(
44+
"azure_endpoint is required for Azure OpenAI client. "
45+
"Set it via argument or AZURE_OPENAI_ENDPOINT environment variable."
46+
)
47+
48+
self.client = openai.AzureOpenAI(
49+
api_key=api_key,
50+
azure_endpoint=azure_endpoint,
51+
api_version=api_version,
52+
azure_deployment=azure_deployment,
53+
)
54+
self.async_client = openai.AsyncAzureOpenAI(
55+
api_key=api_key,
56+
azure_endpoint=azure_endpoint,
57+
api_version=api_version,
58+
azure_deployment=azure_deployment,
59+
)
60+
self.model_name = model_name
61+
self.azure_deployment = azure_deployment
62+
63+
# Per-model usage tracking
64+
self.model_call_counts: dict[str, int] = defaultdict(int)
65+
self.model_input_tokens: dict[str, int] = defaultdict(int)
66+
self.model_output_tokens: dict[str, int] = defaultdict(int)
67+
self.model_total_tokens: dict[str, int] = defaultdict(int)
68+
69+
def completion(
70+
self, prompt: str | list[dict[str, Any]], model: str | None = None
71+
) -> str:
72+
if isinstance(prompt, str):
73+
messages = [{"role": "user", "content": prompt}]
74+
elif isinstance(prompt, list) and all(isinstance(item, dict) for item in prompt):
75+
messages = prompt
76+
else:
77+
raise ValueError(f"Invalid prompt type: {type(prompt)}")
78+
79+
model = model or self.model_name
80+
if not model:
81+
raise ValueError("Model name is required for Azure OpenAI client.")
82+
83+
response = self.client.chat.completions.create(
84+
model=model,
85+
messages=messages,
86+
)
87+
self._track_cost(response, model)
88+
return response.choices[0].message.content
89+
90+
async def acompletion(
91+
self, prompt: str | list[dict[str, Any]], model: str | None = None
92+
) -> str:
93+
if isinstance(prompt, str):
94+
messages = [{"role": "user", "content": prompt}]
95+
elif isinstance(prompt, list) and all(isinstance(item, dict) for item in prompt):
96+
messages = prompt
97+
else:
98+
raise ValueError(f"Invalid prompt type: {type(prompt)}")
99+
100+
model = model or self.model_name
101+
if not model:
102+
raise ValueError("Model name is required for Azure OpenAI client.")
103+
104+
response = await self.async_client.chat.completions.create(
105+
model=model,
106+
messages=messages,
107+
)
108+
self._track_cost(response, model)
109+
return response.choices[0].message.content
110+
111+
def _track_cost(self, response: openai.ChatCompletion, model: str):
112+
self.model_call_counts[model] += 1
113+
114+
usage = getattr(response, "usage", None)
115+
if usage is None:
116+
raise ValueError("No usage data received. Tracking tokens not possible.")
117+
118+
self.model_input_tokens[model] += usage.prompt_tokens
119+
self.model_output_tokens[model] += usage.completion_tokens
120+
self.model_total_tokens[model] += usage.total_tokens
121+
122+
# Track last call for handler to read
123+
self.last_prompt_tokens = usage.prompt_tokens
124+
self.last_completion_tokens = usage.completion_tokens
125+
126+
def get_usage_summary(self) -> UsageSummary:
127+
model_summaries = {}
128+
for model in self.model_call_counts:
129+
model_summaries[model] = ModelUsageSummary(
130+
total_calls=self.model_call_counts[model],
131+
total_input_tokens=self.model_input_tokens[model],
132+
total_output_tokens=self.model_output_tokens[model],
133+
)
134+
return UsageSummary(model_usage_summaries=model_summaries)
135+
136+
def get_last_usage(self) -> ModelUsageSummary:
137+
return ModelUsageSummary(
138+
total_calls=1,
139+
total_input_tokens=self.last_prompt_tokens,
140+
total_output_tokens=self.last_completion_tokens,
141+
)
142+

‎rlm/core/types.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from types import ModuleType
33
from typing import Any, Literal
44

5-
ClientBackend = Literal["openai", "portkey", "openrouter", "vllm", "litellm", "anthropic"]
5+
ClientBackend = Literal["openai", "portkey", "openrouter", "vllm", "litellm", "anthropic", "azure_openai"]
66
EnvironmentType = Literal["local", "prime", "modal"]
77

88

0 commit comments

Comments
 (0)