never executed always true always false
1 module PureClaw.Providers.OpenRouter
2 ( -- * Provider type
3 OpenRouterProvider
4 , mkOpenRouterProvider
5 -- * Errors
6 , OpenRouterError (..)
7 -- * Request/response encoding (exported for testing)
8 , encodeRequest
9 , decodeResponse
10 ) where
11
12 import Control.Exception
13 import Data.ByteString (ByteString)
14 import Data.ByteString.Lazy qualified as BL
15 import Data.Text (Text)
16 import Data.Text qualified as T
17 import Network.HTTP.Client qualified as HTTP
18 import Network.HTTP.Types.Status qualified as Status
19
20 import PureClaw.Core.Errors
21 import PureClaw.Providers.Class
22 import PureClaw.Providers.OpenAI qualified as OAI
23 import PureClaw.Security.Secrets
24
25 -- | OpenRouter provider. Uses the OpenAI-compatible API with a
26 -- different base URL and authentication header.
27 data OpenRouterProvider = OpenRouterProvider
28 { _or_manager :: HTTP.Manager
29 , _or_apiKey :: ApiKey
30 }
31
32 -- | Create an OpenRouter provider.
33 mkOpenRouterProvider :: HTTP.Manager -> ApiKey -> OpenRouterProvider
34 mkOpenRouterProvider = OpenRouterProvider
35
36 instance Provider OpenRouterProvider where
37 complete = openRouterComplete
38
39 -- | Errors from the OpenRouter API.
40 data OpenRouterError
41 = OpenRouterAPIError Int ByteString
42 | OpenRouterParseError Text
43 deriving stock (Show)
44
45 instance Exception OpenRouterError
46
47 instance ToPublicError OpenRouterError where
48 toPublicError (OpenRouterAPIError 429 _) = RateLimitError
49 toPublicError (OpenRouterAPIError 401 _) = NotAllowedError
50 toPublicError _ = TemporaryError "Provider error"
51
52 openRouterBaseUrl :: String
53 openRouterBaseUrl = "https://openrouter.ai/api/v1/chat/completions"
54
55 openRouterComplete :: OpenRouterProvider -> CompletionRequest -> IO CompletionResponse
56 openRouterComplete provider req = do
57 initReq <- HTTP.parseRequest openRouterBaseUrl
58 let httpReq = initReq
59 { HTTP.method = "POST"
60 , HTTP.requestBody = HTTP.RequestBodyLBS (encodeRequest req)
61 , HTTP.requestHeaders =
62 [ ("Authorization", "Bearer " <> withApiKey (_or_apiKey provider) id)
63 , ("content-type", "application/json")
64 , ("HTTP-Referer", "https://github.com/pureclaw/pureclaw")
65 , ("X-Title", "PureClaw")
66 ]
67 }
68 resp <- HTTP.httpLbs httpReq (_or_manager provider)
69 let status = Status.statusCode (HTTP.responseStatus resp)
70 if status /= 200
71 then throwIO (OpenRouterAPIError status (BL.toStrict (HTTP.responseBody resp)))
72 else case decodeResponse (HTTP.responseBody resp) of
73 Left err -> throwIO (OpenRouterParseError (T.pack err))
74 Right response -> pure response
75
76 -- | Encode request — reuses OpenAI format.
77 encodeRequest :: CompletionRequest -> BL.ByteString
78 encodeRequest = OAI.encodeRequest
79
80 -- | Decode response — reuses OpenAI format.
81 decodeResponse :: BL.ByteString -> Either String CompletionResponse
82 decodeResponse = OAI.decodeResponse