@pi-in-go/pigpen-jev 0.1.0
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.
- package/CREDITS.md +22 -0
- package/LICENSE +22 -0
- package/README.md +237 -0
- package/extensions/jev/ask.go +166 -0
- package/extensions/jev/ask_test.go +218 -0
- package/extensions/jev/backend.go +128 -0
- package/extensions/jev/bench_test.go +64 -0
- package/extensions/jev/boundaries_test.go +159 -0
- package/extensions/jev/command.go +224 -0
- package/extensions/jev/commands_test.go +214 -0
- package/extensions/jev/config.go +450 -0
- package/extensions/jev/errors_test.go +191 -0
- package/extensions/jev/extension.go +391 -0
- package/extensions/jev/fakehost_test.go +548 -0
- package/extensions/jev/gate.go +125 -0
- package/extensions/jev/gate_test.go +610 -0
- package/extensions/jev/gatekey_test.go +24 -0
- package/extensions/jev/go.mod +9 -0
- package/extensions/jev/go.sum +2 -0
- package/extensions/jev/go.work +10 -0
- package/extensions/jev/helpers_test.go +404 -0
- package/extensions/jev/memo.go +88 -0
- package/extensions/jev/output.go +89 -0
- package/extensions/jev/output_test.go +187 -0
- package/extensions/jev/ownmodel_test.go +118 -0
- package/extensions/jev/render.go +136 -0
- package/extensions/jev/review_test.go +310 -0
- package/extensions/jev/source_test.go +57 -0
- package/extensions/jev/text.go +174 -0
- package/extensions/jev/trust_test.go +335 -0
- package/extensions/jev/types.go +227 -0
- package/libs/typesafe/CONTRACT.md +125 -0
- package/libs/typesafe/CREDITS.md +37 -0
- package/libs/typesafe/LICENSE +23 -0
- package/libs/typesafe/README.md +19 -0
- package/libs/typesafe/go.mod +3 -0
- package/libs/typesafe/libraries/ownmodel/backend_test.go +496 -0
- package/libs/typesafe/libraries/ownmodel/canon.go +190 -0
- package/libs/typesafe/libraries/ownmodel/convert.go +199 -0
- package/libs/typesafe/libraries/ownmodel/doc.go +15 -0
- package/libs/typesafe/libraries/ownmodel/equivalence_test.go +199 -0
- package/libs/typesafe/libraries/ownmodel/helpers_test.go +155 -0
- package/libs/typesafe/libraries/ownmodel/mutation_test.go +31 -0
- package/libs/typesafe/libraries/ownmodel/ownmodel.go +225 -0
- package/libs/typesafe/libraries/ownmodel/plan.go +442 -0
- package/libs/typesafe/libraries/ownmodel/run.go +288 -0
- package/libs/typesafe/libraries/ownmodel/schema_test.go +254 -0
- package/libs/typesafe/libraries/ownmodel/twins_test.go +169 -0
- package/libs/typesafe/libraries/ownmodel/utils_test.go +125 -0
- package/libs/typesafe/libraries/pigmodel/pigmodel.go +264 -0
- package/libs/typesafe/libraries/pigmodel/pigmodel_test.go +410 -0
- package/libs/typesafe/libraries/typesafe/answers.go +268 -0
- package/libs/typesafe/libraries/typesafe/api_response_test.go +113 -0
- package/libs/typesafe/libraries/typesafe/batch.go +80 -0
- package/libs/typesafe/libraries/typesafe/batch_test.go +133 -0
- package/libs/typesafe/libraries/typesafe/bench_test.go +71 -0
- package/libs/typesafe/libraries/typesafe/client.go +561 -0
- package/libs/typesafe/libraries/typesafe/client_test.go +495 -0
- package/libs/typesafe/libraries/typesafe/crosscheck_test.go +464 -0
- package/libs/typesafe/libraries/typesafe/crosscheck_workflowevals_test.go +219 -0
- package/libs/typesafe/libraries/typesafe/doc.go +27 -0
- package/libs/typesafe/libraries/typesafe/entry.go +142 -0
- package/libs/typesafe/libraries/typesafe/env.go +11 -0
- package/libs/typesafe/libraries/typesafe/errors.go +310 -0
- package/libs/typesafe/libraries/typesafe/errors_test.go +175 -0
- package/libs/typesafe/libraries/typesafe/helpers_test.go +294 -0
- package/libs/typesafe/libraries/typesafe/live_test.go +96 -0
- package/libs/typesafe/libraries/typesafe/logging.go +160 -0
- package/libs/typesafe/libraries/typesafe/logging_test.go +259 -0
- package/libs/typesafe/libraries/typesafe/marshal_test.go +112 -0
- package/libs/typesafe/libraries/typesafe/mutation_test.go +39 -0
- package/libs/typesafe/libraries/typesafe/questions.go +490 -0
- package/libs/typesafe/libraries/typesafe/questions_test.go +166 -0
- package/libs/typesafe/libraries/typesafe/regressions_test.go +159 -0
- package/libs/typesafe/libraries/typesafe/reliability_test.go +649 -0
- package/libs/typesafe/libraries/typesafe/retry.go +350 -0
- package/libs/typesafe/libraries/typesafe/retry_test.go +297 -0
- package/libs/typesafe/libraries/typesafe/runtime_test.go +26 -0
- package/libs/typesafe/libraries/typesafe/transport_test.go +163 -0
- package/libs/typesafe/libraries/typesafe/twins_test.go +127 -0
- package/libs/typesafe/libraries/typesafe/types_test.go +165 -0
- package/libs/typesafe/libraries/typesafe/version.go +10 -0
- package/libs/typesafe/package.json +37 -0
- package/libs/typesafe/provenance.json +49 -0
- package/package.json +42 -0
- package/port/PORT.md +107 -0
- package/port/e2e/gate-and-output.py +35 -0
- package/port/e2e/jev-ask.py +36 -0
- package/port/e2e/model-switch.py +44 -0
- package/port/e2e/off-by-default.py +34 -0
- package/port/gen-scenarios.py +103 -0
- package/port/golden/cache-identical-calls.jsonl +30 -0
- package/port/golden/clear.jsonl +22 -0
- package/port/golden/commands.jsonl +43 -0
- package/port/golden/enforce-accept.jsonl +23 -0
- package/port/golden/enforce-decline.jsonl +22 -0
- package/port/golden/jev-ask.jsonl +20 -0
- package/port/golden/output-advice.jsonl +23 -0
- package/port/golden/output-leak.jsonl +24 -0
- package/port/golden/output-low-confidence.jsonl +22 -0
- package/port/golden/shadow-flagged.jsonl +23 -0
- package/port/golden/unjudged-tools.jsonl +19 -0
- package/port/golden/write-elision.jsonl +21 -0
- package/port/mutate-unit.py +63 -0
- package/port/mutations.json +578 -0
- package/port/oracle/LICENSE +21 -0
- package/port/oracle/README.md +181 -0
- package/port/oracle/SHA256SUMS +8 -0
- package/port/oracle/package.json +43 -0
- package/port/oracle/src/client.ts +409 -0
- package/port/oracle/src/config.ts +363 -0
- package/port/oracle/src/gate.ts +229 -0
- package/port/oracle/src/index.ts +649 -0
- package/port/oracle/src/output.ts +163 -0
- package/port/red-run.log +309 -0
- package/port/scenarios/cache-identical-calls.json +71 -0
- package/port/scenarios/clear.json +61 -0
- package/port/scenarios/commands.json +119 -0
- package/port/scenarios/enforce-accept.json +66 -0
- package/port/scenarios/enforce-decline.json +57 -0
- package/port/scenarios/jev-ask.json +83 -0
- package/port/scenarios/output-advice.json +61 -0
- package/port/scenarios/output-leak.json +61 -0
- package/port/scenarios/output-low-confidence.json +61 -0
- package/port/scenarios/shadow-flagged.json +61 -0
- package/port/scenarios/unjudged-tools.json +55 -0
- package/port/scenarios/write-elision.json +53 -0
- package/provenance.json +18 -0
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
package typesafe
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"context"
|
|
6
|
+
"encoding/json"
|
|
7
|
+
"errors"
|
|
8
|
+
"fmt"
|
|
9
|
+
"io"
|
|
10
|
+
"net/http"
|
|
11
|
+
"os"
|
|
12
|
+
"strings"
|
|
13
|
+
"sync/atomic"
|
|
14
|
+
"time"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
// HTTPDoer is the HTTP transport (the TypeScript `fetch` option); *http.Client
|
|
18
|
+
// satisfies it. The request carries the attempt's context.
|
|
19
|
+
type HTTPDoer interface {
|
|
20
|
+
Do(req *http.Request) (*http.Response, error)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Config configures a [Client]. Explicit values take precedence over environment
|
|
24
|
+
// variables, then the defaults; a zero value means "not set".
|
|
25
|
+
type Config struct {
|
|
26
|
+
// APIKey is required; falls back to TYPESAFE_API_KEY.
|
|
27
|
+
APIKey string
|
|
28
|
+
// BaseURL is the API root; falls back to TYPESAFE_BASE_URL, then DefaultBaseURL.
|
|
29
|
+
// Trailing slashes are removed.
|
|
30
|
+
BaseURL string
|
|
31
|
+
// DefaultModel falls back to TYPESAFE_DEFAULT_MODEL, then DefaultModel.
|
|
32
|
+
DefaultModel string
|
|
33
|
+
// LogLevel falls back to TYPESAFE_LOG_LEVEL, then DefaultLogLevel.
|
|
34
|
+
LogLevel LogLevel
|
|
35
|
+
// Logger is filtered to LogLevel and above; nil uses [NewStderrLogger].
|
|
36
|
+
Logger Logger
|
|
37
|
+
// Retry overrides the retry defaults field by field.
|
|
38
|
+
Retry RetryOverrides
|
|
39
|
+
// Timeout is per attempt (there is no total retry budget); zero means DefaultTimeout,
|
|
40
|
+
// a negative value is an error.
|
|
41
|
+
Timeout time.Duration
|
|
42
|
+
// DefaultHeaders are sent with every request; per-call headers take precedence.
|
|
43
|
+
// The SDK's own headers (Authorization, Content-Type, Accept, User-Agent,
|
|
44
|
+
// X-TypeSafe-*) cannot be replaced.
|
|
45
|
+
DefaultHeaders map[string]string
|
|
46
|
+
// HTTPClient is the transport; nil uses a client with no overall timeout.
|
|
47
|
+
HTTPClient HTTPDoer
|
|
48
|
+
// Getenv reads the environment; nil uses os.Getenv.
|
|
49
|
+
Getenv func(string) string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// RequestOptions are per-call overrides. Cancellation and deadlines come from the
|
|
53
|
+
// context passed to the call.
|
|
54
|
+
type RequestOptions struct {
|
|
55
|
+
// Timeout per attempt; zero inherits the client's, a negative value is an error.
|
|
56
|
+
Timeout time.Duration
|
|
57
|
+
// Retry overrides the client's policy for this call, field by field.
|
|
58
|
+
Retry RetryOverrides
|
|
59
|
+
// Headers are merged over the client's default headers.
|
|
60
|
+
Headers map[string]string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Evaluator answers typed questions. [Client] (the TypeSafe API) and the own-model
|
|
64
|
+
// backend implement it; code that only needs answers should depend on this interface.
|
|
65
|
+
type Evaluator interface {
|
|
66
|
+
SystemOne(ctx context.Context, req SystemOneRequest, opts *RequestOptions) (*SystemOneResult, error)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Client is a client for the TypeSafe AI API. It is safe for concurrent use. Its
|
|
70
|
+
// String, GoString and Format methods never reveal the API key.
|
|
71
|
+
type Client struct {
|
|
72
|
+
// apiKey is a closure so that no formatting of the struct can print the key.
|
|
73
|
+
apiKey func() string
|
|
74
|
+
baseURL string
|
|
75
|
+
defaultModel string
|
|
76
|
+
logLevel LogLevel
|
|
77
|
+
logger Logger
|
|
78
|
+
retry RetryPolicy
|
|
79
|
+
timeout time.Duration
|
|
80
|
+
defaultHeaders map[string]string
|
|
81
|
+
doer HTTPDoer
|
|
82
|
+
models *Models
|
|
83
|
+
requestCount atomic.Int64
|
|
84
|
+
|
|
85
|
+
// random supplies jitter in [0,1); tests replace it. nil uses math/rand.
|
|
86
|
+
random func() float64
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
var _ Evaluator = (*Client)(nil)
|
|
90
|
+
|
|
91
|
+
func readEnv(getenv func(string) string, name string) string { return strings.TrimSpace(getenv(name)) }
|
|
92
|
+
|
|
93
|
+
// NewClient validates the configuration and returns a client. It returns a
|
|
94
|
+
// *TypeSafeError when the API key is missing (naming TYPESAFE_API_KEY), the log level
|
|
95
|
+
// is invalid, or a timeout or retry field is out of range.
|
|
96
|
+
func NewClient(cfg Config) (*Client, error) {
|
|
97
|
+
getenv := cfg.Getenv
|
|
98
|
+
if getenv == nil {
|
|
99
|
+
getenv = os.Getenv
|
|
100
|
+
}
|
|
101
|
+
fromConfigOrEnv := func(explicit, env string) string {
|
|
102
|
+
if explicit != "" {
|
|
103
|
+
return explicit
|
|
104
|
+
}
|
|
105
|
+
return readEnv(getenv, env)
|
|
106
|
+
}
|
|
107
|
+
key := fromConfigOrEnv(cfg.APIKey, EnvAPIKey)
|
|
108
|
+
if key == "" {
|
|
109
|
+
return nil, errorf("No API key was provided. Set `APIKey` in the Config passed to NewClient, or set the %s environment variable.", EnvAPIKey)
|
|
110
|
+
}
|
|
111
|
+
baseURL := fromConfigOrEnv(cfg.BaseURL, EnvBaseURL)
|
|
112
|
+
if baseURL == "" {
|
|
113
|
+
baseURL = DefaultBaseURL
|
|
114
|
+
}
|
|
115
|
+
baseURL = strings.TrimRight(baseURL, "/")
|
|
116
|
+
model := fromConfigOrEnv(cfg.DefaultModel, EnvDefaultModel)
|
|
117
|
+
if model == "" {
|
|
118
|
+
model = DefaultModel
|
|
119
|
+
}
|
|
120
|
+
level := DefaultLogLevel
|
|
121
|
+
var err error
|
|
122
|
+
if cfg.LogLevel != "" {
|
|
123
|
+
level, err = ParseLogLevel(string(cfg.LogLevel), "the `LogLevel` option")
|
|
124
|
+
} else if env := readEnv(getenv, EnvLogLevel); env != "" {
|
|
125
|
+
level, err = ParseLogLevel(env, EnvLogLevel)
|
|
126
|
+
}
|
|
127
|
+
if err != nil {
|
|
128
|
+
return nil, err
|
|
129
|
+
}
|
|
130
|
+
sink := cfg.Logger
|
|
131
|
+
if sink == nil {
|
|
132
|
+
sink = NewStderrLogger()
|
|
133
|
+
}
|
|
134
|
+
retry, err := DefaultRetryPolicy().Resolve(cfg.Retry)
|
|
135
|
+
if err != nil {
|
|
136
|
+
return nil, err
|
|
137
|
+
}
|
|
138
|
+
timeout, err := resolveTimeout("Timeout", cfg.Timeout, DefaultTimeout)
|
|
139
|
+
if err != nil {
|
|
140
|
+
return nil, err
|
|
141
|
+
}
|
|
142
|
+
headers := make(map[string]string, len(cfg.DefaultHeaders))
|
|
143
|
+
for k, v := range cfg.DefaultHeaders {
|
|
144
|
+
headers[k] = v
|
|
145
|
+
}
|
|
146
|
+
doer := cfg.HTTPClient
|
|
147
|
+
if doer == nil {
|
|
148
|
+
doer = &http.Client{}
|
|
149
|
+
}
|
|
150
|
+
c := &Client{
|
|
151
|
+
apiKey: func() string { return key },
|
|
152
|
+
baseURL: baseURL,
|
|
153
|
+
defaultModel: model,
|
|
154
|
+
logLevel: level,
|
|
155
|
+
logger: WithLevel(sink, level),
|
|
156
|
+
retry: retry,
|
|
157
|
+
timeout: timeout,
|
|
158
|
+
defaultHeaders: headers,
|
|
159
|
+
doer: doer,
|
|
160
|
+
}
|
|
161
|
+
c.models = &Models{c: c}
|
|
162
|
+
return c, nil
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// resolveTimeout applies the zero-means-inherit rule and rejects negative values.
|
|
166
|
+
func resolveTimeout(name string, v, inherit time.Duration) (time.Duration, error) {
|
|
167
|
+
if v < 0 {
|
|
168
|
+
return 0, errorf("`%s` must be a positive duration, got %s.", name, v)
|
|
169
|
+
}
|
|
170
|
+
if v == 0 {
|
|
171
|
+
return inherit, nil
|
|
172
|
+
}
|
|
173
|
+
return v, nil
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// BaseURL returns the API root without trailing slashes.
|
|
177
|
+
func (c *Client) BaseURL() string { return c.baseURL }
|
|
178
|
+
|
|
179
|
+
// DefaultModel returns the model used when a request omits one.
|
|
180
|
+
func (c *Client) DefaultModel() string { return c.defaultModel }
|
|
181
|
+
|
|
182
|
+
// LogLevel returns the configured verbosity.
|
|
183
|
+
func (c *Client) LogLevel() LogLevel { return c.logLevel }
|
|
184
|
+
|
|
185
|
+
// Logger returns the configured logger, filtered to the log level.
|
|
186
|
+
func (c *Client) Logger() Logger { return c.logger }
|
|
187
|
+
|
|
188
|
+
// RetryPolicy returns a copy of the resolved retry policy.
|
|
189
|
+
func (c *Client) RetryPolicy() RetryPolicy {
|
|
190
|
+
p, _ := c.retry.Resolve(RetryOverrides{})
|
|
191
|
+
return p
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Timeout returns the per-attempt timeout.
|
|
195
|
+
func (c *Client) Timeout() time.Duration { return c.timeout }
|
|
196
|
+
|
|
197
|
+
// DefaultHeaders returns a copy of the additional default headers.
|
|
198
|
+
func (c *Client) DefaultHeaders() map[string]string {
|
|
199
|
+
out := make(map[string]string, len(c.defaultHeaders))
|
|
200
|
+
for k, v := range c.defaultHeaders {
|
|
201
|
+
out[k] = v
|
|
202
|
+
}
|
|
203
|
+
return out
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// String returns a description without the API key.
|
|
207
|
+
func (c *Client) String() string {
|
|
208
|
+
return fmt.Sprintf("typesafe.Client{BaseURL: %s, DefaultModel: %s, LogLevel: %s}", c.baseURL, c.defaultModel, c.logLevel)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// GoString returns a description without the API key.
|
|
212
|
+
func (c *Client) GoString() string { return c.String() }
|
|
213
|
+
|
|
214
|
+
// Models returns the Models resource.
|
|
215
|
+
func (c *Client) Models() *Models { return c.models }
|
|
216
|
+
|
|
217
|
+
// SystemOne answers named questions about text or structured state. Questions are
|
|
218
|
+
// validated before anything is sent (*TypeSafeError). It returns an *APIError
|
|
219
|
+
// subclass for a non-2xx response after retries, an *APIConnectionError (or
|
|
220
|
+
// *APITimeoutError) for transport failures after retries, and an *APIUserAbortError
|
|
221
|
+
// when ctx ends.
|
|
222
|
+
func (c *Client) SystemOne(ctx context.Context, req SystemOneRequest, opts *RequestOptions) (*SystemOneResult, error) {
|
|
223
|
+
res, err := c.SystemOneWithResponse(ctx, req, opts)
|
|
224
|
+
if err != nil {
|
|
225
|
+
return nil, err
|
|
226
|
+
}
|
|
227
|
+
return res.Data, nil
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Response is a parsed result with the HTTP response metadata.
|
|
231
|
+
type Response[T any] struct {
|
|
232
|
+
Data T
|
|
233
|
+
RawResponse
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// RawResponse is the buffered HTTP response of a successful request.
|
|
237
|
+
type RawResponse struct {
|
|
238
|
+
Status int
|
|
239
|
+
Header http.Header
|
|
240
|
+
// Body is the complete response body (buffered under the attempt timeout).
|
|
241
|
+
Body []byte
|
|
242
|
+
// URL is the requested URL.
|
|
243
|
+
URL string
|
|
244
|
+
// RequestID is the x-typesafe-request-id header, or "".
|
|
245
|
+
RequestID string
|
|
246
|
+
|
|
247
|
+
tag string // the log tag of the request
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// logBody logs a parsed response body at debug level, like the TypeScript parse step.
|
|
251
|
+
func (c *Client) logBody(raw *RawResponse) {
|
|
252
|
+
if c.logLevel != LogDebug {
|
|
253
|
+
return // the logger drops it; do not parse the body to build it
|
|
254
|
+
}
|
|
255
|
+
parsed, _ := parseBody(raw.Body)
|
|
256
|
+
c.logger.Debug(raw.tag+" <- body", parsed)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
func (c *Client) systemOnePayload(req SystemOneRequest) ([]byte, error) {
|
|
260
|
+
if err := req.Questions.Validate(); err != nil {
|
|
261
|
+
return nil, err
|
|
262
|
+
}
|
|
263
|
+
if req.Model == "" {
|
|
264
|
+
req.Model = c.defaultModel
|
|
265
|
+
}
|
|
266
|
+
body, err := marshalPlain(req)
|
|
267
|
+
if err != nil {
|
|
268
|
+
return nil, unwrapMarshalError(err)
|
|
269
|
+
}
|
|
270
|
+
return body, nil
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// SystemOneWithResponse is [Client.SystemOne] with the response metadata.
|
|
274
|
+
func (c *Client) SystemOneWithResponse(ctx context.Context, req SystemOneRequest, opts *RequestOptions) (*Response[*SystemOneResult], error) {
|
|
275
|
+
body, err := c.systemOnePayload(req)
|
|
276
|
+
if err != nil {
|
|
277
|
+
return nil, err
|
|
278
|
+
}
|
|
279
|
+
raw, err := c.request(ctx, http.MethodPost, "/v1/systemone", body, opts)
|
|
280
|
+
if err != nil {
|
|
281
|
+
return nil, err
|
|
282
|
+
}
|
|
283
|
+
c.logBody(raw)
|
|
284
|
+
result := &SystemOneResult{}
|
|
285
|
+
if len(bytes.TrimSpace(raw.Body)) > 0 {
|
|
286
|
+
if err := json.Unmarshal(raw.Body, result); err != nil {
|
|
287
|
+
return nil, &TypeSafeError{Message: "Unexpected response shape from POST /v1/systemone; expected { model, answers, usage }.", Cause: err}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return &Response[*SystemOneResult]{Data: result, RawResponse: *raw}, nil
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// SystemOneRaw sends the request and returns the response without parsing its body
|
|
294
|
+
// (the TypeScript asResponse()). A non-2xx status is still an *APIError.
|
|
295
|
+
func (c *Client) SystemOneRaw(ctx context.Context, req SystemOneRequest, opts *RequestOptions) (*RawResponse, error) {
|
|
296
|
+
body, err := c.systemOnePayload(req)
|
|
297
|
+
if err != nil {
|
|
298
|
+
return nil, err
|
|
299
|
+
}
|
|
300
|
+
return c.request(ctx, http.MethodPost, "/v1/systemone", body, opts)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Models is the Models API resource.
|
|
304
|
+
type Models struct {
|
|
305
|
+
c *Client
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// List lists the models available to the account (GET /v1/models). A body that is not
|
|
309
|
+
// {"models": [...]} is a *TypeSafeError "Unexpected response shape from GET /v1/models; expected { models: [...] }."
|
|
310
|
+
func (m *Models) List(ctx context.Context, opts *RequestOptions) ([]ModelCard, error) {
|
|
311
|
+
res, err := m.ListWithResponse(ctx, opts)
|
|
312
|
+
if err != nil {
|
|
313
|
+
return nil, err
|
|
314
|
+
}
|
|
315
|
+
return res.Data, nil
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ListWithResponse is [Models.List] with the response metadata.
|
|
319
|
+
func (m *Models) ListWithResponse(ctx context.Context, opts *RequestOptions) (*Response[[]ModelCard], error) {
|
|
320
|
+
raw, err := m.c.request(ctx, http.MethodGet, "/v1/models", nil, opts)
|
|
321
|
+
if err != nil {
|
|
322
|
+
return nil, err
|
|
323
|
+
}
|
|
324
|
+
m.c.logBody(raw)
|
|
325
|
+
shapeErr := &TypeSafeError{Message: "Unexpected response shape from GET /v1/models; expected { models: [...] }."}
|
|
326
|
+
var wire map[string]json.RawMessage
|
|
327
|
+
if err := json.Unmarshal(raw.Body, &wire); err != nil || wire == nil {
|
|
328
|
+
return nil, shapeErr
|
|
329
|
+
}
|
|
330
|
+
list := bytes.TrimSpace(wire["models"])
|
|
331
|
+
if len(list) == 0 || list[0] != '[' {
|
|
332
|
+
return nil, shapeErr
|
|
333
|
+
}
|
|
334
|
+
cards := []ModelCard{}
|
|
335
|
+
if err := json.Unmarshal(list, &cards); err != nil {
|
|
336
|
+
return nil, &TypeSafeError{Message: shapeErr.Message, Cause: err}
|
|
337
|
+
}
|
|
338
|
+
return &Response[[]ModelCard]{Data: cards, RawResponse: *raw}, nil
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ListRaw returns the unparsed response, which keeps fields the typed card drops.
|
|
342
|
+
func (m *Models) ListRaw(ctx context.Context, opts *RequestOptions) (*RawResponse, error) {
|
|
343
|
+
return m.c.request(ctx, http.MethodGet, "/v1/models", nil, opts)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// MapResponse transforms the data of a response and keeps its metadata.
|
|
347
|
+
func MapResponse[T, U any](r *Response[T], fn func(T) U) *Response[U] {
|
|
348
|
+
return &Response[U]{Data: fn(r.Data), RawResponse: r.RawResponse}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// header is one request header in the spelling the SDK uses.
|
|
352
|
+
type header struct{ name, value string }
|
|
353
|
+
|
|
354
|
+
// mergeHeaders merges sources in order: a later value wins regardless of the name's
|
|
355
|
+
// casing, and an entry with remove set deletes a protected header.
|
|
356
|
+
func mergeHeaders(sources ...[]headerEdit) []header {
|
|
357
|
+
var out []header
|
|
358
|
+
index := map[string]int{}
|
|
359
|
+
for _, src := range sources {
|
|
360
|
+
for _, e := range src {
|
|
361
|
+
lower := strings.ToLower(e.name)
|
|
362
|
+
i, exists := index[lower]
|
|
363
|
+
switch {
|
|
364
|
+
case e.remove && exists:
|
|
365
|
+
out[i].name = ""
|
|
366
|
+
delete(index, lower)
|
|
367
|
+
case e.remove:
|
|
368
|
+
case exists:
|
|
369
|
+
out[i] = header{e.name, e.value}
|
|
370
|
+
default:
|
|
371
|
+
index[lower] = len(out)
|
|
372
|
+
out = append(out, header{e.name, e.value})
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
kept := out[:0]
|
|
377
|
+
for _, h := range out {
|
|
378
|
+
if h.name != "" {
|
|
379
|
+
kept = append(kept, h)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return kept
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
type headerEdit struct {
|
|
386
|
+
name, value string
|
|
387
|
+
remove bool
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
func editsOf(m map[string]string) []headerEdit {
|
|
391
|
+
out := make([]headerEdit, 0, len(m))
|
|
392
|
+
for k, v := range m {
|
|
393
|
+
out = append(out, headerEdit{name: k, value: v})
|
|
394
|
+
}
|
|
395
|
+
return out
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// request sends one API request with retries and returns its buffered response.
|
|
399
|
+
func (c *Client) request(ctx context.Context, method, path string, body []byte, opts *RequestOptions) (*RawResponse, error) {
|
|
400
|
+
if opts == nil {
|
|
401
|
+
opts = &RequestOptions{}
|
|
402
|
+
}
|
|
403
|
+
timeout, err := resolveTimeout("Timeout", opts.Timeout, c.timeout)
|
|
404
|
+
if err != nil {
|
|
405
|
+
return nil, err
|
|
406
|
+
}
|
|
407
|
+
policy, err := c.retry.Resolve(opts.Retry)
|
|
408
|
+
if err != nil {
|
|
409
|
+
return nil, err
|
|
410
|
+
}
|
|
411
|
+
// Numbered so concurrent requests, and the attempts within one, can be told apart in the logs.
|
|
412
|
+
tag := fmt.Sprintf("#%d %s %s", c.requestCount.Add(1), method, path)
|
|
413
|
+
url := c.baseURL + path
|
|
414
|
+
// User-supplied headers go first so they cannot clobber auth or the JSON content type.
|
|
415
|
+
protected := []headerEdit{
|
|
416
|
+
{name: "Authorization", value: "Bearer " + c.apiKey()},
|
|
417
|
+
{name: "Accept", value: "application/json"},
|
|
418
|
+
{name: "User-Agent", value: "typesafe-sdk-go/" + Version},
|
|
419
|
+
{name: "X-TypeSafe-SDK", value: "typesafe-sdk-go/" + Version},
|
|
420
|
+
{name: "X-TypeSafe-Runtime", value: describeRuntime()},
|
|
421
|
+
{name: "X-TypeSafe-Retry-Count", remove: true},
|
|
422
|
+
}
|
|
423
|
+
if body == nil {
|
|
424
|
+
protected = append(protected, headerEdit{name: "Content-Type", remove: true})
|
|
425
|
+
} else {
|
|
426
|
+
protected = append(protected, headerEdit{name: "Content-Type", value: "application/json"})
|
|
427
|
+
}
|
|
428
|
+
base := mergeHeaders(editsOf(c.defaultHeaders), editsOf(opts.Headers), protected)
|
|
429
|
+
|
|
430
|
+
hooks := RetryHooks{
|
|
431
|
+
Random: c.random,
|
|
432
|
+
OnRetry: func(retry, total int, delay time.Duration, reason string) {
|
|
433
|
+
c.logger.Info(fmt.Sprintf("%s retrying in %dms (retry %d/%d) after %s", tag, delay.Milliseconds(), retry, total, reason))
|
|
434
|
+
},
|
|
435
|
+
OnAbortDuringWait: func() { c.logger.Info(tag + " aborted by caller while waiting to retry") },
|
|
436
|
+
}
|
|
437
|
+
return Retry(ctx, policy, hooks, func(ctx context.Context, attempt int) (*RawResponse, error) {
|
|
438
|
+
headers := base
|
|
439
|
+
if attempt > 0 {
|
|
440
|
+
headers = append(append([]header(nil), base...), header{"X-TypeSafe-Retry-Count", fmt.Sprint(attempt)})
|
|
441
|
+
}
|
|
442
|
+
if c.logLevel == LogDebug { // only debug prints headers and body; the other levels would drop them
|
|
443
|
+
logged := make(map[string]string, len(headers))
|
|
444
|
+
for _, h := range headers {
|
|
445
|
+
logged[h.name] = h.value
|
|
446
|
+
}
|
|
447
|
+
var bodyLog any
|
|
448
|
+
if body != nil {
|
|
449
|
+
bodyLog = json.RawMessage(body)
|
|
450
|
+
}
|
|
451
|
+
c.logger.Debug(fmt.Sprintf("%s -> %s", tag, url), map[string]any{"headers": RedactHeaders(logged), "body": bodyLog})
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
started := time.Now()
|
|
455
|
+
raw, err := c.attempt(ctx, tag, method, url, headers, body, timeout, started)
|
|
456
|
+
if err != nil {
|
|
457
|
+
return nil, err
|
|
458
|
+
}
|
|
459
|
+
requestID := ""
|
|
460
|
+
if raw.RequestID != "" {
|
|
461
|
+
requestID = fmt.Sprintf(" (request %s)", raw.RequestID)
|
|
462
|
+
}
|
|
463
|
+
c.logger.Info(fmt.Sprintf("%s <- %d in %dms%s", tag, raw.Status, time.Since(started).Milliseconds(), requestID))
|
|
464
|
+
if raw.Status >= 200 && raw.Status < 300 {
|
|
465
|
+
return raw, nil
|
|
466
|
+
}
|
|
467
|
+
parsed, text := parseBody(raw.Body)
|
|
468
|
+
c.logger.Debug(tag+" <- error body", parsed)
|
|
469
|
+
return nil, newAPIError(raw.Status, parsed, text, raw.Header)
|
|
470
|
+
})
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// parseBody parses a response body leniently: JSON when it is JSON (whatever the
|
|
474
|
+
// content type), else the text; nil for an empty body. The second value is the raw JSON
|
|
475
|
+
// text when the body parsed.
|
|
476
|
+
func parseBody(body []byte) (parsed any, rawJSON string) {
|
|
477
|
+
if len(body) == 0 {
|
|
478
|
+
return nil, ""
|
|
479
|
+
}
|
|
480
|
+
var v any
|
|
481
|
+
if err := json.Unmarshal(body, &v); err != nil {
|
|
482
|
+
return string(body), ""
|
|
483
|
+
}
|
|
484
|
+
return v, string(body)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// attempt is one HTTP round trip including body delivery, under the attempt timeout. The
|
|
488
|
+
// caller's context and the timer are told apart to choose the error class.
|
|
489
|
+
func (c *Client) attempt(ctx context.Context, tag, method, url string, headers []header, body []byte, timeout time.Duration, started time.Time) (*RawResponse, error) {
|
|
490
|
+
actx, cancel := context.WithTimeout(ctx, timeout)
|
|
491
|
+
defer cancel()
|
|
492
|
+
elapsed := func() string { return fmt.Sprintf("%dms", time.Since(started).Milliseconds()) }
|
|
493
|
+
fail := func(cause error) error {
|
|
494
|
+
if ctx.Err() != nil {
|
|
495
|
+
c.logger.Info(fmt.Sprintf("%s aborted by caller after %s", tag, elapsed()))
|
|
496
|
+
return newAbortError(context.Cause(ctx))
|
|
497
|
+
}
|
|
498
|
+
if errors.Is(actx.Err(), context.DeadlineExceeded) {
|
|
499
|
+
c.logger.Info(fmt.Sprintf("%s timed out after %s", tag, elapsed()))
|
|
500
|
+
return newTimeoutError(timeout, cause)
|
|
501
|
+
}
|
|
502
|
+
c.logger.Info(fmt.Sprintf("%s connection error after %s", tag, elapsed()), cause)
|
|
503
|
+
return newConnectionError(cause)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
var reader io.Reader
|
|
507
|
+
if body != nil {
|
|
508
|
+
reader = bytes.NewReader(body)
|
|
509
|
+
}
|
|
510
|
+
req, err := http.NewRequestWithContext(actx, method, url, reader)
|
|
511
|
+
if err != nil {
|
|
512
|
+
return nil, fail(err)
|
|
513
|
+
}
|
|
514
|
+
for _, h := range headers {
|
|
515
|
+
req.Header.Set(h.name, h.value)
|
|
516
|
+
}
|
|
517
|
+
resp, err := c.doer.Do(req)
|
|
518
|
+
if err != nil {
|
|
519
|
+
return nil, fail(err)
|
|
520
|
+
}
|
|
521
|
+
data, err := readBody(actx, resp.Body)
|
|
522
|
+
if err != nil {
|
|
523
|
+
return nil, fail(err)
|
|
524
|
+
}
|
|
525
|
+
if ctx.Err() != nil { // the caller gave up while the body was arriving
|
|
526
|
+
return nil, fail(context.Cause(ctx))
|
|
527
|
+
}
|
|
528
|
+
return &RawResponse{
|
|
529
|
+
Status: resp.StatusCode,
|
|
530
|
+
Header: resp.Header.Clone(),
|
|
531
|
+
Body: data,
|
|
532
|
+
URL: url,
|
|
533
|
+
RequestID: resp.Header.Get(requestIDHeader),
|
|
534
|
+
tag: tag,
|
|
535
|
+
}, nil
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// readBody reads and closes body, giving up when ctx ends even if the body ignores
|
|
539
|
+
// cancellation (a transport that does not honor the request's context).
|
|
540
|
+
func readBody(ctx context.Context, body io.ReadCloser) ([]byte, error) {
|
|
541
|
+
if body == nil {
|
|
542
|
+
return nil, nil
|
|
543
|
+
}
|
|
544
|
+
type result struct {
|
|
545
|
+
data []byte
|
|
546
|
+
err error
|
|
547
|
+
}
|
|
548
|
+
done := make(chan result, 1)
|
|
549
|
+
go func() {
|
|
550
|
+
data, err := io.ReadAll(body)
|
|
551
|
+
done <- result{data, err}
|
|
552
|
+
}()
|
|
553
|
+
select {
|
|
554
|
+
case r := <-done:
|
|
555
|
+
_ = body.Close()
|
|
556
|
+
return r.data, r.err
|
|
557
|
+
case <-ctx.Done():
|
|
558
|
+
_ = body.Close()
|
|
559
|
+
return nil, context.Cause(ctx)
|
|
560
|
+
}
|
|
561
|
+
}
|