Metadata-Version: 2.5
Name: litellm-mysubs
Version: 0.1.0
Summary: Serve Claude Max, ChatGPT Plus/Codex and Google Antigravity subscriptions as OpenAI-compatible models through a LiteLLM proxy
Project-URL: Homepage, https://github.com/eduardopessin/litellm-mysubs
Project-URL: Source, https://github.com/eduardopessin/litellm-mysubs
Project-URL: Issues, https://github.com/eduardopessin/litellm-mysubs/issues
Project-URL: Changelog, https://github.com/eduardopessin/litellm-mysubs/blob/main/CHANGELOG.md
License-Expression: MIT
License-File: LICENSE
Keywords: antigravity,claude,codex,gemini,litellm,llm,oauth,openai-compatible,proxy,subscription
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: Proxy Servers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: fastapi>=0.110
Requires-Dist: httpx>=0.27
Requires-Dist: pyyaml>=6
Provides-Extra: dev
Requires-Dist: litellm[proxy]>=1.70; extra == 'dev'
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest-cov>=5; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# litellm-mysubs

Serve your Claude Max, ChatGPT Plus (Codex) and Google Antigravity subscriptions as ordinary
OpenAI-compatible models through [LiteLLM](https://github.com/BerriAI/litellm).

[![PyPI](https://img.shields.io/pypi/v/litellm-mysubs.svg)](https://pypi.org/project/litellm-mysubs/)
[![Python](https://img.shields.io/pypi/pyversions/litellm-mysubs.svg)](https://pypi.org/project/litellm-mysubs/)
[![CI](https://github.com/eduardopessin/litellm-mysubs/actions/workflows/ci.yml/badge.svg)](https://github.com/eduardopessin/litellm-mysubs/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

## The problem

A subscription is not an API key, and the difference is not cosmetic:

- **The tokens are first-party OAuth.** They come from the provider's own client flow, they
  rotate, and they expire — there is no static string to paste into `model_list`.
- **The model set is different.** What a subscription serves is not the public API catalog.
  `claude-sonnet-4-20250514` exists on the Anthropic API and returns 404 on a Max account.
  Neither Anthropic nor Codex expose a catalog endpoint for subscription tokens.
- **Each provider speaks its own protocol.** Codex speaks the Responses API, Antigravity
  speaks Cloud Code, Anthropic speaks Messages. None of them is the chat-completions shape
  your client sends.

This package absorbs those three differences so any OpenAI client sees plain LiteLLM models.

## Quickstart

```bash
pip install litellm-mysubs && mysubs-setup
```

1. `mysubs-setup` locates the LiteLLM in your environment and the `config.yaml` it loads,
   then appends one line to it.
2. Restart the proxy.
3. Open the LiteLLM UI as an admin and go to **Experimental → MySubs** (or go straight to
   `<proxy-url>/mysubs`).

<img src="docs/menu.png" alt="The MySubs entry under Experimental in the LiteLLM sidebar" width="300">

4. Press **Connect** on a provider card, sign in, and paste back the URL your browser
   lands on.

![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)

5. Pick the models you want and apply. Connected cards show the quota the provider reports:

![The same page with all three subscriptions connected, each showing its quota windows and usage](docs/mysubs.png)

The single line `mysubs-setup` adds:

```yaml
litellm_settings:
  callbacks: ["litellm_mysubs.proxy_handler_instance"]
```

It does not touch `model_list`, `router_settings` or `general_settings` — routing you
already configured is not the installer's business. It leaves the original at
`config.yaml.mysubs-bak` and refuses to write a file that would no longer load.

### What you end up with

The models you applied appear in **Models + Endpoints** like any other deployment — same
table, same virtual keys, same cost tracking. The subscription is no longer a separate
thing your clients have to know about.

![The applied models listed in the LiteLLM Models page, with per-token costs](docs/models.png)

Every one is callable straight away:

```bash
curl $PROXY/v1/chat/completions -H "Authorization: Bearer $KEY" \
  -d '{"model":"mysubs/claudecode/claude-opus-5","messages":[{"role":"user","content":"hi"}]}'
```

## How connecting works

1. **Press Connect.** The card opens the provider's login page in your browser.
2. **Authenticate** with the provider as usual.
3. **Return the result.** Paste the URL your browser ends up on — see below.
4. **Discovery runs.** The page probes each candidate model against your account and shows
   what actually answered. Nothing is listed as available unless the upstream replied.
5. **Pick and apply.** The selected models are injected into the LiteLLM Router under the
   `mysubs/<subscription>/` prefix and are immediately callable by any client.

Tokens are then refreshed in the background, with a `flock` held across processes so that
multiple proxy workers never race on the same rotating refresh token.

### Returning the result

These OAuth clients register `http://localhost:54545/callback` (and `:1455`, `:51121`) as
their redirect. `localhost` resolves in the **browser**, so that port would have to be open
on the machine you are browsing from — and the proxy usually runs somewhere else. The
redirect therefore lands on a page that cannot load. That is expected, and the page tells
you so before you start.

**Paste the URL.** Copy whatever is in the address bar after you authenticate — the
`This site can't be reached` one — and paste it into the box on the page. The authorization
code is in it. This is the default path, it needs nothing installed anywhere, and it works
over SSH, from a phone, or on a machine with no browser at all.

<details>
<summary><b>Optional: skip the paste with a local command</b></summary>

If you would rather not copy anything, the page also issues a pairing code for a helper you
run on the machine with the browser:

```bash
pip install litellm-mysubs
mysubs-login anthropic --url https://your-proxy --code XXXX-XXXX-XXXX
```

It opens the loopback port the provider expects, catches the redirect itself, and deposits
the credential in the proxy. The page notices and moves on by itself.

The pairing code lives ten minutes, is single-use, and authorises exactly one provider —
that is what keeps the proxy admin key off your command line.

If LiteLLM runs on your own machine, drop `--url` and `--code`: it writes straight to the
local store.

</details>

## What is guaranteed

- **Inert until a subscription is connected.** The patch is only applied once at least one
  credential exists. Installed with no subscriptions, it is indistinguishable from not being
  installed — the Router is untouched.
- **Your `config.yaml` survives.** One line appended, a backup written next to it, and a
  refusal to save a file that would not parse.
- **The UI does not depend on a patched bundle.** `/mysubs` is a mounted FastAPI sub-app and
  always works by direct URL. The **Experimental** menu entry is a best-effort string patch
  of a pre-compiled Next.js chunk whose filename is a build hash; when a new LiteLLM version
  does not match, it logs the direct URL instead of failing. Nothing in `site-packages` is
  ever rewritten — the modified copy is served from memory.
- **Credentials go where your policy says.** A `0600` file at
  `~/.litellm/mysubs/credentials.json` by default, the secret manager LiteLLM already has
  configured (`general_settings.key_management_system`) if you run one, or read-only
  environment variables. Loose permissions on the file are rejected, not silently fixed.
- **Never a fabricated number.** An unreachable provider shows the error or the last real
  snapshot labelled with its age. A model name the subscription does not serve returns the
  upstream error — it is never silently answered by a different model.

### Turning it off

| | |
|---|---|
| `MYSUBS_DISABLE=1` | disables everything without editing `config.yaml` |
| `MYSUBS_DISABLE_AUTH=1` | skips the `proxy_admin` check (proxies with no key database) |
| remove the `callbacks` line | uninstalls |

## Providers

| Provider | Model prefix | Wire protocol | Quota reported |
|---|---|---|---|
| Claude Max | `mysubs/claudecode/` | Messages | 5h / 7d, from headers + `/api/oauth/usage` |
| ChatGPT Plus (Codex) | `mysubs/codex/` | Responses API | 5h / 7d, from headers + `wham/usage` |
| Google Antigravity | `mysubs/antigravity/` | Cloud Code | `:retrieveUserQuotaSummary` only |

On the wire that means `api.anthropic.com/v1/messages`,
`chatgpt.com/backend-api/codex/responses`, and `v1internal:streamGenerateContent`.
Antigravity is the one case where the quota endpoint is the only source: measured against
the real backend, it returns no rate-limit headers at all.

Anthropic and Codex have no catalog endpoint for subscription tokens, so their model lists
come from a curated set of measured names plus a live probe of each one. Antigravity has a
real catalog (`:fetchAvailableModels`) and it is used directly.

## Development

```bash
pip install -e ".[dev]"
pytest                  # unit tests
ruff check . && mypy    # lint and types
```

1628 tests, 86% branch coverage (the suite fails below 85%), `ruff` and `mypy --strict`
clean. Tests that touch real LiteLLM internals need the proxy extras:

```bash
pip install "litellm[proxy]"
pytest tests/test_litellm_contract.py
```

That file asserts the internal symbols the patch depends on — `Router.acompletion`,
`route_llm_request.route_request`, `custom_provider_map`. CI runs it against both the pinned
`litellm[proxy]` 1.101.0 and the current release, which turns an incompatible upstream
upgrade into a red build instead of a production outage.

CI also runs the unit suite on Python 3.11, 3.12 and 3.13, and verifies 186 source anchors
against the upstream implementations they were ported from, on every push. An anchor pins
both the symbol name and, where it matters, its value — a renamed endpoint path or a bumped
client version fails the build rather than drifting silently.

## Architecture

```
src/litellm_mysubs/
├── credentials/   pluggable store (0600 file, secret manager, env), OAuth,
│                  loopback callback server, cross-process refresh lock
├── wire/          one module per provider; they never reference each other
├── transport/     HTTP client, SSE, host failover, retry
├── catalog/       discovery of the models a subscription actually serves, quota
├── ui/            the `/mysubs` sub-app: cards, pairing, apply
├── login_cli.py   `mysubs-login` — the interceptor, run on your own machine
├── setup_cli.py   `mysubs-setup` — the one-line config edit
├── registry.py    Router injection and guards against phantom deployments
└── plugin.py      the only module that mutates global state
```

Every module imports without side effects. Only `plugin.py` modifies LiteLLM, and only when
invoked — which is what keeps the rest unit-testable.

## Design decisions

[`docs/DECISIONS.md`](docs/DECISIONS.md) holds ten entries. Each records what was decided,
**the measurement that supports it**, and what would reopen the question. Without the
measurement it is not a decision, it is a preference.

For example, D1 explains why streaming stays in a monkey-patch instead of the official
`CustomLLM` path: a minimal handler reporting `prompt_tokens=100, completion_tokens=5,
cached_tokens=80` had 8/2 delivered to the client and `cached_tokens` lost, in every one of
the four supported ways of emitting the final chunk. Without real usage, LiteLLM estimates
with `token_counter` and every cache hit becomes invisible in `/spend/logs` — on a
subscription account that is the difference between 8697 and 2876 prompt tokens for the same
request, and the only way to know why the quota ran out.

## License

MIT — see [LICENSE](LICENSE).
