Compare commits
10 Commits
feat/ocr-c
...
fix/hetzne
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0c83ca985 | ||
|
|
bfcb0c46b8 | ||
|
|
88e549bd1d | ||
|
|
6c5a068fd9 | ||
|
|
83f81ab5fc | ||
|
|
6859486334 | ||
|
|
304bbc62ad | ||
|
|
312f3e730e | ||
|
|
6156900fc2 | ||
|
|
5377d379b7 |
69
.github/workflows/deploy-mainnet.yml
vendored
Normal file
69
.github/workflows/deploy-mainnet.yml
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
name: Deploy mainnet
|
||||
|
||||
# CD for the vector-marketplace host (91.98.147.172).
|
||||
#
|
||||
# Trigger: automatically after the "CI" workflow succeeds on main, or manually
|
||||
# via workflow_dispatch (Actions tab -> Deploy mainnet -> Run workflow).
|
||||
#
|
||||
# The runner only SSHes in and executes deploy/mainnet/deploy.sh AT THE NEW
|
||||
# COMMIT (fetched server-side via `git show FETCH_HEAD:`), so deploy logic is
|
||||
# versioned with the code it deploys. The script itself decides which compose
|
||||
# projects to rebuild based on the old..new diff, drains busy suppliers, and
|
||||
# health-gates every restart — see deploy/mainnet/deploy.sh.
|
||||
#
|
||||
# Secrets: DEPLOY_SSH_KEY — ed25519 private key whose public half is in
|
||||
# root@vector-marketplace:~/.ssh/authorized_keys.
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["CI"]
|
||||
types: [completed]
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
force:
|
||||
description: "Redeploy even if the server is already at origin/main"
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
concurrency:
|
||||
group: deploy-mainnet
|
||||
cancel-in-progress: false # never kill a deploy mid-rollout
|
||||
|
||||
env:
|
||||
DEPLOY_HOST: 91.98.147.172
|
||||
DEPLOY_USER: root
|
||||
# Pinned host key (ssh-keyscan -t ed25519 91.98.147.172)
|
||||
DEPLOY_HOSTKEY: "91.98.147.172 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJvjM8D+pNUb2Qr4yJbqVcRrUzbpbfyv0/tupUgD5Th0"
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: deploy to vector-marketplace
|
||||
runs-on: nixos
|
||||
# workflow_run fires on failure too — only deploy green main.
|
||||
if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
|
||||
steps:
|
||||
# Self-hosted runner: keep key material in the job's temp dir, not the
|
||||
# runner user's ~/.ssh (which persists across jobs).
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
install -m 700 -d "$RUNNER_TEMP/ssh"
|
||||
printf '%s\n' "${{ secrets.DEPLOY_SSH_KEY }}" > "$RUNNER_TEMP/ssh/id_ed25519"
|
||||
chmod 600 "$RUNNER_TEMP/ssh/id_ed25519"
|
||||
printf '%s\n' "$DEPLOY_HOSTKEY" > "$RUNNER_TEMP/ssh/known_hosts"
|
||||
|
||||
- name: Run deploy script on server
|
||||
run: |
|
||||
FORCE=0
|
||||
[ "${{ github.event.inputs.force }}" = "true" ] && FORCE=1
|
||||
ssh -o BatchMode=yes \
|
||||
-o IdentitiesOnly=yes \
|
||||
-i "$RUNNER_TEMP/ssh/id_ed25519" \
|
||||
-o UserKnownHostsFile="$RUNNER_TEMP/ssh/known_hosts" \
|
||||
"$DEPLOY_USER@$DEPLOY_HOST" "
|
||||
set -euo pipefail
|
||||
cd /root/agents-marketplace
|
||||
git fetch origin main
|
||||
git show FETCH_HEAD:deploy/mainnet/deploy.sh > /run/marketplace-deploy.sh
|
||||
FORCE=$FORCE bash /run/marketplace-deploy.sh
|
||||
"
|
||||
@@ -289,3 +289,34 @@ For the M1-F-2 (LiveOgmiosProvider) handoff: the supplier compose already
|
||||
joins `apex-dashboard_apex-net` and overrides `OGMIOS_URL=ws://ogmios:1337`,
|
||||
so once the supplier code stops booting `ReadOnlyOgmiosProvider` and
|
||||
starts talking to a real Ogmios, no compose changes are needed.
|
||||
|
||||
## 10. Continuous deployment (mainnet, vector-marketplace host)
|
||||
|
||||
Merges to `main` deploy automatically to the mainnet host once CI is green:
|
||||
|
||||
1. `.github/workflows/ci.yml` runs typecheck + vitest on the merge commit.
|
||||
2. On success, `.github/workflows/deploy-mainnet.yml` SSHes into the host
|
||||
(secret `DEPLOY_SSH_KEY`, host key pinned in the workflow) and executes
|
||||
`deploy/mainnet/deploy.sh` **at the new commit** (`git show FETCH_HEAD:...`),
|
||||
so deploy logic always matches the code being deployed.
|
||||
3. The script hard-resets `/root/agents-marketplace` to `origin/main`, maps the
|
||||
old..new diff to affected compose projects (changes under `packages/`,
|
||||
`patches/`, `contracts/` or root manifests rebuild everything), builds all
|
||||
affected images up front, then does a rolling `up -d` with a health gate per
|
||||
project. Suppliers are drained first: `/status` is polled until not
|
||||
`working` (up to `DRAIN_TIMEOUT_SECS`, default 600) so an in-flight job
|
||||
isn't killed and its escrow bond forfeited.
|
||||
|
||||
Manual controls:
|
||||
|
||||
- **Manual deploy / redeploy**: Actions → "Deploy mainnet" → Run workflow
|
||||
(check *force* to redeploy the same sha), or on the host:
|
||||
`bash /root/agents-marketplace/deploy/mainnet/deploy.sh` (`FORCE=1` to
|
||||
redeploy, `DRY_RUN=1` to preview which projects would restart).
|
||||
- **Rollback**: `cd /root/agents-marketplace && git reset --hard <old-sha> &&
|
||||
FORCE=1 bash deploy/mainnet/deploy.sh`. Every deploy appends
|
||||
`old -> new (projects)` to `/var/log/marketplace-deploy.log`.
|
||||
|
||||
Projects with no running containers on the host (e.g. wallet-monitor) are
|
||||
skipped; image-only projects (ollama, chatmock, tts-piper) redeploy only when
|
||||
their own compose file changes.
|
||||
|
||||
178
deploy/mainnet/deploy.sh
Executable file
178
deploy/mainnet/deploy.sh
Executable file
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy/mainnet/deploy.sh — pull-based CD for the vector-marketplace host.
|
||||
#
|
||||
# Runs ON the server (root@vector-marketplace, repo at /root/agents-marketplace).
|
||||
# Invoked by .github/workflows/deploy-mainnet.yml over SSH, but safe to run by
|
||||
# hand: `bash deploy/mainnet/deploy.sh`.
|
||||
#
|
||||
# What it does:
|
||||
# 1. Fetches origin/main and hard-resets the checkout to it.
|
||||
# 2. Diffs old..new to work out which compose projects actually need a
|
||||
# rebuild (gateway vs suppliers vs buyer vs indexer ...). Changes under
|
||||
# packages/, patches/, contracts/ or the root manifests rebuild everything,
|
||||
# because every Dockerfile COPYs those into its build context.
|
||||
# 3. Builds affected images first (all 17 supplier projects share one
|
||||
# Dockerfile/context, so build #2..#17 are layer-cache hits), THEN does a
|
||||
# rolling `up -d` so the downtime window is restart-only, not build+restart.
|
||||
# 4. Suppliers are drained before recreate: /status is polled until the
|
||||
# supplier is not "working" (a busy supplier killed mid-job forfeits its
|
||||
# escrow bond). After DRAIN_TIMEOUT_SECS we recreate anyway, so a wedged
|
||||
# supplier cannot block the deploy forever.
|
||||
# 5. Every restarted container must report healthy (or plain running when it
|
||||
# has no healthcheck) within HEALTH_TIMEOUT_SECS or the deploy fails.
|
||||
#
|
||||
# Env knobs:
|
||||
# FORCE=1 redeploy even if already at origin/main
|
||||
# DRY_RUN=1 print what would be rebuilt/restarted, change nothing
|
||||
# DRAIN_TIMEOUT_SECS=N max wait per busy supplier (default 600, 0 = skip)
|
||||
# HEALTH_TIMEOUT_SECS=N max wait for healthy (default 180)
|
||||
#
|
||||
# Rollback: git reset --hard <old-sha> && FORCE=1 bash deploy/mainnet/deploy.sh
|
||||
# (old sha is printed below and appended to /var/log/marketplace-deploy.log).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO=/root/agents-marketplace
|
||||
COMPOSE_DIR="$REPO/deploy/mainnet"
|
||||
LOCK=/run/marketplace-deploy.lock
|
||||
LOG=/var/log/marketplace-deploy.log
|
||||
DRAIN_TIMEOUT_SECS="${DRAIN_TIMEOUT_SECS:-600}"
|
||||
HEALTH_TIMEOUT_SECS="${HEALTH_TIMEOUT_SECS:-180}"
|
||||
FORCE="${FORCE:-0}"
|
||||
DRY_RUN="${DRY_RUN:-0}"
|
||||
|
||||
exec 9>"$LOCK"
|
||||
flock -n 9 || { echo "another deploy is already running (lock: $LOCK)"; exit 1; }
|
||||
|
||||
cd "$REPO"
|
||||
|
||||
OLD=$(git rev-parse HEAD)
|
||||
git fetch origin main
|
||||
NEW=$(git rev-parse FETCH_HEAD)
|
||||
|
||||
if [ "$OLD" = "$NEW" ] && [ "$FORCE" != "1" ]; then
|
||||
echo "already at origin/main ($NEW) — nothing to deploy (FORCE=1 to override)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "deploying $OLD -> $NEW"
|
||||
if [ "$DRY_RUN" = "1" ]; then
|
||||
echo "(dry run — checkout not reset, nothing rebuilt or restarted)"
|
||||
else
|
||||
git reset --hard FETCH_HEAD
|
||||
fi
|
||||
|
||||
CHANGED=$(git diff --name-only "$OLD" "$NEW" || true)
|
||||
[ "$FORCE" = "1" ] && [ -z "$CHANGED" ] && CHANGED="(forced)"
|
||||
|
||||
# Paths COPY'd into every image's build context (see */Dockerfile).
|
||||
SHARED_RE='^(packages/|patches/|contracts/|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|tsconfig\.base\.json$|\.npmrc$)'
|
||||
|
||||
changed_matches() { # changed_matches <ere> -> 0 if any changed path matches
|
||||
[ "$FORCE" = "1" ] && return 0
|
||||
echo "$CHANGED" | grep -qE "$1"
|
||||
}
|
||||
|
||||
# Compose project -> the source prefixes that require rebuilding its image.
|
||||
# Image-only projects (ollama, chatmock, tts-piper) redeploy only when their
|
||||
# own compose file changes.
|
||||
project_re() {
|
||||
case "$1" in
|
||||
docker-compose.indexer.yml) echo "^indexer/|^indexer-ui/|$SHARED_RE" ;;
|
||||
docker-compose.buyer.yml) echo "^buyer/|$SHARED_RE" ;;
|
||||
docker-compose.gateway.yml) echo "^gateway/|$SHARED_RE" ;;
|
||||
docker-compose.wallet-monitor.yml) echo "^wallet-monitor/|$SHARED_RE" ;;
|
||||
docker-compose.supplier*.yml) echo "^supplier/|$SHARED_RE" ;;
|
||||
*) echo "" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Restart order: infra first, suppliers last (each supplier drains first).
|
||||
ORDERED_NON_SUPPLIER="docker-compose.indexer.yml docker-compose.buyer.yml docker-compose.gateway.yml docker-compose.wallet-monitor.yml docker-compose.ollama.yml docker-compose.chatmock.yml docker-compose.tts-piper.yml"
|
||||
SUPPLIERS=$(cd "$COMPOSE_DIR" && ls docker-compose.supplier*.yml)
|
||||
|
||||
AFFECTED=()
|
||||
for f in $ORDERED_NON_SUPPLIER $SUPPLIERS; do
|
||||
[ -f "$COMPOSE_DIR/$f" ] || continue
|
||||
re=$(project_re "$f")
|
||||
hit=0
|
||||
[ -n "$re" ] && changed_matches "$re" && hit=1
|
||||
changed_matches "^deploy/mainnet/$f\$" && hit=1
|
||||
[ "$hit" = "1" ] || continue
|
||||
if [ -z "$(docker compose -f "$COMPOSE_DIR/$f" ps -q 2>/dev/null)" ]; then
|
||||
echo "skip $f — affected but no running containers on this host"
|
||||
continue
|
||||
fi
|
||||
AFFECTED+=("$f")
|
||||
done
|
||||
|
||||
if [ "${#AFFECTED[@]}" -eq 0 ]; then
|
||||
echo "no running compose project is affected by $OLD..$NEW — done"
|
||||
echo "$(date -Is) $OLD -> $NEW (no services affected)" >> "$LOG"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "affected projects: ${AFFECTED[*]}"
|
||||
|
||||
if [ "$DRY_RUN" = "1" ]; then
|
||||
echo "(dry run — stopping before build/rollout)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "== build phase =="
|
||||
for f in "${AFFECTED[@]}"; do
|
||||
echo "-- building $f"
|
||||
docker compose -f "$COMPOSE_DIR/$f" build
|
||||
done
|
||||
|
||||
drain_supplier() { # drain_supplier <compose-file>
|
||||
[ "$DRAIN_TIMEOUT_SECS" = "0" ] && return 0
|
||||
local cid deadline status
|
||||
cid=$(docker compose -f "$COMPOSE_DIR/$1" ps -q | head -1)
|
||||
[ -n "$cid" ] || return 0
|
||||
deadline=$(( $(date +%s) + DRAIN_TIMEOUT_SECS ))
|
||||
while [ "$(date +%s)" -lt "$deadline" ]; do
|
||||
status=$(docker exec "$cid" wget -qO- http://localhost:8080/status 2>/dev/null \
|
||||
| grep -o '"status":"[^"]*"' | head -1 | cut -d'"' -f4 || true)
|
||||
if [ "$status" != "working" ]; then
|
||||
return 0
|
||||
fi
|
||||
echo " supplier busy (status=working) — waiting for job to finish..."
|
||||
sleep 15
|
||||
done
|
||||
echo " WARNING: still busy after ${DRAIN_TIMEOUT_SECS}s — recreating anyway"
|
||||
}
|
||||
|
||||
wait_healthy() { # wait_healthy <compose-file>
|
||||
local deadline cid state
|
||||
deadline=$(( $(date +%s) + HEALTH_TIMEOUT_SECS ))
|
||||
for cid in $(docker compose -f "$COMPOSE_DIR/$1" ps -q); do
|
||||
while :; do
|
||||
state=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$cid")
|
||||
case "$state" in
|
||||
healthy|running) break ;;
|
||||
starting|unhealthy|created|restarting)
|
||||
if [ "$(date +%s)" -ge "$deadline" ]; then
|
||||
echo " FAILED: $(docker inspect -f '{{.Name}}' "$cid") is '$state' after ${HEALTH_TIMEOUT_SECS}s; last logs:"
|
||||
docker logs --tail 50 "$cid" 2>&1 | sed 's/^/ | /'
|
||||
return 1
|
||||
fi
|
||||
sleep 5 ;;
|
||||
*) echo " FAILED: container state '$state'"; docker logs --tail 50 "$cid" 2>&1 | sed 's/^/ | /'; return 1 ;;
|
||||
esac
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
echo "== rollout phase =="
|
||||
for f in "${AFFECTED[@]}"; do
|
||||
echo "-- rolling out $f"
|
||||
case "$f" in docker-compose.supplier*) drain_supplier "$f" ;; esac
|
||||
docker compose -f "$COMPOSE_DIR/$f" up -d
|
||||
wait_healthy "$f"
|
||||
echo " ok"
|
||||
done
|
||||
|
||||
echo "$(date -Is) $OLD -> $NEW (${AFFECTED[*]})" >> "$LOG"
|
||||
echo "== deploy complete: $NEW =="
|
||||
echo "rollback: cd $REPO && git reset --hard $OLD && FORCE=1 bash deploy/mainnet/deploy.sh"
|
||||
101
deploy/mainnet/docker-compose.supplier-ds-flash-htz-chat.yml
Normal file
101
deploy/mainnet/docker-compose.supplier-ds-flash-htz-chat.yml
Normal file
@@ -0,0 +1,101 @@
|
||||
# deploy/mainnet/docker-compose.supplier-ds-flash-htz-chat.yml — Supplier — MAINNET
|
||||
# (Hetzner Inference / DeepSeek-V4-Flash-0731 — MULTI-TURN CHAT capability `llm.chat.v1`)
|
||||
#
|
||||
# The chat-session sibling of docker-compose.supplier-ds-flash-htz.yml. Same
|
||||
# image, same Hetzner backend (DeepSeek-V4-Flash-0731), but CAPABILITY_KIND=chat-session so
|
||||
# it mounts /v1/chat/{start,message,end} (escrow bookends, off-chain streaming
|
||||
# turns) instead of the one-off /v1/chat/completions route.
|
||||
#
|
||||
# A SEPARATE process/advert from the one-off supplier on purpose: the chat
|
||||
# session holds the supplier's single-slot lock for the whole conversation
|
||||
# (minutes), which would starve one-off traffic if co-hosted.
|
||||
#
|
||||
# One of 8 Hetzner-backed suppliers (4 models x {one-off, chat-session});
|
||||
# fleet runbook: docs/HETZNER_SETUP.md.
|
||||
#
|
||||
# Pre-flight:
|
||||
# 1. DNS: mp-suppliers-ds-flash-htz-chat.vector.apexfusion.org → host
|
||||
# (individual DNS-only A record → 91.98.147.172; NO wildcard exists —
|
||||
# letsencrypt HTTP-01 issuance needs the unproxied direct record)
|
||||
# 2. supplier/.env.ds-flash-htz-chat written on host (chmod 600) with at minimum:
|
||||
# SUPPLIER_PRIV_KEY_HEX, SUPPLIER_ADDRESS, SUPPLIER_PKH,
|
||||
# SUPPLIER_PUB_KEY_HEX, OPENAI_API_KEY, OGMIOS_URL, NETWORK_ID=1,
|
||||
# VECTOR_ZERO_TIME_MS, ESCROW_REF_UTXO, ADVERT_REF_UTXO, ADVERT_REF,
|
||||
# LIVE_CHAIN=1
|
||||
# The advert must be posted with --capability-id llm.chat.v1 and a large
|
||||
# --max-processing-ms (~1800000) so Claim+Submit both land before deliver_by.
|
||||
# 3. Networks already exist (created by the canonical supplier compose):
|
||||
# docker network create marketplace-mainnet-net # one-shot
|
||||
# # traefik-net is created by the Traefik project
|
||||
|
||||
name: marketplace-mainnet-supplier-ds-flash-htz-chat
|
||||
|
||||
services:
|
||||
supplier:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: supplier/Dockerfile
|
||||
container_name: marketplace-mainnet-supplier-ds-flash-htz-chat
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
# Self-contained env for this Hetzner-backed variant. Holds this
|
||||
# supplier's distinct SUPPLIER_PRIV_KEY_HEX (+ derived wallet vars),
|
||||
# ADVERT_REF, the shared Hetzner OPENAI_API_KEY, and the shared mainnet
|
||||
# plumbing duplicated in full (NETWORK_ID, OGMIOS_URL, VECTOR_ZERO_TIME_MS,
|
||||
# ref-script UTxOs, LIVE_CHAIN). No env_file ordering / override magic.
|
||||
- path: ../../supplier/.env.ds-flash-htz-chat
|
||||
required: false
|
||||
environment:
|
||||
# Multi-turn chat session capability (mounts /v1/chat/{start,message,end}).
|
||||
CAPABILITY_KIND: chat-session
|
||||
LLM_BACKEND: openai
|
||||
# No /v1 suffix — supplier/src/openai.ts appends /v1/chat/completions
|
||||
# (Hetzner serves at https://inference.hetzner.com/api/v1/...).
|
||||
OPENAI_BASE_URL: https://inference.hetzner.com/api
|
||||
# Reasoning-style models can be slow; mirror the one-off compose.
|
||||
OPENAI_TIMEOUT_MS: "300000"
|
||||
# Idle end (supplier Submits the transcript): single-slot suppliers must
|
||||
# free up quickly (the gateway transparently reopens a session on the
|
||||
# next turn). Mirrors the kimi-chat fleet.
|
||||
CHAT_IDLE_TIMEOUT_MS: "900000"
|
||||
CHAT_SETTLE_MODE: ticket
|
||||
MAX_CHAT_SESSIONS: "8"
|
||||
PORT: "8080"
|
||||
# Enable LiveOgmiosProvider so Claim/Submit actually broadcast.
|
||||
LIVE_CHAIN: "1"
|
||||
# NETWORK_ID and OGMIOS_URL intentionally not pinned here — they MUST
|
||||
# come from the operator-provided supplier/.env.ds-flash-htz-chat on a mainnet host.
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/healthz >/dev/null || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
networks:
|
||||
- marketplace-mainnet-net
|
||||
- traefik-net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik-net"
|
||||
- "traefik.http.routers.mp-suppliers-ds-flash-htz-chat-mainnet.entrypoints=web"
|
||||
- "traefik.http.routers.mp-suppliers-ds-flash-htz-chat-mainnet.rule=Host(`mp-suppliers-ds-flash-htz-chat.vector.apexfusion.org`)"
|
||||
- "traefik.http.routers.mp-suppliers-ds-flash-htz-chat-mainnet.middlewares=mp-suppliers-ds-flash-htz-chat-mainnet-https"
|
||||
- "traefik.http.middlewares.mp-suppliers-ds-flash-htz-chat-mainnet-https.redirectscheme.scheme=https"
|
||||
- "traefik.http.middlewares.mp-suppliers-ds-flash-htz-chat-mainnet-https.redirectscheme.permanent=true"
|
||||
- "traefik.http.routers.mp-suppliers-ds-flash-htz-chat-mainnet-tls.entrypoints=websecure"
|
||||
- "traefik.http.routers.mp-suppliers-ds-flash-htz-chat-mainnet-tls.rule=Host(`mp-suppliers-ds-flash-htz-chat.vector.apexfusion.org`)"
|
||||
- "traefik.http.routers.mp-suppliers-ds-flash-htz-chat-mainnet-tls.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.mp-suppliers-ds-flash-htz-chat-mainnet.loadbalancer.server.port=8080"
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
marketplace-mainnet-net:
|
||||
external: true
|
||||
name: marketplace-mainnet-net
|
||||
traefik-net:
|
||||
external: true
|
||||
name: traefik-net
|
||||
89
deploy/mainnet/docker-compose.supplier-ds-flash-htz.yml
Normal file
89
deploy/mainnet/docker-compose.supplier-ds-flash-htz.yml
Normal file
@@ -0,0 +1,89 @@
|
||||
# deploy/mainnet/docker-compose.supplier-ds-flash-htz.yml — Supplier — MAINNET
|
||||
# (Hetzner Inference / DeepSeek-V4-Flash-0731 — one-off `llm.text.generate.v1`)
|
||||
#
|
||||
# Hetzner-backed supplier. LLM_BACKEND=openai pointed directly at Hetzner's
|
||||
# hosted inference API (OpenAI-compatible at https://inference.hetzner.com/api).
|
||||
# No local proxy container — reachable over public HTTPS.
|
||||
#
|
||||
# One of 8 Hetzner-backed suppliers (4 models x {one-off, chat-session});
|
||||
# fleet runbook: docs/HETZNER_SETUP.md.
|
||||
#
|
||||
# Pre-flight:
|
||||
# 1. DNS: mp-suppliers-ds-flash-htz.vector.apexfusion.org → host
|
||||
# (individual DNS-only A record → 91.98.147.172; NO wildcard exists —
|
||||
# letsencrypt HTTP-01 issuance needs the unproxied direct record)
|
||||
# 2. supplier/.env.ds-flash-htz written on host (chmod 600) with at minimum:
|
||||
# SUPPLIER_PRIV_KEY_HEX, SUPPLIER_ADDRESS, SUPPLIER_PKH,
|
||||
# SUPPLIER_PUB_KEY_HEX, OPENAI_API_KEY, OGMIOS_URL, NETWORK_ID=1,
|
||||
# VECTOR_ZERO_TIME_MS, ESCROW_REF_UTXO, ADVERT_REF_UTXO, ADVERT_REF,
|
||||
# LIVE_CHAIN=1
|
||||
# 3. Networks already exist (created by the canonical supplier compose):
|
||||
# docker network create marketplace-mainnet-net # one-shot
|
||||
# # traefik-net is created by the Traefik project
|
||||
|
||||
name: marketplace-mainnet-supplier-ds-flash-htz
|
||||
|
||||
services:
|
||||
supplier:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: supplier/Dockerfile
|
||||
container_name: marketplace-mainnet-supplier-ds-flash-htz
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
# Self-contained env for this Hetzner-backed variant. Holds this
|
||||
# supplier's distinct SUPPLIER_PRIV_KEY_HEX (+ derived wallet vars),
|
||||
# ADVERT_REF, the shared Hetzner OPENAI_API_KEY, and the shared mainnet
|
||||
# plumbing duplicated in full (NETWORK_ID, OGMIOS_URL, VECTOR_ZERO_TIME_MS,
|
||||
# ref-script UTxOs, LIVE_CHAIN). No env_file ordering / override magic.
|
||||
- path: ../../supplier/.env.ds-flash-htz
|
||||
required: false
|
||||
environment:
|
||||
LLM_BACKEND: openai
|
||||
# No /v1 suffix — supplier/src/openai.ts appends /v1/chat/completions
|
||||
# (Hetzner serves at https://inference.hetzner.com/api/v1/...).
|
||||
OPENAI_BASE_URL: https://inference.hetzner.com/api
|
||||
# Reasoning-style models can be slow; matches the advert's
|
||||
# max_processing_ms (300000).
|
||||
OPENAI_TIMEOUT_MS: "300000"
|
||||
# OPENAI_MAX_TOKENS intentionally unset: no max_tokens forwarded upstream,
|
||||
# the provider grants context − prompt. Advert cap = model context length.
|
||||
PORT: "8080"
|
||||
# Enable LiveOgmiosProvider so submitTx/awaitTx actually broadcast.
|
||||
LIVE_CHAIN: "1"
|
||||
# NETWORK_ID and OGMIOS_URL intentionally not pinned here — they MUST
|
||||
# come from the operator-provided supplier/.env.ds-flash-htz on a mainnet host.
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/healthz >/dev/null || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
networks:
|
||||
- marketplace-mainnet-net
|
||||
- traefik-net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik-net"
|
||||
- "traefik.http.routers.mp-suppliers-ds-flash-htz-mainnet.entrypoints=web"
|
||||
- "traefik.http.routers.mp-suppliers-ds-flash-htz-mainnet.rule=Host(`mp-suppliers-ds-flash-htz.vector.apexfusion.org`)"
|
||||
- "traefik.http.routers.mp-suppliers-ds-flash-htz-mainnet.middlewares=mp-suppliers-ds-flash-htz-mainnet-https"
|
||||
- "traefik.http.middlewares.mp-suppliers-ds-flash-htz-mainnet-https.redirectscheme.scheme=https"
|
||||
- "traefik.http.middlewares.mp-suppliers-ds-flash-htz-mainnet-https.redirectscheme.permanent=true"
|
||||
- "traefik.http.routers.mp-suppliers-ds-flash-htz-mainnet-tls.entrypoints=websecure"
|
||||
- "traefik.http.routers.mp-suppliers-ds-flash-htz-mainnet-tls.rule=Host(`mp-suppliers-ds-flash-htz.vector.apexfusion.org`)"
|
||||
- "traefik.http.routers.mp-suppliers-ds-flash-htz-mainnet-tls.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.mp-suppliers-ds-flash-htz-mainnet.loadbalancer.server.port=8080"
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
marketplace-mainnet-net:
|
||||
external: true
|
||||
name: marketplace-mainnet-net
|
||||
traefik-net:
|
||||
external: true
|
||||
name: traefik-net
|
||||
101
deploy/mainnet/docker-compose.supplier-glm-chat.yml
Normal file
101
deploy/mainnet/docker-compose.supplier-glm-chat.yml
Normal file
@@ -0,0 +1,101 @@
|
||||
# deploy/mainnet/docker-compose.supplier-glm-chat.yml — Supplier — MAINNET
|
||||
# (Hetzner Inference / GLM-5.2-NVFP4 — MULTI-TURN CHAT capability `llm.chat.v1`)
|
||||
#
|
||||
# The chat-session sibling of docker-compose.supplier-glm.yml. Same
|
||||
# image, same Hetzner backend (GLM-5.2-NVFP4), but CAPABILITY_KIND=chat-session so
|
||||
# it mounts /v1/chat/{start,message,end} (escrow bookends, off-chain streaming
|
||||
# turns) instead of the one-off /v1/chat/completions route.
|
||||
#
|
||||
# A SEPARATE process/advert from the one-off supplier on purpose: the chat
|
||||
# session holds the supplier's single-slot lock for the whole conversation
|
||||
# (minutes), which would starve one-off traffic if co-hosted.
|
||||
#
|
||||
# One of 8 Hetzner-backed suppliers (4 models x {one-off, chat-session});
|
||||
# fleet runbook: docs/HETZNER_SETUP.md.
|
||||
#
|
||||
# Pre-flight:
|
||||
# 1. DNS: mp-suppliers-glm-chat.vector.apexfusion.org → host
|
||||
# (individual DNS-only A record → 91.98.147.172; NO wildcard exists —
|
||||
# letsencrypt HTTP-01 issuance needs the unproxied direct record)
|
||||
# 2. supplier/.env.glm-chat written on host (chmod 600) with at minimum:
|
||||
# SUPPLIER_PRIV_KEY_HEX, SUPPLIER_ADDRESS, SUPPLIER_PKH,
|
||||
# SUPPLIER_PUB_KEY_HEX, OPENAI_API_KEY, OGMIOS_URL, NETWORK_ID=1,
|
||||
# VECTOR_ZERO_TIME_MS, ESCROW_REF_UTXO, ADVERT_REF_UTXO, ADVERT_REF,
|
||||
# LIVE_CHAIN=1
|
||||
# The advert must be posted with --capability-id llm.chat.v1 and a large
|
||||
# --max-processing-ms (~1800000) so Claim+Submit both land before deliver_by.
|
||||
# 3. Networks already exist (created by the canonical supplier compose):
|
||||
# docker network create marketplace-mainnet-net # one-shot
|
||||
# # traefik-net is created by the Traefik project
|
||||
|
||||
name: marketplace-mainnet-supplier-glm-chat
|
||||
|
||||
services:
|
||||
supplier:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: supplier/Dockerfile
|
||||
container_name: marketplace-mainnet-supplier-glm-chat
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
# Self-contained env for this Hetzner-backed variant. Holds this
|
||||
# supplier's distinct SUPPLIER_PRIV_KEY_HEX (+ derived wallet vars),
|
||||
# ADVERT_REF, the shared Hetzner OPENAI_API_KEY, and the shared mainnet
|
||||
# plumbing duplicated in full (NETWORK_ID, OGMIOS_URL, VECTOR_ZERO_TIME_MS,
|
||||
# ref-script UTxOs, LIVE_CHAIN). No env_file ordering / override magic.
|
||||
- path: ../../supplier/.env.glm-chat
|
||||
required: false
|
||||
environment:
|
||||
# Multi-turn chat session capability (mounts /v1/chat/{start,message,end}).
|
||||
CAPABILITY_KIND: chat-session
|
||||
LLM_BACKEND: openai
|
||||
# No /v1 suffix — supplier/src/openai.ts appends /v1/chat/completions
|
||||
# (Hetzner serves at https://inference.hetzner.com/api/v1/...).
|
||||
OPENAI_BASE_URL: https://inference.hetzner.com/api
|
||||
# Reasoning-style models can be slow; mirror the one-off compose.
|
||||
OPENAI_TIMEOUT_MS: "300000"
|
||||
# Idle end (supplier Submits the transcript): single-slot suppliers must
|
||||
# free up quickly (the gateway transparently reopens a session on the
|
||||
# next turn). Mirrors the kimi-chat fleet.
|
||||
CHAT_IDLE_TIMEOUT_MS: "900000"
|
||||
CHAT_SETTLE_MODE: ticket
|
||||
MAX_CHAT_SESSIONS: "8"
|
||||
PORT: "8080"
|
||||
# Enable LiveOgmiosProvider so Claim/Submit actually broadcast.
|
||||
LIVE_CHAIN: "1"
|
||||
# NETWORK_ID and OGMIOS_URL intentionally not pinned here — they MUST
|
||||
# come from the operator-provided supplier/.env.glm-chat on a mainnet host.
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/healthz >/dev/null || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
networks:
|
||||
- marketplace-mainnet-net
|
||||
- traefik-net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik-net"
|
||||
- "traefik.http.routers.mp-suppliers-glm-chat-mainnet.entrypoints=web"
|
||||
- "traefik.http.routers.mp-suppliers-glm-chat-mainnet.rule=Host(`mp-suppliers-glm-chat.vector.apexfusion.org`)"
|
||||
- "traefik.http.routers.mp-suppliers-glm-chat-mainnet.middlewares=mp-suppliers-glm-chat-mainnet-https"
|
||||
- "traefik.http.middlewares.mp-suppliers-glm-chat-mainnet-https.redirectscheme.scheme=https"
|
||||
- "traefik.http.middlewares.mp-suppliers-glm-chat-mainnet-https.redirectscheme.permanent=true"
|
||||
- "traefik.http.routers.mp-suppliers-glm-chat-mainnet-tls.entrypoints=websecure"
|
||||
- "traefik.http.routers.mp-suppliers-glm-chat-mainnet-tls.rule=Host(`mp-suppliers-glm-chat.vector.apexfusion.org`)"
|
||||
- "traefik.http.routers.mp-suppliers-glm-chat-mainnet-tls.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.mp-suppliers-glm-chat-mainnet.loadbalancer.server.port=8080"
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
marketplace-mainnet-net:
|
||||
external: true
|
||||
name: marketplace-mainnet-net
|
||||
traefik-net:
|
||||
external: true
|
||||
name: traefik-net
|
||||
89
deploy/mainnet/docker-compose.supplier-glm.yml
Normal file
89
deploy/mainnet/docker-compose.supplier-glm.yml
Normal file
@@ -0,0 +1,89 @@
|
||||
# deploy/mainnet/docker-compose.supplier-glm.yml — Supplier — MAINNET
|
||||
# (Hetzner Inference / GLM-5.2-NVFP4 — one-off `llm.text.generate.v1`)
|
||||
#
|
||||
# Hetzner-backed supplier. LLM_BACKEND=openai pointed directly at Hetzner's
|
||||
# hosted inference API (OpenAI-compatible at https://inference.hetzner.com/api).
|
||||
# No local proxy container — reachable over public HTTPS.
|
||||
#
|
||||
# One of 8 Hetzner-backed suppliers (4 models x {one-off, chat-session});
|
||||
# fleet runbook: docs/HETZNER_SETUP.md.
|
||||
#
|
||||
# Pre-flight:
|
||||
# 1. DNS: mp-suppliers-glm.vector.apexfusion.org → host
|
||||
# (individual DNS-only A record → 91.98.147.172; NO wildcard exists —
|
||||
# letsencrypt HTTP-01 issuance needs the unproxied direct record)
|
||||
# 2. supplier/.env.glm written on host (chmod 600) with at minimum:
|
||||
# SUPPLIER_PRIV_KEY_HEX, SUPPLIER_ADDRESS, SUPPLIER_PKH,
|
||||
# SUPPLIER_PUB_KEY_HEX, OPENAI_API_KEY, OGMIOS_URL, NETWORK_ID=1,
|
||||
# VECTOR_ZERO_TIME_MS, ESCROW_REF_UTXO, ADVERT_REF_UTXO, ADVERT_REF,
|
||||
# LIVE_CHAIN=1
|
||||
# 3. Networks already exist (created by the canonical supplier compose):
|
||||
# docker network create marketplace-mainnet-net # one-shot
|
||||
# # traefik-net is created by the Traefik project
|
||||
|
||||
name: marketplace-mainnet-supplier-glm
|
||||
|
||||
services:
|
||||
supplier:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: supplier/Dockerfile
|
||||
container_name: marketplace-mainnet-supplier-glm
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
# Self-contained env for this Hetzner-backed variant. Holds this
|
||||
# supplier's distinct SUPPLIER_PRIV_KEY_HEX (+ derived wallet vars),
|
||||
# ADVERT_REF, the shared Hetzner OPENAI_API_KEY, and the shared mainnet
|
||||
# plumbing duplicated in full (NETWORK_ID, OGMIOS_URL, VECTOR_ZERO_TIME_MS,
|
||||
# ref-script UTxOs, LIVE_CHAIN). No env_file ordering / override magic.
|
||||
- path: ../../supplier/.env.glm
|
||||
required: false
|
||||
environment:
|
||||
LLM_BACKEND: openai
|
||||
# No /v1 suffix — supplier/src/openai.ts appends /v1/chat/completions
|
||||
# (Hetzner serves at https://inference.hetzner.com/api/v1/...).
|
||||
OPENAI_BASE_URL: https://inference.hetzner.com/api
|
||||
# Reasoning-style models can be slow; matches the advert's
|
||||
# max_processing_ms (300000).
|
||||
OPENAI_TIMEOUT_MS: "300000"
|
||||
# OPENAI_MAX_TOKENS intentionally unset: no max_tokens forwarded upstream,
|
||||
# the provider grants context − prompt. Advert cap = model context length.
|
||||
PORT: "8080"
|
||||
# Enable LiveOgmiosProvider so submitTx/awaitTx actually broadcast.
|
||||
LIVE_CHAIN: "1"
|
||||
# NETWORK_ID and OGMIOS_URL intentionally not pinned here — they MUST
|
||||
# come from the operator-provided supplier/.env.glm on a mainnet host.
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/healthz >/dev/null || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
networks:
|
||||
- marketplace-mainnet-net
|
||||
- traefik-net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik-net"
|
||||
- "traefik.http.routers.mp-suppliers-glm-mainnet.entrypoints=web"
|
||||
- "traefik.http.routers.mp-suppliers-glm-mainnet.rule=Host(`mp-suppliers-glm.vector.apexfusion.org`)"
|
||||
- "traefik.http.routers.mp-suppliers-glm-mainnet.middlewares=mp-suppliers-glm-mainnet-https"
|
||||
- "traefik.http.middlewares.mp-suppliers-glm-mainnet-https.redirectscheme.scheme=https"
|
||||
- "traefik.http.middlewares.mp-suppliers-glm-mainnet-https.redirectscheme.permanent=true"
|
||||
- "traefik.http.routers.mp-suppliers-glm-mainnet-tls.entrypoints=websecure"
|
||||
- "traefik.http.routers.mp-suppliers-glm-mainnet-tls.rule=Host(`mp-suppliers-glm.vector.apexfusion.org`)"
|
||||
- "traefik.http.routers.mp-suppliers-glm-mainnet-tls.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.mp-suppliers-glm-mainnet.loadbalancer.server.port=8080"
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
marketplace-mainnet-net:
|
||||
external: true
|
||||
name: marketplace-mainnet-net
|
||||
traefik-net:
|
||||
external: true
|
||||
name: traefik-net
|
||||
101
deploy/mainnet/docker-compose.supplier-kimi-code-chat.yml
Normal file
101
deploy/mainnet/docker-compose.supplier-kimi-code-chat.yml
Normal file
@@ -0,0 +1,101 @@
|
||||
# deploy/mainnet/docker-compose.supplier-kimi-code-chat.yml — Supplier — MAINNET
|
||||
# (Hetzner Inference / Kimi-K2.7-Code — MULTI-TURN CHAT capability `llm.chat.v1`)
|
||||
#
|
||||
# The chat-session sibling of docker-compose.supplier-kimi-code.yml. Same
|
||||
# image, same Hetzner backend (Kimi-K2.7-Code), but CAPABILITY_KIND=chat-session so
|
||||
# it mounts /v1/chat/{start,message,end} (escrow bookends, off-chain streaming
|
||||
# turns) instead of the one-off /v1/chat/completions route.
|
||||
#
|
||||
# A SEPARATE process/advert from the one-off supplier on purpose: the chat
|
||||
# session holds the supplier's single-slot lock for the whole conversation
|
||||
# (minutes), which would starve one-off traffic if co-hosted.
|
||||
#
|
||||
# One of 8 Hetzner-backed suppliers (4 models x {one-off, chat-session});
|
||||
# fleet runbook: docs/HETZNER_SETUP.md.
|
||||
#
|
||||
# Pre-flight:
|
||||
# 1. DNS: mp-suppliers-kimi-code-chat.vector.apexfusion.org → host
|
||||
# (individual DNS-only A record → 91.98.147.172; NO wildcard exists —
|
||||
# letsencrypt HTTP-01 issuance needs the unproxied direct record)
|
||||
# 2. supplier/.env.kimi-code-chat written on host (chmod 600) with at minimum:
|
||||
# SUPPLIER_PRIV_KEY_HEX, SUPPLIER_ADDRESS, SUPPLIER_PKH,
|
||||
# SUPPLIER_PUB_KEY_HEX, OPENAI_API_KEY, OGMIOS_URL, NETWORK_ID=1,
|
||||
# VECTOR_ZERO_TIME_MS, ESCROW_REF_UTXO, ADVERT_REF_UTXO, ADVERT_REF,
|
||||
# LIVE_CHAIN=1
|
||||
# The advert must be posted with --capability-id llm.chat.v1 and a large
|
||||
# --max-processing-ms (~1800000) so Claim+Submit both land before deliver_by.
|
||||
# 3. Networks already exist (created by the canonical supplier compose):
|
||||
# docker network create marketplace-mainnet-net # one-shot
|
||||
# # traefik-net is created by the Traefik project
|
||||
|
||||
name: marketplace-mainnet-supplier-kimi-code-chat
|
||||
|
||||
services:
|
||||
supplier:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: supplier/Dockerfile
|
||||
container_name: marketplace-mainnet-supplier-kimi-code-chat
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
# Self-contained env for this Hetzner-backed variant. Holds this
|
||||
# supplier's distinct SUPPLIER_PRIV_KEY_HEX (+ derived wallet vars),
|
||||
# ADVERT_REF, the shared Hetzner OPENAI_API_KEY, and the shared mainnet
|
||||
# plumbing duplicated in full (NETWORK_ID, OGMIOS_URL, VECTOR_ZERO_TIME_MS,
|
||||
# ref-script UTxOs, LIVE_CHAIN). No env_file ordering / override magic.
|
||||
- path: ../../supplier/.env.kimi-code-chat
|
||||
required: false
|
||||
environment:
|
||||
# Multi-turn chat session capability (mounts /v1/chat/{start,message,end}).
|
||||
CAPABILITY_KIND: chat-session
|
||||
LLM_BACKEND: openai
|
||||
# No /v1 suffix — supplier/src/openai.ts appends /v1/chat/completions
|
||||
# (Hetzner serves at https://inference.hetzner.com/api/v1/...).
|
||||
OPENAI_BASE_URL: https://inference.hetzner.com/api
|
||||
# Reasoning-style models can be slow; mirror the one-off compose.
|
||||
OPENAI_TIMEOUT_MS: "300000"
|
||||
# Idle end (supplier Submits the transcript): single-slot suppliers must
|
||||
# free up quickly (the gateway transparently reopens a session on the
|
||||
# next turn). Mirrors the kimi-chat fleet.
|
||||
CHAT_IDLE_TIMEOUT_MS: "900000"
|
||||
CHAT_SETTLE_MODE: ticket
|
||||
MAX_CHAT_SESSIONS: "8"
|
||||
PORT: "8080"
|
||||
# Enable LiveOgmiosProvider so Claim/Submit actually broadcast.
|
||||
LIVE_CHAIN: "1"
|
||||
# NETWORK_ID and OGMIOS_URL intentionally not pinned here — they MUST
|
||||
# come from the operator-provided supplier/.env.kimi-code-chat on a mainnet host.
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/healthz >/dev/null || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
networks:
|
||||
- marketplace-mainnet-net
|
||||
- traefik-net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik-net"
|
||||
- "traefik.http.routers.mp-suppliers-kimi-code-chat-mainnet.entrypoints=web"
|
||||
- "traefik.http.routers.mp-suppliers-kimi-code-chat-mainnet.rule=Host(`mp-suppliers-kimi-code-chat.vector.apexfusion.org`)"
|
||||
- "traefik.http.routers.mp-suppliers-kimi-code-chat-mainnet.middlewares=mp-suppliers-kimi-code-chat-mainnet-https"
|
||||
- "traefik.http.middlewares.mp-suppliers-kimi-code-chat-mainnet-https.redirectscheme.scheme=https"
|
||||
- "traefik.http.middlewares.mp-suppliers-kimi-code-chat-mainnet-https.redirectscheme.permanent=true"
|
||||
- "traefik.http.routers.mp-suppliers-kimi-code-chat-mainnet-tls.entrypoints=websecure"
|
||||
- "traefik.http.routers.mp-suppliers-kimi-code-chat-mainnet-tls.rule=Host(`mp-suppliers-kimi-code-chat.vector.apexfusion.org`)"
|
||||
- "traefik.http.routers.mp-suppliers-kimi-code-chat-mainnet-tls.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.mp-suppliers-kimi-code-chat-mainnet.loadbalancer.server.port=8080"
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
marketplace-mainnet-net:
|
||||
external: true
|
||||
name: marketplace-mainnet-net
|
||||
traefik-net:
|
||||
external: true
|
||||
name: traefik-net
|
||||
89
deploy/mainnet/docker-compose.supplier-kimi-code.yml
Normal file
89
deploy/mainnet/docker-compose.supplier-kimi-code.yml
Normal file
@@ -0,0 +1,89 @@
|
||||
# deploy/mainnet/docker-compose.supplier-kimi-code.yml — Supplier — MAINNET
|
||||
# (Hetzner Inference / Kimi-K2.7-Code — one-off `llm.text.generate.v1`)
|
||||
#
|
||||
# Hetzner-backed supplier. LLM_BACKEND=openai pointed directly at Hetzner's
|
||||
# hosted inference API (OpenAI-compatible at https://inference.hetzner.com/api).
|
||||
# No local proxy container — reachable over public HTTPS.
|
||||
#
|
||||
# One of 8 Hetzner-backed suppliers (4 models x {one-off, chat-session});
|
||||
# fleet runbook: docs/HETZNER_SETUP.md.
|
||||
#
|
||||
# Pre-flight:
|
||||
# 1. DNS: mp-suppliers-kimi-code.vector.apexfusion.org → host
|
||||
# (individual DNS-only A record → 91.98.147.172; NO wildcard exists —
|
||||
# letsencrypt HTTP-01 issuance needs the unproxied direct record)
|
||||
# 2. supplier/.env.kimi-code written on host (chmod 600) with at minimum:
|
||||
# SUPPLIER_PRIV_KEY_HEX, SUPPLIER_ADDRESS, SUPPLIER_PKH,
|
||||
# SUPPLIER_PUB_KEY_HEX, OPENAI_API_KEY, OGMIOS_URL, NETWORK_ID=1,
|
||||
# VECTOR_ZERO_TIME_MS, ESCROW_REF_UTXO, ADVERT_REF_UTXO, ADVERT_REF,
|
||||
# LIVE_CHAIN=1
|
||||
# 3. Networks already exist (created by the canonical supplier compose):
|
||||
# docker network create marketplace-mainnet-net # one-shot
|
||||
# # traefik-net is created by the Traefik project
|
||||
|
||||
name: marketplace-mainnet-supplier-kimi-code
|
||||
|
||||
services:
|
||||
supplier:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: supplier/Dockerfile
|
||||
container_name: marketplace-mainnet-supplier-kimi-code
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
# Self-contained env for this Hetzner-backed variant. Holds this
|
||||
# supplier's distinct SUPPLIER_PRIV_KEY_HEX (+ derived wallet vars),
|
||||
# ADVERT_REF, the shared Hetzner OPENAI_API_KEY, and the shared mainnet
|
||||
# plumbing duplicated in full (NETWORK_ID, OGMIOS_URL, VECTOR_ZERO_TIME_MS,
|
||||
# ref-script UTxOs, LIVE_CHAIN). No env_file ordering / override magic.
|
||||
- path: ../../supplier/.env.kimi-code
|
||||
required: false
|
||||
environment:
|
||||
LLM_BACKEND: openai
|
||||
# No /v1 suffix — supplier/src/openai.ts appends /v1/chat/completions
|
||||
# (Hetzner serves at https://inference.hetzner.com/api/v1/...).
|
||||
OPENAI_BASE_URL: https://inference.hetzner.com/api
|
||||
# Reasoning-style models can be slow; matches the advert's
|
||||
# max_processing_ms (300000).
|
||||
OPENAI_TIMEOUT_MS: "300000"
|
||||
# OPENAI_MAX_TOKENS intentionally unset: no max_tokens forwarded upstream,
|
||||
# the provider grants context − prompt. Advert cap = model context length.
|
||||
PORT: "8080"
|
||||
# Enable LiveOgmiosProvider so submitTx/awaitTx actually broadcast.
|
||||
LIVE_CHAIN: "1"
|
||||
# NETWORK_ID and OGMIOS_URL intentionally not pinned here — they MUST
|
||||
# come from the operator-provided supplier/.env.kimi-code on a mainnet host.
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/healthz >/dev/null || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
networks:
|
||||
- marketplace-mainnet-net
|
||||
- traefik-net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik-net"
|
||||
- "traefik.http.routers.mp-suppliers-kimi-code-mainnet.entrypoints=web"
|
||||
- "traefik.http.routers.mp-suppliers-kimi-code-mainnet.rule=Host(`mp-suppliers-kimi-code.vector.apexfusion.org`)"
|
||||
- "traefik.http.routers.mp-suppliers-kimi-code-mainnet.middlewares=mp-suppliers-kimi-code-mainnet-https"
|
||||
- "traefik.http.middlewares.mp-suppliers-kimi-code-mainnet-https.redirectscheme.scheme=https"
|
||||
- "traefik.http.middlewares.mp-suppliers-kimi-code-mainnet-https.redirectscheme.permanent=true"
|
||||
- "traefik.http.routers.mp-suppliers-kimi-code-mainnet-tls.entrypoints=websecure"
|
||||
- "traefik.http.routers.mp-suppliers-kimi-code-mainnet-tls.rule=Host(`mp-suppliers-kimi-code.vector.apexfusion.org`)"
|
||||
- "traefik.http.routers.mp-suppliers-kimi-code-mainnet-tls.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.mp-suppliers-kimi-code-mainnet.loadbalancer.server.port=8080"
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
marketplace-mainnet-net:
|
||||
external: true
|
||||
name: marketplace-mainnet-net
|
||||
traefik-net:
|
||||
external: true
|
||||
name: traefik-net
|
||||
101
deploy/mainnet/docker-compose.supplier-qwen35b-chat.yml
Normal file
101
deploy/mainnet/docker-compose.supplier-qwen35b-chat.yml
Normal file
@@ -0,0 +1,101 @@
|
||||
# deploy/mainnet/docker-compose.supplier-qwen35b-chat.yml — Supplier — MAINNET
|
||||
# (Hetzner Inference / Qwen/Qwen3.6-35B-A3B-FP8 — MULTI-TURN CHAT capability `llm.chat.v1`)
|
||||
#
|
||||
# The chat-session sibling of docker-compose.supplier-qwen35b.yml. Same
|
||||
# image, same Hetzner backend (Qwen/Qwen3.6-35B-A3B-FP8), but CAPABILITY_KIND=chat-session so
|
||||
# it mounts /v1/chat/{start,message,end} (escrow bookends, off-chain streaming
|
||||
# turns) instead of the one-off /v1/chat/completions route.
|
||||
#
|
||||
# A SEPARATE process/advert from the one-off supplier on purpose: the chat
|
||||
# session holds the supplier's single-slot lock for the whole conversation
|
||||
# (minutes), which would starve one-off traffic if co-hosted.
|
||||
#
|
||||
# One of 8 Hetzner-backed suppliers (4 models x {one-off, chat-session});
|
||||
# fleet runbook: docs/HETZNER_SETUP.md.
|
||||
#
|
||||
# Pre-flight:
|
||||
# 1. DNS: mp-suppliers-qwen35b-chat.vector.apexfusion.org → host
|
||||
# (individual DNS-only A record → 91.98.147.172; NO wildcard exists —
|
||||
# letsencrypt HTTP-01 issuance needs the unproxied direct record)
|
||||
# 2. supplier/.env.qwen35b-chat written on host (chmod 600) with at minimum:
|
||||
# SUPPLIER_PRIV_KEY_HEX, SUPPLIER_ADDRESS, SUPPLIER_PKH,
|
||||
# SUPPLIER_PUB_KEY_HEX, OPENAI_API_KEY, OGMIOS_URL, NETWORK_ID=1,
|
||||
# VECTOR_ZERO_TIME_MS, ESCROW_REF_UTXO, ADVERT_REF_UTXO, ADVERT_REF,
|
||||
# LIVE_CHAIN=1
|
||||
# The advert must be posted with --capability-id llm.chat.v1 and a large
|
||||
# --max-processing-ms (~1800000) so Claim+Submit both land before deliver_by.
|
||||
# 3. Networks already exist (created by the canonical supplier compose):
|
||||
# docker network create marketplace-mainnet-net # one-shot
|
||||
# # traefik-net is created by the Traefik project
|
||||
|
||||
name: marketplace-mainnet-supplier-qwen35b-chat
|
||||
|
||||
services:
|
||||
supplier:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: supplier/Dockerfile
|
||||
container_name: marketplace-mainnet-supplier-qwen35b-chat
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
# Self-contained env for this Hetzner-backed variant. Holds this
|
||||
# supplier's distinct SUPPLIER_PRIV_KEY_HEX (+ derived wallet vars),
|
||||
# ADVERT_REF, the shared Hetzner OPENAI_API_KEY, and the shared mainnet
|
||||
# plumbing duplicated in full (NETWORK_ID, OGMIOS_URL, VECTOR_ZERO_TIME_MS,
|
||||
# ref-script UTxOs, LIVE_CHAIN). No env_file ordering / override magic.
|
||||
- path: ../../supplier/.env.qwen35b-chat
|
||||
required: false
|
||||
environment:
|
||||
# Multi-turn chat session capability (mounts /v1/chat/{start,message,end}).
|
||||
CAPABILITY_KIND: chat-session
|
||||
LLM_BACKEND: openai
|
||||
# No /v1 suffix — supplier/src/openai.ts appends /v1/chat/completions
|
||||
# (Hetzner serves at https://inference.hetzner.com/api/v1/...).
|
||||
OPENAI_BASE_URL: https://inference.hetzner.com/api
|
||||
# Reasoning-style models can be slow; mirror the one-off compose.
|
||||
OPENAI_TIMEOUT_MS: "300000"
|
||||
# Idle end (supplier Submits the transcript): single-slot suppliers must
|
||||
# free up quickly (the gateway transparently reopens a session on the
|
||||
# next turn). Mirrors the kimi-chat fleet.
|
||||
CHAT_IDLE_TIMEOUT_MS: "900000"
|
||||
CHAT_SETTLE_MODE: ticket
|
||||
MAX_CHAT_SESSIONS: "8"
|
||||
PORT: "8080"
|
||||
# Enable LiveOgmiosProvider so Claim/Submit actually broadcast.
|
||||
LIVE_CHAIN: "1"
|
||||
# NETWORK_ID and OGMIOS_URL intentionally not pinned here — they MUST
|
||||
# come from the operator-provided supplier/.env.qwen35b-chat on a mainnet host.
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/healthz >/dev/null || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
networks:
|
||||
- marketplace-mainnet-net
|
||||
- traefik-net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik-net"
|
||||
- "traefik.http.routers.mp-suppliers-qwen35b-chat-mainnet.entrypoints=web"
|
||||
- "traefik.http.routers.mp-suppliers-qwen35b-chat-mainnet.rule=Host(`mp-suppliers-qwen35b-chat.vector.apexfusion.org`)"
|
||||
- "traefik.http.routers.mp-suppliers-qwen35b-chat-mainnet.middlewares=mp-suppliers-qwen35b-chat-mainnet-https"
|
||||
- "traefik.http.middlewares.mp-suppliers-qwen35b-chat-mainnet-https.redirectscheme.scheme=https"
|
||||
- "traefik.http.middlewares.mp-suppliers-qwen35b-chat-mainnet-https.redirectscheme.permanent=true"
|
||||
- "traefik.http.routers.mp-suppliers-qwen35b-chat-mainnet-tls.entrypoints=websecure"
|
||||
- "traefik.http.routers.mp-suppliers-qwen35b-chat-mainnet-tls.rule=Host(`mp-suppliers-qwen35b-chat.vector.apexfusion.org`)"
|
||||
- "traefik.http.routers.mp-suppliers-qwen35b-chat-mainnet-tls.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.mp-suppliers-qwen35b-chat-mainnet.loadbalancer.server.port=8080"
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
marketplace-mainnet-net:
|
||||
external: true
|
||||
name: marketplace-mainnet-net
|
||||
traefik-net:
|
||||
external: true
|
||||
name: traefik-net
|
||||
89
deploy/mainnet/docker-compose.supplier-qwen35b.yml
Normal file
89
deploy/mainnet/docker-compose.supplier-qwen35b.yml
Normal file
@@ -0,0 +1,89 @@
|
||||
# deploy/mainnet/docker-compose.supplier-qwen35b.yml — Supplier — MAINNET
|
||||
# (Hetzner Inference / Qwen/Qwen3.6-35B-A3B-FP8 — one-off `llm.text.generate.v1`)
|
||||
#
|
||||
# Hetzner-backed supplier. LLM_BACKEND=openai pointed directly at Hetzner's
|
||||
# hosted inference API (OpenAI-compatible at https://inference.hetzner.com/api).
|
||||
# No local proxy container — reachable over public HTTPS.
|
||||
#
|
||||
# One of 8 Hetzner-backed suppliers (4 models x {one-off, chat-session});
|
||||
# fleet runbook: docs/HETZNER_SETUP.md.
|
||||
#
|
||||
# Pre-flight:
|
||||
# 1. DNS: mp-suppliers-qwen35b.vector.apexfusion.org → host
|
||||
# (individual DNS-only A record → 91.98.147.172; NO wildcard exists —
|
||||
# letsencrypt HTTP-01 issuance needs the unproxied direct record)
|
||||
# 2. supplier/.env.qwen35b written on host (chmod 600) with at minimum:
|
||||
# SUPPLIER_PRIV_KEY_HEX, SUPPLIER_ADDRESS, SUPPLIER_PKH,
|
||||
# SUPPLIER_PUB_KEY_HEX, OPENAI_API_KEY, OGMIOS_URL, NETWORK_ID=1,
|
||||
# VECTOR_ZERO_TIME_MS, ESCROW_REF_UTXO, ADVERT_REF_UTXO, ADVERT_REF,
|
||||
# LIVE_CHAIN=1
|
||||
# 3. Networks already exist (created by the canonical supplier compose):
|
||||
# docker network create marketplace-mainnet-net # one-shot
|
||||
# # traefik-net is created by the Traefik project
|
||||
|
||||
name: marketplace-mainnet-supplier-qwen35b
|
||||
|
||||
services:
|
||||
supplier:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: supplier/Dockerfile
|
||||
container_name: marketplace-mainnet-supplier-qwen35b
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
# Self-contained env for this Hetzner-backed variant. Holds this
|
||||
# supplier's distinct SUPPLIER_PRIV_KEY_HEX (+ derived wallet vars),
|
||||
# ADVERT_REF, the shared Hetzner OPENAI_API_KEY, and the shared mainnet
|
||||
# plumbing duplicated in full (NETWORK_ID, OGMIOS_URL, VECTOR_ZERO_TIME_MS,
|
||||
# ref-script UTxOs, LIVE_CHAIN). No env_file ordering / override magic.
|
||||
- path: ../../supplier/.env.qwen35b
|
||||
required: false
|
||||
environment:
|
||||
LLM_BACKEND: openai
|
||||
# No /v1 suffix — supplier/src/openai.ts appends /v1/chat/completions
|
||||
# (Hetzner serves at https://inference.hetzner.com/api/v1/...).
|
||||
OPENAI_BASE_URL: https://inference.hetzner.com/api
|
||||
# Reasoning-style models can be slow; matches the advert's
|
||||
# max_processing_ms (300000).
|
||||
OPENAI_TIMEOUT_MS: "300000"
|
||||
# OPENAI_MAX_TOKENS intentionally unset: no max_tokens forwarded upstream,
|
||||
# the provider grants context − prompt. Advert cap = model context length.
|
||||
PORT: "8080"
|
||||
# Enable LiveOgmiosProvider so submitTx/awaitTx actually broadcast.
|
||||
LIVE_CHAIN: "1"
|
||||
# NETWORK_ID and OGMIOS_URL intentionally not pinned here — they MUST
|
||||
# come from the operator-provided supplier/.env.qwen35b on a mainnet host.
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/healthz >/dev/null || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
networks:
|
||||
- marketplace-mainnet-net
|
||||
- traefik-net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik-net"
|
||||
- "traefik.http.routers.mp-suppliers-qwen35b-mainnet.entrypoints=web"
|
||||
- "traefik.http.routers.mp-suppliers-qwen35b-mainnet.rule=Host(`mp-suppliers-qwen35b.vector.apexfusion.org`)"
|
||||
- "traefik.http.routers.mp-suppliers-qwen35b-mainnet.middlewares=mp-suppliers-qwen35b-mainnet-https"
|
||||
- "traefik.http.middlewares.mp-suppliers-qwen35b-mainnet-https.redirectscheme.scheme=https"
|
||||
- "traefik.http.middlewares.mp-suppliers-qwen35b-mainnet-https.redirectscheme.permanent=true"
|
||||
- "traefik.http.routers.mp-suppliers-qwen35b-mainnet-tls.entrypoints=websecure"
|
||||
- "traefik.http.routers.mp-suppliers-qwen35b-mainnet-tls.rule=Host(`mp-suppliers-qwen35b.vector.apexfusion.org`)"
|
||||
- "traefik.http.routers.mp-suppliers-qwen35b-mainnet-tls.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.mp-suppliers-qwen35b-mainnet.loadbalancer.server.port=8080"
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
marketplace-mainnet-net:
|
||||
external: true
|
||||
name: marketplace-mainnet-net
|
||||
traefik-net:
|
||||
external: true
|
||||
name: traefik-net
|
||||
133
docs/HETZNER_SETUP.md
Normal file
133
docs/HETZNER_SETUP.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# Hetzner Inference Supplier Fleet — Setup Runbook
|
||||
|
||||
> **Status:** shipped. Configures 8 mainnet supplier nodes that use **Hetzner
|
||||
> Inference** (`https://inference.hetzner.com`) as their **compute backend** for
|
||||
> bonded inference work commissioned on Vector. Config only — no code changes.
|
||||
|
||||
---
|
||||
|
||||
## 1. What this is
|
||||
|
||||
Hetzner Inference is an OpenAI-compatible hosted API. Because the supplier's
|
||||
upstream client (`supplier/src/openai.ts`) already speaks plain
|
||||
`POST /v1/chat/completions` with a Bearer token, pointing it at Hetzner is
|
||||
**pure configuration**: `LLM_BACKEND=openai` + `OPENAI_BASE_URL` + the API key.
|
||||
This is the same pattern as the DeepSeek-direct and OpenRouter suppliers, and
|
||||
the direct sibling of `docs/HUGGINGFACE_ROUTER_SETUP.md`.
|
||||
|
||||
One supplier per (model × capability) — 4 models, both the one-off
|
||||
`llm.text.generate.v1` and the multi-turn `llm.chat.v1` capability:
|
||||
|
||||
| Supplier name | Model (on-chain + API param, verbatim) | Capability | Advert max_output_tokens |
|
||||
|---|---|---|---|
|
||||
| `kimi-code` | `Kimi-K2.7-Code` | `llm.text.generate.v1` | 262144 |
|
||||
| `kimi-code-chat` | `Kimi-K2.7-Code` | `llm.chat.v1` | 262144 |
|
||||
| `glm` | `GLM-5.2-NVFP4` | `llm.text.generate.v1` | 512000 |
|
||||
| `glm-chat` | `GLM-5.2-NVFP4` | `llm.chat.v1` | 512000 |
|
||||
| `ds-flash-htz` | `DeepSeek-V4-Flash-0731` | `llm.text.generate.v1` | 512000 |
|
||||
| `ds-flash-htz-chat` | `DeepSeek-V4-Flash-0731` | `llm.chat.v1` | 512000 |
|
||||
| `qwen35b` | `Qwen/Qwen3.6-35B-A3B-FP8` | `llm.text.generate.v1` | 262144 |
|
||||
| `qwen35b-chat` | `Qwen/Qwen3.6-35B-A3B-FP8` | `llm.chat.v1` | 262144 |
|
||||
|
||||
Shared advert parameters: price **200000 lovelace (0.2 AP3X)** flat per job,
|
||||
bonds **1000000 lovelace (1 AP3X)** both sides, `max_processing_ms` **300000**
|
||||
for one-off / **1800000** for chat (the session spans the whole conversation).
|
||||
`max_output_tokens` is set to the model's context length: no `max_tokens` is
|
||||
forwarded upstream (`OPENAI_MAX_TOKENS` unset), so the advert value only gates
|
||||
what buyers may request.
|
||||
|
||||
Names follow the brand-by-model convention; `ds-flash-htz` carries a `-htz`
|
||||
suffix only because `deepseek-flash-*` was already taken by the OpenRouter
|
||||
fleet serving `deepseek/deepseek-v4-flash`.
|
||||
|
||||
## 2. Prerequisites
|
||||
|
||||
1. A Hetzner account with Inference access and an API key (usage-based billing
|
||||
to you as the node operator — the flat 0.2 AP3X job price is a business
|
||||
choice, not cost-derived). One key is shared by all 8 suppliers; Hetzner
|
||||
rate limits apply to the aggregate.
|
||||
2. Eight funded supplier wallets (50 AP3X each recommended; bonds are 1 AP3X
|
||||
per in-flight job and returned on completion).
|
||||
3. The usual mainnet supplier deploy prerequisites (`deploy/README.md`).
|
||||
4. **DNS**: one A record per supplier — `mp-suppliers-<name>.vector.apexfusion.org`
|
||||
→ `91.98.147.172`, **DNS-only/unproxied**. There is NO wildcard for
|
||||
`*.vector.apexfusion.org`; and because traefik issues certs on-box via
|
||||
Let's Encrypt HTTP-01 (port 80), a proxied/CDN record would break issuance.
|
||||
Certs are issued automatically within ~a minute of the record propagating —
|
||||
no restarts needed.
|
||||
|
||||
## 3. Verify the backend before spending on-chain
|
||||
|
||||
The four model ids come from `GET https://inference.hetzner.com/api/v1/models`
|
||||
and are sent **verbatim** as the upstream `model` param (the runtime uses
|
||||
`advert.model`). Before posting adverts, smoke-test each model:
|
||||
|
||||
```bash
|
||||
curl -sS -X POST https://inference.hetzner.com/api/v1/chat/completions \
|
||||
-H "Authorization: Bearer $HETZNER_KEY" -H "Content-Type: application/json" \
|
||||
-d '{"model":"GLM-5.2-NVFP4","messages":[{"role":"user","content":"ping"}]}' \
|
||||
| jq '.choices[0].message.content'
|
||||
```
|
||||
|
||||
`choices[0].message.content` must be a non-empty string with **no `max_tokens`
|
||||
sent** (matches runtime config) — the supplier throws `openai_malformed`
|
||||
otherwise and forfeits its 1 AP3X bond per failed job. For chat suppliers also
|
||||
verify a `tools`/`tool_choice` round-trip. All four models passed both checks
|
||||
on 2026-08-11.
|
||||
|
||||
## 4. Per-supplier bring-up (repeat ×8)
|
||||
|
||||
```bash
|
||||
# 1. wallet — prints privateKeyHex, publicKeyHex, pubKeyHash, address
|
||||
pnpm --filter @marketplace/supplier tx:gen-keypair --network 1
|
||||
# fund the printed address (50 AP3X recommended), then:
|
||||
|
||||
# 2. env file on the mainnet host
|
||||
cp supplier/.env.hetzner.example /root/agents-marketplace/supplier/.env.<name>
|
||||
chmod 600 /root/agents-marketplace/supplier/.env.<name>
|
||||
# fill in: ALL FOUR wallet vars (PRIV_KEY_HEX, ADDRESS, PKH, PUB_KEY_HEX —
|
||||
# they are NOT derived at boot; missing derived vars ⇒ 403 wrong_supplier),
|
||||
# OPENAI_API_KEY, and the ADVERT_* values from the table above.
|
||||
|
||||
# 3. advert (one-off shown; chat: --capability-id llm.chat.v1 --max-processing-ms 1800000)
|
||||
pnpm --filter @marketplace/supplier tx:post-advert \
|
||||
--capability-id llm.text.generate.v1 \
|
||||
--model 'GLM-5.2-NVFP4' \
|
||||
--max-output-tokens 512000 \
|
||||
--max-processing-ms 300000 \
|
||||
--price-lovelace 200000 \
|
||||
--endpoint-url https://mp-suppliers-<name>.vector.apexfusion.org
|
||||
# paste the printed "<txHash>#0" into ADVERT_REF in the env file
|
||||
|
||||
# 4. first start is always manual (CD skips compose projects with no running
|
||||
# containers); subsequent rollouts ride the mainnet CD pipeline
|
||||
docker compose -f deploy/mainnet/docker-compose.supplier-<name>.yml up -d
|
||||
|
||||
# 5. register the wallet for balance alerts
|
||||
# add {name, address} to wallet-monitor/wallets.json on the host
|
||||
```
|
||||
|
||||
## 5. Verification
|
||||
|
||||
- `curl https://mp-suppliers-<name>.vector.apexfusion.org/healthz` → `{"ok":true}`;
|
||||
`/capability` shows the right model + pkh.
|
||||
- Indexer: buyer `GET /v1/indexer/suppliers` lists the new entries with
|
||||
`advert_status: Active`, `status: free`.
|
||||
- Gateway `GET /openai/v1/models` includes the four Hetzner model ids.
|
||||
- Watch `docker logs -f marketplace-mainnet-supplier-<name>` through the first
|
||||
jobs for `openai_malformed` (reasoning-model content-shape risk).
|
||||
|
||||
## 6. Footguns
|
||||
|
||||
- `OPENAI_BASE_URL=https://inference.hetzner.com/api` — **no `/v1`**; the
|
||||
client appends `/v1/chat/completions`.
|
||||
- Do **not** set `OPENAI_REASONING` — `reasoning:{enabled:false}` is an
|
||||
OpenRouter-only param.
|
||||
- The advert `--model` must match the Hetzner id exactly, including the
|
||||
`Qwen/` prefix on `Qwen/Qwen3.6-35B-A3B-FP8` (slash is fine on-chain;
|
||||
precedent `moonshotai/kimi-k2.6`).
|
||||
- `Kimi-K2.7-Code` and `DeepSeek-V4-Flash-0731` auto-join the buyer's PDF pool
|
||||
(case-insensitive substring allowlist `["kimi","deepseek","gpt"]` in
|
||||
`buyer/src/pdf/caps.ts`); GLM and Qwen stay out. Deliberate — adjust
|
||||
`PDF_MODEL_ALLOWLIST`/`PDF_MODEL_DENYLIST` in the buyer env to change it.
|
||||
- `OPENAI_TIMEOUT_MS` must stay ≤ the advert's `max_processing_ms`.
|
||||
168
docs/HUGGINGFACE_ROUTER_SETUP.md
Normal file
168
docs/HUGGINGFACE_ROUTER_SETUP.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# HuggingFace Router Supplier — Setup Runbook (V1 / W1)
|
||||
|
||||
> **Status:** shipped. Configures a Vector supplier node to use the HuggingFace
|
||||
> **Inference Providers router** as its **compute backend** for bonded inference
|
||||
> work commissioned on Vector. Config only — no code changes.
|
||||
|
||||
---
|
||||
|
||||
## 1. What this is
|
||||
|
||||
The HuggingFace **Inference Providers router** (`https://router.huggingface.co`)
|
||||
is an OpenAI-compatible endpoint that fronts hundreds of hosted models across
|
||||
partner providers (Together, Fireworks, DeepInfra, Groq, …). It is the direct
|
||||
analogue of the OpenRouter backend the supplier already supports.
|
||||
|
||||
Because the supplier's upstream client (`supplier/src/openai.ts`) already speaks
|
||||
plain `POST /v1/chat/completions` with a Bearer token, pointing it at HuggingFace
|
||||
is **pure configuration**: set `LLM_BACKEND=openai` and point `OPENAI_BASE_URL` at
|
||||
the router. It is the same as adding any OpenAI-compatible compute backend, with
|
||||
the HuggingFace endpoint and token substituted in.
|
||||
|
||||
```
|
||||
buyer commissions bonded work ──▶ supplier node ──▶ HuggingFace router
|
||||
(AP3X escrow on Vector) │ (compute backend, hf_ token)
|
||||
▼
|
||||
supplier fulfils the work; the escrow settles on-chain
|
||||
```
|
||||
|
||||
The supplier node runs the inference to **fulfil work a buyer has commissioned
|
||||
under stake**; HuggingFace is simply the compute it uses to produce the result.
|
||||
|
||||
**What this does NOT do:** it does not add creator/model-author work-sharing,
|
||||
change protocol economics, or change how an advert's committed amount is sized.
|
||||
Those are separate, larger pieces of work tracked in the team's internal planning
|
||||
notes. This preset is the fast, low-risk on-ramp that proves the path.
|
||||
|
||||
---
|
||||
|
||||
## 2. Prerequisites
|
||||
|
||||
1. A HuggingFace account with **Inference Providers access** (a free monthly
|
||||
credit allowance, then usage-based per token — this is HuggingFace's billing
|
||||
to you as the node operator).
|
||||
2. An access token from <https://huggingface.co/settings/tokens> with the
|
||||
**"Make calls to Inference Providers"** permission. It starts with `hf_`.
|
||||
Treat it like a password.
|
||||
3. A funded supplier wallet (~5 AP3X for tx fees + bonds). Generate a seed with
|
||||
`openssl rand -hex 32`.
|
||||
4. The usual supplier deploy prerequisites (Ogmios URL, a publicly reachable
|
||||
HTTPS endpoint for buyers). See `deploy/README.md`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Steps
|
||||
|
||||
### 3.1 Create the env file
|
||||
|
||||
```bash
|
||||
cp supplier/.env.huggingface-chat.example supplier/.env
|
||||
chmod 600 supplier/.env
|
||||
```
|
||||
|
||||
Fill in `SUPPLIER_PRIV_KEY_HEX`, `OPENAI_API_KEY` (your `hf_…` token),
|
||||
`OGMIOS_URL`, and the `ADVERT_*` values. Key settings for this preset:
|
||||
|
||||
| Var | Value | Why |
|
||||
|-----|-------|-----|
|
||||
| `LLM_BACKEND` | `openai` | routes chat through the OpenAI-compatible client |
|
||||
| `OPENAI_BASE_URL` | `https://router.huggingface.co` | HF router; the `/v1/chat/completions` suffix is appended by the client — do **not** add it |
|
||||
| `OPENAI_API_KEY` | `hf_…` | your HF token (Bearer) |
|
||||
| `OPENAI_REASONING` | **unset** | HF 400s on the OpenRouter-only `reasoning` param (see §5) |
|
||||
| `ADVERT_MODEL` | provider-qualified id, e.g. `deepseek-ai/DeepSeek-V3:fastest` | see §4 |
|
||||
|
||||
### 3.2 Choose a model
|
||||
|
||||
HF uses **provider-qualified** model names. The value goes on-chain in
|
||||
`AdvertDatum.model` and is forwarded verbatim to the router:
|
||||
|
||||
- `deepseek-ai/DeepSeek-V3:fastest` — bare id + `:fastest` picks the fastest provider
|
||||
- `deepseek-ai/DeepSeek-V3:together` — pin a specific provider
|
||||
- `openai/gpt-oss-120b` — open-weight, HF-hosted
|
||||
|
||||
Browse models and their providers at
|
||||
<https://huggingface.co/models?inference_provider=all>.
|
||||
|
||||
Because the model is on-chain data, you can advertise any router-supported chat
|
||||
model without changing the supplier code — just post a new advert.
|
||||
|
||||
### 3.3 Smoke-test the token before going on-chain
|
||||
|
||||
Confirm your token + model + base URL work end to end, independent of the chain:
|
||||
|
||||
```bash
|
||||
curl https://router.huggingface.co/v1/chat/completions \
|
||||
-H "Authorization: Bearer $HF_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"model":"deepseek-ai/DeepSeek-V3:fastest",
|
||||
"messages":[{"role":"user","content":"say hi in 3 words"}],
|
||||
"stream":false}'
|
||||
```
|
||||
|
||||
A `choices[0].message.content` in the response means the compute backend is
|
||||
reachable. (Note: **no** `reasoning` field — see §5.)
|
||||
|
||||
### 3.4 Post the advert
|
||||
|
||||
```bash
|
||||
pnpm --filter @marketplace/supplier tx:post-advert \
|
||||
--capability-id llm.text.generate.v1 \
|
||||
--model deepseek-ai/DeepSeek-V3:fastest \
|
||||
--max-output-tokens 512 \
|
||||
--max-processing-ms 60000 \
|
||||
--price-lovelace 2000000 \
|
||||
--endpoint-url https://your-supplier.example.org
|
||||
```
|
||||
|
||||
Copy the printed `<txHash>#0` into `ADVERT_REF` in `supplier/.env`. Use
|
||||
`--dry-run` first to inspect the tx without submitting.
|
||||
|
||||
### 3.5 Boot
|
||||
|
||||
Start read-only first (`LIVE_CHAIN` unset) to confirm the node boots and can
|
||||
reach both Ogmios and the HF router, then set `LIVE_CHAIN=1` and restart to
|
||||
enable real Claim/Submit. Deploy via the supplier compose file as in
|
||||
`deploy/README.md`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Sizing the advert (read before you post one)
|
||||
|
||||
`ADVERT_PRICE_LOVELACE` is the **flat AP3X amount committed in escrow per job**,
|
||||
but the HuggingFace backend meters **per token**. The committed amount is fixed
|
||||
per job while upstream token usage varies, so:
|
||||
|
||||
- Size it for your **worst-case** prompt length, not the average.
|
||||
- Cap output tightly with `--max-output-tokens` — it is your main lever on
|
||||
upstream token usage.
|
||||
- Prefer **small** models here; large models have wide token-usage variance and
|
||||
are harder to size a fixed per-job amount around.
|
||||
- Buyer **input** tokens are not capped on-chain today. Until an input-token
|
||||
ceiling lands, stick to small models or add your own request-size guard.
|
||||
|
||||
This is a pre-existing protocol design point (fixed per-job advert amount vs.
|
||||
variable upstream token usage), not specific to HuggingFace. It is the main thing
|
||||
to account for when sizing an advert.
|
||||
|
||||
---
|
||||
|
||||
## 5. Footgun: `OPENAI_REASONING` must stay unset
|
||||
|
||||
`reasoning:{enabled:false}` is an **OpenRouter-specific** parameter. The HF
|
||||
router **rejects it with HTTP 400 on every request**. When copying an existing
|
||||
OpenRouter env, it is easy to carry `OPENAI_REASONING=off` over by accident.
|
||||
|
||||
The supplier guards against this: `loadConfig` **refuses to boot** if
|
||||
`OPENAI_REASONING` is disabled while `OPENAI_BASE_URL` resolves to
|
||||
`router.huggingface.co`, with a clear error naming the fix
|
||||
(`supplier/src/config.ts`). Just leave `OPENAI_REASONING` unset for HF.
|
||||
|
||||
---
|
||||
|
||||
## 6. Where this sits
|
||||
|
||||
This preset is the multi-model on-ramp that uses the HuggingFace router as a
|
||||
compute backend. Further work items (additional task adapters, advert-sizing
|
||||
refinements, and protocol-economics changes) are tracked separately in the team's
|
||||
internal planning notes, along with the upstream-terms check every operator must
|
||||
complete before pointing a node at a third-party compute backend.
|
||||
105
supplier/.env.hetzner.example
Normal file
105
supplier/.env.hetzner.example
Normal file
@@ -0,0 +1,105 @@
|
||||
# supplier/.env.hetzner.example
|
||||
#
|
||||
# Preset for a supplier that uses Hetzner Inference
|
||||
# (https://inference.hetzner.com) as its COMPUTE BACKEND to fulfil bonded
|
||||
# inference work on Vector. Hetzner exposes an OpenAI-compatible API, so this
|
||||
# reuses the existing LLM_BACKEND=openai path with NO code changes — config only.
|
||||
#
|
||||
# One template for all 8 Hetzner-backed suppliers (4 models x {one-off, chat}):
|
||||
# kimi-code[-chat] → Kimi-K2.7-Code (ctx 262144)
|
||||
# glm[-chat] → GLM-5.2-NVFP4 (ctx 512000)
|
||||
# ds-flash-htz[-chat] → DeepSeek-V4-Flash-0731 (ctx 512000)
|
||||
# qwen35b[-chat] → Qwen/Qwen3.6-35B-A3B-FP8 (ctx 262144)
|
||||
#
|
||||
# Copy to supplier/.env.<name> on the host (chmod 600) and fill in real values.
|
||||
# .env.* is gitignored — NEVER commit a populated copy.
|
||||
#
|
||||
# Runbook: docs/HETZNER_SETUP.md
|
||||
#
|
||||
# Lines marked SECRET must never be shared, logged, or committed.
|
||||
|
||||
# ─── Wallet identity ──────────────────────────────────────────────────────
|
||||
# Generate all four values in one shot (from repo root):
|
||||
# pnpm --filter @marketplace/supplier tx:gen-keypair --network 1
|
||||
#
|
||||
# ALL FOUR are required. The supplier does NOT re-derive address/pkh/pubkey
|
||||
# from the private key at boot — leaving them unset silently yields an empty
|
||||
# wallet identity and every escrow is rejected 403 wrong_supplier (this took
|
||||
# down the TTS supplier once; don't repeat it).
|
||||
|
||||
# 64-char hex Ed25519 private key seed for this supplier's wallet. SECRET.
|
||||
SUPPLIER_PRIV_KEY_HEX=PASTE_64_HEX_CHARS_HERE
|
||||
SUPPLIER_ADDRESS=PASTE_ADDR1_HERE
|
||||
SUPPLIER_PKH=PASTE_28_BYTE_HEX_HERE
|
||||
SUPPLIER_PUB_KEY_HEX=PASTE_32_BYTE_HEX_HERE
|
||||
|
||||
# ─── Chain plumbing (mainnet) ─────────────────────────────────────────────
|
||||
OGMIOS_URL=https://ogmios.vector.mainnet.apexfusion.org
|
||||
NETWORK_ID=1
|
||||
VECTOR_ZERO_TIME_MS=1756485600000
|
||||
|
||||
# CIP-33 reference scripts (shared mainnet deployment).
|
||||
ESCROW_REF_UTXO=c8d84c6d67ec67a1efe5e9c6c06d53020e05d1bb96d1c55ecb1eb7d5010c4d54#0
|
||||
ADVERT_REF_UTXO=c8d84c6d67ec67a1efe5e9c6c06d53020e05d1bb96d1c55ecb1eb7d5010c4d54#1
|
||||
|
||||
# OutputReference of this supplier's on-chain advertisement UTxO.
|
||||
# Post it first (see tx:post-advert below), then paste the printed "<txHash>#0".
|
||||
ADVERT_REF=PASTE_TXHASH_HERE#0
|
||||
|
||||
# LIVE_CHAIN: only the literal "1" opts in to real Claim/Submit txs.
|
||||
LIVE_CHAIN=1
|
||||
|
||||
# ─── Backend: Hetzner Inference (OpenAI-compatible) ───────────────────────
|
||||
LLM_BACKEND=openai
|
||||
|
||||
# Hetzner base URL. The supplier appends /v1/chat/completions itself, so do
|
||||
# NOT include the /api/v1 → only up to /api.
|
||||
OPENAI_BASE_URL=https://inference.hetzner.com/api
|
||||
|
||||
# Hetzner Inference API key. Shared across all 8 Hetzner-backed suppliers —
|
||||
# rate limits apply to the aggregate. SECRET.
|
||||
OPENAI_API_KEY=PASTE_HETZNER_KEY_HERE
|
||||
|
||||
# All four models are reasoning-style; generations can be slow.
|
||||
OPENAI_TIMEOUT_MS=300000
|
||||
|
||||
# Do NOT set OPENAI_REASONING — `reasoning:{enabled:false}` is an
|
||||
# OpenRouter-only param.
|
||||
# OPENAI_REASONING=
|
||||
|
||||
# OPENAI_MAX_TOKENS intentionally unset: no max_tokens is forwarded upstream,
|
||||
# Hetzner grants context − prompt. The advert's max_output_tokens (= model
|
||||
# context length) only caps what buyers may request.
|
||||
# OPENAI_MAX_TOKENS=
|
||||
|
||||
PORT=8080
|
||||
|
||||
# ─── tx:post-advert — run once before first deploy ────────────────────────
|
||||
# One-off (llm.text.generate.v1) suppliers, from repo root:
|
||||
# pnpm --filter @marketplace/supplier tx:post-advert \
|
||||
# --capability-id llm.text.generate.v1 \
|
||||
# --model <EXACT Hetzner model id, see table above> \
|
||||
# --max-output-tokens <262144 or 512000 per table> \
|
||||
# --max-processing-ms 300000 \
|
||||
# --price-lovelace 200000 \
|
||||
# --endpoint-url https://mp-suppliers-<name>.vector.apexfusion.org
|
||||
#
|
||||
# Chat (llm.chat.v1) suppliers: same, but
|
||||
# --capability-id llm.chat.v1 --max-processing-ms 1800000
|
||||
# (the session spans the whole conversation; Claim+Submit must land before
|
||||
# deliver_by — mirrors the kimi-chat fleet).
|
||||
#
|
||||
# CRITICAL: --model goes on-chain AND is sent verbatim to Hetzner as the API
|
||||
# `model` param (jobRunner uses advert.model). It must be the EXACT id from
|
||||
# GET https://inference.hetzner.com/api/v1/models — including the "Qwen/"
|
||||
# prefix on Qwen/Qwen3.6-35B-A3B-FP8.
|
||||
ADVERT_CAPABILITY_ID=llm.text.generate.v1
|
||||
ADVERT_MODEL=GLM-5.2-NVFP4
|
||||
ADVERT_MAX_OUTPUT_TOKENS=512000
|
||||
ADVERT_MAX_PROCESSING_MS=300000
|
||||
ADVERT_PRICE_LOVELACE=200000
|
||||
ADVERT_SUPPLIER_BOND_LOVELACE=1000000
|
||||
ADVERT_BUYER_BOND_LOVELACE=1000000
|
||||
ADVERT_ENDPOINT_URL=https://mp-suppliers-glm.vector.apexfusion.org
|
||||
ADVERT_DETAIL_URI=
|
||||
ADVERT_DETAIL_HASH=
|
||||
114
supplier/.env.huggingface-chat.example
Normal file
114
supplier/.env.huggingface-chat.example
Normal file
@@ -0,0 +1,114 @@
|
||||
# supplier/.env.huggingface-chat.example
|
||||
#
|
||||
# Preset for a chat supplier that uses the HuggingFace Inference Providers router
|
||||
# (https://router.huggingface.co) as its COMPUTE BACKEND to fulfil bonded
|
||||
# inference work commissioned on Vector. It is the OpenAI-compatible on-ramp:
|
||||
# the router is OpenAI-compatible, so this reuses the existing LLM_BACKEND=openai
|
||||
# path with NO code changes — it is config only.
|
||||
#
|
||||
# Copy to supplier/.env on the host (chmod 600) and fill in real values.
|
||||
# .env is gitignored — NEVER commit a populated copy.
|
||||
#
|
||||
# Runbook: docs/HUGGINGFACE_ROUTER_SETUP.md
|
||||
#
|
||||
# To generate a fresh Ed25519 wallet seed: openssl rand -hex 32
|
||||
# Lines marked SECRET must never be shared, logged, or committed.
|
||||
|
||||
# ─── Required ─────────────────────────────────────────────────────────────
|
||||
|
||||
# 64-char hex Ed25519 private key seed for this supplier's wallet. SECRET.
|
||||
SUPPLIER_PRIV_KEY_HEX=PASTE_64_HEX_CHARS_HERE
|
||||
|
||||
# Ogmios WebSocket URL. Testnet shown; use the mainnet URL for NETWORK_ID=1:
|
||||
# testnet: wss://ogmios.vector.testnet.apexfusion.org
|
||||
# mainnet: wss://ogmios.vector.mainnet.apexfusion.org
|
||||
OGMIOS_URL=wss://ogmios.vector.testnet.apexfusion.org
|
||||
|
||||
# OutputReference of this supplier's on-chain advertisement UTxO.
|
||||
# Format "<64-hex-tx-hash>#<int>". Post it first (see tx:post-advert below),
|
||||
# then paste the printed "<txHash>#0" here.
|
||||
ADVERT_REF=PASTE_TXHASH_HERE#0
|
||||
|
||||
# Cardano network ID. "0" = testnet, "1" = mainnet. Keep "0" until you have
|
||||
# validated the setup end-to-end on testnet.
|
||||
NETWORK_ID=0
|
||||
|
||||
# ─── Backend: HuggingFace router (OpenAI-compatible) ──────────────────────
|
||||
# Route the chat job runner at the HF Inference Providers router instead of a
|
||||
# local Ollama. The supplier POSTs to ${OPENAI_BASE_URL}/v1/chat/completions.
|
||||
LLM_BACKEND=openai
|
||||
|
||||
# HF router base URL. The supplier appends /v1/chat/completions itself, so do
|
||||
# NOT include the /v1 suffix here.
|
||||
OPENAI_BASE_URL=https://router.huggingface.co
|
||||
|
||||
# HuggingFace access token (starts with "hf_"). Create one at
|
||||
# https://huggingface.co/settings/tokens with "Make calls to Inference
|
||||
# Providers" permission. Forwarded as `Authorization: Bearer <token>`. SECRET.
|
||||
OPENAI_API_KEY=PASTE_HF_TOKEN_HERE
|
||||
|
||||
# First-token latency across hosted providers can exceed local Ollama — keep a
|
||||
# generous request timeout.
|
||||
OPENAI_TIMEOUT_MS=180000
|
||||
|
||||
# ─── IMPORTANT: do NOT set OPENAI_REASONING here ──────────────────────────
|
||||
# `reasoning:{enabled:false}` is an OpenRouter-only param. The HF router
|
||||
# rejects it with HTTP 400 on every request. The supplier refuses to boot if
|
||||
# OPENAI_REASONING is disabled while OPENAI_BASE_URL points at the HF router.
|
||||
# (This is the #1 footgun when copying an OpenRouter env — leave it unset.)
|
||||
# OPENAI_REASONING=
|
||||
|
||||
# Optional hard ceiling forwarded as `max_tokens`. Unset = provider grants the
|
||||
# max available (context − prompt). Keep below the model's context window.
|
||||
# OPENAI_MAX_TOKENS=
|
||||
|
||||
# ─── Enable real chain submissions ────────────────────────────────────────
|
||||
# LIVE_CHAIN: only the literal "1" opts in to real Claim/Submit txs. Leave
|
||||
# unset for a dry run (read-only, no chain writes) while testing.
|
||||
LIVE_CHAIN=
|
||||
|
||||
# HTTP listen port inside the container. Compose maps this to the host.
|
||||
PORT=8080
|
||||
|
||||
# ─── tx:post-advert — run once before first deploy ────────────────────────
|
||||
# Execute (from repo root):
|
||||
# pnpm --filter @marketplace/supplier tx:post-advert \
|
||||
# --capability-id llm.text.generate.v1 \
|
||||
# --model deepseek-ai/DeepSeek-V3:fastest \
|
||||
# --max-output-tokens 512 \
|
||||
# --max-processing-ms 60000 \
|
||||
# --price-lovelace 2000000 \
|
||||
# --endpoint-url https://mp-supplier.vector.testnet.apexfusion.org
|
||||
# Copy the printed "<txHash>#0" into ADVERT_REF above.
|
||||
#
|
||||
# NOTE on --model: HF uses PROVIDER-QUALIFIED model names. The bare id picks a
|
||||
# default provider; a ":<hint>" suffix pins routing:
|
||||
# deepseek-ai/DeepSeek-V3:fastest # cheapest/fastest provider for the model
|
||||
# deepseek-ai/DeepSeek-V3:together # pin a specific provider
|
||||
# openai/gpt-oss-120b # open-weight, HF-hosted
|
||||
# Browse models + providers at https://huggingface.co/models?inference_provider=all
|
||||
# The value lives on-chain in AdvertDatum.model — it is data, not code, so you
|
||||
# can advertise any router-supported model without touching the supplier.
|
||||
ADVERT_CAPABILITY_ID=llm.text.generate.v1
|
||||
ADVERT_MODEL=deepseek-ai/DeepSeek-V3:fastest
|
||||
ADVERT_MAX_OUTPUT_TOKENS=512
|
||||
ADVERT_MAX_PROCESSING_MS=60000
|
||||
|
||||
# Flat AP3X amount committed in escrow per job (1 AP3X = 1_000_000 lovelace).
|
||||
# The HF backend meters PER TOKEN while this committed amount is fixed per job,
|
||||
# so size it for your worst-case prompt length and cap output tightly via
|
||||
# ADVERT_MAX_OUTPUT_TOKENS. See docs/HUGGINGFACE_ROUTER_SETUP.md §4.
|
||||
ADVERT_PRICE_LOVELACE=2000000
|
||||
ADVERT_SUPPLIER_BOND_LOVELACE=1000000
|
||||
ADVERT_BUYER_BOND_LOVELACE=1000000
|
||||
|
||||
# HTTPS endpoint buyers will call (must be publicly reachable).
|
||||
ADVERT_ENDPOINT_URL=https://mp-supplier.vector.testnet.apexfusion.org
|
||||
|
||||
# Optional off-chain detail pointer + its sha256 (leave empty for v1).
|
||||
ADVERT_DETAIL_URI=
|
||||
ADVERT_DETAIL_HASH=
|
||||
|
||||
# ─── Optional: CIP-33 reference scripts (recommended on mainnet) ──────────
|
||||
# ESCROW_REF_UTXO=<txhash>#0
|
||||
# ADVERT_REF_UTXO=<txhash>#1
|
||||
@@ -15,7 +15,10 @@
|
||||
*
|
||||
* Required for CAPABILITY_KIND="chat" + LLM_BACKEND="openai":
|
||||
* OPENAI_BASE_URL — http(s):// URL for an OpenAI-compatible /v1/chat/completions
|
||||
* host (e.g. ChatMock localhost proxy backed by Codex OAuth).
|
||||
* host. Examples: ChatMock localhost proxy (Codex OAuth),
|
||||
* OpenRouter (https://openrouter.ai/api), or the HuggingFace
|
||||
* Inference Providers router (https://router.huggingface.co).
|
||||
* See docs/HUGGINGFACE_ROUTER_SETUP.md for the HF preset.
|
||||
*
|
||||
* Required for CAPABILITY_KIND="tts":
|
||||
* PIPER_URL — http(s):// URL for the openedai-speech-min PiperTTS host
|
||||
@@ -51,6 +54,15 @@ const TX_HASH_RE = /^[0-9a-fA-F]{64}$/;
|
||||
const NON_NEG_INT_RE = /^(?:0|[1-9]\d*)$/;
|
||||
const POS_INT_RE = /^[1-9]\d*$/;
|
||||
|
||||
/** Lowercased hostname of a URL, or "" if it doesn't parse. */
|
||||
function hostnameOf(url: string): string {
|
||||
try {
|
||||
return new URL(url).hostname.toLowerCase();
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export interface AdvertRef {
|
||||
txHash: string;
|
||||
index: number;
|
||||
@@ -336,6 +348,24 @@ export function loadConfig(env: Record<string, string | undefined>): SupplierCon
|
||||
reasoningRaw === "0" ||
|
||||
reasoningRaw === "no";
|
||||
|
||||
// HuggingFace router guard. The `reasoning:{enabled:false}` param is
|
||||
// OpenRouter-specific; the HF Inference Providers router
|
||||
// (router.huggingface.co) rejects it with HTTP 400 on EVERY request. This is
|
||||
// the easy footgun when copying an OpenRouter env to the HF preset, so fail
|
||||
// fast at boot with a clear message instead of 400-ing every job.
|
||||
// See docs/HUGGINGFACE_ROUTER_SETUP.md.
|
||||
if (
|
||||
llmBackend === "openai" &&
|
||||
openaiReasoningDisabled &&
|
||||
hostnameOf(openaiBaseUrl) === "router.huggingface.co"
|
||||
) {
|
||||
throw new Error(
|
||||
"loadConfig: OPENAI_REASONING must be unset for the HuggingFace router " +
|
||||
"(OPENAI_BASE_URL=https://router.huggingface.co) — it rejects " +
|
||||
"reasoning:{enabled:false} with HTTP 400. Remove OPENAI_REASONING.",
|
||||
);
|
||||
}
|
||||
|
||||
// OPENAI_MAX_TOKENS — optional hard ceiling forwarded as `max_tokens`. Unset
|
||||
// or non-positive omits it (provider grants max available = context − prompt).
|
||||
// Keep it below the model context (kimi 262144) or the provider 400s when
|
||||
|
||||
@@ -184,6 +184,46 @@ describe("loadConfig() — LIVE_CHAIN parsing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── loadConfig — HuggingFace router preset (W1) ────────────────────────────
|
||||
// The HF Inference Providers router (router.huggingface.co) is OpenAI-compatible
|
||||
// and reuses LLM_BACKEND=openai, but rejects the OpenRouter-specific
|
||||
// reasoning:{enabled:false} param with HTTP 400 — so OPENAI_REASONING must stay
|
||||
// unset for it. loadConfig fails fast at boot to catch the copy-from-OpenRouter
|
||||
// footgun. See docs/HUGGINGFACE_ROUTER_SETUP.md.
|
||||
|
||||
describe("loadConfig() — HuggingFace router guard", () => {
|
||||
const hfEnv = (over: Record<string, string> = {}) => ({
|
||||
...buildSampleEnv(),
|
||||
LLM_BACKEND: "openai",
|
||||
OPENAI_BASE_URL: "https://router.huggingface.co",
|
||||
OPENAI_API_KEY: "hf_sampletoken",
|
||||
...over,
|
||||
});
|
||||
|
||||
it("accepts the HF router with OPENAI_REASONING unset", () => {
|
||||
const cfg = loadConfig(hfEnv());
|
||||
expect(cfg.openaiBaseUrl).toBe("https://router.huggingface.co");
|
||||
expect(cfg.llmBackend).toBe("openai");
|
||||
expect(cfg.openaiReasoningDisabled).toBe(false);
|
||||
});
|
||||
|
||||
it("throws when OPENAI_REASONING=off is set against the HF router", () => {
|
||||
expect(() => loadConfig(hfEnv({ OPENAI_REASONING: "off" }))).toThrow(
|
||||
/OPENAI_REASONING must be unset for the HuggingFace router/i,
|
||||
);
|
||||
});
|
||||
|
||||
it("still allows OPENAI_REASONING=off for non-HF openai backends (e.g. OpenRouter)", () => {
|
||||
const cfg = loadConfig({
|
||||
...buildSampleEnv(),
|
||||
LLM_BACKEND: "openai",
|
||||
OPENAI_BASE_URL: "https://openrouter.ai/api",
|
||||
OPENAI_REASONING: "off",
|
||||
});
|
||||
expect(cfg.openaiReasoningDisabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── parseAdvertRef ──────────────────────────────────────────────────────────
|
||||
|
||||
describe("parseAdvertRef()", () => {
|
||||
|
||||
Reference in New Issue
Block a user