loopers-client 0.4.0__tar.gz
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.
- loopers_client-0.4.0/PKG-INFO +117 -0
- loopers_client-0.4.0/README.md +101 -0
- loopers_client-0.4.0/loopers_client/__init__.py +13 -0
- loopers_client-0.4.0/loopers_client/client.py +294 -0
- loopers_client-0.4.0/loopers_client.egg-info/PKG-INFO +117 -0
- loopers_client-0.4.0/loopers_client.egg-info/SOURCES.txt +9 -0
- loopers_client-0.4.0/loopers_client.egg-info/dependency_links.txt +1 -0
- loopers_client-0.4.0/loopers_client.egg-info/requires.txt +7 -0
- loopers_client-0.4.0/loopers_client.egg-info/top_level.txt +1 -0
- loopers_client-0.4.0/pyproject.toml +27 -0
- loopers_client-0.4.0/setup.cfg +4 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: loopers-client
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: A premium Python client wrapper for the Loopers AI budget & rate-limit proxy.
|
|
5
|
+
License: MIT
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.7
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: httpx>=0.20.0
|
|
12
|
+
Provides-Extra: openai
|
|
13
|
+
Requires-Dist: openai>=1.0.0; extra == "openai"
|
|
14
|
+
Provides-Extra: anthropic
|
|
15
|
+
Requires-Dist: anthropic>=0.3.0; extra == "anthropic"
|
|
16
|
+
|
|
17
|
+
# Loopers Python Client SDK (`loopers-client`)
|
|
18
|
+
|
|
19
|
+
The `loopers-client` package provides a drop-in wrapper around official OpenAI and Anthropic SDK clients to make integration with the Loopers cost firewall seamless.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install loopers-client
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Additionally, install the official provider package you plan to use:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install openai
|
|
31
|
+
# or
|
|
32
|
+
pip install anthropic
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- **Automatic Headers Injection**: Automatically handles injection of Loopers proxy keys (`Authorization`), upstream provider keys (`X-Loopers-Provider-Key`), and session budget limits.
|
|
38
|
+
- **Custom Attributes**: Intercepts response headers and attaches cost metadata directly to the returned objects.
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
### OpenAI Integration
|
|
43
|
+
|
|
44
|
+
Replace `openai.OpenAI` with `LoopersOpenAI`:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from loopers_client import LoopersOpenAI
|
|
48
|
+
|
|
49
|
+
# Initialize client
|
|
50
|
+
client = LoopersOpenAI(
|
|
51
|
+
loopers_url="http://localhost:8080",
|
|
52
|
+
loopers_key="lp-xxx", # Loopers proxy key
|
|
53
|
+
provider_key="sk-proj-xxx", # Upstream OpenAI key
|
|
54
|
+
session_id="agent-run-123", # Optional: track steps and budget for an agent session
|
|
55
|
+
session_budget=2.50, # Optional: limit session to $2.50
|
|
56
|
+
max_steps=20 # Optional: limit session to 20 steps
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Call completions exactly like the official client
|
|
60
|
+
response = client.chat.completions.create(
|
|
61
|
+
model="gpt-4o",
|
|
62
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Inspect budget/cost metadata attached to response
|
|
66
|
+
print(f"Request Cost: ${response.loopers_cost} USD")
|
|
67
|
+
print(f"Estimated Cost: ${response.loopers_cost_estimated} USD")
|
|
68
|
+
print(f"Session Spend: ${response.loopers_session_spend} USD")
|
|
69
|
+
print(f"Session Steps: {response.loopers_session_steps}")
|
|
70
|
+
print(f"Session Remaining: ${response.loopers_session_remaining} USD")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Async OpenAI
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import asyncio
|
|
77
|
+
from loopers_client import LoopersAsyncOpenAI
|
|
78
|
+
|
|
79
|
+
async def main():
|
|
80
|
+
client = LoopersAsyncOpenAI(
|
|
81
|
+
loopers_url="http://localhost:8080",
|
|
82
|
+
loopers_key="lp-xxx",
|
|
83
|
+
provider_key="sk-proj-xxx"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
response = await client.chat.completions.create(
|
|
87
|
+
model="gpt-4o",
|
|
88
|
+
messages=[{"role": "user", "content": "Hello!"}]
|
|
89
|
+
)
|
|
90
|
+
print(response.loopers_cost)
|
|
91
|
+
|
|
92
|
+
asyncio.run(main())
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Anthropic Integration
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from loopers_client import LoopersAnthropic
|
|
99
|
+
|
|
100
|
+
client = LoopersAnthropic(
|
|
101
|
+
loopers_url="http://localhost:8080",
|
|
102
|
+
loopers_key="lp-xxx",
|
|
103
|
+
provider_key="sk-ant-xxx"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
response = client.messages.create(
|
|
107
|
+
model="claude-3-5-sonnet-latest",
|
|
108
|
+
max_tokens=1000,
|
|
109
|
+
messages=[{"role": "user", "content": "Hello!"}]
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
print(response.loopers_cost)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Loopers Python Client SDK (`loopers-client`)
|
|
2
|
+
|
|
3
|
+
The `loopers-client` package provides a drop-in wrapper around official OpenAI and Anthropic SDK clients to make integration with the Loopers cost firewall seamless.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install loopers-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Additionally, install the official provider package you plan to use:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install openai
|
|
15
|
+
# or
|
|
16
|
+
pip install anthropic
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **Automatic Headers Injection**: Automatically handles injection of Loopers proxy keys (`Authorization`), upstream provider keys (`X-Loopers-Provider-Key`), and session budget limits.
|
|
22
|
+
- **Custom Attributes**: Intercepts response headers and attaches cost metadata directly to the returned objects.
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
### OpenAI Integration
|
|
27
|
+
|
|
28
|
+
Replace `openai.OpenAI` with `LoopersOpenAI`:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from loopers_client import LoopersOpenAI
|
|
32
|
+
|
|
33
|
+
# Initialize client
|
|
34
|
+
client = LoopersOpenAI(
|
|
35
|
+
loopers_url="http://localhost:8080",
|
|
36
|
+
loopers_key="lp-xxx", # Loopers proxy key
|
|
37
|
+
provider_key="sk-proj-xxx", # Upstream OpenAI key
|
|
38
|
+
session_id="agent-run-123", # Optional: track steps and budget for an agent session
|
|
39
|
+
session_budget=2.50, # Optional: limit session to $2.50
|
|
40
|
+
max_steps=20 # Optional: limit session to 20 steps
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Call completions exactly like the official client
|
|
44
|
+
response = client.chat.completions.create(
|
|
45
|
+
model="gpt-4o",
|
|
46
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Inspect budget/cost metadata attached to response
|
|
50
|
+
print(f"Request Cost: ${response.loopers_cost} USD")
|
|
51
|
+
print(f"Estimated Cost: ${response.loopers_cost_estimated} USD")
|
|
52
|
+
print(f"Session Spend: ${response.loopers_session_spend} USD")
|
|
53
|
+
print(f"Session Steps: {response.loopers_session_steps}")
|
|
54
|
+
print(f"Session Remaining: ${response.loopers_session_remaining} USD")
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Async OpenAI
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import asyncio
|
|
61
|
+
from loopers_client import LoopersAsyncOpenAI
|
|
62
|
+
|
|
63
|
+
async def main():
|
|
64
|
+
client = LoopersAsyncOpenAI(
|
|
65
|
+
loopers_url="http://localhost:8080",
|
|
66
|
+
loopers_key="lp-xxx",
|
|
67
|
+
provider_key="sk-proj-xxx"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
response = await client.chat.completions.create(
|
|
71
|
+
model="gpt-4o",
|
|
72
|
+
messages=[{"role": "user", "content": "Hello!"}]
|
|
73
|
+
)
|
|
74
|
+
print(response.loopers_cost)
|
|
75
|
+
|
|
76
|
+
asyncio.run(main())
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Anthropic Integration
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from loopers_client import LoopersAnthropic
|
|
83
|
+
|
|
84
|
+
client = LoopersAnthropic(
|
|
85
|
+
loopers_url="http://localhost:8080",
|
|
86
|
+
loopers_key="lp-xxx",
|
|
87
|
+
provider_key="sk-ant-xxx"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
response = client.messages.create(
|
|
91
|
+
model="claude-3-5-sonnet-latest",
|
|
92
|
+
max_tokens=1000,
|
|
93
|
+
messages=[{"role": "user", "content": "Hello!"}]
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
print(response.loopers_cost)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import contextvars
|
|
2
|
+
from typing import Optional, Any
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
# Context variable to hold response headers of the last request safely in multithreaded/async contexts
|
|
7
|
+
_last_headers_context = contextvars.ContextVar("last_headers", default={})
|
|
8
|
+
|
|
9
|
+
def _get_last_headers() -> dict:
|
|
10
|
+
return _last_headers_context.get()
|
|
11
|
+
|
|
12
|
+
def _set_last_headers(headers: dict):
|
|
13
|
+
_last_headers_context.set(headers)
|
|
14
|
+
|
|
15
|
+
def _response_hook(response: httpx.Response):
|
|
16
|
+
headers = response.headers
|
|
17
|
+
loopers_headers = {
|
|
18
|
+
"cost": headers.get("X-Loopers-Request-Cost"),
|
|
19
|
+
"cost_estimated": headers.get("X-Loopers-Request-Cost-Estimated"),
|
|
20
|
+
"session_spend": headers.get("X-Loopers-Session-Spend"),
|
|
21
|
+
"session_steps": headers.get("X-Loopers-Session-Steps"),
|
|
22
|
+
"session_remaining": headers.get("X-Loopers-Session-Remaining"),
|
|
23
|
+
}
|
|
24
|
+
_set_last_headers(loopers_headers)
|
|
25
|
+
|
|
26
|
+
async def _async_response_hook(response: httpx.Response):
|
|
27
|
+
_response_hook(response)
|
|
28
|
+
|
|
29
|
+
def _attach_loopers_attributes(res: Any):
|
|
30
|
+
headers = _get_last_headers()
|
|
31
|
+
if not headers:
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
# Safely attach Loopers metrics to the returned resource/completion/stream object
|
|
35
|
+
for name, header_key in [
|
|
36
|
+
("loopers_cost", "cost"),
|
|
37
|
+
("loopers_cost_estimated", "cost_estimated"),
|
|
38
|
+
("loopers_session_spend", "session_spend"),
|
|
39
|
+
("loopers_session_remaining", "session_remaining"),
|
|
40
|
+
]:
|
|
41
|
+
val = headers.get(header_key)
|
|
42
|
+
try:
|
|
43
|
+
setattr(res, name, float(val) if val else None)
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
steps_val = headers.get("session_steps")
|
|
48
|
+
try:
|
|
49
|
+
setattr(res, "loopers_session_steps", int(steps_val) if steps_val else None)
|
|
50
|
+
except Exception:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Try importing openai
|
|
55
|
+
try:
|
|
56
|
+
import openai
|
|
57
|
+
HAS_OPENAI = True
|
|
58
|
+
except ImportError:
|
|
59
|
+
HAS_OPENAI = False
|
|
60
|
+
|
|
61
|
+
if HAS_OPENAI:
|
|
62
|
+
class LoopersOpenAI(openai.OpenAI):
|
|
63
|
+
"""
|
|
64
|
+
A subclass of openai.OpenAI that automatically routes calls through
|
|
65
|
+
the Loopers budget/rate-limit proxy and parses response metrics.
|
|
66
|
+
"""
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
loopers_url: str,
|
|
70
|
+
loopers_key: str,
|
|
71
|
+
provider_key: Optional[str] = None,
|
|
72
|
+
session_id: Optional[str] = None,
|
|
73
|
+
session_budget: Optional[float] = None,
|
|
74
|
+
max_steps: Optional[int] = None,
|
|
75
|
+
**kwargs
|
|
76
|
+
):
|
|
77
|
+
# Intercept event hooks to capture Loopers response headers
|
|
78
|
+
event_hooks = kwargs.pop("event_hooks", {})
|
|
79
|
+
if "response" not in event_hooks:
|
|
80
|
+
event_hooks["response"] = []
|
|
81
|
+
event_hooks["response"].append(_response_hook)
|
|
82
|
+
|
|
83
|
+
if "http_client" not in kwargs:
|
|
84
|
+
kwargs["http_client"] = httpx.Client(event_hooks=event_hooks)
|
|
85
|
+
|
|
86
|
+
base_url = f"{loopers_url.rstrip('/')}/openai/v1"
|
|
87
|
+
|
|
88
|
+
default_headers = kwargs.pop("default_headers", {})
|
|
89
|
+
default_headers["Authorization"] = f"Bearer {loopers_key}"
|
|
90
|
+
if provider_key:
|
|
91
|
+
default_headers["X-Loopers-Provider-Key"] = provider_key
|
|
92
|
+
if session_id:
|
|
93
|
+
default_headers["X-Loopers-Session-ID"] = session_id
|
|
94
|
+
if session_budget is not None:
|
|
95
|
+
default_headers["X-Loopers-Session-Budget"] = str(session_budget)
|
|
96
|
+
if max_steps is not None:
|
|
97
|
+
default_headers["X-Loopers-Session-Max-Steps"] = str(max_steps)
|
|
98
|
+
|
|
99
|
+
super().__init__(
|
|
100
|
+
base_url=base_url,
|
|
101
|
+
api_key=loopers_key,
|
|
102
|
+
default_headers=default_headers,
|
|
103
|
+
**kwargs
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def request(self, *args, **kwargs):
|
|
107
|
+
_set_last_headers({})
|
|
108
|
+
res = super().request(*args, **kwargs)
|
|
109
|
+
_attach_loopers_attributes(res)
|
|
110
|
+
return res
|
|
111
|
+
|
|
112
|
+
class LoopersAsyncOpenAI(openai.AsyncOpenAI):
|
|
113
|
+
"""
|
|
114
|
+
A subclass of openai.AsyncOpenAI that automatically routes calls through
|
|
115
|
+
the Loopers budget/rate-limit proxy and parses response metrics.
|
|
116
|
+
"""
|
|
117
|
+
def __init__(
|
|
118
|
+
self,
|
|
119
|
+
loopers_url: str,
|
|
120
|
+
loopers_key: str,
|
|
121
|
+
provider_key: Optional[str] = None,
|
|
122
|
+
session_id: Optional[str] = None,
|
|
123
|
+
session_budget: Optional[float] = None,
|
|
124
|
+
max_steps: Optional[int] = None,
|
|
125
|
+
**kwargs
|
|
126
|
+
):
|
|
127
|
+
event_hooks = kwargs.pop("event_hooks", {})
|
|
128
|
+
if "response" not in event_hooks:
|
|
129
|
+
event_hooks["response"] = []
|
|
130
|
+
event_hooks["response"].append(_async_response_hook)
|
|
131
|
+
|
|
132
|
+
if "http_client" not in kwargs:
|
|
133
|
+
kwargs["http_client"] = httpx.AsyncClient(event_hooks=event_hooks)
|
|
134
|
+
|
|
135
|
+
base_url = f"{loopers_url.rstrip('/')}/openai/v1"
|
|
136
|
+
|
|
137
|
+
default_headers = kwargs.pop("default_headers", {})
|
|
138
|
+
default_headers["Authorization"] = f"Bearer {loopers_key}"
|
|
139
|
+
if provider_key:
|
|
140
|
+
default_headers["X-Loopers-Provider-Key"] = provider_key
|
|
141
|
+
if session_id:
|
|
142
|
+
default_headers["X-Loopers-Session-ID"] = session_id
|
|
143
|
+
if session_budget is not None:
|
|
144
|
+
default_headers["X-Loopers-Session-Budget"] = str(session_budget)
|
|
145
|
+
if max_steps is not None:
|
|
146
|
+
default_headers["X-Loopers-Session-Max-Steps"] = str(max_steps)
|
|
147
|
+
|
|
148
|
+
super().__init__(
|
|
149
|
+
base_url=base_url,
|
|
150
|
+
api_key=loopers_key,
|
|
151
|
+
default_headers=default_headers,
|
|
152
|
+
**kwargs
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
async def request(self, *args, **kwargs):
|
|
156
|
+
_set_last_headers({})
|
|
157
|
+
res = await super().request(*args, **kwargs)
|
|
158
|
+
_attach_loopers_attributes(res)
|
|
159
|
+
return res
|
|
160
|
+
else:
|
|
161
|
+
class LoopersOpenAI:
|
|
162
|
+
def __init__(self, *args, **kwargs):
|
|
163
|
+
raise ImportError(
|
|
164
|
+
"The 'openai' package is required to use LoopersOpenAI. "
|
|
165
|
+
"Install it via 'pip install openai'."
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
class LoopersAsyncOpenAI:
|
|
169
|
+
def __init__(self, *args, **kwargs):
|
|
170
|
+
raise ImportError(
|
|
171
|
+
"The 'openai' package is required to use LoopersAsyncOpenAI. "
|
|
172
|
+
"Install it via 'pip install openai'."
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# Try importing anthropic
|
|
177
|
+
try:
|
|
178
|
+
import anthropic
|
|
179
|
+
HAS_ANTHROPIC = True
|
|
180
|
+
except ImportError:
|
|
181
|
+
HAS_ANTHROPIC = False
|
|
182
|
+
|
|
183
|
+
if HAS_ANTHROPIC:
|
|
184
|
+
class LoopersAnthropic(anthropic.Anthropic):
|
|
185
|
+
"""
|
|
186
|
+
A subclass of anthropic.Anthropic that automatically routes calls through
|
|
187
|
+
the Loopers budget/rate-limit proxy and parses response metrics.
|
|
188
|
+
"""
|
|
189
|
+
def __init__(
|
|
190
|
+
self,
|
|
191
|
+
loopers_url: str,
|
|
192
|
+
loopers_key: str,
|
|
193
|
+
provider_key: Optional[str] = None,
|
|
194
|
+
session_id: Optional[str] = None,
|
|
195
|
+
session_budget: Optional[float] = None,
|
|
196
|
+
max_steps: Optional[int] = None,
|
|
197
|
+
**kwargs
|
|
198
|
+
):
|
|
199
|
+
event_hooks = kwargs.pop("event_hooks", {})
|
|
200
|
+
if "response" not in event_hooks:
|
|
201
|
+
event_hooks["response"] = []
|
|
202
|
+
event_hooks["response"].append(_response_hook)
|
|
203
|
+
|
|
204
|
+
if "http_client" not in kwargs:
|
|
205
|
+
kwargs["http_client"] = httpx.Client(event_hooks=event_hooks)
|
|
206
|
+
|
|
207
|
+
base_url = f"{loopers_url.rstrip('/')}/anthropic"
|
|
208
|
+
|
|
209
|
+
default_headers = kwargs.pop("default_headers", {})
|
|
210
|
+
default_headers["Authorization"] = f"Bearer {loopers_key}"
|
|
211
|
+
if provider_key:
|
|
212
|
+
default_headers["X-Loopers-Provider-Key"] = provider_key
|
|
213
|
+
if session_id:
|
|
214
|
+
default_headers["X-Loopers-Session-ID"] = session_id
|
|
215
|
+
if session_budget is not None:
|
|
216
|
+
default_headers["X-Loopers-Session-Budget"] = str(session_budget)
|
|
217
|
+
if max_steps is not None:
|
|
218
|
+
default_headers["X-Loopers-Session-Max-Steps"] = str(max_steps)
|
|
219
|
+
|
|
220
|
+
super().__init__(
|
|
221
|
+
base_url=base_url,
|
|
222
|
+
auth_token=loopers_key,
|
|
223
|
+
default_headers=default_headers,
|
|
224
|
+
**kwargs
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
def request(self, *args, **kwargs):
|
|
228
|
+
_set_last_headers({})
|
|
229
|
+
res = super().request(*args, **kwargs)
|
|
230
|
+
_attach_loopers_attributes(res)
|
|
231
|
+
return res
|
|
232
|
+
|
|
233
|
+
class LoopersAsyncAnthropic(anthropic.AsyncAnthropic):
|
|
234
|
+
"""
|
|
235
|
+
A subclass of anthropic.AsyncAnthropic that automatically routes calls through
|
|
236
|
+
the Loopers budget/rate-limit proxy and parses response metrics.
|
|
237
|
+
"""
|
|
238
|
+
def __init__(
|
|
239
|
+
self,
|
|
240
|
+
loopers_url: str,
|
|
241
|
+
loopers_key: str,
|
|
242
|
+
provider_key: Optional[str] = None,
|
|
243
|
+
session_id: Optional[str] = None,
|
|
244
|
+
session_budget: Optional[float] = None,
|
|
245
|
+
max_steps: Optional[int] = None,
|
|
246
|
+
**kwargs
|
|
247
|
+
):
|
|
248
|
+
event_hooks = kwargs.pop("event_hooks", {})
|
|
249
|
+
if "response" not in event_hooks:
|
|
250
|
+
event_hooks["response"] = []
|
|
251
|
+
event_hooks["response"].append(_async_response_hook)
|
|
252
|
+
|
|
253
|
+
if "http_client" not in kwargs:
|
|
254
|
+
kwargs["http_client"] = httpx.AsyncClient(event_hooks=event_hooks)
|
|
255
|
+
|
|
256
|
+
base_url = f"{loopers_url.rstrip('/')}/anthropic"
|
|
257
|
+
|
|
258
|
+
default_headers = kwargs.pop("default_headers", {})
|
|
259
|
+
default_headers["Authorization"] = f"Bearer {loopers_key}"
|
|
260
|
+
if provider_key:
|
|
261
|
+
default_headers["X-Loopers-Provider-Key"] = provider_key
|
|
262
|
+
if session_id:
|
|
263
|
+
default_headers["X-Loopers-Session-ID"] = session_id
|
|
264
|
+
if session_budget is not None:
|
|
265
|
+
default_headers["X-Loopers-Session-Budget"] = str(session_budget)
|
|
266
|
+
if max_steps is not None:
|
|
267
|
+
default_headers["X-Loopers-Session-Max-Steps"] = str(max_steps)
|
|
268
|
+
|
|
269
|
+
super().__init__(
|
|
270
|
+
base_url=base_url,
|
|
271
|
+
auth_token=loopers_key,
|
|
272
|
+
default_headers=default_headers,
|
|
273
|
+
**kwargs
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
async def request(self, *args, **kwargs):
|
|
277
|
+
_set_last_headers({})
|
|
278
|
+
res = await super().request(*args, **kwargs)
|
|
279
|
+
_attach_loopers_attributes(res)
|
|
280
|
+
return res
|
|
281
|
+
else:
|
|
282
|
+
class LoopersAnthropic:
|
|
283
|
+
def __init__(self, *args, **kwargs):
|
|
284
|
+
raise ImportError(
|
|
285
|
+
"The 'anthropic' package is required to use LoopersAnthropic. "
|
|
286
|
+
"Install it via 'pip install anthropic'."
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
class LoopersAsyncAnthropic:
|
|
290
|
+
def __init__(self, *args, **kwargs):
|
|
291
|
+
raise ImportError(
|
|
292
|
+
"The 'anthropic' package is required to use LoopersAsyncAnthropic. "
|
|
293
|
+
"Install it via 'pip install anthropic'."
|
|
294
|
+
)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: loopers-client
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: A premium Python client wrapper for the Loopers AI budget & rate-limit proxy.
|
|
5
|
+
License: MIT
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.7
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: httpx>=0.20.0
|
|
12
|
+
Provides-Extra: openai
|
|
13
|
+
Requires-Dist: openai>=1.0.0; extra == "openai"
|
|
14
|
+
Provides-Extra: anthropic
|
|
15
|
+
Requires-Dist: anthropic>=0.3.0; extra == "anthropic"
|
|
16
|
+
|
|
17
|
+
# Loopers Python Client SDK (`loopers-client`)
|
|
18
|
+
|
|
19
|
+
The `loopers-client` package provides a drop-in wrapper around official OpenAI and Anthropic SDK clients to make integration with the Loopers cost firewall seamless.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install loopers-client
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Additionally, install the official provider package you plan to use:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install openai
|
|
31
|
+
# or
|
|
32
|
+
pip install anthropic
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- **Automatic Headers Injection**: Automatically handles injection of Loopers proxy keys (`Authorization`), upstream provider keys (`X-Loopers-Provider-Key`), and session budget limits.
|
|
38
|
+
- **Custom Attributes**: Intercepts response headers and attaches cost metadata directly to the returned objects.
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
### OpenAI Integration
|
|
43
|
+
|
|
44
|
+
Replace `openai.OpenAI` with `LoopersOpenAI`:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from loopers_client import LoopersOpenAI
|
|
48
|
+
|
|
49
|
+
# Initialize client
|
|
50
|
+
client = LoopersOpenAI(
|
|
51
|
+
loopers_url="http://localhost:8080",
|
|
52
|
+
loopers_key="lp-xxx", # Loopers proxy key
|
|
53
|
+
provider_key="sk-proj-xxx", # Upstream OpenAI key
|
|
54
|
+
session_id="agent-run-123", # Optional: track steps and budget for an agent session
|
|
55
|
+
session_budget=2.50, # Optional: limit session to $2.50
|
|
56
|
+
max_steps=20 # Optional: limit session to 20 steps
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Call completions exactly like the official client
|
|
60
|
+
response = client.chat.completions.create(
|
|
61
|
+
model="gpt-4o",
|
|
62
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Inspect budget/cost metadata attached to response
|
|
66
|
+
print(f"Request Cost: ${response.loopers_cost} USD")
|
|
67
|
+
print(f"Estimated Cost: ${response.loopers_cost_estimated} USD")
|
|
68
|
+
print(f"Session Spend: ${response.loopers_session_spend} USD")
|
|
69
|
+
print(f"Session Steps: {response.loopers_session_steps}")
|
|
70
|
+
print(f"Session Remaining: ${response.loopers_session_remaining} USD")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Async OpenAI
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import asyncio
|
|
77
|
+
from loopers_client import LoopersAsyncOpenAI
|
|
78
|
+
|
|
79
|
+
async def main():
|
|
80
|
+
client = LoopersAsyncOpenAI(
|
|
81
|
+
loopers_url="http://localhost:8080",
|
|
82
|
+
loopers_key="lp-xxx",
|
|
83
|
+
provider_key="sk-proj-xxx"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
response = await client.chat.completions.create(
|
|
87
|
+
model="gpt-4o",
|
|
88
|
+
messages=[{"role": "user", "content": "Hello!"}]
|
|
89
|
+
)
|
|
90
|
+
print(response.loopers_cost)
|
|
91
|
+
|
|
92
|
+
asyncio.run(main())
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Anthropic Integration
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from loopers_client import LoopersAnthropic
|
|
99
|
+
|
|
100
|
+
client = LoopersAnthropic(
|
|
101
|
+
loopers_url="http://localhost:8080",
|
|
102
|
+
loopers_key="lp-xxx",
|
|
103
|
+
provider_key="sk-ant-xxx"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
response = client.messages.create(
|
|
107
|
+
model="claude-3-5-sonnet-latest",
|
|
108
|
+
max_tokens=1000,
|
|
109
|
+
messages=[{"role": "user", "content": "Hello!"}]
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
print(response.loopers_cost)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
loopers_client/__init__.py
|
|
4
|
+
loopers_client/client.py
|
|
5
|
+
loopers_client.egg-info/PKG-INFO
|
|
6
|
+
loopers_client.egg-info/SOURCES.txt
|
|
7
|
+
loopers_client.egg-info/dependency_links.txt
|
|
8
|
+
loopers_client.egg-info/requires.txt
|
|
9
|
+
loopers_client.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
loopers_client
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "loopers-client"
|
|
7
|
+
version = "0.4.0"
|
|
8
|
+
description = "A premium Python client wrapper for the Loopers AI budget & rate-limit proxy."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.7"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
]
|
|
17
|
+
dependencies = [
|
|
18
|
+
"httpx>=0.20.0",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
openai = ["openai>=1.0.0"]
|
|
23
|
+
anthropic = ["anthropic>=0.3.0"]
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
where = ["."]
|
|
27
|
+
include = ["loopers_client*"]
|