Skip to content

Chapter 18 · Context Serialization and Browser Usage ​

Chapter goals: Master Context JSON serialization/deserialization to实现 conversation history persistence and cross-service migration; understand how to run pi-ai in a browser environment, its limitations, and security boundaries.

18.1 Context Is Plain JSON ​

pi-ai's Context object (systemPrompt + messages) is a plain JSON-serializable structure — no custom codec needed:

typescript
const context: Context = {
  systemPrompt: 'You are a helpful assistant.',
  messages: [
    { role: 'user', content: 'What is TypeScript?', timestamp: Date.now() },
  ],
};

const model = models.getModel('openai', 'gpt-4o-mini')!;
const response = await models.complete(model, context);
context.messages.push(response);

// Serialize the entire context in one line
const serialized = JSON.stringify(context);
// Store in a database, localStorage, a file — your call
localStorage.setItem('conversation', serialized);

18.2 Restoring and Continuing the Conversation ​

Restoration is equally simple — JSON.parse and you can use it directly; you can even switch models and continue:

typescript
// Later: deserialize and continue chatting
const restored: Context = JSON.parse(localStorage.getItem('conversation')!);
restored.messages.push({
  role: 'user',
  content: 'Tell me more about its type system',
  timestamp: Date.now(),
});

// Continue with a different model; context衔接 seamlessly
const newModel = models.getModel('anthropic', 'claude-3-5-haiku-20241022')!;
const continuation = await models.complete(newModel, restored);

These capabilities组合出典型 product features: session share links (put the serialized串 in the URL), customer ticket transfers (hand A's context to B), offline drafts.

18.3 Running in the Browser ​

pi-ai supports the browser environment. Core entry points and provider factories are side-effect-free and can be cleanly bundled into a frontend bundle:

typescript
import { createModels } from '@earendil-works/pi-ai';
import { anthropicProvider } from '@earendil-works/pi-ai/providers/anthropic';

const models = createModels();
models.setProvider(anthropicProvider());

const model = models.getModel('anthropic', 'claude-3-5-haiku-20241022')!;
const response = await models.complete(model, {
  messages: [{ role: 'user', content: 'Hello!', timestamp: Date.now() }],
}, {
  apiKey: 'your-api-key', // No env vars in the browser; pass the key explicitly
});

Key differences between browser and Node:

CapabilityNodeBrowser
Environment variable resolution for API Key✅❌ Must pass key explicitly or inject CredentialStore
Amazon Bedrock (converse-stream)✅❌ Runtime error
OAuth sign-in flow✅❌ Node-only (lazy-loaded, does not pollute bundle)

CredentialStore injection

The document recommends: pass API keys explicitly — or inject a CredentialStore (e.g. localStorage-backed) — to let provider auth resolve automatically from it.

18.4 Security Warning: Do Not Put Production Keys in Frontend Code ​

The官方原话 is very direct:

Security Warning: Exposing API keys in frontend code is dangerous. Anyone can extract and abuse your keys.

The correct approach is a backend proxy: browser → your server (holding the key) → LLM. Frontend direct-connect is only suitable for internal tools or demos.

typescript
// Production architecture recommendation: frontend calls your own backend; key stays on the server
const res = await fetch('/api/chat', {
  method: 'POST',
  body: JSON.stringify({ messages: context.messages }),
});

18.5 Bundling and Tree-shaking ​

Two practical takeaways for bundling:

  1. OAuth lazy loading — registering a provider that supports OAuth does not drag Node-only code into the browser bundle; the login flow is only loaded when actually executed;
  2. On-demand provider imports — import from @earendil-works/pi-ai/providers/xxx sub-paths; unreferenced provider implementations are tree-shaken away, significantly reducing product size.
typescript
// Import only the provider you need; the rest is tree-shaken away
import { openaiProvider } from '@earendil-works/pi-ai/providers/openai';

Chapter Summary ​

  • Context is a plain JSON structure; JSON.stringify/parse completes persistence and restoration;
  • Serialization capability supports typical product features like session sharing and ticket transfer;
  • Browsers are supported but with three limitations: no env vars, Bedrock unavailable, OAuth Node-only;
  • Exposing API keys in frontend code is extremely dangerous; production must go through a backend proxy;
  • Import providers from sub-paths + OAuth lazy loading keeps the bundle clean.

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. What is the standard approach to persisting a pi-ai session context?

2. Regarding API key authentication in the browser environment, which statement is correct?

3. What is the official stance on putting API keys directly in production frontend code?

4. What happens when you注册 an OAuth-capable provider into a browser application?

🛠️ Hands-on Practice ​

  1. Implement an "export conversation as .json file / import and restore" feature as a sub-command of the Chapter 19 CLI.
  2. Set up a minimal React page with Vite and run a complete call on the browser side (demo key).
  3. Write a backend proxy route (Express): receive the frontend message array, inject the key server-side, and forward via pi-ai.

With all the building blocks mastered, the next chapter starts assembling a real application — a mini coding assistant.