Repository: stripe/agent-toolkit
Pull request #31: add: openai toolkit for ts and py
Author: login: willydouhard
Created: 2025-03-01T02:19:10Z | Merged: 2025-03-03T20:38:53Z

## Adding OpenAI toolkits for TS and Python

The current framework integrations are super cool, but I think that examples with raw providers can be helpful.

If this aligns with your vision, a toolkit for Anthropic would also be nice.

## Fix python's `is_tool_allowed`

For tools that have no `actions` at all, the function was throwing as it was trying to call `.items()` on `None`


Diff:
diff --git a/python/examples/openai/.env.template b/python/examples/openai/.env.template
new file mode 100644
index 00000000..1b018f62
--- /dev/null
+++ b/python/examples/openai/.env.template
@@ -0,0 +1,2 @@
+OPENAI_API_KEY=""
+STRIPE_SECRET_KEY=""
\ No newline at end of file
diff --git a/python/examples/openai/main.py b/python/examples/openai/main.py
new file mode 100644
index 00000000..3eaf7240
--- /dev/null
+++ b/python/examples/openai/main.py
@@ -0,0 +1,52 @@
+import os
+from dotenv import load_dotenv
+
+from openai import OpenAI
+from stripe_agent_toolkit.openai.toolkit import StripeAgentToolkit
+
+load_dotenv()
+
+stripe_agent_toolkit = StripeAgentToolkit(
+    secret_key=os.getenv("STRIPE_SECRET_KEY"),
+    configuration={
+        "actions": {
+            "payment_links": {
+                "create": True,
+            },
+            "products": {
+                "create": True,
+            },
+            "prices": {
+                "create": True,
+            },
+        }
+    },
+)
+
+client = OpenAI()
+
+messages = [
+    {
+        "role": "user",
+        "content": """Create a payment link for a new product called 'test' with a price
+of $100. Come up with a funny description about buy bots,
+maybe a haiku.""",
+    },
+]
+
+while True:
+    completion = client.chat.completions.create(
+        model="gpt-4o",
+        messages=messages,
+        tools=stripe_agent_toolkit.get_tools()
+    )
+
+    message = completion.choices[0].message
+    messages.append(message)
+    
+    if message.tool_calls:
+        tool_messages = [stripe_agent_toolkit.handle_tool_call(tc) for tc in message.tool_calls]
+        messages.extend(tool_messages)
+    else:
+        print(completion.choices[0].message)
+        break
diff --git a/python/requirements.txt b/python/requirements.txt
index ec3da484..ce5561fc 100644
--- a/python/requirements.txt
+++ b/python/requirements.txt
@@ -1,4 +1,5 @@
 twine
+openai==1.65.1
 crewai==0.76.2
 crewai-tools===0.13.2
 flake8
diff --git a/python/stripe_agent_toolkit/configuration.py b/python/stripe_agent_toolkit/configuration.py
index b7f41b79..7f44649b 100644
--- a/python/stripe_agent_toolkit/configuration.py
+++ b/python/stripe_agent_toolkit/configuration.py
@@ -52,7 +52,7 @@ class Configuration(TypedDict, total=False):
 
 
 def is_tool_allowed(tool, configuration):
-    for resource, permissions in tool.get("actions").items():
+    for resource, permissions in tool.get("actions", {}).items():
         if resource not in configuration.get("actions", {}):
             return False
         for permission in permissions:
