Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.6.1] - 2026-07-08

Hotfix for a crash in the flagship command, found by the July 2026 product audit.

### Fixed

- **`cloudwright design` no longer crashes on a live API key.** The per-call telemetry line passed structlog-style keyword arguments (`model=`, `tokens=`, ...) to a stdlib logger, so the CLI (which enables root INFO logging) raised `TypeError: Logger._log() got an unexpected keyword argument 'model'` right after the model responded and tokens were billed, producing no spec. The line now uses `%`-style stdlib formatting in both the Anthropic and OpenAI providers. Latent since v1.1.0: the test suite leaves the root logger at WARNING, so the line silently never emitted and never crashed under pytest. Regression test `test_llm_telemetry_logging.py` runs both providers under `configure_logging()`. As a side effect, per-call LLM telemetry (model, latency, tokens, cache hits) now actually emits.

## [1.6.0] - 2026-06-16

This release closes the defensibility + relevance gaps from the June 2026 product audit:
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/cloudwright_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.6.0"
__version__ = "1.6.1"
2 changes: 1 addition & 1 deletion packages/core/cloudwright/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
ValidationResult,
)

__version__ = "1.6.0"
__version__ = "1.6.1"

__all__ = [
"Alternative",
Expand Down
13 changes: 7 additions & 6 deletions packages/core/cloudwright/llm/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,13 +190,14 @@ def _call(
"cached_tokens": cache_read,
"cache_creation_tokens": cache_write,
}
# stdlib logger: kwargs-style fields crash Logger._log, use %-style
log.info(
"llm_call",
model=model,
duration_ms=round((time.perf_counter() - start) * 1000),
tokens=usage["input_tokens"] + usage["output_tokens"],
cache_read=cache_read,
cache_write=cache_write,
"llm_call model=%s duration_ms=%s tokens=%s cache_read=%s cache_write=%s",
model,
round((time.perf_counter() - start) * 1000),
usage["input_tokens"] + usage["output_tokens"],
cache_read,
cache_write,
)
return response.content[0].text, usage
except _RETRYABLE:
Expand Down
11 changes: 6 additions & 5 deletions packages/core/cloudwright/llm/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,12 +217,13 @@ def _call(
"output_tokens": response.usage.completion_tokens,
"cached_tokens": cached,
}
# stdlib logger: kwargs-style fields crash Logger._log, use %-style
log.info(
"llm_call",
model=model,
duration_ms=round((time.perf_counter() - start) * 1000),
tokens=usage["input_tokens"] + usage["output_tokens"],
cached=cached,
"llm_call model=%s duration_ms=%s tokens=%s cached=%s",
model,
round((time.perf_counter() - start) * 1000),
usage["input_tokens"] + usage["output_tokens"],
cached,
)
return content, usage
except _RETRYABLE:
Expand Down
8 changes: 4 additions & 4 deletions packages/core/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ dependencies = [
]

[project.optional-dependencies]
cli = ["cloudwright-ai-cli==1.6.0"]
web = ["cloudwright-ai-cli==1.6.0", "cloudwright-ai-web==1.6.0"]
mcp = ["cloudwright-ai-mcp==1.6.0"]
all = ["cloudwright-ai-cli==1.6.0", "cloudwright-ai-web==1.6.0", "cloudwright-ai-mcp==1.6.0", "databricks-sdk>=0.38.0"]
cli = ["cloudwright-ai-cli==1.6.1"]
web = ["cloudwright-ai-cli==1.6.1", "cloudwright-ai-web==1.6.1"]
mcp = ["cloudwright-ai-mcp==1.6.1"]
all = ["cloudwright-ai-cli==1.6.1", "cloudwright-ai-web==1.6.1", "cloudwright-ai-mcp==1.6.1", "databricks-sdk>=0.38.0"]
pdf = ["weasyprint", "markdown2"]
databricks = ["databricks-sdk>=0.38.0"]
live-import = [
Expand Down
74 changes: 74 additions & 0 deletions packages/core/tests/test_llm_telemetry_logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Regression: the llm_call telemetry line must survive configure_logging().

The CLI enables root INFO via configure_logging() on every invocation. Passing
structlog-style kwargs to the stdlib logger returned by get_logger() raised
TypeError: Logger._log() got an unexpected keyword argument 'model'
AFTER the SDK call returned, so every live design/modify billed tokens and then
crashed. pytest masked it because root stays at WARNING and info() short-circuits.
These tests pin the fix by running the providers with root at INFO.
"""

from __future__ import annotations

import logging as stdlib_logging
from unittest.mock import MagicMock

import cloudwright.logging as cw_logging
import pytest
from cloudwright.llm.anthropic import AnthropicLLM
from cloudwright.llm.openai import OpenAILLM


@pytest.fixture()
def info_level_logging():
root = stdlib_logging.getLogger()
saved_handlers = root.handlers[:]
saved_level = root.level
saved_flag = cw_logging._configured
cw_logging._configured = False
cw_logging.configure_logging()
yield
root.handlers[:] = saved_handlers
root.setLevel(saved_level)
cw_logging._configured = saved_flag


def _anthropic_response(text="ok"):
response = MagicMock()
response.content = [MagicMock(text=text)]
response.usage.input_tokens = 10
response.usage.output_tokens = 5
response.usage.cache_read_input_tokens = 0
response.usage.cache_creation_input_tokens = 0
return response


def _openai_response(text="ok"):
response = MagicMock()
response.choices = [MagicMock(message=MagicMock(content=text))]
response.usage.prompt_tokens = 10
response.usage.completion_tokens = 5
response.usage.prompt_tokens_details = MagicMock(cached_tokens=0)
return response


def test_anthropic_call_survives_configure_logging(info_level_logging):
llm = AnthropicLLM(api_key="test")
llm.client = MagicMock()
llm.client.messages.create.return_value = _anthropic_response("result")

text, usage = llm.generate([{"role": "user", "content": "hi"}], "system")

assert text == "result"
assert usage["input_tokens"] == 10


def test_openai_call_survives_configure_logging(info_level_logging):
llm = OpenAILLM(api_key="test")
llm.client = MagicMock()
llm.client.chat.completions.create.return_value = _openai_response("result")

text, usage = llm.generate([{"role": "user", "content": "hi"}], "system")

assert text == "result"
assert usage["input_tokens"] == 10
2 changes: 1 addition & 1 deletion packages/mcp/cloudwright_mcp/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.6.0"
__version__ = "1.6.1"
2 changes: 1 addition & 1 deletion packages/web/cloudwright_web/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Cloudwright Web — FastAPI backend for architecture intelligence."""

__version__ = "1.6.0"
__version__ = "1.6.1"


def __getattr__(name: str):
Expand Down
4 changes: 2 additions & 2 deletions server.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "io.github.xmpuspus/cloudwright",
"description": "Natural-language cloud architecture: deployable Terraform/Pulumi, cost, compliance control mapping.",
"version": "1.6.0",
"version": "1.6.1",
"repository": {
"url": "https://github.com/xmpuspus/cloudwright",
"source": "github"
Expand All @@ -12,7 +12,7 @@
"registryType": "pypi",
"registryBaseUrl": "https://pypi.org",
"identifier": "cloudwright-ai-mcp",
"version": "1.6.0",
"version": "1.6.1",
"transport": {
"type": "stdio"
}
Expand Down
Loading