How ModelsConfig in pi-web Reads and Writes models.json and Manages Auth Providers
ModelsConfig in pi-web persists user model configurations to ~/.pi/agent/models.json using atomic file operations and synchronizes authentication state via auth.json through the ProviderCredentialStore API.
The pi-web repository implements a robust configuration management system that separates model definitions from authentication credentials. By leveraging atomic file operations in lib/atomic-file.ts, the application prevents data corruption during concurrent read/write cycles. This article examines the exact mechanisms used to persist model configurations and manage OAuth and API-key providers across models.json and auth.json.
Reading Model Configuration from models.json
Atomic File Access in the GET Handler
The read operation originates in app/api/models-config/route.ts, where the GET handler opens ~/.pi/agent/models.json using AtomicFile.read(). This method guarantees a consistent snapshot by preventing reads during active write operations.
// app/api/models-config/route.ts
import { AtomicFile } from '@/lib/atomic-file';
import { NextResponse } from 'next/server';
import { join } from 'path';
import { homedir } from 'os';
const MODELS_PATH = join(homedir(), '.pi', 'agent', 'models.json');
export async function GET() {
try {
const data = await AtomicFile.read(MODELS_PATH);
const modelsConfig = JSON.parse(data);
return NextResponse.json(modelsConfig);
} catch (error) {
return NextResponse.json(
{ error: 'Failed to read configuration' },
{ status: 500 }
);
}
}
The handler returns a ModelsConfig object containing an array of model entries and a map of provider credentials, which the frontend consumes to render the current state.
Writing Configuration with Atomic Updates
Schema Validation and Safe Persistence
When updating configurations via PUT /api/models-config, the system validates the incoming payload against the ModelsConfig schema before calling AtomicFile.write(). This utility writes to a temporary file and renames it atomically, eliminating the risk of partial writes or corruption during the operation.
// app/api/models-config/route.ts
import { AtomicFile } from '@/lib/atomic-file';
import { validateModelsConfig } from '@/lib/validation';
export async function PUT(request: Request) {
try {
const payload = await request.json();
const validatedConfig = validateModelsConfig(payload);
const serialized = JSON.stringify(validatedConfig, null, 2);
await AtomicFile.write(MODELS_PATH, serialized);
return NextResponse.json({ success: true });
} catch (error) {
return NextResponse.json(
{ error: 'Invalid configuration format' },
{ status: 400 }
);
}
}
Managing Authentication Providers
Loading Credentials from auth.json
Authentication data resides in ~/.pi/agent/auth.json and is managed by lib/provider-credential-store.ts. The ProviderCredentialStore.load() method reads this JSON file and builds a memory map keyed by provider ID, supporting both OAuth tokens and API keys.
// lib/provider-credential-store.ts
import { AtomicFile } from './atomic-file';
import { join } from 'path';
import { homedir } from 'os';
const AUTH_PATH = join(homedir(), '.pi', 'agent', 'auth.json');
export interface ProviderCredential {
type: 'oauth' | 'api-key';
value: string;
expiresAt?: number;
}
export class ProviderCredentialStore {
private credentials: Map<string, ProviderCredential> = new Map();
async load(): Promise<void> {
const data = await AtomicFile.read(AUTH_PATH);
const authData = JSON.parse(data);
for (const [providerId, credential] of Object.entries(authData)) {
this.credentials.set(providerId, credential as ProviderCredential);
}
}
get(providerId: string): ProviderCredential | undefined {
return this.credentials.get(providerId);
}
}
Deduplicating Providers in provider-listing.ts
The lib/provider-listing.ts module generates unified provider lists while avoiding duplicate entries. When a provider supports both OAuth and API-key authentication, AuthStorage.listProviders() returns the provider only once, preventing UI clutter while preserving information about available authentication methods.
// lib/provider-listing.ts
import { ProviderCredentialStore } from './provider-credential-store';
interface Provider {
id: string;
name: string;
authenticated: boolean;
supportsOAuth: boolean;
supportsApiKey: boolean;
}
export async function listProviders(): Promise<Provider[]> {
const store = new ProviderCredentialStore();
await store.load();
const providers = await AuthStorage.listProviders();
return providers.map(provider => ({
id: provider.id,
name: provider.name,
authenticated: store.get(provider.id) !== undefined,
supportsOAuth: provider.oauthSupported,
supportsApiKey: provider.apiKeySupported
}));
}
Updating Credentials and State Synchronization
When users modify credentials via /api/auth/login/* or /api/auth/api-key/* routes, the system invokes ProviderCredentialStore.save() or remove(). These methods rewrite auth.json using atomic operations. After any change, the ModelsConfig route re-reads the credentials so the UI reflects the current authentication state.
// lib/provider-credential-store.ts
export class ProviderCredentialStore {
async save(providerId: string, credential: ProviderCredential): Promise<void> {
await this.load();
this.credentials.set(providerId, credential);
await this.persist();
}
async remove(providerId: string): Promise<void> {
this.credentials.delete(providerId);
await this.persist();
}
private async persist(): Promise<void> {
const data = Object.fromEntries(this.credentials);
await AtomicFile.write(AUTH_PATH, JSON.stringify(data, null, 2));
}
}
Frontend Integration
The components/ModelsConfig.tsx React component orchestrates the user interface, calling the API routes to fetch configurations, test model connections, and persist changes. It maintains local state while synchronizing with the atomic file operations occurring server-side in app/api/models-config/route.ts.
Summary
- ModelsConfig stores user configurations in
~/.pi/agent/models.jsonand manages them throughapp/api/models-config/route.tswith GET and PUT handlers. - AtomicFile operations in
lib/atomic-file.tsensure safe concurrent access viaAtomicFile.read()andAtomicFile.write(), which use temporary files and atomic renames. - ProviderCredentialStore in
lib/provider-credential-store.tshandles authentication data in~/.pi/agent/auth.json, supporting both OAuth and API-key methods through a unified interface. - Provider deduplication occurs in
lib/provider-listing.tsthroughAuthStorage.listProviders(), preventing duplicate entries when providers support multiple authentication mechanisms. - All credential modifications route through
/api/auth/login/*or/api/auth/api-key/*endpoints, which trigger atomic writes followed by state synchronization in the ModelsConfig layer.
Frequently Asked Questions
Where does pi-web store model configurations?
Pi-web stores model configurations in ~/.pi/agent/models.json within the user's home directory. This JSON file contains an array of model entries and provider credential references that the ModelsConfig API reads and writes via atomic file operations.
How does ModelsConfig prevent file corruption during concurrent writes?
The system prevents corruption by using AtomicFile.write() from lib/atomic-file.ts, which writes content to a temporary file and then renames it atomically to the target path. This ensures that concurrent readers always see complete, valid data and never encounter partial writes or truncated files.
Can a provider support both OAuth and API key authentication simultaneously?
Yes, providers can implement both methods, but lib/provider-listing.ts deduplicates these entries through AuthStorage.listProviders(). The UI displays each provider once regardless of supported authentication mechanisms, while ProviderCredentialStore tracks the specific credential type and value used for that provider.
How does the UI synchronize when authentication credentials change?
After credential updates via /api/auth/login/* or /api/auth/api-key/* routes, the ModelsConfig endpoint re-reads the authentication state from auth.json. This ensures that when components/ModelsConfig.tsx fetches configuration data, it receives the current authentication status without requiring a page reload or manual refresh.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →