feat: vision support, reasoning effort pass-through, cache metrics - #16
Open
dobexx wants to merge 3 commits into
Open
feat: vision support, reasoning effort pass-through, cache metrics#16dobexx wants to merge 3 commits into
dobexx wants to merge 3 commits into
Conversation
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.
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- The
/admin/usagehandler never populatesusageCache, so the 60s caching branch will never be hit; consider assigningusageCache = { at: Date.now(), data: parsed }afterparseUsage(stdout.toString())so subsequent requests can use the cache. - In
resolveCliInputyou recomputeopenaiToCli(body)just to extracteffort; you could callopenaiToClionce, 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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
usageCache is now written after successful CLI calls; streaming auth guidance is emitted exactly once via the error/close handlers.
Author
|
Thanks for the review – both findings addressed in the latest commits:
@sourcery-ai review |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Feature pass-through improvements, all live-tested in production (OpenWebUI → this proxy → claude-opus-5):
Vision support
image_urlcontent blocks were silently dropped byextractText(). 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 itsReadtoolReasoning effort
reasoning_effort/effortrequest fields (low|medium|high|xhigh|max) → forwarded via the CLI's--effortflag, honored on resumed session turns tooCache metrics
usagenow exposescache_read_input_tokens/cache_creation_input_tokens(streaming + non-streaming)Model normalization
claude-opus-5-...→claude-opus-5instead of collapsing everything to-4Graceful first-run
Note: stacked on #15 (auth/admin layer) – merging in order avoids conflicts in
routes.ts/manager.ts.