omniai-google 3.14.0 → 3.15.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: 5954a8c865af0699193eec624f9ddbb6e7bb52f0df650d16a61ad3237b15aedd
4
- data.tar.gz: 3e455a236f2c073735e44054c55489c93418252259fd556df734d38ea7895043
3
+ metadata.gz: dc72ad3bc8cfe3181908f748f705708827ea8f10a3c533b9d1d4352d6f5d9df0
4
+ data.tar.gz: c29f890a61470dc3ddce22d5753ca4410a04ba91b93faab6381411805e41fdbd
5
5
  SHA512:
6
- metadata.gz: c99cb0f8e5d8e802ff247874ab5c4e4530fd5b91cadc617d506187125ed9603cc5468c09586f36f2ca2ddcde0549c7aba48d1b50285450a7d830b5c2fad3112b
7
- data.tar.gz: d32ccfaf6210fade5a0a7dfaf4c868a9474880d440f4465b959966bc1085509df3d67e50795555402b7ad069da47dedf1c91b2f1c8666799f47a16eb6ac43b7b
6
+ metadata.gz: bdee9a2925e8ea39873fb8e34b94378aa0549d0b1443a3c20b55bef586dda1208514e042ee63c6780ed6ec8f457b24a7af85f9def73d82c1c77e4a6ca84a6db2
7
+ data.tar.gz: 3402943f6994d0489fc2382731e41e116667e3e93b09908dc6549c1f890f7ba2bdb397e27af9d4999fb925a7be19489c2f1566dae6e56dcf9cdb61bf731e20c2
@@ -18,11 +18,69 @@ module OmniAI
18
18
  end
19
19
  end
20
20
 
21
+ validate!
21
22
  @data
22
23
  end
23
24
 
24
25
  protected
25
26
 
27
+ # Google reports a post-200 failure as an `error` object in the stream, which
28
+ # `process_data!` otherwise copies into @data and returns as an empty success.
29
+ def validate!
30
+ error = @data["error"]
31
+ if error
32
+ raise StreamError.new("the stream carried an error: #{error_summary(error)}",
33
+ provider_message: error["message"])
34
+ end
35
+
36
+ # No candidates at all (a usageMetadata-only stream) is incomplete too.
37
+ candidates = @data["candidates"] || []
38
+ incomplete = candidates.select { |candidate| incomplete?(candidate) }
39
+ return unless candidates.empty? || incomplete.any?
40
+
41
+ raise IncompleteStreamError,
42
+ "the stream ended without a finish reason: #{candidates_summary(incomplete)}"
43
+ end
44
+
45
+ # Only thought-only-and-empty counts. A missing finishReason alone would also condemn
46
+ # an unterminated stream that did deliver an answer, and discarding a usable answer to
47
+ # retry is the more expensive mistake.
48
+ def incomplete?(candidate)
49
+ return false if candidate["finishReason"]
50
+
51
+ parts = candidate.dig("content", "parts") || []
52
+ parts.none? { |part| part["functionCall"] || answer_part?(part) }
53
+ end
54
+
55
+ # "" is truthy in Ruby, so an empty text part must not count as an answer.
56
+ def answer_part?(part)
57
+ !thought_part?(part) && !part["text"].to_s.strip.empty?
58
+ end
59
+
60
+ # Code and status only. Google's `message` can echo request content back, and consumers
61
+ # log these, so it is omitted for the same reason candidates_summary omits part text.
62
+ #
63
+ # @param error [Hash]
64
+ # @return [String]
65
+ def error_summary(error)
66
+ "code=#{error['code'].inspect} status=#{error['status'].inspect}"
67
+ end
68
+
69
+ # Structure only: consumers log these messages and part text may be sensitive.
70
+ #
71
+ # @param candidates [Array<Hash>]
72
+ # @return [String]
73
+ def candidates_summary(candidates)
74
+ return "candidates=0" if candidates.empty?
75
+
76
+ candidates.map.with_index do |candidate, index|
77
+ parts = candidate.dig("content", "parts") || []
78
+ "candidate=#{index} parts=#{parts.size} " \
79
+ "thought_parts=#{parts.count { |part| part['thought'] }} " \
80
+ "finish_reason=#{candidate['finishReason'].inspect}"
81
+ end.join("; ")
82
+ end
83
+
26
84
  # @yield [delta]
27
85
  # @yieldparam delta [OmniAI::Chat::Delta]
28
86
  #
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module Google
5
+ # Raised when a stream ends with no `finishReason` and produced nothing but thinking.
6
+ # Rescue StreamError to catch this and stream errors together. `#provider_message` is
7
+ # always nil here -- there is no provider error to carry.
8
+ class IncompleteStreamError < StreamError; end
9
+ end
10
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module Google
5
+ # Raised when a streamed generation does not complete.
6
+ #
7
+ # Deliberately has no `#response`: the request returned 200 and the failure arrived inside
8
+ # the stream. Consumers branch on that to decide whether to retry.
9
+ class StreamError < OmniAI::Error
10
+ # Google's own explanation of the failure, verbatim.
11
+ #
12
+ # Named for what it holds rather than borrowing `value` from FinishReason, which is a
13
+ # provider enum rather than prose. Kept off `#message` because consumers log that and
14
+ # this text can echo request content -- a deliberate departure from HTTPError, which puts
15
+ # the whole response body into its message.
16
+ #
17
+ # @return [String, nil]
18
+ attr_reader :provider_message
19
+
20
+ # @param message [String, nil]
21
+ # @param provider_message [String, nil]
22
+ def initialize(message = nil, provider_message: nil)
23
+ @provider_message = provider_message
24
+ super(message)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OmniAI
4
4
  module Google
5
- VERSION = "3.14.0"
5
+ VERSION = "3.15.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omniai-google
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.14.0
4
+ version: 3.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Sylvestre
@@ -122,6 +122,8 @@ files:
122
122
  - lib/omniai/google/config.rb
123
123
  - lib/omniai/google/credentials.rb
124
124
  - lib/omniai/google/embed.rb
125
+ - lib/omniai/google/incomplete_stream_error.rb
126
+ - lib/omniai/google/stream_error.rb
125
127
  - lib/omniai/google/transcribe.rb
126
128
  - lib/omniai/google/transcribe_helpers.rb
127
129
  - lib/omniai/google/upload.rb