Skip to content

feat: vision support, reasoning effort pass-through, cache metrics - #16

Open
dobexx wants to merge 3 commits into
wende:mainfrom
dobexx:feature/vision-effort
Open

feat: vision support, reasoning effort pass-through, cache metrics#16
dobexx wants to merge 3 commits into
wende:mainfrom
dobexx:feature/vision-effort

Conversation

@dobexx

@dobexx dobexx commented Aug 16, 2026

Copy link
Copy Markdown

What

Feature pass-through improvements, all live-tested in production (OpenWebUI → this proxy → claude-opus-5):

Vision support

  • image_url content blocks were silently dropped by extractText(). Now: base64 data URLs (what OpenWebUI sends) and remote http(s) URLs are staged as temp files; the prompt points Claude Code at them, and it views them via its Read tool
  • Bounded to 20 MB/image, temp dirs cleaned up after every request (finally-block)

Reasoning effort

  • reasoning_effort / effort request fields (low|medium|high|xhigh|max) → forwarded via the CLI's --effort flag, honored on resumed session turns too
  • Unknown values fall back to the CLI default

Cache metrics

  • Claude Code manages prompt caching automatically – but clients couldn't see it. usage now exposes cache_read_input_tokens / cache_creation_input_tokens (streaming + non-streaming)

Model normalization

  • Response model names keep the major version: claude-opus-5-...claude-opus-5 instead of collapsing everything to -4

Graceful first-run

  • Fresh deployments without credentials answer chat requests with actionable relogin guidance immediately, instead of running into the CLI's onboarding failure

Note: stacked on #15 (auth/admin layer) – merging in order avoids conflicts in routes.ts/manager.ts.

dobexx added 2 commits August 16, 2026 14:16
Security & operations layer for server deployments:

- API key auth (src/server/auth.ts): OpenAI-style Bearer via PROXY_API_KEY,
  timing-safe comparison, /health stays public. Generates a random key at
  startup when unset (never runs unprotected by accident); 'off' disables
  explicitly for local dev
- Minimal .env loader (no dependency, existing env vars win); PORT/HOST env
- Admin endpoints (src/server/admin.ts) protected by PROXY_ADMIN_KEY
  (falls back to PROXY_API_KEY; 403 when neither is set):
  - POST /admin/relogin/start + /complete + /status + /cancel: run
    'claude auth login' inside the deployment, return the OAuth URL, feed
    the code back via stdin. Re-authentication without SSH/container access.
    Verified live against Claude CLI 2.1.233 (plain 'auth login' is already
    headless-friendly: prints URL, waits for code on stdin).
  - GET /admin/usage: structured subscription usage via 'claude --print
    /usage' (no API cost, 60s cache)
- Runtime auth-failure detection: stderr signature matching (bounded 4 KB
  tail) + exit code; expired sessions return an actionable in-chat guidance
  message instead of a raw 500 (streaming + non-streaming)
- Startup logs credential state honestly instead of always-OK
- .env.example documents all variables
- Vision: image_url content blocks (base64 data URLs as sent by OpenWebUI,
  or remote http(s) URLs) are staged as temp files and referenced in the
  prompt; Claude Code views them via its Read tool. 20 MB per image bound,
  temp dirs cleaned up after every request. Live-tested with OpenWebUI +
  claude-opus-5.
- Reasoning effort: reasoning_effort / effort request fields (low, medium,
  high, xhigh, max) forwarded via --effort, including on resumed session
  turns. Unknown values fall back to the CLI default.
- Cache metrics: usage objects expose cache_read_input_tokens /
  cache_creation_input_tokens (streaming + non-streaming) so the CLI's
  automatic prompt caching becomes visible to clients.
- Model normalization keeps the major version in responses
  (claude-opus-5 no longer collapses to claude-opus-4).
