AI models
Chat, embeddings, images, videos and transcription paid from your NoClick balance, an OpenAI-compatible endpoint, and your agents as models.
noclick.AI calls models directly, for the parts of your product that aren't an agent: a summary, a classification, a search index, a generated image or video, a transcript. Every call is paid from your NoClick balance in US dollars, so you don't need a key with each model provider, and it is attributed like everything else: to the key, and inside as_user to the user.
Chat
import noclick
reply = noclick.AI.chat(
[
{"role": "system", "content": "You write one-line summaries."},
{"role": "user", "content": "Summarize: the meeting moved to Friday at 3pm."},
],
model="openrouter/anthropic/claude-sonnet-4.6",
max_tokens=200,
)
print(reply.text) # The meeting is now Friday at 3pm.
print(reply.usage, reply.cost_usd)messages is a list of {"role", "content"} messages, or a plain string as one user message. model is an OpenRouter id with an openrouter/ prefix, as for agents; leave it out for NoClick's default. reply.message is the whole assistant message, ready to append to the conversation for the next turn.
Ask for JSON with response_format. NoClick's default model holds a schema strictly, as OpenAI's models do: every property is in required, and additionalProperties is false.
reply = noclick.AI.chat(
"Extract the date and time from: the meeting moved to Friday at 3pm.",
response_format={
"type": "json_schema",
"json_schema": {"name": "when", "strict": True, "schema": {
"type": "object",
"properties": {"day": {"type": "string"}, "time": {"type": "string"}},
"required": ["day", "time"],
"additionalProperties": False,
}},
},
)
print(reply.text) # {"day": "Friday", "time": "15:00"}Give it your users' tools
tools takes OpenAI-format definitions, so a toolkit plugs straight in: the model acts in the user's connections, under your policy.
toolkit = noclick.Toolkit(["gmail", "search"])
messages = [{"role": "user", "content": "Summarize the emails Ana sent me this week"}]
with noclick.as_user("user_42"):
while True:
reply = noclick.AI.chat(messages, tools=toolkit.definitions("openai"))
messages.append(reply.message)
if not reply.tool_calls:
break
messages += toolkit.handle_openai(reply.message)
print(reply.text)If you'd rather NoClick ran that loop, keep its state and answer on channels, use an agent instead.
Embeddings
docs = ["Refunds take 5 days.", "We ship worldwide.", "Support is open 9 to 5."]
result = noclick.AI.embed(docs, model="openrouter/openai/text-embedding-3-small")
print(result.dimensions, len(result.vectors)) # 1536 3One vector per text, in the order you passed them; up to 2,048 texts a call. dimensions shortens the vectors for models that allow it. To search them, keep them in a Postgres database beside the text they came from.
Images
image = noclick.AI.image("A lighthouse at dusk, watercolor", size="1024x1536")
print(image.url, image.width, image.height, image.cost_usd)One image per call, kept as a file in your account (your Files in the app): its url keeps working, and file_id names it. size is "WIDTHxHEIGHT" in an aspect ratio the model makes (1024x1536 is 2:3); a size the model can't make fails with noclick.InvalidRequest naming the ratios it can.
Videos
A video takes minutes, so noclick.AI.video returns an operation at once, and its wait() gives the video:
job = noclick.AI.video("Waves rolling under a lighthouse at dusk", seconds=8, aspect_ratio="9:16")
clip = job.wait(timeout=600)
print(clip.url, clip.seconds, clip.resolution, clip.cost_usd)Like an image, the video is kept as a file in your account (url, file_id). seconds, resolution (720p, 1080p) and aspect_ratio (16:9, 9:16) are among the ones the model makes; leave them out for about 8 seconds at 720p. audio=False leaves the sound out, where the model makes it. Only models priced per second can be used, since the price is quoted before the video starts: a call your balance can't cover is refused then, with nothing charged, and the video is charged at that cost when it's done. A length or shape the model can't make fails with noclick.InvalidRequest naming the ones it can. Keep job.id to pick the video up later, from anywhere (noclick.Operation.from_id(job.id).wait()).
Transcription
Pass a URL to the recording, or its bytes:
words = noclick.AI.transcribe("https://example.com/calls/2026-09-26.mp3")
print(words.language, words.duration_s)
print(words.text)
with open("voicemail.m4a", "rb") as f:
words = noclick.AI.transcribe(f.read(), language="en")
for segment in words.segments:
print(f"{segment.start_s:6.1f}s {segment.text}")MP3, WAV, M4A, MP4, OGG, WebM, AAC, FLAC and AIFF work, up to 2 hours of audio: 25 MB as bytes, 200 MB behind an https URL. language (an ISO 639-1 code like en) skips detection.
The default model transcribes a recording longer than 10 minutes in pieces of about five minutes, cut at a pause where there is one, and the segments come back as one transcript with times from the start of the recording. Each piece is charged as it's transcribed. If a piece fails, the call fails and the error says which piece and where it falls in the recording; the pieces transcribed before then are still charged. The pieces are transcribed side by side, but like any call that doesn't stream, it waits at most 90 seconds (see Long turns).
Who said what
speakers= tells the voices apart, and each segment's speaker says whose it is. It runs on noclick/transcribe-1, which takes the whole recording in one job, so each voice keeps its label from start to end; leave model out and that's the model used, and a model that cuts recordings into pieces refuses speakers with noclick.InvalidRequest.
True labels the voices speaker_1, speaker_2 and on, in the order they first speak. A list of names says how many voices there are, and NoClick works out which voice is which from what they say: the rep introduces the company, the customer asks about the price. A voice it can't tell takes the next unused name in the order the voices first speak, so a call the customer opens with "Hello?" still comes out right when the rep then says who they are:
call = noclick.AI.transcribe("https://example.com/calls/demo.mp3", speakers=["rep", "customer"])
for segment in call.segments:
print(segment.speaker, segment.text)A segment is one voice's turn, from when it starts speaking to when another does. Telling voices apart is priced on its own: usd_per_minute_with_speakers in AI.models (None for a model that can't). noclick/transcribe-1 is charged for the GPU time its job takes, so that is what a long recording comes to a minute; a short one costs more a minute, since starting the GPU is part of its job, and the transcript's cost_usd is what it came to.
Long recordings
A whole recording can take longer than the 90 seconds a request waits: a few minutes for two hours of audio, more when the service is busy. background=True returns an operation at once, whose wait() gives the transcript:
job = noclick.AI.transcribe("https://example.com/meetings/all-hands.mp3", speakers=True, background=True)
meeting = job.wait()
print(len(meeting.segments), meeting.cost_usd)It runs on noclick/transcribe-1, as speakers does. A call without background that passes 90 seconds raises noclick.RequestTimeout naming its operation (operation_id): the transcription goes on, is charged when it's done, and its transcript is the operation's result. An account runs up to 10 such transcriptions at once; one more raises noclick.QuotaExceeded.
Pick a model
for m in noclick.AI.models(kind="chat"):
print(m.id, m.context_length, m.input_usd_per_token, m.output_usd_per_token)kind is chat, embedding, image, transcription, video or agent. Each model carries what it costs you in US dollars, its provider's own price: per token for chat and embedding models, per token of the image an image model makes, usd_per_minute of audio for transcription models (usd_per_minute_with_speakers for one that tells voices apart), and usd_per_second for video models, for the clip a call naming no length or resolution makes. The default of each kind comes first, and agent lists your agents.
Use an OpenAI client
noclick.AI.base_url is an OpenAI-compatible endpoint. Point any OpenAI client, or a framework built on one, at it with your NoClick key:
import os
from openai import OpenAI
gpt = OpenAI(base_url=noclick.AI.base_url, api_key=os.environ["NOCLICK_API_KEY"])
completion = gpt.chat.completions.create(
model="openrouter/anthropic/claude-sonnet-4.6",
messages=[{"role": "user", "content": "Say hello"}],
)
print(completion.choices[0].message.content)It serves chat completions (streaming included, with tools, tool_choice, response_format, max_tokens and temperature passed to the model), the Responses API (gpt.responses.create, with instructions, function tools, streaming and previous_response_id), embeddings and the model list. Every model answers both APIs the same way. A response is kept for 30 days so previous_response_id can continue it, unless you send store=False. Each answer also carries cost_usd.
To act for a user, send their id in the NoClick-User header, or use a user's session token as the key. Without it the account pays and no user's spend cap applies, so build the client's headers with noclick.AI.headers(), which gives your key and, inside as_user, the user's header:
from openai import OpenAI
with noclick.as_user("user_42"):
headers = noclick.AI.headers() # Authorization, and NoClick-User: user_42
gpt = OpenAI(base_url=noclick.AI.base_url, api_key="-", default_headers=headers)api_key is only a placeholder there: the headers carry the key. Errors come in OpenAI's shape, so the client raises its own matching error; NoClick's type (invalid_request, payment_required, ...) is the error's code. Images, videos and transcription are noclick.AI.image, noclick.AI.video and noclick.AI.transcribe only.
Talk to your agents from any OpenAI client
Every agent you can message is also a model: agent:support, or agent: and its id. It talks to that agent in the scope you call from (your account, or the user in NoClick-User or the session), on whatever harness it runs.
import os
from openai import OpenAI
gpt = OpenAI(base_url=noclick.AI.base_url, api_key=os.environ["NOCLICK_API_KEY"])
answer = gpt.chat.completions.create(
model="agent:support",
messages=[{"role": "user", "content": "Where's my refund?"}],
)
print(answer.choices[0].message.content)
conversation = answer.metadata["conversation"]
follow_up = gpt.chat.completions.create(
model="agent:support",
messages=[{"role": "user", "content": "And when will it arrive?"}],
metadata={"conversation": conversation},
)The agent keeps the conversation, so the rule is simple:
- Without
metadata.conversation, the wholemessageslist is the first turn of a new conversation, and the answer'smetadata.conversationnames it. - With it, only the messages after the last assistant message are sent: the agent already has everything before.
The Responses API continues one with previous_response_id, or conversation set to that name. The reply is the assistant message. The agent runs its own tools, so it never answers with tool_calls for you to run, and passing tools to an agent fails with noclick.InvalidRequest; what it called is listed in the answer's noclick.tool_calls. A turn costs what the agent's turns always cost, and counts toward its budget.
The same works from noclick.AI.chat, which passes the conversation for you:
reply = noclick.AI.chat("Where's my refund?", model="agent:support")
follow_up = noclick.AI.chat("And when will it arrive?", model="agent:support", conversation=reply.conversation)
print(follow_up.text)Because agents are listed as models (owned_by: "noclick-agent"), a chat UI that speaks OpenAI shows them in its model menu. In Open WebUI, add an OpenAI connection with https://api.noclick.com/v1/ai as its URL and a key holding the agents scope (and ai, to list the models too), and each agent appears in the model menu. Such a UI sends the whole chat on every turn, so each turn starts a new conversation carrying the chat so far.
A chat UI like that will keep one conversation without knowing it. A request with no metadata.conversation whose messages end in an assistant message the agent sent in the last 24 hours, in the same scope, continues that message's conversation: only the messages after it are sent, as if the UI had named the conversation. A chat whose last assistant message the agent didn't write, or wrote longer ago, starts a new conversation as before, and the answer's metadata.conversation always says which one it was.
Long turns
A stream never times out: it starts at once and, while the model thinks or the agent works, carries a comment line every 15 seconds so nothing between you and NoClick closes it. An agent's reply arrives in one piece when its turn ends, whatever its harness (a coding harness such as Claude Code answers only once its turn is done).
A request that doesn't stream waits at most 90 seconds, because a request through NoClick's edge must start answering within about 100. Past that it fails with noclick.RequestTimeout (HTTP 504, marked so OpenAI clients don't retry it) telling you to stream. A model's call is stopped and not charged; an agent's turn goes on, and its reply lands in the conversation the error names, readable at the operation in operation_id. A whole-recording transcription goes on the same way (invite-only beta; see Long recordings). noclick.AI.chat always streams, so it never hits this limit.
Billing and limits
Each call is charged in US dollars at the model's price (its provider's own) and shows as AI on the Usage page. A call inside as_user counts toward that user's spend cap. A model call takes a key with the ai scope; an agent model takes the agents scope, as messaging an agent always does. In a deployed app, noclick.Project.current().ai is this API acting as the app, billed to it and held to its budget.
noclick.PaymentRequired: your account can't pay for the call; itsoffersare links a person opens to pay (see Errors).noclick.PlanRequired: your plan caps it; itsoffersare the plans that don't.noclick.SpendCapExceeded: the key's or the user's monthly cap is used up.noclick.InvalidRequest: an unknown model, a model of the wrong kind (an image model tochat), a request too long for the model'scontext_length, ortoolsfor an agent.noclick.RateLimited: too many calls at once, or the model's provider is busy; the client retries with backoff before raising it.noclick.UpstreamError: the model's provider failed; retrying in a moment usually works.noclick.RequestTimeout: a call that doesn't stream ran past 90 seconds; see Long turns.
The Python reference has every argument of chat, embed, image, video, transcribe and models.