@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/adapter.d.ts +4 -2
- package/dist/adapter.js +2 -2
- package/dist/assembly.d.ts +8 -1
- package/dist/assembly.js +2 -1
- 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.js +109 -21
- 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 +264 -17
- 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.js +416 -103
- package/dist/transform.d.ts +1 -1
- package/dist/transform.js +19 -4
- package/package.json +5 -4
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
|
-
|
|
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
|
|
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.
|
|
440
|
-
const idx = requireIndex(raw, "
|
|
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:
|
|
488
|
+
throw new ProtocolMismatchError(`harness: block.signature at index ${String(idx)} has no preceding block at that index`, raw);
|
|
444
489
|
}
|
|
445
|
-
|
|
446
|
-
|
|
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.
|
|
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: {
|
|
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
|
-
|
|
841
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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.`);
|
|
@@ -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.
|
|
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:
|
|
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
|
-
|
|
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
|
|
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;
|