Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions kits/storage-resize-images/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
62 changes: 32 additions & 30 deletions kits/storage-resize-images/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -81,26 +81,26 @@ Deploy a single instance with `firebase deploy --only functions:<instance id>`.
Set these values in a `.env` (or `.env.<projectId>`) 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

Expand Down Expand Up @@ -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
Expand Down
6 changes: 1 addition & 5 deletions kits/storage-resize-images/src/content-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
}),
],
Expand Down
32 changes: 27 additions & 5 deletions kits/storage-resize-images/tests/content-filter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading