bloodhound-1.0.0.0: tests/Test/InferenceSpec.hs
{-# LANGUAGE OverloadedStrings #-}
module Test.InferenceSpec (spec) where
import Data.Aeson
import Data.Aeson.Key (Key)
import Data.Aeson.KeyMap qualified as KM
import Data.ByteString.Lazy.Char8 qualified as LBS
import Data.Map.Strict qualified as Map
import Data.Maybe (isJust, isNothing)
import Database.Bloodhound.Client.Cluster (BackendType (..))
import Database.Bloodhound.ElasticSearch8.Client qualified as ClientES8
import Database.Bloodhound.ElasticSearch8.Requests qualified as RequestsES8
import Database.Bloodhound.ElasticSearch8.Types qualified as Types
import Database.Bloodhound.ElasticSearch9.Client qualified as ClientES9
import Database.Bloodhound.ElasticSearch9.Requests qualified as RequestsES9
import TestsUtils.Common
import TestsUtils.Import
hasKey :: Key -> LBS.ByteString -> Bool
hasKey k bs = case decode bs of
Just (Object obj) -> k `KM.member` obj
_ -> False
-- | Decode a @GET /_inference@ response body into a list of
-- 'Types.InferenceInfo'. The wire format is a JSON object keyed by
-- @<task_type>\/<inference_id>@; 'Types.InferenceEndpointsResponse'
-- flattens it on decode.
decodeInferenceList :: LBS.ByteString -> Maybe [Types.InferenceInfo]
decodeInferenceList =
fmap Types.unInferenceEndpointsResponse . decode
spec :: Spec
spec = describe "Inference API (GET/PUT/POST/DELETE /_inference/{task_type}/{inference_id})" $ do
describe "TaskType JSON" $ do
it "serializes known task types to snake_case" $ do
Types.taskTypeToText Types.TextEmbeddingTaskType `shouldBe` "text_embedding"
Types.taskTypeToText Types.SparseEmbeddingTaskType `shouldBe` "sparse_embedding"
Types.taskTypeToText Types.ChatCompletionTaskType `shouldBe` "chat_completion"
Types.taskTypeToText Types.RerankTaskType `shouldBe` "rerank"
it "round-trips every known task type" $ do
mapM_
(\tt -> decode (encode tt) `shouldBe` Just tt)
[ minBound
.. maxBound :: Types.TaskType
]
describe "Similarity JSON" $ do
it "round-trips every valid similarity value" $ do
mapM_
(\s -> decode (encode s) `shouldBe` Just s)
[ Types.SimilarityCosine,
Types.SimilarityDotProduct,
Types.SimilarityL2Norm
]
it "rejects max_inner_product (not valid for any inference provider)" $ do
(decode "\"max_inner_product\"" :: Maybe Types.Similarity)
`shouldBe` Nothing
describe "ChatMessage JSON" $ do
it "round-trips a minimal message (role + content)" $ do
let msg = Types.ChatMessage "user" (Just (toJSON ("hello" :: Text))) Nothing Nothing Nothing Nothing
decode (encode msg) `shouldBe` Just msg
it "round-trips a message with tool_call_id and tool_calls" $ do
let msg =
Types.ChatMessage
{ Types.cmRole = "tool",
Types.cmContent = Nothing,
Types.cmToolCallId = Just "call_abc",
Types.cmToolCalls = Just (toJSON [object ["id" .= ("call_1" :: Text)]]),
Types.cmReasoning = Nothing,
Types.cmReasoningDetails = Nothing
}
decode (encode msg) `shouldBe` Just msg
it "round-trips a message with reasoning fields" $ do
let msg =
Types.ChatMessage
{ Types.cmRole = "assistant",
Types.cmContent = Just (toJSON ("thinking..." :: Text)),
Types.cmToolCallId = Nothing,
Types.cmToolCalls = Nothing,
Types.cmReasoning = Just (toJSON True),
Types.cmReasoningDetails = Just (toJSON [object ["type" .= ("summary" :: Text)]])
}
decode (encode msg) `shouldBe` Just msg
it "omits Nothing optional fields from JSON" $ do
let msg = Types.ChatMessage "system" (Just (toJSON ("hi" :: Text))) Nothing Nothing Nothing Nothing
let json = encode msg
json `shouldNotSatisfy` hasKey "tool_call_id"
json `shouldNotSatisfy` hasKey "tool_calls"
json `shouldNotSatisfy` hasKey "reasoning"
json `shouldNotSatisfy` hasKey "reasoning_details"
it "decodes a message without content" $ do
let raw = LBS.pack "{\"role\":\"assistant\",\"tool_calls\":[]}"
case decode raw :: Maybe Types.ChatMessage of
Just msg -> do
Types.cmRole msg `shouldBe` "assistant"
Types.cmContent msg `shouldBe` Nothing
Types.cmToolCalls msg `shouldSatisfy` isJust
Nothing -> expectationFailure "failed to decode ChatMessage"
describe "ChatCompletionRequest JSON" $ do
it "round-trips a minimal request (messages only)" $ do
let req =
Types.ChatCompletionRequest
{ Types.ccrMessages =
[Types.ChatMessage "user" (Just (toJSON ("hi" :: Text))) Nothing Nothing Nothing Nothing],
Types.ccrModel = Nothing,
Types.ccrMaxCompletionTokens = Nothing,
Types.ccrReasoning = Nothing,
Types.ccrStop = Nothing,
Types.ccrTemperature = Nothing,
Types.ccrToolChoice = Nothing,
Types.ccrTools = Nothing,
Types.ccrTopP = Nothing
}
decode (encode req) `shouldBe` Just req
it "round-trips a fully-populated request with top-level fields" $ do
let req =
Types.ChatCompletionRequest
{ Types.ccrMessages =
[Types.ChatMessage "user" (Just (toJSON ("hi" :: Text))) Nothing Nothing Nothing Nothing],
Types.ccrModel = Just "gpt-4o",
Types.ccrMaxCompletionTokens = Just 1024,
Types.ccrReasoning = Just (toJSON True),
Types.ccrStop = Just (toJSON ("STOP" :: Text)),
Types.ccrTemperature = Just 0.7,
Types.ccrToolChoice = Just (toJSON ("auto" :: Text)),
Types.ccrTools = Just (toJSON [object ["type" .= ("function" :: Text)]]),
Types.ccrTopP = Just 0.9
}
decode (encode req) `shouldBe` Just req
it "does not emit a task_settings wrapper" $ do
let req =
Types.ChatCompletionRequest
{ Types.ccrMessages =
[Types.ChatMessage "user" (Just (toJSON ("hi" :: Text))) Nothing Nothing Nothing Nothing],
Types.ccrModel = Just "gpt-4o",
Types.ccrMaxCompletionTokens = Just 128,
Types.ccrReasoning = Nothing,
Types.ccrStop = Nothing,
Types.ccrTemperature = Just 0.5,
Types.ccrToolChoice = Nothing,
Types.ccrTools = Nothing,
Types.ccrTopP = Nothing
}
let json = encode req
json `shouldNotSatisfy` hasKey "task_settings"
json `shouldSatisfy` hasKey "messages"
json `shouldSatisfy` hasKey "model"
json `shouldSatisfy` hasKey "max_completion_tokens"
json `shouldSatisfy` hasKey "temperature"
describe "optional api_key round-trip" $ do
it "round-trips an OpenAI config with api_key = Nothing" $ do
let cfg =
Types.InferenceConfig
{ Types.inferenceProvider =
Types.OpenAIProvider
(Types.OpenAIServiceSettings Nothing "text-embedding-3-small" Nothing Nothing Nothing Nothing Nothing)
Nothing,
Types.inferenceChunkingSettings = Nothing
}
let json = encode cfg
json `shouldNotSatisfy` hasKey "api_key"
(decode json :: Maybe Types.InferenceConfig) `shouldBe` Just cfg
it "round-trips a Cohere config with api_key = Nothing" $ do
let cfg =
Types.InferenceConfig
{ Types.inferenceProvider =
Types.CohereProvider
(Types.CohereServiceSettings Nothing "embed-v3" Nothing Nothing Nothing)
Nothing,
Types.inferenceChunkingSettings = Nothing
}
(decode (encode cfg) :: Maybe Types.InferenceConfig) `shouldBe` Just cfg
describe "InferenceConfig JSON" $ do
let minimalOpenAI =
Types.InferenceConfig
{ Types.inferenceProvider =
Types.OpenAIProvider
(Types.OpenAIServiceSettings (Just "sk-xxx") "text-embedding-3-small" Nothing Nothing Nothing Nothing Nothing)
Nothing,
Types.inferenceChunkingSettings = Nothing
}
it "emits service and service_settings for a minimal OpenAI config" $ do
let json = encode minimalOpenAI
json `shouldSatisfy` hasKey "service"
json `shouldSatisfy` hasKey "service_settings"
it "omits Nothing optional top-level fields" $ do
let json = encode minimalOpenAI
json `shouldNotSatisfy` hasKey "task_settings"
json `shouldNotSatisfy` hasKey "chunking_settings"
it "writes the OpenAI service name" $ do
let Just (Object o) = decode (encode minimalOpenAI)
KM.lookup "service" o `shouldBe` Just (String "openai")
it "round-trips a minimal OpenAI config" $ do
decode (encode minimalOpenAI) `shouldBe` Just minimalOpenAI
it "round-trips a fully-populated Cohere config" $ do
let cfg =
Types.InferenceConfig
{ Types.inferenceProvider =
Types.CohereProvider
( Types.CohereServiceSettings
{ Types.cssApiKey = Just "cohere-key",
Types.cssModelId = "embed-english-light-v3.0",
Types.cssEmbeddingType = Just Types.CohereEmbeddingByte,
Types.cssSimilarity = Just Types.SimilarityCosine,
Types.cssRateLimit = Just (Types.RateLimit 100)
}
)
( Just
Types.CohereTaskSettings
{ Types.ctsInputType = Just Types.CohereInputIngest,
Types.ctsReturnDocuments = Just True,
Types.ctsTopN = Just 10,
Types.ctsTruncate = Just Types.CohereTruncateEnd
}
),
Types.inferenceChunkingSettings = Nothing
}
decode (encode cfg) `shouldBe` Just cfg
it "round-trips an ELSER config" $ do
let cfg =
Types.InferenceConfig
{ Types.inferenceProvider =
Types.ElserProvider
Types.ElserServiceSettings
{ Types.esserNumAllocations = 1,
Types.esserNumThreads = 1,
Types.esserAdaptiveAllocations = Nothing
},
Types.inferenceChunkingSettings = Nothing
}
let Just (Object o) = decode (encode cfg)
KM.lookup "service" o `shouldBe` Just (String "elser")
decode (encode cfg) `shouldBe` Just cfg
it "round-trips the generic escape hatch" $ do
-- Use a service name not in the typed set so dispatch falls through
-- to GenericProvider on decode (typed names like "mistral" decode to
-- their typed constructor and would not round-trip via the generic).
let rawSettings = object ["model_id" .= ("foo" :: Text), "api_key" .= ("bar" :: Text)]
cfg =
Types.InferenceConfig
{ Types.inferenceProvider =
Types.GenericProvider "some-future-provider" rawSettings Nothing,
Types.inferenceChunkingSettings = Nothing
}
decode (encode cfg) `shouldBe` Just cfg
it "round-trips every typed provider and writes its service wire string" $ do
-- One minimal InferenceConfig per provider. Round-trips through
-- encode/decode and the emitted `service` field matches the wire name.
let rateLimit = Just (Types.RateLimit 100)
providers =
[ ( "openai",
Types.OpenAIProvider
(Types.OpenAIServiceSettings (Just "sk") "m" Nothing Nothing Nothing Nothing rateLimit)
Nothing
),
( "cohere",
Types.CohereProvider
(Types.CohereServiceSettings (Just "sk") "m" Nothing Nothing rateLimit)
Nothing
),
( "elser",
Types.ElserProvider $
Types.ElserServiceSettings 1 1 Nothing
),
( "ai21",
Types.Ai21Provider $
Types.Ai21ServiceSettings "m" (Just "sk") rateLimit
),
( "mistral",
Types.MistralProvider $
Types.MistralServiceSettings (Just "sk") "m" (Just 1024) rateLimit
),
( "anthropic",
Types.AnthropicProvider
(Types.AnthropicServiceSettings (Just "sk") "m" rateLimit)
(Just (Types.AnthropicTaskSettings 128 (Just 0.5) (Just 4) Nothing))
),
( "deepseek",
Types.DeepSeekProvider $
Types.DeepSeekServiceSettings (Just "sk") "m" (Just "https://api.deepseek.com")
),
( "nvidia",
Types.NvidiaProvider
(Types.NvidiaServiceSettings (Just "sk") "m" (Just 2048) rateLimit (Just Types.SimilarityCosine) Nothing)
(Just (Types.NvidiaTaskSettings (Just Types.NvidiaInputSearch) (Just Types.CohereTruncateEnd)))
),
( "contextualai",
Types.ContextualAiProvider
(Types.ContextualAiServiceSettings (Just "sk") "m" rateLimit)
(Just (Types.ContextualAiTaskSettings (Just "ctx") (Just 5)))
),
( "fireworksai",
Types.FireworksAiProvider
(Types.FireworksAiServiceSettings (Just "sk") "m" (Just 768) rateLimit (Just Types.SimilarityDotProduct) (Just "https://api.fireworks.ai"))
(Just (Types.FireworksAiTaskSettings (Just "u") Nothing))
),
( "googleaistudio",
Types.GoogleAiStudioProvider $
Types.GoogleAiStudioServiceSettings (Just "sk") "m" rateLimit
),
( "googlevertexai",
Types.GoogleVertexAiProvider
(Types.GoogleVertexAiServiceSettings "acct-json" (Just Types.GoogleModelGardenGoogle) Nothing Nothing (Just "us") (Just "m") (Just "proj") rateLimit (Just 768) (Just 10))
(Just (Types.GoogleVertexAiTaskSettings (Just True) (Just 1024) (Just (Types.ThinkingConfig (Just 64))) (Just 3)))
),
( "groq",
Types.GroqProvider $
Types.GroqServiceSettings "m" (Just "sk") rateLimit
),
( "hugging_face",
Types.HuggingFaceProvider
(Types.HuggingFaceServiceSettings (Just "sk") "https://api.hf.co" (Just "m") rateLimit)
(Just (Types.HuggingFaceTaskSettings (Just True) (Just 2)))
),
( "jinaai",
Types.JinaAiProvider
(Types.JinaAiServiceSettings (Just "sk") "m" (Just 512) (Just Types.JinaAiElementFloat) (Just False) rateLimit (Just Types.SimilarityL2Norm))
(Just (Types.JinaAiTaskSettings (Just Types.JinaAiInputSearch) (Just True) (Just False) (Just 3)))
),
( "llama",
Types.LlamaProvider $
Types.LlamaServiceSettings "https://llama" "m" (Just 512) rateLimit (Just Types.SimilarityCosine)
),
( "openshift_ai",
Types.OpenShiftAiProvider
(Types.OpenShiftAiServiceSettings (Just "sk") "https://osh" (Just 512) (Just "m") rateLimit (Just Types.SimilarityCosine))
(Just (Types.OpenShiftAiTaskSettings (Just True) (Just 2)))
),
( "voyageai",
Types.VoyageAiProvider
(Types.VoyageAiServiceSettings "m" (Just 512) (Just Types.JinaAiElementBit) rateLimit)
(Just (Types.VoyageAiTaskSettings (Just "search") (Just True) (Just 3) (Just False)))
),
( "elasticsearch",
Types.ElasticsearchProvider
(Types.ElasticsearchServiceSettings "m" 1 Nothing Nothing (Just 1) Nothing Nothing)
(Just (Types.ElasticsearchTaskSettings (Just True)))
),
( "alibabacloud-ai-search",
Types.AlibabaCloudProvider
(Types.AlibabaCloudServiceSettings (Just "sk") "host" "sid" "ws" rateLimit)
(Just (Types.AlibabaCloudTaskSettings (Just "search") (Just True)))
),
( "amazonbedrock",
Types.AmazonBedrockProvider
(Types.AmazonBedrockServiceSettings "ak" "m" "us-east-1" "sk" (Just "anthropic") rateLimit)
(Just (Types.AmazonBedrockTaskSettings (Just 128) (Just 0.5) (Just 4) Nothing))
),
( "amazon_sagemaker",
Types.AmazonSageMakerProvider
(Types.AmazonSageMakerServiceSettings "ak" "ep" Types.AmazonSageMakerApiElastic "us-east-1" "sk" (Just Types.SimilarityCosine) (Just Types.AmazonSageMakerElementFloat) Nothing Nothing Nothing (Just 8) (Just 256))
(Just (Types.AmazonSageMakerTaskSettings (Just "attr") Nothing Nothing Nothing Nothing))
),
( "azureaistudio",
Types.AzureAiStudioProvider
(Types.AzureAiStudioServiceSettings (Just "sk") "openai" "t" "microsoft" rateLimit)
(Just (Types.AzureAiStudioTaskSettings (Just 0.5) (Just 128) (Just True) (Just 0.7) (Just 3) Nothing (Just "u")))
),
( "azureopenai",
Types.AzureOpenAiProvider
(Types.AzureOpenAiServiceSettings "2024-02-15" "dep" "res" (Just "sk") Nothing Nothing Nothing rateLimit (Just ["s"]) Nothing)
(Just (Types.AzureOpenAiTaskSettings (Just "u") Nothing))
),
( "custom",
Types.CustomProvider
(Types.CustomServiceSettings (object ["a" .= (1 :: Int)]) (object ["b" .= (2 :: Int)]) (Types.SecretValue (object [])) (Just 4) Nothing Nothing Nothing (Just "https://custom"))
(Just (Types.CustomTaskSettings (Just (object ["x" .= (1 :: Int)]))))
),
( "watsonxai",
Types.WatsonxProvider $
Types.WatsonxServiceSettings (Just "sk") "2023-05-29" "m" "proj" "https://us-south.ml.cloud.ibm.com" rateLimit
)
]
mapM_
( \(svc, provider) -> do
let cfg =
Types.InferenceConfig
{ Types.inferenceProvider = provider,
Types.inferenceChunkingSettings = Nothing
}
json = encode cfg
json `shouldSatisfy` hasKey "service"
case decode json :: Maybe Value of
Just (Object o) -> KM.lookup "service" o `shouldBe` Just (String svc)
_ -> expectationFailure "expected a JSON object"
(decode json :: Maybe Types.InferenceConfig) `shouldBe` Just cfg
)
providers
describe "InferencePutResponse JSON" $ do
it "decodes a complete response" $ do
let raw =
LBS.pack
"{\"inference_id\":\"my-openai\",\"task_type\":\"text_embedding\",\"service\":\"openai\",\"service_settings\":{\"api_key\":\"sk-x\",\"model_id\":\"text-embedding-3-small\"}}"
let Just resp = decode raw
Types.iprInferenceId resp `shouldBe` "my-openai"
Types.iprTaskType resp `shouldBe` Types.TextEmbeddingTaskType
Types.iprService resp `shouldBe` "openai"
Types.iprServiceSettings resp `shouldSatisfy` isJust
it "round-trips" $ do
let resp =
Types.InferencePutResponse
{ Types.iprInferenceId = "x",
Types.iprTaskType = Types.SparseEmbeddingTaskType,
Types.iprService = "elser",
Types.iprServiceSettings = Nothing,
Types.iprTaskSettings = Nothing,
Types.iprChunkingSettings = Nothing
}
decode (encode resp) `shouldBe` Just resp
describe "InferenceInput JSON" $ do
it "always emits input as an array, omits optional fields" $ do
let input = Types.InferenceInput ["hello"] Nothing Nothing Nothing
let json = encode input
json `shouldSatisfy` hasKey "input"
json `shouldNotSatisfy` hasKey "query"
json `shouldNotSatisfy` hasKey "input_type"
json `shouldNotSatisfy` hasKey "task_settings"
case decode json :: Maybe Types.InferenceInput of
Just decoded -> Types.iiInput decoded `shouldBe` ["hello"]
Nothing -> expectationFailure "failed to decode InferenceInput"
it "includes query and task_settings when set" $ do
let input =
Types.InferenceInput
{ Types.iiInput = ["a", "b"],
Types.iiQuery = Just "star wars",
Types.iiInputType = Nothing,
Types.iiTaskSettings = Just (object ["max_tokens" .= (128 :: Int)])
}
let json = encode input
json `shouldSatisfy` hasKey "query"
json `shouldSatisfy` hasKey "task_settings"
describe "InferenceResult JSON" $ do
it "decodes a text_embedding response" $ do
let raw = LBS.pack "{\"text_embedding\":[{\"embedding\":[0.1,-0.2,0.3]}]}"
let Just (Types.DenseEmbeddingResult fmt xs) = decode raw
fmt `shouldBe` Types.FloatEmbedding
length xs `shouldBe` 1
case xs of
(item : _) -> Types.deiEmbedding item `shouldBe` [0.1, -0.2, 0.3]
[] -> expectationFailure "expected one embedding"
it "decodes an embeddings (plural) response as Float format" $ do
let raw = LBS.pack "{\"embeddings\":[{\"embedding\":[0.5]}]}"
let Just (Types.DenseEmbeddingResult fmt _) = decode raw
fmt `shouldBe` Types.FloatEmbedding
it "decodes an embeddings_bytes response as Bytes format" $ do
let raw = LBS.pack "{\"embeddings_bytes\":[{\"embedding\":[1,2,3]}]}"
let Just (Types.DenseEmbeddingResult fmt _) = decode raw
fmt `shouldBe` Types.BytesEmbedding
it "decodes a text_embedding_bytes response as Bytes format" $ do
let raw = LBS.pack "{\"text_embedding_bytes\":[{\"embedding\":[1,2]}]}"
let Just (Types.DenseEmbeddingResult fmt _) = decode raw
fmt `shouldBe` Types.BytesEmbedding
it "decodes an embeddings_bits response as Bits format" $ do
let raw = LBS.pack "{\"embeddings_bits\":[{\"embedding\":[0,1]}]}"
let Just (Types.DenseEmbeddingResult fmt _) = decode raw
fmt `shouldBe` Types.BitsEmbedding
it "decodes a text_embedding_bits response as Bits format" $ do
let raw = LBS.pack "{\"text_embedding_bits\":[{\"embedding\":[0]}]}"
let Just (Types.DenseEmbeddingResult fmt _) = decode raw
fmt `shouldBe` Types.BitsEmbedding
it "decodes a sparse_embedding response" $ do
let raw = LBS.pack "{\"sparse_embedding\":[{\"is_truncated\":false,\"embedding\":{\"tok1\":0.34,\"tok2\":1.2}}]}"
let Just (Types.SparseEmbeddingResult [item]) = decode raw
Types.seiIsTruncated item `shouldBe` False
Types.seiEmbedding item `shouldBe` Map.fromList [("tok1" :: Text, 0.34), ("tok2", 1.2)]
it "decodes a completion response" $ do
let raw = LBS.pack "{\"completion\":[{\"result\":\"hello world\"}]}"
let Just (Types.CompletionResult [item]) = decode raw
Types.ciResult item `shouldBe` "hello world"
it "decodes a rerank response" $ do
let raw =
LBS.pack
"{\"rerank\":[{\"index\":5,\"relevance_score\":0.97,\"text\":\"star\"},{\"index\":0,\"relevance_score\":0.81}]}"
let Just (Types.RerankResult items) = decode raw
length items `shouldBe` 2
case items of
(first : _) -> do
Types.riIndex first `shouldBe` 5
Types.riRelevanceScore first `shouldBe` 0.97
Types.riText first `shouldBe` Just "star"
[] -> expectationFailure "expected rerank items"
it "rejects a multi-key object" $ do
let raw = LBS.pack "{\"completion\":[],\"rerank\":[]}"
(decode raw :: Maybe Types.InferenceResult) `shouldBe` Nothing
it "rejects an empty object" $ do
let raw = LBS.pack "{}"
(decode raw :: Maybe Types.InferenceResult) `shouldBe` Nothing
it "rejects an unknown result key" $ do
let raw = LBS.pack "{\"whatever\":[]}"
(decode raw :: Maybe Types.InferenceResult) `shouldBe` Nothing
it "round-trips a text_embedding result" $ do
let res =
Types.DenseEmbeddingResult
Types.FloatEmbedding
[Types.DenseEmbeddingItem [0.1, 0.2]]
decode (encode res) `shouldBe` Just res
describe "endpoint shape" $ do
it "PUTs to /_inference/<task_type>/<inference_id> with a body" $ do
let cfg =
Types.InferenceConfig
{ Types.inferenceProvider =
Types.ElserProvider
Types.ElserServiceSettings
{ Types.esserNumAllocations = 1,
Types.esserNumThreads = 1,
Types.esserAdaptiveAllocations = Nothing
},
Types.inferenceChunkingSettings = Nothing
}
let req = RequestsES8.putInferenceEndpoint Types.SparseEmbeddingTaskType "my-elser" cfg
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_inference", "sparse_embedding", "my-elser"]
bhRequestBody req `shouldSatisfy` isJust
it "POSTs to /_inference/<task_type>/<inference_id> with a body" $ do
let input = Types.InferenceInput ["hello"] Nothing Nothing Nothing
let req = RequestsES8.runInference Types.TextEmbeddingTaskType "my-openai" input
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_inference", "text_embedding", "my-openai"]
bhRequestBody req `shouldSatisfy` isJust
it "DELETEs /_inference/<task_type>/<inference_id> with no body (ES8)" $ do
let req = RequestsES8.deleteInferenceEndpoint Types.TextEmbeddingTaskType "my-openai"
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_inference", "text_embedding", "my-openai"]
bhRequestBody req `shouldSatisfy` isNothing
it "DELETEs encode the snake_case task_type segment (ES8)" $ do
let req = RequestsES8.deleteInferenceEndpoint Types.SparseEmbeddingTaskType "my-elser"
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_inference", "sparse_embedding", "my-elser"]
bhRequestBody req `shouldSatisfy` isNothing
it "PUTs to /_inference/<task_type>/<inference_id>/_update with a body (ES8)" $ do
let cfg =
Types.InferenceConfig
{ Types.inferenceProvider =
Types.ElserProvider
Types.ElserServiceSettings
{ Types.esserNumAllocations = 1,
Types.esserNumThreads = 1,
Types.esserAdaptiveAllocations = Nothing
},
Types.inferenceChunkingSettings = Nothing
}
let req = RequestsES8.updateInferenceEndpoint Types.SparseEmbeddingTaskType "my-elser" cfg
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_inference", "sparse_embedding", "my-elser", "_update"]
bhRequestBody req `shouldSatisfy` isJust
it "ES9 update builder hits the same path as ES8" $ do
let cfg =
Types.InferenceConfig
{ Types.inferenceProvider =
Types.OpenAIProvider
(Types.OpenAIServiceSettings (Just "sk-xxx") "text-embedding-3-small" Nothing Nothing Nothing Nothing Nothing)
Nothing,
Types.inferenceChunkingSettings = Nothing
}
let req = RequestsES9.updateInferenceEndpoint Types.TextEmbeddingTaskType "my-openai" cfg
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_inference", "text_embedding", "my-openai", "_update"]
bhRequestBody req `shouldSatisfy` isJust
it "ES9 request builders hit the same paths as ES8" $ do
let cfg =
Types.InferenceConfig
{ Types.inferenceProvider =
Types.ElserProvider
Types.ElserServiceSettings
{ Types.esserNumAllocations = 1,
Types.esserNumThreads = 1,
Types.esserAdaptiveAllocations = Nothing
},
Types.inferenceChunkingSettings = Nothing
}
let input = Types.InferenceInput ["q"] (Just "query") Nothing Nothing
let putReq = RequestsES9.putInferenceEndpoint Types.RerankTaskType "r" cfg
let postReq = RequestsES9.runInference Types.RerankTaskType "r" input
getRawEndpoint (bhRequestEndpoint putReq) `shouldBe` ["_inference", "rerank", "r"]
getRawEndpoint (bhRequestEndpoint postReq) `shouldBe` ["_inference", "rerank", "r"]
it "ES9 DELETE reuses the ES8 path (no body)" $ do
let req = RequestsES9.deleteInferenceEndpoint Types.RerankTaskType "r"
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_inference", "rerank", "r"]
bhRequestBody req `shouldSatisfy` isNothing
it "POSTs to /_inference/completion/<inference_id>/_stream with a body (ES8)" $ do
let input = Types.InferenceInput ["What is Elastic?"] Nothing Nothing Nothing
let req = RequestsES8.streamCompletionInference "my-openai" input
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_inference", "completion", "my-openai", "_stream"]
bhRequestBody req `shouldSatisfy` isJust
it "POSTs to /_inference/chat_completion/<inference_id>/_stream with a body (ES8)" $ do
let chatReq =
Types.ChatCompletionRequest
{ Types.ccrMessages =
[Types.ChatMessage "user" (Just (toJSON ("What is Elastic?" :: Text))) Nothing Nothing Nothing Nothing],
Types.ccrModel = Just "gpt-4o",
Types.ccrMaxCompletionTokens = Nothing,
Types.ccrReasoning = Nothing,
Types.ccrStop = Nothing,
Types.ccrTemperature = Nothing,
Types.ccrToolChoice = Nothing,
Types.ccrTools = Nothing,
Types.ccrTopP = Nothing
}
let req = RequestsES8.streamChatCompletionInference "my-openai" chatReq
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_inference", "chat_completion", "my-openai", "_stream"]
bhRequestBody req `shouldSatisfy` isJust
it "ES9 streaming builders hit the same paths as ES8" $ do
let input = Types.InferenceInput ["q"] Nothing Nothing Nothing
getRawEndpoint
(bhRequestEndpoint $ RequestsES9.streamCompletionInference "c" input)
`shouldBe` ["_inference", "completion", "c", "_stream"]
let chatReq =
Types.ChatCompletionRequest
{ Types.ccrMessages =
[Types.ChatMessage "user" (Just (toJSON ("hi" :: Text))) Nothing Nothing Nothing Nothing],
Types.ccrModel = Nothing,
Types.ccrMaxCompletionTokens = Nothing,
Types.ccrReasoning = Nothing,
Types.ccrStop = Nothing,
Types.ccrTemperature = Nothing,
Types.ccrToolChoice = Nothing,
Types.ccrTools = Nothing,
Types.ccrTopP = Nothing
}
getRawEndpoint
(bhRequestEndpoint $ RequestsES9.streamChatCompletionInference "c" chatReq)
`shouldBe` ["_inference", "chat_completion", "c", "_stream"]
describe "deleteInferenceOptionsParams URI rendering" $ do
it "default emits dry_run=false and force=false" $
Types.deleteInferenceOptionsParams Types.defaultDeleteInferenceOptions
`shouldBe` [ ("dry_run", Just "false"),
("force", Just "false")
]
it "renders dry_run=true when set" $
Types.deleteInferenceOptionsParams
Types.defaultDeleteInferenceOptions {Types.dioDryRun = True}
`shouldBe` [ ("dry_run", Just "true"),
("force", Just "false")
]
it "renders force=true when set" $
Types.deleteInferenceOptionsParams
Types.defaultDeleteInferenceOptions {Types.dioForce = True}
`shouldBe` [ ("dry_run", Just "false"),
("force", Just "true")
]
it "renders both flags true together" $
Types.deleteInferenceOptionsParams
Types.defaultDeleteInferenceOptions {Types.dioDryRun = True, Types.dioForce = True}
`shouldBe` [ ("dry_run", Just "true"),
("force", Just "true")
]
describe "deleteInferenceEndpointWith endpoint shape" $ do
it "attaches dry_run/force query params (ES8)" $ do
let opts = Types.defaultDeleteInferenceOptions {Types.dioForce = True}
let req = RequestsES8.deleteInferenceEndpointWith Types.TextEmbeddingTaskType "my-openai" opts
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_inference", "text_embedding", "my-openai"]
getRawEndpointQueries (bhRequestEndpoint req)
`shouldBe` [ ("dry_run", Just "false"),
("force", Just "true")
]
bhRequestBody req `shouldSatisfy` isNothing
it "shares the path of deleteInferenceEndpoint, which stays query-less (ES8)" $ do
let plain = RequestsES8.deleteInferenceEndpoint Types.RerankTaskType "r"
let withOpts = RequestsES8.deleteInferenceEndpointWith Types.RerankTaskType "r" Types.defaultDeleteInferenceOptions
getRawEndpoint (bhRequestEndpoint plain)
`shouldBe` getRawEndpoint (bhRequestEndpoint withOpts)
getRawEndpointQueries (bhRequestEndpoint plain) `shouldBe` []
it "ES9 With reuses the ES8 builder" $ do
let opts = Types.defaultDeleteInferenceOptions {Types.dioDryRun = True}
let req = RequestsES9.deleteInferenceEndpointWith Types.RerankTaskType "r" opts
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_inference", "rerank", "r"]
getRawEndpointQueries (bhRequestEndpoint req)
`shouldBe` [ ("dry_run", Just "true"),
("force", Just "false")
]
bhRequestBody req `shouldSatisfy` isNothing
describe "inference timeout options URI rendering" $ do
it "putInferenceOptionsParams default emits no params" $
Types.putInferenceOptionsParams Types.defaultPutInferenceOptions
`shouldBe` []
it "putInferenceOptionsParams renders timeout (Seconds)" $
Types.putInferenceOptionsParams
Types.defaultPutInferenceOptions {Types.pioTimeout = Just (Types.TimeUnitSeconds, 30)}
`shouldBe` [("timeout", Just "30s")]
it "runInferenceOptionsParams renders timeout (Milliseconds)" $
Types.runInferenceOptionsParams
Types.defaultRunInferenceOptions {Types.rnioTimeout = Just (Types.TimeUnitMilliseconds, 500)}
`shouldBe` [("timeout", Just "500ms")]
it "streamCompletionInferenceOptionsParams renders timeout" $
Types.streamCompletionInferenceOptionsParams
Types.defaultStreamCompletionInferenceOptions {Types.scioTimeout = Just (Types.TimeUnitSeconds, 5)}
`shouldBe` [("timeout", Just "5s")]
it "streamChatCompletionInferenceOptionsParams renders timeout (Minutes)" $
Types.streamChatCompletionInferenceOptionsParams
Types.defaultStreamChatCompletionInferenceOptions {Types.sccioTimeout = Just (Types.TimeUnitMinutes, 2)}
`shouldBe` [("timeout", Just "2m")]
describe "putInferenceEndpointWith endpoint shape" $ do
let cfg =
Types.InferenceConfig
{ Types.inferenceProvider =
Types.ElserProvider
Types.ElserServiceSettings
{ Types.esserNumAllocations = 1,
Types.esserNumThreads = 1,
Types.esserAdaptiveAllocations = Nothing
},
Types.inferenceChunkingSettings = Nothing
}
it "is byte-identical to putInferenceEndpoint with default opts (ES8)" $ do
let plain = RequestsES8.putInferenceEndpoint Types.SparseEmbeddingTaskType "my-elser" cfg
withOpts = RequestsES8.putInferenceEndpointWith Types.SparseEmbeddingTaskType "my-elser" cfg Types.defaultPutInferenceOptions
getRawEndpoint (bhRequestEndpoint plain) `shouldBe` getRawEndpoint (bhRequestEndpoint withOpts)
getRawEndpointQueries (bhRequestEndpoint plain) `shouldBe` []
getRawEndpointQueries (bhRequestEndpoint withOpts) `shouldBe` []
it "attaches ?timeout=30s with opts (ES8)" $ do
let opts = Types.defaultPutInferenceOptions {Types.pioTimeout = Just (Types.TimeUnitSeconds, 30)}
req = RequestsES8.putInferenceEndpointWith Types.TextEmbeddingTaskType "my-openai" cfg opts
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_inference", "text_embedding", "my-openai"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` [("timeout", Just "30s")]
it "ES9 With reuses the ES8 builder" $ do
let opts = Types.defaultPutInferenceOptions {Types.pioTimeout = Just (Types.TimeUnitSeconds, 30)}
req = RequestsES9.putInferenceEndpointWith Types.RerankTaskType "r" cfg opts
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_inference", "rerank", "r"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` [("timeout", Just "30s")]
describe "runInferenceWith endpoint shape" $ do
let input = Types.InferenceInput ["q"] Nothing Nothing Nothing
it "is byte-identical to runInference with default opts (ES8)" $ do
let plain = RequestsES8.runInference Types.TextEmbeddingTaskType "my-openai" input
withOpts = RequestsES8.runInferenceWith Types.TextEmbeddingTaskType "my-openai" input Types.defaultRunInferenceOptions
getRawEndpoint (bhRequestEndpoint plain) `shouldBe` getRawEndpoint (bhRequestEndpoint withOpts)
getRawEndpointQueries (bhRequestEndpoint plain) `shouldBe` []
it "attaches ?timeout=10s with opts (ES8)" $ do
let opts = Types.defaultRunInferenceOptions {Types.rnioTimeout = Just (Types.TimeUnitSeconds, 10)}
req = RequestsES8.runInferenceWith Types.TextEmbeddingTaskType "my-openai" input opts
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_inference", "text_embedding", "my-openai"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` [("timeout", Just "10s")]
it "ES9 With reuses the ES8 builder" $ do
let opts = Types.defaultRunInferenceOptions {Types.rnioTimeout = Just (Types.TimeUnitSeconds, 10)}
req = RequestsES9.runInferenceWith Types.RerankTaskType "r" input opts
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_inference", "rerank", "r"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` [("timeout", Just "10s")]
describe "streamCompletionInferenceWith endpoint shape" $ do
let input = Types.InferenceInput ["q"] Nothing Nothing Nothing
it "is byte-identical to streamCompletionInference with default opts (ES8)" $ do
let plain = RequestsES8.streamCompletionInference "my-openai" input
withOpts = RequestsES8.streamCompletionInferenceWith "my-openai" input Types.defaultStreamCompletionInferenceOptions
getRawEndpoint (bhRequestEndpoint plain) `shouldBe` getRawEndpoint (bhRequestEndpoint withOpts)
getRawEndpointQueries (bhRequestEndpoint plain) `shouldBe` []
it "attaches ?timeout=5s with opts (ES8)" $ do
let opts = Types.defaultStreamCompletionInferenceOptions {Types.scioTimeout = Just (Types.TimeUnitSeconds, 5)}
req = RequestsES8.streamCompletionInferenceWith "my-openai" input opts
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_inference", "completion", "my-openai", "_stream"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` [("timeout", Just "5s")]
it "ES9 With reuses the ES8 builder" $ do
let opts = Types.defaultStreamCompletionInferenceOptions {Types.scioTimeout = Just (Types.TimeUnitSeconds, 5)}
req = RequestsES9.streamCompletionInferenceWith "c" input opts
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_inference", "completion", "c", "_stream"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` [("timeout", Just "5s")]
describe "streamChatCompletionInferenceWith endpoint shape" $ do
let chatReq =
Types.ChatCompletionRequest
{ Types.ccrMessages = [Types.ChatMessage "user" (Just (toJSON ("hi" :: Text))) Nothing Nothing Nothing Nothing],
Types.ccrModel = Nothing,
Types.ccrMaxCompletionTokens = Nothing,
Types.ccrReasoning = Nothing,
Types.ccrStop = Nothing,
Types.ccrTemperature = Nothing,
Types.ccrToolChoice = Nothing,
Types.ccrTools = Nothing,
Types.ccrTopP = Nothing
}
it "is byte-identical to streamChatCompletionInference with default opts (ES8)" $ do
let plain = RequestsES8.streamChatCompletionInference "my-openai" chatReq
withOpts = RequestsES8.streamChatCompletionInferenceWith "my-openai" chatReq Types.defaultStreamChatCompletionInferenceOptions
getRawEndpoint (bhRequestEndpoint plain) `shouldBe` getRawEndpoint (bhRequestEndpoint withOpts)
getRawEndpointQueries (bhRequestEndpoint plain) `shouldBe` []
it "attaches ?timeout=5s with opts (ES8)" $ do
let opts = Types.defaultStreamChatCompletionInferenceOptions {Types.sccioTimeout = Just (Types.TimeUnitSeconds, 5)}
req = RequestsES8.streamChatCompletionInferenceWith "my-openai" chatReq opts
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_inference", "chat_completion", "my-openai", "_stream"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` [("timeout", Just "5s")]
it "ES9 With reuses the ES8 builder" $ do
let opts = Types.defaultStreamChatCompletionInferenceOptions {Types.sccioTimeout = Just (Types.TimeUnitSeconds, 5)}
req = RequestsES9.streamChatCompletionInferenceWith "c" chatReq opts
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_inference", "chat_completion", "c", "_stream"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` [("timeout", Just "5s")]
describe "GET /_inference endpoint shape" $ do
it "lists every endpoint when no filter is given (ES8)" $ do
let req = RequestsES8.getInferenceEndpoints Nothing Nothing
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_inference"]
bhRequestBody req `shouldSatisfy` isNothing
it "narrows to a task type when only TaskType is given (ES8)" $ do
let req = RequestsES8.getInferenceEndpoints (Just Types.TextEmbeddingTaskType) Nothing
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_inference", "text_embedding", "_all"]
bhRequestBody req `shouldSatisfy` isNothing
it "targets a specific endpoint when both are given (ES8)" $ do
let req =
RequestsES8.getInferenceEndpoints
(Just Types.SparseEmbeddingTaskType)
(Just "my-elser")
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_inference", "sparse_embedding", "my-elser"]
bhRequestBody req `shouldSatisfy` isNothing
it "encodes the snake_case task_type segment (ES8)" $ do
let req = RequestsES8.getInferenceEndpoints (Just Types.ChatCompletionTaskType) Nothing
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_inference", "chat_completion", "_all"]
it "falls back to list-all when only InferenceId is given (ES8)" $ do
-- (Nothing, Just iid) is not a valid ES URL; the builder drops the
-- bare id and lists every endpoint instead of synthesising an
-- unsupported path.
let req = RequestsES8.getInferenceEndpoints Nothing (Just "orphan-id")
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["_inference"]
it "ES9 request builder hits the same paths as ES8" $ do
getRawEndpoint
(bhRequestEndpoint $ RequestsES9.getInferenceEndpoints Nothing Nothing)
`shouldBe` ["_inference"]
getRawEndpoint
( bhRequestEndpoint $
RequestsES9.getInferenceEndpoints (Just Types.RerankTaskType) Nothing
)
`shouldBe` ["_inference", "rerank", "_all"]
getRawEndpoint
( bhRequestEndpoint $
RequestsES9.getInferenceEndpoints
(Just Types.RerankTaskType)
(Just "r")
)
`shouldBe` ["_inference", "rerank", "r"]
describe "InferenceInfo JSON" $ do
it "decodes a single-endpoint response, splitting the key" $ do
let raw =
LBS.pack
"{\"text_embedding/my-openai\":{\"service\":\"openai\",\"service_settings\":{\"model_id\":\"text-embedding-3-small\",\"api_key\":null}}}"
case decodeInferenceList raw of
Just [info] -> do
Types.infTaskType info `shouldBe` Types.TextEmbeddingTaskType
Types.infInferenceId info `shouldBe` "my-openai"
Types.infService info `shouldBe` "openai"
Types.infServiceSettings info `shouldSatisfy` isJust
Types.infTaskSettings info `shouldSatisfy` isNothing
Types.infChunkingSettings info `shouldSatisfy` isNothing
other ->
expectationFailure $ "expected one InferenceInfo, got: " <> show other
it "decodes a multi-endpoint response into a list" $ do
let raw =
LBS.pack
"{\"text_embedding/a\":{\"service\":\"openai\"},\"rerank/b\":{\"service\":\"cohere\",\"task_settings\":{\"top_n\":5}}}"
case decodeInferenceList raw of
Just infos -> length infos `shouldBe` 2
Nothing -> expectationFailure "failed to decode multi-endpoint response"
it "decodes when task_settings and chunking_settings are present" $ do
let raw =
LBS.pack
"{\"completion/c\":{\"service\":\"openai\",\"task_settings\":{\"user\":\"x\"},\"chunking_settings\":{\"strategy\":\"sentence\"}}}"
case decodeInferenceList raw of
Just [info] -> do
Types.infTaskType info `shouldBe` Types.CompletionTaskType
Types.infInferenceId info `shouldBe` "c"
Types.infTaskSettings info `shouldSatisfy` isJust
Types.infChunkingSettings info `shouldSatisfy` isJust
other ->
expectationFailure $ "expected one InferenceInfo, got: " <> show other
it "tolerates a null api_key in service_settings" $ do
let raw =
LBS.pack
"{\"text_embedding/d\":{\"service\":\"openai\",\"service_settings\":{\"api_key\":null}}}"
case decodeInferenceList raw of
Just [info] ->
Types.infServiceSettings info
`shouldBe` Just (object ["api_key" .= Null])
other ->
expectationFailure $ "expected exactly one InferenceInfo, got: " <> show other
it "rejects a key without a '/' separator" $ do
let raw = LBS.pack "{\"no-separator\":{\"service\":\"openai\"}}"
decodeInferenceList raw `shouldBe` Nothing
it "rejects an unknown task_type in the key" $ do
let raw = LBS.pack "{\"bogus_type/x\":{\"service\":\"openai\"}}"
decodeInferenceList raw `shouldBe` Nothing
it "decodes an empty response as the empty list" $ do
let raw = LBS.pack "{}"
decodeInferenceList raw `shouldBe` Just []
it "round-trips a bare InferenceInfo (direct FromJSON/ToJSON)" $ do
let info =
Types.InferenceInfo
{ Types.infTaskType = Types.TextEmbeddingTaskType,
Types.infInferenceId = "my-openai",
Types.infService = "openai",
Types.infServiceSettings = Just (object ["model_id" .= ("text-embedding-3-small" :: Text)]),
Types.infTaskSettings = Nothing,
Types.infChunkingSettings = Nothing
}
(decode (encode info) :: Maybe Types.InferenceInfo) `shouldBe` Just info
it "decodes a bare InferenceInfo object (no key wrapper)" $ do
let raw =
LBS.pack
"{\"task_type\":\"rerank\",\"inference_id\":\"r1\",\"service\":\"cohere\",\"task_settings\":{\"top_n\":5}}"
case (decode raw :: Maybe Types.InferenceInfo) of
Just info -> do
Types.infTaskType info `shouldBe` Types.RerankTaskType
Types.infInferenceId info `shouldBe` "r1"
Types.infService info `shouldBe` "cohere"
Types.infTaskSettings info `shouldSatisfy` isJust
Nothing -> expectationFailure "expected InferenceInfo, got Nothing"
it "InferenceEndpointsResponse decodes the per-id {\"endpoints\":[...]} envelope" $ do
-- GET /_inference/{task_type}/{inference_id} returns this shape, which
-- must decode to the same [InferenceInfo] as the list envelope.
let raw =
LBS.pack
"{\"endpoints\":[{\"task_type\":\"text_embedding\",\"inference_id\":\"x\",\"service\":\"openai\"}]}"
decodeInferenceList raw `shouldBe` Just [Types.InferenceInfo Types.TextEmbeddingTaskType "x" "openai" Nothing Nothing Nothing]
describe "live integration (requires ES8+ backend)" $
backendSpecific [ElasticSearch8, ElasticSearch9] $ do
it "rejects an unknown inference service with a 4xx error" $
withTestEnv $ do
let bogusSettings = object ["model_id" .= ("nope" :: Text)]
cfg =
Types.InferenceConfig
{ Types.inferenceProvider =
Types.GenericProvider "definitely_not_a_real_service" bogusSettings Nothing,
Types.inferenceChunkingSettings = Nothing
}
result <- do
backend <- liftIO detectBackendType
if backend == Just ElasticSearch9
then ClientES9.putInferenceEndpoint Types.TextEmbeddingTaskType "bloodhound-test-bogus" cfg
else ClientES8.putInferenceEndpoint Types.TextEmbeddingTaskType "bloodhound-test-bogus" cfg
case result of
Left e -> do
case errorStatus e of
Just s -> liftIO $ (s >= 400 && s < 500) `shouldBe` True
Nothing ->
liftIO $
expectationFailure $
"expected a 4xx status, got none. Message: " <> show (errorMessage e)
Right _ ->
liftIO $
expectationFailure
"expected the bogus inference config to be rejected, but it succeeded"
it "rejects DELETE of a non-existent inference endpoint with a 4xx error" $
withTestEnv $ do
-- ES9 reuses the ES8 request builder verbatim, so a single call
-- covers both backends; 'tryPerformBHRequest' surfaces the 4xx as
-- 'Left EsError'.
result <-
tryPerformBHRequest $
RequestsES8.deleteInferenceEndpoint Types.TextEmbeddingTaskType "bloodhound-test-definitely-missing"
case result of
Left e ->
case errorStatus e of
Just s -> liftIO $ (s >= 400 && s < 500) `shouldBe` True
Nothing ->
liftIO $
expectationFailure $
"expected a 4xx status, got none. Message: " <> show (errorMessage e)
Right _ ->
liftIO $
expectationFailure
"expected DELETE of a missing inference endpoint to fail, but it succeeded"