feat(tunnel): add named tunnel support for custom domains - #85
Open
mahdiwafy wants to merge 98 commits into
Open
feat(tunnel): add named tunnel support for custom domains#85mahdiwafy wants to merge 98 commits into
mahdiwafy wants to merge 98 commits into
Conversation
…apacity warning note
…+ usageMetadata) in parseSSEToOpenAIResponse
The parseSSEToOpenAIResponse function only understood OpenAI SSE format
(choices[0].delta.content, root-level usage). Antigravity/Gemini SSE
chunks use a different schema:
data: {"response":{"candidates":[{...}],"usageMetadata":{...}}}
- Text content from response.candidates[0].content.parts[].text
- Reasoning from .parts[].thought
- Finish reason from candidate.finishReason
- Usage from response.usageMetadata (promptTokenCount, candidatesTokenCount,
thoughtsTokenCount, totalTokenCount)
Also moves the OpenAI usage extraction before the AG block so the
existing path is preserved for OpenAI-formatted chunks.
…age tracking Tool-call-only turns (agentic coding clients like Claude Code/Kiro sending 100+ tools) stream their real output entirely through delta.tool_calls (and delta.partial_json for Claude tool_use), with delta.content and delta.reasoning_content empty. totalContentLength in createSSEStream() only summed content/reasoning length, so these turns left it stuck at 0. That starved both usage fallbacks: - estimateUsage() (provider sent no/invalid usage at all) - the new hasZeroCompletionWithContent()/fixZeroCompletionUsage() path (provider sent completion_tokens: 0 despite real output) Net effect: usage logs showed "out=0" for most tool-heavy turns even though the client received a large tool_calls payload — matching the live claude-sonnet-5 report on VansRouter (a5a5b570 account/Shiteru). Changes: - stream.js: sum delta.tool_calls[].function.name/arguments length (both PASSTHROUGH and TRANSLATE modes) and delta.partial_json (Claude tool_use) into totalContentLength. - usageTracking.js: add hasZeroCompletionWithContent() and fixZeroCompletionUsage() to patch a provider-reported zero completion count without discarding its (usually accurate) prompt_tokens. - Wire the new fallback into all 4 usage-decision sites in stream.js. Verified: reproduced the exact bug shape in a new integration test (in=166912, completion_tokens: 0 finish chunk, pure tool_calls output) — now estimates a non-zero completion count instead of logging 0. Deployed the fix live to the running container and confirmed in production logs: claude-sonnet-5 tool-heavy turns now log out=44/56/322/etc instead of a long run of out=0. Separately observed (NOT fixed by this change, needs its own investigation): a distinct pattern of genuinely empty completions (response.content === "[Empty streaming response]", no tool_calls either) from the Shiteru-backed claude-sonnet-5 route once prompt size exceeds ~130k tokens — confirmed via requestDetails table, not a usage- counting artifact.
…ojectId persistence, DuckDuckGo browser profile - ModelRow: empty draft on Save now deletes the alias instead of silently no-op - allowedModels: isModelAllowed enforces global list even without apiKeyInfo; appendUserAliasEntries surfaces bare alias names; cache invalidated on alias/disabled mutations - v1/models: alias entries pass through with owned_by=alias - chat.js + chatCore + stream/nonStreaming: echo client-facing model id (alias) back in /v1/chat/completions response.model and SSE chunks - projectId: cache TTL 30d bound to Google account not OAuth token; skip re-fetch when DB already has projectId (seedProjectIdCache) - settingsRepo + systemInject + chatCore: injectDefaultSystemPrompt only when request has no system message yet - DuckDuckGo: undici Agent (HTTP/2) + stable Chrome 131 header set (Sec-CH-UA client hints, full Accept)、 replacing the bot UA - registry/index.js: register duckduckgo provider - duckduckgo.png icon
…h label) validate/route.js had two `case "agentrouter":` labels in the same switch. JS only runs the first match, so agentrouter connections were silently validated by the generic glm/kimi/minimax block (bare x-api-key + anthropic-version, no headers) instead of validateAgentRouterConnection()'s Claude-CLI spoof headers. AgentRouter gates on client fingerprint and returns 401 'unauthorized client detected' for any request missing the spoof headers/UA — which the dashboard's Add API Key flow then reported as 'Invalid API key' even for a valid, unrestricted key. Removed agentrouter from the shared fallthrough case so it reaches the dedicated block. Verified live against the real AgentRouter API: the old (buggy) header set gets a genuine 401 from the same key that gets a non-401/403 response through validateAgentRouterConnection(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ve claude-opus-4-7
…atalog NVIDIA NIM chat/completions returned errors for several registry models: - z-ai/glm-5.1 → HTTP 410 Gone (EOL 2026-07-02). This is the primary failure users hit — the model is globally retired. - moonshotai/kimi-k2.6 (+ none/low/medium/high thinking variants) → HTTP 404 "not found for account": catalog-listed but not served on standard NIM developer keys. suggestedModels also carried dead ids: - z-ai/glm-5.1 (410 EOL), qwen/qwen3.5-397b-a17b (404 account), qwen/qwen3.5-122b-a10b and mistralai/mistral-large-3-675b-instruct-2512 (absent from /v1/models entirely). Replaced with models verified against integrate.api.nvidia.com (present in /v1/models and returning 200 on a real completion): GLM 5.2, DeepSeek V4 Pro/Flash, MiniMax M2.7/M3, GPT-OSS 120B/20B, Llama 4 Maverick, Step 3.7 Flash, Nemotron 3 Ultra/Super 120B, Nemotron Super 49B v1.5. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…atalog NVIDIA NIM chat/completions returned errors for several registry models: - z-ai/glm-5.1 → HTTP 410 Gone (EOL 2026-07-02). This is the primary failure users hit — the model is globally retired. - moonshotai/kimi-k2.6 (+ none/low/medium/high thinking variants) → HTTP 404 "not found for account": catalog-listed but not served on standard NIM developer keys. suggestedModels also carried dead ids: - z-ai/glm-5.1 (410 EOL), qwen/qwen3.5-397b-a17b (404 account), qwen/qwen3.5-122b-a10b and mistralai/mistral-large-3-675b-instruct-2512 (absent from /v1/models entirely). Replaced with models verified against integrate.api.nvidia.com (present in /v1/models and returning 200 on a real completion): GLM 5.2, DeepSeek V4 Pro/Flash, MiniMax M2.7/M3, GPT-OSS 120B/20B, Llama 4 Maverick, Step 3.7 Flash, Nemotron 3 Ultra/Super 120B, Nemotron Super 49B v1.5. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The wasm32 fallback was in the main dependencies block with a pinned 4.3.0 version, causing npm install to fail with EBADPLATFORM on x64 hosts (and any non-wasm32 CPU). Move it to optionalDependencies mirroring the better-sqlite3 pattern and bump the spec to ^4.3.3 to match the resolved @tailwindcss/oxide version. Drop the non-standard comment_better_sqlite3 metadata key while here.
Hardcoded dev origins forced every contributor into the same localhost setup. Read VANS_ALLOWED_DEV_ORIGINS (comma-separated) with the prior 127.0.0.1,localhost as the default. Prod is unaffected — Next 16 ignores allowedDevOrigins outside dev mode.
Race: resolvePrivilegedUserId().then(...) was scheduled without await in
transformRequest, so the first request always went out without the
x-gemini-api-privileged-user-id header (the .then microtask could not
drain before buildHeaders ran synchronously). Fix:
- Prewarm the cache at module load so it is ready before any request.
- Make transformRequest await resolvePrivilegedUserId() so the header
is present on the very first request. Await is safe because
BaseExecutor.execute now wraps transformRequest in
await Promise.resolve(...), keeping the sync-return contract for
every other executor.
Mutation: stripOpenAILeakFields/applyIncludeReasoningToGemini mutated the
input body in place, leaving loggers, error DB saves, and the 401/403
retry path reading a stripped body instead of the client's original.
Fix: structuredClone(body) at the start of transformRequest so the
original translatedBody stays pristine for downstream consumers.
…branch webSearchInject.js (125 lines) was never imported by chatCore, chat, or any executor — the live web-search path lives inline in chat.js. Drop the orphan module. Simplify the redundant provider.id === 'duckduckgo' check in search/index.js (registry already sets executor: 'duckduckgo'). Remove the unused bodyHasReasoning variable in chat.js that nothing read.
The /^[a-zA-Z0-9_.\-]+$/ regex and its hint strings were copied across two API routes and the dashboard page. Extract COMBO_NAME_REGEX, COMBO_NAME_HINT, COMBO_ALIAS_HINT into src/shared/constants/comboValidation.js. Behavior unchanged: same validation, same error messages, same status codes.
Catch-block console.log calls in API routes wrote errors to stdout
instead of stderr, mixing diagnostics with normal output. Convert them
to console.error across providers/[id]/models, v1beta/models
([GEMINI_NATIVE] telemetry), keys, models/alias, models/disabled,
proxy-pools/[id], and cli-tools/claude-settings. Remove the stray
console.log('Deno deployUrl:', deployUrl) in proxy-pools/deno-deploy
that leaked every freshly-minted relay URL to stdout.
When PUT /api/keys/[id] received an allowedModels/allowedProviders/ allowedCombos/allowedKinds value that was neither null nor an array (string, object, number), it silently coerced to null — meaning 'all allowed' — masking caller bugs as over-permissive defaults. Default unexpected types to [] (deny-all) for all four ACL fields, matching the documented contract: null = all, [] = none, [...] = specific.
The system-prompt save handler swallowed all failures silently (empty
catch), leaving users with no indication the save failed. Reuse the
file's existing {type, message} status pattern to surface errors next
to the Save button. Remove the unused isDark destructure from
useTheme() (never referenced in the file).
The chromium executablePath was pinned to /home/vanszs/.cache/ ms-playwright/chromium-1228/chrome-linux64/chrome — one contributor's absolute home path that fails on every other machine. Read VANS_ZCODE_CHROMIUM_PATH from the environment; pass undefined to let playwright-core use its own channel discovery when the env var is unset.
The dev prewarm script fell back to '123456' when VANS_PREWARM_PASSWORD was unset, silently authenticating with the default dashboard password in any environment that forgot to set the env var. Fail closed: if the env var is missing, print a clear error and exit non-zero so the operator sets it explicitly.
The per-process _binCache/_pyCache/_extrasCache never exposed an invalidation path, so installing headroom-ai while the server was running left the dashboard reporting 'not installed' until process restart. Export invalidateHeadroomCaches() so the install endpoint can reset the probes after a successful install.
Extend the shared COMBO_NAME_REGEX to ComboFormModal and the media-providers combo edit page, the two remaining copies of the /^[a-zA-Z0-9_.\-]+$/ regex that were protected by the first DRY pass. Behavior unchanged.
The /api/models response is always { models: [...] } by convention, so
the Array.isArray(data) fallback at line 526 was dead code reachable
only if the endpoint changed shape. Remove it. Restore 'No providers'
capitalization to match the adjacent 'No models' badge.
The commit that converted login-page strings from inline Bahasa to inline English left Indonesian-cookie users seeing English, because no corresponding keys existed in public/i18n/literals/id.json and the runtime DOM translator only matches known English source strings. Wrap the placeholder, dispatch error fallbacks, button label, and OIDC fallback in translate() from @/i18n/runtime (attributes and state-driven strings are not reached by the MutationObserver-driven DOM walker). Add the missing English keys to id.json, using the pre-commit Bahasa strings as the Indonesian translations.
claude.ai/oauth/authorize rejects our redirect_uri (https://api.bevansatria.my.id/callback) with "Redirect URI ... is not supported by client" because it isn't on the whitelist for the public Claude Code client_id. Switch to the same redirect Anthropic's own Claude Code CLI uses (https://platform.claude.com/oauth/code/callback), which is whitelisted. Since that redirect lands on Anthropic's domain instead of our own /callback route, the popup can never postMessage/BroadcastChannel the code back to us — force manual-paste mode for the claude provider and accept Anthropic's bare `code#state` callback format (not a full URL). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Claude Code (cc) provider's model list stopped at Opus 4.8 and was missing the entire Claude 5 generation — even though the CLI defaults in cliTools.js already point at cc/claude-fable-5 and cc/claude-sonnet-5. Add the three generally-available Claude 5 models (per Anthropic's model docs), keeping 4.x as legacy entries: - claude-fable-5 ($10/$50, 1M ctx, adaptive thinking always-on) - claude-opus-5 ($5/$25, 1M ctx, adaptive thinking) - claude-sonnet-5 ($3/$15, 1M ctx, adaptive thinking) Also add exact capabilities entries for claude-opus-5 / claude-fable-5 (so the generic *claude* pattern doesn't downgrade them to claude-budget thinking) and canonical pricing for claude-opus-5 / claude-sonnet-5. claude-mythos-5 is intentionally omitted — it is invitation-only (Project Glasswing) and not generally accessible. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Match the official Claude Code OAuth client scope set by requesting user:sessions:claude_code and user:mcp_servers in addition to the existing profile/inference scopes. Tokens minted without these scopes can connect for basic OAuth but fail Claude Code session/usage endpoints or org policy checks. Also send the same Claude CLI fingerprint headers to the OAuth usage endpoint that we already use for Claude Code inference. When Anthropic rejects OAuth for an organization, surface an explicit org-policy message instead of falling back to the misleading legacy admin-permissions text. Note that oauth_not_allowed_for_organization is still an Anthropic org policy decision; users must reconnect after this change to mint a fresh scoped token, and accounts in orgs that disable OAuth still need an org admin or a different Claude account. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds tracking-based weekly usage buckets alongside the existing 5H windows: - New antigravityWeeklyTracker module: counts request rows in usageHistory for the last 7 days, grouped into Gemini / Claude+GPT buckets - getAntigravityUsage accepts connectionId and attaches gemini_weekly + claude_gpt_weekly buckets with estimated totals (4000 req/week) - parseQuotaData forwards isWeekly flag; QuotaTable shows a 'Weekly' badge and tilde-prefixed reset countdown for weekly rows
…tdown - parseQuotaData skips raw_gemini/raw_claude internal payloads that were rendering as misleading 0/∞ rows (Gemini 3.5 Flash (High) / GPT-OSS) - Weekly reset now starts from each bucket's first use + 7 days, matching Antigravity's behavior where the weekly window countdown begins on first request (same principle as the 5H window)
Adds tracking-based weekly usage buckets alongside the existing 5H windows: - New antigravityWeeklyTracker module: counts request rows in usageHistory for the last 7 days, grouped into Gemini / Claude+GPT buckets - getAntigravityUsage accepts connectionId and attaches gemini_weekly + claude_gpt_weekly buckets with estimated totals (4000 req/week) - parseQuotaData forwards isWeekly flag; QuotaTable shows a 'Weekly' badge and tilde-prefixed reset countdown for weekly rows
…tdown - parseQuotaData skips raw_gemini/raw_claude internal payloads that were rendering as misleading 0/∞ rows (Gemini 3.5 Flash (High) / GPT-OSS) - Weekly reset now starts from each bucket's first use + 7 days, matching Antigravity's behavior where the weekly window countdown begins on first request (same principle as the 5H window)
…tion_call + dedupe [DONE] in passthrough
- test-models: resolve alias from provider node prefix (st, atmo, relayn) instead of raw UUID providerId — fixes 404 'model not found' for all custom provider nodes
- stream: strip empty legacy function_call {name:'',arguments:''} deltas that cause client loops (Shiteru sends these on final chunk)
- stream: skip upstream [DONE] in passthrough so flush handler emits exactly one terminator
- Support both token-based (TUNNEL_TOKEN) and credentials-file-based (TUNNEL_CRED_FILE + TUNNEL_ID) auth modes - Add proper ingress config generation for named tunnels (required for routing) - Fix cloudflared argument ordering: --config and --no-autoupdate must come BEFORE 'run' subcommand - Fix intentionalKill flag reset after killCloudflared() to surface real startup errors - Update getTunnelStatus() to include 'named' flag and correct publicUrl for named mode - Export spawnNamedTunnel from tunnel index Verified end-to-end: enables tunnel on custom domain (vr.mahdiwafy.my.id), Cloudflare edge reachability confirmed, /v1/models API returns 200 OK with valid API key.
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.
Summary
Add support for named tunnels (custom domains) in addition to the existing quick tunnel (). This allows operators to expose VansRouter on their own hostname (e.g., ) instead of a random subdomain.
Changes
1. Dual Auth Modes
2. Required Config
3. Technical Fixes
4. API Changes
Verification
Tested end-to-end on production:
Deployment Notes
For production use with credentials file mode:
The credentials file should contain from or Incorrect Usage: flag needs an argument: -cred-file
NAME:
cloudflared tunnel token - Fetch the credentials token for an existing tunnel (by name or UUID) that allows to run it
USAGE:
cloudflared tunnel [tunnel command options] token [subcommand options] TUNNEL
DESCRIPTION:
cloudflared tunnel token will fetch the credentials token for a given tunnel (by its name or UUID), which is then used to run the tunnel. This command fails if the tunnel does not exist or has been deleted. Use the flag
cloudflared tunnel token --cred-file /my/path/file.json TUNNELto output the token to the credentials JSON file. Note: this command only works for Tunnels created since cloudflared version 2022.3.0TUNNEL COMMAND OPTIONS:
--config value Specifies a config file in YAML format. (default: "/home/mahdiwafy/.cloudflared/config.yml")
--origincert value Path to the certificate generated for your origin when you run cloudflared login. [$TUNNEL_ORIGIN_CERT]
--autoupdate-freq value Autoupdate frequency. Default is 24h0m0s. (default: 24h0m0s)
--no-autoupdate Disable periodic check for updates, restarting the server with the new version. (default: false) [$NO_AUTOUPDATE]
--no-prechecks Skip connectivity pre-checks at startup. (default: false) [$TUNNEL_NO_PRECHECKS]
--metrics value Listen address for metrics reporting. If no address is passed cloudflared will try to bind to [localhost:20241 localhost:20242 localhost:20243 localhost:20244 localhost:20245].
If all are unavailable, a random port will be used. Note that when running cloudflared from an virtual
environment the default address binds to all interfaces, hence, it is important to isolate the host
and virtualized host network stacks from each other (default: "localhost:0") [$TUNNEL_METRICS]
--pidfile value Write the application's PID to this file after first successful connection. [$TUNNEL_PIDFILE]
--loglevel value Application logging level {debug, info, warn, error, fatal}. At debug level cloudflared will log request URL, method, protocol, content length, as well as, all request and response headers. This can expose sensitive information in your logs. (default: "info") [$TUNNEL_LOGLEVEL]
--transport-loglevel value, --proto-loglevel value Transport logging level(previously called protocol logging level) {debug, info, warn, error, fatal} (default: "info") [$TUNNEL_PROTO_LOGLEVEL, $TUNNEL_TRANSPORT_LOGLEVEL]
--logfile value Save application log to this file for reporting issues. [$TUNNEL_LOGFILE]
--log-directory value Save application log to this directory for reporting issues. [$TUNNEL_LOGDIRECTORY]
--trace-output value Name of trace output file, generated when cloudflared stops. [$TUNNEL_TRACE_OUTPUT]
--output value Output format for the logs (default, json) (default: "default") [$TUNNEL_MANAGEMENT_OUTPUT, $TUNNEL_LOG_OUTPUT]
SUBCOMMAND OPTIONS:
--credentials-file value, --cred-file value Filepath at which to read/write the tunnel credentials [$TUNNEL_CRED_FILE]
--help, -h show help (default: false)
.