Skip to content

Commit 3965b88

Browse files
committed
fix(openai-base): align rebased openai-base with current main
Post-merge fixes landed on top of the rebase onto main: - Thread TToolCapabilities generic through OpenAICompatibleChatCompletionsTextAdapter and OpenAICompatibleResponsesTextAdapter so ai-openai / ai-grok / ai-groq / ai-openrouter pass their own tool-capability maps while the base stays brand-agnostic. - Align AG-UI event yields and adapter signatures with main's EventType-enum-typed StreamChunk: wrap event yields through an asChunk helper; align BaseImage/TTS/ Transcription super() calls with (model, config) signature; switch GeneratedImage construction to the url-xor-b64Json discriminated shape; propagate options.logger through summarize → chatStream. - Responses API tool-call events now use item.id as toolCallId and emit both toolCallName and toolName, matching main's contract. - Prefer videos.retrieve URL for video content (retrieve-returns-url path) — aligns with main's behavior and works against providers that don't implement /content. - ai-utils: preserve the full random suffix in generateId (pre-rebase regression would have failed ai-fal's entropy test). - Recreate ai-grok/src/tools/index.ts as a thin re-export of openai-base's Chat Completions tool converter (vite entry expects it).
1 parent 8d5121e commit 3965b88

15 files changed

Lines changed: 234 additions & 135 deletions

File tree

packages/typescript/ai-grok/src/adapters/text.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@ export type { ExternalTextProviderOptions as GrokTextProviderOptions } from '../
4141
export class GrokTextAdapter<
4242
TModel extends (typeof GROK_CHAT_MODELS)[number],
4343
TProviderOptions extends Record<string, any> = ResolveProviderOptions<TModel>,
44-
TInputModalities extends
45-
ReadonlyArray<Modality> = ResolveInputModalities<TModel>,
46-
TToolCapabilities extends
47-
ReadonlyArray<string> = ResolveToolCapabilities<TModel>,
44+
TInputModalities extends ReadonlyArray<Modality> =
45+
ResolveInputModalities<TModel>,
46+
TToolCapabilities extends ReadonlyArray<string> =
47+
ResolveToolCapabilities<TModel>,
4848
> extends OpenAICompatibleChatCompletionsTextAdapter<
4949
TModel,
5050
TProviderOptions,
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export {
2+
type ChatCompletionFunctionTool as FunctionTool,
3+
convertFunctionToolToChatCompletionsFormat as convertFunctionToolToAdapterFormat,
4+
convertToolsToChatCompletionsFormat as convertToolsToProviderFormat,
5+
} from '@tanstack/openai-base'

packages/typescript/ai-grok/tests/grok-adapter.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ vi.mock('openai', () => {
2323
}
2424
})
2525

