diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index d9b7fac2fa..4013dcee36 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -935,16 +935,6 @@ export const ConfigProvidersResult = Schema.Struct({ }).pipe(withStatics((s) => ({ zod: zod(s) }))) export type ConfigProvidersResult = Types.DeepMutable> -export function toPublicInfo(provider: Info): Info { - return JSON.parse( - JSON.stringify(provider, (_, value) => { - if (typeof value === "function" || typeof value === "symbol" || value === undefined) return undefined - if (typeof value === "bigint") return value.toString() - return value - }), - ) -} - export function defaultModelIDs }>(providers: Record) { return mapValues(providers, (item) => sort(Object.values(item.models))[0].id) } @@ -1309,7 +1299,7 @@ const layer: Layer.Layer< const options = yield* Effect.promise(() => plugin.auth!.loader!( () => bridge.promise(auth.get(providerID).pipe(Effect.orDie)) as any, - toPublicInfo(database[plugin.auth!.provider]), + database[plugin.auth!.provider], ), ) const opts = options ?? {} diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/config.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/config.ts index 3d0e8a06c0..753ba03138 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/config.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/config.ts @@ -24,7 +24,7 @@ export const configHandlers = HttpApiBuilder.group(InstanceHttpApi, "config", (h const providers = Effect.fn("ConfigHttpApi.providers")(function* () { const providers = yield* providerSvc.list() return { - providers: Object.values(providers).map(Provider.toPublicInfo), + providers: Object.values(providers), default: Provider.defaultModelIDs(providers), } }) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/provider.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/provider.ts index 7027e666ca..15049fcc55 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/provider.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/provider.ts @@ -30,7 +30,7 @@ export const providerHandlers = HttpApiBuilder.group(InstanceHttpApi, "provider" connected, ) return { - all: Object.values(providers).map(Provider.toPublicInfo), + all: Object.values(providers), default: Provider.defaultModelIDs(providers), connected: Object.keys(connected), } diff --git a/packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts b/packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts index 6f3c33a647..5a018f16da 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts @@ -6,13 +6,39 @@ import { NamedError } from "@opencode-ai/core/util/error" import * as Log from "@opencode-ai/core/util/log" import { Cause, Effect } from "effect" import { HttpRouter, HttpServerError, HttpServerRespondable, HttpServerResponse } from "effect/unstable/http" +import { HttpApiError } from "effect/unstable/httpapi" +import { HttpApiSchemaError } from "effect/unstable/httpapi/HttpApiError" const log = Log.create({ service: "server" }) +function badRequestResponse() { + return HttpServerResponse.jsonUnsafe( + { + data: {}, + errors: [], + success: false, + }, + { status: 400 }, + ) +} + +function normalizeEmptyBadRequest(response: HttpServerResponse.HttpServerResponse) { + if (response.status !== 400 || response.body._tag !== "Empty") return response + return badRequestResponse() +} + // Keep typed HttpApi failures on their declared error path; this boundary only replaces defect-only empty 500s. export const errorLayer = HttpRouter.middleware<{ handles: unknown }>()((effect) => effect.pipe( + Effect.catch((error) => { + if (error instanceof HttpApiError.BadRequest) return Effect.succeed(badRequestResponse()) + return Effect.fail(error) + }), + Effect.map(normalizeEmptyBadRequest), Effect.catchCause((cause) => { + const schemaError = cause.reasons.filter(Cause.isDieReason).find((reason) => HttpApiSchemaError.is(reason.defect)) + if (schemaError) return Effect.succeed(badRequestResponse()) + const defect = cause.reasons.filter(Cause.isDieReason).find((reason) => { if (HttpServerResponse.isHttpServerResponse(reason.defect)) return false if (HttpServerError.isHttpServerError(reason.defect)) return false diff --git a/packages/opencode/test/server/httpapi-provider.test.ts b/packages/opencode/test/server/httpapi-provider.test.ts index d44ff4cd60..12262e30ed 100644 --- a/packages/opencode/test/server/httpapi-provider.test.ts +++ b/packages/opencode/test/server/httpapi-provider.test.ts @@ -21,22 +21,6 @@ function app() { return Server.Default().app } -function providerListHasFetch(list: unknown) { - if (!Array.isArray(list)) return false - return list.some((item: unknown) => { - if (typeof item !== "object" || item === null || !("id" in item) || !("options" in item)) return false - if (item.id !== "google") return false - if (typeof item.options !== "object" || item.options === null) return false - return "fetch" in item.options - }) -} - -function hasProviderWithFetch(input: unknown, key: "all" | "providers") { - if (typeof input !== "object" || input === null) return false - if (key === "all") return "all" in input && providerListHasFetch(input.all) - return "providers" in input && providerListHasFetch(input.providers) -} - function requestAuthorize(input: { app: ReturnType providerID: string @@ -92,39 +76,6 @@ function writeProviderAuthPlugin(dir: string) { }) } -function writeFunctionOptionsPlugin(dir: string) { - return Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem - const path = yield* Path.Path - - yield* fs.makeDirectory(path.join(dir, ".opencode", "plugin"), { recursive: true }) - yield* fs.writeFileString( - path.join(dir, ".opencode", "plugin", "provider-function-options.ts"), - [ - "export default {", - ' id: "test.provider-function-options",', - " server: async () => ({", - " auth: {", - ' provider: "google",', - " loader: async (_getAuth, provider) => {", - " for (const model of Object.values(provider.models ?? {})) {", - " model.cost = { input: 0, output: 0 }", - " }", - " return {", - ' apiKey: "",', - " fetch: async (input, init) => fetch(input, init),", - " }", - " },", - " methods: [{ type: 'api', label: 'API key' }],", - " },", - " }),", - "}", - "", - ].join("\n"), - ) - }) -} - function withProviderProject(self: (dir: string) => Effect.Effect) { return Effect.gen(function* () { const fs = yield* FileSystem.FileSystem @@ -185,43 +136,4 @@ describe("provider HttpApi", () => { }), ), ) - - it.live("serves provider lists when auth loaders add runtime fetch options", () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem - const path = yield* Path.Path - const dir = yield* fs.makeTempDirectoryScoped({ prefix: "opencode-test-" }) - const previous = process.env.OPENCODE_AUTH_CONTENT - - yield* fs.writeFileString( - path.join(dir, "opencode.json"), - JSON.stringify({ $schema: "https://opencode.ai/config.json", formatter: false, lsp: false }), - ) - yield* writeFunctionOptionsPlugin(dir) - yield* Effect.sync(() => { - process.env.OPENCODE_AUTH_CONTENT = JSON.stringify({ - google: { type: "oauth", refresh: "dummy", access: "dummy", expires: 9999999999999 }, - }) - }) - yield* Effect.addFinalizer(() => - Effect.sync(() => { - if (previous === undefined) delete process.env.OPENCODE_AUTH_CONTENT - if (previous !== undefined) process.env.OPENCODE_AUTH_CONTENT = previous - }), - ) - const headers = { "x-opencode-directory": dir } - const providerResponse = yield* Effect.promise(() => Promise.resolve(app().request("/provider", { headers }))) - const configResponse = yield* Effect.promise(() => - Promise.resolve(app().request("/config/providers", { headers })), - ) - - expect(providerResponse.status).toBe(200) - expect(configResponse.status).toBe(200) - - const providerBody = yield* Effect.promise(() => providerResponse.json()) - const configBody = yield* Effect.promise(() => configResponse.json()) - expect(hasProviderWithFetch(providerBody, "all")).toBe(false) - expect(hasProviderWithFetch(configBody, "providers")).toBe(false) - }), - ) })