refactor: move provider and config provider routes onto HttpApi (#23004)

This commit is contained in:
Kit Langton
2026-04-16 23:10:45 -04:00
committed by GitHub
parent c51f3e35ca
commit ee7339f2c6
8 changed files with 323 additions and 158 deletions
+22 -16
View File
@@ -58,6 +58,18 @@ export class Authorization extends Schema.Class<Authorization>("ProviderAuthAuth
static readonly zod = zod(this)
}
export const AuthorizeInput = Schema.Struct({
method: Schema.Number.annotate({ description: "Auth method index" }),
inputs: Schema.optional(Schema.Record(Schema.String, Schema.String)).annotate({ description: "Prompt inputs" }),
}).pipe(withStatics((s) => ({ zod: zod(s) })))
export type AuthorizeInput = Schema.Schema.Type<typeof AuthorizeInput>
export const CallbackInput = Schema.Struct({
method: Schema.Number.annotate({ description: "Auth method index" }),
code: Schema.optional(Schema.String).annotate({ description: "OAuth authorization code" }),
}).pipe(withStatics((s) => ({ zod: zod(s) })))
export type CallbackInput = Schema.Schema.Type<typeof CallbackInput>
export const OauthMissing = NamedError.create("ProviderAuthOauthMissing", z.object({ providerID: ProviderID.zod }))
export const OauthCodeMissing = NamedError.create(
@@ -86,12 +98,12 @@ type Hook = NonNullable<Hooks["auth"]>
export interface Interface {
readonly methods: () => Effect.Effect<Methods>
readonly authorize: (input: {
providerID: ProviderID
method: number
inputs?: Record<string, string>
}) => Effect.Effect<Authorization | undefined, Error>
readonly callback: (input: { providerID: ProviderID; method: number; code?: string }) => Effect.Effect<void, Error>
readonly authorize: (
input: {
providerID: ProviderID
} & AuthorizeInput,
) => Effect.Effect<Authorization | undefined, Error>
readonly callback: (input: { providerID: ProviderID } & CallbackInput) => Effect.Effect<void, Error>
}
interface State {
@@ -153,11 +165,9 @@ export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> =
)
})
const authorize = Effect.fn("ProviderAuth.authorize")(function* (input: {
providerID: ProviderID
method: number
inputs?: Record<string, string>
}) {
const authorize = Effect.fn("ProviderAuth.authorize")(function* (
input: { providerID: ProviderID } & AuthorizeInput,
) {
const { hooks, pending } = yield* InstanceState.get(state)
const method = hooks[input.providerID].methods[input.method]
if (method.type !== "oauth") return
@@ -180,11 +190,7 @@ export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> =
}
})
const callback = Effect.fn("ProviderAuth.callback")(function* (input: {
providerID: ProviderID
method: number
code?: string
}) {
const callback = Effect.fn("ProviderAuth.callback")(function* (input: { providerID: ProviderID } & CallbackInput) {
const pending = (yield* InstanceState.get(state)).pending
const match = pending.get(input.providerID)
if (!match) return yield* Effect.fail(new OauthMissing({ providerID: input.providerID }))
+128 -100
View File
@@ -16,14 +16,16 @@ import { Env } from "../env"
import { Instance } from "../project/instance"
import { InstallationVersion } from "../installation/version"
import { Flag } from "../flag/flag"
import { zod } from "@/util/effect-zod"
import { iife } from "@/util/iife"
import { Global } from "../global"
import path from "path"
import { Effect, Layer, Context } from "effect"
import { Effect, Layer, Context, Schema, Types } from "effect"
import { EffectBridge } from "@/effect"
import { InstanceState } from "@/effect"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { isRecord } from "@/util/record"
import { withStatics } from "@/util/schema"
import * as ProviderTransform from "./transform"
import { ModelID, ProviderID } from "./schema"
@@ -796,91 +798,111 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
}
}
export const Model = z
.object({
id: ModelID.zod,
providerID: ProviderID.zod,
api: z.object({
id: z.string(),
url: z.string(),
npm: z.string(),
}),
name: z.string(),
family: z.string().optional(),
capabilities: z.object({
temperature: z.boolean(),
reasoning: z.boolean(),
attachment: z.boolean(),
toolcall: z.boolean(),
input: z.object({
text: z.boolean(),
audio: z.boolean(),
image: z.boolean(),
video: z.boolean(),
pdf: z.boolean(),
}),
output: z.object({
text: z.boolean(),
audio: z.boolean(),
image: z.boolean(),
video: z.boolean(),
pdf: z.boolean(),
}),
interleaved: z.union([
z.boolean(),
z.object({
field: z.enum(["reasoning_content", "reasoning_details"]),
}),
]),
}),
cost: z.object({
input: z.number(),
output: z.number(),
cache: z.object({
read: z.number(),
write: z.number(),
}),
experimentalOver200K: z
.object({
input: z.number(),
output: z.number(),
cache: z.object({
read: z.number(),
write: z.number(),
}),
})
.optional(),
}),
limit: z.object({
context: z.number(),
input: z.number().optional(),
output: z.number(),
}),
status: z.enum(["alpha", "beta", "deprecated", "active"]),
options: z.record(z.string(), z.any()),
headers: z.record(z.string(), z.string()),
release_date: z.string(),
variants: z.record(z.string(), z.record(z.string(), z.any())).optional(),
})
.meta({
ref: "Model",
})
export type Model = z.infer<typeof Model>
const ProviderApiInfo = Schema.Struct({
id: Schema.String,
url: Schema.String,
npm: Schema.String,
})
export const Info = z
.object({
id: ProviderID.zod,
name: z.string(),
source: z.enum(["env", "config", "custom", "api"]),
env: z.string().array(),
key: z.string().optional(),
options: z.record(z.string(), z.any()),
models: z.record(z.string(), Model),
})
.meta({
ref: "Provider",
})
export type Info = z.infer<typeof Info>
const ProviderModalities = Schema.Struct({
text: Schema.Boolean,
audio: Schema.Boolean,
image: Schema.Boolean,
video: Schema.Boolean,
pdf: Schema.Boolean,
})
const ProviderInterleaved = Schema.Union([
Schema.Boolean,
Schema.Struct({
field: Schema.Literals(["reasoning_content", "reasoning_details"]),
}),
])
const ProviderCapabilities = Schema.Struct({
temperature: Schema.Boolean,
reasoning: Schema.Boolean,
attachment: Schema.Boolean,
toolcall: Schema.Boolean,
input: ProviderModalities,
output: ProviderModalities,
interleaved: ProviderInterleaved,
})
const ProviderCacheCost = Schema.Struct({
read: Schema.Number,
write: Schema.Number,
})
const ProviderCost = Schema.Struct({
input: Schema.Number,
output: Schema.Number,
cache: ProviderCacheCost,
experimentalOver200K: Schema.optional(
Schema.Struct({
input: Schema.Number,
output: Schema.Number,
cache: ProviderCacheCost,
}),
),
})
const ProviderLimit = Schema.Struct({
context: Schema.Number,
input: Schema.optional(Schema.Number),
output: Schema.Number,
})
export const Model = Schema.Struct({
id: ModelID,
providerID: ProviderID,
api: ProviderApiInfo,
name: Schema.String,
family: Schema.optional(Schema.String),
capabilities: ProviderCapabilities,
cost: ProviderCost,
limit: ProviderLimit,
status: Schema.Literals(["alpha", "beta", "deprecated", "active"]),
options: Schema.Record(Schema.String, Schema.Any),
headers: Schema.Record(Schema.String, Schema.String),
release_date: Schema.String,
variants: Schema.optional(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Any))),
})
.annotate({ identifier: "Model" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type Model = Types.DeepMutable<Schema.Schema.Type<typeof Model>>
export const Info = Schema.Struct({
id: ProviderID,
name: Schema.String,
source: Schema.Literals(["env", "config", "custom", "api"]),
env: Schema.Array(Schema.String),
key: Schema.optional(Schema.String),
options: Schema.Record(Schema.String, Schema.Any),
models: Schema.Record(Schema.String, Model),
})
.annotate({ identifier: "Provider" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>
const DefaultModelIDs = Schema.Record(Schema.String, Schema.String)
export const ListResult = Schema.Struct({
all: Schema.Array(Info),
default: DefaultModelIDs,
connected: Schema.Array(Schema.String),
}).pipe(withStatics((s) => ({ zod: zod(s) })))
export type ListResult = Types.DeepMutable<Schema.Schema.Type<typeof ListResult>>
export const ConfigProvidersResult = Schema.Struct({
providers: Schema.Array(Info),
default: DefaultModelIDs,
}).pipe(withStatics((s) => ({ zod: zod(s) })))
export type ConfigProvidersResult = Types.DeepMutable<Schema.Schema.Type<typeof ConfigProvidersResult>>
export function defaultModelIDs<T extends { models: Record<string, { id: string }> }>(providers: Record<string, T>) {
return mapValues(providers, (item) => sort(Object.values(item.models))[0].id)
}
export interface Interface {
readonly list: () => Effect.Effect<Record<ProviderID, Info>>
@@ -928,7 +950,7 @@ function cost(c: ModelsDev.Model["cost"]): Model["cost"] {
}
function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
const m: Model = {
const base: Model = {
id: ModelID.make(model.id),
providerID: ProviderID.make(provider.id),
name: model.name,
@@ -972,9 +994,10 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model
variants: {},
}
m.variants = mapValues(ProviderTransform.variants(m), (v) => v)
return m
return {
...base,
variants: mapValues(ProviderTransform.variants(base), (v) => v),
}
}
export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
@@ -983,17 +1006,22 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
models[key] = fromModelsDevModel(provider, model)
for (const [mode, opts] of Object.entries(model.experimental?.modes ?? {})) {
const id = `${model.id}-${mode}`
const m = fromModelsDevModel(provider, model)
m.id = ModelID.make(id)
m.name = `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}`
if (opts.cost) m.cost = mergeDeep(m.cost, cost(opts.cost))
// convert body params to camelCase for ai sdk compatibility
if (opts.provider?.body)
m.options = Object.fromEntries(
Object.entries(opts.provider.body).map(([k, v]) => [k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()), v]),
)
if (opts.provider?.headers) m.headers = opts.provider.headers
models[id] = m
const base = fromModelsDevModel(provider, model)
models[id] = {
...base,
id: 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
? Object.fromEntries(
Object.entries(opts.provider.body).map(([k, v]) => [
k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()),
v,
]),
)
: base.options,
headers: opts.provider?.headers ?? base.headers,
}
}
}
return {