Skip to content

Tutorials

Building a Python AI Gateway in Rust

This tutorial walks you through setting up a Rust Gateway that natively executes a Python script.

1. The Python Model

First, create your AI logic in model.py:

class MyModel:
    def process(self, prompt: str) -> str:
        return f"AI Processed: {prompt}"

2. The Rust Server

Next, in your Rust project, add helix-rt and tokio:

[dependencies]
helix-rt = "0.1.0"
tokio = { version = "1.0", features = ["full"] }

In src/main.rs:

use helix_rt::server::{HelixServer, RestRoute};
use helix_rt::pyo3_runner::PyModelHandler;
use std::sync::Arc;

#[tokio::main]
async fn main() {
    // Point the PyModelHandler to `model.py`, and the class `MyModel`
    let py_handler = PyModelHandler::new(".", "model", "MyModel").unwrap();

    let mut server = HelixServer::new(
        "127.0.0.1:8080",
        Arc::new(py_handler),
        vec![RestRoute::new("POST", "/predict", "/predict")],
    );

    println!("Starting Rust AI Gateway...");
    server.start().await.unwrap();
}

3. Run It!

cargo run
You can now curl -X POST http://127.0.0.1:8080/predict -d '{"prompt": "Hello"}' and it will securely and instantly cross the FFI boundary, execute the Python code, and return the result!


Setting up Advanced Features

Deadline Propagation (grpc-timeout)

When building microservices, you want to ensure your AI model stops processing if the client has already timed out. Helix RPC automatically extracts the grpc-timeout header and applies it to the Go context.Context and Rust tokio::time::timeout.

Client Side (cURL):

# Send a 50-millisecond timeout
curl -H "grpc-timeout: 50m" -X POST http://127.0.0.1:8080/predict -d '{"prompt": "Hello"}'

Server Side (Go):

// Inside your handler, listen for context cancellation
func(ctx context.Context, req interface{}) (interface{}, error) {
    select {
    case <-ctx.Done():
        // Client timed out! Abort GPU processing to save resources.
        return nil, ctx.Err()
    case res := <-processOnGPU(req):
        return res, nil
    }
}

Per-Message Compression (Gzip)

To enable Gzip compression on large JSON/Protobuf responses, register the compressor when starting your server.

Go:

import runtime "github.com/helixrpc/helix-rt"

server := runtime.NewServer(":8080")

// Register the Gzip Compressor
server.RegisterCompressor(runtime.NewGzipCompressor())

server.Start()
Now, any client sending grpc-encoding: gzip will automatically have their request decompressed, and the response compressed natively by the gateway!

Multi-Protocol Endpoints

The Go Runtime is capable of natively binding gRPC, Thrift, and REST JSON onto the exact same port and the exact same route logic using h2c.

server := runtime.NewServer(":8080")

// 1. Register the underlying handler (Used by gRPC and Thrift natively)
server.RegisterMethod("/v1.ModelService/Predict", runtime.MethodInfo{
    Decoder: myProtobufDecoder,
    Handler: myPredictionLogic,
})

// 2. Map a REST Route to the EXACT SAME METHOD
server.RegisterRESTRoute(
    "POST",                  // HTTP Method
    "/v1/models/predict",    // REST Path
    "/v1.ModelService/Predict", // Target Method
)

server.Start()
Your server can now seamlessly handle standard HTTP POST /v1/models/predict requests with JSON, as well as high-performance HTTP/2 Protobuf frames sent by gRPC clients!


Setting up Node.js / TypeScript Server

Here is a quick-start guide to setting up a TypeScript-based Helix server using the Node.js runtime.

import { HelixServer } from 'helix-rt-node';

const server = new HelixServer('127.0.0.1:8080');

// Register the underlying method handler
server.registerMethod('/helix.example.UserProfileService/GetUserProfile', {
    Decoder: (dec) => {
        const req = { userId: 0, username: '' };
        dec(req);
        return req;
    },
    Handler: async (ctx, req) => {
        return {
            userId: req.userId,
            username: req.username + '-response',
            email: 'verified@example.com'
        };
    }
});

// Map REST HTTP/1.1 endpoint route to the same method
server.registerRESTRoute('POST', '/v1/users', '/helix.example.UserProfileService/GetUserProfile');

console.log('Starting Node.js Helix Server...');
await server.start();

Code Generation & IDL Compilation (Protobuf & Thrift)

Helix RPC allows you to define your schemas once using standard Protobuf (.proto) or Apache Thrift (.thrift) IDL formats and generate high-performance stubs.

1. Define Your Schemas

Protobuf (user.proto):

syntax = "proto3";
package user;

message UserRequest {
  int64 user_id = 1;
}

message UserResponse {
  int64 user_id = 1;
  string name = 2;
}

