Skip to content
Open
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
155 changes: 155 additions & 0 deletions lib/instructor/adapters/deepseek.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
defmodule Instructor.Adapters.DeepSeek do
@moduledoc """
DeepSeek Adapter for Instructor.

## Configuration

```elixir
config :instructor, adapter: Instructor.Adapters.DeepSeek, deepseek: [
api_key: "your_api_key" # Will use DEEPSEEK_API_KEY environment variable if not provided
]
```

or at runtime:

```elixir
Instructor.chat_completion(..., [
adapter: Instructor.Adapters.DeepSeek,
api_key: "your_api_key" # Will use DEEPSEEK_API_KEY environment variable if not provided
])
```

To get an DeepSeek API key, see [DeepSeek](https://platform.deepseek.com/api_keys).
"""

@behaviour Instructor.Adapter
alias Instructor.Adapters
alias Instructor.SSEStreamParser

@supported_modes [:json_schema]

@impl true
def chat_completion(params, user_config \\ nil) do
config = config(user_config)

# Peel off instructor only parameters
{_, params} = Keyword.pop(params, :response_model)
{_, params} = Keyword.pop(params, :validation_context)
{_, params} = Keyword.pop(params, :max_retries)
{mode, params} = Keyword.pop(params, :mode)
{response_format, params} = Keyword.pop(params, :response_format)
stream = Keyword.get(params, :stream, false)
params = Enum.into(params, %{})

if mode not in @supported_modes do
raise "Unsupported DeepSeek mode #{mode}. Supported modes: #{inspect(@supported_modes)}"
end

params =
if mode == :json_schema do
Map.put(params, :response_format, %{type: "json_object"})
else
if response_format do
Map.put(params, :response_format, response_format)
else
params
end
end

if stream do
do_streaming_chat_completion(mode, params, config)
else
do_chat_completion(mode, params, config)
end
end

defp do_chat_completion(mode, params, config) do
response =
Req.new()
|> Req.post(
url: url(config),
headers: %{"Authorization" => "Bearer " <> api_key(config)},
json: params
)

with {:ok, %Req.Response{status: 200, body: body} = response} <- response,
{:ok, body} <- parse_response_for_mode(mode, body) do
{:ok, response, body}
else
{:ok, %Req.Response{status: status, body: body}} ->
{:error, "Unexpected HTTP response code: #{status}\n#{inspect(body)}"}

e ->
e
end
end

defp do_streaming_chat_completion(mode, params, config) do
pid = self()

Stream.resource(
fn ->
Task.async(fn ->
options = [
url: url(config),
headers: %{"Authorization" => "Bearer " <> api_key(config)},
json: params,
into: fn {:data, data}, {req, resp} ->
send(pid, data)
{:cont, {req, resp}}
end
]

Req.post!(options)
send(pid, :done)
end)
end,
fn task ->
receive do
:done ->
{:halt, task}

data ->
{[data], task}
after
15_000 ->
{:halt, task}
end
end,
fn task -> Task.await(task) end
)
|> SSEStreamParser.parse()
|> Stream.map(fn chunk -> parse_stream_chunk_for_mode(mode, chunk) end)
end

defp parse_response_for_mode(:json_schema, %{
"choices" => [%{"message" => %{"content" => text}}]
}) do
Jason.decode(text)
end

defp parse_stream_chunk_for_mode(:json_schema, %{
"choices" => [%{"delta" => %{"content" => chunk}}]
}) do
chunk
end

@impl true
defdelegate reask_messages(raw_response, params, config), to: Adapters.OpenAI

defp url(config), do: api_url(config) <> "/chat/completions"
defp api_url(config), do: Keyword.fetch!(config, :api_url)
defp api_key(config), do: Keyword.fetch!(config, :api_key)

defp config(nil), do: config(Application.get_env(:instructor, :deepseek, []))

defp config(base_config) do
default_config = [
api_url: "https://api.deepseek.com",
api_key: System.get_env("DEEPSEEK_API_KEY"),
http_options: [receive_timeout: 60_000]
]

Keyword.merge(default_config, base_config)
end
end
2 changes: 1 addition & 1 deletion lib/instructor/adapters/gemini.ex
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ defmodule Instructor.Adapters.Gemini do
raise "Unsupported Gemini mode #{mode}. Supported modes: #{inspect(@supported_modes)}"
end

# Format the messages into the correct format for Geminic
# Format the messages into the correct format for Gemini
{messages, params} = Map.pop!(params, :messages)

{system_instruction, contents} =
Expand Down
5 changes: 5 additions & 0 deletions test/instructor_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ defmodule InstructorTest do
Application.put_env(:instructor, :adapter, Instructor.Adapters.XAI)
Application.put_env(:instructor, :xai, api_key: System.fetch_env!("XAI_API_KEY"))

:deep_seek ->
Application.put_env(:instructor, :adapter, Instructor.Adapters.DeepSeek)
Application.put_env(:instructor, :xai, api_key: System.fetch_env!("DEEPSEEK_API_KEY"))

:openai ->
Application.put_env(:instructor, :adapter, Instructor.Adapters.OpenAI)
Application.put_env(:instructor, :openai, api_key: System.fetch_env!("OPENAI_API_KEY"))
Expand Down Expand Up @@ -68,6 +72,7 @@ defmodule InstructorTest do
{:anthropic, [mode: :tools, model: "claude-3-5-haiku-latest", max_tokens: 1024]},
{:xai, [mode: :tools, model: "grok-2-latest"]},
{:xai, [mode: :json_schema, model: "grok-2-latest"]},
{:deep_seek, [mode: :json_schema, model: "deepseek-chat"]},
{:ollama, [mode: :tools, model: "qwen2.5:7b"]},
{:llamacpp, [mode: :json_schema, model: "qwen2.5:7b"]}
] do
Expand Down
1 change: 1 addition & 0 deletions test/test_helper.exs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ ExUnit.configure(
adapter: :anthropic,
adapter: :gemini,
adapter: :xai,
adapter: :deep_seek,
adapter: :llamacpp,
adapter: :ollama
]
Expand Down