Repository navigation
Add POST /v1/systemone: typed decisions on Continuum GLM-5.3-Flash (MVP) - #1045
Conversation
a4a1010 to
979bab2
Compare
benthecarman
left a comment
There was a problem hiding this comment.
the core approach checks out: the token table matches the published GLM-5.3-Flash tokenizer (all 191 ids), the prefill renders as intended through GLM's chat template, and the request fields and score/confidence math match Privatemode's reference implementation (edgelesssys/privatemode-decisions).
things i'd want addressed before merge:
- probabilities aren't calibrated, and raw one-token probabilities are overconfident (L449)
- state-first prompt layout, which the reference dropped for scoring worse (L284)
- no check that every requested logprob came back, so a missing option silently reads as 0 (L424)
- assumes the default proxy is Continuum, and skips health gating (L567, L646)
- every upstream 4xx becomes a 503 (L673)
- a failed question doesn't stop the rest, and the user is billed for partial work (L591)
- limits multiply, and there's no deadline for the whole request (L48, L55)
- caller-supplied question names go into logs (L215)
comments marked nit: are optional.
| question: format!( | ||
| "{} The options are ordered from lowest to highest.", | ||
| render_text(instructions) | ||
| ), |
There was a problem hiding this comment.
the score lowering (level_N labels with the legend as the description, plus "ordered from lowest to highest") isn't something the reference benchmarked, so the accuracy and calibration numbers don't cover it. probably fine, but worth a small eval before relying on it
There was a problem hiding this comment.
Agreed, not benchmarked. Kept as is (level_<n> labels with the level text as the description, plus the ordered-levels sentence); it was exact on the small severity check, but a proper eval against labeled ordinal data is a follow-up. Leaving open as the reminder.
548c290 to
8716cb0
Compare
d85265f to
29e6af4
Compare
A TypeSafe-compatible System One endpoint: a request carries a state and typed noul / choice / score questions (up to 64; choices up to 255 options, scores 2-10 levels; up to 4 images), and the handler answers each with one masked single-token chat completion against Continuum's GLM-5.3-Flash, returning calibrated probabilities, confidence and score expectations. Provider and model are fixed on purpose, the completion router is bypassed, and the chat API is untouched. Decoding follows the Privatemode reference implementation (45f4273) so its benchmark and calibration numbers apply: question-first prompt layout rendered byte-identically with Python's json.dumps separators, the `answer=` prefill, allowed_token_ids over the option-number tokens, logprob_token_ids read in batches of 128, and a default calibration temperature by option count (ln T = 0.962 - 0.076 ln n) with a per-request override; answers carry `type`, `temperature` and `option_mass`, and Score answers are keyed by level number as TypeSafe's are. Options are labelled with single-token numbers (0..190, then the next single-token numbers up to 480 for the 255th), measured against the serving tokenizer and re-verified at runtime every 10 minutes with one batched /v1/completions echo. That check never holds its lock across the probe, bounds the probe to 30 s, keeps its last verdict when a probe cannot conclude, and disables the endpoint on a mismatch rather than returning plausible wrong answers. Operationally the endpoint behaves like chat: the same caller authorization and entitlement through shared helpers rather than copies, usage published per upstream request, the Continuum route taken from the registry through ProxyRouter::continuum_proxy() with a startup warning when it is absent, every upstream send claiming the route's health probe and reporting its terminal, upstream failures classified with chat's helpers (429/503 as capacity errors with capped retry-after, 4xx keeping their status), the first failing question stopping the rest (no partial answers; consumed usage stays billed), typed 422/413 validation errors with fixed codes and messages, caller-ordered options and questions with duplicates rejected, logs naming questions by index and kind only, and limits with a total prompt budget, per-read and per-request deadlines and a process-wide upstream permit count. Also allowlists the path in the Rust SDK's inference endpoints (with a test) and registers it in the standalone proxy's router, banner and README; the proxy forwards the path once it picks up an SDK release containing the allowlist change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
f337d2e to
fe0e93a
Compare
|
SDK and proxy support should be included in this PR before we offer the experimental API.
No Research or Agent classifier-workload integration is needed for this scope. The goal is a usable public SDK/API and proxy surface, rather than requiring callers to hand-build raw requests. |
|
Added in 0ce4e9e (second commit on the PR):
Verified locally against the jevlab stack: both SDK integration suites green with the Pro fixture (422 paths with both auth methods, full three-question answers), and the tree-built proxy answering live plus passing a 422 through unchanged. Rollout per |
0ce4e9e to
ac17ed4
Compare
|
Update: the client surface is now the single second commit ac17ed4 (0ce4e9e folded in), with two changes on top of the summary above:
|
Expose the endpoint so callers do not hand-build requests. The Rust SDK
gains `system_one` / `system_one_with_api_key` with typed request and
answer models (`OrderedEntries` keeps question and option order on the
wire and in answers), the `images` and `temperature` extensions, and
`Error::api_status` / `Error::api_error_code` for the backend's status
and `system_one_*` code; an example and an integration test cover both
credentials and all three answer types. The TypeScript SDK gains
`systemOne(request, { apiKey? })` with the same types, exported from the
package and on `useOpenSecret()`, throwing `SystemOneError { status,
code }` for backend rejections; its dependency-injected variant lives in
a non-public module so the rolled-up package types stay free of
transport internals. Unit tests run on a mocked transport and the AI
integration suite gains the live case.
Maple's own consumers build the in-tree Rust SDK from now on: `proxy`,
`apps/maple-agent` and the Research native shell take maple-sdk from
sdk/rust through a path dependency plus a root `[patch.crates-io]`
entry (so the embedded proxy's registry requirement resolves to the
same source) with regenerated lockfiles. SDK changes are usable here as
soon as they merge; publication stays a separate action for external
users, and docs/sdk-publishing.md, the SDK README and the proxy README
say so. POST /v1/systemone is thus served by the standalone proxy and
by both desktops' embedded proxies as built from this repository; proxy
tests cover the route and the pass-through of 422 status, body and
error-code headers.
Because those consumers now build sdk/rust from the tree, Rust SDK
build inputs (Cargo.toml, src/, build.rs, assets/) select their CI
again: the Research classifier routes them to the native lanes, the
Agent selector follows, and the proxy, Rust-tests and supply-chain
workflows list them in their path filters, with the selector tests and
guidance updated. TypeScript SDK changes still select nothing, since
the frontends pin a published package.
Backend: System One rejections carry `error: {message, code}` in the body
like the plan and quota errors, and answers and choice distributions are
serialized in caller order (`serde_json::Map` had sorted them).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ac17ed4 to
50c292c
Compare
Summary
First-pass MVP of a TypeSafe-compatible "System One" decisions endpoint,
POST /v1/systemone, on OpenSecret.A request carries a
state(string or JSON) and typedquestions(noul,choice,score, each withinstructionsandcriteria, mirroring TypeSafe's SDK). The handler answers every question with one masked, single-token chat completion against Continuum's GLM-5.3-Flash: the prompt numbers the options, the assistant turn is prefilled withanswer=, sampling is restricted to the option-number tokens (allowed_token_ids), and the distribution is read from their log-probabilities (logprob_token_ids, batched at Privatemode's 128-id cap and merged). The model never generates text. Answers carrytypeand come back TypeSafe-shaped:noul(P(true));choice+ probabilities + confidence;score(expected level index) + legend + distribution + confidence, both keyed by level number. Each answer also reports the calibrationtemperatureandoption_mass; usage reports the upstream request count.Prompt layout, prefill and calibration follow Privatemode's reference implementation (edgelesssys/privatemode-decisions at 45f4273) so its benchmark and calibration numbers apply: question-first layout rendered byte-identically (Python
json.dumpsseparators, state embedded as the caller's raw bytes), and a default temperature by option count (ln T = 0.962 − 0.076 ln n;"temperature": 1in the request returns raw probabilities).Deliberate MVP choices, per discussion:
glm-5-3-flash, the only deployment that honors the masked logprob read (Tinfoil's GLM-5.3-Flash returns HTTP 500 onlogprob_token_ids). The completion router is bypassed on purpose instead of inheriting the caller's Router V2 bucket. Route health is still used: every upstream send claims the route's probe and reports its terminal, so an open breaker rejects up front and our 429/5xx feed the shared state.authorize_completion_caller).x-opensecret-error-code(system_one_*) and message. Note that one 64-question call is 64 upstream requests on the shared Continuum key, throttled to 16 at a time; the cap is a one-line change if that ever matters.0–190contiguously, then the next single-token numbers (192,195, …480for the 255th), measured against Continuum's serving tokenizer (0–190 on 2026-09-25 and 2026-10-06, 0–1199 on 2026-10-07; the published tokenizer checked by review) and re-verified at runtime every 10 minutes with one batched/v1/completionsecho (bounded to 30 s, and no lock is held across it: callers that arrive mid-check go by the last verdict); a mismatch disables the endpoint until a later check passes rather than returning plausible wrong answers, and a check that cannot conclude (429, timeout, malformed body) keeps the previous verdict instead of overwriting it. Requests with ≤191 options are unaffected by the sparse tail.images(data URLs shared by all questions; the first question is sent alone to seat the shared prefix) andtemperature.Also:
maple-sdk(Rust):POST /v1/systemoneadded to the inference-endpoint allowlist, with a test. The standalone proxy pins the published crate, so it forwards the new path only after it picks up an SDK release containing this change (local verification used an uncommitted path override).proxy: route registered and listed in the banner/README.openai.rs: the caller authorization block, the non-2xx response classifier, and the usage/retry-after helpers are shared with the new handler instead of copied.Verification
services/opensecret:cargo fmt --check,cargo clippy --locked --all-targets --all-features -D warnings,cargo test --locked --all-features: 746 passed, 0 failed, 31 ignored (14 System One unit tests: schema and caller-order lowering, every validation rule, prompt bytes vs the reference layout, Python-separator formatter, upstream body, read planning, decode with missing ids / no logprobs, calibration and the temperature formula, answer shapes, usage totals, entitlement, upstream error mapping, deadline).proxy:cargo test --locked: passing.sdk/rust: allowlist unit test passing."temperature": 1):is_urgent0.954 vs 0.999,refund_requested0.065 vs 0.003,departmentbilling 0.985 vs 1.000 (T = 2.48 for 2 options, 2.41 for 3);option_mass0.98–1.00. The first call after startup includes the token-table probe (4.5 s); the same request again took 0.39 s.activityfinance 0.980,sensitive0.922; the first question is sent alone to seat the image prefix.model,temperature: 0, and a non-data image each return 400 withx-opensecret-error-codesystem_one_bad_option_count/system_one_unsupported_model/system_one_bad_temperature/system_one_image_not_data_urland a fixed message.answers,probabilities) are JSON objects whose key order is not significant; option numbering in the prompt follows the caller's order.Review round (benthecarman, 2026-10-06)
All eight "before merge" items are addressed in the second commit (calibration, question-first layout, missing-logprob detection, Continuum lookup + health gating, 4xx mapping, short-circuit + billing policy, limits + deadline + global concurrency, no caller strings in logs), plus the nits except where noted on the threads. Not done here, by choice: handler-level HTTP tests of the billing branches (they need a Postgres-backed AppState; the branches now live in
completion_entitlement, which is unit-tested), asend_billed_completionrefactor (billing stays inpublish_usage_event_internalwith two documented call sites), and an eval of the score lowering.Post-review tweaks (2026-10-07)
Caps raised to match the field (64 questions as Clef; 255 options as Jev, Clef and Perplexity) and schema validation moved to 422 as TypeSafe's API. Contract survey against Jev, Clef, OpenAI's Decisions API (public beta 2026-10-06), Perplexity, Liquid, OpenRouter and others: the request/response schema here is the one every System One-compatible provider uses; OpenAI's
/v1/decisionsis the lone outlier (arrays,predicate, typed values).option_mass0.52 vs ~0.98 on small option sets, which is the prompt-fit signal that field exists for); 65 questions, a provider id asmodel, and a 1-option choice return 422 with their codes before any upstream call; an oversized state returns 413; a 2-question request answers as before. The runtime check logged the 255-entry token table as verified against the serving tokenizer. After the table-check fix (bounded probe, verdict kept): the first request after a backend restart probes and logs the table as verified (HTTP 200 in 4.0 s); the next one answers on the fresh verdict in 0.48 s without re-probing.Follow-ups (not in this PR)
maple-sdkwith the allowlist entry and bump the proxy's pin so the standalone proxy forwards/v1/systemone.system_one()helpers (Rust, TypeScript) and a code-docs spec for the wire shape.permutationsfor position bias.Client surface (second commit)
Rust SDK (
maple-sdk4.1.0, unpublished):system_one/system_one_with_api_keywith typedSystemOneRequest/SystemOneQuestion(noul, choice, score) /SystemOneResponse/SystemOneAnswermodels;OrderedEntrieskeeps question and option order on the wire and in answers;imagesandtemperatureextensions and thetemperature/option_mass/ usage fields are modelled;Error::api_status/Error::api_error_coderead the backend's status andsystem_one_*code. Exampleexamples/system_one.rs; unit tests for order and shapes;tests/system_one_integration.rs(422 with JWT, per-request key and configured key; typed answers when a Continuum route exists, typed 503/403 otherwise).TypeScript SDK (
@mapleai/sdk4.2.0, unpublished):systemOne(request, { apiKey? })with the same types, exported from the package and onuseOpenSecret(); rejections throwSystemOneError { status, code }. Unit tests on a mocked transport (order, both credentials, 422 and 403 mapping, pass-through of transport failures); integration case inai.test.ts.Backend:
system_one_*rejections now also carryerror: {message, code}in the body (like the plan/quota errors), so every client sees the code without reading headers. Answers and choice distributions are now serialized in caller order (answersin question order,probabilitiesin option order);serde_json::Maphad been sorting them alphabetically.Standalone proxy and both desktops:
proxy,apps/maple-agentand the Research native shell now build the in-treemaple-sdk(path dependency plus a root[patch.crates-io]entry so the embedded proxy's registry requirement resolves to the same source; lockfiles regenerated and verified withcargo metadata --locked). The route is therefore served by the standalone proxy and by both desktops' embedded proxies as built from this repository. Tests: the route forwards method, body and key; 422 rejections keep status, body andx-opensecret-error-*headers.CI routing for the in-tree SDK: since the proxy and both desktops now build
sdk/rustfrom the tree, Rust SDK build inputs (Cargo.toml,src/,build.rs,assets/) select their CI again —change_detection.pyroutes them to the native lanes,agent_change_detection.pyfollows, and the proxy, Rust-tests and supply-chain workflows list them in their path filters (selector tests + guidance updated; the "accepted gap" from ci: stop running client CI for SDK-only changes #1039 is closed). TypeScript SDK changes still select nothing because the frontends pin a published package.Policy (Anthony, 2026-10-07): Maple's own consumers use the in-tree SDK sources and do not wait for SDK publication or version bumps;
docs/sdk-publishing.md(consumer version policy) and the proxy README say so now. Publishingmaple-sdk4.1.0 /@mapleai/sdk4.2.0 remains a separate action for external SDK users. The Research frontend's@mapleai/sdkpin is unchanged (it does not need the helper; switching it tofile:../../../sdkis the documented one-command change when wanted).🤖 Generated with Claude Code