import type { Agent } from "agents"; import * as Effect from "effect/Effect"; import type { InstallationConfig } from "../configuration/customer"; import type { Secret } from "../configuration/secrets"; import { verifyAssertion } from "../server/bridge"; import { PROVIDER_PURPOSE } from "../shared/bridge"; import { CREDENTIAL_PROVIDERS, isCredentialProvider, } from "../shared/model-providers"; import { AgentFailure, agentValidation } from "./agent-io"; import { OperationFailure } from "./operation-result"; import { decryptProviderKey, DEFAULT_MODEL, encryptProviderKey, parseModelConfiguration, parseProviderKey, type CredentialFailure, type ModelConfiguration, type ModelSettings, } from "./model-settings"; export class PersonalModels { constructor( private readonly sql: Agent["sql"], private readonly storage: Pick, private readonly installation: () => InstallationConfig, private readonly secret: () => Secret, ) {} initialize() { this.sql`CREATE TABLE IF NOT EXISTS flarebot_model_settings ( singleton INTEGER PRIMARY KEY CHECK (singleton = 1), configuration TEXT NOT NULL, anthropic_key TEXT )`; this.sql`INSERT OR IGNORE INTO flarebot_model_settings (singleton, configuration) VALUES (1, ${JSON.stringify(DEFAULT_MODEL)})`; this.sql`CREATE TABLE IF NOT EXISTS flarebot_provider_credentials ( provider TEXT PRIMARY KEY, encrypted_key TEXT NOT NULL )`; this.sql`CREATE TABLE IF NOT EXISTS flarebot_enabled_providers ( provider TEXT PRIMARY KEY )`; // Move legacy BYOK once, atomically. Clearing the old slot prevents a removed // or replaced key from being resurrected on the next Durable Object start. this.storage.transactionSync(() => { this .sql`INSERT OR IGNORE INTO flarebot_provider_credentials (provider, encrypted_key) SELECT 'anthropic', anthropic_key FROM flarebot_model_settings WHERE singleton = 1 AND anthropic_key IS NOT NULL`; this .sql`UPDATE flarebot_model_settings SET anthropic_key = NULL WHERE singleton = 1`; // Retired credentials must never become credentials for a new provider. this .sql`UPDATE flarebot_model_settings SET configuration = ${JSON.stringify(DEFAULT_MODEL)} WHERE json_extract(configuration, '$.provider') = 'opencode-go'`; this .sql`DELETE FROM flarebot_provider_credentials WHERE provider = 'opencode-go'`; this .sql`DELETE FROM flarebot_enabled_providers WHERE provider = 'opencode-go'`; }); } getModelSettings(): ModelSettings { const row = this.modelSettingsRow(); const installation = this.installation(); const setup = new URL("/connect", installation.controlPlaneOrigin); setup.search = new URLSearchParams({ enableProvider: "openrouter", installationId: installation.installationId, }).toString(); return { configuration: parseModelConfiguration(JSON.parse(row.configuration)), providers: { "workers-ai": true, anthropic: true, openrouter: this.openRouterEnabled(), }, providerSetupUrl: installation.bridge ? setup.href : null, credentials: Object.fromEntries( CREDENTIAL_PROVIDERS.map((provider) => [ provider, this.providerKeyRow(provider) ? "configured" : "missing", ]), ) as ModelSettings["credentials"], }; } updateModelSettings(value: unknown): ModelSettings { const configuration = parseModelConfiguration(value); this.requireEnabledProvider(configuration.provider); this .sql`UPDATE flarebot_model_settings SET configuration = ${JSON.stringify(configuration)} WHERE singleton = 1`; return this.getModelSettings(); } setProviderKey(provider: unknown, value: unknown) { return Effect.gen({ self: this }, function* () { const key = yield* agentValidation(() => parseProviderKey(provider, value), ); if (!isCredentialProvider(provider)) return yield* Effect.fail( new AgentFailure({ message: "Unsupported credential provider" }), ); if (key) yield* agentValidation(() => this.requireEnabledProvider(provider)); const installation = this.installation(); const encrypted = key ? yield* encryptProviderKey( key, this.secret(), installation.installationId, ) : null; if (encrypted) this .sql`INSERT INTO flarebot_provider_credentials (provider, encrypted_key) VALUES (${provider}, ${encrypted}) ON CONFLICT(provider) DO UPDATE SET encrypted_key = excluded.encrypted_key`; else this .sql`DELETE FROM flarebot_provider_credentials WHERE provider = ${provider}`; return this.getModelSettings(); }); } enableOpenRouter(assertion: string) { return Effect.gen({ self: this }, function* () { const installation = this.installation(); if (!installation.bridge) return yield* Effect.fail( new AgentFailure({ message: "Provider setup unavailable" }), ); yield* verifyAssertion(assertion, installation.bridge, { iss: installation.controlPlaneOrigin, aud: installation.runtimeOrigin, sub: installation.ownerSubject, installationId: installation.installationId, purpose: PROVIDER_PURPOSE, state: "openrouter", challenge: "enabled", }); // Idempotent receipt: replay can only reaffirm this same enabled state. this .sql`INSERT OR IGNORE INTO flarebot_enabled_providers (provider) VALUES ('openrouter')`; return { provider: "openrouter", enabled: true }; }); } private openRouterEnabled() { return ( this .sql`SELECT provider FROM flarebot_enabled_providers WHERE provider = 'openrouter'` .length > 0 ); } private requireEnabledProvider(provider: string) { if (provider === "openrouter" && !this.openRouterEnabled()) throw new OperationFailure("provider_disabled"); } private modelSettingsRow() { return this.sql<{ configuration: string; }>`SELECT configuration FROM flarebot_model_settings WHERE singleton = 1`[0]; } private providerKeyRow(provider: string) { return this.sql<{ encrypted_key: string }>`SELECT encrypted_key FROM flarebot_provider_credentials WHERE provider = ${provider}`[0] ?.encrypted_key; } readModelConfiguration( override?: ModelConfiguration, ): Effect.Effect< { configuration: ModelConfiguration; apiKey?: string }, CredentialFailure | AgentFailure > { return Effect.gen({ self: this }, function* () { const row = this.modelSettingsRow(); const configuration = yield* agentValidation(() => { const configuration = parseModelConfiguration( override ?? JSON.parse(row.configuration), ); this.requireEnabledProvider(configuration.provider); return configuration; }); const encrypted = this.providerKeyRow(configuration.provider); if (!isCredentialProvider(configuration.provider) || !encrypted) return { configuration }; const key = yield* decryptProviderKey( encrypted, this.secret(), this.installation().installationId, ); return { configuration, apiKey: key.reveal() }; }); } }