ai 7.0.114 → 7.0.116

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.
@@ -82,53 +82,53 @@ Define the `HarnessAgent` on the server:
82
82
  ```ts filename='app/api/chat/agent.ts'
83
83
  import { HarnessAgent } from '@ai-sdk/harness/agent';
84
84
  import { claudeCode } from '@ai-sdk/harness-claude-code';
85
- import { createVercelSandbox } from '@ai-sdk/sandbox-vercel';
86
85
 
87
86
  export const agent = new HarnessAgent({
88
87
  harness: claudeCode,
89
- sandbox: createVercelSandbox({
90
- runtime: 'node24',
91
- ports: [4000],
92
- }),
93
88
  instructions: 'You are a helpful coding assistant.',
94
89
  });
95
90
  ```
96
91
 
97
92
  ## Session Store
98
93
 
99
- Persist only the opaque resume state returned by `session.detach()`. If the
94
+ Persist the opaque resume state returned by `session.detach()`. If the
100
95
  turn paused for approval or was otherwise interrupted, that resume state carries
101
96
  the continuation state internally. The chat id can also be the harness
102
- `sessionId`, which gives the sandbox a stable identity across requests and
103
- processes.
97
+ `sessionId`. Here we derive a stable `sandboxId` from that known chat id. If
98
+ you do not derive one, persist the returned `sandboxSession.id` separately.
104
99
 
105
100
  ```ts filename='app/api/chat/session-store.ts'
106
- import type {
107
- HarnessAgentResumeSessionState,
108
- HarnessAgentSession,
109
- } from '@ai-sdk/harness/agent';
101
+ import type { HarnessAgentResumeSessionState } from '@ai-sdk/harness/agent';
102
+ import {
103
+ createVercelNetworkSandboxSession,
104
+ resumeVercelNetworkSandboxSession,
105
+ } from '@ai-sdk/sandbox-vercel';
110
106
 
111
107
  const states: Record<string, HarnessAgentResumeSessionState | undefined> = {};
112
108
 
113
- type SessionFactory = {
114
- createSession(options?: {
115
- sessionId?: string;
116
- resumeFrom?: HarnessAgentResumeSessionState;
117
- }): Promise<HarnessAgentSession>;
118
- };
119
-
120
109
  export async function resumeOrCreateSession({
121
110
  agent,
122
111
  chatId,
123
112
  }: {
124
- agent: SessionFactory;
113
+ agent: typeof import('./agent').agent;
125
114
  chatId: string;
126
115
  }) {
127
116
  const resumeFrom = states[chatId];
128
-
129
- return agent.createSession(
130
- resumeFrom ? { sessionId: chatId, resumeFrom } : { sessionId: chatId },
131
- );
117
+ const sandboxId = `harness-${chatId}`;
118
+ const sandboxSession = resumeFrom
119
+ ? await resumeVercelNetworkSandboxSession({ sandboxId })
120
+ : await createVercelNetworkSandboxSession({
121
+ sandboxId,
122
+ runtime: 'node24',
123
+ ports: [4000],
124
+ template: await agent.getSandboxTemplate(),
125
+ });
126
+ const session = await agent.createSession({
127
+ sessionId: chatId,
128
+ resumeFrom,
129
+ sandboxSession,
130
+ });
131
+ return { session, sandboxSession };
132
132
  }
133
133
 
134
134
  export async function detachAndPersist({
@@ -136,13 +136,15 @@ export async function detachAndPersist({
136
136
  session,
137
137
  }: {
138
138
  chatId: string;
139
- session: HarnessAgentSession;
139
+ session: Awaited<ReturnType<typeof resumeOrCreateSession>>['session'];
140
140
  }) {
141
141
  states[chatId] = await session.detach();
142
142
  }
143
143
  ```
144
144
 
145
- Use durable storage instead of an in-memory map in production.
145
+ Use durable storage instead of an in-memory map in production. The creator
146
+ always creates a new sandbox and fails on a conflicting `sandboxId`; only the
147
+ resume function reattaches an existing one.
146
148
 
147
149
  ## Route
148
150
 
