Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

## [0.7.0]

### Added
- Added `OpenAIResponsesStream` flavor for OpenAI Responses API (see `examples/openai_responses_example.jl`)

## [0.6.2]

### Fixed
Expand Down
76 changes: 76 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

StreamCallbacks.jl is a Julia package that provides a unified streaming interface for Large Language Model (LLM) APIs. It handles Server-Sent Events (SSE) parsing, chunk processing, and response body reconstruction across multiple providers.

## Common Commands

```bash
# Run all tests
julia --project -e 'using Pkg; Pkg.test()'

# Run a single test file
julia --project -e 'using StreamCallbacks; include("test/stream_openai_chat.jl")'

# Start Julia REPL with project
julia --project

# Build documentation locally
julia --project=docs docs/make.jl
```

## Architecture

### Core Types (`src/interface.jl`)

- `AbstractStreamFlavor` - Base type for provider-specific streaming behavior
- `OpenAIStream` / `OpenAIChatStream` - OpenAI Chat Completions API
- `OpenAIResponsesStream` - OpenAI Responses API
- `AnthropicStream`, `OllamaStream` - Anthropic and Ollama APIs
- `StreamChunk` - Represents a single SSE chunk with `event`, `data`, and parsed `json`
- `StreamCallback` - Main callback object that accumulates chunks and manages output

### Provider-Specific Files

Each provider has its own file implementing three key methods:
- `is_done(flavor, chunk)` - Detect stream termination
- `extract_content(flavor, chunk)` - Extract displayable text from chunk
- `build_response_body(flavor, cb)` - Reconstruct standard API response from chunks

| File | Provider | Flavor | Termination Signal |
|------|----------|--------|-------------------|
| `src/stream_openai_chat.jl` | OpenAI Chat Completions | `OpenAIStream` | `[DONE]` data |
| `src/stream_openai_responses.jl` | OpenAI Responses API | `OpenAIResponsesStream` | `response.completed` event |
| `src/stream_anthropic.jl` | Anthropic | `AnthropicStream` | `:message_stop` event |
| `src/stream_ollama.jl` | Ollama | `OllamaStream` | `done: true` in JSON |

### Request Flow

1. `streamed_request!(cb, url, headers, input)` - Entry point in `src/shared_methods.jl`
2. `extract_chunks(flavor, blob)` - Parse SSE blob into `StreamChunk` array (handles spillover for incomplete messages)
3. For each chunk: `callback(cb, chunk)` → `extract_content()` → `print_content()`
4. `build_response_body(flavor, cb)` - Reconstruct response mimicking non-streaming API

### Extending the Package

To add a new provider:
1. Create new `AbstractStreamFlavor` subtype
2. Implement `is_done`, `extract_content`, and `build_response_body` methods
3. Export the new flavor type

To customize output handling:
- Extend `print_content(out, text)` for custom sinks (IO, Channel, or custom types)
- Create custom callback by subtyping `AbstractStreamCallback`

## Dependencies

- HTTP.jl - HTTP requests and SSE streaming
- JSON3.jl - JSON parsing
- PrecompileTools.jl - Precompilation workloads

## Integration

This package integrates with PromptingTools.jl via `configure_callback!` which auto-configures the `StreamCallback` flavor and necessary `api_kwargs`.
14 changes: 14 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# This is not a true makefile, just a collection of convenient scripts

default: help

format:
# assumes you have JuliaFormatter installed in your global env / somewhere on LOAD_PATH
julia --project=@Fmt -e 'using JuliaFormatter; format(".")'

test:
julia --project=. -e 'using Pkg; Pkg.test()'

help:
echo "make help - show this help"
echo "make format - format the code"
6 changes: 4 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "StreamCallbacks"
uuid = "c1b9e933-98a0-46fc-8ea7-3b58b195fb0a"
authors = ["J S <49557684+svilupp@users.noreply.github.com> and contributors"]
version = "0.6.2"
version = "0.7.0"

[deps]
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
Expand All @@ -13,12 +13,14 @@ Aqua = "0.8"
HTTP = "1.10"
JSON3 = "1.14"
PrecompileTools = "1.2"
Sockets = "1"
Test = "1"
julia = "1.9"

