Repository navigation
[Inference API] Refactor Contextual AI integration and fix multiple issues - #145700
Conversation
Register ContextualAiRerankServiceSettings and ContextualAiRerankTaskSettings in InferenceNamedWriteablesProvider. Require model_id in service settings. Add ContextualAiUtils for transport version; align writeables with supportsVersion. Retry HTTP 503; use ErrorResponse::fromResponse for provider error bodies. Wire return_documents into rerank JSON; simplify top_n serialization. Enforce non-empty api_key via DefaultSecretSettings path in ContextualAiModel. Use Elasticsearch logging in ContextualAiRerankRequest; protected doInfer. Made-with: Cursor
| .build() | ||
| ); | ||
|
|
||
| configurationMap.put( |
There was a problem hiding this comment.
There was a TODO by the engineer that implemented this for giving users ability to specify the URL. Considering there is a such service setting I believe it should be exposed here.
There was a problem hiding this comment.
It was decided to remove this setting.
| config.getTaskType(), | ||
| config.getService(), | ||
| ConfigurationParseContext.PERSISTENT | ||
| ConfigurationParseContext.REQUEST |
There was a problem hiding this comment.
Doing this across the services.
| } | ||
| } | ||
|
|
||
| private static ContextualAiModel createModel( |
There was a problem hiding this comment.
Only used in 1 place. Better to inline.
| public static final String NAME = "contextualai"; | ||
| private static final String SERVICE_NAME = "Contextual AI"; | ||
|
|
||
| private static final TransportVersion CONTEXTUAL_AI_SERVICE = TransportVersion.fromName("contextual_ai_service"); |
There was a problem hiding this comment.
Moved version to common place.
| int statusCode = result.response().getStatusLine().getStatusCode(); | ||
| if (statusCode == 500) { | ||
| throw new RetryException(true, buildError(SERVER_ERROR, request, result)); | ||
| } else if (statusCode == 503) { |
There was a problem hiding this comment.
Retrying both 500s and 503s is standard, added this here as well.
| public class ContextualAiResponseHandler extends BaseResponseHandler { | ||
|
|
||
| public ContextualAiResponseHandler(String requestType, ResponseParser parseFunction, boolean supportsStreaming) { | ||
| super(requestType, parseFunction, ContextualAiErrorResponseEntity::fromResponse, supportsStreaming); |
There was a problem hiding this comment.
Original error response didn't have any value
|
|
||
| public ContextualAiRerankServiceSettings(URI uri, @Nullable String modelId, @Nullable RateLimitSettings rateLimitSettings) { | ||
| this.uri = Objects.requireNonNull(uri); | ||
| this.modelId = modelId; // Can be null for REQUEST context |
There was a problem hiding this comment.
model_id is mandatory on ContextualAI side.
…integration-fixes
…integration-fixes
…arity and consistency
…integration-fixes # Conflicts: # server/src/main/resources/transport/upper_bounds/9.4.csv
|
Hello @DonalEvans & @jonathan-buttner |
DonalEvans
left a comment
There was a problem hiding this comment.
Looks good, just a couple of small suggestions
| * @return the parsed {@link RankedDocsResults} | ||
| * @throws IOException if there is an error parsing the response | ||
| */ | ||
| public static RankedDocsResults fromResponse(HttpResult response) throws IOException { | ||
| var parserConfig = XContentParserConfiguration.EMPTY.withDeprecationHandler(LoggingDeprecationHandler.INSTANCE); | ||
|
|
||
| try (XContentParser jsonParser = XContentFactory.xContent(XContentType.JSON).createParser(parserConfig, response.body())) { | ||
| moveToFirstToken(jsonParser); |
There was a problem hiding this comment.
I'm pretty sure this call is no longer needed (if it ever was). If I delete it, all the tests in ContextualAiRerankResponseEntityTests still pass.
There was a problem hiding this comment.
Done. Thank you
| } | ||
| } | ||
|
|
||
| private static List<RankedDocsResults.RankedDoc> doParse(XContentParser parser) throws IOException { | ||
| private static List<RankedDocsResults.RankedDoc> doParse(XContentParser parser) { | ||
| var responseParser = ResponseParser.PARSER; |
There was a problem hiding this comment.
responseParser can be inlined, and ResponseParser.PARSER can be replaced with ResponseObject.PARSER and the ResponseParser class removed.
There was a problem hiding this comment.
Good catch. Done.
| assertThat( | ||
| thrownException.getMessage(), | ||
| is(Strings.format("unable to parse url [%s]. Reason: Illegal character in path", INVALID_URL)) | ||
| public void testOf_EmptyMap_RequestOverridesAllValues_AllValuesUpdated() { |
There was a problem hiding this comment.
This test name is a little confusing, since the task settings map is not empty.
There was a problem hiding this comment.
Renamed. Now it is clearer. Thanks.
| * @return the parsed {@link RankedDocsResults} | ||
| * @throws IOException if there is an error parsing the response | ||
| */ | ||
| public static RankedDocsResults fromResponse(HttpResult response) throws IOException { | ||
| var parserConfig = XContentParserConfiguration.EMPTY.withDeprecationHandler(LoggingDeprecationHandler.INSTANCE); | ||
|
|
||
| try (XContentParser jsonParser = XContentFactory.xContent(XContentType.JSON).createParser(parserConfig, response.body())) { | ||
| moveToFirstToken(jsonParser); |
There was a problem hiding this comment.
nit: Not your changes but I think we can remove this
There was a problem hiding this comment.
Done. Thanks.
| } | ||
| } | ||
|
|
||
| private static List<RankedDocsResults.RankedDoc> doParse(XContentParser parser) throws IOException { | ||
| private static List<RankedDocsResults.RankedDoc> doParse(XContentParser parser) { | ||
| var responseParser = ResponseParser.PARSER; |
There was a problem hiding this comment.
nit: Also not your changes but I don't think we need the local variable here.
There was a problem hiding this comment.
Done.
| public static boolean supportsContextualAi(TransportVersion version) { | ||
| return version.supports(ML_INFERENCE_CONTEXTUAL_AI_ADDED); | ||
| } | ||
| public static final TransportVersion ML_INFERENCE_CONTEXTUAL_AI_URL_SERVICE_SETTING_REMOVED = TransportVersion.fromName( |
There was a problem hiding this comment.
How about we drop the ML_ part since we're not in machine learning anymore
There was a problem hiding this comment.
Removed here and for the initial version as well.
…integration-fixes # Conflicts: # server/src/main/resources/transport/upper_bounds/9.4.csv
…integration-fixes # Conflicts: # server/src/main/resources/transport/upper_bounds/9.5.csv
…-fixes' into feature/contextualai-integration-fixes
🔍 Preview links for changed docs⏳ Building and deploying preview... View progress This comment will be updated with preview links when the build is complete. |
ℹ️ Important: Docs version tagging👋 Thanks for updating the docs! Just a friendly reminder that our docs are now cumulative. This means all 9.x versions are documented on the same page and published off of the main branch, instead of creating separate pages for each minor version. We use applies_to tags to mark version-specific features and changes. Expand for a quick overviewWhen to use applies_to tags:✅ At the page level to indicate which products/deployments the content applies to (mandatory) What NOT to do:❌ Don't remove or replace information that applies to an older version 🤔 Need help?
|
…integration-fixes # Conflicts: # server/src/main/resources/transport/upper_bounds/9.5.csv
…integration-fixes # Conflicts: # server/src/main/resources/transport/upper_bounds/9.5.csv
Summary
This pull request refactors the Contextual AI service integration in the inference plugin, fixing critical bugs, removing deprecated settings, and introducing architectural improvements for maintainability and consistency with modern service patterns.
Bugs Fixed
Missing
NamedWriteableRegistryregistration —ContextualAiRerankServiceSettingsandContextualAiRerankTaskSettingswere not registered inInferenceNamedWriteablesProvider, preventing models from being serialized/deserialized over the wire.HTTP 503 responses not retried —
ContextualAiResponseHandlerincorrectly treated HTTP 503 as non-retryable. Now correctly retries 503 alongside 500.Error messages returned HTTP status line instead of body —
ContextualAiErrorResponseEntity.fromResponseonly returned the status line (e.g., "HTTP/1.1 400 Bad Request"). Replaced withErrorResponse.fromResponseto read actual response body.ContextualAiErrorResponseEntitydeleted entirely.Duplicate
topNfallback logic —ContextualAiRerankRequestEntitycontained redundant fallback logic that re-read model task settings. Canonical fallback now lives only inContextualAiRerankRequest.getTopN().Overly restrictive type check in
doInfer—doInferrejected models that wereContextualAiModelbut notContextualAiRerankModel. Broadened to accept anyContextualAiModel.Client-side re-sorting and top-N filtering in response parsing —
ContextualAiRerankResponseEntityre-sorted results by relevance score and applied atopNlimit client-side. This duplicated server-side behavior. Response parsing now returns results in the order provided by the API without additional filtering.Input type validation missing —
validateInputTypewas a no-op that accepted any input type. Now properly validates usingServiceUtils.validateInputTypeIsUnspecifiedOrInternal.Functional Changes
Removed
urlservice setting —ContextualAiRerankServiceSettingsno longer accepts/stores theurlfield. The endpoint URI is hardcoded asDEFAULT_RERANK_URI(https://api.contextual.ai/v1/rerank) inContextualAiRerankModel. Transport wire backward-compatibility handled via newTransportVersioncontextual_ai_url_service_setting_removedwith conditional read/write logic: older nodes still send/receive a URL string, which is read and discarded on deserialization, and the default URL is written for older peers on serialization.Removed hardcoded default instruction —
getInstruction()previously always returned "Rerank the given documents based on their relevance to the query." when unset. Now returnsnull, and theinstructionfield is omitted from API requests when unset.Removed
documentfield from response parsing —RankedDocEntryno longer parses thedocumentfield from the API response.RankedDocobjects always passnullfor the document text, sincereturn_documentswas never sent to the ContextualAI API.Added
updateServiceSettingsimplementation —ContextualAiRerankServiceSettings.updateServiceSettingsnow supports updating mutable fields (currently onlyrate_limit). Immutable fields likemodel_idare preserved from the original settings.Changed
doInfervisibility —doInferchanged frompublictoprotected, consistent with theSenderServicebase class contract.Refactoring
Introduced
ContextualAiServiceSettingsabstract class — ReplacesContextualAiRateLimitServiceSettingsinterface, encapsulating common model ID and rate-limit settings with sharedwriteTo/StreamInput,toXContent,fromMap, andupdateCommonSettingslogic via aCommonSettingsrecord.Introduced
ContextualAiUtils— Centralizes shared transport-version constants (ML_INFERENCE_CONTEXTUAL_AI_ADDED,ML_INFERENCE_CONTEXTUAL_AI_URL_SERVICE_SETTING_REMOVED).Simplified
ContextualAiModel— Removed directapiKeyandrateLimitServiceSettingsfields; now delegates togetServiceSettings()andgetSecretSettings(). Stores the request URI directly. Added covariantgetServiceSettings()/getSecretSettings()overrides returningContextualAiServiceSettingsandDefaultSecretSettingsrespectively.Decoupled
ContextualAiRerankRequestEntityfromContextualAiRerankModel— Now takes a plainString modelIdinstead of the model object. Field constants changed fromprivatetopublicfor test assertions.Simplified
ContextualAiRerankRequest— Removed separateinstructionconstructor parameter; instruction is now retrieved frommodel.getTaskSettings().getInstruction()at request-creation time. RemoveddecorateWithAuthhelper method and debug logging with try/catch wrapping. Inlined JSON serialization directly.Simplified
ContextualAiRerankResponseEntity— RemovedContextualAiRerankRequestparameter fromfromResponse; response parsing no longer depends on the request object.Inlined
createModelhelper — Deleted private helper inContextualAiService.parseRequestConfig, usingretrieveModelCreatorFromMapOrThrow(...).createFromMaps(...)directly for consistency with other services.Added covariant return types —
ContextualAiRerankModel.getTaskSettings()returnsContextualAiRerankTaskSettingsandgetServiceSettings()returnsContextualAiRerankServiceSettings, eliminating scattered casts in callers.Optimized
ContextualAiRerankModel.of()— Returns the same model instance when request task settings are empty or produce no effective change, avoiding unnecessary object creation.Optimized
ContextualAiRerankTaskSettings.of()— Returns the original settings instance when the merged result equals the original, avoiding unnecessary object creation.Tests
New
ContextualAiActionCreatorTests— Integration-style tests usingMockWebSerververifying end-to-end request construction (headers, body fields, auth), task settings overrides, request-leveltopNpriority, and error handling for invalid response formats.New
ContextualAiRerankRequestEntityTests— Unit tests verifying XContent serialization with all fields, required-only fields, and null-guard behavior on required parameters.New
ContextualAiRerankRequestTests— Unit tests verifying HTTP request construction (method, URI, headers, auth, body), task settings propagation, and request-leveltopNoverride behavior.New
ContextualAiRerankModelTests— Unit tests verifying model construction from maps,ContextualAiRerankModel.of()merge semantics including identity-return optimizations.New
ContextualAiRerankServiceSettingsTests— BWC wire serialization tests,fromMapparsing (required/optional fields, validation errors),updateServiceSettingsbehavior, and XContent round-tripping.New
ContextualAiRerankTaskSettingsTests— BWC wire serialization tests,fromMapparsing with validation (invalid types, zero/negativetopN),of()merge semantics,updatedTaskSettings,isEmpty, and XContent round-tripping.New
ContextualAiRerankResponseEntityTests— Unit tests verifying response parsing for single/multiple items, empty results, floating-point precision, order preservation, unknown fields tolerance, and error cases (missing required fields, malformed JSON).Updated
ContextualAiResponseHandlerTests— Improved tests to verify error body content is included in exception messages, added 503 retryable assertion, extracted shared test fixtures.New
ContextualAiRerankTestFixtures— Shared test constants for inference entity ID, model ID, API key, documents, query, rate limits, and parameterized test values used across all test classes.Testing
RERANK
ContextualAI API rejects `return_documents`
Create rerank endpoint (URL present, failure)
Create rerank endpoint (success)
Perform rerank (all fields)
Create rerank endpoint with min fields (success)
Perform rerank (minfields)
References