Guidance for coding agents (and humans) working on typesafer, an unofficial R client for the TypeSafe System One API.
References
- API spec: https://docs.typesafe.ai/api.md (docs index: https://docs.typesafe.ai/llms.txt)
- Reference SDK: https://github.com/typesafe-ai/typesafe-sdk-python/tree/main (
src/typesafe_sdk). When behavior is unspecified (retries, error message extraction), match it. - Design inspiration: ellmer (S7 types, httr2).
Layout
| File | Contents |
|---|---|
R/questions.R |
ts_noul(), ts_choice(), ts_score(): S7 classes, normalization, validators, as_wire() serialization |
R/answers.R |
ts_response and ts_answer_* classes, parse_response(), print methods |
R/request.R |
ts_api_key(), base URL, ts_request() (auth, timeout, retry policy), ts_perform()
|
R/errors.R |
Classed error construction from httr2 responses and failures |
R/system_one.R |
system_one() and shared argument checks |
R/system_one_df.R |
system_one_df(): one request per row via httr2::req_perform_parallel()
|
R/models.R |
ts_models() (GET /v1/models) |
R/columns.R |
Answer and usage columns shared by system_one_df() and as_tibble(<ts_response>)
|
R/utils.R |
abort_input(), ts_json()
|
data-raw/mocks.R |
Regenerates the httptest2 fixtures in tests/testthat/mocks/
|
Commands
Rscript -e 'devtools::document()' # after changing roxygen comments
Rscript -e 'devtools::test()' # no API key or network needed
Rscript -e 'devtools::check()' # must end 0 errors | 0 warnings | 0 notes
Rscript data-raw/mocks.R # after an intentional serialization changeDon’t edit NAMESPACE or man/ by hand; they’re generated by roxygen2.
devtools::check() and pkgdown need Pandoc to check README.md and NEWS.md. Without it, check reports a note. If Pandoc isn’t on the PATH, use the copy bundled with Quarto, for example RSTUDIO_PANDOC=/Applications/quarto/bin/tools/aarch64 on macOS.
README.md is generated from README.qmd. Edit the .qmd, then run quarto render README.qmd and commit both files. Rendering runs the examples against the live API, so it needs TYPESAFE_API_KEY. The rendered output must never contain the key.
Conventions
- Dependencies: Imports are httr2, jsonlite, S7, rlang, cli, tibble (plus base stats/utils). Add new ones only with a strong reason; test-only packages go in Suggests.
-
S7: classes are declared with
package = "typesafer". Constructors normalize input and raisetypesafer_error_inputviaabort_input(); validators return a string (orNULL) so they also run on@<-.S7::methods_register()is called in.onLoad(). -
Errors: every error inherits from
typesafer_error. Useabort_input()for argument problems. HTTP errors are built inhttp_error_cnd():-
typesafer_error_auth: missing key or 401 -
typesafer_error_validation: 422, with adetailstibble -
typesafer_error_rate_limit: 429 or 529 -
typesafer_error_server: other 5xx - all of the above also inherit
typesafer_error_http -
typesafer_error_connection: no response
response_bodyfield, because rlang reservesbody. Don’t interpolate server text through cli: pre-format the bullets withrlang::format_error_bullets(). -
-
cli messages: in
{?s}pluralization, pass the count explicitly with{cli::qty(length(x))}. Wrap.dataas{(.data)}. -
Shared columns:
system_one_df()andas_tibble(<ts_response>)must produce identical columns. Change both throughR/columns.R, and keep the test that compares them passing. -
Defaults that the Python SDK takes from the environment come from helper functions:
ts_api_key()(TYPESAFE_API_KEY) andts_default_model()(TYPESAFE_DEFAULT_MODEL).local_ts_env()clears both, plusTYPESAFE_BASE_URL, so the mock fixtures match. -
Retries mirror the Python SDK: 408, 429 and 5xx plus connection failures;
max_tries = 3; 30s budget; backoff 0.5s doubling up to 5s with 25% jitter;retry-after-msbeforeretry-after. Keep HTTP error handling on in httr2, becausereq_perform_parallel()only retries responses it treats as errors. - Scores stay on the API’s 0-based scale. Never shift them to 1-based.
JSON gotchas
All request bodies go through ts_json() (auto_unbox = TRUE, null = "null"), sent with httr2::req_body_raw(), so tests can assert the exact bytes.
- Choice options without a description must serialize as
null, never{}. Usex[i] <- list(NULL)to keep them (x[[i]] <- NULLdrops them). - Score criteria must always be a JSON array: keep them as an unnamed list.
- Noul criteria keys are the strings
"true"and"false". - Score
probabilitiesandlegendarrive keyed"0","1", and so on. Order them numerically (by_level()), not lexically, because"10"sorts before"2".
Testing
-
httptest2 fixtures: the
mocks/fixtures are built from the spec examples. File names hash the exact request body, so a “mock not found” failure means the serialization changed. Rerundata-raw/mocks.Ronly if the change is intended. -
Mocked errors: use
httr2::local_mocked_responses()(seejson_response()inhelper.R). -
Retries: httr2 mocking bypasses the retry loop, so retry behavior is tested against a local webfakes server in
test-retry.R. Those tests are skipped when webfakes isn’t installed. -
Live tests:
test-live.Rcalls the real API throughskip_if_no_live_api(). They run locally only whenTYPESAFE_API_KEYis set, and are skipped on CRAN and CI. Assert structure and clear-cut semantics only, keep the calls few and small, and never print the key. -
Helpers: call
local_ts_env()to use a fake key and the default base URL, andlocal_no_backoff()to make retries instant. -
Snapshots: error and print output are snapshot-tested. Review changes in
_snaps/rather than accepting them blindly.
Secrets
- Never put
TYPESAFE_API_KEY, or any part of it, in a shell command, a command-line argument, or printed output. Tools echo commands back. For example, R’ssystem2()prints the whole command line in a warning when the command exits with a non-zero status. - To check whether the key appears somewhere (git history, a rendered README, logs), read the text into R and compare in-process, printing only a boolean or a count:
grepl(Sys.getenv("TYPESAFE_API_KEY"), text, fixed = TRUE). - After rendering
README.qmd, confirm thatREADME.mddoesn’t contain the key before committing.
