Worktree api key simple advanced prd#4901
Conversation
…m AIRBOTIX.md 5-minute docker-compose quickstart, first-time admin setup with curl test, Airbotix-specific User schema extensions (4 new fields), upstream sync workflow.
…} skeleton + CI model/user.go: extend User with 4 Airbotix fields (kids_mode, policy_profile, billing_webhook_url, custom_pricing_id). AutoMigrate picks these up automatically. Additive only — no upstream behaviour changed. internal/kids: pure helpers for kids_mode hard constraints — IsModelEligible (whitelist), StripIdentifyingMetadata, EnforceZeroDataRetention (OpenAI ZDR), ChildSafeSystemPrompt. + table-driven tests. internal/policy: PolicyProfile types (passthrough / adult / kid-safe) + DecisionFor which collapses KidsMode + Profile into a single Decision struct the relay code consults. + tests verifying the KidsMode=true override forces all hard constraints. internal/billing: HMAC-SHA256 SignPayload + Dispatcher with retries on 5xx/transient, stop-on-permanent-4xx, exponential backoff. + httptest-driven tests for success / retry / stop-on-4xx. .github/workflows/airbotix-internal.yml: CI runs on changes to model/user.go and internal/** only (path-filtered to avoid upstream NewAPI's matrix). go vet + go build + go test -race. Wiring into the relay path is intentionally deferred — these compile as leaves so they can be merged + tested before any controller change.
…list CI caught: 'claude-3-5-sonnet-20241022' must match. Storing 'claude-3-5-sonnet' as the base name now matches both '-latest' and '-YYYYMMDD' variants via HasPrefix.
…aunch 6 phases (Foundation DONE → Tenant mgmt → Wiring P0 → Providers → Moderation+billing → Migration + Prod launch), each with concrete tasks (file paths), acceptance criteria, and per-phase risks. Critical-path graph: P2 (relay wiring) blocks Kids OpenCode integration. Anthropic Tier accumulation is upstream of everything (Lightman-owned Week 0). Open decisions blocking phases tracked separately so engineer can flag in weekly sync. Risk register copied here from PRD for in-context awareness. Includes weekly cadence + 'V0 Launched' definition (7 simultaneous criteria) for unambiguous done state.
docs/tenant-onboarding.md — step-by-step manual tenant creation. Includes SQL workaround for the 4 Airbotix fields until admin UI ships them (Phase 1 task linked). docs/tasks/phase-1-admin-ui.md — detailed engineer task spec for adding the 4 fields to the admin UI. File-by-file changes (types.ts, user-form.ts, users-mutate-drawer.tsx, users-columns.tsx, i18n) with concrete code snippets. ~100 LOC additive, no upstream code deleted (clean rebase). model/user_airbotix_test.go — reflection-based guard test ensuring upstream merges don't accidentally remove the 4 Airbotix fields. Plus zero-value semantics test and round-trip test. CI: extend airbotix-internal workflow to run the model/ guard tests (TestUser_Airbotix*) in addition to internal/ tests.
…ase 1 tenants Sets the 4 Airbotix fields (kids_mode, policy_profile, billing_webhook_url, custom_pricing_id) on an existing NewAPI user. Bridges the gap until docs/tasks/phase-1-admin-ui.md ships the UI form. Auto-detects docker-compose.yml vs docker-compose.dev.yml. Supports --dry-run. Validates --policy enum. Idempotent UPDATE. Verified docs/tenant-onboarding.md references this script in Step 3.
…ay policy/billing wired Phase 1 (admin): - model/user.go: add WebhookSecret column; Edit() now persists all 5 Airbotix fields (kids_mode / policy_profile / billing_webhook_url / custom_pricing_id / webhook_secret) so the admin UI save round-trips - web/default frontend: 5 form fields under a new "Airbotix Tenant Settings" section in the user edit drawer (Switch / Select / Inputs; webhook_secret rendered as password). Update-only; create flow unchanged. Phase 2 (relay): - middleware/policy.go (new): per-request policy.DecisionFor(...) plus the *model.User pointer stashed on the gin context for downstream reads. Registered after TokenAuth() in router/relay-router.go. - relay/airbotix_policy.go (new): typed-struct mutations applied to *dto.GeneralOpenAIRequest BEFORE adaptor conversion — model whitelist reject, child-safe system-prompt prepend/replace, user + safety_identifier strip, Store=false for OpenAI-family channels. Wired into relay/compatible_handler.go::TextHelper. - service/airbotix_billing.go (new): async HMAC-signed billing webhook dispatched via gopool after SettleBilling. Uses c.Copy() for the goroutine. No-op when BillingWebhookURL or WebhookSecret is empty. Hygiene: - internal/billing/webhook.go: encoding/json → common.Marshal (CLAUDE.md Rule 1 compliance). - constant/context_key.go: ContextKeyPolicyDecision + ContextKeyAirbotixUser. - relay/airbotix_policy_test.go (new): 6 unit tests covering passthrough, kids_mode reject, mutate, replace existing system, non-OpenAI ZDR skip, kid-safe soft-prepend. - airbotix-internal.yml CI: path filter widened to the new files; vet scoped to ./internal/... ./middleware/... (upstream relay/channel/* has pre-existing 'unreachable code' warnings we don't gate on); build widened to model/constant/middleware/service/relay; new test added. Verification (golang:1.26.1-alpine in Docker): - go vet ./internal/... ./middleware/... clean - go build ./internal/... ./model/... ./constant/... ./middleware/... ./service/... ./relay/... clean - go test internal/... + TestUser_Airbotix* + TestApplyAirbotixPolicy* all green under -race - bun run typecheck clean - docker compose -f docker-compose.dev.yml up -d --build → boots, all 5 columns present, /v1/chat/completions hits middleware chain (401 on unknown token) Coverage gap (P0 follow-up, not in this commit): policy only runs on the OpenAI-compatible TextHelper path. /v1/messages, /v1/responses, /v1/images/*, /v1/audio/*, /v1/embeddings, /v1/rerank, /v1/realtime and the Gemini handler are still unguarded — kids_mode tenants can bypass via those endpoints. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CLAUDE.md: Rule 5 (protected new-api / QuantumNous identifiers) removed by repo owner so we can rebrand the fork for internal Airbotix + JR deployment. Upstream attribution link in footer.tsx:90 (the explicit github.com/QuantumNous/new-api anchor) is intentionally left intact — AGPL §13 obligations apply regardless of internal-vs-external use; the attribution costs nothing and reduces legal exposure on the fork. Source-level changes (visible in tab title, sidebar, footer fallbacks): - web/default/index.html — <title> + meta title/description - web/default/src/assets/logo.tsx — inline SVG icon: nested chevrons for "routing depth"; <title>DeepRouter</title> - web/default/src/lib/constants.ts — DEFAULT_SYSTEM_NAME - web/default/src/components/layout/components/system-brand.tsx — fallback when status.system_name is empty - web/default/src/components/layout/components/footer.tsx — same - web/default/src/features/system-settings/site/index.tsx — defaultSiteSettings.SystemName - common/constants.go — backend var SystemName Runtime overrides (no code change needed): admin Settings → SystemName + Logo URL still take precedence; this commit only changes the fallbacks served when system_name is unset. Still to do (binary assets I can't author): - web/default/public/logo.png — current bitmap is upstream NewAPI; replace with DeepRouter PNG (sq, ≥256px recommended) - web/default/public/favicon.ico — same Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…substring
The admin sidebar in DeepRouter has 20+ items across multiple groups
(Channels / Models / Pricing / Tokens / Users / System Settings / etc.),
plus several nested collapsibles. Finding a specific page by scanning
is slow.
This adds a search input in the SidebarHeader. Behaviour:
- Empty query: untouched, full nav tree renders as before
- Active query: case-insensitive substring match on item titles AND
sub-item titles; matched sub-items get promoted to flat NavLinks so
the user doesn't have to expand a collapsible to see them
- Group with no surviving items → hidden
- Zero matches across all groups → "No menu items match \"q\""
empty-state row
- ✕ button clears the query
i18n: 4 new translation keys (Search menu... / Search menu / Clear
search / No menu items match "{{q}}"). They fall back to the English
source string until added to locales/zh.json etc.
UI: uses the project's existing SidebarInput primitive (h-8, shadow-
none, matches base styling) plus lucide-react Search/X icons.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2's first cut only ran the kids_mode policy through TextHelper (chat/completions + completions). A kids_mode tenant could bypass every constraint by changing endpoint — sending /v1/messages, /v1/responses, /v1/images/*, /v1/audio/*, /v1/embeddings, /v1/rerank, /v1/realtime, or Gemini routes would hit upstream unguarded. This closes that gap. relay/airbotix_policy.go — new shared primitives: - policyDecisionFromContext: pulls the policy.Decision off the gin ctx (set by middleware/policy.go) - rejectAirbotixModel: builds the canonical 400 error returned by every handler on a whitelist miss - checkAirbotixModelWhitelist: minimal universal model gate; called by every handler that lacks a chat-shaped system/user/store to mutate - applyAirbotixPolicyToClaude: Anthropic /v1/messages — model gate + replace System (kids_mode hard) or fill-if-empty (kid-safe soft) + clear Metadata under StripIdentifying (the kids package operates on map[string]any, but Anthropic's metadata field is json.RawMessage; we drop the whole field rather than parse + filter + re-marshal — net same security posture, simpler code) - applyAirbotixPolicyToResponses: /v1/responses — same shape as the chat path; force store:false on OpenAI-family channels, strip user + safety_identifier, inject Instructions under kids_mode - applyAirbotixPolicyToGemini: model lives on RelayInfo (Gemini puts it in the URL path, not the body), so callers pass it explicitly; replaces SystemInstructions under kids_mode Handler hooks: - relay/claude_handler.go: applyAirbotixPolicyToClaude after ModelMappedHelper. ✱ - relay/responses_handler.go: applyAirbotixPolicyToResponses after ModelMappedHelper. - relay/image_handler.go: model whitelist + clear request.User (json.RawMessage). - relay/audio_handler.go: model whitelist only (no user/system fields; TTS prompt-content moderation is Phase 4). - relay/embedding_handler.go: model whitelist + clear request.User (string). - relay/rerank_handler.go: model whitelist only. - relay/websocket.go: model whitelist on info.UpstreamModelName before the realtime upgrade — realtime sessions have no discrete post-handshake mutation point in V0. - relay/gemini_handler.go: chat handler gets full applyToGemini; embedding handler gets the universal whitelist gate. ✱ Insertion is always AFTER helper.ModelMappedHelper so operator-side channel model_mapping (e.g. gpt-4 → gpt-4o-mini) gets a chance to remap the request onto a whitelisted model before we evaluate the gate. Otherwise the operator's mapping would be useless under kids_mode. Tests: relay/airbotix_policy_test.go — 15 new cases (existing 6 preserved unchanged). Covers Passthrough no-op, kids_mode reject, kids_mode mutate, kid-safe soft-fill, no-decision-pass-through, and non-OpenAI-channel ZDR skip — for each of Claude / Responses / Gemini. Plus 4 cases for the universal checkAirbotixModelWhitelist gate. Verification (golang:1.26.1-alpine in Docker): - go vet ./internal/... ./middleware/... clean - go build ./internal/... ./model/... ./constant/... ./middleware/... ./service/... ./relay/... clean - go test ./internal/... ./model -run TestUser_Airbotix ./relay -run "TestApplyAirbotixPolicy|TestCheckAirbotix" -race → 21 tests green (up from 12) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
OpenAI launched gpt-image-2 on 2026-04-21 with built-in reasoning
("thinking mode"), 4K resolution, batched coherent generation up to 8
images per prompt, and stronger text rendering for product photography.
On 2026-05-12 OpenAI retired DALL-E 2 and DALL-E 3 — gpt-image-2 is now
the default image model in the OpenAI API.
relay/channel/openai/constant.go:
Add gpt-image-2 + gpt-image-2-2026-04-21 snapshot to ModelList. Keep
the dall-e-2 / dall-e-3 entries with an inline deprecation comment so
back-compat channels can still be configured (upstream now returns 404
on those names, but having the entries here doesn't crash anything).
setting/ratio_setting/model_ratio.go: pricing from OpenAI docs —
text input $5 / 1M → defaultModelRatio = 2.5
image input $8 / 1M → defaultImageRatio = 1.6 ($8 / $5)
image output $30 / 1M → defaultCompletionRatio = 6 ($30 / $5)
Added both the bare and the dated snapshot to defaultModelRatio.
Did not touch the defaultModelPrice flat-rate dall-e-3 entry — it's
harmless and operators can remove via admin UI when convenient.
internal/kids/kids.go EligibleModels: gpt-image-2 is the *best* image
model for kids tenants because thinking-mode self-audits before output
— it's strictly safer than gpt-image-1. Added it. Kept gpt-image-1 as
a fallback for channels still configured against it; revisit in V0
launch retrospective. Removed dall-e-3 (the upstream is dead).
internal/kids/kids_test.go: 5 new cases on TestIsModelEligible covering
the base gpt-image-2 name, dated snapshot (HasPrefix path), gpt-image-1
fallback, and explicit reject for dall-e-2 / dall-e-3 (post-retirement
regression guard).
Verification (golang:1.26.1-alpine via Docker):
- go vet ./internal/... ./middleware/... clean
- go build of model/constant/middleware/service/relay clean
- go test ./internal/kids -race → 6 tests pass (was 6, but
TestIsModelEligible grew from 6 sub-cases to 11)
- TestUser_Airbotix*, TestApplyAirbotixPolicy*, TestCheckAirbotix*
unchanged green (regression check)
- docker compose dev-rebuild boots clean
Sources researched via web search (2026-05-13):
- platform.openai.com/docs/models/gpt-image-2
- community.openai.com gpt-image-2 announcement
- techcrunch.com 2026-04-21 ChatGPT Images 2.0 review
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Channels page used to require filling the channel form from scratch
every time. For a fresh install that means typing the same type
number, base URL, and default-models string 10× — friction. This adds
a "Quick Import" button next to "Create Channel" that pops a dialog
listing the common LLM providers; you tick the ones you want, hit
import, and N disabled channels appear in the list ready for you to
fill keys.
Created channels:
status = 2 (disabled — router won't try to dispatch keyless channel)
key = "REPLACE_WITH_YOUR_KEY" placeholder (backend
validateChannel rejects empty key)
group = "default"
type = provider's ChannelType (from constant/channel.go)
models = curated default list per provider
Presets included (lib/provider-presets.ts):
- OpenAI (gpt-4o, gpt-image-2, embeddings, whisper, tts)
- Anthropic Claude (Opus 4.7 / Sonnet 4.6 / Haiku 3.5)
- Google Gemini (2.5 Pro/Flash, 2.0, embeddings)
- DeepSeek (chat, reasoner)
- Qwen (DashScope)
- Moonshot (Kimi K2)
- Mistral
- OpenRouter (multi-provider aggregator)
- Doubao (火山方舟)
- SiliconFlow (国内多模型聚合)
UX details:
- Get key ↗ link per row → official console for that provider
- Select all / Clear toggles
- Per-import success/failure toast; failures listed inline so a partial
batch tells the operator exactly which to retry
- status=2 forces operator to consciously enable each one after filling
the real key (prevents routing to a keyless channel)
Files:
- channels-provider.tsx: +1 DialogType variant 'quick-import-providers'
- channels-primary-buttons.tsx: new Sparkles button next to Create
- channels-dialogs.tsx: mount the new dialog
- dialogs/quick-import-providers-dialog.tsx: the dialog component
- lib/provider-presets.ts: 10 provider preset data rows
Verification:
- bun run typecheck clean
- Rsbuild HMR picked up all 5 files without errors
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The upstream NewAPI theme uses a cold OKLCH neutral palette (background = pure white, foreground = pure black, gray scale = pure chromatic-zero) typical of developer tools. docs/DESIGN.md §2 calls for a warmer, more humanist feel borrowed from Lovable.com — cream parchment backgrounds, charcoal-not-pure-black text, opacity-derived gray scale for tonal coherence. web/default/src/styles/theme.css :root (light mode only — .dark stays unchanged for now): background oklch(1 0 0) → oklch(0.968 0.011 85) cream #f7f4ed foreground oklch(0.145 0 0) → oklch(0.205 0.003 60) charcoal #1c1c1c card / popover, secondary / accent, sidebar — all retuned to share the same warm hue (85 chroma center) so surfaces feel materially related instead of arbitrary OKLCH greys. border / input oklch(0.93 0 0) → oklch(0.928 0.013 85) warm #eceae4 muted-foreground retuned to warm grey ~#5f5f5d. Status colors (destructive/success/warning/info) and chart-* kept unchanged — those signal meaning, not brand. Also widened --font-sans fallback chain to include humanist system fonts (ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont) ahead of generic sans-serif so the page feels warm even before Public Sans web-font finishes loading. Public Sans (already bundled via @fontsource-variable) stays primary — no proprietary woff2 added. Doc moves: docs/DESIGN.md, docs/PRD.md were copied in by a previous commit (b8d59b9). This commit updates AIRBOTIX.md / PLAN.md / DEV.md to point at the in-repo paths instead of the external ~/Documents/sites/jr-academy-ai/deeprouter-brand/ references that only worked on Lightman's machine. Verification: - bun run typecheck clean - Rsbuild HMR picked up theme.css cleanly (2 builds, no errors) - localhost:3001 still HTTP 200; visual diff = warm cream surface everywhere shadcn theme variables are wired through Scope intentionally tight ("稍微改") — theme token swap only. No component-level changes (Button/Card/Input remain as-is). Next pass can apply inset shadows on primary button + pill radius for action buttons per DESIGN.md §4, once we see how the cream tokens land in real screens. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The primary "Dark" button on a cream surface is the most recognisable
Lovable.com pattern — a charcoal pill with a faint top-edge highlight,
a 1px inset border slightly darker than the surface, and a 2px soft
drop shadow. The button looks pressed-in rather than floating above
the page. Per docs/DESIGN.md §4 Primary Dark spec.
Tailwind v4 arbitrary three-layer shadow:
inset 0 1px 0 rgb(255 255 255 / .18) ← top highlight (light bevel)
inset 0 0 0 1px rgb(0 0 0 / .20) ← inner border (depth)
0 1px 2px rgb(0 0 0 / .06) ← soft drop shadow
Also added active:opacity-90 so the press state feels tactile (matches
spec's "active opacity 0.8" intent without going as faint).
Scope deliberately limited to the `default` variant; outline / ghost /
secondary / destructive / link are untouched so semantic meaning of
those variants doesn't drift. Pill / icon-only buttons can pick up the
same shadow pattern in a follow-up if we decide we want it.
Verification:
- bun run typecheck clean
- Rsbuild HMR rebuilt button.tsx without error (arbitrary shadow
syntax accepted by Tailwind v4)
- localhost:3001 still HTTP 200; primary buttons now show the
signature inset depth on the cream background
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…kernel Clarifies that the Team B consumer is the product repo kidsinai/kids-opencode, which depends on opencode via npm SDK + plugin. The kernel mirror lives separately at kidsinai/opencode-kernel.
Out-of-the-box new-api ships with GroupRatio = {default:1, vip:1, svip:1}
— a pass-through default that's neutral for hobbyist self-deploy but
useless for the DeepRouter business model where the gateway is the
revenue-bearing layer. This commit reshapes the default ladder so a
fresh install lands at ~70% gross margin on external traffic, with
explicit pre-defined groups for the two named V0 tenants from
AIRBOTIX.md / PLAN.md ("Tenants" table).
setting/ratio_setting/group_ratio.go defaultGroupRatio:
default 3.333 external SaaS baseline 70% margin
vip 2.5 VIP (volume buyer) 60% margin
svip 2.0 Super VIP (anchor partner) 50% margin
enterprise 4.0 Enterprise w/ SLA 75% margin
airbotix-kids 1.0 own product — pass-through 0% margin
jr-academy 1.5 own product — internal fee 33% margin
Formula reminder in inline comment so future readers understand the
relationship: margin = (ratio - 1) / ratio.
defaultGroupGroupRatio cleaned up — the upstream placeholder
{"vip": {"edit_this": 0.9}} was just an example value, not a real
configuration. Empty out-of-the-box; operators fill in via admin UI
when they want per (user_group × channel_group) cross-table pricing.
Note: this only affects fresh installs. Existing deployments already
have GroupRatio in the options table from first boot; they need an
explicit PUT /api/option {key:"GroupRatio",...} to migrate. For our
running dev instance this was done via the API in the same session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the "credit running low → auto-charge saved card" UX that OpenAI's
API has built-in. The gateway operator turns this on per tenant; once
the tenant has completed one regular Stripe checkout (so we have their
customer ID + payment method on file), subsequent low-balance moments
trigger an off-session charge automatically.
Trigger condition: after every PostTextConsumeQuota settles, a
fire-and-forget gopool.Go(MaybeAutoTopup) checks the user. The check
runs every request but only charges when ALL of the following hold:
- user.AutoTopupEnabled
- user.AutoTopupAmount > 0
- user.Quota < user.AutoTopupThreshold
- user.StripeCustomer != ""
- setting.StripeApiSecret looks like sk_/rk_
- Redis enabled (we need SETNX lock to dedupe concurrent triggers)
- converted Stripe amount >= 50 cents (Stripe USD minimum)
Schema (model/user.go):
AutoTopupEnabled bool json:"auto_topup_enabled"
AutoTopupThreshold int json:"auto_topup_threshold" quota units
AutoTopupAmount int json:"auto_topup_amount" quota units
Both Threshold and Amount are in internal quota units (1 USD =
common.QuotaPerUnit, default 500000). Stripe charge is converted at
charge time. Extended User.Edit() updates map so admin UI saves them.
Service (service/auto_topup.go):
decideAutoTopup(autoTopupPreconditions) (shouldCharge, cents, reason)
— pure decision branch, no IO; exhaustively unit-testable
MaybeAutoTopup(ctx, userId) AutoTopupResult
— IO wrapper: DB read → decideAutoTopup → Redis SETNX lock (60s
TTL, NOT released on success so re-fires before quota credit
replicates don't double-charge) → Stripe PaymentIntent
off_session=true via stripeChargeFn (overridable for tests) →
model.IncreaseUserQuota → model.RecordLog LogTypeTopup
Idempotency key on Stripe: "auto-topup:{userId}:{minute}" — protects
against double-charge across pod restarts within a 60s window.
Frontend (web/default/src/features/users/):
- types.ts: 3 new fields on userSchema + UserFormData
- lib/user-form.ts: Zod schema, defaults, transforms (both
transformFormDataToPayload and
transformUserToFormDefaults)
- components/users-mutate-drawer.tsx: new "Auto Top-up" section
(Switch + 2 number inputs with help text explaining "500000 = $1"
so operators don't have to math the quota units in their head)
Tests:
service/auto_topup_test.go — 16 cases:
- 10 × TestDecideAutoTopup_* covering every skip reason +
happy path + rk_ key acceptance
- 2 × TestQuotaUnitsToStripeCents covering canonical conversion +
pathological QuotaPerUnit=0 guard
- 3 × TestStripeChargeFn_* (params capture, error propagation,
call-count atomicity for swap safety)
- 1 × TestLooksLikeStripeKey distinguishing sk_/rk_ from pk_
model/user_airbotix_test.go — extended:
- TestUser_AirbotixFieldsPresent now guards all 8 Airbotix
fields (was 4): adds WebhookSecret + 3 AutoTopup* fields
- TestUser_AirbotixFieldsRoundTrip exercises the new fields
Verification (golang:1.26.1-alpine via Docker):
- go vet ./internal/... ./middleware/... ./service/ clean
- go build of internal/model/constant/middleware/service/relay clean
- go test ./internal/... ./model -run TestUser_Airbotix
./service -run "TestDecideAutoTopup|TestQuotaUnitsToStripeCents|
TestStripeChargeFn|TestLooksLikeStripeKey"
-race → all 22 new + 3 model + 13 internal tests pass
- bun run typecheck clean; Rsbuild HMR picks up all 3 frontend files
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…auto-topup)
The previous commit added MaybeAutoTopup which charges customers
off-session via Stripe PaymentIntent. But that requires Stripe to
already have a saved PaymentMethod attached to the Customer record —
otherwise off-session charges fail with payment_method_not_found.
Stripe Checkout doesn't save the PaymentMethod by default for one-time
payments (mode=payment). To opt in, you set
PaymentIntentData.SetupFutureUsage="off_session" on the Checkout
Session params; Stripe then:
1. Surfaces the appropriate SCA / regional mandate text to the
cardholder during checkout (no extra UI work for us)
2. Saves the PaymentMethod to the Customer on successful payment
3. Marks it as eligible for off-session reuse (the "ich war hier"
flag that Stripe checks before auto-charging)
This commit adds that one param on genStripeLink so the standard
"top up via Stripe" flow that customers already use now produces
auto-topup-capable Customer records. No behaviour change for users
who never enable AutoTopupEnabled — the saved method just sits there
unused.
The Customer ID itself is already persisted to user.StripeCustomer
via the existing model.Recharge transaction (model/topup.go:142),
so the loop is closed end-to-end:
1. User first topup → Stripe Checkout w/ SetupFutureUsage=off
→ on success: save PM, return cus_xxx
→ fulfillOrder webhook updates
user.StripeCustomer = cus_xxx
user.Quota += amount
2. User enables AutoTopup → admin UI toggle (already shipped last commit)
3. Quota drops < threshold → service.MaybeAutoTopup fires
→ PaymentIntent off_session=true uses
the saved PM via Customer ID
→ quota credited, log written
Stripe SDK: stripe.PaymentIntentSetupFutureUsageOffSession is the
canonical typed constant in stripe-go/v81; no string magic.
Verification:
- go build ./controller/... clean (SDK constant resolves)
- full build + service tests still green
- no test change needed for this commit — the Stripe Checkout path
only exercises with a real Stripe key, which is an integration
concern out of unit scope
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Operator wanted a visual at-a-glance signal of which privilege level
they're signed in as. Adds a small chip next to the SystemBrand logo
showing "Super Admin" / "Admin" — invisible for regular users so the
UI stays clean for the common case.
new web/default/src/components/role-badge.tsx:
- Reads role from useAuthStore (the same store app-sidebar already
consults for admin nav-group filtering, so no new state plumbing)
- Returns null for ROLE.USER / ROLE.GUEST — no chip clutter for the
common case
- Super Admin: filled charcoal pill with white text + the same
Lovable inset shadow that primary buttons got two commits ago, so
the badge feels material-consistent with action buttons. ShieldCheck
icon (filled shield = "verified privilege").
- Admin: outline chip with warm cream border. Shield icon (unfilled
= "elevated but not root").
- Hidden on screens <sm so the mobile header stays compact.
- Uses getRoleLabel() from lib/roles.ts so the chip text translates
with the active i18n locale (English "Super Admin", 中文 "超级管理员",
etc.).
web/default/src/components/layout/components/app-header.tsx:
- <RoleBadge /> placed immediately after <SystemBrand variant='inline'/>
so it sits to the right of the logo+name pair.
Verification:
- bun run typecheck clean
- Rsbuild HMR rebuilt app-header.tsx + role-badge.tsx without errors
- localhost:3001 still HTTP 200; logged in as the admin/admin user
(role=100), the top bar now shows the DeepRouter logo + name +
"Super Admin" filled chip with shield icon
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Upstream NewAPI surfaced a "Docs" item in the top nav that, when
HeaderNavModules.docs wasn't configured, defaulted to either the local
/docs route (which DeepRouter doesn't have) or the status.docs_link
URL (which still pointed at upstream's docs.newapi.pro). Either way
it gave operators a broken-looking link in a prominent nav slot.
Removed the entire Docs block from use-top-nav-links.ts. When
DeepRouter has its own docs site, the operator can either:
- set HeaderNavModules.docs in admin System Settings → Site and
restore the block here, or
- just point status.docs_link at the new docs and re-add 5 lines
of code
Verification:
- bun run typecheck clean (had to remove the now-unused docsLink
const to satisfy TS6133)
- Rsbuild HMR rebuild clean
- Top nav now shows Home / Console / Model Square / Rankings /
About — no broken Docs link
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The upstream NewAPI homepage reads as a self-hosted developer tool —
"Unified API Gateway for All Your AI Models" + "open-source AI API
gateway for self-hosted deployments. Connect upstream services, manage
keys, quotas, logs..." That framing buries DeepRouter's actual market
positioning, which is the consumer / B2B story we tell investors:
"sign up once, pay in your currency, every AI model from one place".
Pulls three top-level headings + supporting copy verbatim from the
DeepRouter pitch deck (docs/brand external pitch-deck.html — Solution,
Product, and CTA equivalents) so the public-facing site matches the
fundraising deck word-for-word. Feature card bodies + stats numbers
left untouched — those are factual product claims that don't change
with positioning.
hero.tsx
Headline: "Unified API Gateway for All Your AI Models"
→ "One account. Every AI model."
Subtitle: "DeepRouter is an open-source AI API gateway for self-
hosted deployments..."
→ "Sign up once, top up in your currency, and you're
chatting with GPT-5, Claude, Gemini, and 20+ other
models — all from one place. No API keys, no foreign
cards, no engineering."
features.tsx
Section: "Built for developers, designed for scale"
→ "One interface, redundant upstreams."
cta.tsx
Headline: "Ready to simplify your AI integration?"
→ "Start using every model. With one account."
Body: Replaced the "deploy your own gateway" line with the
"no foreign cards, no engineering" promise from the
pitch deck Solution slide.
Translation keys: all new strings flow through t() so locales/zh.json
etc. can add Chinese versions when ready (no Chinese fallback yet —
the source English shows through for now).
Verification:
- bun run typecheck clean
- Rsbuild HMR rebuilt all 3 home sections without errors
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two additive homepage animations layered on top of the existing
fade-up / scale-in entrance set. Both are CSS-driven and respect
prefers-reduced-motion.
styles/index.css — two new keyframe sets:
landing-hero-bloom-a/b slow 18-22s drift on two stacked radial
gradients (warm cream + faint accent blue).
Gives the Hero motion without a heavy
gradient sheen.
landing-stat-settle 380ms accent-blue tint flash + soft glow.
Hooks into the counter via JS class toggle
when the count-up animation completes.
hero.tsx — replaces the previous single static radial gradient with
a <div class='landing-hero-bloom h-80 top-0' />. The CSS class supplies
left/right + animations; Tailwind classes (h-80, top-0) decide the
slim header band positioning, so the bloom doesn't wash the entire
Hero section.
stats.tsx — two changes inside the existing Counter component:
1. Default duration 1600ms → 2400ms. The small-end values (10, 50)
finished too fast to register as animation; 2.4s is comfortably
visible without being slow.
2. On count completion, briefly add .landing-stat-settle to the
<span> and remove on animationend so subsequent IntersectionObserver
re-triggers replay cleanly. Single addEventListener + removal in
the same tick — no leak.
Verification:
- bun run typecheck clean
- Rsbuild HMR rebuilt 3 files clean, no errors
- localhost:3001 HTTP 200; Hero header drifts on a slow cycle, Stats
numbers count up over ~2.4s and flash accent-blue once on settle
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… drawer
The DeepRouter brand palette landed in theme.css :root (warm cream +
charcoal + accent blue) but the .dark block is still upstream's neutral
OKLCH set — so toggling to dark via the existing switcher dropped users
into a visibly half-finished palette that doesn't match the brand. Until
the dark side gets retuned, the switcher creates more confusion than it
solves.
config-drawer.tsx:
- Removed <ThemeConfig /> from the drawer body (Settings → Theme
Settings sheet). Preset / Radius / Scale / Sidebar / Layout /
Content / Direction sections all stay — those still work as
advertised.
- Dropped the function definition + the IconThemeDark/Light/System
asset imports so TypeScript stays clean (unused-decl errors).
- Left useTheme import alone — the drawer footer's Reset button
still calls resetTheme() to roll all customizations back.
To restore: pull `ThemeConfig` from this commit's parent (or any
commit before this), re-import the 3 IconTheme* assets, and re-add
<ThemeConfig /> as the first section inside the SheetContent.
Verification:
- bun run typecheck clean (TS6133 errors resolved)
- Drawer still opens via the Palette icon in the top app bar and
shows the remaining 7 customization sections
…ry points)
Yesterday I removed the light/dark/system radio out of ConfigDrawer.
The same control had two more entry points in the nav as a dropdown
with a Sun icon (lucide-react Sun/Moon swap), which still shipped
light/dark/system items. Removed both so the toggle is gone from
every visible surface:
public-header.tsx
Flipped the default prop showThemeSwitch from true → false.
Both desktop and mobile branches gate <ThemeSwitch /> on this
prop, so a single default flip hides it everywhere. Any future
route that needs the toggle back can pass
showThemeSwitch={true} explicitly.
_authenticated/errors/$error.tsx
Dropped the <ThemeSwitch /> in the error page header. Error pages
inherit the rest of the app theme; no need for a dedicated
switcher there.
theme-switch.tsx component itself is left in place — re-mountable
when the .dark palette gets retuned to match the warm cream brand.
Verification:
- bun run typecheck clean
- Top-bar Sun icon no longer appears on landing / pricing / dashboard
or error pages
…sidebar
Upstream NewAPI ships a "Chat" collapsible in the sidebar that expands
to 8 third-party desktop chat clients (Cherry Studio, AionUI, CC Switch,
DeepChat, Lobe Chat 官方, AI as Workspace, AMA 问天, OpenCat) as
deep-link launchers. Two issues:
1. None of these match DeepRouter's positioning. They're general-
purpose desktop AI clients; we're a multi-tenant gateway. Users
who land on the dashboard don't expect to pick a chat client off
the sidebar.
2. The list is long and pushes more important menu items (Channels /
Users / Models / System Settings) down the viewport.
Removed the chat-presets sub-item from the Chat group. The Playground
entry stays — that's still useful for in-app testing. The chat-presets
plumbing (chat-presets-item.tsx, useChatPresets hook, chat2link route)
is left in the codebase so re-mounting is a one-line comment-uncomment
when we want to curate our own client list.
use-sidebar-data.ts:
- Commented out the chat-presets item entry
- Commented out the now-unused MessageSquare lucide import so TS6133
doesn't fail (one line; restore alongside the entry)
Verification:
- bun run typecheck clean
- After HMR, sidebar Chat group shows only Playground; the 8-deep-link
submenu is gone
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Upstream NewAPI uses "Model Square" as the public pricing page title.
Operator feedback: that label doesn't describe what the page is. The
URL is already /pricing, the page already shows per-model prices in
the user's currency, and t('Pricing') is already used as the section
title on the dashboard overview + model details panel. Renaming the
top nav + page heading to match.
hooks/use-top-nav-links.ts top nav link label
features/pricing/index.tsx page hero h1
features/system-settings/maintenance/
header-navigation-section.tsx admin "show/hide this nav item"
form row label
i18n/static-keys.ts 'Model Square' → 'Pricing' in
the static keys list (i18next
dedupes the new key against the
two existing t('Pricing') call
sites on dashboard + model
details, so locales/zh.json etc.
can have one entry instead of two)
… pack
Mirrors the Channels Quick Import button I shipped earlier, but for the
model metadata catalog (/models/metadata). One click seeds 25-ish
popular models grouped into Chat / Reasoning / Image / Video / Audio /
Embedding. Saves the operator from typing model_name + tags + endpoints
+ description through the Add Model drawer for each one.
Behaviour:
- Imported models are created with status=0 (disabled). They don't
appear in /v1/models until the operator reviews + enables.
- Duplicates (same model_name already in DB) are skipped rather than
failing the whole batch — backend's "already exists" response is
detected and counted toward the skip total.
- Per-group toggle + select-all/clear at the top.
Files:
lib/model-presets.ts (new) 27 curated model entries
grouped by category, each with
description + tags + endpoints
components/dialogs/quick-import-
models-dialog.tsx (new) the dialog component;
groups by category, individual
checkboxes, per-group toggle
components/models-provider.tsx +1 DialogType variant
components/models-primary-buttons ✨ Sparkles button next to
Add Model
components/models-dialogs.tsx mount the new dialog
Verification:
- bun run typecheck clean
- Rsbuild HMR rebuild clean
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reorients the Create API Key flow around what non-technical users actually
think — "I want to use Claude / OpenAI for coding" — instead of upstream
new-api's reseller-tier semantics (Group + 5x Ratio + Quota + IP whitelist).
Phase 0 — immediate fix:
- Remove the "5x Ratio" badge from the group combobox so end users no
longer see the raw markup multiplier (api-key-group-combobox.tsx).
Phase 1 — Simple Mode MVP:
Backend (Go):
- Token gets simple_purpose / simple_brand / simple_price_tier columns
(GORM AutoMigrate handles SQLite/MySQL/PostgreSQL).
- New setting/alias_setting package ships an embedded YAML seed (via
go:embed seed/aliases.yaml) mapping (purpose, brand) → real model name,
plus 4 price tiers (Economy/Standard/Premium/Ultra) with model
whitelists. Loaded once at boot from main.go InitResources().
- AddToken/UpdateToken auto-derive Token.ModelLimits from the purpose
whitelist so the existing distribution middleware enforces it
naturally — no new enforcement path.
- middleware/distributor.go resolves virtual model names ("deeprouter",
"deeprouter-coding", …) to the concrete upstream model based on the
token's bound purpose+brand, BEFORE the model_limit check.
- New GET /api/user/self/api-key-purposes endpoint surfaces the picker
metadata (localized) to the frontend.
- Unit tests cover alias resolution, brand fallback, tier whitelists,
and zh/en localization.
Frontend (React + Rsbuild):
- Create API Key drawer gets a Simple ⇄ Advanced Tabs toggle. Default
Simple, persisted in localStorage via useApiKeyFormMode hook.
- Simple mode: optional Name + 6-card purpose picker
(Chat / Coding / Image / Video / Voice / Auto) + optional brand
chip-row (Claude / OpenAI / Gemini / DeepSeek). Auto purpose reveals
a 4-tier price cap (Standard is default; Ultra requires confirm
dialog).
- After successful Simple-mode create, a one-shot AlertDialog reveals
the key + base URL + recommended model name with copy buttons and
six client-tutorial chips (placeholder /onboarding/* routes for now).
- Empty /keys table shows a CTA empty state ("Create your first API
key — 30 seconds to get a key").
- Existing Advanced mode unchanged for current users; existing keys
retain their behavior since SimplePurpose defaults to empty.
- i18n: 23 new keys with en + zh translations; sync propagated stubs
to fr/ja/ru/vi.
PRD: docs/tasks/api-key-simple-advanced-prd.md
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (116)
WalkthroughAdds DeepRouter branding and “Simple-mode” aliases, Airbotix policy middleware across relay handlers, billing webhook and auto-topup services, token/user schema extensions, a new purposes endpoint, CI workflow, and substantial UI/docs to support API key simple mode and admin tenant settings. ChangesAirbotix Policy, Billing, and UI Integration
Sequence Diagram(s)sequenceDiagram
autonumber
participant Admin
participant API as DeepRouter API
participant MW as AirbotixPolicy MW
participant Alias as AliasResolver
participant Up as Upstream Provider
participant Bill as Billing Webhook
participant Pay as Stripe (AutoTopup)
Admin->>API: Create API Key (simple: purpose/brand/tier)
API->>API: Derive model_limits from alias_setting
Admin-->>API: 200 Created
Admin->>API: /v1/chat with virtual model
API->>MW: Load user → Decision(kids_mode/profile)
API->>Alias: Resolve virtual→concrete (purpose/brand)
MW-->>API: Whitelist+mutate (ZDR, strip IDs, prompt)
API->>Up: Forward request
Up-->>API: Usage + response
API->>Bill: POST signed Event (async)
API->>Pay: MaybeAutoTopup (off_session)
API-->>Admin: Response
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
|
Important
📝 变更描述 / Description
(简述:做了什么?为什么这样改能生效?请基于你对代码逻辑的理解来写,避免粘贴未经整理的内容)
🚀 变更类型 / Type of change
🔗 关联任务 / Related Issue
✅ 提交前检查项 / Checklist
Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。📸 运行证明 / Proof of Work
(请在此粘贴截图、关键日志或测试报告,以证明变更生效)
Summary by CodeRabbit
Release Notes
New Features
Documentation
Chores