server.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. import { Log } from "../util/log"
  2. import { Bus } from "../bus"
  3. import { describeRoute, generateSpecs, openAPISpecs } from "hono-openapi"
  4. import { Hono } from "hono"
  5. import { streamSSE } from "hono/streaming"
  6. import { Session } from "../session"
  7. import { resolver, validator as zValidator } from "hono-openapi/zod"
  8. import { z } from "zod"
  9. import { Message } from "../session/message"
  10. import { Provider } from "../provider/provider"
  11. import { App } from "../app/app"
  12. import { Global } from "../global"
  13. import { mapValues } from "remeda"
  14. import { NamedError } from "../util/error"
  15. import { ModelsDev } from "../provider/models"
  16. import { Ripgrep } from "../external/ripgrep"
  17. import { Installation } from "../installation"
  18. import { Config } from "../config/config"
  19. const ERRORS = {
  20. 400: {
  21. description: "Bad request",
  22. content: {
  23. "application/json": {
  24. schema: resolver(
  25. z
  26. .object({
  27. data: z.record(z.string(), z.any()),
  28. })
  29. .openapi({
  30. ref: "Error",
  31. }),
  32. ),
  33. },
  34. },
  35. },
  36. } as const
  37. export namespace Server {
  38. const log = Log.create({ service: "server" })
  39. export type Routes = ReturnType<typeof app>
  40. function app() {
  41. const app = new Hono()
  42. const result = app
  43. .onError((err, c) => {
  44. if (err instanceof NamedError) {
  45. return c.json(err.toObject(), {
  46. status: 400,
  47. })
  48. }
  49. return c.json(
  50. new NamedError.Unknown({ message: err.toString() }).toObject(),
  51. {
  52. status: 400,
  53. },
  54. )
  55. })
  56. .use(async (c, next) => {
  57. log.info("request", {
  58. method: c.req.method,
  59. path: c.req.path,
  60. })
  61. const start = Date.now()
  62. await next()
  63. log.info("response", {
  64. duration: Date.now() - start,
  65. })
  66. })
  67. .get(
  68. "/openapi",
  69. openAPISpecs(app, {
  70. documentation: {
  71. info: {
  72. title: "opencode",
  73. version: "1.0.0",
  74. description: "opencode api",
  75. },
  76. openapi: "3.0.0",
  77. },
  78. }),
  79. )
  80. .get(
  81. "/event",
  82. describeRoute({
  83. description: "Get events",
  84. responses: {
  85. 200: {
  86. description: "Event stream",
  87. content: {
  88. "application/json": {
  89. schema: resolver(
  90. Bus.payloads().openapi({
  91. ref: "Event",
  92. }),
  93. ),
  94. },
  95. },
  96. },
  97. },
  98. }),
  99. async (c) => {
  100. log.info("event connected")
  101. return streamSSE(c, async (stream) => {
  102. stream.writeSSE({
  103. data: JSON.stringify({}),
  104. })
  105. const unsub = Bus.subscribeAll(async (event) => {
  106. await stream.writeSSE({
  107. data: JSON.stringify(event),
  108. })
  109. })
  110. await new Promise<void>((resolve) => {
  111. stream.onAbort(() => {
  112. unsub()
  113. resolve()
  114. log.info("event disconnected")
  115. })
  116. })
  117. })
  118. },
  119. )
  120. .post(
  121. "/app_info",
  122. describeRoute({
  123. description: "Get app info",
  124. responses: {
  125. 200: {
  126. description: "200",
  127. content: {
  128. "application/json": {
  129. schema: resolver(App.Info),
  130. },
  131. },
  132. },
  133. },
  134. }),
  135. async (c) => {
  136. return c.json(App.info())
  137. },
  138. )
  139. .post(
  140. "/config_get",
  141. describeRoute({
  142. description: "Get config info",
  143. responses: {
  144. 200: {
  145. description: "Get config info",
  146. content: {
  147. "application/json": {
  148. schema: resolver(Config.Info),
  149. },
  150. },
  151. },
  152. },
  153. }),
  154. async (c) => {
  155. return c.json(await Config.get())
  156. },
  157. )
  158. .post(
  159. "/app_initialize",
  160. describeRoute({
  161. description: "Initialize the app",
  162. responses: {
  163. 200: {
  164. description: "Initialize the app",
  165. content: {
  166. "application/json": {
  167. schema: resolver(z.boolean()),
  168. },
  169. },
  170. },
  171. },
  172. }),
  173. async (c) => {
  174. await App.initialize()
  175. return c.json(true)
  176. },
  177. )
  178. .post(
  179. "/session_initialize",
  180. describeRoute({
  181. description: "Analyze the app and create an AGENTS.md file",
  182. responses: {
  183. 200: {
  184. description: "200",
  185. content: {
  186. "application/json": {
  187. schema: resolver(z.boolean()),
  188. },
  189. },
  190. },
  191. },
  192. }),
  193. zValidator(
  194. "json",
  195. z.object({
  196. sessionID: z.string(),
  197. providerID: z.string(),
  198. modelID: z.string(),
  199. }),
  200. ),
  201. async (c) => {
  202. const body = c.req.valid("json")
  203. await Session.initialize(body)
  204. return c.json(true)
  205. },
  206. )
  207. .post(
  208. "/path_get",
  209. describeRoute({
  210. description: "Get paths",
  211. responses: {
  212. 200: {
  213. description: "200",
  214. content: {
  215. "application/json": {
  216. schema: resolver(
  217. z.object({
  218. root: z.string(),
  219. data: z.string(),
  220. cwd: z.string(),
  221. config: z.string(),
  222. }),
  223. ),
  224. },
  225. },
  226. },
  227. },
  228. }),
  229. async (c) => {
  230. const app = App.info()
  231. return c.json({
  232. root: app.path.root,
  233. data: app.path.data,
  234. cwd: app.path.cwd,
  235. config: Global.Path.data,
  236. })
  237. },
  238. )
  239. .post(
  240. "/session_create",
  241. describeRoute({
  242. description: "Create a new session",
  243. responses: {
  244. ...ERRORS,
  245. 200: {
  246. description: "Successfully created session",
  247. content: {
  248. "application/json": {
  249. schema: resolver(Session.Info),
  250. },
  251. },
  252. },
  253. },
  254. }),
  255. async (c) => {
  256. const session = await Session.create()
  257. return c.json(session)
  258. },
  259. )
  260. .post(
  261. "/session_share",
  262. describeRoute({
  263. description: "Share the session",
  264. responses: {
  265. 200: {
  266. description: "Successfully shared session",
  267. content: {
  268. "application/json": {
  269. schema: resolver(Session.Info),
  270. },
  271. },
  272. },
  273. },
  274. }),
  275. zValidator(
  276. "json",
  277. z.object({
  278. sessionID: z.string(),
  279. }),
  280. ),
  281. async (c) => {
  282. const body = c.req.valid("json")
  283. await Session.share(body.sessionID)
  284. const session = await Session.get(body.sessionID)
  285. return c.json(session)
  286. },
  287. )
  288. .post(
  289. "/session_messages",
  290. describeRoute({
  291. description: "Get messages for a session",
  292. responses: {
  293. 200: {
  294. description: "Successfully created session",
  295. content: {
  296. "application/json": {
  297. schema: resolver(Message.Info.array()),
  298. },
  299. },
  300. },
  301. },
  302. }),
  303. zValidator(
  304. "json",
  305. z.object({
  306. sessionID: z.string(),
  307. }),
  308. ),
  309. async (c) => {
  310. const messages = await Session.messages(c.req.valid("json").sessionID)
  311. return c.json(messages)
  312. },
  313. )
  314. .post(
  315. "/session_list",
  316. describeRoute({
  317. description: "List all sessions",
  318. responses: {
  319. 200: {
  320. description: "List of sessions",
  321. content: {
  322. "application/json": {
  323. schema: resolver(Session.Info.array()),
  324. },
  325. },
  326. },
  327. },
  328. }),
  329. async (c) => {
  330. const sessions = await Array.fromAsync(Session.list())
  331. return c.json(sessions)
  332. },
  333. )
  334. .post(
  335. "/session_abort",
  336. describeRoute({
  337. description: "Abort a session",
  338. responses: {
  339. 200: {
  340. description: "Aborted session",
  341. content: {
  342. "application/json": {
  343. schema: resolver(z.boolean()),
  344. },
  345. },
  346. },
  347. },
  348. }),
  349. zValidator(
  350. "json",
  351. z.object({
  352. sessionID: z.string(),
  353. }),
  354. ),
  355. async (c) => {
  356. const body = c.req.valid("json")
  357. return c.json(Session.abort(body.sessionID))
  358. },
  359. )
  360. .post(
  361. "/session_summarize",
  362. describeRoute({
  363. description: "Summarize the session",
  364. responses: {
  365. 200: {
  366. description: "Summarize the session",
  367. content: {
  368. "application/json": {
  369. schema: resolver(z.boolean()),
  370. },
  371. },
  372. },
  373. },
  374. }),
  375. zValidator(
  376. "json",
  377. z.object({
  378. sessionID: z.string(),
  379. providerID: z.string(),
  380. modelID: z.string(),
  381. }),
  382. ),
  383. async (c) => {
  384. const body = c.req.valid("json")
  385. await Session.summarize(body)
  386. return c.json(true)
  387. },
  388. )
  389. .post(
  390. "/session_chat",
  391. describeRoute({
  392. description: "Chat with a model",
  393. responses: {
  394. 200: {
  395. description: "Chat with a model",
  396. content: {
  397. "application/json": {
  398. schema: resolver(Message.Info),
  399. },
  400. },
  401. },
  402. },
  403. }),
  404. zValidator(
  405. "json",
  406. z.object({
  407. sessionID: z.string(),
  408. providerID: z.string(),
  409. modelID: z.string(),
  410. parts: Message.Part.array(),
  411. }),
  412. ),
  413. async (c) => {
  414. const body = c.req.valid("json")
  415. const msg = await Session.chat(body)
  416. return c.json(msg)
  417. },
  418. )
  419. .post(
  420. "/provider_list",
  421. describeRoute({
  422. description: "List all providers",
  423. responses: {
  424. 200: {
  425. description: "List of providers",
  426. content: {
  427. "application/json": {
  428. schema: resolver(
  429. z.object({
  430. providers: ModelsDev.Provider.array(),
  431. default: z.record(z.string(), z.string()),
  432. }),
  433. ),
  434. },
  435. },
  436. },
  437. },
  438. }),
  439. async (c) => {
  440. const providers = await Provider.list().then((x) =>
  441. mapValues(x, (item) => item.info),
  442. )
  443. return c.json({
  444. providers: Object.values(providers),
  445. default: mapValues(
  446. providers,
  447. (item) => Provider.sort(Object.values(item.models))[0].id,
  448. ),
  449. })
  450. },
  451. )
  452. .post(
  453. "/file_search",
  454. describeRoute({
  455. description: "Search for files",
  456. responses: {
  457. 200: {
  458. description: "Search for files",
  459. content: {
  460. "application/json": {
  461. schema: resolver(z.string().array()),
  462. },
  463. },
  464. },
  465. },
  466. }),
  467. zValidator(
  468. "json",
  469. z.object({
  470. query: z.string(),
  471. }),
  472. ),
  473. async (c) => {
  474. const body = c.req.valid("json")
  475. const app = App.info()
  476. const result = await Ripgrep.files({
  477. cwd: app.path.cwd,
  478. query: body.query,
  479. limit: 10,
  480. })
  481. return c.json(result)
  482. },
  483. )
  484. .post(
  485. "installation_info",
  486. describeRoute({
  487. description: "Get installation info",
  488. responses: {
  489. 200: {
  490. description: "Get installation info",
  491. content: {
  492. "application/json": {
  493. schema: resolver(Installation.Info),
  494. },
  495. },
  496. },
  497. },
  498. }),
  499. async (c) => {
  500. return c.json(Installation.info())
  501. },
  502. )
  503. return result
  504. }
  505. export async function openapi() {
  506. const a = app()
  507. const result = await generateSpecs(a, {
  508. documentation: {
  509. info: {
  510. title: "opencode",
  511. version: "1.0.0",
  512. description: "opencode api",
  513. },
  514. openapi: "3.0.0",
  515. },
  516. })
  517. return result
  518. }
  519. export function listen() {
  520. const server = Bun.serve({
  521. port: 0,
  522. hostname: "0.0.0.0",
  523. idleTimeout: 0,
  524. fetch: app().fetch,
  525. })
  526. return server
  527. }
  528. }