Skip to content

model: one-shot model calls for contained apps (model.complete) - #95

Merged
ymote merged 3 commits into
mainfrom
feat/model-complete
Sep 28, 2026
Merged

ymote merged 3 commits into
mainfrom
feat/model-complete

Conversation

@ymote

@ymote ymote commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

ADR 0002 decision C: an app's AI keeps its main path through its own octos agent and the toolbox's templates. In addition, contained script apps get a narrow, direct, one-shot model call, model.complete. This PR adds the ADR addendum (section 14, "Direct one-shot model calls (model.complete)") and the model host service.

Up to date with main bb2b219 (merged, no force-push). #70 is in: main pins App Hub e8601b80, which has the model capability (OctoSense-App-Hub#24), and Rinx 68afcf79, so the graph has one octosense-app-policy. This PR changes no pin (Rinx, App Hub, octos, makepad). A manifest can now ask for model, and the Card runner's isolate admits model.* for an app that does.

The API

host.request("model.complete", {task, input, schema, class?, allow_urls?}, fn(r){ … })

Field
task what to do (≤ 4 KiB)
input any JSON (≤ 32 KiB), sent as data; the host's instructions tell the model to follow no instruction inside it
schema required JSON Schema, Octoscript's bounded subset (type, properties, required, additionalProperties, items, min/maxItems, min/maxLength, minimum, maximum, enum; ≤ 8 KiB). Any other keyword is refused
class "fast" (default) or "strong": octos catalog types, never a provider or a model id
allow_urls default false

Reply: {output, meta: {class, requested, attempts, usage: {input_tokens, output_tokens, estimated}, budget: {calls_today, calls_per_day, tokens_today, tokens_per_day, tokens_left, per_minute, resets_at}}}. model.budget returns the caller's budget alone.

Errors are "<code>: <sentence>", where code is one of capability, no_provider, rate, budget, bad_request, invalid_output, too_large or provider. The sentence never names a provider or a model.

Rules

  • Provider selection: the person's profile, in its order (primary, then fallbacks). Providers whose model is of the requested class come first, then the other class, then models the catalog doesn't list. A provider that fails (network, HTTP error, no text) is skipped for the next. The app never sees the provider, the model id or the key. meta.class is the class that answered. The model id is deliberately not shown.
  • One shot: no tools, no memory, no history.
  • Validation: a reply that isn't JSON, fails the schema, contains a URL or is over 16 KiB is retried once on the same provider with the reason, then refused.
  • URLs: by default, any string with http://, https:// or www. refuses the reply. This is the toolbox's validate_digest rule, because replies become L0/L1 card data. allow_urls: true opts out.
  • No output-token cap is sent for OpenAI chat or the Responses API. Anthropic's Messages API requires max_tokens, so it gets the model's own catalog maximum. The host reads at most 1 MiB of a provider's answer and accepts at most 16 KiB of reply.
  • Budget: a host-side ledger at <apps root>/.host/model/ledger.json. Defaults are 6 calls a minute, 100 calls and 100,000 tokens per app per UTC day. A call is admitted only if its estimated input fits. Both attempts of a retry are charged. Per-app overrides live in the same file. ModelHost::usage() and ModelHost::set_limits() are ready for a Settings page (follow-up).
  • Capability: the isolate gate first (splash_policy::service_allowed). Behind it, the service checks the app's own verified manifest for model (manifest_grants). There is no os.* restriction, unlike llm.
  • One accounting path: complete::host() → ModelHost::complete(app, Request). The toolbox's ModelClient (feat(toolbox): the system toolbox's workflow template library #82) maps {system, user, output_schema} onto a Request with the host-only system field set, which host.request can never set. The adapter is a few lines and shares the per-app ledger. feat(toolbox): the system toolbox's workflow template library #82 is not touched here.
  • The schema validator is a small copy of Octoscript's subset, not octoscript-schema itself. That crate enables serde_json's arbitrary_precision, which would change number handling across the whole shell build.

