tep 0.11.3 → 0.11.5

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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/Makefile +42 -2
  3. data/README.md +4 -4
  4. data/SINATRA_COMPAT.md +20 -20
  5. data/bin/tep +47 -10
  6. data/examples/api_gateway/app.rb +1 -1
  7. data/examples/blog/app.rb +17 -17
  8. data/examples/chat/app.rb +12 -12
  9. data/examples/chatbot/README.md +2 -2
  10. data/examples/chatbot/app.rb +24 -24
  11. data/examples/llm_gateway/app.rb +4 -4
  12. data/examples/pg_hello.rb +11 -1
  13. data/lib/spinel_kit/hex.rb +65 -0
  14. data/lib/spinel_kit/json.rb +151 -0
  15. data/lib/spinel_kit/json_decoder.rb +396 -0
  16. data/lib/{tep/logger.rb → spinel_kit/log.rb} +25 -21
  17. data/lib/spinel_kit/url.rb +166 -0
  18. data/lib/tep/auth_bearer_token.rb +6 -6
  19. data/lib/tep/auth_oauth2.rb +4 -4
  20. data/lib/tep/broadcast.rb +18 -80
  21. data/lib/tep/events.rb +37 -37
  22. data/lib/tep/http.rb +3 -3
  23. data/lib/tep/job.rb +2 -2
  24. data/lib/tep/jwt.rb +4 -4
  25. data/lib/tep/live_view.rb +4 -4
  26. data/lib/tep/llm.rb +13 -45
  27. data/lib/tep/mcp.rb +12 -12
  28. data/lib/tep/multipart.rb +1 -1
  29. data/lib/tep/net.rb +8 -3
  30. data/lib/tep/openai_server.rb +102 -94
  31. data/lib/tep/parser.rb +2 -2
  32. data/lib/tep/pg.rb +468 -14
  33. data/lib/tep/presence.rb +33 -329
  34. data/lib/tep/proxy.rb +7 -7
  35. data/lib/tep/request.rb +1 -1
  36. data/lib/tep/response.rb +1 -1
  37. data/lib/tep/router.rb +1 -1
  38. data/lib/tep/session.rb +2 -2
  39. data/lib/tep/version.rb +1 -1
  40. data/lib/tep.rb +57 -137
  41. data/spinel-ext.json +6 -0
  42. data/test/helper.rb +95 -8
  43. data/test/run_parallel.rb +44 -7
  44. data/test/test_auth.rb +17 -17
  45. data/test/test_auth_oauth2.rb +5 -5
  46. data/test/test_broadcast_pg.rb +1 -0
  47. data/test/test_http_pool.rb +4 -4
  48. data/test/test_http_pool_send.rb +3 -3
  49. data/test/test_json.rb +12 -12
  50. data/test/test_jwt.rb +4 -4
  51. data/test/test_live_view.rb +3 -3
  52. data/test/test_llm.rb +12 -9
  53. data/test/test_llm_gateway.rb +2 -2
  54. data/test/test_logger.rb +2 -2
  55. data/test/test_openai_server.rb +10 -1
  56. data/test/test_password.rb +3 -3
  57. data/test/test_pg.rb +1 -0
  58. data/test/test_presence_pg.rb +1 -0
  59. data/test/test_real_world.rb +6 -1
  60. data/test/test_shutdown.rb +40 -0
  61. metadata +23 -8
  62. data/lib/tep/json.rb +0 -572
  63. data/lib/tep/url.rb +0 -161
@@ -1,28 +1,33 @@
1
- # Tep::Logger -- minimal levelled logger for spinel-AOT'd apps.
1
+ # VENDORED from OriPekelman/spinelkit @ 09e8558 -- DO NOT EDIT HERE.
2
+ # Edit upstream and re-sync with `make vendor-spinelkit`.
3
+ # SpinelKit::Log -- minimal levelled logger for spinel-AOT'd apps.
2
4
  #
3
- # Why bundle one? CRuby's stdlib `Logger` is metaprogrammed (the
4
- # severity dispatch loop, the formatter API, the device-rotation
5
- # logic) and doesn't compile through spinel. Most app code that
6
- # wants logging really wants three things: a level guard, a
7
- # formatted line, and a destination.
5
+ # WHY THIS EXISTS. CRuby's stdlib `Logger` is metaprogrammed (the severity
6
+ # dispatch loop, the formatter API, the device-rotation logic) and the
7
+ # spinelgems catalog rejects it (unresolved calls). Most app code that wants
8
+ # logging really wants three things: a level guard, a formatted line, and a
9
+ # destination. Ported verbatim from Tep::Logger; toy gains it for free.
8
10
  #
