@intx/inference 0.2.2 → 0.3.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
  /**
@@ -128,6 +129,9 @@ async function* runSingleAttempt(opts) {
128
129
  // different keys.
129
130
  const citationsByIndex = new Map();
130
131
  const unindexedCitations = [];
132
+ // Prompt-level safety signals (no candidate index on the first
133
+ // capture). Appended to the finalized turn after indexed blocks.
134
+ const unindexedSafetyRatings = [];
131
135
  let usageSeen = null;
132
136
  const openToolCalls = new Map();
133
137
  // OpenAI uses index-based tracking before we have a real callId.
@@ -142,7 +146,7 @@ async function* runSingleAttempt(opts) {
142
146
  }
143
147
  let adapter;
144
148
  try {
145
- adapter = deps.adapters.resolve(lastCycleSource);
149
+ adapter = deps.adapters.resolve(lastCycleSource, source.quirks);
146
150
  }
147
151
  catch (cause) {
148
152
  yield {
@@ -309,13 +313,55 @@ async function* runSingleAttempt(opts) {
309
313
  };
310
314
  return;
311
315
  }
316
+ // Captured as a const so the non-null narrowing from the guard above
317
+ // carries into the SSE branch of the event-source generator below (a
318
+ // bare `response.body` re-widens to nullable across the closure).
319
+ const responseBody = response.body;
320
+ let responseKind;
321
+ try {
322
+ responseKind = detectResponseKind(response.headers);
323
+ }
324
+ catch (cause) {
325
+ // A 2xx whose Content-Type is neither SSE nor JSON is a protocol
326
+ // violation, not a transient failure — surface it loudly rather than
327
+ // pushing unknown bytes through the SSE parser to yield an empty turn.
328
+ yield {
329
+ type: "inference.error",
330
+ seq: nextSeq(),
331
+ data: {
332
+ error: classifyProtocolMismatch(cause instanceof Error ? cause.message : String(cause)),
333
+ partial: snapshotPartial(partial),
334
+ },
335
+ };
336
+ return;
337
+ }
312
338
  // Arm the inactivity timer now that the SSE stream is open. Every
313
339
  // event we yield below resets it; sustained silence past
314
340
  // `inactivityTimeoutMs` aborts the controller and the loop's catch
315
- // surfaces the timeout error.
316
- armInactivity();
341
+ // surfaces the timeout error. A non-streaming JSON body has no
342
+ // inter-event silence to detect, so the timer stays disarmed there and
343
+ // the total-timeout controller alone bounds the buffered read.
344
+ if (responseKind === "sse") {
345
+ armInactivity();
346
+ }
347
+ // The event source: one branch per response kind, both feeding batches
348
+ // of raw adapter events into the shared accumulator below. SSE yields
349
+ // one batch per wire chunk; JSON buffers the whole body and yields a
350
+ // single batch.
351
+ const rawEventBatches = async function* () {
352
+ if (responseKind === "json") {
353
+ const body = await awaitWithSignal(response.text(), fetchSignal);
354
+ yield adapter.parseJSONResponse(body);
355
+ return;
356
+ }
357
+ for await (const sseData of parseSSE(responseBody)) {
358
+ // Reset inactivity timer — we just got something from the wire.
359
+ armInactivity();
360
+ yield adapter.parseResponse(sseData);
361
+ }
362
+ };
317
363
  try {
318
- for await (const sseData of parseSSE(response.body)) {
364
+ for await (const rawEvents of rawEventBatches()) {
319
365
  if (timeoutReason !== null) {
320
366
  // The timeout aborted the stream; bubble up the right error
321
367
  // shape rather than letting the abort masquerade as a
@@ -344,9 +390,6 @@ async function* runSingleAttempt(opts) {
344
390
  };
345
391
  return;
346
392
  }
347
- // Reset inactivity timer — we just got something from the wire.
348
- armInactivity();
349
- const rawEvents = adapter.parseResponse(sseData);
350
393
  for (const raw of rawEvents) {
351
394
  switch (raw.type) {
352
395
  case "inference.text.delta": {
@@ -372,6 +415,7 @@ async function* runSingleAttempt(opts) {
372
415
  data: {
373
416
  token: raw.data.token,
374
417
  partial: snapshotPartial(partial),
418
+ index: idx,
375
419
  },
376
420
  };
377
421
  break;
@@ -432,24 +476,33 @@ async function* runSingleAttempt(opts) {
432
476
  data: {
433
477
  token: raw.data.token,
434
478
  partial: snapshotPartial(partial),
479
+ index: idx,
435
480
  },
436
481
  };
437
482
  break;
438
483
  }
439
- case "inference.thinking.signature": {
440
- const idx = requireIndex(raw, "thinking.signature");
484
+ case "inference.block.signature": {
485
+ const idx = requireIndex(raw, "block.signature");
441
486
  const existing = blockMap.get(idx);
442
487
  if (existing === undefined) {
443
- throw new ProtocolMismatchError(`harness: thinking.signature at index ${String(idx)} has no preceding thinking block at that index`, raw);
488
+ throw new ProtocolMismatchError(`harness: block.signature at index ${String(idx)} has no preceding block at that index`, raw);
444
489
  }
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);
490
+ // A signature authenticates the block whose part it rides on.
491
+ // The signable kinds are the ones whose ContentBlock carries a
492
+ // `signature` field; the others (redacted_thinking, refusal,
493
+ // code_execution_result) have no place to hold one.
494
+ if (existing.kind !== "thinking" &&
495
+ existing.kind !== "text" &&
496
+ existing.kind !== "tool_use" &&
497
+ existing.kind !== "image" &&
498
+ existing.kind !== "code_execution_request") {
499
+ throw new ProtocolMismatchError(`harness: block.signature at index ${String(idx)} targets an existing ${existing.kind} block, which does not carry a signature`, raw);
447
500
  }
448
501
  existing.signature = raw.data.signature;
449
502
  yield {
450
- type: "inference.thinking.signature",
503
+ type: "inference.block.signature",
451
504
  seq: nextSeq(),
452
- data: { signature: raw.data.signature },
505
+ data: { signature: raw.data.signature, index: idx },
453
506
  };
454
507
  break;
455
508
  }
@@ -476,6 +529,16 @@ async function* runSingleAttempt(opts) {
476
529
  };
477
530
  break;
478
531
  }
532
+ case "inference.safety_rating": {
533
+ const safetyRating = raw.data.safetyRating;
534
+ unindexedSafetyRatings.push(safetyRating);
535
+ yield {
536
+ type: "inference.safety_rating",
537
+ seq: nextSeq(),
538
+ data: { safetyRating },
539
+ };
540
+ break;
541
+ }
479
542
  case "inference.thinking.redacted": {
480
543
  const idx = requireIndex(raw, "thinking.redacted");
481
544
  const existing = blockMap.get(idx);
@@ -536,7 +599,12 @@ async function* runSingleAttempt(opts) {
536
599
  yield {
537
600
  type: "inference.tool_call.start",
538
601
  seq: nextSeq(),
539
- data: { callId, name, partial: snapshotPartial(partial) },
602
+ data: {
603
+ callId,
604
+ name,
605
+ partial: snapshotPartial(partial),
606
+ index: toolIdx,
607
+ },
540
608
  };
541
609
  break;
542
610
  }
@@ -837,9 +905,19 @@ async function* runSingleAttempt(opts) {
837
905
  };
838
906
  for (const [idx, entry] of blockMap.entries()) {
839
907
  if (entry.kind === "text") {
840
- if (entry.text.length > 0) {
841
- emit({ type: "text", text: entry.text }, idx);
908
+ // Emit even with empty text if a signature was captured, so a
909
+ // signature riding on an otherwise-empty text carrier still
910
+ // round-trips (mirrors the thinking-block rule below).
911
+ if (entry.text.length === 0 && entry.signature === undefined) {
912
+ continue;
842
913
  }
914
+ emit({
915
+ type: "text",
916
+ text: entry.text,
917
+ ...(entry.signature !== undefined
918
+ ? { signature: entry.signature }
919
+ : {}),
920
+ }, idx);
843
921
  continue;
844
922
  }
845
923
  if (entry.kind === "thinking") {
@@ -890,7 +968,12 @@ async function* runSingleAttempt(opts) {
890
968
  // the tool call from the final turn.
891
969
  throw new ProtocolMismatchError(`harness: tool_use marker at callId ${entry.callId} has no matching completed tool call`, entry);
892
970
  }
893
- emit(finalized, idx);
971
+ if (finalized.type !== "tool_call") {
972
+ throw new ProtocolMismatchError(`harness: tool_use marker at callId ${entry.callId} resolved to a ${finalized.type} block, not a tool_call`, entry);
973
+ }
974
+ emit(entry.signature !== undefined
975
+ ? { ...finalized, signature: entry.signature }
976
+ : finalized, idx);
894
977
  continue;
895
978
  }
896
979
  if (entry.kind === "image") {
@@ -900,7 +983,9 @@ async function* runSingleAttempt(opts) {
900
983
  // atomic, not streamed), so the final-walk emits it
901
984
  // verbatim. Citation interleave applies the same way as
902
985
  // any other block kind.
903
- emit(entry.image, idx);
986
+ emit(entry.signature !== undefined
987
+ ? { ...entry.image, signature: entry.signature }
988
+ : entry.image, idx);
904
989
  continue;
905
990
  }
906
991
  if (entry.kind === "code_execution_request") {
@@ -910,7 +995,9 @@ async function* runSingleAttempt(opts) {
910
995
  // current wire delivers all of it atomically on `start`;
911
996
  // streaming providers would extend `request.code` via the
912
997
  // delta handler before this walk runs.
913
- emit(entry.request, idx);
998
+ emit(entry.signature !== undefined
999
+ ? { ...entry.request, signature: entry.signature }
1000
+ : entry.request, idx);
914
1001
  continue;
915
1002
  }
916
1003
  if (entry.kind === "code_execution_result") {
@@ -931,6 +1018,7 @@ async function* runSingleAttempt(opts) {
931
1018
  throw new ProtocolMismatchError(`harness: ${String(citationsByIndex.size)} citation index/indices have no matching emitted block in the final turn: ${orphanIndices.join(", ")}`, { orphanIndices });
932
1019
  }
933
1020
  contentBlocks.push(...unindexedCitations);
1021
+ contentBlocks.push(...unindexedSafetyRatings);
934
1022
  const finalTurn = {
935
1023
  role: "assistant",
936
1024
  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;
@@ -1,5 +1,5 @@
1
1
  import { type } from "arktype";
2
- import { CitationBlock as CitationBlockType } from "@intx/types/runtime";
2
+ import { CitationBlock as CitationBlockType, formatSafetyRatingText, } from "@intx/types/runtime";
3
3
  import { CREDENTIAL_SENTINEL } from "../auth.js";
4
4
  import { ProtocolMismatchError } from "../errors.js";
5
5
  import { decodeToolName, encodeToolName, } from "../tool-name.js";
@@ -10,6 +10,27 @@ const ANTHROPIC_TOOL_NAME_LIMIT = {
10
10
  provider: "anthropic",
11
11
  maxLength: 128,
12
12
  };
13
+ // Models that reject thinking:{type:"enabled",budget_tokens} and require
14
+ // thinking:{type:"adaptive"} with output_config.effort. The discovery
15
+ // plug-in's ADAPTIVE_THINKING_MODELS set must match this one; a guard test in
16
+ // the anthropic discovery package pins the two equal so they cannot drift.
17
+ export const ADAPTIVE_THINKING_MODELS = new Set([
18
+ "claude-sonnet-5",
19
+ "claude-opus-5",
20
+ "claude-fable-5",
21
+ "claude-opus-4-8",
22
+ "claude-opus-4-6",
23
+ "claude-opus-4-7",
24
+ "claude-sonnet-4-6",
25
+ ]);
26
+ // The effort this adapter sends on the adaptive-thinking wire in production.
27
+ // "high" is the Anthropic API default. The discovery capture rig deliberately
28
+ // sends "max" instead: only "max" reliably elicits a thinking block to capture,
29
+ // so the production default and the capture value are an intentional pair, not
30
+ // drift. ADAPTIVE_THINKING_MODELS above must match across the two layers; the
31
+ // effort values, by contrast, are meant to differ. A guard test in the
32
+ // discovery package checks both effort values.
33
+ export const ADAPTIVE_THINKING_EFFORT = "high";
13
34
  // ---------------------------------------------------------------------------
14
35
  // Request building
15
36
  // ---------------------------------------------------------------------------
@@ -47,10 +68,19 @@ function buildRequest(messages, model, options) {
47
68
  ];
48
69
  }
49
70
  if (options.thinking?.enabled) {
50
- body["thinking"] = {
51
- type: "enabled",
52
- budget_tokens: options.thinking.budgetTokens ?? 1024,
53
- };
71
+ // Adaptive models reject the classic budget_tokens shape with
72
+ // invalid_request_error and require thinking:{type:"adaptive"}
73
+ // plus output_config.effort.
74
+ if (ADAPTIVE_THINKING_MODELS.has(model)) {
75
+ body["thinking"] = { type: "adaptive" };
76
+ body["output_config"] = { effort: ADAPTIVE_THINKING_EFFORT };
77
+ }
78
+ else {
79
+ body["thinking"] = {
80
+ type: "enabled",
81
+ budget_tokens: options.thinking.budgetTokens ?? 1024,
82
+ };
83
+ }
54
84
  }
55
85
  if (options.tools !== undefined && options.tools.length > 0) {
56
86
  const tools = options.tools.map((t) => ({
@@ -94,7 +124,18 @@ function rejectUnsupportedResponseFormat(format) {
94
124
  }
95
125
  function toAnthropicMessage(msg, cacheLastBlock) {
96
126
  const role = msg.role === "assistant" ? "assistant" : "user";
97
- const content = msg.content.map(toAnthropicBlock);
127
+ // safety_rating is Gemini output-only metadata. Rewrite as text so
128
+ // role alternation and the block reason survive Anthropic history
129
+ // without a native safety_rating input shape.
130
+ const content = msg.content.map((block) => {
131
+ if (block.type === "safety_rating") {
132
+ return toAnthropicBlock({
133
+ type: "text",
134
+ text: formatSafetyRatingText(block),
135
+ });
136
+ }
137
+ return toAnthropicBlock(block);
138
+ });
98
139
  if (cacheLastBlock) {
99
140
  const lastBlock = content[content.length - 1];
100
141
  if (lastBlock !== undefined) {
@@ -241,12 +282,21 @@ function toAnthropicBlock(block) {
241
282
  case "image":
242
283
  return { type: "image", source: toAnthropicMediaSource(block.source) };
243
284
  case "document":
244
- return { type: "document", source: toAnthropicMediaSource(block.source) };
285
+ return {
286
+ type: "document",
287
+ source: toAnthropicMediaSource(block.source),
288
+ ...(block.title !== undefined ? { title: block.title } : {}),
289
+ ...(block.context !== undefined ? { context: block.context } : {}),
290
+ };
245
291
  case "audio":
246
292
  case "video":
247
293
  throw new Error(`Anthropic adapter does not yet handle ${block.type} content blocks.`);
248
294
  case "citation":
249
295
  throw new Error("Anthropic adapter does not yet emit citation content blocks.");
296
+ case "safety_rating":
297
+ // Rewritten to text in toAnthropicMessage before this switch.
298
+ throw new Error("Anthropic adapter: safety_rating blocks must be rewritten to " +
299
+ "text before toAnthropicBlock.");
250
300
  case "code_execution_request":
251
301
  case "code_execution_result":
252
302
  throw new Error(`Anthropic adapter does not yet emit ${block.type} content blocks.`);
@@ -405,6 +455,19 @@ const AnthropicSSEEvent = ContentBlockDelta.or(ContentBlockStart)
405
455
  .or(MessageStart)
406
456
  .or(MessageStop)
407
457
  .or(Ping);
458
+ // Maps Anthropic's wire usage object onto the internal TokenUsage. Anthropic
459
+ // never reports a distinct thinking-token count, so `thinking` is always 0.
460
+ // Shared by the streaming `message_start` path and the non-streaming
461
+ // `parseJSONResponse`, whose usage objects carry the same field names.
462
+ function toInferenceUsage(usage) {
463
+ return {
464
+ input: usage.input_tokens ?? 0,
465
+ output: usage.output_tokens ?? 0,
466
+ cacheRead: usage.cache_read_input_tokens ?? 0,
467
+ cacheWrite: usage.cache_creation_input_tokens ?? 0,
468
+ thinking: 0,
469
+ };
470
+ }
408
471
  function parseResponse(sseData, blockIndexToCallId, source) {
409
472
  // Same protocol-mismatch posture as the openai adapter: a JSON parse
410
473
  // failure or arktype rejection means the upstream emitted bytes that
@@ -460,7 +523,7 @@ function parseResponse(sseData, blockIndexToCallId, source) {
460
523
  const signature = delta.signature ?? "";
461
524
  return [
462
525
  {
463
- type: "inference.thinking.signature",
526
+ type: "inference.block.signature",
464
527
  seq,
465
528
  data: { signature, index },
466
529
  },
@@ -603,18 +666,11 @@ function parseResponse(sseData, blockIndexToCallId, source) {
603
666
  const msgUsage = event.message?.usage;
604
667
  if (msgUsage === undefined)
605
668
  return [];
606
- const inferenceUsage = {
607
- input: msgUsage.input_tokens ?? 0,
608
- output: msgUsage.output_tokens ?? 0,
609
- cacheRead: msgUsage.cache_read_input_tokens ?? 0,
610
- cacheWrite: msgUsage.cache_creation_input_tokens ?? 0,
611
- thinking: 0,
612
- };
613
669
  return [
614
670
  {
615
671
  type: "inference.usage",
616
672
  seq,
617
- data: { usage: inferenceUsage, source },
673
+ data: { usage: toInferenceUsage(msgUsage), source },
618
674
  },
619
675
  ];
620
676
  }
@@ -623,6 +679,186 @@ function parseResponse(sseData, blockIndexToCallId, source) {
623
679
  return [];
624
680
  }
625
681
  }
682
+ // ---------------------------------------------------------------------------
683
+ // Non-streaming response parsing
684
+ //
685
+ // The non-streaming Messages endpoint returns the same content blocks the
686
+ // streaming protocol delivers incrementally, delivered whole in one JSON
687
+ // body. `parseJSONResponse` re-expresses each complete block as the same
688
+ // InferenceEvent vocabulary `parseResponse` emits, so a replayed
689
+ // non-streaming capture feeds the harness accumulator identically to its
690
+ // streaming sibling. Block types the streaming parser does not model
691
+ // (server_tool_use, web_search_tool_result, code_execution_tool_result)
692
+ // emit nothing here too; bringing those cells to parity across both paths is
693
+ // owned by the strict-mode replay regression, not this parser.
694
+ // ---------------------------------------------------------------------------
695
+ const NonStreamingUsage = type({
696
+ "input_tokens?": "number",
697
+ "output_tokens?": "number",
698
+ "cache_read_input_tokens?": "number",
699
+ "cache_creation_input_tokens?": "number",
700
+ });
701
+ const NonStreamingMessage = type({
702
+ type: "'message'",
703
+ content: "unknown[]",
704
+ usage: NonStreamingUsage,
705
+ });
706
+ const BlockTag = type({ type: "string" });
707
+ const NonStreamingTextBlock = type({
708
+ type: "'text'",
709
+ "text?": "string",
710
+ "citations?": AnthropicCitation.array(),
711
+ });
712
+ const NonStreamingToolUseBlock = type({
713
+ type: "'tool_use'",
714
+ "id?": "string",
715
+ "name?": "string",
716
+ "input?": "unknown",
717
+ });
718
+ const NonStreamingThinkingBlock = type({
719
+ type: "'thinking'",
720
+ "thinking?": "string",
721
+ "signature?": "string",
722
+ });
723
+ const NonStreamingRedactedThinkingBlock = type({
724
+ type: "'redacted_thinking'",
725
+ "data?": "string",
726
+ });
727
+ function parseJSONResponse(body, source) {
728
+ let parsed;
729
+ try {
730
+ parsed = JSON.parse(body);
731
+ }
732
+ catch (cause) {
733
+ const message = cause instanceof Error ? cause.message : String(cause);
734
+ throw new ProtocolMismatchError(`anthropic parseJSONResponse: malformed JSON response body: ${message}`, body);
735
+ }
736
+ const message = NonStreamingMessage(parsed);
737
+ if (message instanceof type.errors) {
738
+ throw new ProtocolMismatchError(`anthropic parseJSONResponse: response failed schema validation: ${message.summary}`, parsed);
739
+ }
740
+ // The seq field is a placeholder 0 — the harness assigns real sequence
741
+ // numbers, exactly as on the streaming path.
742
+ const seq = 0;
743
+ const events = [];
744
+ message.content.forEach((rawBlock, index) => {
745
+ const tagged = BlockTag(rawBlock);
746
+ if (tagged instanceof type.errors) {
747
+ throw new ProtocolMismatchError(`anthropic parseJSONResponse: content block ${String(index)} has no string type: ${tagged.summary}`, rawBlock);
748
+ }
749
+ switch (tagged.type) {
750
+ case "text": {
751
+ const block = NonStreamingTextBlock(rawBlock);
752
+ if (block instanceof type.errors) {
753
+ throw new ProtocolMismatchError(`anthropic parseJSONResponse: text block ${String(index)} failed validation: ${block.summary}`, rawBlock);
754
+ }
755
+ events.push({
756
+ type: "inference.text.delta",
757
+ seq,
758
+ data: { token: block.text ?? "", partial: EMPTY_PARTIAL, index },
759
+ });
760
+ // The streaming path emits one inference.citation per citations_delta
761
+ // keyed to the enclosing text block's index; the non-streaming shape
762
+ // carries those same citations inline on the block.
763
+ for (const citation of block.citations ?? []) {
764
+ events.push({
765
+ type: "inference.citation",
766
+ seq,
767
+ data: { citation: toCitationBlock(citation, index), index },
768
+ });
769
+ }
770
+ break;
771
+ }
772
+ case "tool_use": {
773
+ const block = NonStreamingToolUseBlock(rawBlock);
774
+ if (block instanceof type.errors) {
775
+ throw new ProtocolMismatchError(`anthropic parseJSONResponse: tool_use block ${String(index)} failed validation: ${block.summary}`, rawBlock);
776
+ }
777
+ // callId falls back to the block index exactly as the streaming
778
+ // content_block_start does, so a tool_use block with no id still
779
+ // correlates its start and args delta.
780
+ const callId = block.id ?? String(index);
781
+ events.push({
782
+ type: "inference.tool_call.start",
783
+ seq,
784
+ data: {
785
+ callId,
786
+ name: decodeToolName(block.name ?? ""),
787
+ partial: EMPTY_PARTIAL,
788
+ index,
789
+ },
790
+ });
791
+ events.push({
792
+ type: "inference.tool_call.delta",
793
+ seq,
794
+ data: {
795
+ callId,
796
+ argumentFragment: JSON.stringify(block.input ?? {}),
797
+ partial: EMPTY_PARTIAL,
798
+ index,
799
+ },
800
+ });
801
+ break;
802
+ }
803
+ case "thinking": {
804
+ const block = NonStreamingThinkingBlock(rawBlock);
805
+ if (block instanceof type.errors) {
806
+ throw new ProtocolMismatchError(`anthropic parseJSONResponse: thinking block ${String(index)} failed validation: ${block.summary}`, rawBlock);
807
+ }
808
+ // Emit the thinking delta first so the harness has a thinking block
809
+ // at this index before the signature arrives; a signature with no
810
+ // preceding thinking entry is a protocol violation the harness
811
+ // rejects.
812
+ events.push({
813
+ type: "inference.thinking.delta",
814
+ seq,
815
+ data: { token: block.thinking ?? "", partial: EMPTY_PARTIAL, index },
816
+ });
817
+ if (block.signature !== undefined) {
818
+ events.push({
819
+ type: "inference.block.signature",
820
+ seq,
821
+ data: { signature: block.signature, index },
822
+ });
823
+ }
824
+ break;
825
+ }
826
+ case "redacted_thinking": {
827
+ const block = NonStreamingRedactedThinkingBlock(rawBlock);
828
+ if (block instanceof type.errors) {
829
+ throw new ProtocolMismatchError(`anthropic parseJSONResponse: redacted_thinking block ${String(index)} failed validation: ${block.summary}`, rawBlock);
830
+ }
831
+ // The opaque `data` blob must echo back verbatim on follow-up turns;
832
+ // a missing `data` is a protocol violation, not a default-to-empty
833
+ // case, matching the streaming redacted_thinking handling.
834
+ if (block.data === undefined) {
835
+ throw new ProtocolMismatchError(`anthropic parseJSONResponse: redacted_thinking block ${String(index)} missing required \`data\` field`, rawBlock);
836
+ }
837
+ events.push({
838
+ type: "inference.thinking.redacted",
839
+ seq,
840
+ data: {
841
+ redactedThinking: { type: "redacted_thinking", data: block.data },
842
+ index,
843
+ },
844
+ });
845
+ break;
846
+ }
847
+ default:
848
+ // server_tool_use, web_search_tool_result,
849
+ // code_execution_tool_result, and any future block type: the
850
+ // streaming parser emits nothing for these, so mirror that rather
851
+ // than diverge from a path with no passing reference yet.
852
+ break;
853
+ }
854
+ });
855
+ events.push({
856
+ type: "inference.usage",
857
+ seq,
858
+ data: { usage: toInferenceUsage(message.usage), source },
859
+ });
860
+ return events;
861
+ }
626
862
  function extractRetryAfterMs(headers) {
627
863
  const raw = headers.get("retry-after");
628
864
  if (raw === null)
@@ -659,11 +895,22 @@ function extractPacingDelayMs(headers) {
659
895
  }
660
896
  return delays.length > 0 ? Math.max(...delays) : undefined;
661
897
  }
662
- export function createAnthropicAdapter(source) {
898
+ // The anthropic adapter carries no per-source accommodations today, so its
899
+ // quirks shape is empty. A quirks bag is deployment configuration crossing
900
+ // into the system at this boundary; rejecting unknown keys makes a
901
+ // misconfigured bag — for example an openai quirk pasted onto an anthropic
902
+ // source — fail loudly here rather than run silently ignored.
903
+ export const AnthropicQuirks = type({ "+": "reject" });
904
+ export function createAnthropicAdapter(source, quirks) {
905
+ const parsedQuirks = AnthropicQuirks(quirks ?? {});
906
+ if (parsedQuirks instanceof type.errors) {
907
+ throw new Error(`anthropic adapter: invalid quirks: ${parsedQuirks.summary}`);
908
+ }
663
909
  const blockIndexToCallId = new Map();
664
910
  return {
665
911
  buildRequest,
666
912
  parseResponse: (sseData) => parseResponse(sseData, blockIndexToCallId, source),
913
+ parseJSONResponse: (body) => parseJSONResponse(body, source),
667
914
  extractRetryAfterMs,
668
915
  extractPacingDelayMs,
669
916
  };
@@ -1,3 +1,5 @@
1
1
  import type { LastCycleSource } from "@intx/types/runtime";
2
2
  import type { ProviderAdapter } from "../adapter.js";
3
- export declare function createGoogleGenAIAdapter(source: LastCycleSource): ProviderAdapter;
3
+ export declare const GoogleGenAIQuirks: import("arktype/internal/variants/object.ts").ObjectType<{}, {}>;
4
+ export type GoogleGenAIQuirks = typeof GoogleGenAIQuirks.infer;
5
+ export declare function createGoogleGenAIAdapter(source: LastCycleSource, quirks?: unknown): ProviderAdapter;