diff --git a/common/api-review/generative-ai-server.api.md b/common/api-review/generative-ai-server.api.md index b4963e1ef..dc5a20d83 100644 --- a/common/api-review/generative-ai-server.api.md +++ b/common/api-review/generative-ai-server.api.md @@ -4,6 +4,8 @@ ```ts +/// + // Warning: (ae-incompatible-release-tags) The symbol "ArraySchema" is marked as @public, but its signature references "BaseSchema" which is marked as @internal // // @public @@ -31,8 +33,6 @@ export interface BooleanSchema extends BaseSchema { type: typeof SchemaType.BOOLEAN; } -/// - // @public export interface CachedContent extends CachedContentBase { createTime?: string; diff --git a/common/api-review/generative-ai.api.md b/common/api-review/generative-ai.api.md index 0b32ff57b..febb59458 100644 --- a/common/api-review/generative-ai.api.md +++ b/common/api-review/generative-ai.api.md @@ -537,6 +537,7 @@ export class GoogleGenerativeAI { apiKey: string; getGenerativeModel(modelParams: ModelParams, requestOptions?: RequestOptions): GenerativeModel; getGenerativeModelFromCachedContent(cachedContent: CachedContent, modelParams?: Partial, requestOptions?: RequestOptions): GenerativeModel; + listModels(pageSize?: number, pageToken?: string, requestOptions?: RequestOptions): Promise; } // @public diff --git a/src/gen-ai.test.ts b/src/gen-ai.test.ts index fd17aa4ef..2112d8828 100644 --- a/src/gen-ai.test.ts +++ b/src/gen-ai.test.ts @@ -118,4 +118,114 @@ describe("GoogleGenerativeAI", () => { `Different value for "systemInstruction" specified in modelParams (yo) and cachedContent (hi)`, ); }); + + it("listModels gets a Response without any params passed", () => { + const genAI = new GoogleGenerativeAI("apikey"); + const genModel = genAI.listModels(); + expect(genModel).to.be.an.instanceOf(Promise); + }); + it("listModels gets a Response when passed only pageSize", async () => { + const genAI = new GoogleGenerativeAI("apikey"); + const genModel = genAI.listModels({ pageSize: 10 }); + expect(genModel).to.be.an.instanceOf(Promise); + }); + it("listModels gets a Response when passed only pageToken", async () => { + const genAI = new GoogleGenerativeAI("apikey"); + const genModel = genAI.listModels({ pageToken: "token" }); + expect(genModel).to.be.an.instanceOf(Promise); + }); + it("listModels gets a Response when passed both pageSize and pageToken", async () => { + const genAI = new GoogleGenerativeAI("apikey"); + const genModel = genAI.listModels({ pageSize: 10, pageToken: "token" }); + expect(genModel).to.be.an.instanceOf(Promise); + }); + + it("listTunedModels gets a Response without any params passed", () => { + const genAI = new GoogleGenerativeAI("apikey"); + const genModel = genAI.listTunedModels(); + expect(genModel).to.be.an.instanceOf(Promise); + }); + it("listTunedModels gets a Response when passed only pageSize", async () => { + const genAI = new GoogleGenerativeAI("apikey"); + const genModel = genAI.listTunedModels({ pageSize: 10 }); + expect(genModel).to.be.an.instanceOf(Promise); + }); + it("listTunedModels gets a Response when passed only pageToken", async () => { + const genAI = new GoogleGenerativeAI("apikey"); + const genModel = genAI.listTunedModels({ pageToken: "token" }); + expect(genModel).to.be.an.instanceOf(Promise); + }); + it("listTunedModels gets a Response when passed only filter", async () => { + const genAI = new GoogleGenerativeAI("apikey"); + const genModel = genAI.listTunedModels({ filter: "filter" }); + expect(genModel).to.be.an.instanceOf(Promise); + }); + it("listTunedModels gets a Response when passed all params pageSize, pageToken and filter", async () => { + const genAI = new GoogleGenerativeAI("apikey"); + const genModel = genAI.listTunedModels({ pageSize: 10, pageToken: "token", filter: "filter" }); + expect(genModel).to.be.an.instanceOf(Promise); + }); + + it("listCachedContent gets a Response without any params passed", () => { + const genAI = new GoogleGenerativeAI("apikey"); + const genModel = genAI.listCachedContent(); + expect(genModel).to.be.an.instanceOf(Promise); + }); + it("listCachedContent gets a Response when passed only pageSize", async () => { + const genAI = new GoogleGenerativeAI("apikey"); + const genModel = genAI.listCachedContent({ pageSize: 10 }); + expect(genModel).to.be.an.instanceOf(Promise); + }); + it("listCachedContent gets a Response when passed only pageToken", async () => { + const genAI = new GoogleGenerativeAI("apikey"); + const genModel = genAI.listCachedContent({ pageToken: "token" }); + expect(genModel).to.be.an.instanceOf(Promise); + }); + it("listCachedContent gets a Response when passed both pageSize and pageToken", async () => { + const genAI = new GoogleGenerativeAI("apikey"); + const genModel = genAI.listCachedContent({ pageSize: 10, pageToken: "token" }); + expect(genModel).to.be.an.instanceOf(Promise); + }); + + it("listCorpora gets a Response without any params passed", () => { + const genAI = new GoogleGenerativeAI("apikey"); + const genModel = genAI.listCorpora(); + expect(genModel).to.be.an.instanceOf(Promise); + }); + it("listCorpora gets a Response when passed only pageSize", async () => { + const genAI = new GoogleGenerativeAI("apikey"); + const genModel = genAI.listCorpora({ pageSize: 10 }); + expect(genModel).to.be.an.instanceOf(Promise); + }); + it("listCorpora gets a Response when passed only pageToken", async () => { + const genAI = new GoogleGenerativeAI("apikey"); + const genModel = genAI.listCorpora({ pageToken: "token" }); + expect(genModel).to.be.an.instanceOf(Promise); + }); + it("listCorpora gets a Response when passed both pageSize and pageToken", async () => { + const genAI = new GoogleGenerativeAI("apikey"); + const genModel = genAI.listCorpora({ pageSize: 10, pageToken: "token" }); + expect(genModel).to.be.an.instanceOf(Promise); + }); + + it("listFiles gets a Response without any params passed", () => { + const genAI = new GoogleGenerativeAI("apikey"); + const genModel = genAI.listFiles(); + expect(genModel).to.be.an.instanceOf(Promise); + }); + it("listFiles gets a Response when passed only pageSize", async () => { + const genAI = new GoogleGenerativeAI("apikey"); + const genModel = genAI.listFiles({ pageSize: 10 }); + expect(genModel).to.be.an.instanceOf(Promise); + }); + it("listFiles gets a Response when passed only pageToken", async () => { + const genAI = new GoogleGenerativeAI("apikey"); + const genModel = genAI.listFiles({ pageToken: "token" }); + expect(genModel).to.be.an.instanceOf(Promise); + }); + it("listFiles gets a Response when passed both pageSize and pageToken", async () => { + const genAI = new GoogleGenerativeAI("apikey"); + const genModel = genAI.listFiles({ pageSize: 10, pageToken: "token" }); + expect(genModel).to.be.an.instanceOf(Promise); + }); }); diff --git a/src/gen-ai.ts b/src/gen-ai.ts index f65f489db..8f79512d5 100644 --- a/src/gen-ai.ts +++ b/src/gen-ai.ts @@ -21,6 +21,7 @@ import { } from "./errors"; import { CachedContent, ModelParams, RequestOptions } from "../types"; import { GenerativeModel } from "./models/generative-model"; +import { List, makeListRequest } from "./requests/request-list"; export { ChatSession } from "./methods/chat-session"; export { GenerativeModel }; @@ -30,7 +31,7 @@ export { GenerativeModel }; * @public */ export class GoogleGenerativeAI { - constructor(public apiKey: string) {} + constructor(public apiKey: string) { } /** * Gets a {@link GenerativeModel} instance for the provided model name. @@ -42,7 +43,7 @@ export class GoogleGenerativeAI { if (!modelParams.model) { throw new GoogleGenerativeAIError( `Must provide a model name. ` + - `Example: genai.getGenerativeModel({ model: 'my-model-name' })`, + `Example: genai.getGenerativeModel({ model: 'my-model-name' })`, ); } return new GenerativeModel(this.apiKey, modelParams, requestOptions); @@ -93,7 +94,7 @@ export class GoogleGenerativeAI { } throw new GoogleGenerativeAIRequestInputError( `Different value for "${key}" specified in modelParams` + - ` (${modelParams[key]}) and cachedContent (${cachedContent[key]})`, + ` (${modelParams[key]}) and cachedContent (${cachedContent[key]})`, ); } } @@ -112,4 +113,115 @@ export class GoogleGenerativeAI { requestOptions, ); } + + /** + * Gets a list of {@link GenerativeModel} available. + */ + async listModels( + params: { pageSize?: number; pageToken?: string } = {}, + requestOptions?: RequestOptions, + ): Promise { + + const filteredParams = Object.fromEntries( + Object.entries({ params }).filter(([_, v]) => v != null) + ); + + const response = await makeListRequest( + List.MODELS, + this.apiKey, + filteredParams, + requestOptions, + ); + + return response.json(); + } + + /** + * Gets a list of tuned {@link GenerativeModel} available. + */ + async listTunedModels( + params: { pageSize?: number; pageToken?: string; filter?: string } = {}, + requestOptions?: RequestOptions, + ): Promise { + + const filteredParams = Object.fromEntries( + Object.entries({ params }).filter(([_, v]) => v != null) + ); + + const response = await makeListRequest( + List.TUNED_MODELS, + this.apiKey, + filteredParams, + requestOptions, + ); + + return response.json(); + } + + /** + * Gets a list of CachedContent. + */ + async listCachedContent( + params: { pageSize?: number; pageToken?: string } = {}, + requestOptions?: RequestOptions, + ): Promise { + + const filteredParams = Object.fromEntries( + Object.entries({ params }).filter(([_, v]) => v != null) + ); + + const response = await makeListRequest( + List.CASHED_CONTENTS, + this.apiKey, + filteredParams, + requestOptions, + ); + + return response.json(); + } + + /** + * Gets a list of all Corpora owned by the user. + */ + async listCorpora( + params: { pageSize?: number; pageToken?: string } = {}, + requestOptions?: RequestOptions, + ): Promise { + + const filteredParams = Object.fromEntries( + Object.entries({ params }).filter(([_, v]) => v != null) + ); + + const response = await makeListRequest( + List.CORPORA, + this.apiKey, + filteredParams, + requestOptions, + ); + + return response.json(); + } + + /** + * Gets a list of the metadata for Files owned by the requesting project. + */ + async listFiles( + params: { pageSize?: number; pageToken?: string } = {}, + requestOptions?: RequestOptions, + ): Promise { + + const filteredParams = Object.fromEntries( + Object.entries({ params }).filter(([_, v]) => v != null) + ); + + const response = await makeListRequest( + List.CORPORA, + this.apiKey, + filteredParams, + requestOptions, + ); + + return response.json(); + } + } diff --git a/src/requests/request-list.test.ts b/src/requests/request-list.test.ts new file mode 100644 index 000000000..32ed0ddd8 --- /dev/null +++ b/src/requests/request-list.test.ts @@ -0,0 +1,331 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, use } from "chai"; +import { match, restore, stub } from "sinon"; +import * as sinonChai from "sinon-chai"; +import * as chaiAsPromised from "chai-as-promised"; +import { + DEFAULT_API_VERSION, + DEFAULT_BASE_URL, + List, + RequestUrl, + getListRequest, + makeListRequest +} from "./request-list"; +import { + GoogleGenerativeAIAbortError, + GoogleGenerativeAIFetchError, + GoogleGenerativeAIRequestInputError, +} from "../errors"; + +use(sinonChai); +use(chaiAsPromised); + +describe("request-list methods", () => { + afterEach(() => { + restore(); + }); + describe("RequestUrl", () => { + it("stream", async () => { + const url = new RequestUrl( + List.MODELS, + "key", + {}, + ); + expect(url.toString()).to.include(DEFAULT_API_VERSION); + expect(url.toString()).to.include(DEFAULT_BASE_URL); + expect(url.toString()).to.include("/models"); + expect(url.toString()).to.not.include("key"); + }); + it("custom apiVersion", async () => { + const url = new RequestUrl( + List.MODELS, + "key", + { apiVersion: "v2beta" }, + ); + expect(url.toString()).to.include("/v2beta/models"); + }); + it("custom baseUrl", async () => { + const url = new RequestUrl( + List.MODELS, + "key", + { baseUrl: "http://my.staging.website" }, + ); + expect(url.toString()).to.include("http://my.staging.website"); + }); + }); + describe("constructRequest", () => { + it("handles basic request", async () => { + const request = await getListRequest( + List.MODELS, + "key", + {}, + ); + expect( + (request.fetchOptions.headers as Headers).get("x-goog-api-client"), + ).to.equal("genai-js/__PACKAGE_VERSION__"); + expect( + (request.fetchOptions.headers as Headers).get("x-goog-api-key"), + ).to.equal("key"); + expect( + (request.fetchOptions.headers as Headers).get("Content-Type"), + ).to.equal("application/json"); + }); + it("passes apiClient", async () => { + const request = await getListRequest( + List.MODELS, + "key", + { + apiClient: "client/version", + }, + ); + expect( + (request.fetchOptions.headers as Headers).get("x-goog-api-client"), + ).to.equal("client/version genai-js/__PACKAGE_VERSION__"); + }); + it("passes timeout", async () => { + const request = await getListRequest( + List.MODELS, + "key", + { + timeout: 5000, + }, + ); + expect(request.fetchOptions.signal).to.be.instanceOf(AbortSignal); + }); + it("passes custom headers", async () => { + const request = await getListRequest( + List.MODELS, + "key", + { + customHeaders: new Headers({ customerHeader: "customerHeaderValue" }), + }, + ); + expect( + (request.fetchOptions.headers as Headers).get("customerHeader"), + ).to.equal("customerHeaderValue"); + }); + it("passes custom x-goog-api-client header", async () => { + await expect( + getListRequest( + List.MODELS, + "key", + { + customHeaders: new Headers({ + "x-goog-api-client": "client/version", + }), + }, + ), + ).to.be.rejectedWith(GoogleGenerativeAIRequestInputError); + }); + it("passes apiClient and custom x-goog-api-client header", async () => { + await expect( + getListRequest( + List.MODELS, + "key", + { + apiClient: "client/version", + customHeaders: new Headers({ + "x-goog-api-client": "client/version2", + }), + }, + ), + ).to.be.rejectedWith(GoogleGenerativeAIRequestInputError); + }); + }); + describe("makeListRequest", () => { + it("no error", async () => { + const fetchStub = stub().resolves({ + ok: true, + } as Response); + const response = await makeListRequest( + List.MODELS, + "key", + {}, + {}, + fetchStub as typeof fetch, + ); + expect(fetchStub).to.be.calledWith(match.string, { + method: "GET", + headers: match.instanceOf(Headers), + }); + expect(response.ok).to.be.true; + }); + it("error with local timeout", async () => { + const abortError = new DOMException("Request timeout.", "AbortError"); + const fetchStub = stub().rejects(abortError); + + try { + await makeListRequest( + List.MODELS, + "key", + {}, + { + timeout: 100, + }, + fetchStub as typeof fetch, + ); + } catch (e) { + expect((e as GoogleGenerativeAIAbortError).message).to.include( + "Request aborted", + ); + } + expect(fetchStub).to.be.calledOnce; + }); + it("error with server timeout", async () => { + const fetchStub = stub().resolves({ + ok: false, + status: 500, + statusText: "AbortError", + } as Response); + + try { + await makeListRequest( + List.MODELS, + "key", + {}, + { + timeout: 0, + }, + fetchStub as typeof fetch, + ); + } catch (e) { + expect((e as GoogleGenerativeAIFetchError).status).to.equal(500); + expect((e as GoogleGenerativeAIFetchError).statusText).to.equal( + "AbortError", + ); + expect((e as GoogleGenerativeAIFetchError).message).to.include( + "500 AbortError", + ); + } + expect(fetchStub).to.be.calledOnce; + }); + it("Network error, no response.json()", async () => { + const fetchStub = stub().resolves({ + ok: false, + status: 500, + statusText: "Server Error", + } as Response); + try { + await makeListRequest( + List.MODELS, + "key", + {}, + {}, + fetchStub as typeof fetch, + ); + } catch (e) { + expect((e as GoogleGenerativeAIFetchError).status).to.equal(500); + expect((e as GoogleGenerativeAIFetchError).statusText).to.equal( + "Server Error", + ); + expect((e as GoogleGenerativeAIFetchError).message).to.include( + "500 Server Error", + ); + } + expect(fetchStub).to.be.calledOnce; + }); + it("Network error, includes response.json()", async () => { + const fetchStub = stub().resolves({ + ok: false, + status: 500, + statusText: "Server Error", + json: () => Promise.resolve({ error: { message: "extra info" } }), + } as Response); + + try { + await makeListRequest( + List.MODELS, + "key", + {}, + {}, + fetchStub as typeof fetch, + ); + } catch (e) { + expect((e as GoogleGenerativeAIFetchError).status).to.equal(500); + expect((e as GoogleGenerativeAIFetchError).statusText).to.equal( + "Server Error", + ); + expect((e as GoogleGenerativeAIFetchError).message).to.match( + /500 Server Error.*extra info/, + ); + } + expect(fetchStub).to.be.calledOnce; + }); + it("Network error, includes response.json() and details", async () => { + const fetchStub = stub().resolves({ + ok: false, + status: 500, + statusText: "Server Error", + json: () => + Promise.resolve({ + error: { + message: "extra info", + details: [ + { + "@type": "type.googleapis.com/google.rpc.DebugInfo", + detail: + "[ORIGINAL ERROR] generic::invalid_argument: invalid status photos.thumbnailer.Status.Code::5: Source image 0 too short", + }, + ], + }, + }), + } as Response); + + try { + await makeListRequest( + List.MODELS, + "key", + {}, + {}, + fetchStub as typeof fetch, + ); + } catch (e) { + expect((e as GoogleGenerativeAIFetchError).status).to.equal(500); + expect((e as GoogleGenerativeAIFetchError).statusText).to.equal( + "Server Error", + ); + expect( + (e as GoogleGenerativeAIFetchError).errorDetails[0].detail, + ).to.equal( + "[ORIGINAL ERROR] generic::invalid_argument: invalid status photos.thumbnailer.Status.Code::5: Source image 0 too short", + ); + expect((e as GoogleGenerativeAIFetchError).message).to.match( + /500 Server Error.*extra info.*generic::invalid_argument/, + ); + } + expect(fetchStub).to.be.calledOnce; + }); + it("has invalid custom header", async () => { + const fetchStub = stub(); + await expect( + makeListRequest( + List.MODELS, + "key", + {}, + { + customHeaders: new Headers({ + "x-goog-api-client": "client/version", + }), + }, + fetchStub as typeof fetch, + ), + ).to.be.rejectedWith(GoogleGenerativeAIRequestInputError); + }); + }); +}); diff --git a/src/requests/request-list.ts b/src/requests/request-list.ts new file mode 100644 index 000000000..3867a31e1 --- /dev/null +++ b/src/requests/request-list.ts @@ -0,0 +1,233 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RequestOptions, SingleRequestOptions } from "../../types"; +import { + GoogleGenerativeAIAbortError, + GoogleGenerativeAIError, + GoogleGenerativeAIFetchError, + GoogleGenerativeAIRequestInputError, +} from "../errors"; + +export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com"; + +export const DEFAULT_API_VERSION = "v1beta"; + +/** + * We can't `require` package.json if this runs on web. We will use rollup to + * swap in the version number here at build time. + */ +const PACKAGE_VERSION = "__PACKAGE_VERSION__"; +const PACKAGE_LOG_HEADER = "genai-js"; + +export enum List { + MODELS = "models", + TUNED_MODELS = "tunedModels", + CASHED_CONTENTS = "cachedContents", + CORPORA = "corpora", + FILES = "files", +} + +export class RequestUrl { + constructor( + public list: List, + public apiKey: string, + public requestOptions: RequestOptions, + ) { } + toString(): string { + const apiVersion = this.requestOptions?.apiVersion || DEFAULT_API_VERSION; + const baseUrl = this.requestOptions?.baseUrl || DEFAULT_BASE_URL; + return `${baseUrl}/${apiVersion}/${this.list}`; + } +} + +/** + * Simple, but may become more complex if we add more versions to log. + */ +export function getClientHeaders(requestOptions: RequestOptions): string { + const clientHeaders = []; + if (requestOptions?.apiClient) { + clientHeaders.push(requestOptions.apiClient); + } + clientHeaders.push(`${PACKAGE_LOG_HEADER}/${PACKAGE_VERSION}`); + return clientHeaders.join(" "); +} + +export async function getHeaders(url: RequestUrl): Promise { + const headers = new Headers(); + headers.append("Content-Type", "application/json"); + headers.append("x-goog-api-client", getClientHeaders(url.requestOptions)); + headers.append("x-goog-api-key", url.apiKey); + + let customHeaders = url.requestOptions?.customHeaders; + if (customHeaders) { + if (!(customHeaders instanceof Headers)) { + try { + customHeaders = new Headers(customHeaders); + } catch (e) { + throw new GoogleGenerativeAIRequestInputError( + `unable to convert customHeaders value ${JSON.stringify( + customHeaders, + )} to Headers: ${e.message}`, + ); + } + } + + for (const [headerName, headerValue] of customHeaders.entries()) { + if (headerName === "x-goog-api-key") { + throw new GoogleGenerativeAIRequestInputError( + `Cannot set reserved header name ${headerName}`, + ); + } else if (headerName === "x-goog-api-client") { + throw new GoogleGenerativeAIRequestInputError( + `Header name ${headerName} can only be set using the apiClient field`, + ); + } + + headers.append(headerName, headerValue); + } + } + + return headers; +} + +export async function getListRequest( + list: List, + apiKey: string, + requestOptions: SingleRequestOptions, +): Promise<{ url: string; fetchOptions: RequestInit }> { + const url = new RequestUrl(list, apiKey, requestOptions); + return { + url: url.toString(), + fetchOptions: { + ...buildFetchOptions(requestOptions), + method: "GET", + headers: await getHeaders(url), + }, + }; +} + +export async function makeListRequest( + list: List, + apiKey: string, + params?: {}, + requestOptions: SingleRequestOptions = {}, + // Allows this to be stubbed for tests + fetchFn = fetch, +): Promise { + const { url, fetchOptions } = await getListRequest( + list, + apiKey, + requestOptions, + ); + + let finalUrl = url; + + if (params && Object.keys(params).length > 0) { + const queryString = new URLSearchParams(params).toString(); + finalUrl += `?${queryString}`; + } + + return makeRequest(finalUrl, fetchOptions, fetchFn); +} + +export async function makeRequest( + url: string, + fetchOptions: RequestInit, + fetchFn = fetch, +): Promise { + let response; + try { + response = await fetchFn(url, fetchOptions); + } catch (e) { + handleResponseError(e, url); + } + + if (!response.ok) { + await handleResponseNotOk(response, url); + } + + return response; +} + +function handleResponseError(e: Error, url: string): void { + let err = e; + if (err.name === "AbortError") { + err = new GoogleGenerativeAIAbortError( + `Request aborted when fetching ${url.toString()}: ${e.message}`, + ); + err.stack = e.stack; + } else if ( + !( + e instanceof GoogleGenerativeAIFetchError || + e instanceof GoogleGenerativeAIRequestInputError + ) + ) { + err = new GoogleGenerativeAIError( + `Error fetching from ${url.toString()}: ${e.message}`, + ); + err.stack = e.stack; + } + throw err; +} + +async function handleResponseNotOk( + response: Response, + url: string, +): Promise { + let message = ""; + let errorDetails; + try { + const json = await response.json(); + message = json.error.message; + if (json.error.details) { + message += ` ${JSON.stringify(json.error.details)}`; + errorDetails = json.error.details; + } + } catch (e) { + // ignored + } + throw new GoogleGenerativeAIFetchError( + `Error fetching from ${url.toString()}: [${response.status} ${response.statusText + }] ${message}`, + response.status, + response.statusText, + errorDetails, + ); +} + +/** + * Generates the request options to be passed to the fetch API. + * @param requestOptions - The user-defined request options. + * @returns The generated request options. + */ +function buildFetchOptions(requestOptions?: SingleRequestOptions): RequestInit { + const fetchOptions = {} as RequestInit; + if (requestOptions?.signal !== undefined || requestOptions?.timeout >= 0) { + const controller = new AbortController(); + if (requestOptions?.timeout >= 0) { + setTimeout(() => controller.abort(), requestOptions.timeout); + } + if (requestOptions?.signal) { + requestOptions.signal.addEventListener("abort", () => { + controller.abort(); + }); + } + fetchOptions.signal = controller.signal; + } + return fetchOptions; +}