Imaginons un service qui envoie des notes de réparation à un modèle DeepSeek et attend en retour un objet JSON : ce qui a été inspecté, ce qui a été recommandé, si le client a donné son accord, si le travail a été fait. Aujourd’hui, il appelle deepseek-chat, deepseek-flash ou une route OpenRouter, et vous voulez le faire tourner sur DeepSeek V4.1 Flash via SpicyAPI (spicyapi.ai).
Quatre modifications suffisent : utiliser l’ID de modèle deepseek/v4.1-flash/chat, supprimer chaque champ que ce modèle n’accepte pas, choisir un reasoning_effort et laisser à max_tokens une vraie marge, car le raisonnement est payé sur ce même budget. Exécutez ensuite un petit jeu de test avant le trafic réel. Utilisez la page du modèle V4.1 Flash pour essayer la requête.
Ce qu’il faut changer dans la requête
L’URL de base, la clé et l’ID du modèle sont la partie facile (guide de migration). C’est le corps de la requête qui pose problème :
| Dans votre client actuel | Sur SpicyAPI |
|---|---|
URL de base https://api.deepseek.com et une clé DeepSeek | https://api.spicyapi.ai/v1 et une clé qui commence par sk-spicy- |
Modèle deepseek-flash, ou les anciens deepseek-chat, deepseek-reasoner, deepseek-v4-flash, deepseek-v4-pro | deepseek/v4.1-flash/chat |
thinking: {"type": "disabled"}, le comportement qu’offrait deepseek-chat | reasoning_effort: "none" saute l’étape de raisonnement |
| Thinking activé (par défaut) | Omettez reasoning_effort pour high, ou réglez low pour le travail en volume |
reasoning_effort à max, medium, minimal, xhigh | Seuls none, low et high sont acceptés ; toute autre valeur renvoie 400 invalid_request |
temperature, top_p, presence_penalty, frequency_penalty | À supprimer ; 400 unsupported_parameter |
parallel_tool_calls | À supprimer ; non exposé sur ce modèle |
stop | À conserver : jusqu’à 4 séquences, et le texte correspondant est exclu de la réponse |
Parties image_url ou fichier | Conservez les parties image_url pour les images ; retirez les parties PDF/fichier et extrayez d’abord leur texte |
OpenRouter models, route, provider, usage, transforms, plugins et un objet reasoning | Supprimez les six premiers ; remplacez reasoning par reasoning_effort au niveau supérieur |
Ce modèle hébergé n’offre aucun contrôle d’échantillonnage, même avec reasoning_effort: "none". Validez les résultats de la tâche plutôt que de compter sur temperature: 0 pour la répétabilité.
Ce qui reste identique : messages (contenu texte ou image, jusqu’à 256), tools (jusqu’à 128 définitions) avec tool_choice (auto, none, required ou une fonction nommée), response_format avec {"type": "json_object"}, stop, max_tokens (32 768 par défaut, 384 000 au maximum) et une fenêtre d’entrée de 1 048 576 tokens qui compte chaque message envoyé, y compris le prompt système et les résultats d’outils précédents.
SpicyAPI n’ignore jamais un champ en silence : l’erreur 400 nomme le champ et rien n’est facturé. L’API propre de DeepSeek accepte et ignore temperature et les champs de pénalité en mode thinking, si bien qu’un ancien client peut les traîner sans que personne ne le remarque.
L’échec discret, c’est le nom du modèle. Sur l’API propre de DeepSeek, V4 Flash a été retiré et deepseek-v4-flash redirige temporairement vers V4.1 Flash ; le nom actuel est deepseek-flash. V4 Pro est un cas différent : DeepSeek avait d’abord annoncé un basculement vers Flash le 14 septembre, puis son journal des modifications a confirmé que le service V4 Pro continuerait avec une facturation inchangée. Ne migrez pas en supposant que tous les anciens appels Pro tournent déjà sur Flash.
Sur SpicyAPI, les trois routes de chat DeepSeek ont chacune un contrat publié distinct : garder deepseek/v4-flash/chat dans votre configuration est une requête valide, avec un plafond de sortie de 65 536 tokens et des prix plus élevés. Un alias du service natif ne change pas ce contrat hébergé. Utilisez explicitement deepseek/v4.1-flash/chat pour cette migration.
Cette route accepte les images via des parties image_url, mais pas les parties PDF/fichier. Extrayez le texte d’un PDF avant de l’envoyer ; les exemples de notes de réparation en texte seul ci-dessous n’ont pas besoin d’image.
Laisser de la place à la réponse
Le modèle raisonne avant de répondre, et ce raisonnement est prélevé sur max_tokens. L’effort par défaut est high, et un plafond de 120 tokens n’a renvoyé aucun contenu. Le max_tokens par défaut est de 32 768, et un livrable plus long est tronqué. Dimensionnez le plafond d’après la réponse visible plus plusieurs milliers de tokens de marge de réflexion, pas d’après le maximum de 384 000.
Commencez l’extraction avec un effort Low. S’il manque une distinction nécessaire, essayez High sur cet enregistrement plutôt que de renchérir chaque ligne. Pour les grandes entrées, utilisez l’interface de streaming.
Essayer d’abord dans Playground
- Ouvrez DeepSeek V4.1 Flash, choisissez Playground et réglez le contrôle d’effort sur Low.
- Placez la consigne de forme JSON du script ci-dessous dans System prompt et la note de réparation R17 dans Message to the model, puis cliquez sur Run model.
- Vérifiez qu’un accord en attente ne devient pas une réparation terminée avant de migrer votre application.
Exécuter un jeu de test qui attrape l’erreur coûteuse
Une note de réparation fait un bon jeu de test, car un résumé fluide peut être faux d’une façon qui coûte de l’argent. « Remplacement recommandé » n’est pas un accord du client, et l’accord n’est pas un travail terminé.
Mettez une clé de API Keys dans SPICY_API_KEY, enregistrez ce script sous check_repair.py et lancez python3 check_repair.py. Le prompt système montre la forme du JSON et contient le mot « json », que demande le guide du mode JSON de DeepSeek.
import json
import os
import uuid
import urllib.request
import re
SOURCE = (
'Repair note R17: "Inspected the rear brake pads. Recommend replacement before '
'the next long ride. Customer approval is pending." No later work record is supplied.'
)
KEYS = {"inspected", "recommended_action", "authorized", "completed", "evidence"}
REQUIRED_QUOTES = (
"Inspected the rear brake pads.",
"Recommend replacement before the next long ride.",
"Customer approval is pending.",
)
def check(response: dict) -> list[str]:
choice = response["choices"][0]
problems = []
if choice.get("finish_reason") != "stop":
problems.append(f"finish_reason is {choice.get('finish_reason')!r}: the reply may be cut off")
content = (choice.get("message") or {}).get("content")
if not content or not content.strip():
return problems + ["empty content: reasoning may have used the whole max_tokens budget"]
try:
data = json.loads(content)
except json.JSONDecodeError as error:
return problems + [f"not valid JSON: {error}"]
if not isinstance(data, dict):
return problems + ["JSON must be an object"]
if missing := KEYS - data.keys():
problems.append(f"missing keys: {sorted(missing)}")
for key in ("inspected", "authorized", "completed"):
if data.get(key) is not None and type(data.get(key)) is not bool:
problems.append(f"{key} must be true, false or null")
if data.get("inspected") is not True:
problems.append("inspected should be true")
if data.get("authorized") is not False:
problems.append("authorized must be false: approval is pending")
if data.get("completed") is not None:
problems.append("completed must be null: no completion record is supplied")
action = data.get("recommended_action")
if not isinstance(action, str) or not action.strip():
problems.append("recommended_action must be a non-empty string")
else:
words = set(re.findall(r"[a-z]+", action.lower()))
# A lexical screen, not a proof of semantic equivalence.
required = {"rear", "long", "ride"}
compatible = (required <= words
and bool(words & {"replace", "replacement", "replacing"})
and bool(words & {"pad", "pads"})
and not words & {"not", "no", "never", "front", "after"})
if not compatible:
problems.append("review recommendation wording against R17")
quotes = data.get("evidence")
if not isinstance(quotes, list) or not quotes:
problems.append("evidence must be a non-empty list of quotes")
else:
problems += [f"invalid or unmatched quote: {q!r}" for q in quotes
if not isinstance(q, str) or not q.strip() or q not in SOURCE]
for expected in REQUIRED_QUOTES:
if not any(isinstance(q, str) and expected in q for q in quotes):
problems.append(f"missing supporting quote: {expected}")
return problems
body = {'model': 'deepseek/v4.1-flash/chat',
'messages': [{'role': 'system',
'content': 'Extract only facts stated in the source. Reply with one '
'JSON object shaped like {"inspected": null, '
'"recommended_action": null, "authorized": null, '
'"completed": null, "evidence": []}. inspected, '
'authorized and completed are true, false or null; use '
'null for anything the source does not state. evidence '
'lists exact quotes from the source that support every '
'non-null value.'},
{'role': 'user',
'content': 'Repair note R17: "Inspected the rear brake pads. '
'Recommend replacement before the next long ride. '
'Customer approval is pending." No later work record is '
'supplied.'}],
'max_tokens': 2048,
'reasoning_effort': 'low',
'response_format': {'type': 'json_object'}}
request = urllib.request.Request(
"https://api.spicyapi.ai/v1/chat/completions",
data=json.dumps(body).encode(),
headers={"Authorization": "Bearer " + os.environ["SPICY_API_KEY"],
"Content-Type": "application/json",
"Idempotency-Key": str(uuid.uuid4())},
)
with urllib.request.urlopen(request, timeout=90) as reply:
response = json.load(reply)
problems = check(response)
print(json.dumps(response.get("usage", {})))
if problems:
raise RuntimeError("; ".join(problems))
print(response["choices"][0]["message"]["content"])La décision attendue : inspecté, accord toujours en attente, achèvement inconnu. La recommandation doit conserver les plaquettes arrière et l’échéance « avant la prochaine longue sortie ».
Ajouter l’accord et l’achèvement comme jeux de test séparés
Ne vous arrêtez pas au cas de l’accord en attente. Servez-vous de ceux-ci comme modèles pour d’autres jeux de test : mettez à jour SOURCE, REQUIRED_QUOTES et les valeurs attendues d’accord et d’achèvement avant de lancer chacun. Le vérificateur de R17 ne s’exécute pas tel quel sur R18 ou R19 :
| Dernier enregistrement | Authorized | Completed |
|---|---|---|
| R17 : “Customer approval is pending.” Aucun enregistrement postérieur | false | null |
| R18 : “Customer approved the quoted replacement. Parts ordered; work scheduled for Thursday.” | true | null |
| R19 : “Replaced pads; road check completed Thursday.” Lié à l’intervention approuvée | true | true |
Conservez avec la source l’ID d’intervention, l’ordre des enregistrements et les dates : R18 remplace l’accord en attente, mais la planification du travail n’établit pas son achèvement. Si une note « terminé » sans date ne peut pas être rattachée à la même intervention, renvoyez le lien non résolu au lieu de la fusionner avec R17.
Cette même séparation en trois se retrouve dans le flux de production d’un créateur : « Approuvez ce design de personnage », « design approuvé » et « vidéo générée à partir du design approuvé » sont une demande, une décision et une exécution. Demandez au prompt d’extraction ces étapes séparément, avec l’enregistrement qui étaye chacune, et conservez tout conflit entre notes au lieu de demander au modèle de choisir la phrase qui sonne le plus sûr.
Séparer accord et achèvement dans les flux avec outils
Un enregistrement d’accord ne prouve pas que la réparation a eu lieu : cherchez l’enregistrement d’achèvement avant de changer le statut, et gardez accord et achèvement comme champs distincts dans ce que renvoient vos outils.
Le chat sans outils n’a besoin de rien de plus. Avec tools dans la requête, un client qui reconstruit chaque message de l’assistant avec seulement un rôle et un contenu passe le premier tour et échoue au second avec une erreur 400, si bien qu’un test à un seul tour masque le bug. Ajoutez le message de l’assistant exactement tel que renvoyé, reasoning_content compris, puis un message tool par appel avec le tool_call_id correspondant, et plafonnez le nombre de tours. Testez deux tours, pas un. Les recettes de chat montrent la boucle complète.
Laisser un Agent préparer l’appel
Pour l’extraction répétée d’enregistrements, demandez à votre Agent de code d’utiliser le Skill SpicyAPI : “Read deepseek/v4.1-flash/chat, then adapt the example to my input; show the request before running it.” La consultation via la CLI est spicyapi models get deepseek/v4.1-flash/chat --json ; via MCP, c’est spicyapi_model_get avec model: "deepseek/v4.1-flash/chat". Les appels de chat passent par un client compatible ou l’API de chat ; les commandes de tâches média ne génèrent pas de réponses de chat.
Recalculer la facture
Les tarifs sont de 0,30 $ par million de tokens d’entrée hors cache, 0,006 $ par million d’entrée en cache et 1,20 $ par million de sortie, un tarif unique quelle que soit la longueur de la requête. Un prompt de 40 000 tokens hors cache avec 2 000 tokens de sortie coûte 0,0144 $. Si 30 000 des tokens d’entrée sont facturés en cache et 10 000 hors cache, le même appel coûte 0,00558 $. Les tokens de raisonnement sont facturés comme de la sortie : une courte réponse visible peut donc consommer plus de sortie que prévu ; none est le seul réglage qui les garde hors de la facture.
Deux comparaisons comptent quand vous quittez l’API propre de DeepSeek. Ses tarifs publics sont les mêmes 0,30 $, 0,006 $ et 1,20 $ aux heures de pointe, mais les heures creuses y sont à moitié prix alors qu’ici le tarif est unique : un lot que vous programmez en heures creuses chez DeepSeek coûte environ deux fois plus sur SpicyAPI pour les mêmes tokens. Et le tarif en cache vaut un cinquantième du tarif d’entrée, mais seulement pour un préfixe identique à l’octet près d’un appel à l’autre : un prompt système de 1 500 tokens sur 10 000 appels représente 15 millions de tokens d’entrée, soit 4,50 $ hors cache et au plus 0,09 $ si chacun était une lecture en cache. Comparez le usage renvoyé et le coût réglé à ces estimations. Un horodatage ou des instructions réordonnées peuvent casser un préfixe partagé ; une entrée inchangée ne garantit pas à elle seule un succès de cache.
Où atterrit chaque route DeepSeek
| V4.1 Flash | V4 Flash | V4 Pro | |
|---|---|---|---|
| ID du modèle | deepseek/v4.1-flash/chat | deepseek/v4-flash/chat | deepseek/v4-pro/chat |
| Sortie max. | 384 000 | 65 536 | 384 000 |
| Entrée / cache / sortie par 1 M | 0,30 $ / 0,006 $ / 1,20 $ | 0,44 $ / 0,014 $ / 1,32 $ | 1,32 $ / 0,044 $ / 3,96 $ |
parallel_tool_calls | Non exposé | Non exposé | Exposé |
Les trois acceptent 1 048 576 tokens d’entrée, un effort none, low ou high (par défaut high) et un max_tokens par défaut de 32 768. En venant de deepseek/v4-flash/chat, le corps de la requête est identique : seul l’ID change, le plafond de sortie est multiplié par près de six et tous les tarifs baissent. En venant de V4 Pro, vous perdez parallel_tool_calls et payez 77 % de moins pour l’entrée et environ 70 % de moins pour la sortie. Savoir si V4 Flash ou V4 Pro convient le mieux à une tâche donnée est une autre décision, traitée dans DeepSeek V4 Flash vs Pro.
Si votre application écrit de la fiction pour adultes
V4.1 Flash est répertorié sans censure sur SpicyAPI, mais ce n’est pas une promesse qu’il traitera toute demande de fiction pour adultes. Lors de nos tests de septembre 2026, les demandes directes de scènes matures intenses sont revenues adoucies à chaque fois, tandis que les présenter comme de la fiction ou poser le cadre dans le prompt système en a rendu davantage en entier, sans que ce soit toujours le cas (détails dans le test de V4.1 Flash). Si vos prompts ont été écrits pour un modèle qui acceptait les demandes brutes, déplacez le cadrage dans le message système pendant la migration et gardez chaque personnage adulte.
Corriger directement les échecs de migration
| Symptôme | Cause probable | Correction |
|---|---|---|
400 unsupported_parameter nommant temperature, top_p, une pénalité, parallel_tool_calls ou thinking | Champ hérité de l’ancien client | Le supprimer |
content est vide ou finish_reason vaut length | max_tokens consommé par le raisonnement, ou la réponse l’a dépassé | Augmenter max_tokens, baisser l’effort, réessayer |
| Le JSON s’analyse mais déclare un travail terminé | Recommandation lue comme un achèvement | Garder des champs séparés, exiger des citations, lancer le vérificateur |
| Erreur au second tour d’une conversation avec outils | Message de l’assistant reconstruit sans reasoning_content | Ajouter le message renvoyé tel quel |
| 402 malgré un solde suffisant | Le blocage (hold) 40201 dépasse le solde, ou le plafond de dépenses 40202 est atteint | Inspecter l’erreur : réduire un budget de sortie excessif pour le blocage, ou vérifier les plafonds de la clé ou de l’espace de travail |
Idempotency-Key réutilisée rejetée (400 ou 409) | La clé appartient à une requête terminée ou à un autre corps | Un 400 pour une requête terminée ne peut pas rejouer la réponse ; un 409 peut signaler un corps modifié. Ne réutilisez la clé que pour une relance réseau identique dans sa fenêtre ; vérifiez le règlement de la tâche avant de choisir un nouvel appel payant |
| Un long appel sans streaming meurt vers 100 secondes | Coupure en périphérie du réseau | Diffuser la réponse en streaming |
Basculer sans perdre l’ancienne référence
Gardez l’ancien ID de modèle dans la configuration et lancez le jeu de test sur les deux. Basculez le trafic une fois que le test passe avec le nouvel ID, que vos propres sources se valident et que le usage journalisé correspond au calcul ci-dessus.








