From dabcf3503c1951f6c6daab60323d4ca95bc53656 Mon Sep 17 00:00:00 2001 From: Jeroen Date: Thu, 30 Apr 2026 15:17:40 +0200 Subject: [PATCH] Strip non-schema message fields for Mistral chat requests Mistral's chat completions API uses strict Pydantic validation (extra="forbid") and rejects internal bookkeeping fields (tokenCount, kind, source) that the prompt pipeline attaches to message objects. Add a headless _core/mistral module with shared endpoint detection and a body-rewrite helper, wired through extension hooks on prepareOnscreenAgentApiRequest and prepareAdminAgentApiRequest. Non-Mistral requests are untouched. --- app/L0/_all/mod/_core/mistral/AGENTS.md | 34 ++++++++++++ .../end/mistral.js | 15 ++++++ .../end/mistral.js | 15 ++++++ app/L0/_all/mod/_core/mistral/request.js | 53 +++++++++++++++++++ 4 files changed, 117 insertions(+) create mode 100644 app/L0/_all/mod/_core/mistral/AGENTS.md create mode 100644 app/L0/_all/mod/_core/mistral/ext/js/_core/admin/views/agent/api.js/prepareAdminAgentApiRequest/end/mistral.js create mode 100644 app/L0/_all/mod/_core/mistral/ext/js/_core/onscreen_agent/api.js/prepareOnscreenAgentApiRequest/end/mistral.js create mode 100644 app/L0/_all/mod/_core/mistral/request.js diff --git a/app/L0/_all/mod/_core/mistral/AGENTS.md b/app/L0/_all/mod/_core/mistral/AGENTS.md new file mode 100644 index 00000000..a717881a --- /dev/null +++ b/app/L0/_all/mod/_core/mistral/AGENTS.md @@ -0,0 +1,34 @@ +# AGENTS + +## Purpose + +`_core/mistral/` owns Mistral-specific frontend request customization. + +It is a headless helper module. It does not own chat UI or prompt assembly. It owns reusable Mistral endpoint detection plus extension files that patch API-mode chat requests for the first-party chat surfaces. + +Mistral's chat completions API uses strict Pydantic validation (`extra="forbid"`) and rejects any field on a message object that is not part of its defined schema. Space Agent's prompt pipeline attaches internal bookkeeping fields such as `tokenCount`, `kind`, and `source` to message objects for prompt budgeting and UI display. These fields must be stripped before the request reaches Mistral. This module performs that strip at the API-request seam. + +Documentation is top priority for this module. After any change under this subtree, update this file and any affected parent or consumer docs in the same session. + +## Ownership + +This module owns: + +- `request.js`: shared Mistral endpoint detection and request-body mutation helpers +- `ext/js/_core/onscreen_agent/api.js/prepareOnscreenAgentApiRequest/end/mistral.js`: overlay-chat API request customization +- `ext/js/_core/admin/views/agent/api.js/prepareAdminAgentApiRequest/end/mistral.js`: admin-chat API request customization + +## Local Contracts + +- this module contributes behavior only through JS extension hooks and shared helpers; it must not fork or duplicate the admin or onscreen chat runtimes +- Mistral detection should use the configured upstream API endpoint, not the proxied fetch URL, because frontend fetches may be rerouted through `/api/proxy` +- the two shipped extension hooks may mutate the prepared API request object, including headers, body, URL, method, or extra fetch-init fields, but they must leave non-Mistral requests untouched +- the body rewrite preserves only the message fields accepted by Mistral's schema: `role`, `content`, `name`, `tool_calls`, `tool_call_id` +- provider-specific HTTP policy belongs here or in similar headless provider modules, not hard-coded into `_core/onscreen_agent/llm.js` or `_core/admin/views/agent/api.js` + +## Development Guidance + +- keep provider detection small and explicit +- prefer one shared helper for endpoint matching and body mutation so the admin and onscreen hooks stay in sync +- if Mistral expands its accepted message schema, extend `ALLOWED_MESSAGE_FIELDS` in `request.js` rather than removing the strip entirely +- if additional Mistral request shaping is needed later (e.g. header mutation, endpoint rewriting), extend the prepared request object here instead of reintroducing per-surface hard-coded branches diff --git a/app/L0/_all/mod/_core/mistral/ext/js/_core/admin/views/agent/api.js/prepareAdminAgentApiRequest/end/mistral.js b/app/L0/_all/mod/_core/mistral/ext/js/_core/admin/views/agent/api.js/prepareAdminAgentApiRequest/end/mistral.js new file mode 100644 index 00000000..f675735b --- /dev/null +++ b/app/L0/_all/mod/_core/mistral/ext/js/_core/admin/views/agent/api.js/prepareAdminAgentApiRequest/end/mistral.js @@ -0,0 +1,15 @@ +import { applyMistralBodyRewrite, isMistralEndpoint } from "/mod/_core/mistral/request.js"; + +export default async function mistralAdminRequestHook(hookContext) { + const apiRequest = hookContext?.result; + + if (!apiRequest || typeof apiRequest !== "object") { + return; + } + + if (!isMistralEndpoint(apiRequest.apiEndpoint || apiRequest.settings?.apiEndpoint || "")) { + return; + } + + hookContext.result = applyMistralBodyRewrite(apiRequest); +} diff --git a/app/L0/_all/mod/_core/mistral/ext/js/_core/onscreen_agent/api.js/prepareOnscreenAgentApiRequest/end/mistral.js b/app/L0/_all/mod/_core/mistral/ext/js/_core/onscreen_agent/api.js/prepareOnscreenAgentApiRequest/end/mistral.js new file mode 100644 index 00000000..d809a4c5 --- /dev/null +++ b/app/L0/_all/mod/_core/mistral/ext/js/_core/onscreen_agent/api.js/prepareOnscreenAgentApiRequest/end/mistral.js @@ -0,0 +1,15 @@ +import { applyMistralBodyRewrite, isMistralEndpoint } from "/mod/_core/mistral/request.js"; + +export default async function mistralOnscreenRequestHook(hookContext) { + const apiRequest = hookContext?.result; + + if (!apiRequest || typeof apiRequest !== "object") { + return; + } + + if (!isMistralEndpoint(apiRequest.apiEndpoint || apiRequest.settings?.apiEndpoint || "")) { + return; + } + + hookContext.result = applyMistralBodyRewrite(apiRequest); +} diff --git a/app/L0/_all/mod/_core/mistral/request.js b/app/L0/_all/mod/_core/mistral/request.js new file mode 100644 index 00000000..3a244559 --- /dev/null +++ b/app/L0/_all/mod/_core/mistral/request.js @@ -0,0 +1,53 @@ +const MISTRAL_HOST = "mistral.ai"; + +const ALLOWED_MESSAGE_FIELDS = ["role", "content", "name", "tool_calls", "tool_call_id"]; + +export function isMistralEndpoint(endpoint = "") { + const normalizedEndpoint = String(endpoint || "").trim(); + + if (!normalizedEndpoint) { + return false; + } + + try { + const url = new URL(normalizedEndpoint, globalThis.location?.origin || "http://localhost"); + return url.hostname === MISTRAL_HOST || url.hostname.endsWith(`.${MISTRAL_HOST}`); + } catch { + return normalizedEndpoint.includes(MISTRAL_HOST); + } +} + +function stripMessage(message) { + if (!message || typeof message !== "object") { + return message; + } + + const stripped = {}; + + for (const field of ALLOWED_MESSAGE_FIELDS) { + if (message[field] !== undefined) { + stripped[field] = message[field]; + } + } + + return stripped; +} + +export function applyMistralBodyRewrite(apiRequest = {}) { + const requestBody = + apiRequest?.requestBody && typeof apiRequest.requestBody === "object" + ? apiRequest.requestBody + : null; + + if (!requestBody || !Array.isArray(requestBody.messages)) { + return apiRequest; + } + + return { + ...apiRequest, + requestBody: { + ...requestBody, + messages: requestBody.messages.map((message) => stripMessage(message)) + } + }; +}