diff --git a/python/stripe_agent_toolkit/openai/toolkit.py b/python/stripe_agent_toolkit/openai/toolkit.py
new file mode 100644
index 00000000..f83c3946
--- /dev/null
+++ b/python/stripe_agent_toolkit/openai/toolkit.py
@@ -0,0 +1,67 @@
+"""Stripe Agent Toolkit."""
+
+from typing import List, Optional
+from pydantic import PrivateAttr
+import json
+from openai.types.chat import ChatCompletion, ChatCompletionToolParam, ChatCompletionToolMessageParam, ChatCompletionMessageToolCall
+
+
+from ..api import StripeAPI
+from ..tools import tools
+from ..configuration import Configuration, is_tool_allowed
+
+
+class StripeAgentToolkit:
+    _tools: List[ChatCompletionToolParam] = PrivateAttr(default=[])
+    _stripe_api: StripeAPI
+
+    def __init__(
+        self, secret_key: str, configuration: Optional[Configuration] = None
+    ):
+        super().__init__()
+
+        context = configuration.get("context") if configuration else None
+
+        self._stripe_api = StripeAPI(secret_key=secret_key, context=context)
+        
+        filtered_tools = [
+            tool for tool in tools if is_tool_allowed(tool, configuration)
+        ]
+
+        self._tools = [
+            {"type": "function",
+             "function": {
+                 "name": tool["method"],
+                 "description": tool["description"],
+                 "parameters": tool["args_schema"].model_json_schema()
+             }}
+            for tool in filtered_tools
+        ]
+
+    def get_tools(self) -> List[ChatCompletionToolParam]:
+        """Get the tools in the toolkit."""
+        return self._tools
+    
+    def handle_tool_call(self, tool_call: ChatCompletionMessageToolCall) -> ChatCompletionToolMessageParam:
+        """Process a single OpenAI tool call and return its execution result.
+        
+        This method takes a tool call object, extracts and parses its arguments, 
+        executes the corresponding API function, and formats the response
+        as a tool message for the chat completion.
+        
+        Args:
+            tool_call: A ChatCompletionMessageToolCall object containing the function
+                    name and arguments to be executed.
+                    
+        Returns:
+            A dictionary containing the tool execution result in the format
+            required by the chat completion API, including role, tool_call_id,
+            and content fields.
+        """
+        args = json.loads(tool_call.function.arguments)
+        response = self._stripe_api.run(tool_call.function.name, **args)
+        return {
+            "role": "tool",
+            "tool_call_id": tool_call.id,
+            "content": response
+        }
\ No newline at end of file
diff --git a/typescript/examples/openai/.env.template b/typescript/examples/openai/.env.template
new file mode 100644
index 00000000..1b018f62
--- /dev/null
+++ b/typescript/examples/openai/.env.template
@@ -0,0 +1,2 @@
+OPENAI_API_KEY=""
+STRIPE_SECRET_KEY=""
\ No newline at end of file
diff --git a/typescript/examples/openai/README.md b/typescript/examples/openai/README.md
new file mode 100644
index 00000000..094b8099
--- /dev/null
+++ b/typescript/examples/openai/README.md
@@ -0,0 +1,15 @@
+# OpenAI Example
+
+## Setup
+
+Copy the `.env.template` and populate with your values.
+
+```
+cp .env.template .env
+```
+
+## Usage
+
+```
+npx ts-node index.ts --env
+```
diff --git a/typescript/examples/openai/index.ts b/typescript/examples/openai/index.ts
new file mode 100644
index 00000000..56ea008f
--- /dev/null
+++ b/typescript/examples/openai/index.ts
@@ -0,0 +1,59 @@
+import {StripeAgentToolkit} from '@stripe/agent-toolkit/openai';
+import OpenAI from 'openai';
+import type {ChatCompletionMessageParam} from 'openai/resources';
+
+require('dotenv').config();
+
+const openai = new OpenAI();
+
+const stripeAgentToolkit = new StripeAgentToolkit({
+  secretKey: process.env.STRIPE_SECRET_KEY!,
+  configuration: {
+    actions: {
+      paymentLinks: {
+        create: true,
+      },
+      products: {
+        create: true,
+      },
+      prices: {
+        create: true,
+      },
+    },
+  },
+});
+
+(async (): Promise<void> => {
+  let messages: ChatCompletionMessageParam[] = [
+    {
+      role: 'user',
+      content: `Create a payment link for a new product called 'test' with a price
+of $100. Come up with a funny description about buy bots,
+maybe a haiku.`,
+    },
+  ];
+
+  while (true) {
+    // eslint-disable-next-line no-await-in-loop
+    const completion = await openai.chat.completions.create({
+      model: 'gpt-4o',
+      messages,
+      tools: stripeAgentToolkit.getTools(),
+    });
+
+    const message = completion.choices[0].message;
+
+    messages.push(message);
+
+    if (message.tool_calls) {
+      // eslint-disable-next-line no-await-in-loop
+      const toolMessages = await Promise.all(
+        message.tool_calls.map((tc) => stripeAgentToolkit.handleToolCall(tc))
+      );
+      messages = [...messages, ...toolMessages];
+    } else {
+      console.log(completion.choices[0].message);
+      break;
+    }
+  }
+})();
diff --git a/typescript/examples/openai/package.json b/typescript/examples/openai/package.json
new file mode 100644
index 00000000..ca0701dd
--- /dev/null
+++ b/typescript/examples/openai/package.json
@@ -0,0 +1,19 @@
+{
+  "name": "stripe-agent-toolkit-examples-openai",
+  "version": "0.1.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "",
+  "license": "MIT",
+  "dependencies": {
+    "dotenv": "^16.4.5",
+    "openai": "^4.86.1",
+    "@stripe/agent-toolkit": "latest"
+  },
+  "devDependencies": {
+    "@types/node": "^22.7.4"
+  }
+}
diff --git a/typescript/examples/openai/tsconfig.json b/typescript/examples/openai/tsconfig.json
new file mode 100644
index 00000000..a7e7234d
--- /dev/null
+++ b/typescript/examples/openai/tsconfig.json
@@ -0,0 +1,8 @@
+{
+  "extends": "../../tsconfig.json",
+  "compilerOptions": {
+    "outDir": "dist"
+  },
+  "include": ["index.ts"],
+  "exclude": ["node_modules", "dist"]
+}
diff --git a/typescript/package.json b/typescript/package.json
index 56ee1d7d..2c3b56b1 100644
--- a/typescript/package.json
+++ b/typescript/package.json
@@ -21,6 +21,11 @@
       "require": "./ai-sdk/index.js",
       "import": "./ai-sdk/index.mjs"
     },
