refactor: move provider and config provider routes onto HttpApi (#23004)
This commit is contained in:
parent
c51f3e35ca
commit
ee7339f2c6
8 changed files with 323 additions and 158 deletions
|
|
@ -58,6 +58,18 @@ export class Authorization extends Schema.Class<Authorization>("ProviderAuthAuth
|
||||||
static readonly zod = zod(this)
|
static readonly zod = zod(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const AuthorizeInput = Schema.Struct({
|
||||||
|
method: Schema.Number.annotate({ description: "Auth method index" }),
|
||||||
|
inputs: Schema.optional(Schema.Record(Schema.String, Schema.String)).annotate({ description: "Prompt inputs" }),
|
||||||
|
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||||
|
export type AuthorizeInput = Schema.Schema.Type<typeof AuthorizeInput>
|
||||||
|
|
||||||
|
export const CallbackInput = Schema.Struct({
|
||||||
|
method: Schema.Number.annotate({ description: "Auth method index" }),
|
||||||
|
code: Schema.optional(Schema.String).annotate({ description: "OAuth authorization code" }),
|
||||||
|
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||||
|
export type CallbackInput = Schema.Schema.Type<typeof CallbackInput>
|
||||||
|
|
||||||
export const OauthMissing = NamedError.create("ProviderAuthOauthMissing", z.object({ providerID: ProviderID.zod }))
|
export const OauthMissing = NamedError.create("ProviderAuthOauthMissing", z.object({ providerID: ProviderID.zod }))
|
||||||
|
|
||||||
export const OauthCodeMissing = NamedError.create(
|
export const OauthCodeMissing = NamedError.create(
|
||||||
|
|
@ -86,12 +98,12 @@ type Hook = NonNullable<Hooks["auth"]>
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly methods: () => Effect.Effect<Methods>
|
readonly methods: () => Effect.Effect<Methods>
|
||||||
readonly authorize: (input: {
|
readonly authorize: (
|
||||||
providerID: ProviderID
|
input: {
|
||||||
method: number
|
providerID: ProviderID
|
||||||
inputs?: Record<string, string>
|
} & AuthorizeInput,
|
||||||
}) => Effect.Effect<Authorization | undefined, Error>
|
) => Effect.Effect<Authorization | undefined, Error>
|
||||||
readonly callback: (input: { providerID: ProviderID; method: number; code?: string }) => Effect.Effect<void, Error>
|
readonly callback: (input: { providerID: ProviderID } & CallbackInput) => Effect.Effect<void, Error>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
|
@ -153,11 +165,9 @@ export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> =
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const authorize = Effect.fn("ProviderAuth.authorize")(function* (input: {
|
const authorize = Effect.fn("ProviderAuth.authorize")(function* (
|
||||||
providerID: ProviderID
|
input: { providerID: ProviderID } & AuthorizeInput,
|
||||||
method: number
|
) {
|
||||||
inputs?: Record<string, string>
|
|
||||||
}) {
|
|
||||||
const { hooks, pending } = yield* InstanceState.get(state)
|
const { hooks, pending } = yield* InstanceState.get(state)
|
||||||
const method = hooks[input.providerID].methods[input.method]
|
const method = hooks[input.providerID].methods[input.method]
|
||||||
if (method.type !== "oauth") return
|
if (method.type !== "oauth") return
|
||||||
|
|
@ -180,11 +190,7 @@ export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> =
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const callback = Effect.fn("ProviderAuth.callback")(function* (input: {
|
const callback = Effect.fn("ProviderAuth.callback")(function* (input: { providerID: ProviderID } & CallbackInput) {
|
||||||
providerID: ProviderID
|
|
||||||
method: number
|
|
||||||
code?: string
|
|
||||||
}) {
|
|
||||||
const pending = (yield* InstanceState.get(state)).pending
|
const pending = (yield* InstanceState.get(state)).pending
|
||||||
const match = pending.get(input.providerID)
|
const match = pending.get(input.providerID)
|
||||||
if (!match) return yield* Effect.fail(new OauthMissing({ providerID: input.providerID }))
|
if (!match) return yield* Effect.fail(new OauthMissing({ providerID: input.providerID }))
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,16 @@ import { Env } from "../env"
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
import { InstallationVersion } from "../installation/version"
|
import { InstallationVersion } from "../installation/version"
|
||||||
import { Flag } from "../flag/flag"
|
import { Flag } from "../flag/flag"
|
||||||
|
import { zod } from "@/util/effect-zod"
|
||||||
import { iife } from "@/util/iife"
|
import { iife } from "@/util/iife"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect, Layer, Context } from "effect"
|
import { Effect, Layer, Context, Schema, Types } from "effect"
|
||||||
import { EffectBridge } from "@/effect"
|
import { EffectBridge } from "@/effect"
|
||||||
import { InstanceState } from "@/effect"
|
import { InstanceState } from "@/effect"
|
||||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||||
import { isRecord } from "@/util/record"
|
import { isRecord } from "@/util/record"
|
||||||
|
import { withStatics } from "@/util/schema"
|
||||||
|
|
||||||
import * as ProviderTransform from "./transform"
|
import * as ProviderTransform from "./transform"
|
||||||
import { ModelID, ProviderID } from "./schema"
|
import { ModelID, ProviderID } from "./schema"
|
||||||
|
|
@ -796,91 +798,111 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Model = z
|
const ProviderApiInfo = Schema.Struct({
|
||||||
.object({
|
id: Schema.String,
|
||||||
id: ModelID.zod,
|
url: Schema.String,
|
||||||
providerID: ProviderID.zod,
|
npm: Schema.String,
|
||||||
api: z.object({
|
})
|
||||||
id: z.string(),
|
|
||||||
url: z.string(),
|
|
||||||
npm: z.string(),
|
|
||||||
}),
|
|
||||||
name: z.string(),
|
|
||||||
family: z.string().optional(),
|
|
||||||
capabilities: z.object({
|
|
||||||
temperature: z.boolean(),
|
|
||||||
reasoning: z.boolean(),
|
|
||||||
attachment: z.boolean(),
|
|
||||||
toolcall: z.boolean(),
|
|
||||||
input: z.object({
|
|
||||||
text: z.boolean(),
|
|
||||||
audio: z.boolean(),
|
|
||||||
image: z.boolean(),
|
|
||||||
video: z.boolean(),
|
|
||||||
pdf: z.boolean(),
|
|
||||||
}),
|
|
||||||
output: z.object({
|
|
||||||
text: z.boolean(),
|
|
||||||
audio: z.boolean(),
|
|
||||||
image: z.boolean(),
|
|
||||||
video: z.boolean(),
|
|
||||||
pdf: z.boolean(),
|
|
||||||
}),
|
|
||||||
interleaved: z.union([
|
|
||||||
z.boolean(),
|
|
||||||
z.object({
|
|
||||||
field: z.enum(["reasoning_content", "reasoning_details"]),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
cost: z.object({
|
|
||||||
input: z.number(),
|
|
||||||
output: z.number(),
|
|
||||||
cache: z.object({
|
|
||||||
read: z.number(),
|
|
||||||
write: z.number(),
|
|
||||||
}),
|
|
||||||
experimentalOver200K: z
|
|
||||||
.object({
|
|
||||||
input: z.number(),
|
|
||||||
output: z.number(),
|
|
||||||
cache: z.object({
|
|
||||||
read: z.number(),
|
|
||||||
write: z.number(),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
}),
|
|
||||||
limit: z.object({
|
|
||||||
context: z.number(),
|
|
||||||
input: z.number().optional(),
|
|
||||||
output: z.number(),
|
|
||||||
}),
|
|
||||||
status: z.enum(["alpha", "beta", "deprecated", "active"]),
|
|
||||||
options: z.record(z.string(), z.any()),
|
|
||||||
headers: z.record(z.string(), z.string()),
|
|
||||||
release_date: z.string(),
|
|
||||||
variants: z.record(z.string(), z.record(z.string(), z.any())).optional(),
|
|
||||||
})
|
|
||||||
.meta({
|
|
||||||
ref: "Model",
|
|
||||||
})
|
|
||||||
export type Model = z.infer<typeof Model>
|
|
||||||
|
|
||||||
export const Info = z
|
const ProviderModalities = Schema.Struct({
|
||||||
.object({
|
text: Schema.Boolean,
|
||||||
id: ProviderID.zod,
|
audio: Schema.Boolean,
|
||||||
name: z.string(),
|
image: Schema.Boolean,
|
||||||
source: z.enum(["env", "config", "custom", "api"]),
|
video: Schema.Boolean,
|
||||||
env: z.string().array(),
|
pdf: Schema.Boolean,
|
||||||
key: z.string().optional(),
|
})
|
||||||
options: z.record(z.string(), z.any()),
|
|
||||||
models: z.record(z.string(), Model),
|
const ProviderInterleaved = Schema.Union([
|
||||||
})
|
Schema.Boolean,
|
||||||
.meta({
|
Schema.Struct({
|
||||||
ref: "Provider",
|
field: Schema.Literals(["reasoning_content", "reasoning_details"]),
|
||||||
})
|
}),
|
||||||
export type Info = z.infer<typeof Info>
|
])
|
||||||
|
|
||||||
|
const ProviderCapabilities = Schema.Struct({
|
||||||
|
temperature: Schema.Boolean,
|
||||||
|
reasoning: Schema.Boolean,
|
||||||
|
attachment: Schema.Boolean,
|
||||||
|
toolcall: Schema.Boolean,
|
||||||
|
input: ProviderModalities,
|
||||||
|
output: ProviderModalities,
|
||||||
|
interleaved: ProviderInterleaved,
|
||||||
|
})
|
||||||
|
|
||||||
|
const ProviderCacheCost = Schema.Struct({
|
||||||
|
read: Schema.Number,
|
||||||
|
write: Schema.Number,
|
||||||
|
})
|
||||||
|
|
||||||
|
const ProviderCost = Schema.Struct({
|
||||||
|
input: Schema.Number,
|
||||||
|
output: Schema.Number,
|
||||||
|
cache: ProviderCacheCost,
|
||||||
|
experimentalOver200K: Schema.optional(
|
||||||
|
Schema.Struct({
|
||||||
|
input: Schema.Number,
|
||||||
|
output: Schema.Number,
|
||||||
|
cache: ProviderCacheCost,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
const ProviderLimit = Schema.Struct({
|
||||||
|
context: Schema.Number,
|
||||||
|
input: Schema.optional(Schema.Number),
|
||||||
|
output: Schema.Number,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Model = Schema.Struct({
|
||||||
|
id: ModelID,
|
||||||
|
providerID: ProviderID,
|
||||||
|
api: ProviderApiInfo,
|
||||||
|
name: Schema.String,
|
||||||
|
family: Schema.optional(Schema.String),
|
||||||
|
capabilities: ProviderCapabilities,
|
||||||
|
cost: ProviderCost,
|
||||||
|
limit: ProviderLimit,
|
||||||
|
status: Schema.Literals(["alpha", "beta", "deprecated", "active"]),
|
||||||
|
options: Schema.Record(Schema.String, Schema.Any),
|
||||||
|
headers: Schema.Record(Schema.String, Schema.String),
|
||||||
|
release_date: Schema.String,
|
||||||
|
variants: Schema.optional(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Any))),
|
||||||
|
})
|
||||||
|
.annotate({ identifier: "Model" })
|
||||||
|
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||||
|
export type Model = Types.DeepMutable<Schema.Schema.Type<typeof Model>>
|
||||||
|
|
||||||
|
export const Info = Schema.Struct({
|
||||||
|
id: ProviderID,
|
||||||
|
name: Schema.String,
|
||||||
|
source: Schema.Literals(["env", "config", "custom", "api"]),
|
||||||
|
env: Schema.Array(Schema.String),
|
||||||
|
key: Schema.optional(Schema.String),
|
||||||
|
options: Schema.Record(Schema.String, Schema.Any),
|
||||||
|
models: Schema.Record(Schema.String, Model),
|
||||||
|
})
|
||||||
|
.annotate({ identifier: "Provider" })
|
||||||
|
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||||
|
export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>
|
||||||
|
|
||||||
|
const DefaultModelIDs = Schema.Record(Schema.String, Schema.String)
|
||||||
|
|
||||||
|
export const ListResult = Schema.Struct({
|
||||||
|
all: Schema.Array(Info),
|
||||||
|
default: DefaultModelIDs,
|
||||||
|
connected: Schema.Array(Schema.String),
|
||||||
|
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||||
|
export type ListResult = Types.DeepMutable<Schema.Schema.Type<typeof ListResult>>
|
||||||
|
|
||||||
|
export const ConfigProvidersResult = Schema.Struct({
|
||||||
|
providers: Schema.Array(Info),
|
||||||
|
default: DefaultModelIDs,
|
||||||
|
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||||
|
export type ConfigProvidersResult = Types.DeepMutable<Schema.Schema.Type<typeof ConfigProvidersResult>>
|
||||||
|
|
||||||
|
export function defaultModelIDs<T extends { models: Record<string, { id: string }> }>(providers: Record<string, T>) {
|
||||||
|
return mapValues(providers, (item) => sort(Object.values(item.models))[0].id)
|
||||||
|
}
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly list: () => Effect.Effect<Record<ProviderID, Info>>
|
readonly list: () => Effect.Effect<Record<ProviderID, Info>>
|
||||||
|
|
@ -928,7 +950,7 @@ function cost(c: ModelsDev.Model["cost"]): Model["cost"] {
|
||||||
}
|
}
|
||||||
|
|
||||||
function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
|
function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
|
||||||
const m: Model = {
|
const base: Model = {
|
||||||
id: ModelID.make(model.id),
|
id: ModelID.make(model.id),
|
||||||
providerID: ProviderID.make(provider.id),
|
providerID: ProviderID.make(provider.id),
|
||||||
name: model.name,
|
name: model.name,
|
||||||
|
|
@ -972,9 +994,10 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model
|
||||||
variants: {},
|
variants: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
m.variants = mapValues(ProviderTransform.variants(m), (v) => v)
|
return {
|
||||||
|
...base,
|
||||||
return m
|
variants: mapValues(ProviderTransform.variants(base), (v) => v),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
|
export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
|
||||||
|
|
@ -983,17 +1006,22 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
|
||||||
models[key] = fromModelsDevModel(provider, model)
|
models[key] = fromModelsDevModel(provider, model)
|
||||||
for (const [mode, opts] of Object.entries(model.experimental?.modes ?? {})) {
|
for (const [mode, opts] of Object.entries(model.experimental?.modes ?? {})) {
|
||||||
const id = `${model.id}-${mode}`
|
const id = `${model.id}-${mode}`
|
||||||
const m = fromModelsDevModel(provider, model)
|
const base = fromModelsDevModel(provider, model)
|
||||||
m.id = ModelID.make(id)
|
models[id] = {
|
||||||
m.name = `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}`
|
...base,
|
||||||
if (opts.cost) m.cost = mergeDeep(m.cost, cost(opts.cost))
|
id: ModelID.make(id),
|
||||||
// convert body params to camelCase for ai sdk compatibility
|
name: `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}`,
|
||||||
if (opts.provider?.body)
|
cost: opts.cost ? mergeDeep(base.cost, cost(opts.cost)) : base.cost,
|
||||||
m.options = Object.fromEntries(
|
options: opts.provider?.body
|
||||||
Object.entries(opts.provider.body).map(([k, v]) => [k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()), v]),
|
? Object.fromEntries(
|
||||||
)
|
Object.entries(opts.provider.body).map(([k, v]) => [
|
||||||
if (opts.provider?.headers) m.headers = opts.provider.headers
|
k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()),
|
||||||
models[id] = m
|
v,
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
: base.options,
|
||||||
|
headers: opts.provider?.headers ?? base.headers,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { describeRoute, validator, resolver } from "hono-openapi"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Config } from "../../config"
|
import { Config } from "../../config"
|
||||||
import { Provider } from "../../provider"
|
import { Provider } from "../../provider"
|
||||||
import { mapValues } from "remeda"
|
|
||||||
import { errors } from "../error"
|
import { errors } from "../error"
|
||||||
import { lazy } from "../../util/lazy"
|
import { lazy } from "../../util/lazy"
|
||||||
import { AppRuntime } from "../../effect/app-runtime"
|
import { AppRuntime } from "../../effect/app-runtime"
|
||||||
|
|
@ -70,12 +69,7 @@ export const ConfigRoutes = lazy(() =>
|
||||||
description: "List of providers",
|
description: "List of providers",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: resolver(
|
schema: resolver(Provider.ConfigProvidersResult.zod),
|
||||||
z.object({
|
|
||||||
providers: Provider.Info.array(),
|
|
||||||
default: z.record(z.string(), z.string()),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -84,10 +78,10 @@ export const ConfigRoutes = lazy(() =>
|
||||||
async (c) =>
|
async (c) =>
|
||||||
jsonRequest("ConfigRoutes.providers", c, function* () {
|
jsonRequest("ConfigRoutes.providers", c, function* () {
|
||||||
const svc = yield* Provider.Service
|
const svc = yield* Provider.Service
|
||||||
const providers = mapValues(yield* svc.list(), (item) => item)
|
const providers = yield* svc.list()
|
||||||
return {
|
return {
|
||||||
providers: Object.values(providers),
|
providers: Object.values(providers),
|
||||||
default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
|
default: Provider.defaultModelIDs(providers),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
51
packages/opencode/src/server/instance/httpapi/config.ts
Normal file
51
packages/opencode/src/server/instance/httpapi/config.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { Config } from "@/config"
|
||||||
|
import { Provider } from "@/provider"
|
||||||
|
import { Effect, Layer } from "effect"
|
||||||
|
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
|
|
||||||
|
const root = "/config"
|
||||||
|
|
||||||
|
export const ConfigApi = HttpApi.make("config")
|
||||||
|
.add(
|
||||||
|
HttpApiGroup.make("config")
|
||||||
|
.add(
|
||||||
|
HttpApiEndpoint.get("providers", `${root}/providers`, {
|
||||||
|
success: Provider.ConfigProvidersResult,
|
||||||
|
}).annotateMerge(
|
||||||
|
OpenApi.annotations({
|
||||||
|
identifier: "config.providers",
|
||||||
|
summary: "List config providers",
|
||||||
|
description: "Get a list of all configured AI providers and their default models.",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.annotateMerge(
|
||||||
|
OpenApi.annotations({
|
||||||
|
title: "config",
|
||||||
|
description: "Experimental HttpApi config routes.",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.annotateMerge(
|
||||||
|
OpenApi.annotations({
|
||||||
|
title: "opencode experimental HttpApi",
|
||||||
|
version: "0.0.1",
|
||||||
|
description: "Experimental HttpApi surface for selected instance routes.",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const configHandlers = Layer.unwrap(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const svc = yield* Provider.Service
|
||||||
|
|
||||||
|
const providers = Effect.fn("ConfigHttpApi.providers")(function* () {
|
||||||
|
const providers = yield* svc.list()
|
||||||
|
return {
|
||||||
|
providers: Object.values(providers),
|
||||||
|
default: Provider.defaultModelIDs(providers),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return HttpApiBuilder.group(ConfigApi, "config", (handlers) => handlers.handle("providers", providers))
|
||||||
|
}),
|
||||||
|
).pipe(Layer.provide(Provider.defaultLayer), Layer.provide(Config.defaultLayer))
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
import { ProviderAuth } from "@/provider"
|
import { ProviderAuth } from "@/provider"
|
||||||
import { Effect, Layer } from "effect"
|
import { Config } from "@/config"
|
||||||
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
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"
|
const root = "/provider"
|
||||||
|
|
||||||
|
|
@ -8,6 +13,15 @@ export const ProviderApi = HttpApi.make("provider")
|
||||||
.add(
|
.add(
|
||||||
HttpApiGroup.make("provider")
|
HttpApiGroup.make("provider")
|
||||||
.add(
|
.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`, {
|
HttpApiEndpoint.get("auth", `${root}/auth`, {
|
||||||
success: ProviderAuth.Methods,
|
success: ProviderAuth.Methods,
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
|
|
@ -17,6 +31,28 @@ export const ProviderApi = HttpApi.make("provider")
|
||||||
description: "Retrieve available authentication methods for all AI providers.",
|
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(
|
.annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
|
|
@ -35,12 +71,72 @@ export const ProviderApi = HttpApi.make("provider")
|
||||||
|
|
||||||
export const providerHandlers = Layer.unwrap(
|
export const providerHandlers = Layer.unwrap(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
const cfg = yield* Config.Service
|
||||||
|
const provider = yield* Provider.Service
|
||||||
const svc = yield* ProviderAuth.Service
|
const svc = yield* ProviderAuth.Service
|
||||||
|
|
||||||
|
const list = Effect.fn("ProviderHttpApi.list")(function* () {
|
||||||
|
const config = yield* cfg.get()
|
||||||
|
const all = yield* Effect.promise(() => ModelsDev.get())
|
||||||
|
const disabled = new Set(config.disabled_providers ?? [])
|
||||||
|
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
|
||||||
|
const filtered: Record<string, (typeof all)[string]> = {}
|
||||||
|
for (const [key, value] of Object.entries(all)) {
|
||||||
|
if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) {
|
||||||
|
filtered[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const connected = yield* provider.list()
|
||||||
|
const providers = Object.assign(
|
||||||
|
mapValues(filtered, (item) => Provider.fromModelsDevProvider(item)),
|
||||||
|
connected,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
all: Object.values(providers),
|
||||||
|
default: Provider.defaultModelIDs(providers),
|
||||||
|
connected: Object.keys(connected),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const auth = Effect.fn("ProviderHttpApi.auth")(function* () {
|
const auth = Effect.fn("ProviderHttpApi.auth")(function* () {
|
||||||
return yield* svc.methods()
|
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),
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { InstanceBootstrap } from "@/project/bootstrap"
|
||||||
import { Instance } from "@/project/instance"
|
import { Instance } from "@/project/instance"
|
||||||
import { lazy } from "@/util/lazy"
|
import { lazy } from "@/util/lazy"
|
||||||
import { Filesystem } from "@/util"
|
import { Filesystem } from "@/util"
|
||||||
|
import { ConfigApi, configHandlers } from "./config"
|
||||||
import { PermissionApi, permissionHandlers } from "./permission"
|
import { PermissionApi, permissionHandlers } from "./permission"
|
||||||
import { ProviderApi, providerHandlers } from "./provider"
|
import { ProviderApi, providerHandlers } from "./provider"
|
||||||
import { QuestionApi, questionHandlers } from "./question"
|
import { QuestionApi, questionHandlers } from "./question"
|
||||||
|
|
@ -108,8 +109,10 @@ const instance = HttpRouter.middleware()(
|
||||||
const QuestionSecured = QuestionApi.middleware(Authorization)
|
const QuestionSecured = QuestionApi.middleware(Authorization)
|
||||||
const PermissionSecured = PermissionApi.middleware(Authorization)
|
const PermissionSecured = PermissionApi.middleware(Authorization)
|
||||||
const ProviderSecured = ProviderApi.middleware(Authorization)
|
const ProviderSecured = ProviderApi.middleware(Authorization)
|
||||||
|
const ConfigSecured = ConfigApi.middleware(Authorization)
|
||||||
|
|
||||||
export const routes = Layer.mergeAll(
|
export const routes = Layer.mergeAll(
|
||||||
|
HttpApiBuilder.layer(ConfigSecured).pipe(Layer.provide(configHandlers)),
|
||||||
HttpApiBuilder.layer(QuestionSecured).pipe(Layer.provide(questionHandlers)),
|
HttpApiBuilder.layer(QuestionSecured).pipe(Layer.provide(questionHandlers)),
|
||||||
HttpApiBuilder.layer(PermissionSecured).pipe(Layer.provide(permissionHandlers)),
|
HttpApiBuilder.layer(PermissionSecured).pipe(Layer.provide(permissionHandlers)),
|
||||||
HttpApiBuilder.layer(ProviderSecured).pipe(Layer.provide(providerHandlers)),
|
HttpApiBuilder.layer(ProviderSecured).pipe(Layer.provide(providerHandlers)),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi"
|
import { describeRoute, resolver, validator } from "hono-openapi"
|
||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
import type { UpgradeWebSocket } from "hono/ws"
|
import type { UpgradeWebSocket } from "hono/ws"
|
||||||
import { Effect } from "effect"
|
import { Context, Effect } from "effect"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Format } from "../../format"
|
import { Format } from "../../format"
|
||||||
import { TuiRoutes } from "./tui"
|
import { TuiRoutes } from "./tui"
|
||||||
|
|
@ -41,12 +41,17 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => {
|
||||||
|
|
||||||
if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) {
|
if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) {
|
||||||
const handler = ExperimentalHttpApiServer.webHandler().handler
|
const handler = ExperimentalHttpApiServer.webHandler().handler
|
||||||
app
|
const context = Context.empty() as Context.Context<unknown>
|
||||||
.all("/question", (c) => handler(c.req.raw))
|
app.get("/question", (c) => handler(c.req.raw, context))
|
||||||
.all("/question/*", (c) => handler(c.req.raw))
|
app.post("/question/:requestID/reply", (c) => handler(c.req.raw, context))
|
||||||
.all("/permission", (c) => handler(c.req.raw))
|
app.post("/question/:requestID/reject", (c) => handler(c.req.raw, context))
|
||||||
.all("/permission/*", (c) => handler(c.req.raw))
|
app.get("/permission", (c) => handler(c.req.raw, context))
|
||||||
.all("/provider/auth", (c) => handler(c.req.raw))
|
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
|
return app
|
||||||
|
|
|
||||||
|
|
@ -25,13 +25,7 @@ export const ProviderRoutes = lazy(() =>
|
||||||
description: "List of providers",
|
description: "List of providers",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: resolver(
|
schema: resolver(Provider.ListResult.zod),
|
||||||
z.object({
|
|
||||||
all: Provider.Info.array(),
|
|
||||||
default: z.record(z.string(), z.string()),
|
|
||||||
connected: z.array(z.string()),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -59,7 +53,7 @@ export const ProviderRoutes = lazy(() =>
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
all: Object.values(providers),
|
all: Object.values(providers),
|
||||||
default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
|
default: Provider.defaultModelIDs(providers),
|
||||||
connected: Object.keys(connected),
|
connected: Object.keys(connected),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
@ -116,13 +110,7 @@ export const ProviderRoutes = lazy(() =>
|
||||||
providerID: ProviderID.zod.meta({ description: "Provider ID" }),
|
providerID: ProviderID.zod.meta({ description: "Provider ID" }),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
validator(
|
validator("json", ProviderAuth.AuthorizeInput.zod),
|
||||||
"json",
|
|
||||||
z.object({
|
|
||||||
method: z.number().meta({ description: "Auth method index" }),
|
|
||||||
inputs: z.record(z.string(), z.string()).optional().meta({ description: "Prompt inputs" }),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const providerID = c.req.valid("param").providerID
|
const providerID = c.req.valid("param").providerID
|
||||||
const { method, inputs } = c.req.valid("json")
|
const { method, inputs } = c.req.valid("json")
|
||||||
|
|
@ -162,13 +150,7 @@ export const ProviderRoutes = lazy(() =>
|
||||||
providerID: ProviderID.zod.meta({ description: "Provider ID" }),
|
providerID: ProviderID.zod.meta({ description: "Provider ID" }),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
validator(
|
validator("json", ProviderAuth.CallbackInput.zod),
|
||||||
"json",
|
|
||||||
z.object({
|
|
||||||
method: z.number().meta({ description: "Auth method index" }),
|
|
||||||
code: z.string().optional().meta({ description: "OAuth authorization code" }),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const providerID = c.req.valid("param").providerID
|
const providerID = c.req.valid("param").providerID
|
||||||
const { method, code } = c.req.valid("json")
|
const { method, code } = c.req.valid("json")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue