Files
cli-e2e-mqo8538j/packages/functions/src/services.ts
2026-06-21 22:13:34 +02:00

100 lines
4.0 KiB
TypeScript

import {
JsonConsoleLogger,
LocalSecretService,
LocalVariablesService,
InMemoryQueueService,
} from '@pikku/core/services'
import { pikkuServices } from '../.pikku/pikku-types.gen.js'
import { TypedSecretService } from '../.pikku/secrets/pikku-secrets.gen.js'
import { TypedVariablesService } from '../.pikku/variables/pikku-variables.gen.js'
import { CFWorkerSchemaService } from '@pikku/schema-cfworker'
import { Kysely, CamelCasePlugin } from 'kysely'
import { LibsqlWebDialect } from '@pikku/kysely-sqlite'
import type { VercelAIAgentRunner } from '@pikku/ai-vercel'
import type { DB } from './types/db.types.js'
export const createSingletonServices = pikkuServices(async (config, existingServices) => {
const variables =
existingServices?.variables ?? new TypedVariablesService(new LocalVariablesService())
const secrets =
existingServices?.secrets ?? new TypedSecretService(new LocalSecretService(variables))
const logger = existingServices?.logger ?? new JsonConsoleLogger()
const schema = existingServices?.schema ?? new CFWorkerSchemaService(logger)
// In CF Workers and containers DATABASE_URL is injected by fabric.
// In local dev (pikku dev) existingServices.kysely is the node:sqlite instance
// set up by the CLI — prefer it over DATABASE_URL so sandbox mode always uses
// the local migrated SQLite rather than the remote Turso URL from the cascade.
// In tests existingServices.kysely is provided by the test support layer.
// node:sqlite / createNodeSqliteKysely is intentionally absent — it is not
// available in CF Workers. See tests/support/services.ts for the dev path.
let kysely: Kysely<DB> | undefined
if (existingServices?.kysely) {
kysely = existingServices.kysely as Kysely<DB>
} else {
const databaseUrl = await variables.get('DATABASE_URL')
if (databaseUrl) {
if (/^postgres(ql)?:\/\//.test(databaseUrl)) {
const [{ PostgresJSDialect }, postgres] = await Promise.all([
import('kysely-postgres-js'),
import('postgres'),
])
kysely = new Kysely<DB>({
dialect: new PostgresJSDialect({
postgres: postgres.default(databaseUrl),
}),
plugins: [new CamelCasePlugin()],
})
} else {
kysely = new Kysely<DB>({
dialect: new LibsqlWebDialect({ url: databaseUrl }),
plugins: [new CamelCasePlugin()],
})
}
} else {
throw new Error(
'kysely service not provided: set DATABASE_URL (Postgres or libsql) or pass kysely via existingServices',
)
}
}
const litellmProxyUrl = process.env.LITELLM_PROXY_URL ?? null
const litellmApiKey = process.env.LITELLM_API_KEY ?? null
let aiAgentRunner: VercelAIAgentRunner | undefined
if (litellmProxyUrl && litellmApiKey) {
// The AI SDKs (~3MB) are stubbed out of non-agent units at bundle time —
// only units with the `ai-model` capability keep them. So import them
// dynamically and guard on the module resolving to a real export; in a
// stubbed unit the import yields `{}` and the runner is simply not built.
const aiVercel = await import('@pikku/ai-vercel')
const aiSdk = await import('@ai-sdk/openai-compatible')
if (aiVercel.VercelAIAgentRunner && aiSdk.createOpenAICompatible) {
const litellmProvider = aiSdk.createOpenAICompatible({
name: 'litellm',
baseURL: litellmProxyUrl,
apiKey: litellmApiKey,
})
aiAgentRunner = new aiVercel.VercelAIAgentRunner({
openai: (modelId: string) => litellmProvider.chatModel(modelId),
anthropic: (modelId: string) => litellmProvider.chatModel(modelId),
google: (modelId: string) => litellmProvider.chatModel(modelId),
deepseek: (modelId: string) => litellmProvider.chatModel(modelId),
})
}
}
const queueService = existingServices?.queueService ?? new InMemoryQueueService()
return {
...(existingServices ?? {}),
config,
variables,
secrets,
logger,
schema,
kysely,
queueService,
...(aiAgentRunner ? { aiAgentRunner } : {}),
}
})