9
11
  # Surface
10
12
  # -------
11
- #
12
- # logger = Tep::Logger.new
13
+ # logger = SpinelKit::Log.new
13
14
  # logger.set_level("info") # one of: debug / info / warn / error
14
15
  # logger.info("server up on " + port.to_s)
15
16
  # logger.error("db connect failed")
16
17
  #
17
18
  # # File output: appends to the path. Leave unset for stderr.
18
- # logger.to_file("/var/log/tep.log")
19
+ # logger.to_file("/var/log/app.log")
20
+ #
21
+ # Each line is `[<unix_seconds>] [<level>] <message>`. The integer-seconds
22
+ # timestamp is what spinel exposes from `Time.now`; wider strftime support
23
+ # would need a C-shim (defer until callers ask for it).
19
24
  #
20
- # Each line is `[<unix_seconds>] [<level>] <message>`. The
21
- # integer-seconds timestamp is what spinel exposes from `Time.now`;
22
- # wider strftime support would need a C-shim (defer until callers
23
- # ask for it).
24
- module Tep
25
- class Logger
25
+ # SPINEL NAMING DISCIPLINE: the method/param names below are the donor's
26
+ # proven-green spellings; the class-side `level_value` keeps the comparison
27
+ # a pure function so spinel pins its arg type to :str cleanly via a consumer
28
+ # type-seed. See docs/spinel-discipline.md.
29
+ module SpinelKit
30
+ class Log
26
31
  attr_accessor :min_level, :file_path
27
32
 
28
33
  def initialize
@@ -54,12 +59,11 @@ module Tep
54
59
  end
55
60
 
56
61
  def should_log?(level)
57
- Logger.level_value(level) >= Logger.level_value(@min_level)
62
+ Log.level_value(level) >= Log.level_value(@min_level)
58
63
  end
59
64
 
60
- # Class-side helper so the comparison stays a pure function and
61
- # spinel pins its arg type to :str cleanly via the type-seed in
62
- # tep.rb.
65
+ # Class-side helper so the comparison stays a pure function and spinel
66
+ # pins its arg type to :str cleanly via a consumer-side type-seed.
63
67
  def self.level_value(name)
64
68
  if name == "debug"
65
69
  return 0
@@ -73,8 +77,8 @@ module Tep
73
77
  if name == "error"
74
78
  return 3
75
79
  end
76
- # Unknown level -- treat as info so misspelled labels don't
77
- # vanish silently.
80
+ # Unknown level -- treat as info so misspelled labels don't vanish
81
+ # silently.
78
82
  1
79
83
  end
80
84
 
