Nous avons cessé d'utiliser les SDK clients de Stripe, WorkOS et Slack, et sommes en train de migrer le reste. À la place, nous appelons leurs API REST directement via une petite classe wrapper que nous appelons HttpBaseClient.
Cela semble contre-intuitif. Les SDK sont censés vous faire gagner du temps. Mais vous entendez parler de l'IA qui porte davantage de code vers une exécution native. La même idée s'applique aux SDK. Pourquoi ?
- Les SDK masquent les détails bruts, comme les en-têtes de réponse HTTP et les corps de réponse bruts, qui sont essentiels pour les agents qui déboguent des problèmes ou envoient des détails à l'équipe amont pour investigation.
- Ils s'attendent à des réponses correctes, mais en réalité, des réponses malformées reviennent des pare-feux, des équilibreurs de charge et des passerelles. Cela masque les détails de débogage dont nous avons besoin.
- Les SDK contournent la fonction de contrainte d'un point d'entrée centralisé pour les tentatives, la gestion des erreurs et l'observabilité, ce qui permet aux agents de réécrire leurs propres modèles.
- Ils transportent un lourd surplus, généralement auto-généré à partir d'OpenAPI, alors que nous n'utilisons qu'un tout petit sous-ensemble de points de terminaison.
Pensez à pourquoi les SDK ont commencé. Ils sont nés parce que les entreprises voulaient rendre l'adoption facile : fournir une bibliothèque partagée, faire gagner du temps aux clients et masquer le travail d'intégration répétitif. Mais l'IA a changé cette courbe de coût. Intégrer un SDK est désormais souvent autant de travail que d'appeler l'API HTTP directement, et il est peu coûteux d'écrire un client REST sur mesure pour exactement les points de terminaison que nous utilisons, tout en offrant une meilleure observabilité unifiée.
Le jeu sans fin de la taupe

