lib.rsannotatedlib.rssource1957 lines · 82.8 KB · raw
1//! The pgrx extension: SQL entry points gluing `jev-protocol` to
2//! `postjevsql-pg` through their safe APIs (contract §0).
3#![forbid(unsafe_code)]
4
5use std::cell::{Cell, RefCell};
6use std::collections::HashMap;
7use std::ffi::{CStr, CString};
8use std::rc::Rc;
9use std::task::{Poll, Waker};
10use std::time::Duration;
11
12use pgrx::datum::Internal;
13use pgrx::prelude::*;
14use pgrx::{AnyElement, GucContext, GucFlags, GucRegistry, GucSetting, PostgresGucEnum};
15use jev_client::{Client, ClientError};
16use jev_protocol::{
17    Choice, ChoiceAnswer, Json, Key, ModelId, Noul, NoulAnswer, Question, Questions, Response, Score, ScoreAnswer,
18    question_bytes,
19};
20use postjevsql_core::label_tree::{self, Branch, Describe, Outcome, Params, Search, Step, Tau, Tree};
21use postjevsql_core::shortlist::{self, Shortlist};
22use postjevsql_core::{CacheKey, KeyScope, Layout, PROMPT_VERSION, Placement, Planned, Planner, Sample, TokenRatio};
23use postjevsql_pg::scan::{
24    self, Arg, Call, Estimate, Field, Function, Judge, Judgement, Out, Property, Refusal, Returns, Shown, Statement, Value,
25};
26use postjevsql_pg::owner::as_owner_of;
27use postjevsql_pg::stats::{self, Telemetry};
28use postjevsql_pg::{Backend, Endpoint, HttpsTransport, KeepAlive, RateLimit, StringCheck, define_checked_string, join_all, stream_limit};
29
30mod failure;
31use failure::Failure;
32
33pgrx::pg_module_magic!(name, version);
34
35static ENDPOINT: GucSetting<Option<CString>> =
36    GucSetting::<Option<CString>>::new(Some(c"https://api.typesafe.ai/v1/systemone"));
37static MODEL: GucSetting<Option<CString>> = GucSetting::<Option<CString>>::new(None);
38static CA_FILE: GucSetting<Option<CString>> = GucSetting::<Option<CString>>::new(None);
39static DNS_SERVERS: GucSetting<Option<CString>> = GucSetting::<Option<CString>>::new(None);
40static KEEPALIVE_INTERVAL: GucSetting<i32> = GucSetting::<i32>::new(20_000);
41static KEEPALIVE_TIMEOUT: GucSetting<i32> = GucSetting::<i32>::new(10_000);
42static API_KEY_FILE: GucSetting<Option<CString>> = GucSetting::<Option<CString>>::new(None);
43static CONCURRENCY: GucSetting<i32> = GucSetting::<i32>::new(100);
44static PRICE_PER_MTOK: GucSetting<f64> = GucSetting::<f64>::new(0.042);
45static MAX_TOKENS_PER_SECOND: GucSetting<i32> = GucSetting::<i32>::new(250_000);
46static MAX_REQUESTS_PER_MINUTE: GucSetting<i32> = GucSetting::<i32>::new(1_200);
47static MAX_ROWS: GucSetting<i32> = GucSetting::<i32>::new(-1);
48static MAX_COST: GucSetting<f64> = GucSetting::<f64>::new(-1.0);
49static CACHE_NAMESPACE: GucSetting<Option<CString>> = GucSetting::<Option<CString>>::new(None);
50static CACHE_ONLY: GucSetting<bool> = GucSetting::<bool>::new(false);
51static ON_ERROR: GucSetting<OnError> = GucSetting::<OnError>::new(OnError::Error);
52static THRESHOLD: GucSetting<f64> = GucSetting::<f64>::new(0.5);
53
54/// What a failed judgment does (contract *SQL surface*).
55#[derive(Clone, Copy, Debug, PartialEq, Eq, PostgresGucEnum)]
56enum OnError {
57    /// The statement fails.
58    #[name = c"error"]
59    Error,
60    /// The call is NULL: `jev_prob` NULL, and `jev()` false through
61    /// its qual, so a fault skips a row and never passes one.
62    #[name = c"unsure"]
63    Unsure,
64}
65
66#[pg_guard]
67pub extern "C-unwind" fn _PG_init() {
68    // Superuser-only: whoever sets the endpoint receives the API key.
69    GucRegistry::define_string_guc(
70        c"jev.endpoint",
71        c"TypeSafe evaluation endpoint.",
72        c"Must be https. Requests carry the server's API key.",
73        &ENDPOINT,
74        GucContext::Suset,
75        GucFlags::default(),
76    );
77    // Refused at SET, not first at query time: an alias moves under you.
78    define_checked_string::<PinnedModel>(
79        c"jev.model",
80        c"The pinned Jev model version, e.g. jev-1.13.0.",
81        c"Aliases such as jev-latest are refused. Answers from any other model are rejected.",
82        &MODEL,
83        GucContext::Userset,
84        GucFlags::default(),
85    );
86    GucRegistry::define_string_guc(
87        c"jev.ca_file",
88        c"PEM file of extra CA certificates to trust.",
89        c"Added to the Mozilla roots; for tests and private endpoints.",
90        &CA_FILE,
91        GucContext::Suset,
92        GucFlags::default(),
93    );
94    // Superuser-only: it chooses whose account pays.
95    GucRegistry::define_string_guc(
96        c"jev.api_key_file",
97        c"File holding the TypeSafe API key.",
98        c"Exclusive with TYPESAFE_API_KEY in the server environment.",
99        &API_KEY_FILE,
100        GucContext::Suset,
101        GucFlags::default(),
102    );
103    // Superuser-only, like the endpoint: it decides where names point.
104    GucRegistry::define_string_guc(
105        c"jev.dns_servers",
106        c"DNS servers for jev.endpoint's host, as ip[:port], comma-separated.",
107        c"Empty uses /etc/resolv.conf.",
108        &DNS_SERVERS,
109        GucContext::Suset,
110        GucFlags::default(),
111    );
112    // Superuser-only, like the endpoint: they shape the one connection
113    // every role's requests share. Tests shorten them.
114    GucRegistry::define_int_guc(
115        c"jev.keepalive_interval",
116        c"Quiet time on the connection before a keep-alive ping.",
117        c"Pings are sent only while a jev scan runs.",
118        &KEEPALIVE_INTERVAL,
119        1,
120        i32::MAX,
121        GucContext::Suset,
122        GucFlags::UNIT_MS,
123    );
124    GucRegistry::define_int_guc(
125        c"jev.keepalive_timeout",
126        c"How long a keep-alive ping may go unanswered before the connection is closed.",
127        c"The next request opens a new connection.",
128        &KEEPALIVE_TIMEOUT,
129        1,
130        i32::MAX,
131        GucContext::Suset,
132        GucFlags::UNIT_MS,
133    );
134    GucRegistry::define_int_guc(
135        c"jev.concurrency",
136        c"Rows a jev scan judges at once.",
137        c"Each is one HTTP/2 stream on the backend's connection. A LIMIT stops judging after this many rows past the last one returned.",
138        &CONCURRENCY,
139        1,
140        1000,
141        GucContext::Userset,
142        GucFlags::default(),
143    );
144    // Superuser-only: spend is reckoned from it.
145    GucRegistry::define_float_guc(
146        c"jev.price_per_mtok",
147        c"Dollars per million input tokens for jev.model.",
148        c"Output tokens are not billed. The default is jev-1.13.0's launch price.",
149        &PRICE_PER_MTOK,
150        0.0,
151        f64::MAX,
152        GucContext::Suset,
153        GucFlags::default(),
154    );
155    // Sighup: one account, one limit, so every backend must agree on it.
156    // The defaults are jev-1.13.0's published account limits.
157    GucRegistry::define_int_guc(
158        c"jev.max_tokens_per_second",
159        c"Ceiling on input tokens per second across the whole cluster; 0 is no limit.",
160        c"Enforced before sending, in shared memory. The rate in force halves on each 429 and climbs back as answers arrive.",
161        &MAX_TOKENS_PER_SECOND,
162        0,
163        i32::MAX,
164        GucContext::Sighup,
165        GucFlags::default(),
166    );
167    GucRegistry::define_int_guc(
168        c"jev.max_requests_per_minute",
169        c"Ceiling on requests per minute across the whole cluster; 0 is no limit.",
170        c"Enforced before sending, in shared memory. The rate in force halves on each 429 and climbs back as answers arrive.",
171        &MAX_REQUESTS_PER_MINUTE,
172        0,
173        i32::MAX,
174        GucContext::Sighup,
175        GucFlags::default(),
176    );
177    // Like temp_file_limit, -1 is no limit, so 0 can mean "send nothing".
178    GucRegistry::define_int_guc(
179        c"jev.max_rows",
180        c"Rows a statement may send to Jev; -1 for no limit.",
181        c"Checked against the planner's estimate before the first request, and against the rows sent while it runs.",
182        &MAX_ROWS,
183        -1,
184        i32::MAX,
185        GucContext::Userset,
186        GucFlags::default(),
187    );
188    GucRegistry::define_float_guc(
189        c"jev.max_cost",
190        c"Dollars a statement may spend on Jev; -1 for no limit.",
191        c"Each request is budgeted at three billed attempts, at jev.price_per_mtok. Checked against the estimate before the first request, and while it runs against the input tokens each answer reports plus the worst case of the requests in flight.",
192        &MAX_COST,
193        -1.0,
194        f64::MAX,
195        GucContext::Userset,
196        GucFlags::default(),
197    );
198    GucRegistry::define_string_guc(
199        c"jev.cache_namespace",
200        c"Namespace of the answer cache.",
201        c"Part of every cache key: changing it re-judges every input.",
202        &CACHE_NAMESPACE,
203        GucContext::Userset,
204        GucFlags::default(),
205    );
206    // Keeps the cache usable once a pinned model is withdrawn: nothing
207    // is sent, so no API key is needed and nothing is spent.
208    GucRegistry::define_bool_guc(
209        c"jev.cache_only",
210        c"Serve jev answers from the cache only.",
211        c"A judgment with no cached answer fails instead of being sent.",
212        &CACHE_ONLY,
213        GucContext::Userset,
214        GucFlags::default(),
215    );
216    // Userset: it decides only what this session's own failures do.
217    GucRegistry::define_enum_guc(
218        c"jev.on_error",
219        c"What a failed jev judgment does: error or unsure.",
220        c"unsure answers NULL for a call whose request failed at the remote or on the way to it, so the row is skipped by a jev qual. Settings, key, budget and cache_only failures still raise.",
221        &ON_ERROR,
222        GucContext::Userset,
223        GucFlags::default(),
224    );
225    // Noul only: thresholds do not transfer between question types
226    // (contract *SQL surface*). Its default is the last step of jev()'s
227    // precedence, so RESET falls back to 0.5.
228    GucRegistry::define_float_guc(
229        c"jev.threshold",
230        c"Probability at or above which jev() is true, when the call gives none.",
231        c"A threshold passed to jev() takes precedence. Record a tuned value against the pinned jev.model.",
232        &THRESHOLD,
233        0.0,
234        1.0,
235        GucContext::Userset,
236        GucFlags::default(),
237    );
238    scan::register::<JevScan>();
239}
240
241// The durable cache (contract *Cache*). Each row is an audit receipt for
242// one judgment: `key` is `postjevsql_core::CacheKey`, the hash of the
243// model pin, prompt version, namespace, layout, `state` and `question`,
244// which are the exact bytes sent.
245pgrx::extension_sql!(
246    "CREATE TABLE jev_cache (
247         key bytea PRIMARY KEY,
248         model text NOT NULL,
249         answered_model text NOT NULL,
250         namespace text NOT NULL,
251         layout text NOT NULL,
252         state text NOT NULL,
253         question bytea NOT NULL,
254         request_id text,
255         input_tokens bigint NOT NULL,
256         output_tokens bigint NOT NULL,
257         answer jsonb NOT NULL,
258         at timestamptz NOT NULL DEFAULT now()
259     );
260     REVOKE ALL ON TABLE jev_cache FROM PUBLIC;",
261    name = "jev_cache",
262);
263
264// The typed results of `jev_eval` and the `*_full` functions (contract
265// *SQL surface*): each judgment's answer with the model that answered it
266// and the tokens of the request it was asked in, from its `jev_cache`
267// row. A Noul answer carries no confidence. `probabilities` keeps the
268// order asked: an array, of [label, p] pairs for a Choice.
269pgrx::extension_sql!(
270    "CREATE TYPE jev_noul_result AS (
271         probability float8, model text, input_tokens int, output_tokens int
272     );
273     CREATE TYPE jev_score_result AS (
274         score float8, confidence float8, probabilities jsonb, legend jsonb,
275         model text, input_tokens int, output_tokens int
276     );
277     CREATE TYPE jev_choice_result AS (
278         choice text, confidence float8, probabilities jsonb,
279         model text, input_tokens int, output_tokens int
280     );
281     CREATE TYPE jev_tree_result AS (
282         path text, probability float8, separation float8, depth int, leaf bool, fit float8
283     );
284     CREATE TYPE jev_shortlist_result AS (
285         choice text, confidence float8, probabilities jsonb,
286         model text, input_tokens int, output_tokens int, fit float8
287     );",
288    name = "result_types",
289);
290
291// Spending the API key takes an explicit GRANT (contract *Cost and
292// safety*). `finalize` runs after every function exists.
293pgrx::extension_sql!(
294    "REVOKE ALL ON FUNCTION jev_prob(anyelement, text) FROM PUBLIC;
295     REVOKE ALL ON FUNCTION jev(anyelement, text) FROM PUBLIC;
296     REVOKE ALL ON FUNCTION jev(anyelement, text, float8) FROM PUBLIC;
297     REVOKE ALL ON FUNCTION jev_score(anyelement, text, text[]) FROM PUBLIC;
298     REVOKE ALL ON FUNCTION jev_score_norm(anyelement, text, text[]) FROM PUBLIC;
299     REVOKE ALL ON FUNCTION jev_choice(anyelement, text, text[]) FROM PUBLIC;
300     REVOKE ALL ON FUNCTION jev_eval(anyelement, text) FROM PUBLIC;
301     REVOKE ALL ON FUNCTION jev_score_full(anyelement, text, text[]) FROM PUBLIC;
302     REVOKE ALL ON FUNCTION jev_choice_full(anyelement, text, text[]) FROM PUBLIC;
303     REVOKE ALL ON FUNCTION jev_confidence(anyelement, text, text, text[]) FROM PUBLIC;
304     REVOKE ALL ON FUNCTION jev_choice_tree(anyelement, text, text[]) FROM PUBLIC;
305     REVOKE ALL ON FUNCTION jev_choice_tree(anyelement, text, text[], float8) FROM PUBLIC;
306     REVOKE ALL ON FUNCTION jev_choice_tree_full(anyelement, text, text[]) FROM PUBLIC;
307     REVOKE ALL ON FUNCTION jev_choice_tree_full(anyelement, text, text[], float8) FROM PUBLIC;
308     REVOKE ALL ON FUNCTION jev_choice_shortlist(anyelement, text, text[]) FROM PUBLIC;
309     REVOKE ALL ON FUNCTION jev_choice_shortlist(anyelement, text, text[], text[]) FROM PUBLIC;
310     REVOKE ALL ON FUNCTION jev_choice_shortlist_full(anyelement, text, text[]) FROM PUBLIC;
311     REVOKE ALL ON FUNCTION jev_choice_shortlist_full(anyelement, text, text[], text[]) FROM PUBLIC;
312     REVOKE ALL ON FUNCTION jev_support(internal) FROM PUBLIC;
313     REVOKE ALL ON FUNCTION jev_stats() FROM PUBLIC;",
314    name = "revoke_from_public",
315    finalize,
316);
317
318struct PinnedModel;
319
320impl StringCheck for PinnedModel {
321    fn check(value: &str) -> Result<(), String> {
322        ModelId::pinned(value).map(drop).map_err(|e| e.to_string())
323    }
324}
325
326/// The probability that the answer to yes/no `question` about `row` is
327/// yes. The jev scan evaluates every call it can claim; this body runs
328/// only for a call it could not, and refuses.
329#[pg_extern(volatile, parallel_restricted, support = jev_support)]
330fn jev_prob(_row: AnyElement, _question: &str) -> f64 {
331    Failure::Unbatched("jev_prob").raise()
332}
333
334/// Whether the answer to yes/no `question` about `row` is yes: its
335/// probability is at least `jev.threshold` (0.5 unless set). Evaluated by
336/// the jev scan, like [`jev_prob`], which it shares a judgment with.
337#[pg_extern(name = "jev", volatile, parallel_restricted, support = jev_support)]
338fn jev_at_setting(_row: AnyElement, _question: &str) -> bool {
339    Failure::Unbatched("jev").raise()
340}
341
342/// Whether the probability that the answer to `question` about `row` is
343/// yes is at least `threshold`, which takes precedence over
344/// `jev.threshold`.
345#[pg_extern(name = "jev", volatile, parallel_restricted, support = jev_support)]
346fn jev_at(_row: AnyElement, _question: &str, _threshold: f64) -> bool {
347    Failure::Unbatched("jev").raise()
348}
349
350/// The probability-weighted level, from 0 to `levels` − 1, of `row` on
351/// the rubric `question` whose `levels` are given lowest first. Evaluated
352/// by the jev scan, which sends the levels in the caller's order.
353#[pg_extern(volatile, parallel_restricted, support = jev_support)]
354fn jev_score(_row: AnyElement, _question: &str, _levels: Vec<Option<String>>) -> f64 {
355    Failure::Unbatched("jev_score").raise()
356}
357
358/// [`jev_score`] scaled to 0 to 1, so rubrics of different lengths
359/// compare. The two share one judgment and one cache row.
360#[pg_extern(volatile, parallel_restricted, support = jev_support)]
361fn jev_score_norm(_row: AnyElement, _question: &str, _levels: Vec<Option<String>>) -> f64 {
362    Failure::Unbatched("jev_score_norm").raise()
363}
364
365/// The label of `options` that best answers `question` about `row`.
366/// Evaluated by the jev scan, which sends the options in the caller's
367/// order; a single option is the answer without asking.
368#[pg_extern(volatile, parallel_restricted, support = jev_support)]
369fn jev_choice(_row: AnyElement, _question: &str, _options: Vec<Option<String>>) -> String {
370    Failure::Unbatched("jev_choice").raise()
371}
372
373/// The Noul [`jev_prob`] asks, in full: its probability, the model that
374/// answered and the request's tokens. Shares a judgment with `jev_prob`
375/// and `jev()`.
376#[pg_extern(volatile, parallel_restricted, support = jev_support, requires = ["result_types"])]
377fn jev_eval(_row: AnyElement, _question: &str) -> pgrx::composite_type!('static, "jev_noul_result") {
378    Failure::Unbatched("jev_eval").raise()
379}
380
381/// [`jev_score`] in full: the level, the vendor's confidence, each
382/// level's probability and description, the model and the tokens.
383#[pg_extern(volatile, parallel_restricted, support = jev_support, requires = ["result_types"])]
384fn jev_score_full(
385    _row: AnyElement,
386    _question: &str,
387    _levels: Vec<Option<String>>,
388) -> pgrx::composite_type!('static, "jev_score_result") {
389    Failure::Unbatched("jev_score_full").raise()
390}
391
392/// [`jev_choice`] in full: the label, the vendor's confidence, each
393/// option's probability in the order given, the model and the tokens.
394#[pg_extern(volatile, parallel_restricted, support = jev_support, requires = ["result_types"])]
395fn jev_choice_full(
396    _row: AnyElement,
397    _question: &str,
398    _options: Vec<Option<String>>,
399) -> pgrx::composite_type!('static, "jev_choice_result") {
400    Failure::Unbatched("jev_choice_full").raise()
401}
402
403/// The vendor's confidence in the answer to the Choice or Score `question`
404/// about `row`, as it returned it and never recomputed. `kind` is
405/// `'choice'` or `'score'`, and `options` the options or levels, as
406/// pg-jev's signature has them. Shares a judgment with [`jev_choice`] or
407/// [`jev_score`] on the same arguments. A Noul carries no confidence, so
408/// `'noul'` is refused rather than derived from the probability.
409#[pg_extern(volatile, parallel_restricted, support = jev_support)]
410fn jev_confidence(_row: AnyElement, _question: &str, _kind: &str, _options: Vec<Option<String>>) -> f64 {
411    Failure::Unbatched("jev_confidence").raise()
412}
413
414/// The leaf of the label tree `paths` that best answers `question` about
415/// `row`, as its path. Each path is a leaf's labels from the top down,
416/// joined by ` > `; siblings are sent in the order they first appear.
417/// Evaluated by the jev scan as a best-first search, one Choice per node,
418/// so more than 255 labels can be chosen from (contract *SQL surface*).
419#[pg_extern(volatile, parallel_restricted, support = jev_support)]
420fn jev_choice_tree(_row: AnyElement, _question: &str, _paths: Vec<Option<String>>) -> String {
421    Failure::Unbatched("jev_choice_tree").raise()
422}
423
424/// [`jev_choice_tree`] stopping early: the path of the deepest node whose
425/// probability is at least `tau`, which may be a group rather than a
426/// leaf; NULL when no label is that likely.
427#[pg_extern(name = "jev_choice_tree", volatile, parallel_restricted, support = jev_support)]
428fn jev_choice_tree_tau(_row: AnyElement, _question: &str, _paths: Vec<Option<String>>, _tau: f64) -> String {
429    Failure::Unbatched("jev_choice_tree").raise()
430}
431
432/// [`jev_choice_tree`] in full: the chosen path, its probability (the
433/// product of its edges'), its separation from the runner-up leaf, its
434/// depth and whether it is a leaf. Shares every node's judgment with
435/// `jev_choice_tree` on the same arguments.
436#[pg_extern(volatile, parallel_restricted, support = jev_support, requires = ["result_types"])]
437fn jev_choice_tree_full(
438    _row: AnyElement,
439    _question: &str,
440    _paths: Vec<Option<String>>,
441) -> pgrx::composite_type!('static, "jev_tree_result") {
442    Failure::Unbatched("jev_choice_tree_full").raise()
443}
444
445/// [`jev_choice_tree_tau`] in full. The separation is NULL: stopping at τ
446/// proves no runner-up. A stop at the root has a NULL path and depth 0.
447#[pg_extern(
448    name = "jev_choice_tree_full",
449    volatile,
450    parallel_restricted,
451    support = jev_support,
452    requires = ["result_types"]
453)]
454fn jev_choice_tree_full_tau(
455    _row: AnyElement,
456    _question: &str,
457    _paths: Vec<Option<String>>,
458    _tau: f64,
459) -> pgrx::composite_type!('static, "jev_tree_result") {
460    Failure::Unbatched("jev_choice_tree_full").raise()
461}
462
463/// The option of `options` that best answers `question` about `row`, by
464/// chunk and shortlist, for label sets with no meaningful hierarchy: one
465/// request asks a Choice over each run of up to 254 options, labels only,
466/// and a second asks one Choice over the top 3 of each, in the caller's
467/// order (contract *SQL surface*, "More than 255 options").
468#[pg_extern(volatile, parallel_restricted, support = jev_support)]
469fn jev_choice_shortlist(_row: AnyElement, _question: &str, _options: Vec<Option<String>>) -> String {
470    Failure::Unbatched("jev_choice_shortlist").raise()
471}
472
473/// [`jev_choice_shortlist`] with a description per option, sent with the
474/// finalists only; a NULL description sends none.
475#[pg_extern(name = "jev_choice_shortlist", volatile, parallel_restricted, support = jev_support)]
476fn jev_choice_shortlist_described(
477    _row: AnyElement,
478    _question: &str,
479    _options: Vec<Option<String>>,
480    _descriptions: Vec<Option<String>>,
481) -> String {
482    Failure::Unbatched("jev_choice_shortlist").raise()
483}
484
485/// [`jev_choice_shortlist`] in full, as a `jev_shortlist_result`: the
486/// chosen label, and the finalist Choice's confidence, probabilities as
487/// [label, p] pairs in the caller's order (among the finalists, not over
488/// every option), model and request tokens; then `fit`, the probability
489/// that any option fits, from a Noul asked in the first request. Shares
490/// every Choice's judgment with `jev_choice_shortlist` on the same
491/// arguments. A single option is 1.0, with a NULL model, 0 tokens and a
492/// NULL `fit`, since nothing is sent.
493#[pg_extern(volatile, parallel_restricted, support = jev_support, requires = ["result_types"])]
494fn jev_choice_shortlist_full(
495    _row: AnyElement,
496    _question: &str,
497    _options: Vec<Option<String>>,
498) -> pgrx::composite_type!('static, "jev_shortlist_result") {
499    Failure::Unbatched("jev_choice_shortlist_full").raise()
500}
501
502/// [`jev_choice_shortlist_described`] in full.
503#[pg_extern(
504    name = "jev_choice_shortlist_full",
505    volatile,
506    parallel_restricted,
507    support = jev_support,
508    requires = ["result_types"]
509)]
510fn jev_choice_shortlist_full_described(
511    _row: AnyElement,
512    _question: &str,
513    _options: Vec<Option<String>>,
514    _descriptions: Vec<Option<String>>,
515) -> pgrx::composite_type!('static, "jev_shortlist_result") {
516    Failure::Unbatched("jev_choice_shortlist_full").raise()
517}
518
519/// The planner support function of every `jev*` function.
520#[pg_extern(immutable, parallel_safe)]
521fn jev_support(request: Internal) -> Internal {
522    scan::support(request)
523}
524
525/// This session's work since the backend started.
526#[pg_extern(volatile, parallel_restricted)]
527#[allow(clippy::type_complexity, reason = "pgrx reads the column names from this signature")]
528fn jev_stats() -> TableIterator<
529    'static,
530    (
531        name!(requests, i64),
532        name!(retries, i64),
533        name!(redials, i64),
534        name!(connections, i64),
535        name!(cache_hits, i64),
536        name!(cache_misses, i64),
537        name!(dedupe_hits, i64),
538        name!(input_tokens, i64),
539        name!(output_tokens, i64),
540        name!(cost, f64),
541        name!(in_flight, i64),
542    ),
543> {
544    let s = stats::snapshot();
545    let n = |v: u64| i64::try_from(v).unwrap_or(i64::MAX);
546    TableIterator::once((
547        n(s.requests),
548        n(s.retries),
549        n(s.redials),
550        n(s.connections),
551        n(s.cache_hits),
552        n(s.cache_misses),
553        n(s.dedupe_hits),
554        n(s.input_tokens),
555        n(s.output_tokens),
556        s.cost,
557        n(s.in_flight),
558    ))
559}
560
561/// One scan's settings and client, read once in the executor's startup
562/// (contract *Execution*: a generic plan outlives a `SET`).
563struct JevScan {
564    /// `None` under `jev.cache_only`, which never sends, and in an
565    /// EvalPlanQual recheck.
566    client: Option<Rc<Client<HttpsTransport, Backend, Telemetry>>>,
567    /// An EvalPlanQual recheck: a miss is a row version the statement
568    /// did not judge, refused rather than sent (contract *SQL surface*).
569    rechecking: bool,
570    endpoint: Endpoint,
571    concurrency: usize,
572    /// `jev.on_error = unsure`.
573    unsure: bool,
574    /// `jev.threshold`, for a `jev()` call that gives none.
575    threshold: f64,
576    budget: Rc<Budget>,
577    scope: KeyScope,
578    /// Answers received and not yet stored: judgements cannot call
579    /// Postgres, so `settle` writes them.
580    answered: Rc<RefCell<Vec<Receipt>>>,
581    asking: Rc<Asking>,
582    /// What this scan did, for EXPLAIN ANALYZE.
583    counts: Rc<Counts>,
584}
585
586/// One scan's work, as EXPLAIN ANALYZE shows it. `jev_stats()` counts
587/// the same things for the whole backend.
588#[derive(Default)]
589struct Counts {
590    hits: Cell<u64>,
591    misses: Cell<u64>,
592    deduped: Cell<u64>,
593    requests: Cell<u64>,
594    input_tokens: Cell<u64>,
595}
596
597/// The judgments the statement has asked, by cache key: an equal call in
598/// any of its scans waits on the first instead of sending (contract
599/// *Execution*, "Deduplicate before sending"). One per statement, like
600/// the [`Budget`].
601#[derive(Default)]
602struct Asking(RefCell<HashMap<CacheKey, Rc<Shared>>>);
603
604/// One judgment's answer, shared by every equal call in the statement.
605#[derive(Default)]
606struct Shared {
607    answer: RefCell<Option<Result<Judged, Unanswered>>>,
608    waiting: RefCell<Vec<Waker>>,
609}
610
611/// Why a judgment's request gave no answer.
612#[derive(Clone, Copy)]
613enum Unanswered {
614    /// It failed; the scan that sent it raises the error.
615    Failed,
616    /// Its scan dropped it (a rescan, a cancel): a waiter asks again.
617    Dropped,
618}
619
620impl Shared {
621    fn resolve(&self, answer: Result<Judged, Unanswered>) {
622        if self.answer.borrow().is_none() {
623            *self.answer.borrow_mut() = Some(answer);
624            for waker in self.waiting.take() {
625                waker.wake();
626            }
627        }
628    }
629
630    async fn wait(&self) -> Result<Judged, Unanswered> {
631        std::future::poll_fn(|cx| match self.answer.borrow().clone() {
632            Some(answer) => Poll::Ready(answer),
633            None => {
634                self.waiting.borrow_mut().push(cx.waker().clone());
635                Poll::Pending
636            }
637        })
638        .await
639    }
640}
641
642/// The judgments a request owes to their waiters. Those it drops
643/// unanswered leave `asking`, so a later equal call asks again, and wake
644/// their waiters with why.
645struct Owed {
646    asking: Rc<Asking>,
647    shared: Vec<(CacheKey, Rc<Shared>)>,
648    /// Set when the request failed; otherwise it was dropped.
649    failed: bool,
650}
651
652impl Drop for Owed {
653    fn drop(&mut self) {
654        // Never panics: this runs while a cancel unwinds.
655        let why = if self.failed { Unanswered::Failed } else { Unanswered::Dropped };
656        for (key, shared) in self.shared.drain(..) {
657            if shared.answer.borrow().is_none() {
658                if let Ok(mut asking) = self.asking.0.try_borrow_mut()
659                    && asking.get(&key).is_some_and(|s| Rc::ptr_eq(s, &shared))
660                {
661                    asking.remove(&key);
662                }
663                shared.resolve(Err(why));
664            }
665        }
666    }
667}
668
669/// One answered judgment, as `jev_cache` stores it.
670struct Receipt {
671    key: CacheKey,
672    layout: Layout,
673    state: String,
674    question: Vec<u8>,
675    answered_model: String,
676    request_id: Option<String>,
677    usage: jev_protocol::Usage,
678    /// The answer as `jev_cache.answer` stores it.
679    answer: serde_json::Value,
680}
681
682/// The spend guards, and what the statement has spent against them. One
683/// per statement, shared by its scans (contract *Cost and safety*).
684struct Budget {
685    max_rows: Option<u64>,
686    max_cost: Option<f64>,
687    price_per_mtok: f64,
688    /// Characters per input token, learned from the cache.
689    ratio: TokenRatio,
690    rows: Cell<u64>,
691    /// Dollars billed by the answers received, from their `usage`.
692    spent: Cell<f64>,
693    /// Worst-case dollars of the requests sent and not yet answered.
694    in_flight: Cell<f64>,
695}
696
697impl Budget {
698    fn read() -> Budget {
699        Budget {
700            max_rows: u64::try_from(MAX_ROWS.get()).ok(),
701            max_cost: Some(MAX_COST.get()).filter(|&c| c >= 0.0),
702            price_per_mtok: PRICE_PER_MTOK.get(),
703            ratio: learned_ratio(),
704            rows: Cell::new(0),
705            spent: Cell::new(0.0),
706            in_flight: Cell::new(0.0),
707        }
708    }
709
710    /// Dollars `chars` of request would cost at worst.
711    fn price(&self, requests: f64, chars: f64) -> f64 {
712        self.ratio.worst_case(requests, chars, self.price_per_mtok)
713    }
714
715    /// Refuses before sending when this row would take the statement over
716    /// a guard, counting what it has spent and the worst case of what it
717    /// has in flight; otherwise commits the row's worst case.
718    fn commit(self: &Rc<Self>, requests: u64, chars: usize) -> Result<Committed, Failure> {
719        let rows = self.rows.get() + 1;
720        if let Some(max) = self.max_rows.filter(|&max| rows > max) {
721            return Err(Failure::Budget(format!("this statement would send more than jev.max_rows ({max}) rows")));
722        }
723        let worst = self.price(requests as f64, chars as f64);
724        if let Some(max) = self.max_cost.filter(|&max| self.spent.get() + self.in_flight.get() + worst > max) {
725            return Err(Failure::Budget(format!("this statement would spend more than jev.max_cost (${max})")));
726        }
727        self.rows.set(rows);
728        self.in_flight.set(self.in_flight.get() + worst);
729        Ok(Committed { budget: self.clone(), worst: Some(worst) })
730    }
731}
732
733/// A row's worst case, held in flight until its answers report what was
734/// billed.
735struct Committed {
736    budget: Rc<Budget>,
737    /// `None` once settled.
738    worst: Option<f64>,
739}
740
741impl Committed {
742    /// Replaces the worst case with the input tokens the answers report,
743    /// for every attempt each took: with no idempotency key, an attempt
744    /// that timed out may have been billed too.
745    fn settle(&mut self, answers: &[jev_client::Answered]) {
746        let tokens: f64 = answers.iter().map(|a| a.response.usage().input_tokens as f64 * f64::from(a.attempts)).sum();
747        if let Some(worst) = self.worst.take() {
748            let b = &self.budget;
749            b.in_flight.set(b.in_flight.get() - worst);
750            b.spent.set(b.spent.get() + tokens * b.price_per_mtok / 1e6);
751        }
752    }
753}
754
755impl Drop for Committed {
756    /// Unanswered (failed, cancelled, cut off by a LIMIT): what it was
757    /// billed is unknown, so the worst case stays spent.
758    fn drop(&mut self) {
759        if let Some(worst) = self.worst.take() {
760            let b = &self.budget;
761            b.in_flight.set(b.in_flight.get() - worst);
762            b.spent.set(b.spent.get() + worst);
763        }
764    }
765}
766
767impl Judge for JevScan {
768    const NAME: &'static CStr = c"JevScan";
769    const EXTENSION: &'static CStr = c"postjevsql";
770    /// Indexed by [`Call::function`]; [`JevScan::lookup`] matches on it.
771    const FUNCTIONS: &'static [Function] = &[
772        Function { name: "jev_prob", args: &[Arg::Row, Arg::Text], returns: Returns::Float8 },
773        Function { name: "jev", args: &[Arg::Row, Arg::Text], returns: Returns::Bool },
774        Function { name: "jev", args: &[Arg::Row, Arg::Text, Arg::Float8], returns: Returns::Bool },
775        Function { name: "jev_score", args: &[Arg::Row, Arg::Text, Arg::TextArray], returns: Returns::Float8 },
776        Function { name: "jev_score_norm", args: &[Arg::Row, Arg::Text, Arg::TextArray], returns: Returns::Float8 },
777        Function { name: "jev_choice", args: &[Arg::Row, Arg::Text, Arg::TextArray], returns: Returns::Text },
778        Function { name: "jev_eval", args: &[Arg::Row, Arg::Text], returns: Returns::Record },
779        Function { name: "jev_score_full", args: &[Arg::Row, Arg::Text, Arg::TextArray], returns: Returns::Record },
780        Function { name: "jev_choice_full", args: &[Arg::Row, Arg::Text, Arg::TextArray], returns: Returns::Record },
781        Function {
782            name: "jev_confidence",
783            args: &[Arg::Row, Arg::Text, Arg::Text, Arg::TextArray],
784            returns: Returns::Float8,
785        },
786        Function { name: "jev_choice_tree", args: &[Arg::Row, Arg::Text, Arg::TextArray], returns: Returns::Text },
787        Function {
788            name: "jev_choice_tree",
789            args: &[Arg::Row, Arg::Text, Arg::TextArray, Arg::Float8],
790            returns: Returns::Text,
791        },
792        Function { name: "jev_choice_tree_full", args: &[Arg::Row, Arg::Text, Arg::TextArray], returns: Returns::Record },
793        Function {
794            name: "jev_choice_tree_full",
795            args: &[Arg::Row, Arg::Text, Arg::TextArray, Arg::Float8],
796            returns: Returns::Record,
797        },
798        Function { name: "jev_choice_shortlist", args: &[Arg::Row, Arg::Text, Arg::TextArray], returns: Returns::Text },
799        Function {
800            name: "jev_choice_shortlist",
801            args: &[Arg::Row, Arg::Text, Arg::TextArray, Arg::TextArray],
802            returns: Returns::Text,
803        },
804        Function {
805            name: "jev_choice_shortlist_full",
806            args: &[Arg::Row, Arg::Text, Arg::TextArray],
807            returns: Returns::Record,
808        },
809        Function {
810            name: "jev_choice_shortlist_full",
811            args: &[Arg::Row, Arg::Text, Arg::TextArray, Arg::TextArray],
812            returns: Returns::Record,
813        },
814    ];
815
816    fn begin(statement: &Statement) -> Result<Self, Refusal> {
817        let setting_err = |e: &dyn std::fmt::Display| Failure::Setting(e.to_string()).refusal();
818        let model = setting(&MODEL).ok_or_else(|| setting_err(&"jev.model is not set"))?;
819        let model = ModelId::pinned(&model).map_err(|e| setting_err(&e))?;
820        let endpoint = setting(&ENDPOINT).ok_or_else(|| setting_err(&"jev.endpoint is not set"))?;
821        let endpoint = Endpoint::parse(&endpoint, setting(&CA_FILE).as_deref(), setting(&DNS_SERVERS).as_deref())
822            .map_err(|e| setting_err(&e))?
823            .with_keep_alive(KeepAlive {
824                interval: Duration::from_millis(KEEPALIVE_INTERVAL.get() as u64),
825                timeout: Duration::from_millis(KEEPALIVE_TIMEOUT.get() as u64),
826            });
827        // The token ratio is learned once, with the statement's budget,
828        // and prices the rate limit's admissions and the tree's lookahead.
829        let budget = statement.shared(Budget::read);
830        let client = if CACHE_ONLY.get() || statement.rechecking() {
831            None
832        } else {
833            let key = api_key().map_err(|e| Failure::Key(e).refusal())?;
834            let limit = RateLimit::attach(
835                MAX_TOKENS_PER_SECOND.get() as f64,
836                MAX_REQUESTS_PER_MINUTE.get() as f64,
837                budget.ratio,
838            );
839            let transport = HttpsTransport::new(endpoint.clone()).rate_limited(limit);
840            let client = Client::new(transport, Backend, model.clone(), &key)
841                .map_err(|e| Failure::Client(e).refusal())?
842                .observed(Telemetry { price_per_mtok: PRICE_PER_MTOK.get() });
843            Some(Rc::new(client))
844        };
845        let scope = KeyScope {
846            model,
847            prompt: PROMPT_VERSION,
848            namespace: setting(&CACHE_NAMESPACE).unwrap_or_default(),
849        };
850        Ok(JevScan {
851            client,
852            rechecking: statement.rechecking(),
853            endpoint,
854            concurrency: CONCURRENCY.get() as usize,
855            unsure: ON_ERROR.get() == OnError::Unsure,
856            threshold: THRESHOLD.get(),
857            budget,
858            scope,
859            answered: Rc::default(),
860            asking: statement.shared(Asking::default),
861            counts: Rc::default(),
862        })
863    }
864
865    fn afford(estimate: &Estimate) -> Result<(), Refusal> {
866        // Nothing to price; and the cache query learning the token ratio
867        // passes through here itself, with no jev scan.
868        if CACHE_ONLY.get() || estimate.rows <= 0.0 {
869            return Ok(());
870        }
871        let budget = Budget::read();
872        let refuse = |message: String| {
873            Err(Failure::Budget(message).refusal())
874        };
875        if let Some(max) = budget.max_rows.filter(|&max| estimate.rows > max as f64) {
876            return refuse(format!("this statement is estimated to send {:.0} rows, over jev.max_rows ({max})", estimate.rows));
877        }
878        let cost = budget.price(estimate.rows, estimate.bytes);
879        if let Some(max) = budget.max_cost.filter(|&max| cost > max) {
880            return refuse(format!("this statement is estimated to spend up to ${cost:.6}, over jev.max_cost (${max})"));
881        }
882        Ok(())
883    }
884
885    /// The price before it is paid, and under ANALYZE what was paid
886    /// (contract *Cost and safety*). Plain EXPLAIN runs no rows, so it
887    /// cannot know the cache hits: it prices every candidate as a miss,
888    /// the same bound [`Judge::afford`] refuses on.
889    fn explain(estimate: Option<&Estimate>, run: Option<&Self>) -> Vec<Property> {
890        let estimated = |label, v: f64| Property { label, unit: None, value: Shown::Integer(v.round() as i64) };
891        let counted = |label, c: &Cell<u64>| Property {
892            label,
893            unit: None,
894            value: Shown::Integer(i64::try_from(c.get()).unwrap_or(i64::MAX)),
895        };
896        let dollars = |label, v| Property { label, unit: Some(c"USD"), value: Shown::Float(v, 6) };
897        let mut shown = Vec::new();
898        if let Some(e) = estimate {
899            let budget = Budget::read();
900            shown.push(estimated(c"Candidate Rows", e.rows));
901            shown.push(estimated(c"Estimated Requests", e.rows));
902            shown.push(estimated(c"Estimated Input Tokens", budget.ratio.tokens(e.rows, e.bytes)));
903            shown.push(dollars(c"Worst-Case Cost", budget.price(e.rows, e.bytes)));
904        }
905        if let Some(scan) = run {
906            let c = &scan.counts;
907            shown.push(counted(c"Cache Hits", &c.hits));
908            shown.push(counted(c"Cache Misses", &c.misses));
909            shown.push(counted(c"Shared Judgments", &c.deduped));
910            shown.push(counted(c"Requests", &c.requests));
911            shown.push(counted(c"Input Tokens", &c.input_tokens));
912            shown.push(dollars(c"Cost", c.input_tokens.get() as f64 * scan.budget.price_per_mtok / 1e6));
913        }
914        shown
915    }
916
917    /// `jev.concurrency`, clamped to the streams the server accepts.
918    fn window(&self) -> usize {
919        self.concurrency.min(stream_limit(&self.endpoint))
920    }
921
922    /// A call equal to one the statement has already asked waits for
923    /// that answer; the rest are looked up in the cache. Each distinct row
924    /// argument with a miss is one request, with the row as the state and
925    /// each distinct missed question about it asked once (contract
926    /// *Execution*, "row as state").
927    ///
928    /// A `jev_choice_tree` call is a label-tree search of several rounds:
929    /// each round's Choices are looked up on the backend and the misses
930    /// sent in one request, with the row as the state. A
931    /// `jev_choice_shortlist` call is the same with two rounds.
932    fn judge(&self, calls: Vec<Call>) -> Judgement {
933        let (lists, calls): (Vec<_>, Vec<_>) =
934            calls.into_iter().enumerate().partition(|(_, c)| SHORTLIST_FUNCTIONS.contains(&c.function));
935        let (trees, calls): (Vec<_>, Vec<_>) =
936            calls.into_iter().partition(|(_, c)| TREE_FUNCTIONS.contains(&c.function));
937        let (at, calls): (Vec<usize>, Vec<Call>) = calls.into_iter().unzip();
938        let trees = match trees.into_iter().map(|(i, c)| TreeCall::new(&c).map(|t| (i, t))).collect::<Result<Vec<_>, _>>() {
939            Ok(trees) => trees,
940            Err(e) => return Box::pin(async move { Err(e) }),
941        };
942        let lists = match lists
943            .into_iter()
944            .map(|(i, c)| ShortlistCall::new(&c).map(|l| (i, l)))
945            .collect::<Result<Vec<_>, _>>()
946        {
947            Ok(lists) => lists,
948            Err(e) => return Box::pin(async move { Err(e) }),
949        };
950        let flat = self.judge_flat(calls);
951        let (lookup, sender, unsure) = (self.lookup_state(), self.sender(), self.unsure);
952        Box::pin(async move {
953            let flat = flat.await?;
954            let mut outs: Vec<Option<Out>> = (0..at.len() + trees.len() + lists.len()).map(|_| None).collect();
955            for (i, out) in at.into_iter().zip(flat) {
956                outs[i] = out;
957            }
958            for (i, tree) in trees {
959                outs[i] = tree.search(&lookup, &sender, unsure).await?;
960            }
961            for (i, list) in lists {
962                outs[i] = list.search(&lookup, &sender, unsure).await?;
963            }
964            Ok(outs)
965        })
966    }
967    /// Stores the answers received since the last call.
968    fn settle(&self) -> Result<(), Refusal> {
969        let receipts = std::mem::take(&mut *self.answered.borrow_mut());
970        let (model, namespace) = (self.scope.model.as_str(), &self.scope.namespace);
971        if receipts.is_empty() {
972            return Ok(());
973        }
974        let store = || -> Result<(), pgrx::spi::Error> {
975        for r in receipts {
976            let answer = pgrx::JsonB(r.answer);
977            Spi::run_with_args(
978                "INSERT INTO jev_cache (key, model, answered_model, namespace, layout, state, question,
979                                        request_id, input_tokens, output_tokens, answer)
980                 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
981                 ON CONFLICT (key) DO NOTHING",
982                &[
983                    r.key.as_bytes().to_vec().into(),
984                    model.into(),
985                    r.answered_model.into(),
986                    namespace.as_str().into(),
987                    r.layout.tag().into(),
988                    r.state.into(),
989                    r.question.into(),
990                    r.request_id.into(),
991                    i64::try_from(r.usage.input_tokens).unwrap_or(i64::MAX).into(),
992                    i64::try_from(r.usage.output_tokens).unwrap_or(i64::MAX).into(),
993                    answer.into(),
994                ],
995            )?;
996        }
997        Ok(())
998        };
999        as_owner_of(c"jev_cache", store)
1000            .and_then(|r| r.map_err(|e| e.to_string()))
1001            .map_err(|e| Failure::Cache(e).refusal())
1002    }
1003}
1004
1005impl JevScan {
1006    /// Every call but a label-tree search's.
1007    fn judge_flat(&self, calls: Vec<Call>) -> Judgement {
1008        let asked = match self.lookup(&calls) {
1009            Ok(asked) => asked,
1010            Err(e) => return Box::pin(async move { Err(e) }),
1011        };
1012        let sender = self.sender();
1013        // Made now, not in the future: a judgement dropped before its
1014        // first poll still owes its keys.
1015        let owed = sender.owe(&asked.requests);
1016        let unsure = self.unsure;
1017        // Under `jev.on_error = unsure` a failed judgment is NULL; any
1018        // other failure, and every failure under `error`, raises.
1019        let judged = move |r: Result<Judged, Failure>| match r {
1020            Ok(p) => Ok(Some(p)),
1021            Err(e) if unsure && e.is_failed_judgment() => Ok(None),
1022            Err(e) => Err(e.refusal()),
1023        };
1024        Box::pin(async move {
1025            let Asked { requests, answers, shapes } = asked;
1026            let responses = match sender.send(&requests, owed).await {
1027                Ok(responses) => Some(responses),
1028                Err(e) => judged(Err(e)).map(|_| None)?,
1029            };
1030            let mut outs = Vec::with_capacity(answers.len());
1031            for (answer, shape) in answers.into_iter().zip(shapes) {
1032                let p = match answer {
1033                    Answer::Cached(p) => Some(p),
1034                    Answer::Asked(request, key) => responses
1035                        .as_ref()
1036                        .map(|r| requests[request].keys[&key].key.read(&r[request].response).0),
1037                    Answer::Shared(shared, again) => judged(sender.wait(shared, again).await)?,
1038                };
1039                outs.push(p.map(|j| shape.out(j)));
1040            }
1041            Ok(outs)
1042        })
1043    }
1044
1045    fn lookup_state(&self) -> Lookup {
1046        Lookup {
1047            scope: self.scope.clone(),
1048            planner: Planner::new(),
1049            asking: self.asking.clone(),
1050            counts: self.counts.clone(),
1051            sends: self.client.is_some(),
1052            rechecking: self.rechecking,
1053        }
1054    }
1055
1056    fn sender(&self) -> Sender {
1057        Sender {
1058            client: self.client.clone(),
1059            budget: self.budget.clone(),
1060            answered: self.answered.clone(),
1061            asking: self.asking.clone(),
1062            counts: self.counts.clone(),
1063        }
1064    }
1065
1066    /// Answers each call from the cache where it can, and groups the rest
1067    /// into requests. Runs on the backend, so it may query.
1068    fn lookup(&self, calls: &[Call]) -> Result<Asked, Refusal> {
1069        let mut requests: Vec<Request> = Vec::new();
1070        let mut answers = Vec::with_capacity(calls.len());
1071        let mut shapes = Vec::with_capacity(calls.len());
1072        let lookup = self.lookup_state();
1073        // Answered at once, or asked once every row's questions are known,
1074        // since those choose its layout.
1075        let mut pending: Vec<Option<(&String, &String, Question)>> = Vec::with_capacity(calls.len());
1076        for call in calls {
1077            let noul = |q: &String| Question::Noul(Noul::new(Json::text(q)));
1078            let (row, question, asked, shape) = match (call.function, call.args.as_slice()) {
1079                (0, [Value::Json(row), Value::Text(q)]) => (row, q, noul(q), Shape::Probability),
1080                (6, [Value::Json(row), Value::Text(q)]) => (row, q, noul(q), Shape::Full),
1081                (1, [Value::Json(row), Value::Text(q)]) => (row, q, noul(q), Shape::AtLeast(self.threshold)),
1082                (2, [Value::Json(row), Value::Text(q), Value::Float8(t)]) => {
1083                    if !(0.0..=1.0).contains(t) {
1084                        return Err(Failure::Setting(format!("jev() threshold must be between 0 and 1, not {t}")).refusal());
1085                    }
1086                    (row, q, noul(q), Shape::AtLeast(*t))
1087                }
1088                (f @ (3 | 4 | 7), [Value::Json(row), Value::Text(q), Value::TextArray(levels)]) => {
1089                    let shape = match f {
1090                        3 => Shape::Level,
1091                        4 => Shape::Normalized,
1092                        _ => Shape::Full,
1093                    };
1094                    (row, q, Question::Score(score(q, levels)?), shape)
1095                }
1096                (f @ (5 | 8), [Value::Json(row), Value::Text(q), Value::TextArray(options)]) => {
1097                    let shape = if f == 5 { Shape::Label } else { Shape::Full };
1098                    match choice(q, options)? {
1099                        Choosing::Ask(c) => (row, q, Question::Choice(c), shape),
1100                        Choosing::Only(label) => {
1101                            // Nothing to judge: no request, no cache row.
1102                            shapes.push(shape);
1103                            answers.push(Some(Answer::Cached(Judged::only(label))));
1104                            pending.push(None);
1105                            continue;
1106                        }
1107                    }
1108                }
1109                (9, [Value::Json(row), Value::Text(q), Value::Text(kind), Value::TextArray(options)]) => {
1110                    match kind.as_str() {
1111                        "score" => (row, q, Question::Score(score(q, options)?), Shape::Confidence),
1112                        "choice" => match choice(q, options)? {
1113                            Choosing::Ask(c) => (row, q, Question::Choice(c), Shape::Confidence),
1114                            Choosing::Only(label) => {
1115                                shapes.push(Shape::Confidence);
1116                                answers.push(Some(Answer::Cached(Judged::only(label))));
1117                                pending.push(None);
1118                                continue;
1119                            }
1120                        },
1121                        other => {
1122                            return Err(Failure::Setting(format!(
1123                                "jev_confidence: kind must be 'choice' or 'score', not '{other}'; a Noul answer carries no confidence"
1124                            ))
1125                            .refusal());
1126                        }
1127                    }
1128                }
1129                (f, args) => unreachable!("FUNCTIONS[{f}] does not take {args:?}"),
1130            };
1131            shapes.push(shape);
1132            answers.push(None);
1133            pending.push(Some((row, question, asked)));
1134        }
1135        let layouts = lookup.layouts(pending.iter().flatten().map(|(row, _, asked)| (*row, asked)))?;
1136        let answers = answers
1137            .into_iter()
1138            .zip(pending)
1139            .map(|(answer, asked)| match (answer, asked) {
1140                (Some(answer), _) => Ok(answer),
1141                (None, Some((row, question, asked))) => lookup.one(&mut requests, row, layouts[row], question, asked),
1142                (None, None) => unreachable!("every call is answered or asked"),
1143            })
1144            .collect::<Result<Vec<_>, _>>()?;
1145        Ok(Asked { requests, answers, shapes })
1146    }
1147}
1148
1149/// [`JevScan::FUNCTIONS`]' indexes of `jev_choice_tree`, without and with
1150/// `tau`, then of `jev_choice_tree_full`.
1151const TREE_FUNCTIONS: [usize; 4] = [10, 11, 12, 13];
1152
1153/// Of [`TREE_FUNCTIONS`], those returning `jev_tree_result`.
1154const TREE_FULL_FUNCTIONS: [usize; 2] = [12, 13];
1155
1156/// How a `jev_choice_tree` path separates its labels, as `Within:` in each
1157/// node's instructions shows the parent path.
1158const PATH_SEPARATOR: &str = " > ";
1159
1160/// One `jev_choice_tree` call: its row, question and tree, built on the
1161/// backend before anything is sent.
1162struct TreeCall {
1163    row: String,
1164    question: String,
1165    tree: Tree,
1166    tau: Option<Tau>,
1167    /// `jev_choice_tree_full`: the whole [`Outcome`], not only its path.
1168    full: bool,
1169}
1170
1171impl TreeCall {
1172    /// Refused with 22023 before anything is sent: a NULL or empty path, a
1173    /// repeated one, a path that is also a group, or `tau` outside [0, 1].
1174    fn new(call: &Call) -> Result<TreeCall, Refusal> {
1175        let refuse = |m: String| Failure::Setting(format!("jev_choice_tree: {m}")).refusal();
1176        let (row, question, paths, tau) = match call.args.as_slice() {
1177            [Value::Json(row), Value::Text(q), Value::TextArray(paths)] => (row, q, paths, None),
1178            [Value::Json(row), Value::Text(q), Value::TextArray(paths), Value::Float8(t)] => {
1179                let tau = Tau::new(*t).ok_or_else(|| refuse(format!("tau must be between 0 and 1, not {t}")))?;
1180                (row, q, paths, Some(tau))
1181            }
1182            args => unreachable!("jev_choice_tree does not take {args:?}"),
1183        };
1184        let paths = paths
1185            .iter()
1186            .map(|p| p.as_deref().ok_or_else(|| refuse("a path is NULL".into())))
1187            .collect::<Result<Vec<_>, _>>()?;
1188        if paths.is_empty() {
1189            return Err(refuse("no paths".into()));
1190        }
1191        let branch = Branch::from_paths(paths.iter().map(|p| p.split(PATH_SEPARATOR)))
1192            .map_err(|e| refuse(e.to_string()))?;
1193        let tree = Tree::new(branch).map_err(|e| refuse(e.to_string()))?;
1194        let full = TREE_FULL_FUNCTIONS.contains(&call.function);
1195        Ok(TreeCall { row: row.clone(), question: question.clone(), tree, tau, full })
1196    }
1197
1198    /// The label-tree search: each round's Choices are looked up on the
1199    /// backend, and the misses sent together in one request with the row
1200    /// as the state. Each node's Choice is its own judgment and its own
1201    /// `jev_cache` row. Returns the path of the leaf, or under `tau` of the
1202    /// deepest node at or above it (NULL when that is the root).
1203    async fn search(self, lookup: &Lookup, sender: &Sender, unsure: bool) -> Result<Option<Out>, Refusal> {
1204        let TreeCall { row, question, tree, tau, full } = self;
1205        let describe = Describe::default();
1206        let choice = |a: label_tree::Ask| {
1207            tree.choice(a.node(), a.order(), &question, describe).expect("the search asks only nodes of two or more children")
1208        };
1209        let mut search = Search::new(&tree, Params { tau, ..Params::default() });
1210        let ratio = sender.budget.ratio;
1211        // Under `full`, the "does any label fit" Noul rides in the first
1212        // round's request; NULL when the search sends nothing.
1213        let mut gate = full.then(|| tree.gate(&question, describe)).flatten();
1214        let mut fit = None;
1215        loop {
1216            let asks = match search.step(|a| {
1217                question_bytes(&Question::Choice(choice(a))).map_or(0, |b| (b.len() as f64 / ratio.chars_per_token()) as usize)
1218            }) {
1219                Step::Done(outcome) => return Ok(tree_out(&tree, &outcome, full, fit)),
1220                Step::Ask(asks) => asks,
1221            };
1222            let choices: Vec<Question> = asks.iter().map(|&a| Question::Choice(choice(a))).collect();
1223            // A failed Choice under `unsure` leaves the search unfinished.
1224            let Some((answered, gated)) = round(lookup, sender, unsure, &row, &question, choices, gate.take()).await? else {
1225                return Ok(None);
1226            };
1227            fit = fit.or(gated);
1228            for (ask, judged) in asks.into_iter().zip(answered) {
1229                search.answer(ask, &probabilities(&judged)?).map_err(|e| wrong_answer(&e))?;
1230            }
1231        }
1232    }
1233}
1234
1235/// A Choice judgment's probabilities, in the order its options were sent.
1236fn probabilities(judged: &Judged) -> Result<Vec<f64>, Refusal> {
1237    judged.choice_probabilities().map_err(|e| Failure::Cache(e).refusal())
1238}
1239
1240/// One round of a multi-round search about `row`: `choices` looked up on
1241/// the backend, and the misses sent together in one request with the row
1242/// as the state. Each Choice is its own judgment and its own `jev_cache`
1243/// row. `gate`, the "does any label fit" Noul, is one more judgment in
1244/// the same request. Returns each Choice's judgment in the order sent and
1245/// the gate's probability, or `None` when one failed under
1246/// `jev.on_error = unsure`.
1247async fn round(
1248    lookup: &Lookup,
1249    sender: &Sender,
1250    unsure: bool,
1251    row: &str,
1252    question: &str,
1253    choices: Vec<Question>,
1254    gate: Option<Noul>,
1255) -> Result<Option<(Vec<Judged>, Option<f64>)>, Refusal> {
1256    let (l, r, q) = (lookup.clone(), row.to_owned(), question.to_owned());
1257    let gated = gate.is_some();
1258    let (requests, answers) = scan::on_backend(move || {
1259        let mut requests = Vec::new();
1260        let asked: Vec<Question> = choices.into_iter().chain(gate.map(Question::Noul)).collect();
1261        let layouts = l.layouts(asked.iter().map(|c| (&r, c)))?;
1262        let answers = asked
1263            .into_iter()
1264            .map(|c| l.one(&mut requests, &r, layouts[&r], &q, c))
1265            .collect::<Result<Vec<_>, _>>()?;
1266        Ok::<_, Refusal>((requests, answers))
1267    })
1268    .await?;
1269    let judged = |r: Result<Judged, Failure>| match r {
1270        Ok(p) => Ok(Some(p)),
1271        Err(e) if unsure && e.is_failed_judgment() => Ok(None),
1272        Err(e) => Err(e.refusal()),
1273    };
1274    let owed = sender.owe(&requests);
1275    let responses = match sender.send(&requests, owed).await {
1276        Ok(responses) => Some(responses),
1277        Err(e) => judged(Err(e)).map(|_| None)?,
1278    };
1279    let mut answered = Vec::with_capacity(answers.len());
1280    for answer in answers {
1281        let p = match answer {
1282            Answer::Cached(p) => Some(p),
1283            Answer::Asked(request, key) => {
1284                responses.as_ref().map(|r| requests[request].keys[&key].key.read(&r[request].response).0)
1285            }
1286            Answer::Shared(shared, again) => judged(sender.wait(shared, again).await)?,
1287        };
1288        let Some(p) = p else { return Ok(None) };
1289        answered.push(p);
1290    }
1291    let fit = if gated {
1292        let gate = answered.pop().expect("the gate was asked last");
1293        Some(gate.noul_probability().map_err(|e| Failure::Cache(e).refusal())?)
1294    } else {
1295        None
1296    };
1297    Ok(Some((answered, fit)))
1298}
1299
1300/// An answer a search cannot take: the server answered a question other
1301/// than the one asked.
1302fn wrong_answer(e: &dyn std::fmt::Display) -> Refusal {
1303    let error = jev_protocol::ProtocolError::Response(e.to_string());
1304    Failure::Client(ClientError::Response { error, request_id: None }).refusal()
1305}
1306
1307/// [`JevScan::FUNCTIONS`]' indexes of `jev_choice_shortlist`, without and
1308/// with descriptions, then of `jev_choice_shortlist_full`.
1309const SHORTLIST_FUNCTIONS: [usize; 4] = [14, 15, 16, 17];
1310
1311/// Of [`SHORTLIST_FUNCTIONS`], those returning `jev_choice_result`.
1312const SHORTLIST_FULL_FUNCTIONS: [usize; 2] = [16, 17];
1313
1314/// One `jev_choice_shortlist` call, validated on the backend before
1315/// anything is sent.
1316struct ShortlistCall {
1317    row: String,
1318    question: String,
1319    list: Shortlist,
1320    /// `jev_choice_shortlist_full`: the finalist Choice's judgment, not
1321    /// only its label.
1322    full: bool,
1323}
1324
1325impl ShortlistCall {
1326    /// Refused with 22023 before anything is sent: no options, a NULL,
1327    /// empty or repeated one, descriptions not one per option, or more
1328    /// options than one finalist Choice can take.
1329    fn new(call: &Call) -> Result<ShortlistCall, Refusal> {
1330        let refuse = |m: String| Failure::Setting(format!("jev_choice_shortlist: {m}")).refusal();
1331        let (row, question, options, descriptions) = match call.args.as_slice() {
1332            [Value::Json(row), Value::Text(q), Value::TextArray(o)] => (row, q, o, None),
1333            [Value::Json(row), Value::Text(q), Value::TextArray(o), Value::TextArray(d)] => (row, q, o, Some(d)),
1334            args => unreachable!("jev_choice_shortlist does not take {args:?}"),
1335        };
1336        if let Some(d) = descriptions.filter(|d| d.len() != options.len()) {
1337            return Err(refuse(format!("{} options but {} descriptions", options.len(), d.len())));
1338        }
1339        let options = options
1340            .iter()
1341            .enumerate()
1342            .map(|(i, o)| {
1343                let label = o.clone().ok_or_else(|| refuse(format!("option {} is NULL", i + 1)))?;
1344                let description = descriptions.and_then(|d| d[i].as_deref()).map(Json::text);
1345                Ok((label, description))
1346            })
1347            .collect::<Result<Vec<_>, Refusal>>()?;
1348        let list = Shortlist::new(options, shortlist::Params::default()).map_err(|e| refuse(e.to_string()))?;
1349        let full = SHORTLIST_FULL_FUNCTIONS.contains(&call.function);
1350        Ok(ShortlistCall { row: row.clone(), question: question.clone(), list, full })
1351    }
1352
1353    /// Two rounds, each one request: the chunks, then the finalists.
1354    /// Returns the chosen label, or under `full` the `jev_choice_result`
1355    /// of the finalist Choice with that label.
1356    async fn search(self, lookup: &Lookup, sender: &Sender, unsure: bool) -> Result<Option<Out>, Refusal> {
1357        let ShortlistCall { row, question, list, full } = self;
1358        let mut search = shortlist::Search::new(&list);
1359        let mut finalists = None;
1360        // Under `full`, the "does any label fit" Noul rides in the first
1361        // round's request; NULL when the search sends nothing.
1362        let mut gate = full.then(|| list.gate(&question)).flatten();
1363        let mut fit = None;
1364        loop {
1365            let asks = match search.step() {
1366                shortlist::Step::Done(outcome) => {
1367                    let label = list.label(outcome.choice);
1368                    if !full {
1369                        return Ok(Some(Out::Text(label.to_owned())));
1370                    }
1371                    let judged = finalists.unwrap_or_else(|| Judged::only(label.into()));
1372                    let mut record = judged.record();
1373                    // The label the search chose, as `jev_choice_shortlist`
1374                    // returns it, even where a tie let the vendor name another.
1375                    record[0] = Some(Field::Text(label.to_owned()));
1376                    record.push(fit.map(Field::Float8));
1377                    return Ok(Some(Out::Record(record)));
1378                }
1379                shortlist::Step::Ask(asks) => asks,
1380            };
1381            let choices = asks.iter().map(|&a| Question::Choice(list.choice(&search, a, &question))).collect();
1382            let Some((answered, gated)) = round(lookup, sender, unsure, &row, &question, choices, gate.take()).await? else {
1383                return Ok(None);
1384            };
1385            fit = fit.or(gated);
1386            for (ask, judged) in asks.into_iter().zip(answered) {
1387                search.answer(ask, &probabilities(&judged)?).map_err(|e| wrong_answer(&e))?;
1388                if ask == shortlist::Ask::Finalists {
1389                    finalists = Some(judged);
1390                }
1391            }
1392        }
1393    }
1394}
1395
1396/// A finished search as its function returns it: the path, NULL at the
1397/// root, or under `full` the `jev_tree_result` record, with `fit` the
1398/// gate's probability.
1399fn tree_out(tree: &Tree, outcome: &Outcome, full: bool, fit: Option<f64>) -> Option<Out> {
1400    let (node, probability, separation, depth, leaf) = match *outcome {
1401        Outcome::Leaf { leaf, probability, separation } => (leaf, probability, separation, tree.depth(leaf), true),
1402        Outcome::Stop { node, depth, leaf, probability } => (node, probability, None, depth, leaf),
1403    };
1404    let path = tree.path(node);
1405    let path = (!path.is_empty()).then(|| path.join(PATH_SEPARATOR));
1406    if !full {
1407        return path.map(Out::Text);
1408    }
1409    let depth = i32::try_from(depth).expect("a label path is shorter than i32::MAX");
1410    Some(Out::Record(vec![
1411        path.map(Field::Text),
1412        Some(Field::Float8(probability)),
1413        separation.map(Field::Float8),
1414        Some(Field::Int4(depth)),
1415        Some(Field::Bool(leaf)),
1416        fit.map(Field::Float8),
1417    ]))
1418}
1419
1420/// What a judgement needs to look a question up: the cache, and the
1421/// statement's judgments already asked. Cloned into [`scan::on_backend`]
1422/// work by a label-tree search, which looks each round up.
1423#[derive(Clone)]
1424struct Lookup {
1425    scope: KeyScope,
1426    /// Chooses each row's layout and state (contract *Execution*).
1427    planner: Planner,
1428    asking: Rc<Asking>,
1429    counts: Rc<Counts>,
1430    /// Whether a miss may be sent: not under `jev.cache_only`, nor in a
1431    /// recheck.
1432    sends: bool,
1433    /// An EvalPlanQual recheck, whose miss is refused as a serialization
1434    /// failure.
1435    rechecking: bool,
1436}
1437
1438impl Lookup {
1439    /// Each row's layout, from the distinct questions asked about it.
1440    fn layouts<'r, 'q>(&self, asks: impl Iterator<Item = (&'r String, &'q Question)>) -> Result<HashMap<&'r String, Planned>, Refusal> {
1441        let invalid = |e| Failure::Client(ClientError::Invalid(e)).refusal();
1442        let asks = asks.map(|(row, q)| Ok((row, question_bytes(q).map_err(invalid)?))).collect::<Result<Vec<_>, Refusal>>()?;
1443        Ok(self.planner.layouts(asks.iter().map(|(row, q)| (*row, q.as_slice()))))
1444    }
1445
1446    /// Answers `asked` about `row` from the statement's judgments or the
1447    /// cache, or adds it to `row`'s request, placed under the `layout` the
1448    /// planner chose for it. Runs on the backend, so it may query.
1449    /// `question` is its text, for errors.
1450    fn one(
1451        &self,
1452        requests: &mut Vec<Request>,
1453        row: &String,
1454        layout: Planned,
1455        question: &str,
1456        asked: Question,
1457    ) -> Result<Answer, Refusal> {
1458        let invalid = |e| Failure::Client(ClientError::Invalid(e)).refusal();
1459        let placed = self.planner.place(layout, row).map_err(invalid)?;
1460        let bytes = question_bytes(&asked).map_err(invalid)?;
1461        let key = CacheKey::new(&self.scope, &placed, &asked).map_err(invalid)?;
1462        if let Some(shared) = self.asking.0.borrow().get(&key) {
1463            stats::deduped();
1464            self.counts.deduped.set(self.counts.deduped.get() + 1);
1465            let again = Again { row: row.clone(), placed, text: question.to_owned(), question: asked, cache_key: key, bytes };
1466            return Ok(Answer::Shared(shared.clone(), again));
1467        }
1468        let hit = as_owner_of(c"jev_cache", || Spi::get_one_with_args::<pgrx::JsonB>(
1469            // One row always: pgrx's get_one refuses an empty result.
1470            "SELECT (SELECT jsonb_build_object('answer', answer, 'model', answered_model,
1471                                               'input_tokens', input_tokens, 'output_tokens', output_tokens)
1472                     FROM jev_cache WHERE key = $1)",
1473            &[key.as_bytes().to_vec().into()],
1474        ))
1475        .and_then(|r| r.map_err(|e| e.to_string()))
1476        .and_then(|hit| hit.map(|a| Judged::stored(&a.0)).transpose())
1477        .map_err(|e| Failure::Cache(e).refusal())?;
1478        stats::cached(hit.is_some());
1479        let counted = if hit.is_some() { &self.counts.hits } else { &self.counts.misses };
1480        counted.set(counted.get() + 1);
1481        if let Some(p) = hit {
1482            return Ok(Answer::Cached(p));
1483        }
1484        if self.rechecking {
1485            return Err(Failure::Recheck(question.to_owned()).refusal());
1486        }
1487        if !self.sends {
1488            return Err(Failure::CacheOnly(question.to_owned()).refusal());
1489        }
1490        let request = match requests.iter().position(|r| r.row == *row && r.placed.layout() == layout.layout()) {
1491            Some(i) => i,
1492            None => {
1493                requests.push(Request { row: row.clone(), placed, questions: Questions::new(), keys: HashMap::new() });
1494                requests.len() - 1
1495            }
1496        };
1497        let r = &mut requests[request];
1498        let id = if r.keys.is_empty() { "q".to_owned() } else { format!("q{}", r.keys.len()) };
1499        let k = AnswerKey::ask(&mut r.questions, &id, asked).map_err(invalid)?;
1500        let shared = Rc::new(Shared::default());
1501        self.asking.0.borrow_mut().insert(key, shared.clone());
1502        r.keys.insert(key, Pending { key: k, cache_key: key, text: question.to_owned(), bytes, shared });
1503        Ok(Answer::Asked(request, key))
1504    }
1505}
1506
1507/// A Score of `levels`, lowest first, in the caller's order. Refused
1508/// with 22023 before anything is sent: a NULL level, or a count outside
1509/// 2 to 10.
1510fn score(question: &str, levels: &[Option<String>]) -> Result<Score, Refusal> {
1511    let refuse = |m: String| Failure::Setting(format!("jev_score: {m}")).refusal();
1512    let levels = levels
1513        .iter()
1514        .map(|l| l.as_deref().map(Json::text).ok_or_else(|| refuse("a level is NULL".into())))
1515        .collect::<Result<Vec<_>, _>>()?;
1516    Score::new(Json::text(question), levels).map_err(|e| refuse(e.to_string()))
1517}
1518
1519/// A Choice of `options`, in the caller's order, or the one option there
1520/// is. Refused with 22023 before anything is sent: a NULL, empty or
1521/// repeated option, or a count outside 1 to 255.
1522fn choice(question: &str, options: &[Option<String>]) -> Result<Choosing, Refusal> {
1523    let refuse = |m: String| Failure::Setting(format!("jev_choice: {m}")).refusal();
1524    let options = options
1525        .iter()
1526        .map(|o| o.clone().ok_or_else(|| refuse("an option is NULL".into())))
1527        .collect::<Result<Vec<_>, _>>()?;
1528    match options.as_slice() {
1529        [only] if !only.is_empty() => Ok(Choosing::Only(only.as_str().into())),
1530        _ => Choice::new(Json::text(question), options.into_iter().map(|o| (o, None)))
1531            .map(Choosing::Ask)
1532            .map_err(|e| refuse(e.to_string())),
1533    }
1534}
1535
1536enum Choosing {
1537    Ask(Choice),
1538    /// A single option, answered with probability 1 and never sent, as the
1539    /// vendor's hierarchical cookbook does.
1540    Only(Rc<str>),
1541}
1542
1543/// A judgment's answer, as its calls read it: one per cache row, shared
1544/// by every call asking the same question about the same row.
1545#[derive(Clone, Debug)]
1546struct Judged {
1547    verdict: Verdict,
1548    /// What the `*_full` functions add, from the same cache row.
1549    full: Rc<Full>,
1550}
1551
1552#[derive(Clone, Debug)]
1553enum Verdict {
1554    /// The probability that the answer is yes.
1555    Noul(f64),
1556    /// The probability-weighted level, how many levels there are, and the
1557    /// vendor's confidence.
1558    Score { score: f64, levels: usize, confidence: f64 },
1559    /// The label chosen, and the vendor's confidence.
1560    Choice { label: Rc<str>, confidence: f64 },
1561}
1562
1563#[derive(Debug)]
1564struct Full {
1565    /// `jev_cache.answer`.
1566    answer: serde_json::Value,
1567    /// `None` for a judgment nobody answered (a single option).
1568    model: Option<String>,
1569    input_tokens: u64,
1570    output_tokens: u64,
1571}
1572
1573impl Judged {
1574    /// A Noul's probability of yes.
1575    fn noul_probability(&self) -> Result<f64, String> {
1576        match self.verdict {
1577            Verdict::Noul(p) => Ok(p),
1578            _ => Err(format!("not a Noul answer: {}", self.full.answer)),
1579        }
1580    }
1581
1582    /// Reads the cache lookup: `jev_cache.answer`, as [`AnswerKey::read`]
1583    /// writes it, with its row's model and tokens.
1584    fn stored(row: &serde_json::Value) -> Result<Judged, String> {
1585        let answer = &row["answer"];
1586        let verdict = if let Some(p) = answer["noul"].as_f64() {
1587            Verdict::Noul(p)
1588        } else {
1589            let unreadable = || format!("unreadable cached answer {answer}");
1590            let confidence = answer["confidence"].as_f64().ok_or_else(unreadable)?;
1591            if let Some(label) = answer["choice"].as_str() {
1592                Verdict::Choice { label: label.into(), confidence }
1593            } else {
1594                match (answer["score"].as_f64(), answer["probabilities"].as_array()) {
1595                    (Some(score), Some(ps)) if ps.len() >= 2 => Verdict::Score { score, levels: ps.len(), confidence },
1596                    _ => return Err(unreadable()),
1597                }
1598            }
1599        };
1600        let full = Full {
1601            answer: answer.clone(),
1602            model: row["model"].as_str().map(str::to_owned),
1603            input_tokens: row["input_tokens"].as_u64().unwrap_or(0),
1604            output_tokens: row["output_tokens"].as_u64().unwrap_or(0),
1605        };
1606        Ok(Judged { verdict, full: Rc::new(full) })
1607    }
1608
1609    /// A Choice's probabilities, in the order its options were sent.
1610    fn choice_probabilities(&self) -> Result<Vec<f64>, String> {
1611        let unreadable = || format!("unreadable Choice probabilities {}", self.full.answer);
1612        self.full.answer["probabilities"]
1613            .as_array()
1614            .ok_or_else(unreadable)?
1615            .iter()
1616            .map(|pair| pair[1].as_f64().ok_or_else(unreadable))
1617            .collect()
1618    }
1619
1620    /// The one option there is: probability 1, never sent, so no model
1621    /// and no tokens.
1622    fn only(label: Rc<str>) -> Judged {
1623        let answer = serde_json::json!({ "choice": &*label, "confidence": 1.0, "probabilities": [[&*label, 1.0]] });
1624        let full = Full { answer, model: None, input_tokens: 0, output_tokens: 0 };
1625        Judged { verdict: Verdict::Choice { label, confidence: 1.0 }, full: Rc::new(full) }
1626    }
1627
1628    /// The `*_full` record, in its composite type's column order.
1629    fn record(&self) -> Vec<Option<Field>> {
1630        let Full { answer, model, input_tokens, output_tokens } = &*self.full;
1631        let json = |k: &str| (!answer[k].is_null()).then(|| Field::Jsonb(answer[k].to_string()));
1632        let tokens = |t: u64| Some(Field::Int4(i32::try_from(t).unwrap_or(i32::MAX)));
1633        let mut fields = match &self.verdict {
1634            Verdict::Noul(p) => vec![Some(Field::Float8(*p))],
1635            Verdict::Score { score, confidence, .. } => {
1636                vec![Some(Field::Float8(*score)), Some(Field::Float8(*confidence)), json("probabilities"), json("legend")]
1637            }
1638            Verdict::Choice { label, confidence } => {
1639                vec![Some(Field::Text(label.to_string())), Some(Field::Float8(*confidence)), json("probabilities")]
1640            }
1641        };
1642        fields.extend([model.clone().map(Field::Text), tokens(*input_tokens), tokens(*output_tokens)]);
1643        fields
1644    }
1645}
1646
1647/// What a call returns from its judgment.
1648#[derive(Clone, Copy)]
1649enum Shape {
1650    /// `jev_prob`.
1651    Probability,
1652    /// `jev()`: true at or above the threshold, as pg-jev's `>=`.
1653    AtLeast(f64),
1654    /// `jev_score`.
1655    Level,
1656    /// `jev_score_norm`.
1657    Normalized,
1658    /// `jev_choice`.
1659    Label,
1660    /// `jev_confidence`, of a Choice or a Score.
1661    Confidence,
1662    /// `jev_eval`, `jev_score_full` and `jev_choice_full`.
1663    Full,
1664}
1665
1666impl Shape {
1667    /// The cache key covers the question's type, so a call's judgment is
1668    /// always of its own kind.
1669    fn out(self, judged: Judged) -> Out {
1670        match (self, &judged.verdict) {
1671            (Shape::Full, _) => Out::Record(judged.record()),
1672            (Shape::Probability, Verdict::Noul(p)) => Out::Float8(*p),
1673            (Shape::AtLeast(t), Verdict::Noul(p)) => Out::Bool(*p >= t),
1674            (Shape::Level, Verdict::Score { score, .. }) => Out::Float8(*score),
1675            (Shape::Normalized, Verdict::Score { score, levels, .. }) => Out::Float8(score / (levels - 1) as f64),
1676            (Shape::Label, Verdict::Choice { label, .. }) => Out::Text(label.to_string()),
1677            (Shape::Confidence, Verdict::Score { confidence, .. } | Verdict::Choice { confidence, .. }) => {
1678                Out::Float8(*confidence)
1679            }
1680            (_, verdict) => unreachable!("{verdict:?} answered a call of another kind"),
1681        }
1682    }
1683}
1684
1685/// A question's key in its response, by kind.
1686#[derive(Clone, Copy)]
1687enum AnswerKey {
1688    Noul(Key<NoulAnswer>),
1689    Score(Key<ScoreAnswer>),
1690    Choice(Key<ChoiceAnswer>),
1691}
1692
1693impl AnswerKey {
1694    fn ask(questions: &mut Questions, id: &str, question: Question) -> Result<AnswerKey, jev_protocol::ProtocolError> {
1695        match question {
1696            Question::Noul(q) => questions.noul(id, q).map(AnswerKey::Noul),
1697            Question::Score(q) => questions.score(id, q).map(AnswerKey::Score),
1698            Question::Choice(q) => questions.choice(id, q).map(AnswerKey::Choice),
1699        }
1700    }
1701
1702    /// The judgment, and the full answer `jev_cache` stores.
1703    fn read(self, response: &Response) -> (Judged, serde_json::Value) {
1704        let (verdict, stored) = match self {
1705            AnswerKey::Noul(k) => {
1706                let p = response.get(k).noul;
1707                (Verdict::Noul(p), serde_json::json!({ "noul": p }))
1708            }
1709            AnswerKey::Score(k) => {
1710                let a = response.get(k);
1711                let verdict = Verdict::Score { score: a.score, levels: a.probabilities.len(), confidence: a.confidence };
1712                let stored = serde_json::json!({
1713                    "score": a.score,
1714                    "confidence": a.confidence,
1715                    "probabilities": a.probabilities,
1716                    "legend": a.legend,
1717                });
1718                (verdict, stored)
1719            }
1720            AnswerKey::Choice(k) => {
1721                let a = response.get(k);
1722                // Probabilities as pairs, in the order asked: a jsonb
1723                // object would sort them.
1724                let stored = serde_json::json!({
1725                    "choice": a.choice,
1726                    "confidence": a.confidence,
1727                    "probabilities": a.probabilities,
1728                });
1729                (Verdict::Choice { label: a.choice.as_str().into(), confidence: a.confidence }, stored)
1730            }
1731        };
1732        let usage = response.usage();
1733        let full = Full {
1734            answer: stored.clone(),
1735            model: Some(response.model().to_owned()),
1736            input_tokens: usage.input_tokens,
1737            output_tokens: usage.output_tokens,
1738        };
1739        (Judged { verdict, full: Rc::new(full) }, stored)
1740    }
1741}
1742
1743/// A row's calls after the cache: the requests still to send, and where
1744/// each call's answer comes from.
1745struct Asked {
1746    requests: Vec<Request>,
1747    answers: Vec<Answer>,
1748    /// Per call, beside `answers`.
1749    shapes: Vec<Shape>,
1750}
1751
1752enum Answer {
1753    Cached(Judged),
1754    /// Request index, and the judgment within it.
1755    Asked(usize, CacheKey),
1756    /// An equal judgment's, asked earlier in this statement; and how to
1757    /// ask it again if that request is dropped.
1758    Shared(Rc<Shared>, Again),
1759}
1760
1761/// A shared call's own judgment, to send if the request it waited on is
1762/// dropped unanswered.
1763struct Again {
1764    row: String,
1765    placed: Placement,
1766    /// The question's text, for errors.
1767    text: String,
1768    question: Question,
1769    cache_key: CacheKey,
1770    bytes: Vec<u8>,
1771}
1772
1773/// What a scan's judgements send with: none of it calls Postgres.
1774struct Sender {
1775    /// `None` under `jev.cache_only`.
1776    client: Option<Rc<Client<HttpsTransport, Backend, Telemetry>>>,
1777    budget: Rc<Budget>,
1778    answered: Rc<RefCell<Vec<Receipt>>>,
1779    asking: Rc<Asking>,
1780    counts: Rc<Counts>,
1781}
1782
1783impl Sender {
1784    fn owe(&self, requests: &[Request]) -> Owed {
1785        Owed {
1786            asking: self.asking.clone(),
1787            shared: requests.iter().flat_map(|r| r.keys.values().map(|q| (q.cache_key, q.shared.clone()))).collect(),
1788            failed: false,
1789        }
1790    }
1791
1792    /// Sends `requests` within the budget, serves their waiters, and
1793    /// queues their receipts.
1794    async fn send(&self, requests: &[Request], mut owed: Owed) -> Result<Vec<jev_client::Answered>, Failure> {
1795        let responses = match self.exchange(requests).await {
1796            Ok(responses) => responses,
1797            Err(e) => {
1798                owed.failed = true;
1799                return Err(e);
1800            }
1801        };
1802        for (r, answer) in requests.iter().zip(&responses) {
1803            for Pending { key, cache_key, bytes, shared, .. } in r.keys.values() {
1804                let (judged, stored) = key.read(&answer.response);
1805                shared.resolve(Ok(judged));
1806                self.answered.borrow_mut().push(Receipt {
1807                    key: *cache_key,
1808                    layout: r.placed.layout(),
1809                    state: r.placed.state().as_str().to_owned(),
1810                    question: bytes.clone(),
1811                    answered_model: answer.response.model().to_owned(),
1812                    request_id: answer.request_id.clone(),
1813                    usage: answer.response.usage(),
1814                    answer: stored,
1815                });
1816            }
1817        }
1818        // Answered, so its waiters are served.
1819        drop(owed);
1820        Ok(responses)
1821    }
1822
1823    async fn exchange(&self, requests: &[Request]) -> Result<Vec<jev_client::Answered>, Failure> {
1824        if requests.is_empty() {
1825            return Ok(Vec::new());
1826        }
1827        let Some(client) = &self.client else {
1828            // A miss is refused in lookup; this is a waiter whose request
1829            // was dropped, which under cache_only no scan sends again.
1830            let question = requests[0].keys.values().next().map(|p| p.text.clone()).unwrap_or_default();
1831            return Err(Failure::CacheOnly(question));
1832        };
1833        let chars: usize = requests.iter().map(|r| r.row.len() + r.keys.values().map(|p| p.bytes.len()).sum::<usize>()).sum();
1834        let mut committed = self.budget.commit(requests.len() as u64, chars)?;
1835        let counts = &self.counts;
1836        counts.requests.set(counts.requests.get() + requests.len() as u64);
1837        let responses = join_all(requests.iter().map(|r| client.ask(r.placed.state(), &r.questions)).collect()).await;
1838        let responses =
1839            responses.into_iter().collect::<Result<Vec<_>, _>>().map_err(Failure::Client)?;
1840        committed.settle(&responses);
1841        let tokens: u64 = responses.iter().map(|a| a.response.usage().input_tokens).sum();
1842        counts.input_tokens.set(counts.input_tokens.get() + tokens);
1843        Ok(responses)
1844    }
1845
1846    /// The shared answer; or, when its request was dropped (another
1847    /// scan's rescan, say), this call's own, waiting again on whichever
1848    /// equal call asked first.
1849    async fn wait(&self, mut shared: Rc<Shared>, again: Again) -> Result<Judged, Failure> {
1850        loop {
1851            match shared.wait().await {
1852                Ok(p) => return Ok(p),
1853                Err(Unanswered::Failed) => return Err(Failure::Shared),
1854                Err(Unanswered::Dropped) => {}
1855            }
1856            let asked = self.asking.0.borrow().get(&again.cache_key).cloned();
1857            if let Some(asked) = asked {
1858                shared = asked;
1859                continue;
1860            }
1861            let invalid = |e| Failure::Client(ClientError::Invalid(e));
1862            let mut questions = Questions::new();
1863            let key = AnswerKey::ask(&mut questions, "q", again.question.clone()).map_err(invalid)?;
1864            let mine = Rc::new(Shared::default());
1865            self.asking.0.borrow_mut().insert(again.cache_key, mine.clone());
1866            let pending =
1867                Pending { key, cache_key: again.cache_key, text: again.text.clone(), bytes: again.bytes.clone(), shared: mine };
1868            let request = Request {
1869                row: again.row.clone(),
1870                placed: again.placed.clone(),
1871                questions,
1872                keys: HashMap::from([(again.cache_key, pending)]),
1873            };
1874            let requests = [request];
1875            let owed = self.owe(&requests);
1876            let responses = self.send(&requests, owed).await?;
1877            return Ok(key.read(&responses[0].response).0);
1878        }
1879    }
1880}
1881
1882/// One request: a row and the questions asked about it.
1883struct Request {
1884    row: String,
1885    /// The row's layout and the request's `state`, from the planner.
1886    placed: Placement,
1887    questions: Questions,
1888    /// Per judgment.
1889    keys: HashMap<CacheKey, Pending>,
1890}
1891
1892/// One question of a request.
1893struct Pending {
1894    /// Its key in the response.
1895    key: AnswerKey,
1896    cache_key: CacheKey,
1897    /// The question's text, for errors.
1898    text: String,
1899    /// Its bytes as sent.
1900    bytes: Vec<u8>,
1901    /// The answer its equal calls wait on.
1902    shared: Rc<Shared>,
1903}
1904
1905/// The API key from exactly one source. Both set is an error rather than
1906/// a precedence rule, which would silently bill the wrong account.
1907/// Messages name the sources, never the key.
1908fn api_key() -> Result<String, String> {
1909    let env = std::env::var("TYPESAFE_API_KEY").ok().map(|k| k.trim().to_owned()).filter(|k| !k.is_empty());
1910    match (env, setting(&API_KEY_FILE)) {
1911        (Some(_), Some(_)) => Err(
1912            "the API key is set twice, by TYPESAFE_API_KEY and jev.api_key_file; unset one".into(),
1913        ),
1914        (Some(key), None) => Ok(key),
1915        (None, Some(path)) => {
1916            let contents =
1917                std::fs::read_to_string(&path).map_err(|e| format!("jev.api_key_file {path}: {e}"))?;
1918            let key = contents.trim();
1919            if key.is_empty() {
1920                return Err(format!("jev.api_key_file {path} is empty"));
1921            }
1922            Ok(key.to_owned())
1923        }
1924        (None, None) => Err(
1925            "no API key: set TYPESAFE_API_KEY in the server's environment, or jev.api_key_file".into(),
1926        ),
1927    }
1928}
1929
1930/// A string setting's value; empty means unset, as for Postgres's own
1931/// string GUCs.
1932/// Characters per input token for `jev.model`, learned from what its
1933/// cached answers reported (contract *Execution*): each request's
1934/// characters, its state once and each question it asked, against its
1935/// `usage.input_tokens`. Rows without a request id are their own request.
1936/// The fallback when the model is unset or nothing is cached.
1937fn learned_ratio() -> TokenRatio {
1938    let Some(model) = setting(&MODEL) else { return TokenRatio::FALLBACK };
1939    let samples = as_owner_of(c"jev_cache", || Spi::get_one_with_args::<pgrx::JsonB>(
1940        "SELECT coalesce(jsonb_agg(jsonb_build_array(chars, tokens)), '[]')
1941         FROM (SELECT max(octet_length(state)) + sum(octet_length(question)) AS chars,
1942                      max(input_tokens) AS tokens
1943               FROM jev_cache WHERE model = $1
1944               GROUP BY coalesce(request_id, encode(key, 'hex'))) r",
1945        &[model.into()],
1946    ))
1947    .and_then(|r| r.map_err(|e| e.to_string()));
1948    let Ok(Some(samples)) = samples else { return TokenRatio::FALLBACK };
1949    let samples = serde_json::from_value::<Vec<(u64, u64)>>(samples.0).unwrap_or_default();
1950    TokenRatio::learn(samples.into_iter().map(|(chars, input_tokens)| Sample { chars, input_tokens }))
1951}
1952
1953fn setting(guc: &GucSetting<Option<CString>>) -> Option<String> {
1954    guc.get()
1955        .map(|s| s.to_string_lossy().trim().to_owned())
1956        .filter(|s| !s.is_empty())
1957}