@@ -0,0 +1,166 @@
1
+ # VENDORED from OriPekelman/spinelkit @ 09e8558 -- DO NOT EDIT HERE.
2
+ # Edit upstream and re-sync with `make vendor-spinelkit`.
3
+ require_relative "hex"
4
+
5
+ # SpinelKit::Url -- percent-encode/decode (the CGI / URI-component surface
6
+ # Spinel can't get from stdlib) plus a form-query parser and a small URL
7
+ # splitter. Ported from Tep::Url; the hex digits now come from SpinelKit::Hex.
8
+ #
9
+ # Self-contained: the empty str=>str hashes are seeded inline (the
10
+ # `{"" => ""}`-then-delete idiom that pins Spinel's value type), and substring
11
+ # search is a private `find_idx` (the `< 0` callsites can't narrow against
12
+ # String#index's int|nil under Spinel's current model). All pure string ops.
13
+ module SpinelKit
14
+ class Url
15
+ # "%41+b" -> "A b" (form-decode: `+` is space, `%XX` is a byte).
16
+ def self.unescape(s)
17
+ out = ""
18
+ i = 0
19
+ n = s.length
20
+ while i < n
21
+ c = s[i]
22
+ if c == "+"
23
+ out = out + " "
24
+ i += 1
25
+ elsif c == "%" && i + 2 < n
26
+ hi = Hex.nibble(s[i + 1])
27
+ lo = Hex.nibble(s[i + 2])
28
+ if hi >= 0 && lo >= 0
29
+ out = out + ((hi * 16 + lo).chr)
30
+ i += 3
31
+ else
32
+ out = out + c
33
+ i += 1
34
+ end
35
+ else
36
+ out = out + c
37
+ i += 1
38
+ end
39
+ end
40
+ out
41
+ end
42
+
43
+ # Percent-encode everything outside the RFC 3986 unreserved set
44
+ # (ALPHA / DIGIT / `-._~`); the rest becomes `%XX` with UPPERCASE hex.
45
+ # (Space -> `%20`, not `+` -- this is the URI-component, not form, encoder.)
46
+ #
47
+ # Byte-oriented: under Spinel `String#[]` indexes BYTES, so a multi-byte
48
+ # UTF-8 char is encoded byte-by-byte (correct %XX of each byte). Under CRuby,
49
+ # pass a binary string for the same behaviour (else `[]` splits on chars).
50
+ def self.escape(s)
51
+ out = ""
52
+ i = 0
53
+ while i < s.length
54
+ c = s[i]
55
+ if (c >= "a" && c <= "z") || (c >= "A" && c <= "Z") ||
56
+ (c >= "0" && c <= "9") || c == "-" || c == "." ||
57
+ c == "_" || c == "~"
58
+ out = out + c
59
+ else
60
+ b = c.getbyte(0)
61
+ out = out + "%" + Hex.nibble_char(b / 16) + Hex.nibble_char(b % 16)
62
+ end
63
+ i += 1
64
+ end
65
+ out
66
+ end
67
+
68
+ # "a=1&b=2&c" -> {"a"=>"1","b"=>"2","c"=>""}. Keys + values are
69
+ # form-decoded (`unescape`).
70
+ def self.parse_query(s)
71
+ h = {"" => ""}
72
+ h.delete("")
73
+ if s.length == 0
74
+ return h
75
+ end
76
+ pairs = s.split("&")
77
+ pairs.each do |pair|
78
+ if pair.length > 0
79
+ eq = Url.find_idx(pair, "=", 0)
80
+ if eq < 0
81
+ h[Url.unescape(pair)] = ""
82
+ else
83
+ k = pair[0, eq]
84
+ v = pair[eq + 1, pair.length - eq - 1]
85
+ h[Url.unescape(k)] = Url.unescape(v)
86
+ end
87
+ end
88
+ end
89
+ h
90
+ end
91
+
92
+ # Split `http(s)://host[:port]/path?query` into a str=>str hash keyed
93
+ # scheme / host / port / path / query. Without a scheme the input is
94
+ # treated as a path (host stays empty). Default ports follow the scheme
95
+ # (80 / 443); `query` is the raw substring after `?` (not decoded).
96
+ #
97
+ # One body on purpose: Spinel widens a Hash-typed value when a helper
98
+ # mutates it and the caller keeps reading, so `out` stays StrStrHash only
99
+ # if nothing factors the mutation out (find_idx returns an int, no mutate).
100
+ def self.split_url(u)
101
+ out = {"" => ""}
102
+ out.delete("")
103
+ out["scheme"] = ""
104
+ out["host"] = ""
105
+ out["port"] = ""
106
+ out["path"] = "/"
107
+ out["query"] = ""
108
+
109
+ rest = u
110
+ if rest.length >= 7 && rest[0, 7] == "http://"
111
+ out["scheme"] = "http"
112
+ out["port"] = "80"
113
+ rest = rest[7, rest.length - 7]
114
+ elsif rest.length >= 8 && rest[0, 8] == "https://"
115
+ out["scheme"] = "https"
116
+ out["port"] = "443"
117
+ rest = rest[8, rest.length - 8]
118
+ end
119
+
120
+ if out["scheme"].length > 0
121
+ slash = Url.find_idx(rest, "/", 0)
122
+ hostport = rest
123
+ tail = "/"
124
+ if slash >= 0
125
+ hostport = rest[0, slash]
126
+ tail = rest[slash, rest.length - slash]
127
+ end
128
+ colon = Url.find_idx(hostport, ":", 0)
129
+ if colon >= 0
130
+ out["host"] = hostport[0, colon]
131
+ out["port"] = hostport[colon + 1, hostport.length - colon - 1]
132
+ else
133
+ out["host"] = hostport
134
+ end
135
+ rest = tail
136
+ end
137
+
138
+ qi = Url.find_idx(rest, "?", 0)
139
+ if qi >= 0
140
+ out["path"] = rest[0, qi]
141
+ out["query"] = rest[qi + 1, rest.length - qi - 1]
142
+ else
143
+ out["path"] = rest
144
+ end
145
+ if out["path"].length == 0
146
+ out["path"] = "/"
147
+ end
148
+ out
149
+ end
150
+
151
+ # First index of `needle` in `s` at/after `start`, or -1. Internal
152
+ # (Spinel-safe substring search; see the module comment).
153
+ def self.find_idx(s, needle, start)
154
+ nlen = needle.length
155
+ slen = s.length
156
+ pos = start
157
+ while pos <= slen - nlen
158
+ if s[pos, nlen] == needle
159
+ return pos
160
+ end
161
+ pos += 1
162
+ end
163
+ -1
164
+ end
165
+ end
166
+ end
@@ -9,7 +9,7 @@
9
9
  # Tep::AuthBearerToken.set_secret(ENV["JWT_SECRET"])
