Skip to content

OpenAI-compatible API server

tdfs4ds can expose its two conversational agents over HTTP as an OpenAI-compatible API, so any OpenAI client (the openai Python SDK, Open WebUI, LibreChat, …) can talk to them. A LangServe playground is mounted alongside for interactive debugging.

The server publishes the two agents as two OpenAI models:

Model id Agent Purpose
tdfs4ds-consumer consumer_agent Business-user Q&A over the feature store / business dictionary.
tdfs4ds-query-optimizer query_optimizer_agent EXPLAIN / simplify / optimize a Teradata SQL query.

The OpenAI model field of each request selects the agent.


Prerequisites

Choose how you want to run the server:

Only Docker is required on the host. No Python, no pip install.

The image bundles everything — the package, all dependencies, and the server entry point. All you need is the repo checkout (for the Dockerfile and .env.example) and a working Docker installation.

# Verify Docker is available
docker --version

Install the [serve] extra, which adds FastAPI, uvicorn, and LangServe on top of the core package:

pip install "tdfs4ds[serve]"

This also installs pyvis (lineage graph), gradio (chatbot UI), and langserve (playground routes). These are kept as an optional extra so the base pip install tdfs4ds stays lean for notebook/script use.


Configure

All configuration is supplied through TDFS4DS_* environment variables (or a .env file). Copy .env.example and fill it in:

# Teradata connection (read by the server bootstrap)
TDFS4DS_TD_HOST=your-vantage-host
TDFS4DS_TD_USERNAME=your_user
TDFS4DS_TD_PASSWORD=your_password
# TDFS4DS_TD_LOGMECH=LDAP        # optional
# TDFS4DS_TD_DATABASE=your_db    # optional
# TDFS4DS_TD_ENCRYPT=true        # optional wire encryption ('true' or 'false')

# Feature store scope
TDFS4DS_SCHEMA=your_feature_store_db
# TDFS4DS_SCHEMA_VIEW=your_views_db   # optional: separate database for views (blank = same as SCHEMA)
TDFS4DS_DATA_DOMAIN=your_project

# Instruct (LLM) model
TDFS4DS_INSTRUCT_MODEL_PROVIDER=openai
TDFS4DS_INSTRUCT_MODEL_MODEL=gpt-4o
TDFS4DS_INSTRUCT_MODEL_API_KEY=sk-...

# Embedding model (needed for the consumer agent's search)
TDFS4DS_EMBEDDING_MODEL_PROVIDER=openai
TDFS4DS_EMBEDDING_MODEL_MODEL=text-embedding-3-small
TDFS4DS_EMBEDDING_MODEL_API_KEY=sk-...
TDFS4DS_EMBEDDING_MODEL_DIM=1536

# Vector store + server behaviour
TDFS4DS_VECTOR_STORE_BACKEND=chroma
TDFS4DS_CHROMA_PATH=/data/chroma
TDFS4DS_SERVE_PORT=8000
TDFS4DS_SERVE_BUILD_INDEX=true     # set false for a query-optimizer-only deployment

# Vector Store authentication (only needed for VECTOR_STORE_BACKEND=teradata)
# TDFS4DS_VECTOR_STORE_AUTH_BASE_URL=https://your-vantage-vs-endpoint
# TDFS4DS_VECTOR_STORE_AUTH_AUTH_TOKEN=...      # JWT — a token you already hold (tried first);
#                                               # in-cluster, map the platform's injected token
#                                               # onto it, e.g. =$OAUTH_ACCESS_TOKEN
# TDFS4DS_VECTOR_STORE_AUTH_PAT_TOKEN=...       # PAT
# TDFS4DS_VECTOR_STORE_AUTH_PEM_FILE=/path/to/key.pem
# TDFS4DS_VECTOR_STORE_AUTH_USERNAME=your_user  # optional for PAT, required for Basic
# TDFS4DS_VECTOR_STORE_AUTH_PASSWORD=...        # Basic
# TDFS4DS_VECTOR_STORE_AUTH_CLIENT_ID=...       # OAuth

Verify a deployment's configuration

tdfs4ds.check_configuration() validates Vector Store auth, the Vector Store service, the instruct model and the embedding model in one call, and names the setting to change for anything that fails — useful as a container smoke test before serving traffic. It is also available in the admin UI under Parameters → Configuration check. See Checking your configuration.

Teradata connection vs. feature-store config

The TDFS4DS_TD_* group (HOST, USERNAME, PASSWORD, and optionally LOGMECH, DATABASE, ENCRYPT) and the TDFS4DS_VECTOR_STORE_AUTH_* group are both read directly by the server's startup bootstrap — the former to open the teradataml context, the latter to authenticate against the Vantage Vector Store REST API (only relevant when VECTOR_STORE_BACKEND=teradata; see Vector Store authentication). Everything else is loaded by tdfs4ds itself — see Environment variables and .env file for the full list and precedence rules.

On startup the server opens the Teradata context, applies the TDFS4DS_* config, and (unless TDFS4DS_SERVE_BUILD_INDEX=false) updates the consumer agent's vector index. Since 0.3.3.15 the update runs in the background: the server answers at once, the status bar at the bottom of the admin UI shows the build with its progress, and a search meanwhile reads what is already indexed.


Run locally

python -m tdfs4ds.serve

The server listens on 0.0.0.0:8000 (override with TDFS4DS_SERVE_PORT). The admin web UI and the embedded chatbot are served on the same port as the API; a standalone chatbot and the docs server (both default enabled) start on their own ports.

Port Service URL
8000 Admin web UI (Teradata-styled shell) http://localhost:8000/
8000 Chatbot (embedded Gradio iframe) http://localhost:8000/chatbot
8000 API (OpenAI /v1 + LangServe + /reports) http://localhost:8000/v1
8000 Lineage graph http://localhost:8000/lineage
7860 Standalone chatbot (Gradio, backward-compat) http://localhost:7860
8080 Documentation (static HTML) http://localhost:8080

The admin UI at / reproduces the Teradata "Tera" console. Its left rail is grouped by theme: Assistant (Chatbot, Skills, Evaluation), Explore (Report, EFS map, Lineage), Documentation (Coverage, Objects), Operations (Execution plan, Migration, Maintenance), Performance (Query optimization, Benchmark) and Settings (Parameters). Hovering an entry says what it does. The Chatbot entry embeds the Gradio chat at /chatbot. Toggle the standalone chatbot with TDFS4DS_SERVE_CHATBOT (default true, TDFS4DS_SERVE_CHATBOT_PORT default 7860) and the docs server with TDFS4DS_SERVE_DOCS (default true).

