openai

Version, currently main branch1 version
  • main branchlatestDec 19, 2025

github.com/dsisnero/openai

A comprehensive Crystal client for the OpenAI API, auto-generated from the official OpenAPI specification. Supports all OpenAI API endpoints including chat completions, embeddings, image generation, audio processing, and more. Includes provider extension system for OpenAI-compatible APIs like DeepSeek.

0 stars
0 dependents
License: MIT

Installation

# Add this to your shard.yml
dependencies:
  openai:
    github: dsisnero/openai
    branch: main

main is a branch, not a release, so this tracks it rather than pinning a version.

Then run:

shards install

shard.yml

Crystal
>= 1.18.2
License
MIT
Author
Dominic Sisneros
Targets
  • list_models from examples/list_models.cr
  • tool_calling_with_schema from examples/tool_calling_with_schema.cr

Dependencies

Runtime Dependencies

Development Dependencies

  • webmock~> 0.14.0github: manastech/webmock.crdev

README

OpenAI Crystal Client

CI GitHub Crystal Shards

A comprehensive Crystal client for the OpenAI API, auto-generated from the official OpenAPI specification.

Features

  • Full coverage of the OpenAI API (2480 types, 220 endpoints)
  • Type-safe request and response objects with JSON serialization
  • Server-Sent Events (SSE) streaming support for chat completions
  • Multipart form data support for file uploads
  • Comprehensive error handling with typed exceptions
  • Provider extension system for OpenAI-compatible APIs (DeepSeek, Groq, etc.)
  • Auto-generated from OpenAPI spec v2.3.0

Installation

  1. Add the dependency to your shard.yml:

    dependencies:
      openai:
        github: dsisnero/openai
  2. Run shards install

Usage

Basic Setup

require "openai"

# Create a client with your API key
client = OpenAI::Client.new(api_key: ENV["OPENAI_API_KEY"])

# Or use a custom base URL (for Azure OpenAI, proxies, etc.)
client = OpenAI::Client.new(
  api_key: ENV["OPENAI_API_KEY"],
  base_url: "https://your-resource.openai.azure.com"
)

Chat Completions

# Using helper methods (recommended)
request = OpenAI::CreateChatCompletionRequest.new(model: "gpt-4o")
  .add_system_message("You are a helpful assistant")
  .add_user_message("Hello, how are you?")

response = client.create_chat_completion(request)
puts response.choices.first.message.content

Using ChatMessages Module

# Using the ChatMessages factory module
request = OpenAI::CreateChatCompletionRequest.new(
  model: "gpt-4o",
  messages: [
    OpenAI::ChatMessages.system("You are a helpful assistant"),
    OpenAI::ChatMessages.user("What is 2 + 2?"),
  ] of OpenAI::ChatCompletionRequestMessage
)

response = client.create_chat_completion(request)
puts response.choices.first.message.content

Parsing Messages from JSON

# Parse messages from JSON (useful for loading conversations)
json_messages = %([
  {"role": "system", "content": "You are a helpful assistant"},
  {"role": "user", "content": "Hi there!"}
])

request = OpenAI::CreateChatCompletionRequest.new(model: "gpt-4o")
  .add_messages_from_json(json_messages)

response = client.create_chat_completion(request)
puts response.choices.first.message.content

Streaming Chat Completions

request = OpenAI::CreateChatCompletionRequest.new(model: "gpt-4o")
  .add_user_message("Tell me a story")

request.stream = true

client.create_chat_completion_stream(request) do |chunk|
  chunk.choices.each do |choice|
    if content = choice.delta.content
      print content
    end
  end
end
puts

Embeddings

request = OpenAI::CreateEmbeddingRequest.new(
  model: "text-embedding-3-small",
  input: "The quick brown fox jumps over the lazy dog"
)

response = client.create_embedding(request)
embedding = response.data.first.embedding
puts "Embedding dimension: #{embedding.size}"

Image Generation

request = OpenAI::CreateImageRequest.new(
  model: "dall-e-3",
  prompt: "A white siamese cat",
  n: 1,
  size: OpenAI::CreateImageRequestSize::N1024X1024
)

response = client.create_image(request)
puts response.data.first.url

Error Handling

begin
  response = client.create_chat_completion(request)
rescue ex : OpenAI::AuthenticationError
  puts "Invalid API key: #{ex.message}"
rescue ex : OpenAI::RateLimitError
  puts "Rate limited: #{ex.message}"
rescue ex : OpenAI::BadRequestError
  puts "Bad request: #{ex.message}"
rescue ex : OpenAI::APIError
  puts "API error: #{ex.message}"
end

OpenAI-Compatible Providers

This client supports OpenAI-compatible APIs through a provider extension system. Providers allow you to override types and methods that differ from the OpenAI spec.

Using the DeepSeek Provider

require "openai"
require "openai/providers/deepseek"

# Create a DeepSeek client
client = OpenAI::Providers::DeepSeek::Client.new(
  api_key: ENV["DEEPSEEK_API_KEY"]
)

# List models (uses DeepSeek-compatible types)
models = client.list_models
models.data.each { |m| puts m.id }

# Chat completions work the same as OpenAI
request = OpenAI::CreateChatCompletionRequest.new(model: "deepseek-chat")
  .add_system_message("You are a helpful assistant")
  .add_user_message("Hello!")

response = client.create_chat_completion(request)
puts response.choices.first.message.content

# Streaming also works
request.stream = true
client.create_chat_completion_stream(request) do |chunk|
  chunk.choices.each do |choice|
    if content = choice.delta.content
      print content
    end
  end
