├── 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 |
5 |
6 |
7 |
8 |
9 |
10 |
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).
--------------------------------------------------------------------------------