1use std::collections::BTreeMap;
31
32use crate::{
33 Dispatch, PackageDeclaration, Param, PropertySig, Span, Type, TypeKind, TypeSymbol, ValueKind,
34 ValueSymbol,
35};
36
37use super::MODULE_NAME;
38
39const LLM_CALL: &str = "call";
43const LLM_BATCH: &str = "batch";
44
45const CALL_TYPE_PARAM: &str = "T";
47
48fn call_type_var() -> Type {
50 Type::TypeVar(CALL_TYPE_PARAM.to_string())
51}
52
53pub fn is_checked_batch(mangled: &crate::MangledName) -> bool {
58 *mangled == crate::mangle::package_symbol(MODULE_NAME, LLM_BATCH)
59}
60
61pub fn untyped_result_type(mangled: &crate::MangledName) -> Type {
67 if is_checked_batch(mangled) {
68 Type::Array(Box::new(completion_type()))
69 } else {
70 completion_type()
71 }
72}
73
74pub fn is_checked_call(mangled: &crate::MangledName) -> bool {
81 *mangled == crate::mangle::package_symbol(MODULE_NAME, LLM_CALL)
82 || *mangled == crate::mangle::package_symbol(MODULE_NAME, LLM_BATCH)
83}
84
85pub fn package_declaration() -> PackageDeclaration {
86 let mut defs = PackageDeclaration::with_package(MODULE_NAME);
87 insert_completion_interface(&mut defs);
88 insert_model_interface(&mut defs);
89 insert_generic_fn(
90 &mut defs,
91 LLM_CALL,
92 vec![CALL_TYPE_PARAM.to_string()],
93 vec![
94 Param::new("model", Type::String),
95 Param::new("prompt", Type::String),
96 schema_param(),
97 ],
98 call_type_var(),
99 "/**\n * Send one prompt to `model` and return its completion.\n *\n * With a \
100 type argument — `call<Severity>(model, prompt)` — a JSON Schema for `T` is \
101 emitted at compile time and sent to the provider, and the response is then \
102 checked structurally against `T` field by field. A response that does not \
103 match throws a catchable `TypeError`; nothing is coerced. The schema is \
104 advisory and the check is not, so a provider that ignores the schema and \
105 answers in prose throws rather than returning a wrongly-typed value. The \
106 typed form returns `T` itself, trading the `Completion` envelope for the \
107 checked value, exactly as `session.get<T>` does.\n *\n * `T` must be a type \
108 the schema can carry, which is narrower than what `as` can check: no \
109 functions, no `bigint`, no `Uint8Array`, no `unknown`, and no recursive type \
110 — each is rejected at compile time, naming the offending field.\n *\n * Check \
111 `ok` before reading `text`: `ok` means the model stopped naturally, not \
112 merely that nothing threw. A completion cut off at the output cap is `ok: \
113 false` with `reason` `\"truncated\"` and still carries the partial `text`, \
114 so a loop that skips every `!ok` element discards usable output.\n *\n * \
115 That envelope is what the typed form gives up, so a typed call on a \
116 failed completion throws a `TypeError` naming the reason and pointing \
117 back at the untyped form — there is no `ok` left to branch on, and the \
118 partial `text` is reachable only without a type argument.\n *\n * \
119 Throws a `QuotaExceededError` when the \
120 execution or server token budget cannot cover the call, a `RangeError` for prompt bounds, and a \
121 catchable error naming the model when no provider is configured. No error \
122 ever quotes the prompt or the completion.\n * @param model Model name. Call \
123 `models()` for the ones this runtime serves; an undeclared name throws.\n * \
124 @param prompt The prompt text. Bounded in bytes independently of the token \
125 ceiling.\n * @capability llm.call { model: $model, \
126 prompt_count: 1 }\n */",
127 );
128 insert_generic_fn(
129 &mut defs,
130 LLM_BATCH,
131 vec![CALL_TYPE_PARAM.to_string()],
132 vec![
133 Param::new("model", Type::String),
134 Param::new("prompts", Type::Array(Box::new(Type::String))),
135 schema_param(),
136 ],
137 call_type_var(),
138 "/**\n * Send every prompt to `model` and return one completion each, \
139 positionally: `result[i]` is the outcome of `prompts[i]`, including when \
140 that element failed.\n *\n * Fan-out happens host-side at a bounded \
141 concurrency, so this is not sugar for a loop — a sequential version would \
142 misrepresent its own cost. One failing element never discards the ones that \
143 succeeded: each element carries its own `ok`, `reason`, and token counts.\n \
144 *\n * The whole slice is checked against the prompt-count and prompt-size \
145 bounds, and its tokens reserved, before any bytes leave the process — so an \
146 oversized batch is refused without partially dispatching.\n *\n * A type \
147 argument names the shape of the *whole result*, not of one element, so the \
148 typed form is `batch<Severity[]>(model, prompts)`. Every element is then \
149 checked structurally, and one non-conforming response throws for the batch \
150 rather than yielding a wrongly-typed element — which is why the untyped form, \
151 whose per-element `ok` survives a partial failure, is the right one when some \
152 prompts are expected to fail.\n * @param model Model name. \
153 Call `models()` for the ones this runtime serves.\n * @param prompts The \
154 prompts, in the order the results come back. Bounded in count, and each in \
155 bytes.\n * @capability llm.call { model: $model, \
156 prompt_count: $prompts.length }\n */",
157 );
158 insert_fn(
159 &mut defs,
160 "models",
161 Vec::new(),
162 Type::Array(Box::new(model_type())),
163 "/**\n * The models this runtime serves and this caller may call.\n *\n * \
164 Each candidate is filtered under `llm.call` by the same `model` \
165 filter that gates calling — so a listing never offers a model the caller \
166 would be denied at `call` time. The list can therefore come back short, or \
167 empty, and nothing in it reveals how many candidates were filtered out.\n \
168 *\n * Choosing a model is the delegation decision — a cheap model for a \
169 thousand classifications, a strong one for the synthesis — so branch on \
170 `contextWindow`, which is a fact you can compute against. Both \
171 `contextWindow` and `description` are `undefined` when the operator declared \
172 none; a program filtering on `contextWindow` drops those, which is correct \
173 for a chunk-size decision, because you should not size against a number \
174 nobody asserted.\n *\n * Treat `description` as advice, not fact: it is \
175 operator-authored free text that steers which model your program calls.\n * \
176 @capability llm.call { prompt_count: 0 }\n */",
177 );
178 defs
179}
180
181fn schema_param() -> Param {
185 Param::with_default(
186 "schema",
187 Type::union(vec![Type::String, Type::Undefined]),
188 crate::DefaultValue::Undefined,
189 )
190}
191
192fn completion_type() -> Type {
193 Type::InterfaceRef {
194 mangled: crate::mangle::package_symbol(MODULE_NAME, "Completion"),
195 package: crate::Package(MODULE_NAME.to_string()),
196 name: "Completion".to_string(),
197 args: Vec::new(),
198 }
199}
200
201fn model_type() -> Type {
202 Type::InterfaceRef {
203 mangled: crate::mangle::package_symbol(MODULE_NAME, "Model"),
204 package: crate::Package(MODULE_NAME.to_string()),
205 name: "Model".to_string(),
206 args: Vec::new(),
207 }
208}
209
210fn optional_string() -> Type {
211 Type::union(vec![Type::String, Type::Undefined])
212}
213
214fn optional_number() -> Type {
215 Type::union(vec![Type::Number, Type::Undefined])
216}
217
218fn insert_completion_interface(defs: &mut PackageDeclaration) {
219 let mut properties = BTreeMap::new();
220 insert_property(
221 &mut properties,
222 "ok",
223 Type::Boolean,
224 "/** Whether the model stopped naturally. Not \"nothing threw\": a truncated or content-filtered completion is `false` and still carries `text`. */",
225 );
226 insert_property(
227 &mut properties,
228 "text",
229 optional_string(),
230 "/** The completion text. Always a string when `ok`; on the failure arm `undefined` only when the model produced none at all, so `\"\"` and `undefined` mean different things. */",
231 );
232 insert_property(
233 &mut properties,
234 "reason",
235 optional_string(),
236 "/** Why this element is not a clean completion, `undefined` when `ok`. One of `\"truncated\"`, `\"content-filtered\"`, `\"invalid-output\"`, `\"rate-limited\"`, `\"request-rejected\"`, `\"provider-unavailable\"`, `\"transport\"`, `\"cancelled\"`, `\"incomplete\"` — branch on this, not on `message`. */",
237 );
238 insert_property(
239 &mut properties,
240 "message",
241 optional_string(),
242 "/** A fixed classification of the failure, `undefined` when `ok`. Never the prompt, the completion, or a provider response body. */",
243 );
244 insert_property(
245 &mut properties,
246 "retryable",
247 Type::Boolean,
248 "/** Whether re-sending this identical prompt could plausibly succeed. `false` on the success arm, where there is nothing to retry. */",
249 );
250 insert_property(
251 &mut properties,
252 "status",
253 optional_number(),
254 "/** The HTTP status the provider answered with, `undefined` when none was observed — which is what distinguishes a dead connection from a provider that answered with an error. */",
255 );
256 insert_property(
257 &mut properties,
258 "finishReason",
259 optional_string(),
260 "/** The provider's own raw stop reason, for diagnosis only. Varies per provider and per SDK version — branch on `reason` instead. */",
261 );
262 insert_property(
263 &mut properties,
264 "inputTokens",
265 optional_number(),
266 "/** Prompt tokens the provider reported. `undefined` means indeterminate, not free: a throttled call may still have been billed, so it is never `0` as a stand-in for unknown. */",
267 );
268 insert_property(
269 &mut properties,
270 "outputTokens",
271 optional_number(),
272 "/** Completion tokens the provider reported, with the same `undefined`-means-indeterminate rule as `inputTokens`. */",
273 );
274 insert_interface(
275 defs,
276 "Completion",
277 properties,
278 "/** One prompt's outcome, positionally matched to the prompt that produced it. Success and failure are one shape rather than two, because a truncated or filtered completion is `ok: false` and still carries usable `text`. Constructed only by `call` and `batch`. */",
279 );
280}
281
282fn insert_model_interface(defs: &mut PackageDeclaration) {
283 let mut properties = BTreeMap::new();
284 insert_property(
285 &mut properties,
286 "name",
287 Type::String,
288 "/** The model name, exactly as `call` and `batch` expect it. */",
289 );
290 insert_property(
291 &mut properties,
292 "description",
293 optional_string(),
294 "/** Operator-authored deployment intent, or `undefined` 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. */",
295 );
296 insert_property(
297 &mut properties,
298 "contextWindow",
299 optional_number(),
300 "/** The model's context window in tokens, or `undefined` 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. */",
301 );
302 insert_interface(
303 defs,
304 "Model",
305 properties,
306 "/** 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. */",
307 );
308}
309
310fn insert_interface(
311 defs: &mut PackageDeclaration,
312 name: &str,
313 properties: BTreeMap<String, PropertySig>,
314 doc: &str,
315) {
316 defs.types.insert(
317 name.to_string(),
318 TypeSymbol {
319 name: name.to_string(),
320 mangled_name: crate::mangle::package_symbol(MODULE_NAME, name),
321 declaration_span: Span::at(crate::FileId::LLM),
322 kind: TypeKind::Interface {
323 index: None,
324 generics: Vec::new(),
325 methods: BTreeMap::new(),
326 properties,
327 dispatch: Dispatch::Direct,
328 doc: crate::doc(crate::FileId::LLM, doc),
329 },
330 },
331 );
332}
333
334fn insert_property(
335 properties: &mut BTreeMap<String, PropertySig>,
336 name: &str,
337 ty: Type,
338 doc: &str,
339) {
340 properties.insert(
341 name.to_string(),
342 PropertySig {
343 ty,
344 readonly: true,
345 optional: false,
346 intrinsic: false,
347 doc: crate::doc(crate::FileId::LLM, doc),
348 },
349 );
350}
351
352fn insert_fn(defs: &mut PackageDeclaration, name: &str, params: Vec<Param>, ret: Type, doc: &str) {
353 insert_generic_fn(defs, name, Vec::new(), params, ret, doc);
354}
355
356fn insert_generic_fn(
357 defs: &mut PackageDeclaration,
358 name: &str,
359 generics: Vec<String>,
360 params: Vec<Param>,
361 ret: Type,
362 doc: &str,
363) {
364 defs.values.insert(
365 name.to_string(),
366 ValueSymbol {
367 name: name.to_string(),
368 mangled_name: crate::mangle::package_symbol(MODULE_NAME, name),
369 declaration_span: Span::at(crate::FileId::LLM),
370 kind: ValueKind::Function {
371 generics,
372 params,
373 ret,
374 type_predicate: None,
375 doc: crate::doc(crate::FileId::LLM, doc),
376 },
377 },
378 );
379}