diff --git a/.eslintrc.json b/.eslintrc.json index 7e5a1dd078..44510807ab 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -12,6 +12,7 @@ "node/no-unsupported-features/es-syntax": ["off"] }, "parserOptions": { + "ecmaVersion": 2020, "sourceType": "module" } } diff --git a/.github/config/nodejs-dev.jsonc b/.github/config/nodejs-dev.jsonc index d1c6f6f801..57cefd057b 100644 --- a/.github/config/nodejs-dev.jsonc +++ b/.github/config/nodejs-dev.jsonc @@ -185,6 +185,7 @@ "functions/v2/tips/retry", "functions/v2/typed/googlechatbot", "functions/v2/typed/greeting", + "genai", // parent directory "generative-ai/snippets", "healthcare/consent", "healthcare/datasets", diff --git a/.github/config/nodejs.jsonc b/.github/config/nodejs.jsonc index 65d19696e6..4e3cb6de8e 100644 --- a/.github/config/nodejs.jsonc +++ b/.github/config/nodejs.jsonc @@ -70,6 +70,7 @@ "functions/http/uploadFile", // no tests exist "functions/log", // parent directory "functions/pubsub", // parent directory + "genai", // parent directory "memorystore/redis", // parent directory "recaptcha_enterprise/demosite/app", // no tests exist diff --git a/ai-platform/snippets/test/imagen.test.js b/ai-platform/snippets/test/imagen.test.js index fa6aa1cbe6..97293f672b 100644 --- a/ai-platform/snippets/test/imagen.test.js +++ b/ai-platform/snippets/test/imagen.test.js @@ -66,7 +66,8 @@ describe('AI platform edit image using Imagen inpainting and outpainting', () => }); }); -describe('AI platform get image captions and responses using Imagen', () => { +// b/452720552 +describe.skip('AI platform get image captions and responses using Imagen', () => { it('should get short form captions for an image', async () => { const stdout = execSync('node ./imagen-get-short-form-image-captions.js', { cwd, diff --git a/appengine/building-an-app/build/app.yaml b/appengine/building-an-app/build/app.yaml index e865ee3bab..8896d06b25 100755 --- a/appengine/building-an-app/build/app.yaml +++ b/appengine/building-an-app/build/app.yaml @@ -12,5 +12,5 @@ # limitations under the License. # [START gae_build_app_yaml_node] -runtime: nodejs20 +runtime: nodejs24 # [END gae_build_app_yaml_node] diff --git a/appengine/building-an-app/build/package.json b/appengine/building-an-app/build/package.json index 1e1e483490..e73a78d1b2 100644 --- a/appengine/building-an-app/build/package.json +++ b/appengine/building-an-app/build/package.json @@ -14,7 +14,7 @@ "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" }, "engines": { - "node": "20.x.x" + "node": "24.x.x" }, "author": "Google Inc.", "license": "Apache-2.0", diff --git a/appengine/hello-world/flexible/app.yaml b/appengine/hello-world/flexible/app.yaml index 5be91b6d06..e1253e2a89 100644 --- a/appengine/hello-world/flexible/app.yaml +++ b/appengine/hello-world/flexible/app.yaml @@ -16,7 +16,7 @@ runtime: nodejs env: flex runtime_config: - operating_system: ubuntu22 + operating_system: ubuntu24 # This sample incurs costs to run on the App Engine flexible environment. # The settings below are to reduce costs during testing and are not appropriate # for production use. For more information, see: diff --git a/auth/.eslintrc.json b/auth/.eslintrc.json new file mode 100644 index 0000000000..95f214a816 --- /dev/null +++ b/auth/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": "../.eslintrc.json", + "rules": { + "no-unused-vars": "off" + } +} diff --git a/auth/README.md b/auth/README.md index 636015860f..15d899867d 100644 --- a/auth/README.md +++ b/auth/README.md @@ -64,4 +64,4 @@ information](https://developers.google.com/identity/protocols/application-defaul For more information on downscoped credentials you can visit: -> https://github.com/googleapis/google-auth-library-nodejs \ No newline at end of file +> https://github.com/googleapis/google-auth-library-nodejs diff --git a/auth/customcredentials/aws/Dockerfile b/auth/customcredentials/aws/Dockerfile new file mode 100644 index 0000000000..7f8e9cc0b3 --- /dev/null +++ b/auth/customcredentials/aws/Dockerfile @@ -0,0 +1,15 @@ +FROM node:20-slim + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install --omit=dev + +RUN useradd -m appuser + +COPY --chown=appuser:appuser . . + +USER appuser + +CMD [ "node", "customCredentialSupplierAws.js" ] diff --git a/auth/customcredentials/aws/README.md b/auth/customcredentials/aws/README.md new file mode 100644 index 0000000000..8d5669ea1f --- /dev/null +++ b/auth/customcredentials/aws/README.md @@ -0,0 +1,121 @@ +# Running the Custom AWS Credential Supplier Sample (Node.js) + +This sample demonstrates how to use a custom AWS security credential supplier to authenticate with Google Cloud using AWS as an external identity provider. It uses the **AWS SDK for JavaScript (v3)** to fetch credentials from sources like Amazon Elastic Kubernetes Service (EKS) with IAM Roles for Service Accounts (IRSA), Elastic Container Service (ECS), or Fargate. + +## Prerequisites + +* An AWS account. +* A Google Cloud project with the IAM API enabled. +* A GCS bucket. +* **Node.js 16** or later installed. +* **npm** installed. + +If you want to use AWS security credentials that cannot be retrieved using methods supported natively by the Google Auth library, a custom `AwsSecurityCredentialsSupplier` implementation may be specified. The supplier must return valid, unexpired AWS security credentials when called by the Google Cloud Auth library. + +## Running Locally + +For local development, you can provide credentials and configuration in a JSON file. + +### Install Dependencies + +Ensure you have Node.js installed, then install the required libraries: + +```bash +npm install +``` + +### Configure Credentials for Local Development + +1. Copy the example secrets file to a new file named `custom-credentials-aws-secrets.json` in the project root: + ```bash + cp custom-credentials-aws-secrets.json.example custom-credentials-aws-secrets.json + ``` +2. Open `custom-credentials-aws-secrets.json` and fill in the required values for your AWS and Google Cloud configuration. Do not check your `custom-credentials-aws-secrets.json` file into version control. + + +### Run the Application + +Execute the script using node: + +```bash +node customCredentialSupplierAws.js +``` + +When run locally, the application will detect the `custom-credentials-aws-secrets.json` file and use it to configure the necessary environment variables for the AWS SDK. + +## Running in a Containerized Environment (EKS) + +This section provides a brief overview of how to run the sample in an Amazon EKS cluster. + +### EKS Cluster Setup + +First, you need an EKS cluster. You can create one using `eksctl` or the AWS Management Console. For detailed instructions, refer to the [Amazon EKS documentation](https://docs.aws.amazon.com/eks/latest/userguide/create-cluster.html). + +### Configure IAM Roles for Service Accounts (IRSA) + +IRSA enables you to associate an IAM role with a Kubernetes service account. This provides a secure way for your pods to access AWS services without hardcoding long-lived credentials. + +Run the following command to create the IAM role and bind it to a Kubernetes Service Account: + +```bash +eksctl create iamserviceaccount \ + --name your-k8s-service-account \ + --namespace default \ + --cluster your-cluster-name \ + --region your-aws-region \ + --role-name your-role-name \ + --attach-policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess \ + --approve +``` + +> **Note**: The `--attach-policy-arn` flag is used here to demonstrate attaching permissions. Update this with the specific AWS policy ARN your application requires. + +For a deep dive into how this works without using `eksctl`, refer to the [IAM Roles for Service Accounts](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html) documentation. + +### Configure Google Cloud to Trust the AWS Role + +To allow your AWS role to authenticate as a Google Cloud service account, you need to configure Workload Identity Federation. This process involves these key steps: + +1. **Create a Workload Identity Pool and an AWS Provider:** The pool holds the configuration, and the provider is set up to trust your AWS account. + +2. **Create or select a Google Cloud Service Account:** This service account will be impersonated by your AWS role. + +3. **Bind the AWS Role to the Google Cloud Service Account:** Create an IAM policy binding that gives your AWS role the `Workload Identity User` (`roles/iam.workloadIdentityUser`) role on the Google Cloud service account. + +For more detailed information, see the documentation on [Configuring Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds). + +### Containerize and Package the Application + +Create a `Dockerfile` for the Node.js application and push the image to a container registry (for example Amazon ECR) that your EKS cluster can access. + +**Note:** The provided [`Dockerfile`](Dockerfile) is an example and may need modification for your specific needs. + +Build and push the image: +```bash +docker build -t your-container-image:latest . +docker push your-container-image:latest +``` + +### Deploy to EKS + +Create a Kubernetes deployment manifest to deploy your application to the EKS cluster. See the [`pod.yaml`](pod.yaml) file for an example. + +**Note:** The provided [`pod.yaml`](pod.yaml) is an example and may need to be modified for your specific needs. + +Deploy the pod: + +```bash +kubectl apply -f pod.yaml +``` + +### Clean Up + +To clean up the resources, delete the EKS cluster and any other AWS and Google Cloud resources you created. + +```bash +eksctl delete cluster --name your-cluster-name +``` + +## Testing + +This sample is not continuously tested. It is provided for instructional purposes and may require modifications to work in your environment. diff --git a/auth/customcredentials/aws/custom-credentials-aws-secrets.json.example b/auth/customcredentials/aws/custom-credentials-aws-secrets.json.example new file mode 100644 index 0000000000..300dc70c13 --- /dev/null +++ b/auth/customcredentials/aws/custom-credentials-aws-secrets.json.example @@ -0,0 +1,8 @@ +{ + "aws_access_key_id": "YOUR_AWS_ACCESS_KEY_ID", + "aws_secret_access_key": "YOUR_AWS_SECRET_ACCESS_KEY", + "aws_region": "YOUR_AWS_REGION", + "gcp_workload_audience": "YOUR_GCP_WORKLOAD_AUDIENCE", + "gcs_bucket_name": "YOUR_GCS_BUCKET_NAME", + "gcp_service_account_impersonation_url": "YOUR_GCP_SERVICE_ACCOUNT_IMPERSONATION_URL" +} diff --git a/auth/customcredentials/aws/customCredentialSupplierAws.js b/auth/customcredentials/aws/customCredentialSupplierAws.js new file mode 100644 index 0000000000..c8f136bcf0 --- /dev/null +++ b/auth/customcredentials/aws/customCredentialSupplierAws.js @@ -0,0 +1,184 @@ +// Copyright 2025 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. + +// [START auth_custom_credential_supplier_aws] +const {AwsClient} = require('google-auth-library'); +const {fromNodeProviderChain} = require('@aws-sdk/credential-providers'); +const fs = require('fs'); +const path = require('path'); +const {STSClient} = require('@aws-sdk/client-sts'); +const {Storage} = require('@google-cloud/storage'); + +/** + * Custom AWS Security Credentials Supplier. + * + * This implementation resolves AWS credentials using the default Node provider + * chain from the AWS SDK. This allows fetching credentials from environment + * variables, shared credential files (~/.aws/credentials), or IAM roles + * for service accounts (IRSA) in EKS, etc. + */ +class CustomAwsSupplier { + constructor() { + this.region = null; + + this.awsCredentialsProvider = fromNodeProviderChain(); + } + + /** + * Returns the AWS region. This is required for signing the AWS request. + * It resolves the region automatically by using the default AWS region + * provider chain, which searches for the region in the standard locations + * (environment variables, AWS config file, etc.). + */ + async getAwsRegion(_context) { + if (this.region) { + return this.region; + } + + const client = new STSClient({}); + this.region = await client.config.region(); + + if (!this.region) { + throw new Error( + 'CustomAwsSupplier: Unable to resolve AWS region. Please set the AWS_REGION environment variable or configure it in your ~/.aws/config file.' + ); + } + + return this.region; + } + + /** + * Retrieves AWS security credentials using the AWS SDK's default provider chain. + */ + async getAwsSecurityCredentials(_context) { + const awsCredentials = await this.awsCredentialsProvider(); + + if (!awsCredentials.accessKeyId || !awsCredentials.secretAccessKey) { + throw new Error( + 'Unable to resolve AWS credentials from the node provider chain. ' + + 'Ensure your AWS CLI is configured, or AWS environment variables (like AWS_ACCESS_KEY_ID) are set.' + ); + } + + return { + accessKeyId: awsCredentials.accessKeyId, + secretAccessKey: awsCredentials.secretAccessKey, + token: awsCredentials.sessionToken, + }; + } +} + +/** + * Authenticates with Google Cloud using AWS credentials and retrieves bucket metadata. + * + * @param {string} bucketName The name of the bucket to retrieve. + * @param {string} audience The Workload Identity Pool audience. + * @param {string} [impersonationUrl] Optional Service Account impersonation URL. + */ +async function authenticateWithAwsCredentials( + bucketName, + audience, + impersonationUrl +) { + const customSupplier = new CustomAwsSupplier(); + + const clientOptions = { + audience: audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + service_account_impersonation_url: impersonationUrl, + aws_security_credentials_supplier: customSupplier, + }; + + const authClient = new AwsClient(clientOptions); + + const storage = new Storage({ + authClient: authClient, + }); + + const [metadata] = await storage.bucket(bucketName).getMetadata(); + return metadata; +} +// [END auth_custom_credential_supplier_aws] + +/** + * If a local secrets file is present, load it into the process environment. + * This is a "just-in-time" configuration for local development. These + * variables are only set for the current process. + */ +function loadConfigFromFile() { + const secretsPath = path.resolve( + __dirname, + 'custom-credentials-aws-secrets.json' + ); + if (!fs.existsSync(secretsPath)) return; + + try { + const secrets = JSON.parse(fs.readFileSync(secretsPath, 'utf8')); + + const envMap = { + aws_access_key_id: 'AWS_ACCESS_KEY_ID', + aws_secret_access_key: 'AWS_SECRET_ACCESS_KEY', + aws_region: 'AWS_REGION', + gcp_workload_audience: 'GCP_WORKLOAD_AUDIENCE', + gcs_bucket_name: 'GCS_BUCKET_NAME', + gcp_service_account_impersonation_url: + 'GCP_SERVICE_ACCOUNT_IMPERSONATION_URL', + }; + + for (const [jsonKey, envKey] of Object.entries(envMap)) { + if (secrets[jsonKey]) { + process.env[envKey] = secrets[jsonKey]; + } + } + } catch (error) { + console.error(`Error reading secrets file: ${error.message}`); + } +} + +async function main() { + loadConfigFromFile(); + + const gcpAudience = process.env.GCP_WORKLOAD_AUDIENCE; + const saImpersonationUrl = process.env.GCP_SERVICE_ACCOUNT_IMPERSONATION_URL; + const gcsBucketName = process.env.GCS_BUCKET_NAME; + + if (!gcpAudience || !gcsBucketName) { + throw new Error( + 'Missing required configuration. Please provide it in a ' + + 'secrets.json file or as environment variables: ' + + 'GCP_WORKLOAD_AUDIENCE, GCS_BUCKET_NAME' + ); + } + + try { + console.log(`Retrieving metadata for bucket: ${gcsBucketName}...`); + const bucketMetadata = await authenticateWithAwsCredentials( + gcsBucketName, + gcpAudience, + saImpersonationUrl + ); + console.log('\n--- SUCCESS! ---'); + console.log('Bucket Metadata:', JSON.stringify(bucketMetadata, null, 2)); + } catch (error) { + console.error('\n--- FAILED ---'); + console.error(error.message || error); + process.exitCode = 1; + } +} + +if (require.main === module) { + main(); +} + +exports.authenticateWithAwsCredentials = authenticateWithAwsCredentials; diff --git a/auth/customcredentials/aws/pod.yaml b/auth/customcredentials/aws/pod.yaml new file mode 100644 index 0000000000..20ca4bf710 --- /dev/null +++ b/auth/customcredentials/aws/pod.yaml @@ -0,0 +1,44 @@ +# Copyright 2025 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. + +apiVersion: v1 +kind: Pod +metadata: + name: custom-credential-pod-node +spec: + # The Kubernetes Service Account that is annotated with the corresponding + # AWS IAM role ARN. See the README for instructions on setting up IAM + # Roles for Service Accounts (IRSA). + serviceAccountName: your-k8s-service-account + containers: + - name: gcp-auth-sample-node + # The container image pushed to the container registry + # For example, Amazon Elastic Container Registry + image: your-container-image:latest + env: + # REQUIRED: The AWS region. The AWS SDK for Node.js requires this + # to be set explicitly in containers. + - name: AWS_REGION + value: "your-aws-region" + + # REQUIRED: The full identifier of the Workload Identity Pool provider + - name: GCP_WORKLOAD_AUDIENCE + value: "your-gcp-workload-audience" + + # OPTIONAL: Enable Google Cloud service account impersonation + # - name: GCP_SERVICE_ACCOUNT_IMPERSONATION_URL + # value: "your-gcp-service-account-impersonation-url" + + # REQUIRED: The bucket to list + - name: GCS_BUCKET_NAME + value: "your-gcs-bucket-name" diff --git a/auth/customcredentials/okta/README.md b/auth/customcredentials/okta/README.md new file mode 100644 index 0000000000..daca8c1e03 --- /dev/null +++ b/auth/customcredentials/okta/README.md @@ -0,0 +1,81 @@ +# Running the Custom Okta Credential Supplier Sample (Node.js) + +This sample demonstrates how to use a custom subject token supplier to authenticate with Google Cloud using Okta as an external identity provider. It uses the Client Credentials flow for machine-to-machine (M2M) authentication. + +## Prerequisites + +* An Okta developer account. +* A Google Cloud project with the IAM API enabled. +* A Google Cloud Storage bucket. Ensure that the authenticated user has access to this bucket. +* **Node.js 16** or later installed. +* **npm** installed. + +## Okta Configuration + +Before running the sample, you need to configure an Okta application for Machine-to-Machine (M2M) communication. + +### Create an M2M Application in Okta + +1. Log in to your Okta developer console. +2. Navigate to **Applications** > **Applications** and click **Create App Integration**. +3. Select **API Services** as the sign-on method and click **Next**. +4. Give your application a name and click **Save**. + +### Obtain Okta Credentials + +Once the application is created, you will find the following information in the **General** tab: + +* **Okta Domain**: Your Okta developer domain (e.g., `https://dev-123456.okta.com`). +* **Client ID**: The client ID for your application. +* **Client Secret**: The client secret for your application. + +You will need these values to configure the sample. + +## Google Cloud Configuration + +You need to configure a Workload Identity Pool in Google Cloud to trust the Okta application. + +### Set up Workload Identity Federation + +1. In the Google Cloud Console, navigate to **IAM & Admin** > **Workload Identity Federation**. +2. Click **Create Pool** to create a new Workload Identity Pool. +3. Add a new **OIDC provider** to the pool. +4. Configure the provider with your Okta domain as the issuer URL. +5. Map the Okta `sub` (subject) assertion to a GCP principal. + +For detailed instructions, refer to the [Workload Identity Federation documentation](https://cloud.google.com/iam/docs/workload-identity-federation). + +## Running the Sample + +To run the sample on your local system, you need to install dependencies and configure your credentials. + +### 1. Install Dependencies + +This command downloads all required Node.js libraries. + +```bash +npm install +``` + +### 2. Configure Credentials for Local Development + +1. Copy the example secrets file to a new file named `custom-credentials-okta-secrets.json` in the project root: + ```bash + cp custom-credentials-okta-secrets.json.example custom-credentials-okta-secrets.json + ``` +2. Open `custom-credentials-okta-secrets.json` and fill in the required values for your AWS and Google Cloud configuration. Do not check your `custom-credentials-okta-secrets.json` file into version control. + + +### 3. Run the Application + +Execute the script using Node.js: + +```bash +node customCredentialSupplierOkta.js +``` + +The script authenticates with Okta to get an OIDC token, exchanges that token for a Google Cloud federated token, and uses it to list metadata for the specified Google Cloud Storage bucket. + +## Testing + +This sample is not continuously tested. It is provided for instructional purposes and may require modifications to work in your environment. diff --git a/auth/customcredentials/okta/custom-credentials-okta-secrets.json.example b/auth/customcredentials/okta/custom-credentials-okta-secrets.json.example new file mode 100644 index 0000000000..fa04fda7cb --- /dev/null +++ b/auth/customcredentials/okta/custom-credentials-okta-secrets.json.example @@ -0,0 +1,8 @@ +{ + "okta_domain": "https://your-okta-domain.okta.com", + "okta_client_id": "your-okta-client-id", + "okta_client_secret": "your-okta-client-secret", + "gcp_workload_audience": "//iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider", + "gcs_bucket_name": "your-gcs-bucket-name", + "gcp_service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/my-service-account@my-project.iam.gserviceaccount.com:generateAccessToken" +} diff --git a/auth/customcredentials/okta/customCredentialSupplierOkta.js b/auth/customcredentials/okta/customCredentialSupplierOkta.js new file mode 100644 index 0000000000..b4ed10b654 --- /dev/null +++ b/auth/customcredentials/okta/customCredentialSupplierOkta.js @@ -0,0 +1,220 @@ +// Copyright 2025 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. + +// [START auth_custom_credential_supplier_okta] +const {IdentityPoolClient} = require('google-auth-library'); +const {Storage} = require('@google-cloud/storage'); +const {Gaxios} = require('gaxios'); +const fs = require('fs'); +const path = require('path'); + +/** + * A custom SubjectTokenSupplier that authenticates with Okta using the + * Client Credentials grant flow. + */ +class OktaClientCredentialsSupplier { + constructor(domain, clientId, clientSecret) { + const cleanDomain = domain.endsWith('/') ? domain.slice(0, -1) : domain; + this.oktaTokenUrl = `${cleanDomain}/oauth2/default/v1/token`; + + this.clientId = clientId; + this.clientSecret = clientSecret; + this.accessToken = null; + this.expiryTime = 0; + this.gaxios = new Gaxios(); + } + + /** + * Main method called by the auth library. It will fetch a new token if one + * is not already cached. + * @returns {Promise} A promise that resolves with the Okta Access token. + */ + async getSubjectToken() { + const isTokenValid = + this.accessToken && Date.now() < this.expiryTime - 60 * 1000; + + if (isTokenValid) { + return this.accessToken; + } + + const {accessToken, expiresIn} = await this.fetchOktaAccessToken(); + this.accessToken = accessToken; + this.expiryTime = Date.now() + expiresIn * 1000; + return this.accessToken; + } + + /** + * Performs the Client Credentials grant flow with Okta. + */ + async fetchOktaAccessToken() { + const params = new URLSearchParams(); + params.append('grant_type', 'client_credentials'); + params.append('scope', 'gcp.test.read'); + + const authHeader = + 'Basic ' + + Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64'); + + try { + const response = await this.gaxios.request({ + url: this.oktaTokenUrl, + method: 'POST', + headers: { + Authorization: authHeader, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + data: params.toString(), + }); + + const {access_token, expires_in} = response.data; + if (access_token && expires_in) { + return {accessToken: access_token, expiresIn: expires_in}; + } else { + throw new Error( + 'Access token or expires_in not found in Okta response.' + ); + } + } catch (error) { + throw new Error( + `Failed to authenticate with Okta: ${error.response?.data || error.message}` + ); + } + } +} + +/** + * Authenticates with Google Cloud using Okta credentials and retrieves bucket metadata. + * + * @param {string} bucketName The name of the bucket to retrieve. + * @param {string} audience The Workload Identity Pool audience. + * @param {string} domain The Okta domain. + * @param {string} clientId The Okta client ID. + * @param {string} clientSecret The Okta client secret. + * @param {string} [impersonationUrl] Optional Service Account impersonation URL. + */ +async function authenticateWithOktaCredentials( + bucketName, + audience, + domain, + clientId, + clientSecret, + impersonationUrl +) { + const oktaSupplier = new OktaClientCredentialsSupplier( + domain, + clientId, + clientSecret + ); + + const authClient = new IdentityPoolClient({ + audience: audience, + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: 'https://sts.googleapis.com/v1/token', + subject_token_supplier: oktaSupplier, + service_account_impersonation_url: impersonationUrl, + }); + + const storage = new Storage({ + authClient: authClient, + }); + + const [metadata] = await storage.bucket(bucketName).getMetadata(); + return metadata; +} +// [END auth_custom_credential_supplier_okta] + +/** + * If a local secrets file is present, load it into the process environment. + * This is a "just-in-time" configuration for local development. These + * variables are only set for the current process. + */ +function loadConfigFromFile() { + const secretsPath = path.resolve( + __dirname, + 'custom-credentials-okta-secrets.json' + ); + if (!fs.existsSync(secretsPath)) return; + + try { + const secrets = JSON.parse(fs.readFileSync(secretsPath, 'utf8')); + + // Define the mapping: JSON Key -> Environment Variable + const envMap = { + gcp_workload_audience: 'GCP_WORKLOAD_AUDIENCE', + gcs_bucket_name: 'GCS_BUCKET_NAME', + gcp_service_account_impersonation_url: + 'GCP_SERVICE_ACCOUNT_IMPERSONATION_URL', + okta_domain: 'OKTA_DOMAIN', + okta_client_id: 'OKTA_CLIENT_ID', + okta_client_secret: 'OKTA_CLIENT_SECRET', + }; + + // Iterate and assign + for (const [jsonKey, envKey] of Object.entries(envMap)) { + if (secrets[jsonKey]) { + process.env[envKey] = secrets[jsonKey]; + } + } + } catch (error) { + console.error(`Error reading secrets file: ${error.message}`); + } +} + +loadConfigFromFile(); + +async function main() { + const gcpAudience = process.env.GCP_WORKLOAD_AUDIENCE; + const saImpersonationUrl = process.env.GCP_SERVICE_ACCOUNT_IMPERSONATION_URL; + const gcsBucketName = process.env.GCS_BUCKET_NAME; + const oktaDomain = process.env.OKTA_DOMAIN; + const oktaClientId = process.env.OKTA_CLIENT_ID; + const oktaClientSecret = process.env.OKTA_CLIENT_SECRET; + + if ( + !gcpAudience || + !gcsBucketName || + !oktaDomain || + !oktaClientId || + !oktaClientSecret + ) { + throw new Error( + 'Missing required configuration. Please provide it in a ' + + 'secrets.json file or as environment variables.' + ); + } + + try { + console.log(`Retrieving metadata for bucket: ${gcsBucketName}...`); + const bucketMetadata = await authenticateWithOktaCredentials( + gcsBucketName, + gcpAudience, + oktaDomain, + oktaClientId, + oktaClientSecret, + saImpersonationUrl + ); + console.log('\n--- SUCCESS! ---'); + console.log('Bucket Metadata:', JSON.stringify(bucketMetadata, null, 2)); + } catch (error) { + console.error('\n--- FAILED ---'); + console.error(error.message || error); + process.exitCode = 1; + } +} + +if (require.main === module) { + main(); +} + +exports.authenticateWithOktaCredentials = authenticateWithOktaCredentials; diff --git a/auth/package.json b/auth/package.json index 71988b2b5f..00c59e7b65 100644 --- a/auth/package.json +++ b/auth/package.json @@ -15,12 +15,17 @@ "test:auth": "c8 mocha -p -j 2 system-test/auth.test.js --timeout=30000", "test:downscoping": "c8 mocha -p -j 2 system-test/downscoping.test.js --timeout=30000", "test:accessTokenFromImpersonatedCredentials": "c8 mocha -p -j 2 system-test/accessTokenFromImpersonatedCredentials.test.js --timeout=30000", + "test:customcredentials": "c8 mocha -p -j 2 \"system-test/customcredentials/**/*.test.js\" --timeout=30000", "test": "npm -- run system-test", - "system-test": "c8 mocha -p -j 2 system-test/*.test.js --timeout=30000" + "system-test": "c8 mocha -p -j 2 \"system-test/**/*.test.js\" --timeout=30000" }, "dependencies": { - "@google-cloud/storage": "^7.0.0", + "@aws-sdk/client-sts": "^3.58.0", + "@aws-sdk/credential-providers": "^3.0.0", + "@google-cloud/storage": "^7.18.0", + "dotenv": "^17.0.0", "fix": "0.0.6", + "gaxios": "^6.0.0", "google-auth-library": "^9.0.0", "yargs": "^17.0.0" }, diff --git a/auth/system-test/customcredentials/aws/customCredentialSupplierAws.test.js b/auth/system-test/customcredentials/aws/customCredentialSupplierAws.test.js new file mode 100644 index 0000000000..5a3993828b --- /dev/null +++ b/auth/system-test/customcredentials/aws/customCredentialSupplierAws.test.js @@ -0,0 +1,113 @@ +// Copyright 2025 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. + +'use strict'; + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const { + authenticateWithAwsCredentials, +} = require('../../../customcredentials/aws/customCredentialSupplierAws.js'); + +describe('Custom Credential Supplier AWS', () => { + // Variables to hold the original environment to restore after tests + const originalEnv = {}; + + // The configuration we need to run the test + let bucketName, audience, impersonationUrl; + + before(function () { + const secretsPath = path.resolve( + __dirname, + '../../../customcredentials/aws/custom-credentials-aws-secrets.json' + ); + + if (fs.existsSync(secretsPath)) { + try { + const content = fs.readFileSync(secretsPath, 'utf8'); + const secrets = JSON.parse(content); + + // Helper to safely set env var if it exists in the JSON + const setEnv = (envKey, jsonKey) => { + if (secrets[jsonKey]) { + // Save original value to restore later + if (process.env[envKey] === undefined) { + originalEnv[envKey] = undefined; // Mark that it was undefined + } else if ( + !Object.prototype.hasOwnProperty.call(originalEnv, envKey) + ) { + originalEnv[envKey] = process.env[envKey]; + } + process.env[envKey] = secrets[jsonKey]; + } + }; + + // Map JSON keys to Environment Variables + setEnv('GCP_WORKLOAD_AUDIENCE', 'gcp_workload_audience'); + setEnv('GCS_BUCKET_NAME', 'gcs_bucket_name'); + setEnv( + 'GCP_SERVICE_ACCOUNT_IMPERSONATION_URL', + 'gcp_service_account_impersonation_url' + ); + setEnv('AWS_ACCESS_KEY_ID', 'aws_access_key_id'); + setEnv('AWS_SECRET_ACCESS_KEY', 'aws_secret_access_key'); + setEnv('AWS_REGION', 'aws_region'); + } catch (err) { + console.warn( + 'Failed to parse secrets file, relying on system env vars.', + err + ); + } + } + + // Extract values from the Environment (whether from file or system) + bucketName = process.env.GCS_BUCKET_NAME; + audience = process.env.GCP_WORKLOAD_AUDIENCE; + impersonationUrl = process.env.GCP_SERVICE_ACCOUNT_IMPERSONATION_URL; + const awsKey = process.env.AWS_ACCESS_KEY_ID; + const awsSecret = process.env.AWS_SECRET_ACCESS_KEY; + const awsRegion = process.env.AWS_REGION; + + // Skip test if requirements are missing (mimics Java assumeTrue) + if (!bucketName || !audience || !awsKey || !awsSecret || !awsRegion) { + console.log('Skipping AWS system test: Required configuration missing.'); + this.skip(); + } + }); + + after(() => { + // Restore environment variables to their original state + for (const key in originalEnv) { + if (originalEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = originalEnv[key]; + } + } + }); + + it('should authenticate using AWS credentials', async () => { + // Act + const metadata = await authenticateWithAwsCredentials( + bucketName, + audience, + impersonationUrl + ); + + // Assert + assert.strictEqual(metadata.name, bucketName); + assert.ok(metadata.location); + }); +}); diff --git a/auth/system-test/customcredentials/okta/customCredentialSupplierOkta.test.js b/auth/system-test/customcredentials/okta/customCredentialSupplierOkta.test.js new file mode 100644 index 0000000000..1156a0b094 --- /dev/null +++ b/auth/system-test/customcredentials/okta/customCredentialSupplierOkta.test.js @@ -0,0 +1,116 @@ +// Copyright 2025 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. + +'use strict'; + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const { + authenticateWithOktaCredentials, +} = require('../../../customcredentials/okta/customCredentialSupplierOkta.js'); + +describe('Custom Credential Supplier Okta', () => { + const originalEnv = {}; + let bucketName, + audience, + impersonationUrl, + oktaDomain, + oktaClientId, + oktaClientSecret; + + before(function () { + const secretsPath = path.resolve( + __dirname, + '../../../customcredentials/okta/custom-credentials-okta-secrets.json' + ); + + if (fs.existsSync(secretsPath)) { + try { + const content = fs.readFileSync(secretsPath, 'utf8'); + const secrets = JSON.parse(content); + + const setEnv = (envKey, jsonKey) => { + if (secrets[jsonKey]) { + if (process.env[envKey] === undefined) { + originalEnv[envKey] = undefined; + } else if ( + !Object.prototype.hasOwnProperty.call(originalEnv, envKey) + ) { + originalEnv[envKey] = process.env[envKey]; + } + process.env[envKey] = secrets[jsonKey]; + } + }; + + setEnv('GCP_WORKLOAD_AUDIENCE', 'gcp_workload_audience'); + setEnv('GCS_BUCKET_NAME', 'gcs_bucket_name'); + setEnv( + 'GCP_SERVICE_ACCOUNT_IMPERSONATION_URL', + 'gcp_service_account_impersonation_url' + ); + setEnv('OKTA_DOMAIN', 'okta_domain'); + setEnv('OKTA_CLIENT_ID', 'okta_client_id'); + setEnv('OKTA_CLIENT_SECRET', 'okta_client_secret'); + } catch (err) { + console.warn( + 'Failed to parse secrets file, relying on system env vars.', + err + ); + } + } + + bucketName = process.env.GCS_BUCKET_NAME; + audience = process.env.GCP_WORKLOAD_AUDIENCE; + impersonationUrl = process.env.GCP_SERVICE_ACCOUNT_IMPERSONATION_URL; + oktaDomain = process.env.OKTA_DOMAIN; + oktaClientId = process.env.OKTA_CLIENT_ID; + oktaClientSecret = process.env.OKTA_CLIENT_SECRET; + + if ( + !bucketName || + !audience || + !oktaDomain || + !oktaClientId || + !oktaClientSecret + ) { + console.log('Skipping Okta system test: Required configuration missing.'); + this.skip(); + } + }); + + after(() => { + for (const key in originalEnv) { + if (originalEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = originalEnv[key]; + } + } + }); + + it('should authenticate using Okta credentials', async () => { + const metadata = await authenticateWithOktaCredentials( + bucketName, + audience, + oktaDomain, + oktaClientId, + oktaClientSecret, + impersonationUrl + ); + + assert.strictEqual(metadata.name, bucketName); + assert.ok(metadata.location); + }); +}); diff --git a/genai/batch-prediction/batchpredict-embeddings-with-gcs.js b/genai/batch-prediction/batchpredict-embeddings-with-gcs.js new file mode 100644 index 0000000000..f8786efee6 --- /dev/null +++ b/genai/batch-prediction/batchpredict-embeddings-with-gcs.js @@ -0,0 +1,82 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +// [START googlegenaisdk_batchpredict_embeddings_with_gcs] +const {GoogleGenAI} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = + process.env.GOOGLE_CLOUD_LOCATION || 'us-central1'; +const OUTPUT_URI = 'gs://your-bucket/your-prefix'; + +async function runBatchPredictionJob( + outputUri = OUTPUT_URI, + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + httpOptions: { + apiVersion: 'v1', + }, + }); + + // See the documentation: https://googleapis.github.io/js-genai/release_docs/classes/batches.Batches.html + let job = await client.batches.create({ + model: 'text-embedding-005', + // Source link: https://storage.cloud.google.com/cloud-samples-data/batch/prompt_for_batch_gemini_predict.jsonl + src: 'gs://cloud-samples-data/generative-ai/embeddings/embeddings_input.jsonl', + config: { + dest: outputUri, + }, + }); + + console.log(`Job name: ${job.name}`); + console.log(`Job state: ${job.state}`); + + // Example response: + // Job name: projects/%PROJECT_ID%/locations/us-central1/batchPredictionJobs/9876453210000000000 + // Job state: JOB_STATE_PENDING + + const completedStates = new Set([ + 'JOB_STATE_SUCCEEDED', + 'JOB_STATE_FAILED', + 'JOB_STATE_CANCELLED', + 'JOB_STATE_PAUSED', + ]); + + while (!completedStates.has(job.state)) { + await new Promise(resolve => setTimeout(resolve, 30000)); + job = await client.batches.get({name: job.name}); + console.log(`Job state: ${job.state}`); + } + + // Example response: + // Job state: JOB_STATE_PENDING + // Job state: JOB_STATE_RUNNING + // Job state: JOB_STATE_RUNNING + // ... + // Job state: JOB_STATE_SUCCEEDED + + return job.state; +} +// [END googlegenaisdk_batchpredict_embeddings_with_gcs] + +module.exports = { + runBatchPredictionJob, +}; diff --git a/genai/batch-prediction/batchpredict-with-bq.js b/genai/batch-prediction/batchpredict-with-bq.js new file mode 100644 index 0000000000..7c2648223d --- /dev/null +++ b/genai/batch-prediction/batchpredict-with-bq.js @@ -0,0 +1,83 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +// [START googlegenaisdk_batchpredict_with_bq] +const {GoogleGenAI} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = + process.env.GOOGLE_CLOUD_LOCATION || 'us-central1'; +const OUTPUT_URI = 'bq://your-project.your_dataset.your_table'; + +async function runBatchPredictionJob( + outputUri = OUTPUT_URI, + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + httpOptions: { + apiVersion: 'v1', + }, + }); + + // See the documentation: https://googleapis.github.io/js-genai/release_docs/classes/batches.Batches.html + let job = await client.batches.create({ + // To use a tuned model, set the model param to your tuned model using the following format: + // model="projects/{PROJECT_ID}/locations/{LOCATION}/models/{MODEL_ID}" + model: 'gemini-2.5-flash', + src: 'bq://storage-samples.generative_ai.batch_requests_for_multimodal_input', + config: { + dest: outputUri, + }, + }); + + console.log(`Job name: ${job.name}`); + console.log(`Job state: ${job.state}`); + + // Example response: + // Job name: projects/%PROJECT_ID%/locations/us-central1/batchPredictionJobs/9876453210000000000 + // Job state: JOB_STATE_PENDING + + const completedStates = new Set([ + 'JOB_STATE_SUCCEEDED', + 'JOB_STATE_FAILED', + 'JOB_STATE_CANCELLED', + 'JOB_STATE_PAUSED', + ]); + + while (!completedStates.has(job.state)) { + await new Promise(resolve => setTimeout(resolve, 30000)); + job = await client.batches.get({name: job.name}); + console.log(`Job state: ${job.state}`); + } + + // Example response: + // Job state: JOB_STATE_PENDING + // Job state: JOB_STATE_RUNNING + // Job state: JOB_STATE_RUNNING + // ... + // Job state: JOB_STATE_SUCCEEDED + + return job.state; +} +// [END googlegenaisdk_batchpredict_with_bq] + +module.exports = { + runBatchPredictionJob, +}; diff --git a/genai/batch-prediction/batchpredict-with-gcs.js b/genai/batch-prediction/batchpredict-with-gcs.js new file mode 100644 index 0000000000..9c5c69b12a --- /dev/null +++ b/genai/batch-prediction/batchpredict-with-gcs.js @@ -0,0 +1,84 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +// [START googlegenaisdk_batchpredict_with_gcs] +const {GoogleGenAI} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = + process.env.GOOGLE_CLOUD_LOCATION || 'us-central1'; +const OUTPUT_URI = 'gs://your-bucket/your-prefix'; + +async function runBatchPredictionJob( + outputUri = OUTPUT_URI, + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + httpOptions: { + apiVersion: 'v1', + }, + }); + + // See the documentation: https://googleapis.github.io/js-genai/release_docs/classes/batches.Batches.html + let job = await client.batches.create({ + // To use a tuned model, set the model param to your tuned model using the following format: + // model="projects/{PROJECT_ID}/locations/{LOCATION}/models/{MODEL_ID}" + model: 'gemini-2.5-flash', + // Source link: https://storage.cloud.google.com/cloud-samples-data/batch/prompt_for_batch_gemini_predict.jsonl + src: 'gs://cloud-samples-data/batch/prompt_for_batch_gemini_predict.jsonl', + config: { + dest: outputUri, + }, + }); + + console.log(`Job name: ${job.name}`); + console.log(`Job state: ${job.state}`); + + // Example response: + // Job name: projects/%PROJECT_ID%/locations/us-central1/batchPredictionJobs/9876453210000000000 + // Job state: JOB_STATE_PENDING + + const completedStates = new Set([ + 'JOB_STATE_SUCCEEDED', + 'JOB_STATE_FAILED', + 'JOB_STATE_CANCELLED', + 'JOB_STATE_PAUSED', + ]); + + while (!completedStates.has(job.state)) { + await new Promise(resolve => setTimeout(resolve, 30000)); + job = await client.batches.get({name: job.name}); + console.log(`Job state: ${job.state}`); + } + + // Example response: + // Job state: JOB_STATE_PENDING + // Job state: JOB_STATE_RUNNING + // Job state: JOB_STATE_RUNNING + // ... + // Job state: JOB_STATE_SUCCEEDED + + return job.state; +} +// [END googlegenaisdk_batchpredict_with_gcs] + +module.exports = { + runBatchPredictionJob, +}; diff --git a/genai/batch-prediction/package.json b/genai/batch-prediction/package.json new file mode 100644 index 0000000000..74d7f072ce --- /dev/null +++ b/genai/batch-prediction/package.json @@ -0,0 +1,29 @@ +{ + "name": "nodejs-genai-batch-prediction-samples", + "version": "0.0.1", + "private": true, + "license": "Apache-2.0", + "author": "Google LLC", + "repository": { + "type": "git", + "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" + }, + "engines": { + "node": ">=16.0.0" + }, + "files": [ + "*.js" + ], + "scripts": { + "test": "c8 mocha -p -j 2 --timeout 2400000 test/*.test.js test/**/*.test.js" + }, + "dependencies": { + "@google/genai": "1.30.0" + }, + "devDependencies": { + "c8": "^10.0.0", + "chai": "^4.5.0", + "mocha": "^10.0.0", + "proxyquire": "^2.1.3" + } +} diff --git a/genai/batch-prediction/test/batchpredict-embeddings-with-gcs.test.js b/genai/batch-prediction/test/batchpredict-embeddings-with-gcs.test.js new file mode 100644 index 0000000000..ca122b5f59 --- /dev/null +++ b/genai/batch-prediction/test/batchpredict-embeddings-with-gcs.test.js @@ -0,0 +1,56 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const location = 'us-central1'; +const {delay} = require('../../test/util'); +const proxyquire = require('proxyquire'); +const {GoogleGenAI_Mock} = require('./batchprediction-utils'); + +const sample = proxyquire('../batchpredict-embeddings-with-gcs', { + '@google/genai': { + GoogleGenAI: GoogleGenAI_Mock, + }, +}); + +async function getGcsOutputUri() { + return { + uri: 'gs://mock/output', + async cleanup() {}, + }; +} + +describe('batchpredict-with-gcs', () => { + it('should return the batch job state', async function () { + this.timeout(500000); + this.retries(4); + await delay(this.test); + const gcsOutput = await getGcsOutputUri(); + try { + const output = await sample.runBatchPredictionJob( + gcsOutput.uri, + projectId, + location + ); + assert.notEqual(output, undefined); + } finally { + await gcsOutput.cleanup(); + } + }); +}); diff --git a/genai/batch-prediction/test/batchpredict-with-bq.test.js b/genai/batch-prediction/test/batchpredict-with-bq.test.js new file mode 100644 index 0000000000..7d4b47d5d2 --- /dev/null +++ b/genai/batch-prediction/test/batchpredict-with-bq.test.js @@ -0,0 +1,57 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const location = 'us-central1'; +const {delay} = require('../../test/util'); + +const proxyquire = require('proxyquire'); +const {GoogleGenAI_Mock} = require('./batchprediction-utils'); + +const sample = proxyquire('../batchpredict-with-bq', { + '@google/genai': { + GoogleGenAI: GoogleGenAI_Mock, + }, +}); + +async function getBqOutputUri() { + return { + uri: 'gs://mock/output', + async cleanup() {}, + }; +} + +describe('batchpredict-with-bq', () => { + it('should return the batch job state', async function () { + this.timeout(500000); + this.retries(4); + await delay(this.test); + const bqOutput = await getBqOutputUri(); + try { + const output = await sample.runBatchPredictionJob( + bqOutput.uri, + projectId, + location + ); + assert.notEqual(output, undefined); + } finally { + await bqOutput.cleanup(); + } + }); +}); diff --git a/genai/batch-prediction/test/batchpredict-with-gcs.test.js b/genai/batch-prediction/test/batchpredict-with-gcs.test.js new file mode 100644 index 0000000000..2ca16e1a5d --- /dev/null +++ b/genai/batch-prediction/test/batchpredict-with-gcs.test.js @@ -0,0 +1,60 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); +const proxyquire = require('proxyquire'); + +const projectId = process.env.CAIP_PROJECT_ID; +const location = 'us-central1'; +const {delay} = require('../../test/util'); +const {GoogleGenAI_Mock} = require('./batchprediction-utils'); + +const sample = proxyquire('../batchpredict-with-gcs', { + '@google/genai': { + GoogleGenAI: GoogleGenAI_Mock, + }, +}); + +async function getGcsOutputUri() { + return { + uri: 'gs://mock/output', + async cleanup() {}, + }; +} + +describe('batchpredict-with-gcs (mocked)', () => { + it('should return the batch job state', async function () { + this.timeout(500000); + this.retries(4); + await delay(this.test); + + const gcsOutput = await getGcsOutputUri(); + + try { + const output = await sample.runBatchPredictionJob( + gcsOutput.uri, + projectId, + location + ); + + console.log('output', output); + assert.equal(output, 'JOB_STATE_SUCCEEDED'); + } finally { + await gcsOutput.cleanup(); + } + }); +}); diff --git a/genai/batch-prediction/test/batchprediction-utils.js b/genai/batch-prediction/test/batchprediction-utils.js new file mode 100644 index 0000000000..52aed75f8e --- /dev/null +++ b/genai/batch-prediction/test/batchprediction-utils.js @@ -0,0 +1,46 @@ +// Copyright 2025 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 +// +// https://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. + +const GoogleGenAI_Mock = function () { + let getCalled = false; + + return { + batches: { + create: async () => ({ + name: 'projects/mock/locations/mock/batchPredictionJobs/123', + state: 'JOB_STATE_PENDING', + }), + + get: async () => { + // First call returns running, second call returns success + if (!getCalled) { + getCalled = true; + return { + name: 'projects/mock/locations/mock/batchPredictionJobs/123', + state: 'JOB_STATE_RUNNING', + }; + } + + return { + name: 'projects/mock/locations/mock/batchPredictionJobs/123', + state: 'JOB_STATE_SUCCEEDED', + }; + }, + }, + }; +}; + +module.exports = { + GoogleGenAI_Mock, +}; diff --git a/genai/bounding-box/boundingbox-with-txt-img.js b/genai/bounding-box/boundingbox-with-txt-img.js new file mode 100644 index 0000000000..11bdfef438 --- /dev/null +++ b/genai/bounding-box/boundingbox-with-txt-img.js @@ -0,0 +1,159 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +// [START googlegenaisdk_boundingbox_with_txt_img] +const {GoogleGenAI} = require('@google/genai'); + +const {createCanvas, loadImage} = require('canvas'); +const fetch = require('node-fetch'); +const fs = require('fs'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = process.env.GOOGLE_CLOUD_LOCATION || 'global'; + +async function fetchImageAsBase64(uri) { + const response = await fetch(uri); + const buffer = await response.buffer(); + return buffer.toString('base64'); +} + +async function plotBoundingBoxes(imageUri, boundingBoxes) { + console.log('Creating bounding boxes'); + const image = await loadImage(imageUri); + const canvas = createCanvas(image.width, image.height); + const ctx = canvas.getContext('2d'); + + ctx.drawImage(image, 0, 0); + + const colors = ['red', 'blue', 'green', 'orange']; + + boundingBoxes.forEach((bbox, i) => { + const [yMin, xMin, yMax, xMax] = bbox.box_2d; + + const absYMin = Math.floor((yMin / 1000) * image.height); + const absXMin = Math.floor((xMin / 1000) * image.width); + const absYMax = Math.floor((yMax / 1000) * image.height); + const absXMax = Math.floor((xMax / 1000) * image.width); + + ctx.strokeStyle = colors[i % colors.length]; + ctx.lineWidth = 4; + ctx.strokeRect(absXMin, absYMin, absXMax - absXMin, absYMax - absYMin); + + ctx.fillStyle = colors[i % colors.length]; + ctx.font = '20px Arial'; + ctx.fillText(bbox.label, absXMin + 8, absYMin + 20); + }); + + fs.writeFileSync('output.png', canvas.toBuffer('image/png')); + console.log('Saved output to file: output.png'); +} + +async function createBoundingBox( + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + }); + + const systemInstruction = ` + Return bounding boxes as an array with labels. + Never return masks. Limit to 25 objects. + If an object is present multiple times, give each object a unique label + according to its distinct characteristics (colors, size, position, etc). + `; + + const safetySettings = [ + { + category: 'HARM_CATEGORY_DANGEROUS_CONTENT', + threshold: 'BLOCK_ONLY_HIGH', + }, + ]; + + const imageUri = + 'https://storage.googleapis.com/generativeai-downloads/images/socks.jpg'; + const base64Image = await fetchImageAsBase64(imageUri); + + const boundingBoxSchema = { + type: 'ARRAY', + description: 'List of bounding boxes for detected objects', + items: { + type: 'OBJECT', + title: 'BoundingBox', + description: 'Represents a bounding box with coordinates and label', + properties: { + box_2d: { + type: 'ARRAY', + description: + 'Bounding box coordinates in format [y_min, x_min, y_max, x_max]', + items: { + type: 'INTEGER', + format: 'int32', + }, + minItems: 4, + maxItems: 4, + }, + label: { + type: 'STRING', + description: 'Label describing the object within the bounding box', + }, + }, + required: ['box_2d', 'label'], + }, + }; + + const response = await client.models.generateContent({ + model: 'gemini-2.5-flash', + contents: [ + { + role: 'user', + parts: [ + { + text: 'Output the positions of the socks with a face. Label according to position in the image', + }, + { + inlineData: { + data: base64Image, + mimeType: 'image/jpeg', + }, + }, + ], + }, + ], + config: { + systemInstruction: systemInstruction, + safetySettings: safetySettings, + responseMimeType: 'application/json', + temperature: 0.5, + responseSchema: boundingBoxSchema, + }, + }); + + const candidate = response.candidates[0].content.parts[0].text; + const boundingBoxes = JSON.parse(candidate); + + console.log('Bounding boxes:', boundingBoxes); + + await plotBoundingBoxes(imageUri, boundingBoxes); + return boundingBoxes; +} +// [END googlegenaisdk_boundingbox_with_txt_img] + +module.exports = { + createBoundingBox, +}; diff --git a/genai/bounding-box/package.json b/genai/bounding-box/package.json new file mode 100644 index 0000000000..5118da2e42 --- /dev/null +++ b/genai/bounding-box/package.json @@ -0,0 +1,22 @@ +{ + "name": "nodejs-genai-bounding-box", + "private": true, + "license": "Apache-2.0", + "author": "Google LLC", + "engines": { + "node": ">=16.0.0" + }, + "scripts": { + "test": "c8 mocha -p -j 2 --timeout 2400000 test/*.test.js" + }, + "dependencies": { + "@google/genai": "1.30.0", + "canvas": "^3.2.0", + "node-fetch": "^2.7.0" + }, + "devDependencies": { + "c8": "^10.0.0", + "chai": "^4.5.0", + "mocha": "^10.0.0" + } +} diff --git a/genai/bounding-box/test/boundingbox-with-txt-img.test.js b/genai/bounding-box/test/boundingbox-with-txt-img.test.js new file mode 100644 index 0000000000..05df7fb73b --- /dev/null +++ b/genai/bounding-box/test/boundingbox-with-txt-img.test.js @@ -0,0 +1,29 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const sample = require('../boundingbox-with-txt-img'); + +describe('boundingbox-with-txt-img', async () => { + it('should return the bounding box', async function () { + this.timeout(100000); + const output = await sample.createBoundingBox(projectId); + assert(output.length > 0); + }); +}); diff --git a/genai/content-cache/package.json b/genai/content-cache/package.json new file mode 100644 index 0000000000..5ab6dc42b3 --- /dev/null +++ b/genai/content-cache/package.json @@ -0,0 +1,21 @@ +{ + "name": "nodejs-genai-content-cache", + "private": true, + "license": "Apache-2.0", + "author": "Google LLC", + "engines": { + "node": ">=16.0.0" + }, + "scripts": { + "test": "c8 mocha -p -j 2 --timeout 2400000 test/*.test.js" + }, + "dependencies": { + "@google/genai": "1.30.0", + "luxon": "^3.7.1" + }, + "devDependencies": { + "c8": "^10.0.0", + "chai": "^4.5.0", + "mocha": "^10.0.0" + } +} diff --git a/genai/test/content-cache-create-use-update-delete.test.js b/genai/content-cache/test/content-cache-create-use-update-delete.test.js similarity index 82% rename from genai/test/content-cache-create-use-update-delete.test.js rename to genai/content-cache/test/content-cache-create-use-update-delete.test.js index f12e7bd73e..94a1de6a38 100644 --- a/genai/test/content-cache-create-use-update-delete.test.js +++ b/genai/content-cache/test/content-cache-create-use-update-delete.test.js @@ -19,11 +19,11 @@ const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const createSample = require('../content-cache/content-cache-create-with-txt-gcs-pdf.js'); -const useSample = require('../content-cache/content-cache-use-with-txt.js'); -const updateSample = require('../content-cache/content-cache-update.js'); -const deleteSample = require('../content-cache/content-cache-delete.js'); -const {delay} = require('./util'); +const createSample = require('../content-cache-create-with-txt-gcs-pdf.js'); +const useSample = require('../content-cache-use-with-txt.js'); +const updateSample = require('../content-cache-update.js'); +const deleteSample = require('../content-cache-delete.js'); +const {delay} = require('../../test/util'); describe('content-cache-create-use-update-delete', async function () { this.timeout(600000); diff --git a/genai/test/content-cache-list.test.js b/genai/content-cache/test/content-cache-list.test.js similarity index 93% rename from genai/test/content-cache-list.test.js rename to genai/content-cache/test/content-cache-list.test.js index 079580431b..c5f7d1cfcb 100644 --- a/genai/test/content-cache-list.test.js +++ b/genai/content-cache/test/content-cache-list.test.js @@ -18,7 +18,7 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../content-cache/content-cache-list.js'); +const sample = require('../content-cache-list.js'); describe('contentcache-list', async () => { it('should return object with names of catches', async () => { diff --git a/genai/controlled-generation/package.json b/genai/controlled-generation/package.json new file mode 100644 index 0000000000..08dd02ee9c --- /dev/null +++ b/genai/controlled-generation/package.json @@ -0,0 +1,20 @@ +{ + "name": "nodejs-genai-controlled-generation", + "private": true, + "license": "Apache-2.0", + "author": "Google LLC", + "engines": { + "node": ">=16.0.0" + }, + "scripts": { + "test": "c8 mocha -p -j 2 --timeout 2400000 test/*.test.js" + }, + "dependencies": { + "@google/genai": "1.30.0" + }, + "devDependencies": { + "c8": "^10.0.0", + "chai": "^4.5.0", + "mocha": "^10.0.0" + } +} diff --git a/genai/test/ctrlgen-with-class-schema.test.js b/genai/controlled-generation/test/ctrlgen-with-class-schema.test.js similarity index 92% rename from genai/test/ctrlgen-with-class-schema.test.js rename to genai/controlled-generation/test/ctrlgen-with-class-schema.test.js index 10acd5d676..fe90c51d66 100644 --- a/genai/test/ctrlgen-with-class-schema.test.js +++ b/genai/controlled-generation/test/ctrlgen-with-class-schema.test.js @@ -18,7 +18,7 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../controlled-generation/ctrlgen-with-class-schema.js'); +const sample = require('../ctrlgen-with-class-schema.js'); describe('ctrlgen-with-class-schema', () => { it('should generate text content in Json', async function () { diff --git a/genai/test/ctrlgen-with-enum-class-schema.test.js b/genai/controlled-generation/test/ctrlgen-with-enum-class-schema.test.js similarity index 81% rename from genai/test/ctrlgen-with-enum-class-schema.test.js rename to genai/controlled-generation/test/ctrlgen-with-enum-class-schema.test.js index 7c54e6b82e..3a77d900a0 100644 --- a/genai/test/ctrlgen-with-enum-class-schema.test.js +++ b/genai/controlled-generation/test/ctrlgen-with-enum-class-schema.test.js @@ -18,12 +18,15 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../controlled-generation/ctrlgen-with-enum-class-schema.js'); +const sample = require('../ctrlgen-with-enum-class-schema.js'); +const {delay} = require('../../test/util'); describe('ctrlgen-with-enum-class-schema', () => { it('should generate text content matching enum schema', async function () { - this.timeout(10000); + this.timeout(180000); + this.retries(4); + await delay(this.test); const output = await sample.generateEnumClassSchema(projectId); - assert(output.length > 0 && output.includes('String')); + assert(output.length > 0); }); }); diff --git a/genai/test/ctrlgen-with-enum-schema.test.js b/genai/controlled-generation/test/ctrlgen-with-enum-schema.test.js similarity index 89% rename from genai/test/ctrlgen-with-enum-schema.test.js rename to genai/controlled-generation/test/ctrlgen-with-enum-schema.test.js index df51778225..26811bc797 100644 --- a/genai/test/ctrlgen-with-enum-schema.test.js +++ b/genai/controlled-generation/test/ctrlgen-with-enum-schema.test.js @@ -18,8 +18,8 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../controlled-generation/ctrlgen-with-enum-schema.js'); -const {delay} = require('./util'); +const sample = require('../ctrlgen-with-enum-schema.js'); +const {delay} = require('../../test/util'); describe('ctrlgen-with-enum-schema', async () => { it('should generate text content in Json', async function () { diff --git a/genai/test/ctrlgen-with-nested-class-schema.test.js b/genai/controlled-generation/test/ctrlgen-with-nested-class-schema.test.js similarity index 89% rename from genai/test/ctrlgen-with-nested-class-schema.test.js rename to genai/controlled-generation/test/ctrlgen-with-nested-class-schema.test.js index 8bb16e3f62..59fe3b60d0 100644 --- a/genai/test/ctrlgen-with-nested-class-schema.test.js +++ b/genai/controlled-generation/test/ctrlgen-with-nested-class-schema.test.js @@ -18,8 +18,8 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../controlled-generation/ctrlgen-with-nested-class-schema.js'); -const {delay} = require('./util'); +const sample = require('../ctrlgen-with-nested-class-schema.js'); +const {delay} = require('../../test/util'); describe('ctrlgen-with-nested-class-schema', () => { it('should generate text content using nested schema', async function () { diff --git a/genai/test/ctrlgen-with-nullable-schema.test.js b/genai/controlled-generation/test/ctrlgen-with-nullable-schema.test.js similarity index 89% rename from genai/test/ctrlgen-with-nullable-schema.test.js rename to genai/controlled-generation/test/ctrlgen-with-nullable-schema.test.js index 7edf7055b5..47f2dfe0ea 100644 --- a/genai/test/ctrlgen-with-nullable-schema.test.js +++ b/genai/controlled-generation/test/ctrlgen-with-nullable-schema.test.js @@ -18,11 +18,11 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../controlled-generation/ctrlgen-with-nullable-schema.js'); +const sample = require('../ctrlgen-with-nullable-schema.js'); describe('ctrlgen-with-nullable-schema', () => { it('should generate text content using nullable schema', async function () { - this.timeout(10000); + this.timeout(100000); const output = await sample.generateNullableSchema(projectId); assert(output.length > 0); }); diff --git a/genai/test/ctrlgen-with-resp-schema.test.js b/genai/controlled-generation/test/ctrlgen-with-resp-schema.test.js similarity index 92% rename from genai/test/ctrlgen-with-resp-schema.test.js rename to genai/controlled-generation/test/ctrlgen-with-resp-schema.test.js index 3e0a4e2608..a20208fa91 100644 --- a/genai/test/ctrlgen-with-resp-schema.test.js +++ b/genai/controlled-generation/test/ctrlgen-with-resp-schema.test.js @@ -18,7 +18,7 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../controlled-generation/ctrlgen-with-resp-schema.js'); +const sample = require('../ctrlgen-with-resp-schema.js'); describe('ctrlgen-with-resp-schema', () => { it('should generate text content in given schema', async function () { diff --git a/genai/count-tokens/package.json b/genai/count-tokens/package.json new file mode 100644 index 0000000000..8b797ab0c4 --- /dev/null +++ b/genai/count-tokens/package.json @@ -0,0 +1,20 @@ +{ + "name": "nodejs-genai-count-tokens", + "private": true, + "license": "Apache-2.0", + "author": "Google LLC", + "engines": { + "node": ">=16.0.0" + }, + "scripts": { + "test": "c8 mocha -p -j 2 --timeout 2400000 test/*.test.js" + }, + "dependencies": { + "@google/genai": "1.30.0" + }, + "devDependencies": { + "c8": "^10.0.0", + "chai": "^4.5.0", + "mocha": "^10.0.0" + } +} diff --git a/genai/test/counttoken-compute-with-txt.test.js b/genai/count-tokens/test/counttoken-compute-with-txt.test.js similarity index 90% rename from genai/test/counttoken-compute-with-txt.test.js rename to genai/count-tokens/test/counttoken-compute-with-txt.test.js index b0b29c9131..6fb3eba49b 100644 --- a/genai/test/counttoken-compute-with-txt.test.js +++ b/genai/count-tokens/test/counttoken-compute-with-txt.test.js @@ -18,8 +18,8 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../count-tokens/counttoken-compute-with-txt.js'); -const {delay} = require('./util'); +const sample = require('../counttoken-compute-with-txt.js'); +const {delay} = require('../../test/util'); describe('counttoken-compute-with-txt', () => { it('should return tokensInfo from text prompt', async function () { diff --git a/genai/count-tokens/test/counttoken-localtokenizer-compute-with-txt.test.js b/genai/count-tokens/test/counttoken-localtokenizer-compute-with-txt.test.js new file mode 100644 index 0000000000..986850ba3c --- /dev/null +++ b/genai/count-tokens/test/counttoken-localtokenizer-compute-with-txt.test.js @@ -0,0 +1,32 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const sample = require('../counttoken-localtokenizer-compute-with-txt.js'); +const {delay} = require('../../test/util'); + +describe('counttoken-localtokenizer-compute-with-txt', () => { + it('should return tokensInfo from text prompt', async function () { + this.timeout(18000); + this.retries(4); + await delay(this.test); + const output = await sample.countTokenLocalTokenizerCompute(projectId); + assert(output.length > 0); + }); +}); diff --git a/genai/count-tokens/test/counttoken-localtokenizer-with-txt.test.js b/genai/count-tokens/test/counttoken-localtokenizer-with-txt.test.js new file mode 100644 index 0000000000..bc02fe9bba --- /dev/null +++ b/genai/count-tokens/test/counttoken-localtokenizer-with-txt.test.js @@ -0,0 +1,32 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const sample = require('../counttoken-localtokenizer-with-txt.js'); +const {delay} = require('../../test/util'); + +describe('counttoken-localtokenizer-with-txt', () => { + it('should return totalTokens from text prompt', async function () { + this.timeout(18000); + this.retries(4); + await delay(this.test); + const output = await sample.countTokenLocalTokenizer(projectId); + assert(output > 0); + }); +}); diff --git a/genai/test/counttoken-resp-with-txt.test.js b/genai/count-tokens/test/counttoken-resp-with-txt.test.js similarity index 93% rename from genai/test/counttoken-resp-with-txt.test.js rename to genai/count-tokens/test/counttoken-resp-with-txt.test.js index 45fb11fcff..60bb76b7d7 100644 --- a/genai/test/counttoken-resp-with-txt.test.js +++ b/genai/count-tokens/test/counttoken-resp-with-txt.test.js @@ -18,7 +18,7 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../count-tokens/counttoken-resp-with-txt.js'); +const sample = require('../counttoken-resp-with-txt.js'); describe('counttoken-resp-with-txt', () => { it('should return the usageMetadata from text prompt', async function () { diff --git a/genai/test/counttoken-with-txt-vid.test.js b/genai/count-tokens/test/counttoken-with-txt-vid.test.js similarity index 90% rename from genai/test/counttoken-with-txt-vid.test.js rename to genai/count-tokens/test/counttoken-with-txt-vid.test.js index eebe23ac1c..e43bd509a7 100644 --- a/genai/test/counttoken-with-txt-vid.test.js +++ b/genai/count-tokens/test/counttoken-with-txt-vid.test.js @@ -18,8 +18,8 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../count-tokens/counttoken-with-txt-vid.js'); -const {delay} = require('./util'); +const sample = require('../counttoken-with-txt-vid.js'); +const {delay} = require('../../test/util'); describe('counttoken-with-txt-vid', async () => { it('should return the total token count for a text and video prompt', async function () { diff --git a/genai/test/counttoken-with-txt.test.js b/genai/count-tokens/test/counttoken-with-txt.test.js similarity index 93% rename from genai/test/counttoken-with-txt.test.js rename to genai/count-tokens/test/counttoken-with-txt.test.js index 390382d3ba..367ba662a5 100644 --- a/genai/test/counttoken-with-txt.test.js +++ b/genai/count-tokens/test/counttoken-with-txt.test.js @@ -18,7 +18,7 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../count-tokens/counttoken-with-txt.js'); +const sample = require('../counttoken-with-txt.js'); describe('counttoken-with-txt', async () => { it('should return the total token count for a text prompt', async function () { diff --git a/genai/embeddings/embeddings-docretrieval-with-txt.js b/genai/embeddings/embeddings-docretrieval-with-txt.js new file mode 100644 index 0000000000..3c7a70af17 --- /dev/null +++ b/genai/embeddings/embeddings-docretrieval-with-txt.js @@ -0,0 +1,59 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +// [START googlegenaisdk_embeddings_docretrieval_with_txt] +const {GoogleGenAI} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; + +async function generateEmbeddingsForRetrieval( + projectId = GOOGLE_CLOUD_PROJECT +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + }); + + const prompt = [ + "How do I get a driver's license/learner's permit?", + "How long is my driver's license valid for?", + "Driver's knowledge test study guide", + ]; + + const response = await client.models.embedContent({ + model: 'gemini-embedding-001', + contents: prompt, + config: { + taskType: 'RETRIEVAL_DOCUMENT', // Optional + outputDimensionality: 3072, // Optional + title: "Driver's License", // Optional + }, + }); + + console.log(response); + + // Example response: + // embeddings=[ContentEmbedding(values=[-0.06302902102470398, 0.00928034819662571, 0.014716853387653828, -0.028747491538524628, ... ], + // statistics=ContentEmbeddingStatistics(truncated=False, token_count=13.0))] + // metadata=EmbedContentMetadata(billable_character_count=112) + + return response; +} +// [END googlegenaisdk_embeddings_docretrieval_with_txt] + +module.exports = { + generateEmbeddingsForRetrieval, +}; diff --git a/genai/embeddings/package.json b/genai/embeddings/package.json new file mode 100644 index 0000000000..7d82cd5e2e --- /dev/null +++ b/genai/embeddings/package.json @@ -0,0 +1,28 @@ +{ + "name": "nodejs-genai-embeddings-samples", + "version": "0.0.1", + "private": true, + "license": "Apache-2.0", + "author": "Google LLC", + "repository": { + "type": "git", + "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" + }, + "engines": { + "node": ">=16.0.0" + }, + "files": [ + "*.js" + ], + "scripts": { + "test": "c8 mocha -p -j 2 --timeout 2400000 test/*.test.js test/**/*.test.js" + }, + "dependencies": { + "@google/genai": "1.30.0" + }, + "devDependencies": { + "c8": "^10.0.0", + "chai": "^4.5.0", + "mocha": "^10.0.0" + } +} diff --git a/genai/embeddings/test/embeddings-docretrieval-with-txt.test.js b/genai/embeddings/test/embeddings-docretrieval-with-txt.test.js new file mode 100644 index 0000000000..27abb4f59b --- /dev/null +++ b/genai/embeddings/test/embeddings-docretrieval-with-txt.test.js @@ -0,0 +1,31 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const sample = require('../embeddings-docretrieval-with-txt.js'); +const {delay} = require('../../test/util'); + +describe('embeddings-docretrieval-with-txt', () => { + it('should return an object containing embeddings and metadata', async function () { + this.retries(4); + await delay(this.test); + const result = await sample.generateEmbeddingsForRetrieval(projectId); + assert.containsAllKeys(result, ['embeddings', 'metadata']); + }); +}); diff --git a/genai/express-mode/api-key-example.js b/genai/express-mode/api-key-example.js new file mode 100644 index 0000000000..c7d08cd9a4 --- /dev/null +++ b/genai/express-mode/api-key-example.js @@ -0,0 +1,43 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +// [START googlegenaisdk_vertexai_express_mode] +const {GoogleGenAI} = require('@google/genai'); +const API_KEY = 'YOUR_EXPRESS_MODE_API_KEY'; + +async function generateWithApiKey(apiKey = API_KEY) { + const client = new GoogleGenAI({ + vertexai: true, + apiKey: apiKey, + }); + + const response = await client.models.generateContentStream({ + model: 'gemini-2.5-flash', + contents: 'Explain bubble sort to me.', + }); + + console.log(response.text); + + // Example response: + // Bubble Sort is a simple sorting algorithm that repeatedly steps through the list + + return response; +} +// [END googlegenaisdk_vertexai_express_mode] + +module.exports = { + generateWithApiKey, +}; diff --git a/genai/express-mode/package.json b/genai/express-mode/package.json new file mode 100644 index 0000000000..1df8e9b34e --- /dev/null +++ b/genai/express-mode/package.json @@ -0,0 +1,29 @@ +{ + "name": "nodejs-genai-express-mode-samples", + "version": "0.0.1", + "private": true, + "license": "Apache-2.0", + "author": "Google LLC", + "repository": { + "type": "git", + "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" + }, + "engines": { + "node": ">=16.0.0" + }, + "files": [ + "*.js" + ], + "scripts": { + "test": "c8 mocha -p -j 2 --timeout 2400000 test/*.test.js test/**/*.test.js" + }, + "dependencies": { + "@google/genai": "1.30.0" + }, + "devDependencies": { + "c8": "^10.0.0", + "chai": "^4.5.0", + "mocha": "^10.0.0", + "proxyquire": "^2.1.3" + } +} diff --git a/genai/express-mode/test/api-key-example.test.js b/genai/express-mode/test/api-key-example.test.js new file mode 100644 index 0000000000..68ed9181c4 --- /dev/null +++ b/genai/express-mode/test/api-key-example.test.js @@ -0,0 +1,53 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); +const {delay} = require('../../test/util'); +const proxyquire = require('proxyquire').noCallThru(); + +describe('vertexai-express-mode', () => { + it('should call generateContentStream and return the mocked response', async function () { + this.timeout(10000); + + this.retries(4); + await delay(this.test); + + const mockGenerateContentStreamResult = { + text: 'Bubble sort works by repeatedly swapping adjacent elements until sorted.', + }; + + class MockModels { + async generateContentStream() { + return mockGenerateContentStreamResult; + } + } + + class MockGoogleGenAI { + constructor() { + this.models = new MockModels(); + } + } + + const sample = proxyquire('../api-key-example.js', { + '@google/genai': {GoogleGenAI: MockGoogleGenAI}, + }); + + const response = await sample.generateWithApiKey('FAKE_API_KEY'); + + assert.strictEqual(response.text, mockGenerateContentStreamResult.text); + }); +}); diff --git a/genai/image-generation/imggen-canny-ctrl-type-with-txt-img.js b/genai/image-generation/imggen-canny-ctrl-type-with-txt-img.js new file mode 100644 index 0000000000..e1a94a29b5 --- /dev/null +++ b/genai/image-generation/imggen-canny-ctrl-type-with-txt-img.js @@ -0,0 +1,70 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +// [START googlegenaisdk_imggen_canny_ctrl_type_with_txt_img] +const {GoogleGenAI, ControlReferenceImage} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = + process.env.GOOGLE_CLOUD_LOCATION || 'us-central1'; + +async function generateImage( + outputGcsUri, + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + }); + + const controlReferenceImage = new ControlReferenceImage(); + controlReferenceImage.referenceId = 1; + controlReferenceImage.referenceImage = { + gcsUri: 'gs://cloud-samples-data/generative-ai/image/car_canny.png', + }; + controlReferenceImage.config = { + controlType: 'CONTROL_TYPE_CANNY', + }; + + // TODO(developer): Update and un-comment below line + // outputGcsUri = "gs://your-bucket/your-prefix" + + const response = await client.models.editImage({ + model: 'imagen-3.0-capability-001', + // The '[1]' in the prompt corresponds to the controlReferenceImage.referenceId above. + prompt: 'a watercolor painting of a red car[1] driving on a road', + referenceImages: [controlReferenceImage], + config: { + editMode: 'EDIT_MODE_CONTROLLED_EDITING', + numberOfImages: 1, + safetyFilterLevel: 'BLOCK_MEDIUM_AND_ABOVE', + personGeneration: 'ALLOW_ADULT', + outputGcsUri: outputGcsUri, + }, + }); + + console.log(response.generatedImages[0].image.gcsUri); + // Example response: + // gs://your-bucket/your-prefix + return response.generatedImages[0].image.gcsUri; +} +// [END googlegenaisdk_imggen_canny_ctrl_type_with_txt_img] + +module.exports = { + generateImage, +}; diff --git a/genai/image-generation/imggen-mmflash-edit-img-with-txt-img.js b/genai/image-generation/imggen-mmflash-edit-img-with-txt-img.js new file mode 100644 index 0000000000..44f8112260 --- /dev/null +++ b/genai/image-generation/imggen-mmflash-edit-img-with-txt-img.js @@ -0,0 +1,85 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +// [START googlegenaisdk_imggen_mmflash_edit_img_with_txt_img] +const fs = require('fs'); +const {GoogleGenAI, Modality} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = + process.env.GOOGLE_CLOUD_LOCATION || 'us-central1'; + +const FILE_NAME = 'test-data/example-image-eiffel-tower.png'; + +async function generateImage( + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + }); + + const imageBytes = fs.readFileSync(FILE_NAME); + + const response = await client.models.generateContent({ + model: 'gemini-2.5-flash-image', + contents: [ + { + role: 'user', + parts: [ + { + inlineData: { + mimeType: 'image/png', + data: imageBytes.toString('base64'), + }, + }, + { + text: 'Edit this image to make it look like a cartoon', + }, + ], + }, + ], + config: { + responseModalities: [Modality.TEXT, Modality.IMAGE], + }, + }); + + for (const part of response.candidates[0].content.parts) { + if (part.text) { + console.log(`${part.text}`); + } else if (part.inlineData) { + const outputDir = 'output-folder'; + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, {recursive: true}); + } + const imageBytes = Buffer.from(part.inlineData.data, 'base64'); + const filename = `${outputDir}/bw-example-image.png`; + fs.writeFileSync(filename, imageBytes); + } + } + + // Example response: + // Okay, I will edit this image to give it a cartoonish style, with bolder outlines, simplified details, and more vibrant colors. + return response; +} + +// [END googlegenaisdk_imggen_mmflash_edit_img_with_txt_img] + +module.exports = { + generateImage, +}; diff --git a/genai/image-generation/imggen-mmflash-locale-aware-with-txt.js b/genai/image-generation/imggen-mmflash-locale-aware-with-txt.js new file mode 100644 index 0000000000..e16876b6b7 --- /dev/null +++ b/genai/image-generation/imggen-mmflash-locale-aware-with-txt.js @@ -0,0 +1,71 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +// [START googlegenaisdk_imggen_mmflash_locale_aware_with_txt] +const fs = require('fs'); +const {GoogleGenAI, Modality} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = + process.env.GOOGLE_CLOUD_LOCATION || 'us-central1'; + +async function generateImage( + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + }); + + const response = await client.models.generateContent({ + model: 'gemini-2.5-flash-image', + contents: 'Generate a photo of a breakfast meal.', + config: { + responseModalities: [Modality.TEXT, Modality.IMAGE], + }, + }); + + console.log(response); + + for (const part of response.candidates[0].content.parts) { + if (part.text) { + console.log(`${part.text}`); + } else if (part.inlineData) { + const outputDir = 'output-folder'; + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, {recursive: true}); + } + const imageBytes = Buffer.from(part.inlineData.data, 'base64'); + const filename = `${outputDir}/example-breakfast-meal.png`; + fs.writeFileSync(filename, imageBytes); + } + } + + // Example response: + // Generates a photo of a vibrant and appetizing breakfast meal. + // The scene will feature a white plate with golden-brown pancakes + // stacked neatly, drizzled with rich maple syrup and ... + + return response; +} + +// [END googlegenaisdk_imggen_mmflash_locale_aware_with_txt] + +module.exports = { + generateImage, +}; diff --git a/genai/image-generation/imggen-mmflash-multiple-imgs-with-txt.js b/genai/image-generation/imggen-mmflash-multiple-imgs-with-txt.js new file mode 100644 index 0000000000..649c4f754c --- /dev/null +++ b/genai/image-generation/imggen-mmflash-multiple-imgs-with-txt.js @@ -0,0 +1,83 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +// [START googlegenaisdk_imggen_mmflash_multiple_imgs_with_txt] +const fs = require('fs'); +const {GoogleGenAI, Modality} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = + process.env.GOOGLE_CLOUD_LOCATION || 'us-central1'; + +async function generateImage( + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + }); + + const response = await client.models.generateContent({ + model: 'gemini-2.5-flash-image', + contents: 'Generate 3 images of a cat sitting on a chair.', + config: { + responseModalities: [Modality.TEXT, Modality.IMAGE], + }, + }); + + console.log(response); + + const generatedFileNames = []; + let imageCounter = 1; + + for (const part of response.candidates[0].content.parts) { + if (part.text) { + console.log(part.text); + } else if (part.inlineData) { + const outputDir = 'output-folder'; + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, {recursive: true}); + } + const imageBytes = Buffer.from(part.inlineData.data, 'base64'); + const filename = `${outputDir}/example-cats-0${imageCounter}.png`; + fs.writeFileSync(filename, imageBytes); + generatedFileNames.push(filename); + console.log(`Saved image: ${filename}`); + + imageCounter++; + } + } + + return generatedFileNames; +} +// Example response: +// Image 1: A fluffy calico cat with striking green eyes is perched elegantly on a vintage wooden +// chair with a woven seat. Sunlight streams through a nearby window, casting soft shadows and +// highlighting the cat's fur. +// +// Image 2: A sleek black cat with intense yellow eyes is sitting upright on a modern, minimalist +// white chair. The background is a plain grey wall, putting the focus entirely on the feline's +// graceful posture. +// +// Image 3: A ginger tabby cat with playful amber eyes is comfortably curled up asleep on a plush, +// oversized armchair upholstered in a soft, floral fabric. A corner of a cozy living room with a +// [END googlegenaisdk_imggen_mmflash_multiple_imgs_with_txt] + +module.exports = { + generateImage, +}; diff --git a/genai/image-generation/imggen-mmflash-txt-and-img-with-txt.js b/genai/image-generation/imggen-mmflash-txt-and-img-with-txt.js new file mode 100644 index 0000000000..3e24458130 --- /dev/null +++ b/genai/image-generation/imggen-mmflash-txt-and-img-with-txt.js @@ -0,0 +1,85 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +// [START googlegenaisdk_imggen_mmflash_txt_and_img_with_txt] +const fs = require('fs'); +const {GoogleGenAI, Modality} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = + process.env.GOOGLE_CLOUD_LOCATION || 'us-central1'; + +async function savePaellaRecipe(response) { + const parts = response.candidates[0].content.parts; + + let mdText = ''; + const outputDir = 'output-folder'; + + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + + if (part.text) { + mdText += part.text + '\n'; + } else if (part.inlineData) { + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, {recursive: true}); + } + const imageBytes = Buffer.from(part.inlineData.data, 'base64'); + const imagePath = `example-image-${i + 1}.png`; + const saveImagePath = `${outputDir}/${imagePath}`; + + fs.writeFileSync(saveImagePath, imageBytes); + mdText += `![image](./${imagePath})\n`; + } + } + const mdFile = `${outputDir}/paella-recipe.md`; + + fs.writeFileSync(mdFile, mdText); + console.log(`Saved recipe to: ${mdFile}`); +} + +async function generateImage( + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + }); + + const response = await client.models.generateContent({ + model: 'gemini-2.5-flash-image', + contents: + 'Generate an illustrated recipe for a paella. Create images to go alongside the text as you generate the recipe', + config: { + responseModalities: [Modality.TEXT, Modality.IMAGE], + }, + }); + console.log(response); + + await savePaellaRecipe(response); + + return response; +} +// Example response: +// A markdown page for a Paella recipe(`paella-recipe.md`) has been generated. +// It includes detailed steps and several images illustrating the cooking process. +// [END googlegenaisdk_imggen_mmflash_txt_and_img_with_txt] + +module.exports = { + generateImage, +}; diff --git a/genai/image-generation/imggen-mmflash-with-txt.js b/genai/image-generation/imggen-mmflash-with-txt.js index 44c2ef0397..3642e9671a 100644 --- a/genai/image-generation/imggen-mmflash-with-txt.js +++ b/genai/image-generation/imggen-mmflash-with-txt.js @@ -22,7 +22,7 @@ const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; const GOOGLE_CLOUD_LOCATION = process.env.GOOGLE_CLOUD_LOCATION || 'us-central1'; -async function generateContent( +async function generateImage( projectId = GOOGLE_CLOUD_PROJECT, location = GOOGLE_CLOUD_LOCATION ) { @@ -43,13 +43,18 @@ async function generateContent( const generatedFileNames = []; let imageIndex = 0; + for await (const chunk of response) { const text = chunk.text; const data = chunk.data; if (text) { console.debug(text); } else if (data) { - const fileName = `generate_content_streaming_image_${imageIndex++}.png`; + const outputDir = 'output-folder'; + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, {recursive: true}); + } + const fileName = `${outputDir}/generate_content_streaming_image_${imageIndex++}.png`; console.debug(`Writing response image to file: ${fileName}.`); try { fs.writeFileSync(fileName, data); @@ -60,10 +65,16 @@ async function generateContent( } } + // Example response: + // I will generate an image of the Eiffel Tower at night, with a vibrant display of + // colorful fireworks exploding in the dark sky behind it. The tower will be + // illuminated, standing tall as the focal point of the scene, with the bursts of + // light from the fireworks creating a festive atmosphere. + return generatedFileNames; } // [END googlegenaisdk_imggen_mmflash_with_txt] module.exports = { - generateContent, + generateImage, }; diff --git a/genai/image-generation/imggen-raw-reference-with-txt-img.js b/genai/image-generation/imggen-raw-reference-with-txt-img.js new file mode 100644 index 0000000000..ff22f4cb83 --- /dev/null +++ b/genai/image-generation/imggen-raw-reference-with-txt-img.js @@ -0,0 +1,70 @@ +// Copyright 2025 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 +// +// https://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. + +// [START googlegenaisdk_imggen_raw_reference_with_txt_img] + +'use strict'; + +const {GoogleGenAI, RawReferenceImage} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = + process.env.GOOGLE_CLOUD_LOCATION || 'us-central1'; + +async function generateImage( + outputGcsUri, + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + }); + + const referenceImages = new RawReferenceImage(); + referenceImages.referenceId = 1; + referenceImages.referenceImage = { + gcsUri: 'gs://cloud-samples-data/generative-ai/image/teacup-1.png', + }; + + // TODO(developer): Update and un-comment below line + // outputGcsUri = "gs://your-bucket/your-prefix" + + const response = await client.models.editImage({ + model: 'imagen-3.0-capability-001', + prompt: + 'transform the subject in the image so that the teacup[1] is made entirely out of chocolate', + referenceImages: [referenceImages], + config: { + editMode: 'EDIT_MODE_DEFAULT', + numberOfImages: 1, + safetyFilterLevel: 'BLOCK_MEDIUM_AND_ABOVE', + personGeneration: 'ALLOW_ADULT', + outputGcsUri: outputGcsUri, + }, + }); + + console.log(response); + + // Example response: + // gs://your-bucket/your-prefix + + return response.generatedImages[0].image.gcsUri; +} +// [END googlegenaisdk_imggen_raw_reference_with_txt_img] + +module.exports = { + generateImage, +}; diff --git a/genai/image-generation/imggen-scribble-ctrl-type-with-txt-img.js b/genai/image-generation/imggen-scribble-ctrl-type-with-txt-img.js new file mode 100644 index 0000000000..0b42934f9c --- /dev/null +++ b/genai/image-generation/imggen-scribble-ctrl-type-with-txt-img.js @@ -0,0 +1,69 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +// [START googlegenaisdk_imggen_scribble_ctrl_type_with_txt_img] +const {GoogleGenAI, ControlReferenceImage} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = + process.env.GOOGLE_CLOUD_LOCATION || 'us-central1'; + +async function generateImage( + outputGcsUri, + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + }); + + const controlReferenceImage = new ControlReferenceImage(); + controlReferenceImage.referenceId = 1; + controlReferenceImage.referenceImage = { + gcsUri: 'gs://cloud-samples-data/generative-ai/image/car_scribble.png', + }; + controlReferenceImage.config = { + controlType: 'CONTROL_TYPE_SCRIBBLE', + }; + + // TODO(developer): Update and un-comment below line + // outputGcsUri = "gs://your-bucket/your-prefix" + + const response = await client.models.editImage({ + model: 'imagen-3.0-capability-001', + prompt: 'an oil painting showing the side of a red car[1]', + referenceImages: [controlReferenceImage], + config: { + editMode: 'EDIT_MODE_CONTROLLED_EDITING', + numberOfImages: 1, + safetyFilterLevel: 'BLOCK_MEDIUM_AND_ABOVE', + personGeneration: 'ALLOW_ADULT', + outputGcsUri: outputGcsUri, + }, + }); + console.log(response.generatedImages); + + // Example response: + // gs://your-bucket/your-prefix + return response.generatedImages; +} +// [END googlegenaisdk_imggen_scribble_ctrl_type_with_txt_img] + +module.exports = { + generateImage, +}; diff --git a/genai/image-generation/imggen-style-reference-with-txt-img.js b/genai/image-generation/imggen-style-reference-with-txt-img.js new file mode 100644 index 0000000000..163687278e --- /dev/null +++ b/genai/image-generation/imggen-style-reference-with-txt-img.js @@ -0,0 +1,73 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +// [START googlegenaisdk_imggen_style_reference_with_txt_img] +const {GoogleGenAI, StyleReferenceImage} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = + process.env.GOOGLE_CLOUD_LOCATION || 'us-central1'; + +async function generateImage( + outputGcsUri, + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + }); + + // Create a style reference image of a neon sign stored in Google Cloud Storage + // using https://storage.googleapis.com/cloud-samples-data/generative-ai/image/neon.png + + // TODO(developer): Update and un-comment below line + // outputGcsUri = "gs://your-bucket/your-prefix" + + const styleReferenceImage = new StyleReferenceImage(); + styleReferenceImage.referenceId = 1; + styleReferenceImage.referenceImage = { + gcsUri: 'gs://cloud-samples-data/generative-ai/image/neon.png', + }; + styleReferenceImage.config = { + styleDescription: 'neon sign', + }; + + const response = await client.models.editImage({ + model: 'imagen-3.0-capability-001', + prompt: + 'generate an image of a neon sign [1] with the words: have a great day', + referenceImages: [styleReferenceImage], + config: { + editMode: 'EDIT_MODE_CONTROLLED_EDITING', + numberOfImages: 1, + safetyFilterLevel: 'BLOCK_MEDIUM_AND_ABOVE', + personGeneration: 'ALLOW_ADULT', + outputGcsUri: outputGcsUri, + }, + }); + console.log(response.generatedImages); + + // Example response: + // gs://your-bucket/your-prefix + return response.generatedImages; +} +// [END googlegenaisdk_imggen_style_reference_with_txt_img] + +module.exports = { + generateImage, +}; diff --git a/genai/image-generation/imggen-subj-refer-ctrl-refer-with-txt-imgs.js b/genai/image-generation/imggen-subj-refer-ctrl-refer-with-txt-imgs.js new file mode 100644 index 0000000000..e98b9a97b4 --- /dev/null +++ b/genai/image-generation/imggen-subj-refer-ctrl-refer-with-txt-imgs.js @@ -0,0 +1,85 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +// [START googlegenaisdk_imggen_subj_refer_ctrl_refer_with_txt_imgs] +const { + GoogleGenAI, + ControlReferenceImage, + SubjectReferenceImage, +} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = + process.env.GOOGLE_CLOUD_LOCATION || 'us-central1'; + +async function generateImage( + outputGcsUri, + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + }); + + const subjectReferenceImage = new SubjectReferenceImage(); + subjectReferenceImage.referenceId = 1; + subjectReferenceImage.referenceImage = { + gcsUri: 'gs://cloud-samples-data/generative-ai/image/person.png', + }; + subjectReferenceImage.config = { + subjectDescription: 'a headshot of a woman', + subjectType: 'SUBJECT_TYPE_PERSON', + }; + + const controlReferenceImage = new ControlReferenceImage(); + controlReferenceImage.referenceId = 2; + controlReferenceImage.referenceImage = { + gcsUri: 'gs://cloud-samples-data/generative-ai/image/person.png', + }; + controlReferenceImage.config = { + controlType: 'CONTROL_TYPE_FACE_MESH', + }; + + // TODO(developer): Update and un-comment below line + // outputGcsUri = "gs://your-bucket/your-prefix" + + const response = await client.models.editImage({ + model: 'imagen-3.0-capability-001', + prompt: `a portrait of a woman[1] in the pose of the control image[2]in a watercolor style by a professional artist, + light and low-contrast stokes, bright pastel colors, a warm atmosphere, clean background, grainy paper, + bold visible brushstrokes, patchy details`, + referenceImages: [subjectReferenceImage, controlReferenceImage], + config: { + editMode: 'EDIT_MODE_CONTROLLED_EDITING', + numberOfImages: 1, + safetyFilterLevel: 'BLOCK_MEDIUM_AND_ABOVE', + personGeneration: 'ALLOW_ADULT', + outputGcsUri: outputGcsUri, + }, + }); + console.log(response.generatedImages); + + // Example response: + // gs://your-bucket/your-prefix + return response.generatedImages; +} +// [END googlegenaisdk_imggen_subj_refer_ctrl_refer_with_txt_imgs] + +module.exports = { + generateImage, +}; diff --git a/genai/image-generation/imggen-with-txt.js b/genai/image-generation/imggen-with-txt.js new file mode 100644 index 0000000000..003c7563b9 --- /dev/null +++ b/genai/image-generation/imggen-with-txt.js @@ -0,0 +1,63 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +// [START googlegenaisdk_imggen_with_txt] +const {GoogleGenAI} = require('@google/genai'); +const fs = require('fs'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = + process.env.GOOGLE_CLOUD_LOCATION || 'us-central1'; + +async function generateImage( + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + }); + + const image = await client.models.generateImages({ + model: 'imagen-4.0-generate-001', + prompt: 'A dog reading a newspaper', + config: { + imageSize: '2K', + }, + }); + console.log(image.generatedImages[0].image); + console.log('Created output image'); + const outputDir = 'output-folder'; + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, {recursive: true}); + } + + const imageBytes = image.generatedImages[0].image.imageBytes; + const buffer = Buffer.from(imageBytes, 'base64'); + const fileName = `${outputDir}/dog-image.png`; + + fs.writeFileSync(fileName, buffer); + + // Example response: + // gs://your-bucket/your-prefix + return image.generatedImages; +} +// [END googlegenaisdk_imggen_with_txt] + +module.exports = { + generateImage, +}; diff --git a/genai/image-generation/imggen_virtual-try-on-with-txt-img.js b/genai/image-generation/imggen_virtual-try-on-with-txt-img.js new file mode 100644 index 0000000000..ff0686357a --- /dev/null +++ b/genai/image-generation/imggen_virtual-try-on-with-txt-img.js @@ -0,0 +1,76 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +// [START googlegenaisdk_imggen_virtual_try_on_with_txt_img] +const fs = require('fs'); +const {GoogleGenAI} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = + process.env.GOOGLE_CLOUD_LOCATION || 'us-central1'; + +async function virtualTryOn( + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + }); + + const source = { + personImage: { + imageBytes: fs.readFileSync('test-data/man.png').toString('base64'), + }, + productImages: [ + { + productImage: { + imageBytes: fs + .readFileSync('test-data/sweater.jpg') + .toString('base64'), + }, + }, + ], + }; + + const image = await client.models.recontextImage({ + model: 'virtual-try-on-preview-08-04', + source: source, + }); + + console.log('Created output image'); + const outputDir = 'output-folder'; + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, {recursive: true}); + } + const outputPath = `${outputDir}/image.png`; + const imageBytes = image.generatedImages[0].image.imageBytes; + const buffer = Buffer.from(imageBytes, 'base64'); + + fs.writeFileSync(outputPath, buffer); + + // Example response: + // Created output image using 1234567 bytes + + return image.generatedImages[0]; +} + +// [END googlegenaisdk_imggen_virtual_try_on_with_txt_img] + +module.exports = { + virtualTryOn, +}; diff --git a/genai/image-generation/package.json b/genai/image-generation/package.json new file mode 100644 index 0000000000..50a1968737 --- /dev/null +++ b/genai/image-generation/package.json @@ -0,0 +1,22 @@ +{ + "name": "nodejs-genai-image-generation", + "private": true, + "license": "Apache-2.0", + "author": "Google LLC", + "engines": { + "node": ">=16.0.0" + }, + "scripts": { + "test": "c8 mocha -p -j 2 --timeout 2400000 test/*.test.js" + }, + "dependencies": { + "@google/genai": "1.34.0", + "@google-cloud/storage": "^7.17.3", + "date-fns": "^4.1.0" + }, + "devDependencies": { + "c8": "^10.0.0", + "chai": "^4.5.0", + "mocha": "^10.0.0" + } +} diff --git a/genai/image-generation/test-data/example-image-eiffel-tower.png b/genai/image-generation/test-data/example-image-eiffel-tower.png new file mode 100644 index 0000000000..2a602e6269 Binary files /dev/null and b/genai/image-generation/test-data/example-image-eiffel-tower.png differ diff --git a/genai/image-generation/test-data/man.png b/genai/image-generation/test-data/man.png new file mode 100644 index 0000000000..7cf652e8e6 Binary files /dev/null and b/genai/image-generation/test-data/man.png differ diff --git a/genai/image-generation/test-data/sweater.jpg b/genai/image-generation/test-data/sweater.jpg new file mode 100644 index 0000000000..69cc18f921 Binary files /dev/null and b/genai/image-generation/test-data/sweater.jpg differ diff --git a/genai/image-generation/test/imggen-canny-ctrl-type-with-txt-img.test.js b/genai/image-generation/test/imggen-canny-ctrl-type-with-txt-img.test.js new file mode 100644 index 0000000000..1325a933ed --- /dev/null +++ b/genai/image-generation/test/imggen-canny-ctrl-type-with-txt-img.test.js @@ -0,0 +1,41 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; + +const sample = require('../imggen-canny-ctrl-type-with-txt-img'); +const location = 'us-central1'; +const {delay} = require('../../test/util'); +const {createOutputGcsUri} = require('./imggen-util'); + +describe('imggen-canny-ctrl-type-with-txt-img', () => { + it('should return an array of generated image URIs', async function () { + this.timeout(180000); + this.retries(4); + const output = await createOutputGcsUri(); + console.log(output.uri); + await delay(this.test); + const generatedFileNames = await sample.generateImage( + output.uri, + projectId, + location + ); + assert(generatedFileNames.length > 0); + }); +}); diff --git a/genai/image-generation/test/imggen-mmflash-edit-img-with-txt-img.test.js b/genai/image-generation/test/imggen-mmflash-edit-img-with-txt-img.test.js new file mode 100644 index 0000000000..18d4ebc6ea --- /dev/null +++ b/genai/image-generation/test/imggen-mmflash-edit-img-with-txt-img.test.js @@ -0,0 +1,34 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const location = 'global'; +const sample = require('../imggen-mmflash-edit-img-with-txt-img'); +const {delay} = require('../../test/util'); + +describe('imggen-mmflash-edit-img-with-txt-img', async () => { + it('should return a response object containing image parts', async function () { + this.timeout(180000); + this.retries(4); + await delay(this.test); + const response = await sample.generateImage(projectId, location); + console.log(response); + assert(response); + }); +}); diff --git a/genai/image-generation/test/imggen-mmflash-locale-aware-with-txt.test.js b/genai/image-generation/test/imggen-mmflash-locale-aware-with-txt.test.js new file mode 100644 index 0000000000..51030fea3f --- /dev/null +++ b/genai/image-generation/test/imggen-mmflash-locale-aware-with-txt.test.js @@ -0,0 +1,33 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const location = 'global'; +const sample = require('../imggen-mmflash-locale-aware-with-txt'); +const {delay} = require('../../test/util'); + +describe('imggen-mmflash-locale-aware-with-txt', async () => { + it('should generate a response with text and image parts', async function () { + this.timeout(180000); + this.retries(4); + await delay(this.test); + const response = await sample.generateImage(projectId, location); + assert(response); + }); +}); diff --git a/genai/image-generation/test/imggen-mmflash-multiple-imgs-with-txt.test.js b/genai/image-generation/test/imggen-mmflash-multiple-imgs-with-txt.test.js new file mode 100644 index 0000000000..21228a290a --- /dev/null +++ b/genai/image-generation/test/imggen-mmflash-multiple-imgs-with-txt.test.js @@ -0,0 +1,34 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const location = 'global'; + +const sample = require('../imggen-mmflash-multiple-imgs-with-txt'); +const {delay} = require('../../test/util'); + +describe('imggen-mmflash-multiple-imgs-with-txt', async () => { + it('should return a response object containing image parts', async function () { + this.timeout(180000); + this.retries(4); + await delay(this.test); + const response = await sample.generateImage(projectId, location); + assert(response); + }); +}); diff --git a/genai/image-generation/test/imggen-mmflash-txt-and-img-with-txt.test.js b/genai/image-generation/test/imggen-mmflash-txt-and-img-with-txt.test.js new file mode 100644 index 0000000000..c2cd436aac --- /dev/null +++ b/genai/image-generation/test/imggen-mmflash-txt-and-img-with-txt.test.js @@ -0,0 +1,33 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const location = 'global'; +const sample = require('../imggen-mmflash-txt-and-img-with-txt'); +const {delay} = require('../../test/util'); + +describe('imggen-mmflash-txt-and-img-with-txt', async () => { + it('should generate a response with text and image parts', async function () { + this.timeout(180000); + this.retries(4); + await delay(this.test); + const response = await sample.generateImage(projectId, location); + assert(response); + }); +}); diff --git a/genai/test/imggen-mmflash-with-txt.test.js b/genai/image-generation/test/imggen-mmflash-with-txt.test.js similarity index 82% rename from genai/test/imggen-mmflash-with-txt.test.js rename to genai/image-generation/test/imggen-mmflash-with-txt.test.js index 87cd1b8238..0e801c53d4 100644 --- a/genai/test/imggen-mmflash-with-txt.test.js +++ b/genai/image-generation/test/imggen-mmflash-with-txt.test.js @@ -20,18 +20,15 @@ const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; const location = 'global'; -const sample = require('../image-generation/imggen-mmflash-with-txt.js'); -const {delay} = require('./util'); +const sample = require('../imggen-mmflash-with-txt.js'); +const {delay} = require('../../test/util'); describe('imggen-mmflash-with-txt', async () => { it('should generate images from a text prompt', async function () { this.timeout(180000); this.retries(5); await delay(this.test); - const generatedFileNames = await sample.generateContent( - projectId, - location - ); + const generatedFileNames = await sample.generateImage(projectId, location); assert(generatedFileNames.length > 0); }); }); diff --git a/genai/image-generation/test/imggen-raw-reference-with-txt-img.test.js b/genai/image-generation/test/imggen-raw-reference-with-txt-img.test.js new file mode 100644 index 0000000000..4d050a6822 --- /dev/null +++ b/genai/image-generation/test/imggen-raw-reference-with-txt-img.test.js @@ -0,0 +1,50 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; + +const sample = require('../imggen-raw-reference-with-txt-img'); +const {delay} = require('../../test/util'); +const {createOutputGcsUri} = require('./imggen-util'); +const location = 'us-central1'; + +describe('imggen-raw-reference-with-txt-img', () => { + it('should return an array of generated image URIs', async function () { + this.timeout(600000); + this.retries(3); + + const output = await createOutputGcsUri(); + console.log('Output GCS URI:', output.uri); + + try { + await delay(this.test); + const generatedFileNames = await sample.generateImage( + output.uri, + projectId, + location + ); + console.log('Generated files:', generatedFileNames); + + assert(generatedFileNames.length > 0); + } catch (err) { + console.error('Image generation failed:', err); + throw err; + } + }); +}); diff --git a/genai/image-generation/test/imggen-scribble-ctrl-type-with-txt-img.test.js b/genai/image-generation/test/imggen-scribble-ctrl-type-with-txt-img.test.js new file mode 100644 index 0000000000..511c51a592 --- /dev/null +++ b/genai/image-generation/test/imggen-scribble-ctrl-type-with-txt-img.test.js @@ -0,0 +1,49 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const sample = require('../imggen-scribble-ctrl-type-with-txt-img'); +const {delay} = require('../../test/util'); +const {createOutputGcsUri} = require('./imggen-util'); +const location = 'us-central1'; + +describe('imggen-scribble-ctrl-type-with-txt-img', async () => { + it('should generate images from a text prompt with control reference image', async function () { + this.timeout(600000); + this.retries(3); + + const output = await createOutputGcsUri(); + console.log('Output GCS URI:', output.uri); + + try { + await delay(this.test); + const generatedFileNames = await sample.generateImage( + output.uri, + projectId, + location + ); + console.log('Generated files:', generatedFileNames); + + assert(generatedFileNames.length > 0); + } catch (err) { + console.error('Image generation failed:', err); + throw err; + } + }); +}); diff --git a/genai/image-generation/test/imggen-style-reference-with-txt-img.test.js b/genai/image-generation/test/imggen-style-reference-with-txt-img.test.js new file mode 100644 index 0000000000..ac106c0001 --- /dev/null +++ b/genai/image-generation/test/imggen-style-reference-with-txt-img.test.js @@ -0,0 +1,40 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const sample = require('../imggen-style-reference-with-txt-img'); +const {delay} = require('../../test/util'); +const {createOutputGcsUri} = require('./imggen-util'); +const location = 'us-central1'; + +describe('imggen-style-reference-with-txt-img', async () => { + it('should generate images from a text prompt with style reference', async function () { + this.timeout(180000); + this.retries(4); + const output = await createOutputGcsUri(); + console.log(output.uri); + await delay(this.test); + const generatedFileNames = await sample.generateImage( + output.uri, + projectId, + location + ); + assert(generatedFileNames.length > 0); + }); +}); diff --git a/genai/image-generation/test/imggen-subj-refer-ctrl-refer-with-txt-imgs.test.js b/genai/image-generation/test/imggen-subj-refer-ctrl-refer-with-txt-imgs.test.js new file mode 100644 index 0000000000..c94b7cf848 --- /dev/null +++ b/genai/image-generation/test/imggen-subj-refer-ctrl-refer-with-txt-imgs.test.js @@ -0,0 +1,40 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const sample = require('../imggen-subj-refer-ctrl-refer-with-txt-imgs'); +const {delay} = require('../../test/util'); +const {createOutputGcsUri} = require('./imggen-util'); +const location = 'us-central1'; + +describe('imggen-subj-refer-ctrl-refer-with-txt-imgs', async () => { + it('should generate images from a text prompt with subject reference image and control reference image', async function () { + this.timeout(180000); + this.retries(4); + const output = await createOutputGcsUri(); + console.log(output.uri); + await delay(this.test); + const generatedFileNames = await sample.generateImage( + output.uri, + projectId, + location + ); + assert(generatedFileNames.length > 0); + }); +}); diff --git a/genai/image-generation/test/imggen-util.js b/genai/image-generation/test/imggen-util.js new file mode 100644 index 0000000000..36e77bb2d8 --- /dev/null +++ b/genai/image-generation/test/imggen-util.js @@ -0,0 +1,35 @@ +// Copyright 2025 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 +// +// https://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. + +const {Storage} = require('@google-cloud/storage'); +const {format} = require('date-fns'); + +const gcsOutputBucket = 'nodejs-docs-samples-tests'; + +module.exports.createOutputGcsUri = async function () { + const prefix = `text_output/${format(new Date(), 'yyyy-MM-dd-HH-mm-ss')}`; + const gcsUri = `gs://${gcsOutputBucket}/${prefix}`; + + return { + uri: gcsUri, + async cleanup() { + const storage = new Storage(); + const bucket = storage.bucket(gcsOutputBucket); + + const [files] = await bucket.getFiles({prefix}); + await Promise.all(files.map(file => file.delete())); + console.log(`Deleted ${files.length} files from ${prefix}`); + }, + }; +}; diff --git a/genai/image-generation/test/imggen-with-txt.test.js b/genai/image-generation/test/imggen-with-txt.test.js new file mode 100644 index 0000000000..e3f37437cc --- /dev/null +++ b/genai/image-generation/test/imggen-with-txt.test.js @@ -0,0 +1,32 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const sample = require('../imggen-with-txt.js'); +const {delay} = require('../../test/util'); + +describe('imggen-with-txt', async () => { + it('should generate images from a text prompt', async function () { + this.timeout(180000); + this.retries(4); + await delay(this.test); + const image = await sample.generateImage(projectId); + assert(image.length > 0); + }); +}); diff --git a/genai/image-generation/test/imggen_virtual-try-on-with-txt-img.test.js b/genai/image-generation/test/imggen_virtual-try-on-with-txt-img.test.js new file mode 100644 index 0000000000..12f085ce14 --- /dev/null +++ b/genai/image-generation/test/imggen_virtual-try-on-with-txt-img.test.js @@ -0,0 +1,33 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; + +const sample = require('../imggen_virtual-try-on-with-txt-img'); +const {delay} = require('../../test/util'); + +describe('imggen_virtual-try-on-with-txt-img', async () => { + it('should return a response object containing image parts', async function () { + this.timeout(180000); + this.retries(4); + await delay(this.test); + const response = await sample.virtualTryOn(projectId); + assert(response); + }); +}); diff --git a/genai/live/hello_gemini_are_you_there.wav b/genai/live/hello_gemini_are_you_there.wav new file mode 100644 index 0000000000..ef60adee2a Binary files /dev/null and b/genai/live/hello_gemini_are_you_there.wav differ diff --git a/genai/live/live-audio-with-txt.js b/genai/live/live-audio-with-txt.js new file mode 100644 index 0000000000..e6c257862d --- /dev/null +++ b/genai/live/live-audio-with-txt.js @@ -0,0 +1,122 @@ +// Copyright 2025 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 +// +// https://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. + +// [START googlegenaisdk_live_audio_with_txt] + +'use strict'; + +const {GoogleGenAI, Modality} = require('@google/genai'); +const fs = require('fs'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = process.env.GOOGLE_CLOUD_LOCATION || 'global'; + +async function generateLiveConversation( + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + }); + + const voiceName = 'Aoede'; + const modelId = 'gemini-2.0-flash-live-preview-04-09'; + const config = { + responseModalities: [Modality.AUDIO], + speechConfig: { + voiceConfig: { + prebuiltVoiceConfig: { + voiceName: voiceName, + }, + }, + }, + }; + + const responseQueue = []; + + async function waitMessage() { + while (responseQueue.length === 0) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + return responseQueue.shift(); + } + + async function handleTurn() { + const audioChunks = []; + let done = false; + + while (!done) { + const message = await waitMessage(); + + const serverContent = message.serverContent; + if ( + serverContent && + serverContent.modelTurn && + serverContent.modelTurn.parts + ) { + for (const part of serverContent.modelTurn.parts) { + if (part && part.inlineData && part.inlineData.data) { + audioChunks.push(Buffer.from(part.inlineData.data)); + } + } + } + + if (serverContent && serverContent.turnComplete) { + done = true; + } + } + + return audioChunks; + } + + const session = await client.live.connect({ + model: modelId, + config: config, + callbacks: { + onmessage: msg => responseQueue.push(msg), + onerror: e => console.error('Error:', e.message), + }, + }); + + const textInput = 'Hello? Gemini are you there?'; + console.log('> ', textInput, '\n'); + + await session.sendClientContent({ + turns: [{role: 'user', parts: [{text: textInput}]}], + }); + + const audioChunks = await handleTurn(); + + session.close(); + + if (audioChunks.length > 0) { + const audioBuffer = Buffer.concat(audioChunks); + fs.writeFileSync('response.raw', audioBuffer); + console.log('Received audio answer (saved to response.raw)'); + } + + // Example output: + //> Hello? Gemini, are you there? + // Received audio answer (saved to response.raw) + + return audioChunks; +} + +// [END googlegenaisdk_live_audio_with_txt] + +module.exports = { + generateLiveConversation, +}; diff --git a/genai/live/live-code-exec-with-txt.js b/genai/live/live-code-exec-with-txt.js new file mode 100644 index 0000000000..da269ccbed --- /dev/null +++ b/genai/live/live-code-exec-with-txt.js @@ -0,0 +1,101 @@ +// Copyright 2025 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 +// +// https://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. + +// [START googlegenaisdk_live_code_exec_with_txt] + +'use strict'; + +const {GoogleGenAI, Modality} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = process.env.GOOGLE_CLOUD_LOCATION || 'global'; + +async function generateLiveCodeExec( + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + }); + + const modelId = 'gemini-2.0-flash-live-preview-04-09'; + const config = { + responseModalities: [Modality.TEXT], + tools: [ + { + codeExecution: {}, + }, + ], + }; + + const responseQueue = []; + + async function waitMessage() { + while (responseQueue.length === 0) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + return responseQueue.shift(); + } + + async function handleTurn() { + const turns = []; + let done = false; + while (!done) { + const message = await waitMessage(); + turns.push(message); + if (message.serverContent && message.serverContent.turnComplete) { + done = true; + } + } + return turns; + } + + const session = await client.live.connect({ + model: modelId, + config: config, + callbacks: { + onmessage: msg => responseQueue.push(msg), + onerror: e => console.error('Error:', e.message), + }, + }); + + const textInput = 'Compute the largest prime palindrome under 10'; + console.log('> ', textInput, '\n'); + + await session.sendClientContent({ + turns: [{role: 'user', parts: [{text: textInput}]}], + }); + + const turns = await handleTurn(); + for (const turn of turns) { + if (turn.text) { + console.log('Received text:', turn.text); + } + } + + // Example output: + // > Compute the largest prime palindrome under 10 + // The largest prime palindrome under 10 is 7. + + session.close(); + return turns; +} + +// [END googlegenaisdk_live_code_exec_with_txt] + +module.exports = { + generateLiveCodeExec, +}; diff --git a/genai/live/live-conversation-audio-with-audio.js b/genai/live/live-conversation-audio-with-audio.js new file mode 100644 index 0000000000..147514ce54 --- /dev/null +++ b/genai/live/live-conversation-audio-with-audio.js @@ -0,0 +1,170 @@ +// Copyright 2025 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 +// +// https://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. + +// [START googlegenaisdk_live_conversation_audio_with_audio] + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const {GoogleGenAI, Modality} = require('@google/genai'); + +const MODEL = 'gemini-2.0-flash-live-preview-04-09'; +const INPUT_RATE = 16000; +const OUTPUT_RATE = 24000; +const SAMPLE_WIDTH = 2; // 16-bit + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = process.env.GOOGLE_CLOUD_LOCATION || 'global'; + +function readWavefile(filepath) { + const buffer = fs.readFileSync(filepath); + const audioBytes = buffer.subarray(44); + const base64Data = audioBytes.toString('base64'); + const mimeType = `audio/pcm;rate=${INPUT_RATE}`; + return {base64Data, mimeType}; +} + +// Utility: write bytes -> .wav file +function writeWavefile(filepath, audioFrames, rate = OUTPUT_RATE) { + const rawAudioBytes = Buffer.concat(audioFrames); + const header = Buffer.alloc(44); + header.write('RIFF', 0); + header.writeUInt32LE(36 + rawAudioBytes.length, 4); + header.write('WAVE', 8); + header.write('fmt ', 12); + header.writeUInt32LE(16, 16); + header.writeUInt16LE(1, 20); + header.writeUInt16LE(1, 22); + header.writeUInt32LE(rate, 24); + header.writeUInt32LE(rate * SAMPLE_WIDTH, 28); + header.writeUInt16LE(SAMPLE_WIDTH, 32); + header.writeUInt16LE(16, 34); + header.write('data', 36); + header.writeUInt32LE(rawAudioBytes.length, 40); + + fs.writeFileSync(filepath, Buffer.concat([header, rawAudioBytes])); + console.log(`Model response saved to ${filepath}`); +} + +async function generateLiveConversation( + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + console.log('Starting audio conversation sample...'); + console.log(`Project: ${projectId}, Location: ${location}`); + + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + }); + + const responseQueue = []; + + async function waitMessage(timeoutMs = 60 * 1000) { + const startTime = Date.now(); + + while (responseQueue.length === 0) { + if (Date.now() - startTime > timeoutMs) { + console.warn('No messages received within timeout. Exiting...'); + return null; // timeout occurred + } + await new Promise(resolve => setTimeout(resolve, 100)); + } + + return responseQueue.shift(); + } + + async function handleTurn() { + const audioFrames = []; + let done = false; + + while (!done) { + const message = await waitMessage(); + const serverContent = message.serverContent; + + if (serverContent && serverContent.inputTranscription) { + console.log('Input transcription', serverContent.inputTranscription); + } + if (serverContent && serverContent.outputTranscription) { + console.log('Output transcription', serverContent.outputTranscription); + } + if ( + serverContent && + serverContent.modelTurn && + serverContent.modelTurn.parts + ) { + for (const part of serverContent.modelTurn.parts) { + if (part && part.inlineData && part.inlineData.data) { + const audioData = Buffer.from(part.inlineData.data, 'base64'); + audioFrames.push(audioData); + } + } + } + if (serverContent && serverContent.turnComplete) { + done = true; + } + } + + return audioFrames; + } + + const session = await client.live.connect({ + model: MODEL, + config: { + responseModalities: [Modality.AUDIO], + inputAudioTranscription: {}, + outputAudioTranscription: {}, + }, + callbacks: { + onmessage: msg => responseQueue.push(msg), + onerror: e => console.error(e.message), + onclose: () => console.log('Closed'), + }, + }); + + const wavFilePath = path.join(__dirname, 'hello_gemini_are_you_there.wav'); + console.log('Reading file:', wavFilePath); + + const {base64Data, mimeType} = readWavefile(wavFilePath); + const audioBytes = Buffer.from(base64Data, 'base64'); + + await session.sendRealtimeInput({ + media: { + data: audioBytes.toString('base64'), + mimeType: mimeType, + }, + }); + + console.log('Audio sent, waiting for response...'); + + const audioFrames = await handleTurn(); + if (audioFrames.length > 0) { + writeWavefile( + path.join(__dirname, 'example_model_response.wav'), + audioFrames, + OUTPUT_RATE + ); + } + + await session.close(); + return audioFrames; +} + +// [END googlegenaisdk_live_conversation_audio_with_audio] + +module.exports = { + generateLiveConversation, +}; diff --git a/genai/live/live-func-call-with-txt.js b/genai/live/live-func-call-with-txt.js new file mode 100644 index 0000000000..25277b8133 --- /dev/null +++ b/genai/live/live-func-call-with-txt.js @@ -0,0 +1,125 @@ +// Copyright 2025 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 +// +// https://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. + +// [START googlegenaisdk_live_func_call_with_txt] + +'use strict'; + +const {GoogleGenAI, Modality} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = process.env.GOOGLE_CLOUD_LOCATION || 'global'; + +async function generateLiveFunctionCall( + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + }); + + const modelId = 'gemini-2.0-flash-live-preview-04-09'; + + const config = { + responseModalities: [Modality.TEXT], + tools: [ + { + functionDeclarations: [ + {name: 'turn_on_the_lights'}, + {name: 'turn_off_the_lights'}, + ], + }, + ], + }; + + const responseQueue = []; + + async function waitMessage() { + while (responseQueue.length === 0) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + return responseQueue.shift(); + } + + async function handleTurn() { + const turns = []; + let done = false; + while (!done) { + const message = await waitMessage(); + turns.push(message); + + if (message.toolCall) { + for (const fc of message.toolCall.functionCalls) { + console.log(`Model requested function call: ${fc.name}`); + + await session.sendToolResponse({ + functionResponses: [ + { + id: fc.id, + name: fc.name, + response: {result: 'ok'}, + }, + ], + }); + console.log(`Sent tool response for ${fc.name}:`, {result: 'ok'}); + } + } + + if (message.serverContent && message.serverContent.turnComplete) { + done = true; + } + } + return turns; + } + + const session = await client.live.connect({ + model: modelId, + config: config, + callbacks: { + onmessage: msg => responseQueue.push(msg), + onerror: e => console.error('Error:', e.message), + }, + }); + + const textInput = 'Turn on the lights please'; + console.log('> ', textInput, '\n'); + + await session.sendClientContent({ + turns: [{role: 'user', parts: [{text: textInput}]}], + }); + + const turns = await handleTurn(); + + for (const turn of turns) { + if (turn.text) { + console.log('Received text:', turn.text); + } + } + + // Example output: + //>> Turn on the lights please + // Model requested function call: turn_on_the_lights + // Sent tool response for turn_on_the_lights: { result: 'ok' } + + session.close(); + return turns; +} + +// [END googlegenaisdk_live_func_call_with_txt] + +module.exports = { + generateLiveFunctionCall, +}; diff --git a/genai/live/live-ground-googsearch-with-txt.js b/genai/live/live-ground-googsearch-with-txt.js new file mode 100644 index 0000000000..c81b5fe618 --- /dev/null +++ b/genai/live/live-ground-googsearch-with-txt.js @@ -0,0 +1,100 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; +// [START googlegenaisdk_live_ground_googsearch_with_txt] +const {GoogleGenAI, Modality} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = process.env.GOOGLE_CLOUD_LOCATION || 'global'; + +async function generateLiveGoogleSearch( + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + }); + + const modelId = 'gemini-2.0-flash-live-preview-04-09'; + + const config = { + responseModalities: [Modality.TEXT], + tools: [{googleSearch: {}}], + }; + + const responseQueue = []; + + async function waitMessage() { + while (responseQueue.length === 0) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + return responseQueue.shift(); + } + + async function handleTurn() { + const turns = []; + let done = false; + while (!done) { + const message = await waitMessage(); + turns.push(message); + if (message.serverContent && message.serverContent.turnComplete) { + done = true; + } + } + return turns; + } + + const session = await client.live.connect({ + model: modelId, + config: config, + callbacks: { + onmessage: msg => responseQueue.push(msg), + onerror: e => console.error('Error:', e.message), + }, + }); + + const textInput = + 'When did the last Brazil vs. Argentina soccer match happen?'; + console.log('> ', textInput, '\n'); + + await session.sendClientContent({ + turns: [{role: 'user', parts: [{text: textInput}]}], + }); + + const turns = await handleTurn(); + for (const turn of turns) { + if (turn.text) { + console.log('Received text:', turn.text); + } + } + + // Example output: + // > When did the last Brazil vs. Argentina soccer match happen? + // Received text: The last + // Received text: Brazil vs. Argentina soccer match was on March 25, 202 + // Received text: 5.Argentina won 4-1 in the 2026 FIFA World Cup + // Received text: qualifier. + + session.close(); + return turns; +} + +// [END googlegenaisdk_live_ground_googsearch_with_txt] + +module.exports = { + generateLiveGoogleSearch, +}; diff --git a/genai/live/live-ground-ragengine-with-txt.js b/genai/live/live-ground-ragengine-with-txt.js new file mode 100644 index 0000000000..2e407cbfe9 --- /dev/null +++ b/genai/live/live-ground-ragengine-with-txt.js @@ -0,0 +1,123 @@ +// Copyright 2025 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 +// +// https://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. + +// [START googlegenaisdk_live_ground_ragengine_with_txt] + +'use strict'; + +const {GoogleGenAI, Modality} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = process.env.GOOGLE_CLOUD_LOCATION || 'global'; + +// (DEVELOPER) put here your memory corpus +const RAG_CORPUS_ID = ''; + +async function generateLiveRagTextResponse( + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION, + rag_corpus_id = RAG_CORPUS_ID +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + }); + const memoryCorpus = `projects/${projectId}/locations/${location}/ragCorpora/${rag_corpus_id}`; + const modelId = 'gemini-2.0-flash-live-preview-04-09'; + + // RAG store config + const ragStore = { + ragResources: [ + { + ragCorpus: memoryCorpus, // Use memory corpus if you want to store context + }, + ], + storeContext: true, // sink context into your memory corpus + }; + + const config = { + responseModalities: [Modality.TEXT], + tools: [ + { + retrieval: { + vertexRagStore: ragStore, + }, + }, + ], + }; + + const responseQueue = []; + + async function waitMessage() { + while (responseQueue.length === 0) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + return responseQueue.shift(); + } + + async function handleTurn() { + const turns = []; + let done = false; + while (!done) { + const message = await waitMessage(); + turns.push(message); + if (message.serverContent && message.serverContent.turnComplete) { + done = true; + } + } + return turns; + } + + const session = await client.live.connect({ + model: modelId, + config: config, + callbacks: { + onmessage: msg => responseQueue.push(msg), + onerror: e => console.error('Error:', e.message), + }, + }); + + const textInput = 'What are newest gemini models?'; + console.log('> ', textInput, '\n'); + + await session.sendClientContent({ + turns: [{role: 'user', parts: [{text: textInput}]}], + }); + + const turns = await handleTurn(); + const response = []; + + for (const turn of turns) { + if (turn.text) { + response.push(turn.text); + } + } + + console.log(response.join('')); + + // Example output: + // > What are newest gemini models? + // In December 2023, Google launched Gemini, their "most capable and general model". It's multimodal, meaning it understands and combines different types of information like text, code, audio, images, and video. + + session.close(); + + return response; +} + +// [END googlegenaisdk_live_ground_ragengine_with_txt] + +module.exports = { + generateLiveRagTextResponse, +}; diff --git a/genai/live/live-structured-ouput-with-txt.js b/genai/live/live-structured-ouput-with-txt.js new file mode 100644 index 0000000000..f77bba8f98 --- /dev/null +++ b/genai/live/live-structured-ouput-with-txt.js @@ -0,0 +1,93 @@ +// Copyright 2025 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 +// +// https://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. + +// [START googlegenaisdk_live_structured_output_with_txt] + +'use strict'; +const {OpenAI} = require('openai'); +const {GoogleAuth} = require('google-auth-library'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = + process.env.GOOGLE_CLOUD_LOCATION || 'us-central1'; + +const CalendarEventSchema = { + type: 'object', + properties: { + name: {type: 'string'}, + date: {type: 'string'}, + participants: { + type: 'array', + items: {type: 'string'}, + }, + }, + required: ['name', 'date', 'participants'], +}; + +async function generateStructuredTextResponse( + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const auth = new GoogleAuth({ + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }); + const client = await auth.getClient(); + const tokenResponse = await client.getAccessToken(); + + const token = tokenResponse.token; + + const ENDPOINT_ID = 'openapi'; + const baseURL = `https://${location}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/endpoints/${ENDPOINT_ID}`; + + const openAI = new OpenAI({ + apiKey: token, + baseURL: baseURL, + }); + + const completion = await openAI.chat.completions.create({ + model: 'google/gemini-2.0-flash-001', + messages: [ + {role: 'system', content: 'Extract the event information.'}, + { + role: 'user', + content: 'Alice and Bob are going to a science fair on Friday.', + }, + ], + response_format: { + type: 'json_schema', + json_schema: { + name: 'CalendarEvent', + schema: CalendarEventSchema, + }, + }, + }); + + const response = completion.choices[0].message.content; + console.log(response); + + // Example expected output: + // { + // name: 'science fair', + // date: 'Friday', + // participants: ['Alice', 'Bob'] + // } + + return response; +} + +// [END googlegenaisdk_live_structured_output_with_txt] + +module.exports = { + generateStructuredTextResponse, +}; diff --git a/genai/live/live-transcribe-with-audio.js b/genai/live/live-transcribe-with-audio.js new file mode 100644 index 0000000000..dcc23c1f0c --- /dev/null +++ b/genai/live/live-transcribe-with-audio.js @@ -0,0 +1,110 @@ +// Copyright 2025 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 +// +// https://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. + +// [START googlegenaisdk_live_transcribe_with_audio] + +'use strict'; + +const {GoogleGenAI, Modality} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = process.env.GOOGLE_CLOUD_LOCATION || 'global'; + +async function generateLiveAudioTranscription( + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + }); + + const modelId = 'gemini-live-2.5-flash-preview-native-audio'; + const config = { + responseModalities: [Modality.AUDIO], + inputAudioTranscription: {}, + outputAudioTranscription: {}, + }; + + const responseQueue = []; + + async function waitMessage() { + while (responseQueue.length === 0) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + return responseQueue.shift(); + } + + async function handleTurn() { + const turns = []; + let done = false; + const outputMessage = []; + while (!done) { + const message = await waitMessage(); + turns.push(message); + + const serverContent = message.serverContent; + if (serverContent && serverContent.modelTurn) { + console.log('Model turn:', serverContent.modelTurn); + } + if (serverContent && serverContent.inputTranscription) { + console.log('Input transcript:', serverContent.inputTranscription.text); + } + if ( + serverContent && + serverContent.outputTranscription && + serverContent.outputTranscription.text + ) { + outputMessage.push(serverContent.outputTranscription.text); + } + if (serverContent && serverContent.turnComplete) { + done = true; + } + } + console.log('Output transcript:', outputMessage.join('')); + return turns; + } + + const session = await client.live.connect({ + model: modelId, + config: config, + callbacks: { + onmessage: msg => responseQueue.push(msg), + onerror: e => console.error('Error:', e.message), + }, + }); + + const inputTxt = 'Hello? Gemini, are you there?'; + console.log('> ', inputTxt, '\n'); + + await session.sendClientContent({ + turns: [{role: 'user', parts: [{text: inputTxt}]}], + }); + + const turns = await handleTurn(session); + + // Example output: + //> Hello? Gemini, are you there? + // Yes, I'm here. What would you like to talk about? + + session.close(); + return turns; +} + +// [END googlegenaisdk_live_transcribe_with_audio] + +module.exports = { + generateLiveAudioTranscription, +}; diff --git a/genai/live/live-txt-with-audio.js b/genai/live/live-txt-with-audio.js new file mode 100644 index 0000000000..92a5fdd594 --- /dev/null +++ b/genai/live/live-txt-with-audio.js @@ -0,0 +1,111 @@ +// Copyright 2025 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 +// +// https://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. + +// [START googlegenaisdk_live_txt_with_audio] + +'use strict'; + +const {GoogleGenAI, Modality} = require('@google/genai'); +const fetch = require('node-fetch'); +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = process.env.GOOGLE_CLOUD_LOCATION || 'global'; + +async function generateLiveConversation( + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + }); + + const modelId = 'gemini-2.0-flash-live-preview-04-09'; + const config = { + responseModalities: [Modality.TEXT], + }; + + const responseQueue = []; + + async function waitMessage() { + while (responseQueue.length === 0) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + return responseQueue.shift(); + } + + async function handleTurn() { + const turns = []; + let done = false; + while (!done) { + const message = await waitMessage(); + turns.push(message); + if (message.serverContent && message.serverContent.turnComplete) { + done = true; + } + } + return turns; + } + + const session = await client.live.connect({ + model: modelId, + config: config, + callbacks: { + onmessage: msg => responseQueue.push(msg), + onerror: e => console.error('Error:', e.message), + }, + }); + + const audioUrl = + 'https://storage.googleapis.com/generativeai-downloads/data/16000.wav'; + + console.log('> Answer to this audio url', audioUrl); + + const res = await fetch(audioUrl); + if (!res.ok) throw new Error(`Failed to fetch audio: ${res.status}`); + const arrayBuffer = await res.arrayBuffer(); + const audioBytes = Buffer.from(arrayBuffer).toString('base64'); + + await session.sendRealtimeInput({ + media: { + data: audioBytes, + mimeType: 'audio/pcm;rate=16000', + }, + }); + + const turns = await handleTurn(); + + const response = []; + for (const turn of turns) { + if (turn.text) { + response.push(turn.text); + } + } + + console.log('Final response:', response.join('')); + + // Example output: + //> Answer to this audio url https://storage.googleapis.com/generativeai-downloads/data/16000.wav + // Final response: Yes, I can hear you. How are you doing today? + + session.close(); + + return response; +} + +// [END googlegenaisdk_live_txt_with_audio] + +module.exports = { + generateLiveConversation, +}; diff --git a/genai/live/live-with-txt.js b/genai/live/live-with-txt.js new file mode 100644 index 0000000000..8eaeacf7f1 --- /dev/null +++ b/genai/live/live-with-txt.js @@ -0,0 +1,93 @@ +// Copyright 2025 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 +// +// https://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. + +// [START googlegenaisdk_live_with_txt] + +'use strict'; + +const {GoogleGenAI, Modality} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = process.env.GOOGLE_CLOUD_LOCATION || 'global'; + +async function generateLiveConversation( + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + }); + + const modelId = 'gemini-2.0-flash-live-preview-04-09'; + const config = {responseModalities: [Modality.TEXT]}; + + const responseQueue = []; + + async function waitMessage() { + while (responseQueue.length === 0) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + return responseQueue.shift(); + } + + async function handleTurn() { + const turns = []; + let done = false; + while (!done) { + const message = await waitMessage(); + turns.push(message); + if (message.serverContent && message.serverContent.turnComplete) { + done = true; + } + } + return turns; + } + + const session = await client.live.connect({ + model: modelId, + config: config, + callbacks: { + onmessage: msg => responseQueue.push(msg), + onerror: e => console.error('Error:', e.message), + }, + }); + + const textInput = 'Hello? Gemini, are you there?'; + console.log('> ', textInput, '\n'); + + await session.sendClientContent({ + turns: [{role: 'user', parts: [{text: textInput}]}], + }); + + const turns = await handleTurn(); + for (const turn of turns) { + if (turn.text) { + console.log('Received text:', turn.text); + } + } + // Example output: + //> Hello? Gemini, are you there? + // Received text: Yes + // Received text: I'm here. How can I help you today? + session.close(); + return turns; +} + +// [END googlegenaisdk_live_with_txt] + +module.exports = { + generateLiveConversation, +}; diff --git a/genai/live/package.json b/genai/live/package.json new file mode 100644 index 0000000000..6df7d1b49b --- /dev/null +++ b/genai/live/package.json @@ -0,0 +1,25 @@ +{ + "name": "nodejs-genai-live", + "private": true, + "license": "Apache-2.0", + "author": "Google LLC", + "engines": { + "node": ">=16.0.0" + }, + "scripts": { + "test": "c8 mocha -p -j 2 --timeout 2400000 test/*.test.js" + }, + "dependencies": { + "@google/genai": "1.30.0", + "google-auth-library": "^10.3.0", + "node-fetch": "^2.7.0", + "openai": "^5.19.1" + }, + "devDependencies": { + "c8": "^10.0.0", + "chai": "^4.5.0", + "mocha": "^10.0.0", + "proxyquire": "^2.1.3", + "sinon": "^18.0.0" + } +} diff --git a/genai/live/test/live-audio-with-txt.test.js b/genai/live/test/live-audio-with-txt.test.js new file mode 100644 index 0000000000..2a353a8b0b --- /dev/null +++ b/genai/live/test/live-audio-with-txt.test.js @@ -0,0 +1,30 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const sample = require('../live-audio-with-txt'); + +describe('live-audio-with-txt', () => { + it('should generate audio content in a live session conversation from a text prompt', async function () { + this.timeout(180000); + const output = await sample.generateLiveConversation(projectId); + console.log('Generated output:', output); + assert(output.length > 0); + }); +}); diff --git a/genai/live/test/live-code-exec-with-txt.test.js b/genai/live/test/live-code-exec-with-txt.test.js new file mode 100644 index 0000000000..ee4e0e08c3 --- /dev/null +++ b/genai/live/test/live-code-exec-with-txt.test.js @@ -0,0 +1,30 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const sample = require('../live-code-exec-with-txt'); + +describe('live-code-exec-with-txt', () => { + it('should generate code execution in a live session from a text prompt', async function () { + this.timeout(180000); + const output = await sample.generateLiveCodeExec(projectId); + console.log('Generated output:', output); + assert(output.length > 0); + }); +}); diff --git a/genai/live/test/live-conversation-audio-with-audio.test.js b/genai/live/test/live-conversation-audio-with-audio.test.js new file mode 100644 index 0000000000..e832d7743e --- /dev/null +++ b/genai/live/test/live-conversation-audio-with-audio.test.js @@ -0,0 +1,89 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); +const sinon = require('sinon'); + +const projectId = process.env.CAIP_PROJECT_ID; +const {delay} = require('../../test/util'); +const proxyquire = require('proxyquire'); + +describe('live-conversation-audio-with-audio', () => { + it('should generate content in a live session conversation from a text prompt', async function () { + const mockClient = { + live: { + connect: async (opts = {}) => { + setImmediate(() => + opts.callbacks.onmessage({ + serverContent: { + inputTranscription: 'Hello Gemini', + outputTranscription: 'Hi Mocked Gemini there!', + modelTurn: { + parts: [ + { + inlineData: { + data: Buffer.from('fake audio data').toString('base64'), + }, + }, + ], + }, + turnComplete: false, + }, + }) + ); + + setImmediate(() => + opts.callbacks.onmessage({ + serverContent: { + modelTurn: {parts: []}, + turnComplete: true, + }, + }) + ); + + return { + sendRealtimeInput: async () => {}, + close: async () => {}, + }; + }, + }, + }; + + const sample = proxyquire('../live-conversation-audio-with-audio', { + '@google/genai': { + GoogleGenAI: function () { + return mockClient; + }, + Modality: {AUDIO: 'AUDIO'}, + }, + fs: { + readFileSync: sinon.stub().returns(Buffer.alloc(100, 0)), + writeFileSync: sinon.stub().returns(), + }, + path: { + join: (...args) => args.join('/'), + }, + }); + + this.timeout(180000); + this.retries(4); + await delay(this.test); + const output = await sample.generateLiveConversation(projectId); + console.log('Generated output:', output); + assert(output.length > 0); + }); +}); diff --git a/genai/live/test/live-func-call-with-txt.test.js b/genai/live/test/live-func-call-with-txt.test.js new file mode 100644 index 0000000000..f2b5a4ef06 --- /dev/null +++ b/genai/live/test/live-func-call-with-txt.test.js @@ -0,0 +1,30 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const sample = require('../live-func-call-with-txt'); + +describe('live-func-call-with-txt', () => { + it('should generate function call in a live session from a text prompt', async function () { + this.timeout(180000); + const output = await sample.generateLiveFunctionCall(projectId); + console.log('Generated output:', output); + assert(output.length > 0); + }); +}); diff --git a/genai/live/test/live-ground-googsearch-with-txt.test.js b/genai/live/test/live-ground-googsearch-with-txt.test.js new file mode 100644 index 0000000000..cafa6c67b6 --- /dev/null +++ b/genai/live/test/live-ground-googsearch-with-txt.test.js @@ -0,0 +1,30 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const sample = require('../live-ground-googsearch-with-txt.js'); + +describe('live-ground-googsearch-with-txt', () => { + it('should generate Google Search in a live session from a text prompt', async function () { + this.timeout(180000); + const output = await sample.generateLiveGoogleSearch(projectId); + console.log('Generated output:', output); + assert(output.length > 0); + }); +}); diff --git a/genai/live/test/live-ground-ragengine-with-txt.test.js b/genai/live/test/live-ground-ragengine-with-txt.test.js new file mode 100644 index 0000000000..5e24f17e40 --- /dev/null +++ b/genai/live/test/live-ground-ragengine-with-txt.test.js @@ -0,0 +1,67 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); +const proxyquire = require('proxyquire'); + +const {delay} = require('../../test/util'); + +describe('live-ground-ragengine-with-txt', () => { + it('should return text from mocked RAG session', async function () { + const fakeSession = { + sendClientContent: async () => {}, + close: async () => {}, + }; + + const mockClient = { + live: { + connect: async (opts = {}) => { + setImmediate(() => + opts.callbacks.onmessage({ + text: 'In December 2023, Google launched Gemini...', + serverContent: {turnComplete: false}, + }) + ); + setImmediate(() => + opts.callbacks.onmessage({ + text: 'Mock final message.', + serverContent: {turnComplete: true}, + }) + ); + + return fakeSession; + }, + }, + }; + + const sample = proxyquire('../live-ground-ragengine-with-txt', { + '@google/genai': { + GoogleGenAI: function () { + return mockClient; + }, + Modality: {TEXT: 'TEXT'}, + }, + }); + + this.timeout(10000); + this.retries(4); + await delay(this.test); + const output = await sample.generateLiveRagTextResponse(); + console.log('Generated output:', output); + assert(output.length > 0); + }); +}); diff --git a/genai/live/test/live-structured-ouput-with-txt.test.js b/genai/live/test/live-structured-ouput-with-txt.test.js new file mode 100644 index 0000000000..208c329240 --- /dev/null +++ b/genai/live/test/live-structured-ouput-with-txt.test.js @@ -0,0 +1,30 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const sample = require('../live-structured-ouput-with-txt'); + +describe('live-structured-ouput-with-txt', () => { + it('should extract structured information from text input using the model', async function () { + this.timeout(18000); + const output = await sample.generateStructuredTextResponse(projectId); + console.log('Generated output:', output); + assert(output.length > 0); + }); +}); diff --git a/genai/live/test/live-transcribe-with-audio.test.js b/genai/live/test/live-transcribe-with-audio.test.js new file mode 100644 index 0000000000..c6f72a6ce4 --- /dev/null +++ b/genai/live/test/live-transcribe-with-audio.test.js @@ -0,0 +1,30 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const sample = require('../live-transcribe-with-audio'); + +describe('live-transcribe-with-audio', () => { + it('should transcribe audio input into text using the live model', async function () { + this.timeout(180000); + const output = await sample.generateLiveAudioTranscription(projectId); + console.log('Generated output:', output); + assert(output.length > 0); + }); +}); diff --git a/genai/live/test/live-txt-with-audio.test.js b/genai/live/test/live-txt-with-audio.test.js new file mode 100644 index 0000000000..fe69267e26 --- /dev/null +++ b/genai/live/test/live-txt-with-audio.test.js @@ -0,0 +1,86 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const {delay} = require('../../test/util'); + +const proxyquire = require('proxyquire'); + +describe('live-txt-with-audio', () => { + it('should generate txt content in a live session from an audio', async function () { + const fakeFetch = async () => ({ + ok: true, + arrayBuffer: async () => Buffer.from('fake audio'), + }); + + const fakeClient = { + live: { + connect: async (opts = {}) => { + console.log('Mock is called'); + + if ( + opts && + opts.callbacks && + typeof opts.callbacks.onmessage === 'function' + ) { + setImmediate(() => + opts.callbacks.onmessage({ + text: 'Yes, I can hear you.', + serverContent: { + turnComplete: false, + }, + }) + ); + + setImmediate(() => + opts.callbacks.onmessage({ + text: 'Here is the final response.', + serverContent: { + turnComplete: true, + }, + }) + ); + } + + return { + sendRealtimeInput: async () => {}, + close: async () => {}, + }; + }, + }, + }; + + const sample = proxyquire('../live-txt-with-audio', { + 'node-fetch': fakeFetch, + '@google/genai': { + GoogleGenAI: function () { + return fakeClient; + }, + Modality: {TEXT: 'TEXT'}, + }, + }); + + this.timeout(180000); + this.retries(4); + await delay(this.test); + const output = await sample.generateLiveConversation(projectId); + console.log('Generated output:', output); + assert(output.length > 0); + }); +}); diff --git a/genai/live/test/live-with-txt.test.js b/genai/live/test/live-with-txt.test.js new file mode 100644 index 0000000000..a95468fa02 --- /dev/null +++ b/genai/live/test/live-with-txt.test.js @@ -0,0 +1,30 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const sample = require('../live-with-txt'); + +describe('live-with-txt', () => { + it('should generate content in a live session from a text prompt', async function () { + this.timeout(180000); + const output = await sample.generateLiveConversation(projectId); + console.log('Generated output:', output); + assert(output.length > 0); + }); +}); diff --git a/genai/output-folder/bw-example-image.png b/genai/output-folder/bw-example-image.png new file mode 100644 index 0000000000..a2210daa1a Binary files /dev/null and b/genai/output-folder/bw-example-image.png differ diff --git a/genai/output-folder/example-breakfast-meal.png b/genai/output-folder/example-breakfast-meal.png new file mode 100644 index 0000000000..41de3d7dad Binary files /dev/null and b/genai/output-folder/example-breakfast-meal.png differ diff --git a/genai/output-folder/example-cats-01.png b/genai/output-folder/example-cats-01.png new file mode 100644 index 0000000000..a0daeefd51 Binary files /dev/null and b/genai/output-folder/example-cats-01.png differ diff --git a/genai/output-folder/example-cats-02.png b/genai/output-folder/example-cats-02.png new file mode 100644 index 0000000000..56670df78d Binary files /dev/null and b/genai/output-folder/example-cats-02.png differ diff --git a/genai/output-folder/example-cats-03.png b/genai/output-folder/example-cats-03.png new file mode 100644 index 0000000000..a04ebdc559 Binary files /dev/null and b/genai/output-folder/example-cats-03.png differ diff --git a/genai/output-folder/example-image-10.png b/genai/output-folder/example-image-10.png new file mode 100644 index 0000000000..63d0c81a2a Binary files /dev/null and b/genai/output-folder/example-image-10.png differ diff --git a/genai/output-folder/example-image-12.png b/genai/output-folder/example-image-12.png new file mode 100644 index 0000000000..641e374c9c Binary files /dev/null and b/genai/output-folder/example-image-12.png differ diff --git a/genai/output-folder/example-image-2.png b/genai/output-folder/example-image-2.png new file mode 100644 index 0000000000..e516b14a64 Binary files /dev/null and b/genai/output-folder/example-image-2.png differ diff --git a/genai/output-folder/example-image-4.png b/genai/output-folder/example-image-4.png new file mode 100644 index 0000000000..18a55c0f68 Binary files /dev/null and b/genai/output-folder/example-image-4.png differ diff --git a/genai/output-folder/example-image-6.png b/genai/output-folder/example-image-6.png new file mode 100644 index 0000000000..a37525b8b6 Binary files /dev/null and b/genai/output-folder/example-image-6.png differ diff --git a/genai/output-folder/example-image-8.png b/genai/output-folder/example-image-8.png new file mode 100644 index 0000000000..1a17809919 Binary files /dev/null and b/genai/output-folder/example-image-8.png differ diff --git a/genai/output-folder/image.png b/genai/output-folder/image.png new file mode 100644 index 0000000000..3d9abc609a Binary files /dev/null and b/genai/output-folder/image.png differ diff --git a/genai/output-folder/output.png b/genai/output-folder/output.png new file mode 100644 index 0000000000..dffbae708d Binary files /dev/null and b/genai/output-folder/output.png differ diff --git a/genai/output-folder/paella-recipe.md b/genai/output-folder/paella-recipe.md new file mode 100644 index 0000000000..04b9fd81d9 --- /dev/null +++ b/genai/output-folder/paella-recipe.md @@ -0,0 +1,41 @@ +Let's cook some delicious paella! Here's an illustrated recipe for a classic Valencian paella. + +## Illustrated Paella Recipe + +### Ingredients: + +* 4 cups short-grain rice (Bomba or Calasparra) +* 6 cups chicken or vegetable broth +* 1 lb boneless, skinless chicken thighs, cut into 1-inch pieces +* 1 lb rabbit or pork ribs (optional, traditional) +* 1/2 lb fresh green beans, trimmed and halved +* 1/2 lb large lima beans or butter beans (fresh or frozen) +* 1 large ripe tomato, grated or finely chopped +* 1/2 cup olive oil +* 1 tsp sweet paprika +* Pinch of saffron threads, dissolved in a little warm broth +* Salt and freshly ground black pepper +* Fresh rosemary sprigs (for garnish, optional) +* Lemon wedges (for serving) + +### Equipment: + +* Paella pan (18-20 inches recommended) +* Large cutting board +* Sharp knife +* Measuring cups and spoons + +### Instructions: + +**Step 1: Prepare Your Ingredients** + +Gather all your ingredients and do your prep work. Cut the chicken and any other meats, chop your vegetables, and have your broth and spices ready. This makes the cooking process much smoother. +![image](./example-image-2.png) +**Step 2: Sauté the Meat** + +Heat the olive oil in your paella pan over medium-high heat. Add the chicken and optional rabbit/pork. Season with salt and pepper. Brown the meat well on all sides, ensuring it's cooked through. This browning adds a lot of flavor to your paella. Once browned, push the meat to the sides of the pan. +![image](./example-image-4.png) +**Step 3: Add Vegetables** + +Add the green beans and lima beans to the center of the pan. Sauté for about 5-7 minutes until they start to soften. Then, add the grated tomato and paprika. Cook for another 5 minutes, stirring occasionally, until the tomato breaks down and the mixture is fragrant. +![image](./example-image-6.png) diff --git a/genai/package.json b/genai/package.json index 9d94f3811b..f14d1d38f7 100644 --- a/genai/package.json +++ b/genai/package.json @@ -3,27 +3,35 @@ "private": true, "license": "Apache-2.0", "author": "Google LLC", + "repository": { + "type": "git", + "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" + }, "engines": { "node": ">=16.0.0" }, "files": [ "*.js" ], - "scripts": { - "test": "c8 mocha -p -j 2 --timeout 2400000 test/*.test.js test/**/*.test.js" - }, "dependencies": { - "@google/genai": "1.20.0", + "@google/genai": "1.34.0", "axios": "^1.6.2", - "luxon": "^3.7.1", + "luxon": "^3.7.2", + "node-fetch": "^3.3.2", + "openai": "^6.9.1", + "canvas": "^3.2.0", + "date-fns": "^4.1.0", + "google-auth-library": "^10.3.0", + "proxyquire": "^2.1.3", "supertest": "^7.0.0" }, "devDependencies": { "c8": "^10.0.0", "chai": "^4.5.0", "mocha": "^10.0.0", + "node-fetch": "^2.7.0", + "proxyquire": "^2.1.3", "sinon": "^18.0.0", - "uuid": "^10.0.0", - "proxyquire": "^2.1.3" + "uuid": "^10.0.0" } } diff --git a/genai/provisioned-throughput/package.json b/genai/provisioned-throughput/package.json new file mode 100644 index 0000000000..12bbc618e2 --- /dev/null +++ b/genai/provisioned-throughput/package.json @@ -0,0 +1,28 @@ +{ + "name": "nodejs-genai-provisioned-throughput-samples", + "version": "0.0.1", + "private": true, + "license": "Apache-2.0", + "author": "Google LLC", + "repository": { + "type": "git", + "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" + }, + "engines": { + "node": ">=16.0.0" + }, + "files": [ + "*.js" + ], + "scripts": { + "test": "c8 mocha -p -j 2 --timeout 2400000 test/*.test.js test/**/*.test.js" + }, + "dependencies": { + "@google/genai": "1.30.0" + }, + "devDependencies": { + "c8": "^10.0.0", + "chai": "^4.5.0", + "mocha": "^10.0.0" + } +} diff --git a/genai/provisioned-throughput/provisionedthroughput-with-txt.js b/genai/provisioned-throughput/provisionedthroughput-with-txt.js new file mode 100644 index 0000000000..6463284272 --- /dev/null +++ b/genai/provisioned-throughput/provisionedthroughput-with-txt.js @@ -0,0 +1,62 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +// [START googlegenaisdk_provisionedthroughput_with_txt] +const {GoogleGenAI} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = process.env.GOOGLE_CLOUD_LOCATION || 'global'; + +async function generateWithProvisionedThroughput( + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + httpOptions: { + apiVersion: 'v1', + headers: { + // Options: + // - "dedicated": Use Provisioned Throughput + // - "shared": Use pay-as-you-go + // https://cloud.google.com/vertex-ai/generative-ai/docs/use-provisioned-throughput + 'X-Vertex-AI-LLM-Request-Type': 'shared', + }, + }, + }); + + const response = await client.models.generateContent({ + model: 'gemini-2.5-flash', + contents: 'How does AI work?', + }); + + console.log(response.text); + + // Example response: + // Okay, let's break down how AI works. It's a broad field, so I'll focus on the ... + // Here's a simplified overview: + // ... + + return response.text; +} + +// [END googlegenaisdk_provisionedthroughput_with_txt] + +module.exports = { + generateWithProvisionedThroughput, +}; diff --git a/genai/provisioned-throughput/test/provisionedthroughput-with-txt.test.js b/genai/provisioned-throughput/test/provisionedthroughput-with-txt.test.js new file mode 100644 index 0000000000..9c9bb24f80 --- /dev/null +++ b/genai/provisioned-throughput/test/provisionedthroughput-with-txt.test.js @@ -0,0 +1,35 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const sample = require('../provisionedthroughput-with-txt.js'); +const {delay} = require('../../test/util'); + +describe('provisioned-throughput-with-txt', () => { + it('should return provisioned throughput result', async function () { + this.timeout(50000); + this.retries(4); + + await delay(this.test); + + const output = await sample.generateWithProvisionedThroughput(projectId); + + assert(output.length > 0); + }); +}); diff --git a/genai/safety/package.json b/genai/safety/package.json new file mode 100644 index 0000000000..6923065a92 --- /dev/null +++ b/genai/safety/package.json @@ -0,0 +1,28 @@ +{ + "name": "nodejs-genai-safety-samples", + "version": "0.0.1", + "private": true, + "license": "Apache-2.0", + "author": "Google LLC", + "repository": { + "type": "git", + "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" + }, + "engines": { + "node": ">=16.0.0" + }, + "files": [ + "*.js" + ], + "scripts": { + "test": "c8 mocha -p -j 2 --timeout 2400000 test/*.test.js test/**/*.test.js" + }, + "dependencies": { + "@google/genai": "1.30.0" + }, + "devDependencies": { + "c8": "^10.0.0", + "chai": "^4.5.0", + "mocha": "^10.0.0" + } +} diff --git a/genai/safety/safety-with-txt.js b/genai/safety/safety-with-txt.js new file mode 100644 index 0000000000..94390cdc8f --- /dev/null +++ b/genai/safety/safety-with-txt.js @@ -0,0 +1,115 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +// [START googlegenaisdk_safety_with_txt] +const {GoogleGenAI} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = process.env.GOOGLE_CLOUD_LOCATION || 'global'; + +async function generateWithSafetySettings( + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + }); + + const systemInstruction = 'Be as mean as possible.'; + + const prompt = + 'Write a list of 5 disrespectful things that I might say to the universe after stubbing my toe in the dark.'; + + const safetySettings = [ + { + category: 'HARM_CATEGORY_DANGEROUS_CONTENT', + threshold: 'BLOCK_LOW_AND_ABOVE', + }, + { + category: 'HARM_CATEGORY_HARASSMENT', + threshold: 'BLOCK_LOW_AND_ABOVE', + }, + { + category: 'HARM_CATEGORY_HATE_SPEECH', + threshold: 'BLOCK_LOW_AND_ABOVE', + }, + { + category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', + threshold: 'BLOCK_LOW_AND_ABOVE', + }, + ]; + + const response = await client.models.generateContent({ + model: 'gemini-2.5-flash', + contents: prompt, + config: { + systemInstruction: systemInstruction, + safetySettings: safetySettings, + }, + }); + + // console.log(response.text); + // console.log(response.candidates[0].finishMessage); + // + // for (const each of response.candidates[0].safetyRatings) { + // console.log('\nCategory:', String(each.category)); + // console.log('Is Blocked:', each.blocked); + // console.log('Probability:', each.probability); + // console.log('Probability Score:', each.probabilityScore); + // console.log('Severity:', each.severity); + // console.log('Severity Score:', each.severityScore); + // } + + // Example response: + // + // Category: HarmCategory.HARM_CATEGORY_HATE_SPEECH + // Is Blocked: False + // Probability: HarmProbability.NEGLIGIBLE + // Probability Score: 2.547714e-05 + // Severity: HarmSeverity.HARM_SEVERITY_NEGLIGIBLE + // Severity Score: None + // + // Category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT + // Is Blocked: False + // Probability: HarmProbability.NEGLIGIBLE + // Probability Score: 3.6103818e-06 + // Severity: HarmSeverity.HARM_SEVERITY_NEGLIGIBLE + // Severity Score: None + // + // Category: HarmCategory.HARM_CATEGORY_HARASSMENT + // Is Blocked: True + // Probability: HarmProbability.MEDIUM + // Probability Score: 0.71599233 + // Severity: HarmSeverity.HARM_SEVERITY_MEDIUM + // Severity Score: 0.30782545 + // + // Category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT + // Is Blocked: False + // Probability: HarmProbability.NEGLIGIBLE + // Probability Score: 1.5624657e-05 + // Severity: HarmSeverity.HARM_SEVERITY_NEGLIGIBLE + // Severity Score: None + + return response; +} + +// [END googlegenaisdk_safety_with_txt] + +module.exports = { + generateWithSafetySettings, +}; diff --git a/genai/safety/test/safety-with-txt.test.js b/genai/safety/test/safety-with-txt.test.js new file mode 100644 index 0000000000..dc7707aac8 --- /dev/null +++ b/genai/safety/test/safety-with-txt.test.js @@ -0,0 +1,33 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const sample = require('../safety-with-txt.js'); +const {delay} = require('../../test/util'); + +describe('safety-with-txt', () => { + it('should call generateContentStream with safety instructions', async function () { + this.timeout(50000); + this.retries(4); + await delay(this.test); + const output = await sample.generateWithSafetySettings(projectId); + console.log('output', output); + assert.isObject(output); + }); +}); diff --git a/genai/text-generation/package.json b/genai/text-generation/package.json new file mode 100644 index 0000000000..409a09dd2f --- /dev/null +++ b/genai/text-generation/package.json @@ -0,0 +1,20 @@ +{ + "name": "nodejs-genai-text-generation", + "private": true, + "license": "Apache-2.0", + "author": "Google LLC", + "engines": { + "node": ">=16.0.0" + }, + "scripts": { + "test": "c8 mocha -p -j 2 --timeout 2400000 test/*.test.js" + }, + "dependencies": { + "@google/genai": "1.30.0" + }, + "devDependencies": { + "c8": "^10.0.0", + "chai": "^4.5.0", + "mocha": "^10.0.0" + } +} diff --git a/genai/test-data/describe_video_content.mp4 b/genai/text-generation/test-data/describe_video_content.mp4 similarity index 100% rename from genai/test-data/describe_video_content.mp4 rename to genai/text-generation/test-data/describe_video_content.mp4 diff --git a/genai/test/test-data/latte.jpg b/genai/text-generation/test-data/latte.jpg similarity index 100% rename from genai/test/test-data/latte.jpg rename to genai/text-generation/test-data/latte.jpg diff --git a/genai/test/test-data/scones.jpg b/genai/text-generation/test-data/scones.jpg similarity index 100% rename from genai/test/test-data/scones.jpg rename to genai/text-generation/test-data/scones.jpg diff --git a/genai/test/textgen-async-with-txt.test.js b/genai/text-generation/test/textgen-async-with-txt.test.js similarity index 93% rename from genai/test/textgen-async-with-txt.test.js rename to genai/text-generation/test/textgen-async-with-txt.test.js index 540ff3e682..29a615e40b 100644 --- a/genai/test/textgen-async-with-txt.test.js +++ b/genai/text-generation/test/textgen-async-with-txt.test.js @@ -18,7 +18,7 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../text-generation/textgen-async-with-txt.js'); +const sample = require('../textgen-async-with-txt.js'); describe('textgen-async-with-txt', () => { it('should generate text content from a text prompt and with system instructions', async function () { diff --git a/genai/test/textgen-chat-stream-with-txt.test.js b/genai/text-generation/test/textgen-chat-stream-with-txt.test.js similarity index 92% rename from genai/test/textgen-chat-stream-with-txt.test.js rename to genai/text-generation/test/textgen-chat-stream-with-txt.test.js index a81fdb4031..041f4795f6 100644 --- a/genai/test/textgen-chat-stream-with-txt.test.js +++ b/genai/text-generation/test/textgen-chat-stream-with-txt.test.js @@ -18,7 +18,7 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../text-generation/textgen-chat-stream-with-txt.js'); +const sample = require('../textgen-chat-stream-with-txt.js'); describe('textgen-chat-stream-with-txt', () => { it('should generate text content from a mute video', async function () { diff --git a/genai/test/textgen-chat-with-txt.test.js b/genai/text-generation/test/textgen-chat-with-txt.test.js similarity index 93% rename from genai/test/textgen-chat-with-txt.test.js rename to genai/text-generation/test/textgen-chat-with-txt.test.js index 184013536c..f319f29938 100644 --- a/genai/test/textgen-chat-with-txt.test.js +++ b/genai/text-generation/test/textgen-chat-with-txt.test.js @@ -18,7 +18,7 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../text-generation/textgen-chat-with-txt.js'); +const sample = require('../textgen-chat-with-txt.js'); describe('textgen-chat-with-txt', () => { it('should generate chat content from a text prompt', async function () { diff --git a/genai/test/textgen-code-with-pdf.test.js b/genai/text-generation/test/textgen-code-with-pdf.test.js similarity index 93% rename from genai/test/textgen-code-with-pdf.test.js rename to genai/text-generation/test/textgen-code-with-pdf.test.js index 22ec5b77fe..0987fc72e1 100644 --- a/genai/test/textgen-code-with-pdf.test.js +++ b/genai/text-generation/test/textgen-code-with-pdf.test.js @@ -18,7 +18,7 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../text-generation/textgen-code-with-pdf.js'); +const sample = require('../textgen-code-with-pdf.js'); describe('textgen-code-with-pdf', () => { it('should generate text content from a pdf', async function () { diff --git a/genai/test/textgen-config-with-txt.test.js b/genai/text-generation/test/textgen-config-with-txt.test.js similarity index 93% rename from genai/test/textgen-config-with-txt.test.js rename to genai/text-generation/test/textgen-config-with-txt.test.js index b070952515..504bccbd61 100644 --- a/genai/test/textgen-config-with-txt.test.js +++ b/genai/text-generation/test/textgen-config-with-txt.test.js @@ -18,7 +18,7 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../text-generation/textgen-config-with-txt.js'); +const sample = require('../textgen-config-with-txt.js'); describe('textgen-config-with-txt', () => { it('should generate text content from a text prompt with config', async function () { diff --git a/genai/test/textgen-sys-instr-with-txt.test.js b/genai/text-generation/test/textgen-sys-instr-with-txt.test.js similarity index 92% rename from genai/test/textgen-sys-instr-with-txt.test.js rename to genai/text-generation/test/textgen-sys-instr-with-txt.test.js index 4e53e1de4f..f2f8ba13a6 100644 --- a/genai/test/textgen-sys-instr-with-txt.test.js +++ b/genai/text-generation/test/textgen-sys-instr-with-txt.test.js @@ -18,7 +18,7 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../text-generation/textgen-sys-instr-with-txt.js'); +const sample = require('../textgen-sys-instr-with-txt.js'); describe('textgen-sys-instr-with-txt', async () => { it('should generate text content from a text prompt and with system instructions', async () => { diff --git a/genai/test/textgen-transcript-with-gcs-audio.test.js b/genai/text-generation/test/textgen-transcript-with-gcs-audio.test.js similarity index 89% rename from genai/test/textgen-transcript-with-gcs-audio.test.js rename to genai/text-generation/test/textgen-transcript-with-gcs-audio.test.js index b1b7240bc9..7d9e063196 100644 --- a/genai/test/textgen-transcript-with-gcs-audio.test.js +++ b/genai/text-generation/test/textgen-transcript-with-gcs-audio.test.js @@ -18,8 +18,8 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../text-generation/textgen-transcript-with-gcs-audio.js'); -const {delay} = require('./util'); +const sample = require('../textgen-transcript-with-gcs-audio.js'); +const {delay} = require('../../test/util'); describe('textgen-transcript-with-gcs-audio', async () => { it('should generate text content from gsc audio with transcript', async function () { diff --git a/genai/test/textgen-with-gcs-audio.test.js b/genai/text-generation/test/textgen-with-gcs-audio.test.js similarity index 93% rename from genai/test/textgen-with-gcs-audio.test.js rename to genai/text-generation/test/textgen-with-gcs-audio.test.js index 4a86977bcd..4e7c9f62a1 100644 --- a/genai/test/textgen-with-gcs-audio.test.js +++ b/genai/text-generation/test/textgen-with-gcs-audio.test.js @@ -18,7 +18,7 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../text-generation/textgen-with-gcs-audio'); +const sample = require('../textgen-with-gcs-audio'); describe('textgen-with-gcs-audio', async () => { it('should generate text content from gsc audio', async function () { diff --git a/genai/test/textgen-with-local-video.test.js b/genai/text-generation/test/textgen-with-local-video.test.js similarity index 84% rename from genai/test/textgen-with-local-video.test.js rename to genai/text-generation/test/textgen-with-local-video.test.js index 4a315dbe6d..5db4bd6647 100644 --- a/genai/test/textgen-with-local-video.test.js +++ b/genai/text-generation/test/textgen-with-local-video.test.js @@ -18,11 +18,14 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../text-generation/textgen-with-local-video.js'); +const sample = require('../textgen-with-local-video.js'); +const {delay} = require('../../test/util'); describe('textgen-with-local-video', async () => { it('should generate text content from local video', async function () { - this.timeout(30000); + this.timeout(180000); + this.retries(5); + await delay(this.test); const output = await sample.generateText(projectId); assert(output.length > 0); }); diff --git a/genai/test/textgen-with-multi-img.test.js b/genai/text-generation/test/textgen-with-multi-img.test.js similarity index 88% rename from genai/test/textgen-with-multi-img.test.js rename to genai/text-generation/test/textgen-with-multi-img.test.js index 85ce27650b..e62c1fc249 100644 --- a/genai/test/textgen-with-multi-img.test.js +++ b/genai/text-generation/test/textgen-with-multi-img.test.js @@ -18,11 +18,14 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../text-generation/textgen-with-multi-img.js'); +const sample = require('../textgen-with-multi-img.js'); +const {delay} = require('../../test/util'); describe('textgen-with-multi-img', () => { it('should generate text content from a text prompt and multiple images', async function () { this.timeout(180000); + this.retries(5); + await delay(this.test); const output = await sample.generateContent(projectId); console.log('Generated output:', output); diff --git a/genai/test/textgen-with-multi-local-img.test.js b/genai/text-generation/test/textgen-with-multi-local-img.test.js similarity index 79% rename from genai/test/textgen-with-multi-local-img.test.js rename to genai/text-generation/test/textgen-with-multi-local-img.test.js index 6fae3d4d3c..1bdccb5b2c 100644 --- a/genai/test/textgen-with-multi-local-img.test.js +++ b/genai/text-generation/test/textgen-with-multi-local-img.test.js @@ -20,13 +20,16 @@ const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; const location = process.env.GOOGLE_CLOUD_LOCATION || 'global'; -const sample = require('../text-generation/textgen-with-multi-local-img.js'); +const sample = require('../textgen-with-multi-local-img.js'); +const {delay} = require('../../test/util'); describe('textgen-with-multi-local-img', () => { it('should generate text content from multiple images', async function () { - this.timeout(100000); - const imagePath1 = './test/test-data/latte.jpg'; - const imagePath2 = './test/test-data/scones.jpg'; + this.timeout(180000); + this.retries(5); + await delay(this.test); + const imagePath1 = './test-data/latte.jpg'; + const imagePath2 = './test-data/scones.jpg'; const output = await sample.generateContent( projectId, location, diff --git a/genai/test/textgen-with-mute-video.test.js b/genai/text-generation/test/textgen-with-mute-video.test.js similarity index 93% rename from genai/test/textgen-with-mute-video.test.js rename to genai/text-generation/test/textgen-with-mute-video.test.js index 65b5013126..a7a0b815b4 100644 --- a/genai/test/textgen-with-mute-video.test.js +++ b/genai/text-generation/test/textgen-with-mute-video.test.js @@ -18,7 +18,7 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../text-generation/textgen-with-mute-video.js'); +const sample = require('../textgen-with-mute-video.js'); describe('textgen-with-mute-video', () => { it('should generate text content from a mute video', async function () { diff --git a/genai/test/textgen-with-pdf.test.js b/genai/text-generation/test/textgen-with-pdf.test.js similarity index 84% rename from genai/test/textgen-with-pdf.test.js rename to genai/text-generation/test/textgen-with-pdf.test.js index 9651bb465a..3b517aebdd 100644 --- a/genai/test/textgen-with-pdf.test.js +++ b/genai/text-generation/test/textgen-with-pdf.test.js @@ -18,11 +18,14 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../text-generation/textgen-with-pdf.js'); +const sample = require('../textgen-with-pdf.js'); +const {delay} = require('../../test/util'); describe('textgen-with-pdf', async () => { it('should generate text content from pdf', async function () { - this.timeout(30000); + this.timeout(180000); + this.retries(5); + await delay(this.test); const output = await sample.generateText(projectId); assert(output.length > 0); }); diff --git a/genai/test/textgen-with-txt-img.test.js b/genai/text-generation/test/textgen-with-txt-img.test.js similarity index 90% rename from genai/test/textgen-with-txt-img.test.js rename to genai/text-generation/test/textgen-with-txt-img.test.js index 3a7e20c14f..d50fe4e424 100644 --- a/genai/test/textgen-with-txt-img.test.js +++ b/genai/text-generation/test/textgen-with-txt-img.test.js @@ -18,8 +18,8 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../text-generation/textgen-with-txt-img.js'); -const {delay} = require('./util'); +const sample = require('../textgen-with-txt-img.js'); +const {delay} = require('../../test/util'); describe('textgen-with-txt-img', async () => { it('should generate text content from a text prompt and an image', async function () { diff --git a/genai/test/textgen-with-txt-routing.test.js b/genai/text-generation/test/textgen-with-txt-routing.test.js similarity index 78% rename from genai/test/textgen-with-txt-routing.test.js rename to genai/text-generation/test/textgen-with-txt-routing.test.js index 28460ce373..19143796f2 100644 --- a/genai/test/textgen-with-txt-routing.test.js +++ b/genai/text-generation/test/textgen-with-txt-routing.test.js @@ -18,11 +18,15 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../text-generation/textgen-with-txt-routing.js'); +const sample = require('../textgen-with-txt-routing.js'); +const {delay} = require('../../test/util'); describe('textgen-with-txt-routing', async () => { - it('should generate text content from a text prompt and with routing configuration', async () => { + it('should generate text content from a text prompt and with routing configuration', async function () { + this.timeout(180000); + this.retries(2); + await delay(this.test); const output = await sample.generateContent(projectId); - assert(output.length > 0 && output.includes('AI')); + assert(output.length > 0); }); }); diff --git a/genai/test/textgen-with-txt-stream.test.js b/genai/text-generation/test/textgen-with-txt-stream.test.js similarity index 92% rename from genai/test/textgen-with-txt-stream.test.js rename to genai/text-generation/test/textgen-with-txt-stream.test.js index 5bfb88cade..127bdbced6 100644 --- a/genai/test/textgen-with-txt-stream.test.js +++ b/genai/text-generation/test/textgen-with-txt-stream.test.js @@ -18,7 +18,7 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../text-generation/textgen-with-txt-stream.js'); +const sample = require('../textgen-with-txt-stream.js'); describe('textgen-with-txt-stream', async () => { it('should generate streaming text content from a text prompt', async () => { diff --git a/genai/test/textgen-with-txt.test.js b/genai/text-generation/test/textgen-with-txt.test.js similarity index 93% rename from genai/test/textgen-with-txt.test.js rename to genai/text-generation/test/textgen-with-txt.test.js index a6f772a3a4..dabfd416f3 100644 --- a/genai/test/textgen-with-txt.test.js +++ b/genai/text-generation/test/textgen-with-txt.test.js @@ -18,7 +18,7 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../text-generation/textgen-with-txt.js'); +const sample = require('../textgen-with-txt.js'); describe('textgen-with-txt', async () => { it('should generate text content from a text prompt', async () => { diff --git a/genai/test/textgen-with-video.test.js b/genai/text-generation/test/textgen-with-video.test.js similarity index 93% rename from genai/test/textgen-with-video.test.js rename to genai/text-generation/test/textgen-with-video.test.js index ba81b8cd4a..8fb5f4cd96 100644 --- a/genai/test/textgen-with-video.test.js +++ b/genai/text-generation/test/textgen-with-video.test.js @@ -18,7 +18,7 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../text-generation/textgen-with-video.js'); +const sample = require('../textgen-with-video.js'); describe('textgen-with-video', async () => { it('should generate text content from a text prompt and a video', async () => { diff --git a/genai/test/textgen-with-youtube-video.test.js b/genai/text-generation/test/textgen-with-youtube-video.test.js similarity index 93% rename from genai/test/textgen-with-youtube-video.test.js rename to genai/text-generation/test/textgen-with-youtube-video.test.js index 4da0174f9a..948d4f974d 100644 --- a/genai/test/textgen-with-youtube-video.test.js +++ b/genai/text-generation/test/textgen-with-youtube-video.test.js @@ -18,7 +18,7 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../text-generation/textgen-with-youtube-video'); +const sample = require('../textgen-with-youtube-video'); describe('textgen-with-youtube-video', async () => { it('should generate text content from yt video', async function () { diff --git a/genai/text-generation/textgen-with-local-video.js b/genai/text-generation/textgen-with-local-video.js index d405bbbaef..d44eb9a190 100644 --- a/genai/text-generation/textgen-with-local-video.js +++ b/genai/text-generation/textgen-with-local-video.js @@ -34,9 +34,8 @@ async function generateText( const videoContent = fs.readFileSync('test-data/describe_video_content.mp4'); const response = await client.models.generateContent({ - model: 'gemini-2.5-flash', + model: 'gemini-2.5-flash-lite', contents: [ - {text: 'hello-world'}, { inlineData: { data: videoContent.toString('base64'), @@ -45,6 +44,9 @@ async function generateText( }, {text: 'Write a short and engaging blog post based on this video.'}, ], + config: { + mediaResolution: 'MEDIA_RESOLUTION_LOW', + }, }); console.log(response.text); diff --git a/genai/text-generation/textgen-with-mute-video.js b/genai/text-generation/textgen-with-mute-video.js index f2a7805064..b02daab172 100644 --- a/genai/text-generation/textgen-with-mute-video.js +++ b/genai/text-generation/textgen-with-mute-video.js @@ -31,7 +31,7 @@ async function generateText( }); const response = await client.models.generateContent({ - model: 'gemini-2.5-flash', + model: 'gemini-2.5-flash-lite', contents: [ { role: 'user', @@ -49,6 +49,9 @@ async function generateText( ], }, ], + config: { + mediaResolution: 'MEDIA_RESOLUTION_LOW', + }, }); console.log(response.text); diff --git a/genai/text-generation/textgen-with-txt-routing.js b/genai/text-generation/textgen-with-txt-routing.js index c3976d702e..41763abdea 100644 --- a/genai/text-generation/textgen-with-txt-routing.js +++ b/genai/text-generation/textgen-with-txt-routing.js @@ -38,7 +38,7 @@ async function generateContent( }; const response = await client.models.generateContent({ - model: 'model-optimizer-exp-04-09', + model: 'gemini-2.5-flash', contents: 'How does AI work?', config: generateContentConfig, }); diff --git a/genai/text-generation/textgen-with-txt.js b/genai/text-generation/textgen-with-txt.js index ba3294ac75..a81767e664 100644 --- a/genai/text-generation/textgen-with-txt.js +++ b/genai/text-generation/textgen-with-txt.js @@ -31,7 +31,7 @@ async function generateContent( }); const response = await client.models.generateContent({ - model: 'gemini-2.5-flash', + model: 'gemini-3-flash-preview', contents: 'How does AI work?', }); diff --git a/genai/text-generation/textgen-with-video.js b/genai/text-generation/textgen-with-video.js index 76b552facb..6ef043b598 100644 --- a/genai/text-generation/textgen-with-video.js +++ b/genai/text-generation/textgen-with-video.js @@ -46,6 +46,9 @@ async function generateContent( const response = await client.models.generateContent({ model: 'gemini-2.5-flash', contents: [video, prompt], + config: { + mediaResolution: 'MEDIA_RESOLUTION_LOW', + }, }); console.log(response.text); diff --git a/genai/text-generation/textgen-with-youtube-video.js b/genai/text-generation/textgen-with-youtube-video.js index f037417b30..b6eb192c6a 100644 --- a/genai/text-generation/textgen-with-youtube-video.js +++ b/genai/text-generation/textgen-with-youtube-video.js @@ -40,8 +40,11 @@ async function generateText( }; const response = await client.models.generateContent({ - model: 'gemini-2.5-flash', + model: 'gemini-2.5-flash-lite', contents: [ytVideo, prompt], + config: { + mediaResolution: 'MEDIA_RESOLUTION_LOW', + }, }); console.log(response.text); diff --git a/genai/thinking/package.json b/genai/thinking/package.json new file mode 100644 index 0000000000..eb92893f30 --- /dev/null +++ b/genai/thinking/package.json @@ -0,0 +1,20 @@ +{ + "name": "nodejs-genai-thinking", + "private": true, + "license": "Apache-2.0", + "author": "Google LLC", + "engines": { + "node": ">=16.0.0" + }, + "scripts": { + "test": "c8 mocha -p -j 2 --timeout 2400000 test/*.test.js" + }, + "dependencies": { + "@google/genai": "1.30.0" + }, + "devDependencies": { + "c8": "^10.0.0", + "chai": "^4.5.0", + "mocha": "^10.0.0" + } +} diff --git a/genai/test/thinking-budget-with-txt.test.js b/genai/thinking/test/thinking-budget-with-txt.test.js similarity index 93% rename from genai/test/thinking-budget-with-txt.test.js rename to genai/thinking/test/thinking-budget-with-txt.test.js index 5daaa2997f..58993ac023 100644 --- a/genai/test/thinking-budget-with-txt.test.js +++ b/genai/thinking/test/thinking-budget-with-txt.test.js @@ -18,7 +18,7 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../thinking/thinking-budget-with-txt.js'); +const sample = require('../thinking-budget-with-txt.js'); describe('thinking-budget-with-txt', () => { it('should return Thought Process', async function () { diff --git a/genai/test/thinking-includethoughts-with-txt.test.js b/genai/thinking/test/thinking-includethoughts-with-txt.test.js similarity index 92% rename from genai/test/thinking-includethoughts-with-txt.test.js rename to genai/thinking/test/thinking-includethoughts-with-txt.test.js index 69a8a32800..46b50baf58 100644 --- a/genai/test/thinking-includethoughts-with-txt.test.js +++ b/genai/thinking/test/thinking-includethoughts-with-txt.test.js @@ -18,7 +18,7 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../thinking/thinking-includethoughts-with-txt.js'); +const sample = require('../thinking-includethoughts-with-txt.js'); describe('thinking-includethoughts-with-txt', () => { it('should return Thought Process', async function () { diff --git a/genai/test/thinking-with-txt.test.js b/genai/thinking/test/thinking-with-txt.test.js similarity index 86% rename from genai/test/thinking-with-txt.test.js rename to genai/thinking/test/thinking-with-txt.test.js index 0ed201f75a..687f694b42 100644 --- a/genai/test/thinking-with-txt.test.js +++ b/genai/thinking/test/thinking-with-txt.test.js @@ -18,11 +18,14 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../thinking/thinking-with-txt.js'); +const sample = require('../thinking-with-txt.js'); +const {delay} = require('../../test/util'); describe('thinking-with-txt', () => { it('should return Thought Process', async function () { this.timeout(50000); + this.retries(4); + await delay(this.test); const output = await sample.generateWithThoughts(projectId); assert(output.length > 0); }); diff --git a/genai/tools/package.json b/genai/tools/package.json new file mode 100644 index 0000000000..4e5e1aed4e --- /dev/null +++ b/genai/tools/package.json @@ -0,0 +1,20 @@ +{ + "name": "nodejs-genai-tools", + "private": true, + "license": "Apache-2.0", + "author": "Google LLC", + "engines": { + "node": ">=16.0.0" + }, + "scripts": { + "test": "c8 mocha -p -j 2 --timeout 2400000 test/*.test.js" + }, + "dependencies": { + "@google/genai": "1.30.0" + }, + "devDependencies": { + "c8": "^10.0.0", + "chai": "^4.5.0", + "mocha": "^10.0.0" + } +} diff --git a/genai/tools/test-data/640px-Monty_open_door.svg.png b/genai/tools/test-data/640px-Monty_open_door.svg.png new file mode 100644 index 0000000000..90f83375e3 Binary files /dev/null and b/genai/tools/test-data/640px-Monty_open_door.svg.png differ diff --git a/genai/tools/test/tools-code-exec-with-txt-local-img.test.js b/genai/tools/test/tools-code-exec-with-txt-local-img.test.js new file mode 100644 index 0000000000..7581da4899 --- /dev/null +++ b/genai/tools/test/tools-code-exec-with-txt-local-img.test.js @@ -0,0 +1,29 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const sample = require('../tools-code-exec-with-txt-local-img.js'); + +describe('tools-code-exec-with-txt-local-img', () => { + it('should generate a function definition', async function () { + this.timeout(100000); + const output = await sample.generateAndExecuteMultimodalCode(projectId); + assert(output.length > 0); + }); +}); diff --git a/genai/test/tools-code-exec-with-txt.test.js b/genai/tools/test/tools-code-exec-with-txt.test.js similarity index 85% rename from genai/test/tools-code-exec-with-txt.test.js rename to genai/tools/test/tools-code-exec-with-txt.test.js index 8ce7402e4d..4d88eb2cb1 100644 --- a/genai/test/tools-code-exec-with-txt.test.js +++ b/genai/tools/test/tools-code-exec-with-txt.test.js @@ -18,15 +18,15 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../tools/tools-code-exec-with-txt.js'); -const {delay} = require('./util'); +const sample = require('../tools-code-exec-with-txt.js'); +const {delay} = require('../../test/util'); describe('tools-code-exec-with-txt', async () => { it('should generate code and execution result', async function () { this.timeout(180000); this.retries(4); await delay(this.test); - const output = await sample.generateContent(projectId); + const output = await sample.generateAndExecuteCode(projectId); assert(output.length > 0); }); }); diff --git a/genai/test/tools-func-desc-with-txt.test.js b/genai/tools/test/tools-func-desc-with-txt.test.js similarity index 85% rename from genai/test/tools-func-desc-with-txt.test.js rename to genai/tools/test/tools-func-desc-with-txt.test.js index 1ff31c89a7..206a3dcf73 100644 --- a/genai/test/tools-func-desc-with-txt.test.js +++ b/genai/tools/test/tools-func-desc-with-txt.test.js @@ -17,14 +17,14 @@ const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../tools/tools-func-desc-with-txt.js'); -const {delay} = require('./util'); +const sample = require('../tools-func-desc-with-txt.js'); +const {delay} = require('../../test/util'); describe('tools-func-desc-with-txt', async () => { it('should generate a function call', async function () { this.timeout(180000); this.retries(4); await delay(this.test); - await sample.generateContent(projectId); + await sample.generateFunctionDesc(projectId); }); }); diff --git a/genai/tools/test/tools-google-maps-coordinates-with-txt.test.js b/genai/tools/test/tools-google-maps-coordinates-with-txt.test.js new file mode 100644 index 0000000000..0cd9e406f9 --- /dev/null +++ b/genai/tools/test/tools-google-maps-coordinates-with-txt.test.js @@ -0,0 +1,32 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const sample = require('../tools-google-maps-coordinates-with-txt'); +const {delay} = require('../../test/util'); +const {assert} = require('chai'); + +describe('tools-google-maps-coordinates-with-txt', () => { + it('should use google maps coordinates', async function () { + this.timeout(180000); + this.retries(4); + await delay(this.test); + const output = await sample.generateContent(projectId); + assert(output.length > 0); + }); +}); diff --git a/genai/tools/test/tools-google-search-and-urlcontext-with-txt.test.js b/genai/tools/test/tools-google-search-and-urlcontext-with-txt.test.js new file mode 100644 index 0000000000..b5b875975c --- /dev/null +++ b/genai/tools/test/tools-google-search-and-urlcontext-with-txt.test.js @@ -0,0 +1,32 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const sample = require('../tools-google-search-and-urlcontext-with-txt'); +const {delay} = require('../../test/util'); +const {assert} = require('chai'); + +describe('tools-google-search-and-urlcontext-with-txt', () => { + it('should create urlcontext and google search', async function () { + this.timeout(180000); + this.retries(4); + await delay(this.test); + const output = await sample.generateContent(projectId); + assert(output.length > 0); + }); +}); diff --git a/genai/tools/test/tools-google-search-with-txt.test.js b/genai/tools/test/tools-google-search-with-txt.test.js new file mode 100644 index 0000000000..d92e303bed --- /dev/null +++ b/genai/tools/test/tools-google-search-with-txt.test.js @@ -0,0 +1,29 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const sample = require('../tools-google-search-with-txt.js'); + +describe('tools-google-search-with-txt', () => { + it('should generate answer to a question in prompt using google search', async function () { + this.timeout(100000); + const output = await sample.generateGoogleSearch(projectId); + assert(output.length > 0); + }); +}); diff --git a/genai/tools/test/tools-urlcontext-with-txt.test.js b/genai/tools/test/tools-urlcontext-with-txt.test.js new file mode 100644 index 0000000000..a77a4fc909 --- /dev/null +++ b/genai/tools/test/tools-urlcontext-with-txt.test.js @@ -0,0 +1,32 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {describe, it} = require('mocha'); + +const projectId = process.env.CAIP_PROJECT_ID; +const sample = require('../tools-urlcontext-with-txt'); +const {delay} = require('../../test/util'); +const {assert} = require('chai'); + +describe('tools-urlcontext-with-txt', () => { + it('should create urlcontext with txt', async function () { + this.timeout(180000); + this.retries(4); + await delay(this.test); + const output = await sample.generateContent(projectId); + assert(output.length > 0); + }); +}); diff --git a/genai/test/tools-vais-with-txt.test.js b/genai/tools/test/tools-vais-with-txt.test.js similarity index 95% rename from genai/test/tools-vais-with-txt.test.js rename to genai/tools/test/tools-vais-with-txt.test.js index 9cd58c4b2a..281c5775b9 100644 --- a/genai/test/tools-vais-with-txt.test.js +++ b/genai/tools/test/tools-vais-with-txt.test.js @@ -20,7 +20,7 @@ // const {describe, it} = require('mocha'); // // const projectId = process.env.CAIP_PROJECT_ID; -// const sample = require('../tools/tools-vais-with-txt.js'); +// const sample = require('../tools-vais-with-txt.js'); // const location = process.env.GOOGLE_CLOUD_LOCATION || 'global'; // const datastore = `projects/${projectId}/locations/global/collections/default_collection/dataStores/grounding-test-datastore`; diff --git a/genai/tools/tools-code-exec-with-txt-local-img.js b/genai/tools/tools-code-exec-with-txt-local-img.js new file mode 100644 index 0000000000..6c54922a49 --- /dev/null +++ b/genai/tools/tools-code-exec-with-txt-local-img.js @@ -0,0 +1,95 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +// [START googlegenaisdk_tools_exec_with_txt_local_img] +const fs = require('fs').promises; +const path = require('path'); + +const {GoogleGenAI} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = process.env.GOOGLE_CLOUD_LOCATION || 'global'; + +async function generateAndExecuteMultimodalCode( + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + }); + + const imagePath = path.join( + __dirname, + 'test-data/640px-Monty_open_door.svg.png' + ); + const imageBuffer = await fs.readFile(imagePath); + const imageBase64 = imageBuffer.toString('base64'); + + const prompt = ` + Run a simulation of the Monty Hall Problem with 1,000 trials. + Here's how this works as a reminder. In the Monty Hall Problem, you're on a game + show with three doors. Behind one is a car, and behind the others are goats. You + pick a door. The host, who knows what's behind the doors, opens a different door + to reveal a goat. Should you switch to the remaining unopened door? + The answer has always been a little difficult for me to understand when people + solve it with math - so please run a simulation with Python to show me what the + best strategy is. + Thank you! + `; + + const contents = [ + { + role: 'user', + parts: [ + { + inlineData: { + mimeType: 'image/png', + data: imageBase64, + }, + }, + { + text: prompt, + }, + ], + }, + ]; + + const response = await client.models.generateContent({ + model: 'gemini-2.5-flash', + contents: contents, + config: { + tools: [{codeExecution: {}}], + temperature: 0, + }, + }); + + console.debug(response.executableCode); + console.debug(response.codeExecutionResult); + + // Example response: + // Win percentage when switching: 65.50% + // Win percentage when not switching: 34.50% + + return response.codeExecutionResult; +} + +// [END googlegenaisdk_tools_exec_with_txt_local_img] + +module.exports = { + generateAndExecuteMultimodalCode, +}; diff --git a/genai/tools/tools-code-exec-with-txt.js b/genai/tools/tools-code-exec-with-txt.js index 6aaeb0ec81..8a6a137941 100644 --- a/genai/tools/tools-code-exec-with-txt.js +++ b/genai/tools/tools-code-exec-with-txt.js @@ -20,7 +20,7 @@ const {GoogleGenAI} = require('@google/genai'); const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; const GOOGLE_CLOUD_LOCATION = process.env.GOOGLE_CLOUD_LOCATION || 'global'; -async function generateContent( +async function generateAndExecuteCode( projectId = GOOGLE_CLOUD_PROJECT, location = GOOGLE_CLOUD_LOCATION ) { @@ -33,7 +33,7 @@ async function generateContent( const response = await client.models.generateContent({ model: 'gemini-2.5-flash', contents: - 'What is the sum of the first 50 prime numbers? Generate and run code for the calculation, and make sure you get all 50.', + 'Calculate 20th fibonacci number. Then find the nearest palindrome to it.', config: { tools: [{codeExecution: {}}], temperature: 0, @@ -41,12 +41,36 @@ async function generateContent( }); console.debug(response.executableCode); + + // Example response: + // Code: + // function fibonacci(n) { + // if (n <= 0) { + // return 0; + // } else if (n === 1) { + // return 1; + // } else { + // let a = 0, b = 1; + // for (let i = 2; i <= n; i++) { + // [a, b] = [b, a + b]; + // } + // return b; + // } + // } + // + // const fib20 = fibonacci(20); + // console.log(`fib20=${fib20}`); + console.debug(response.codeExecutionResult); + // Outcome: + // fib20=6765 + return response.codeExecutionResult; } + // [END googlegenaisdk_tools_code_exec_with_txt] module.exports = { - generateContent, + generateAndExecuteCode, }; diff --git a/genai/tools/tools-func-desc-with-txt.js b/genai/tools/tools-func-desc-with-txt.js index b3f38f1c86..f9b3c55036 100644 --- a/genai/tools/tools-func-desc-with-txt.js +++ b/genai/tools/tools-func-desc-with-txt.js @@ -19,8 +19,7 @@ const {GoogleGenAI, Type} = require('@google/genai'); const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; const GOOGLE_CLOUD_LOCATION = process.env.GOOGLE_CLOUD_LOCATION || 'global'; - -async function generateContent( +async function generateFunctionDesc( projectId = GOOGLE_CLOUD_PROJECT, location = GOOGLE_CLOUD_LOCATION ) { @@ -71,23 +70,36 @@ async function generateContent( trends in music consumption. `; - const MODEL_NAME = 'gemini-2.5-flash'; - const response = await client.models.generateContent({ - model: MODEL_NAME, + model: 'gemini-2.5-flash', contents: prompt, config: { tools: [sales_tool], temperature: 0, }, }); + const output = JSON.stringify(response.functionCalls, null, 2); + console.log(output); - console.log(response.functionCalls); + // Example response: + // [FunctionCall( + // id=None, + // name="get_album_sales", + // args={ + // "albums": [ + // {"album_name": "Echoes of the Night", "copies_sold": 350000}, + // {"copies_sold": 120000, "album_name": "Reckless Hearts"}, + // {"copies_sold": 75000, "album_name": "Whispers of Dawn"}, + // {"copies_sold": 100000, "album_name": "Street Symphony"}, + // ] + // }, + // )] - return response.functionCalls; + return output; } + // [END googlegenaisdk_tools_func_desc_with_txt] module.exports = { - generateContent, + generateFunctionDesc, }; diff --git a/genai/tools/tools-google-maps-coordinates-with-txt.js b/genai/tools/tools-google-maps-coordinates-with-txt.js new file mode 100644 index 0000000000..f9b51c342e --- /dev/null +++ b/genai/tools/tools-google-maps-coordinates-with-txt.js @@ -0,0 +1,64 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +// [START googlegenaisdk_tools_google_maps_coordinates_with_txt] +const {GoogleGenAI} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = process.env.GOOGLE_CLOUD_LOCATION || 'global'; + +async function generateContent( + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + }); + + const response = await client.models.generateContent({ + model: 'gemini-2.5-flash', + contents: 'Where can I get the best espresso near me?', + config: { + tools: [ + { + googleMaps: {}, + }, + ], + toolConfig: { + retrievalConfig: { + latLng: { + latitude: 40.7128, + longitude: -74.006, + }, + languageCode: 'en_US', + }, + }, + }, + }); + + console.log(response.text); + // Example response: + // 'Here are some of the top-rated places to get espresso near you: ...' + + return response.text; +} +// [END googlegenaisdk_tools_google_maps_coordinates_with_txt] + +module.exports = { + generateContent, +}; diff --git a/genai/tools/tools-google-search-and-urlcontext-with-txt.js b/genai/tools/tools-google-search-and-urlcontext-with-txt.js new file mode 100644 index 0000000000..3db4923acb --- /dev/null +++ b/genai/tools/tools-google-search-and-urlcontext-with-txt.js @@ -0,0 +1,106 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +// [START googlegenaisdk_tools_google_search_and_urlcontext_with_txt] +const {GoogleGenAI} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = process.env.GOOGLE_CLOUD_LOCATION || 'global'; + +async function generateContent( + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + httpOptions: {apiVersion: 'v1beta1'}, + }); + + // TODO(developer): Here put your URLs! + const url = 'https://www.google.com/search?q=events+in+New+York'; + + const response = await client.models.generateContent({ + model: 'gemini-2.5-flash', + contents: `Give me a three-day events schedule based on ${url}. Also let me know what to take care of considering weather and commute.`, + config: { + tools: [{urlContext: {}}, {googleSearch: {}}], + responseModalities: ['TEXT'], + }, + }); + + const output = []; + + { + for (const part of response.candidates[0].content.parts) { + console.log(part.text); + output.push(part.text); + } + } + + // Here is a possible three-day event schedule for New York City, focusing on the dates around October 7-9, 2025, along with weather and commute considerations. + // + // ### Three-Day Event Schedule: New York City (October 7-9, 2025) + // + // **Day 1: Tuesday, October 7, 2025 - Art and Culture** + // + // * **Morning (10:00 AM - 1:00 PM):** Visit "Phillips Visual Language: The Art of Irving Penn" at 432 Park Avenue. This exhibition is scheduled to end on this day, offering a last chance to see it. + // * **Lunch (1:00 PM - 2:00 PM):** Grab a quick lunch near Park Avenue. + // * **Afternoon (2:30 PM - 5:30 PM):** Explore the "Lincoln Center Festival of Firsts" at Lincoln Center. This festival runs until October 23rd, offering various performances or exhibits. Check their specific schedule for the day. + // * **Evening (7:00 PM onwards):** Experience a classic Broadway show. Popular options mentioned for October 2025 include "Six The Musical," "Wicked," "Hadestown," or "MJ - The Musical." + // + // **Day 2: Wednesday, October 8, 2025 - Unique Experiences and SoHo Vibes** + // + // * **Morning (11:00 AM - 1:00 PM):** Head to Brooklyn for the "Secret Room at IKEA Brooklyn" at 1 Beard Street. This unique event is scheduled to end on October 9th. + // * **Lunch (1:00 PM - 2:00 PM):** Enjoy lunch in Brooklyn, perhaps exploring local eateries in the area. + // * **Afternoon (2:30 PM - 5:30 PM):** Immerse yourself in the "The Weeknd & Nespresso Samra Origins Vinyl Cafe" at 579 Broadway in SoHo. This pop-up, curated by The Weeknd, combines coffee and music and runs until October 14th. + // * **Evening (6:00 PM onwards):** Explore the vibrant SoHo neighborhood, known for its shopping and dining. You could also consider a dinner cruise to see the illuminated Manhattan skyline and the Statue of Liberty. + // + // **Day 3: Thursday, October 9, 2025 - Film and Scenic Views** + // + // * **Morning (10:00 AM - 1:00 PM):** Attend a screening at the New York Greek Film Expo, which runs until October 12th in New York City. + // * **Lunch (1:00 PM - 2:00 PM):** Have lunch near the film expo's location. + // * **Afternoon (2:30 PM - 5:30 PM):** Take advantage of the pleasant October weather and enjoy outdoor activities. Consider biking along the rivers or through Central Park to admire the early autumn foliage. + // * **Evening (6:00 PM onwards):** Visit an observation deck like the Empire State Building or Top of the Rock for panoramic city views. Afterwards, enjoy dinner in a neighborhood of your choice. + // + // ### Weather and Commute Considerations: + // + // **Weather in Early October:** + // + // * **Temperatures:** Expect mild to cool temperatures. Average daily temperatures in early October range from 10°C (50°F) to 18°C (64°F), with occasional warmer days reaching the mid-20s°C (mid-70s°F). Evenings can be quite chilly. + // * **Rainfall:** October has a higher chance of rainfall compared to other months, with an average of 33mm and a 32% chance of rain on any given day. + // * **Sunshine:** You can generally expect about 7 hours of sunshine per day. + // * **What to Pack:** Pack layers! Bring a light jacket or sweater for the daytime, and a warmer coat for the evenings. An umbrella or a light raincoat is highly recommended due to the chance of showers. Comfortable walking shoes are a must for exploring the city. + // + // **Commute in New York City:** + // + // * **Public Transportation is Key:** The subway is generally the fastest and most efficient way to get around New York City, especially during the day. Buses are good for East-West travel, but can be slower due to traffic. + // * **Using Apps:** Utilize Google Maps or official MTA apps to plan your routes and check for real-time service updates. The subway runs 24/7, but expect potential delays or changes to routes during nights and weekends due to maintenance. + // * **Rush Hour:** Avoid subway and commuter train travel during peak rush hours (8 AM - 10 AM and 5 PM - 7 PM) if possible, as trains can be extremely crowded. + // * **Subway Etiquette:** When on the subway, stand to the side of the doors to let people exit before boarding, and move to the center of the car to make space. Hold onto a pole or seat, and remove your backpack to free up space. + // * **Transfers:** Subway fare is $2.90 per ride, and you get one free transfer between the subway and bus within a two-hour window. + // * **Walking:** New York City is very walkable. If the weather is pleasant, walking between nearby attractions is an excellent way to see the city. + // * **Taxis/Ride-sharing:** Uber, Lyft, and Curb (for NYC taxis) are available, but driving in the city is generally discouraged due to traffic and parking difficulties. + // * **Allow Extra Time:** Always factor in an additional 20-30 minutes for travel time, as delays can occur. + + return output; +} +// [END googlegenaisdk_tools_google_search_and_urlcontext_with_txt] + +module.exports = { + generateContent, +}; diff --git a/genai/tools/tools-google-search-with-txt.js b/genai/tools/tools-google-search-with-txt.js new file mode 100644 index 0000000000..2b123944cd --- /dev/null +++ b/genai/tools/tools-google-search-with-txt.js @@ -0,0 +1,57 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +// [START googlegenaisdk_tools_google_search_with_txt] +const {GoogleGenAI} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = process.env.GOOGLE_CLOUD_LOCATION || 'global'; + +async function generateGoogleSearch( + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + }); + + const response = await client.models.generateContent({ + model: 'gemini-2.5-flash', + contents: 'When is the next total solar eclipse in the United States?', + config: { + tools: [ + { + googleSearch: {}, + }, + ], + }, + }); + + console.log(response.text); + + // Example response: + // 'The next total solar eclipse in United States will occur on ...' + + return response.text; +} + +// [END googlegenaisdk_tools_google_search_with_txt] + +module.exports = { + generateGoogleSearch, +}; diff --git a/genai/tools/tools-urlcontext-with-txt.js b/genai/tools/tools-urlcontext-with-txt.js new file mode 100644 index 0000000000..1c4788aff3 --- /dev/null +++ b/genai/tools/tools-urlcontext-with-txt.js @@ -0,0 +1,90 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +// [START googlegenaisdk_tools_urlcontext_with_txt] +const {GoogleGenAI} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = process.env.GOOGLE_CLOUD_LOCATION || 'global'; + +async function generateContent( + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + httpOptions: {apiVersion: 'v1beta1'}, + }); + + // TODO(developer): Here put your URLs! + const url1 = 'https://cloud.google.com/vertex-ai/generative-ai/docs'; + const url2 = 'https://cloud.google.com/docs/overview'; + + const response = await client.models.generateContent({ + model: 'gemini-2.5-flash', + contents: `Compare the content, purpose, and audiences of ${url1} and ${url2}.`, + config: { + tools: [{urlContext: {}}], + responseModalities: ['TEXT'], + }, + }); + + const output = []; + + { + for (const part of response.candidates[0].content.parts) { + console.log(part.text); + output.push(part.text); + } + } + + // Gemini 2.5 Pro and Gemini 2.5 Flash are both advanced models offered by Google AI, but they are optimized for different use cases. + // + // Here's a comparison: + // + // **Gemini 2.5 Pro** + // * **Description**: This is Google's most advanced model, described as a "state-of-the-art thinking model". It excels at reasoning over complex problems in areas like code, mathematics, and STEM, and can analyze large datasets, codebases, and documents using a long context window. + // * **Input Data Types**: It supports audio, images, video, text, and PDF inputs. + // * **Output Data Types**: It produces text outputs. + // * **Token Limits**: It has an input token limit of 1,048,576 and an output token limit of 65,536. + // * **Supported Capabilities**: Gemini 2.5 Pro supports Batch API, Caching, Code execution, Function calling, Search grounding, Structured outputs, Thinking, and URL context. + // * **Knowledge Cutoff**: January 2025. + // + // **Gemini 2.5 Flash** + // * **Description**: Positioned as "fast and intelligent," Gemini 2.5 Flash is highlighted as Google's best model in terms of price-performance, offering well-rounded capabilities. It is ideal for large-scale processing, low-latency, high-volume tasks that require thinking, and agentic use cases. + // * **Input Data Types**: It supports text, images, video, and audio inputs. + // * **Output Data Types**: It produces text outputs. + // * **Token Limits**: Similar to Pro, it has an input token limit of 1,048,576 and an output token limit of 65,536. + // * **Supported Capabilities**: Gemini 2.5 Flash supports Batch API, Caching, Code execution, Function calling, Search grounding, Structured outputs, Thinking, and URL con// + // **Key Differences and Similarities:** + // + // * **Primary Focus**: Gemini 2.5 Pro is geared towards advanced reasoning and in-depth analysis of complex problems and large documents. Gemini 2.5 Flash, on the other hand, is optimized for efficiency, scale, and high-volume, low-latency applications, making it a strong choice for price-performance sensitive scenarios. + // * **Input Modalities**: Both models handle various input types including text, images, video, and audio. Gemini 2.5 Pro explicitly lists PDF as an input type, while Gemini 2.5 Flash lists text, images, video, audio. + // * **Technical Specifications (for primary stable versions)**: Both models share the same substantial input and output token limits (1,048,576 input and 65,536 output). They also support a very similar set of core capabilities, including code execution, function calling, and URL context. Neither model supports audio generation, image generation, or Live API in their standard stable versions. + // * **Knowledge Cutoff**: Both models have a knowledge cutoff of January 2025. + // + // In essence, while both models are powerful and capable, Gemini 2.5 Pro is designed for maximum performance in complex reasoning tasks, whereas Gemini 2.5 Flash prioritizes cost-effectiveness and speed for broader, high-throughput applications. + // get URLs retrieved for context + + return output; +} +// [END googlegenaisdk_tools_urlcontext_with_txt] + +module.exports = { + generateContent, +}; diff --git a/genai/tools/tools-vais-with-txt.js b/genai/tools/tools-vais-with-txt.js index 67f8d3f12f..58c31d021f 100644 --- a/genai/tools/tools-vais-with-txt.js +++ b/genai/tools/tools-vais-with-txt.js @@ -20,8 +20,7 @@ const {GoogleGenAI} = require('@google/genai'); const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; const GOOGLE_CLOUD_LOCATION = process.env.GOOGLE_CLOUD_LOCATION || 'global'; // (Developer) put your path Data Store -const DATASTORE = - 'projects/cloud-ai-devrel-softserve/locations/global/collections/default_collection/dataStores/example-adk-website-datastore_1755611010401'; +const DATASTORE = `projects/${GOOGLE_CLOUD_PROJECT}/locations/${GOOGLE_CLOUD_LOCATION}/collections/default_collection/dataStores/data-store-id `; async function generateContent( datastore = DATASTORE, diff --git a/genai/tuning/package.json b/genai/tuning/package.json new file mode 100644 index 0000000000..44ccf7e813 --- /dev/null +++ b/genai/tuning/package.json @@ -0,0 +1,21 @@ +{ + "name": "nodejs-genai-tuning", + "private": true, + "license": "Apache-2.0", + "author": "Google LLC", + "engines": { + "node": ">=16.0.0" + }, + "scripts": { + "test": "c8 mocha -p -j 2 --timeout 2400000 test/*.test.js" + }, + "dependencies": { + "@google/genai": "1.30.0" + }, + "devDependencies": { + "c8": "^10.0.0", + "chai": "^4.5.0", + "mocha": "^10.0.0", + "proxyquire": "^2.1.3" + } +} diff --git a/genai/test/tuning-job-create.test.js b/genai/tuning/test/tuning-job-create.test.js similarity index 95% rename from genai/test/tuning-job-create.test.js rename to genai/tuning/test/tuning-job-create.test.js index 3785c8b977..bf3bb8eda7 100644 --- a/genai/test/tuning-job-create.test.js +++ b/genai/tuning/test/tuning-job-create.test.js @@ -45,7 +45,7 @@ describe('tuning-job-create', () => { } } - const sample = proxyquire('../tuning/tuning-job-create.js', { + const sample = proxyquire('../tuning-job-create.js', { '@google/genai': {GoogleGenAI: MockGoogleGenAI}, }); diff --git a/genai/test/tuning-job-get.test.js b/genai/tuning/test/tuning-job-get.test.js similarity index 96% rename from genai/test/tuning-job-get.test.js rename to genai/tuning/test/tuning-job-get.test.js index 06b136f66d..810ee81e62 100644 --- a/genai/test/tuning-job-get.test.js +++ b/genai/tuning/test/tuning-job-get.test.js @@ -47,7 +47,7 @@ describe('tuning-job-get', () => { } } - const sample = proxyquire('../tuning/tuning-job-get.js', { + const sample = proxyquire('../tuning-job-get.js', { '@google/genai': {GoogleGenAI: MockGoogleGenAI}, }); diff --git a/genai/test/tuning-job-list.test.js b/genai/tuning/test/tuning-job-list.test.js similarity index 94% rename from genai/test/tuning-job-list.test.js rename to genai/tuning/test/tuning-job-list.test.js index 500e81d7e7..db41dcda23 100644 --- a/genai/test/tuning-job-list.test.js +++ b/genai/tuning/test/tuning-job-list.test.js @@ -18,7 +18,7 @@ const {assert} = require('chai'); const {describe, it} = require('mocha'); const projectId = process.env.CAIP_PROJECT_ID; -const sample = require('../tuning/tuning-job-list.js'); +const sample = require('../tuning-job-list.js'); describe('tuning-job-list', () => { it('should return tuning job list', async () => { diff --git a/genai/test/tuning-textgen-with-txt.test.js b/genai/tuning/test/tuning-textgen-with-txt.test.js similarity index 96% rename from genai/test/tuning-textgen-with-txt.test.js rename to genai/tuning/test/tuning-textgen-with-txt.test.js index 3cd9886553..2f0b2288a1 100644 --- a/genai/test/tuning-textgen-with-txt.test.js +++ b/genai/tuning/test/tuning-textgen-with-txt.test.js @@ -56,7 +56,7 @@ describe('tuning-textgen-with-txt', () => { } } - const sample = proxyquire('../tuning/tuning-textgen-with-txt.js', { + const sample = proxyquire('../tuning-textgen-with-txt.js', { '@google/genai': {GoogleGenAI: MockGoogleGenAI}, }); diff --git a/genai/video-generation/package.json b/genai/video-generation/package.json new file mode 100644 index 0000000000..7fa7f58d1c --- /dev/null +++ b/genai/video-generation/package.json @@ -0,0 +1,29 @@ +{ + "name": "nodejs-genai-video-generation-samples", + "version": "0.0.1", + "private": true, + "license": "Apache-2.0", + "author": "Google LLC", + "repository": { + "type": "git", + "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" + }, + "engines": { + "node": ">=16.0.0" + }, + "files": [ + "*.js" + ], + "scripts": { + "test": "c8 mocha -p -j 2 --timeout 2400000 test/*.test.js test/**/*.test.js" + }, + "dependencies": { + "@google-cloud/storage": "^7.17.3", + "@google/genai": "1.30.0" + }, + "devDependencies": { + "c8": "^10.0.0", + "chai": "^4.5.0", + "mocha": "^10.0.0" + } +} diff --git a/genai/video-generation/test/videogen-with-img.test.js b/genai/video-generation/test/videogen-with-img.test.js new file mode 100644 index 0000000000..0cf1c62efa --- /dev/null +++ b/genai/video-generation/test/videogen-with-img.test.js @@ -0,0 +1,59 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); +const {Storage} = require('@google-cloud/storage'); + +const location = process.env.GOOGLE_CLOUD_LOCATION || 'global'; +const projectId = process.env.CAIP_PROJECT_ID; +const sample = require('../videogen-with-img.js'); +const {delay} = require('../../test/util'); + +const storage = new Storage(); + +const GCS_OUTPUT_BUCKET = 'nodejs-docs-samples-tests'; + +async function gcs_output_uri() { + const dt = new Date(); + const prefix = `video_output/${dt.toISOString()}`; + const fullUri = `gs://${GCS_OUTPUT_BUCKET}/${prefix}`; + + return { + uri: fullUri, + async cleanup() { + const [files] = await storage.bucket(GCS_OUTPUT_BUCKET).getFiles({ + prefix, + }); + for (const file of files) { + await file.delete(); + } + }, + }; +} + +describe('videogen-with-img', async () => { + it('should generate video content from an image', async function () { + this.timeout(180000); + this.retries(4); + await delay(this.test); + const gscOutput = gcs_output_uri(); + const gscUri = (await gscOutput).uri; + const output = await sample.generateVideo(gscUri, projectId, location); + console.log('output', output); + assert(output); + }); +}); diff --git a/genai/video-generation/test/videogen-with-txt.test.js b/genai/video-generation/test/videogen-with-txt.test.js new file mode 100644 index 0000000000..ba4c18b718 --- /dev/null +++ b/genai/video-generation/test/videogen-with-txt.test.js @@ -0,0 +1,59 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +const {assert} = require('chai'); +const {describe, it} = require('mocha'); +const {Storage} = require('@google-cloud/storage'); + +const location = process.env.GOOGLE_CLOUD_LOCATION || 'global'; +const projectId = process.env.CAIP_PROJECT_ID; +const sample = require('../videogen-with-txt.js'); +const {delay} = require('../../test/util'); + +const storage = new Storage(); + +const GCS_OUTPUT_BUCKET = 'nodejs-docs-samples-tests'; + +async function gcs_output_uri() { + const dt = new Date(); + const prefix = `text_output/${dt.toISOString()}`; + const fullUri = `gs://${GCS_OUTPUT_BUCKET}/${prefix}`; + + return { + uri: fullUri, + async cleanup() { + const [files] = await storage.bucket(GCS_OUTPUT_BUCKET).getFiles({ + prefix, + }); + for (const file of files) { + await file.delete(); + } + }, + }; +} + +describe('videogen-with-txt', async () => { + it('should generate video content from a text prompt', async function () { + this.timeout(180000); + this.retries(4); + await delay(this.test); + const gscOutput = gcs_output_uri(); + const gscUri = (await gscOutput).uri; + const output = await sample.generateVideo(gscUri, projectId, location); + console.log('output', output); + assert(output); + }); +}); diff --git a/genai/video-generation/videogen-with-img.js b/genai/video-generation/videogen-with-img.js new file mode 100644 index 0000000000..27f4e48457 --- /dev/null +++ b/genai/video-generation/videogen-with-img.js @@ -0,0 +1,64 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +// [START googlegenaisdk_videogen_with_img] +const {GoogleGenAI} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = process.env.GOOGLE_CLOUD_LOCATION || 'global'; + +async function generateVideo( + outputGcsUri, + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + }); + + let operation = await client.models.generateVideos({ + model: 'veo-3.1-fast-generate-001', + prompt: + 'Extreme close-up of a cluster of vibrant wildflowers swaying gently in a sun-drenched meadow', + image: { + gcsUri: 'gs://cloud-samples-data/generative-ai/image/flowers.png', + mimeType: 'image/png', + }, + config: { + aspectRatio: '16:9', + outputGcsUri: outputGcsUri, + }, + }); + + while (!operation.done) { + await new Promise(resolve => setTimeout(resolve, 15000)); + operation = await client.operations.get({operation: operation}); + console.log(operation); + } + + if (operation.response) { + console.log(operation.response.generatedVideos[0].video.uri); + } + return operation; +} + +// [END googlegenaisdk_videogen_with_img] + +module.exports = { + generateVideo, +}; diff --git a/genai/video-generation/videogen-with-txt.js b/genai/video-generation/videogen-with-txt.js new file mode 100644 index 0000000000..26e4877308 --- /dev/null +++ b/genai/video-generation/videogen-with-txt.js @@ -0,0 +1,59 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +// [START googlegenaisdk_videogen_with_txt] +const {GoogleGenAI} = require('@google/genai'); + +const GOOGLE_CLOUD_PROJECT = process.env.GOOGLE_CLOUD_PROJECT; +const GOOGLE_CLOUD_LOCATION = process.env.GOOGLE_CLOUD_LOCATION || 'global'; + +async function generateVideo( + outputGcsUri, + projectId = GOOGLE_CLOUD_PROJECT, + location = GOOGLE_CLOUD_LOCATION +) { + const client = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: location, + }); + + let operation = await client.models.generateVideos({ + model: 'veo-3.1-fast-generate-001', + prompt: 'a cat reading a book', + config: { + aspectRatio: '16:9', + outputGcsUri: outputGcsUri, + }, + }); + + while (!operation.done) { + await new Promise(resolve => setTimeout(resolve, 15000)); + operation = await client.operations.get({operation: operation}); + console.log(operation); + } + + if (operation.response) { + console.log(operation.response.generatedVideos[0].video.uri); + } + return operation; +} + +// [END googlegenaisdk_videogen_with_txt] + +module.exports = { + generateVideo, +}; diff --git a/run/markdown-preview/editor/app.js b/run/markdown-preview/editor/app.js index 3cd0f38714..3873d9166f 100644 --- a/run/markdown-preview/editor/app.js +++ b/run/markdown-preview/editor/app.js @@ -12,10 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -const express = require('express'); -const handlebars = require('handlebars'); -const {readFile} = require('fs').promises; -const renderRequest = require('./render.js'); +import express from 'express'; +import handlebars from 'handlebars'; +import fs from 'fs'; +import renderRequest from './render.js'; const app = express(); app.use(express.json()); @@ -23,11 +23,14 @@ app.use(express.json()); let markdownDefault, compiledTemplate, renderedHtml; // Load the template files and serve them with the Editor service. -const buildRenderedHtml = async () => { +export const buildRenderedHtml = async () => { + const dirname = process.cwd(); try { - markdownDefault = await readFile(__dirname + '/templates/markdown.md'); + markdownDefault = await fs.promises.readFile( + dirname + '/templates/markdown.md' + ); compiledTemplate = handlebars.compile( - await readFile(__dirname + '/templates/index.html', 'utf8') + await fs.promises.readFile(dirname + '/templates/index.html', 'utf8') ); renderedHtml = compiledTemplate({default: markdownDefault}); return renderedHtml; @@ -62,7 +65,4 @@ app.post('/render', async (req, res) => { // [END cloudrun_secure_request_do] // Exports for testing purposes. -module.exports = { - app, - buildRenderedHtml, -}; +export default app; diff --git a/run/markdown-preview/editor/index.js b/run/markdown-preview/editor/index.js index 831ee1db01..8631e1de62 100644 --- a/run/markdown-preview/editor/index.js +++ b/run/markdown-preview/editor/index.js @@ -12,8 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -const {app} = require('./app'); -const pkg = require('./package.json'); -const PORT = parseInt(process.env.PORT) || 8080; +import app from './app.js'; +import fs from 'fs'; +const pkg = JSON.parse(fs.readFileSync('./package.json')); +const PORT = parseInt(process.env.PORT) || 8080; app.listen(PORT, () => console.log(`${pkg.name} listening on port ${PORT}`)); diff --git a/run/markdown-preview/editor/package.json b/run/markdown-preview/editor/package.json index 5ffa5dd772..61179d8b08 100644 --- a/run/markdown-preview/editor/package.json +++ b/run/markdown-preview/editor/package.json @@ -9,8 +9,9 @@ "type": "git", "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" }, + "type": "module", "engines": { - "node": ">=16.0.0" + "node": ">=20.0.0" }, "main": "main.js", "scripts": { @@ -23,12 +24,12 @@ "dependencies": { "express": "^4.17.1", "google-auth-library": "^9.0.0", - "got": "^11.5.0", - "handlebars": "^4.7.6" + "got": "^14.6.5", + "handlebars": "^4.7.8" }, "devDependencies": { "c8": "^10.0.0", - "mocha": "^10.0.0", + "mocha": "^11.0.0", "supertest": "^7.0.0" } } diff --git a/run/markdown-preview/editor/render.js b/run/markdown-preview/editor/render.js index cc75c4c533..cd68f1a219 100644 --- a/run/markdown-preview/editor/render.js +++ b/run/markdown-preview/editor/render.js @@ -13,8 +13,8 @@ // limitations under the License. // [START cloudrun_secure_request] -const {GoogleAuth} = require('google-auth-library'); -const got = require('got'); +import {GoogleAuth} from 'google-auth-library'; +import got from 'got'; const auth = new GoogleAuth(); let client, serviceUrl; @@ -33,7 +33,9 @@ const renderRequest = async markdown => { 'Content-Type': 'text/plain', }, body: markdown, - timeout: 3000, + timeout: { + request: 3000, + }, }; try { @@ -69,4 +71,4 @@ const renderRequest = async markdown => { // [END cloudrun_secure_request] -module.exports = renderRequest; +export default renderRequest; diff --git a/run/markdown-preview/editor/test/app.test.js b/run/markdown-preview/editor/test/app.test.js index a2c92d5502..f89cb3f169 100644 --- a/run/markdown-preview/editor/test/app.test.js +++ b/run/markdown-preview/editor/test/app.test.js @@ -14,14 +14,13 @@ 'use strict'; -const assert = require('assert'); -const path = require('path'); -const supertest = require('supertest'); +import assert from 'assert'; +import supertest from 'supertest'; +import app, {buildRenderedHtml} from '../app.js'; describe('Editor unit tests', () => { describe('Initialize app', () => { it('should successfully load the index page', async () => { - const {app} = require(path.join(__dirname, '..', 'app')); const request = supertest(app); await request.get('/').retry(3).expect(200); }); @@ -31,7 +30,6 @@ describe('Editor unit tests', () => { let template; before(async () => { - const {buildRenderedHtml} = require(path.join(__dirname, '..', 'app')); template = await buildRenderedHtml(); }); @@ -48,7 +46,6 @@ describe('Integration tests', () => { before(async () => { process.env.EDITOR_UPSTREAM_RENDER_URL = 'https://www.example.com/'; - const {app} = require(path.join(__dirname, '..', 'app')); request = supertest(app); }); diff --git a/run/markdown-preview/editor/test/system.test.js b/run/markdown-preview/editor/test/system.test.js index f4a9971531..710ded7372 100644 --- a/run/markdown-preview/editor/test/system.test.js +++ b/run/markdown-preview/editor/test/system.test.js @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -const assert = require('assert'); -const got = require('got'); -const {execSync} = require('child_process'); +import assert from 'assert'; +import got from 'got'; +import {execSync} from 'child_process'; describe('End-to-End Tests', () => { // Retrieve Cloud Run service test config @@ -88,7 +88,9 @@ describe('End-to-End Tests', () => { headers: { Authorization: `Bearer ${ID_TOKEN.trim()}`, }, - retry: 3, + retry: { + limit: 3, + }, }; const response = await got('', options); assert.strictEqual(response.statusCode, 200); diff --git a/run/markdown-preview/renderer/app.js b/run/markdown-preview/renderer/app.js index a984b37045..8a7b7de408 100644 --- a/run/markdown-preview/renderer/app.js +++ b/run/markdown-preview/renderer/app.js @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -const express = require('express'); -const MarkdownIt = require('markdown-it'); +import express from 'express'; +import MarkdownIt from 'markdown-it'; const app = express(); app.use(express.text()); @@ -40,4 +40,4 @@ app.post('/', (req, res) => { }); // Export for testing purposes. -module.exports = app; +export default app; diff --git a/run/markdown-preview/renderer/index.js b/run/markdown-preview/renderer/index.js index 43782b14ea..e5ee725a5e 100644 --- a/run/markdown-preview/renderer/index.js +++ b/run/markdown-preview/renderer/index.js @@ -12,8 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -const app = require('./app'); -const pkg = require('./package.json'); +import app from './app.js'; +import fs from 'fs'; + +const pkg = JSON.parse(fs.readFileSync('./package.json')); const PORT = parseInt(process.env.PORT) || 8080; app.listen(PORT, () => console.log(`${pkg.name} listening on port ${PORT}`)); diff --git a/run/markdown-preview/renderer/package.json b/run/markdown-preview/renderer/package.json index d947efe077..6f7b5cf0bb 100644 --- a/run/markdown-preview/renderer/package.json +++ b/run/markdown-preview/renderer/package.json @@ -8,8 +8,9 @@ "type": "git", "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" }, + "type": "module", "engines": { - "node": ">=16.0.0" + "node": ">=20.0.0" }, "main": "index.js", "scripts": { @@ -26,8 +27,8 @@ "devDependencies": { "c8": "^10.0.0", "google-auth-library": "^9.0.0", - "got": "^11.5.0", - "mocha": "^10.0.0", + "got": "^14.6.5", + "mocha": "^11.0.0", "sinon": "^18.0.0", "supertest": "^7.0.0" } diff --git a/run/markdown-preview/renderer/test/app.test.js b/run/markdown-preview/renderer/test/app.test.js index e8770795a5..84d225d1ba 100644 --- a/run/markdown-preview/renderer/test/app.test.js +++ b/run/markdown-preview/renderer/test/app.test.js @@ -14,16 +14,15 @@ 'use strict'; -const assert = require('assert'); -const path = require('path'); -const sinon = require('sinon'); -const supertest = require('supertest'); +import assert from 'assert'; +import sinon from 'sinon'; +import supertest from 'supertest'; +import app from '../app.js'; let request; describe('Unit Tests', () => { before(() => { - const app = require(path.join(__dirname, '..', 'app')); request = supertest(app); }); diff --git a/run/markdown-preview/renderer/test/system.test.js b/run/markdown-preview/renderer/test/system.test.js index 4b8213b129..19485f2b9e 100644 --- a/run/markdown-preview/renderer/test/system.test.js +++ b/run/markdown-preview/renderer/test/system.test.js @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -const assert = require('assert'); -const got = require('got'); -const {execSync} = require('child_process'); +import assert from 'assert'; +import got from 'got'; +import {execSync} from 'child_process'; describe('End-to-End Tests', () => { // Retrieve Cloud Run service test config @@ -74,7 +74,9 @@ describe('End-to-End Tests', () => { Authorization: `Bearer ${ID_TOKEN.trim()}`, }, method: 'POST', - retry: 3, + retry: { + limit: 3, + }, throwHttpErrors: false, }; const response = await got('', options); diff --git a/storage-control/createAnywhereCache.js b/storage-control/createAnywhereCache.js new file mode 100644 index 0000000000..1b43d6e4d5 --- /dev/null +++ b/storage-control/createAnywhereCache.js @@ -0,0 +1,122 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +/** + * This application demonstrates how to perform basic operations on an Anywhere Cache + * instance with the Google Cloud Storage API. + * + * For more information, see the documentation at https://cloud.google.com/storage/docs/anywhere-cache. + */ + +function main(bucketName, zoneName) { + // [START storage_control_create_anywhere_cache] + + /** + * Creates an Anywhere Cache instance for a Cloud Storage bucket. + * Anywhere Cache is a feature that provides an SSD-backed zonal read cache. + * This can significantly improve read performance for frequently accessed data + * by caching it in the same zone as your compute resources. + * + * @param {string} bucketName The name of the bucket to create the cache for. + * Example: 'your-gcp-bucket-name' + * @param {string} zoneName The zone where the cache will be created. + * Example: 'us-central1-a' + */ + + // Imports the Control library + const {StorageControlClient} = require('@google-cloud/storage-control').v2; + + // Instantiates a client + const controlClient = new StorageControlClient(); + + async function callCreateAnywhereCache() { + const bucketPath = controlClient.bucketPath('_', bucketName); + + // Create the request + const request = { + parent: bucketPath, + anywhereCache: { + zone: zoneName, + ttl: { + seconds: '10000s', + }, // Optional. Default: '86400s'(1 day) + admissionPolicy: 'admit-on-first-miss', // Optional. Default: 'admit-on-first-miss' + }, + }; + + try { + // Run the request, which returns an Operation object + const [operation] = await controlClient.createAnywhereCache(request); + console.log(`Waiting for operation ${operation.name} to complete...`); + + // Wait for the operation to complete and get the final resource + const anywhereCache = await checkCreateAnywhereCacheProgress( + operation.name + ); + console.log(`Created anywhere cache: ${anywhereCache.result.name}.`); + } catch (error) { + // Handle any error that occurred during the creation or polling process. + console.error('Failed to create Anywhere Cache:', error.message); + throw error; + } + } + + // A custom function to check the operation's progress. + async function checkCreateAnywhereCacheProgress(operationName) { + let operation = {done: false}; + console.log('Starting manual polling for operation...'); + + // Poll the operation until it's done. + while (!operation.done) { + await new Promise(resolve => setTimeout(resolve, 180000)); // Wait for 3 minutes before the next check. + const request = { + name: operationName, + }; + try { + const [latestOperation] = await controlClient.getOperation(request); + operation = latestOperation; + } catch (err) { + // Handle potential errors during polling. + console.error('Error while polling:', err.message); + break; // Exit the loop on error. + } + } + + // Return the final result of the operation. + if (operation.response) { + // Decode the operation response into a usable Operation object + const decodeOperation = new controlClient._gaxModule.Operation( + operation, + controlClient.descriptors.longrunning.createAnywhereCache, + controlClient._gaxModule.createDefaultBackoffSettings() + ); + // Return the decoded operation + return decodeOperation; + } else { + // If there's no response, it indicates an issue, so throw an error + throw new Error('Operation completed without a response.'); + } + } + + callCreateAnywhereCache(); + // [END storage_control_create_anywhere_cache] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/storage-control/disableAnywhereCache.js b/storage-control/disableAnywhereCache.js new file mode 100644 index 0000000000..882ff25151 --- /dev/null +++ b/storage-control/disableAnywhereCache.js @@ -0,0 +1,101 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +/** + * This application demonstrates how to perform basic operations on an Anywhere Cache + * instance with the Google Cloud Storage API. + * + * For more information, see the documentation at https://cloud.google.com/storage/docs/anywhere-cache. + */ + +function main(bucketName, cacheName) { + // [START storage_control_disable_anywhere_cache] + /** + * Disables an Anywhere Cache instance. + * + * Disabling a cache is the first step to permanently removing it. Once disabled, + * the cache stops ingesting new data. After a grace period, the cache and its + * contents are deleted. This is useful for decommissioning caches that are no + * longer needed. + * + * @param {string} bucketName The name of the bucket where the cache resides. + * Example: 'your-gcp-bucket-name' + * @param {string} cacheName The unique identifier of the cache instance to disable. + * Example: 'cacheName' + */ + + // Imports the Control library + const {StorageControlClient} = require('@google-cloud/storage-control').v2; + + // Instantiates a client + const controlClient = new StorageControlClient(); + + async function callDisableAnywhereCache() { + // You have a one-hour grace period after disabling a cache to resume it and prevent its deletion. + // If you don't resume the cache within that hour, it will be deleted, its data will be evicted, + // and the cache will be permanently removed from the bucket. + + const anywhereCachePath = controlClient.anywhereCachePath( + '_', + bucketName, + cacheName + ); + + // Create the request + const request = { + name: anywhereCachePath, + }; + + try { + // Run request. This initiates the disablement process. + const [response] = await controlClient.disableAnywhereCache(request); + + console.log( + `Successfully initiated disablement for Anywhere Cache: '${cacheName}'.` + ); + console.log(` Current State: ${response.state}`); + console.log(` Resource Name: ${response.name}`); + } catch (error) { + // Catch and handle potential API errors. + console.error( + `Error disabling Anywhere Cache '${cacheName}': ${error.message}` + ); + + if (error.code === 5) { + // NOT_FOUND (gRPC code 5) error can occur if the bucket or cache does not exist. + console.error( + `Please ensure the cache '${cacheName}' exists in bucket '${bucketName}'.` + ); + } else if (error.code === 9) { + // FAILED_PRECONDITION (gRPC code 9) can occur if the cache is already being disabled + // or is not in a RUNNING state that allows the disable operation. + console.error( + `Cache '${cacheName}' may not be in a state that allows disabling (e.g., must be RUNNING).` + ); + } + throw error; + } + } + + callDisableAnywhereCache(); + // [END storage_control_disable_anywhere_cache] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/storage-control/getAnywhereCache.js b/storage-control/getAnywhereCache.js new file mode 100644 index 0000000000..2633ede7e4 --- /dev/null +++ b/storage-control/getAnywhereCache.js @@ -0,0 +1,91 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +/** + * This application demonstrates how to perform basic operations on an Anywhere Cache + * instance with the Google Cloud Storage API. + * + * For more information, see the documentation at https://cloud.google.com/storage/docs/anywhere-cache. + */ + +function main(bucketName, cacheName) { + // [START storage_control_get_anywhere_cache] + /** + * Retrieves details of a specific Anywhere Cache instance. + * + * This function is useful for checking the current state, configuration (like TTL), + * and other metadata of an existing cache. + * + * @param {string} bucketName The name of the bucket where the cache resides. + * Example: 'your-gcp-bucket-name' + * @param {string} cacheName The unique identifier of the cache instance. + * Example: 'my-anywhere-cache-id' + */ + + // Imports the Control library + const {StorageControlClient} = require('@google-cloud/storage-control').v2; + + // Instantiates a client + const controlClient = new StorageControlClient(); + + async function callGetAnywhereCache() { + const anywhereCachePath = controlClient.anywhereCachePath( + '_', + bucketName, + cacheName + ); + + // Create the request + const request = { + name: anywhereCachePath, + }; + + try { + // Run request + const [response] = await controlClient.getAnywhereCache(request); + console.log(`Anywhere Cache details for '${cacheName}':`); + console.log(` Name: ${response.name}`); + console.log(` Zone: ${response.zone}`); + console.log(` State: ${response.state}`); + console.log(` TTL: ${response.ttl.seconds}s`); + console.log(` Admission Policy: ${response.admissionPolicy}`); + console.log( + ` Create Time: ${new Date(response.createTime.seconds * 1000).toISOString()}` + ); + } catch (error) { + // Handle errors (e.g., cache not found, permission denied). + console.error( + `Error retrieving Anywhere Cache '${cacheName}': ${error.message}` + ); + + if (error.code === 5) { + console.error( + `Ensure the cache '${cacheName}' exists in bucket '${bucketName}'.` + ); + } + throw error; + } + } + + callGetAnywhereCache(); + // [END storage_control_get_anywhere_cache] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/storage-control/listAnywhereCaches.js b/storage-control/listAnywhereCaches.js new file mode 100644 index 0000000000..9bf5175981 --- /dev/null +++ b/storage-control/listAnywhereCaches.js @@ -0,0 +1,82 @@ +// Copyright 2025 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 +// +// https://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. +// + +'use strict'; + +/** + * This application demonstrates how to perform basic operations on an Anywhere Cache + * instance with the Google Cloud Storage API. + * + * For more information, see the documentation at https://cloud.google.com/storage/docs/anywhere-cache. + */ + +function main(bucketName) { + // [START storage_control_list_anywhere_caches] + /** + * Lists all Anywhere Cache instances for a Cloud Storage bucket. + * This function helps you discover all active and pending caches associated with + * a specific bucket, which is useful for auditing and management. + * + * @param {string} bucketName The name of the bucket to list caches for. + * Example: 'your-gcp-bucket-name' + */ + + // Imports the Control library + const {StorageControlClient} = require('@google-cloud/storage-control').v2; + + // Instantiates a client + const controlClient = new StorageControlClient(); + + async function callListAnywhereCaches() { + const bucketPath = controlClient.bucketPath('_', bucketName); + + // Create the request + const request = { + parent: bucketPath, + }; + + try { + // Run request. The response is an array where the first element is the list of caches. + const [response] = await controlClient.listAnywhereCaches(request); + + if (response && response.length > 0) { + console.log( + `Found ${response.length} Anywhere Caches for bucket: ${bucketName}` + ); + for (const anywhereCache of response) { + console.log(anywhereCache.name); + } + } else { + // Case: Successful but empty list (No Anywhere Caches found) + console.log(`No Anywhere Caches found for bucket: ${bucketName}.`); + } + } catch (error) { + console.error( + `Error listing Anywhere Caches for bucket ${bucketName}:`, + error.message + ); + throw error; + } + } + + callListAnywhereCaches(); + // [END storage_control_list_anywhere_caches] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/storage-control/package.json b/storage-control/package.json index 0f6d932845..98f61559c5 100644 --- a/storage-control/package.json +++ b/storage-control/package.json @@ -12,8 +12,8 @@ "author": "Google Inc.", "license": "Apache-2.0", "devDependencies": { - "@google-cloud/storage": "^7.12.0", - "@google-cloud/storage-control": "^0.2.0", + "@google-cloud/storage": "^7.17.0", + "@google-cloud/storage-control": "^0.5.0", "c8": "^10.0.0", "chai": "^4.5.0", "mocha": "^10.7.0", diff --git a/storage-control/pauseAnywhereCache.js b/storage-control/pauseAnywhereCache.js new file mode 100644 index 0000000000..dcb974b0fe --- /dev/null +++ b/storage-control/pauseAnywhereCache.js @@ -0,0 +1,92 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +/** + * This application demonstrates how to perform basic operations on an Anywhere Cache + * instance with the Google Cloud Storage API. + * + * For more information, see the documentation at https://cloud.google.com/storage/docs/anywhere-cache. + */ + +function main(bucketName, cacheName) { + // [START storage_control_pause_anywhere_cache] + /** + * Pauses an Anywhere Cache instance. + * + * This synchronous function stops the ingestion of new data for a cache that's in a RUNNING state. + * While PAUSED, you can still read existing data (which resets the TTL), but no new data is ingested. + * The cache can be returned to the RUNNING state by calling the resume function. + * + * @param {string} bucketName The name of the bucket where the cache resides. + * Example: 'your-gcp-bucket-name' + * @param {string} cacheName The unique identifier of the cache instance. + * Example: 'my-anywhere-cache-id' + */ + + // Imports the Control library + const {StorageControlClient} = require('@google-cloud/storage-control').v2; + + // Instantiates a client + const controlClient = new StorageControlClient(); + + async function callPauseAnywhereCache() { + const anywhereCachePath = controlClient.anywhereCachePath( + '_', + bucketName, + cacheName + ); + + // Create the request + const request = { + name: anywhereCachePath, + }; + + try { + // Run request + const [response] = await controlClient.pauseAnywhereCache(request); + + console.log(`Successfully paused anywhere cache: ${response.name}.`); + console.log(` Current State: ${response.state}`); + } catch (error) { + // Catch and handle potential API errors. + console.error( + `Error pausing Anywhere Cache '${cacheName}': ${error.message}` + ); + + if (error.code === 5) { + // NOT_FOUND (gRPC code 5) + console.error( + `Please ensure the cache '${cacheName}' exists in bucket '${bucketName}'.` + ); + } else if (error.code === 9) { + // FAILED_PRECONDITION (gRPC code 9) + console.error( + `Cache '${cacheName}' may not be in a state that allows pausing (e.g., must be RUNNING).` + ); + } + throw error; + } + } + + callPauseAnywhereCache(); + // [END storage_control_pause_anywhere_cache] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/storage-control/resumeAnywhereCache.js b/storage-control/resumeAnywhereCache.js new file mode 100644 index 0000000000..1cea5035d3 --- /dev/null +++ b/storage-control/resumeAnywhereCache.js @@ -0,0 +1,91 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +/** + * This application demonstrates how to perform basic operations on an Anywhere Cache + * instance with the Google Cloud Storage API. + * + * For more information, see the documentation at https://cloud.google.com/storage/docs/anywhere-cache. + */ + +function main(bucketName, cacheName) { + // [START storage_control_resume_anywhere_cache] + /** + * Resumes a disabled Anywhere Cache instance. + * + * This action reverts a cache from a PAUSED state or a DISABLED state back to RUNNING, + * provided it is done within the 1-hour grace period before the cache is permanently deleted. + * + * @param {string} bucketName The name of the bucket where the cache resides. + * Example: 'your-gcp-bucket-name' + * @param {string} cacheName The unique identifier of the cache instance. + * Example: 'my-anywhere-cache-id' + */ + + // Imports the Control library + const {StorageControlClient} = require('@google-cloud/storage-control').v2; + + // Instantiates a client + const controlClient = new StorageControlClient(); + + async function callResumeAnywhereCache() { + const anywhereCachePath = controlClient.anywhereCachePath( + '_', + bucketName, + cacheName + ); + + // Create the request + const request = { + name: anywhereCachePath, + }; + + try { + // Run request + const [response] = await controlClient.resumeAnywhereCache(request); + + console.log(`Successfully resumed anywhere cache: ${response.name}.`); + console.log(` Current State: ${response.state}`); + } catch (error) { + // Catch and handle potential API errors. + console.error( + `Error resuming Anywhere Cache '${cacheName}': ${error.message}` + ); + + if (error.code === 5) { + // NOT_FOUND (gRPC code 5) + console.error( + `Please ensure the cache '${cacheName}' exists in bucket '${bucketName}'.` + ); + } else if (error.code === 9) { + // FAILED_PRECONDITION (gRPC code 9) + console.error( + `Cache '${cacheName}' may not be in a state that allows resuming (e.g., already RUNNING or past the 1-hour deletion grace period).` + ); + } + throw error; + } + } + + callResumeAnywhereCache(); + // [END storage_control_resume_anywhere_cache] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/storage-control/system-test/anywhereCache.test.js b/storage-control/system-test/anywhereCache.test.js new file mode 100644 index 0000000000..59a6bbadb6 --- /dev/null +++ b/storage-control/system-test/anywhereCache.test.js @@ -0,0 +1,164 @@ +// Copyright 2025 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 +// +// https://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. + +const {Storage, Bucket} = require('@google-cloud/storage'); +const {StorageControlClient} = require('@google-cloud/storage-control').v2; +const cp = require('child_process'); +const {assert} = require('chai'); +const {describe, it, before, after} = require('mocha'); +const uuid = require('uuid'); + +const execSync = cmd => cp.execSync(cmd, {encoding: 'utf-8'}); +const bucketPrefix = `storage-control-samples-${uuid.v4()}`; +const bucketName = `${bucketPrefix}-a`; +const controlClient = new StorageControlClient(); +const storage = new Storage(); +const bucket = new Bucket(storage, bucketName); +const zoneName = 'us-west1-c'; +const cacheName = 'us-west1-c'; +let anywhereCachePath; + +// Skipped to prevent CI timeouts caused by long-running operations. +// Un-skip for deliberate, manual runs. +describe.skip('Anywhere Cache', () => { + before(async () => { + await storage.createBucket(bucketName, { + iamConfiguration: { + uniformBucketLevelAccess: { + enabled: true, + }, + }, + hierarchicalNamespace: {enabled: true}, + location: 'us-west1', + }); + + anywhereCachePath = controlClient.anywhereCachePath( + '_', + bucketName, + cacheName + ); + }); + + after(async function () { + // Sets the timeout for the test to 3600000 milliseconds (1 hour). + // This is necessary for long-running operations, such as waiting for a + // cache to be disabled, to prevent the test from failing due to a timeout. + this.timeout(3600000); + let caches = false; + // The `while` loop will continue to run as long as the `caches` flag is `false`. + while (!caches) { + await new Promise(resolve => setTimeout(resolve, 30000)); + const bucketPath = controlClient.bucketPath('_', bucketName); + + try { + // Call the `listAnywhereCaches` method to check for any active caches. + // The response is an array of caches. + const [response] = await controlClient.listAnywhereCaches({ + parent: bucketPath, + }); + // Check if the response array is empty. If so, it means there are no more caches, and we can exit the loop. + if (response.length === 0) { + // Set `caches` to `true` to break out of the `while` loop. + caches = true; + } + } catch (err) { + console.error('Error while polling:', err.message); + break; + } + } + // After the loop has finished (i.e., no more caches are found), we proceed with deleting the bucket. + await bucket.delete(); + }); + + it('should create an anywhere cache', async function () { + // Sets the timeout for the test to 3600000 milliseconds (1 hour). + // This is necessary for long-running operations, such as waiting for a + // cache to be created, to prevent the test from failing due to a timeout. + this.timeout(3600000); + const output = execSync( + `node createAnywhereCache.js ${bucketName} ${zoneName}` + ); + assert.match(output, /Created anywhere cache:/); + assert.match(output, new RegExp(anywhereCachePath)); + }); + + it('should get an anywhere cache', async () => { + const output = execSync( + `node getAnywhereCache.js ${bucketName} ${cacheName}` + ); + const detailsHeader = `Anywhere Cache details for '${cacheName}':`; + assert.match(output, new RegExp(detailsHeader)); + assert.match(output, /Name:/); + assert.match(output, new RegExp(anywhereCachePath)); + assert.match(output, /Zone:/); + assert.match(output, /State:/); + assert.match(output, /TTL:/); + assert.match(output, /Admission Policy:/); + assert.match(output, /Create Time:/); + }); + + it('should list anywhere caches', async () => { + const output = execSync(`node listAnywhereCaches.js ${bucketName}`); + assert.match(output, new RegExp(anywhereCachePath)); + }); + + it('should update an anywhere cache', async () => { + const admissionPolicy = 'admit-on-second-miss'; + const output = execSync( + `node updateAnywhereCache.js ${bucketName} ${cacheName} ${admissionPolicy}` + ); + assert.match(output, /Updated anywhere cache:/); + assert.match(output, new RegExp(anywhereCachePath)); + }); + + it('should pause an anywhere cache', async () => { + const output = execSync( + `node pauseAnywhereCache.js ${bucketName} ${cacheName}` + ); + assert.match(output, /Successfully paused anywhere cache:/); + assert.match(output, new RegExp(anywhereCachePath)); + assert.match(output, /Current State:/); + }); + + it('should resume an anywhere cache', async () => { + const output = execSync( + `node resumeAnywhereCache.js ${bucketName} ${cacheName}` + ); + assert.match(output, /Successfully resumed anywhere cache:/); + assert.match(output, new RegExp(anywhereCachePath)); + assert.match(output, /Current State:/); + }); + + it('should disable an anywhere cache', async () => { + try { + const output = execSync( + `node disableAnywhereCache.js ${bucketName} ${cacheName}` + ); + assert.match( + output, + /Successfully initiated disablement for Anywhere Cache:/ + ); + assert.match(output, new RegExp(anywhereCachePath)); + assert.match(output, /Current State:/); + assert.match(output, /Resource Name:/); + } catch (error) { + const errorMessage = error.stderr.toString(); + + assert.match( + errorMessage, + /9 FAILED_PRECONDITION: The requested DISABLE operation can't be applied on cache in DISABLED state./ + ); + } + }); +}); diff --git a/storage-control/updateAnywhereCache.js b/storage-control/updateAnywhereCache.js new file mode 100644 index 0000000000..1d006e4119 --- /dev/null +++ b/storage-control/updateAnywhereCache.js @@ -0,0 +1,100 @@ +// Copyright 2025 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 +// +// https://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. + +'use strict'; + +/** + * This application demonstrates how to perform basic operations on an Anywhere Cache + * instance with the Google Cloud Storage API. + * + * For more information, see the documentation at https://cloud.google.com/storage/docs/anywhere-cache. + */ + +function main(bucketName, cacheName, admissionPolicy) { + // [START storage_control_update_anywhere_cache] + /** + * Updates the Admission Policy of an Anywhere Cache instance. + * + * @param {string} bucketName The name of the bucket where the cache resides. + * Example: 'your-gcp-bucket-name' + * @param {string} cacheName The unique identifier of the cache instance to update. + * Example: 'my-anywhere-cache-id' + * @param {string} admissionPolicy Determines when data is ingested into the cache + * Example: 'admit-on-second-miss' + */ + + // Imports the Control library + const {StorageControlClient} = require('@google-cloud/storage-control').v2; + + // Instantiates a client + const controlClient = new StorageControlClient(); + + async function callUpdateAnywhereCache() { + const anywhereCachePath = controlClient.anywhereCachePath( + '_', + bucketName, + cacheName + ); + + // Create the request + const request = { + anywhereCache: { + name: anywhereCachePath, + admissionPolicy: admissionPolicy, + }, + updateMask: { + paths: ['admission_policy'], + }, + }; + + try { + // Run request + const [operation] = await controlClient.updateAnywhereCache(request); + console.log( + `Waiting for update operation ${operation.name} to complete...` + ); + + const [response] = await operation.promise(); + + console.log(`Updated anywhere cache: ${response.name}.`); + } catch (error) { + // Handle errors during the initial request or during the LRO polling. + console.error( + `Error updating Anywhere Cache '${cacheName}': ${error.message}` + ); + + if (error.code === 5) { + // NOT_FOUND (gRPC code 5) + console.error( + `Ensure the cache '${cacheName}' exists in bucket '${bucketName}'.` + ); + } else if (error.code === 3) { + // INVALID_ARGUMENT (gRPC code 3) + console.error( + `Ensure '${admissionPolicy}' is a valid Admission Policy.` + ); + } + throw error; + } + } + + callUpdateAnywhereCache(); + // [END storage_control_update_anywhere_cache] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/vision/detect.js b/vision/detect.js index c728c8569f..07255a61c9 100644 --- a/vision/detect.js +++ b/vision/detect.js @@ -202,6 +202,7 @@ async function detectTextGCS(bucketName, fileName) { async function detectLogos(fileName) { // [START vision_logo_detection] const vision = require('@google-cloud/vision'); + const fs = require('fs'); // Creates a client const client = new vision.ImageAnnotatorClient(); @@ -211,17 +212,42 @@ async function detectLogos(fileName) { */ // const fileName = 'Local image file, e.g. /path/to/image.png'; + const imageBuffer = fs.readFileSync(fileName); + const base64Image = imageBuffer.toString('base64'); + + const request = { + requests: [ + { + image: { + content: base64Image, + }, + features: [ + { + type: 'LOGO_DETECTION', + }, + ], + }, + ], + }; + // Performs logo detection on the local file - const [result] = await client.logoDetection(fileName); - const logos = result.logoAnnotations; - console.log('Logos:'); - logos.forEach(logo => console.log(logo)); + const [response] = await client.batchAnnotateImages(request); + + response.responses.forEach(res => { + if (res.logoAnnotations) { + console.log('Logos:'); + res.logoAnnotations.forEach(logo => console.log(logo)); + } + + if (res.error) { + console.error(` - Error: ${res.error.message}`); + } + }); // [END vision_logo_detection] } async function detectLogosGCS(bucketName, fileName) { // [START vision_logo_detection_gcs] - // Imports the Google Cloud client libraries const vision = require('@google-cloud/vision'); // Creates a client @@ -233,11 +259,36 @@ async function detectLogosGCS(bucketName, fileName) { // const bucketName = 'Bucket where the file resides, e.g. my-bucket'; // const fileName = 'Path to file within bucket, e.g. path/to/image.png'; + const request = { + requests: [ + { + image: { + source: { + imageUri: `gs://${bucketName}/${fileName}`, + }, + }, + features: [ + { + type: 'LOGO_DETECTION', + }, + ], + }, + ], + }; + // Performs logo detection on the gcs file - const [result] = await client.logoDetection(`gs://${bucketName}/${fileName}`); - const logos = result.logoAnnotations; - console.log('Logos:'); - logos.forEach(logo => console.log(logo)); + const [response] = await client.batchAnnotateImages(request); + + response.responses.forEach(res => { + if (res.logoAnnotations) { + console.log('Logos:'); + res.logoAnnotations.forEach(logo => console.log(logo)); + } + + if (res.error) { + console.error(` - Error: ${res.error.message}`); + } + }); // [END vision_logo_detection_gcs] }