janus-ai 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 (163) hide show
  1. janus_ai-0.1.0/.dockerignore +10 -0
  2. janus_ai-0.1.0/.env.example +6 -0
  3. janus_ai-0.1.0/.github/workflows/ci.yml +32 -0
  4. janus_ai-0.1.0/.github/workflows/docs.yml +28 -0
  5. janus_ai-0.1.0/.github/workflows/publish.yml +29 -0
  6. janus_ai-0.1.0/.gitignore +83 -0
  7. janus_ai-0.1.0/.python-version +1 -0
  8. janus_ai-0.1.0/AGENTS.md +152 -0
  9. janus_ai-0.1.0/CHANGELOG.md +27 -0
  10. janus_ai-0.1.0/CONTRIBUTING.md +86 -0
  11. janus_ai-0.1.0/Dockerfile +14 -0
  12. janus_ai-0.1.0/LICENSE +674 -0
  13. janus_ai-0.1.0/PKG-INFO +37 -0
  14. janus_ai-0.1.0/README.md +159 -0
  15. janus_ai-0.1.0/docker-compose.yml +14 -0
  16. janus_ai-0.1.0/docs/api-reference.md +294 -0
  17. janus_ai-0.1.0/docs/architecture.md +237 -0
  18. janus_ai-0.1.0/docs/budgets.md +118 -0
  19. janus_ai-0.1.0/docs/cli.md +289 -0
  20. janus_ai-0.1.0/docs/client-setup.md +70 -0
  21. janus_ai-0.1.0/docs/combos.md +130 -0
  22. janus_ai-0.1.0/docs/configuration.md +268 -0
  23. janus_ai-0.1.0/docs/contributing.md +86 -0
  24. janus_ai-0.1.0/docs/dashboard.md +117 -0
  25. janus_ai-0.1.0/docs/getting-started.md +125 -0
  26. janus_ai-0.1.0/docs/index.md +69 -0
  27. janus_ai-0.1.0/docs/providers.md +262 -0
  28. janus_ai-0.1.0/docs/superpowers/plans/2026-06-24-janus-phase1-core-router.md +3118 -0
  29. janus_ai-0.1.0/docs/superpowers/plans/2026-06-24-janus-phase2-fallback-combos.md +894 -0
  30. janus_ai-0.1.0/docs/superpowers/plans/2026-06-24-janus-phase3-token-savers.md +652 -0
  31. janus_ai-0.1.0/docs/superpowers/plans/2026-06-24-janus-phase4-sqlite-persistence.md +601 -0
  32. janus_ai-0.1.0/docs/superpowers/plans/2026-06-24-janus-phase5-dashboard.md +560 -0
  33. janus_ai-0.1.0/docs/superpowers/plans/2026-06-25-docs-packaging.md +1150 -0
  34. janus_ai-0.1.0/docs/superpowers/plans/2026-06-25-phase6-quota-usage-analytics.md +3180 -0
  35. janus_ai-0.1.0/docs/superpowers/plans/2026-06-25-phase7-deployment.md +837 -0
  36. janus_ai-0.1.0/docs/superpowers/specs/2026-06-24-janus-phase1-core-router-design.md +355 -0
  37. janus_ai-0.1.0/docs/superpowers/specs/2026-06-24-janus-phase2-fallback-combos-design.md +160 -0
  38. janus_ai-0.1.0/docs/superpowers/specs/2026-06-24-janus-phase3-token-savers-design.md +124 -0
  39. janus_ai-0.1.0/docs/superpowers/specs/2026-06-24-janus-phase4-sqlite-persistence-design.md +67 -0
  40. janus_ai-0.1.0/docs/superpowers/specs/2026-06-24-janus-phase5-dashboard-design.md +66 -0
  41. janus_ai-0.1.0/docs/superpowers/specs/2026-06-25-docs-packaging-design.md +173 -0
  42. janus_ai-0.1.0/docs/superpowers/specs/2026-06-25-phase6-quota-usage-analytics-design.md +419 -0
  43. janus_ai-0.1.0/docs/superpowers/specs/2026-06-25-phase7-deployment-design.md +184 -0
  44. janus_ai-0.1.0/docs/token-savers.md +108 -0
  45. janus_ai-0.1.0/mkdocs.yml +60 -0
  46. janus_ai-0.1.0/pyproject.toml +71 -0
  47. janus_ai-0.1.0/src/janus/__init__.py +0 -0
  48. janus_ai-0.1.0/src/janus/__main__.py +4 -0
  49. janus_ai-0.1.0/src/janus/api/__init__.py +0 -0
  50. janus_ai-0.1.0/src/janus/api/deps.py +21 -0
  51. janus_ai-0.1.0/src/janus/api/routes.py +225 -0
  52. janus_ai-0.1.0/src/janus/app.py +85 -0
  53. janus_ai-0.1.0/src/janus/canonical/__init__.py +0 -0
  54. janus_ai-0.1.0/src/janus/canonical/events.py +64 -0
  55. janus_ai-0.1.0/src/janus/canonical/models.py +120 -0
  56. janus_ai-0.1.0/src/janus/cli.py +346 -0
  57. janus_ai-0.1.0/src/janus/config/__init__.py +0 -0
  58. janus_ai-0.1.0/src/janus/config/loader.py +37 -0
  59. janus_ai-0.1.0/src/janus/config/schema.py +46 -0
  60. janus_ai-0.1.0/src/janus/dashboard/__init__.py +0 -0
  61. janus_ai-0.1.0/src/janus/dashboard/routes.py +249 -0
  62. janus_ai-0.1.0/src/janus/dashboard/templates/analytics.html +151 -0
  63. janus_ai-0.1.0/src/janus/dashboard/templates/base.html +67 -0
  64. janus_ai-0.1.0/src/janus/dashboard/templates/budgets.html +41 -0
  65. janus_ai-0.1.0/src/janus/dashboard/templates/budgets_partial.html +61 -0
  66. janus_ai-0.1.0/src/janus/dashboard/templates/combos.html +29 -0
  67. janus_ai-0.1.0/src/janus/dashboard/templates/keys.html +30 -0
  68. janus_ai-0.1.0/src/janus/dashboard/templates/keys_partial.html +61 -0
  69. janus_ai-0.1.0/src/janus/dashboard/templates/overview.html +75 -0
  70. janus_ai-0.1.0/src/janus/dashboard/templates/providers.html +46 -0
  71. janus_ai-0.1.0/src/janus/dashboard/templates/usage.html +58 -0
  72. janus_ai-0.1.0/src/janus/formats/__init__.py +0 -0
  73. janus_ai-0.1.0/src/janus/formats/anthropic.py +456 -0
  74. janus_ai-0.1.0/src/janus/formats/base.py +30 -0
  75. janus_ai-0.1.0/src/janus/formats/gemini.py +413 -0
  76. janus_ai-0.1.0/src/janus/formats/openai.py +485 -0
  77. janus_ai-0.1.0/src/janus/pricing/__init__.py +0 -0
  78. janus_ai-0.1.0/src/janus/pricing/builtin.py +39 -0
  79. janus_ai-0.1.0/src/janus/pricing/calculator.py +17 -0
  80. janus_ai-0.1.0/src/janus/pricing/models.py +11 -0
  81. janus_ai-0.1.0/src/janus/pricing/registry.py +24 -0
  82. janus_ai-0.1.0/src/janus/providers/__init__.py +0 -0
  83. janus_ai-0.1.0/src/janus/providers/anthropic.py +51 -0
  84. janus_ai-0.1.0/src/janus/providers/base.py +20 -0
  85. janus_ai-0.1.0/src/janus/providers/gemini.py +56 -0
  86. janus_ai-0.1.0/src/janus/providers/openai_compat.py +50 -0
  87. janus_ai-0.1.0/src/janus/providers/opencode_free.py +10 -0
  88. janus_ai-0.1.0/src/janus/providers/registry.py +60 -0
  89. janus_ai-0.1.0/src/janus/routing/__init__.py +0 -0
  90. janus_ai-0.1.0/src/janus/routing/errors.py +34 -0
  91. janus_ai-0.1.0/src/janus/routing/fallback.py +59 -0
  92. janus_ai-0.1.0/src/janus/routing/resolver.py +7 -0
  93. janus_ai-0.1.0/src/janus/settings.py +14 -0
  94. janus_ai-0.1.0/src/janus/storage/__init__.py +0 -0
  95. janus_ai-0.1.0/src/janus/storage/analytics.py +99 -0
  96. janus_ai-0.1.0/src/janus/storage/api_keys.py +57 -0
  97. janus_ai-0.1.0/src/janus/storage/budgets.py +111 -0
  98. janus_ai-0.1.0/src/janus/storage/database.py +78 -0
  99. janus_ai-0.1.0/src/janus/storage/usage.py +76 -0
  100. janus_ai-0.1.0/src/janus/streaming/__init__.py +0 -0
  101. janus_ai-0.1.0/src/janus/streaming/sse.py +33 -0
  102. janus_ai-0.1.0/src/janus/streaming/translator.py +32 -0
  103. janus_ai-0.1.0/src/janus/tokensavers/__init__.py +0 -0
  104. janus_ai-0.1.0/src/janus/tokensavers/base.py +9 -0
  105. janus_ai-0.1.0/src/janus/tokensavers/caveman.py +15 -0
  106. janus_ai-0.1.0/src/janus/tokensavers/pipeline.py +22 -0
  107. janus_ai-0.1.0/src/janus/tokensavers/ponytail.py +34 -0
  108. janus_ai-0.1.0/src/janus/tokensavers/rtk.py +103 -0
  109. janus_ai-0.1.0/tests/__init__.py +0 -0
  110. janus_ai-0.1.0/tests/conftest.py +0 -0
  111. janus_ai-0.1.0/tests/fixtures/.gitkeep +0 -0
  112. janus_ai-0.1.0/tests/fixtures/__init__.py +0 -0
  113. janus_ai-0.1.0/tests/fixtures/anthropic_message_request.json +14 -0
  114. janus_ai-0.1.0/tests/fixtures/anthropic_stream.txt +17 -0
  115. janus_ai-0.1.0/tests/fixtures/gemini_request.json +7 -0
  116. janus_ai-0.1.0/tests/fixtures/openai_chat_request.json +9 -0
  117. janus_ai-0.1.0/tests/fixtures/openai_nonstream_response.json +1 -0
  118. janus_ai-0.1.0/tests/fixtures/openai_stream.txt +9 -0
  119. janus_ai-0.1.0/tests/fixtures/usage_seed.py +36 -0
  120. janus_ai-0.1.0/tests/integration/__init__.py +0 -0
  121. janus_ai-0.1.0/tests/integration/test_api.py +463 -0
  122. janus_ai-0.1.0/tests/integration/test_budget_enforcement.py +105 -0
  123. janus_ai-0.1.0/tests/integration/test_cost_recording.py +67 -0
  124. janus_ai-0.1.0/tests/integration/test_dashboard.py +81 -0
  125. janus_ai-0.1.0/tests/unit/__init__.py +0 -0
  126. janus_ai-0.1.0/tests/unit/canonical/__init__.py +0 -0
  127. janus_ai-0.1.0/tests/unit/canonical/test_events.py +59 -0
  128. janus_ai-0.1.0/tests/unit/canonical/test_models.py +71 -0
  129. janus_ai-0.1.0/tests/unit/config/__init__.py +0 -0
  130. janus_ai-0.1.0/tests/unit/config/test_loader.py +85 -0
  131. janus_ai-0.1.0/tests/unit/config/test_pricing_config.py +21 -0
  132. janus_ai-0.1.0/tests/unit/config/test_schema.py +61 -0
  133. janus_ai-0.1.0/tests/unit/formats/__init__.py +0 -0
  134. janus_ai-0.1.0/tests/unit/formats/test_anthropic.py +80 -0
  135. janus_ai-0.1.0/tests/unit/formats/test_base.py +6 -0
  136. janus_ai-0.1.0/tests/unit/formats/test_gemini.py +65 -0
  137. janus_ai-0.1.0/tests/unit/formats/test_openai.py +151 -0
  138. janus_ai-0.1.0/tests/unit/pricing/__init__.py +0 -0
  139. janus_ai-0.1.0/tests/unit/pricing/test_builtin.py +27 -0
  140. janus_ai-0.1.0/tests/unit/pricing/test_calculator.py +68 -0
  141. janus_ai-0.1.0/tests/unit/pricing/test_registry.py +77 -0
  142. janus_ai-0.1.0/tests/unit/providers/__init__.py +0 -0
  143. janus_ai-0.1.0/tests/unit/providers/test_providers.py +91 -0
  144. janus_ai-0.1.0/tests/unit/providers/test_registry.py +72 -0
  145. janus_ai-0.1.0/tests/unit/routing/__init__.py +0 -0
  146. janus_ai-0.1.0/tests/unit/routing/test_errors.py +44 -0
  147. janus_ai-0.1.0/tests/unit/routing/test_resolver.py +169 -0
  148. janus_ai-0.1.0/tests/unit/storage/__init__.py +0 -0
  149. janus_ai-0.1.0/tests/unit/storage/test_analytics.py +153 -0
  150. janus_ai-0.1.0/tests/unit/storage/test_api_keys.py +79 -0
  151. janus_ai-0.1.0/tests/unit/storage/test_budgets.py +121 -0
  152. janus_ai-0.1.0/tests/unit/storage/test_database.py +28 -0
  153. janus_ai-0.1.0/tests/unit/storage/test_migration.py +47 -0
  154. janus_ai-0.1.0/tests/unit/storage/test_usage.py +128 -0
  155. janus_ai-0.1.0/tests/unit/streaming/__init__.py +0 -0
  156. janus_ai-0.1.0/tests/unit/streaming/test_sse.py +38 -0
  157. janus_ai-0.1.0/tests/unit/streaming/test_translator.py +102 -0
  158. janus_ai-0.1.0/tests/unit/test_cli.py +72 -0
  159. janus_ai-0.1.0/tests/unit/tokensavers/__init__.py +0 -0
  160. janus_ai-0.1.0/tests/unit/tokensavers/test_caveman.py +22 -0
  161. janus_ai-0.1.0/tests/unit/tokensavers/test_pipeline.py +45 -0
  162. janus_ai-0.1.0/tests/unit/tokensavers/test_ponytail.py +44 -0
  163. janus_ai-0.1.0/tests/unit/tokensavers/test_rtk.py +96 -0
