Skip to content

Commit 316cf09

Browse files
authored
refactor(provider): migrate provider domain to Effect Schema (anomalyco#24027)
1 parent 51f68f5 commit 316cf09

4 files changed

Lines changed: 94 additions & 114 deletions

File tree

packages/opencode/specs/effect/schema.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -274,9 +274,9 @@ Possible later tightening after the Schema-first migration is stable:
274274

275275
### Provider domain
276276

277-
- [ ] `src/provider/auth.ts`
278-
- [ ] `src/provider/models.ts`
279-
- [ ] `src/provider/provider.ts`
277+
- [x] `src/provider/auth.ts`
278+
- [x] `src/provider/models.ts`
279+
- [x] `src/provider/provider.ts`
280280

281281
### Tool schemas
282282

packages/opencode/src/provider/auth.ts

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import type { AuthOAuthResult, Hooks } from "@opencode-ai/plugin"
2-
import { NamedError } from "@opencode-ai/shared/util/error"
32
import { Auth } from "@/auth"
43
import { InstanceState } from "@/effect"
54
import { zod } from "@/util/effect-zod"
5+
import { namedSchemaError } from "@/util/named-schema-error"
66
import { withStatics } from "@/util/schema"
77
import { Plugin } from "../plugin"
88
import { ProviderID } from "./schema"
99
import { Array as Arr, Effect, Layer, Record, Result, Context, Schema } from "effect"
10-
import z from "zod"
1110

1211
const When = Schema.Struct({
1312
key: Schema.String,
@@ -70,22 +69,16 @@ export const CallbackInput = Schema.Struct({
7069
}).pipe(withStatics((s) => ({ zod: zod(s) })))
7170
export type CallbackInput = Schema.Schema.Type<typeof CallbackInput>
7271

73-
export const OauthMissing = NamedError.create("ProviderAuthOauthMissing", z.object({ providerID: ProviderID.zod }))
72+
export const OauthMissing = namedSchemaError("ProviderAuthOauthMissing", { providerID: ProviderID })
7473

75-
export const OauthCodeMissing = NamedError.create(
76-
"ProviderAuthOauthCodeMissing",
77-
z.object({ providerID: ProviderID.zod }),
78-
)
74+
export const OauthCodeMissing = namedSchemaError("ProviderAuthOauthCodeMissing", { providerID: ProviderID })
7975

80-
export const OauthCallbackFailed = NamedError.create("ProviderAuthOauthCallbackFailed", z.object({}))
76+
export const OauthCallbackFailed = namedSchemaError("ProviderAuthOauthCallbackFailed", {})
8177

82-
export const ValidationFailed = NamedError.create(
83-
"ProviderAuthValidationFailed",
84-
z.object({
85-
field: z.string(),
86-
message: z.string(),
87-
}),
88-
)
78+
export const ValidationFailed = namedSchemaError("ProviderAuthValidationFailed", {
79+
field: Schema.String,
80+
message: Schema.String,
81+
})
8982

9083
export type Error =
9184
| Auth.AuthError

packages/opencode/src/provider/models.ts

Lines changed: 73 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Global } from "../global"
22
import { Log } from "../util"
33
import path from "path"
4-
import z from "zod"
4+
import { Schema } from "effect"
55
import { Installation } from "../installation"
66
import { Flag } from "../flag/flag"
77
import { lazy } from "@/util/lazy"
@@ -21,91 +21,85 @@ const filepath = path.join(
2121
)
2222
const ttl = 5 * 60 * 1000
2323

24-
type JsonValue = string | number | boolean | null | { [key: string]: JsonValue } | JsonValue[]
25-
26-
const JsonValue: z.ZodType<JsonValue> = z.lazy(() =>
27-
z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(JsonValue), z.record(z.string(), JsonValue)]),
28-
)
29-
30-
const Cost = z.object({
31-
input: z.number(),
32-
output: z.number(),
33-
cache_read: z.number().optional(),
34-
cache_write: z.number().optional(),
35-
context_over_200k: z
36-
.object({
37-
input: z.number(),
38-
output: z.number(),
39-
cache_read: z.number().optional(),
40-
cache_write: z.number().optional(),
41-
})
42-
.optional(),
24+
const Cost = Schema.Struct({
25+
input: Schema.Number,
26+
output: Schema.Number,
27+
cache_read: Schema.optional(Schema.Number),
28+
cache_write: Schema.optional(Schema.Number),
29+
context_over_200k: Schema.optional(
30+
Schema.Struct({
31+
input: Schema.Number,
32+
output: Schema.Number,
33+
cache_read: Schema.optional(Schema.Number),
34+
cache_write: Schema.optional(Schema.Number),
35+
}),
36+
),
4337
})
4438

45-
export const Model = z.object({
46-
id: z.string(),
47-
name: z.string(),
48-
family: z.string().optional(),
49-
release_date: z.string(),
50-
attachment: z.boolean(),
51-
reasoning: z.boolean(),
52-
temperature: z.boolean(),
53-
tool_call: z.boolean(),
54-
interleaved: z
55-
.union([
56-
z.literal(true),
57-
z
58-
.object({
59-
field: z.enum(["reasoning_content", "reasoning_details"]),
60-
})
61-
.strict(),
62-
])
63-
.optional(),
64-
cost: Cost.optional(),
65-
limit: z.object({
66-
context: z.number(),
67-
input: z.number().optional(),
68-
output: z.number(),
39+
export const Model = Schema.Struct({
40+
id: Schema.String,
41+
name: Schema.String,
42+
family: Schema.optional(Schema.String),
43+
release_date: Schema.String,
44+
attachment: Schema.Boolean,
45+
reasoning: Schema.Boolean,
46+
temperature: Schema.Boolean,
47+
tool_call: Schema.Boolean,
48+
interleaved: Schema.optional(
49+
Schema.Union([
50+
Schema.Literal(true),
51+
Schema.Struct({
52+
field: Schema.Literals(["reasoning_content", "reasoning_details"]),
53+
}),
54+
]),
55+
),
56+
cost: Schema.optional(Cost),
57+
limit: Schema.Struct({
58+
context: Schema.Number,
59+
input: Schema.optional(Schema.Number),
60+
output: Schema.Number,
6961
}),
70-
modalities: z
71-
.object({
72-
input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
73-
output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
74-
})
75-
.optional(),
76-
experimental: z
77-
.object({
78-
modes: z
79-
.record(
80-
z.string(),
81-
z.object({
82-
cost: Cost.optional(),
83-
provider: z
84-
.object({
85-
body: z.record(z.string(), JsonValue).optional(),
86-
headers: z.record(z.string(), z.string()).optional(),
87-
})
88-
.optional(),
62+
modalities: Schema.optional(
63+
Schema.Struct({
64+
input: Schema.Array(Schema.Literals(["text", "audio", "image", "video", "pdf"])),
65+
output: Schema.Array(Schema.Literals(["text", "audio", "image", "video", "pdf"])),
66+
}),
67+
),
68+
experimental: Schema.optional(
69+
Schema.Struct({
70+
modes: Schema.optional(
71+
Schema.Record(
72+
Schema.String,
73+
Schema.Struct({
74+
cost: Schema.optional(Cost),
75+
provider: Schema.optional(
76+
Schema.Struct({
77+
body: Schema.optional(Schema.Record(Schema.String, Schema.MutableJson)),
78+
headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
79+
}),
80+
),
8981
}),
90-
)
91-
.optional(),
92-
})
93-
.optional(),
94-
status: z.enum(["alpha", "beta", "deprecated"]).optional(),
95-
provider: z.object({ npm: z.string().optional(), api: z.string().optional() }).optional(),
82+
),
83+
),
84+
}),
85+
),
86+
status: Schema.optional(Schema.Literals(["alpha", "beta", "deprecated"])),
87+
provider: Schema.optional(
88+
Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) }),
89+
),
9690
})
97-
export type Model = z.infer<typeof Model>
98-
99-
export const Provider = z.object({
100-
api: z.string().optional(),
101-
name: z.string(),
102-
env: z.array(z.string()),
103-
id: z.string(),
104-
npm: z.string().optional(),
105-
models: z.record(z.string(), Model),
91+
export type Model = Schema.Schema.Type<typeof Model>
92+
93+
export const Provider = Schema.Struct({
94+
api: Schema.optional(Schema.String),
95+
name: Schema.String,
96+
env: Schema.Array(Schema.String),
97+
id: Schema.String,
98+
npm: Schema.optional(Schema.String),
99+
models: Schema.Record(Schema.String, Model),
106100
})
107101

108-
export type Provider = z.infer<typeof Provider>
102+
export type Provider = Schema.Schema.Type<typeof Provider>
109103

110104
function url() {
111105
return Flag.OPENCODE_MODELS_URL || "https://models.dev"

packages/opencode/src/provider/provider.ts

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import z from "zod"
21
import os from "os"
32
import fuzzysort from "fuzzysort"
43
import { Config } from "../config"
@@ -8,14 +7,14 @@ import { Log } from "../util"
87
import { Npm } from "../npm"
98
import { Hash } from "@opencode-ai/shared/util/hash"
109
import { Plugin } from "../plugin"
11-
import { NamedError } from "@opencode-ai/shared/util/error"
1210
import { type LanguageModelV3 } from "@ai-sdk/provider"
1311
import * as ModelsDev from "./models"
1412
import { Auth } from "../auth"
1513
import { Env } from "../env"
1614
import { InstallationVersion } from "../installation/version"
1715
import { Flag } from "../flag/flag"
1816
import { zod } from "@/util/effect-zod"
17+
import { namedSchemaError } from "@/util/named-schema-error"
1918
import { iife } from "@/util/iife"
2019
import { Global } from "../global"
2120
import path from "path"
@@ -1047,7 +1046,7 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
10471046
id: ProviderID.make(provider.id),
10481047
source: "custom",
10491048
name: provider.name,
1050-
env: provider.env ?? [],
1049+
env: [...(provider.env ?? [])],
10511050
options: {},
10521051
models,
10531052
}
@@ -1713,18 +1712,12 @@ export function parseModel(model: string) {
17131712
}
17141713
}
17151714

1716-
export const ModelNotFoundError = NamedError.create(
1717-
"ProviderModelNotFoundError",
1718-
z.object({
1719-
providerID: ProviderID.zod,
1720-
modelID: ModelID.zod,
1721-
suggestions: z.array(z.string()).optional(),
1722-
}),
1723-
)
1715+
export const ModelNotFoundError = namedSchemaError("ProviderModelNotFoundError", {
1716+
providerID: ProviderID,
1717+
modelID: ModelID,
1718+
suggestions: Schema.optional(Schema.Array(Schema.String)),
1719+
})
17241720

1725-
export const InitError = NamedError.create(
1726-
"ProviderInitError",
1727-
z.object({
1728-
providerID: ProviderID.zod,
1729-
}),
1730-
)
1721+
export const InitError = namedSchemaError("ProviderInitError", {
1722+
providerID: ProviderID,
1723+
})

0 commit comments

Comments
 (0)