Enhance user configuration management and logging
- Introduced user configuration command to set API key. - Updated README and documentation for user config and logging paths. - Refactored logging to support user-specific log files. - Added tests for user configuration and logging behavior.
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,6 +3,7 @@
|
|||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
.test-tmp/
|
.test-tmp/
|
||||||
pytest-cache-files-*/
|
pytest-cache-files-*/
|
||||||
|
dist/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
@@ -17,4 +18,4 @@ nexus-claude-api.local.json
|
|||||||
.agents
|
.agents
|
||||||
|
|
||||||
# logs
|
# logs
|
||||||
logs
|
logs
|
||||||
|
|||||||
22
README.md
22
README.md
@@ -9,6 +9,7 @@ AI Nexus currently documents AWS Bedrock Converse API as the workaround while An
|
|||||||
```powershell
|
```powershell
|
||||||
cd nexus-claude-api
|
cd nexus-claude-api
|
||||||
uv sync --extra dev
|
uv sync --extra dev
|
||||||
|
uv run nexus-claude-api config set --api-key "your-nexus-api-key"
|
||||||
uv run nexus-claude-api start --port 4141 --claude-code
|
uv run nexus-claude-api start --port 4141 --claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -32,11 +33,17 @@ The printed command ends with `claude --model <configured-model>` so the current
|
|||||||
Credential lookup order:
|
Credential lookup order:
|
||||||
|
|
||||||
1. `--api-key`
|
1. `--api-key`
|
||||||
2. local `nexus-claude-api.local.json`
|
2. user config `~/.config/nexus-claude-api/config.json`
|
||||||
3. `NEXUS_API_KEY`
|
3. `NEXUS_API_KEY`
|
||||||
4. `AWS_BEARER_TOKEN_BEDROCK`
|
4. `AWS_BEARER_TOKEN_BEDROCK`
|
||||||
|
|
||||||
For local hardcoded configuration, create `nexus-claude-api.local.json`:
|
To write user config:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
nexus-claude-api config set --api-key "your-nexus-api-key"
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates `~/.config/nexus-claude-api/config.json`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -44,7 +51,15 @@ For local hardcoded configuration, create `nexus-claude-api.local.json`:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
This file is ignored by git.
|
Logs are written to `~/.config/nexus-claude-api/logs/nexus-claude-api.log` by default.
|
||||||
|
|
||||||
|
For repository-local development only, pass `--dev` and create `nexus-claude-api.local.json` in the current directory:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run nexus-claude-api start --dev --port 4141 --claude-code
|
||||||
|
```
|
||||||
|
|
||||||
|
In development mode, config is read from `nexus-claude-api.local.json` and logs are written to `logs/nexus-claude-api.log` in the current directory. The local config file is ignored by git.
|
||||||
|
|
||||||
The service binds to `127.0.0.1` by default and does not persist API keys.
|
The service binds to `127.0.0.1` by default and does not persist API keys.
|
||||||
|
|
||||||
@@ -53,3 +68,4 @@ The service binds to `127.0.0.1` by default and does not persist API keys.
|
|||||||
- [PRD](docs/PRD.md)
|
- [PRD](docs/PRD.md)
|
||||||
- [Requirements Design](docs/REQUIREMENTS_DESIGN.md)
|
- [Requirements Design](docs/REQUIREMENTS_DESIGN.md)
|
||||||
- [AI Nexus Claude Documentation](docs/AI_NEXUS_CLAUDE.md)
|
- [AI Nexus Claude Documentation](docs/AI_NEXUS_CLAUDE.md)
|
||||||
|
- [Python Internal Distribution Guide](docs/PACKAGING_DISTRIBUTION.md)
|
||||||
|
|||||||
187
docs/PACKAGING_DISTRIBUTION.md
Normal file
187
docs/PACKAGING_DISTRIBUTION.md
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
# Python Internal Distribution Guide
|
||||||
|
|
||||||
|
This document describes how to distribute `nexus-claude-api` internally without publishing to PyPI.
|
||||||
|
|
||||||
|
## Recommended Options
|
||||||
|
|
||||||
|
Use one of these two approaches:
|
||||||
|
|
||||||
|
- **Wheel distribution**: build a `.whl` file and send it to colleagues. This is best for stable internal releases.
|
||||||
|
- **Private Git installation**: let colleagues install directly from the company Git repository. This is best for teammates who can access the source code or need frequent updates.
|
||||||
|
|
||||||
|
Both options require:
|
||||||
|
|
||||||
|
- Python `>=3.11`
|
||||||
|
- Access to PyPI or the company Python package mirror for runtime dependencies
|
||||||
|
- A personal AI Nexus API key
|
||||||
|
|
||||||
|
Do not share `nexus-claude-api.local.json`, `.env`, logs, or any personal API key.
|
||||||
|
|
||||||
|
## Option 1: Build and Share a Wheel
|
||||||
|
|
||||||
|
A wheel is the standard Python install package format. It is not an `.exe`; colleagues install it with `pip`, then run the installed `nexus-claude-api` command.
|
||||||
|
|
||||||
|
### Publisher Steps
|
||||||
|
|
||||||
|
1. Confirm the package version in `pyproject.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[project]
|
||||||
|
name = "nexus-claude-api"
|
||||||
|
version = "0.1.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
Update the version for every released package. Avoid sending different builds with the same version.
|
||||||
|
|
||||||
|
2. Run the supported checks:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv sync --extra dev
|
||||||
|
uv run pytest
|
||||||
|
uv run nexus-claude-api start --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Build the package:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv build
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates files under `dist/`, usually:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dist/nexus_claude_api-0.1.0-py3-none-any.whl
|
||||||
|
dist/nexus_claude_api-0.1.0.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Test the wheel in a clean environment:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
py -3.11 -m venv C:\Temp\nexus-claude-api-test
|
||||||
|
C:\Temp\nexus-claude-api-test\Scripts\python.exe -m pip install .\dist\nexus_claude_api-0.1.0-py3-none-any.whl
|
||||||
|
C:\Temp\nexus-claude-api-test\Scripts\nexus-claude-api.exe start --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Send only the `.whl` file to colleagues through an approved internal channel.
|
||||||
|
|
||||||
|
### Colleague Installation
|
||||||
|
|
||||||
|
Install the wheel:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
py -3.11 -m pip install .\nexus_claude_api-0.1.0-py3-none-any.whl
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure the personal Nexus key:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
nexus-claude-api config set --api-key "your-nexus-api-key"
|
||||||
|
```
|
||||||
|
|
||||||
|
Start the local proxy and print the Claude Code helper:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
nexus-claude-api start --port 4141 --claude-code
|
||||||
|
```
|
||||||
|
|
||||||
|
If PowerShell cannot find the installed script, use the module form:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
py -3.11 -m nexus_claude_api start --port 4141 --claude-code
|
||||||
|
```
|
||||||
|
|
||||||
|
### Upgrade
|
||||||
|
|
||||||
|
Send a new wheel with a new version, then ask colleagues to run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
py -3.11 -m pip install --upgrade .\nexus_claude_api-0.1.1-py3-none-any.whl
|
||||||
|
```
|
||||||
|
|
||||||
|
## Option 2: Install from Private Git
|
||||||
|
|
||||||
|
Private Git installation avoids manual wheel sharing. It still uses the Python package metadata from `pyproject.toml`, so the installed command is the same.
|
||||||
|
|
||||||
|
### Publisher Steps
|
||||||
|
|
||||||
|
1. Push the code to the company Git repository.
|
||||||
|
|
||||||
|
2. Update the version in `pyproject.toml` for each released version.
|
||||||
|
|
||||||
|
3. Tag stable versions:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git tag v0.1.0
|
||||||
|
git push origin v0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
For the next release:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git tag v0.1.1
|
||||||
|
git push origin v0.1.1
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Share an install command that points to a tag:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
py -3.11 -m pip install "git+https://company-git.example.com/group/nexus-claude-api.git@v0.1.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
Prefer tags or exact commit SHAs. Avoid asking normal users to install from `main`, because repeated installs can produce different code over time.
|
||||||
|
|
||||||
|
### Colleague Installation
|
||||||
|
|
||||||
|
Install from the company Git repository:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
py -3.11 -m pip install "git+https://company-git.example.com/group/nexus-claude-api.git@v0.1.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure the personal Nexus key:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
nexus-claude-api config set --api-key "your-nexus-api-key"
|
||||||
|
```
|
||||||
|
|
||||||
|
Start the local proxy:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
nexus-claude-api start --port 4141 --claude-code
|
||||||
|
```
|
||||||
|
|
||||||
|
### Upgrade
|
||||||
|
|
||||||
|
Upgrade to a newer tag:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
py -3.11 -m pip install --upgrade "git+https://company-git.example.com/group/nexus-claude-api.git@v0.1.1"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Clone
|
||||||
|
|
||||||
|
For colleagues who will modify or test the source code:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git clone https://company-git.example.com/group/nexus-claude-api.git
|
||||||
|
cd nexus-claude-api
|
||||||
|
uv sync --extra dev
|
||||||
|
uv run nexus-claude-api start --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Choosing Between the Two
|
||||||
|
|
||||||
|
- Use **wheel distribution** for normal internal users who only need a stable tool.
|
||||||
|
- Use **private Git installation** for colleagues who understand the repository, need fast updates, or help develop the project.
|
||||||
|
- Keep version numbers and Git tags aligned. For example, `pyproject.toml` version `0.1.0` should correspond to tag `v0.1.0`.
|
||||||
|
|
||||||
|
## Release Checklist
|
||||||
|
|
||||||
|
Before sharing a release:
|
||||||
|
|
||||||
|
- Run `uv run pytest`.
|
||||||
|
- Run `uv run nexus-claude-api start --dry-run`.
|
||||||
|
- Test wheel installation in a clean Python environment.
|
||||||
|
- If using Git installation, test install from the exact tag.
|
||||||
|
- Confirm no personal credentials are included.
|
||||||
|
- Confirm `nexus-claude-api.local.json`, `.env`, logs, caches, and `dist/` artifacts are not committed accidentally.
|
||||||
14
docs/PRD.md
14
docs/PRD.md
@@ -53,7 +53,8 @@ The proxy defaults to Opus because this deployment is intended for users whose N
|
|||||||
|
|
||||||
## User Stories
|
## User Stories
|
||||||
|
|
||||||
- As a developer, I can store my Nexus key in ignored local config or set `NEXUS_API_KEY`, then run `nexus-claude-api start --claude-code` to get a Claude Code launch command.
|
- As a developer, I can store my Nexus key in user config with `nexus-claude-api config set --api-key <key>` or set `NEXUS_API_KEY`, then run `nexus-claude-api start --claude-code` to get a Claude Code launch command.
|
||||||
|
- As a contributor working from the repository, I can pass `--dev` to use current-directory development config and logs.
|
||||||
- As a Claude Code user, I can run coding workflows through local `http://127.0.0.1:4141`.
|
- As a Claude Code user, I can run coding workflows through local `http://127.0.0.1:4141`.
|
||||||
- As a Claude Code user, I can receive streaming model output.
|
- As a Claude Code user, I can receive streaming model output.
|
||||||
- As a Claude Code user, I can use tool calls and tool results.
|
- As a Claude Code user, I can use tool calls and tool results.
|
||||||
@@ -63,7 +64,12 @@ The proxy defaults to Opus because this deployment is intended for users whose N
|
|||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
- `uv run nexus-claude-api start --port 4141 --claude-code` starts a local server.
|
- `uv run nexus-claude-api start --port 4141 --claude-code` starts a local server.
|
||||||
|
- `nexus-claude-api config set --api-key <key>` writes user config to `~/.config/nexus-claude-api/config.json`.
|
||||||
- The server binds to `127.0.0.1` by default.
|
- The server binds to `127.0.0.1` by default.
|
||||||
|
- Default startup reads credentials from user config before environment variables.
|
||||||
|
- `--dev` startup reads current-directory `nexus-claude-api.local.json` instead of user config.
|
||||||
|
- Default logs are written to `~/.config/nexus-claude-api/logs/nexus-claude-api.log`.
|
||||||
|
- `--dev` logs are written to current-directory `logs/nexus-claude-api.log`.
|
||||||
- Missing Nexus credentials fail fast with a clear error.
|
- Missing Nexus credentials fail fast with a clear error.
|
||||||
- `GET /health` returns healthy status.
|
- `GET /health` returns healthy status.
|
||||||
- `GET /v1/models` returns the supported Claude models.
|
- `GET /v1/models` returns the supported Claude models.
|
||||||
@@ -76,8 +82,10 @@ The proxy defaults to Opus because this deployment is intended for users whose N
|
|||||||
|
|
||||||
## Security Requirements
|
## Security Requirements
|
||||||
|
|
||||||
- Do not persist API keys automatically.
|
- Do not persist API keys automatically during startup.
|
||||||
- If the user chooses hardcoded local configuration, keep it in ignored `nexus-claude-api.local.json`.
|
- Persist API keys only when the user explicitly runs `nexus-claude-api config set --api-key <key>` or manually writes a config file.
|
||||||
|
- Store normal user configuration in `~/.config/nexus-claude-api/config.json`.
|
||||||
|
- Use ignored current-directory `nexus-claude-api.local.json` only for explicit `--dev` repository-local development mode.
|
||||||
- Do not print or log API keys.
|
- Do not print or log API keys.
|
||||||
- Redact authorization headers in debug logs.
|
- Redact authorization headers in debug logs.
|
||||||
- Bind locally by default.
|
- Bind locally by default.
|
||||||
|
|||||||
@@ -54,16 +54,41 @@ Primary command:
|
|||||||
uv run nexus-claude-api start --port 4141 --claude-code
|
uv run nexus-claude-api start --port 4141 --claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
|
User configuration command:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
nexus-claude-api config set --api-key <key>
|
||||||
|
```
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
|
||||||
- `--host`: default `127.0.0.1`
|
- `--host`: default `127.0.0.1`
|
||||||
- `--port`, `-p`: default `4141`
|
- `--port`, `-p`: default `4141`
|
||||||
- `--endpoint-url`: default `https://genai-nexus.api.corpinter.net`
|
- `--endpoint-url`: default `https://genai-nexus.api.corpinter.net`
|
||||||
- `--api-key`: optional; fallback to ignored local config, `NEXUS_API_KEY`, then `AWS_BEARER_TOKEN_BEDROCK`
|
- `--api-key`: optional; overrides config files and environment variables
|
||||||
- `--model`: default `claude-opus-4.6`
|
- `--model`: default `claude-opus-4.6`
|
||||||
- `--small-model`: default `claude-opus-4.6`
|
- `--small-model`: default `claude-opus-4.6`
|
||||||
- `--claude-code`: print Claude Code launch command
|
- `--claude-code`: print Claude Code launch command
|
||||||
- `--verbose`, `-v`: debug logging without secrets
|
- `--verbose`, `-v`: debug logging without secrets
|
||||||
|
- `--dev`: use current-directory development config and logs
|
||||||
|
- `--dry-run`: validate config and print helper output without starting the server
|
||||||
|
|
||||||
|
Credential lookup order:
|
||||||
|
|
||||||
|
1. `--api-key`
|
||||||
|
2. Current-mode config file
|
||||||
|
3. `NEXUS_API_KEY`
|
||||||
|
4. `AWS_BEARER_TOKEN_BEDROCK`
|
||||||
|
|
||||||
|
Config file paths:
|
||||||
|
|
||||||
|
- Default mode: `~/.config/nexus-claude-api/config.json`
|
||||||
|
- Development mode with `--dev`: current-directory `nexus-claude-api.local.json`
|
||||||
|
|
||||||
|
Log file paths:
|
||||||
|
|
||||||
|
- Default mode: `~/.config/nexus-claude-api/logs/nexus-claude-api.log`
|
||||||
|
- Development mode with `--dev`: current-directory `logs/nexus-claude-api.log`
|
||||||
|
|
||||||
When `--claude-code` is used, print a PowerShell command that sets:
|
When `--claude-code` is used, print a PowerShell command that sets:
|
||||||
|
|
||||||
@@ -89,7 +114,7 @@ Expose:
|
|||||||
|
|
||||||
`ANTHROPIC_AUTH_TOKEN` is printed as `dummy` because Claude Code expects an Anthropic auth token variable to exist. This local proxy does not validate that inbound token by default. It is not the Nexus key.
|
`ANTHROPIC_AUTH_TOKEN` is printed as `dummy` because Claude Code expects an Anthropic auth token variable to exist. This local proxy does not validate that inbound token by default. It is not the Nexus key.
|
||||||
|
|
||||||
Inbound authentication headers are accepted for compatibility but not validated by default because the service is local. Outbound Nexus authentication uses `--api-key`, ignored local `nexus-claude-api.local.json`, `NEXUS_API_KEY`, or `AWS_BEARER_TOKEN_BEDROCK`.
|
Inbound authentication headers are accepted for compatibility but not validated by default because the service is local. Outbound Nexus authentication uses `--api-key`, the current-mode config file, `NEXUS_API_KEY`, or `AWS_BEARER_TOKEN_BEDROCK`.
|
||||||
|
|
||||||
## Model Mapping
|
## Model Mapping
|
||||||
|
|
||||||
@@ -182,7 +207,7 @@ Return Anthropic-compatible errors:
|
|||||||
Status mapping:
|
Status mapping:
|
||||||
|
|
||||||
- invalid request: `400`
|
- invalid request: `400`
|
||||||
- missing local Nexus credential: startup failure
|
- missing Nexus credential: startup failure
|
||||||
- Nexus auth failure: `401` or `403`
|
- Nexus auth failure: `401` or `403`
|
||||||
- Nexus throttling: `429`
|
- Nexus throttling: `429`
|
||||||
- Nexus network/timeout: `502` or `504`
|
- Nexus network/timeout: `502` or `504`
|
||||||
@@ -215,3 +240,14 @@ CLI tests:
|
|||||||
- `nexus-claude-api --help`
|
- `nexus-claude-api --help`
|
||||||
- Claude Code command generation.
|
- Claude Code command generation.
|
||||||
- Missing API key validation.
|
- Missing API key validation.
|
||||||
|
- User config writing with `nexus-claude-api config set --api-key <key>`.
|
||||||
|
- Default-mode user config lookup from `~/.config/nexus-claude-api/config.json`.
|
||||||
|
- Default mode ignores current-directory `nexus-claude-api.local.json`.
|
||||||
|
- Development mode uses current-directory `nexus-claude-api.local.json`.
|
||||||
|
- `--api-key` overrides config files and config files override environment variables.
|
||||||
|
|
||||||
|
Logging tests:
|
||||||
|
|
||||||
|
- Default-mode log path resolves to `~/.config/nexus-claude-api/logs/nexus-claude-api.log`.
|
||||||
|
- Development-mode log path resolves to current-directory `logs/nexus-claude-api.log`.
|
||||||
|
- Verbose logging enables debug details without logging secrets.
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import sys
|
|||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
|
import nexus_claude_api.config as config
|
||||||
from nexus_claude_api.config import (
|
from nexus_claude_api.config import (
|
||||||
DEFAULT_ENDPOINT_URL,
|
DEFAULT_ENDPOINT_URL,
|
||||||
DEFAULT_HOST,
|
DEFAULT_HOST,
|
||||||
@@ -13,7 +14,7 @@ from nexus_claude_api.config import (
|
|||||||
DEFAULT_SMALL_MODEL,
|
DEFAULT_SMALL_MODEL,
|
||||||
Settings,
|
Settings,
|
||||||
)
|
)
|
||||||
from nexus_claude_api.logging_config import configure_logging
|
from nexus_claude_api.logging_config import configure_logging, resolve_log_file
|
||||||
from nexus_claude_api.server import create_app
|
from nexus_claude_api.server import create_app
|
||||||
from nexus_claude_api.shell import generate_claude_code_powershell
|
from nexus_claude_api.shell import generate_claude_code_powershell
|
||||||
|
|
||||||
@@ -31,11 +32,21 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
start.add_argument("--small-model", default=DEFAULT_SMALL_MODEL)
|
start.add_argument("--small-model", default=DEFAULT_SMALL_MODEL)
|
||||||
start.add_argument("--claude-code", action="store_true")
|
start.add_argument("--claude-code", action="store_true")
|
||||||
start.add_argument("--verbose", "-v", action="store_true")
|
start.add_argument("--verbose", "-v", action="store_true")
|
||||||
|
start.add_argument(
|
||||||
|
"--dev",
|
||||||
|
action="store_true",
|
||||||
|
help="Use local development config and logs in the current directory.",
|
||||||
|
)
|
||||||
start.add_argument(
|
start.add_argument(
|
||||||
"--dry-run",
|
"--dry-run",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Validate config and print helper output without starting the server.",
|
help="Validate config and print helper output without starting the server.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
config = subparsers.add_parser("config", help="Manage user configuration")
|
||||||
|
config_subparsers = config.add_subparsers(dest="config_command")
|
||||||
|
config_set = config_subparsers.add_parser("set", help="Write user configuration")
|
||||||
|
config_set.add_argument("--api-key", required=True)
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
@@ -44,10 +55,21 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
if args.command == "start":
|
if args.command == "start":
|
||||||
return run_start(args)
|
return run_start(args)
|
||||||
|
if args.command == "config":
|
||||||
|
return run_config(args, parser)
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def run_config(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int:
|
||||||
|
if args.config_command == "set":
|
||||||
|
config_path = config.write_user_config({"api_key": args.api_key})
|
||||||
|
print(f"Wrote user config to {config_path}")
|
||||||
|
return 0
|
||||||
|
parser.parse_args(["config", "--help"])
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def run_start(args: argparse.Namespace) -> int:
|
def run_start(args: argparse.Namespace) -> int:
|
||||||
settings = Settings.from_values(
|
settings = Settings.from_values(
|
||||||
host=args.host,
|
host=args.host,
|
||||||
@@ -57,12 +79,15 @@ def run_start(args: argparse.Namespace) -> int:
|
|||||||
model=args.model,
|
model=args.model,
|
||||||
small_model=args.small_model,
|
small_model=args.small_model,
|
||||||
verbose=args.verbose,
|
verbose=args.verbose,
|
||||||
|
dev=args.dev,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not settings.api_key:
|
if not settings.api_key:
|
||||||
print(
|
print(
|
||||||
"Missing Nexus API key. Add nexus-claude-api.local.json, set "
|
"Missing Nexus API key. Run "
|
||||||
"NEXUS_API_KEY or AWS_BEARER_TOKEN_BEDROCK, or pass --api-key.",
|
"`nexus-claude-api config set --api-key <key>`, set NEXUS_API_KEY "
|
||||||
|
"or AWS_BEARER_TOKEN_BEDROCK, pass --api-key, or use --dev with "
|
||||||
|
"nexus-claude-api.local.json.",
|
||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
return 2
|
return 2
|
||||||
@@ -74,7 +99,10 @@ def run_start(args: argparse.Namespace) -> int:
|
|||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
log_file = configure_logging(verbose=settings.verbose)
|
log_file = configure_logging(
|
||||||
|
verbose=settings.verbose,
|
||||||
|
log_file=resolve_log_file(dev=args.dev),
|
||||||
|
)
|
||||||
print(f"Writing detailed logs to {log_file}")
|
print(f"Writing detailed logs to {log_file}")
|
||||||
app = create_app(settings)
|
app = create_app(settings)
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ DEFAULT_OPUS_MODEL = "claude-opus-4.6"
|
|||||||
DEFAULT_MODEL = DEFAULT_OPUS_MODEL
|
DEFAULT_MODEL = DEFAULT_OPUS_MODEL
|
||||||
DEFAULT_SMALL_MODEL = DEFAULT_OPUS_MODEL
|
DEFAULT_SMALL_MODEL = DEFAULT_OPUS_MODEL
|
||||||
LOCAL_CONFIG_FILENAME = "nexus-claude-api.local.json"
|
LOCAL_CONFIG_FILENAME = "nexus-claude-api.local.json"
|
||||||
|
USER_CONFIG_DIR = Path.home() / ".config" / "nexus-claude-api"
|
||||||
|
USER_CONFIG_FILE = USER_CONFIG_DIR / "config.json"
|
||||||
|
|
||||||
|
|
||||||
MODEL_ID_MAP = {
|
MODEL_ID_MAP = {
|
||||||
@@ -48,12 +50,13 @@ class Settings:
|
|||||||
small_model: str = DEFAULT_SMALL_MODEL,
|
small_model: str = DEFAULT_SMALL_MODEL,
|
||||||
verbose: bool = False,
|
verbose: bool = False,
|
||||||
require_api_key: bool = True,
|
require_api_key: bool = True,
|
||||||
|
dev: bool = False,
|
||||||
) -> "Settings":
|
) -> "Settings":
|
||||||
local_config = load_local_config()
|
config = load_config(dev=dev)
|
||||||
resolved_api_key = (
|
resolved_api_key = (
|
||||||
api_key
|
api_key
|
||||||
or local_config.get("api_key")
|
or config.get("api_key")
|
||||||
or local_config.get("nexus_api_key")
|
or config.get("nexus_api_key")
|
||||||
or os.environ.get("NEXUS_API_KEY")
|
or os.environ.get("NEXUS_API_KEY")
|
||||||
or os.environ.get("AWS_BEARER_TOKEN_BEDROCK")
|
or os.environ.get("AWS_BEARER_TOKEN_BEDROCK")
|
||||||
)
|
)
|
||||||
@@ -78,6 +81,30 @@ def resolve_backend_model(model: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def load_local_config(path: str | Path = LOCAL_CONFIG_FILENAME) -> dict[str, str]:
|
def load_local_config(path: str | Path = LOCAL_CONFIG_FILENAME) -> dict[str, str]:
|
||||||
|
return load_config_file(path)
|
||||||
|
|
||||||
|
|
||||||
|
def load_user_config(path: str | Path | None = None) -> dict[str, str]:
|
||||||
|
return load_config_file(path or USER_CONFIG_FILE)
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(*, dev: bool = False) -> dict[str, str]:
|
||||||
|
if dev:
|
||||||
|
return load_local_config()
|
||||||
|
return load_user_config()
|
||||||
|
|
||||||
|
|
||||||
|
def write_user_config(config: dict[str, str], path: str | Path | None = None) -> Path:
|
||||||
|
config_path = Path(path or USER_CONFIG_FILE)
|
||||||
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
config_path.write_text(
|
||||||
|
json.dumps(config, indent=2, sort_keys=True) + "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return config_path
|
||||||
|
|
||||||
|
|
||||||
|
def load_config_file(path: str | Path) -> dict[str, str]:
|
||||||
config_path = Path(path)
|
config_path = Path(path)
|
||||||
if not config_path.exists():
|
if not config_path.exists():
|
||||||
return {}
|
return {}
|
||||||
|
|||||||
@@ -3,9 +3,13 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import nexus_claude_api.config as config
|
||||||
|
|
||||||
LOG_DIR = Path("logs")
|
LOCAL_LOG_DIR = Path("logs")
|
||||||
LOG_FILE = LOG_DIR / "nexus-claude-api.log"
|
LOCAL_LOG_FILE = LOCAL_LOG_DIR / "nexus-claude-api.log"
|
||||||
|
USER_LOG_DIR = config.USER_CONFIG_DIR / "logs"
|
||||||
|
USER_LOG_FILE = USER_LOG_DIR / "nexus-claude-api.log"
|
||||||
|
LOG_FILE = USER_LOG_FILE
|
||||||
LOG_FORMAT = "%(asctime)s %(levelname)s %(name)s: %(message)s"
|
LOG_FORMAT = "%(asctime)s %(levelname)s %(name)s: %(message)s"
|
||||||
|
|
||||||
|
|
||||||
@@ -24,3 +28,9 @@ def configure_logging(*, verbose: bool = False, log_file: Path = LOG_FILE) -> Pa
|
|||||||
app_logger.propagate = False
|
app_logger.propagate = False
|
||||||
|
|
||||||
return log_file
|
return log_file
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_log_file(*, dev: bool = False) -> Path:
|
||||||
|
if dev:
|
||||||
|
return LOCAL_LOG_FILE
|
||||||
|
return config.USER_CONFIG_DIR / "logs" / "nexus-claude-api.log"
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from nexus_claude_api.cli import main
|
from nexus_claude_api.cli import main
|
||||||
from nexus_claude_api.config import Settings, load_local_config
|
from nexus_claude_api.config import (
|
||||||
|
Settings,
|
||||||
|
load_local_config,
|
||||||
|
load_user_config,
|
||||||
|
write_user_config,
|
||||||
|
)
|
||||||
from nexus_claude_api.shell import generate_claude_code_powershell
|
from nexus_claude_api.shell import generate_claude_code_powershell
|
||||||
|
|
||||||
|
|
||||||
@@ -41,6 +47,8 @@ def test_claude_code_command_uses_custom_model() -> None:
|
|||||||
|
|
||||||
def test_missing_api_key_fails(monkeypatch) -> None:
|
def test_missing_api_key_fails(monkeypatch) -> None:
|
||||||
tmp_path = _workspace_tmp("missing-key")
|
tmp_path = _workspace_tmp("missing-key")
|
||||||
|
user_config = tmp_path / ".config" / "nexus-claude-api" / "config.json"
|
||||||
|
monkeypatch.setattr("nexus_claude_api.config.USER_CONFIG_FILE", user_config)
|
||||||
monkeypatch.delenv("NEXUS_API_KEY", raising=False)
|
monkeypatch.delenv("NEXUS_API_KEY", raising=False)
|
||||||
monkeypatch.delenv("AWS_BEARER_TOKEN_BEDROCK", raising=False)
|
monkeypatch.delenv("AWS_BEARER_TOKEN_BEDROCK", raising=False)
|
||||||
monkeypatch.chdir(tmp_path)
|
monkeypatch.chdir(tmp_path)
|
||||||
@@ -54,8 +62,43 @@ def test_missing_api_key_fails(monkeypatch) -> None:
|
|||||||
assert exit_code == 2
|
assert exit_code == 2
|
||||||
|
|
||||||
|
|
||||||
def test_local_config_api_key(monkeypatch) -> None:
|
def test_user_config_api_key(monkeypatch) -> None:
|
||||||
tmp_path = _workspace_tmp("local-config")
|
tmp_path = _workspace_tmp("user-config")
|
||||||
|
user_config = tmp_path / ".config" / "nexus-claude-api" / "config.json"
|
||||||
|
monkeypatch.setattr("nexus_claude_api.config.USER_CONFIG_FILE", user_config)
|
||||||
|
write_user_config({"api_key": "user-test-key"})
|
||||||
|
|
||||||
|
settings = Settings.from_values(require_api_key=False)
|
||||||
|
|
||||||
|
assert settings.api_key == "user-test-key"
|
||||||
|
assert load_user_config()["api_key"] == "user-test-key"
|
||||||
|
shutil.rmtree(tmp_path, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_dev_local_config_api_key(monkeypatch) -> None:
|
||||||
|
tmp_path = _workspace_tmp("dev-local-config")
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
(tmp_path / "nexus-claude-api.local.json").write_text(
|
||||||
|
'{"api_key": "local-test-key"}',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
settings = Settings.from_values(require_api_key=False, dev=True)
|
||||||
|
|
||||||
|
assert settings.api_key == "local-test-key"
|
||||||
|
assert load_local_config()["api_key"] == "local-test-key"
|
||||||
|
finally:
|
||||||
|
monkeypatch.chdir(Path(__file__).parents[1])
|
||||||
|
shutil.rmtree(tmp_path, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_mode_ignores_local_config(monkeypatch) -> None:
|
||||||
|
tmp_path = _workspace_tmp("default-ignores-local")
|
||||||
|
user_config = tmp_path / ".config" / "nexus-claude-api" / "config.json"
|
||||||
|
monkeypatch.setattr("nexus_claude_api.config.USER_CONFIG_FILE", user_config)
|
||||||
|
monkeypatch.delenv("NEXUS_API_KEY", raising=False)
|
||||||
|
monkeypatch.delenv("AWS_BEARER_TOKEN_BEDROCK", raising=False)
|
||||||
monkeypatch.chdir(tmp_path)
|
monkeypatch.chdir(tmp_path)
|
||||||
(tmp_path / "nexus-claude-api.local.json").write_text(
|
(tmp_path / "nexus-claude-api.local.json").write_text(
|
||||||
'{"api_key": "local-test-key"}',
|
'{"api_key": "local-test-key"}',
|
||||||
@@ -65,13 +108,51 @@ def test_local_config_api_key(monkeypatch) -> None:
|
|||||||
try:
|
try:
|
||||||
settings = Settings.from_values(require_api_key=False)
|
settings = Settings.from_values(require_api_key=False)
|
||||||
|
|
||||||
assert settings.api_key == "local-test-key"
|
assert settings.api_key is None
|
||||||
assert load_local_config()["api_key"] == "local-test-key"
|
|
||||||
finally:
|
finally:
|
||||||
monkeypatch.chdir(Path(__file__).parents[1])
|
monkeypatch.chdir(Path(__file__).parents[1])
|
||||||
shutil.rmtree(tmp_path, ignore_errors=True)
|
shutil.rmtree(tmp_path, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_key_precedence_config_before_environment(monkeypatch) -> None:
|
||||||
|
tmp_path = _workspace_tmp("config-precedence")
|
||||||
|
user_config = tmp_path / ".config" / "nexus-claude-api" / "config.json"
|
||||||
|
monkeypatch.setattr("nexus_claude_api.config.USER_CONFIG_FILE", user_config)
|
||||||
|
monkeypatch.setenv("NEXUS_API_KEY", "env-key")
|
||||||
|
write_user_config({"api_key": "config-key"})
|
||||||
|
|
||||||
|
settings = Settings.from_values(require_api_key=False)
|
||||||
|
|
||||||
|
assert settings.api_key == "config-key"
|
||||||
|
shutil.rmtree(tmp_path, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_key_argument_overrides_config(monkeypatch) -> None:
|
||||||
|
tmp_path = _workspace_tmp("arg-precedence")
|
||||||
|
user_config = tmp_path / ".config" / "nexus-claude-api" / "config.json"
|
||||||
|
monkeypatch.setattr("nexus_claude_api.config.USER_CONFIG_FILE", user_config)
|
||||||
|
write_user_config({"api_key": "config-key"})
|
||||||
|
|
||||||
|
settings = Settings.from_values(api_key="arg-key", require_api_key=False)
|
||||||
|
|
||||||
|
assert settings.api_key == "arg-key"
|
||||||
|
shutil.rmtree(tmp_path, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_set_writes_user_config(monkeypatch) -> None:
|
||||||
|
tmp_path = _workspace_tmp("config-set")
|
||||||
|
user_config = tmp_path / ".config" / "nexus-claude-api" / "config.json"
|
||||||
|
monkeypatch.setattr("nexus_claude_api.config.USER_CONFIG_FILE", user_config)
|
||||||
|
|
||||||
|
exit_code = main(["config", "set", "--api-key", "written-key"])
|
||||||
|
|
||||||
|
assert exit_code == 0
|
||||||
|
assert json.loads(user_config.read_text(encoding="utf-8")) == {
|
||||||
|
"api_key": "written-key"
|
||||||
|
}
|
||||||
|
shutil.rmtree(tmp_path, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
def _workspace_tmp(name: str) -> Path:
|
def _workspace_tmp(name: str) -> Path:
|
||||||
path = Path(__file__).parents[1] / ".test-tmp" / name
|
path = Path(__file__).parents[1] / ".test-tmp" / name
|
||||||
shutil.rmtree(path, ignore_errors=True)
|
shutil.rmtree(path, ignore_errors=True)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from nexus_claude_api.logging_config import configure_logging
|
from nexus_claude_api.logging_config import configure_logging, resolve_log_file
|
||||||
|
|
||||||
|
|
||||||
def test_configure_logging_writes_app_logs_to_file_without_console(
|
def test_configure_logging_writes_app_logs_to_file_without_console(
|
||||||
@@ -55,3 +56,17 @@ def test_configure_logging_verbose_writes_debug_app_logs_to_file(
|
|||||||
app_logger.handlers.extend(original_handlers)
|
app_logger.handlers.extend(original_handlers)
|
||||||
app_logger.setLevel(original_level)
|
app_logger.setLevel(original_level)
|
||||||
app_logger.propagate = original_propagate
|
app_logger.propagate = original_propagate
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_log_file_uses_user_config_dir_by_default(
|
||||||
|
tmp_path,
|
||||||
|
monkeypatch,
|
||||||
|
) -> None:
|
||||||
|
user_config_dir = tmp_path / ".config" / "nexus-claude-api"
|
||||||
|
monkeypatch.setattr("nexus_claude_api.config.USER_CONFIG_DIR", user_config_dir)
|
||||||
|
|
||||||
|
assert resolve_log_file() == user_config_dir / "logs" / "nexus-claude-api.log"
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_log_file_uses_local_logs_in_dev_mode() -> None:
|
||||||
|
assert resolve_log_file(dev=True) == Path("logs") / "nexus-claude-api.log"
|
||||||
|
|||||||
Reference in New Issue
Block a user