- Graceful first-run: containers without credentials answer chat requests
  immediately with actionable relogin guidance instead of hitting the
  CLI's onboarding failure.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • The /admin/usage handler never populates usageCache, so the 60s caching branch will never be hit; consider assigning usageCache = { at: Date.now(), data: parsed } after parseUsage(stdout.toString()) so subsequent requests can use the cache.
  • In resolveCliInput you recompute openaiToCli(body) just to extract effort; you could call openaiToCli once, store the result, and reuse both for the initial and resumed-turn paths to avoid duplicated parsing work.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `/admin/usage` handler never populates `usageCache`, so the 60s caching branch will never be hit; consider assigning `usageCache = { at: Date.now(), data: parsed }` after `parseUsage(stdout.toString())` so subsequent requests can use the cache.
- In `resolveCliInput` you recompute `openaiToCli(body)` just to extract `effort`; you could call `openaiToCli` once, store the result, and reuse both for the initial and resumed-turn paths to avoid duplicated parsing work.

## Individual Comments

### Comment 1
<location path="src/server/routes.ts" line_range="286-289" />
<code_context>
     subprocess.on("content_delta", (event: ClaudeCliStreamEvent) => {
       const delta = event.event.delta;
-      const text = (delta?.type === "text_delta" && delta.text) || "";
+      let text = (delta?.type === "text_delta" && delta.text) || "";
+      // CLI surfaces auth failures as plain text on stdout in some failure
+      // modes - replace with actionable guidance
+      if (text && subprocess.hasAuthError()) {
+        text = AUTH_EXPIRED_MESSAGE;
+        isComplete = true;
</code_context>
<issue_to_address>
**issue (bug_risk):** Auth-expired guidance can be sent twice in streaming mode.

Because `content_delta` already replaces the text with `AUTH_EXPIRED_MESSAGE` and sets `isComplete = true` when `hasAuthError()` is true, and `close` also checks `hasAuthError()` and sends the full guidance plus `[DONE]`, the same guidance can be emitted twice in one stream. Please centralize this logic (e.g., handle it only in `close` or guard with an `authGuidanceSent` flag) so the message is sent exactly once.
</issue_to_address>

### Comment 2
<location path="src/server/admin.ts" line_range="242-251" />
<code_context>
+   */
+  router.get("/usage", (_req, res) => {
+    const now = Date.now();
+    if (usageCache && now - usageCache.at < 60_000) {
+      res.json({ ...usageCache.data, cached: true });
+      return;
+    }
+
+    execFile(
+      "claude",
+      ["--print", "/usage"],
+      { timeout: 20_000, maxBuffer: 64 * 1024 },
+      (err, stdout, stderr) => {
+        if (err) {
+          res.status(502).json({
+            error: {
+              message: `Usage query failed: ${stderr?.toString().trim() || err.message}`,
+              type: "server_error",
+              code: null,
+            },
+          });
+          return;
+        }
+        res.json({ ...parseUsage(stdout.toString()), cached: false });
+      }
+    );
</code_context>
<issue_to_address>
**issue (bug_risk):** Usage endpoint declares a cache but never populates it.

After a cache miss, the handler always returns `parseUsage(stdout)` with `cached: false` but never assigns to `usageCache`, so the cache is never populated and the hot path is never used. If you want a 60s cache, assign `usageCache = { at: Date.now(), data: parseUsage(stdout.toString()) }` after a successful `execFile` and return from that cached value on subsequent requests.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/server/routes.ts
Comment thread src/server/admin.ts
usageCache is now written after successful CLI calls; streaming auth
guidance is emitted exactly once via the error/close handlers.
@dobexx

dobexx commented Aug 16, 2026

Copy link
Copy Markdown
Author

Thanks for the review – both findings addressed in the latest commits:

  1. Usage cache: now populated after successful CLI calls (same fix as in feat: API key auth, .env config, admin relogin & usage endpoints #15, which this branch is stacked on).
  2. Duplicate auth guidance (streaming): content_delta no longer emits the guidance itself; the error/close handlers own it and send it exactly once.

@sourcery-ai review

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