compassmock/src/db/schema-ai-config.ts
Nicholai c53f3a5fac feat(auth): add Anthropic OAuth with PKCE
Browser-based OAuth flow using Anthropic's hosted callback.
Users authorize on claude.ai, paste the code back, and tokens
are encrypted and stored in D1. Includes auto-refresh, Bearer
auth via custom fetch wrapper, and mcp_ tool name prefixing
required by the OAuth endpoint.

Also fixes provider-config save bug that required encryption
key unconditionally — now only checks when API key is present.
2026-02-16 22:05:01 -07:00

111 lines
3.6 KiB
TypeScript
Executable File

import {
sqliteTable,
text,
integer,
} from "drizzle-orm/sqlite-core"
import { users, agentConversations } from "./schema"
// singleton config row (id = "global")
export const agentConfig = sqliteTable("agent_config", {
id: text("id").primaryKey(),
modelId: text("model_id").notNull(),
modelName: text("model_name").notNull(),
provider: text("provider").notNull(),
promptCost: text("prompt_cost").notNull(),
completionCost: text("completion_cost").notNull(),
contextLength: integer("context_length").notNull(),
maxCostPerMillion: text("max_cost_per_million"),
allowUserSelection: integer("allow_user_selection")
.notNull()
.default(1),
updatedBy: text("updated_by")
.notNull()
.references(() => users.id),
updatedAt: text("updated_at").notNull(),
})
// per-user model preference
export const userModelPreference = sqliteTable(
"user_model_preference",
{
userId: text("user_id")
.primaryKey()
.references(() => users.id),
modelId: text("model_id").notNull(),
promptCost: text("prompt_cost").notNull(),
completionCost: text("completion_cost").notNull(),
updatedAt: text("updated_at").notNull(),
}
)
// per-user provider configuration
export const userProviderConfig = sqliteTable(
"user_provider_config",
{
userId: text("user_id")
.primaryKey()
.references(() => users.id),
providerType: text("provider_type").notNull(), // anthropic-oauth | anthropic-key | openrouter | ollama | custom
apiKey: text("api_key"), // encrypted, nullable
baseUrl: text("base_url"), // nullable
modelOverrides: text("model_overrides"), // JSON, nullable
isActive: integer("is_active").notNull().default(1),
updatedAt: text("updated_at").notNull(),
}
)
// one row per streamText invocation
export const agentUsage = sqliteTable("agent_usage", {
id: text("id").primaryKey(),
conversationId: text("conversation_id")
.notNull()
.references(() => agentConversations.id, {
onDelete: "cascade",
}),
userId: text("user_id")
.notNull()
.references(() => users.id),
modelId: text("model_id").notNull(),
promptTokens: integer("prompt_tokens")
.notNull()
.default(0),
completionTokens: integer("completion_tokens")
.notNull()
.default(0),
totalTokens: integer("total_tokens")
.notNull()
.default(0),
estimatedCost: text("estimated_cost").notNull(),
createdAt: text("created_at").notNull(),
})
// per-user Anthropic OAuth tokens (separate from provider config
// because OAuth needs refresh token + expiry tracking)
export const anthropicOauthTokens = sqliteTable(
"anthropic_oauth_tokens",
{
userId: text("user_id")
.primaryKey()
.references(() => users.id, { onDelete: "cascade" }),
accessToken: text("access_token").notNull(),
refreshToken: text("refresh_token").notNull(),
expiresAt: text("expires_at").notNull(),
updatedAt: text("updated_at").notNull(),
}
)
export type AgentConfig = typeof agentConfig.$inferSelect
export type NewAgentConfig = typeof agentConfig.$inferInsert
export type UserProviderConfig = typeof userProviderConfig.$inferSelect
export type NewUserProviderConfig = typeof userProviderConfig.$inferInsert
export type AgentUsage = typeof agentUsage.$inferSelect
export type NewAgentUsage = typeof agentUsage.$inferInsert
export type UserModelPreference =
typeof userModelPreference.$inferSelect
export type NewUserModelPreference =
typeof userModelPreference.$inferInsert
export type AnthropicOauthToken =
typeof anthropicOauthTokens.$inferSelect
export type NewAnthropicOauthToken =
typeof anthropicOauthTokens.$inferInsert