Run against the hosted service#

Closed beta: nothing to install. The package is not distributed during the closed beta; with a token you mint at app.tauopt.com/settings/tokens you drive the service at https://api.tauopt.com over plain HTTP from Python’s standard library, as the thirteen *_rest.py scripts of the hosted examples do (files this site serves; save one beside your shell) and as Use the service with no package shows call by call, or from an agent through the MCP server. Hosted beta: getting started walks the sign-in, the token page and the first loop once; this page is the reference for driving the service from a script. The client sections apply with repository access, and to everyone after the public release.

The beta’s three hosts are https://app.tauopt.com (sign-in, tokens, your experiments), https://api.tauopt.com (the REST API) and https://mcp.tauopt.com (the MCP server); one tau_ token works on both service hosts. With the base URL and the token, a script of the standard library drives the service (Use the service with no package); with repository access, a script written against Client(driver="local") runs against it by changing the constructor. The hosted client itself is featherweight (httpx + pydantic), the math runs on the service, and parallel workers across processes or nodes become possible. The client snippets on this page were verified by reading the client source and the two client-driven example scripts and were not run against a live service while validating this page; the package-free section’s calls, quickstart_rest.py, and the refusals it names were run against the service on the validation date, when the service ran engine 0.4.0, the version these pages describe.

One Client with two drivers: the local driver runs the engine in your process; the hosted driver sends the same calls over HTTPS to the service, where a token check, the quotas and rate limit, the same engine, and the experiment store answer; the MCP server fronts the same service for agents.

One Client, two drivers. Client(driver="local") runs the engine in your process; Client(base_url=..., token=...) sends the same four calls over HTTPS to the hosted service, where your token is checked, the quotas and the rate limit apply, and the same engine answers from an experiment store that takes asks and observations one at a time, which is what lets workers share one experiment. An agent reaches the same service through the MCP server.#

Before you start#

  • Repository access: any install tier works for the hosted client, the featherweight one included (Install TauOpt).

  • Your token was shown once, when you minted it on the tokens page, and is lane-scoped: a lane is one deployment of the service (the beta is one), and a token works only on the lane that issued it. Keep the token in an environment variable or a secrets manager, never in a script or a notebook cell (the curl lines on this page expand it into a short-lived process’s argument list on your own machine, readable by that machine’s other users while it runs; on a shared machine prefer the scripts, which read it from the environment); a lost token is replaced on the same page (mint a new one, or rotate), never recovered.

  • A lane runs the engine version it was deployed with, and these pages describe one version (the landing page states it); GET /health on the API host names the lane’s in build.engine_version, every suggestion’s provenance.engine_version repeats it, and on the validation date (2026-10-28) the beta ran engine 0.4.0, the version these pages describe. After an engine release the pages can lead the lane until it is redeployed: a strategy, policy, or provenance field the changelog dates after the lane’s version does not exist there yet.

  • The hosted snippets on this page read TAUOPT_BASE_URL and TAUOPT_TOKEN from the environment:

    export TAUOPT_BASE_URL="https://api.tauopt.com"
    export TAUOPT_TOKEN="tau_<the-value-the-tokens-page-showed>"
    

Use the service with no package#

Every call is one route of the REST API with the token as a bearer header, and the five below are the whole quickstart loop (Hosted beta: getting started walks the same five with each answer pasted in full). Ids come back as strings; the service accepts them back as numbers or as numeric strings in paths and bodies (the scripts send them as they came). The lines were run against the service on the validation date; the ids are that run’s. First, the token check (no usage is counted):

curl -sS -H "Authorization: Bearer $TAUOPT_TOKEN" "$TAUOPT_BASE_URL/v1/auth/check"
{"user_id":2}

Create an experiment (the body is an ExperimentIn); the answer is the experiment object, and its id is the handle for every later call:

curl -sS -H "Authorization: Bearer $TAUOPT_TOKEN" -H "Content-Type: application/json" -d '{"name":"hosted-smoke","parameters":[{"name":"x","type":"double","bounds":[0.0,1.0]}],"metric":{"name":"y","objective":"minimize"}}' "$TAUOPT_BASE_URL/v1/experiments"

Keep the experiment’s id in a shell variable (this run’s was 20), ask for a suggestion (an empty body), keep its id too (243 here), evaluate its assignments, and report the value against it:

