Skip to content

Commit 760268f

Browse files
committed
Merge branch 'main' into refactor/radix-to-base-ui
# Conflicts: # src/components/ds/ui/index.tsx # src/components/examples/ExampleWorkbench.client.tsx # src/components/examples/SandboxBrowser.client.tsx # src/components/notebook/NotebookAssistant.client.tsx
2 parents e960733 + 9ad7dd5 commit 760268f

63 files changed

Lines changed: 7990 additions & 1705 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
diff --git a/src/adapters/responses-text.ts b/src/adapters/responses-text.ts
2+
--- a/src/adapters/responses-text.ts
3+
+++ b/src/adapters/responses-text.ts
4+
@@ -37,10 +37,15 @@
5+
* Responses function calls have two identifiers: `call_id` correlates the
6+
* function output with the call, while `id` identifies the output item itself.
7+
* TanStack AI uses `call_id` as the canonical tool-call ID and carries the
8+
- * item ID here so stateless follow-up requests can replay both values.
9+
+ * item ID here so stateless follow-up requests can replay both values. The
10+
+ * response ID lets an immediately-following server tool result continue the
11+
+ * complete provider response, including opaque reasoning items that must not
12+
+ * be exposed through AG-UI.
13+
*/
14+
export interface OpenAIResponsesToolCallMetadata {
15+
itemId: string
16+
+ responseId?: string
17+
+ model?: string
18+
}
19+
20+
interface StreamedFunctionCallMetadata {
21+
@@ -790,6 +795,7 @@
22+
23+
// Preserve response metadata across events
24+
let model: string = options.model
25+
+ let responseId: string | undefined
26+
27+
// AG-UI lifecycle tracking
28+
let stepId: string | null = null
29+
@@ -887,6 +893,7 @@
30+
// response.created marks the start of a fresh run — safe to reset
31+
// the per-run accumulators here.
32+
if (chunk.type === 'response.created') {
33+
+ responseId = chunk.response.id
34+
hasStreamedContentDeltas = false
35+
hasStreamedReasoningDeltas = false
36+
hasEmittedTextMessageStart = false
37+
@@ -1214,6 +1221,7 @@
38+
index: chunk.output_index,
39+
metadata: {
40+
itemId: item.id,
41+
+ ...(responseId && { responseId, model }),
42+
} satisfies OpenAIResponsesToolCallMetadata,
43+
}
44+
metadata.started = true
45+
@@ -1365,6 +1373,7 @@
46+
index: metadata.index,
47+
metadata: {
48+
itemId: item.id,
49+
+ ...(responseId && { responseId, model }),
50+
} satisfies OpenAIResponsesToolCallMetadata,
51+
}
52+
metadata.started = true
53+
@@ -1417,6 +1426,7 @@
54+
}
55+
56+
if (chunk.type === 'response.completed') {
57+
+ responseId = chunk.response.id
58+
// Final backstop for function_call lifecycle: if a function_call
59+
// appears in `response.output[]` but was never matched by an
60+
// output_item.added/done with a name, recover the missing START
61+
@@ -1450,6 +1460,8 @@
62+
index: metadata.index,
63+
metadata: {
64+
itemId: item.id,
65+
+ responseId,
66+
+ model,
67+
} satisfies OpenAIResponsesToolCallMetadata,
68+
}
69+
metadata.started = true
70+
@@ -1638,7 +1650,12 @@
71+
protected mapOptionsToRequest(
72+
options: TextOptions<TProviderOptions>,
73+
): Omit<ResponseCreateParams, 'stream'> {
74+
- const input = this.convertMessagesToInput(options.messages)
75+
+ const continuation = this.getToolContinuation(
76+
+ options.messages,
77+
+ options.model,
78+
+ )
79+
+ const input =
80+
+ continuation?.input ?? this.convertMessagesToInput(options.messages)
81+
82+
const tools = options.tools
83+
? convertToolsToResponsesFormat(
84+
@@ -1685,6 +1702,9 @@
85+
return {
86+
...modelOptions,
87+
model: options.model,
88+
+ ...(continuation && {
89+
+ previous_response_id: continuation.responseId,
90+
+ }),
91+
...(options.metadata !== undefined && { metadata: options.metadata }),
92+
...(() => {
93+
const prompts = normalizeSystemPrompts(options.systemPrompts)
94+
@@ -1706,6 +1726,70 @@
95+
*/
96+
supportsCombinedToolsAndSchema(): boolean {
97+
return true
98+
+ }
99+
+
100+
+ /**
101+
+ * Continue the provider response only for the server-tool phase immediately
102+
+ * following it. `previous_response_id` makes OpenAI restore every output
103+
+ * item in its original order, including reasoning items that cannot safely
104+
+ * cross the public AG-UI stream.
105+
+ */
106+
+ private getToolContinuation(
107+
+ messages: Array<ModelMessage>,
108+
+ model: string,
109+
+ ): { responseId: string; input: ResponseInput } | undefined {
110+
+ for (let index = messages.length - 1; index >= 0; index--) {
111+
+ const message = messages[index]
112+
+ if (!message) continue
113+
+
114+
+ if (message.role === 'user') return undefined
115+
+ if (message.role !== 'assistant' || !message.toolCalls?.length) continue
116+
+
117+
+ const responseIds = new Set<string>()
118+
+ const toolCallIds = new Set<string>()
119+
+ for (const toolCall of message.toolCalls) {
120+
+ const metadata = toolCall.metadata
121+
+ if (
122+
+ typeof metadata !== 'object' ||
123+
+ metadata === null ||
124+
+ !('responseId' in metadata) ||
125+
+ typeof metadata.responseId !== 'string' ||
126+
+ !('model' in metadata) ||
127+
+ metadata.model !== model
128+
+ ) {
129+
+ return undefined
130+
+ }
131+
+ responseIds.add(metadata.responseId)
132+
+ toolCallIds.add(toolCall.id)
133+
+ }
134+
+
135+
+ if (responseIds.size !== 1) return undefined
136+
+
137+
+ const toolResults = messages.slice(index + 1)
138+
+ if (toolResults.length !== toolCallIds.size) return undefined
139+
+
140+
+ const resultIds = new Set<string>()
141+
+ for (const toolResult of toolResults) {
142+
+ if (
143+
+ toolResult.role !== 'tool' ||
144+
+ !toolResult.toolCallId ||
145+
+ !toolCallIds.has(toolResult.toolCallId)
146+
+ ) {
147+
+ return undefined
148+
+ }
149+
+ resultIds.add(toolResult.toolCallId)
150+
+ }
151+
+ if (resultIds.size !== toolCallIds.size) return undefined
152+
+
153+
+ const responseId = responseIds.values().next().value
154+
+ if (!responseId) return undefined
155+
+ return {
156+
+ responseId,
157+
+ input: this.convertMessagesToInput(toolResults),
158+
+ }
159+
+ }
160+
+
161+
+ return undefined
162+
}
163+
164+
/**
165+
diff --git a/dist/esm/adapters/responses-text.js b/dist/esm/adapters/responses-text.js
166+
--- a/dist/esm/adapters/responses-text.js
167+
+++ b/dist/esm/adapters/responses-text.js
168+
@@ -508,6 +508,7 @@
169+
let hasStreamedContentDeltas = false;
170+
let hasStreamedReasoningDeltas = false;
171+
let model = options.model;
172+
+ let responseId;
173+
let stepId = null;
174+
let hasEmittedTextMessageStart = false;
175+
let hasEmittedStepStarted = false;
176+
@@ -571,6 +572,7 @@
177+
};
178+
if (chunk.type === "response.created" || chunk.type === "response.incomplete" || chunk.type === "response.failed") model = chunk.response.model;
179+
if (chunk.type === "response.created") {
180+
+ responseId = chunk.response.id;
181+
hasStreamedContentDeltas = false;
182+
hasStreamedReasoningDeltas = false;
183+
hasEmittedTextMessageStart = false;
184+
@@ -778,7 +780,10 @@
185+
model: model || options.model,
186+
timestamp: Date.now(),
187+
index: chunk.output_index,
188+
- metadata: { itemId: item.id }
189+
+ metadata: {
190+
+ itemId: item.id,
191+
+ ...responseId && { responseId, model }
192+
+ }
193+
};
194+
metadata.started = true;
195+
}
196+
@@ -867,7 +872,10 @@
197+
model: model || options.model,
198+
timestamp: Date.now(),
199+
index: metadata.index,
200+
- metadata: { itemId: item.id }
201+
+ metadata: {
202+
+ itemId: item.id,
203+
+ ...responseId && { responseId, model }
204+
+ }
205+
};
206+
metadata.started = true;
207+
}
208+
@@ -904,6 +912,7 @@
209+
}
210+
}
211+
if (chunk.type === "response.completed") {
212+
+ responseId = chunk.response.id;
213+
for (const item of chunk.response.output) {
214+
if (item.type !== "function_call" || !item.id) continue;
215+
const metadata = toolCallMetadata.get(item.id) ?? {
216+
@@ -927,7 +936,11 @@
217+
model: model || options.model,
218+
timestamp: Date.now(),
219+
index: metadata.index,
220+
- metadata: { itemId: item.id }
221+
+ metadata: {
222+
+ itemId: item.id,
223+
+ responseId,
224+
+ model
225+
+ }
226+
};
227+
metadata.started = true;
228+
}
229+
@@ -1044,7 +1057,8 @@
230+
* Override this in subclasses to add provider-specific options.
231+
*/
232+
mapOptionsToRequest(options) {
233+
- const input = this.convertMessagesToInput(options.messages);
234+
+ const continuation = this.getToolContinuation(options.messages, options.model);
235+
+ const input = continuation?.input ?? this.convertMessagesToInput(options.messages);
236+
const tools = options.tools ? convertToolsToResponsesFormat(options.tools, this.makeStructuredOutputCompatible.bind(this)) : void 0;
237+
const modelOptions = options.modelOptions;
238+
const combinedSchema = options.outputSchema;
239+
@@ -1057,6 +1071,7 @@
240+
return {
241+
...modelOptions,
242+
model: options.model,
243+
+ ...continuation && { previous_response_id: continuation.responseId },
244+
...options.metadata !== void 0 && { metadata: options.metadata },
245+
...(() => {
246+
const prompts = normalizeSystemPrompts(options.systemPrompts);
247+
@@ -1075,6 +1090,43 @@
248+
*/
249+
supportsCombinedToolsAndSchema() {
250+
return true;
251+
+ }
252+
+ /**
253+
+ * Continue the provider response only for the server-tool phase immediately
254+
+ * following it. `previous_response_id` makes OpenAI restore every output
255+
+ * item in its original order, including reasoning items that cannot safely
256+
+ * cross the public AG-UI stream.
257+
+ */
258+
+ getToolContinuation(messages, model) {
259+
+ for (let index = messages.length - 1; index >= 0; index--) {
260+
+ const message = messages[index];
261+
+ if (!message) continue;
262+
+ if (message.role === "user") return;
263+
+ if (message.role !== "assistant" || !message.toolCalls?.length) continue;
264+
+ const responseIds = /* @__PURE__ */ new Set();
265+
+ const toolCallIds = /* @__PURE__ */ new Set();
266+
+ for (const toolCall of message.toolCalls) {
267+
+ const metadata = toolCall.metadata;
268+
+ if (typeof metadata !== "object" || metadata === null || !("responseId" in metadata) || typeof metadata.responseId !== "string" || !("model" in metadata) || metadata.model !== model) return;
269+
+ responseIds.add(metadata.responseId);
270+
+ toolCallIds.add(toolCall.id);
271+
+ }
272+
+ if (responseIds.size !== 1) return;
273+
+ const toolResults = messages.slice(index + 1);
274+
+ if (toolResults.length !== toolCallIds.size) return;
275+
+ const resultIds = /* @__PURE__ */ new Set();
276+
+ for (const toolResult of toolResults) {
277+
+ if (toolResult.role !== "tool" || !toolResult.toolCallId || !toolCallIds.has(toolResult.toolCallId)) return;
278+
+ resultIds.add(toolResult.toolCallId);
279+
+ }
280+
+ if (resultIds.size !== toolCallIds.size) return;
281+
+ const responseId = responseIds.values().next().value;
282+
+ if (!responseId) return;
283+
+ return {
284+
+ responseId,
285+
+ input: this.convertMessagesToInput(toolResults)
286+
+ };
287+
+ }
288+
}
289+
/**
290+
* Converts ModelMessage[] to Responses API ResponseInput format.
291+
diff --git a/dist/esm/adapters/responses-text.d.ts b/dist/esm/adapters/responses-text.d.ts
292+
--- a/dist/esm/adapters/responses-text.d.ts
293+
+++ b/dist/esm/adapters/responses-text.d.ts
294+
@@ -8,10 +8,15 @@
295+
* Responses function calls have two identifiers: `call_id` correlates the
296+
* function output with the call, while `id` identifies the output item itself.
297+
* TanStack AI uses `call_id` as the canonical tool-call ID and carries the
298+
- * item ID here so stateless follow-up requests can replay both values.
299+
+ * item ID here so stateless follow-up requests can replay both values. The
300+
+ * response ID lets an immediately-following server tool result continue the
301+
+ * complete provider response, including opaque reasoning items that must not
302+
+ * be exposed through AG-UI.
303+
*/
304+
export interface OpenAIResponsesToolCallMetadata {
305+
itemId: string;
306+
+ responseId?: string;
307+
+ model?: string;
308+
}
309+
interface StreamedFunctionCallMetadata {
310+
callId: string;
311+
@@ -119,6 +124,7 @@
312+
* that route to providers without this capability should override.
313+
*/
314+
supportsCombinedToolsAndSchema(): boolean;
315+
+ private getToolContinuation;
316+
/**
317+
* Converts ModelMessage[] to Responses API ResponseInput format.
318+
* Override this in subclasses for provider-specific message format quirks.

pnpm-lock.yaml

Lines changed: 5 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
packages:
22
- .
33

4+
patchedDependencies:
5+
'@tanstack/openai-base@0.9.12': patches/@tanstack__openai-base@0.9.12.patch
6+
47
minimumReleaseAgeExclude:
58
- '@tanstack/charts@0.13.0'
69
- '@tanstack/react-table@9.0.0'

scripts/local-notebook-ai-vite.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import type { IncomingMessage, ServerResponse } from 'node:http'
44
import path from 'node:path'
55
import readline from 'node:readline'
66
import type { PluginOption } from 'vite'
7+
import { notebookAiLocalValidationEndpoint } from '../src/utils/notebook-ai-local-validation'
78

89
const maxRequestBytes = 2 * 1024 * 1024
9-
const maxResponseBytes = 4 * 1024 * 1024
10+
const maxResponseBytes = 24 * 1024 * 1024
1011

1112
export function localNotebookAi(): PluginOption {
1213
let bridgeProcess: ReturnType<typeof spawn> | undefined
@@ -23,7 +24,8 @@ export function localNotebookAi(): PluginOption {
2324
const url = new URL(request.url, 'http://localhost')
2425
if (
2526
url.pathname !== '/api/notebook/chatgpt' &&
26-
url.pathname !== '/api/notebook/chatgpt/assist'
27+
url.pathname !== '/api/notebook/chatgpt/assist' &&
28+
url.pathname !== notebookAiLocalValidationEndpoint
2729
) {
2830
return next()
2931
}
@@ -225,6 +227,9 @@ function getBridgePath(
225227
if (pathname === '/api/notebook/chatgpt/assist' && method === 'POST') {
226228
return '/assist'
227229
}
230+
if (pathname === notebookAiLocalValidationEndpoint && method === 'POST') {
231+
return '/validation'
232+
}
228233
if (pathname !== '/api/notebook/chatgpt') return
229234
if (method === 'GET') return '/account'
230235
if (method !== 'POST') return

0 commit comments

Comments
 (0)