Bump version to 0.1.2; update logging paths and enhance CLI with version command
This commit is contained in:
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
2
uv.lock
generated
@@ -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" },
|
||||||
|
|||||||
Reference in New Issue
Block a user