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.
Files changed (104) hide show
  1. litellm_mysubs-0.1.0/.gitignore +16 -0
  2. litellm_mysubs-0.1.0/CHANGELOG.md +48 -0
  3. litellm_mysubs-0.1.0/LICENSE +21 -0
  4. litellm_mysubs-0.1.0/PKG-INFO +263 -0
  5. litellm_mysubs-0.1.0/README.md +229 -0
  6. litellm_mysubs-0.1.0/docs/DECISIONS.md +528 -0
  7. litellm_mysubs-0.1.0/docs/OMP.md +89 -0
  8. litellm_mysubs-0.1.0/docs/menu.png +0 -0
  9. litellm_mysubs-0.1.0/docs/models.png +0 -0
  10. litellm_mysubs-0.1.0/docs/mysubs-empty.png +0 -0
  11. litellm_mysubs-0.1.0/docs/mysubs.png +0 -0
  12. litellm_mysubs-0.1.0/pyproject.toml +120 -0
  13. litellm_mysubs-0.1.0/src/litellm_mysubs/__init__.py +30 -0
  14. litellm_mysubs-0.1.0/src/litellm_mysubs/bootstrap.py +174 -0
  15. litellm_mysubs-0.1.0/src/litellm_mysubs/callback.py +195 -0
  16. litellm_mysubs-0.1.0/src/litellm_mysubs/catalog/__init__.py +0 -0
  17. litellm_mysubs-0.1.0/src/litellm_mysubs/catalog/deployments.py +153 -0
  18. litellm_mysubs-0.1.0/src/litellm_mysubs/catalog/discovery.py +691 -0
  19. litellm_mysubs-0.1.0/src/litellm_mysubs/catalog/selection.py +117 -0
  20. litellm_mysubs-0.1.0/src/litellm_mysubs/catalog/usage.py +565 -0
  21. litellm_mysubs-0.1.0/src/litellm_mysubs/catalog/usage_probe.py +96 -0
  22. litellm_mysubs-0.1.0/src/litellm_mysubs/credentials/__init__.py +0 -0
  23. litellm_mysubs-0.1.0/src/litellm_mysubs/credentials/callback_server.py +439 -0
  24. litellm_mysubs-0.1.0/src/litellm_mysubs/credentials/env_store.py +68 -0
  25. litellm_mysubs-0.1.0/src/litellm_mysubs/credentials/file_store.py +114 -0
  26. litellm_mysubs-0.1.0/src/litellm_mysubs/credentials/lock.py +160 -0
  27. litellm_mysubs-0.1.0/src/litellm_mysubs/credentials/oauth.py +820 -0
  28. litellm_mysubs-0.1.0/src/litellm_mysubs/credentials/refresher.py +268 -0
  29. litellm_mysubs-0.1.0/src/litellm_mysubs/credentials/secret_store.py +199 -0
  30. litellm_mysubs-0.1.0/src/litellm_mysubs/credentials/store.py +116 -0
  31. litellm_mysubs-0.1.0/src/litellm_mysubs/login_cli.py +235 -0
  32. litellm_mysubs-0.1.0/src/litellm_mysubs/plugin.py +1015 -0
  33. litellm_mysubs-0.1.0/src/litellm_mysubs/py.typed +0 -0
  34. litellm_mysubs-0.1.0/src/litellm_mysubs/registry.py +129 -0
  35. litellm_mysubs-0.1.0/src/litellm_mysubs/setup_cli.py +331 -0
  36. litellm_mysubs-0.1.0/src/litellm_mysubs/transport/__init__.py +0 -0
  37. litellm_mysubs-0.1.0/src/litellm_mysubs/transport/client.py +288 -0
  38. litellm_mysubs-0.1.0/src/litellm_mysubs/transport/hosts.py +89 -0
  39. litellm_mysubs-0.1.0/src/litellm_mysubs/transport/retry.py +100 -0
  40. litellm_mysubs-0.1.0/src/litellm_mysubs/transport/sse.py +50 -0
  41. litellm_mysubs-0.1.0/src/litellm_mysubs/ui/__init__.py +7 -0
  42. litellm_mysubs-0.1.0/src/litellm_mysubs/ui/app.py +1168 -0
  43. litellm_mysubs-0.1.0/src/litellm_mysubs/ui/auth.py +203 -0
  44. litellm_mysubs-0.1.0/src/litellm_mysubs/ui/install.py +144 -0
  45. litellm_mysubs-0.1.0/src/litellm_mysubs/ui/local_flow.py +288 -0
  46. litellm_mysubs-0.1.0/src/litellm_mysubs/ui/menu.py +213 -0
  47. litellm_mysubs-0.1.0/src/litellm_mysubs/ui/pairing.py +171 -0
  48. litellm_mysubs-0.1.0/src/litellm_mysubs/ui/service.py +573 -0
  49. litellm_mysubs-0.1.0/src/litellm_mysubs/ui/throttle.py +197 -0
  50. litellm_mysubs-0.1.0/src/litellm_mysubs/wire/__init__.py +0 -0
  51. litellm_mysubs-0.1.0/src/litellm_mysubs/wire/anthropic.py +762 -0
  52. litellm_mysubs-0.1.0/src/litellm_mysubs/wire/antigravity.py +694 -0
  53. litellm_mysubs-0.1.0/src/litellm_mysubs/wire/antigravity_models.py +191 -0
  54. litellm_mysubs-0.1.0/src/litellm_mysubs/wire/codex.py +785 -0
  55. litellm_mysubs-0.1.0/src/litellm_mysubs/wire/planning_leak.py +201 -0
  56. litellm_mysubs-0.1.0/src/litellm_mysubs/wire/schema.py +1440 -0
  57. litellm_mysubs-0.1.0/src/litellm_mysubs/wire/thinking_loop.py +317 -0
  58. litellm_mysubs-0.1.0/src/litellm_mysubs/wire/usage.py +141 -0
  59. litellm_mysubs-0.1.0/tests/__init__.py +0 -0
  60. litellm_mysubs-0.1.0/tests/conftest.py +47 -0
  61. litellm_mysubs-0.1.0/tests/fixtures/cca_schema_cases.json +14038 -0
  62. litellm_mysubs-0.1.0/tests/fixtures/cca_schema_expected.json +1 -0
  63. litellm_mysubs-0.1.0/tests/test_antigravity_retired.py +312 -0
  64. litellm_mysubs-0.1.0/tests/test_bootstrap.py +231 -0
  65. litellm_mysubs-0.1.0/tests/test_callback_cors.py +209 -0
  66. litellm_mysubs-0.1.0/tests/test_callback_server.py +234 -0
  67. litellm_mysubs-0.1.0/tests/test_catalog_deployments.py +95 -0
  68. litellm_mysubs-0.1.0/tests/test_catalog_discovery.py +666 -0
  69. litellm_mysubs-0.1.0/tests/test_catalog_usage.py +89 -0
  70. litellm_mysubs-0.1.0/tests/test_credentials.py +130 -0
  71. litellm_mysubs-0.1.0/tests/test_credentials_oauth.py +780 -0
  72. litellm_mysubs-0.1.0/tests/test_credentials_secret_store.py +153 -0
  73. litellm_mysubs-0.1.0/tests/test_deployments_prefix.py +270 -0
  74. litellm_mysubs-0.1.0/tests/test_litellm_contract.py +73 -0
  75. litellm_mysubs-0.1.0/tests/test_local_flow.py +311 -0
  76. litellm_mysubs-0.1.0/tests/test_lock.py +263 -0
  77. litellm_mysubs-0.1.0/tests/test_omp_drift.py +187 -0
  78. litellm_mysubs-0.1.0/tests/test_pairing.py +176 -0
  79. litellm_mysubs-0.1.0/tests/test_planning_leak.py +136 -0
  80. litellm_mysubs-0.1.0/tests/test_plugin.py +923 -0
  81. litellm_mysubs-0.1.0/tests/test_refresher.py +302 -0
  82. litellm_mysubs-0.1.0/tests/test_registry.py +135 -0
  83. litellm_mysubs-0.1.0/tests/test_schema.py +257 -0
  84. litellm_mysubs-0.1.0/tests/test_schema_differential.py +54 -0
  85. litellm_mysubs-0.1.0/tests/test_selection_store.py +157 -0
  86. litellm_mysubs-0.1.0/tests/test_setup_cli.py +208 -0
  87. litellm_mysubs-0.1.0/tests/test_thinking_loop.py +265 -0
  88. litellm_mysubs-0.1.0/tests/test_throttle.py +262 -0
  89. litellm_mysubs-0.1.0/tests/test_transport.py +148 -0
  90. litellm_mysubs-0.1.0/tests/test_transport_client.py +395 -0
  91. litellm_mysubs-0.1.0/tests/test_ui.py +404 -0
  92. litellm_mysubs-0.1.0/tests/test_ui_interceptor.py +864 -0
  93. litellm_mysubs-0.1.0/tests/test_ui_menu.py +154 -0
  94. litellm_mysubs-0.1.0/tests/test_usage.py +140 -0
  95. litellm_mysubs-0.1.0/tests/test_usage_antigravity.py +332 -0
  96. litellm_mysubs-0.1.0/tests/test_usage_limits.py +123 -0
  97. litellm_mysubs-0.1.0/tests/test_usage_probe.py +249 -0
  98. litellm_mysubs-0.1.0/tests/test_wire_anthropic.py +451 -0
  99. litellm_mysubs-0.1.0/tests/test_wire_anthropic_edges.py +231 -0
  100. litellm_mysubs-0.1.0/tests/test_wire_antigravity.py +574 -0
  101. litellm_mysubs-0.1.0/tests/test_wire_codex.py +541 -0
  102. litellm_mysubs-0.1.0/tests/test_wire_codex_edges.py +244 -0
  103. litellm_mysubs-0.1.0/tools/check_omp_drift.py +212 -0
  104. litellm_mysubs-0.1.0/tools/spike_custom_llm.py +132 -0