@@ -177,7 +179,7 @@ export async function POST(request: Request) {
177
179
  return createUIMessageStreamResponse({
178
180
  stream: createUIMessageStream({
179
181
  execute: async ({ writer }) => {
180
- const session = await resumeOrCreateSession({ agent, chatId });
182
+ const { session } = await resumeOrCreateSession({ agent, chatId });
181
183
  const result = await agent.stream({ session, messages });
182
184
 
183
185
  writer.merge(
@@ -212,9 +214,10 @@ sandbox warm for the next request. Bridge-backed adapters can usually reattach
212
214
  or replay efficiently. If the turn is unfinished, `detach()` includes the turn
213
215
  continuation state in the returned resume state.
214
216
 
215
- Use `session.stop()` when you want to save resume state and stop the runtime and
216
- sandbox after each turn. The next request resumes from persisted state and
217
- continues any unfinished turn before accepting a new prompt.
217
+ `session.stop()` saves resume state and stops the harness runtime, but does not
218
+ stop a supplied sandbox. The caller owns that network session and should call
219
+ its `destroy()` method when the chat ends, not while a later request needs to
220
+ reattach. Preserve the chosen `sandboxId` across processes.
218
221
 
219
222
  ## Rendering Harness Parts
220
223
 
@@ -19,15 +19,11 @@ session for the lifetime of the terminal UI.
19
19
  ```ts filename='tui.ts'
20
20
  import { HarnessAgent, type HarnessAgentSession } from '@ai-sdk/harness/agent';
21
21
  import { codex } from '@ai-sdk/harness-codex';
22
- import { createVercelSandbox } from '@ai-sdk/sandbox-vercel';
22
+ import { createVercelNetworkSandboxSession } from '@ai-sdk/sandbox-vercel';
23
23
  import { runAgentTUI, type AgentTUIAgent } from '@ai-sdk/tui';
24
24
 
25
25
  const agent = new HarnessAgent({
26
26
  harness: codex,
27
- sandbox: createVercelSandbox({
28
- runtime: 'node24',
29
- ports: [4000],
30
- }),
31
27
  });
32
28
 
33
29
  function createTUIAgent({
@@ -56,7 +52,12 @@ function createTUIAgent({
56
52
  } as AgentTUIAgent;
57
53
  }
58
54
 
59
- const session = await agent.createSession();
55
+ const sandboxSession = await createVercelNetworkSandboxSession({
56
+ runtime: 'node24',
57
+ ports: [4000],
58
+ template: await agent.getSandboxTemplate(),
59
+ });
60
+ const session = await agent.createSession({ sandboxSession });
60
61
 
61
62
  try {
62
63
  await runAgentTUI({
@@ -67,6 +68,7 @@ try {
67
68
  });
68
69
  } finally {
69
70
  await session.destroy();
71
+ await sandboxSession.destroy();
70
72
  }
71
73
  ```
72
74
 
@@ -45,6 +45,45 @@ When the SDK fetches a URL taken from a provider response, it:
45
45
 
46
46
  A blocked URL surfaces as a `DownloadError`.
47
47
 
48
+ ## Fetching untrusted URLs directly
49
+
50
+ For custom integrations using `@ai-sdk/provider-utils`, use `fetchUntrustedUrl`
51
+ when the URL comes from a response or other untrusted input. It validates URLs
52
+ and redirects and withholds credentials and unknown custom headers on the first
53
+ request unless the URL matches a configured origin:
54
+
55
+ ```ts
56
+ import { fetchUntrustedUrl } from '@ai-sdk/provider-utils';
57
+
58
+ const response = await fetchUntrustedUrl({
59
+ url: result.downloadUrl,
60
+ headers: {
61
+ authorization: `Bearer ${apiKey}`,
62
+ accept: 'image/*',
63
+ },
64
+ credentialedOrigin: providerBaseURL,
65
+ });
66
+ ```
67
+
68
+ Here, `Authorization` is sent only when the initial URL has the same origin as
69
+ `providerBaseURL`. Otherwise, only allowlisted metadata such as `Accept`,
70
+ `Range`, and `User-Agent` is sent. A cross-origin redirect drops all caller
71
+ headers except `User-Agent`, and credentials are never reattached on later hops.
72
+
73
+ `credentialedOrigin` must come from your configuration, never from the response
74
+ URL. It does not disable URL validation. For a configured private endpoint, pass
75
+ `trustedOrigin` as well; if `credentialedOrigin` is omitted, `fetchUntrustedUrl`
76
+ uses `trustedOrigin` as the origin allowed to receive credentials. Extra protocol
77
+ metadata can be allowed with `untrustedFirstHopHeaders: ['x-protocol-version']`.
78
+ Only list headers that are safe to disclose to any destination; use a configured
79
+ origin for credentials.
80
+
81
+ The existing `fetchWithValidatedRedirects` helper preserves its first-request
82
+ behavior: it sanitizes risky headers but still forwards authentication and
83
+ custom headers. Switch to `fetchUntrustedUrl` to opt into first-request
84
+ credential isolation. Existing calls to `getFromApi` also retain their behavior;
85
+ pass `credentialedOrigin` when fetching response-supplied URLs with credentials.
86
+
48
87
  ## Self-hosted and local endpoints
49
88
 
50
89
  URLs that are same-origin with the provider endpoint **you configured** (e.g. a
@@ -191,17 +191,21 @@ await runAgentTUI({
191
191
  ## Example with Sandbox
192
192
 
193
193
  ```ts
194
- import { createJustBashSandbox } from '@ai-sdk/sandbox-just-bash';
194
+ import { createJustBashNetworkSandboxSession } from '@ai-sdk/sandbox-just-bash';
195
195
 
196
- const sandboxSession = await createJustBashSandbox({
196
+ const sandboxSession = await createJustBashNetworkSandboxSession({
197
197
  cwd: '/home/user',
198
- }).createSession();
199
-
200
- await runAgentTUI({
201
- title: 'Sandbox Assistant',
202
- agent,
203
- sandbox: sandboxSession.restricted(),
204
198
  });
199
+
200
+ try {
201
+ await runAgentTUI({
202
+ title: 'Sandbox Assistant',
203
+ agent,
204
+ sandbox: sandboxSession.restricted(),
205
+ });
206
+ } finally {
207
+ await sandboxSession.destroy();
208
+ }
205
209
  ```
206
210
 
207
211
  The sandbox is forwarded to every `agent.stream()` call as
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "7.0.114",
3
+ "version": "7.0.116",
4
4
  "type": "module",
5
5
  "description": "AI SDK by Vercel - build apps like ChatGPT, Claude, Gemini, and more with a single interface for any model using the Vercel AI Gateway or go direct to OpenAI, Anthropic, Google, or any other model provider.",
6
6
  "license": "Apache-2.0",
@@ -42,20 +42,20 @@
42
42
  }
43
43
  },
44
44
  "dependencies": {
45
- "@ai-sdk/gateway": "4.0.92",
45
+ "@ai-sdk/gateway": "4.0.94",
46
46
  "@ai-sdk/provider": "4.0.18",
47
- "@ai-sdk/provider-utils": "5.0.47"
47
+ "@ai-sdk/provider-utils": "5.0.49"
48
48
  },
49
49
  "devDependencies": {
50
- "@ai-sdk/amazon-bedrock": "5.0.94",
51
- "@ai-sdk/deepseek": "3.0.52",
52
- "@ai-sdk/google": "4.0.80",
53
- "@ai-sdk/groq": "4.0.48",
54
- "@ai-sdk/huggingface": "2.0.55",
55
- "@ai-sdk/moonshotai": "3.0.56",
56
- "@ai-sdk/openai": "4.0.75",
57
- "@ai-sdk/test-server": "2.0.1",
58
- "@ai-sdk/xai": "5.0.8",
50
+ "@ai-sdk/amazon-bedrock": "5.0.96",
51
+ "@ai-sdk/deepseek": "3.0.54",
52
+ "@ai-sdk/google": "4.0.82",
53
+ "@ai-sdk/groq": "4.0.50",
54
+ "@ai-sdk/huggingface": "2.0.57",
55
+ "@ai-sdk/moonshotai": "3.0.58",
56
+ "@ai-sdk/openai": "4.0.77",
57
+ "@ai-sdk/test-server": "2.0.2",
58
+ "@ai-sdk/xai": "5.0.10",
59
59
  "@edge-runtime/vm": "^5.0.0",
60
60
  "@smithy/eventstream-codec": "^4.3.3",
61
61
  "@smithy/util-utf8": "^4.3.3",
@@ -24,7 +24,10 @@ type ConvertResult = {
24
24
  mediaType: string | undefined;
25
25
  };
26
26
 
27
- function convertUrlToFilePartData(url: URL): ConvertResult {
27
+ function convertUrlToFilePartData(
28
+ url: URL,
29
+ originalUrl?: string,
30
+ ): ConvertResult {
28
31
  if (url.protocol === 'data:') {
29
32
  const { mediaType, base64Content } = splitDataUrl(url.toString());
30
33
 
@@ -38,7 +41,10 @@ function convertUrlToFilePartData(url: URL): ConvertResult {
38
41
  return { data: { type: 'data', data: base64Content }, mediaType };
39
42
  }
40
43
 
41
- return { data: { type: 'url', url }, mediaType: undefined };
44
+ return {
45
+ data: { type: 'url', url, ...(originalUrl != null ? { originalUrl } : {}) },
46
+ mediaType: undefined,
47
+ };
42
48
  }
43
49
 
44
50
  function convertUrlStringToFilePartData(content: string): ConvertResult {
@@ -98,7 +104,7 @@ export function convertToLanguageModelV4FilePart(
98
104
  }
99
105
  return convertInlineDataToFilePartData(content.data);
100
106
  case 'url':
101
- return convertUrlToFilePartData(content.url);
107
+ return convertUrlToFilePartData(content.url, content.originalUrl);
102
108
  case 'reference':
103
109
  return {
104
110
  data: { type: 'reference', reference: content.reference },
@@ -3,7 +3,7 @@ import {
3
3
  DownloadError,
4
4
  readResponseWithSizeLimit,
5
5
  DEFAULT_MAX_DOWNLOAD_SIZE,
6
- fetchWithValidatedRedirects,
6
+ fetchUntrustedUrl,
7
7
  withUserAgentSuffix,
8
8
  getRuntimeEnvironmentUserAgent,
9
9
  } from '@ai-sdk/provider-utils';
@@ -36,7 +36,7 @@ export const download = async ({
36
36
  getRuntimeEnvironmentUserAgent(),
37
37
  );
38
38
 
39
- const response = await fetchWithValidatedRedirects({
39
+ const response = await fetchUntrustedUrl({
40
40
  url: urlText,
41
41
  headers,
42
42
  abortSignal,