10
10
  #
11
11
  # Token payload schema (flat JSON, single level -- matches
12
- # Tep::Json's flat-object extraction surface):
12
+ # SpinelKit::Json's flat-object extraction surface):
13
13
  #
14
14
  # {
15
15
  # "sub": "user:42", # principal_id (required)
@@ -22,7 +22,7 @@
22
22
  # # agent_id|issued_at|expires_at|origin
23
23
  # }
24
24
  #
25
- # Why flat (not nested `acting_via: { ... }`): Tep::Json today
25
+ # Why flat (not nested `acting_via: { ... }`): SpinelKit::Json today
26
26
  # extracts flat keys only. A nested-object getter is a separate
27
27
  # tiny battery; for v1 of Auth the flat pipe-encoded delegate
28
28
  # string is the smallest thing that ships and round-trips
@@ -64,20 +64,20 @@ module Tep
64
64
 
65
65
  # Check expiry first -- a token whose exp passed gets rejected
66
66
  # even if the signature still verifies. exp is unix epoch sec.
67
- exp = Tep::Json.get_int(payload, "exp")
67
+ exp = SpinelKit::Json.get_int(payload, "exp")
68
68
  if exp > 0 && Time.now.to_i >= exp
69
69
  return nil
70
70
  end
71
71
 
72
- sub = Tep::Json.get_str(payload, "sub")
72
+ sub = SpinelKit::Json.get_str(payload, "sub")
73
73
  if sub.length == 0
74
74
  return nil
75
75
  end
76
76
 
77
- caps_str = Tep::Json.get_str(payload, "caps")
77
+ caps_str = SpinelKit::Json.get_str(payload, "caps")
78
78
  caps = Tep::AuthBearerToken.parse_caps(caps_str)
79
79
 
80
- delegate_str = Tep::Json.get_str(payload, "delegate")
80
+ delegate_str = SpinelKit::Json.get_str(payload, "delegate")
81
81
  delegation = Tep::AuthBearerToken.parse_delegate(delegate_str)
82
82
 
83
83
  Tep::Identity.new(sub, delegation, caps)
@@ -161,10 +161,10 @@ module Tep
161
161
  delegate_str = rec.client_id + "|" + now_ts.to_s + "|" +
162
162
  exp_ts.to_s + "|oauth_grant"
163
163
  payload = "{" +
164
- Tep::Json.encode_pair_str("sub", rec.principal_id) + "," +
165
- Tep::Json.encode_pair_int("exp", exp_ts) + "," +
166
- Tep::Json.encode_pair_str("caps", rec.caps_str) + "," +
167
- Tep::Json.encode_pair_str("delegate", delegate_str) +
164
+ SpinelKit::Json.encode_pair_str("sub", rec.principal_id) + "," +
165
+ SpinelKit::Json.encode_pair_int("exp", exp_ts) + "," +
166
+ SpinelKit::Json.encode_pair_str("caps", rec.caps_str) + "," +
167
+ SpinelKit::Json.encode_pair_str("delegate", delegate_str) +
168
168
  "}"
169
169
  Tep::Jwt.encode_hs256(payload, secret)
170
170
  end
data/lib/tep/broadcast.rb CHANGED
@@ -114,14 +114,21 @@ module Tep
114
114
  # remote deliveries are best-effort and not counted here.
115
115
  def self.publish(topic, payload)
116
116
  matched = Tep::Broadcast.publish_local_only(topic, payload)
117
- if Tep::APP.broadcast_pg_enabled != 0
118
- wire = Tep::Broadcast.encode_wire(topic, payload)
119
- Tep::APP.broadcast_pg_conn.notify(
120
- Tep::APP.broadcast_pg_channel, wire)
121
- end
117
+ # Cross-worker fan-out is an OPT-IN hook (#216): the core build
118
+ # has no PG reference, so a non-PG app DCEs the entire libpq
119
+ # closure. `require "tep/pg"` redefines cross_worker_notify with
120
+ # the real broadcast_pg_conn.notify (last-definition-wins).
121
+ Tep::Broadcast.cross_worker_notify(topic, payload)
122
122
  matched
