@expandai/sdk 0.18.0 → 0.19.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@expandai/sdk",
3
3
  "type": "module",
4
- "version": "0.18.0",
4
+ "version": "0.19.0",
5
5
  "private": false,
6
6
  "description": "TypeScript SDK for ExpandAI",
7
7
  "author": "ExpandAI",
@@ -44,7 +44,8 @@
44
44
  "./*": {
45
45
  "types": "./build/dts/*.d.ts",
46
46
  "import": "./build/esm/*.js"
47
- }
47
+ },
48
+ "./internal/*": "./src/*.ts"
48
49
  },
49
50
  "scripts": {
50
51
  "generate": "tsx scripts/generate-client.ts",
package/src/FetchJson.ts CHANGED
@@ -14,29 +14,22 @@ import { decodeError, unexpectedStatus } from './HttpClientHelpers.js'
14
14
 
15
15
  /** Search snippet with the extracted `json` value preserved (statejson snippets only). */
16
16
  export const FetchSearchSnippetWithJson = S.Struct({
17
- type: S.optionalWith(S.Literal('text'), { nullable: true, default: () => 'text' as const }),
18
- source: S.Literal('markdown', 'appendix', 'statejson'),
19
- text: S.String,
17
+ ...Generated.FetchSearchResult.fields.snippets.value.fields,
20
18
  json: S.optionalWith(S.Unknown, { nullable: true }),
21
- score: S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1)),
22
- location: S.optionalWith(Generated.FetchSearchSnippetLocation, { nullable: true }),
23
19
  })
24
20
 
25
21
  export const FetchSearchResultWithJson = S.Struct({
26
- query: S.String,
22
+ ...Generated.FetchSearchResult.fields,
27
23
  snippets: S.Array(FetchSearchSnippetWithJson),
28
- meta: S.optionalWith(Generated.FetchSearchMeta, { nullable: true }),
29
24
  })
30
25
 
