pi-para 0.5.5 → 0.6.1
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.
- package/ARCHITECTURE.md +0 -9
- package/CHANGELOG.md +35 -0
- package/CONFIGURATION.md +0 -23
- package/README.md +1 -29
- package/SETUP.md +1 -1
- package/STRUCTURE.md +0 -9
- package/dist/cli.js +0 -84
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +0 -10
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -14
- package/dist/config.js.map +1 -1
- package/dist/doctor.js +5 -0
- package/dist/doctor.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -18
- package/dist/index.js.map +1 -1
- package/dist/templates/prompts.d.ts +0 -12
- package/dist/templates/prompts.d.ts.map +1 -1
- package/dist/templates/prompts.js +0 -57
- package/dist/templates/prompts.js.map +1 -1
- package/package.json +9 -5
- package/scripts/build-assets.mjs +17 -0
- package/scripts/install.ps1 +27 -16
- package/scripts/install.sh +3 -2
- package/scripts/smoke-install.mjs +5 -3
- package/skills/setup/SKILL.md +1 -1
- package/src/cli.ts +0 -70
- package/src/config.ts +0 -25
- package/src/doctor.ts +5 -0
- package/src/index.ts +1 -38
- package/src/templates/prompts.ts +0 -56
- package/dist/gepa/dataset.d.ts +0 -36
- package/dist/gepa/dataset.d.ts.map +0 -1
- package/dist/gepa/dataset.js +0 -148
- package/dist/gepa/dataset.js.map +0 -1
- package/dist/gepa/evaluator.d.ts +0 -51
- package/dist/gepa/evaluator.d.ts.map +0 -1
- package/dist/gepa/evaluator.js +0 -317
- package/dist/gepa/evaluator.js.map +0 -1
- package/dist/gepa/index.d.ts +0 -66
- package/dist/gepa/index.d.ts.map +0 -1
- package/dist/gepa/index.js +0 -240
- package/dist/gepa/index.js.map +0 -1
- package/dist/gepa/llm.d.ts +0 -35
- package/dist/gepa/llm.d.ts.map +0 -1
- package/dist/gepa/llm.js +0 -268
- package/dist/gepa/llm.js.map +0 -1
- package/dist/gepa/optimized/capture-prompt.txt +0 -65
- package/dist/gepa/optimizer.d.ts +0 -29
- package/dist/gepa/optimizer.d.ts.map +0 -1
- package/dist/gepa/optimizer.js +0 -249
- package/dist/gepa/optimizer.js.map +0 -1
- package/dist/gepa/targets.d.ts +0 -39
- package/dist/gepa/targets.d.ts.map +0 -1
- package/dist/gepa/targets.js +0 -326
- package/dist/gepa/targets.js.map +0 -1
- package/dist/gepa/types.d.ts +0 -168
- package/dist/gepa/types.d.ts.map +0 -1
- package/dist/gepa/types.js +0 -124
- package/dist/gepa/types.js.map +0 -1
- package/scripts/gepa/__pycache__/dataset.cpython-313.pyc +0 -0
- package/scripts/gepa/__pycache__/lm_providers.cpython-313.pyc +0 -0
- package/scripts/gepa/__pycache__/metric.cpython-313.pyc +0 -0
- package/scripts/gepa/__pycache__/program.cpython-313.pyc +0 -0
- package/scripts/gepa/__pycache__/wiki_reader.cpython-313.pyc +0 -0
- package/scripts/gepa/dataset.py +0 -192
- package/scripts/gepa/lm_providers.py +0 -472
- package/scripts/gepa/metric.py +0 -279
- package/scripts/gepa/optimize.py +0 -367
- package/scripts/gepa/program.py +0 -99
- package/scripts/gepa/pyproject.toml +0 -19
- package/scripts/gepa/run-all.sh +0 -53
- package/scripts/gepa/uv.lock +0 -2241
- package/scripts/gepa/wiki_reader.py +0 -113
- package/src/gepa/index.ts +0 -287
- package/src/gepa/optimized/capture-prompt.txt +0 -65
|
@@ -1,472 +0,0 @@
|
|
|
1
|
-
"""Custom DSPy LM providers — no litellm.
|
|
2
|
-
|
|
3
|
-
Provides BaseLM subclasses for:
|
|
4
|
-
- Anthropic with OAuth token support + Claude Code headers
|
|
5
|
-
- MiniMax via Anthropic-compatible API
|
|
6
|
-
- OpenRouter via OpenAI-compatible API
|
|
7
|
-
|
|
8
|
-
API keys are passed via environment variables by the TypeScript orchestrator.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from __future__ import annotations
|
|
12
|
-
|
|
13
|
-
import json
|
|
14
|
-
import logging
|
|
15
|
-
import os
|
|
16
|
-
import time
|
|
17
|
-
from typing import Any, Literal
|
|
18
|
-
|
|
19
|
-
import httpx
|
|
20
|
-
import anthropic
|
|
21
|
-
import openai
|
|
22
|
-
from dspy.clients.base_lm import BaseLM
|
|
23
|
-
|
|
24
|
-
logger = logging.getLogger(__name__)
|
|
25
|
-
|
|
26
|
-
# -- Constants ----------------------------------------------------------------
|
|
27
|
-
|
|
28
|
-
CLAUDE_CODE_VERSION = "2.1.96"
|
|
29
|
-
AUTH_JSON_PATH = os.path.expanduser("~/.pi/agent/auth.json")
|
|
30
|
-
TOKEN_REFRESH_BUFFER_S = 300 # refresh 5 minutes before expiry
|
|
31
|
-
|
|
32
|
-
MINIMAX_BASE_URL = "https://api.minimax.io/anthropic"
|
|
33
|
-
MINIMAX_CN_BASE_URL = "https://api.minimaxi.com/anthropic"
|
|
34
|
-
|
|
35
|
-
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
# -- Anthropic OAuth LM -------------------------------------------------------
|
|
39
|
-
|
|
40
|
-
class AnthropicOAuthLM(BaseLM):
|
|
41
|
-
"""DSPy LM for Anthropic with OAuth token + Claude Code billing headers.
|
|
42
|
-
|
|
43
|
-
Bypasses litellm entirely. Uses anthropic SDK with streaming to avoid
|
|
44
|
-
10-minute timeout on large max_tokens.
|
|
45
|
-
|
|
46
|
-
Supports both OAuth tokens (sk-ant-oat*) and regular API keys (sk-ant-api*).
|
|
47
|
-
OAuth tokens route billing through Claude Code subscription (free for Pro/Max).
|
|
48
|
-
"""
|
|
49
|
-
|
|
50
|
-
def __init__(
|
|
51
|
-
self,
|
|
52
|
-
model: str = "claude-sonnet-4-20250514",
|
|
53
|
-
api_key: str | None = None,
|
|
54
|
-
temperature: float | None = None,
|
|
55
|
-
max_tokens: int | None = None,
|
|
56
|
-
cache: bool = True,
|
|
57
|
-
num_retries: int = 3,
|
|
58
|
-
model_type: Literal["chat", "text"] = "chat",
|
|
59
|
-
**kwargs,
|
|
60
|
-
):
|
|
61
|
-
self.model = model.split("/")[-1] if "/" in model else model
|
|
62
|
-
self.model_type = model_type
|
|
63
|
-
self.cache = cache
|
|
64
|
-
self.num_retries = num_retries
|
|
65
|
-
self.history = []
|
|
66
|
-
self.callbacks = []
|
|
67
|
-
self.provider = None
|
|
68
|
-
self.finetuning_model = None
|
|
69
|
-
self.launch_kwargs = {}
|
|
70
|
-
self.train_kwargs = {}
|
|
71
|
-
|
|
72
|
-
self.kwargs = {
|
|
73
|
-
"temperature": temperature or 0.0,
|
|
74
|
-
"max_tokens": max_tokens or 8192,
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
# Resolve API key
|
|
78
|
-
self._auth_token = api_key or os.environ.get("ANTHROPIC_OAUTH_TOKEN") or os.environ.get("ANTHROPIC_API_KEY")
|
|
79
|
-
if not self._auth_token:
|
|
80
|
-
raise ValueError("No Anthropic API key. Set ANTHROPIC_API_KEY or ANTHROPIC_OAUTH_TOKEN.")
|
|
81
|
-
|
|
82
|
-
self._is_oauth = "sk-ant-oat" in self._auth_token
|
|
83
|
-
|
|
84
|
-
# Build client with appropriate headers
|
|
85
|
-
if self._is_oauth:
|
|
86
|
-
default_headers = {
|
|
87
|
-
"accept": "application/json",
|
|
88
|
-
"anthropic-dangerous-direct-browser-access": "true",
|
|
89
|
-
"anthropic-beta": f"claude-code-20250219,oauth-2025-04-20",
|
|
90
|
-
"user-agent": f"claude-cli/{CLAUDE_CODE_VERSION} (external, cli)",
|
|
91
|
-
"x-app": "cli",
|
|
92
|
-
}
|
|
93
|
-
self._client = anthropic.Anthropic(
|
|
94
|
-
api_key=None,
|
|
95
|
-
auth_token=self._auth_token,
|
|
96
|
-
max_retries=num_retries,
|
|
97
|
-
default_headers=default_headers,
|
|
98
|
-
)
|
|
99
|
-
logger.info("AnthropicOAuthLM: model=%s auth=OAuth", self.model)
|
|
100
|
-
else:
|
|
101
|
-
self._client = anthropic.Anthropic(
|
|
102
|
-
api_key=self._auth_token,
|
|
103
|
-
max_retries=num_retries,
|
|
104
|
-
)
|
|
105
|
-
logger.info("AnthropicOAuthLM: model=%s auth=APIKey", self.model)
|
|
106
|
-
|
|
107
|
-
def _maybe_refresh_token(self) -> None:
|
|
108
|
-
"""Refresh OAuth token if it's about to expire."""
|
|
109
|
-
if not self._is_oauth:
|
|
110
|
-
return
|
|
111
|
-
try:
|
|
112
|
-
auth = json.loads(open(AUTH_JSON_PATH).read())
|
|
113
|
-
anthropic_auth = auth.get("anthropic", {})
|
|
114
|
-
expires_ms = anthropic_auth.get("expires", 0)
|
|
115
|
-
refresh_token = anthropic_auth.get("refresh")
|
|
116
|
-
if not refresh_token:
|
|
117
|
-
return
|
|
118
|
-
|
|
119
|
-
now_ms = time.time() * 1000
|
|
120
|
-
if expires_ms - now_ms > TOKEN_REFRESH_BUFFER_S * 1000:
|
|
121
|
-
return # Still valid
|
|
122
|
-
|
|
123
|
-
logger.info("OAuth token expiring soon, refreshing...")
|
|
124
|
-
resp = httpx.post(
|
|
125
|
-
"https://console.anthropic.com/v1/oauth/token",
|
|
126
|
-
json={
|
|
127
|
-
"grant_type": "refresh_token",
|
|
128
|
-
"refresh_token": refresh_token,
|
|
129
|
-
"client_id": anthropic_auth.get("client_id", "pi"),
|
|
130
|
-
},
|
|
131
|
-
headers={"Content-Type": "application/json"},
|
|
132
|
-
timeout=30,
|
|
133
|
-
)
|
|
134
|
-
if resp.status_code == 200:
|
|
135
|
-
data = resp.json()
|
|
136
|
-
new_access = data.get("access_token", "")
|
|
137
|
-
new_expires = int(time.time() * 1000) + data.get("expires_in", 3600) * 1000
|
|
138
|
-
# Update auth.json
|
|
139
|
-
auth["anthropic"]["access"] = new_access
|
|
140
|
-
auth["anthropic"]["expires"] = new_expires
|
|
141
|
-
with open(AUTH_JSON_PATH, "w") as f:
|
|
142
|
-
json.dump(auth, f, indent=2)
|
|
143
|
-
# Update self
|
|
144
|
-
self._auth_token = new_access
|
|
145
|
-
self._client = anthropic.Anthropic(
|
|
146
|
-
api_key=None,
|
|
147
|
-
auth_token=new_access,
|
|
148
|
-
max_retries=self.num_retries,
|
|
149
|
-
default_headers={
|
|
150
|
-
"accept": "application/json",
|
|
151
|
-
"anthropic-dangerous-direct-browser-access": "true",
|
|
152
|
-
"anthropic-beta": "claude-code-20250219,oauth-2025-04-20",
|
|
153
|
-
"user-agent": f"claude-cli/{CLAUDE_CODE_VERSION} (external, cli)",
|
|
154
|
-
"x-app": "cli",
|
|
155
|
-
},
|
|
156
|
-
)
|
|
157
|
-
logger.info("OAuth token refreshed successfully")
|
|
158
|
-
else:
|
|
159
|
-
logger.warning("Token refresh failed: %d %s", resp.status_code, resp.text[:200])
|
|
160
|
-
except Exception as e:
|
|
161
|
-
logger.warning("Token refresh error: %s", e)
|
|
162
|
-
|
|
163
|
-
def __call__(
|
|
164
|
-
self,
|
|
165
|
-
prompt: str | None = None,
|
|
166
|
-
messages: list[dict] | None = None,
|
|
167
|
-
**kwargs,
|
|
168
|
-
) -> list[dict]:
|
|
169
|
-
"""Call Anthropic API. Returns list of dicts with 'text' key."""
|
|
170
|
-
self._maybe_refresh_token()
|
|
171
|
-
|
|
172
|
-
if messages is None:
|
|
173
|
-
messages = []
|
|
174
|
-
if prompt:
|
|
175
|
-
messages = [{"role": "user", "content": prompt}]
|
|
176
|
-
|
|
177
|
-
# Separate system from messages
|
|
178
|
-
converted, system = self._convert_messages(messages)
|
|
179
|
-
|
|
180
|
-
gen_kwargs = {**self.kwargs, **kwargs}
|
|
181
|
-
temperature = gen_kwargs.pop("temperature", 0.0)
|
|
182
|
-
max_tokens = gen_kwargs.pop("max_tokens", 8192)
|
|
183
|
-
|
|
184
|
-
# Remove unsupported kwargs
|
|
185
|
-
for key in ["response_format", "n", "stop", "logprobs", "top_logprobs",
|
|
186
|
-
"presence_penalty", "frequency_penalty", "logit_bias", "user"]:
|
|
187
|
-
gen_kwargs.pop(key, None)
|
|
188
|
-
|
|
189
|
-
create_kwargs: dict[str, Any] = {
|
|
190
|
-
"model": self.model,
|
|
191
|
-
"messages": converted,
|
|
192
|
-
"max_tokens": max_tokens,
|
|
193
|
-
}
|
|
194
|
-
if temperature is not None:
|
|
195
|
-
create_kwargs["temperature"] = temperature
|
|
196
|
-
|
|
197
|
-
# System prompt — for OAuth, prepend CC identity block
|
|
198
|
-
if self._is_oauth:
|
|
199
|
-
system_blocks = [
|
|
200
|
-
{
|
|
201
|
-
"type": "text",
|
|
202
|
-
"text": "You are Claude Code, Anthropic's official CLI for Claude.",
|
|
203
|
-
"cache_control": {"type": "ephemeral"},
|
|
204
|
-
}
|
|
205
|
-
]
|
|
206
|
-
if system:
|
|
207
|
-
system_blocks.append({
|
|
208
|
-
"type": "text",
|
|
209
|
-
"text": system,
|
|
210
|
-
"cache_control": {"type": "ephemeral"},
|
|
211
|
-
})
|
|
212
|
-
create_kwargs["system"] = system_blocks
|
|
213
|
-
elif system:
|
|
214
|
-
create_kwargs["system"] = system
|
|
215
|
-
|
|
216
|
-
# Stream to avoid 10-minute timeout
|
|
217
|
-
text = ""
|
|
218
|
-
try:
|
|
219
|
-
with self._client.messages.stream(**create_kwargs) as stream:
|
|
220
|
-
for chunk in stream.text_stream:
|
|
221
|
-
text += chunk
|
|
222
|
-
except anthropic.APIError as e:
|
|
223
|
-
logger.error("Anthropic API error: %s", e)
|
|
224
|
-
raise ValueError(f"Anthropic API error: {e}")
|
|
225
|
-
|
|
226
|
-
self.history.append({
|
|
227
|
-
"prompt": prompt,
|
|
228
|
-
"messages": messages,
|
|
229
|
-
"kwargs": kwargs,
|
|
230
|
-
"response": {"text": text},
|
|
231
|
-
})
|
|
232
|
-
return [{"text": text}]
|
|
233
|
-
|
|
234
|
-
def _convert_messages(self, messages: list[dict]) -> tuple[list[dict], str | None]:
|
|
235
|
-
converted = []
|
|
236
|
-
system = None
|
|
237
|
-
for msg in messages:
|
|
238
|
-
role = msg.get("role", "user")
|
|
239
|
-
content = msg.get("content", "")
|
|
240
|
-
if role == "system":
|
|
241
|
-
system = f"{system}\n\n{content}" if system else content
|
|
242
|
-
elif role == "assistant":
|
|
243
|
-
converted.append({"role": "assistant", "content": content})
|
|
244
|
-
else:
|
|
245
|
-
converted.append({"role": "user", "content": content})
|
|
246
|
-
return converted, system
|
|
247
|
-
|
|
248
|
-
def __getstate__(self) -> dict:
|
|
249
|
-
state = self.__dict__.copy()
|
|
250
|
-
state.pop("_client", None)
|
|
251
|
-
return state
|
|
252
|
-
|
|
253
|
-
def __setstate__(self, state: dict) -> None:
|
|
254
|
-
self.__dict__.update(state)
|
|
255
|
-
if self._is_oauth:
|
|
256
|
-
self._client = anthropic.Anthropic(
|
|
257
|
-
api_key=None, auth_token=self._auth_token,
|
|
258
|
-
max_retries=self.num_retries,
|
|
259
|
-
default_headers={
|
|
260
|
-
"accept": "application/json",
|
|
261
|
-
"anthropic-dangerous-direct-browser-access": "true",
|
|
262
|
-
"anthropic-beta": "claude-code-20250219,oauth-2025-04-20",
|
|
263
|
-
"user-agent": f"claude-cli/{CLAUDE_CODE_VERSION} (external, cli)",
|
|
264
|
-
"x-app": "cli",
|
|
265
|
-
},
|
|
266
|
-
)
|
|
267
|
-
else:
|
|
268
|
-
self._client = anthropic.Anthropic(api_key=self._auth_token, max_retries=self.num_retries)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
# -- MiniMax LM ---------------------------------------------------------------
|
|
272
|
-
|
|
273
|
-
class MiniMaxLM(BaseLM):
|
|
274
|
-
"""DSPy LM for MiniMax via Anthropic-compatible API."""
|
|
275
|
-
|
|
276
|
-
def __init__(
|
|
277
|
-
self,
|
|
278
|
-
model: str = "MiniMax-M2",
|
|
279
|
-
api_key: str | None = None,
|
|
280
|
-
china: bool = False,
|
|
281
|
-
temperature: float | None = None,
|
|
282
|
-
max_tokens: int | None = None,
|
|
283
|
-
cache: bool = True,
|
|
284
|
-
num_retries: int = 3,
|
|
285
|
-
model_type: Literal["chat", "text"] = "chat",
|
|
286
|
-
**kwargs,
|
|
287
|
-
):
|
|
288
|
-
super().__init__(
|
|
289
|
-
model=f"minimax/{model}" if not model.startswith("minimax/") else model,
|
|
290
|
-
model_type=model_type,
|
|
291
|
-
temperature=temperature or 0.0,
|
|
292
|
-
max_tokens=max_tokens or 8192,
|
|
293
|
-
cache=cache,
|
|
294
|
-
num_retries=num_retries,
|
|
295
|
-
)
|
|
296
|
-
self._model_id = model.replace("minimax/", "") if model.startswith("minimax/") else model
|
|
297
|
-
self._china = china
|
|
298
|
-
self._temperature = temperature or 0.0
|
|
299
|
-
self._max_tokens = max_tokens or 8192
|
|
300
|
-
self._base_url = MINIMAX_CN_BASE_URL if china else MINIMAX_BASE_URL
|
|
301
|
-
|
|
302
|
-
env_var = "MINIMAX_CN_API_KEY" if china else "MINIMAX_API_KEY"
|
|
303
|
-
self._api_key = api_key or os.environ.get(env_var)
|
|
304
|
-
if not self._api_key:
|
|
305
|
-
raise ValueError(f"No MiniMax API key. Set {env_var}.")
|
|
306
|
-
|
|
307
|
-
self._client = anthropic.Anthropic(
|
|
308
|
-
api_key=self._api_key,
|
|
309
|
-
base_url=self._base_url,
|
|
310
|
-
default_headers={"accept": "application/json"},
|
|
311
|
-
)
|
|
312
|
-
logger.info("MiniMaxLM: model=%s endpoint=%s", self._model_id, self._base_url)
|
|
313
|
-
|
|
314
|
-
def __call__(
|
|
315
|
-
self,
|
|
316
|
-
prompt: str | None = None,
|
|
317
|
-
messages: list[dict] | None = None,
|
|
318
|
-
**kwargs,
|
|
319
|
-
) -> list[dict]:
|
|
320
|
-
if messages is None:
|
|
321
|
-
messages = []
|
|
322
|
-
if prompt:
|
|
323
|
-
messages = [{"role": "user", "content": prompt}]
|
|
324
|
-
|
|
325
|
-
converted = []
|
|
326
|
-
system = None
|
|
327
|
-
for msg in messages:
|
|
328
|
-
role = msg.get("role", "user")
|
|
329
|
-
content = msg.get("content", "")
|
|
330
|
-
if role == "system":
|
|
331
|
-
system = content
|
|
332
|
-
elif role == "assistant":
|
|
333
|
-
converted.append({"role": "assistant", "content": content})
|
|
334
|
-
else:
|
|
335
|
-
converted.append({"role": "user", "content": content})
|
|
336
|
-
|
|
337
|
-
request_kwargs: dict[str, Any] = {
|
|
338
|
-
"model": self._model_id,
|
|
339
|
-
"messages": converted,
|
|
340
|
-
"max_tokens": kwargs.get("max_tokens", self._max_tokens),
|
|
341
|
-
"temperature": kwargs.get("temperature", self._temperature),
|
|
342
|
-
}
|
|
343
|
-
if system:
|
|
344
|
-
request_kwargs["system"] = system
|
|
345
|
-
|
|
346
|
-
text = ""
|
|
347
|
-
try:
|
|
348
|
-
with self._client.messages.stream(**request_kwargs) as stream:
|
|
349
|
-
for chunk in stream.text_stream:
|
|
350
|
-
text += chunk
|
|
351
|
-
except anthropic.APIError as e:
|
|
352
|
-
logger.error("MiniMax API error: %s", e)
|
|
353
|
-
raise ValueError(f"MiniMax API error: {e}")
|
|
354
|
-
|
|
355
|
-
self.history.append({"prompt": prompt, "messages": messages, "response": {"text": text}})
|
|
356
|
-
return [{"text": text}]
|
|
357
|
-
|
|
358
|
-
def __getstate__(self) -> dict:
|
|
359
|
-
state = self.__dict__.copy()
|
|
360
|
-
state.pop("_client", None)
|
|
361
|
-
return state
|
|
362
|
-
|
|
363
|
-
def __setstate__(self, state: dict) -> None:
|
|
364
|
-
self.__dict__.update(state)
|
|
365
|
-
self._client = anthropic.Anthropic(
|
|
366
|
-
api_key=self._api_key, base_url=self._base_url,
|
|
367
|
-
default_headers={"accept": "application/json"},
|
|
368
|
-
)
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
# -- OpenRouter LM ------------------------------------------------------------
|
|
372
|
-
|
|
373
|
-
class OpenRouterLM(BaseLM):
|
|
374
|
-
"""DSPy LM for OpenRouter via OpenAI-compatible API."""
|
|
375
|
-
|
|
376
|
-
def __init__(
|
|
377
|
-
self,
|
|
378
|
-
model: str = "anthropic/claude-sonnet-4",
|
|
379
|
-
api_key: str | None = None,
|
|
380
|
-
temperature: float | None = None,
|
|
381
|
-
max_tokens: int | None = None,
|
|
382
|
-
cache: bool = True,
|
|
383
|
-
num_retries: int = 3,
|
|
384
|
-
model_type: Literal["chat", "text"] = "chat",
|
|
385
|
-
**kwargs,
|
|
386
|
-
):
|
|
387
|
-
super().__init__(
|
|
388
|
-
model=f"openrouter/{model}" if not model.startswith("openrouter/") else model,
|
|
389
|
-
model_type=model_type,
|
|
390
|
-
temperature=temperature or 0.0,
|
|
391
|
-
max_tokens=max_tokens or 8192,
|
|
392
|
-
cache=cache,
|
|
393
|
-
num_retries=num_retries,
|
|
394
|
-
)
|
|
395
|
-
self._model_id = model.replace("openrouter/", "") if model.startswith("openrouter/") else model
|
|
396
|
-
self._temperature = temperature or 0.0
|
|
397
|
-
self._max_tokens = max_tokens or 8192
|
|
398
|
-
|
|
399
|
-
self._api_key = api_key or os.environ.get("OPENROUTER_API_KEY")
|
|
400
|
-
if not self._api_key:
|
|
401
|
-
raise ValueError("No OpenRouter API key. Set OPENROUTER_API_KEY.")
|
|
402
|
-
|
|
403
|
-
self._client = openai.OpenAI(
|
|
404
|
-
api_key=self._api_key,
|
|
405
|
-
base_url=OPENROUTER_BASE_URL,
|
|
406
|
-
)
|
|
407
|
-
logger.info("OpenRouterLM: model=%s", self._model_id)
|
|
408
|
-
|
|
409
|
-
def __call__(
|
|
410
|
-
self,
|
|
411
|
-
prompt: str | None = None,
|
|
412
|
-
messages: list[dict] | None = None,
|
|
413
|
-
**kwargs,
|
|
414
|
-
) -> list[dict]:
|
|
415
|
-
if messages is None:
|
|
416
|
-
messages = []
|
|
417
|
-
if prompt:
|
|
418
|
-
messages = [{"role": "user", "content": prompt}]
|
|
419
|
-
|
|
420
|
-
response = self._client.chat.completions.create(
|
|
421
|
-
model=self._model_id,
|
|
422
|
-
messages=messages,
|
|
423
|
-
temperature=kwargs.get("temperature", self._temperature),
|
|
424
|
-
max_tokens=kwargs.get("max_tokens", self._max_tokens),
|
|
425
|
-
)
|
|
426
|
-
text = response.choices[0].message.content or ""
|
|
427
|
-
self.history.append({"prompt": prompt, "messages": messages, "response": {"text": text}})
|
|
428
|
-
return [{"text": text}]
|
|
429
|
-
|
|
430
|
-
def __getstate__(self) -> dict:
|
|
431
|
-
state = self.__dict__.copy()
|
|
432
|
-
state.pop("_client", None)
|
|
433
|
-
return state
|
|
434
|
-
|
|
435
|
-
def __setstate__(self, state: dict) -> None:
|
|
436
|
-
self.__dict__.update(state)
|
|
437
|
-
self._client = openai.OpenAI(api_key=self._api_key, base_url=OPENROUTER_BASE_URL)
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
# -- Factory ------------------------------------------------------------------
|
|
441
|
-
|
|
442
|
-
def create_lm(model_spec: str, **kwargs) -> BaseLM:
|
|
443
|
-
"""Create a DSPy LM from a provider/model spec string.
|
|
444
|
-
|
|
445
|
-
Supported formats:
|
|
446
|
-
anthropic/claude-sonnet-4-20250514
|
|
447
|
-
minimax/MiniMax-M2
|
|
448
|
-
minimax-cn/MiniMax-M2 (China endpoint)
|
|
449
|
-
openrouter/anthropic/claude-sonnet-4
|
|
450
|
-
openrouter/openai/gpt-4o
|
|
451
|
-
|
|
452
|
-
API keys from env vars:
|
|
453
|
-
ANTHROPIC_API_KEY or ANTHROPIC_OAUTH_TOKEN
|
|
454
|
-
MINIMAX_API_KEY or MINIMAX_CN_API_KEY
|
|
455
|
-
OPENROUTER_API_KEY
|
|
456
|
-
"""
|
|
457
|
-
parts = model_spec.split("/", 1)
|
|
458
|
-
if len(parts) < 2:
|
|
459
|
-
raise ValueError(f"Model spec must be 'provider/model': {model_spec}")
|
|
460
|
-
|
|
461
|
-
provider, model_id = parts[0], parts[1]
|
|
462
|
-
|
|
463
|
-
if provider == "anthropic":
|
|
464
|
-
return AnthropicOAuthLM(model=model_id, **kwargs)
|
|
465
|
-
elif provider == "minimax":
|
|
466
|
-
return MiniMaxLM(model=model_id, china=False, **kwargs)
|
|
467
|
-
elif provider == "minimax-cn":
|
|
468
|
-
return MiniMaxLM(model=model_id, china=True, **kwargs)
|
|
469
|
-
elif provider == "openrouter":
|
|
470
|
-
return OpenRouterLM(model=model_id, **kwargs)
|
|
471
|
-
else:
|
|
472
|
-
raise ValueError(f"Unknown provider '{provider}'. Use: anthropic, minimax, minimax-cn, openrouter")
|