rework types

This commit is contained in:
Dax Raad
2025-05-29 10:21:59 -04:00
parent d70201cd93
commit 33a831d2be
12 changed files with 494 additions and 432 deletions

View File

@@ -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);

View File

@@ -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;
},

View File

@@ -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()) {

View File

@@ -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"],
});
});
});

View File

@@ -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;

View File

@@ -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",
});

View File

@@ -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;

160
js/src/session/message.ts Normal file
View File

@@ -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>;
}

View File

@@ -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 = {