subrouter 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 (82) hide show
  1. subrouter-0.1.0/.github/workflows/release.yml +173 -0
  2. subrouter-0.1.0/.gitignore +11 -0
  3. subrouter-0.1.0/AGENTS.md +11 -0
  4. subrouter-0.1.0/LICENSE +21 -0
  5. subrouter-0.1.0/Makefile +24 -0
  6. subrouter-0.1.0/PKG-INFO +227 -0
  7. subrouter-0.1.0/README.md +206 -0
  8. subrouter-0.1.0/cmd/mockupstream/main.go +151 -0
  9. subrouter-0.1.0/cmd/subrouter/codex.go +116 -0
  10. subrouter-0.1.0/cmd/subrouter/codex_test.go +115 -0
  11. subrouter-0.1.0/cmd/subrouter/cx.go +1467 -0
  12. subrouter-0.1.0/cmd/subrouter/cx_auto_switch.go +128 -0
  13. subrouter-0.1.0/cmd/subrouter/cx_auto_switch_test.go +112 -0
  14. subrouter-0.1.0/cmd/subrouter/cx_claude.go +430 -0
  15. subrouter-0.1.0/cmd/subrouter/cx_claude_test.go +51 -0
  16. subrouter-0.1.0/cmd/subrouter/cx_command.go +33 -0
  17. subrouter-0.1.0/cmd/subrouter/cx_compat.go +25 -0
  18. subrouter-0.1.0/cmd/subrouter/cx_gemini.go +51 -0
  19. subrouter-0.1.0/cmd/subrouter/cx_server.go +415 -0
  20. subrouter-0.1.0/cmd/subrouter/cx_server_test.go +176 -0
  21. subrouter-0.1.0/cmd/subrouter/cx_test.go +791 -0
  22. subrouter-0.1.0/cmd/subrouter/install_daemon.go +350 -0
  23. subrouter-0.1.0/cmd/subrouter/install_daemon_test.go +88 -0
  24. subrouter-0.1.0/cmd/subrouter/main.go +290 -0
  25. subrouter-0.1.0/cmd/subrouter/main_test.go +59 -0
  26. subrouter-0.1.0/deploy/gcp/README.md +109 -0
  27. subrouter-0.1.0/deploy/gcp/create-subrouter-vm.sh +118 -0
  28. subrouter-0.1.0/deploy/gcp/publish-subrouter.sh +51 -0
  29. subrouter-0.1.0/deploy/gcp/startup.sh +103 -0
  30. subrouter-0.1.0/deploy/gcp/upload-codex-accounts.sh +209 -0
  31. subrouter-0.1.0/docs/codex.md +78 -0
  32. subrouter-0.1.0/docs/design.md +149 -0
  33. subrouter-0.1.0/docs/saturation.md +46 -0
  34. subrouter-0.1.0/go.mod +5 -0
  35. subrouter-0.1.0/go.sum +2 -0
  36. subrouter-0.1.0/internal/accounts/admin_store.go +229 -0
  37. subrouter-0.1.0/internal/accounts/codex_auth.go +412 -0
  38. subrouter-0.1.0/internal/accounts/codex_auth_test.go +238 -0
  39. subrouter-0.1.0/internal/accounts/codex_store.go +291 -0
  40. subrouter-0.1.0/internal/accounts/codex_store_lock_unix.go +38 -0
  41. subrouter-0.1.0/internal/accounts/codex_store_lock_windows.go +28 -0
  42. subrouter-0.1.0/internal/accounts/codex_switch.go +112 -0
  43. subrouter-0.1.0/internal/accounts/codex_switch_test.go +118 -0
  44. subrouter-0.1.0/internal/accounts/codex_usage.go +201 -0
  45. subrouter-0.1.0/internal/accounts/openai_admin.go +476 -0
  46. subrouter-0.1.0/internal/accounts/openai_admin_test.go +100 -0
  47. subrouter-0.1.0/internal/accounts/types.go +36 -0
  48. subrouter-0.1.0/internal/agents/claude/store.go +505 -0
  49. subrouter-0.1.0/internal/agents/claude/store_test.go +68 -0
  50. subrouter-0.1.0/internal/agents/gemini/store.go +92 -0
  51. subrouter-0.1.0/internal/agents/opencode/auth.go +102 -0
  52. subrouter-0.1.0/internal/agents/pi/auth.go +118 -0
  53. subrouter-0.1.0/internal/proxy/dashboard.go +316 -0
  54. subrouter-0.1.0/internal/proxy/dashboard_test.go +83 -0
  55. subrouter-0.1.0/internal/proxy/proxy.go +478 -0
  56. subrouter-0.1.0/internal/proxy/proxy_websocket_test.go +1055 -0
  57. subrouter-0.1.0/internal/selectacct/limits.go +64 -0
  58. subrouter-0.1.0/internal/selectacct/saturation_test.go +193 -0
  59. subrouter-0.1.0/internal/selectacct/scheduler.go +118 -0
  60. subrouter-0.1.0/internal/selectacct/scheduler_ref.go +24 -0
  61. subrouter-0.1.0/internal/selectacct/scheduler_test.go +170 -0
  62. subrouter-0.1.0/internal/session/extract.go +236 -0
  63. subrouter-0.1.0/internal/session/extract_test.go +132 -0
  64. subrouter-0.1.0/internal/session/store.go +154 -0
  65. subrouter-0.1.0/internal/session/store_test.go +103 -0
  66. subrouter-0.1.0/internal/transcript/analytics.go +412 -0
  67. subrouter-0.1.0/internal/transcript/analytics_test.go +67 -0
  68. subrouter-0.1.0/internal/transcript/gcs_sync.go +122 -0
  69. subrouter-0.1.0/internal/transcript/gcs_sync_test.go +55 -0
  70. subrouter-0.1.0/internal/transcript/index.go +234 -0
  71. subrouter-0.1.0/internal/transcript/index_test.go +59 -0
  72. subrouter-0.1.0/internal/transcript/recorder.go +161 -0
  73. subrouter-0.1.0/internal/transcript/recorder_test.go +69 -0
  74. subrouter-0.1.0/npm/bin/cx.js +2 -0
  75. subrouter-0.1.0/npm/bin/runner.js +96 -0
  76. subrouter-0.1.0/npm/bin/sr.js +2 -0
  77. subrouter-0.1.0/npm/bin/subrouter.js +2 -0
  78. subrouter-0.1.0/package.json +25 -0
  79. subrouter-0.1.0/pyproject.toml +35 -0
  80. subrouter-0.1.0/scripts/build-release.sh +50 -0
  81. subrouter-0.1.0/subrouter_cli/__init__.py +110 -0
  82. subrouter-0.1.0/subrouter_cli/__main__.py +3 -0
