// @harn-entrypoint-category llm.stdlib
//
// std/llm/defaults — task-pinned, provider/lineage-aware option packs that
// produce a complete `llm_call`-ready dict by layering:
//
// 1. resolved_options(opts) (runtime catalog defaults)
// 2. lineage + scale patch (quality/latency calibration)
// 3. lineage + policy patch (provider-neutral reasoning intent;
// skipped when `thinking` or `effort`
// pins a low-level setting)
// 4. task overlay (only fills unset fields)
// 5. recommend_max_output_tokens() (only when prompt is provided and
// neither user nor scale calibration
// already set max_tokens)
// 6. user opts (highest precedence — wins)
//
// User opts always win. Example: pack_for({reasoning_task: "judge", temperature: 0.42})
// returns temperature == 0.42 (overrides judge's 0.0 task default).
//
// Calibration sources (each table cites its source above the lookup fn):
// - Anthropic extended thinking budgets:
// <https://platform.claude.com/docs/en/build-with-claude/extended-thinking>
// - Anthropic Opus 4.7 adaptive thinking (manual budget returns 400):
// <https://platform.claude.com/docs/en/about-claude/models/whats-new-claude-4-7>
// - OpenAI reasoning_effort levels:
// <https://developers.openai.com/api/docs/guides/reasoning>
// - OpenAI GPT-5.6 ("none"-allowed, no "minimal", through "max"):
// <https://developers.openai.com/api/docs/models/gpt-5.6-sol>
// - Gemini thinkingBudget (0=off, -1=dynamic, max 24576 Flash / 32768 Pro):
// <https://ai.google.dev/gemini-api/docs/thinking>
// - Ollama num_predict default 128:
// <https://docs.ollama.com/modelfile>
// - Per-task temperature/top_p/output defaults: tunable; not a
// published recommendation. Match commonly-cited cookbook values.
import { agent_emit_event } from "std/agent/state"
import { recommend_max_output_tokens } from "std/llm/budget"
import { lineage_of, resolved_options } from "std/llm/catalog"
fn __has_key(d, key) {
if type_of(d) != "dict" {
return false
}
return contains(d.keys(), key)
}
// -------------------------------------------------------------------------------------------------
// reasoning-policy patches
// -------------------------------------------------------------------------------------------------
/**
* Map a provider-neutral reasoning policy to a lineage-specific patch.
* An empty dict means "omit the low-level reasoning knob entirely".
*
* - Anthropic Sonnet/Opus (pre-4.7): extended thinking budgets per
* <https://platform.claude.com/docs/en/build-with-claude/extended-thinking>
* - Anthropic Opus 4.7+: adaptive thinking; manual budgets return 400 —
* strip the field. See __maybe_emit_strip for the warn event.
* - Anthropic Haiku 4.x: no extended-thinking support; omit.
* - OpenAI GPT-5: reasoning_effort levels per
* <https://developers.openai.com/api/docs/guides/reasoning>; "off" maps to
* "none" for GPT-5.6 and "minimal" for older GPT-5 models.
* - OpenAI legacy GPT-4o/4.1: omit.
* - Gemini 2.5 Flash max thinkingBudget = 24576; Pro max = 32768; Harn
* lowers typed `thinking` to native generationConfig.thinkingConfig.
* "auto" maps to adaptive/dynamic thinking per
* <https://ai.google.dev/gemini-api/docs/thinking>).
* - Ollama qwen3: host injects /no_think capability-side; don't
* duplicate. All other Ollama: omit.
*/
fn __reasoning_policy_patch(lineage, model, policy) {
const thinking = policy
if lineage == "claude-sonnet-opus" {
if thinking == "off" {
return {}
}
if thinking == "low" {
return {thinking: {enabled: true, budget_tokens: 1024}}
}
if thinking == "high" {
return {thinking: {enabled: true, budget_tokens: 16000}}
}
// medium and auto both map to a moderate budget; non-adaptive families
// can't honor "auto" the way Opus 4.7 can, so we treat auto≡medium.
return {thinking: {enabled: true, budget_tokens: 4096}}
}
if lineage == "claude-opus-adaptive" {
// Opus 4.7+ rejects manual `thinking` budgets — always omit.
return {}
}
if lineage == "claude-haiku" {
return {}
}
if lineage == "openai-gpt5" {
if thinking == "off" {
if model.starts_with("gpt-5.6") {
return {effort: "none"}
}
return {effort: "minimal"}
}
if thinking == "low" {
return {effort: "low"}
}
if thinking == "high" {
return {effort: "high"}
}
return {effort: "medium"}
}
if lineage == "openai-legacy" {
return {}
}
if lineage == "gemini-pro" {
if thinking == "off" {
return {thinking: {mode: "disabled"}}
}
if thinking == "low" {
return {thinking: {mode: "enabled", budget_tokens: 1024}}
}
if thinking == "high" {
return {thinking: {mode: "enabled", budget_tokens: 16384}}
}
if thinking == "auto" {
return {thinking: {mode: "adaptive"}}
}
// medium.
return {thinking: {mode: "enabled", budget_tokens: 8192}}
}
if lineage == "gemini-flash" {
if thinking == "off" {
return {thinking: {mode: "disabled"}}
}
if thinking == "low" {
return {thinking: {mode: "enabled", budget_tokens: 1024}}
}
if thinking == "high" {
// Flash max budget 24576.
return {thinking: {mode: "enabled", budget_tokens: 24576}}
}
if thinking == "auto" {
return {thinking: {mode: "adaptive"}}
}
return {thinking: {mode: "enabled", budget_tokens: 8192}}
}
// qwen3, gemma4, and generic lineages have no thinking knob to set here;
// the host's capability-driven /no_think directive (Qwen3) handles "off".
// Future Gemini lineages should branch here when their thinking controls
// diverge from the current Pro/Flash shapes.
return {}
}
// -------------------------------------------------------------------------------------------------
// reasoning-scale patches
// -------------------------------------------------------------------------------------------------
//
// reasoning_scale ∈ {"small","medium","large","auto"}. The three
// sizes map to the old fast/balanced/quality calibration internally.
//
// Anthropic temperatures sweep 0.2 / 0.7 / 1.0 (cookbook values).
// max_tokens caps reflect typical Claude messages-API budgets:
// Sonnet/Opus 1024/4096/8192; Haiku is capped lower (1024/2048/4096).
// OpenAI GPT-5 lineage piggybacks on reasoning_effort; GPT-4o/4.1 use
// temperature only (their max_tokens default flows from the catalog).
// Gemini scale calibration maps to typed thinking steps, which the native
// provider lowers to generationConfig.thinkingConfig.
// Ollama exposes num_predict (default 128 per Modelfile reference) so
// we override it for "balanced" / "quality" to give meaningful output.
@complexity(allow)
fn __reasoning_scale_patch(lineage, scale) {
const kind = if scale == "small" {
"fast"
} else if scale == "large" {
"quality"
} else {
"balanced"
}
if lineage == "claude-sonnet-opus" {
if kind == "fast" {
return {temperature: 0.2, max_tokens: 1024}
}
if kind == "quality" {
// Quality bumps the thinking budget to medium (4096) implicitly via
// the layered thinking patch — we don't double-set it here.
return {temperature: 1.0, max_tokens: 8192}
}
return {temperature: 0.7, max_tokens: 4096}
}
if lineage == "claude-opus-adaptive" {
if kind == "fast" {
return {temperature: 0.2, max_tokens: 1024}
}
if kind == "quality" {
// Opus 4.7+ does its own adaptive thinking; no manual thinking knob.
return {temperature: 1.0, max_tokens: 8192}
}
return {temperature: 0.7, max_tokens: 4096}
}
if lineage == "claude-haiku" {
if kind == "fast" {
return {temperature: 0.2, max_tokens: 1024}
}
if kind == "quality" {
return {temperature: 1.0, max_tokens: 4096}
}
return {temperature: 0.7, max_tokens: 2048}
}
if lineage == "openai-gpt5" {
if kind == "fast" || kind == "low" {
return {effort: "low"}
}
if kind == "quality" || kind == "high" {
return {effort: "high"}
}
return {effort: "medium"}
}
if lineage == "openai-legacy" {
if kind == "fast" {
return {temperature: 0.2}
}
if kind == "quality" {
return {temperature: 1.0, max_tokens: 8192}
}
return {temperature: 0.7}
}
if lineage == "gemini-pro" {
if kind == "fast" {
return {thinking: {mode: "disabled"}}
}
if kind == "quality" {
return {thinking: {mode: "enabled", budget_tokens: 16384}}
}
return {thinking: {mode: "enabled", budget_tokens: 4096}}
}
if lineage == "gemini-flash" {
if kind == "fast" {
return {thinking: {mode: "disabled"}}
}
if kind == "quality" {
return {thinking: {mode: "enabled", budget_tokens: 16384}}
}
return {thinking: {mode: "enabled", budget_tokens: 2048}}
}
if lineage == "qwen3" || lineage == "gemma4" {
if kind == "fast" {
return {provider_options: {ollama: {num_predict: 512}}}
}
if kind == "quality" {
return {provider_options: {ollama: {num_predict: 4096}}}
}
return {provider_options: {ollama: {num_predict: 2048}}}
}
// generic
return {}
}
// -------------------------------------------------------------------------------------------------
// task overlay
// -------------------------------------------------------------------------------------------------
/**
* Per-task defaults; tunable, NOT a published vendor recommendation.
* Only fills fields that effort/thinking layers haven't already set.
* task ∈ {"chat","agent","refine","judge","summarize","code","json"}.
*/
fn __task_overlay(task) {
if task == "chat" {
return {temperature: 0.7, top_p: 0.95, schema_retries: 0, output: "text"}
}
if task == "agent" {
return {temperature: 0.5, top_p: 0.95, schema_retries: 0, output: "text"}
}
if task == "refine" {
return {temperature: 0.4, top_p: 0.9, schema_retries: 1, output: "text"}
}
if task == "judge" {
return {temperature: 0.0, top_p: 1.0, schema_retries: 2, output: "json"}
}
if task == "summarize" {
return {temperature: 0.3, top_p: 0.9, schema_retries: 0, output: "text"}
}
if task == "code" {
return {temperature: 0.2, top_p: 0.95, schema_retries: 0, output: "text"}
}
if task == "json" {
return {temperature: 0.1, top_p: 1.0, schema_retries: 2, output: "json"}
}
// unknown task → no overlay
return {}
}
// -------------------------------------------------------------------------------------------------
// helpers
// -------------------------------------------------------------------------------------------------
fn __safe_resolved_options(opts) {
// Fall back to a minimal echo dict if resolved_options throws (e.g. when
// opts.model is missing). pack_for already requires opts.model, so this
// is defensive — never hit in normal paths.
const r = try {
resolved_options(opts)
}
if is_err(r) {
return {model: opts?.model ?? "", provider: opts?.provider ?? ""}
}
return unwrap(r)
}
fn __maybe_emit_strip(opts, requested) {
// Best-effort warn when manual thinking is stripped on adaptive Opus. If no
// session_id is in opts (the usual case for pack_for), skip silently.
// This mirrors the "emit when bound; punt otherwise" pattern from
// std/llm/budget.
const sid = opts?.session_id ?? opts?._session_id
if sid == nil || sid == "" {
return
}
try {
agent_emit_event(
sid,
"pack_thinking_stripped",
{model: opts?.model, requested: requested, reason: "claude_opus_adaptive"},
)
}
}
fn __fill_unset(result, overlay) {
// task_overlay's "fill only when unset" semantics. Iterate overlay keys,
// assign only those missing from result.
let out = result
for key in overlay.keys() {
if !__has_key(out, key) {
out[key] = overlay[key]
}
}
return out
}
// -------------------------------------------------------------------------------------------------
// public API
// -------------------------------------------------------------------------------------------------
/**
* pack_for(opts) -> dict
*
* Returns an `llm_call`-ready options dict, calibrated for the model's
* provider/lineage and pinned to a task. User opts always win.
*
* Required: opts.model
* Optional: every canonical `llm_call` option, including `reasoning_policy`,
* `reasoning_scale`, `reasoning_task`, `thinking`, and `effort`, plus
* opts.tool_format, opts.schema_retries, opts.session_id
*
* Example: pack_for({model: "claude-sonnet-4-5", reasoning_task: "judge", temperature: 0.42})
* → result has temperature == 0.42 (user override wins over judge's 0.0
* default).
*
* Side effect: when a knob conflicts with a known model constraint
* (e.g. manual thinking on Opus 4.7), may emit an agent event tagged
* "pack_thinking_stripped" if a session_id is present in opts.
*
* @effects: []
* @errors: []
*/
pub fn pack_for(opts) {
if type_of(opts) != "dict" {
throw "pack_for: opts must be a dict"
}
if opts?.model == nil || opts.model == "" {
throw "pack_for: opts.model is required"
}
const model = opts.model
// 1. Runtime catalog defaults. Pass only model+provider so unrelated
// user-supplied policy and low-level reasoning keys don't leak into the
// resolved dict prematurely.
const resolved_input = if opts?.provider != nil {
{model: model, provider: opts.provider}
} else {
{model: model}
}
let result = __safe_resolved_options(resolved_input)
// Lineage classification uses the inferred provider in the catalog.
const lineage = lineage_of(model)
// 2. Scale calibration. `reasoning_scale` is the policy input; `effort` is
// a canonical low-level request setting and is never consumed here.
const caller_pinned_reasoning = __has_key(opts, "thinking") || __has_key(opts, "effort")
const scale = opts?.reasoning_scale ?? "medium"
let scale_patch = __reasoning_scale_patch(lineage, scale)
if caller_pinned_reasoning {
scale_patch = scale_patch.remove("thinking").remove("effort")
}
result = result + scale_patch
// 3. Policy lowering. Direct `thinking` or `effort` always wins and bypasses
// this provider-neutral policy layer.
if !caller_pinned_reasoning {
const policy = opts?.reasoning_policy ?? "auto"
if lineage == "claude-opus-adaptive" && policy != "auto" && policy != nil {
__maybe_emit_strip(opts, policy)
}
result = result + __reasoning_policy_patch(lineage, model, policy)
}
// 4. Task overlay — only fill fields not already set above.
const reasoning_task = opts?.reasoning_task ?? "chat"
const overlay = __task_overlay(reasoning_task)
result = __fill_unset(result, overlay)
// 5. Recommended max_tokens when caller supplied a prompt and neither
// they nor the scale patch already set max_tokens.
if opts?.prompt != nil && opts?.max_tokens == nil && !__has_key(result, "max_tokens") {
const recommended = try {
recommend_max_output_tokens(
{
prompt: opts.prompt,
system: opts?.system ?? "",
model: model,
task_kind: reasoning_task,
headroom: 0.1,
},
)
}
if !is_err(recommended) {
result = result + {max_tokens: unwrap(recommended)}
}
}
// 6. Canonical user options — highest precedence. Policy keys remain in the
// returned request for introspection; direct `thinking` and `effort` pins
// replace any generated low-level setting.
result = result + opts
// 7. Re-pin model. resolved_options already set provider, but if the
// caller passed a different model id at the top level it must win.
result = result + {model: model}
return result
}
/**
* pack_chat(model, opts) — convenience wrapper for task: "chat".
*
* @effects: []
* @errors: []
*/
pub fn pack_chat(model, opts = nil) {
const base = opts ?? {}
return pack_for(base + {model: model, reasoning_task: "chat"})
}
/**
* pack_agent(model, opts) — convenience wrapper for task: "agent".
*
* @effects: []
* @errors: []
*/
pub fn pack_agent(model, opts = nil) {
const base = opts ?? {}
return pack_for(base + {model: model, reasoning_task: "agent"})
}
/**
* pack_refine(model, opts) — convenience wrapper for task: "refine".
*
* @effects: []
* @errors: []
*/
pub fn pack_refine(model, opts = nil) {
const base = opts ?? {}
return pack_for(base + {model: model, reasoning_task: "refine"})
}
/**
* pack_judge(model, opts) — convenience wrapper for task: "judge".
*
* @effects: []
* @errors: []
*/
pub fn pack_judge(model, opts = nil) {
const base = opts ?? {}
return pack_for(base + {model: model, reasoning_task: "judge"})
}
/**
* pack_summarize(model, opts) — convenience wrapper for task: "summarize".
*
* @effects: []
* @errors: []
*/
pub fn pack_summarize(model, opts = nil) {
const base = opts ?? {}
return pack_for(base + {model: model, reasoning_task: "summarize"})
}
/**
* pack_code(model, opts) — convenience wrapper for task: "code".
*
* @effects: []
* @errors: []
*/
pub fn pack_code(model, opts = nil) {
const base = opts ?? {}
return pack_for(base + {model: model, reasoning_task: "code"})
}
/**
* pack_json(model, opts) — convenience wrapper for task: "json".
*
* @effects: []
* @errors: []
*/
pub fn pack_json(model, opts = nil) {
const base = opts ?? {}
return pack_for(base + {model: model, reasoning_task: "json"})
}