@ai-sdk/provider-utils
Version:
103 lines (100 loc) • 3.34 kB
text/typescript
import type { LanguageModelV4StreamPart } from '@ai-sdk/provider';
/**
* Providers that cannot use native structured output for a request can use a
* JSON response tool instead. This utility converts that tool's streamed input
* into text events so the SDK can parse it as structured output.
*
* Conversational text is suppressed. Application tool calls pass through so the
* caller can execute them. A response containing only the JSON tool finishes
* with `stop`; a response containing application calls keeps its finish reason.
*/
export function convertJsonResponseToolStream(toolName: string) {
const responseToolIds = new Set<string>();
const responseToolsWithDeltas = new Set<string>();
let hasResponseTool = false;
let hasApplicationTool = false;
return new TransformStream<
LanguageModelV4StreamPart,
LanguageModelV4StreamPart
>({
transform(part, controller) {
switch (part.type) {
case 'text-start':
case 'text-delta':
case 'text-end':
// The response tool supplies the output; omit conversational text.
return;
case 'tool-input-start':
if (part.toolName === toolName) {
responseToolIds.add(part.id);
controller.enqueue({
type: 'text-start',
id: part.id,
...(part.providerMetadata && {
providerMetadata: part.providerMetadata,
}),
});
return;
}
break;
case 'tool-input-delta':
if (responseToolIds.has(part.id)) {
if (part.delta.length > 0) responseToolsWithDeltas.add(part.id);
controller.enqueue({
type: 'text-delta',
id: part.id,
delta: part.delta,
...(part.providerMetadata && {
providerMetadata: part.providerMetadata,
}),
});
return;
}
break;
case 'tool-input-end':
if (responseToolIds.has(part.id)) return;
break;
case 'tool-call':
if (part.toolName === toolName) {
if (!responseToolsWithDeltas.has(part.toolCallId)) {
controller.enqueue({
type: 'text-delta',
id: part.toolCallId,
delta: part.input,
...(part.providerMetadata && {
providerMetadata: part.providerMetadata,
}),
});
}
controller.enqueue({
type: 'text-end',
id: part.toolCallId,
...(part.providerMetadata && {
providerMetadata: part.providerMetadata,
}),
});
hasResponseTool = true;
return;
}
if (!part.providerExecuted) {
hasApplicationTool = true;
}
break;
case 'finish':
if (
hasResponseTool &&
!hasApplicationTool &&
part.finishReason.unified === 'tool-calls'
) {
controller.enqueue({
...part,
finishReason: { ...part.finishReason, unified: 'stop' },
});
return;
}
break;
}
controller.enqueue(part);
},
});
}