Przejdź do treści
Biblioteka skilli
AI i agenci · Integracja z narzędziem

glaze-byok-ai

/glaze-byok-ai

Daje użytkownikom aplikacji Glaze wybór silnika AI: wbudowane Glaze AI, własny darmowy klucz API (Google Gemini, OpenRouter) lub lokalne Claude Code CLI, z szyfrowanym przechowywaniem klucza.

Repozytorium
glaze-coder ↗
Licencja
MIT
Ostatnia zmiana repozytorium
14.08.2026

Ocena i bezpieczeństwo

100/ 100Klasa: dobry
Format25 / 25
Frontmatter z nazwą i opisem, poprawna nazwa, opis do 1024 znaków, nazwa zgodna z folderem.
Odkrywalność25 / 25
Opis mówi, kiedy użyć skilla, ma sensowną długość i jest w 3. osobie.
Treść35 / 35
Co najmniej 150 słów i do 500 linii, nagłówki, listy kroków, przykłady albo kod.
Higiena15 / 15
Bez zaślepek typu TODO, bez ścieżek Windows, z licencją.
Ryzykobrak sygnałów w skanie
Pliki w paczce
1
Skrypty
0
Komentarze HTML
0
Adresy URL w SKILL.md
4
Przestarzałość

Brak sygnałów. Repozytorium zmienione 14.08.2026, bez odwołań do wycofanych modeli i API.

Licencja

MIT, otwarta: można używać i zmieniać, z zachowaniem informacji o autorze.

Co robi i kiedy

Daje użytkownikom aplikacji Glaze wybór silnika AI: wbudowane Glaze AI, własny darmowy klucz API (Google Gemini, OpenRouter) lub lokalne Claude Code CLI, z szyfrowanym przechowywaniem klucza.

Oryginalny opis (EN)

Give Glaze app users a choice of AI engine - built-in Glaze AI (zero setup, uses their Glaze credits), their own free API key (Google Gemini free tier, optionally OpenRouter free models), or their local Claude Code CLI (their Claude subscription). Use whenever an app built with glaze-coder gets an AI feature, so end users can decide whether using the app costs them anything. Covers the provider service, encrypted key storage, the Claude CLI engine, IPC handlers, settings UI, and blocked-state fallback.

Jak działa

  1. The four engines
  2. Dependencies
  3. Backend: main/services/ai-provider.ts
  4. The Claude engine: main/services/claude-cli.ts
  5. Wire the app's AI call sites through one entry point
  6. IPC handlers
  7. Settings UI
  8. Verify (no AI calls)
Sekcje z nagłówków SKILL.md, w oryginale · 324 linii, 2 337 słów
Podgląd SKILL.md
---
name: glaze-byok-ai
description: Give Glaze app users a choice of AI engine - built-in Glaze AI (zero setup, uses their Glaze credits), their own free API key (Google Gemini free tier, optionally OpenRouter free models), or their local Claude Code CLI (their Claude subscription). Use whenever an app built with glaze-coder gets an AI feature, so end users can decide whether using the app costs them anything. Covers the provider service, encrypted key storage, the Claude CLI engine, IPC handlers, settings UI, and blocked-state fallback.
---

# User-selectable AI engine for Glaze apps (Glaze AI or BYOK)

