OpenAI SDK
The official openai SDKs (Python, Node.js) talk to Console unchanged. Point base_url at Console, use an API token as the key, and put agent:<key> in model. Everything you know from the OpenAI API (streaming, background, retrieve, cancel, structured output) works against an agent.
When to use it: you already have OpenAI SDK code, or you want the shortest path from a script to an agent. For CI, scheduling and callbacks, the runs API offers more; the SDK reaches the same engine, so you can mix both.
Set up
import os
from openai import OpenAI
client = OpenAI(
base_url=f"{os.environ['CONSOLE_URL']}/api/client/v1",
api_key=os.environ["COGNIPEER_API_TOKEN"], # cpeer_...
)
AGENT = "agent:support-bot"import OpenAI from 'openai';
const client = new OpenAI({
baseURL: `${process.env.CONSOLE_URL}/api/client/v1`,
apiKey: process.env.COGNIPEER_API_TOKEN, // cpeer_...
});
const AGENT = 'agent:support-bot';The agent runs its published version. Add version=3 or version="draft" in extra_body to pin or test (see extras).
Responses API
Synchronous
resp = client.responses.create(model=AGENT, input="Where is order 1234?")
print(resp.output_text)
print(resp.id) # resp_<runId>
print(resp.status) # completed | in_progress | queued | failed | ...const resp = await client.responses.create({ model: AGENT, input: 'Where is order 1234?' });
console.log(resp.output_text, resp.id, resp.status);The call waits up to the server's wait window (10 minutes by default). If the turn is still going then, the answer is 202 with status: "in_progress"; the SDK returns it like any response. Follow it with retrieve:
import time
while resp.status in ("queued", "in_progress"):
time.sleep(2)
resp = client.responses.retrieve(resp.id)Stream
with client.responses.stream(model=AGENT, input="Draft a status update") as stream:
for event in stream:
if event.type == "response.output_text.delta":
print(event.delta, end="", flush=True)
final = stream.get_final_response()
print("\n", final.id)const stream = await client.responses.create({ model: AGENT, input: 'Draft a status update', stream: true });
for await (const event of stream) {
if (event.type === 'response.output_text.delta') process.stdout.write(event.delta);
}Events: response.created, response.in_progress, response.output_item.added, response.content_part.added, response.output_text.delta, response.output_text.done, response.content_part.done, response.output_item.done, then response.completed (the full object), response.failed or response.incomplete. When the agent shows reasoning, reasoning-summary events precede the text. Closing the connection cancels the run, unless you sent background=True.
Background, retrieve, cancel
background=True returns at once with a run you follow later, and a dropped connection no longer cancels it.
resp = client.responses.create(
model=AGENT,
input="Research competitor pricing and write a summary",
background=True,
extra_headers={"Idempotency-Key": "pricing-2026-10-18"},
)
print(resp.id, resp.status) # resp_..., queued | in_progress
# Poll
while resp.status in ("queued", "in_progress"):
time.sleep(3)
resp = client.responses.retrieve(resp.id)
print(resp.status, resp.output_text)
# Or stop it
client.responses.cancel(resp.id) # POST /responses/{id}/cancellet r = await client.responses.create(
{ model: AGENT, input: 'Research competitor pricing', background: true },
{ headers: { 'Idempotency-Key': 'pricing-2026-10-18' } },
);
while (r.status === 'queued' || r.status === 'in_progress') {
await new Promise((res) => setTimeout(res, 3000));
r = await client.responses.retrieve(r.id);
}
await client.responses.cancel(r.id); // only while it still runsresp_<runId> is the run's id in the Responses API. The same run is run_<runId> in the runs API, and GET /responses/{id} accepts either. Cancelling a finished response returns the finished response, as OpenAI does.
Structured output
Ask for a JSON answer with text.format. The agent's own instructions and tools still run; only the final answer is shaped by the schema, which Console validates.
import json
schema = {
"type": "object",
"properties": {
"category": {"type": "string", "enum": ["billing", "bug", "feature", "other"]},
"urgency": {"type": "integer"},
"summary": {"type": "string"},
},
"required": ["category", "urgency", "summary"],
"additionalProperties": False,
}
resp = client.responses.create(
model=AGENT,
input="Classify: 'I was charged twice and the invoice page throws a 500'",
text={"format": {"type": "json_schema", "name": "ticket", "schema": schema, "strict": True}},
)
ticket = json.loads(resp.output_text)
# Console also returns the validated object (or why validation failed):
parsed = resp.model_extra.get("output_parsed")
error = resp.model_extra.get("output_error")const resp = await client.responses.create({
model: AGENT,
input: "Classify: 'I was charged twice and the invoice page throws a 500'",
text: { format: { type: 'json_schema', name: 'ticket', schema, strict: true } },
});
const ticket = JSON.parse(resp.output_text);
const parsed = (resp as any).output_parsed; // validated objectA schema Console cannot convert (an empty schema, an unsupported keyword) is 400 invalid_response_format, before any work starts. {"type": "json_object"} asks for any JSON object.
Files and images
resp = client.responses.create(
model=AGENT,
input=[{
"role": "user",
"content": [
{"type": "input_text", "text": "Extract the totals from this invoice"},
{"type": "input_file", "file_url": "https://files.example.com/invoice-1042.pdf"},
],
}],
)input_file takes file_id (a session file), file_data (a base64 data: URL) or file_url (http or https, fetched by Console, size-capped). input_image takes image_url. The agent must have attachments enabled, otherwise 403 attachments_disabled. Several input_text parts in the last message are joined. An unsupported part (input_audio) is 400 unsupported_content_part.
Continue a conversation
second = client.responses.create(model=AGENT, input="And its shipping address?", previous_response_id=resp.id)previous_response_id (a resp_ or run_ id), conversation and session_id all name the same Console session. One run per session at a time: a second request while the first is running is 409 agent_run_conflict.
Chat completions
The same agent behind the older API. Use it for clients that only know chat completions.
chat = client.chat.completions.create(
model=AGENT,
messages=[{"role": "user", "content": "Where is order 1234?"}],
)
print(chat.choices[0].message.content)
# Stream
stream = client.chat.completions.create(
model=AGENT,
messages=[{"role": "user", "content": "Write a haiku about queues"}],
stream=True,
stream_options={"include_usage": True},
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="")const stream = await client.chat.completions.create({
model: AGENT,
messages: [{ role: 'user', content: 'Write a haiku about queues' }],
stream: true,
stream_options: { include_usage: true },
});
for await (const chunk of stream) process.stdout.write(chunk.choices[0]?.delta?.content ?? '');Structured output uses response_format; the message content is then the JSON string:
chat = client.chat.completions.create(
model=AGENT,
messages=[{"role": "user", "content": "Classify: 'App crashes on login'"}],
response_format={"type": "json_schema", "json_schema": {"name": "ticket", "schema": schema, "strict": True}},
)
ticket = json.loads(chat.choices[0].message.content)Session handling (no session id in the OpenAI protocol): send X-Cognipeer-Session-Id, or conversation_id in the body, to continue a session; otherwise Console matches the resent history to a stored session (details). Every response returns X-Cognipeer-Session-Id. If a non-streaming call outlasts the wait window you get 202 and a body with choices: [] and cognipeer: { status, run_id, session_id }; follow run_id in the runs API. The SDK treats that as a normal response, so check chat.choices before reading it.
Extras: extra_body and headers
Console accepts fields the OpenAI API does not have. Send them with extra_body (Python) or by adding them to the request object (Node.js, with a cast, since the type does not know them).
| Field | Effect |
|---|---|
service_tier: "flex" | Run in the deferred class: low priority, own queue. Pair with background=True. See run classes. |
metadata: {...} | Up to 16 string key/values, stored on the run and echoed back. Searchable with GET /agents/runs?metadata[ticket]=OPS-12. |
variables: {...} | Values for the agent's variables, sent next to the message (extension). Checked before the model runs; for calls from code, not for chat. |
version: 3 or "draft" | Pin a version, or run the draft. |
agent_model: "claude-deep" | Pick one of the models the agent offers (Agent models). |
reasoning: {"effort": "low"} | Reasoning effort for this turn (reasoning_effort on chat). |
callback_url, callback_secret | For async runs: where to POST the result, and the signing secret. |
runtime_context, budget | Per-turn headers and prompt placeholders; the session budget. |
header Idempotency-Key | A retry of the same request returns the same run. |
resp = client.responses.create(
model=AGENT,
input="Triage the overnight alerts",
background=True,
service_tier="flex",
metadata={"ticket": "OPS-12", "source": "nightly"},
extra_body={"variables": {"environment": "prod"}, "version": 3},
extra_headers={"Idempotency-Key": "triage-2026-10-18"},
)const r = await client.responses.create(
{
model: AGENT,
input: 'Triage the overnight alerts',
background: true,
service_tier: 'flex',
metadata: { ticket: 'OPS-12', source: 'nightly' },
...({ variables: { environment: 'prod' }, version: 3 } as object),
},
{ headers: { 'Idempotency-Key': 'triage-2026-10-18' } },
);What the agent ignores
An agent has its own configuration. These request fields are accepted but not applied to agents, and are listed back in the X-Cognipeer-Ignored-Params response header: instructions, temperature, top_p, max_output_tokens, tools, tool_choice, parallel_tool_calls, truncation, and the system and developer messages. To call a model with those, use a model key instead of agent:<key>; the same endpoints serve models.
Errors
The SDK raises its normal exceptions with an OpenAI-shaped body (error.message, type, param, code):
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_response_format, invalid_variables, unsupported_content_part, invalid_json, invalid_version, attachment_unreachable | Validation; errors lists the fields of variables. invalid_json: the body is not a JSON object; invalid_version: version is not a positive integer, a numeric string or "draft"; attachment_unreachable: a remote file or image URL did not arrive in time. |
| 403 | attachments_disabled, permission_error | The agent takes no files; or the token lacks the agents service. |
| 404 | model_not_found | No agent or model by that name in the token's project. |
| 409 | agent_run_conflict, idempotency_key_conflict | Session busy; key reused for a different request. |
| 429 | agent_run_concurrency_limit, agent_quota_exceeded, spend_limit_exceeded | Too many active runs (retry later, or use flex; Retry-After: 30); the agent quota is used up (Retry-After: 60); the spend limit is used up. A stream is checked before it opens, so this is a plain 429 and not a stream error. |
| 503 | agent_run_interrupted | The run the call waited on was ended elsewhere (the server shut down, the worker was lost) before it finished. Retry; tool side effects may have happened. |
The SDKs retry 429 and 5xx by default. With an Idempotency-Key that is safe; without one a retry can start the turn twice, so set max_retries=0 (Python) or maxRetries: 0 (Node.js) on calls that have side effects and carry no key.

