@intx/inference 0.2.2 → 0.4.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/dist/harness.js CHANGED
@@ -14,9 +14,10 @@
14
14
  // handling, or event emission. They translate request/response shapes.
15
15
  import { type } from "arktype";
16
16
  import { getLogger } from "@intx/log";
17
+ import { detectResponseKind, } from "@intx/types/content-type";
17
18
  import { parseSSE } from "./sse.js";
18
19
  import { injectCredentials } from "./auth.js";
19
- import { classifyHTTPError, classifyNetworkError, classifyAbortError, classifyStreamError, classifyTimeoutError, ProtocolMismatchError, } from "./errors.js";
20
+ import { classifyHTTPError, classifyNetworkError, classifyAbortError, classifyStreamError, classifyTimeoutError, classifyProtocolMismatch, ProtocolMismatchError, } from "./errors.js";
20
21
  import { createDefaultRetryPolicy } from "./retry-policy.js";
21
22
  const logger = getLogger(["interchange", "inference", "harness"]);
22
23
  /**
@@ -70,6 +71,13 @@ export function createDependencies(adapters) {
70
71
  adapters,
71
72
  };
72
73
  }
74
+ // Fail-closed default resolver, installed when a caller supplies no
75
+ // `readMaterial`. It throws only if a request actually reaches a credential
76
+ // sentinel, so a sentinel-free mock harness runs without a resolver while a
77
+ // real credentialed request surfaces the missing wiring loudly.
78
+ const unconfiguredCredentialResolver = (credentialId) => {
79
+ throw new Error(`no credential resolver supplied to the inference harness, but a request needs the secret for credential ${credentialId}`);
80
+ };
73
81
  /**
74
82
  * Run one fetch lifecycle and yield its events. Ends on the first
75
83
  * `inference.error` or `inference.done`. The outer `runInference`
@@ -81,7 +89,7 @@ export function createDependencies(adapters) {
81
89
  * directly would bypass retry handling.
82
90
  */
