Skip to content

Commit f5cb313

Browse files
committed
refactor(provider): migrate provider domain to Effect Schema
Migrates the three files in the provider domain to Effect Schema as the source of truth per specs/effect/schema.md. - auth.ts: OauthMissing, OauthCodeMissing, OauthCallbackFailed, ValidationFailed switch from NamedError.create to namedSchemaError; the domain schemas were already Schema-first. - models.ts: Cost, Model, Provider, and the recursive JsonValue schema rewritten as Schema.Struct / Schema.Record / Schema.suspend. The inferred types stay as Schema.Schema.Type (readonly) rather than Types.DeepMutable — the recursive JsonValue trips DeepMutable into TS2589. One call site in provider.ts spreads provider.env into a new mutable array to match Provider.Info. - provider.ts: ModelNotFoundError and InitError migrate to namedSchemaError; drops the remaining zod import from this file. SDK output stays byte-identical (verified via ./packages/sdk/js/script/build.ts with clean git status on packages/sdk). All 2064 package tests still pass.
1 parent 8b2f835 commit f5cb313

4 files changed

Lines changed: 108 additions & 112 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: 87 additions & 77 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"
@@ -23,89 +23,99 @@ const ttl = 5 * 60 * 1000
2323

2424
type JsonValue = string | number | boolean | null | { [key: string]: JsonValue } | JsonValue[]
2525

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(),
26+
// Declared with a mutable type because `Types.DeepMutable` cannot walk a
27+
// self-recursive readonly type — it trips TS2589. The derived Schema.Type
28+
// is readonly, so we cast at the Schema boundary.
29+
const JsonValue: Schema.Schema<JsonValue> = Schema.suspend(() =>
30+
Schema.Union([
31+
Schema.String,
32+
Schema.Number,
33+
Schema.Boolean,
34+
Schema.Null,
35+
Schema.Array(JsonValue),
36+
Schema.Record(Schema.String, JsonValue),
37+
]),
38+
) as Schema.Schema<JsonValue>
39+
40+
const Cost = Schema.Struct({
41+
input: Schema.Number,
42+
output: Schema.Number,
43+
cache_read: Schema.optional(Schema.Number),
44+
cache_write: Schema.optional(Schema.Number),
45+
context_over_200k: Schema.optional(
46+
Schema.Struct({
47+
input: Schema.Number,
48+
output: Schema.Number,
49+
cache_read: Schema.optional(Schema.Number),
50+
cache_write: Schema.optional(Schema.Number),
51+
}),
52+
),
4353
})
4454

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(),
55+
export const Model = Schema.Struct({
56+
id: Schema.String,
57+
name: Schema.String,
58+
family: Schema.optional(Schema.String),
59+
release_date: Schema.String,
60+
attachment: Schema.Boolean,
61+
reasoning: Schema.Boolean,
62+
temperature: Schema.Boolean,
63+
tool_call: Schema.Boolean,
64+
interleaved: Schema.optional(
65+
Schema.Union([
66+
Schema.Literal(true),
67+
Schema.Struct({
68+
field: Schema.Literals(["reasoning_content", "reasoning_details"]),
69+
}),
70+
]),
71+
),
72+
cost: Schema.optional(Cost),
73+
limit: Schema.Struct({
74+
context: Schema.Number,
75+
input: Schema.optional(Schema.Number),
76+
output: Schema.Number,
6977
}),
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(),
78+
modalities: Schema.optional(
79+
Schema.Struct({
80+
input: Schema.Array(Schema.Literals(["text", "audio", "image", "video", "pdf"])),
81+
output: Schema.Array(Schema.Literals(["text", "audio", "image", "video", "pdf"])),
82+
}),
83+
),
84+
experimental: Schema.optional(
85+
Schema.Struct({
86+
modes: Schema.optional(
87+
Schema.Record(
88+
Schema.String,
89+
Schema.Struct({
90+
cost: Schema.optional(Cost),
91+
provider: Schema.optional(
92+
Schema.Struct({
93+
body: Schema.optional(Schema.Record(Schema.String, JsonValue)),
94+
headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
95+
}),
96+
),
8997
}),
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(),
98+
),
99+
),
100+
}),
101+
),
102+
status: Schema.optional(Schema.Literals(["alpha", "beta", "deprecated"])),
103+
provider: Schema.optional(
104+
Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) }),
105+
),
96106
})
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),
107+
export type Model = Schema.Schema.Type<typeof Model>
108+
109+
export const Provider = Schema.Struct({
110+
api: Schema.optional(Schema.String),
111+
name: Schema.String,
112+
env: Schema.Array(Schema.String),
113+
id: Schema.String,
114+
npm: Schema.optional(Schema.String),
115+
models: Schema.Record(Schema.String, Model),
106116
})
107117

108-
export type Provider = z.infer<typeof Provider>
118+
export type Provider = Schema.Schema.Type<typeof Provider>
109119

110120
function url() {
111121
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)