codex-router 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.
- codex_router-0.1.0/LICENSE +21 -0
- codex_router-0.1.0/PKG-INFO +186 -0
- codex_router-0.1.0/README.md +154 -0
- codex_router-0.1.0/codex_router/__init__.py +11 -0
- codex_router-0.1.0/codex_router/cli.py +134 -0
- codex_router-0.1.0/codex_router/config.py +143 -0
- codex_router-0.1.0/codex_router/display.py +105 -0
- codex_router-0.1.0/codex_router/orchestrator.py +144 -0
- codex_router-0.1.0/codex_router/providers.py +173 -0
- codex_router-0.1.0/codex_router/router.py +184 -0
- codex_router-0.1.0/codex_router/tracker.py +148 -0
- codex_router-0.1.0/codex_router.egg-info/PKG-INFO +186 -0
- codex_router-0.1.0/codex_router.egg-info/SOURCES.txt +21 -0
- codex_router-0.1.0/codex_router.egg-info/dependency_links.txt +1 -0
- codex_router-0.1.0/codex_router.egg-info/entry_points.txt +2 -0
- codex_router-0.1.0/codex_router.egg-info/requires.txt +12 -0
- codex_router-0.1.0/codex_router.egg-info/top_level.txt +1 -0
- codex_router-0.1.0/pyproject.toml +66 -0
- codex_router-0.1.0/setup.cfg +4 -0
- codex_router-0.1.0/tests/test_config.py +150 -0
- codex_router-0.1.0/tests/test_providers.py +173 -0
- codex_router-0.1.0/tests/test_router.py +135 -0
- codex_router-0.1.0/tests/test_tracker.py +147 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Intellirim
|
|
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,186 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codex-router
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Intelligent routing and orchestration for multi-model AI coding agents
|
|
5
|
+
Author-email: Intellirim <hello@intellirim.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Intellirim/codex-router
|
|
8
|
+
Project-URL: Issues, https://github.com/Intellirim/codex-router/issues
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: click>=8.1.0
|
|
21
|
+
Requires-Dist: anthropic>=0.18.0
|
|
22
|
+
Requires-Dist: openai>=1.12.0
|
|
23
|
+
Requires-Dist: google-generativeai>=0.3.0
|
|
24
|
+
Requires-Dist: pydantic>=2.5.0
|
|
25
|
+
Requires-Dist: pyyaml>=6.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
29
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# codex-router
|
|
34
|
+
|
|
35
|
+