EXP=20
curl -sS -H "Authorization: Bearer $TAUOPT_TOKEN" -H "Content-Type: application/json" -d '{}' "$TAUOPT_BASE_URL/v1/experiments/$EXP/suggestions"
SUG=243
curl -sS -H "Authorization: Bearer $TAUOPT_TOKEN" -H "Content-Type: application/json" -d "{\"suggestion_id\":$SUG,\"value\":0.25}" "$TAUOPT_BASE_URL/v1/experiments/$EXP/observations"

Read the best point, and archive the experiment when the study is done (a 204 with no body):

curl -sS -H "Authorization: Bearer $TAUOPT_TOKEN" "$TAUOPT_BASE_URL/v1/experiments/$EXP/best_assignments"
curl -sS -X DELETE -H "Authorization: Bearer $TAUOPT_TOKEN" "$TAUOPT_BASE_URL/v1/experiments/$EXP"
{"count":1,"data":[{"assignments":{"x":0.043668270111083984},"value":0.25}]}

Two more calls a loop reaches for, from the run that validated this page: the experiment list, called before that run’s create (archived experiments are not listed, so it is empty here; the window is limit and offset), and an ask on the experiment the run then created, whose body pins a strategy and the load-independent caps timebox (the config fields are the strategies and configuration table’s):

curl -sS -H "Authorization: Bearer $TAUOPT_TOKEN" "$TAUOPT_BASE_URL/v1/experiments?limit=50&offset=0"
{"count":0,"data":[]}
EXP=34
curl -sS -H "Authorization: Bearer $TAUOPT_TOKEN" -H "Content-Type: application/json" -d '{"config":{"strategy":"gp_ei","seed":7,"timebox_mode":"caps"}}' "$TAUOPT_BASE_URL/v1/experiments/$EXP/suggestions"

The answer is the suggestion object; its provenance.strategy names what answered: sobol on a fresh experiment, as the engine’s never-fail ladder answers a pinned GP strategy on an empty experiment (fallback_from: gp_ei; a GP needs one observation), and gp_ei once there is one.

