Tous les articles
IA & dev

Spring AI 2.1 : API Responses, upsert de vecteurs et Spring Boot 4.2

Par Elouan Laurent

Spring AI 2.1.0-M1 : qu'apporte ce jalon, et faut-il migrer ?

Spring AI 2.1.0-M1, publié le 25 septembre 2026, est un jalon : une version préliminaire. Il ajoute le support de l'API Responses d'OpenAI, une méthode VectorStore.upsert pour des embeddings déjà calculés et un nouveau modèle de messages. Il passe aussi à Spring Boot 4.2.

La documentation 2.1 porte un bandeau clair : cette version n'est pas stable, et la version stable à utiliser est la 2.0.1. Pour la production, restez donc sur la 2.0.1 ; le jalon sert à tester. Le détail est dans les notes de version.

OpenAiResponsesChatModel : l'API Responses d'OpenAI

Spring AI ajoute OpenAiResponsesChatModel, un second ChatModel OpenAI qui appelle /v1/responses, à côté de celui de Chat Completions. La documentation donne une raison de le choisir. À partir de GPT-5.4, Chat Completions ne gère pas l'appel d'outils avec un effort de raisonnement autre que none ; Responses le gère.

Elle cite aussi des outils exécutés côté OpenAI : recherche web, recherche de fichiers, interpréteur de code, MCP distant et génération d'images.

Un seul réglage choisit l'endpoint :

spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.openai.chat.api=responses
spring.ai.openai.responses.model=gpt-5-mini
spring.ai.openai.responses.reasoning-effort=medium

Avec responses, l'auto-configuration crée un seul bean OpenAiResponsesChatModel et plus de bean OpenAiChatModel. Pour garder les deux, laissez la valeur par défaut (chat-completions) et déclarez le second modèle vous-même. Deux ChatModel rendent l'auto-configuration de ChatClient ambiguë : marquez-en un @Primary.

Si vous visez un serveur compatible OpenAI, comme dans mon article sur OpenRouter, vérifiez qu'il accepte /v1/responses avant de changer la propriété.

Le modèle est sans état. Chaque appel envoie le Prompt complet, et store: false part à chaque requête sans être configurable. Ni previous_response_id ni les conversations côté serveur ne sont gérés.

Les réglages de rétention de données de votre organisation chez OpenAI continuent de s'appliquer à la requête.

Attention à la mémoire de conversation. Aucun dépôt de mémoire ne conserve encore les parties de message, donc seul InMemoryChatMemoryRepository garde la conversation complète, raisonnement compris. Les autres implémentations sont attendues en 2.1.0-M2 ; en attendant, la conversation reste correcte, mais le modèle raisonne depuis zéro à chaque tour.

VectorStore.upsert : écrire des embeddings déjà calculés

add calcule l'embedding de chaque document avec le modèle du store. upsert écrit un vecteur que vous fournissez, joint au document dans un EmbeddedDocument, et ne calcule rien. La documentation cite trois cas :

  • des vecteurs calculés la nuit par une API de traitement par lots, à moitié prix ;
  • un pipeline d'embeddings tenu par une autre équipe ;
  • une migration depuis un système qui exporte texte et vecteurs ensemble.
float[] embedding = ...; // un vecteur calculé ailleurs
Document document = new Document("8f14e45f-ceea-467a-9a3e-5b1c2d6f7a90", "some text",
    Map.of("source", "manual"));
vectorStore.upsert(List.of(new EmbeddedDocument(document, embedding)));

upsert remplace par identifiant : réécrire le même id écrase la ligne, ce qui rend une ingestion rejouable. Il est disponible sur pgvector, Redis, Elasticsearch et Qdrant. Sur les autres stores, l'implémentation par défaut lève UnsupportedOperationException.

Qdrant exige un UUID comme id, et pgvector stocke l'id dans une colonne UUID sauf si vous configurez idType. EmbeddedDocument refuse un vecteur vide ou contenant NaN ou une infinité.

Trois choses doivent concorder, et seule la première est vérifiée pour vous :

  • la largeur : l'index doit avoir la dimension de votre modèle d'embeddings, sinon la première écriture échoue ;
  • l'espace vectoriel : rien n'enregistre quel modèle a produit une ligne. Un autre modèle à la recherche donne des résultats plausibles mais sans sens, sans erreur. La documentation conseille de noter le nom et la version du modèle dans les métadonnées ;
  • la requête : similaritySearch embarque toujours la question avec l'EmbeddingModel du store, qui doit produire des vecteurs de même espace et de même largeur.

MessagePart : un message devient une suite de parties ordonnées

AssistantMessage et UserMessage portent désormais une liste ordonnée de parties : TextPart, ReasoningPart, ToolCallPart, ToolResultPart, MediaPart et UnknownPart, accessibles par getParts(). Un tour d'assistant n'est plus un message, mais une suite : le raisonnement, les appels d'outils qu'il a produits, puis le texte. Les accesseurs getText(), getMedia() et getToolCalls() restent comme des vues sur les parties, et getReasoning() s'ajoute.

La raison tient au raisonnement chiffré. Un modèle de raisonnement renvoie sa chaîne de pensée sous forme d'un bloc illisible, à rendre tel quel à la requête suivante. L'omettre ne produit aucune erreur : seulement de moins bonnes réponses, plus de tokens et du travail répété.

La documentation annonce qu'aucun changement de code n'est requis. Elle liste pourtant des changements de comportement à vérifier :

  • getMedia() et getToolCalls() renvoient des listes non modifiables : ajoutez par le builder ;
  • equals() et hashCode() comparent les parties, donc les médias comptent dans l'égalité ;
  • getText() ne renvoie null que pour un message sans aucune partie ;
  • les champs protégés textContent, media et responses sont conservés une version, puis retirés.

En streaming, un fragment n'a jamais de texte null, et les appels d'outils restent mis en tampon par défaut.

Spring Boot 4.2 et ce qu'il reste avant la version finale

Spring AI 2.1.0-M1 passe à Spring Boot 4.2, dont le jalon 4.2.0-M2 est sorti le 24 septembre. Le jalon GitHub de la 4.2.0-RC1 est daté du 22 octobre. C'est un calendrier de travail, pas une annonce de date de sortie.

La documentation de Spring AI cite aussi un jalon 2.1.0-M2, pour les mémoires de conversation.

Restez au moins sur la 2.0.1 pour une raison de sécurité. Elle corrige la CVE-2026-59318, publiée le 20 août 2026 avec une sévérité moyenne. À la suite d'une injection de prompt, un outil non proposé à la requête pouvait être appelé.

Le correctif en 1.1.9 et 1.0.10 est réservé au support entreprise.

Que faire d'ici la version finale ?

Mon avis : ce jalon apporte deux choses utiles, l'API Responses pour les modèles de raisonnement avec outils et l'upsert pour les ingestions rejouables. Je n'ai pas exécuté M1 : tout ce qui précède vient des notes de version, des demandes de fusion et de la documentation 2.1.

Trois choses à faire :

  1. Garder la production sur la 2.0.1, comme le recommande la documentation 2.1, et appliquer les correctifs de sécurité.
  2. Tester M1 dans une branche si vous utilisez des modèles de raisonnement OpenAI avec des outils, ou des embeddings calculés hors de l'application.
  3. Vérifier votre mémoire de conversation. Tant que les dépôts persistants ne gardent pas les parties de message, un agent à raisonnement perd sa continuité.

Vous voulez brancher un modèle de langage ou un agent IA sur une application Java / Spring Boot existante ? Je développe sur cette stack depuis 2015. Réserver un appel découverte de 30 minutes.

À lire ensuite