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;
+}