@r8s/librechat 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.tsx DELETED
@@ -1,384 +0,0 @@
1
- import { jsx, Fragment, useContext, declareOperator } from '@r8s/core'
2
- import { OperatorContext, SecretContext } from '@r8s/core/defaults'
3
- import { operators } from '@r8s/crds'
4
- import { WebService, Endpoint } from '@r8s/recipes'
5
- import { RedisReplicationComponent } from '@r8s/crds/redis'
6
- import type { SecretRef } from '@r8s/recipes'
7
- import type { ConfigMap, EnvVar } from '@r8s/k8s-types'
8
-
9
- export interface MongoConnection {
10
- /** MongoDB host (cluster-internal service or external host) */
11
- host: string
12
- /** MongoDB port (defaults to 27017) */
13
- port?: number
14
- /**
15
- * Database username. Inlined as a plain env var — usernames are
16
- * identifiers, not secrets (same convention as the n8n/outline DB
17
- * recipes). When omitted, the username is read from the password
18
- * secret (key: `username`).
19
- */
20
- username?: string
21
- /**
22
- * Name of an existing Secret holding the MongoDB credentials.
23
- * Keys: `username`, `password`.
24
- */
25
- passwordSecret: string
26
- /**
27
- * authSource query parameter appended to MONGO_URI when set
28
- * (e.g. 'admin' for databases authenticating against the admin db).
29
- */
30
- authSource?: string
31
- }
32
-
33
- export interface LibreChatProps {
34
- /** Resource name (defaults to 'librechat') */
35
- name?: string
36
- /** Kubernetes namespace (defaults to 'default') */
37
- namespace?: string
38
- /** Container image tag (defaults to 'latest' — pin a version in production) */
39
- version?: string
40
- /** Public hostname for the chat UI (required) */
41
- host: string
42
- /** Port the app listens on in-container (defaults to 3080) */
43
- port?: number
44
- /** Number of replicas (defaults to 1) */
45
- replicas?: number
46
- /**
47
- * External MongoDB connection (REQUIRED). LibreChat stores users,
48
- * conversations and messages in MongoDB — this component does NOT
49
- * provision it. Run MongoDB separately (replica-set StatefulSet,
50
- * operator or managed service) and point this prop at it.
51
- */
52
- mongodb: MongoConnection
53
- /** Provision a redis replication group for session caching (default: true) */
54
- cache?: boolean
55
- /**
56
- * Add a Meilisearch sidecar service for full-text / RAG search
57
- * (default: false). MEILI_MASTER_KEY is shared from the app secrets
58
- * bundle (key: meiliMasterKey). Sets SEARCH=true on the app so it
59
- * actually queries the meilisearch instance.
60
- */
61
- search?: boolean
62
- /**
63
- * OIDC SSO client — register LibreChat as a client in Keycloak (the
64
- * Auth recipe) and reference the client secret through the backend.
65
- * Uses the upstream OPENID_* env names; ALLOW_SOCIAL_LOGIN plus
66
- * DOMAIN_SERVER/DOMAIN_CLIENT are set from `host`.
67
- */
68
- sso?: {
69
- issuer: string
70
- clientId: string
71
- clientSecretRef: SecretRef
72
- scopes?: string
73
- }
74
- /** OpenAI-compatible API base URL for model calls (defaults to https://api.berget.ai/v1) */
75
- backend?: string
76
- /**
77
- * Name of an existing Secret holding `secretKey`, `modelApiKey`,
78
- * the multi-user session credentials `jwtSecret`, `jwtRefreshSecret`,
79
- * `credsKey`, `credsIv` — and `meiliMasterKey` when `search` is enabled.
80
- *
81
- * Hex sizing: jwtSecret / jwtRefreshSecret / credsKey are 64 hex chars
82
- * (32 bytes); credsIv is 32 hex chars (16 bytes — AES-IV).
83
- *
84
- * Required unless a secrets backend (openbao/vault) is configured on
85
- * the surrounding Platform — the backend then provisions them.
86
- * Plaintext secrets are not supported.
87
- */
88
- secretsName?: string
89
- /** Requested resources */
90
- resources?: {
91
- requests?: { cpu?: string; memory?: string }
92
- limits?: { cpu?: string; memory?: string }
93
- }
94
- /** TLS configuration (defaults to letsencrypt-prod cluster issuer) */
95
- tls?: {
96
- secretName: string
97
- clusterIssuer: string
98
- }
99
- }
100
-
101
- const OPERATOR_REDIS = 'redis-operator'
102
-
103
- /**
104
- * LibreChat — multi-model AI chat UI with external MongoDB, Redis
105
- * sessions, optional Meilisearch and OIDC SSO.
106
- *
107
- * @title LibreChat
108
- * @category AI & Chat
109
- *
110
- * Composes:
111
- * - LibreChat Deployment + Service + Endpoint (MongoDB is provisioned
112
- * externally — pass its coordinates via the required `mongodb` prop)
113
- * - Redis replication group for session caching (default on, service
114
- * ${name}-redis; USE_REDIS=true points sessions at it)
115
- * - Optional Meilisearch Deployment + Service for message/RAG search
116
- * (SEARCH=true wires the app to it)
117
- * - SECRET_KEY / OPENAI_API_KEY / JWT + CREDS session credentials /
118
- * MEILI_MASTER_KEY provisioned through the Platform secrets backend
119
- * (openbao / vault), or referenced from an existing Secret
120
- * - OIDC SSO against the Keycloak `Auth` recipe
121
- *
122
- * Wrap the component in `<Platform secrets={{ backend: 'openbao' }}>` and
123
- * the app secrets bundle (keys: secretKey, modelApiKey, jwtSecret,
124
- * jwtRefreshSecret, credsKey, credsIv, meiliMasterKey) is provisioned
125
- * for you. Without a backend you must point `secretsName` at a
126
- * pre-created Secret. MongoDB itself is NOT provisioned here — run it
127
- * separately and connect via `mongodb`.
128
- *
129
- * @example
130
- * import { Platform } from '@r8s/recipes'
131
- * import { LibreChat } from '@r8s/librechat'
132
- *
133
- * export default (
134
- * <Platform secrets={{ backend: 'openbao', mount: 'kv', path: 'apps' }}>
135
- * <LibreChat
136
- * name="chat"
137
- * host="chat.example.com"
138
- * mongodb={{ host: 'mongo.data.svc.cluster.local', passwordSecret: 'chat-mongodb-credentials' }}
139
- * sso={{
140
- * issuer: 'https://keycloak.example.com/realms/platform',
141
- * clientId: 'librechat',
142
- * clientSecretRef: { secret: 'librechat-sso', key: 'clientSecret' },
143
- * }}
144
- * />
145
- * </Platform>
146
- * )
147
- */
148
- export function LibreChat(props: LibreChatProps) {
149
- const {
150
- name = 'librechat',
151
- namespace = 'default',
152
- version = 'latest',
153
- host,
154
- port = 3080,
155
- replicas = 1,
156
- mongodb,
157
- cache = true,
158
- search = false,
159
- sso,
160
- backend = 'https://api.berget.ai/v1',
161
- secretsName,
162
- resources = {
163
- requests: { memory: '512Mi', cpu: '250m' },
164
- limits: { memory: '2Gi', cpu: '1000m' },
165
- },
166
- tls = { secretName: `${name}-tls`, clusterIssuer: 'letsencrypt-prod' },
167
- } = props
168
-
169
- const sharedOperators = useContext(OperatorContext)
170
- const secretProvider = useContext(SecretContext)
171
- const resources_: ReturnType<typeof jsx>[] = []
172
-
173
- const appSecretsName = secretsName ?? `${name}-secrets`
174
-
175
- // --- Secret provisioning ---------------------------------------------------
176
- // SECRET_KEY signs sessions, modelApiKey pays for inference, the JWT +
177
- // CREDS keys carry multi-user login state and meiliMasterKey guards the
178
- // search index — none of them may be rendered as plaintext. With a
179
- // secrets backend the bundle (keys: secretKey, modelApiKey, jwtSecret,
180
- // jwtRefreshSecret, credsKey, credsIv, meiliMasterKey) is provisioned
181
- // through the backend; otherwise reference a pre-created Secret.
182
- if (!secretsName) {
183
- if (
184
- !secretProvider ||
185
- (secretProvider.backend !== 'vault' && secretProvider.backend !== 'openbao')
186
- ) {
187
- throw new Error(
188
- `LibreChat "${name}" requires application secrets (SECRET_KEY, modelApiKey).\n` +
189
- `\n` +
190
- `These must not be rendered as plaintext.\n` +
191
- `\n` +
192
- `Fix: configure a secrets backend on the Platform:\n` +
193
- ` <Platform secrets={{ backend: 'openbao', mount: 'kv', path: 'apps' }}>\n` +
194
- ` <LibreChat name="${name}" host="${host}" />\n` +
195
- ` </Platform>\n` +
196
- `\n` +
197
- `Or reference a pre-created Secret (keys: secretKey, modelApiKey, jwtSecret, jwtRefreshSecret, credsKey, credsIv` +
198
- `${search ? ', meiliMasterKey' : ''}):\n` +
199
- ` <LibreChat name="${name}" host="${host}" secretsName="${name}-secrets" />`
200
- )
201
- }
202
-
203
- const spec = {
204
- ...(secretProvider.backend === 'vault'
205
- ? { vaultAuthRef: secretProvider.authRef }
206
- : { openbaoAuthRef: secretProvider.authRef }),
207
- mount: secretProvider.mount,
208
- type: 'kv-v2' as const,
209
- path: `${secretProvider.path ?? name}/${name}/secrets`,
210
- destination: { create: true, name: appSecretsName },
211
- }
212
- resources_.push(
213
- secretProvider.backend === 'vault'
214
- ? jsx('VaultStaticSecret', {
215
- apiVersion: 'secrets.hashicorp.com/v1beta1',
216
- kind: 'VaultStaticSecret',
217
- metadata: { name: `${name}-secrets`, namespace },
218
- spec,
219
- })
220
- : jsx('OpenBaoStaticSecret', {
221
- apiVersion: 'secrets.openbao.org/v1beta1',
222
- kind: 'OpenBaoStaticSecret',
223
- metadata: { name: `${name}-secrets`, namespace },
224
- spec,
225
- })
226
- )
227
- }
228
-
229
- // --- Operators --------------------------------------------------------------
230
- if (cache && !sharedOperators.some((op) => op.name === OPERATOR_REDIS)) {
231
- resources_.push(declareOperator(operators[OPERATOR_REDIS]()))
232
- }
233
-
234
- // Redis — session cache (OT-Container-Kit operator). A replication group
235
- // so the single ${name}-redis master service fronts a 3-node set.
236
- if (cache) {
237
- resources_.push(
238
- RedisReplicationComponent({
239
- metadata: { name: `${name}-redis`, namespace },
240
- spec: {
241
- clusterSize: 3,
242
- kubernetesConfig: { image: 'redis:7.2-alpine' },
243
- },
244
- })
245
- )
246
- }
247
-
248
- // --- Env wiring ----------------------------------------------------------------
249
- // Every credential is referenced via secretKeyRef — no plaintext and no
250
- // redundant $(VAR) self-echoes in the manifest. The WebService declares
251
- // secret-backed vars before plain env vars, so dependent expansion on
252
- // MONGO_URI resolves.
253
- const mongoPort = mongodb.port ?? 27017
254
-
255
- const env: Record<string, string> = {
256
- HOST: '0.0.0.0',
257
- PORT: String(port),
258
- // Deterministic parts inlined; credentials arrive via MONGO_USERNAME /
259
- // MONGO_PASSWORD declared earlier through secretKeyRef
260
- MONGO_URI:
261
- `mongodb://$(MONGO_USERNAME):$(MONGO_PASSWORD)@${mongodb.host}:${mongoPort}/${name}` +
262
- (mongodb.authSource ? `?authSource=${mongodb.authSource}` : ''),
263
- // LibreChat speaks to OpenAI-compatible backends through its reverse
264
- // proxy (there is no OPENAI_API_BASE_URL env upstream)
265
- OPENAI_REVERSE_PROXY: `${backend}/chat/completions`,
266
- ...(cache ? { USE_REDIS: 'true', REDIS_URI: `redis://${name}-redis:6379` } : {}),
267
- ...(search
268
- ? {
269
- SEARCH: 'true',
270
- MEILI_HOST: `http://${name}-meilisearch:7700`,
271
- MEILI_NO_SYNC: 'false',
272
- }
273
- : {}),
274
- ...(sso
275
- ? {
276
- ALLOW_SOCIAL_LOGIN: 'true',
277
- DOMAIN_SERVER: `https://${host}`,
278
- DOMAIN_CLIENT: `https://${host}`,
279
- OPENID_ISSUER: sso.issuer,
280
- OPENID_CLIENT_ID: sso.clientId,
281
- OPENID_SCOPES: sso.scopes ?? 'openid profile email',
282
- OPENID_CALLBACK_URL: `https://${host}/oauth/openid/callback`,
283
- }
284
- : {}),
285
- }
286
-
287
- // The username is an identifier, not a secret — when the caller supplies
288
- // it explicitly it is inlined as plain env; otherwise it is read from the
289
- // MongoDB credentials secret (key: username) like the password.
290
- if (mongodb.username) {
291
- env.MONGO_USERNAME = mongodb.username
292
- }
293
-
294
- // Credentials delivered via secretKeyRef (runtime injection). JWT_SECRET /
295
- // JWT_REFRESH_SECRET / CREDS_KEY / CREDS_IV back multi-user sessions:
296
- // per-user message encryption and refresh-token issuance need shared
297
- // random values across replicas. Hex sizing: jwtSecret, jwtRefreshSecret
298
- // and credsKey are 64 hex chars (32 bytes); credsIv is 32 hex chars
299
- // (16 bytes).
300
- const secrets: Record<string, SecretRef | string> = {
301
- ...(mongodb.username
302
- ? {}
303
- : { MONGO_USERNAME: { secret: mongodb.passwordSecret, key: 'username' } }),
304
- MONGO_PASSWORD: { secret: mongodb.passwordSecret, key: 'password' },
305
- SECRET_KEY: { secret: appSecretsName, key: 'secretKey' },
306
- OPENAI_API_KEY: { secret: appSecretsName, key: 'modelApiKey' },
307
- JWT_SECRET: { secret: appSecretsName, key: 'jwtSecret' },
308
- JWT_REFRESH_SECRET: { secret: appSecretsName, key: 'jwtRefreshSecret' },
309
- CREDS_KEY: { secret: appSecretsName, key: 'credsKey' },
310
- CREDS_IV: { secret: appSecretsName, key: 'credsIv' },
311
- ...(search ? { MEILI_MASTER_KEY: { secret: appSecretsName, key: 'meiliMasterKey' } } : {}),
312
- ...(sso ? { OPENID_CLIENT_SECRET: sso.clientSecretRef } : {}),
313
- }
314
-
315
- // --- Runtime config (non-secret) -------------------------------------------
316
- // REFRESH_TOKEN_EXPIRY is a plain TTL (7 days), not a credential — it is
317
- // rendered as a ConfigMap and injected via configMapKeyRef, keeping
318
- // plaintext env wiring (and the no-plaintext-secrets guardrail) clean.
319
- const configMapName = `${name}-config`
320
- const configMap: ConfigMap = {
321
- apiVersion: 'v1',
322
- kind: 'ConfigMap',
323
- metadata: { name: configMapName, namespace },
324
- data: { REFRESH_TOKEN_EXPIRY: '604800' },
325
- }
326
- resources_.push(jsx('ConfigMap', configMap))
327
-
328
- // --- App + optional Meilisearch + endpoint --------------------------------------
329
- resources_.push(
330
- <WebService
331
- name={name}
332
- namespace={namespace}
333
- image={`ghcr.io/danny-avila/librechat:${version}`}
334
- port={port}
335
- replicas={replicas}
336
- resources={resources}
337
- env={env}
338
- secrets={secrets}
339
- rawEnv={[
340
- {
341
- name: 'REFRESH_TOKEN_EXPIRY',
342
- valueFrom: { configMapKeyRef: { name: configMapName, key: 'REFRESH_TOKEN_EXPIRY' } },
343
- } as EnvVar,
344
- ]}
345
- />
346
- )
347
-
348
- if (search) {
349
- resources_.push(
350
- <WebService
351
- name={`${name}-meilisearch`}
352
- namespace={namespace}
353
- image="getmeili/meilisearch:v1.6"
354
- port={7700}
355
- replicas={1}
356
- env={{ MEILI_NO_ANALYTICS: 'true', MEILI_ENV: 'production' }}
357
- secrets={{ MEILI_MASTER_KEY: { secret: appSecretsName, key: 'meiliMasterKey' } }}
358
- probes={{ liveness: { path: '/health' }, readiness: { path: '/health' } }}
359
- resources={{
360
- requests: { memory: '256Mi', cpu: '100m' },
361
- limits: { memory: '1Gi', cpu: '500m' },
362
- }}
363
- />
364
- )
365
- }
366
-
367
- resources_.push(
368
- <Endpoint
369
- name={`${name}-endpoint`}
370
- namespace={namespace}
371
- host={host}
372
- serviceName={name}
373
- servicePort={port}
374
- tls={tls}
375
- annotations={{
376
- 'nginx.ingress.kubernetes.io/proxy-read-timeout': '300',
377
- 'nginx.ingress.kubernetes.io/proxy-send-timeout': '300',
378
- 'nginx.ingress.kubernetes.io/proxy-buffering': 'off',
379
- }}
380
- />
381
- )
382
-
383
- return jsx(Fragment, { children: resources_ })
384
- }
package/tsconfig.json DELETED
@@ -1,15 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "./dist",
5
- "rootDir": "./src",
6
- "composite": true
7
- },
8
- "include": ["src/**/*"],
9
- "references": [
10
- { "path": "../core" },
11
- { "path": "../k8s-types" },
12
- { "path": "../crds" },
13
- { "path": "../recipes" }
14
- ]
15
- }