Electron + Cloud LLM Integration
Architecture
- Main process: Store API key (electron-store + safeStorage), expose get/set via IPC.
- Renderer: Fetch from API directly (add domain to CSP). Never pass raw API key through IPC in logs or errors.
API Key Flow
- User enters key in Settings UI.
- Renderer calls
window.electronAPI.setApiKey(key).
- Main encrypts with
safeStorage.encryptString(), stores in electron-store.
- On load: main decrypts, returns via
getApiKey; renderer keeps in React state for API calls.
OpenAI-Compatible Streaming (Z.AI, OpenAI, etc.)
Endpoint pattern: POST /chat/completions, stream: true, SSE response.
async function* generateStream(apiKey: string, model: string, messages: Message[], signal: AbortSignal) {
const res = await fetch(`${BASE}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({ model, messages, stream: true }),
signal,
})
const reader = res.body?.getReader()
if (!reader) throw new Error('No response body')
const decoder = new TextDecoder()
let buffer = ''
try {
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() || ''
for (const line of lines) {
if (line.trim().startsWith('data: ') && !line.includes('[DONE]')) {
const json = JSON.parse(line.slice(6))
const content = json.choices?.[0]?.delta?.content
if (content) yield content
}
}
}
} finally {
reader.releaseLock()
}
}
Throttle Stream Updates
Avoid re-rendering on every chunk:
const UPDATE_INTERVAL_MS = 50
let pending = ''
let lastUpdate = 0
for await (const chunk of generateStream(...)) {
pending += chunk
const now = Date.now()
if (now - lastUpdate >= UPDATE_INTERVAL_MS) {
lastUpdate = now
setState(prev => ({ ...prev, outputText: pending }))
}
}
setState(prev => ({ ...prev, outputText: pending, isStreaming: false }))
Abort Handling
Pass AbortController.signal to fetch; on user "Stop", call abortController.abort(). Catch AbortError and preserve pending output when stopping.
Z.AI GLM Specifics
- Base URL:
https://api.z.ai/api/paas/v4
- Auth:
Authorization: Bearer <api_key>
- Models:
glm-5, glm-4.7, glm-4.7-flash, glm-4.6, glm-4.5
- Request:
{ model, messages: [{role, content}], stream: true, temperature: 0.7, max_tokens: 4096 }
- No model-list endpoint; use hardcoded list.
CSP
Allow the API domain:
connect-src 'self' https://api.z.ai
1---2name: electron-cloud-llm-integration3description: Integrate cloud LLM APIs (OpenAI-compatible, Z.AI GLM) with Electron apps. Secure API key storage, streaming responses, and throttled renderer updates. Use when adding AI/LLM features to Electron apps or implementing chat completions with streaming.4---56# Electron + Cloud LLM Integration78## Architecture910- **Main process**: Store API key (electron-store + safeStorage), expose get/set via IPC.11- **Renderer**: Fetch from API directly (add domain to CSP). Never pass raw API key through IPC in logs or errors.1213## API Key Flow14151. User enters key in Settings UI.162. Renderer calls `window.electronAPI.setApiKey(key)`.173. Main encrypts with `safeStorage.encryptString()`, stores in electron-store.184. On load: main decrypts, returns via `getApiKey`; renderer keeps in React state for API calls.1920## OpenAI-Compatible Streaming (Z.AI, OpenAI, etc.)2122Endpoint pattern: `POST /chat/completions`, `stream: true`, SSE response.2324```typescript25async function* generateStream(apiKey: string, model: string, messages: Message[], signal: AbortSignal) {26 const res = await fetch(`${BASE}/chat/completions`, {27 method: 'POST',28 headers: {29 'Content-Type': 'application/json',30 'Authorization': `Bearer ${apiKey}`,31 },32 body: JSON.stringify({ model, messages, stream: true }),33 signal,34 })35 const reader = res.body?.getReader()36 if (!reader) throw new Error('No response body')37 const decoder = new TextDecoder()38 let buffer = ''39 try {40 while (true) {41 const { done, value } = await reader.read()42 if (done) break43 buffer += decoder.decode(value, { stream: true })44 const lines = buffer.split('\n')45 buffer = lines.pop() || ''46 for (const line of lines) {47 if (line.trim().startsWith('data: ') && !line.includes('[DONE]')) {48 const json = JSON.parse(line.slice(6))49 const content = json.choices?.[0]?.delta?.content50 if (content) yield content51 }52 }53 }54 } finally {55 reader.releaseLock()56 }57}58```5960## Throttle Stream Updates6162Avoid re-rendering on every chunk:6364```typescript65const UPDATE_INTERVAL_MS = 5066let pending = ''67let lastUpdate = 06869for await (const chunk of generateStream(...)) {70 pending += chunk71 const now = Date.now()72 if (now - lastUpdate >= UPDATE_INTERVAL_MS) {73 lastUpdate = now74 setState(prev => ({ ...prev, outputText: pending }))75 }76}77setState(prev => ({ ...prev, outputText: pending, isStreaming: false }))78```7980## Abort Handling8182Pass `AbortController.signal` to fetch; on user "Stop", call `abortController.abort()`. Catch `AbortError` and preserve `pending` output when stopping.8384## Z.AI GLM Specifics8586- Base URL: `https://api.z.ai/api/paas/v4`87- Auth: `Authorization: Bearer <api_key>`88- Models: `glm-5`, `glm-4.7`, `glm-4.7-flash`, `glm-4.6`, `glm-4.5`89- Request: `{ model, messages: [{role, content}], stream: true, temperature: 0.7, max_tokens: 4096 }`90- No model-list endpoint; use hardcoded list.9192## CSP9394Allow the API domain:9596```97connect-src 'self' https://api.z.ai98```