Skip to content

feat: wrap AI, Images, Analytics Engine, Vectorize and RateLimit Bindings to accept native Python objects#130

Open
ryanking13 wants to merge 5 commits into
mainfrom
gyeongjae/vectorize-ai
Open

feat: wrap AI, Images, Analytics Engine, Vectorize and RateLimit Bindings to accept native Python objects#130
ryanking13 wants to merge 5 commits into
mainfrom
gyeongjae/vectorize-ai

Conversation

@ryanking13

@ryanking13 ryanking13 commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Now that we have a good BindingWrapper class, these just work. Unfortunately AI and Vectorize bindings require a real prod account even for the local testing, so we cannot run unittest in CI.

I verified them working my making a demo worker with my local account + patched sdk.

Vectorize test worker

This requires 64-dimension DB in the account

import traceback

from workers import Response, WorkerEntrypoint


class Default(WorkerEntrypoint):
    async def fetch(self, request):
        from urllib.parse import urlparse

        path = urlparse(request.url).path

        if path == "/probe":
            vec = self.env.VECTORIZE
            return Response.json({
                "type": str(type(vec)),
                "wrapped": hasattr(vec, "_binding"),
                "has_insert": hasattr(vec, "insert"),
                "has_query": hasattr(vec, "query"),
            })

        if path == "/describe":
            try:
                result = await self.env.VECTORIZE.describe()
                return Response.json({"success": True, "result": result})
            except Exception as e:
                return Response.json({
                    "success": False,
                    "error": str(e),
                    "type": type(e).__name__,
                    "traceback": traceback.format_exc(),
                }, status=500)

        if path == "/insert":
            try:
                vectors = [
                    {"id": "vec-1", "values": [0.1] * 64},
                    {"id": "vec-2", "values": [0.4] * 64},
                ]
                result = await self.env.VECTORIZE.insert(vectors)
                return Response.json({"success": True, "result": result})
            except Exception as e:
                return Response.json({
                    "success": False,
                    "error": str(e),
                    "type": type(e).__name__,
                    "traceback": traceback.format_exc(),
                }, status=500)

        if path == "/query":
            try:
                result = await self.env.VECTORIZE.query(
                    [0.1] * 64,
                    {"topK": 5},
                )
                return Response.json({"success": True, "result": result})
            except Exception as e:
                return Response.json({
                    "success": False,
                    "error": str(e),
                    "type": type(e).__name__,
                    "traceback": traceback.format_exc(),
                }, status=500)

        if path == "/upsert":
            try:
                vectors = [
                    {"id": "vec-3", "values": [0.7] * 64, "metadata": {"label": "test"}},
                ]
                result = await self.env.VECTORIZE.upsert(vectors)
                return Response.json({"success": True, "result": result})
            except Exception as e:
                return Response.json({
                    "success": False,
                    "error": str(e),
                    "type": type(e).__name__,
                    "traceback": traceback.format_exc(),
                }, status=500)

        if path == "/get":
            try:
                result = await self.env.VECTORIZE.getByIds(["vec-1", "vec-2"])
                return Response.json({"success": True, "result": result})
            except Exception as e:
                return Response.json({
                    "success": False,
                    "error": str(e),
                    "type": type(e).__name__,
                    "traceback": traceback.format_exc(),
                }, status=500)

        if path == "/delete":
            try:
                result = await self.env.VECTORIZE.deleteByIds(["vec-1"])
                return Response.json({"success": True, "result": result})
            except Exception as e:
                return Response.json({
                    "success": False,
                    "error": str(e),
                    "type": type(e).__name__,
                    "traceback": traceback.format_exc(),
                }, status=500)

        return Response.json({
            "endpoints": ["/probe", "/describe", "/insert", "/query", "/upsert", "/get", "/delete"],
        })

AI test worker
import json
import traceback

from workers import Response, WorkerEntrypoint


class Default(WorkerEntrypoint):
    async def fetch(self, request):
        from urllib.parse import urlparse

        path = urlparse(request.url).path

        if path == "/probe":
            ai = self.env.AI
            return Response.json({
                "type": str(type(ai)),
                "wrapped": hasattr(ai, "_binding"),
                "has_run": hasattr(ai, "run"),
            })

        if path == "/run":
            try:
                result = await self.env.AI.run(
                    "@cf/meta/llama-3.3-70b-instruct-fp8-fast",
                    {"messages": [{"role": "user", "content": "Say hello in one word"}]},
                )
                return Response.json({"success": True, "result": result})
            except Exception as e:
                return Response.json({
                    "success": False,
                    "error": str(e),
                    "type": type(e).__name__,
                    "traceback": traceback.format_exc(),
                }, status=500)

        if path == "/models":
            try:
                result = await self.env.AI.models()
                return Response.json({"success": True, "result": result})
            except Exception as e:
                return Response.json({
                    "success": False,
                    "error": str(e),
                    "type": type(e).__name__,
                }, status=500)

        return Response.json({
            "endpoints": ["/probe", "/run", "/models"],
        })

@ask-bonk

ask-bonk Bot commented Jun 18, 2026

Copy link
Copy Markdown

The change is straightforward. Let me review the complete picture:

  1. _workers.py: Adds "Ai" and "VectorizeIndexImpl" to the _BINDING_TYPES set. This means when _EnvWrapper.__getattr__ encounters these JS binding types, they'll be wrapped with _BindingWrapper, which auto-converts Python objects to/from JS via python_to_rpc/python_from_rpc.

  2. wrangler.jsonc: Adds a vectorize binding config for tests.

  3. test_vectorize.py: A minimal test that just checks the binding is wrapped. Makes sense given vectorize needs a real remote database.

The change is minimal and clean. The only thing I notice is the absence of a parallel test for the AI binding — but the same rationale applies (AI needs a real account), and arguably if you can't even configure a local AI binding in wrangler, there's nothing to test. The PR description explains this clearly.

The code is correct — "Ai" and "VectorizeIndexImpl" are the JS constructor names used by workerd for these bindings, and adding them to the set is the right mechanism to get _BindingWrapper applied.

LGTM

github run

@ryanking13 ryanking13 changed the title feat: wrap AI bindings to accept native Python objects feat: wrap AI, Images, Analytics Engine, Vectorize and RateLimit Bindings to accept native Python objects Jun 18, 2026
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.

1 participant