Behind a reverse-proxy path prefix

To serve the whole UI + API under a sub-path (e.g. https://api-dmproject.myddns.me/tdfs4ds2), set TDFS4DS_SERVE_ROOT_PATH to the prefix:

TDFS4DS_SERVE_ROOT_PATH=/tdfs4ds2 python -m tdfs4ds.serve

create_app installs a lightweight ASGI middleware that strips the prefix from inbound paths and stashes the external base on each request, so the prefix-less shell / /ui/* / /v1/* routes match whether or not the reverse proxy already stripped it. The shell injects <base href="{prefix}/"> so all assets, fetch() calls and the /chatbot iframe resolve under the prefix. Gradio is mounted at the full prefixed path ({prefix}/chatbot) so its internal routing stays consistent. Configure your reverse proxy to forward the prefix unchanged (e.g. nginx location /tdfs4ds2/ { proxy_pass http://app:8000/tdfs4ds2/; }). Leave TDFS4DS_SERVE_ROOT_PATH blank to serve at the root.


Run with Docker

Quickstart

# 1. Clone the repo (skip if you already have it)
git clone https://github.com/denismolin/tdfs4ds.git
cd tdfs4ds

# 2. Create your .env from the template and fill in your values
cp .env.example .env

# 3. Build the image and start all services
docker compose up --build

That's it. docker compose up --build copies the working tree into the image, installs tdfs4ds[serve] and all dependencies inside it, and starts the app on port 8000 (admin UI + chatbot + API), the standalone chatbot on 7860, and the docs server on 8080. No Python or pip install needed on the host.

Manual docker run (without Compose)

docker build -t tdfs4ds-serve .
docker run --env-file .env \
  -p 8000:8000 \
  -p 7860:7860 \
  -p 8080:8080 \
  -v ./docs:/app/docs:ro \
  -v tdfs4ds_chroma:/data \
  tdfs4ds-serve

The /data volume persists the Chroma vector index across restarts. The ./docs bind mount allows mkdocs build on your host to update the documentation server without restarting the container.

Toggle the standalone chatbot and docs server with the TDFS4DS_SERVE_CHATBOT and TDFS4DS_SERVE_DOCS environment variables (both default true).

Build and push to Docker Hub

The repo ships two Dockerfiles:

File Source Use
Dockerfile the working tree (COPY . /app) local development / unreleased changes
Dockerfile.pypi the published wheel (pip install) building a release image to publish

Dockerfile.pypi installs the released package straight from PyPI, so no repo checkout is needed and the build is reproducible from the version tag alone:

FROM python:3.11-slim
RUN apt-get update \
    && apt-get install -y --no-install-recommends build-essential \
    && rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir "tdfs4ds[serve]"   # or pin: "tdfs4ds[serve]==0.3.1.108"
ENV TDFS4DS_CHROMA_PATH=/data/chroma
ENV TDFS4DS_SERVE_PORT=8000
EXPOSE 8000 7860 8080
VOLUME ["/data"]
CMD ["python", "-m", "tdfs4ds.serve"]

Build, tag, and push it to your Docker Hub account (replace you with your Docker Hub username and use the tdfs4ds version as the tag):

# 1. Build from the PyPI-based Dockerfile, tagging with the package version.
docker build -f Dockerfile.pypi -t you/tdfs4ds-serve:0.3.1.81 .
docker tag you/tdfs4ds-serve:0.3.1.81 you/tdfs4ds-serve:latest

# 2. Authenticate (once per machine; use a Docker Hub access token, not your password).
docker login -u you

# 3. Push both tags.
docker push you/tdfs4ds-serve:0.3.1.81
docker push you/tdfs4ds-serve:latest

Build for the server's architecture

If your dev machine is ARM (Apple Silicon) and your server is x86-64, build a multi-arch image with buildx so the server can pull a matching layer — this also pushes in one step:

docker buildx build -f Dockerfile.pypi \
  --platform linux/amd64,linux/arm64 \
  -t you/tdfs4ds-serve:0.3.1.81 -t you/tdfs4ds-serve:latest \
  --push .

On the target host, pull and run it with that host's .env:

docker pull you/tdfs4ds-serve:0.3.1.108
docker run --env-file .env -p 8000:8000 \
  -v tdfs4ds_chroma:/data you/tdfs4ds-serve:0.3.1.108

The image carries no secrets — every credential and connection setting is supplied at run time through --env-file / TDFS4DS_* variables — so the same image is safe to publish as a public or private Docker Hub repository.


Deploy to the cloud

The image carries no secrets — credentials are always passed at run time via environment variables — so the same image works on any container platform. The steps below use the Docker Hub image (you/tdfs4ds-serve:latest) built and pushed in the previous section. Replace you with your registry path.

Chroma is for development only — use the Teradata vector store in production

The default TDFS4DS_VECTOR_STORE_BACKEND=chroma stores the vector index as a local directory inside the container (/data/chroma). This is convenient for local development and demos but not suitable for production cloud deployments:

  • The index is lost every time the container restarts unless you mount a persistent volume.
  • Local Chroma cannot be shared across container replicas.

For production, switch to the Teradata backend:

TDFS4DS_VECTOR_STORE_BACKEND=teradata

This stores the vector index inside Teradata Vantage — the same system you already have, fully persistent, accessible from any replica, and requiring no extra cloud storage. See Configuration › LLM & embeddings for the TERADATA_VECTOR_STORE_PREFIX and TERADATA_AI_* variables.

Secrets management

Avoid putting .env files on cloud hosts. Each platform has a native secrets store — the commands below show how to wire them in.

Services used: ECR (image registry), ECS Fargate (serverless container runtime), Secrets Manager (credentials), ALB (HTTPS termination).

1 — Push the image to ECR

# Create a private ECR repository (once)
aws ecr create-repository --repository-name tdfs4ds-serve --region eu-west-1

# Log Docker into ECR
aws ecr get-login-password --region eu-west-1 \
  | docker login --username AWS --password-stdin \
      123456789012.dkr.ecr.eu-west-1.amazonaws.com

# Tag and push
docker tag you/tdfs4ds-serve:latest \
  123456789012.dkr.ecr.eu-west-1.amazonaws.com/tdfs4ds-serve:latest
docker push \
  123456789012.dkr.ecr.eu-west-1.amazonaws.com/tdfs4ds-serve:latest

2 — Store secrets in Secrets Manager

aws secretsmanager create-secret \
  --name tdfs4ds/env \
  --secret-string '{
    "TDFS4DS_TD_HOST":       "your-vantage-host",
    "TDFS4DS_TD_USERNAME":   "your_user",
    "TDFS4DS_TD_PASSWORD":   "your_password",
    "TDFS4DS_SCHEMA":        "your_feature_store_db",
    "TDFS4DS_DATA_DOMAIN":   "your_project",
    "TDFS4DS_INSTRUCT_MODEL_PROVIDER": "bedrock",
    "TDFS4DS_INSTRUCT_MODEL_MODEL":    "anthropic.claude-3-5-sonnet-20241022-v2:0",
    "TDFS4DS_INSTRUCT_MODEL_REGION":   "eu-west-1",
    "TDFS4DS_EMBEDDING_MODEL_PROVIDER":"bedrock",
    "TDFS4DS_EMBEDDING_MODEL_MODEL":   "amazon.titan-embed-text-v2:0",
    "TDFS4DS_EMBEDDING_MODEL_REGION":  "eu-west-1",
    "TDFS4DS_VECTOR_STORE_BACKEND":    "teradata"
  }'

Set TDFS4DS_VECTOR_STORE_BACKEND=teradata (recommended for production) — vectors are stored inside Vantage, no EFS mount needed.

3 — Create an ECS task definition

Save the JSON below as task-def.json, substituting your account ID, region, and secret ARN:

{
  "family": "tdfs4ds-serve",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "1024",
  "memory": "2048",
  "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
  "containerDefinitions": [{
    "name": "tdfs4ds-serve",
    "image": "123456789012.dkr.ecr.eu-west-1.amazonaws.com/tdfs4ds-serve:latest",
    "portMappings": [{"containerPort": 8000, "protocol": "tcp"}],
    "secrets": [{
      "name": "TDFS4DS_TD_HOST",
      "valueFrom": "arn:aws:secretsmanager:eu-west-1:123456789012:secret:tdfs4ds/env:TDFS4DS_TD_HOST::"
    }],
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "/ecs/tdfs4ds-serve",
        "awslogs-region": "eu-west-1",
        "awslogs-stream-prefix": "ecs"
      }
    }
  }]
}

Tip

ECS Fargate can also inject an entire Secrets Manager JSON blob as individual environment variables using the valueFrom: "arn:...:secret:tdfs4ds/env:::" wildcard form — one entry per key. This avoids repeating the ARN for every variable.

aws ecs register-task-definition --cli-input-json file://task-def.json

4 — Create the service

aws ecs create-service \
  --cluster your-cluster \
  --service-name tdfs4ds-serve \
  --task-definition tdfs4ds-serve \
  --launch-type FARGATE \
  --desired-count 1 \
  --network-configuration "awsvpcConfiguration={
      subnets=[subnet-aaa,subnet-bbb],
      securityGroups=[sg-xxx],
      assignPublicIp=ENABLED}" \
  --load-balancers "targetGroupArn=arn:aws:elasticloadbalancing:...,
      containerName=tdfs4ds-serve,containerPort=8000"

The ALB target group handles HTTPS termination; the container only speaks HTTP on port 8000.

Connecting to Teradata

The container opens a JDBC/ODBC connection to TDFS4DS_TD_HOST on port 1025 (Teradata default). Fargate tasks run inside a VPC subnet, so Vantage must be reachable from that subnet.

Vantage location Connectivity approach
AWS-hosted (e.g. ClearScape on EC2, Vantage on AWS) Place the ECS task and the Vantage host in the same VPC, or use VPC peering between the two VPCs. Add a security-group rule: inbound TCP 1025 from the ECS task's security group.
On-premises Connect the VPC to your data-center via AWS Direct Connect or Site-to-Site VPN. The Fargate task then reaches Vantage over the private link.
ClearScape Analytics Experience (SaaS trial) The host is internet-reachable. Set TDFS4DS_TD_ENCRYPT=true and allow outbound TCP 1025 from the task's security group to the ClearScape hostname.

Tip

Set TDFS4DS_TD_ENCRYPT=true whenever the connection crosses a network boundary you do not fully control (VPN, Direct Connect, public internet).

Bedrock credentials

With INSTRUCT_MODEL_PROVIDER=bedrock the SDK auto-discovers credentials from the task's IAM execution role — no API key needed. Attach the AmazonBedrockFullAccess managed policy (or a scoped equivalent) to the ECS task role and remove INSTRUCT_MODEL_API_KEY from the secret.

Required AWS permissions

Two principals need permissions: the deployer (the human or CI role running the commands above) and the ECS task (the runtime identity of the container).

Deployer IAM policy — attach to the human/CI role that runs steps 1–4:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ECR",
      "Effect": "Allow",
      "Action": [
        "ecr:CreateRepository",
        "ecr:GetAuthorizationToken",
        "ecr:BatchCheckLayerAvailability",
        "ecr:InitiateLayerUpload",
        "ecr:UploadLayerPart",
        "ecr:CompleteLayerUpload",
        "ecr:PutImage"
      ],
      "Resource": "*"
    },
    {
      "Sid": "SecretsManager",
      "Effect": "Allow",
      "Action": [
        "secretsmanager:CreateSecret",
        "secretsmanager:PutSecretValue",
        "secretsmanager:DescribeSecret"
      ],
      "Resource": "arn:aws:secretsmanager:*:*:secret:tdfs4ds/*"
    },
    {
      "Sid": "ECS",
      "Effect": "Allow",
      "Action": [
        "ecs:RegisterTaskDefinition",
        "ecs:CreateService",
        "ecs:UpdateService",
        "ecs:DescribeServices",
        "ecs:DescribeTaskDefinition",
        "ecs:ListTaskDefinitions"
      ],
      "Resource": "*"
    },
    {
      "Sid": "IAMPassRole",
      "Effect": "Allow",
      "Action": "iam:PassRole",
      "Resource": "arn:aws:iam::*:role/ecsTaskExecutionRole"
    },
    {
      "Sid": "CloudWatchLogs",
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:DescribeLogGroups"
      ],
      "Resource": "*"
    }
  ]
}

ECS task execution role (ecsTaskExecutionRole) — the standard AWS managed policy AmazonECSTaskExecutionRolePolicy covers ECR pull and CloudWatch Logs. Add one inline policy to let the task read secrets:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "ReadSecrets",
    "Effect": "Allow",
    "Action": [
      "secretsmanager:GetSecretValue",
      "ssm:GetParameters"
    ],
    "Resource": "arn:aws:secretsmanager:*:*:secret:tdfs4ds/*"
  }]
}

ECS task role (the identity inside the container at runtime) — needed only when using Bedrock without an API key:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "Bedrock",
    "Effect": "Allow",
    "Action": [
      "bedrock:InvokeModel",
      "bedrock:InvokeModelWithResponseStream"
    ],
    "Resource": "*"
  }]
}

Least privilege

Scope Resource to the specific model ARNs you use, e.g. arn:aws:bedrock:eu-west-1::foundation-model/anthropic.claude-3-5-sonnet-20241022-v2:0.

Services used: Artifact Registry (image registry), Cloud Run (serverless container runtime), Secret Manager (credentials).

1 — Push the image to Artifact Registry

# Create a repository (once)
gcloud artifacts repositories create tdfs4ds \
  --repository-format docker \
  --location europe-west1

# Authenticate Docker
gcloud auth configure-docker europe-west1-docker.pkg.dev

# Tag and push
docker tag you/tdfs4ds-serve:latest \
  europe-west1-docker.pkg.dev/your-project/tdfs4ds/tdfs4ds-serve:latest
docker push \
  europe-west1-docker.pkg.dev/your-project/tdfs4ds/tdfs4ds-serve:latest

2 — Store secrets in Secret Manager

# Create each secret (repeat for every variable)
echo -n "your-vantage-host" \
  | gcloud secrets create TDFS4DS_TD_HOST --data-file=-

echo -n "your_password" \
  | gcloud secrets create TDFS4DS_TD_PASSWORD --data-file=-

# ... repeat for TD_USERNAME, SCHEMA, DATA_DOMAIN,
#     INSTRUCT_MODEL_*, EMBEDDING_MODEL_*, etc.

Set TDFS4DS_VECTOR_STORE_BACKEND=teradata (recommended for production) — vectors are stored inside Vantage, no Filestore / Persistent Disk mount needed.

3 — Deploy to Cloud Run

gcloud run deploy tdfs4ds-serve \
  --image europe-west1-docker.pkg.dev/your-project/tdfs4ds/tdfs4ds-serve:latest \
  --region europe-west1 \
  --platform managed \
  --port 8000 \
  --memory 2Gi \
  --cpu 1 \
  --min-instances 1 \
  --no-allow-unauthenticated \
  --set-secrets \
      TDFS4DS_TD_HOST=TDFS4DS_TD_HOST:latest,\
      TDFS4DS_TD_USERNAME=TDFS4DS_TD_USERNAME:latest,\
      TDFS4DS_TD_PASSWORD=TDFS4DS_TD_PASSWORD:latest,\
      TDFS4DS_SCHEMA=TDFS4DS_SCHEMA:latest,\
      TDFS4DS_DATA_DOMAIN=TDFS4DS_DATA_DOMAIN:latest,\
      TDFS4DS_INSTRUCT_MODEL_PROVIDER=TDFS4DS_INSTRUCT_MODEL_PROVIDER:latest,\
      TDFS4DS_INSTRUCT_MODEL_MODEL=TDFS4DS_INSTRUCT_MODEL_MODEL:latest,\
      TDFS4DS_VECTOR_STORE_BACKEND=TDFS4DS_VECTOR_STORE_BACKEND:latest

--no-allow-unauthenticated restricts access to callers with a valid Google identity token — add --allow-unauthenticated only if the service is internet-facing and you control access another way (API gateway, VPC, etc.).

Connecting to Teradata

Cloud Run tasks run on Google-managed infrastructure. Port 1025 must be reachable from that infrastructure to your Vantage system.

Vantage location Connectivity approach
GCP-hosted (e.g. Vantage on GCP, Compute Engine) Use Direct VPC egress (--vpc-connector or --network / --subnet flags) to route Cloud Run traffic into your VPC. Add a firewall rule: allow TCP 1025 from the Cloud Run service account or subnet.
On-premises Connect your GCP VPC to the data-center via Cloud Interconnect or Cloud VPN, then use Direct VPC egress.
ClearScape Analytics Experience (SaaS trial) The host is internet-reachable. Cloud Run has outbound internet access by default. Set TDFS4DS_TD_ENCRYPT=true.
# Example: enable Direct VPC egress for an existing Cloud Run service
gcloud run services update tdfs4ds-serve \
  --region europe-west1 \
  --network your-vpc-network \
  --subnet your-subnet \
  --vpc-egress all-traffic

Vertex AI credentials

With INSTRUCT_MODEL_PROVIDER=gcp the SDK uses Application Default Credentials from the Cloud Run service account — no API key needed. Grant the service account the Vertex AI User IAM role and set:

TDFS4DS_INSTRUCT_MODEL_GCP_PROJECT=your-project
TDFS4DS_INSTRUCT_MODEL_GCP_LOCATION=europe-west1
TDFS4DS_INSTRUCT_MODEL_MODEL=gemini-2.0-flash-001

Accessing the service

Cloud Run issues a https://<hash>-<region>.a.run.app URL. Callers must include a bearer token:

TOKEN=$(gcloud auth print-identity-token)
curl -H "Authorization: Bearer $TOKEN" \
  https://tdfs4ds-serve-<hash>-ew.a.run.app/v1/models

For service-to-service calls within GCP, use Workload Identity.

Required GCP permissions

Two principals need roles: the deployer (human or CI service account running steps 1–3) and the Cloud Run service account (the runtime identity of the container).

Deployer — roles to grant at project level:

Role Purpose
roles/artifactregistry.admin Create repository, push images
roles/secretmanager.admin Create and version secrets
roles/run.admin Deploy and update Cloud Run services
roles/iam.serviceAccountUser Allow the deployer to act as the Cloud Run service account (--service-account)
roles/compute.networkViewer Required when enabling VPC egress (read VPC/subnet details)
# Grant all deployer roles to a CI service account
SA="deployer@your-project.iam.gserviceaccount.com"
for ROLE in \
    roles/artifactregistry.admin \
    roles/secretmanager.admin \
    roles/run.admin \
    roles/iam.serviceAccountUser \
    roles/compute.networkViewer; do
  gcloud projects add-iam-policy-binding your-project \
    --member="serviceAccount:$SA" --role="$ROLE"
done

Cloud Run service account — the identity the container runs as at runtime. Create a dedicated one (recommended over using the default compute service account):

gcloud iam service-accounts create tdfs4ds-runtime \
  --display-name "tdfs4ds Cloud Run runtime"

Grant it the minimum roles it needs at runtime:

Role Purpose
roles/secretmanager.secretAccessor Read the secrets injected as env vars
roles/aiplatform.user Call Vertex AI (Gemini / embeddings) — only if using INSTRUCT_MODEL_PROVIDER=gcp
roles/artifactregistry.reader Pull the container image from Artifact Registry
RT="tdfs4ds-runtime@your-project.iam.gserviceaccount.com"
for ROLE in \
    roles/secretmanager.secretAccessor \
    roles/aiplatform.user \
    roles/artifactregistry.reader; do
  gcloud projects add-iam-policy-binding your-project \
    --member="serviceAccount:$RT" --role="$ROLE"
done

Then pass --service-account=$RT to gcloud run deploy.

Least privilege

If you are not using Vertex AI (e.g. using an OpenAI-compatible endpoint instead), omit roles/aiplatform.user. If secrets are already set from a previous deploy, the deployer only needs roles/run.admin for updates.

Services used: Azure Container Registry (ACR), Azure Container Apps (ACA), Key Vault (secrets).

1 — Push the image to ACR

# Create ACR (once)
az acr create --name youracr --resource-group your-rg \
  --sku Basic --admin-enabled true

# Log Docker in
az acr login --name youracr

# Tag and push
docker tag you/tdfs4ds-serve:latest \
  youracr.azurecr.io/tdfs4ds-serve:latest
docker push youracr.azurecr.io/tdfs4ds-serve:latest

2 — Store secrets in Key Vault

az keyvault create --name your-kv --resource-group your-rg \
  --location westeurope

az keyvault secret set --vault-name your-kv \
  --name TDFS4DS-TD-HOST     --value "your-vantage-host"
az keyvault secret set --vault-name your-kv \
  --name TDFS4DS-TD-PASSWORD --value "your_password"
# ... repeat for USERNAME, SCHEMA, DATA_DOMAIN, model config, etc.

3 — Create a Container Apps environment

az containerapp env create \
  --name tdfs4ds-env \
  --resource-group your-rg \
  --location westeurope

4 — Deploy the container app

Container Apps injects Key Vault secrets as environment variables via a managed identity. First create the app, then bind each secret:

az containerapp create \
  --name tdfs4ds-serve \
  --resource-group your-rg \
  --environment tdfs4ds-env \
  --image youracr.azurecr.io/tdfs4ds-serve:latest \
  --registry-server youracr.azurecr.io \
  --target-port 8000 \
  --ingress external \
  --min-replicas 1 \
  --max-replicas 3 \
  --cpu 1.0 --memory 2.0Gi \
  --secrets \
      td-host=keyvaultref:https://your-kv.vault.azure.net/secrets/TDFS4DS-TD-HOST,identityref:/subscriptions/.../resourceGroups/your-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/your-mi \
      td-password=keyvaultref:https://your-kv.vault.azure.net/secrets/TDFS4DS-TD-PASSWORD,identityref:/subscriptions/... \
  --env-vars \
      TDFS4DS_TD_HOST=secretref:td-host \
      TDFS4DS_TD_PASSWORD=secretref:td-password \
      TDFS4DS_VECTOR_STORE_BACKEND=teradata \
      TDFS4DS_INSTRUCT_MODEL_PROVIDER=azure \
      TDFS4DS_INSTRUCT_MODEL_MODEL=gpt-4o \
      TDFS4DS_INSTRUCT_MODEL_URL=https://your-instance.openai.azure.com/ \
      TDFS4DS_INSTRUCT_MODEL_AZURE_DEPLOYMENT=gpt-4o

Connecting to Teradata

Container Apps run inside an ACA environment. Port 1025 must be reachable from that environment to your Vantage system.

Vantage location Connectivity approach
Azure-hosted (e.g. Vantage on Azure, Azure VM) Deploy the Container Apps environment in a custom VNet (--infrastructure-subnet-resource-id). Add a network security group (NSG) rule: allow outbound TCP 1025 to the Vantage host or subnet.
On-premises Connect the Azure VNet to your data-center via ExpressRoute or Azure VPN Gateway, then use the same custom-VNet Container Apps environment.
ClearScape Analytics Experience (SaaS trial) The host is internet-reachable. Container Apps have outbound internet access by default. Set TDFS4DS_TD_ENCRYPT=true.
# Create a VNet-integrated Container Apps environment
az containerapp env create \
  --name tdfs4ds-env \
  --resource-group your-rg \
  --location westeurope \
  --infrastructure-subnet-resource-id \
      /subscriptions/.../resourceGroups/your-rg/providers/Microsoft.Network/virtualNetworks/your-vnet/subnets/aca-subnet

Azure OpenAI credentials

With INSTRUCT_MODEL_PROVIDER=azure set TDFS4DS_INSTRUCT_MODEL_API_KEY from Key Vault the same way as the Teradata password above. Alternatively, grant the container app's managed identity the Cognitive Services OpenAI User role on the Azure OpenAI resource — the SDK then uses managed identity and no key is needed.

Required Azure permissions

Two principals need roles: the deployer (human or CI principal running steps 1–4) and the managed identity assigned to the container app at runtime.

Deployer — built-in roles to assign at resource-group scope:

Role Purpose
Contributor Create and manage ACR, Container Apps environment, Container App, Key Vault
AcrPush Push images to ACR (can be scoped to the ACR resource instead of the RG)
Key Vault Secrets Officer Create Key Vault and write secrets
User Access Administrator Assign roles to the managed identity (required for the Key Vault → Container App binding)
DEPLOYER="<your-user-or-sp-object-id>"
RG="/subscriptions/<sub>/resourceGroups/your-rg"

az role assignment create --assignee $DEPLOYER \
  --role "Contributor"              --scope $RG
az role assignment create --assignee $DEPLOYER \
  --role "Key Vault Secrets Officer" --scope $RG
az role assignment create --assignee $DEPLOYER \
  --role "User Access Administrator" --scope $RG

Least privilege alternative

If your organisation forbids broad Contributor, break it down: AcrPush on the ACR, Container Apps Contributor + Container Apps Environment Contributor on the ACA namespace, and Key Vault Secrets Officer on the Key Vault. User Access Administrator can be scoped to just the Key Vault resource.

Managed identity — the runtime identity of the container. Create a user-assigned managed identity (recommended) and grant it:

Role Scope Purpose
AcrPull ACR resource Pull the container image
Key Vault Secrets User Key Vault resource Read the secrets at startup
Cognitive Services OpenAI User Azure OpenAI resource Call Azure OpenAI without an API key (optional)
MI_ID=$(az identity create --name tdfs4ds-runtime \
  --resource-group your-rg --query id -o tsv)
MI_PRINCIPAL=$(az identity show --name tdfs4ds-runtime \
  --resource-group your-rg --query principalId -o tsv)

# Pull images from ACR
ACR_ID=$(az acr show --name youracr --query id -o tsv)
az role assignment create --assignee-object-id $MI_PRINCIPAL \
  --role "AcrPull" --scope $ACR_ID

# Read Key Vault secrets
KV_ID=$(az keyvault show --name your-kv --query id -o tsv)
az role assignment create --assignee-object-id $MI_PRINCIPAL \
  --role "Key Vault Secrets User" --scope $KV_ID

# (Optional) Call Azure OpenAI without an API key
AOAI_ID=$(az cognitiveservices account show \
  --name your-aoai --resource-group your-rg --query id -o tsv)
az role assignment create --assignee-object-id $MI_PRINCIPAL \
  --role "Cognitive Services OpenAI User" --scope $AOAI_ID

Pass the managed identity to the container app:

az containerapp identity assign \
  --name tdfs4ds-serve --resource-group your-rg \
  --user-assigned $MI_ID

Accessing the service

ACA issues a public FQDN when --ingress external is set. Restrict access with built-in IP allow-lists or by setting --ingress internal and placing an API Management gateway in front.

az containerapp show \
  --name tdfs4ds-serve --resource-group your-rg \
  --query properties.configuration.ingress.fqdn -o tsv
# → tdfs4ds-serve.<hash>.westeurope.azurecontainerapps.io

Common notes for all cloud deployments

Topic Recommendation
Teradata connectivity The container connects to Vantage on TCP port 1025. On all cloud platforms this requires private networking (VPC / VNet integration, Direct Connect / Interconnect / ExpressRoute, or VPN) unless your Vantage host is internet-reachable (ClearScape SaaS). Always enable TDFS4DS_TD_ENCRYPT=true when the connection crosses a network boundary.
Vector store Chroma is for dev/demo only — it is a local directory that is lost on container restart and cannot be shared across replicas. Use TDFS4DS_VECTOR_STORE_BACKEND=teradata in production: the index is stored inside Vantage, persistent and replica-safe with no extra cloud storage required.
Port 8080 (docs) The admin UI and chatbot share the single API port (8000), so one ingress port serves them. The docs server uses a second port (8080) — disable it (TDFS4DS_SERVE_DOCS=false) on single-ingress managed services.
Min replicas = 1 The container holds in-memory LangGraph state (conversation threads). Set --min-instances/--min-replicas to 1 to avoid cold starts and keep the Teradata connection warm.
Health check All platforms can use GET /health (returns {"status": "ok"}) as the liveness probe.
HTTPS Always terminate TLS at the platform's load balancer / ingress controller. The container speaks plain HTTP on port 8000.

Call the API

List models

curl http://localhost:8000/v1/models

Consumer agent

curl -s http://localhost:8000/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{
        "model": "tdfs4ds-consumer",
        "messages": [{"role": "user", "content": "What features are available for pharmacies?"}]
      }'
from openai import OpenAI

client = OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")

resp = client.chat.completions.create(
    model="tdfs4ds-consumer",
    messages=[{"role": "user", "content": "What features are available for pharmacies?"}],
)
print(resp.choices[0].message.content)
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage

llm = ChatOpenAI(
    base_url="http://localhost:8000/v1",
    api_key="not-needed",
    model="tdfs4ds-consumer",
)

response = llm.invoke([HumanMessage(content="What features are available for pharmacies?")])
print(response.content)
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate

llm = ChatOpenAI(
    base_url="http://localhost:8000/v1",
    api_key="not-needed",
    model="tdfs4ds-consumer",
)

prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a data analyst. Answer using the feature store."),
    ("human", "{question}"),
])
chain = prompt | llm

response = chain.invoke({"question": "Which pharmacy features are freshest?"})
print(response.content)

Query optimizer — /v1/optimize (programmatic, structured JSON)

POST /v1/optimize is the dedicated programmatic API for SQL optimisation. It accepts raw SQL and returns a structured JSON response — no chat framing needed, no report download required.

Request body

Field Type Required Description
sql string ✓ The Teradata SQL query to optimise
user_request string — Optional context hint (default: "Optimize this Teradata SQL query for performance.")

Response

Field Type Description
optimized_sql string Recommended rewrite (equals the original when no improvement found)
optimization_strategy string Plain-language explanation of the chosen strategy
token_consumption object {prompt_tokens, completion_tokens, total_tokens}
curl -s http://localhost:8000/v1/optimize \
  -H 'Content-Type: application/json' \
  -d '{
        "sql": "SELECT * FROM sales s JOIN customers c ON s.cust_id = c.id WHERE c.region = '\''EU'\''"
      }'

Example response:

{
  "optimized_sql": "SELECT s.*, c.region\nFROM sales s\nINNER JOIN customers c ON s.cust_id = c.id\nWHERE c.region = 'EU'",
  "optimization_strategy": "Replaced SELECT * with explicit column list to reduce spool usage. Changed to INNER JOIN to allow the optimizer to apply join-order heuristics. Added partition pruning on region.",
  "token_consumption": {
    "prompt_tokens": 1842,
    "completion_tokens": 312,
    "total_tokens": 2154
  }
}
import requests

resp = requests.post(
    "http://localhost:8000/v1/optimize",
    json={
        "sql": "SELECT * FROM sales s JOIN customers c ON s.cust_id = c.id WHERE c.region = 'EU'",
        "user_request": "Focus on reducing spool usage.",
    },
)
data = resp.json()

print("Optimized SQL:\n", data["optimized_sql"])
print("\nStrategy:", data["optimization_strategy"])
print("\nTokens used:", data["token_consumption"]["total_tokens"])
$body = @{
    sql          = "SELECT * FROM sales s JOIN customers c ON s.cust_id = c.id WHERE c.region = 'EU'"
    user_request = "Focus on reducing spool usage."
} | ConvertTo-Json

$resp = Invoke-RestMethod `
    -Uri  "http://localhost:8000/v1/optimize" `
    -Method POST `
    -ContentType "application/json" `
    -Body $body

$resp.optimized_sql
$resp.optimization_strategy
$resp.token_consumption

Query optimizer — /v1/chat/completions (conversational, Markdown report)

For interactive use or when you need the full Markdown report with scores, candidates, and FilterManager guidance, use the chat-completions surface and pass the SQL inline in the user message:

The full report is a download link, not the answer

For an OPTIMIZE request the answer is a compact summary — the score delta (before → after /5), whether an improvement was found, the optimized SQL, and FilterManager applicability — followed by a download link to the full Markdown report:

**Full report:** http://localhost:8000/reports/<DATA_DOMAIN>/tdfs4ds_optim_<ts>_<view>_<id>.md

The report is written under QUERY_OPTIMIZER_REPORT_DIR (tdfs4ds_optim_reports/<DATA_DOMAIN>/ by default) and served by the container. Browse all reports at GET /reports, or download one at GET /reports/{path}. Mount that directory (or the /data volume) to keep reports across restarts.

curl -s http://localhost:8000/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{
        "model": "tdfs4ds-query-optimizer",
        "messages": [{"role": "user", "content": "Optimize this query: SELECT * FROM sales s JOIN customers c ON s.cust_id = c.id WHERE c.region = '\''EU'\''"}]
      }'
from openai import OpenAI

client = OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")

resp = client.chat.completions.create(
    model="tdfs4ds-query-optimizer",
    messages=[{
        "role": "user",
        "content": "Optimize this query: SELECT * FROM sales s JOIN customers c ON s.cust_id = c.id WHERE c.region = 'EU'",
    }],
)
print(resp.choices[0].message.content)

# The usage block also carries optimiser-specific metadata
usage = resp.usage
# usage.prompt_tokens, usage.completion_tokens, usage.total_tokens
# usage.model_extra["optimized"]    → True/False
# usage.model_extra["score_delta"]  → {original_user_score, best_user_score, ...}
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage

llm = ChatOpenAI(
    base_url="http://localhost:8000/v1",
    api_key="not-needed",
    model="tdfs4ds-query-optimizer",
)

sql = "SELECT * FROM sales s JOIN customers c ON s.cust_id = c.id WHERE c.region = 'EU'"
response = llm.invoke([HumanMessage(content=f"Optimize this query: {sql}")])
print(response.content)

Choosing between the two optimizer endpoints

/v1/optimize /v1/chat/completions
Output format Structured JSON Markdown answer (chat)
Multi-turn No (stateless) Yes (via user field / thread_id)
Full report No (strategy inline) Download link in answer
Score delta No In usage.model_extra
Best for Programmatic pipelines, CI checks, bulk rewrites Interactive exploration, full audit trail

With the openai Python SDK (multi-model)

from openai import OpenAI

client = OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")

# Consumer agent
resp = client.chat.completions.create(
    model="tdfs4ds-consumer",
    messages=[{"role": "user", "content": "Summarise the customer features."}],
)
print(resp.choices[0].message.content)

# Query optimizer — full Markdown report via chat
resp = client.chat.completions.create(
    model="tdfs4ds-query-optimizer",
    messages=[{"role": "user", "content": "Explain the plan for: SELECT ..."}],
)
print(resp.choices[0].message.content)

Conversation memory

The agents own their multi-turn memory (LangGraph MemorySaver, keyed by a thread_id). To keep a conversation continuous across requests, set the OpenAI user field to a stable id — the server maps it to the agent's thread_id:

client.chat.completions.create(
    model="tdfs4ds-consumer",
    user="analyst-42",                       # stable id → continuous thread
    messages=[{"role": "user", "content": "And which of those are fresh?"}],
)

Without a user field each request is independent (a fresh thread_id).


Streaming

stream: true is supported and emits OpenAI chat.completion.chunk events. Because the agents return a complete answer (not a token stream), the current implementation sends the answer as a single content chunk followed by [DONE] — enough for clients that require streaming, without true token-level deltas.


Admin web UI

The container serves a Teradata-styled admin web UI on the same port as the API (default 8000):

http://localhost:8000/

It is a server-rendered HTML/JS shell that reproduces the Teradata "Tera" console. Its left rail groups the panels in sections: Assistant (the chat), Explore (Report, EFS map, Feature catalog, Dataset builder, Lineage), Documentation, Operations, Performance, Settings (Parameters) and Help (the tdfs4ds guide, 0.3.6.0). The chat is rendered by the shell (since 0.3.2.0; the Gradio chat is still mounted at /chatbot); every panel fetches its data from /ui/* endpoints. Everything runs in the same process as the API, sharing the Teradata connection and the vector index — no second connection or index build.

v0.3.1.108 — single port, Gradio only for the chatbot

Earlier versions ran a full Gradio tabbed admin app on a second port (7860, via TDFS4DS_SERVE_CHATBOT*). That is gone: Gradio now renders only the chat, embedded in the custom shell, all on TDFS4DS_SERVE_PORT.

The ⚙️ Parameters panel lets you change, live:

  • Feature stores — the primary store (read-write) and, since 0.3.3.15, any number of other stores read from (read-only): their features, processes, datasets, documentation and run history are shown and searched next to the primary's, and nothing is written to them. Applying calls tdfs4ds.connect(database=…, databases_readonly=[…], create_if_missing=False); a database that does not contain a tdfs4ds feature store is rejected and the switch does not happen, and a read-only store that cannot be read is named with the reason. Their vector index is then brought up to date in the background.
  • Vector store — backend (chroma / teradata) and Chroma path, plus a Refresh / rebuild index button (useful because the local Chroma index goes stale as the business dictionary changes).
  • Instruct model and Embedding model — provider, model, URL, API key, and proxy (plus the embedding dimension).

Applying any change that affects the data or the model updates the index in the background when needed, resets the agent, rotates the conversation thread, and clears the chat so the session starts clean.

Status bar. The bar at the bottom of every page names the feature stores the session reads and what runs in the background: an index build, a maintenance run, a documentation, migration, evaluation or benchmark run, with its progress. details lists every job of the last minute, with the whole error of one that failed.

Maintenance → Rebuild vector index lists every feature store found, the attached ones ticked, with what the local index holds for each. On Chroma you can re-index any of them, including a store not attached yet (it is read through its public views). Each row also says whether that store's index is behind its documentation (rows documented since the last indexing date), without re-indexing anything. force vector rebuild re-embeds only the ticked stores.

Objects / Coverage show every attached store's documentation; another store's objects carry a 🔒 and cannot be documented or edited from here (their documentation is written in their own store).

The panel shares the agent's configuration

The panel does not hold its own settings — each field is the same tdfs4ds.* global the agents read (SCHEMA, INSTRUCT_MODEL_*, EMBEDDING_MODEL_*, VECTOR_STORE_BACKEND, CHROMA_PATH, …). Fields are pre-filled from the values the container loaded from your .env / env vars at startup, and applying a change updates that global for the whole process. So the TDFS4DS_* variables you set in the .env are the panel's initial state, and a panel edit overrides them until the next restart.

The standalone chat-only Gradio UI is still available in notebooks:

from tdfs4ds.agent import launch_chatbot       # or launch_chatbot_with_index
launch_chatbot(port=7860)

launch_chatbot / launch_chatbot_with_index are unchanged. The full admin experience (the panels above) is the web shell served by python -m tdfs4ds.serve.


Documentation server (port 8080)

Since 0.3.6.0 the documentation ships with the package (tdfs4ds/docs_site, about 7 MB in the wheel) and the admin app serves it on its own port at /documentation (Help › tdfs4ds guide in the rail), so it also works behind a path prefix and through Jupyter's proxy (launch_app). In a notebook, tdfs4ds.docs_path() gives the local path of the home page. A source checkout serves its own docs/ folder instead (the freshest mkdocs build).

The container also serves it on a third port (default 8080):

http://localhost:8080

Toggle it with TDFS4DS_SERVE_DOCS (default true; set false to disable) and set the port with TDFS4DS_SERVE_DOCS_PORT.

Live documentation updates

The docs/ directory is bind-mounted from your host:

# docker-compose.yml
volumes:
  - ./docs:/app/docs:ro   # host ./docs → /app/docs in container

So you can run mkdocs build locally and the served documentation updates without restarting the container — just refresh your browser.

If you prefer the docs baked into the image (for offline deployment or immutability), remove the bind mount from the compose file. The image already carries the documentation from the working tree — just omit the mount and the image's /app/docs directory will be served unchanged across container restarts.


Dependency graph / lineage (port 8000 /lineage)

The API exposes an interactive lineage graph at:

http://localhost:8000/lineage

This renders the persisted FS_LINEAGE_GRAPH temporal table (built by tdfs4ds.rebuild_lineage_graph()) as an interactive pyvis network diagram, showing all object dependencies and relationships.

Scoped subgraph

Filter the graph to a single object and its ancestors/descendants with the seed query parameter:

http://localhost:8000/lineage?seed=MY_DATABASE.MY_VIEW

The seed is parsed as DATABASE.OBJECT or OBJECT (auto-detected). Omit it for the full graph.

Typical workflow

  1. Populate the lineage once by running in Python:

    import tdfs4ds
    tdfs4ds.rebuild_lineage_graph()
    

  2. Open the graph in your browser at http://localhost:8000/lineage.

  3. Explore interactively — pan, zoom, and click nodes to inspect them.

Per-user database choice (multi-tenant)

The configuration is process-wide: tdfs4ds.SCHEMA and the catalog-name globals are module-level and shared by every request in the process. Switching the feature-store database — whether via the panel or TDFS4DS_SCHEMA — therefore changes it for all users of that container and for the OpenAI /v1 API in the same process.

To give each user their own feature-store database (or their own Teradata system), run one container per user rather than sharing one. Each container is an isolated process with its own globals, Teradata context, and Chroma index, so there is no cross-talk and nothing to change in code.

Recommended: one Compose service, one env file per user. A single service definition is instantiated once per user — the Compose project name (-p) namespaces both the container and the named volume, so each user automatically gets an isolated Chroma index with no extra wiring:

# docker-compose.yml — instantiated once per user via -p + --env-file
services:
  tdfs4ds-serve:
    image: you/tdfs4ds-serve:latest        # built once, reused for every user
    env_file:
      - ${TDFS4DS_ENV_FILE:-.env}
    ports:
      - "${TDFS4DS_API_PORT:-8000}:8000"        # host port varies; container port fixed (UI + chatbot + API)
    volumes:
      - chroma:/data                            # per-project volume → isolated index
    restart: unless-stopped

volumes:
  chroma:
# User alice → feature store FS_TEAM_A on vantage-prod
docker compose -p alice --env-file alice.env up -d    # container alice-…, volume alice_chroma
# User bob → feature store FS_TEAM_B on a different system
docker compose -p bob   --env-file bob.env   up -d    # container bob-…,   volume bob_chroma

The container port stays fixed (8000, baked into the image); only the host side is a variable (TDFS4DS_API_PORT) so several users can co-exist on one host without clashing. If each user runs on their own host/VM you can keep the fixed 8000:8000 mapping and only swap the env file.

What each per-user .env must change vs. share:

Scope Variables
Per user TDFS4DS_TD_HOST/USERNAME/PASSWORD (+ _LOGMECH/_DATABASE/_ENCRYPT), TDFS4DS_SCHEMA, TDFS4DS_DATA_DOMAIN, and the host ports (if co-located)
Shared INSTRUCT_MODEL_*, EMBEDDING_MODEL_*, VECTOR_STORE_BACKEND, SERVE_*

Keep TDFS4DS_CHROMA_PATH=/data/chroma identical for everyone — isolation comes from the per-project named volume (alice_chroma vs bob_chroma), not from the path. (If you bind-mount host directories instead, those must differ per user.)

Equivalent with raw docker run if you prefer not to use Compose:

docker run --env-file alice.env -p 8001:8000 -v alice_chroma:/data you/tdfs4ds-serve
docker run --env-file bob.env   -p 8002:8000 -v bob_chroma:/data   you/tdfs4ds-serve

Within a single container, the panel's database switch is safe — it just isn't a per-session boundary, so don't share one container across users who need different databases. Route users to their container with your normal reverse proxy / orchestration layer.


Outbound proxy (HTTP or SOCKS5)

LLM and embedding calls can be routed through an outbound proxy — useful in locked-down environments. Set a single proxy URL whose scheme picks the protocol:

TDFS4DS_INSTRUCT_MODEL_PROXY=socks5://proxy-host:1080   # or http://proxy-host:3128
# TDFS4DS_EMBEDDING_MODEL_PROXY=...    # optional; blank reuses the instruct proxy

Notes:

  • Only the OpenAI-family clients (openai / vllm / openai-compatible / mistralai / azure) honour this; it does not proxy the Teradata connection. Bedrock / GCP use their own SDK proxy mechanisms.
  • socks5:// requires the httpx[socks] backend, which ships with the package.
  • Leave both unset for a direct connection (default).