De si charmantes réponses « JSON »
Voici à quoi ressemblait notre tableau de bord d'erreurs Sentry. Nous constations qu'un faible pourcentage de requêtes échouaient sur un chemin de code, puis nous appliquions un monkey-patch juste sur ce chemin pour gérer l'incident du SDK pour l'utilisateur. Chaque jour, un autre chemin de code déclenchait une erreur inattendue ; nous corrigions celui-ci aussi, et le jeu ne finissait jamais.
Les SDK sont fragiles dans un mode de défaillance courant en production : le serveur dit que quelque chose s'est mal passé, mais la réponse n'a pas la forme JSON attendue par le SDK.
Une passerelle Nginx surchargée renvoie du HTML inattendu. Cloudflare bloque une requête. Un pare-feu affiche une page de limitation de débit. L'API du fournisseur est généralement en JSON, mais ce qui se trouve devant elle n'est pas toujours l'API du fournisseur.
Lorsque cela se produit, de nombreux SDK tentent d'analyser la réponse, échouent avec une erreur d'analyse générique et jettent les informations utiles. Le corps brut est perdu. Le texte de statut est enterré. Les en-têtes HTTP ne sont souvent pas renvoyés de manière utilisable. Si le support du fournisseur demande un ID de requête à partir d'un en-tête HTTP, nous n'avons pas de chance car le SDK ne le renvoie pas.
Les agents abusent de la simplicité des SDK
Nos appels aux SDK clients étaient comme le Far West. Stripe avait un modèle. WorkOS en avait un autre. Slack avait ses propres bizarreries. D'autres intégrations utilisaient du fetch brut, des appels SDK, une logique de tentative unique, ou aucune logique de tentative du tout.
Les SDK rendaient la mauvaise chose facile. Un agent pouvait toujours aller directement chez le fournisseur et appeler \stripe.customers.create(...)\ depuis n'importe quelle route. Cela semble productif, mais cela contourne l'endroit partagé où l'authentification, les tentatives, les métriques, les journaux et la traduction des erreurs devraient résider. Nous avions des wrappers de fermeture comme celui-ci éparpillés dans notre codebase :
1const response = await catchRateLimitError(() =>2 stripe.customers.retrieve(stripeCustomerId)3);
Si vous manquiez un endroit et oubliez d'envelopper l'appel SDK, vous passiez une mauvaise journée. Une limitation de débit de Stripe et une limitation de débit de WorkOS signifient la même chose pour notre produit : le fournisseur nous demande de ralentir. Mais au niveau des types, ils étaient des objets totalement différents. Certains codes attrapaient des exceptions spécifiques au SDK. D'autres attrapaient l'erreur générique Error. Certains n'attrapaient rien. Cela s'est transformé en jeu de la taupe Sentry : corriger un 429 à un endroit, attendre la même classe d'échec ailleurs.
Les SDK sont gonflés
Les paquets NPM openai et anthropic-ai/sdk sont auto-générés à partir de spécifications OpenAPI par Stainless. Stripe est également généré à partir de sa spécification OpenAPI. C'est ainsi que l'on peut maintenir à l'échelle un SDK public pour une grande API dans des dizaines de langages de programmation.
Mais un bon SDK public doit servir tout le monde. Notre backend n'a pas besoin du SDK de tout le monde. Il a besoin de nos huit points de terminaison Stripe, de nos points de terminaison utilisateur et organisation WorkOS, et des méthodes Slack que nous appelons réellement. Stripe pèse 6,5 Mo, workos-inc/node 6,9 Mo, slack/web-api 7,7 Mo, et linear/sdk 34 Mo. Dans l'extrême, googleapis atteint 198 Mo.
Les SDK générés apportent toute la plateforme avec eux : des centaines de méthodes, des surcharges, des aides à la pagination, un comportement de tentative, la détection d'environnement, des shims de compatibilité, et d'anciennes surfaces qui ne peuvent pas disparaître parce que quelqu'un, quelque part, en dépend encore. Dans notre propre backend, cette généralité est généralement un surplus. Pire, elle se place entre nous et le fil.
Nous oublions que les API HTTP sont des contrats API
Les gens parlent parfois des API HTTP comme si elles étaient des détails d'implémentation de bas niveau et que les SDK étaient la véritable interface stable. L'API REST publique est un contrat. Les fournisseurs ne peuvent pas la briser à la légère. Les auteurs de SDK le savent aussi, car ils ne peuvent pas forcer tous les clients à mettre à jour. De nombreux clients continuent d'exécuter des versions de SDK vieilles de plusieurs années en production, ce qui signifie que l'ancien contrat de fil doit continuer à fonctionner de toute façon.
Notre propre HttpBaseClient
HttpBaseClient est notre remplacement des SDK clients. Les sous-classes de fournisseurs fournissent les éléments spécifiques au fournisseur : URL de base, en-têtes d'authentification, type de contenu, mappage d'erreurs et méthodes étroites pour les points de terminaison que nous utilisons réellement. HttpBaseClient possède le reste : sérialisation, analyse, erreurs de transport, journaux structurés, métriques, mappage de statut et suivi de durée. Cela unifie l'observabilité afin que chaque fournisseur suive des normes cohérentes. Voici la forme simplifiée :
1abstract class HttpBaseClient<TEndpoint extends string> {2 protected abstract readonly baseUrl: string;34 protected constructor(private readonly dependency: string) {}56 protected abstract buildAuthHeaders(): Promise<Record<string, string>>;78 protected async request<TBody, TResponse>(config: {9 method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';10 path: string;11 endpoint: TEndpoint;12 body?: TBody;13 }): Promise<TResponse> {14 const url = `${this.baseUrl}${config.path}`;15 const headers = await this.buildAuthHeaders();16 const labels = {17 dependency: this.dependency,18 endpoint: config.endpoint,19 method: config.method,20 };21 const start = performance.now();2223 logInfo('upstream request starting', labels);2425 try {26 const response = await callWithMetrics(27 () =>28 fetch(url, {29 method: config.method,30 headers,31 body: config.body === undefined ? undefined : JSON.stringify(config.body),32 }),33 this.dependency,34 labels35 );3637 const body = await parseBody(response);38 if (!response.ok) throw this.mapHttpError(response, body);3940 logInfo('upstream request succeeded', {41 ...labels,42 statusCode: response.status,43 durationMs: performance.now() - start,44 });4546 return body as TResponse;47 } catch (cause) {48 logWarn('upstream request failed', {49 ...labels,50 durationMs: performance.now() - start,51 cause,52 });53 throw cause;54 }55 }56}5758// Ensuite, un wrapper Stripe devient petit et explicite :59enum StripeEndpoint {60 CustomersCreate = 'stripe/customers/create',61}6263class StripeHttpClient extends HttpBaseClient<StripeEndpoint> {64 protected readonly baseUrl = 'https://api.stripe.com';6566 constructor(private readonly apiKey: string) {67 super(ClientVendor.Stripe);68 }6970 createCustomer(body: { email: string; name: string }) {71 return this.request<typeof body, Stripe.Customer>({72 method: 'POST',73 path: '/v1/customers',74 endpoint: StripeEndpoint.CustomersCreate,75 body,76 });77 }7879 // D'autres points de terminaison vont ici.80}
C'est là l'astuce. La classe n'essaie pas de modéliser tout Stripe. Elle modélise le comportement HTTP que nous voulons pour chaque appel fournisseur. Stripe reçoit toujours un encodage de formulaire. WorkOS reçoit toujours une authentification par jeton porteur et des corps JSON. Slack conserve son comportement étrange ok: false sur HTTP 200. Mais le reste de notre backend voit une forme cohérente.
Vous pouvez voir une version plus longue de ce à quoi ressemble notre HttpBaseClient ici, modifiée pour être générique et plus facile à lire en tant qu'exemple de code.
Où nous utilisons encore des SDK
Notre approche actuelle est hybride : utiliser notre propre client HTTP à l'exécution, mais conserver les SDK là où leurs types font encore gagner du temps. StripeHttpClient peut retourner Stripe.Customer, SlackHttpClient peut emprunter les types d'arguments de slack/web-api, et les types WorkOS peuvent encore décrire la réponse sur le fil.
Je m'attends à ce que nous abandonnions cela aussi avec le temps. À mesure que l'IA s'améliore pour générer et maintenir les types de requête et de réponse exacts dont nous avons besoin, la raison de conserver un paquet SDK entier juste pour les types s'affaiblit. Mais le comportement à l'exécution est la partie douloureuse, donc c'est par là que la migration commence.
Nous utilisons encore des SDK lorsque le SDK est la frontière du produit, pas seulement un wrapper autour de REST. L'observabilité est l'exemple le plus clair. Pour Sentry, le SDK gère l'instrumentation à l'exécution, la capture d'erreurs, la propagation de portée, les métadonnées de version, et des intégrations que nous ne voudrions pas développer nous-mêmes. C'est différent de l'utilisation d'un SDK fournisseur comme un client léger pour des appels HTTP backend ordinaires.
Toutes les API ne sont pas du REST sur HTTP, et ce n'est pas grave. Les appels de base de données en sont un bon exemple. Notre abstraction de plus bas niveau est BaseClient : elle donne à chaque client le même contrat de métriques, de journalisation et de gestion d'erreurs, tout en permettant à une classe fille de redéfinir ce que « fetch » signifie pour son transport.
Où nous allons
Les développeurs traiteront la documentation API comme le véritable guide d'intégration et les SDK comme des implémentations de référence : des modèles pour l'authentification, les charges utiles, la pagination, les tentatives et les cas limites. La prochaine version de « ship an SDK » pourrait être « ship an agent skill » qui apprend aux agents comment appeler l'API correctement, réutiliser les bons modèles, et éviter de faire passer chaque appel à l'exécution par un paquet fournisseur.





