@codehz/ai 0.1.3 → 0.1.5

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.
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Mock Adapter
3
3
  *
4
- * 面向测试的脚本化 adapter:
5
- * - turn 顺序消费请求,验证调用方是否正确续接 replay / tool_result
4
+ * 面向测试的回调驱动 adapter:
5
+ * - 每次请求执行用户提供的 handler
6
+ * - 验证调用方是否正确续接 replay / tool_result
6
7
  * - 发出可控的 message / reasoning / tool_call 流
7
8
  * - 注入 warning / auxiliary / content_filter / 中断 / provider error
8
9
  *
@@ -55,18 +56,20 @@ export type MockRequestExpectation = {
55
56
  items?: MockInputExpectation[];
56
57
  };
57
58
 
58
- export type MockTurnContext = {
59
+ export type MockHistoryRecord = {
60
+ turnIndex: number;
61
+ requestId: string;
62
+ replay: ReplayItem[];
63
+ toolCalls: ToolCallItem[];
64
+ };
65
+
66
+ export type MockHandlerContext = {
59
67
  turnIndex: number;
60
68
  previousReplay: ReplayItem[];
61
69
  pendingToolCalls: readonly ToolCallItem[];
62
- history: readonly MockTurnRecord[];
70
+ history: readonly MockHistoryRecord[];
63
71
  };
64
72
 
65
- export type MockTurnValidator = (
66
- request: NormalizedRequest,
67
- context: MockTurnContext,
68
- ) => void | Promise<void>;
69
-
70
73
  export type MockWarningStep = {
71
74
  type: "warning";
72
75
  message: string;
@@ -167,32 +170,24 @@ export type MockStep =
167
170
  | MockInterruptStep
168
171
  | MockThrowStep;
169
172
 
170
- export type MockTurn = {
171
- name?: string;
172
- expect?: MockRequestExpectation | MockTurnValidator;
173
- steps: MockStep[];
174
- };
173
+ export type MockHandler = (request: NormalizedRequest, context: MockHandlerContext) => AsyncIterable<MockStep>;
174
+
175
+ type MockHandlerSource = Iterable<MockStep> | AsyncIterable<MockStep>;
176
+
177
+ export type MockStaticHandler = (
178
+ request: NormalizedRequest,
179
+ context: MockHandlerContext,
180
+ ) => MockHandlerSource | Promise<MockHandlerSource>;
175
181
 
176
182
  export type MockAdapterOptions = {
177
- turns: MockTurn[];
178
- onExhausted?: "throw" | "repeat-last" | "complete-empty";
183
+ handler: MockHandler;
179
184
  providerMetadata?: Record<string, unknown>;
180
- stream?: MockTextStreamOptions;
181
- };
182
-
183
- type MockTurnRecord = {
184
- turnIndex: number;
185
- turnName?: string;
186
- requestId: string;
187
- replay: ReplayItem[];
188
- toolCalls: ToolCallItem[];
189
185
  };
190
186
 
191
187
  type MockProviderRequest = {
192
188
  request: NormalizedRequest;
193
- turn: MockTurn;
189
+ handlerResult: AsyncIterable<MockStep>;
194
190
  turnIndex: number;
195
- turnName?: string;
196
191
  remainingPendingToolCalls: ToolCallItem[];
197
192
  };
198
193
 
@@ -202,52 +197,102 @@ type ResolvedMockTextStreamOptions = {
202
197
  initialDelayMs: number;
203
198
  };
204
199
 
200
+ export function assertMockRequest(
201
+ request: NormalizedRequest,
202
+ expectation: MockRequestExpectation,
203
+ context: MockHandlerContext,
204
+ ): void {
205
+ const prefix = `MockAdapter turn ${context.turnIndex + 1} expectation failed`;
206
+
207
+ if (expectation.minItems !== undefined && request.input.length < expectation.minItems) {
208
+ throw new AIRequestError(
209
+ `${prefix}: expected at least ${expectation.minItems} input item(s)`,
210
+ "MOCK_EXPECTATION_FAILED",
211
+ );
212
+ }
213
+
214
+ if (expectation.maxItems !== undefined && request.input.length > expectation.maxItems) {
215
+ throw new AIRequestError(
216
+ `${prefix}: expected at most ${expectation.maxItems} input item(s)`,
217
+ "MOCK_EXPECTATION_FAILED",
218
+ );
219
+ }
220
+
221
+ if (expectation.tools === "present" && (!request.tools || request.tools.length === 0)) {
222
+ throw new AIRequestError(`${prefix}: expected tools to be present`, "MOCK_EXPECTATION_FAILED");
223
+ }
224
+
225
+ if (expectation.tools === "absent" && request.tools && request.tools.length > 0) {
226
+ throw new AIRequestError(`${prefix}: expected tools to be absent`, "MOCK_EXPECTATION_FAILED");
227
+ }
228
+
229
+ if (expectation.toolChoice === "present" && request.toolChoice === undefined) {
230
+ throw new AIRequestError(`${prefix}: expected toolChoice to be present`, "MOCK_EXPECTATION_FAILED");
231
+ }
232
+
233
+ if (expectation.toolChoice === "absent" && request.toolChoice !== undefined) {
234
+ throw new AIRequestError(`${prefix}: expected toolChoice to be absent`, "MOCK_EXPECTATION_FAILED");
235
+ }
236
+
237
+ if (expectation.requireReplayFromPreviousTurn && context.previousReplay.length > 0) {
238
+ assertReplayIncluded(request.input, context.previousReplay, prefix);
239
+ }
240
+
241
+ if (expectation.requireToolResultsForPendingCalls && context.pendingToolCalls.length > 0) {
242
+ const toolResultIds = new Set(
243
+ request.input.filter((item): item is ToolResultItem => item.type === "tool_result").map((item) => item.callId),
244
+ );
245
+
246
+ for (const call of context.pendingToolCalls) {
247
+ if (!toolResultIds.has(call.id)) {
248
+ throw new AIRequestError(
249
+ `${prefix}: expected tool_result for pending tool call "${call.id}"`,
250
+ "MOCK_EXPECTATION_FAILED",
251
+ );
252
+ }
253
+ }
254
+ }
255
+
256
+ if (expectation.items && expectation.items.length > 0) {
257
+ if (expectation.ordered) {
258
+ assertOrderedItems(request.input, expectation.items, prefix);
259
+ } else {
260
+ assertUnorderedItems(request.input, expectation.items, prefix);
261
+ }
262
+ }
263
+ }
264
+
205
265
  export class MockAdapter extends AdapterBase {
206
266
  readonly kind = "mock" as const;
207
267
  readonly nativeStreaming = false;
208
268
 
209
- private readonly turns: MockTurn[];
210
- private readonly onExhausted: NonNullable<MockAdapterOptions["onExhausted"]>;
269
+ private readonly handler: MockHandler;
211
270
  private readonly providerMetadata?: Record<string, unknown>;
212
- private readonly defaultStream?: ResolvedMockTextStreamOptions;
213
271
 
214
272
  private cursor = 0;
215
273
  private previousReplay: ReplayItem[] = [];
216
274
  private pendingToolCalls: ToolCallItem[] = [];
217
- private history: MockTurnRecord[] = [];
275
+ private history: MockHistoryRecord[] = [];
218
276
  private activeStream = false;
219
277
 
220
278
  constructor(options: MockAdapterOptions) {
221
279
  super();
222
- this.turns = options.turns;
223
- this.onExhausted = options.onExhausted ?? "throw";
280
+ this.handler = options.handler;
224
281
  this.providerMetadata = options.providerMetadata;
225
- this.defaultStream = resolveMockTextStreamOptions(options.stream, "adapter stream");
226
282
  }
227
283
 
228
284
  protected async buildRequest(request: NormalizedRequest): Promise<MockProviderRequest> {
229
285
  const turnIndex = this.cursor;
230
- const turn = this.resolveTurn(turnIndex);
231
- const turnName = turn.name;
232
- const context = this.buildTurnContext(turnIndex);
233
-
234
- if (turn.expect) {
235
- if (typeof turn.expect === "function") {
236
- await turn.expect(request, context);
237
- } else {
238
- assertRequestMatchesExpectation(request, turn.expect, context);
239
- }
240
- }
241
-
286
+ const context = this.buildHandlerContext(turnIndex);
242
287
  const remainingPendingToolCalls = consumePendingToolCalls(this.pendingToolCalls, request.input);
288
+ const handlerResult = this.handler(request, context);
243
289
 
244
290
  this.cursor += 1;
245
291
 
246
292
  return {
247
293
  request,
248
- turn,
294
+ handlerResult,
249
295
  turnIndex,
250
- turnName,
251
296
  remainingPendingToolCalls,
252
297
  };
253
298
  }
@@ -266,8 +311,11 @@ export class MockAdapter extends AdapterBase {
266
311
  try {
267
312
  const mockRequest = providerRequest as MockProviderRequest;
268
313
  const output: OutputItem[] = [];
314
+ let stepCount = 0;
315
+
316
+ for await (const step of mockRequest.handlerResult) {
317
+ stepCount += 1;
269
318
 
270
- for (const [stepIndex, step] of mockRequest.turn.steps.entries()) {
271
319
  switch (step.type) {
272
320
  case "warning":
273
321
  yield factory.responseWarning(step.message, step.code);
@@ -280,14 +328,14 @@ export class MockAdapter extends AdapterBase {
280
328
  });
281
329
  break;
282
330
  case "message": {
283
- const item = createMessageFromStep(step, request, mockRequest.turnIndex, stepIndex);
284
- yield* emitMessage(factory, item, resolveStepStreamOptions(this.defaultStream, step.stream, "message"));
331
+ const item = createMessageFromStep(step, request, mockRequest.turnIndex, stepCount - 1);
332
+ yield* emitMessage(factory, item, resolveStepStreamOptions(undefined, step.stream, "message"));
285
333
  output.push(item);
286
334
  break;
287
335
  }
288
336
  case "reasoning": {
289
- const item = createReasoningFromStep(step, request, mockRequest.turnIndex, stepIndex);
290
- yield* emitReasoning(factory, item, resolveStepStreamOptions(this.defaultStream, step.stream, "reasoning"));
337
+ const item = createReasoningFromStep(step, request, mockRequest.turnIndex, stepCount - 1);
338
+ yield* emitReasoning(factory, item, resolveStepStreamOptions(undefined, step.stream, "reasoning"));
291
339
  output.push(item);
292
340
  break;
293
341
  }
@@ -297,30 +345,37 @@ export class MockAdapter extends AdapterBase {
297
345
  factory,
298
346
  item,
299
347
  step.streamArguments ?? true,
300
- resolveStepStreamOptions(this.defaultStream, step.stream, "tool_call"),
348
+ resolveStepStreamOptions(undefined, step.stream, "tool_call"),
301
349
  );
302
350
  output.push(item);
303
351
  break;
304
352
  }
305
353
  case "output": {
306
354
  assertSupportedOutputItem(step.item);
307
- const item = attachSyntheticId(step.item, request, mockRequest.turnIndex, stepIndex);
308
- yield* emitOutputItem(factory, item, resolveStepStreamOptions(this.defaultStream, step.stream, "output"));
355
+ const item = attachSyntheticId(step.item, request, mockRequest.turnIndex, stepCount - 1);
356
+ yield* emitOutputItem(factory, item, resolveStepStreamOptions(undefined, step.stream, "output"));
309
357
  output.push(item);
310
358
  break;
311
359
  }
312
360
  case "complete": {
313
- const response = this.finalizeTurn(request, factory, mockRequest, output, step);
361
+ const response = this.finalizeTurn(request, factory, mockRequest, output, step, stepCount);
314
362
  yield factory.responseCompleted(response);
315
363
  return;
316
364
  }
317
365
  case "error": {
318
366
  yield factory.responseWarning(step.message, step.code);
319
- const response = this.finalizeTurn(request, factory, mockRequest, output, {
320
- type: "complete",
321
- stopReason: step.stopReason ?? "error",
322
- providerMetadata: step.providerMetadata,
323
- });
367
+ const response = this.finalizeTurn(
368
+ request,
369
+ factory,
370
+ mockRequest,
371
+ output,
372
+ {
373
+ type: "complete",
374
+ stopReason: step.stopReason ?? "error",
375
+ providerMetadata: step.providerMetadata,
376
+ },
377
+ stepCount,
378
+ );
324
379
  yield factory.responseCompleted(response);
325
380
  return;
326
381
  }
@@ -332,9 +387,16 @@ export class MockAdapter extends AdapterBase {
332
387
  }
333
388
  }
334
389
 
335
- const response = this.finalizeTurn(request, factory, mockRequest, output, {
336
- type: "complete",
337
- });
390
+ const response = this.finalizeTurn(
391
+ request,
392
+ factory,
393
+ mockRequest,
394
+ output,
395
+ {
396
+ type: "complete",
397
+ },
398
+ stepCount,
399
+ );
338
400
  yield factory.responseCompleted(response);
339
401
  } finally {
340
402
  this.activeStream = false;
@@ -347,6 +409,7 @@ export class MockAdapter extends AdapterBase {
347
409
  mockRequest: MockProviderRequest,
348
410
  output: OutputItem[],
349
411
  completion: MockCompleteStep,
412
+ stepCount: number,
350
413
  ) {
351
414
  const replay = completion.replay ?? replayFromOutput(output);
352
415
  const toolCalls = output.filter((item): item is ToolCallItem => item.type === "tool_call");
@@ -355,7 +418,6 @@ export class MockAdapter extends AdapterBase {
355
418
  this.pendingToolCalls = [...mockRequest.remainingPendingToolCalls, ...toolCalls];
356
419
  this.history.push({
357
420
  turnIndex: mockRequest.turnIndex,
358
- turnName: mockRequest.turnName,
359
421
  requestId: request.requestId,
360
422
  replay,
361
423
  toolCalls,
@@ -372,8 +434,7 @@ export class MockAdapter extends AdapterBase {
372
434
  auxiliary: completion.auxiliary,
373
435
  providerMetadata: {
374
436
  turnIndex: mockRequest.turnIndex,
375
- turnName: mockRequest.turnName,
376
- scriptedSteps: mockRequest.turn.steps.length,
437
+ stepCount,
377
438
  pendingToolCallIds: this.pendingToolCalls.map((item) => item.id),
378
439
  historyLength: this.history.length,
379
440
  ...this.providerMetadata,
@@ -387,28 +448,7 @@ export class MockAdapter extends AdapterBase {
387
448
  );
388
449
  }
389
450
 
390
- private resolveTurn(turnIndex: number): MockTurn {
391
- const turn = this.turns[turnIndex];
392
- if (turn !== undefined) {
393
- return turn;
394
- }
395
-
396
- const lastTurn = this.turns.at(-1);
397
- if (this.onExhausted === "repeat-last" && lastTurn !== undefined) {
398
- return lastTurn;
399
- }
400
-
401
- if (this.onExhausted === "complete-empty") {
402
- return { name: "exhausted", steps: [] };
403
- }
404
-
405
- throw new AIRequestError(
406
- `MockAdapter turn ${turnIndex + 1} requested, but only ${this.turns.length} turn(s) were scripted`,
407
- "MOCK_TURN_EXHAUSTED",
408
- );
409
- }
410
-
411
- private buildTurnContext(turnIndex: number): MockTurnContext {
451
+ private buildHandlerContext(turnIndex: number): MockHandlerContext {
412
452
  return {
413
453
  turnIndex,
414
454
  previousReplay: this.previousReplay.map(cloneItem),
@@ -422,6 +462,46 @@ export class MockAdapter extends AdapterBase {
422
462
  }
423
463
  }
424
464
 
465
+ export function withMockStreaming(handler: MockStaticHandler, options: MockTextStreamOptions): MockHandler {
466
+ const defaults = resolveMockTextStreamOptions(options, "mock stream wrapper");
467
+ if (!defaults) {
468
+ throw new AIRequestError("mock stream wrapper requires streaming options", "MOCK_STREAM_CONFIG_INVALID");
469
+ }
470
+
471
+ return async function* streamWrappedHandler(
472
+ request: NormalizedRequest,
473
+ context: MockHandlerContext,
474
+ ): AsyncIterable<MockStep> {
475
+ const source = await handler(request, context);
476
+
477
+ for await (const step of source) {
478
+ yield applyDefaultStreaming(step, defaults);
479
+ }
480
+ };
481
+ }
482
+
483
+ function applyDefaultStreaming(step: MockStep, defaults: ResolvedMockTextStreamOptions): MockStep {
484
+ switch (step.type) {
485
+ case "message":
486
+ case "reasoning":
487
+ case "tool_call":
488
+ case "output":
489
+ if (step.stream !== undefined) {
490
+ return step;
491
+ }
492
+ return {
493
+ ...step,
494
+ stream: {
495
+ charsPerSecond: defaults.charsPerSecond,
496
+ chunkSize: defaults.chunkSize,
497
+ initialDelayMs: defaults.initialDelayMs,
498
+ },
499
+ };
500
+ default:
501
+ return step;
502
+ }
503
+ }
504
+
425
505
  function createMessageFromStep(
426
506
  step: MockMessageStep,
427
507
  request: NormalizedRequest,
@@ -465,7 +545,10 @@ function normalizeBlocks(content: string | ContentBlock[]): ContentBlock[] {
465
545
 
466
546
  function assertSupportedOutputItem(item: OutputItem): void {
467
547
  if (item.type === "opaque") {
468
- throw new AIRequestError("MockAdapter does not stream opaque output items; use complete.replay if needed", "MOCK_OPAQUE_OUTPUT");
548
+ throw new AIRequestError(
549
+ "MockAdapter does not stream opaque output items; use complete.replay if needed",
550
+ "MOCK_OPAQUE_OUTPUT",
551
+ );
469
552
  }
470
553
  }
471
554
 
@@ -680,75 +763,12 @@ function resolveStopReason(output: OutputItem[]): StopReason {
680
763
 
681
764
  function consumePendingToolCalls(pending: readonly ToolCallItem[], input: readonly InputItem[]): ToolCallItem[] {
682
765
  const fulfilledIds = new Set(
683
- input
684
- .filter((item): item is ToolResultItem => item.type === "tool_result")
685
- .map((item) => item.callId),
766
+ input.filter((item): item is ToolResultItem => item.type === "tool_result").map((item) => item.callId),
686
767
  );
687
768
 
688
769
  return pending.filter((item) => !fulfilledIds.has(item.id)).map(cloneItem);
689
770
  }
690
771
 
691
- function assertRequestMatchesExpectation(
692
- request: NormalizedRequest,
693
- expectation: MockRequestExpectation,
694
- context: MockTurnContext,
695
- ): void {
696
- const prefix = `MockAdapter turn ${context.turnIndex + 1} expectation failed`;
697
-
698
- if (expectation.minItems !== undefined && request.input.length < expectation.minItems) {
699
- throw new AIRequestError(`${prefix}: expected at least ${expectation.minItems} input item(s)`, "MOCK_EXPECTATION_FAILED");
700
- }
701
-
702
- if (expectation.maxItems !== undefined && request.input.length > expectation.maxItems) {
703
- throw new AIRequestError(`${prefix}: expected at most ${expectation.maxItems} input item(s)`, "MOCK_EXPECTATION_FAILED");
704
- }
705
-
706
- if (expectation.tools === "present" && (!request.tools || request.tools.length === 0)) {
707
- throw new AIRequestError(`${prefix}: expected tools to be present`, "MOCK_EXPECTATION_FAILED");
708
- }
709
-
710
- if (expectation.tools === "absent" && request.tools && request.tools.length > 0) {
711
- throw new AIRequestError(`${prefix}: expected tools to be absent`, "MOCK_EXPECTATION_FAILED");
712
- }
713
-
714
- if (expectation.toolChoice === "present" && request.toolChoice === undefined) {
715
- throw new AIRequestError(`${prefix}: expected toolChoice to be present`, "MOCK_EXPECTATION_FAILED");
716
- }
717
-
718
- if (expectation.toolChoice === "absent" && request.toolChoice !== undefined) {
719
- throw new AIRequestError(`${prefix}: expected toolChoice to be absent`, "MOCK_EXPECTATION_FAILED");
720
- }
721
-
722
- if (expectation.requireReplayFromPreviousTurn && context.previousReplay.length > 0) {
723
- assertReplayIncluded(request.input, context.previousReplay, prefix);
724
- }
725
-
726
- if (expectation.requireToolResultsForPendingCalls && context.pendingToolCalls.length > 0) {
727
- const toolResultIds = new Set(
728
- request.input
729
- .filter((item): item is ToolResultItem => item.type === "tool_result")
730
- .map((item) => item.callId),
731
- );
732
-
733
- for (const call of context.pendingToolCalls) {
734
- if (!toolResultIds.has(call.id)) {
735
- throw new AIRequestError(
736
- `${prefix}: expected tool_result for pending tool call "${call.id}"`,
737
- "MOCK_EXPECTATION_FAILED",
738
- );
739
- }
740
- }
741
- }
742
-
743
- if (expectation.items && expectation.items.length > 0) {
744
- if (expectation.ordered) {
745
- assertOrderedItems(request.input, expectation.items, prefix);
746
- } else {
747
- assertUnorderedItems(request.input, expectation.items, prefix);
748
- }
749
- }
750
- }
751
-
752
772
  function assertReplayIncluded(input: readonly InputItem[], replay: readonly ReplayItem[], prefix: string): void {
753
773
  const fingerprints = input.map(fingerprintItem);
754
774
  let cursor = 0;
@@ -757,13 +777,20 @@ function assertReplayIncluded(input: readonly InputItem[], replay: readonly Repl
757
777
  const target = fingerprintItem(replayItem);
758
778
  const foundIndex = fingerprints.indexOf(target, cursor);
759
779
  if (foundIndex === -1) {
760
- throw new AIRequestError(`${prefix}: previous replay item was not carried into the next request`, "MOCK_EXPECTATION_FAILED");
780
+ throw new AIRequestError(
781
+ `${prefix}: previous replay item was not carried into the next request`,
782
+ "MOCK_EXPECTATION_FAILED",
783
+ );
761
784
  }
762
785
  cursor = foundIndex + 1;
763
786
  }
764
787
  }
765
788
 
766
- function assertOrderedItems(input: readonly InputItem[], expectations: readonly MockInputExpectation[], prefix: string): void {
789
+ function assertOrderedItems(
790
+ input: readonly InputItem[],
791
+ expectations: readonly MockInputExpectation[],
792
+ prefix: string,
793
+ ): void {
767
794
  let cursor = 0;
768
795
 
769
796
  for (const expected of expectations) {
@@ -787,7 +814,11 @@ function assertOrderedItems(input: readonly InputItem[], expectations: readonly
787
814
  }
788
815
  }
789
816
 
790
- function assertUnorderedItems(input: readonly InputItem[], expectations: readonly MockInputExpectation[], prefix: string): void {
817
+ function assertUnorderedItems(
818
+ input: readonly InputItem[],
819
+ expectations: readonly MockInputExpectation[],
820
+ prefix: string,
821
+ ): void {
791
822
  for (const expected of expectations) {
792
823
  const matched = input.some((item) => matchesItemExpectation(item, expected));
793
824
  if (!matched) {
@@ -811,8 +842,7 @@ function matchesItemExpectation(item: InputItem, expected: MockInputExpectation)
811
842
  switch (item.type) {
812
843
  case "message":
813
844
  return (
814
- (expected.role === undefined || item.role === expected.role) &&
815
- matchesText(item.content, expected.textIncludes)
845
+ (expected.role === undefined || item.role === expected.role) && matchesText(item.content, expected.textIncludes)
816
846
  );
817
847
  case "reasoning":
818
848
  return (
@@ -112,7 +112,7 @@ function ensureOllamaReasoningBlocks(
112
112
  });
113
113
  }
114
114
 
115
- function instructionsToOllamaText(instructions: string | import("../index.js").ContentBlock[]): string {
115
+ function instructionsToOllamaText(instructions: string | import("../index.js").InstructionBlock[]): string {
116
116
  return typeof instructions === "string"
117
117
  ? instructions
118
118
  : contentBlocksToText(ensureOllamaTextBlocks(instructions, "instructions"));
@@ -207,19 +207,22 @@ function rollbackTrailingAssistantMessages(messages: OllamaMessage[]): void {
207
207
  }
208
208
 
209
209
  function isOllamaToolCalls(value: unknown): value is OllamaToolCall[] {
210
- return Array.isArray(value) && value.every((entry) => {
211
- if (!entry || typeof entry !== "object" || !("function" in entry)) return false;
212
- const fn = (entry as { function?: unknown }).function;
213
- return (
214
- !!fn &&
215
- typeof fn === "object" &&
216
- "name" in fn &&
217
- typeof (fn as { name?: unknown }).name === "string" &&
218
- "arguments" in fn &&
219
- typeof (fn as { arguments?: unknown }).arguments === "object" &&
220
- (fn as { arguments?: unknown }).arguments !== null
221
- );
222
- });
210
+ return (
211
+ Array.isArray(value) &&
212
+ value.every((entry) => {
213
+ if (!entry || typeof entry !== "object" || !("function" in entry)) return false;
214
+ const fn = (entry as { function?: unknown }).function;
215
+ return (
216
+ !!fn &&
217
+ typeof fn === "object" &&
218
+ "name" in fn &&
219
+ typeof (fn as { name?: unknown }).name === "string" &&
220
+ "arguments" in fn &&
221
+ typeof (fn as { arguments?: unknown }).arguments === "object" &&
222
+ (fn as { arguments?: unknown }).arguments !== null
223
+ );
224
+ })
225
+ );
223
226
  }
224
227
 
225
228
  // ── Adapter ───────────────────────────────────────────────────
@@ -256,15 +259,11 @@ export class OllamaAdapter extends AdapterBase {
256
259
  for (const item of request.input) {
257
260
  switch (item.type) {
258
261
  case "message": {
259
- const role =
260
- item.role === "developer"
261
- ? "system"
262
- : item.role === "system"
263
- ? "system"
264
- : item.role === "user"
265
- ? "user"
266
- : "assistant";
267
- messages.push({ role, content: contentBlocksToText(ensureOllamaTextBlocks(item.content, `input message (${item.role}) content`)) });
262
+ const role = item.role;
263
+ messages.push({
264
+ role,
265
+ content: contentBlocksToText(ensureOllamaTextBlocks(item.content, `input message (${item.role}) content`)),
266
+ });
268
267
  break;
269
268
  }
270
269
  case "tool_call": {
@@ -301,7 +300,12 @@ export class OllamaAdapter extends AdapterBase {
301
300
  }
302
301
  case "opaque": {
303
302
  // Best-effort restore from opaque replay
304
- if (item.source === "ollama" && item.purpose === "replay" && typeof item.payload === "object" && item.payload !== null) {
303
+ if (
304
+ item.source === "ollama" &&
305
+ item.purpose === "replay" &&
306
+ typeof item.payload === "object" &&
307
+ item.payload !== null
308
+ ) {
305
309
  const payload = item.payload as Record<string, unknown>;
306
310
  if (payload.role === "assistant" && typeof payload.content === "string") {
307
311
  rollbackTrailingAssistantMessages(messages);
@@ -477,9 +481,10 @@ export class OllamaAdapter extends AdapterBase {
477
481
  {
478
482
  inputTokens: chunk.prompt_eval_count,
479
483
  outputTokens: chunk.eval_count,
480
- totalTokens: chunk.prompt_eval_count !== undefined && chunk.eval_count !== undefined
481
- ? chunk.prompt_eval_count + chunk.eval_count
482
- : undefined,
484
+ totalTokens:
485
+ chunk.prompt_eval_count !== undefined && chunk.eval_count !== undefined
486
+ ? chunk.prompt_eval_count + chunk.eval_count
487
+ : undefined,
483
488
  },
484
489
  "final",
485
490
  {
@@ -51,7 +51,7 @@ type ResponsesAPIRequest = {
51
51
  };
52
52
 
53
53
  type ResponsesInputItem =
54
- | { type: "message"; role: "user" | "assistant" | "system" | "developer"; content: string }
54
+ | { type: "message"; role: "user" | "assistant"; content: string }
55
55
  | { type: "message"; role: "assistant"; content: ResponsesContentBlock[] }
56
56
  | { type: "function_call"; id: string; name: string; arguments: string; call_id?: string }
57
57
  | { type: "function_call_output"; call_id: string; output: string }
@@ -104,7 +104,7 @@ function ensureResponsesReasoningBlocks(
104
104
  });
105
105
  }
106
106
 
107
- function instructionsToResponsesText(instructions: string | import("../index.js").ContentBlock[]): string {
107
+ function instructionsToResponsesText(instructions: string | import("../index.js").InstructionBlock[]): string {
108
108
  return typeof instructions === "string"
109
109
  ? instructions
110
110
  : contentBlocksToText(ensureResponsesTextBlocks(instructions, "instructions"));
@@ -164,9 +164,7 @@ function parseSSE(chunk: string): { events: ResponsesSSEEvent[]; rest: string; m
164
164
 
165
165
  function isReplayCanonicalInput(item: ResponsesInputItem): boolean {
166
166
  return (
167
- (item.type === "message" && item.role === "assistant") ||
168
- item.type === "reasoning" ||
169
- item.type === "function_call"
167
+ (item.type === "message" && item.role === "assistant") || item.type === "reasoning" || item.type === "function_call"
170
168
  );
171
169
  }
172
170
 
@@ -452,9 +450,9 @@ export class ResponsesAdapter extends AdapterBase {
452
450
  if (completedResponse.usage) {
453
451
  auxiliary.recordUsage(
454
452
  {
455
- inputTokens: completedResponse.usage.input_tokens,
456
- outputTokens: completedResponse.usage.output_tokens,
457
- totalTokens: completedResponse.usage.total_tokens,
453
+ inputTokens: completedResponse.usage.input_tokens,
454
+ outputTokens: completedResponse.usage.output_tokens,
455
+ totalTokens: completedResponse.usage.total_tokens,
458
456
  },
459
457
  "final",
460
458
  completedResponse.usage,