Skip to content

chore(default): drop fr/ja/ru/vi locales — keep en + zh only#4902

Closed
hades217 wants to merge 33 commits into
QuantumNous:mainfrom
deeprouter-ai:chore/drop-fr-ja-ru-vi-locales
Closed

chore(default): drop fr/ja/ru/vi locales — keep en + zh only#4902
hades217 wants to merge 33 commits into
QuantumNous:mainfrom
deeprouter-ai:chore/drop-fr-ja-ru-vi-locales

Conversation

@hades217
Copy link
Copy Markdown

@hades217 hades217 commented May 16, 2026

Summary

  • 删除 web/default/src/i18n/locales/{fr,ja,ru,vi}.json 及对应 _reports/*.untranslated.json + _sync-report.json
  • 移除 i18n/config.tslanguage-switcher.tsxlanguage-preferences-card.tsx 里的 fr/ja/ru/vi 引用
  • 顺手 regen routeTree.gen.ts(upstream commit 764f4ddhelp/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 eslint 0 error / 0 warning
  • 本地 bun run dev,确认语言切换器只显示 English / 中文
  • Profile 页语言下拉只有 简体中文 / English
  • 切到 zh 时所有页面正常显示中文

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Simple Mode API key creation with purpose/brand/price tier selection
    • Kids mode enforcement with model whitelisting and data retention controls
    • Automatic user quota top-ups via Stripe
    • Billing webhook dispatch for tenant integrations
    • Quick import dialogs for providers and models
    • Pricing reference guide for administrators
    • Sidebar search and navigation filtering
    • Role badge display in application header
  • Documentation

    • Development guide, product requirements, and design system documentation
    • Tenant onboarding workflow guide
    • Development plan with milestones and acceptance criteria
  • UI/UX Improvements

    • Rebranded as "DeepRouter" with updated logo and warm color theme
    • Streamlined language support (English/Chinese only)
    • Simplified configuration options
    • Enhanced header navigation
  • Chores

    • License and attribution file updates
    • CI/CD workflow setup
    • Added default pricing ratios and model configurations

Review Change Stack

Lightman and others added 30 commits May 12, 2026 15:32
…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>
hades217 and others added 3 commits May 16, 2026 15:21
…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>
@hades217
Copy link
Copy Markdown
Author

Wrong target — re-opening on deeprouter-ai/deeprouter fork. Sorry for the noise.

@hades217 hades217 closed this May 16, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 16, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a38ffd19-c421-4346-a33f-95e0a24d0d23

📥 Commits

Reviewing files that changed from the base of the PR and between 18282e6 and 9bb62a2.

⛔ Files ignored due to path filters (1)
  • web/default/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (123)
  • .github/workflows/airbotix-internal.yml
  • AIRBOTIX.md
  • CLAUDE.md
  • DEV.md
  • LICENSE
  • NOTICE
  • PLAN.md
  • THIRD-PARTY-LICENSES.md
  • bin/airbotix-set-tenant-fields.sh
  • common/constants.go
  • constant/context_key.go
  • controller/purpose.go
  • controller/token.go
  • controller/topup_stripe.go
  • docs/DESIGN.md
  • docs/PRD.md
  • docs/tasks/api-key-simple-advanced-prd.md
  • docs/tasks/phase-1-admin-ui.md
  • docs/tenant-onboarding.md
  • internal/billing/webhook.go
  • internal/billing/webhook_test.go
  • internal/kids/kids.go
  • internal/kids/kids_test.go
  • internal/policy/profile.go
  • internal/policy/profile_test.go
  • main.go
  • middleware/auth.go
  • middleware/distributor.go
  • middleware/policy.go
  • model/token.go
  • model/user.go
  • model/user_airbotix_test.go
  • relay/airbotix_policy.go
  • relay/airbotix_policy_test.go
  • relay/audio_handler.go
  • relay/channel/openai/constant.go
  • relay/claude_handler.go
  • relay/compatible_handler.go
  • relay/embedding_handler.go
  • relay/gemini_handler.go
  • relay/image_handler.go
  • relay/rerank_handler.go
  • relay/responses_handler.go
  • relay/websocket.go
  • router/api-router.go
  • router/relay-router.go
  • service/airbotix_billing.go
  • service/auto_topup.go
  • service/auto_topup_test.go
  • service/text_quota.go
  • setting/alias_setting/alias_setting.go
  • setting/alias_setting/alias_setting_test.go
  • setting/alias_setting/seed/aliases.yaml
  • setting/ratio_setting/group_ratio.go
  • setting/ratio_setting/model_ratio.go
  • web/default/index.html
  • web/default/rsbuild.config.ts
  • web/default/src/assets/logo.tsx
  • web/default/src/components/config-drawer.tsx
  • web/default/src/components/language-switcher.tsx
  • web/default/src/components/layout/components/app-header.tsx
  • web/default/src/components/layout/components/app-sidebar.tsx
  • web/default/src/components/layout/components/footer.tsx
  • web/default/src/components/layout/components/public-header.tsx
  • web/default/src/components/layout/components/system-brand.tsx
  • web/default/src/components/role-badge.tsx
  • web/default/src/components/ui/button.tsx
  • web/default/src/features/channels/components/channels-dialogs.tsx
  • web/default/src/features/channels/components/channels-primary-buttons.tsx
  • web/default/src/features/channels/components/channels-provider.tsx
  • web/default/src/features/channels/components/dialogs/quick-import-providers-dialog.tsx
  • web/default/src/features/channels/lib/provider-presets.ts
  • web/default/src/features/help/pricing-cheatsheet.tsx
  • web/default/src/features/home/components/sections/cta.tsx
  • web/default/src/features/home/components/sections/features.tsx
  • web/default/src/features/home/components/sections/hero.tsx
  • web/default/src/features/home/components/sections/stats.tsx
  • web/default/src/features/keys/api.ts
  • web/default/src/features/keys/components/api-key-brand-filter.tsx
  • web/default/src/features/keys/components/api-key-group-combobox.tsx
  • web/default/src/features/keys/components/api-key-price-tier.tsx
  • web/default/src/features/keys/components/api-key-purpose-picker.tsx
  • web/default/src/features/keys/components/api-key-success-dialog.tsx
  • web/default/src/features/keys/components/api-keys-empty-state.tsx
  • web/default/src/features/keys/components/api-keys-mutate-drawer.tsx
  • web/default/src/features/keys/components/api-keys-table.tsx
  • web/default/src/features/keys/lib/api-key-form.ts
  • web/default/src/features/keys/types.ts
  • web/default/src/features/models/components/dialogs/quick-import-models-dialog.tsx
  • web/default/src/features/models/components/models-dialogs.tsx
  • web/default/src/features/models/components/models-primary-buttons.tsx
  • web/default/src/features/models/components/models-provider.tsx
  • web/default/src/features/models/lib/model-presets.ts
  • web/default/src/features/pricing/index.tsx
  • web/default/src/features/profile/components/language-preferences-card.tsx
  • web/default/src/features/system-settings/maintenance/header-navigation-section.tsx
  • web/default/src/features/system-settings/site/index.tsx
  • web/default/src/features/users/components/users-mutate-drawer.tsx
  • web/default/src/features/users/lib/user-form.ts
  • web/default/src/features/users/types.ts
  • web/default/src/hooks/use-api-key-form-mode.ts
  • web/default/src/hooks/use-sidebar-data.ts
  • web/default/src/hooks/use-top-nav-links.ts
  • web/default/src/i18n/config.ts
  • web/default/src/i18n/locales/_reports/_sync-report.json
  • web/default/src/i18n/locales/_reports/fr.untranslated.json
  • web/default/src/i18n/locales/_reports/ja.untranslated.json
  • web/default/src/i18n/locales/_reports/ru.untranslated.json
  • web/default/src/i18n/locales/_reports/vi.untranslated.json
  • web/default/src/i18n/locales/_reports/zh.untranslated.json
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/fr.json
  • web/default/src/i18n/locales/ja.json
  • web/default/src/i18n/locales/ru.json
  • web/default/src/i18n/locales/vi.json
  • web/default/src/i18n/locales/zh.json
  • web/default/src/i18n/static-keys.ts
  • web/default/src/lib/constants.ts
  • web/default/src/routeTree.gen.ts
  • web/default/src/routes/_authenticated/errors/$error.tsx
  • web/default/src/routes/_authenticated/help/pricing.tsx
  • web/default/src/styles/index.css
  • web/default/src/styles/theme.css

Walkthrough

Adds 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.

Changes

DeepRouter Airbotix integration

Layer / File(s) Summary
Airbotix end-to-end stack wiring
middleware/*, relay/*, internal/*, service/*, setting/alias_setting/*, model/*, controller/*, router/*, web/default/src/features/*, web/default/src/components/*, web/default/src/hooks/*, web/default/src/styles/*, common/*, constant/*, main.go, .github/workflows/*, docs/*
Implements policy middleware and request mutations, simple-mode alias seed/init and token fields, billing webhook + auto-topup services, frontend simple-mode key creation and quick-import UIs, CI, docs, and branding/theme updates.

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
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • QuantumNous/new-api#4802 — Also modifies Stripe Checkout link generation in controller/topup_stripe.go around return URL handling.

Suggested reviewers

  • Calcium-Ion
  • creamlike1024

Poem

A rabbit routes with nimble cheer,
Through kids-safe lanes now crystal-clear.
Webhooks hop, Stripe taps a drum,
Aliases bloom—deeprouter hum.
Keys grow simple, models queue—
Carrots cached in Redis, too.
Ship it swift—thump-thump, woo-hoo! 🥕🐇

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch chore/drop-fr-ja-ru-vi-locales

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant