From 016848e7954397dadf57cb7ffb5fe4ab7e0f9e6e Mon Sep 17 00:00:00 2001 From: Xavier Puspus <36430014+xmpuspus@users.noreply.github.com> Date: Wed, 8 Jul 2026 06:23:08 -0700 Subject: [PATCH] Fix live design crash: llm_call telemetry line kwargs to stdlib logger (v1.6.1) The per-call telemetry log passed structlog-style kwargs (model=, tokens=, ...) to the stdlib logger returned by get_logger(). Under the CLI, which enables root INFO via configure_logging(), this raised TypeError: Logger._log() got an unexpected keyword argument 'model' right after the model responded and tokens were billed, so every live `cloudwright design`/`modify` produced a traceback and no spec. Latent since v1.1.0: pytest leaves the root logger at WARNING, so info() short- circuits and the line never emitted or crashed under tests. Switch both the Anthropic and OpenAI providers to %-style stdlib formatting. Side effect: LLM telemetry (model, latency, tokens, cache hits) now actually emits. Regression: test_llm_telemetry_logging.py runs both providers under configure_logging() and asserts no crash. --- CHANGELOG.md | 8 ++ packages/cli/cloudwright_cli/__init__.py | 2 +- packages/core/cloudwright/__init__.py | 2 +- packages/core/cloudwright/llm/anthropic.py | 13 ++-- packages/core/cloudwright/llm/openai.py | 11 +-- packages/core/pyproject.toml | 8 +- .../core/tests/test_llm_telemetry_logging.py | 74 +++++++++++++++++++ packages/mcp/cloudwright_mcp/__init__.py | 2 +- packages/web/cloudwright_web/__init__.py | 2 +- server.json | 4 +- 10 files changed, 105 insertions(+), 21 deletions(-) create mode 100644 packages/core/tests/test_llm_telemetry_logging.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c16b28..250795a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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: diff --git a/packages/cli/cloudwright_cli/__init__.py b/packages/cli/cloudwright_cli/__init__.py index e4adfb8..f49459c 100644 --- a/packages/cli/cloudwright_cli/__init__.py +++ b/packages/cli/cloudwright_cli/__init__.py @@ -1 +1 @@ -__version__ = "1.6.0" +__version__ = "1.6.1" diff --git a/packages/core/cloudwright/__init__.py b/packages/core/cloudwright/__init__.py index 6d25d0d..c4535e6 100644 --- a/packages/core/cloudwright/__init__.py +++ b/packages/core/cloudwright/__init__.py @@ -17,7 +17,7 @@ ValidationResult, ) -__version__ = "1.6.0" +__version__ = "1.6.1" __all__ = [ "Alternative", diff --git a/packages/core/cloudwright/llm/anthropic.py b/packages/core/cloudwright/llm/anthropic.py index e74fbce..d4e0edc 100644 --- a/packages/core/cloudwright/llm/anthropic.py +++ b/packages/core/cloudwright/llm/anthropic.py @@ -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: diff --git a/packages/core/cloudwright/llm/openai.py b/packages/core/cloudwright/llm/openai.py index e920677..176a527 100644 --- a/packages/core/cloudwright/llm/openai.py +++ b/packages/core/cloudwright/llm/openai.py @@ -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: diff --git a/packages/core/pyproject.toml b/packages/core/pyproject.toml index 58af9a1..708851f 100644 --- a/packages/core/pyproject.toml +++ b/packages/core/pyproject.toml @@ -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 = [ diff --git a/packages/core/tests/test_llm_telemetry_logging.py b/packages/core/tests/test_llm_telemetry_logging.py new file mode 100644 index 0000000..dbb72a7 --- /dev/null +++ b/packages/core/tests/test_llm_telemetry_logging.py @@ -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 diff --git a/packages/mcp/cloudwright_mcp/__init__.py b/packages/mcp/cloudwright_mcp/__init__.py index e4adfb8..f49459c 100644 --- a/packages/mcp/cloudwright_mcp/__init__.py +++ b/packages/mcp/cloudwright_mcp/__init__.py @@ -1 +1 @@ -__version__ = "1.6.0" +__version__ = "1.6.1" diff --git a/packages/web/cloudwright_web/__init__.py b/packages/web/cloudwright_web/__init__.py index 91db08f..b72f32c 100644 --- a/packages/web/cloudwright_web/__init__.py +++ b/packages/web/cloudwright_web/__init__.py @@ -1,6 +1,6 @@ """Cloudwright Web — FastAPI backend for architecture intelligence.""" -__version__ = "1.6.0" +__version__ = "1.6.1" def __getattr__(name: str): diff --git a/server.json b/server.json index 09504d2..92a50d0 100644 --- a/server.json +++ b/server.json @@ -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" @@ -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" }