index.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766
  1. import { Slug } from "@opencode-ai/util/slug"
  2. import path from "path"
  3. import { BusEvent } from "@/bus/bus-event"
  4. import { Bus } from "@/bus"
  5. import { Decimal } from "decimal.js"
  6. import z from "zod"
  7. import { type ProviderMetadata } from "ai"
  8. import { Config } from "../config/config"
  9. import { Flag } from "../flag/flag"
  10. import { Identifier } from "../id/id"
  11. import { Installation } from "../installation"
  12. import { Database, NotFoundError, eq, and, or, like } from "../storage/db"
  13. import { SessionTable, MessageTable, PartTable } from "./session.sql"
  14. import { Storage } from "@/storage/storage"
  15. import { Log } from "../util/log"
  16. import { MessageV2 } from "./message-v2"
  17. import { Instance } from "../project/instance"
  18. import { SessionPrompt } from "./prompt"
  19. import { fn } from "@/util/fn"
  20. import { Command } from "../command"
  21. import { Snapshot } from "@/snapshot"
  22. import type { Provider } from "@/provider/provider"
  23. import { PermissionNext } from "@/permission/next"
  24. import { Global } from "@/global"
  25. import type { LanguageModelV2Usage } from "@ai-sdk/provider"
  26. import { iife } from "@/util/iife"
  27. export namespace Session {
  28. const log = Log.create({ service: "session" })
  29. const parentTitlePrefix = "New session - "
  30. const childTitlePrefix = "Child session - "
  31. function createDefaultTitle(isChild = false) {
  32. return (isChild ? childTitlePrefix : parentTitlePrefix) + new Date().toISOString()
  33. }
  34. export function isDefaultTitle(title: string) {
  35. return new RegExp(
  36. `^(${parentTitlePrefix}|${childTitlePrefix})\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$`,
  37. ).test(title)
  38. }
  39. type SessionRow = typeof SessionTable.$inferSelect
  40. export function fromRow(row: SessionRow): Info {
  41. const summary =
  42. row.summary_additions !== null || row.summary_deletions !== null || row.summary_files !== null
  43. ? {
  44. additions: row.summary_additions ?? 0,
  45. deletions: row.summary_deletions ?? 0,
  46. files: row.summary_files ?? 0,
  47. diffs: row.summary_diffs ?? undefined,
  48. }
  49. : undefined
  50. const share = row.share_url ? { url: row.share_url } : undefined
  51. const revert = row.revert ?? undefined
  52. return {
  53. id: row.id,
  54. slug: row.slug,
  55. projectID: row.project_id,
  56. directory: row.directory,
  57. parentID: row.parent_id ?? undefined,
  58. title: row.title,
  59. version: row.version,
  60. summary,
  61. share,
  62. revert,
  63. permission: row.permission ?? undefined,
  64. time: {
  65. created: row.time_created,
  66. updated: row.time_updated,
  67. compacting: row.time_compacting ?? undefined,
  68. archived: row.time_archived ?? undefined,
  69. },
  70. }
  71. }
  72. export function toRow(info: Info) {
  73. return {
  74. id: info.id,
  75. project_id: info.projectID,
  76. parent_id: info.parentID,
  77. slug: info.slug,
  78. directory: info.directory,
  79. title: info.title,
  80. version: info.version,
  81. share_url: info.share?.url,
  82. summary_additions: info.summary?.additions,
  83. summary_deletions: info.summary?.deletions,
  84. summary_files: info.summary?.files,
  85. summary_diffs: info.summary?.diffs,
  86. revert: info.revert ?? null,
  87. permission: info.permission,
  88. time_created: info.time.created,
  89. time_updated: info.time.updated,
  90. time_compacting: info.time.compacting,
  91. time_archived: info.time.archived,
  92. }
  93. }
  94. function getForkedTitle(title: string): string {
  95. const match = title.match(/^(.+) \(fork #(\d+)\)$/)
  96. if (match) {
  97. const base = match[1]
  98. const num = parseInt(match[2], 10)
  99. return `${base} (fork #${num + 1})`
  100. }
  101. return `${title} (fork #1)`
  102. }
  103. export const Info = z
  104. .object({
  105. id: Identifier.schema("session"),
  106. slug: z.string(),
  107. projectID: z.string(),
  108. directory: z.string(),
  109. parentID: Identifier.schema("session").optional(),
  110. summary: z
  111. .object({
  112. additions: z.number(),
  113. deletions: z.number(),
  114. files: z.number(),
  115. diffs: Snapshot.FileDiff.array().optional(),
  116. })
  117. .optional(),
  118. share: z
  119. .object({
  120. url: z.string(),
  121. })
  122. .optional(),
  123. title: z.string(),
  124. version: z.string(),
  125. time: z.object({
  126. created: z.number(),
  127. updated: z.number(),
  128. compacting: z.number().optional(),
  129. archived: z.number().optional(),
  130. }),
  131. permission: PermissionNext.Ruleset.optional(),
  132. revert: z
  133. .object({
  134. messageID: z.string(),
  135. partID: z.string().optional(),
  136. snapshot: z.string().optional(),
  137. diff: z.string().optional(),
  138. })
  139. .optional(),
  140. })
  141. .meta({
  142. ref: "Session",
  143. })
  144. export type Info = z.output<typeof Info>
  145. export const Event = {
  146. Created: BusEvent.define(
  147. "session.created",
  148. z.object({
  149. info: Info,
  150. }),
  151. ),
  152. Updated: BusEvent.define(
  153. "session.updated",
  154. z.object({
  155. info: Info,
  156. }),
  157. ),
  158. Deleted: BusEvent.define(
  159. "session.deleted",
  160. z.object({
  161. info: Info,
  162. }),
  163. ),
  164. Diff: BusEvent.define(
  165. "session.diff",
  166. z.object({
  167. sessionID: z.string(),
  168. diff: Snapshot.FileDiff.array(),
  169. }),
  170. ),
  171. Error: BusEvent.define(
  172. "session.error",
  173. z.object({
  174. sessionID: z.string().optional(),
  175. error: MessageV2.Assistant.shape.error,
  176. }),
  177. ),
  178. }
  179. export const create = fn(
  180. z
  181. .object({
  182. parentID: Identifier.schema("session").optional(),
  183. title: z.string().optional(),
  184. permission: Info.shape.permission,
  185. })
  186. .optional(),
  187. async (input) => {
  188. return createNext({
  189. parentID: input?.parentID,
  190. directory: Instance.directory,
  191. title: input?.title,
  192. permission: input?.permission,
  193. })
  194. },
  195. )
  196. export const fork = fn(
  197. z.object({
  198. sessionID: Identifier.schema("session"),
  199. messageID: Identifier.schema("message").optional(),
  200. }),
  201. async (input) => {
  202. const original = await get(input.sessionID)
  203. if (!original) throw new Error("session not found")
  204. const title = getForkedTitle(original.title)
  205. const session = await createNext({
  206. directory: Instance.directory,
  207. title,
  208. })
  209. const msgs = await messages({ sessionID: input.sessionID })
  210. const idMap = new Map<string, string>()
  211. for (const msg of msgs) {
  212. if (input.messageID && msg.info.id >= input.messageID) break
  213. const newID = Identifier.ascending("message")
  214. idMap.set(msg.info.id, newID)
  215. const parentID = msg.info.role === "assistant" && msg.info.parentID ? idMap.get(msg.info.parentID) : undefined
  216. const cloned = await updateMessage({
  217. ...msg.info,
  218. sessionID: session.id,
  219. id: newID,
  220. ...(parentID && { parentID }),
  221. })
  222. for (const part of msg.parts) {
  223. await updatePart({
  224. ...part,
  225. id: Identifier.ascending("part"),
  226. messageID: cloned.id,
  227. sessionID: session.id,
  228. })
  229. }
  230. }
  231. return session
  232. },
  233. )
  234. export const touch = fn(Identifier.schema("session"), async (sessionID) => {
  235. const now = Date.now()
  236. Database.use((db) => {
  237. const row = db
  238. .update(SessionTable)
  239. .set({ time_updated: now })
  240. .where(eq(SessionTable.id, sessionID))
  241. .returning()
  242. .get()
  243. if (!row) throw new NotFoundError({ message: `Session not found: ${sessionID}` })
  244. const info = fromRow(row)
  245. Database.effect(() => Bus.publish(Event.Updated, { info }))
  246. })
  247. })
  248. export async function createNext(input: {
  249. id?: string
  250. title?: string
  251. parentID?: string
  252. directory: string
  253. permission?: PermissionNext.Ruleset
  254. }) {
  255. const result: Info = {
  256. id: Identifier.descending("session", input.id),
  257. slug: Slug.create(),
  258. version: Installation.VERSION,
  259. projectID: Instance.project.id,
  260. directory: input.directory,
  261. parentID: input.parentID,
  262. title: input.title ?? createDefaultTitle(!!input.parentID),
  263. permission: input.permission,
  264. time: {
  265. created: Date.now(),
  266. updated: Date.now(),
  267. },
  268. }
  269. log.info("created", result)
  270. Database.use((db) => {
  271. db.insert(SessionTable).values(toRow(result)).run()
  272. Database.effect(() =>
  273. Bus.publish(Event.Created, {
  274. info: result,
  275. }),
  276. )
  277. })
  278. const cfg = await Config.get()
  279. if (!result.parentID && (Flag.OPENCODE_AUTO_SHARE || cfg.share === "auto"))
  280. share(result.id).catch(() => {
  281. // Silently ignore sharing errors during session creation
  282. })
  283. Bus.publish(Event.Updated, {
  284. info: result,
  285. })
  286. return result
  287. }
  288. export function plan(input: { slug: string; time: { created: number } }) {
  289. const base = Instance.project.vcs
  290. ? path.join(Instance.worktree, ".opencode", "plans")
  291. : path.join(Global.Path.data, "plans")
  292. return path.join(base, [input.time.created, input.slug].join("-") + ".md")
  293. }
  294. export const get = fn(Identifier.schema("session"), async (id) => {
  295. const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get())
  296. if (!row) throw new NotFoundError({ message: `Session not found: ${id}` })
  297. return fromRow(row)
  298. })
  299. export const share = fn(Identifier.schema("session"), async (id) => {
  300. const cfg = await Config.get()
  301. if (cfg.share === "disabled") {
  302. throw new Error("Sharing is disabled in configuration")
  303. }
  304. const { ShareNext } = await import("@/share/share-next")
  305. const share = await ShareNext.create(id)
  306. Database.use((db) => {
  307. const row = db.update(SessionTable).set({ share_url: share.url }).where(eq(SessionTable.id, id)).returning().get()
  308. if (!row) throw new NotFoundError({ message: `Session not found: ${id}` })
  309. const info = fromRow(row)
  310. Database.effect(() => Bus.publish(Event.Updated, { info }))
  311. })
  312. return share
  313. })
  314. export const unshare = fn(Identifier.schema("session"), async (id) => {
  315. // Use ShareNext to remove the share (same as share function uses ShareNext to create)
  316. const { ShareNext } = await import("@/share/share-next")
  317. await ShareNext.remove(id)
  318. Database.use((db) => {
  319. const row = db.update(SessionTable).set({ share_url: null }).where(eq(SessionTable.id, id)).returning().get()
  320. if (!row) throw new NotFoundError({ message: `Session not found: ${id}` })
  321. const info = fromRow(row)
  322. Database.effect(() => Bus.publish(Event.Updated, { info }))
  323. })
  324. })
  325. export const setTitle = fn(
  326. z.object({
  327. sessionID: Identifier.schema("session"),
  328. title: z.string(),
  329. }),
  330. async (input) => {
  331. return Database.use((db) => {
  332. const row = db
  333. .update(SessionTable)
  334. .set({ title: input.title })
  335. .where(eq(SessionTable.id, input.sessionID))
  336. .returning()
  337. .get()
  338. if (!row) throw new NotFoundError({ message: `Session not found: ${input.sessionID}` })
  339. const info = fromRow(row)
  340. Database.effect(() => Bus.publish(Event.Updated, { info }))
  341. return info
  342. })
  343. },
  344. )
  345. export const setArchived = fn(
  346. z.object({
  347. sessionID: Identifier.schema("session"),
  348. time: z.number().optional(),
  349. }),
  350. async (input) => {
  351. return Database.use((db) => {
  352. const row = db
  353. .update(SessionTable)
  354. .set({ time_archived: input.time })
  355. .where(eq(SessionTable.id, input.sessionID))
  356. .returning()
  357. .get()
  358. if (!row) throw new NotFoundError({ message: `Session not found: ${input.sessionID}` })
  359. const info = fromRow(row)
  360. Database.effect(() => Bus.publish(Event.Updated, { info }))
  361. return info
  362. })
  363. },
  364. )
  365. export const setPermission = fn(
  366. z.object({
  367. sessionID: Identifier.schema("session"),
  368. permission: PermissionNext.Ruleset,
  369. }),
  370. async (input) => {
  371. return Database.use((db) => {
  372. const row = db
  373. .update(SessionTable)
  374. .set({ permission: input.permission, time_updated: Date.now() })
  375. .where(eq(SessionTable.id, input.sessionID))
  376. .returning()
  377. .get()
  378. if (!row) throw new NotFoundError({ message: `Session not found: ${input.sessionID}` })
  379. const info = fromRow(row)
  380. Database.effect(() => Bus.publish(Event.Updated, { info }))
  381. return info
  382. })
  383. },
  384. )
  385. export const setRevert = fn(
  386. z.object({
  387. sessionID: Identifier.schema("session"),
  388. revert: Info.shape.revert,
  389. summary: Info.shape.summary,
  390. }),
  391. async (input) => {
  392. return Database.use((db) => {
  393. const row = db
  394. .update(SessionTable)
  395. .set({
  396. revert: input.revert ?? null,
  397. summary_additions: input.summary?.additions,
  398. summary_deletions: input.summary?.deletions,
  399. summary_files: input.summary?.files,
  400. time_updated: Date.now(),
  401. })
  402. .where(eq(SessionTable.id, input.sessionID))
  403. .returning()
  404. .get()
  405. if (!row) throw new NotFoundError({ message: `Session not found: ${input.sessionID}` })
  406. const info = fromRow(row)
  407. Database.effect(() => Bus.publish(Event.Updated, { info }))
  408. return info
  409. })
  410. },
  411. )
  412. export const clearRevert = fn(Identifier.schema("session"), async (sessionID) => {
  413. return Database.use((db) => {
  414. const row = db
  415. .update(SessionTable)
  416. .set({
  417. revert: null,
  418. time_updated: Date.now(),
  419. })
  420. .where(eq(SessionTable.id, sessionID))
  421. .returning()
  422. .get()
  423. if (!row) throw new NotFoundError({ message: `Session not found: ${sessionID}` })
  424. const info = fromRow(row)
  425. Database.effect(() => Bus.publish(Event.Updated, { info }))
  426. return info
  427. })
  428. })
  429. export const setSummary = fn(
  430. z.object({
  431. sessionID: Identifier.schema("session"),
  432. summary: Info.shape.summary,
  433. }),
  434. async (input) => {
  435. return Database.use((db) => {
  436. const row = db
  437. .update(SessionTable)
  438. .set({
  439. summary_additions: input.summary?.additions,
  440. summary_deletions: input.summary?.deletions,
  441. summary_files: input.summary?.files,
  442. time_updated: Date.now(),
  443. })
  444. .where(eq(SessionTable.id, input.sessionID))
  445. .returning()
  446. .get()
  447. if (!row) throw new NotFoundError({ message: `Session not found: ${input.sessionID}` })
  448. const info = fromRow(row)
  449. Database.effect(() => Bus.publish(Event.Updated, { info }))
  450. return info
  451. })
  452. },
  453. )
  454. export const diff = fn(Identifier.schema("session"), async (sessionID) => {
  455. try {
  456. return await Storage.read<Snapshot.FileDiff[]>(["session_diff", sessionID])
  457. } catch {
  458. return []
  459. }
  460. })
  461. export const messages = fn(
  462. z.object({
  463. sessionID: Identifier.schema("session"),
  464. limit: z.number().optional(),
  465. }),
  466. async (input) => {
  467. const result = [] as MessageV2.WithParts[]
  468. for await (const msg of MessageV2.stream(input.sessionID)) {
  469. if (input.limit && result.length >= input.limit) break
  470. result.push(msg)
  471. }
  472. result.reverse()
  473. return result
  474. },
  475. )
  476. export function* list() {
  477. const project = Instance.project
  478. const rel = path.relative(Instance.worktree, Instance.directory)
  479. const suffix = path.sep + rel
  480. const rows = Database.use((db) =>
  481. db
  482. .select()
  483. .from(SessionTable)
  484. .where(
  485. and(
  486. eq(SessionTable.project_id, project.id),
  487. or(eq(SessionTable.directory, Instance.directory), like(SessionTable.directory, `%${suffix}`)),
  488. ),
  489. )
  490. .all(),
  491. )
  492. for (const row of rows) {
  493. yield fromRow(row)
  494. }
  495. }
  496. export const children = fn(Identifier.schema("session"), async (parentID) => {
  497. const project = Instance.project
  498. const rows = Database.use((db) =>
  499. db
  500. .select()
  501. .from(SessionTable)
  502. .where(and(eq(SessionTable.project_id, project.id), eq(SessionTable.parent_id, parentID)))
  503. .all(),
  504. )
  505. return rows.map(fromRow)
  506. })
  507. export const remove = fn(Identifier.schema("session"), async (sessionID) => {
  508. const project = Instance.project
  509. try {
  510. const session = await get(sessionID)
  511. for (const child of await children(sessionID)) {
  512. await remove(child.id)
  513. }
  514. await unshare(sessionID).catch(() => {})
  515. // CASCADE delete handles messages and parts automatically
  516. Database.use((db) => {
  517. db.delete(SessionTable).where(eq(SessionTable.id, sessionID)).run()
  518. Database.effect(() =>
  519. Bus.publish(Event.Deleted, {
  520. info: session,
  521. }),
  522. )
  523. })
  524. } catch (e) {
  525. log.error(e)
  526. }
  527. })
  528. export const updateMessage = fn(MessageV2.Info, async (msg) => {
  529. const time_created = msg.role === "user" ? msg.time.created : msg.time.created
  530. const { id, sessionID, ...data } = msg
  531. Database.use((db) => {
  532. db.insert(MessageTable)
  533. .values({
  534. id,
  535. session_id: sessionID,
  536. time_created,
  537. data,
  538. })
  539. .onConflictDoUpdate({ target: MessageTable.id, set: { data } })
  540. .run()
  541. Database.effect(() =>
  542. Bus.publish(MessageV2.Event.Updated, {
  543. info: msg,
  544. }),
  545. )
  546. })
  547. return msg
  548. })
  549. export const removeMessage = fn(
  550. z.object({
  551. sessionID: Identifier.schema("session"),
  552. messageID: Identifier.schema("message"),
  553. }),
  554. async (input) => {
  555. // CASCADE delete handles parts automatically
  556. Database.use((db) => {
  557. db.delete(MessageTable).where(eq(MessageTable.id, input.messageID)).run()
  558. Database.effect(() =>
  559. Bus.publish(MessageV2.Event.Removed, {
  560. sessionID: input.sessionID,
  561. messageID: input.messageID,
  562. }),
  563. )
  564. })
  565. return input.messageID
  566. },
  567. )
  568. export const removePart = fn(
  569. z.object({
  570. sessionID: Identifier.schema("session"),
  571. messageID: Identifier.schema("message"),
  572. partID: Identifier.schema("part"),
  573. }),
  574. async (input) => {
  575. Database.use((db) => {
  576. db.delete(PartTable).where(eq(PartTable.id, input.partID)).run()
  577. Database.effect(() =>
  578. Bus.publish(MessageV2.Event.PartRemoved, {
  579. sessionID: input.sessionID,
  580. messageID: input.messageID,
  581. partID: input.partID,
  582. }),
  583. )
  584. })
  585. return input.partID
  586. },
  587. )
  588. const UpdatePartInput = MessageV2.Part
  589. export const updatePart = fn(UpdatePartInput, async (part) => {
  590. const { id, messageID, sessionID, ...data } = part
  591. const time = Date.now()
  592. Database.use((db) => {
  593. db.insert(PartTable)
  594. .values({
  595. id,
  596. message_id: messageID,
  597. session_id: sessionID,
  598. time_created: time,
  599. data,
  600. })
  601. .onConflictDoUpdate({ target: PartTable.id, set: { data } })
  602. .run()
  603. Database.effect(() =>
  604. Bus.publish(MessageV2.Event.PartUpdated, {
  605. part,
  606. }),
  607. )
  608. })
  609. return part
  610. })
  611. export const updatePartDelta = fn(
  612. z.object({
  613. sessionID: z.string(),
  614. messageID: z.string(),
  615. partID: z.string(),
  616. field: z.string(),
  617. delta: z.string(),
  618. }),
  619. async (input) => {
  620. Bus.publish(MessageV2.Event.PartDelta, input)
  621. },
  622. )
  623. export const getUsage = fn(
  624. z.object({
  625. model: z.custom<Provider.Model>(),
  626. usage: z.custom<LanguageModelV2Usage>(),
  627. metadata: z.custom<ProviderMetadata>().optional(),
  628. }),
  629. (input) => {
  630. const safe = (value: number) => {
  631. if (!Number.isFinite(value)) return 0
  632. return value
  633. }
  634. const inputTokens = safe(input.usage.inputTokens ?? 0)
  635. const outputTokens = safe(input.usage.outputTokens ?? 0)
  636. const reasoningTokens = safe(input.usage.reasoningTokens ?? 0)
  637. const cacheReadInputTokens = safe(input.usage.cachedInputTokens ?? 0)
  638. const cacheWriteInputTokens = safe(
  639. (input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
  640. // @ts-expect-error
  641. input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
  642. // @ts-expect-error
  643. input.metadata?.["venice"]?.["usage"]?.["cacheCreationInputTokens"] ??
  644. 0) as number,
  645. )
  646. // OpenRouter provides inputTokens as the total count of input tokens (including cached).
  647. // AFAIK other providers (OpenRouter/OpenAI/Gemini etc.) do it the same way e.g. vercel/ai#8794 (comment)
  648. // Anthropic does it differently though - inputTokens doesn't include cached tokens.
  649. // It looks like OpenCode's cost calculation assumes all providers return inputTokens the same way Anthropic does (I'm guessing getUsage logic was originally implemented with anthropic), so it's causing incorrect cost calculation for OpenRouter and others.
  650. const excludesCachedTokens = !!(input.metadata?.["anthropic"] || input.metadata?.["bedrock"])
  651. const adjustedInputTokens = safe(
  652. excludesCachedTokens ? inputTokens : inputTokens - cacheReadInputTokens - cacheWriteInputTokens,
  653. )
  654. const total = iife(() => {
  655. // Anthropic doesn't provide total_tokens, also ai sdk will vastly undercount if we
  656. // don't compute from components
  657. if (
  658. input.model.api.npm === "@ai-sdk/anthropic" ||
  659. input.model.api.npm === "@ai-sdk/amazon-bedrock" ||
  660. input.model.api.npm === "@ai-sdk/google-vertex/anthropic"
  661. ) {
  662. return adjustedInputTokens + outputTokens + cacheReadInputTokens + cacheWriteInputTokens
  663. }
  664. return input.usage.totalTokens
  665. })
  666. const tokens = {
  667. total,
  668. input: adjustedInputTokens,
  669. output: outputTokens,
  670. reasoning: reasoningTokens,
  671. cache: {
  672. write: cacheWriteInputTokens,
  673. read: cacheReadInputTokens,
  674. },
  675. }
  676. const costInfo =
  677. input.model.cost?.experimentalOver200K && tokens.input + tokens.cache.read > 200_000
  678. ? input.model.cost.experimentalOver200K
  679. : input.model.cost
  680. return {
  681. cost: safe(
  682. new Decimal(0)
  683. .add(new Decimal(tokens.input).mul(costInfo?.input ?? 0).div(1_000_000))
  684. .add(new Decimal(tokens.output).mul(costInfo?.output ?? 0).div(1_000_000))
  685. .add(new Decimal(tokens.cache.read).mul(costInfo?.cache?.read ?? 0).div(1_000_000))
  686. .add(new Decimal(tokens.cache.write).mul(costInfo?.cache?.write ?? 0).div(1_000_000))
  687. // TODO: update models.dev to have better pricing model, for now:
  688. // charge reasoning tokens at the same rate as output tokens
  689. .add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000))
  690. .toNumber(),
  691. ),
  692. tokens,
  693. }
  694. },
  695. )
  696. export class BusyError extends Error {
  697. constructor(public readonly sessionID: string) {
  698. super(`Session ${sessionID} is busy`)
  699. }
  700. }
  701. export const initialize = fn(
  702. z.object({
  703. sessionID: Identifier.schema("session"),
  704. modelID: z.string(),
  705. providerID: z.string(),
  706. messageID: Identifier.schema("message"),
  707. }),
  708. async (input) => {
  709. await SessionPrompt.command({
  710. sessionID: input.sessionID,
  711. messageID: input.messageID,
  712. model: input.providerID + "/" + input.modelID,
  713. command: Command.Default.INIT,
  714. arguments: "",
  715. })
  716. },
  717. )
  718. }