@@ -0,0 +1,173 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+ workflow_dispatch:
8
+ inputs:
9
+ create_github_release:
10
+ description: "Create or update the GitHub release"
11
+ required: true
12
+ type: boolean
13
+ default: false
14
+ publish_npm:
15
+ description: "Publish to npm through trusted publishing"
16
+ required: true
17
+ type: boolean
18
+ default: false
19
+ publish_pypi:
20
+ description: "Publish to PyPI through trusted publishing"
21
+ required: true
22
+ type: boolean
23
+ default: true
24
+
25
+ permissions:
26
+ contents: read
27
+
28
+ jobs:
29
+ build:
30
+ name: Build and verify artifacts
31
+ runs-on: ubuntu-latest
32
+ outputs:
33
+ version: ${{ steps.version.outputs.version }}
34
+ steps:
35
+ - name: Check out repository
36
+ uses: actions/checkout@v6
37
+
38
+ - name: Set up Go
39
+ uses: actions/setup-go@v6
40
+ with:
41
+ go-version-file: go.mod
42
+
43
+ - name: Set up Node.js
44
+ uses: actions/setup-node@v6
45
+ with:
46
+ node-version: "24"
47
+ registry-url: "https://registry.npmjs.org"
48
+
49
+ - name: Set up Python
50
+ uses: actions/setup-python@v6
51
+ with:
52
+ python-version: "3.12"
53
+
54
+ - name: Determine release version
55
+ id: version
56
+ shell: bash
57
+ run: |
58
+ set -euo pipefail
59
+ if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
60
+ version="${GITHUB_REF_NAME#v}"
61
+ else
62
+ version="$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')"
63
+ fi
64
+
65
+ package_version="$(node -p "require('./package.json').version")"
66
+ python_version="$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')"
67
+
68
+ if [[ "${version}" != "${package_version}" || "${version}" != "${python_version}" ]]; then
69
+ echo "version mismatch: release=${version} package.json=${package_version} pyproject.toml=${python_version}" >&2
70
+ exit 1
71
+ fi
72
+
73
+ echo "version=${version}" >> "${GITHUB_OUTPUT}"
74
+
75
+ - name: Test Go packages
76
+ run: CGO_ENABLED=0 go test ./...
77
+
78
+ - name: Build Go release binaries
79
+ run: scripts/build-release.sh "${{ steps.version.outputs.version }}"
80
+
81
+ - name: Check npm package
82
+ run: npm pack --dry-run
83
+
84
+ - name: Build Python package
85
+ run: |
86
+ python -m pip install --upgrade build twine
87
+ python -m build
88
+ python -m twine check dist/subrouter-*.tar.gz dist/subrouter-*.whl
89
+
90
+ - name: Upload Go release artifacts
91
+ uses: actions/upload-artifact@v5
92
+ with:
93
+ name: release-assets
94
+ path: dist/release/*
95
+ if-no-files-found: error
96
+
97
+ - name: Upload Python distributions
98
+ uses: actions/upload-artifact@v5
99
+ with:
100
+ name: python-dist
101
+ path: |
102
+ dist/subrouter-*.tar.gz
103
+ dist/subrouter-*.whl
104
+ if-no-files-found: error
105
+
106
+ github-release:
107
+ name: Publish GitHub release
108
+ needs: build
109
+ runs-on: ubuntu-latest
110
+ if: ${{ github.event_name == 'push' || github.event.inputs.create_github_release == 'true' }}
111
+ permissions:
112
+ contents: write
113
+ steps:
114
+ - name: Download Go release artifacts
115
+ uses: actions/download-artifact@v5
116
+ with:
117
+ name: release-assets
118
+ path: dist/release
119
+
120
+ - name: Create or update GitHub release
121
+ env:
122
+ GH_TOKEN: ${{ github.token }}
123
+ TAG_NAME: v${{ needs.build.outputs.version }}
124
+ run: |
125
+ set -euo pipefail
126
+ if gh release view "${TAG_NAME}" >/dev/null 2>&1; then
127
+ gh release upload "${TAG_NAME}" dist/release/* --clobber
128
+ else
129
+ gh release create "${TAG_NAME}" dist/release/* \
130
+ --title "Subrouter ${TAG_NAME}" \
131
+ --notes "Subrouter ${TAG_NAME}"
132
+ fi
133
+
134
+ npm-publish:
135
+ name: Publish npm package
136
+ needs: build
137
+ runs-on: ubuntu-latest
138
+ if: ${{ github.event_name == 'push' || github.event.inputs.publish_npm == 'true' }}
139
+ environment: npm
140
+ permissions:
141
+ contents: read
142
+ id-token: write
143
+ steps:
144
+ - name: Check out repository
145
+ uses: actions/checkout@v6
146
+
147
+ - name: Set up Node.js
148
+ uses: actions/setup-node@v6
149
+ with:
150
+ node-version: "24"
151
+ registry-url: "https://registry.npmjs.org"
152
+
153
+ - name: Publish to npm
154
+ run: npm publish --access public
155
+
156
+ pypi-publish:
157
+ name: Publish PyPI package
158
+ needs: build
159
+ runs-on: ubuntu-latest
160
+ if: ${{ github.event_name == 'push' || github.event.inputs.publish_pypi == 'true' }}
161
+ environment: pypi
162
+ permissions:
163
+ contents: read
164
+ id-token: write
165
+ steps:
166
+ - name: Download Python distributions
167
+ uses: actions/download-artifact@v5
168
+ with:
169
+ name: python-dist
170
+ path: dist
171
+
172
+ - name: Publish to PyPI
173
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,11 @@
1
+ .DS_Store
2
+ /subrouter
3
+ bin/
4
+ !npm/bin/
5
+ !npm/bin/*.js
6
+ *.test
7
+ coverage.out
8
+ .venv/
9
+ __pycache__/
10
+ dist/
11
+ *.egg-info/
@@ -0,0 +1,11 @@
1
+ # subrouter
2
+
3
+ Go service for routing AI coding-agent traffic across subscription accounts and API keys.
4
+
5
+ ## Development
6
+
7
+ - Use `go test ./...` before handing off changes.
8
+ - Keep credential handling read-only unless a command explicitly delegates to the upstream account manager, such as `cx`.
9
+ - Do not log access tokens, refresh tokens, API keys, request bodies, or complete Authorization headers.
10
+ - Prefer standard-library networking primitives unless a dependency removes meaningful complexity.
11
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Manaflow
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,24 @@
1
+ export CGO_ENABLED := 0
2
+
3
+ .PHONY: test run build build-linux accounts mock-upstream
4
+
5
+ test:
6
+ go test ./...
7
+
8
+ run:
9
+ go run ./cmd/subrouter serve
10
+
11
+ build:
12
+ CGO_ENABLED=1 go build -ldflags='-linkmode external' -o bin/subrouter ./cmd/subrouter
13
+ CGO_ENABLED=1 go build -ldflags='-linkmode external' -o bin/mockupstream ./cmd/mockupstream
14
+ codesign -s - -f bin/subrouter
15
+ codesign -s - -f bin/mockupstream
16
+
17
+ build-linux:
18
+ GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o bin/subrouter-linux-amd64 ./cmd/subrouter
19
+
20
+ accounts:
21
+ go run ./cmd/subrouter accounts
22
+
23
+ mock-upstream:
24
+ go run ./cmd/mockupstream
@@ -0,0 +1,227 @@
1
+ Metadata-Version: 2.4
2
+ Name: subrouter
3
+ Version: 0.1.0
4
+ Summary: Routes AI coding-agent traffic across subscription accounts and API keys.
5
+ Project-URL: Homepage, https://github.com/manaflow-ai/subrouter
6
+ Project-URL: Repository, https://github.com/manaflow-ai/subrouter
7
+ Project-URL: Issues, https://github.com/manaflow-ai/subrouter/issues
8
+ Author: Manaflow
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: MacOS
14
+ Classifier: Operating System :: Microsoft :: Windows
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Programming Language :: Go
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Topic :: Software Development
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+
22
+ # Subrouter
23
+
24
+ Subrouter is a local AI coding-agent proxy. It routes traffic across Codex accounts with sticky conversation-to-account assignment so cached context stays useful.
25
+
26
+ ## Goals
27
+
28
+ - Run fast on a Mac Mini.
29
+ - Forward requests with normal Go reverse-proxy behavior, including headers and streaming responses.
30
+ - Support subscription accounts first, API keys second.
31
+ - Keep each conversation pinned to one account.
32
+ - Pick a fresh account for a new conversation based on available rate-limit headroom.
33
+ - Provide the Codex account manager and daemon in one Go binary.
34
+
35
+ ## Current shape
36
+
37
+ ```bash
38
+ make accounts
39
+ make run
40
+ ```
41
+
42
+ This repo sets `CGO_ENABLED=0` in `Makefile` because the local macOS Go 1.22 toolchain is currently producing cgo test binaries that fail before startup with `missing LC_UUID load command`.
43
+
44
+ ## Install
45
+
46
+ Install with npm:
47
+
48
+ ```bash
49
+ npm install -g subrouter
50
+ ```
51
+
52
+ Install with Python:
53
+
54
+ ```bash
55
+ pipx install subrouter
56
+ ```
57
+
58
+ Both packages install `subrouter`, `sr`, and `cx`. The wrappers download the matching Go release binary for macOS, Linux, Windows, FreeBSD, OpenBSD, or NetBSD on amd64, arm64, or supported 32-bit variants. Set `SUBROUTER_BIN` to use a local binary instead.
59
+
60
+ ## Local macOS daemon
61
+
62
+ On macOS, install Subrouter as a localhost-only LaunchAgent:
63
+
64
+ ```bash
65
+ make build
66
+ ./bin/subrouter install-daemon
67
+ ```
68
+
69
+ This installs the binary to `~/bin/subrouter`, installs `~/bin/cx` as a symlink to the same Go binary, writes `~/Library/LaunchAgents/ai.manaflow.subrouter.plist`, creates `~/.subrouter/transcripts`, starts the service, and runs:
70
+
71
+ ```bash
72
+ ~/bin/subrouter serve --addr 127.0.0.1:31415 --transcripts ~/.subrouter/transcripts --cx-switch-interval 10m
73
+ ```
74
+
75
+ The 10 minute `cx` auto-switch interval is the default. Override it with `subrouter install-daemon --cx-switch-interval 5m`, or disable it with `--cx-switch-interval 0`. This command is macOS-specific; use a systemd unit or another supervisor on Linux.
76
+
77
+ Useful endpoints:
78
+
79
+ ```text
80
+ GET /_subrouter/health
81
+ GET /_subrouter/accounts
82
+ GET /_subrouter/sessions
83
+ GET /_subrouter/dashboard
84
+ GET /_subrouter/transcripts
85
+ ```
86
+
87
+ ## GCP deployment
88
+
89
+ See [deploy/gcp/README.md](deploy/gcp/README.md) for the small GCP + Tailscale Subrouter deployment flow.
90
+
91
+ To persist raw Subrouter transcripts, pass a transcript directory:
92
+
93
+ ```bash
94
+ subrouter serve --transcripts ~/.subrouter/transcripts
95
+ ```
96
+
97
+ Transcripts are JSONL files keyed by agent type and session id under `by-agent/<agent-type>/by-session/<agent-session-id>.jsonl`. They include Subrouter metadata, redacted headers, HTTP bodies, SSE bodies, and WebSocket message payloads as base64 with byte counts and SHA-256 hashes. Each event includes `agent_type` and `agent_session_id`; Codex events also include `codex_session_id` for matching `~/.codex/sessions` JSONL files. This is intentionally storage-heavy and can contain sensitive request/response payloads. Authorization-style headers are redacted, but bodies are stored in full.
98
+
99
+ When transcript recording is enabled, `/_subrouter/dashboard` serves an internal HTML dashboard over the same Subrouter listener. It shows token usage over time, usage by user email, usage by selected account, session assignments, transcript summaries, and links to sanitized transcript event JSON under `/_subrouter/transcripts/<agent-type>/<session-id>`. Raw internal trajectory JSON with decoded body text is available under `/_subrouter/transcripts/<agent-type>/<session-id>/raw`.
100
+
101
+ To mirror transcripts to GCS without blocking proxy requests, also pass a `gs://` destination:
102
+
103
+ ```bash
104
+ subrouter serve --transcripts ~/.subrouter/transcripts --transcript-gcs-uri gs://bucket/prefix
105
+ ```
106
+
107
+ The daemon shells out to `gsutil -m rsync -r` on a background interval. Local transcript writes stay on the request path; GCS upload failures are logged and retried later.
108
+
109
+ For best cache behavior, clients should send a stable header per conversation:
110
+
111
+ ```text
112
+ X-Subrouter-Session: <conversation-or-thread-id>
113
+ ```
114
+
115
+ If that header is missing, Subrouter checks Codex headers such as `x-codex-window-id` and `x-codex-turn-state`, common session headers, query params, and small JSON bodies for `session_id`, `conversation_id`, or `thread_id`.
116
+
117
+ Subrouter scopes sticky assignments and transcript files by agent type. It infers `codex`, `claude`, or `gemini` from provider session headers, and clients can set an explicit namespace:
118
+
119
+ ```text
120
+ X-Subrouter-Agent: codex
121
+ ```
122
+
123
+ For teammate-level graphs, clients can also send a self-reported user header:
124
+
125
+ ```text
126
+ X-Subrouter-User-Email: alice@example.com
127
+ ```
128
+
129
+ Subrouter stores the normalized email on the session assignment, includes it in proxy logs as `user`, and exposes it in `GET /_subrouter/sessions`. This is observability metadata, not authentication. To force a selected account, send `X-Subrouter-Account-ID`; API-key labels can omit the `apikey:` prefix. Subrouter strips `X-Subrouter-Session`, `X-Subrouter-Agent`, `X-Subrouter-User-Email`, `X-Subrouter-User`, `X-User-Email`, `X-Subrouter-Account-ID`, and `X-Subrouter-Account` before forwarding upstream.
130
+
131
+ ## Codex CLI
132
+
133
+ `subrouter codex` is a direct Codex wrapper. Use it anywhere you would use `codex`:
134
+
135
+ ```bash
136
+ subrouter codex
137
+ subrouter codex exec "your prompt"
138
+ subrouter codex --version
139
+ ```
140
+
141
+ The wrapper injects this config override into the child Codex process:
142
+
143
+ ```toml
144
+ openai_base_url = "http://127.0.0.1:31415/v1"
145
+ ```
146
+
147
+ It does not edit Codex config or set auth environment variables. Do not set a dummy `OPENAI_API_KEY` for normal subscription routing. Leave Codex logged in the same way it already is. If Codex is in ChatGPT auth mode, `/model` keeps the subscription model picker. Subrouter replaces outbound credentials with the selected `cx` account before forwarding.
148
+
149
+ Override the subrouter URL with `SUBROUTER_CODEX_BASE_URL` if needed. See [docs/codex.md](docs/codex.md) for details and the custom-provider fallback.
150
+
151
+ Set `SUBROUTER_CODEX_USER_EMAIL` to attribute Codex traffic to a teammate:
152
+
153
+ ```bash
154
+ SUBROUTER_CODEX_USER_EMAIL=alice@example.com subrouter codex exec "your prompt"
155
+ ```
156
+
157
+ Force a specific Subrouter account, including an API-key account, with `SUBROUTER_CODEX_ACCOUNT_ID`:
158
+
159
+ ```bash
160
+ SUBROUTER_CODEX_ACCOUNT_ID=team-codex-1 subrouter codex exec "your prompt"
161
+ SUBROUTER_CODEX_ACCOUNT_ID=apikey:team-codex-1 subrouter codex exec "your prompt"
162
+ ```
163
+
164
+ When either variable is set, the wrapper uses a custom `subrouter` provider with WebSockets enabled so Codex can send `X-Subrouter-User-Email` and `X-Subrouter-Account-ID`. Subrouter still replaces outbound credentials before forwarding upstream. `SUBROUTER_CODEX_USER_EMAIL` is only teammate observability metadata; account selection belongs in `SUBROUTER_CODEX_ACCOUNT_ID`.
165
+
166
+ ## Codex accounts
167
+
168
+ Subrouter has a native Go implementation of the Codex account manager. It reads and writes the existing `cx` store format:
169
+
170
+ ```text
171
+ ~/.codex-accounts/accounts/*.json
172
+ ```
173
+
174
+ Account-management commands are built into the `subrouter` binary:
175
+
176
+ ```bash
177
+ go run ./cmd/subrouter cx add
178
+ go run ./cmd/subrouter cx import
179
+ go run ./cmd/subrouter cx list
180
+ go run ./cmd/subrouter cx status
181
+
182
+ # direct aliases also work
183
+ go run ./cmd/subrouter status
184
+ sr status
185
+ ```
186
+
187
+ The supported Codex commands include `add`, `add-key`, `import`, `list`, `switch`, `gui-switch`, `remove`, `status`, `usage`, `add-admin-key`, `admin-keys`, `remove-admin-key`, and `attach-project`.
188
+
189
+ `cx switch` also syncs compatible ChatGPT Codex credentials into:
190
+
191
+ ```text
192
+ ~/.codex/auth.json
193
+ ~/.local/share/opencode/auth.json # provider key: openai
194
+ ~/.pi/agent/auth.json # provider key: openai-codex
195
+ ```
196
+
197
+ OpenCode uses XDG data home, so `XDG_DATA_HOME` changes its auth path. pi uses `PI_CODING_AGENT_DIR` when set. Existing unrelated provider credentials in those files are preserved.
198
+
199
+ Claude profiles are also native Go and use the existing `~/.codex-accounts/claude.json` format:
200
+
201
+ ```bash
202
+ cx claude list
203
+ cx claude switch <profile>
204
+ cx claude env
205
+ cx claude run <profile>
206
+ ```
207
+
208
+ Gemini has its own `cx gemini` namespace and store scaffold so future routing cannot collide with Codex or Claude state.
209
+
210
+ ## Selection policy
211
+
212
+ On startup, Subrouter fetches current Codex usage for OAuth accounts and scores each account by its most constrained usage window. The scheduler keeps existing sessions sticky. For a new session it protects low-headroom accounts, spends healthy quota that resets soonest, then breaks ties by live assigned-session counts.
213
+ If all else ties, subscription OAuth accounts are preferred before API-key accounts.
214
+
215
+ The daemon also refreshes usage and updates Codex, OpenCode, and pi auth every 10 minutes by default so local agents follow the same OAuth-only policy. Configure it with `subrouter serve --cx-switch-interval 5m`, or disable it with `--cx-switch-interval 0`. If `--fetch-usage=false`, auto-switch is disabled because fresh usage is required.
216
+
217
+ By default, OAuth accounts are forwarded to `https://chatgpt.com/backend-api/codex` and API-key accounts are forwarded to `https://api.openai.com`. Subrouter accepts either `/v1/responses` or `/responses` from clients and normalizes the path for the selected account type.
218
+
219
+ Live headroom comes from Codex subscription usage. API-key spend comes from the OpenAI organization usage endpoints through stored `sk-admin-*` keys. Claude profile usage comes from the Anthropic OAuth usage endpoint when profile credentials are readable.
220
+
221
+ See [docs/saturation.md](docs/saturation.md) for the 5h/7d placement strategy and simulation tests.
222
+
223
+ ## Security defaults
224
+
225
+ - Bind to `127.0.0.1` unless explicitly exposed.
226
+ - Do not log tokens, refresh tokens, API keys, request bodies, or full Authorization headers.
227
+ - Keep `~/.codex-accounts` credentials as the canonical local store.
@@ -0,0 +1,206 @@
1
+ # Subrouter
2
+
3
+ Subrouter is a local AI coding-agent proxy. It routes traffic across Codex accounts with sticky conversation-to-account assignment so cached context stays useful.
4
+
5
+ ## Goals
6
+
7
+ - Run fast on a Mac Mini.
8
+ - Forward requests with normal Go reverse-proxy behavior, including headers and streaming responses.
9
+ - Support subscription accounts first, API keys second.
10
+ - Keep each conversation pinned to one account.
11
+ - Pick a fresh account for a new conversation based on available rate-limit headroom.
12
+ - Provide the Codex account manager and daemon in one Go binary.
13
+
14
+ ## Current shape
15
+
16
+ ```bash
17
+ make accounts
18
+ make run
19
+ ```
20
+
21
+ This repo sets `CGO_ENABLED=0` in `Makefile` because the local macOS Go 1.22 toolchain is currently producing cgo test binaries that fail before startup with `missing LC_UUID load command`.
22
+
23
+ ## Install
24
+
25
+ Install with npm:
26
+
27
+ ```bash
28
+ npm install -g subrouter
29
+ ```
30
+
31
+ Install with Python:
32
+
33
+ ```bash
34
+ pipx install subrouter
35
+ ```
36
+
37
+ Both packages install `subrouter`, `sr`, and `cx`. The wrappers download the matching Go release binary for macOS, Linux, Windows, FreeBSD, OpenBSD, or NetBSD on amd64, arm64, or supported 32-bit variants. Set `SUBROUTER_BIN` to use a local binary instead.
38
+
39
+ ## Local macOS daemon
40
+
41
+ On macOS, install Subrouter as a localhost-only LaunchAgent:
42
+
43
+ ```bash
44
+ make build
45
+ ./bin/subrouter install-daemon
46
+ ```
47
+
48
+ This installs the binary to `~/bin/subrouter`, installs `~/bin/cx` as a symlink to the same Go binary, writes `~/Library/LaunchAgents/ai.manaflow.subrouter.plist`, creates `~/.subrouter/transcripts`, starts the service, and runs:
49
+
50
+ ```bash
51
+ ~/bin/subrouter serve --addr 127.0.0.1:31415 --transcripts ~/.subrouter/transcripts --cx-switch-interval 10m
52
+ ```
53
+
54
+ The 10 minute `cx` auto-switch interval is the default. Override it with `subrouter install-daemon --cx-switch-interval 5m`, or disable it with `--cx-switch-interval 0`. This command is macOS-specific; use a systemd unit or another supervisor on Linux.
55
+
56
+ Useful endpoints:
57
+
58
+ ```text
59
+ GET /_subrouter/health
60
+ GET /_subrouter/accounts
61
+ GET /_subrouter/sessions
62
+ GET /_subrouter/dashboard
63
+ GET /_subrouter/transcripts
64
+ ```
65
+
66
+ ## GCP deployment
67
+
68
+ See [deploy/gcp/README.md](deploy/gcp/README.md) for the small GCP + Tailscale Subrouter deployment flow.
69
+
70
+ To persist raw Subrouter transcripts, pass a transcript directory:
71
+
72
+ ```bash
73
+ subrouter serve --transcripts ~/.subrouter/transcripts
74
+ ```
75
+
76
+ Transcripts are JSONL files keyed by agent type and session id under `by-agent/<agent-type>/by-session/<agent-session-id>.jsonl`. They include Subrouter metadata, redacted headers, HTTP bodies, SSE bodies, and WebSocket message payloads as base64 with byte counts and SHA-256 hashes. Each event includes `agent_type` and `agent_session_id`; Codex events also include `codex_session_id` for matching `~/.codex/sessions` JSONL files. This is intentionally storage-heavy and can contain sensitive request/response payloads. Authorization-style headers are redacted, but bodies are stored in full.
77
+
78
+ When transcript recording is enabled, `/_subrouter/dashboard` serves an internal HTML dashboard over the same Subrouter listener. It shows token usage over time, usage by user email, usage by selected account, session assignments, transcript summaries, and links to sanitized transcript event JSON under `/_subrouter/transcripts/<agent-type>/<session-id>`. Raw internal trajectory JSON with decoded body text is available under `/_subrouter/transcripts/<agent-type>/<session-id>/raw`.
79
+
80
+ To mirror transcripts to GCS without blocking proxy requests, also pass a `gs://` destination:
81
+
82
+ ```bash
83
+ subrouter serve --transcripts ~/.subrouter/transcripts --transcript-gcs-uri gs://bucket/prefix
84
+ ```
85
+
86
+ The daemon shells out to `gsutil -m rsync -r` on a background interval. Local transcript writes stay on the request path; GCS upload failures are logged and retried later.
87
+
88
+ For best cache behavior, clients should send a stable header per conversation:
89
+
90
+ ```text
91
+ X-Subrouter-Session: <conversation-or-thread-id>
92
+ ```
93
+
94
+ If that header is missing, Subrouter checks Codex headers such as `x-codex-window-id` and `x-codex-turn-state`, common session headers, query params, and small JSON bodies for `session_id`, `conversation_id`, or `thread_id`.
95
+
96
+ Subrouter scopes sticky assignments and transcript files by agent type. It infers `codex`, `claude`, or `gemini` from provider session headers, and clients can set an explicit namespace:
97
+
98
+ ```text
99
+ X-Subrouter-Agent: codex
100
+ ```
101
+
102
+ For teammate-level graphs, clients can also send a self-reported user header:
103
+
104
+ ```text
105
+ X-Subrouter-User-Email: alice@example.com
106
+ ```
107
+
108
+ Subrouter stores the normalized email on the session assignment, includes it in proxy logs as `user`, and exposes it in `GET /_subrouter/sessions`. This is observability metadata, not authentication. To force a selected account, send `X-Subrouter-Account-ID`; API-key labels can omit the `apikey:` prefix. Subrouter strips `X-Subrouter-Session`, `X-Subrouter-Agent`, `X-Subrouter-User-Email`, `X-Subrouter-User`, `X-User-Email`, `X-Subrouter-Account-ID`, and `X-Subrouter-Account` before forwarding upstream.
109
+
110
+ ## Codex CLI
111
+
112
+ `subrouter codex` is a direct Codex wrapper. Use it anywhere you would use `codex`:
113
+
114
+ ```bash
115
+ subrouter codex
116
+ subrouter codex exec "your prompt"
117
+ subrouter codex --version
118
+ ```
119
+
120
+ The wrapper injects this config override into the child Codex process:
121
+
122
+ ```toml
123
+ openai_base_url = "http://127.0.0.1:31415/v1"
124
+ ```
125
+
126
+ It does not edit Codex config or set auth environment variables. Do not set a dummy `OPENAI_API_KEY` for normal subscription routing. Leave Codex logged in the same way it already is. If Codex is in ChatGPT auth mode, `/model` keeps the subscription model picker. Subrouter replaces outbound credentials with the selected `cx` account before forwarding.
127
+
128
+ Override the subrouter URL with `SUBROUTER_CODEX_BASE_URL` if needed. See [docs/codex.md](docs/codex.md) for details and the custom-provider fallback.
129
+
130
+ Set `SUBROUTER_CODEX_USER_EMAIL` to attribute Codex traffic to a teammate:
131
+
132
+ ```bash
133
+ SUBROUTER_CODEX_USER_EMAIL=alice@example.com subrouter codex exec "your prompt"
134
+ ```
135
+
136
+ Force a specific Subrouter account, including an API-key account, with `SUBROUTER_CODEX_ACCOUNT_ID`:
137
+
138
+ ```bash
139
+ SUBROUTER_CODEX_ACCOUNT_ID=team-codex-1 subrouter codex exec "your prompt"
140
+ SUBROUTER_CODEX_ACCOUNT_ID=apikey:team-codex-1 subrouter codex exec "your prompt"
141
+ ```
142
+
143
+ When either variable is set, the wrapper uses a custom `subrouter` provider with WebSockets enabled so Codex can send `X-Subrouter-User-Email` and `X-Subrouter-Account-ID`. Subrouter still replaces outbound credentials before forwarding upstream. `SUBROUTER_CODEX_USER_EMAIL` is only teammate observability metadata; account selection belongs in `SUBROUTER_CODEX_ACCOUNT_ID`.
144
+
145
+ ## Codex accounts
146
+
147
+ Subrouter has a native Go implementation of the Codex account manager. It reads and writes the existing `cx` store format:
148
+
149
+ ```text
150
+ ~/.codex-accounts/accounts/*.json
151
+ ```
152
+
153
+ Account-management commands are built into the `subrouter` binary:
154
+
155
+ ```bash
156
+ go run ./cmd/subrouter cx add
157
+ go run ./cmd/subrouter cx import
158
+ go run ./cmd/subrouter cx list
159
+ go run ./cmd/subrouter cx status
160
+
161
+ # direct aliases also work
162
+ go run ./cmd/subrouter status
163
+ sr status
164
+ ```
165
+
166
+ The supported Codex commands include `add`, `add-key`, `import`, `list`, `switch`, `gui-switch`, `remove`, `status`, `usage`, `add-admin-key`, `admin-keys`, `remove-admin-key`, and `attach-project`.
167
+
168
+ `cx switch` also syncs compatible ChatGPT Codex credentials into:
169
+
170
+ ```text
171
+ ~/.codex/auth.json
172
+ ~/.local/share/opencode/auth.json # provider key: openai
173
+ ~/.pi/agent/auth.json # provider key: openai-codex
174
+ ```
175
+
176
+ OpenCode uses XDG data home, so `XDG_DATA_HOME` changes its auth path. pi uses `PI_CODING_AGENT_DIR` when set. Existing unrelated provider credentials in those files are preserved.
177
+
178
+ Claude profiles are also native Go and use the existing `~/.codex-accounts/claude.json` format:
179
+
180
+ ```bash
181
+ cx claude list
182
+ cx claude switch <profile>
183
+ cx claude env
184
+ cx claude run <profile>
185
+ ```
186
+
187
+ Gemini has its own `cx gemini` namespace and store scaffold so future routing cannot collide with Codex or Claude state.
188
+
189
+ ## Selection policy
190
+
191
+ On startup, Subrouter fetches current Codex usage for OAuth accounts and scores each account by its most constrained usage window. The scheduler keeps existing sessions sticky. For a new session it protects low-headroom accounts, spends healthy quota that resets soonest, then breaks ties by live assigned-session counts.
192
+ If all else ties, subscription OAuth accounts are preferred before API-key accounts.
193
+
194
+ The daemon also refreshes usage and updates Codex, OpenCode, and pi auth every 10 minutes by default so local agents follow the same OAuth-only policy. Configure it with `subrouter serve --cx-switch-interval 5m`, or disable it with `--cx-switch-interval 0`. If `--fetch-usage=false`, auto-switch is disabled because fresh usage is required.
195
+
196
+ By default, OAuth accounts are forwarded to `https://chatgpt.com/backend-api/codex` and API-key accounts are forwarded to `https://api.openai.com`. Subrouter accepts either `/v1/responses` or `/responses` from clients and normalizes the path for the selected account type.
197
+
198
+ Live headroom comes from Codex subscription usage. API-key spend comes from the OpenAI organization usage endpoints through stored `sk-admin-*` keys. Claude profile usage comes from the Anthropic OAuth usage endpoint when profile credentials are readable.
199
+
200
+ See [docs/saturation.md](docs/saturation.md) for the 5h/7d placement strategy and simulation tests.
201
+
202
+ ## Security defaults
203
+
204
+ - Bind to `127.0.0.1` unless explicitly exposed.
205
+ - Do not log tokens, refresh tokens, API keys, request bodies, or full Authorization headers.
206
+ - Keep `~/.codex-accounts` credentials as the canonical local store.