@ai-sdk/svelte
Version:
[Svelte](https://svelte.dev/) UI components for the [AI SDK](https://ai-sdk.dev/docs):
254 lines (219 loc) • 6.82 kB
text/typescript
import {
generateId,
isAbortError,
safeValidateTypes,
type FetchFunction,
type InferSchema,
} from '@ai-sdk/provider-utils';
import {
asSchema,
isDeepEqualData,
parsePartialJson,
type DeepPartial,
type Schema,
} from 'ai';
import type * as z3 from 'zod/v3';
import type * as z4 from 'zod/v4';
import {
getStructuredObjectContext,
hasStructuredObjectContext,
KeyedStructuredObjectStore,
type StructuredObjectStore,
} from './structured-object-context.svelte.js';
export type Experimental_StructuredObjectOptions<
SCHEMA extends z3.Schema | z4.core.$ZodType | Schema,
RESULT = InferSchema<SCHEMA>,
> = {
/**
* The API endpoint. It should stream JSON that matches the schema as chunked text.
*/
api: string;
/**
* A Zod schema that defines the shape of the complete object.
*/
schema: SCHEMA;
/**
* An unique identifier. If not provided, a random one will be
* generated. When provided, the `useObject` hook with the same `id` will
* have shared states across components.
*/
id?: string;
/**
* An optional value for the initial object.
*/
initialValue?: DeepPartial<RESULT>;
/**
* Custom fetch implementation. You can use it as a middleware to intercept requests,
* or to provide a custom fetch implementation for e.g. testing.
*/
fetch?: FetchFunction;
/**
* Callback that is called when the stream has finished.
*/
onFinish?: (event: {
/**
* The generated object (typed according to the schema).
* Can be undefined if the final object does not match the schema.
*/
object: RESULT | undefined;
/**
* Optional error object. This is e.g. a TypeValidationError when the final object does not match the schema.
*/
error: Error | undefined;
}) => Promise<void> | void;
/**
* Callback function to be called when an error is encountered.
*/
onError?: (error: Error) => void;
/**
* Additional HTTP headers to be included in the request.
*/
headers?: Record<string, string> | Headers;
/**
* The credentials mode to be used for the fetch request.
* Possible values are: 'omit', 'same-origin', 'include'.
* Defaults to 'same-origin'.
*/
credentials?: RequestCredentials;
};
export class StructuredObject<
SCHEMA extends z3.Schema | z4.core.$ZodType | Schema,
RESULT = InferSchema<SCHEMA>,
INPUT = unknown,
> {
{} as Experimental_StructuredObjectOptions<SCHEMA, RESULT>;
readonly
readonly
readonly
this.
) as StructuredObjectStore<RESULT>;
/**
* The current value for the generated object. Updated as the API streams JSON chunks.
*/
get object(): DeepPartial<RESULT> | undefined {
return this.
}
set
this.
}
/** The error object of the API request */
get error() {
return this.
}
/**
* Flag that indicates whether an API request is in progress.
*/
get loading() {
return this.
}
constructor(options: Experimental_StructuredObjectOptions<SCHEMA, RESULT>) {
if (hasStructuredObjectContext()) {
this.
} else {
this.
}
this.
this.
}
/**
* Abort the current request immediately, keep the current partial object if any.
*/
stop = () => {
try {
this.
} catch {
// ignore
} finally {
this.
this.
}
};
/**
* Calls the API with the provided input as JSON body.
*/
submit = async (input: INPUT) => {
try {
this.
this.
const abortController = new AbortController();
this.
const actualFetch = this.
const response = await actualFetch(this.
method: 'POST',
headers: {
'Content-Type': 'application/json',
...this.
},
credentials: this.
signal: abortController.signal,
body: JSON.stringify(input),
});
if (!response.ok) {
throw new Error(
(await response.text()) ?? 'Failed to fetch the response.',
);
}
if (response.body == null) {
throw new Error('The response body is empty.');
}
let accumulatedText = '';
let latestObject: DeepPartial<RESULT> | undefined = undefined;
await response.body.pipeThrough(new TextDecoderStream()).pipeTo(
new WritableStream<string>({
write: async chunk => {
if (abortController?.signal.aborted) {
throw new DOMException('Stream aborted', 'AbortError');
}
accumulatedText += chunk;
const { value } = await parsePartialJson(accumulatedText);
const currentObject = value as DeepPartial<RESULT>;
if (!isDeepEqualData(latestObject, currentObject)) {
latestObject = currentObject;
this.
}
},
close: async () => {
this.
this.
if (this.
const validationResult = await safeValidateTypes({
value: latestObject,
schema: asSchema(this.
});
this.
validationResult.success
? { object: validationResult.value, error: undefined }
: { object: undefined, error: validationResult.error },
);
}
},
}),
);
} catch (error) {
if (isAbortError(error)) {
return;
}
const coalescedError =
error instanceof Error ? error : new Error(String(error));
if (this.
this.
}
this.
this.
}
};
/**
* Clears the object state.
*/
clear = () => {
this.stop();
this.
};
this.
this.
this.
};
}