Skip to content

Tolerant content parts and mcp 1.30.0 (2.13.2) - #56

Closed
ttlequals0 wants to merge 96 commits into
RichardAtCT:mainfrom
ttlequals0:release/2.13.2
Closed

ttlequals0 wants to merge 96 commits into
RichardAtCT:mainfrom
ttlequals0:release/2.13.2

Conversation

@ttlequals0

Copy link
Copy Markdown

Why

Agentic clients (OpenClaw among them) send chat messages the strict text-only content model rejected with 422, and Dependabot #55 flags mcp below 1.30.0 for GHSA-84m7-p3x7-pcfv.

What changed

  • Content parts accept tool_use, tool_result and image entries; non-text parts are skipped and the text inside a tool_result is kept. Ported from fix: handle OpenClaw message format to resolve 422 validation errors #41 with the author credited on the commit.
  • mcp raised to 1.30.0 (kept below 2.0), lock refreshed; anyio, pyjwt and urllib3 move to current releases with it.
  • Version 2.13.2 across src/__init__.py, pyproject.toml, the changelog and the README pin.

Verification

  • 827 unit tests pass (4 new); the 7 failures are the live-server integration tests, unchanged from main.
  • black, bandit and pip-audit clean; mypy unchanged from main.
  • Image built for linux/amd64, scanned and deployed.

ttlequals0 and others added 30 commits January 30, 2026 19:52
- Add response_format parameter for OpenAI-compatible JSON mode
- Add ModelService for dynamic model fetching from Anthropic API
- Add claude-opus-4-5-20251101 model to supported models
- Add JSON extraction and enforcement methods to MessageAdapter
- Update docker-compose.yml to use published image
- Bump version to 2.3.0
Claude Code SDK was ignoring JSON_MODE_INSTRUCTION in the system prompt
and returning conversational text instead of JSON. Added JSON_PROMPT_SUFFIX
constant that is now appended to the user prompt alongside the system
prompt instruction, ensuring the model follows JSON output requirements.

Changes:
- Add JSON_PROMPT_SUFFIX constant to message_adapter.py
- Append suffix to user prompt in both streaming and non-streaming paths
- Update log messages to reflect dual-prompt approach
- Bump version to 2.3.1
- Updated JSON_MODE_INSTRUCTION with explicit first/last character rules
- Added explicit prohibition of markdown code blocks in instructions
- Updated JSON_PROMPT_SUFFIX with more concise output format
- Added log_json_structure() helper for debugging JSON responses
- Added boundary and structure logging in streaming/non-streaming paths
…-models

Add JSON response format support and dynamic model fetching
- Improve JSON mode instructions with numbered rules and explicit
  prohibition of preambles
- Add COMMON_PREAMBLES constant with 19 common Claude preambles
- Implement balanced brace/bracket matching algorithm that handles
  escaped quotes and braces inside strings correctly
- Add JsonExtractionResult dataclass and extract_json_with_metadata()
  for detailed extraction tracking
- Add enforce_json_format_with_metadata() for metadata-enabled
  JSON enforcement
- Add _log_extraction_diagnostics() for debugging extraction failures
- Create optional request deduplication cache with LRU eviction and TTL
- Add cache management endpoints: GET /v1/cache/stats, POST /v1/cache/clear
- Update version to 2.4.0
- Add comprehensive unit tests for all new functionality

The JSON extraction priority order is now:
1. Pure JSON (fast path)
2. Preamble removal + parse
3. Markdown code block extraction
4. Balanced brace/bracket matching
5. First-to-last fallback
- Add POST /v1/models/refresh to refresh models from Anthropic API at runtime
- Add GET /v1/models/status for service observability (source, count, last refresh)
- Track model source (api/fallback) and last refresh timestamp in ModelService
- Add comprehensive unit tests for refresh functionality

