From b2badebad61fa866686d93c5b5f3a16b51758c1c Mon Sep 17 00:00:00 2001 From: Nathan Schneider Date: Sun, 18 Oct 2026 16:51:36 -0600 Subject: [PATCH] =?UTF-8?q?Add=20image=20model=20(regolo-qwen-image)=20to?= =?UTF-8?q?=20modelSpecs,=20enabling=20UI=20image=E2=80=91generation=20but?= =?UTF-8?q?ton?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 10 +++ README.md | 32 +++++++++ librechat.yaml | 165 +++++++++++++++++++++++++++++++++++++++++++ patches/keyBroker.js | 76 ++++++++++++++++++++ 4 files changed, 283 insertions(+) create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 librechat.yaml create mode 100644 patches/keyBroker.js diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4e56ed5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM cloudron/ai.librechat.cloudronapp:202610150224320000 + +# Copy custom configuration and patches into the image. +COPY librechat.yaml /app/data/librechat.yaml +COPY patches/keyBroker.js /app/code/librechat/api/server/controllers/auth/keyBroker.js +COPY patches/oauth.js /app/code/librechat/api/server/controllers/auth/oauth.js + +RUN chmod 644 /app/data/librechat.yaml \ + && chmod 644 /app/code/librechat/api/server/controllers/auth/keyBroker.js \ + && chmod 644 /app/code/librechat/api/server/controllers/auth/oauth.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..c6a5127 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# LibreChat sandbox maintenance + +**Current image** +- Docker image: `cloudron/ai.librechat.cloudronapp:202610150224320000` +- Deployed as app **0752e430‑aa36‑436e‑8668‑ef18da474f47** (LibreChat sandbox at `https://librechat-test.inference.coop`). + +**Automatic updates** +- The "Automatic updates" toggle in the Cloudron UI has been turned **off** (as of 2026‑10‑18). This prevents the sandbox from pulling newer Docker images automatically. + +**Version‑pinning workflow** +1. **Create a version tag** when a new LibreChat release is approved (e.g. + `cloudron/ai.librechat.cloudronapp:202610250100000000`). +2. **Push the tagged image** to the co‑op registry (e.g. `registry.inference.coop/librechat:sandbox‑2026‑10‑25`). + ```bash + docker pull cloudron/ai.librechat.cloudronapp: + docker tag cloudron/ai.librechat.cloudronapp: registry.inference.coop/librechat:sandbox‑ + docker login registry.inference.coop # use registry‑bot credentials + docker push registry.inference.coop/librechat:sandbox‑ + ``` +3. **Redeploy** the sandbox with the pinned tag: + ```bash + cloudron update --app 0752e430-aa36-436e-8668-ef18da474f47 \ + --image registry.inference.coop/librechat:sandbox‑ + ``` +4. **Verify** the app is running (`cloudron status --app `) and that the image field reflects the new tag. +5. **Record** the change in this document (date, tag, reason) and optionally open a GitHub/Gitea issue linking to the upstream LibreChat release. + +**Rollback** +- To roll back, repeat step 3 with the previous image tag. + +**Future maintainers** +- All steps are captured in this doc and in the corresponding issue in `co‑op/support` (Issue #24). The `co‑op/docs` repository should contain this file; keep it up‑to‑date whenever the sandbox image changes or the automatic‑update flag is toggled. diff --git a/librechat.yaml b/librechat.yaml new file mode 100644 index 0000000..81c9044 --- /dev/null +++ b/librechat.yaml @@ -0,0 +1,165 @@ +version: 1.3.0 + +# Voice: server-side STT/TTS via the co-op gateway (Tinfoil TEE). +# Server STT works in every browser (Firefox has no Web Speech API). +speech: + stt: + openai: + url: "https://gateway.inference.coop/v1/audio/transcriptions" + apiKey: "${GATEWAY_KEY}" + model: "tinfoil/whisper-large-v3-turbo" + tts: + openai: + url: "https://gateway.inference.coop/v1/audio/speech" + apiKey: "${GATEWAY_KEY}" + model: "tinfoil/voxtral-tts" + voices: ["neutral_female", "neutral_male", "casual_female", "casual_male", "cheerful_female"] +cache: true + +# Inference Co-op gateway — PER-USER keys (production pattern): no server-side +# apiKey here, so each user pastes their own dashboard key once. Usage then +# bills to that member's own LiteLLM team/budget, exactly like the API path. +endpoints: + custom: + - name: "Inference Co-op" + iconURL: "/assets/logo-coop.png" + apiKey: "user_provided" + baseURL: "https://gateway.inference.coop/v1" + models: + fetch: true + default: + - greenpt/deepseek-v4.1-flash + - tinfoil/deepseek-v4-1-flash + - tinfoil/gpt-oss-120b + - tinfoil/glm-5.3 + - greenpt/glm-5.3-flash + - greenpt/glm-5.3 + - regolo/apertus-70b + - regolo/qwen3.5-122b + titleConvo: true + titleModel: "greenpt/glm-5.3-flash" + titleMethod: "completion" + +# Web search via the co-op's self-hosted SearXNG (public URL, JSON enabled for OWUI already). +# scraperProvider keenable = keyless page-fetch, so members get one-click search +# with no Firecrawl prompt. (Production note: keenable is an external keyless +# service — if that offends self-host values, swap in a self-hosted Firecrawl.) +webSearch: + searchProvider: "searxng" + scraperProvider: "keenable" + searxngInstanceUrl: "${SEARXNG_INSTANCE_URL}" + searxngSearchOptions: + engines: "google,bing,duckduckgo" + language: "en" + timeout: 15000 + +# MCP: the co-op docs, opt-in per conversation (stdlib stdio server in /app/data). +mcpServers: + inference-docs: + type: stdio + command: python3 + args: + - /app/data/inference-docs-mcp/server.py + +# Emoji-ified model picker (same feature-tier convention as models.md / OWUI): +# 🌱 = renewable-powered (GreenPT + Regolo), 🔒 = TEE-architected private (Tinfoil). +# Utility models (audio STT/TTS, image generation) are excluded from chat. +modelSpecs: + enforce: true + prioritize: true + list: + - name: tinfoil-deepseek-v4-1-flash + iconURL: "/assets/logo-coop.png" + label: "🔒 tinfoil/deepseek-v4-1-flash" + softDefault: true + preset: + endpoint: "Inference Co-op" + model: "tinfoil/deepseek-v4-1-flash" + - name: tinfoil-gpt-oss-120b + iconURL: "/assets/logo-coop.png" + label: "🔒 tinfoil/gpt-oss-120b" + preset: + endpoint: "Inference Co-op" + model: "tinfoil/gpt-oss-120b" + - name: tinfoil-glm-5-3 + iconURL: "/assets/logo-coop.png" + label: "🔒 tinfoil/glm-5.3" + preset: + endpoint: "Inference Co-op" + model: "tinfoil/glm-5.3" + - name: greenpt-deepseek-v4-1-flash + iconURL: "/assets/logo-coop.png" + label: "🌱 greenpt/deepseek-v4.1-flash" + preset: + endpoint: "Inference Co-op" + model: "greenpt/deepseek-v4.1-flash" + - name: greenpt-glm-5-3-flash + iconURL: "/assets/logo-coop.png" + label: "🌱 greenpt/glm-5.3-flash" + preset: + endpoint: "Inference Co-op" + model: "greenpt/glm-5.3-flash" + - name: greenpt-glm-5-3 + iconURL: "/assets/logo-coop.png" + label: "🌱 greenpt/glm-5.3" + preset: + endpoint: "Inference Co-op" + model: "greenpt/glm-5.3" + - name: regolo-apertus-70b + iconURL: "/assets/logo-coop.png" + label: "🌱 regolo/apertus-70b" + preset: + endpoint: "Inference Co-op" + model: "regolo/apertus-70b" + - name: regolo-qwen3-5-122b + iconURL: "/assets/logo-coop.png" + label: "🌱 regolo/qwen3.5-122b" + preset: + endpoint: "Inference Co-op" + model: "regolo/qwen3.5-122b" + - name: regolo-qwen-image + iconURL: "/assets/logo-coop.png" + label: "🖼️ regolo/qwen-image" + preset: + endpoint: "Inference Co-op" + model: "regolo/qwen-image" + + + +# Inference Cooperative branding +interface: + customWelcome: "Welcome to your co-op AI." + theme: + version: 1 + name: inference + modes: + light: + colors: + rgb-surface-primary: '250 248 245' + rgb-surface-secondary: '241 237 230' + rgb-surface-tertiary: '232 227 217' + rgb-surface-hover: '236 234 217' + rgb-surface-submit: '91 140 90' + rgb-surface-submit-hover: '74 122 74' + rgb-accent-primary: '91 140 90' + rgb-accent-primary-hover: '74 122 74' + rgb-ring-primary: '91 140 90' + rgb-link: '74 122 74' + rgb-text-primary: '36 48 32' + rgb-text-secondary: '90 107 82' + rgb-border-light: '213 221 208' + dark: + colors: + rgb-surface-primary: '31 36 27' + rgb-surface-secondary: '38 45 34' + rgb-surface-tertiary: '46 54 40' + rgb-surface-hover: '49 57 45' + rgb-surface-submit: '122 184 122' + rgb-surface-submit-hover: '142 200 142' + rgb-accent-primary: '122 184 122' + rgb-accent-primary-hover: '142 200 142' + rgb-ring-primary: '142 200 142' + rgb-link: '142 200 142' + rgb-text-primary: '236 233 226' + rgb-text-secondary: '168 173 158' + rgb-border-light: '74 82 64' diff --git a/patches/keyBroker.js b/patches/keyBroker.js new file mode 100644 index 0000000..079d91a --- /dev/null +++ b/patches/keyBroker.js @@ -0,0 +1,76 @@ +/** + * Key broker sync — automatically provision a member's gateway key at SSO login. + * + * Inference Cooperative patch (MIT, same as LibreChat). When KEY_BROKER_URL is + * set, an OpenID (Cloudron SSO) login fetches-or-creates the member's chat key + * via the member portal's broker and stores it as the user's LibreChat key for + * the configured endpoint — so members never paste a key and all usage bills + * to their own LiteLLM team/budget. + * + * Env: + * KEY_BROKER_URL e.g. https://portal.inference.coop + * KEY_BROKER_SECRET the portal's BROKER_SECRET (x-broker-secret header) + * KEY_BROKER_KEY_NAME portal-side key name, default "chat" + * KEY_BROKER_ENDPOINT LibreChat endpoint (user-key) name, default "Inference Co-op" + * + * Semantics: create-on-first-login. If the member already has a portal-side + * "chat" key but LibreChat lost its stored copy, we deliberately do NOT rotate + * (rotation would silently break the member's own API scripts); they can + * rotate from the dashboard and log in again. + */ +const { updateUserKey, getUserKeyExpiry } = require('~/models'); +const { logger } = require('@librechat/data-schemas'); + +async function syncBrokerUserKey(user) { + const base = process.env.KEY_BROKER_URL; + const secret = process.env.KEY_BROKER_SECRET; + const userId = user?._id?.toString?.() || user?.id; + if (!base || !secret || !userId || !user?.email) return; + const keyName = process.env.KEY_BROKER_KEY_NAME || 'chat'; + const endpointName = process.env.KEY_BROKER_ENDPOINT || 'Inference Co-op'; + const headers = { + 'x-broker-secret': secret, + 'x-member-email': user.email, + }; + + // Does the member already have a portal-side chat key? + const listRes = await fetch(`${base}/broker/keys`, { headers }); + if (!listRes.ok) throw new Error(`broker list ${listRes.status}`); + const list = await listRes.json(); + const exists = (list.keys || []).some((k) => k.name === keyName); + + if (!exists) { + const createRes = await fetch(`${base}/broker/keys`, { + method: 'POST', + headers: { ...headers, 'content-type': 'application/json' }, + body: JSON.stringify({ name: keyName }), + }); + if (!createRes.ok) throw new Error(`broker create ${createRes.status}`); + const created = await createRes.json(); + if (!created.sk_token) throw new Error('broker returned no sk_token'); + // Store the key in LibreChat (JSON string as required by LibreChat storage) + await updateUserKey({ + userId: userId, + name: endpointName, + value: JSON.stringify({ apiKey: created.sk_token }), + expiresAt: null, + }); + logger.info(`[KeyBroker] provisioned "${keyName}" key for ${user.email} (endpoint: ${endpointName})`); + return; + } + + // Portal-side key exists — check whether LibreChat still holds a stored copy. + try { + const stored = await getUserKeyExpiry({ userId: user.id, name: endpointName }); + if (!stored) { + logger.warn( + `[KeyBroker] ${user.email} has a portal "${keyName}" key but no stored copy for endpoint "${endpointName}". ` + + 'Rotate the key on the dashboard and log in again (manual rotation avoids breaking their API scripts).', + ); + } + } catch (e) { + logger.warn(`[KeyBroker] stored-key check failed for ${user.email}: ${e.message}`); + } +} + +module.exports = { syncBrokerUserKey };