umans-gate 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/LICENSE +21 -0
  3. package/README.md +354 -0
  4. package/dashboard/dist/assets/alert-dialog-DlhAyxD3.js +2 -0
  5. package/dashboard/dist/assets/alert-dialog-DlhAyxD3.js.map +1 -0
  6. package/dashboard/dist/assets/clear-confirm-dialog-BOcl93wD.js +2 -0
  7. package/dashboard/dist/assets/clear-confirm-dialog-BOcl93wD.js.map +1 -0
  8. package/dashboard/dist/assets/config-tab-pnBjiSyC.js +4 -0
  9. package/dashboard/dist/assets/config-tab-pnBjiSyC.js.map +1 -0
  10. package/dashboard/dist/assets/economics-tab-BJXa5ydD.js +2 -0
  11. package/dashboard/dist/assets/economics-tab-BJXa5ydD.js.map +1 -0
  12. package/dashboard/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  13. package/dashboard/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  14. package/dashboard/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  15. package/dashboard/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  16. package/dashboard/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  17. package/dashboard/dist/assets/headers-viewer-C9FKdG-_.js +2 -0
  18. package/dashboard/dist/assets/headers-viewer-C9FKdG-_.js.map +1 -0
  19. package/dashboard/dist/assets/index-72wCsW4_.js +3 -0
  20. package/dashboard/dist/assets/index-72wCsW4_.js.map +1 -0
  21. package/dashboard/dist/assets/index-DFhG4t-C.css +1 -0
  22. package/dashboard/dist/assets/json-viewer-CXkZMoVn.js +2 -0
  23. package/dashboard/dist/assets/json-viewer-CXkZMoVn.js.map +1 -0
  24. package/dashboard/dist/assets/mode-toggle-BxkGFiYG.js +2 -0
  25. package/dashboard/dist/assets/mode-toggle-BxkGFiYG.js.map +1 -0
  26. package/dashboard/dist/assets/models-tab-Deqk3WT3.js +2 -0
  27. package/dashboard/dist/assets/models-tab-Deqk3WT3.js.map +1 -0
  28. package/dashboard/dist/assets/performance-meter-Bd152C4C.js +2 -0
  29. package/dashboard/dist/assets/performance-meter-Bd152C4C.js.map +1 -0
  30. package/dashboard/dist/assets/select-mZPf2BR3.js +2 -0
  31. package/dashboard/dist/assets/select-mZPf2BR3.js.map +1 -0
  32. package/dashboard/dist/assets/sheet-BgQXtbdw.js +2 -0
  33. package/dashboard/dist/assets/sheet-BgQXtbdw.js.map +1 -0
  34. package/dashboard/dist/assets/sonner-DGu1DGeO.js +2 -0
  35. package/dashboard/dist/assets/sonner-DGu1DGeO.js.map +1 -0
  36. package/dashboard/dist/assets/sse-viewer-BHnn3FcY.js +3 -0
  37. package/dashboard/dist/assets/sse-viewer-BHnn3FcY.js.map +1 -0
  38. package/dashboard/dist/assets/use-capture-done-listener-u5RxfYQP.js +2 -0
  39. package/dashboard/dist/assets/use-capture-done-listener-u5RxfYQP.js.map +1 -0
  40. package/dashboard/dist/assets/use-models-B4MLkfpt.js +2 -0
  41. package/dashboard/dist/assets/use-models-B4MLkfpt.js.map +1 -0
  42. package/dashboard/dist/assets/use-polling-resource-D_vWHFwW.js +2 -0
  43. package/dashboard/dist/assets/use-polling-resource-D_vWHFwW.js.map +1 -0
  44. package/dashboard/dist/assets/vendor-CVKsgXJU.js +312 -0
  45. package/dashboard/dist/assets/vendor-CVKsgXJU.js.map +1 -0
  46. package/dashboard/dist/assets/vendor-DtX4s2-2.css +1 -0
  47. package/dashboard/dist/assets/vendor-react-Dtud3GyP.js +41 -0
  48. package/dashboard/dist/assets/vendor-react-Dtud3GyP.js.map +1 -0
  49. package/dashboard/dist/assets/vision-calls-BquNiDzm.js +3 -0
  50. package/dashboard/dist/assets/vision-calls-BquNiDzm.js.map +1 -0
  51. package/dashboard/dist/index.html +34 -0
  52. package/dist/cli.d.ts +1 -0
  53. package/dist/cli.js +6788 -0
  54. package/dist/cli.js.map +1 -0
  55. package/dist/index.d.ts +1094 -0
  56. package/dist/index.js +6443 -0
  57. package/dist/index.js.map +1 -0
  58. package/package.json +85 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,113 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-07-10
11
+
12
+ ### Added
13
+
14
+ - **Vision handoff pipeline**: replaces image blocks with text descriptions
15
+ generated by a separate vision model (`umans-flash`). Images are transcoded
16
+ to JPEG/PNG, sent to the vision model, and descriptions are cached (7-day TTL)
17
+ with persistent storage. Strategies: `always`, `catalog`, `never`.
18
+ - Modules: `src/vision/` (`handoff.ts`, `detect.ts`, `cache.ts`,
19
+ `persistent-cache.ts`, `transcode.ts`, `wrapper.ts`, `sink.ts`)
20
+ - `src/vision-description-store.ts` — persistent description storage in SQLite
21
+ - **Concurrency gate** (`src/limiter/`): semaphore + circuit breaker with
22
+ intention-based reservations (main vs vision), hard cap, soft limit driven by
23
+ `/v1/usage`, queue timeout, and over-subscription fallback.
24
+ - `src/limiter/gate.ts` — `ConcurrencyGate`, `Semaphore`, `CircuitBreaker`
25
+ - `src/limiter/types.ts` — gate option types
26
+ - **Rate limiting** (`src/rate.ts`): sliding-window weighted rate limiter for
27
+ pro-tier request limits. Auto-derives from `/v1/usage` or explicit config.
28
+ - **Connection warmer** (`src/warmer.ts`): periodic `/v1/models` pings to keep
29
+ TLS warm. Skips when real traffic occurred in the last interval.
30
+ - **Usage tracking** (`src/usage.ts`, `src/usage-extract.ts`): fetches and
31
+ reconciles `/v1/usage` data to size concurrency limits, detect rate-boxing,
32
+ and demote priority. Extractors for Anthropic + OpenAI streaming/non-streaming.
33
+ - **Economics module** (`src/economics.ts`): token cost calculation per capture.
34
+ - **Metrics module** (`src/metrics.ts`): aggregated runtime statistics.
35
+ - **Model info parser** (`src/model-info-parser.ts`): parses `/v1/models`
36
+ response to determine vision support per model.
37
+ - **Model policy** (`src/model-policy.ts`): model-aware stamping decisions.
38
+ - **Compressed capture storage** (`src/compress.ts`): zstd compression for
39
+ request/response bodies (default: on).
40
+ - **Worker-based capture pipeline** (`src/workers/`): offload capture writes to
41
+ a worker thread for non-blocking streaming.
42
+ - **Stamp pipeline** (`src/stamp-pipeline.ts`): unified stamping orchestrator
43
+ that applies TTL, `top_k`, `max_tokens`, `thinking`, `output_config`,
44
+ `context_management`, and `temperature` stamps in the correct order.
45
+ - **Individual stamp modules**:
46
+ - `src/stamp-temperature.ts` — forces `temperature` value
47
+ - `src/stamp-thinking.ts` — injects `thinking`, `max_tokens`, `output_config`
48
+ - `src/stamp-topk.ts` — injects `top_k`
49
+ - `src/stamp-reasoning.ts` — OpenAI-compatible `reasoning_effort` stamping
50
+ - **Bundled stamp toggle**: `stamp_claude_code_enabled` replaces individual
51
+ stamp toggles — one switch applies the full Claude Code stamp bundle.
52
+ - **Dashboard config validation**: dispatch-table-based validation with
53
+ hot-reload support and restart-required field detection.
54
+ - **Dashboard polling consolidation**: `usePollingResource` hook unifies
55
+ triplicated polling logic.
56
+ - **WebSocket backpressure management**: configurable limit and auto-close.
57
+ - **Upstream timeout** (`upstream_timeout_ms`): configurable fetch timeout.
58
+ - **Capture body size limit** (`capture_body_max_bytes`).
59
+ - **Logger** (`src/logger.ts`): structured scoped logging replacing `console.*`.
60
+ - **Shared utilities** (`src/shared/`): extracted domain helpers for
61
+ model-name parsing, usage fetching, dashboard constants, and vision sink.
62
+
63
+ ### Changed
64
+
65
+ - **SOLID refactor (Waves 1–5)**: split monolithic modules into focused,
66
+ single-responsibility units. Helpers split into domain modules, `ConcurrencyGate`
67
+ decomposed into `Semaphore` + `CircuitBreaker`, `useCaptures` split into
68
+ `useCaptureList` + `useCaptureDetail` + `useGateStats`, config validation
69
+ replaced if/else chains with dispatch tables.
70
+ - **Stamp toggles consolidated**: individual `stamp_cache_ttl_enabled`,
71
+ `stamp_top_k_enabled`, `stamp_max_tokens_enabled`, `stamp_thinking_enabled`,
72
+ `stamp_output_config_enabled` → single `stamp_claude_code_enabled`.
73
+ - **Vision strategy default**: `always` → `catalog` (only intercept when model
74
+ lacks vision support).
75
+ - **Vision image format default**: `jpeg` → `png` (higher fidelity for OCR).
76
+ - **Vision cache TTL default**: 24h → 7 days (`604800000ms`).
77
+ - **`max_tokens` stamp values**: GLM models get `131071`, non-GLM get `32767`.
78
+ - **Config validation**: now uses dispatch table instead of if/else chains.
79
+ - **Dashboard capture detail**: copy-status derived from source state.
80
+ - **Usage extraction**: replaced provider×streaming dispatch with `EXTRACTORS`
81
+ lookup table.
82
+ - **ConnectionWarmer**: narrowed constructor to `Pick<ProxyConfig, ...>`.
83
+
84
+ ### Fixed
85
+
86
+ - Streaming `duration_ms` now floors at wall-clock to prevent timing collapse
87
+ - Limiter allows permit acquisition when reservations are over-subscribed
88
+ - `stamp-reasoning` correctly removes `max_tokens`/`thinking` before injection
89
+
90
+ ## [0.0.1] - 2025-07-02
91
+
92
+ ### Added
93
+
94
+ - TypeScript rewrite of the capture proxy (modular `src/` structure)
95
+ - Vite + React + Tailwind + shadcn/ui dashboard
96
+ - `createProxyServer()` programmatic API
97
+ - Biome for linting and formatting
98
+ - GitHub Actions CI (matrix: ubuntu + macOS, Bun 1.1 + latest)
99
+ - npm publish workflow with Sigstore provenance
100
+ - Issue templates, PR template, dependabot config
101
+ - `.env.example` documenting all environment variables
102
+ - MIT license
103
+
104
+ ### Changed
105
+
106
+ - Single-file `capture.js` → modular TypeScript in `src/`
107
+ - Vanilla JS dashboard → React + shadcn/ui dashboard
108
+ - Test helpers reference new entry point (no more `proxy.js`/`capture-only.js`)
109
+
110
+ ### Fixed
111
+
112
+ - Broken tests that spawned non-existent `proxy.js` file
113
+ - Runtime artifacts (`capture.db*`) now gitignored
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Umans AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,354 @@
1
+ # umans-gate
2
+
3
+ [![npm version](https://img.shields.io/npm/v/umans-gate.svg)](https://www.npmjs.com/package/umans-gate)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![Runtime: Bun](https://img.shields.io/badge/Runtime-Bun-fbf0df.svg)](https://bun.sh)
6
+ [![CI](https://github.com/umans-ai/umans-gate/actions/workflows/ci.yml/badge.svg)](https://github.com/umans-ai/umans-gate/actions/workflows/ci.yml)
7
+
8
+ > **Disclaimer:** This is a community project. The author is not an official
9
+ > Umans developer — just a contributor building tools around the Umans API.
10
+ > "umans" and related marks belong to their respective owners.
11
+
12
+ LLM capture proxy with Anthropic `cache_control` TTL stamping, vision handoff,
13
+ concurrency gating, rate limiting, and a live inspection dashboard.
14
+
15
+ Point your LLM harness at `http://localhost:1945`, and every request/response is
16
+ captured to SQLite with a live WebSocket-updating inspector UI at
17
+ `http://localhost:1945/dashboard/`.
18
+
19
+ ## Table of Contents
20
+
21
+ - [Features](#features)
22
+ - [Quick Start](#quick-start)
23
+ - [Configuration](#configuration)
24
+ - [Config Fields](#config-fields)
25
+ - [Hot Reload and Restart](#hot-reload-and-restart)
26
+ - [Stamp Pipeline](#stamp-pipeline)
27
+ - [Vision Handoff](#vision-handoff)
28
+ - [Concurrency Gate](#concurrency-gate)
29
+ - [Rate Limiting](#rate-limiting)
30
+ - [Programmatic API](#programmatic-api)
31
+ - [Inspector Dashboard](#inspector-dashboard)
32
+ - [Development](#development)
33
+ - [Project Structure](#project-structure)
34
+ - [Documentation](#documentation)
35
+ - [Contributing](#contributing)
36
+ - [License](#license)
37
+
38
+ ## Features
39
+
40
+ - **Capture proxy**: intercepts all LLM API traffic (Anthropic, OpenAI-compatible) and stores it in SQLite with optional zstd compression
41
+ - **Stamp pipeline**: unified stamping orchestrator that applies TTL, `top_k`, `max_tokens`, `thinking`, `output_config`, `context_management`, and `temperature` stamps in the correct order — toggled by a single `stamp_claude_code_enabled` switch
42
+ - **Vision handoff**: replaces image blocks with text descriptions generated by a vision model (`umans-flash`), enabling text-only models to "see" images. Descriptions are cached (7-day TTL) with persistent SQLite storage
43
+ - **Concurrency gate**: semaphore + circuit breaker with intention-based reservations (main vs vision), hard cap, soft limit driven by `/v1/usage`, queue timeout, and over-subscription fallback
44
+ - **Rate limiting**: sliding-window weighted rate limiter for pro-tier request limits, auto-derived from `/v1/usage` or explicitly configured
45
+ - **Connection warmer**: periodic `/v1/models` pings keep TLS warm, skipping when real traffic occurred recently
46
+ - **Usage tracking**: fetches and reconciles `/v1/usage` to size concurrency limits, detect rate-boxing, and manage priority demotion
47
+ - **Live inspector dashboard**: React + shadcn/ui dashboard with WebSocket live updates
48
+ - **SSE rendering**: streaming responses are captured and rendered with expandable event previews
49
+ - **Ring buffer storage**: keeps the last N captures (default 200) with WAL mode SQLite
50
+ - **Write-behind queue**: batched database writes to minimize blocking during streaming
51
+ - **Worker-based capture pipeline**: offloads capture writes to a worker thread for non-blocking streaming
52
+ - **Hop-by-hop header stripping**: correct HTTP proxy behavior
53
+ - **Protocol flexibility**: configurable upstream HTTP/1.1 (default) or HTTP/2
54
+
55
+ ## Quick Start
56
+
57
+ ```bash
58
+ # Install
59
+ bun install
60
+
61
+ # Start the proxy (reads config.json + env overrides)
62
+ bun src/cli.ts
63
+
64
+ # Or after build:
65
+ bun run build
66
+ ./dist/cli.js
67
+ ```
68
+
69
+ Point your harness base URL to `http://localhost:1945` and open the inspector at `http://localhost:1945/dashboard/`.
70
+
71
+ ## Configuration
72
+
73
+ > **⚠️ Dashboard-first:** The recommended way to change configuration is via
74
+ > the **Config** tab in the dashboard at `http://localhost:1945/dashboard/`.
75
+ > Editing `config.json` directly works, but the dashboard validates against
76
+ > the upstream `/v1/usage` hard cap, shows field descriptions, and can hot-reload
77
+ > or restart the server for you. If you do edit the file, restart the server
78
+ > afterward (the dashboard auto-reloads on save).
79
+
80
+ Configuration is loaded from a JSON file with environment variable overrides.
81
+
82
+ **Config file path** (auto-created on first run):
83
+
84
+ | OS | Path |
85
+ |----|------|
86
+ | Linux/macOS | `$XDG_CONFIG_HOME/umans-gate/config.json` or `~/.config/umans-gate/config.json` |
87
+ | Windows | `%APPDATA%/umans-gate/config.json` |
88
+
89
+ **Precedence**: environment variables > JSON config file > built-in defaults.
90
+
91
+ On first run, `loadConfig()` writes a `config.json` with defaults to the
92
+ resolved path if it does not already exist. Existing configs are never
93
+ overwritten.
94
+
95
+ All env vars from `.env.example` have JSON equivalents using `snake_case`
96
+ (e.g., `UPSTREAM_PROTOCOL` → `upstream_protocol`).
97
+
98
+ ### Config Fields
99
+
100
+ | Variable | Default | Description |
101
+ |---|---|---|
102
+ | `PORT` | `1945` | Listen port |
103
+ | `MAX_CAPTURES` | `200` | Ring buffer size (keeps last N) |
104
+ | `DB_PATH` | `./umans-gate.db` | SQLite database path |
105
+ | `IDLE_TIMEOUT` | `255` | Bun.serve idleTimeout — HTTP connection idle timeout in seconds (1–255) |
106
+ | `UPSTREAM_PROTOCOL` | `http1.1` | Upstream protocol: `http1.1` or `http2` |
107
+ | `UPSTREAM_TIMEOUT_MS` | `300000` | Upstream fetch timeout in ms (5 min) |
108
+ | `STAMP_CLAUDE_CODE_ENABLED` | `false` | Toggle full Claude Code stamp bundle (TTL, `top_k`, `max_tokens`, `thinking`, `output_config`, `context_management`, `temperature`) |
109
+ | `STAMP_REASONING_EFFORT_ENABLED` | `false` | Toggle OpenAI-compatible `reasoning_effort` stamping (`high`, `max` for `umans-glm*`; removes `max_tokens`/`thinking`) |
110
+ | `WARMER_ENABLED` | `true` | Toggle TLS connection warmer |
111
+ | `WARMER_INTERVAL_MS` | `20000` | Warmer ping interval in ms |
112
+ | `UMANS_API_KEY` | _(empty)_ | Required for `/v1/usage` fetch, concurrency gate sizing, and rate-limit validation |
113
+ | `USAGE_REFRESH_MS` | `60000` | `/v1/usage` poll interval in ms |
114
+ | `MODELS_REFRESH_MS` | `3600000` | `/v1/models` poll interval in ms |
115
+ | `CONCURRENCY_HARD_CAP` | `1` | Maximum concurrent upstream requests (hard ceiling) |
116
+ | `CONCURRENCY_SOFT_LIMIT` | `1` | Soft limit (driven by `/v1/usage`, adjustable at runtime) |
117
+ | `CONCURRENCY_MAIN_RESERVATION` | `1` | Reserved slots for main requests |
118
+ | `CONCURRENCY_VISION_RESERVATION` | `1` | Reserved slots for vision requests |
119
+ | `RATE_LIMIT_REQUESTS` | `0` | Pro-tier rolling-window limit. `-1` = unlimited, `0` = auto-derive from `/v1/usage`, `>0` = explicit |
120
+ | `QUEUE_TIMEOUT_MS` | `30000` | Max wait time for queued permits |
121
+ | `MAX_QUEUE_DEPTH` | `256` | Max queued permits |
122
+ | `RELEASE_COOLDOWN_MS` | `1000` | Cooldown after releasing a permit |
123
+ | `BREAKER_THRESHOLD` | `5` | Circuit breaker failure threshold |
124
+ | `BREAKER_WINDOW_MS` | `300000` | Circuit breaker failure window (5 min) |
125
+ | `BREAKER_COOLDOWN_MS` | `60000` | Circuit breaker cooldown (1 min) |
126
+ | `VISION_STRATEGY` | `catalog` | When to intercept images: `never`, `catalog` (only if model lacks vision), `always` |
127
+ | `VISION_MODEL` | `umans-flash` | Vision model used for image description |
128
+ | `VISION_MAX_IMAGES` | `5` | Max images processed per request |
129
+ | `VISION_MAX_DESCRIPTION_TOKENS` | `4096` | Max tokens for vision descriptions |
130
+ | `VISION_CONCURRENCY` | `1` | Vision model call concurrency |
131
+ | `VISION_MAX_DIMENSION` | `2048` | Max image dimension before resize |
132
+ | `VISION_JPEG_QUALITY` | `92` | JPEG compression quality |
133
+ | `VISION_IMAGE_FORMAT` | `png` | Image format sent to vision model: `jpeg` or `png` |
134
+ | `VISION_CACHE_SIZE` | `1000` | In-memory description cache size |
135
+ | `VISION_CACHE_TTL_MS` | `604800000` | Description cache TTL (7 days) |
136
+ | `VISION_PERSISTENT_CACHE` | `true` | Persist vision descriptions to SQLite |
137
+ | `VISION_TIMEOUT_MS` | `0` | Vision call timeout (0 = no timeout) |
138
+ | `CAPTURE_BODY_MAX_BYTES` | `10000000` | Max captured request/response body size (10 MB, 0 = unlimited) |
139
+ | `COMPRESSION_ENABLED` | `true` | zstd compression for stored bodies |
140
+ | `QUEUE_MAX_DEPTH` | `100` | Max write-behind response queue depth |
141
+ | `WS_BACKPRESSURE_LIMIT` | `1048576` | WebSocket backpressure limit in bytes (1 MB) |
142
+ | `WS_CLOSE_ON_BACKPRESSURE_LIMIT` | `true` | Close WebSocket connections exceeding backpressure limit |
143
+
144
+ The upstream target (`https://api.code.umans.ai`), OpenAI chat path
145
+ (`chat/completions`), warmer path (`/v1/models`), and vision target are
146
+ hardcoded — not configurable.
147
+
148
+ ### Hot Reload and Restart
149
+
150
+ The dashboard's Config tab can save changes and trigger a hot reload
151
+ via `POST /dashboard/api/config/reload`. Hot-reloadable fields (e.g.
152
+ `stamp_claude_code_enabled`, `breaker_*`, `rate_limit_*`, `vision_*`)
153
+ apply live; fields marked `restartRequired` (e.g. `port`, `db_path`,
154
+ `upstream_protocol`) require a server restart.
155
+
156
+ The dashboard also has a **Restart** button (`POST /dashboard/api/restart`)
157
+ that calls `process.exit(0)`. This requires an external process manager
158
+ to restart the server automatically:
159
+
160
+ | Manager | Command |
161
+ |---------|---------|
162
+ | `bun --watch` | `bun --watch src/cli.ts` |
163
+ | systemd | `Restart=always` in the unit file |
164
+ | pm2 | `pm2 start src/cli.ts --interpreter bun --watch` |
165
+
166
+ Without a process manager, the server exits and stays down until you
167
+ start it manually.
168
+
169
+ ### Stamp Pipeline
170
+
171
+ When `STAMP_CLAUDE_CODE_ENABLED` is on, the proxy applies the full Claude
172
+ Code stamp bundle to Anthropic requests:
173
+
174
+ 1. **TTL stamping**: adds `"ttl": "1h"` to every `cache_control: {type:"ephemeral"}` block
175
+ 2. **`top_k` injection**: injects `"top_k": 20` after the `model` field
176
+ 3. **`temperature` stamping**: forces `temperature: 1.0`
177
+ 4. **`max_tokens` stamping**: `131071` for `umans-glm*` models, `32767` for others
178
+ 5. **`thinking` injection**: `{ "type": "adaptive" }` for `umans-coder`, `umans-flash`, `umans-kimi*`, `umans-qwen*`
179
+ 6. **`output_config` injection**: `{ "effort": "high" }` for most models, `{ "effort": "max" }` for `umans-glm*`
180
+ 7. **`context_management` injection**: `{ "edits": [{ "type": "clear_thinking_20251015", "keep": "all" }] }`
181
+
182
+ For OpenAI-compatible requests, `STAMP_REASONING_EFFORT_ENABLED` injects
183
+ `"reasoning_effort": "high"` (or `"max"` for `umans-glm*`) and removes
184
+ `max_tokens`/`thinking` from the body.
185
+
186
+ The stamped body is what gets forwarded upstream AND what gets captured — so
187
+ the inspector shows exactly what went to the API.
188
+
189
+ ### Vision Handoff
190
+
191
+ The vision handoff pipeline replaces image blocks in the request body with text
192
+ descriptions generated by a vision model:
193
+
194
+ 1. Detects image blocks in Anthropic and OpenAI request bodies
195
+ 2. Transcodes images to the configured format (PNG/JPEG) with max dimension
196
+ 3. Sends to the vision model (`umans-flash`) with a detailed OCR prompt
197
+ 4. Caches descriptions in-memory (1000 entries) and persistently in SQLite
198
+ 5. Replaces image blocks with text descriptions
199
+
200
+ Strategies:
201
+ - `catalog` (default): intercept only if the model is known to lack vision support
202
+ - `always`: intercept all images regardless of model
203
+ - `never`: disabled
204
+
205
+ Vision calls are serialized by the concurrency gate (default concurrency=1)
206
+ because the upstream has limited vision slots.
207
+
208
+ ### Concurrency Gate
209
+
210
+ The concurrency gate (`src/limiter/`) prevents overwhelming the upstream:
211
+
212
+ - **Semaphore**: enforces a soft limit (driven by `/v1/usage`) and hard cap
213
+ - **Circuit breaker**: opens after `breaker_threshold` 429s in `breaker_window_ms`,
214
+ blocks traffic for `breaker_cooldown_ms`, then half-opens to test
215
+ - **Intention-based reservations**: main and vision requests have reserved slots
216
+ - **Queue**: over-limit requests are queued up to `max_queue_depth` with
217
+ `queue_timeout_ms` timeout
218
+ - **Over-subscription fallback**: when reservations exceed capacity, permits
219
+ are still granted if actual usage allows
220
+
221
+ ### Rate Limiting
222
+
223
+ The sliding-window rate limiter (`src/rate.ts`) enforces pro-tier request limits:
224
+
225
+ - `rate_limit_requests: 0` — auto-derive from `/v1/usage` (default)
226
+ - `rate_limit_requests: -1` — unlimited (no limiter)
227
+ - `rate_limit_requests: N` — explicit limit with weighted sliding window
228
+
229
+ The window size is derived from `/v1/usage` and is not configurable.
230
+
231
+ ## Programmatic API
232
+
233
+ ```typescript
234
+ import { createProxyServer } from "umans-gate";
235
+
236
+ const server = createProxyServer({
237
+ config: { port: 8080 },
238
+ });
239
+
240
+ // server.db — CaptureDB instance
241
+ // server.ws — WsBroadcaster instance
242
+ // server.queue — WriteQueue instance
243
+ // server.shutdown() — graceful shutdown
244
+ ```
245
+
246
+ ## Inspector Dashboard
247
+
248
+ The dashboard is a Vite + React + TypeScript + Tailwind + shadcn/ui app in `dashboard/`.
249
+
250
+ ```bash
251
+ cd dashboard
252
+ bun install
253
+ bun run dev # dev server at localhost:5173
254
+ bun run build # production build to dashboard/dist/
255
+ ```
256
+
257
+ The dashboard connects to:
258
+ - `GET /dashboard/api/captures` — capture list
259
+ - `GET /dashboard/api/captures/:id` — full capture detail
260
+ - `POST /dashboard/api/clear` — clear all captures
261
+ - `WS /dashboard/ws` — live updates (`new`, `update`, `clear` messages)
262
+
263
+ ## Development
264
+
265
+ ```bash
266
+ bun run dev # start proxy server
267
+ bun run typecheck # TypeScript checking
268
+ bun run lint # Biome lint
269
+ bun run lint:fix # Biome lint + auto-fix
270
+ bun run test # run server tests
271
+ bun run test:dashboard # run dashboard tests (vitest + jsdom)
272
+ bun run test:all # run server + dashboard tests
273
+ bun run build # build server (tsup) + dashboard (vite)
274
+ ```
275
+
276
+ ## Project Structure
277
+
278
+ ```
279
+ umans-gate/
280
+ ├── src/
281
+ │ ├── cli.ts # CLI entry point
282
+ │ ├── index.ts # createProxyServer() factory + exports
283
+ │ ├── config.ts # env-driven configuration
284
+ │ ├── db.ts # SQLite capture store (WAL, ring buffer)
285
+ │ ├── proxy.ts # proxy handler (capture + stamping + streaming)
286
+ │ ├── stamp.ts # cache_control TTL stamping logic
287
+ │ ├── stamp-pipeline.ts # unified stamp orchestrator
288
+ │ ├── stamp-temperature.ts # temperature stamping
289
+ │ ├── stamp-thinking.ts # thinking / max_tokens / output_config stamping
290
+ │ ├── stamp-topk.ts # top_k injection
291
+ │ ├── stamp-reasoning.ts # OpenAI reasoning_effort stamping
292
+ │ ├── viewer.ts # inspector dashboard + REST API router
293
+ │ ├── ws.ts # WebSocket broadcast manager
294
+ │ ├── queue.ts # write-behind batched flush queue
295
+ │ ├── rate.ts # sliding-window rate limiter
296
+ │ ├── warmer.ts # TLS connection warmer
297
+ │ ├── metrics.ts # aggregated runtime statistics
298
+ │ ├── economics.ts # token cost calculation
299
+ │ ├── usage.ts # /v1/usage fetch + reconciliation
300
+ │ ├── usage-extract.ts # usage extraction (Anthropic + OpenAI)
301
+ │ ├── model-info-parser.ts # /v1/models response parser
302
+ │ ├── model-policy.ts # model-aware stamping decisions
303
+ │ ├── compress.ts # zstd body compression
304
+ │ ├── vision-description-store.ts # persistent vision description storage
305
+ │ ├── helpers.ts # shared utilities
306
+ │ ├── logger.ts # structured scoped logging
307
+ │ ├── banner.ts # startup banner
308
+ │ ├── types.ts # shared types
309
+ │ ├── limiter/
310
+ │ │ ├── gate.ts # ConcurrencyGate (Semaphore + CircuitBreaker)
311
+ │ │ ├── circuit-breaker.ts # CircuitBreaker implementation
312
+ │ │ ├── types.ts # gate option types
313
+ │ │ └── index.ts # re-exports
314
+ │ ├── vision/
315
+ │ │ ├── handoff.ts # vision handoff orchestrator
316
+ │ │ ├── detect.ts # image block detection
317
+ │ │ ├── cache.ts # in-memory description cache
318
+ │ │ ├── persistent-cache.ts # SQLite-backed description cache
319
+ │ │ ├── transcode.ts # image transcoding
320
+ │ │ ├── wrapper.ts # description wrapper + policy
321
+ │ │ └── sink.ts # vision record sink
322
+ │ ├── workers/ # worker-based capture pipeline
323
+ │ └── shared/ # extracted domain helpers
324
+ ├── dashboard/ # React + shadcn/ui dashboard
325
+ ├── test/ # bun:test test suite
326
+ ├── docs/ # additional documentation
327
+ │ ├── ARCHITECTURE.md # system architecture
328
+ │ ├── TROUBLESHOOTING.md # troubleshooting guide
329
+ │ ├── BENCHMARKS.md # benchmark results
330
+ │ └── proxy-modifications.md # proxy modification inventory
331
+ ├── benchmark/ # benchmark scripts + results
332
+ ├── .github/ # issue templates, workflows, PR template
333
+ └── dist/ # build output (gitignored)
334
+ ```
335
+
336
+ ## Documentation
337
+
338
+ - [Architecture](docs/ARCHITECTURE.md) — system design and data flow
339
+ - [Proxy Modifications](docs/proxy-modifications.md) — complete inventory of proxy modifications
340
+ - [Troubleshooting](docs/TROUBLESHOOTING.md) — common issues and solutions
341
+ - [Benchmarks](docs/BENCHMARKS.md) — benchmark methodology and results
342
+ - [Dashboard Design System](dashboard/DESIGN.md) — design system documentation
343
+ - [Changelog](CHANGELOG.md) — version history
344
+ - [Contributing](CONTRIBUTING.md) — how to contribute
345
+ - [Roadmap](ROADMAP.md) — future plans
346
+
347
+ ## Contributing
348
+
349
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, code style,
350
+ testing, and release instructions.
351
+
352
+ ## License
353
+
354
+ MIT
@@ -0,0 +1,2 @@
1
+ import{j as e,r as d}from"./vendor-react-Dtud3GyP.js";import{c as o,B as n}from"./index-72wCsW4_.js";import{aC as u,aD as c,aE as f,aF as m,aG as g,aH as p,aI as x,aJ as D}from"./vendor-CVKsgXJU.js";function h({...a}){return e.jsx(u,{"data-slot":"alert-dialog",...a})}function v({...a}){return e.jsx(D,{"data-slot":"alert-dialog-trigger",...a})}function j({...a}){return e.jsx(p,{"data-slot":"alert-dialog-portal",...a})}function A({className:a,...t}){return e.jsx(x,{"data-slot":"alert-dialog-overlay",className:o("fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",a),...t})}function N({className:a,size:t="default",...l}){return e.jsxs(j,{children:[e.jsx(A,{}),e.jsx(c,{"data-slot":"alert-dialog-content","data-size":t,className:o("group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",a),...l})]})}function y({className:a,...t}){return e.jsx("div",{"data-slot":"alert-dialog-header",className:o("grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]",a),...t})}function C({className:a,...t}){return e.jsx("div",{"data-slot":"alert-dialog-footer",className:o("-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",a),...t})}function k({className:a,...t}){return e.jsx(f,{"data-slot":"alert-dialog-title",className:o("font-heading text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",a),...t})}function R({className:a,...t}){return e.jsx(m,{"data-slot":"alert-dialog-description",className:o("text-sm text-balance text-muted-foreground md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",a),...t})}const T=d.forwardRef(function({className:t,variant:l="default",size:r="default",...s},i){return e.jsx(g,{ref:i,"data-slot":"alert-dialog-action",className:o(t),render:e.jsx(n,{variant:l,size:r}),...s})}),_=d.forwardRef(function({className:t,variant:l="outline",size:r="default",...s},i){return e.jsx(g,{ref:i,"data-slot":"alert-dialog-cancel",className:o(t),render:e.jsx(n,{variant:l,size:r}),...s})});export{h as A,v as a,N as b,y as c,k as d,R as e,C as f,_ as g,T as h};
2
+ //# sourceMappingURL=alert-dialog-DlhAyxD3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alert-dialog-DlhAyxD3.js","sources":["../../src/components/ui/alert-dialog.tsx"],"sourcesContent":["\"use client\";\n\nimport { AlertDialog as AlertDialogPrimitive } from \"@base-ui/react/alert-dialog\";\nimport * as React from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nfunction AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) {\n return <AlertDialogPrimitive.Root data-slot=\"alert-dialog\" {...props} />;\n}\n\nfunction AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) {\n return <AlertDialogPrimitive.Trigger data-slot=\"alert-dialog-trigger\" {...props} />;\n}\n\nfunction AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) {\n return <AlertDialogPrimitive.Portal data-slot=\"alert-dialog-portal\" {...props} />;\n}\n\nfunction AlertDialogOverlay({ className, ...props }: AlertDialogPrimitive.Backdrop.Props) {\n return (\n <AlertDialogPrimitive.Backdrop\n data-slot=\"alert-dialog-overlay\"\n className={cn(\n \"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction AlertDialogContent({\n className,\n size = \"default\",\n ...props\n}: AlertDialogPrimitive.Popup.Props & {\n size?: \"default\" | \"sm\";\n}) {\n return (\n <AlertDialogPortal>\n <AlertDialogOverlay />\n <AlertDialogPrimitive.Popup\n data-slot=\"alert-dialog-content\"\n data-size={size}\n className={cn(\n \"group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95\",\n className,\n )}\n {...props}\n />\n </AlertDialogPortal>\n );\n}\n\nfunction AlertDialogHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"alert-dialog-header\"\n className={cn(\n \"grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction AlertDialogFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"alert-dialog-footer\"\n className={cn(\n \"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction AlertDialogMedia({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"alert-dialog-media\"\n className={cn(\n \"mb-2 inline-flex size-10 items-center justify-center rounded-md bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction AlertDialogTitle({\n className,\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {\n return (\n <AlertDialogPrimitive.Title\n data-slot=\"alert-dialog-title\"\n className={cn(\n \"font-heading text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction AlertDialogDescription({\n className,\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {\n return (\n <AlertDialogPrimitive.Description\n data-slot=\"alert-dialog-description\"\n className={cn(\n \"text-sm text-balance text-muted-foreground md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground\",\n className,\n )}\n {...props}\n />\n );\n}\n\nconst AlertDialogAction = React.forwardRef<\n HTMLButtonElement,\n Omit<AlertDialogPrimitive.Close.Props, \"render\"> &\n Pick<React.ComponentProps<typeof Button>, \"variant\" | \"size\">\n>(function AlertDialogAction({ className, variant = \"default\", size = \"default\", ...props }, ref) {\n return (\n <AlertDialogPrimitive.Close\n ref={ref}\n data-slot=\"alert-dialog-action\"\n className={cn(className)}\n render={<Button variant={variant} size={size} />}\n {...props}\n />\n );\n});\n\nconst AlertDialogCancel = React.forwardRef<\n HTMLButtonElement,\n AlertDialogPrimitive.Close.Props & Pick<React.ComponentProps<typeof Button>, \"variant\" | \"size\">\n>(function AlertDialogCancel({ className, variant = \"outline\", size = \"default\", ...props }, ref) {\n return (\n <AlertDialogPrimitive.Close\n ref={ref}\n data-slot=\"alert-dialog-cancel\"\n className={cn(className)}\n render={<Button variant={variant} size={size} />}\n {...props}\n />\n );\n});\n\nexport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogMedia,\n AlertDialogOverlay,\n AlertDialogPortal,\n AlertDialogTitle,\n AlertDialogTrigger,\n};\n"],"names":["AlertDialog","props","AlertDialogPrimitive.Root","AlertDialogTrigger","AlertDialogPrimitive.Trigger","AlertDialogPortal","AlertDialogPrimitive.Portal","AlertDialogOverlay","className","jsx","AlertDialogPrimitive.Backdrop","cn","AlertDialogContent","size","AlertDialogPrimitive.Popup","AlertDialogHeader","AlertDialogFooter","AlertDialogTitle","AlertDialogPrimitive.Title","AlertDialogDescription","AlertDialogPrimitive.Description","AlertDialogAction","React.forwardRef","variant","ref","AlertDialogPrimitive.Close","Button","AlertDialogCancel"],"mappings":"uMAQA,SAASA,EAAY,CAAE,GAAGC,GAA0C,CAClE,aAAQC,EAAA,CAA0B,YAAU,eAAgB,GAAGD,EAAO,CACxE,CAEA,SAASE,EAAmB,CAAE,GAAGF,GAA6C,CAC5E,aAAQG,EAAA,CAA6B,YAAU,uBAAwB,GAAGH,EAAO,CACnF,CAEA,SAASI,EAAkB,CAAE,GAAGJ,GAA4C,CAC1E,aAAQK,EAAA,CAA4B,YAAU,sBAAuB,GAAGL,EAAO,CACjF,CAEA,SAASM,EAAmB,CAAE,UAAAC,EAAW,GAAGP,GAA8C,CACxF,OACEQ,EAAAA,IAACC,EAAA,CACC,YAAU,uBACV,UAAWC,EACT,wLACAH,CAAA,EAED,GAAGP,CAAA,CAAA,CAGV,CAEA,SAASW,EAAmB,CAC1B,UAAAJ,EACA,KAAAK,EAAO,UACP,GAAGZ,CACL,EAEG,CACD,cACGI,EAAA,CACC,SAAA,CAAAI,EAAAA,IAACF,EAAA,EAAmB,EACpBE,EAAAA,IAACK,EAAA,CACC,YAAU,uBACV,YAAWD,EACX,UAAWF,EACT,6aACAH,CAAA,EAED,GAAGP,CAAA,CAAA,CACN,EACF,CAEJ,CAEA,SAASc,EAAkB,CAAE,UAAAP,EAAW,GAAGP,GAAsC,CAC/E,OACEQ,EAAAA,IAAC,MAAA,CACC,YAAU,sBACV,UAAWE,EACT,qZACAH,CAAA,EAED,GAAGP,CAAA,CAAA,CAGV,CAEA,SAASe,EAAkB,CAAE,UAAAR,EAAW,GAAGP,GAAsC,CAC/E,OACEQ,EAAAA,IAAC,MAAA,CACC,YAAU,sBACV,UAAWE,EACT,gNACAH,CAAA,EAED,GAAGP,CAAA,CAAA,CAGV,CAeA,SAASgB,EAAiB,CACxB,UAAAT,EACA,GAAGP,CACL,EAA4D,CAC1D,OACEQ,EAAAA,IAACS,EAAA,CACC,YAAU,qBACV,UAAWP,EACT,iKACAH,CAAA,EAED,GAAGP,CAAA,CAAA,CAGV,CAEA,SAASkB,EAAuB,CAC9B,UAAAX,EACA,GAAGP,CACL,EAAkE,CAChE,OACEQ,EAAAA,IAACW,EAAA,CACC,YAAU,2BACV,UAAWT,EACT,iIACAH,CAAA,EAED,GAAGP,CAAA,CAAA,CAGV,CAEA,MAAMoB,EAAoBC,EAAAA,WAIxB,SAA2B,CAAE,UAAAd,EAAW,QAAAe,EAAU,UAAW,KAAAV,EAAO,UAAW,GAAGZ,CAAA,EAASuB,EAAK,CAChG,OACEf,EAAAA,IAACgB,EAAA,CACC,IAAAD,EACA,YAAU,sBACV,UAAWb,EAAGH,CAAS,EACvB,OAAQC,EAAAA,IAACiB,EAAA,CAAO,QAAAH,EAAkB,KAAAV,CAAA,CAAY,EAC7C,GAAGZ,CAAA,CAAA,CAGV,CAAC,EAEK0B,EAAoBL,EAAAA,WAGxB,SAA2B,CAAE,UAAAd,EAAW,QAAAe,EAAU,UAAW,KAAAV,EAAO,UAAW,GAAGZ,CAAA,EAASuB,EAAK,CAChG,OACEf,EAAAA,IAACgB,EAAA,CACC,IAAAD,EACA,YAAU,sBACV,UAAWb,EAAGH,CAAS,EACvB,OAAQC,EAAAA,IAACiB,EAAA,CAAO,QAAAH,EAAkB,KAAAV,CAAA,CAAY,EAC7C,GAAGZ,CAAA,CAAA,CAGV,CAAC"}
@@ -0,0 +1,2 @@
1
+ import{r as h,j as e}from"./vendor-react-Dtud3GyP.js";import{A as j,b as g,c as u,d as m,e as D,f,g as A,h as w}from"./alert-dialog-DlhAyxD3.js";import{T as n,a,b as o}from"./index-72wCsW4_.js";import"./vendor-CVKsgXJU.js";function k({open:t,onConfirm:d,onClose:s,count:l,title:c="Clear all captures?",itemLabel:p="captured request",confirmTooltip:x="Permanently delete all captured requests"}){return h.useEffect(()=>{if(!t)return;const r=i=>{i.key==="Escape"&&(i.preventDefault(),i.stopPropagation(),s())};return window.addEventListener("keydown",r,!0),()=>window.removeEventListener("keydown",r,!0)},[t,s]),e.jsx(j,{open:t,onOpenChange:r=>{r||s()},children:e.jsxs(g,{children:[e.jsxs(u,{children:[e.jsx(m,{children:c}),e.jsxs(D,{children:["Delete ",l," ",p,l===1?"":"s","? This cannot be undone."]})]}),e.jsxs(f,{children:[e.jsxs(n,{children:[e.jsx(a,{render:e.jsx(A,{}),children:"Cancel"}),e.jsx(o,{side:"top",children:"Dismiss without deleting"})]}),e.jsxs(n,{children:[e.jsx(a,{render:e.jsx(w,{onClick:d}),children:"Clear"}),e.jsx(o,{side:"top",className:"max-w-[220px]",children:x})]})]})]})})}export{k as ClearConfirmDialog};
2
+ //# sourceMappingURL=clear-confirm-dialog-BOcl93wD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clear-confirm-dialog-BOcl93wD.js","sources":["../../src/components/clear-confirm-dialog.tsx"],"sourcesContent":["import { useEffect } from \"react\";\n\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n} from \"@/components/ui/alert-dialog\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"@/components/ui/tooltip\";\n\ninterface ClearConfirmDialogProps {\n open: boolean;\n onConfirm: () => void;\n onClose: () => void;\n count: number;\n title?: string;\n itemLabel?: string;\n confirmTooltip?: string;\n}\n\nexport function ClearConfirmDialog({\n open,\n onConfirm,\n onClose,\n count,\n title = \"Clear all captures?\",\n itemLabel = \"captured request\",\n confirmTooltip = \"Permanently delete all captured requests\",\n}: ClearConfirmDialogProps) {\n useEffect(() => {\n if (!open) return;\n\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") {\n e.preventDefault();\n e.stopPropagation();\n onClose();\n }\n };\n\n // Use capture phase so we intercept Escape before Base UI's AlertDialog\n // (which blocks Escape to enforce non-dismissable behavior) can swallow it.\n window.addEventListener(\"keydown\", handleKeyDown, true);\n return () => window.removeEventListener(\"keydown\", handleKeyDown, true);\n }, [open, onClose]);\n\n return (\n <AlertDialog\n open={open}\n onOpenChange={(v) => {\n if (!v) onClose();\n }}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>{title}</AlertDialogTitle>\n <AlertDialogDescription>\n Delete {count} {itemLabel}\n {count === 1 ? \"\" : \"s\"}? This cannot be undone.\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <Tooltip>\n <TooltipTrigger render={<AlertDialogCancel />}>Cancel</TooltipTrigger>\n <TooltipContent side=\"top\">Dismiss without deleting</TooltipContent>\n </Tooltip>\n <Tooltip>\n <TooltipTrigger render={<AlertDialogAction onClick={onConfirm} />}>\n Clear\n </TooltipTrigger>\n <TooltipContent side=\"top\" className=\"max-w-[220px]\">\n {confirmTooltip}\n </TooltipContent>\n </Tooltip>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n );\n}\n"],"names":["ClearConfirmDialog","open","onConfirm","onClose","count","title","itemLabel","confirmTooltip","useEffect","handleKeyDown","e","jsx","AlertDialog","v","AlertDialogContent","jsxs","AlertDialogHeader","AlertDialogTitle","AlertDialogDescription","AlertDialogFooter","Tooltip","TooltipTrigger","AlertDialogCancel","TooltipContent","AlertDialogAction"],"mappings":"+NAwBO,SAASA,EAAmB,CACjC,KAAAC,EACA,UAAAC,EACA,QAAAC,EACA,MAAAC,EACA,MAAAC,EAAQ,sBACR,UAAAC,EAAY,mBACZ,eAAAC,EAAiB,0CACnB,EAA4B,CAC1BC,OAAAA,EAAAA,UAAU,IAAM,CACd,GAAI,CAACP,EAAM,OAEX,MAAMQ,EAAiBC,GAAqB,CACtCA,EAAE,MAAQ,WACZA,EAAE,eAAA,EACFA,EAAE,gBAAA,EACFP,EAAA,EAEJ,EAIA,cAAO,iBAAiB,UAAWM,EAAe,EAAI,EAC/C,IAAM,OAAO,oBAAoB,UAAWA,EAAe,EAAI,CACxE,EAAG,CAACR,EAAME,CAAO,CAAC,EAGhBQ,EAAAA,IAACC,EAAA,CACC,KAAAX,EACA,aAAeY,GAAM,CACdA,GAAGV,EAAA,CACV,EAEA,gBAACW,EAAA,CACC,SAAA,CAAAC,OAACC,EAAA,CACC,SAAA,CAAAL,EAAAA,IAACM,GAAkB,SAAAZ,CAAA,CAAM,SACxBa,EAAA,CAAuB,SAAA,CAAA,UACdd,EAAM,IAAEE,EACfF,IAAU,EAAI,GAAK,IAAI,0BAAA,CAAA,CAC1B,CAAA,EACF,SACCe,EAAA,CACC,SAAA,CAAAJ,OAACK,EAAA,CACC,SAAA,CAAAT,MAACU,EAAA,CAAe,OAAQV,EAAAA,IAACW,EAAA,EAAkB,EAAI,SAAA,SAAM,EACrDX,EAAAA,IAACY,EAAA,CAAe,KAAK,MAAM,SAAA,0BAAA,CAAwB,CAAA,EACrD,SACCH,EAAA,CACC,SAAA,CAAAT,EAAAA,IAACU,GAAe,OAAQV,EAAAA,IAACa,GAAkB,QAAStB,CAAA,CAAW,EAAI,SAAA,OAAA,CAEnE,QACCqB,EAAA,CAAe,KAAK,MAAM,UAAU,gBAClC,SAAAhB,CAAA,CACH,CAAA,CAAA,CACF,CAAA,CAAA,CACF,CAAA,CAAA,CACF,CAAA,CAAA,CAGN"}
@@ -0,0 +1,4 @@
1
+ import{j as t,r as h}from"./vendor-react-Dtud3GyP.js";import{T as w,a as j,B as _,b as R,I as U,c as C,L as ne,d as L,e as ae,f as oe,P as le,u as ue,g as de,S as ce}from"./index-72wCsW4_.js";import{Y as me,Z as pe,_ as he,$ as fe,a0 as be,a1 as G,a2 as ge,a3 as xe,a4 as ve,m as b,H as ye,R as H,a5 as ke}from"./vendor-CVKsgXJU.js";import{S as we,a as je,b as _e,c as Re,d as Se}from"./select-mZPf2BR3.js";import{A as qe,a as Ne,b as De,c as Ce,d as Me,e as Te,f as Ie,g as Ee,h as Ae}from"./alert-dialog-DlhAyxD3.js";import{u as Le}from"./use-models-B4MLkfpt.js";import{u as ze}from"./use-polling-resource-D_vWHFwW.js";import"./use-capture-done-listener-u5RxfYQP.js";function Pe({className:e,min:r,max:s,suffix:i,onChange:a,value:n,disabled:o,...d}){const u=typeof n=="string"||typeof n=="number"?n:"";function l(c){if(o)return;const m=typeof u=="string"?Number(u):u;if(Number.isNaN(m))return;let f=m+c;r!==void 0&&f<r&&(f=r),s!==void 0&&f>s&&(f=s),a==null||a(String(f))}return t.jsxs("div",{className:"flex items-stretch",children:[t.jsxs(w,{children:[t.jsx(j,{render:t.jsx(_,{type:"button",variant:"outline",size:"icon",className:"h-8 w-8 rounded-r-none border-r-0 text-muted-foreground hover:text-foreground",onClick:()=>l(-1),disabled:o||r!==void 0&&typeof u=="number"&&u<=r,"aria-label":"Decrease",tabIndex:-1}),children:t.jsx(me,{className:"h-3.5 w-3.5"})}),t.jsx(R,{side:"top",children:"Decrease value"})]}),t.jsxs("div",{className:"relative flex-1",children:[t.jsx(U,{type:"number",inputMode:"numeric",className:C("no-spinner rounded-none text-center",i&&"pr-9",e),value:n,onChange:c=>a==null?void 0:a(c.target.value),disabled:o,...d}),i?t.jsx("span",{className:"pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-xs text-muted-foreground",children:i}):null]}),t.jsxs(w,{children:[t.jsx(j,{render:t.jsx(_,{type:"button",variant:"outline",size:"icon",className:"h-8 w-8 rounded-l-none border-l-0 text-muted-foreground hover:text-foreground",onClick:()=>l(1),disabled:o||s!==void 0&&typeof u=="number"&&u>=s,"aria-label":"Increase",tabIndex:-1}),children:t.jsx(pe,{className:"h-3.5 w-3.5"})}),t.jsx(R,{side:"top",children:"Increase value"})]})]})}function J({className:e,orientation:r="horizontal",...s}){return t.jsx(he,{"data-slot":"separator",orientation:r,className:C("shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px data-[orientation=vertical]:self-stretch",e),...s})}function Q({className:e,size:r="default",...s}){return t.jsx(fe,{"data-slot":"switch","data-size":r,className:C("peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",e),...s,children:t.jsx(be,{"data-slot":"switch-thumb",className:"pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"})})}function K({className:e,...r}){return t.jsx("textarea",{"data-slot":"textarea",className:C("flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",e),...r})}function z(e,r){return e==null?"":String(e)}function $(e){if(e==null)return"";try{return JSON.stringify(e,null,2)}catch{return String(e)}}function Fe({id:e,value:r,onChange:s}){return t.jsx(Q,{id:e,checked:!!r,onCheckedChange:i=>s(i)})}function Oe({id:e,value:r,onChange:s}){return t.jsxs("div",{className:"flex items-center gap-2",children:[t.jsx(Q,{id:e,checked:!!r,onCheckedChange:i=>s(i)}),t.jsx("span",{className:"text-xs text-muted-foreground",children:r?"On":"Off"})]})}function Be({def:e,value:r,onChange:s,id:i,isInvalid:a}){return t.jsxs(we,{value:String(r??""),onValueChange:n=>s(e.nullable&&n===""?null:n),children:[t.jsx(je,{id:i,"aria-invalid":a||void 0,children:t.jsx(_e,{placeholder:e.placeholder??"Select…"})}),t.jsx(Re,{children:(e.options??[]).map(n=>t.jsx(Se,{value:n.value,children:n.label},n.value))})]})}function We({def:e,value:r,onChange:s,id:i,isInvalid:a,placeholder:n}){return t.jsx(K,{id:i,value:z(r,e.nullable),placeholder:n,onChange:o=>s(o.target.value),rows:3,"aria-invalid":a||void 0})}function He({def:e,value:r,onChange:s,id:i,isInvalid:a,placeholder:n}){return t.jsx(le,{id:i,value:z(r,e.nullable),placeholder:n,onChange:o=>s(o.target.value),disabled:e.disabled,"aria-invalid":a||void 0})}function $e({def:e,value:r,onChange:s,id:i,isInvalid:a,placeholder:n}){return t.jsx(Pe,{id:i,min:e.min,max:e.max,suffix:e.suffix,value:z(r,e.nullable),placeholder:n,onChange:o=>s(o),disabled:e.disabled,"aria-invalid":a||void 0})}function Ve({def:e,value:r,onChange:s,id:i,isInvalid:a}){return t.jsx(Je,{def:e,value:r,onChange:s,id:i,isInvalid:a})}function Ue({def:e,value:r,onChange:s,id:i,isInvalid:a,placeholder:n}){return t.jsxs("div",{className:"relative flex items-center",children:[t.jsx(U,{id:i,type:e.inputMode==="url"?"url":"text",value:z(r,e.nullable),placeholder:n,onChange:o=>s(o.target.value),disabled:e.disabled,"aria-invalid":a||void 0}),e.suffix?t.jsx("span",{className:"pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-xs text-muted-foreground",children:e.suffix}):null]})}const Ge={boolean:Fe,toggle:Oe,select:Be,textarea:We,password:He,number:$e,json:Ve,text:Ue};function Je({def:e,value:r,onChange:s,id:i,isInvalid:a}){const[n,o]=h.useState(()=>$(r)),[d,u]=h.useState(null),l=h.useRef(r);h.useEffect(()=>{r!==l.current&&(l.current=r,o($(r)),u(null))},[r]);const c=m=>{if(o(m),m.trim()===""){u(null);const f=e.nullable?null:{};l.current!==f&&(l.current=f,s(f));return}try{const f=JSON.parse(m);u(null),l.current=f,s(f)}catch(f){u(f instanceof Error?f.message:"Invalid JSON")}};return t.jsxs(t.Fragment,{children:[t.jsx(K,{id:i,value:n,placeholder:e.placeholder??"JSON object",onChange:m=>c(m.target.value),rows:6,"aria-invalid":a||!!d||void 0,className:"font-mono text-xs"}),d?t.jsx("p",{className:"text-xs font-medium text-destructive",role:"alert",children:d}):null]})}function Qe({def:e,value:r,onChange:s,dirty:i,error:a,warning:n,onRefreshSource:o,refreshing:d}){const u=`cfg-${e.key}`,l=!!a,c=l,m=e.placeholder??e.patternHint??void 0;return t.jsxs("div",{className:"grid grid-cols-1 gap-1.5 py-2.5 sm:grid-cols-[minmax(200px,1fr)_2fr] sm:items-start sm:gap-4",children:[t.jsxs("div",{className:"flex flex-col gap-1",children:[t.jsxs("div",{className:"flex flex-wrap items-center gap-1.5",children:[t.jsx(ne,{htmlFor:u,className:C(i&&"text-primary",l&&"text-destructive"),children:e.label}),e.required?t.jsx(L,{variant:"destructive",size:"sm",children:"Required"}):null,e.restartRequired?t.jsxs(L,{variant:"secondary",size:"sm",children:[t.jsx(G,{className:"h-2.5 w-2.5"}),"restart"]}):null,e.experimental?t.jsxs(L,{variant:"secondary",size:"sm",className:ae,children:[t.jsx(ge,{className:"h-2.5 w-2.5"}),"experimental"]}):null,e.umansSourced?t.jsxs(L,{variant:"secondary",size:"sm",className:oe,children:[t.jsx(xe,{className:"h-2.5 w-2.5"}),"Umans API"]}):null]}),e.description?t.jsx("span",{className:"text-xs text-muted-foreground",children:e.description}):null]}),t.jsxs("div",{className:"flex flex-col gap-1",children:[t.jsxs("div",{className:"flex items-center gap-1.5",children:[t.jsx("div",{className:"flex-1",children:Ge[e.kind]({def:e,value:r,onChange:s,id:u,isInvalid:c,placeholder:m})}),e.refreshSource&&o?t.jsxs(w,{children:[t.jsx(j,{asChild:!0,children:t.jsx(_,{type:"button",variant:"ghost",size:"icon",className:"h-8 w-8 shrink-0",disabled:d,onClick:o,"aria-label":`Refresh ${e.label} from source`,children:t.jsx(ve,{className:C("h-3.5 w-3.5",d&&"animate-pulse")})})}),t.jsx(R,{side:"left",children:"Re-fetch from upstream rate-limit headers"})]}):null]}),l?t.jsx("p",{className:"text-xs font-medium text-destructive",role:"alert",children:a}):null,n&&!l?t.jsx("p",{className:"text-xs font-medium text-amber-600 dark:text-amber-400",children:n}):null]})]})}function Ke({section:e,values:r,originals:s,onField:i,errors:a,warnings:n,isLast:o,onRefreshSource:d,refreshingSource:u}){return t.jsxs("section",{className:"space-y-0.5",children:[t.jsx("div",{className:"flex items-baseline justify-between gap-3 pb-1",children:t.jsxs("div",{children:[t.jsx("h4",{className:"text-sm font-semibold tracking-tight",children:e.title}),t.jsx("p",{className:"text-xs text-muted-foreground",children:e.description})]})}),e.fields.map(l=>t.jsx(Qe,{def:l,value:r[l.key],onChange:c=>i(l.key,c),dirty:JSON.stringify(r[l.key]??null)!==JSON.stringify(s[l.key]??null),error:a[l.key],warning:n[l.key],onRefreshSource:l.refreshSource?d:void 0,refreshing:u},l.key)),o?null:t.jsx(J,{className:"my-2"})]})}function Ye({group:e,values:r,originals:s,onField:i,errors:a,warnings:n,isLast:o,onRefreshSource:d,refreshingSource:u}){return t.jsxs("div",{className:"space-y-1",children:[t.jsxs("div",{className:"pb-1",children:[t.jsx("h3",{className:"text-base font-semibold tracking-tight",children:e.title}),t.jsx("p",{className:"text-xs text-muted-foreground",children:e.description})]}),e.sections.map((l,c)=>t.jsx(Ke,{section:l,values:r,originals:s,onField:i,errors:a,warnings:n,isLast:o&&c===e.sections.length-1,onRefreshSource:d,refreshingSource:u},l.title)),o?null:t.jsx(J,{className:"my-3"})]})}const Ze=[{key:"vision_strategy",label:"Strategy",kind:"select",description:'Controls when the proxy intercepts image-bearing requests to generate text descriptions via a vision model. "never" disables interception entirely; "catalog" uses a cache-first strategy where vision-capable models process images themselves and only non-capable models get descriptions; "always" forces interception for all requests regardless of model capability. Default: catalog.',options:[{value:"never",label:"Never"},{value:"catalog",label:"Catalog"},{value:"always",label:"Always"}],restartRequired:!0,required:!0},{key:"vision_model",label:"Model",kind:"select",description:"Model used to generate image descriptions during vision interception. Must be a vision-capable model. The dropdown is populated from /v1/models/info at runtime. Default: umans-flash.",restartRequired:!0,required:!0}],Xe=[{key:"vision_prompt",label:"Prompt",kind:"textarea",description:"System prompt sent to the vision model when generating image descriptions. Changing this increments vision_prompt_version to bust the cache. Default: a built-in prompt that produces exhaustive, structured descriptions (image type, OCR, visual elements, data/charts, contextual clues, quality notes).",restartRequired:!0,required:!0},{key:"vision_prompt_version",label:"Prompt Version",kind:"number",description:"Version tag for the vision prompt. Bump this when you edit vision_prompt to invalidate cached descriptions generated with an older prompt. Cached entries with a mismatched version are treated as misses. Default: 2.",restartRequired:!0,required:!0,min:1},{key:"vision_max_images",label:"Max Images",kind:"number",description:"Maximum number of images processed in a single vision interception request. Additional images in the same request are ignored. Range: 1–100. Default: 5.",restartRequired:!0,required:!0,min:1,max:100},{key:"vision_max_description_tokens",label:"Max Description Tokens",kind:"number",description:"Maximum number of tokens the vision model may generate per image description. Higher values allow more detailed descriptions at the cost of latency. Range: 1–200,000. Default: 4,096.",restartRequired:!0,required:!0,min:1,max:2e5},{key:"vision_reasoning_effort",label:"Reasoning Effort",kind:"select",description:`Reasoning effort for the vision model. "none" disables reasoning; "low"/"medium"/"high" increase reasoning depth at the cost of latency. Selecting "Default (null)" uses the model's built-in default. Default: none.`,options:[{value:"",label:"Default (null)"},{value:"none",label:"None"},{value:"low",label:"Low"},{value:"medium",label:"Medium"},{value:"high",label:"High"}],nullable:!0,restartRequired:!0},{key:"vision_timeout_ms",label:"Timeout",kind:"number",description:"Timeout for vision interception requests in milliseconds. 0 means no timeout — the proxy waits indefinitely for the vision model to respond. Default: 0 (no timeout).",restartRequired:!0,required:!0,min:0,suffix:"ms"},{key:"vision_cache_size",label:"Cache Size",kind:"number",description:"Maximum number of entries in the in-memory vision description cache. When the cache is full, oldest entries are evicted. Must be ≥ 100. Default: 1,000.",restartRequired:!0,required:!0,min:100},{key:"vision_cache_ttl_ms",label:"Cache TTL",kind:"number",description:"Time-to-live for vision description cache entries in milliseconds. Expired entries are treated as misses and re-fetched from the vision model. Must be ≥ 1,000 ms. Default: 604,800,000 ms (7 days).",restartRequired:!0,min:1e3,suffix:"ms"},{key:"vision_cache_max_rows",label:"Cache Max Rows",kind:"number",description:"Maximum number of rows in the persistent (on-disk SQLite) vision cache. When the table is full, oldest entries are evicted. Must be ≥ 100. Default: 10,000.",restartRequired:!0,min:100},{key:"vision_persistent_cache",label:"Persistent Cache",kind:"boolean",description:"When on, vision description cache is persisted to disk (SQLite) and survives restarts. When off, cache is in-memory only and lost on restart. Default: true.",restartRequired:!0},{key:"vision_concurrency",label:"Concurrency",kind:"number",description:"Maximum number of concurrent vision interception requests. Higher values allow parallel image processing but increase upstream load. Range: 1–20. Default: 1.",restartRequired:!0,required:!0,min:1,max:20},{key:"vision_max_dimension",label:"Max Dimension",kind:"number",description:"Maximum width or height (in pixels) for preprocessed images. Images exceeding this in either dimension are downscaled proportionally before being sent to the vision model. Range: 256–8,192. Default: 2,048 px.",restartRequired:!0,min:256,max:8192,suffix:"px"},{key:"vision_jpeg_quality",label:"JPEG Quality",kind:"number",description:'JPEG encoding quality (1–100) for preprocessed images when vision_image_format is "jpeg". Higher values produce sharper images at larger file sizes. Ignored when format is "png". Range: 1–100. Default: 92.',restartRequired:!0,min:1,max:100},{key:"vision_image_format",label:"Image Format",kind:"select",description:'Output image format for preprocessed images sent to the vision model. "jpeg" produces smaller payloads with lossy compression; "png" is lossless but larger. Default: png.',options:[{value:"jpeg",label:"JPEG"},{value:"png",label:"PNG"}],restartRequired:!0},{key:"vision_image_detail",label:"Image Detail",kind:"select",description:'OpenAI-style image detail level sent to the vision model. "low" uses a single low-resolution pass; "high" processes the image at higher detail (more tokens, more accurate); "auto" lets the model decide based on image size. Default: high.',options:[{value:"auto",label:"Auto"},{value:"low",label:"Low"},{value:"high",label:"High"}],restartRequired:!0},{key:"vision_pending_max_batch",label:"Pending Max Batch",kind:"number",description:"Maximum number of pending vision requests to batch together into a single processing cycle. Higher values increase throughput under load but add latency for the first request in the batch. Must be ≥ 1. Default: 50.",restartRequired:!0,min:1}],et=[{key:"port",label:"Port",kind:"number",description:"TCP port the proxy listens on. Range: 1–65,535. Default: 1,945.",restartRequired:!0,required:!0,min:1,max:65535},{key:"max_captures",label:"Max Captures",kind:"number",description:"Maximum number of captures kept in the SQLite ring buffer. When this limit is reached, the oldest entries are evicted to make room for new ones. Must be ≥ 1. Default: 200.",restartRequired:!0,required:!0,min:1},{key:"db_path",label:"DB Path",kind:"text",description:"Filesystem path to the SQLite database used for storing captures. Relative paths are resolved from the proxy's working directory. Default: ./umans-gate.db.",restartRequired:!0,required:!0},{key:"idle_timeout",label:"Idle Timeout",kind:"number",description:"Bun.serve idleTimeout — seconds before an idle HTTP connection is closed. Range: 1–255 (Bun's maximum). Default: 255 s.",restartRequired:!0,required:!0,min:1,max:255,suffix:"s"},{key:"upstream_protocol",label:"Upstream Protocol",kind:"select",description:'HTTP protocol used for upstream connections. "http1.1" uses HTTP/1.1 (default, broadest compatibility); "http2" uses HTTP/2 (multiplexing, lower latency for concurrent requests). Default: http1.1.',options:[{value:"http1.1",label:"HTTP/1.1"},{value:"http2",label:"HTTP/2"}],restartRequired:!0,required:!0},{key:"compression_enabled",label:"Compression",kind:"toggle",restartRequired:!1,description:"When on, stored request/response bodies are compressed with zstd to reduce SQLite database size. Hot-reloadable — changes take effect immediately without restart. Default: on."}],tt=[{key:"stamp_claude_code_enabled",label:"Claude Code Style",kind:"toggle",description:"When on, applies the full Claude Code stamp bundle on Anthropic requests: cache TTL=1h, temperature=1.0, top_k=20 (GLM only), max_tokens (131,071 GLM / 32,767 others), thinking={type:'adaptive'}, output_config (effort=max GLM / high others), and context_management injection (requires anthropic-version: 2023-06-01). Off by default.",experimental:!0},{key:"stamp_reasoning_effort_enabled",label:"Reasoning Effort (OpenAI)",kind:"toggle",description:"When on, stamps reasoning_effort='high' onto OpenAI-compatible requests (umans-glm* models get 'max') and removes max_tokens/thinking from the request body. Off by default.",experimental:!0}],rt=[{key:"warmer_enabled",label:"Enabled",kind:"boolean",description:"When on, the proxy periodically pings the upstream /v1/models endpoint to keep connections warm, avoiding ~750 ms cold-start latency on the first request after an idle period. Default: true.",restartRequired:!0},{key:"warmer_interval_ms",label:"Interval",kind:"number",description:"Interval between upstream keep-alive pings in milliseconds. Shorter intervals keep connections warmer but increase background traffic. Must be ≥ 1,000 ms. Default: 20,000 ms (20 s).",restartRequired:!0,required:!0,min:1e3,suffix:"ms"}],st=[{key:"concurrency_hard_cap",label:"Hard Cap",kind:"number",description:"Absolute maximum number of concurrent upstream requests the proxy will allow. Excess requests are queued. This value is validated against the upstream /v1/usage limit when an Umans API key is set. Must be ≥ 1. Default: 1.",required:!0,min:1,refreshSource:!0,umansSourced:!0},{key:"concurrency_soft_limit",label:"Soft Limit",kind:"number",description:"Soft concurrency limit read from the upstream /v1/usage endpoint. Read-only — auto-derived from the upstream and cannot be edited directly. Use the refresh button to re-fetch from source. Default: 1.",disabled:!0,min:1,refreshSource:!0,umansSourced:!0},{key:"concurrency_main_reservation",label:"Main Reservation",kind:"number",description:"Number of concurrency slots reserved exclusively for main (non-vision) requests. Ensures vision traffic cannot starve main requests. Must be ≥ 1 and ≤ (hard_cap − 2) when hard_cap ≥ 3. Default: 1.",required:!0,min:1},{key:"concurrency_vision_reservation",label:"Vision Reservation",kind:"number",description:"Number of concurrency slots reserved exclusively for vision interception requests. Ensures main traffic cannot starve vision handoffs. Must be ≥ 1 and ≤ (hard_cap − 2) when hard_cap ≥ 3. Default: 1.",required:!0,min:1},{key:"release_cooldown_ms",label:"Release Cooldown",kind:"number",description:"Delay in milliseconds before releasing a concurrency slot back to the pool after a response completes. A non-zero value adds a brief cooldown to prevent immediate burst re-use. Must be ≥ 0. Default: 1,000 ms (1 s).",required:!0,min:0,suffix:"ms"},{key:"rate_limit_requests",label:"Rate Limit",kind:"number",description:"Controls upstream request rate limiting. -1 = unlimited (no limiter at all), 0 = auto-derive the limit from the upstream /v1/usage endpoint (requires umans_api_key), >0 = use this explicit value as the max requests per rate-limit window. Default: 0 (auto-derive).",min:-1,umansSourced:!0,refreshSource:!0}],it=[{key:"breaker_threshold",label:"Breaker Threshold",kind:"number",description:"Number of upstream failures within the breaker window required to open (trip) the circuit breaker. When open, the proxy short-circuits upstream requests instead of retrying. Must be ≥ 1. Default: 5.",required:!0,min:1},{key:"breaker_window_ms",label:"Breaker Window",kind:"number",description:"Sliding time window in milliseconds during which upstream failures are counted toward the breaker threshold. Must be ≥ 1,000 ms. Default: 300,000 ms (5 minutes).",required:!0,min:1e3,suffix:"ms"},{key:"breaker_cooldown_ms",label:"Breaker Cooldown",kind:"number",description:"Duration in milliseconds the circuit breaker stays open before allowing a half-open probe request through. If the probe succeeds, the breaker closes; if it fails, the timer restarts. Must be ≥ 1,000 ms. Default: 60,000 ms (1 minute).",required:!0,min:1e3,suffix:"ms"}],nt=[{key:"queue_timeout_ms",label:"Queue Timeout",kind:"number",description:"Maximum time in milliseconds a request can wait in the concurrency queue before timing out and returning an error to the client. Must be ≥ 100 ms. Default: 30,000 ms (30 s).",required:!0,min:100,suffix:"ms"},{key:"max_queue_depth",label:"Max Queue Depth",kind:"number",description:"Maximum number of requests that can be waiting in the concurrency queue simultaneously. When the queue is full, new requests are rejected immediately. Must be ≥ 1. Default: 256.",required:!0,min:1},{key:"queue_max_depth",label:"Write Queue Depth",kind:"number",description:"Maximum depth of the write-behind database flush queue (distinct from the concurrency waiters queue above). Controls how many capture writes can be buffered before the queue applies backpressure. Must be ≥ 1. Default: 100.",min:1}],at=[{key:"capture_body_max_bytes",label:"Capture Body Max Bytes",kind:"number",description:"Maximum size (in bytes) of captured request/response bodies stored in SQLite. Bodies exceeding this limit are truncated. 0 = no limit (capture full bodies). Must be ≥ 0. Default: 10,000,000 (10 MB).",min:0},{key:"ws_backpressure_limit",label:"WS Backpressure Limit",kind:"number",description:"Maximum buffered bytes per WebSocket connection before backpressure is applied. When a client falls behind, the proxy stops sending until the client catches up. 0 = use Bun's default limit. Must be ≥ 0. Default: 1,048,576 (1 MB).",min:0,suffix:"bytes"},{key:"ws_close_on_backpressure_limit",label:"Close on Backpressure Limit",kind:"boolean",description:"When on, WebSocket connections that exceed the backpressure limit are forcibly closed instead of merely pausing. This prevents slow clients from accumulating unbounded buffered data. Default: true."}],ot=[{key:"umans_api_key",label:"Umans API Key",kind:"password",description:"Umans API key used to authenticate upstream requests and fetch /v1/usage data (concurrency limits, rate-limit source). When empty, the proxy runs in fail-safe mode with worst-case limits and priority_low=true. Leave empty to disable upstream usage tracking. Default: empty.",restartRequired:!0},{key:"usage_refresh_ms",label:"Usage Refresh",kind:"number",description:"Interval in milliseconds between background /v1/usage fetches. The usage data drives concurrency limit auto-derivation and rate-limit source values. Shorter intervals keep limits fresher but increase API calls. Must be ≥ 1,000 ms. Default: 60,000 ms (1 minute).",restartRequired:!0,required:!0,min:1e3,suffix:"ms"},{key:"models_refresh_ms",label:"Models Refresh",kind:"number",description:"Interval in milliseconds between background /v1/models fetches. The models list populates the vision model dropdown and validates model names. Must be ≥ 1,000 ms. Default: 3,600,000 ms (1 hour).",restartRequired:!0,required:!0,min:1e3,suffix:"ms"}],Y=[{title:"General",description:"Core proxy, credentials, vision, and concurrency settings.",sections:[{title:"Server",description:"Core network settings and SQLite storage for captures.",fields:et},{title:"Credentials",description:"API key and upstream usage/model refresh intervals.",fields:ot},{title:"Vision",description:"Vision image-to-description interception pipeline.",fields:Ze},{title:"Warmer",description:"Background keep-alive pings to the upstream /v1/models endpoint.",fields:rt},{title:"Concurrency & Gate",description:"Concurrency limits, reservations, and rate limiting.",fields:st}]},{title:"Experimental",description:"Optional request-body stamping features that may change behavior.",sections:[{title:"Request Stamp",description:"Optional request-body stamping for Anthropic and OpenAI-compatible routes.",fields:tt}]},{title:"Advanced",description:"Circuit breaker, queue tuning, vision parameters, and storage internals.",sections:[{title:"Circuit Breaker",description:"Failure detection and circuit breaker thresholds.",fields:it},{title:"Queue",description:"Request queue timeouts, depths, and write-behind buffering.",fields:nt},{title:"Vision Tuning",description:"Vision prompt, cache, concurrency, and image processing parameters.",fields:Xe},{title:"Capture & Storage",description:"Body capture limits and WebSocket backpressure controls.",fields:at}]}];Y.flatMap(e=>e.sections);function lt(e){const[r,s]=h.useState(null);h.useEffect(()=>{e&&s({...e})},[e]);const i=h.useMemo(()=>{if(!r||!e)return new Set;const o=new Set;for(const d of Object.keys(r))JSON.stringify(r[d]??null)!==JSON.stringify(e[d]??null)&&o.add(d);return o},[r,e]);function a(){e&&s({...e})}function n(o,d){s(u=>u&&{...u,[o]:d})}return{draft:r,setDraft:s,resetDraft:a,updateField:n,isDirty:i.size>0,dirtyKeys:i}}function ut(e){const[r,s]=h.useState(!1),[i,a]=h.useState(!1),[n,o]=h.useState(!1),[d,u]=h.useState(!1),[l,c]=h.useState([]),m=h.useCallback(async()=>{if(!e.draft)return;if(!e.isDirty){b.info("Nothing to save",{description:"No changes since last load."});return}s(!0);const p={};for(const y of e.dirtyKeys)p[y]=e.draft[y];const g=await e.save(p);if(!g){s(!1),b.error("Save failed",{description:"Server unreachable."}),c(["Server unreachable."]);return}if(g.ok){const y=await e.reloadFromDisk();s(!1),y!=null&&y.ok?b.success("Config saved",{description:g.warnings.length?`${g.warnings.length} warning(s)`:"Saved and applied live."}):b.warning("Config saved",{description:"Saved to disk, but live reload failed. Restart to apply."}),c([])}else s(!1),b.error("Save rejected",{description:g.errors.join(`
2
+ `)}),c(g.errors);if(g.warnings.length)for(const y of g.warnings)b.warning("Warning",{description:y})},[e]),f=h.useCallback(()=>{e.resetDraft(),b.info("Draft reset",{description:"Reverted to last loaded config."})},[e]),P=h.useCallback(async()=>{u(!0);const p=await e.resetToDefault();if(u(!1),!p){b.error("Reset failed",{description:"Server unreachable."});return}p.ok?(b.success("Config reset to defaults",{description:"API key preserved. Limits refetched from upstream."}),c([])):b.error("Reset failed",{description:p.errors.join(`
3
+ `)})},[e]),D=h.useCallback(async()=>{a(!0);const p=await e.refreshFromSource();if(a(!1),!p){b.error("Refresh failed",{description:"Server unreachable."});return}if(p.ok){const g=[`Hard cap set to ${p.hardCap}`,`soft limit set to ${p.softLimit}`];p.requestsHardCap!==null&&p.requestsHardCap!==void 0?g.push(`rate limit hard cap set to ${p.requestsHardCap}`):g.push("rate limit: unlimited"),b.success("Limits reloaded from source",{description:`${g.join(", ")}.`})}else b.error("Refresh failed",{description:p.error})},[e]),M=h.useCallback(async()=>{o(!0);const p=await e.restart();if(!p){b.error("Restart failed",{description:"Server unreachable."}),o(!1);return}p.ok?(b.success("Restarting",{description:p.message??"Server is restarting. Reconnect in a few seconds. Requires a process manager (bun --watch, systemd, pm2) to auto-restart."}),setTimeout(()=>o(!1),5e3)):(b.error("Restart failed",{description:p.error}),o(!1))},[e]);return{save:m,refreshFromSource:D,restart:M,reset:f,resetToDefault:P,loading:{saving:r,refreshingSource:i,restarting:n,resetting:d},validationErrors:l,setValidationErrors:c}}const dt=3e4;function ct(){const e=n=>n??null,{data:r,loading:s,error:i,refresh:a}=ze({endpoint:"/dashboard/api/usage",pollInterval:dt,errorMessage:"Failed to fetch usage",parse:e});return{data:r,loading:s,error:i,refresh:a}}const mt={number:pt,text:V,password:V,json:ht,boolean:()=>({}),select:()=>({}),textarea:()=>({}),toggle:()=>({})};function pt(e,r){const s=r==null?"":String(r),i=Number(s);return s.length===0?{}:Number.isInteger(i)?e.min!==void 0&&i<e.min?{error:`${e.label} must be ≥ ${e.min}`}:e.max!==void 0&&i>e.max?{error:`${e.label} must be ≤ ${e.max}`}:{}:{error:`${e.label} must be an integer`}}function V(e,r){const s=r==null?"":String(r);if(e.pattern&&s.length>0&&!new RegExp(e.pattern).test(s))return{error:`${e.label} must match ${e.patternHint??e.pattern}`};if(e.inputMode==="url"&&s.length>0)try{const i=new URL(s);if(i.protocol!=="http:"&&i.protocol!=="https:")return{error:`${e.label} must use http: or https: protocol`}}catch{return{error:`${e.label} is not a valid URL`}}return{}}function ht(e,r){if(r==null)return{};if(typeof r!="object"||Array.isArray(r))return{error:`${e.label} must be a JSON object`};for(const[s,i]of Object.entries(r))if(typeof i!="number"||!Number.isFinite(i)||i<=0)return{error:`${e.label}: value for "${s}" must be a positive number`};return{}}const ft={rate_limit_requests:e=>Number(e)===-1?"Unlimited — no request cap is enforced. The upstream may still reject excessive traffic.":null};function bt(e,r){const s=r==null?"":String(r);if(e.required&&!e.nullable&&s.length===0)return{error:`${e.label} is required`};if(e.nullable&&s.length===0)return{};const i=mt[e.kind];return i?i(e,r):{}}function gt(e,r){const s={},i={};for(const a of r)for(const n of a.fields){if(n.disabled)continue;const o=bt(n,e[n.key]);o.error&&(s[n.key]=o.error);const d=ft[n.key];if(d){const u=d(e[n.key]);u&&(i[n.key]=u)}}return{errors:s,warnings:i}}function St(){const{config:e,loading:r,error:s,reload:i,save:a,reloadFromDisk:n,refreshFromSource:o,restart:d,resetToDefault:u}=ue(),{data:l}=Le(),{data:c}=ct(),{draft:m,updateField:f,resetDraft:P,isDirty:D,dirtyKeys:M}=lt(e),{save:p,refreshFromSource:g,restart:y,reset:Z,resetToDefault:X,loading:S,validationErrors:T,setValidationErrors:ee}=ut({draft:m,dirtyKeys:M,isDirty:D,resetDraft:P,save:a,reloadFromDisk:n,refreshFromSource:o,restart:d,resetToDefault:u}),[te,F]=h.useState(!1),E=h.useMemo(()=>{const x=((l==null?void 0:l.models)??[]).filter(k=>{var N;return((N=k.info)==null?void 0:N.capabilities.supports_vision)===!0}).map(k=>({value:k.id,label:k.id})).sort((k,N)=>k.label.localeCompare(N.label)),v=(c==null?void 0:c.concurrencyHardCap)??void 0,B=v!==void 0?v-2:void 0,W=(c==null?void 0:c.requestsHardCap)??void 0,q={};return v!==void 0&&(q.concurrency_hard_cap=v,q.concurrency_soft_limit=v,q.concurrency_main_reservation=B,q.concurrency_vision_reservation=B),W!==void 0&&(q.rate_limit_requests=W),Y.map(k=>({...k,sections:k.sections.map(N=>({...N,fields:N.fields.map(A=>{let I=A;return A.key==="vision_model"&&(I={...I,options:x}),A.key in q&&(I={...I,max:q[A.key]}),I})}))}))},[l,c]),{errors:O,warnings:re}=h.useMemo(()=>{if(!m)return{errors:{},warnings:{}};const x=E.flatMap(v=>v.sections);return gt(m,x)},[m,E]),se=Object.keys(O).length>0;function ie(x,v){m&&f(x,v),T.length>0&&ee([])}return r&&!m?t.jsx("div",{className:"flex h-full items-center justify-center",children:t.jsx(de,{})}):s&&!m?t.jsxs("div",{className:"flex h-full flex-col items-center justify-center gap-3 p-8 text-center text-muted-foreground",children:[t.jsx(ye,{className:"h-12 w-12 text-destructive"}),t.jsx("p",{className:"text-sm font-medium text-destructive",children:"Something went wrong"}),t.jsx("p",{className:"text-xs",children:s}),t.jsxs(w,{children:[t.jsx(j,{asChild:!0,children:t.jsxs(_,{variant:"outline",size:"sm",onClick:i,children:[t.jsx(H,{className:"mr-1.5 h-3.5 w-3.5"}),"Retry"]})}),t.jsx(R,{side:"top",children:"Retry loading config"})]})]}):m?t.jsxs("div",{className:"flex h-full flex-col",children:[t.jsxs("header",{className:"flex items-center justify-between border-b border-border bg-card px-4 py-3",children:[t.jsx("h2",{className:"sr-only",children:"Configuration"}),t.jsxs("div",{className:"flex items-center gap-2",children:[t.jsxs(w,{children:[t.jsx(j,{asChild:!0,children:t.jsx(_,{size:"sm",variant:"default",disabled:S.saving||!D||se,onClick:p,children:S.saving?"Saving…":"Save"})}),t.jsx(R,{side:"bottom",children:"Save changes to disk and apply live fields automatically"})]}),t.jsxs(w,{children:[t.jsx(j,{asChild:!0,children:t.jsx(_,{size:"sm",variant:"ghost",disabled:!D,onClick:Z,children:"Reset Draft"})}),t.jsx(R,{side:"bottom",children:"Discard unsaved changes"})]}),t.jsxs(qe,{open:te,onOpenChange:F,children:[t.jsxs(w,{children:[t.jsx(j,{asChild:!0,children:t.jsx(Ne,{render:t.jsxs(_,{size:"sm",variant:"outline",disabled:S.resetting,children:[t.jsx(H,{className:"mr-1.5 h-3.5 w-3.5"}),S.resetting?"Resetting…":"Reset to Default"]})})}),t.jsx(R,{side:"bottom",children:"Reset all config fields to defaults (API key is preserved)"})]}),t.jsxs(De,{children:[t.jsxs(Ce,{children:[t.jsx(Me,{children:"Reset all config to defaults?"}),t.jsx(Te,{children:"This writes the default configuration to disk, replacing all current values. Your API key is preserved. A restart will be needed for some fields to take effect."})]}),t.jsxs(Ie,{children:[t.jsx(Ee,{children:"Cancel"}),t.jsx(Ae,{variant:"destructive",onClick:async()=>{F(!1),await X()},children:"Reset to Default"})]})]})]}),t.jsxs(w,{children:[t.jsx(j,{asChild:!0,children:t.jsxs(_,{size:"sm",variant:"destructive",disabled:S.restarting,onClick:y,children:[t.jsx(ke,{className:"mr-1.5 h-3.5 w-3.5"}),S.restarting?"Restarting…":"Restart"]})}),t.jsx(R,{side:"bottom",className:"max-w-[260px]",children:"Restart the server. Requires an external process manager (bun --watch, systemd, pm2)."})]})]}),D?t.jsxs("span",{className:"text-xs text-muted-foreground",children:[M.size," unsaved change",M.size>1?"s":""]}):t.jsx("span",{className:"text-xs text-muted-foreground",children:"All changes saved"})]}),T.length>0&&t.jsxs("div",{role:"alert","aria-live":"polite",className:"border-b border-destructive/30 bg-destructive/5 px-4 py-2",children:[t.jsx("p",{className:"text-xs font-medium text-destructive",children:T.length===1?"1 issue":`${T.length} issues`}),t.jsx("ul",{className:"mt-1 space-y-0.5",children:T.map(x=>t.jsx("li",{className:"text-xs text-destructive/80",children:x},x))})]}),t.jsx(ce,{className:"min-h-0 flex-1",children:t.jsxs("div",{className:"mx-auto w-full max-w-3xl px-6 py-6",children:[E.map((x,v)=>t.jsx(Ye,{group:x,values:m,originals:e??{},onField:ie,errors:O,warnings:re,isLast:v===E.length-1,onRefreshSource:g,refreshingSource:S.refreshingSource},x.title)),t.jsxs("div",{className:"pt-6 text-xs text-muted-foreground",children:["Fields marked"," ",t.jsxs("span",{className:"text-muted-foreground",children:[t.jsx(G,{className:"inline h-3 w-3 mr-1","aria-hidden":!0}),"restart"]})," ","require a server restart to take effect. Other fields are applied live when you save."]})]})})]}):null}export{St as ConfigTab};
4
+ //# sourceMappingURL=config-tab-pnBjiSyC.js.map