+    "./openai": {
+      "types": "./openai/index.d.ts",
+      "require": "./openai/index.js",
+      "import": "./openai/index.mjs"
+    },
     "./modelcontextprotocol": {
       "types": "./modelcontextprotocol/index.d.ts",
       "require": "./modelcontextprotocol/index.js",
@@ -56,9 +61,11 @@
   },
   "dependencies": {
     "stripe": "^17.5.0",
-    "zod": "^3.24.1"
+    "zod": "^3.24.1",
+    "zod-to-json-schema": "^3.24.3"
   },
   "peerDependencies": {
+    "openai": "^4.86.1",
     "@langchain/core": "^0.3.6",
     "@modelcontextprotocol/sdk": "^1.4.1",
     "ai": "^3.4.7 || ^4.0.0"
diff --git a/typescript/pnpm-lock.yaml b/typescript/pnpm-lock.yaml
index 7edbe727..9ae9032f 100644
--- a/typescript/pnpm-lock.yaml
+++ b/typescript/pnpm-lock.yaml
@@ -10,19 +10,25 @@ importers:
     dependencies:
       '@langchain/core':
         specifier: ^0.3.6
-        version: 0.3.27(openai@4.67.1(zod@3.24.1))
+        version: 0.3.27(openai@4.86.1(zod@3.24.1))
       '@modelcontextprotocol/sdk':
         specifier: ^1.4.1
         version: 1.4.1
       ai:
         specifier: ^3.4.7 || ^4.0.0
         version: 4.0.28(react@18.3.1)(zod@3.24.1)
+      openai:
+        specifier: ^4.86.1
+        version: 4.86.1(zod@3.24.1)
       stripe:
         specifier: ^17.5.0
         version: 17.5.0
       zod:
         specifier: ^3.24.1
         version: 3.24.1
+      zod-to-json-schema:
+        specifier: ^3.24.3
+        version: 3.24.3(zod@3.24.1)
     devDependencies:
       '@eslint/compat':
         specifier: ^1.2.4
@@ -80,10 +86,10 @@ importers:
         version: 0.0.63(zod@3.24.1)
       '@stripe/agent-toolkit':
         specifier: latest
-        version: 0.2.1(@langchain/core@0.3.27(openai@4.67.1(zod@3.24.1)))(ai@3.4.7(openai@4.67.1(zod@3.24.1))(react@18.3.1)(sswr@2.1.0(svelte@4.2.19))(svelte@4.2.19)(vue@3.5.10(typescript@5.7.2))(zod@3.24.1))
+        version: 0.2.1(@langchain/core@0.3.27(openai@4.86.1(zod@3.24.1)))(ai@3.4.7(openai@4.86.1(zod@3.24.1))(react@18.3.1)(sswr@2.1.0(svelte@4.2.19))(svelte@4.2.19)(vue@3.5.10(typescript@5.7.2))(zod@3.24.1))
       ai:
         specifier: ^3.4.7 || ^4.0.0
-        version: 3.4.7(openai@4.67.1(zod@3.24.1))(react@18.3.1)(sswr@2.1.0(svelte@4.2.19))(svelte@4.2.19)(vue@3.5.10(typescript@5.7.2))(zod@3.24.1)
+        version: 3.4.7(openai@4.86.1(zod@3.24.1))(react@18.3.1)(sswr@2.1.0(svelte@4.2.19))(svelte@4.2.19)(vue@3.5.10(typescript@5.7.2))(zod@3.24.1)
       dotenv:
         specifier: ^16.4.5
         version: 16.4.5
@@ -96,32 +102,35 @@ importers:
     dependencies:
       '@langchain/core':
         specifier: ^0.3.6
-        version: 0.3.6(openai@4.67.1(zod@3.24.1))
+        version: 0.3.6(openai@4.86.1(zod@3.24.1))
       '@langchain/openai':
         specifier: ^0.3.5
-        version: 0.3.5(@langchain/core@0.3.6(openai@4.67.1(zod@3.24.1)))
+        version: 0.3.5(@langchain/core@0.3.6(openai@4.86.1(zod@3.24.1)))
       '@stripe/agent-toolkit':
         specifier: latest
-        version: 0.2.1(@langchain/core@0.3.6(openai@4.67.1(zod@3.24.1)))(ai@4.0.28(react@18.3.1)(zod@3.24.1))
+        version: 0.2.1(@langchain/core@0.3.6(openai@4.86.1(zod@3.24.1)))(ai@4.0.28(react@18.3.1)(zod@3.24.1))
       dotenv:
         specifier: ^16.4.5
         version: 16.4.5
       langchain:
         specifier: ^0.3.2
-        version: 0.3.2(@langchain/core@0.3.6(openai@4.67.1(zod@3.24.1)))(openai@4.67.1(zod@3.24.1))
+        version: 0.3.2(@langchain/core@0.3.6(openai@4.86.1(zod@3.24.1)))(openai@4.86.1(zod@3.24.1))
     devDependencies:
       '@types/node':
         specifier: ^22.7.4
         version: 22.7.4
 
-  examples/modelcontextprotocol:
+  examples/openai:
     dependencies:
       '@stripe/agent-toolkit':
         specifier: latest
-        version: 0.2.2(@langchain/core@0.3.27(openai@4.67.1(zod@3.24.1)))(ai@4.0.28(react@18.3.1)(zod@3.24.1))
+        version: 0.4.1(@langchain/core@0.3.27(openai@4.86.1(zod@3.24.1)))(@modelcontextprotocol/sdk@1.4.1)(ai@4.0.28(react@18.3.1)(zod@3.24.1))
       dotenv:
         specifier: ^16.4.5
         version: 16.4.5
+      openai:
+        specifier: ^4.86.1
+        version: 4.86.1(zod@3.24.1)
     devDependencies:
       '@types/node':
         specifier: ^22.7.4
@@ -876,11 +885,12 @@ packages:
       '@langchain/core': ^0.3.6
       ai: ^3.4.7 || ^4.0.0
 
-  '@stripe/agent-toolkit@0.2.2':
-    resolution: {integrity: sha512-+VPdFFYqu3IX6wFGuzx7kUPLc0MeMy4AzR4epwMO6DxKkXoMJKoeBXQiweqPtL/uZxVuHp5d+BI+KnJUMuKwnA==}
+  '@stripe/agent-toolkit@0.4.1':
+    resolution: {integrity: sha512-z9BB7igKXQPDtzvIqz4wS8EeNMrHcashVg2W8Oltdl9v8ScvZaB+gtr7G5PED96W7QlFgSkZxJrj0+l+Nrm/4A==}
     engines: {node: '>=18'}
     peerDependencies:
       '@langchain/core': ^0.3.6
+      '@modelcontextprotocol/sdk': ^1.4.1
       ai: ^3.4.7 || ^4.0.0
 
   '@tsconfig/node10@1.0.11':
@@ -2547,12 +2557,15 @@ packages:
     resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
     engines: {node: '>=6'}
 
-  openai@4.67.1:
-    resolution: {integrity: sha512-2YbRFy6qaYRJabK2zLMn4txrB2xBy0KP5g/eoqeSPTT31mIJMnkT75toagvfE555IKa2RdrzJrZwdDsUipsAMw==}
+  openai@4.86.1:
+    resolution: {integrity: sha512-x3iCLyaC3yegFVZaxOmrYJjitKxZ9hpVbLi+ZlT5UHuHTMlEQEbKXkGOM78z9qm2T5GF+XRUZCP2/aV4UPFPJQ==}
     hasBin: true
     peerDependencies:
+      ws: ^8.18.0
       zod: ^3.23.8
     peerDependenciesMeta:
+      ws:
+        optional: true
       zod:
         optional: true
 
@@ -3287,13 +3300,8 @@ packages:
     peerDependencies:
       zod: ^3.23.3
 
-  zod-to-json-schema@3.23.3:
-    resolution: {integrity: sha512-TYWChTxKQbRJp5ST22o/Irt9KC5nj7CdBKYB/AosCRdj/wxEMvv4NNaj9XVUHDOIp53ZxArGhnw5HMZziPFjog==}
-    peerDependencies:
-      zod: ^3.23.3
-
-  zod-to-json-schema@3.24.1:
-    resolution: {integrity: sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==}
+  zod-to-json-schema@3.24.3:
+    resolution: {integrity: sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A==}
     peerDependencies:
       zod: ^3.24.1
 
@@ -3384,7 +3392,7 @@ snapshots:
     dependencies:
       '@ai-sdk/provider': 1.0.4
       '@ai-sdk/provider-utils': 2.0.6(zod@3.24.1)
-      zod-to-json-schema: 3.24.1(zod@3.24.1)
+      zod-to-json-schema: 3.24.3(zod@3.24.1)
     optionalDependencies:
       zod: 3.24.1
 
@@ -3932,52 +3940,53 @@ snapshots:
       '@jridgewell/resolve-uri': 3.1.2
       '@jridgewell/sourcemap-codec': 1.5.0
 
-  '@langchain/core@0.3.27(openai@4.67.1(zod@3.24.1))':
+  '@langchain/core@0.3.27(openai@4.86.1(zod@3.24.1))':
     dependencies:
       '@cfworker/json-schema': 4.1.0
       ansi-styles: 5.2.0
       camelcase: 6.3.0
       decamelize: 1.2.0
       js-tiktoken: 1.0.16
-      langsmith: 0.2.14(openai@4.67.1(zod@3.24.1))
+      langsmith: 0.2.14(openai@4.86.1(zod@3.24.1))
       mustache: 4.2.0
       p-queue: 6.6.2
       p-retry: 4.6.2
       uuid: 10.0.0
       zod: 3.24.1
-      zod-to-json-schema: 3.24.1(zod@3.24.1)
+      zod-to-json-schema: 3.24.3(zod@3.24.1)
     transitivePeerDependencies:
       - openai
 
-  '@langchain/core@0.3.6(openai@4.67.1(zod@3.24.1))':
+  '@langchain/core@0.3.6(openai@4.86.1(zod@3.24.1))':
     dependencies:
       ansi-styles: 5.2.0
       camelcase: 6.3.0
       decamelize: 1.2.0
       js-tiktoken: 1.0.14
-      langsmith: 0.1.61(openai@4.67.1(zod@3.24.1))
+      langsmith: 0.1.61(openai@4.86.1(zod@3.24.1))
       mustache: 4.2.0
       p-queue: 6.6.2
       p-retry: 4.6.2
       uuid: 10.0.0
       zod: 3.24.1
-      zod-to-json-schema: 3.23.3(zod@3.24.1)
+      zod-to-json-schema: 3.24.3(zod@3.24.1)
     transitivePeerDependencies:
       - openai
 
-  '@langchain/openai@0.3.5(@langchain/core@0.3.6(openai@4.67.1(zod@3.24.1)))':
+  '@langchain/openai@0.3.5(@langchain/core@0.3.6(openai@4.86.1(zod@3.24.1)))':
     dependencies:
-      '@langchain/core': 0.3.6(openai@4.67.1(zod@3.24.1))
+      '@langchain/core': 0.3.6(openai@4.86.1(zod@3.24.1))
       js-tiktoken: 1.0.14
-      openai: 4.67.1(zod@3.24.1)
+      openai: 4.86.1(zod@3.24.1)
       zod: 3.24.1
-      zod-to-json-schema: 3.23.3(zod@3.24.1)
+      zod-to-json-schema: 3.24.3(zod@3.24.1)
     transitivePeerDependencies:
       - encoding
+      - ws
 
-  '@langchain/textsplitters@0.1.0(@langchain/core@0.3.6(openai@4.67.1(zod@3.24.1)))':
+  '@langchain/textsplitters@0.1.0(@langchain/core@0.3.6(openai@4.86.1(zod@3.24.1)))':
     dependencies:
-      '@langchain/core': 0.3.6(openai@4.67.1(zod@3.24.1))
+      '@langchain/core': 0.3.6(openai@4.86.1(zod@3.24.1))
       js-tiktoken: 1.0.14
 
   '@modelcontextprotocol/sdk@1.4.1':
@@ -3986,7 +3995,7 @@ snapshots:
       eventsource: 3.0.5
       raw-body: 3.0.0
       zod: 3.24.1
-      zod-to-json-schema: 3.24.1(zod@3.24.1)
+      zod-to-json-schema: 3.24.3(zod@3.24.1)
 
   '@nodelib/fs.scandir@2.1.5':
     dependencies:
@@ -4076,23 +4085,24 @@ snapshots:
     dependencies:
       '@sinonjs/commons': 3.0.1
 
-  '@stripe/agent-toolkit@0.2.1(@langchain/core@0.3.27(openai@4.67.1(zod@3.24.1)))(ai@3.4.7(openai@4.67.1(zod@3.24.1))(react@18.3.1)(sswr@2.1.0(svelte@4.2.19))(svelte@4.2.19)(vue@3.5.10(typescript@5.7.2))(zod@3.24.1))':
+  '@stripe/agent-toolkit@0.2.1(@langchain/core@0.3.27(openai@4.86.1(zod@3.24.1)))(ai@3.4.7(openai@4.86.1(zod@3.24.1))(react@18.3.1)(sswr@2.1.0(svelte@4.2.19))(svelte@4.2.19)(vue@3.5.10(typescript@5.7.2))(zod@3.24.1))':
     dependencies:
-      '@langchain/core': 0.3.27(openai@4.67.1(zod@3.24.1))
-      ai: 3.4.7(openai@4.67.1(zod@3.24.1))(react@18.3.1)(sswr@2.1.0(svelte@4.2.19))(svelte@4.2.19)(vue@3.5.10(typescript@5.7.2))(zod@3.24.1)
+      '@langchain/core': 0.3.27(openai@4.86.1(zod@3.24.1))
+      ai: 3.4.7(openai@4.86.1(zod@3.24.1))(react@18.3.1)(sswr@2.1.0(svelte@4.2.19))(svelte@4.2.19)(vue@3.5.10(typescript@5.7.2))(zod@3.24.1)
       stripe: 17.5.0
       zod: 3.24.1
 
-  '@stripe/agent-toolkit@0.2.1(@langchain/core@0.3.6(openai@4.67.1(zod@3.24.1)))(ai@4.0.28(react@18.3.1)(zod@3.24.1))':
+  '@stripe/agent-toolkit@0.2.1(@langchain/core@0.3.6(openai@4.86.1(zod@3.24.1)))(ai@4.0.28(react@18.3.1)(zod@3.24.1))':
     dependencies:
-      '@langchain/core': 0.3.6(openai@4.67.1(zod@3.24.1))
+      '@langchain/core': 0.3.6(openai@4.86.1(zod@3.24.1))
       ai: 4.0.28(react@18.3.1)(zod@3.24.1)
       stripe: 17.5.0
       zod: 3.24.1
 
-  '@stripe/agent-toolkit@0.2.2(@langchain/core@0.3.27(openai@4.67.1(zod@3.24.1)))(ai@4.0.28(react@18.3.1)(zod@3.24.1))':
+  '@stripe/agent-toolkit@0.4.1(@langchain/core@0.3.27(openai@4.86.1(zod@3.24.1)))(@modelcontextprotocol/sdk@1.4.1)(ai@4.0.28(react@18.3.1)(zod@3.24.1))':
     dependencies:
-      '@langchain/core': 0.3.27(openai@4.67.1(zod@3.24.1))
+      '@langchain/core': 0.3.27(openai@4.86.1(zod@3.24.1))
+      '@modelcontextprotocol/sdk': 1.4.1
       ai: 4.0.28(react@18.3.1)(zod@3.24.1)
       stripe: 17.5.0
       zod: 3.24.1
@@ -4359,7 +4369,7 @@ snapshots:
     dependencies:
       humanize-ms: 1.2.1
 
-  ai@3.4.7(openai@4.67.1(zod@3.24.1))(react@18.3.1)(sswr@2.1.0(svelte@4.2.19))(svelte@4.2.19)(vue@3.5.10(typescript@5.7.2))(zod@3.24.1):
+  ai@3.4.7(openai@4.86.1(zod@3.24.1))(react@18.3.1)(sswr@2.1.0(svelte@4.2.19))(svelte@4.2.19)(vue@3.5.10(typescript@5.7.2))(zod@3.24.1):
     dependencies:
       '@ai-sdk/provider': 0.0.24
       '@ai-sdk/provider-utils': 1.0.20(zod@3.24.1)
@@ -4376,7 +4386,7 @@ snapshots:
       secure-json-parse: 2.7.0
       zod-to-json-schema: 3.23.2(zod@3.24.1)
     optionalDependencies:
-      openai: 4.67.1(zod@3.24.1)
+      openai: 4.86.1(zod@3.24.1)
       react: 18.3.1
       sswr: 2.1.0(svelte@4.2.19)
       svelte: 4.2.19
@@ -4393,7 +4403,7 @@ snapshots:
       '@ai-sdk/ui-utils': 1.0.7(zod@3.24.1)
       '@opentelemetry/api': 1.9.0
       jsondiffpatch: 0.6.0
-      zod-to-json-schema: 3.24.1(zod@3.24.1)
+      zod-to-json-schema: 3.24.3(zod@3.24.1)
     optionalDependencies:
       react: 18.3.1
       zod: 3.24.1
@@ -5868,26 +5878,27 @@ snapshots:
 
   kleur@3.0.3: {}
 
-  langchain@0.3.2(@langchain/core@0.3.6(openai@4.67.1(zod@3.24.1)))(openai@4.67.1(zod@3.24.1)):
+  langchain@0.3.2(@langchain/core@0.3.6(openai@4.86.1(zod@3.24.1)))(openai@4.86.1(zod@3.24.1)):
     dependencies:
-      '@langchain/core': 0.3.6(openai@4.67.1(zod@3.24.1))
-      '@langchain/openai': 0.3.5(@langchain/core@0.3.6(openai@4.67.1(zod@3.24.1)))
-      '@langchain/textsplitters': 0.1.0(@langchain/core@0.3.6(openai@4.67.1(zod@3.24.1)))
+      '@langchain/core': 0.3.6(openai@4.86.1(zod@3.24.1))
+      '@langchain/openai': 0.3.5(@langchain/core@0.3.6(openai@4.86.1(zod@3.24.1)))
+      '@langchain/textsplitters': 0.1.0(@langchain/core@0.3.6(openai@4.86.1(zod@3.24.1)))
       js-tiktoken: 1.0.14
       js-yaml: 4.1.0
       jsonpointer: 5.0.1
-      langsmith: 0.1.61(openai@4.67.1(zod@3.24.1))
+      langsmith: 0.1.61(openai@4.86.1(zod@3.24.1))
       openapi-types: 12.1.3
       p-retry: 4.6.2
       uuid: 10.0.0
       yaml: 2.5.1
       zod: 3.24.1
-      zod-to-json-schema: 3.23.3(zod@3.24.1)
+      zod-to-json-schema: 3.24.3(zod@3.24.1)
     transitivePeerDependencies:
       - encoding
       - openai
+      - ws
 
-  langsmith@0.1.61(openai@4.67.1(zod@3.24.1)):
+  langsmith@0.1.61(openai@4.86.1(zod@3.24.1)):
     dependencies:
       '@types/uuid': 10.0.0
       commander: 10.0.1
@@ -5896,9 +5907,9 @@ snapshots:
       semver: 7.6.3
       uuid: 10.0.0
     optionalDependencies:
-      openai: 4.67.1(zod@3.24.1)
+      openai: 4.86.1(zod@3.24.1)
 
-  langsmith@0.2.14(openai@4.67.1(zod@3.24.1)):
+  langsmith@0.2.14(openai@4.86.1(zod@3.24.1)):
     dependencies:
       '@types/uuid': 10.0.0
       commander: 10.0.1
@@ -5907,7 +5918,7 @@ snapshots:
       semver: 7.6.3
       uuid: 10.0.0
     optionalDependencies:
-      openai: 4.67.1(zod@3.24.1)
+      openai: 4.86.1(zod@3.24.1)
 
   leven@3.1.0: {}
 
@@ -6074,7 +6085,7 @@ snapshots:
     dependencies:
       mimic-fn: 2.1.0
 
-  openai@4.67.1(zod@3.24.1):
+  openai@4.86.1(zod@3.24.1):
     dependencies:
       '@types/node': 18.19.70
       '@types/node-fetch': 2.6.11
@@ -6884,11 +6895,7 @@ snapshots:
     dependencies:
       zod: 3.24.1
 
-  zod-to-json-schema@3.23.3(zod@3.24.1):
-    dependencies:
-      zod: 3.24.1
-
-  zod-to-json-schema@3.24.1(zod@3.24.1):
+  zod-to-json-schema@3.24.3(zod@3.24.1):
     dependencies:
       zod: 3.24.1
 
diff --git a/typescript/src/openai/index.ts b/typescript/src/openai/index.ts
new file mode 100644
index 00000000..d356aec2
--- /dev/null
+++ b/typescript/src/openai/index.ts
@@ -0,0 +1,2 @@
+import StripeAgentToolkit from './toolkit';
+export {StripeAgentToolkit};
diff --git a/typescript/src/openai/toolkit.ts b/typescript/src/openai/toolkit.ts
new file mode 100644
index 00000000..ed87c2c5
--- /dev/null
+++ b/typescript/src/openai/toolkit.ts
@@ -0,0 +1,62 @@
+import StripeAPI from '../shared/api';
+import tools from '../shared/tools';
+import {isToolAllowed, type Configuration} from '../shared/configuration';
+import {zodToJsonSchema} from 'zod-to-json-schema';
+import type {
+  ChatCompletionTool,
+  ChatCompletionMessageToolCall,
+  ChatCompletionToolMessageParam,
+} from 'openai/resources';
+
+class StripeAgentToolkit {
+  private _stripe: StripeAPI;
+
+  tools: ChatCompletionTool[];
+
+  constructor({
+    secretKey,
+    configuration,
+  }: {
+    secretKey: string;
+    configuration: Configuration;
+  }) {
+    this._stripe = new StripeAPI(secretKey, configuration.context);
+
+    const filteredTools = tools.filter((tool) =>
+      isToolAllowed(tool, configuration)
+    );
+
+    this.tools = filteredTools.map((tool) => ({
+      type: 'function',
+      function: {
+        name: tool.method,
+        description: tool.description,
+        parameters: zodToJsonSchema(tool.parameters),
+      },
+    }));
+  }
+
+  getTools(): ChatCompletionTool[] {
+    return this.tools;
+  }
+
+  /**
+   * Processes a single OpenAI tool call by executing the requested function.
+   *
+   * @param {ChatCompletionMessageToolCall} toolCall - The tool call object from OpenAI containing
+   *   function name, arguments, and ID.
+   * @returns {Promise<ChatCompletionToolMessageParam>} A promise that resolves to a tool message
+   *   object containing the result of the tool execution with the proper format for the OpenAI API.
+   */
+  async handleToolCall(toolCall: ChatCompletionMessageToolCall) {
+    const args = JSON.parse(toolCall.function.arguments);
+    const response = await this._stripe.run(toolCall.function.name, args);
+    return {
+      role: 'tool',
+      tool_call_id: toolCall.id,
+      content: response,
+    } as ChatCompletionToolMessageParam;
+  }
+}
+
+export default StripeAgentToolkit;


Reviews:
user: login: willydouhard
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-01T02:20:12Z
user: login: stevekaliski-stripe
body: @willydouhard Hi! Thank you for the contribution!

I took a scan. Overall LGTM. My preference is to put more of the logic around tool execution into the users application instead of the framework.

Concretely, let's create a handleToolCall (ts) and handle_tool_call (python) methods that just execute a singular tool call and pass the tool invocation rather than the whole completion output.
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T17:46:08Z
user: login: willydouhard
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T19:16:03Z
user: login: matv-stripe
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T19:57:49Z
user: login: matv-stripe
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T19:58:06Z
user: login: willydouhard
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T20:03:48Z
user: login: stevekaliski-stripe
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T20:16:12Z

Review comments:
diff_hunk: @@ -52,7 +52,7 @@ class Configuration(TypedDict, total=False):
 
 
 def is_tool_allowed(tool, configuration):
-    for resource, permissions in tool.get("actions").items():
+    for resource, permissions in tool.get("actions", {}).items():
path: python/stripe_agent_toolkit/configuration.py
user: login: willydouhard
body: The tool `list_payment_intents` has no actions. `.items()` is called on `None`
created_at: 2025-03-01T02:20:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,52 @@
+import os
+from dotenv import load_dotenv
+
+from openai import OpenAI
+from stripe_agent_toolkit.openai.toolkit import StripeAgentToolkit
+
+load_dotenv()
+
+stripe_agent_toolkit = StripeAgentToolkit(
+    secret_key=os.getenv("STRIPE_SECRET_KEY"),
+    configuration={
+        "actions": {
+            "payment_links": {
+                "create": True,
+            },
+            "products": {
+                "create": True,
+            },
+            "prices": {
+                "create": True,
+            },
+        }
+    },
+)
+
+client = OpenAI()
+
+messages = [
+    {
+        "role": "user",
+        "content": """Create a payment link for a new product called 'test' with a price
+of $100. Come up with a funny description about buy bots,
+maybe a haiku.""",
+    },
+]
+
+while True:
+    completion = client.chat.completions.create(
+        model="gpt-4o",
+        messages=messages,
+        tools=stripe_agent_toolkit.get_tools()
+    )
+
+    message = completion.choices[0].message
+    messages.append(message)
+    
+    if message.tool_calls:
+        tool_messages = [stripe_agent_toolkit.handle_tool_call(tc) for tc in message.tool_calls]
path: python/examples/openai/main.py
user: login: willydouhard
body: We now handle tool call individually. This example will only work if all tool calls belong to the stripe agent toolkit
created_at: 2025-03-03T19:16:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,62 @@
+import StripeAPI from '../shared/api';
+import tools from '../shared/tools';
+import {isToolAllowed, type Configuration} from '../shared/configuration';
+import {zodToJsonSchema} from 'zod-to-json-schema';
+import type {
+  ChatCompletionTool,
+  ChatCompletionMessageToolCall,
+  ChatCompletionToolMessageParam,
+} from 'openai/resources';
+
+class StripeAgentToolkit {
+  private _stripe: StripeAPI;
+
+  tools: ChatCompletionTool[];
+
+  constructor({
+    secretKey,
+    configuration,
+  }: {
+    secretKey: string;
+    configuration: Configuration;
+  }) {
+    this._stripe = new StripeAPI(secretKey, configuration.context);
+
+    const filteredTools = tools.filter((tool) =>
+      isToolAllowed(tool, configuration)
+    );
+
+    this.tools = filteredTools.map((tool) => ({
+      type: 'function',
+      function: {
+        name: tool.method,
+        description: tool.description,
+        parameters: zodToJsonSchema(tool.parameters),
+      },
+    }));
+  }
+
+  getTools(): ChatCompletionTool[] {
+    return this.tools;
+  }
+
+  /**
+   * Processes a single OpenAI tool call by executing the requested function.
+   *
+   * @param {ChatCompletionMessageToolCall} toolCall - The tool call object from OpenAI containing
+   *   function name, arguments, and ID.
+   * @returns {Promise<ChatCompletionToolMessageParam>} A promise that resolves to a tool message
+   *   object containing the result of the tool execution with the proper format for the OpenAI API.
+   */
+  async handleToolCall(toolCall: ChatCompletionMessageToolCall) {
+    const args = JSON.parse(toolCall.function.arguments);
+    const response = await this._stripe.run(toolCall.function.name, args);
+    return {
+      role: 'tool',
+      tool_call_id: toolCall.id,
+      content: response,
+    } as ChatCompletionToolMessageParam;
path: typescript/src/openai/toolkit.ts
user: login: matv-stripe
body: question: would it be better to use `satisfies` here? 
created_at: 2025-03-03T19:57:49Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,62 @@
+import StripeAPI from '../shared/api';
+import tools from '../shared/tools';
+import {isToolAllowed, type Configuration} from '../shared/configuration';
+import {zodToJsonSchema} from 'zod-to-json-schema';
+import type {
+  ChatCompletionTool,
+  ChatCompletionMessageToolCall,
+  ChatCompletionToolMessageParam,
+} from 'openai/resources';
+
+class StripeAgentToolkit {
+  private _stripe: StripeAPI;
+
+  tools: ChatCompletionTool[];
+
+  constructor({
+    secretKey,
+    configuration,
+  }: {
+    secretKey: string;
+    configuration: Configuration;
+  }) {
+    this._stripe = new StripeAPI(secretKey, configuration.context);
+
+    const filteredTools = tools.filter((tool) =>
+      isToolAllowed(tool, configuration)
+    );
+
+    this.tools = filteredTools.map((tool) => ({
+      type: 'function',
+      function: {
+        name: tool.method,
+        description: tool.description,
+        parameters: zodToJsonSchema(tool.parameters),
+      },
+    }));
+  }
+
+  getTools(): ChatCompletionTool[] {
+    return this.tools;
+  }
+
+  /**
+   * Processes a single OpenAI tool call by executing the requested function.
+   *
+   * @param {ChatCompletionMessageToolCall} toolCall - The tool call object from OpenAI containing
+   *   function name, arguments, and ID.
+   * @returns {Promise<ChatCompletionToolMessageParam>} A promise that resolves to a tool message
+   *   object containing the result of the tool execution with the proper format for the OpenAI API.
+   */
+  async handleToolCall(toolCall: ChatCompletionMessageToolCall) {
+    const args = JSON.parse(toolCall.function.arguments);
+    const response = await this._stripe.run(toolCall.function.name, args);
+    return {
+      role: 'tool',
+      tool_call_id: toolCall.id,
+      content: response,
+    } as ChatCompletionToolMessageParam;
path: typescript/src/openai/toolkit.ts
user: login: matv-stripe
body: ```suggestion
    } satisfies ChatCompletionToolMessageParam;
```
created_at: 2025-03-03T19:58:06Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,62 @@
+import StripeAPI from '../shared/api';
+import tools from '../shared/tools';
+import {isToolAllowed, type Configuration} from '../shared/configuration';
+import {zodToJsonSchema} from 'zod-to-json-schema';
+import type {
+  ChatCompletionTool,
+  ChatCompletionMessageToolCall,
+  ChatCompletionToolMessageParam,
+} from 'openai/resources';
+
+class StripeAgentToolkit {
+  private _stripe: StripeAPI;
+
+  tools: ChatCompletionTool[];
+
+  constructor({
+    secretKey,
+    configuration,
+  }: {
+    secretKey: string;
+    configuration: Configuration;
+  }) {
+    this._stripe = new StripeAPI(secretKey, configuration.context);
+
+    const filteredTools = tools.filter((tool) =>
+      isToolAllowed(tool, configuration)
+    );
+
+    this.tools = filteredTools.map((tool) => ({
+      type: 'function',
+      function: {
+        name: tool.method,
+        description: tool.description,
+        parameters: zodToJsonSchema(tool.parameters),
+      },
+    }));
+  }
+
+  getTools(): ChatCompletionTool[] {
+    return this.tools;
+  }
+
+  /**
+   * Processes a single OpenAI tool call by executing the requested function.
+   *
+   * @param {ChatCompletionMessageToolCall} toolCall - The tool call object from OpenAI containing
+   *   function name, arguments, and ID.
+   * @returns {Promise<ChatCompletionToolMessageParam>} A promise that resolves to a tool message
+   *   object containing the result of the tool execution with the proper format for the OpenAI API.
+   */
+  async handleToolCall(toolCall: ChatCompletionMessageToolCall) {
+    const args = JSON.parse(toolCall.function.arguments);
+    const response = await this._stripe.run(toolCall.function.name, args);
+    return {
+      role: 'tool',
+      tool_call_id: toolCall.id,
+      content: response,
+    } as ChatCompletionToolMessageParam;
path: typescript/src/openai/toolkit.ts
user: login: willydouhard
body: If we do the inferred return type of the function will be the literal object not the OpenAI type:

```typescript
Promise<{
    role: "tool";
    tool_call_id: string;
    content: string;
}>
```

 It depends on what you guys prefer for the DX.
created_at: 2025-03-03T20:03:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,62 @@
+import StripeAPI from '../shared/api';
+import tools from '../shared/tools';
+import {isToolAllowed, type Configuration} from '../shared/configuration';
+import {zodToJsonSchema} from 'zod-to-json-schema';
+import type {
+  ChatCompletionTool,
+  ChatCompletionMessageToolCall,
+  ChatCompletionToolMessageParam,
+} from 'openai/resources';
+
+class StripeAgentToolkit {
+  private _stripe: StripeAPI;
+
+  tools: ChatCompletionTool[];
+
+  constructor({
+    secretKey,
+    configuration,
+  }: {
+    secretKey: string;
+    configuration: Configuration;
+  }) {
+    this._stripe = new StripeAPI(secretKey, configuration.context);
+
+    const filteredTools = tools.filter((tool) =>
+      isToolAllowed(tool, configuration)
+    );
+
+    this.tools = filteredTools.map((tool) => ({
+      type: 'function',
+      function: {
+        name: tool.method,
+        description: tool.description,
+        parameters: zodToJsonSchema(tool.parameters),
+      },
+    }));
+  }
+
+  getTools(): ChatCompletionTool[] {
+    return this.tools;
+  }
+
+  /**
+   * Processes a single OpenAI tool call by executing the requested function.
+   *
+   * @param {ChatCompletionMessageToolCall} toolCall - The tool call object from OpenAI containing
+   *   function name, arguments, and ID.
+   * @returns {Promise<ChatCompletionToolMessageParam>} A promise that resolves to a tool message
+   *   object containing the result of the tool execution with the proper format for the OpenAI API.
+   */
+  async handleToolCall(toolCall: ChatCompletionMessageToolCall) {
+    const args = JSON.parse(toolCall.function.arguments);
+    const response = await this._stripe.run(toolCall.function.name, args);
+    return {
+      role: 'tool',
+      tool_call_id: toolCall.id,
+      content: response,
+    } as ChatCompletionToolMessageParam;
path: typescript/src/openai/toolkit.ts
user: login: stevekaliski-stripe
body: i think keep `as`
created_at: 2025-03-03T20:16:12Z
author_association: COLLABORATOR

PR comments:
user: login: cla-assistant[bot]
created_at: 2025-03-01T02:19:25Z
body: [![CLA assistant check](https://cla-assistant.io/pull/badge/signed)](https://cla-assistant.io/stripe/agent-toolkit?pullRequest=31) <br/>All committers have signed the CLA.
author_association: NONE
user: login: stevekaliski-stripe
created_at: 2025-03-03T14:32:47Z
body: Thanks for sharing! Will take a look today
author_association: COLLABORATOR
user: login: willydouhard
created_at: 2025-03-03T18:42:02Z
body: Thank you for the quick review @stevekaliski-stripe.

I agree that developers want the freedom to execute a single tool call and handle the logic themselves.

Would you be open to have `handleToolCall` (as you suggested) and `handleToolCalls` built on top of `handleToolCall`? I think it would benefit both developers who want full control on the execution and developers who want to spin up something fast.

If not I'll just implement `handleToolCall`.
author_association: CONTRIBUTOR
user: login: stevekaliski-stripe
created_at: 2025-03-03T18:45:00Z
body: @willydouhard let's start with `handleToolCall` -- in part because I think for cases where there are multiple tools available across many toolkits/systems, you may have a centralized mechanism to call anyways. But if we get user feedback that it'd be helpful to add a multiple tool caller, we'll add!
author_association: COLLABORATOR
user: login: willydouhard
created_at: 2025-03-03T18:46:49Z
body: Should `handleToolCall` return an OpenAI formatted tool message or the raw response from the tool call?
author_association: CONTRIBUTOR
user: login: willydouhard
created_at: 2025-03-03T22:53:27Z
body: Good stuff!
author_association: CONTRIBUTOR