31
26
  export class FetchObjectModeResultWithJson extends S.Class<FetchObjectModeResultWithJson>(
32
27
  'FetchObjectModeResultWithJson',
33
28
  )({
34
- meta: Generated.FetchObjectModeMeta,
35
- markdown: S.String,
36
- json: S.Array(Generated.FetchObjectModeEvidence),
29
+ ...Generated.FetchObjectModeResult.fields,
37
30
  data: S.optionalWith(
38
31
  S.Struct({
39
- assets: S.optionalWith(S.Array(Generated.FetchAssetReference), { nullable: true }),
32
+ ...Generated.FetchObjectModeResult.fields.data.from.fields,
40
33
  search: S.optionalWith(FetchSearchResultWithJson, { nullable: true }),
41
34
  }),
42
35
  { nullable: true },
@@ -75,7 +68,10 @@ export const fetchObjectModeJson = (
75
68
  '413': (response) => decodeError(response, Generated.PayloadTooLarge),
76
69
  '429': (response) => decodeError(response, Generated.TooManyRequests),
77
70
  '500': (response) => decodeError(response, Generated.InternalError),
71
+ '502': (response) => decodeError(response, Generated.FetchNavigationFailed),
78
72
  '503': (response) => decodeError(response, Generated.ServiceUnavailable),
73
+ '504': (response) => decodeError(response, Generated.FetchCaptureTimeout),
74
+ '529': (response) => decodeError(response, Generated.FetchCapacityTimeout),
79
75
  orElse: (response) => unexpectedStatus(request, response),
80
76
  }),
81
77
  ),
package/src/Generated.ts CHANGED
@@ -186,6 +186,16 @@ export class InternalError extends S.Class<InternalError>('InternalError')({
186
186
  _tag: InternalErrorTag,
187
187
  }) {}
188
188
 
189
+ export class FetchNavigationFailedFailureType extends S.Literal('browserInternalDocument') {}
190
+
191
+ export class FetchNavigationFailedTag extends S.Literal('FetchNavigationFailed') {}
192
+
193
+ export class FetchNavigationFailed extends S.Class<FetchNavigationFailed>('FetchNavigationFailed')({
194
+ url: S.String,
195
+ failureType: FetchNavigationFailedFailureType,
196
+ _tag: FetchNavigationFailedTag,
197
+ }) {}
198
+
189
199
  export class ServiceUnavailableTag extends S.Literal('ServiceUnavailable') {}
190
200
 
191
201
  export class ServiceUnavailable extends S.Class<ServiceUnavailable>('ServiceUnavailable')({
@@ -194,6 +204,22 @@ export class ServiceUnavailable extends S.Class<ServiceUnavailable>('ServiceUnav
194
204
  _tag: ServiceUnavailableTag,
195
205
  }) {}
196
206
 
207
+ export class FetchCaptureTimeoutTag extends S.Literal('FetchCaptureTimeout') {}
208
+
209
+ export class FetchCaptureTimeout extends S.Class<FetchCaptureTimeout>('FetchCaptureTimeout')({
210
+ url: S.String,
211
+ timeoutMs: S.optionalWith(S.Number, { nullable: true }),
212
+ _tag: FetchCaptureTimeoutTag,
213
+ }) {}
214
+
215
+ export class FetchCapacityTimeoutTag extends S.Literal('FetchCapacityTimeout') {}
216
+
217
+ export class FetchCapacityTimeout extends S.Class<FetchCapacityTimeout>('FetchCapacityTimeout')({
218
+ url: S.String,
219
+ timeoutMs: S.optionalWith(S.Number, { nullable: true }),
220
+ _tag: FetchCapacityTimeoutTag,
221
+ }) {}
222
+
197
223
  export class FetchFetchJsonParams extends S.Struct({
198
224
  include: S.optionalWith(S.String, { nullable: true }),
199
225
  }) {}
@@ -276,7 +302,7 @@ export class FetchObjectModeStateJsonHint extends S.Struct({
276
302
 
277
303
  export class FetchObjectModeMarkdownHint extends S.Struct({
278
304
  url: S.String,
279
- moreTokens: S.Number,
305
+ moreTokens: S.optionalWith(S.Number, { nullable: true }),
280
306
  }) {}
281
307
 
282
308
  export class FetchObjectModeMeta extends S.Struct({
@@ -319,6 +345,10 @@ export class FetchObjectModeEvidenceRole extends S.Literal(
319
345
  'other',
320
346
  ) {}
321
347
 
348
+ export class FetchObjectModeEvidenceUrlRedacted extends S.Literal(true) {}
349
+
350
+ export class FetchObjectModeEvidenceUrlOmitted extends S.Literal('invalid', 'unsafe', 'too-long') {}
351
+
322
352
  export class FetchObjectModeEvidence extends S.Struct({
323
353
  id: S.Number,
324
354
  kind: FetchObjectModeEvidenceKind,
@@ -328,6 +358,8 @@ export class FetchObjectModeEvidence extends S.Struct({
328
358
  method: S.optionalWith(S.String, { nullable: true }),
329
359
  moreTokens: S.optionalWith(S.Number, { nullable: true }),
330
360
  keys: S.optionalWith(S.Array(S.String), { nullable: true }),
361
+ urlRedacted: S.optionalWith(FetchObjectModeEvidenceUrlRedacted, { nullable: true }),
362
+ urlOmitted: S.optionalWith(FetchObjectModeEvidenceUrlOmitted, { nullable: true }),
331
363
  }) {}
332
364
 
333
365
  export class FetchAssetReferenceKind extends S.Literal('data-url') {}
@@ -342,6 +374,20 @@ export class FetchAssetReference extends S.Struct({
342
374
  bytes: S.Number,
343
375
  }) {}
344
376
 
377
+ export class FetchDegradedInfoReason extends S.Literal('pageTooLarge', 'modelInputTooLarge') {}
378
+
379
+ export class FetchDegradedInfoLimitKind extends S.Literal('domsnapshot_bytes', 'ast_bytes', 'hfc_tokens') {}
380
+
381
+ export class FetchDegradedInfo extends S.Struct({
382
+ reason: FetchDegradedInfoReason,
383
+ skipped: S.Array(S.Literal('hfc')),
384
+ limitKind: FetchDegradedInfoLimitKind,
385
+ measuredBytes: S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0)), { nullable: true }),
386
+ thresholdBytes: S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0)), { nullable: true }),
387
+ measuredTokens: S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0)), { nullable: true }),
388
+ thresholdTokens: S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0)), { nullable: true }),
389
+ }) {}
390
+
345
391
  export class Int extends S.Int {}
346
392
 
