Skip to content

feat: add Amazon Nova 2 multimodal embeddings support - #222

Merged
zxkane merged 4 commits into
aws-samples:mainfrom
gabrielkoo:feat/nova-embed-v2
Feb 26, 2026
Merged

feat: add Amazon Nova 2 multimodal embeddings support#222
zxkane merged 4 commits into
aws-samples:mainfrom
gabrielkoo:feat/nova-embed-v2

Conversation

@gabrielkoo

@gabrielkoo gabrielkoo commented Feb 19, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds amazon.nova-2-multimodal-embeddings-v1:0 to SUPPORTED_BEDROCK_EMBEDDING_MODELS
  • Implements NovaEmbeddingsModel using the taskType/singleEmbeddingParams request format from the Nova 2 embeddings docs
  • Supports batch inputs (one API call per text), custom dimensions (256/512/1024/2048/3072, default 3072), and float/base64 encoding formats

Test plan

Start the gateway:

cd src && uvicorn api.app:app --port 8000

Then run the following (requires pip install openai):

import os
from openai import OpenAI

BASE_URL = os.environ.get("BASE_URL", "http://localhost:8000/api/v1")
MODEL = "amazon.nova-2-multimodal-embeddings-v1:0"
client = OpenAI(base_url=BASE_URL, api_key=os.environ.get("AWS_BEDROCK_BEARER_TOKEN", "dummy"))

# Single text
resp = client.embeddings.create(model=MODEL, input="Hello, world!")
assert len(resp.data[0].embedding) == 3072
print(f"[PASS] single text -> dim={len(resp.data[0].embedding)}")

# Batch
resp = client.embeddings.create(model=MODEL, input=["semantic search", "vector database", "RAG pipeline"])
assert len(resp.data) == 3
print(f"[PASS] batch (3 texts) -> dim={len(resp.data[0].embedding)}")

# Custom dimensions
resp = client.embeddings.create(model=MODEL, input="test", dimensions=256)
assert len(resp.data[0].embedding) == 256
print(f"[PASS] custom dimensions -> dim={len(resp.data[0].embedding)}")

# base64 encoding
resp = client.embeddings.create(model=MODEL, input="base64 test", encoding_format="base64")
assert isinstance(resp.data[0].embedding, str)
print("[PASS] base64 encoding")

# Model listed
assert MODEL in [m.id for m in client.models.list().data]
print("[PASS] model listed in /models")

gabrielkoo and others added 3 commits February 19, 2026 16:27
Adds support for `amazon.nova-2-multimodal-embeddings-v1:0` via the
new `NovaEmbeddingsModel` class, using the `taskType`/`singleEmbeddingParams`
request format documented in the Nova 2 user guide.

- Supports single and batch text inputs
- Respects the `dimensions` parameter (256/512/1024/2048/3072, default 3072)
- Supports `float` and `base64` encoding formats
- Includes `test_nova_embed.py` for quick end-to-end verification

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Test script moved to PR description instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add VALID_DIMENSIONS set and upfront validation with a clear error message
- Fix `dimensions or DEFAULT` which would incorrectly ignore dimensions=0
- Add inline comment explaining approximate token counting (Nova API
  does not return token counts in the response)

@zxkane zxkane left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding Nova 2 multimodal embeddings support! The overall structure follows the existing patterns well. A few issues to address before merge:

Critical

1. VALID_DIMENSIONS values are incorrect per AWS documentation

According to the Nova 2 embeddings docs and the embeddings schema reference, the allowed embeddingDimension values are 256, 384, 1024, 3072.

The current code {256, 512, 1024, 2048, 3072} includes two invalid values (512, 2048) and omits one valid value (384).

# Fix:
VALID_DIMENSIONS = {256, 384, 1024, 3072}

2. str(item) fallback silently embeds garbage

else:
    texts.append(str(item))

If an unexpected type reaches this branch, it silently converts to its string representation (e.g., "{'key': 'value'}") and returns HTTP 200 with meaningless embeddings. This is worse than an error — users won't know their results are wrong. Should raise an error instead:

else:
    raise HTTPException(
        status_code=400,
        detail=f"Unsupported input item type: {type(item).__name__}. Expected str, int, or list of ints.",
    )

Important

3. Stale comment in schema.py:185

The dimensions field comment says # not used. but this PR makes it actively functional for Nova. Please update it, e.g.:

dimensions: int | None = None  # Used by Nova embeddings; ignored by other models.

4. getattr is unnecessary for a Pydantic model field

dimensions = getattr(embeddings_request, "dimensions", None)

EmbeddingsRequest defines dimensions: int | None = None as a Pydantic field. Direct access works and is consistent with how all other attributes are accessed in this codebase:

dimensions = embeddings_request.dimensions

5. Batch failure loses context

Since Nova processes each text as a separate API call, a failure mid-batch gives no indication of which item failed. Consider using enumerate and including the index in error context:

for idx, text in enumerate(texts):
    # ... on error, include idx in the error detail

Suggestions (optional)

  • Move dimension validation before the loop (it's constant across texts but validated per-text currently)
  • Add a comment explaining why embeddingPurpose is hardcoded to "GENERIC_INDEX" (Nova supports 9 different purposes)
  • Consider using isinstance(item, list) instead of isinstance(item, Iterable) for more precise type matching

- Fix VALID_DIMENSIONS to {256, 384, 1024, 3072} per Nova embeddings schema docs
  (previous values 512/2048 were mistakenly referenced from Titan embedding model docs)
- Replace str(item) fallback with HTTPException(400) to avoid silent garbage embeddings
- Update schema.py dimensions comment: 'not used' -> 'Used by Nova embeddings'
- Replace getattr() with direct .dimensions access on Pydantic model
- Move dimension validation before the loop (validates once, not per-text)
- Add enumerate to batch loop; include input index in error detail
- Switch isinstance(item, Iterable) to isinstance(item, list) for precise matching
- Add comment explaining embeddingPurpose hardcoded to GENERIC_INDEX
@zxkane

zxkane commented Feb 26, 2026

Copy link
Copy Markdown
Member

Thanks for the contribution @gabrielkoo! 🎉 Great work adding Nova 2 multimodal embeddings support. The implementation is clean, well-documented, and consistent with the existing codebase. Merging now!

@zxkane
zxkane merged commit d14596f into aws-samples:main Feb 26, 2026
@gabrielkoo
gabrielkoo deleted the feat/nova-embed-v2 branch February 26, 2026 04:15
@pgodzin

pgodzin commented Feb 27, 2026

Copy link
Copy Markdown

@zxkane @gabrielkoo note that this still only supports text embeddings, despite being a multimodal embedding model that supports image and video input.

see #159

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants