1use std::collections::BTreeMap;
24
25use crate::{
26 Dispatch, MangledName, PackageDeclaration, Param, PropertySig, Span, Type, TypeKind,
27 TypeSymbol, ValueKind, ValueSymbol,
28};
29
30use super::MODULE_NAME;
31
32const LLM_CALL: &str = "call";
35const LLM_BATCH: &str = "batch";
36const LLM_TEXT: &str = "text";
38const LLM_TEXTS: &str = "texts";
39
40const CALL_TYPE_PARAM: &str = "T";
42
43fn call_type_var() -> Type {
45 Type::TypeVar(CALL_TYPE_PARAM.to_string())
46}
47
48pub fn is_checked_call(mangled: &MangledName) -> bool {
57 *mangled == crate::mangle::package_symbol(MODULE_NAME, LLM_CALL)
58}
59
60pub fn is_checked_batch(mangled: &MangledName) -> bool {
62 *mangled == crate::mangle::package_symbol(MODULE_NAME, LLM_BATCH)
63}
64
65pub fn package_declaration() -> PackageDeclaration {
66 let mut defs = PackageDeclaration::with_package(MODULE_NAME);
67 insert_success_interface(&mut defs);
68 insert_failure_interface(&mut defs);
69 insert_completion_alias(&mut defs);
70 insert_model_interface(&mut defs);
71 insert_fn(
72 &mut defs,
73 LLM_TEXT,
74 vec![
75 Param::new("model", Type::String),
76 Param::new("prompt", Type::String),
77 Param::with_default(
78 "schema",
79 Type::Union(vec![Type::String, Type::Undefined]),
80 crate::DefaultValue::Undefined,
81 ),
82 ],
83 completion_type(Type::String),
84 "/**\n * Send one prompt to `model` and return its completion text as a \
85 `Completion<string>`: `{ ok: true, value }` or `{ ok: false, reason, message }`. \
86 Check `ok` before reading `value`; the compiler will not let you read it \
87 otherwise.\n *\n * `schema`, when given, is a JSON Schema sent to the provider \
88 as written, to constrain the output. It must be a JSON object, else a catchable `TypeError`, and it is held to the same \
89 byte bound as a prompt. The answer is not parsed or checked against it: \
90 `value` is the text, and a program that wants a checked value calls \
91 `call<T>` instead, whose schema the compiler emits from `T`.\n *\n * \
92 `ok: false` means the model did not produce a clean completion; branch on \
93 `reason`, which is a closed set, not on `message`. A completion cut off at \
94 the output cap is `reason: \"truncated\"` and still carries the partial \
95 `text`, so a loop that only reads `ok` elements discards usable output.\n *\n \
96 * Throws a `QuotaExceededError` when the execution or server token budget \
97 cannot cover the call, a `RangeError` for prompt bounds, and a catchable \
98 error naming the model when no provider is configured.\n * @param model \
99 Model name. Call `models()` for the ones this runtime serves; an undeclared \
100 name throws.\n * @param prompt The prompt text. Bounded in bytes \
101 independently of the token ceiling.\n * @param schema A JSON Schema for the \
102 provider, sent unchanged; the result stays text.\n * @capability llm.call \
103 { model: $model, prompt_count: 1 }\n */",
104 );
105 insert_fn(
106 &mut defs,
107 LLM_TEXTS,
108 vec![
109 Param::new("model", Type::String),
110 Param::new("prompts", Type::Array(Box::new(Type::String))),
111 Param::with_default(
112 "schema",
113 Type::Union(vec![Type::String, Type::Undefined]),
114 crate::DefaultValue::Undefined,
115 ),
116 ],
117 Type::Array(Box::new(completion_type(Type::String))),
118 "/**\n * Send every prompt to `model` and return one `Completion<string>` each, \
119 positionally: `result[i]` is the text outcome of `prompts[i]`, including when \
120 that element failed. An empty `prompts` returns `[]`.\n *\n * Fan-out \
121 happens host-side at a bounded concurrency, so this is not sugar for a loop. \
122 One failing element never discards the ones that succeeded: each element \
123 carries its own `ok`, `reason` and token counts. `schema`, when given, goes \
124 to the provider with every prompt and never checks an answer; it must be a \
125 JSON object, and it is held to the per-prompt byte \
126 bound. For checked values call `batch<T>`.\n *\n * The whole slice is \
127 checked against the \
128 prompt-count and prompt-size bounds, and its tokens reserved, before any \
129 bytes leave the process.\n * @param model Model name. Call `models()` for \
130 the ones this runtime serves.\n * @param prompts The prompts, in the order \
131 the results come back. Bounded in count, and each in bytes.\n * @param \
132 schema A JSON Schema for the provider, sent unchanged; the results stay \
133 text.\n * @capability llm.call { model: $model, prompt_count: \
134 $prompts.length }\n */",
135 );
136 insert_generic_fn(
137 &mut defs,
138 LLM_CALL,
139 vec![CALL_TYPE_PARAM.to_string()],
140 vec![
141 Param::new("model", Type::String),
142 Param::new("prompt", Type::String),
143 ],
144 completion_type(call_type_var()),
145 "/**\n * Send one prompt to `model` and return its answer checked as a `T`: \
146 `{ ok: true, value }` or `{ ok: false, reason, message }`. Check `ok` before \
147 reading `value`; the compiler will not let you read it otherwise.\n *\n * \
148 `call<Verdict>(model, prompt)` emits a JSON Schema for `Verdict` at compile \
149 time and sends it to the provider; the response is parsed as JSON and \
150 checked field by field, and `value` is the checked `Verdict`. `T` is \
151 required: write it, or let the expected type supply it. For the completion \
152 text call `text(model, prompt)` instead. A response that \
153 is not JSON, or that does not match, is `ok: false` with `reason: \
154 \"invalid-output\"` and a `message` naming the expected type; nothing is \
155 coerced, and no error ever quotes the prompt or the completion.\n *\n * `T` \
156 must be a type the schema can carry: no functions, no \
157 `bigint`, no `Uint8Array`, no `unknown`, and no recursive type — each is \
158 rejected at compile time, naming the offending field. A generic may pass \
159 its own type parameter (`call<T>` inside `function f<T>`): the caller's \
160 type is asked for and checked, and one with no JSON Schema throws a \
161 catchable `TypeError` when the call runs.\n *\n * `ok: false` \
162 means the model did not produce a clean completion; branch on `reason`, \
163 which is a closed set, not on `message`. A completion cut off at the output \
164 cap is `reason: \"truncated\"` and carries no text: a typed `value` could \
165 never hold it.\n *\n * Throws a \
166 `QuotaExceededError` when the execution or server token budget cannot cover \
167 the call, a `RangeError` for prompt bounds, and a catchable error naming the \
168 model when no provider is configured.\n * @param model Model name. Call \
169 `models()` for the ones this runtime serves; an undeclared name throws.\n * \
170 @param prompt The prompt text. Bounded in bytes independently of the token \
171 ceiling.\n * @capability llm.call { model: $model, prompt_count: 1 }\n */",
172 );
173 insert_generic_fn(
174 &mut defs,
175 LLM_BATCH,
176 vec![CALL_TYPE_PARAM.to_string()],
177 vec![
178 Param::new("model", Type::String),
179 Param::new("prompts", Type::Array(Box::new(Type::String))),
180 ],
181 Type::Array(Box::new(completion_type(call_type_var()))),
182 "/**\n * Send every prompt to `model` and return one `Completion` each, \
183 positionally: `result[i]` is the outcome of `prompts[i]`, including when that \
184 element failed. An empty `prompts` returns `[]`.\n *\n * Fan-out happens \
185 host-side at a bounded concurrency, so this is not sugar for a loop — a \
186 sequential version would misrepresent its own cost. One failing element never \
187 discards the ones that succeeded: each element carries its own `ok`, `reason` \
188 and token counts.\n *\n * A type argument names the shape of *one* response: \
189 `batch<Verdict>(model, prompts)` returns `Completion<Verdict>[]`, each element \
190 parsed and checked on its own, so a response that is not JSON or does not \
191 match `Verdict` is `ok: false` with `reason: \"invalid-output\"` for that \
192 element only. `T` is required; for the completion text of each prompt call \
193 `texts(model, prompts)` instead.\n *\n * The whole slice is checked against the prompt-count and \
194 prompt-size bounds, and its tokens reserved, before any bytes leave the \
195 process — so an oversized batch is refused without partially dispatching.\n \
196 * @param model Model name. Call `models()` for the ones this runtime \
197 serves.\n * @param prompts The prompts, in the order the results come back. \
198 Bounded in count, and each in bytes.\n * @capability llm.call { model: \
199 $model, prompt_count: $prompts.length }\n */",
200 );
201 for name in [LLM_CALL, LLM_BATCH] {
202 defs.runtime_generics
203 .insert(crate::mangle::package_symbol(MODULE_NAME, name));
204 }
205 insert_fn(
206 &mut defs,
207 "models",
208 Vec::new(),
209 Type::Array(Box::new(model_type())),
210 "/**\n * The models this runtime serves and this caller may call.\n *\n * \
211 Each candidate is filtered under `llm.call` by the same `model` \
212 filter that gates calling — so a listing never offers a model the caller \
213 would be denied at `call` time. The list can therefore come back short, or \
214 empty, and nothing in it reveals how many candidates were filtered out.\n \
215 *\n * Choosing a model is the delegation decision — a cheap model for a \
216 thousand classifications, a strong one for the synthesis — so branch on \
217 `contextWindow`, which is a fact you can compute against. Both \
218 `contextWindow` and `description` are absent when the operator declared \
219 none; a program filtering on `contextWindow` drops those, which is correct \
220 for a chunk-size decision, because you should not size against a number \
221 nobody asserted.\n *\n * Treat `description` as advice, not fact: it is \
222 operator-authored free text that steers which model your program calls.\n * \
223 @capability llm.call { prompt_count: 0 }\n */",
224 );
225 defs
226}
227
228fn package() -> crate::Package {
229 crate::Package(MODULE_NAME.to_string())
230}
231
232fn completion_type(value: Type) -> Type {
235 Type::AliasRef {
236 mangled: crate::mangle::package_symbol(MODULE_NAME, "Completion"),
237 package: package(),
238 name: "Completion".to_string(),
239 args: vec![value],
240 }
241}
242
243fn success_type(value: Type) -> Type {
244 Type::InterfaceRef {
245 mangled: crate::mangle::package_symbol(MODULE_NAME, "Success"),
246 package: package(),
247 name: "Success".to_string(),
248 args: vec![value],
249 }
250}
251
252fn failure_type() -> Type {
253 Type::InterfaceRef {
254 mangled: crate::mangle::package_symbol(MODULE_NAME, "Failure"),
255 package: package(),
256 name: "Failure".to_string(),
257 args: Vec::new(),
258 }
259}
260
261fn model_type() -> Type {
262 Type::InterfaceRef {
263 mangled: crate::mangle::package_symbol(MODULE_NAME, "Model"),
264 package: package(),
265 name: "Model".to_string(),
266 args: Vec::new(),
267 }
268}
269
270fn reason_type() -> Type {
273 Type::union(
274 crate::runtime::llm::FailureReason::ALL
275 .iter()
276 .map(|reason| Type::StringLiteral(reason.as_str().to_string()))
277 .collect(),
278 )
279}
280
281fn insert_success_interface(defs: &mut PackageDeclaration) {
282 let mut properties = BTreeMap::new();
283 insert_property(
284 &mut properties,
285 "ok",
286 Type::BooleanLiteral(true),
287 "/** `true`: the model stopped naturally and `value` holds its answer. */",
288 );
289 insert_property(
290 &mut properties,
291 "value",
292 call_type_var(),
293 "/** The completion text from `text`/`texts`, or from `call<T>`/`batch<T>` the parsed response, checked against `T` field by field. Only what passed the check is ever stored here. */",
294 );
295 insert_optional_property(
296 &mut properties,
297 "inputTokens",
298 Type::Number,
299 "/** Prompt tokens the provider reported. Absent means indeterminate, not free: a throttled call may still have been billed, so it is never `0` as a stand-in for unknown. */",
300 );
301 insert_optional_property(
302 &mut properties,
303 "outputTokens",
304 Type::Number,
305 "/** Completion tokens the provider reported, with the same absent-means-indeterminate rule as `inputTokens`. */",
306 );
307 insert_interface(
308 defs,
309 "Success",
310 vec![CALL_TYPE_PARAM.to_string()],
311 properties,
312 "/** The `ok: true` arm of a `Completion<T>`: the model stopped naturally and `value` is its answer — the text, or the checked `T`. */",
313 );
314}
315
316fn insert_failure_interface(defs: &mut PackageDeclaration) {
317 let mut properties = BTreeMap::new();
318 insert_property(
319 &mut properties,
320 "ok",
321 Type::BooleanLiteral(false),
322 "/** `false`: this element is not a clean completion. Not \"something threw\": a truncated or content-filtered completion is `ok: false` and may still carry `text`. */",
323 );
324 insert_property(
325 &mut properties,
326 "reason",
327 reason_type(),
328 "/** Why this element is not a clean completion. One of `\"truncated\"`, `\"content-filtered\"`, `\"invalid-output\"`, `\"rate-limited\"`, `\"request-rejected\"`, `\"provider-unavailable\"`, `\"transport\"`, `\"cancelled\"`, `\"incomplete\"` — branch on this, not on `message`. A typed call's response that is not JSON or does not match `T` is `\"invalid-output\"`. */",
329 );
330 insert_property(
331 &mut properties,
332 "message",
333 Type::String,
334 "/** A fixed classification of the failure, or for `\"invalid-output\"` the type the response did not match. Never the prompt, the completion, or a provider response body. */",
335 );
336 insert_property(
337 &mut properties,
338 "retryable",
339 Type::Boolean,
340 "/** Whether re-sending this identical prompt could plausibly succeed. */",
341 );
342 insert_optional_property(
343 &mut properties,
344 "status",
345 Type::Number,
346 "/** The HTTP status the provider answered with, absent when none was observed — which is what distinguishes a dead connection from a provider that answered with an error. */",
347 );
348 insert_optional_property(
349 &mut properties,
350 "finishReason",
351 Type::String,
352 "/** The provider's own raw stop reason, for diagnosis only. Varies per provider and per SDK version — branch on `reason` instead. */",
353 );
354 insert_optional_property(
355 &mut properties,
356 "text",
357 Type::String,
358 "/** The partial text of a truncated or filtered completion, absent when the model produced none at all. Usable output: a loop that skips every `!ok` element discards it. Only `text` and `texts` carry it: a checked `call<T>` never does. */",
359 );
360 insert_optional_property(
361 &mut properties,
362 "inputTokens",
363 Type::Number,
364 "/** Prompt tokens the provider reported for this element, absent when indeterminate. A failed element may still have been billed. */",
365 );
366 insert_optional_property(
367 &mut properties,
368 "outputTokens",
369 Type::Number,
370 "/** Completion tokens the provider reported for this element, absent when indeterminate. */",
371 );
372 insert_interface(
373 defs,
374 "Failure",
375 Vec::new(),
376 properties,
377 "/** The `ok: false` arm of a `Completion<T>`: why this element is not a clean completion, and the partial `text` when the model produced some. */",
378 );
379}
380
381fn insert_completion_alias(defs: &mut PackageDeclaration) {
382 defs.types.insert(
383 "Completion".to_string(),
384 TypeSymbol {
385 name: "Completion".to_string(),
386 mangled_name: crate::mangle::package_symbol(MODULE_NAME, "Completion"),
387 declaration_span: Span::at(crate::FileId::LLM),
388 kind: TypeKind::Alias {
389 generics: crate::GenericParameters::unconstrained(vec![CALL_TYPE_PARAM.to_string()]),
390 ty: Type::union(vec![success_type(call_type_var()), failure_type()]),
391 doc: crate::doc(
392 crate::FileId::LLM,
393 "/** One prompt's outcome: `Success<T>` when the model stopped naturally, `Failure` otherwise. Narrow on `ok` before reading `value`. `T` is `string` for `text` and `texts`. Constructed only by this package's calls, but a plain object of the same shape is one too. */",
394 ),
395 },
396 },
397 );
398}
399
400fn insert_model_interface(defs: &mut PackageDeclaration) {
401 let mut properties = BTreeMap::new();
402 insert_property(
403 &mut properties,
404 "name",
405 Type::String,
406 "/** The model name, exactly as `call` and `batch` expect it. */",
407 );
408 insert_optional_property(
409 &mut properties,
410 "description",
411 Type::String,
412 "/** Operator-authored deployment intent, absent when none was declared. Advice, not fact — it is free text that steers model selection, so weigh it after `contextWindow`. Always a single line, within a fixed length bound. */",
413 );
414 insert_optional_property(
415 &mut properties,
416 "contextWindow",
417 Type::Number,
418 "/** The model's context window in tokens, absent when the operator declared none. Absent means unknown, not zero, and there is no fallback table — a program sizing chunks should drop a model rather than guess. */",
419 );
420 insert_interface(
421 defs,
422 "Model",
423 Vec::new(),
424 properties,
425 "/** One model this caller may call. Constructed only by `models()`, and the list carries nothing — no index, no position, no count — derived from the candidates the policy filtered out. */",
426 );
427}
428
429fn insert_interface(
433 defs: &mut PackageDeclaration,
434 name: &str,
435 generics: Vec<String>,
436 properties: BTreeMap<String, PropertySig>,
437 doc: &str,
438) {
439 defs.types.insert(
440 name.to_string(),
441 TypeSymbol {
442 name: name.to_string(),
443 mangled_name: crate::mangle::package_symbol(MODULE_NAME, name),
444 declaration_span: Span::at(crate::FileId::LLM),
445 kind: TypeKind::Interface {
446 index: None,
447 generics: crate::GenericParameters::unconstrained(generics),
448 methods: BTreeMap::new(),
449 properties,
450 dispatch: Dispatch::VTable,
451 doc: crate::doc(crate::FileId::LLM, doc),
452 },
453 },
454 );
455}
456
457fn insert_property(
458 properties: &mut BTreeMap<String, PropertySig>,
459 name: &str,
460 ty: Type,
461 doc: &str,
462) {
463 properties.insert(
464 name.to_string(),
465 PropertySig {
466 ty,
467 readonly: true,
468 optional: false,
469 intrinsic: false,
470 doc: crate::doc(crate::FileId::LLM, doc),
471 },
472 );
473}
474
475fn insert_optional_property(
478 properties: &mut BTreeMap<String, PropertySig>,
479 name: &str,
480 ty: Type,
481 doc: &str,
482) {
483 properties.insert(
484 name.to_string(),
485 PropertySig {
486 ty,
487 readonly: true,
488 optional: true,
489 intrinsic: false,
490 doc: crate::doc(crate::FileId::LLM, doc),
491 },
492 );
493}
494
495fn insert_fn(defs: &mut PackageDeclaration, name: &str, params: Vec<Param>, ret: Type, doc: &str) {
496 insert_generic_fn(defs, name, Vec::new(), params, ret, doc);
497}
498
499fn insert_generic_fn(
500 defs: &mut PackageDeclaration,
501 name: &str,
502 generics: Vec<String>,
503 params: Vec<Param>,
504 ret: Type,
505 doc: &str,
506) {
507 defs.values.insert(
508 name.to_string(),
509 ValueSymbol {
510 name: name.to_string(),
511 mangled_name: crate::mangle::package_symbol(MODULE_NAME, name),
512 declaration_span: Span::at(crate::FileId::LLM),
513 kind: ValueKind::Function {
514 generics: crate::GenericParameters::unconstrained(generics),
515 params,
516 ret,
517 type_predicate: None,
518 doc: crate::doc(crate::FileId::LLM, doc),
519 },
520 },
521 );
522}