From 0f1e4a8ce95bb1c80e97f93fa11c08be9f767ebc Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 22:13:50 -0400 Subject: [PATCH 1/4] refactor: move provider and config provider routes onto HttpApi --- packages/opencode/src/provider/auth.ts | 52 +- packages/opencode/src/provider/provider.ts | 195 ++++--- .../opencode/src/server/instance/config.ts | 7 +- .../src/server/instance/httpapi/config.ts | 52 ++ .../src/server/instance/httpapi/provider.ts | 104 +++- .../src/server/instance/httpapi/server.ts | 3 + .../opencode/src/server/instance/index.ts | 21 +- .../opencode/src/server/instance/provider.ts | 24 +- packages/sdk/js/src/v2/gen/sdk.gen.ts | 506 ++++++++--------- packages/sdk/js/src/v2/gen/types.gen.ts | 530 +++++++++--------- 10 files changed, 835 insertions(+), 659 deletions(-) create mode 100644 packages/opencode/src/server/instance/httpapi/config.ts diff --git a/packages/opencode/src/provider/auth.ts b/packages/opencode/src/provider/auth.ts index c0c73b2cc1e8..d05b04314bc7 100644 --- a/packages/opencode/src/provider/auth.ts +++ b/packages/opencode/src/provider/auth.ts @@ -58,6 +58,32 @@ export class Authorization extends Schema.Class("ProviderAuthAuth static readonly zod = zod(this) } +const AuthorizeInputZod = z.object({ + method: z.number().meta({ description: "Auth method index" }), + inputs: z.record(z.string(), z.string()).optional().meta({ description: "Prompt inputs" }), +}) + +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" }), +}) + +export const AuthorizeInput = Object.assign(_AuthorizeInput, { zod: AuthorizeInputZod }) +export type AuthorizeInput = Schema.Schema.Type + +const CallbackInputZod = z.object({ + method: z.number().meta({ description: "Auth method index" }), + code: z.string().optional().meta({ description: "OAuth authorization code" }), +}) + +const _CallbackInput = Schema.Struct({ + method: Schema.Number.annotate({ description: "Auth method index" }), + code: Schema.optional(Schema.String).annotate({ description: "OAuth authorization code" }), +}) + +export const CallbackInput = Object.assign(_CallbackInput, { zod: CallbackInputZod }) +export type CallbackInput = Schema.Schema.Type + export const OauthMissing = NamedError.create("ProviderAuthOauthMissing", z.object({ providerID: ProviderID.zod })) export const OauthCodeMissing = NamedError.create( @@ -86,12 +112,12 @@ type Hook = NonNullable export interface Interface { readonly methods: () => Effect.Effect - readonly authorize: (input: { - providerID: ProviderID - method: number - inputs?: Record - }) => Effect.Effect - readonly callback: (input: { providerID: ProviderID; method: number; code?: string }) => Effect.Effect + readonly authorize: ( + input: { + providerID: ProviderID + } & AuthorizeInput, + ) => Effect.Effect + readonly callback: (input: { providerID: ProviderID } & CallbackInput) => Effect.Effect } interface State { @@ -153,11 +179,9 @@ export const layer: Layer.Layer = ) }) - const authorize = Effect.fn("ProviderAuth.authorize")(function* (input: { - providerID: ProviderID - method: number - inputs?: Record - }) { + 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 +204,7 @@ export const layer: Layer.Layer = } }) - 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 })) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index a7297634e722..546667a96879 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -16,10 +16,11 @@ 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 } from "effect" import { EffectBridge } from "@/effect" import { InstanceState } from "@/effect" import { AppFileSystem } from "@opencode-ai/shared/filesystem" @@ -30,6 +31,14 @@ import { ModelID, ProviderID } from "./schema" const log = Log.create({ service: "provider" }) +type Mutable = T extends string | number | boolean | bigint | symbol | null | undefined + ? T + : T extends ReadonlyArray + ? Mutable[] + : T extends object + ? { -readonly [K in keyof T]: Mutable } + : T + function shouldUseCopilotResponsesApi(modelID: string): boolean { const match = /^gpt-(\d+)/.exec(modelID) if (!match) return false @@ -796,91 +805,107 @@ function custom(dep: CustomDep): Record { } } -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 - -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 + ), +}) + +const ProviderLimit = Schema.Struct({ + context: Schema.Number, + input: Schema.optional(Schema.Number), + output: Schema.Number, +}) + +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" }) +export const Model = Object.assign(_Model, { zod: zod(_Model) }) +export type Model = Mutable> + +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" }) +export const Info = Object.assign(_Info, { zod: zod(_Info) }) +export type Info = Mutable> + +const DefaultModelIDs = Schema.Record(Schema.String, Schema.String) + +const _ListResult = Schema.Struct({ + all: Schema.Array(Info), + default: DefaultModelIDs, + connected: Schema.Array(Schema.String), +}) +export const ListResult = Object.assign(_ListResult, { zod: zod(_ListResult) }) +export type ListResult = Mutable> + +const _ConfigProvidersResult = Schema.Struct({ + providers: Schema.Array(Info), + default: DefaultModelIDs, +}) +export const ConfigProvidersResult = Object.assign(_ConfigProvidersResult, { zod: zod(_ConfigProvidersResult) }) +export type ConfigProvidersResult = Mutable> export interface Interface { readonly list: () => Effect.Effect> diff --git a/packages/opencode/src/server/instance/config.ts b/packages/opencode/src/server/instance/config.ts index e3291a8c3663..27a2718a6b68 100644 --- a/packages/opencode/src/server/instance/config.ts +++ b/packages/opencode/src/server/instance/config.ts @@ -70,12 +70,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), }, }, }, diff --git a/packages/opencode/src/server/instance/httpapi/config.ts b/packages/opencode/src/server/instance/httpapi/config.ts new file mode 100644 index 000000000000..8a249e423062 --- /dev/null +++ b/packages/opencode/src/server/instance/httpapi/config.ts @@ -0,0 +1,52 @@ +import { Config } from "@/config" +import { Provider } from "@/provider" +import { mapValues } from "remeda" +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 = mapValues(yield* svc.list(), (item) => item) + return { + providers: Object.values(providers), + default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id), + } + }) + + return HttpApiBuilder.group(ConfigApi, "config", (handlers) => handlers.handle("providers", providers)) + }), +).pipe(Layer.provide(Provider.defaultLayer), Layer.provide(Config.defaultLayer)) diff --git a/packages/opencode/src/server/instance/httpapi/provider.ts b/packages/opencode/src/server/instance/httpapi/provider.ts index 31dd1446a03e..5eda039b3289 100644 --- a/packages/opencode/src/server/instance/httpapi/provider.ts +++ b/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 = {} + 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: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id), + 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), +) diff --git a/packages/opencode/src/server/instance/httpapi/server.ts b/packages/opencode/src/server/instance/httpapi/server.ts index 362d0970b9d9..64332fd2a08b 100644 --- a/packages/opencode/src/server/instance/httpapi/server.ts +++ b/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)), diff --git a/packages/opencode/src/server/instance/index.ts b/packages/opencode/src/server/instance/index.ts index 9ef6da63ac03..abc062debda4 100644 --- a/packages/opencode/src/server/instance/index.ts +++ b/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" @@ -32,24 +32,27 @@ import { AppRuntime } from "@/effect/app-runtime" export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { const app = new Hono() .use(WorkspaceRouterMiddleware(upgrade)) - .route("/project", ProjectRoutes()) .route("/pty", PtyRoutes(upgrade)) - .route("/config", ConfigRoutes()) .route("/experimental", ExperimentalRoutes()) .route("/session", SessionRoutes()) .route("/permission", PermissionRoutes()) 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 + app.all("/question", (c) => handler(c.req.raw, context)) + app.all("/question/*", (c) => handler(c.req.raw, context)) + app.all("/permission", (c) => handler(c.req.raw, context)) + app.all("/permission/*", (c) => handler(c.req.raw, context)) + app.all("/config/providers", (c) => handler(c.req.raw, context)) + app.all("/provider", (c) => handler(c.req.raw, context)) + app.all("/provider/auth", (c) => handler(c.req.raw, context)) + app.all("/provider/*", (c) => handler(c.req.raw, context)) } return app + .route("/project", ProjectRoutes()) + .route("/config", ConfigRoutes()) .route("/question", QuestionRoutes()) .route("/provider", ProviderRoutes()) .route("/sync", SyncRoutes()) diff --git a/packages/opencode/src/server/instance/provider.ts b/packages/opencode/src/server/instance/provider.ts index c1580437dab0..199509886f76 100644 --- a/packages/opencode/src/server/instance/provider.ts +++ b/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), }, }, }, @@ -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") diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index d7bf43f506f8..b68d43d2b826 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -510,150 +510,6 @@ export class App extends HeyApiClient { } } -export class Project extends HeyApiClient { - /** - * List all projects - * - * Get a list of projects that have been opened with OpenCode. - */ - public list( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/project", - ...options, - ...params, - }) - } - - /** - * Get current project - * - * Retrieve the currently active project that OpenCode is working with. - */ - public current( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/project/current", - ...options, - ...params, - }) - } - - /** - * Initialize git repository - * - * Create a git repository for the current project and return the refreshed project info. - */ - public initGit( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/project/git/init", - ...options, - ...params, - }) - } - - /** - * Update project - * - * Update project properties such as name, icon, and commands. - */ - public update( - parameters: { - projectID: string - directory?: string - workspace?: string - name?: string - icon?: { - url?: string - override?: string - color?: string - } - commands?: { - /** - * Startup script to run when creating a new workspace (worktree) - */ - start?: string - } - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "projectID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "name" }, - { in: "body", key: "icon" }, - { in: "body", key: "commands" }, - ], - }, - ], - ) - return (options?.client ?? this.client).patch({ - url: "/project/{projectID}", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } -} - export class Pty extends HeyApiClient { /** * List PTY sessions @@ -873,105 +729,6 @@ export class Pty extends HeyApiClient { } } -export class Config2 extends HeyApiClient { - /** - * Get configuration - * - * Retrieve the current OpenCode configuration settings and preferences. - */ - public get( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/config", - ...options, - ...params, - }) - } - - /** - * Update configuration - * - * Update OpenCode configuration settings and preferences. - */ - public update( - parameters?: { - directory?: string - workspace?: string - config?: Config3 - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { key: "config", map: "body" }, - ], - }, - ], - ) - return (options?.client ?? this.client).patch({ - url: "/config", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * List config providers - * - * Get a list of all configured AI providers and their default models. - */ - public providers( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/config/providers", - ...options, - ...params, - }) - } -} - export class Console extends HeyApiClient { /** * Get active Console provider metadata @@ -2746,6 +2503,249 @@ export class Permission extends HeyApiClient { } } +export class Project extends HeyApiClient { + /** + * List all projects + * + * Get a list of projects that have been opened with OpenCode. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/project", + ...options, + ...params, + }) + } + + /** + * Get current project + * + * Retrieve the currently active project that OpenCode is working with. + */ + public current( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/project/current", + ...options, + ...params, + }) + } + + /** + * Initialize git repository + * + * Create a git repository for the current project and return the refreshed project info. + */ + public initGit( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/project/git/init", + ...options, + ...params, + }) + } + + /** + * Update project + * + * Update project properties such as name, icon, and commands. + */ + public update( + parameters: { + projectID: string + directory?: string + workspace?: string + name?: string + icon?: { + url?: string + override?: string + color?: string + } + commands?: { + /** + * Startup script to run when creating a new workspace (worktree) + */ + start?: string + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "projectID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "name" }, + { in: "body", key: "icon" }, + { in: "body", key: "commands" }, + ], + }, + ], + ) + return (options?.client ?? this.client).patch({ + url: "/project/{projectID}", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Config2 extends HeyApiClient { + /** + * Get configuration + * + * Retrieve the current OpenCode configuration settings and preferences. + */ + public get( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/config", + ...options, + ...params, + }) + } + + /** + * Update configuration + * + * Update OpenCode configuration settings and preferences. + */ + public update( + parameters?: { + directory?: string + workspace?: string + config?: Config3 + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { key: "config", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).patch({ + url: "/config", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * List config providers + * + * Get a list of all configured AI providers and their default models. + */ + public providers( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/config/providers", + ...options, + ...params, + }) + } +} + export class Question extends HeyApiClient { /** * List pending questions @@ -4314,21 +4314,11 @@ export class OpencodeClient extends HeyApiClient { return (this._app ??= new App({ client: this.client })) } - private _project?: Project - get project(): Project { - return (this._project ??= new Project({ client: this.client })) - } - private _pty?: Pty get pty(): Pty { return (this._pty ??= new Pty({ client: this.client })) } - private _config?: Config2 - get config(): Config2 { - return (this._config ??= new Config2({ client: this.client })) - } - private _experimental?: Experimental get experimental(): Experimental { return (this._experimental ??= new Experimental({ client: this.client })) @@ -4359,6 +4349,16 @@ export class OpencodeClient extends HeyApiClient { return (this._permission ??= new Permission({ client: this.client })) } + private _project?: Project + get project(): Project { + return (this._project ??= new Project({ client: this.client })) + } + + private _config?: Config2 + get config(): Config2 { + return (this._config ??= new Config2({ client: this.client })) + } + private _question?: Question get question(): Question { return (this._question ??= new Question({ client: this.client })) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 25c3cfa66981..c61dd6d2e999 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1713,91 +1713,6 @@ export type NotFoundError = { } } -export type Model = { - id: string - providerID: string - api: { - id: string - url: string - npm: string - } - name: string - family?: string - capabilities: { - temperature: boolean - reasoning: boolean - attachment: boolean - toolcall: boolean - input: { - text: boolean - audio: boolean - image: boolean - video: boolean - pdf: boolean - } - output: { - text: boolean - audio: boolean - image: boolean - video: boolean - pdf: boolean - } - interleaved: - | boolean - | { - field: "reasoning_content" | "reasoning_details" - } - } - cost: { - input: number - output: number - cache: { - read: number - write: number - } - experimentalOver200K?: { - input: number - output: number - cache: { - read: number - write: number - } - } - } - limit: { - context: number - input?: number - output: number - } - status: "alpha" | "beta" | "deprecated" | "active" - options: { - [key: string]: unknown - } - headers: { - [key: string]: string - } - release_date: string - variants?: { - [key: string]: { - [key: string]: unknown - } - } -} - -export type Provider = { - id: string - name: string - source: "env" | "config" | "custom" | "api" - env: Array - key?: string - options: { - [key: string]: unknown - } - models: { - [key: string]: Model - } -} - export type ToolIds = Array export type ToolListItem = { @@ -1936,6 +1851,91 @@ export type SubtaskPartInput = { command?: string } +export type Model = { + id: string + providerID: string + api: { + id: string + url: string + npm: string + } + name: string + family?: string + capabilities: { + temperature: boolean + reasoning: boolean + attachment: boolean + toolcall: boolean + input: { + text: boolean + audio: boolean + image: boolean + video: boolean + pdf: boolean + } + output: { + text: boolean + audio: boolean + image: boolean + video: boolean + pdf: boolean + } + interleaved: + | boolean + | { + field: "reasoning_content" | "reasoning_details" + } + } + cost: { + input: number + output: number + cache: { + read: number + write: number + } + experimentalOver200K?: { + input: number + output: number + cache: { + read: number + write: number + } + } + } + limit: { + context: number + input?: number + output: number + } + status: "alpha" | "beta" | "deprecated" | "active" + options: { + [key: string]: unknown + } + headers: { + [key: string]: string + } + release_date: string + variants?: { + [key: string]: { + [key: string]: unknown + } + } +} + +export type Provider = { + id: string + name: string + source: "env" | "config" | "custom" | "api" + env: Array + key?: string + options: { + [key: string]: unknown + } + models: { + [key: string]: Model + } +} + export type ProviderAuthMethod = { type: "oauth" | "api" label: string @@ -2383,120 +2383,16 @@ export type AppLogErrors = { 400: BadRequestError } -export type AppLogError = AppLogErrors[keyof AppLogErrors] - -export type AppLogResponses = { - /** - * Log entry written successfully - */ - 200: boolean -} - -export type AppLogResponse = AppLogResponses[keyof AppLogResponses] - -export type ProjectListData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/project" -} - -export type ProjectListResponses = { - /** - * List of projects - */ - 200: Array -} - -export type ProjectListResponse = ProjectListResponses[keyof ProjectListResponses] - -export type ProjectCurrentData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/project/current" -} - -export type ProjectCurrentResponses = { - /** - * Current project information - */ - 200: Project -} - -export type ProjectCurrentResponse = ProjectCurrentResponses[keyof ProjectCurrentResponses] - -export type ProjectInitGitData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/project/git/init" -} - -export type ProjectInitGitResponses = { - /** - * Project information after git initialization - */ - 200: Project -} - -export type ProjectInitGitResponse = ProjectInitGitResponses[keyof ProjectInitGitResponses] - -export type ProjectUpdateData = { - body?: { - name?: string - icon?: { - url?: string - override?: string - color?: string - } - commands?: { - /** - * Startup script to run when creating a new workspace (worktree) - */ - start?: string - } - } - path: { - projectID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/project/{projectID}" -} - -export type ProjectUpdateErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type ProjectUpdateError = ProjectUpdateErrors[keyof ProjectUpdateErrors] +export type AppLogError = AppLogErrors[keyof AppLogErrors] -export type ProjectUpdateResponses = { +export type AppLogResponses = { /** - * Updated project information + * Log entry written successfully */ - 200: Project + 200: boolean } -export type ProjectUpdateResponse = ProjectUpdateResponses[keyof ProjectUpdateResponses] +export type AppLogResponse = AppLogResponses[keyof AppLogResponses] export type PtyListData = { body?: never @@ -2679,77 +2575,6 @@ export type PtyConnectResponses = { export type PtyConnectResponse = PtyConnectResponses[keyof PtyConnectResponses] -export type ConfigGetData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/config" -} - -export type ConfigGetResponses = { - /** - * Get config info - */ - 200: Config -} - -export type ConfigGetResponse = ConfigGetResponses[keyof ConfigGetResponses] - -export type ConfigUpdateData = { - body?: Config - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/config" -} - -export type ConfigUpdateErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ConfigUpdateError = ConfigUpdateErrors[keyof ConfigUpdateErrors] - -export type ConfigUpdateResponses = { - /** - * Successfully updated config - */ - 200: Config -} - -export type ConfigUpdateResponse = ConfigUpdateResponses[keyof ConfigUpdateResponses] - -export type ConfigProvidersData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/config/providers" -} - -export type ConfigProvidersResponses = { - /** - * List of providers - */ - 200: { - providers: Array - default: { - [key: string]: string - } - } -} - -export type ConfigProvidersResponse = ConfigProvidersResponses[keyof ConfigProvidersResponses] - export type ExperimentalConsoleGetData = { body?: never path?: never @@ -4280,6 +4105,181 @@ export type PermissionListResponses = { export type PermissionListResponse = PermissionListResponses[keyof PermissionListResponses] +export type ProjectListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/project" +} + +export type ProjectListResponses = { + /** + * List of projects + */ + 200: Array +} + +export type ProjectListResponse = ProjectListResponses[keyof ProjectListResponses] + +export type ProjectCurrentData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/project/current" +} + +export type ProjectCurrentResponses = { + /** + * Current project information + */ + 200: Project +} + +export type ProjectCurrentResponse = ProjectCurrentResponses[keyof ProjectCurrentResponses] + +export type ProjectInitGitData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/project/git/init" +} + +export type ProjectInitGitResponses = { + /** + * Project information after git initialization + */ + 200: Project +} + +export type ProjectInitGitResponse = ProjectInitGitResponses[keyof ProjectInitGitResponses] + +export type ProjectUpdateData = { + body?: { + name?: string + icon?: { + url?: string + override?: string + color?: string + } + commands?: { + /** + * Startup script to run when creating a new workspace (worktree) + */ + start?: string + } + } + path: { + projectID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/project/{projectID}" +} + +export type ProjectUpdateErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type ProjectUpdateError = ProjectUpdateErrors[keyof ProjectUpdateErrors] + +export type ProjectUpdateResponses = { + /** + * Updated project information + */ + 200: Project +} + +export type ProjectUpdateResponse = ProjectUpdateResponses[keyof ProjectUpdateResponses] + +export type ConfigGetData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/config" +} + +export type ConfigGetResponses = { + /** + * Get config info + */ + 200: Config +} + +export type ConfigGetResponse = ConfigGetResponses[keyof ConfigGetResponses] + +export type ConfigUpdateData = { + body?: Config + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/config" +} + +export type ConfigUpdateErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ConfigUpdateError = ConfigUpdateErrors[keyof ConfigUpdateErrors] + +export type ConfigUpdateResponses = { + /** + * Successfully updated config + */ + 200: Config +} + +export type ConfigUpdateResponse = ConfigUpdateResponses[keyof ConfigUpdateResponses] + +export type ConfigProvidersData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/config/providers" +} + +export type ConfigProvidersResponses = { + /** + * List of providers + */ + 200: { + providers: Array + default: { + [key: string]: string + } + } +} + +export type ConfigProvidersResponse = ConfigProvidersResponses[keyof ConfigProvidersResponses] + export type QuestionListData = { body?: never path?: never From b5b39de3e20762f250d5d760b80a05b196c4564f Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 22:33:39 -0400 Subject: [PATCH 2/4] fix: preserve sdk output for provider HttpApi migration --- packages/opencode/src/provider/auth.ts | 26 +- packages/opencode/src/provider/provider.ts | 69 ++- .../opencode/src/server/instance/index.ts | 22 +- packages/sdk/js/src/v2/gen/sdk.gen.ts | 506 ++++++++--------- packages/sdk/js/src/v2/gen/types.gen.ts | 530 +++++++++--------- 5 files changed, 574 insertions(+), 579 deletions(-) diff --git a/packages/opencode/src/provider/auth.ts b/packages/opencode/src/provider/auth.ts index d05b04314bc7..5d8b2765decc 100644 --- a/packages/opencode/src/provider/auth.ts +++ b/packages/opencode/src/provider/auth.ts @@ -58,31 +58,17 @@ export class Authorization extends Schema.Class("ProviderAuthAuth static readonly zod = zod(this) } -const AuthorizeInputZod = z.object({ - method: z.number().meta({ description: "Auth method index" }), - inputs: z.record(z.string(), z.string()).optional().meta({ description: "Prompt inputs" }), -}) - -const _AuthorizeInput = Schema.Struct({ +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" }), -}) - -export const AuthorizeInput = Object.assign(_AuthorizeInput, { zod: AuthorizeInputZod }) -export type AuthorizeInput = Schema.Schema.Type +}).pipe(withStatics((s) => ({ zod: zod(s) }))) +export type AuthorizeInput = Schema.Schema.Type -const CallbackInputZod = z.object({ - method: z.number().meta({ description: "Auth method index" }), - code: z.string().optional().meta({ description: "OAuth authorization code" }), -}) - -const _CallbackInput = Schema.Struct({ +export const CallbackInput = Schema.Struct({ method: Schema.Number.annotate({ description: "Auth method index" }), code: Schema.optional(Schema.String).annotate({ description: "OAuth authorization code" }), -}) - -export const CallbackInput = Object.assign(_CallbackInput, { zod: CallbackInputZod }) -export type CallbackInput = Schema.Schema.Type +}).pipe(withStatics((s) => ({ zod: zod(s) }))) +export type CallbackInput = Schema.Schema.Type export const OauthMissing = NamedError.create("ProviderAuthOauthMissing", z.object({ providerID: ProviderID.zod })) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 546667a96879..ea83e1528f9d 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -25,6 +25,7 @@ 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" @@ -860,7 +861,7 @@ const ProviderLimit = Schema.Struct({ output: Schema.Number, }) -const _Model = Schema.Struct({ +export const Model = Schema.Struct({ id: ModelID, providerID: ProviderID, api: ProviderApiInfo, @@ -874,11 +875,12 @@ const _Model = Schema.Struct({ 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" }) -export const Model = Object.assign(_Model, { zod: zod(_Model) }) -export type Model = Mutable> +}) + .annotate({ identifier: "Model" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Model = Mutable> -const _Info = Schema.Struct({ +export const Info = Schema.Struct({ id: ProviderID, name: Schema.String, source: Schema.Literals(["env", "config", "custom", "api"]), @@ -886,26 +888,25 @@ const _Info = Schema.Struct({ key: Schema.optional(Schema.String), options: Schema.Record(Schema.String, Schema.Any), models: Schema.Record(Schema.String, Model), -}).annotate({ identifier: "Provider" }) -export const Info = Object.assign(_Info, { zod: zod(_Info) }) -export type Info = Mutable> +}) + .annotate({ identifier: "Provider" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Info = Mutable> const DefaultModelIDs = Schema.Record(Schema.String, Schema.String) -const _ListResult = Schema.Struct({ +export const ListResult = Schema.Struct({ all: Schema.Array(Info), default: DefaultModelIDs, connected: Schema.Array(Schema.String), -}) -export const ListResult = Object.assign(_ListResult, { zod: zod(_ListResult) }) -export type ListResult = Mutable> +}).pipe(withStatics((s) => ({ zod: zod(s) }))) +export type ListResult = Mutable> -const _ConfigProvidersResult = Schema.Struct({ +export const ConfigProvidersResult = Schema.Struct({ providers: Schema.Array(Info), default: DefaultModelIDs, -}) -export const ConfigProvidersResult = Object.assign(_ConfigProvidersResult, { zod: zod(_ConfigProvidersResult) }) -export type ConfigProvidersResult = Mutable> +}).pipe(withStatics((s) => ({ zod: zod(s) }))) +export type ConfigProvidersResult = Mutable> export interface Interface { readonly list: () => Effect.Effect> @@ -953,7 +954,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, @@ -997,9 +998,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 { @@ -1008,17 +1010,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 { diff --git a/packages/opencode/src/server/instance/index.ts b/packages/opencode/src/server/instance/index.ts index abc062debda4..6a290093c514 100644 --- a/packages/opencode/src/server/instance/index.ts +++ b/packages/opencode/src/server/instance/index.ts @@ -32,7 +32,9 @@ import { AppRuntime } from "@/effect/app-runtime" export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { const app = new Hono() .use(WorkspaceRouterMiddleware(upgrade)) + .route("/project", ProjectRoutes()) .route("/pty", PtyRoutes(upgrade)) + .route("/config", ConfigRoutes()) .route("/experimental", ExperimentalRoutes()) .route("/session", SessionRoutes()) .route("/permission", PermissionRoutes()) @@ -40,19 +42,19 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) { const handler = ExperimentalHttpApiServer.webHandler().handler const context = Context.empty() as Context.Context - app.all("/question", (c) => handler(c.req.raw, context)) - app.all("/question/*", (c) => handler(c.req.raw, context)) - app.all("/permission", (c) => handler(c.req.raw, context)) - app.all("/permission/*", (c) => handler(c.req.raw, context)) - app.all("/config/providers", (c) => handler(c.req.raw, context)) - app.all("/provider", (c) => handler(c.req.raw, context)) - app.all("/provider/auth", (c) => handler(c.req.raw, context)) - app.all("/provider/*", (c) => handler(c.req.raw, context)) + 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 - .route("/project", ProjectRoutes()) - .route("/config", ConfigRoutes()) .route("/question", QuestionRoutes()) .route("/provider", ProviderRoutes()) .route("/sync", SyncRoutes()) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index b68d43d2b826..d7bf43f506f8 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -510,6 +510,150 @@ export class App extends HeyApiClient { } } +export class Project extends HeyApiClient { + /** + * List all projects + * + * Get a list of projects that have been opened with OpenCode. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/project", + ...options, + ...params, + }) + } + + /** + * Get current project + * + * Retrieve the currently active project that OpenCode is working with. + */ + public current( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/project/current", + ...options, + ...params, + }) + } + + /** + * Initialize git repository + * + * Create a git repository for the current project and return the refreshed project info. + */ + public initGit( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/project/git/init", + ...options, + ...params, + }) + } + + /** + * Update project + * + * Update project properties such as name, icon, and commands. + */ + public update( + parameters: { + projectID: string + directory?: string + workspace?: string + name?: string + icon?: { + url?: string + override?: string + color?: string + } + commands?: { + /** + * Startup script to run when creating a new workspace (worktree) + */ + start?: string + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "projectID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "name" }, + { in: "body", key: "icon" }, + { in: "body", key: "commands" }, + ], + }, + ], + ) + return (options?.client ?? this.client).patch({ + url: "/project/{projectID}", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + export class Pty extends HeyApiClient { /** * List PTY sessions @@ -729,6 +873,105 @@ export class Pty extends HeyApiClient { } } +export class Config2 extends HeyApiClient { + /** + * Get configuration + * + * Retrieve the current OpenCode configuration settings and preferences. + */ + public get( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/config", + ...options, + ...params, + }) + } + + /** + * Update configuration + * + * Update OpenCode configuration settings and preferences. + */ + public update( + parameters?: { + directory?: string + workspace?: string + config?: Config3 + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { key: "config", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).patch({ + url: "/config", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * List config providers + * + * Get a list of all configured AI providers and their default models. + */ + public providers( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/config/providers", + ...options, + ...params, + }) + } +} + export class Console extends HeyApiClient { /** * Get active Console provider metadata @@ -2503,249 +2746,6 @@ export class Permission extends HeyApiClient { } } -export class Project extends HeyApiClient { - /** - * List all projects - * - * Get a list of projects that have been opened with OpenCode. - */ - public list( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/project", - ...options, - ...params, - }) - } - - /** - * Get current project - * - * Retrieve the currently active project that OpenCode is working with. - */ - public current( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/project/current", - ...options, - ...params, - }) - } - - /** - * Initialize git repository - * - * Create a git repository for the current project and return the refreshed project info. - */ - public initGit( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/project/git/init", - ...options, - ...params, - }) - } - - /** - * Update project - * - * Update project properties such as name, icon, and commands. - */ - public update( - parameters: { - projectID: string - directory?: string - workspace?: string - name?: string - icon?: { - url?: string - override?: string - color?: string - } - commands?: { - /** - * Startup script to run when creating a new workspace (worktree) - */ - start?: string - } - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "projectID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "name" }, - { in: "body", key: "icon" }, - { in: "body", key: "commands" }, - ], - }, - ], - ) - return (options?.client ?? this.client).patch({ - url: "/project/{projectID}", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } -} - -export class Config2 extends HeyApiClient { - /** - * Get configuration - * - * Retrieve the current OpenCode configuration settings and preferences. - */ - public get( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/config", - ...options, - ...params, - }) - } - - /** - * Update configuration - * - * Update OpenCode configuration settings and preferences. - */ - public update( - parameters?: { - directory?: string - workspace?: string - config?: Config3 - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { key: "config", map: "body" }, - ], - }, - ], - ) - return (options?.client ?? this.client).patch({ - url: "/config", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * List config providers - * - * Get a list of all configured AI providers and their default models. - */ - public providers( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/config/providers", - ...options, - ...params, - }) - } -} - export class Question extends HeyApiClient { /** * List pending questions @@ -4314,11 +4314,21 @@ export class OpencodeClient extends HeyApiClient { return (this._app ??= new App({ client: this.client })) } + private _project?: Project + get project(): Project { + return (this._project ??= new Project({ client: this.client })) + } + private _pty?: Pty get pty(): Pty { return (this._pty ??= new Pty({ client: this.client })) } + private _config?: Config2 + get config(): Config2 { + return (this._config ??= new Config2({ client: this.client })) + } + private _experimental?: Experimental get experimental(): Experimental { return (this._experimental ??= new Experimental({ client: this.client })) @@ -4349,16 +4359,6 @@ export class OpencodeClient extends HeyApiClient { return (this._permission ??= new Permission({ client: this.client })) } - private _project?: Project - get project(): Project { - return (this._project ??= new Project({ client: this.client })) - } - - private _config?: Config2 - get config(): Config2 { - return (this._config ??= new Config2({ client: this.client })) - } - private _question?: Question get question(): Question { return (this._question ??= new Question({ client: this.client })) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index c61dd6d2e999..25c3cfa66981 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1713,6 +1713,91 @@ export type NotFoundError = { } } +export type Model = { + id: string + providerID: string + api: { + id: string + url: string + npm: string + } + name: string + family?: string + capabilities: { + temperature: boolean + reasoning: boolean + attachment: boolean + toolcall: boolean + input: { + text: boolean + audio: boolean + image: boolean + video: boolean + pdf: boolean + } + output: { + text: boolean + audio: boolean + image: boolean + video: boolean + pdf: boolean + } + interleaved: + | boolean + | { + field: "reasoning_content" | "reasoning_details" + } + } + cost: { + input: number + output: number + cache: { + read: number + write: number + } + experimentalOver200K?: { + input: number + output: number + cache: { + read: number + write: number + } + } + } + limit: { + context: number + input?: number + output: number + } + status: "alpha" | "beta" | "deprecated" | "active" + options: { + [key: string]: unknown + } + headers: { + [key: string]: string + } + release_date: string + variants?: { + [key: string]: { + [key: string]: unknown + } + } +} + +export type Provider = { + id: string + name: string + source: "env" | "config" | "custom" | "api" + env: Array + key?: string + options: { + [key: string]: unknown + } + models: { + [key: string]: Model + } +} + export type ToolIds = Array export type ToolListItem = { @@ -1851,91 +1936,6 @@ export type SubtaskPartInput = { command?: string } -export type Model = { - id: string - providerID: string - api: { - id: string - url: string - npm: string - } - name: string - family?: string - capabilities: { - temperature: boolean - reasoning: boolean - attachment: boolean - toolcall: boolean - input: { - text: boolean - audio: boolean - image: boolean - video: boolean - pdf: boolean - } - output: { - text: boolean - audio: boolean - image: boolean - video: boolean - pdf: boolean - } - interleaved: - | boolean - | { - field: "reasoning_content" | "reasoning_details" - } - } - cost: { - input: number - output: number - cache: { - read: number - write: number - } - experimentalOver200K?: { - input: number - output: number - cache: { - read: number - write: number - } - } - } - limit: { - context: number - input?: number - output: number - } - status: "alpha" | "beta" | "deprecated" | "active" - options: { - [key: string]: unknown - } - headers: { - [key: string]: string - } - release_date: string - variants?: { - [key: string]: { - [key: string]: unknown - } - } -} - -export type Provider = { - id: string - name: string - source: "env" | "config" | "custom" | "api" - env: Array - key?: string - options: { - [key: string]: unknown - } - models: { - [key: string]: Model - } -} - export type ProviderAuthMethod = { type: "oauth" | "api" label: string @@ -2383,16 +2383,120 @@ export type AppLogErrors = { 400: BadRequestError } -export type AppLogError = AppLogErrors[keyof AppLogErrors] +export type AppLogError = AppLogErrors[keyof AppLogErrors] + +export type AppLogResponses = { + /** + * Log entry written successfully + */ + 200: boolean +} + +export type AppLogResponse = AppLogResponses[keyof AppLogResponses] + +export type ProjectListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/project" +} + +export type ProjectListResponses = { + /** + * List of projects + */ + 200: Array +} + +export type ProjectListResponse = ProjectListResponses[keyof ProjectListResponses] + +export type ProjectCurrentData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/project/current" +} + +export type ProjectCurrentResponses = { + /** + * Current project information + */ + 200: Project +} + +export type ProjectCurrentResponse = ProjectCurrentResponses[keyof ProjectCurrentResponses] + +export type ProjectInitGitData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/project/git/init" +} + +export type ProjectInitGitResponses = { + /** + * Project information after git initialization + */ + 200: Project +} + +export type ProjectInitGitResponse = ProjectInitGitResponses[keyof ProjectInitGitResponses] + +export type ProjectUpdateData = { + body?: { + name?: string + icon?: { + url?: string + override?: string + color?: string + } + commands?: { + /** + * Startup script to run when creating a new workspace (worktree) + */ + start?: string + } + } + path: { + projectID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/project/{projectID}" +} + +export type ProjectUpdateErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type ProjectUpdateError = ProjectUpdateErrors[keyof ProjectUpdateErrors] -export type AppLogResponses = { +export type ProjectUpdateResponses = { /** - * Log entry written successfully + * Updated project information */ - 200: boolean + 200: Project } -export type AppLogResponse = AppLogResponses[keyof AppLogResponses] +export type ProjectUpdateResponse = ProjectUpdateResponses[keyof ProjectUpdateResponses] export type PtyListData = { body?: never @@ -2575,6 +2679,77 @@ export type PtyConnectResponses = { export type PtyConnectResponse = PtyConnectResponses[keyof PtyConnectResponses] +export type ConfigGetData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/config" +} + +export type ConfigGetResponses = { + /** + * Get config info + */ + 200: Config +} + +export type ConfigGetResponse = ConfigGetResponses[keyof ConfigGetResponses] + +export type ConfigUpdateData = { + body?: Config + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/config" +} + +export type ConfigUpdateErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ConfigUpdateError = ConfigUpdateErrors[keyof ConfigUpdateErrors] + +export type ConfigUpdateResponses = { + /** + * Successfully updated config + */ + 200: Config +} + +export type ConfigUpdateResponse = ConfigUpdateResponses[keyof ConfigUpdateResponses] + +export type ConfigProvidersData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/config/providers" +} + +export type ConfigProvidersResponses = { + /** + * List of providers + */ + 200: { + providers: Array + default: { + [key: string]: string + } + } +} + +export type ConfigProvidersResponse = ConfigProvidersResponses[keyof ConfigProvidersResponses] + export type ExperimentalConsoleGetData = { body?: never path?: never @@ -4105,181 +4280,6 @@ export type PermissionListResponses = { export type PermissionListResponse = PermissionListResponses[keyof PermissionListResponses] -export type ProjectListData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/project" -} - -export type ProjectListResponses = { - /** - * List of projects - */ - 200: Array -} - -export type ProjectListResponse = ProjectListResponses[keyof ProjectListResponses] - -export type ProjectCurrentData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/project/current" -} - -export type ProjectCurrentResponses = { - /** - * Current project information - */ - 200: Project -} - -export type ProjectCurrentResponse = ProjectCurrentResponses[keyof ProjectCurrentResponses] - -export type ProjectInitGitData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/project/git/init" -} - -export type ProjectInitGitResponses = { - /** - * Project information after git initialization - */ - 200: Project -} - -export type ProjectInitGitResponse = ProjectInitGitResponses[keyof ProjectInitGitResponses] - -export type ProjectUpdateData = { - body?: { - name?: string - icon?: { - url?: string - override?: string - color?: string - } - commands?: { - /** - * Startup script to run when creating a new workspace (worktree) - */ - start?: string - } - } - path: { - projectID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/project/{projectID}" -} - -export type ProjectUpdateErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type ProjectUpdateError = ProjectUpdateErrors[keyof ProjectUpdateErrors] - -export type ProjectUpdateResponses = { - /** - * Updated project information - */ - 200: Project -} - -export type ProjectUpdateResponse = ProjectUpdateResponses[keyof ProjectUpdateResponses] - -export type ConfigGetData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/config" -} - -export type ConfigGetResponses = { - /** - * Get config info - */ - 200: Config -} - -export type ConfigGetResponse = ConfigGetResponses[keyof ConfigGetResponses] - -export type ConfigUpdateData = { - body?: Config - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/config" -} - -export type ConfigUpdateErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ConfigUpdateError = ConfigUpdateErrors[keyof ConfigUpdateErrors] - -export type ConfigUpdateResponses = { - /** - * Successfully updated config - */ - 200: Config -} - -export type ConfigUpdateResponse = ConfigUpdateResponses[keyof ConfigUpdateResponses] - -export type ConfigProvidersData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/config/providers" -} - -export type ConfigProvidersResponses = { - /** - * List of providers - */ - 200: { - providers: Array - default: { - [key: string]: string - } - } -} - -export type ConfigProvidersResponse = ConfigProvidersResponses[keyof ConfigProvidersResponses] - export type QuestionListData = { body?: never path?: never From 94b1760d9e123b24e0d23247412d721cbcd1c42e Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 22:47:08 -0400 Subject: [PATCH 3/4] refactor: use effect deepmutable in provider schema migration --- packages/opencode/src/provider/provider.ts | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index ea83e1528f9d..c4d7567dc4a5 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -20,7 +20,7 @@ import { zod } from "@/util/effect-zod" import { iife } from "@/util/iife" import { Global } from "../global" import path from "path" -import { Effect, Layer, Context, Schema } 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" @@ -32,14 +32,6 @@ import { ModelID, ProviderID } from "./schema" const log = Log.create({ service: "provider" }) -type Mutable = T extends string | number | boolean | bigint | symbol | null | undefined - ? T - : T extends ReadonlyArray - ? Mutable[] - : T extends object - ? { -readonly [K in keyof T]: Mutable } - : T - function shouldUseCopilotResponsesApi(modelID: string): boolean { const match = /^gpt-(\d+)/.exec(modelID) if (!match) return false @@ -878,7 +870,7 @@ export const Model = Schema.Struct({ }) .annotate({ identifier: "Model" }) .pipe(withStatics((s) => ({ zod: zod(s) }))) -export type Model = Mutable> +export type Model = Types.DeepMutable> export const Info = Schema.Struct({ id: ProviderID, @@ -891,7 +883,7 @@ export const Info = Schema.Struct({ }) .annotate({ identifier: "Provider" }) .pipe(withStatics((s) => ({ zod: zod(s) }))) -export type Info = Mutable> +export type Info = Types.DeepMutable> const DefaultModelIDs = Schema.Record(Schema.String, Schema.String) @@ -900,13 +892,13 @@ export const ListResult = Schema.Struct({ default: DefaultModelIDs, connected: Schema.Array(Schema.String), }).pipe(withStatics((s) => ({ zod: zod(s) }))) -export type ListResult = Mutable> +export type ListResult = Types.DeepMutable> export const ConfigProvidersResult = Schema.Struct({ providers: Schema.Array(Info), default: DefaultModelIDs, }).pipe(withStatics((s) => ({ zod: zod(s) }))) -export type ConfigProvidersResult = Mutable> +export type ConfigProvidersResult = Types.DeepMutable> export interface Interface { readonly list: () => Effect.Effect> From fa2fdc28dca871bb6def5829543dc624542a83a6 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 23:03:10 -0400 Subject: [PATCH 4/4] refactor: reuse provider default model selection --- packages/opencode/src/provider/provider.ts | 4 ++++ packages/opencode/src/server/instance/config.ts | 5 ++--- packages/opencode/src/server/instance/httpapi/config.ts | 5 ++--- packages/opencode/src/server/instance/httpapi/provider.ts | 2 +- packages/opencode/src/server/instance/provider.ts | 2 +- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index c4d7567dc4a5..711481d80ac2 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -900,6 +900,10 @@ export const ConfigProvidersResult = Schema.Struct({ }).pipe(withStatics((s) => ({ zod: zod(s) }))) export type ConfigProvidersResult = Types.DeepMutable> +export function defaultModelIDs }>(providers: Record) { + return mapValues(providers, (item) => sort(Object.values(item.models))[0].id) +} + export interface Interface { readonly list: () => Effect.Effect> readonly getProvider: (providerID: ProviderID) => Effect.Effect diff --git a/packages/opencode/src/server/instance/config.ts b/packages/opencode/src/server/instance/config.ts index 27a2718a6b68..15c393fe5a0a 100644 --- a/packages/opencode/src/server/instance/config.ts +++ b/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" @@ -79,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), } }), ), diff --git a/packages/opencode/src/server/instance/httpapi/config.ts b/packages/opencode/src/server/instance/httpapi/config.ts index 8a249e423062..14aa94f9fcf0 100644 --- a/packages/opencode/src/server/instance/httpapi/config.ts +++ b/packages/opencode/src/server/instance/httpapi/config.ts @@ -1,6 +1,5 @@ import { Config } from "@/config" import { Provider } from "@/provider" -import { mapValues } from "remeda" import { Effect, Layer } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" @@ -40,10 +39,10 @@ export const configHandlers = Layer.unwrap( const svc = yield* Provider.Service const providers = Effect.fn("ConfigHttpApi.providers")(function* () { - 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), } }) diff --git a/packages/opencode/src/server/instance/httpapi/provider.ts b/packages/opencode/src/server/instance/httpapi/provider.ts index 5eda039b3289..67831a1fafb2 100644 --- a/packages/opencode/src/server/instance/httpapi/provider.ts +++ b/packages/opencode/src/server/instance/httpapi/provider.ts @@ -93,7 +93,7 @@ export const providerHandlers = Layer.unwrap( ) 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), } }) diff --git a/packages/opencode/src/server/instance/provider.ts b/packages/opencode/src/server/instance/provider.ts index 199509886f76..a81ae00d5908 100644 --- a/packages/opencode/src/server/instance/provider.ts +++ b/packages/opencode/src/server/instance/provider.ts @@ -53,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), } }),