diff --git a/kits/storage-resize-images/CHANGELOG.md b/kits/storage-resize-images/CHANGELOG.md index 711eb60d36..bddce1dca9 100644 --- a/kits/storage-resize-images/CHANGELOG.md +++ b/kits/storage-resize-images/CHANGELOG.md @@ -1 +1,2 @@ +- fix: restore the `us-central1` content-filter fallback. `checkImageContent` threw `FUNCTION_REGION is required for Vertex AI filtering.` when no region was available; the extension fell back to `us-central1`. The Vertex AI call now uses the function's region when known and `us-central1` otherwise, matching the extension. Normal CLI deploys were unaffected (the Firebase CLI sets `FUNCTION_REGION` on deployed functions); the throw was reachable for library consumers, emulator runs, and hand-rolled environments. - Initial release of kit, see README for differences between the legacy extension and this kit diff --git a/kits/storage-resize-images/README.md b/kits/storage-resize-images/README.md index bb18cf74b3..ab13f0e79d 100644 --- a/kits/storage-resize-images/README.md +++ b/kits/storage-resize-images/README.md @@ -23,13 +23,13 @@ below, enables the listed APIs, and attaches the account to every function in this kit. Do not set a custom runtime service account for this codebase — it conflicts with that automatic setup. -| Role / API | Why | -|---|---| -| `roles/storage.admin` | read originals and write resized objects | -| `roles/aiplatform.user` | optional content filtering via Vertex AI | -| `roles/eventarc.eventReceiver` | receive Gen2 Storage trigger events | -| `roles/run.invoker` | allow Eventarc to invoke the Gen2 Cloud Run service | -| `storage-component.googleapis.com` | use Cloud Storage | +| Role / API | Why | +| ---------------------------------- | --------------------------------------------------- | +| `roles/storage.admin` | read originals and write resized objects | +| `roles/aiplatform.user` | optional content filtering via Vertex AI | +| `roles/eventarc.eventReceiver` | receive Gen2 Storage trigger events | +| `roles/run.invoker` | allow Eventarc to invoke the Gen2 Cloud Run service | +| `storage-component.googleapis.com` | use Cloud Storage | ## Usage @@ -81,26 +81,26 @@ Deploy a single instance with `firebase deploy --only functions:`. Set these values in a `.env` (or `.env.`) file. The Firebase CLI loads them at deploy time and prompts for any required values that are missing. -| Field | Env var | Required | Default | Description | -|---|---|---|---|---| -| `bucket` | `IMG_BUCKET` | no | default Storage bucket | Bucket to watch | -| `sizes` | `IMG_SIZES` | no | `200x200` | Comma-separated resize sizes | -| `deleteOriginal` | `DELETE_ORIGINAL_FILE` | no | `false` | Delete original after resize | -| `makePublic` | `MAKE_PUBLIC` | no | `false` | Make resized objects public | -| `resizedImagesPath` | `RESIZED_IMAGES_PATH` | no | (empty) | Output path prefix | -| `includePathList` | `INCLUDE_PATH_LIST` | no | (empty) | Comma-separated absolute paths to include (for example, `/users/avatars,/design/pictures`) | -| `excludePathList` | `EXCLUDE_PATH_LIST` | no | (empty) | Comma-separated absolute paths to exclude (for example, `/users/avatars/thumbs`) | -| `failedImagesPath` | `FAILED_IMAGES_PATH` | no | (empty) | Failed-image output path | -| `cacheControlHeader` | `CACHE_CONTROL_HEADER` | no | (empty) | Cache-Control for outputs | -| `imageTypes` | `IMAGE_TYPE` | no | `["false"]` | Output image types list | -| `outputOptions` | `OUTPUT_OPTIONS` | no | (empty) | JSON output options | -| `sharpOptions` | `SHARP_OPTIONS` | no | `{}` | JSON Sharp options | -| `isAnimated` | `IS_ANIMATED` | no | `true` | Preserve animation | -| `memory` | `FUNCTION_MEMORY` | no | `1024` | Function memory (MiB) | -| `regenerateToken` | `REGENERATE_TOKEN` | no | `true` | Regenerate download tokens | -| `contentFilterLevel` | `CONTENT_FILTER_LEVEL` | no | `OFF` | Content filter level | -| `customFilterPrompt` | `CUSTOM_FILTER_PROMPT` | no | (empty) | Custom filter prompt | -| `placeholderImagePath` | `PLACEHOLDER_IMAGE_PATH` | no | (empty) | Placeholder for filtered images | +| Field | Env var | Required | Default | Description | +| ---------------------- | ------------------------ | -------- | ---------------------- | ------------------------------------------------------------------------------------------ | +| `bucket` | `IMG_BUCKET` | no | default Storage bucket | Bucket to watch | +| `sizes` | `IMG_SIZES` | no | `200x200` | Comma-separated resize sizes | +| `deleteOriginal` | `DELETE_ORIGINAL_FILE` | no | `false` | Delete original after resize | +| `makePublic` | `MAKE_PUBLIC` | no | `false` | Make resized objects public | +| `resizedImagesPath` | `RESIZED_IMAGES_PATH` | no | (empty) | Output path prefix | +| `includePathList` | `INCLUDE_PATH_LIST` | no | (empty) | Comma-separated absolute paths to include (for example, `/users/avatars,/design/pictures`) | +| `excludePathList` | `EXCLUDE_PATH_LIST` | no | (empty) | Comma-separated absolute paths to exclude (for example, `/users/avatars/thumbs`) | +| `failedImagesPath` | `FAILED_IMAGES_PATH` | no | (empty) | Failed-image output path | +| `cacheControlHeader` | `CACHE_CONTROL_HEADER` | no | (empty) | Cache-Control for outputs | +| `imageTypes` | `IMAGE_TYPE` | no | `["false"]` | Output image types list | +| `outputOptions` | `OUTPUT_OPTIONS` | no | (empty) | JSON output options | +| `sharpOptions` | `SHARP_OPTIONS` | no | `{}` | JSON Sharp options | +| `isAnimated` | `IS_ANIMATED` | no | `true` | Preserve animation | +| `memory` | `FUNCTION_MEMORY` | no | `1024` | Function memory (MiB) | +| `regenerateToken` | `REGENERATE_TOKEN` | no | `true` | Regenerate download tokens | +| `contentFilterLevel` | `CONTENT_FILTER_LEVEL` | no | `OFF` | Content filter level | +| `customFilterPrompt` | `CUSTOM_FILTER_PROMPT` | no | (empty) | Custom filter prompt | +| `placeholderImagePath` | `PLACEHOLDER_IMAGE_PATH` | no | (empty) | Placeholder for filtered images | ## Multiple instances @@ -144,9 +144,11 @@ below are worth knowing before you deploy. ### Content filtering runs in the function's region When `CONTENT_FILTER_LEVEL` is set (or you supply a `CUSTOM_FILTER_PROMPT`), -the Vertex AI call now uses the region the function is deployed to. The -extension used the region you picked at install time, falling back to -`us-central1`. +the Vertex AI call now uses the region the function is deployed to, falling +back to `us-central1` when that region is unknown - for example when calling +`checkImageContent` from the library or running outside a normal CLI deploy, +where `FUNCTION_REGION` is not set. The extension behaved the same way with +the region you picked at install time. Gemini is not available in every region. If you deploy to a region it does not serve, filtering fails and the image is treated as a filter error: it is not diff --git a/kits/storage-resize-images/src/content-filter.ts b/kits/storage-resize-images/src/content-filter.ts index a2c46b32b9..0c863921b1 100644 --- a/kits/storage-resize-images/src/content-filter.ts +++ b/kits/storage-resize-images/src/content-filter.ts @@ -78,16 +78,12 @@ export async function checkImageContent( if (filterLevel === null && prompt === null) { return true; } - if (!location) { - throw new Error("FUNCTION_REGION is required for Vertex AI filtering."); - } - const imageBuffer = fs.readFileSync(localOriginalFile); const dataUrl = createImageDataUrl(imageBuffer, contentType); const ai = genkit({ plugins: [ vertexAI({ - location, + location: location ?? "us-central1", models: ["gemini-2.5-flash"], }), ], diff --git a/kits/storage-resize-images/tests/content-filter.test.ts b/kits/storage-resize-images/tests/content-filter.test.ts index 127668b428..6ed515101a 100644 --- a/kits/storage-resize-images/tests/content-filter.test.ts +++ b/kits/storage-resize-images/tests/content-filter.test.ts @@ -49,6 +49,7 @@ vi.mock("../src/logs", () => ({ retryScheduled: vi.fn(), })); +import vertexAI from "@genkit-ai/vertexai"; import { checkImageContent } from "../src/content-filter"; import * as log from "../src/logs"; @@ -77,11 +78,32 @@ describe("checkImageContent with mocks", () => { expect(result).toBe(true); }); - it("should throw when no region is available", async () => { - // The extension falls back to "us-central1"; the kit refuses to guess. - await expect( - checkImageContent(imagePath, "BLOCK_ONLY_HIGH", null, "image/png") - ).rejects.toThrow("FUNCTION_REGION is required for Vertex AI filtering."); + it("should fall back to us-central1 when no region is available", async () => { + const result = await checkImageContent( + imagePath, + "BLOCK_ONLY_HIGH", + null, + "image/png" + ); + + expect(result).toBe(true); + expect(vi.mocked(vertexAI)).toHaveBeenCalledWith( + expect.objectContaining({ location: "us-central1" }) + ); + }); + + it("should pass an explicit region through to the Vertex AI plugin", async () => { + await checkImageContent( + imagePath, + "BLOCK_ONLY_HIGH", + null, + "image/png", + "europe-west1" + ); + + expect(vi.mocked(vertexAI)).toHaveBeenCalledWith( + expect.objectContaining({ location: "europe-west1" }) + ); }); it("should return true when the API response is positive", async () => { diff --git a/kits/storage-resize-images/tests/integration/content-filter.live.test.ts b/kits/storage-resize-images/tests/integration/content-filter.live.test.ts index c0e8869866..85493bd029 100644 --- a/kits/storage-resize-images/tests/integration/content-filter.live.test.ts +++ b/kits/storage-resize-images/tests/integration/content-filter.live.test.ts @@ -57,7 +57,6 @@ function guessContentType(filePath: string): string { const runLive = process.env.RUN_LIVE_CONTENT_FILTER_TESTS === "true"; const describeLive = runLive ? describe : describe.skip; -// The kit requires an explicit region — it has no "us-central1" fallback. const LOCATION = process.env.FUNCTION_REGION ?? "us-central1"; describeLive(