tep 0.11.2 → 0.11.4
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.
- checksums.yaml +4 -4
- data/Makefile +31 -1
- data/README.md +4 -4
- data/SINATRA_COMPAT.md +20 -20
- data/bin/tep +8 -8
- data/examples/api_gateway/app.rb +1 -1
- data/examples/blog/app.rb +17 -17
- data/examples/chat/app.rb +12 -12
- data/examples/chatbot/README.md +2 -2
- data/examples/chatbot/app.rb +24 -24
- data/examples/llm_gateway/README.md +6 -5
- data/examples/llm_gateway/app.rb +4 -4
- data/lib/spinel_kit/hex.rb +65 -0
- data/lib/spinel_kit/json.rb +151 -0
- data/lib/spinel_kit/json_decoder.rb +396 -0
- data/lib/{tep/logger.rb → spinel_kit/log.rb} +25 -21
- data/lib/spinel_kit/url.rb +166 -0
- data/lib/tep/auth_bearer_token.rb +6 -6
- data/lib/tep/auth_oauth2.rb +4 -4
- data/lib/tep/events.rb +37 -37
- data/lib/tep/http.rb +3 -3
- data/lib/tep/job.rb +2 -2
- data/lib/tep/jwt.rb +4 -4
- data/lib/tep/live_view.rb +4 -4
- data/lib/tep/llm.rb +13 -45
- data/lib/tep/mcp.rb +12 -12
- data/lib/tep/multipart.rb +1 -1
- data/lib/tep/openai_server.rb +134 -93
- data/lib/tep/parser.rb +2 -2
- data/lib/tep/presence.rb +11 -11
- data/lib/tep/proxy.rb +7 -7
- data/lib/tep/request.rb +1 -1
- data/lib/tep/response.rb +1 -1
- data/lib/tep/router.rb +1 -1
- data/lib/tep/session.rb +2 -2
- data/lib/tep/version.rb +1 -1
- data/lib/tep.rb +30 -29
- data/test/helper.rb +95 -8
- data/test/run_parallel.rb +44 -7
- data/test/test_auth.rb +17 -17
- data/test/test_auth_oauth2.rb +5 -5
- data/test/test_http_pool.rb +4 -4
- data/test/test_http_pool_send.rb +3 -3
- data/test/test_json.rb +12 -12
- data/test/test_jwt.rb +4 -4
- data/test/test_live_view.rb +3 -3
- data/test/test_llm.rb +12 -9
- data/test/test_llm_gateway.rb +2 -2
- data/test/test_logger.rb +2 -2
- data/test/test_openai_server.rb +72 -1
- data/test/test_password.rb +3 -3
- data/test/test_real_world.rb +6 -1
- data/test/test_shutdown.rb +40 -0
- metadata +9 -8
- data/lib/tep/json.rb +0 -572
- data/lib/tep/url.rb +0 -161
data/test/test_llm_gateway.rb
CHANGED
|
@@ -51,8 +51,8 @@ class TestLlmGateway < TepTest
|
|
|
51
51
|
0
|
|
52
52
|
end
|
|
53
53
|
def on_stream_end(req, out, stats)
|
|
54
|
-
model =
|
|
55
|
-
extra = "{" +
|
|
54
|
+
model = SpinelKit::Json.get_str(req.raw_body, "model")
|
|
55
|
+
extra = "{" + SpinelKit::Json.encode_pair_str("request_id", "req-1") + "}"
|
|
56
56
|
EVENTS.inference(model, 0, stats.chunk_count, 1000000, extra)
|
|
57
57
|
0
|
|
58
58
|
end
|
data/test/test_logger.rb
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
require_relative "helper"
|
|
2
2
|
|
|
3
|
-
#
|
|
3
|
+
# SpinelKit::Log -- levelled logger with stderr / file output.
|
|
4
4
|
class TestLogger < TepTest
|
|
5
5
|
TMP_LOG = "/tmp/tep_logger_test_#{$$}.log"
|
|
6
6
|
|
|
7
7
|
app_source <<~RB
|
|
8
8
|
require 'sinatra'
|
|
9
9
|
|
|
10
|
-
LOGGER =
|
|
10
|
+
LOGGER = SpinelKit::Log.new
|
|
11
11
|
LOGGER.set_level("debug")
|
|
12
12
|
LOGGER.to_file("#{TMP_LOG}")
|
|
13
13
|
|
data/test/test_openai_server.rb
CHANGED
|
@@ -89,6 +89,8 @@ class TestOpenAIServer < TepTest
|
|
|
89
89
|
assert_equal "200", res.code
|
|
90
90
|
body = JSON.parse(res.body)
|
|
91
91
|
assert_equal "text_completion", body["object"]
|
|
92
|
+
# Backend leaves Completion#id default -> "cmpl-tep" (back-compat).
|
|
93
|
+
assert_equal "cmpl-tep", body["id"]
|
|
92
94
|
assert_equal "echo-1", body["model"]
|
|
93
95
|
assert_equal "echoed 3 tokens t=1.0 p=1.0", body["choices"][0]["text"]
|
|
94
96
|
assert_equal "stop", body["choices"][0]["finish_reason"]
|
|
@@ -131,6 +133,9 @@ class TestOpenAIServerEvents < TepTest
|
|
|
131
133
|
c.text = "echoed " + token_ids.length.to_s + " tokens"
|
|
132
134
|
c.prompt_tokens = token_ids.length
|
|
133
135
|
c.completion_tokens = sampling.max_tokens
|
|
136
|
+
# Backend-minted per-request id (#209): must surface as the
|
|
137
|
+
# response `id` AND the inference event's request_id.
|
|
138
|
+
c.id = "cmpl-evt-" + token_ids.length.to_s
|
|
134
139
|
c
|
|
135
140
|
end
|
|
136
141
|
end
|
|
@@ -168,6 +173,8 @@ class TestOpenAIServerEvents < TepTest
|
|
|
168
173
|
res = post("/v1/completions",
|
|
169
174
|
"{\"model\":\"echo-1\",\"prompt\":[1,2,3,4],\"max_tokens\":7}")
|
|
170
175
|
assert_equal "200", res.code
|
|
176
|
+
# Backend-minted id (#209) surfaces as the response `id`.
|
|
177
|
+
assert_equal "cmpl-evt-4", JSON.parse(res.body)["id"]
|
|
171
178
|
|
|
172
179
|
lines2 = File.readlines(EVENTS_PATH).map { |l| JSON.parse(l) }
|
|
173
180
|
# #136: inference events are kind:"eval"+name:"request"; per-request
|
|
@@ -182,7 +189,9 @@ class TestOpenAIServerEvents < TepTest
|
|
|
182
189
|
assert_equal 7, extra["completion_tokens"]
|
|
183
190
|
assert_kind_of Integer, extra["latency_us"]
|
|
184
191
|
assert extra["latency_us"] >= 0
|
|
185
|
-
|
|
192
|
+
# request_id tracks the backend-minted Completion#id, same value as
|
|
193
|
+
# the response `id` above (the request_id == response id invariant).
|
|
194
|
+
assert_equal "cmpl-evt-4", extra["request_id"]
|
|
186
195
|
assert_match(/\Auser:/, extra["principal_id"])
|
|
187
196
|
end
|
|
188
197
|
end
|
|
@@ -596,3 +605,65 @@ class TestOpenAIEmbeddings < TepTest
|
|
|
596
605
|
assert_equal "invalid_request_error", body["error"]["type"]
|
|
597
606
|
end
|
|
598
607
|
end
|
|
608
|
+
|
|
609
|
+
# IDs-only backend (toy#30 convergence): a backend with no detokenizer
|
|
610
|
+
# returns the generated token IDs in Completion#token_ids. The
|
|
611
|
+
# CompletionsHandler then emits choices[0].ids (text stays ""), honors
|
|
612
|
+
# Completion#finish_reason, and ModelsHandler reflects Backend#model_owner
|
|
613
|
+
# + a created stamp. This is the exact surface toy's serve path adopts to
|
|
614
|
+
# drop its hand-rolled handlers.
|
|
615
|
+
class TestOpenAIServerIdsBackend < TepTest
|
|
616
|
+
app_source <<~RB
|
|
617
|
+
require 'sinatra'
|
|
618
|
+
|
|
619
|
+
class IdsBackend < Tep::Llm::OpenAI::Backend
|
|
620
|
+
def list_models
|
|
621
|
+
["toy-1"]
|
|
622
|
+
end
|
|
623
|
+
def model_owner
|
|
624
|
+
"toy"
|
|
625
|
+
end
|
|
626
|
+
def generate_from_tokens(model, token_ids, sampling)
|
|
627
|
+
c = Tep::Llm::OpenAI::Completion.new
|
|
628
|
+
# Echo input IDs +1000 as the "generated" IDs so the test can
|
|
629
|
+
# assert the ids field round-trips; a real backend decodes.
|
|
630
|
+
ids = [0]; ids.delete_at(0)
|
|
631
|
+
i = 0
|
|
632
|
+
while i < token_ids.length
|
|
633
|
+
ids.push(token_ids[i] + 1000)
|
|
634
|
+
i = i + 1
|
|
635
|
+
end
|
|
636
|
+
c.token_ids = ids
|
|
637
|
+
c.prompt_tokens = token_ids.length
|
|
638
|
+
c.completion_tokens = ids.length
|
|
639
|
+
c.finish_reason = "length"
|
|
640
|
+
c
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
Tep::Llm::OpenAI::Server.use(IdsBackend.new)
|
|
645
|
+
Tep::Llm::OpenAI::Server.serve!
|
|
646
|
+
RB
|
|
647
|
+
|
|
648
|
+
def test_completions_emit_ids_field
|
|
649
|
+
res = post("/v1/completions",
|
|
650
|
+
"{\"model\":\"toy-1\",\"prompt\":[10,20,30],\"max_tokens\":3}")
|
|
651
|
+
assert_equal "200", res.code
|
|
652
|
+
body = JSON.parse(res.body)
|
|
653
|
+
assert_equal "text_completion", body["object"]
|
|
654
|
+
# Generated IDs surface as choices[0].ids (input + 1000); text is "".
|
|
655
|
+
assert_equal [1010, 1020, 1030], body["choices"][0]["ids"]
|
|
656
|
+
assert_equal "", body["choices"][0]["text"]
|
|
657
|
+
assert_equal "length", body["choices"][0]["finish_reason"]
|
|
658
|
+
assert_equal 3, body["usage"]["prompt_tokens"]
|
|
659
|
+
assert_equal 3, body["usage"]["completion_tokens"]
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
def test_models_reflects_backend_owner_and_created
|
|
663
|
+
body = JSON.parse(get("/v1/models").body)
|
|
664
|
+
m = body["data"][0]
|
|
665
|
+
assert_equal "toy-1", m["id"]
|
|
666
|
+
assert_equal "toy", m["owned_by"]
|
|
667
|
+
assert_kind_of Integer, m["created"]
|
|
668
|
+
end
|
|
669
|
+
end
|
data/test/test_password.rb
CHANGED
|
@@ -7,14 +7,14 @@ class TestPassword < TepTest
|
|
|
7
7
|
|
|
8
8
|
post '/hash' do
|
|
9
9
|
res.headers["Content-Type"] = "text/plain"
|
|
10
|
-
pwd =
|
|
10
|
+
pwd = SpinelKit::Json.get_str(req.raw_body, "password")
|
|
11
11
|
Tep::Password.hash(pwd)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
post '/verify' do
|
|
15
15
|
res.headers["Content-Type"] = "text/plain"
|
|
16
|
-
pwd =
|
|
17
|
-
hash =
|
|
16
|
+
pwd = SpinelKit::Json.get_str(req.raw_body, "password")
|
|
17
|
+
hash = SpinelKit::Json.get_str(req.raw_body, "hash")
|
|
18
18
|
Tep::Password.verify(pwd, hash) ? "ok" : "bad"
|
|
19
19
|
end
|
|
20
20
|
|
data/test/test_real_world.rb
CHANGED
|
@@ -245,7 +245,12 @@ class TestRealWorld < TepTest
|
|
|
245
245
|
# With a successful login + cookie jar...
|
|
246
246
|
uri = URI("http://127.0.0.1:#{@port}/login")
|
|
247
247
|
net = Net::HTTP.new(uri.host, uri.port)
|
|
248
|
-
|
|
248
|
+
# Explicit form Content-Type: this direct net.post bypasses the
|
|
249
|
+
# harness req() helper, and Ruby 4.0's Net::HTTP no longer auto-sets
|
|
250
|
+
# it for a bodied request (3.x did), so without it the login form
|
|
251
|
+
# isn't parsed -> 401 instead of 302.
|
|
252
|
+
r_login = net.post(uri.path, "user=alice&password=hunter2",
|
|
253
|
+
{"Content-Type" => "application/x-www-form-urlencoded"})
|
|
249
254
|
assert_equal "302", r_login.code
|
|
250
255
|
cookie = r_login["Set-Cookie"]
|
|
251
256
|
refute_nil cookie
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
# #188 regression guard for the #186 fix.
|
|
4
|
+
#
|
|
5
|
+
# A server that does NOT mount the OpenAI events surface (the common case)
|
|
6
|
+
# must not SIGSEGV on SIGTERM. The #186 bug: App#initialize never set
|
|
7
|
+
# @openai_events, so Tep.on_shutdown's unconditional `openai_events.enabled?`
|
|
8
|
+
# was a null-receiver deref -- a hard SIGSEGV under Spinel (exit 139) on a
|
|
9
|
+
# clean `kill -TERM`. #186 (0.11.2) initialises @openai_events to a disabled
|
|
10
|
+
# default, so on_shutdown is a safe no-op for apps that never call
|
|
11
|
+
# Tep::Llm::OpenAI::Server.serve!.
|
|
12
|
+
#
|
|
13
|
+
# The events-mounted path is already covered by
|
|
14
|
+
# test_openai_server#test_sigterm_emits_run_end; this is the no-events path
|
|
15
|
+
# that the original bug actually hit. We assert the exit is NOT a SEGV.
|
|
16
|
+
# Whether the clean exit is 143 (signal-terminated) or 0 (graceful return)
|
|
17
|
+
# is the build/timing-sensitive residual tracked in #188 and is acceptable
|
|
18
|
+
# here -- only a SIGSEGV is the regression.
|
|
19
|
+
class TestShutdownNoEvents < TepTest
|
|
20
|
+
app_source <<~RB
|
|
21
|
+
require 'sinatra'
|
|
22
|
+
|
|
23
|
+
get '/ping' do
|
|
24
|
+
"pong"
|
|
25
|
+
end
|
|
26
|
+
RB
|
|
27
|
+
|
|
28
|
+
def test_sigterm_on_no_events_app_does_not_segv
|
|
29
|
+
assert_equal "pong", get("/ping").body, "server should be live before SIGTERM"
|
|
30
|
+
|
|
31
|
+
status = TepHarness.terminate_status(@port)
|
|
32
|
+
refute_nil status, "spawned server not found / never reaped"
|
|
33
|
+
|
|
34
|
+
segv = Signal.list["SEGV"] # 11 -> exit 139
|
|
35
|
+
crashed = status.signaled? && status.termsig == segv
|
|
36
|
+
refute crashed,
|
|
37
|
+
"no-events app SIGSEGV'd on SIGTERM (the #186 regression) -- " \
|
|
38
|
+
"termsig=#{status.termsig.inspect} exitstatus=#{status.exitstatus.inspect}"
|
|
39
|
+
end
|
|
40
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tep
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.11.
|
|
4
|
+
version: 0.11.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ori Pekelman
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: prism
|
|
@@ -88,6 +87,11 @@ files:
|
|
|
88
87
|
- examples/qdrant/README.md
|
|
89
88
|
- examples/sinatra_style.rb
|
|
90
89
|
- examples/websocket_echo.rb
|
|
90
|
+
- lib/spinel_kit/hex.rb
|
|
91
|
+
- lib/spinel_kit/json.rb
|
|
92
|
+
- lib/spinel_kit/json_decoder.rb
|
|
93
|
+
- lib/spinel_kit/log.rb
|
|
94
|
+
- lib/spinel_kit/url.rb
|
|
91
95
|
- lib/tep.rb
|
|
92
96
|
- lib/tep/agent_delegation.rb
|
|
93
97
|
- lib/tep/app.rb
|
|
@@ -107,11 +111,9 @@ files:
|
|
|
107
111
|
- lib/tep/http.rb
|
|
108
112
|
- lib/tep/identity.rb
|
|
109
113
|
- lib/tep/job.rb
|
|
110
|
-
- lib/tep/json.rb
|
|
111
114
|
- lib/tep/jwt.rb
|
|
112
115
|
- lib/tep/live_view.rb
|
|
113
116
|
- lib/tep/llm.rb
|
|
114
|
-
- lib/tep/logger.rb
|
|
115
117
|
- lib/tep/mcp.rb
|
|
116
118
|
- lib/tep/multipart.rb
|
|
117
119
|
- lib/tep/net.rb
|
|
@@ -137,7 +139,6 @@ files:
|
|
|
137
139
|
- lib/tep/streamer.rb
|
|
138
140
|
- lib/tep/tep_pg.c
|
|
139
141
|
- lib/tep/tep_sqlite.c
|
|
140
|
-
- lib/tep/url.rb
|
|
141
142
|
- lib/tep/version.rb
|
|
142
143
|
- lib/tep/websocket.rb
|
|
143
144
|
- lib/tep/websocket/connection.rb
|
|
@@ -215,6 +216,7 @@ files:
|
|
|
215
216
|
- test/test_server_scheduled.rb
|
|
216
217
|
- test/test_sessions.rb
|
|
217
218
|
- test/test_shell.rb
|
|
219
|
+
- test/test_shutdown.rb
|
|
218
220
|
- test/test_sqlite.rb
|
|
219
221
|
- test/test_sqlite_cached.rb
|
|
220
222
|
- test/test_static.rb
|
|
@@ -257,8 +259,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
257
259
|
- !ruby/object:Gem::Version
|
|
258
260
|
version: '0'
|
|
259
261
|
requirements: []
|
|
260
|
-
rubygems_version:
|
|
261
|
-
signing_key:
|
|
262
|
+
rubygems_version: 4.0.3
|
|
262
263
|
specification_version: 4
|
|
263
264
|
summary: A Sinatra-flavoured web framework that compiles to a native binary via Spinel
|
|
264
265
|
test_files: []
|