@@ -0,0 +1,10 @@
1
+ .venv/
2
+ .git/
3
+ __pycache__/
4
+ *.pyc
5
+ tests/
6
+ docs/
7
+ .mypy_cache/
8
+ .ruff_cache/
9
+ *.egg-info/
10
+ janus-data/
@@ -0,0 +1,6 @@
1
+ # env.example
2
+ JANUS_PORT=20128
3
+ JANUS_HOST=127.0.0.1
4
+ JANUS_DATA_DIR=~/.janus
5
+ JANUS_REQUIRE_API_KEY=false
6
+ GLM_API_KEY=
@@ -0,0 +1,32 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["*"]
6
+ pull_request:
7
+ branches: ["main"]
8
+
9
+ jobs:
10
+ ci:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.11"
18
+
19
+ - name: Install dependencies
20
+ run: pip install -e ".[dev]"
21
+
22
+ - name: Ruff check
23
+ run: ruff check src/janus/ tests/
24
+
25
+ - name: Ruff format check
26
+ run: ruff format --check src/janus/ tests/
27
+
28
+ - name: Mypy
29
+ run: mypy src/janus/
30
+
31
+ - name: Pytest
32
+ run: python -m pytest -q
@@ -0,0 +1,28 @@
1
+ name: Deploy Docs
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths:
7
+ - "docs/**"
8
+ - "mkdocs.yml"
9
+ - "README.md"
10
+
11
+ jobs:
12
+ deploy:
13
+ runs-on: ubuntu-latest
14
+ permissions:
15
+ contents: write
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - uses: actions/setup-python@v5
21
+ with:
22
+ python-version: "3.11"
23
+
24
+ - name: Install MkDocs Material
25
+ run: pip install mkdocs-material
26
+
27
+ - name: Build and deploy
28
+ run: mkdocs gh-deploy --force
@@ -0,0 +1,29 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ environment: pypi
12
+ permissions:
13
+ id-token: write
14
+
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - uses: actions/setup-python@v5
19
+ with:
20
+ python-version: "3.11"
21
+
22
+ - name: Install build tool
23
+ run: pip install build
24
+
25
+ - name: Build distributions
26
+ run: python -m build
27
+
28
+ - name: Publish to PyPI
29
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,83 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ share/python-wheels/
20
+ *.egg-info/
21
+ .installed.cfg
22
+ *.egg
23
+ MANIFEST
24
+
25
+ # Virtual environments
26
+ .venv/
27
+ venv/
28
+ env/
29
+ ENV/
30
+
31
+ # uv
32
+ uv.lock
33
+
34
+ # Testing / coverage
35
+ .tox/
36
+ .nox/
37
+ .coverage
38
+ .coverage.*
39
+ .cache
40
+ nosetests.xml
41
+ coverage.xml
42
+ *.cover
43
+ *.py,cover
44
+ .hypothesis/
45
+ .pytest_cache/
46
+ .mypy_cache/
47
+ .ruff_cache/
48
+ .pytype/
49
+
50
+ # Jupyter
51
+ .ipynb_checkpoints
52
+
53
+ # IDEs
54
+ .idea/
55
+ .vscode/
56
+ *.swp
57
+ *.swo
58
+ *~
59
+
60
+ # OS
61
+ .DS_Store
62
+ Thumbs.db
63
+
64
+ # Project data (local-first runtime)
65
+ .janus/
66
+ *.sqlite
67
+ *.sqlite3
68
+ db.sqlite
69
+ logs/
70
+
71
+ # Env / secrets
72
+ .env
73
+ .env.*
74
+ !.env.example
75
+
76
+ # Distribution
77
+ *.manifest
78
+ *.spec
79
+ pip-log.txt
80
+ pip-delete-this-directory.txt
81
+
82
+ # MkDocs build output
83
+ site/
@@ -0,0 +1 @@
1
+ 3.11
@@ -0,0 +1,152 @@
1
+ # AGENTS.md
2
+
3
+ ## Dev environment
4
+
5
+ - Python 3.11 in a `.venv` at repo root. Always use `.venv/bin/python -m pytest`, not bare `pytest`.
6
+ - Install: `pip install -e ".[dev]"` (editable + dev extras including respx, ruff, mypy).
7
+ - CI runs automatically on push/PR via `.github/workflows/ci.yml` (ruff check + format + mypy + pytest).
8
+
9
+ ## Commands
10
+
11
+ ```bash
12
+ # Run all tests
13
+ .venv/bin/python -m pytest
14
+
15
+ # Run a single test
16
+ .venv/bin/python -m pytest tests/unit/formats/test_openai.py::test_name -v
17
+
18
+ # Lint + typecheck (run both before committing)
19
+ .venv/bin/ruff check src/janus/ tests/
20
+ .venv/bin/mypy src/janus/
21
+
22
+ # Format check
23
+ .venv/bin/ruff format --check src/janus/ tests/
24
+
25
+ # Start dev server
26
+ .venv/bin/janus serve --port 20128 --config ~/.janus/config.yaml
27
+
28
+ # Docker
29
+ docker compose up -d # builds, starts, persists data in ./janus-data/
30
+
31
+ # Preview docs site locally
32
+ .venv/bin/mkdocs serve
33
+
34
+ # Verify docs build (strict mode)
35
+ .venv/bin/mkdocs build --strict
36
+
37
+ # Build wheel + sdist
38
+ .venv/bin/python -m build
39
+ ```
40
+
41
+ ## Architecture constraint
42
+
43
+ Janus uses a **canonical intermediate model**. The rule: `formats/` and `providers/` never import or call each other — they only talk to `canonical/`. This is intentional (2N adapters instead of N² translators). Do not break this boundary.
44
+
45
+ Request flow: client format → `parse_request` → `CanonicalRequest` → `SaverPipeline.apply` → `FallbackHandler.resolve_attempts` → budget check (`_check_budgets`) → per-attempt: `build_upstream_request` → upstream call → `parse_upstream_response` → `CanonicalResponse` → `emit_response` → `record_usage` (with cost). On 429/5xx/auth/network errors, the account is cooled down and the next attempt is tried.
46
+
47
+ ## Routing & fallback layer
48
+
49
+ - `ProviderRegistry` stores `list[ProviderConfig]` per prefix (multi-account). `lookup()` returns `list[ResolvedTarget]`, not a single target.
50
+ - `FallbackHandler` (`routing/fallback.py`) expands combos → models → available accounts, filtering out cooled-down accounts. Cooldown durations: 429→60s, 5xx→30s, auth→300s, network→15s. State is in-memory (`time.monotonic()`).
51
+ - `routing/errors.py` has `classify_error(status_code)` and `is_fallback_eligible(error)` — these drive fallback decisions in `_handle()`.
52
+ - The retry loop lives in `api/routes.py::_handle()`. Streaming requests do NOT retry mid-stream (can't replay partial output).
53
+ - Adding multi-account: register multiple `ProviderConfig` entries with the same `prefix` but different `id`/`api_key`.
54
+
55
+ ## Provider lifecycle & connection pooling
56
+
57
+ - Providers are built once in `create_app()` via `_build_provider()` in **`app.py`** (not routes.py) and cached in `app.state.providers` as `dict[str, Provider]` keyed by `config.id`.
58
+ - Each provider holds a shared `httpx.AsyncClient` with pool limits (100 connections, 20 keepalive). Clients are NOT created per-request.
59
+ - Providers are closed on shutdown via the FastAPI lifespan handler in `app.py`.
60
+ - `_handle()` looks up cached providers by `target.provider_config.id` — never constructs providers inline.
61
+
62
+ ## Adding a new format adapter
63
+
64
+ 1. Create `src/janus/formats/<name>.py` implementing all six methods: `parse_request`, `build_upstream_request`, `parse_upstream_response`, `emit_response`, `stream_parser`, `stream_emitter`.
65
+ 2. Register in the `FORMATS` dict in `src/janus/api/routes.py`.
66
+
67
+ ## Adding a new provider executor
68
+
69
+ 1. Create `src/janus/providers/<name>.py` with a `call(payload, stream) -> RawResult` method and a `close()` method (the `Provider` protocol requires it).
70
+ 2. Add a case to `_build_provider()` in **`src/janus/app.py`**.
71
+ 3. If the provider's native format differs from its `api_type`, update `_resolve_format()` in routes.py.
72
+
73
+ ## Code style (enforced by tooling)
74
+
75
+ - `ruff` with line-length 100, rules: E, F, I, N, W, UP.
76
+ - `mypy --strict` — bare `dict`/`list` must be typed (`dict[str, Any]`). Use `X | Y` not `Union`. Use `StrEnum` not `str, Enum`.
77
+ - No code comments unless explicitly requested.
78
+ - src layout: package code lives under `src/janus/`, not repo root.
79
+
80
+ ## Testing
81
+
82
+ - `pytest-asyncio` with `asyncio_mode = "auto"` — async test functions work without `@pytest.mark.asyncio`.
83
+ - Provider tests mock httpx with `respx` (no real network calls).
84
+ - Integration tests use FastAPI ASGI transport (`httpx.ASGITransport`) in-process.
85
+ - Test fixtures (sample API payloads, usage seed helpers) live in `tests/fixtures/`.
86
+
87
+ ## Token savers
88
+
89
+ The `tokensavers/` package runs on the canonical request after parsing, before provider routing. Each saver is a pure `transform(req) -> CanonicalRequest`. The pipeline (`tokensavers/pipeline.py`) runs enabled savers in sequence and is fail-safe — exceptions are caught and logged, never breaking the request.
90
+
91
+ - **RTK** (default ON) — compresses `tool_result` content parts (git diff, ls, grep, logs). Auto-detects format, strips ANSI/diff-mode/permissions, deduplicates, smart-truncates.
92
+ - **Caveman** — prepends a terse-output system prompt.
93
+ - **Ponytail** — prepends a lazy-dev system prompt (3 levels: lite/full/ultra).
94
+ - Config: `token_savers:` section in YAML. Savers stack (all enabled ones run in order).
95
+ - To add a new saver: implement `TokenSaver` protocol in `tokensavers/`, add to pipeline construction in `app.py`.
96
+
97
+ ## SQLite storage
98
+
99
+ The `storage/` package manages runtime state in SQLite (`~/.janus/janus.db`). DB is auto-created on app startup via FastAPI lifespan (`app.py`). Schema migrations are idempotent — `init_db()` uses `PRAGMA table_info` + `ALTER TABLE ADD COLUMN` for new columns.
100
+
101
+ - `storage/database.py` — `init_db()` + `get_connection()` (async context manager using `aiosqlite`).
102
+ - `storage/api_keys.py` — keys are `sk-janus-{32hex}`, stored as SHA256 hash. `verify_key()` returns `int | None` (DB row ID). The API-key gate (`api/deps.py`) checks both config `api_keys` (static list) AND DB keys. When a DB key is used, `request.state.client_key_id` is set.
103
+ - `storage/usage.py` — `record_usage()` records per-request token usage (fire-and-forget, non-streaming only). Params include `cost`, `cache_creation_tokens`, `cache_read_tokens`, `client_key_id`.
104
+ - `storage/analytics.py` — aggregated queries: `get_spend_summary(days)`, `get_breakdown(dimension, days)`, `get_success_rate(days)`.
105
+ - `storage/budgets.py` — budget CRUD + `get_budget_status(key_id)`.
106
+ - CLI key management: `janus keys create/list/revoke`.
107
+
108
+ ## Pricing & cost tracking
109
+
110
+ The `pricing/` package provides per-model cost estimation. `PricingRegistry` merges builtin defaults (~28 popular models) with YAML overrides from the `pricing:` config section. Cost is computed at recording time via `compute_cost(usage, model, registry)` and stored in the `usage.cost` column. Unknown models cost $0.0 (not an error).
111
+
112
+ - `pricing/builtin.py` — hardcoded `dict[str, ModelPricing]` seed data ($ per million tokens: input, output, cache_creation, cache_read).
113
+ - `pricing/registry.py` — `PricingRegistry(overrides)`, exact match then progressively shorter prefix matching for model variants.
114
+ - `pricing/calculator.py` — `compute_cost(usage, model, registry) -> float`, pure function.
115
+ - Config: `pricing:` section in YAML, same dict structure as builtin.
116
+
117
+ ## Budget enforcement
118
+
119
+ Budgets are daily spending limits stored in the `budgets` SQLite table. Each budget targets either a specific API key (`key_id`) or is global (`key_id = NULL`). Enforcement happens in `_handle()` before routing:
120
+
121
+ - **Warn threshold** (default 80%): request proceeds, dashboard shows amber.
122
+ - **Hard threshold** (100%): request rejected with `429` + `Retry-After` header.
123
+ - Both per-key and global budgets are checked; most restrictive wins.
124
+ - Fail-safe: DB errors don't block requests.
125
+ - `storage/budgets.py` — CRUD + `get_budget_status(key_id)`.
126
+ - CLI: `janus budgets list/set/delete`.
127
+
128
+ ## Analytics
129
+
130
+ `storage/analytics.py` provides aggregated queries: `get_spend_summary(days)`, `get_breakdown(dimension, days)`, `get_success_rate(days)`. The dashboard `/dashboard/analytics` page uses Chart.js (via CDN) for spend trends and success-rate donut charts. Breakdowns available by model, provider, account, or client key.
131
+
132
+ ## Dashboard
133
+
134
+ The `dashboard/` package serves an HTMX + Jinja2 UI at `/dashboard`. No npm, no build step — Tailwind, HTMX, and Chart.js via CDN. Templates are in `dashboard/templates/`. Management API endpoints (`POST /dashboard/api/keys`, `DELETE /dashboard/api/keys/{id}`, `POST /dashboard/api/budgets`, `DELETE /dashboard/api/budgets/{id}`) return HTMX partials, not JSON.
135
+
136
+ ## Config
137
+
138
+ Runtime config is YAML at `~/.janus/config.yaml` with `${ENV_VAR}` token resolution. The `providers:`, `combos:`, and `token_savers:` keys can be null (all commented out) — the loader filters None values. Generate a template with `janus config-init`.
139
+
140
+ Combos are named ordered model sequences. A client sends `"model": "combo-name"` and Janus tries each model in order with all its accounts.
141
+
142
+ ## Documentation & Packaging
143
+
144
+ Docs site uses MkDocs Material. Config in `mkdocs.yml`, pages in `docs/`. Internal design specs in `docs/superpowers/` are excluded from the site nav via `exclude_docs`. Preview with `mkdocs serve`, verify with `mkdocs build --strict`.
145
+
146
+ Build backend is hatchling. Wheel + sdist via `python -m build`. PyPI publishing is automated via `.github/workflows/publish.yml` (OIDC trusted publisher, triggered on `v*` tag push). GitHub Pages deployment via `.github/workflows/docs.yml` (triggered on push to `main` when `docs/`, `mkdocs.yml`, or `README.md` change).
147
+
148
+ Dev dependencies (`mkdocs-material`, `build`) are in the `[dev]` extras.
149
+
150
+ Manual prerequisites (one-time):
151
+ - PyPI: Add GitHub as trusted publisher with environment `pypi`
152
+ - GitHub Pages: Set source to `gh-pages` branch in repo Settings > Pages
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be 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
+ ## [0.1.0] - 2026-06-25
9
+
10
+ ### Added
11
+ - Core routing gateway with canonical intermediate model translation
12
+ - Three format adapters: OpenAI, Anthropic, Gemini
13
+ - Four provider executors: openai_compat, anthropic, gemini, opencode_free
14
+ - SSE streaming translation between all supported formats
15
+ - Multi-account fallback routing with cooldowns (429→60s, 5xx→30s, auth→300s, network→15s)
16
+ - Named combos: ordered model sequences for automatic fallback chains
17
+ - Token savers: RTK compression (default ON), Caveman terse prompt, Ponytail lazy-dev prompt (3 levels)
18
+ - SQLite persistence: API keys (SHA256-hashed), usage tracking, budget storage
19
+ - Pricing engine: 28 builtin model prices, YAML-overridable, progressive prefix matching
20
+ - Budget enforcement: per-key and global daily limits with warn (80%) and block (100%) thresholds
21
+ - Analytics: cost tracking, spend trends, success rates, per-model/provider/key breakdowns
22
+ - HTMX dashboard with 7 pages: Overview, Providers, Combos, API Keys, Usage, Analytics, Budgets
23
+ - CLI: serve, config-init, config-path, keys (create/list/revoke), usage (stats/cost/by-key), budgets (list/set/delete), pricing (list/show)
24
+ - Docker support: multi-stage build, docker-compose with volume persistence
25
+ - GitHub Actions CI: ruff check + format, mypy --strict, pytest
26
+ - Connection pooling: shared httpx.AsyncClient per provider (100 connections, 20 keepalive)
27
+ - Provider caching in app.state.providers keyed by config.id
@@ -0,0 +1,86 @@
1
+ # Contributing to Janus
2
+
3
+ Thanks for your interest in contributing! This guide covers setup, development workflows, and how to extend Janus.
4
+
5
+ ## Development Setup
6
+
7
+ ```bash
8
+ git clone https://github.com/amanverasia/Janus.git
9
+ cd Janus
10
+ python -m venv .venv
11
+ pip install -e ".[dev]"
12
+ ```
13
+
14
+ ## Daily Commands
15
+
16
+ ```bash
17
+ # Run all tests (never use bare 'pytest')
18
+ .venv/bin/python -m pytest
19
+
20
+ # Run a single test
21
+ .venv/bin/python -m pytest tests/unit/formats/test_openai.py::test_name -v
22
+
23
+ # Lint
24
+ .venv/bin/ruff check src/janus/ tests/
25
+
26
+ # Format check
27
+ .venv/bin/ruff format --check src/janus/ tests/
28
+
29
+ # Typecheck
30
+ .venv/bin/mypy src/janus/
31
+
32
+ # Start dev server
33
+ .venv/bin/janus serve --port 20128 --reload
34
+
35
+ # Preview docs locally
36
+ .venv/bin/mkdocs serve
37
+ ```
38
+
39
+ Run `ruff check`, `ruff format --check`, and `mypy` before every commit. CI enforces all three.
40
+
41
+ ## Architecture Constraint
42
+
43
+ Janus uses a **canonical intermediate model**. The rule is simple:
44
+
45
+ > `formats/` and `providers/` never import or call each other — they only talk to `canonical/`.
46
+
47
+ This gives 2N adapters instead of N² translators. **Do not break this boundary.** If you need a format to talk to a provider, you're doing it wrong — go through the canonical model.
48
+
49
+ ### Request Flow
50
+
51
+ ```
52
+ client format → parse_request → CanonicalRequest → SaverPipeline.apply
53
+ → budget check → FallbackHandler.resolve_attempts
54
+ → per-attempt: build_upstream_request → upstream call → parse_upstream_response
55
+ → CanonicalResponse → emit_response → record_usage (with cost)
56
+ ```
57
+
58
+ On 429/5xx/auth/network errors, the account is cooled down and the next attempt is tried.
59
+
60
+ ## Adding a New Format Adapter
61
+
62
+ 1. Create `src/janus/formats/<name>.py` implementing all six methods: `parse_request`, `build_upstream_request`, `parse_upstream_response`, `emit_response`, `stream_parser`, `stream_emitter`.
63
+ 2. Register in the `FORMATS` dict in `src/janus/api/routes.py`.
64
+
65
+ ## Adding a New Provider Executor
66
+
67
+ 1. Create `src/janus/providers/<name>.py` with an `async call(payload, stream) -> RawResult` method and an `async close()` method.
68
+ 2. Add a case to `_build_provider()` in `src/janus/app.py`.
69
+ 3. If the provider's native format differs from its `api_type`, update `_resolve_format()` in `src/janus/api/routes.py`.
70
+
71
+ ## Adding a New Token Saver
72
+
73
+ 1. Implement the `TokenSaver` protocol (`transform(req) -> CanonicalRequest`) in `src/janus/tokensavers/`.
74
+ 2. Add to pipeline construction in `src/janus/app.py`.
75
+ 3. Savers must be fail-safe — exceptions are caught by the pipeline and logged, never breaking the request.
76
+
77
+ ## PR Process
78
+
79
+ - Squash-merge PRs to `main`. Branches are deleted after merge.
80
+ - Write tests for all new functionality. Tests use `pytest-asyncio` with `asyncio_mode = "auto"`.
81
+ - Provider tests mock httpx with `respx` — no real network calls.
82
+ - Integration tests use FastAPI ASGI transport (`httpx.ASGITransport`) in-process.
83
+ - Test fixtures live in `tests/fixtures/`.
84
+ - No code comments unless explicitly requested.
85
+ - `ruff` with line-length 100, rules: E, F, I, N, W, UP.
86
+ - `mypy --strict` — bare `dict`/`list` must be typed. Use `X | Y` not `Union`. Use `StrEnum` not `str, Enum`.
@@ -0,0 +1,14 @@
1
+ FROM python:3.11-slim AS builder
2
+ WORKDIR /build
3
+ COPY pyproject.toml ./
4
+ COPY src/ ./src/
5
+ RUN pip install --no-cache-dir .
6
+
7
+ FROM python:3.11-slim
8
+ RUN useradd -m -s /bin/bash janus
9
+ COPY --from=builder /usr/local/lib/python3.11/site-packages/ /usr/local/lib/python3.11/site-packages/
10
+ COPY --from=builder /usr/local/bin/janus /usr/local/bin/janus
11
+ WORKDIR /app
12
+ USER janus
13
+ EXPOSE 20128
14
+ CMD ["janus", "serve", "--host", "0.0.0.0", "--port", "20128", "--config", "/home/janus/.janus/config.yaml"]