Repository navigation
Expand file tree
/
Copy pathllm_client.py
More file actions
162 lines (132 loc) · 5.27 KB
/
Copy pathllm_client.py
File metadata and controls
162 lines (132 loc) · 5.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
"""
LLM Client — Provider-Agnostic Interface
This module provides a single interface for interacting with multiple LLM providers
(OpenAI, Claude, etc.). Switch providers via environment variable.
The LLMClient abstracts away provider-specific API differences so analysis logic
remains agnostic to which LLM is used.
Example:
>>> from llm_client import LLMClient
>>> llm = LLMClient() # Reads LLM_PROVIDER from env
>>> response = llm.complete("Summarize this call", max_tokens=500)
>>> response = llm.complete_with_retry(prompt, max_retries=3)
"""
import os
import time
from config import LLM_PROVIDER, OPENAI_API_KEY, OPENAI_MODEL, CLAUDE_API_KEY
class LLMClient:
"""
Provider-agnostic LLM client.
Wraps OpenAI and Claude APIs behind a single interface.
Switch provider via LLM_PROVIDER environment variable.
Attributes:
provider (str): "openai" or "claude"
client: Underlying SDK client (openai.OpenAI or anthropic.Anthropic)
model (str): Model ID for the provider
"""
def __init__(self, provider=None):
"""
Initialize LLM client for the specified provider.
Args:
provider (str, optional): "openai" or "claude".
If None, reads from LLM_PROVIDER env var.
Raises:
ValueError: If provider is unknown or API key is missing.
ImportError: If required SDK is not installed.
"""
self.provider = (provider or LLM_PROVIDER).lower()
if self.provider == "openai":
try:
import openai
except ImportError:
raise ImportError(
"OpenAI SDK not found. Install with: pip install openai"
)
if not OPENAI_API_KEY:
raise ValueError(
"OPENAI_API_KEY environment variable is not set. "
"Set it in .env or export it."
)
self.client = openai.OpenAI(api_key=OPENAI_API_KEY)
self.model = OPENAI_MODEL
elif self.provider == "claude":
try:
import anthropic
except ImportError:
raise ImportError(
"Anthropic SDK not found. Install with: pip install anthropic"
)
if not CLAUDE_API_KEY:
raise ValueError(
"CLAUDE_API_KEY environment variable is not set. "
"Set it in .env or export it."
)
self.client = anthropic.Anthropic(api_key=CLAUDE_API_KEY)
self.model = "claude-opus-4-8" # TODO: make configurable
else:
raise ValueError(
f"Unknown LLM_PROVIDER: {self.provider}. "
f"Must be 'openai' or 'claude'."
)
def complete(self, prompt, max_tokens=1500):
"""
Send a prompt to the LLM and return the response.
Args:
prompt (str): The input prompt for the LLM.
max_tokens (int): Maximum tokens in the response (default: 1500).
Returns:
str: The LLM's response text.
Raises:
Exception: If the LLM API call fails.
"""
if self.provider == "claude":
# Anthropic API
resp = self.client.messages.create(
model=self.model,
max_tokens=max_tokens,
messages=[{"role": "user", "content": prompt}],
)
return resp.content[0].text
elif self.provider == "openai":
# OpenAI API
resp = self.client.chat.completions.create(
model=self.model,
max_tokens=max_tokens,
messages=[{"role": "user", "content": prompt}],
)
return resp.choices[0].message.content
def complete_with_retry(
self, prompt, max_tokens=1500, max_retries=3, initial_delay=1
):
"""
Send a prompt to the LLM with exponential backoff retry.
Automatically retries on failure with exponential backoff:
- Attempt 1: Immediate
- Attempt 2: 1s delay
- Attempt 3: 2s delay
- ...
Args:
prompt (str): The input prompt for the LLM.
max_tokens (int): Maximum tokens in the response (default: 1500).
max_retries (int): Number of retry attempts (default: 3).
initial_delay (int): Initial delay in seconds (default: 1).
Returns:
str: The LLM's response text.
Raises:
Exception: If all retries fail.
"""
for attempt in range(max_retries):
try:
return self.complete(prompt, max_tokens=max_tokens)
except Exception as e:
# If this was the last attempt, raise the exception
if attempt == max_retries - 1:
raise Exception(
f"LLM call failed after {max_retries} retries: {e}"
) from e
# Otherwise, wait and retry
wait = initial_delay * (2 ** attempt)
print(
f" ⚠️ Retry {attempt + 1}/{max_retries} in {wait}s "
f"({type(e).__name__})"
)
time.sleep(wait)