| .forge | ||
| .github | ||
| examples | ||
| .gitignore | 166B | |
| CHANGELOG.md | 1.3K | |
| client_test.go | 6.3K | |
| client.go | 3.1K | |
| CONTRIBUTING.md | 840B | |
| doc.go | 356B | |
| errors.go | 1.1K | |
| example_test.go | 2.1K | |
| go.mod | 60B | |
| integration_test.go | 1.5K | |
| LICENSE | 1.0K | |
| options_test.go | 2.6K | |
| options.go | 5.1K | |
| questions_test.go | 6.1K | |
| questions.go | 7.0K | |
| README.md | 7.2K | |
| responses_test.go | 4.1K | |
| responses.go | 10.7K | |
| retry_test.go | 5.3K | |
| retry.go | 3.5K | |
| transport_test.go | 3.8K | |
| transport.go | 4.1K | |
| types.go | 1.5K |
typesafe-go
An unofficial, community-maintained Go SDK for the hosted TypeSafe API. It is an independent project by FelineStateMachine, with no official affiliation with or endorsement from TypeSafe. The SDK sends state and questions to the hosted service; it does not run inference locally.
The module is pure Go and uses only the standard library.
go get github.com/FelineStateMachine/typesafe-go@v0.2.0
Documentation: pkg.go.dev/github.com/FelineStateMachine/typesafe-go
Quickstart
Set TYPESAFE_API_KEY and create a client. NewClient also accepts
WithAPIKey when applications prefer explicit configuration.
package main
import (
"context"
"fmt"
"log"
"github.com/FelineStateMachine/typesafe-go"
)
func main() {
client, err := typesafe.NewClient()
if err != nil {
log.Fatal(err)
}
result, err := client.SystemOne(context.Background(), typesafe.SystemOneRequest{
State: map[string]any{"text": "The package has tests and documentation."},
Questions: map[string]typesafe.Question{
"is_ready": typesafe.NoulQuestion{
Instructions: "Is this ready to publish?",
Criteria: &typesafe.NoulCriteria{
True: "The package is ready for users.",
False: "The package still needs substantial work.",
},
},
"release_channel": typesafe.ChoiceQuestion{
Instructions: "Which release channel fits?",
Criteria: map[string]any{
"stable": "Ready for a stable release.",
"beta": "Useful, but still needs real-world feedback.",
},
},
"readiness": typesafe.ScoreQuestion{
Instructions: "Score the release readiness.",
Criteria: []any{
"Not ready",
"Needs review",
"Ready",
},
},
},
})
if err != nil {
log.Fatal(err)
}
isReady, err := result.Noul("is_ready")
if err != nil {
log.Fatal(err)
}
channel, err := result.Choice("release_channel")
if err != nil {
log.Fatal(err)
}
readiness, err := result.Score("readiness")
if err != nil {
log.Fatal(err)
}
fmt.Printf("ready=%v channel=%s score=%.1f\n", isReady.Noul >= 0.5, channel.Choice, readiness.Score)
}
The accessors check the answer's wire type and return an error when a question
has a different type, is missing, or contains malformed known fields. Unknown
answer types are retained as UnknownAnswer so a newer service response is not
silently discarded.
NoulAnswer.Noul is the service's yes probability. ChoiceAnswer.Confidence
and ScoreAnswer.Confidence describe confidence in the selected result; they
are not probabilities for the answer's truth. Treat all model output as
uncertain application data and choose thresholds appropriate to your use case.
Models
models, err := client.ListModels(context.Background())
if err != nil {
// Handle *typesafe.APIError or *typesafe.ResponseError as appropriate.
log.Fatal(err)
}
for _, model := range models.Models {
fmt.Println(model.Name, model.Description)
}
Requests use jev-latest by default. Select another model with
typesafe.WithModel("model-name"), or set TYPESAFE_DEFAULT_MODEL. A request's
SystemOneRequest.Model takes precedence for that request.
Configuration
NewClient reads these environment variables when the corresponding option
is not supplied:
| Setting | Option | Environment | Default |
|---|---|---|---|
| API key | WithAPIKey |
TYPESAFE_API_KEY |
required |
| Base URL | WithBaseURL |
TYPESAFE_BASE_URL |
https:/ |
| Model | WithModel |
TYPESAFE_DEFAULT_MODEL |
jev-latest |
| HTTP client | WithHTTPClient |
— | fresh http.Client |
| Attempt timeout | WithTimeout |
— | 10 seconds |
| Retries | WithMaxRetries or WithRetryPolicy |
— | 2 retries |
Whitespace-only environment values are ignored. Explicit options take
precedence over environment values. WithHeaders adds headers to requests;
the SDK sets authentication and content headers itself.
Errors, context, and retries
Every request accepts a context.Context. Cancellation and an expired caller
deadline stop the request and are returned through the wrapped error chain.
WithTimeout limits each attempt; use the context for an overall deadline.
The default policy retries connection errors, timeouts, HTTP 408, HTTP 429,
and HTTP 5xx responses. It uses exponential backoff with jitter and honors
retry-after-ms and Retry-After up to the policy's maximum. Set a custom
RetryPolicy when the defaults do not fit, or WithMaxRetries(0) to disable
retries. Caller cancellation is never retried. Redirects are rejected to avoid
replaying API keys or state to another endpoint. When WithHTTPClient is used,
the client configuration is copied before the SDK installs its redirect policy;
the caller's *http.Client is never mutated.
SystemOne validates requests locally before sending them, matching the
hosted service's limits: State must not be null, a Noul needs instructions
or a non-null criterion, a Choice has 2 to MaxChoiceOptions (255) labels,
and a Score has 2 to MaxScoreLevels (10) non-null levels. Call
SystemOneRequest.Validate to check a request without sending it.
HTTP failures return *APIError, which exposes status, request ID, headers,
method, URL, and response bytes without placing the body in its Error()
string. Response decoding validates required and known answer fields and
returns *ResponseError when it fails; the error retains the request ID when
one was supplied. The typed accessors then check only whether an answer exists
and has the requested type. Use errors.Is for context errors and
errors.AsType for typed errors:
apiErr, ok := errors.AsType[*typesafe.APIError](err)
if ok {
fmt.Println(apiErr.StatusCode, apiErr.RequestID)
}
Responses larger than 16 MiB are rejected before decoding. This bounds memory use when an endpoint returns an unexpectedly large payload.
Compatibility and scope
This release targets Go 1.27 and later, supports Go modules, and has no
third-party dependencies. The public surface covers the documented
SystemOne and models operations, typed Noul, Choice, and Score questions,
and forward-compatible handling of unknown answer types. The API contract was
checked against the TypeSafe API documentation; the documentation is a
reference for wire compatibility and does not imply that this repository is an
official TypeSafe SDK.
Go 1.27's encoding/ is used for strict response handling: duplicate
object members and invalid UTF-8 are rejected, while unknown answer payloads
are preserved as jsontext.Value. In addition to the named convenience
methods, a response can be read with a generic accessor:
answer, err := result.Answer[typesafe.ChoiceAnswer]("release_channel")
Development
The default test suite is offline. Run the same checks used by CI with:
gofmt -w .
go test ./...
go test -race ./...
go vet ./...
The live API check makes billable requests and is explicitly gated. Run it only with a configured API key:
TYPESAFE_LIVE_TEST=1 go test -run TestLiveAPI -v
See CONTRIBUTING.md, CHANGELOG.md, and the TypeSafe API documentation for more context.
License
MIT. See LICENSE.