@@ -0,0 +1,16 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+ .coverage
8
+ dist/
9
+ build/
10
+ *.egg-info/
11
+
12
+ # Session material, not product
13
+ local/
14
+
15
+ # Lockfile of a tool the CI does not use
16
+ uv.lock
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/litellm-mysubs.svg)](https://pypi.org/project/litellm-mysubs/)
41
+ [![Python](https://img.shields.io/pypi/pyversions/litellm-mysubs.svg)](https://pypi.org/project/litellm-mysubs/)
42
+ [![CI](https://github.com/eduardopessin/litellm-mysubs/actions/workflows/ci.yml/badge.svg)](https://github.com/eduardopessin/litellm-mysubs/actions/workflows/ci.yml)
43
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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
+ ![The MySubs page as it looks before anything is connected: three provider cards, each with a Connect button and a box to paste the return URL](docs/mysubs-empty.png)
78
+
79
+ 5. Pick the models you want and apply. Connected cards show the quota the provider reports:
80
+
81
+ ![The same page with all three subscriptions connected, each showing its quota windows and usage](docs/mysubs.png)
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
+ ![The applied models listed in the LiteLLM Models page, with per-token costs](docs/models.png)
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
+ [![PyPI](https://img.shields.io/pypi/v/litellm-mysubs.svg)](https://pypi.org/project/litellm-mysubs/)
7
+ [![Python](https://img.shields.io/pypi/pyversions/litellm-mysubs.svg)](https://pypi.org/project/litellm-mysubs/)
8
+ [![CI](https://github.com/eduardopessin/litellm-mysubs/actions/workflows/ci.yml/badge.svg)](https://github.com/eduardopessin/litellm-mysubs/actions/workflows/ci.yml)
9
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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
+ ![The MySubs page as it looks before anything is connected: three provider cards, each with a Connect button and a box to paste the return URL](docs/mysubs-empty.png)
44
+
45
+ 5. Pick the models you want and apply. Connected cards show the quota the provider reports:
46
+
47
+ ![The same page with all three subscriptions connected, each showing its quota windows and usage](docs/mysubs.png)
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
+ ![The applied models listed in the LiteLLM Models page, with per-token costs](docs/models.png)
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).