clone

an unofficial pure go sdk for the typesafe api: typed answers, retries and no dependencies

main
typesafe-go / README.md
README.md 7.2K rendered
1# typesafe-go23An unofficial, community-maintained Go SDK for the hosted [TypeSafe](https://typesafe.ai/) API.4It is an independent project by FelineStateMachine, with no official affiliation5with or endorsement from TypeSafe. The SDK sends state and questions to the6hosted service; it does not run inference locally.78The module is pure Go and uses only the standard library.910```sh11go get github.com/FelineStateMachine/typesafe-go@v0.2.012```1314Documentation: [pkg.go.dev/github.com/FelineStateMachine/typesafe-go](https://pkg.go.dev/github.com/FelineStateMachine/typesafe-go)1516## Quickstart1718Set `TYPESAFE_API_KEY` and create a client. `NewClient` also accepts19`WithAPIKey` when applications prefer explicit configuration.2021```go22package main2324import (25 "context"26 "fmt"27 "log"2829 "github.com/FelineStateMachine/typesafe-go"30)3132func main() {33 client, err := typesafe.NewClient()34 if err != nil {35 log.Fatal(err)36 }3738 result, err := client.SystemOne(context.Background(), typesafe.SystemOneRequest{39 State: map[string]any{"text": "The package has tests and documentation."},40 Questions: map[string]typesafe.Question{41 "is_ready": typesafe.NoulQuestion{42 Instructions: "Is this ready to publish?",43 Criteria: &typesafe.NoulCriteria{44 True: "The package is ready for users.",45 False: "The package still needs substantial work.",46 },47 },48 "release_channel": typesafe.ChoiceQuestion{49 Instructions: "Which release channel fits?",50 Criteria: map[string]any{51 "stable": "Ready for a stable release.",52 "beta": "Useful, but still needs real-world feedback.",53 },54 },55 "readiness": typesafe.ScoreQuestion{56 Instructions: "Score the release readiness.",57 Criteria: []any{58 "Not ready",59 "Needs review",60 "Ready",61 },62 },63 },64 })65 if err != nil {66 log.Fatal(err)67 }6869 isReady, err := result.Noul("is_ready")70 if err != nil {71 log.Fatal(err)72 }73 channel, err := result.Choice("release_channel")74 if err != nil {75 log.Fatal(err)76 }77 readiness, err := result.Score("readiness")78 if err != nil {79 log.Fatal(err)80 }81 fmt.Printf("ready=%v channel=%s score=%.1f\n", isReady.Noul >= 0.5, channel.Choice, readiness.Score)82}83```8485The accessors check the answer's wire type and return an error when a question86has a different type, is missing, or contains malformed known fields. Unknown87answer types are retained as `UnknownAnswer` so a newer service response is not88silently discarded.8990`NoulAnswer.Noul` is the service's yes probability. `ChoiceAnswer.Confidence`91and `ScoreAnswer.Confidence` describe confidence in the selected result; they92are not probabilities for the answer's truth. Treat all model output as93uncertain application data and choose thresholds appropriate to your use case.9495## Models9697```go98models, err := client.ListModels(context.Background())99if err != nil {100 // Handle *typesafe.APIError or *typesafe.ResponseError as appropriate.101 log.Fatal(err)102}103for _, model := range models.Models {104 fmt.Println(model.Name, model.Description)105}106```107108Requests use `jev-latest` by default. Select another model with109`typesafe.WithModel("model-name")`, or set `TYPESAFE_DEFAULT_MODEL`. A request's110`SystemOneRequest.Model` takes precedence for that request.111112## Configuration113114`NewClient` reads these environment variables when the corresponding option115is not supplied:116117| Setting | Option | Environment | Default |118| --- | --- | --- | --- |119| API key | `WithAPIKey` | `TYPESAFE_API_KEY` | required |120| Base URL | `WithBaseURL` | `TYPESAFE_BASE_URL` | `https://api.typesafe.ai` |121| Model | `WithModel` | `TYPESAFE_DEFAULT_MODEL` | `jev-latest` |122| HTTP client | `WithHTTPClient` | — | fresh `http.Client` |123| Attempt timeout | `WithTimeout` | — | 10 seconds |124| Retries | `WithMaxRetries` or `WithRetryPolicy` | — | 2 retries |125126Whitespace-only environment values are ignored. Explicit options take127precedence over environment values. `WithHeaders` adds headers to requests;128the SDK sets authentication and content headers itself.129130## Errors, context, and retries131132Every request accepts a `context.Context`. Cancellation and an expired caller133deadline stop the request and are returned through the wrapped error chain.134`WithTimeout` limits each attempt; use the context for an overall deadline.135136The default policy retries connection errors, timeouts, HTTP 408, HTTP 429,137and HTTP 5xx responses. It uses exponential backoff with jitter and honors138`retry-after-ms` and `Retry-After` up to the policy's maximum. Set a custom139`RetryPolicy` when the defaults do not fit, or `WithMaxRetries(0)` to disable140retries. Caller cancellation is never retried. Redirects are rejected to avoid141replaying API keys or state to another endpoint. When `WithHTTPClient` is used,142the client configuration is copied before the SDK installs its redirect policy;143the caller's `*http.Client` is never mutated.144145`SystemOne` validates requests locally before sending them, matching the146hosted service's limits: `State` must not be null, a Noul needs instructions147or a non-null criterion, a Choice has 2 to `MaxChoiceOptions` (255) labels,148and a Score has 2 to `MaxScoreLevels` (10) non-null levels. Call149`SystemOneRequest.Validate` to check a request without sending it.150151HTTP failures return `*APIError`, which exposes status, request ID, headers,152method, URL, and response bytes without placing the body in its `Error()`153string. Response decoding validates required and known answer fields and154returns `*ResponseError` when it fails; the error retains the request ID when155one was supplied. The typed accessors then check only whether an answer exists156and has the requested type. Use `errors.Is` for context errors and157`errors.AsType` for typed errors:158159```go160apiErr, ok := errors.AsType[*typesafe.APIError](err)161if ok {162 fmt.Println(apiErr.StatusCode, apiErr.RequestID)163}164```165166Responses larger than 16 MiB are rejected before decoding. This bounds memory167use when an endpoint returns an unexpectedly large payload.168169## Compatibility and scope170171This release targets Go 1.27 and later, supports Go modules, and has no172third-party dependencies. The public surface covers the documented173`SystemOne` and `models` operations, typed Noul, Choice, and Score questions,174and forward-compatible handling of unknown answer types. The API contract was175checked against the TypeSafe API documentation; the documentation is a176reference for wire compatibility and does not imply that this repository is an177official TypeSafe SDK.178179Go 1.27's `encoding/json/v2` is used for strict response handling: duplicate180object members and invalid UTF-8 are rejected, while unknown answer payloads181are preserved as `jsontext.Value`. In addition to the named convenience182methods, a response can be read with a generic accessor:183184```go185answer, err := result.Answer[typesafe.ChoiceAnswer]("release_channel")186```187188## Development189190The default test suite is offline. Run the same checks used by CI with:191192```sh193gofmt -w .194go test ./...195go test -race ./...196go vet ./...197```198199The live API check makes billable requests and is explicitly gated. Run it200only with a configured API key:201202```sh203TYPESAFE_LIVE_TEST=1 go test -run TestLiveAPI -v204```205206See [CONTRIBUTING.md](CONTRIBUTING.md), [CHANGELOG.md](CHANGELOG.md), and the207[TypeSafe API documentation](https://docs.typesafe.ai/api) for more context.208209## License210211MIT. See [LICENSE](LICENSE).