Skip to content

fix: draft-technical 422 on /predict, mocked-service crash; feat: opt-in recommendation mode#43

Open
sleibach wants to merge 5 commits into
cap-js:mainfrom
sleibach:main
Open

fix: draft-technical 422 on /predict, mocked-service crash; feat: opt-in recommendation mode#43
sleibach wants to merge 5 commits into
cap-js:mainfrom
sleibach:main

Conversation

@sleibach

Copy link
Copy Markdown
Contributor

This PR bundles two fixes and one feature for the recommendations plugin. Each is an independent commit; happy to split into separate PRs if preferred.


1. fix: exclude association elements from RPT-1 prediction rows (5e55030)

The draft branch of the recommendations READ handler builds its column list from all elements of req.target.actives, filtering out only audit fields, cds.LargeBinary and cds.Vector. Association/composition elements pass the filter — including the compiler-added DraftAdministrativeData on draft-enabled entities. These have no RPT-1 dtype (cdsToPythonDtype returns undefined), so they are omitted from data_schema but still present in rows. Whenever other edit drafts of the entity exist, context rows resolve DraftAdministrativeData to a non-scalar value and AI Core rejects the whole payload:

422 {"detail":[{"loc":["rows",7,"DraftAdministrativeData","str"],"msg":"Input should be a valid string",...}]}

The error is swallowed (empty predictions), so recommendations silently disappear while every draft READ with $expand=SAP_Recommendations still pays a full failed round-trip to AI Core — observed as recurring latency on a production-adjacent stage with concurrent drafts.

Repro: open an edit draft of an entity with @UI.Recommendations, then READ another draft of that entity with $expand=SAP_Recommendations.

Fix: skip isAssociation elements when building the column list. Flattened foreign keys (author_ID, …) are separate scalar elements and remain included — they are the actual prediction targets and features. A regression test opens an edit draft (so an active context row carries draft administrative data) and asserts the rows handed to fetchPredictions contain no association keys or non-scalar values; it fails on main and passes with the fix.


2. feat: opt-in recommendation mode (533e7ac)

Today the plugin is opt-out: every value-helped field auto-enrolls, and each unwanted one must be silenced with @UI.RecommendationState : 0. On large existing models this enrolls dozens of fields at once and generates unnecessary /predict traffic. New config flag:

// cds.requires
"AICore": { "recommendations": "opt-in" }
  • "auto" (default): unchanged behavior — absence of the flag is byte-for-byte identical to today.
  • "opt-in": a field is enrolled only if it carries a truthy @UI.RecommendationState (1 or a dynamic expression); value-helped fields without it stay out.

@UI.RecommendationState : 0 wins in both modes; entities with zero enrolled fields get no SAP_Recommendations companion (existing guard). Tests boot the bookshop in opt-in mode in its own process; README and CHANGELOG updated.


3. fix: skip mocked services and guard undefined req.target (2b3fc60)

cds-plugin.js registered the recommendations READ handler on every served service, including external services running with a mock fallback (srv.mocked). A free-form READ on such a service (e.g. srv.send(new cds.Request({ method, path }))) has no req.target, so the handler crashed with a TypeError on req.target.isDraft. Mocked services are now skipped at registration, and the handler additionally bails out early when req.target is undefined (defense in depth).


Verification

  • npm test: 18/18 (node --test, sqlite bookshop + MockAICoreService), including the new 422 regression test and 3 opt-in mode tests
  • eslint and prettier --check clean

@sleibach
sleibach requested a review from a team as a code owner July 20, 2026 23:56

@SirSimon04 SirSimon04 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.

Hi @sleibach, thank you for your contribution! In general, it looks really good and sounds like a good feature to us.

Could you remove those big comments or shorten them? Most of the times, the code speaks for itself

Comment thread cds-plugin.js Outdated
// @sap/cds/lib/srv/cds-serve.js (`if (d.is_external) srv.mocked = true`).
// Without this filter, the handler below crashes on `req.target.isDraft`
// for free-form remote calls like `srv.send(new cds.Request({ path }))`.
if (srv.mocked) continue;

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.

Can you check whether the service is external right on the service? Because remote services can exist without mocking, and relying on mocked instead may lead to confusion.

Soeren Leibach added 3 commits July 26, 2026 21:29
…ndler

The recommendations READ handler was attached to every non-db service,
including external services. Free-form remote calls can have no resolved
entity target, so dereferencing req.target aborts the calling flow.

Skip services whose CAP definition is marked external. Unlike srv.mocked,
the definition-level check covers both mocked external services and real
remote service clients with credentials.

Keep an early req.target guard as defense in depth for free-form READs on
any other service.
The draft branch of the recommendations READ handler builds its column
list from all elements of req.target.actives, filtering out only audit
fields, LargeBinary and Vector. Association/composition elements pass
the filter — including the compiler-added DraftAdministrativeData on
draft-enabled entities.

Those elements have no RPT-1 dtype (cdsToPythonDtype returns undefined),
so they are omitted from data_schema but still present in the rows sent
to /predict. As soon as context rows resolve DraftAdministrativeData to
a non-scalar value (i.e. whenever other edit drafts of the entity
exist), AI Core rejects the whole payload with HTTP 422:

  {"detail":[{"loc":["rows",7,"DraftAdministrativeData","str"],
    "msg":"Input should be a valid string",...}]}

The error is swallowed (empty predictions returned), so recommendations
silently disappear while every draft READ with $expand=SAP_Recommendations
still pays a full failed round-trip to AI Core.

Fix: skip elements with isAssociation when building the column list.
Flattened foreign keys (e.g. author_ID) are separate scalar elements and
remain included — they are the actual prediction targets and features.

Adds a regression test that opens an edit draft (so an active context
row carries draft administrative data) and asserts the rows handed to
fetchPredictions contain no association keys or non-scalar values.
The plugin is opt-out today: every field with a value help auto-enrolls
for recommendations and each unwanted one must be silenced individually
with @UI.RecommendationState : 0. On large existing models this enrolls
dozens of fields at once and generates unnecessary /predict traffic.

Add a config flag to invert the default for controlled rollouts:

  "AICore": { "recommendations": "opt-in" }

- "auto" (default): unchanged behavior — value-helped fields
  auto-enroll, @UI.RecommendationState : 0 excludes.
- "opt-in": a field is enrolled only if it carries a truthy
  @UI.RecommendationState (1 or a dynamic expression); everything else,
  including value-helped fields, stays out.

@UI.RecommendationState : 0 wins in both modes, and entities with zero
enrolled fields get no SAP_Recommendations companion (existing guard).
Absence of the flag is byte-for-byte identical to current behavior.

Includes tests booting the bookshop in opt-in mode (own process under
node --test), README docs and a CHANGELOG entry.
@sleibach

sleibach commented Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @SirSimon04 for the feedback! External services are now detected directly, and the comments have been shortened.

Skip virtual, @Core.Computed, and @AI.Feature:false fields from prediction
rows without removing them from the OData API.
@sleibach
sleibach deployed to pr-approval July 26, 2026 20:52 — with GitHub Actions Active
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants