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