Compare commits

..

1 Commits

Author SHA1 Message Date
682df6a573 fix(backend): disable reasoning and fallback to reasoning field
Set reasoning effort to none in the LLM client to reduce latency
and token usage. Fall back to the reasoning field when content is
absent in the response.

Co-authored-by: Ona <no-reply@ona.com>
2026-03-22 22:46:03 +00:00
7 changed files with 20 additions and 150 deletions

View File

@@ -16,9 +16,6 @@ export function createAuth(db: Database) {
provider: "pg", provider: "pg",
schema, schema,
}), }),
advanced: {
disableCSRFCheck: process.env.NODE_ENV !== "production",
},
emailAndPassword: { emailAndPassword: {
enabled: true, enabled: true,
}, },

View File

@@ -1,5 +1,4 @@
import { Hono } from "hono" import { Hono } from "hono"
import { cors } from "hono/cors"
import { registerAdminHttpHandlers } from "./admin/http.ts" import { registerAdminHttpHandlers } from "./admin/http.ts"
import { createRequireAdmin } from "./auth/admin-middleware.ts" import { createRequireAdmin } from "./auth/admin-middleware.ts"
@@ -51,34 +50,6 @@ function main() {
const app = new Hono() const app = new Hono()
const isDev = process.env.NODE_ENV !== "production"
const allowedOrigins = process.env.CORS_ORIGINS?.split(",").map((o) => o.trim()) ?? []
function resolveOrigin(origin: string): string | undefined {
if (isDev) return origin
return allowedOrigins.includes(origin) ? origin : undefined
}
app.use(
"/api/auth/*",
cors({
origin: resolveOrigin,
allowHeaders: ["Content-Type", "Authorization"],
allowMethods: ["POST", "GET", "OPTIONS"],
exposeHeaders: ["Content-Length"],
maxAge: 600,
credentials: true,
}),
)
app.use(
"*",
cors({
origin: resolveOrigin,
credentials: true,
}),
)
app.get("/health", (c) => c.json({ status: "ok" })) app.get("/health", (c) => c.json({ status: "ok" }))
const authSessionMiddleware = createRequireSession(auth) const authSessionMiddleware = createRequireSession(auth)

View File

@@ -5,10 +5,10 @@ import merge from "lodash.merge"
import type { Database } from "../db/index.ts" import type { Database } from "../db/index.ts"
import type { FeedEnhancer } from "../enhancement/enhance-feed.ts" import type { FeedEnhancer } from "../enhancement/enhance-feed.ts"
import type { FeedSourceProvider } from "./feed-source-provider.ts"
import { InvalidSourceConfigError, SourceNotFoundError } from "../sources/errors.ts" import { InvalidSourceConfigError, SourceNotFoundError } from "../sources/errors.ts"
import { sources } from "../sources/user-sources.ts" import { sources } from "../sources/user-sources.ts"
import type { FeedSourceProvider } from "./feed-source-provider.ts"
import { UserSession } from "./user-session.ts" import { UserSession } from "./user-session.ts"
export interface UserSessionManagerConfig { export interface UserSessionManagerConfig {
@@ -125,14 +125,16 @@ export class UserSessionManager {
// read stale config. Use SELECT FOR UPDATE or atomic jsonb merge if // read stale config. Use SELECT FOR UPDATE or atomic jsonb merge if
// this becomes a problem. // this becomes a problem.
let mergedConfig: Record<string, unknown> | undefined let mergedConfig: Record<string, unknown> | undefined
if (update.config !== undefined && provider.configSchema) { if (update.config !== undefined) {
const existing = await sources(this.db, userId).find(sourceId) const existing = await sources(this.db, userId).find(sourceId)
const existingConfig = (existing?.config ?? {}) as Record<string, unknown> const existingConfig = (existing?.config ?? {}) as Record<string, unknown>
mergedConfig = merge({}, existingConfig, update.config) mergedConfig = merge({}, existingConfig, update.config)
const validated = provider.configSchema(mergedConfig) if (provider.configSchema) {
if (validated instanceof type.errors) { const validated = provider.configSchema(mergedConfig)
throw new InvalidSourceConfigError(sourceId, validated.summary) if (validated instanceof type.errors) {
throw new InvalidSourceConfigError(sourceId, validated.summary)
}
} }
} }
@@ -167,24 +169,23 @@ export class UserSessionManager {
async upsertSourceConfig( async upsertSourceConfig(
userId: string, userId: string,
sourceId: string, sourceId: string,
data: { enabled: boolean; config?: unknown }, data: { enabled: boolean; config: unknown },
): Promise<void> { ): Promise<void> {
const provider = this.providers.get(sourceId) const provider = this.providers.get(sourceId)
if (!provider) { if (!provider) {
throw new SourceNotFoundError(sourceId, userId) throw new SourceNotFoundError(sourceId, userId)
} }
if (provider.configSchema && data.config !== undefined) { if (provider.configSchema) {
const validated = provider.configSchema(data.config) const validated = provider.configSchema(data.config)
if (validated instanceof type.errors) { if (validated instanceof type.errors) {
throw new InvalidSourceConfigError(sourceId, validated.summary) throw new InvalidSourceConfigError(sourceId, validated.summary)
} }
} }
const config = data.config ?? {}
await sources(this.db, userId).upsertConfig(sourceId, { await sources(this.db, userId).upsertConfig(sourceId, {
enabled: data.enabled, enabled: data.enabled,
config, config: data.config,
}) })
const session = this.sessions.get(userId) const session = this.sessions.get(userId)
@@ -192,7 +193,7 @@ export class UserSessionManager {
if (!data.enabled) { if (!data.enabled) {
session.removeSource(sourceId) session.removeSource(sourceId)
} else { } else {
const source = await provider.feedSourceForUser(userId, config) const source = await provider.feedSourceForUser(userId, data.config)
if (session.hasSource(sourceId)) { if (session.hasSource(sourceId)) {
session.replaceSource(sourceId, source) session.replaceSource(sourceId, source)
} else { } else {

View File

@@ -287,31 +287,6 @@ describe("PATCH /api/sources/:sourceId", () => {
expect(body.error).toContain("Invalid JSON") expect(body.error).toContain("Invalid JSON")
}) })
test("returns 400 when request body contains unknown fields", async () => {
activeStore = createInMemoryStore()
activeStore.seed(MOCK_USER_ID, "aelis.weather")
const { app } = createApp([createStubProvider("aelis.weather", weatherConfig)], MOCK_USER_ID)
const res = await patch(app, "aelis.weather", {
enabled: true,
unknownField: "hello",
})
expect(res.status).toBe(400)
})
test("returns 400 when weather config contains unknown fields", async () => {
activeStore = createInMemoryStore()
activeStore.seed(MOCK_USER_ID, "aelis.weather")
const { app } = createApp([createStubProvider("aelis.weather", weatherConfig)], MOCK_USER_ID)
const res = await patch(app, "aelis.weather", {
config: { units: "metric", unknownField: "hello" },
})
expect(res.status).toBe(400)
})
test("returns 400 when weather config fails validation", async () => { test("returns 400 when weather config fails validation", async () => {
activeStore = createInMemoryStore() activeStore = createInMemoryStore()
activeStore.seed(MOCK_USER_ID, "aelis.weather") activeStore.seed(MOCK_USER_ID, "aelis.weather")
@@ -435,7 +410,7 @@ describe("PATCH /api/sources/:sourceId", () => {
removeSpy.mockRestore() removeSpy.mockRestore()
}) })
test("returns 400 when config is provided for source without schema", async () => { test("accepts location source with arbitrary config (no schema)", async () => {
activeStore = createInMemoryStore() activeStore = createInMemoryStore()
activeStore.seed(MOCK_USER_ID, "aelis.location") activeStore.seed(MOCK_USER_ID, "aelis.location")
const { app } = createApp([createStubProvider("aelis.location")], MOCK_USER_ID) const { app } = createApp([createStubProvider("aelis.location")], MOCK_USER_ID)
@@ -444,19 +419,7 @@ describe("PATCH /api/sources/:sourceId", () => {
config: { something: "value" }, config: { something: "value" },
}) })
expect(res.status).toBe(400) expect(res.status).toBe(204)
})
test("returns 400 when empty config is provided for source without schema", async () => {
activeStore = createInMemoryStore()
activeStore.seed(MOCK_USER_ID, "aelis.location")
const { app } = createApp([createStubProvider("aelis.location")], MOCK_USER_ID)
const res = await patch(app, "aelis.location", {
config: {},
})
expect(res.status).toBe(400)
}) })
test("updates enabled on location source", async () => { test("updates enabled on location source", async () => {
@@ -530,31 +493,6 @@ describe("PUT /api/sources/:sourceId", () => {
expect(res.status).toBe(400) expect(res.status).toBe(400)
}) })
test("returns 400 when request body contains unknown fields", async () => {
activeStore = createInMemoryStore()
const { app } = createApp([createStubProvider("aelis.weather", weatherConfig)], MOCK_USER_ID)
const res = await put(app, "aelis.weather", {
enabled: true,
config: { units: "metric" },
unknownField: "hello",
})
expect(res.status).toBe(400)
})
test("returns 400 when weather config contains unknown fields", async () => {
activeStore = createInMemoryStore()
const { app } = createApp([createStubProvider("aelis.weather", weatherConfig)], MOCK_USER_ID)
const res = await put(app, "aelis.weather", {
enabled: true,
config: { units: "metric", unknownField: "hello" },
})
expect(res.status).toBe(400)
})
test("returns 400 when config fails schema validation", async () => { test("returns 400 when config fails schema validation", async () => {
activeStore = createInMemoryStore() activeStore = createInMemoryStore()
const { app } = createApp([createStubProvider("aelis.weather", weatherConfig)], MOCK_USER_ID) const { app } = createApp([createStubProvider("aelis.weather", weatherConfig)], MOCK_USER_ID)
@@ -673,7 +611,7 @@ describe("PUT /api/sources/:sourceId", () => {
expect(session.hasSource("aelis.weather")).toBe(true) expect(session.hasSource("aelis.weather")).toBe(true)
}) })
test("returns 400 when config is provided for source without schema", async () => { test("accepts location source with arbitrary config (no schema)", async () => {
activeStore = createInMemoryStore() activeStore = createInMemoryStore()
const { app } = createApp([createStubProvider("aelis.location")], MOCK_USER_ID) const { app } = createApp([createStubProvider("aelis.location")], MOCK_USER_ID)
@@ -682,29 +620,9 @@ describe("PUT /api/sources/:sourceId", () => {
config: { something: "value" }, config: { something: "value" },
}) })
expect(res.status).toBe(400)
})
test("returns 400 when empty config is provided for source without schema", async () => {
activeStore = createInMemoryStore()
const { app } = createApp([createStubProvider("aelis.location")], MOCK_USER_ID)
const res = await put(app, "aelis.location", {
enabled: true,
config: {},
})
expect(res.status).toBe(400)
})
test("returns 204 without config field for source without schema", async () => {
activeStore = createInMemoryStore()
const { app } = createApp([createStubProvider("aelis.location")], MOCK_USER_ID)
const res = await put(app, "aelis.location", {
enabled: true,
})
expect(res.status).toBe(204) expect(res.status).toBe(204)
const row = activeStore.rows.get(`${MOCK_USER_ID}:aelis.location`)
expect(row).toBeDefined()
expect(row!.config).toEqual({ something: "value" })
}) })
}) })

View File

@@ -20,22 +20,15 @@ interface SourcesHttpHandlersDeps {
} }
const UpdateSourceConfigRequestBody = type({ const UpdateSourceConfigRequestBody = type({
"+": "reject",
"enabled?": "boolean", "enabled?": "boolean",
"config?": "unknown", "config?": "unknown",
}) })
const ReplaceSourceConfigRequestBody = type({ const ReplaceSourceConfigRequestBody = type({
"+": "reject",
enabled: "boolean", enabled: "boolean",
config: "unknown", config: "unknown",
}) })
const ReplaceSourceConfigNoConfigRequestBody = type({
"+": "reject",
enabled: "boolean",
})
export function registerSourcesHttpHandlers( export function registerSourcesHttpHandlers(
app: Hono, app: Hono,
{ sessionManager, authSessionMiddleware }: SourcesHttpHandlersDeps, { sessionManager, authSessionMiddleware }: SourcesHttpHandlersDeps,
@@ -97,10 +90,6 @@ async function handleUpdateSource(c: Context<Env>) {
return c.json({ error: parsed.summary }, 400) return c.json({ error: parsed.summary }, 400)
} }
if (!provider.configSchema && "config" in parsed) {
return c.json({ error: `Source "${sourceId}" does not accept config` }, 400)
}
const { enabled, config: newConfig } = parsed const { enabled, config: newConfig } = parsed
const user = c.get("user")! const user = c.get("user")!
@@ -142,16 +131,12 @@ async function handleReplaceSource(c: Context<Env>) {
return c.json({ error: "Invalid JSON" }, 400) return c.json({ error: "Invalid JSON" }, 400)
} }
const schema = provider.configSchema const parsed = ReplaceSourceConfigRequestBody(body)
? ReplaceSourceConfigRequestBody
: ReplaceSourceConfigNoConfigRequestBody
const parsed = schema(body)
if (parsed instanceof type.errors) { if (parsed instanceof type.errors) {
return c.json({ error: parsed.summary }, 400) return c.json({ error: parsed.summary }, 400)
} }
const { enabled } = parsed const { enabled, config } = parsed
const config = "config" in parsed ? parsed.config : undefined
const user = c.get("user")! const user = c.get("user")!
try { try {

View File

@@ -8,7 +8,6 @@ export type TflSourceProviderOptions =
| { apiKey?: never; client: ITflApi } | { apiKey?: never; client: ITflApi }
export const tflConfig = type({ export const tflConfig = type({
"+": "reject",
"lines?": "string[]", "lines?": "string[]",
}) })

View File

@@ -9,7 +9,6 @@ export interface WeatherSourceProviderOptions {
} }
export const weatherConfig = type({ export const weatherConfig = type({
"+": "reject",
"units?": "'metric' | 'imperial'", "units?": "'metric' | 'imperial'",
"hourlyLimit?": "number", "hourlyLimit?": "number",
"dailyLimit?": "number", "dailyLimit?": "number",