Patterns for using OpenAI-compatible SDKs reliably inside agent loops.
An agent loop calls the model repeatedly — planning, acting, observing — so small inefficiencies compound fast. These patterns keep your loops predictable and cheap when running against the OpenDunes API.
Instantiate the SDK client once and reuse it for every call in the loop. Creating a new client per iteration adds TLS handshake overhead and makes it harder to share retry state.
import os
from openai import OpenAI
client = OpenAI(
base_url="https://opendunes.com/v1",
api_key=os.environ["OPENDUNES_API_KEY"],
)
def agent_loop(goal: str):
messages = [{"role": "user", "content": goal}]
while True:
resp = client.chat.completions.create(
model="google/gemini-2.5-pro",
messages=messages,
)
reply = resp.choices[0].message
messages.append(reply)
# ... parse reply, call tools, append tool results, break when doneStreaming keeps each turn responsive even when a step produces a long answer. The final chunk includes usage so you can track token spend per turn.
with client.chat.completions.stream(
model="meta-llama/llama-4-maverick",
messages=messages,
) as stream:
for chunk in stream:
delta = chunk.choices[0].delta.content or ""
print(delta, end="", flush=True)
usage = stream.get_final_completion().usageThe X-Balance-Available response header carries your remaining balance in micro-DA after each call. Check it and halt the loop before you run dry — a 402 (payment_required) mid-loop is harder to recover from than a graceful stop.
import httpx
# Use the raw HTTP response to read headers alongside the SDK response
# (or switch to the httpx client if you need header access natively)See Response Headers for the full header reference.
429 with backoff#OpenDunes puts no cap on the number of requests. A 429 comes from the provider serving the model when it is limiting its rate, and fast agent loops can meet one. Respect the Retry-After header rather than spinning.
import time
def call_with_backoff(client, **kwargs):
while True:
try:
return client.chat.completions.create(**kwargs)
except Exception as e:
if hasattr(e, "status_code") and e.status_code == 429:
retry_after = int(getattr(e, "response", {}).headers.get("Retry-After", 5))
time.sleep(retry_after)
else:
raiseRewrite the system message only when the agent's role changes. Keeping it constant across turns improves model consistency and makes logs easier to read in your dashboard.
X-Request-Id#Each response includes an X-Request-Id header (UUID v7). Store it alongside the turn index so you can correlate agent traces to specific API calls when debugging.