Stripe、WorkOS、Slack のクライアント SDK の使用を中止し、残りの部分も移行中です。代わりに、HttpBaseClient という小さなラッパークラスを通して、それらの REST API を直接呼び出しています。
逆のことをしているように聞こえます。SDK は時間を節約してくれるはずです。しかし、AI がより多くのコードをネイティブ実行に移植しているという話は聞いているでしょう。同じことが SDK にも当てはまります。なぜでしょうか?
- SDK は HTTP レスポンスヘッダーや生のレスポンスボディといった生の詳細を隠してしまいます。これらの詳細は、問題のデバッグや上流チームへの調査依頼を行うエージェントにとって不可欠です。
- SDK は正常なレスポンスを期待しますが、実際にはファイアウォール、ロードバランサー、ゲートウェイから不正な形式のレスポンスが返ってくることがあります。そのため、必要なデバッグ情報が隠されてしまいます。
- SDK は、リトライ、エラーハンドリング、可観測性のための集中エントリポイントという強制力を回避するため、エージェントが独自のパターンを書き直すことを許してしまいます。
- SDK は、通常 OpenAPI から自動生成される、重厚なコードを持ち込みますが、私たちが実際に使うのはごく一部のエンドポイントだけです。
そもそもなぜ SDK が生まれたのか考えてみてください。企業が「採用を簡単に感じさせる」ために生まれました。つまり、共有ライブラリを配布し、顧客の時間を節約し、繰り返しの統合作業を隠すためです。しかし、AI がそのコスト曲線を変えました。今や SDK との統合は HTTP API を直接呼び出すのと同じくらいの作業量になることが多く、使用するエンドポイントだけに特化した REST クライアントを書く方が安上がりで、より優れた統一的な可観測性も提供できます。
終わりのないモグラたたきゲーム

