Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/rude-readers-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ai-sdk/vue': patch
---

Add useObject support to Vue
5 changes: 4 additions & 1 deletion content/docs/04-ai-sdk-ui/08-object-generation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ description: Learn how to use the useObject hook.

# Object Generation

<Note>`useObject` is an experimental feature and only available in React.</Note>
<Note>
`useObject` is an experimental feature and only available in React, Svelte,
and Vue.
</Note>

The [`useObject`](/docs/reference/ai-sdk-ui/use-object) hook allows you to create interfaces that represent a structured JSON object that is being streamed.

Expand Down
3 changes: 2 additions & 1 deletion content/docs/07-reference/02-ai-sdk-ui/03-use-object.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ description: API reference for the useObject hook.
# `experimental_useObject()`

<Note>
`useObject` is an experimental feature and only available in React and Svelte.
`useObject` is an experimental feature and only available in React, Svelte,
and Vue.
</Note>

Allows you to consume text streams that represent a JSON object and parse them into a complete object based on a schema.
Expand Down
69 changes: 69 additions & 0 deletions examples/nuxt-openai/pages/use-object/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<script setup lang="ts">
import { experimental_useObject as useObject } from '@ai-sdk/vue';
import { notificationSchema } from '~/shared/notification-schema';

const { submit, isLoading, object, stop, error, clear } = useObject({
api: '/api/use-object',
schema: notificationSchema,
});
</script>

<template>
<div class="flex flex-col items-center min-h-screen p-4 m-4">
<button
class="px-4 py-2 mt-4 text-white bg-blue-500 rounded-md disabled:bg-blue-200"
@click="() => submit('Messages during finals week.')"
:disabled="isLoading"
>
Generate notifications
</button>

<div v-if="error" class="mt-4 text-red-500">
An error occurred. {{ error.message }}
</div>

<div v-if="isLoading" class="mt-4 text-gray-500">
<div>Loading...</div>
<button
type="button"
class="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md"
@click="stop"
>
STOP
</button>
</div>

<div class="mt-4 text-gray-500">
<button
type="button"
@click="clear"
class="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md"
>
Clear
</button>
</div>

<div class="flex flex-col gap-4 mt-4">
<div
v-for="(notification, index) in (object?.notifications || [])"
:key="index"
class="flex items-start gap-4 p-4 bg-gray-100 rounded-md dark:bg-gray-800"
>
<div class="flex-1 space-y-1">
<div class="flex items-center justify-between">
<p class="font-medium dark:text-white">
{{ notification?.name }}
</p>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ notification?.minutesAgo }}
<template v-if="notification?.minutesAgo != null"> minutes ago</template>
</p>
</div>
<p class="text-gray-700 dark:text-gray-300">
{{ notification?.message }}
</p>
</div>
</div>
</div>
</div>
</template>
22 changes: 22 additions & 0 deletions examples/nuxt-openai/server/api/use-object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { createOpenAI } from '@ai-sdk/openai';
import { streamObject } from 'ai';
import { notificationSchema } from '~/shared/notification-schema';

export default defineLazyEventHandler(async () => {
const apiKey = useRuntimeConfig().openaiApiKey;
if (!apiKey) throw new Error('Missing OpenAI API key');
const openai = createOpenAI({ apiKey });

return defineEventHandler(async (event: any) => {
const context = await readBody(event);

// Stream generated notifications as objects
const result = streamObject({
model: openai('gpt-4.1'),
prompt: `Generate 5 notifications for a messages app in this context: ${context}`,
schema: notificationSchema,
});

return result.toTextStreamResponse();
});
});
11 changes: 11 additions & 0 deletions examples/nuxt-openai/shared/notification-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { z } from 'zod/v4';

export const notificationSchema = z.object({
notifications: z.array(
z.object({
name: z.string().describe('Name of a fictional person.'),
message: z.string().describe('Message. Do not use emojis or links.'),
minutesAgo: z.number(),
}),
),
});
6 changes: 5 additions & 1 deletion packages/vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,15 @@
"vitest": "2.1.4"
},
"peerDependencies": {
"vue": "^3.3.4"
"vue": "^3.3.4",
"zod": "^3.25.76 || ^4"
},
"peerDependenciesMeta": {
"vue": {
"optional": true
},
"zod": {
"optional": true
}
},
"engines": {
Expand Down
45 changes: 45 additions & 0 deletions packages/vue/src/TestUseObjectComponent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<script setup lang="ts">
import { experimental_useObject } from './use-object';
import { z } from 'zod/v4';
import { ref, reactive } from 'vue';

const onFinishCalls: Array<{
object: { content: string } | undefined;
error: Error | undefined;
}> = reactive([]);

const onErrorResult: Error | undefined = ref(undefined);

const { object, error, submit, isLoading, stop, clear } = experimental_useObject({
api: '/api/use-object',
schema: z.object({ content: z.string() }),
onError(error) {
onErrorResult.value = error;
},
onFinish(event) {
onFinishCalls.push(event);
},
});
</script>

<template>
<div>
<div data-testid="loading">{{ isLoading.toString() }}</div>
<div data-testid="object">{{ JSON.stringify(object) }}</div>
<div data-testid="error">{{ error?.toString() }}</div>
<button
data-testid="submit-button"
@click="submit('test-input')"
>
Generate
</button>
<button data-testid="stop-button" @click="stop">
Stop
</button>
<button data-testid="clear-button" @click="clear">
Clear
</button>
<div data-testid="on-error-result">{{ onErrorResult?.toString() }}</div>
<div data-testid="on-finish-calls">{{ JSON.stringify(onFinishCalls) }}</div>
</div>
</template>
35 changes: 35 additions & 0 deletions packages/vue/src/TestUseObjectCustomTransportComponent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<script setup lang="ts">
import { experimental_useObject } from './use-object';
import { z } from 'zod/v4';
import { ref, reactive } from 'vue';

const { object, error, submit, isLoading, stop, clear } = experimental_useObject({
api: '/api/use-object',
schema: z.object({ content: z.string() }),
headers: {
Authorization: 'Bearer TEST_TOKEN',
'X-Custom-Header': 'CustomValue',
},
credentials: 'include',
});
</script>

<template>
<div>
<div data-testid="loading">{{ isLoading.toString() }}</div>
<div data-testid="object">{{ JSON.stringify(object) }}</div>
<div data-testid="error">{{ error?.toString() }}</div>
<button
data-testid="submit-button"
@click="submit('test-input')"
>
Generate
</button>
<button data-testid="stop-button" @click="stop">
Stop
</button>
<button data-testid="clear-button" @click="clear">
Clear
</button>
</div>
</template>
1 change: 1 addition & 0 deletions packages/vue/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './use-completion';
export { Chat } from './chat.vue';
export * from './use-object';
Loading
Loading