CX+AI

Ask a model, then act on the answer

The reply is a string — test it, do not assume it

Ask a model, then act on the answer

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.


The whole call

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.


And the whole of the error handling

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.


Pick a provider, or let it pick

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.


The cache is on the prompt text

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.


Acting on the answer

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.


What makes this different from calling an API

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.


Where to go next

A note on spelling. These builtins are written with underscores — ai_call, ai_get_error, ai_set_provider. CX folds case, so AI_Call also works, but it does not insert underscores: aiCall is a different name and the compiler will tell you it does not know it.