refactor(core): move database schema ownership (#29068)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
Dax
2026-05-30 21:08:38 -04:00
committed by GitHub
co-authored by copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
parent 6bcb9cb9bb
commit 7f571d36ea
390 changed files with 11127 additions and 9164 deletions
+11 -11
View File
@@ -4,7 +4,7 @@ import { Auth } from "@/auth"
import { InstanceState } from "@/effect/instance-state"
import { optionalOmitUndefined } from "@opencode-ai/core/schema"
import { Plugin } from "../plugin"
import { ProviderID } from "./schema"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { Array as Arr, Effect, Layer, Record, Result, Context, Schema } from "effect"
const When = Schema.Struct({
@@ -65,11 +65,11 @@ export const CallbackInput = Schema.Struct({
export type CallbackInput = Schema.Schema.Type<typeof CallbackInput>
export class OauthMissing extends Schema.TaggedErrorClass<OauthMissing>()("ProviderAuthOauthMissing", {
providerID: ProviderID,
providerID: ProviderV2.ID,
}) {}
export class OauthCodeMissing extends Schema.TaggedErrorClass<OauthCodeMissing>()("ProviderAuthOauthCodeMissing", {
providerID: ProviderID,
providerID: ProviderV2.ID,
}) {}
export class OauthCallbackFailed extends Schema.TaggedErrorClass<OauthCallbackFailed>()(
@@ -90,15 +90,15 @@ export interface Interface {
readonly methods: () => Effect.Effect<Methods>
readonly authorize: (
input: {
providerID: ProviderID
providerID: ProviderV2.ID
} & AuthorizeInput,
) => Effect.Effect<Authorization | undefined, Error>
readonly callback: (input: { providerID: ProviderID } & CallbackInput) => Effect.Effect<void, Error>
readonly callback: (input: { providerID: ProviderV2.ID } & CallbackInput) => Effect.Effect<void, Error>
}
interface State {
hooks: Record<ProviderID, Hook>
pending: Map<ProviderID, AuthOAuthResult>
hooks: Record<ProviderV2.ID, Hook>
pending: Map<ProviderV2.ID, AuthOAuthResult>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/ProviderAuth") {}
@@ -117,11 +117,11 @@ export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> =
hooks: Record.fromEntries(
Arr.filterMap(plugins, (x) =>
x.auth?.provider !== undefined
? Result.succeed([ProviderID.make(x.auth.provider), x.auth] as const)
? Result.succeed([ProviderV2.ID.make(x.auth.provider), x.auth] as const)
: Result.failVoid,
),
),
pending: new Map<ProviderID, AuthOAuthResult>(),
pending: new Map<ProviderV2.ID, AuthOAuthResult>(),
}
}),
)
@@ -160,7 +160,7 @@ export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> =
})
const authorize = Effect.fn("ProviderAuth.authorize")(function* (
input: { providerID: ProviderID } & AuthorizeInput,
input: { providerID: ProviderV2.ID } & AuthorizeInput,
) {
const { hooks, pending } = yield* InstanceState.get(state)
const method = hooks[input.providerID].methods[input.method]
@@ -184,7 +184,7 @@ export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> =
}
})
const callback = Effect.fn("ProviderAuth.callback")(function* (input: { providerID: ProviderID } & CallbackInput) {
const callback = Effect.fn("ProviderAuth.callback")(function* (input: { providerID: ProviderV2.ID } & CallbackInput) {
const pending = (yield* InstanceState.get(state)).pending
const match = pending.get(input.providerID)
if (!match) return yield* new OauthMissing({ providerID: input.providerID })
+3 -3
View File
@@ -1,7 +1,7 @@
import { APICallError } from "ai"
import { STATUS_CODES } from "http"
import { iife } from "@/util/iife"
import type { ProviderID } from "./schema"
import type { ProviderV2 } from "@opencode-ai/core/provider"
export class HeaderTimeoutError extends Error {
public override readonly name = "ProviderHeaderTimeoutError"
@@ -61,7 +61,7 @@ function isOverflow(message: string) {
return /^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message)
}
function message(providerID: ProviderID, e: APICallError) {
function message(providerID: ProviderV2.ID, e: APICallError) {
return iife(() => {
const msg = e.message
if (msg === "") {
@@ -194,7 +194,7 @@ export type ParsedAPICallError =
metadata?: Record<string, string>
}
export function parseAPICallError(input: { providerID: ProviderID; error: APICallError }): ParsedAPICallError {
export function parseAPICallError(input: { providerID: ProviderV2.ID; error: APICallError }): ParsedAPICallError {
const m = message(input.providerID, input.error)
const body = json(input.error.responseBody)
if (isOverflow(m) || input.error.statusCode === 413 || body?.error?.code === "context_length_exceeded") {
+53 -53
View File
@@ -25,7 +25,7 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { isRecord } from "@/util/record"
import { optionalOmitUndefined } from "@opencode-ai/core/schema"
import * as ProviderTransform from "./transform"
import { ModelID, ProviderID } from "./schema"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelStatus } from "./model-status"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { ProviderError } from "./error"
@@ -663,8 +663,8 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
for (const m of result.models) {
if (!input.models[m.id]) {
models[m.id] = {
id: ModelID.make(m.id),
providerID: ProviderID.make("gitlab"),
id: ProviderV2.ModelID.make(m.id),
providerID: ProviderV2.ID.make("gitlab"),
name: `Agent Platform (${m.name})`,
family: "",
api: {
@@ -928,8 +928,8 @@ const ProviderLimit = Schema.Struct({
})
export const Model = Schema.Struct({
id: ModelID,
providerID: ProviderID,
id: ProviderV2.ModelID,
providerID: ProviderV2.ID,
api: ProviderApiInfo,
name: Schema.String,
family: optionalOmitUndefined(Schema.String),
@@ -945,7 +945,7 @@ export const Model = Schema.Struct({
export type Model = Types.DeepMutable<Schema.Schema.Type<typeof Model>>
export const Info = Schema.Struct({
id: ProviderID,
id: ProviderV2.ID,
name: Schema.String,
source: Schema.Literals(["env", "config", "custom", "api"]),
env: Schema.Array(Schema.String),
@@ -985,8 +985,8 @@ export function defaultModelIDs<T extends { models: Record<string, { id: string
}
export class ModelNotFoundError extends Schema.TaggedErrorClass<ModelNotFoundError>()("ProviderModelNotFoundError", {
providerID: ProviderID,
modelID: ModelID,
providerID: ProviderV2.ID,
modelID: ProviderV2.ModelID,
suggestions: Schema.optional(Schema.Array(Schema.String)),
cause: Schema.optional(Schema.Defect),
}) {
@@ -996,7 +996,7 @@ export class ModelNotFoundError extends Schema.TaggedErrorClass<ModelNotFoundErr
}
export class InitError extends Schema.TaggedErrorClass<InitError>()("ProviderInitError", {
providerID: ProviderID,
providerID: ProviderV2.ID,
cause: Schema.optional(Schema.Defect),
}) {
static isInstance(input: unknown): input is InitError {
@@ -1011,7 +1011,7 @@ export class NoProvidersError extends Schema.TaggedErrorClass<NoProvidersError>(
}
export class NoModelsError extends Schema.TaggedErrorClass<NoModelsError>()("ProviderNoModelsError", {
providerID: ProviderID,
providerID: ProviderV2.ID,
}) {
static isInstance(input: unknown): input is NoModelsError {
return input instanceof NoModelsError
@@ -1022,22 +1022,22 @@ export type DefaultModelError = ModelNotFoundError | NoProvidersError | NoModels
export type Error = ModelNotFoundError | InitError | NoProvidersError | NoModelsError
export interface Interface {
readonly list: () => Effect.Effect<Record<ProviderID, Info>>
readonly getProvider: (providerID: ProviderID) => Effect.Effect<Info>
readonly getModel: (providerID: ProviderID, modelID: ModelID) => Effect.Effect<Model, ModelNotFoundError>
readonly list: () => Effect.Effect<Record<ProviderV2.ID, Info>>
readonly getProvider: (providerID: ProviderV2.ID) => Effect.Effect<Info>
readonly getModel: (providerID: ProviderV2.ID, modelID: ProviderV2.ModelID) => Effect.Effect<Model, ModelNotFoundError>
readonly getLanguage: (model: Model) => Effect.Effect<LanguageModelV3, ModelNotFoundError>
readonly closest: (
providerID: ProviderID,
providerID: ProviderV2.ID,
query: string[],
) => Effect.Effect<{ providerID: ProviderID; modelID: string } | undefined>
readonly getSmallModel: (providerID: ProviderID) => Effect.Effect<Model | undefined>
readonly defaultModel: () => Effect.Effect<{ providerID: ProviderID; modelID: ModelID }, DefaultModelError>
) => Effect.Effect<{ providerID: ProviderV2.ID; modelID: string } | undefined>
readonly getSmallModel: (providerID: ProviderV2.ID) => Effect.Effect<Model | undefined>
readonly defaultModel: () => Effect.Effect<{ providerID: ProviderV2.ID; modelID: ProviderV2.ModelID }, DefaultModelError>
}
interface State {
models: Map<string, LanguageModelV3>
providers: Record<ProviderID, Info>
catalog: Record<ProviderID, Info>
providers: Record<ProviderV2.ID, Info>
catalog: Record<ProviderV2.ID, Info>
sdk: Map<string, BundledSDK>
modelLoaders: Record<string, CustomModelLoader>
varsLoaders: Record<string, CustomVarsLoader>
@@ -1082,8 +1082,8 @@ function cost(c: ModelsDev.Model["cost"]): Model["cost"] {
function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
const base: Model = {
id: ModelID.make(model.id),
providerID: ProviderID.make(provider.id),
id: ProviderV2.ModelID.make(model.id),
providerID: ProviderV2.ID.make(provider.id),
name: model.name,
family: model.family,
api: {
@@ -1140,7 +1140,7 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
const base = fromModelsDevModel(provider, model)
models[id] = {
...base,
id: ModelID.make(id),
id: ProviderV2.ModelID.make(id),
name: `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}`,
cost: opts.cost ? mergeDeep(base.cost, cost(opts.cost)) : base.cost,
options: opts.provider?.body
@@ -1156,7 +1156,7 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
}
}
return {
id: ProviderID.make(provider.id),
id: ProviderV2.ID.make(provider.id),
source: "custom",
name: provider.name,
env: [...(provider.env ?? [])],
@@ -1175,7 +1175,7 @@ function suggestionModelIDs(provider: Info | undefined, enableExperimentalModels
})
}
function modelSuggestions(provider: Info | undefined, modelID: ModelID, enableExperimentalModels: boolean) {
function modelSuggestions(provider: Info | undefined, modelID: ProviderV2.ModelID, enableExperimentalModels: boolean) {
const available = suggestionModelIDs(provider, enableExperimentalModels)
const fuzzy = fuzzysort.go(modelID, available, { limit: 3, threshold: -10000 }).map((m) => m.target)
if (fuzzy.length) return fuzzy
@@ -1217,7 +1217,7 @@ export const layer = Layer.effect(
const catalog = mapValues(modelsDev, fromModelsDevProvider)
const database = mapValues(catalog, toPublicInfo)
const providers: Record<ProviderID, Info> = {} as Record<ProviderID, Info>
const providers: Record<ProviderV2.ID, Info> = {} as Record<ProviderV2.ID, Info>
const languages = new Map<string, LanguageModelV3>()
const modelLoaders: {
[providerID: string]: CustomModelLoader
@@ -1238,7 +1238,7 @@ export const layer = Layer.effect(
log.info("init")
function mergeProvider(providerID: ProviderID, provider: Partial<Info>) {
function mergeProvider(providerID: ProviderV2.ID, provider: Partial<Info>) {
const existing = providers[providerID]
if (existing) {
// @ts-expect-error
@@ -1259,7 +1259,7 @@ export const layer = Layer.effect(
const disabled = new Set(cfg.disabled_providers ?? [])
const enabled = cfg.enabled_providers ? new Set(cfg.enabled_providers) : null
function isProviderAllowed(providerID: ProviderID): boolean {
function isProviderAllowed(providerID: ProviderV2.ID): boolean {
if (enabled && !enabled.has(providerID)) return false
if (disabled.has(providerID)) return false
return true
@@ -1270,7 +1270,7 @@ export const layer = Layer.effect(
const models = p?.models
if (!p || !models) continue
const providerID = ProviderID.make(p.id)
const providerID = ProviderV2.ID.make(p.id)
if (disabled.has(providerID)) continue
const provider = database[providerID]
@@ -1284,7 +1284,7 @@ export const layer = Layer.effect(
id,
{
...model,
id: ModelID.make(id),
id: ProviderV2.ModelID.make(id),
providerID,
},
]),
@@ -1296,7 +1296,7 @@ export const layer = Layer.effect(
for (const [providerID, provider] of configProviders) {
const existing = database[providerID]
const parsed: Info = {
id: ProviderID.make(providerID),
id: ProviderV2.ID.make(providerID),
name: provider.name ?? existing?.name ?? providerID,
env: provider.env ?? existing?.env ?? [],
options: mergeDeep(existing?.options ?? {}, provider.options ?? {}),
@@ -1319,7 +1319,7 @@ export const layer = Layer.effect(
return existingModel?.name ?? modelID
})
const parsedModel: Model = {
id: ModelID.make(modelID),
id: ProviderV2.ModelID.make(modelID),
api: {
id: apiID,
npm: apiNpm,
@@ -1327,7 +1327,7 @@ export const layer = Layer.effect(
},
status: model.status ?? existingModel?.status ?? "active",
name,
providerID: ProviderID.make(providerID),
providerID: ProviderV2.ID.make(providerID),
capabilities: {
temperature: model.temperature ?? existingModel?.capabilities.temperature ?? false,
reasoning: model.reasoning ?? existingModel?.capabilities.reasoning ?? false,
@@ -1389,7 +1389,7 @@ export const layer = Layer.effect(
// load env
const envs = yield* env.all()
for (const [id, provider] of Object.entries(database)) {
const providerID = ProviderID.make(id)
const providerID = ProviderV2.ID.make(id)
if (disabled.has(providerID)) continue
const apiKey = provider.env.map((item) => envs[item]).find(Boolean)
if (!apiKey) continue
@@ -1402,7 +1402,7 @@ export const layer = Layer.effect(
// load apikeys
const auths = yield* auth.all().pipe(Effect.orDie)
for (const [id, provider] of Object.entries(auths)) {
const providerID = ProviderID.make(id)
const providerID = ProviderV2.ID.make(id)
if (disabled.has(providerID)) continue
if (provider.type === "api") {
mergeProvider(providerID, {
@@ -1415,7 +1415,7 @@ export const layer = Layer.effect(
// plugin auth loader - database now has entries for config providers
for (const plugin of plugins) {
if (!plugin.auth) continue
const providerID = ProviderID.make(plugin.auth.provider)
const providerID = ProviderV2.ID.make(plugin.auth.provider)
if (disabled.has(providerID)) continue
const stored = yield* auth.get(providerID).pipe(Effect.orDie)
@@ -1434,7 +1434,7 @@ export const layer = Layer.effect(
}
for (const [id, fn] of Object.entries(custom(dep))) {
const providerID = ProviderID.make(id)
const providerID = ProviderV2.ID.make(id)
if (disabled.has(providerID)) continue
const data = database[providerID]
if (!data) {
@@ -1454,7 +1454,7 @@ export const layer = Layer.effect(
// load config - re-apply with updated data
for (const [id, provider] of configProviders) {
const providerID = ProviderID.make(id)
const providerID = ProviderV2.ID.make(id)
const partial: Partial<Info> = { source: "config" }
if (provider.env) partial.env = provider.env
if (provider.name) partial.name = provider.name
@@ -1462,7 +1462,7 @@ export const layer = Layer.effect(
mergeProvider(providerID, partial)
}
const gitlab = ProviderID.make("gitlab")
const gitlab = ProviderV2.ID.make("gitlab")
if (discoveryLoaders[gitlab] && providers[gitlab] && isProviderAllowed(gitlab)) {
yield* Effect.promise(async () => {
try {
@@ -1479,7 +1479,7 @@ export const layer = Layer.effect(
}
for (const [id, provider] of Object.entries(providers)) {
const providerID = ProviderID.make(id)
const providerID = ProviderV2.ID.make(id)
if (!isProviderAllowed(providerID)) {
delete providers[providerID]
continue
@@ -1493,10 +1493,10 @@ export const layer = Layer.effect(
// These chat aliases are invalid for the special handling in the
// built-in providers below, but custom providers may support them.
(modelID === "gpt-5-chat-latest" &&
(providerID === ProviderID.openai ||
providerID === ProviderID.githubCopilot ||
providerID === ProviderID.openrouter)) ||
(providerID === ProviderID.openrouter && modelID === "openai/gpt-5-chat")
(providerID === ProviderV2.ID.openai ||
providerID === ProviderV2.ID.githubCopilot ||
providerID === ProviderV2.ID.openrouter)) ||
(providerID === ProviderV2.ID.openrouter && modelID === "openai/gpt-5-chat")
)
delete provider.models[modelID]
if (model.status === "alpha" && !runtimeFlags.enableExperimentalModels) delete provider.models[modelID]
@@ -1702,11 +1702,11 @@ export const layer = Layer.effect(
}
}
const getProvider = Effect.fn("Provider.getProvider")((providerID: ProviderID) =>
const getProvider = Effect.fn("Provider.getProvider")((providerID: ProviderV2.ID) =>
InstanceState.use(state, (s) => s.providers[providerID]),
)
const getModel = Effect.fn("Provider.getModel")(function* (providerID: ProviderID, modelID: ModelID) {
const getModel = Effect.fn("Provider.getModel")(function* (providerID: ProviderV2.ID, modelID: ProviderV2.ModelID) {
const s = yield* InstanceState.get(state)
const provider = s.providers[providerID]
if (!provider) {
@@ -1756,7 +1756,7 @@ export const layer = Layer.effect(
)
})
const closest = Effect.fn("Provider.closest")(function* (providerID: ProviderID, query: string[]) {
const closest = Effect.fn("Provider.closest")(function* (providerID: ProviderV2.ID, query: string[]) {
const s = yield* InstanceState.get(state)
const provider = s.providers[providerID]
if (!provider) return undefined
@@ -1768,7 +1768,7 @@ export const layer = Layer.effect(
return undefined
})
const getSmallModel = Effect.fn("Provider.getSmallModel")(function* (providerID: ProviderID) {
const getSmallModel = Effect.fn("Provider.getSmallModel")(function* (providerID: ProviderV2.ID) {
const cfg = yield* config.get()
if (cfg.small_model) {
@@ -1798,7 +1798,7 @@ export const layer = Layer.effect(
priority = ["gpt-5-mini", "claude-haiku-4.5", ...priority]
}
for (const item of priority) {
if (providerID === ProviderID.amazonBedrock) {
if (providerID === ProviderV2.ID.amazonBedrock) {
const crossRegionPrefixes = ["global.", "us.", "eu."]
const candidates = Object.keys(provider.models).filter((m) => m.includes(item))
@@ -1832,16 +1832,16 @@ export const layer = Layer.effect(
const s = yield* InstanceState.get(state)
const recent = yield* fs.readJson(path.join(Global.Path.state, "model.json")).pipe(
Effect.map((x): { providerID: ProviderID; modelID: ModelID }[] => {
Effect.map((x): { providerID: ProviderV2.ID; modelID: ProviderV2.ModelID }[] => {
if (!isRecord(x) || !Array.isArray(x.recent)) return []
return x.recent.flatMap((item) => {
if (!isRecord(item)) return []
if (typeof item.providerID !== "string") return []
if (typeof item.modelID !== "string") return []
return [{ providerID: ProviderID.make(item.providerID), modelID: ModelID.make(item.modelID) }]
return [{ providerID: ProviderV2.ID.make(item.providerID), modelID: ProviderV2.ModelID.make(item.modelID) }]
})
}),
Effect.catch(() => Effect.succeed([] as { providerID: ProviderID; modelID: ModelID }[])),
Effect.catch(() => Effect.succeed([] as { providerID: ProviderV2.ID; modelID: ProviderV2.ModelID }[])),
)
for (const entry of recent) {
const provider = s.providers[entry.providerID]
@@ -1889,8 +1889,8 @@ export function sort<T extends { id: string }>(models: T[]) {
export function parseModel(model: string) {
const [providerID, ...rest] = model.split("/")
return {
providerID: ProviderID.make(providerID),
modelID: ModelID.make(rest.join("/")),
providerID: ProviderV2.ID.make(providerID),
modelID: ProviderV2.ModelID.make(rest.join("/")),
}
}
-30
View File
@@ -1,30 +0,0 @@
import { Schema } from "effect"
import { withStatics } from "@opencode-ai/core/schema"
const providerIdSchema = Schema.String.pipe(Schema.brand("ProviderID"))
export type ProviderID = typeof providerIdSchema.Type
export const ProviderID = providerIdSchema.pipe(
withStatics((schema: typeof providerIdSchema) => ({
// Well-known providers
opencode: schema.make("opencode"),
anthropic: schema.make("anthropic"),
openai: schema.make("openai"),
google: schema.make("google"),
googleVertex: schema.make("google-vertex"),
githubCopilot: schema.make("github-copilot"),
amazonBedrock: schema.make("amazon-bedrock"),
azure: schema.make("azure"),
openrouter: schema.make("openrouter"),
mistral: schema.make("mistral"),
gitlab: schema.make("gitlab"),
})),
)
const modelIdSchema = Schema.String.pipe(Schema.brand("ModelID"))
export type ModelID = typeof modelIdSchema.Type
export const ModelID = modelIdSchema