@neutrome/lilsdk-ts 0.3.1 → 0.3.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neutrome/lilsdk-ts",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts",
@@ -13,7 +13,7 @@
13
13
  "./managed": "./src/managed/index.ts"
14
14
  },
15
15
  "dependencies": {
16
- "@neutrome/lil-engine": "0.3.1"
16
+ "@neutrome/lil-engine": "0.3.2"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/node": "^25.9.3",
@@ -5,11 +5,14 @@ export {
5
5
  createTwoPassExecutor,
6
6
  INTERNAL_DRAFT_CALL_ID,
7
7
  INTERNAL_DRAFT_TOOL_NAME,
8
- invokeModel,
9
- streamModel,
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 const INTERNAL_DRAFT_TOOL_NAME = "knowledge";
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: string;
33
- finalModel: string;
34
- draftSystemPrompt?: string;
35
- finalSystemPrompt?: string;
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 invokeModel(
48
+ export function invokeExecutor(
42
49
  ctx: ExecutorContext,
43
50
  request: Program,
44
- model: string,
51
+ executor: ModelExecutor,
45
52
  options?: InvokeOptions,
46
53
  ): Promise<Program> {
47
- return ctx.invoke(setModel(request, model), options);
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* streamModel(
59
+ export async function* streamExecutor(
51
60
  ctx: ExecutorContext,
52
61
  request: Program,
53
- model: string,
62
+ executor: ModelExecutor,
54
63
  options?: InvokeOptions,
55
64
  ): AsyncIterable<Program> {
56
- yield* ctx.invokeStream(setModel(request, model), options);
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 draftRequest = buildDraftRequest(options, request);
91
- if (lastMessageRole(request) === "tool") {
92
- return invokeModel(ctx, draftRequest, options.draftModel);
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 invokeModel(
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 invokeModel(
97
+ return invokeFinal(
98
+ options,
99
+ request,
105
100
  ctx,
106
- buildFinalRequest(options, request, contentText(draftResponse)),
107
- options.finalModel,
101
+ settings.final,
102
+ contentText(draftResponse),
108
103
  );
109
104
  },
110
105
 
111
106
  async *stream(request, ctx) {
112
- const draftRequest = buildDraftRequest(options, request);
113
- if (lastMessageRole(request) === "tool") {
114
- yield* streamModel(ctx, draftRequest, options.draftModel);
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 draftStream = streamModel(ctx, draftRequest, options.draftModel);
122
-
123
- for await (const chunk of draftStream) {
124
- if (ctx.signal.aborted) {
125
- return;
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 buildDraftRequest(
155
+ async function resolveSettings(
172
156
  options: TwoPassExecutorOptions,
173
157
  request: Program,
174
- ): Program {
175
- if (!options.draftSystemPrompt) {
176
- return request;
177
- }
178
-
179
- const systemSpans = messages(request).filter(
180
- (span) => span.role === "system",
181
- );
182
- const previousSystemPrompt = systemSpans
183
- .map((span) => messageText(request, span).trim())
184
- .filter((text) => text.length > 0)
185
- .join("\n\n");
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 buildFinalRequest(
174
+ function invokeFinal(
207
175
  options: TwoPassExecutorOptions,
208
176
  request: Program,
209
- draft: string,
210
- ): Program {
211
- if (options.buildFinalRequest) {
212
- return options.buildFinalRequest(request, draft);
213
- }
214
-
215
- const withDraft = appendInternalDraft(request, draft);
216
- return options.finalSystemPrompt
217
- ? prependSystemPrompt(withDraft, options.finalSystemPrompt)
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 normalizeDrafts(draft: InternalDraft): string[] {
222
- const drafts = Array.isArray(draft)
223
- ? draft.map((item) => (typeof item === "string" ? item : item.text))
224
- : [draft];
225
- return drafts.map((text) => text.trim()).filter((text) => text.length > 0);
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
  }
@@ -27,9 +27,9 @@ import { fallback, retry, runGoal } from "../src/loops/index.ts";
27
27
  import {
28
28
  appendInternalDraft,
29
29
  createTwoPassExecutor,
30
- invokeModel,
30
+ invokeExecutor,
31
31
  INTERNAL_DRAFT_TOOL_NAME,
32
- streamModel,
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 prompt config", async () => {
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
- finalSystemPrompt: "system prompt",
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
- draftSystemPrompt: "draft system",
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 another model through executor context helpers", async () => {
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 invokeModel(ctx, request, "smart-model");
326
+ const first = await invokeExecutor(ctx, request, "smart-model");
323
327
  const chunks: Program[] = [];
324
- for await (const chunk of streamModel(ctx, request, "base-model")) {
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 () => {