This document details all custom logic added on top of the upstream 9router codebase. Each section covers the feature's purpose, flow, files, and behavioral rules.
Restrict which providers, combos, service kinds, and models each API key can access. Enables multi-tenant setups where different users get different levels of access.
All ACL fields use the same tri-state pattern:
null/undefined→ all allowed (permissive default, unrestricted key)[](empty array) → none allowed (deny everything)["x", "y"]→ only listed items allowed (whitelist)
Request arrives
↓
1. isTrustedInternalRequest? → bypass ALL ACL (machine-bound CLI token)
↓
2. requireApiKey enabled? → validate API key (isValidApiKey)
↓
3. isKindAllowed(apiKeyInfo, "llm") → 403 if denied
↓
4. Is model a combo?
├── Yes → isComboAllowed(apiKeyInfo, comboName) → 403 if denied
└── No → isProviderAllowed(apiKeyInfo, provider) → 403 if denied
↓
5. isModelAllowed(resolvedModelStr, apiKeyInfo) → 404 if denied
↓
6. Proceed to upstream provider call
Identical ACL flow but uses isKindAllowed(apiKeyInfo, "stt") instead of "llm".
- File:
src/sse/services/internalTrust.js - Reads
x-9r-cli-tokenheader - Computes expected token from
getConsistentMachineId("9r-cli-auth")(16-char hex, machine-bound) - Compares using
crypto.timingSafeEqual(constant-time, no timing side-channel) - Fails closed: missing header, wrong length, bad token, exception → all return
false - Token is memoized after first computation
- File:
src/sse/services/auth.js(line ~379) - Checks
apiKeyInfo.allowedKindsagainst the service kind - Kinds:
"llm","embedding","image","tts","stt","web"
- File:
src/sse/services/auth.js(line ~344) - Checks
apiKeyInfo.allowedProvidersagainst the provider - Resolution chain: direct ID → alias → resolved ID → provider node prefix (cached 30s TTL)
- File:
src/sse/services/auth.js(line ~365) - Checks
apiKeyInfo.allowedCombosagainst the combo name - Strips
combo/prefix before matching
- File:
src/sse/services/allowedModels.js(line ~461) - Builds full allowed model ID set via
getAllowedModelIds()(cached 30s) - Checks both alias form and resolved
provider/modelform modelKind()function resolves kind frommodel.kind,model.type, or defaults to"llm"ALL_KINDS:["llm", "tts", "embedding", "image", "imageToText", "stt", "webSearch", "webFetch"]
- File:
src/app/api/v1/models/route.js(line ~456) - After building full model list, filters by ACL if
apiKeyInfois present - Combos →
isComboAllowed+stripComboPrefix - Regular models →
isProviderAllowed - Uses
Promise.allfor parallel evaluation - Result: restricted key sees only allowed models (e.g., 19 instead of 108)
tests/unit/handler-acl-enforcement.test.js— 37 tests covering all ACL layerstests/unit/all-endpoints-robust.test.js— 24 endpoint tests with ACL keys
Inject a "lazy senior developer" ruleset into the system prompt to reduce unnecessary code output. Orthogonal to Caveman: Ponytail governs what the model builds; Caveman governs how it talks.
Every POST /v1/chat/completions
→ handleChatCore() in chatCore.js
→ translateRequest() (format conversion)
→ RTK compressMessages() (context compression)
→ Headroom compressWithHeadroom() (optional external proxy compression)
→ injectCaveman() (if enabled)
→ injectPonytail() (if enabled) ← HERE
→ executor.execute() (send to provider)
open-sse/rtk/ponytail.js— Injector (delegates toinjectSystemPromptfromsystemInject.js)open-sse/rtk/ponytailPrompts.js— Prompt text for all 3 levelsopen-sse/rtk/systemInject.js— Shared format-dispatched system prompt injector (used by caveman + ponytail)open-sse/handlers/chatCore.js(line ~159) — Injection point
| Level | Behavior |
|---|---|
lite |
Build what's asked, name the lazier alternative in one line |
full |
Ladder enforced — stdlib and native first, shortest diff |
ultra |
YAGNI extremist — deletion before addition, ship one-liner |
| Component | Purpose |
|---|---|
SHARED_LADDER |
6-rung decision ladder: skip → stdlib → native → dep → one-line → minimal |
SHARED_RULES |
No unrequested abstractions, no boilerplate, boring > clever |
SHARED_BOUNDARIES |
Never simplify: validation, error handling, security, accessibility |
SHARED_SKEPTICAL |
6 verification rules (see section 3 below) |
SHARED_OUTPUT |
Code first, then ≤3 lines explaining what was skipped |
SHARED_PERSISTENCE |
Active every response, no drift back to over-building |
ponytailEnabled(boolean, defaultfalse)ponytailLevel(string, default"full")- Toggled in Dashboard → Endpoint page → Token Saver section
Mandate evidence-based claims from AI agents working on this codebase or responding via Ponytail. Prevents false "fixed" claims, fabricated test reports, and hidden regressions.
- No assumptions without evidence — Never claim "fixed"/"working"/"correct" without concrete proof
- Be skeptical of own results — Verify tests test what you think, check side effects
- Never fabricate reports — Don't claim "all tests pass" without running them
- Distinguish pre-existing vs caused-by-me — Run tests BEFORE and AFTER, diff results
- Report honestly — If broken, say so. If skipped, explain why. If caveats, state them
- Verify before declaring done — Run tests AFTER changes, show actual output
open-sse/rtk/ponytailPrompts.jsline ~40 (SHARED_SKEPTICAL) — injected into every Ponytail-enabled requestAGENTS.mdlines 1–30 — static rules for AI agents scanning the repo- Both locations have the same 6 rules in different formats
Self-hosted search provider for the webSearch service kind, no API key needed.
- File:
open-sse/providers/registry/searxng.js baseUrl: "http://127.0.0.1:8888/search"— hardcoded to localhost- Docker container:
searxng/searxng:latest, port 8888 authType: "none",noAuth: truesearchTypes: ["web", "news"]costPerQuery: 0,freeMonthlyQuota: 999999timeoutMs: 10000,cacheTTLMs: 180000
The search handler expects model: "searxng" (provider ID), NOT "searxng/search" (full path from /v1/models/web).
Add default Indonesian and regional voices to the Edge-TTS provider.
- File:
open-sse/config/ttsModels.js(line ~74) id-ID-ArdiNeural— Indonesian maleid-ID-GadisNeural— Indonesian femaleth-TH-PremwadeeNeural— Thaims-MY-YasminNeural— Malaytl-PH-BlessicaNeural— Filipino/Tagalog- Plus Vietnamese, Chinese, Japanese, Korean defaults
Opt-in setting allowing non-loopback requests to reach /v1/* without an API key.
Only effective when requireApiKey is OFF. Fail-closed default.
Request to /v1/*
→ dashboardGuard.js middleware
→ isLocalRequest? → allow (loopback)
→ hasValidCliToken? → allow
→ hasValidApiKey? → allow
→ requireApiKey !== true AND allowRemoteNoApiKey === true? → allow
→ else → 401 "API key required for remote API access"
src/dashboardGuard.js(line ~128) —canAccessPublicLlmApi()functionsrc/lib/db/repos/settingsRepo.js(line ~19) — defaultallowRemoteNoApiKey: falsesrc/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js— UI toggle (shown when Require API Key is off)
tests/unit/dashboard-guard.test.js— 10 allowRemoteNoApiKey tests (28 total in file)
- File:
open-sse/translator/concerns/message.js(line ~1) - If ALL parts are text → join with
"\n"into plain string (OpenAI canonical format) - Otherwise → return array as-is (mixed content with images)
- Used in
open-sse/translator/formats/openai.jsfilterToOpenAIFormat()
- File:
open-sse/utils/reasoningContentInjector.js - Injects
" "(single space) asreasoning_contentplaceholder for thinking-mode providers - Provider-level:
PROVIDERS[provider]?.reasoningInject - Model-level: regex
/kimi-/i→ scope"toolCalls",/deepseek/i→ scope"all" - Only on
role === "assistant"messages, skips if already non-empty
- File:
open-sse/services/combo.js(line ~115) - Scans
body.toolsfortype === "web_search"→ adds"search"to required capabilities - Ensures combo auto-switch floats search-capable models to the front
- Returns original array reference when no reordering needed (no required capabilities, single model, or no model matches any hard capability)
- Prevents pointless reshuffling
- Strips
"combo/"prefix from combo names - Imported by
models/route.jsfor ACL filtering
src/app/layout.js— Title: "VansAI - AI Infrastructure Management"src/dashboardGuard.js— API welcome: "Welcome to VansAI! Use {baseUrl}/v1 as your API endpoint."- Default fallback host:
api.bevansatria.my.id
| File | Bug | Fix |
|---|---|---|
errorConfig.js |
Missing COOLDOWN_MS export | Added backward-compat COOLDOWN_MS object (upstream later fixed this too) |
rtk/constants.js |
LS_NOISE_DIRS was Array | Changed to Set for O(1) lookup |
rtk/index.js |
setRtkEnabled/isRtkEnabled broken | Fixed boolean logic |
combo.js |
web_search not detected in tools | Added scanning for type === "web_search" |
combo.js |
reorderByCapabilities no early return | Added early return for empty/single/no-match cases |
embeddings.js |
Double-prefix normalization (nvidia/nvidia/model) |
Build candidates array checking multiple forms |
message.js |
collapseTextParts only handled single text part | Now joins ALL text parts with \n |
reasoningContentInjector.js |
Regex pattern incorrect | Fixed regex for provider/model matching |
Custom provider for Z.ai (ZCode) — a Claude-format LLM provider offering GLM-5.x models via ZCode Plan's API with CAPTCHA solving and OAuth token management.
- File:
open-sse/providers/registry/zcode.js - ID:
zcode, aliaszc, priority141 - Transport:
baseUrl: https://api.z.ai/api/anthropic/v1/messages, formatclaude - Auth:
x-api-keyheader (raw scheme, combined) - Models:
GLM-5.2,GLM-5.2-Max,GLM-5-Turbo,GLM-5-Turbo-Max
- File:
open-sse/executors/zcode.js - Proxy endpoint:
https://zcode.z.ai/api/v1/zcode-plan/anthropic/v1/messages - Auth:
Bearer {zcodeJwtToken}fromcredentials.providerSpecificData - Spoof headers: Full ZCode Desktop v3.1.0 fingerprint (User-Agent, platform, timezone)
- CAPTCHA solving: Pre-flight Aliyun CAPTCHA via headless Playwright Chromium, cached 4 min TTL. On 401/403, re-solve and retry.
- Reasoning models:
-Maxsuffix stripped for upstream call,thinking: { type: "enabled", budget_tokens: 4096 }injected.max_tokensauto-bumped if too low. - Token refresh: POSTs
accessTokentoapi.z.ai/api/auth/z/loginfor freshbusinessToken.
- File:
src/lib/oauth/constants/oauth.js—ZAI_CONFIG = { ...PROVIDER_OAUTH["zcode"] } - Flow:
authorization_codewith custom scheme redirect (zcode://zai-auth/callback) - User opens auth URL → browser shows
ERR_UNKNOWN_URL_SCHEME→ copies callback URL → pastes into OAuthModal - Token exchange returns
accessToken,refreshToken,zcodeJwtToken - Post-exchange: user info fetch → business token exchange → subscription/plan fetch (quota pools, model access)
| File | Purpose |
|---|---|
open-sse/executors/zcode.js |
Executor with CAPTCHA + spoof headers |
open-sse/providers/registry/zcode.js |
Provider definition + models |
src/lib/oauth/constants/oauth.js |
ZAI_CONFIG + ZCODE in PROVIDERS |
src/shared/components/OAuthModal.js |
zcode:// URL parsing for manual paste |
Allow operators to create multiple connections (each with its own API key) on the same compatible provider node, enabling round-robin/sticky routing across accounts for load balancing and quota management.
- File:
src/app/api/providers/route.js - Removed the one-connection-per-node guard (
getProviderConnectionscheck + 400 rejection) for:- OpenAI-compatible providers
- Anthropic-compatible providers
- Custom-embedding providers
| Provider Type | Merged Fields |
|---|---|
| OpenAI-compatible | prefix, apiType, baseUrl, nodeName |
| Anthropic-compatible | prefix, baseUrl, nodeName |
| Custom-embedding | prefix, baseUrl, nodeName |
If the node row doesn't exist (deleted between listing and POST), returns 404.
932698a2 — fix(providers): allow multiple connections per compatible node (+10, −43)
| File | Purpose |
|---|---|
open-sse/rtk/ponytail.js |
Ponytail injector |
open-sse/rtk/ponytailPrompts.js |
Ponytail prompt levels + skeptical rules |
open-sse/executors/zcode.js |
ZCode executor (CAPTCHA + spoof + reasoning) |
open-sse/providers/registry/zcode.js |
ZCode provider definition + GLM models |
src/sse/services/allowedModels.js |
Model allowlist (isModelAllowed, modelKind) |
src/sse/services/internalTrust.js |
Trusted internal request detection |
AGENTS.md |
Behavioral rules for AI agents |
CUSTOM_LOGIC.md |
This file |
tests/unit/all-endpoints-robust.test.js |
Comprehensive endpoint tests (38) |
tests/unit/handler-acl-enforcement.test.js |
ACL enforcement tests (37) |
| File | Custom Change |
|---|---|
src/sse/handlers/chat.js |
ACL enforcement (isKind/provider/combo/model) + apiKeyInfo propagation |
src/sse/handlers/stt.js |
ACL enforcement (same pattern as chat.js) |
src/app/api/v1/models/route.js |
ACL filter for /v1/models + capabilitiesFromServiceKind import |
open-sse/handlers/chatCore.js |
Ponytail injection section |
open-sse/config/ttsModels.js |
Indonesian + SE Asian voices |
open-sse/providers/registry/searxng.js |
Local baseUrl 127.0.0.1:8888 |
open-sse/translator/concerns/message.js |
collapseTextParts fix |
open-sse/translator/formats/openai.js |
Import + use collapseTextParts |
open-sse/utils/reasoningContentInjector.js |
Regex fix |
open-sse/services/combo.js |
web_search detection + early return + stripComboPrefix export |
src/app/layout.js |
VansAI branding + upstream GA/font script |
src/dashboardGuard.js |
canAccessPublicLlmApi with allowRemoteNoApiKey gate |
src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js |
allowRemoteNoApiKey toggle UI |
src/app/api/providers/route.js |
Multi-connection per compatible node (removed one-connection guard) |
tests/unit/dashboard-guard.test.js |
10 allowRemoteNoApiKey tests (28 total) |