@cueai/omni-reader-mcp 1.6.0 → 1.7.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,12 +1,12 @@
1
1
  import { createHash } from "node:crypto";
2
- import { DEFAULT_IIIS_GRANTED_BASE_URL, GRANTED_STREAM_PROTOCOL_VERSION, RESULT_CHUNK_MAX_BYTES, } from "./constants.js";
2
+ import { DEFAULT_IIIS_GRANTED_BASE_URL, GRANTED_STREAM_PROTOCOL_VERSION, MAX_FILE_BYTES, RESULT_CHUNK_MAX_BYTES, } from "./constants.js";
3
3
  import { OmniBridgeError } from "./errors.js";
4
4
  import { createMultipartBody } from "./multipart-body.js";
5
5
  import { NOOP_PROGRESS, } from "./progress.js";
6
6
  class TransportFailure extends Error {
7
7
  }
8
8
  const ACTIVE_STATUSES = new Set(["CLAIMED", "UPLOADING", "PROCESSING", "SETTLING"]);
9
- const TERMINAL_STATUSES = new Set([
9
+ const LEGACY_TERMINAL_STATUSES = new Set([
10
10
  "EXPIRED",
11
11
  "SETTLEMENT_DENIED",
12
12
  "FAILED",
@@ -14,14 +14,27 @@ const TERMINAL_STATUSES = new Set([
14
14
  "DELIVERY_EXPIRED",
15
15
  "DELIVERED",
16
16
  ]);
17
- const OPERATION_STATUSES = new Set([
17
+ const PHASE2_TERMINAL_STATUSES = new Set([
18
+ ...LEGACY_TERMINAL_STATUSES,
19
+ "UNSUPPORTED",
20
+ ]);
21
+ const SETTLED_PHASE2_STATUSES = new Set([
22
+ "RELEASED",
23
+ "DELIVERED",
24
+ "DELIVERY_EXPIRED",
25
+ ]);
26
+ const LEGACY_OPERATION_STATUSES = new Set([
18
27
  "ISSUED",
19
28
  "CLAIMED",
20
29
  "UPLOADING",
21
30
  "PROCESSING",
22
31
  "SETTLING",
23
32
  "RELEASED",
24
- ...TERMINAL_STATUSES,
33
+ ...LEGACY_TERMINAL_STATUSES,
34
+ ]);
35
+ const PHASE2_OPERATION_STATUSES = new Set([
36
+ ...LEGACY_OPERATION_STATUSES,
37
+ "UNSUPPORTED",
25
38
  ]);
26
39
  const RECOVERABLE_SETTLEMENT_ERRORS = new Set([
27
40
  "SETTLEMENT_IN_PROGRESS",
@@ -33,6 +46,15 @@ const RECOVERABLE_SETTLEMENT_ERRORS = new Set([
33
46
  ]);
34
47
  const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;
35
48
  const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/;
49
+ const STREAM_V2_PROTOCOL = "omni.granted_parse_stream.v2";
50
+ const STREAM_V3_PROTOCOL = "omni.granted_parse_stream.v3";
51
+ const OPERATION_V2_PROTOCOL = "omni.direct_operation.v2";
52
+ const OPERATION_V3_PROTOCOL = "omni.direct_operation.v3";
53
+ const TEXT_BILLING_PROFILE = "omni.direct_text_billing.v1";
54
+ const GROUNDING_BILLING_PROFILE = "omni.direct_grounding_billing.v1";
55
+ const LEGACY_GROUNDING_PROFILE = "omni.direct_grounding.v1";
56
+ const TEXT_MEDIA_TYPE = "text/markdown; charset=utf-8";
57
+ const BUNDLE_MEDIA_TYPE = "application/vnd.cue.omni-result-bundle+json; version=1";
36
58
  const ERROR_CODE_PATTERN = /^[A-Z][A-Z0-9_]{0,127}$/;
37
59
  const RFC3339_PATTERN = /^(\d{4})-(\d{2})-(\d{2})[Tt](?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:[Zz]|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/u;
38
60
  const PROGRESS_UNITS = new Set([
@@ -59,6 +81,7 @@ function bridgeError(code, message, facts = {}) {
59
81
  function recoveryFacts(value) {
60
82
  return {
61
83
  fileUploaded: value?.fileUploaded ?? false,
84
+ parserStarted: value?.parserStarted ?? false,
62
85
  billed: value?.billed ?? false,
63
86
  contentReleased: value?.contentReleased ?? false,
64
87
  };
@@ -66,6 +89,7 @@ function recoveryFacts(value) {
66
89
  function mergeRecoveryFacts(left, right) {
67
90
  return {
68
91
  fileUploaded: left.fileUploaded || right.fileUploaded === true,
92
+ parserStarted: left.parserStarted || right.parserStarted === true,
69
93
  billed: left.billed || right.billed === true,
70
94
  contentReleased: left.contentReleased || right.contentReleased === true,
71
95
  };
@@ -73,6 +97,7 @@ function mergeRecoveryFacts(left, right) {
73
97
  function errorWithRecoveryFacts(error, facts) {
74
98
  return bridgeError(error.code, error.message, {
75
99
  fileUploaded: error.fileUploaded || facts.fileUploaded,
100
+ parserStarted: error.parserStarted || facts.parserStarted,
76
101
  billed: error.billed || facts.billed,
77
102
  contentReleased: error.contentReleased || facts.contentReleased,
78
103
  retryable: error.retryable,
@@ -125,6 +150,154 @@ function safeInteger(value, minimum = 0) {
125
150
  && Number.isSafeInteger(value)
126
151
  && value >= minimum;
127
152
  }
153
+ function inputDirectProfile(input) {
154
+ return input.directProfile ?? null;
155
+ }
156
+ function isBillingProfile(profile) {
157
+ return profile === TEXT_BILLING_PROFILE || profile === GROUNDING_BILLING_PROFILE;
158
+ }
159
+ function operationStatusesFor(profile) {
160
+ return isBillingProfile(profile)
161
+ ? PHASE2_OPERATION_STATUSES
162
+ : LEGACY_OPERATION_STATUSES;
163
+ }
164
+ function terminalStatusesFor(profile) {
165
+ return isBillingProfile(profile)
166
+ ? PHASE2_TERMINAL_STATUSES
167
+ : LEGACY_TERMINAL_STATUSES;
168
+ }
169
+ function streamProtocolFor(profile) {
170
+ if (isBillingProfile(profile))
171
+ return STREAM_V3_PROTOCOL;
172
+ if (profile === LEGACY_GROUNDING_PROFILE)
173
+ return STREAM_V2_PROTOCOL;
174
+ return GRANTED_STREAM_PROTOCOL_VERSION;
175
+ }
176
+ function operationProtocolFor(profile) {
177
+ if (isBillingProfile(profile))
178
+ return OPERATION_V3_PROTOCOL;
179
+ if (profile === LEGACY_GROUNDING_PROFILE)
180
+ return OPERATION_V2_PROTOCOL;
181
+ return GRANTED_STREAM_PROTOCOL_VERSION;
182
+ }
183
+ function invalidPhase2Response() {
184
+ return bridgeError("IIIS_INVALID_RESPONSE", "IIIS returned invalid billing-aware operation data.");
185
+ }
186
+ const PHASE2_STATUS_REQUIRED_KEYS = [
187
+ "protocol_version",
188
+ "operation_id",
189
+ "status",
190
+ "parser_started",
191
+ "file_uploaded",
192
+ "billed",
193
+ "content_released",
194
+ "retryable",
195
+ "expires_at",
196
+ "profile",
197
+ "detail",
198
+ "approved_result_max_bytes",
199
+ ];
200
+ const PHASE2_RELEASE_KEYS = [
201
+ "type",
202
+ "protocol_version",
203
+ "operation_id",
204
+ "stream_protocol_version",
205
+ "profile",
206
+ "detail",
207
+ "approved_result_max_bytes",
208
+ "result",
209
+ "billing",
210
+ "billed",
211
+ "content_released",
212
+ ];
213
+ const PHASE2_PROGRESS_KEYS = [
214
+ "type",
215
+ "protocol_version",
216
+ "source",
217
+ "stage",
218
+ "done",
219
+ "total",
220
+ "message",
221
+ ];
222
+ const PHASE2_RESULT_CHUNK_KEYS = [
223
+ "type",
224
+ "protocol_version",
225
+ "operation_id",
226
+ "offset",
227
+ "decoded_bytes",
228
+ "data",
229
+ ];
230
+ const PHASE2_RESULT_COMPLETE_KEYS = [
231
+ "type",
232
+ "protocol_version",
233
+ "operation_id",
234
+ "result_bytes",
235
+ "result_digest",
236
+ ];
237
+ const PHASE2_ERROR_KEYS = [
238
+ "type",
239
+ "protocol_version",
240
+ "code",
241
+ "message",
242
+ "file_uploaded",
243
+ "billed",
244
+ "content_released",
245
+ "retryable",
246
+ ];
247
+ function hasExactKeys(value, required, optional = []) {
248
+ const allowed = new Set([...required, ...optional]);
249
+ return required.every((key) => Object.prototype.hasOwnProperty.call(value, key))
250
+ && Object.keys(value).every((key) => allowed.has(key));
251
+ }
252
+ function billingValue(value) {
253
+ if (!isRecord(value)
254
+ || typeof value.credits_charged !== "number"
255
+ || !Number.isFinite(value.credits_charged)
256
+ || value.credits_charged < 0
257
+ || typeof value.credits_remaining !== "number"
258
+ || !Number.isFinite(value.credits_remaining)
259
+ || value.credits_remaining < 0
260
+ || Object.keys(value).some((key) => key !== "credits_charged" && key !== "credits_remaining")) {
261
+ throw invalidPhase2Response();
262
+ }
263
+ return {
264
+ credits_charged: value.credits_charged,
265
+ credits_remaining: value.credits_remaining,
266
+ };
267
+ }
268
+ function profileDetail(profile, detail) {
269
+ return profile === TEXT_BILLING_PROFILE
270
+ ? detail === "text"
271
+ : detail === "grounded" || detail === "layout";
272
+ }
273
+ function expectedDetailFor(input, profile) {
274
+ const detail = input.expectedDetail;
275
+ if (!profileDetail(profile, detail))
276
+ throw invalidPhase2Response();
277
+ return detail;
278
+ }
279
+ function profileMaxBytes(profile) {
280
+ return profile === TEXT_BILLING_PROFILE ? MAX_FILE_BYTES : 67108864;
281
+ }
282
+ function profileMediaType(profile) {
283
+ return profile === TEXT_BILLING_PROFILE ? TEXT_MEDIA_TYPE : BUNDLE_MEDIA_TYPE;
284
+ }
285
+ function resultIdentityValue(value, profile) {
286
+ if (!isRecord(value)
287
+ || typeof value.digest !== "string"
288
+ || !SHA256_PATTERN.test(value.digest)
289
+ || !safeInteger(value.bytes)
290
+ || value.bytes > profileMaxBytes(profile)
291
+ || value.media_type !== profileMediaType(profile)
292
+ || Object.keys(value).some((key) => key !== "digest" && key !== "bytes" && key !== "media_type")) {
293
+ throw invalidPhase2Response();
294
+ }
295
+ return {
296
+ digest: value.digest,
297
+ bytes: value.bytes,
298
+ mediaType: value.media_type,
299
+ };
300
+ }
128
301
  function isRfc3339(value) {
129
302
  if (typeof value !== "string")
130
303
  return false;
@@ -141,8 +314,8 @@ function isRfc3339(value) {
141
314
  const days = month === 2 && leapYear ? 29 : monthDays[month - 1];
142
315
  return day <= days;
143
316
  }
144
- function validateProtocol(event) {
145
- if (event.protocol_version !== GRANTED_STREAM_PROTOCOL_VERSION) {
317
+ function validateProtocol(event, expectedProtocol = GRANTED_STREAM_PROTOCOL_VERSION) {
318
+ if (event.protocol_version !== expectedProtocol) {
146
319
  throw bridgeError("PROTOCOL_MISMATCH", "IIIS returned an unsupported protocol version.");
147
320
  }
148
321
  }
@@ -169,11 +342,31 @@ function stableProgressMessage(stage) {
169
342
  return "Finalizing result";
170
343
  return "Processing document";
171
344
  }
172
- function progressValue(event) {
173
- validateProtocol(event);
345
+ function progressValue(event, expectedProtocol = GRANTED_STREAM_PROTOCOL_VERSION, retained = false) {
346
+ if (expectedProtocol === STREAM_V3_PROTOCOL) {
347
+ const requiredKeys = retained
348
+ ? [...PHASE2_PROGRESS_KEYS, "sequence"]
349
+ : PHASE2_PROGRESS_KEYS;
350
+ if (!hasExactKeys(event, requiredKeys)
351
+ || event.type !== "progress"
352
+ || event.protocol_version !== STREAM_V3_PROTOCOL
353
+ || (event.source !== "bridge" && event.source !== "l1")
354
+ || typeof event.stage !== "string"
355
+ || event.stage.length < 1
356
+ || event.stage.length > 64
357
+ || typeof event.message !== "string"
358
+ || event.message.length < 1
359
+ || event.message.length > 256
360
+ || (retained && !safeInteger(event.sequence, 1))) {
361
+ throw invalidPhase2Response();
362
+ }
363
+ }
364
+ validateProtocol(event, expectedProtocol);
174
365
  if (!safeInteger(event.done) ||
175
366
  !safeInteger(event.total) ||
176
367
  event.done > event.total) {
368
+ if (expectedProtocol === STREAM_V3_PROTOCOL)
369
+ throw invalidPhase2Response();
177
370
  throw bridgeError("INVALID_PROGRESS_EVENT", "IIIS returned an invalid progress event.");
178
371
  }
179
372
  const unit = typeof event.unit === "string" &&
@@ -316,21 +509,65 @@ async function protocolJson(response, input, description) {
316
509
  throw bridgeError("PROTOCOL_MISMATCH", `IIIS returned invalid ${description} JSON.`);
317
510
  }
318
511
  }
319
- function operationStatusValue(value, operationId) {
512
+ function operationStatusValue(value, operationId, directProfile, expectedDetail) {
320
513
  if (!isRecord(value)
321
- || value.protocol_version !== GRANTED_STREAM_PROTOCOL_VERSION
514
+ || value.protocol_version !== operationProtocolFor(directProfile)
322
515
  || value.operation_id !== operationId
323
516
  || typeof value.status !== "string"
324
- || !OPERATION_STATUSES.has(value.status)
517
+ || !operationStatusesFor(directProfile).has(value.status)
325
518
  || typeof value.parser_started !== "boolean"
326
519
  || typeof value.file_uploaded !== "boolean"
327
520
  || typeof value.billed !== "boolean"
328
521
  || typeof value.content_released !== "boolean"
329
522
  || typeof value.retryable !== "boolean"
330
523
  || !(value.expires_at === null || isRfc3339(value.expires_at))) {
524
+ if (isBillingProfile(directProfile))
525
+ throw invalidPhase2Response();
331
526
  throw bridgeError("PROTOCOL_MISMATCH", "IIIS returned an invalid operation status.");
332
527
  }
333
- return value;
528
+ let billing = null;
529
+ let result = null;
530
+ if (isBillingProfile(directProfile)) {
531
+ if (!hasExactKeys(value, PHASE2_STATUS_REQUIRED_KEYS, ["result", "billing"])
532
+ || value.profile !== directProfile
533
+ || value.protocol_version !== OPERATION_V3_PROTOCOL
534
+ || !profileDetail(directProfile, expectedDetail)
535
+ || value.detail !== expectedDetail
536
+ || value.approved_result_max_bytes !== profileMaxBytes(directProfile)) {
537
+ throw invalidPhase2Response();
538
+ }
539
+ const settled = SETTLED_PHASE2_STATUSES.has(value.status);
540
+ if (settled) {
541
+ if (!value.billed
542
+ || !value.content_released
543
+ || !Object.prototype.hasOwnProperty.call(value, "result")
544
+ || !Object.prototype.hasOwnProperty.call(value, "billing")) {
545
+ throw invalidPhase2Response();
546
+ }
547
+ result = resultIdentityValue(value.result, directProfile);
548
+ billing = billingValue(value.billing);
549
+ }
550
+ else if (value.billed
551
+ || value.content_released
552
+ || Object.prototype.hasOwnProperty.call(value, "result")
553
+ || Object.prototype.hasOwnProperty.call(value, "billing")) {
554
+ throw invalidPhase2Response();
555
+ }
556
+ }
557
+ return {
558
+ protocol_version: value.protocol_version,
559
+ operation_id: value.operation_id,
560
+ status: value.status,
561
+ parser_started: value.parser_started,
562
+ file_uploaded: value.file_uploaded,
563
+ billed: value.billed,
564
+ content_released: value.content_released,
565
+ retryable: value.retryable,
566
+ expires_at: value.expires_at,
567
+ directProfile,
568
+ billing,
569
+ result,
570
+ };
334
571
  }
335
572
  function operationSnapshot(status) {
336
573
  return {
@@ -341,6 +578,8 @@ function operationSnapshot(status) {
341
578
  contentReleased: status.content_released,
342
579
  retryable: status.retryable,
343
580
  expiresAt: status.expires_at,
581
+ directProfile: status.directProfile,
582
+ billing: status.billing,
344
583
  };
345
584
  }
346
585
  async function* sseEvents(response) {
@@ -454,10 +693,16 @@ export class IiisClient {
454
693
  return this.inspectOperation(input);
455
694
  }
456
695
  const value = await protocolJson(response, input, "operation cancellation");
457
- return operationSnapshot(operationStatusValue(value, input.operationId));
696
+ return operationSnapshot(operationStatusValue(value, input.operationId, inputDirectProfile(input), input.expectedDetail));
458
697
  }
459
- async downloadResult(input, progress = NOOP_PROGRESS) {
698
+ async downloadResult(input, progress = NOOP_PROGRESS, settledStatus) {
460
699
  throwIfCanceled(input);
700
+ const directProfile = settledStatus?.directProfile ?? inputDirectProfile(input);
701
+ const billing = settledStatus?.billing ?? null;
702
+ const expectedResult = settledStatus?.result ?? null;
703
+ if (isBillingProfile(directProfile) && (billing === null || expectedResult === null)) {
704
+ throw invalidPhase2Response();
705
+ }
461
706
  const confirmedFacts = recoveryFacts({
462
707
  fileUploaded: true,
463
708
  billed: true,
@@ -501,15 +746,22 @@ export class IiisClient {
501
746
  throw errorWithRecoveryFacts(await errorFromResponse(response, input), confirmedFacts);
502
747
  }
503
748
  const declared = Number(response.headers.get("content-length"));
749
+ const mediaType = response.headers.get("content-type")
750
+ ?? "application/octet-stream";
504
751
  if (!safeInteger(declared)) {
505
752
  throw bridgeError("RESULT_INTEGRITY_FAILED", "The recovered result length is invalid.", {
506
753
  ...confirmedFacts,
507
754
  });
508
755
  }
756
+ if (expectedResult !== null
757
+ && (declared !== expectedResult.bytes
758
+ || mediaType !== expectedResult.mediaType)) {
759
+ throw invalidPhase2Response();
760
+ }
509
761
  const start = {
510
762
  operationId: input.operationId,
511
763
  resultBytes: declared,
512
- mediaType: response.headers.get("content-type") ?? "application/octet-stream",
764
+ mediaType,
513
765
  source: "recovery",
514
766
  };
515
767
  const hash = createHash("sha256");
@@ -549,9 +801,16 @@ export class IiisClient {
549
801
  ...confirmedFacts,
550
802
  });
551
803
  }
804
+ const resultDigest = `sha256:${hash.digest("hex")}`;
805
+ if (expectedResult !== null
806
+ && resultDigest !== expectedResult.digest) {
807
+ throw invalidPhase2Response();
808
+ }
552
809
  const metadata = {
553
810
  ...start,
554
- resultDigest: `sha256:${hash.digest("hex")}`,
811
+ resultDigest,
812
+ directProfile,
813
+ billing,
555
814
  };
556
815
  await input.retention.complete(metadata);
557
816
  throwIfCanceled(input);
@@ -644,6 +903,11 @@ export class IiisClient {
644
903
  async #consumeReleasedSse(input, response, progress) {
645
904
  let released;
646
905
  let completedMetadata;
906
+ const directProfile = inputDirectProfile(input);
907
+ const expectedProtocol = streamProtocolFor(directProfile);
908
+ const expectedDetail = isBillingProfile(directProfile)
909
+ ? expectedDetailFor(input, directProfile)
910
+ : undefined;
647
911
  let offset = 0;
648
912
  let complete = false;
649
913
  let streamFacts = recoveryFacts();
@@ -660,37 +924,84 @@ export class IiisClient {
660
924
  });
661
925
  }
662
926
  if (event.type === "progress") {
663
- const parsed = progressValue(event);
927
+ const parsed = progressValue(event, expectedProtocol);
664
928
  await progress.report(parserProgress(parsed.done, parsed.total), 100, parsed.message, parsed.detail);
665
929
  continue;
666
930
  }
667
931
  if (event.type === "error") {
932
+ if (isBillingProfile(directProfile)) {
933
+ if (!hasExactKeys(event, PHASE2_ERROR_KEYS)
934
+ || event.protocol_version !== STREAM_V3_PROTOCOL
935
+ || typeof event.code !== "string"
936
+ || !ERROR_CODE_PATTERN.test(event.code)
937
+ || typeof event.message !== "string"
938
+ || event.message.length < 1
939
+ || event.message.length > 512
940
+ || typeof event.file_uploaded !== "boolean"
941
+ || event.billed !== false
942
+ || event.content_released !== false
943
+ || typeof event.retryable !== "boolean") {
944
+ throw invalidPhase2Response();
945
+ }
946
+ }
668
947
  const code = stableErrorCode(event.code, "IIIS_PARSE_FAILED");
669
948
  throw bridgeError(code, stableErrorMessage(code), {
670
949
  fileUploaded: event.file_uploaded === true,
671
- billed: event.billed === true,
672
- contentReleased: event.content_released === true,
950
+ billed: isBillingProfile(directProfile)
951
+ ? false
952
+ : event.billed === true,
953
+ contentReleased: isBillingProfile(directProfile)
954
+ ? false
955
+ : event.content_released === true,
673
956
  retryable: event.retryable === true,
674
957
  });
675
958
  }
676
959
  if (event.type === "released") {
677
- validateProtocol(event);
960
+ if (isBillingProfile(directProfile)
961
+ && !hasExactKeys(event, PHASE2_RELEASE_KEYS)) {
962
+ throw invalidPhase2Response();
963
+ }
964
+ validateProtocol(event, expectedProtocol);
678
965
  if (released !== undefined
679
966
  || event.operation_id !== input.operationId
680
967
  || event.billed !== true
681
- || event.content_released !== true
682
- || !safeInteger(event.result_bytes)
683
- || typeof event.media_type !== "string"
684
- || event.media_type.length === 0
685
- || typeof event.result_digest !== "string"
686
- || !SHA256_PATTERN.test(event.result_digest)) {
968
+ || event.content_released !== true) {
969
+ if (isBillingProfile(directProfile))
970
+ throw invalidPhase2Response();
687
971
  throw bridgeError("INVALID_RELEASE_EVENT", "IIIS returned an invalid release event.");
688
972
  }
689
- released = {
690
- resultBytes: event.result_bytes,
691
- mediaType: event.media_type,
692
- resultDigest: event.result_digest,
693
- };
973
+ if (isBillingProfile(directProfile)) {
974
+ if (event.stream_protocol_version !== STREAM_V3_PROTOCOL
975
+ || event.profile !== directProfile
976
+ || event.detail !== expectedDetail
977
+ || event.approved_result_max_bytes !== profileMaxBytes(directProfile)) {
978
+ throw invalidPhase2Response();
979
+ }
980
+ const result = resultIdentityValue(event.result, directProfile);
981
+ released = {
982
+ resultBytes: result.bytes,
983
+ mediaType: result.mediaType,
984
+ resultDigest: result.digest,
985
+ directProfile,
986
+ billing: billingValue(event.billing),
987
+ };
988
+ }
989
+ else {
990
+ if (!safeInteger(event.result_bytes)
991
+ || typeof event.media_type !== "string"
992
+ || event.media_type.length === 0
993
+ || typeof event.result_digest !== "string"
994
+ || !SHA256_PATTERN.test(event.result_digest)) {
995
+ throw bridgeError("INVALID_RELEASE_EVENT", "IIIS returned an invalid release event.");
996
+ }
997
+ released = {
998
+ resultBytes: event.result_bytes,
999
+ mediaType: event.media_type,
1000
+ resultDigest: event.result_digest,
1001
+ directProfile,
1002
+ billing: null,
1003
+ };
1004
+ }
694
1005
  streamFacts = recoveryFacts({
695
1006
  fileUploaded: true,
696
1007
  billed: true,
@@ -705,7 +1016,11 @@ export class IiisClient {
705
1016
  continue;
706
1017
  }
707
1018
  if (event.type === "result_chunk") {
708
- validateProtocol(event);
1019
+ if (isBillingProfile(directProfile)
1020
+ && !hasExactKeys(event, PHASE2_RESULT_CHUNK_KEYS)) {
1021
+ throw invalidPhase2Response();
1022
+ }
1023
+ validateProtocol(event, expectedProtocol);
709
1024
  if (released === undefined) {
710
1025
  throw bridgeError("RESULT_BEFORE_RELEASE", "IIIS sent result bytes before billing release.");
711
1026
  }
@@ -734,7 +1049,11 @@ export class IiisClient {
734
1049
  continue;
735
1050
  }
736
1051
  if (event.type === "result_complete") {
737
- validateProtocol(event);
1052
+ if (isBillingProfile(directProfile)
1053
+ && !hasExactKeys(event, PHASE2_RESULT_COMPLETE_KEYS)) {
1054
+ throw invalidPhase2Response();
1055
+ }
1056
+ validateProtocol(event, expectedProtocol);
738
1057
  if (released === undefined
739
1058
  || event.operation_id !== input.operationId
740
1059
  || !safeInteger(event.result_bytes)
@@ -754,6 +1073,8 @@ export class IiisClient {
754
1073
  mediaType: released.mediaType,
755
1074
  resultDigest: released.resultDigest,
756
1075
  source: "sse",
1076
+ directProfile: released.directProfile,
1077
+ billing: released.billing,
757
1078
  };
758
1079
  await input.retention.complete(metadata);
759
1080
  throwIfCanceled(input);
@@ -761,6 +1082,8 @@ export class IiisClient {
761
1082
  complete = true;
762
1083
  continue;
763
1084
  }
1085
+ if (isBillingProfile(directProfile))
1086
+ throw invalidPhase2Response();
764
1087
  throw bridgeError("INVALID_SSE_EVENT", "IIIS returned an unsupported progress event.");
765
1088
  }
766
1089
  if (completedMetadata !== undefined) {
@@ -772,7 +1095,9 @@ export class IiisClient {
772
1095
  }
773
1096
  catch (error) {
774
1097
  const facts = error instanceof OmniBridgeError
775
- ? mergeRecoveryFacts(streamFacts, recoveryFacts(error))
1098
+ ? error.code === "IIIS_INVALID_RESPONSE"
1099
+ ? recoveryFacts(error)
1100
+ : mergeRecoveryFacts(streamFacts, recoveryFacts(error))
776
1101
  : streamFacts;
777
1102
  try {
778
1103
  await input.retention.abort();
@@ -808,7 +1133,7 @@ export class IiisClient {
808
1133
  if (!response.ok)
809
1134
  throw await errorFromResponse(response, input);
810
1135
  const value = await protocolJson(response, input, "operation status");
811
- return operationStatusValue(value, input.operationId);
1136
+ return operationStatusValue(value, input.operationId, inputDirectProfile(input), input.expectedDetail);
812
1137
  }
813
1138
  async #events(input, afterSequence) {
814
1139
  throwIfCanceled(input);
@@ -823,8 +1148,9 @@ export class IiisClient {
823
1148
  if (!response.ok)
824
1149
  throw await errorFromResponse(response, input);
825
1150
  const value = await protocolJson(response, input, "operation events");
1151
+ const expectedProtocol = streamProtocolFor(inputDirectProfile(input));
826
1152
  if (!isRecord(value)
827
- || value.protocol_version !== GRANTED_STREAM_PROTOCOL_VERSION
1153
+ || value.protocol_version !== expectedProtocol
828
1154
  || value.operation_id !== input.operationId
829
1155
  || typeof value.status !== "string"
830
1156
  || value.after_sequence !== afterSequence
@@ -839,7 +1165,7 @@ export class IiisClient {
839
1165
  if (!isRecord(raw) || raw.sequence !== expected || raw.type !== "progress") {
840
1166
  throw bridgeError("EVENT_CURSOR_EXPIRED", "IIIS returned a non-contiguous operation event page.");
841
1167
  }
842
- progressValue(raw);
1168
+ progressValue(raw, expectedProtocol, true);
843
1169
  events.push(raw);
844
1170
  expected += 1;
845
1171
  }
@@ -859,6 +1185,7 @@ export class IiisClient {
859
1185
  const status = await this.#status(input);
860
1186
  facts = mergeRecoveryFacts(facts, {
861
1187
  fileUploaded: status.file_uploaded,
1188
+ parserStarted: status.parser_started,
862
1189
  billed: status.billed,
863
1190
  contentReleased: status.content_released,
864
1191
  });
@@ -881,19 +1208,20 @@ export class IiisClient {
881
1208
  }
882
1209
  }
883
1210
  else if (status.status === "RELEASED") {
884
- return await this.downloadResult(input, progress);
1211
+ return await this.downloadResult(input, progress, status);
885
1212
  }
886
1213
  else if (ACTIVE_STATUSES.has(status.status)) {
887
1214
  const page = await this.#events(input, eventCursor);
888
1215
  for (const event of page.events) {
889
- const parsed = progressValue(event);
1216
+ const parsed = progressValue(event, streamProtocolFor(inputDirectProfile(input)), true);
890
1217
  await progress.report(parserProgress(parsed.done, parsed.total), 100, parsed.message, parsed.detail);
891
1218
  }
892
1219
  eventCursor = page.nextSequence;
893
1220
  }
894
- else if (TERMINAL_STATUSES.has(status.status)) {
1221
+ else if (terminalStatusesFor(inputDirectProfile(input)).has(status.status)) {
895
1222
  throw bridgeError(status.status, stableErrorMessage(status.status), {
896
1223
  fileUploaded: status.file_uploaded,
1224
+ parserStarted: status.parser_started,
897
1225
  billed: status.billed,
898
1226
  contentReleased: status.content_released,
899
1227
  retryable: status.retryable,
@@ -1,3 +1,4 @@
1
+ import type { DirectProfile } from "./capabilities.js";
1
2
  import { type RepresentationIntent } from "./protocol.js";
2
3
  export type JournalState = "CREATED" | "GRANT_PENDING" | "GRANT_ISSUED" | "UPLOADING" | "PROCESSING" | "RESULT_READY" | "ACK_PENDING" | "CLEANUP_PENDING" | "COMPLETED" | "FAILED" | "CANCELED" | "EXPIRED";
3
4
  export type JournalProgressUnit = "page" | "sheet" | "slide" | "frame" | "segment";
@@ -9,6 +10,14 @@ export interface JournalProgress {
9
10
  readonly completed: number;
10
11
  readonly total: number;
11
12
  }
13
+ export interface JournalBillingFacts {
14
+ readonly creditsCharged: number;
15
+ readonly creditsRemaining: number;
16
+ }
17
+ export interface JournalSettlementFacts {
18
+ readonly directProfile: DirectProfile | null;
19
+ readonly billing: JournalBillingFacts;
20
+ }
12
21
  export type JournalSourceKind = "local" | "url";
13
22
  export type JournalDetail = "text" | "grounded" | "layout";
14
23
  export type JournalGroundingSchemaVersion = "none" | "omni.grounding.v1";
@@ -24,6 +33,8 @@ export interface JournalRecord {
24
33
  readonly groundingSchemaVersion: JournalGroundingSchemaVersion;
25
34
  readonly bundleProtocolVersion: JournalBundleProtocolVersion;
26
35
  readonly resultDeliveryEffective: JournalResultDelivery;
36
+ readonly directProfile: DirectProfile | null;
37
+ readonly billing: JournalBillingFacts | null;
27
38
  readonly operationId: string | null;
28
39
  readonly operationToken: string | null;
29
40
  readonly uploadUrl: string | null;
@@ -91,6 +102,8 @@ export declare class OperationJournal {
91
102
  migrateLegacyRecord(clientRequestId: string, canonicalIdentityJson: string, sourceLocator?: string | null): Promise<JournalRecord | null>;
92
103
  transition(clientRequestId: string, expectedState: JournalState, nextState: JournalState, patch: JournalPatch): Promise<JournalRecord>;
93
104
  strengthenResultDelivery(clientRequestId: string, requested: JournalResultDelivery): Promise<JournalRecord>;
105
+ bindDirectProfile(clientRequestId: string, directProfile: DirectProfile): Promise<JournalRecord>;
106
+ bindSettlementFacts(clientRequestId: string, facts: JournalSettlementFacts): Promise<JournalRecord>;
94
107
  loadByRequestId(clientRequestId: string): Promise<JournalRecord | null>;
95
108
  loadByOperationId(operationId: string): Promise<JournalRecord | null>;
96
109
  loadLatestByRequestIdentityHmac(requestIdentityHmac: string): Promise<JournalRecord | null>;