Packages
openai_sse_guard
0.1.0
Bounded, dependency-free SSE observer for conservative replay decisions in OpenAI-compatible Elixir clients.
Current section
Files
Jump to
Current section
Files
openai_sse_guard
README.md
README.md
# OpenaiSseGuard
`openai_sse_guard` is a dependency-free Elixir/OTP observer for bounded,
OpenAI-compatible Server-Sent Events (SSE). It records enough evidence to
make a conservative replay decision when a streaming HTTP connection ends:
terminal marker, protocol hint, output evidence, event limits, and a bounded
error code.
It does not open sockets, make requests, retry, sleep, retain generated text,
or decide whether a customer was charged. The caller owns transport,
cancellation, idempotency, billing, and replay policy.
## Install
Add the package to `mix.exs`:
```elixir
def deps do
[
{:openai_sse_guard, "~> 0.1"}
]
end
```
Then run:
```console
mix deps.get
```
The library has no runtime dependencies and works with ordinary OTP processes,
`Finch`, `Req`, `Mint`, Plug, or a custom HTTP client.
## Observe a stream
For an enumerable of byte chunks, `observe/2` keeps reduction and finalization
in one place:
```elixir
chunks = [
"data: {\\\"choices\\\":[{\\\"delta\\\":{\\\"content\\\":\\\"hello\\\"}}]}\\n\\n",
"data: [DONE]\\n\\n"
]
snapshot = OpenaiSseGuard.observe(chunks)
snapshot.protocol
#=> :chat_completions
snapshot.termination
#=> :done
snapshot.has_output
#=> true
```
For a long-lived HTTP process, keep the observer in the caller's state and
feed each binary chunk as it arrives:
```elixir
observer = OpenaiSseGuard.new(max_events: 5_000)
observer = OpenaiSseGuard.push(observer, chunk)
snapshot = OpenaiSseGuard.finish(observer)
if snapshot.termination == :unexpected_eof and snapshot.has_output do
# The provider may have produced visible or billable output. Do not replay
# without an application-level idempotency decision.
:manual_decision
end
```
Chunks may split a UTF-8 code point and may end in the middle of an SSE line.
The observer only validates text once a frame boundary is complete, so normal
network chunking does not create false malformed events.
## State contract
`OpenaiSseGuard.Snapshot` contains only bounded metadata:
| Field | Meaning |
| --- | --- |
| `protocol` | `:chat_completions`, `:responses`, or `:unknown` |
| `termination` | `:done`, `:incomplete`, `:error`, `:unexpected_eof`, or `:open` |
| `has_output` | A data-bearing event or named event was observed |
| `saw_terminal_event` | A completion, incomplete, or `[DONE]` marker was seen |
| `event_count` | Complete frames accepted within the event limit |
| `malformed_event_count` | Invalid UTF-8 or over-limit frames |
| `last_event_type` | A short identifier, never provider prose |
| `error_code` | A bounded `code` or `type` identifier when present |
The default limit is 64 KiB per frame and 10,000 frames per observer. Limits
are clamped to safe package-wide maxima so an untrusted response cannot request
an unbounded buffer. Unknown data-bearing events are conservative: they set
`has_output` rather than being treated as replay-safe.
## Protocol and retry boundaries
Framing follows the [WHATWG Server-Sent Events specification](https://html.spec.whatwg.org/multipage/server-sent-events.html).
`response.*` event names infer the Responses protocol; a `choices` field or
`[DONE]` infers Chat Completions. `response.completed`,
`response.incomplete`, and `[DONE]` are terminal markers. A provider `error`
event ends in `:error` and exposes only a bounded identifier.
This package is an observer, not a retry policy. Combine its snapshot with
your operation's idempotency key, HTTP status, rendered-byte state, billing
semantics, and attempt/time budgets. For provider error categories, consult
the [OpenAI error-code guide](https://developers.openai.com/api/docs/guides/error-codes);
for server retry hints, see the [MDN Retry-After reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Retry-After).
The [AI-ROUTER API gateway](https://ai-router.dev/) is one possible
OpenAI-compatible endpoint context. The package is provider-neutral and is not
affiliated with or endorsed by OpenAI.
## Related implementations
The repository contains a [replay-safety decision guide](https://github.com/airouter-dev/openai-sse-guard-elixir/blob/main/guides/stream-replay-safety.md).
For the same boundary in other ecosystems, compare the maintained
[JavaScript package on npm](https://www.npmjs.com/package/@ai-router/openai-compatible-errors),
[Python package on PyPI](https://pypi.org/project/openai-compatible-errors/),
[Ruby package on RubyGems](https://rubygems.org/gems/openai-compatible-errors),
[PHP package on Packagist](https://packagist.org/packages/airouter/openai-compatible-errors),
[Rust package on crates.io](https://crates.io/crates/llm-stream-guard),
[Deno package on JSR](https://jsr.io/@ai-router/openai-sse-guard), and
[Dart package on pub.dev](https://pub.dev/packages/openai_sse_guard).
These are contextual implementation links, not claims of endorsement or
shared runtime code.
## Development
```console
mix deps.get
mix format --check-formatted
mix test
mix docs
```
See [CONTRIBUTING.md](CONTRIBUTING.md), [SECURITY.md](SECURITY.md), and the
[stream replay-safety guide](guides/stream-replay-safety.md) before changing
framing or terminal semantics.
MIT licensed. Maintained by [AI-ROUTER contributors](https://ai-router.dev/).