83
91
  async function* runSingleAttempt(opts) {
84
- const { turns, source, inferenceOptions, signal, nextSeq, deps } = opts;
92
+ const { turns, source, inferenceOptions, signal, nextSeq, readMaterial, deps, } = opts;
85
93
  // Per-call options override source-bound defaults. The merge happens
86
94
  // here, once, so the adapter and timeout-resolution paths below all
87
95
  // see the effective option set without having to remember the
@@ -128,6 +136,9 @@ async function* runSingleAttempt(opts) {
128
136
  // different keys.
129
137
  const citationsByIndex = new Map();
130
138
  const unindexedCitations = [];
139
+ // Prompt-level safety signals (no candidate index on the first
140
+ // capture). Appended to the finalized turn after indexed blocks.
141
+ const unindexedSafetyRatings = [];
131
142
  let usageSeen = null;
132
143
  const openToolCalls = new Map();
133
144
  // OpenAI uses index-based tracking before we have a real callId.
@@ -142,7 +153,7 @@ async function* runSingleAttempt(opts) {
142
153
  }
143
154
  let adapter;
144
155
  try {
145
- adapter = deps.adapters.resolve(lastCycleSource);
156
+ adapter = deps.adapters.resolve(lastCycleSource, source.quirks);
146
157
  }
147
158
  catch (cause) {
148
159
  yield {
@@ -177,7 +188,7 @@ async function* runSingleAttempt(opts) {
177
188
  }
178
189
  // Resolve the full URL and inject credentials.
179
190
  const url = resolveURL(builtRequest.url, source.baseURL);
180
- const headers = injectCredentials(builtRequest.headers, source);
191
+ const headers = injectCredentials(builtRequest.headers, source, readMaterial ?? unconfiguredCredentialResolver);
181
192
  // Per-call timeouts. The inactivity timer fires when the harness
182
193
  // hasn't yielded an event for `inactivityTimeoutMs`; the total timer
183
194
  // is a wall-clock cap from fetch onwards. We own one AbortController,
@@ -309,13 +320,55 @@ async function* runSingleAttempt(opts) {
309
320
  };
310
321
  return;
311
322
  }
323
+ // Captured as a const so the non-null narrowing from the guard above
324
+ // carries into the SSE branch of the event-source generator below (a
325
+ // bare `response.body` re-widens to nullable across the closure).
326
+ const responseBody = response.body;
327
+ let responseKind;
328
+ try {
329
+ responseKind = detectResponseKind(response.headers);
330
+ }
331
+ catch (cause) {
332
+ // A 2xx whose Content-Type is neither SSE nor JSON is a protocol
333
+ // violation, not a transient failure — surface it loudly rather than
334
+ // pushing unknown bytes through the SSE parser to yield an empty turn.
335
+ yield {
336
+ type: "inference.error",
337
+ seq: nextSeq(),
338
+ data: {
339
+ error: classifyProtocolMismatch(cause instanceof Error ? cause.message : String(cause)),
340
+ partial: snapshotPartial(partial),
341
+ },
342
+ };
343
+ return;
344
+ }
312
345
  // Arm the inactivity timer now that the SSE stream is open. Every
313
346
  // event we yield below resets it; sustained silence past
314
347
  // `inactivityTimeoutMs` aborts the controller and the loop's catch
315
- // surfaces the timeout error.
316
- armInactivity();
348
+ // surfaces the timeout error. A non-streaming JSON body has no
349
+ // inter-event silence to detect, so the timer stays disarmed there and
350
+ // the total-timeout controller alone bounds the buffered read.
351
+ if (responseKind === "sse") {
352
+ armInactivity();
353
+ }
354
+ // The event source: one branch per response kind, both feeding batches
355
+ // of raw adapter events into the shared accumulator below. SSE yields
356
+ // one batch per wire chunk; JSON buffers the whole body and yields a
357
+ // single batch.
358
+ const rawEventBatches = async function* () {
359
+ if (responseKind === "json") {
360
+ const body = await awaitWithSignal(response.text(), fetchSignal);
361
+ yield adapter.parseJSONResponse(body);
362
+ return;
363
+ }
364
+ for await (const sseData of parseSSE(responseBody)) {
365
+ // Reset inactivity timer — we just got something from the wire.
366
+ armInactivity();
367
+ yield adapter.parseResponse(sseData);
368
+ }
369
+ };
317
370
  try {
318
- for await (const sseData of parseSSE(response.body)) {
371
+ for await (const rawEvents of rawEventBatches()) {
319
372
  if (timeoutReason !== null) {
320
373
  // The timeout aborted the stream; bubble up the right error
321
374
  // shape rather than letting the abort masquerade as a
@@ -344,9 +397,6 @@ async function* runSingleAttempt(opts) {
344
397
  };
345
398
  return;
346
399
  }
347
- // Reset inactivity timer — we just got something from the wire.
348
- armInactivity();
349
- const rawEvents = adapter.parseResponse(sseData);
350
400
  for (const raw of rawEvents) {
351
401
  switch (raw.type) {
352
402
  case "inference.text.delta": {
@@ -372,6 +422,7 @@ async function* runSingleAttempt(opts) {
372
422
  data: {
373
423
  token: raw.data.token,
374
424
  partial: snapshotPartial(partial),
425
+ index: idx,
375
426
  },
376
427
  };
377
428
  break;
@@ -432,24 +483,33 @@ async function* runSingleAttempt(opts) {
432
483
  data: {
433
484
  token: raw.data.token,
434
485
  partial: snapshotPartial(partial),
486
+ index: idx,
435
487
  },
436
488
  };
437
489
  break;
438
490
  }
439
- case "inference.thinking.signature": {
440
- const idx = requireIndex(raw, "thinking.signature");
491
+ case "inference.block.signature": {
492
+ const idx = requireIndex(raw, "block.signature");
441
493
  const existing = blockMap.get(idx);
442
494
  if (existing === undefined) {
443
- throw new ProtocolMismatchError(`harness: thinking.signature at index ${String(idx)} has no preceding thinking block at that index`, raw);
495
+ throw new ProtocolMismatchError(`harness: block.signature at index ${String(idx)} has no preceding block at that index`, raw);
444
496
  }
445
- if (existing.kind !== "thinking") {
446
- throw new ProtocolMismatchError(`harness: thinking.signature at index ${String(idx)} targets an existing ${existing.kind} block, not a thinking block`, raw);
497
+ // A signature authenticates the block whose part it rides on.
498
+ // The signable kinds are the ones whose ContentBlock carries a
499
+ // `signature` field; the others (redacted_thinking, refusal,
500
+ // code_execution_result) have no place to hold one.
501
+ if (existing.kind !== "thinking" &&
502
+ existing.kind !== "text" &&
503
+ existing.kind !== "tool_use" &&
504
+ existing.kind !== "image" &&
505
+ existing.kind !== "code_execution_request") {
506
+ throw new ProtocolMismatchError(`harness: block.signature at index ${String(idx)} targets an existing ${existing.kind} block, which does not carry a signature`, raw);
447
507
  }
448
508
  existing.signature = raw.data.signature;
449
509
  yield {
450
- type: "inference.thinking.signature",
510
+ type: "inference.block.signature",
451
511
  seq: nextSeq(),
452
- data: { signature: raw.data.signature },
512
+ data: { signature: raw.data.signature, index: idx },
453
513
  };
454
514
  break;
455
515
  }
@@ -476,6 +536,16 @@ async function* runSingleAttempt(opts) {
476
536
  };
477
537
  break;
478
538
  }
539
+ case "inference.safety_rating": {
540
+ const safetyRating = raw.data.safetyRating;
541
+ unindexedSafetyRatings.push(safetyRating);
542
+ yield {
543
+ type: "inference.safety_rating",
544
+ seq: nextSeq(),
545
+ data: { safetyRating },
546
+ };
547
+ break;
548
+ }
479
549
  case "inference.thinking.redacted": {
480
550
  const idx = requireIndex(raw, "thinking.redacted");
481
551
  const existing = blockMap.get(idx);
@@ -536,7 +606,12 @@ async function* runSingleAttempt(opts) {
536
606
  yield {
537
607
  type: "inference.tool_call.start",
538
608
  seq: nextSeq(),
539
- data: { callId, name, partial: snapshotPartial(partial) },
609
+ data: {
610
+ callId,
611
+ name,
612
+ partial: snapshotPartial(partial),
613
+ index: toolIdx,
614
+ },
540
615
  };
541
616
  break;
542
617
  }
@@ -837,9 +912,19 @@ async function* runSingleAttempt(opts) {
837
912
  };
838
913
  for (const [idx, entry] of blockMap.entries()) {
839
914
  if (entry.kind === "text") {
840
- if (entry.text.length > 0) {
841
- emit({ type: "text", text: entry.text }, idx);
915
+ // Emit even with empty text if a signature was captured, so a
916
+ // signature riding on an otherwise-empty text carrier still
917
+ // round-trips (mirrors the thinking-block rule below).
918
+ if (entry.text.length === 0 && entry.signature === undefined) {
919
+ continue;
842
920
  }
921
+ emit({
922
+ type: "text",
923
+ text: entry.text,
924
+ ...(entry.signature !== undefined
925
+ ? { signature: entry.signature }
926
+ : {}),
927
+ }, idx);
843
928
  continue;
844
929
  }
845
930
  if (entry.kind === "thinking") {
@@ -890,7 +975,12 @@ async function* runSingleAttempt(opts) {
890
975
  // the tool call from the final turn.
891
976
  throw new ProtocolMismatchError(`harness: tool_use marker at callId ${entry.callId} has no matching completed tool call`, entry);
892
977
  }
893
- emit(finalized, idx);
978
+ if (finalized.type !== "tool_call") {
979
+ throw new ProtocolMismatchError(`harness: tool_use marker at callId ${entry.callId} resolved to a ${finalized.type} block, not a tool_call`, entry);
980
+ }
981
+ emit(entry.signature !== undefined
982
+ ? { ...finalized, signature: entry.signature }
983
+ : finalized, idx);
894
984
  continue;
895
985
  }
896
986
  if (entry.kind === "image") {
@@ -900,7 +990,9 @@ async function* runSingleAttempt(opts) {
900
990
  // atomic, not streamed), so the final-walk emits it
901
991
  // verbatim. Citation interleave applies the same way as
902
992
  // any other block kind.
903
- emit(entry.image, idx);
993
+ emit(entry.signature !== undefined
994
+ ? { ...entry.image, signature: entry.signature }
995
+ : entry.image, idx);
904
996
  continue;
905
997
  }
906
998
  if (entry.kind === "code_execution_request") {
@@ -910,7 +1002,9 @@ async function* runSingleAttempt(opts) {
910
1002
  // current wire delivers all of it atomically on `start`;
911
1003
  // streaming providers would extend `request.code` via the
912
1004
  // delta handler before this walk runs.
913
- emit(entry.request, idx);
1005
+ emit(entry.signature !== undefined
1006
+ ? { ...entry.request, signature: entry.signature }
1007
+ : entry.request, idx);
914
1008
  continue;
915
1009
  }
916
1010
  if (entry.kind === "code_execution_result") {
@@ -931,6 +1025,7 @@ async function* runSingleAttempt(opts) {
931
1025
  throw new ProtocolMismatchError(`harness: ${String(citationsByIndex.size)} citation index/indices have no matching emitted block in the final turn: ${orphanIndices.join(", ")}`, { orphanIndices });
932
1026
  }
933
1027
  contentBlocks.push(...unindexedCitations);
1028
+ contentBlocks.push(...unindexedSafetyRatings);
934
1029
  const finalTurn = {
935
1030
  role: "assistant",
936
1031
  content: contentBlocks,
package/dist/index.d.ts CHANGED
@@ -13,7 +13,7 @@ export { createDefaultRetryPolicy } from "./retry-policy.js";
13
13
  export type { RetryPolicy, RetrySituation, RetryDecision, } from "@intx/types/runtime";
14
14
  export { uploadGoogleGenAIFile } from "./providers/google-genai-files.js";
15
15
  export type { UploadGoogleGenAIFileOpts, UploadGoogleGenAIFileFetch, UploadedGoogleGenAIFile, } from "./providers/google-genai-files.js";
16
- export { createInboundTurn } from "./turns.js";
16
+ export { createInboundTurn, assertWellFormedToolSequence } from "./turns.js";
17
17
  export { createReactor } from "./reactor.js";
18
18
  export type { Reactor, ReactorConfig, ReactorEmittedEvent } from "./reactor.js";
19
19
  export { validateActions } from "./actions.js";
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ export { classifyHTTPError, classifyNetworkError, classifyAbortError, classifySt
7
7
  export { transformMessages, createIDNormalizer } from "./transform.js";
8
8
  export { createDefaultRetryPolicy } from "./retry-policy.js";
9
9
  export { uploadGoogleGenAIFile } from "./providers/google-genai-files.js";
10
- export { createInboundTurn } from "./turns.js";
10
+ export { createInboundTurn, assertWellFormedToolSequence } from "./turns.js";
11
11
  export { createReactor } from "./reactor.js";
12
12
  export { validateActions } from "./actions.js";
13
13
  export { createGateManager } from "./gates.js";
@@ -1,5 +1,7 @@
1
1
  import type { LastCycleSource } from "@intx/types/runtime";
2
2
  import type { ProviderAdapter } from "../adapter.js";
3
+ export declare const ADAPTIVE_THINKING_MODELS: ReadonlySet<string>;
4
+ export declare const ADAPTIVE_THINKING_EFFORT = "high";
3
5
  export type AnthropicRawEvent = {
4
6
  kind: "text_delta";
5
7
  token: string;
@@ -30,4 +32,6 @@ export type AnthropicRawEvent = {
30
32
  } | {
31
33
  kind: "skip";
32
34
  };
33
- export declare function createAnthropicAdapter(source: LastCycleSource): ProviderAdapter;
35
+ export declare const AnthropicQuirks: import("arktype/internal/variants/object.ts").ObjectType<{}, {}>;
36
+ export type AnthropicQuirks = typeof AnthropicQuirks.infer;
37
+ export declare function createAnthropicAdapter(source: LastCycleSource, quirks?: unknown): ProviderAdapter;