Safactory v2 routes all model traffic through gateway. The gateway exposes OpenAI-compatible endpoints, maps model names to configured upstream routes, records telemetry into the same storage backend used by launcher.py, and enforces per-session limits such as max_steps.
Create a local config first:
cp gateway/config.example.yaml gateway/config.local.yamlUse your own route keys and upstream model endpoints:
listen_host: 0.0.0.0
listen_port: 8000
base_session_path: /v1/sessions
max_steps: -1
storage_type: sqlite
storage_config:
db_url: sqlite://env_trajs.db
llm_routes:
geo3k_model:
base_url: http://YOUR_LLM_HOST/v1
api_key: YOUR_API_KEY
supports_stream: true
max_concurrency: 64Run it:
python -m gateway --config gateway/config.local.yamlThe gateway writes its service log to logs/gateway.log by default. Override that path with SAFACTORY_GATEWAY_LOG_PATH.
With the default base_session_path: /v1/sessions, the gateway exposes:
| Endpoint | Purpose |
|---|---|
GET /healthz |
Liveness probe. |
GET /readyz |
Readiness probe. Includes storage_type, storage_config, and max_steps. |
GET /metrics |
Prometheus-style metrics for admission, telemetry, and route inflight counts. |
POST /v1/chat/completions |
Standard OpenAI-compatible chat completions passthrough without session binding. |
POST /v1/responses |
Standard OpenAI-compatible responses passthrough without session binding. |
POST /v1/sessions/{session_id}/chat/completions |
Session-scoped chat completions. This is the main endpoint used by agent runtimes. |
POST /v1/sessions/{session_id}/responses |
Session-scoped responses endpoint. |
GET /v1/sessions/{session_id} |
Gateway session status. |
POST /v1/sessions/{session_id}/close |
Start or poll session close. A closing response includes Retry-After. |
GET /v1/sessions/{session_id}/latest-success-step?model=... |
Largest HTTP-200 step ID observed for the session and model. |
POST /v1/sessions/{session_id}/clean |
Idempotently clear a closed session from Gateway memory. |
Session-scoped requests are what make trajectory rows attach to a Safactory session_id.
llm_routes is a map from route key to upstream endpoint:
llm_routes:
dsv4pro:
base_url: http://10.0.0.10:8182/v1
api_key: null
supports_stream: true
max_concurrency: 256The request body model must be one of these keys. For launcher.py, this means --llm-model dsv4pro must match gateway.config.llm_routes.dsv4pro.
If AIEVOBOX_GATEWAY_CONFIG points to the gateway config file, launcher.py validates --llm-model before starting.
For SQLite, gateway and launcher must use the same DB:
# gateway/config.local.yaml
storage_type: sqlite
storage_config:
db_url: sqlite://env_trajs.dbpython launcher.py \
--db-path sqlite://env_trajs.db \
--gateway-base-url http://127.0.0.1:8000/v1/sessions \
--llm-model geo3k_model \
...At startup, launcher.py calls GET /readyz and fails early if gateway storage does not match the launcher storage settings. This prevents evaluators from reading an empty or partial trajectory DB.
For cloud storage, start both processes with storage_type: cloud and the cloud storage defaults expected by wt-data-gateway.
Gateway telemetry writes rows into session_steps with is_trainable = false. Request-side history is stored in messages, the current output in response, and event metadata in meta_json.
Relevant config:
telemetry:
mode: strict
loss_policy: fail_closed
capture_payload: full
payload_sample_rate: 1.0
redact_sensitive_fields: true
batch_size: 200
flush_interval_ms: 100
request_log:
enabled: true
path: logs/gateway_requests.jsonl
max_bytes: 104857600
backup_count: 5
body_limit_bytes: 0telemetry.mode: strict makes request handling fail closed when telemetry cannot be written. best_effort allows requests to proceed when telemetry fails.
request_log.body_limit_bytes: 0 records full request and response bodies in the JSONL request log. Increase privacy by setting a positive byte limit or disabling request logs.
- An agent runtime receives a
SimulationStartRequestfromlauncher.py. - The runtime calls
POST /v1/sessions/{session_id}/chat/completionsor/responses. - Gateway resolves the session, binds it to an environment row if available, routes the request, and enqueues telemetry.
launcher.pycallsPOST /v1/sessions/{session_id}/close; Gateway marks the in-memory sessionclosing, rejects new admission, and returns HTTP 200 withRetry-After: 10.- Launcher polls with exponential backoff until Gateway drains in-flight requests and flushes telemetry (
closed), or until the configured total timeout. Evaluation then continues in either case. - Reward commit asks
latest-success-stepfor the launcher model and reads that session/model/step from the DB. If the row is not yet visible, it retries three times at 10-second intervals, then falls back to the DB's largest step whosemeta_json.status_codeis 200. - After reward commit and the successful
finishedupdate of the environment row, Manager callscleanto release the Resolver, Telemetry, and Storage caches for that session.
Gateway-side close timing is controlled by session_close_timeout_s (default 90) and session_close_retry_after_s (default 10). Close coordination state is in memory only.
If max_steps is non-negative, the gateway emits a synthetic stop response once a model reaches the configured step budget for that session.
curl http://127.0.0.1:8000/readyz
curl http://127.0.0.1:8000/metrics
curl http://127.0.0.1:8000/v1/sessions/<session-id>Common startup failures:
| Symptom | Likely cause |
|---|---|
gateway is not reachable |
Gateway process is not running or --gateway-base-url points to the wrong host or port. |
gateway SQLite DB does not match launcher --db-path |
storage_config.db_url and --db-path differ. |
gateway model route(s) are not configured |
--llm-model is not present in llm_routes. |
| Upstream 401 or 404 | Route api_key, base_url, or upstream model naming is wrong. |