sandboxy 0.0.1__py3-none-any.whl

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 (60) hide show
  1. sandboxy/__init__.py +3 -0
  2. sandboxy/agents/__init__.py +21 -0
  3. sandboxy/agents/base.py +66 -0
  4. sandboxy/agents/llm_prompt.py +308 -0
  5. sandboxy/agents/loader.py +222 -0
  6. sandboxy/api/__init__.py +5 -0
  7. sandboxy/api/app.py +76 -0
  8. sandboxy/api/routes/__init__.py +1 -0
  9. sandboxy/api/routes/agents.py +92 -0
  10. sandboxy/api/routes/local.py +1388 -0
  11. sandboxy/api/routes/tools.py +106 -0
  12. sandboxy/cli/__init__.py +1 -0
  13. sandboxy/cli/main.py +1196 -0
  14. sandboxy/cli/type_detector.py +48 -0
  15. sandboxy/config.py +49 -0
  16. sandboxy/core/__init__.py +1 -0
  17. sandboxy/core/async_runner.py +824 -0
  18. sandboxy/core/mdl_parser.py +441 -0
  19. sandboxy/core/runner.py +599 -0
  20. sandboxy/core/safe_eval.py +165 -0
  21. sandboxy/core/state.py +234 -0
  22. sandboxy/datasets/__init__.py +20 -0
  23. sandboxy/datasets/loader.py +193 -0
  24. sandboxy/datasets/runner.py +442 -0
  25. sandboxy/errors.py +166 -0
  26. sandboxy/local/context.py +235 -0
  27. sandboxy/local/results.py +173 -0
  28. sandboxy/logging.py +31 -0
  29. sandboxy/mcp/__init__.py +25 -0
  30. sandboxy/mcp/client.py +360 -0
  31. sandboxy/mcp/wrapper.py +99 -0
  32. sandboxy/providers/__init__.py +34 -0
  33. sandboxy/providers/anthropic_provider.py +271 -0
  34. sandboxy/providers/base.py +123 -0
  35. sandboxy/providers/http_client.py +101 -0
  36. sandboxy/providers/openai_provider.py +282 -0
  37. sandboxy/providers/openrouter.py +958 -0
  38. sandboxy/providers/registry.py +199 -0
  39. sandboxy/scenarios/__init__.py +11 -0
  40. sandboxy/scenarios/comparison.py +491 -0
  41. sandboxy/scenarios/loader.py +262 -0
  42. sandboxy/scenarios/runner.py +468 -0
  43. sandboxy/scenarios/unified.py +1434 -0
  44. sandboxy/session/__init__.py +21 -0
  45. sandboxy/session/manager.py +278 -0
  46. sandboxy/tools/__init__.py +34 -0
  47. sandboxy/tools/base.py +127 -0
  48. sandboxy/tools/loader.py +270 -0
  49. sandboxy/tools/yaml_tools.py +708 -0
  50. sandboxy/ui/__init__.py +27 -0
  51. sandboxy/ui/dist/assets/index-CgAkYWrJ.css +1 -0
  52. sandboxy/ui/dist/assets/index-D4zoGFcr.js +347 -0
  53. sandboxy/ui/dist/index.html +14 -0
  54. sandboxy/utils/__init__.py +3 -0
  55. sandboxy/utils/time.py +20 -0
  56. sandboxy-0.0.1.dist-info/METADATA +241 -0
  57. sandboxy-0.0.1.dist-info/RECORD +60 -0
  58. sandboxy-0.0.1.dist-info/WHEEL +4 -0
  59. sandboxy-0.0.1.dist-info/entry_points.txt +3 -0
  60. sandboxy-0.0.1.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,958 @@
