Skip to content

Roll up #42/#43, GHCR image workflow, and durable SQLite sessions with SDK resume - #44

Closed
brandonros wants to merge 49 commits into
RichardAtCT:mainfrom
brandonros:main
Closed

brandonros wants to merge 49 commits into
RichardAtCT:mainfrom
brandonros:main

Conversation

@brandonros

@brandonros brandonros commented Apr 18, 2026 •

Copy link
Copy Markdown

Summary

This PR rolls up four streams of work into RichardAtCT:main:

  1. gustavokch's contributions (equivalent to #42, minus the Gemini proxy — see below)
  2. prodigy-sln's security audit (equivalent to #43)
  3. operator ergonomics: env knobs, GHCR image workflow, and landing-page cleanup
  4. durable sessions: SQLite-backed sessions with native Claude SDK resume

The branch is up to date with main, including #46 (live model list) and the ^0.2.159 SDK bump.

Conflicts between (1) and (2) were resolved by hand so both sets of fixes coexist (notes below). If #42 and #43 merge separately first, this PR still applies cleanly on top because it was built from the same merge bases.

What's included

From gustavokch (PR #42)

  • Critical bug fixes: continue_session → continue_conversation (sessions now actually continue), max_thinking_tokens wiring, real SDK token counts, proper stop_reason → OpenAI finish_reason mapping
  • Async session management: threading.Lock → asyncio.Lock; all SessionManager methods converted to async
  • Concurrency: removed the os.environ mutex — auth passed via options.env so SDK calls run fully concurrent
  • Perf: parallel CLI prewarming, MAX_CONCURRENT_PROCESSES semaphore
  • SDK options: reasoning_effort, response_format, thinking, max_budget_usd, user field forwarded
  • Streaming fixes: AssistantMessage / ContentBlockDelta handling, conversational fallbacks, timeout on query()
  • Session/model-switch fixes, content-filter refinement
  • Deps: poetry.lock + test suite updated for pydantic 2.13 / poetry 2.3

From prodigy-sln (PR #43)

  • Redaction of sensitive info in logs and validation errors (redact_key, redact_request_body, [REDACTED] debug strings)
  • Session limits: MAX_SESSIONS, MAX_SESSION_MESSAGES, SessionLimitExceeded, check_session_limit() pre-check for streaming paths
  • Rate limiting: TRUSTED_PROXIES / X-Forwarded-For validation via slowapi
  • CORS / endpoint hardening: CORS_ORIGINS, minimal /v1/auth/status (doesn't leak auth method), X-Claude-Model-Warning header for unrecognized models
  • Filesystem sandboxing: CLAUDE_CWD_ALLOWED_BASE for the CWD escape guard
  • Build: multi-stage Dockerfile + non-root user
  • Deps: SDK pin tightened (superseded by upstream's ^0.2.159)

Operator ergonomics (new in this PR)

  • CI: GitHub Actions workflow publishes container to ghcr.io/<owner>/claude-code-openai-wrapper on push-to-main / workflow_dispatch
  • Model list: static fallback list updated to the Claude 5.5 generation: claude-opus-5-5, claude-sonnet-5-5, claude-haiku-5-5 first, then Fable 5.x, Claude 5, and still-served 4.x models. Drops the retired claude-opus-4-1-20250805 and the invalid claude-opus-4-5-20250929 id. Fallback DEFAULT_MODEL is now claude-sonnet-5-5 and FAST_MODEL is claude-haiku-5-5. Dated snapshots of a listed alias count as recognized (no X-Claude-Model-Warning). CLAUDE_MODELS_OVERRIDE and FAST_MODEL themselves landed upstream in feat: dynamically refresh Anthropic model list #46
  • DISALLOWED_TOOLS env var — comma-separated tool slugs; default is [] so operators opt in explicitly
  • .env loading fix: constants.py and rate_limiter.py read env vars at import time, but load_dotenv() ran later in main.py/auth.py, so .env values for those settings were ignored. .env now loads once in src/__init__.py
  • Landing page: extracted the 591-line HTML f-string in src/main.py to src/templates/landing.html + string.Template-based substitution; main.py drops ~592 lines, JS/CSS braces no longer need {{ }} escaping

Durable sessions (new in this PR)

Sessions used to live in an in-memory dict, and every turn replayed the full history into a fresh SDK session.

  • SQLite persistence: sessions are stored in SESSION_DB_PATH (default ~/.claude-wrapper/sessions.db; :memory: disables) and restored on startup
  • Native SDK resume: each wrapper session records the SDK session that produced its last reply. When the next request's history extends it, the wrapper resumes that SDK session and sends only the new messages
  • Transcript mirroring: SDK transcripts are mirrored into the same SQLite file through a SessionStore adapter keyed by SDK session ID, so resume works after a restart even with a fresh temp workspace
  • Fallback: edited or regenerated history, a new system message, or a failed resume falls back to replaying the full history into a fresh SDK session
  • Limits: one replica per database file (use sticky sessions if you run more than one); past MAX_SESSION_MESSAGES the stored history is trimmed and turns fall back to replay

Explicitly excluded

  • Gemini CLI proxy: gustavokch's PR Fix proxy bugs, improve concurrency, and update SDK options #42 adds Gemini support; this PR deliberately removes it. Rationale: scope creep for a Claude wrapper. All Gemini-specific files (src/gemini_cli.py, tests, GEMINI_MODELS, GEMINI_API_KEY auth path, is_gemini message-adapter branches, the /v1/models Gemini listing, the Docker Node.js prewarm branch for Gemini) are gone. prodigy-sln's security fixes were re-verified intact after the removal.

Notable conflict resolutions

  • src/session_manager.py: took gustavokch's async rewrite, then ported prodigy's check_session_limit() to async and kept the max_messages field on Session. All callers of check_session_limit now await.
  • src/constants.py: kept CLAUDE_MODELS_OVERRIDE env mechanism and put claude-opus-4-7 at the head of the list; merged prodigy's fuller 4.x family + "Claude 4+ only" note.
  • src/main.py: kept prodigy's security hardenings (redacted debug logging, redact_request_body() on validation errors, minimal /v1/auth/status, model-warning header) while adopting gustavokch's real-metadata usage / stop_reason extraction.
  • Merge with upstream main (feat: dynamically refresh Anthropic model list #46, aaad0b9): took upstream's live model discovery and ^0.2.159 SDK pin; regenerated poetry.lock for python = "^3.12".

Test plan

  • Test suite passes locally: 553 passed, 30 skipped (includes 20 new tests in tests/test_session_persistence.py)
  • Live: resume from SQLite after deleting the local transcript and starting in a new temp workspace (model recalled context from the earlier turn)
  • Live: resuming a nonexistent SDK session falls back to a full-history replay and answers correctly
  • Live: claude-opus-5-5, claude-sonnet-5-5, claude-haiku-5-5 work through the SDK, with and without max_thinking_tokens
  • .env values for SESSION_DB_PATH, MAX_SESSIONS, FAST_MODEL now take effect
  • Manual: /v1/chat/completions non-streaming returns real token counts from SDK metadata
  • Manual: /v1/chat/completions streaming — no echo, proper finish_reason
  • Manual: /v1/messages (Anthropic shape) returns correct stop_reason + usage
  • Manual: session reuse across requests through the HTTP endpoints, including after a server restart
  • Manual: SessionLimitExceeded returns HTTP 429 on new session beyond MAX_SESSIONS (prodigy)
  • Manual: /v1/auth/status does not leak auth method to unauthenticated callers (prodigy FR-7.2)
  • Manual: CLAUDE_MODELS_OVERRIDE=foo,bar overrides /v1/models
  • Manual: landing page renders; JSON endpoints render in expandable accordions
  • Docker image builds on GHCR workflow

gustavokch and others added 30 commits March 16, 2026 16:00
…condition, and more

- Bug 1: Remove filter_content() from user input (silently stripped XML-like tags)
- Bug 2: Add asyncio.Lock to serialize os.environ mutation under concurrent requests
- Bug 3: Replace session message append with replace (prevent exponential duplication)
- Bug 4: Replace bare except: with except Exception: in 3 locations
- Bug 5: Use __version__ from src/__init__.py instead of hardcoded "1.0.0"
- Bug 7: Add verify_api_key() auth guard to /v1/auth/status endpoint
- Bug 8: Replace deprecated datetime.utcnow() with datetime.now(timezone.utc)
- Bug 9: Fix GitHub URL in landing page (aaronlippold → RichardAtCT)
- Bug 10: Mark Bash tool as is_safe=False
- Bug 11: Use DEFAULT_MODEL constant in debug endpoint example request
- Wrap async query() iteration with asyncio.timeout(self.timeout) to
  prevent indefinite hangs when the SDK subprocess stalls
- Change AnthropicMessagesRequest.enable_tools default to False so
  simple message requests don't trigger bypassPermissions + 10 turns
- Add diagnostic print() statements in /v1/messages handler to surface
  handler entry and run_completion call in server output
- Improve test_message.py: pipe server output to stderr, add
  DEBUG_MODE=true, reduce client timeout from 120s to 60s
PR 1 — Critical bug fixes:
- Fix continue_session → continue_conversation (sessions now actually continue)
- Wire max_thinking_tokens through to SDK via generic setattr approach
- Extract real token counts from SDK ResultMessage usage field
- Map stop_reason to proper finish_reason (max_tokens → length, etc.)

PR 2 — Concurrency & reliability:
- Remove os.environ mutex (_env_lock) — pass auth via options.env instead,
  allowing fully concurrent SDK calls (no more per-worker serialization)
- Replace threading.Lock with asyncio.Lock in SessionManager to avoid
  blocking the event loop; all session methods converted to async

PR 3 — SDK options wiring:
- Refactor run_completion to accept claude_options dict; apply via setattr
- Add reasoning_effort, response_format, thinking, max_budget_usd fields
- Forward user field to SDK
- Bump version to 2.3.0
Updated repository URL in the README file.
Lock file was out of sync with the pyproject.toml SDK version bump.
Docker build requires matching lock file.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The bundled claude CLI in claude-agent-sdk requires Node.js to run.
Without it, the SDK verification times out and API calls fail.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: Gemini CLI proxy support and interactive chat client
…ency-and-concurrency

Optimize CLI latency and add process concurrency cap
- Optimize prompt generation to only send new messages when resuming sessions
- Remove redundant 'Human:'/'Assistant:' prefixes for Gemini models
- Add prompt echo filtering to response content
- Update interactive chat client to use persistent session IDs
…choing

Fix Gemini history echoing and improve session continuity
- Implement content buffering and prompt stripping for Gemini streaming
- Set default max_thinking_tokens to 4000 for Claude 4 models
- Add improved logging for empty response cases
…nd-claude-response

Fix streaming echo and Claude response failures
- Support AssistantMessage and ContentBlockDelta message types in streaming
- Enhance MessageAdapter.filter_content to provide conversational fallbacks
- Prevent 'Unable to respond' errors by ensuring content_sent is correctly tracked
- Replace tool tags with placeholders instead of deleting them
- Support both <thinking> and <thought> tags
- Add raw content logging for easier debugging
- Update unit tests for new filtering behavior
…-chat-model-switch

Fix wrapper session handling for model switches
brandonros and others added 7 commits April 17, 2026 21:47
Publishes the wrapper's Dockerfile build to
ghcr.io/brandonros/claude-code-openai-wrapper on every push to main
and on workflow_dispatch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the stale list with the three generally-available models per
Anthropic's current model docs: Opus 4.7, Sonnet 4.6, Haiku 4.5.
Dropped deprecated Opus 4 / Sonnet 4 (retire 2026-06-15) and the
legacy 4.5 entries. Also fixed a wrong snapshot date that the list
had for Opus 4.5 (20250929 → was never a valid Opus 4.5 slug;
Anthropic publishes it as 20251101).

Order is used as-is by /v1/models, so the first entry (Opus 4.7)
is what clients like Open WebUI pick as their default.

DEFAULT_MODEL fallback moved from claude-sonnet-4-5-20250929 to
claude-sonnet-4-6 so model-less requests don't resolve to a slug
that's no longer in the supported set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
/v1/models has always returned a hardcoded list from constants.py
instead of proxying ${ANTHROPIC_BASE_URL}/v1/models. Two changes:

1. Runtime escape hatch: if CLAUDE_MODELS_OVERRIDE is set (comma-
   separated slugs), use it instead of the built-in list. Lets
   operators add or swap models without a fork edit + rebuild.
2. TODO block above DEFAULT_CLAUDE_MODELS documents why the list
   is hardcoded today and sketches the proxy-with-filter future
   (OpenRouter returns ~100 models; we'd want anthropic/* only).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prior commit aligned the inline comments with extra spaces; black
wants a single space before # and was failing CI on 94999a1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pyproject.toml's ^0.1.18 constraint already allows every patch up to
<0.2.0 — this just regenerates poetry.lock to the current latest.
45 patch bumps worth of fixes and features, notably:

- Session management: fork_session(), delete_session(), paginated listing
- task_budget option for token-budget control
- SystemPromptFile (--system-prompt-file)
- get_context_usage() on ClaudeSDKClient
- Annotated[...] parameter descriptions in @tool / create_sdk_mcp_server
- "auto" PermissionMode (parity with TS SDK + CLI 2.1.90+)
- Bundled Claude CLI updated to 2.1.105 as of 0.1.59

Wrapper's own tests should catch any incompatibilities; if CI goes
red we roll back the lock entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Blended prodigy-sln/feature/2026-03-29-security-audit-sdk (PR RichardAtCT#43 on
RichardAtCT) into our main.

Conflict resolutions:
- src/constants.py: kept our CLAUDE_MODELS_OVERRIDE env-var mechanism and
  added claude-opus-4-7 to the front of the default list; merged prodigy's
  fuller 4.x family list and the "Claude 4+ only" note.
- pyproject.toml: tightened claude-agent-sdk from ^0.1.18 to ^0.1.63 (we
  were already running that lock; prodigy's PR bumped to ^0.1.52).
- poetry.lock: regenerated via uv-run poetry against the new constraint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Blended gustavokch/main (PR RichardAtCT#42 on RichardAtCT) into our main, on top of
prodigy-sln's security audit.

Pulled in:
- Gemini CLI proxy support (+src/gemini_cli.py, GEMINI_MODELS)
- Async conversion of SessionManager (asyncio.Lock) for concurrency
- Latency optimizations (parallel prewarming, process semaphore)
- Real SDK token-count and stop_reason extraction
- Fixes for session model switches, streaming echo, history echoing
- Interactive chat client, setup scripts

Conflict resolutions:
- src/constants.py: kept our DEFAULT_CLAUDE_MODELS + CLAUDE_MODELS_OVERRIDE
  env mechanism, extended with gustavokch's fuller 4.x family list and his
  new GEMINI_MODELS block.
- src/session_manager.py: took gustavokch's async rewrite but ported
  prodigy's check_session_limit() to async and kept the max_messages
  Session field.
- src/main.py: kept prodigy's security hardenings — redacted debug logging,
  redact_request_body() for validation errors, minimal /v1/auth/status
  response, and X-Claude-Model-Warning header — while adopting gustavokch's
  real-metadata-based usage/stop_reason extraction. Caller of
  check_session_limit now awaits it.
- tests/test_session_manager_unit.py: prodigy's TestSessionManagerSessionLimit
  and TestCheckSessionLimit tests converted to async + await.
- pyproject.toml: tightened claude-agent-sdk to ^0.1.63.
- poetry.lock: regenerated against new constraint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
brandonros and others added 3 commits April 17, 2026 23:33
The prior merge pulled in Gemini CLI proxy support as a side-effect.
We only want the Claude path, so strip it entirely:

- deleted src/gemini_cli.py, tests/test_gemini_cli_unit.py, PR_GEMINI.md
- src/constants.py: removed GEMINI_MODELS block
- src/main.py: removed GeminiCodeCLI init, get_cli_for_model() router,
  active_cli indirection, Gemini prewarm branch, all is_gemini echo-
  stripping logic, Gemini chunk-format parsers, and /v1/models Gemini
  listing. claude_cli is now called directly.
- src/auth.py: removed "gemini" auth method (GEMINI_API_KEY /
  GOOGLE_API_KEY detection, _validate_gemini_auth, env-var forwarding).
  File now identical to prodigy-sln's version.
- src/message_adapter.py: removed is_gemini branches that suppressed
  Human:/Assistant: prefixes and the "Please continue" nudge.
- .env.example: removed GEMINI_API_KEY / GEMINI_CLI_PATH entries.
- tests/test_message_adapter_unit.py: dropped the two Gemini-only tests.
- examples/interactive_chat.py: renamed banner + example model to Claude.

Verified prodigy-sln's security fixes are all intact: redact_key,
redact_request_body, check_session_limit (now awaited), SessionLimitExceeded,
TRUSTED_PROXIES, CLAUDE_CWD_ALLOWED_BASE, X-Claude-Model-Warning,
CORS hardening, and [REDACTED] debug strings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- src/constants.py: FAST_MODEL now reads from env (mirrors DEFAULT_MODEL).
- src/constants.py: DEFAULT_DISALLOWED_TOOLS defaults to [] and is
  overridable via DISALLOWED_TOOLS env var (comma-separated, e.g.
  DISALLOWED_TOOLS=Task,WebFetch,WebSearch). Note: this relaxes the
  previous hard-coded security default — operators who want the old
  behaviour must set the env var explicitly.
- src/main.py: moved the 591-line landing-page HTML out of the root()
  f-string and into src/templates/landing.html. Loaded once at module
  init via string.Template ($-placeholders), so JS/CSS braces don't
  need the f-string {{/}} escaping anymore. main.py drops ~592 lines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
brandonros and others added 5 commits April 17, 2026 23:57
Resolve conflicts with dynamic model discovery (RichardAtCT#46) and SDK bump:
take upstream model resolution/discovery and SDK ^0.2.159, keep
claude-opus-4-7 in the static list, keep fork-side Path/__version__/
DEFAULT_MODEL usage, and regenerate poetry.lock for python ^3.12.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sessions were an in-memory dict and every turn replayed the full history
into a fresh SDK session. Now:

- SessionManager writes sessions to SQLite (SESSION_DB_PATH, default
  ~/.claude-wrapper/sessions.db; ":memory:" disables) and restores them
  on startup.
- Each wrapper session records the SDK session that produced its last
  reply. When the next request's history extends it, the turn resumes
  that SDK session and sends only the new messages; edited/regenerated
  history or a new system message falls back to a full replay.
- SDK transcripts are mirrored into the same database via a SessionStore
  adapter keyed by SDK session id, so resume works after restarts even
  with a fresh temp workspace.
- run_completion retries a resume that fails before any output as a
  fresh session with the full-history prompt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
constants.py and rate_limiter.py read env vars at import, but load_dotenv()
ran later in main.py/auth.py, so .env values for those settings were
ignored. Load it once in src/__init__.py instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brandonros brandonros changed the title github actions for container, CLAUDE_MODELS_OVERRIDE, bump sdk Roll up #42/#43, GHCR image workflow, and durable SQLite sessions with SDK resume Oct 14, 2026
@brandonros

Copy link
Copy Markdown
Author

Updated this branch:

  • Merged current main (feat: dynamically refresh Anthropic model list #46 and the ^0.2.159 SDK bump) and resolved the conflicts. CLAUDE_MODELS_OVERRIDE and FAST_MODEL now come from upstream.
  • Durable sessions: sessions are stored in SQLite and each one is backed by a native Claude SDK session. Follow-up turns resume it and send only the new messages instead of replaying the whole history, and this works across restarts.
  • .env loading fix: settings read at import time (MAX_SESSIONS, FAST_MODEL, etc.) were ignoring .env.

Tests: 553 passed, 30 skipped. The description is updated with details.

brandonros and others added 3 commits October 13, 2026 20:43
- Static fallback list now leads with claude-opus-5-5, claude-sonnet-5-5,
  claude-haiku-5-5, then Fable 5.x, Claude 5, and still-served 4.x models.
  Drops claude-opus-4-1-20250805 (retired 2026-08-05) and the invalid
  claude-opus-4-5-20250929 id.
- DEFAULT_MODEL fallback -> claude-sonnet-5-5, FAST_MODEL -> claude-haiku-5-5.
- Dated snapshots of a listed alias count as recognized models.
- Refresh README, .env.example, and landing-page examples.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Apple Silicon hosts (e.g. a Lima k3s VM) can't run the amd64-only image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With enable_tools=true, all four completion paths overwrote allowed_tools
and permission_mode after merging headers, so X-Claude-Allowed-Tools and
X-Claude-Permission-Mode had no effect, and the DISALLOWED_TOOLS env
default was never applied. Move the four copies into apply_tool_options,
which fills defaults only where headers left them unset. Disabled tools
still force single-turn Q&A regardless of headers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brandonros

Copy link
Copy Markdown
Author

Closing this PR. After using it as the backend for Open WebUI and for a Temporal workflow, I don't think the approach is the right one, and I'd rather explain why than leave it open.

What the project tries to do: make Claude Code look like an OpenAI Chat Completions model, so any OpenAI client can use it.

Why that doesn't hold up:

  1. For plain chat, it isn't needed. Anthropic's API already serves Claude to OpenAI-style clients (https://api.anthropic.com/v1), and Open WebUI connects to Anthropic natively. If you want a model behind a chat box, call the model.
  2. For agent use, the Chat Completions format can't carry what matters. Claude Code's value is the agent: tool calls and their results, thinking, subagents, permission prompts, interruption, and a session with real tool context. Chat Completions has room for one final text answer. Everything that makes an agent understandable and controllable gets dropped, and the client is left guessing.
  3. Session state ends up in two places. The client holds a message history and the server holds a Claude session. Neither is complete, and keeping them in sync is guesswork. Small bugs come out of that, such as system prompts silently dropped, errors returned as successful replies, and a "tools off" mode that isn't. But they're symptoms. The mismatch is in the design.
  4. The Agent SDK is already the integration point. It's a library meant to be embedded. Wrapping it in a lossy chat API and then re-encoding agent behaviour through headers and extension fields adds a layer without adding capability.

What I'd do instead:

  • A chat UI with plain Claude: connect it to Anthropic's API directly.
  • A chat UI driving Claude Code: run the Agent SDK in a small service that streams its own events. Then give the UI a thin adapter (for Open WebUI, a pipe) that renders tool calls, results and approvals with the UI's built-in components.
  • Background workflows: embed the Agent SDK in the workflow's activity, or call that same event-streaming service. Not a chat endpoint.
  • Or let Anthropic host it: Managed Agents provides the agent loop, sandbox, sessions and an event stream.

Thanks for the project. It was a useful way to discover all of this.

@brandonros brandonros closed this Oct 17, 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.

3 participants