end

Creating a Custom Provider

To add support for another OpenAI-compatible API, create a provider module:

# src/openai/providers/my_provider.cr

require "../client"
require "../types"

module OpenAI
  module Providers
    module MyProvider
      BASE_URL = "https://api.myprovider.com/v1"

      # Override types that differ from OpenAI
      # Example: MyProvider doesn't return `created` timestamp for models
      class Model
        include JSON::Serializable

        property id : String
        
        @[JSON::Field(emit_null: false)]
        property created : Int64?  # Make optional
        
        property object : String
        property owned_by : String

        def initialize(
          @id : String = "",
          @created : Int64? = nil,
          @object : String = "model",
          @owned_by : String = ""
        )
        end
      end

      class ListModelsResponse
        include JSON::Serializable

        property object : String
        property data : Array(Model)

        def initialize(
          @object : String = "list",
          @data : Array(Model) = [] of Model
        )
        end
      end

      # Create a client that inherits from OpenAI::Client
      class Client < OpenAI::Client
        def initialize(
          api_key : String = ENV["MY_PROVIDER_API_KEY"]? || "",
          base_url : String = BASE_URL,
          timeout : Time::Span = OpenAI::Client::DEFAULT_TIMEOUT,
          organization : String? = nil
        )
          super(
            api_key: api_key,
            base_url: base_url,
            timeout: timeout,
            organization: organization
          )
        end

        # Override methods that need different return types
        def list_models : ListModelsResponse
          path = "/models"
          response = request("GET", path)
          ListModelsResponse.from_json(response.body)
        end
      end
    end
  end
end

Provider Extension Guidelines

  1. Only override what's different - Most endpoints work without changes
  2. Use @[JSON::Field(emit_null: false)] - For optional fields not in the provider's response
  3. Inherit from OpenAI::Client - Get all standard functionality for free
  4. Use protected methods - request() and request_stream() are available to subclasses
  5. Add conversion methods - For interoperability with standard OpenAI types

Known Provider Differences

ProviderDifferences
DeepSeekModel.created not returned in /models
GroqTBD
TogetherTBD
AnthropicDifferent API structure (not directly compatible)

API Coverage

The client covers all OpenAI API endpoints organized by category:

CategoryEndpointsDescription
Chat12Chat completions, including streaming
Completions1Legacy text completions
Embeddings1Text embeddings
Images3Image generation and editing
Audio3Speech, transcription, translation
Files5File upload and management
Fine-tuning13Model fine-tuning
Assistants23Assistants API
Models3Model listing and info
Moderations1Content moderation
Batch4Batch processing
Uploads4Large file uploads
Vector Stores16Vector storage for RAG
Evals12Model evaluations

Development

Regenerating the Client from OpenAPI Spec

The client is auto-generated from the OpenAPI specification. To regenerate after updating the spec:

Quick Rebuild

crystal run build.cr

Full Rebuild with Validation

# 1. Remove old generated files (keeps providers/)
rm -rf src/openai/api src/openai/types src/openai/client.cr src/openai/error.cr \
       src/openai/multipart.cr src/openai/streaming.cr src/openai/types.cr
rm -rf spec/openai

# 2. Regenerate from OpenAPI spec
crystal run build.cr

# 3. Format generated code
crystal tool format src/openai/ spec/

# 4. Verify compilation
crystal build src/openai.cr --no-codegen

# 5. Run tests
crystal spec

# 6. Test examples
crystal run examples/completion.cr --no-codegen

Customizing the Generator

The build.cr generator can be modified for custom needs:

# Key areas in build.cr:

# Type mapping (line ~380)
# - Modify how OpenAPI types map to Crystal types

# Endpoint parsing (line ~1000)  
# - Customize how API methods are generated

# Chat helpers (line ~1730)
# - Add/modify convenience methods for chat

# Client generation (line ~2200)
# - Customize HTTP client behavior

Project Structure

src/openai/
  client.cr       # Main HTTP client (generated)
  types.cr        # All generated types (2480+)
  error.cr        # Error hierarchy (generated)
  multipart.cr    # Multipart form encoding (generated)
  streaming.cr    # SSE streaming support (generated)
  api/            # API methods by category (generated)
    chat.cr
    embeddings.cr
    ...
  types/          # Type references by category (generated)
    chat.cr
    embeddings.cr
    ...
  providers/      # Provider extensions (NOT generated, manually maintained)
    deepseek.cr
    ...

build.cr          # Code generator
openapi.documented.yml  # OpenAPI specification

Running Tests

# Install dependencies
shards install

# Run all tests
crystal spec

# Run specific test file
crystal spec spec/openai/types/chat_spec.cr

Running Examples

# Set API keys
export OPENAI_API_KEY="your-openai-key"
export DEEPSEEK_API_KEY="your-deepseek-key"

# Run examples
crystal run examples/completion.cr
crystal run examples/image.cr
crystal run examples/deepseek.cr
crystal run examples/list_models.cr

Contributing

Please read CONTRIBUTING.md for details on our code of conduct and the process for submitting pull requests.

Contributing a Provider

  1. Create src/openai/providers/your_provider.cr
  2. Add an example in examples/your_provider.cr
  3. Document differences in the README
  4. Submit a PR

Support

License

This project is licensed under the MIT License - see the LICENSE file for details.

Changelog

See CHANGELOG.md for a list of changes.

Contributors

Acknowledgments

  • OpenAI for their comprehensive API
  • The Crystal community for excellent tooling and support
  • All contributors who help improve this project