123
123
  end
124
124
 
125
+ # No-op cross-worker hook. Overridden by lib/tep/pg.rb when the PG
126
+ # backend is loaded. Keeping the PG NOTIFY out of core is what lets
127
+ # `Tep::Broadcast.publish` compile without pulling tep_pg_*/libpq.
128
+ def self.cross_worker_notify(topic, payload)
129
+ 0
130
+ end
131
+
125
132
  # Total subscription count across all topics. Useful for
126
133
  # diagnostics and the v1 test surface.
127
134
  def self.subscriber_count
@@ -157,81 +164,12 @@ module Tep
157
164
 
158
165
  # ---- PG backend (cross-worker pub/sub) ----
159
166
  #
160
- # Opens a dedicated PG connection and issues `LISTEN <channel>`.
161
- # Subsequent publishes NOTIFY this channel too -- other workers
162
- # subscribed to the same channel can receive the message via
163
- # poll_pg_once.
164
- #
165
- # `conninfo` is the libpq connect string. `channel` must be a
166
- # safe SQL identifier (e.g. "tep_broadcast") since it lands
167
- # inside a LISTEN / NOTIFY command unescaped.
168
- #
169
- # Returns 0 on success, -1 on connection or LISTEN failure.
170
- def self.enable_pg_backend(conninfo, channel)
171
- conn = PG::Connection.new(conninfo)
172
- if conn.pgh < 0
173
- return -1
174
- end
175
- if conn.listen(channel) < 0
176
- return -1
177
- end
178
- Tep::APP.set_broadcast_pg_conn(conn)
179
- Tep::APP.set_broadcast_pg_channel(channel)
180
- Tep::APP.set_broadcast_pg_enabled(1)
181
- 0
182
- end
183
-
184
- def self.disable_pg_backend
185
- if Tep::APP.broadcast_pg_enabled == 0
186
- return 0
187
- end
188
- Tep::APP.broadcast_pg_conn.unlisten(Tep::APP.broadcast_pg_channel)
189
- Tep::APP.broadcast_pg_conn.finish
190
- Tep::APP.set_broadcast_pg_enabled(0)
191
- 0
192
- end
193
-
194
- # Process one notification from the PG channel: parse the wire
195
- # format, dispatch to local subscribers as if `publish` had
196
- # been called locally (but WITHOUT re-NOTIFYing -- that would
197
- # loop). Returns 1 if a notification was processed, 0 on
198
- # timeout, -1 on connection error or unenabled backend.
199
- def self.poll_pg_once(timeout_ms)
200
- if Tep::APP.broadcast_pg_enabled == 0
201
- return -1
202
- end
203
- r = Tep::APP.broadcast_pg_conn.poll_notification(timeout_ms)
204
- if r != 1
205
- return r
206
- end
207
- wire = Tep::APP.broadcast_pg_conn.last_notify_payload
208
- Tep::Broadcast.deliver_wire_local(wire)
209
- 1
210
- end
211
-
212
- # Wire format: "<topic_byte_length>:<topic><payload>".
213
- # Length-prefixed so topics and payloads with arbitrary chars
214
- # (commas, colons, embedded quotes, newlines) round-trip
215
- # unambiguously. Encoded by `publish` when the PG backend is
216
- # enabled; decoded by `deliver_wire_local`.
217
- def self.encode_wire(topic, payload)
218
- topic.length.to_s + ":" + topic + payload
219
- end
220
-
221
- def self.deliver_wire_local(wire)
222
- colon = Tep.str_find(wire, ":", 0)
223
- if colon <= 0
224
- return -1
225
- end
226
- len_str = wire[0, colon]
227
- tlen = len_str.to_i
228
- if tlen < 0 || colon + 1 + tlen > wire.length
229
- return -1
230
- end
231
- topic = wire[colon + 1, tlen]
232
- payload = wire[colon + 1 + tlen, wire.length - colon - 1 - tlen]
233
- Tep::Broadcast.publish_local_only(topic, payload)
234
- end
167
+ # The PG LISTEN/NOTIFY backend is OPT-IN (#216). enable_pg_backend /
168
+ # disable_pg_backend / poll_pg_once, plus the wire encode/decode
169
+ # helpers (encode_wire / deliver_wire_local) and the
170
+ # cross_worker_notify override, live in lib/tep/pg.rb and only
171
+ # compile into apps that `require "tep/pg"`. Core Broadcast carries
172
+ # no PG reference so a non-PG app DCEs the libpq closure entirely.
235
173
 
