llm-simple-0.1.0.1: src/LLM/Providers/DeepSeek.hs
module LLM.Providers.DeepSeek
( deepSeekGateway,
deepSeekGatewayWith,
deepSeekProvider,
deepSeekProviderWith,
deepSeekBuildBodyPairs,
)
where
import Data.Aeson
( KeyValue ((.=)),
Value,
decodeStrict',
encode,
object,
withObject,
(.:),
)
import Data.Aeson.Types (Pair, Parser, parseMaybe)
import Data.Text (Text)
import Data.Text.Encoding (encodeUtf8)
import Data.Text.Lazy qualified as TL
import Data.Text.Lazy.Encoding qualified as TLE
import LLM.Core.LLMProvider (LLMProvider (..), toGateway)
import LLM.Core.ProviderUtils (handleStreamResponse, lenientConfig, stripJsonFences)
import LLM.Core.Types
( ChatRequest
( reqConversation,
reqMaxTokens,
reqModel,
reqTemperature,
reqThinking,
reqTools
),
LLMError (EmptyResponse),
LLMGateway,
ThinkingMode (..),
Turn (UserTurn),
deepSeekMessageEncodeOptions,
)
import LLM.Providers.OpenAI
( authHeader,
buildMessages,
encodeToolDef,
parseOpenAIResponse,
parseOpenAIStream,
parseOpenAIUsage,
)
import Network.HTTP.Req
( Option,
POST (POST),
ReqBodyJson (ReqBodyJson),
Url,
https,
jsonResponse,
req,
reqBr,
responseBody,
responseStatusCode,
runReq,
(/:),
)
-- | Create a LLMGateway for the DeepSeek provider. Takes the API key as a parameter.
deepSeekGateway :: Text -> LLMGateway
deepSeekGateway apiKey = toGateway $ deepSeekProvider apiKey
-- | Create a LLMGateway for the DeepSeek provider with a custom base URL. Takes the API key and base URL as parameters.
deepSeekGatewayWith :: Url scheme -> Option scheme -> Text -> LLMGateway
deepSeekGatewayWith baseUrl baseOpts apiKey = toGateway (deepSeekProviderWith baseUrl baseOpts apiKey)
-- | DeepSeek provider at api.deepseek.com.
deepSeekProvider :: Text -> LLMProvider
deepSeekProvider = deepSeekProviderWith (https "api.deepseek.com") mempty
-- | DeepSeek-compatible provider with a custom base URL.
deepSeekProviderWith :: Url scheme -> Option scheme -> Text -> LLMProvider
deepSeekProviderWith baseUrl baseOpts apiKey =
LLMProvider
{ providerName = "deepseek",
buildBody = deepSeekBuildBody,
sendRequest = sendRequest,
sendStreamRequest = \body callback ->
runReq lenientConfig $ do
let url = baseUrl /: "v1" /: "chat" /: "completions"
opts = baseOpts <> authHeader apiKey
reqBr POST url (ReqBodyJson body) opts $ \resp ->
handleStreamResponse resp (`parseOpenAIStream` callback),
parseResponse = pure . parseOpenAIResponse,
buildObjectBody = \r schema ->
let schemaText = TL.toStrict . TLE.decodeUtf8 $ encode schema
instruction =
"Respond with a raw JSON object matching this JSON schema. "
<> "No markdown, no code fences, no explanation:\n"
<> schemaText
r' = r {reqConversation = r.reqConversation <> [UserTurn instruction]}
in object $
deepSeekBuildBodyPairs False r'
<> ["response_format" .= object ["type" .= ("json_object" :: Text)]],
sendObjectRequest = sendRequest,
parseObjectResponse = \v -> case parseMaybe parseObject v of
Nothing -> pure $ Left EmptyResponse
Just contentStr -> case decodeStrict' (encodeUtf8 (stripJsonFences contentStr)) of
Nothing -> pure $ Left EmptyResponse
Just obj -> pure $ Right (obj, parseOpenAIUsage v)
}
where
sendRequest body =
runReq lenientConfig $ do
let url = baseUrl /: "v1" /: "chat" /: "completions"
opts = baseOpts <> authHeader apiKey
resp <- req POST url (ReqBodyJson body) jsonResponse opts
pure (responseStatusCode resp, responseBody resp)
parseObject :: Value -> Parser Text
parseObject = withObject "OpenAIObjectResponse" $ \o -> do
(choice : _) <- o .: "choices" :: Parser [Value]
withObject "choice" (\co -> co .: "message" >>= withObject "message" (.: "content")) choice
-- | OpenAI-compatible body; DeepSeek uses @max_tokens@ (not @max_completion_tokens@).
deepSeekBuildBody :: Bool -> ChatRequest -> Value
deepSeekBuildBody stream r = object $ deepSeekBuildBodyPairs stream r
deepSeekBuildBodyPairs :: Bool -> ChatRequest -> [Pair]
deepSeekBuildBodyPairs stream r =
[ "model" .= r.reqModel,
"max_tokens" .= r.reqMaxTokens,
"messages" .= buildMessages deepSeekMessageEncodeOptions r
]
++ thinkingPairs r
++ ["temperature" .= t | Just t <- [r.reqTemperature]]
++ ["tools" .= map encodeToolDef r.reqTools | not (null r.reqTools)]
++ ["stream" .= True | stream]
++ ["stream_options" .= object ["include_usage" .= True] | stream]
thinkingPairs :: ChatRequest -> [Pair]
thinkingPairs r =
case r.reqThinking of
Just tm
| not tm.tmEnabled ->
["thinking" .= object ["type" .= ("disabled" :: Text)]]
Just tm ->
("thinking" .= object ["type" .= ("enabled" :: Text)]) : ["reasoning_effort" .= e | Just e <- [tm.tmEffort]]
Nothing ->
["thinking" .= object ["type" .= ("disabled" :: Text)]]