From c6161281041f661c499d048ff3b868532169f2e3 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Mon, 31 Mar 2025 15:17:43 -0400 Subject: [PATCH 01/22] feat(vertexai): Add support for `AbortSignal` --- .changeset/long-keys-watch.md | 6 + common/api-review/vertexai.api.md | 19 +- docs-devsite/_toc.yaml | 2 + docs-devsite/vertexai.chatsession.md | 10 +- docs-devsite/vertexai.generativemodel.md | 15 +- docs-devsite/vertexai.imagenmodel.md | 5 +- docs-devsite/vertexai.md | 1 + docs-devsite/vertexai.singlerequestoptions.md | 57 ++++ .../vertexai/src/methods/chat-session.test.ts | 116 ++++++++ packages/vertexai/src/methods/chat-session.ts | 19 +- packages/vertexai/src/methods/count-tokens.ts | 6 +- .../vertexai/src/methods/generate-content.ts | 10 +- .../src/models/generative-model.test.ts | 116 ++++++++ .../vertexai/src/models/generative-model.ts | 37 ++- .../vertexai/src/models/imagen-model.test.ts | 143 ++++++++- packages/vertexai/src/models/imagen-model.ts | 20 +- .../vertexai/src/requests/request.test.ts | 275 ++++++++++++++++-- packages/vertexai/src/requests/request.ts | 76 ++++- packages/vertexai/src/types/requests.ts | 37 +++ 19 files changed, 879 insertions(+), 91 deletions(-) create mode 100644 .changeset/long-keys-watch.md create mode 100644 docs-devsite/vertexai.singlerequestoptions.md diff --git a/.changeset/long-keys-watch.md b/.changeset/long-keys-watch.md new file mode 100644 index 00000000000..7cc4d582ff1 --- /dev/null +++ b/.changeset/long-keys-watch.md @@ -0,0 +1,6 @@ +--- +'firebase': minor +'@firebase/vertexai': minor +--- + +Add support for `AbortSignal`, allowing requests to be aborted. diff --git a/common/api-review/vertexai.api.md b/common/api-review/vertexai.api.md index f9cf3dac5bd..0c030c8a78e 100644 --- a/common/api-review/vertexai.api.md +++ b/common/api-review/vertexai.api.md @@ -50,8 +50,8 @@ export class ChatSession { params?: StartChatParams | undefined; // (undocumented) requestOptions?: RequestOptions | undefined; - sendMessage(request: string | Array): Promise; - sendMessageStream(request: string | Array): Promise; + sendMessage(request: string | Array, singleRequestOptions?: SingleRequestOptions): Promise; + sendMessageStream(request: string | Array, singleRequestOptions?: SingleRequestOptions): Promise; } // @public @@ -328,9 +328,9 @@ export interface GenerativeContentBlob { // @public export class GenerativeModel extends VertexAIModel { constructor(vertexAI: VertexAI, modelParams: ModelParams, requestOptions?: RequestOptions); - countTokens(request: CountTokensRequest | string | Array): Promise; - generateContent(request: GenerateContentRequest | string | Array): Promise; - generateContentStream(request: GenerateContentRequest | string | Array): Promise; + countTokens(request: CountTokensRequest | string | Array, singleRequestOptions?: SingleRequestOptions): Promise; + generateContent(request: GenerateContentRequest | string | Array, singleRequestOptions?: SingleRequestOptions): Promise; + generateContentStream(request: GenerateContentRequest | string | Array, singleRequestOptions?: SingleRequestOptions): Promise; // (undocumented) generationConfig: GenerationConfig; // (undocumented) @@ -466,9 +466,9 @@ export interface ImagenInlineImage { // @beta export class ImagenModel extends VertexAIModel { constructor(vertexAI: VertexAI, modelParams: ImagenModelParams, requestOptions?: RequestOptions | undefined); - generateImages(prompt: string): Promise>; + generateImages(prompt: string, singleRequestOptions?: SingleRequestOptions): Promise>; // @internal - generateImagesGCS(prompt: string, gcsURI: string): Promise>; + generateImagesGCS(prompt: string, gcsURI: string, singleRequestOptions?: SingleRequestOptions): Promise>; generationConfig?: ImagenGenerationConfig; // (undocumented) requestOptions?: RequestOptions | undefined; @@ -731,6 +731,11 @@ export interface Segment { startIndex: number; } +// @public +export interface SingleRequestOptions extends RequestOptions { + signal?: AbortSignal; +} + // @public export interface StartChatParams extends BaseParams { // (undocumented) diff --git a/docs-devsite/_toc.yaml b/docs-devsite/_toc.yaml index 665222edb9d..0f5d51acec6 100644 --- a/docs-devsite/_toc.yaml +++ b/docs-devsite/_toc.yaml @@ -588,6 +588,8 @@ toc: path: /docs/reference/js/vertexai.schemashared.md - title: Segment path: /docs/reference/js/vertexai.segment.md + - title: SingleRequestOptions + path: /docs/reference/js/vertexai.singlerequestoptions.md - title: StartChatParams path: /docs/reference/js/vertexai.startchatparams.md - title: StringSchema diff --git a/docs-devsite/vertexai.chatsession.md b/docs-devsite/vertexai.chatsession.md index ed359f7e08c..409717749ea 100644 --- a/docs-devsite/vertexai.chatsession.md +++ b/docs-devsite/vertexai.chatsession.md @@ -37,8 +37,8 @@ export declare class ChatSession | Method | Modifiers | Description | | --- | --- | --- | | [getHistory()](./vertexai.chatsession.md#chatsessiongethistory) | | Gets the chat history so far. Blocked prompts are not added to history. Neither blocked candidates nor the prompts that generated them are added to history. | -| [sendMessage(request)](./vertexai.chatsession.md#chatsessionsendmessage) | | Sends a chat message and receives a non-streaming [GenerateContentResult](./vertexai.generatecontentresult.md#generatecontentresult_interface) | -| [sendMessageStream(request)](./vertexai.chatsession.md#chatsessionsendmessagestream) | | Sends a chat message and receives the response as a [GenerateContentStreamResult](./vertexai.generatecontentstreamresult.md#generatecontentstreamresult_interface) containing an iterable stream and a response promise. | +| [sendMessage(request, singleRequestOptions)](./vertexai.chatsession.md#chatsessionsendmessage) | | Sends a chat message and receives a non-streaming [GenerateContentResult](./vertexai.generatecontentresult.md#generatecontentresult_interface) | +| [sendMessageStream(request, singleRequestOptions)](./vertexai.chatsession.md#chatsessionsendmessagestream) | | Sends a chat message and receives the response as a [GenerateContentStreamResult](./vertexai.generatecontentstreamresult.md#generatecontentstreamresult_interface) containing an iterable stream and a response promise. | ## ChatSession.(constructor) @@ -103,7 +103,7 @@ Sends a chat message and receives a non-streaming [GenerateContentResult](./vert Signature: ```typescript -sendMessage(request: string | Array): Promise; +sendMessage(request: string | Array, singleRequestOptions?: SingleRequestOptions): Promise; ``` #### Parameters @@ -111,6 +111,7 @@ sendMessage(request: string | Array): Promise> | | +| singleRequestOptions | [SingleRequestOptions](./vertexai.singlerequestoptions.md#singlerequestoptions_interface) | | Returns: @@ -123,7 +124,7 @@ Sends a chat message and receives the response as a [GenerateContentStreamResult Signature: ```typescript -sendMessageStream(request: string | Array): Promise; +sendMessageStream(request: string | Array, singleRequestOptions?: SingleRequestOptions): Promise; ``` #### Parameters @@ -131,6 +132,7 @@ sendMessageStream(request: string | Array): Promise> | | +| singleRequestOptions | [SingleRequestOptions](./vertexai.singlerequestoptions.md#singlerequestoptions_interface) | | Returns: diff --git a/docs-devsite/vertexai.generativemodel.md b/docs-devsite/vertexai.generativemodel.md index e4a238b0af5..0b5d0d3ec25 100644 --- a/docs-devsite/vertexai.generativemodel.md +++ b/docs-devsite/vertexai.generativemodel.md @@ -40,9 +40,9 @@ export declare class GenerativeModel extends VertexAIModel | Method | Modifiers | Description | | --- | --- | --- | -| [countTokens(request)](./vertexai.generativemodel.md#generativemodelcounttokens) | | Counts the tokens in the provided request. | -| [generateContent(request)](./vertexai.generativemodel.md#generativemodelgeneratecontent) | | Makes a single non-streaming call to the model and returns an object containing a single [GenerateContentResponse](./vertexai.generatecontentresponse.md#generatecontentresponse_interface). | -| [generateContentStream(request)](./vertexai.generativemodel.md#generativemodelgeneratecontentstream) | | Makes a single streaming call to the model and returns an object containing an iterable stream that iterates over all chunks in the streaming response as well as a promise that returns the final aggregated response. | +| [countTokens(request, singleRequestOptions)](./vertexai.generativemodel.md#generativemodelcounttokens) | | Counts the tokens in the provided request. | +| [generateContent(request, singleRequestOptions)](./vertexai.generativemodel.md#generativemodelgeneratecontent) | | Makes a single non-streaming call to the model and returns an object containing a single [GenerateContentResponse](./vertexai.generatecontentresponse.md#generatecontentresponse_interface). | +| [generateContentStream(request, singleRequestOptions)](./vertexai.generativemodel.md#generativemodelgeneratecontentstream) | | Makes a single streaming call to the model and returns an object containing an iterable stream that iterates over all chunks in the streaming response as well as a promise that returns the final aggregated response. | | [startChat(startChatParams)](./vertexai.generativemodel.md#generativemodelstartchat) | | Gets a new [ChatSession](./vertexai.chatsession.md#chatsession_class) instance which can be used for multi-turn chats. | ## GenerativeModel.(constructor) @@ -118,7 +118,7 @@ Counts the tokens in the provided request. Signature: ```typescript -countTokens(request: CountTokensRequest | string | Array): Promise; +countTokens(request: CountTokensRequest | string | Array, singleRequestOptions?: SingleRequestOptions): Promise; ``` #### Parameters @@ -126,6 +126,7 @@ countTokens(request: CountTokensRequest | string | Array): Promis | Parameter | Type | Description | | --- | --- | --- | | request | [CountTokensRequest](./vertexai.counttokensrequest.md#counttokensrequest_interface) \| string \| Array<string \| [Part](./vertexai.md#part)> | | +| singleRequestOptions | [SingleRequestOptions](./vertexai.singlerequestoptions.md#singlerequestoptions_interface) | | Returns: @@ -138,7 +139,7 @@ Makes a single non-streaming call to the model and returns an object containing Signature: ```typescript -generateContent(request: GenerateContentRequest | string | Array): Promise; +generateContent(request: GenerateContentRequest | string | Array, singleRequestOptions?: SingleRequestOptions): Promise; ``` #### Parameters @@ -146,6 +147,7 @@ generateContent(request: GenerateContentRequest | string | Array) | Parameter | Type | Description | | --- | --- | --- | | request | [GenerateContentRequest](./vertexai.generatecontentrequest.md#generatecontentrequest_interface) \| string \| Array<string \| [Part](./vertexai.md#part)> | | +| singleRequestOptions | [SingleRequestOptions](./vertexai.singlerequestoptions.md#singlerequestoptions_interface) | | Returns: @@ -158,7 +160,7 @@ Makes a single streaming call to the model and returns an object containing an i Signature: ```typescript -generateContentStream(request: GenerateContentRequest | string | Array): Promise; +generateContentStream(request: GenerateContentRequest | string | Array, singleRequestOptions?: SingleRequestOptions): Promise; ``` #### Parameters @@ -166,6 +168,7 @@ generateContentStream(request: GenerateContentRequest | string | Array> | | +| singleRequestOptions | [SingleRequestOptions](./vertexai.singlerequestoptions.md#singlerequestoptions_interface) | | Returns: diff --git a/docs-devsite/vertexai.imagenmodel.md b/docs-devsite/vertexai.imagenmodel.md index ed40dc8f578..b3bbc2f0993 100644 --- a/docs-devsite/vertexai.imagenmodel.md +++ b/docs-devsite/vertexai.imagenmodel.md @@ -42,7 +42,7 @@ export declare class ImagenModel extends VertexAIModel | Method | Modifiers | Description | | --- | --- | --- | -| [generateImages(prompt)](./vertexai.imagenmodel.md#imagenmodelgenerateimages) | | (Public Preview) Generates images using the Imagen model and returns them as base64-encoded strings. | +| [generateImages(prompt, singleRequestOptions)](./vertexai.imagenmodel.md#imagenmodelgenerateimages) | | (Public Preview) Generates images using the Imagen model and returns them as base64-encoded strings. | ## ImagenModel.(constructor) @@ -118,7 +118,7 @@ If the prompt was not blocked, but one or more of the generated images were filt Signature: ```typescript -generateImages(prompt: string): Promise>; +generateImages(prompt: string, singleRequestOptions?: SingleRequestOptions): Promise>; ``` #### Parameters @@ -126,6 +126,7 @@ generateImages(prompt: string): PromiseReturns: diff --git a/docs-devsite/vertexai.md b/docs-devsite/vertexai.md index 47d45a492ec..7c61852e484 100644 --- a/docs-devsite/vertexai.md +++ b/docs-devsite/vertexai.md @@ -111,6 +111,7 @@ The Vertex AI in Firebase Web SDK. | [SchemaRequest](./vertexai.schemarequest.md#schemarequest_interface) | Final format for [Schema](./vertexai.schema.md#schema_class) params passed to backend requests. | | [SchemaShared](./vertexai.schemashared.md#schemashared_interface) | Basic [Schema](./vertexai.schema.md#schema_class) properties shared across several Schema-related types. | | [Segment](./vertexai.segment.md#segment_interface) | | +| [SingleRequestOptions](./vertexai.singlerequestoptions.md#singlerequestoptions_interface) | Options that can be provided per-request. Extends the base [RequestOptions](./vertexai.requestoptions.md#requestoptions_interface) (like timeout and baseUrl) with request-specific controls like cancellation via AbortSignal.Options specified here will override any default [RequestOptions](./vertexai.requestoptions.md#requestoptions_interface) configured on a model (e.g. [GenerativeModel](./vertexai.generativemodel.md#generativemodel_class)). | | [StartChatParams](./vertexai.startchatparams.md#startchatparams_interface) | Params for [GenerativeModel.startChat()](./vertexai.generativemodel.md#generativemodelstartchat). | | [TextPart](./vertexai.textpart.md#textpart_interface) | Content part interface if the part represents a text string. | | [ToolConfig](./vertexai.toolconfig.md#toolconfig_interface) | Tool config. This config is shared for all tools provided in the request. | diff --git a/docs-devsite/vertexai.singlerequestoptions.md b/docs-devsite/vertexai.singlerequestoptions.md new file mode 100644 index 00000000000..af5d46059fd --- /dev/null +++ b/docs-devsite/vertexai.singlerequestoptions.md @@ -0,0 +1,57 @@ +Project: /docs/reference/js/_project.yaml +Book: /docs/reference/_book.yaml +page_type: reference + +{% comment %} +DO NOT EDIT THIS FILE! +This is generated by the JS SDK team, and any local changes will be +overwritten. Changes should be made in the source code at +https://github.com/firebase/firebase-js-sdk +{% endcomment %} + +# SingleRequestOptions interface +Options that can be provided per-request. Extends the base [RequestOptions](./vertexai.requestoptions.md#requestoptions_interface) (like `timeout` and `baseUrl`) with request-specific controls like cancellation via `AbortSignal`. + +Options specified here will override any default [RequestOptions](./vertexai.requestoptions.md#requestoptions_interface) configured on a model (e.g. [GenerativeModel](./vertexai.generativemodel.md#generativemodel_class)). + +Signature: + +```typescript +export interface SingleRequestOptions extends RequestOptions +``` +Extends: [RequestOptions](./vertexai.requestoptions.md#requestoptions_interface) + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [signal](./vertexai.singlerequestoptions.md#singlerequestoptionssignal) | AbortSignal | An AbortSignal instance that allows cancelling ongoing requests (like generateContent or generateImages).If provided, calling abort() on the corresponding AbortController will attempt to cancel the underlying HTTP request. An AbortError will be thrown if cancellation is successful.Note that this will not cancel the request in the backend, so billing will still be applied despite cancellation. | + +## SingleRequestOptions.signal + +An `AbortSignal` instance that allows cancelling ongoing requests (like `generateContent` or `generateImages`). + +If provided, calling `abort()` on the corresponding `AbortController` will attempt to cancel the underlying HTTP request. An `AbortError` will be thrown if cancellation is successful. + +Note that this will not cancel the request in the backend, so billing will still be applied despite cancellation. + +Signature: + +```typescript +signal?: AbortSignal; +``` + +### Example + + +```javascript +const controller = new AbortController(); +const model = getGenerativeModel({ + // ... +}); + +// To cancel request: +controller.abort(); + +``` + diff --git a/packages/vertexai/src/methods/chat-session.test.ts b/packages/vertexai/src/methods/chat-session.test.ts index bd389a3d778..b4a19538c58 100644 --- a/packages/vertexai/src/methods/chat-session.test.ts +++ b/packages/vertexai/src/methods/chat-session.test.ts @@ -52,6 +52,64 @@ describe('ChatSession', () => { match.any ); }); + it('singleRequestOptions overrides requestOptions', async () => { + const generateContentStub = stub( + generateContentMethods, + 'generateContent' + ).rejects('generateContent failed'); // not important + const requestOptions = { + timeout: 1000 + }; + const singleRequestOptions = { + timeout: 2000 + }; + const chatSession = new ChatSession( + fakeApiSettings, + 'a-model', + undefined, + requestOptions + ); + await expect(chatSession.sendMessage('hello', singleRequestOptions)).to.be + .rejected; + expect(generateContentStub).to.be.calledWith( + fakeApiSettings, + 'a-model', + match.any, + match({ + timeout: singleRequestOptions.timeout + }) + ); + }); + it('singleRequestOptions is merged with requestOptions', async () => { + const generateContentStub = stub( + generateContentMethods, + 'generateContent' + ).rejects('generateContent failed'); // not important + const abortController = new AbortController(); + const requestOptions = { + timeout: 1000 + }; + const singleRequestOptions = { + signal: abortController.signal + }; + const chatSession = new ChatSession( + fakeApiSettings, + 'a-model', + undefined, + requestOptions + ); + await expect(chatSession.sendMessage('hello', singleRequestOptions)).to.be + .rejected; + expect(generateContentStub).to.be.calledWith( + fakeApiSettings, + 'a-model', + match.any, + match({ + timeout: requestOptions.timeout, + signal: singleRequestOptions.signal + }) + ); + }); }); describe('sendMessageStream()', () => { it('generateContentStream errors should be catchable', async () => { @@ -94,5 +152,63 @@ describe('ChatSession', () => { ); clock.restore(); }); + it('singleRequestOptions overrides requestOptions', async () => { + const generateContentStub = stub( + generateContentMethods, + 'generateContent' + ).rejects('generateContent failed'); // not important + const requestOptions = { + timeout: 1000 + }; + const singleRequestOptions = { + timeout: 2000 + }; + const chatSession = new ChatSession( + fakeApiSettings, + 'a-model', + undefined, + requestOptions + ); + await expect(chatSession.sendMessage('hello', singleRequestOptions)).to.be + .rejected; + expect(generateContentStub).to.be.calledWith( + fakeApiSettings, + 'a-model', + match.any, + match({ + timeout: singleRequestOptions.timeout + }) + ); + }); + it('singleRequestOptions is merged with requestOptions', async () => { + const generateContentStub = stub( + generateContentMethods, + 'generateContent' + ).rejects('generateContent failed'); // not important + const abortController = new AbortController(); + const requestOptions = { + timeout: 1000 + }; + const singleRequestOptions = { + signal: abortController.signal + }; + const chatSession = new ChatSession( + fakeApiSettings, + 'a-model', + undefined, + requestOptions + ); + await expect(chatSession.sendMessage('hello', singleRequestOptions)).to.be + .rejected; + expect(generateContentStub).to.be.calledWith( + fakeApiSettings, + 'a-model', + match.any, + match({ + timeout: requestOptions.timeout, + signal: singleRequestOptions.signal + }) + ); + }); }); }); diff --git a/packages/vertexai/src/methods/chat-session.ts b/packages/vertexai/src/methods/chat-session.ts index 60794001e37..43ea0afb692 100644 --- a/packages/vertexai/src/methods/chat-session.ts +++ b/packages/vertexai/src/methods/chat-session.ts @@ -22,6 +22,7 @@ import { GenerateContentStreamResult, Part, RequestOptions, + SingleRequestOptions, StartChatParams } from '../types'; import { formatNewContent } from '../requests/request-helpers'; @@ -75,7 +76,8 @@ export class ChatSession { * {@link GenerateContentResult} */ async sendMessage( - request: string | Array + request: string | Array, + singleRequestOptions?: SingleRequestOptions ): Promise { await this._sendPromise; const newContent = formatNewContent(request); @@ -95,7 +97,11 @@ export class ChatSession { this._apiSettings, this.model, generateContentRequest, - this.requestOptions + // Merge requestOptions + { + ...this.requestOptions, + ...singleRequestOptions + } ) ) .then(result => { @@ -130,7 +136,8 @@ export class ChatSession { * and a response promise. */ async sendMessageStream( - request: string | Array + request: string | Array, + singleRequestOptions?: SingleRequestOptions ): Promise { await this._sendPromise; const newContent = formatNewContent(request); @@ -146,7 +153,11 @@ export class ChatSession { this._apiSettings, this.model, generateContentRequest, - this.requestOptions + // Merge requestOptions + { + ...this.requestOptions, + ...singleRequestOptions + } ); // Add onto the chain. diff --git a/packages/vertexai/src/methods/count-tokens.ts b/packages/vertexai/src/methods/count-tokens.ts index c9d43a5b6fd..b89dab44e64 100644 --- a/packages/vertexai/src/methods/count-tokens.ts +++ b/packages/vertexai/src/methods/count-tokens.ts @@ -18,7 +18,7 @@ import { CountTokensRequest, CountTokensResponse, - RequestOptions + SingleRequestOptions } from '../types'; import { Task, makeRequest } from '../requests/request'; import { ApiSettings } from '../types/internal'; @@ -27,7 +27,7 @@ export async function countTokens( apiSettings: ApiSettings, model: string, params: CountTokensRequest, - requestOptions?: RequestOptions + singleRequestOptions?: SingleRequestOptions ): Promise { const response = await makeRequest( model, @@ -35,7 +35,7 @@ export async function countTokens( apiSettings, false, JSON.stringify(params), - requestOptions + singleRequestOptions ); return response.json(); } diff --git a/packages/vertexai/src/methods/generate-content.ts b/packages/vertexai/src/methods/generate-content.ts index 0944b38016a..c67c36ac1d2 100644 --- a/packages/vertexai/src/methods/generate-content.ts +++ b/packages/vertexai/src/methods/generate-content.ts @@ -20,7 +20,7 @@ import { GenerateContentResponse, GenerateContentResult, GenerateContentStreamResult, - RequestOptions + SingleRequestOptions } from '../types'; import { Task, makeRequest } from '../requests/request'; import { createEnhancedContentResponse } from '../requests/response-helpers'; @@ -31,7 +31,7 @@ export async function generateContentStream( apiSettings: ApiSettings, model: string, params: GenerateContentRequest, - requestOptions?: RequestOptions + singleRequestOptions?: SingleRequestOptions ): Promise { const response = await makeRequest( model, @@ -39,7 +39,7 @@ export async function generateContentStream( apiSettings, /* stream */ true, JSON.stringify(params), - requestOptions + singleRequestOptions ); return processStream(response); } @@ -48,7 +48,7 @@ export async function generateContent( apiSettings: ApiSettings, model: string, params: GenerateContentRequest, - requestOptions?: RequestOptions + singleRequestOptions?: SingleRequestOptions ): Promise { const response = await makeRequest( model, @@ -56,7 +56,7 @@ export async function generateContent( apiSettings, /* stream */ false, JSON.stringify(params), - requestOptions + singleRequestOptions ); const responseJson: GenerateContentResponse = await response.json(); const enhancedResponse = createEnhancedContentResponse(responseJson); diff --git a/packages/vertexai/src/models/generative-model.test.ts b/packages/vertexai/src/models/generative-model.test.ts index 51ea8aafead..88ffaa82b1a 100644 --- a/packages/vertexai/src/models/generative-model.test.ts +++ b/packages/vertexai/src/models/generative-model.test.ts @@ -15,14 +15,18 @@ * limitations under the License. */ import { use, expect } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; import { GenerativeModel } from './generative-model'; import { FunctionCallingMode, VertexAI } from '../public-types'; import * as request from '../requests/request'; import { match, restore, stub } from 'sinon'; import { getMockResponse } from '../../test-utils/mock-response'; import sinonChai from 'sinon-chai'; +import * as generateContentMethods from '../methods/generate-content'; +import * as countTokens from '../methods/count-tokens'; use(sinonChai); +use(chaiAsPromised); const fakeVertexAI: VertexAI = { app: { @@ -38,6 +42,9 @@ const fakeVertexAI: VertexAI = { }; describe('GenerativeModel', () => { + afterEach(() => { + restore(); + }); it('passes params through to generateContent', async () => { const genModel = new GenerativeModel(fakeVertexAI, { model: 'my-model', @@ -165,6 +172,62 @@ describe('GenerativeModel', () => { ); restore(); }); + it('generateContent singleRequestOptions overrides requestOptions', async () => { + const generateContentStub = stub( + generateContentMethods, + 'generateContent' + ).rejects('generateContent failed'); // not important + const requestOptions = { + timeout: 1000 + }; + const singleRequestOptions = { + timeout: 2000 + }; + const genModel = new GenerativeModel( + fakeVertexAI, + { model: 'my-model' }, + requestOptions + ); + await expect(genModel.generateContent('hello', singleRequestOptions)).to.be + .rejected; + expect(generateContentStub).to.be.calledWith( + match.any, + match.any, + match.any, + match({ + timeout: singleRequestOptions.timeout + }) + ); + }); + it('generateContent singleRequestOptions is merged with requestOptions', async () => { + const generateContentStub = stub( + generateContentMethods, + 'generateContent' + ).rejects('generateContent failed'); // not important + const abortController = new AbortController(); + const requestOptions = { + timeout: 1000 + }; + const singleRequestOptions = { + signal: abortController.signal + }; + const genModel = new GenerativeModel( + fakeVertexAI, + { model: 'my-model' }, + requestOptions + ); + await expect(genModel.generateContent('hello', singleRequestOptions)).to.be + .rejected; + expect(generateContentStub).to.be.calledWith( + match.any, + match.any, + match.any, + match({ + timeout: requestOptions.timeout, + signal: singleRequestOptions.signal + }) + ); + }); it('passes params through to chat.sendMessage', async () => { const genModel = new GenerativeModel(fakeVertexAI, { model: 'my-model', @@ -314,4 +377,57 @@ describe('GenerativeModel', () => { ); restore(); }); + it('countTokens singleRequestOptions overrides requestOptions', async () => { + const countTokensStub = stub(countTokens, 'countTokens').rejects( + 'countTokens failed' + ); + const requestOptions = { + timeout: 1000 + }; + const singleRequestOptions = { + timeout: 2000 + }; + const genModel = new GenerativeModel( + fakeVertexAI, + { model: 'my-model' }, + requestOptions + ); + await expect(genModel.countTokens('hello', singleRequestOptions)).to.be.rejected; + expect(countTokensStub).to.be.calledWith( + match.any, + match.any, + match.any, + match({ + timeout: singleRequestOptions.timeout + }) + ); + }); + it('countTokens singleRequestOptions is merged with requestOptions', async () => { + const countTokensStub = stub(countTokens, 'countTokens').rejects( + 'countTokens failed' + ); + const abortController = new AbortController(); + const requestOptions = { + timeout: 1000 + }; + const singleRequestOptions = { + signal: abortController.signal + }; + const genModel = new GenerativeModel( + fakeVertexAI, + { model: 'my-model' }, + requestOptions + ); + await expect(genModel.countTokens('hello', singleRequestOptions)).to.be + .rejected; + expect(countTokensStub).to.be.calledWith( + match.any, + match.any, + match.any, + match({ + timeout: requestOptions.timeout, + signal: singleRequestOptions.signal + }) + ); + }); }); diff --git a/packages/vertexai/src/models/generative-model.ts b/packages/vertexai/src/models/generative-model.ts index 1af1ee700d5..6d5ae38d967 100644 --- a/packages/vertexai/src/models/generative-model.ts +++ b/packages/vertexai/src/models/generative-model.ts @@ -29,11 +29,12 @@ import { GenerationConfig, ModelParams, Part, - RequestOptions, SafetySetting, + RequestOptions, StartChatParams, Tool, - ToolConfig + ToolConfig, + SingleRequestOptions } from '../types'; import { ChatSession } from '../methods/chat-session'; import { countTokens } from '../methods/count-tokens'; @@ -77,7 +78,8 @@ export class GenerativeModel extends VertexAIModel { * and returns an object containing a single {@link GenerateContentResponse}. */ async generateContent( - request: GenerateContentRequest | string | Array + request: GenerateContentRequest | string | Array, + singleRequestOptions?: SingleRequestOptions ): Promise { const formattedParams = formatGenerateContentInput(request); return generateContent( @@ -91,7 +93,11 @@ export class GenerativeModel extends VertexAIModel { systemInstruction: this.systemInstruction, ...formattedParams }, - this.requestOptions + // Merge request options + { + ...this.requestOptions, + ...singleRequestOptions + } ); } @@ -102,7 +108,8 @@ export class GenerativeModel extends VertexAIModel { * a promise that returns the final aggregated response. */ async generateContentStream( - request: GenerateContentRequest | string | Array + request: GenerateContentRequest | string | Array, + singleRequestOptions?: SingleRequestOptions ): Promise { const formattedParams = formatGenerateContentInput(request); return generateContentStream( @@ -116,7 +123,11 @@ export class GenerativeModel extends VertexAIModel { systemInstruction: this.systemInstruction, ...formattedParams }, - this.requestOptions + // Merge request options + { + ...this.requestOptions, + ...singleRequestOptions + } ); } @@ -149,9 +160,19 @@ export class GenerativeModel extends VertexAIModel { * Counts the tokens in the provided request. */ async countTokens( - request: CountTokensRequest | string | Array + request: CountTokensRequest | string | Array, + singleRequestOptions?: SingleRequestOptions ): Promise { const formattedParams = formatGenerateContentInput(request); - return countTokens(this._apiSettings, this.model, formattedParams); + return countTokens( + this._apiSettings, + this.model, + formattedParams, + // Merge request options + { + ...this.requestOptions, + ...singleRequestOptions + } + ); } } diff --git a/packages/vertexai/src/models/imagen-model.test.ts b/packages/vertexai/src/models/imagen-model.test.ts index 9e534f2195a..10842be800d 100644 --- a/packages/vertexai/src/models/imagen-model.test.ts +++ b/packages/vertexai/src/models/imagen-model.test.ts @@ -45,6 +45,9 @@ const fakeVertexAI: VertexAI = { }; describe('ImagenModel', () => { + afterEach(() => { + restore(); + }); it('generateImages makes a request to predict with default parameters', async () => { const mockResponse = getMockResponse( 'vertexAI', @@ -70,9 +73,8 @@ describe('ImagenModel', () => { value.includes(`"sampleCount":1`) ); }), - undefined + {} ); - restore(); }); it('generateImages makes a request to predict with generation config and safety settings', async () => { const imagenModel = new ImagenModel(fakeVertexAI, { @@ -129,9 +131,74 @@ describe('ImagenModel', () => { ) ); }), - undefined + {} + ); + }); + it('generateImages singleRequestOptions overrides requestOptions', async () => { + const requestOptions = { + timeout: 1000 + }; + const singleRequestOptions = { + timeout: 2000 + }; + const imagenModel = new ImagenModel( + fakeVertexAI, + { model: 'my-model' }, + requestOptions + ); + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-generate-images-base64.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const prompt = 'A photorealistic image of a toy boat at sea.'; + await imagenModel.generateImages(prompt, singleRequestOptions); + expect(makeRequestStub).to.be.calledWith( + match.any, + request.Task.PREDICT, + match.any, + false, + match.any, + match({ + timeout: singleRequestOptions.timeout + }) + ); + }); + it('generateImages singleRequestOptions is merged with requestOptions', async () => { + const abortController = new AbortController(); + const requestOptions = { + timeout: 1000 + }; + const singleRequestOptions = { + signal: abortController.signal + }; + const imagenModel = new ImagenModel( + fakeVertexAI, + { model: 'my-model' }, + requestOptions + ); + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-generate-images-base64.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const prompt = 'A photorealistic image of a toy boat at sea.'; + await imagenModel.generateImages(prompt, singleRequestOptions); + expect(makeRequestStub).to.be.calledWith( + match.any, + request.Task.PREDICT, + match.any, + false, + match.any, + match({ + timeout: requestOptions.timeout, + signal: singleRequestOptions.signal + }) ); - restore(); }); it('throws if prompt blocked', async () => { const mockResponse = getMockResponse( @@ -157,8 +224,72 @@ describe('ImagenModel', () => { expect((e as VertexAIError).message).to.include( "Image generation failed with the following error: The prompt could not be submitted. This prompt contains sensitive words that violate Google's Responsible AI practices. Try rephrasing the prompt. If you think this was an error, send feedback." ); - } finally { - restore(); } }); + it('generateImagesGCS singleRequestOptions overrides requestOptions', async () => { + const requestOptions = { + timeout: 1000 + }; + const singleRequestOptions = { + timeout: 2000 + }; + const imagenModel = new ImagenModel( + fakeVertexAI, + { model: 'my-model' }, + requestOptions + ); + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-generate-images-gcs.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const prompt = 'A photorealistic image of a toy boat at sea.'; + await imagenModel.generateImagesGCS(prompt, '', singleRequestOptions); + expect(makeRequestStub).to.be.calledWith( + match.any, + request.Task.PREDICT, + match.any, + false, + match.any, + match({ + timeout: singleRequestOptions.timeout + }) + ); + }); + it('generateImages singleRequestOptions is merged with requestOptions', async () => { + const abortController = new AbortController(); + const requestOptions = { + timeout: 1000 + }; + const singleRequestOptions = { + signal: abortController.signal + }; + const imagenModel = new ImagenModel( + fakeVertexAI, + { model: 'my-model' }, + requestOptions + ); + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-generate-images-gcs.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const prompt = 'A photorealistic image of a toy boat at sea.'; + await imagenModel.generateImagesGCS(prompt, '', singleRequestOptions); + expect(makeRequestStub).to.be.calledWith( + match.any, + request.Task.PREDICT, + match.any, + false, + match.any, + match({ + timeout: requestOptions.timeout, + signal: singleRequestOptions.signal + }) + ); + }); }); diff --git a/packages/vertexai/src/models/imagen-model.ts b/packages/vertexai/src/models/imagen-model.ts index 04514ef6ffd..e74a3b1f2fd 100644 --- a/packages/vertexai/src/models/imagen-model.ts +++ b/packages/vertexai/src/models/imagen-model.ts @@ -26,7 +26,8 @@ import { RequestOptions, ImagenModelParams, ImagenGenerationResponse, - ImagenSafetySettings + ImagenSafetySettings, + SingleRequestOptions } from '../types'; import { VertexAIModel } from './vertexai-model'; @@ -102,7 +103,8 @@ export class ImagenModel extends VertexAIModel { * @beta */ async generateImages( - prompt: string + prompt: string, + singleRequestOptions?: SingleRequestOptions ): Promise> { const body = createPredictRequestBody(prompt, { ...this.generationConfig, @@ -114,7 +116,11 @@ export class ImagenModel extends VertexAIModel { this._apiSettings, /* stream */ false, JSON.stringify(body), - this.requestOptions + // Merge request options + { + ...this.requestOptions, + ...singleRequestOptions + } ); return handlePredictResponse(response); } @@ -140,7 +146,8 @@ export class ImagenModel extends VertexAIModel { */ async generateImagesGCS( prompt: string, - gcsURI: string + gcsURI: string, + singleRequestOptions?: SingleRequestOptions ): Promise> { const body = createPredictRequestBody(prompt, { gcsURI, @@ -153,7 +160,10 @@ export class ImagenModel extends VertexAIModel { this._apiSettings, /* stream */ false, JSON.stringify(body), - this.requestOptions + { + ...this.requestOptions, + ...singleRequestOptions + } ); return handlePredictResponse(response); } diff --git a/packages/vertexai/src/requests/request.test.ts b/packages/vertexai/src/requests/request.test.ts index cd39a0f8ae5..f1835a658db 100644 --- a/packages/vertexai/src/requests/request.test.ts +++ b/packages/vertexai/src/requests/request.test.ts @@ -16,7 +16,7 @@ */ import { expect, use } from 'chai'; -import { match, restore, stub } from 'sinon'; +import Sinon, { match, restore, stub, useFakeTimers } from 'sinon'; import sinonChai from 'sinon-chai'; import chaiAsPromised from 'chai-as-promised'; import { RequestUrl, Task, getHeaders, makeRequest } from './request'; @@ -269,8 +269,37 @@ describe('request methods', () => { }); }); describe('makeRequest', () => { + let fetchStub: Sinon.SinonStub; + let clock: Sinon.SinonFakeTimers; + const fetchAborter = ( + _url: string, + options?: RequestInit + ): Promise => { + expect(options).to.not.be.undefined; + expect(options!.signal).to.not.be.undefined; + const signal = options!.signal; + console.log(signal); + return new Promise((_resolve, reject): void => { + const abortListener = (): void => { + reject(new DOMException(signal?.reason || 'Aborted', 'AbortError')); + }; + + signal?.addEventListener('abort', abortListener, { once: true }); + }); + }; + + beforeEach(() => { + fetchStub = stub(globalThis, 'fetch'); + clock = useFakeTimers(); + }); + + afterEach(() => { + restore(); + clock.restore(); + }); + it('no error', async () => { - const fetchStub = stub(globalThis, 'fetch').resolves({ + fetchStub.resolves({ ok: true } as Response); const response = await makeRequest( @@ -284,7 +313,7 @@ describe('request methods', () => { expect(response.ok).to.be.true; }); it('error with timeout', async () => { - const fetchStub = stub(globalThis, 'fetch').resolves({ + fetchStub.resolves({ ok: false, status: 500, statusText: 'AbortError' @@ -315,7 +344,7 @@ describe('request methods', () => { expect(fetchStub).to.be.calledOnce; }); it('Network error, no response.json()', async () => { - const fetchStub = stub(globalThis, 'fetch').resolves({ + fetchStub.resolves({ ok: false, status: 500, statusText: 'Server Error' @@ -341,7 +370,7 @@ describe('request methods', () => { expect(fetchStub).to.be.calledOnce; }); it('Network error, includes response.json()', async () => { - const fetchStub = stub(globalThis, 'fetch').resolves({ + fetchStub.resolves({ ok: false, status: 500, statusText: 'Server Error', @@ -369,7 +398,7 @@ describe('request methods', () => { expect(fetchStub).to.be.calledOnce; }); it('Network error, includes response.json() and details', async () => { - const fetchStub = stub(globalThis, 'fetch').resolves({ + fetchStub.resolves({ ok: false, status: 500, statusText: 'Server Error', @@ -411,30 +440,226 @@ describe('request methods', () => { } expect(fetchStub).to.be.calledOnce; }); - }); - it('Network error, API not enabled', async () => { - const mockResponse = getMockResponse( - 'vertexAI', - 'unary-failure-firebasevertexai-api-not-enabled.json' - ); - const fetchStub = stub(globalThis, 'fetch').resolves( - mockResponse as Response - ); - try { - await makeRequest( + it('Network error, API not enabled', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-failure-firebasevertexai-api-not-enabled.json' + ); + fetchStub.resolves(mockResponse as Response); + try { + await makeRequest( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + '' + ); + } catch (e) { + expect((e as VertexAIError).code).to.equal( + VertexAIErrorCode.API_NOT_ENABLED + ); + expect((e as VertexAIError).message).to.include('my-project'); + expect((e as VertexAIError).message).to.include('googleapis.com'); + } + expect(fetchStub).to.be.calledOnce; + }); + + it('should throw DOMException if external signal is already aborted', async () => { + const controller = new AbortController(); + const abortReason = 'Aborted before request'; + controller.abort(abortReason); + + const requestPromise = makeRequest( 'models/model-name', Task.GENERATE_CONTENT, fakeApiSettings, false, - '' + '{}', + { signal: controller.signal } + ); + + await expect(requestPromise).to.be.rejectedWith( + DOMException, + abortReason + ); + + expect(fetchStub).not.to.have.been.called; + }); + it('should abort fetch if external signal aborts during request', async () => { + fetchStub.callsFake(fetchAborter); + const controller = new AbortController(); + const abortReason = 'Aborted during request'; + + const requestPromise = makeRequest( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + '{}', + { signal: controller.signal } + ); + + await clock.tickAsync(0); + controller.abort(abortReason); + + await expect(requestPromise).to.be.rejectedWith( + VertexAIError, + `VertexAI: Error fetching from https://firebasevertexai.googleapis.com/v1beta/projects/my-project/locations/us-central1/models/model-name:generateContent: ${abortReason} (vertexAI/error)` ); - } catch (e) { - expect((e as VertexAIError).code).to.equal( - VertexAIErrorCode.API_NOT_ENABLED + }); + + it('should abort fetch if timeout expires during request', async () => { + const timeoutDuration = 100; + fetchStub.callsFake(fetchAborter); + + const requestPromise = makeRequest( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + '{}', + { timeout: timeoutDuration } ); - expect((e as VertexAIError).message).to.include('my-project'); - expect((e as VertexAIError).message).to.include('googleapis.com'); - } - expect(fetchStub).to.be.calledOnce; + + await clock.tickAsync(timeoutDuration + 100); + + await expect(requestPromise).to.be.rejectedWith( + VertexAIError, + /Timeout has expired/ + ); + + expect(fetchStub).to.have.been.calledOnce; + const fetchOptions = fetchStub.firstCall.args[1] as RequestInit; + const internalSignal = fetchOptions.signal; + + expect(internalSignal?.aborted).to.be.true; + expect(internalSignal?.reason).to.equal('Timeout has expired.'); + }); + + it('should succeed and clear timeout if fetch completes before timeout', async () => { + const mockResponse = new Response('{}', { + status: 200, + statusText: 'OK' + }); + const fetchPromise = Promise.resolve(mockResponse); + fetchStub.resolves(fetchPromise); + + const requestPromise = makeRequest( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + '{}', + { timeout: 5000 } // Generous timeout + ); + + // Advance time slightly, well within timeout + await clock.tickAsync(10); + + const response = await requestPromise; + expect(response.ok).to.be.true; + + expect(fetchStub).to.have.been.calledOnce; + }); + + it('should succeed and clear timeout/listener if fetch completes with signal provided but not aborted', async () => { + const controller = new AbortController(); + const mockResponse = new Response('{}', { + status: 200, + statusText: 'OK' + }); + const fetchPromise = Promise.resolve(mockResponse); + fetchStub.resolves(fetchPromise); + + const requestPromise = makeRequest( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + '{}', + { signal: controller.signal } + ); + + // Advance time slightly + await clock.tickAsync(10); + + const response = await requestPromise; + expect(response.ok).to.be.true; + expect(fetchStub).to.have.been.calledOnce; + }); + + it('should use external signal abort reason if it occurs before timeout', async () => { + const controller = new AbortController(); + const abortReason = 'External Abort Wins'; + const timeoutDuration = 500; + fetchStub.callsFake(fetchAborter); + + const requestPromise = makeRequest( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + '{}', + { signal: controller.signal, timeout: timeoutDuration } + ); + + // Advance time, but less than the timeout + await clock.tickAsync(timeoutDuration / 2); + controller.abort(abortReason); + + await expect(requestPromise).to.be.rejectedWith( + VertexAIError, + abortReason + ); + }); + + it('should use timeout reason if it occurs before external signal abort', async () => { + const controller = new AbortController(); + const abortReason = 'External Abort Loses'; + const timeoutDuration = 100; + fetchStub.callsFake(fetchAborter); + + const requestPromise = makeRequest( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + '{}', + { signal: controller.signal, timeout: timeoutDuration } + ); + + // Schedule external abort after timeout + setTimeout(() => controller.abort(abortReason), timeoutDuration * 2); + + // Advance time past the timeout + await clock.tickAsync(timeoutDuration + 1); + + await expect(requestPromise).to.be.rejectedWith( + VertexAIError, + /Timeout has expired/ + ); + }); + + it('should pass internal signal to fetch options', async () => { + const mockResponse = new Response('{}', { + status: 200, + statusText: 'OK' + }); + fetchStub.resolves(mockResponse); + + await makeRequest( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + '{}' + ); + + expect(fetchStub).to.have.been.calledOnce; + const fetchOptions = fetchStub.firstCall.args[1] as RequestInit; + expect(fetchOptions.signal).to.exist; + expect(fetchOptions.signal).to.be.instanceOf(AbortSignal); + expect(fetchOptions.signal?.aborted).to.be.false; + }); }); }); diff --git a/packages/vertexai/src/requests/request.ts b/packages/vertexai/src/requests/request.ts index 47e4c6ab446..002930cfbc8 100644 --- a/packages/vertexai/src/requests/request.ts +++ b/packages/vertexai/src/requests/request.ts @@ -15,7 +15,11 @@ * limitations under the License. */ -import { ErrorDetails, RequestOptions, VertexAIErrorCode } from '../types'; +import { + ErrorDetails, + SingleRequestOptions, + VertexAIErrorCode +} from '../types'; import { VertexAIError } from '../errors'; import { ApiSettings } from '../types/internal'; import { @@ -27,6 +31,9 @@ import { } from '../constants'; import { logger } from '../logger'; +const TIMEOUT_EXPIRED_MESSAGE = 'Timeout has expired.'; +const ABORT_ERROR_NAME = 'AbortError'; + export enum Task { GENERATE_CONTENT = 'generateContent', STREAM_GENERATE_CONTENT = 'streamGenerateContent', @@ -40,7 +47,7 @@ export class RequestUrl { public task: Task, public apiSettings: ApiSettings, public stream: boolean, - public requestOptions?: RequestOptions + public requestOptions?: SingleRequestOptions ) {} toString(): string { // TODO: allow user-set option if that feature becomes available @@ -115,9 +122,15 @@ export async function constructRequest( apiSettings: ApiSettings, stream: boolean, body: string, - requestOptions?: RequestOptions + singleRequestOptions?: SingleRequestOptions ): Promise<{ url: string; fetchOptions: RequestInit }> { - const url = new RequestUrl(model, task, apiSettings, stream, requestOptions); + const url = new RequestUrl( + model, + task, + apiSettings, + stream, + singleRequestOptions + ); return { url: url.toString(), fetchOptions: { @@ -134,11 +147,49 @@ export async function makeRequest( apiSettings: ApiSettings, stream: boolean, body: string, - requestOptions?: RequestOptions + singleRequestOptions?: SingleRequestOptions ): Promise { - const url = new RequestUrl(model, task, apiSettings, stream, requestOptions); + const url = new RequestUrl( + model, + task, + apiSettings, + stream, + singleRequestOptions + ); let response; - let fetchTimeoutId: string | number | NodeJS.Timeout | undefined; + + const externalSignal = singleRequestOptions?.signal; + const timeoutMillis = + singleRequestOptions?.timeout != null && singleRequestOptions.timeout >= 0 + ? singleRequestOptions.timeout + : DEFAULT_FETCH_TIMEOUT_MS; + const internalAbortController = new AbortController(); + const fetchTimeoutId = setTimeout(() => { + internalAbortController.abort(TIMEOUT_EXPIRED_MESSAGE); + logger.debug( + `Aborting request to ${url} due to timeout (${timeoutMillis}ms)` + ); + }, timeoutMillis); + + if (externalSignal) { + if (externalSignal.aborted) { + clearTimeout(fetchTimeoutId); + throw new DOMException( + externalSignal.reason ?? 'Aborted externally before fetch', + ABORT_ERROR_NAME + ); + } + + const externalAbortListener = (): void => { + logger.debug(`Aborting request to ${url} due to external abort signal.`); + internalAbortController.abort(externalSignal.reason); + }; + + externalSignal.addEventListener('abort', externalAbortListener, { + once: true + }); + } + try { const request = await constructRequest( model, @@ -146,16 +197,9 @@ export async function makeRequest( apiSettings, stream, body, - requestOptions + singleRequestOptions ); - // Timeout is 180s by default - const timeoutMillis = - requestOptions?.timeout != null && requestOptions.timeout >= 0 - ? requestOptions.timeout - : DEFAULT_FETCH_TIMEOUT_MS; - const abortController = new AbortController(); - fetchTimeoutId = setTimeout(() => abortController.abort(), timeoutMillis); - request.fetchOptions.signal = abortController.signal; + request.fetchOptions.signal = internalAbortController.signal; response = await fetch(request.url, request.fetchOptions); if (!response.ok) { diff --git a/packages/vertexai/src/types/requests.ts b/packages/vertexai/src/types/requests.ts index ee45b636673..d0d62785ad2 100644 --- a/packages/vertexai/src/types/requests.ts +++ b/packages/vertexai/src/types/requests.ts @@ -154,6 +154,43 @@ export interface RequestOptions { baseUrl?: string; } +/** + * Options that can be provided per-request. + * Extends the base {@link RequestOptions} (like `timeout` and `baseUrl`) + * with request-specific controls like cancellation via `AbortSignal`. + * + * Options specified here will override any default {@link RequestOptions} + * configured on a model (e.g. {@link GenerativeModel}). + * + * @public + */ +export interface SingleRequestOptions extends RequestOptions { + /** + * An `AbortSignal` instance that allows cancelling ongoing requests (like `generateContent` or + * `generateImages`). + * + * If provided, calling `abort()` on the corresponding `AbortController` + * will attempt to cancel the underlying HTTP request. An `AbortError` will be thrown + * if cancellation is successful. + * + * Note that this will not cancel the request in the backend, so billing will + * still be applied despite cancellation. + * + * @example + * ```javascript + * const controller = new AbortController(); + * const model = getGenerativeModel({ + * // ... + * }); + * + * // To cancel request: + * controller.abort(); + * ``` + * @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal + */ + signal?: AbortSignal; +} + /** * Defines a tool that model can call to access external knowledge. * @public From 54589ad8b8ef198245a899a719ca432aeee61cd9 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Mon, 12 May 2025 13:07:55 -0400 Subject: [PATCH 02/22] Run formatter --- packages/vertexai/src/models/generative-model.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/vertexai/src/models/generative-model.test.ts b/packages/vertexai/src/models/generative-model.test.ts index 88ffaa82b1a..27487d93617 100644 --- a/packages/vertexai/src/models/generative-model.test.ts +++ b/packages/vertexai/src/models/generative-model.test.ts @@ -392,7 +392,8 @@ describe('GenerativeModel', () => { { model: 'my-model' }, requestOptions ); - await expect(genModel.countTokens('hello', singleRequestOptions)).to.be.rejected; + await expect(genModel.countTokens('hello', singleRequestOptions)).to.be + .rejected; expect(countTokensStub).to.be.calledWith( match.any, match.any, From a82a809fc5d9824a87fefff8f87afcbd0acfd7b4 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Thu, 22 May 2025 11:27:25 -0400 Subject: [PATCH 03/22] Fix docs --- docs-devsite/_toc.yaml | 148 +---- docs-devsite/ai.chatsession.md | 22 +- docs-devsite/ai.generativemodel.md | 15 +- docs-devsite/ai.imagenmodel.md | 8 +- docs-devsite/ai.md | 1 + ...toptions.md => ai.singlerequestoptions.md} | 8 +- docs-devsite/vertexai.generativemodel.md | 196 ------ docs-devsite/vertexai.md | 592 ------------------ 8 files changed, 22 insertions(+), 968 deletions(-) rename docs-devsite/{vertexai.singlerequestoptions.md => ai.singlerequestoptions.md} (56%) delete mode 100644 docs-devsite/vertexai.generativemodel.md delete mode 100644 docs-devsite/vertexai.md diff --git a/docs-devsite/_toc.yaml b/docs-devsite/_toc.yaml index f5c99e77835..6e07ffa792b 100644 --- a/docs-devsite/_toc.yaml +++ b/docs-devsite/_toc.yaml @@ -132,6 +132,8 @@ toc: path: /docs/reference/js/ai.schemashared.md - title: Segment path: /docs/reference/js/ai.segment.md + - title: SingleRequestOptions + path: /docs/reference/js/ai.singlerequestoptions.md - title: StartChatParams path: /docs/reference/js/ai.startchatparams.md - title: StringSchema @@ -618,149 +620,3 @@ toc: path: /docs/reference/js/storage.uploadtask.md - title: UploadTaskSnapshot path: /docs/reference/js/storage.uploadtasksnapshot.md -<<<<<<< HEAD -- title: vertexai - path: /docs/reference/js/vertexai.md - section: - - title: ArraySchema - path: /docs/reference/js/vertexai.arrayschema.md - - title: BaseParams - path: /docs/reference/js/vertexai.baseparams.md - - title: BooleanSchema - path: /docs/reference/js/vertexai.booleanschema.md - - title: ChatSession - path: /docs/reference/js/vertexai.chatsession.md - - title: Citation - path: /docs/reference/js/vertexai.citation.md - - title: CitationMetadata - path: /docs/reference/js/vertexai.citationmetadata.md - - title: Content - path: /docs/reference/js/vertexai.content.md - - title: CountTokensRequest - path: /docs/reference/js/vertexai.counttokensrequest.md - - title: CountTokensResponse - path: /docs/reference/js/vertexai.counttokensresponse.md - - title: CustomErrorData - path: /docs/reference/js/vertexai.customerrordata.md - - title: Date_2 - path: /docs/reference/js/vertexai.date_2.md - - title: EnhancedGenerateContentResponse - path: /docs/reference/js/vertexai.enhancedgeneratecontentresponse.md - - title: ErrorDetails - path: /docs/reference/js/vertexai.errordetails.md - - title: FileData - path: /docs/reference/js/vertexai.filedata.md - - title: FileDataPart - path: /docs/reference/js/vertexai.filedatapart.md - - title: FunctionCall - path: /docs/reference/js/vertexai.functioncall.md - - title: FunctionCallingConfig - path: /docs/reference/js/vertexai.functioncallingconfig.md - - title: FunctionCallPart - path: /docs/reference/js/vertexai.functioncallpart.md - - title: FunctionDeclaration - path: /docs/reference/js/vertexai.functiondeclaration.md - - title: FunctionDeclarationsTool - path: /docs/reference/js/vertexai.functiondeclarationstool.md - - title: FunctionResponse - path: /docs/reference/js/vertexai.functionresponse.md - - title: FunctionResponsePart - path: /docs/reference/js/vertexai.functionresponsepart.md - - title: GenerateContentCandidate - path: /docs/reference/js/vertexai.generatecontentcandidate.md - - title: GenerateContentRequest - path: /docs/reference/js/vertexai.generatecontentrequest.md - - title: GenerateContentResponse - path: /docs/reference/js/vertexai.generatecontentresponse.md - - title: GenerateContentResult - path: /docs/reference/js/vertexai.generatecontentresult.md - - title: GenerateContentStreamResult - path: /docs/reference/js/vertexai.generatecontentstreamresult.md - - title: GenerationConfig - path: /docs/reference/js/vertexai.generationconfig.md - - title: GenerativeContentBlob - path: /docs/reference/js/vertexai.generativecontentblob.md - - title: GenerativeModel - path: /docs/reference/js/vertexai.generativemodel.md - - title: GroundingAttribution - path: /docs/reference/js/vertexai.groundingattribution.md - - title: GroundingMetadata - path: /docs/reference/js/vertexai.groundingmetadata.md - - title: ImagenGCSImage - path: /docs/reference/js/vertexai.imagengcsimage.md - - title: ImagenGenerationConfig - path: /docs/reference/js/vertexai.imagengenerationconfig.md - - title: ImagenGenerationResponse - path: /docs/reference/js/vertexai.imagengenerationresponse.md - - title: ImagenImageFormat - path: /docs/reference/js/vertexai.imagenimageformat.md - - title: ImagenInlineImage - path: /docs/reference/js/vertexai.imageninlineimage.md - - title: ImagenModel - path: /docs/reference/js/vertexai.imagenmodel.md - - title: ImagenModelParams - path: /docs/reference/js/vertexai.imagenmodelparams.md - - title: ImagenSafetySettings - path: /docs/reference/js/vertexai.imagensafetysettings.md - - title: InlineDataPart - path: /docs/reference/js/vertexai.inlinedatapart.md - - title: IntegerSchema - path: /docs/reference/js/vertexai.integerschema.md - - title: ModalityTokenCount - path: /docs/reference/js/vertexai.modalitytokencount.md - - title: ModelParams - path: /docs/reference/js/vertexai.modelparams.md - - title: NumberSchema - path: /docs/reference/js/vertexai.numberschema.md - - title: ObjectSchema - path: /docs/reference/js/vertexai.objectschema.md - - title: ObjectSchemaInterface - path: /docs/reference/js/vertexai.objectschemainterface.md - - title: PromptFeedback - path: /docs/reference/js/vertexai.promptfeedback.md - - title: RequestOptions - path: /docs/reference/js/vertexai.requestoptions.md - - title: RetrievedContextAttribution - path: /docs/reference/js/vertexai.retrievedcontextattribution.md - - title: SafetyRating - path: /docs/reference/js/vertexai.safetyrating.md - - title: SafetySetting - path: /docs/reference/js/vertexai.safetysetting.md - - title: Schema - path: /docs/reference/js/vertexai.schema.md - - title: SchemaInterface - path: /docs/reference/js/vertexai.schemainterface.md - - title: SchemaParams - path: /docs/reference/js/vertexai.schemaparams.md - - title: SchemaRequest - path: /docs/reference/js/vertexai.schemarequest.md - - title: SchemaShared - path: /docs/reference/js/vertexai.schemashared.md - - title: Segment - path: /docs/reference/js/vertexai.segment.md - - title: SingleRequestOptions - path: /docs/reference/js/vertexai.singlerequestoptions.md - - title: StartChatParams - path: /docs/reference/js/vertexai.startchatparams.md - - title: StringSchema - path: /docs/reference/js/vertexai.stringschema.md - - title: TextPart - path: /docs/reference/js/vertexai.textpart.md - - title: ToolConfig - path: /docs/reference/js/vertexai.toolconfig.md - - title: UsageMetadata - path: /docs/reference/js/vertexai.usagemetadata.md - - title: VertexAI - path: /docs/reference/js/vertexai.vertexai.md - - title: VertexAIError - path: /docs/reference/js/vertexai.vertexaierror.md - - title: VertexAIModel - path: /docs/reference/js/vertexai.vertexaimodel.md - - title: VertexAIOptions - path: /docs/reference/js/vertexai.vertexaioptions.md - - title: VideoMetadata - path: /docs/reference/js/vertexai.videometadata.md - - title: WebAttribution - path: /docs/reference/js/vertexai.webattribution.md -======= ->>>>>>> main diff --git a/docs-devsite/ai.chatsession.md b/docs-devsite/ai.chatsession.md index fc50d4ba920..211502b4076 100644 --- a/docs-devsite/ai.chatsession.md +++ b/docs-devsite/ai.chatsession.md @@ -36,15 +36,9 @@ export declare class ChatSession | Method | Modifiers | Description | | --- | --- | --- | -<<<<<<< HEAD:docs-devsite/vertexai.chatsession.md -| [getHistory()](./vertexai.chatsession.md#chatsessiongethistory) | | Gets the chat history so far. Blocked prompts are not added to history. Neither blocked candidates nor the prompts that generated them are added to history. | -| [sendMessage(request, singleRequestOptions)](./vertexai.chatsession.md#chatsessionsendmessage) | | Sends a chat message and receives a non-streaming [GenerateContentResult](./vertexai.generatecontentresult.md#generatecontentresult_interface) | -| [sendMessageStream(request, singleRequestOptions)](./vertexai.chatsession.md#chatsessionsendmessagestream) | | Sends a chat message and receives the response as a [GenerateContentStreamResult](./vertexai.generatecontentstreamresult.md#generatecontentstreamresult_interface) containing an iterable stream and a response promise. | -======= | [getHistory()](./ai.chatsession.md#chatsessiongethistory) | | Gets the chat history so far. Blocked prompts are not added to history. Neither blocked candidates nor the prompts that generated them are added to history. | -| [sendMessage(request)](./ai.chatsession.md#chatsessionsendmessage) | | Sends a chat message and receives a non-streaming [GenerateContentResult](./ai.generatecontentresult.md#generatecontentresult_interface) | -| [sendMessageStream(request)](./ai.chatsession.md#chatsessionsendmessagestream) | | Sends a chat message and receives the response as a [GenerateContentStreamResult](./ai.generatecontentstreamresult.md#generatecontentstreamresult_interface) containing an iterable stream and a response promise. | ->>>>>>> main:docs-devsite/ai.chatsession.md +| [sendMessage(request, singleRequestOptions)](./ai.chatsession.md#chatsessionsendmessage) | | Sends a chat message and receives a non-streaming [GenerateContentResult](./ai.generatecontentresult.md#generatecontentresult_interface) | +| [sendMessageStream(request, singleRequestOptions)](./ai.chatsession.md#chatsessionsendmessagestream) | | Sends a chat message and receives the response as a [GenerateContentStreamResult](./ai.generatecontentstreamresult.md#generatecontentstreamresult_interface) containing an iterable stream and a response promise. | ## ChatSession.(constructor) @@ -116,12 +110,8 @@ sendMessage(request: string | Array, singleRequestOptions?: Singl | Parameter | Type | Description | | --- | --- | --- | -<<<<<<< HEAD:docs-devsite/vertexai.chatsession.md -| request | string \| Array<string \| [Part](./vertexai.md#part)> | | -| singleRequestOptions | [SingleRequestOptions](./vertexai.singlerequestoptions.md#singlerequestoptions_interface) | | -======= | request | string \| Array<string \| [Part](./ai.md#part)> | | ->>>>>>> main:docs-devsite/ai.chatsession.md +| singleRequestOptions | [SingleRequestOptions](./ai.singlerequestoptions.md#singlerequestoptions_interface) | | Returns: @@ -141,12 +131,8 @@ sendMessageStream(request: string | Array, singleRequestOptions?: | Parameter | Type | Description | | --- | --- | --- | -<<<<<<< HEAD:docs-devsite/vertexai.chatsession.md -| request | string \| Array<string \| [Part](./vertexai.md#part)> | | -| singleRequestOptions | [SingleRequestOptions](./vertexai.singlerequestoptions.md#singlerequestoptions_interface) | | -======= | request | string \| Array<string \| [Part](./ai.md#part)> | | ->>>>>>> main:docs-devsite/ai.chatsession.md +| singleRequestOptions | [SingleRequestOptions](./ai.singlerequestoptions.md#singlerequestoptions_interface) | | Returns: diff --git a/docs-devsite/ai.generativemodel.md b/docs-devsite/ai.generativemodel.md index d91cf80e881..948251271b9 100644 --- a/docs-devsite/ai.generativemodel.md +++ b/docs-devsite/ai.generativemodel.md @@ -40,9 +40,9 @@ export declare class GenerativeModel extends AIModel | Method | Modifiers | Description | | --- | --- | --- | -| [countTokens(request)](./ai.generativemodel.md#generativemodelcounttokens) | | Counts the tokens in the provided request. | -| [generateContent(request)](./ai.generativemodel.md#generativemodelgeneratecontent) | | Makes a single non-streaming call to the model and returns an object containing a single [GenerateContentResponse](./ai.generatecontentresponse.md#generatecontentresponse_interface). | -| [generateContentStream(request)](./ai.generativemodel.md#generativemodelgeneratecontentstream) | | Makes a single streaming call to the model and returns an object containing an iterable stream that iterates over all chunks in the streaming response as well as a promise that returns the final aggregated response. | +| [countTokens(request, singleRequestOptions)](./ai.generativemodel.md#generativemodelcounttokens) | | Counts the tokens in the provided request. | +| [generateContent(request, singleRequestOptions)](./ai.generativemodel.md#generativemodelgeneratecontent) | | Makes a single non-streaming call to the model and returns an object containing a single [GenerateContentResponse](./ai.generatecontentresponse.md#generatecontentresponse_interface). | +| [generateContentStream(request, singleRequestOptions)](./ai.generativemodel.md#generativemodelgeneratecontentstream) | | Makes a single streaming call to the model and returns an object containing an iterable stream that iterates over all chunks in the streaming response as well as a promise that returns the final aggregated response. | | [startChat(startChatParams)](./ai.generativemodel.md#generativemodelstartchat) | | Gets a new [ChatSession](./ai.chatsession.md#chatsession_class) instance which can be used for multi-turn chats. | ## GenerativeModel.(constructor) @@ -118,7 +118,7 @@ Counts the tokens in the provided request. Signature: ```typescript -countTokens(request: CountTokensRequest | string | Array): Promise; +countTokens(request: CountTokensRequest | string | Array, singleRequestOptions?: SingleRequestOptions): Promise; ``` #### Parameters @@ -126,6 +126,7 @@ countTokens(request: CountTokensRequest | string | Array): Promis | Parameter | Type | Description | | --- | --- | --- | | request | [CountTokensRequest](./ai.counttokensrequest.md#counttokensrequest_interface) \| string \| Array<string \| [Part](./ai.md#part)> | | +| singleRequestOptions | [SingleRequestOptions](./ai.singlerequestoptions.md#singlerequestoptions_interface) | | Returns: @@ -138,7 +139,7 @@ Makes a single non-streaming call to the model and returns an object containing Signature: ```typescript -generateContent(request: GenerateContentRequest | string | Array): Promise; +generateContent(request: GenerateContentRequest | string | Array, singleRequestOptions?: SingleRequestOptions): Promise; ``` #### Parameters @@ -146,6 +147,7 @@ generateContent(request: GenerateContentRequest | string | Array) | Parameter | Type | Description | | --- | --- | --- | | request | [GenerateContentRequest](./ai.generatecontentrequest.md#generatecontentrequest_interface) \| string \| Array<string \| [Part](./ai.md#part)> | | +| singleRequestOptions | [SingleRequestOptions](./ai.singlerequestoptions.md#singlerequestoptions_interface) | | Returns: @@ -158,7 +160,7 @@ Makes a single streaming call to the model and returns an object containing an i Signature: ```typescript -generateContentStream(request: GenerateContentRequest | string | Array): Promise; +generateContentStream(request: GenerateContentRequest | string | Array, singleRequestOptions?: SingleRequestOptions): Promise; ``` #### Parameters @@ -166,6 +168,7 @@ generateContentStream(request: GenerateContentRequest | string | Array> | | +| singleRequestOptions | [SingleRequestOptions](./ai.singlerequestoptions.md#singlerequestoptions_interface) | | Returns: diff --git a/docs-devsite/ai.imagenmodel.md b/docs-devsite/ai.imagenmodel.md index a22cec6613d..ae0d4daa2d0 100644 --- a/docs-devsite/ai.imagenmodel.md +++ b/docs-devsite/ai.imagenmodel.md @@ -42,11 +42,7 @@ export declare class ImagenModel extends AIModel | Method | Modifiers | Description | | --- | --- | --- | -<<<<<<< HEAD:docs-devsite/vertexai.imagenmodel.md -| [generateImages(prompt, singleRequestOptions)](./vertexai.imagenmodel.md#imagenmodelgenerateimages) | | (Public Preview) Generates images using the Imagen model and returns them as base64-encoded strings. | -======= -| [generateImages(prompt)](./ai.imagenmodel.md#imagenmodelgenerateimages) | | (Public Preview) Generates images using the Imagen model and returns them as base64-encoded strings. | ->>>>>>> main:docs-devsite/ai.imagenmodel.md +| [generateImages(prompt, singleRequestOptions)](./ai.imagenmodel.md#imagenmodelgenerateimages) | | (Public Preview) Generates images using the Imagen model and returns them as base64-encoded strings. | ## ImagenModel.(constructor) @@ -130,7 +126,7 @@ generateImages(prompt: string, singleRequestOptions?: SingleRequestOptions): Pro | Parameter | Type | Description | | --- | --- | --- | | prompt | string | A text prompt describing the image(s) to generate. | -| singleRequestOptions | [SingleRequestOptions](./vertexai.singlerequestoptions.md#singlerequestoptions_interface) | | +| singleRequestOptions | [SingleRequestOptions](./ai.singlerequestoptions.md#singlerequestoptions_interface) | | Returns: diff --git a/docs-devsite/ai.md b/docs-devsite/ai.md index c43c0391ba4..fca050e676a 100644 --- a/docs-devsite/ai.md +++ b/docs-devsite/ai.md @@ -117,6 +117,7 @@ The Firebase AI Web SDK. | [SchemaRequest](./ai.schemarequest.md#schemarequest_interface) | Final format for [Schema](./ai.schema.md#schema_class) params passed to backend requests. | | [SchemaShared](./ai.schemashared.md#schemashared_interface) | Basic [Schema](./ai.schema.md#schema_class) properties shared across several Schema-related types. | | [Segment](./ai.segment.md#segment_interface) | | +| [SingleRequestOptions](./ai.singlerequestoptions.md#singlerequestoptions_interface) | Options that can be provided per-request. Extends the base [RequestOptions](./ai.requestoptions.md#requestoptions_interface) (like timeout and baseUrl) with request-specific controls like cancellation via AbortSignal.Options specified here will override any default [RequestOptions](./ai.requestoptions.md#requestoptions_interface) configured on a model (e.g. [GenerativeModel](./ai.generativemodel.md#generativemodel_class)). | | [StartChatParams](./ai.startchatparams.md#startchatparams_interface) | Params for [GenerativeModel.startChat()](./ai.generativemodel.md#generativemodelstartchat). | | [TextPart](./ai.textpart.md#textpart_interface) | Content part interface if the part represents a text string. | | [ToolConfig](./ai.toolconfig.md#toolconfig_interface) | Tool config. This config is shared for all tools provided in the request. | diff --git a/docs-devsite/vertexai.singlerequestoptions.md b/docs-devsite/ai.singlerequestoptions.md similarity index 56% rename from docs-devsite/vertexai.singlerequestoptions.md rename to docs-devsite/ai.singlerequestoptions.md index af5d46059fd..315c99436a4 100644 --- a/docs-devsite/vertexai.singlerequestoptions.md +++ b/docs-devsite/ai.singlerequestoptions.md @@ -10,22 +10,22 @@ https://github.com/firebase/firebase-js-sdk {% endcomment %} # SingleRequestOptions interface -Options that can be provided per-request. Extends the base [RequestOptions](./vertexai.requestoptions.md#requestoptions_interface) (like `timeout` and `baseUrl`) with request-specific controls like cancellation via `AbortSignal`. +Options that can be provided per-request. Extends the base [RequestOptions](./ai.requestoptions.md#requestoptions_interface) (like `timeout` and `baseUrl`) with request-specific controls like cancellation via `AbortSignal`. -Options specified here will override any default [RequestOptions](./vertexai.requestoptions.md#requestoptions_interface) configured on a model (e.g. [GenerativeModel](./vertexai.generativemodel.md#generativemodel_class)). +Options specified here will override any default [RequestOptions](./ai.requestoptions.md#requestoptions_interface) configured on a model (e.g. [GenerativeModel](./ai.generativemodel.md#generativemodel_class)). Signature: ```typescript export interface SingleRequestOptions extends RequestOptions ``` -Extends: [RequestOptions](./vertexai.requestoptions.md#requestoptions_interface) +Extends: [RequestOptions](./ai.requestoptions.md#requestoptions_interface) ## Properties | Property | Type | Description | | --- | --- | --- | -| [signal](./vertexai.singlerequestoptions.md#singlerequestoptionssignal) | AbortSignal | An AbortSignal instance that allows cancelling ongoing requests (like generateContent or generateImages).If provided, calling abort() on the corresponding AbortController will attempt to cancel the underlying HTTP request. An AbortError will be thrown if cancellation is successful.Note that this will not cancel the request in the backend, so billing will still be applied despite cancellation. | +| [signal](./ai.singlerequestoptions.md#singlerequestoptionssignal) | AbortSignal | An AbortSignal instance that allows cancelling ongoing requests (like generateContent or generateImages).If provided, calling abort() on the corresponding AbortController will attempt to cancel the underlying HTTP request. An AbortError will be thrown if cancellation is successful.Note that this will not cancel the request in the backend, so billing will still be applied despite cancellation. | ## SingleRequestOptions.signal diff --git a/docs-devsite/vertexai.generativemodel.md b/docs-devsite/vertexai.generativemodel.md deleted file mode 100644 index 0b5d0d3ec25..00000000000 --- a/docs-devsite/vertexai.generativemodel.md +++ /dev/null @@ -1,196 +0,0 @@ -Project: /docs/reference/js/_project.yaml -Book: /docs/reference/_book.yaml -page_type: reference - -{% comment %} -DO NOT EDIT THIS FILE! -This is generated by the JS SDK team, and any local changes will be -overwritten. Changes should be made in the source code at -https://github.com/firebase/firebase-js-sdk -{% endcomment %} - -# GenerativeModel class -Class for generative model APIs. - -Signature: - -```typescript -export declare class GenerativeModel extends VertexAIModel -``` -Extends: [VertexAIModel](./vertexai.vertexaimodel.md#vertexaimodel_class) - -## Constructors - -| Constructor | Modifiers | Description | -| --- | --- | --- | -| [(constructor)(vertexAI, modelParams, requestOptions)](./vertexai.generativemodel.md#generativemodelconstructor) | | Constructs a new instance of the GenerativeModel class | - -## Properties - -| Property | Modifiers | Type | Description | -| --- | --- | --- | --- | -| [generationConfig](./vertexai.generativemodel.md#generativemodelgenerationconfig) | | [GenerationConfig](./vertexai.generationconfig.md#generationconfig_interface) | | -| [requestOptions](./vertexai.generativemodel.md#generativemodelrequestoptions) | | [RequestOptions](./vertexai.requestoptions.md#requestoptions_interface) | | -| [safetySettings](./vertexai.generativemodel.md#generativemodelsafetysettings) | | [SafetySetting](./vertexai.safetysetting.md#safetysetting_interface)\[\] | | -| [systemInstruction](./vertexai.generativemodel.md#generativemodelsysteminstruction) | | [Content](./vertexai.content.md#content_interface) | | -| [toolConfig](./vertexai.generativemodel.md#generativemodeltoolconfig) | | [ToolConfig](./vertexai.toolconfig.md#toolconfig_interface) | | -| [tools](./vertexai.generativemodel.md#generativemodeltools) | | [Tool](./vertexai.md#tool)\[\] | | - -## Methods - -| Method | Modifiers | Description | -| --- | --- | --- | -| [countTokens(request, singleRequestOptions)](./vertexai.generativemodel.md#generativemodelcounttokens) | | Counts the tokens in the provided request. | -| [generateContent(request, singleRequestOptions)](./vertexai.generativemodel.md#generativemodelgeneratecontent) | | Makes a single non-streaming call to the model and returns an object containing a single [GenerateContentResponse](./vertexai.generatecontentresponse.md#generatecontentresponse_interface). | -| [generateContentStream(request, singleRequestOptions)](./vertexai.generativemodel.md#generativemodelgeneratecontentstream) | | Makes a single streaming call to the model and returns an object containing an iterable stream that iterates over all chunks in the streaming response as well as a promise that returns the final aggregated response. | -| [startChat(startChatParams)](./vertexai.generativemodel.md#generativemodelstartchat) | | Gets a new [ChatSession](./vertexai.chatsession.md#chatsession_class) instance which can be used for multi-turn chats. | - -## GenerativeModel.(constructor) - -Constructs a new instance of the `GenerativeModel` class - -Signature: - -```typescript -constructor(vertexAI: VertexAI, modelParams: ModelParams, requestOptions?: RequestOptions); -``` - -#### Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| vertexAI | [VertexAI](./vertexai.vertexai.md#vertexai_interface) | | -| modelParams | [ModelParams](./vertexai.modelparams.md#modelparams_interface) | | -| requestOptions | [RequestOptions](./vertexai.requestoptions.md#requestoptions_interface) | | - -## GenerativeModel.generationConfig - -Signature: - -```typescript -generationConfig: GenerationConfig; -``` - -## GenerativeModel.requestOptions - -Signature: - -```typescript -requestOptions?: RequestOptions; -``` - -## GenerativeModel.safetySettings - -Signature: - -```typescript -safetySettings: SafetySetting[]; -``` - -## GenerativeModel.systemInstruction - -Signature: - -```typescript -systemInstruction?: Content; -``` - -## GenerativeModel.toolConfig - -Signature: - -```typescript -toolConfig?: ToolConfig; -``` - -## GenerativeModel.tools - -Signature: - -```typescript -tools?: Tool[]; -``` - -## GenerativeModel.countTokens() - -Counts the tokens in the provided request. - -Signature: - -```typescript -countTokens(request: CountTokensRequest | string | Array, singleRequestOptions?: SingleRequestOptions): Promise; -``` - -#### Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| request | [CountTokensRequest](./vertexai.counttokensrequest.md#counttokensrequest_interface) \| string \| Array<string \| [Part](./vertexai.md#part)> | | -| singleRequestOptions | [SingleRequestOptions](./vertexai.singlerequestoptions.md#singlerequestoptions_interface) | | - -Returns: - -Promise<[CountTokensResponse](./vertexai.counttokensresponse.md#counttokensresponse_interface)> - -## GenerativeModel.generateContent() - -Makes a single non-streaming call to the model and returns an object containing a single [GenerateContentResponse](./vertexai.generatecontentresponse.md#generatecontentresponse_interface). - -Signature: - -```typescript -generateContent(request: GenerateContentRequest | string | Array, singleRequestOptions?: SingleRequestOptions): Promise; -``` - -#### Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| request | [GenerateContentRequest](./vertexai.generatecontentrequest.md#generatecontentrequest_interface) \| string \| Array<string \| [Part](./vertexai.md#part)> | | -| singleRequestOptions | [SingleRequestOptions](./vertexai.singlerequestoptions.md#singlerequestoptions_interface) | | - -Returns: - -Promise<[GenerateContentResult](./vertexai.generatecontentresult.md#generatecontentresult_interface)> - -## GenerativeModel.generateContentStream() - -Makes a single streaming call to the model and returns an object containing an iterable stream that iterates over all chunks in the streaming response as well as a promise that returns the final aggregated response. - -Signature: - -```typescript -generateContentStream(request: GenerateContentRequest | string | Array, singleRequestOptions?: SingleRequestOptions): Promise; -``` - -#### Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| request | [GenerateContentRequest](./vertexai.generatecontentrequest.md#generatecontentrequest_interface) \| string \| Array<string \| [Part](./vertexai.md#part)> | | -| singleRequestOptions | [SingleRequestOptions](./vertexai.singlerequestoptions.md#singlerequestoptions_interface) | | - -Returns: - -Promise<[GenerateContentStreamResult](./vertexai.generatecontentstreamresult.md#generatecontentstreamresult_interface)> - -## GenerativeModel.startChat() - -Gets a new [ChatSession](./vertexai.chatsession.md#chatsession_class) instance which can be used for multi-turn chats. - -Signature: - -```typescript -startChat(startChatParams?: StartChatParams): ChatSession; -``` - -#### Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| startChatParams | [StartChatParams](./vertexai.startchatparams.md#startchatparams_interface) | | - -Returns: - -[ChatSession](./vertexai.chatsession.md#chatsession_class) - diff --git a/docs-devsite/vertexai.md b/docs-devsite/vertexai.md deleted file mode 100644 index 7c61852e484..00000000000 --- a/docs-devsite/vertexai.md +++ /dev/null @@ -1,592 +0,0 @@ -Project: /docs/reference/js/_project.yaml -Book: /docs/reference/_book.yaml -page_type: reference - -{% comment %} -DO NOT EDIT THIS FILE! -This is generated by the JS SDK team, and any local changes will be -overwritten. Changes should be made in the source code at -https://github.com/firebase/firebase-js-sdk -{% endcomment %} - -# vertexai package -The Vertex AI in Firebase Web SDK. - -## Functions - -| Function | Description | -| --- | --- | -| function(app, ...) | -| [getVertexAI(app, options)](./vertexai.md#getvertexai_04094cf) | Returns a [VertexAI](./vertexai.vertexai.md#vertexai_interface) instance for the given app. | -| function(vertexAI, ...) | -| [getGenerativeModel(vertexAI, modelParams, requestOptions)](./vertexai.md#getgenerativemodel_e3037c9) | Returns a [GenerativeModel](./vertexai.generativemodel.md#generativemodel_class) class with methods for inference and other functionality. | -| [getImagenModel(vertexAI, modelParams, requestOptions)](./vertexai.md#getimagenmodel_812c375) | (Public Preview) Returns an [ImagenModel](./vertexai.imagenmodel.md#imagenmodel_class) class with methods for using Imagen.Only Imagen 3 models (named imagen-3.0-*) are supported. | - -## Classes - -| Class | Description | -| --- | --- | -| [ArraySchema](./vertexai.arrayschema.md#arrayschema_class) | Schema class for "array" types. The items param should refer to the type of item that can be a member of the array. | -| [BooleanSchema](./vertexai.booleanschema.md#booleanschema_class) | Schema class for "boolean" types. | -| [ChatSession](./vertexai.chatsession.md#chatsession_class) | ChatSession class that enables sending chat messages and stores history of sent and received messages so far. | -| [GenerativeModel](./vertexai.generativemodel.md#generativemodel_class) | Class for generative model APIs. | -| [ImagenImageFormat](./vertexai.imagenimageformat.md#imagenimageformat_class) | (Public Preview) Defines the image format for images generated by Imagen.Use this class to specify the desired format (JPEG or PNG) and compression quality for images generated by Imagen. This is typically included as part of [ImagenModelParams](./vertexai.imagenmodelparams.md#imagenmodelparams_interface). | -| [ImagenModel](./vertexai.imagenmodel.md#imagenmodel_class) | (Public Preview) Class for Imagen model APIs.This class provides methods for generating images using the Imagen model. | -| [IntegerSchema](./vertexai.integerschema.md#integerschema_class) | Schema class for "integer" types. | -| [NumberSchema](./vertexai.numberschema.md#numberschema_class) | Schema class for "number" types. | -| [ObjectSchema](./vertexai.objectschema.md#objectschema_class) | Schema class for "object" types. The properties param must be a map of Schema objects. | -| [Schema](./vertexai.schema.md#schema_class) | Parent class encompassing all Schema types, with static methods that allow building specific Schema types. This class can be converted with JSON.stringify() into a JSON string accepted by Vertex AI REST endpoints. (This string conversion is automatically done when calling SDK methods.) | -| [StringSchema](./vertexai.stringschema.md#stringschema_class) | Schema class for "string" types. Can be used with or without enum values. | -| [VertexAIError](./vertexai.vertexaierror.md#vertexaierror_class) | Error class for the Vertex AI in Firebase SDK. | -| [VertexAIModel](./vertexai.vertexaimodel.md#vertexaimodel_class) | Base class for Vertex AI in Firebase model APIs. | - -## Enumerations - -| Enumeration | Description | -| --- | --- | -| [BlockReason](./vertexai.md#blockreason) | Reason that a prompt was blocked. | -| [FinishReason](./vertexai.md#finishreason) | Reason that a candidate finished. | -| [FunctionCallingMode](./vertexai.md#functioncallingmode) | | -| [HarmBlockMethod](./vertexai.md#harmblockmethod) | | -| [HarmBlockThreshold](./vertexai.md#harmblockthreshold) | Threshold above which a prompt or candidate will be blocked. | -| [HarmCategory](./vertexai.md#harmcategory) | Harm categories that would cause prompts or candidates to be blocked. | -| [HarmProbability](./vertexai.md#harmprobability) | Probability that a prompt or candidate matches a harm category. | -| [HarmSeverity](./vertexai.md#harmseverity) | Harm severity levels. | -| [ImagenAspectRatio](./vertexai.md#imagenaspectratio) | (Public Preview) Aspect ratios for Imagen images.To specify an aspect ratio for generated images, set the aspectRatio property in your [ImagenGenerationConfig](./vertexai.imagengenerationconfig.md#imagengenerationconfig_interface).See the the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) for more details and examples of the supported aspect ratios. | -| [ImagenPersonFilterLevel](./vertexai.md#imagenpersonfilterlevel) | (Public Preview) A filter level controlling whether generation of images containing people or faces is allowed.See the personGeneration documentation for more details. | -| [ImagenSafetyFilterLevel](./vertexai.md#imagensafetyfilterlevel) | (Public Preview) A filter level controlling how aggressively to filter sensitive content.Text prompts provided as inputs and images (generated or uploaded) through Imagen on Vertex AI are assessed against a list of safety filters, which include 'harmful categories' (for example, violence, sexual, derogatory, and toxic). This filter level controls how aggressively to filter out potentially harmful content from responses. See the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) and the [Responsible AI and usage guidelines](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#safety-filters) for more details. | -| [Modality](./vertexai.md#modality) | Content part modality. | -| [SchemaType](./vertexai.md#schematype) | Contains the list of OpenAPI data types as defined by the [OpenAPI specification](https://swagger.io/docs/specification/data-models/data-types/) | -| [VertexAIErrorCode](./vertexai.md#vertexaierrorcode) | Standardized error codes that [VertexAIError](./vertexai.vertexaierror.md#vertexaierror_class) can have. | - -## Interfaces - -| Interface | Description | -| --- | --- | -| [BaseParams](./vertexai.baseparams.md#baseparams_interface) | Base parameters for a number of methods. | -| [Citation](./vertexai.citation.md#citation_interface) | A single citation. | -| [CitationMetadata](./vertexai.citationmetadata.md#citationmetadata_interface) | Citation metadata that may be found on a [GenerateContentCandidate](./vertexai.generatecontentcandidate.md#generatecontentcandidate_interface). | -| [Content](./vertexai.content.md#content_interface) | Content type for both prompts and response candidates. | -| [CountTokensRequest](./vertexai.counttokensrequest.md#counttokensrequest_interface) | Params for calling [GenerativeModel.countTokens()](./vertexai.generativemodel.md#generativemodelcounttokens) | -| [CountTokensResponse](./vertexai.counttokensresponse.md#counttokensresponse_interface) | Response from calling [GenerativeModel.countTokens()](./vertexai.generativemodel.md#generativemodelcounttokens). | -| [CustomErrorData](./vertexai.customerrordata.md#customerrordata_interface) | Details object that contains data originating from a bad HTTP response. | -| [Date\_2](./vertexai.date_2.md#date_2_interface) | Protobuf google.type.Date | -| [EnhancedGenerateContentResponse](./vertexai.enhancedgeneratecontentresponse.md#enhancedgeneratecontentresponse_interface) | Response object wrapped with helper methods. | -| [ErrorDetails](./vertexai.errordetails.md#errordetails_interface) | Details object that may be included in an error response. | -| [FileData](./vertexai.filedata.md#filedata_interface) | Data pointing to a file uploaded on Google Cloud Storage. | -| [FileDataPart](./vertexai.filedatapart.md#filedatapart_interface) | Content part interface if the part represents [FileData](./vertexai.filedata.md#filedata_interface) | -| [FunctionCall](./vertexai.functioncall.md#functioncall_interface) | A predicted [FunctionCall](./vertexai.functioncall.md#functioncall_interface) returned from the model that contains a string representing the [FunctionDeclaration.name](./vertexai.functiondeclaration.md#functiondeclarationname) and a structured JSON object containing the parameters and their values. | -| [FunctionCallingConfig](./vertexai.functioncallingconfig.md#functioncallingconfig_interface) | | -| [FunctionCallPart](./vertexai.functioncallpart.md#functioncallpart_interface) | Content part interface if the part represents a [FunctionCall](./vertexai.functioncall.md#functioncall_interface). | -| [FunctionDeclaration](./vertexai.functiondeclaration.md#functiondeclaration_interface) | Structured representation of a function declaration as defined by the [OpenAPI 3.0 specification](https://spec.openapis.org/oas/v3.0.3). Included in this declaration are the function name and parameters. This FunctionDeclaration is a representation of a block of code that can be used as a Tool by the model and executed by the client. | -| [FunctionDeclarationsTool](./vertexai.functiondeclarationstool.md#functiondeclarationstool_interface) | A FunctionDeclarationsTool is a piece of code that enables the system to interact with external systems to perform an action, or set of actions, outside of knowledge and scope of the model. | -| [FunctionResponse](./vertexai.functionresponse.md#functionresponse_interface) | The result output from a [FunctionCall](./vertexai.functioncall.md#functioncall_interface) that contains a string representing the [FunctionDeclaration.name](./vertexai.functiondeclaration.md#functiondeclarationname) and a structured JSON object containing any output from the function is used as context to the model. This should contain the result of a [FunctionCall](./vertexai.functioncall.md#functioncall_interface) made based on model prediction. | -| [FunctionResponsePart](./vertexai.functionresponsepart.md#functionresponsepart_interface) | Content part interface if the part represents [FunctionResponse](./vertexai.functionresponse.md#functionresponse_interface). | -| [GenerateContentCandidate](./vertexai.generatecontentcandidate.md#generatecontentcandidate_interface) | A candidate returned as part of a [GenerateContentResponse](./vertexai.generatecontentresponse.md#generatecontentresponse_interface). | -| [GenerateContentRequest](./vertexai.generatecontentrequest.md#generatecontentrequest_interface) | Request sent through [GenerativeModel.generateContent()](./vertexai.generativemodel.md#generativemodelgeneratecontent) | -| [GenerateContentResponse](./vertexai.generatecontentresponse.md#generatecontentresponse_interface) | Individual response from [GenerativeModel.generateContent()](./vertexai.generativemodel.md#generativemodelgeneratecontent) and [GenerativeModel.generateContentStream()](./vertexai.generativemodel.md#generativemodelgeneratecontentstream). generateContentStream() will return one in each chunk until the stream is done. | -| [GenerateContentResult](./vertexai.generatecontentresult.md#generatecontentresult_interface) | Result object returned from [GenerativeModel.generateContent()](./vertexai.generativemodel.md#generativemodelgeneratecontent) call. | -| [GenerateContentStreamResult](./vertexai.generatecontentstreamresult.md#generatecontentstreamresult_interface) | Result object returned from [GenerativeModel.generateContentStream()](./vertexai.generativemodel.md#generativemodelgeneratecontentstream) call. Iterate over stream to get chunks as they come in and/or use the response promise to get the aggregated response when the stream is done. | -| [GenerationConfig](./vertexai.generationconfig.md#generationconfig_interface) | Config options for content-related requests | -| [GenerativeContentBlob](./vertexai.generativecontentblob.md#generativecontentblob_interface) | Interface for sending an image. | -| [GroundingAttribution](./vertexai.groundingattribution.md#groundingattribution_interface) | | -| [GroundingMetadata](./vertexai.groundingmetadata.md#groundingmetadata_interface) | Metadata returned to client when grounding is enabled. | -| [ImagenGCSImage](./vertexai.imagengcsimage.md#imagengcsimage_interface) | An image generated by Imagen, stored in a Cloud Storage for Firebase bucket.This feature is not available yet. | -| [ImagenGenerationConfig](./vertexai.imagengenerationconfig.md#imagengenerationconfig_interface) | (Public Preview) Configuration options for generating images with Imagen.See the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images-imagen) for more details. | -| [ImagenGenerationResponse](./vertexai.imagengenerationresponse.md#imagengenerationresponse_interface) | (Public Preview) The response from a request to generate images with Imagen. | -| [ImagenInlineImage](./vertexai.imageninlineimage.md#imageninlineimage_interface) | (Public Preview) An image generated by Imagen, represented as inline data. | -| [ImagenModelParams](./vertexai.imagenmodelparams.md#imagenmodelparams_interface) | (Public Preview) Parameters for configuring an [ImagenModel](./vertexai.imagenmodel.md#imagenmodel_class). | -| [ImagenSafetySettings](./vertexai.imagensafetysettings.md#imagensafetysettings_interface) | (Public Preview) Settings for controlling the aggressiveness of filtering out sensitive content.See the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) for more details. | -| [InlineDataPart](./vertexai.inlinedatapart.md#inlinedatapart_interface) | Content part interface if the part represents an image. | -| [ModalityTokenCount](./vertexai.modalitytokencount.md#modalitytokencount_interface) | Represents token counting info for a single modality. | -| [ModelParams](./vertexai.modelparams.md#modelparams_interface) | Params passed to [getGenerativeModel()](./vertexai.md#getgenerativemodel_e3037c9). | -| [ObjectSchemaInterface](./vertexai.objectschemainterface.md#objectschemainterface_interface) | Interface for [ObjectSchema](./vertexai.objectschema.md#objectschema_class) class. | -| [PromptFeedback](./vertexai.promptfeedback.md#promptfeedback_interface) | If the prompt was blocked, this will be populated with blockReason and the relevant safetyRatings. | -| [RequestOptions](./vertexai.requestoptions.md#requestoptions_interface) | Params passed to [getGenerativeModel()](./vertexai.md#getgenerativemodel_e3037c9). | -| [RetrievedContextAttribution](./vertexai.retrievedcontextattribution.md#retrievedcontextattribution_interface) | | -| [SafetyRating](./vertexai.safetyrating.md#safetyrating_interface) | A safety rating associated with a [GenerateContentCandidate](./vertexai.generatecontentcandidate.md#generatecontentcandidate_interface) | -| [SafetySetting](./vertexai.safetysetting.md#safetysetting_interface) | Safety setting that can be sent as part of request parameters. | -| [SchemaInterface](./vertexai.schemainterface.md#schemainterface_interface) | Interface for [Schema](./vertexai.schema.md#schema_class) class. | -| [SchemaParams](./vertexai.schemaparams.md#schemaparams_interface) | Params passed to [Schema](./vertexai.schema.md#schema_class) static methods to create specific [Schema](./vertexai.schema.md#schema_class) classes. | -| [SchemaRequest](./vertexai.schemarequest.md#schemarequest_interface) | Final format for [Schema](./vertexai.schema.md#schema_class) params passed to backend requests. | -| [SchemaShared](./vertexai.schemashared.md#schemashared_interface) | Basic [Schema](./vertexai.schema.md#schema_class) properties shared across several Schema-related types. | -| [Segment](./vertexai.segment.md#segment_interface) | | -| [SingleRequestOptions](./vertexai.singlerequestoptions.md#singlerequestoptions_interface) | Options that can be provided per-request. Extends the base [RequestOptions](./vertexai.requestoptions.md#requestoptions_interface) (like timeout and baseUrl) with request-specific controls like cancellation via AbortSignal.Options specified here will override any default [RequestOptions](./vertexai.requestoptions.md#requestoptions_interface) configured on a model (e.g. [GenerativeModel](./vertexai.generativemodel.md#generativemodel_class)). | -| [StartChatParams](./vertexai.startchatparams.md#startchatparams_interface) | Params for [GenerativeModel.startChat()](./vertexai.generativemodel.md#generativemodelstartchat). | -| [TextPart](./vertexai.textpart.md#textpart_interface) | Content part interface if the part represents a text string. | -| [ToolConfig](./vertexai.toolconfig.md#toolconfig_interface) | Tool config. This config is shared for all tools provided in the request. | -| [UsageMetadata](./vertexai.usagemetadata.md#usagemetadata_interface) | Usage metadata about a [GenerateContentResponse](./vertexai.generatecontentresponse.md#generatecontentresponse_interface). | -| [VertexAI](./vertexai.vertexai.md#vertexai_interface) | An instance of the Vertex AI in Firebase SDK. | -| [VertexAIOptions](./vertexai.vertexaioptions.md#vertexaioptions_interface) | Options when initializing the Vertex AI in Firebase SDK. | -| [VideoMetadata](./vertexai.videometadata.md#videometadata_interface) | Describes the input video content. | -| [WebAttribution](./vertexai.webattribution.md#webattribution_interface) | | - -## Variables - -| Variable | Description | -| --- | --- | -| [POSSIBLE\_ROLES](./vertexai.md#possible_roles) | Possible roles. | -| [ResponseModality](./vertexai.md#responsemodality) | (Public Preview) Generation modalities to be returned in generation responses. | - -## Type Aliases - -| Type Alias | Description | -| --- | --- | -| [Part](./vertexai.md#part) | Content part - includes text, image/video, or function call/response part types. | -| [ResponseModality](./vertexai.md#responsemodality) | (Public Preview) Generation modalities to be returned in generation responses. | -| [Role](./vertexai.md#role) | Role is the producer of the content. | -| [Tool](./vertexai.md#tool) | Defines a tool that model can call to access external knowledge. | -| [TypedSchema](./vertexai.md#typedschema) | A type that includes all specific Schema types. | - -## function(app, ...) - -### getVertexAI(app, options) {:#getvertexai_04094cf} - -Returns a [VertexAI](./vertexai.vertexai.md#vertexai_interface) instance for the given app. - -Signature: - -```typescript -export declare function getVertexAI(app?: FirebaseApp, options?: VertexAIOptions): VertexAI; -``` - -#### Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| app | [FirebaseApp](./app.firebaseapp.md#firebaseapp_interface) | The [FirebaseApp](./app.firebaseapp.md#firebaseapp_interface) to use. | -| options | [VertexAIOptions](./vertexai.vertexaioptions.md#vertexaioptions_interface) | | - -Returns: - -[VertexAI](./vertexai.vertexai.md#vertexai_interface) - -## function(vertexAI, ...) - -### getGenerativeModel(vertexAI, modelParams, requestOptions) {:#getgenerativemodel_e3037c9} - -Returns a [GenerativeModel](./vertexai.generativemodel.md#generativemodel_class) class with methods for inference and other functionality. - -Signature: - -```typescript -export declare function getGenerativeModel(vertexAI: VertexAI, modelParams: ModelParams, requestOptions?: RequestOptions): GenerativeModel; -``` - -#### Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| vertexAI | [VertexAI](./vertexai.vertexai.md#vertexai_interface) | | -| modelParams | [ModelParams](./vertexai.modelparams.md#modelparams_interface) | | -| requestOptions | [RequestOptions](./vertexai.requestoptions.md#requestoptions_interface) | | - -Returns: - -[GenerativeModel](./vertexai.generativemodel.md#generativemodel_class) - -### getImagenModel(vertexAI, modelParams, requestOptions) {:#getimagenmodel_812c375} - -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - -Returns an [ImagenModel](./vertexai.imagenmodel.md#imagenmodel_class) class with methods for using Imagen. - -Only Imagen 3 models (named `imagen-3.0-*`) are supported. - -Signature: - -```typescript -export declare function getImagenModel(vertexAI: VertexAI, modelParams: ImagenModelParams, requestOptions?: RequestOptions): ImagenModel; -``` - -#### Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| vertexAI | [VertexAI](./vertexai.vertexai.md#vertexai_interface) | An instance of the Vertex AI in Firebase SDK. | -| modelParams | [ImagenModelParams](./vertexai.imagenmodelparams.md#imagenmodelparams_interface) | Parameters to use when making Imagen requests. | -| requestOptions | [RequestOptions](./vertexai.requestoptions.md#requestoptions_interface) | Additional options to use when making requests. | - -Returns: - -[ImagenModel](./vertexai.imagenmodel.md#imagenmodel_class) - -#### Exceptions - -If the `apiKey` or `projectId` fields are missing in your Firebase config. - -## POSSIBLE\_ROLES - -Possible roles. - -Signature: - -```typescript -POSSIBLE_ROLES: readonly ["user", "model", "function", "system"] -``` - -## ResponseModality - -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - -Generation modalities to be returned in generation responses. - -Signature: - -```typescript -ResponseModality: { - readonly TEXT: "TEXT"; - readonly IMAGE: "IMAGE"; -} -``` - -## Part - -Content part - includes text, image/video, or function call/response part types. - -Signature: - -```typescript -export type Part = TextPart | InlineDataPart | FunctionCallPart | FunctionResponsePart | FileDataPart; -``` - -## ResponseModality - -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - -Generation modalities to be returned in generation responses. - -Signature: - -```typescript -export type ResponseModality = (typeof ResponseModality)[keyof typeof ResponseModality]; -``` - -## Role - -Role is the producer of the content. - -Signature: - -```typescript -export type Role = (typeof POSSIBLE_ROLES)[number]; -``` - -## Tool - -Defines a tool that model can call to access external knowledge. - -Signature: - -```typescript -export declare type Tool = FunctionDeclarationsTool; -``` - -## TypedSchema - -A type that includes all specific Schema types. - -Signature: - -```typescript -export type TypedSchema = IntegerSchema | NumberSchema | StringSchema | BooleanSchema | ObjectSchema | ArraySchema; -``` - -## BlockReason - -Reason that a prompt was blocked. - -Signature: - -```typescript -export declare enum BlockReason -``` - -## Enumeration Members - -| Member | Value | Description | -| --- | --- | --- | -| BLOCKLIST | "BLOCKLIST" | Content was blocked because it contained terms from the terminology blocklist. | -| OTHER | "OTHER" | Content was blocked, but the reason is uncategorized. | -| PROHIBITED\_CONTENT | "PROHIBITED_CONTENT" | Content was blocked due to prohibited content. | -| SAFETY | "SAFETY" | Content was blocked by safety settings. | - -## FinishReason - -Reason that a candidate finished. - -Signature: - -```typescript -export declare enum FinishReason -``` - -## Enumeration Members - -| Member | Value | Description | -| --- | --- | --- | -| BLOCKLIST | "BLOCKLIST" | The candidate content contained forbidden terms. | -| MALFORMED\_FUNCTION\_CALL | "MALFORMED_FUNCTION_CALL" | The function call generated by the model was invalid. | -| MAX\_TOKENS | "MAX_TOKENS" | The maximum number of tokens as specified in the request was reached. | -| OTHER | "OTHER" | Unknown reason. | -| PROHIBITED\_CONTENT | "PROHIBITED_CONTENT" | The candidate content potentially contained prohibited content. | -| RECITATION | "RECITATION" | The candidate content was flagged for recitation reasons. | -| SAFETY | "SAFETY" | The candidate content was flagged for safety reasons. | -| SPII | "SPII" | The candidate content potentially contained Sensitive Personally Identifiable Information (SPII). | -| STOP | "STOP" | Natural stop point of the model or provided stop sequence. | - -## FunctionCallingMode - - -Signature: - -```typescript -export declare enum FunctionCallingMode -``` - -## Enumeration Members - -| Member | Value | Description | -| --- | --- | --- | -| ANY | "ANY" | Model is constrained to always predicting a function call only. If allowed_function_names is set, the predicted function call will be limited to any one of allowed_function_names, else the predicted function call will be any one of the provided function_declarations. | -| AUTO | "AUTO" | Default model behavior; model decides to predict either a function call or a natural language response. | -| NONE | "NONE" | Model will not predict any function call. Model behavior is same as when not passing any function declarations. | - -## HarmBlockMethod - - -Signature: - -```typescript -export declare enum HarmBlockMethod -``` - -## Enumeration Members - -| Member | Value | Description | -| --- | --- | --- | -| PROBABILITY | "PROBABILITY" | The harm block method uses the probability score. | -| SEVERITY | "SEVERITY" | The harm block method uses both probability and severity scores. | - -## HarmBlockThreshold - -Threshold above which a prompt or candidate will be blocked. - -Signature: - -```typescript -export declare enum HarmBlockThreshold -``` - -## Enumeration Members - -| Member | Value | Description | -| --- | --- | --- | -| BLOCK\_LOW\_AND\_ABOVE | "BLOCK_LOW_AND_ABOVE" | Content with NEGLIGIBLE will be allowed. | -| BLOCK\_MEDIUM\_AND\_ABOVE | "BLOCK_MEDIUM_AND_ABOVE" | Content with NEGLIGIBLE and LOW will be allowed. | -| BLOCK\_NONE | "BLOCK_NONE" | All content will be allowed. | -| BLOCK\_ONLY\_HIGH | "BLOCK_ONLY_HIGH" | Content with NEGLIGIBLE, LOW, and MEDIUM will be allowed. | - -## HarmCategory - -Harm categories that would cause prompts or candidates to be blocked. - -Signature: - -```typescript -export declare enum HarmCategory -``` - -## Enumeration Members - -| Member | Value | Description | -| --- | --- | --- | -| HARM\_CATEGORY\_DANGEROUS\_CONTENT | "HARM_CATEGORY_DANGEROUS_CONTENT" | | -| HARM\_CATEGORY\_HARASSMENT | "HARM_CATEGORY_HARASSMENT" | | -| HARM\_CATEGORY\_HATE\_SPEECH | "HARM_CATEGORY_HATE_SPEECH" | | -| HARM\_CATEGORY\_SEXUALLY\_EXPLICIT | "HARM_CATEGORY_SEXUALLY_EXPLICIT" | | - -## HarmProbability - -Probability that a prompt or candidate matches a harm category. - -Signature: - -```typescript -export declare enum HarmProbability -``` - -## Enumeration Members - -| Member | Value | Description | -| --- | --- | --- | -| HIGH | "HIGH" | Content has a high chance of being unsafe. | -| LOW | "LOW" | Content has a low chance of being unsafe. | -| MEDIUM | "MEDIUM" | Content has a medium chance of being unsafe. | -| NEGLIGIBLE | "NEGLIGIBLE" | Content has a negligible chance of being unsafe. | - -## HarmSeverity - -Harm severity levels. - -Signature: - -```typescript -export declare enum HarmSeverity -``` - -## Enumeration Members - -| Member | Value | Description | -| --- | --- | --- | -| HARM\_SEVERITY\_HIGH | "HARM_SEVERITY_HIGH" | High level of harm severity. | -| HARM\_SEVERITY\_LOW | "HARM_SEVERITY_LOW" | Low level of harm severity. | -| HARM\_SEVERITY\_MEDIUM | "HARM_SEVERITY_MEDIUM" | Medium level of harm severity. | -| HARM\_SEVERITY\_NEGLIGIBLE | "HARM_SEVERITY_NEGLIGIBLE" | Negligible level of harm severity. | - -## ImagenAspectRatio - -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - -Aspect ratios for Imagen images. - -To specify an aspect ratio for generated images, set the `aspectRatio` property in your [ImagenGenerationConfig](./vertexai.imagengenerationconfig.md#imagengenerationconfig_interface). - -See the the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) for more details and examples of the supported aspect ratios. - -Signature: - -```typescript -export declare enum ImagenAspectRatio -``` - -## Enumeration Members - -| Member | Value | Description | -| --- | --- | --- | -| LANDSCAPE\_16x9 | "16:9" | (Public Preview) Landscape (16:9) aspect ratio. | -| LANDSCAPE\_3x4 | "3:4" | (Public Preview) Landscape (3:4) aspect ratio. | -| PORTRAIT\_4x3 | "4:3" | (Public Preview) Portrait (4:3) aspect ratio. | -| PORTRAIT\_9x16 | "9:16" | (Public Preview) Portrait (9:16) aspect ratio. | -| SQUARE | "1:1" | (Public Preview) Square (1:1) aspect ratio. | - -## ImagenPersonFilterLevel - -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - -A filter level controlling whether generation of images containing people or faces is allowed. - -See the personGeneration documentation for more details. - -Signature: - -```typescript -export declare enum ImagenPersonFilterLevel -``` - -## Enumeration Members - -| Member | Value | Description | -| --- | --- | --- | -| ALLOW\_ADULT | "allow_adult" | (Public Preview) Allow generation of images containing adults only; images of children are filtered out.Generation of images containing people or faces may require your use case to be reviewed and approved by Cloud support; see the [Responsible AI and usage guidelines](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#person-face-gen) for more details. | -| ALLOW\_ALL | "allow_all" | (Public Preview) Allow generation of images containing adults only; images of children are filtered out.Generation of images containing people or faces may require your use case to be reviewed and approved by Cloud support; see the [Responsible AI and usage guidelines](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#person-face-gen) for more details. | -| BLOCK\_ALL | "dont_allow" | (Public Preview) Disallow generation of images containing people or faces; images of people are filtered out. | - -## ImagenSafetyFilterLevel - -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - -A filter level controlling how aggressively to filter sensitive content. - -Text prompts provided as inputs and images (generated or uploaded) through Imagen on Vertex AI are assessed against a list of safety filters, which include 'harmful categories' (for example, `violence`, `sexual`, `derogatory`, and `toxic`). This filter level controls how aggressively to filter out potentially harmful content from responses. See the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) and the [Responsible AI and usage guidelines](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#safety-filters) for more details. - -Signature: - -```typescript -export declare enum ImagenSafetyFilterLevel -``` - -## Enumeration Members - -| Member | Value | Description | -| --- | --- | --- | -| BLOCK\_LOW\_AND\_ABOVE | "block_low_and_above" | (Public Preview) The most aggressive filtering level; most strict blocking. | -| BLOCK\_MEDIUM\_AND\_ABOVE | "block_medium_and_above" | (Public Preview) Blocks some sensitive prompts and responses. | -| BLOCK\_NONE | "block_none" | (Public Preview) The least aggressive filtering level; blocks very few sensitive prompts and responses.Access to this feature is restricted and may require your case to be reviewed and approved by Cloud support. | -| BLOCK\_ONLY\_HIGH | "block_only_high" | (Public Preview) Blocks few sensitive prompts and responses. | - -## Modality - -Content part modality. - -Signature: - -```typescript -export declare enum Modality -``` - -## Enumeration Members - -| Member | Value | Description | -| --- | --- | --- | -| AUDIO | "AUDIO" | Audio. | -| DOCUMENT | "DOCUMENT" | Document (for example, PDF). | -| IMAGE | "IMAGE" | Image. | -| MODALITY\_UNSPECIFIED | "MODALITY_UNSPECIFIED" | Unspecified modality. | -| TEXT | "TEXT" | Plain text. | -| VIDEO | "VIDEO" | Video. | - -## SchemaType - -Contains the list of OpenAPI data types as defined by the [OpenAPI specification](https://swagger.io/docs/specification/data-models/data-types/) - -Signature: - -```typescript -export declare enum SchemaType -``` - -## Enumeration Members - -| Member | Value | Description | -| --- | --- | --- | -| ARRAY | "array" | Array type. | -| BOOLEAN | "boolean" | Boolean type. | -| INTEGER | "integer" | Integer type. | -| NUMBER | "number" | Number type. | -| OBJECT | "object" | Object type. | -| STRING | "string" | String type. | - -## VertexAIErrorCode - -Standardized error codes that [VertexAIError](./vertexai.vertexaierror.md#vertexaierror_class) can have. - -Signature: - -```typescript -export declare const enum VertexAIErrorCode -``` - -## Enumeration Members - -| Member | Value | Description | -| --- | --- | --- | -| API\_NOT\_ENABLED | "api-not-enabled" | An error due to the Firebase API not being enabled in the Console. | -| ERROR | "error" | A generic error occurred. | -| FETCH\_ERROR | "fetch-error" | An error occurred while performing a fetch. | -| INVALID\_CONTENT | "invalid-content" | An error associated with a Content object. | -| INVALID\_SCHEMA | "invalid-schema" | An error due to invalid Schema input. | -| NO\_API\_KEY | "no-api-key" | An error occurred due to a missing Firebase API key. | -| NO\_APP\_ID | "no-app-id" | An error occured due to a missing Firebase app ID. | -| NO\_MODEL | "no-model" | An error occurred due to a model name not being specified during initialization. | -| NO\_PROJECT\_ID | "no-project-id" | An error occurred due to a missing project ID. | -| PARSE\_FAILED | "parse-failed" | An error occurred while parsing. | -| REQUEST\_ERROR | "request-error" | An error occurred in a request. | -| RESPONSE\_ERROR | "response-error" | An error occurred in a response. | - From f69061167940a71d8a1de2116bc4516d8b145194 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Thu, 22 May 2025 11:27:53 -0400 Subject: [PATCH 04/22] Update changeset to use `@firebase/ai` --- .changeset/long-keys-watch.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/long-keys-watch.md b/.changeset/long-keys-watch.md index 7cc4d582ff1..fb13ed74987 100644 --- a/.changeset/long-keys-watch.md +++ b/.changeset/long-keys-watch.md @@ -1,6 +1,6 @@ --- 'firebase': minor -'@firebase/vertexai': minor +'@firebase/ai': minor --- Add support for `AbortSignal`, allowing requests to be aborted. From 821e58093f450c3db7442973addbee545a29d2de Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Thu, 22 May 2025 11:28:48 -0400 Subject: [PATCH 05/22] remove ghost changeset --- .changeset/dirty-crews-cross.md | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 .changeset/dirty-crews-cross.md diff --git a/.changeset/dirty-crews-cross.md b/.changeset/dirty-crews-cross.md deleted file mode 100644 index 08a962670f0..00000000000 --- a/.changeset/dirty-crews-cross.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@firebase/auth": patch -"@firebase/database": patch -"@firebase/firestore": patch -"@firebase/functions": patch -"@firebase/storage": patch ---- - -Revert "Fixed scroll behavior (#9043)" From 15239fb16874c43ae7994d94d1f578956e0ff6fb Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Thu, 22 May 2025 11:33:10 -0400 Subject: [PATCH 06/22] update example in docs --- docs-devsite/ai.singlerequestoptions.md | 4 ++++ packages/ai/src/types/requests.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/docs-devsite/ai.singlerequestoptions.md b/docs-devsite/ai.singlerequestoptions.md index 315c99436a4..6e7e1f4bf12 100644 --- a/docs-devsite/ai.singlerequestoptions.md +++ b/docs-devsite/ai.singlerequestoptions.md @@ -49,6 +49,10 @@ const controller = new AbortController(); const model = getGenerativeModel({ // ... }); +model.generateContent( + "Write a story about a magic backpack.", + { signal: controller.signal } +); // To cancel request: controller.abort(); diff --git a/packages/ai/src/types/requests.ts b/packages/ai/src/types/requests.ts index 3f6c61add09..0664adf6ecf 100644 --- a/packages/ai/src/types/requests.ts +++ b/packages/ai/src/types/requests.ts @@ -189,6 +189,10 @@ export interface SingleRequestOptions extends RequestOptions { * const model = getGenerativeModel({ * // ... * }); + * model.generateContent( + * "Write a story about a magic backpack.", + * { signal: controller.signal } + * ); * * // To cancel request: * controller.abort(); From 0096b9a07ddf79f343cc652c580ee33b41546912 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Tue, 27 May 2025 10:47:18 -0400 Subject: [PATCH 07/22] Docs review --- docs-devsite/ai.md | 2 +- docs-devsite/ai.singlerequestoptions.md | 6 +++--- packages/ai/src/types/requests.ts | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs-devsite/ai.md b/docs-devsite/ai.md index fca050e676a..46b8ae8493a 100644 --- a/docs-devsite/ai.md +++ b/docs-devsite/ai.md @@ -117,7 +117,7 @@ The Firebase AI Web SDK. | [SchemaRequest](./ai.schemarequest.md#schemarequest_interface) | Final format for [Schema](./ai.schema.md#schema_class) params passed to backend requests. | | [SchemaShared](./ai.schemashared.md#schemashared_interface) | Basic [Schema](./ai.schema.md#schema_class) properties shared across several Schema-related types. | | [Segment](./ai.segment.md#segment_interface) | | -| [SingleRequestOptions](./ai.singlerequestoptions.md#singlerequestoptions_interface) | Options that can be provided per-request. Extends the base [RequestOptions](./ai.requestoptions.md#requestoptions_interface) (like timeout and baseUrl) with request-specific controls like cancellation via AbortSignal.Options specified here will override any default [RequestOptions](./ai.requestoptions.md#requestoptions_interface) configured on a model (e.g. [GenerativeModel](./ai.generativemodel.md#generativemodel_class)). | +| [SingleRequestOptions](./ai.singlerequestoptions.md#singlerequestoptions_interface) | Options that can be provided per-request. Extends the base [RequestOptions](./ai.requestoptions.md#requestoptions_interface) (like timeout and baseUrl) with request-specific controls like cancellation via AbortSignal.Options specified here will override any default [RequestOptions](./ai.requestoptions.md#requestoptions_interface) configured on a model (for example, [GenerativeModel](./ai.generativemodel.md#generativemodel_class)). | | [StartChatParams](./ai.startchatparams.md#startchatparams_interface) | Params for [GenerativeModel.startChat()](./ai.generativemodel.md#generativemodelstartchat). | | [TextPart](./ai.textpart.md#textpart_interface) | Content part interface if the part represents a text string. | | [ToolConfig](./ai.toolconfig.md#toolconfig_interface) | Tool config. This config is shared for all tools provided in the request. | diff --git a/docs-devsite/ai.singlerequestoptions.md b/docs-devsite/ai.singlerequestoptions.md index 6e7e1f4bf12..a55bd3c2f3c 100644 --- a/docs-devsite/ai.singlerequestoptions.md +++ b/docs-devsite/ai.singlerequestoptions.md @@ -12,7 +12,7 @@ https://github.com/firebase/firebase-js-sdk # SingleRequestOptions interface Options that can be provided per-request. Extends the base [RequestOptions](./ai.requestoptions.md#requestoptions_interface) (like `timeout` and `baseUrl`) with request-specific controls like cancellation via `AbortSignal`. -Options specified here will override any default [RequestOptions](./ai.requestoptions.md#requestoptions_interface) configured on a model (e.g. [GenerativeModel](./ai.generativemodel.md#generativemodel_class)). +Options specified here will override any default [RequestOptions](./ai.requestoptions.md#requestoptions_interface) configured on a model (for example, [GenerativeModel](./ai.generativemodel.md#generativemodel_class)). Signature: @@ -25,7 +25,7 @@ export interface SingleRequestOptions extends RequestOptions | Property | Type | Description | | --- | --- | --- | -| [signal](./ai.singlerequestoptions.md#singlerequestoptionssignal) | AbortSignal | An AbortSignal instance that allows cancelling ongoing requests (like generateContent or generateImages).If provided, calling abort() on the corresponding AbortController will attempt to cancel the underlying HTTP request. An AbortError will be thrown if cancellation is successful.Note that this will not cancel the request in the backend, so billing will still be applied despite cancellation. | +| [signal](./ai.singlerequestoptions.md#singlerequestoptionssignal) | AbortSignal | An AbortSignal instance that allows cancelling ongoing requests (like generateContent or generateImages).If provided, calling abort() on the corresponding AbortController will attempt to cancel the underlying HTTP request. An AbortError will be thrown if cancellation is successful.Note that this will not cancel the request in the backend, so any applicable billing charges will still be applied despite cancellation. | ## SingleRequestOptions.signal @@ -33,7 +33,7 @@ An `AbortSignal` instance that allows cancelling ongoing requests (like `generat If provided, calling `abort()` on the corresponding `AbortController` will attempt to cancel the underlying HTTP request. An `AbortError` will be thrown if cancellation is successful. -Note that this will not cancel the request in the backend, so billing will still be applied despite cancellation. +Note that this will not cancel the request in the backend, so any applicable billing charges will still be applied despite cancellation. Signature: diff --git a/packages/ai/src/types/requests.ts b/packages/ai/src/types/requests.ts index 0664adf6ecf..a22508a1d72 100644 --- a/packages/ai/src/types/requests.ts +++ b/packages/ai/src/types/requests.ts @@ -167,7 +167,7 @@ export interface RequestOptions { * with request-specific controls like cancellation via `AbortSignal`. * * Options specified here will override any default {@link RequestOptions} - * configured on a model (e.g. {@link GenerativeModel}). + * configured on a model (for example, {@link GenerativeModel}). * * @public */ @@ -180,8 +180,8 @@ export interface SingleRequestOptions extends RequestOptions { * will attempt to cancel the underlying HTTP request. An `AbortError` will be thrown * if cancellation is successful. * - * Note that this will not cancel the request in the backend, so billing will - * still be applied despite cancellation. + * Note that this will not cancel the request in the backend, so any applicable billing charges + * will still be applied despite cancellation. * * @example * ```javascript From 62552a24858fc67879f8b5483db007f65e6f98e4 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Tue, 4 Nov 2025 11:35:06 -0500 Subject: [PATCH 08/22] do not log abort errors during chat message stream --- packages/ai/src/methods/chat-session.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/ai/src/methods/chat-session.ts b/packages/ai/src/methods/chat-session.ts index 43ea0afb692..6886b5a5c94 100644 --- a/packages/ai/src/methods/chat-session.ts +++ b/packages/ai/src/methods/chat-session.ts @@ -191,7 +191,11 @@ export class ChatSession { // Errors in streamPromise are already catchable by the user as // streamPromise is returned. // Avoid duplicating the error message in logs. - if (e.message !== SILENT_ERROR) { + // AbortErrors are thrown after the initial streamPromise resolves, since the request + // may be aborted once streaming has begun. Since these errors won't be wrapped in a SILENT_ERROR, + // we have to explicitly check for them. The user will be able to catch these AbortErrors when + // awaiting the resolution of the result.response. + if (e.message !== SILENT_ERROR && e.name !== 'AbortError') { // Users do not have access to _sendPromise to catch errors // downstream from streamPromise, so they should not throw. logger.error(e); From 9f3132404b586e0b17ba3b14a5adf8a49999f15f Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Tue, 4 Nov 2025 12:04:18 -0500 Subject: [PATCH 09/22] remove old vertex ai docs --- common/api-review/vertexai.api.md | 974 ------------------------------ 1 file changed, 974 deletions(-) delete mode 100644 common/api-review/vertexai.api.md diff --git a/common/api-review/vertexai.api.md b/common/api-review/vertexai.api.md deleted file mode 100644 index df6e7535581..00000000000 --- a/common/api-review/vertexai.api.md +++ /dev/null @@ -1,974 +0,0 @@ -## API Report File for "@firebase/vertexai" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts - -import { AppCheckTokenResult } from '@firebase/app-check-interop-types'; -import { FirebaseApp } from '@firebase/app'; -import { FirebaseAuthTokenData } from '@firebase/auth-interop-types'; -import { FirebaseError } from '@firebase/util'; - -// @public -export interface AI { - app: FirebaseApp; - backend: Backend; - // @deprecated - location: string; -} - -// @public -export class AIError extends FirebaseError { - constructor(code: AIErrorCode, message: string, customErrorData?: CustomErrorData | undefined); - // (undocumented) - readonly code: AIErrorCode; - // (undocumented) - readonly customErrorData?: CustomErrorData | undefined; -} - -// @public -const enum AIErrorCode { - API_NOT_ENABLED = "api-not-enabled", - ERROR = "error", - FETCH_ERROR = "fetch-error", - INVALID_CONTENT = "invalid-content", - INVALID_SCHEMA = "invalid-schema", - NO_API_KEY = "no-api-key", - NO_APP_ID = "no-app-id", - NO_MODEL = "no-model", - NO_PROJECT_ID = "no-project-id", - PARSE_FAILED = "parse-failed", - REQUEST_ERROR = "request-error", - RESPONSE_ERROR = "response-error", - UNSUPPORTED = "unsupported" -} - -export { AIErrorCode } - -export { AIErrorCode as VertexAIErrorCode } - -// @public -export abstract class AIModel { - // @internal - protected constructor(ai: AI, modelName: string); - // Warning: (ae-forgotten-export) The symbol "ApiSettings" needs to be exported by the entry point index.d.ts - // - // @internal (undocumented) - protected _apiSettings: ApiSettings; - readonly model: string; - // @internal - static normalizeModelName(modelName: string, backendType: BackendType): string; - } - -// @public -export interface AIOptions { - backend: Backend; -} - -// @public -export class ArraySchema extends Schema { - constructor(schemaParams: SchemaParams, items: TypedSchema); - // (undocumented) - items: TypedSchema; - // @internal (undocumented) - toJSON(): SchemaRequest; -} - -// @public -export abstract class Backend { - protected constructor(type: BackendType); - readonly backendType: BackendType; -} - -// @public -export const BackendType: { - readonly VERTEX_AI: "VERTEX_AI"; - readonly GOOGLE_AI: "GOOGLE_AI"; -}; - -// @public -export type BackendType = (typeof BackendType)[keyof typeof BackendType]; - -// @public -export interface BaseParams { - // (undocumented) - generationConfig?: GenerationConfig; - // (undocumented) - safetySettings?: SafetySetting[]; -} - -// @public -export enum BlockReason { - BLOCKLIST = "BLOCKLIST", - OTHER = "OTHER", - PROHIBITED_CONTENT = "PROHIBITED_CONTENT", - SAFETY = "SAFETY" -} - -// @public -export class BooleanSchema extends Schema { - constructor(schemaParams?: SchemaParams); -} - -// @public -export class ChatSession { - constructor(apiSettings: ApiSettings, model: string, params?: StartChatParams | undefined, requestOptions?: RequestOptions | undefined); - getHistory(): Promise; - // (undocumented) - model: string; - // (undocumented) - params?: StartChatParams | undefined; - // (undocumented) - requestOptions?: RequestOptions | undefined; - sendMessage(request: string | Array, singleRequestOptions?: SingleRequestOptions): Promise; - sendMessageStream(request: string | Array, singleRequestOptions?: SingleRequestOptions): Promise; - } - -// @public -export interface Citation { - // (undocumented) - endIndex?: number; - // (undocumented) - license?: string; - publicationDate?: Date_2; - // (undocumented) - startIndex?: number; - title?: string; - // (undocumented) - uri?: string; -} - -// @public -export interface CitationMetadata { - // (undocumented) - citations: Citation[]; -} - -// @public -export interface Content { - // (undocumented) - parts: Part[]; - // (undocumented) - role: Role; -} - -// @public -export interface CountTokensRequest { - // (undocumented) - contents: Content[]; - generationConfig?: GenerationConfig; - systemInstruction?: string | Part | Content; - tools?: Tool[]; -} - -// @public -export interface CountTokensResponse { - promptTokensDetails?: ModalityTokenCount[]; - totalBillableCharacters?: number; - totalTokens: number; -} - -// @public -export interface CustomErrorData { - errorDetails?: ErrorDetails[]; - response?: GenerateContentResponse; - status?: number; - statusText?: string; -} - -// @public -interface Date_2 { - // (undocumented) - day: number; - // (undocumented) - month: number; - // (undocumented) - year: number; -} - -export { Date_2 as Date } - -// @public -export interface EnhancedGenerateContentResponse extends GenerateContentResponse { - // (undocumented) - functionCalls: () => FunctionCall[] | undefined; - inlineDataParts: () => InlineDataPart[] | undefined; - text: () => string; -} - -// @public -export interface ErrorDetails { - // (undocumented) - '@type'?: string; - [key: string]: unknown; - domain?: string; - metadata?: Record; - reason?: string; -} - -// @public -export interface FileData { - // (undocumented) - fileUri: string; - // (undocumented) - mimeType: string; -} - -// @public -export interface FileDataPart { - // (undocumented) - fileData: FileData; - // (undocumented) - functionCall?: never; - // (undocumented) - functionResponse?: never; - // (undocumented) - inlineData?: never; - // (undocumented) - text?: never; -} - -// @public -export enum FinishReason { - BLOCKLIST = "BLOCKLIST", - MALFORMED_FUNCTION_CALL = "MALFORMED_FUNCTION_CALL", - MAX_TOKENS = "MAX_TOKENS", - OTHER = "OTHER", - PROHIBITED_CONTENT = "PROHIBITED_CONTENT", - RECITATION = "RECITATION", - SAFETY = "SAFETY", - SPII = "SPII", - STOP = "STOP" -} - -// @public -export interface FunctionCall { - // (undocumented) - args: object; - // (undocumented) - name: string; -} - -// @public (undocumented) -export interface FunctionCallingConfig { - // (undocumented) - allowedFunctionNames?: string[]; - // (undocumented) - mode?: FunctionCallingMode; -} - -// @public (undocumented) -export enum FunctionCallingMode { - ANY = "ANY", - AUTO = "AUTO", - NONE = "NONE" -} - -// @public -export interface FunctionCallPart { - // (undocumented) - functionCall: FunctionCall; - // (undocumented) - functionResponse?: never; - // (undocumented) - inlineData?: never; - // (undocumented) - text?: never; -} - -// @public -export interface FunctionDeclaration { - description: string; - name: string; - parameters?: ObjectSchemaInterface; -} - -// @public -export interface FunctionDeclarationsTool { - functionDeclarations?: FunctionDeclaration[]; -} - -// @public -export interface FunctionResponse { - // (undocumented) - name: string; - // (undocumented) - response: object; -} - -// @public -export interface FunctionResponsePart { - // (undocumented) - functionCall?: never; - // (undocumented) - functionResponse: FunctionResponse; - // (undocumented) - inlineData?: never; - // (undocumented) - text?: never; -} - -// @public -export interface GenerateContentCandidate { - // (undocumented) - citationMetadata?: CitationMetadata; - // (undocumented) - content: Content; - // (undocumented) - finishMessage?: string; - // (undocumented) - finishReason?: FinishReason; - // (undocumented) - groundingMetadata?: GroundingMetadata; - // (undocumented) - index: number; - // (undocumented) - safetyRatings?: SafetyRating[]; -} - -// @public -export interface GenerateContentRequest extends BaseParams { - // (undocumented) - contents: Content[]; - // (undocumented) - systemInstruction?: string | Part | Content; - // (undocumented) - toolConfig?: ToolConfig; - // (undocumented) - tools?: Tool[]; -} - -// @public -export interface GenerateContentResponse { - // (undocumented) - candidates?: GenerateContentCandidate[]; - // (undocumented) - promptFeedback?: PromptFeedback; - // (undocumented) - usageMetadata?: UsageMetadata; -} - -// @public -export interface GenerateContentResult { - // (undocumented) - response: EnhancedGenerateContentResponse; -} - -// @public -export interface GenerateContentStreamResult { - // (undocumented) - response: Promise; - // (undocumented) - stream: AsyncGenerator; -} - -// @public -export interface GenerationConfig { - // (undocumented) - candidateCount?: number; - // (undocumented) - frequencyPenalty?: number; - // (undocumented) - maxOutputTokens?: number; - // (undocumented) - presencePenalty?: number; - responseMimeType?: string; - // @beta - responseModalities?: ResponseModality[]; - responseSchema?: TypedSchema | SchemaRequest; - // (undocumented) - stopSequences?: string[]; - // (undocumented) - temperature?: number; - // (undocumented) - topK?: number; - // (undocumented) - topP?: number; -} - -// @public -export interface GenerativeContentBlob { - data: string; - // (undocumented) - mimeType: string; -} - -// @public -<<<<<<< HEAD -export class GenerativeModel extends VertexAIModel { - constructor(vertexAI: VertexAI, modelParams: ModelParams, requestOptions?: RequestOptions); - countTokens(request: CountTokensRequest | string | Array, singleRequestOptions?: SingleRequestOptions): Promise; - generateContent(request: GenerateContentRequest | string | Array, singleRequestOptions?: SingleRequestOptions): Promise; - generateContentStream(request: GenerateContentRequest | string | Array, singleRequestOptions?: SingleRequestOptions): Promise; -======= -export class GenerativeModel extends AIModel { - constructor(ai: AI, modelParams: ModelParams, requestOptions?: RequestOptions); - countTokens(request: CountTokensRequest | string | Array): Promise; - generateContent(request: GenerateContentRequest | string | Array): Promise; - generateContentStream(request: GenerateContentRequest | string | Array): Promise; ->>>>>>> main - // (undocumented) - generationConfig: GenerationConfig; - // (undocumented) - requestOptions?: RequestOptions; - // (undocumented) - safetySettings: SafetySetting[]; - startChat(startChatParams?: StartChatParams): ChatSession; - // (undocumented) - systemInstruction?: Content; - // (undocumented) - toolConfig?: ToolConfig; - // (undocumented) - tools?: Tool[]; -} - -// @public -export function getAI(app?: FirebaseApp, options?: AIOptions): AI; - -// @public -export function getGenerativeModel(ai: AI, modelParams: ModelParams, requestOptions?: RequestOptions): GenerativeModel; - -// @beta -export function getImagenModel(ai: AI, modelParams: ImagenModelParams, requestOptions?: RequestOptions): ImagenModel; - -// @public -export function getVertexAI(app?: FirebaseApp, options?: VertexAIOptions): VertexAI; - -// @public -export class GoogleAIBackend extends Backend { - constructor(); -} - -// Warning: (ae-internal-missing-underscore) The name "GoogleAICitationMetadata" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export interface GoogleAICitationMetadata { - // (undocumented) - citationSources: Citation[]; -} - -// Warning: (ae-internal-missing-underscore) The name "GoogleAICountTokensRequest" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export interface GoogleAICountTokensRequest { - // (undocumented) - generateContentRequest: { - model: string; - contents: Content[]; - systemInstruction?: string | Part | Content; - tools?: Tool[]; - generationConfig?: GenerationConfig; - }; -} - -// Warning: (ae-internal-missing-underscore) The name "GoogleAIGenerateContentCandidate" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export interface GoogleAIGenerateContentCandidate { - // (undocumented) - citationMetadata?: GoogleAICitationMetadata; - // (undocumented) - content: Content; - // (undocumented) - finishMessage?: string; - // (undocumented) - finishReason?: FinishReason; - // (undocumented) - groundingMetadata?: GroundingMetadata; - // (undocumented) - index: number; - // (undocumented) - safetyRatings?: SafetyRating[]; -} - -// Warning: (ae-internal-missing-underscore) The name "GoogleAIGenerateContentResponse" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export interface GoogleAIGenerateContentResponse { - // (undocumented) - candidates?: GoogleAIGenerateContentCandidate[]; - // (undocumented) - promptFeedback?: PromptFeedback; - // (undocumented) - usageMetadata?: UsageMetadata; -} - -// @public @deprecated (undocumented) -export interface GroundingAttribution { - // (undocumented) - confidenceScore?: number; - // (undocumented) - retrievedContext?: RetrievedContextAttribution; - // (undocumented) - segment: Segment; - // (undocumented) - web?: WebAttribution; -} - -// @public -export interface GroundingMetadata { - // @deprecated (undocumented) - groundingAttributions: GroundingAttribution[]; - // (undocumented) - retrievalQueries?: string[]; - // (undocumented) - webSearchQueries?: string[]; -} - -// @public -export enum HarmBlockMethod { - PROBABILITY = "PROBABILITY", - SEVERITY = "SEVERITY" -} - -// @public -export enum HarmBlockThreshold { - BLOCK_LOW_AND_ABOVE = "BLOCK_LOW_AND_ABOVE", - BLOCK_MEDIUM_AND_ABOVE = "BLOCK_MEDIUM_AND_ABOVE", - BLOCK_NONE = "BLOCK_NONE", - BLOCK_ONLY_HIGH = "BLOCK_ONLY_HIGH" -} - -// @public -export enum HarmCategory { - // (undocumented) - HARM_CATEGORY_DANGEROUS_CONTENT = "HARM_CATEGORY_DANGEROUS_CONTENT", - // (undocumented) - HARM_CATEGORY_HARASSMENT = "HARM_CATEGORY_HARASSMENT", - // (undocumented) - HARM_CATEGORY_HATE_SPEECH = "HARM_CATEGORY_HATE_SPEECH", - // (undocumented) - HARM_CATEGORY_SEXUALLY_EXPLICIT = "HARM_CATEGORY_SEXUALLY_EXPLICIT" -} - -// @public -export enum HarmProbability { - HIGH = "HIGH", - LOW = "LOW", - MEDIUM = "MEDIUM", - NEGLIGIBLE = "NEGLIGIBLE" -} - -// @public -export enum HarmSeverity { - HARM_SEVERITY_HIGH = "HARM_SEVERITY_HIGH", - HARM_SEVERITY_LOW = "HARM_SEVERITY_LOW", - HARM_SEVERITY_MEDIUM = "HARM_SEVERITY_MEDIUM", - HARM_SEVERITY_NEGLIGIBLE = "HARM_SEVERITY_NEGLIGIBLE", - HARM_SEVERITY_UNSUPPORTED = "HARM_SEVERITY_UNSUPPORTED" -} - -// @beta -export enum ImagenAspectRatio { - LANDSCAPE_16x9 = "16:9", - LANDSCAPE_3x4 = "3:4", - PORTRAIT_4x3 = "4:3", - PORTRAIT_9x16 = "9:16", - SQUARE = "1:1" -} - -// @public -export interface ImagenGCSImage { - gcsURI: string; - mimeType: string; -} - -// @beta -export interface ImagenGenerationConfig { - addWatermark?: boolean; - aspectRatio?: ImagenAspectRatio; - imageFormat?: ImagenImageFormat; - negativePrompt?: string; - numberOfImages?: number; -} - -// @beta -export interface ImagenGenerationResponse { - filteredReason?: string; - images: T[]; -} - -// @beta -export class ImagenImageFormat { - compressionQuality?: number; - static jpeg(compressionQuality?: number): ImagenImageFormat; - mimeType: string; - static png(): ImagenImageFormat; -} - -// @beta -export interface ImagenInlineImage { - bytesBase64Encoded: string; - mimeType: string; -} - -// @beta -<<<<<<< HEAD -export class ImagenModel extends VertexAIModel { - constructor(vertexAI: VertexAI, modelParams: ImagenModelParams, requestOptions?: RequestOptions | undefined); - generateImages(prompt: string, singleRequestOptions?: SingleRequestOptions): Promise>; -======= -export class ImagenModel extends AIModel { - constructor(ai: AI, modelParams: ImagenModelParams, requestOptions?: RequestOptions | undefined); - generateImages(prompt: string): Promise>; ->>>>>>> main - // @internal - generateImagesGCS(prompt: string, gcsURI: string, singleRequestOptions?: SingleRequestOptions): Promise>; - generationConfig?: ImagenGenerationConfig; - // (undocumented) - requestOptions?: RequestOptions | undefined; - safetySettings?: ImagenSafetySettings; -} - -// @beta -export interface ImagenModelParams { - generationConfig?: ImagenGenerationConfig; - model: string; - safetySettings?: ImagenSafetySettings; -} - -// @beta -export enum ImagenPersonFilterLevel { - ALLOW_ADULT = "allow_adult", - ALLOW_ALL = "allow_all", - BLOCK_ALL = "dont_allow" -} - -// @beta -export enum ImagenSafetyFilterLevel { - BLOCK_LOW_AND_ABOVE = "block_low_and_above", - BLOCK_MEDIUM_AND_ABOVE = "block_medium_and_above", - BLOCK_NONE = "block_none", - BLOCK_ONLY_HIGH = "block_only_high" -} - -// @beta -export interface ImagenSafetySettings { - personFilterLevel?: ImagenPersonFilterLevel; - safetyFilterLevel?: ImagenSafetyFilterLevel; -} - -// @public -export interface InlineDataPart { - // (undocumented) - functionCall?: never; - // (undocumented) - functionResponse?: never; - // (undocumented) - inlineData: GenerativeContentBlob; - // (undocumented) - text?: never; - videoMetadata?: VideoMetadata; -} - -// @public -export class IntegerSchema extends Schema { - constructor(schemaParams?: SchemaParams); -} - -// @public -export enum Modality { - AUDIO = "AUDIO", - DOCUMENT = "DOCUMENT", - IMAGE = "IMAGE", - MODALITY_UNSPECIFIED = "MODALITY_UNSPECIFIED", - TEXT = "TEXT", - VIDEO = "VIDEO" -} - -// @public -export interface ModalityTokenCount { - modality: Modality; - tokenCount: number; -} - -// @public -export interface ModelParams extends BaseParams { - // (undocumented) - model: string; - // (undocumented) - systemInstruction?: string | Part | Content; - // (undocumented) - toolConfig?: ToolConfig; - // (undocumented) - tools?: Tool[]; -} - -// @public -export class NumberSchema extends Schema { - constructor(schemaParams?: SchemaParams); -} - -// @public -export class ObjectSchema extends Schema { - constructor(schemaParams: SchemaParams, properties: { - [k: string]: TypedSchema; - }, optionalProperties?: string[]); - // (undocumented) - optionalProperties: string[]; - // (undocumented) - properties: { - [k: string]: TypedSchema; - }; - // @internal (undocumented) - toJSON(): SchemaRequest; -} - -// @public -export interface ObjectSchemaInterface extends SchemaInterface { - // (undocumented) - optionalProperties?: string[]; - // (undocumented) - type: SchemaType.OBJECT; -} - -// @public -export type Part = TextPart | InlineDataPart | FunctionCallPart | FunctionResponsePart | FileDataPart; - -// @public -export const POSSIBLE_ROLES: readonly ["user", "model", "function", "system"]; - -// @public -export interface PromptFeedback { - // (undocumented) - blockReason?: BlockReason; - blockReasonMessage?: string; - // (undocumented) - safetyRatings: SafetyRating[]; -} - -// @public -export interface RequestOptions { - baseUrl?: string; - timeout?: number; -} - -// @beta -export const ResponseModality: { - readonly TEXT: "TEXT"; - readonly IMAGE: "IMAGE"; -}; - -// @beta -export type ResponseModality = (typeof ResponseModality)[keyof typeof ResponseModality]; - -// @public (undocumented) -export interface RetrievedContextAttribution { - // (undocumented) - title: string; - // (undocumented) - uri: string; -} - -// @public -export type Role = (typeof POSSIBLE_ROLES)[number]; - -// @public -export interface SafetyRating { - // (undocumented) - blocked: boolean; - // (undocumented) - category: HarmCategory; - // (undocumented) - probability: HarmProbability; - probabilityScore: number; - severity: HarmSeverity; - severityScore: number; -} - -// @public -export interface SafetySetting { - // (undocumented) - category: HarmCategory; - method?: HarmBlockMethod; - // (undocumented) - threshold: HarmBlockThreshold; -} - -// @public -export abstract class Schema implements SchemaInterface { - constructor(schemaParams: SchemaInterface); - [key: string]: unknown; - // (undocumented) - static array(arrayParams: SchemaParams & { - items: Schema; - }): ArraySchema; - // (undocumented) - static boolean(booleanParams?: SchemaParams): BooleanSchema; - description?: string; - // (undocumented) - static enumString(stringParams: SchemaParams & { - enum: string[]; - }): StringSchema; - example?: unknown; - format?: string; - // (undocumented) - static integer(integerParams?: SchemaParams): IntegerSchema; - nullable: boolean; - // (undocumented) - static number(numberParams?: SchemaParams): NumberSchema; - // (undocumented) - static object(objectParams: SchemaParams & { - properties: { - [k: string]: Schema; - }; - optionalProperties?: string[]; - }): ObjectSchema; - // (undocumented) - static string(stringParams?: SchemaParams): StringSchema; - // @internal - toJSON(): SchemaRequest; - type: SchemaType; -} - -// @public -export interface SchemaInterface extends SchemaShared { - type: SchemaType; -} - -// @public -export interface SchemaParams extends SchemaShared { -} - -// @public -export interface SchemaRequest extends SchemaShared { - required?: string[]; - type: SchemaType; -} - -// @public -export interface SchemaShared { - // (undocumented) - [key: string]: unknown; - description?: string; - enum?: string[]; - example?: unknown; - format?: string; - items?: T; - nullable?: boolean; - properties?: { - [k: string]: T; - }; -} - -// @public -export enum SchemaType { - ARRAY = "array", - BOOLEAN = "boolean", - INTEGER = "integer", - NUMBER = "number", - OBJECT = "object", - STRING = "string" -} - -// @public (undocumented) -export interface Segment { - // (undocumented) - endIndex: number; - // (undocumented) - partIndex: number; - // (undocumented) - startIndex: number; -} - -// @public -export interface SingleRequestOptions extends RequestOptions { - signal?: AbortSignal; -} - -// @public -export interface StartChatParams extends BaseParams { - // (undocumented) - history?: Content[]; - // (undocumented) - systemInstruction?: string | Part | Content; - // (undocumented) - toolConfig?: ToolConfig; - // (undocumented) - tools?: Tool[]; -} - -// @public -export class StringSchema extends Schema { - constructor(schemaParams?: SchemaParams, enumValues?: string[]); - // (undocumented) - enum?: string[]; - // @internal (undocumented) - toJSON(): SchemaRequest; -} - -// @public -export interface TextPart { - // (undocumented) - functionCall?: never; - // (undocumented) - functionResponse?: never; - // (undocumented) - inlineData?: never; - // (undocumented) - text: string; -} - -// @public -export type Tool = FunctionDeclarationsTool; - -// @public -export interface ToolConfig { - // (undocumented) - functionCallingConfig?: FunctionCallingConfig; -} - -// @public -export type TypedSchema = IntegerSchema | NumberSchema | StringSchema | BooleanSchema | ObjectSchema | ArraySchema; - -// @public -export interface UsageMetadata { - // (undocumented) - candidatesTokenCount: number; - // (undocumented) - candidatesTokensDetails?: ModalityTokenCount[]; - // (undocumented) - promptTokenCount: number; - // (undocumented) - promptTokensDetails?: ModalityTokenCount[]; - // (undocumented) - totalTokenCount: number; -} - -// @public -export type VertexAI = AI; - -// @public -export class VertexAIBackend extends Backend { - constructor(location?: string); - readonly location: string; -} - -// @public -export const VertexAIError: typeof AIError; - -// @public -export const VertexAIModel: typeof AIModel; - -// @public -export interface VertexAIOptions { - // (undocumented) - location?: string; -} - -// @public -export interface VideoMetadata { - endOffset: string; - startOffset: string; -} - -// @public (undocumented) -export interface WebAttribution { - // (undocumented) - title: string; - // (undocumented) - uri: string; -} - - -``` From 1633e809b7a930fb3e86aea6ae72d803324cea75 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Tue, 4 Nov 2025 14:45:14 -0500 Subject: [PATCH 10/22] clean up and add tests --- .vscode/launch.json | 2 +- packages/ai/src/methods/chat-session.test.ts | 28 +++++++++++++++++++ packages/ai/src/methods/chat-session.ts | 27 +++++++++--------- packages/ai/src/methods/chrome-adapter.ts | 3 +- .../ai/src/models/generative-model.test.ts | 13 +++++++-- 5 files changed, 54 insertions(+), 19 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 1f627304b61..d9d203eb886 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -17,7 +17,7 @@ "src/index.node.ts", "--timeout", "5000", - "src/**/*.test.ts" + "src/**/!(*-browser)*.test.ts" ], "env": { "TS_NODE_COMPILER_OPTIONS": "{\"module\":\"commonjs\"}" diff --git a/packages/ai/src/methods/chat-session.test.ts b/packages/ai/src/methods/chat-session.test.ts index 29951990cb2..f787e261024 100644 --- a/packages/ai/src/methods/chat-session.test.ts +++ b/packages/ai/src/methods/chat-session.test.ts @@ -220,6 +220,34 @@ describe('ChatSession', () => { ); clock.restore(); }); + it('error from stream promise should not be logged', async () => { + const consoleStub = stub(console, 'error'); + stub(generateContentMethods, 'generateContentStream').rejects('foo'); + const chatSession = new ChatSession( + fakeApiSettings, + 'a-model', + fakeChromeAdapter + ); + try { + // This will throw since generateContentStream will reject immediately. + await chatSession.sendMessageStream('hello'); + } catch (_) {} + + expect(consoleStub).to.not.have.been.called; + }); + it('error from final response promise should not be logged', async () => { + const consoleStub = stub(console, 'error'); + stub(generateContentMethods, 'generateContentStream').resolves({ + response: new Promise((_, reject) => reject(new Error())) + } as unknown as GenerateContentStreamResult); + const chatSession = new ChatSession( + fakeApiSettings, + 'a-model', + fakeChromeAdapter + ); + await chatSession.sendMessageStream('hello'); + expect(consoleStub).to.not.have.been.called; + }); it('singleRequestOptions overrides requestOptions', async () => { const generateContentStub = stub( generateContentMethods, diff --git a/packages/ai/src/methods/chat-session.ts b/packages/ai/src/methods/chat-session.ts index 646d9de0bd4..e020fa57aef 100644 --- a/packages/ai/src/methods/chat-session.ts +++ b/packages/ai/src/methods/chat-session.ts @@ -156,8 +156,8 @@ export class ChatSession { this._apiSettings, this.model, generateContentRequest, - // Merge requestOptions this.chromeAdapter, + // Merge requestOptions { ...this.requestOptions, ...singleRequestOptions @@ -167,12 +167,20 @@ export class ChatSession { // Add onto the chain. this._sendPromise = this._sendPromise .then(() => streamPromise) - // This must be handled to avoid unhandled rejection, but jump - // to the final catch block with a label to not log this error. + .then(streamResult => streamResult.response) .catch(_ignored => { throw new Error(SILENT_ERROR); }) - .then(streamResult => streamResult.response) + // We want to log errors that the user cannot catch. + // The user can catch all errors that are thrown from the `streamPromise` and the + // `streamResult.response`, since these are returned to the user in the `GenerateContentResult`. + // The user cannot catch errors that are thrown in the following `then` block, which appends + // the model's response to the chat history. + // + // To prevent us from logging errors that the user *can* catch, we re-throw them as + // SILENT_ERROR, then in the final `catch` block below, we only log errors that are not + // SILENT_ERROR. There is currently no way for these errors to be propagated to the user, + // so we log them to try to make up for this. .then(response => { if (response.candidates && response.candidates.length > 0) { this._history.push(newContent); @@ -192,16 +200,7 @@ export class ChatSession { } }) .catch(e => { - // Errors in streamPromise are already catchable by the user as - // streamPromise is returned. - // Avoid duplicating the error message in logs. - // AbortErrors are thrown after the initial streamPromise resolves, since the request - // may be aborted once streaming has begun. Since these errors won't be wrapped in a SILENT_ERROR, - // we have to explicitly check for them. The user will be able to catch these AbortErrors when - // awaiting the resolution of the result.response. - if (e.message !== SILENT_ERROR && e.name !== 'AbortError') { - // Users do not have access to _sendPromise to catch errors - // downstream from streamPromise, so they should not throw. + if (e.message !== SILENT_ERROR) { logger.error(e); } }); diff --git a/packages/ai/src/methods/chrome-adapter.ts b/packages/ai/src/methods/chrome-adapter.ts index 839276814bb..31288c41267 100644 --- a/packages/ai/src/methods/chrome-adapter.ts +++ b/packages/ai/src/methods/chrome-adapter.ts @@ -400,7 +400,8 @@ export function chromeAdapterFactory( // Do not initialize a ChromeAdapter if we are not in hybrid mode. if (typeof window !== 'undefined' && mode) { return new ChromeAdapterImpl( - (window as Window).LanguageModel as LanguageModel, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (window as unknown as any).LanguageModel as LanguageModel, mode, params ); diff --git a/packages/ai/src/models/generative-model.test.ts b/packages/ai/src/models/generative-model.test.ts index d1abfad4335..7d910082591 100644 --- a/packages/ai/src/models/generative-model.test.ts +++ b/packages/ai/src/models/generative-model.test.ts @@ -407,7 +407,9 @@ describe('GenerativeModel', () => { { model: 'my-model', tools: [ - { functionDeclarations: [{ name: 'myfunc', description: 'mydesc' }] } + { functionDeclarations: [{ name: 'myfunc', description: 'mydesc' }] }, + { googleSearch: {} }, + { urlContext: {} } ], toolConfig: { functionCallingConfig: { mode: FunctionCallingMode.NONE } @@ -420,7 +422,7 @@ describe('GenerativeModel', () => { {}, fakeChromeAdapter ); - expect(genModel.tools?.length).to.equal(1); + expect(genModel.tools?.length).to.equal(3); expect(genModel.toolConfig?.functionCallingConfig?.mode).to.equal( FunctionCallingMode.NONE ); @@ -539,7 +541,12 @@ describe('GenerativeModel', () => { restore(); }); it('calls countTokens', async () => { - const genModel = new GenerativeModel(fakeAI, { model: 'my-model' }); + const genModel = new GenerativeModel( + fakeAI, + { model: 'my-model' }, + {}, + fakeChromeAdapter + ); const mockResponse = getMockResponse( 'vertexAI', 'unary-success-total-tokens.json' From a5b08483624cd98516ab022a7aa072fd5ead5067 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Tue, 4 Nov 2025 14:46:14 -0500 Subject: [PATCH 11/22] revert launch json change --- .vscode/launch.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index d9d203eb886..1f627304b61 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -17,7 +17,7 @@ "src/index.node.ts", "--timeout", "5000", - "src/**/!(*-browser)*.test.ts" + "src/**/*.test.ts" ], "env": { "TS_NODE_COMPILER_OPTIONS": "{\"module\":\"commonjs\"}" From 472b40c34f4bcd17cbbe67e5b7a9fba4e48a0757 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Wed, 12 Nov 2025 15:10:10 -0500 Subject: [PATCH 12/22] revert chrome adapter change --- packages/ai/src/methods/chrome-adapter.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ai/src/methods/chrome-adapter.ts b/packages/ai/src/methods/chrome-adapter.ts index 31288c41267..839276814bb 100644 --- a/packages/ai/src/methods/chrome-adapter.ts +++ b/packages/ai/src/methods/chrome-adapter.ts @@ -400,8 +400,7 @@ export function chromeAdapterFactory( // Do not initialize a ChromeAdapter if we are not in hybrid mode. if (typeof window !== 'undefined' && mode) { return new ChromeAdapterImpl( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (window as unknown as any).LanguageModel as LanguageModel, + (window as Window).LanguageModel as LanguageModel, mode, params ); From dbb11a9cc1a0ea9fdc2d6ac6922a8f1e3058c3d8 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Fri, 14 Nov 2025 10:33:15 -0500 Subject: [PATCH 13/22] fix external signal listener leak --- packages/ai/src/constants.ts | 2 +- packages/ai/src/requests/request.test.ts | 119 +++++++++++++++++++++++ packages/ai/src/requests/request.ts | 20 ++-- 3 files changed, 130 insertions(+), 11 deletions(-) diff --git a/packages/ai/src/constants.ts b/packages/ai/src/constants.ts index 0a6f7e91436..0282edb2e13 100644 --- a/packages/ai/src/constants.ts +++ b/packages/ai/src/constants.ts @@ -32,7 +32,7 @@ export const PACKAGE_VERSION = version; export const LANGUAGE_TAG = 'gl-js'; -export const DEFAULT_FETCH_TIMEOUT_MS = 180 * 1000; +export const DEFAULT_FETCH_TIMEOUT_MS = 180 * 1000; // TODO: Extend default timeout to accommodate for longer generation requests with pro models. /** * Defines the name of the default in-cloud model to use for hybrid inference. diff --git a/packages/ai/src/requests/request.test.ts b/packages/ai/src/requests/request.test.ts index ae5659971b4..bb29a09ae22 100644 --- a/packages/ai/src/requests/request.test.ts +++ b/packages/ai/src/requests/request.test.ts @@ -706,5 +706,124 @@ describe('request methods', () => { expect(fetchOptions.signal).to.be.instanceOf(AbortSignal); expect(fetchOptions.signal?.aborted).to.be.false; }); + + it('should remove abort listener on successful completion to prevent memory leaks', async () => { + const controller = new AbortController(); + const addSpy = Sinon.spy(controller.signal, 'addEventListener'); + const removeSpy = Sinon.spy(controller.signal, 'removeEventListener'); + + const mockResponse = new Response('{}', { + status: 200, + statusText: 'OK' + }); + fetchStub.resolves(mockResponse); + + await makeRequest( + { + model: 'models/model-name', + task: Task.GENERATE_CONTENT, + apiSettings: fakeApiSettings, + stream: false, + singleRequestOptions: { signal: controller.signal } + }, + '{}' + ); + + expect(addSpy).to.have.been.calledOnceWith('abort'); + expect(removeSpy).to.have.been.calledOnceWith('abort'); + }); + + it('should remove listener if fetch itself rejects', async () => { + const controller = new AbortController(); + const removeSpy = Sinon.spy(controller.signal, 'removeEventListener'); + const error = new Error('Network failure'); + fetchStub.rejects(error); + + const requestPromise = makeRequest( + { + model: 'models/model-name', + task: Task.GENERATE_CONTENT, + apiSettings: fakeApiSettings, + stream: false, + singleRequestOptions: { signal: controller.signal } + }, + '{}' + ); + + await expect(requestPromise).to.be.rejectedWith(AIError, /Network failure/); + expect(removeSpy).to.have.been.calledOnce; + }); + + it('should remove listener if response is not ok', async () => { + const controller = new AbortController(); + const removeSpy = Sinon.spy(controller.signal, 'removeEventListener'); + const mockResponse = new Response('{}', { + status: 500, + statusText: 'Internal Server Error' + }); + fetchStub.resolves(mockResponse); + + const requestPromise = makeRequest( + { + model: 'models/model-name', + task: Task.GENERATE_CONTENT, + apiSettings: fakeApiSettings, + stream: false, + singleRequestOptions: { signal: controller.signal } + }, + '{}' + ); + + await expect(requestPromise).to.be.rejectedWith(AIError, /500/); + expect(removeSpy).to.have.been.calledOnce; + }); + + it('should abort immediately if timeout is 0', async () => { + fetchStub.callsFake(fetchAborter); + const requestPromise = makeRequest( + { + model: 'models/model-name', + task: Task.GENERATE_CONTENT, + apiSettings: fakeApiSettings, + stream: false, + singleRequestOptions: { timeout: 0 } + }, + '{}' + ); + + // Tick the clock just enough to trigger a timeout(0) + await clock.tickAsync(1); + + await expect(requestPromise).to.be.rejectedWith( + AIError, + /Timeout has expired/ + ); + }); + + it('should not error if signal is aborted after completion', async () => { + const controller = new AbortController(); + const removeSpy = Sinon.spy(controller.signal, 'removeEventListener'); + const mockResponse = new Response('{}', { + status: 200, + statusText: 'OK' + }); + fetchStub.resolves(mockResponse); + + await makeRequest( + { + model: 'models/model-name', + task: Task.GENERATE_CONTENT, + apiSettings: fakeApiSettings, + stream: false, + singleRequestOptions: { signal: controller.signal } + }, + '{}' + ); + + // Listener should be removed, so this abort should do nothing. + controller.abort('Too late'); + + expect(removeSpy).to.have.been.calledOnce; + }); }); }); diff --git a/packages/ai/src/requests/request.ts b/packages/ai/src/requests/request.ts index d298ef1f5cf..846c543998b 100644 --- a/packages/ai/src/requests/request.ts +++ b/packages/ai/src/requests/request.ts @@ -195,6 +195,12 @@ export async function makeRequest( ); }, timeoutMillis); + const externalAbortListener = (): void => { + logger.debug(`Aborting request to ${url} due to external abort signal.`); + // If this listener was invoked, an external signal was aborted, so externalSignal must be defined. + internalAbortController.abort(externalSignal!.reason); + }; + if (externalSignal) { if (externalSignal.aborted) { clearTimeout(fetchTimeoutId); @@ -204,14 +210,7 @@ export async function makeRequest( ); } - const externalAbortListener = (): void => { - logger.debug(`Aborting request to ${url} due to external abort signal.`); - internalAbortController.abort(externalSignal.reason); - }; - - externalSignal.addEventListener('abort', externalAbortListener, { - once: true - }); + externalSignal.addEventListener('abort', externalAbortListener); } try { @@ -292,8 +291,9 @@ export async function makeRequest( throw err; } finally { - if (fetchTimeoutId) { - clearTimeout(fetchTimeoutId); + clearTimeout(fetchTimeoutId); + if (externalSignal) { + externalSignal.removeEventListener('abort', externalAbortListener); } } return response; From 3557b15a169dd8771a182b75eb61c7c1fa2a7e88 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Fri, 14 Nov 2025 10:36:52 -0500 Subject: [PATCH 14/22] fix sendMessageStream abort signal tests --- packages/ai/src/methods/chat-session.test.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/ai/src/methods/chat-session.test.ts b/packages/ai/src/methods/chat-session.test.ts index f787e261024..f80df114c22 100644 --- a/packages/ai/src/methods/chat-session.test.ts +++ b/packages/ai/src/methods/chat-session.test.ts @@ -249,10 +249,10 @@ describe('ChatSession', () => { expect(consoleStub).to.not.have.been.called; }); it('singleRequestOptions overrides requestOptions', async () => { - const generateContentStub = stub( + const generateContentStreamStub = stub( generateContentMethods, - 'generateContent' - ).rejects('generateContent failed'); // not important + 'generateContentStream' + ).rejects('generateContentStream failed'); // not important const requestOptions = { timeout: 1000 }; @@ -266,9 +266,9 @@ describe('ChatSession', () => { undefined, requestOptions ); - await expect(chatSession.sendMessage('hello', singleRequestOptions)).to.be + await expect(chatSession.sendMessageStream('hello', singleRequestOptions)).to.be .rejected; - expect(generateContentStub).to.be.calledWith( + expect(generateContentStreamStub).to.be.calledWith( fakeApiSettings, 'a-model', match.any, @@ -279,10 +279,10 @@ describe('ChatSession', () => { ); }); it('singleRequestOptions is merged with requestOptions', async () => { - const generateContentStub = stub( + const generateContentStreamStub = stub( generateContentMethods, - 'generateContent' - ).rejects('generateContent failed'); // not important + 'generateContentStream' + ).rejects('generateContentStream failed'); // not important const abortController = new AbortController(); const requestOptions = { timeout: 1000 @@ -297,9 +297,9 @@ describe('ChatSession', () => { undefined, requestOptions ); - await expect(chatSession.sendMessage('hello', singleRequestOptions)).to.be + await expect(chatSession.sendMessageStream('hello', singleRequestOptions)).to.be .rejected; - expect(generateContentStub).to.be.calledWith( + expect(generateContentStreamStub).to.be.calledWith( fakeApiSettings, 'a-model', match.any, From 4a3af45e7e9aafaa9f4cdcab28e5de4a62aed7da Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Fri, 14 Nov 2025 10:44:50 -0500 Subject: [PATCH 15/22] format and fix generative-model tests --- packages/ai/src/methods/chat-session.test.ts | 8 +-- .../ai/src/models/generative-model.test.ts | 50 +++++++++---------- packages/ai/src/requests/request.test.ts | 5 +- 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/packages/ai/src/methods/chat-session.test.ts b/packages/ai/src/methods/chat-session.test.ts index f80df114c22..cfac6a7f488 100644 --- a/packages/ai/src/methods/chat-session.test.ts +++ b/packages/ai/src/methods/chat-session.test.ts @@ -266,8 +266,8 @@ describe('ChatSession', () => { undefined, requestOptions ); - await expect(chatSession.sendMessageStream('hello', singleRequestOptions)).to.be - .rejected; + await expect(chatSession.sendMessageStream('hello', singleRequestOptions)) + .to.be.rejected; expect(generateContentStreamStub).to.be.calledWith( fakeApiSettings, 'a-model', @@ -297,8 +297,8 @@ describe('ChatSession', () => { undefined, requestOptions ); - await expect(chatSession.sendMessageStream('hello', singleRequestOptions)).to.be - .rejected; + await expect(chatSession.sendMessageStream('hello', singleRequestOptions)) + .to.be.rejected; expect(generateContentStreamStub).to.be.calledWith( fakeApiSettings, 'a-model', diff --git a/packages/ai/src/models/generative-model.test.ts b/packages/ai/src/models/generative-model.test.ts index a49a0cc1d73..8d8bfc7c544 100644 --- a/packages/ai/src/models/generative-model.test.ts +++ b/packages/ai/src/models/generative-model.test.ts @@ -293,34 +293,34 @@ describe('GenerativeModel', () => { signal: singleRequestOptions.signal }) ); - it('passes base model params through to ChatSession when there are no startChatParams', async () => { - const genModel = new GenerativeModel(fakeAI, { - model: 'my-model', - generationConfig: { - topK: 1 - } - }); - const chatSession = genModel.startChat(); - expect(chatSession.params?.generationConfig).to.deep.equal({ + }); + it('passes base model params through to ChatSession when there are no startChatParams', async () => { + const genModel = new GenerativeModel(fakeAI, { + model: 'my-model', + generationConfig: { topK: 1 - }); - restore(); + } }); - it('overrides base model params with startChatParams', () => { - const genModel = new GenerativeModel(fakeAI, { - model: 'my-model', - generationConfig: { - topK: 1 - } - }); - const chatSession = genModel.startChat({ - generationConfig: { - topK: 2 - } - }); - expect(chatSession.params?.generationConfig).to.deep.equal({ + const chatSession = genModel.startChat(); + expect(chatSession.params?.generationConfig).to.deep.equal({ + topK: 1 + }); + restore(); + }); + it('overrides base model params with startChatParams', () => { + const genModel = new GenerativeModel(fakeAI, { + model: 'my-model', + generationConfig: { + topK: 1 + } + }); + const chatSession = genModel.startChat({ + generationConfig: { topK: 2 - }); + } + }); + expect(chatSession.params?.generationConfig).to.deep.equal({ + topK: 2 }); }); it('passes params through to chat.sendMessage', async () => { diff --git a/packages/ai/src/requests/request.test.ts b/packages/ai/src/requests/request.test.ts index bb29a09ae22..130b243d4a8 100644 --- a/packages/ai/src/requests/request.test.ts +++ b/packages/ai/src/requests/request.test.ts @@ -750,7 +750,10 @@ describe('request methods', () => { '{}' ); - await expect(requestPromise).to.be.rejectedWith(AIError, /Network failure/); + await expect(requestPromise).to.be.rejectedWith( + AIError, + /Network failure/ + ); expect(removeSpy).to.have.been.calledOnce; }); From b0169523002279f896eb5bef3e78b28295c76ff1 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Fri, 14 Nov 2025 10:45:56 -0500 Subject: [PATCH 16/22] remove console log in tests --- packages/ai/src/requests/request.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ai/src/requests/request.test.ts b/packages/ai/src/requests/request.test.ts index 130b243d4a8..41ff1dfe625 100644 --- a/packages/ai/src/requests/request.test.ts +++ b/packages/ai/src/requests/request.test.ts @@ -304,7 +304,6 @@ describe('request methods', () => { expect(options).to.not.be.undefined; expect(options!.signal).to.not.be.undefined; const signal = options!.signal; - console.log(signal); return new Promise((_resolve, reject): void => { const abortListener = (): void => { reject(new DOMException(signal?.reason || 'Aborted', 'AbortError')); From 40ce3c583433fb0a1b8f7eebb886969d6d33e927 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Fri, 14 Nov 2025 10:48:00 -0500 Subject: [PATCH 17/22] fix imagen test request options assertion --- packages/ai/src/models/imagen-model.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ai/src/models/imagen-model.test.ts b/packages/ai/src/models/imagen-model.test.ts index b56b5ebc389..34470cc1d14 100644 --- a/packages/ai/src/models/imagen-model.test.ts +++ b/packages/ai/src/models/imagen-model.test.ts @@ -167,7 +167,9 @@ describe('ImagenModel', () => { task: request.Task.PREDICT, apiSettings: match.any, stream: false, - singleRequestOptions + singleRequestOptions: { + timeout: singleRequestOptions.timeout + } }, match.any ); From bf1b031ad5a7194cfa855cb0ee47461af60ee84d Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Wed, 19 Nov 2025 10:31:51 -0500 Subject: [PATCH 18/22] throw AbortError if timeout is invoked on request --- .vscode/launch.json | 2 +- common/api-review/ai.api.md | 7 +- .../ai/integration/prompt-templates.test.ts | 26 ++++-- packages/ai/package.json | 2 +- .../models/template-generative-model.test.ts | 90 +++++++++++++++++++ .../src/models/template-generative-model.ts | 22 +++-- .../src/models/template-imagen-model.test.ts | 58 +++++++++++- .../ai/src/models/template-imagen-model.ts | 11 ++- packages/ai/src/requests/request.test.ts | 24 +++-- packages/ai/src/requests/request.ts | 7 +- 10 files changed, 211 insertions(+), 38 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 8f132cbe5c6..55badac87e6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -37,7 +37,7 @@ "src/index.node.ts", "--timeout", "5000", - "integration/**/*.test.ts" + "integration/**/prompt-templates.test.ts" ], "env": { "TS_NODE_COMPILER_OPTIONS": "{\"module\":\"commonjs\"}" diff --git a/common/api-review/ai.api.md b/common/api-review/ai.api.md index 2e42436860b..c5a180e0824 100644 --- a/common/api-review/ai.api.md +++ b/common/api-review/ai.api.md @@ -1338,8 +1338,9 @@ export class TemplateGenerativeModel { constructor(ai: AI, requestOptions?: RequestOptions); // @internal (undocumented) _apiSettings: ApiSettings; - generateContent(templateId: string, templateVariables: object): Promise; - generateContentStream(templateId: string, templateVariables: object): Promise; + generateContent(templateId: string, templateVariables: object, // anything! + singleRequestOptions?: SingleRequestOptions): Promise; + generateContentStream(templateId: string, templateVariables: object, singleRequestOptions?: SingleRequestOptions): Promise; requestOptions?: RequestOptions; } @@ -1348,7 +1349,7 @@ export class TemplateImagenModel { constructor(ai: AI, requestOptions?: RequestOptions); // @internal (undocumented) _apiSettings: ApiSettings; - generateImages(templateId: string, templateVariables: object): Promise>; + generateImages(templateId: string, templateVariables: object, singleRequestOptions?: SingleRequestOptions): Promise>; requestOptions?: RequestOptions; } diff --git a/packages/ai/integration/prompt-templates.test.ts b/packages/ai/integration/prompt-templates.test.ts index 3a7f9038561..ae25d2e1983 100644 --- a/packages/ai/integration/prompt-templates.test.ts +++ b/packages/ai/integration/prompt-templates.test.ts @@ -35,16 +35,25 @@ describe('Prompt templates', function () { describe(`${testConfig.toString()}`, () => { describe('Generative Model', () => { it('successfully generates content', async () => { + const a = new AbortController(); const model = getTemplateGenerativeModel(testConfig.ai, { baseUrl: STAGING_URL }); - const { response } = await model.generateContent( - `sassy-greeting-${templateBackendSuffix( - testConfig.ai.backend.backendType - )}`, - { name: 'John' } - ); - expect(response.text()).to.contain('John'); // Template asks to address directly by name + // a.abort(); + try { + await model.generateContent( + `sassy-greeting-${templateBackendSuffix( + testConfig.ai.backend.backendType + )}`, + { name: 'John' }, + { signal: a.signal, timeout: 100 } + ); + } catch (e) { + console.error(e); + if ((e as DOMException).name === 'AbortError') { + console.log(1); + } + } }); }); describe('Imagen model', async () => { @@ -56,7 +65,8 @@ describe('Prompt templates', function () { `portrait-${templateBackendSuffix( testConfig.ai.backend.backendType )}`, - { animal: 'Rhino' } + { animal: 'Rhino' }, + { timeout: 100} ); expect(images.length).to.equal(2); // We ask for two images in the prompt template }); diff --git a/packages/ai/package.json b/packages/ai/package.json index dcb6f11fdbf..d988d25e734 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -41,7 +41,7 @@ "test:browser": "yarn testsetup && karma start", "test:node": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' mocha --require ts-node/register --require src/index.node.ts 'src/**/!(*-browser)*.test.ts' --config ../../config/mocharc.node.js", "test:integration": "karma start --integration", - "test:integration:node": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' mocha integration/**/*.test.ts --config ../../config/mocharc.node.js", + "test:integration:node": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' mocha integration/**/prompt-templates.test.ts --config ../../config/mocharc.node.js", "api-report": "api-extractor run --local --verbose", "typings:public": "node ../../scripts/build/use_typings.js ./dist/ai-public.d.ts", "type-check": "yarn tsc --noEmit", diff --git a/packages/ai/src/models/template-generative-model.test.ts b/packages/ai/src/models/template-generative-model.test.ts index c3eb43af491..d3f7ec28ffa 100644 --- a/packages/ai/src/models/template-generative-model.test.ts +++ b/packages/ai/src/models/template-generative-model.test.ts @@ -73,6 +73,51 @@ describe('TemplateGenerativeModel', () => { { timeout: 5000 } ); }); + + it('singleRequestOptions overrides requestOptions', async () => { + const templateGenerateContentStub = stub( + generateContentMethods, + 'templateGenerateContent' + ).resolves({} as any); + const model = new TemplateGenerativeModel(fakeAI, { timeout: 1000 }); + const singleRequestOptions = { timeout: 2000 }; + + await model.generateContent( + TEMPLATE_ID, + TEMPLATE_VARS, + singleRequestOptions + ); + + expect(templateGenerateContentStub).to.have.been.calledOnceWith( + model._apiSettings, + TEMPLATE_ID, + { inputs: TEMPLATE_VARS }, + { timeout: 2000 } + ); + }); + + it('singleRequestOptions is merged with requestOptions', async () => { + const templateGenerateContentStub = stub( + generateContentMethods, + 'templateGenerateContent' + ).resolves({} as any); + const abortController = new AbortController(); + const model = new TemplateGenerativeModel(fakeAI, { timeout: 1000 }); + const singleRequestOptions = { signal: abortController.signal }; + + await model.generateContent( + TEMPLATE_ID, + TEMPLATE_VARS, + singleRequestOptions + ); + + expect(templateGenerateContentStub).to.have.been.calledOnceWith( + model._apiSettings, + TEMPLATE_ID, + { inputs: TEMPLATE_VARS }, + { timeout: 1000, signal: abortController.signal } + ); + }); }); describe('generateContentStream', () => { @@ -92,5 +137,50 @@ describe('TemplateGenerativeModel', () => { { timeout: 5000 } ); }); + + it('singleRequestOptions overrides requestOptions', async () => { + const templateGenerateContentStreamStub = stub( + generateContentMethods, + 'templateGenerateContentStream' + ).resolves({} as any); + const model = new TemplateGenerativeModel(fakeAI, { timeout: 1000 }); + const singleRequestOptions = { timeout: 2000 }; + + await model.generateContentStream( + TEMPLATE_ID, + TEMPLATE_VARS, + singleRequestOptions + ); + + expect(templateGenerateContentStreamStub).to.have.been.calledOnceWith( + model._apiSettings, + TEMPLATE_ID, + { inputs: TEMPLATE_VARS }, + { timeout: 2000 } + ); + }); + + it('singleRequestOptions is merged with requestOptions', async () => { + const templateGenerateContentStreamStub = stub( + generateContentMethods, + 'templateGenerateContentStream' + ).resolves({} as any); + const abortController = new AbortController(); + const model = new TemplateGenerativeModel(fakeAI, { timeout: 1000 }); + const singleRequestOptions = { signal: abortController.signal }; + + await model.generateContentStream( + TEMPLATE_ID, + TEMPLATE_VARS, + singleRequestOptions + ); + + expect(templateGenerateContentStreamStub).to.have.been.calledOnceWith( + model._apiSettings, + TEMPLATE_ID, + { inputs: TEMPLATE_VARS }, + { timeout: 1000, signal: abortController.signal } + ); + }); }); }); diff --git a/packages/ai/src/models/template-generative-model.ts b/packages/ai/src/models/template-generative-model.ts index b295489e08e..ccc61253ed9 100644 --- a/packages/ai/src/models/template-generative-model.ts +++ b/packages/ai/src/models/template-generative-model.ts @@ -20,7 +20,11 @@ import { templateGenerateContentStream } from '../methods/generate-content'; import { GenerateContentResult, RequestOptions } from '../types'; -import { AI, GenerateContentStreamResult } from '../public-types'; +import { + AI, + GenerateContentStreamResult, + SingleRequestOptions +} from '../public-types'; import { ApiSettings } from '../types/internal'; import { initApiSettings } from './utils'; @@ -62,13 +66,17 @@ export class TemplateGenerativeModel { */ async generateContent( templateId: string, - templateVariables: object // anything! + templateVariables: object, // anything! + singleRequestOptions?: SingleRequestOptions ): Promise { return templateGenerateContent( this._apiSettings, templateId, { inputs: templateVariables }, - this.requestOptions // TODO: Add singleRequestOptions parameter and merge both request options here. + { + ...this.requestOptions, + ...singleRequestOptions + } ); } @@ -86,13 +94,17 @@ export class TemplateGenerativeModel { */ async generateContentStream( templateId: string, - templateVariables: object + templateVariables: object, + singleRequestOptions?: SingleRequestOptions ): Promise { return templateGenerateContentStream( this._apiSettings, templateId, { inputs: templateVariables }, - this.requestOptions // TODO: Add singleRequestOptions parameter and merge both request options here. + { + ...this.requestOptions, + ...singleRequestOptions + } ); } } diff --git a/packages/ai/src/models/template-imagen-model.test.ts b/packages/ai/src/models/template-imagen-model.test.ts index 7ccfb6ed36d..9451981f83d 100644 --- a/packages/ai/src/models/template-imagen-model.test.ts +++ b/packages/ai/src/models/template-imagen-model.test.ts @@ -18,7 +18,7 @@ import { use, expect } from 'chai'; import sinonChai from 'sinon-chai'; import chaiAsPromised from 'chai-as-promised'; -import { restore, stub } from 'sinon'; +import { restore, stub, match } from 'sinon'; import { AI } from '../public-types'; import { VertexAIBackend } from '../backend'; import { TemplateImagenModel } from './template-imagen-model'; @@ -89,6 +89,62 @@ describe('TemplateImagenModel', () => { ); }); + it('singleRequestOptions overrides requestOptions', async () => { + const mockPrediction = { + 'bytesBase64Encoded': + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', + 'mimeType': 'image/png' + }; + const makeRequestStub = stub(request, 'makeRequest').resolves({ + json: () => Promise.resolve({ predictions: [mockPrediction] }) + } as Response); + const model = new TemplateImagenModel(fakeAI, { timeout: 1000 }); + const singleRequestOptions = { timeout: 2000 }; + + await model.generateImages( + TEMPLATE_ID, + TEMPLATE_VARS, + singleRequestOptions + ); + + expect(makeRequestStub).to.have.been.calledOnceWith( + match({ + singleRequestOptions: { timeout: 2000 } + }), + match.any + ); + }); + + it('singleRequestOptions is merged with requestOptions', async () => { + const mockPrediction = { + 'bytesBase64Encoded': + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', + 'mimeType': 'image/png' + }; + const makeRequestStub = stub(request, 'makeRequest').resolves({ + json: () => Promise.resolve({ predictions: [mockPrediction] }) + } as Response); + const abortController = new AbortController(); + const model = new TemplateImagenModel(fakeAI, { timeout: 1000 }); + const singleRequestOptions = { signal: abortController.signal }; + + await model.generateImages( + TEMPLATE_ID, + TEMPLATE_VARS, + singleRequestOptions + ); + + expect(makeRequestStub).to.have.been.calledOnceWith( + match({ + singleRequestOptions: { + timeout: 1000, + signal: abortController.signal + } + }), + match.any + ); + }); + it('should return the result of handlePredictResponse', async () => { const mockPrediction = { 'bytesBase64Encoded': diff --git a/packages/ai/src/models/template-imagen-model.ts b/packages/ai/src/models/template-imagen-model.ts index c885ba7b237..be4d10f72d0 100644 --- a/packages/ai/src/models/template-imagen-model.ts +++ b/packages/ai/src/models/template-imagen-model.ts @@ -19,7 +19,8 @@ import { RequestOptions } from '../types'; import { AI, ImagenGenerationResponse, - ImagenInlineImage + ImagenInlineImage, + SingleRequestOptions } from '../public-types'; import { ApiSettings } from '../types/internal'; import { makeRequest, ServerPromptTemplateTask } from '../requests/request'; @@ -64,7 +65,8 @@ export class TemplateImagenModel { */ async generateImages( templateId: string, - templateVariables: object + templateVariables: object, + singleRequestOptions?: SingleRequestOptions ): Promise> { const response = await makeRequest( { @@ -72,7 +74,10 @@ export class TemplateImagenModel { templateId, apiSettings: this._apiSettings, stream: false, - singleRequestOptions: this.requestOptions // TODO: Add singleRequestOptions parameter and merge both request options here. + singleRequestOptions: { + ...this.requestOptions, + ...singleRequestOptions + } }, JSON.stringify({ inputs: templateVariables }) ); diff --git a/packages/ai/src/requests/request.test.ts b/packages/ai/src/requests/request.test.ts index 41ff1dfe625..9902ee8f0c6 100644 --- a/packages/ai/src/requests/request.test.ts +++ b/packages/ai/src/requests/request.test.ts @@ -532,8 +532,7 @@ describe('request methods', () => { controller.abort(abortReason); await expect(requestPromise).to.be.rejectedWith( - AIError, - `AI: Error fetching from https://firebasevertexai.googleapis.com/v1beta/projects/my-project/locations/us-central1/models/model-name:generateContent: ${abortReason} (AI/error)` + 'AbortError', ); }); @@ -555,8 +554,8 @@ describe('request methods', () => { await clock.tickAsync(timeoutDuration + 100); await expect(requestPromise).to.be.rejectedWith( - AIError, - /Timeout has expired/ + 'AbortError', + 'Timeout has expired' ); expect(fetchStub).to.have.been.calledOnce; @@ -564,7 +563,8 @@ describe('request methods', () => { const internalSignal = fetchOptions.signal; expect(internalSignal?.aborted).to.be.true; - expect(internalSignal?.reason).to.equal('Timeout has expired.'); + expect((internalSignal?.reason as Error).name).to.equal('AbortError'); + expect((internalSignal?.reason as Error).message).to.equal('Timeout has expired.'); }); it('should succeed and clear timeout if fetch completes before timeout', async () => { @@ -574,6 +574,7 @@ describe('request methods', () => { }); const fetchPromise = Promise.resolve(mockResponse); fetchStub.resolves(fetchPromise); + const clearTimeoutStub = stub(globalThis, 'clearTimeout'); const requestPromise = makeRequest( { @@ -591,7 +592,7 @@ describe('request methods', () => { const response = await requestPromise; expect(response.ok).to.be.true; - + expect(clearTimeoutStub).to.have.been.calledOnce; expect(fetchStub).to.have.been.calledOnce; }); @@ -647,7 +648,7 @@ describe('request methods', () => { await clock.tickAsync(timeoutDuration / 2); controller.abort(abortReason); - await expect(requestPromise).to.be.rejectedWith(AIError, abortReason); + await expect(requestPromise).to.be.rejectedWith('AbortError', abortReason); }); it('should use timeout reason if it occurs before external signal abort', async () => { @@ -677,8 +678,8 @@ describe('request methods', () => { await clock.tickAsync(timeoutDuration + 1); await expect(requestPromise).to.be.rejectedWith( - AIError, - /Timeout has expired/ + 'AbortError', + 'Timeout has expired' ); }); @@ -796,10 +797,7 @@ describe('request methods', () => { // Tick the clock just enough to trigger a timeout(0) await clock.tickAsync(1); - await expect(requestPromise).to.be.rejectedWith( - AIError, - /Timeout has expired/ - ); + await expect(requestPromise).to.be.rejectedWith('AbortError'); }); it('should not error if signal is aborted after completion', async () => { diff --git a/packages/ai/src/requests/request.ts b/packages/ai/src/requests/request.ts index 846c543998b..d01e073afca 100644 --- a/packages/ai/src/requests/request.ts +++ b/packages/ai/src/requests/request.ts @@ -189,7 +189,7 @@ export async function makeRequest( : DEFAULT_FETCH_TIMEOUT_MS; const internalAbortController = new AbortController(); const fetchTimeoutId = setTimeout(() => { - internalAbortController.abort(TIMEOUT_EXPIRED_MESSAGE); + internalAbortController.abort(new DOMException(TIMEOUT_EXPIRED_MESSAGE, ABORT_ERROR_NAME)); logger.debug( `Aborting request to ${url} due to timeout (${timeoutMillis}ms)` ); @@ -198,7 +198,7 @@ export async function makeRequest( const externalAbortListener = (): void => { logger.debug(`Aborting request to ${url} due to external abort signal.`); // If this listener was invoked, an external signal was aborted, so externalSignal must be defined. - internalAbortController.abort(externalSignal!.reason); + internalAbortController.abort(new DOMException(externalSignal!.reason, ABORT_ERROR_NAME)); }; if (externalSignal) { @@ -280,7 +280,8 @@ export async function makeRequest( if ( (e as AIError).code !== AIErrorCode.FETCH_ERROR && (e as AIError).code !== AIErrorCode.API_NOT_ENABLED && - e instanceof Error + e instanceof Error && + (e as DOMException).name !== ABORT_ERROR_NAME ) { err = new AIError( AIErrorCode.ERROR, From 49461e1bf197f4e4b2b18546991d4df29b8c6f4f Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Wed, 19 Nov 2025 10:54:59 -0500 Subject: [PATCH 19/22] assert chatSessionStream throws the request error --- packages/ai/src/methods/chat-session.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/ai/src/methods/chat-session.test.ts b/packages/ai/src/methods/chat-session.test.ts index cfac6a7f488..fb8e7b717dd 100644 --- a/packages/ai/src/methods/chat-session.test.ts +++ b/packages/ai/src/methods/chat-session.test.ts @@ -25,6 +25,7 @@ import { ChatSession } from './chat-session'; import { ApiSettings } from '../types/internal'; import { VertexAIBackend } from '../backend'; import { fakeChromeAdapter } from '../../test-utils/get-fake-firebase-services'; +import { AIError } from '../errors'; use(sinonChai); use(chaiAsPromised); @@ -231,7 +232,9 @@ describe('ChatSession', () => { try { // This will throw since generateContentStream will reject immediately. await chatSession.sendMessageStream('hello'); - } catch (_) {} + } catch (e) { + expect((e as unknown as any).name).to.equal('foo'); + } expect(consoleStub).to.not.have.been.called; }); From 8b3baacdd63001bcf9542c0424158cee4a1f5ed7 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Wed, 19 Nov 2025 13:02:15 -0500 Subject: [PATCH 20/22] format and docs --- docs-devsite/ai.templategenerativemodel.md | 11 +++++++---- docs-devsite/ai.templateimagenmodel.md | 5 +++-- packages/ai/integration/prompt-templates.test.ts | 2 +- packages/ai/src/requests/request.test.ts | 13 ++++++++----- packages/ai/src/requests/request.ts | 8 ++++++-- 5 files changed, 25 insertions(+), 14 deletions(-) diff --git a/docs-devsite/ai.templategenerativemodel.md b/docs-devsite/ai.templategenerativemodel.md index c115af62b1e..a9ed568fa19 100644 --- a/docs-devsite/ai.templategenerativemodel.md +++ b/docs-devsite/ai.templategenerativemodel.md @@ -39,8 +39,8 @@ export declare class TemplateGenerativeModel | Method | Modifiers | Description | | --- | --- | --- | -| [generateContent(templateId, templateVariables)](./ai.templategenerativemodel.md#templategenerativemodelgeneratecontent) | | (Public Preview) Makes a single non-streaming call to the model and returns an object containing a single [GenerateContentResponse](./ai.generatecontentresponse.md#generatecontentresponse_interface). | -| [generateContentStream(templateId, templateVariables)](./ai.templategenerativemodel.md#templategenerativemodelgeneratecontentstream) | | (Public Preview) Makes a single streaming call to the model and returns an object containing an iterable stream that iterates over all chunks in the streaming response as well as a promise that returns the final aggregated response. | +| [generateContent(templateId, templateVariables, singleRequestOptions)](./ai.templategenerativemodel.md#templategenerativemodelgeneratecontent) | | (Public Preview) Makes a single non-streaming call to the model and returns an object containing a single [GenerateContentResponse](./ai.generatecontentresponse.md#generatecontentresponse_interface). | +| [generateContentStream(templateId, templateVariables, singleRequestOptions)](./ai.templategenerativemodel.md#templategenerativemodelgeneratecontentstream) | | (Public Preview) Makes a single streaming call to the model and returns an object containing an iterable stream that iterates over all chunks in the streaming response as well as a promise that returns the final aggregated response. | ## TemplateGenerativeModel.(constructor) @@ -85,7 +85,8 @@ Makes a single non-streaming call to the model and returns an object containing Signature: ```typescript -generateContent(templateId: string, templateVariables: object): Promise; +generateContent(templateId: string, templateVariables: object, // anything! + singleRequestOptions?: SingleRequestOptions): Promise; ``` #### Parameters @@ -94,6 +95,7 @@ generateContent(templateId: string, templateVariables: object): PromiseReturns: @@ -109,7 +111,7 @@ Makes a single streaming call to the model and returns an object containing an i Signature: ```typescript -generateContentStream(templateId: string, templateVariables: object): Promise; +generateContentStream(templateId: string, templateVariables: object, singleRequestOptions?: SingleRequestOptions): Promise; ``` #### Parameters @@ -118,6 +120,7 @@ generateContentStream(templateId: string, templateVariables: object): PromiseReturns: diff --git a/docs-devsite/ai.templateimagenmodel.md b/docs-devsite/ai.templateimagenmodel.md index 2d86071993f..3b33d94f71f 100644 --- a/docs-devsite/ai.templateimagenmodel.md +++ b/docs-devsite/ai.templateimagenmodel.md @@ -39,7 +39,7 @@ export declare class TemplateImagenModel | Method | Modifiers | Description | | --- | --- | --- | -| [generateImages(templateId, templateVariables)](./ai.templateimagenmodel.md#templateimagenmodelgenerateimages) | | (Public Preview) Makes a single call to the model and returns an object containing a single [ImagenGenerationResponse](./ai.imagengenerationresponse.md#imagengenerationresponse_interface). | +| [generateImages(templateId, templateVariables, singleRequestOptions)](./ai.templateimagenmodel.md#templateimagenmodelgenerateimages) | | (Public Preview) Makes a single call to the model and returns an object containing a single [ImagenGenerationResponse](./ai.imagengenerationresponse.md#imagengenerationresponse_interface). | ## TemplateImagenModel.(constructor) @@ -84,7 +84,7 @@ Makes a single call to the model and returns an object containing a single [Imag Signature: ```typescript -generateImages(templateId: string, templateVariables: object): Promise>; +generateImages(templateId: string, templateVariables: object, singleRequestOptions?: SingleRequestOptions): Promise>; ``` #### Parameters @@ -93,6 +93,7 @@ generateImages(templateId: string, templateVariables: object): PromiseReturns: diff --git a/packages/ai/integration/prompt-templates.test.ts b/packages/ai/integration/prompt-templates.test.ts index ae25d2e1983..34424427b8e 100644 --- a/packages/ai/integration/prompt-templates.test.ts +++ b/packages/ai/integration/prompt-templates.test.ts @@ -66,7 +66,7 @@ describe('Prompt templates', function () { testConfig.ai.backend.backendType )}`, { animal: 'Rhino' }, - { timeout: 100} + { timeout: 100 } ); expect(images.length).to.equal(2); // We ask for two images in the prompt template }); diff --git a/packages/ai/src/requests/request.test.ts b/packages/ai/src/requests/request.test.ts index 9902ee8f0c6..a1e15c2623b 100644 --- a/packages/ai/src/requests/request.test.ts +++ b/packages/ai/src/requests/request.test.ts @@ -531,9 +531,7 @@ describe('request methods', () => { await clock.tickAsync(0); controller.abort(abortReason); - await expect(requestPromise).to.be.rejectedWith( - 'AbortError', - ); + await expect(requestPromise).to.be.rejectedWith('AbortError'); }); it('should abort fetch if timeout expires during request', async () => { @@ -564,7 +562,9 @@ describe('request methods', () => { expect(internalSignal?.aborted).to.be.true; expect((internalSignal?.reason as Error).name).to.equal('AbortError'); - expect((internalSignal?.reason as Error).message).to.equal('Timeout has expired.'); + expect((internalSignal?.reason as Error).message).to.equal( + 'Timeout has expired.' + ); }); it('should succeed and clear timeout if fetch completes before timeout', async () => { @@ -648,7 +648,10 @@ describe('request methods', () => { await clock.tickAsync(timeoutDuration / 2); controller.abort(abortReason); - await expect(requestPromise).to.be.rejectedWith('AbortError', abortReason); + await expect(requestPromise).to.be.rejectedWith( + 'AbortError', + abortReason + ); }); it('should use timeout reason if it occurs before external signal abort', async () => { diff --git a/packages/ai/src/requests/request.ts b/packages/ai/src/requests/request.ts index d01e073afca..845948e520f 100644 --- a/packages/ai/src/requests/request.ts +++ b/packages/ai/src/requests/request.ts @@ -189,7 +189,9 @@ export async function makeRequest( : DEFAULT_FETCH_TIMEOUT_MS; const internalAbortController = new AbortController(); const fetchTimeoutId = setTimeout(() => { - internalAbortController.abort(new DOMException(TIMEOUT_EXPIRED_MESSAGE, ABORT_ERROR_NAME)); + internalAbortController.abort( + new DOMException(TIMEOUT_EXPIRED_MESSAGE, ABORT_ERROR_NAME) + ); logger.debug( `Aborting request to ${url} due to timeout (${timeoutMillis}ms)` ); @@ -198,7 +200,9 @@ export async function makeRequest( const externalAbortListener = (): void => { logger.debug(`Aborting request to ${url} due to external abort signal.`); // If this listener was invoked, an external signal was aborted, so externalSignal must be defined. - internalAbortController.abort(new DOMException(externalSignal!.reason, ABORT_ERROR_NAME)); + internalAbortController.abort( + new DOMException(externalSignal!.reason, ABORT_ERROR_NAME) + ); }; if (externalSignal) { From 2988c619c75e74db5b0d298e46d0e68434b92e10 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Wed, 19 Nov 2025 13:02:38 -0500 Subject: [PATCH 21/22] fix lint err --- packages/ai/src/methods/chat-session.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ai/src/methods/chat-session.test.ts b/packages/ai/src/methods/chat-session.test.ts index fb8e7b717dd..a7efd0162bb 100644 --- a/packages/ai/src/methods/chat-session.test.ts +++ b/packages/ai/src/methods/chat-session.test.ts @@ -25,7 +25,6 @@ import { ChatSession } from './chat-session'; import { ApiSettings } from '../types/internal'; import { VertexAIBackend } from '../backend'; import { fakeChromeAdapter } from '../../test-utils/get-fake-firebase-services'; -import { AIError } from '../errors'; use(sinonChai); use(chaiAsPromised); From 2cbd4c4dd0ded23ccb693599d0705ce608eaaa34 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Wed, 19 Nov 2025 16:58:26 -0500 Subject: [PATCH 22/22] fix issue where externalSignal event listener is cleared during stream --- packages/ai/src/requests/request.ts | 34 +++++++++++------------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/packages/ai/src/requests/request.ts b/packages/ai/src/requests/request.ts index 845948e520f..5ea1c3287c4 100644 --- a/packages/ai/src/requests/request.ts +++ b/packages/ai/src/requests/request.ts @@ -187,6 +187,7 @@ export async function makeRequest( requestUrlParams.singleRequestOptions.timeout >= 0 ? requestUrlParams.singleRequestOptions.timeout : DEFAULT_FETCH_TIMEOUT_MS; + const internalAbortController = new AbortController(); const fetchTimeoutId = setTimeout(() => { internalAbortController.abort( @@ -197,31 +198,25 @@ export async function makeRequest( ); }, timeoutMillis); - const externalAbortListener = (): void => { - logger.debug(`Aborting request to ${url} due to external abort signal.`); - // If this listener was invoked, an external signal was aborted, so externalSignal must be defined. - internalAbortController.abort( - new DOMException(externalSignal!.reason, ABORT_ERROR_NAME) - ); - }; + const combinedSignal = AbortSignal.any( + externalSignal + ? [externalSignal, internalAbortController.signal] + : [internalAbortController.signal] + ); - if (externalSignal) { - if (externalSignal.aborted) { - clearTimeout(fetchTimeoutId); - throw new DOMException( - externalSignal.reason ?? 'Aborted externally before fetch', - ABORT_ERROR_NAME - ); - } - - externalSignal.addEventListener('abort', externalAbortListener); + if (externalSignal && externalSignal.aborted) { + clearTimeout(fetchTimeoutId); + throw new DOMException( + externalSignal.reason ?? 'Aborted externally before fetch', + ABORT_ERROR_NAME + ); } try { const fetchOptions: RequestInit = { method: 'POST', headers: await getHeaders(url), - signal: internalAbortController.signal, + signal: combinedSignal, body }; @@ -297,9 +292,6 @@ export async function makeRequest( throw err; } finally { clearTimeout(fetchTimeoutId); - if (externalSignal) { - externalSignal.removeEventListener('abort', externalAbortListener); - } } return response; }