Bump version to 0.1.2; update logging paths and enhance CLI with version command

This commit is contained in:
2026-06-27 14:32:52 +08:00
parent 2fc815b788
commit ac79dd0618
13 changed files with 161 additions and 33 deletions

View File

@@ -16,6 +16,7 @@ uv run nexus-claude-api start --port 4141 --claude-code
If the `nexus-claude-api` command is not on your `PATH`, run the module entrypoint instead: If the `nexus-claude-api` command is not on your `PATH`, run the module entrypoint instead:
```powershell ```powershell
py -m nexus_claude_api config set --api-key "your-nexus-api-key"
py -m nexus_claude_api start --port 4141 --claude-code py -m nexus_claude_api start --port 4141 --claude-code
``` ```

View File

@@ -68,8 +68,8 @@ The proxy defaults to Opus because this deployment is intended for users whose N
- 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. - Default startup reads credentials from user config before environment variables.
- `--dev` startup reads current-directory `nexus-claude-api.local.json` instead of user config. - `--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`. - Default logs are written to `~/.config/nexus-claude-api/logs/nexus-claude-api-YYYY-MM-DD.log`.
- `--dev` logs are written to current-directory `logs/nexus-claude-api.log`. - `--dev` logs are written to current-directory `logs/nexus-claude-api-YYYY-MM-DD.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.

View File

@@ -87,8 +87,8 @@ Config file paths:
Log file paths: Log file paths:
- Default mode: `~/.config/nexus-claude-api/logs/nexus-claude-api.log` - Default mode: `~/.config/nexus-claude-api/logs/nexus-claude-api-YYYY-MM-DD.log`
- Development mode with `--dev`: current-directory `logs/nexus-claude-api.log` - Development mode with `--dev`: current-directory `logs/nexus-claude-api-YYYY-MM-DD.log`
When `--claude-code` is used, print a PowerShell command that sets: When `--claude-code` is used, print a PowerShell command that sets:
@@ -248,6 +248,6 @@ CLI tests:
Logging tests: Logging tests:
- Default-mode log path resolves to `~/.config/nexus-claude-api/logs/nexus-claude-api.log`. - Default-mode log path resolves to `~/.config/nexus-claude-api/logs/nexus-claude-api-YYYY-MM-DD.log`.
- Development-mode log path resolves to current-directory `logs/nexus-claude-api.log`. - Development-mode log path resolves to current-directory `logs/nexus-claude-api-YYYY-MM-DD.log`.
- Verbose logging enables debug details without logging secrets. - Verbose logging enables debug details without logging secrets.

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "nexus-claude-api" name = "nexus-claude-api"
version = "0.1.1" version = "0.1.2"
description = "Local Anthropic-compatible Claude Code proxy for AI Nexus Bedrock Converse." description = "Local Anthropic-compatible Claude Code proxy for AI Nexus Bedrock Converse."
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
@@ -30,4 +30,3 @@ packages = ["src/nexus_claude_api"]
[tool.pytest.ini_options] [tool.pytest.ini_options]
testpaths = ["tests"] testpaths = ["tests"]
pythonpath = ["src"] pythonpath = ["src"]

View File

@@ -1,4 +1,3 @@
"""Local Anthropic-compatible proxy for AI Nexus Claude models.""" """Local Anthropic-compatible proxy for AI Nexus Claude models."""
__version__ = "0.1.0" __version__ = "0.1.2"

View File

@@ -5,6 +5,7 @@ import sys
import uvicorn import uvicorn
from nexus_claude_api import __version__
import nexus_claude_api.config as config 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,
@@ -21,6 +22,7 @@ from nexus_claude_api.shell import generate_claude_code_powershell
def build_parser() -> argparse.ArgumentParser: def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="nexus-claude-api") parser = argparse.ArgumentParser(prog="nexus-claude-api")
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
subparsers = parser.add_subparsers(dest="command") subparsers = parser.add_subparsers(dest="command")
start = subparsers.add_parser("start", help="Start the local API server") start = subparsers.add_parser("start", help="Start the local API server")
@@ -71,16 +73,20 @@ def run_config(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int
def run_start(args: argparse.Namespace) -> int: def run_start(args: argparse.Namespace) -> int:
settings = Settings.from_values( try:
host=args.host, settings = Settings.from_values(
port=args.port, host=args.host,
endpoint_url=args.endpoint_url, port=args.port,
api_key=args.api_key, endpoint_url=args.endpoint_url,
model=args.model, api_key=args.api_key,
small_model=args.small_model, model=args.model,
verbose=args.verbose, small_model=args.small_model,
dev=args.dev, verbose=args.verbose,
) dev=args.dev,
)
except ValueError as exc:
print(f"Invalid configuration: {exc}", file=sys.stderr)
return 2
if not settings.api_key: if not settings.api_key:
print( print(

View File

@@ -2,7 +2,8 @@ from __future__ import annotations
import logging import logging
import uuid import uuid
from typing import Annotated from collections.abc import Iterable, Iterator
from typing import Annotated, Any
from fastapi import APIRouter, Depends, Request from fastapi import APIRouter, Depends, Request
from fastapi.responses import JSONResponse, Response, StreamingResponse from fastapi.responses import JSONResponse, Response, StreamingResponse
@@ -60,6 +61,46 @@ def normalize_legacy_system_messages(raw: object) -> object:
return normalized return normalized
def anthropic_sse_stream(
stream: Iterable[dict[str, Any]],
*,
model: str,
correlation_id: str,
) -> Iterator[str]:
try:
for event in bedrock_stream_to_anthropic_events(stream, model=model):
yield sse_frame(event)
except NexusClaudeError as exc:
yield sse_frame(
{
"type": "error",
"error": {
"type": exc.error_type,
"message": f"{exc.message} [correlation_id={correlation_id}]",
"correlation_id": correlation_id,
},
}
)
except Exception:
logger.exception(
"anthropic_messages_stream_error correlation_id=%s",
correlation_id,
)
yield sse_frame(
{
"type": "error",
"error": {
"type": "api_error",
"message": (
"Unexpected error while streaming response "
f"[correlation_id={correlation_id}]"
),
"correlation_id": correlation_id,
},
}
)
@router.post("/v1/messages", response_model=None) @router.post("/v1/messages", response_model=None)
async def create_message( async def create_message(
request: Request, request: Request,
@@ -85,12 +126,10 @@ async def create_message(
correlation_id=correlation_id, correlation_id=correlation_id,
) )
return StreamingResponse( return StreamingResponse(
( anthropic_sse_stream(
sse_frame(event) stream,
for event in bedrock_stream_to_anthropic_events( model=payload.model,
stream, correlation_id=correlation_id,
model=payload.model,
)
), ),
media_type="text/event-stream", media_type="text/event-stream",
) )

View File

@@ -5,6 +5,7 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.requests import Request from fastapi.requests import Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from nexus_claude_api import __version__
from nexus_claude_api.config import Settings from nexus_claude_api.config import Settings
from nexus_claude_api.errors import NexusClaudeError, anthropic_error_response from nexus_claude_api.errors import NexusClaudeError, anthropic_error_response
from nexus_claude_api.nexus_client import NexusClient from nexus_claude_api.nexus_client import NexusClient
@@ -18,7 +19,7 @@ def create_app(
nexus_client: NexusClient | None = None, nexus_client: NexusClient | None = None,
) -> FastAPI: ) -> FastAPI:
resolved_settings = settings or Settings.from_values(require_api_key=False) resolved_settings = settings or Settings.from_values(require_api_key=False)
app = FastAPI(title="nexus-claude-api", version="0.1.0") app = FastAPI(title="nexus-claude-api", version=__version__)
app.state.settings = resolved_settings app.state.settings = resolved_settings
app.state.nexus_client = nexus_client or NexusClient(resolved_settings) app.state.nexus_client = nexus_client or NexusClient(resolved_settings)

View File

@@ -34,12 +34,11 @@ def anthropic_to_bedrock_request(payload: AnthropicMessagesRequest) -> dict[str,
inference_config["temperature"] = payload.temperature inference_config["temperature"] = payload.temperature
if payload.top_p is not None: if payload.top_p is not None:
inference_config["topP"] = payload.top_p inference_config["topP"] = payload.top_p
if payload.stop_sequences:
inference_config["stopSequences"] = payload.stop_sequences
if inference_config: if inference_config:
request["inferenceConfig"] = inference_config request["inferenceConfig"] = inference_config
if payload.stop_sequences:
request["stopSequences"] = payload.stop_sequences
tool_config = _tools_to_bedrock(payload) tool_config = _tools_to_bedrock(payload)
if tool_config: if tool_config:
request["toolConfig"] = tool_config request["toolConfig"] = tool_config
@@ -161,4 +160,3 @@ def _tools_to_bedrock(payload: AnthropicMessagesRequest) -> dict[str, Any] | Non
tool_config["toolChoice"] = {"tool": {"name": choice.name}} tool_config["toolChoice"] = {"tool": {"name": choice.name}}
return tool_config return tool_config

View File

@@ -4,6 +4,7 @@ import json
import shutil import shutil
from pathlib import Path from pathlib import Path
from nexus_claude_api import __version__
from nexus_claude_api.cli import main from nexus_claude_api.cli import main
from nexus_claude_api.config import ( from nexus_claude_api.config import (
Settings, Settings,
@@ -62,6 +63,38 @@ def test_missing_api_key_fails(monkeypatch) -> None:
assert exit_code == 2 assert exit_code == 2
def test_version_prints_package_version(capsys) -> None:
try:
main(["--version"])
except SystemExit as exc:
assert exc.code == 0
output = capsys.readouterr()
assert output.out.strip() == f"nexus-claude-api {__version__}"
def test_invalid_user_config_returns_clean_error(monkeypatch, capsys) -> None:
tmp_path = _workspace_tmp("invalid-user-config")
user_config = tmp_path / ".config" / "nexus-claude-api" / "config.json"
user_config.parent.mkdir(parents=True, exist_ok=True)
user_config.write_text("{not json", encoding="utf-8")
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)
try:
exit_code = main(["start", "--dry-run"])
finally:
monkeypatch.chdir(Path(__file__).parents[1])
shutil.rmtree(tmp_path, ignore_errors=True)
output = capsys.readouterr()
assert exit_code == 2
assert "Invalid configuration: Invalid JSON" in output.err
assert "Traceback" not in output.err
def test_user_config_api_key(monkeypatch) -> None: def test_user_config_api_key(monkeypatch) -> None:
tmp_path = _workspace_tmp("user-config") tmp_path = _workspace_tmp("user-config")
user_config = tmp_path / ".config" / "nexus-claude-api" / "config.json" user_config = tmp_path / ".config" / "nexus-claude-api" / "config.json"

View File

@@ -5,6 +5,7 @@ import logging
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from nexus_claude_api.errors import NexusClaudeError
from nexus_claude_api.config import Settings from nexus_claude_api.config import Settings
from nexus_claude_api.nexus_client import _map_client_error from nexus_claude_api.nexus_client import _map_client_error
from nexus_claude_api.server import create_app from nexus_claude_api.server import create_app
@@ -52,6 +53,21 @@ class AccessDeniedNexusClient:
) )
class StreamErrorNexusClient:
def converse_stream(self, request: dict, *, correlation_id: str | None = None):
assert correlation_id
def events():
yield {"contentBlockDelta": {"contentBlockIndex": 0, "delta": {"text": "Hi"}}}
raise NexusClaudeError(
"stream failed",
status_code=502,
error_type="api_error",
)
return events()
def client(nexus_client: object | None = None) -> TestClient: def client(nexus_client: object | None = None) -> TestClient:
settings = Settings.from_values(api_key="test", require_api_key=False) settings = Settings.from_values(api_key="test", require_api_key=False)
return TestClient( return TestClient(
@@ -183,6 +199,26 @@ def test_messages_stream() -> None:
assert "event: message_stop" in body assert "event: message_stop" in body
def test_messages_stream_returns_sse_error_when_stream_iteration_fails() -> None:
with client(StreamErrorNexusClient()).stream(
"POST",
"/v1/messages",
json={
"model": "claude-opus-4.6",
"messages": [{"role": "user", "content": "Hello"}],
"max_tokens": 32,
"stream": True,
},
) as response:
body = response.read().decode("utf-8")
assert response.status_code == 200
assert "event: content_block_delta" in body
assert "event: error" in body
assert "stream failed" in body
assert "correlation_id" in body
def test_count_tokens() -> None: def test_count_tokens() -> None:
response = client().post( response = client().post(
"/v1/messages/count_tokens", "/v1/messages/count_tokens",

View File

@@ -28,6 +28,22 @@ def test_text_request_translation() -> None:
assert request["inferenceConfig"]["temperature"] == 0.2 assert request["inferenceConfig"]["temperature"] == 0.2
def test_stop_sequences_are_translated_into_inference_config() -> None:
payload = AnthropicMessagesRequest.model_validate(
{
"model": "claude-opus-4.6",
"messages": [{"role": "user", "content": "Hello"}],
"max_tokens": 100,
"stop_sequences": ["STOP"],
}
)
request = anthropic_to_bedrock_request(payload)
assert request["inferenceConfig"]["stopSequences"] == ["STOP"]
assert "stopSequences" not in request
def test_image_request_translation() -> None: def test_image_request_translation() -> None:
image_data = base64.b64encode(b"image-bytes").decode("ascii") image_data = base64.b64encode(b"image-bytes").decode("ascii")
payload = AnthropicMessagesRequest.model_validate( payload = AnthropicMessagesRequest.model_validate(

2
uv.lock generated
View File

@@ -173,7 +173,7 @@ wheels = [
[[package]] [[package]]
name = "nexus-claude-api" name = "nexus-claude-api"
version = "0.1.1" version = "0.1.2"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "boto3" }, { name = "boto3" },