|
|
36
|
+
|
|
37
|
+
**Intelligent routing and orchestration for multi-model AI coding agents**
|
|
38
|
+
|
|
39
|
+
`codex-router` is a lightweight CLI tool that automatically routes coding tasks to the best available AI model (Claude, GPT, Gemini, or local) based on task complexity, cost, and availability. Manage parallel agent sessions, track token usage and costs across providers, and get unified output streaming. Makes it trivial to leverage multiple AI models without manual context switching.
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- **Smart routing**: Analyzes task complexity and routes to optimal model (fast models for simple tasks, frontier models for complex ones)
|
|
44
|
+
- **Parallel orchestration**: Run multiple AI agents on different subtasks simultaneously with unified output
|
|
45
|
+
- **Cost tracking**: Real-time token usage and cost monitoring across all providers (Claude, OpenAI, Gemini)
|
|
46
|
+
- **Auto-fallback**: Automatically switches to alternative model if primary hits rate limits or errors
|
|
47
|
+
- **Unified config**: Single configuration file for all API keys and preferences
|
|
48
|
+
- **Session management**: Save and resume multi-agent sessions with full context
|
|
49
|
+
- **ASCII-only output**: Cross-platform terminal compatibility (Windows, macOS, Linux)
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
Install via pip:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install codex-router
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
60
|
+
|
|
61
|
+
Configure your API keys:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
codex-router config --set anthropic_api_key YOUR_CLAUDE_KEY
|
|
65
|
+
codex-router config --set openai_api_key YOUR_OPENAI_KEY
|
|
66
|
+
codex-router config --set google_api_key YOUR_GEMINI_KEY
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Set default preferences:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
codex-router config --set-default-model claude
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Usage Examples
|
|
76
|
+
|
|
77
|
+
Run a single coding task with intelligent model selection:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
codex-router task "refactor auth module"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Output:
|
|
84
|
+
```
|
|
85
|
+
[Router] Analyzing task complexity...
|
|
86
|
+
[Router] Task complexity: HIGH -> Selected model: claude-opus-4
|
|
87
|
+
[Agent-1] Reading auth module...
|
|
88
|
+
[Agent-1] Identified 3 refactoring opportunities
|
|
89
|
+
[Agent-1] Applying changes...
|
|
90
|
+
[Agent-1] DONE - 2,450 tokens used ($0.073)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Run parallel agents on multiple subtasks:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
codex-router task "add unit tests" --parallel 2
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Output:
|
|
100
|
+
```
|
|
101
|
+
[Router] Splitting task into 2 parallel agents
|
|
102
|
+
[Agent-1] Testing user authentication flow...
|
|
103
|
+
[Agent-2] Testing database connections...
|
|
104
|
+
[Agent-1] Created 5 test cases
|
|
105
|
+
[Agent-2] Created 3 test cases
|
|
106
|
+
[Router] DONE - Total: 3,120 tokens ($0.094)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Constrain budget for cost control:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
codex-router task "add unit tests" --model claude --budget 0.50
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Check usage statistics:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
codex-router status --show-costs
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Output:
|
|
122
|
+
```
|
|
123
|
+
Token Usage Summary (Last 7 Days)
|
|
124
|
+
---------------------------------
|
|
125
|
+
Provider | Tokens | Cost
|
|
126
|
+
---------------------------------
|
|
127
|
+
Claude | 45,230 | $1.35
|
|
128
|
+
OpenAI | 12,500 | $0.25
|
|
129
|
+
Gemini | 8,900 | $0.00
|
|
130
|
+
---------------------------------
|
|
131
|
+
Total | $1.60
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Configuration
|
|
135
|
+
|
|
136
|
+
Configuration is stored in `~/.codex-router/config.yaml`. You can edit it manually or use the CLI:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
# Set API keys
|
|
140
|
+
codex-router config --set anthropic_api_key YOUR_KEY
|
|
141
|
+
codex-router config --set openai_api_key YOUR_KEY
|
|
142
|
+
codex-router config --set google_api_key YOUR_KEY
|
|
143
|
+
|
|
144
|
+
# Set default model
|
|
145
|
+
codex-router config --set-default-model gpt-4
|
|
146
|
+
|
|
147
|
+
# Set budget limits
|
|
148
|
+
codex-router config --set daily_budget 5.00
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## How It Works
|
|
152
|
+
|
|
153
|
+
1. **Task Analysis**: The router analyzes your task description for complexity, required context, and estimated token usage
|
|
154
|
+
2. **Model Selection**: Based on complexity and your preferences, selects the optimal model (e.g., GPT-3.5 for simple tasks, Claude Opus for complex refactoring)
|
|
155
|
+
3. **Execution**: Sends the task to the selected provider's API with proper context and streaming
|
|
156
|
+
4. **Cost Tracking**: Records token usage and costs in a local database for monitoring
|
|
157
|
+
5. **Auto-Fallback**: If the primary model fails (rate limit, timeout), automatically retries with an alternative model
|
|
158
|
+
|
|
159
|
+
## Development
|
|
160
|
+
|
|
161
|
+
Clone the repository:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
git clone https://github.com/Intellirim/codex-router.git
|
|
165
|
+
cd codex-router
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Install in development mode:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
pip install -e .
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Run tests:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
pytest tests/
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT License - Copyright (c) 2026 Intellirim
|
|
183
|
+
|
|
184
|
+
## Contributing
|
|
185
|
+
|
|
186
|
+
Contributions welcome! Please open an issue or submit a pull request.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# codex-router
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
**Intelligent routing and orchestration for multi-model AI coding agents**
|
|
6
|
+
|
|
7
|
+
`codex-router` is a lightweight CLI tool that automatically routes coding tasks to the best available AI model (Claude, GPT, Gemini, or local) based on task complexity, cost, and availability. Manage parallel agent sessions, track token usage and costs across providers, and get unified output streaming. Makes it trivial to leverage multiple AI models without manual context switching.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Smart routing**: Analyzes task complexity and routes to optimal model (fast models for simple tasks, frontier models for complex ones)
|
|
12
|
+
- **Parallel orchestration**: Run multiple AI agents on different subtasks simultaneously with unified output
|
|
13
|
+
- **Cost tracking**: Real-time token usage and cost monitoring across all providers (Claude, OpenAI, Gemini)
|
|
14
|
+
- **Auto-fallback**: Automatically switches to alternative model if primary hits rate limits or errors
|
|
15
|
+
- **Unified config**: Single configuration file for all API keys and preferences
|
|
16
|
+
- **Session management**: Save and resume multi-agent sessions with full context
|
|
17
|
+
- **ASCII-only output**: Cross-platform terminal compatibility (Windows, macOS, Linux)
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
Install via pip:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install codex-router
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
Configure your API keys:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
codex-router config --set anthropic_api_key YOUR_CLAUDE_KEY
|
|
33
|
+
codex-router config --set openai_api_key YOUR_OPENAI_KEY
|
|
34
|
+
codex-router config --set google_api_key YOUR_GEMINI_KEY
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Set default preferences:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
codex-router config --set-default-model claude
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage Examples
|
|
44
|
+
|
|
45
|
+
Run a single coding task with intelligent model selection:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
codex-router task "refactor auth module"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Output:
|
|
52
|
+
```
|
|
53
|
+
[Router] Analyzing task complexity...
|
|
54
|
+
[Router] Task complexity: HIGH -> Selected model: claude-opus-4
|
|
55
|
+
[Agent-1] Reading auth module...
|
|
56
|
+
[Agent-1] Identified 3 refactoring opportunities
|
|
57
|
+
[Agent-1] Applying changes...
|
|
58
|
+
[Agent-1] DONE - 2,450 tokens used ($0.073)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Run parallel agents on multiple subtasks:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
codex-router task "add unit tests" --parallel 2
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Output:
|
|
68
|
+
```
|
|
69
|
+
[Router] Splitting task into 2 parallel agents
|
|
70
|
+
[Agent-1] Testing user authentication flow...
|
|
71
|
+
[Agent-2] Testing database connections...
|
|
72
|
+
[Agent-1] Created 5 test cases
|
|
73
|
+
[Agent-2] Created 3 test cases
|
|
74
|
+
[Router] DONE - Total: 3,120 tokens ($0.094)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Constrain budget for cost control:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
codex-router task "add unit tests" --model claude --budget 0.50
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Check usage statistics:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
codex-router status --show-costs
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Output:
|
|
90
|
+
```
|
|
91
|
+
Token Usage Summary (Last 7 Days)
|
|
92
|
+
---------------------------------
|
|
93
|
+
Provider | Tokens | Cost
|
|
94
|
+
---------------------------------
|
|
95
|
+
Claude | 45,230 | $1.35
|
|
96
|
+
OpenAI | 12,500 | $0.25
|
|
97
|
+
Gemini | 8,900 | $0.00
|
|
98
|
+
---------------------------------
|
|
99
|
+
Total | $1.60
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Configuration
|
|
103
|
+
|
|
104
|
+
Configuration is stored in `~/.codex-router/config.yaml`. You can edit it manually or use the CLI:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# Set API keys
|
|
108
|
+
codex-router config --set anthropic_api_key YOUR_KEY
|
|
109
|
+
codex-router config --set openai_api_key YOUR_KEY
|
|
110
|
+
codex-router config --set google_api_key YOUR_KEY
|
|
111
|
+
|
|
112
|
+
# Set default model
|
|
113
|
+
codex-router config --set-default-model gpt-4
|
|
114
|
+
|
|
115
|
+
# Set budget limits
|
|
116
|
+
codex-router config --set daily_budget 5.00
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## How It Works
|
|
120
|
+
|
|
121
|
+
1. **Task Analysis**: The router analyzes your task description for complexity, required context, and estimated token usage
|
|
122
|
+
2. **Model Selection**: Based on complexity and your preferences, selects the optimal model (e.g., GPT-3.5 for simple tasks, Claude Opus for complex refactoring)
|
|
123
|
+
3. **Execution**: Sends the task to the selected provider's API with proper context and streaming
|
|
124
|
+
4. **Cost Tracking**: Records token usage and costs in a local database for monitoring
|
|
125
|
+
5. **Auto-Fallback**: If the primary model fails (rate limit, timeout), automatically retries with an alternative model
|
|
126
|
+
|
|
127
|
+
## Development
|
|
128
|
+
|
|
129
|
+
Clone the repository:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
git clone https://github.com/Intellirim/codex-router.git
|
|
133
|
+
cd codex-router
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Install in development mode:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
pip install -e .
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Run tests:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
pytest tests/
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT License - Copyright (c) 2026 Intellirim
|
|
151
|
+
|
|
152
|
+
## Contributing
|
|
153
|
+
|
|
154
|
+
Contributions welcome! Please open an issue or submit a pull request.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""codex-router: Intelligent routing and orchestration for multi-model AI coding agents."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
__author__ = "Intellirim"
|
|
5
|
+
__license__ = "MIT"
|
|
6
|
+
|
|
7
|
+
from codex_router.router import TaskRouter
|
|
8
|
+
from codex_router.config import Config
|
|
9
|
+
from codex_router.tracker import CostTracker
|
|
10
|
+
|
|
11
|
+
__all__ = ["TaskRouter", "Config", "CostTracker", "__version__"]
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""CLI entry point for codex-router."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from codex_router import __version__
|
|
6
|
+
from codex_router.config import Config
|
|
7
|
+
from codex_router.router import TaskRouter
|
|
8
|
+
from codex_router.orchestrator import Orchestrator
|
|
9
|
+
from codex_router.tracker import CostTracker
|
|
10
|
+
from codex_router.display import Display
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group()
|
|
14
|
+
@click.version_option(version=__version__)
|
|
15
|
+
def main():
|
|
16
|
+
"""Intelligent routing and orchestration for multi-model AI coding agents."""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@main.command()
|
|
21
|
+
@click.argument("description")
|
|
22
|
+
@click.option("--model", "-m", help="Force specific model (claude, gpt-4, gemini)")
|
|
23
|
+
@click.option("--parallel", "-p", type=int, default=1, help="Number of parallel agents")
|
|
24
|
+
@click.option("--budget", "-b", type=float, help="Maximum budget in USD")
|
|
25
|
+
def task(description: str, model: str, parallel: int, budget: float):
|
|
26
|
+
"""Execute a coding task with intelligent routing."""
|
|
27
|
+
try:
|
|
28
|
+
config = Config.load()
|
|
29
|
+
except FileNotFoundError:
|
|
30
|
+
click.echo("Error: Config not found. Run 'codex-router config --init' first.", err=True)
|
|
31
|
+
raise SystemExit(1)
|
|
32
|
+
|
|
33
|
+
display = Display()
|
|
34
|
+
tracker = CostTracker()
|
|
35
|
+
|
|
36
|
+
if budget and budget <= 0:
|
|
37
|
+
click.echo("Error: Budget must be positive", err=True)
|
|
38
|
+
raise SystemExit(1)
|
|
39
|
+
|
|
40
|
+
router = TaskRouter(config)
|
|
41
|
+
|
|
42
|
+
if parallel > 1:
|
|
43
|
+
display.info(f"Splitting task into {parallel} parallel agents")
|
|
44
|
+
orchestrator = Orchestrator(config, tracker)
|
|
45
|
+
try:
|
|
46
|
+
result = orchestrator.run_parallel(description, parallel, model, budget)
|
|
47
|
+
display.success(f"DONE - Total: {result['total_tokens']} tokens (${result['total_cost']:.3f})")
|
|
48
|
+
except Exception as e:
|
|
49
|
+
display.error(f"Orchestration failed: {str(e)}")
|
|
50
|
+
raise SystemExit(2)
|
|
51
|
+
else:
|
|
52
|
+
display.info("Analyzing task complexity...")
|
|
53
|
+
try:
|
|
54
|
+
selected_model = router.select_model(description, force_model=model)
|
|
55
|
+
display.info(f"Selected model: {selected_model}")
|
|
56
|
+
|
|
57
|
+
result = router.execute_task(description, selected_model, budget)
|
|
58
|
+
tracker.record_usage(selected_model, result["tokens"], result["cost"])
|
|
59
|
+
|
|
60
|
+
display.show_result(result["output"])
|
|
61
|
+
display.success(f"DONE - {result['tokens']} tokens (${result['cost']:.3f})")
|
|
62
|
+
except ValueError as e:
|
|
63
|
+
display.error(str(e))
|
|
64
|
+
raise SystemExit(1)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
display.error(f"Task execution failed: {str(e)}")
|
|
67
|
+
raise SystemExit(2)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@main.command()
|
|
71
|
+
@click.option("--show-costs", is_flag=True, help="Show cost breakdown by provider")
|
|
72
|
+
@click.option("--days", "-d", type=int, default=7, help="Number of days to show")
|
|
73
|
+
def status(show_costs: bool, days: int):
|
|
74
|
+
"""Show usage statistics and costs."""
|
|
75
|
+
tracker = CostTracker()
|
|
76
|
+
display = Display()
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
stats = tracker.get_stats(days)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
click.echo(f"Error: Failed to load statistics: {str(e)}", err=True)
|
|
82
|
+
raise SystemExit(2)
|
|
83
|
+
|
|
84
|
+
if show_costs:
|
|
85
|
+
display.show_cost_table(stats, days)
|
|
86
|
+
else:
|
|
87
|
+
click.echo(f"Total requests (last {days} days): {stats['total_requests']}")
|
|
88
|
+
click.echo(f"Total tokens: {stats['total_tokens']}")
|
|
89
|
+
click.echo(f"Total cost: ${stats['total_cost']:.2f}")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@main.command()
|
|
93
|
+
@click.option("--set", "set_value", nargs=2, metavar="KEY VALUE", help="Set config value")
|
|
94
|
+
@click.option("--set-default-model", metavar="MODEL", help="Set default model")
|
|
95
|
+
@click.option("--init", is_flag=True, help="Initialize config with defaults")
|
|
96
|
+
def config(set_value: tuple, set_default_model: str, init: bool):
|
|
97
|
+
"""Manage configuration."""
|
|
98
|
+
if init:
|
|
99
|
+
try:
|
|
100
|
+
Config.init_default()
|
|
101
|
+
click.echo("Config initialized at ~/.codex-router/config.yaml")
|
|
102
|
+
except Exception as e:
|
|
103
|
+
click.echo(f"Error: Failed to initialize config: {str(e)}", err=True)
|
|
104
|
+
raise SystemExit(2)
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
cfg = Config.load()
|
|
109
|
+
except FileNotFoundError:
|
|
110
|
+
click.echo("Error: Config not found. Run 'codex-router config --init' first.", err=True)
|
|
111
|
+
raise SystemExit(1)
|
|
112
|
+
|
|
113
|
+
if set_value:
|
|
114
|
+
key, value = set_value
|
|
115
|
+
try:
|
|
116
|
+
cfg.set(key, value)
|
|
117
|
+
cfg.save()
|
|
118
|
+
click.echo(f"Set {key} = {value}")
|
|
119
|
+
except ValueError as e:
|
|
120
|
+
click.echo(f"Error: {str(e)}", err=True)
|
|
121
|
+
raise SystemExit(1)
|
|
122
|
+
|
|
123
|
+
if set_default_model:
|
|
124
|
+
valid_models = ["claude", "gpt-4", "gpt-3.5", "gemini"]
|
|
125
|
+
if set_default_model not in valid_models:
|
|
126
|
+
click.echo(f"Error: Invalid model. Choose from: {', '.join(valid_models)}", err=True)
|
|
127
|
+
raise SystemExit(1)
|
|
128
|
+
cfg.set("default_model", set_default_model)
|
|
129
|
+
cfg.save()
|
|
130
|
+
click.echo(f"Set default_model = {set_default_model}")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if __name__ == "__main__":
|
|
134
|
+
main()
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Configuration management for API keys, defaults, and budgets."""
|
|
2
|
+
|
|
3
|
+
import yaml
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Config:
|
|
9
|
+
"""Manage configuration file with API keys and preferences."""
|
|
10
|
+
|
|
11
|
+
DEFAULT_CONFIG = {
|
|
12
|
+
"anthropic_api_key": "",
|
|
13
|
+
"openai_api_key": "",
|
|
14
|
+
"google_api_key": "",
|
|
15
|
+
"default_model": "claude",
|
|
16
|
+
"daily_budget": 10.0,
|
|
17
|
+
"max_parallel_agents": 5
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
def __init__(self, config_path: Path, data: dict):
|
|
21
|
+
"""Initialize configuration.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
config_path: Path to config file
|
|
25
|
+
data: Configuration dictionary
|
|
26
|
+
"""
|
|
27
|
+
self.config_path = config_path
|
|
28
|
+
self.data = data
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def load(cls, config_path: Optional[str] = None) -> "Config":
|
|
32
|
+
"""Load configuration from file.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
config_path: Optional custom config path
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Config instance
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
FileNotFoundError: If config file doesn't exist
|
|
42
|
+
"""
|
|
43
|
+
if config_path:
|
|
44
|
+
path = Path(config_path)
|
|
45
|
+
else:
|
|
46
|
+
path = Path.home() / ".codex-router" / "config.yaml"
|
|
47
|
+
|
|
48
|
+
if not path.exists():
|
|
49
|
+
raise FileNotFoundError(f"Config not found: {path}")
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
53
|
+
except yaml.YAMLError as e:
|
|
54
|
+
raise ValueError(f"Invalid YAML in config: {str(e)}")
|
|
55
|
+
|
|
56
|
+
if not isinstance(data, dict):
|
|
57
|
+
raise ValueError("Config must be a YAML dictionary")
|
|
58
|
+
|
|
59
|
+
return cls(path, data)
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def init_default(cls, config_path: Optional[str] = None) -> "Config":
|
|
63
|
+
"""Initialize config with default values.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
config_path: Optional custom config path
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
New Config instance
|
|
70
|
+
"""
|
|
71
|
+
if config_path:
|
|
72
|
+
path = Path(config_path)
|
|
73
|
+
else:
|
|
74
|
+
path = Path.home() / ".codex-router" / "config.yaml"
|
|
75
|
+
|
|
76
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
|
|
78
|
+
data = cls.DEFAULT_CONFIG.copy()
|
|
79
|
+
path.write_text(yaml.dump(data, default_flow_style=False), encoding="utf-8")
|
|
80
|
+
|
|
81
|
+
return cls(path, data)
|
|
82
|
+
|
|
83
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
84
|
+
"""Get configuration value.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
key: Configuration key
|
|
88
|
+
default: Default value if key not found
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Configuration value
|
|
92
|
+
"""
|
|
93
|
+
return self.data.get(key, default)
|
|
94
|
+
|
|
95
|
+
def set(self, key: str, value: Any) -> None:
|
|
96
|
+
"""Set configuration value.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
key: Configuration key
|
|
100
|
+
value: Value to set
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
ValueError: If key is invalid
|
|
104
|
+
"""
|
|
105
|
+
valid_keys = {
|
|
106
|
+
"anthropic_api_key", "openai_api_key", "google_api_key",
|
|
107
|
+
"default_model", "daily_budget", "max_parallel_agents"
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if key not in valid_keys:
|
|
111
|
+
raise ValueError(f"Invalid config key: {key}. Valid keys: {', '.join(valid_keys)}")
|
|
112
|
+
|
|
113
|
+
if key == "daily_budget" or key == "max_parallel_agents":
|
|
114
|
+
try:
|
|
115
|
+
value = float(value) if key == "daily_budget" else int(value)
|
|
116
|
+
except ValueError:
|
|
117
|
+
raise ValueError(f"{key} must be a number")
|
|
118
|
+
|
|
119
|
+
if value < 0:
|
|
120
|
+
raise ValueError(f"{key} must be non-negative")
|
|
121
|
+
|
|
122
|
+
self.data[key] = value
|
|
123
|
+
|
|
124
|
+
def save(self) -> None:
|
|
125
|
+
"""Save configuration to file."""
|
|
126
|
+
self.config_path.write_text(
|
|
127
|
+
yaml.dump(self.data, default_flow_style=False),
|
|
128
|
+
encoding="utf-8"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def validate(self) -> bool:
|
|
132
|
+
"""Check if at least one API key is configured.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
True if valid, False otherwise
|
|
136
|
+
"""
|
|
137
|
+
api_keys = [
|
|
138
|
+
self.data.get("anthropic_api_key"),
|
|
139
|
+
self.data.get("openai_api_key"),
|
|
140
|
+
self.data.get("google_api_key")
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
return any(key and key.strip() for key in api_keys)
|