Version 2.4.1
- Model refresh now respects CLAUDE_AUTH_METHOD configuration
- Only 'anthropic' auth supports dynamic API fetch; others use static fallback
- Added auth_method field to /v1/models/refresh and /v1/models/status responses
- Updated CLAUDE_MODELS: added claude-opus-4-6, removed claude-opus-4-5-20250929
- Added model status/refresh endpoint cards to landing page UI
- Comprehensive unit tests for all auth methods
feat: JSON extraction improvements, request cache, and dynamic model refresh
…5.0)

- Add model metadata (context windows, output limits) and pricing from source
- Add claude-sonnet-4-6 and re-enable 3.x models confirmed supported
- Expand tool registry from 15 to 33 tools matching actual inventory
- Add retry module with exponential backoff and Opus-to-Sonnet fallback
- Add cost tracker with per-session accumulation and auto-cleanup
- Add X-Claude-Effort and X-Claude-Thinking header support
- Add model-specific max_tokens validation
- Extract shared options-building helper for streaming/non-streaming paths
- Rewrite README, trim historical migration docs
feat: v2.5.0 - models, tools, pricing from open-sourced Claude Code
- Replace generic landing page with clean utilitarian design
- Fix GitHub URL to ttlequals0/claude-code-openai-wrapper
- Fix OpenAPI docs version (was hardcoded 1.0.0, now dynamic)
- Add all 25 endpoints to landing page grouped by category
- Drop Pico CSS, use DM Sans + JetBrains Mono typography
- Bump version to 2.5.1
…date

feat: redesign landing page and update API docs (v2.5.1)
- Add JSON response mode documentation with usage example
- Expand API endpoints table from 14 to 25 entries, grouped by category
- Fix Installation git clone URL (was RichardAtCT, now ttlequals0)
- Bump version reference to 2.5.1
- Fix SDK version reference (removed pinned version, installed is 0.1.26)
- Fix production command (main.py does not exist, use claude-wrapper)
- Fix test command path (tests/test_endpoints.py not test_endpoints.py)
- Fix MAX_TIMEOUT units in Docker table (ms not seconds, 600000 not 300)
- Add missing env vars to config table (DEBUG_MODE, CORS_ORIGINS, etc.)
- Update temperature/top_p limitation (now applied via system prompt)
- Tighten prose, remove AI-ish phrasing
- Sync pyproject.toml version to 2.5.1
….5.2)

- Remove BashOutput, KillShell, SlashCommand (not in Claude Code registry)
- Add Brief, Config, ListPeers, REPL, Sleep, Monitor, SendUserFile,
  PushNotification, ListMcpResources, ReadMcpResource, VerifyPlanExecution
- Tool count: 33 -> 41, verified against Claude Code src/tools.ts
fix: remove fake tools, add missing real tools (v2.5.2)
…2.6.0)

- OpenAI function calling simulation via system prompt injection and
  response parsing (tools/tool_choice parameters, multi-turn support)
- JSON schema in response_format (type=json_schema with schema definition)
- Real-time streaming markdown fence stripping (JsonFenceStripper)
- CPU watchdog for Docker/Linux (WATCHDOG_ENABLED=true to enable)
- New models: ToolCall, FunctionCall, ToolDefinition, JsonSchema
- Message model extended with tool role, tool_calls, tool_call_id
- Extract duplicated JSON schema instructions to MessageAdapter.JSON_SCHEMA_TEMPLATE
- Remove no-op fence_str=fence assignments in JsonFenceStripper
- Fix filter_content(None) to return "" instead of None (type safety)
- Fix greedy bare JSON regex in parse_tool_calls (use json.loads validation)
- Add log when tools + json_mode both active in streaming
- Add precise return type annotation to parse_tool_calls
- Add tests: json_schema model, dict message conversion, nested array parsing
…-json-schema

