A Blueprint file is one YAML document. This page describes each of its top-level keys and their fields, in the order the CLI writes them, and the errors that refuse a file when it is linted or registered.
Top-level keys
Section titled “Top-level keys”| Key | Type | Required | Default |
|---|---|---|---|
kind |
string | no | absent |
name |
string | yes | |
allow_insecure_http |
boolean | no | false |
idle_timeout |
duration | no | 24h |
vfs |
mode name or map | no | ephemeral |
secrets |
map | no | empty |
variables |
map | no | empty |
packages |
list | no | empty |
auth_proxy |
list | no | empty |
git |
map | no | absent: submilli:git disabled |
default |
deny, allow |
no | deny |
permissions |
map | no | empty |
mcp |
map | no | empty |
llm |
map | no | empty |
embedding |
map | no | empty |
Any other top-level key is a parse error, and so is an unknown field in any
block. A key repeated at the top level or within one block’s fields is
refused. Repeated names in secrets, variables, permissions, mcp,
llm.providers, llm.models, embedding.providers,
embedding.models, and the map form of packages are also
refused. The same rule applies to MCP and auth-proxy header maps,
auth-proxy query maps, and paths in vfs.mounts.
error: case.yaml: blueprint parse error: unknown field `permision`, expected one of `kind`, `name`, `allow_insecure_http`, `idle_timeout`, `vfs`, `secrets`, `variables`, `packages`, `auth_proxy`, `git`, `default`, `permissions`, `mcp`, `llm`, `embedding` at line 3 column 1error: case.yaml: blueprint parse error: duplicate field `name`The CLI’s editing commands (submilli blueprint secret, variable,
auth-proxy, add-package, git, capability, add-mcp) rewrite the
whole file: keys in the order of the table, map entries sorted by key,
durations in seconds ('3600s'), sizes in bytes, and comments removed.
Example
Section titled “Example”Every key except allow_insecure_http and packages, as the CLI writes it:
kind: blueprintname: supportidle_timeout: '3600s'vfs: mode: per_session size_limit: 104857600 cwd: /notes mounts: /notes: mode: named volume: notes subPath: users/${vars.customerId}secrets: ANTHROPIC_API_KEY: store: anthropic_api_key LINEAR_API_KEY: harness: required: true STATUS_TOKEN: store: status_tokenvariables: customerId: required: true region: default: usauth_proxy:- host: status.acme.com auth: bearer: STATUS_TOKENgit: identity: name: Support Agent (${vars.customerId}) email: agent@acme.exampledefault: denypermissions: main: - capability: http.get filter: host == "status.acme.com" action: allowmcp: linear: url: https://mcp.linear.app/mcp headers: Authorization: Bearer ${secrets.LINEAR_API_KEY}llm: providers: anthropic: type: anthropic api_key: ${secrets.ANTHROPIC_API_KEY} models: claude-haiku-4-5: provider: anthropic context_window: 200000 output_reserve: 4000 description: Cheap and fast.embedding: providers: voyage: type: voyage api_key: ${secrets.VOYAGE_API_KEY} models: notes-embedding: provider: voyage model: voyage-3.5 dimensions: 1024References to variables and secrets
Section titled “References to variables and secrets”${vars.NAME} is replaced by the session’s value of a declared
variable. ${secrets.NAME} is replaced, outside the program,
by the value of a declared secret. Each may appear only in these
fields. Elsewhere the text is taken literally. A reference to a name the
file doesn’t declare is an error.
| Reference | Fields |
|---|---|
${vars.NAME} |
permissions rule filter; git.identity.name, git.identity.email, git.username; vfs.subPath, vfs.cwd, and a mount’s subPath, as a whole path component |
${secrets.NAME} |
auth_proxy headers and query values; mcp headers values; mcp auth client_id, authorization_endpoint, token_endpoint, scopes; llm.providers api_key and base_url; embedding.providers api_key and base_url |
| A secret name, bare | auth_proxy auth.bearer and auth.basic.password |
| Type | string |
| Required | no |
| Allowed values | blueprint |
error: case.yaml: invalid blueprint kind: unknown kind 'policy': expected `blueprint` (or omit `kind`)| Type | string |
| Required | yes |
| Constraints | non-empty, with only ASCII letters, digits, _ and - |
The name a server registers the Blueprint under and an application names
when it opens a session. submilli server blueprint apply registers the
file under this name, replacing a Blueprint registered with it.
error: case.yaml: blueprint parse error: missing field `name`error: case.yaml: invalid blueprint name: blueprint name 'my agent' contains characters outside [A-Za-z0-9_-]allow_insecure_http
Section titled “allow_insecure_http”| Type | boolean |
| Default | false |
true permits http:// requests through submilli:http, including
Package calls and downloads. With false, only https:// is permitted. An
http:// request to a host that has an auth_proxy rule is
permitted only when that rule’s own allow_insecure_http is true as well.
The check applies to every redirect too.
idle_timeout
Section titled “idle_timeout”| Type | duration, a whole number followed by s, m or h |
| Default | 24h |
How long a session may go unused before the server closes it. Closing a
session deletes its submilli:session state and its per_session files.
The CLI writes the value back in seconds, so 1h becomes '3600s'.
error: case.yaml: blueprint parse error: idle_timeout: duration '10' needs a unit (s, m, or h)error: case.yaml: blueprint parse error: idle_timeout: unknown duration unit 'd' in '1d' (use s, m, or h)The program’s filesystem. Either a mode name, vfs: per_session, or a map
with mode and that mode’s fields.
mode |
The program’s / |
|---|---|
none |
No filesystem. Every submilli:fs call fails |
ephemeral (default) |
A directory created for the run and deleted when it returns |
per_session |
A directory that lasts as long as the session |
named |
A named volume declared in the server’s config. It is kept across sessions and restarts, and shared with every Blueprint that names it |
| Field | Type | Modes | Default | Constraints |
|---|---|---|---|---|
mode |
none, ephemeral, per_session, named |
all | ephemeral |
|
size_limit |
size | ephemeral, per_session |
no limit | A named volume’s limit is set in the server’s config |
volume |
string | named |
Required under named. The non-empty name of a volume declared on the server |
|
subPath |
string | named |
the volume’s root | A relative, normalized path inside the volume |
access |
read_only, read_write |
named |
the server’s declaration | Can only narrow the server’s declaration |
cwd |
string | ephemeral, per_session, named |
/ |
An absolute, normalized guest path |
mounts |
map | ephemeral, per_session, named |
none | See Mounts |
grace_period |
duration | per_session |
Accepted and ignored | |
path_limit |
number | ephemeral, per_session |
Accepted and ignored |
A field that doesn’t belong to the block’s mode is refused, with a message naming the field and the mode:
error: case.yaml: blueprint parse error: vfs.size_limit: invalid vfs config: 'size_limit' is not valid for vfs mode 'named'; a named volume's size limit is set by the operator where the server declares it at line 5 column 15error: case.yaml: blueprint parse error: vfs: invalid vfs config: `volume` is only valid under `mode: named`, and this vfs block has no `mode:` (it defaults to `ephemeral`); add `mode: named` to this vfs block, or move the volume under `mounts:` to keep an ephemeral root at line 3 column 3error: case.yaml: blueprint parse error: vfs: invalid vfs config: vfs mode 'named' requires a 'volume': the name of a volume the operator declared in the server config at line 3 column 3Two retired forms are refused with the replacement: mode persistent, and
the path key.
error: case.yaml: blueprint parse error: vfs: vfs mode `persistent` was removed: write `mode: named` and keep the `volume:` line (`vfs: {mode: named, volume: <name>}`). A named volume keeps its files across calls, sessions and restarts as before; leave `access:` out to keep the access the server declares for it at line 2 column 6error: case.yaml: blueprint parse error: vfs.path: the `path` key is retired: a blueprint can no longer name a host directory. Use `volume: <name>` under `mode: named` — the operator declares each volume by name in the server config at line 4 column 9submilli run refuses a Blueprint that names a volume, as the root or as a
mount, because volumes are declared only in a server’s config.
A size is a byte count, 104857600, or a whole number followed by a unit:
B, KB, MB, GB, TB, with K, M, G, T and KiB, MiB,
GiB, TiB accepted as the same units. Units are 1024-based and
case-insensitive, so 100MB is 104,857,600 bytes.
error: case.yaml: blueprint parse error: vfs: invalid vfs config: unknown size unit 'XB' in '10XB' (use B, KB, MB, GB, TB) at line 3 column 3Under per_session, size_limit covers all of the session’s files. A write
that would pass it throws QuotaExceededError.
Mounts
Section titled “Mounts”mounts maps an absolute guest path to a named volume mounted there, below
the root.
vfs: mode: per_session mounts: /memory: mode: named volume: project-memory access: read_write /handbook: mode: named volume: company-handbook access: read_only| Field | Type | Required | Default | Constraints |
|---|---|---|---|---|
mode |
named |
yes | The only mount mode | |
volume |
string | yes | The non-empty name of a volume declared on the server | |
subPath |
string | no | the volume’s root | A relative, normalized path inside the volume |
access |
read_only, read_write |
no | the server’s declaration | Can only narrow the server’s declaration |
A mount path:
- is absolute and is not
/ - has at most 4,096 bytes and 64 components
- contains only ASCII letters, digits,
.,_,-and/ - has no empty,
.or..component, no trailing/, and no component ending in. - names no
.gitcomponent - is not inside another mount, does not contain one, and differs from every other mount by more than letter case
A Blueprint has at most 16 mounts. The same volume may be mounted at several
paths. mounts under mode: none is refused.
error: case.yaml: blueprint parse error: vfs.mounts./a/b: mount `/a/b` is inside mount `/a`; mounts may not nest — mount the volumes side by side, such as `/a` and `/b` at line 5 column 11error: case.yaml: blueprint parse error: vfs.mounts./a: a mount needs `mode: named`: mounts are named volumes the operator declares in the server config at line 4 column 9error: case.yaml: blueprint parse error: vfs.mounts./a.size_limit: unknown field `size_limit` in a mount, expected one of `mode`, `volume`, `access`, `subPath` at line 4 column 46subPath and cwd
Section titled “subPath and cwd”subPath (on a named root or a mount) is relative to the volume’s root. The
program sees the selected directory as the root of that volume. cwd is an
absolute guest path, and the directory relative paths resolve against for
submilli:fs, Packages, and http.download destinations. fs.cwd()
returns it. In both, ${vars.NAME} may stand for one whole component, and
must resolve to a non-empty name containing no /, \ or NUL that is not
. or ... A path is at most 4,096 bytes, before and after substitution.
variables: userId: required: truevfs: mode: ephemeral cwd: /notes mounts: /notes: mode: named volume: notes subPath: users/${vars.userId}error: case.yaml: invalid vfs config: must be a relative volume path of at most 4096 byteserror: case.yaml: invalid vfs config: use ${vars.NAME} as a whole path componenterror: case.yaml: invalid vfs config: must be an absolute guest path of at most 4096 bytessecrets
Section titled “secrets”A map from secret name to the source its value comes from.
${secrets.NAME} and auth_proxy auth fields reference a name declared
here. The file holds names, never values.
| Source | YAML | The value comes from |
|---|---|---|
store |
store: <key> |
The secret store under <key>. On a server, that is the server’s encrypted store (submilli server secret put), and in submilli run, the local store (submilli secret put). Read on each use. |
harness |
harness: {} or harness: {required: true} |
The application, when it opens or rebinds a session |
| Field | Type | Default | |
|---|---|---|---|
harness.required |
boolean | false |
true refuses a session that doesn’t supply a non-empty value |
secrets: BILLING_API_KEY: store: billing_api_key LINEAR_API_KEY: harness: required: trueA session that supplies a value for a name the file doesn’t declare, or for
a store secret, is refused. An empty supplied value counts as absent.
error: case.yaml: blueprint parse error: secrets: a secret needs exactly one source: store / harness at line 3 column 3error: case.yaml: blueprint parse error: secrets.A: unknown field `env`, expected `store` or `harness` at line 4 column 5Registration on a server checks that every store secret has a value in
the server’s store. See Registration.
variables
Section titled “variables”A map from variable name to its rule. An application supplies variable values, as strings, when it opens a session. The program can’t read or change them.
| Field | Type | Default | Constraints |
|---|---|---|---|
required |
boolean | false |
true refuses a session that doesn’t supply a non-empty value |
default |
string | none | The value bound when the session supplies none. Can’t be combined with required: true |
An optional variable with no value and no default is unbound, and a filter comparing against it doesn’t match. A session that supplies a name the file doesn’t declare is refused.
error: case.yaml: invalid variables config: variable 'a': `required: true` and `default:` are mutually exclusiveerror: case.yaml: invalid variables config: permissions for 'main': filter references undeclared variable '${vars.p}'Packages
Section titled “Packages”A list of Package names a program may import. A map whose keys are the names is accepted as well, and its values are ignored.
Each name:
- has the scoped form
@org/name, with exactly one/ - has an
orgof 1 to 39 ASCII letters, digits, and single inner hyphens, with no leading or trailing hyphen - has a
nameof ASCII letters, digits,.,_and-, other than.and.. - is not a
submilli:*module and not an@mcp/*Package - is listed once
error: case.yaml: invalid packages config: package `@acme.co/billing` scope `@acme.co` must be a GitHub org: ASCII letters, digits, and single internal hyphens only (no dots), 1–39 characterserror: case.yaml: invalid packages config: `@mcp/linear` is an MCP virtual package; declare the server in `mcp:` insteaderror: case.yaml: blueprint parse error: duplicate package `@acme/a` in packages:submilli blueprint lint checks each Package against the local Package
store, and registration against the server’s:
error: case.yaml: cannot validate package `@acme/billing` capabilities: package `@acme/billing` was not found in /…/packages; no packages are availableauth_proxy
Section titled “auth_proxy”A list of rules, each adding credentials to outbound submilli:http
requests to one host. For a request, the first rule whose host equals the
request’s host applies. The permission check runs before it. A request that
received credentials follows a redirect only to the same scheme, host, and
port.
| Field | Type | Required | Default | Constraints |
|---|---|---|---|---|
host |
string | yes | Matched exactly | |
allow_insecure_http |
boolean | no | false |
Also needs the top-level allow_insecure_http |
auth.bearer |
secret name | one of auth, headers, query |
Sends Authorization: Bearer <value> |
|
auth.basic.username |
string | with auth.basic |
A literal | |
auth.basic.password |
secret name | with auth.basic |
Sends Authorization: Basic <base64(username:value)> |
|
headers |
map of name to string | one of auth, headers, query |
Values may hold ${secrets.NAME}. No Authorization header alongside auth |
|
query |
map of name to string | one of auth, headers, query |
Values may hold ${secrets.NAME} |
auth sets exactly one of bearer and basic.
auth_proxy:- host: status.acme.com auth: bearer: STATUS_TOKEN- host: legacy.acme.com headers: X-Api-Key: ${secrets.LEGACY_KEY}error: case.yaml: invalid auth_proxy config: auth_proxy rule for host 'a.com' must set auth, headers, and/or queryerror: case.yaml: invalid auth_proxy config: auth_proxy rule for host 'a.com' must set exactly one of `auth.bearer` or `auth.basic`error: case.yaml: invalid auth_proxy config: auth_proxy rule for host 'a.com' sets both `auth:` and an explicit `Authorization` header — use one or the othererror: case.yaml: invalid auth_proxy config: auth_proxy rule for host 'a.com' references undeclared secret 'K'The commit identity for submilli:git. Without a git key, programs can’t
import submilli:git.
| Field | Type | Required | Constraints |
|---|---|---|---|
identity.name |
string | yes | Non-empty, with no control characters, < or > |
identity.email |
string | yes | Non-empty, with no control characters, < or > |
username |
string | no | Non-empty, with no control characters or :. The HTTPS username for private repositories, sent with the secret named GIT_TOKEN |
Each value may hold ${vars.NAME}, resolved when the session opens. No
other ${…} reference is accepted.
git: identity: name: Support Agent (${vars.customerId}) email: agent@acme.example username: agenterror: case.yaml: blueprint parse error: git: missing field `identity` at line 3 column 3error: case.yaml: invalid git config: only ${vars.NAME} references are supportederror: case.yaml: invalid git config: must be nonempty and contain no control characters or identity delimitersdefault
Section titled “default”| Type | string |
| Allowed values | deny, allow |
| Default | deny |
The action for a capability check that no permissions
rule matches.
error: case.yaml: blueprint parse error: default: unknown variant `block`, expected one of … at line 2 column 10permissions
Section titled “permissions”A map from caller to an ordered list of rules. The caller is main for the
program, or a Package name for that Package’s own calls.
| Field | Type | Required |
|---|---|---|
name |
string, non-empty | no |
capability |
string, non-empty | yes |
filter |
filter expression | no |
action |
allow, deny |
yes |
name is an optional label for the rule. It must be unique within a caller’s
list (blueprint lint reports a repeat as an error). It gives the rule a
stable name to refer to instead of its position in the list.
permissions: main: - capability: acme.com/credits.apply filter: customerId == ${vars.customerId} and customerClass == "premium" action: allowHow rules are matched, and every capability and its fields, are in Permissions. The filter grammar is in Filter language. A filter is parsed with the file, so a malformed one is a parse error:
error: case.yaml: blueprint parse error: permissions.main[0]: invalid filter `path ===`: expected `==`; a single `=` is not an operator path === ^ at line 4 column 5A map from a server name, chosen by the file, to an outbound MCP server.
The name becomes the Package @mcp/<name> and the capability
mcp.<name>.
| Field | Type | Required | Default |
|---|---|---|---|
url |
string | yes | |
transport |
streamable_http, sse |
no | streamable_http |
headers |
map of name to string | no | none |
auth |
map with type: oauth2 |
no | none |
mcp: linear: url: https://mcp.linear.app/mcp auth: type: oauth2Every field, the OAuth fields, and the errors are in MCP servers.
The model providers submilli:llm reaches and the models a program may
name. A model this block doesn’t declare can’t be called.
| Field | Type | Default |
|---|---|---|
providers |
map from provider name to provider | empty |
models |
map from model name to model | empty |
llm: providers: anthropic: type: anthropic api_key: ${secrets.ANTHROPIC_API_KEY} models: claude-haiku-4-5: provider: anthropic output_reserve: 4000 description: "Cheap and fast; use for bulk per-item classification." claude-sonnet-5: provider: anthropicThe block has no budget fields. Token budgets are server settings. See Server settings.
providers
Section titled “providers”| Field | Type | Required | Default | Constraints |
|---|---|---|---|---|
type |
anthropic, google, openai, openai-compatible |
yes | ||
base_url |
string | for openai-compatible |
the provider’s own endpoint | An absolute https:// URL that is not localhost or a loopback, unspecified, link-local, private, broadcast, or carrier-grade NAT address literal. May hold ${secrets.NAME} |
api_key |
string | no | none | Normally ${secrets.NAME} |
supports_structured_outputs |
boolean | no | true |
false sends no JSON Schema with typed calls |
error: case.yaml: invalid llm config: llm provider 'p': unknown type 'mistral' (use one of: anthropic, google, openai, openai-compatible)error: case.yaml: invalid llm config: llm provider 'p': type 'openai-compatible' has no default endpoint; set 'base_url' to the https:// URL of the endpointerror: case.yaml: invalid llm config: llm provider 'p': base_url 'http://api.x.com/v1' uses the 'http' scheme; the API key travels in the Authorization header, so the endpoint must be https://error: case.yaml: invalid llm config: llm provider 'p': base_url 'https://10.0.0.5/v1' resolves to loopback, link-local, or private address space, which a credentialed client must not be pointed at; use the endpoint's public https:// hostnameerror: case.yaml: invalid llm config: llm provider 'p' references undeclared secret 'K'; add it under 'secrets:'models
Section titled “models”The key is the name a program passes to llm.call, llm.batch, and the
name llm.models() returns.
| Field | Type | Required | Default | Constraints |
|---|---|---|---|---|
provider |
string | yes | A key of llm.providers |
|
context_window |
non-negative integer | no | unknown | Returned by models() as contextWindow |
output_reserve |
non-negative integer | no | 64,000 reserved, no output cap sent | Output tokens per prompt |
description |
string | no | none | One line of at most 512 characters, with no control or invisible formatting characters. Returned by models() |
output_reserve is counted against the run’s and the server’s token budgets
for each prompt, with the prompt’s estimated size, before the prompt is
sent. A prompt that doesn’t fit is refused with QuotaExceededError
without being sent. When set, it is also sent as the request’s output cap.
When absent, 64,000 tokens are reserved and the request sets no cap, except
Anthropic requests, which always carry one and use 4,096.
error: case.yaml: invalid llm config: llm model 'm' names undeclared provider 'p'; declare it under 'llm.providers:' or point the model at one of: (none declared)error: case.yaml: invalid llm config: llm model 'm': description contains a newline; it reaches a model's selection reasoning verbatim, so keep it to a single line of printable texterror: case.yaml: blueprint parse error: llm.models.m.output_reserve: invalid type: string "4k", expected u64 at line 6 column 38An llm.call rule whose filter tests model == "…" must name a declared
model:
error: case.yaml: invalid llm config: caller 'main': permission filter names undeclared llm model 'gpt-9'; declare it under 'llm.models:' or filter on one of: membedding
Section titled “embedding”The embedding providers submilli:embedding reaches and the model aliases a
program may name. An alias this block doesn’t declare can’t be used.
| Field | Type | Default |
|---|---|---|
providers |
map from provider name to provider | empty |
models |
map from alias to alias | empty |
embedding: providers: voyage: type: voyage api_key: ${secrets.VOYAGE_API_KEY} models: notes-embedding: provider: voyage model: voyage-3.5 dimensions: 1024 description: "Embeds notes for semantic search."The block has no budget fields. Embedding budgets are server settings; see Server settings.
Embedding providers
Section titled “Embedding providers”| Field | Type | Required | Default | Constraints |
|---|---|---|---|---|
type |
voyage, openai, google, jina, huggingface |
yes | ||
base_url |
string | for a Hugging Face dedicated endpoint | the provider’s own endpoint | An absolute https:// URL; not localhost, nor a loopback, unspecified, link-local, private, broadcast, or carrier-grade NAT address literal. May hold ${secrets.NAME} |
api_key |
string | yes, except for huggingface with a base_url |
none | ${secrets.NAME}; a literal key is refused |
A huggingface provider without base_url calls the shared Inference
Providers router. With base_url it calls the dedicated Inference Endpoint at
that address.
error: bad.yaml: invalid embedding config: embedding provider 'p': unknown type 'cohere' (use one of: voyage, openai, google, jina, huggingface)error: bad.yaml: invalid embedding config: embedding provider 'p': base_url 'http://api.x.com/v1' uses the 'http' scheme; the API key travels in the Authorization header, so the endpoint must be https://error: bad.yaml: invalid embedding config: embedding provider 'p': 'api_key' must be a "${secrets.NAME}" reference, not a literal key; declare the key under 'secrets:'error: bad.yaml: invalid embedding config: embedding provider 'p': 'api_key' is required; set it to "${secrets.NAME}" naming a declared secret (only a huggingface provider with a 'base_url' may omit it)error: case.yaml: invalid embedding config: embedding provider 'p' references undeclared secret 'Q'; add it under 'secrets:'Embedding models
Section titled “Embedding models”The key is the name a program passes to embed and the name models()
returns. Every field except provider, model, and dimensions is optional.
| Field | Type | Required | Default | Constraints |
|---|---|---|---|---|
provider |
string | yes | A key of embedding.providers |
|
model |
string | yes | The provider’s own model id, including any version suffix | |
dimensions |
integer | yes | 1 to 8,192. Sent to the provider where the model accepts it, and checked against every response either way. text-embedding-ada-002 must be 1,536 |
|
max_input_tokens |
positive integer | no | Per provider, below | At most half the provider’s per-request token cap, below |
description |
string | no | none | One line; at most 512 characters; no control or invisible formatting characters. Returned by models() |
query_prompt_name |
string | no | none | huggingface with a base_url only; 1 to 128 printable characters. The prompt applied to queries |
document_prompt_name |
string | no | none | huggingface with a base_url only; same limits. The prompt applied to documents |
Prompt names are refused on Hugging Face shared hosting (a provider without
a base_url), because the shared router might truncate the text it prefixes
with the prompt.
The alias’s embedding-space identity changes with the provider’s type,
model, dimensions, the prompt names, how the alias applies purpose
(input_type for Voyage, task for Jina, taskType or a text template for
Google, the prompt names for Hugging Face, nothing for OpenAI), and, for a
Hugging Face dedicated endpoint, the host of its base_url. It doesn’t change
with max_input_tokens, description, or the provider’s key.
Limits by provider. maxInputBytes, which models() reports, is three bytes
per token of the alias’s input limit and, for Google, no more than the byte
bound. On Hugging Face shared hosting it is the input limit minus 16 bytes
instead (496 by default), because the shared router ignores a request not to
truncate and cuts long input silently with a success response; an alias there
must declare a max_input_tokens above 16, and it must not exceed the
model’s real maximum, or the router truncates silently. A dedicated endpoint
keeps three bytes per token. For longer chunks, use a dedicated endpoint or another
provider. Submilli splits a batch into requests that fit the request caps.
| Provider | Default max_input_tokens |
Byte bound (Google, shared Hugging Face) | Texts per request | Other request cap | Ceiling for max_input_tokens |
|---|---|---|---|---|---|
| Voyage | 32,000 | 1,000 | Tokens per request: 1,000,000 for voyage-4-lite and voyage-3.5-lite, 320,000 for voyage-4 and voyage-3.5, 120,000 for other models |
Half the token cap | |
| OpenAI | 8,192 | 2,048 | 300,000 tokens | 150,000 | |
2,048; 8,192 for gemini-embedding-2 |
2,032 bytes; 8,147 for gemini-embedding-2 |
100 | None | None | |
| Jina | 8,192 | 512 | None | None | |
| Hugging Face | 512 | 496 bytes on shared hosting (input limit minus 16) | 32 | None | None |
A Google model other than gemini-embedding-2 takes the gemini-embedding-001
row. gemini-embedding-001 reports no usage, so its estimates stay held against
the server-wide budget like Hugging Face’s; gemini-embedding-2 reports usage.
The token counts behind budgets are an estimate of three bytes per token, not
the provider’s tokenizer. The input limit uses the same three bytes per token,
except for Google and shared Hugging Face hosting, whose byte bounds above
assume one byte per token so that nothing is silently truncated.
error: bad.yaml: invalid embedding config: embedding model 'm': dimensions 0 is out of range; use a whole number from 1 to 8192error: bad.yaml: invalid embedding config: embedding model 'm': text-embedding-ada-002 always returns 1536 dimensions and cannot be shortened; set 'dimensions: 1536'error: bad.yaml: invalid embedding config: embedding model 'm': max_input_tokens 200000 is over 150000, half of the openai provider's per-request token cap; lower iterror: bad.yaml: invalid embedding config: embedding model 'm': 'query_prompt_name' applies only to huggingface providers; remove iterror: bad.yaml: invalid embedding config: embedding model 'm' names undeclared provider 'q'; declare it under 'embedding.providers:' or point the model at one of: pAn embedding.embed rule whose filter tests model == "…" must name a declared
alias:
error: bad.yaml: invalid embedding config: caller 'main': permission filter names undeclared embedding model 'gpt'; declare it under 'embedding.models:' or filter on one of: (none declared)Errors
Section titled “Errors”submilli blueprint lint, submilli run --blueprint, and registration
parse the file the same way, and parsing stops at the first error. The CLI prints it
as error: <file>: <prefix>: <message>, with the YAML path and the line and
column when they are known. Over HTTP, the same error is a 400 response
whose error field names the class. See HTTP API.
| Prefix | error over HTTP |
Raised by |
|---|---|---|
blueprint YAML is empty |
parse_error |
An empty file |
blueprint parse error |
parse_error |
YAML syntax, unknown or repeated keys, wrong types, unknown values, vfs fields and mounts, filters, idle_timeout |
invalid blueprint kind |
invalid_kind |
kind |
invalid blueprint name |
invalid_name |
name |
invalid vfs config |
invalid_vfs |
subPath and cwd |
invalid packages config |
invalid_packages |
packages names |
invalid variables config |
invalid_variables |
variables, undeclared ${vars.NAME} in filters |
invalid auth_proxy config |
invalid_auth_proxy |
auth_proxy |
invalid permissions config |
invalid_permissions |
Empty caller or capability names |
invalid git config |
invalid_git |
git |
invalid mcp config |
invalid_mcp |
mcp, mcp.<name> rules |
invalid llm config |
invalid_llm |
llm, llm.call model filters |
invalid embedding config |
invalid_embedding |
embedding, embedding.embed model filters |
Registration
Section titled “Registration”submilli server blueprint apply sends the file to the server, which parses
it and then checks it against what the server holds. A file that fails a
check isn’t registered.
| Check | Message |
|---|---|
| Every volume, as the root or a mount, is declared on the server | volume 'team' is not declared on this server; declared volumes: handbook, notes |
access doesn’t exceed the server’s declaration |
volume 'handbook' is read_only on this server; drop `access: read_write` (or write `access: read_only`), or ask the operator to declare it read_write |
Every store secret has a value in the server’s store |
secret check failed: missing secret 'K' |
| Every Package, and every Package it depends on, is installed on the server | package check failed: package `@acme/billing` is not installed; install it with `submilli server packages install <org/repo> @acme/billing` |
| Each capability a Package requires for its own calls has a rule under that Package’s caller | package check failed: followed by the Package, the capability, and the missing rule |
A filter tests only fields its capability reports, for standard-library, Package, and mcp.<name> capabilities |
The rule, the field, and the fields the capability reports |
submilli blueprint lint makes the Package and filter checks against the
local Package store, and reports every filter field it finds:
error: case.yaml: `permissions.main` rule 2 for `fs.read` tests `host`, which the operation doesn't report, so a condition on it is false for every call, and true under `not`; its fields are: chunkSize, length, path, recursiveharness secrets aren’t checked at registration. The secret check is made
once, so a value removed from the store later fails the call that needs it.