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/
|
||||
.test-tmp/
|
||||
pytest-cache-files-*/
|
||||
dist/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
@@ -17,4 +18,4 @@ nexus-claude-api.local.json
|
||||
.agents
|
||||
|
||||
# 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
|
||||
cd nexus-claude-api
|
||||
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
|
||||
```
|
||||
|
||||
@@ -32,11 +33,17 @@ The printed command ends with `claude --model <configured-model>` so the current
|
||||
Credential lookup order:
|
||||
|
||||
1. `--api-key`
|
||||
2. local `nexus-claude-api.local.json`
|
||||
2. user config `~/.config/nexus-claude-api/config.json`
|
||||
3. `NEXUS_API_KEY`
|
||||
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
|
||||
{
|
||||
@@ -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.
|
||||
|
||||
@@ -53,3 +68,4 @@ The service binds to `127.0.0.1` by default and does not persist API keys.
|
||||
- [PRD](docs/PRD.md)
|
||||
- [Requirements Design](docs/REQUIREMENTS_DESIGN.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
|
||||
|
||||
- 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 receive streaming model output.
|
||||
- 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
|
||||
|
||||
- `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.
|
||||
- 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.
|
||||
- `GET /health` returns healthy status.
|
||||
- `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
|
||||
|
||||
- Do not persist API keys automatically.
|
||||
- If the user chooses hardcoded local configuration, keep it in ignored `nexus-claude-api.local.json`.
|
||||
- Do not persist API keys automatically during startup.
|
||||
- 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.
|
||||
- Redact authorization headers in debug logs.
|
||||
- Bind locally by default.
|
||||
|
||||
@@ -54,16 +54,41 @@ Primary command:
|
||||
uv run nexus-claude-api start --port 4141 --claude-code
|
||||
```
|
||||
|
||||
User configuration command:
|
||||
|
||||
```powershell
|
||||
nexus-claude-api config set --api-key <key>
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
- `--host`: default `127.0.0.1`
|
||||
- `--port`, `-p`: default `4141`
|
||||
- `--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`
|
||||
- `--small-model`: default `claude-opus-4.6`
|
||||
- `--claude-code`: print Claude Code launch command
|
||||
- `--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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
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
|
||||
|
||||
@@ -182,7 +207,7 @@ Return Anthropic-compatible errors:
|
||||
Status mapping:
|
||||
|
||||
- invalid request: `400`
|
||||
- missing local Nexus credential: startup failure
|
||||
- missing Nexus credential: startup failure
|
||||
- Nexus auth failure: `401` or `403`
|
||||
- Nexus throttling: `429`
|
||||
- Nexus network/timeout: `502` or `504`
|
||||
@@ -215,3 +240,14 @@ CLI tests:
|
||||
- `nexus-claude-api --help`
|
||||
- Claude Code command generation.
|
||||
- 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 nexus_claude_api.config as config
|
||||
from nexus_claude_api.config import (
|
||||
DEFAULT_ENDPOINT_URL,
|
||||
DEFAULT_HOST,
|
||||
@@ -13,7 +14,7 @@ from nexus_claude_api.config import (
|
||||
DEFAULT_SMALL_MODEL,
|
||||
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.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("--claude-code", 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(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
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
|
||||
|
||||
|
||||
@@ -44,10 +55,21 @@ def main(argv: list[str] | None = None) -> int:
|
||||
args = parser.parse_args(argv)
|
||||
if args.command == "start":
|
||||
return run_start(args)
|
||||
if args.command == "config":
|
||||
return run_config(args, parser)
|
||||
parser.print_help()
|
||||
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:
|
||||
settings = Settings.from_values(
|
||||
host=args.host,
|
||||
@@ -57,12 +79,15 @@ def run_start(args: argparse.Namespace) -> int:
|
||||
model=args.model,
|
||||
small_model=args.small_model,
|
||||
verbose=args.verbose,
|
||||
dev=args.dev,
|
||||
)
|
||||
|
||||
if not settings.api_key:
|
||||
print(
|
||||
"Missing Nexus API key. Add nexus-claude-api.local.json, set "
|
||||
"NEXUS_API_KEY or AWS_BEARER_TOKEN_BEDROCK, or pass --api-key.",
|
||||
"Missing Nexus API key. Run "
|
||||
"`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,
|
||||
)
|
||||
return 2
|
||||
@@ -74,7 +99,10 @@ def run_start(args: argparse.Namespace) -> int:
|
||||
if args.dry_run:
|
||||
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}")
|
||||
app = create_app(settings)
|
||||
uvicorn.run(
|
||||
|
||||
@@ -13,6 +13,8 @@ DEFAULT_OPUS_MODEL = "claude-opus-4.6"
|
||||
DEFAULT_MODEL = DEFAULT_OPUS_MODEL
|
||||
DEFAULT_SMALL_MODEL = DEFAULT_OPUS_MODEL
|
||||
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 = {
|
||||
@@ -48,12 +50,13 @@ class Settings:
|
||||
small_model: str = DEFAULT_SMALL_MODEL,
|
||||
verbose: bool = False,
|
||||
require_api_key: bool = True,
|
||||
dev: bool = False,
|
||||
) -> "Settings":
|
||||
local_config = load_local_config()
|
||||
config = load_config(dev=dev)
|
||||
resolved_api_key = (
|
||||
api_key
|
||||
or local_config.get("api_key")
|
||||
or local_config.get("nexus_api_key")
|
||||
or config.get("api_key")
|
||||
or config.get("nexus_api_key")
|
||||
or os.environ.get("NEXUS_API_KEY")
|
||||
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]:
|
||||
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)
|
||||
if not config_path.exists():
|
||||
return {}
|
||||
|
||||
@@ -3,9 +3,13 @@ from __future__ import annotations
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import nexus_claude_api.config as config
|
||||
|
||||
LOG_DIR = Path("logs")
|
||||
LOG_FILE = LOG_DIR / "nexus-claude-api.log"
|
||||
LOCAL_LOG_DIR = Path("logs")
|
||||
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"
|
||||
|
||||
|
||||
@@ -24,3 +28,9 @@ def configure_logging(*, verbose: bool = False, log_file: Path = LOG_FILE) -> Pa
|
||||
app_logger.propagate = False
|
||||
|
||||
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
|
||||
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -41,6 +47,8 @@ def test_claude_code_command_uses_custom_model() -> None:
|
||||
|
||||
def test_missing_api_key_fails(monkeypatch) -> None:
|
||||
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("AWS_BEARER_TOKEN_BEDROCK", raising=False)
|
||||
monkeypatch.chdir(tmp_path)
|
||||
@@ -54,8 +62,43 @@ def test_missing_api_key_fails(monkeypatch) -> None:
|
||||
assert exit_code == 2
|
||||
|
||||
|
||||
def test_local_config_api_key(monkeypatch) -> None:
|
||||
tmp_path = _workspace_tmp("local-config")
|
||||
def test_user_config_api_key(monkeypatch) -> None:
|
||||
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)
|
||||
(tmp_path / "nexus-claude-api.local.json").write_text(
|
||||
'{"api_key": "local-test-key"}',
|
||||
@@ -65,13 +108,51 @@ def test_local_config_api_key(monkeypatch) -> None:
|
||||
try:
|
||||
settings = Settings.from_values(require_api_key=False)
|
||||
|
||||
assert settings.api_key == "local-test-key"
|
||||
assert load_local_config()["api_key"] == "local-test-key"
|
||||
assert settings.api_key is None
|
||||
finally:
|
||||
monkeypatch.chdir(Path(__file__).parents[1])
|
||||
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:
|
||||
path = Path(__file__).parents[1] / ".test-tmp" / name
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
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(
|
||||
@@ -55,3 +56,17 @@ def test_configure_logging_verbose_writes_debug_app_logs_to_file(
|
||||
app_logger.handlers.extend(original_handlers)
|
||||
app_logger.setLevel(original_level)
|
||||
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