Skip to content

[inference] Sanitize tool schemas for Google Vertex AI inference endpoints - #284810

Merged
chrisbmar merged 3 commits into
elastic:mainfrom
chrisbmar:fix-vertex-inference-connector-sdh-3724
Aug 14, 2026
Merged

chrisbmar merged 3 commits into
elastic:mainfrom
chrisbmar:fix-vertex-inference-connector-sdh-3724

Conversation

@chrisbmar

@chrisbmar chrisbmar commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Closes #284647

Agent Builder (and any chatComplete consumer) fails with a 400 when using a Google Vertex AI chat_completion inference endpoint: tool schemas produced by zod v4's toJSONSchema contain JSON Schema keywords (propertyNames, exclusiveMinimum/exclusiveMaximum, const, nested in oneOf/anyOf) that Vertex's function declaration schema — a strict OpenAPI 3.0 subset — rejects as unknown proto fields.

The inference_endpoint adapter (the path all inference-endpoint traffic takes since #258530) applies no tool schema transforms — this was lost when the path was added in #256071. The deprecated Gemini connector is unaffected because its adapter rebuilds schemas via an allowlist. (toolSchemaToGemini in adapters/gemini/gemini_adapter.ts) - is the same approach this PR applies to the endpoint path, in OpenAI-format instead of the Gemini SDK's.

Changes

  • New sanitizeToolSchemasForVertex: recursively rebuilds tool schemas keeping only the fields Vertex accepts (allowlist sourced from the Vertex Schema definition — the consensus approach used by LiteLLM (filter_schema_fields), the Vercel AI SDK (convertJSONSchemaToOpenAPISchema), and Google's own genai SDK (processJsonSchema)), with two intent-preserving conversions: const: "x" → enum: ["x"] and anyOf-with-{type: "null"} → nullable: true.
  • inference_endpoint_adapter applies it only when the endpoint's service is googlevertexai; callback_api.ts threads the endpoint's service through as provider. All other providers pass through unchanged (Bedrock endpoints verified working live without transforms).
  • The stack-connector adapter (adapters/inference) is intentionally untouched.

Testing

  • Unit tests build fixtures with real z.toJSONSchema (mirroring resolveToolSchema in @kbn/inference-langchain) and assert the offending keywords are present pre-sanitization, covering records, number bounds, literals, unions, nullable, and discriminated-union-in-array — the exact shapes observed failing live.
  • Verified end-to-end against a live Vertex AI endpoint (gemini-2.5-flash): default agent works; before the fix both reproduce the customer's 400.

Note: multi-round tool use against Vertex endpoints still requires the ES-side fix for tool-call arguments in this PR elastic/elasticsearch#156665. mapStrings() rejects non-string argument values).

Before
image

After
image

@@ -0,0 +1,122 @@
/*

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments in this file are verbose, but considering what it does, I think it's worth keeping them to be explicitly clear about why these functions are needed. Will remove if anyone thinks otherwise.

@chrisbmar
chrisbmar marked this pull request as ready for review August 13, 2026 11:31
@chrisbmar
chrisbmar requested a review from a team as a code owner August 13, 2026 11:31

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One concrete correctness finding on the Vertex schema allowlist (oneOf is not a supported Vertex proto field). Left inline. Otherwise the approach (allowlist rebuild + provider gating) looks sound.

Generated by Claude Reviewer for #284810 · 127.4 AIC · ⌖ 16.4 AIC · ⊞ 4.6K

'pattern',
'example',
'anyOf',
'oneOf',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oneOf is not a field in Vertex's Schema proto — that message defines any_of (field 11) but no one_of/oneOf. Since the PR's stated failure mechanism is that Vertex rejects unknown proto fields with a 400, keeping oneOf in the allowlist means discriminated-union tool schemas still fail.

zod v4 emits oneOf for z.discriminatedUnion (as the operations fixture in sanitize_tool_schemas_for_vertex.test.ts shows), and the recursion below preserves it as-is, so the sanitized output still contains oneOf. The test asserts this and therefore locks in the broken behavior — discriminated-union tools will 400 against a real Vertex endpoint, the same class of bug this PR is fixing. The live end-to-end check ("default agent works") likely didn't exercise a discriminated-union tool schema, so this path went unverified.

Suggested fix: drop 'oneOf' from the allowlist and convert oneOf → anyOf during recursion (this is what the referenced LiteLLM / genai-SDK implementations do), then update the test expectation accordingly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestingly, I did test oneOf and it worked!

image

The parser steps into oneOf successfully and the failure is with const (which I have fixed in this PR).

But I'll look into this further, I'd prefer that we conform to the exact documented schema so the bot's comment is valid.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed 56a0d29

@kibanamachine

Copy link
Copy Markdown
Contributor

💛 Build succeeded, but was flaky

Failed CI Steps

Metrics [docs]

✅ unchanged

Test Failures

  • [job] [logs] Jest Integration Tests #7 / capacity based claiming should claim tasks until the next task will exceed capacity
  • [job] [logs] FTR Configs #10 / lens app - group 1 lens chart style settings should allow creation of a multi-axis chart and switching multiple times
  • [job] [logs] FTR Configs #10 / lens app - group 1 lens chart style settings should override axis title
  • [job] [logs] Scout Lane #18 - stateful-classic / default / local-stateful-classic - GettingStarted - creates a basic monitor from getting started page
  • [job] [logs] Scout Lane #18 - stateful-classic / default / local-stateful-classic - StepDetailsPage - displays step detail metrics
  • [job] [logs] FTR Configs #125 / Serverless Common UI - Management Data View Management creating and deleting default data view index pattern deletion "before all" hook for "should return to index pattern list"
  • [job] [logs] FTR Configs #148 / serverless search UI - search features Console Notebooks has notebooks view available

cc @chrisbmar

@pgayvallet pgayvallet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment on lines +80 to +83
const sanitizedTools =
provider === InferenceEndpointProvider.GoogleVertexAI
? sanitizeToolSchemasForVertex(tools)
: tools;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would move that inside createEndpointRequest, better isolation of concerns ihmo

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simple condition, if it ever expands to more providers, definitely makes sense, gonna merge now to unblock.

Comment on lines +70 to +78
/**
* Rebuilds a tool schema keeping only the fields Vertex AI accepts, so that
* JSON Schema keywords zod v4 (or user-provided tool schemas) emit never reach
* Vertex's parser. Conversions preserve the intent of dropped fields:
* `const: 'x'` becomes `enum: ['x']`, `oneOf` branches become `anyOf`, and a
* `null` branch in `anyOf` (zod's representation of nullable values) becomes
* `nullable: true`.
*/
const toVertexSchema = <T extends ToolSchemaType>(schemaPart: T): T => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I really hate that we have to do this, it's crazy that they didn't improve this in 2 years, but I don't see an alternative, so...

@pgayvallet

Copy link
Copy Markdown
Contributor

@chrisbmar have you tested the behavior with the (legacy) Kibana gemini connector? I suspect we may want/need to do the same thing here too?

@chrisbmar

Copy link
Copy Markdown
Contributor Author

The deprecated Gemini connector is unaffected because its adapter rebuilds schemas via an allowlist. (toolSchemaToGemini in adapters/gemini/gemini_adapter.ts) - is the same approach this PR applies to the endpoint path, in OpenAI-format instead of the Gemini SDK's.

Yes, see PR description:

The deprecated Gemini connector is unaffected because its adapter rebuilds schemas via an allowlist. (toolSchemaToGemini in adapters/gemini/gemini_adapter.ts) - is the same approach this PR applies to the endpoint path, in OpenAI-format instead of the Gemini SDK's.

@chrisbmar
chrisbmar merged commit 9ab5bdc into elastic:main Aug 14, 2026
44 checks passed
@kibanamachine

Copy link
Copy Markdown
Contributor

Starting backport for target branches: 9.5

https://github.com/elastic/kibana/actions/runs/31784071668

@kibanamachine

Copy link
Copy Markdown
Contributor

💔 All backports failed

Status Branch Result
❌ 9.5 Backport failed because of merge conflicts

Manual backport

To create the backport manually run:

node scripts/backport --pr 284810

Questions ?

Please refer to the Backport tool documentation

@chrisbmar

Copy link
Copy Markdown
Contributor Author

💚 All backports created successfully

Status Branch Result
✅ 9.5

Note: Successful backport PRs will be merged automatically after passing CI.

Questions ?

Please refer to the Backport tool documentation

chrisbmar added a commit that referenced this pull request Aug 14, 2026
…e endpoints (#284810) (#285146)

# Backport

This will backport the following commits from `main` to `9.5`:
- [[inference] Sanitize tool schemas for Google Vertex AI inference
endpoints (#284810)](#284810)

<!--- Backport version: 11.0.2 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT
[{"author":{"name":"Chris","email":"50221462+chrisbmar@users.noreply.github.com"},"sourceCommit":{"committedDate":"2026-08-14T08:28:54Z","message":"[inference]
Sanitize tool schemas for Google Vertex AI inference endpoints
(#284810)","sha":"9ab5bdc4277d70b8e650966afee40fdcf3451827","branchLabelMapping":{"^v9.6.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","backport:version","v9.5.0","v9.6.0"],"title":"[inference]
Sanitize tool schemas for Google Vertex AI inference
endpoints","number":284810,"url":"https://github.com/elastic/kibana/pull/284810","mergeCommit":{"message":"[inference]
Sanitize tool schemas for Google Vertex AI inference endpoints
(#284810)","sha":"9ab5bdc4277d70b8e650966afee40fdcf3451827"}},"sourceBranch":"main","suggestedTargetBranches":["9.5"],"targetPullRequestStates":[{"branch":"9.5","label":"v9.5.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.6.0","branchLabelMappingKey":"^v9.6.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/284810","number":284810,"mergeCommit":{"message":"[inference]
Sanitize tool schemas for Google Vertex AI inference endpoints
(#284810)","sha":"9ab5bdc4277d70b8e650966afee40fdcf3451827"}}]}]
BACKPORT-->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[inference] Agent Builder fails with Google Vertex AI inference endpoints: tool schemas contain JSON Schema keywords Vertex rejects

4 participants