Commit Graph
183 Commits
Author SHA1 Message Date
RainbowBird 4f80affcd3 feat(admin-dashboard): add support for Bedrock and OpenAI-compatible LLM slices in admin router config
- Implemented `buildBedrockSlice` function to handle multi-kilobyte Bedrock bearer tokens.
- Enhanced `createAdminRouterConfigService` to classify Bedrock and OpenAI-compatible LLM upstreams by baseURL.
- Added new interfaces for `AdminRouterBedrockSlice` and `AdminRouterOpenAICompatibleSlice`.
- Updated router config form to support Bedrock and OpenAI-compatible slices.
- Created tests for Bedrock and OpenAI-compatible slice compilation and behavior.
- Modified UI components to accommodate new slice types and improve user experience.
- Ensured proper normalization of API server URLs to HTTPS when necessary.
2026-07-01 02:14:12 +08:00
Lovehsigure_520 3d7a8300d8 feat(analytics): add product instrumentation for activation and voice flows (#2023) 2026-06-30 23:36:08 +08:00
RainbowBird 8036501fb0 feat(server): add default TTS model handling and update related tests 2026-06-28 16:55:09 +08:00
Lovehsigure_520 16d50433b9 fix(server): auth UI callback trust (#1976) 2026-06-14 17:34:43 +08:00
RainbowBird 3215687e98 feat(server): add Aliyun NLS ASR gateway and official transcription provider (#1970) 2026-06-14 04:22:07 +08:00
RainbowBird 8e572ad93c feat(server): add stepfun tts provider
Signed-off-by: RainbowBird <git@luoling.moe>

Commit-Message-Assisted-by: Claude (via Claude Code)
2026-06-11 00:23:04 +08:00
RainbowBird 5bf6deab89 feat(admin-ui): load and edit router config from forms
Add a form-first LLM/TTS router config editor and a redacted admin config snapshot so operators can inspect configKV state before applying changes.

Preserve existing encrypted key entries when loaded slices are submitted without new plaintext keys.

Signed-off-by: RainbowBird <git@luoling.moe>
Commit-Message-Assisted-by: Claude (via Claude Code)
2026-06-11 00:14:34 +08:00
RainbowBird 16f9aaa4e3 feat(auth): enhance OAuth linking flow with account selection and error handling
- Added `prompt: 'select_account'` for Google and GitHub providers to force account selection during OAuth linking.
- Introduced localized error messages for account linking issues in multiple languages.
- Implemented a function to resolve OAuth error codes to specific i18n keys for better user feedback.
- Updated account settings page to display relevant error messages based on OAuth linking status.
2026-06-10 16:42:13 +08:00
RainbowBird a331d3a42a feat(auth): update AUTH_UI_URL to point to accounts.airi.build 2026-06-09 18:01:34 +08:00
RainbowBird 7fa7f8a072 feat(admin-ui): update admin UI URLs to remove trailing slashes and adjust redirects 2026-06-09 17:46:58 +08:00
RainbowBird 2ac4e90d56 feat(auth): add account linking support for different email providers 2026-06-09 17:46:51 +08:00
RainbowBird 11b820a77f feat(admin-ui): implement standalone admin UI with Cloudflare Pages deployment 2026-06-09 17:09:06 +08:00
RainbowBird 57ba232432 feat(auth): migrate auth ui to unique domain 2026-06-09 00:15:34 +08:00
RainbowBird 9382a12137 chore: cleanup and lint fix 2026-06-07 20:31:22 +08:00
RainbowBird 4de2aef746 chore(server): type issues 2026-06-07 19:53:44 +08:00
RainbowBird abb6d16b67 feat(server): track blocked TTS preflight 2026-06-06 00:22:11 +08:00
RainbowBird e6fa39ed4b feat(server): voice pack and tts routing (#1905)
Track per-app TTS concurrency in Redis, route capped upstreams by
available pool capacity, and surface pool saturation metrics. Document
the Voice Pack plan so the remaining backend and card-binding work has
an explicit implementation map.
2026-06-05 22:39:30 +08:00
RainbowBird cf5ed0766b fix(server): make analytics best-effort
Remove server-side PostHog wiring so request handlers no longer wait on third-party analytics delivery.

Keep auth activity and product event writes best-effort so degraded analytics do not fail session creation.

Signed-off-by: RainbowBird <git@luoling.moe>

Commit-Message-Assisted-by: Codex (via Codex)
2026-06-05 16:27:10 +08:00
Liet Blue dc6807a21f feat(server-dashboard): add admin UI for AIRI operations (#1942) 2026-06-04 16:16:19 +08:00
RainbowBird 7ac69db4ef feat(server): add product analytics events (#1941) 2026-06-03 23:03:33 +08:00
RainbowBird 41e8cd7626 chore(eventa): use upstream hono adapter
Upgrade @moeru/eventa to v1.0.0-beta.8 through the pnpm catalog and remove the local Hono adapter copy.

Keep AIRI on business-level Eventa usage while adapter contract tests live upstream in @moeru/eventa.

Signed-off-by: RainbowBird <git@luoling.moe>

Commit-Message-Assisted-by: Claude (via Claude Code)
2026-06-03 17:12:13 +08:00
RainbowBird e4acc78e25 refactor(server): enhance validation schemas for auth and audio processing 2026-06-03 13:48:19 +08:00
RainbowBird a795806e81 refactor(server): split auth route helpers 2026-06-03 13:48:19 +08:00
RainbowBird f5b051d969 refactor(server): split chat ws runtime 2026-06-03 13:48:19 +08:00
RainbowBird baf4f1fa51 refactor(server): split audio speech ws session 2026-06-03 13:48:18 +08:00
RainbowBird ea1c658c8d refactor(server): split stripe route operations 2026-06-03 13:48:18 +08:00
RainbowBirdandNeko 6f6fe01b3e refactor(server): openai route gateway
Co-authored-by: Neko <neko@ayaka.moe>
2026-06-03 00:21:25 +08:00
RainbowBird ee3ae8fc31 refactor(server): tts migrate to unspeech 2026-06-02 17:34:08 +08:00
RainbowBird c64e48c233 refactor(server): nanoid 2026-06-02 15:40:05 +08:00
RainbowBird 1936a22c95 refactor(server): split openai v1 route pipeline
Separate OpenAI-compatible route wiring from chat, speech, catalog, billing, and telemetry pipeline code.

Signed-off-by: RainbowBird <git@luoling.moe>

Commit-Message-Assisted-by: Claude (via Claude Code)
2026-06-02 15:39:25 +08:00
RainbowBird 03018a6a1e refactor(server): use es-toolkit clamp for query bounds (#1923) 2026-06-02 15:00:37 +08:00
RainbowBird f728f4d639 feat(server): add testing-only bearer token bypass for authentication 2026-06-01 18:16:21 +08:00
RainbowBird f2897f7663 feat(server): total user metrics 2026-05-30 21:20:36 +08:00
RainbowBird b5b6e4fb23 chore(server): forbidden vi.mock and vi.hoist 2026-05-30 20:18:50 +08:00
RainbowBird 42436bca53 refactor(server): langfuse upstream model 2026-05-30 20:13:53 +08:00
RainbowBird 6e871253e7 feat(server): langfuse 2026-05-30 19:35:37 +08:00
RainbowBird dc1037f349 feat(server): user metrics 2026-05-30 00:37:51 +08:00
RainbowBird 95915923e1 feat(server): enhance TTS configuration with support for default voices 2026-05-28 16:59:08 +08:00
RainbowBird 0ac9b709a2 refactor(server): integrate better-auth admin plugin (#1873) 2026-05-26 02:04:25 +08:00
RainbowBird 7e96386ec0 fix(server): unblock FLUX checkout from Electron desktop renderer
The packaged Electron renderer loads from file://, so its Stripe checkout
and portal requests carry no Referer and an opaque/absent Origin.
resolveTrustedRequestOrigin() returned undefined and the routes threw
INVALID_ORIGIN ("Missing trusted request origin"), blocking FLUX purchases
on desktop. Web and mobile were unaffected because they send a trusted web
origin. CORS was not the blocker: file:// requests already reach the API
(balance, providers, characters all work), so only the checkout-specific
redirect-base dependency failed.

Stripe success_url/cancel_url/return_url must be http(s) URLs, so file://
can never be a valid redirect base. Replace the trusted-origin requirement
with resolveCheckoutRedirectBase(), which prefers the request's trusted
origin (web/mobile return to where they started) and falls back to a
configured canonical web app URL (WEB_APP_URL, default https://airi.moeru.ai)
for origin-less clients like the desktop app.

On the client, the desktop app opens checkout in the system browser via
window.open (routed through setWindowOpenHandler -> shell.openExternal)
instead of navigating the file:// window to Stripe, and refreshes the FLUX
balance on window focus since it never receives the success_url redirect.
2026-05-26 01:56:47 +08:00
RainbowBird 6f63ce96e9 feat(server): server-controlled default streaming TTS model
The streaming speech provider settings page was seeding its model
picker with a hardcoded `volcengine/seed-tts-2.0`, which contradicts
the just-landed "streaming model catalog comes from the server"
contract. Now the default also comes from configKV.

- `UNSPEECH_UPSTREAM.streaming` gains `defaultModel?: string`.
- `GET /api/v1/audio/models/streaming` response gains `default: string
  | null` reading that field.
- admin slice apply: `streaming.defaultModel` survives key/URL
  rotation alongside `streaming.models`.
- `providerOfficialSpeechStreaming.listModels` stashes the value in a
  module-scope ref, exposed via `getDefaultStreamingModel()`.
- Streaming speech settings page reads the helper instead of the
  hardcoded id; falls back to the first server-returned model if the
  operator hasn't curated a default. `handleGenerateSpeech` no longer
  has a `seed-tts-2.0` backstop — a missing backend prefix in the
  selected model id now throws instead of silently defaulting.
2026-05-24 21:13:52 +08:00
RainbowBird be2c355b0e refactor(server): merge STREAMING_TTS_UPSTREAM + UNSPEECH_REST_BASE_URL into UNSPEECH_UPSTREAM
One unspeech deployment = one configKV entry. The split into two keys
modelled a hypothetical split deployment (operators pointing REST and
WS at different unspeech instances) that nobody actually runs, and the
explanatory comments justifying the split were exactly the migration-
narrative anti-pattern we just banned from source.

Schema:

  UNSPEECH_UPSTREAM = {
    restBaseURL: string,                  // required, http(s)://host:port
    streaming?: {                         // optional, only when ws TTS is on
      baseURL: string,                    // ws(s)://host:port/...
      keys: [{ id, ciphertext }],
      adapterParams: {},
      models?: [{ id, name?, description? }],
    },
  }

Admin slice surface flattens to one `kind: 'unspeech'`:

  { kind: 'unspeech', restBaseURL,
    streaming?: { upstreamURL, plaintextKey, keyEntryId? } }

Read-site changes:
- routeTts + listTtsVoices read UNSPEECH_UPSTREAM.restBaseURL via
  getOrThrow; absent entry → 503 CONFIG_NOT_SET.
- audio-speech-ws dials UNSPEECH_UPSTREAM.streaming and 1008-closes
  with streaming_tts_not_configured when the subtree is absent.
- handleListStreamingVoices reads .restBaseURL directly (no more
  ws→http scheme swap) and 503s on missing streaming subtree.
- handleListStreamingTTSModels reads .streaming.models.
- config-sync-subscriber listens for UNSPEECH_UPSTREAM invalidations
  instead of UNSPEECH_REST_BASE_URL.

Drive-by: tighten ConfigKVService.getOrThrow return to
`Exclude<ConfigDefinitions[K], undefined>` so call sites stop needing
non-null assertions on optional schema entries — the runtime already
throws, the type now reflects it.

Migration: operators repost a single admin slice to rewrite the merged
entry. Old STREAMING_TTS_UPSTREAM and UNSPEECH_REST_BASE_URL rows
become dead data and can be deleted from configKV; nothing reads them.
2026-05-19 23:28:42 +08:00
RainbowBird 615e0441e8 refactor(server): proxy TTS through unspeech, drop implicit fallbacks
Frontend voice picker silently lost Azure voices because /audio/voices
was called without ?model=, so the server fell back to DEFAULT_TTS_MODEL
and only ever returned one model's catalog. /audio/models also hardcoded
a single `auto` alias, so the client could never request a specific
model id even if it wanted to. On top of that, the Azure adapter shipped
only 2 hand-typed voices in voices/azure.json — the rest of Microsoft's
catalog was unreachable.

Fixed in one pass:

- Drop voices/*.json. Every getVoiceCatalog now calls unspeech REST
  (backend=microsoft|alibaba|volcengine). Azure proxies Microsoft's live
  voices/list; cosyvoice + volcengine use unspeech's embedded catalogs.
- Drop direct upstream provider calls from send(). All three adapters
  POST to <UNSPEECH_REST_BASE_URL>/v1/audio/speech with the openai-shaped
  body unspeech expects (model: `<backend>/<id>`). Azure SSML still built
  on our side so <prosody rate> speed survives — unspeech accepts
  pre-built <speak> documents.
- Fail-fast on every voices endpoint: empty ?model= → 400 MISSING_MODEL
  (no implicit DEFAULT_TTS_MODEL fallback), only `auto` resolves;
  streaming upstream missing → 503; unspeech non-2xx / network err → 502;
  malformed body → 502 with grepable message. No silent `{voices: []}`.
- /audio/models lists every LLM_ROUTER_CONFIG.tts.models key + `auto`.
- Frontend providerOfficialSpeech.listVoices passes ?model= and throws
  on upstream failure instead of returning [].
- New UNSPEECH_REST_BASE_URL configKV entry (no default, missing → 503).
  config-sync subscriber invalidates the voice cache when this key OR
  LLM_ROUTER_CONFIG changes.
- Router voice catalog Redis cache covers all providers with per-provider
  TTL (azure 6h live, alibaba/volcengine 24h since unspeech embeds them
  at build time).

Pending billing-SKU verification: DashScope cosyvoice now goes through
unspeech's WS-internal alibaba backend (was REST `/SpeechSynthesizer`).
Functional output equivalent — confirm bill after deploy, revert via
git history if WS pricing is materially higher.
2026-05-19 23:14:16 +08:00
RainbowBird 6b0788dd60 feat(server): expose streaming TTS model catalog from config
Streaming TTS model list was hardcoded in the official provider with two
Volcengine ids. This moves it server-side so operators control the
catalog without a client release.

Catalog lives on `STREAMING_TTS_UPSTREAM.models` (same configKV entry as
the ws upstream + key envelope) rather than a separate kv key — connection
target, keys, and surfaced ids are one deployment decision, splitting
them risks drift on rotation. `LLM_ROUTER_CONFIG.tts.models` is the
router/fallback domain for HTTP TTS; streaming is single-ws and has no
router semantics, so it gets its own `streamingTtsUpstreamSchema`
instead of reusing `ttsUpstreamSchema`.

New `GET /api/v1/audio/models/streaming` returns the configured list;
empty when upstream or models is unset (UI renders "no models" instead
of 5xx). Admin slice apply preserves existing `models` across key/connection
rotation so admin POSTs that only carry upstream+keys do not wipe the
catalog. Frontend `providerOfficialSpeechStreaming.listModels` fetches
the endpoint and throws on upstream errors (no silent empty array).
2026-05-19 23:14:16 +08:00
RainbowBird 201beae5ac fix(server): import 2026-05-18 23:48:11 +08:00
RainbowBird 4ed7315860 chore(server): finish test layout cleanup and tuck oidc under auth
- services/domain/llm-router and services/domain/user-deletion still had
  colocated *.test.ts; moved them into the local tests/ subdir so every
  service module follows the same layout as billing/, admin/*/, etc.
- routes/oidc/ wasn't an independent route group — it's wholly consumed
  by routes/auth/index.ts (electron-callback relay + OIDC bearer token
  helper). Moved to routes/auth/oidc/ to reflect that ownership.
- Fixed the relative imports in src/libs/tests/{auth,env,request-auth,
  ws-auth}.test.ts. They were moved to libs/tests/ in the previous
  commit without updating the './<source>' specifiers; typecheck only
  caught it once the rewriter ran across the tree.

pnpm typecheck passes; eslint matches the main baseline.
2026-05-18 23:39:14 +08:00
RainbowBird 812b2db4ab chore(server): drop flux_grant_batch schema and tables
Why
- Code, routes, service, worker, tests, and ai-context references for the
  legacy flux_grant_batch flow were removed in the previous commit. The
  schema file and the corresponding production tables were intentionally
  left for a separate DDL-only PR (this one) so the destructive change is
  easy to time and roll back.

What
- Delete src/schemas/flux-grant-batch.ts.
- Drop the re-export line in src/schemas/index.ts.
- drizzle-kit generate produced drizzle/0011_open_unus.sql:
    DROP TABLE flux_grant_batch CASCADE;
    DROP TABLE flux_grant_batch_recipient CASCADE;
  CASCADE removes the 6 associated indexes in one shot.
- docs/ai-context/architecture-overview.md updated: the dead-code
  reminder now points at the migration and explains the rollback story.

Also rolls in a pre-existing local move that was sitting uncommitted:
src/libs/{auth,env,request-auth,ws-auth}.test.ts → src/libs/tests/...
(aligning with the libs/tests/eventa-hono-adapter.test.ts placement that
was already on HEAD).

Deployment
- pnpm typecheck: passes.
- DO NOT run pnpm db:push on prod from this branch automatically. The
  drop is intentionally a separate operator action that requires picking
  a deploy window where no instance is still on an older image that
  could try to read flux_grant_batch. Until 0011 is applied to prod the
  table sits as an orphaned shell — safe to leave indefinitely.
2026-05-18 23:39:14 +08:00
RainbowBird c627bce9c9 refactor(server): split services into domain/adapter layers, drop dead code
Why
- src/services/ was an unordered mix of single-file services and module
  directories with no shared classification axis, plus several long-dead
  admin batch helpers that survived the move to the simpler synchronous
  admin-flux-grants flow.

What
- services/ now has two top-level layers:
    domain/   — DB state + business rules (billing, characters, chats,
                flux, flux-transaction, llm-router, providers, request-log,
                stripe, user-deletion, admin/{flux-grants,router-config})
    adapters/ — thin wrappers over external SDKs / infra (config-kv, email,
                posthog, tts/)
- admin/* moved under domain/admin/ with consistent plural names
  (flux-grants, router-config).
- tts-adapters/ collapsed to adapters/tts/ (no redundant -adapters suffix
  once nested under adapters/).
- 63 src files + scripts/e2e-llm-router.ts + tests/verifications/_harness.ts
  had relative imports rewritten; git mv preserves blame.
- apps/server/CLAUDE.md and docs/ai-context/*.md updated to match new paths.

Dead code removed
- services/admin-flux-grant-batches/ (service + worker + tests, 1090 LOC) —
  superseded by admin-flux-grants and never wired into app.ts.
- routes/admin/flux-grant-batches/ — same.
- utils/redis-compressed.ts + test — zero production call sites.
- llm-router/index.ts re-exports trimmed from 26 to 6; only symbols with
  external consumers are kept.

Intentionally kept
- schemas/flux-grant-batch.ts and its schemas/index.ts export remain so the
  drizzle-kit generate diff stays empty. Removing them is a separate PR
  that owns the drop-table migration for flux_grant_batch /
  flux_grant_batch_recipient.

Verification
- pnpm -F @proj-airi/server typecheck: passes.
- pnpm exec eslint apps/server: 49 errors, identical to main baseline
  (all are pre-existing node/prefer-global/buffer in envelope-crypto and
  scripts/e2e-llm-router; untouched by this change).
- Vitest passes per-file; the 6 mockDB hook timeouts under full-parallel
  run are the known pushSchema-per-worker infra cost, not a regression.
2026-05-18 23:36:45 +08:00
RainbowBird 45fb765df7 fix(server): ws conn 401 2026-05-18 23:36:08 +08:00
RainbowBird 8ef5844928 refactor(server): drop seed-router-config / seed-streaming-tts scripts
The two seed scripts are fully superseded by the new admin endpoint
`POST /api/admin/config/router` — same encryption, same configKV
writes, same `configkv:invalidate` publish, plus auth + audit + body
limits. Keeping both code paths created a drift risk on the AAD label
and the merge semantics.

Doc + test fallout:
- `e2e-llm-router.ts` now points readers to the admin endpoint for
  the prerequisite seed step.
- `docs/ai-context/verifications/llm-router.md` and
  `streaming-tts.md` get curl-based seed instructions; the 2026-05-15
  llm-router evidence stays intact with a note that the script it
  used has since been removed.
- The U9 follow-up entry in `llm-router.md` flips from "not shipped"
  to "partially shipped" — ETag + HMAC publish are still deferred,
  so the `config_write` / `config_invalid_hmac` Grafana panels stay
  parked.
- Self-edit on the admin route + `app.ts` docstrings to drop the
  earlier "scripts stay as break-glass" wording.
2026-05-18 23:36:07 +08:00