236
174
  # Same fan-out as #publish but skips the PG NOTIFY step. Used
237
175
  # internally by poll_pg_once when delivering a cross-worker
data/lib/tep/events.rb CHANGED
@@ -80,19 +80,19 @@ module Tep
80
80
  # was a bare string before #115. os + arch come from uname() via
81
81
  # Sock.sphttp_os_kind / sphttp_arch_kind.
82
82
  line = "{" +
83
- Json.encode_pair_str("kind", "run_start") + "," +
84
- Json.encode_pair_str("schema", "toy/v1") + "," +
85
- Json.encode_pair_int("t", 0) + "," +
86
- Json.encode_pair_str("started_at", started) + "," +
83
+ SpinelKit::Json.encode_pair_str("kind", "run_start") + "," +
84
+ SpinelKit::Json.encode_pair_str("schema", "toy/v1") + "," +
85
+ SpinelKit::Json.encode_pair_int("t", 0) + "," +
86
+ SpinelKit::Json.encode_pair_str("started_at", started) + "," +
87
87
  "\"host\":{" +
88
- Json.encode_pair_str("name", host) + "," +
89
- Json.encode_pair_str("os", Sock.sphttp_os_kind) + "," +
90
- Json.encode_pair_str("arch", Sock.sphttp_arch_kind) +
88
+ SpinelKit::Json.encode_pair_str("name", host) + "," +
89
+ SpinelKit::Json.encode_pair_str("os", Sock.sphttp_os_kind) + "," +
90
+ SpinelKit::Json.encode_pair_str("arch", Sock.sphttp_arch_kind) +
91
91
  "}," +
92
- "\"backend\":{" + Json.encode_pair_str("kind", backend_kind) + "}," +
92
+ "\"backend\":{" + SpinelKit::Json.encode_pair_str("kind", backend_kind) + "}," +
93
93
  "\"model\":{" +
94
- Json.encode_pair_str("name", model_name) + "," +
95
- Json.encode_pair_str("path", model_path) +
94
+ SpinelKit::Json.encode_pair_str("name", model_name) + "," +
95
+ SpinelKit::Json.encode_pair_str("path", model_path) +
96
96
  "}," +
97
97
  "\"config\":" + config_json +
98
98
  "}"
@@ -120,10 +120,10 @@ module Tep
120
120
  # Build the merged extra: spec fields first, then caller's
121
121
  # fields appended (if non-empty).
122
122
  extra = "{" +
123
- Json.encode_pair_str("model", model) + "," +
124
- Json.encode_pair_int("prompt_tokens", prompt_tokens) + "," +
125
- Json.encode_pair_int("completion_tokens", completion_tokens) + "," +
126
- Json.encode_pair_int("latency_us", wall_us)
123
+ SpinelKit::Json.encode_pair_str("model", model) + "," +
124
+ SpinelKit::Json.encode_pair_int("prompt_tokens", prompt_tokens) + "," +
125
+ SpinelKit::Json.encode_pair_int("completion_tokens", completion_tokens) + "," +
126
+ SpinelKit::Json.encode_pair_int("latency_us", wall_us)
127
127
  caller_inner = ""
128
128
  if extra_json.length > 2
129
129
  # Strip the outer braces -- "{...}" -> "...".
@@ -134,10 +134,10 @@ module Tep
134
134
  end
135
135
  extra = extra + "}"
136
136
  line = "{" +
137
- Json.encode_pair_str("kind", "eval") + "," +
138
- Json.encode_pair_str("phase", "serve") + "," +
139
- Json.encode_pair_int("t", rel_t) + "," +
140
- Json.encode_pair_str("name", "request") + "," +
137
+ SpinelKit::Json.encode_pair_str("kind", "eval") + "," +
138
+ SpinelKit::Json.encode_pair_str("phase", "serve") + "," +
139
+ SpinelKit::Json.encode_pair_int("t", rel_t) + "," +
140
+ SpinelKit::Json.encode_pair_str("name", "request") + "," +
141
141
  "\"extra\":" + extra +
142
142
  "}"
143
143
  append_line(line)
@@ -163,14 +163,14 @@ module Tep
163
163
  end
164
164
  ended = Sock.sphttp_iso8601_utc(Time.now.to_i)
165
165
  line = "{" +