What the client does for you, a script of your own must do itself: a 429 names its wait in Retry-After (bandwidth_full, quota_exceeded, quota_unverifiable, rate_limited; the active-experiment cap’s experiment_quota_exceeded sends none, so archive an experiment instead); a 503 whose body names one of the transport’s pre-commit set (experiment_busy, queue_full, and ask_slots_busy, retired at deploy 2 with the lane it guarded but kept in the set; each a refusal before any suggestion or observation is committed, with Retry-After) is retried by the client like a 429, and any other 503 on a POST – connection_pool_busy included, which can follow a committed write – is raised at once; a 409 space_exhausted on an ask is completion (not an error); a redirect is never followed (an http:// URL against the https-only service would turn the POST into a GET); and a lost answer is reconciled by listing before re-sending (GET .../suggestions?state=open, GET .../observations, GET /v1/experiments), as Recover after a lost response explains for the client. python/examples/quickstart_rest.py, branin_rest.py, and parallel_workers_rest.py do the first three of those in a shared helper of about 140 lines (every route waits out a 429 that names a Retry-After and a 503 experiment_busy, under a per-call budget of 600 s; a 429 with no Retry-After – the active-experiment cap’s, or an edge rate limit’s non-JSON answer, which the helper raises as [429] http_error – is raised at once; the listing after a lost answer is yours); Run the example scripts describes them. Three more answers the service can give mid-loop, each named on the REST reference: 503 experiment_busy (an update, an archive or a dismissal of yours found the experiment’s lock held by a running ask, usually – an ask itself waits for the lock on its worker and is never refused this way; wait its Retry-After of 1 s and resend), 409 suggestion_expired (an open suggestion older than the experiment’s suggestion_ttl_s, 24 h by default, was closed on the way in; its details carry the assignments, so report the result as a free-form point with them, then ask again), and 409 observation_cap_reached (4,000 observations per experiment). The admission answers an ask can meet under load (429 too_many_inflight_asks, too_many_queued and too_many_parked, 503 queue_full, the pre-authentication 429 credential_rate_limited) are explained with their remedies on Hosted beta: getting started.

Point the client at the service#

import os

from tauopt import Client

client = Client(base_url=os.environ["TAUOPT_BASE_URL"], token=os.environ["TAUOPT_TOKEN"])
exp = client.create_experiment(
    name="hosted-smoke",
    parameters=[{"name": "x", "type": "double", "bounds": (0.0, 1.0)}],
    metric={"name": "y", "objective": "minimize"},
)
print(exp.id, exp.state)
client.archive_experiment(exp.id)

hosted is the default driver, so a Client() with either argument missing raises ValueError: hosted driver requires base_url and token. Use https://api.tauopt.com exactly as written: the transport never follows redirects, and an http:// URL against an https-only service raises [301] unexpected_redirect (or [302]) rather than letting a redirect turn your POST into a GET.

Set the two timeouts#

Two independent budgets apply to every ask, and they live on different objects:

Setting

Where

Default

What it bounds

timeout_s

Client(..., timeout_s=)

60.0

httpx’s per-phase timeout on each attempt (connect, read, write, pool), not a total deadline; a timeout raises the httpx exception raw (ConnectTimeout when the connection stalls, ReadTimeout on a slow route) and is not retried. One exception: the suggest POST’s READ phase listens for the backstop of the ask’s deadline, max(1.25 * max_wait_s, max_wait_s + 3 s) + 40 s (415 s when max_wait_s is unset), whenever that exceeds timeout_s

max_wait_s

client.suggest(..., max_wait_s=)

unset: the engine ladder’s own per-rung timeboxes (10 s each by default); the client then listens 415 s on the suggest

The whole ask, server-side; the accepted domain is a number in (0, 300]

The client listens past max_wait_s on the suggest by construction, so a long ask needs no raised timeout_s; timeout_s still bounds every other route and phase. client.experiment(exp.id).progress.expected_suggest_s estimates the next ask’s cost from history.

When max_wait_s expires, on_timeout decides what happens: "ladder", the default, degrades down the fallback ladder and records the fallback in the suggestion’s provenance; "fail" raises [503] suggest_timeout instead of degrading, with Retry-After parsed onto e.retry_after so you can run your own backoff. Any other value raises ValueError before a request is sent. One deadline is outside on_timeout: with a config carrying timebox_mode: "caps", an expired max_wait_s raises AskDeadlineExceeded, a 503 ask_deadline_exceeded ApiError with no Retry-After (a caps re-ask repeats deterministic work) and the refusal receipt in e.details.

import os
import time

from tauopt import ApiError, Client

client = Client(
    base_url=os.environ["TAUOPT_BASE_URL"], token=os.environ["TAUOPT_TOKEN"], timeout_s=120.0
)
exp = client.create_experiment(
    name="hosted-timeouts",
    parameters=[{"name": "x", "type": "double", "bounds": (0.0, 1.0)}],
    metric={"name": "y", "objective": "minimize"},
)
for _ in range(5):
    try:
        sug = client.suggest(exp.id, max_wait_s=90.0, on_timeout="fail")
    except ApiError as e:
        if e.status_code == 503 and e.error_type != "ask_deadline_exceeded":
            time.sleep(e.retry_after or 5.0)
            continue
        raise
    client.observe(exp.id, suggestion_id=sug.id, value=(sug.assignments["x"] - 0.3) ** 2)
    print(sug.provenance.strategy, sug.provenance.fallback_from)
print(client.experiment(exp.id).progress.expected_suggest_s)
client.archive_experiment(exp.id)

The guard on error_type is the loop’s one machine-readable do-not-retry signal: a 503 suggest_timeout is waited out, while a caps-mode deadline (ask_deadline_exceeded, the 503 with no Retry-After) is re-raised, since a re-ask repeats the same deterministic work and only a larger max_wait_s, lower caps, or a cheaper strategy changes the answer. With the typed class, put except AskDeadlineExceeded before except ApiError: it is a subclass, so the order decides which handler sees it.

Set the suggestion TTL#

Every open suggestion on the hosted plane carries a time to live: suggestion_ttl_s on the experiment, in seconds, the service default of 86400 s when it is None and never expiring at 0. The next ask on the experiment closes any open suggestion older than the TTL, sets its expired_at, and reissues its bandwidth slot; an observation reported against an expired suggestion answers [409] suggestion_expired, with details carrying suggestion_id, expired_at, created_at, and the assignments, so the result can still be reported as a free-form point. Set the TTL at creation or move it with update_experiment (the new value applies from the next ask); read it back as suggestion_ttl_s on the experiment and read a suggestion’s expiry as expired_at on the suggestion listing. Because None means unchanged on update_experiment, as for every field, reverting an experiment to the service default has no Client form: send the REST PATCH with an explicit "suggestion_ttl_s": null (the MCP server’s update_experiment drops null inputs the same way). The local driver records and serves the value and never expires a suggestion, so expired_at is always None there. The hosted plane serves both fields (every experiment the walk above created read back "suggestion_ttl_s":null and every suggestion "expired_at":null).

import os

from tauopt import ApiError, Client

client = Client(base_url=os.environ["TAUOPT_BASE_URL"], token=os.environ["TAUOPT_TOKEN"])
exp = client.create_experiment(
    name="hosted-ttl",
    parameters=[{"name": "x", "type": "double", "bounds": (0.0, 1.0)}],
    metric={"name": "y", "objective": "minimize"},
    suggestion_ttl_s=3600,  # an hour per evaluation; unset = the service default (86400 s), 0 = never
)
print(exp.suggestion_ttl_s)
sug = client.suggest(exp.id)
value = (sug.assignments["x"] - 0.3) ** 2
try:
    client.observe(exp.id, suggestion_id=sug.id, value=value)
except ApiError as e:
    if e.error_type != "suggestion_expired":
        raise
    client.observe(exp.id, assignments=e.details["assignments"], value=value)  # the evidence still lands
print([(s.id, s.state, s.expired_at) for s in client.suggestions(exp.id)])
client.update_experiment(exp.id, suggestion_ttl_s=0)  # never expire, from the next ask on
client.archive_experiment(exp.id)

Handle 429 responses#

With the Python client, the transport retries a 429 on its own (and, under the same policy, a 503 whose body names one of the transport’s pre-commit set: experiment_busy, queue_full, and ask_slots_busy, retired at deploy 2 with the lane it guarded but kept in the set; a package-free script handles each answer itself: When the service says no): up to 5 attempts, honoring a numeric Retry-After (an HTTP-date value is treated as absent; capped at 8 seconds per attempt; without the header, the backoff starts at 0.5 seconds and doubles up to the same 8-second cap). When those attempts run out, the final response raises as ApiError with status_code == 429, the response headers on e.headers, and the parsed delay on e.retry_after. The 429 error types are told apart by e.error_type; their meanings and headers are in the errors reference and the REST reference. The ones a client loop meets most, and what to do about each:

  • bandwidth_full: every parallel_bandwidth slot holds an open suggestion. This is the expected backpressure signal under parallelism rather than a fault. Observe or close an open suggestion, or create the experiment with a larger parallel_bandwidth; workers that want to wait longer than the transport’s budget loop at their own layer, as python/examples/parallel_workers.py does. Details: The ask loop in Troubleshooting.

  • quota_exceeded: the suggestion quota (default 1,000 suggestions per rolling 24 hours); wait for Retry-After.

  • experiment_quota_exceeded: the active-experiment cap (default 25); it carries no Retry-After, and waiting does not help: archive finished experiments instead.

  • quota_unverifiable: the quota store could not be read, so the request was refused rather than admitted unchecked; wait for Retry-After.

  • rate_limited: the beta lane’s per-instance pre-authentication rate limit (150 requests per second with a burst of 300, every caller together) applies to every /v1 request on that lane, reads included, and answers with Retry-After 1. Its per-credential sibling credential_rate_limited (more than eight requests in flight from one token, more than 40 a second with a burst of 80, or more than 15 history-sized reads a second with a burst of 30) answers the same way; the transport’s bounded retries wait out both.

  • too_many_queued (Retry-After 1): asks beyond your in-flight bound (8 unless your organization carries an override; GET /v1/me reports it as caps.member.inflight_per_user) are not refused – they wait in your queue for a worker, up to eight, with X-Tauopt-Queued on the answer – and the ninth waiting ask is this refusal (details carries inflight, inflight_limit, queued and queued_limit; X-Tauopt-Admission reads refused_queue); the refused ask did no work, so the transport’s retry waits the second and resends. too_many_inflight_asks (Retry-After 1) is the per-user cap on retries that wait under one Idempotency-Key (one; details.joined is true): the client never sends that header, so it cannot meet this answer; a script that reuses a key can. Workers that share a token keep their fan-in at eight concurrent asks (Run parallel workers).

  • suggest_rate_limited (Retry-After 1): asks faster than about ten a second from one user (a burst of twenty).

The quotas never throttle reads or observations, so a running loop can land its results; the lane’s rate limits are the 429s a read or an observe can meet, and the transport’s bounded retries honor their 1-second Retry-After. Both quotas: Hosted beta quotas; every admission answer with its remedy: When the service says no.

Archive finished experiments#

client.archive_experiment(exp.id) soft-deletes an experiment (DELETE /v1/experiments/{id}, 204) and frees its slot under the active-experiment cap. The experiment stays readable: experiment() still returns it with state == "deleted", and best(), suggestions(), observations(), and suggestion dismissals keep working. suggest, observe, and update_experiment on it refuse with a 404, archiving twice 404s, and there is no unarchive. One transport subtlety: the DELETE retries on 429 and on 500, 502, 503, and 504, so an archive whose success response was lost can surface the retry’s 404 even though it landed; treat not_found from this call as already archived, which the snippet does. The snippet prints deleted.

import os

from tauopt import ApiError, Client

client = Client(base_url=os.environ["TAUOPT_BASE_URL"], token=os.environ["TAUOPT_TOKEN"])
exp = client.create_experiment(
    name="hosted-archive",
    parameters=[{"name": "x", "type": "double", "bounds": (0.0, 1.0)}],
    metric={"name": "y", "objective": "minimize"},
)
try:
    client.archive_experiment(exp.id)
except ApiError as e:
    if e.error_type != "not_found":
        raise
print(client.experiment(exp.id).state)

Recover after a lost response#

A POST that dies with a 5xx, or a dropped connection, may have committed server-side, so the transport never blindly retries a POST (its 5xx retries apply to the other methods; a POST retries only a 429 and a 503 whose body names one of the transport’s pre-commit set – experiment_busy, queue_full, and ask_slots_busy, retired at deploy 2 with the lane it guarded but kept in the set – each a refusal before any suggestion or observation is committed; a connection_pool_busy on a POST raises at once, since the pooled connection is released at commit and a post-commit read can answer it for a row already written). Before re-sending a suggest or an observe, list what the server holds. suggestions(experiment_id, state=None) lists suggestions in id order: every row by default, only the "open" ones (holding a bandwidth slot, awaiting an observation), or only the "closed" ones. observations(experiment_id) lists observations in id order, which is report order. Both return a Page (a list plus total, the service’s count under the filters; the list’s own count(value) method is untouched) and take limit and offset for a long history (limit absent lists every row and present clamps to 1..200; a negative offset is a 400), and both work on archived experiments. A lost create_experiment is a POST too: reconcile it with experiments() (GET /v1/experiments, one page of your active experiments newest first, each with its progress, with count the active total the cap counts; limit clamps to 1..200 and a negative offset is a 400) before creating again, because the active-experiment cap counts every create that landed. The suggest POST also takes an Idempotency-Key header (at most 128 bytes; a 400 validation_error past that): a resend with the same key on the same experiment joins the original ask instead of minting a second suggestion, and the same key on another experiment answers 409 idempotency_key_mismatch; the Python client does not send one, so list the open suggestions before resending. health() (GET /health, no token needed) and auth_check() (GET /v1/auth/check, the token’s user_id or a 401) are the two probes to run before blaming a lost response on the engine.

import os

from tauopt import Client

client = Client(base_url=os.environ["TAUOPT_BASE_URL"], token=os.environ["TAUOPT_TOKEN"])
exp = client.create_experiment(
    name="hosted-recovery",
    parameters=[{"name": "x", "type": "double", "bounds": (0.0, 1.0)}],
    metric={"name": "y", "objective": "minimize"},
)
first = client.suggest(exp.id)
client.observe(exp.id, suggestion_id=first.id, value=0.5)
second = client.suggest(exp.id)   # left open on purpose
print([(s.id, s.state) for s in client.suggestions(exp.id, state="open")])
print([(o.suggestion_id, o.value) for o in client.observations(exp.id)])
client.close_suggestion(exp.id, second.id)
client.archive_experiment(exp.id)

The two print lines have the same shape on both drivers (ids differ by driver). Run with Client(driver="local") in place of the hosted constructor, they read:

[('local-1-s2', 'open')]
[('local-1-s1', 0.5)]

If the lost call was an observe and the suggestion is listed as closed with your value, do not re-send it: a second observation on the same suggestion answers [409] conflict. If the lost call was a suggest and an open suggestion you never saw is listed, evaluate that one instead of asking again.

Run the example scripts#

Fifteen scripts exercise the hosted path end to end: thirteen over plain HTTP without the TauOpt package (five need their objective’s libraries), each a file this site serves under the hosted examples, and two through the client (in the repository’s python/examples/).

Without the TauOpt package#

quickstart_rest.py, branin_rest.py, and parallel_workers_rest.py need Python 3.10 or newer and the two environment variables, nothing else: save each from the hosted examples into the directory you run python from (no repository checkout is involved). Without the variables each prints one line and exits 0 (CI runs them that way, and python/tests/test_rest_examples.py pins that contract and runs all thirteen against a fake service); with them, each archives the experiment it created at the end unless --keep. quickstart_rest.py was run against the service on the validation date and reached the local run’s checkpoint (layers: 4, -0.0; a later run against the service is pasted on Hosted beta: getting started); the Branin and parallel-workers scripts were run against the service on 2026-10-22, when the Branin loop’s best fell below 1.0 and four workers completed 24 observations with zero bandwidth violations.

The ten use-case twins (exhaustion_rest.py, multimetric_rest.py, mixed_space_rest.py, experimental_design_rest.py, outcome_constraints_rest.py, sklearn_cross_validation_rest.py, xgboost_tuning_rest.py, pytorch_mlp_rest.py, ode_calibration_rest.py, convergence_plot_rest.py) follow the same contract with the objective code of their local scripts (a twin that evaluates a model or draws a plot needs its libraries: numpy, scipy, scikit-learn, xgboost, torch, matplotlib, and nothing else); in nine of them the seed rides each ask as an explicit config with timebox_mode: "caps", the load-independent mode a hosted user may set (exhaustion_rest.py enumerates a finite eight-point space to proof and sends no config). replay_suggestion.py has no twin: the replay runs the engine in-process, which the hosted service does not expose.

python quickstart_rest.py --help
usage: quickstart_rest.py [-h] [--budget BUDGET] [--keep]

The quickstart loop against the hosted TauOpt service with no package
installed.

options:
  -h, --help       show this help message and exit
  --budget BUDGET  observations to report (default 30)
  --keep           leave the experiment active at the end

quickstart_rest.py runs the quickstart page’s loop (lr and layers, val_score maximized) for --budget observations, prints each observation with the strategy that produced it, then the best assignments and value on one line.

python branin_rest.py --help
usage: branin_rest.py [-h] [--budget BUDGET] [--keep]

Branin end-to-end against the hosted TauOpt service with no package installed.

options:
  -h, --help       show this help message and exit
  --budget BUDGET  observations to report (default 30)
  --keep           leave the experiment active at the end

branin_rest.py is branin_hosted.py with no package: the same loop, the same per-observation lines, the final best, one suggestion’s provenance identity triple (engine_version, engine_sha, deps_fingerprint: the lane’s engine, which can trail the version these pages describe), and the < 1.0 smoke assert.

python parallel_workers_rest.py --help
usage: parallel_workers_rest.py [-h] [--workers WORKERS]
                                [--observations-per-worker OBSERVATIONS_PER_WORKER]
                                [--experiment-id EXPERIMENT_ID]
                                [--deadline-s DEADLINE_S] [--keep]

Parallel workers against the hosted TauOpt service with no package installed.

options:
  -h, --help            show this help message and exit
  --workers WORKERS     parallel workers, one open suggestion each (default 4)
  --observations-per-worker OBSERVATIONS_PER_WORKER
                        observations each worker reports before it stops
                        (default 6)
  --experiment-id EXPERIMENT_ID
                        reuse this experiment (a batch job array shares one
                        id) instead of creating one
  --deadline-s DEADLINE_S
                        seconds after start past which a 429 wait (quota or
                        bandwidth) exits nonzero (default 600.0)
  --keep                leave a created experiment active at the end

parallel_workers_rest.py is parallel_workers.py with no package: --workers threads (default 4) share one experiment created with parallel_bandwidth equal to the worker count, each reporting --observations-per-worker observations (default 6); --experiment-id reuses an existing experiment (never archived by the script). A 429 bandwidth_full is waited out as backpressure (the shared helper waits its Retry-After; the worker sleeps 0.5 s itself without the header), any other 429 with a Retry-After and a 503 experiment_busy the same way (a quota 429 without a header, 5 s), every wait bounded by --deadline-s (default 600; every call hands the helper what is left of it as its wait budget); a 409 space_exhausted stops the workers normally. The script exits nonzero on a bandwidth violation or a duplicate assignment anywhere in the run.

With the Python client#

branin_hosted.py and parallel_workers.py take --base-url and --token, default them to TAUOPT_BASE_URL and TAUOPT_TOKEN, and refuse to start when neither source is set. Neither archives its experiment, so each run takes one active-experiment slot until you archive it.

cd python
python examples/branin_hosted.py --help
usage: branin_hosted.py [-h] [--base-url BASE_URL] [--token TOKEN]
                        [--budget BUDGET]

options:
  -h, --help           show this help message and exit
  --base-url BASE_URL
  --token TOKEN
  --budget BUDGET

branin_hosted.py runs the Branin loop for --budget observations (default 30), prints each observation with the strategy that produced it, the final best, and one suggestion’s provenance identity triple (engine_version, engine_sha, deps_fingerprint), and asserts that the best value is below 1.0 as a smoke threshold. With the two variables exported, cd python && python examples/branin_hosted.py is the whole invocation; pass --budget to change the observation count.

python examples/parallel_workers.py --help
usage: parallel_workers.py [-h] [--base-url BASE_URL] [--token TOKEN]
                           [--workers WORKERS]
                           [--observations-per-worker OBSERVATIONS_PER_WORKER]
                           [--experiment-id EXPERIMENT_ID]
                           [--deadline-s DEADLINE_S]

options:
  -h, --help            show this help message and exit
  --base-url BASE_URL
  --token TOKEN
  --workers WORKERS
  --observations-per-worker OBSERVATIONS_PER_WORKER
  --experiment-id EXPERIMENT_ID
                        reuse this experiment (a batch job array shares one
                        id) instead of creating one
  --deadline-s DEADLINE_S
                        seconds after start past which a 429 wait (quota or
                        bandwidth) exits nonzero

parallel_workers.py starts --workers threads (default 4) that share one experiment created with parallel_bandwidth equal to the worker count, each reporting --observations-per-worker observations (default 6); --experiment-id reuses an existing experiment instead of creating one, so a batch job array shares a single id. Bandwidth-full 429s are treated as the expected backpressure: a worker waits Retry-After (1 s from the service; 0.5 s without the header) and re-asks. Any other 429 is a quota: the worker waits retry_after (5 s without a header) and re-asks. Every wait ends at --deadline-s seconds after the start (default 600): the worker then exits nonzero naming the error, which on a reused experiment whose slots never free points at stale open suggestions (close_all_suggestions frees them). A reused finite experiment that runs dry stops its workers normally and the run reports the observations it completed. The script exits nonzero on a bandwidth violation or a duplicate assignment anywhere in the run. With the two variables exported, cd python && python examples/parallel_workers.py --workers 4 is the whole invocation.

Without either source of credentials, both scripts exit with code 2:

branin_hosted.py: error: --base-url/--token required (or TAUOPT_BASE_URL/TAUOPT_TOKEN env)

Read a connection or auth failure#

  • httpx.ConnectError: the base URL or the network is wrong. Raised raw, never retried.

  • httpx.ReadTimeout: no answer within the read timeout – timeout_s on every route but the suggest POST, whose read timeout is the derived backstop above whenever that exceeds timeout_s (on its queue lane, provenance.lane worker, the service bounds every ask with its own backstop and answers 503 ask_deadline_exceeded past it, so a ReadTimeout on the suggest names a stalled connection rather than a hung ask – list the open suggestions before asking again). The other members of the httpx.TimeoutException family raise the same way and are never retried: ConnectTimeout when the connection itself stalls, WriteTimeout, and PoolTimeout.

  • [401] unauthorized ApiError: the token was rejected because it is unknown, revoked, expired, or issued for another lane (tokens are lane-scoped and shown once); mint a new one on the tokens page. The service answers every token failure with 401.

  • [301] or [302] unexpected_redirect: the scheme or host redirects (usually http:// to https://); use the exact URL you were given.

All four, with the full retry policy, are in Hosted transport.