service UserService {
  rpc GetUser (UserRequest) returns (UserResponse);
}

Thrift (user.thrift):

namespace go user

struct UserRequest {
  1: i64 user_id;
}

struct UserResponse {
  1: i64 user_id;
  2: string name;
}

service UserService {
  UserResponse GetUser(1: UserRequest request)
}

2. Compile Stubs using helix-gen

Generate stubs for Go, Rust, or Python using the unified compiler binary:

# Compile Protobuf IDL to Rust stubs
helix-gen -idl user.proto -lang rust -out ./src/generated.rs

# Compile Thrift IDL to Go stubs
helix-gen -idl user.thrift -lang go -out ./generated/

3. Serving Dual-Protocol (gRPC & Thrift) in Go

The compiled stubs handle serialization automatically. The Go sniffer routes incoming connections:

package main

import (
    "context"
    "log"

    runtime "github.com/helixrpc/helix-rt"
    generated "github.com/helix-rpc/helix/generated"
)

type UserHandler struct{}

func (h *UserHandler) GetUser(ctx context.Context, req *generated.UserRequest) (*generated.UserResponse, error) {
    return &generated.UserResponse{
        UserId: req.UserId,
        Name:   "Alex",
    }, nil
}

func main() {
    server := runtime.NewServer(":8080")
    handler := &UserHandler{}

    // Register generated method handlers
    server.RegisterMethod("/user.UserService/GetUser", runtime.MethodInfo{
        Decoder: func(dec func(interface{}) error) (interface{}, error) {
            req := &generated.UserRequest{}
            return req, dec(req)
        },
        Handler: func(ctx context.Context, req interface{}) (interface{}, error) {
            return handler.GetUser(ctx, req.(*generated.UserRequest))
        },
    })

    log.Println("Starting dual-protocol (gRPC/Thrift) Server on :8080...")
    log.Fatal(server.Start())
}

Unified Runtime Reference (All Languages)

To make it easy to adopt Helix, here is how you implement and run a standard server executing the exact same UserProfileService method (/helix.example.UserProfileService/GetUserProfile) across all four supported runtimes:

1. Go (runtimes/go)

package main

import (
    "context"
    "log"

    runtime "github.com/helix-rpc/helix/runtime-go"
)

func main() {
    server := runtime.NewServer(":8080")

    server.RegisterMethod("/helix.example.UserProfileService/GetUserProfile", runtime.MethodInfo{
        Decoder: func(dec func(interface{}) error) (interface{}, error) {
            req := &struct{ UserId int64 }{}
            return req, dec(req)
        },
        Handler: func(ctx context.Context, req interface{}) (interface{}, error) {
            return map[string]interface{}{
                "userId":   123,
                "username": "alex",
                "email":    "alex@example.com",
            }, nil
        },
    })

    log.Println("Starting Go Helix Server on :8080...")
    log.Fatal(server.Start())
}

2. Rust (runtimes/rust)

use std::sync::Arc;
use helix_rt::{HttpServiceHandler, RestRoute, handle_http_connection};

struct UserProfileService;

#[async_trait::async_trait]
impl HttpServiceHandler for UserProfileService {
    async fn handle_request(&self, path: &str, body: Vec<u8>, is_json: bool)
        -> Result<(Vec<u8>, String), String> 
    {
        if path == "/helix.example.UserProfileService/GetUserProfile" {
            let resp = serde_json::json!({
                "userId": 123,
                "username": "alex",
                "email": "alex@example.com"
            });
            Ok((serde_json::to_vec(&resp).unwrap(), "application/json".to_string()))
        } else {
            Err(format!("unknown path: {}", path))
        }
    }
}

3. Node.js / TypeScript (runtimes/node)

import { HelixServer } from 'helix-rt-node';

const server = new HelixServer('127.0.0.1:8080');

server.registerMethod('/helix.example.UserProfileService/GetUserProfile', {
    Decoder: (dec) => {
        const req = { userId: 0 };
        dec(req);
        return req;
    },
    Handler: async (ctx, req) => {
        return {
            userId: 123,
            username: "alex",
            email: "alex@example.com"
        };
    }
});

console.log("Starting Node.js Helix Server on :8080...");
await server.start();

4. Python (runtimes/python)

import asyncio
from helix_rt import HelixServer, MethodInfo

async def handle_get_profile(ctx, req):
    return {
        "userId": 123,
        "username": "alex",
        "email": "alex@example.com"
    }

async def main():
    server = HelixServer("127.0.0.1:8080")

    server.register_method(
        "/helix.example.UserProfileService/GetUserProfile",
        MethodInfo(
            decoder=lambda data: data, # Pass-through
            handler=handle_get_profile
        )
    )

    print("Starting Python Helix Server on :8080...")
    await server.start()

if __name__ == "__main__":
    asyncio.run(main())