fix(auth): clarify return type and avoid TS2742 in createAuth function

fix(tests): cast response data to any in character and provider tests
refactor(v1completions): use Array.at() for last data line retrieval
feat(accounts): add indexes for session, account, and verification tables
chore(deps): update package.json and pnpm-lock.yaml with new dependencies
refactor(onboarding): replace context injection with props in step-welcome component
This commit is contained in:
RainbowBird
2026-03-28 02:25:44 +08:00
committed by RainbowBird
parent 97cadd5f7a
commit 5b1ccb55c2
9 changed files with 122 additions and 145 deletions
+4 -1
View File
@@ -7,7 +7,10 @@ import { bearer } from 'better-auth/plugins'
import * as authSchema from '../schemas/accounts'
export function createAuth(db: Database, env: Env) {
// NOTICE: return type uses `any` to avoid TS2742 — betterAuth's inferred type
// references internal pnpm paths (@better-auth/core) that aren't directly accessible
export function createAuth(db: Database, env: Env): any {
return betterAuth({
database: drizzleAdapter(db, {
provider: 'pg',
@@ -78,7 +78,7 @@ describe('characterRoutes', () => {
}), { user: testUser } as any)
expect(res.status).toBe(201)
const data = await res.json()
const data = await res.json() as any
expect(data.id).toBeDefined()
const char = await characterService.findById(data.id)
@@ -88,7 +88,7 @@ describe('characterRoutes', () => {
it('get / should return created character', async () => {
const res = await app.fetch(new Request('http://localhost/'), { user: testUser } as any)
expect(res.status).toBe(200)
const data = await res.json()
const data = await res.json() as any
expect(data.length).toBe(1)
expect(data[0].i18n[0].name).toBe('Aster')
})
@@ -102,7 +102,7 @@ describe('characterRoutes', () => {
expect(await res.json()).toEqual({ liked: true })
const res2 = await app.fetch(new Request('http://localhost/'), { user: testUser } as any)
const data = await res2.json()
const data = await res2.json() as any
expect(data[0].likesCount).toBe(1)
})
@@ -78,7 +78,7 @@ describe('providerRoutes', () => {
}), { user: testUser } as any)
expect(res.status).toBe(201)
const data = await res.json()
const data = await res.json() as any
expect(data.id).toBeDefined()
expect(data.name).toBe('My OpenAI')
})
@@ -94,7 +94,7 @@ describe('providerRoutes', () => {
const res = await app.fetch(new Request('http://localhost/'), { user: testUser } as any)
expect(res.status).toBe(200)
const data = await res.json()
const data = await res.json() as any
expect(data.length).toBe(2)
expect(data.some((p: any) => p.isSystem === true)).toBe(true)
expect(data.some((p: any) => p.isSystem === false)).toBe(true)
@@ -106,14 +106,14 @@ describe('providerRoutes', () => {
const res = await app.fetch(new Request(`http://localhost/${providerId}`), { user: testUser } as any)
expect(res.status).toBe(200)
const data = await res.json()
const data = await res.json() as any
expect(data.id).toBe(providerId)
expect(data.isSystem).toBe(false)
// Test system config access
const resSys = await app.fetch(new Request('http://localhost/sys-1'), { user: testUser } as any)
expect(resSys.status).toBe(200)
const dataSys = await resSys.json()
const dataSys = await resSys.json() as any
expect(dataSys.id).toBe('sys-1')
expect(dataSys.isSystem).toBe(true)
})
@@ -146,7 +146,7 @@ describe('v1CompletionsRoutes', () => {
)
expect(res.status).toBe(200)
const data = await res.json()
const data = await res.json() as any
expect(data.id).toBe('chatcmpl-1')
// Verify flux was consumed
+6 -127
View File
@@ -1,5 +1,3 @@
/* eslint-disable unused-imports/no-unused-vars */
import type { Context } from 'hono'
import type { initOtel } from '../libs/otel'
@@ -28,10 +26,10 @@ const SAFE_RESPONSE_HEADERS = new Set([
function buildSafeResponseHeaders(response: Response): Headers {
const headers = new Headers()
for (const [key, value] of response.headers) {
response.headers.forEach((value, key) => {
if (SAFE_RESPONSE_HEADERS.has(key.toLowerCase()))
headers.set(key, value)
}
})
return headers
}
@@ -155,7 +153,7 @@ export function createV1CompletionsRoutes(fluxService: FluxService, configKV: Co
let usage: UsageInfo = {}
try {
const lines = tailBuffer.split('\n').filter(l => l.startsWith('data: ') && !l.includes('[DONE]'))
const lastDataLine = lines[lines.length - 1]
const lastDataLine = lines.at(-1)
if (lastDataLine) {
const json = JSON.parse(lastDataLine.slice(6))
usage = extractUsageFromBody(json)
@@ -230,133 +228,14 @@ export function createV1CompletionsRoutes(fluxService: FluxService, configKV: Co
return c.json(responseBody)
}
async function handleTTS(c: Context<HonoEnv>) {
const user = c.get('user')!
const flux = await fluxService.getFlux(user.id)
if (flux.flux <= 0) {
throw createPaymentRequiredError('Insufficient flux')
}
const body = await c.req.json()
const gatewayBaseUrl = await configKV.getOrThrow('GATEWAY_BASE_URL')
const baseUrl = normalizeBaseUrl(gatewayBaseUrl)
const requestModel = body.model || 'auto'
const span = tracer.startSpan('llm.gateway.tts', {
attributes: { 'llm.model': requestModel },
})
const startedAt = Date.now()
const response = await context.with(trace.setSpan(context.active(), span), () =>
fetch(`${baseUrl}audio/speech`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}))
const durationMs = Date.now() - startedAt
span.setAttribute('http.response.status_code', response.status)
if (!response.ok) {
span.setStatus({ code: SpanStatusCode.ERROR, message: `Gateway ${response.status}` })
span.end()
recordMetrics({ model: requestModel, status: response.status, type: 'tts', durationMs, fluxConsumed: 0 })
return new Response(response.body, {
status: response.status,
headers: buildSafeResponseHeaders(response),
})
}
const fluxPerRequest = await configKV.getOrThrow('FLUX_PER_REQUEST_TTS')
await fluxService.consumeFlux(user.id, fluxPerRequest)
span.setAttribute('llm.flux_consumed', fluxPerRequest)
span.end()
recordMetrics({ model: requestModel, status: response.status, type: 'tts', durationMs, fluxConsumed: fluxPerRequest })
requestLogService.logRequest({
userId: user.id,
model: requestModel,
status: response.status,
durationMs,
fluxConsumed: fluxPerRequest,
}).catch(err => logger.withError(err).warn('Failed to log TTS request'))
return new Response(response.body, {
status: response.status,
headers: buildSafeResponseHeaders(response),
})
}
async function handleTranscription(c: Context<HonoEnv>) {
const user = c.get('user')!
const flux = await fluxService.getFlux(user.id)
if (flux.flux <= 0) {
throw createPaymentRequiredError('Insufficient flux')
}
const gatewayBaseUrl = await configKV.getOrThrow('GATEWAY_BASE_URL')
const baseUrl = normalizeBaseUrl(gatewayBaseUrl)
const span = tracer.startSpan('llm.gateway.asr', {
attributes: { 'llm.model': 'auto' },
})
const startedAt = Date.now()
const rawBody = await c.req.arrayBuffer()
const contentType = c.req.header('content-type') || 'multipart/form-data'
const response = await context.with(trace.setSpan(context.active(), span), () =>
fetch(`${baseUrl}audio/transcriptions`, {
method: 'POST',
headers: { 'Content-Type': contentType },
body: rawBody,
}))
const durationMs = Date.now() - startedAt
span.setAttribute('http.response.status_code', response.status)
if (!response.ok) {
span.setStatus({ code: SpanStatusCode.ERROR, message: `Gateway ${response.status}` })
span.end()
recordMetrics({ model: 'auto', status: response.status, type: 'asr', durationMs, fluxConsumed: 0 })
return new Response(response.body, {
status: response.status,
headers: buildSafeResponseHeaders(response),
})
}
const fluxPerRequest = await configKV.getOrThrow('FLUX_PER_REQUEST_ASR')
await fluxService.consumeFlux(user.id, fluxPerRequest)
span.setAttribute('llm.flux_consumed', fluxPerRequest)
span.end()
recordMetrics({ model: 'auto', status: response.status, type: 'asr', durationMs, fluxConsumed: fluxPerRequest })
requestLogService.logRequest({
userId: user.id,
model: 'auto',
status: response.status,
durationMs,
fluxConsumed: fluxPerRequest,
}).catch(err => logger.withError(err).warn('Failed to log ASR request'))
return new Response(response.body, {
status: response.status,
headers: buildSafeResponseHeaders(response),
})
}
// TODO: TTS and ASR handlers are implemented but routes are disabled until ready
// async function handleTTS(c: Context<HonoEnv>) { ... }
// async function handleTranscription(c: Context<HonoEnv>) { ... }
const chatGuard = configGuard(configKV, ['FLUX_PER_REQUEST', 'GATEWAY_BASE_URL', 'DEFAULT_CHAT_MODEL'], 'Service is not available yet')
const ttsGuard = configGuard(configKV, ['FLUX_PER_REQUEST_TTS', 'GATEWAY_BASE_URL'], 'TTS service is not available yet')
const asrGuard = configGuard(configKV, ['FLUX_PER_REQUEST_ASR', 'GATEWAY_BASE_URL'], 'ASR service is not available yet')
return new Hono<HonoEnv>()
.use('*', authGuard)
.post('/chat/completions', chatGuard, handleCompletion)
.post('/chat/completion', chatGuard, handleCompletion)
// .post('/audio/speech', ttsGuard, handleTTS)
// .post('/audio/transcriptions', bodyLimit({ maxSize: 25 * 1024 * 1024 }), asrGuard, handleTranscription)
}
+4 -1
View File
@@ -1,5 +1,5 @@
import { relations } from 'drizzle-orm'
import { boolean, pgTable, text, timestamp } from 'drizzle-orm/pg-core'
import { boolean, index, pgTable, text, timestamp } from 'drizzle-orm/pg-core'
export const user = pgTable('user', {
id: text('id').primaryKey(),
@@ -30,6 +30,7 @@ export const session = pgTable(
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
},
table => [index('session_userId_idx').on(table.userId)],
)
export const account = pgTable(
@@ -53,6 +54,7 @@ export const account = pgTable(
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
},
table => [index('account_userId_idx').on(table.userId)],
)
export const verification = pgTable(
@@ -68,6 +70,7 @@ export const verification = pgTable(
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
},
table => [index('verification_identifier_idx').on(table.identifier)],
)
export const userRelations = relations(user, ({ many }) => ({
@@ -131,7 +131,7 @@ describe('fluxService (Redis-backed)', () => {
}).returning()
await service.getFlux(user3.id)
const results = await Promise.allSettled(
Array.from({ length: 10 }, () => service.consumeFlux(user3.id, 10)),
Array.from({ length: 10 }).fill(service.consumeFlux(user3.id, 10)),
)
const fulfilled = results.filter(r => r.status === 'fulfilled')
const rejected = results.filter(r => r.status === 'rejected')
@@ -151,7 +151,7 @@ describe('fluxService (Redis-backed)', () => {
}).returning()
await service.getFlux(user4.id)
await Promise.all(
Array.from({ length: 10 }, () => service.addFlux(user4.id, 5)),
Array.from({ length: 10 }).fill(service.addFlux(user4.id, 5)),
)
const final = await service.getFlux(user4.id)
expect(final.flux).toBe(150)