1
+ """OpenRouter provider - unified API for 400+ models."""
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ import time
7
+ from collections.abc import AsyncIterator
8
+ from typing import Any
9
+
10
+ import httpx
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ from sandboxy.providers.base import BaseProvider, ModelInfo, ModelResponse, ProviderError
15
+
16
+ # Popular models with their metadata (subset - OpenRouter has 400+)
17
+ OPENROUTER_MODELS = {
18
+ # ==========================================================================
19
+ # OpenAI (Latest: GPT-5.x series)
20
+ # ==========================================================================
21
+ "openai/gpt-5.2-pro": ModelInfo(
22
+ id="openai/gpt-5.2-pro",
23
+ name="GPT-5.2 Pro",
24
+ provider="openai",
25
+ context_length=400000,
26
+ input_cost_per_million=21.00,
27
+ output_cost_per_million=168.00,
28
+ supports_vision=True,
29
+ ),
30
+ "openai/gpt-5.2": ModelInfo(
31
+ id="openai/gpt-5.2",
32
+ name="GPT-5.2",
33
+ provider="openai",
34
+ context_length=400000,
35
+ input_cost_per_million=1.75,
36
+ output_cost_per_million=14.00,
37
+ supports_vision=True,
38
+ ),
39
+ "openai/gpt-5.2-codex": ModelInfo(
40
+ id="openai/gpt-5.2-codex",
41
+ name="GPT-5.2 Codex",
42
+ provider="openai",
43
+ context_length=400000,
44
+ input_cost_per_million=1.75,
45
+ output_cost_per_million=14.00,
46
+ supports_vision=True,
47
+ ),
48
+ "openai/gpt-5-mini": ModelInfo(
49
+ id="openai/gpt-5-mini",
50
+ name="GPT-5 Mini",
51
+ provider="openai",
52
+ context_length=128000,
53
+ input_cost_per_million=0.30,
54
+ output_cost_per_million=2.40,
55
+ supports_vision=True,
56
+ ),
57
+ "openai/gpt-5.1": ModelInfo(
58
+ id="openai/gpt-5.1",
59
+ name="GPT-5.1",
60
+ provider="openai",
61
+ context_length=400000,
62
+ input_cost_per_million=1.25,
63
+ output_cost_per_million=10.00,
64
+ supports_vision=True,
65
+ ),
66
+ "openai/gpt-5.1-codex": ModelInfo(
67
+ id="openai/gpt-5.1-codex",
68
+ name="GPT-5.1 Codex",
69
+ provider="openai",
70
+ context_length=400000,
71
+ input_cost_per_million=0.30,
72
+ output_cost_per_million=2.40,
73
+ supports_vision=True,
74
+ ),
75
+ "openai/gpt-4.1": ModelInfo(
76
+ id="openai/gpt-4.1",
77
+ name="GPT-4.1",
78
+ provider="openai",
79
+ context_length=1000000,
80
+ input_cost_per_million=2.00,
81
+ output_cost_per_million=8.00,
82
+ supports_vision=True,
83
+ ),
84
+ "openai/gpt-4.1-mini": ModelInfo(
85
+ id="openai/gpt-4.1-mini",
86
+ name="GPT-4.1 Mini",
87
+ provider="openai",
88
+ context_length=1000000,
89
+ input_cost_per_million=0.40,
90
+ output_cost_per_million=1.60,
91
+ supports_vision=True,
92
+ ),
93
+ "openai/gpt-4.1-nano": ModelInfo(
94
+ id="openai/gpt-4.1-nano",
95
+ name="GPT-4.1 Nano",
96
+ provider="openai",
97
+ context_length=1000000,
98
+ input_cost_per_million=0.10,
99
+ output_cost_per_million=0.40,
100
+ supports_vision=True,
101
+ ),
102
+ "openai/gpt-4o": ModelInfo(
103
+ id="openai/gpt-4o",
104
+ name="GPT-4o",
105
+ provider="openai",
106
+ context_length=128000,
107
+ input_cost_per_million=2.50,
108
+ output_cost_per_million=10.00,
109
+ supports_vision=True,
110
+ ),
111
+ "openai/gpt-4o-mini": ModelInfo(
112
+ id="openai/gpt-4o-mini",
113
+ name="GPT-4o Mini",
114
+ provider="openai",
115
+ context_length=128000,
116
+ input_cost_per_million=0.15,
117
+ output_cost_per_million=0.60,
118
+ supports_vision=True,
119
+ ),
120
+ "openai/o3": ModelInfo(
121
+ id="openai/o3",
122
+ name="o3",
123
+ provider="openai",
124
+ context_length=200000,
125
+ input_cost_per_million=20.00,
126
+ output_cost_per_million=80.00,
127
+ ),
128
+ "openai/o3-mini": ModelInfo(
129
+ id="openai/o3-mini",
130
+ name="o3 Mini",
131
+ provider="openai",
132
+ context_length=200000,
133
+ input_cost_per_million=1.10,
134
+ output_cost_per_million=4.40,
135
+ ),
136
+ "openai/o1": ModelInfo(
137
+ id="openai/o1",
138
+ name="o1",
139
+ provider="openai",
140
+ context_length=200000,
141
+ input_cost_per_million=15.00,
142
+ output_cost_per_million=60.00,
143
+ ),
144
+ "openai/o1-mini": ModelInfo(
145
+ id="openai/o1-mini",
146
+ name="o1 Mini",
147
+ provider="openai",
148
+ context_length=128000,
149
+ input_cost_per_million=3.00,
150
+ output_cost_per_million=12.00,
151
+ ),
152
+ "openai/o1-pro": ModelInfo(
153
+ id="openai/o1-pro",
154
+ name="o1 Pro",
155
+ provider="openai",
156
+ context_length=200000,
157
+ input_cost_per_million=150.00,
158
+ output_cost_per_million=600.00,
159
+ ),
160
+ # ==========================================================================
161
+ # Anthropic (Latest: Claude 4.5 series)
162
+ # ==========================================================================
163
+ "anthropic/claude-opus-4.5": ModelInfo(
164
+ id="anthropic/claude-opus-4.5",
165
+ name="Claude Opus 4.5",
166
+ provider="anthropic",
167
+ context_length=200000,
168
+ input_cost_per_million=5.00,
169
+ output_cost_per_million=25.00,
170
+ supports_vision=True,
171
+ ),
172
+ "anthropic/claude-sonnet-4.5": ModelInfo(
173
+ id="anthropic/claude-sonnet-4.5",
174
+ name="Claude Sonnet 4.5",
175
+ provider="anthropic",
176
+ context_length=1000000,
177
+ input_cost_per_million=3.00,
178
+ output_cost_per_million=15.00,
179
+ supports_vision=True,
180
+ ),
181
+ "anthropic/claude-haiku-4.5": ModelInfo(
182
+ id="anthropic/claude-haiku-4.5",
183
+ name="Claude Haiku 4.5",
184
+ provider="anthropic",
185
+ context_length=200000,
186
+ input_cost_per_million=1.00,
187
+ output_cost_per_million=5.00,
188
+ supports_vision=True,
189
+ ),
190
+ "anthropic/claude-sonnet-4": ModelInfo(
191
+ id="anthropic/claude-sonnet-4",
192
+ name="Claude Sonnet 4",
193
+ provider="anthropic",
194
+ context_length=200000,
195
+ input_cost_per_million=3.00,
196
+ output_cost_per_million=15.00,
197
+ supports_vision=True,
198
+ ),
199
+ "anthropic/claude-opus-4": ModelInfo(
200
+ id="anthropic/claude-opus-4",
201
+ name="Claude Opus 4",
202
+ provider="anthropic",
203
+ context_length=200000,
204
+ input_cost_per_million=15.00,
205
+ output_cost_per_million=75.00,
206
+ supports_vision=True,
207
+ ),
208
+ "anthropic/claude-3.5-sonnet": ModelInfo(
209
+ id="anthropic/claude-3.5-sonnet",
210
+ name="Claude 3.5 Sonnet",
211
+ provider="anthropic",
212
+ context_length=200000,
213
+ input_cost_per_million=3.00,
214
+ output_cost_per_million=15.00,
215
+ supports_vision=True,
216
+ ),
217
+ "anthropic/claude-3.5-haiku": ModelInfo(
218
+ id="anthropic/claude-3.5-haiku",
219
+ name="Claude 3.5 Haiku",
220
+ provider="anthropic",
221
+ context_length=200000,
222
+ input_cost_per_million=0.80,
223
+ output_cost_per_million=4.00,
224
+ supports_vision=True,
225
+ ),
226
+ "anthropic/claude-3-opus": ModelInfo(
227
+ id="anthropic/claude-3-opus",
228
+ name="Claude 3 Opus",
229
+ provider="anthropic",
230
+ context_length=200000,
231
+ input_cost_per_million=15.00,
232
+ output_cost_per_million=75.00,
233
+ supports_vision=True,
234
+ ),
235
+ "anthropic/claude-3-haiku": ModelInfo(
236
+ id="anthropic/claude-3-haiku",
237
+ name="Claude 3 Haiku",
238
+ provider="anthropic",
239
+ context_length=200000,
240
+ input_cost_per_million=0.25,
241
+ output_cost_per_million=1.25,
242
+ supports_vision=True,
243
+ ),
244
+ # ==========================================================================
245
+ # Google (Latest: Gemini 3.x series)
246
+ # ==========================================================================
247
+ "google/gemini-3-pro": ModelInfo(
248
+ id="google/gemini-3-pro",
249
+ name="Gemini 3 Pro",
250
+ provider="google",
251
+ context_length=1048576,
252
+ input_cost_per_million=2.00,
253
+ output_cost_per_million=12.00,
254
+ supports_vision=True,
255
+ ),
256
+ "google/gemini-3-flash": ModelInfo(
257
+ id="google/gemini-3-flash",
258
+ name="Gemini 3 Flash",
259
+ provider="google",
260
+ context_length=1048576,
261
+ input_cost_per_million=0.30,
262
+ output_cost_per_million=1.20,
263
+ supports_vision=True,
264
+ ),
265
+ "google/gemini-2.5-pro": ModelInfo(
266
+ id="google/gemini-2.5-pro",
267
+ name="Gemini 2.5 Pro",
268
+ provider="google",
269
+ context_length=1048576,
270
+ input_cost_per_million=1.25,
271
+ output_cost_per_million=10.00,
272
+ supports_vision=True,
273
+ ),
274
+ "google/gemini-2.5-flash": ModelInfo(
275
+ id="google/gemini-2.5-flash",
276
+ name="Gemini 2.5 Flash",
277
+ provider="google",
278
+ context_length=1048576,
279
+ input_cost_per_million=0.30,
280
+ output_cost_per_million=2.50,
281
+ supports_vision=True,
282
+ ),
283
+ "google/gemini-2.0-flash": ModelInfo(
284
+ id="google/gemini-2.0-flash",
285
+ name="Gemini 2.0 Flash",
286
+ provider="google",
287
+ context_length=1000000,
288
+ input_cost_per_million=0.10,
289
+ output_cost_per_million=0.40,
290
+ supports_vision=True,
291
+ ),
292
+ "google/gemini-2.0-flash-exp:free": ModelInfo(
293
+ id="google/gemini-2.0-flash-exp:free",
294
+ name="Gemini 2.0 Flash (Free)",
295
+ provider="google",
296
+ context_length=1000000,
297
+ input_cost_per_million=0.0,
298
+ output_cost_per_million=0.0,
299
+ supports_vision=True,
300
+ ),
301
+ "google/gemini-2.0-flash-thinking-exp:free": ModelInfo(
302
+ id="google/gemini-2.0-flash-thinking-exp:free",
303
+ name="Gemini 2.0 Flash Thinking (Free)",
304
+ provider="google",
305
+ context_length=1000000,
306
+ input_cost_per_million=0.0,
307
+ output_cost_per_million=0.0,
308
+ supports_vision=True,
309
+ ),
310
+ "google/gemini-pro-1.5": ModelInfo(
311
+ id="google/gemini-pro-1.5",
312
+ name="Gemini Pro 1.5",
313
+ provider="google",
314
+ context_length=2000000,
315
+ input_cost_per_million=1.25,
316
+ output_cost_per_million=5.00,
317
+ supports_vision=True,
318
+ ),
319
+ "google/gemini-flash-1.5": ModelInfo(
320
+ id="google/gemini-flash-1.5",
321
+ name="Gemini Flash 1.5",
322
+ provider="google",
323
+ context_length=1000000,
324
+ input_cost_per_million=0.075,
325
+ output_cost_per_million=0.30,
326
+ supports_vision=True,
327
+ ),
328
+ # ==========================================================================
329
+ # xAI (Latest: Grok 4)
330
+ # ==========================================================================
331
+ "x-ai/grok-4": ModelInfo(
332
+ id="x-ai/grok-4",
333
+ name="Grok 4",
334
+ provider="xai",
335
+ context_length=2000000,
336
+ input_cost_per_million=3.00,
337
+ output_cost_per_million=15.00,
338
+ supports_vision=True,
339
+ ),
340
+ "x-ai/grok-4-fast": ModelInfo(
341
+ id="x-ai/grok-4-fast",
342
+ name="Grok 4 Fast",
343
+ provider="xai",
344
+ context_length=2000000,
345
+ input_cost_per_million=0.20,
346
+ output_cost_per_million=0.50,
347
+ supports_vision=True,
348
+ ),
349
+ "x-ai/grok-3": ModelInfo(
350
+ id="x-ai/grok-3",
351
+ name="Grok 3",
352
+ provider="xai",
353
+ context_length=131072,
354
+ input_cost_per_million=3.00,
355
+ output_cost_per_million=15.00,
356
+ ),
357
+ "x-ai/grok-3-mini": ModelInfo(
358
+ id="x-ai/grok-3-mini",
359
+ name="Grok 3 Mini",
360
+ provider="xai",
361
+ context_length=131072,
362
+ input_cost_per_million=0.30,
363
+ output_cost_per_million=0.50,
364
+ ),
365
+ # ==========================================================================
366
+ # DeepSeek
367
+ # ==========================================================================
368
+ "deepseek/deepseek-chat": ModelInfo(
369
+ id="deepseek/deepseek-chat",
370
+ name="DeepSeek V3",
371
+ provider="deepseek",
372
+ context_length=64000,
373
+ input_cost_per_million=0.14,
374
+ output_cost_per_million=0.28,
375
+ ),
376
+ "deepseek/deepseek-r1": ModelInfo(
377
+ id="deepseek/deepseek-r1",
378
+ name="DeepSeek R1",
379
+ provider="deepseek",
380
+ context_length=64000,
381
+ input_cost_per_million=0.55,
382
+ output_cost_per_million=2.19,
383
+ ),
384
+ "deepseek/deepseek-r1-distill-llama-70b": ModelInfo(
385
+ id="deepseek/deepseek-r1-distill-llama-70b",
386
+ name="DeepSeek R1 Distill Llama 70B",
387
+ provider="deepseek",
388
+ context_length=128000,
389
+ input_cost_per_million=0.23,
390
+ output_cost_per_million=0.69,
391
+ ),
392
+ "deepseek/deepseek-r1-distill-qwen-32b": ModelInfo(
393
+ id="deepseek/deepseek-r1-distill-qwen-32b",
394
+ name="DeepSeek R1 Distill Qwen 32B",
395
+ provider="deepseek",
396
+ context_length=128000,
397
+ input_cost_per_million=0.14,
398
+ output_cost_per_million=0.28,
399
+ ),
400
+ "deepseek/deepseek-r1:free": ModelInfo(
401
+ id="deepseek/deepseek-r1:free",
402
+ name="DeepSeek R1 (Free)",
403
+ provider="deepseek",
404
+ context_length=64000,
405
+ input_cost_per_million=0.0,
406
+ output_cost_per_million=0.0,
407
+ ),
408
+ # ==========================================================================
409
+ # Meta (Llama)
410
+ # ==========================================================================
411
+ "meta-llama/llama-3.3-70b-instruct": ModelInfo(
412
+ id="meta-llama/llama-3.3-70b-instruct",
413
+ name="Llama 3.3 70B",
414
+ provider="meta",
415
+ context_length=131072,
416
+ input_cost_per_million=0.12,
417
+ output_cost_per_million=0.30,
418
+ ),
419
+ "meta-llama/llama-3.3-70b-instruct:free": ModelInfo(
420
+ id="meta-llama/llama-3.3-70b-instruct:free",
421
+ name="Llama 3.3 70B (Free)",
422
+ provider="meta",
423
+ context_length=131072,
424
+ input_cost_per_million=0.0,
425
+ output_cost_per_million=0.0,
426
+ ),
427
+ "meta-llama/llama-3.1-405b-instruct": ModelInfo(
428
+ id="meta-llama/llama-3.1-405b-instruct",
429
+ name="Llama 3.1 405B",
430
+ provider="meta",
431
+ context_length=131072,
432
+ input_cost_per_million=2.00,
433
+ output_cost_per_million=2.00,
434
+ ),
435
+ "meta-llama/llama-3.1-70b-instruct": ModelInfo(
436
+ id="meta-llama/llama-3.1-70b-instruct",
437
+ name="Llama 3.1 70B",
438
+ provider="meta",
439
+ context_length=131072,
440
+ input_cost_per_million=0.35,
441
+ output_cost_per_million=0.40,
442
+ ),
443
+ "meta-llama/llama-3.1-8b-instruct": ModelInfo(
444
+ id="meta-llama/llama-3.1-8b-instruct",
445
+ name="Llama 3.1 8B",
446
+ provider="meta",
447
+ context_length=131072,
448
+ input_cost_per_million=0.05,
449
+ output_cost_per_million=0.08,
450
+ ),
451
+ "meta-llama/llama-3.1-8b-instruct:free": ModelInfo(
452
+ id="meta-llama/llama-3.1-8b-instruct:free",
453
+ name="Llama 3.1 8B (Free)",
454
+ provider="meta",
455
+ context_length=131072,
456
+ input_cost_per_million=0.0,
457
+ output_cost_per_million=0.0,
458
+ ),
459
+ "meta-llama/llama-guard-3-8b": ModelInfo(
460
+ id="meta-llama/llama-guard-3-8b",
461
+ name="Llama Guard 3 8B",
462
+ provider="meta",
463
+ context_length=8192,
464
+ input_cost_per_million=0.05,
465
+ output_cost_per_million=0.05,
466
+ ),
467
+ # ==========================================================================
468
+ # Mistral
469
+ # ==========================================================================
470
+ "mistralai/mistral-large-2411": ModelInfo(
471
+ id="mistralai/mistral-large-2411",
472
+ name="Mistral Large",
473
+ provider="mistral",
474
+ context_length=128000,
475
+ input_cost_per_million=2.00,
476
+ output_cost_per_million=6.00,
477
+ ),
478
+ "mistralai/mistral-medium-3": ModelInfo(
479
+ id="mistralai/mistral-medium-3",
480
+ name="Mistral Medium 3",
481
+ provider="mistral",
482
+ context_length=128000,
483
+ input_cost_per_million=0.40,
484
+ output_cost_per_million=2.00,
485
+ ),
486
+ "mistralai/mistral-small-2409": ModelInfo(
487
+ id="mistralai/mistral-small-2409",
488
+ name="Mistral Small",
489
+ provider="mistral",
490
+ context_length=32000,
491
+ input_cost_per_million=0.20,
492
+ output_cost_per_million=0.60,
493
+ ),
494
+ "mistralai/mistral-small-3.1-24b-instruct": ModelInfo(
495
+ id="mistralai/mistral-small-3.1-24b-instruct",
496
+ name="Mistral Small 3.1 24B",
497
+ provider="mistral",
498
+ context_length=96000,
499
+ input_cost_per_million=0.10,
500
+ output_cost_per_million=0.30,
501
+ ),
502
+ "mistralai/ministral-8b": ModelInfo(
503
+ id="mistralai/ministral-8b",
504
+ name="Ministral 8B",
505
+ provider="mistral",
506
+ context_length=128000,
507
+ input_cost_per_million=0.10,
508
+ output_cost_per_million=0.10,
509
+ ),
510
+ "mistralai/ministral-3b": ModelInfo(
511
+ id="mistralai/ministral-3b",
512
+ name="Ministral 3B",
513
+ provider="mistral",
514
+ context_length=128000,
515
+ input_cost_per_million=0.04,
516
+ output_cost_per_million=0.04,
517
+ ),
518
+ "mistralai/codestral-2501": ModelInfo(
519
+ id="mistralai/codestral-2501",
520
+ name="Codestral",
521
+ provider="mistral",
522
+ context_length=256000,
523
+ input_cost_per_million=0.30,
524
+ output_cost_per_million=0.90,
525
+ ),
526
+ "mistralai/codestral-mamba": ModelInfo(
527
+ id="mistralai/codestral-mamba",
528
+ name="Codestral Mamba",
529
+ provider="mistral",
530
+ context_length=256000,
531
+ input_cost_per_million=0.25,
532
+ output_cost_per_million=0.25,
533
+ ),
534
+ "mistralai/pixtral-large-2411": ModelInfo(
535
+ id="mistralai/pixtral-large-2411",
536
+ name="Pixtral Large",
537
+ provider="mistral",
538
+ context_length=128000,
539
+ input_cost_per_million=2.00,
540
+ output_cost_per_million=6.00,
541
+ supports_vision=True,
542
+ ),
543
+ "mistralai/pixtral-12b": ModelInfo(
544
+ id="mistralai/pixtral-12b",
545
+ name="Pixtral 12B",
546
+ provider="mistral",
547
+ context_length=128000,
548
+ input_cost_per_million=0.10,
549
+ output_cost_per_million=0.10,
550
+ supports_vision=True,
551
+ ),
552
+ # ==========================================================================
553
+ # Qwen
554
+ # ==========================================================================
555
+ "qwen/qwen-2.5-72b-instruct": ModelInfo(
556
+ id="qwen/qwen-2.5-72b-instruct",
557
+ name="Qwen 2.5 72B",
558
+ provider="qwen",
559
+ context_length=131072,
560
+ input_cost_per_million=0.35,
561
+ output_cost_per_million=0.40,
562
+ ),
563
+ "qwen/qwen-2.5-32b-instruct": ModelInfo(
564
+ id="qwen/qwen-2.5-32b-instruct",
565
+ name="Qwen 2.5 32B",
566
+ provider="qwen",
567
+ context_length=131072,
568
+ input_cost_per_million=0.18,
569
+ output_cost_per_million=0.18,
570
+ ),
571
+ "qwen/qwen-2.5-7b-instruct": ModelInfo(
572
+ id="qwen/qwen-2.5-7b-instruct",
573
+ name="Qwen 2.5 7B",
574
+ provider="qwen",
575
+ context_length=131072,
576
+ input_cost_per_million=0.05,
577
+ output_cost_per_million=0.05,
578
+ ),
579
+ "qwen/qwen-2.5-coder-32b-instruct": ModelInfo(
580
+ id="qwen/qwen-2.5-coder-32b-instruct",
581
+ name="Qwen 2.5 Coder 32B",
582
+ provider="qwen",
583
+ context_length=131072,
584
+ input_cost_per_million=0.18,
585
+ output_cost_per_million=0.18,
586
+ ),
587
+ "qwen/qwq-32b-preview": ModelInfo(
588
+ id="qwen/qwq-32b-preview",
589
+ name="QwQ 32B (Reasoning)",
590
+ provider="qwen",
591
+ context_length=32768,
592
+ input_cost_per_million=0.12,
593
+ output_cost_per_million=0.18,
594
+ ),
595
+ "qwen/qwen-2.5-72b-instruct:free": ModelInfo(
596
+ id="qwen/qwen-2.5-72b-instruct:free",
597
+ name="Qwen 2.5 72B (Free)",
598
+ provider="qwen",
599
+ context_length=131072,
600
+ input_cost_per_million=0.0,
601
+ output_cost_per_million=0.0,
602
+ ),
603
+ "qwen/qwen-2-vl-72b-instruct": ModelInfo(
604
+ id="qwen/qwen-2-vl-72b-instruct",
605
+ name="Qwen 2 VL 72B",
606
+ provider="qwen",
607
+ context_length=32768,
608
+ input_cost_per_million=0.40,
609
+ output_cost_per_million=0.40,
610
+ supports_vision=True,
611
+ ),
612
+ # ==========================================================================
613
+ # Cohere
614
+ # ==========================================================================
615
+ "cohere/command-r-plus": ModelInfo(
616
+ id="cohere/command-r-plus",
617
+ name="Command R+",
618
+ provider="cohere",
619
+ context_length=128000,
620
+ input_cost_per_million=2.50,
621
+ output_cost_per_million=10.00,
622
+ ),
623
+ "cohere/command-r": ModelInfo(
624
+ id="cohere/command-r",
625
+ name="Command R",
626
+ provider="cohere",
627
+ context_length=128000,
628
+ input_cost_per_million=0.15,
629
+ output_cost_per_million=0.60,
630
+ ),
631
+ "cohere/command-r7b-12-2024": ModelInfo(
632
+ id="cohere/command-r7b-12-2024",
633
+ name="Command R 7B",
634
+ provider="cohere",
635
+ context_length=128000,
636
+ input_cost_per_million=0.0375,
637
+ output_cost_per_million=0.15,
638
+ ),
639
+ # ==========================================================================
640
+ # Perplexity
641
+ # ==========================================================================
642
+ "perplexity/sonar-pro": ModelInfo(
643
+ id="perplexity/sonar-pro",
644
+ name="Sonar Pro (Online)",
645
+ provider="perplexity",
646
+ context_length=200000,
647
+ input_cost_per_million=3.00,
648
+ output_cost_per_million=15.00,
649
+ ),
650
+ "perplexity/sonar": ModelInfo(
651
+ id="perplexity/sonar",
652
+ name="Sonar (Online)",
653
+ provider="perplexity",
654
+ context_length=128000,
655
+ input_cost_per_million=1.00,
656
+ output_cost_per_million=1.00,
657
+ ),
658
+ "perplexity/sonar-reasoning": ModelInfo(
659
+ id="perplexity/sonar-reasoning",
660
+ name="Sonar Reasoning (Online)",
661
+ provider="perplexity",
662
+ context_length=128000,
663
+ input_cost_per_million=1.00,
664
+ output_cost_per_million=5.00,
665
+ ),
666
+ # ==========================================================================
667
+ # AI21
668
+ # ==========================================================================
669
+ "ai21/jamba-1.5-large": ModelInfo(
670
+ id="ai21/jamba-1.5-large",
671
+ name="Jamba 1.5 Large",
672
+ provider="ai21",
673
+ context_length=256000,
674
+ input_cost_per_million=2.00,
675
+ output_cost_per_million=8.00,
676
+ ),
677
+ "ai21/jamba-1.5-mini": ModelInfo(
678
+ id="ai21/jamba-1.5-mini",
679
+ name="Jamba 1.5 Mini",
680
+ provider="ai21",
681
+ context_length=256000,
682
+ input_cost_per_million=0.20,
683
+ output_cost_per_million=0.40,
684
+ ),
685
+ # ==========================================================================
686
+ # Amazon
687
+ # ==========================================================================
688
+ "amazon/nova-pro-v1": ModelInfo(
689
+ id="amazon/nova-pro-v1",
690
+ name="Amazon Nova Pro",
691
+ provider="amazon",
692
+ context_length=300000,
693
+ input_cost_per_million=0.80,
694
+ output_cost_per_million=3.20,
695
+ supports_vision=True,
696
+ ),
697
+ "amazon/nova-lite-v1": ModelInfo(
698
+ id="amazon/nova-lite-v1",
699
+ name="Amazon Nova Lite",
700
+ provider="amazon",
701
+ context_length=300000,
702
+ input_cost_per_million=0.06,
703
+ output_cost_per_million=0.24,
704
+ supports_vision=True,
705
+ ),
706
+ "amazon/nova-micro-v1": ModelInfo(
707
+ id="amazon/nova-micro-v1",
708
+ name="Amazon Nova Micro",
709
+ provider="amazon",
710
+ context_length=128000,
711
+ input_cost_per_million=0.035,
712
+ output_cost_per_million=0.14,
713
+ ),
714
+ # ==========================================================================
715
+ # Microsoft
716
+ # ==========================================================================
717
+ "microsoft/phi-4": ModelInfo(
718
+ id="microsoft/phi-4",
719
+ name="Phi-4",
720
+ provider="microsoft",
721
+ context_length=16384,
722
+ input_cost_per_million=0.07,
723
+ output_cost_per_million=0.14,
724
+ ),
725
+ "microsoft/wizardlm-2-8x22b": ModelInfo(
726
+ id="microsoft/wizardlm-2-8x22b",
727
+ name="WizardLM 2 8x22B",
728
+ provider="microsoft",
729
+ context_length=65536,
730
+ input_cost_per_million=0.50,
731
+ output_cost_per_million=0.50,
732
+ ),
733
+ # ==========================================================================
734
+ # Nous Research
735
+ # ==========================================================================
736
+ "nousresearch/hermes-3-llama-3.1-405b": ModelInfo(
737
+ id="nousresearch/hermes-3-llama-3.1-405b",
738
+ name="Hermes 3 405B",
739
+ provider="nous",
740
+ context_length=131072,
741
+ input_cost_per_million=2.00,
742
+ output_cost_per_million=2.00,
743
+ ),
744
+ "nousresearch/hermes-3-llama-3.1-70b": ModelInfo(
745
+ id="nousresearch/hermes-3-llama-3.1-70b",
746
+ name="Hermes 3 70B",
747
+ provider="nous",
748
+ context_length=131072,
749
+ input_cost_per_million=0.35,
750
+ output_cost_per_million=0.40,
751
+ ),
752
+ # ==========================================================================
753
+ # Other Notable Models
754
+ # ==========================================================================
755
+ "nvidia/llama-3.1-nemotron-70b-instruct": ModelInfo(
756
+ id="nvidia/llama-3.1-nemotron-70b-instruct",
757
+ name="Nemotron 70B",
758
+ provider="nvidia",
759
+ context_length=131072,
760
+ input_cost_per_million=0.35,
761
+ output_cost_per_million=0.40,
762
+ ),
763
+ "databricks/dbrx-instruct": ModelInfo(
764
+ id="databricks/dbrx-instruct",
765
+ name="DBRX Instruct",
766
+ provider="databricks",
767
+ context_length=32768,
768
+ input_cost_per_million=0.75,
769
+ output_cost_per_million=0.75,
770
+ ),
771
+ "inflection/inflection-3-pi": ModelInfo(
772
+ id="inflection/inflection-3-pi",
773
+ name="Inflection 3 Pi",
774
+ provider="inflection",
775
+ context_length=8192,
776
+ input_cost_per_million=0.80,
777
+ output_cost_per_million=3.20,
778
+ ),
779
+ "sao10k/l3.3-euryale-70b": ModelInfo(
780
+ id="sao10k/l3.3-euryale-70b",
781
+ name="Euryale 70B",
782
+ provider="sao10k",
783
+ context_length=131072,
784
+ input_cost_per_million=0.50,
785
+ output_cost_per_million=0.60,
786
+ ),
787
+ }
788
+
789
+
790
+ class OpenRouterProvider(BaseProvider):
791
+ """OpenRouter - unified API for 400+ models.
792
+
793
+ OpenRouter provides access to models from OpenAI, Anthropic, Google,
794
+ Meta, Mistral, and many others through a single API endpoint.
795
+
796
+ No markup on provider pricing - you pay what you'd pay directly.
797
+ """
798
+
799
+ provider_name = "openrouter"
800
+ base_url = "https://openrouter.ai/api/v1"
801
+
802
+ def __init__(self, api_key: str | None = None):
803
+ """Initialize OpenRouter provider.
804
+
805
+ Args:
806
+ api_key: OpenRouter API key. If not provided, reads from
807
+ OPENROUTER_API_KEY environment variable.
808
+ """
809
+ self.api_key = api_key or os.getenv("OPENROUTER_API_KEY")
810
+ if not self.api_key:
811
+ raise ProviderError(
812
+ "API key required. Set OPENROUTER_API_KEY or pass api_key.",
813
+ provider=self.provider_name,
814
+ )
815
+
816
+ def _get_headers(self) -> dict[str, str]:
817
+ """Get request headers."""
818
+ return {
819
+ "Authorization": f"Bearer {self.api_key}",
820
+ "Content-Type": "application/json",
821
+ "HTTP-Referer": "https://sandboxy.ai",
822
+ "X-Title": "Sandboxy",
823
+ }
824
+
825
+ async def complete(
826
+ self,
827
+ model: str,
828
+ messages: list[dict[str, Any]],
829
+ temperature: float = 0.7,
830
+ max_tokens: int = 1024,
831
+ **kwargs: Any,
832
+ ) -> ModelResponse:
833
+ """Send completion request via OpenRouter."""
834
+ start_time = time.time()
835
+
836
+ payload = {
837
+ "model": model,
838
+ "messages": messages,
839
+ "temperature": temperature,
840
+ "max_tokens": max_tokens,
841
+ **kwargs,
842
+ }
843
+
844
+ async with httpx.AsyncClient(timeout=120.0) as client:
845
+ try:
846
+ response = await client.post(
847
+ f"{self.base_url}/chat/completions",
848
+ headers=self._get_headers(),
849
+ json=payload,
850
+ )
851
+ response.raise_for_status()
852
+ data = response.json()
853
+ except httpx.HTTPStatusError as e:
854
+ error_body = e.response.text
855
+ raise ProviderError(
856
+ f"HTTP {e.response.status_code}: {error_body}",
857
+ provider=self.provider_name,
858
+ model=model,
859
+ ) from e
860
+ except httpx.RequestError as e:
861
+ raise ProviderError(
862
+ f"Request failed: {e}",
863
+ provider=self.provider_name,
864
+ model=model,
865
+ ) from e
866
+
867
+ latency_ms = int((time.time() - start_time) * 1000)
868
+
869
+ # Extract response data
870
+ choice = data.get("choices", [{}])[0]
871
+ message = choice.get("message", {})
872
+ usage = data.get("usage", {})
873
+
874
+ # Calculate cost if we have token counts
875
+ input_tokens = usage.get("prompt_tokens", 0)
876
+ output_tokens = usage.get("completion_tokens", 0)
877
+ cost = self._calculate_cost(model, input_tokens, output_tokens)
878
+
879
+ return ModelResponse(
880
+ content=message.get("content", ""),
881
+ model_id=data.get("model", model),
882
+ latency_ms=latency_ms,
883
+ input_tokens=input_tokens,
884
+ output_tokens=output_tokens,
885
+ cost_usd=cost,
886
+ finish_reason=choice.get("finish_reason"),
887
+ raw_response=data,
888
+ )
889
+
890
+ async def stream(
891
+ self,
892
+ model: str,
893
+ messages: list[dict[str, Any]],
894
+ temperature: float = 0.7,
895
+ max_tokens: int = 1024,
896
+ **kwargs: Any,
897
+ ) -> AsyncIterator[str]:
898
+ """Stream completion response."""
899
+ payload = {
900
+ "model": model,
901
+ "messages": messages,
902
+ "temperature": temperature,
903
+ "max_tokens": max_tokens,
904
+ "stream": True,
905
+ **kwargs,
906
+ }
907
+
908
+ async with (
909
+ httpx.AsyncClient(timeout=120.0) as client,
910
+ client.stream(
911
+ "POST",
912
+ f"{self.base_url}/chat/completions",
913
+ headers=self._get_headers(),
914
+ json=payload,
915
+ ) as response,
916
+ ):
917
+ response.raise_for_status()
918
+ async for line in response.aiter_lines():
919
+ if line.startswith("data: "):
920
+ data = line[6:]
921
+ if data == "[DONE]":
922
+ break
923
+ try:
924
+ chunk = json.loads(data)
925
+ delta = chunk.get("choices", [{}])[0].get("delta", {})
926
+ content = delta.get("content", "")
927
+ if content:
928
+ yield content
929
+ except (json.JSONDecodeError, KeyError):
930
+ continue
931
+
932
+ def list_models(self) -> list[ModelInfo]:
933
+ """List popular models available through OpenRouter."""
934
+ return list(OPENROUTER_MODELS.values())
935
+
936
+ def _calculate_cost(self, model: str, input_tokens: int, output_tokens: int) -> float | None:
937
+ """Calculate cost in USD for a request."""
938
+ model_info = OPENROUTER_MODELS.get(model)
939
+ if not model_info or not model_info.input_cost_per_million:
940
+ return None
941
+
942
+ input_cost = (input_tokens / 1_000_000) * model_info.input_cost_per_million
943
+ output_cost = (output_tokens / 1_000_000) * model_info.output_cost_per_million
944
+ return round(input_cost + output_cost, 6)
945
+
946
+ async def fetch_models(self) -> list[dict[str, Any]]:
947
+ """Fetch full model list from OpenRouter API.
948
+
949
+ Returns live model data including pricing and availability.
950
+ """
951
+ async with httpx.AsyncClient(timeout=30.0) as client:
952
+ response = await client.get(
953
+ f"{self.base_url}/models",
954
+ headers=self._get_headers(),
955
+ )
956
+ response.raise_for_status()
957
+ data = response.json()
958
+ return data.get("data", [])