useoutlet 0.1.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.
- useoutlet-0.1.0/.gitignore +13 -0
- useoutlet-0.1.0/LICENSE +21 -0
- useoutlet-0.1.0/PKG-INFO +238 -0
- useoutlet-0.1.0/README.md +211 -0
- useoutlet-0.1.0/pyproject.toml +75 -0
- useoutlet-0.1.0/src/useoutlet/__init__.py +79 -0
- useoutlet-0.1.0/src/useoutlet/_version.py +1 -0
- useoutlet-0.1.0/src/useoutlet/client.py +304 -0
- useoutlet-0.1.0/src/useoutlet/direct.py +164 -0
- useoutlet-0.1.0/src/useoutlet/ended.py +59 -0
- useoutlet-0.1.0/src/useoutlet/grants.py +107 -0
- useoutlet-0.1.0/src/useoutlet/http.py +96 -0
- useoutlet-0.1.0/src/useoutlet/pkce.py +117 -0
- useoutlet-0.1.0/src/useoutlet/providers.json +548 -0
- useoutlet-0.1.0/src/useoutlet/providers.py +130 -0
- useoutlet-0.1.0/src/useoutlet/py.typed +0 -0
- useoutlet-0.1.0/src/useoutlet/types.py +162 -0
- useoutlet-0.1.0/tests/conftest.py +122 -0
- useoutlet-0.1.0/tests/support.py +47 -0
- useoutlet-0.1.0/tests/test_client.py +153 -0
- useoutlet-0.1.0/tests/test_direct.py +110 -0
- useoutlet-0.1.0/tests/test_ended.py +190 -0
- useoutlet-0.1.0/tests/test_pkce.py +211 -0
- useoutlet-0.1.0/tests/test_providers.py +39 -0
- useoutlet-0.1.0/tests/test_refresh.py +88 -0
- useoutlet-0.1.0/tests/test_registry.py +155 -0
- useoutlet-0.1.0/tests/test_status.py +30 -0
- useoutlet-0.1.0/tests/test_support_data.py +36 -0
- useoutlet-0.1.0/tests/test_vault.py +71 -0
useoutlet-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Outlet
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
useoutlet-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: useoutlet
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Connect your users' AI accounts to your app. Capped, revocable App keys, never raw credentials. Outlet is never in the data path.
|
|
5
|
+
Project-URL: Homepage, https://useoutlet.dev
|
|
6
|
+
Project-URL: Documentation, https://useoutlet.dev/docs/
|
|
7
|
+
Project-URL: Source, https://github.com/PARZ1V3L/outlet/tree/main/sdk-python
|
|
8
|
+
Project-URL: Issues, https://github.com/PARZ1V3L/outlet/issues
|
|
9
|
+
Author-email: Parz <hello@useoutlet.dev>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ai,anthropic,api-key-management,api-keys,bring-your-own-key,byok,connect-your-ai,fal,gemini,google,llm,mcp,oauth,openai,openrouter,outlet,python
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
<p align="center">
|
|
29
|
+
<a href="https://useoutlet.dev"><img src="https://useoutlet.dev/og.png" alt="outlet: use the AI power you already pay for" width="640"></a>
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
<p align="center">
|
|
33
|
+
<a href="https://pypi.org/project/useoutlet/"><img src="https://img.shields.io/pypi/v/useoutlet?labelColor=18181A&color=D6F436" alt="PyPI version"></a>
|
|
34
|
+
<img src="https://img.shields.io/badge/dependencies-0-D6F436?labelColor=18181A" alt="zero dependencies">
|
|
35
|
+
<img src="https://img.shields.io/badge/types-included-D6F436?labelColor=18181A" alt="type hints included">
|
|
36
|
+
<img src="https://img.shields.io/badge/license-MIT-D6F436?labelColor=18181A" alt="MIT license">
|
|
37
|
+
</p>
|
|
38
|
+
|
|
39
|
+
# useoutlet
|
|
40
|
+
|
|
41
|
+
**Let your users plug in the AI they already pay for.**
|
|
42
|
+
|
|
43
|
+
Outlet connects a user's existing AI account (Anthropic, OpenAI, Google, or
|
|
44
|
+
any OpenAI-compatible provider) to your app through **capped, revocable App
|
|
45
|
+
keys, one per app.** Never their raw credentials. Your app calls the provider
|
|
46
|
+
directly with the official SDK. Outlet is **never in the data path**.
|
|
47
|
+
|
|
48
|
+
> Status: **direct mode works today** (validated bring-your-own-key, no
|
|
49
|
+
> server). Vault mode (capped, revocable App keys) is open.
|
|
50
|
+
> Register your app at useoutlet.dev/register.
|
|
51
|
+
> The same session in both modes. Protocol docs at [useoutlet.dev](https://useoutlet.dev).
|
|
52
|
+
> Feedback welcome.
|
|
53
|
+
|
|
54
|
+
This is the Python SDK, the server side of Outlet. It starts a Vault
|
|
55
|
+
connection, takes the session back, and refreshes, checks and revokes it.
|
|
56
|
+
Standard library only. Python 3.10 and up. The Connect your AI button, the
|
|
57
|
+
CLI and the MCP docs server live in the npm package,
|
|
58
|
+
[@useoutlet/sdk](https://www.npmjs.com/package/@useoutlet/sdk).
|
|
59
|
+
|
|
60
|
+
## Why
|
|
61
|
+
|
|
62
|
+
- Users already pay for AI. They shouldn't pay again inside every app.
|
|
63
|
+
- Developers shouldn't store customer API keys (breach target, compliance).
|
|
64
|
+
- Raw keys are all-or-nothing. Outlet keys are per-app, capped, revocable.
|
|
65
|
+
|
|
66
|
+
## Install
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
pip install useoutlet
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Quickstart
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
pip install useoutlet
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Start a Vault connection from your server. Vault needs a registered app ID
|
|
79
|
+
and app secret. [Register your app](https://useoutlet.dev/register) to get
|
|
80
|
+
them. The app secret stays on your server.
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
import os
|
|
84
|
+
|
|
85
|
+
from useoutlet import Outlet
|
|
86
|
+
|
|
87
|
+
outlet = Outlet(app_id="app_yourapp", app_secret=os.environ["OUTLET_APP_SECRET"])
|
|
88
|
+
|
|
89
|
+
# 1. Start a Vault connection request. Send the user to its grant URL.
|
|
90
|
+
request = outlet.connect(providers=["openai"], requested_cap_usd=10)
|
|
91
|
+
print("Approve the Vault connection at", request.grant_url)
|
|
92
|
+
|
|
93
|
+
# 2. Wait for the approval. The session holds the Vault App key.
|
|
94
|
+
session = outlet.wait(request.id)
|
|
95
|
+
print("Connected:", session.grant_id)
|
|
96
|
+
|
|
97
|
+
# 3. Call the provider like you already do, with its official SDK.
|
|
98
|
+
# ai = OpenAI(api_key=session.keys["openai"])
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Save it as quickstart.py and run it.
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
python quickstart.py
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`session.keys["openai"]` holds the Vault App key. Store `session.grant_id`,
|
|
108
|
+
not the key. Then call the provider like you already do, with its official
|
|
109
|
+
SDK.
|
|
110
|
+
|
|
111
|
+
### When a connection ends
|
|
112
|
+
|
|
113
|
+
A connection can end while your app is running: the user's Vault connection
|
|
114
|
+
is paused at its cap, revoked or disconnected, or its refresh token is gone.
|
|
115
|
+
The SDK turns each into one typed error, `ConnectionEndedError`, with
|
|
116
|
+
`reason` (`capped`, `revoked` or `expired`) and `grant_id`. `status()`,
|
|
117
|
+
`refresh()` and `wait()` raise it.
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from useoutlet import ConnectionEndedError
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
info = outlet.status(session.grant_id)
|
|
124
|
+
except ConnectionEndedError as e:
|
|
125
|
+
print(e.reason) # "capped", "revoked" or "expired"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
For a capped connection, the user raises the cap on useoutlet.dev and
|
|
129
|
+
`refresh()` returns the key. For a revoked or expired one, ask the user to
|
|
130
|
+
connect again.
|
|
131
|
+
|
|
132
|
+
## Usage today (direct mode)
|
|
133
|
+
|
|
134
|
+
Your user pastes their own API key; the SDK validates it locally (catches
|
|
135
|
+
provider mix-ups, **refuses admin keys**) and hands back a session. No
|
|
136
|
+
network, nothing sent to Outlet. A local format check.
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from useoutlet import direct
|
|
140
|
+
|
|
141
|
+
session = direct({"openai": user_pasted_key})
|
|
142
|
+
|
|
143
|
+
from openai import OpenAI
|
|
144
|
+
|
|
145
|
+
ai = OpenAI(api_key=session.keys["openai"])
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Works with **any OpenAI-compatible provider**. Pass the key under that
|
|
149
|
+
provider and point the OpenAI SDK at its base URL:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
session = direct({"groq": user_pasted_key})
|
|
153
|
+
|
|
154
|
+
ai = OpenAI(api_key=session.keys["groq"], base_url="https://api.groq.com/openai/v1")
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
OpenAI, Anthropic, and Google get strict key-format checks (mix-ups caught,
|
|
158
|
+
admin keys refused); other providers are accepted with the same admin-key
|
|
159
|
+
safety check, since their key formats vary.
|
|
160
|
+
|
|
161
|
+
## Vault mode (the same session)
|
|
162
|
+
|
|
163
|
+
Same session shape. The pasted key becomes an App key: provisioned inside the
|
|
164
|
+
user's own account for your app alone, capped, and revocable.
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
# later: re-fetch keys (session.grant_id is the thing you persist)
|
|
168
|
+
fresh = outlet.refresh(session.grant_id)
|
|
169
|
+
|
|
170
|
+
# check spend / status
|
|
171
|
+
info = outlet.status(session.grant_id)
|
|
172
|
+
print(f"{info.spend_usd} of {info.cap_usd} used")
|
|
173
|
+
|
|
174
|
+
# the app's side of revoke
|
|
175
|
+
outlet.revoke(session.grant_id)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### No app secret? Public clients use PKCE
|
|
179
|
+
|
|
180
|
+
An app without an app secret registers as a **public client** instead: no
|
|
181
|
+
secret, just a registered return address. Grant creation is secured by PKCE
|
|
182
|
+
(protocol docs at [useoutlet.dev](https://useoutlet.dev)), so the flow spans
|
|
183
|
+
the return address in two calls:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
outlet = Outlet(app_id="app_yourapp") # no secret: a public client
|
|
187
|
+
|
|
188
|
+
# on your [ Connect your AI ] route: generates PKCE, send the user to the grant URL
|
|
189
|
+
request = outlet.connect(providers=["anthropic"], redirect_uri="https://yourapp.com/outlet/return")
|
|
190
|
+
|
|
191
|
+
# on the route at the return address: verifies state, exchanges the code
|
|
192
|
+
session = outlet.exchange(code, state)
|
|
193
|
+
ai = Anthropic(api_key=session.keys["anthropic"])
|
|
194
|
+
|
|
195
|
+
# session.refresh_token (not an app secret) authorizes later calls. The client holds it.
|
|
196
|
+
fresh = outlet.refresh(session.grant_id)
|
|
197
|
+
# fresh.refresh_token replaces the one you sent; the old one now gets 401. Store the new one.
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The provider key arrives only on the back-channel exchange, never in the
|
|
201
|
+
redirect URL. The `refresh_token` belongs to this one grant.
|
|
202
|
+
|
|
203
|
+
Outlet registers `http://localhost/outlet/return` for every app. It matches
|
|
204
|
+
any port.
|
|
205
|
+
|
|
206
|
+
The client holds each grant's refresh token in memory. From another process,
|
|
207
|
+
pass `refresh_token=` to `refresh()`, `status()` or `revoke()` from your own
|
|
208
|
+
store, and pass the `GrantRequest` you kept to `exchange()`. `AsyncOutlet`
|
|
209
|
+
has the same methods for asyncio apps.
|
|
210
|
+
|
|
211
|
+
Example: [examples/python/](https://github.com/PARZ1V3L/outlet/tree/main/examples/python),
|
|
212
|
+
a FastAPI app with the connect route and the return address.
|
|
213
|
+
|
|
214
|
+
## Several providers
|
|
215
|
+
|
|
216
|
+
Create a separate connection for each provider your app uses. A text
|
|
217
|
+
connection can sit alongside connections for video and voice. If the same
|
|
218
|
+
provider serves several models, those models can use that provider’s
|
|
219
|
+
connection.
|
|
220
|
+
|
|
221
|
+
## What your app never sees (vault mode)
|
|
222
|
+
|
|
223
|
+
- The user's root API key or account credentials
|
|
224
|
+
- Other apps' keys or spend
|
|
225
|
+
- Anything after revocation. A revoked grant stops working
|
|
226
|
+
|
|
227
|
+
The user's own key, in your app, by their choice. Validated, never an admin
|
|
228
|
+
credential. The same API you keep when you upgrade to vault mode.
|
|
229
|
+
|
|
230
|
+
## What Outlet never does
|
|
231
|
+
|
|
232
|
+
No proxying. No token markup. No model routing. No prompt storage. Free for
|
|
233
|
+
end users, forever. Direct mode is free. The vault is the paid product for
|
|
234
|
+
developers. MIT licensed.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
Docs: [useoutlet.dev](https://useoutlet.dev) · [Source](https://github.com/PARZ1V3L/outlet) · Contact: hello@useoutlet.dev
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://useoutlet.dev"><img src="https://useoutlet.dev/og.png" alt="outlet: use the AI power you already pay for" width="640"></a>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://pypi.org/project/useoutlet/"><img src="https://img.shields.io/pypi/v/useoutlet?labelColor=18181A&color=D6F436" alt="PyPI version"></a>
|
|
7
|
+
<img src="https://img.shields.io/badge/dependencies-0-D6F436?labelColor=18181A" alt="zero dependencies">
|
|
8
|
+
<img src="https://img.shields.io/badge/types-included-D6F436?labelColor=18181A" alt="type hints included">
|
|
9
|
+
<img src="https://img.shields.io/badge/license-MIT-D6F436?labelColor=18181A" alt="MIT license">
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
# useoutlet
|
|
13
|
+
|
|
14
|
+
**Let your users plug in the AI they already pay for.**
|
|
15
|
+
|
|
16
|
+
Outlet connects a user's existing AI account (Anthropic, OpenAI, Google, or
|
|
17
|
+
any OpenAI-compatible provider) to your app through **capped, revocable App
|
|
18
|
+
keys, one per app.** Never their raw credentials. Your app calls the provider
|
|
19
|
+
directly with the official SDK. Outlet is **never in the data path**.
|
|
20
|
+
|
|
21
|
+
> Status: **direct mode works today** (validated bring-your-own-key, no
|
|
22
|
+
> server). Vault mode (capped, revocable App keys) is open.
|
|
23
|
+
> Register your app at useoutlet.dev/register.
|
|
24
|
+
> The same session in both modes. Protocol docs at [useoutlet.dev](https://useoutlet.dev).
|
|
25
|
+
> Feedback welcome.
|
|
26
|
+
|
|
27
|
+
This is the Python SDK, the server side of Outlet. It starts a Vault
|
|
28
|
+
connection, takes the session back, and refreshes, checks and revokes it.
|
|
29
|
+
Standard library only. Python 3.10 and up. The Connect your AI button, the
|
|
30
|
+
CLI and the MCP docs server live in the npm package,
|
|
31
|
+
[@useoutlet/sdk](https://www.npmjs.com/package/@useoutlet/sdk).
|
|
32
|
+
|
|
33
|
+
## Why
|
|
34
|
+
|
|
35
|
+
- Users already pay for AI. They shouldn't pay again inside every app.
|
|
36
|
+
- Developers shouldn't store customer API keys (breach target, compliance).
|
|
37
|
+
- Raw keys are all-or-nothing. Outlet keys are per-app, capped, revocable.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
pip install useoutlet
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quickstart
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
pip install useoutlet
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Start a Vault connection from your server. Vault needs a registered app ID
|
|
52
|
+
and app secret. [Register your app](https://useoutlet.dev/register) to get
|
|
53
|
+
them. The app secret stays on your server.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import os
|
|
57
|
+
|
|
58
|
+
from useoutlet import Outlet
|
|
59
|
+
|
|
60
|
+
outlet = Outlet(app_id="app_yourapp", app_secret=os.environ["OUTLET_APP_SECRET"])
|
|
61
|
+
|
|
62
|
+
# 1. Start a Vault connection request. Send the user to its grant URL.
|
|
63
|
+
request = outlet.connect(providers=["openai"], requested_cap_usd=10)
|
|
64
|
+
print("Approve the Vault connection at", request.grant_url)
|
|
65
|
+
|
|
66
|
+
# 2. Wait for the approval. The session holds the Vault App key.
|
|
67
|
+
session = outlet.wait(request.id)
|
|
68
|
+
print("Connected:", session.grant_id)
|
|
69
|
+
|
|
70
|
+
# 3. Call the provider like you already do, with its official SDK.
|
|
71
|
+
# ai = OpenAI(api_key=session.keys["openai"])
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Save it as quickstart.py and run it.
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
python quickstart.py
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`session.keys["openai"]` holds the Vault App key. Store `session.grant_id`,
|
|
81
|
+
not the key. Then call the provider like you already do, with its official
|
|
82
|
+
SDK.
|
|
83
|
+
|
|
84
|
+
### When a connection ends
|
|
85
|
+
|
|
86
|
+
A connection can end while your app is running: the user's Vault connection
|
|
87
|
+
is paused at its cap, revoked or disconnected, or its refresh token is gone.
|
|
88
|
+
The SDK turns each into one typed error, `ConnectionEndedError`, with
|
|
89
|
+
`reason` (`capped`, `revoked` or `expired`) and `grant_id`. `status()`,
|
|
90
|
+
`refresh()` and `wait()` raise it.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from useoutlet import ConnectionEndedError
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
info = outlet.status(session.grant_id)
|
|
97
|
+
except ConnectionEndedError as e:
|
|
98
|
+
print(e.reason) # "capped", "revoked" or "expired"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
For a capped connection, the user raises the cap on useoutlet.dev and
|
|
102
|
+
`refresh()` returns the key. For a revoked or expired one, ask the user to
|
|
103
|
+
connect again.
|
|
104
|
+
|
|
105
|
+
## Usage today (direct mode)
|
|
106
|
+
|
|
107
|
+
Your user pastes their own API key; the SDK validates it locally (catches
|
|
108
|
+
provider mix-ups, **refuses admin keys**) and hands back a session. No
|
|
109
|
+
network, nothing sent to Outlet. A local format check.
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from useoutlet import direct
|
|
113
|
+
|
|
114
|
+
session = direct({"openai": user_pasted_key})
|
|
115
|
+
|
|
116
|
+
from openai import OpenAI
|
|
117
|
+
|
|
118
|
+
ai = OpenAI(api_key=session.keys["openai"])
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Works with **any OpenAI-compatible provider**. Pass the key under that
|
|
122
|
+
provider and point the OpenAI SDK at its base URL:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
session = direct({"groq": user_pasted_key})
|
|
126
|
+
|
|
127
|
+
ai = OpenAI(api_key=session.keys["groq"], base_url="https://api.groq.com/openai/v1")
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
OpenAI, Anthropic, and Google get strict key-format checks (mix-ups caught,
|
|
131
|
+
admin keys refused); other providers are accepted with the same admin-key
|
|
132
|
+
safety check, since their key formats vary.
|
|
133
|
+
|
|
134
|
+
## Vault mode (the same session)
|
|
135
|
+
|
|
136
|
+
Same session shape. The pasted key becomes an App key: provisioned inside the
|
|
137
|
+
user's own account for your app alone, capped, and revocable.
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
# later: re-fetch keys (session.grant_id is the thing you persist)
|
|
141
|
+
fresh = outlet.refresh(session.grant_id)
|
|
142
|
+
|
|
143
|
+
# check spend / status
|
|
144
|
+
info = outlet.status(session.grant_id)
|
|
145
|
+
print(f"{info.spend_usd} of {info.cap_usd} used")
|
|
146
|
+
|
|
147
|
+
# the app's side of revoke
|
|
148
|
+
outlet.revoke(session.grant_id)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### No app secret? Public clients use PKCE
|
|
152
|
+
|
|
153
|
+
An app without an app secret registers as a **public client** instead: no
|
|
154
|
+
secret, just a registered return address. Grant creation is secured by PKCE
|
|
155
|
+
(protocol docs at [useoutlet.dev](https://useoutlet.dev)), so the flow spans
|
|
156
|
+
the return address in two calls:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
outlet = Outlet(app_id="app_yourapp") # no secret: a public client
|
|
160
|
+
|
|
161
|
+
# on your [ Connect your AI ] route: generates PKCE, send the user to the grant URL
|
|
162
|
+
request = outlet.connect(providers=["anthropic"], redirect_uri="https://yourapp.com/outlet/return")
|
|
163
|
+
|
|
164
|
+
# on the route at the return address: verifies state, exchanges the code
|
|
165
|
+
session = outlet.exchange(code, state)
|
|
166
|
+
ai = Anthropic(api_key=session.keys["anthropic"])
|
|
167
|
+
|
|
168
|
+
# session.refresh_token (not an app secret) authorizes later calls. The client holds it.
|
|
169
|
+
fresh = outlet.refresh(session.grant_id)
|
|
170
|
+
# fresh.refresh_token replaces the one you sent; the old one now gets 401. Store the new one.
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The provider key arrives only on the back-channel exchange, never in the
|
|
174
|
+
redirect URL. The `refresh_token` belongs to this one grant.
|
|
175
|
+
|
|
176
|
+
Outlet registers `http://localhost/outlet/return` for every app. It matches
|
|
177
|
+
any port.
|
|
178
|
+
|
|
179
|
+
The client holds each grant's refresh token in memory. From another process,
|
|
180
|
+
pass `refresh_token=` to `refresh()`, `status()` or `revoke()` from your own
|
|
181
|
+
store, and pass the `GrantRequest` you kept to `exchange()`. `AsyncOutlet`
|
|
182
|
+
has the same methods for asyncio apps.
|
|
183
|
+
|
|
184
|
+
Example: [examples/python/](https://github.com/PARZ1V3L/outlet/tree/main/examples/python),
|
|
185
|
+
a FastAPI app with the connect route and the return address.
|
|
186
|
+
|
|
187
|
+
## Several providers
|
|
188
|
+
|
|
189
|
+
Create a separate connection for each provider your app uses. A text
|
|
190
|
+
connection can sit alongside connections for video and voice. If the same
|
|
191
|
+
provider serves several models, those models can use that provider’s
|
|
192
|
+
connection.
|
|
193
|
+
|
|
194
|
+
## What your app never sees (vault mode)
|
|
195
|
+
|
|
196
|
+
- The user's root API key or account credentials
|
|
197
|
+
- Other apps' keys or spend
|
|
198
|
+
- Anything after revocation. A revoked grant stops working
|
|
199
|
+
|
|
200
|
+
The user's own key, in your app, by their choice. Validated, never an admin
|
|
201
|
+
credential. The same API you keep when you upgrade to vault mode.
|
|
202
|
+
|
|
203
|
+
## What Outlet never does
|
|
204
|
+
|
|
205
|
+
No proxying. No token markup. No model routing. No prompt storage. Free for
|
|
206
|
+
end users, forever. Direct mode is free. The vault is the paid product for
|
|
207
|
+
developers. MIT licensed.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
Docs: [useoutlet.dev](https://useoutlet.dev) · [Source](https://github.com/PARZ1V3L/outlet) · Contact: hello@useoutlet.dev
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "useoutlet"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Connect your users' AI accounts to your app. Capped, revocable App keys, never raw credentials. Outlet is never in the data path."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [{ name = "Parz", email = "hello@useoutlet.dev" }]
|
|
14
|
+
keywords = [
|
|
15
|
+
"ai",
|
|
16
|
+
"byok",
|
|
17
|
+
"bring-your-own-key",
|
|
18
|
+
"api-keys",
|
|
19
|
+
"api-key-management",
|
|
20
|
+
"llm",
|
|
21
|
+
"anthropic",
|
|
22
|
+
"openai",
|
|
23
|
+
"google",
|
|
24
|
+
"gemini",
|
|
25
|
+
"oauth",
|
|
26
|
+
"outlet",
|
|
27
|
+
"mcp",
|
|
28
|
+
"openrouter",
|
|
29
|
+
"fal",
|
|
30
|
+
"connect-your-ai",
|
|
31
|
+
"python",
|
|
32
|
+
]
|
|
33
|
+
classifiers = [
|
|
34
|
+
"Development Status :: 3 - Alpha",
|
|
35
|
+
"Intended Audience :: Developers",
|
|
36
|
+
"Operating System :: OS Independent",
|
|
37
|
+
"Programming Language :: Python :: 3",
|
|
38
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
39
|
+
"Programming Language :: Python :: 3.10",
|
|
40
|
+
"Programming Language :: Python :: 3.11",
|
|
41
|
+
"Programming Language :: Python :: 3.12",
|
|
42
|
+
"Programming Language :: Python :: 3.13",
|
|
43
|
+
"Programming Language :: Python :: 3.14",
|
|
44
|
+
"Topic :: Software Development :: Libraries",
|
|
45
|
+
"Typing :: Typed",
|
|
46
|
+
]
|
|
47
|
+
dependencies = []
|
|
48
|
+
|
|
49
|
+
[project.urls]
|
|
50
|
+
Homepage = "https://useoutlet.dev"
|
|
51
|
+
Documentation = "https://useoutlet.dev/docs/"
|
|
52
|
+
Source = "https://github.com/PARZ1V3L/outlet/tree/main/sdk-python"
|
|
53
|
+
Issues = "https://github.com/PARZ1V3L/outlet/issues"
|
|
54
|
+
|
|
55
|
+
[dependency-groups]
|
|
56
|
+
test = ["pytest>=9"]
|
|
57
|
+
|
|
58
|
+
[tool.hatch.version]
|
|
59
|
+
path = "src/useoutlet/_version.py"
|
|
60
|
+
|
|
61
|
+
[tool.hatch.build.targets.wheel]
|
|
62
|
+
packages = ["src/useoutlet"]
|
|
63
|
+
|
|
64
|
+
[tool.hatch.build.targets.sdist]
|
|
65
|
+
exclude = ["/scripts"]
|
|
66
|
+
|
|
67
|
+
[tool.pytest.ini_options]
|
|
68
|
+
testpaths = ["tests"]
|
|
69
|
+
|
|
70
|
+
[tool.ruff]
|
|
71
|
+
line-length = 100
|
|
72
|
+
target-version = "py310"
|
|
73
|
+
|
|
74
|
+
[tool.ruff.lint]
|
|
75
|
+
select = ["E", "F", "W", "I", "UP", "B", "SIM"]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""useoutlet: connect your users' AI accounts to your app.
|
|
2
|
+
|
|
3
|
+
Status: direct mode is live today. Vault mode (connect / refresh / status /
|
|
4
|
+
revoke) is open. Register your app at useoutlet.dev/register.
|
|
5
|
+
|
|
6
|
+
from useoutlet import Outlet
|
|
7
|
+
|
|
8
|
+
outlet = Outlet(app_id="app_yourapp", app_secret=os.environ["OUTLET_APP_SECRET"])
|
|
9
|
+
request = outlet.connect(providers=["openai"], requested_cap_usd=10)
|
|
10
|
+
# send the user to request.grant_url, then
|
|
11
|
+
session = outlet.wait(request.id)
|
|
12
|
+
|
|
13
|
+
# use the official provider SDK: Outlet is not in the data path
|
|
14
|
+
ai = OpenAI(api_key=session.keys["openai"])
|
|
15
|
+
|
|
16
|
+
A connection that ends (capped, revoked or expired) is a ConnectionEndedError
|
|
17
|
+
from status(), refresh() and wait(). The Connect your AI button, the CLI and
|
|
18
|
+
the MCP docs server live in the npm package, @useoutlet/sdk.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from ._version import __version__
|
|
22
|
+
from .client import AsyncOutlet, Outlet
|
|
23
|
+
from .direct import direct
|
|
24
|
+
from .grants import refresh, revoke, status
|
|
25
|
+
from .pkce import PkceChallenge, create_grant, exchange_code, pkce_challenge
|
|
26
|
+
from .providers import (
|
|
27
|
+
KeyShape,
|
|
28
|
+
ProviderCheck,
|
|
29
|
+
ProviderEntry,
|
|
30
|
+
ProviderKind,
|
|
31
|
+
ProviderMode,
|
|
32
|
+
ProviderModes,
|
|
33
|
+
get_provider,
|
|
34
|
+
provider_ids,
|
|
35
|
+
providers,
|
|
36
|
+
)
|
|
37
|
+
from .types import (
|
|
38
|
+
CapReason,
|
|
39
|
+
ConnectionEndedError,
|
|
40
|
+
EndReason,
|
|
41
|
+
GrantInfo,
|
|
42
|
+
GrantRequest,
|
|
43
|
+
GrantStatus,
|
|
44
|
+
Mode,
|
|
45
|
+
OutletError,
|
|
46
|
+
OutletSession,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"AsyncOutlet",
|
|
51
|
+
"CapReason",
|
|
52
|
+
"ConnectionEndedError",
|
|
53
|
+
"EndReason",
|
|
54
|
+
"GrantInfo",
|
|
55
|
+
"GrantRequest",
|
|
56
|
+
"GrantStatus",
|
|
57
|
+
"KeyShape",
|
|
58
|
+
"Mode",
|
|
59
|
+
"Outlet",
|
|
60
|
+
"OutletError",
|
|
61
|
+
"OutletSession",
|
|
62
|
+
"PkceChallenge",
|
|
63
|
+
"ProviderCheck",
|
|
64
|
+
"ProviderEntry",
|
|
65
|
+
"ProviderKind",
|
|
66
|
+
"ProviderMode",
|
|
67
|
+
"ProviderModes",
|
|
68
|
+
"__version__",
|
|
69
|
+
"create_grant",
|
|
70
|
+
"direct",
|
|
71
|
+
"exchange_code",
|
|
72
|
+
"get_provider",
|
|
73
|
+
"pkce_challenge",
|
|
74
|
+
"provider_ids",
|
|
75
|
+
"providers",
|
|
76
|
+
"refresh",
|
|
77
|
+
"revoke",
|
|
78
|
+
"status",
|
|
79
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|