fast-completions 0.6.0 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d3db818f96038ddf53481fea90ce309a3a01be84d924e8d4707ce121a9232ba
4
- data.tar.gz: a6aa0363ad9ba1c6c2b45e47645137390254d8c1b0c8067e8de7f6cc64ed7e43
3
+ metadata.gz: e38eb0a31e93234c4564f2719e8be0d4076f5342170989f2bf5fce7d5e03969b
4
+ data.tar.gz: 91c6a0579beb8638180cf0dc131eb8e47ffd1bd57dda30a2206cc47f7a6f5034
5
5
  SHA512:
6
- metadata.gz: 63a9237cef1bbf316295f96bb6bd13107f5efadef2f0434070070edbd1a74a4668c725d6b7690a73cdfa7d02b750df2c4a85f5a60a1f77b96d0c48d4767c6f20
7
- data.tar.gz: 90c846372d3c94697e85de1075d61c7b40525b9c0ad7d2dcc11f632c6204d002e946e5ffb9b62529da0695ebeab930af24413475366ef7d09204f3f233ffefea
6
+ metadata.gz: 631605668caff279ac0da9d65c381e98c5245bd842a3fd5312bb03b1f52d3b13269f7daa430b4890205f50b7f0d6858dd9af9f965cd038610f713f8da81c0b33
7
+ data.tar.gz: 457ea4b6084d1df35789b5840078d990d5d66971b556ba0603377097378fff00160d406c89c848524cae5e0a9676072a6e205fa9a1e9f71561219c9b79d46499
data/CHANGELOG.md CHANGED
@@ -3,6 +3,43 @@
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.8.0 (2026-10-30)
7
+
8
+ - Ruby 3.3 support dropped: `required_ruby_version` moves from
9
+ `>= 3.3` to `>= 3.4`, and the monorepo CI matrix narrows to 3.4/4.0
10
+ to match. No other platform change — no new dependencies, no API
11
+ movement.
12
+
13
+ ## 0.7.0 (2026-10-25)
14
+
15
+ - Telemetry sink interface: `Client.new` accepts `telemetry:` — any
16
+ Enumerable of sink objects (an Array, a Set, …), validated loudly at
17
+ construction (a non-Enumerable, or a
18
+ sink not responding to all five hooks, raises `ArgumentError` naming
19
+ the offender and the missing hook; duck-typed sinks are accepted
20
+ without module inclusion; the client keeps its own frozen Array —
21
+ `entries` — so the caller's container is never frozen or mutated) — and dispatches five
22
+ observation hooks around a completion, ONE `Client#stream` invocation
23
+ (creation retries inside, tool execution outside):
24
+ `on_completion_start(model.id, body bytesize)` before the first POST,
25
+ `on_completion_ttfb` at the first parser item of the successful
26
+ attempt (a retried attempt's clock restarts), `on_event` for every
27
+ Event — before its consumer twin, terminals and the salvage `:error`
28
+ included, and even with no consumer block — `on_completion_end(stop
29
+ reason, duration, live message)` when `stream` returns, and
30
+ `on_completion_error(error Symbol derived from the raising class,
31
+ duration)` on every raise-out, cancellation included. Multiple sinks
32
+ dispatch in registration order. The allocation discipline is pinned
33
+ by spec measurement gates: zero added heap objects on the hot paths
34
+ with no sink registered, zero ADDED objects with a no-op sink (only
35
+ refs and immediates cross the boundary, durations are flonum
36
+ differences, every call site short-circuits on a precomputed
37
+ emptiness check), and zero added `Process.clock_gettime` reads. The
38
+ new reference page `docs/reference/telemetry.md` documents the
39
+ protocol, the ordering rule (telemetry is the ground-truth record
40
+ when consumer code is the suspect), the read-only convention for
41
+ sinks, and that metric naming belongs to the adapter.
42
+
6
43
  ## 0.6.0 (2026-10-25)
7
44
 
8
45
  - `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
- # The POST attempt — creation through status receipt (§2),
113
- # bounded-retried on pi's trigger set; every final creation
114
- # failure raises its mapped error from in there.
115
- response = create_response(headers, body, watchdog)
116
- yield accumulator.start_event if block # :start precedes every block event (§10.6)
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 ? raise(TimeoutError, "no data within #{@timeout}s") : raise(CancelledError, "stream cancelled")
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: any other streaming failure — same terminal, raised unchanged
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
- # Complete bodies return the connection to the pool; an aborted
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
- raise TimeoutError, "no data within #{@timeout}s" if watchdog.fired
138
-
139
- deliver_terminal(accumulator, &block)
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
- def feed_stream(response, accumulator, watchdog, &block)
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
- events = accumulator.consume(json)
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
- def deliver_terminal(accumulator, &block)
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
- events.each { |event| yield event } if block
343
- if outcome[0] == :stream_error
344
- yield Event.new(type: :error, reason: :error, message: accumulator.partial) if block
345
- raise StreamError.new(outcome[1], stop_reason: :error)
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
- yield Event.new(type: :done, reason: outcome[1], message: accumulator.partial) if block
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
- # Statusless — no response was received (pi's statusless trigger).
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
- return response if (200...300).cover?(response.status)
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
- def salvage(accumulator)
423
- return unless block_given?
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
- yield Event.new(type: :error, reason: :error, message: accumulator.partial)
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). Same discipline as stream's ensure.
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Fast
4
4
  module Completions
5
- VERSION = "0.6.0"
5
+ VERSION = "0.8.0"
6
6
  end
7
7
  end
@@ -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.6.0
4
+ version: 0.8.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
@@ -62,7 +63,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
62
63
  requirements:
63
64
  - - ">="
64
65
  - !ruby/object:Gem::Version
65
- version: '3.3'
66
+ version: '3.4'
66
67
  required_rubygems_version: !ruby/object:Gem::Requirement
67
68
  requirements:
68
69
  - - ">="