@neutrome/lilsdk-ts 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # `@neutrome/lilsdk-ts`
2
+
3
+ High-level TypeScript SDK for transport-neutral model authoring on top of `@neutrome/lil-engine`.
4
+
5
+ ```ts
6
+ import {
7
+ appendSyntheticToolExchange,
8
+ executeWithModel,
9
+ observeExecutionStream,
10
+ writeReasoning,
11
+ type ModelHandler,
12
+ } from "@neutrome/lilsdk-ts";
13
+ ```
14
+
15
+ This package owns:
16
+
17
+ - model execution interfaces
18
+ - request/program rewriting helpers
19
+ - stream passthrough helpers
20
+ - multi-pass authoring primitives used by virtual models such as `enei-1`
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@neutrome/lilsdk-ts",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./src/index.ts"
7
+ },
8
+ "scripts": {
9
+ "test": "vitest run",
10
+ "typecheck": "tsc -p tsconfig.json --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "@neutrome/lil-engine": "workspace:*"
14
+ },
15
+ "devDependencies": {
16
+ "@types/node": "^25.9.3",
17
+ "typescript": "^6.0.3",
18
+ "vitest": "^4.1.6"
19
+ }
20
+ }
@@ -0,0 +1,64 @@
1
+ import { setModel, setStreaming, type Program } from "@neutrome/lil-engine";
2
+ import type { ExecuteModelOptions, ModelRuntime, OutputSink, StreamModelOptions } from "./types.ts";
3
+
4
+ export async function executeWithModel(
5
+ request: Program,
6
+ runtime: ModelRuntime,
7
+ model: string,
8
+ options?: ExecuteModelOptions,
9
+ ): Promise<Program> {
10
+ return runtime.execute(setStreaming(setModel(request, model), false), options);
11
+ }
12
+
13
+ export async function pipeProgramStream(
14
+ source: AsyncIterable<Program>,
15
+ sink: OutputSink,
16
+ ): Promise<void> {
17
+ for await (const chunk of source) {
18
+ await sink.write(chunk);
19
+ }
20
+ }
21
+
22
+ export async function streamWithModel(
23
+ request: Program,
24
+ runtime: ModelRuntime,
25
+ sink: OutputSink,
26
+ model: string,
27
+ options: StreamModelOptions = {},
28
+ ): Promise<void> {
29
+ const { close = true, ...invokeOptions } = options;
30
+ await pipeProgramStream(
31
+ runtime.stream(setStreaming(setModel(request, model), true), invokeOptions),
32
+ sink,
33
+ );
34
+ if (options?.close !== false) {
35
+ await sink.close();
36
+ }
37
+ }
38
+
39
+ export function passthroughPrograms(
40
+ programs: Iterable<Program> | AsyncIterable<Program>,
41
+ sink: OutputSink,
42
+ options: { close?: boolean } = {},
43
+ ): Promise<void> {
44
+ return passthroughProgramsInternal(programs, sink, options);
45
+ }
46
+
47
+ async function passthroughProgramsInternal(
48
+ programs: Iterable<Program> | AsyncIterable<Program>,
49
+ sink: OutputSink,
50
+ options: { close?: boolean },
51
+ ): Promise<void> {
52
+ if (Symbol.asyncIterator in Object(programs)) {
53
+ for await (const chunk of programs as AsyncIterable<Program>) {
54
+ await sink.write(chunk);
55
+ }
56
+ } else {
57
+ for (const chunk of programs as Iterable<Program>) {
58
+ await sink.write(chunk);
59
+ }
60
+ }
61
+ if (options?.close !== false) {
62
+ await sink.close();
63
+ }
64
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ export type { ModelHandler, ModelRuntime, OutputSink } from "./types.ts";
2
+ export type { ExecuteModelOptions, StreamModelOptions } from "./types.ts";
3
+ export type { ObservedExecution, StreamObservationHooks } from "./observe.ts";
4
+ export type { SyntheticToolExchange } from "./request.ts";
5
+
6
+ export { appendSyntheticToolExchange } from "./request.ts";
7
+ export { writeReasoning } from "./output.ts";
8
+ export { executeWithModel, passthroughPrograms, pipeProgramStream, streamWithModel } from "./execution.ts";
9
+ export { observeExecutionStream } from "./observe.ts";
package/src/observe.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { observeExecutionStream } from "@neutrome/lil-engine";
2
+ export type { ObservedExecution, StreamObservationHooks } from "@neutrome/lil-engine";
package/src/output.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { createProgram, Opcode } from "@neutrome/lil-engine";
2
+ import type { OutputSink } from "./types.ts";
3
+
4
+ export async function writeReasoning(sink: OutputSink, text: string): Promise<void> {
5
+ await sink.write(createProgram({
6
+ code: [
7
+ { opcode: Opcode.STREAM_THINK_DELTA, value: { kind: "string", value: text } },
8
+ ],
9
+ }));
10
+ }
package/src/request.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { appendToolExchange, type Program } from "@neutrome/lil-engine";
2
+
3
+ export type SyntheticToolExchange = {
4
+ callId: string;
5
+ name: string;
6
+ args: string;
7
+ result: string;
8
+ };
9
+
10
+ export function appendSyntheticToolExchange(
11
+ request: Program,
12
+ exchange: SyntheticToolExchange,
13
+ ): Program {
14
+ return appendToolExchange(
15
+ request,
16
+ exchange.callId,
17
+ exchange.name,
18
+ exchange.args,
19
+ exchange.result,
20
+ );
21
+ }
package/src/types.ts ADDED
@@ -0,0 +1,29 @@
1
+ import type { Program } from "@neutrome/lil-engine";
2
+ import type { ExecutionRuntime, InvokeOptions } from "@neutrome/lil-engine";
3
+
4
+ export type ModelRuntime = Pick<ExecutionRuntime, "execute" | "stream">;
5
+
6
+ export type OutputSink = {
7
+ write(chunk: Program): void | Promise<void>;
8
+ close(): void | Promise<void>;
9
+ };
10
+
11
+ export type ExecuteModelOptions = InvokeOptions;
12
+
13
+ export type StreamModelOptions = InvokeOptions & {
14
+ close?: boolean;
15
+ };
16
+
17
+ export type ModelHandler = {
18
+ execute(
19
+ request: Program,
20
+ runtime: ModelRuntime,
21
+ options?: ExecuteModelOptions,
22
+ ): Promise<Program>;
23
+ stream(
24
+ request: Program,
25
+ runtime: ModelRuntime,
26
+ sink: OutputSink,
27
+ options?: StreamModelOptions,
28
+ ): Promise<void>;
29
+ };
@@ -0,0 +1,185 @@
1
+ import {
2
+ appendAssistantMessage,
3
+ contentText,
4
+ emitChatCompletionsRequest,
5
+ emitChatCompletionsStreamChunk,
6
+ getModel,
7
+ isStreaming,
8
+ parseChatCompletionsRequest,
9
+ parseChatCompletionsStreamChunk,
10
+ prependSystemPrompt,
11
+ type Program,
12
+ } from "@neutrome/lil-engine";
13
+ import { describe, expect, it } from "vitest";
14
+ import {
15
+ appendSyntheticToolExchange,
16
+ executeWithModel,
17
+ observeExecutionStream,
18
+ passthroughPrograms,
19
+ streamWithModel,
20
+ writeReasoning,
21
+ type ModelRuntime,
22
+ type OutputSink,
23
+ } from "../src/index.ts";
24
+
25
+ const encoder = new TextEncoder();
26
+ const decoder = new TextDecoder();
27
+
28
+ describe("@neutrome/lilsdk-ts", () => {
29
+ it("observes tool-call streams and supports passthrough", async () => {
30
+ const observed = await observeExecutionStream((async function* () {
31
+ yield parseChatCompletionsStreamChunk(encoder.encode(JSON.stringify({
32
+ id: "tool-stream",
33
+ object: "chat.completion.chunk",
34
+ model: "smart",
35
+ choices: [{ index: 0, delta: { role: "assistant" } }],
36
+ })));
37
+ yield parseChatCompletionsStreamChunk(encoder.encode(JSON.stringify({
38
+ id: "tool-stream",
39
+ object: "chat.completion.chunk",
40
+ model: "smart",
41
+ choices: [{
42
+ index: 0,
43
+ delta: {
44
+ tool_calls: [{
45
+ index: 0,
46
+ id: "call_lookup",
47
+ type: "function",
48
+ function: { name: "lookup", arguments: "{\"q\":\"kyiv\"}" },
49
+ }],
50
+ },
51
+ }],
52
+ })));
53
+ yield parseChatCompletionsStreamChunk(encoder.encode(JSON.stringify({
54
+ id: "tool-stream",
55
+ object: "chat.completion.chunk",
56
+ model: "smart",
57
+ choices: [{ index: 0, delta: {}, finish_reason: "tool_calls" }],
58
+ })));
59
+ })());
60
+
61
+ expect(observed.mode).toBe("tool");
62
+ if (observed.mode !== "tool") {
63
+ throw new Error("expected tool mode");
64
+ }
65
+
66
+ const emitted: string[] = [];
67
+ const sink: OutputSink = {
68
+ write(chunk) {
69
+ emitted.push(decoder.decode(emitChatCompletionsStreamChunk(chunk)));
70
+ },
71
+ close() {
72
+ emitted.push("[DONE]");
73
+ },
74
+ };
75
+
76
+ await passthroughPrograms(observed.chunks, sink);
77
+
78
+ expect(emitted.at(-1)).toBe("[DONE]");
79
+ const parsed = emitted.slice(0, -1).map((chunk) => JSON.parse(chunk));
80
+ expect(parsed.some((chunk) => chunk.choices?.[0]?.delta?.tool_calls)).toBe(true);
81
+ });
82
+
83
+ it("appends synthetic tool exchanges to a request program", () => {
84
+ const request = parseChatCompletionsRequest(encoder.encode(JSON.stringify({
85
+ model: "virtual-model",
86
+ messages: [{ role: "user", content: "hello" }],
87
+ })));
88
+
89
+ const updated = appendSyntheticToolExchange(request, {
90
+ callId: "knowledge_0",
91
+ name: "knowledge",
92
+ args: "{\"question\":\"thinking\"}",
93
+ result: "draft answer",
94
+ });
95
+
96
+ const emitted = JSON.parse(decoder.decode(emitChatCompletionsRequest(updated)));
97
+ expect(emitted.messages).toHaveLength(3);
98
+ expect(emitted.messages[1].tool_calls[0].function.name).toBe("knowledge");
99
+ expect(emitted.messages[2].tool_call_id).toBe("knowledge_0");
100
+ expect(JSON.parse(decoder.decode(emitChatCompletionsRequest(request))).messages).toHaveLength(1);
101
+ });
102
+
103
+ it("supports chained model execution across multiple passes", async () => {
104
+ const calls: Array<{ model: string; streaming: boolean }> = [];
105
+ const runtime: ModelRuntime = {
106
+ async execute(request) {
107
+ calls.push({ model: getModel(request) ?? "", streaming: isStreaming(request) });
108
+ if (calls.length === 1) {
109
+ return appendAssistantMessage({ code: [], buffers: [] }, "draft answer");
110
+ }
111
+ return appendAssistantMessage({ code: [], buffers: [] }, "final answer");
112
+ },
113
+ async *stream(): AsyncIterable<Program> {
114
+ throw new Error("streaming path is not used in this test");
115
+ },
116
+ };
117
+
118
+ const request = parseChatCompletionsRequest(encoder.encode(JSON.stringify({
119
+ model: "virtual-model",
120
+ messages: [{ role: "user", content: "hello" }],
121
+ })));
122
+ const first = await executeWithModel(request, runtime, "smart-model");
123
+ const draft = contentText(first);
124
+
125
+ let secondRequest = appendSyntheticToolExchange(request, {
126
+ callId: "knowledge_0",
127
+ name: "knowledge",
128
+ args: "{\"question\":\"thinking\"}",
129
+ result: draft,
130
+ });
131
+ secondRequest = prependSystemPrompt(secondRequest, "system prompt");
132
+
133
+ const second = await executeWithModel(secondRequest, runtime, "base-model");
134
+
135
+ expect(calls).toEqual([
136
+ { model: "smart-model", streaming: false },
137
+ { model: "base-model", streaming: false },
138
+ ]);
139
+ expect(contentText(second)).toBe("final answer");
140
+ expect(getModel(request)).toBe("virtual-model");
141
+ });
142
+
143
+ it("streams model output through a sink and writes reasoning helpers", async () => {
144
+ const request = parseChatCompletionsRequest(encoder.encode(JSON.stringify({
145
+ model: "virtual-model",
146
+ messages: [{ role: "user", content: "hello" }],
147
+ })));
148
+ const emitted: string[] = [];
149
+ const sink: OutputSink = {
150
+ write(chunk) {
151
+ emitted.push(decoder.decode(emitChatCompletionsStreamChunk(chunk)));
152
+ },
153
+ close() {
154
+ emitted.push("[DONE]");
155
+ },
156
+ };
157
+
158
+ const runtime: ModelRuntime = {
159
+ async execute() {
160
+ throw new Error("non-streaming path is not used in this test");
161
+ },
162
+ async *stream(program) {
163
+ expect(getModel(program)).toBe("base-model");
164
+ expect(isStreaming(program)).toBe(true);
165
+ yield parseChatCompletionsStreamChunk(encoder.encode(JSON.stringify({
166
+ id: "stream-response",
167
+ model: "base-model",
168
+ choices: [{ index: 0, delta: { content: "hello" } }],
169
+ })));
170
+ yield parseChatCompletionsStreamChunk(encoder.encode(JSON.stringify({
171
+ id: "stream-response",
172
+ model: "base-model",
173
+ choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
174
+ })));
175
+ },
176
+ };
177
+
178
+ await writeReasoning(sink, "thinking");
179
+ await streamWithModel(request, runtime, sink, "base-model");
180
+
181
+ expect(emitted.at(-1)).toBe("[DONE]");
182
+ expect(JSON.parse(emitted[0]!).choices[0].delta.reasoning_content).toBe("thinking");
183
+ expect(JSON.parse(emitted[1]!).choices[0].delta.content).toBe("hello");
184
+ });
185
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "types": ["node", "vitest/globals"],
5
+ "outDir": "dist"
6
+ },
7
+ "include": ["src/**/*.ts", "test/**/*.ts"],
8
+ "exclude": ["dist", "node_modules"]
9
+ }
@@ -0,0 +1,3 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({});