Before scaffolding, read the developer's saved policy if present:
`~/.config/glaze-coder/defaults.json` with `{"aiDefault": "glaze" | "byok-fallback"
| "byok-first"}` (written by the Free Coder for Glaze onboarding app).
`byok-fallback` = Glaze default + auto-fallback on (this skill's default);
`byok-first` = scaffold with a free engine as the app's default provider and Glaze
never running unasked; `glaze` = built-in AI only, skip the engine picker. A missing
file means `byok-fallback`.

Default policy for apps built with glaze-coder: when an app calls AI, the end user
gets to pick the engine. Glaze AI works with zero setup but spends the user's Glaze
credits; a bring-your-own-key engine is free for them (Gemini has a real free tier);
the local Claude Code CLI is free for users who already pay for a Claude subscription.
Implement this choice unless the user building the app explicitly wants Glaze-only.

Reference implementation: the News Flow app (`ai-provider.ts` + `claude-cli.ts` +
"AI-motor" settings section + onboarding engine step). This file contains everything
needed to reproduce it in any app.

## The four engines

| Engine | Cost for the user | Setup | Notes |
| --- | --- | --- | --- |
| `glaze` (default) | Glaze credits | none | Always the default and the fallback |
| `gemini` | free | paste a free AI Studio key | ~250 calls/day free tier; recommended free option |
| `openrouter` | free | paste an OpenRouter key | ~50 calls/day unfunded; `openrouter/free` auto-routes; optional third choice |
| `claude` | free with a Claude subscription | Claude Code installed + logged in | For users known to have Claude Code; settings-only, slower (seconds to a minute per call) |

Rules that keep this correct and honest:

- Glaze stays the default. A missing or invalid BYOK key must silently fall back to
  Glaze AI, never break the feature.
- Keep `glaze.capabilities.ai` declared in `package.json` (grades matching the code),
  since Glaze remains default + fallback. Publish rejects AI apps without it.
- Keys are encrypted with `safeStorage`, stored in userData, and never sent to the
  renderer. Only boolean `hasKey` flags cross IPC.
- Key links: Gemini keys at `https://aistudio.google.com/apikey`, OpenRouter keys at
  `https://openrouter.ai/settings/keys`. Open via `shell.openExternal`. Automatic key
  provisioning is not possible; a deep link plus paste field is the simplest flow.
- The "Test" button makes one tiny `generateText` call against the user's own key,
  only when clicked. Never run it (or any AI call) yourself during verification;
  verify with type-check and build only.
- Do not put pricing copy next to AI controls beyond the engine descriptions; Glaze's
  own consent UI explains credits.
- The Claude engine spawns the user's own `claude` CLI (subscription login). Never
  collect Anthropic API keys, OAuth tokens, or Claude credential files for it, and
  never persist `CLAUDE_CODE_OAUTH_TOKEN`. Offer it to audiences known to have Claude
  Code; keep it in full settings, not onboarding. Detection gates the UI state (a
  found/not-found badge), and a missing CLI falls back to Glaze like a missing key.

## Dependencies

From the app's `.glaze-sources/` (bundled Node on PATH, managed npmrc):

```bash
npm install --include=dev @ai-sdk/google @ai-sdk/openai-compatible
```

Both are pure JS and bundle cleanly. The SDK's bundled `ai` package accepts
third-party provider models (it upgrades provider spec v2/v3 internally), so these
plug straight into the `generateObject`/`generateText` re-exported by `@glaze/core/ai`.

## Backend: `main/services/ai-provider.ts`

Adapt storage to the app: if it already has a JSON store helper, reuse it; otherwise
keep the inline `readJson`/`writeJson` below. Everything else drops in as is.

```typescript
// AI engine selection: "glaze" (built-in, credits) | "gemini" | "openrouter" (BYOK).
// Keys are safeStorage-encrypted and never leave the backend.
import { generateText, glaze, type LanguageModel } from "@glaze/core/ai";
import { app, logger, safeStorage } from "@glaze/core/backend";
import { createGoogleGenerativeAI } from "@ai-sdk/google";
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import fs from "fs/promises";
import path from "path";

export type AIProviderId = "glaze" | "gemini" | "openrouter";
export interface AISettings { provider: AIProviderId; openrouterModel: string; }
export interface AISettingsView extends AISettings { hasGeminiKey: boolean; hasOpenrouterKey: boolean; }

export const GEMINI_MODEL = "gemini-2.5-flash";
export const DEFAULT_OPENROUTER_MODEL = "openrouter/free";
const DEFAULTS: AISettings = { provider: "glaze", openrouterModel: DEFAULT_OPENROUTER_MODEL };

async function fileIn(name: string): Promise<string> {
  const dir = app.getPath("userData");
  await fs.mkdir(dir, { recursive: true });
  return path.join(dir, name);
}
async function readJson<T>(name: string, fallback: T): Promise<T> {
  try { return JSON.parse(await fs.readFile(await fileIn(name), "utf-8")) as T; }
  catch { return fallback; }
}
async function writeJson(name: string, value: unknown): Promise<void> {
  await fs.writeFile(await fileIn(name), JSON.stringify(value, null, 2), "utf-8");
}

const SETTINGS_FILE = "ai-settings.json";
const KEYS_FILE = "ai-keys.json"; // base64 of safeStorage-encrypted buffers

export async function getAISettings(): Promise<AISettingsView> {
  const s = { ...DEFAULTS, ...(await readJson<Partial<AISettings>>(SETTINGS_FILE, {})) };
  const keys = await readJson<Partial<Record<AIProviderId, string>>>(KEYS_FILE, {});
  return { ...s, hasGeminiKey: !!keys.gemini, hasOpenrouterKey: !!keys.openrouter };
}

export async function updateAISettings(patch: Partial<AISettings>): Promise<AISettingsView> {
  const current = { ...DEFAULTS, ...(await readJson<Partial<AISettings>>(SETTINGS_FILE, {})) };
  const provider: AIProviderId =
    patch.provider === "glaze" || patch.provider === "gemini" || patch.provider === "openrouter"
      ? patch.provider : current.provider;
  const openrouterModel = patch.openrouterModel?.trim() || current.openrouterModel || DEFAULT_OPENROUTER_MODEL;
  await writeJson(SETTINGS_FILE, { provider, openrouterModel });
  return getAISettings();
}

export async function setProviderKey(provider: AIProviderId, key: string): Promise<AISettingsView> {
  if (provider === "glaze") return getAISettings();
  const keys = await readJson<Partial<Record<AIProviderId, string>>>(KEYS_FILE, {});
  const trimmed = key.trim();
  if (!trimmed) delete keys[provider];
  else keys[provider] = (await safeStorage.encryptString(trimmed)).toString("base64");
  await writeJson(KEYS_FILE, keys);
  return getAISettings();
}

async function getProviderKey(provider: AIProviderId): Promise<string | null> {
  const keys = await readJson<Partial<Record<AIProviderId, string>>>(KEYS_FILE, {});
  const stored = keys[provider];
  if (!stored) return null;
  try { return await safeStorage.decryptString(Buffer.from(stored, "base64")); }
  catch (error) { logger.error("ai-provider", `Could not decrypt ${provider} key`, error); return null; }
}

export interface ResolvedModel { provider: AIProviderId; model: LanguageModel; }

/** Model for the engine the user picked. Missing key = silent fallback to Glaze. */
export async function resolveModel(): Promise<ResolvedModel> {
  const s = { ...DEFAULTS, ...(await readJson<Partial<AISettings>>(SETTINGS_FILE, {})) };
  if (s.provider === "gemini") {
    const apiKey = await getProviderKey("gemini");
    if (apiKey) return { provider: "gemini", model: createGoogleGenerativeAI({ apiKey })(GEMINI_MODEL) };
  } else if (s.provider === "openrouter") {
    const apiKey = await getProviderKey("openrouter");
    if (apiKey) {
      const openrouter = createOpenAICompatible({ name: "openrouter", baseURL: "https://openrouter.ai/api/v1", apiKey });
      return { provider: "openrouter", model: openrouter(s.openrouterModel || DEFAULT_OPENROUTER_MODEL) };
    }
  }
  return { provider: "glaze", model: glaze("fast") };
}

/** Map BYOK errors to blocked states so callers keep their non-AI fallbacks. */
export function byokBlockedState(error: unknown): string {
  const m = error instanceof Error ? `${error.message} ${String((error as { statusCode?: unknown }).statusCode ?? "")}` : String(error);
  if (/\b(401|403)\b|api.?key|unauthorized|forbidden/i.test(m)) return "byok-invalid-key";
  if (/\b429\b|rate.?limit|quota|resource.?exhausted/i.test(m)) return "byok-rate-limit";
  if (/fetch failed|network|enotfound|econnrefused|etimedout|socket/i.test(m)) return "byok-network";
  return "byok-error";
}

/** One tiny live call to confirm the stored key works. Only ever user-triggered. */
export async function testProvider(provider: AIProviderId): Promise<{ ok: boolean; error?: string }> {
  try {
    if (provider === "glaze") return { ok: true };
    const s = { ...DEFAULTS, ...(await readJson<Partial<AISettings>>(SETTINGS_FILE, {})) };
    const apiKey = await getProviderKey(provider);
    if (!apiKey) return { ok: false, error: "No key stored." };
    const model: LanguageModel = provider === "gemini"
      ? createGoogleGenerativeAI({ apiKey })(GEMINI_MODEL)
      : createOpenAICompatible({ name: "openrouter", baseURL: "https://openrouter.ai/api/v1", apiKey })(s.openrouterModel || DEFAULT_OPENROUTER_MODEL);
    const { text } = await generateText({ model, prompt: "Reply with the single word: ok", maxOutputTokens: 20 });
    return text.trim() ? { ok: true } : { ok: false, error: "Empty reply from the model." };
  } catch (error) {
    logger.error("ai-provider", `Key test failed for ${provider}`, error);
    const state = byokBlockedState(error);
    const messages: Record<string, string> = {
      "byok-invalid-key": "The key was rejected. Check that it is correct.",
      "byok-rate-limit": "The key works, but its free quota is used up right now.",
      "byok-network": "Could not reach the service. Check the network.",
    };
    return { ok: false, error: messages[state] ?? "The test failed. Try again." };
  }
}
```

## The Claude engine: `main/services/claude-cli.ts`

Spawns the user's local Claude Code CLI for structured output. Follow the
`glaze-claude-cli` skill for the full binary-resolution, spawn, env-scrubbing, and
JSON-parsing recipes; the engine-specific decisions are:

- Extend the provider id union with `"claude"` and add a `hasClaudeCli` flag to the
  settings view (cached `resolveClaudeBinary()` check: the skill's candidate paths,
  then `zsh -lc "command -v claude"`).
- One call shape: `claudeGenerateJson(prompt, jsonSchema, timeoutMs = 120_000)`.
  Direct spawn with the prompt on stdin and args `-p <instruction> --output-format
  json --json-schema <schema> --tools "" --no-session-persistence`, env scrubbed of
  API-key/gateway vars. Parse `structured_output` from the JSON envelope, falling
  back to `JSON.parse(envelope.result)` with markdown fences stripped.
- GUI-spawned processes cannot read Claude Code's keychain item, so `claude` reports
  "Not logged in" even when Terminal works. On login errors only, retry once through
  a LOGIN pseudo-terminal: `/usr/bin/script -q /dev/null /usr/bin/login -fpq <user>
  /bin/zsh -c '<claude cmd>'`. login(1) creates a real login session where the
  keychain is reachable (works as the same non-root user); script(1) supplies the
  pty login needs. Write the prompt and schema to temp files and inline them with
  `"$(cat '<file>')"` to dodge shell quoting; delete the temp dir afterwards. This
  avoids a node-pty dependency.
- Claude Code can exit 0 with an error envelope (`is_error: true`, result "Not
  logged in - Please run /login"); detect that in the JSON parser and classify it as
  a login error, or it surfaces as a generic parse failure.
- Auto-remediation: when the user-triggered Test hits the not-logged-in state, open
  Terminal running `claude` via osascript so the only remaining step is the
  interactive login itself, and say so in the error message. Never auto-open
  Terminal from background AI calls, and never try to automate the OAuth flow - it
  must stay user-driven.
- Typed failures: `claude-not-installed`, `claude-not-logged-in`, `claude-error`.
- The JSON Schema comes from the app's zod schema via `z.toJSONSchema(schema)` (the
  SDK bundles zod v4; `z` is re-exported by `@glaze/core/ai`). Strip the `$schema`
  key before passing it: zod stamps `draft/2020-12`, which the CLI's validator
  rejects with `--json-schema is not a valid JSON Schema`.
- When spawning `/usr/bin/script`, its stdin must be /dev/null (`stdio: ["ignore",
  ...]` in Node), never a pipe: a Node pipe is a socketpair on macOS and script's
  tcgetattr on a socket is a fatal `Operation not supported on socket`; /dev/null
  gives ENOTTY, which script tolerates.

## Wire the app's AI call sites through one entry point

Give `ai-provider.ts` a single function the rest of the backend uses, so call sites
never branch on engine:

```typescript
export async function generateStructured<T>(schema, prompt): Promise<AIResult<T>> {
  const settings = await loadSettings();
  if (settings.provider === "claude" && (await hasClaudeCli())) {
    try {
      const raw = await claudeGenerateJson(prompt, z.toJSONSchema(schema));
      const parsed = schema.safeParse(raw);
      if (parsed.success) return { ok: true, data: parsed.data };
      return { ok: false, blocked: "claude-error" };
    } catch (error) {
      return { ok: false, blocked: error instanceof ClaudeCliError ? error.state : "claude-error" };
    }
  }
  const { provider, model } = await resolveModel();
  try {
    const { object } = await generateObject({ model, schema, prompt });
    return { ok: true, data: object };
  } catch (error) {
    if (error instanceof GlazeAIError) return { ok: false, blocked: error.state };
    if (provider !== "glaze") return { ok: false, blocked: byokBlockedState(error) };
    throw error;
  }
}
```

Every function that called `glaze("fast")` directly becomes a thin wrapper:
build the prompt, call `generateStructured(schema, prompt)`, transform the data.

### Auto-fallback chain (recommended, on by default)

Add an `autoFallback: boolean` setting (default true) with a checkbox in the engine
UI. When the primary engine returns a blocked result (Glaze credits empty, daily
limit, BYOK quota, Claude not logged in), retry through the user's CONFIGURED free
engines in order Claude CLI -> Gemini -> OpenRouter, skipping the engine that just
ran, and return the first success. If all fail, return the original blocked state so
the app's non-AI fallback and messaging still apply.

The one hard rule: **never enter Glaze via auto-fallback.** Glaze spends the user's
credits; switching into it without an explicit choice is spending their money
unasked. Glaze participates only as the pre-call substitute when the chosen engine
is unconfigured (missing key/CLI), which is the state the UI already explains.

Keep (or add) the app's non-AI fallback for blocked results; with the mapping above
it now covers every engine. Extend the app's blocked-message map with the `byok-*`
and `claude-*` states, in the app's UI language, alongside Glaze's seven states from
`glaze-ai` (example: "byok-invalid-key": "The API key was rejected. Check it in
settings.").

## IPC handlers

```typescript
ipcMain.handle("ai:getSettings", async () => getAISettings());
ipcMain.handle("ai:updateSettings", async (_e, arg) => updateAISettings(arg ?? {}));
ipcMain.handle("ai:setKey", async (_e, arg) => setProviderKey(arg?.provider, arg?.key ?? ""));
ipcMain.handle("ai:testProvider", async (_e, arg) => testProvider(arg?.provider));
```

Validate `arg` with the app's usual guards (only accept known provider ids; an empty
key string clears the stored key). Add matching methods to the renderer's typed IPC
client.

## Settings UI

Add an "AI engine" FieldSet to the app's settings surface (dialog or settings
window), following the app's existing form patterns:

