@neutrome/lilsdk-ts 0.3.1 → 0.3.2
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/package.json +1 -1
- package/src/managed/index.ts +5 -2
- package/src/managed/two-pass-request.ts +164 -0
- package/src/managed/twoPassExecutor.ts +109 -135
- package/test/lilsdk-ts.test.ts +87 -8
package/package.json
CHANGED
package/src/managed/index.ts
CHANGED
|
@@ -5,11 +5,14 @@ export {
|
|
|
5
5
|
createTwoPassExecutor,
|
|
6
6
|
INTERNAL_DRAFT_CALL_ID,
|
|
7
7
|
INTERNAL_DRAFT_TOOL_NAME,
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
invokeExecutor,
|
|
9
|
+
streamExecutor,
|
|
10
10
|
} from "./twoPassExecutor.ts";
|
|
11
11
|
|
|
12
12
|
export type {
|
|
13
13
|
InternalDraft,
|
|
14
|
+
ModelExecutor,
|
|
14
15
|
TwoPassExecutorOptions,
|
|
16
|
+
TwoPassSettings,
|
|
17
|
+
TwoPassSettingsResolver,
|
|
15
18
|
} from "./twoPassExecutor.ts";
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendInstructions,
|
|
3
|
+
contentText,
|
|
4
|
+
insertBefore,
|
|
5
|
+
messageText,
|
|
6
|
+
messages,
|
|
7
|
+
removeRange,
|
|
8
|
+
type Program,
|
|
9
|
+
} from "@neutrome/lil-engine";
|
|
10
|
+
import { Opcode } from "@neutrome/lil-engine";
|
|
11
|
+
import {
|
|
12
|
+
appendToolInteraction,
|
|
13
|
+
makeMessage,
|
|
14
|
+
prependSystemPrompt,
|
|
15
|
+
} from "../synthetic/index.ts";
|
|
16
|
+
import type { ExecutorContext } from "../types.ts";
|
|
17
|
+
|
|
18
|
+
export const INTERNAL_DRAFT_TOOL_NAME = "knowledge";
|
|
19
|
+
export const INTERNAL_DRAFT_CALL_ID = "knowledge_0";
|
|
20
|
+
|
|
21
|
+
export type InternalDraft =
|
|
22
|
+
| string
|
|
23
|
+
| readonly string[]
|
|
24
|
+
| readonly { text: string }[];
|
|
25
|
+
|
|
26
|
+
export type TwoPassSettings = {
|
|
27
|
+
reasoningLevel?: string;
|
|
28
|
+
systemPrompt?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type TwoPassSettingsResolver = (
|
|
32
|
+
request: Program,
|
|
33
|
+
ctx: ExecutorContext,
|
|
34
|
+
) => TwoPassSettings | null | Promise<TwoPassSettings | null>;
|
|
35
|
+
|
|
36
|
+
export type TwoPassRequestOptions = {
|
|
37
|
+
maxTotalContextLength?: number;
|
|
38
|
+
buildFinalRequest?: (request: Program, draft: string) => Program;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function appendInternalDraft(
|
|
42
|
+
request: Program,
|
|
43
|
+
draft: InternalDraft,
|
|
44
|
+
options: { callId?: string } = {},
|
|
45
|
+
): Program {
|
|
46
|
+
const callId = options.callId ?? INTERNAL_DRAFT_CALL_ID;
|
|
47
|
+
const drafts = normalizeDrafts(draft);
|
|
48
|
+
if (drafts.length === 0) return request;
|
|
49
|
+
return drafts.reduce(
|
|
50
|
+
(program, text, index) =>
|
|
51
|
+
appendToolInteraction(program, {
|
|
52
|
+
callId: drafts.length === 1 ? callId : `${callId}_${index}`,
|
|
53
|
+
name: INTERNAL_DRAFT_TOOL_NAME,
|
|
54
|
+
args: {},
|
|
55
|
+
result: text,
|
|
56
|
+
}),
|
|
57
|
+
request,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function buildDraftRequest(
|
|
62
|
+
request: Program,
|
|
63
|
+
settings: TwoPassSettings,
|
|
64
|
+
): Program {
|
|
65
|
+
const withPrompt = settings.systemPrompt
|
|
66
|
+
? replaceSystemPrompt(request, settings.systemPrompt)
|
|
67
|
+
: request;
|
|
68
|
+
return applySettings(withPrompt, settings);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function buildFinalRequest(
|
|
72
|
+
options: TwoPassRequestOptions,
|
|
73
|
+
request: Program,
|
|
74
|
+
settings: TwoPassSettings,
|
|
75
|
+
draft: string,
|
|
76
|
+
): Program {
|
|
77
|
+
const limitedDraft = limitDraft(options, request, settings, draft);
|
|
78
|
+
const withDraft = options.buildFinalRequest
|
|
79
|
+
? options.buildFinalRequest(request, limitedDraft)
|
|
80
|
+
: appendInternalDraft(request, limitedDraft);
|
|
81
|
+
const withPrompt = settings.systemPrompt
|
|
82
|
+
? prependSystemPrompt(withDraft, settings.systemPrompt)
|
|
83
|
+
: withDraft;
|
|
84
|
+
return applySettings(withPrompt, settings);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function validateMaxContextLength(value: number | undefined): void {
|
|
88
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
|
|
89
|
+
throw new Error("maxTotalContextLength must be a non-negative integer");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function replaceSystemPrompt(request: Program, systemPrompt: string): Program {
|
|
94
|
+
const systemSpans = messages(request).filter(
|
|
95
|
+
(span) => span.role === "system",
|
|
96
|
+
);
|
|
97
|
+
const previousSystemPrompt = systemSpans
|
|
98
|
+
.map((span) => messageText(request, span).trim())
|
|
99
|
+
.filter(Boolean)
|
|
100
|
+
.join("\n\n");
|
|
101
|
+
let draft = request;
|
|
102
|
+
for (const span of systemSpans.slice().reverse()) {
|
|
103
|
+
draft = removeRange(draft, span.start, span.end);
|
|
104
|
+
}
|
|
105
|
+
draft = prependSystemPrompt(draft, systemPrompt);
|
|
106
|
+
if (!previousSystemPrompt) return draft;
|
|
107
|
+
const firstNonSystem = messages(draft).find((span) => span.role !== "system");
|
|
108
|
+
return insertBefore(
|
|
109
|
+
draft,
|
|
110
|
+
firstNonSystem?.start ?? draft.code.length,
|
|
111
|
+
makeMessage("user", previousSystemPrompt).code,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function applySettings(request: Program, settings: TwoPassSettings): Program {
|
|
116
|
+
if (!settings.reasoningLevel) return request;
|
|
117
|
+
return appendInstructions(
|
|
118
|
+
{
|
|
119
|
+
...request,
|
|
120
|
+
code: request.code.filter(
|
|
121
|
+
(instruction) => instruction.opcode !== Opcode.SET_REASON_EFFORT,
|
|
122
|
+
),
|
|
123
|
+
},
|
|
124
|
+
[
|
|
125
|
+
{
|
|
126
|
+
opcode: Opcode.SET_REASON_EFFORT,
|
|
127
|
+
value: { kind: "string", value: settings.reasoningLevel },
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function limitDraft(
|
|
134
|
+
options: TwoPassRequestOptions,
|
|
135
|
+
request: Program,
|
|
136
|
+
settings: TwoPassSettings,
|
|
137
|
+
draft: string,
|
|
138
|
+
): string {
|
|
139
|
+
if (options.maxTotalContextLength === undefined) return draft;
|
|
140
|
+
const base = buildFinalRequestWithoutDraft(options, request, settings);
|
|
141
|
+
const available = options.maxTotalContextLength - contentText(base).length;
|
|
142
|
+
return available > 0 ? draft.slice(0, available) : "";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function buildFinalRequestWithoutDraft(
|
|
146
|
+
options: TwoPassRequestOptions,
|
|
147
|
+
request: Program,
|
|
148
|
+
settings: TwoPassSettings,
|
|
149
|
+
): Program {
|
|
150
|
+
const withDraft = options.buildFinalRequest
|
|
151
|
+
? options.buildFinalRequest(request, "")
|
|
152
|
+
: request;
|
|
153
|
+
const withPrompt = settings.systemPrompt
|
|
154
|
+
? prependSystemPrompt(withDraft, settings.systemPrompt)
|
|
155
|
+
: withDraft;
|
|
156
|
+
return applySettings(withPrompt, settings);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function normalizeDrafts(draft: InternalDraft): string[] {
|
|
160
|
+
const drafts = Array.isArray(draft)
|
|
161
|
+
? draft.map((item) => (typeof item === "string" ? item : item.text))
|
|
162
|
+
: [draft];
|
|
163
|
+
return drafts.map((text) => text.trim()).filter((text) => text.length > 0);
|
|
164
|
+
}
|
|
@@ -4,79 +4,69 @@ import {
|
|
|
4
4
|
deltaText,
|
|
5
5
|
finishReason,
|
|
6
6
|
hasToolDelta,
|
|
7
|
-
insertBefore,
|
|
8
7
|
lastMessageRole,
|
|
9
|
-
messageText,
|
|
10
|
-
messages,
|
|
11
|
-
removeRange,
|
|
12
8
|
setModel,
|
|
13
9
|
type Program,
|
|
14
10
|
} from "@neutrome/lil-engine";
|
|
15
11
|
import { streamReasoningDelta } from "../output.ts";
|
|
16
|
-
import {
|
|
17
|
-
appendToolInteraction,
|
|
18
|
-
makeMessage,
|
|
19
|
-
prependSystemPrompt,
|
|
20
|
-
} from "../synthetic/index.ts";
|
|
21
12
|
import type { Executor, ExecutorContext, InvokeOptions } from "../types.ts";
|
|
13
|
+
import {
|
|
14
|
+
appendInternalDraft,
|
|
15
|
+
buildDraftRequest,
|
|
16
|
+
buildFinalRequest,
|
|
17
|
+
INTERNAL_DRAFT_CALL_ID,
|
|
18
|
+
INTERNAL_DRAFT_TOOL_NAME,
|
|
19
|
+
validateMaxContextLength,
|
|
20
|
+
type InternalDraft,
|
|
21
|
+
type TwoPassRequestOptions,
|
|
22
|
+
type TwoPassSettings,
|
|
23
|
+
type TwoPassSettingsResolver,
|
|
24
|
+
} from "./two-pass-request.ts";
|
|
25
|
+
|
|
26
|
+
export type {
|
|
27
|
+
InternalDraft,
|
|
28
|
+
TwoPassSettings,
|
|
29
|
+
TwoPassSettingsResolver,
|
|
30
|
+
} from "./two-pass-request.ts";
|
|
31
|
+
export {
|
|
32
|
+
appendInternalDraft,
|
|
33
|
+
INTERNAL_DRAFT_CALL_ID,
|
|
34
|
+
INTERNAL_DRAFT_TOOL_NAME,
|
|
35
|
+
};
|
|
22
36
|
|
|
23
|
-
export
|
|
24
|
-
export const INTERNAL_DRAFT_CALL_ID = "knowledge_0";
|
|
25
|
-
|
|
26
|
-
export type InternalDraft =
|
|
27
|
-
| string
|
|
28
|
-
| readonly string[]
|
|
29
|
-
| readonly { text: string }[];
|
|
37
|
+
export type ModelExecutor = string | Executor;
|
|
30
38
|
|
|
31
|
-
export type TwoPassExecutorOptions = {
|
|
32
|
-
draftModel:
|
|
33
|
-
finalModel:
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
export type TwoPassExecutorOptions = TwoPassRequestOptions & {
|
|
40
|
+
draftModel: ModelExecutor;
|
|
41
|
+
finalModel: ModelExecutor;
|
|
42
|
+
resolveDraftSettings?: TwoPassSettingsResolver;
|
|
43
|
+
resolveFinalSettings?: TwoPassSettingsResolver;
|
|
36
44
|
reasoningIntro?: string;
|
|
37
45
|
reasoningSeparator?: string;
|
|
38
|
-
buildFinalRequest?: (request: Program, draft: string) => Program;
|
|
39
46
|
};
|
|
40
47
|
|
|
41
|
-
export function
|
|
48
|
+
export function invokeExecutor(
|
|
42
49
|
ctx: ExecutorContext,
|
|
43
50
|
request: Program,
|
|
44
|
-
|
|
51
|
+
executor: ModelExecutor,
|
|
45
52
|
options?: InvokeOptions,
|
|
46
53
|
): Promise<Program> {
|
|
47
|
-
return
|
|
54
|
+
return typeof executor === "string"
|
|
55
|
+
? ctx.invoke(setModel(request, executor), options)
|
|
56
|
+
: executor.execute(request, ctx);
|
|
48
57
|
}
|
|
49
58
|
|
|
50
|
-
export async function*
|
|
59
|
+
export async function* streamExecutor(
|
|
51
60
|
ctx: ExecutorContext,
|
|
52
61
|
request: Program,
|
|
53
|
-
|
|
62
|
+
executor: ModelExecutor,
|
|
54
63
|
options?: InvokeOptions,
|
|
55
64
|
): AsyncIterable<Program> {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
export function appendInternalDraft(
|
|
60
|
-
request: Program,
|
|
61
|
-
draft: InternalDraft,
|
|
62
|
-
options: { callId?: string } = {},
|
|
63
|
-
): Program {
|
|
64
|
-
const callId = options.callId ?? INTERNAL_DRAFT_CALL_ID;
|
|
65
|
-
const drafts = normalizeDrafts(draft);
|
|
66
|
-
if (drafts.length === 0) {
|
|
67
|
-
return request;
|
|
65
|
+
if (typeof executor === "string") {
|
|
66
|
+
yield* ctx.invokeStream(setModel(request, executor), options);
|
|
67
|
+
return;
|
|
68
68
|
}
|
|
69
|
-
|
|
70
|
-
return drafts.reduce(
|
|
71
|
-
(program, text, index) =>
|
|
72
|
-
appendToolInteraction(program, {
|
|
73
|
-
callId: drafts.length === 1 ? callId : `${callId}_${index}`,
|
|
74
|
-
name: INTERNAL_DRAFT_TOOL_NAME,
|
|
75
|
-
args: {},
|
|
76
|
-
result: text,
|
|
77
|
-
}),
|
|
78
|
-
request,
|
|
79
|
-
);
|
|
69
|
+
yield* executor.stream(request, ctx);
|
|
80
70
|
}
|
|
81
71
|
|
|
82
72
|
export function createTwoPassExecutor(
|
|
@@ -84,65 +74,67 @@ export function createTwoPassExecutor(
|
|
|
84
74
|
): Executor {
|
|
85
75
|
const reasoningIntro = options.reasoningIntro ?? "Let me think...\n\n";
|
|
86
76
|
const reasoningSeparator = options.reasoningSeparator ?? "\n\n";
|
|
77
|
+
validateMaxContextLength(options.maxTotalContextLength);
|
|
87
78
|
|
|
88
79
|
return {
|
|
89
80
|
async execute(request, ctx) {
|
|
90
|
-
const
|
|
91
|
-
if (
|
|
92
|
-
return
|
|
81
|
+
const settings = await resolveSettings(options, request, ctx);
|
|
82
|
+
if (!settings.draft) {
|
|
83
|
+
return invokeFinal(options, request, ctx, settings.final!);
|
|
84
|
+
}
|
|
85
|
+
const draftRequest = buildDraftRequest(request, settings.draft);
|
|
86
|
+
if (!settings.final || lastMessageRole(request) === "tool") {
|
|
87
|
+
return invokeExecutor(ctx, draftRequest, options.draftModel);
|
|
93
88
|
}
|
|
94
89
|
|
|
95
|
-
const draftResponse = await
|
|
90
|
+
const draftResponse = await invokeExecutor(
|
|
96
91
|
ctx,
|
|
97
92
|
draftRequest,
|
|
98
93
|
options.draftModel,
|
|
99
94
|
);
|
|
100
|
-
if (callData(draftResponse).length > 0)
|
|
101
|
-
return draftResponse;
|
|
102
|
-
}
|
|
95
|
+
if (callData(draftResponse).length > 0) return draftResponse;
|
|
103
96
|
|
|
104
|
-
return
|
|
97
|
+
return invokeFinal(
|
|
98
|
+
options,
|
|
99
|
+
request,
|
|
105
100
|
ctx,
|
|
106
|
-
|
|
107
|
-
|
|
101
|
+
settings.final,
|
|
102
|
+
contentText(draftResponse),
|
|
108
103
|
);
|
|
109
104
|
},
|
|
110
105
|
|
|
111
106
|
async *stream(request, ctx) {
|
|
112
|
-
const
|
|
113
|
-
if (
|
|
114
|
-
yield*
|
|
107
|
+
const settings = await resolveSettings(options, request, ctx);
|
|
108
|
+
if (!settings.draft) {
|
|
109
|
+
yield* streamFinal(options, request, ctx, settings.final!);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const draftRequest = buildDraftRequest(request, settings.draft);
|
|
113
|
+
if (!settings.final || lastMessageRole(request) === "tool") {
|
|
114
|
+
yield* streamExecutor(ctx, draftRequest, options.draftModel);
|
|
115
115
|
return;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
let transcript = "";
|
|
119
119
|
let emittedReasoning = false;
|
|
120
120
|
let toolMode = false;
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
121
|
+
for await (const chunk of streamExecutor(
|
|
122
|
+
ctx,
|
|
123
|
+
draftRequest,
|
|
124
|
+
options.draftModel,
|
|
125
|
+
)) {
|
|
126
|
+
if (ctx.signal.aborted) return;
|
|
128
127
|
if (toolMode) {
|
|
129
128
|
yield chunk;
|
|
130
129
|
continue;
|
|
131
130
|
}
|
|
132
|
-
|
|
133
|
-
const toolRequested =
|
|
134
|
-
hasToolDelta(chunk) || finishReason(chunk) === "tool_calls";
|
|
135
|
-
if (toolRequested) {
|
|
131
|
+
if (hasToolDelta(chunk) || finishReason(chunk) === "tool_calls") {
|
|
136
132
|
toolMode = true;
|
|
137
133
|
yield chunk;
|
|
138
134
|
continue;
|
|
139
135
|
}
|
|
140
|
-
|
|
141
136
|
const text = deltaText(chunk);
|
|
142
|
-
if (!text)
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
|
|
137
|
+
if (!text) continue;
|
|
146
138
|
transcript += text;
|
|
147
139
|
if (!emittedReasoning) {
|
|
148
140
|
emittedReasoning = true;
|
|
@@ -151,76 +143,58 @@ export function createTwoPassExecutor(
|
|
|
151
143
|
yield streamReasoningDelta(text);
|
|
152
144
|
}
|
|
153
145
|
|
|
154
|
-
if (toolMode || ctx.signal.aborted)
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
146
|
+
if (toolMode || ctx.signal.aborted) return;
|
|
158
147
|
if (emittedReasoning && reasoningSeparator) {
|
|
159
148
|
yield streamReasoningDelta(reasoningSeparator);
|
|
160
149
|
}
|
|
161
|
-
|
|
162
|
-
yield* streamModel(
|
|
163
|
-
ctx,
|
|
164
|
-
buildFinalRequest(options, request, transcript),
|
|
165
|
-
options.finalModel,
|
|
166
|
-
);
|
|
150
|
+
yield* streamFinal(options, request, ctx, settings.final, transcript);
|
|
167
151
|
},
|
|
168
152
|
};
|
|
169
153
|
}
|
|
170
154
|
|
|
171
|
-
function
|
|
155
|
+
async function resolveSettings(
|
|
172
156
|
options: TwoPassExecutorOptions,
|
|
173
157
|
request: Program,
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
let draft = request;
|
|
188
|
-
for (const span of systemSpans.slice().reverse()) {
|
|
189
|
-
draft = removeRange(draft, span.start, span.end);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
draft = prependSystemPrompt(draft, options.draftSystemPrompt);
|
|
193
|
-
|
|
194
|
-
if (!previousSystemPrompt) {
|
|
195
|
-
return draft;
|
|
158
|
+
ctx: ExecutorContext,
|
|
159
|
+
): Promise<{ draft: TwoPassSettings | null; final: TwoPassSettings | null }> {
|
|
160
|
+
const [draft, final] = await Promise.all([
|
|
161
|
+
options.resolveDraftSettings
|
|
162
|
+
? options.resolveDraftSettings(request, ctx)
|
|
163
|
+
: {},
|
|
164
|
+
options.resolveFinalSettings
|
|
165
|
+
? options.resolveFinalSettings(request, ctx)
|
|
166
|
+
: {},
|
|
167
|
+
]);
|
|
168
|
+
if (!draft && !final) {
|
|
169
|
+
throw new Error("Two-pass executor requires at least one enabled pass");
|
|
196
170
|
}
|
|
197
|
-
|
|
198
|
-
const firstNonSystem = messages(draft).find((span) => span.role !== "system");
|
|
199
|
-
return insertBefore(
|
|
200
|
-
draft,
|
|
201
|
-
firstNonSystem?.start ?? draft.code.length,
|
|
202
|
-
makeMessage("user", previousSystemPrompt).code,
|
|
203
|
-
);
|
|
171
|
+
return { draft, final };
|
|
204
172
|
}
|
|
205
173
|
|
|
206
|
-
function
|
|
174
|
+
function invokeFinal(
|
|
207
175
|
options: TwoPassExecutorOptions,
|
|
208
176
|
request: Program,
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
: withDraft;
|
|
177
|
+
ctx: ExecutorContext,
|
|
178
|
+
settings: TwoPassSettings,
|
|
179
|
+
draft = "",
|
|
180
|
+
): Promise<Program> {
|
|
181
|
+
return invokeExecutor(
|
|
182
|
+
ctx,
|
|
183
|
+
buildFinalRequest(options, request, settings, draft),
|
|
184
|
+
options.finalModel,
|
|
185
|
+
);
|
|
219
186
|
}
|
|
220
187
|
|
|
221
|
-
function
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
188
|
+
async function* streamFinal(
|
|
189
|
+
options: TwoPassExecutorOptions,
|
|
190
|
+
request: Program,
|
|
191
|
+
ctx: ExecutorContext,
|
|
192
|
+
settings: TwoPassSettings,
|
|
193
|
+
draft = "",
|
|
194
|
+
): AsyncIterable<Program> {
|
|
195
|
+
yield* streamExecutor(
|
|
196
|
+
ctx,
|
|
197
|
+
buildFinalRequest(options, request, settings, draft),
|
|
198
|
+
options.finalModel,
|
|
199
|
+
);
|
|
226
200
|
}
|
package/test/lilsdk-ts.test.ts
CHANGED
|
@@ -27,9 +27,9 @@ import { fallback, retry, runGoal } from "../src/loops/index.ts";
|
|
|
27
27
|
import {
|
|
28
28
|
appendInternalDraft,
|
|
29
29
|
createTwoPassExecutor,
|
|
30
|
-
|
|
30
|
+
invokeExecutor,
|
|
31
31
|
INTERNAL_DRAFT_TOOL_NAME,
|
|
32
|
-
|
|
32
|
+
streamExecutor,
|
|
33
33
|
} from "../src/managed/twoPassExecutor.ts";
|
|
34
34
|
import type { Executor, ExecutorContext, OutputSink } from "../src/types.ts";
|
|
35
35
|
|
|
@@ -182,7 +182,7 @@ describe("@neutrome/lilsdk-ts", () => {
|
|
|
182
182
|
});
|
|
183
183
|
});
|
|
184
184
|
|
|
185
|
-
it("builds two-pass executors from model and
|
|
185
|
+
it("builds two-pass executors from model and settings resolvers", async () => {
|
|
186
186
|
const request = parseChatCompletionsRequest(
|
|
187
187
|
encoder.encode(
|
|
188
188
|
JSON.stringify({
|
|
@@ -214,7 +214,10 @@ describe("@neutrome/lilsdk-ts", () => {
|
|
|
214
214
|
const executor = createTwoPassExecutor({
|
|
215
215
|
draftModel: "smart-model",
|
|
216
216
|
finalModel: "base-model",
|
|
217
|
-
|
|
217
|
+
resolveFinalSettings: () => ({
|
|
218
|
+
reasoningLevel: "high",
|
|
219
|
+
systemPrompt: "system prompt",
|
|
220
|
+
}),
|
|
218
221
|
});
|
|
219
222
|
|
|
220
223
|
const result = await executor.execute(request, ctx);
|
|
@@ -232,6 +235,7 @@ describe("@neutrome/lilsdk-ts", () => {
|
|
|
232
235
|
role: "system",
|
|
233
236
|
content: "system prompt",
|
|
234
237
|
});
|
|
238
|
+
expect(finalRequest.reasoning_effort).toBe("high");
|
|
235
239
|
expect(finalRequest.messages[2].tool_calls[0].function).toEqual({
|
|
236
240
|
name: INTERNAL_DRAFT_TOOL_NAME,
|
|
237
241
|
arguments: "{}",
|
|
@@ -272,7 +276,7 @@ describe("@neutrome/lilsdk-ts", () => {
|
|
|
272
276
|
const executor = createTwoPassExecutor({
|
|
273
277
|
draftModel: "smart-model",
|
|
274
278
|
finalModel: "base-model",
|
|
275
|
-
|
|
279
|
+
resolveDraftSettings: () => ({ systemPrompt: "draft system" }),
|
|
276
280
|
});
|
|
277
281
|
|
|
278
282
|
await executor.execute(request, ctx);
|
|
@@ -287,7 +291,7 @@ describe("@neutrome/lilsdk-ts", () => {
|
|
|
287
291
|
]);
|
|
288
292
|
});
|
|
289
293
|
|
|
290
|
-
it("invokes and streams
|
|
294
|
+
it("invokes and streams string or custom executors", async () => {
|
|
291
295
|
const calls: Array<{ model: string; streaming: boolean }> = [];
|
|
292
296
|
const ctx = buildExecutorContext({
|
|
293
297
|
async invoke(program) {
|
|
@@ -319,9 +323,9 @@ describe("@neutrome/lilsdk-ts", () => {
|
|
|
319
323
|
}),
|
|
320
324
|
),
|
|
321
325
|
);
|
|
322
|
-
const first = await
|
|
326
|
+
const first = await invokeExecutor(ctx, request, "smart-model");
|
|
323
327
|
const chunks: Program[] = [];
|
|
324
|
-
for await (const chunk of
|
|
328
|
+
for await (const chunk of streamExecutor(ctx, request, "base-model")) {
|
|
325
329
|
chunks.push(chunk);
|
|
326
330
|
}
|
|
327
331
|
|
|
@@ -334,6 +338,81 @@ describe("@neutrome/lilsdk-ts", () => {
|
|
|
334
338
|
decoder.decode(emitChatCompletionsStreamChunk(chunks[0]!)),
|
|
335
339
|
).toContain("hello");
|
|
336
340
|
expect(getModel(request)).toBe("virtual-model");
|
|
341
|
+
|
|
342
|
+
const customExecutor = executorFromExecute(async () =>
|
|
343
|
+
appendAssistantMessage({ code: [], buffers: [] }, "custom answer"),
|
|
344
|
+
);
|
|
345
|
+
const custom = await invokeExecutor(ctx, request, customExecutor);
|
|
346
|
+
expect(contentText(custom)).toBe("custom answer");
|
|
347
|
+
|
|
348
|
+
const twoPass = createTwoPassExecutor({
|
|
349
|
+
draftModel: customExecutor,
|
|
350
|
+
finalModel: customExecutor,
|
|
351
|
+
});
|
|
352
|
+
expect(contentText(await twoPass.execute(request, ctx))).toBe(
|
|
353
|
+
"custom answer",
|
|
354
|
+
);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("can skip the draft pass and limits the final context length", async () => {
|
|
358
|
+
const request = parseChatCompletionsRequest(
|
|
359
|
+
encoder.encode(
|
|
360
|
+
JSON.stringify({
|
|
361
|
+
model: "virtual-model",
|
|
362
|
+
messages: [{ role: "user", content: "request" }],
|
|
363
|
+
}),
|
|
364
|
+
),
|
|
365
|
+
);
|
|
366
|
+
const seen: Program[] = [];
|
|
367
|
+
const ctx = buildExecutorContext({
|
|
368
|
+
async invoke(program) {
|
|
369
|
+
seen.push(program);
|
|
370
|
+
return appendAssistantMessage(
|
|
371
|
+
{ code: [], buffers: [] },
|
|
372
|
+
seen.length === 1 ? "a very long draft" : "final",
|
|
373
|
+
);
|
|
374
|
+
},
|
|
375
|
+
async *invokeStream() {
|
|
376
|
+
throw new Error("streaming path is not used in this test");
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const executor = createTwoPassExecutor({
|
|
381
|
+
draftModel: "smart-model",
|
|
382
|
+
finalModel: "base-model",
|
|
383
|
+
maxTotalContextLength: 10,
|
|
384
|
+
resolveDraftSettings: () => ({ systemPrompt: "draft" }),
|
|
385
|
+
});
|
|
386
|
+
await executor.execute(request, ctx);
|
|
387
|
+
|
|
388
|
+
const finalRequest = JSON.parse(
|
|
389
|
+
decoder.decode(emitChatCompletionsRequest(seen[1]!)),
|
|
390
|
+
);
|
|
391
|
+
expect(finalRequest.messages.at(-1)?.content).toBe("a v");
|
|
392
|
+
|
|
393
|
+
let receivedRequest: Program | undefined;
|
|
394
|
+
let receivedContext: ExecutorContext | undefined;
|
|
395
|
+
const directFinal = createTwoPassExecutor({
|
|
396
|
+
draftModel: "unused",
|
|
397
|
+
finalModel: "base-model",
|
|
398
|
+
resolveDraftSettings: (incomingRequest, incomingContext) => {
|
|
399
|
+
receivedRequest = incomingRequest;
|
|
400
|
+
receivedContext = incomingContext;
|
|
401
|
+
return null;
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
await directFinal.execute(request, ctx);
|
|
405
|
+
expect(getModel(seen.at(-1)!)).toBe("base-model");
|
|
406
|
+
expect(receivedRequest).toBe(request);
|
|
407
|
+
expect(receivedContext).toBe(ctx);
|
|
408
|
+
|
|
409
|
+
const draftOnly = createTwoPassExecutor({
|
|
410
|
+
draftModel: "smart-model",
|
|
411
|
+
finalModel: "unused",
|
|
412
|
+
resolveFinalSettings: () => null,
|
|
413
|
+
});
|
|
414
|
+
await draftOnly.execute(request, ctx);
|
|
415
|
+
expect(getModel(seen.at(-1)!)).toBe("smart-model");
|
|
337
416
|
});
|
|
338
417
|
|
|
339
418
|
it("writes reasoning helpers to a sink", async () => {
|