166
- Json.encode_pair_str("kind", "run_end") + "," +
167
- Json.encode_pair_int("t", rel_t) + "," +
168
- Json.encode_pair_str("ended_at", ended) + "," +
169
- Json.encode_pair_str("reason", reason) + "," +
166
+ SpinelKit::Json.encode_pair_str("kind", "run_end") + "," +
167
+ SpinelKit::Json.encode_pair_int("t", rel_t) + "," +
168
+ SpinelKit::Json.encode_pair_str("ended_at", ended) + "," +
169
+ SpinelKit::Json.encode_pair_str("reason", reason) + "," +
170
170
  "\"stats\":{" +
171
- Json.encode_pair_int("requests", @req_count) + "," +
172
- Json.encode_pair_int("errors", @err_count) + "," +
173
- Json.encode_pair_int("tokens_out", @tok_out) +
171
+ SpinelKit::Json.encode_pair_int("requests", @req_count) + "," +
172
+ SpinelKit::Json.encode_pair_int("errors", @err_count) + "," +
173
+ SpinelKit::Json.encode_pair_int("tokens_out", @tok_out) +
174
174
  "}" +
175
175
  "}"
176
176
  append_line(line)
@@ -205,28 +205,28 @@ module Tep
205
205
  Tep.str_find(line_s, "\"name\":\"request\"", 0) >= 0
206
206
  reqs += 1
207
207
  # completion_tokens now lives nested inside the `extra`
208
- # object. Tep::Json.find_value_start walks only the
208
+ # object. SpinelKit::Json.find_value_start walks only the
209
209
  # top-level keys (it skips over nested objects), so we
210
210
  # have to extract extra first, then get_int within it.
211
- extra_pos = Json.find_value_start(line_s, "extra")
211
+ extra_pos = SpinelKit::Json.find_value_start(line_s, "extra")
212
212
  if extra_pos >= 0
213
- obj_end = Json.skip_container(line_s, extra_pos)
213
+ obj_end = SpinelKit::Json.skip_container(line_s, extra_pos)
214
214
  extra_obj = line_s[extra_pos, obj_end - extra_pos]
215
- toks += Json.get_int(extra_obj, "completion_tokens")
215
+ toks += SpinelKit::Json.get_int(extra_obj, "completion_tokens")
216
216
  end
217
217
  end
218
218
  i += 1
219
219
  end
220
220
  ended = Sock.sphttp_iso8601_utc(Time.now.to_i)
221
221
  out = "{" +
222
- Json.encode_pair_str("kind", "run_end") + "," +
223
- Json.encode_pair_int("t", rel_t) + "," +
224
- Json.encode_pair_str("ended_at", ended) + "," +
225
- Json.encode_pair_str("reason", reason) + "," +
222
+ SpinelKit::Json.encode_pair_str("kind", "run_end") + "," +
223
+ SpinelKit::Json.encode_pair_int("t", rel_t) + "," +
224
+ SpinelKit::Json.encode_pair_str("ended_at", ended) + "," +
225
+ SpinelKit::Json.encode_pair_str("reason", reason) + "," +
226
226
  "\"stats\":{" +
227
- Json.encode_pair_int("requests", reqs) + "," +
228
- Json.encode_pair_int("errors", errs) + "," +
229
- Json.encode_pair_int("tokens_out", toks) +
227
+ SpinelKit::Json.encode_pair_int("requests", reqs) + "," +
228
+ SpinelKit::Json.encode_pair_int("errors", errs) + "," +
229
+ SpinelKit::Json.encode_pair_int("tokens_out", toks) +
230
230
  "}" +
231
231
  "}"
232
232
  append_line(out)
@@ -243,7 +243,7 @@ module Tep
243
243
  end
244
244
 
245
245
  # Append one JSON line. Best-effort, append mode -- mirrors
246
- # Tep::Logger's file sink. Telemetry must never fail a request, so
246
+ # SpinelKit::Log's file sink. Telemetry must never fail a request, so
247
247
  # a malformed/unwritable path degrades to a dropped line rather
248
248
  # than a raised error reaching the handler. Callers gate on a
249
249
  # non-empty @path before reaching here.
data/lib/tep/http.rb CHANGED
@@ -151,7 +151,7 @@ module Tep
151
151
 
152
152
  def self.send_req_blocking(verb, url, body, headers)
153
153
  out = Tep::Http::Response.new
154
- parts = Tep::Url.split_url(url)
154
+ parts = SpinelKit::Url.split_url(url)
155
155
  scheme = parts["scheme"]
156
156
  if scheme != "http" && scheme != "https"
157
157
  # Unknown scheme.
@@ -168,7 +168,7 @@ module Tep
168
168
  # pooling TLS sockets is out of scope for 6.7b (#126). HTTP/1.0 +
169
169
  # Connection: close + recv-until-EOF over a fresh verified socket.