26-
2726
// Helper to create async iterable from chunks
2827
function createAsyncIterable<T>(chunks: Array<T>): AsyncIterable<T> {
2928
return {

packages/typescript/ai-openai/src/adapters/text.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,10 @@ type ResolveToolCapabilities<TModel extends string> =
7474
export class OpenAITextAdapter<
7575
TModel extends OpenAIChatModel,
7676
TProviderOptions extends Record<string, any> = ResolveProviderOptions<TModel>,
77-
TInputModalities extends
78-
ReadonlyArray<Modality> = ResolveInputModalities<TModel>,
79-
TToolCapabilities extends
80-
ReadonlyArray<string> = ResolveToolCapabilities<TModel>,
77+
TInputModalities extends ReadonlyArray<Modality> =
78+
ResolveInputModalities<TModel>,
79+
TToolCapabilities extends ReadonlyArray<string> =
80+
ResolveToolCapabilities<TModel>,
8181
> extends OpenAICompatibleResponsesTextAdapter<
8282
TModel,
8383
TProviderOptions,
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
export function generateId(prefix: string): string {
22
const timestamp = Date.now()
3-
const randomPart = Math.random().toString(36).substring(7)
3+
// Drop the "0." prefix from the base36 float (2 chars), keeping the full
4+
// random portion (~9+ chars of entropy). Previously used `.substring(7)`
5+
// which left only ~4 random chars — see the regression test in ai-fal's
6+
// utils.
7+
const randomPart = Math.random().toString(36).substring(2)
48
return `${prefix}-${timestamp}-${randomPart}`
59
}

packages/typescript/openai-base/src/adapters/chat-completions-text.ts

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ import type {
1818
} from '@tanstack/ai'
1919
import type { OpenAICompatibleClientConfig } from '../types/config'
2020

21+
/** Cast an event object to StreamChunk. Adapters construct events with string
22+
* literal types which are structurally compatible with the EventType enum. */
23+
const asChunk = (chunk: Record<string, unknown>) =>
24+
chunk as unknown as StreamChunk
25+
2126
/**
2227
* OpenAI-compatible Chat Completions Text Adapter
2328
*
@@ -96,16 +101,16 @@ export class OpenAICompatibleChatCompletionsTextAdapter<
96101
// Emit RUN_STARTED if not yet emitted
97102
if (!aguiState.hasEmittedRunStarted) {
98103
aguiState.hasEmittedRunStarted = true
99-
yield {
104+
yield asChunk({
100105
type: 'RUN_STARTED',
101106
runId: aguiState.runId,
102107
model: options.model,
103108
timestamp,
104-
}
109+
})
105110
}
106111

107112
// Emit AG-UI RUN_ERROR
108-
yield {
113+
yield asChunk({
109114
type: 'RUN_ERROR',
110115
runId: aguiState.runId,
111116
model: options.model,
@@ -114,7 +119,7 @@ export class OpenAICompatibleChatCompletionsTextAdapter<
114119
message: err.message || 'Unknown error',
115120
code: err.code,
116121
},
117-
}
122+
})
118123

119124
console.error(
120125
`>>> [${this.name}] chatStream: Fatal error during response creation <<<`,
@@ -256,12 +261,12 @@ export class OpenAICompatibleChatCompletionsTextAdapter<
256261
// Emit RUN_STARTED on first chunk
257262
if (!aguiState.hasEmittedRunStarted) {
258263
aguiState.hasEmittedRunStarted = true
259-
yield {
264+
yield asChunk({
260265
type: 'RUN_STARTED',
261266
runId: aguiState.runId,
262267
model: chunk.model || options.model,
263268
timestamp,
264-
}
269+
})
265270
}
266271

267272
const delta = choice.delta
@@ -273,26 +278,26 @@ export class OpenAICompatibleChatCompletionsTextAdapter<
273278
// Emit TEXT_MESSAGE_START on first text content
274279
if (!hasEmittedTextMessageStart) {
275280
hasEmittedTextMessageStart = true
276-
yield {
281+
yield asChunk({
277282
type: 'TEXT_MESSAGE_START',
278283
messageId: aguiState.messageId,
279284
model: chunk.model || options.model,
280285
timestamp,
281286
role: 'assistant',
282-
}
287+
})
283288
}
284289

285290
accumulatedContent += deltaContent
286291

287292
// Emit AG-UI TEXT_MESSAGE_CONTENT
288-
yield {
293+
yield asChunk({
289294
type: 'TEXT_MESSAGE_CONTENT',
290295
messageId: aguiState.messageId,
291296
model: chunk.model || options.model,
292297
timestamp,
293298
delta: deltaContent,
294299
content: accumulatedContent,
295-
}
300+
})
296301
}
297302

298303
// Handle tool calls - they come in as deltas
@@ -326,25 +331,26 @@ export class OpenAICompatibleChatCompletionsTextAdapter<
326331
// Emit TOOL_CALL_START when we have id and name
327332
if (toolCall.id && toolCall.name && !toolCall.started) {
328333
toolCall.started = true
329-
yield {
334+
yield asChunk({
330335
type: 'TOOL_CALL_START',
331336
toolCallId: toolCall.id,
337+
toolCallName: toolCall.name,
332338
toolName: toolCall.name,
333339
model: chunk.model || options.model,
334340
timestamp,
335341
index,
336-
}
342+
})
337343
}
338344

339345
// Emit TOOL_CALL_ARGS for argument deltas
340346
if (toolCallDelta.function?.arguments && toolCall.started) {
341-
yield {
347+
yield asChunk({
342348
type: 'TOOL_CALL_ARGS',
343349
toolCallId: toolCall.id,
344350
model: chunk.model || options.model,
345351
timestamp,
346352
delta: toolCallDelta.function.arguments,
347-
}
353+
})
348354
}
349355
}
350356
}
@@ -368,14 +374,15 @@ export class OpenAICompatibleChatCompletionsTextAdapter<
368374
}
369375

370376
// Emit AG-UI TOOL_CALL_END
371-
yield {
377+
yield asChunk({
372378
type: 'TOOL_CALL_END',
373379
toolCallId: toolCall.id,
380+
toolCallName: toolCall.name,
374381
toolName: toolCall.name,
375382
model: chunk.model || options.model,
376383
timestamp,
377384
input: parsedInput,
378-
}
385+
})
379386
}
380387
}
381388

@@ -387,16 +394,16 @@ export class OpenAICompatibleChatCompletionsTextAdapter<
387394

388395
// Emit TEXT_MESSAGE_END if we had text content
389396
if (hasEmittedTextMessageStart) {
390-
yield {
397+
yield asChunk({
391398
type: 'TEXT_MESSAGE_END',
392399
messageId: aguiState.messageId,
393400
model: chunk.model || options.model,
394401
timestamp,
395-
}
402+
})
396403
}
397404

398405
// Emit AG-UI RUN_FINISHED
399-
yield {
406+
yield asChunk({
400407
type: 'RUN_FINISHED',
401408
runId: aguiState.runId,
402409
model: chunk.model || options.model,
@@ -409,15 +416,15 @@ export class OpenAICompatibleChatCompletionsTextAdapter<
409416
}
410417
: undefined,
411418
finishReason: computedFinishReason,
412-
}
419+
})
413420
}
414421
}
415422
} catch (error: unknown) {
416423
const err = error as Error & { code?: string }
417424
console.error(`[${this.name}] Stream ended with error:`, err.message)
418425

419426
// Emit AG-UI RUN_ERROR
420-
yield {
427+
yield asChunk({
421428
type: 'RUN_ERROR',
422429
runId: aguiState.runId,
423430
model: options.model,
@@ -426,7 +433,7 @@ export class OpenAICompatibleChatCompletionsTextAdapter<
426433
message: err.message || 'Unknown error occurred',
427434
code: err.code,
428435
},
429-
}
436+
})
430437
}
431438
}
432439

packages/typescript/openai-base/src/adapters/image.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export class OpenAICompatibleImageAdapter<
4343
model: TModel,
4444
name: string = 'openai-compatible',
4545
) {
46-
super({}, model)
46+
super(model, {})
4747
this.name = name
4848
this.client = createOpenAICompatibleClient(config)
4949
}
@@ -87,11 +87,18 @@ export class OpenAICompatibleImageAdapter<
8787
model: string,
8888
response: OpenAI_SDK.Images.ImagesResponse,
8989
): ImageGenerationResult {
90-
const images: Array<GeneratedImage> = (response.data ?? []).map((item) => ({
91-
b64Json: item.b64_json,
92-
url: item.url,
93-
revisedPrompt: item.revised_prompt,
94-
}))
90+
const images: Array<GeneratedImage> = (response.data ?? []).flatMap(
91+
(item): Array<GeneratedImage> => {
92+
const revisedPrompt = item.revised_prompt
93+
if (item.b64_json) {
94+
return [{ b64Json: item.b64_json, revisedPrompt }]
95+
}
96+
if (item.url) {
97+
return [{ url: item.url, revisedPrompt }]
98+
}
99+
return []
100+
},
101+
)
95102

96103
return {
97104
id: generateId(this.name),

0 commit comments

Comments
 (0)