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.
- janus_ai-0.1.0/.dockerignore +10 -0
- janus_ai-0.1.0/.env.example +6 -0
- janus_ai-0.1.0/.github/workflows/ci.yml +32 -0
- janus_ai-0.1.0/.github/workflows/docs.yml +28 -0
- janus_ai-0.1.0/.github/workflows/publish.yml +29 -0
- janus_ai-0.1.0/.gitignore +83 -0
- janus_ai-0.1.0/.python-version +1 -0
- janus_ai-0.1.0/AGENTS.md +152 -0
- janus_ai-0.1.0/CHANGELOG.md +27 -0
- janus_ai-0.1.0/CONTRIBUTING.md +86 -0
- janus_ai-0.1.0/Dockerfile +14 -0
- janus_ai-0.1.0/LICENSE +674 -0
- janus_ai-0.1.0/PKG-INFO +37 -0
- janus_ai-0.1.0/README.md +159 -0
- janus_ai-0.1.0/docker-compose.yml +14 -0
- janus_ai-0.1.0/docs/api-reference.md +294 -0
- janus_ai-0.1.0/docs/architecture.md +237 -0
- janus_ai-0.1.0/docs/budgets.md +118 -0
- janus_ai-0.1.0/docs/cli.md +289 -0
- janus_ai-0.1.0/docs/client-setup.md +70 -0
- janus_ai-0.1.0/docs/combos.md +130 -0
- janus_ai-0.1.0/docs/configuration.md +268 -0
- janus_ai-0.1.0/docs/contributing.md +86 -0
- janus_ai-0.1.0/docs/dashboard.md +117 -0
- janus_ai-0.1.0/docs/getting-started.md +125 -0
- janus_ai-0.1.0/docs/index.md +69 -0
- janus_ai-0.1.0/docs/providers.md +262 -0
- janus_ai-0.1.0/docs/superpowers/plans/2026-06-24-janus-phase1-core-router.md +3118 -0
- janus_ai-0.1.0/docs/superpowers/plans/2026-06-24-janus-phase2-fallback-combos.md +894 -0
- janus_ai-0.1.0/docs/superpowers/plans/2026-06-24-janus-phase3-token-savers.md +652 -0
- janus_ai-0.1.0/docs/superpowers/plans/2026-06-24-janus-phase4-sqlite-persistence.md +601 -0
- janus_ai-0.1.0/docs/superpowers/plans/2026-06-24-janus-phase5-dashboard.md +560 -0
- janus_ai-0.1.0/docs/superpowers/plans/2026-06-25-docs-packaging.md +1150 -0
- janus_ai-0.1.0/docs/superpowers/plans/2026-06-25-phase6-quota-usage-analytics.md +3180 -0
- janus_ai-0.1.0/docs/superpowers/plans/2026-06-25-phase7-deployment.md +837 -0
- janus_ai-0.1.0/docs/superpowers/specs/2026-06-24-janus-phase1-core-router-design.md +355 -0
- janus_ai-0.1.0/docs/superpowers/specs/2026-06-24-janus-phase2-fallback-combos-design.md +160 -0
- janus_ai-0.1.0/docs/superpowers/specs/2026-06-24-janus-phase3-token-savers-design.md +124 -0
- janus_ai-0.1.0/docs/superpowers/specs/2026-06-24-janus-phase4-sqlite-persistence-design.md +67 -0
- janus_ai-0.1.0/docs/superpowers/specs/2026-06-24-janus-phase5-dashboard-design.md +66 -0
- janus_ai-0.1.0/docs/superpowers/specs/2026-06-25-docs-packaging-design.md +173 -0
- janus_ai-0.1.0/docs/superpowers/specs/2026-06-25-phase6-quota-usage-analytics-design.md +419 -0
- janus_ai-0.1.0/docs/superpowers/specs/2026-06-25-phase7-deployment-design.md +184 -0
- janus_ai-0.1.0/docs/token-savers.md +108 -0
- janus_ai-0.1.0/mkdocs.yml +60 -0
- janus_ai-0.1.0/pyproject.toml +71 -0
- janus_ai-0.1.0/src/janus/__init__.py +0 -0
- janus_ai-0.1.0/src/janus/__main__.py +4 -0
- janus_ai-0.1.0/src/janus/api/__init__.py +0 -0
- janus_ai-0.1.0/src/janus/api/deps.py +21 -0
- janus_ai-0.1.0/src/janus/api/routes.py +225 -0
- janus_ai-0.1.0/src/janus/app.py +85 -0
- janus_ai-0.1.0/src/janus/canonical/__init__.py +0 -0
- janus_ai-0.1.0/src/janus/canonical/events.py +64 -0
- janus_ai-0.1.0/src/janus/canonical/models.py +120 -0
- janus_ai-0.1.0/src/janus/cli.py +346 -0
- janus_ai-0.1.0/src/janus/config/__init__.py +0 -0
- janus_ai-0.1.0/src/janus/config/loader.py +37 -0
- janus_ai-0.1.0/src/janus/config/schema.py +46 -0
- janus_ai-0.1.0/src/janus/dashboard/__init__.py +0 -0
- janus_ai-0.1.0/src/janus/dashboard/routes.py +249 -0
- janus_ai-0.1.0/src/janus/dashboard/templates/analytics.html +151 -0
- janus_ai-0.1.0/src/janus/dashboard/templates/base.html +67 -0
- janus_ai-0.1.0/src/janus/dashboard/templates/budgets.html +41 -0
- janus_ai-0.1.0/src/janus/dashboard/templates/budgets_partial.html +61 -0
- janus_ai-0.1.0/src/janus/dashboard/templates/combos.html +29 -0
- janus_ai-0.1.0/src/janus/dashboard/templates/keys.html +30 -0
- janus_ai-0.1.0/src/janus/dashboard/templates/keys_partial.html +61 -0
- janus_ai-0.1.0/src/janus/dashboard/templates/overview.html +75 -0
- janus_ai-0.1.0/src/janus/dashboard/templates/providers.html +46 -0
- janus_ai-0.1.0/src/janus/dashboard/templates/usage.html +58 -0
- janus_ai-0.1.0/src/janus/formats/__init__.py +0 -0
- janus_ai-0.1.0/src/janus/formats/anthropic.py +456 -0
- janus_ai-0.1.0/src/janus/formats/base.py +30 -0
- janus_ai-0.1.0/src/janus/formats/gemini.py +413 -0
- janus_ai-0.1.0/src/janus/formats/openai.py +485 -0
- janus_ai-0.1.0/src/janus/pricing/__init__.py +0 -0
- janus_ai-0.1.0/src/janus/pricing/builtin.py +39 -0
- janus_ai-0.1.0/src/janus/pricing/calculator.py +17 -0
- janus_ai-0.1.0/src/janus/pricing/models.py +11 -0
- janus_ai-0.1.0/src/janus/pricing/registry.py +24 -0
- janus_ai-0.1.0/src/janus/providers/__init__.py +0 -0
- janus_ai-0.1.0/src/janus/providers/anthropic.py +51 -0
- janus_ai-0.1.0/src/janus/providers/base.py +20 -0
- janus_ai-0.1.0/src/janus/providers/gemini.py +56 -0
- janus_ai-0.1.0/src/janus/providers/openai_compat.py +50 -0
- janus_ai-0.1.0/src/janus/providers/opencode_free.py +10 -0
- janus_ai-0.1.0/src/janus/providers/registry.py +60 -0
- janus_ai-0.1.0/src/janus/routing/__init__.py +0 -0
- janus_ai-0.1.0/src/janus/routing/errors.py +34 -0
- janus_ai-0.1.0/src/janus/routing/fallback.py +59 -0
- janus_ai-0.1.0/src/janus/routing/resolver.py +7 -0
- janus_ai-0.1.0/src/janus/settings.py +14 -0
- janus_ai-0.1.0/src/janus/storage/__init__.py +0 -0
- janus_ai-0.1.0/src/janus/storage/analytics.py +99 -0
- janus_ai-0.1.0/src/janus/storage/api_keys.py +57 -0
- janus_ai-0.1.0/src/janus/storage/budgets.py +111 -0
- janus_ai-0.1.0/src/janus/storage/database.py +78 -0
- janus_ai-0.1.0/src/janus/storage/usage.py +76 -0
- janus_ai-0.1.0/src/janus/streaming/__init__.py +0 -0
- janus_ai-0.1.0/src/janus/streaming/sse.py +33 -0
- janus_ai-0.1.0/src/janus/streaming/translator.py +32 -0
- janus_ai-0.1.0/src/janus/tokensavers/__init__.py +0 -0
- janus_ai-0.1.0/src/janus/tokensavers/base.py +9 -0
- janus_ai-0.1.0/src/janus/tokensavers/caveman.py +15 -0
- janus_ai-0.1.0/src/janus/tokensavers/pipeline.py +22 -0
- janus_ai-0.1.0/src/janus/tokensavers/ponytail.py +34 -0
- janus_ai-0.1.0/src/janus/tokensavers/rtk.py +103 -0
- janus_ai-0.1.0/tests/__init__.py +0 -0
- janus_ai-0.1.0/tests/conftest.py +0 -0
- janus_ai-0.1.0/tests/fixtures/.gitkeep +0 -0
- janus_ai-0.1.0/tests/fixtures/__init__.py +0 -0
- janus_ai-0.1.0/tests/fixtures/anthropic_message_request.json +14 -0
- janus_ai-0.1.0/tests/fixtures/anthropic_stream.txt +17 -0
- janus_ai-0.1.0/tests/fixtures/gemini_request.json +7 -0
- janus_ai-0.1.0/tests/fixtures/openai_chat_request.json +9 -0
- janus_ai-0.1.0/tests/fixtures/openai_nonstream_response.json +1 -0
- janus_ai-0.1.0/tests/fixtures/openai_stream.txt +9 -0
- janus_ai-0.1.0/tests/fixtures/usage_seed.py +36 -0
- janus_ai-0.1.0/tests/integration/__init__.py +0 -0
- janus_ai-0.1.0/tests/integration/test_api.py +463 -0
- janus_ai-0.1.0/tests/integration/test_budget_enforcement.py +105 -0
- janus_ai-0.1.0/tests/integration/test_cost_recording.py +67 -0
- janus_ai-0.1.0/tests/integration/test_dashboard.py +81 -0
- janus_ai-0.1.0/tests/unit/__init__.py +0 -0
- janus_ai-0.1.0/tests/unit/canonical/__init__.py +0 -0
- janus_ai-0.1.0/tests/unit/canonical/test_events.py +59 -0
- janus_ai-0.1.0/tests/unit/canonical/test_models.py +71 -0
- janus_ai-0.1.0/tests/unit/config/__init__.py +0 -0
- janus_ai-0.1.0/tests/unit/config/test_loader.py +85 -0
- janus_ai-0.1.0/tests/unit/config/test_pricing_config.py +21 -0
- janus_ai-0.1.0/tests/unit/config/test_schema.py +61 -0
- janus_ai-0.1.0/tests/unit/formats/__init__.py +0 -0
- janus_ai-0.1.0/tests/unit/formats/test_anthropic.py +80 -0
- janus_ai-0.1.0/tests/unit/formats/test_base.py +6 -0
- janus_ai-0.1.0/tests/unit/formats/test_gemini.py +65 -0
- janus_ai-0.1.0/tests/unit/formats/test_openai.py +151 -0
- janus_ai-0.1.0/tests/unit/pricing/__init__.py +0 -0
- janus_ai-0.1.0/tests/unit/pricing/test_builtin.py +27 -0
- janus_ai-0.1.0/tests/unit/pricing/test_calculator.py +68 -0
- janus_ai-0.1.0/tests/unit/pricing/test_registry.py +77 -0
- janus_ai-0.1.0/tests/unit/providers/__init__.py +0 -0
- janus_ai-0.1.0/tests/unit/providers/test_providers.py +91 -0
- janus_ai-0.1.0/tests/unit/providers/test_registry.py +72 -0
- janus_ai-0.1.0/tests/unit/routing/__init__.py +0 -0
- janus_ai-0.1.0/tests/unit/routing/test_errors.py +44 -0
- janus_ai-0.1.0/tests/unit/routing/test_resolver.py +169 -0
- janus_ai-0.1.0/tests/unit/storage/__init__.py +0 -0
- janus_ai-0.1.0/tests/unit/storage/test_analytics.py +153 -0
- janus_ai-0.1.0/tests/unit/storage/test_api_keys.py +79 -0
- janus_ai-0.1.0/tests/unit/storage/test_budgets.py +121 -0
- janus_ai-0.1.0/tests/unit/storage/test_database.py +28 -0
- janus_ai-0.1.0/tests/unit/storage/test_migration.py +47 -0
- janus_ai-0.1.0/tests/unit/storage/test_usage.py +128 -0
- janus_ai-0.1.0/tests/unit/streaming/__init__.py +0 -0
- janus_ai-0.1.0/tests/unit/streaming/test_sse.py +38 -0
- janus_ai-0.1.0/tests/unit/streaming/test_translator.py +102 -0
- janus_ai-0.1.0/tests/unit/test_cli.py +72 -0
- janus_ai-0.1.0/tests/unit/tokensavers/__init__.py +0 -0
- janus_ai-0.1.0/tests/unit/tokensavers/test_caveman.py +22 -0
- janus_ai-0.1.0/tests/unit/tokensavers/test_pipeline.py +45 -0
- janus_ai-0.1.0/tests/unit/tokensavers/test_ponytail.py +44 -0
- janus_ai-0.1.0/tests/unit/tokensavers/test_rtk.py +96 -0
|
@@ -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
|
janus_ai-0.1.0/AGENTS.md
ADDED
|
@@ -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"]
|