@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,294 @@
|
|
|
1
|
+
package typesafe
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"context"
|
|
6
|
+
"encoding/json"
|
|
7
|
+
"errors"
|
|
8
|
+
"fmt"
|
|
9
|
+
"io"
|
|
10
|
+
"math"
|
|
11
|
+
"net/http"
|
|
12
|
+
"reflect"
|
|
13
|
+
"strings"
|
|
14
|
+
"sync"
|
|
15
|
+
"testing"
|
|
16
|
+
"time"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
// twin marks a test as the twin of upstream cases (see twins_test.go): ids are
|
|
20
|
+
// "<file> | <full name>" from port/twins/typesafe-sdk-js.txt. Ported cases keep the
|
|
21
|
+
// original inputs and expectations, adapted only where the notes on the test say so.
|
|
22
|
+
func twin(t testing.TB, ids ...string) { t.Helper() }
|
|
23
|
+
|
|
24
|
+
// skipTwin records upstream cases with no Go counterpart, with the reason, and skips.
|
|
25
|
+
func skipTwin(t testing.TB, reason string, ids ...string) {
|
|
26
|
+
t.Helper()
|
|
27
|
+
t.Skip(reason)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// recordedRequest is what the fake transport saw.
|
|
31
|
+
type recordedRequest struct {
|
|
32
|
+
URL string
|
|
33
|
+
Method string
|
|
34
|
+
Header http.Header
|
|
35
|
+
Raw []byte
|
|
36
|
+
Body any // decoded JSON body, nil when there is none
|
|
37
|
+
Ctx context.Context
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// mockDoer is the analog of the upstream mockFetch: it records requests and answers with
|
|
41
|
+
// whatever respond returns.
|
|
42
|
+
type mockDoer struct {
|
|
43
|
+
mu sync.Mutex
|
|
44
|
+
requests []recordedRequest
|
|
45
|
+
respond func(r *recordedRequest) (*http.Response, error)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func newMock(respond func(r *recordedRequest) (*http.Response, error)) *mockDoer {
|
|
49
|
+
return &mockDoer{respond: respond}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func (m *mockDoer) Do(req *http.Request) (*http.Response, error) {
|
|
53
|
+
rec := recordedRequest{URL: req.URL.String(), Method: req.Method, Header: req.Header.Clone(), Ctx: req.Context()}
|
|
54
|
+
if req.Body != nil {
|
|
55
|
+
rec.Raw, _ = io.ReadAll(req.Body)
|
|
56
|
+
if len(rec.Raw) > 0 {
|
|
57
|
+
_ = json.Unmarshal(rec.Raw, &rec.Body)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
m.mu.Lock()
|
|
61
|
+
m.requests = append(m.requests, rec)
|
|
62
|
+
m.mu.Unlock()
|
|
63
|
+
return m.respond(&rec)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func (m *mockDoer) count() int {
|
|
67
|
+
m.mu.Lock()
|
|
68
|
+
defer m.mu.Unlock()
|
|
69
|
+
return len(m.requests)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func (m *mockDoer) req(i int) recordedRequest {
|
|
73
|
+
m.mu.Lock()
|
|
74
|
+
defer m.mu.Unlock()
|
|
75
|
+
return m.requests[i]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// jsonResp builds a JSON response; headers are name, value pairs.
|
|
79
|
+
func jsonResp(status int, data any, headers ...string) *http.Response {
|
|
80
|
+
raw, err := json.Marshal(data)
|
|
81
|
+
if err != nil {
|
|
82
|
+
panic(err)
|
|
83
|
+
}
|
|
84
|
+
return textResp(status, string(raw), append([]string{"content-type", "application/json"}, headers...)...)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func textResp(status int, body string, headers ...string) *http.Response {
|
|
88
|
+
h := http.Header{}
|
|
89
|
+
for i := 0; i+1 < len(headers); i += 2 {
|
|
90
|
+
h.Set(headers[i], headers[i+1])
|
|
91
|
+
}
|
|
92
|
+
return &http.Response{StatusCode: status, Status: fmt.Sprintf("%d %s", status, http.StatusText(status)), Header: h, Body: io.NopCloser(strings.NewReader(body)), ContentLength: int64(len(body))}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
func always(resp func() *http.Response) *mockDoer {
|
|
96
|
+
return newMock(func(*recordedRequest) (*http.Response, error) { return resp(), nil })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// noEnv is an empty environment.
|
|
100
|
+
func noEnv(string) string { return "" }
|
|
101
|
+
|
|
102
|
+
func envOf(m map[string]string) func(string) string { return func(k string) string { return m[k] } }
|
|
103
|
+
|
|
104
|
+
// newClient builds a client with no environment and jitter-free delays.
|
|
105
|
+
func newClient(t testing.TB, doer HTTPDoer, mutate ...func(*Config)) *Client {
|
|
106
|
+
t.Helper()
|
|
107
|
+
cfg := Config{APIKey: "k", HTTPClient: doer, Getenv: noEnv}
|
|
108
|
+
for _, m := range mutate {
|
|
109
|
+
m(&cfg)
|
|
110
|
+
}
|
|
111
|
+
c, err := NewClient(cfg)
|
|
112
|
+
if err != nil {
|
|
113
|
+
t.Fatalf("NewClient: %v", err)
|
|
114
|
+
}
|
|
115
|
+
c.random = func() float64 { return 0 }
|
|
116
|
+
return c
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const modelsBody = `{"models":[{"name":"m","description":"d","release_date":"2026"}]}`
|
|
120
|
+
|
|
121
|
+
var modelCards = []ModelCard{{Name: "m", Description: "d", ReleaseDate: "2026"}}
|
|
122
|
+
|
|
123
|
+
var systemOneResponse = map[string]any{
|
|
124
|
+
"model": "m",
|
|
125
|
+
"answers": map[string]any{"q1": map[string]any{"type": "noul", "noul": 0.5}},
|
|
126
|
+
"usage": map[string]any{"input_tokens": 1, "output_tokens": 1},
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// jsonEq compares got (any Go value, marshalled) with want (JSON text), ignoring key order.
|
|
130
|
+
func jsonEq(t testing.TB, got any, want string) {
|
|
131
|
+
t.Helper()
|
|
132
|
+
raw, err := json.Marshal(got)
|
|
133
|
+
if err != nil {
|
|
134
|
+
t.Fatalf("marshal: %v", err)
|
|
135
|
+
}
|
|
136
|
+
var g, w any
|
|
137
|
+
if err := json.Unmarshal(raw, &g); err != nil {
|
|
138
|
+
t.Fatalf("got is not JSON: %s", raw)
|
|
139
|
+
}
|
|
140
|
+
if err := json.Unmarshal([]byte(want), &w); err != nil {
|
|
141
|
+
t.Fatalf("bad want %s: %v", want, err)
|
|
142
|
+
}
|
|
143
|
+
if !reflect.DeepEqual(g, w) {
|
|
144
|
+
t.Fatalf("JSON mismatch\n got: %s\nwant: %s", raw, want)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
func decode(t testing.TB, s string) any {
|
|
149
|
+
t.Helper()
|
|
150
|
+
var v any
|
|
151
|
+
if err := json.Unmarshal([]byte(s), &v); err != nil {
|
|
152
|
+
t.Fatalf("bad JSON %s: %v", s, err)
|
|
153
|
+
}
|
|
154
|
+
return v
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// recordingLogger records every call.
|
|
158
|
+
type logCall struct {
|
|
159
|
+
Level string
|
|
160
|
+
Message string
|
|
161
|
+
Args []any
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
type recordingLogger struct {
|
|
165
|
+
mu sync.Mutex
|
|
166
|
+
calls []logCall
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
func (l *recordingLogger) add(level, msg string, args []any) {
|
|
170
|
+
l.mu.Lock()
|
|
171
|
+
defer l.mu.Unlock()
|
|
172
|
+
l.calls = append(l.calls, logCall{level, msg, args})
|
|
173
|
+
}
|
|
174
|
+
func (l *recordingLogger) Debug(m string, a ...any) { l.add("debug", m, a) }
|
|
175
|
+
func (l *recordingLogger) Info(m string, a ...any) { l.add("info", m, a) }
|
|
176
|
+
func (l *recordingLogger) Warn(m string, a ...any) { l.add("warn", m, a) }
|
|
177
|
+
func (l *recordingLogger) Error(m string, a ...any) { l.add("error", m, a) }
|
|
178
|
+
|
|
179
|
+
func (l *recordingLogger) snapshot() []logCall {
|
|
180
|
+
l.mu.Lock()
|
|
181
|
+
defer l.mu.Unlock()
|
|
182
|
+
return append([]logCall(nil), l.calls...)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
func (l *recordingLogger) messages(level string) []string {
|
|
186
|
+
var out []string
|
|
187
|
+
for _, c := range l.snapshot() {
|
|
188
|
+
if level == "" || c.Level == level {
|
|
189
|
+
out = append(out, c.Message)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return out
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// hangingDoer waits for the request's context to end, like a hung fetch.
|
|
196
|
+
func hangingDoer() *mockDoer {
|
|
197
|
+
return newMock(func(r *recordedRequest) (*http.Response, error) {
|
|
198
|
+
<-r.Ctx.Done()
|
|
199
|
+
return nil, r.Ctx.Err()
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// blockingBody is a response body that never yields until closed, whatever the context.
|
|
204
|
+
type blockingBody struct {
|
|
205
|
+
mu sync.Mutex
|
|
206
|
+
closed chan struct{}
|
|
207
|
+
closes int
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
func newBlockingBody() *blockingBody { return &blockingBody{closed: make(chan struct{})} }
|
|
211
|
+
|
|
212
|
+
func (b *blockingBody) Read([]byte) (int, error) {
|
|
213
|
+
<-b.closed
|
|
214
|
+
return 0, io.ErrClosedPipe
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
func (b *blockingBody) Close() error {
|
|
218
|
+
b.mu.Lock()
|
|
219
|
+
defer b.mu.Unlock()
|
|
220
|
+
if b.closes == 0 {
|
|
221
|
+
close(b.closed)
|
|
222
|
+
}
|
|
223
|
+
b.closes++
|
|
224
|
+
return nil
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
func (b *blockingBody) closeCount() int {
|
|
228
|
+
b.mu.Lock()
|
|
229
|
+
defer b.mu.Unlock()
|
|
230
|
+
return b.closes
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// errBody fails its first read with err.
|
|
234
|
+
type errBody struct{ err error }
|
|
235
|
+
|
|
236
|
+
func (b errBody) Read([]byte) (int, error) { return 0, b.err }
|
|
237
|
+
func (b errBody) Close() error { return nil }
|
|
238
|
+
|
|
239
|
+
func respWithBody(status int, body io.ReadCloser, headers ...string) *http.Response {
|
|
240
|
+
r := textResp(status, "", headers...)
|
|
241
|
+
r.Body = body
|
|
242
|
+
r.ContentLength = -1
|
|
243
|
+
return r
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
func mustAs[T any](t testing.TB, err error) T {
|
|
247
|
+
t.Helper()
|
|
248
|
+
var target T
|
|
249
|
+
if !errors.As(err, &target) {
|
|
250
|
+
t.Fatalf("error %T (%v) is not %T", err, err, target)
|
|
251
|
+
}
|
|
252
|
+
return target
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
func notAs[T any](t testing.TB, err error) {
|
|
256
|
+
t.Helper()
|
|
257
|
+
var target T
|
|
258
|
+
if errors.As(err, &target) {
|
|
259
|
+
t.Fatalf("error %T (%v) unexpectedly is %T", err, err, target)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
func contains(t testing.TB, s, sub string) {
|
|
264
|
+
t.Helper()
|
|
265
|
+
if !strings.Contains(s, sub) {
|
|
266
|
+
t.Fatalf("%q does not contain %q", s, sub)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
func eq[T any](t testing.TB, got, want T) {
|
|
271
|
+
t.Helper()
|
|
272
|
+
if !reflect.DeepEqual(got, want) {
|
|
273
|
+
t.Fatalf("got %#v, want %#v", got, want)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
func noErr(t testing.TB, err error) {
|
|
278
|
+
t.Helper()
|
|
279
|
+
if err != nil {
|
|
280
|
+
t.Fatalf("unexpected error %T: %v", err, err)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
func ctxBG() context.Context { return context.Background() }
|
|
285
|
+
|
|
286
|
+
var _ = bytes.NewReader
|
|
287
|
+
var _ = time.Second
|
|
288
|
+
|
|
289
|
+
func fmtAny(v any) string { return fmt.Sprintf("%v %+v", v, v) }
|
|
290
|
+
|
|
291
|
+
func nan() float64 { return math.NaN() }
|
|
292
|
+
func inf() float64 { return math.Inf(1) }
|
|
293
|
+
|
|
294
|
+
func asErr[T any](err error, target *T) bool { return errors.As(err, target) }
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
package typesafe_test
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"os"
|
|
7
|
+
"os/exec"
|
|
8
|
+
"sort"
|
|
9
|
+
"testing"
|
|
10
|
+
|
|
11
|
+
"github.com/MichaelKinsy/pigpen/components/typesafe/libraries/typesafe"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
// TestLive_AgainstTheRealAPI runs only when the owner supplies TYPESAFE_API_KEY; without it,
|
|
15
|
+
// nothing touches the network. It checks that a real response decodes into the typed
|
|
16
|
+
// results and, when TYPESAFE_LIVE_JS_DIST names the official SDK's dist/index.mjs and node is
|
|
17
|
+
// installed, that the official SDK sees the same model list and the same answer shapes.
|
|
18
|
+
func TestLive_AgainstTheRealAPI(t *testing.T) {
|
|
19
|
+
if os.Getenv("TYPESAFE_API_KEY") == "" {
|
|
20
|
+
t.Skip("TYPESAFE_API_KEY is not set: no live check (the fake-server cross-check runs instead)")
|
|
21
|
+
}
|
|
22
|
+
client, err := typesafe.NewClient(typesafe.Config{})
|
|
23
|
+
if err != nil {
|
|
24
|
+
t.Fatal(err)
|
|
25
|
+
}
|
|
26
|
+
models, err := client.Models().List(t.Context(), nil)
|
|
27
|
+
if err != nil || len(models) == 0 {
|
|
28
|
+
t.Fatalf("models: %v %v", models, err)
|
|
29
|
+
}
|
|
30
|
+
res, err := client.SystemOne(t.Context(), typesafe.SystemOneRequest{
|
|
31
|
+
State: typesafe.Text("I was charged twice for my subscription. Please refund one payment."),
|
|
32
|
+
Questions: typesafe.Questions{
|
|
33
|
+
typesafe.Ask("billing", typesafe.Noul("Is this about billing?")),
|
|
34
|
+
typesafe.Ask("kind", typesafe.Choice("What does the customer want?", typesafe.Opt("refund", "a refund"), typesafe.Opt("cancel", "to cancel"), typesafe.Opt("other", nil))),
|
|
35
|
+
typesafe.Ask("urgency", typesafe.Score("How urgent is it?", "not urgent", "somewhat urgent", "urgent")),
|
|
36
|
+
},
|
|
37
|
+
}, nil)
|
|
38
|
+
if err != nil {
|
|
39
|
+
t.Fatal(err)
|
|
40
|
+
}
|
|
41
|
+
billing, _ := res.Noul("billing")
|
|
42
|
+
kind, _ := res.Choice("kind")
|
|
43
|
+
urgency, _ := res.Score("urgency")
|
|
44
|
+
if billing.Noul < 0 || billing.Noul > 1 || len(kind.Probabilities) != 3 || len(urgency.Probabilities) != 3 || res.Usage.InputTokens == 0 {
|
|
45
|
+
t.Fatalf("unexpected result: %+v", res)
|
|
46
|
+
}
|
|
47
|
+
dist := os.Getenv("TYPESAFE_LIVE_JS_DIST")
|
|
48
|
+
if dist == "" {
|
|
49
|
+
t.Log("TYPESAFE_LIVE_JS_DIST not set: no comparison with the official SDK")
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
out, err := exec.CommandContext(t.Context(), "node", "../../port/crosscheck/live_js.mjs", dist).Output()
|
|
53
|
+
if err != nil {
|
|
54
|
+
t.Fatalf("live_js.mjs: %v", err)
|
|
55
|
+
}
|
|
56
|
+
var js struct {
|
|
57
|
+
Models []typesafe.ModelCard `json:"models"`
|
|
58
|
+
Result struct {
|
|
59
|
+
Answers map[string]struct {
|
|
60
|
+
Type string `json:"type"`
|
|
61
|
+
Probabilities map[string]float64 `json:"probabilities"`
|
|
62
|
+
} `json:"answers"`
|
|
63
|
+
} `json:"result"`
|
|
64
|
+
}
|
|
65
|
+
if err := json.NewDecoder(bytes.NewReader(out)).Decode(&js); err != nil {
|
|
66
|
+
t.Fatal(err)
|
|
67
|
+
}
|
|
68
|
+
names := func(ms []typesafe.ModelCard) []string {
|
|
69
|
+
var n []string
|
|
70
|
+
for _, m := range ms {
|
|
71
|
+
n = append(n, m.Name)
|
|
72
|
+
}
|
|
73
|
+
sort.Strings(n)
|
|
74
|
+
return n
|
|
75
|
+
}
|
|
76
|
+
if a, b := names(models), names(js.Models); !equalStrings(a, b) {
|
|
77
|
+
t.Errorf("model lists differ: go %v, official %v", a, b)
|
|
78
|
+
}
|
|
79
|
+
for name, want := range map[string]int{"kind": 3, "urgency": 3} {
|
|
80
|
+
if got := len(js.Result.Answers[name].Probabilities); got != want {
|
|
81
|
+
t.Errorf("official SDK: %s has %d probabilities, want %d", name, got, want)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
func equalStrings(a, b []string) bool {
|
|
87
|
+
if len(a) != len(b) {
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
for i := range a {
|
|
91
|
+
if a[i] != b[i] {
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return true
|
|
96
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
package typesafe
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
"regexp"
|
|
8
|
+
"strings"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// LogLevel is the log verbosity; LogOff disables logging.
|
|
12
|
+
type LogLevel string
|
|
13
|
+
|
|
14
|
+
// Log levels from most to least verbose.
|
|
15
|
+
const (
|
|
16
|
+
LogDebug LogLevel = "debug"
|
|
17
|
+
LogInfo LogLevel = "info"
|
|
18
|
+
LogWarn LogLevel = "warn"
|
|
19
|
+
LogError LogLevel = "error"
|
|
20
|
+
LogOff LogLevel = "off"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
// LogLevels lists the supported levels, most verbose first.
|
|
24
|
+
var LogLevels = []LogLevel{LogDebug, LogInfo, LogWarn, LogError, LogOff}
|
|
25
|
+
|
|
26
|
+
// DefaultLogLevel applies when neither the configuration nor the environment set one.
|
|
27
|
+
const DefaultLogLevel = LogWarn
|
|
28
|
+
|
|
29
|
+
// ParseLogLevel validates a level; the error names value and source (for example
|
|
30
|
+
// `Invalid log level "loud" from TYPESAFE_LOG_LEVEL. Expected one of: debug, info, warn, error, off.`).
|
|
31
|
+
func ParseLogLevel(value, source string) (LogLevel, error) {
|
|
32
|
+
for _, l := range LogLevels {
|
|
33
|
+
if string(l) == value {
|
|
34
|
+
return l, nil
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
names := make([]string, len(LogLevels))
|
|
38
|
+
for i, l := range LogLevels {
|
|
39
|
+
names[i] = string(l)
|
|
40
|
+
}
|
|
41
|
+
return "", errorf("Invalid log level %q from %s. Expected one of: %s.", value, source, strings.Join(names, ", "))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Logger receives a message and structured values. The SDK never logs at warn or
|
|
45
|
+
// error by itself; info logs request summaries and debug adds headers and bodies.
|
|
46
|
+
type Logger interface {
|
|
47
|
+
Debug(message string, args ...any)
|
|
48
|
+
Info(message string, args ...any)
|
|
49
|
+
Warn(message string, args ...any)
|
|
50
|
+
Error(message string, args ...any)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const logPrefix = "[typesafe-sdk] "
|
|
54
|
+
|
|
55
|
+
type stderrLogger struct{}
|
|
56
|
+
|
|
57
|
+
// NewStderrLogger returns the default logger: lines prefixed "[typesafe-sdk] " on stderr.
|
|
58
|
+
func NewStderrLogger() Logger { return stderrLogger{} }
|
|
59
|
+
|
|
60
|
+
func (stderrLogger) write(message string, args []any) {
|
|
61
|
+
var b strings.Builder
|
|
62
|
+
b.WriteString(logPrefix)
|
|
63
|
+
b.WriteString(message)
|
|
64
|
+
for _, a := range args {
|
|
65
|
+
b.WriteByte(' ')
|
|
66
|
+
switch v := a.(type) {
|
|
67
|
+
case error:
|
|
68
|
+
b.WriteString(v.Error())
|
|
69
|
+
case string:
|
|
70
|
+
b.WriteString(v)
|
|
71
|
+
default:
|
|
72
|
+
if raw, err := json.Marshal(a); err == nil {
|
|
73
|
+
b.Write(raw)
|
|
74
|
+
} else {
|
|
75
|
+
fmt.Fprintf(&b, "%v", a)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
b.WriteByte('\n')
|
|
80
|
+
_, _ = os.Stderr.WriteString(b.String())
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func (l stderrLogger) Debug(m string, a ...any) { l.write(m, a) }
|
|
84
|
+
func (l stderrLogger) Info(m string, a ...any) { l.write(m, a) }
|
|
85
|
+
func (l stderrLogger) Warn(m string, a ...any) { l.write(m, a) }
|
|
86
|
+
func (l stderrLogger) Error(m string, a ...any) { l.write(m, a) }
|
|
87
|
+
|
|
88
|
+
var levelRank = map[LogLevel]int{LogDebug: 0, LogInfo: 1, LogWarn: 2, LogError: 3, LogOff: 4}
|
|
89
|
+
|
|
90
|
+
type levelLogger struct {
|
|
91
|
+
sink Logger
|
|
92
|
+
rank int
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// WithLevel filters a logger to level and above.
|
|
96
|
+
func WithLevel(sink Logger, level LogLevel) Logger {
|
|
97
|
+
return levelLogger{sink: sink, rank: levelRank[level]}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
func (l levelLogger) Debug(m string, a ...any) {
|
|
101
|
+
if l.rank <= levelRank[LogDebug] {
|
|
102
|
+
l.sink.Debug(m, a...)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
func (l levelLogger) Info(m string, a ...any) {
|
|
106
|
+
if l.rank <= levelRank[LogInfo] {
|
|
107
|
+
l.sink.Info(m, a...)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
func (l levelLogger) Warn(m string, a ...any) {
|
|
111
|
+
if l.rank <= levelRank[LogWarn] {
|
|
112
|
+
l.sink.Warn(m, a...)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
func (l levelLogger) Error(m string, a ...any) {
|
|
116
|
+
if l.rank <= levelRank[LogError] {
|
|
117
|
+
l.sink.Error(m, a...)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
var whitespaceRun = regexp.MustCompile(`\s+`)
|
|
122
|
+
|
|
123
|
+
// redactKey masks a key, preserving its scheme and the last four characters of secrets longer than eight.
|
|
124
|
+
func redactKey(value string) string {
|
|
125
|
+
scheme, secret := "", value
|
|
126
|
+
if strings.Contains(value, " ") {
|
|
127
|
+
parts := whitespaceRun.Split(value, -1) // JavaScript split(/\s+/, 2) keeps the first two parts
|
|
128
|
+
scheme = parts[0]
|
|
129
|
+
secret = ""
|
|
130
|
+
if len(parts) > 1 {
|
|
131
|
+
secret = parts[1]
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
tail := ""
|
|
135
|
+
if r := []rune(secret); len(r) > 8 {
|
|
136
|
+
tail = string(r[len(r)-4:])
|
|
137
|
+
}
|
|
138
|
+
if scheme != "" {
|
|
139
|
+
scheme += " "
|
|
140
|
+
}
|
|
141
|
+
return scheme + "***" + tail
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// RedactHeaders returns a copy with credentials masked: authorization,
|
|
145
|
+
// proxy-authorization and x-api-key keep their scheme and the last four characters
|
|
146
|
+
// of secrets longer than eight ("Bearer ***cdef"); cookie and set-cookie become "***".
|
|
147
|
+
func RedactHeaders(headers map[string]string) map[string]string {
|
|
148
|
+
out := make(map[string]string, len(headers))
|
|
149
|
+
for name, value := range headers {
|
|
150
|
+
switch strings.ToLower(name) {
|
|
151
|
+
case "authorization", "proxy-authorization", "x-api-key":
|
|
152
|
+
out[name] = redactKey(value)
|
|
153
|
+
case "cookie", "set-cookie":
|
|
154
|
+
out[name] = "***"
|
|
155
|
+
default:
|
|
156
|
+
out[name] = value
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return out
|
|
160
|
+
}
|