bkper 5.8.1 → 5.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Call Bkper AI models from your own client or code — the guide, with a page for each operation: models, responses, streaming, and decisions.
4
4
 
5
- The Bkper AI Gateway serves the models behind Bkper's agents and apps to your own clients and code: typed answers from decision models and the stateless Bkper [Open Responses 2026-04-24](https://www.openresponses.org/specification/2026-04-24) profile for language models.
5
+ The Bkper AI Gateway serves the models behind Bkper's agents and apps to your own clients and code: typed answers from System One models and the stateless Bkper [Open Responses 2026-04-24](https://www.openresponses.org/specification/2026-04-24) profile for language models.
6
6
 
7
7
  [Models and Usage](https://bkper.com/docs/ai/models) covers the models, usage rates, the monthly allowance, and privacy. To run an agent that works on Books instead of calling a model, use the [Managed Agent API](https://bkper.com/docs/api/managed-agent). Building a Bkper app? Start with [Add Bkper AI to an App](https://bkper.com/docs/platform/apps/ai). The canonical machine-readable contract is [https://ai.bkper.app/openapi.json](https://ai.bkper.app/openapi.json).
8
8
 
@@ -10,9 +10,9 @@ The Bkper AI Gateway serves the models behind Bkper's agents and apps to your ow
10
10
 
11
11
  1. Call `GET /v1/models` to discover the current public model IDs, types, capabilities, limits, and usage rates.
12
12
  2. Select a returned model ID.
13
- 3. Call `POST /v1/responses` for language models or `POST /v1/decisions` for decision models.
13
+ 3. Call `POST /v1/responses` for language models or `POST /v1/systemone` for System One models.
14
14
 
15
- Responses support complete JSON with `stream: false` or semantic server-sent events with `stream: true`. Decision models return complete JSON and do not stream.
15
+ Responses support complete JSON with `stream: false` or semantic server-sent events with `stream: true`. System One models return complete JSON and do not stream.
16
16
 
17
17
  ## Requirements
18
18
 
@@ -20,7 +20,7 @@ You need:
20
20
 
21
21
  - a Bkper account with an eligible subscription or trial allowance;
22
22
  - a valid Bkper OAuth access token;
23
- - an Open Responses client with a custom base URL for language generation, or an HTTP client or TypeSafe SDK for decision models.
23
+ - an Open Responses client with a custom base URL for language generation, or an HTTP client or TypeSafe SDK for System One models.
24
24
 
25
25
  Requests are attributed to the authenticated Bkper user. Business subscriptions use a shared domain allowance. See [Models and Usage](https://bkper.com/docs/ai/models#monthly-allowance) for allowance scope, current rates, and model capabilities.
26
26
 
@@ -32,7 +32,7 @@ Requests are attributed to the authenticated Bkper user. Business subscriptions
32
32
  | Authentication | `Authorization: Bearer <Bkper access token>` |
33
33
  | Model discovery | `GET /v1/models` |
34
34
  | Language generation | `POST /v1/responses` |
35
- | Decision models | `POST /v1/decisions` |
35
+ | System One models | `POST /v1/systemone` |
36
36
  | Open Responses profile | `2026-04-24` for language generation only |
37
37
 
38
38
  The live `GET /v1/models` response is authoritative for available IDs, model types, capabilities, limits, and effective usage rates. The catalog publishes `default_model` as the default language model. Inspect each entry's `type` before choosing its endpoint.
@@ -45,7 +45,7 @@ Bkper AI implements a documented subset of Open Responses. It does not claim ful
45
45
 
46
46
  Use the model IDs returned by `GET /v1/models`, such as `gpt-luna`, `grok`, `gemini-flash`, and `jev`.
47
47
 
48
- Language model entries have `type: "language"` and use `POST /v1/responses`. Decision model entries have `type: "decision"` and use `POST /v1/decisions`. Sending a model to the wrong endpoint fails explicitly.
48
+ Language model entries have `type: "language"` and use `POST /v1/responses`. System One model entries have `type: "system-one"`, list the question types they answer in `question_types`, and use `POST /v1/systemone`. Entries with `preview: true` are provider previews whose answers, limits, and availability may change without notice. Sending a model to the wrong endpoint fails explicitly.
49
49
 
50
50
  Model IDs remain stable as Bkper updates the model behind them. Older versioned and publisher-prefixed IDs still work, but they select the current model, not the older version. Catalogs, responses, and usage reports use the current ID.
51
51
 
@@ -62,7 +62,7 @@ Treat the token as a secret. Do not commit it, print it in shared logs, or put i
62
62
 
63
63
  ## Call from a browser
64
64
 
65
- Web pages on any origin can call `POST /v1/responses`, `POST /v1/decisions`, and `GET /v1/models` directly. Bkper AI authenticates only the bearer token and uses no cookies, so browser requests carry no credentials.
65
+ Web pages on any origin can call `POST /v1/responses`, `POST /v1/systemone`, and `GET /v1/models` directly. Bkper AI authenticates only the bearer token and uses no cookies, so browser requests carry no credentials.
66
66
 
67
67
  - **Use the signed-in user's token at runtime.** On `*.bkper.app` pages, `@bkper/web-auth` supplies it. On other domains, use your own Bkper OAuth flow. Never ship a fixed token in a page.
68
68
  - **Usage counts against the signed-in user's allowance.** Label it with a `bkper-ai-source` header, such as `my-page`.
@@ -75,7 +75,7 @@ import { BkperAuth } from '@bkper/web-auth';
75
75
  const auth = new BkperAuth();
76
76
  await auth.init();
77
77
 
78
- const response = await auth.authenticatedFetch('https://ai.bkper.app/v1/decisions', {
78
+ const response = await auth.authenticatedFetch('https://ai.bkper.app/v1/systemone', {
79
79
  method: 'POST',
80
80
  headers: { 'Content-Type': 'application/json', 'bkper-ai-source': 'my-page' },
81
81
  body: JSON.stringify({
@@ -108,14 +108,14 @@ The response is an Open Responses resource. Its `model` contains the canonical p
108
108
 
109
109
  The `bkper-ai-source` header is optional. Set it to a stable lowercase identifier such as `my-harness` to see which client or application made a request in the usage dashboard. Without a valid identifier, the source appears as unknown.
110
110
 
111
- ## Decision models
111
+ ## Decision questions
112
112
 
113
- `POST /v1/decisions` answers typed questions about a shared state instead of generating text. Each question returns a noul probability, one choice from options you define, or a level on an ordered score, and your code decides what to do with it. Designing the questions, thresholds, and human review is covered in the [Decision Models guide](https://bkper.com/docs/ai/decision-models).
113
+ `POST /v1/systemone` answers typed questions about a shared state instead of generating text. Each question returns a noul probability, one choice from options you define, or a level on an ordered score, and your code decides what to do with it. Designing the questions, thresholds, and human review is covered in the [System One Models guide](https://bkper.com/docs/ai/system-one-models). `POST /v1/decisions` is a deprecated alias with identical behavior.
114
114
 
115
- For an incoming bank line, this request asks three questions at once: which Account it belongs to, whether it is already recorded, and how urgently it needs review.
115
+ For an incoming bank line, this request asks for three judgments at once: which Account it belongs to, whether it is already recorded, and how unusual it is as a business expense. Code combines the answers with facts, such as the amount, into what to do with the line.
116
116
 
117
117
  ```bash
118
- curl --fail-with-body https://ai.bkper.app/v1/decisions \
118
+ curl --fail-with-body https://ai.bkper.app/v1/systemone \
119
119
  -H "Authorization: Bearer ${BKPER_TOKEN}" \
120
120
  -H "Content-Type: application/json" \
121
121
  -H "bkper-ai-source: my-app" \
@@ -152,13 +152,13 @@ curl --fail-with-body https://ai.bkper.app/v1/decisions \
152
152
  "type": "noul",
153
153
  "instructions": "Does `existing_transaction` already record the movement in `bank_line`?"
154
154
  },
155
- "review_priority": {
155
+ "unusual": {
156
156
  "type": "score",
157
- "instructions": "How urgently should a bookkeeper review `bank_line`?",
157
+ "instructions": "How unusual is `bank_line` as a business expense?",
158
158
  "criteria": [
159
- "Routine: consistent with normal activity",
160
- "Check: unusual but plausibly legitimate",
161
- "Urgent: large, unapproved, or suspicious"
159
+ "Routine: a common business expense",
160
+ "Unusual: plausible, but uncommon for a business",
161
+ "Suspicious: looks like fraud, an error, or a personal expense"
162
162
  ]
163
163
  }
164
164
  }
@@ -167,14 +167,15 @@ curl --fail-with-body https://ai.bkper.app/v1/decisions \
167
167
 
168
168
  ### Decision request
169
169
 
170
- - **`model`** is any model with `type: "decision"` in `GET /v1/models`, such as `jev`. Each entry lists its `question_types`, `context_window`, and `max_state_question_tokens`.
171
- - **`state`** is what every question judges: a string, JSON object, or array. Prefer an object with descriptive field names. Decision models read text only, so convert images and files to text first.
170
+ - **`model`** is any model with `type: "system-one"` in `GET /v1/models`, such as `jev` or `clef`. Each entry lists its `question_types`, `context_window`, and `max_state_question_tokens`; a question whose type is not in the model's `question_types` returns `unsupported_question_type`.
171
+ - **`state`** is what every question judges: a string, JSON object, or array. Prefer an object with descriptive field names. Convert PDFs and other files to text first, or to image `assets` for models with image input.
172
172
  - **`questions`** is a nonempty map. Question IDs such as `account` are yours, and answers come back under the same IDs, but **IDs are not sent to the model**, so each `instructions` must stand on its own. Refer to state fields by name in backticks, as in `` `bank_line` ``.
173
173
  - Each question accepts only `type`, `instructions`, and `criteria`. Instructions, descriptions, and levels can be strings or [structured JSON](https://docs.typesafe.ai/primitives/advanced):
174
174
  - **`noul`**: optional `criteria` describe what yes and no mean.
175
175
  - **`choice`**: `criteria` maps 1–255 options to descriptions, which may be `null`.
176
176
  - **`score`**: `criteria` lists 2–10 levels, lowest first.
177
- - The body accepts only `model`, `state`, and `questions`. Responses fields such as `input`, `stream`, `store`, `temperature`, and `metadata` are rejected.
177
+ - The body accepts only `model`, `state`, and `questions`, plus image `assets` on models with image input. `reference_date` applies only to extraction questions; `jev` and `clef` reject it with `unsupported_parameter`. Responses fields such as `input`, `stream`, `store`, `temperature`, and `metadata` are rejected.
178
+ - **`clef`** answers at most 64 questions, named with up to 100 letters, digits, `_`, `.`, or `-`, and needs at least 2 choice options. It reads up to 4 image `assets` (4 MiB each, 8 MiB in total) before the state, in `assets` order. Requests beyond these limits return `unsupported_parameter`; input over its 65,536-token context returns `provider_rejected`.
178
179
 
179
180
  The model evaluates each question independently and in parallel against the same state.
180
181
 
@@ -191,33 +192,33 @@ A real response to the request above:
191
192
  "choice": "Office Supplies",
192
193
  "confidence": 1,
193
194
  "probabilities": {
194
- "Office Supplies": 1,
195
195
  "Cloud Hosting": 0,
196
- "Travel": 0,
197
- "Software Subscriptions": 0
196
+ "Software Subscriptions": 0,
197
+ "Office Supplies": 1,
198
+ "Travel": 0
198
199
  }
199
200
  },
200
201
  "already_recorded": {
201
202
  "type": "noul",
202
- "noul": 0.89
203
+ "noul": 0.91
203
204
  },
204
- "review_priority": {
205
+ "unusual": {
205
206
  "type": "score",
206
- "score": 0.09,
207
- "confidence": 0.86,
207
+ "score": 0.17,
208
+ "confidence": 0.74,
208
209
  "legend": {
209
- "0": "Routine: consistent with normal activity",
210
- "1": "Check: unusual but plausibly legitimate",
211
- "2": "Urgent: large, unapproved, or suspicious"
210
+ "0": "Routine: a common business expense",
211
+ "1": "Unusual: plausible, but uncommon for a business",
212
+ "2": "Suspicious: looks like fraud, an error, or a personal expense"
212
213
  },
213
214
  "probabilities": {
214
- "0": 0.91,
215
- "1": 0.09,
216
- "2": 0
215
+ "0": 0.9,
216
+ "1": 0.03,
217
+ "2": 0.07
217
218
  }
218
219
  }
219
220
  },
220
- "usage": { "input_tokens": 607, "output_tokens": 86 }
221
+ "usage": { "input_tokens": 614, "output_tokens": 87 }
221
222
  }
222
223
  ```
223
224
 
@@ -248,23 +249,96 @@ Decision errors use the shared [error envelope](#errors). Validation failures na
248
249
  }
249
250
  ```
250
251
 
251
- Besides `invalid_request`, decision validation returns `400` with `missing_model`, `unsupported_model`, `invalid_state`, `invalid_questions`, `invalid_question`, or `unsupported_question_type`. Fix the field named in `error.param`; do not retry unchanged. A `400` with `provider_rejected` means the model rejected the request: check its size against the model's limits.
252
-
253
- ### Decision caching
254
-
255
- Bkper caches decision results for 15 minutes per user. An identical request in that window returns the same answers without calling the model or consuming allowance, and repeats the original `usage`. Aliases of the same model share one cache entry. To get fresh answers, change the request or wait. See [Privacy, retention, and caching](https://bkper.com/docs/ai/models#privacy-retention-and-caching).
252
+ Besides `invalid_request`, decision validation returns `400` with `missing_model`, `unsupported_model`, `invalid_state`, `invalid_questions`, `invalid_question`, `unsupported_question_type`, or `unsupported_parameter`. The last two follow [Open System One](https://github.com/bkper/open-systemone/blob/9c454705012d1b659320340e4846a5824bd8e4fc/SPEC.md#errors): `unsupported_question_type` for a question type the selected model does not answer, and `unsupported_parameter` for a field the specification defines that the model cannot honor, such as `reference_date`. Fix the field named in `error.param`; do not retry unchanged. A `400` with `provider_rejected` means the model rejected the request: check its size against the model's limits. A `502` with `provider_invalid_answer` means the model's answers broke an [Open System One answer guarantee](https://github.com/bkper/open-systemone/blob/9c454705012d1b659320340e4846a5824bd8e4fc/SPEC.md#answer-guarantees); `error.message` names the question and the rule, and no answers are returned.
256
253
 
257
254
  ### TypeSafe compatibility
258
255
 
259
- Decision models come from TypeSafe's [System One](https://docs.typesafe.ai/concepts/system-one), and request and answer shapes match TypeSafe's [`POST /v1/systemone`](https://docs.typesafe.ai/api). TypeSafe's guidance on questions, state, confidence, and patterns applies unchanged. Only the surroundings differ:
256
+ Decision questions come from TypeSafe's [System One](https://docs.typesafe.ai/concepts/system-one), and request and answer shapes match TypeSafe's [`POST /v1/systemone`](https://docs.typesafe.ai/api). TypeSafe's guidance on questions, state, confidence, and patterns applies unchanged. Only the surroundings differ:
260
257
 
261
- - **Endpoint and authentication.** Call `https://ai.bkper.app/v1/decisions` with a Bkper access token. TypeSafe API keys do not work here.
262
- - **SDKs.** TypeSafe SDKs work with base URL `https://ai.bkper.app`, without `/v1`, and a Bkper access token. See [Add Bkper AI to an App](https://bkper.com/docs/platform/apps/ai#ask-a-decision-model).
258
+ - **Endpoint and authentication.** Call `https://ai.bkper.app/v1/systemone` with a Bkper access token. TypeSafe API keys do not work here.
259
+ - **SDKs.** TypeSafe SDKs work with base URL `https://ai.bkper.app`, without `/v1`, and a Bkper access token. See [Add Bkper AI to an App](https://bkper.com/docs/platform/apps/ai#ask-a-system-one-model).
263
260
  - **Errors.** Bkper returns `400` where TypeSafe returns `422`, and `503` with `provider_overloaded` where TypeSafe returns `529`. A `429` can also mean the Bkper AI allowance is exhausted.
264
261
  - **Usage.** Requests count against the Bkper AI allowance, not a TypeSafe account.
265
- - **Images.** Send images as base64 `assets` in a JSON request, as in TypeSafe's [Images](https://docs.typesafe.ai/images) guide. TypeSafe SDKs upload images as multipart, which Bkper does not accept, so send image requests over HTTP. Bkper turns on TypeSafe's experimental image input for you. Box questions are not supported. Image tokens count as input tokens.
262
+ - **Images.** Send images as base64 `assets` in a JSON request, as in TypeSafe's [Images](https://docs.typesafe.ai/images) guide. TypeSafe SDKs upload images as multipart, which Bkper does not accept, so send image requests over HTTP. Bkper turns on TypeSafe's experimental image input for you. Jev reads `png`, `jpg`, and `jpeg` images; `webp`, which `clef` and `clairic` read, returns `unsupported_parameter`. Box questions are not supported. Image tokens count as input tokens.
266
263
  - **Coding agents.** TypeSafe's [agent skill](https://docs.typesafe.ai/agent-skill) teaches questions and patterns. For a Bkper integration, also point the agent to this guide, `https://bkper.com/docs/api/ai-gateway.md`, for the endpoint, authentication, model IDs, and errors.
267
264
 
265
+ ## Extraction questions
266
+
267
+ Extraction questions to `POST /v1/systemone` ask for values read, computed, estimated, or forecast from the state instead of judgments: `text`, `number`, `date`, `time`, `datetime`, `duration`, `list`, `estimate`, and `forecast`. `clairic` is a Cloudflare preview (`preview: true` in `GET /v1/models`). Question and answer shapes follow [Open System One](https://github.com/bkper/open-systemone/blob/9c454705012d1b659320340e4846a5824bd8e4fc/SPEC.md#question-types).
268
+
269
+ This request reads a supplier email: it copies the invoice number, reads the issue date and payment terms, and estimates when the invoice will be paid. An extraction question can also ask for a computed value, such as a due date, but when a value feeds a Book, ask for its inputs and compute it in code.
270
+
271
+ ```bash
272
+ curl --fail-with-body https://ai.bkper.app/v1/systemone \
273
+ -H "Authorization: Bearer ${BKPER_TOKEN}" \
274
+ -H "Content-Type: application/json" \
275
+ -H "bkper-ai-source: my-app" \
276
+ --data '{
277
+ "model": "clairic",
278
+ "state": "Hi, this is Marta Silva from Acme Paper. Invoice AP-5531 for 3 boxes of A4 paper is attached: total $184.50, issued 2026-03-02 with net 30 terms. Delivery is booked for March 9 at 10:15 am, and unloading takes about 40 minutes. Our last four monthly invoices were $172.00, $178.25, $181.10 and $184.50.",
279
+ "reference_date": "2026-03-05",
280
+ "questions": {
281
+ "invoice_number": {"type": "text", "instructions": "Invoice number"},
282
+ "contacts": {"type": "list", "items": "text", "instructions": "Each person named"},
283
+ "total": {"type": "number", "decimals": 2, "unit": "USD", "instructions": "Invoice total"},
284
+ "issue_date": {"type": "date", "instructions": "Invoice issue date"},
285
+ "net_days": {"type": "number", "integer": true, "unit": "days", "instructions": "Net payment terms"},
286
+ "delivery_time": {"type": "time", "instructions": "Time the delivery is booked for"},
287
+ "delivery_at": {"type": "datetime", "instructions": "Date and time of the delivery"},
288
+ "unloading": {"type": "duration", "unit": "minutes", "integer": true, "instructions": "How long unloading takes"},
289
+ "monthly_amounts": {"type": "list", "items": "number", "decimals": 2, "instructions": "Each monthly invoice amount"},
290
+ "days_to_pay": {"type": "estimate", "min": 0, "max": 90, "step": 1, "instructions": "Days after issue until this invoice is paid"},
291
+ "next_invoices": {"type": "forecast", "min": 0, "max": 1000, "horizon": 2, "interval": "month", "unit": "USD", "instructions": "Amount of the next monthly invoices", "step": 0.01},
292
+ "po_number": {"type": "text", "instructions": "Purchase order number"}
293
+ }
294
+ }'
295
+ ```
296
+
297
+ ### Extraction request
298
+
299
+ - **Settings** are optional except `items` on lists and `min` and `max` (and `horizon` on forecasts) on estimates and forecasts: `null_threshold` on value types; `unit`, `integer`, and `decimals` on numbers and durations; `step` and `scale` on estimates and forecasts; `quantiles: true` on estimates for the 1st to 99th percentiles; `interval` on forecasts. An invalid setting returns `400` with `invalid_question` and the setting in `error.param`.
300
+ - **`instructions`** is optional. Structured state and instructions are read as compact JSON text.
301
+ - **`reference_date`** (`YYYY-MM-DD`) resolves relative and year-less dates, such as "March 9" above.
302
+ - **`clairic`** accepts question names of up to 64 letters, digits, `_`, `.`, or `-`, starting with a letter or `_`, forecasts of up to 64 steps, and up to 4 image `assets` (4 MiB each, 8 MiB in total). Requests beyond these limits return `unsupported_parameter`.
303
+
304
+ ### Extraction answers
305
+
306
+ A real response to the request above, with probabilities and means rounded:
307
+
308
+ ```json
309
+ {
310
+ "model": "clairic",
311
+ "answers": {
312
+ "invoice_number": {"type": "text", "value": "AP-5531", "guess": "AP-5531", "span": [49, 56], "p_null": 0, "confidence": 1},
313
+ "contacts": {"type": "list", "value": ["Marta Silva"], "spans": [[12, 23]], "confidence": 0.28},
314
+ "total": {"type": "number", "value": 184.5, "guess": 184.5, "p_null": 0, "confidence": 1},
315
+ "issue_date": {"type": "date", "value": "2026-03-02", "guess": "2026-03-02", "p_null": 0, "confidence": 1},
316
+ "net_days": {"type": "number", "value": 30, "guess": 30, "p_null": 0, "confidence": 1},
317
+ "delivery_time": {"type": "time", "value": "10:15:00", "guess": "10:15:00", "p_null": 0, "confidence": 1},
318
+ "delivery_at": {"type": "datetime", "value": "2026-03-09T10:15:00", "guess": "2026-03-09T10:15:00", "p_null": 0, "confidence": 1},
319
+ "unloading": {"type": "duration", "value": 40, "guess": 40, "p_null": 0, "confidence": 1, "unit": "minutes"},
320
+ "monthly_amounts": {"type": "list", "value": [172, 178.25, 181.1, 184.5], "confidence": 0.74},
321
+ "days_to_pay": {"type": "estimate", "value": 30, "median": 30, "p10": 24, "p90": 35, "mean": 29.78, "p_below": 0, "p_above": 0},
322
+ "next_invoices": {"type": "forecast", "steps": [{"value": 182.95, "median": 183.47, "p10": 171.24, "p90": 195.86, "mean": 182.95, "p_below": 0, "p_above": 0}, {"value": 183.74, "median": 184.1, "p10": 171.64, "p90": 196.86, "mean": 183.74, "p_below": 0, "p_above": 0}]},
323
+ "po_number": {"type": "text", "value": null, "guess": "AP-5531", "span": [49, 56], "p_null": 1, "confidence": 0.99}
324
+ },
325
+ "truncated": false,
326
+ "usage": { "input_tokens": 1337, "output_tokens": 0 }
327
+ }
328
+ ```
329
+
330
+ Bkper checks every answer against the [Open System One answer guarantees](https://github.com/bkper/open-systemone/blob/9c454705012d1b659320340e4846a5824bd8e4fc/SPEC.md#answer-guarantees) before returning it:
331
+
332
+ - Value types have `value`, `p_null`, and `confidence`. `value` is `null` exactly when `p_null` is above the question's `null_threshold` (default 0.5), as for `po_number`; `guess` is still the most likely value.
333
+ - `span` is where `guess` was read: a half-open `[start, end)` range of Unicode code point offsets into `state_text` when present, else into `state`. For `text`, it covers exactly the guess. Computed values, and values read from images, have a `null` or absent span.
334
+ - Lists are never `null` and keep input order. Text lists have `spans`, one entry per item: its span, or `null` for an item read from an image.
335
+ - Estimates and every forecast step are within `[min, max]`, on the `step` grid, with `p10` ≤ `median` ≤ `p90`. A forecast has exactly `horizon` `steps`, nearest first. Estimates have `quantiles` only when their question sets `quantiles: true`; forecast steps never do.
336
+ - `state_text`, the text the model read, is returned when it differs from `state`, for example for structured state. `truncated: true` means the model read only the start of the state.
337
+
338
+ ## System One caching
339
+
340
+ Bkper caches System One results, for decision and extraction questions alike, for 15 minutes per user. An identical request in that window returns the same answers without calling the model or consuming allowance, and repeats the original `usage`. Aliases of the same model share one cache entry. To get fresh answers, change the request or wait. See [Privacy, retention, and caching](https://bkper.com/docs/ai/models#privacy-retention-and-caching).
341
+
268
342
  ## Stream a response
269
343
 
270
344
  Set `stream` to `true` to receive semantic server-sent events:
@@ -362,7 +436,7 @@ Unsupported fields fail explicitly rather than being ignored or passed to only o
362
436
 
363
437
  ## Errors
364
438
 
365
- Errors use an Open Responses-shaped envelope with stable Bkper error codes. `POST /v1/responses` and `POST /v1/decisions` normalize provider failures the same way. Branch on `error.code`, not only on the HTTP status, because `429` has two meanings.
439
+ Errors use an Open Responses-shaped envelope with stable Bkper error codes. `POST /v1/responses` and `POST /v1/systemone` normalize provider failures the same way. Branch on `error.code`, not only on the HTTP status, because `429` has two meanings.
366
440
 
367
441
  | Status | Meaning | `error.code` examples | Retry |
368
442
  | ------ | ------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------- |
@@ -374,7 +448,7 @@ Errors use an Open Responses-shaped envelope with stable Bkper error codes. `POS
374
448
  | `429` | The monthly Bkper AI allowance is exhausted | `usage_limit_exceeded` | No |
375
449
  | `429` | The upstream provider is throttled | `provider_rate_limited` | Yes, with backoff |
376
450
  | `499` | The client aborted the request | `request_aborted` | — |
377
- | `502` | The selected upstream model provider or transport failed | `provider_error`, `provider_rejected` | A limited number of times |
451
+ | `502` | The selected upstream model provider or transport failed | `provider_error`, `provider_rejected`, `provider_invalid_answer` | A limited number of times |
378
452
  | `503` | The provider is overloaded or quota usage is temporarily unavailable | `provider_overloaded`, `usage_unavailable` | Yes, with backoff |
379
453
 
380
454
  When a provider sends `retry-after` with a `429` or `503`, Bkper AI forwards it; wait at least that long before retrying. `401` and `403` always refer to your Bkper token or account. Upstream provider credential problems surface as `502`.
@@ -393,7 +467,8 @@ Each operation has its own page. The machine-readable contract is [openapi.json]
393
467
 
394
468
  - [`POST /v1/responses` — Generate a response](https://bkper.com/docs/api/ai-gateway/operations/createresponse.md)
395
469
 
396
- ### Decisions
470
+ ### System One
397
471
 
472
+ - [`POST /v1/systemone` — Evaluate typed questions](https://bkper.com/docs/api/ai-gateway/operations/createsystemone.md)
398
473
  - [`POST /v1/decisions` — Evaluate typed questions](https://bkper.com/docs/api/ai-gateway/operations/createdecision.md)
399
474
 
@@ -111,6 +111,7 @@ the totals you checked.
111
111
 
112
112
  After your `instructions`, the agent reads its context: an `AGENTS.md` and skills (`.agents/skills/<name>/SKILL.md`) that people, the agent, or any other agent following the [AGENTS.md](https://agents.md/) and [Agent Skills](https://agentskills.io/specification) standards keep in a context repository per scope. A session loads the user's company's (their verified domain's, when they have one: shared by everyone in the domain, changed only by its admin), then the user's own, then its Book's (from its first input's `bookContext`, shared by everyone who can view the Book). Each is more specific than the one before and wins: a Book's skills replace the user's of the same name, and the user's the company's. It loads when the session starts and again at the start of each turn (an input that is not a steer or follow-up: a run never changes its context midway), each scope from the sandbox's working copy when the sandbox is running and the user or agent changed it there (local changes win, as on a local machine), else as published: the session's `contextScopes` show what loaded. When it changed, the agent is told so with that input. A fork starts with what its parent loaded.
113
113
  It loads as the person in every workspace, an App's included (such as a Slack bot's): permissions are the person's. Context of other Books never loads; the agent clones one only when asked.
114
+ `GET /v1/skills?bookId=` lists the skills a session starting now could call by name, as published, before there is one.
114
115
 
115
116
  - Loading never delays a session by more than about two seconds: at the start, a scope that is slow or fails is skipped and shown as `unavailable`, never as an error in the conversation; at a turn, the session keeps all of what it had.
116
117
  - Context guides; it never grants anything. The user's messages override it, and it cannot relax Bkper's base.
@@ -537,6 +538,7 @@ Each operation has its own page. The machine-readable contract is [openapi.json]
537
538
  - [`GET /v1/sessions/{sessionId}/entries/{entryId}` — Get a full entry](https://bkper.com/docs/api/managed-agent/operations/getentry.md)
538
539
  - [`POST /v1/sessions/{sessionId}/compact` — Compact earlier context](https://bkper.com/docs/api/managed-agent/operations/compactsession.md)
539
540
  - [`GET /v1/sessions/{sessionId}/export` — Export a session](https://bkper.com/docs/api/managed-agent/operations/exportsession.md)
541
+ - [`GET /v1/skills` — List skills to call](https://bkper.com/docs/api/managed-agent/operations/listskills.md)
540
542
 
541
543
  ### Inputs
542
544
 
@@ -6,7 +6,8 @@ Model output is a suggestion, not permission to change a Book. Keep decisions ab
6
6
 
7
7
  ## Choose the kind of response
8
8
 
9
- - **A yes/no, a choice, or a level on a scale?** Ask a decision model, such as `jev`, with TypeSafe's SDK, `@typesafe-ai/sdk`.
9
+ - **A judgment: a yes/no, one of known options, or a level on a scale?** [Ask a System One model](#ask-a-system-one-model) a decision question, with a model such as `jev`.
10
+ - **A value read from a document, such as an invoice total, or a likely range for an uncertain number, such as days until an invoice is paid?** [Ask a System One model](#ask-a-system-one-model) an extraction question, with a model such as `clairic`.
10
11
  - **Text or a custom JSON object?** Ask a language model (LLM), such as `gpt-luna`, with AI SDK's Open Responses provider, `@ai-sdk/open-responses`.
11
12
  - **A conversation that works on Books?** Reading and comparing Books, computing with scripts, delivering files, remembering earlier messages: [run an agent](#run-an-agent) with the Managed Agent API instead of calling a model.
12
13
 
@@ -35,10 +36,19 @@ The client can call Bkper AI directly with `auth.authenticatedFetch()`, which se
35
36
 
36
37
  In both cases usage counts against the user's allowance. See [Call from a browser](https://bkper.com/docs/api/ai-gateway.md#call-from-a-browser) for a request example.
37
38
 
38
- ## Ask a decision model
39
+ ## Ask a System One model
40
+
41
+ Every System One question, decision or extraction, goes to `POST /v1/systemone`. There are two ways to send it:
42
+
43
+ - **TypeSafe's SDK**, `@typesafe-ai/sdk`, types decision questions and their answers for you. It does not send extraction questions or images.
44
+ - **`fetch`** sends any question type, and images.
45
+
46
+ Bkper AI validates every answer against its question before responding, so your code can use it directly. Keep the questions, the thresholds, and the function that turns answers and facts into an action in one module.
47
+
48
+ ### With TypeSafe's SDK
39
49
 
40
50
  ```ts
41
- import { score, TypeSafeClient } from '@typesafe-ai/sdk';
51
+ import { noul, TypeSafeClient } from '@typesafe-ai/sdk';
42
52
 
43
53
  const decisionClient = new TypeSafeClient({
44
54
  apiKey: 'bkper-platform-outbound',
@@ -46,26 +56,69 @@ const decisionClient = new TypeSafeClient({
46
56
  defaultModel: 'jev',
47
57
  });
48
58
 
49
- export async function scoreRecurring(description: string) {
59
+ export async function recurringProbability(description: string): Promise<number> {
50
60
  const { answers } = await decisionClient.systemOne({
51
61
  state: { description },
52
62
  questions: {
53
- recurring: score('How likely is this a recurring charge?', [
54
- 'Unlikely',
55
- 'Possible',
56
- 'Likely',
57
- ]),
63
+ recurring: noul('Is this a recurring charge, such as a subscription or a monthly bill?'),
58
64
  },
59
65
  });
60
- return answers.recurring;
66
+ return answers.recurring.noul;
61
67
  }
62
68
  ```
63
69
 
64
70
  - **`apiKey`** is required by the SDK. In a Platform Worker, pass any placeholder: platform outbound replaces it with the user's authorization.
65
71
  - **`baseURL`** has no `/v1`. The SDK calls `POST /v1/systemone`, which behaves exactly like `POST /v1/decisions`.
66
- - **The answer is typed** from the question. Bkper AI validates every answer against its question before responding, so your code can use it directly.
72
+ - **The answer is typed** from the question: here, `noul` is the probability of yes, which your code compares with a threshold.
73
+
74
+ ### With fetch
75
+
76
+ ```ts
77
+ interface ValueAnswer {
78
+ value: T | null;
79
+ guess?: T;
80
+ p_null: number;
81
+ confidence: number;
82
+ }
83
+
84
+ interface InvoiceReading {
85
+ model: string;
86
+ answers: { total: ValueAnswer<number>; issue_date: ValueAnswer<string> };
87
+ truncated?: boolean;
88
+ }
89
+
90
+ interface BkperAiErrorBody {
91
+ error: { code: string; message: string };
92
+ }
93
+
94
+ export async function readInvoice(email: string): Promise {
95
+ // No Authorization header: the platform adds the user's.
96
+ const response = await fetch('https://ai.bkper.app/v1/systemone', {
97
+ method: 'POST',
98
+ headers: { 'Content-Type': 'application/json' },
99
+ body: JSON.stringify({
100
+ model: 'clairic',
101
+ state: email,
102
+ questions: {
103
+ total: { type: 'number', decimals: 2, instructions: 'Invoice total' },
104
+ issue_date: { type: 'date', instructions: 'Invoice issue date' },
105
+ },
106
+ }),
107
+ });
108
+ if (!response.ok) {
109
+ const { error } = (await response.json()) as BkperAiErrorBody;
110
+ throw new Error(`${error.code}: ${error.message}`);
111
+ }
112
+ // Bkper AI validated each answer against its question.
113
+ return (await response.json()) as InvoiceReading;
114
+ }
115
+ ```
116
+
117
+ - **`value` can be `null`** when the email probably lacks it. Send such items, and any response with `truncated: true`, to a person with the `guess`.
118
+ - **The model reads; code checks.** Compare the total with the invoice lines, and compute dates such as the due date, before anything reaches a Book.
119
+ - **Images** go in the same request, as base64 `assets`.
67
120
 
68
- [Decision Models](https://bkper.com/docs/ai/decision-models.md) covers designing the questions, facts, and thresholds; the [AI Gateway API](https://bkper.com/docs/api/ai-gateway.md#decision-models) covers the request, answers, errors, and caching. The open-source [Merge Duplicates app](https://github.com/bkper/bkper-apps/tree/main/merge-duplicates) uses this setup to suggest duplicate pairs for human review.
121
+ [System One Models](https://bkper.com/docs/ai/system-one-models.md) covers designing the questions, facts, and thresholds, and every question type; the [AI Gateway API](https://bkper.com/docs/api/ai-gateway.md#decision-questions) covers the request, answers, errors, and caching. The open-source [Merge Duplicates app](https://github.com/bkper/bkper-apps/tree/main/merge-duplicates) uses TypeSafe's SDK to suggest duplicate pairs for human review.
69
122
 
70
123
  ## Generate a language response
71
124
 
@@ -175,6 +228,7 @@ Every Bkper AI error has the same envelope: `{ error: { message, type, param, co
175
228
 
176
229
  - **TypeSafe SDK:** an `APIError` with `status` and the envelope in `body`, so read `error.body.error.code`. Network failures and timeouts throw `APIConnectionError`.
177
230
  - **AI SDK:** an `APICallError` with `statusCode` and the envelope in `data`, so read `error.data.error.code`. Output that does not match your schema throws `NoObjectGeneratedError`.
231
+ - **`fetch`:** a response that is not `ok` has the envelope as its JSON body. `fetch` does not retry.
178
232
 
179
233
  Both SDKs retry rate limits and server errors twice by default. A failed attempt costs nothing: Bkper AI charges only successful answers.
180
234
 
@@ -184,7 +238,7 @@ Map these errors to your route's typed error response. Keep the code and message
184
238
 
185
239
  - **Passing your own `fetch`.** The TypeSafe SDK calls `fetch` as its own method, which the Workers runtime rejects with `Illegal invocation`. Wrap it: `fetch: (input, init) => myFetch(input, init)`. Without the option, the SDK's default works.
186
240
  - **No `null` values.** The TypeSafe SDK's types accept `null` for state, instructions, and some criteria. Bkper AI rejects them with `400`.
187
- - **No SDK image uploads.** The TypeSafe SDK uploads images as multipart, which Bkper AI rejects with `400`. Send image requests with `fetch` as JSON, with base64 `assets`. See [Decision Models](https://bkper.com/docs/ai/decision-models.md#jev-by-typesafe).
241
+ - **No SDK image uploads.** The TypeSafe SDK uploads images as multipart, which Bkper AI rejects with `400`. Send image requests [with `fetch`](#with-fetch) as JSON, with base64 `assets`. See [System One Models](https://bkper.com/docs/ai/system-one-models.md#models) for each model's image limits.
188
242
  - **No `client.models.list()`.** It expects TypeSafe's catalog shape. Use the [`GET /v1/models`](https://ai.bkper.app/v1/models) catalog.
189
243
  - **Other System One clients** take `https://ai.bkper.app/v1` as their base URL.
190
244
  - **Flexible JSON schemas.** Open Responses always requests `strict: true`. For a schema that needs `strict: false`, such as a typed dynamic map, use `@ai-sdk/openai` with base URL `https://ai.bkper.app/v1`, its `.responses()` model, and `strictJsonSchema: false`.
@@ -193,11 +247,12 @@ Map these errors to your route's typed error response. Keep the code and message
193
247
 
194
248
  - **Send only task-relevant data.** Validate input and Book permissions at the app API boundary.
195
249
  - **Keep writes deterministic.** An answer or generated text must not create, merge, or alter a Transaction without application rules and any required human confirmation.
250
+ - **Test the decision without the model.** Unit-test the function that turns answers and facts into an action with fixed answers, just below and just above each threshold.
196
251
  - **Test the service.** Mock `fetch` and check what is sent, that the Worker adds no user token, and that errors stay actionable. Then run the app's normal check and build.
197
252
 
198
253
  ## Next steps
199
254
 
200
- - [Decision Models](https://bkper.com/docs/ai/decision-models.md): questions, answers, thresholds, and caching.
255
+ - [System One Models](https://bkper.com/docs/ai/system-one-models.md): decision and extraction questions, answers, and thresholds.
201
256
  - [Models and Usage](https://bkper.com/docs/ai/models.md): models, usage rates, the allowance, and privacy.
202
257
  - [AI Gateway API](https://bkper.com/docs/api/ai-gateway.md): tokens, raw HTTP requests, capabilities, and errors.
203
258
  - [Managed Agent API](https://bkper.com/docs/api/managed-agent.md): sessions, instructions, files, and streams.
@@ -113,8 +113,9 @@ The following variables can be used in `menuUrl` and `menuUrlDev`:
113
113
  | `${group.id}` | Selected group ID |
114
114
  | `${group.name}` | Selected group name |
115
115
  | `${group.properties.xxx}` | Group property value |
116
- | `${transactions.ids}` | Comma-separated selected transaction IDs |
116
+ | `${transactions.ids}` | Space-separated selected transaction IDs |
117
117
  | `${transactions.query}` | Current search query |
118
+ | `${app.state}` | The App's state in the Book URL, URL-encoded; empty without one. See [Keeping state in the Book URL](https://bkper.com/docs/platform/apps/context-menu.md#keeping-state-in-the-book-url). |
118
119
 
119
120
  ### Event handling
120
121