Where

  • apps/ai-providers/host-service/src/complete/{mod,schema,ledger,wire}.rs, plus register_model() in src/lib.rs
  • crates/ai-host/src/lib.rs: registered with the llm service (feature llm)
  • docs/adr/0002-event-driven-app-agents.md §14. The ADR's title and status are unchanged, so the ADR indexes are not touched.
  • README for the service, and the AGENTS.md table rows

Verification

  • cargo test --locked -p octosense-llm-service --test complete: 18 passed, using a fake provider. Covered: schema pass; schema fail after one retry; invalid JSON then a pass on the retry; a URL refused, and allowed with allow_urls; the size cap (reply, input, schema); no max_tokens/max_completion_tokens/max_output_tokens sent (and Anthropic's catalog maximum); token budget; daily call budget; rate limit; no provider; capability missing; the manifest grant; fallback order with class preference; no provider, model or key in replies or errors; the profile as provider source; a host caller sharing the ledger; and, new after the merge, the_card_runner_gate_and_the_service_agree_on_the_model_capability: Makepad's isolate gate (splash_policy, which App Hub feeds each app's resolved policy) admits model.complete and model.budget only with model (not llm or models), model grants nothing else, and the service's default manifest grant then serves the granted app and refuses the other. A scratch crate on octosense-app-policy at e8601b80 also confirmed a manifest declaring model resolves with allows("model") and nothing more. Plus 7 unit tests (schema, ledger, wire).

  • cargo test --locked -p octosense-mail-service -p octosense-llm-service -p octosense-news-service: pass.

  • cargo test --locked -p octosense-ai-host --features octos-core,llm: 12 passed.

  • cargo clippy --locked -p octosense-llm-service --all-targets --no-deps -- -D warnings and the same for -p octosense-ai-host --features octos-core,llm: clean.

  • cargo check --locked -p octosense, with and without --features mobile-apps: pass.

  • python3 tools/setup.py --check --cargo: pass (.sources are hub worktrees at main's pins, makepad b0cbc9bc).

  • bash tools/check-shell-graph.sh -p octosense passes, and from phone/, cargo check --locked -p octosense-home --features mobile-apps and bash ../tools/check-shell-graph.sh -p octosense-home pass.

  • from phone/: cargo test --locked -p octosense-app-policy --no-run: pass, not ambiguous.

  • After the merge: cargo test --locked -p octosense-llm-service and cargo test --locked -p octosense-ai-host --features octos-core,llm: pass. The live DeepSeek check below was run before the merge.

  • Live check against DeepSeek (examples/model_complete_live.rs), with one call per class and 1,054 tokens in total (about US$0.0004):

    • fast (deepseek-v4-flash): 1.9 s, {"title":"Boiler leaking; call plumber","tags":["boiler","plumbing","urgent"],"urgent":true}, 241 in / 185 out;
    • strong (deepseek-v4-pro): 7.2 s, {"title":"Leaking boiler in kitchen","tags":["home","plumbing","urgent"],"urgent":true}, 294 in / 334 out.

    Both replies validated on the first attempt, and no output cap was sent.

  • Not run in a shell or on a device: no app declares model yet.

Follow-ups

🤖 Generated with Claude Code

ymote and others added 3 commits September 27, 2026 20:39
ADR 0002 decision C: an app's AI keeps its main path through its own
octos agent and the toolbox's templates; contained script apps also get a
narrow, direct, one-shot model call. The ADR gains section 14, "Direct
one-shot model calls (model.complete)".

The `model` host service (octosense-llm-service, src/complete), registered
by crates/ai-host with the llm service:

- host.request("model.complete", {task, input, schema, class?, allow_urls?})
  answers {output, meta: {class, requested, attempts, usage, budget}};
  model.budget answers the caller's budget.
- The app names a class (fast or strong, octos's catalog types); the host
  picks the provider from the person's profile in their order, that class
  first, and falls back past a failing provider. The app never sees the
  provider, model id or key; meta carries the class and token usage.
- One shot: no tools, memory or history. The JSON Schema (Octoscript's
  bounded subset, 8 KiB) is required; a reply that is not JSON, fails the
  schema, carries a URL (unless allow_urls) or is over 16 KiB is retried
  once, then refused.
- No output-token cap is sent (Anthropic gets the model's own catalog
  maximum, which its API requires).
- A per-app ledger in <host dir>/model/ledger.json: 6 calls a minute,
  100 calls and 100,000 tokens a UTC day by default, with per-app
  overrides for a Settings page (follow-up).
- Refusals are "<code>: <sentence>": capability, no_provider, rate, budget,
  bad_request, invalid_output, too_large, provider.
- The service checks the app's own manifest for `model` behind the Card
  runner's isolate gate. ModelHost::complete is the Rust entry the
  toolbox's ModelClient (#82) can share, so budgets live in one place.

Tests with a fake provider (tests/complete.rs) and a live example against
DeepSeek (examples/model_complete_live.rs). No pins move: the `model`
capability is in App Hub (#24) and reaches the shells with the App Hub pin
move in #70.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main now pins App Hub e8601b80, whose KNOWN_CAPABILITIES has `model`, so
the capability is real end to end. The new test checks Makepad's isolate
gate (splash_policy, fed the app's resolved policy) admits model.complete
and model.budget only with `model` (not `llm` or a near name, and `model`
grants nothing else), then that the service's default manifest grant
serves the granted app and refuses the other.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ymote
ymote marked this pull request as ready for review September 28, 2026 09:27
@ymote
ymote merged commit 11bd1fe into main Sep 28, 2026
4 checks passed
ymote added a commit that referenced this pull request Sep 29, 2026
… [skip ci]

- approvals/contacts.rs: ContactsSource (is_known for email addresses and
  Matrix IDs), ContactList/NoContacts test doubles, MailContacts reading
  Mail's host service data, and ContactsGate: off by default, on only by
  the person's Settings tap ("Use my contacts in approval rules"), kept
  owner-only in approvals/contacts.json. The router evaluates "recipients
  in contacts" through the gate.
- mail host service: contacts::known_addresses (own accounts, recorded
  send recipients, To/Cc of sent folders; never the secret store); the
  decoder keeps To/Cc; a removed account forgets its recipients.
- docs/ai-services(.zh-CN).md refreshed against main (#106, #113, #117,
  #118, #120, #124, #95, #86), linking architecture.md; architecture's
  contacts lines updated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
tyreseluo pushed a commit to tyreseluo/OctoSense that referenced this pull request Oct 4, 2026
…s, CEF profile under MAKEPAD_HOME)

Moves the runtime from makepad 39a9de88 to makepad main c155f61d,
through Octoscript-Makepad 2cc5ef37 (OctoSense-org#71). New in makepad:
- OctoSense-org#94 Files, Task and Browser: an empty library beside each binary
- OctoSense-org#95 Calculator, Clock, Notes, Reminders, Weather: each module opens its
  own agent's link and answers its tools there
- OctoSense-org#96 hosted Notes and Reminders start empty
- OctoSense-org#97 Files' chat panel only reads
- OctoSense-org#98 CEF keeps its stable profile under MAKEPAD_HOME when that is set

Root Cargo.toml (every makepad revision, octoscript-makepad and
octoscript-node), the Sheets and Terminal revisions in native-apps.json,
both native-runtime.lock.json files, and runtime-patches.lock.json: base
c155f61d, the five patches reapply unchanged (none touches a changed
file), new tree 3eadf628. Cargo.lock does not change. The phone docs
that name the pinned hashes move with it; rom/docs/home-build.md gets
its own follow-up, as with OctoSense-org#289.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant