opencode_offline/packages/opencode/src/server/server.ts

537 lines
13 KiB
TypeScript
Raw Normal View History

2025-06-01 02:41:00 +08:00
import { Log } from "../util/log"
import { Bus } from "../bus"
import { describeRoute, generateSpecs, openAPISpecs } from "hono-openapi"
import { Hono } from "hono"
import { streamSSE } from "hono/streaming"
import { Session } from "../session"
2025-06-01 02:41:00 +08:00
import { resolver, validator as zValidator } from "hono-openapi/zod"
import { z } from "zod"
import { Message } from "../session/message"
import { Provider } from "../provider/provider"
2025-06-02 03:01:57 +08:00
import { App } from "../app/app"
import { Global } from "../global"
2025-06-20 12:57:28 +08:00
import { filter, mapValues } from "remeda"
2025-06-10 02:01:11 +08:00
import { NamedError } from "../util/error"
import { ModelsDev } from "../provider/models"
import { Ripgrep } from "../external/ripgrep"
import { Installation } from "../installation"
2025-06-19 10:20:03 +08:00
import { Config } from "../config/config"
2025-06-10 02:01:11 +08:00
const ERRORS = {
400: {
description: "Bad request",
content: {
"application/json": {
schema: resolver(
z
.object({
data: z.record(z.string(), z.any()),
})
.openapi({
ref: "Error",
}),
),
},
},
},
} as const
2025-05-19 02:13:04 +08:00
export namespace Server {
2025-06-01 02:41:00 +08:00
const log = Log.create({ service: "server" })
2025-05-19 02:13:04 +08:00
export type Routes = ReturnType<typeof app>
2025-05-19 02:13:04 +08:00
2025-05-19 02:28:08 +08:00
function app() {
2025-06-01 02:41:00 +08:00
const app = new Hono()
2025-05-19 10:30:41 +08:00
const result = app
2025-06-05 08:49:28 +08:00
.onError((err, c) => {
2025-06-10 02:01:11 +08:00
if (err instanceof NamedError) {
return c.json(err.toObject(), {
status: 400,
})
}
2025-06-05 08:49:28 +08:00
return c.json(
2025-06-10 02:01:11 +08:00
new NamedError.Unknown({ message: err.toString() }).toObject(),
2025-06-05 08:49:28 +08:00
{
2025-06-10 02:01:11 +08:00
status: 400,
2025-06-05 08:49:28 +08:00
},
)
})
.use(async (c, next) => {
2025-06-04 01:00:27 +08:00
log.info("request", {
method: c.req.method,
path: c.req.path,
})
const start = Date.now()
await next()
log.info("response", {
duration: Date.now() - start,
})
2025-06-04 01:00:27 +08:00
})
2025-05-19 10:30:41 +08:00
.get(
"/openapi",
openAPISpecs(app, {
documentation: {
info: {
title: "opencode",
version: "1.0.0",
description: "opencode api",
},
2025-05-29 22:21:59 +08:00
openapi: "3.0.0",
2025-05-19 10:30:41 +08:00
},
}),
)
2025-05-29 23:32:55 +08:00
.get(
"/event",
describeRoute({
description: "Get events",
responses: {
200: {
description: "Event stream",
content: {
"application/json": {
schema: resolver(
Bus.payloads().openapi({
ref: "Event",
}),
),
},
},
},
},
}),
async (c) => {
2025-06-01 02:41:00 +08:00
log.info("event connected")
2025-05-29 23:32:55 +08:00
return streamSSE(c, async (stream) => {
stream.writeSSE({
data: JSON.stringify({}),
2025-06-01 02:41:00 +08:00
})
2025-05-29 23:32:55 +08:00
const unsub = Bus.subscribeAll(async (event) => {
await stream.writeSSE({
data: JSON.stringify(event),
2025-06-01 02:41:00 +08:00
})
})
2025-05-29 23:32:55 +08:00
await new Promise<void>((resolve) => {
stream.onAbort(() => {
2025-06-01 02:41:00 +08:00
unsub()
resolve()
log.info("event disconnected")
})
})
})
2025-05-29 23:32:55 +08:00
},
)
.post(
"/app_info",
describeRoute({
description: "Get app info",
responses: {
200: {
description: "200",
content: {
"application/json": {
schema: resolver(App.Info),
},
},
},
},
}),
async (c) => {
return c.json(App.info())
},
)
2025-06-19 10:20:03 +08:00
.post(
"/config_get",
describeRoute({
description: "Get config info",
responses: {
200: {
description: "Get config info",
content: {
"application/json": {
schema: resolver(Config.Info),
},
},
},
},
}),
async (c) => {
return c.json(await Config.get())
},
)
2025-06-04 02:24:45 +08:00
.post(
"/app_initialize",
describeRoute({
description: "Initialize the app",
responses: {
200: {
description: "Initialize the app",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
},
}),
async (c) => {
await App.initialize()
return c.json(true)
},
)
2025-06-04 00:38:48 +08:00
.post(
"/session_initialize",
describeRoute({
description: "Analyze the app and create an AGENTS.md file",
responses: {
200: {
description: "200",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
},
}),
zValidator(
"json",
z.object({
sessionID: z.string(),
providerID: z.string(),
modelID: z.string(),
}),
),
async (c) => {
const body = c.req.valid("json")
await Session.initialize(body)
return c.json(true)
},
)
2025-06-02 03:01:57 +08:00
.post(
"/path_get",
describeRoute({
description: "Get paths",
responses: {
200: {
description: "200",
content: {
"application/json": {
schema: resolver(
z.object({
root: z.string(),
data: z.string(),
cwd: z.string(),
config: z.string(),
}),
),
},
},
},
},
}),
async (c) => {
const app = App.info()
2025-06-02 03:01:57 +08:00
return c.json({
root: app.path.root,
data: app.path.data,
cwd: app.path.cwd,
config: Global.Path.data,
2025-06-02 03:01:57 +08:00
})
},
)
2025-05-19 10:30:41 +08:00
.post(
"/session_create",
describeRoute({
description: "Create a new session",
responses: {
2025-06-10 02:01:11 +08:00
...ERRORS,
2025-05-19 10:30:41 +08:00
200: {
description: "Successfully created session",
content: {
"application/json": {
2025-05-29 23:38:55 +08:00
schema: resolver(Session.Info),
2025-05-20 23:11:06 +08:00
},
},
},
},
}),
async (c) => {
2025-06-01 02:41:00 +08:00
const session = await Session.create()
return c.json(session)
2025-05-20 23:11:06 +08:00
},
)
2025-05-27 06:06:41 +08:00
.post(
"/session_share",
describeRoute({
description: "Share the session",
responses: {
200: {
description: "Successfully shared session",
content: {
"application/json": {
2025-05-29 23:38:55 +08:00
schema: resolver(Session.Info),
2025-05-27 06:06:41 +08:00
},
},
},
},
}),
zValidator(
"json",
z.object({
sessionID: z.string(),
}),
),
async (c) => {
2025-06-01 02:41:00 +08:00
const body = c.req.valid("json")
await Session.share(body.sessionID)
const session = await Session.get(body.sessionID)
return c.json(session)
2025-05-27 06:06:41 +08:00
},
)
2025-05-20 23:11:06 +08:00
.post(
"/session_messages",
describeRoute({
description: "Get messages for a session",
responses: {
200: {
description: "Successfully created session",
content: {
"application/json": {
2025-05-29 22:21:59 +08:00
schema: resolver(Message.Info.array()),
2025-05-19 10:30:41 +08:00
},
},
},
},
}),
2025-05-27 06:10:10 +08:00
zValidator(
"json",
z.object({
sessionID: z.string(),
}),
),
2025-05-19 10:30:41 +08:00
async (c) => {
2025-06-01 02:41:00 +08:00
const messages = await Session.messages(c.req.valid("json").sessionID)
return c.json(messages)
2025-05-19 10:30:41 +08:00
},
)
2025-05-28 03:34:46 +08:00
.post(
"/session_list",
describeRoute({
description: "List all sessions",
responses: {
200: {
description: "List of sessions",
content: {
"application/json": {
2025-05-28 03:35:31 +08:00
schema: resolver(Session.Info.array()),
2025-05-28 03:34:46 +08:00
},
},
},
},
}),
async (c) => {
2025-06-01 02:41:00 +08:00
const sessions = await Array.fromAsync(Session.list())
return c.json(sessions)
2025-05-28 03:34:46 +08:00
},
)
2025-05-29 03:07:51 +08:00
.post(
"/session_abort",
describeRoute({
description: "Abort a session",
responses: {
200: {
description: "Aborted session",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
},
}),
zValidator(
"json",
z.object({
sessionID: z.string(),
}),
),
async (c) => {
2025-06-01 02:41:00 +08:00
const body = c.req.valid("json")
return c.json(Session.abort(body.sessionID))
2025-05-29 03:07:51 +08:00
},
2025-05-30 01:17:56 +08:00
)
.post(
"/session_summarize",
describeRoute({
description: "Summarize the session",
responses: {
200: {
description: "Summarize the session",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
},
}),
zValidator(
"json",
z.object({
sessionID: z.string(),
providerID: z.string(),
modelID: z.string(),
}),
),
async (c) => {
2025-06-01 02:41:00 +08:00
const body = c.req.valid("json")
await Session.summarize(body)
return c.json(true)
2025-05-30 01:17:56 +08:00
},
2025-05-29 03:07:51 +08:00
)
2025-05-19 02:13:04 +08:00
.post(
"/session_chat",
2025-05-29 03:07:51 +08:00
describeRoute({
description: "Chat with a model",
responses: {
200: {
description: "Chat with a model",
content: {
"application/json": {
2025-05-29 22:21:59 +08:00
schema: resolver(Message.Info),
2025-05-29 03:07:51 +08:00
},
},
},
},
}),
2025-05-19 02:13:04 +08:00
zValidator(
"json",
z.object({
sessionID: z.string(),
2025-05-29 00:53:22 +08:00
providerID: z.string(),
modelID: z.string(),
2025-05-29 22:21:59 +08:00
parts: Message.Part.array(),
2025-05-19 02:13:04 +08:00
}),
),
async (c) => {
2025-06-01 02:41:00 +08:00
const body = c.req.valid("json")
const msg = await Session.chat(body)
return c.json(msg)
2025-05-18 09:31:42 +08:00
},
2025-05-29 00:53:22 +08:00
)
.post(
"/provider_list",
describeRoute({
description: "List all providers",
responses: {
200: {
description: "List of providers",
content: {
"application/json": {
2025-06-06 02:59:07 +08:00
schema: resolver(
z.object({
providers: ModelsDev.Provider.array(),
2025-06-06 02:59:07 +08:00
default: z.record(z.string(), z.string()),
}),
),
2025-05-29 00:53:22 +08:00
},
},
},
},
}),
async (c) => {
2025-06-11 03:43:14 +08:00
const providers = await Provider.list().then((x) =>
mapValues(x, (item) => item.info),
)
2025-06-06 02:59:07 +08:00
return c.json({
providers: Object.values(providers),
2025-06-13 21:30:54 +08:00
default: mapValues(
2025-06-06 02:59:07 +08:00
providers,
(item) => Provider.sort(Object.values(item.models))[0].id,
),
})
2025-05-29 00:53:22 +08:00
},
2025-06-01 02:41:00 +08:00
)
.post(
"/file_search",
describeRoute({
description: "Search for files",
responses: {
200: {
description: "Search for files",
content: {
"application/json": {
schema: resolver(z.string().array()),
},
},
},
},
}),
zValidator(
"json",
z.object({
query: z.string(),
}),
),
async (c) => {
const body = c.req.valid("json")
const app = App.info()
const result = await Ripgrep.files({
cwd: app.path.cwd,
query: body.query,
limit: 10,
})
return c.json(result)
},
)
.post(
"installation_info",
describeRoute({
description: "Get installation info",
responses: {
200: {
description: "Get installation info",
content: {
"application/json": {
schema: resolver(Installation.Info),
},
},
},
},
}),
async (c) => {
return c.json(Installation.info())
},
)
2025-05-19 10:30:41 +08:00
2025-06-01 02:41:00 +08:00
return result
2025-05-19 10:30:41 +08:00
}
export async function openapi() {
2025-06-01 02:41:00 +08:00
const a = app()
2025-05-19 10:30:41 +08:00
const result = await generateSpecs(a, {
documentation: {
info: {
title: "opencode",
version: "1.0.0",
description: "opencode api",
},
2025-05-29 22:21:59 +08:00
openapi: "3.0.0",
2025-05-19 10:30:41 +08:00
},
2025-06-01 02:41:00 +08:00
})
return result
2025-05-19 02:28:08 +08:00
}
2025-05-19 02:13:04 +08:00
2025-05-19 02:28:08 +08:00
export function listen() {
const server = Bun.serve({
2025-06-04 04:35:37 +08:00
port: 0,
2025-05-19 02:13:04 +08:00
hostname: "0.0.0.0",
idleTimeout: 0,
2025-05-19 02:28:08 +08:00
fetch: app().fetch,
2025-06-01 02:41:00 +08:00
})
return server
2025-05-18 09:31:42 +08:00
}
}