fast-completions 0.6.0 → 0.7.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -0
- data/lib/fast/completions/client.rb +195 -39
- data/lib/fast/completions/telemetry.rb +66 -0
- data/lib/fast/completions/version.rb +1 -1
- data/lib/fast/completions.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 296c0ef9c66b4933e440f14845ecca83e7ab18e96f125a572adda19e853be9b7
|
|
4
|
+
data.tar.gz: 12fe00808afa0c4c81a9b36dc3d898803c63dd570fcb29d86292d6c526b96557
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 63b785f46a6a15265ddf700f6039d920670059a5a0c39b4b40c92892462577fc9bc1b02c0dc33645be44a28fc991961bdbd437568ed32188081e6b62105f9ba4
|
|
7
|
+
data.tar.gz: c02fdca12bb4133152fc6f41a4c2c2157e011937b93f2eb2dff319c18bda3124836d2a588386ddf96a00022488f13b31b1623edb609c86b7f0c29ea0d77f33c8
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,36 @@
|
|
|
3
3
|
Notable changes to the fast-completions gem. The format is prose-first,
|
|
4
4
|
one section per released version, newest first.
|
|
5
5
|
|
|
6
|
+
## 0.7.0 (2026-10-25)
|
|
7
|
+
|
|
8
|
+
- Telemetry sink interface: `Client.new` accepts `telemetry:` — any
|
|
9
|
+
Enumerable of sink objects (an Array, a Set, …), validated loudly at
|
|
10
|
+
construction (a non-Enumerable, or a
|
|
11
|
+
sink not responding to all five hooks, raises `ArgumentError` naming
|
|
12
|
+
the offender and the missing hook; duck-typed sinks are accepted
|
|
13
|
+
without module inclusion; the client keeps its own frozen Array —
|
|
14
|
+
`entries` — so the caller's container is never frozen or mutated) — and dispatches five
|
|
15
|
+
observation hooks around a completion, ONE `Client#stream` invocation
|
|
16
|
+
(creation retries inside, tool execution outside):
|
|
17
|
+
`on_completion_start(model.id, body bytesize)` before the first POST,
|
|
18
|
+
`on_completion_ttfb` at the first parser item of the successful
|
|
19
|
+
attempt (a retried attempt's clock restarts), `on_event` for every
|
|
20
|
+
Event — before its consumer twin, terminals and the salvage `:error`
|
|
21
|
+
included, and even with no consumer block — `on_completion_end(stop
|
|
22
|
+
reason, duration, live message)` when `stream` returns, and
|
|
23
|
+
`on_completion_error(error Symbol derived from the raising class,
|
|
24
|
+
duration)` on every raise-out, cancellation included. Multiple sinks
|
|
25
|
+
dispatch in registration order. The allocation discipline is pinned
|
|
26
|
+
by spec measurement gates: zero added heap objects on the hot paths
|
|
27
|
+
with no sink registered, zero ADDED objects with a no-op sink (only
|
|
28
|
+
refs and immediates cross the boundary, durations are flonum
|
|
29
|
+
differences, every call site short-circuits on a precomputed
|
|
30
|
+
emptiness check), and zero added `Process.clock_gettime` reads. The
|
|
31
|
+
new reference page `docs/reference/telemetry.md` documents the
|
|
32
|
+
protocol, the ordering rule (telemetry is the ground-truth record
|
|
33
|
+
when consumer code is the suspect), the read-only convention for
|
|
34
|
+
sinks, and that metric naming belongs to the adapter.
|
|
35
|
+
|
|
6
36
|
## 0.6.0 (2026-10-25)
|
|
7
37
|
|
|
8
38
|
- `Client#raw_stream` — the raw completions surface a proxy needs: the
|
|
@@ -58,13 +58,31 @@ module Fast
|
|
|
58
58
|
Errno::ENETUNREACH, Errno::ETIMEDOUT, EOFError, IOError, SocketError,
|
|
59
59
|
].freeze
|
|
60
60
|
|
|
61
|
+
# The five Telemetry::Sink hooks a registered sink must respond to
|
|
62
|
+
# (I01). Checked ONCE at construction — the only sanctioned
|
|
63
|
+
# respond_to? use; notification-time dispatch never reflects.
|
|
64
|
+
TELEMETRY_HOOKS = %i[
|
|
65
|
+
on_completion_start on_completion_ttfb on_event on_completion_end on_completion_error
|
|
66
|
+
].freeze
|
|
67
|
+
|
|
61
68
|
attr_reader :timeout, :max_retries, :dropped_chunks, :ttfb_timeout, :inactivity_timeout
|
|
62
69
|
|
|
63
70
|
def initialize(base_url:, api_key: nil, timeout: DEFAULT_TIMEOUT, max_retries: DEFAULT_MAX_RETRIES,
|
|
64
|
-
ttfb_timeout: nil, inactivity_timeout: nil)
|
|
71
|
+
ttfb_timeout: nil, inactivity_timeout: nil, telemetry: [])
|
|
65
72
|
raise ArgumentError, "max_retries must be an Integer >= 0" unless max_retries.is_a?(Integer) && max_retries >= 0
|
|
66
73
|
validate_deadline!(:ttfb_timeout, ttfb_timeout)
|
|
67
74
|
validate_deadline!(:inactivity_timeout, inactivity_timeout)
|
|
75
|
+
unless telemetry.is_a?(Enumerable)
|
|
76
|
+
raise ArgumentError, "telemetry must be an Enumerable of sink objects, got #{telemetry.class}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
telemetry.each do |sink|
|
|
80
|
+
missing = TELEMETRY_HOOKS.reject { |hook| sink.respond_to?(hook) }
|
|
81
|
+
next if missing.empty?
|
|
82
|
+
|
|
83
|
+
raise ArgumentError,
|
|
84
|
+
"telemetry sink #{sink.inspect} does not respond to #{missing.join(", ")} (Telemetry::Sink hooks)"
|
|
85
|
+
end
|
|
68
86
|
|
|
69
87
|
@endpoint = Async::HTTP::Endpoint.parse(base_url)
|
|
70
88
|
@api_key = api_key
|
|
@@ -74,6 +92,14 @@ module Fast
|
|
|
74
92
|
@inactivity_timeout = inactivity_timeout
|
|
75
93
|
@client = Async::HTTP::Client.new(@endpoint)
|
|
76
94
|
@path = @endpoint.path.chomp("/") + "/chat/completions"
|
|
95
|
+
# I01: the client's own frozen copy (the caller's container is
|
|
96
|
+
# never frozen or mutated) + the precomputed emptiness check every
|
|
97
|
+
# call site short-circuits on — zero allocation, zero added
|
|
98
|
+
# clock reads when no sink is registered. `entries` always
|
|
99
|
+
# builds a fresh Array — for an Array input too, so any
|
|
100
|
+
# Enumerable input lands in the same frozen-Array shape.
|
|
101
|
+
@telemetry = telemetry.entries.freeze
|
|
102
|
+
@telemetry_active = !@telemetry.empty?
|
|
77
103
|
end
|
|
78
104
|
|
|
79
105
|
def close
|
|
@@ -105,38 +131,40 @@ module Fast
|
|
|
105
131
|
headers = default_headers.merge(session_headers(model.dialect, session_id))
|
|
106
132
|
body = JSON.generate(request)
|
|
107
133
|
accumulator = Protocol::StreamAccumulator.new(model, model.dialect)
|
|
108
|
-
|
|
134
|
+
started_at = notify_start(model.id, body.bytesize) # I01 §1: the completion clock, nil without sinks
|
|
109
135
|
watchdog = Watchdog.new(Async::Task.current, @timeout)
|
|
110
136
|
response = nil
|
|
137
|
+
post_started_at = nil # the committed attempt's POST clock (I01 §2), set by create_response's yield
|
|
111
138
|
begin
|
|
112
|
-
|
|
113
|
-
#
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
feed_stream(response, accumulator, watchdog, &block)
|
|
139
|
+
response = create_response(headers, body, watchdog) { |t0| post_started_at = t0 }
|
|
140
|
+
if block || @telemetry_active # :start even blockless (I01 §4); a consumer raise flies to the ladder
|
|
141
|
+
dispatch_event(accumulator.start_event, &block)
|
|
142
|
+
end
|
|
143
|
+
feed_stream(response, accumulator, watchdog, post_started_at, &block)
|
|
118
144
|
rescue Async::Cancel
|
|
119
|
-
watchdog.fired
|
|
145
|
+
if watchdog.fired # I01 §7: fired → timeout, else user cancel (D8); cancel never salvages (AC3)
|
|
146
|
+
notify_error(:timeout_error, started_at)
|
|
147
|
+
raise(TimeoutError, "no data within #{@timeout}s")
|
|
148
|
+
end
|
|
149
|
+
notify_error(:cancelled_error, started_at)
|
|
150
|
+
raise(CancelledError, "stream cancelled")
|
|
120
151
|
rescue *NETWORK_ERRORS => e
|
|
121
|
-
salvage(accumulator, &block) # the :error terminal carries the partial BEFORE the raise (§1)
|
|
152
|
+
salvage(accumulator, started_at, &block) # the :error terminal carries the partial BEFORE the raise (§1)
|
|
153
|
+
notify_error(:connection_error, started_at) # I01 §7: the symbol of the ConnectionError about to raise
|
|
122
154
|
raise ConnectionError, "#{e.class}: #{e.message}"
|
|
123
|
-
rescue StandardError
|
|
124
|
-
salvage(accumulator, &block) # pi's catch-all:
|
|
155
|
+
rescue StandardError => e
|
|
156
|
+
salvage(accumulator, started_at, &block) # pi's catch-all: same terminal, raised unchanged
|
|
157
|
+
notify_error(telemetry_symbol_for(e), started_at) # I01 §7: derived from the re-raised class
|
|
125
158
|
raise
|
|
126
159
|
ensure
|
|
127
|
-
#
|
|
128
|
-
# read (cancel/timeout) discards it — a cancelled connection is
|
|
129
|
-
# never reused (AC3/D8).
|
|
130
|
-
begin
|
|
131
|
-
response.finish
|
|
132
|
-
rescue StandardError, Async::Cancel
|
|
133
|
-
nil
|
|
134
|
-
end
|
|
160
|
+
finish_quietly(response) # nil-safe: no response yet, or an aborted read (AC3/D8)
|
|
135
161
|
watchdog.stop
|
|
136
162
|
end
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
163
|
+
if watchdog.fired # I01 §7: the post-ensure watchdog raise
|
|
164
|
+
notify_error(:timeout_error, started_at)
|
|
165
|
+
raise TimeoutError, "no data within #{@timeout}s"
|
|
166
|
+
end
|
|
167
|
+
deliver_terminal(accumulator, started_at, &block)
|
|
140
168
|
end
|
|
141
169
|
|
|
142
170
|
# The raw surface a proxy needs (I03, BRIEF §3): the caller's
|
|
@@ -295,19 +323,31 @@ module Fast
|
|
|
295
323
|
raise CancelledError, "stream cancelled"
|
|
296
324
|
end
|
|
297
325
|
|
|
298
|
-
|
|
326
|
+
# The wire read (§3): SSE parse → accumulator → dispatch. TTFB
|
|
327
|
+
# fires at the FIRST parser-yielded item of the SUCCESSFUL attempt
|
|
328
|
+
# (I01 §3 — a `[DONE]` sentinel counts; comments and keepalives
|
|
329
|
+
# never yield items, sse.rb drops them structurally), measured
|
|
330
|
+
# from that attempt's POST (post_started_at, threaded in from
|
|
331
|
+
# create_response; a retried attempt's clock restarts), at most
|
|
332
|
+
# once per completion. Consumer raises here fly to stream's
|
|
333
|
+
# ladder, which closes the record after salvage.
|
|
334
|
+
def feed_stream(response, accumulator, watchdog, post_started_at, &block)
|
|
299
335
|
parser = SSE.new
|
|
300
336
|
@dropped_chunks = 0
|
|
337
|
+
ttfb_fired = false
|
|
301
338
|
response.body.each do |chunk|
|
|
302
339
|
watchdog.arm # re-arm the inactivity read-timeout per chunk (D5)
|
|
303
340
|
parser.push(chunk).each do |item|
|
|
341
|
+
if @telemetry_active && !ttfb_fired # before this item's dispatches; a flonum difference — no heap
|
|
342
|
+
ttfb_fired = true
|
|
343
|
+
notify_ttfb(Process.clock_gettime(Process::CLOCK_MONOTONIC) - post_started_at)
|
|
344
|
+
end
|
|
304
345
|
next if item == SSE::DONE # terminal token; no payload
|
|
305
346
|
|
|
306
347
|
json = parse_chunk(item)
|
|
307
348
|
next if json.nil? # dropped (I61): counted, the stream continues
|
|
308
349
|
|
|
309
|
-
|
|
310
|
-
events.each { |event| yield event } if block
|
|
350
|
+
accumulator.consume(json).each { |event| dispatch_event(event, &block) }
|
|
311
351
|
end
|
|
312
352
|
end
|
|
313
353
|
end
|
|
@@ -337,14 +377,29 @@ module Fast
|
|
|
337
377
|
# stop and a missing finish both emit :error FIRST, then raise
|
|
338
378
|
# StreamError; otherwise :done with the mapped reason. Cancel never
|
|
339
379
|
# reaches here — it unwinds mid-read with NO terminal event (AC3).
|
|
340
|
-
|
|
380
|
+
# I01 §5: every terminal dispatch reaches the sinks (blockless
|
|
381
|
+
# included), on_completion_error rides the StreamError raise, and
|
|
382
|
+
# on_completion_end fires after the :done dispatches. The terminal
|
|
383
|
+
# Events are built only under the observer guard (zero-path
|
|
384
|
+
# discipline); every yield below is a ladder-UNCOVERED site — this
|
|
385
|
+
# method runs AFTER the ladder — so dispatch_event closes the
|
|
386
|
+
# record on a consumer raise here (see dispatch_event).
|
|
387
|
+
def deliver_terminal(accumulator, started_at, &block)
|
|
341
388
|
events, outcome = accumulator.finish
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
389
|
+
if block || @telemetry_active
|
|
390
|
+
events.each { |event| dispatch_event(event, started_at, closing: true, &block) }
|
|
391
|
+
if outcome[0] == :stream_error
|
|
392
|
+
dispatch_event(Event.new(type: :error, reason: :error, message: accumulator.partial),
|
|
393
|
+
started_at, closing: true, &block)
|
|
394
|
+
notify_error(:stream_error, started_at) # the symbol of the StreamError about to raise
|
|
395
|
+
else
|
|
396
|
+
dispatch_event(Event.new(type: :done, reason: outcome[1], message: accumulator.partial),
|
|
397
|
+
started_at, closing: true, &block)
|
|
398
|
+
notify_end(outcome[1], accumulator.partial, started_at) # after the :done dispatches, before the return
|
|
399
|
+
end
|
|
346
400
|
end
|
|
347
|
-
|
|
401
|
+
raise StreamError.new(outcome[1], stop_reason: :error) if outcome[0] == :stream_error
|
|
402
|
+
|
|
348
403
|
accumulator.partial
|
|
349
404
|
end
|
|
350
405
|
|
|
@@ -365,6 +420,7 @@ module Fast
|
|
|
365
420
|
remaining = @max_retries
|
|
366
421
|
loop do
|
|
367
422
|
watchdog.arm # re-armed per attempt: a retried TTFB deadline restarts (D5)
|
|
423
|
+
t0 = @telemetry_active ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : nil # I01 §2, guarded (gate 3)
|
|
368
424
|
begin
|
|
369
425
|
response = @client.post(@path, headers: headers, body: body)
|
|
370
426
|
rescue Async::Cancel
|
|
@@ -373,12 +429,14 @@ module Fast
|
|
|
373
429
|
remaining -= 1
|
|
374
430
|
next
|
|
375
431
|
rescue *NETWORK_ERRORS => e
|
|
376
|
-
|
|
377
|
-
raise ConnectionError, "#{e.class}: #{e.message}" if remaining.zero?
|
|
432
|
+
raise ConnectionError, "#{e.class}: #{e.message}" if remaining.zero? # statusless trigger
|
|
378
433
|
remaining -= 1
|
|
379
434
|
next
|
|
380
435
|
end
|
|
381
|
-
|
|
436
|
+
if (200...300).cover?(response.status) # committed to streaming: never retried past a 2xx
|
|
437
|
+
yield(t0) if block_given? # the committed attempt's POST clock → stream's TTFB local (allocation-free)
|
|
438
|
+
return response
|
|
439
|
+
end
|
|
382
440
|
|
|
383
441
|
if retry_response?(response) && remaining.positive?
|
|
384
442
|
finish_quietly(response) # the discarded attempt's connection returns to the pool
|
|
@@ -419,16 +477,22 @@ module Fast
|
|
|
419
477
|
# finalize_blocks — the thinking reasoning_details serialized into
|
|
420
478
|
# the signature, tool arguments finalized — whose end events we
|
|
421
479
|
# discard here, so a salvaged partial replays like a completed one's.
|
|
422
|
-
|
|
423
|
-
|
|
480
|
+
# The yield below runs INSIDE a rescue arm — ladder-uncovered — so
|
|
481
|
+
# dispatch_event closes the record on a consumer raise; the early
|
|
482
|
+
# return keeps the blockless-no-sink path byte-identical (I01 §6).
|
|
483
|
+
def salvage(accumulator, started_at, &block)
|
|
484
|
+
return unless block_given? || @telemetry_active # the blockless-no-sink early return (I01 §6)
|
|
424
485
|
|
|
425
486
|
accumulator.finish
|
|
426
|
-
|
|
487
|
+
dispatch_event(Event.new(type: :error, reason: :error, message: accumulator.partial),
|
|
488
|
+
started_at, closing: true, &block)
|
|
427
489
|
end
|
|
428
490
|
|
|
429
491
|
# Complete bodies return the connection to the pool; an aborted read
|
|
430
492
|
# (cancel/timeout) discards it — a cancelled connection is never
|
|
431
|
-
# reused (AC3/D8).
|
|
493
|
+
# reused (AC3/D8). Serves the discarded retry attempt and stream's
|
|
494
|
+
# ensure; nil-safe (no response yet → the rescue swallows the
|
|
495
|
+
# NoMethodError exactly as the inline form did).
|
|
432
496
|
def finish_quietly(response)
|
|
433
497
|
response.finish
|
|
434
498
|
rescue StandardError, Async::Cancel
|
|
@@ -442,6 +506,98 @@ module Fast
|
|
|
442
506
|
body = response.body.read
|
|
443
507
|
raise Errors.class_for_status(response.status, body).new(status: response.status, body: body)
|
|
444
508
|
end
|
|
509
|
+
|
|
510
|
+
# ── I01 telemetry dispatch ─────────────────────────────────────
|
|
511
|
+
# Every helper short-circuits on the precomputed emptiness check
|
|
512
|
+
# BEFORE any clock read or dispatch: with no sink registered the
|
|
513
|
+
# call sites cost one no-op method call, zero allocations, zero
|
|
514
|
+
# Process.clock_gettime reads. Per-completion state (the
|
|
515
|
+
# completion-start clock, the per-attempt POST clock, the
|
|
516
|
+
# ttfb-once flag) is threaded as locals/args, never Client ivars
|
|
517
|
+
# (I03); durations are flonum differences; dispatch is in
|
|
518
|
+
# registration order; the loop block itself is never materialized,
|
|
519
|
+
# so a no-op sink adds zero heap objects (gate 2).
|
|
520
|
+
|
|
521
|
+
# The ONE event dispatch seam: sinks first, consumer second (I01
|
|
522
|
+
# §4/§5 — telemetry is the ground-truth record). `closing:` marks
|
|
523
|
+
# yield sites the rescue ladder does NOT cover — deliver_terminal
|
|
524
|
+
# runs after it, salvage inside a rescue arm — where a consumer
|
|
525
|
+
# raise closes the record here (the raiser's symbol) before the
|
|
526
|
+
# unchanged, backtrace-preserving re-raise; on StandardError only,
|
|
527
|
+
# the ladder's own catch scope, so cancel and Exception-class
|
|
528
|
+
# raises keep today's escape semantics. At the ladder-covered
|
|
529
|
+
# sites the StandardError arm does the closing (after salvage) —
|
|
530
|
+
# closing here too would double-fire the record. Dispatch is
|
|
531
|
+
# implicit yield/block_given? (no captured &block of its own — no
|
|
532
|
+
# Proc materialization, gate 2); callers forward their
|
|
533
|
+
# already-captured Proc at the call site, which is free.
|
|
534
|
+
def dispatch_event(event, started_at = nil, closing: false)
|
|
535
|
+
return unless block_given? || @telemetry_active
|
|
536
|
+
|
|
537
|
+
notify_event(event)
|
|
538
|
+
if closing
|
|
539
|
+
begin
|
|
540
|
+
yield event if block_given?
|
|
541
|
+
rescue StandardError => e
|
|
542
|
+
notify_error(telemetry_symbol_for(e), started_at)
|
|
543
|
+
raise
|
|
544
|
+
end
|
|
545
|
+
else
|
|
546
|
+
yield event if block_given?
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# I01 §1: the completion clock (every duration below differs from
|
|
551
|
+
# it) + on_completion_start, before the first POST of the first
|
|
552
|
+
# attempt. Returns the clock — nil without a sink, zero added
|
|
553
|
+
# clock reads (gate 3).
|
|
554
|
+
def notify_start(model_id, request_bytes)
|
|
555
|
+
return nil unless @telemetry_active
|
|
556
|
+
|
|
557
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
558
|
+
@telemetry.each { |sink| sink.on_completion_start(model_id, request_bytes) }
|
|
559
|
+
started_at
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def notify_ttfb(elapsed)
|
|
563
|
+
return unless @telemetry_active
|
|
564
|
+
|
|
565
|
+
@telemetry.each { |sink| sink.on_completion_ttfb(elapsed) }
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def notify_event(event)
|
|
569
|
+
return unless @telemetry_active
|
|
570
|
+
|
|
571
|
+
@telemetry.each { |sink| sink.on_event(event) }
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
def notify_end(stop_reason, message, started_at)
|
|
575
|
+
return unless @telemetry_active
|
|
576
|
+
|
|
577
|
+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
|
|
578
|
+
@telemetry.each { |sink| sink.on_completion_end(stop_reason, duration, message) }
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
def notify_error(error, started_at)
|
|
582
|
+
return unless @telemetry_active
|
|
583
|
+
|
|
584
|
+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
|
|
585
|
+
@telemetry.each { |sink| sink.on_completion_error(error, duration) }
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
# The re-raise arm's error Symbol: derived from the raising class —
|
|
589
|
+
# demodulized, underscored (RateLimitError → :rate_limit_error,
|
|
590
|
+
# RuntimeError → :runtime_error). Computed only on the raise path,
|
|
591
|
+
# under the active guard; an anonymous class has no name.
|
|
592
|
+
def telemetry_symbol_for(error)
|
|
593
|
+
name = error.class.name
|
|
594
|
+
return :anonymous_error if name.nil?
|
|
595
|
+
|
|
596
|
+
name.split("::").last
|
|
597
|
+
.gsub(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
|
|
598
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
599
|
+
.downcase.to_sym
|
|
600
|
+
end
|
|
445
601
|
end
|
|
446
602
|
end
|
|
447
603
|
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fast
|
|
4
|
+
module Completions
|
|
5
|
+
# The telemetry observation protocol (I01): a sink includes this
|
|
6
|
+
# module and overrides what it wants — the defaults are no-ops. No
|
|
7
|
+
# base class, no respond_to? dispatch at notification time (Client
|
|
8
|
+
# validates the five hooks ONCE at construction, the only sanctioned
|
|
9
|
+
# respond_to? use); duck-typed sinks that define all five are
|
|
10
|
+
# accepted without including the module.
|
|
11
|
+
#
|
|
12
|
+
# Unit of observation: a COMPLETION — ONE Client#stream invocation
|
|
13
|
+
# (one logical request-response on POST {base}/chat/completions with
|
|
14
|
+
# stream: true). Creation retries are INSIDE the completion (a
|
|
15
|
+
# retried completion is one completion); tool execution (the Chat
|
|
16
|
+
# batch) is OUTSIDE it.
|
|
17
|
+
#
|
|
18
|
+
# All hook arguments are existing references or immediates — no Hash
|
|
19
|
+
# payloads, no structs, no interpolated strings, no keyword splats —
|
|
20
|
+
# so dispatching to a no-op sink allocates nothing on the hot paths.
|
|
21
|
+
module Telemetry
|
|
22
|
+
# A sink includes this module and overrides what it wants. The
|
|
23
|
+
# defaults are no-ops. No base class; no respond_to? dispatch at
|
|
24
|
+
# notification time.
|
|
25
|
+
module Sink
|
|
26
|
+
# Exactly once per completion, before the first POST of the
|
|
27
|
+
# first attempt. `model_id` is the Model#id String ref
|
|
28
|
+
# (equal?-stable, never a copy); `request_bytes` is the Integer
|
|
29
|
+
# bytesize of the serialized request body actually POSTed.
|
|
30
|
+
def on_completion_start(model_id, request_bytes); end
|
|
31
|
+
|
|
32
|
+
# At most once, at the FIRST item the SSE parser yields for the
|
|
33
|
+
# SUCCESSFUL (2xx) attempt — measured from that attempt's POST
|
|
34
|
+
# initiation (a retried attempt's clock restarts). Seconds since
|
|
35
|
+
# the POST, a Float. Does not fire when the completion fails
|
|
36
|
+
# before any item.
|
|
37
|
+
def on_completion_ttfb(seconds); end
|
|
38
|
+
|
|
39
|
+
# Every Event the consumer block sees, BEFORE the consumer
|
|
40
|
+
# block, same order — terminals (:done, :error) and the
|
|
41
|
+
# salvage-path :error included; with a registered sink but no
|
|
42
|
+
# consumer block, sinks still receive every Event produced at
|
|
43
|
+
# the yield sites. The argument is the Event ref the accumulator
|
|
44
|
+
# already allocated.
|
|
45
|
+
def on_event(event); end
|
|
46
|
+
|
|
47
|
+
# Exactly once, when Client#stream returns: after the :done
|
|
48
|
+
# event's dispatches. `stop_reason` is the mapped Symbol (:stop,
|
|
49
|
+
# :length, :tool_use); `duration` is the Float from the
|
|
50
|
+
# completion-start clock; `message` is the accumulator's LIVE
|
|
51
|
+
# assistant Message (see the read-only convention in
|
|
52
|
+
# docs/reference/telemetry.md).
|
|
53
|
+
def on_completion_end(stop_reason, duration, message); end
|
|
54
|
+
|
|
55
|
+
# Exactly once on EVERY raise-out of Client#stream — the mapped
|
|
56
|
+
# paths and any other raise re-raised through the ladder.
|
|
57
|
+
# `error` is a Symbol derived from the raising class
|
|
58
|
+
# (demodulized, underscored: :rate_limit_error,
|
|
59
|
+
# :connection_error, :timeout_error, :cancelled_error,
|
|
60
|
+
# :stream_error, :runtime_error, …); `duration` is the Float
|
|
61
|
+
# start-to-raise on the same completion-start clock.
|
|
62
|
+
def on_completion_error(error, duration); end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
data/lib/fast/completions.rb
CHANGED
|
@@ -13,6 +13,7 @@ require_relative "completions/sse"
|
|
|
13
13
|
require_relative "completions/codec"
|
|
14
14
|
require_relative "completions/constrained_sampling"
|
|
15
15
|
require_relative "completions/protocol"
|
|
16
|
+
require_relative "completions/telemetry"
|
|
16
17
|
require_relative "completions/client"
|
|
17
18
|
require_relative "completions/tool"
|
|
18
19
|
require_relative "completions/cancellation"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fast-completions
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Eric Jacobs
|
|
@@ -44,6 +44,7 @@ files:
|
|
|
44
44
|
- lib/fast/completions/errors.rb
|
|
45
45
|
- lib/fast/completions/protocol.rb
|
|
46
46
|
- lib/fast/completions/sse.rb
|
|
47
|
+
- lib/fast/completions/telemetry.rb
|
|
47
48
|
- lib/fast/completions/tool.rb
|
|
48
49
|
- lib/fast/completions/types.rb
|
|
49
50
|
- lib/fast/completions/version.rb
|