Skip to content

Commit ee7339f

Browse files
authored
refactor: move provider and config provider routes onto HttpApi (#23004)
1 parent c51f3e3 commit ee7339f

8 files changed

Lines changed: 323 additions & 158 deletions

File tree

packages/opencode/src/provider/auth.ts

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,18 @@ export class Authorization extends Schema.Class<Authorization>("ProviderAuthAuth
5858
static readonly zod = zod(this)
5959
}
6060

61+
export const AuthorizeInput = Schema.Struct({
62+
method: Schema.Number.annotate({ description: "Auth method index" }),
63+
inputs: Schema.optional(Schema.Record(Schema.String, Schema.String)).annotate({ description: "Prompt inputs" }),
64+
}).pipe(withStatics((s) => ({ zod: zod(s) })))
65+
export type AuthorizeInput = Schema.Schema.Type<typeof AuthorizeInput>
66+
67+
export const CallbackInput = Schema.Struct({
68+
method: Schema.Number.annotate({ description: "Auth method index" }),
69+
code: Schema.optional(Schema.String).annotate({ description: "OAuth authorization code" }),
70+
}).pipe(withStatics((s) => ({ zod: zod(s) })))
71+
export type CallbackInput = Schema.Schema.Type<typeof CallbackInput>
72+
6173
export const OauthMissing = NamedError.create("ProviderAuthOauthMissing", z.object({ providerID: ProviderID.zod }))
6274

6375
export const OauthCodeMissing = NamedError.create(
@@ -86,12 +98,12 @@ type Hook = NonNullable<Hooks["auth"]>
8698

8799
export interface Interface {
88100
readonly methods: () => Effect.Effect<Methods>
89-
readonly authorize: (input: {
90-
providerID: ProviderID
91-
method: number
92-
inputs?: Record<string, string>
93-
}) => Effect.Effect<Authorization | undefined, Error>
94-
readonly callback: (input: { providerID: ProviderID; method: number; code?: string }) => Effect.Effect<void, Error>
101+
readonly authorize: (
102+
input: {
103+
providerID: ProviderID
104+
} & AuthorizeInput,
105+
) => Effect.Effect<Authorization | undefined, Error>
106+
readonly callback: (input: { providerID: ProviderID } & CallbackInput) => Effect.Effect<void, Error>
95107
}
96108

97109
interface State {
@@ -153,11 +165,9 @@ export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> =
153165
)
154166
})
155167