170
170
  if scheme == "https"
171
- fd = Sock.sphttp_connect_tls(host, port) # port 443 via Tep::Url
171
+ fd = Sock.sphttp_connect_tls(host, port) # port 443 via SpinelKit::Url
172
172
  if fd < 0
173
173
  return out
174
174
  end
@@ -275,7 +275,7 @@ module Tep
275
275
  # docs/MACOS-CONCURRENCY.md for the why.
276
276
  def self.send_req_coop(verb, url, body, headers)
277
277
  out = Tep::Http::Response.new
278
- parts = Tep::Url.split_url(url)
278
+ parts = SpinelKit::Url.split_url(url)
279
279
  scheme = parts["scheme"]
280
280
  if scheme != "http" && scheme != "https"
281
281
  return out
data/lib/tep/job.rb CHANGED
@@ -24,7 +24,7 @@
24
24
  # )
25
25
  #
26
26
  # The single-arg payload is intentional: structured data goes
27
- # through JSON (Tep::Json) which we already ship. Sidekiq's
27
+ # through JSON (SpinelKit::Json) which we already ship. Sidekiq's
28
28
  # multi-arg `perform_async(a, b, c)` translates to encoding the
29
29
  # tuple as a JSON string and decoding it in `perform`.
30
30
  #
@@ -34,7 +34,7 @@
34
34
  #
35
35
  # class HelloJob < Tep::Job
36
36
  # def perform(arg)
37
- # Tep::Logger.new.info("hello " + arg)
37
+ # SpinelKit::Log.new.info("hello " + arg)
38
38
  # "done"
39
39
  # end
40
40
  # end
data/lib/tep/jwt.rb CHANGED
@@ -13,14 +13,14 @@
13
13
  # Surface
14
14
  # -------
15
15
  #
16
- # payload_json = "{" + Tep::Json.encode_pair_str("sub", user_id) + "," +
17
- # Tep::Json.encode_pair_int("exp", exp_unix) + "}"
16
+ # payload_json = "{" + SpinelKit::Json.encode_pair_str("sub", user_id) + "," +
17
+ # SpinelKit::Json.encode_pair_int("exp", exp_unix) + "}"
18
18
  # token = Tep::Jwt.encode_hs256(payload_json, secret)
19
19
  #
20
20
  # # On the receiving side:
21
21
  # if Tep::Jwt.verify_hs256(token, secret)
22
22
  # payload = Tep::Jwt.decode_payload(token) # the JSON string
23
- # sub = Tep::Json.get_str(payload, "sub")
23
+ # sub = SpinelKit::Json.get_str(payload, "sub")
24
24
  # end
25
25
  #
26
26
  # Scope
@@ -32,7 +32,7 @@
32
32
  #
33
33
  # **Claims validation:** the `verify_hs256` only checks the
34
34
  # signature. `exp` / `nbf` / `iss` / `aud` claim checks are left
35
- # to caller code -- pull them with `Tep::Json.get_int(payload, "exp")`
35
+ # to caller code -- pull them with `SpinelKit::Json.get_int(payload, "exp")`
36
36
  # and compare against `Time.now.to_i`. This keeps the surface
37
37
  # small and lets the app's policy decide what's required (some
38
38
  # apps want skew tolerance, some want strict expiry).
data/lib/tep/live_view.rb CHANGED
@@ -132,8 +132,8 @@ module Tep
132
132
  # "note": <free text>,
133
133
  # "until_ts": <unix ts or 0> }
134
134
  #
135
- # Subclasses typically pull a few keys via Tep::Json.get_str /
136
- # Tep::Json.get_int + update an @presence ivar; then either
135
+ # Subclasses typically pull a few keys via SpinelKit::Json.get_str /
136
+ # SpinelKit::Json.get_int + update an @presence ivar; then either
137
137
  # call broadcast_render (to fan out the new HTML to every
138
138
  # subscriber) or just mutate (if the LiveView is the only
139
139
  # observer).
@@ -176,8 +176,8 @@ module Tep
176
176
  # imeth on the base class dispatches through the typed slot
177
177
  # of the subclass instance and avoids the box.
178
178
  def dispatch_event_json(json_msg, req)
179
- event = Tep::Json.get_str(json_msg, "event")
180
- payload = Tep::Json.get_str(json_msg, "payload")
179
+ event = SpinelKit::Json.get_str(json_msg, "event")
180
+ payload = SpinelKit::Json.get_str(json_msg, "payload")
181
181
  handle_event(event, payload, req)
182
182
  0
183
183
  end