litellm-mysubs 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.
- litellm_mysubs-0.1.0/.gitignore +16 -0
- litellm_mysubs-0.1.0/CHANGELOG.md +48 -0
- litellm_mysubs-0.1.0/LICENSE +21 -0
- litellm_mysubs-0.1.0/PKG-INFO +263 -0
- litellm_mysubs-0.1.0/README.md +229 -0
- litellm_mysubs-0.1.0/docs/DECISIONS.md +528 -0
- litellm_mysubs-0.1.0/docs/OMP.md +89 -0
- litellm_mysubs-0.1.0/docs/menu.png +0 -0
- litellm_mysubs-0.1.0/docs/models.png +0 -0
- litellm_mysubs-0.1.0/docs/mysubs-empty.png +0 -0
- litellm_mysubs-0.1.0/docs/mysubs.png +0 -0
- litellm_mysubs-0.1.0/pyproject.toml +120 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/__init__.py +30 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/bootstrap.py +174 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/callback.py +195 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/catalog/__init__.py +0 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/catalog/deployments.py +153 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/catalog/discovery.py +691 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/catalog/selection.py +117 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/catalog/usage.py +565 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/catalog/usage_probe.py +96 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/credentials/__init__.py +0 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/credentials/callback_server.py +439 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/credentials/env_store.py +68 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/credentials/file_store.py +114 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/credentials/lock.py +160 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/credentials/oauth.py +820 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/credentials/refresher.py +268 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/credentials/secret_store.py +199 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/credentials/store.py +116 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/login_cli.py +235 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/plugin.py +1015 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/py.typed +0 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/registry.py +129 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/setup_cli.py +331 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/transport/__init__.py +0 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/transport/client.py +288 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/transport/hosts.py +89 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/transport/retry.py +100 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/transport/sse.py +50 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/ui/__init__.py +7 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/ui/app.py +1168 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/ui/auth.py +203 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/ui/install.py +144 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/ui/local_flow.py +288 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/ui/menu.py +213 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/ui/pairing.py +171 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/ui/service.py +573 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/ui/throttle.py +197 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/wire/__init__.py +0 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/wire/anthropic.py +762 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/wire/antigravity.py +694 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/wire/antigravity_models.py +191 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/wire/codex.py +785 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/wire/planning_leak.py +201 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/wire/schema.py +1440 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/wire/thinking_loop.py +317 -0
- litellm_mysubs-0.1.0/src/litellm_mysubs/wire/usage.py +141 -0
- litellm_mysubs-0.1.0/tests/__init__.py +0 -0
- litellm_mysubs-0.1.0/tests/conftest.py +47 -0
- litellm_mysubs-0.1.0/tests/fixtures/cca_schema_cases.json +14038 -0
- litellm_mysubs-0.1.0/tests/fixtures/cca_schema_expected.json +1 -0
- litellm_mysubs-0.1.0/tests/test_antigravity_retired.py +312 -0
- litellm_mysubs-0.1.0/tests/test_bootstrap.py +231 -0
- litellm_mysubs-0.1.0/tests/test_callback_cors.py +209 -0
- litellm_mysubs-0.1.0/tests/test_callback_server.py +234 -0
- litellm_mysubs-0.1.0/tests/test_catalog_deployments.py +95 -0
- litellm_mysubs-0.1.0/tests/test_catalog_discovery.py +666 -0
- litellm_mysubs-0.1.0/tests/test_catalog_usage.py +89 -0
- litellm_mysubs-0.1.0/tests/test_credentials.py +130 -0
- litellm_mysubs-0.1.0/tests/test_credentials_oauth.py +780 -0
- litellm_mysubs-0.1.0/tests/test_credentials_secret_store.py +153 -0
- litellm_mysubs-0.1.0/tests/test_deployments_prefix.py +270 -0
- litellm_mysubs-0.1.0/tests/test_litellm_contract.py +73 -0
- litellm_mysubs-0.1.0/tests/test_local_flow.py +311 -0
- litellm_mysubs-0.1.0/tests/test_lock.py +263 -0
- litellm_mysubs-0.1.0/tests/test_omp_drift.py +187 -0
- litellm_mysubs-0.1.0/tests/test_pairing.py +176 -0
- litellm_mysubs-0.1.0/tests/test_planning_leak.py +136 -0
- litellm_mysubs-0.1.0/tests/test_plugin.py +923 -0
- litellm_mysubs-0.1.0/tests/test_refresher.py +302 -0
- litellm_mysubs-0.1.0/tests/test_registry.py +135 -0
- litellm_mysubs-0.1.0/tests/test_schema.py +257 -0
- litellm_mysubs-0.1.0/tests/test_schema_differential.py +54 -0
- litellm_mysubs-0.1.0/tests/test_selection_store.py +157 -0
- litellm_mysubs-0.1.0/tests/test_setup_cli.py +208 -0
- litellm_mysubs-0.1.0/tests/test_thinking_loop.py +265 -0
- litellm_mysubs-0.1.0/tests/test_throttle.py +262 -0
- litellm_mysubs-0.1.0/tests/test_transport.py +148 -0
- litellm_mysubs-0.1.0/tests/test_transport_client.py +395 -0
- litellm_mysubs-0.1.0/tests/test_ui.py +404 -0
- litellm_mysubs-0.1.0/tests/test_ui_interceptor.py +864 -0
- litellm_mysubs-0.1.0/tests/test_ui_menu.py +154 -0
- litellm_mysubs-0.1.0/tests/test_usage.py +140 -0
- litellm_mysubs-0.1.0/tests/test_usage_antigravity.py +332 -0
- litellm_mysubs-0.1.0/tests/test_usage_limits.py +123 -0
- litellm_mysubs-0.1.0/tests/test_usage_probe.py +249 -0
- litellm_mysubs-0.1.0/tests/test_wire_anthropic.py +451 -0
- litellm_mysubs-0.1.0/tests/test_wire_anthropic_edges.py +231 -0
- litellm_mysubs-0.1.0/tests/test_wire_antigravity.py +574 -0
- litellm_mysubs-0.1.0/tests/test_wire_codex.py +541 -0
- litellm_mysubs-0.1.0/tests/test_wire_codex_edges.py +244 -0
- litellm_mysubs-0.1.0/tools/check_omp_drift.py +212 -0
- litellm_mysubs-0.1.0/tools/spike_custom_llm.py +132 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-09-19
|
|
11
|
+
|
|
12
|
+
First release.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- LiteLLM proxy plugin loaded as a single `callbacks` entry in `config.yaml`
|
|
17
|
+
(`litellm_mysubs.proxy_handler_instance`). It does not modify `model_list`,
|
|
18
|
+
`router_settings` or `general_settings`.
|
|
19
|
+
- `mysubs-setup` command: locates the LiteLLM configuration used by the environment,
|
|
20
|
+
adds the callback line, and reports the current state with `--status`.
|
|
21
|
+
- `mysubs-login <provider> --url <proxy> --code <pairing>` command: runs on the user's
|
|
22
|
+
machine, opens the local callback port and completes the OAuth return without exposing
|
|
23
|
+
the proxy to the browser. Pasting the return URL into the page is supported as a
|
|
24
|
+
fallback.
|
|
25
|
+
- MySubs page mounted on the proxy and reachable from the LiteLLM UI under
|
|
26
|
+
**Experimental → MySubs**, gated by LiteLLM administrator authentication.
|
|
27
|
+
- OAuth connection flows for three subscription providers: Anthropic (Claude Max),
|
|
28
|
+
OpenAI (ChatGPT Plus / Codex) and Google (Antigravity).
|
|
29
|
+
- Model discovery: each connected subscription is probed to determine which models it
|
|
30
|
+
actually serves; the selected ones are injected into the LiteLLM Router as deployments
|
|
31
|
+
prefixed with `mysubs/<subscription>/`.
|
|
32
|
+
- Protocol bridges that translate OpenAI-compatible requests into each provider's own
|
|
33
|
+
wire format (Anthropic Messages, OpenAI Responses, Antigravity Cloud Code), including
|
|
34
|
+
streaming, reasoning content, vision input and usage accounting.
|
|
35
|
+
- Background token refresh with cross-process `flock` coordination, safe under a
|
|
36
|
+
multi-worker proxy.
|
|
37
|
+
- Three credential stores: local file (mode `0600`), the LiteLLM Secret Manager, and
|
|
38
|
+
environment variables.
|
|
39
|
+
- Per-subscription usage reporting surfaced on the MySubs page.
|
|
40
|
+
- Request throttling per worker, with retry policy derived from provider responses.
|
|
41
|
+
- `docs/DECISIONS.md`: ten architecture decisions, each recorded with the measurement
|
|
42
|
+
that supports it and the conditions that would reopen it.
|
|
43
|
+
- CI covering Python 3.11-3.13, a LiteLLM version matrix (pinned `1.101.0` and `latest`),
|
|
44
|
+
a contract test against the LiteLLM internal symbols the plugin depends on, and a
|
|
45
|
+
drift check over the source anchors.
|
|
46
|
+
|
|
47
|
+
[Unreleased]: https://github.com/eduardopessin/litellm-mysubs/compare/v0.1.0...HEAD
|
|
48
|
+
[0.1.0]: https://github.com/eduardopessin/litellm-mysubs/releases/tag/v0.1.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eduardo Pessin
|
|
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.
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: litellm-mysubs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Serve Claude Max, ChatGPT Plus/Codex and Google Antigravity subscriptions as OpenAI-compatible models through a LiteLLM proxy
|
|
5
|
+
Project-URL: Homepage, https://github.com/eduardopessin/litellm-mysubs
|
|
6
|
+
Project-URL: Source, https://github.com/eduardopessin/litellm-mysubs
|
|
7
|
+
Project-URL: Issues, https://github.com/eduardopessin/litellm-mysubs/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/eduardopessin/litellm-mysubs/blob/main/CHANGELOG.md
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: antigravity,claude,codex,gemini,litellm,llm,oauth,openai-compatible,proxy,subscription
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Framework :: FastAPI
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Internet :: Proxy Servers
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Requires-Dist: fastapi>=0.110
|
|
24
|
+
Requires-Dist: httpx>=0.27
|
|
25
|
+
Requires-Dist: pyyaml>=6
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: litellm[proxy]>=1.70; extra == 'dev'
|
|
28
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-cov>=5; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# litellm-mysubs
|
|
36
|
+
|
|
37
|
+
Serve your Claude Max, ChatGPT Plus (Codex) and Google Antigravity subscriptions as ordinary
|
|
38
|
+
OpenAI-compatible models through [LiteLLM](https://github.com/BerriAI/litellm).
|
|
39
|
+
|
|
40
|
+
[](https://pypi.org/project/litellm-mysubs/)
|
|
41
|
+
[](https://pypi.org/project/litellm-mysubs/)
|
|
42
|
+
[](https://github.com/eduardopessin/litellm-mysubs/actions/workflows/ci.yml)
|
|
43
|
+
[](LICENSE)
|
|
44
|
+
|
|
45
|
+
## The problem
|
|
46
|
+
|
|
47
|
+
A subscription is not an API key, and the difference is not cosmetic:
|
|
48
|
+
|
|
49
|
+
- **The tokens are first-party OAuth.** They come from the provider's own client flow, they
|
|
50
|
+
rotate, and they expire — there is no static string to paste into `model_list`.
|
|
51
|
+
- **The model set is different.** What a subscription serves is not the public API catalog.
|
|
52
|
+
`claude-sonnet-4-20250514` exists on the Anthropic API and returns 404 on a Max account.
|
|
53
|
+
Neither Anthropic nor Codex expose a catalog endpoint for subscription tokens.
|
|
54
|
+
- **Each provider speaks its own protocol.** Codex speaks the Responses API, Antigravity
|
|
55
|
+
speaks Cloud Code, Anthropic speaks Messages. None of them is the chat-completions shape
|
|
56
|
+
your client sends.
|
|
57
|
+
|
|
58
|
+
This package absorbs those three differences so any OpenAI client sees plain LiteLLM models.
|
|
59
|
+
|
|
60
|
+
## Quickstart
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install litellm-mysubs && mysubs-setup
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
1. `mysubs-setup` locates the LiteLLM in your environment and the `config.yaml` it loads,
|
|
67
|
+
then appends one line to it.
|
|
68
|
+
2. Restart the proxy.
|
|
69
|
+
3. Open the LiteLLM UI as an admin and go to **Experimental → MySubs** (or go straight to
|
|
70
|
+
`<proxy-url>/mysubs`).
|
|
71
|
+
|
|
72
|
+
<img src="docs/menu.png" alt="The MySubs entry under Experimental in the LiteLLM sidebar" width="300">
|
|
73
|
+
|
|
74
|
+
4. Press **Connect** on a provider card, sign in, and paste back the URL your browser
|
|
75
|
+
lands on.
|
|
76
|
+
|
|
77
|
+

|
|
78
|
+
|
|
79
|
+
5. Pick the models you want and apply. Connected cards show the quota the provider reports:
|
|
80
|
+
|
|
81
|
+

|
|
82
|
+
|
|
83
|
+
The single line `mysubs-setup` adds:
|
|
84
|
+
|
|
85
|
+
```yaml
|
|
86
|
+
litellm_settings:
|
|
87
|
+
callbacks: ["litellm_mysubs.proxy_handler_instance"]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
It does not touch `model_list`, `router_settings` or `general_settings` — routing you
|
|
91
|
+
already configured is not the installer's business. It leaves the original at
|
|
92
|
+
`config.yaml.mysubs-bak` and refuses to write a file that would no longer load.
|
|
93
|
+
|
|
94
|
+
### What you end up with
|
|
95
|
+
|
|
96
|
+
The models you applied appear in **Models + Endpoints** like any other deployment — same
|
|
97
|
+
table, same virtual keys, same cost tracking. The subscription is no longer a separate
|
|
98
|
+
thing your clients have to know about.
|
|
99
|
+
|
|
100
|
+

|
|
101
|
+
|
|
102
|
+
Every one is callable straight away:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
curl $PROXY/v1/chat/completions -H "Authorization: Bearer $KEY" \
|
|
106
|
+
-d '{"model":"mysubs/claudecode/claude-opus-5","messages":[{"role":"user","content":"hi"}]}'
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## How connecting works
|
|
110
|
+
|
|
111
|
+
1. **Press Connect.** The card opens the provider's login page in your browser.
|
|
112
|
+
2. **Authenticate** with the provider as usual.
|
|
113
|
+
3. **Return the result.** Paste the URL your browser ends up on — see below.
|
|
114
|
+
4. **Discovery runs.** The page probes each candidate model against your account and shows
|
|
115
|
+
what actually answered. Nothing is listed as available unless the upstream replied.
|
|
116
|
+
5. **Pick and apply.** The selected models are injected into the LiteLLM Router under the
|
|
117
|
+
`mysubs/<subscription>/` prefix and are immediately callable by any client.
|
|
118
|
+
|
|
119
|
+
Tokens are then refreshed in the background, with a `flock` held across processes so that
|
|
120
|
+
multiple proxy workers never race on the same rotating refresh token.
|
|
121
|
+
|
|
122
|
+
### Returning the result
|
|
123
|
+
|
|
124
|
+
These OAuth clients register `http://localhost:54545/callback` (and `:1455`, `:51121`) as
|
|
125
|
+
their redirect. `localhost` resolves in the **browser**, so that port would have to be open
|
|
126
|
+
on the machine you are browsing from — and the proxy usually runs somewhere else. The
|
|
127
|
+
redirect therefore lands on a page that cannot load. That is expected, and the page tells
|
|
128
|
+
you so before you start.
|
|
129
|
+
|
|
130
|
+
**Paste the URL.** Copy whatever is in the address bar after you authenticate — the
|
|
131
|
+
`This site can't be reached` one — and paste it into the box on the page. The authorization
|
|
132
|
+
code is in it. This is the default path, it needs nothing installed anywhere, and it works
|
|
133
|
+
over SSH, from a phone, or on a machine with no browser at all.
|
|
134
|
+
|
|
135
|
+
<details>
|
|
136
|
+
<summary><b>Optional: skip the paste with a local command</b></summary>
|
|
137
|
+
|
|
138
|
+
If you would rather not copy anything, the page also issues a pairing code for a helper you
|
|
139
|
+
run on the machine with the browser:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
pip install litellm-mysubs
|
|
143
|
+
mysubs-login anthropic --url https://your-proxy --code XXXX-XXXX-XXXX
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
It opens the loopback port the provider expects, catches the redirect itself, and deposits
|
|
147
|
+
the credential in the proxy. The page notices and moves on by itself.
|
|
148
|
+
|
|
149
|
+
The pairing code lives ten minutes, is single-use, and authorises exactly one provider —
|
|
150
|
+
that is what keeps the proxy admin key off your command line.
|
|
151
|
+
|
|
152
|
+
If LiteLLM runs on your own machine, drop `--url` and `--code`: it writes straight to the
|
|
153
|
+
local store.
|
|
154
|
+
|
|
155
|
+
</details>
|
|
156
|
+
|
|
157
|
+
## What is guaranteed
|
|
158
|
+
|
|
159
|
+
- **Inert until a subscription is connected.** The patch is only applied once at least one
|
|
160
|
+
credential exists. Installed with no subscriptions, it is indistinguishable from not being
|
|
161
|
+
installed — the Router is untouched.
|
|
162
|
+
- **Your `config.yaml` survives.** One line appended, a backup written next to it, and a
|
|
163
|
+
refusal to save a file that would not parse.
|
|
164
|
+
- **The UI does not depend on a patched bundle.** `/mysubs` is a mounted FastAPI sub-app and
|
|
165
|
+
always works by direct URL. The **Experimental** menu entry is a best-effort string patch
|
|
166
|
+
of a pre-compiled Next.js chunk whose filename is a build hash; when a new LiteLLM version
|
|
167
|
+
does not match, it logs the direct URL instead of failing. Nothing in `site-packages` is
|
|
168
|
+
ever rewritten — the modified copy is served from memory.
|
|
169
|
+
- **Credentials go where your policy says.** A `0600` file at
|
|
170
|
+
`~/.litellm/mysubs/credentials.json` by default, the secret manager LiteLLM already has
|
|
171
|
+
configured (`general_settings.key_management_system`) if you run one, or read-only
|
|
172
|
+
environment variables. Loose permissions on the file are rejected, not silently fixed.
|
|
173
|
+
- **Never a fabricated number.** An unreachable provider shows the error or the last real
|
|
174
|
+
snapshot labelled with its age. A model name the subscription does not serve returns the
|
|
175
|
+
upstream error — it is never silently answered by a different model.
|
|
176
|
+
|
|
177
|
+
### Turning it off
|
|
178
|
+
|
|
179
|
+
| | |
|
|
180
|
+
|---|---|
|
|
181
|
+
| `MYSUBS_DISABLE=1` | disables everything without editing `config.yaml` |
|
|
182
|
+
| `MYSUBS_DISABLE_AUTH=1` | skips the `proxy_admin` check (proxies with no key database) |
|
|
183
|
+
| remove the `callbacks` line | uninstalls |
|
|
184
|
+
|
|
185
|
+
## Providers
|
|
186
|
+
|
|
187
|
+
| Provider | Model prefix | Wire protocol | Quota reported |
|
|
188
|
+
|---|---|---|---|
|
|
189
|
+
| Claude Max | `mysubs/claudecode/` | Messages | 5h / 7d, from headers + `/api/oauth/usage` |
|
|
190
|
+
| ChatGPT Plus (Codex) | `mysubs/codex/` | Responses API | 5h / 7d, from headers + `wham/usage` |
|
|
191
|
+
| Google Antigravity | `mysubs/antigravity/` | Cloud Code | `:retrieveUserQuotaSummary` only |
|
|
192
|
+
|
|
193
|
+
On the wire that means `api.anthropic.com/v1/messages`,
|
|
194
|
+
`chatgpt.com/backend-api/codex/responses`, and `v1internal:streamGenerateContent`.
|
|
195
|
+
Antigravity is the one case where the quota endpoint is the only source: measured against
|
|
196
|
+
the real backend, it returns no rate-limit headers at all.
|
|
197
|
+
|
|
198
|
+
Anthropic and Codex have no catalog endpoint for subscription tokens, so their model lists
|
|
199
|
+
come from a curated set of measured names plus a live probe of each one. Antigravity has a
|
|
200
|
+
real catalog (`:fetchAvailableModels`) and it is used directly.
|
|
201
|
+
|
|
202
|
+
## Development
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
pip install -e ".[dev]"
|
|
206
|
+
pytest # unit tests
|
|
207
|
+
ruff check . && mypy # lint and types
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
1628 tests, 86% branch coverage (the suite fails below 85%), `ruff` and `mypy --strict`
|
|
211
|
+
clean. Tests that touch real LiteLLM internals need the proxy extras:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
pip install "litellm[proxy]"
|
|
215
|
+
pytest tests/test_litellm_contract.py
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
That file asserts the internal symbols the patch depends on — `Router.acompletion`,
|
|
219
|
+
`route_llm_request.route_request`, `custom_provider_map`. CI runs it against both the pinned
|
|
220
|
+
`litellm[proxy]` 1.101.0 and the current release, which turns an incompatible upstream
|
|
221
|
+
upgrade into a red build instead of a production outage.
|
|
222
|
+
|
|
223
|
+
CI also runs the unit suite on Python 3.11, 3.12 and 3.13, and verifies 186 source anchors
|
|
224
|
+
against the upstream implementations they were ported from, on every push. An anchor pins
|
|
225
|
+
both the symbol name and, where it matters, its value — a renamed endpoint path or a bumped
|
|
226
|
+
client version fails the build rather than drifting silently.
|
|
227
|
+
|
|
228
|
+
## Architecture
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
src/litellm_mysubs/
|
|
232
|
+
├── credentials/ pluggable store (0600 file, secret manager, env), OAuth,
|
|
233
|
+
│ loopback callback server, cross-process refresh lock
|
|
234
|
+
├── wire/ one module per provider; they never reference each other
|
|
235
|
+
├── transport/ HTTP client, SSE, host failover, retry
|
|
236
|
+
├── catalog/ discovery of the models a subscription actually serves, quota
|
|
237
|
+
├── ui/ the `/mysubs` sub-app: cards, pairing, apply
|
|
238
|
+
├── login_cli.py `mysubs-login` — the interceptor, run on your own machine
|
|
239
|
+
├── setup_cli.py `mysubs-setup` — the one-line config edit
|
|
240
|
+
├── registry.py Router injection and guards against phantom deployments
|
|
241
|
+
└── plugin.py the only module that mutates global state
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Every module imports without side effects. Only `plugin.py` modifies LiteLLM, and only when
|
|
245
|
+
invoked — which is what keeps the rest unit-testable.
|
|
246
|
+
|
|
247
|
+
## Design decisions
|
|
248
|
+
|
|
249
|
+
[`docs/DECISIONS.md`](docs/DECISIONS.md) holds ten entries. Each records what was decided,
|
|
250
|
+
**the measurement that supports it**, and what would reopen the question. Without the
|
|
251
|
+
measurement it is not a decision, it is a preference.
|
|
252
|
+
|
|
253
|
+
For example, D1 explains why streaming stays in a monkey-patch instead of the official
|
|
254
|
+
`CustomLLM` path: a minimal handler reporting `prompt_tokens=100, completion_tokens=5,
|
|
255
|
+
cached_tokens=80` had 8/2 delivered to the client and `cached_tokens` lost, in every one of
|
|
256
|
+
the four supported ways of emitting the final chunk. Without real usage, LiteLLM estimates
|
|
257
|
+
with `token_counter` and every cache hit becomes invisible in `/spend/logs` — on a
|
|
258
|
+
subscription account that is the difference between 8697 and 2876 prompt tokens for the same
|
|
259
|
+
request, and the only way to know why the quota ran out.
|
|
260
|
+
|
|
261
|
+
## License
|
|
262
|
+
|
|
263
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# litellm-mysubs
|
|
2
|
+
|
|
3
|
+
Serve your Claude Max, ChatGPT Plus (Codex) and Google Antigravity subscriptions as ordinary
|
|
4
|
+
OpenAI-compatible models through [LiteLLM](https://github.com/BerriAI/litellm).
|
|
5
|
+
|
|
6
|
+
[](https://pypi.org/project/litellm-mysubs/)
|
|
7
|
+
[](https://pypi.org/project/litellm-mysubs/)
|
|
8
|
+
[](https://github.com/eduardopessin/litellm-mysubs/actions/workflows/ci.yml)
|
|
9
|
+
[](LICENSE)
|
|
10
|
+
|
|
11
|
+
## The problem
|
|
12
|
+
|
|
13
|
+
A subscription is not an API key, and the difference is not cosmetic:
|
|
14
|
+
|
|
15
|
+
- **The tokens are first-party OAuth.** They come from the provider's own client flow, they
|
|
16
|
+
rotate, and they expire — there is no static string to paste into `model_list`.
|
|
17
|
+
- **The model set is different.** What a subscription serves is not the public API catalog.
|
|
18
|
+
`claude-sonnet-4-20250514` exists on the Anthropic API and returns 404 on a Max account.
|
|
19
|
+
Neither Anthropic nor Codex expose a catalog endpoint for subscription tokens.
|
|
20
|
+
- **Each provider speaks its own protocol.** Codex speaks the Responses API, Antigravity
|
|
21
|
+
speaks Cloud Code, Anthropic speaks Messages. None of them is the chat-completions shape
|
|
22
|
+
your client sends.
|
|
23
|
+
|
|
24
|
+
This package absorbs those three differences so any OpenAI client sees plain LiteLLM models.
|
|
25
|
+
|
|
26
|
+
## Quickstart
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install litellm-mysubs && mysubs-setup
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
1. `mysubs-setup` locates the LiteLLM in your environment and the `config.yaml` it loads,
|
|
33
|
+
then appends one line to it.
|
|
34
|
+
2. Restart the proxy.
|
|
35
|
+
3. Open the LiteLLM UI as an admin and go to **Experimental → MySubs** (or go straight to
|
|
36
|
+
`<proxy-url>/mysubs`).
|
|
37
|
+
|
|
38
|
+
<img src="docs/menu.png" alt="The MySubs entry under Experimental in the LiteLLM sidebar" width="300">
|
|
39
|
+
|
|
40
|
+
4. Press **Connect** on a provider card, sign in, and paste back the URL your browser
|
|
41
|
+
lands on.
|
|
42
|
+
|
|
43
|
+

|
|
44
|
+
|
|
45
|
+
5. Pick the models you want and apply. Connected cards show the quota the provider reports:
|
|
46
|
+
|
|
47
|
+

|
|
48
|
+
|
|
49
|
+
The single line `mysubs-setup` adds:
|
|
50
|
+
|
|
51
|
+
```yaml
|
|
52
|
+
litellm_settings:
|
|
53
|
+
callbacks: ["litellm_mysubs.proxy_handler_instance"]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
It does not touch `model_list`, `router_settings` or `general_settings` — routing you
|
|
57
|
+
already configured is not the installer's business. It leaves the original at
|
|
58
|
+
`config.yaml.mysubs-bak` and refuses to write a file that would no longer load.
|
|
59
|
+
|
|
60
|
+
### What you end up with
|
|
61
|
+
|
|
62
|
+
The models you applied appear in **Models + Endpoints** like any other deployment — same
|
|
63
|
+
table, same virtual keys, same cost tracking. The subscription is no longer a separate
|
|
64
|
+
thing your clients have to know about.
|
|
65
|
+
|
|
66
|
+

|
|
67
|
+
|
|
68
|
+
Every one is callable straight away:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
curl $PROXY/v1/chat/completions -H "Authorization: Bearer $KEY" \
|
|
72
|
+
-d '{"model":"mysubs/claudecode/claude-opus-5","messages":[{"role":"user","content":"hi"}]}'
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## How connecting works
|
|
76
|
+
|
|
77
|
+
1. **Press Connect.** The card opens the provider's login page in your browser.
|
|
78
|
+
2. **Authenticate** with the provider as usual.
|
|
79
|
+
3. **Return the result.** Paste the URL your browser ends up on — see below.
|
|
80
|
+
4. **Discovery runs.** The page probes each candidate model against your account and shows
|
|
81
|
+
what actually answered. Nothing is listed as available unless the upstream replied.
|
|
82
|
+
5. **Pick and apply.** The selected models are injected into the LiteLLM Router under the
|
|
83
|
+
`mysubs/<subscription>/` prefix and are immediately callable by any client.
|
|
84
|
+
|
|
85
|
+
Tokens are then refreshed in the background, with a `flock` held across processes so that
|
|
86
|
+
multiple proxy workers never race on the same rotating refresh token.
|
|
87
|
+
|
|
88
|
+
### Returning the result
|
|
89
|
+
|
|
90
|
+
These OAuth clients register `http://localhost:54545/callback` (and `:1455`, `:51121`) as
|
|
91
|
+
their redirect. `localhost` resolves in the **browser**, so that port would have to be open
|
|
92
|
+
on the machine you are browsing from — and the proxy usually runs somewhere else. The
|
|
93
|
+
redirect therefore lands on a page that cannot load. That is expected, and the page tells
|
|
94
|
+
you so before you start.
|
|
95
|
+
|
|
96
|
+
**Paste the URL.** Copy whatever is in the address bar after you authenticate — the
|
|
97
|
+
`This site can't be reached` one — and paste it into the box on the page. The authorization
|
|
98
|
+
code is in it. This is the default path, it needs nothing installed anywhere, and it works
|
|
99
|
+
over SSH, from a phone, or on a machine with no browser at all.
|
|
100
|
+
|
|
101
|
+
<details>
|
|
102
|
+
<summary><b>Optional: skip the paste with a local command</b></summary>
|
|
103
|
+
|
|
104
|
+
If you would rather not copy anything, the page also issues a pairing code for a helper you
|
|
105
|
+
run on the machine with the browser:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
pip install litellm-mysubs
|
|
109
|
+
mysubs-login anthropic --url https://your-proxy --code XXXX-XXXX-XXXX
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
It opens the loopback port the provider expects, catches the redirect itself, and deposits
|
|
113
|
+
the credential in the proxy. The page notices and moves on by itself.
|
|
114
|
+
|
|
115
|
+
The pairing code lives ten minutes, is single-use, and authorises exactly one provider —
|
|
116
|
+
that is what keeps the proxy admin key off your command line.
|
|
117
|
+
|
|
118
|
+
If LiteLLM runs on your own machine, drop `--url` and `--code`: it writes straight to the
|
|
119
|
+
local store.
|
|
120
|
+
|
|
121
|
+
</details>
|
|
122
|
+
|
|
123
|
+
## What is guaranteed
|
|
124
|
+
|
|
125
|
+
- **Inert until a subscription is connected.** The patch is only applied once at least one
|
|
126
|
+
credential exists. Installed with no subscriptions, it is indistinguishable from not being
|
|
127
|
+
installed — the Router is untouched.
|
|
128
|
+
- **Your `config.yaml` survives.** One line appended, a backup written next to it, and a
|
|
129
|
+
refusal to save a file that would not parse.
|
|
130
|
+
- **The UI does not depend on a patched bundle.** `/mysubs` is a mounted FastAPI sub-app and
|
|
131
|
+
always works by direct URL. The **Experimental** menu entry is a best-effort string patch
|
|
132
|
+
of a pre-compiled Next.js chunk whose filename is a build hash; when a new LiteLLM version
|
|
133
|
+
does not match, it logs the direct URL instead of failing. Nothing in `site-packages` is
|
|
134
|
+
ever rewritten — the modified copy is served from memory.
|
|
135
|
+
- **Credentials go where your policy says.** A `0600` file at
|
|
136
|
+
`~/.litellm/mysubs/credentials.json` by default, the secret manager LiteLLM already has
|
|
137
|
+
configured (`general_settings.key_management_system`) if you run one, or read-only
|
|
138
|
+
environment variables. Loose permissions on the file are rejected, not silently fixed.
|
|
139
|
+
- **Never a fabricated number.** An unreachable provider shows the error or the last real
|
|
140
|
+
snapshot labelled with its age. A model name the subscription does not serve returns the
|
|
141
|
+
upstream error — it is never silently answered by a different model.
|
|
142
|
+
|
|
143
|
+
### Turning it off
|
|
144
|
+
|
|
145
|
+
| | |
|
|
146
|
+
|---|---|
|
|
147
|
+
| `MYSUBS_DISABLE=1` | disables everything without editing `config.yaml` |
|
|
148
|
+
| `MYSUBS_DISABLE_AUTH=1` | skips the `proxy_admin` check (proxies with no key database) |
|
|
149
|
+
| remove the `callbacks` line | uninstalls |
|
|
150
|
+
|
|
151
|
+
## Providers
|
|
152
|
+
|
|
153
|
+
| Provider | Model prefix | Wire protocol | Quota reported |
|
|
154
|
+
|---|---|---|---|
|
|
155
|
+
| Claude Max | `mysubs/claudecode/` | Messages | 5h / 7d, from headers + `/api/oauth/usage` |
|
|
156
|
+
| ChatGPT Plus (Codex) | `mysubs/codex/` | Responses API | 5h / 7d, from headers + `wham/usage` |
|
|
157
|
+
| Google Antigravity | `mysubs/antigravity/` | Cloud Code | `:retrieveUserQuotaSummary` only |
|
|
158
|
+
|
|
159
|
+
On the wire that means `api.anthropic.com/v1/messages`,
|
|
160
|
+
`chatgpt.com/backend-api/codex/responses`, and `v1internal:streamGenerateContent`.
|
|
161
|
+
Antigravity is the one case where the quota endpoint is the only source: measured against
|
|
162
|
+
the real backend, it returns no rate-limit headers at all.
|
|
163
|
+
|
|
164
|
+
Anthropic and Codex have no catalog endpoint for subscription tokens, so their model lists
|
|
165
|
+
come from a curated set of measured names plus a live probe of each one. Antigravity has a
|
|
166
|
+
real catalog (`:fetchAvailableModels`) and it is used directly.
|
|
167
|
+
|
|
168
|
+
## Development
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
pip install -e ".[dev]"
|
|
172
|
+
pytest # unit tests
|
|
173
|
+
ruff check . && mypy # lint and types
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
1628 tests, 86% branch coverage (the suite fails below 85%), `ruff` and `mypy --strict`
|
|
177
|
+
clean. Tests that touch real LiteLLM internals need the proxy extras:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
pip install "litellm[proxy]"
|
|
181
|
+
pytest tests/test_litellm_contract.py
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
That file asserts the internal symbols the patch depends on — `Router.acompletion`,
|
|
185
|
+
`route_llm_request.route_request`, `custom_provider_map`. CI runs it against both the pinned
|
|
186
|
+
`litellm[proxy]` 1.101.0 and the current release, which turns an incompatible upstream
|
|
187
|
+
upgrade into a red build instead of a production outage.
|
|
188
|
+
|
|
189
|
+
CI also runs the unit suite on Python 3.11, 3.12 and 3.13, and verifies 186 source anchors
|
|
190
|
+
against the upstream implementations they were ported from, on every push. An anchor pins
|
|
191
|
+
both the symbol name and, where it matters, its value — a renamed endpoint path or a bumped
|
|
192
|
+
client version fails the build rather than drifting silently.
|
|
193
|
+
|
|
194
|
+
## Architecture
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
src/litellm_mysubs/
|
|
198
|
+
├── credentials/ pluggable store (0600 file, secret manager, env), OAuth,
|
|
199
|
+
│ loopback callback server, cross-process refresh lock
|
|
200
|
+
├── wire/ one module per provider; they never reference each other
|
|
201
|
+
├── transport/ HTTP client, SSE, host failover, retry
|
|
202
|
+
├── catalog/ discovery of the models a subscription actually serves, quota
|
|
203
|
+
├── ui/ the `/mysubs` sub-app: cards, pairing, apply
|
|
204
|
+
├── login_cli.py `mysubs-login` — the interceptor, run on your own machine
|
|
205
|
+
├── setup_cli.py `mysubs-setup` — the one-line config edit
|
|
206
|
+
├── registry.py Router injection and guards against phantom deployments
|
|
207
|
+
└── plugin.py the only module that mutates global state
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Every module imports without side effects. Only `plugin.py` modifies LiteLLM, and only when
|
|
211
|
+
invoked — which is what keeps the rest unit-testable.
|
|
212
|
+
|
|
213
|
+
## Design decisions
|
|
214
|
+
|
|
215
|
+
[`docs/DECISIONS.md`](docs/DECISIONS.md) holds ten entries. Each records what was decided,
|
|
216
|
+
**the measurement that supports it**, and what would reopen the question. Without the
|
|
217
|
+
measurement it is not a decision, it is a preference.
|
|
218
|
+
|
|
219
|
+
For example, D1 explains why streaming stays in a monkey-patch instead of the official
|
|
220
|
+
`CustomLLM` path: a minimal handler reporting `prompt_tokens=100, completion_tokens=5,
|
|
221
|
+
cached_tokens=80` had 8/2 delivered to the client and `cached_tokens` lost, in every one of
|
|
222
|
+
the four supported ways of emitting the final chunk. Without real usage, LiteLLM estimates
|
|
223
|
+
with `token_counter` and every cache hit becomes invisible in `/spend/logs` — on a
|
|
224
|
+
subscription account that is the difference between 8697 and 2876 prompt tokens for the same
|
|
225
|
+
request, and the only way to know why the quota ran out.
|
|
226
|
+
|
|
227
|
+
## License
|
|
228
|
+
|
|
229
|
+
MIT — see [LICENSE](LICENSE).
|