Compare commits
1 commit
dev
...
kit/revert
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e53975314 |
5 changed files with 29 additions and 101 deletions
|
|
@ -935,16 +935,6 @@ export const ConfigProvidersResult = Schema.Struct({
|
||||||
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||||
export type ConfigProvidersResult = Types.DeepMutable<Schema.Schema.Type<typeof ConfigProvidersResult>>
|
export type ConfigProvidersResult = Types.DeepMutable<Schema.Schema.Type<typeof ConfigProvidersResult>>
|
||||||
|
|
||||||
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<T extends { models: Record<string, { id: string }> }>(providers: Record<string, T>) {
|
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)
|
return mapValues(providers, (item) => sort(Object.values(item.models))[0].id)
|
||||||
}
|
}
|
||||||
|
|
@ -1309,7 +1299,7 @@ const layer: Layer.Layer<
|
||||||
const options = yield* Effect.promise(() =>
|
const options = yield* Effect.promise(() =>
|
||||||
plugin.auth!.loader!(
|
plugin.auth!.loader!(
|
||||||
() => bridge.promise(auth.get(providerID).pipe(Effect.orDie)) as any,
|
() => bridge.promise(auth.get(providerID).pipe(Effect.orDie)) as any,
|
||||||
toPublicInfo(database[plugin.auth!.provider]),
|
database[plugin.auth!.provider],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
const opts = options ?? {}
|
const opts = options ?? {}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export const configHandlers = HttpApiBuilder.group(InstanceHttpApi, "config", (h
|
||||||
const providers = Effect.fn("ConfigHttpApi.providers")(function* () {
|
const providers = Effect.fn("ConfigHttpApi.providers")(function* () {
|
||||||
const providers = yield* providerSvc.list()
|
const providers = yield* providerSvc.list()
|
||||||
return {
|
return {
|
||||||
providers: Object.values(providers).map(Provider.toPublicInfo),
|
providers: Object.values(providers),
|
||||||
default: Provider.defaultModelIDs(providers),
|
default: Provider.defaultModelIDs(providers),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export const providerHandlers = HttpApiBuilder.group(InstanceHttpApi, "provider"
|
||||||
connected,
|
connected,
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
all: Object.values(providers).map(Provider.toPublicInfo),
|
all: Object.values(providers),
|
||||||
default: Provider.defaultModelIDs(providers),
|
default: Provider.defaultModelIDs(providers),
|
||||||
connected: Object.keys(connected),
|
connected: Object.keys(connected),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,39 @@ import { NamedError } from "@opencode-ai/core/util/error"
|
||||||
import * as Log from "@opencode-ai/core/util/log"
|
import * as Log from "@opencode-ai/core/util/log"
|
||||||
import { Cause, Effect } from "effect"
|
import { Cause, Effect } from "effect"
|
||||||
import { HttpRouter, HttpServerError, HttpServerRespondable, HttpServerResponse } from "effect/unstable/http"
|
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" })
|
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.
|
// 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) =>
|
export const errorLayer = HttpRouter.middleware<{ handles: unknown }>()((effect) =>
|
||||||
effect.pipe(
|
effect.pipe(
|
||||||
|
Effect.catch((error) => {
|
||||||
|
if (error instanceof HttpApiError.BadRequest) return Effect.succeed(badRequestResponse())
|
||||||
|
return Effect.fail(error)
|
||||||
|
}),
|
||||||
|
Effect.map(normalizeEmptyBadRequest),
|
||||||
Effect.catchCause((cause) => {
|
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) => {
|
const defect = cause.reasons.filter(Cause.isDieReason).find((reason) => {
|
||||||
if (HttpServerResponse.isHttpServerResponse(reason.defect)) return false
|
if (HttpServerResponse.isHttpServerResponse(reason.defect)) return false
|
||||||
if (HttpServerError.isHttpServerError(reason.defect)) return false
|
if (HttpServerError.isHttpServerError(reason.defect)) return false
|
||||||
|
|
|
||||||
|
|
@ -21,22 +21,6 @@ function app() {
|
||||||
return Server.Default().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: {
|
function requestAuthorize(input: {
|
||||||
app: ReturnType<typeof app>
|
app: ReturnType<typeof app>
|
||||||
providerID: string
|
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<A, E, R>(self: (dir: string) => Effect.Effect<A, E, R>) {
|
function withProviderProject<A, E, R>(self: (dir: string) => Effect.Effect<A, E, R>) {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const fs = yield* FileSystem.FileSystem
|
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)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue