@ai-sdk/provider-utils 5.0.46 → 5.0.49

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/provider-utils",
3
- "version": "5.0.46",
3
+ "version": "5.0.49",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -4,7 +4,7 @@ export function combineHeaders(
4
4
  return headers.reduce(
5
5
  (combinedHeaders, currentHeaders) => ({
6
6
  ...combinedHeaders,
7
- ...(currentHeaders ?? {}),
7
+ ...currentHeaders,
8
8
  }),
9
9
  {},
10
10
  ) as Record<string, string | undefined>;
package/src/delay.ts CHANGED
@@ -12,7 +12,7 @@ export async function delay(
12
12
  },
13
13
  ): Promise<void> {
14
14
  if (delayInMs == null) {
15
- return Promise.resolve();
15
+ return;
16
16
  }
17
17
 
18
18
  const signal = options?.abortSignal;
@@ -299,8 +299,6 @@ function detectMediaTypeBySignatures<T extends MediaTypeSignatures>({
299
299
  return signature.mediaType;
300
300
  }
301
301
  }
302
-
303
- return undefined;
304
302
  }
305
303
 
306
304
  const topLevelSignatureTables = {
@@ -1,6 +1,6 @@
1
1
  import { cancelResponseBody } from './cancel-response-body';
2
2
  import { DownloadError } from './download-error';
3
- import { fetchWithValidatedRedirects } from './fetch-with-validated-redirects';
3
+ import { fetchUntrustedUrl } from './fetch-untrusted-url';
4
4
  import {
5
5
  readResponseWithSizeLimit,
6
6
  DEFAULT_MAX_DOWNLOAD_SIZE,
@@ -22,7 +22,7 @@ export async function downloadBlob(
22
22
  options?: { maxBytes?: number; abortSignal?: AbortSignal },
23
23
  ): Promise<Blob> {
24
24
  try {
25
- const response = await fetchWithValidatedRedirects({
25
+ const response = await fetchUntrustedUrl({
26
26
  url,
27
27
  abortSignal: options?.abortSignal,
28
28
  });
@@ -1,3 +1,5 @@
1
+ import type { SharedV4ProviderOptions } from '@ai-sdk/provider';
2
+
1
3
  /**
2
4
  * Symbol for exposing the UTF-8 input byte budget of an embedding model.
3
5
  *
@@ -7,3 +9,24 @@
7
9
  export const EMBEDDING_MODEL_MAX_INPUT_BYTES_PER_CALL = Symbol.for(
8
10
  'vercel.ai.embeddingModel.maxInputBytesPerCall',
9
11
  );
12
+
13
+ /**
14
+ * Symbol for transforming provider options for an automatically batched
15
+ * embedding model call.
16
+ *
17
+ * This capability is experimental and intentionally lives outside the versioned
18
+ * embedding model specification.
19
+ */
20
+ export const EMBEDDING_MODEL_PROVIDER_OPTIONS_TRANSFORMER = Symbol.for(
21
+ 'vercel.ai.embeddingModel.providerOptionsTransformer',
22
+ );
23
+
24
+ export type EmbeddingModelProviderOptionsTransformer = (options: {
25
+ providerOptions: SharedV4ProviderOptions | undefined;
26
+ values: Array<string>;
27
+ startIndex: number;
28
+ endIndex: number;
29
+ }) =>
30
+ | SharedV4ProviderOptions
31
+ | undefined
32
+ | PromiseLike<SharedV4ProviderOptions | undefined>;
@@ -0,0 +1,86 @@
1
+ import { fetchWithValidatedRedirects } from './fetch-with-validated-redirects';
2
+ import { isSameOrigin } from './is-same-origin';
3
+ import { sanitizeRequestHeaders } from './sanitize-request-headers';
4
+
5
+ // Providers can use arbitrary credential header names. Only established
6
+ // non-credential request metadata is safe to forward without an origin assertion.
7
+ const SAFE_UNTRUSTED_FIRST_HOP_HEADERS = new Set([
8
+ 'accept',
9
+ 'accept-language',
10
+ 'baggage',
11
+ 'cache-control',
12
+ 'idempotency-key',
13
+ 'if-match',
14
+ 'if-modified-since',
15
+ 'if-none-match',
16
+ 'if-range',
17
+ 'if-unmodified-since',
18
+ 'pragma',
19
+ 'range',
20
+ 'traceparent',
21
+ 'tracestate',
22
+ 'user-agent',
23
+ 'x-correlation-id',
24
+ 'x-request-id',
25
+ ]);
26
+
27
+ /**
28
+ * Fetches an untrusted URL with first-hop credential isolation and validated
29
+ * redirects. Uses the URL validation, DNS-pinned Node.js transport, redirect
30
+ * limits, and cross-origin header stripping of {@link fetchWithValidatedRedirects}.
31
+ * An injected fetch must provide equivalent connect-time DNS validation.
32
+ *
33
+ * Without a matching `credentialedOrigin` (or `trustedOrigin` when it is
34
+ * omitted), only allowlisted request metadata is sent on the first hop.
35
+ * Arbitrary caller headers require an explicit matching origin because vendor
36
+ * credential names cannot be inferred safely. Proxy, metadata, cookie, and
37
+ * hop-by-hop headers are sanitized even for a matching origin.
38
+ *
39
+ * `trustedOrigin` also exempts same-origin hops from URL validation, allowing
40
+ * developer-configured private endpoints. Both origin options must come from
41
+ * developer configuration, never from untrusted response data.
42
+ *
43
+ * This is an opt-in alternative to `fetchWithValidatedRedirects`, whose
44
+ * existing first-hop header behavior is preserved for compatibility.
45
+ */
46
+ export async function fetchUntrustedUrl({
47
+ headers,
48
+ credentialedOrigin,
49
+ untrustedFirstHopHeaders,
50
+ ...options
51
+ }: Parameters<typeof fetchWithValidatedRedirects>[0] & {
52
+ /**
53
+ * The developer-configured origin allowed to receive arbitrary caller
54
+ * headers on the first hop. Defaults to `trustedOrigin` when omitted.
55
+ * An explicit value takes precedence over `trustedOrigin` and does not
56
+ * exempt the URL from validation.
57
+ */
58
+ credentialedOrigin?: string;
59
+ /**
60
+ * Additional sanitized header names safe to disclose to an untrusted first
61
+ * hop. Use only for non-credential protocol metadata. Credentials require
62
+ * a matching `credentialedOrigin` instead. Names are case-insensitive.
63
+ */
64
+ untrustedFirstHopHeaders?: readonly string[];
65
+ }): Promise<Response> {
66
+ let firstHopHeaders: Headers | undefined;
67
+ if (headers !== undefined) {
68
+ firstHopHeaders = sanitizeRequestHeaders(headers);
69
+ const origin = credentialedOrigin ?? options.trustedOrigin;
70
+
71
+ if (origin === undefined || !isSameOrigin(options.url, origin)) {
72
+ const allowedHeaders = new Set([
73
+ ...SAFE_UNTRUSTED_FIRST_HOP_HEADERS,
74
+ ...(untrustedFirstHopHeaders ?? []).map(name => name.toLowerCase()),
75
+ ]);
76
+ firstHopHeaders = new Headers(
77
+ [...firstHopHeaders].filter(([name]) => allowedHeaders.has(name)),
78
+ );
79
+ }
80
+ }
81
+
82
+ return fetchWithValidatedRedirects({
83
+ ...options,
84
+ headers: firstHopHeaders,
85
+ });
86
+ }
@@ -83,6 +83,9 @@ export async function fetchWithValidatedEndpoint({
83
83
  * Request headers are also protected: {@link sanitizeRequestHeaders} strips
84
84
  * proxy/metadata/cookie/hop-by-hop headers before the first request, and all
85
85
  * caller headers except `User-Agent` are dropped on a cross-origin redirect.
86
+ * Credentials and custom headers are preserved on the first hop for backwards
87
+ * compatibility. The caller must ensure that the initial URL may receive them.
88
+ * Use `fetchUntrustedUrl` for URLs that require first-hop credential isolation.
86
89
  * The fetch spec only strips `Authorization` on cross-origin redirects because
87
90
  * in a browser, CORS preflighting protects custom headers; there is no CORS on
88
91
  * the server, so provider API keys carried in custom headers (e.g. `x-key`)
@@ -36,8 +36,6 @@ function findNetworkError(
36
36
 
37
37
  current = (current as Error & { cause?: unknown }).cause;
38
38
  }
39
-
40
- return undefined;
41
39
  }
42
40
 
43
41
  export function handleFetchError({
package/src/index.ts CHANGED
@@ -30,7 +30,11 @@ export {
30
30
  } from './detect-media-type';
31
31
  export { downloadBlob } from './download-blob';
32
32
  export { DownloadError } from './download-error';
33
- export { EMBEDDING_MODEL_MAX_INPUT_BYTES_PER_CALL as EXPERIMENTAL_EMBEDDING_MODEL_MAX_INPUT_BYTES_PER_CALL } from './embedding-model-capabilities';
33
+ export {
34
+ EMBEDDING_MODEL_MAX_INPUT_BYTES_PER_CALL as EXPERIMENTAL_EMBEDDING_MODEL_MAX_INPUT_BYTES_PER_CALL,
35
+ EMBEDDING_MODEL_PROVIDER_OPTIONS_TRANSFORMER as EXPERIMENTAL_EMBEDDING_MODEL_PROVIDER_OPTIONS_TRANSFORMER,
36
+ type EmbeddingModelProviderOptionsTransformer,
37
+ } from './embedding-model-capabilities';
34
38
  export {
35
39
  fetchWithValidatedEndpoint,
36
40
  fetchWithValidatedRedirects,
@@ -38,6 +42,7 @@ export {
38
42
  export { extractLines } from './extract-lines';
39
43
  export * from './extract-response-headers';
40
44
  export * from './fetch-function';
45
+ export { fetchUntrustedUrl } from './fetch-untrusted-url';
41
46
  export { filterNullable } from './filter-nullable';
42
47
  export { createIdGenerator, generateId, type IdGenerator } from './generate-id';
43
48
  export * from './get-error-message';
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Checks if the given URL is supported natively by the model.
3
3
  *
4
- * @param mediaType - The media type of the URL. Case-sensitive. May be a full
4
+ * @param mediaType - The media type of the URL. Case-insensitive. May be a full
5
5
  * `type/subtype`, a wildcard `type/*`, or just the
6
6
  * top-level segment (e.g. `image`).
7
7
  * @param url - The URL to check.
8
- * @param supportedUrls - A record where keys are case-sensitive media types (or '*')
8
+ * @param supportedUrls - A record where keys are case-insensitive media types (or '*')
9
9
  * and values are arrays of RegExp patterns for URLs.
10
10
  *
11
11
  * @returns `true` if the URL matches a pattern under the specific media type
@@ -46,7 +46,9 @@ export function isUrlSupported({
46
46
  if (isTopLevelOnly) {
47
47
  return `${mediaType}/` === mediaTypePrefix;
48
48
  }
49
- return mediaType.startsWith(mediaTypePrefix);
49
+ return mediaTypePrefix.endsWith('/')
50
+ ? mediaType.startsWith(mediaTypePrefix)
51
+ : mediaType === mediaTypePrefix;
50
52
  })
51
53
  .flatMap(({ regexes }) => regexes)
52
54
  // check if any pattern matches the url:
@@ -31,8 +31,6 @@ export function normalizeBatchRequestCounts({
31
31
  failed,
32
32
  };
33
33
  }
34
-
35
- return undefined;
36
34
  }
37
35
 
38
36
  function isNonNegativeSafeInteger(
package/src/resolve.ts CHANGED
@@ -28,5 +28,5 @@ export async function resolve<T>(value: Resolvable<T>): Promise<T> {
28
28
  }
29
29
 
30
30
  // Otherwise just resolve whatever we got (value or promise)
31
- return Promise.resolve(value as T);
31
+ return value as T;
32
32
  }
@@ -4,9 +4,11 @@
4
4
  * transport headers (RFC 7230 §6.1).
5
5
  *
6
6
  * `Authorization` and other credential-bearing caller headers (e.g. `x-key`)
7
- * are intentionally not listed — they're needed on the first hop of some
8
- * provider polling calls. Instead, all caller headers except the user-agent are
9
- * dropped on a cross-origin redirect (see `fetch-with-validated-redirects`).
7
+ * are intentionally not listed because trusted provider requests may need
8
+ * them. `fetchUntrustedUrl` separately restricts an untrusted first hop to an
9
+ * explicit allowlist of non-credential request metadata. Both it and
10
+ * `fetchWithValidatedRedirects` drop all caller headers except the user-agent
11
+ * on a cross-origin redirect.
10
12
  */
11
13
  const BLOCKED_REQUEST_HEADERS: readonly string[] = [
12
14
  // Hop-by-hop / transport (RFC 7230 §6.1)
@@ -20,7 +20,7 @@ export const zodPatterns = {
20
20
  * `a-z` was added to replicate /i flag
21
21
  */
22
22
  email:
23
- /^(?!\.)(?!.*\.\.)([a-zA-Z0-9_'+\-\.]*)[a-zA-Z0-9_+-]@([a-zA-Z0-9][a-zA-Z0-9\-]*\.)+[a-zA-Z]{2,}$/,
23
+ /^(?!\.)(?!.*\.\.)([a-zA-Z0-9_'+\-.]*)[a-zA-Z0-9_+-]@([a-zA-Z0-9][a-zA-Z0-9-]*\.)+[a-zA-Z]{2,}$/,
24
24
  /**
25
25
  * Constructed a valid Unicode RegExp
26
26
  *
@@ -31,4 +31,12 @@ export type ToolApprovalRequest = {
31
31
  * Present only when `experimental_toolApprovalSecret` is configured.
32
32
  */
33
33
  signature?: string;
34
+
35
+ /**
36
+ * Tool input before input schema validation and transformation.
37
+ *
38
+ * This is included when it differs from the validated tool input so that
39
+ * approved tool calls can be safely revalidated before execution.
40
+ */
41
+ inputSchemaInput?: unknown;
34
42
  };
@@ -78,7 +78,6 @@ export function validateDownloadUrl(url: string): void {
78
78
  message: `URL with IP address ${hostname} is not allowed`,
79
79
  });
80
80
  }
81
- return;
82
81
  }
83
82
  }
84
83