pi-para 0.6.0 → 0.6.2

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.
Files changed (213) hide show
  1. package/ARCHITECTURE.md +0 -9
  2. package/CHANGELOG.md +22 -0
  3. package/CONFIGURATION.md +5 -23
  4. package/README.md +7 -31
  5. package/SETUP.md +5 -3
  6. package/STRUCTURE.md +0 -9
  7. package/dist/atomic-write.d.ts +2 -0
  8. package/dist/atomic-write.d.ts.map +1 -0
  9. package/dist/atomic-write.js +18 -0
  10. package/dist/atomic-write.js.map +1 -0
  11. package/dist/capture.d.ts +34 -0
  12. package/dist/capture.d.ts.map +1 -0
  13. package/dist/capture.js +461 -0
  14. package/dist/capture.js.map +1 -0
  15. package/dist/cli-legacy.d.ts +4 -0
  16. package/dist/cli-legacy.d.ts.map +1 -0
  17. package/dist/cli-legacy.js +41 -0
  18. package/dist/cli-legacy.js.map +1 -0
  19. package/dist/cli.d.ts +8 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +427 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/commands.d.ts +15 -0
  24. package/dist/commands.d.ts.map +1 -0
  25. package/dist/commands.js +791 -0
  26. package/dist/commands.js.map +1 -0
  27. package/dist/config.d.ts +94 -0
  28. package/dist/config.d.ts.map +1 -0
  29. package/dist/config.js +228 -0
  30. package/dist/config.js.map +1 -0
  31. package/dist/context.d.ts +63 -0
  32. package/dist/context.d.ts.map +1 -0
  33. package/dist/context.js +311 -0
  34. package/dist/context.js.map +1 -0
  35. package/dist/credentials.d.ts +27 -0
  36. package/dist/credentials.d.ts.map +1 -0
  37. package/dist/credentials.js +85 -0
  38. package/dist/credentials.js.map +1 -0
  39. package/dist/daemon.d.ts +49 -0
  40. package/dist/daemon.d.ts.map +1 -0
  41. package/dist/daemon.js +226 -0
  42. package/dist/daemon.js.map +1 -0
  43. package/dist/doctor.d.ts +20 -0
  44. package/dist/doctor.d.ts.map +1 -0
  45. package/dist/doctor.js +197 -0
  46. package/dist/doctor.js.map +1 -0
  47. package/dist/frontmatter.d.ts +59 -0
  48. package/dist/frontmatter.d.ts.map +1 -0
  49. package/dist/frontmatter.js +170 -0
  50. package/dist/frontmatter.js.map +1 -0
  51. package/dist/index.d.ts +21 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +357 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/ingest.d.ts +88 -0
  56. package/dist/ingest.d.ts.map +1 -0
  57. package/dist/ingest.js +105 -0
  58. package/dist/ingest.js.map +1 -0
  59. package/dist/link-discovery.d.ts +40 -0
  60. package/dist/link-discovery.d.ts.map +1 -0
  61. package/dist/link-discovery.js +122 -0
  62. package/dist/link-discovery.js.map +1 -0
  63. package/dist/link-utils.d.ts +30 -0
  64. package/dist/link-utils.d.ts.map +1 -0
  65. package/dist/link-utils.js +145 -0
  66. package/dist/link-utils.js.map +1 -0
  67. package/dist/lint.d.ts +39 -0
  68. package/dist/lint.d.ts.map +1 -0
  69. package/dist/lint.js +827 -0
  70. package/dist/lint.js.map +1 -0
  71. package/dist/maintainer.d.ts +24 -0
  72. package/dist/maintainer.d.ts.map +1 -0
  73. package/dist/maintainer.js +358 -0
  74. package/dist/maintainer.js.map +1 -0
  75. package/dist/model-resolver.d.ts +30 -0
  76. package/dist/model-resolver.d.ts.map +1 -0
  77. package/dist/model-resolver.js +71 -0
  78. package/dist/model-resolver.js.map +1 -0
  79. package/dist/paths.d.ts +20 -0
  80. package/dist/paths.d.ts.map +1 -0
  81. package/dist/paths.js +33 -0
  82. package/dist/paths.js.map +1 -0
  83. package/dist/processor.d.ts +14 -0
  84. package/dist/processor.d.ts.map +1 -0
  85. package/dist/processor.js +361 -0
  86. package/dist/processor.js.map +1 -0
  87. package/dist/qmd-providers.d.ts +12 -0
  88. package/dist/qmd-providers.d.ts.map +1 -0
  89. package/dist/qmd-providers.js +45 -0
  90. package/dist/qmd-providers.js.map +1 -0
  91. package/dist/query.d.ts +69 -0
  92. package/dist/query.d.ts.map +1 -0
  93. package/dist/query.js +120 -0
  94. package/dist/query.js.map +1 -0
  95. package/dist/raw.d.ts +57 -0
  96. package/dist/raw.d.ts.map +1 -0
  97. package/dist/raw.js +219 -0
  98. package/dist/raw.js.map +1 -0
  99. package/dist/redact.d.ts +21 -0
  100. package/dist/redact.d.ts.map +1 -0
  101. package/dist/redact.js +143 -0
  102. package/dist/redact.js.map +1 -0
  103. package/dist/repair.d.ts +11 -0
  104. package/dist/repair.d.ts.map +1 -0
  105. package/dist/repair.js +41 -0
  106. package/dist/repair.js.map +1 -0
  107. package/dist/scheduler/controls.d.ts +35 -0
  108. package/dist/scheduler/controls.d.ts.map +1 -0
  109. package/dist/scheduler/controls.js +98 -0
  110. package/dist/scheduler/controls.js.map +1 -0
  111. package/dist/scheduler/index.d.ts +70 -0
  112. package/dist/scheduler/index.d.ts.map +1 -0
  113. package/dist/scheduler/index.js +245 -0
  114. package/dist/scheduler/index.js.map +1 -0
  115. package/dist/scheduler/leases.d.ts +22 -0
  116. package/dist/scheduler/leases.d.ts.map +1 -0
  117. package/dist/scheduler/leases.js +74 -0
  118. package/dist/scheduler/leases.js.map +1 -0
  119. package/dist/scheduler/session-capture.d.ts +33 -0
  120. package/dist/scheduler/session-capture.d.ts.map +1 -0
  121. package/dist/scheduler/session-capture.js +108 -0
  122. package/dist/scheduler/session-capture.js.map +1 -0
  123. package/dist/scheduler/state.d.ts +56 -0
  124. package/dist/scheduler/state.d.ts.map +1 -0
  125. package/dist/scheduler/state.js +210 -0
  126. package/dist/scheduler/state.js.map +1 -0
  127. package/dist/scope.d.ts +54 -0
  128. package/dist/scope.d.ts.map +1 -0
  129. package/dist/scope.js +161 -0
  130. package/dist/scope.js.map +1 -0
  131. package/dist/scripts/bulk-link-fix.d.ts +9 -0
  132. package/dist/scripts/bulk-link-fix.d.ts.map +1 -0
  133. package/dist/scripts/bulk-link-fix.js +106 -0
  134. package/dist/scripts/bulk-link-fix.js.map +1 -0
  135. package/dist/session-tools.d.ts +33 -0
  136. package/dist/session-tools.d.ts.map +1 -0
  137. package/dist/session-tools.js +211 -0
  138. package/dist/session-tools.js.map +1 -0
  139. package/dist/settings.d.ts +37 -0
  140. package/dist/settings.d.ts.map +1 -0
  141. package/dist/settings.js +176 -0
  142. package/dist/settings.js.map +1 -0
  143. package/dist/setup.d.ts +29 -0
  144. package/dist/setup.d.ts.map +1 -0
  145. package/dist/setup.js +90 -0
  146. package/dist/setup.js.map +1 -0
  147. package/dist/state.d.ts +54 -0
  148. package/dist/state.d.ts.map +1 -0
  149. package/dist/state.js +176 -0
  150. package/dist/state.js.map +1 -0
  151. package/dist/status.d.ts +22 -0
  152. package/dist/status.d.ts.map +1 -0
  153. package/dist/status.js +93 -0
  154. package/dist/status.js.map +1 -0
  155. package/dist/store.d.ts +71 -0
  156. package/dist/store.d.ts.map +1 -0
  157. package/dist/store.js +282 -0
  158. package/dist/store.js.map +1 -0
  159. package/dist/summarize.d.ts +60 -0
  160. package/dist/summarize.d.ts.map +1 -0
  161. package/dist/summarize.js +266 -0
  162. package/dist/summarize.js.map +1 -0
  163. package/dist/tag-registry.d.ts +56 -0
  164. package/dist/tag-registry.d.ts.map +1 -0
  165. package/dist/tag-registry.js +146 -0
  166. package/dist/tag-registry.js.map +1 -0
  167. package/dist/templates/prompts.d.ts +51 -0
  168. package/dist/templates/prompts.d.ts.map +1 -0
  169. package/dist/templates/prompts.js +203 -0
  170. package/dist/templates/prompts.js.map +1 -0
  171. package/dist/tools.d.ts +26 -0
  172. package/dist/tools.d.ts.map +1 -0
  173. package/dist/tools.js +1198 -0
  174. package/dist/tools.js.map +1 -0
  175. package/dist/watcher.d.ts +25 -0
  176. package/dist/watcher.d.ts.map +1 -0
  177. package/dist/watcher.js +84 -0
  178. package/dist/watcher.js.map +1 -0
  179. package/dist/webui/client/dist/index.html +1885 -0
  180. package/dist/webui/server.d.ts +31 -0
  181. package/dist/webui/server.d.ts.map +1 -0
  182. package/dist/webui/server.js +566 -0
  183. package/dist/webui/server.js.map +1 -0
  184. package/dist/wiki-tool-guidance.d.ts +6 -0
  185. package/dist/wiki-tool-guidance.d.ts.map +1 -0
  186. package/dist/wiki-tool-guidance.js +70 -0
  187. package/dist/wiki-tool-guidance.js.map +1 -0
  188. package/dist/wiki.d.ts +108 -0
  189. package/dist/wiki.d.ts.map +1 -0
  190. package/dist/wiki.js +497 -0
  191. package/dist/wiki.js.map +1 -0
  192. package/package.json +2 -1
  193. package/scripts/build-assets.mjs +1 -13
  194. package/skills/setup/SKILL.md +1 -1
  195. package/src/cli.ts +0 -70
  196. package/src/commands.ts +78 -3
  197. package/src/config.ts +0 -25
  198. package/src/doctor.ts +21 -1
  199. package/src/index.ts +16 -38
  200. package/src/scheduler/index.ts +64 -0
  201. package/src/settings.ts +88 -0
  202. package/src/setup.ts +1 -1
  203. package/src/templates/prompts.ts +0 -56
  204. package/scripts/gepa/dataset.py +0 -192
  205. package/scripts/gepa/lm_providers.py +0 -472
  206. package/scripts/gepa/metric.py +0 -279
  207. package/scripts/gepa/optimize.py +0 -367
  208. package/scripts/gepa/program.py +0 -99
  209. package/scripts/gepa/pyproject.toml +0 -19
  210. package/scripts/gepa/run-all.sh +0 -53
  211. package/scripts/gepa/wiki_reader.py +0 -113
  212. package/src/gepa/index.ts +0 -287
  213. 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")