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.
Install the [serve] extra, which adds FastAPI, uvicorn, and LangServe on
top of the core package:
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¶
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:
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:
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:
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.
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):
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¶
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.
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¶
Consumer agent¶
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:
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.
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):
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:
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):
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:
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:
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:
The seed is parsed as DATABASE.OBJECT or OBJECT (auto-detected). Omit it
for the full graph.
Typical workflow¶
-
Populate the lineage once by running in Python:
-
Open the graph in your browser at
http://localhost:8000/lineage. -
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 thehttpx[socks]backend, which ships with the package.- Leave both unset for a direct connection (default).