Sfoglia il codice sorgente

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

Kit Langton 16 ore fa
parent
commit
ee7339f2c6

+ 22 - 16
packages/opencode/src/provider/auth.ts

@@ -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
packages/opencode/src/provider/provider.ts

@@ -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(),
+const ProviderApiInfo = Schema.Struct({
+  id: Schema.String,
+  url: Schema.String,
+  npm: Schema.String,
+})
+
+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,
     }),
-    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>
-
-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 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 {

+ 3 - 9
packages/opencode/src/server/instance/config.ts

@@ -3,7 +3,6 @@ import { describeRoute, validator, resolver } from "hono-openapi"
 import z from "zod"
 import { Config } from "../../config"
 import { Provider } from "../../provider"
-import { mapValues } from "remeda"
 import { errors } from "../error"
 import { lazy } from "../../util/lazy"
 import { AppRuntime } from "../../effect/app-runtime"
@@ -70,12 +69,7 @@ export const ConfigRoutes = lazy(() =>
             description: "List of providers",
             content: {
               "application/json": {
-                schema: resolver(
-                  z.object({
-                    providers: Provider.Info.array(),
-                    default: z.record(z.string(), z.string()),
-                  }),
-                ),
+                schema: resolver(Provider.ConfigProvidersResult.zod),
               },
             },
           },
@@ -84,10 +78,10 @@ export const ConfigRoutes = lazy(() =>
       async (c) =>
         jsonRequest("ConfigRoutes.providers", c, function* () {
           const svc = yield* Provider.Service
-          const providers = mapValues(yield* svc.list(), (item) => item)
+          const providers = yield* svc.list()
           return {
             providers: Object.values(providers),
-            default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
+            default: Provider.defaultModelIDs(providers),
           }
         }),
     ),

+ 51 - 0
packages/opencode/src/server/instance/httpapi/config.ts

@@ -0,0 +1,51 @@
+import { Config } from "@/config"
+import { Provider } from "@/provider"
+import { Effect, Layer } from "effect"
+import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
+
+const root = "/config"
+
+export const ConfigApi = HttpApi.make("config")
+  .add(
+    HttpApiGroup.make("config")
+      .add(
+        HttpApiEndpoint.get("providers", `${root}/providers`, {
+          success: Provider.ConfigProvidersResult,
+        }).annotateMerge(
+          OpenApi.annotations({
+            identifier: "config.providers",
+            summary: "List config providers",
+            description: "Get a list of all configured AI providers and their default models.",
+          }),
+        ),
+      )
+      .annotateMerge(
+        OpenApi.annotations({
+          title: "config",
+          description: "Experimental HttpApi config routes.",
+        }),
+      ),
+  )
+  .annotateMerge(
+    OpenApi.annotations({
+      title: "opencode experimental HttpApi",
+      version: "0.0.1",
+      description: "Experimental HttpApi surface for selected instance routes.",
+    }),
+  )
+
+export const configHandlers = Layer.unwrap(
+  Effect.gen(function* () {
+    const svc = yield* Provider.Service
+
+    const providers = Effect.fn("ConfigHttpApi.providers")(function* () {
+      const providers = yield* svc.list()
+      return {
+        providers: Object.values(providers),
+        default: Provider.defaultModelIDs(providers),
+      }
+    })
+
+    return HttpApiBuilder.group(ConfigApi, "config", (handlers) => handlers.handle("providers", providers))
+  }),
+).pipe(Layer.provide(Provider.defaultLayer), Layer.provide(Config.defaultLayer))

+ 100 - 4
packages/opencode/src/server/instance/httpapi/provider.ts

@@ -1,6 +1,11 @@
 import { ProviderAuth } from "@/provider"
-import { Effect, Layer } from "effect"
-import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
+import { Config } from "@/config"
+import { ModelsDev } from "@/provider"
+import { Provider } from "@/provider"
+import { ProviderID } from "@/provider/schema"
+import { mapValues } from "remeda"
+import { Effect, Layer, Schema } from "effect"
+import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
 
 const root = "/provider"
 
@@ -8,6 +13,15 @@ export const ProviderApi = HttpApi.make("provider")
   .add(
     HttpApiGroup.make("provider")
       .add(
+        HttpApiEndpoint.get("list", root, {
+          success: Provider.ListResult,
+        }).annotateMerge(
+          OpenApi.annotations({
+            identifier: "provider.list",
+            summary: "List providers",
+            description: "Get a list of all available AI providers, including both available and connected ones.",
+          }),
+        ),
         HttpApiEndpoint.get("auth", `${root}/auth`, {
           success: ProviderAuth.Methods,
         }).annotateMerge(
@@ -17,6 +31,28 @@ export const ProviderApi = HttpApi.make("provider")
             description: "Retrieve available authentication methods for all AI providers.",
           }),
         ),
+        HttpApiEndpoint.post("authorize", `${root}/:providerID/oauth/authorize`, {
+          params: { providerID: ProviderID },
+          payload: ProviderAuth.AuthorizeInput,
+          success: ProviderAuth.Authorization,
+        }).annotateMerge(
+          OpenApi.annotations({
+            identifier: "provider.oauth.authorize",
+            summary: "Start OAuth authorization",
+            description: "Start the OAuth authorization flow for a provider.",
+          }),
+        ),
+        HttpApiEndpoint.post("callback", `${root}/:providerID/oauth/callback`, {
+          params: { providerID: ProviderID },
+          payload: ProviderAuth.CallbackInput,
+          success: Schema.Boolean,
+        }).annotateMerge(
+          OpenApi.annotations({
+            identifier: "provider.oauth.callback",
+            summary: "Handle OAuth callback",
+            description: "Handle the OAuth callback from a provider after user authorization.",
+          }),
+        ),
       )
       .annotateMerge(
         OpenApi.annotations({
@@ -35,12 +71,72 @@ export const ProviderApi = HttpApi.make("provider")
 
 export const providerHandlers = Layer.unwrap(
   Effect.gen(function* () {
+    const cfg = yield* Config.Service
+    const provider = yield* Provider.Service
     const svc = yield* ProviderAuth.Service
 
+    const list = Effect.fn("ProviderHttpApi.list")(function* () {
+      const config = yield* cfg.get()
+      const all = yield* Effect.promise(() => ModelsDev.get())
+      const disabled = new Set(config.disabled_providers ?? [])
+      const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
+      const filtered: Record<string, (typeof all)[string]> = {}
+      for (const [key, value] of Object.entries(all)) {
+        if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) {
+          filtered[key] = value
+        }
+      }
+      const connected = yield* provider.list()
+      const providers = Object.assign(
+        mapValues(filtered, (item) => Provider.fromModelsDevProvider(item)),
+        connected,
+      )
+      return {
+        all: Object.values(providers),
+        default: Provider.defaultModelIDs(providers),
+        connected: Object.keys(connected),
+      }
+    })
+
     const auth = Effect.fn("ProviderHttpApi.auth")(function* () {
       return yield* svc.methods()
     })
 
-    return HttpApiBuilder.group(ProviderApi, "provider", (handlers) => handlers.handle("auth", auth))
+    const authorize = Effect.fn("ProviderHttpApi.authorize")(function* (ctx: {
+      params: { providerID: ProviderID }
+      payload: ProviderAuth.AuthorizeInput
+    }) {
+      const result = yield* svc
+        .authorize({
+          providerID: ctx.params.providerID,
+          method: ctx.payload.method,
+          inputs: ctx.payload.inputs,
+        })
+        .pipe(Effect.catch(() => Effect.fail(new HttpApiError.BadRequest({}))))
+      if (!result) return yield* new HttpApiError.BadRequest({})
+      return result
+    })
+
+    const callback = Effect.fn("ProviderHttpApi.callback")(function* (ctx: {
+      params: { providerID: ProviderID }
+      payload: ProviderAuth.CallbackInput
+    }) {
+      yield* svc
+        .callback({
+          providerID: ctx.params.providerID,
+          method: ctx.payload.method,
+          code: ctx.payload.code,
+        })
+        .pipe(Effect.catch(() => Effect.fail(new HttpApiError.BadRequest({}))))
+      return true
+    })
+
+    return HttpApiBuilder.group(ProviderApi, "provider", (handlers) =>
+      handlers.handle("list", list).handle("auth", auth).handle("authorize", authorize).handle("callback", callback),
+    )
   }),
-).pipe(Layer.provide(ProviderAuth.defaultLayer))
+).pipe(
+  Layer.provide(ProviderAuth.defaultLayer),
+  Layer.provide(Provider.defaultLayer),
+  Layer.provide(Config.defaultLayer),
+)

+ 3 - 0
packages/opencode/src/server/instance/httpapi/server.ts

@@ -10,6 +10,7 @@ import { InstanceBootstrap } from "@/project/bootstrap"
 import { Instance } from "@/project/instance"
 import { lazy } from "@/util/lazy"
 import { Filesystem } from "@/util"
+import { ConfigApi, configHandlers } from "./config"
 import { PermissionApi, permissionHandlers } from "./permission"
 import { ProviderApi, providerHandlers } from "./provider"
 import { QuestionApi, questionHandlers } from "./question"
@@ -108,8 +109,10 @@ const instance = HttpRouter.middleware()(
 const QuestionSecured = QuestionApi.middleware(Authorization)
 const PermissionSecured = PermissionApi.middleware(Authorization)
 const ProviderSecured = ProviderApi.middleware(Authorization)
+const ConfigSecured = ConfigApi.middleware(Authorization)
 
 export const routes = Layer.mergeAll(
+  HttpApiBuilder.layer(ConfigSecured).pipe(Layer.provide(configHandlers)),
   HttpApiBuilder.layer(QuestionSecured).pipe(Layer.provide(questionHandlers)),
   HttpApiBuilder.layer(PermissionSecured).pipe(Layer.provide(permissionHandlers)),
   HttpApiBuilder.layer(ProviderSecured).pipe(Layer.provide(providerHandlers)),

+ 12 - 7
packages/opencode/src/server/instance/index.ts

@@ -1,7 +1,7 @@
 import { describeRoute, resolver, validator } from "hono-openapi"
 import { Hono } from "hono"
 import type { UpgradeWebSocket } from "hono/ws"
-import { Effect } from "effect"
+import { Context, Effect } from "effect"
 import z from "zod"
 import { Format } from "../../format"
 import { TuiRoutes } from "./tui"
@@ -41,12 +41,17 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => {
 
   if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) {
     const handler = ExperimentalHttpApiServer.webHandler().handler
-    app
-      .all("/question", (c) => handler(c.req.raw))
-      .all("/question/*", (c) => handler(c.req.raw))
-      .all("/permission", (c) => handler(c.req.raw))
-      .all("/permission/*", (c) => handler(c.req.raw))
-      .all("/provider/auth", (c) => handler(c.req.raw))
+    const context = Context.empty() as Context.Context<unknown>
+    app.get("/question", (c) => handler(c.req.raw, context))
+    app.post("/question/:requestID/reply", (c) => handler(c.req.raw, context))
+    app.post("/question/:requestID/reject", (c) => handler(c.req.raw, context))
+    app.get("/permission", (c) => handler(c.req.raw, context))
+    app.post("/permission/:requestID/reply", (c) => handler(c.req.raw, context))
+    app.get("/config/providers", (c) => handler(c.req.raw, context))
+    app.get("/provider", (c) => handler(c.req.raw, context))
+    app.get("/provider/auth", (c) => handler(c.req.raw, context))
+    app.post("/provider/:providerID/oauth/authorize", (c) => handler(c.req.raw, context))
+    app.post("/provider/:providerID/oauth/callback", (c) => handler(c.req.raw, context))
   }
 
   return app

+ 4 - 22
packages/opencode/src/server/instance/provider.ts

@@ -25,13 +25,7 @@ export const ProviderRoutes = lazy(() =>
             description: "List of providers",
             content: {
               "application/json": {
-                schema: resolver(
-                  z.object({
-                    all: Provider.Info.array(),
-                    default: z.record(z.string(), z.string()),
-                    connected: z.array(z.string()),
-                  }),
-                ),
+                schema: resolver(Provider.ListResult.zod),
               },
             },
           },
@@ -59,7 +53,7 @@ export const ProviderRoutes = lazy(() =>
             )
             return {
               all: Object.values(providers),
-              default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
+              default: Provider.defaultModelIDs(providers),
               connected: Object.keys(connected),
             }
           }),
@@ -116,13 +110,7 @@ export const ProviderRoutes = lazy(() =>
           providerID: ProviderID.zod.meta({ description: "Provider ID" }),
         }),
       ),
-      validator(
-        "json",
-        z.object({
-          method: z.number().meta({ description: "Auth method index" }),
-          inputs: z.record(z.string(), z.string()).optional().meta({ description: "Prompt inputs" }),
-        }),
-      ),
+      validator("json", ProviderAuth.AuthorizeInput.zod),
       async (c) => {
         const providerID = c.req.valid("param").providerID
         const { method, inputs } = c.req.valid("json")
@@ -162,13 +150,7 @@ export const ProviderRoutes = lazy(() =>
           providerID: ProviderID.zod.meta({ description: "Provider ID" }),
         }),
       ),
-      validator(
-        "json",
-        z.object({
-          method: z.number().meta({ description: "Auth method index" }),
-          code: z.string().optional().meta({ description: "OAuth authorization code" }),
-        }),
-      ),
+      validator("json", ProviderAuth.CallbackInput.zod),
       async (c) => {
         const providerID = c.req.valid("param").providerID
         const { method, code } = c.req.valid("json")