@@ -29,10 +29,17 @@ const (
2929 ProviderErrorKindUnknown ProviderErrorKind = "unknown"
3030)
3131
32- // providerErrorCodeEmptyStream is the stable code carried by empty-stream
33- // provider errors. Detection goes through errors.Is(err, ErrEmptyStream);
34- // the code exists for observability (span/error attributes), not matching.
35- const providerErrorCodeEmptyStream = "empty_stream"
32+ const (
33+ // providerErrorCodeEmptyStream is the stable code carried by empty-stream
34+ // provider errors. Detection goes through errors.Is(err, ErrEmptyStream);
35+ // the code exists for observability (span/error attributes), not matching.
36+ providerErrorCodeEmptyStream = "empty_stream"
37+
38+ // providerErrorCodeTruncatedStream is the stable code carried by provider
39+ // errors for streams that closed mid-message. Like the empty-stream code,
40+ // it exists for observability, not matching.
41+ providerErrorCodeTruncatedStream = "truncated_stream"
42+ )
3643
3744// ProviderError describes a failure returned by a model provider (e.g. Bedrock).
3845// It is intended to cross package boundaries so runtimes can surface stable,
@@ -154,6 +161,36 @@ func NewEmptyStreamError(provider, operation, message string) error {
154161 return errors .Join (ErrEmptyStream , pe )
155162}
156163
164+ // NewStreamEndedEarlyError classifies an event stream that terminated cleanly
165+ // before the provider's message-stop boundary. Adapters call it from their
166+ // stream event loops when the provider closes the connection without error
167+ // but the message protocol is unfinished. started reports whether an
168+ // assistant message had begun:
169+ //
170+ // - started=false means the provider produced an empty completion; the
171+ // result is an empty-stream error (see NewEmptyStreamError) that callers
172+ // may retry via errors.Is(err, ErrEmptyStream).
173+ // - started=true means the stream was truncated mid-generation; the result
174+ // is a retryable unavailable ProviderError (code truncated_stream)
175+ // without the empty-stream sentinel, so pre-output retry policies never
176+ // match partially delivered responses.
177+ func NewStreamEndedEarlyError (provider , operation string , started bool ) error {
178+ if ! started {
179+ return NewEmptyStreamError (provider , operation , "stream ended before message start" )
180+ }
181+ return NewProviderError (
182+ provider ,
183+ operation ,
184+ 0 ,
185+ ProviderErrorKindUnavailable ,
186+ providerErrorCodeTruncatedStream ,
187+ "stream ended before message stop" ,
188+ "" ,
189+ true ,
190+ nil ,
191+ )
192+ }
193+
157194// ClassifyHTTPStatus maps an HTTP status code returned by a model provider to
158195// the goa-ai provider error contract. It is the single status-to-kind table
159196// for adapters that need only status-based classification (Vertex Gemini,
0 commit comments