├── CHANGELOG.md ├── spec ├── deepseek │ ├── configuration_spec.rb │ └── client_spec.rb └── spec_helper.rb ├── .rspec ├── lib ├── deepseek │ ├── version.rb │ ├── configuration.rb │ ├── errors.rb │ └── client.rb └── deepseek.rb ├── .gitignore ├── Rakefile ├── Gemfile ├── bin ├── setup └── console ├── LICENSE.txt ├── examples └── basic_usage.rb ├── .github └── workflows │ └── ci.yml ├── deepseek-client.gemspec ├── CODE_OF_CONDUCT.md ├── API.md ├── CONTRIBUTING.md └── README.md /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/deepseek/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | --format documentation 3 | --color 4 | -------------------------------------------------------------------------------- /lib/deepseek/version.rb: -------------------------------------------------------------------------------- 1 | module Deepseek 2 | VERSION = "0.1.0" 3 | end -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.gem 3 | *.rbc 4 | /.config 5 | /coverage/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | .rspec_status 10 | Gemfile.lock 11 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) do |t| 5 | t.rspec_opts = '--format documentation' 6 | end 7 | 8 | task :default => :spec 9 | task :test => :spec # Add this line to make 'rake test' work -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in deepseek.gemspec 4 | gemspec 5 | 6 | gem "rake", "~> 13.0" 7 | gem "rspec", "~> 3.0" 8 | gem "webmock", "~> 3.0" 9 | gem "simplecov", require: false 10 | gem "simplecov-cobertura", require: false -------------------------------------------------------------------------------- /lib/deepseek.rb: -------------------------------------------------------------------------------- 1 | require 'faraday' 2 | require 'json' 3 | 4 | require_relative 'deepseek/version' 5 | require_relative 'deepseek/configuration' 6 | require_relative 'deepseek/errors' 7 | require_relative 'deepseek/client' 8 | 9 | module Deepseek 10 | class << self 11 | attr_writer :configuration 12 | 13 | def configuration 14 | @configuration ||= Configuration.new 15 | end 16 | 17 | def configure 18 | yield(configuration) 19 | end 20 | 21 | def reset_configuration! 22 | @configuration = Configuration.new 23 | end 24 | end 25 | end -------------------------------------------------------------------------------- /lib/deepseek/configuration.rb: -------------------------------------------------------------------------------- 1 | module Deepseek 2 | class Configuration 3 | attr_accessor :api_key, :api_base_url, :timeout, :max_retries 4 | 5 | def initialize 6 | @api_key = ENV['DEEPSEEK_API_KEY'] 7 | @api_base_url = ENV['DEEPSEEK_API_BASE_URL'] || 'https://api.deepseek.com' 8 | @timeout = ENV['DEEPSEEK_TIMEOUT']&.to_i || 30 9 | @max_retries = ENV['DEEPSEEK_MAX_RETRIES']&.to_i || 3 10 | end 11 | 12 | def validate! 13 | raise ConfigurationError, 'API key must be set' if api_key.nil? || api_key.to_s.strip.empty? 14 | end 15 | end 16 | end -------------------------------------------------------------------------------- /lib/deepseek/errors.rb: -------------------------------------------------------------------------------- 1 | module Deepseek 2 | class Error < StandardError; end 3 | 4 | class ConfigurationError < Error; end 5 | 6 | class APIError < Error 7 | attr_reader :code, :response 8 | 9 | def initialize(message = nil, code: nil, response: nil) 10 | @code = code 11 | @response = response 12 | super(message) 13 | end 14 | end 15 | 16 | class AuthenticationError < APIError; end 17 | class RateLimitError < APIError; end 18 | class InvalidRequestError < APIError; end 19 | class APIConnectionError < APIError; end 20 | class ServiceUnavailableError < APIError; end 21 | end -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path('..', __dir__) 6 | 7 | def system!(*args) 8 | system(*args) || abort("\n== Command #{args} failed ==") 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to set up or update your development environment automatically. 13 | puts '== Installing dependencies ==' 14 | system! 'gem install bundler --conservative' 15 | system('bundle check') || system!('bundle install') 16 | 17 | puts "\n== Creating example .env file ==" 18 | if File.exist?('.env') 19 | puts '== .env file already exists ==' 20 | else 21 | FileUtils.cp '.env.example', '.env' if File.exist?('.env.example') 22 | end 23 | 24 | puts "\n== Removing old logs and tempfiles ==" 25 | system! 'bin/rake log:clear tmp:clear' 26 | 27 | puts "\n== Setup complete ==" 28 | end -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "deepseek" 5 | require "pry" 6 | 7 | # Load environment variables from .env file if present 8 | begin 9 | require 'dotenv' 10 | Dotenv.load 11 | rescue LoadError 12 | # dotenv gem not installed, skipping 13 | end 14 | 15 | # Configure the client with environment variables 16 | Deepseek.configure do |config| 17 | config.api_key = ENV['DEEPSEEK_API_KEY'] 18 | end 19 | 20 | # Create a client instance for convenience 21 | @client = Deepseek::Client.new 22 | 23 | puts "Loaded Deepseek Ruby SDK (v#{Deepseek::VERSION})" 24 | puts "Environment: #{ENV['DEEPSEEK_ENV'] || 'development'}" 25 | puts "\nAvailable variables:" 26 | puts " @client - Preconfigured Deepseek::Client instance" 27 | puts "\nExample usage:" 28 | puts ' response = @client.chat(messages: [{ role: "user", content: "Hello!" }])' 29 | puts "\n" 30 | 31 | Pry.start -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Nagendra Dhanakeerthi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /examples/basic_usage.rb: -------------------------------------------------------------------------------- 1 | require 'deepseek' 2 | 3 | # Configure the client 4 | Deepseek.configure do |config| 5 | config.api_key = ENV['DEEPSEEK_API_KEY'] 6 | config.timeout = 60 # Optional: Set custom timeout 7 | end 8 | 9 | # Initialize client 10 | client = Deepseek::Client.new 11 | 12 | # Simple chat completion 13 | response = client.chat( 14 | messages: [ 15 | { role: 'user', content: 'What is the capital of France?' } 16 | ], 17 | model: 'deepseek-chat' 18 | ) 19 | 20 | puts "Response: #{response['choices'].first['message']['content']}" 21 | 22 | # Chat completion with system message 23 | response = client.chat( 24 | messages: [ 25 | { role: 'system', content: 'You are a helpful assistant who speaks like Shakespeare.' }, 26 | { role: 'user', content: 'Tell me about artificial intelligence.' } 27 | ], 28 | model: 'deepseek-chat', 29 | temperature: 0.7 30 | ) 31 | 32 | puts "\nShakespearean AI response: #{response['choices'].first['message']['content']}" 33 | 34 | # Error handling example 35 | begin 36 | client.chat(messages: [{ role: 'user', content: 'Hello' }], model: 'nonexistent-model') 37 | rescue Deepseek::InvalidRequestError => e 38 | puts "\nError: #{e.message}" 39 | rescue Deepseek::APIError => e 40 | puts "\nAPI Error: #{e.message}" 41 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | require 'simplecov-cobertura' 3 | 4 | # Configure SimpleCov 5 | SimpleCov.start do 6 | add_filter '/spec/' 7 | add_filter '/vendor/' 8 | 9 | enable_coverage :branch 10 | 11 | # Add groups for better organization 12 | add_group 'Client', 'lib/deepseek/client.rb' 13 | add_group 'Configuration', 'lib/deepseek/configuration.rb' 14 | add_group 'Errors', 'lib/deepseek/errors.rb' 15 | 16 | track_files 'lib/**/*.rb' 17 | end 18 | 19 | # Set formatter for CodeClimate 20 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ 21 | SimpleCov::Formatter::HTMLFormatter, 22 | SimpleCov::Formatter::CoberturaFormatter 23 | ]) 24 | 25 | require 'deepseek' 26 | require 'webmock/rspec' 27 | 28 | RSpec.configure do |config| 29 | config.expect_with :rspec do |expectations| 30 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 31 | end 32 | 33 | config.mock_with :rspec do |mocks| 34 | mocks.verify_partial_doubles = true 35 | end 36 | 37 | config.shared_context_metadata_behavior = :apply_to_host_groups 38 | config.order = :random 39 | config.example_status_persistence_file_path = "spec/examples.txt" 40 | 41 | config.before(:each) do 42 | WebMock.disable_net_connect! 43 | end 44 | 45 | config.after(:each) do 46 | WebMock.reset! 47 | end 48 | end -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | ruby-version: ['3.0', '3.1', '3.2'] 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Ruby ${{ matrix.ruby-version }} 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | ruby-version: ${{ matrix.ruby-version }} 23 | 24 | - name: Setup Bundler 25 | run: | 26 | gem install bundler 27 | bundle config set --local path 'vendor/bundle' 28 | bundle lock --add-platform x86_64-linux 29 | bundle lock --add-platform ruby 30 | bundle install --jobs 4 31 | 32 | - name: Run tests 33 | run: bundle exec rake spec 34 | env: 35 | COVERAGE: true 36 | DEEPSEEK_API_KEY: 'test-key' 37 | 38 | - name: Publish code coverage 39 | if: github.ref == 'refs/heads/main' && matrix.ruby-version == '3.2' 40 | uses: paambaati/codeclimate-action@v5.0.0 41 | env: 42 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 43 | with: 44 | coverageCommand: bundle exec rake spec 45 | coverageLocations: ${{github.workspace}}/coverage/coverage.xml:cobertura 46 | debug: true 47 | 48 | - name: Upload coverage artifacts 49 | uses: actions/upload-artifact@v4 50 | with: 51 | name: coverage-report-ruby-${{ matrix.ruby-version }} 52 | path: coverage/ 53 | retention-days: 14 54 | if-no-files-found: error -------------------------------------------------------------------------------- /deepseek-client.gemspec: -------------------------------------------------------------------------------- 1 | require_relative 'lib/deepseek/version' 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "deepseek-client" 5 | spec.version = Deepseek::VERSION 6 | spec.authors = ["Nagendra Dhanakeerthi"] 7 | spec.email = ["nagendra.dhanakeerthi@gmail.com"] 8 | 9 | spec.summary = "Ruby SDK for Deepseek AI API" 10 | spec.description = "A comprehensive Ruby client library for interacting with Deepseek's AI APIs, " \ 11 | "providing a simple and intuitive interface for making API calls, handling responses, " \ 12 | "and managing API resources." 13 | spec.homepage = "https://github.com/nagstler/deepseek-ruby" 14 | spec.license = "MIT" 15 | spec.required_ruby_version = ">= 2.7.0" 16 | 17 | spec.metadata = { 18 | "homepage_uri" => spec.homepage, 19 | "source_code_uri" => spec.homepage, 20 | "changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md", 21 | "documentation_uri" => "#{spec.homepage}/blob/main/README.md", 22 | "bug_tracker_uri" => "#{spec.homepage}/issues" 23 | } 24 | 25 | # Specify which files should be added to the gem when it is released. 26 | spec.files = Dir[ 27 | "lib/**/*", 28 | "CHANGELOG.md", 29 | "LICENSE.txt", 30 | "README.md", 31 | "bin/*" 32 | ] 33 | spec.bindir = "bin" 34 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 35 | spec.require_paths = ["lib"] 36 | 37 | # Runtime dependencies 38 | spec.add_dependency "faraday", "~> 2.0" 39 | spec.add_dependency "faraday-retry", "~> 2.0" 40 | 41 | # Development dependencies 42 | spec.add_development_dependency "bundler", "~> 2.0" 43 | spec.add_development_dependency "rake", "~> 13.0" 44 | spec.add_development_dependency "rspec", "~> 3.0" 45 | spec.add_development_dependency "webmock", "~> 3.0" 46 | spec.add_development_dependency "simplecov", "~> 0.22.0" 47 | spec.add_development_dependency "simplecov-cobertura", "~> 2.1" 48 | spec.add_development_dependency "rubocop", "~> 1.0" 49 | spec.add_development_dependency "yard", "~> 0.9" 50 | spec.add_development_dependency "pry", "~> 0.14" 51 | end -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all community spaces, and also applies when 49 | an individual is officially representing the community in public spaces. 50 | 51 | ## Enforcement 52 | 53 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 54 | reported to the -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # Deepseek Ruby SDK API Reference 2 | 3 | ## Table of Contents 4 | 5 | - [Client](#client) 6 | - [Configuration](#configuration) 7 | - [Methods](#methods) 8 | - [Error Handling](#error-handling) 9 | - [Response Format](#response-format) 10 | 11 | ## Client 12 | 13 | ### Configuration 14 | 15 | The `Deepseek::Client` can be configured with the following options: 16 | 17 | ```ruby 18 | client = Deepseek::Client.new( 19 | api_key: 'your-api-key', # Required 20 | timeout: 30, # Optional (default: 30) 21 | max_retries: 3, # Optional (default: 3) 22 | api_base_url: 'custom_url' # Optional (default: https://api.deepseek.com) 23 | ) 24 | ``` 25 | 26 | #### Environment Variables 27 | 28 | All configuration options can be set via environment variables: 29 | 30 | ```bash 31 | DEEPSEEK_API_KEY=your-api-key 32 | DEEPSEEK_TIMEOUT=30 33 | DEEPSEEK_MAX_RETRIES=3 34 | DEEPSEEK_API_BASE_URL=https://api.deepseek.com 35 | ``` 36 | 37 | ### Methods 38 | 39 | #### chat(messages:, model: 'deepseek-chat', **params) 40 | 41 | Make a chat completion request. 42 | 43 | ```ruby 44 | response = client.chat( 45 | messages: [ 46 | { role: 'user', content: 'Hello!' } 47 | ], 48 | model: 'deepseek-chat', # Optional 49 | temperature: 0.7 # Optional 50 | ) 51 | ``` 52 | 53 | Parameters: 54 | - `messages` (Array, required): Array of message objects 55 | - `model` (String, optional): Model to use, defaults to 'deepseek-chat' 56 | - `temperature` (Float, optional): Sampling temperature 57 | 58 | ## Error Handling 59 | 60 | The SDK provides several error classes for different types of errors: 61 | 62 | ### Deepseek::AuthenticationError 63 | 64 | Raised when there are authentication issues (e.g., invalid API key). 65 | 66 | ```ruby 67 | begin 68 | client.chat(messages: messages) 69 | rescue Deepseek::AuthenticationError => e 70 | puts "Authentication failed: #{e.message}" 71 | puts "Status code: #{e.code}" 72 | end 73 | ``` 74 | 75 | ### Deepseek::RateLimitError 76 | 77 | Raised when you've exceeded the API rate limits. 78 | 79 | ```ruby 80 | rescue Deepseek::RateLimitError => e 81 | puts "Rate limit exceeded: #{e.message}" 82 | puts "Retry after: #{e.response[:headers]['retry-after']}" 83 | end 84 | ``` 85 | 86 | ### Deepseek::InvalidRequestError 87 | 88 | Raised when the request is malformed or invalid. 89 | 90 | ```ruby 91 | rescue Deepseek::InvalidRequestError => e 92 | puts "Invalid request: #{e.message}" 93 | end 94 | ``` 95 | 96 | ### Deepseek::ServiceUnavailableError 97 | 98 | Raised when the API service is having issues. 99 | 100 | ```ruby 101 | rescue Deepseek::ServiceUnavailableError => e 102 | puts "Service unavailable: #{e.message}" 103 | end 104 | ``` 105 | 106 | ### Deepseek::APIError 107 | 108 | Generic error class for unexpected API errors. 109 | 110 | ```ruby 111 | rescue Deepseek::APIError => e 112 | puts "API error: #{e.message}" 113 | end 114 | ``` 115 | 116 | ## Response Format 117 | 118 | The API returns responses in the following format: 119 | 120 | ```ruby 121 | { 122 | "choices" => [{ 123 | "message" => { 124 | "content" => "Hello! How can I help you today?", 125 | "role" => "assistant" 126 | }, 127 | "finish_reason" => "stop" 128 | }], 129 | "created" => 1677649420, 130 | "id" => "chatcmpl-123", 131 | "model" => "deepseek-chat", 132 | "usage" => { 133 | "completion_tokens" => 17, 134 | "prompt_tokens" => 57, 135 | "total_tokens" => 74 136 | } 137 | } -------------------------------------------------------------------------------- /spec/deepseek/client_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Deepseek::Client do 2 | let(:api_key) { 'test-key' } 3 | let(:client) { described_class.new(api_key: api_key) } 4 | let(:base_url) { 'https://api.deepseek.com' } 5 | let(:messages) { [{ role: 'user', content: 'Hello' }] } 6 | let(:headers) do 7 | { 8 | 'Authorization' => "Bearer #{api_key}", 9 | 'Content-Type' => 'application/json', 10 | 'Accept' => 'application/json', 11 | 'User-Agent' => "deepseek-ruby/#{Deepseek::VERSION}" 12 | } 13 | end 14 | 15 | describe '#initialize' do 16 | before do 17 | ENV['DEEPSEEK_API_KEY'] = nil 18 | end 19 | 20 | it 'creates a client with an API key' do 21 | expect(client.config.api_key).to eq(api_key) 22 | end 23 | 24 | it 'raises an error without an API key' do 25 | expect { 26 | described_class.new 27 | }.to raise_error(Deepseek::ConfigurationError, 'API key must be set') 28 | end 29 | 30 | it 'accepts configuration options' do 31 | client = described_class.new(api_key: api_key, timeout: 60) 32 | expect(client.config.timeout).to eq(60) 33 | end 34 | end 35 | 36 | describe '#chat' do 37 | let(:endpoint) { "#{base_url}/v1/chat/completions" } 38 | let(:request_body) { { model: 'deepseek-chat', messages: messages } } 39 | 40 | context 'with successful response' do 41 | before do 42 | stub_request(:post, endpoint) 43 | .with( 44 | body: request_body, 45 | headers: headers 46 | ) 47 | .to_return( 48 | status: 200, 49 | headers: { 'Content-Type' => 'application/json' }, 50 | body: { choices: [{ message: { content: 'Hi there!' } }] }.to_json 51 | ) 52 | end 53 | 54 | it 'makes a successful API call' do 55 | response = client.chat(messages: messages) 56 | expect(response['choices'][0]['message']['content']).to eq('Hi there!') 57 | end 58 | end 59 | 60 | context 'with API errors' do 61 | it 'handles authentication errors' do 62 | stub_request(:post, endpoint) 63 | .with( 64 | body: request_body, 65 | headers: headers 66 | ) 67 | .to_return( 68 | status: 401, 69 | headers: { 'Content-Type' => 'application/json' }, 70 | body: { error: { message: 'Invalid API key' } }.to_json 71 | ) 72 | 73 | expect { 74 | client.chat(messages: messages) 75 | }.to raise_error(Deepseek::AuthenticationError, /Invalid API key/) 76 | end 77 | 78 | it 'handles rate limit errors' do 79 | stub_request(:post, endpoint) 80 | .with( 81 | body: request_body, 82 | headers: headers 83 | ) 84 | .to_return( 85 | status: 429, 86 | headers: { 'Content-Type' => 'application/json' }, 87 | body: { error: { message: 'Rate limit exceeded' } }.to_json 88 | ) 89 | 90 | expect { 91 | client.chat(messages: messages) 92 | }.to raise_error(Deepseek::RateLimitError, /Rate limit exceeded/) 93 | end 94 | 95 | it 'handles invalid requests' do 96 | stub_request(:post, endpoint) 97 | .with( 98 | body: request_body, 99 | headers: headers 100 | ) 101 | .to_return( 102 | status: 400, 103 | headers: { 'Content-Type' => 'application/json' }, 104 | body: { error: { message: 'Invalid request parameters' } }.to_json 105 | ) 106 | 107 | expect { 108 | client.chat(messages: messages) 109 | }.to raise_error(Deepseek::InvalidRequestError, /Invalid request parameters/) 110 | end 111 | end 112 | end 113 | end -------------------------------------------------------------------------------- /lib/deepseek/client.rb: -------------------------------------------------------------------------------- 1 | require 'faraday' 2 | require 'faraday/retry' 3 | require 'json' 4 | 5 | module Deepseek 6 | class Client 7 | attr_reader :config 8 | 9 | def initialize(api_key: nil, **options) 10 | @config = Configuration.new 11 | @config.api_key = api_key if api_key 12 | options.each do |key, value| 13 | @config.send("#{key}=", value) if @config.respond_to?("#{key}=") 14 | end 15 | @config.validate! # This will raise ConfigurationError if no API key is set 16 | end 17 | 18 | def chat(messages:, model: 'deepseek-chat', **params) 19 | post('/v1/chat/completions', { 20 | model: model, 21 | messages: messages, 22 | **params 23 | }) 24 | rescue Faraday::Error => e 25 | handle_error_response(e.response) if e.response 26 | raise e 27 | end 28 | 29 | private 30 | 31 | def connection 32 | @connection ||= Faraday.new(url: config.api_base_url) do |faraday| 33 | faraday.request :json 34 | 35 | faraday.request :retry, { 36 | max: config.max_retries, 37 | interval: 0.5, 38 | interval_randomness: 0.5, 39 | backoff_factor: 2, 40 | exceptions: [ 41 | Faraday::ConnectionFailed, 42 | Faraday::TimeoutError, 43 | 'Timeout::Error', 44 | 'Error::TimeoutError' 45 | ] 46 | } 47 | 48 | # Don't automatically raise errors, we want to handle them ourselves 49 | faraday.response :raise_error 50 | 51 | faraday.adapter Faraday.default_adapter 52 | faraday.options.timeout = config.timeout 53 | end 54 | end 55 | 56 | def handle_error_response(response) 57 | status = response[:status] 58 | error_info = parse_error_response(response[:body]) 59 | 60 | case status 61 | when 401 62 | raise AuthenticationError.new(error_info[:message], code: status, response: response) 63 | when 429 64 | raise RateLimitError.new(error_info[:message], code: status, response: response) 65 | when 400, 404 66 | raise InvalidRequestError.new(error_info[:message], code: status, response: response) 67 | when 500..599 68 | raise ServiceUnavailableError.new("Service error: #{status}", code: status, response: response) 69 | else 70 | raise APIError.new("Unknown error: #{status}", code: status, response: response) 71 | end 72 | end 73 | 74 | def parse_error_response(body) 75 | return { message: body } unless body.is_a?(String) 76 | 77 | parsed = JSON.parse(body, symbolize_names: true) 78 | { 79 | message: parsed.dig(:error, :message) || parsed[:message] || "Unknown error", 80 | code: parsed.dig(:error, :code) || parsed[:code] 81 | } 82 | rescue JSON::ParserError 83 | { message: body } 84 | end 85 | 86 | def request_headers 87 | { 88 | 'Authorization' => "Bearer #{config.api_key}", 89 | 'Content-Type' => 'application/json', 90 | 'Accept' => 'application/json', 91 | 'User-Agent' => "deepseek-ruby/#{VERSION}" 92 | } 93 | end 94 | 95 | def post(endpoint, body = {}) 96 | response = connection.post(endpoint) do |req| 97 | req.headers = request_headers 98 | req.body = body 99 | end 100 | 101 | case response.status 102 | when 200..299 103 | JSON.parse(response.body) 104 | else 105 | handle_error_response( 106 | status: response.status, 107 | body: response.body, 108 | headers: response.headers, 109 | request: { 110 | method: 'POST', 111 | url: response.env.url.to_s, 112 | headers: response.env.request_headers, 113 | body: body 114 | } 115 | ) 116 | end 117 | rescue JSON::ParserError 118 | raise APIError.new("Invalid JSON response", response: response) 119 | end 120 | end 121 | end -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Deepseek Ruby SDK 2 | 3 | First off, thanks for taking the time to contribute! 🎉 👍 4 | 5 | The following is a set of guidelines for contributing to the Deepseek Ruby SDK. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. 6 | 7 | ## Table of Contents 8 | 9 | - [Code of Conduct](#code-of-conduct) 10 | - [How Can I Contribute?](#how-can-i-contribute) 11 | - [Reporting Bugs](#reporting-bugs) 12 | - [Suggesting Enhancements](#suggesting-enhancements) 13 | - [Your First Code Contribution](#your-first-code-contribution) 14 | - [Pull Requests](#pull-requests) 15 | - [Development Process](#development-process) 16 | - [Set Up Your Environment](#set-up-your-environment) 17 | - [Running Tests](#running-tests) 18 | - [Coding Standards](#coding-standards) 19 | - [Commit Messages](#commit-messages) 20 | 21 | ## Code of Conduct 22 | 23 | This project and everyone participating in it is governed by our [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. 24 | 25 | ## How Can I Contribute? 26 | 27 | ### Reporting Bugs 28 | 29 | Before creating bug reports, please check the issue list as you might find out that you don't need to create one. When you are creating a bug report, please include as many details as possible: 30 | 31 | * Use a clear and descriptive title 32 | * Describe the exact steps which reproduce the problem 33 | * Provide specific examples to demonstrate the steps 34 | * Describe the behavior you observed after following the steps 35 | * Explain which behavior you expected to see instead and why 36 | * Include details about your configuration and environment 37 | 38 | ### Suggesting Enhancements 39 | 40 | Enhancement suggestions are tracked as GitHub issues. When creating an enhancement suggestion, please include: 41 | 42 | * Use a clear and descriptive title 43 | * Provide a step-by-step description of the suggested enhancement 44 | * Provide specific examples to demonstrate the steps 45 | * Describe the current behavior and explain which behavior you expected to see instead 46 | * Explain why this enhancement would be useful 47 | 48 | ### Your First Code Contribution 49 | 50 | Unsure where to begin contributing? You can start by looking through these issues: 51 | 52 | * `beginner` - issues which should only require a few lines of code 53 | * `help-wanted` - issues which should be a bit more involved than `beginner` issues 54 | * `documentation` - issues related to improving documentation 55 | 56 | ### Pull Requests 57 | 58 | * Fill in the required template 59 | * Do not include issue numbers in the PR title 60 | * Follow the Ruby styleguides 61 | * Include thoughtfully-worded, well-structured tests 62 | * Update documentation for any changed functionality 63 | * End all files with a newline 64 | 65 | ## Development Process 66 | 67 | ### Set Up Your Environment 68 | 69 | 1. Fork the repo 70 | 2. Clone your fork 71 | 3. Set up development environment: 72 | 73 | ```bash 74 | bin/setup 75 | ``` 76 | 77 | ### Running Tests 78 | 79 | ```bash 80 | # Run all tests 81 | bundle exec rake spec 82 | 83 | # Run specific test file 84 | bundle exec rspec spec/path/to/test_file.rb 85 | 86 | # Run with specific line number 87 | bundle exec rspec spec/path/to/test_file.rb:123 88 | ``` 89 | 90 | ### Coding Standards 91 | 92 | * Use 2 spaces for indentation 93 | * Use snake_case for methods and variables 94 | * Use CamelCase for classes and modules 95 | * Keep lines under 100 characters 96 | * Write descriptive commit messages 97 | * Add tests for new code 98 | * Update documentation 99 | 100 | ## Commit Messages 101 | 102 | We follow the [Conventional Commits](https://www.conventionalcommits.org/) specification: 103 | 104 | ``` 105 | type(scope): description 106 | 107 | [optional body] 108 | 109 | [optional footer] 110 | ``` 111 | 112 | Types: 113 | * feat: A new feature 114 | * fix: A bug fix 115 | * docs: Documentation only changes 116 | * style: Changes that do not affect the meaning of the code 117 | * refactor: A code change that neither fixes a bug nor adds a feature 118 | * perf: A code change that improves performance 119 | * test: Adding missing tests or correcting existing tests 120 | * chore: Changes to the build process or auxiliary tools 121 | 122 | Example: 123 | ``` 124 | feat(client): add retry mechanism for failed requests 125 | 126 | Added exponential backoff retry mechanism for failed API requests. 127 | This helps handle temporary network issues and rate limits. 128 | 129 | Closes #123 130 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deepseek Ruby SDK 2 | 3 | 4 | Gem Version 5 | 6 | License 7 | Maintainability 8 | Test Coverage 9 | CI 10 | GitHub stars 11 | 12 | A Ruby SDK for interacting with the Deepseek AI API. This SDK provides a simple and intuitive interface for making API calls, handling responses, and managing errors. 13 | 14 | ## Features 15 | 16 | - 🚀 Simple and intuitive interface 17 | - ⚡️ Automatic retries with exponential backoff 18 | - 🛡️ Comprehensive error handling 19 | - ⚙️ Flexible configuration options 20 | - 🔒 Secure API key management 21 | - 📝 Detailed response handling 22 | 23 | ## Table of Contents 24 | 25 | - [Installation](#installation) 26 | - [Quick Start](#quick-start) 27 | - [Configuration](#configuration) 28 | - [API Reference](#api-reference) 29 | - [Usage Examples](#usage-examples) 30 | - [Error Handling](#error-handling) 31 | - [Retry Handling](#retry-handling) 32 | - [Development](#development) 33 | - [Contributing](#contributing) 34 | - [Support](#support) 35 | - [License](#license) 36 | 37 | ## Installation 38 | 39 | Add this line to your application's Gemfile: 40 | 41 | ```ruby 42 | gem 'deepseek-client' 43 | ``` 44 | 45 | And then execute: 46 | 47 | ```bash 48 | $ bundle install 49 | ``` 50 | 51 | Or install it yourself as: 52 | 53 | ```bash 54 | $ gem install deepseek-client 55 | ``` 56 | 57 | ## Quick Start 58 | 59 | ```ruby 60 | require 'deepseek' 61 | 62 | # Initialize client 63 | client = Deepseek::Client.new(api_key: 'your-api-key') 64 | 65 | # Make a chat completion request 66 | response = client.chat( 67 | messages: [ 68 | { role: 'user', content: 'What is artificial intelligence?' } 69 | ], 70 | model: 'deepseek-chat' 71 | ) 72 | 73 | puts response['choices'][0]['message']['content'] 74 | ``` 75 | 76 | ## Configuration 77 | 78 | ### Basic Configuration 79 | 80 | ```ruby 81 | client = Deepseek::Client.new( 82 | api_key: 'your-api-key', 83 | timeout: 60, # Custom timeout in seconds 84 | max_retries: 3 # Number of retries for failed requests 85 | ) 86 | ``` 87 | 88 | ### Environment Variables 89 | 90 | The SDK supports configuration through environment variables: 91 | 92 | ```bash 93 | DEEPSEEK_API_KEY=your-api-key 94 | DEEPSEEK_API_BASE_URL=https://api.deepseek.com # Optional 95 | DEEPSEEK_TIMEOUT=30 # Optional 96 | DEEPSEEK_MAX_RETRIES=3 # Optional 97 | ``` 98 | 99 | ## API Reference 100 | 101 | ### Available Methods 102 | 103 | #### chat(messages:, model: 'deepseek-chat', **params) 104 | 105 | Make a chat completion request. 106 | 107 | Parameters: 108 | - `messages` (Array, required): Array of message objects 109 | - `model` (String, optional): Model to use, defaults to 'deepseek-chat' 110 | - `temperature` (Float, optional): Sampling temperature 111 | 112 | Response Format: 113 | ```ruby 114 | { 115 | "choices" => [{ 116 | "message" => { 117 | "content" => "Hello! How can I help you today?", 118 | "role" => "assistant" 119 | }, 120 | "finish_reason" => "stop" 121 | }], 122 | "created" => 1677649420, 123 | "id" => "chatcmpl-123", 124 | "model" => "deepseek-chat", 125 | "usage" => { 126 | "completion_tokens" => 17, 127 | "prompt_tokens" => 57, 128 | "total_tokens" => 74 129 | } 130 | } 131 | ``` 132 | 133 | ## Usage Examples 134 | 135 | ### Chat with System Message 136 | ```ruby 137 | response = client.chat( 138 | messages: [ 139 | { role: 'system', content: 'You are a friendly AI assistant.' }, 140 | { role: 'user', content: 'Hello!' } 141 | ], 142 | temperature: 0.7, 143 | model: 'deepseek-chat' 144 | ) 145 | ``` 146 | 147 | ### Conversation with History 148 | ```ruby 149 | conversation = [ 150 | { role: 'user', content: 'What is your favorite color?' }, 151 | { role: 'assistant', content: 'I don\'t have personal preferences, but I can discuss colors!' }, 152 | { role: 'user', content: 'Tell me about blue.' } 153 | ] 154 | 155 | response = client.chat( 156 | messages: conversation, 157 | temperature: 0.8 158 | ) 159 | ``` 160 | 161 | ### Advanced Configuration Example 162 | ```ruby 163 | client = Deepseek::Client.new( 164 | api_key: ENV['DEEPSEEK_API_KEY'], 165 | timeout: 60, # Custom timeout 166 | max_retries: 5, # Custom retry limit 167 | api_base_url: 'https://custom.deepseek.api.com' # Custom API URL 168 | ) 169 | ``` 170 | 171 | ## Error Handling 172 | 173 | The SDK provides comprehensive error handling for various scenarios: 174 | 175 | ```ruby 176 | begin 177 | response = client.chat(messages: messages) 178 | rescue Deepseek::AuthenticationError => e 179 | # Handle authentication errors (e.g., invalid API key) 180 | puts "Authentication error: #{e.message}" 181 | rescue Deepseek::RateLimitError => e 182 | # Handle rate limit errors 183 | puts "Rate limit exceeded: #{e.message}" 184 | rescue Deepseek::InvalidRequestError => e 185 | # Handle invalid request errors 186 | puts "Invalid request: #{e.message}" 187 | rescue Deepseek::ServiceUnavailableError => e 188 | # Handle API service errors 189 | puts "Service error: #{e.message}" 190 | rescue Deepseek::APIError => e 191 | # Handle other API errors 192 | puts "API error: #{e.message}" 193 | end 194 | ``` 195 | 196 | ## Retry Handling 197 | 198 | The SDK automatically handles retries with exponential backoff for failed requests: 199 | 200 | - Automatic retry on network failures 201 | - Exponential backoff strategy 202 | - Configurable max retry attempts 203 | - Retry on rate limits and server errors 204 | 205 | ## Development 206 | 207 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 208 | 209 | ### Running Tests 210 | 211 | ```bash 212 | bundle exec rake spec 213 | ``` 214 | 215 | ### Running the Console 216 | 217 | ```bash 218 | bin/console 219 | ``` 220 | 221 | ## Contributing 222 | 223 | Bug reports and pull requests are welcome on GitHub at https://github.com/nagstler/deepseek-ruby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](CODE_OF_CONDUCT.md). 224 | 225 | For detailed contribution guidelines, please see our [Contributing Guide](CONTRIBUTING.md). 226 | 227 | ## Support 228 | 229 | If you discover any issues or have questions, please create an issue on GitHub. 230 | 231 | ## License 232 | 233 | The gem is available as open source under the terms of the MIT License. See [LICENSE.txt](LICENSE.txt) for details. 234 | 235 | ## Code of Conduct 236 | 237 | Everyone interacting in the Deepseek project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](CODE_OF_CONDUCT.md). --------------------------------------------------------------------------------