@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/adapter.d.ts +4 -2
- package/dist/adapter.js +2 -2
- package/dist/assembly.d.ts +16 -1
- package/dist/assembly.js +4 -1
- package/dist/auth.d.ts +2 -1
- package/dist/auth.js +14 -5
- package/dist/authz-extension.d.ts +15 -1
- package/dist/authz-extension.js +93 -9
- package/dist/correlation.d.ts +1 -0
- package/dist/correlation.js +8 -1
- package/dist/default-director.d.ts +1 -1
- package/dist/default-director.js +37 -8
- package/dist/gates.d.ts +1 -0
- package/dist/gates.js +24 -1
- package/dist/harness.d.ts +2 -0
- package/dist/harness.js +118 -23
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/providers/anthropic.d.ts +5 -1
- package/dist/providers/anthropic.js +265 -23
- package/dist/providers/google-genai.d.ts +3 -1
- package/dist/providers/google-genai.js +224 -215
- package/dist/providers/index.d.ts +3 -3
- package/dist/providers/index.js +3 -3
- package/dist/providers/openai.d.ts +7 -1
- package/dist/providers/openai.js +353 -59
- package/dist/reactor.d.ts +18 -0
- package/dist/reactor.js +510 -116
- package/dist/transform.d.ts +1 -1
- package/dist/transform.js +19 -4
- package/package.json +5 -4
|
@@ -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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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 {
|
|
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 =
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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
|
|
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;
|