[extras]
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
Sockets = "6462fe0b-24de-5631-8697-dd941f90decc"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Aqua", "Test"]
test = ["Aqua", "Sockets", "Test"]
49 changes: 31 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ StreamCallbacks.jl is designed to unify streaming interfaces for Large Language
- [Installation](#installation)
- [Getting Started](#getting-started)
- [Usage Examples](#usage-examples)
- [Example with OpenAI API](#example-with-openai-api)
- [OpenAI Chat Completions API](#openai-chat-completions-api)
- [OpenAI Responses API](#openai-responses-api)
- [Example with PromptingTools.jl](#example-with-promptingtoolsjl)
- [Extending StreamCallbacks.jl](#extending-streamcallbacksjl)
- [StreamCallback Interface](#streamcallback-interface)
Expand All @@ -28,8 +29,12 @@ StreamCallbacks.jl is designed to unify streaming interfaces for Large Language
## Supported Providers

- **OpenAI API** (and all compatible providers)
- **Anthropic API**
- **Ollama API** (`api/chat` endpoint, OpenAI-compatible endpoint)
- `OpenAIChatStream` (alias: `OpenAIStream`) for Chat Completions API (`/v1/chat/completions`)
- `OpenAIResponsesStream` for Responses API (`/v1/responses`)
- **Anthropic API** (`AnthropicStream`)
- **Ollama API** (`OllamaStream` for `/api/chat` endpoint)

When used with PromptingTools.jl, these flavors map to `AbstractOpenAISchema` and `AbstractOpenAIResponsesSchema` respectively.

## Installation

Expand All @@ -55,36 +60,44 @@ cb = StreamCallback(out = stdout)

## Usage Examples

### Example with OpenAI API
### OpenAI Chat Completions API

```julia
using HTTP
using JSON3
using StreamCallbacks
using HTTP, JSON3, StreamCallbacks

# Prepare target URL and headers
url = "https://api.openai.com/v1/chat/completions"
headers = [
"Content-Type" => "application/json",
"Authorization" => "Bearer $(get(ENV, "OPENAI_API_KEY", ""))"
"Authorization" => "Bearer $(ENV["OPENAI_API_KEY"])"
]

# Create a StreamCallback object
cb = StreamCallback(out = stdout, flavor = OpenAIStream())

# Prepare the request payload
messages = [Dict("role" => "user", "content" => "Count from 1 to 100.")]
cb = StreamCallback(out = stdout, flavor = OpenAIChatStream())
messages = [Dict("role" => "user", "content" => "What is 2+2?")]
payload = IOBuffer()
JSON3.write(payload, (; stream = true, messages, model = "gpt-4o-mini", stream_options = (; include_usage = true)))

# Send the streamed request
resp = streamed_request!(cb, url, headers, payload)
```

### OpenAI Responses API

```julia
using HTTP, JSON3, StreamCallbacks

# Check the response
println("Response status: ", resp.status)
url = "https://api.openai.com/v1/responses"
headers = [
"Content-Type" => "application/json",
"Authorization" => "Bearer $(ENV["OPENAI_API_KEY"])"
]

cb = StreamCallback(out = stdout, flavor = OpenAIResponsesStream())
payload = IOBuffer()
JSON3.write(payload, (; stream = true, input = "What is 2+2?", model = "gpt-4o-mini"))

resp = streamed_request!(cb, url, headers, payload)
```

**Note**: For debugging, you can set `verbose = true` in the `StreamCallback` constructor to get detailed logs of each chunk. Ensure you enable DEBUG logging level in your environment.
**Note**: For debugging, set `verbose = true` in the `StreamCallback` constructor and enable DEBUG logging level.

### Example with PromptingTools.jl

Expand Down
45 changes: 29 additions & 16 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ StreamCallbacks.jl is designed to unify streaming interfaces for Large Language
## Supported Providers

- **OpenAI API** (and all compatible providers)
- **Anthropic API**
- `OpenAIChatStream` (alias: `OpenAIStream`) for Chat Completions API (`/v1/chat/completions`)
- `OpenAIResponsesStream` for Responses API (`/v1/responses`)
- **Anthropic API** (`AnthropicStream`)
- **Ollama API** (`OllamaStream` for `/api/chat` endpoint)

When used with PromptingTools.jl, these flavors map to `AbstractOpenAISchema` and `AbstractOpenAIResponsesSchema` respectively.

## Installation

Expand All @@ -42,36 +47,44 @@ cb = StreamCallback(out = stdout)

## Usage Examples

### Example with OpenAI API
### OpenAI Chat Completions API

```julia
using HTTP
using JSON3
using StreamCallbacks
using HTTP, JSON3, StreamCallbacks

# Prepare target URL and headers
url = "https://api.openai.com/v1/chat/completions"
headers = [
"Content-Type" => "application/json",
"Authorization" => "Bearer $(get(ENV, "OPENAI_API_KEY", ""))"
"Authorization" => "Bearer $(ENV["OPENAI_API_KEY"])"
]

# Create a StreamCallback object
cb = StreamCallback(out = stdout, flavor = OpenAIStream())

# Prepare the request payload
messages = [Dict("role" => "user", "content" => "Count from 1 to 100.")]
cb = StreamCallback(out = stdout, flavor = OpenAIChatStream())
messages = [Dict("role" => "user", "content" => "What is 2+2?")]
payload = IOBuffer()
JSON3.write(payload, (; stream = true, messages, model = "gpt-4o-mini", stream_options = (; include_usage = true)))

# Send the streamed request
resp = streamed_request!(cb, url, headers, payload)
```

### OpenAI Responses API

```julia
using HTTP, JSON3, StreamCallbacks

# Check the response
println("Response status: ", resp.status)
url = "https://api.openai.com/v1/responses"
headers = [
"Content-Type" => "application/json",
"Authorization" => "Bearer $(ENV["OPENAI_API_KEY"])"
]

cb = StreamCallback(out = stdout, flavor = OpenAIResponsesStream())
payload = IOBuffer()
JSON3.write(payload, (; stream = true, input = "What is 2+2?", model = "gpt-4o-mini"))

resp = streamed_request!(cb, url, headers, payload)
```

**Note**: For debugging, you can set `verbose = true` in the `StreamCallback` constructor to get detailed logs of each chunk. Ensure you enable DEBUG logging level in your environment.
**Note**: For debugging, set `verbose = true` in the `StreamCallback` constructor and enable DEBUG logging level.

### Example with PromptingTools.jl

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# Calling OpenAI with StreamCallbacks
# Calling OpenAI Chat Completions API with StreamCallbacks
using HTTP, JSON3
using StreamCallbacks

## Prepare target and auth
url = "https://api.openai.com/v1/chat/completions"
headers = [
"Content-Type" => "application/json",
"Authorization" => "Bearer $(get(ENV, "OPENAI_API_KEY", ""))"
"Authorization" => "Bearer $(ENV["OPENAI_API_KEY"])"
];

## Send the request
cb = StreamCallback(; out = stdout, flavor = OpenAIStream())
cb = StreamCallback(; out = stdout, flavor = OpenAIChatStream())
messages = [Dict("role" => "user",
"content" => "Count from 1 to 100.")]
payload = IOBuffer()
Expand Down
24 changes: 24 additions & 0 deletions examples/openai_responses_example.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Calling OpenAI Responses API with StreamCallbacks
using HTTP, JSON3
using StreamCallbacks

## Prepare target and auth
url = "https://api.openai.com/v1/responses"
headers = [
"Content-Type" => "application/json",
"Authorization" => "Bearer $(ENV["OPENAI_API_KEY"])"
];

## Send the request
cb = StreamCallback(; out = stdout, flavor = OpenAIResponsesStream())
payload = IOBuffer()
JSON3.write(payload, (; stream = true, input = "Count from 1 to 20", model = "gpt-5-mini"))
resp = streamed_request!(cb, url, headers, payload);

## Check the response
resp # should be a `HTTP.Response` object with a reconstructed response body

## Check the callback
cb.chunks # should be a vector of `StreamChunk` objects with received SSE events

# TIP: For debugging, use `cb.verbose = true` in the `StreamCallback` constructor and enable DEBUG loglevel.
2 changes: 1 addition & 1 deletion scripts/bootstrap_with_llm.jl
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ purpose = "Library to unify streaming interfaces for LLMs across many providers.
# # Update README
task = """Update the provided README.md file with best in class information for what the package could be.
Highlight that the package in experimental stage and under development (use strong warning at the top)."""
user_files = files_to_prompt(["README.md", "examples/openai_example.jl"])
user_files = files_to_prompt(["README.md", "examples/openai_chat_example.jl"])

conv = aigenerate(tpl; pkg_name, purpose, user_files, task,
model = "gpt4o", return_all = true)
Expand Down
8 changes: 5 additions & 3 deletions src/StreamCallbacks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ module StreamCallbacks
using HTTP, JSON3
using PrecompileTools

export StreamCallback, StreamChunk, OpenAIStream, AnthropicStream, OllamaStream,
streamed_request!
export StreamCallback, StreamChunk, OpenAIStream, OpenAIChatStream, OpenAIResponsesStream,
AnthropicStream, OllamaStream, streamed_request!
include("interface.jl")

include("shared_methods.jl")

include("stream_openai.jl")
include("stream_openai_chat.jl")

include("stream_openai_responses.jl")

include("stream_anthropic.jl")

Expand Down
6 changes: 5 additions & 1 deletion src/interface.jl
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,15 @@ abstract type AbstractStreamCallback end
Abstract type for the stream flavor, ie, the API provider.

Available flavors:
- `OpenAIStream` for OpenAI API
- `OpenAIStream` / `OpenAIChatStream` for OpenAI Chat Completions API
- `OpenAIResponsesStream` for OpenAI Responses API
- `AnthropicStream` for Anthropic API
- `OllamaStream` for Ollama API
"""
abstract type AbstractStreamFlavor end
struct OpenAIStream <: AbstractStreamFlavor end
const OpenAIChatStream = OpenAIStream
struct OpenAIResponsesStream <: AbstractStreamFlavor end
struct AnthropicStream <: AbstractStreamFlavor end
struct OllamaStream <: AbstractStreamFlavor end

Expand Down
Loading