From 8651674cf58faad64413f1c9f740c2de815baa30 Mon Sep 17 00:00:00 2001
From: Martin Dvorak
Date: Wed, 14 Oct 2026 07:09:05 +0000
Subject: [PATCH] First-run gate offers free Ollama vs paid providers; add
Google Gemini
The gate (user-key installs) now headlines Ollama with a "Free" badge and
adds an "or - Use a paid provider instead" option that opens the same
provider rows as Settings, framed as billed by the provider directly.
Saving a provider key there lists its models and switches study
generation to it automatically, so the gate steps aside with no Ollama
key at all.
Gemini joins the registry via its OpenAI-compatible endpoint
(generativelanguage.googleapis.com/v1beta/openai); the privacy policy
names it (en+cs).
Providers suite grows to 20 checks: the gate scenario runs against a
second user-key-mode app instance (free badge, paid option, provider
rows, pass-through, generation switch). All other gates green:
contrast 0 findings, mobile 126/126, chat 102/102, wizard 95/95,
bubbles 34/34, regression 44/44, i18n 13/13, native 33/33.
Version 0.4.9, SW -> freezmo-v73.
---
README.md | 13 ++++++++--
components/ApiKeyGate.tsx | 50 ++++++++++++++++++++++++++++++++++++--
lib/i18n-dict.ts | 10 ++++++++
lib/legal.ts | 4 +--
lib/providers.ts | 8 ++++++
package.json | 2 +-
public/sw.js | 2 +-
tests/providers-test.mjs | 51 +++++++++++++++++++++++++++++++++++++++
8 files changed, 132 insertions(+), 8 deletions(-)
diff --git a/README.md b/README.md
index 9e951a1..ddf1e63 100644
--- a/README.md
+++ b/README.md
@@ -1411,9 +1411,18 @@ Things I chose while building, with the reasoning:
pre-blurred wallpaper — the scene is clearly visible through the glass, and the
pixel-contrast scan still passes.
+82. **First run offers the choice: free Ollama or a paid provider.** The gate (user-key installs)
+ now shows Ollama with a "Free" badge as the headline option, and below it an "or · Use a paid
+ provider instead" button that opens the same provider rows as Settings — with the framing that
+ those are billed by the provider directly (Freezmo never sees a card) and that keys stay in the
+ browser. Saving a provider key on that screen lists its models and switches study generation to
+ it automatically, so the gate steps aside without an Ollama key at all — pinned end to end in
+ the providers suite with a second, user-key-mode app instance.
+
81. **Bring-your-own keys for cloud providers.** Besides the server's Ollama, Settings → Providers takes
- keys for OpenAI, Anthropic (their official OpenAI-compatibility layer), DeepSeek, Groq, OpenRouter
- and any custom OpenAI-compatible endpoint. One adapter (`lib/openai-compat`) covers them all —
+ keys for OpenAI, Anthropic (their official OpenAI-compatibility layer), Google Gemini (its
+ OpenAI-compatible endpoint), DeepSeek, Groq, OpenRouter and any custom OpenAI-compatible
+ endpoint. One adapter (`lib/openai-compat`) covers them all —
model listing, streaming chat, JSON generation and vision — and every model flow can run on a
provider: the chat picker lists each enabled provider's models (labelled, remembered across
visits), and a "Study generation" choice moves decks, homework, grading and attachment
diff --git a/components/ApiKeyGate.tsx b/components/ApiKeyGate.tsx
index fe2f1a7..7dfdb89 100644
--- a/components/ApiKeyGate.tsx
+++ b/components/ApiKeyGate.tsx
@@ -17,6 +17,9 @@ import {
import { Spinner } from "@/components/ui";
import { LOCALES, useI18n } from "@/lib/i18n";
import { verifyOllamaKey } from "@/lib/client-api";
+import { ProvidersSection } from "@/components/ProvidersSection";
+import { PROVIDERS } from "@/lib/providers";
+import { modelsFor, setGeneration, useProviders } from "@/lib/providers-client";
import { saveSettings, settingsStore } from "@/lib/settings";
import { useHealth } from "@/lib/use-health";
import { cn } from "@/lib/utils";
@@ -40,13 +43,26 @@ export function ApiKeyGate({ children }: { children: React.ReactNode }) {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
const { health } = useHealth();
+ const providers = useProviders();
// Server-key and local-daemon installs never need a browser key.
const needsKeyMode = health?.authMode !== "local" && health?.authMode !== "server";
+ // A paid provider is a valid first-run choice too. As soon as a key is saved
+ // the study generation switches to that provider — Ollama stays untouched —
+ // and the gate steps aside (the provider's models were listed on save).
+ const firstEnabled = PROVIDERS.find((provider) => providers.keys[provider.id]);
+ useEffect(() => {
+ if (!firstEnabled || providers.generation) return;
+ const models = modelsFor(providers, firstEnabled.id);
+ if (models.length === 0) return;
+ setGeneration({ provider: firstEnabled.id, textModel: models[0], visionModel: models[0] });
+ }, [firstEnabled, providers]);
+ const providerReady = Boolean(providers.generation && providers.keys[providers.generation.provider]);
+
// Until hydration finishes, render the page as usual so a key holder never
// sees the gate flash. After that, a missing key replaces the app.
- if (!mounted || storedKey || !needsKeyMode) return <>{children}>;
+ if (!mounted || storedKey || providerReady || !needsKeyMode) return <>{children}>;
return ;
}
@@ -85,6 +101,7 @@ function GateScreen() {
const [busy, setBusy] = useState(false);
const [error, setError] = useState(null);
const [helpOpen, setHelpOpen] = useState(false);
+ const [providersOpen, setProvidersOpen] = useState(false);
const [keyboardOpen, setKeyboardOpen] = useState(false);
const inputRef = useRef(null);
const rootRef = useRef(null);
@@ -262,7 +279,14 @@ function GateScreen() {
{t("gate.subtitle")}
-