The reply is a string — test it, do not assume it
A model's reply is a string. Everything hard about using one in a program comes from what you do with that string when it is not the string you hoped for.
One page. Everything here was run.
reply.s = ai_call("Name one advantage of a register VM. One sentence.");
printf("[%s]\n", reply);
That is it. No client object, no session, no async ceremony. ai_call picks a provider if you have not named one, sends the prompt, and gives you back what came back.
A failure is an empty string. Not an exception, not a null, not a partial answer — an empty string, every time, for every reason.
reply.s = ai_call("Name one advantage of a register VM. One sentence.");
if (reply == "") {
printf("no answer: %s\n", ai_get_error());
} else {
printf("%s\n", reply);
}
On a machine with nothing configured, that prints:
no answer: no provider set
Test the result. Do not assume it. This is the one habit worth building, because the failure is quiet by design: a program that forgets the test carries on with an empty string and produces something that looks like a bug in your own logic three functions later.
ai_get_error() is the reason, in a sentence, for the last call. It is worth printing rather than swallowing — "no provider set", "no key", a transport message — because those are three different problems with three different fixes.
ai_set_provider("anthropic");
Named explicitly, that is the provider used. Left alone, ai_call auto-picks from what is configured — which is what makes the two-line version above work on a machine that has one model and no opinions.
Naming one sharpens the error, which is the point of naming it:
ai_set_provider("anthropic");
reply.s = ai_call("hi");
if (reply == "") { printf("no answer: %s\n", ai_get_error()); }
no answer: no api key for anthropic
“No provider set” and “no api key for anthropic” are two different problems with two different fixes, and the second one names the provider you chose.
A successful reply is cached under the prompt that produced it, so the second identical question costs nothing. This matters more than it sounds: a loop that asks the same question per row is the easiest way to spend real money by accident.
You can see it working:
printf("cached %d, hits %d\n", ai_cache_size(), ai_cache_hits());
cached 0, hits 0
ai_cache_get, ai_cache_put and ai_cache_clear are there when you want to drive it yourself. There is no separate "is it cached" door: ai_cache_get answers the empty string on a miss, which is the test ai_cache_has performed before it retired.
The reply is text, so the useful patterns are text patterns. Ask for a shape you can check, then check it:
ai_set_provider("anthropic");
answer.s = ai_call("Reply with exactly one word: yes or no. Is 17 prime?");
if (answer == "") {
printf("no answer: %s\n", ai_get_error());
} else {
if (answer == "yes") { printf("prime\n"); }
else {
if (answer == "no") { printf("not prime\n"); }
else { printf("unexpected reply: [%s]\n", answer); }
}
}
The third branch is the one people leave out, and it is the one that fires. A model asked for one word will sometimes send a sentence; the program that has somewhere to put that is the program that survives.
If you asked for JSON, parse it and check it parsed. A reply is a string like any other, so it goes through parseDoc and answers ->valid:
string replied = "{ \"ok\": 1, \"why\": \"looks fine\" }";
json out = parseDoc(replied);
if (out->valid) {
printf("ok=%d why=%s\n", out["ok"], out["why"]);
} else {
printf("the reply was not json\n");
}
ok=1 why=looks fine
From there it is an ordinary document — see Read some JSON and use it.
Two things, and both are the reason CX+AI exists.
The answer can become behaviour, not just data. A reply is text, and CX+AI compiles text to bytecode in-process — no system(), no compiler on the box, no library load. That is what the rules system is for; see Rules that watch your data.
The model can be local. Nothing above assumes a remote provider. The same two lines run against a model on your own machine, which changes the arithmetic of asking a question inside a loop.
aifunc, markers and codeswap.A note on spelling. These builtins are written with underscores —ai_call,ai_get_error,ai_set_provider. CX folds case, soAI_Callalso works, but it does not insert underscores:aiCallis a different name and the compiler will tell you it does not know it.