refactor(provider): migrate provider domain to Effect Schema (#24027)

This commit is contained in:
Kit Langton 2026-04-23 13:17:33 -04:00 committed by GitHub
parent 1e439b8226
commit 93940a1859
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 93 additions and 113 deletions

View file

@ -274,9 +274,9 @@ Possible later tightening after the Schema-first migration is stable:
### Provider domain ### Provider domain
- [ ] `src/provider/auth.ts` - [x] `src/provider/auth.ts`
- [ ] `src/provider/models.ts` - [x] `src/provider/models.ts`
- [ ] `src/provider/provider.ts` - [x] `src/provider/provider.ts`
### Tool schemas ### Tool schemas

View file

@ -1,13 +1,12 @@
import type { AuthOAuthResult, Hooks } from "@opencode-ai/plugin" import type { AuthOAuthResult, Hooks } from "@opencode-ai/plugin"
import { NamedError } from "@opencode-ai/shared/util/error"
import { Auth } from "@/auth" import { Auth } from "@/auth"
import { InstanceState } from "@/effect" import { InstanceState } from "@/effect"
import { zod } from "@/util/effect-zod" import { zod } from "@/util/effect-zod"
import { namedSchemaError } from "@/util/named-schema-error"
import { withStatics } from "@/util/schema" import { withStatics } from "@/util/schema"
import { Plugin } from "../plugin" import { Plugin } from "../plugin"
import { ProviderID } from "./schema" import { ProviderID } from "./schema"
import { Array as Arr, Effect, Layer, Record, Result, Context, Schema } from "effect" import { Array as Arr, Effect, Layer, Record, Result, Context, Schema } from "effect"
import z from "zod"
const When = Schema.Struct({ const When = Schema.Struct({
key: Schema.String, key: Schema.String,
@ -70,22 +69,16 @@ export const CallbackInput = Schema.Struct({
}).pipe(withStatics((s) => ({ zod: zod(s) }))) }).pipe(withStatics((s) => ({ zod: zod(s) })))
export type CallbackInput = Schema.Schema.Type<typeof CallbackInput> export type CallbackInput = Schema.Schema.Type<typeof CallbackInput>
export const OauthMissing = NamedError.create("ProviderAuthOauthMissing", z.object({ providerID: ProviderID.zod })) export const OauthMissing = namedSchemaError("ProviderAuthOauthMissing", { providerID: ProviderID })
export const OauthCodeMissing = NamedError.create( export const OauthCodeMissing = namedSchemaError("ProviderAuthOauthCodeMissing", { providerID: ProviderID })
"ProviderAuthOauthCodeMissing",
z.object({ providerID: ProviderID.zod }),
)
export const OauthCallbackFailed = NamedError.create("ProviderAuthOauthCallbackFailed", z.object({})) export const OauthCallbackFailed = namedSchemaError("ProviderAuthOauthCallbackFailed", {})
export const ValidationFailed = NamedError.create( export const ValidationFailed = namedSchemaError("ProviderAuthValidationFailed", {
"ProviderAuthValidationFailed", field: Schema.String,
z.object({ message: Schema.String,
field: z.string(), })
message: z.string(),
}),
)
export type Error = export type Error =
| Auth.AuthError | Auth.AuthError

View file

@ -1,7 +1,7 @@
import { Global } from "../global" import { Global } from "../global"
import { Log } from "../util" import { Log } from "../util"
import path from "path" import path from "path"
import z from "zod" import { Schema } from "effect"
import { Installation } from "../installation" import { Installation } from "../installation"
import { Flag } from "../flag/flag" import { Flag } from "../flag/flag"
import { lazy } from "@/util/lazy" import { lazy } from "@/util/lazy"
@ -21,91 +21,85 @@ const filepath = path.join(
) )
const ttl = 5 * 60 * 1000 const ttl = 5 * 60 * 1000
type JsonValue = string | number | boolean | null | { [key: string]: JsonValue } | JsonValue[] const Cost = Schema.Struct({
input: Schema.Number,
const JsonValue: z.ZodType<JsonValue> = z.lazy(() => output: Schema.Number,
z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(JsonValue), z.record(z.string(), JsonValue)]), cache_read: Schema.optional(Schema.Number),
) cache_write: Schema.optional(Schema.Number),
context_over_200k: Schema.optional(
const Cost = z.object({ Schema.Struct({
input: z.number(), input: Schema.Number,
output: z.number(), output: Schema.Number,
cache_read: z.number().optional(), cache_read: Schema.optional(Schema.Number),
cache_write: z.number().optional(), cache_write: Schema.optional(Schema.Number),
context_over_200k: z }),
.object({ ),
input: z.number(),
output: z.number(),
cache_read: z.number().optional(),
cache_write: z.number().optional(),
})
.optional(),
}) })
export const Model = z.object({ export const Model = Schema.Struct({
id: z.string(), id: Schema.String,
name: z.string(), name: Schema.String,
family: z.string().optional(), family: Schema.optional(Schema.String),
release_date: z.string(), release_date: Schema.String,
attachment: z.boolean(), attachment: Schema.Boolean,
reasoning: z.boolean(), reasoning: Schema.Boolean,
temperature: z.boolean(), temperature: Schema.Boolean,
tool_call: z.boolean(), tool_call: Schema.Boolean,
interleaved: z interleaved: Schema.optional(
.union([ Schema.Union([
z.literal(true), Schema.Literal(true),
z Schema.Struct({
.object({ field: Schema.Literals(["reasoning_content", "reasoning_details"]),
field: z.enum(["reasoning_content", "reasoning_details"]), }),
}) ]),
.strict(), ),
]) cost: Schema.optional(Cost),
.optional(), limit: Schema.Struct({
cost: Cost.optional(), context: Schema.Number,
limit: z.object({ input: Schema.optional(Schema.Number),
context: z.number(), output: Schema.Number,
input: z.number().optional(),
output: z.number(),
}), }),
modalities: z modalities: Schema.optional(
.object({ Schema.Struct({
input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])), input: Schema.Array(Schema.Literals(["text", "audio", "image", "video", "pdf"])),
output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])), output: Schema.Array(Schema.Literals(["text", "audio", "image", "video", "pdf"])),
}) }),
.optional(), ),
experimental: z experimental: Schema.optional(
.object({ Schema.Struct({
modes: z modes: Schema.optional(
.record( Schema.Record(
z.string(), Schema.String,
z.object({ Schema.Struct({
cost: Cost.optional(), cost: Schema.optional(Cost),
provider: z provider: Schema.optional(
.object({ Schema.Struct({
body: z.record(z.string(), JsonValue).optional(), body: Schema.optional(Schema.Record(Schema.String, Schema.MutableJson)),
headers: z.record(z.string(), z.string()).optional(), headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
}) }),
.optional(), ),
}), }),
) ),
.optional(), ),
}) }),
.optional(), ),
status: z.enum(["alpha", "beta", "deprecated"]).optional(), status: Schema.optional(Schema.Literals(["alpha", "beta", "deprecated"])),
provider: z.object({ npm: z.string().optional(), api: z.string().optional() }).optional(), provider: Schema.optional(
Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) }),
),
}) })
export type Model = z.infer<typeof Model> export type Model = Schema.Schema.Type<typeof Model>
export const Provider = z.object({ export const Provider = Schema.Struct({
api: z.string().optional(), api: Schema.optional(Schema.String),
name: z.string(), name: Schema.String,
env: z.array(z.string()), env: Schema.Array(Schema.String),
id: z.string(), id: Schema.String,
npm: z.string().optional(), npm: Schema.optional(Schema.String),
models: z.record(z.string(), Model), models: Schema.Record(Schema.String, Model),
}) })
export type Provider = z.infer<typeof Provider> export type Provider = Schema.Schema.Type<typeof Provider>
function url() { function url() {
return Flag.OPENCODE_MODELS_URL || "https://models.dev" return Flag.OPENCODE_MODELS_URL || "https://models.dev"

View file

@ -1,4 +1,3 @@
import z from "zod"
import os from "os" import os from "os"
import fuzzysort from "fuzzysort" import fuzzysort from "fuzzysort"
import { Config } from "../config" import { Config } from "../config"
@ -8,7 +7,6 @@ import { Log } from "../util"
import { Npm } from "../npm" import { Npm } from "../npm"
import { Hash } from "@opencode-ai/shared/util/hash" import { Hash } from "@opencode-ai/shared/util/hash"
import { Plugin } from "../plugin" import { Plugin } from "../plugin"
import { NamedError } from "@opencode-ai/shared/util/error"
import { type LanguageModelV3 } from "@ai-sdk/provider" import { type LanguageModelV3 } from "@ai-sdk/provider"
import * as ModelsDev from "./models" import * as ModelsDev from "./models"
import { Auth } from "../auth" import { Auth } from "../auth"
@ -16,6 +14,7 @@ import { Env } from "../env"
import { InstallationVersion } from "../installation/version" import { InstallationVersion } from "../installation/version"
import { Flag } from "../flag/flag" import { Flag } from "../flag/flag"
import { zod } from "@/util/effect-zod" import { zod } from "@/util/effect-zod"
import { namedSchemaError } from "@/util/named-schema-error"
import { iife } from "@/util/iife" import { iife } from "@/util/iife"
import { Global } from "../global" import { Global } from "../global"
import path from "path" import path from "path"
@ -1047,7 +1046,7 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
id: ProviderID.make(provider.id), id: ProviderID.make(provider.id),
source: "custom", source: "custom",
name: provider.name, name: provider.name,
env: provider.env ?? [], env: [...(provider.env ?? [])],
options: {}, options: {},
models, models,
} }
@ -1713,18 +1712,12 @@ export function parseModel(model: string) {
} }
} }
export const ModelNotFoundError = NamedError.create( export const ModelNotFoundError = namedSchemaError("ProviderModelNotFoundError", {
"ProviderModelNotFoundError", providerID: ProviderID,
z.object({ modelID: ModelID,
providerID: ProviderID.zod, suggestions: Schema.optional(Schema.Array(Schema.String)),
modelID: ModelID.zod, })
suggestions: z.array(z.string()).optional(),
}),
)
export const InitError = NamedError.create( export const InitError = namedSchemaError("ProviderInitError", {
"ProviderInitError", providerID: ProviderID,
z.object({ })
providerID: ProviderID.zod,
}),
)