347
393
  export class FetchSearchSnippetLocation extends S.Struct({
@@ -354,7 +400,7 @@ export class FetchSearchSnippetLocation extends S.Struct({
354
400
 
355
401
  export class FetchSearchSourceMetaSource extends S.Literal('markdown', 'appendix', 'statejson') {}
356
402
 
357
- export class FetchSearchSourceMetaRanker extends S.Literal('zeroentropy', 'local_lexical') {}
403
+ export class FetchSearchSourceMetaRanker extends S.Literal('zeroentropy', 'mixedbread', 'local_lexical') {}
358
404
 
359
405
  export class FetchSearchSourceMetaFallbackReason extends S.Literal('timeout', 'error') {}
360
406
 
@@ -394,6 +440,7 @@ export class FetchObjectModeResult extends S.Class<FetchObjectModeResult>('Fetch
394
440
  data: S.optionalWith(
395
441
  S.Struct({
396
442
  assets: S.optionalWith(S.Array(FetchAssetReference), { nullable: true }),
443
+ degraded: S.optionalWith(FetchDegradedInfo, { nullable: true }),
397
444
  search: S.optionalWith(FetchSearchResult, { nullable: true }),
398
445
  screenshot: S.optionalWith(
399
446
  S.Struct({
@@ -692,10 +739,14 @@ export class BatchedFetchItem extends S.Struct({
692
739
  appendix: S.optionalWith(S.String.pipe(S.minLength(1)), { nullable: true }),
693
740
  renderEngines: S.optionalWith(RenderEnginesInfo, { nullable: true }),
694
741
  assets: S.optionalWith(S.Array(FetchAssetReference), { nullable: true }),
742
+ degraded: S.optionalWith(FetchDegradedInfo, { nullable: true }),
695
743
  }),
696
744
  { nullable: true },
697
745
  ),
698
- error: S.optionalWith(S.Union(FetchBlocked, FetchPageTooLarge), { nullable: true }),
746
+ error: S.optionalWith(
747
+ S.Union(FetchBlocked, FetchPageTooLarge, FetchNavigationFailed, FetchCaptureTimeout, FetchCapacityTimeout),
748
+ { nullable: true },
749
+ ),
699
750
  }) {}
700
751
 
701
752
  export class PaginationMeta extends S.Struct({
@@ -782,7 +833,10 @@ export const make = (
782
833
  '413': (r) => decodeError(r, FetchFetch413),
783
834
  '429': (r) => decodeError(r, TooManyRequests),
784
835
  '500': (r) => decodeError(r, InternalError),
836
+ '502': (r) => decodeError(r, FetchNavigationFailed),
785
837
  '503': (r) => decodeError(r, ServiceUnavailable),
838
+ '504': (r) => decodeError(r, FetchCaptureTimeout),
839
+ '529': (r) => decodeError(r, FetchCapacityTimeout),
786
840
  orElse: (response) => unexpectedStatus(request, response),
787
841
  }),
788
842
  ),
@@ -805,7 +859,10 @@ export const make = (
805
859
  '413': (r) => decodeError(r, FetchFetchJson413),
806
860
  '429': (r) => decodeError(r, TooManyRequests),
807
861
  '500': (r) => decodeError(r, InternalError),
862
+ '502': (r) => decodeError(r, FetchNavigationFailed),
808
863
  '503': (r) => decodeError(r, ServiceUnavailable),
864
+ '504': (r) => decodeError(r, FetchCaptureTimeout),
865
+ '529': (r) => decodeError(r, FetchCapacityTimeout),
809
866
  orElse: (response) => unexpectedStatus(request, response),
810
867
  }),
811
868
  ),
@@ -924,7 +981,10 @@ export interface Client {
924
981
  | typeof FetchFetch413.Type
925
982
  | typeof TooManyRequests.Type
926
983
  | typeof InternalError.Type
984
+ | typeof FetchNavigationFailed.Type
927
985
  | typeof ServiceUnavailable.Type
986
+ | typeof FetchCaptureTimeout.Type
987
+ | typeof FetchCapacityTimeout.Type
928
988
  >
929
989
  readonly fetchFetchJson: (options: {
930
990
  readonly params: typeof FetchFetchJsonParams.Encoded
@@ -939,7 +999,10 @@ export interface Client {
939
999
  | typeof FetchFetchJson413.Type
940
1000
  | typeof TooManyRequests.Type
941
1001
  | typeof InternalError.Type
1002
+ | typeof FetchNavigationFailed.Type
942
1003
  | typeof ServiceUnavailable.Type
1004
+ | typeof FetchCaptureTimeout.Type
1005
+ | typeof FetchCapacityTimeout.Type
943
1006
  >
944
1007
  readonly fetchFetchSearch: (
945
1008
  options: typeof FetchFetchSearchRequest.Encoded,
package/src/Internal.ts CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  type RequestOptions,
28
28
  } from './Service.js'
29
29
 
30
- const RETRYABLE_STATUS_CODES = HashSet.make(408, 409, 429, 500, 502, 503, 504)
30
+ const RETRYABLE_STATUS_CODES = HashSet.make(408, 409, 429, 500, 502, 503, 504, 529)
31
31
 
32
32
  interface TaggedApiError {
33
33
  readonly _tag: string
@@ -57,7 +57,9 @@ function apiError(status: number, error: unknown): ExpandApiError {
57
57
  * its public error channel.
58
58
  */
59
59
  export function toExpandError(error: unknown, timeoutMs: number): ExpandError {
60
- return Match.value(error).pipe(
60
+ // Split across two `pipe` calls: the arm list is longer than `pipe`'s 20-argument overloads.
61
+ // Arms are evaluated top to bottom across both halves, so the ordering is unaffected.
62
+ const transportAndClientErrors = Match.value(error).pipe(
61
63
  Match.when(
62
64
  (error: unknown) => error instanceof Cause.TimeoutException,
63
65
  (error) => new ExpandTimeoutError({ message: `Request timed out after ${timeoutMs}ms`, timeoutMs, cause: error }),
@@ -109,6 +111,8 @@ export function toExpandError(error: unknown, timeoutMs: number): ExpandError {
109
111
  (error: unknown) => hasApiErrorTag(error, 'TooManyRequests'),
110
112
  (error) => apiError(429, error),
111
113
  ),
114
+ )
115
+ return transportAndClientErrors.pipe(
112
116
  Match.when(
113
117
  (error: unknown) => error instanceof Generated.InternalError,
114
118
  (error) => new ExpandApiError({ message: 'InternalError', status: 500, body: error, cause: error }),
@@ -118,13 +122,23 @@ export function toExpandError(error: unknown, timeoutMs: number): ExpandError {
118
122
  (error) => apiError(500, error),
119
123
  ),
120
124
  Match.when(
121
- (error: unknown) => error instanceof Generated.ServiceUnavailable,
122
- (error) => apiError(503, error),
125
+ (error: unknown) => hasApiErrorTag(error, 'FetchNavigationFailed'),
126
+ (error) => apiError(502, error),
123
127
  ),
128
+ // No `instanceof Generated.ServiceUnavailable` arm: decoded instances carry `_tag`, so the
129
+ // tag predicate already matches them and both would produce the same `apiError(503, ...)`.
124
130
  Match.when(
125
131
  (error: unknown) => hasApiErrorTag(error, 'ServiceUnavailable'),
126
132
  (error) => apiError(503, error),
127
133
  ),
134
+ Match.when(
135
+ (error: unknown) => hasApiErrorTag(error, 'FetchCaptureTimeout'),
136
+ (error) => apiError(504, error),
137
+ ),
138
+ Match.when(
139
+ (error: unknown) => hasApiErrorTag(error, 'FetchCapacityTimeout'),
140
+ (error) => apiError(529, error),
141
+ ),
128
142
  Match.when(
129
143
  (error: unknown) => error instanceof Generated.HttpApiDecodeError,
130
144
  (error) => new ExpandApiError({ message: 'HttpApiDecodeError', status: 400, body: error, cause: error }),
@@ -158,6 +172,19 @@ export function isRetryable(error: unknown, timeoutMs: number): boolean {
158
172
  if (hasApiErrorTag(error, 'BatchedIdempotencyConflict') || hasApiErrorTag(error, 'BatchedCancellationConflict')) {
159
173
  return false
160
174
  }
175
+ // A 502 FetchNavigationFailed is a deterministic verdict about the target page (the browser
176
+ // committed an internal error document), not a transient gateway failure — retrying re-runs a
177
+ // full capture for the same outcome.
178
+ if (hasApiErrorTag(error, 'FetchNavigationFailed')) {
179
+ return false
180
+ }
181
+ // A typed 504 FetchCaptureTimeout is the conservative fallback when no result was publishable by
182
+ // the request deadline and queue-only expiry was not proven. Automatic retry remains disabled so
183
+ // ambiguous timeouts do not amplify a control-plane incident. A deliberate retry may still be
184
+ // appropriate; a bare intermediary 504 stays on the normal retryable-status path.
185
+ if (hasApiErrorTag(error, 'FetchCaptureTimeout')) {
186
+ return false
187
+ }
161
188
  // An undecodable response (a transient 5xx with an empty/garbled body, a proxy error page, etc.)
162
189
  // surfaces as a bare ParseError with no HTTP status, so the status-based classification below can't
163
190
  // see the 5xx and the request would fail un-retried. Treat decode failures as retryable — the
package/src/Service.ts CHANGED
@@ -83,6 +83,21 @@ export type BatchedParams = typeof Generated.FetchBatchedRequest.Encoded
83
83
  export type BatchedResponse = typeof Generated.BatchedRun.Type
84
84
  export interface BatchedRequestOptions extends RequestOptions {
85
85
  readonly idempotencyKey?: string
86
+ /**
87
+ * Body fields the public contract does not declare, merged verbatim into the
88
+ * request payload.
89
+ *
90
+ * The API accepts a small number of options that are deliberately absent from
91
+ * the published OpenAPI document and refused for any organization not on the
92
+ * server's internal allowlist. Because the generated request schema is built
93
+ * from that same public document, it drops undeclared keys on encode — so an
94
+ * internal caller passing one through `params` would get a request that looks
95
+ * correct and carries nothing. This is the supported way to send one.
96
+ *
97
+ * Not for application code: anything here is unversioned, unvalidated by the
98
+ * SDK, and rejected by the API unless the calling organization is allowlisted.
99
+ */
100
+ readonly internalOverrides?: Readonly<Record<string, unknown>>
86
101
  }
87
102
  export type GetBatchedParams = typeof Generated.FetchGetBatchedParams.Encoded
88
103
  export type GetBatchedResponse = typeof Generated.BatchedStatus.Type
@@ -163,7 +178,10 @@ export class ExpandService extends Effect.Service<ExpandService>()('@expandai/sd
163
178
  '413': (response) => decodeError(response, Generated.PayloadTooLarge),
164
179
  '429': (response) => decodeError(response, Generated.TooManyRequests),
165
180
  '500': (response) => decodeError(response, Generated.InternalError),
181
+ '502': (response) => decodeError(response, Generated.FetchNavigationFailed),
166
182
  '503': (response) => decodeError(response, Generated.ServiceUnavailable),
183
+ '504': (response) => decodeError(response, Generated.FetchCaptureTimeout),
184
+ '529': (response) => decodeError(response, Generated.FetchCapacityTimeout),
167
185
  orElse: (response) => unexpectedStatus(request, response),
168
186
  }),
169
187
  ),
@@ -238,9 +256,30 @@ export class ExpandService extends Effect.Service<ExpandService>()('@expandai/sd
238
256
  ),
239
257
  }),
240
258
  )
259
+ // Merged after the generated payload because the generated schema drops any
260
+ // key it does not declare — silently, which is the dangerous part. See
261
+ // `BatchedRequestOptions.internalOverrides` for why it is not declared.
262
+ //
263
+ // A collision is refused rather than resolved: this hatch exists to add
264
+ // undeclared fields, and letting it win over `urls` or `include` would
265
+ // mean a request could quietly fetch something other than what the caller
266
+ // passed.
267
+ const overrides = requestOptions.internalOverrides ?? {}
268
+ // Checked against the schema's declared fields, not against the keys this
269
+ // particular call happened to set: an omitted-but-declared field like
270
+ // `include` could otherwise be supplied here and bypass the generated
271
+ // contract entirely.
272
+ const collisions = Object.keys(overrides).filter((key) =>
273
+ Object.hasOwn(Generated.FetchBatchedRequest.fields, key),
274
+ )
275
+ if (collisions.length > 0) {
276
+ return yield* new ExpandSdkError({
277
+ message: `internalOverrides may not replace declared request fields: ${collisions.join(', ')}`,
278
+ })
279
+ }
241
280
  const generatedRequest: Parameters<typeof generated.fetchBatched>[0] = {
242
281
  params: { 'x-idempotency-key': idempotencyKey },
243
- payload: params,
282
+ payload: { ...params, ...overrides },
244
283
  }
245
284
  return yield* withRetries(generated.fetchBatched(generatedRequest), retryOptions)
246
285
  })