@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.
@@ -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.`);
@@ -399,12 +449,20 @@ const MessageStart = type({
399
449
  });
400
450
  const MessageStop = type({ type: "'message_stop'" });
401
451
  const Ping = type({ type: "'ping'" });
402
- const AnthropicSSEEvent = ContentBlockDelta.or(ContentBlockStart)
403
- .or(ContentBlockStop)
404
- .or(MessageDelta)
405
- .or(MessageStart)
406
- .or(MessageStop)
407
- .or(Ping);
452
+ const AnthropicSSEEvent = type.or(ContentBlockDelta, ContentBlockStart, ContentBlockStop, MessageDelta, MessageStart, MessageStop, Ping);
453
+ // Maps Anthropic's wire usage object onto the internal TokenUsage. Anthropic
454
+ // never reports a distinct thinking-token count, so `thinking` is always 0.
455
+ // Shared by the streaming `message_start` path and the non-streaming
456
+ // `parseJSONResponse`, whose usage objects carry the same field names.
457
+ function toInferenceUsage(usage) {
458
+ return {
459
+ input: usage.input_tokens ?? 0,
460
+ output: usage.output_tokens ?? 0,
461
+ cacheRead: usage.cache_read_input_tokens ?? 0,
462
+ cacheWrite: usage.cache_creation_input_tokens ?? 0,
463
+ thinking: 0,
464
+ };
465
+ }
408
466
  function parseResponse(sseData, blockIndexToCallId, source) {
409
467
  // Same protocol-mismatch posture as the openai adapter: a JSON parse
410
468
  // failure or arktype rejection means the upstream emitted bytes that
@@ -460,7 +518,7 @@ function parseResponse(sseData, blockIndexToCallId, source) {
460
518
  const signature = delta.signature ?? "";
461
519
  return [
462
520
  {
463
- type: "inference.thinking.signature",
521
+ type: "inference.block.signature",
464
522
  seq,
465
523
  data: { signature, index },
466
524
  },
@@ -603,18 +661,11 @@ function parseResponse(sseData, blockIndexToCallId, source) {
603
661
  const msgUsage = event.message?.usage;
604
662
  if (msgUsage === undefined)
605
663
  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
664
  return [
614
665
  {
615
666
  type: "inference.usage",
616
667
  seq,
617
- data: { usage: inferenceUsage, source },
668
+ data: { usage: toInferenceUsage(msgUsage), source },
618
669
  },
619
670
  ];
620
671
  }
@@ -623,6 +674,186 @@ function parseResponse(sseData, blockIndexToCallId, source) {
623
674
  return [];
624
675
  }
625
676
  }
677
+ // ---------------------------------------------------------------------------
678
+ // Non-streaming response parsing
679
+ //
680
+ // The non-streaming Messages endpoint returns the same content blocks the
681
+ // streaming protocol delivers incrementally, delivered whole in one JSON
682
+ // body. `parseJSONResponse` re-expresses each complete block as the same
683
+ // InferenceEvent vocabulary `parseResponse` emits, so a replayed
684
+ // non-streaming capture feeds the harness accumulator identically to its
685
+ // streaming sibling. Block types the streaming parser does not model
686
+ // (server_tool_use, web_search_tool_result, code_execution_tool_result)
687
+ // emit nothing here too; bringing those cells to parity across both paths is
688
+ // owned by the strict-mode replay regression, not this parser.
689
+ // ---------------------------------------------------------------------------
690
+ const NonStreamingUsage = type({
691
+ "input_tokens?": "number",
692
+ "output_tokens?": "number",
693
+ "cache_read_input_tokens?": "number",
694
+ "cache_creation_input_tokens?": "number",
695
+ });
696
+ const NonStreamingMessage = type({
697
+ type: "'message'",
698
+ content: "unknown[]",
699
+ usage: NonStreamingUsage,
700
+ });
701
+ const BlockTag = type({ type: "string" });
702
+ const NonStreamingTextBlock = type({
703
+ type: "'text'",
704
+ "text?": "string",
705
+ "citations?": AnthropicCitation.array(),
706
+ });
707
+ const NonStreamingToolUseBlock = type({
708
+ type: "'tool_use'",
709
+ "id?": "string",
710
+ "name?": "string",
711
+ "input?": "unknown",
712
+ });
713
+ const NonStreamingThinkingBlock = type({
714
+ type: "'thinking'",
715
+ "thinking?": "string",
716
+ "signature?": "string",
717
+ });
718
+ const NonStreamingRedactedThinkingBlock = type({
719
+ type: "'redacted_thinking'",
720
+ "data?": "string",
721
+ });
722
+ function parseJSONResponse(body, source) {
723
+ let parsed;
724
+ try {
725
+ parsed = JSON.parse(body);
726
+ }
727
+ catch (cause) {
728
+ const message = cause instanceof Error ? cause.message : String(cause);
729
+ throw new ProtocolMismatchError(`anthropic parseJSONResponse: malformed JSON response body: ${message}`, body);
730
+ }
731
+ const message = NonStreamingMessage(parsed);
732
+ if (message instanceof type.errors) {
733
+ throw new ProtocolMismatchError(`anthropic parseJSONResponse: response failed schema validation: ${message.summary}`, parsed);
734
+ }
735
+ // The seq field is a placeholder 0 — the harness assigns real sequence
736
+ // numbers, exactly as on the streaming path.
737
+ const seq = 0;
738
+ const events = [];
739
+ message.content.forEach((rawBlock, index) => {
740
+ const tagged = BlockTag(rawBlock);
741
+ if (tagged instanceof type.errors) {
742
+ throw new ProtocolMismatchError(`anthropic parseJSONResponse: content block ${String(index)} has no string type: ${tagged.summary}`, rawBlock);
743
+ }
744
+ switch (tagged.type) {
745
+ case "text": {
746
+ const block = NonStreamingTextBlock(rawBlock);
747
+ if (block instanceof type.errors) {
748
+ throw new ProtocolMismatchError(`anthropic parseJSONResponse: text block ${String(index)} failed validation: ${block.summary}`, rawBlock);
749
+ }
750
+ events.push({
751
+ type: "inference.text.delta",
752
+ seq,
753
+ data: { token: block.text ?? "", partial: EMPTY_PARTIAL, index },
754
+ });
755
+ // The streaming path emits one inference.citation per citations_delta
756
+ // keyed to the enclosing text block's index; the non-streaming shape
757
+ // carries those same citations inline on the block.
758
+ for (const citation of block.citations ?? []) {
759
+ events.push({
760
+ type: "inference.citation",
761
+ seq,
762
+ data: { citation: toCitationBlock(citation, index), index },
763
+ });
764
+ }
765
+ break;
766
+ }
767
+ case "tool_use": {
768
+ const block = NonStreamingToolUseBlock(rawBlock);
769
+ if (block instanceof type.errors) {
770
+ throw new ProtocolMismatchError(`anthropic parseJSONResponse: tool_use block ${String(index)} failed validation: ${block.summary}`, rawBlock);
771
+ }
772
+ // callId falls back to the block index exactly as the streaming
773
+ // content_block_start does, so a tool_use block with no id still
774
+ // correlates its start and args delta.
775
+ const callId = block.id ?? String(index);
776
+ events.push({
777
+ type: "inference.tool_call.start",
778
+ seq,
779
+ data: {
780
+ callId,
781
+ name: decodeToolName(block.name ?? ""),
782
+ partial: EMPTY_PARTIAL,
783
+ index,
784
+ },
785
+ });
786
+ events.push({
787
+ type: "inference.tool_call.delta",
788
+ seq,
789
+ data: {
790
+ callId,
791
+ argumentFragment: JSON.stringify(block.input ?? {}),
792
+ partial: EMPTY_PARTIAL,
793
+ index,
794
+ },
795
+ });
796
+ break;
797
+ }
798
+ case "thinking": {
799
+ const block = NonStreamingThinkingBlock(rawBlock);
800
+ if (block instanceof type.errors) {
801
+ throw new ProtocolMismatchError(`anthropic parseJSONResponse: thinking block ${String(index)} failed validation: ${block.summary}`, rawBlock);
802
+ }
803
+ // Emit the thinking delta first so the harness has a thinking block
804
+ // at this index before the signature arrives; a signature with no
805
+ // preceding thinking entry is a protocol violation the harness
806
+ // rejects.
807
+ events.push({
808
+ type: "inference.thinking.delta",
809
+ seq,
810
+ data: { token: block.thinking ?? "", partial: EMPTY_PARTIAL, index },
811
+ });
812
+ if (block.signature !== undefined) {
813
+ events.push({
814
+ type: "inference.block.signature",
815
+ seq,
816
+ data: { signature: block.signature, index },
817
+ });
818
+ }
819
+ break;
820
+ }
821
+ case "redacted_thinking": {
822
+ const block = NonStreamingRedactedThinkingBlock(rawBlock);
823
+ if (block instanceof type.errors) {
824
+ throw new ProtocolMismatchError(`anthropic parseJSONResponse: redacted_thinking block ${String(index)} failed validation: ${block.summary}`, rawBlock);
825
+ }
826
+ // The opaque `data` blob must echo back verbatim on follow-up turns;
827
+ // a missing `data` is a protocol violation, not a default-to-empty
828
+ // case, matching the streaming redacted_thinking handling.
829
+ if (block.data === undefined) {
830
+ throw new ProtocolMismatchError(`anthropic parseJSONResponse: redacted_thinking block ${String(index)} missing required \`data\` field`, rawBlock);
831
+ }
832
+ events.push({
833
+ type: "inference.thinking.redacted",
834
+ seq,
835
+ data: {
836
+ redactedThinking: { type: "redacted_thinking", data: block.data },
837
+ index,
838
+ },
839
+ });
840
+ break;
841
+ }
842
+ default:
843
+ // server_tool_use, web_search_tool_result,
844
+ // code_execution_tool_result, and any future block type: the
845
+ // streaming parser emits nothing for these, so mirror that rather
846
+ // than diverge from a path with no passing reference yet.
847
+ break;
848
+ }
849
+ });
850
+ events.push({
851
+ type: "inference.usage",
852
+ seq,
853
+ data: { usage: toInferenceUsage(message.usage), source },
854
+ });
855
+ return events;
856
+ }
626
857
  function extractRetryAfterMs(headers) {
627
858
  const raw = headers.get("retry-after");
628
859
  if (raw === null)
@@ -659,11 +890,22 @@ function extractPacingDelayMs(headers) {
659
890
  }
660
891
  return delays.length > 0 ? Math.max(...delays) : undefined;
661
892
  }
662
- export function createAnthropicAdapter(source) {
893
+ // The anthropic adapter carries no per-source accommodations today, so its
894
+ // quirks shape is empty. A quirks bag is deployment configuration crossing
895
+ // into the system at this boundary; rejecting unknown keys makes a
896
+ // misconfigured bag — for example an openai quirk pasted onto an anthropic
897
+ // source — fail loudly here rather than run silently ignored.
898
+ export const AnthropicQuirks = type({ "+": "reject" });
899
+ export function createAnthropicAdapter(source, quirks) {
900
+ const parsedQuirks = AnthropicQuirks(quirks ?? {});
901
+ if (parsedQuirks instanceof type.errors) {
902
+ throw new Error(`anthropic adapter: invalid quirks: ${parsedQuirks.summary}`);
903
+ }
663
904
  const blockIndexToCallId = new Map();
664
905
  return {
665
906
  buildRequest,
666
907
  parseResponse: (sseData) => parseResponse(sseData, blockIndexToCallId, source),
908
+ parseJSONResponse: (body) => parseJSONResponse(body, source),
667
909
  extractRetryAfterMs,
668
910
  extractPacingDelayMs,
669
911
  };
@@ -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;