diff --git a/apps/server/docs/ai-context/architecture-overview.md b/apps/server/docs/ai-context/architecture-overview.md index 1092fc715..de2b23b8c 100644 --- a/apps/server/docs/ai-context/architecture-overview.md +++ b/apps/server/docs/ai-context/architecture-overview.md @@ -137,7 +137,7 @@ CLI 入口在 `src/bin/run.ts`,支持两种角色: ### LLM 网关代理而不是本地 provider 编排 -`/api/v1` 并不直接调具体模型 provider,而是转发到 `config: GATEWAY_BASE_URL`。因此: +`/api/v1/openai` 并不直接调具体模型 provider,而是转发到 `config: GATEWAY_BASE_URL`。因此: - 服务端关心的是鉴权、限流、计费、日志、观测 - 具体模型执行和 usage 返回格式由 gateway 决定 @@ -157,4 +157,4 @@ Redis 在这里同时承担: - `src/services/request-log.ts` 和 `src/services/llm-request-log.ts` 职责重复,当前实际注入的是前者。 - `src/schemas/accounts.ts` 和 `src/schemas/auth.ts` 内容重复,`createAuth()` 使用的是 `accounts.ts`。 -- `v1completions.ts` 已实现 `handleTTS` / `handleTranscription`,但路由仍被注释掉,当前只开放 chat completions。 +- `src/routes/openai/v1/index.ts` 已实现 `handleTTS` / `handleTranscription`,但路由仍被注释掉,当前只开放 chat completions。 diff --git a/apps/server/docs/ai-context/config-and-naming-conventions.md b/apps/server/docs/ai-context/config-and-naming-conventions.md index 6e1334ae4..86677cb9f 100644 --- a/apps/server/docs/ai-context/config-and-naming-conventions.md +++ b/apps/server/docs/ai-context/config-and-naming-conventions.md @@ -130,8 +130,8 @@ lock:{domain}:{id} 推荐: ```txt -/api/users/me/flux -/api/users/me/flux/history +/api/v1/flux +/api/v1/flux/history ``` 不推荐: @@ -147,15 +147,20 @@ lock:{domain}:{id} ```txt /api/v1/... -/api/openai/v1/... +/api/v1/openai/... ``` 不要让“部分资源版本化、部分资源裸挂”长期并存而没有说明。 ## TODO +- Replace DB-derived HTTP request schemas for characters/providers/chats with explicit DTO schemas. +- Move ownership and membership authorization rules behind actor-aware service APIs instead of splitting them across routes and services. +- Stabilize HTTP response shapes so services no longer leak raw Drizzle returning arrays to routes. +- Encode chat member invariants in schema validation and map those failures to 4xx API errors. +- Split the OpenAI compat route and chat WebSocket handler into smaller modules so transport code stops owning orchestration complexity. - 把 `apps/server` 中现有 Redis key / channel 继续向 helper 收口,避免业务代码里散落模板字符串。 - 统一把旧式 key 命名迁移到分段命名风格,优先处理 Flux cache、chat broadcast、lock key。 - 给 `configKV` 增补一份“哪些配置属于 infra、哪些属于运营策略”的清单,避免继续模糊放置位置。 - 把所有 `configKV.getOptional(...) ?? defaultValue` 模式清理掉,默认值统一回到 `ConfigEntrySchemas`。 -- 评估是否把 `/api/v1` 收口为兼容层 API,并明确业务资源是否也需要统一版本化。 +- 评估是否继续沿用统一 `/api/v1/*` 版本树,还是为兼容 API 与业务 API 引入更明确的子域分隔。 diff --git a/apps/server/docs/ai-context/transport-and-routes.md b/apps/server/docs/ai-context/transport-and-routes.md index 5b65770c5..70ab2630f 100644 --- a/apps/server/docs/ai-context/transport-and-routes.md +++ b/apps/server/docs/ai-context/transport-and-routes.md @@ -6,12 +6,12 @@ - `GET /health` - `/api/auth/*` -- `/api/characters` -- `/api/providers` -- `/api/chats` -- `/api/v1` -- `/api/users/me/flux` -- `/api/stripe` +- `/api/v1/characters` +- `/api/v1/providers` +- `/api/v1/chats` +- `/api/v1/openai` +- `/api/v1/flux` +- `/api/v1/stripe` - `GET /ws/chat` ## 鉴权链路 @@ -52,11 +52,11 @@ - Bearer plugin 已启用 - `/api/auth/*` 有独立 IP 限流,每分钟 20 次 -### `/api/characters` +### `/api/v1/characters` 实现位置: -- route: `src/routes/characters.ts` +- route: `src/routes/characters/index.ts` - service: `src/services/characters.ts` 主要能力: @@ -77,11 +77,11 @@ - 更新和删除会额外校验 `ownerId === user.id` - 点赞和收藏是 toggle 语义 -### `/api/providers` +### `/api/v1/providers` 实现位置: -- route: `src/routes/providers.ts` +- route: `src/routes/providers/index.ts` - service: `src/services/providers.ts` 主要能力: @@ -96,11 +96,11 @@ - `findAll(ownerId)` 通过 `unionAll` 合并系统配置和用户配置 - 用户只能改自己的 user config,不能改 system config -### `/api/chats` +### `/api/v1/chats` 实现位置: -- route: `src/routes/chats.ts` +- route: `src/routes/chats/index.ts` - service: `src/services/chats.ts` 主要能力: @@ -119,7 +119,7 @@ 实现位置: - route 注册:`src/app.ts` -- handler factory: `src/routes/chat-ws.ts` +- handler factory: `src/routes/chat-ws/index.ts` - 底层事件适配:`src/libs/eventa-hono-adapter.ts` 主要 RPC: @@ -141,11 +141,11 @@ - key / channel 与 payload 边界应集中收口,不要在调用点散落模板字符串和裸 `JSON.parse` - 具体规范见 `redis-boundaries-and-pubsub.md` -### `/api/v1` +### `/api/v1/openai` 实现位置: -- route: `src/routes/v1completions.ts` +- route: `src/routes/openai/v1/index.ts` - 依赖服务: - `fluxService` - `billingService` @@ -154,10 +154,10 @@ 当前已开放: -- `POST /api/v1/chat/completions` -- `POST /api/v1/chat/completion` -- `POST /api/v1/audio/speech` -- `POST /api/v1/audio/transcriptions` +- `POST /api/v1/openai/chat/completions` +- `POST /api/v1/openai/chat/completion` +- `POST /api/v1/openai/audio/speech` +- `POST /api/v1/openai/audio/transcriptions` 请求流程: @@ -181,27 +181,27 @@ - 流结束后再 best-effort 扣费 - 扣费失败只打 error log,不回滚给客户端 -### `/api/users/me/flux` +### `/api/v1/flux` 实现位置: -- route: `src/routes/flux.ts` +- route: `src/routes/flux/index.ts` - services: - `fluxService` - `fluxAuditService` 主要能力: -- `GET /api/users/me/flux` +- `GET /api/v1/flux` - 读取当前用户余额 -- `GET /api/users/me/flux/history` +- `GET /api/v1/flux/history` - 读取用户可见流水 -### `/api/stripe` +### `/api/v1/stripe` 实现位置: -- route: `src/routes/stripe.ts` +- route: `src/routes/stripe/index.ts` - services: - `fluxService` - `stripeService` @@ -226,7 +226,7 @@ ## 参数校验方式 -输入 schema 位于 `src/api/*.schema.ts`: +输入 schema 位于各资源路由目录下的 `schema.ts`: - `characters.schema.ts` - `chats.schema.ts` diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index e8df24aed..131485737 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -38,9 +38,9 @@ import { createCharacterRoutes } from './routes/characters' import { createChatWsHandlers } from './routes/chat-ws' import { createChatRoutes } from './routes/chats' import { createFluxRoutes } from './routes/flux' +import { createV1CompletionsRoutes } from './routes/openai/v1' import { createProviderRoutes } from './routes/providers' import { createStripeRoutes } from './routes/stripe' -import { createV1CompletionsRoutes } from './routes/v1completions' import { createBillingMq } from './services/billing/billing-events' import { createBillingService } from './services/billing/billing-service' import { createCharacterService } from './services/characters' @@ -109,7 +109,7 @@ function buildApp(deps: AppDeps) { .use('*', sessionMiddleware(deps.auth)) .use('*', async (c, next) => { // Skip global body limit for ASR transcription route (has its own 25MB limit) - if (c.req.path === '/api/v1/audio/transcriptions') { + if (c.req.path === '/api/v1/openai/audio/transcriptions') { return next() } return bodyLimit({ maxSize: 1024 * 1024 })(c, next) @@ -153,32 +153,32 @@ function buildApp(deps: AppDeps) { /** * Character routes are handled by the character service. */ - .route('/api/characters', createCharacterRoutes(deps.characterService)) + .route('/api/v1/characters', createCharacterRoutes(deps.characterService)) /** * Provider routes are handled by the provider service. */ - .route('/api/providers', createProviderRoutes(deps.providerService)) + .route('/api/v1/providers', createProviderRoutes(deps.providerService)) /** * Chat routes are handled by the chat service. */ - .route('/api/chats', createChatRoutes(deps.chatService)) + .route('/api/v1/chats', createChatRoutes(deps.chatService)) /** * V1 routes for official provider. */ - .route('/api/v1', createV1CompletionsRoutes(deps.fluxService, deps.billingService, deps.configKV, deps.billingMq, deps.otel?.llm)) + .route('/api/v1/openai', createV1CompletionsRoutes(deps.fluxService, deps.billingService, deps.configKV, deps.billingMq, deps.otel?.llm)) /** * Flux routes. */ - .route('/api/flux', createFluxRoutes(deps.fluxService, deps.fluxAuditService)) + .route('/api/v1/flux', createFluxRoutes(deps.fluxService, deps.fluxAuditService)) /** * Stripe routes. */ - .route('/api/stripe', createStripeRoutes(deps.fluxService, deps.stripeService, deps.billingService, deps.configKV, deps.env, deps.otel?.revenue)) + .route('/api/v1/stripe', createStripeRoutes(deps.fluxService, deps.stripeService, deps.billingService, deps.configKV, deps.env, deps.otel?.revenue)) return { app: builtApp, injectWebSocket } } diff --git a/apps/server/src/routes/characters.ts b/apps/server/src/routes/characters/index.ts similarity index 84% rename from apps/server/src/routes/characters.ts rename to apps/server/src/routes/characters/index.ts index 0c78fc1f7..aee0ef691 100644 --- a/apps/server/src/routes/characters.ts +++ b/apps/server/src/routes/characters/index.ts @@ -1,12 +1,12 @@ -import type { CharacterService } from '../services/characters' -import type { HonoEnv } from '../types/hono' +import type { CharacterService } from '../../services/characters' +import type { HonoEnv } from '../../types/hono' import { Hono } from 'hono' import { safeParse } from 'valibot' -import { CreateCharacterSchema, UpdateCharacterSchema } from '../api/characters.schema' -import { authGuard } from '../middlewares/auth' -import { createBadRequestError, createForbiddenError, createNotFoundError } from '../utils/error' +import { authGuard } from '../../middlewares/auth' +import { createBadRequestError, createForbiddenError, createNotFoundError } from '../../utils/error' +import { CreateCharacterSchema, UpdateCharacterSchema } from './schema' export function createCharacterRoutes(characterService: CharacterService) { return new Hono() @@ -65,6 +65,7 @@ export function createCharacterRoutes(characterService: CharacterService) { throw createBadRequestError('Invalid Request', 'INVALID_REQUEST', result.issues) } + // TODO: Move ownership checks into the service layer with an actor-aware API such as updateByOwner(user.id, id, input). const existing = await characterService.findById(id) if (!existing) throw createNotFoundError() @@ -79,6 +80,7 @@ export function createCharacterRoutes(characterService: CharacterService) { const user = c.get('user')! const id = c.req.param('id') + // TODO: Move ownership checks into the service layer with an actor-aware API such as deleteByOwner(user.id, id). const existing = await characterService.findById(id) if (!existing) throw createNotFoundError() diff --git a/apps/server/src/routes/tests/characters.test.ts b/apps/server/src/routes/characters/route.test.ts similarity index 98% rename from apps/server/src/routes/tests/characters.test.ts rename to apps/server/src/routes/characters/route.test.ts index fddd99d1c..6a6da29b9 100644 --- a/apps/server/src/routes/tests/characters.test.ts +++ b/apps/server/src/routes/characters/route.test.ts @@ -4,10 +4,10 @@ import type { HonoEnv } from '../../types/hono' import { Hono } from 'hono' import { beforeAll, describe, expect, it } from 'vitest' +import { createCharacterRoutes } from '.' import { mockDB } from '../../libs/mock-db' import { createCharacterService } from '../../services/characters' import { ApiError } from '../../utils/error' -import { createCharacterRoutes } from '../characters' import * as schema from '../../schemas' diff --git a/apps/server/src/api/characters.schema.ts b/apps/server/src/routes/characters/schema.ts similarity index 89% rename from apps/server/src/api/characters.schema.ts rename to apps/server/src/routes/characters/schema.ts index 80e88b445..071375f85 100644 --- a/apps/server/src/api/characters.schema.ts +++ b/apps/server/src/routes/characters/schema.ts @@ -1,7 +1,7 @@ import { createInsertSchema, createSelectSchema } from 'drizzle-valibot' import { array, literal, number, object, optional, pipe, string, transform, union } from 'valibot' -import * as schema from '../schemas/characters' +import * as schema from '../../schemas/characters' export const AvatarModelConfigSchema = object({ vrm: optional(object({ @@ -69,6 +69,8 @@ const DateSchema = pipe( ) export const CreateCharacterSchema = object({ + // TODO: Replace createInsertSchema-derived request bodies with explicit HTTP DTO schemas. + // The current shape still leaks persistence fields such as ownerId/creatorId into the API boundary. character: createInsertSchema(schema.character, { creatorId: optional(string()), ownerId: optional(string()), @@ -98,6 +100,8 @@ export const CreateCharacterSchema = object({ }))), }) +// TODO: Split update request schema from DB insert schema. +// This route should reject server-managed fields like id/ownerId/creatorId/timestamps instead of allowing them here. export const UpdateCharacterSchema = createInsertSchema(schema.character, { id: optional(string()), version: optional(string()), diff --git a/apps/server/src/routes/chat-ws.ts b/apps/server/src/routes/chat-ws/index.ts similarity index 90% rename from apps/server/src/routes/chat-ws.ts rename to apps/server/src/routes/chat-ws/index.ts index b598da478..ad115babe 100644 --- a/apps/server/src/routes/chat-ws.ts +++ b/apps/server/src/routes/chat-ws/index.ts @@ -1,16 +1,16 @@ import type Redis from 'ioredis' -import type { HonoWsInvocableEventContext } from '../libs/eventa-hono-adapter' -import type { EngagementMetrics } from '../libs/otel' -import type { ChatService } from '../services/chats' +import type { HonoWsInvocableEventContext } from '../../libs/eventa-hono-adapter' +import type { EngagementMetrics } from '../../libs/otel' +import type { ChatService } from '../../services/chats' import { useLogger } from '@guiiai/logg' import { defineInvokeHandler } from '@moeru/eventa' import { newMessages, pullMessages, sendMessages } from '@proj-airi/server-sdk-shared' -import { createPeerHooks, wsDisconnectedEvent } from '../libs/eventa-hono-adapter' -import { createChatBroadcastMessage, parseChatBroadcastMessage } from '../utils/chat-broadcast' -import { userChatBroadcastRedisKey } from '../utils/redis-keys' +import { createPeerHooks, wsDisconnectedEvent } from '../../libs/eventa-hono-adapter' +import { createChatBroadcastMessage, parseChatBroadcastMessage } from '../../utils/chat-broadcast' +import { userChatBroadcastRedisKey } from '../../utils/redis-keys' const log = useLogger('chat-ws').useGlobalConfig() @@ -54,6 +54,8 @@ export function createChatWsHandlers( redis: Redis, metrics?: EngagementMetrics | null, ) { + // TODO: Separate connection lifecycle, cross-instance broadcast, and RPC orchestration into smaller modules. + // This file is still acting as both transport adapter and chat delivery coordinator. // Dedicated subscriber connection (ioredis requires a separate connection for subscribe mode) const sub = redis.duplicate() diff --git a/apps/server/src/routes/chats.ts b/apps/server/src/routes/chats/index.ts similarity index 90% rename from apps/server/src/routes/chats.ts rename to apps/server/src/routes/chats/index.ts index d283a3f5b..ce179f989 100644 --- a/apps/server/src/routes/chats.ts +++ b/apps/server/src/routes/chats/index.ts @@ -1,12 +1,12 @@ -import type { ChatService } from '../services/chats' -import type { HonoEnv } from '../types/hono' +import type { ChatService } from '../../services/chats' +import type { HonoEnv } from '../../types/hono' import { Hono } from 'hono' import { safeParse } from 'valibot' -import { AddMemberSchema, CreateChatSchema, UpdateChatSchema } from '../api/chats.schema' -import { authGuard } from '../middlewares/auth' -import { createBadRequestError } from '../utils/error' +import { authGuard } from '../../middlewares/auth' +import { createBadRequestError } from '../../utils/error' +import { AddMemberSchema, CreateChatSchema, UpdateChatSchema } from './schema' export function createChatRoutes(chatService: ChatService) { return new Hono() diff --git a/apps/server/src/api/chats.schema.ts b/apps/server/src/routes/chats/schema.ts similarity index 75% rename from apps/server/src/api/chats.schema.ts rename to apps/server/src/routes/chats/schema.ts index ebd94aeee..278f67b52 100644 --- a/apps/server/src/api/chats.schema.ts +++ b/apps/server/src/routes/chats/schema.ts @@ -13,6 +13,9 @@ const ChatMemberTypeSchema = union([ literal('bot'), ]) +// TODO: Encode member invariants directly in schema: +// - type === 'user' requires userId +// - non-user member types require characterId export const CreateChatSchema = object({ id: optional(pipe(string(), minLength(1), maxLength(30))), type: optional(ChatTypeSchema), @@ -28,6 +31,7 @@ export const UpdateChatSchema = object({ title: optional(string()), }) +// TODO: Promote the same discriminated validation rules to AddMemberSchema so invalid combinations fail as 4xx at the HTTP boundary. export const AddMemberSchema = object({ type: ChatMemberTypeSchema, userId: optional(string()), diff --git a/apps/server/src/routes/flux.ts b/apps/server/src/routes/flux/index.ts similarity index 77% rename from apps/server/src/routes/flux.ts rename to apps/server/src/routes/flux/index.ts index 04f8ad31a..51cd9ccfd 100644 --- a/apps/server/src/routes/flux.ts +++ b/apps/server/src/routes/flux/index.ts @@ -1,12 +1,12 @@ -import type { FluxService } from '../services/flux' -import type { FluxAuditService } from '../services/flux-audit' -import type { HonoEnv } from '../types/hono' +import type { FluxService } from '../../services/flux' +import type { FluxAuditService } from '../../services/flux-audit' +import type { HonoEnv } from '../../types/hono' import { Hono } from 'hono' import { parse } from 'valibot' -import { authGuard } from '../middlewares/auth' -import { LimitOffsetPaginationQuerySchema } from '../utils/http-query' +import { authGuard } from '../../middlewares/auth' +import { LimitOffsetPaginationQuerySchema } from '../../utils/http-query' export function createFluxRoutes(fluxService: FluxService, fluxAuditService: FluxAuditService) { return new Hono() diff --git a/apps/server/src/routes/tests/flux.test.ts b/apps/server/src/routes/flux/route.test.ts similarity index 87% rename from apps/server/src/routes/tests/flux.test.ts rename to apps/server/src/routes/flux/route.test.ts index 44600ab83..a455d4cf3 100644 --- a/apps/server/src/routes/tests/flux.test.ts +++ b/apps/server/src/routes/flux/route.test.ts @@ -5,8 +5,8 @@ import type { HonoEnv } from '../../types/hono' import { Hono } from 'hono' import { describe, expect, it, vi } from 'vitest' +import { createFluxRoutes } from '.' import { ApiError } from '../../utils/error' -import { createFluxRoutes } from '../flux' function createMockFluxService(): FluxService { return { @@ -59,19 +59,19 @@ function createTestApp(fluxService: FluxService, fluxAuditService: FluxAuditServ await next() }) - app.route('/api/users/me/flux', routes) + app.route('/api/v1/flux', routes) return app } const testUser = { id: 'user-1', name: 'Test User', email: 'test@example.com' } describe('fluxRoutes', () => { - it('get /api/users/me/flux should return the current user balance', async () => { + it('get /api/v1/flux should return the current user balance', async () => { const fluxService = createMockFluxService() const app = createTestApp(fluxService, createMockFluxAuditService()) const res = await app.fetch( - new Request('http://localhost/api/users/me/flux'), + new Request('http://localhost/api/v1/flux'), { user: testUser } as any, ) @@ -80,12 +80,12 @@ describe('fluxRoutes', () => { expect(fluxService.getFlux).toHaveBeenCalledWith('user-1') }) - it('get /api/users/me/flux/history should clamp pagination query values', async () => { + it('get /api/v1/flux/history should clamp pagination query values', async () => { const fluxAuditService = createMockFluxAuditService() const app = createTestApp(createMockFluxService(), fluxAuditService) const res = await app.fetch( - new Request('http://localhost/api/users/me/flux/history?limit=999&offset=-12'), + new Request('http://localhost/api/v1/flux/history?limit=999&offset=-12'), { user: testUser } as any, ) diff --git a/apps/server/src/routes/v1completions.ts b/apps/server/src/routes/openai/v1/index.ts similarity index 93% rename from apps/server/src/routes/v1completions.ts rename to apps/server/src/routes/openai/v1/index.ts index bd829bbb3..4750cec28 100644 --- a/apps/server/src/routes/v1completions.ts +++ b/apps/server/src/routes/openai/v1/index.ts @@ -1,25 +1,25 @@ import type { Context } from 'hono' -import type { MqService } from '../libs/mq' -import type { LlmMetrics } from '../libs/otel' -import type { UsageInfo } from '../services/billing/billing' -import type { BillingEvent } from '../services/billing/billing-events' -import type { BillingService } from '../services/billing/billing-service' -import type { ConfigKVService } from '../services/config-kv' -import type { FluxService } from '../services/flux' -import type { HonoEnv } from '../types/hono' +import type { MqService } from '../../../libs/mq' +import type { LlmMetrics } from '../../../libs/otel' +import type { UsageInfo } from '../../../services/billing/billing' +import type { BillingEvent } from '../../../services/billing/billing-events' +import type { BillingService } from '../../../services/billing/billing-service' +import type { ConfigKVService } from '../../../services/config-kv' +import type { FluxService } from '../../../services/flux' +import type { HonoEnv } from '../../../types/hono' import { useLogger } from '@guiiai/logg' import { context, SpanStatusCode, trace } from '@opentelemetry/api' import { Hono } from 'hono' import { bodyLimit } from 'hono/body-limit' -import { authGuard } from '../middlewares/auth' -import { configGuard } from '../middlewares/config-guard' -import { rateLimiter } from '../middlewares/rate-limit' -import { calculateFluxFromUsage, extractUsageFromBody } from '../services/billing/billing' -import { createPaymentRequiredError } from '../utils/error' -import { nanoid } from '../utils/id' +import { authGuard } from '../../../middlewares/auth' +import { configGuard } from '../../../middlewares/config-guard' +import { rateLimiter } from '../../../middlewares/rate-limit' +import { calculateFluxFromUsage, extractUsageFromBody } from '../../../services/billing/billing' +import { createPaymentRequiredError } from '../../../utils/error' +import { nanoid } from '../../../utils/id' import { AIRI_ATTR_BILLING_FLUX_CONSUMED, AIRI_ATTR_GEN_AI_OPERATION_KIND, @@ -30,7 +30,7 @@ import { GEN_AI_ATTR_USAGE_INPUT_TOKENS, GEN_AI_ATTR_USAGE_OUTPUT_TOKENS, getServerConnectionAttributes, -} from '../utils/observability' +} from '../../../utils/observability' const tracer = trace.getTracer('v1-completions') @@ -72,6 +72,8 @@ function getLlmMetricAttributes(opts: { model: string, type: string, status: num export function createV1CompletionsRoutes(fluxService: FluxService, billingService: BillingService, configKV: ConfigKVService, billingMq: MqService, llm?: LlmMetrics | null) { const logger = useLogger('v1-completions').useGlobalConfig() + // TODO: Extract this compat route into smaller facades/modules. + // It currently mixes auth, rate limiting, proxying, billing, telemetry, and event publishing in one transport layer entrypoint. function recordMetrics(opts: { model: string, status: number, type: string, durationMs: number, fluxConsumed: number, promptTokens?: number, completionTokens?: number }) { if (!llm) diff --git a/apps/server/src/routes/tests/v1completions.test.ts b/apps/server/src/routes/openai/v1/route.test.ts similarity index 87% rename from apps/server/src/routes/tests/v1completions.test.ts rename to apps/server/src/routes/openai/v1/route.test.ts index 3f3447547..958f061e7 100644 --- a/apps/server/src/routes/tests/v1completions.test.ts +++ b/apps/server/src/routes/openai/v1/route.test.ts @@ -1,16 +1,16 @@ -import type { MqService } from '../../libs/mq' -import type { BillingEvent } from '../../services/billing/billing-events' -import type { BillingService } from '../../services/billing/billing-service' -import type { ConfigKVService } from '../../services/config-kv' -import type { FluxService } from '../../services/flux' -import type { HonoEnv } from '../../types/hono' +import type { MqService } from '../../../libs/mq' +import type { BillingEvent } from '../../../services/billing/billing-events' +import type { BillingService } from '../../../services/billing/billing-service' +import type { ConfigKVService } from '../../../services/config-kv' +import type { FluxService } from '../../../services/flux' +import type { HonoEnv } from '../../../types/hono' import { Hono } from 'hono' import { afterAll, describe, expect, it, vi } from 'vitest' -import { ApiError } from '../../utils/error' -import { DEFAULT_BILLING_EVENTS_STREAM } from '../../utils/redis-keys' -import { createV1CompletionsRoutes } from '../v1completions' +import { createV1CompletionsRoutes } from '.' +import { ApiError } from '../../../utils/error' +import { DEFAULT_BILLING_EVENTS_STREAM } from '../../../utils/redis-keys' // --- Mock helpers --- @@ -95,7 +95,7 @@ function createTestApp( await next() }) - app.route('/api/v1', routes) + app.route('/api/v1/openai', routes) return app } @@ -110,14 +110,14 @@ describe('v1CompletionsRoutes', () => { globalThis.fetch = originalFetch }) - describe('pOST /api/v1/chat/completions', () => { + describe('pOST /api/v1/openai/chat/completions', () => { it('should return 401 when unauthenticated', async () => { const app = createTestApp( createMockFluxService(), createMockConfigKV(), ) - const res = await app.request('/api/v1/chat/completions', { + const res = await app.request('/api/v1/openai/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'auto', messages: [{ role: 'user', content: 'hi' }] }), @@ -132,7 +132,7 @@ describe('v1CompletionsRoutes', () => { ) const res = await app.fetch( - new Request('http://localhost/api/v1/chat/completions', { + new Request('http://localhost/api/v1/openai/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'auto', messages: [{ role: 'user', content: 'hi' }] }), @@ -155,7 +155,7 @@ describe('v1CompletionsRoutes', () => { const app = createTestApp(fluxService, configKV, billingService) const res = await app.fetch( - new Request('http://localhost/api/v1/chat/completions', { + new Request('http://localhost/api/v1/openai/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'auto', messages: [{ role: 'user', content: 'hi' }] }), @@ -194,7 +194,7 @@ describe('v1CompletionsRoutes', () => { ) await app.fetch( - new Request('http://localhost/api/v1/chat/completions', { + new Request('http://localhost/api/v1/openai/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'auto', messages: [] }), @@ -219,7 +219,7 @@ describe('v1CompletionsRoutes', () => { const app = createTestApp(createMockFluxService(), createMockConfigKV()) await app.fetch( - new Request('http://localhost/api/v1/chat/completions', { + new Request('http://localhost/api/v1/openai/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'openai/gpt-5-mini', messages: [] }), @@ -245,7 +245,7 @@ describe('v1CompletionsRoutes', () => { const app = createTestApp(createMockFluxService(100), createMockConfigKV(), billingService) const res = await app.fetch( - new Request('http://localhost/api/v1/chat/completions', { + new Request('http://localhost/api/v1/openai/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'auto', messages: [] }), @@ -266,7 +266,7 @@ describe('v1CompletionsRoutes', () => { const app = createTestApp(createMockFluxService(), configKV) const res = await app.fetch( - new Request('http://localhost/api/v1/chat/completions', { + new Request('http://localhost/api/v1/openai/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'auto', messages: [] }), @@ -286,7 +286,7 @@ describe('v1CompletionsRoutes', () => { const app = createTestApp(createMockFluxService(), createMockConfigKV(), undefined, billingMq) await app.fetch( - new Request('http://localhost/api/v1/chat/completions', { + new Request('http://localhost/api/v1/openai/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'gpt-4', messages: [] }), @@ -332,7 +332,7 @@ describe('v1CompletionsRoutes', () => { const app = createTestApp(createMockFluxService(100), createMockConfigKV(), billingService, billingMq) const res = await app.fetch( - new Request('http://localhost/api/v1/chat/completions', { + new Request('http://localhost/api/v1/openai/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'auto', stream: true, messages: [{ role: 'user', content: 'hi' }] }), @@ -350,7 +350,7 @@ describe('v1CompletionsRoutes', () => { }) }) - describe.skip('pOST /api/v1/audio/speech', () => { + describe.skip('pOST /api/v1/openai/audio/speech', () => { it('should proxy TTS request to upstream', async () => { const audioData = new Uint8Array([1, 2, 3, 4]) globalThis.fetch = vi.fn(async () => new Response(audioData, { @@ -361,7 +361,7 @@ describe('v1CompletionsRoutes', () => { const app = createTestApp(createMockFluxService(), createMockConfigKV()) const res = await app.fetch( - new Request('http://localhost/api/v1/audio/speech', { + new Request('http://localhost/api/v1/openai/audio/speech', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'tts-1', input: 'hello', voice: 'alloy' }), @@ -377,7 +377,7 @@ describe('v1CompletionsRoutes', () => { }) }) - describe.skip('pOST /api/v1/audio/transcriptions', () => { + describe.skip('pOST /api/v1/openai/audio/transcriptions', () => { it('should proxy transcription request to upstream', async () => { globalThis.fetch = vi.fn(async () => new Response('{"text":"hello"}', { status: 200, @@ -391,7 +391,7 @@ describe('v1CompletionsRoutes', () => { formData.append('model', 'whisper-1') const res = await app.fetch( - new Request('http://localhost/api/v1/audio/transcriptions', { + new Request('http://localhost/api/v1/openai/audio/transcriptions', { method: 'POST', body: formData, }), @@ -407,17 +407,17 @@ describe('v1CompletionsRoutes', () => { }) describe('route matching', () => { - it('gET /api/v1/chat/completions should return 404', async () => { + it('gET /api/v1/openai/chat/completions should return 404', async () => { const app = createTestApp(createMockFluxService(), createMockConfigKV()) const res = await app.fetch( - new Request('http://localhost/api/v1/chat/completions', { method: 'GET' }), + new Request('http://localhost/api/v1/openai/chat/completions', { method: 'GET' }), { user: testUser } as any, ) expect(res.status).toBe(404) }) - it('pOST /api/v1/chat/completion (singular) should also work', async () => { + it('pOST /api/v1/openai/chat/completion (singular) should also work', async () => { globalThis.fetch = vi.fn(async () => new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' }, @@ -426,7 +426,7 @@ describe('v1CompletionsRoutes', () => { const app = createTestApp(createMockFluxService(), createMockConfigKV()) const res = await app.fetch( - new Request('http://localhost/api/v1/chat/completion', { + new Request('http://localhost/api/v1/openai/chat/completion', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'auto', messages: [] }), diff --git a/apps/server/src/routes/providers.ts b/apps/server/src/routes/providers/index.ts similarity index 82% rename from apps/server/src/routes/providers.ts rename to apps/server/src/routes/providers/index.ts index 50af4499b..d6d0b2554 100644 --- a/apps/server/src/routes/providers.ts +++ b/apps/server/src/routes/providers/index.ts @@ -1,12 +1,12 @@ -import type { ProviderService } from '../services/providers' -import type { HonoEnv } from '../types/hono' +import type { ProviderService } from '../../services/providers' +import type { HonoEnv } from '../../types/hono' import { Hono } from 'hono' import { safeParse } from 'valibot' -import { CreateProviderConfigSchema, UpdateProviderConfigSchema } from '../api/providers.schema' -import { authGuard } from '../middlewares/auth' -import { createBadRequestError, createForbiddenError, createNotFoundError } from '../utils/error' +import { authGuard } from '../../middlewares/auth' +import { createBadRequestError, createForbiddenError, createNotFoundError } from '../../utils/error' +import { CreateProviderConfigSchema, UpdateProviderConfigSchema } from './schema' export function createProviderRoutes(providerService: ProviderService) { return new Hono() @@ -55,6 +55,7 @@ export function createProviderRoutes(providerService: ProviderService) { throw createBadRequestError('Invalid Request', 'INVALID_REQUEST', result.issues) } + // TODO: Move ownership checks into the service layer with an actor-aware API such as updateUserConfigByOwner(user.id, id, input). const existing = await providerService.findUserConfigById(id) if (!existing) throw createNotFoundError() @@ -69,6 +70,7 @@ export function createProviderRoutes(providerService: ProviderService) { const user = c.get('user')! const id = c.req.param('id') + // TODO: Move ownership checks into the service layer with an actor-aware API such as deleteUserConfigByOwner(user.id, id). const existing = await providerService.findUserConfigById(id) if (!existing) throw createNotFoundError() diff --git a/apps/server/src/routes/tests/providers.test.ts b/apps/server/src/routes/providers/route.test.ts similarity index 99% rename from apps/server/src/routes/tests/providers.test.ts rename to apps/server/src/routes/providers/route.test.ts index b6c5016d4..480e6a8f0 100644 --- a/apps/server/src/routes/tests/providers.test.ts +++ b/apps/server/src/routes/providers/route.test.ts @@ -4,10 +4,10 @@ import type { HonoEnv } from '../../types/hono' import { Hono } from 'hono' import { beforeAll, describe, expect, it } from 'vitest' +import { createProviderRoutes } from '.' import { mockDB } from '../../libs/mock-db' import { createProviderService } from '../../services/providers' import { ApiError } from '../../utils/error' -import { createProviderRoutes } from '../providers' import * as schema from '../../schemas' diff --git a/apps/server/src/api/providers.schema.ts b/apps/server/src/routes/providers/schema.ts similarity index 78% rename from apps/server/src/api/providers.schema.ts rename to apps/server/src/routes/providers/schema.ts index 7cc9deaf7..6687b34d3 100644 --- a/apps/server/src/api/providers.schema.ts +++ b/apps/server/src/routes/providers/schema.ts @@ -1,7 +1,7 @@ import { createInsertSchema, createSelectSchema } from 'drizzle-valibot' import { boolean, object, optional, record, string } from 'valibot' -import * as schema from '../schemas/providers' +import * as schema from '../../schemas/providers' export const UserProviderConfigSchema = createSelectSchema(schema.userProviderConfigs) export const InsertUserProviderConfigSchema = createInsertSchema(schema.userProviderConfigs) @@ -9,6 +9,8 @@ export const InsertUserProviderConfigSchema = createInsertSchema(schema.userProv export const SystemProviderConfigSchema = createSelectSchema(schema.systemProviderConfigs) export const InsertSystemProviderConfigSchema = createInsertSchema(schema.systemProviderConfigs) +// TODO: Replace these schemas with explicit HTTP request DTOs. +// validated/validationBypassed are server-managed state and should not be client-writable. export const CreateProviderConfigSchema = object({ id: optional(string()), definitionId: string(), @@ -18,6 +20,7 @@ export const CreateProviderConfigSchema = object({ validationBypassed: optional(boolean()), }) +// TODO: Restrict updates to user-editable fields only. export const UpdateProviderConfigSchema = object({ name: optional(string()), config: optional(record(string(), string())), diff --git a/apps/server/src/routes/stripe.ts b/apps/server/src/routes/stripe/index.ts similarity index 94% rename from apps/server/src/routes/stripe.ts rename to apps/server/src/routes/stripe/index.ts index dbcb62df6..88fd1d9d3 100644 --- a/apps/server/src/routes/stripe.ts +++ b/apps/server/src/routes/stripe/index.ts @@ -1,30 +1,27 @@ -import type { Env } from '../libs/env' -import type { RevenueMetrics } from '../libs/otel' -import type { BillingService } from '../services/billing/billing-service' -import type { ConfigKVService } from '../services/config-kv' -import type { FluxService } from '../services/flux' -import type { StripeService } from '../services/stripe' -import type { HonoEnv } from '../types/hono' +import type { Env } from '../../libs/env' +import type { RevenueMetrics } from '../../libs/otel' +import type { BillingService } from '../../services/billing/billing-service' +import type { ConfigKVService } from '../../services/config-kv' +import type { FluxService } from '../../services/flux' +import type { StripeService } from '../../services/stripe' +import type { HonoEnv } from '../../types/hono' import Stripe from 'stripe' import { useLogger } from '@guiiai/logg' import { Hono } from 'hono' -import { integer, minValue, number, object, pipe, safeParse } from 'valibot' +import { safeParse } from 'valibot' -import { authGuard } from '../middlewares/auth' -import { configGuard } from '../middlewares/config-guard' -import { rateLimiter } from '../middlewares/rate-limit' -import { createBadRequestError, createServiceUnavailableError } from '../utils/error' -import { errorMessageFromUnknown } from '../utils/error-message' -import { resolveTrustedRequestOrigin } from '../utils/origin' +import { authGuard } from '../../middlewares/auth' +import { configGuard } from '../../middlewares/config-guard' +import { rateLimiter } from '../../middlewares/rate-limit' +import { createBadRequestError, createServiceUnavailableError } from '../../utils/error' +import { errorMessageFromUnknown } from '../../utils/error-message' +import { resolveTrustedRequestOrigin } from '../../utils/origin' +import { CheckoutBodySchema } from './schema' const logger = useLogger('stripe') -const CheckoutBodySchema = object({ - amount: pipe(number(), integer(), minValue(1)), -}) - export function createStripeRoutes( fluxService: FluxService, stripeService: StripeService, diff --git a/apps/server/src/routes/tests/stripe.test.ts b/apps/server/src/routes/stripe/route.test.ts similarity index 89% rename from apps/server/src/routes/tests/stripe.test.ts rename to apps/server/src/routes/stripe/route.test.ts index 6cd88b5dd..178a05702 100644 --- a/apps/server/src/routes/tests/stripe.test.ts +++ b/apps/server/src/routes/stripe/route.test.ts @@ -8,8 +8,8 @@ import type { HonoEnv } from '../../types/hono' import { Hono } from 'hono' import { describe, expect, it, vi } from 'vitest' +import { createStripeRoutes } from '.' import { ApiError } from '../../utils/error' -import { createStripeRoutes } from '../stripe' // --- Mock helpers --- @@ -148,14 +148,14 @@ function createTestApp( await next() }) - app.route('/api/stripe', routes) + app.route('/api/v1/stripe', routes) return app } // --- Tests --- describe('stripeRoutes', () => { - describe('gET /api/stripe/packages', () => { + describe('gET /api/v1/stripe/packages', () => { it('returns configured packages', async () => { const app = createTestApp( createMockFluxService(), @@ -164,7 +164,7 @@ describe('stripeRoutes', () => { createMockConfigKV(), ) - const res = await app.request('/api/stripe/packages') + const res = await app.request('/api/v1/stripe/packages') expect(res.status).toBe(200) const data = await res.json() @@ -180,13 +180,13 @@ describe('stripeRoutes', () => { configKV, ) - const res = await app.request('/api/stripe/packages') + const res = await app.request('/api/v1/stripe/packages') expect(res.status).toBe(200) expect(await res.json()).toEqual([]) }) }) - describe('pOST /api/stripe/checkout', () => { + describe('pOST /api/v1/stripe/checkout', () => { it('returns 401 when unauthenticated', async () => { const app = createTestApp( createMockFluxService(), @@ -195,7 +195,7 @@ describe('stripeRoutes', () => { createMockConfigKV(), ) - const res = await app.request('/api/stripe/checkout', { + const res = await app.request('/api/v1/stripe/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: 500 }), @@ -212,7 +212,7 @@ describe('stripeRoutes', () => { ) const res = await app.fetch( - new Request('http://localhost/api/stripe/checkout', { + new Request('http://localhost/api/v1/stripe/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: 0 }), @@ -231,7 +231,7 @@ describe('stripeRoutes', () => { ) const res = await app.fetch( - new Request('http://localhost/api/stripe/checkout', { + new Request('http://localhost/api/v1/stripe/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: -100 }), @@ -250,7 +250,7 @@ describe('stripeRoutes', () => { ) const res = await app.fetch( - new Request('http://localhost/api/stripe/checkout', { + new Request('http://localhost/api/v1/stripe/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: 1_000_001 }), @@ -269,7 +269,7 @@ describe('stripeRoutes', () => { ) const res = await app.fetch( - new Request('http://localhost/api/stripe/checkout', { + new Request('http://localhost/api/v1/stripe/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: 501 }), @@ -289,7 +289,7 @@ describe('stripeRoutes', () => { ) const res = await app.fetch( - new Request('http://localhost/api/stripe/checkout', { + new Request('http://localhost/api/v1/stripe/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: 9.99 }), @@ -317,9 +317,9 @@ describe('stripeRoutes', () => { c.set('user', testUser as any) await next() }) - app.route('/api/stripe', routes) + app.route('/api/v1/stripe', routes) - const res = await app.request('/api/stripe/checkout', { + const res = await app.request('/api/v1/stripe/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: 500 }), @@ -328,7 +328,7 @@ describe('stripeRoutes', () => { }) }) - describe('gET /api/stripe/orders', () => { + describe('gET /api/v1/stripe/orders', () => { it('returns 401 when unauthenticated', async () => { const app = createTestApp( createMockFluxService(), @@ -337,7 +337,7 @@ describe('stripeRoutes', () => { createMockConfigKV(), ) - const res = await app.request('/api/stripe/orders') + const res = await app.request('/api/v1/stripe/orders') expect(res.status).toBe(401) }) @@ -357,7 +357,7 @@ describe('stripeRoutes', () => { ) const res = await app.fetch( - new Request('http://localhost/api/stripe/orders'), + new Request('http://localhost/api/v1/stripe/orders'), { user: testUser } as any, ) expect(res.status).toBe(200) @@ -368,7 +368,7 @@ describe('stripeRoutes', () => { }) }) - describe('gET /api/stripe/invoices', () => { + describe('gET /api/v1/stripe/invoices', () => { it('returns 401 when unauthenticated', async () => { const app = createTestApp( createMockFluxService(), @@ -377,7 +377,7 @@ describe('stripeRoutes', () => { createMockConfigKV(), ) - const res = await app.request('/api/stripe/invoices') + const res = await app.request('/api/v1/stripe/invoices') expect(res.status).toBe(401) }) @@ -394,7 +394,7 @@ describe('stripeRoutes', () => { ) const res = await app.fetch( - new Request('http://localhost/api/stripe/invoices'), + new Request('http://localhost/api/v1/stripe/invoices'), { user: testUser } as any, ) expect(res.status).toBe(200) @@ -405,7 +405,7 @@ describe('stripeRoutes', () => { }) }) - describe('pOST /api/stripe/portal', () => { + describe('pOST /api/v1/stripe/portal', () => { it('returns 401 when unauthenticated', async () => { const app = createTestApp( createMockFluxService(), @@ -414,7 +414,7 @@ describe('stripeRoutes', () => { createMockConfigKV(), ) - const res = await app.request('/api/stripe/portal', { method: 'POST' }) + const res = await app.request('/api/v1/stripe/portal', { method: 'POST' }) expect(res.status).toBe(401) }) @@ -430,7 +430,7 @@ describe('stripeRoutes', () => { ) const res = await app.fetch( - new Request('http://localhost/api/stripe/portal', { method: 'POST' }), + new Request('http://localhost/api/v1/stripe/portal', { method: 'POST' }), { user: testUser } as any, ) expect(res.status).toBe(400) @@ -440,7 +440,7 @@ describe('stripeRoutes', () => { }) }) - describe('pOST /api/stripe/webhook', () => { + describe('pOST /api/v1/stripe/webhook', () => { it('returns 400 when signature is missing', async () => { const app = createTestApp( createMockFluxService(), @@ -449,7 +449,7 @@ describe('stripeRoutes', () => { createMockConfigKV(), ) - const res = await app.request('/api/stripe/webhook', { + const res = await app.request('/api/v1/stripe/webhook', { method: 'POST', body: '{}', }) @@ -467,7 +467,7 @@ describe('stripeRoutes', () => { createMockConfigKV(), ) - const res = await app.request('/api/stripe/webhook', { + const res = await app.request('/api/v1/stripe/webhook', { method: 'POST', headers: { 'stripe-signature': 'invalid_sig' }, body: '{}', @@ -492,9 +492,9 @@ describe('stripeRoutes', () => { return c.json({ error: err.errorCode }, err.statusCode) return c.json({ error: 'Internal Server Error' }, 500) }) - app.route('/api/stripe', routes) + app.route('/api/v1/stripe', routes) - const res = await app.request('/api/stripe/webhook', { + const res = await app.request('/api/v1/stripe/webhook', { method: 'POST', headers: { 'stripe-signature': 'test_sig' }, body: '{}', diff --git a/apps/server/src/routes/stripe/schema.ts b/apps/server/src/routes/stripe/schema.ts new file mode 100644 index 000000000..caeb67e8d --- /dev/null +++ b/apps/server/src/routes/stripe/schema.ts @@ -0,0 +1,5 @@ +import { integer, minValue, number, object, pipe } from 'valibot' + +export const CheckoutBodySchema = object({ + amount: pipe(number(), integer(), minValue(1)), +}) diff --git a/apps/server/src/services/characters.ts b/apps/server/src/services/characters.ts index 046be50e4..8aace4b1b 100644 --- a/apps/server/src/services/characters.ts +++ b/apps/server/src/services/characters.ts @@ -191,6 +191,8 @@ export function createCharacterService(db: Database, metrics?: EngagementMetrics }, async update(id: string, data: Partial) { + // TODO: Return a stable single-object response shape for HTTP callers. + // leaking Drizzle returning() arrays across the service boundary makes route contracts drift. const result = await db.update(schema.character) .set({ ...data, updatedAt: new Date() }) .where(and( diff --git a/apps/server/src/services/chats.ts b/apps/server/src/services/chats.ts index 63fc77bea..5c3f5c3af 100644 --- a/apps/server/src/services/chats.ts +++ b/apps/server/src/services/chats.ts @@ -173,6 +173,7 @@ export function createChatService(db: Database, metrics?: EngagementMetrics | nu }, async addMember(userId: string, chatId: string, member: { type: ChatMemberType, userId?: string, characterId?: string }) { + // TODO: Push these invariants up into the HTTP schema and convert failures to API errors instead of generic Error. // Validate that user-type members have a userId and non-user members have a characterId if (member.type === 'user' && !member.userId) { throw new Error('userId is required for user-type members') diff --git a/apps/server/src/utils/tests/origin.test.ts b/apps/server/src/utils/tests/origin.test.ts index 3c9a1cfbc..c180c2861 100644 --- a/apps/server/src/utils/tests/origin.test.ts +++ b/apps/server/src/utils/tests/origin.test.ts @@ -12,7 +12,7 @@ describe('origin utils', () => { }) it('prefers a trusted referer origin', () => { - const request = new Request('http://localhost/api/stripe/checkout', { + const request = new Request('http://localhost/api/v1/stripe/checkout', { headers: { referer: 'https://airi.moeru.ai/settings/flux', origin: 'https://example.com', @@ -23,7 +23,7 @@ describe('origin utils', () => { }) it('falls back to a trusted origin header when referer is missing', () => { - const request = new Request('http://localhost/api/stripe/checkout', { + const request = new Request('http://localhost/api/v1/stripe/checkout', { headers: { origin: 'http://localhost:5173', }, diff --git a/packages/stage-ui/src/libs/providers/providers/official/shared.ts b/packages/stage-ui/src/libs/providers/providers/official/shared.ts index 8cd7a464f..c97926c9e 100644 --- a/packages/stage-ui/src/libs/providers/providers/official/shared.ts +++ b/packages/stage-ui/src/libs/providers/providers/official/shared.ts @@ -14,5 +14,5 @@ export function withCredentials() { } export function createOfficialOpenAIProvider() { - return createOpenAI('', `${SERVER_URL}/api/v1/`) + return createOpenAI('', `${SERVER_URL}/api/v1/openai/`) } diff --git a/packages/stage-ui/src/stores/auth.ts b/packages/stage-ui/src/stores/auth.ts index c1a7b5fcb..8833acbc9 100644 --- a/packages/stage-ui/src/stores/auth.ts +++ b/packages/stage-ui/src/stores/auth.ts @@ -87,7 +87,7 @@ export const useAuthStore = defineStore('auth', () => { const updateCredits = async () => { if (!isAuthenticated.value) return - const res = await client.api.flux.$get() + const res = await client.api.v1.flux.$get() if (res.ok) { const data = await res.json() credits.value = data.flux diff --git a/packages/stage-ui/src/stores/characters.ts b/packages/stage-ui/src/stores/characters.ts index 3d9560242..1d66e48e1 100644 --- a/packages/stage-ui/src/stores/characters.ts +++ b/packages/stage-ui/src/stores/characters.ts @@ -88,7 +88,7 @@ export const useCharacterStore = defineStore('characters', () => { } }, remote: async () => { - const res = await client.api.characters.$get({ + const res = await client.api.v1.characters.$get({ query: { all: String(all) }, }) if (!res.ok) { @@ -118,7 +118,7 @@ export const useCharacterStore = defineStore('characters', () => { return cached }, remote: async () => { - const res = await client.api.characters[':id'].$get({ + const res = await client.api.v1.characters[':id'].$get({ param: { id }, }) if (!res.ok) { @@ -144,7 +144,7 @@ export const useCharacterStore = defineStore('characters', () => { return localCharacter }, remote: async () => { - const res = await client.api.characters.$post({ + const res = await client.api.v1.characters.$post({ json: payload, }) if (!res.ok) { @@ -182,7 +182,7 @@ export const useCharacterStore = defineStore('characters', () => { return character }, remote: async () => { - const res = await (client.api.characters[':id'].$patch)({ + const res = await (client.api.v1.characters[':id'].$patch)({ param: { id }, // @ts-expect-error FIXME: hono client typing misses json option for this route json: payload, @@ -207,7 +207,7 @@ export const useCharacterStore = defineStore('characters', () => { await charactersRepo.remove(id) }, remote: async () => { - const res = await client.api.characters[':id'].$delete({ + const res = await client.api.v1.characters[':id'].$delete({ param: { id }, }) if (!res.ok) { @@ -235,7 +235,7 @@ export const useCharacterStore = defineStore('characters', () => { } }, remote: async () => { - const res = await client.api.characters[':id'].like.$post({ + const res = await client.api.v1.characters[':id'].like.$post({ param: { id }, }) if (!res.ok) { @@ -268,7 +268,7 @@ export const useCharacterStore = defineStore('characters', () => { } }, remote: async () => { - const res = await client.api.characters[':id'].bookmark.$post({ + const res = await client.api.v1.characters[':id'].bookmark.$post({ param: { id }, }) if (!res.ok) { diff --git a/packages/stage-ui/src/stores/provider-catalog.ts b/packages/stage-ui/src/stores/provider-catalog.ts index e81c43ed5..51cc4581a 100644 --- a/packages/stage-ui/src/stores/provider-catalog.ts +++ b/packages/stage-ui/src/stores/provider-catalog.ts @@ -22,7 +22,7 @@ export const useProviderCatalogStore = defineStore('provider-catalog', () => { } }, remote: async () => { - const res = await client.api.providers.$get() + const res = await client.api.v1.providers.$get() if (!res.ok) { throw new Error('Failed to fetch providers') } @@ -68,7 +68,7 @@ export const useProviderCatalogStore = defineStore('provider-catalog', () => { return provider }, remote: async () => { - const res = await client.api.providers.$post({ + const res = await client.api.v1.providers.$post({ json: { id, definitionId, @@ -109,7 +109,7 @@ export const useProviderCatalogStore = defineStore('provider-catalog', () => { await providersRepo.remove(providerId) }, remote: async () => { - const res = await client.api.providers[':id'].$delete({ + const res = await client.api.v1.providers[':id'].$delete({ param: { id: providerId }, }) if (!res.ok) { @@ -134,7 +134,7 @@ export const useProviderCatalogStore = defineStore('provider-catalog', () => { return provider }, remote: async () => { - const res = await client.api.providers[':id'].$patch({ + const res = await client.api.v1.providers[':id'].$patch({ param: { id: providerId }, // @ts-expect-error hono client typing misses json option for this route json: {