chore(default): drop fr/ja/ru/vi locales — keep en + zh only#4902
Closed
hades217 wants to merge 33 commits into
Closed
chore(default): drop fr/ja/ru/vi locales — keep en + zh only#4902hades217 wants to merge 33 commits into
hades217 wants to merge 33 commits into
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>
Two bundled changes, both about the operator never accidentally losing
money or getting confused about how pricing works.
1) defaultGroupRatio dropped from the aggressive 70%-margin ladder to a
flat conservative 1.2× across all groups.
Why the change:
- v0.1 default was default=3.333, vip=2.5, svip=2.0, enterprise=4.0,
airbotix-kids=1.0, jr-academy=1.5 targeting 50-75% gross margin.
- Risk: if any other ratio (ModelRatio, CompletionRatio) is
mis-configured, the gateway silently over-charges customers
before anyone notices. Losing a customer costs more than missing
a percent of margin.
- Operator (Lightman, 2026-05-16): "默认先都加价20%,要不然出错越做越亏"
— preferring safer launch defaults that operators dial up
deliberately per business decision, not accept by accident.
New default: every group = 1.2 (+20% markup, ~17% margin). Operators
raise specific groups to 2.0-3.5× later, with intent.
Existing deployments still need an explicit PUT /api/option
{key:"GroupRatio",...} to migrate — the AddAll() in init() only
fills missing keys. For our running dev instance this was done
live via API in the same session.
File: setting/ratio_setting/group_ratio.go — rewrote the comment
block to record the decision + history so future readers know why
the ladder is flat and when to dial up.
2) Pricing Cheatsheet page + sidebar entry — "Admin → 📘 Pricing
Cheatsheet" → /help/pricing.
Why:
- Operator gets confused about Channel vs Model vs Group every
time they come back to the dashboard.
- The quota formula
(prompt × ModelRatio + completion × ModelRatio × CompletionRatio)
× GroupRatio
is not derivable from looking at the UI alone.
- Better to put the cheatsheet one click away than to keep
re-deriving it in Slack.
New files:
web/default/src/features/help/pricing-cheatsheet.tsx
Single page: 3-card layer diagram (Model / Channel / Group) +
quota formula in monospace + a worked example with real numbers
(gpt-4o, 1000+500 tokens, default group → $0.025 charged,
$0.0075 cost, $0.0175 margin shown explicitly) + lever table
linking to the right admin page + 5 common-confusion Q&A.
web/default/src/routes/_authenticated/help/pricing.tsx
TanStack file-based route under the authenticated layout. Pure
wrapper that mounts <PricingCheatsheet />.
Sidebar:
web/default/src/hooks/use-sidebar-data.ts — added
"Pricing Cheatsheet" item with HelpCircle icon to the bottom of
the Admin group so it's beside System Settings.
Verification:
- bun run typecheck blocked locally by an unrelated macOS TCC
permission issue on this repo (Rsbuild dev server also can't read
tsconfig.json currently — see commit-message log on next push for
follow-up). Code itself reviewed for syntax; the new route file
follows the same shape as existing /_authenticated/* routes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…calhost Symptoms on Lightman's machine: /sign-in returned "Request failed with status code 404" when posting to /api/user/login through the Rsbuild dev proxy. Root cause: an unrelated Airbotix dev server was listening on IPv6 only — `[::1]:3000`. The DeepRouter backend (Docker container) was listening on the IPv4+IPv6 wildcard `*:3000`. macOS's resolver returns IPv6 first for `localhost`, so the Rsbuild proxy's http.request(host: 'localhost', port: 3000) landed on the Airbotix process and got a 404 for our /api/* routes. Pinning the proxy default to 'http://127.0.0.1:3000' forces IPv4 and bypasses any unrelated IPv6 dev servers that happen to be running on port 3000 at the same time. Docker's port publish to 3000 listens on both families (0.0.0.0 + [::]), so this change does not break operators who don't have this specific IPv6 collision. The VITE_REACT_APP_SERVER_URL env var still takes precedence for anyone who needs a custom target. 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>
The repo is moving to en + zh only. Removes:
- locales/{fr,ja,ru,vi}.json
- locales/_reports/{fr,ja,ru,vi,zh}.untranslated.json + _sync-report.json
- fr/ja/ru/vi entries from i18n/config.ts, language-switcher.tsx, and
profile language-preferences-card.tsx
Side fix: regenerated routeTree.gen.ts to include the help/pricing
route added in 764f4dd (gen file was stale).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Author
|
Wrong target — re-opening on deeprouter-ai/deeprouter fork. Sorry for the noise. |
Contributor
|
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 (123)
WalkthroughAdds Airbotix policy middleware and kids_mode enforcement, simple-mode aliasing, billing webhooks, auto top-up, frontend keys/models quick-import and simple-mode UI, CI workflow, documentation, and rebrands to DeepRouter. ChangesDeepRouter Airbotix integration
Sequence Diagram(s)sequenceDiagram
participant Admin as Admin UI
participant API as API Router
participant Alias as alias_setting
participant Relay as Relay Router
participant Policy as AirbotixPolicy
participant Handler as Relay Handler
participant Upstream as Provider
participant Bill as Billing Dispatcher
participant Pay as Stripe
participant Cache as Redis
Admin->>API: Create API Key (simple_purpose/brand/tier)
API->>Alias: ModelWhitelistForToken / PurposeSummary
API-->>Admin: Created key + purpose metadata
Client->>Relay: /v1/* request (token, model)
Relay->>Policy: Load user → DecisionFor(...)
Policy-->>Relay: Context(policy,user)
Relay->>Handler: Apply whitelist, ZDR, strip IDs, prompt
Handler->>Upstream: Adapted request
Upstream-->>Handler: Response + usage
Handler->>Bill: Async webhook(Event,HMAC)
Handler->>Cache: MaybeAutoTopup lock
Handler->>Pay: Off-session PaymentIntent (if eligible)
Handler-->>Client: Response
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
|
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
web/default/src/i18n/locales/{fr,ja,ru,vi}.json及对应_reports/*.untranslated.json+_sync-report.jsoni18n/config.ts、language-switcher.tsx、language-preferences-card.tsx里的 fr/ja/ru/vi 引用routeTree.gen.ts(upstream commit 764f4dd 加help/pricing路由时漏更新 gen file,typecheck 当前会报 missing route key)背景
仓库收敛到 en + zh 双语支持。上一个 PR 已合并的 Phase 1 commit (87fd7d0) 因为我在 worktree 没继承到你本地 working tree 里的删除意图,跑
bun run i18n:sync又把 fr/ja/ru/vi 全部 propagate 回来了。这个 PR 把它们清掉。Test plan
bun run typecheck通过bunx eslint0 error / 0 warningbun run dev,确认语言切换器只显示 English / 中文🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Documentation
UI/UX Improvements
Chores