- `RadioGroup`/`RadioGroupItem` from `@glaze/core/components` with one option per
  engine: label + one-line honest cost hint ("No setup. Uses your Glaze credits." /
  "Free with your own Google key, about 250 AI calls per day.").
- When a BYOK engine is selected: a key status `Badge` ("Key stored" / "Missing
  key"), a button that opens the provider's key page via `openExternal`, a
  `type="password"` `Input` for pasting, a "Save key" button, and a "Test" button
  wired to `ai:testProvider`.
- For OpenRouter, an editable model field defaulting to `openrouter/free`.
- A short note when a key is missing: the app quietly falls back to Glaze AI.

If the app has a first-run onboarding flow, offer the engine choice there too (two
choice cards: Glaze AI vs the free option, with the guided key setup inline), and
keep OpenRouter in full settings only. Persist engine settings immediately, not on
wizard completion, so escaping the wizard keeps the choice.

## Verify (no AI calls)

- `npm run type-check` and `npm run build` pass.
- `grep -c "generativelanguage.googleapis.com" ../.glaze/build/main/index.js` and the
  same for `openrouter.ai/api/v1` both return at least 1 (providers really bundled).
- Launch, open settings, confirm the engine section renders; quit. Do not click Test
  and do not trigger AI features.
- Record the engine setup in the app's `.glaze_memory/PROJECT-CONTEXT.md`.

Wersje

W bazie nie ma innych skilli o nazwie glaze-byok-ai.

Migawka bazy z 28.10.2026 · wersja paczki 4a8e2b29e8c0 (skrót SHA-256 całej paczki)

Pobrania w ostatnich 30 dniach: 0 · ranking

Zgłoś problem z tym skillem: szkodliwa treść, błędny opis albo prośba autora o usunięcie.

Podobne skille

  • 100glaze-app-dev
    Buduje, edytuje, uruchamia i debuguje aplikacje desktopowe Glaze (Raycast) z poziomu Claude Code na własnej subskrypcji Claude zamiast płatnych kredytów Glaze.
  • 100glaze
    Stosuje reguły systemu projektowego Glaze przy budowie lub zmianie ekranów, komponentów, formularzy i stylów oraz przy przenoszeniu istniejących ekranów na Glaze.
  • 100openrouter-usage
    Odpytuje API OpenRouter o koszty, kredyty i podział użycia według modelu, dostawcy i daty z ostatnich 30 dni.
  • 100gemini-cli
    Uruchamia Gemini CLI do zapytań AI, porównań Claude z Gemini i delegowania zadań do Gemini.
  • 100openrouter-install-auth
    Konfiguruje uwierzytelnianie API OpenRouter i klucze API przy rozpoczynaniu nowej integracji lub rozwiązywaniu problemów z autoryzacją.
  • 100agent-review
    Zbiera zewnętrzną opinię o kodzie, architekturze lub dowolnej kwestii, kierując modele Anthropic przez Claude Agent SDK, a pozostałe przez API OpenRouter.

Uwagi i zgłoszenia

Wiadomość trafi do Bartosza Sokolińskiego. Adres e-mail jest potrzebny tylko do odpowiedzi. Szczegóły w polityce prywatności.