14. Ask a model
Any app can ask a language model a question without holding a key.
The ai app holds the key; an app calls it with rc_app_headers("ai", true) and gets an answer marked who asked, for whom.
This is the code serving the zoo right now: read from disk for this request, from commit
77ff74344d (staging). The zoo's own self-check fetches this
page and compares it byte for byte with the file it runs.
exhibits/14-ask-a-model.php sha256 853ce0232029 · raw
1<?php 2// Exhibit 14. The zoo has no model key. It asks the ai app, over HTTPS, for the signed-in person: 3// curl POST rc_app_url("ai") . "/?api=complete" with rc_app_headers("ai", true) 4// body: {"model": "fast", "max_tokens": 400, "messages": [{"role": "user", "content": "..."}]} 5// The answer is the Messages shape plus rc: {via, app, user}, which is what the card prints. 6 7function ask_ai(string $question, int $max = 400): array { 8 $body = json_encode(["model" => "fast", "max_tokens" => $max, "messages" => [["role" => "user", "content" => $question]]]); 9 [$s, $out] = call_app("ai", "/?api=complete", true, "POST", $body, ["Content-Type: application/json"], 60); 10 $r = json_decode($out, true); 11 if ($s !== 200 || !is_array($r)) return ["error" => ($r["error"]["message"] ?? "ai answered $s"), "status" => $s === 429 ? 429 : 502]; 12 $text = trim(implode("", array_map(fn($b) => $b["type"] === "text" ? $b["text"] : "", $r["content"] ?? []))); 13 return ["answer" => $text, "via" => $r["rc"]["via"] ?? "", "for" => $r["rc"]["app"] ?? "", "model" => $r["model"] ?? "", 14 "tokens" => ($r["usage"]["input_tokens"] ?? 0) + ($r["usage"]["output_tokens"] ?? 0)]; 15} 16 17return [ 18 "n" => 14, "try" => "ask the model a question", "wing" => "Intelligence", "kind" => "self", 19 "title" => "Ask a model", 20 "promise" => "Any app can ask a language model a question without holding a key.", 21 "block" => 'The ai app holds the key; an app calls it with rc_app_headers("ai", true) and gets an answer marked who asked, for whom.', 22 "show" => function (?array $me): string { 23 if (!$me) return '<p class="muted"><a href="' . h(rc_signin_url()) . '">Sign in</a> to ask the model something.</p>'; 24 return '<p class="muted small">One question at a time, up to 500 characters; each answer goes on your ledger (exhibit 15).</p><form class="row" id="ask-form"><input name="q" maxlength="500" placeholder="Try: name three Swedish lakes" required><button class="primary">Ask</button></form>' 25 . '<div id="ask-out" class="note"></div>'; 26 }, 27 "api" => function (string $do, ?array $me, array $in, bool $post): ?array { 28 if ($do !== "ask" || !$post) return null; 29 if (!$me) return ["error" => "sign in first", "status" => 401]; 30 $q = trim(mb_substr((string)($in["question"] ?? ""), 0, 500)); 31 if ($q === "") return ["error" => "ask something", "status" => 400]; 32 return ask_ai($q, min(400, max(1, (int)($in["max_tokens"] ?? 400)))); 33 }, 34 // The robot asks a fixed cheap question; and nothing key-shaped may sit in what we serve. 35 "check" => function (): array { 36 require_once ZOO_ROOT . "/lib/robot.php"; 37 [$code, , $j] = robot_at_zoo()->post("https://" . env("RC_HOST") . "/api/14/ask", ["question" => "Reply with the single word: pong", "max_tokens" => 20]); 38 if ($code !== 200 || !is_array($j)) return [false, "the question failed ($code): " . (is_array($j) ? ($j["error"] ?? "") : substr((string)$j, 0, 120))]; 39 if ($j["answer"] === "") return [false, "ai answered, but with nothing"]; 40 if ($j["via"] !== "ai" || $j["for"] !== "zoo") return [false, "the answer is not marked 'via ai, for zoo' (via '{$j['via']}', for '{$j['for']}')"]; 41 $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(ZOO_ROOT, FilesystemIterator::SKIP_DOTS)); 42 foreach ($it as $f) { 43 if (!preg_match('/\.(php|js|css|conf|md)$/', $f->getFilename()) || str_contains($f->getPathname(), "/.git/")) continue; 44 if (preg_match('/\bsk-[A-Za-z0-9_-]{20,}|\b[0-9a-f]{32}\.[A-Za-z0-9]{12,}|-----BEGIN [A-Z ]*PRIVATE KEY/', (string)file_get_contents($f->getPathname()))) return [false, "something key-shaped is in " . substr($f->getPathname(), strlen(ZOO_ROOT) + 1)]; 45 } 46 return [true, "asked the fast model through ai as the robot: \"" . mb_substr($j["answer"], 0, 40) . "\" ({$j['tokens']} tokens); no key in the zoo's files", ["tokens" => $j["tokens"]]]; 47 }, 48 "script" => <<<'JS' 49document.getElementById("ask-form")?.addEventListener("submit", async (ev) => { 50 ev.preventDefault(); 51 const out = document.getElementById("ask-out"), btn = ev.target.querySelector("button"); 52 btn.disabled = true; out.textContent = "thinking…"; 53 const j = await zoo.call("/api/14/ask", { question: ev.target.q.value }); 54 btn.disabled = false; 55 if (j.error) { out.textContent = j.error; return; } 56 out.innerHTML = '<div class="answer">' + zoo.md(j.answer) + '</div><p class="muted">answered via ' + zoo.esc(j.via) + ', for ' + zoo.esc(j.for) + ', on your behalf (' + zoo.esc(j.model) + ', ' + j.tokens + ' tokens)</p>'; 57 window.dispatchEvent(new Event("zoo-asked")); 58}); 59 60JS, 61];