@pi-in-go/pigpen-pi-typesafe-api 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 +14 -0
- package/LICENSE +22 -0
- package/README.md +30 -0
- package/ask.go +62 -0
- package/ask_test.go +76 -0
- package/auth.go +249 -0
- package/auth_test.go +131 -0
- package/backends.go +336 -0
- package/backends_test.go +404 -0
- package/batch.go +202 -0
- package/batch_test.go +202 -0
- package/battery_test.go +41 -0
- package/calibrate.go +354 -0
- package/calibrate_test.go +186 -0
- package/client.go +615 -0
- package/client_test.go +490 -0
- package/credentials.go +252 -0
- package/credentials_test.go +216 -0
- package/doc.go +14 -0
- package/errors.go +143 -0
- package/evaluation.go +86 -0
- package/evaluation_schema.json +264 -0
- package/gaps_test.go +77 -0
- package/go.mod +9 -0
- package/go.sum +2 -0
- package/helpers_test.go +169 -0
- package/hostmodel/hostmodel.go +87 -0
- package/json.go +299 -0
- package/json_test.go +92 -0
- package/ownmodel_test.go +79 -0
- package/package.json +40 -0
- package/port/PORT.md +6 -0
- package/provenance.json +18 -0
- package/review_test.go +23 -0
- package/schema.go +473 -0
- package/schema_test.go +262 -0
- package/testdata/tools/typebox-messages.mts +5 -0
- package/testdata/typebox-messages.json +285 -0
- package/twin_test.go +28 -0
- package/ui/fakehost_test.go +548 -0
- package/ui/keyprompt.go +115 -0
- package/ui/login.go +106 -0
- package/ui/twin_test.go +28 -0
- package/ui/ui_test.go +285 -0
- package/usage.go +366 -0
- package/usage_test.go +139 -0
package/json.go
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
package pitypesafe
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"errors"
|
|
7
|
+
"fmt"
|
|
8
|
+
"io"
|
|
9
|
+
"math"
|
|
10
|
+
"strconv"
|
|
11
|
+
"strings"
|
|
12
|
+
"unicode/utf8"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// The library moves JSON as a tree of nil, bool, float64, string, []any and
|
|
16
|
+
// *Object. Object remembers key order the way a JavaScript object does, because
|
|
17
|
+
// the order of questions, criteria and answers is observable: it decides how a
|
|
18
|
+
// large request is chunked, the order the model reads Choice options in, and the
|
|
19
|
+
// order answers are printed. Go maps cannot carry it.
|
|
20
|
+
|
|
21
|
+
// Object is a JSON object that keeps insertion order.
|
|
22
|
+
type Object struct {
|
|
23
|
+
keys []string
|
|
24
|
+
vals map[string]any
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// NewObject returns an empty Object.
|
|
28
|
+
func NewObject() *Object { return &Object{vals: map[string]any{}} }
|
|
29
|
+
|
|
30
|
+
// Set stores a value. Setting an existing key keeps its position, as JavaScript does.
|
|
31
|
+
func (o *Object) Set(key string, value any) {
|
|
32
|
+
if _, ok := o.vals[key]; !ok {
|
|
33
|
+
o.keys = append(o.keys, key)
|
|
34
|
+
}
|
|
35
|
+
o.vals[key] = value
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Get returns the value for key and whether it is present.
|
|
39
|
+
func (o *Object) Get(key string) (any, bool) {
|
|
40
|
+
v, ok := o.vals[key]
|
|
41
|
+
return v, ok
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Has reports whether key is present.
|
|
45
|
+
func (o *Object) Has(key string) bool { _, ok := o.vals[key]; return ok }
|
|
46
|
+
|
|
47
|
+
// Delete removes a key.
|
|
48
|
+
func (o *Object) Delete(key string) {
|
|
49
|
+
if _, ok := o.vals[key]; !ok {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
delete(o.vals, key)
|
|
53
|
+
for i, k := range o.keys {
|
|
54
|
+
if k == key {
|
|
55
|
+
o.keys = append(o.keys[:i:i], o.keys[i+1:]...)
|
|
56
|
+
break
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Keys returns the keys in insertion order. The slice is a copy.
|
|
62
|
+
func (o *Object) Keys() []string { return append([]string(nil), o.keys...) }
|
|
63
|
+
|
|
64
|
+
// Len is the number of keys.
|
|
65
|
+
func (o *Object) Len() int { return len(o.keys) }
|
|
66
|
+
|
|
67
|
+
// Clone copies the object and every value below it.
|
|
68
|
+
func (o *Object) Clone() *Object {
|
|
69
|
+
c := NewObject()
|
|
70
|
+
for _, k := range o.keys {
|
|
71
|
+
c.Set(k, cloneValue(o.vals[k]))
|
|
72
|
+
}
|
|
73
|
+
return c
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
func cloneValue(v any) any {
|
|
77
|
+
switch t := v.(type) {
|
|
78
|
+
case *Object:
|
|
79
|
+
return t.Clone()
|
|
80
|
+
case []any:
|
|
81
|
+
out := make([]any, len(t))
|
|
82
|
+
for i, e := range t {
|
|
83
|
+
out[i] = cloneValue(e)
|
|
84
|
+
}
|
|
85
|
+
return out
|
|
86
|
+
}
|
|
87
|
+
return v
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ParseJSON decodes JSON text into a tree that keeps object key order. Numbers are float64, like JavaScript's.
|
|
91
|
+
func ParseJSON(data []byte) (any, error) {
|
|
92
|
+
dec := json.NewDecoder(bytes.NewReader(data))
|
|
93
|
+
dec.UseNumber()
|
|
94
|
+
v, err := parseValue(dec)
|
|
95
|
+
if err != nil {
|
|
96
|
+
return nil, err
|
|
97
|
+
}
|
|
98
|
+
if _, err := dec.Token(); !errors.Is(err, io.EOF) {
|
|
99
|
+
return nil, errors.New("unexpected data after the JSON value")
|
|
100
|
+
}
|
|
101
|
+
return v, nil
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
func parseValue(dec *json.Decoder) (any, error) {
|
|
105
|
+
tok, err := dec.Token()
|
|
106
|
+
if err != nil {
|
|
107
|
+
return nil, err
|
|
108
|
+
}
|
|
109
|
+
switch t := tok.(type) {
|
|
110
|
+
case json.Delim:
|
|
111
|
+
switch t {
|
|
112
|
+
case '{':
|
|
113
|
+
obj := NewObject()
|
|
114
|
+
for dec.More() {
|
|
115
|
+
keyTok, err := dec.Token()
|
|
116
|
+
if err != nil {
|
|
117
|
+
return nil, err
|
|
118
|
+
}
|
|
119
|
+
key, _ := keyTok.(string)
|
|
120
|
+
v, err := parseValue(dec)
|
|
121
|
+
if err != nil {
|
|
122
|
+
return nil, err
|
|
123
|
+
}
|
|
124
|
+
obj.Set(key, v)
|
|
125
|
+
}
|
|
126
|
+
if _, err := dec.Token(); err != nil {
|
|
127
|
+
return nil, err
|
|
128
|
+
}
|
|
129
|
+
return obj, nil
|
|
130
|
+
case '[':
|
|
131
|
+
arr := []any{}
|
|
132
|
+
for dec.More() {
|
|
133
|
+
v, err := parseValue(dec)
|
|
134
|
+
if err != nil {
|
|
135
|
+
return nil, err
|
|
136
|
+
}
|
|
137
|
+
arr = append(arr, v)
|
|
138
|
+
}
|
|
139
|
+
if _, err := dec.Token(); err != nil {
|
|
140
|
+
return nil, err
|
|
141
|
+
}
|
|
142
|
+
return arr, nil
|
|
143
|
+
}
|
|
144
|
+
return nil, fmt.Errorf("unexpected delimiter %q", t)
|
|
145
|
+
case json.Number:
|
|
146
|
+
f, err := strconv.ParseFloat(string(t), 64)
|
|
147
|
+
if err != nil && !math.IsInf(f, 0) {
|
|
148
|
+
return nil, err
|
|
149
|
+
}
|
|
150
|
+
return f, nil
|
|
151
|
+
default:
|
|
152
|
+
return tok, nil // string, bool, nil
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// FromGo converts any Go value to a tree: a tree passes through, everything else takes the
|
|
157
|
+
// encoding/json round trip. A value JSON cannot carry (a cycle, NaN, a function) is an error.
|
|
158
|
+
func FromGo(v any) (any, error) {
|
|
159
|
+
if isTree(v) {
|
|
160
|
+
return v, nil
|
|
161
|
+
}
|
|
162
|
+
data, err := json.Marshal(v)
|
|
163
|
+
if err != nil {
|
|
164
|
+
return nil, err
|
|
165
|
+
}
|
|
166
|
+
return ParseJSON(data)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
func isTree(v any) bool {
|
|
170
|
+
switch t := v.(type) {
|
|
171
|
+
case nil, bool, string:
|
|
172
|
+
return true
|
|
173
|
+
case float64:
|
|
174
|
+
return !math.IsNaN(t) && !math.IsInf(t, 0)
|
|
175
|
+
case *Object:
|
|
176
|
+
for _, k := range t.keys {
|
|
177
|
+
if !isTree(t.vals[k]) {
|
|
178
|
+
return false
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return t != nil
|
|
182
|
+
case []any:
|
|
183
|
+
for _, e := range t {
|
|
184
|
+
if !isTree(e) {
|
|
185
|
+
return false
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return true
|
|
189
|
+
}
|
|
190
|
+
return false
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// EncodeJSON serializes a tree the way JSON.stringify does: keys in insertion order, no
|
|
194
|
+
// insignificant whitespace, U+2028 and U+2029 unescaped, numbers in JavaScript's format. The
|
|
195
|
+
// byte length of this text is what the input limit measures.
|
|
196
|
+
func EncodeJSON(v any) ([]byte, error) {
|
|
197
|
+
var b bytes.Buffer
|
|
198
|
+
if err := encodeValue(&b, v); err != nil {
|
|
199
|
+
return nil, err
|
|
200
|
+
}
|
|
201
|
+
return b.Bytes(), nil
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
func encodeValue(b *bytes.Buffer, v any) error {
|
|
205
|
+
switch t := v.(type) {
|
|
206
|
+
case nil:
|
|
207
|
+
b.WriteString("null")
|
|
208
|
+
case bool:
|
|
209
|
+
b.WriteString(strconv.FormatBool(t))
|
|
210
|
+
case string:
|
|
211
|
+
writeJSString(b, t)
|
|
212
|
+
case float64:
|
|
213
|
+
if math.IsNaN(t) || math.IsInf(t, 0) {
|
|
214
|
+
return fmt.Errorf("cannot encode %v as JSON", t)
|
|
215
|
+
}
|
|
216
|
+
b.WriteString(jsNumber(t))
|
|
217
|
+
case *Object:
|
|
218
|
+
b.WriteByte('{')
|
|
219
|
+
for i, k := range t.keys {
|
|
220
|
+
if i > 0 {
|
|
221
|
+
b.WriteByte(',')
|
|
222
|
+
}
|
|
223
|
+
writeJSString(b, k)
|
|
224
|
+
b.WriteByte(':')
|
|
225
|
+
if err := encodeValue(b, t.vals[k]); err != nil {
|
|
226
|
+
return err
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
b.WriteByte('}')
|
|
230
|
+
case []any:
|
|
231
|
+
b.WriteByte('[')
|
|
232
|
+
for i, e := range t {
|
|
233
|
+
if i > 0 {
|
|
234
|
+
b.WriteByte(',')
|
|
235
|
+
}
|
|
236
|
+
if err := encodeValue(b, e); err != nil {
|
|
237
|
+
return err
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
b.WriteByte(']')
|
|
241
|
+
default:
|
|
242
|
+
tree, err := FromGo(v)
|
|
243
|
+
if err != nil {
|
|
244
|
+
return err
|
|
245
|
+
}
|
|
246
|
+
if isTree(tree) {
|
|
247
|
+
return encodeValue(b, tree)
|
|
248
|
+
}
|
|
249
|
+
return fmt.Errorf("cannot encode %T as JSON", v)
|
|
250
|
+
}
|
|
251
|
+
return nil
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
func writeJSString(b *bytes.Buffer, s string) {
|
|
255
|
+
b.WriteByte('"')
|
|
256
|
+
for _, r := range strings.ToValidUTF8(s, "\uFFFD") {
|
|
257
|
+
switch r {
|
|
258
|
+
case '"':
|
|
259
|
+
b.WriteString(`\"`)
|
|
260
|
+
case '\\':
|
|
261
|
+
b.WriteString(`\\`)
|
|
262
|
+
case '\b':
|
|
263
|
+
b.WriteString(`\b`)
|
|
264
|
+
case '\f':
|
|
265
|
+
b.WriteString(`\f`)
|
|
266
|
+
case '\n':
|
|
267
|
+
b.WriteString(`\n`)
|
|
268
|
+
case '\r':
|
|
269
|
+
b.WriteString(`\r`)
|
|
270
|
+
case '\t':
|
|
271
|
+
b.WriteString(`\t`)
|
|
272
|
+
default:
|
|
273
|
+
if r < 0x20 {
|
|
274
|
+
fmt.Fprintf(b, `\u%04x`, r)
|
|
275
|
+
} else {
|
|
276
|
+
var buf [utf8.UTFMax]byte
|
|
277
|
+
n := utf8.EncodeRune(buf[:], r)
|
|
278
|
+
b.Write(buf[:n])
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
b.WriteByte('"')
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// jsNumber formats like JavaScript's Number.prototype.toString for the JSON range.
|
|
286
|
+
func jsNumber(f float64) string {
|
|
287
|
+
if f == 0 {
|
|
288
|
+
return "0" // also -0
|
|
289
|
+
}
|
|
290
|
+
abs := math.Abs(f)
|
|
291
|
+
if abs >= 1e21 || abs < 1e-6 {
|
|
292
|
+
s := strconv.FormatFloat(f, 'e', -1, 64) // 1e+21, 1.5e-07
|
|
293
|
+
mant, exp, _ := strings.Cut(s, "e")
|
|
294
|
+
sign := exp[0]
|
|
295
|
+
digits := strings.TrimLeft(exp[1:], "0")
|
|
296
|
+
return mant + "e" + string(sign) + digits
|
|
297
|
+
}
|
|
298
|
+
return strconv.FormatFloat(f, 'f', -1, 64)
|
|
299
|
+
}
|
package/json_test.go
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
package pitypesafe
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"math"
|
|
5
|
+
"testing"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
func TestObjectKeepsInsertionOrderThroughParseAndEncode(t *testing.T) {
|
|
9
|
+
in := `{"z":1,"a":{"y":[true,null,"x"],"b":2},"m":"é\u2028\n"}`
|
|
10
|
+
v, err := ParseJSON([]byte(in))
|
|
11
|
+
if err != nil {
|
|
12
|
+
t.Fatal(err)
|
|
13
|
+
}
|
|
14
|
+
obj := v.(*Object)
|
|
15
|
+
if got := obj.Keys(); len(got) != 3 || got[0] != "z" || got[1] != "a" || got[2] != "m" {
|
|
16
|
+
t.Fatalf("keys = %v", got)
|
|
17
|
+
}
|
|
18
|
+
out, err := EncodeJSON(v)
|
|
19
|
+
if err != nil {
|
|
20
|
+
t.Fatal(err)
|
|
21
|
+
}
|
|
22
|
+
// U+2028 stays raw (JSON.stringify does not escape it), newline is escaped.
|
|
23
|
+
want := "{\"z\":1,\"a\":{\"y\":[true,null,\"x\"],\"b\":2},\"m\":\"é\u2028\\n\"}"
|
|
24
|
+
if string(out) != want {
|
|
25
|
+
t.Fatalf("got %q want %q", out, want)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func TestEncodeJSONNumbersFollowJavaScript(t *testing.T) {
|
|
30
|
+
cases := map[float64]string{0: "0", math.Copysign(0, -1): "0", 1: "1", 0.5: "0.5", 1e21: "1e+21", 1.5e-7: "1.5e-7", 123456789012: "123456789012", -2.25: "-2.25"}
|
|
31
|
+
for in, want := range cases {
|
|
32
|
+
out, err := EncodeJSON(in)
|
|
33
|
+
if err != nil || string(out) != want {
|
|
34
|
+
t.Errorf("EncodeJSON(%v) = %q, %v; want %q", in, out, err, want)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if _, err := EncodeJSON(math.NaN()); err == nil {
|
|
38
|
+
t.Error("NaN must not encode")
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func TestSetKeepsPositionAndDeleteRemoves(t *testing.T) {
|
|
43
|
+
o := NewObject()
|
|
44
|
+
o.Set("a", 1.0)
|
|
45
|
+
o.Set("b", 2.0)
|
|
46
|
+
o.Set("a", 3.0)
|
|
47
|
+
o.Delete("missing")
|
|
48
|
+
if k := o.Keys(); k[0] != "a" || k[1] != "b" || o.Len() != 2 {
|
|
49
|
+
t.Fatalf("keys = %v", k)
|
|
50
|
+
}
|
|
51
|
+
o.Delete("a")
|
|
52
|
+
if k := o.Keys(); len(k) != 1 || k[0] != "b" {
|
|
53
|
+
t.Fatalf("keys after delete = %v", k)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func TestFromGoRejectsWhatJSONCannotCarry(t *testing.T) {
|
|
58
|
+
cyc := map[string]any{}
|
|
59
|
+
cyc["self"] = cyc
|
|
60
|
+
for name, v := range map[string]any{"cycle": cyc, "nan": map[string]any{"n": math.NaN()}, "inf": math.Inf(1), "func": func() {}, "chan": make(chan int)} {
|
|
61
|
+
if _, err := FromGo(v); err == nil {
|
|
62
|
+
t.Errorf("%s: want an error", name)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
tree, err := FromGo(map[string]any{"b": 1, "a": []int{1, 2}})
|
|
66
|
+
if err != nil {
|
|
67
|
+
t.Fatal(err)
|
|
68
|
+
}
|
|
69
|
+
if k := tree.(*Object).Keys(); k[0] != "a" || k[1] != "b" {
|
|
70
|
+
t.Fatalf("a Go map has no order, so keys are sorted: %v", k)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
func TestParseJSONRejectsTrailingData(t *testing.T) {
|
|
75
|
+
if _, err := ParseJSON([]byte(`{"a":1} x`)); err == nil {
|
|
76
|
+
t.Fatal("trailing data must fail")
|
|
77
|
+
}
|
|
78
|
+
if _, err := ParseJSON([]byte(`{"a":`)); err == nil {
|
|
79
|
+
t.Fatal("truncated input must fail")
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func TestCloneIsDeep(t *testing.T) {
|
|
84
|
+
v, _ := ParseJSON([]byte(`{"a":{"b":[1,{"c":2}]}}`))
|
|
85
|
+
c := v.(*Object).Clone()
|
|
86
|
+
inner, _ := c.Get("a")
|
|
87
|
+
inner.(*Object).Set("b", "changed")
|
|
88
|
+
orig, _ := v.(*Object).Get("a")
|
|
89
|
+
if b, _ := orig.(*Object).Get("b"); b == "changed" {
|
|
90
|
+
t.Fatal("clone shares structure with the original")
|
|
91
|
+
}
|
|
92
|
+
}
|
package/ownmodel_test.go
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
package pitypesafe
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"errors"
|
|
6
|
+
"strings"
|
|
7
|
+
"testing"
|
|
8
|
+
"time"
|
|
9
|
+
|
|
10
|
+
"github.com/MichaelKinsy/pigpen/components/typesafe/libraries/typesafe"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
type evalFunc func(ctx context.Context, req typesafe.SystemOneRequest) (*typesafe.SystemOneResult, error)
|
|
14
|
+
|
|
15
|
+
func (f evalFunc) SystemOne(ctx context.Context, req typesafe.SystemOneRequest, _ *typesafe.RequestOptions) (*typesafe.SystemOneResult, error) {
|
|
16
|
+
return f(ctx, req)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func TestOwnModelClientNeedsAnEvaluatorAndUsesNoKeyOrHost(t *testing.T) {
|
|
20
|
+
isolate(t)
|
|
21
|
+
if _, err := New(Options{Backend: "ownmodel"}); !hasCode(err, CodeConfiguration) || !strings.Contains(err.Error(), "Evaluator") {
|
|
22
|
+
t.Fatalf("err = %v", err)
|
|
23
|
+
}
|
|
24
|
+
c, err := New(Options{Backend: "ownmodel", Ledger: OpenUsageLedger(LedgerOptions{Path: t.TempDir() + "/u.json"}), Evaluator: evalFunc(func(_ context.Context, req typesafe.SystemOneRequest) (*typesafe.SystemOneResult, error) {
|
|
25
|
+
return &typesafe.SystemOneResult{Model: "m", Answers: map[string]typesafe.Answer{"yes": typesafe.NoulAnswer{Noul: 0.4}}, Usage: typesafe.Usage{InputTokens: 3}}, nil
|
|
26
|
+
})})
|
|
27
|
+
if err != nil {
|
|
28
|
+
t.Fatal(err)
|
|
29
|
+
}
|
|
30
|
+
ev, err := c.Evaluate(context.Background(), sampleRequest())
|
|
31
|
+
if err != nil || ev.Model != "m" || c.GetUsage().InputTokens != 3 {
|
|
32
|
+
t.Fatalf("ev=%v err=%v", ev, err)
|
|
33
|
+
}
|
|
34
|
+
// No key is involved, so nothing is written to the auth state.
|
|
35
|
+
if s := mustAuth(t, nil); s.Verified {
|
|
36
|
+
t.Error("an own-model success must not verify a TypeSafe key")
|
|
37
|
+
}
|
|
38
|
+
if _, err := c.ListModels(context.Background()); !hasCode(err, CodeConfiguration) {
|
|
39
|
+
t.Errorf("list models: %v", err)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func TestOwnModelTimeoutAndErrorsAreClassified(t *testing.T) {
|
|
44
|
+
isolate(t)
|
|
45
|
+
slow := evalFunc(func(ctx context.Context, _ typesafe.SystemOneRequest) (*typesafe.SystemOneResult, error) {
|
|
46
|
+
<-ctx.Done()
|
|
47
|
+
return nil, ctx.Err()
|
|
48
|
+
})
|
|
49
|
+
c, _ := New(Options{Backend: "ownmodel", Timeout: 10 * time.Millisecond, Evaluator: slow, Ledger: OpenUsageLedger(LedgerOptions{Path: t.TempDir() + "/u.json"})})
|
|
50
|
+
if _, err := c.Evaluate(context.Background(), sampleRequest()); !hasCode(err, CodeTimeout) || !strings.Contains(err.Error(), "The configured model") {
|
|
51
|
+
t.Fatalf("timeout: %v", err)
|
|
52
|
+
}
|
|
53
|
+
c2, _ := New(Options{Backend: "ownmodel", Evaluator: evalFunc(func(context.Context, typesafe.SystemOneRequest) (*typesafe.SystemOneResult, error) {
|
|
54
|
+
return nil, errors.New("provider body with sk-secret")
|
|
55
|
+
}), Ledger: OpenUsageLedger(LedgerOptions{Path: t.TempDir() + "/u.json"})})
|
|
56
|
+
_, err := c2.Evaluate(context.Background(), sampleRequest())
|
|
57
|
+
if !hasCode(err, CodeResponse) || strings.Contains(err.Error(), "sk-secret") {
|
|
58
|
+
t.Fatalf("plain error: %v", err)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func TestEvaluationJSONKeepsQuestionOrderAndAddsElapsed(t *testing.T) {
|
|
63
|
+
ev := &Evaluation{
|
|
64
|
+
SystemOneResult: &typesafe.SystemOneResult{Model: "m", Answers: map[string]typesafe.Answer{"b": typesafe.NoulAnswer{Noul: 0.5}, "a": typesafe.NoulAnswer{Noul: 0.25}}, Usage: typesafe.Usage{InputTokens: 2}},
|
|
65
|
+
ElapsedMs: 7, Order: []string{"b", "a"},
|
|
66
|
+
}
|
|
67
|
+
out, err := ev.MarshalJSON()
|
|
68
|
+
if err != nil {
|
|
69
|
+
t.Fatal(err)
|
|
70
|
+
}
|
|
71
|
+
s := string(out)
|
|
72
|
+
if !(strings.Index(s, `"b"`) < strings.Index(s, `"a"`)) || !strings.HasSuffix(s, `"elapsedMs":7}`) || strings.Index(s, `"model"`) > strings.Index(s, `"answers"`) {
|
|
73
|
+
t.Fatalf("json = %s", s)
|
|
74
|
+
}
|
|
75
|
+
d, _ := ev.DetailsJSON()
|
|
76
|
+
if !strings.Contains(string(d), `"order":["b","a"]`) {
|
|
77
|
+
t.Fatalf("details = %s", d)
|
|
78
|
+
}
|
|
79
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pi-in-go/pigpen-pi-typesafe-api",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Typed Go API for extension authors, a port of pi-typesafe's library half: one admission rule, request and spend caps, key store and auth state, judgment backends (TypeSafe, OpenRouter, Command Code, custom endpoint, own model), Ask, batching, calibration. Libraries only, no extension of its own.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pig-package",
|
|
7
|
+
"typesafe",
|
|
8
|
+
"jev",
|
|
9
|
+
"library",
|
|
10
|
+
"pigpen"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "Michael Kinsy",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/MichaelKinsy/pigpen.git",
|
|
17
|
+
"directory": "components/pi-typesafe-api"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/MichaelKinsy/pigpen/tree/main/components/pi-typesafe-api#readme",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/MichaelKinsy/pigpen/issues"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"*.go",
|
|
28
|
+
"hostmodel",
|
|
29
|
+
"ui",
|
|
30
|
+
"testdata",
|
|
31
|
+
"evaluation_schema.json",
|
|
32
|
+
"go.mod",
|
|
33
|
+
"port",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE",
|
|
36
|
+
"CREDITS.md",
|
|
37
|
+
"provenance.json",
|
|
38
|
+
"go.sum"
|
|
39
|
+
]
|
|
40
|
+
}
|
package/port/PORT.md
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Port record: pi-typesafe-api
|
|
2
|
+
|
|
3
|
+
The library half of the pi-typesafe port. The unmodified original (pinned commit `ed439f834665ad6fc652787f5ba7c23852566d49`,
|
|
4
|
+
MIT, Ryan Gapac) and the complete record (file mapping, twins, scenarios, mutation results, deliberate differences) live in
|
|
5
|
+
[`components/pi-typesafe/port/`](../../pi-typesafe/port/PORT.md); the original is vendored there once, not twice. This Package's
|
|
6
|
+
Go twins of the library test files (`*_test.go` next to the code) are counted in that record.
|
package/provenance.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"origin": "ported",
|
|
3
|
+
"authors": ["Michael Kinsy"],
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"licenseFile": "LICENSE",
|
|
6
|
+
"upstreams": [
|
|
7
|
+
{
|
|
8
|
+
"name": "pi-typesafe",
|
|
9
|
+
"authors": ["Ryan Gapac"],
|
|
10
|
+
"url": "https://github.com/DevMortimer/pi-typesafe",
|
|
11
|
+
"revision": "ed439f834665ad6fc652787f5ba7c23852566d49",
|
|
12
|
+
"path": "port/PORT.md",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"licenseFile": "LICENSE",
|
|
15
|
+
"attributionFile": "CREDITS.md"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
package/review_test.go
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
package pitypesafe
|
|
2
|
+
|
|
3
|
+
import "testing"
|
|
4
|
+
|
|
5
|
+
// Cases added by the review (rev-pigpen-typesafe).
|
|
6
|
+
|
|
7
|
+
// Plain http: is allowed only for a loopback address. The original's WHATWG URL parser rejects a dotted
|
|
8
|
+
// address with an octet above 255 (new URL throws), so 127.999.0.1 never passes its 127.0.0.0/8 rule; Go's
|
|
9
|
+
// url.Parse accepts it as a host name, which the resolver would then look up in DNS and reach over plain
|
|
10
|
+
// http: with the endpoint's key.
|
|
11
|
+
func TestLoopbackHTTPNeedsARealLoopbackAddress(t *testing.T) {
|
|
12
|
+
for _, host := range []string{"http://127.999.0.1", "http://127.0.0.256:8080"} {
|
|
13
|
+
_, err := ResolveBackend(map[string]any{"label": "Local", "host": host, "keyEnv": "GATEWAY_JEV_KEY"})
|
|
14
|
+
if !hasCode(err, CodeConfiguration) || err.Error() != hostMessage {
|
|
15
|
+
t.Errorf("%s: %v", host, err)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
for _, host := range []string{"http://127.0.0.1:8787", "http://127.255.255.254", "http://localhost:1", "http://[::1]:9"} {
|
|
19
|
+
if _, err := ResolveBackend(map[string]any{"label": "Local", "host": host, "keyEnv": "GATEWAY_JEV_KEY"}); err != nil {
|
|
20
|
+
t.Errorf("%s: %v", host, err)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|