なんとも素敵な "JSON" レスポンスですね
これが以前の Sentry エラーダッシュボードの様子です。あるコードパスからのリクエストのごく一部が失敗しているのを発見し、そのパスだけをモンキーパッチして SDK の不具合をユーザーに代わって処理していました。次の日には別のコードパスで予期しないエラーが発生し、またそのパスを修正する。そんなゲームが延々と続きました。
SDK は一般的なプロダクション障害モードに弱いです。サーバーが何か問題が起きたと応答するが、そのレスポンスが SDK の想定する JSON の形ではない、というケースです。
過負荷の Nginx ゲートウェイは予期しない HTML を返します。Cloudflare がリクエストをブロックします。ファイアウォールがレート制限ページを表示します。ベンダー API は通常 JSON ですが、その前にいるものは必ずしもベンダー API とは限りません。
こうした場合、多くの SDK はレスポンスのパースを試み、汎用的なパースエラーで失敗し、有用な情報を捨ててしまいます。生のボディは失われ、ステータステキストは埋もれ、HTTP ヘッダーは利用可能な形で返されないことがよくあります。ベンダーサポートが HTTP ヘッダー内のリクエスト ID を尋ねてきても、SDK がそれを返さないため手の施しようがありません。
エージェントは SDK の直接性を悪用する
クライアント SDK の呼び出しは無法地帯でした。Stripe はあるパターン、WorkOS は別のパターン、Slack は独自の癖がありました。他の統合では生の fetch を使ったり、SDK 呼び出しをしたり、アドホックなリトライロジックがあったり、リトライロジックがまったくなかったりしました。
SDK は間違った方法を簡単に見せていました。エージェントはいつでもベンダークライアントに直接アクセスし、任意のルートから stripe.customers.create(...) を呼び出すことができました。一見生産的に見えますが、認証、リトライ、メトリクス、ログ、エラー変換を一元管理する共通の場所を迂回してしまいます。コードベース中には次のようなクロージャラッパーが散在していました。
1const response = await catchRateLimitError(() =>2 stripe.customers.retrieve(stripeCustomerId)3);
もしある箇所で SDK 呼び出しをラップし忘れると、大変なことになりました。Stripe のレート制限も WorkOS のレート制限も、私たちの製品にとっては同じ意味です。つまり、上流が速度を落とすよう要求しているということです。しかし、型レベルではまったく異なるオブジェクトでした。あるコードは SDK 固有の例外をキャッチし、別のコードは汎用的な Error をキャッチし、さらに別のコードは何もキャッチしませんでした。これが Sentry でのモグラたたきとなりました。ある呼び出し箇所で 429 を修正しても、同じ種類の障害が別の場所で発生するのを待つだけです。
SDK は肥大化している
NPM パッケージの openai や anthropic-ai/sdk は Stainless によって OpenAPI 仕様から自動生成されています。Stripe も Stripe の OpenAPI 仕様から生成されています。これが、大規模な API 向けの公開 SDK を何十ものプログラミング言語でメンテナンスするためのスケーリング方法です。
しかし、優れた公開 SDK はあらゆるユーザーに対応しなければなりません。私たちのバックエンドには、あらゆるユーザー向けの SDK は必要ありません。必要なのは、私たちが使う 8 つの Stripe エンドポイント、WorkOS のユーザーと組織のエンドポイント、そして実際に呼び出す Slack のメソッドだけです。Stripe は 6.5 MB、workos-inc/node は 6.9 MB、slack/web-api は 7.7 MB、linear/sdk は 34 MB です。極端な例では、googleapis は 198 MB に達します。
生成された SDK はプラットフォーム全体を引き連れてきます。何百ものメソッド、オーバーロード、ページネーションヘルパー、リトライ動作、環境検出、互換性シム、そしてどこかで誰かがまだ依存しているために消せない古いインターフェースが含まれます。私たちのバックエンド内部では、その汎用性はたいてい無駄でしかありません。さらに悪いことに、それが私たちと通信線の間に立ちはだかります。
HTTP API が API 契約であることを忘れている
HTTP API を、あたかも低レベルの実装詳細であり、SDK こそが真の安定したインターフェースであるかのように語る人がいます。公開 REST API は契約です。 ベンダーは軽々しくそれを破ることはできません。SDK の作者もそれを理解しています。なぜなら、すべての顧客にアップグレードを強制することはできないからです。多くの顧客は何年も前の SDK バージョンを本番環境で使い続けており、つまり古い通信契約も引き続き機能しなければならないのです。
独自の HttpBaseClient
HttpBaseClient は、クライアント SDK に代わるものです。プロバイダーのサブクラスがベンダー固有の部分(ベース URL、認証ヘッダー、コンテンツタイプ、エラーマッピング、実際に使用するエンドポイントのメソッド)を提供します。HttpBaseClient はそれ以外のすべて(シリアライゼーション、パース、トランスポートエラー、構造化ログ、メトリクス、ステータスマッピング、所要時間追跡)を担当します。これにより可観測性が統一され、すべてのベンダーが一貫した標準に従うようになります。以下は、簡略化した形状です。
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// そして Stripe ラッパーは小さく明示的になります: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 // さらにエンドポイントをここに追加します。80}
これがポイントです。このクラスは Stripe 全体をモデル化しようとはしません。すべてのベンダー呼び出しに持たせたい HTTP の振る舞いをモデル化しています。Stripe は引き続きフォームエンコーディングを使い、WorkOS はベアラー認証と JSON ボディを使い、Slack は HTTP 200 での奇妙な ok: false 動作を維持します。しかし、バックエンドの他の部分からは一貫した形に見えます。
こちらの Gist で、より読みやすいサンプルコードとして汎用的に修正した HttpBaseClient の詳細なバージョンをご覧いただけます。
今も SDK を使っている場所
現在のアプローチはハイブリッドです。実行時は独自の HTTP クライアントを使用しますが、型がまだ時間を節約してくれる場所では SDK を維持します。StripeHttpClient は Stripe.Customer を返せますし、SlackHttpClient は slack/web-api の引数型を借用でき、WorkOS の型は引き続き通信レスポンスを記述できます。
これもいずれは段階的に廃止していくつもりです。AI が必要なリクエスト型とレスポンス型を正確に生成・メンテナンスできるようになれば、型のためだけに SDK パッケージ全体を維持する理由は弱まります。しかし、実行時の振る舞いが厄介な部分なので、そこから移行を始めています。
SDK が単なる REST のラッパーではなく、製品の境界そのものである場合には、今も SDK を使用しています。可観測性が最も明確な例です。Sentry の場合、SDK はランタイムインスツルメンテーション、エラーキャプチャ、スコープ伝搬、リリースメタデータ、そして自前で実装したくない統合機能を処理します。これは、ベンダー SDK を通常のバックエンド HTTP 呼び出しの薄いクライアントとして使うのとは異なります。
すべての API が HTTP 上の REST であるとは限らず、それは問題ありません。データベース呼び出しは良い例です。私たちの低レベルの抽象化は BaseClient です。各クライアントに同じメトリクス、ログ、エラーハンドリングの規約を提供しつつ、子クラスが自身のトランスポートに応じて「fetch」の意味をオーバーライドできるようにします。
今後の方向性
開発者は API ドキュメントを真の統合ガイドとして扱い、SDK はリファレンス実装(認証、ペイロード、ページネーション、リトライ、エッジケースのテンプレート)と見なすようになるでしょう。「SDK を配布する」の次のバージョンは、「エージェントに API の正しい呼び方、適切なパターンの再利用、すべてのランタイム呼び出しをベンダーパッケージ経由にしない方法を教えるエージェントスキルを配布する」ことになるかもしれません。


![Yusuke Narita's Genius AI Utilization Techniques [Preservation Edition]](/cdn-cgi/image/width=1920,quality=90,format=auto,metadata=none/https%3A%2F%2Fcms-assets.youmind.com%2Fmedia%2F1784137658627_u4bwry_HNMS89bbsAAUPJI.jpg)