156-
const authorize = Effect.fn("ProviderAuth.authorize")(function* (input: {
157-
providerID: ProviderID
158-
method: number
159-
inputs?: Record<string, string>
160-
}) {
168+
const authorize = Effect.fn("ProviderAuth.authorize")(function* (
169+
input: { providerID: ProviderID } & AuthorizeInput,
170+
) {
161171
const { hooks, pending } = yield* InstanceState.get(state)
162172
const method = hooks[input.providerID].methods[input.method]
163173
if (method.type !== "oauth") return
@@ -180,11 +190,7 @@ export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> =
180190
}
181191
})
182192

183-
const callback = Effect.fn("ProviderAuth.callback")(function* (input: {
184-
providerID: ProviderID
185-
method: number
186-
code?: string
187-
}) {
193+
const callback = Effect.fn("ProviderAuth.callback")(function* (input: { providerID: ProviderID } & CallbackInput) {
188194
const pending = (yield* InstanceState.get(state)).pending
189195
const match = pending.get(input.providerID)
190196
if (!match) return yield* Effect.fail(new OauthMissing({ providerID: input.providerID }))

packages/opencode/src/provider/provider.ts

Lines changed: 128 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@ import { Env } from "../env"
1616
import { Instance } from "../project/instance"
1717
import { InstallationVersion } from "../installation/version"
1818
import { Flag } from "../flag/flag"
19+
import { zod } from "@/util/effect-zod"
1920
import { iife } from "@/util/iife"
2021
import { Global } from "../global"
2122
import path from "path"
22-
import { Effect, Layer, Context } from "effect"
23+
import { Effect, Layer, Context, Schema, Types } from "effect"
2324
import { EffectBridge } from "@/effect"
2425
import { InstanceState } from "@/effect"
2526
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
2627
import { isRecord } from "@/util/record"
28+
import { withStatics } from "@/util/schema"
2729

2830
import * as ProviderTransform from "./transform"
2931
import { ModelID, ProviderID } from "./schema"
@@ -796,91 +798,111 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
796798
}
797799
}
798800

799-
export const Model = z
800-
.object({
801-
id: ModelID.zod,
802-
providerID: ProviderID.zod,
803-
api: z.object({
804-
id: z.string(),
805-
url: z.string(),
806-
npm: z.string(),
807-
}),
808-
name: z.string(),
809-
family: z.string().optional(),
810-
capabilities: z.object({
811-
temperature: z.boolean(),
812-
reasoning: z.boolean(),
813-
attachment: z.boolean(),
814-
toolcall: z.boolean(),
815-
input: z.object({
816-
text: z.boolean(),
817-
audio: z.boolean(),
818-
image: z.boolean(),
819-
video: z.boolean(),
820-
pdf: z.boolean(),
821-
}),
822-
output: z.object({
823-
text: z.boolean(),
824-
audio: z.boolean(),
825-
image: z.boolean(),
826-
video: z.boolean(),
827-
pdf: z.boolean(),
828-
}),
829-
interleaved: z.union([
830-
z.boolean(),
831-
z.object({
832-
field: z.enum(["reasoning_content", "reasoning_details"]),
833-
}),
834-
]),
835-
}),
836-
cost: z.object({
837-
input: z.number(),
838-
output: z.number(),
839-
cache: z.object({
840-
read: z.number(),
841-
write: z.number(),
842-
}),
843-
experimentalOver200K: z
844-
.object({
845-
input: z.number(),
846-
output: z.number(),
847-
cache: z.object({
848-
read: z.number(),
849-
write: z.number(),
850-
}),
851-
})
852-
.optional(),
853-
}),
854-
limit: z.object({
855-
context: z.number(),
856-
input: z.number().optional(),
857-
output: z.number(),
801+
const ProviderApiInfo = Schema.Struct({
802+
id: Schema.String,
803+
url: Schema.String,
804+
npm: Schema.String,
805+
})
806+
807+
const ProviderModalities = Schema.Struct({
808+
text: Schema.Boolean,
809+
audio: Schema.Boolean,
810+
image: Schema.Boolean,
811+
video: Schema.Boolean,
812+
pdf: Schema.Boolean,
813+
})
814+
815+
const ProviderInterleaved = Schema.Union([
816+
Schema.Boolean,
817+
Schema.Struct({
818+
field: Schema.Literals(["reasoning_content", "reasoning_details"]),
819+
}),
820+
])
821+
822+
const ProviderCapabilities = Schema.Struct({
823+
temperature: Schema.Boolean,
824+
reasoning: Schema.Boolean,
825+
attachment: Schema.Boolean,
826+
toolcall: Schema.Boolean,
827+
input: ProviderModalities,
828+
output: ProviderModalities,
829+
interleaved: ProviderInterleaved,
830+
})
831+
832+
const ProviderCacheCost = Schema.Struct({
833+
read: Schema.Number,
834+
write: Schema.Number,
835+
})
836+
837+
const ProviderCost = Schema.Struct({
838+
input: Schema.Number,
839+
output: Schema.Number,
840+
cache: ProviderCacheCost,
841+
experimentalOver200K: Schema.optional(
842+
Schema.Struct({
843+
input: Schema.Number,
844+
output: Schema.Number,
845+
cache: ProviderCacheCost,
858846
}),
859-
status: z.enum(["alpha", "beta", "deprecated", "active"]),
860-
options: z.record(z.string(), z.any()),
861-
headers: z.record(z.string(), z.string()),
862-
release_date: z.string(),
863-
variants: z.record(z.string(), z.record(z.string(), z.any())).optional(),
864-
})
865-
.meta({
866-
ref: "Model",
867-
})
868-
export type Model = z.infer<typeof Model>
869-
870-
export const Info = z
871-
.object({
872-
id: ProviderID.zod,
873-
name: z.string(),
874-
source: z.enum(["env", "config", "custom", "api"]),
875-
env: z.string().array(),
876-
key: z.string().optional(),
877-
options: z.record(z.string(), z.any()),
878-
models: z.record(z.string(), Model),
879-
})
880-
.meta({
881-
ref: "Provider",
882-
})
883-
export type Info = z.infer<typeof Info>
847+
),
848+
})
849+
850+
const ProviderLimit = Schema.Struct({
851+
context: Schema.Number,
852+
input: Schema.optional(Schema.Number),
853+
output: Schema.Number,
854+
})
855+
856+
export const Model = Schema.Struct({
857+
id: ModelID,
858+
providerID: ProviderID,
859+
api: ProviderApiInfo,
860+
name: Schema.String,
861+
family: Schema.optional(Schema.String),
862+
capabilities: ProviderCapabilities,
863+
cost: ProviderCost,
864+
limit: ProviderLimit,
865+
status: Schema.Literals(["alpha", "beta", "deprecated", "active"]),
866+
options: Schema.Record(Schema.String, Schema.Any),
867+
headers: Schema.Record(Schema.String, Schema.String),
868+
release_date: Schema.String,
869+
variants: Schema.optional(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Any))),
870+
})
871+
.annotate({ identifier: "Model" })
872+
.pipe(withStatics((s) => ({ zod: zod(s) })))
873+
export type Model = Types.DeepMutable<Schema.Schema.Type<typeof Model>>
874+
875+
export const Info = Schema.Struct({
876+
id: ProviderID,
877+
name: Schema.String,
878+
source: Schema.Literals(["env", "config", "custom", "api"]),
879+
env: Schema.Array(Schema.String),
880+
key: Schema.optional(Schema.String),
881+
options: Schema.Record(Schema.String, Schema.Any),
882+
models: Schema.Record(Schema.String, Model),
883+
})
884+
.annotate({ identifier: "Provider" })
885+
.pipe(withStatics((s) => ({ zod: zod(s) })))
886+
export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>
887+
888+
const DefaultModelIDs = Schema.Record(Schema.String, Schema.String)
889+
890+
export const ListResult = Schema.Struct({
891+
all: Schema.Array(Info),
892+
default: DefaultModelIDs,
893+
connected: Schema.Array(Schema.String),
894+
}).pipe(withStatics((s) => ({ zod: zod(s) })))
895+
export type ListResult = Types.DeepMutable<Schema.Schema.Type<typeof ListResult>>
896+
897+
export const ConfigProvidersResult = Schema.Struct({
898+
providers: Schema.Array(Info),
899+
default: DefaultModelIDs,
900+
}).pipe(withStatics((s) => ({ zod: zod(s) })))
901+
export type ConfigProvidersResult = Types.DeepMutable<Schema.Schema.Type<typeof ConfigProvidersResult>>
902+
903+
export function defaultModelIDs<T extends { models: Record<string, { id: string }> }>(providers: Record<string, T>) {
904+
return mapValues(providers, (item) => sort(Object.values(item.models))[0].id)
905+
}
884906

885907
export interface Interface {
886908
readonly list: () => Effect.Effect<Record<ProviderID, Info>>
@@ -928,7 +950,7 @@ function cost(c: ModelsDev.Model["cost"]): Model["cost"] {
928950
}
929951

930952
function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
931-
const m: Model = {
953+
const base: Model = {
932954
id: ModelID.make(model.id),
933955
providerID: ProviderID.make(provider.id),
934956
name: model.name,
@@ -972,9 +994,10 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model
972994
variants: {},
973995
}
974996

975-
m.variants = mapValues(ProviderTransform.variants(m), (v) => v)
976-
977-
return m
997+
return {
998+
...base,
999+
variants: mapValues(ProviderTransform.variants(base), (v) => v),
1000+
}
9781001
}
9791002

9801003
export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
@@ -983,17 +1006,22 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
9831006
models[key] = fromModelsDevModel(provider, model)
9841007
for (const [mode, opts] of Object.entries(model.experimental?.modes ?? {})) {
9851008
const id = `${model.id}-${mode}`
986-
const m = fromModelsDevModel(provider, model)
987-
m.id = ModelID.make(id)
988-
m.name = `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}`
989-
if (opts.cost) m.cost = mergeDeep(m.cost, cost(opts.cost))
990-
// convert body params to camelCase for ai sdk compatibility
991-
if (opts.provider?.body)
992-
m.options = Object.fromEntries(
993-
Object.entries(opts.provider.body).map(([k, v]) => [k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()), v]),
994-
)
995-
if (opts.provider?.headers) m.headers = opts.provider.headers
996-
models[id] = m
1009+
const base = fromModelsDevModel(provider, model)
1010+
models[id] = {
1011+
...base,
1012+
id: ModelID.make(id),
1013+
name: `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}`,
1014+
cost: opts.cost ? mergeDeep(base.cost, cost(opts.cost)) : base.cost,
1015+
options: opts.provider?.body
1016+
? Object.fromEntries(
1017+
Object.entries(opts.provider.body).map(([k, v]) => [
1018+
k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()),
1019+
v,
1020+
]),
1021+
)
1022+
: base.options,
1023+
headers: opts.provider?.headers ?? base.headers,
1024+
}
9971025
}
9981026
}
9991027
return {

packages/opencode/src/server/instance/config.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { describeRoute, validator, resolver } from "hono-openapi"
33
import z from "zod"
44
import { Config } from "../../config"
55
import { Provider } from "../../provider"
6-
import { mapValues } from "remeda"
76
import { errors } from "../error"
87
import { lazy } from "../../util/lazy"
98
import { AppRuntime } from "../../effect/app-runtime"
@@ -70,12 +69,7 @@ export const ConfigRoutes = lazy(() =>
7069
description: "List of providers",
7170
content: {
7271
"application/json": {
73-
schema: resolver(
74-
z.object({
75-
providers: Provider.Info.array(),
76-
default: z.record(z.string(), z.string()),
77-
}),
78-
),
72+
schema: resolver(Provider.ConfigProvidersResult.zod),
7973
},
8074
},
8175
},
@@ -84,10 +78,10 @@ export const ConfigRoutes = lazy(() =>
8478
async (c) =>
8579
jsonRequest("ConfigRoutes.providers", c, function* () {
8680
const svc = yield* Provider.Service
87-
const providers = mapValues(yield* svc.list(), (item) => item)
81+
const providers = yield* svc.list()
8882
return {
8983
providers: Object.values(providers),
90-
default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
84+
default: Provider.defaultModelIDs(providers),
9185
}
9286
}),
9387
),

0 commit comments

Comments
 (0)