client.chat.completions.create() sends a conversation to a model and returns
its reply. It is OpenAI's method, with the same arguments and the same response
shape, and it is what almost everything else here is built around.
from trace_sdk import Trace
client = Trace()
completion = client.chat.completions.create(
messages=[
{"role": "system", "content": "You answer in one sentence."},
{"role": "user", "content": "Why is the sky blue?"},
],
temperature=0.2,
max_tokens=200,
)
print(completion.choices[0].message.content)
Parameters#
| Parameter | Type | Default | Meaning |
|---|---|---|---|
messages | list[dict] | required | The conversation so far, oldest first. |
model | str | the roster default | A model id from the roster. |
tools | list | None | Python functions or OpenAI tool dicts. See Tool calling. |
tool_choice | str or dict | "auto" with tools | "auto", "none", "required", or a named function. |
temperature | float | provider default | 0 to 2. Lower is more predictable. |
top_p | float | provider default | Nucleus sampling. Prefer changing one of temperature or top_p, not both. |
max_tokens | int | 512 | Cap on the reply length. Trace caps this at 4000. |
stream | bool | False | Return an iterator of chunks instead. See Streaming. |
seed | int | None | Ask the provider for a repeatable sample. |
stop | str or list[str] | None | Strings that end the reply when generated. |
Only the parameters listed above are accepted. Any other keyword returns a
400 invalid_request_error, so upgrade this package to use a parameter Trace
adds later.
Parameters left at None are not sent at all — the provider's own default
applies, and the request body stays small.
model is optional#
The one place Trace loosens OpenAI's signature. Three ways to choose, most specific first:
client.chat.completions.create(messages=[...], model="openai/gpt-5.6-luna") # this request
client = Trace(model="openai/gpt-5.6-luna") # every request
client.chat.completions.create(messages=[...]) # the roster default
The roster default is published, not hidden: client.models.list() marks it
with is_default, trace models shows the same list, and every completion
reports the model that actually answered on completion.model. It is the
cheapest chat model on the roster.
Messages#
Each message is a dict with a role and content.
| Role | Use |
|---|---|
system | Standing instructions. Put it first. |
user | What the person said. |
assistant | What the model said previously. |
tool | The result of a tool call, keyed by tool_call_id. See Tool calling. |
The gateway is stateless: it remembers nothing between requests. To hold a conversation, append the assistant's message to your own list and send the whole list again.
messages = [{"role": "user", "content": "My name is Ada."}]
first = client.chat.completions.create(messages=messages)
messages.append(first.choices[0].message.to_dict())
messages.append({"role": "user", "content": "What is my name?"})
second = client.chat.completions.create(messages=messages)
print(second.text)
Append message.to_dict() rather than a message you rebuilt — with tools, the
ids that match a call to its result only live in there.
Determinism with seed#
seed is passed to the provider. With the same model, the same messages, the
same temperature, and the same seed, providers try to return the same sample.
Treat it as a strong hint rather than a guarantee: providers change hardware and
kernels, and identical output across weeks is not promised by anyone.
completion = client.chat.completions.create(
messages=[{"role": "user", "content": "Name one primary colour."}],
temperature=0,
seed=7,
)
The completion#
The mirrored surface, identical to OpenAI's:
completion.id
completion.model # the model that answered
completion.created
completion.choices[0].message.role # "assistant"
completion.choices[0].message.content # the text, or None on a tool turn
completion.choices[0].message.tool_calls # list[ToolCall]
completion.choices[0].finish_reason # "stop", "length", "tool_calls", …
completion.usage.prompt_tokens
completion.usage.completion_tokens
completion.usage.total_tokens
finish_reason is worth checking. "length" means the reply was cut off by
max_tokens, not that the model finished.
Trace's additions, which sit beside those rather than replacing them:
completion.usage.credits_spent # added to OpenAI's usage object
completion.text # choices[0].message.content, "" instead of None
completion.credits_spent # usage.credits_spent
completion.request_id # matches request_id on the ledger rows
completion.run_tools(messages, confirm=approve) # see Tool calling
completion.text is the same string as the mirrored path, with one difference:
a tool-only turn has no text, and .text is "" there where .content is
None. That makes printing and concatenating safe without a guard.
Anything the gateway returns that this SDK does not model is still reachable:
completion["object"] # "chat.completion"
completion.to_dict() # the whole body as a dict
credits_spent#
The number on the completion is the settled cost, not an estimate. Trace reserves before the call and settles against provider-reported cost before you can read the response. It is the number the curriculum's checkpoints refer to. See Credits and limits.
One choice per request#
Trace returns exactly one choice. Passing n greater than 1 is a 400 with
code: "unsupported_parameter". Call twice if you want two samples — you will
be charged for two, which is the honest accounting.
Safety#
Message text is redacted server-side before it reaches any provider, and only models on the reviewed child-safe roster can be called. See Authentication.
Errors#
Every failure raises a typed exception. The ones this method produces most often:
ModelNotFoundError— the model id is not on the roster; the exception lists the ids that are.InsufficientCreditsError— the balance cannot cover the request.BadRequestError—max_tokensover the cap, a malformed message,n > 1.RateLimitError— more than 30 requests in a minute from one key.
Full list, with the recovery for each: Errors.