diff options
| author | Dax Raad <[email protected]> | 2025-05-29 10:21:59 -0400 |
|---|---|---|
| committer | Dax Raad <[email protected]> | 2025-05-29 10:22:07 -0400 |
| commit | 33a831d2be1fd7bea60421287f118be0bd968650 (patch) | |
| tree | 9b82f8b958fa78c18b13284b9c7cd496dcec651e /js | |
| parent | d70201cd9365aec6c88f9794eb63f411f5040cb9 (diff) | |
| download | opencode-33a831d2be1fd7bea60421287f118be0bd968650.tar.gz opencode-33a831d2be1fd7bea60421287f118be0bd968650.zip | |
rework types
Diffstat (limited to 'js')
| -rw-r--r-- | js/src/app/config.ts | 13 | ||||
| -rw-r--r-- | js/src/app/index.ts | 8 | ||||
| -rw-r--r-- | js/src/bus/index.ts | 11 | ||||
| -rw-r--r-- | js/src/index.ts | 7 | ||||
| -rw-r--r-- | js/src/llm/llm.ts | 9 | ||||
| -rw-r--r-- | js/src/server/message.ts | 135 | ||||
| -rw-r--r-- | js/src/server/server.ts | 10 | ||||
| -rw-r--r-- | js/src/session/message.ts | 160 | ||||
| -rw-r--r-- | js/src/session/session.ts | 50 |
9 files changed, 214 insertions, 189 deletions
diff --git a/js/src/app/config.ts b/js/src/app/config.ts index 1f1491540..348eb77c3 100644 --- a/js/src/app/config.ts +++ b/js/src/app/config.ts @@ -1,11 +1,16 @@ import path from "path"; import { Log } from "../util/log"; import { z } from "zod"; -import { LLM } from "../llm/llm"; +import { App } from "."; export namespace Config { const log = Log.create({ service: "config" }); + export const state = App.state("config", async (app) => { + const result = await load(app.root); + return result; + }); + export const Model = z.object({ name: z.string().optional(), cost: z.object({ @@ -35,7 +40,11 @@ export namespace Config { export type Info = z.output<typeof Info>; - export async function load(directory: string) { + export function get() { + return state(); + } + + async function load(directory: string) { let result: Info = {}; for (const file of ["opencode.jsonc", "opencode.json"]) { const resolved = path.join(directory, file); diff --git a/js/src/app/index.ts b/js/src/app/index.ts index f0d371a34..0c6260bc7 100644 --- a/js/src/app/index.ts +++ b/js/src/app/index.ts @@ -2,7 +2,6 @@ import fs from "fs/promises"; import { AppPath } from "./path"; import { Log } from "../util/log"; import { Context } from "../util/context"; -import { Config } from "./config"; export namespace App { const log = Log.create({ service: "app" }); @@ -16,10 +15,6 @@ export namespace App { await fs.mkdir(dataDir, { recursive: true }); await Log.file(input.directory); - log.info("creating"); - - const config = await Config.load(input.directory); - log.info("created", { path: dataDir }); const services = new Map< @@ -34,9 +29,6 @@ export namespace App { get services() { return services; }, - get config() { - return config; - }, get root() { return input.directory; }, diff --git a/js/src/bus/index.ts b/js/src/bus/index.ts index 15d2b1107..82bc614e0 100644 --- a/js/src/bus/index.ts +++ b/js/src/bus/index.ts @@ -30,6 +30,17 @@ export namespace Bus { return result; } + export function payloads() { + return registry + .entries() + .map(([type, def]) => + z.object({ + type: z.string("hey"), + }), + ) + .toArray(); + } + export function specs() { const children = {} as any; for (const [type, def] of registry.entries()) { diff --git a/js/src/index.ts b/js/src/index.ts index 9ba9535a1..d5b388166 100644 --- a/js/src/index.ts +++ b/js/src/index.ts @@ -15,6 +15,13 @@ cli.command("", "Start the opencode in interactive mode").action(async () => { await App.provide({ directory: process.cwd() }, async () => { await Share.init(); Server.listen(); + + Bun.spawnSync({ + stderr: "inherit", + stdout: "inherit", + stdin: "inherit", + cmd: ["go", "run", "cmd/main.go"], + }); }); }); diff --git a/js/src/llm/llm.ts b/js/src/llm/llm.ts index 9c409c604..e6230584a 100644 --- a/js/src/llm/llm.ts +++ b/js/src/llm/llm.ts @@ -5,7 +5,7 @@ import path from "path"; import type { LanguageModel, Provider } from "ai"; import { NoSuchModelError } from "ai"; -import type { Config } from "../app/config"; +import { Config } from "../app/config"; import { BunProc } from "../bun"; import { Global } from "../global"; @@ -25,8 +25,8 @@ export namespace LLM { name: "Claude 4 Sonnet", cost: { input: 3.0 / 1_000_000, - inputCached: 3.75 / 1_000_000, output: 15.0 / 1_000_000, + inputCached: 3.75 / 1_000_000, outputCached: 0.3 / 1_000_000, }, contextWindow: 200000, @@ -77,6 +77,7 @@ export namespace LLM { }; const state = App.state("llm", async (app) => { + const config = await Config.get(); const providers: Record< string, { @@ -89,11 +90,11 @@ export namespace LLM { { info: Config.Model; instance: LanguageModel } >(); - const list = mergeDeep(NATIVE_PROVIDERS, app.config.providers ?? {}); + const list = mergeDeep(NATIVE_PROVIDERS, config.providers ?? {}); for (const [providerID, providerInfo] of Object.entries(list)) { if ( - !app.config.providers?.[providerID] && + !config.providers?.[providerID] && !AUTODETECT[providerID]?.some((env) => process.env[env]) ) continue; diff --git a/js/src/server/message.ts b/js/src/server/message.ts deleted file mode 100644 index 4ad301a35..000000000 --- a/js/src/server/message.ts +++ /dev/null @@ -1,135 +0,0 @@ -import z from "zod"; - -const ToolCall = z - .object({ - state: z.literal("call"), - step: z.number().optional(), - toolCallId: z.string(), - toolName: z.string(), - args: z.record(z.string(), z.any()), - }) - .openapi({ - ref: "Session.Message.ToolInvocation.ToolCall", - }); - -const ToolPartialCall = z - .object({ - state: z.literal("partial-call"), - step: z.number().optional(), - toolCallId: z.string(), - toolName: z.string(), - args: z.record(z.string(), z.any()), - }) - .openapi({ - ref: "Session.Message.ToolInvocation.ToolPartialCall", - }); - -const ToolResult = z - .object({ - state: z.literal("result"), - step: z.number().optional(), - toolCallId: z.string(), - toolName: z.string(), - args: z.record(z.string(), z.any()), - result: z.string(), - }) - .openapi({ - ref: "Session.Message.ToolInvocation.ToolResult", - }); - -const ToolInvocation = z - .discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult]) - .openapi({ - ref: "Session.Message.ToolInvocation", - }); -export type ToolInvocation = z.infer<typeof ToolInvocation>; - -const TextPart = z - .object({ - type: z.literal("text"), - text: z.string(), - }) - .openapi({ - ref: "Session.Message.Part.Text", - }); - -const ReasoningPart = z - .object({ - type: z.literal("reasoning"), - text: z.string(), - providerMetadata: z.record(z.any()).optional(), - }) - .openapi({ - ref: "Session.Message.Part.Reasoning", - }); - -const ToolInvocationPart = z - .object({ - type: z.literal("tool-invocation"), - toolInvocation: ToolInvocation, - }) - .openapi({ - ref: "Session.Message.Part.ToolInvocation", - }); - -const SourceUrlPart = z - .object({ - type: z.literal("source-url"), - sourceId: z.string(), - url: z.string(), - title: z.string().optional(), - providerMetadata: z.record(z.any()).optional(), - }) - .openapi({ - ref: "Session.Message.Part.SourceUrl", - }); - -const FilePart = z - .object({ - type: z.literal("file"), - mediaType: z.string(), - filename: z.string().optional(), - url: z.string(), - }) - .openapi({ - ref: "Session.Message.Part.File", - }); - -const StepStartPart = z - .object({ - type: z.literal("step-start"), - }) - .openapi({ - ref: "Session.Message.Part.StepStart", - }); - -const Part = z - .discriminatedUnion("type", [ - TextPart, - ReasoningPart, - ToolInvocationPart, - SourceUrlPart, - FilePart, - StepStartPart, - ]) - .openapi({ - ref: "Session.Message.Part", - }); - -export const SessionMessage = z - .object({ - id: z.string(), - role: z.enum(["system", "user", "assistant"]), - parts: z.array(Part), - metadata: z.object({ - time: z.object({ - created: z.number(), - completed: z.number().optional(), - }), - sessionID: z.string(), - tool: z.record(z.string(), z.any()), - }), - }) - .openapi({ - ref: "Session.Message", - }); diff --git a/js/src/server/server.ts b/js/src/server/server.ts index 5fb0dfe62..2f388a183 100644 --- a/js/src/server/server.ts +++ b/js/src/server/server.ts @@ -9,7 +9,7 @@ import { z } from "zod"; import "zod-openapi/extend"; import { Config } from "../app/config"; import { LLM } from "../llm/llm"; -import { SessionMessage } from "./message"; +import { Message } from "../session/message"; const SessionInfo = Session.Info.openapi({ ref: "Session.Info", @@ -40,6 +40,7 @@ export namespace Server { version: "1.0.0", description: "opencode api", }, + openapi: "3.0.0", }, }), ) @@ -120,7 +121,7 @@ export namespace Server { description: "Successfully created session", content: { "application/json": { - schema: resolver(SessionMessage.array()), + schema: resolver(Message.Info.array()), }, }, }, @@ -194,7 +195,7 @@ export namespace Server { description: "Chat with a model", content: { "application/json": { - schema: resolver(SessionMessage), + schema: resolver(Message.Info), }, }, }, @@ -206,7 +207,7 @@ export namespace Server { sessionID: z.string(), providerID: z.string(), modelID: z.string(), - parts: SessionMessage.shape.parts, + parts: Message.Part.array(), }), ), async (c) => { @@ -252,6 +253,7 @@ export namespace Server { version: "1.0.0", description: "opencode api", }, + openapi: "3.0.0", }, }); return result; diff --git a/js/src/session/message.ts b/js/src/session/message.ts new file mode 100644 index 000000000..75c22ef0b --- /dev/null +++ b/js/src/session/message.ts @@ -0,0 +1,160 @@ +import z from "zod"; + +export namespace Message { + export const ToolCall = z + .object({ + state: z.literal("call"), + step: z.number().optional(), + toolCallId: z.string(), + toolName: z.string(), + args: z.custom<Required<unknown>>(), + }) + .openapi({ + ref: "Message.ToolInvocation.ToolCall", + }); + export type ToolCall = z.infer<typeof ToolCall>; + + export const ToolPartialCall = z + .object({ + state: z.literal("partial-call"), + step: z.number().optional(), + toolCallId: z.string(), + toolName: z.string(), + args: z.custom<Required<unknown>>(), + }) + .openapi({ + ref: "Message.ToolInvocation.ToolPartialCall", + }); + export type ToolPartialCall = z.infer<typeof ToolPartialCall>; + + export const ToolResult = z + .object({ + state: z.literal("result"), + step: z.number().optional(), + toolCallId: z.string(), + toolName: z.string(), + args: z.custom<Required<unknown>>(), + result: z.string(), + }) + .openapi({ + ref: "Message.ToolInvocation.ToolResult", + }); + export type ToolResult = z.infer<typeof ToolResult>; + + export const ToolInvocation = z + .discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult]) + .openapi({ + ref: "Message.ToolInvocation", + }); + export type ToolInvocation = z.infer<typeof ToolInvocation>; + + export const TextPart = z + .object({ + type: z.literal("text"), + text: z.string(), + }) + .openapi({ + ref: "Message.Part.Text", + }); + export type TextPart = z.infer<typeof TextPart>; + + export const ReasoningPart = z + .object({ + type: z.literal("reasoning"), + text: z.string(), + providerMetadata: z.record(z.any()).optional(), + }) + .openapi({ + ref: "Message.Part.Reasoning", + }); + export type ReasoningPart = z.infer<typeof ReasoningPart>; + + export const ToolInvocationPart = z + .object({ + type: z.literal("tool-invocation"), + toolInvocation: ToolInvocation, + }) + .openapi({ + ref: "Message.Part.ToolInvocation", + }); + export type ToolInvocationPart = z.infer<typeof ToolInvocationPart>; + + export const SourceUrlPart = z + .object({ + type: z.literal("source-url"), + sourceId: z.string(), + url: z.string(), + title: z.string().optional(), + providerMetadata: z.record(z.any()).optional(), + }) + .openapi({ + ref: "Message.Part.SourceUrl", + }); + export type SourceUrlPart = z.infer<typeof SourceUrlPart>; + + export const FilePart = z + .object({ + type: z.literal("file"), + mediaType: z.string(), + filename: z.string().optional(), + url: z.string(), + }) + .openapi({ + ref: "Message.Part.File", + }); + export type FilePart = z.infer<typeof FilePart>; + + export const StepStartPart = z + .object({ + type: z.literal("step-start"), + }) + .openapi({ + ref: "Message.Part.StepStart", + }); + export type StepStartPart = z.infer<typeof StepStartPart>; + + export const Part = z + .discriminatedUnion("type", [ + TextPart, + ReasoningPart, + ToolInvocationPart, + SourceUrlPart, + FilePart, + StepStartPart, + ]) + .openapi({ + ref: "Message.Part", + }); + export type Part = z.infer<typeof Part>; + + export const Info = z + .object({ + id: z.string(), + role: z.enum(["system", "user", "assistant"]), + parts: z.array(Part), + metadata: z.object({ + time: z.object({ + created: z.number(), + completed: z.number().optional(), + }), + sessionID: z.string(), + tool: z.record(z.string(), z.any()), + assistant: z + .object({ + modelID: z.string(), + providerID: z.string(), + cost: z.number(), + tokens: z.object({ + input: z.number(), + output: z.number(), + reasoning: z.number(), + }), + }) + .optional(), + }), + }) + .openapi({ + ref: "Message.Info", + }); + export type Info = z.infer<typeof Info>; +} diff --git a/js/src/session/session.ts b/js/src/session/session.ts index 110315619..abeb29842 100644 --- a/js/src/session/session.ts +++ b/js/src/session/session.ts @@ -9,11 +9,6 @@ import { generateText, stepCountIs, streamText, - type TextUIPart, - type ToolInvocationUIPart, - type UIDataTypes, - type UIMessage, - type UIMessagePart, } from "ai"; import { z } from "zod"; import * as tools from "../tool"; @@ -22,8 +17,8 @@ import { Decimal } from "decimal.js"; import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"; import PROMPT_TITLE from "./prompt/title.txt"; -import type { Tool } from "../tool/tool"; import { Share } from "../share/share"; +import type { Message } from "./message"; export namespace Session { const log = Log.create({ service: "session" }); @@ -35,28 +30,9 @@ export namespace Session { }); export type Info = z.output<typeof Info>; - export type Message = UIMessage<{ - assistant?: { - modelID: string; - providerID: string; - cost: number; - tokens: { - input: number; - output: number; - reasoning: number; - }; - }; - time: { - created: number; - completed?: number; - }; - sessionID: string; - tool: Record<string, Tool.Metadata>; - }>; - const state = App.state("session", () => { const sessions = new Map<string, Info>(); - const messages = new Map<string, Message[]>(); + const messages = new Map<string, Message.Info[]>(); return { sessions, @@ -112,10 +88,10 @@ export namespace Session { if (match) { return match; } - const result = [] as Message[]; + const result = [] as Message.Info[]; const list = Storage.list("session/message/" + sessionID); for await (const p of list) { - const read = await Storage.readJSON<Message>(p); + const read = await Storage.readJSON<Message.Info>(p); result.push(read); } state().messages.set(sessionID, result); @@ -143,13 +119,13 @@ export namespace Session { sessionID: string; providerID: string; modelID: string; - parts: UIMessagePart<UIDataTypes>[]; + parts: Message.Part[]; }) { const l = log.clone().tag("session", input.sessionID); l.info("chatting"); const model = await LLM.findModel(input.providerID, input.modelID); const msgs = await messages(input.sessionID); - async function write(msg: Message) { + async function write(msg: Message.Info) { return Storage.writeJSON( "session/message/" + input.sessionID + "/" + msg.id, msg, @@ -157,7 +133,7 @@ export namespace Session { } const app = await App.use(); if (msgs.length === 0) { - const system: Message = { + const system: Message.Info = { id: Identifier.ascending("message"), role: "system", parts: [ @@ -208,7 +184,7 @@ export namespace Session { }); await write(system); } - const msg: Message = { + const msg: Message.Info = { role: "user", id: Identifier.ascending("message"), parts: input.parts, @@ -223,7 +199,7 @@ export namespace Session { msgs.push(msg); await write(msg); - const next: Message = { + const next: Message.Info = { id: Identifier.ascending("message"), role: "assistant", parts: [], @@ -269,7 +245,7 @@ export namespace Session { }); msgs.push(next); - let text: TextUIPart | undefined; + let text: Message.TextPart | undefined; const reader = result.toUIMessageStream().getReader(); while (true) { const result = await reader.read().catch((e) => { @@ -308,6 +284,8 @@ export namespace Session { toolInvocation: { state: "call", ...value, + // hack until zod v4 + args: value.args as any, }, }); break; @@ -317,8 +295,8 @@ export namespace Session { (p) => p.type === "tool-invocation" && p.toolInvocation.toolCallId === value.toolCallId, - ) as ToolInvocationUIPart | undefined; - if (match) { + ); + if (match && match.type === "tool-invocation") { const { output, metadata } = value.result as any; next.metadata!.tool[value.toolCallId] = metadata; match.toolInvocation = { |
