mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-24 19:24:22 +01:00
rework types
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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"],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
@@ -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
160
js/src/session/message.ts
Normal 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>;
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user