feat: function calling, JSON schema, fence stripping, watchdog (v2.6.0)
…87;187;187m �[39msupported�[38;2;187;187;187m �[39mmodel�[38;2;187;187;187m �[39mlist�[38;2;102;102;102m,�[39m�[38;2;187;187;187m �[39madd�[38;2;187;187;187m �[39mClaude�[38;2;187;187;187m �[39mOpus�[38;2;187;187;187m �[39m�[38;2;102;102;102m4.7�[39m�[38;2;187;187;187m �[39m�[38;2;102;102;102m(�[39mv2�[38;2;102;102;102m.�[39m�[38;2;102;102;102m7.0�[39m�[38;2;102;102;102m)�[39m

Align�[38;2;187;187;187m �[39mCLAUDE_MODELS�[38;2;102;102;102m,�[39m�[38;2;187;187;187m �[39mMODEL_METADATA�[38;2;102;102;102m,�[39m�[38;2;187;187;187m �[39mMODEL_PRICING�[38;2;102;102;102m,�[39m�[38;2;187;187;187m �[39mand�[38;2;187;187;187m �[39mMODEL_FALLBACK_MAP
�[38;2;170;34;255;01mwith�[39;00m�[38;2;187;187;187m �[39mthe�[38;2;187;187;187m �[39mAnthropic�[38;2;187;187;187m �[39mmodels�[38;2;187;187;187m �[39mdocs�[38;2;187;187;187m �[39m�[38;2;170;34;255;01mas�[39;00m�[38;2;187;187;187m �[39mof�[38;2;187;187;187m �[39m�[38;2;102;102;102m2026�[39m�[38;2;102;102;102m-�[39m�[38;2;102;102;102m04�[39m�[38;2;102;102;102m-�[39m�[38;2;102;102;102m16�[39m�[38;2;102;102;102m.�[39m�[38;2;187;187;187m �[39mRemove�[38;2;187;187;187m �[39mthree�[38;2;187;187;187m �[39mmodels�[38;2;187;187;187m �[39malready
retired�[38;2;187;187;187m �[39mat�[38;2;187;187;187m �[39mthe�[38;2;187;187;187m �[39mAPI�[38;2;187;187;187m �[39mand�[38;2;187;187;187m �[39madd�[38;2;187;187;187m �[39mthe�[38;2;187;187;187m �[39m�[38;2;170;34;255;01mnew�[39;00m�[38;2;187;187;187m �[39mflagship�[38;2;187;187;187m �[39mOpus�[38;2;187;187;187m �[39m�[38;2;102;102;102m4.7�[39m�[38;2;102;102;102m.�[39m

�[38;2;102;102;102m-�[39m�[38;2;187;187;187m �[39mAdd�[38;2;187;187;187m �[39mclaude�[38;2;102;102;102m-�[39mopus�[38;2;102;102;102m-�[39m�[38;2;102;102;102m4�[39m�[38;2;102;102;102m-�[39m�[38;2;102;102;102m7�[39m�[38;2;187;187;187m �[39m�[38;2;102;102;102m(�[39m�[38;2;102;102;102m1�[39mM�[38;2;187;187;187m �[39mcontext�[38;2;102;102;102m,�[39m�[38;2;187;187;187m �[39m�[38;2;102;102;102m128�[39mK�[38;2;187;187;187m �[39mmax�[38;2;187;187;187m �[39moutput�[38;2;102;102;102m,�[39m�[38;2;187;187;187m �[39m$5�[38;2;102;102;102m/�[39m$25�[38;2;187;187;187m �[39mper�[38;2;187;187;187m �[39mMTok�[38;2;102;102;102m)�[39m
�[38;2;102;102;102m-�[39m�[38;2;187;187;187m �[39mRemove�[38;2;187;187;187m �[39mretired�[38;2;102;102;102m:�[39m�[38;2;187;187;187m �[39mclaude�[38;2;102;102;102m-�[39m�[38;2;102;102;102m3�[39m�[38;2;102;102;102m-�[39m�[38;2;102;102;102m7�[39m�[38;2;102;102;102m-�[39msonnet�[38;2;102;102;102m-�[39m�[38;2;102;102;102m20250219�[39m�[38;2;102;102;102m,�[39m�[38;2;187;187;187m �[39mclaude�[38;2;102;102;102m-�[39m�[38;2;102;102;102m3�[39m�[38;2;102;102;102m-�[39m�[38;2;102;102;102m5�[39m�[38;2;102;102;102m-�[39msonnet�[38;2;102;102;102m-�[39m�[38;2;102;102;102m20241022�[39m�[38;2;102;102;102m,�[39m
�[38;2;187;187;187m  �[39mclaude�[38;2;102;102;102m-�[39m�[38;2;102;102;102m3�[39m�[38;2;102;102;102m-�[39m�[38;2;102;102;102m5�[39m�[38;2;102;102;102m-�[39mhaiku�[38;2;102;102;102m-�[39m�[38;2;102;102;102m20241022�[39m
�[38;2;102;102;102m-�[39m�[38;2;187;187;187m �[39mFix�[38;2;187;187;187m �[39mcontext�[38;2;187;187;187m �[39mwindow�[38;2;187;187;187m �[39mto�[38;2;187;187;187m �[39m�[38;2;102;102;102m1�[39mM�[38;2;187;187;187m �[39m�[38;2;170;34;255;01mfor�[39;00m�[38;2;187;187;187m �[39mopus�[38;2;102;102;102m-�[39m�[38;2;102;102;102m4�[39m�[38;2;102;102;102m-�[39m�[38;2;102;102;102m7�[39m�[38;2;102;102;102m,�[39m�[38;2;187;187;187m �[39mopus�[38;2;102;102;102m-�[39m�[38;2;102;102;102m4�[39m�[38;2;102;102;102m-�[39m�[38;2;102;102;102m6�[39m�[38;2;102;102;102m,�[39m�[38;2;187;187;187m �[39msonnet�[38;2;102;102;102m-�[39m�[38;2;102;102;102m4�[39m�[38;2;102;102;102m-�[39m�[38;2;102;102;102m6�[39m
�[38;2;102;102;102m-�[39m�[38;2;187;187;187m �[39mFix�[38;2;187;187;187m �[39mmax�[38;2;187;187;187m �[39moutput�[38;2;187;187;187m �[39mto�[38;2;187;187;187m �[39m�[38;2;102;102;102m32�[39mK�[38;2;187;187;187m �[39m�[38;2;170;34;255;01mfor�[39;00m�[38;2;187;187;187m �[39mopus�[38;2;102;102;102m-�[39m�[38;2;102;102;102m4�[39m�[38;2;102;102;102m-�[39m�[38;2;102;102;102m1�[39m�[38;2;102;102;102m-�[39m�[38;2;102;102;102m20250805�[39m�[38;2;187;187;187m �[39mand�[38;2;187;187;187m �[39mopus�[38;2;102;102;102m-�[39m�[38;2;102;102;102m4�[39m�[38;2;102;102;102m-�[39m�[38;2;102;102;102m20250514�[39m
�[38;2;102;102;102m-�[39m�[38;2;187;187;187m �[39mFix�[38;2;187;187;187m �[39mmax�[38;2;187;187;187m �[39moutput�[38;2;187;187;187m �[39mto�[38;2;187;187;187m �[39m�[38;2;102;102;102m64�[39mK�[38;2;187;187;187m �[39m�[38;2;170;34;255;01mfor�[39;00m�[38;2;187;187;187m �[39msonnet�[38;2;102;102;102m-�[39m�[38;2;102;102;102m4�[39m�[38;2;102;102;102m-�[39m�[38;2;102;102;102m6�[39m�[38;2;187;187;187m �[39m�[38;2;102;102;102m(�[39msynchronous�[38;2;187;187;187m �[39mMessages�[38;2;187;187;187m �[39mAPI�[38;2;102;102;102m)�[39m
�[38;2;102;102;102m-�[39m�[38;2;187;187;187m �[39mSync�[38;2;187;187;187m �[39m�[38;2;102;102;102m.�[39m�[38;2;187;68;68menv�[39m�[38;2;102;102;102m.�[39m�[38;2;187;68;68mexample�[39m�[38;2;187;187;187m �[39mDEFAULT_MODEL�[38;2;187;187;187m �[39m�[38;2;170;34;255;01mwith�[39;00m�[38;2;187;187;187m �[39mcode�[38;2;187;187;187m �[39m�[38;2;170;34;255;01mdefault�[39;00m�[38;2;187;187;187m �[39m�[38;2;102;102;102m(�[39mclaude�[38;2;102;102;102m-�[39msonnet�[38;2;102;102;102m-�[39m�[38;2;102;102;102m4�[39m�[38;2;102;102;102m-�[39m�[38;2;102;102;102m6�[39m�[38;2;102;102;102m)�[39m
�[38;2;102;102;102m-�[39m�[38;2;187;187;187m �[39mUpdate�[38;2;187;187;187m �[39mlanding�[38;2;102;102;102m-�[39mpage�[38;2;187;187;187m �[39mquickstart�[38;2;187;187;187m �[39mand�[38;2;187;187;187m �[39mdebug�[38;2;187;187;187m �[39mexample�[38;2;187;187;187m �[39mto�[38;2;187;187;187m �[39mclaude�[38;2;102;102;102m-�[39msonnet�[38;2;102;102;102m-�[39m�[38;2;102;102;102m4�[39m�[38;2;102;102;102m-�[39m�[38;2;102;102;102m6�[39m
The existing rule only ignored test_roocode_compatibility.py.hypothesis/.
Pytest-hypothesis regenerates .hypothesis/ at the repo root on every run,
making it repeatedly show up as untracked.
Refresh supported model list, add Claude Opus 4.7 (v2.7.0)
…y user] as response content

Fixes the class of SDK failures where ResultMessage.subtype != "success"
fell through parse_claude_message and the synthetic
UserMessage('[Request interrupted by user]') was returned verbatim as the
assistant response. Raises ClaudeResultError instead, translated by the
HTTP layer to finish_reason="length" (error_max_turns) or a status-coded
error body (assistant rate_limit/billing/auth, generic SDK failure).

Also raises the OpenAI-compat max_turns default from 1 to 3 (env-configurable),
drops the max_tokens -> max_thinking_tokens misleading remap (opt-in via
env var), pins the SDK exactly, adds a circuit breaker and /healthz/deep
end-to-end probe, structured completion_result log line, multi-stage
Dockerfile dev/prod targets, BUILD_INFO stamp, and 13 new regression tests.

Upstream consumer affected was MinusPod; see that project's 2.0.12 notes
for the parallel defensive changes.
…ture CLI stderr

2.8.0 surfaced error_during_execution correctly but opened a tighter
secondary problem: the breaker tripped on a 5/10 intra-episode failure
burst, cascading into 503s for verification windows. Also discovered the
R5 structured-log extras were being dropped by the plain-text formatter
so circuit_breaker_open and completion_result logs shipped to Loki empty.

Three fixes, no new behavior:

- Inline key=value fields into log message strings via a _kv helper so
  default-formatter installs see the structured data.
- Raise CircuitBreakerConfig defaults to min_requests_for_trip=20 and
  failure_ratio_threshold=0.75. Add env-var overrides for every knob
  plus WRAPPER_CIRCUIT_BREAKER_ENABLED kill switch.
- Install stderr callback on ClaudeAgentOptions, ring-buffer 40 lines,
  emit + attach to ResultMessage dict on non-success. Propagates
  through ClaudeResultError.stderr_tail so the HTTP error path logs
  the real subprocess failure reason instead of just "num_turns=2".

12 new unit tests. Full suite 640 passed, 31 skipped.
Adds security-floor pins for starlette, urllib3, cryptography, pyjwt,
authlib, mcp, and nltk -- each is transitive via fastapi or claude-agent-sdk
but required a newer version than the parent's ceiling allowed. Widened
fastapi to >=0.119 to admit starlette 0.49.x (for CVE-2025-62727).

Clears 2 CRITICAL + 18 HIGH from the trivy scan against 2.8.1. Remaining
findings are nltk XML CVEs with no published fix and Debian base-image
packages that need a debian:13 rebase.

No code change. 640 tests pass on the new deps.
ttlequals0 and others added 29 commits August 30, 2026 18:47
/v1/usage reports the last quota state the CLI told us about, per window:
status, utilization, resets_at as unix and ISO, seconds_until_reset, the
source of the reading (passive or probe), and whether it has gone stale.
Windows appear only once reported, so an idle wrapper returns an empty set
rather than a guess.

Follows the */stats convention: bare dict, verify_api_key in the body,
@rate_limit_endpoint("general"), no response_model. /v1/auth/status gains a
"quota" key beside "cli_health", matching how that endpoint nests one key
per subsystem.

687 passed, 31 skipped; black clean.
Retry-After was hardcoded to "30" with a comment saying to use the upstream
reset "once the SDK exposes them". It does, and parse_claude_message now
carries resets_at through, so the header reflects the real window.

Capped at 3600s: a rejected seven_day window can reset days out, and a client
honouring the header verbatim would sleep through it. The true reset time
goes in the body as resets_at, resets_at_iso and seconds_until_reset. With no
reset reported the old conservative 30s default stands.

- /v1/messages raised HTTPException(502) for every subtype except
  error_max_turns, so a rate limit reached the Anthropic SDK as a broken
  upstream rather than a busy one. It now returns 429 with an Anthropic-shaped
  rate_limit_error. JSONResponse rather than HTTPException, because the global
  handler rewrites detail bodies to error.type=api_error.
- The streaming SSE error frame was a generic upstream_sdk_error regardless of
  subtype. The status line is already flushed by then, so the body is the only
  channel left for the reset time; rate limits now carry it.
- retry_delay never received the retry_after argument calculate_delay already
  accepted, so a known reset had nowhere to go. Wired through and bounded at
  60s, since this runs inside a live request. Retry status detection also
  falls back to ResultError.api_error_status.

687 passed, 31 skipped; black clean.
_check_quota_or_429 runs after the auth gate on /v1/chat/completions and
/v1/messages. Off by default via WRAPPER_QUOTA_ENFORCEMENT_ENABLED: refusing
requests is a behaviour change for existing callers, and the accurate
Retry-After on real upstream rejections ships either way. When on, a doomed
round-trip is skipped and the caller gets 429 with the reset time.

The gate counts every request for probe cadence whether or not enforcement is
on, so /v1/usage stays fresh regardless.

Probe cadence is request-count driven with a time floor
(WRAPPER_QUOTA_PROBE_EVERY_N_REQUESTS=100,
WRAPPER_QUOTA_PROBE_MIN_INTERVAL_SECONDS=300, 0 disables). The bundled CLI has
no usage subcommand, so a probe is a real inference call that spends the quota
it measures; a plain interval would burn quota while idle and still lag under
load. The loop reuses probe_cli_auth, whose verify_cli call records the
rate-limit event, so one probe refreshes both auth and quota state. It
complements rather than replaces the fixed-cadence auth probe, which already
keeps quota fresh when traffic is light.

687 passed, 31 skipped; black clean.
New tests/test_quota_tracker_unit.py (29 cases): recording from both the SDK
dataclass and an equivalent dict, per-window independence, overage handling
including the stale-overage case, blocking and expiry, the rejection with no
reported reset, snapshot shape and staleness, probe cadence, and env config
with defaults / overrides / invalid values.

Also covered:
- parse_claude_message against the nested rate_limit_info shape, the branch
  that has been unreachable until now, asserting resets_at and
  rate_limit_type reach ClaudeResultError.
- Retry-After derivation: reported reset, the 3600s cap on a week-long
  window, and the 30s fallback when no reset is reported.
- /v1/usage populated and idle.
- _check_quota_or_429 off by default, blocking with reset detail when on,
  passing on a healthy quota, and counting requests regardless.
- /v1/messages returning 429 with reset detail instead of 502.

730 passed, 31 skipped; black clean.
pip vendors its own copies of msgpack and setuptools, which have tripped the
trivy HIGH/CRITICAL gate on every build since 2.9.13 even though nothing
imports them. The container never installs packages at runtime, so pip is
removed from the app virtualenv, system site-packages, Poetry's own installer
venv, and the virtualenv wheel cache. Runs last, because poetry needs pip to
install, and asserts the app still imports afterwards.

Trivy on the resulting image: 28 HIGH/CRITICAL, all Debian 13 base-image CVEs
with no fix available, and zero language-package findings. Previously 31, of
which 2 were the pip-vendored pair.

Smoke tested: container starts, /health healthy, /version and /v1/usage
respond.
Without it the mount fails with permission denied on SELinux-enforcing hosts
(Fedora, RHEL) and under rootless Podman. The label is ignored where SELinux
is not enforcing, so plain Docker is unaffected.

Ported from RichardAtCT#49 by safrano9999.
Applied by hand rather than cherry-picked: this fork's docker-compose.yml has
diverged and the pick conflicts on the surrounding comment.
Subscription users who authenticate with `claude auth` had no way to reach
the containerised CLI without also setting an API key. The token is read by
the bundled CLI, not the wrapper, so passing it through is the whole change:
_detect_auth_method falls through to claude_cli when no ANTHROPIC_API_KEY or
Bedrock/Vertex flag is set, which is the correct path for OAuth.

Defaulted to empty so the variable stays unset when the host does not define
it, rather than being injected as an empty override.

Ported from RichardAtCT#50 by safrano9999.
Applied by hand rather than cherry-picked: this fork's docker-compose.yml and
.env.example have diverged and the pick conflicts.
Version bumped in src/__init__.py and pyproject.toml. CHANGELOG entry covers
the 401 misclassification fix and its production impact, the /v1/usage
endpoint, the Retry-After and 429 routing corrections, the SDK and
cryptography bumps, the pip strip, and the two upstream ports.

README: new env-var rows for the WRAPPER_QUOTA_* settings, /v1/usage in the
endpoint table, and a correction to the CLI_AUTH_PROBE_INTERVAL_SECONDS row,
which still claimed any probe failure returns 401. The version block was also
stale at 2.9.12, having been missed by both 2.9.13 and 2.9.14; test counts
updated from 673 to 730.

.env.example documents the four quota settings, commented out with defaults
shown, per existing convention.

730 passed, 31 skipped; black clean.
An audit of README against src/constants.py found the model tables had been
left behind by two releases. claude-fable-5, claude-opus-5 and claude-sonnet-5
were added in 2.9.11 and 2.9.12 but never documented, and claude-sonnet-4-6
was still labelled the default even though DEFAULT_MODEL_FALLBACK moved to
claude-sonnet-5. Deprecated-model replacements pointed at superseded targets,
and the Docker pin example still read 2.9.6.

Adds a Quota and Rate Limits section covering GET /v1/usage: the response
shape, window keys, what source and stale mean, and the 429 contract with its
capped Retry-After. The sample response was checked field by field against a
live snapshot rather than written by hand.

Endpoint and env-var tables were also diffed against every route in
src/main.py and every os.getenv in src/; both were already complete.
Trivy reported openssl CVE-2026-14456 (HIGH) as affecting the image, and
unlike the rest of the Debian findings it had a patched version available:
3.5.7-1~deb13u2 against the 3.5.6-1~deb13u2 the published python:3.12-slim
tag carries. apt-get upgrade picks up anything Debian has patched since the
base tag was cut.

Trivy on the result: 25 HIGH/CRITICAL, down from 28, none with a fix
available and none in language packages. Remaining findings are curl,
perl-base, ncurses, sqlite3, gzip, libacl1 and libssh2 with no Debian fix
published.
…mantics

v2.10.0: quota visibility, correct rate-limit semantics, and the 401 misclassification fix
/v1/usage was missing the seven_day window entirely and reported a null
utilization for every window. Both came from reading only the fields the SDK
models.

RateLimitInfo exposes the representative window plus the overage pool, but
the CLI sends every window under raw["unifiedWindows"], and that is the only
place utilization appears. There is no top-level utilization field, which is
why production showed null everywhere. Observed payload:

  {"status": "allowed", "rateLimitType": "five_hour", "resetsAt": 1788145200,
   "overageStatus": "rejected", "overageDisabledReason": "org_level_disabled",
   "unifiedWindows": {"five_hour": {"utilization": 0.29, ...},
                      "seven_day": {"utilization": 0.05, ...}}}

This mattered more than a missing field. The 2026-08-30 outage ran 13h26m,
far longer than a five-hour window can account for, so the weekly cap is the
likely cause, and seven_day was exactly the window not being reported.

- Every window in unifiedWindows is recorded with its own utilization and
  reset time, falling back to the modelled fields when the key is absent.
- status is set only on the window the CLI names representative. The payload
  carries no per-window status, and deriving one from utilization would be
  inventing data. Each window gains a representative flag.
- New closest_to_limit names the window nearest its cap, which is not always
  the one the CLI names, and is the number an operator actually wants.
- New binding_window names the representative window.
- overage carries disabled_reason, so org_level_disabled reads as the
  ordinary configuration it is rather than an outage.

738 passed, 31 skipped; black clean.
v2.10.1: report every rate-limit window, not just the one the CLI names
An exhausted subscription quota held the caller's connection through
10x60s inline retries per request, then returned 502 'SDK returned
success' with no reset time, because the CLI reports it as
ResultMessage(subtype='success', is_error=True) and raises prose the
classifier did not recognize.

Quota rejections now fail fast (no inline retry on HTTP 429 or
quota-shaped prose), classify as assistant_rate_limit so the existing
mapping answers 429 with Retry-After, and the reset hour the CLI names
('resets 6pm (UTC)') is parsed into a real epoch and recorded into the
quota tracker. The quota text markers gain 'session limit' and are
shared between the probe classifier and the completion error path.
Answer 429 with the real reset when the quota is exhausted (2.10.2)
Bumps the pip group with 1 update in the / directory: [nltk](https://github.com/nltk/nltk).


Updates `nltk` from 3.10.0 to 3.10.3
- [Release notes](https://github.com/nltk/nltk/releases)
- [Changelog](https://github.com/nltk/nltk/blob/develop/ChangeLog)
- [Commits](nltk/nltk@v3.10.0...v3.10.3)

---
updated-dependencies:
- dependency-name: nltk
  dependency-version: 3.10.3
  dependency-type: direct:production
  dependency-group: pip
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Pin anyio >=4.14.2 (locked 4.15.1) as a direct constraint to close
GHSA-82r6-8w77-94w6 (TLSStream IDNA 2003 host encoding enabling TLS
certificate spoofing, critical) and GHSA-5p39-cfhj-2xmp (process-pool
workers block on undrained stderr, medium). Dependabot cannot bump a
transitive dependency without a direct constraint.

Reconcile the version to 2.10.3 across __init__, pyproject, CHANGELOG,
and README (pyproject and README were stale at 2.10.1).
Bump claude-agent-sdk 0.2.148 to 0.2.152 (supersedes #45).

Drop the runtime nltk pin. nltk is only required by the dev-only safety
tool, and CVE-2026-81726 (GHSA-8mgp-746c-j5xp, path traversal in
model-artifact APIs) has no patched release. nltk is now dev-only and no
longer ships in the --only main production image.
…safety/nltk (#50)

- Add claude-opus-5-5 and claude-fable-5-1 with their own cache-read tiers; Sonnet 5 priced at the now-standard $2/$10.
- claude-agent-sdk 0.2.152 -> 0.2.157 (supersedes #49).
- Remove unused dev-only safety, the only parent of nltk (CVE-2026-81726, Dependabot #35, no upstream fix).
- README/CHANGELOG/.env.example audited against code.
Update the bundled SDK and validate its CLI during image builds. Classify version and authentication failures safely, patch PyJWT, and correct configuration documentation.
…ents

Clients such as OpenClaw send content arrays with non-text parts and
assistant messages with null content. The strict text-only part model
rejected them with 422. Non-text parts are now accepted and skipped, and
the text inside tool_result parts is kept.

Co-authored-by: Bhargav Donga <bhargavkumar.donga@gmail.com>
@ttlequals0 ttlequals0 closed this Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant