#
tokens: 46201/50000 15/256 files (page 4/7)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 4 of 7. Use http://codebase.md/stripe/agent-toolkit?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .github
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.yml
│   │   ├── config.yml
│   │   └── feature_request.yml
│   └── workflows
│       ├── main.yml
│       ├── npm_release_shared.yml
│       ├── pypi_release.yml
│       └── sync-skills.yml
├── .gitignore
├── .vscode
│   ├── extensions.json
│   ├── launch.json
│   └── settings.json
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── gemini-extension.json
├── LICENSE
├── llm
│   ├── ai-sdk
│   │   ├── jest.config.ts
│   │   ├── LICENSE
│   │   ├── meter
│   │   │   ├── examples
│   │   │   │   ├── .env.example
│   │   │   │   ├── .gitignore
│   │   │   │   ├── anthropic.ts
│   │   │   │   ├── google.ts
│   │   │   │   ├── openai.ts
│   │   │   │   ├── README.md
│   │   │   │   └── tsconfig.json
│   │   │   ├── index.ts
│   │   │   ├── meter-event-logging.ts
│   │   │   ├── meter-event-types.ts
│   │   │   ├── README.md
│   │   │   ├── tests
│   │   │   │   ├── ai-sdk-billing-wrapper-anthropic.test.ts
│   │   │   │   ├── ai-sdk-billing-wrapper-general.test.ts
│   │   │   │   ├── ai-sdk-billing-wrapper-google.test.ts
│   │   │   │   ├── ai-sdk-billing-wrapper-openai.test.ts
│   │   │   │   ├── ai-sdk-billing-wrapper-other-providers.test.ts
│   │   │   │   ├── meter-event-logging.test.ts
│   │   │   │   └── model-name-normalization.test.ts
│   │   │   ├── tsconfig.json
│   │   │   ├── types.ts
│   │   │   ├── utils.ts
│   │   │   └── wrapperV2.ts
│   │   ├── package.json
│   │   ├── pnpm-lock.yaml
│   │   ├── provider
│   │   │   ├── examples
│   │   │   │   ├── .env.example
│   │   │   │   ├── .gitignore
│   │   │   │   ├── anthropic.ts
│   │   │   │   ├── google.ts
│   │   │   │   ├── openai.ts
│   │   │   │   ├── README.md
│   │   │   │   └── tsconfig.json
│   │   │   ├── index.ts
│   │   │   ├── README.md
│   │   │   ├── stripe-language-model.ts
│   │   │   ├── stripe-provider.ts
│   │   │   ├── tests
│   │   │   │   ├── stripe-language-model.test.ts
│   │   │   │   ├── stripe-provider.test.ts
│   │   │   │   └── utils.test.ts
│   │   │   ├── tsconfig.build.json
│   │   │   ├── tsconfig.json
│   │   │   ├── types.ts
│   │   │   └── utils.ts
│   │   ├── README.md
│   │   ├── tsconfig.json
│   │   └── tsup.config.ts
│   ├── README.md
│   └── token-meter
│       ├── examples
│       │   ├── anthropic.ts
│       │   ├── gemini.ts
│       │   └── openai.ts
│       ├── index.ts
│       ├── jest.config.ts
│       ├── LICENSE
│       ├── meter-event-logging.ts
│       ├── meter-event-types.ts
│       ├── package.json
│       ├── pnpm-lock.yaml
│       ├── README.md
│       ├── tests
│       │   ├── meter-event-logging.test.ts
│       │   ├── model-name-normalization.test.ts
│       │   ├── token-meter-anthropic.test.ts
│       │   ├── token-meter-gemini.test.ts
│       │   ├── token-meter-general.test.ts
│       │   ├── token-meter-openai.test.ts
│       │   └── type-detection.test.ts
│       ├── token-meter.ts
│       ├── tsconfig.build.json
│       ├── tsconfig.json
│       ├── types.ts
│       └── utils
│           └── type-detection.ts
├── README.md
├── SECURITY.md
├── skills
│   ├── get-started-kiro.md
│   ├── README.md
│   ├── stripe-best-practices.md
│   └── sync.js
└── tools
    ├── modelcontextprotocol
    │   ├── .dxtignore
    │   ├── .gitignore
    │   ├── .node-version
    │   ├── .prettierrc
    │   ├── build-dxt.js
    │   ├── Dockerfile
    │   ├── eslint.config.mjs
    │   ├── jest.config.ts
    │   ├── LICENSE
    │   ├── manifest.json
    │   ├── package.json
    │   ├── pnpm-lock.yaml
    │   ├── README.md
    │   ├── server.json
    │   ├── src
    │   │   ├── index.ts
    │   │   └── test
    │   │       └── index.test.ts
    │   ├── stripe_icon.png
    │   └── tsconfig.json
    ├── python
    │   ├── .editorconfig
    │   ├── .flake8
    │   ├── examples
    │   │   ├── crewai
    │   │   │   ├── .env.template
    │   │   │   ├── main.py
    │   │   │   └── README.md
    │   │   ├── langchain
    │   │   │   ├── __init__.py
    │   │   │   ├── .env.template
    │   │   │   ├── main.py
    │   │   │   └── README.md
    │   │   ├── openai
    │   │   │   ├── .env.template
    │   │   │   ├── customer_support
    │   │   │   │   ├── .env.template
    │   │   │   │   ├── emailer.py
    │   │   │   │   ├── env.py
    │   │   │   │   ├── main.py
    │   │   │   │   ├── pyproject.toml
    │   │   │   │   ├── README.md
    │   │   │   │   ├── repl.py
    │   │   │   │   └── support_agent.py
    │   │   │   ├── file_search
    │   │   │   │   ├── main.py
    │   │   │   │   └── README.md
    │   │   │   └── web_search
    │   │   │       ├── .env.template
    │   │   │       ├── main.py
    │   │   │       └── README.md
    │   │   └── strands
    │   │       └── main.py
    │   ├── Makefile
    │   ├── pyproject.toml
    │   ├── README.md
    │   ├── requirements.txt
    │   ├── stripe_agent_toolkit
    │   │   ├── __init__.py
    │   │   ├── api.py
    │   │   ├── configuration.py
    │   │   ├── crewai
    │   │   │   ├── tool.py
    │   │   │   └── toolkit.py
    │   │   ├── functions.py
    │   │   ├── langchain
    │   │   │   ├── tool.py
    │   │   │   └── toolkit.py
    │   │   ├── openai
    │   │   │   ├── hooks.py
    │   │   │   ├── tool.py
    │   │   │   └── toolkit.py
    │   │   ├── prompts.py
    │   │   ├── schema.py
    │   │   ├── strands
    │   │   │   ├── __init__.py
    │   │   │   ├── hooks.py
    │   │   │   ├── tool.py
    │   │   │   └── toolkit.py
    │   │   └── tools.py
    │   └── tests
    │       ├── __init__.py
    │       ├── test_configuration.py
    │       └── test_functions.py
    ├── README.md
    └── typescript
        ├── .gitignore
        ├── .prettierrc
        ├── eslint.config.mjs
        ├── examples
        │   ├── ai-sdk
        │   │   ├── .env.template
        │   │   ├── index.ts
        │   │   ├── package.json
        │   │   ├── README.md
        │   │   └── tsconfig.json
        │   ├── cloudflare
        │   │   ├── .dev.vars.example
        │   │   ├── .gitignore
        │   │   ├── biome.json
        │   │   ├── package.json
        │   │   ├── README.md
        │   │   ├── src
        │   │   │   ├── app.ts
        │   │   │   ├── imageGenerator.ts
        │   │   │   ├── index.ts
        │   │   │   ├── oauth.ts
        │   │   │   └── utils.ts
        │   │   ├── tsconfig.json
        │   │   ├── worker-configuration.d.ts
        │   │   └── wrangler.jsonc
        │   ├── langchain
        │   │   ├── .env.template
        │   │   ├── index.ts
        │   │   ├── package.json
        │   │   ├── README.md
        │   │   └── tsconfig.json
        │   └── openai
        │       ├── .env.template
        │       ├── index.ts
        │       ├── package.json
        │       ├── README.md
        │       └── tsconfig.json
        ├── jest.config.ts
        ├── LICENSE
        ├── package.json
        ├── pnpm-lock.yaml
        ├── pnpm-workspace.yaml
        ├── README.md
        ├── src
        │   ├── ai-sdk
        │   │   ├── index.ts
        │   │   ├── tool.ts
        │   │   └── toolkit.ts
        │   ├── cloudflare
        │   │   ├── index.ts
        │   │   └── README.md
        │   ├── langchain
        │   │   ├── index.ts
        │   │   ├── tool.ts
        │   │   └── toolkit.ts
        │   ├── modelcontextprotocol
        │   │   ├── index.ts
        │   │   ├── README.md
        │   │   ├── register-paid-tool.ts
        │   │   └── toolkit.ts
        │   ├── openai
        │   │   ├── index.ts
        │   │   └── toolkit.ts
        │   ├── shared
        │   │   ├── api.ts
        │   │   ├── balance
        │   │   │   └── retrieveBalance.ts
        │   │   ├── configuration.ts
        │   │   ├── coupons
        │   │   │   ├── createCoupon.ts
        │   │   │   └── listCoupons.ts
        │   │   ├── customers
        │   │   │   ├── createCustomer.ts
        │   │   │   └── listCustomers.ts
        │   │   ├── disputes
        │   │   │   ├── listDisputes.ts
        │   │   │   └── updateDispute.ts
        │   │   ├── documentation
        │   │   │   └── searchDocumentation.ts
        │   │   ├── invoiceItems
        │   │   │   └── createInvoiceItem.ts
        │   │   ├── invoices
        │   │   │   ├── createInvoice.ts
        │   │   │   ├── finalizeInvoice.ts
        │   │   │   └── listInvoices.ts
        │   │   ├── paymentIntents
        │   │   │   └── listPaymentIntents.ts
        │   │   ├── paymentLinks
        │   │   │   └── createPaymentLink.ts
        │   │   ├── prices
        │   │   │   ├── createPrice.ts
        │   │   │   └── listPrices.ts
        │   │   ├── products
        │   │   │   ├── createProduct.ts
        │   │   │   └── listProducts.ts
        │   │   ├── refunds
        │   │   │   └── createRefund.ts
        │   │   ├── subscriptions
        │   │   │   ├── cancelSubscription.ts
        │   │   │   ├── listSubscriptions.ts
        │   │   │   └── updateSubscription.ts
        │   │   └── tools.ts
        │   └── test
        │       ├── modelcontextprotocol
        │       │   └── register-paid-tool.test.ts
        │       └── shared
        │           ├── balance
        │           │   ├── functions.test.ts
        │           │   └── parameters.test.ts
        │           ├── configuration.test.ts
        │           ├── customers
        │           │   ├── functions.test.ts
        │           │   └── parameters.test.ts
        │           ├── disputes
        │           │   └── functions.test.ts
        │           ├── documentation
        │           │   ├── functions.test.ts
        │           │   └── parameters.test.ts
        │           ├── invoiceItems
        │           │   ├── functions.test.ts
        │           │   ├── parameters.test.ts
        │           │   └── prompts.test.ts
        │           ├── invoices
        │           │   ├── functions.test.ts
        │           │   ├── parameters.test.ts
        │           │   └── prompts.test.ts
        │           ├── paymentIntents
        │           │   ├── functions.test.ts
        │           │   ├── parameters.test.ts
        │           │   └── prompts.test.ts
        │           ├── paymentLinks
        │           │   ├── functions.test.ts
        │           │   ├── parameters.test.ts
        │           │   └── prompts.test.ts
        │           ├── prices
        │           │   ├── functions.test.ts
        │           │   └── parameters.test.ts
        │           ├── products
        │           │   ├── functions.test.ts
        │           │   └── parameters.test.ts
        │           ├── refunds
        │           │   ├── functions.test.ts
        │           │   └── parameters.test.ts
        │           └── subscriptions
        │               ├── functions.test.ts
        │               ├── parameters.test.ts
        │               └── prompts.test.ts
        ├── tsconfig.json
        └── tsup.config.ts
```

# Files

--------------------------------------------------------------------------------
/llm/ai-sdk/meter/tests/ai-sdk-billing-wrapper-general.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * General tests for AI SDK billing wrapper functionality
  3 |  * Tests provider detection, model wrapping, and error handling with mocks
  4 |  */
  5 | 
  6 | import Stripe from 'stripe';
  7 | import {openai} from '@ai-sdk/openai';
  8 | import {anthropic} from '@ai-sdk/anthropic';
  9 | import {google} from '@ai-sdk/google';
 10 | import {meteredModel} from '../index';
 11 | import {determineProvider} from '../utils';
 12 | 
 13 | // Mock Stripe
 14 | jest.mock('stripe');
 15 | 
 16 | describe('AI SDK Billing Wrapper - General', () => {
 17 |   let mockMeterEventsCreate: jest.Mock;
 18 |   const TEST_API_KEY = 'sk_test_mock_key';
 19 | 
 20 |   beforeEach(() => {
 21 |     mockMeterEventsCreate = jest.fn().mockResolvedValue({});
 22 |     
 23 |     // Mock the Stripe constructor
 24 |     (Stripe as unknown as jest.Mock).mockImplementation(() => ({
 25 |       v2: {
 26 |         billing: {
 27 |           meterEvents: {
 28 |             create: mockMeterEventsCreate,
 29 |           },
 30 |         },
 31 |       },
 32 |     }));
 33 |   });
 34 | 
 35 |   afterEach(() => {
 36 |     jest.clearAllMocks();
 37 |   });
 38 | 
 39 |   describe('Provider Detection', () => {
 40 |     it('should correctly detect OpenAI provider', () => {
 41 |       expect(determineProvider('openai')).toBe('openai');
 42 |       expect(determineProvider('openai.chat')).toBe('openai');
 43 |     });
 44 | 
 45 |     it('should correctly detect Anthropic provider', () => {
 46 |       expect(determineProvider('anthropic')).toBe('anthropic');
 47 |       expect(determineProvider('anthropic.messages')).toBe('anthropic');
 48 |     });
 49 | 
 50 |     it('should correctly detect Google provider', () => {
 51 |       expect(determineProvider('google')).toBe('google');
 52 |       expect(determineProvider('google-generative-ai')).toBe('google');
 53 |       expect(determineProvider('gemini')).toBe('google');
 54 |     });
 55 | 
 56 |     it('should correctly detect Azure provider', () => {
 57 |       expect(determineProvider('azure')).toBe('azure');
 58 |       expect(determineProvider('azure-openai')).toBe('azure');
 59 |     });
 60 | 
 61 |     it('should correctly detect Bedrock provider', () => {
 62 |       expect(determineProvider('bedrock')).toBe('bedrock');
 63 |       expect(determineProvider('amazon-bedrock')).toBe('bedrock');
 64 |     });
 65 | 
 66 |     it('should correctly detect other providers', () => {
 67 |       expect(determineProvider('groq')).toBe('groq');
 68 |       expect(determineProvider('huggingface')).toBe('huggingface');
 69 |       expect(determineProvider('together')).toBe('together');
 70 |     });
 71 | 
 72 |     it('should return lowercased provider name for unknown providers', () => {
 73 |       expect(determineProvider('unknown-provider')).toBe('unknown-provider');
 74 |       expect(determineProvider('Custom-Provider')).toBe('custom-provider');
 75 |       expect(determineProvider('MY-NEW-AI')).toBe('my-new-ai');
 76 |     });
 77 |   });
 78 | 
 79 |   describe('Model Wrapping', () => {
 80 |     it('should return wrapped model with same specification version', () => {
 81 |       const originalModel = openai('gpt-4o-mini');
 82 |       const wrappedModel = meteredModel(originalModel, TEST_API_KEY, 'cus_test123');
 83 | 
 84 |       expect(wrappedModel.specificationVersion).toBe(originalModel.specificationVersion);
 85 |     });
 86 | 
 87 |     it('should preserve model ID', () => {
 88 |       const originalModel = openai('gpt-4o-mini');
 89 |       const wrappedModel = meteredModel(originalModel, TEST_API_KEY, 'cus_test123');
 90 | 
 91 |       expect(wrappedModel.modelId).toBe(originalModel.modelId);
 92 |     });
 93 | 
 94 |     it('should preserve provider', () => {
 95 |       const originalModel = openai('gpt-4o-mini');
 96 |       const wrappedModel = meteredModel(originalModel, TEST_API_KEY, 'cus_test123');
 97 | 
 98 |       expect(wrappedModel.provider).toBe(originalModel.provider);
 99 |     });
100 |   });
101 | 
102 |   describe('Error Handling', () => {
103 |     it('should throw error for model without specification version', () => {
104 |       const mockModel = {
105 |         modelId: 'test-model',
106 |         provider: 'test-provider',
107 |       } as any;
108 | 
109 |       expect(() => {
110 |         meteredModel(mockModel, TEST_API_KEY, 'cus_test123');
111 |       }).toThrow('Only LanguageModelV2 models are supported');
112 |     });
113 | 
114 |     it('should throw error for unsupported specification version', () => {
115 |       const mockModel = {
116 |         modelId: 'test-model',
117 |         provider: 'test-provider',
118 |         specificationVersion: 'v99',
119 |       } as any;
120 | 
121 |       expect(() => {
122 |         meteredModel(mockModel, TEST_API_KEY, 'cus_test123');
123 |       }).toThrow('Only LanguageModelV2 models are supported');
124 |     });
125 | 
126 |     it('should provide clear error messages', () => {
127 |       const mockModel = {
128 |         modelId: 'test-model',
129 |         provider: 'test-provider',
130 |         specificationVersion: 'v1',
131 |       } as any;
132 | 
133 |       expect(() => {
134 |         meteredModel(mockModel, TEST_API_KEY, 'cus_test123');
135 |       }).toThrow(/specificationVersion "v2"/);
136 |       expect(() => {
137 |         meteredModel(mockModel, TEST_API_KEY, 'cus_test123');
138 |       }).toThrow(/OpenAI, Anthropic, Google/);
139 |     });
140 |   });
141 | 
142 |   describe('Multi-Provider Integration', () => {
143 |     it('should work with different providers', () => {
144 |       const openaiModel = meteredModel(openai('gpt-4o-mini'), TEST_API_KEY, 'cus_test');
145 |       const anthropicModel = meteredModel(anthropic('claude-3-5-haiku-20241022'), TEST_API_KEY, 'cus_test');
146 |       const googleModel = meteredModel(google('gemini-2.5-flash'), TEST_API_KEY, 'cus_test');
147 | 
148 |       // Verify all models are wrapped correctly
149 |       expect(openaiModel).toBeDefined();
150 |       expect(anthropicModel).toBeDefined();
151 |       expect(googleModel).toBeDefined();
152 |       
153 |       // Verify model IDs are preserved
154 |       expect(openaiModel.modelId).toBe('gpt-4o-mini');
155 |       expect(anthropicModel.modelId).toBe('claude-3-5-haiku-20241022');
156 |       expect(googleModel.modelId).toBe('gemini-2.5-flash');
157 |     });
158 | 
159 |     it('should support custom v2 provider', () => {
160 |       const customModel = {
161 |         modelId: 'custom-123',
162 |         provider: 'custom-ai',
163 |         specificationVersion: 'v2',
164 |         doGenerate: jest.fn(),
165 |         doStream: jest.fn(),
166 |       } as any;
167 | 
168 |       const wrapped = meteredModel(customModel, TEST_API_KEY, 'cus_test');
169 |       
170 |       expect(wrapped).toBeDefined();
171 |       expect(wrapped.provider).toBe('custom-ai');
172 |       expect(wrapped.modelId).toBe('custom-123');
173 |     });
174 |   });
175 | });
176 | 
```

--------------------------------------------------------------------------------
/tools/modelcontextprotocol/src/test/index.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {main} from '../index';
  2 | import {parseArgs} from '../index';
  3 | import {StripeAgentToolkit} from '@stripe/agent-toolkit/modelcontextprotocol';
  4 | import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
  5 | describe('parseArgs function', () => {
  6 |   describe('success cases', () => {
  7 |     it('should parse api-key, tools and stripe-header arguments correctly', () => {
  8 |       const args = [
  9 |         '--api-key=sk_test_123',
 10 |         '--tools=all',
 11 |         '--stripe-account=acct_123',
 12 |       ];
 13 |       const options = parseArgs(args);
 14 |       expect(options.apiKey).toBe('sk_test_123');
 15 |       expect(options.tools).toEqual(['all']);
 16 |       expect(options.stripeAccount).toBe('acct_123');
 17 |     });
 18 | 
 19 |     it('should parse api-key and tools arguments correctly', () => {
 20 |       const args = ['--api-key=sk_test_123', '--tools=all'];
 21 |       const options = parseArgs(args);
 22 |       expect(options.apiKey).toBe('sk_test_123');
 23 |       expect(options.tools).toEqual(['all']);
 24 |     });
 25 | 
 26 |     it('should parse restricted api key correctly', () => {
 27 |       const args = ['--api-key=rk_test_123', '--tools=all'];
 28 |       const options = parseArgs(args);
 29 |       expect(options.apiKey).toBe('rk_test_123');
 30 |       expect(options.tools).toEqual(['all']);
 31 |     });
 32 | 
 33 |     it('if api key set in env variable, should parse tools argument correctly', () => {
 34 |       process.env.STRIPE_SECRET_KEY = 'sk_test_123';
 35 |       const args = ['--tools=all'];
 36 |       const options = parseArgs(args);
 37 |       expect(options.apiKey).toBe('sk_test_123');
 38 |       expect(options.tools).toEqual(['all']);
 39 |     });
 40 | 
 41 |     it('if api key set in env variable but also passed into args, should prefer args key', () => {
 42 |       process.env.STRIPE_SECRET_KEY = 'sk_test_123';
 43 |       const args = ['--api-key=sk_test_456', '--tools=all'];
 44 |       const options = parseArgs(args);
 45 |       expect(options.apiKey).toBe('sk_test_456');
 46 |       expect(options.tools).toEqual(['all']);
 47 |       delete process.env.STRIPE_SECRET_KEY;
 48 |     });
 49 | 
 50 |     it('should parse tools argument correctly if a list of tools is provided', () => {
 51 |       const args = [
 52 |         '--api-key=sk_test_123',
 53 |         '--tools=customers.create,products.read,documentation.read',
 54 |       ];
 55 |       const options = parseArgs(args);
 56 |       expect(options.tools).toEqual([
 57 |         'customers.create',
 58 |         'products.read',
 59 |         'documentation.read',
 60 |       ]);
 61 |       expect(options.apiKey).toBe('sk_test_123');
 62 |     });
 63 | 
 64 |     it('ignore all arguments not prefixed with --', () => {
 65 |       const args = [
 66 |         '--api-key=sk_test_123',
 67 |         '--tools=all',
 68 |         'stripe-account=acct_123',
 69 |       ];
 70 |       const options = parseArgs(args);
 71 |       expect(options.apiKey).toBe('sk_test_123');
 72 |       expect(options.tools).toEqual(['all']);
 73 |       expect(options.stripeAccount).toBeUndefined();
 74 |     });
 75 |   });
 76 | 
 77 |   describe('error cases', () => {
 78 |     it("should throw an error if api-key does not start with 'sk_' or 'rk_'", () => {
 79 |       const args = ['--api-key=test_123', '--tools=all'];
 80 |       expect(() => parseArgs(args)).toThrow(
 81 |         'API key must start with "sk_" or "rk_".'
 82 |       );
 83 |     });
 84 | 
 85 |     it('should throw an error if api-key is not provided', () => {
 86 |       const args = ['--tools=all'];
 87 |       expect(() => parseArgs(args)).toThrow(
 88 |         'Stripe API key not provided. Please either pass it as an argument --api-key=$KEY or set the STRIPE_SECRET_KEY environment variable.'
 89 |       );
 90 |     });
 91 | 
 92 |     it("should throw an error if stripe-account does not start with 'acct_'", () => {
 93 |       const args = [
 94 |         '--api-key=sk_test_123',
 95 |         '--tools=all',
 96 |         '--stripe-account=test_123',
 97 |       ];
 98 |       expect(() => parseArgs(args)).toThrow(
 99 |         'Stripe account must start with "acct_".'
100 |       );
101 |     });
102 | 
103 |     it('should throw an error if tools argument is not provided', () => {
104 |       const args = ['--api-key=sk_test_123'];
105 |       expect(() => parseArgs(args)).toThrow(
106 |         'The --tools arguments must be provided.'
107 |       );
108 |     });
109 | 
110 |     it('should throw an error if an invalid argument is provided', () => {
111 |       const args = [
112 |         '--invalid-arg=value',
113 |         '--api-key=sk_test_123',
114 |         '--tools=all',
115 |       ];
116 |       expect(() => parseArgs(args)).toThrow(
117 |         'Invalid argument: invalid-arg. Accepted arguments are: api-key, tools, stripe-account'
118 |       );
119 |     });
120 | 
121 |     it('should throw an error if tools is not in accepted tool list', () => {
122 |       const args = [
123 |         '--api-key=sk_test_123',
124 |         '--tools=customers.create,products.read,fake.tool',
125 |       ];
126 |       expect(() => parseArgs(args)).toThrow(
127 |         /Invalid tool: fake\.tool\. Accepted tools are: .*$/
128 |       );
129 |     });
130 |   });
131 | });
132 | 
133 | jest.mock('@stripe/agent-toolkit/modelcontextprotocol');
134 | jest.mock('@modelcontextprotocol/sdk/server/stdio.js');
135 | 
136 | describe('main function', () => {
137 |   beforeEach(() => {
138 |     jest.clearAllMocks();
139 |   });
140 | 
141 |   it('should initialize the server with tools=all correctly', async () => {
142 |     process.argv = ['node', 'index.js', '--api-key=sk_test_123', '--tools=all'];
143 | 
144 |     await main();
145 | 
146 |     expect(StripeAgentToolkit).toHaveBeenCalledWith({
147 |       secretKey: 'sk_test_123',
148 |       configuration: {
149 |         actions: ALL_ACTIONS,
150 |         context: {mode: 'modelcontextprotocol'},
151 |       },
152 |     });
153 | 
154 |     expect(StdioServerTransport).toHaveBeenCalled();
155 |   });
156 | 
157 |   it('should initialize the server with specific list of tools correctly', async () => {
158 |     process.argv = [
159 |       'node',
160 |       'index.js',
161 |       '--api-key=sk_test_123',
162 |       '--tools=customers.create,products.read,documentation.read',
163 |     ];
164 | 
165 |     await main();
166 | 
167 |     expect(StripeAgentToolkit).toHaveBeenCalledWith({
168 |       secretKey: 'sk_test_123',
169 |       configuration: {
170 |         actions: {
171 |           customers: {
172 |             create: true,
173 |           },
174 |           products: {
175 |             read: true,
176 |           },
177 |           documentation: {
178 |             read: true,
179 |           },
180 |         },
181 |         context: {
182 |           mode: 'modelcontextprotocol',
183 |         },
184 |       },
185 |     });
186 | 
187 |     expect(StdioServerTransport).toHaveBeenCalled();
188 |   });
189 | 
190 |   it('should initialize the server with stripe header', async () => {
191 |     process.argv = [
192 |       'node',
193 |       'index.js',
194 |       '--api-key=sk_test_123',
195 |       '--tools=all',
196 |       '--stripe-account=acct_123',
197 |     ];
198 | 
199 |     await main();
200 | 
201 |     expect(StripeAgentToolkit).toHaveBeenCalledWith({
202 |       secretKey: 'sk_test_123',
203 |       configuration: {
204 |         actions: ALL_ACTIONS,
205 |         context: {account: 'acct_123', mode: 'modelcontextprotocol'},
206 |       },
207 |     });
208 | 
209 |     expect(StdioServerTransport).toHaveBeenCalled();
210 |   });
211 | });
212 | 
213 | const ALL_ACTIONS = {
214 |   customers: {
215 |     create: true,
216 |     read: true,
217 |   },
218 |   coupons: {
219 |     create: true,
220 |     read: true,
221 |   },
222 |   invoices: {
223 |     create: true,
224 |     update: true,
225 |     read: true,
226 |   },
227 |   invoiceItems: {
228 |     create: true,
229 |   },
230 |   paymentLinks: {
231 |     create: true,
232 |   },
233 |   products: {
234 |     create: true,
235 |     read: true,
236 |   },
237 |   prices: {
238 |     create: true,
239 |     read: true,
240 |   },
241 |   balance: {
242 |     read: true,
243 |   },
244 |   refunds: {
245 |     create: true,
246 |   },
247 |   subscriptions: {
248 |     read: true,
249 |     update: true,
250 |   },
251 |   paymentIntents: {
252 |     read: true,
253 |   },
254 |   disputes: {
255 |     read: true,
256 |     update: true,
257 |   },
258 |   documentation: {
259 |     read: true,
260 |   },
261 | };
262 | 
```

--------------------------------------------------------------------------------
/tools/typescript/src/test/shared/subscriptions/functions.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {listSubscriptions} from '@/shared/subscriptions/listSubscriptions';
  2 | import {cancelSubscription} from '@/shared/subscriptions/cancelSubscription';
  3 | import {updateSubscription} from '@/shared/subscriptions/updateSubscription';
  4 | 
  5 | const Stripe = jest.fn().mockImplementation(() => ({
  6 |   subscriptions: {
  7 |     list: jest.fn(),
  8 |     cancel: jest.fn(),
  9 |     update: jest.fn(),
 10 |   },
 11 | }));
 12 | 
 13 | let stripe: ReturnType<typeof Stripe>;
 14 | 
 15 | beforeEach(() => {
 16 |   stripe = new Stripe('fake-api-key');
 17 | });
 18 | 
 19 | describe('listSubscriptions', () => {
 20 |   it('should list subscriptions and return data', async () => {
 21 |     const mockSubscriptions = [
 22 |       {
 23 |         id: 'sub_123456',
 24 |         customer: 'cus_123456',
 25 |         status: 'active',
 26 |         current_period_start: 1609459200, // 2021-01-01
 27 |         current_period_end: 1612137600, // 2021-02-01
 28 |         items: {
 29 |           data: [
 30 |             {
 31 |               id: 'si_123',
 32 |               price: 'price_123',
 33 |               quantity: 1,
 34 |             },
 35 |           ],
 36 |         },
 37 |       },
 38 |       {
 39 |         id: 'sub_789012',
 40 |         customer: 'cus_123456',
 41 |         status: 'canceled',
 42 |         current_period_start: 1609459200, // 2021-01-01
 43 |         current_period_end: 1612137600, // 2021-02-01
 44 |         items: {
 45 |           data: [
 46 |             {
 47 |               id: 'si_456',
 48 |               price: 'price_456',
 49 |               quantity: 2,
 50 |             },
 51 |           ],
 52 |         },
 53 |       },
 54 |     ];
 55 | 
 56 |     const context = {};
 57 |     const params = {};
 58 | 
 59 |     stripe.subscriptions.list.mockResolvedValue({data: mockSubscriptions});
 60 |     const result = await listSubscriptions(stripe, context, params);
 61 | 
 62 |     expect(stripe.subscriptions.list).toHaveBeenCalledWith(params, undefined);
 63 |     expect(result).toEqual(mockSubscriptions);
 64 |   });
 65 | 
 66 |   it('should add customer from context if provided', async () => {
 67 |     const mockSubscriptions = [
 68 |       {
 69 |         id: 'sub_123456',
 70 |         customer: 'cus_123456',
 71 |         status: 'active',
 72 |         current_period_start: 1609459200,
 73 |         current_period_end: 1612137600,
 74 |         items: {
 75 |           data: [
 76 |             {
 77 |               id: 'si_123',
 78 |               price: 'price_123',
 79 |               quantity: 1,
 80 |             },
 81 |           ],
 82 |         },
 83 |       },
 84 |     ];
 85 | 
 86 |     const context = {
 87 |       customer: 'cus_123456',
 88 |     };
 89 |     const params = {};
 90 | 
 91 |     stripe.subscriptions.list.mockResolvedValue({data: mockSubscriptions});
 92 |     const result = await listSubscriptions(stripe, context, params);
 93 | 
 94 |     expect(stripe.subscriptions.list).toHaveBeenCalledWith(
 95 |       {customer: 'cus_123456'},
 96 |       undefined
 97 |     );
 98 |     expect(result).toEqual(mockSubscriptions);
 99 |   });
100 | 
101 |   it('should specify the connected account if included in context', async () => {
102 |     const mockSubscriptions = [
103 |       {
104 |         id: 'sub_123456',
105 |         customer: 'cus_123456',
106 |         status: 'active',
107 |         current_period_start: 1609459200,
108 |         current_period_end: 1612137600,
109 |         items: {
110 |           data: [
111 |             {
112 |               id: 'si_123',
113 |               price: 'price_123',
114 |               quantity: 1,
115 |             },
116 |           ],
117 |         },
118 |       },
119 |     ];
120 | 
121 |     const context = {
122 |       account: 'acct_123456',
123 |     };
124 |     const params = {};
125 | 
126 |     stripe.subscriptions.list.mockResolvedValue({data: mockSubscriptions});
127 |     const result = await listSubscriptions(stripe, context, params);
128 | 
129 |     expect(stripe.subscriptions.list).toHaveBeenCalledWith(params, {
130 |       stripeAccount: context.account,
131 |     });
132 |     expect(result).toEqual(mockSubscriptions);
133 |   });
134 | 
135 |   it('should handle errors gracefully', async () => {
136 |     const context = {};
137 |     const params = {};
138 | 
139 |     stripe.subscriptions.list.mockRejectedValue(new Error('API Error'));
140 |     const result = await listSubscriptions(stripe, context, params);
141 | 
142 |     expect(result).toBe('Failed to list subscriptions');
143 |   });
144 | });
145 | 
146 | describe('cancelSubscription', () => {
147 |   it('should cancel a subscription and return the result', async () => {
148 |     const mockSubscription = {
149 |       id: 'sub_123456',
150 |       customer: 'cus_123456',
151 |       status: 'active',
152 |       current_period_start: 1609459200,
153 |       current_period_end: 1612137600,
154 |       items: {
155 |         data: [
156 |           {
157 |             id: 'si_123',
158 |             price: 'price_123',
159 |             quantity: 1,
160 |           },
161 |         ],
162 |       },
163 |     };
164 | 
165 |     const context = {};
166 |     const params = {
167 |       subscription: 'sub_123456',
168 |     };
169 | 
170 |     stripe.subscriptions.cancel.mockResolvedValue(mockSubscription);
171 |     const result = await cancelSubscription(stripe, context, params);
172 | 
173 |     expect(stripe.subscriptions.cancel).toHaveBeenCalledWith(
174 |       'sub_123456',
175 |       {},
176 |       undefined
177 |     );
178 |     expect(result).toEqual(mockSubscription);
179 |   });
180 | 
181 |   it('should handle errors gracefully', async () => {
182 |     const context = {};
183 |     const params = {
184 |       subscription: 'sub_123456',
185 |     };
186 | 
187 |     stripe.subscriptions.cancel.mockRejectedValue(new Error('API Error'));
188 |     const result = await cancelSubscription(stripe, context, params);
189 | 
190 |     expect(result).toBe('Failed to cancel subscription');
191 |   });
192 | });
193 | 
194 | describe('updateSubscription', () => {
195 |   it('should update a subscription and return the result', async () => {
196 |     const mockSubscription = {
197 |       id: 'sub_123456',
198 |       customer: 'cus_123456',
199 |       status: 'active',
200 |       current_period_start: 1609459200,
201 |       current_period_end: 1612137600,
202 |       items: {
203 |         data: [
204 |           {
205 |             id: 'si_123',
206 |             price: 'price_123',
207 |             quantity: 1,
208 |           },
209 |         ],
210 |       },
211 |     };
212 | 
213 |     const context = {};
214 |     const params = {
215 |       subscription: 'sub_123456',
216 |       items: [
217 |         {
218 |           id: 'si_123',
219 |           quantity: 2,
220 |         },
221 |       ],
222 |     };
223 | 
224 |     stripe.subscriptions.update.mockResolvedValue(mockSubscription);
225 |     const result = await updateSubscription(stripe, context, params);
226 | 
227 |     expect(stripe.subscriptions.update).toHaveBeenCalledWith(
228 |       'sub_123456',
229 |       {
230 |         items: [
231 |           {
232 |             id: 'si_123',
233 |             quantity: 2,
234 |           },
235 |         ],
236 |       },
237 |       undefined
238 |     );
239 |     expect(result).toEqual(mockSubscription);
240 |   });
241 | 
242 |   it('should handle errors gracefully', async () => {
243 |     const context = {};
244 |     const params = {
245 |       subscription: 'sub_123456',
246 |       items: [
247 |         {
248 |           id: 'si_123',
249 |           quantity: 2,
250 |         },
251 |       ],
252 |     };
253 | 
254 |     stripe.subscriptions.update.mockRejectedValue(new Error('API Error'));
255 |     const result = await updateSubscription(stripe, context, params);
256 | 
257 |     expect(result).toBe('Failed to update subscription');
258 |   });
259 | 
260 |   it('should specify the connected account if included in context', async () => {
261 |     const mockSubscription = {
262 |       id: 'sub_123456',
263 |       customer: 'cus_123456',
264 |       status: 'active',
265 |       current_period_start: 1609459200,
266 |       current_period_end: 1612137600,
267 |       items: {
268 |         data: [
269 |           {
270 |             id: 'si_123',
271 |             price: 'price_123',
272 |             quantity: 1,
273 |           },
274 |         ],
275 |       },
276 |     };
277 | 
278 |     const context = {
279 |       account: 'acct_123456',
280 |     };
281 |     const params = {
282 |       subscription: 'sub_123456',
283 |       cancel_at_period_end: true,
284 |     };
285 | 
286 |     stripe.subscriptions.update.mockResolvedValue(mockSubscription);
287 |     const result = await updateSubscription(stripe, context, params);
288 | 
289 |     expect(stripe.subscriptions.update).toHaveBeenCalledWith(
290 |       'sub_123456',
291 |       {
292 |         cancel_at_period_end: true,
293 |       },
294 |       {
295 |         stripeAccount: context.account,
296 |       }
297 |     );
298 |     expect(result).toEqual(mockSubscription);
299 |   });
300 | });
301 | 
```

--------------------------------------------------------------------------------
/llm/token-meter/token-meter.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Generic token metering implementation
  3 |  */
  4 | 
  5 | import Stripe from 'stripe';
  6 | import type OpenAI from 'openai';
  7 | import type {Stream} from 'openai/streaming';
  8 | import type Anthropic from '@anthropic-ai/sdk';
  9 | import type {Stream as AnthropicStream} from '@anthropic-ai/sdk/streaming';
 10 | import type {
 11 |   GenerateContentResult,
 12 |   GenerateContentStreamResult,
 13 | } from '@google/generative-ai';
 14 | import type {MeterConfig} from './types';
 15 | import {logUsageEvent} from './meter-event-logging';
 16 | import {
 17 |   detectResponse,
 18 |   isGeminiStream,
 19 |   extractUsageFromChatStream,
 20 |   extractUsageFromResponseStream,
 21 |   extractUsageFromAnthropicStream,
 22 |   type DetectedResponse,
 23 | } from './utils/type-detection';
 24 | 
 25 | /**
 26 |  * Supported response types from all AI providers
 27 |  */
 28 | export type SupportedResponse =
 29 |   | OpenAI.ChatCompletion
 30 |   | OpenAI.Responses.Response
 31 |   | OpenAI.CreateEmbeddingResponse
 32 |   | Anthropic.Messages.Message
 33 |   | GenerateContentResult;
 34 | 
 35 | /**
 36 |  * Supported stream types from all AI providers
 37 |  */
 38 | export type SupportedStream =
 39 |   | Stream<OpenAI.ChatCompletionChunk>
 40 |   | Stream<OpenAI.Responses.ResponseStreamEvent>
 41 |   | AnthropicStream<Anthropic.Messages.RawMessageStreamEvent>
 42 |   | GenerateContentStreamResult;
 43 | 
 44 | /**
 45 |  * Generic token meter interface
 46 |  */
 47 | export interface TokenMeter {
 48 |   /**
 49 |    * Track usage from any supported response type (fire-and-forget)
 50 |    * Automatically detects provider and response type
 51 |    */
 52 |   trackUsage(response: SupportedResponse, stripeCustomerId: string): void;
 53 | 
 54 |   /**
 55 |    * Track usage from OpenAI streaming response
 56 |    * Model name is automatically extracted from the stream
 57 |    * Returns the wrapped stream for consumption
 58 |    */
 59 |   trackUsageStreamOpenAI<
 60 |     T extends
 61 |       | Stream<OpenAI.ChatCompletionChunk>
 62 |       | Stream<OpenAI.Responses.ResponseStreamEvent>
 63 |   >(
 64 |     stream: T,
 65 |     stripeCustomerId: string
 66 |   ): T;
 67 | 
 68 |   /**
 69 |    * Track usage from Anthropic streaming response
 70 |    * Model name is automatically extracted from the stream
 71 |    * Returns the wrapped stream for consumption
 72 |    */
 73 |   trackUsageStreamAnthropic(
 74 |     stream: AnthropicStream<Anthropic.Messages.RawMessageStreamEvent>,
 75 |     stripeCustomerId: string
 76 |   ): AnthropicStream<Anthropic.Messages.RawMessageStreamEvent>;
 77 | 
 78 |   /**
 79 |    * Track usage from Gemini/Google streaming response
 80 |    * Model name must be provided as Gemini streams don't include it
 81 |    * Returns the wrapped stream for consumption
 82 |    */
 83 |   trackUsageStreamGemini(
 84 |     stream: GenerateContentStreamResult,
 85 |     stripeCustomerId: string,
 86 |     modelName: string
 87 |   ): GenerateContentStreamResult;
 88 | }
 89 | 
 90 | /**
 91 |  * Create a generic token meter that works with any supported AI provider
 92 |  *
 93 |  * @param stripeApiKey - Your Stripe API key
 94 |  * @param config - Optional configuration for the meter
 95 |  * @returns TokenMeter instance for tracking usage
 96 |  */
 97 | export function createTokenMeter(
 98 |   stripeApiKey: string,
 99 |   config: MeterConfig = {}
100 | ): TokenMeter {
101 |   // Construct Stripe client with the API key
102 |   const stripeClient = new Stripe(stripeApiKey, {
103 |     appInfo: {
104 |       name: '@stripe/token-meter',
105 |       version: '0.1.0',
106 |     },
107 |   });
108 |   return {
109 |     trackUsage(response: SupportedResponse, stripeCustomerId: string): void {
110 |       const detected = detectResponse(response);
111 | 
112 |       if (!detected) {
113 |         console.warn(
114 |           'Unable to detect response type. Supported types: OpenAI ChatCompletion, Responses API, Embeddings'
115 |         );
116 |         return;
117 |       }
118 | 
119 |       // Fire-and-forget logging
120 |       logUsageEvent(stripeClient, config, {
121 |         model: detected.model,
122 |         provider: detected.provider,
123 |         usage: {
124 |           inputTokens: detected.inputTokens,
125 |           outputTokens: detected.outputTokens,
126 |         },
127 |         stripeCustomerId,
128 |       });
129 |     },
130 | 
131 |     trackUsageStreamGemini(
132 |       stream: GenerateContentStreamResult,
133 |       stripeCustomerId: string,
134 |       modelName: string
135 |     ): GenerateContentStreamResult {
136 |       const originalStream = stream.stream;
137 |         
138 |         const wrappedStream = (async function* () {
139 |           let lastUsageMetadata: any = null;
140 | 
141 |           for await (const chunk of originalStream) {
142 |             if (chunk.usageMetadata) {
143 |               lastUsageMetadata = chunk.usageMetadata;
144 |             }
145 |             yield chunk;
146 |           }
147 | 
148 |           // Log usage after stream completes
149 |           if (lastUsageMetadata) {
150 |             const baseOutputTokens = lastUsageMetadata?.candidatesTokenCount ?? 0;
151 |             // thoughtsTokenCount is for extended thinking models, may not always be present
152 |             const reasoningTokens = (lastUsageMetadata as any)?.thoughtsTokenCount ?? 0;
153 | 
154 |             logUsageEvent(stripeClient, config, {
155 |               model: modelName,
156 |               provider: 'google',
157 |               usage: {
158 |                 inputTokens: lastUsageMetadata?.promptTokenCount ?? 0,
159 |                 outputTokens: baseOutputTokens + reasoningTokens,
160 |               },
161 |               stripeCustomerId,
162 |             });
163 |           }
164 |         })();
165 | 
166 |         // Return the wrapped structure
167 |         return {
168 |           stream: wrappedStream,
169 |           response: stream.response,
170 |         };
171 |     },
172 | 
173 |     trackUsageStreamOpenAI<
174 |       T extends
175 |         | Stream<OpenAI.ChatCompletionChunk>
176 |         | Stream<OpenAI.Responses.ResponseStreamEvent>
177 |     >(stream: T, stripeCustomerId: string): T {
178 |       const [peekStream, stream2] = stream.tee();
179 | 
180 |       (async () => {
181 |         // Peek at the first chunk to determine stream type
182 |         const [stream1, meterStream] = peekStream.tee();
183 |         const reader = stream1[Symbol.asyncIterator]();
184 |         const firstChunk = await reader.next();
185 |         
186 |         let detected: DetectedResponse | null = null;
187 |         
188 |         if (!firstChunk.done && firstChunk.value) {
189 |           const chunk = firstChunk.value as any;
190 |           
191 |           // Check if it's an OpenAI Chat stream (has choices array)
192 |           if ('choices' in chunk && Array.isArray(chunk.choices)) {
193 |             detected = await extractUsageFromChatStream(meterStream as any);
194 |           }
195 |           // Check if it's an OpenAI Response API stream (has type starting with 'response.')
196 |           else if (chunk.type && typeof chunk.type === 'string' && chunk.type.startsWith('response.')) {
197 |             detected = await extractUsageFromResponseStream(meterStream as any);
198 |           }
199 |           else {
200 |             console.warn('Unable to detect OpenAI stream type from first chunk:', chunk);
201 |           }
202 |         }
203 | 
204 |         if (detected) {
205 |           logUsageEvent(stripeClient, config, {
206 |             model: detected.model,
207 |             provider: detected.provider,
208 |             usage: {
209 |               inputTokens: detected.inputTokens,
210 |               outputTokens: detected.outputTokens,
211 |             },
212 |             stripeCustomerId,
213 |           });
214 |         } else {
215 |           console.warn('Unable to extract usage from OpenAI stream');
216 |         }
217 |       })();
218 | 
219 |       return stream2 as T;
220 |     },
221 | 
222 |     trackUsageStreamAnthropic(
223 |       stream: AnthropicStream<Anthropic.Messages.RawMessageStreamEvent>,
224 |       stripeCustomerId: string
225 |     ): AnthropicStream<Anthropic.Messages.RawMessageStreamEvent> {
226 |       const [peekStream, stream2] = stream.tee();
227 | 
228 |       (async () => {
229 |         const detected = await extractUsageFromAnthropicStream(peekStream);
230 | 
231 |         if (detected) {
232 |           logUsageEvent(stripeClient, config, {
233 |             model: detected.model,
234 |             provider: detected.provider,
235 |             usage: {
236 |               inputTokens: detected.inputTokens,
237 |               outputTokens: detected.outputTokens,
238 |             },
239 |             stripeCustomerId,
240 |           });
241 |         } else {
242 |           console.warn('Unable to extract usage from Anthropic stream');
243 |         }
244 |       })();
245 | 
246 |       return stream2;
247 |     },
248 |   };
249 | }
250 | 
251 | 
```

--------------------------------------------------------------------------------
/tools/typescript/src/test/modelcontextprotocol/register-paid-tool.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {z} from 'zod';
  2 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
  3 | import {registerPaidTool} from '../../modelcontextprotocol/register-paid-tool';
  4 | import Stripe from 'stripe';
  5 | import type {
  6 |   ServerNotification,
  7 |   ServerRequest,
  8 | } from '@modelcontextprotocol/sdk/types.js';
  9 | import type {RequestHandlerExtra} from '@modelcontextprotocol/sdk/shared/protocol.js';
 10 | 
 11 | // Mock Stripe
 12 | jest.mock('stripe');
 13 | const mockSecretKey = 'sk_test_123';
 14 | 
 15 | describe('registerPaidTool', () => {
 16 |   let mockMcpServer: jest.Mocked<McpServer>;
 17 |   let mockStripe: jest.Mocked<any>;
 18 |   let mockExtra: RequestHandlerExtra<ServerRequest, ServerNotification>;
 19 | 
 20 |   beforeEach(() => {
 21 |     // Reset all mocks
 22 |     jest.clearAllMocks();
 23 | 
 24 |     // Mock McpServer
 25 |     mockMcpServer = {
 26 |       tool: jest.fn(),
 27 |     } as any;
 28 | 
 29 |     // Mock Stripe instance and methods
 30 |     mockStripe = {
 31 |       customers: {
 32 |         list: jest.fn(),
 33 |         create: jest.fn(),
 34 |       },
 35 |       checkout: {
 36 |         sessions: {
 37 |           create: jest.fn(),
 38 |           retrieve: jest.fn(),
 39 |           list: jest.fn(),
 40 |         },
 41 |       },
 42 |       subscriptions: {
 43 |         list: jest.fn(),
 44 |       },
 45 |       billing: {
 46 |         meterEvents: {
 47 |           create: jest.fn(),
 48 |         },
 49 |       },
 50 |     };
 51 | 
 52 |     (Stripe as unknown as jest.Mock).mockImplementation(() => mockStripe);
 53 | 
 54 |     // Mock request handler extra
 55 |     mockExtra = {
 56 |       signal: new AbortController().signal,
 57 |       sendNotification: jest.fn(),
 58 |       sendRequest: jest.fn(),
 59 |       requestId: '123',
 60 |     };
 61 |   });
 62 | 
 63 |   it('should register a tool with the McpServer', async () => {
 64 |     const toolName = 'testTool';
 65 |     const toolDescription = 'Test tool description';
 66 |     const paramsSchema = {
 67 |       testParam: z.string(),
 68 |     };
 69 |     const callback = jest.fn();
 70 | 
 71 |     // @ts-ignore: https://github.com/modelcontextprotocol/typescript-sdk/issues/494
 72 |     await registerPaidTool(
 73 |       mockMcpServer,
 74 |       toolName,
 75 |       toolDescription,
 76 |       paramsSchema,
 77 |       callback,
 78 |       {
 79 |         paymentReason: 'Test payment',
 80 |         stripeSecretKey: mockSecretKey,
 81 |         userEmail: '[email protected]',
 82 |         checkout: {
 83 |           success_url: 'https://example.com/success',
 84 |           line_items: [{price: 'price_123', quantity: 1}],
 85 |           mode: 'subscription',
 86 |         },
 87 |       }
 88 |     );
 89 | 
 90 |     expect(mockMcpServer.tool).toHaveBeenCalledWith(
 91 |       toolName,
 92 |       toolDescription,
 93 |       paramsSchema,
 94 |       expect.any(Function)
 95 |     );
 96 |   });
 97 | 
 98 |   it('should create a new customer if one does not exist', async () => {
 99 |     mockStripe.customers.list.mockResolvedValue({data: []});
100 |     mockStripe.customers.create.mockResolvedValue({id: 'cus_123'});
101 |     mockStripe.subscriptions.list.mockResolvedValue({
102 |       data: [
103 |         {
104 |           items: {
105 |             data: [
106 |               {
107 |                 price: {
108 |                   id: 'price_123',
109 |                 },
110 |               },
111 |             ],
112 |           },
113 |         },
114 |       ],
115 |     });
116 |     mockStripe.checkout.sessions.list.mockResolvedValue({data: []});
117 |     mockStripe.checkout.sessions.create.mockResolvedValue({
118 |       id: 'cs_123',
119 |       url: 'https://checkout.stripe.com/123',
120 |     });
121 | 
122 |     const toolName = 'testTool';
123 |     const callback = jest.fn();
124 | 
125 |     await registerPaidTool(
126 |       mockMcpServer,
127 |       toolName,
128 |       'Test description',
129 |       {testParam: z.string()},
130 |       callback,
131 |       {
132 |         paymentReason: 'Test payment',
133 |         stripeSecretKey: mockSecretKey,
134 |         userEmail: '[email protected]',
135 |         checkout: {
136 |           success_url: 'https://example.com/success',
137 |           line_items: [{price: 'price_123', quantity: 1}],
138 |           mode: 'subscription',
139 |         },
140 |       }
141 |     );
142 | 
143 |     const registeredCallback = mockMcpServer.tool.mock.calls[0]?.[3];
144 |     // @ts-ignore: TypeScript can't disambiguate between params schema and annotations
145 |     await registeredCallback({testParam: 'test'}, mockExtra);
146 | 
147 |     expect(mockStripe.customers.list).toHaveBeenCalledWith({
148 |       email: '[email protected]',
149 |     });
150 |     expect(mockStripe.customers.create).toHaveBeenCalledWith({
151 |       email: '[email protected]',
152 |     });
153 |   });
154 | 
155 |   it('should create a checkout session for unpaid tools', async () => {
156 |     mockStripe.customers.list.mockResolvedValue({
157 |       data: [{id: 'cus_123', email: '[email protected]'}],
158 |     });
159 |     mockStripe.checkout.sessions.create.mockResolvedValue({
160 |       id: 'cs_123',
161 |       url: 'https://checkout.stripe.com/123',
162 |     });
163 |     mockStripe.subscriptions.list.mockResolvedValue({
164 |       data: [], // No active subscriptions
165 |     });
166 |     mockStripe.checkout.sessions.list.mockResolvedValue({
167 |       data: [], // No paid sessions
168 |     });
169 | 
170 |     const toolName = 'testTool';
171 |     const callback = jest.fn();
172 | 
173 |     await registerPaidTool(
174 |       mockMcpServer,
175 |       toolName,
176 |       'Test description',
177 |       {testParam: z.string()},
178 |       callback,
179 |       {
180 |         paymentReason: 'Test payment',
181 |         stripeSecretKey: mockSecretKey,
182 |         userEmail: '[email protected]',
183 |         checkout: {
184 |           success_url: 'https://example.com/success',
185 |           line_items: [{price: 'price_123', quantity: 1}],
186 |           mode: 'subscription',
187 |         },
188 |       }
189 |     );
190 | 
191 |     const registeredCallback = mockMcpServer.tool.mock.calls[0]?.[3];
192 |     // @ts-ignore: TypeScript can't disambiguate between params schema and annotations
193 |     const result = await registeredCallback({testParam: 'test'}, mockExtra);
194 | 
195 |     expect(mockStripe.checkout.sessions.create).toHaveBeenCalledWith({
196 |       success_url: 'https://example.com/success',
197 |       line_items: [
198 |         {
199 |           price: 'price_123',
200 |           quantity: 1,
201 |         },
202 |       ],
203 |       mode: 'subscription',
204 |       customer: 'cus_123',
205 |       metadata: {toolName},
206 |     });
207 |     expect(result).toEqual({
208 |       content: [
209 |         {
210 |           type: 'text',
211 |           text: JSON.stringify({
212 |             status: 'payment_required',
213 |             data: {
214 |               paymentType: 'oneTimeSubscription',
215 |               checkoutUrl: 'https://checkout.stripe.com/123',
216 |               paymentReason: 'Test payment',
217 |             },
218 |           }),
219 |         },
220 |       ],
221 |     });
222 |     expect(callback).not.toHaveBeenCalled();
223 |   });
224 | 
225 |   it('should handle usage-based billing when meterEvent is provided', async () => {
226 |     const toolName = 'testTool';
227 |     mockStripe.customers.list.mockResolvedValue({
228 |       data: [{id: 'cus_123', email: '[email protected]'}],
229 |     });
230 |     mockStripe.checkout.sessions.list.mockResolvedValue({
231 |       data: [
232 |         {
233 |           id: 'cs_123',
234 |           metadata: {toolName},
235 |           payment_status: 'paid',
236 |           subscription: 'sub_123',
237 |         },
238 |       ],
239 |     });
240 |     mockStripe.subscriptions.list.mockResolvedValue({
241 |       data: [
242 |         {
243 |           items: {
244 |             data: [
245 |               {
246 |                 price: {
247 |                   id: 'price_123',
248 |                 },
249 |               },
250 |             ],
251 |           },
252 |         },
253 |       ],
254 |     });
255 |     const callback = jest.fn().mockResolvedValue({
256 |       content: [{type: 'text', text: 'Success'}],
257 |     });
258 | 
259 |     await registerPaidTool(
260 |       mockMcpServer,
261 |       toolName,
262 |       'Test description',
263 |       {testParam: z.string()},
264 |       callback,
265 |       {
266 |         paymentReason: 'Test payment',
267 |         meterEvent: 'test.event',
268 |         stripeSecretKey: mockSecretKey,
269 |         userEmail: '[email protected]',
270 |         checkout: {
271 |           success_url: 'https://example.com/success',
272 |           line_items: [{price: 'price_123'}],
273 |           mode: 'subscription',
274 |         },
275 |       }
276 |     );
277 | 
278 |     const registeredCallback = mockMcpServer.tool.mock.calls[0]?.[3];
279 |     // @ts-ignore: TypeScript can't disambiguate between params schema and annotations
280 |     await registeredCallback({testParam: 'test'}, mockExtra);
281 | 
282 |     expect(mockStripe.billing.meterEvents.create).toHaveBeenCalledWith({
283 |       event_name: 'test.event',
284 |       payload: {
285 |         stripe_customer_id: 'cus_123',
286 |         value: '1',
287 |       },
288 |     });
289 |   });
290 | });
291 | 
```

--------------------------------------------------------------------------------
/llm/ai-sdk/provider/utils.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Utility functions for Stripe AI SDK Provider
  3 |  */
  4 | 
  5 | import {
  6 |   LanguageModelV2Prompt,
  7 |   LanguageModelV2FinishReason,
  8 | } from '@ai-sdk/provider';
  9 | 
 10 | type AssistantMessage = Extract<
 11 |   LanguageModelV2Prompt[number],
 12 |   {role: 'assistant'}
 13 | >;
 14 | type UserMessage = Extract<LanguageModelV2Prompt[number], {role: 'user'}>;
 15 | 
 16 | type AssistantContentPart = AssistantMessage['content'][number];
 17 | type UserContentPart = UserMessage['content'][number];
 18 | 
 19 | /**
 20 |  * Type guards for content parts
 21 |  */
 22 | function isTextPart(
 23 |   part: AssistantContentPart
 24 | ): part is Extract<AssistantContentPart, {type: 'text'}> {
 25 |   return part.type === 'text';
 26 | }
 27 | 
 28 | function isToolCallPart(
 29 |   part: AssistantContentPart
 30 | ): part is Extract<AssistantContentPart, {type: 'tool-call'}> {
 31 |   return part.type === 'tool-call';
 32 | }
 33 | 
 34 | function isUserTextPart(
 35 |   part: UserContentPart
 36 | ): part is Extract<UserContentPart, {type: 'text'}> {
 37 |   return part.type === 'text';
 38 | }
 39 | 
 40 | function isUserFilePart(
 41 |   part: UserContentPart
 42 | ): part is Extract<UserContentPart, {type: 'file'}> {
 43 |   return part.type === 'file';
 44 | }
 45 | 
 46 | /**
 47 |  * Converts AI SDK V2 prompt to OpenAI-compatible messages format
 48 |  */
 49 | export function convertToOpenAIMessages(
 50 |   prompt: LanguageModelV2Prompt
 51 | ): Array<{
 52 |   role: string;
 53 |   content:
 54 |     | string
 55 |     | Array<{
 56 |         type: string;
 57 |         text?: string;
 58 |         image_url?: {url: string};
 59 |       }>;
 60 |   tool_calls?: Array<{
 61 |     id: string;
 62 |     type: string;
 63 |     function: {name: string; arguments: string};
 64 |   }>;
 65 |   tool_call_id?: string;
 66 |   name?: string;
 67 | }> {
 68 |   return prompt.map((message) => {
 69 |     switch (message.role) {
 70 |       case 'system':
 71 |         return [{
 72 |           role: 'system',
 73 |           content: message.content,
 74 |         }];
 75 | 
 76 |       case 'user': {
 77 |         const contentParts = message.content.map((part) => {
 78 |           if (isUserTextPart(part)) {
 79 |             return {
 80 |               type: 'text',
 81 |               text: part.text,
 82 |             };
 83 |           } else if (isUserFilePart(part)) {
 84 |             // Convert file data to data URL if needed
 85 |             let fileUrl: string;
 86 |             if (typeof part.data === 'string') {
 87 |               // Could be a URL or base64 data
 88 |               if (part.data.startsWith('http://') || part.data.startsWith('https://')) {
 89 |                 fileUrl = part.data;
 90 |               } else {
 91 |                 // Assume it's base64
 92 |                 fileUrl = `data:${part.mediaType};base64,${part.data}`;
 93 |               }
 94 |             } else if (part.data instanceof URL) {
 95 |               fileUrl = part.data.toString();
 96 |             } else {
 97 |               // Convert Uint8Array to base64 data URL
 98 |               const base64 = btoa(
 99 |                 String.fromCharCode(...Array.from(part.data))
100 |               );
101 |               fileUrl = `data:${part.mediaType};base64,${base64}`;
102 |             }
103 |             
104 |             // For images, use image_url format
105 |             if (part.mediaType.startsWith('image/')) {
106 |               return {
107 |                 type: 'image_url',
108 |                 image_url: {url: fileUrl},
109 |               };
110 |             }
111 |             
112 |             // For other file types, use text representation
113 |             // (OpenAI format doesn't support arbitrary file types well)
114 |             return {
115 |               type: 'text',
116 |               text: `[File: ${part.filename || 'unknown'}, type: ${part.mediaType}]`,
117 |             };
118 |           } else {
119 |             // TypeScript should prevent this, but handle it for runtime safety
120 |             const _exhaustiveCheck: never = part;
121 |             throw new Error(`Unsupported user message part type`);
122 |           }
123 |         });
124 | 
125 |         // If there's only one text part, send as a simple string
126 |         // This is more compatible with Anthropic's requirements
127 |         const content = 
128 |           contentParts.length === 1 && contentParts[0].type === 'text'
129 |             ? contentParts[0].text!
130 |             : contentParts;
131 | 
132 |         return [{
133 |           role: 'user',
134 |           content,
135 |         }];
136 |       }
137 | 
138 |       case 'assistant': {
139 |         // Extract text content
140 |         const textParts = message.content.filter(isTextPart);
141 |         const textContent = textParts.length > 0
142 |           ? textParts.map((part) => part.text).join('')
143 |           : '';
144 | 
145 |         // Extract tool calls
146 |         const toolCallParts = message.content.filter(isToolCallPart);
147 |         const toolCalls = toolCallParts.length > 0
148 |           ? toolCallParts.map((part) => ({
149 |               id: part.toolCallId,
150 |               type: 'function' as const,
151 |               function: {
152 |                 name: part.toolName,
153 |                 arguments:
154 |                   typeof part.input === 'string'
155 |                     ? part.input
156 |                     : JSON.stringify(part.input),
157 |               },
158 |             }))
159 |           : undefined;
160 | 
161 |         return [{
162 |           role: 'assistant',
163 |           content: textContent || (toolCalls ? '' : ''),
164 |           tool_calls: toolCalls,
165 |         }];
166 |       }
167 | 
168 |       case 'tool':
169 |         // In OpenAI format, each tool result is a separate message
170 |         // Note: This returns an array, so we need to flatten it later
171 |         return message.content.map((part) => {
172 |           let content: string;
173 |           
174 |           // Handle different output types
175 |           if (part.output.type === 'text') {
176 |             content = part.output.value;
177 |           } else if (part.output.type === 'json') {
178 |             content = JSON.stringify(part.output.value);
179 |           } else if (part.output.type === 'error-text') {
180 |             content = `Error: ${part.output.value}`;
181 |           } else if (part.output.type === 'error-json') {
182 |             content = `Error: ${JSON.stringify(part.output.value)}`;
183 |           } else if (part.output.type === 'content') {
184 |             // Convert content array to string
185 |             content = part.output.value
186 |               .map((item) => {
187 |                 if (item.type === 'text') {
188 |                   return item.text;
189 |                 } else if (item.type === 'media') {
190 |                   return `[Media: ${item.mediaType}]`;
191 |                 }
192 |                 return '';
193 |               })
194 |               .join('\n');
195 |           } else {
196 |             content = String(part.output);
197 |           }
198 | 
199 |           return {
200 |             role: 'tool',
201 |             tool_call_id: part.toolCallId,
202 |             name: part.toolName,
203 |             content,
204 |           };
205 |         });
206 | 
207 |       default:
208 |         // TypeScript should ensure we never get here, but just in case
209 |         const exhaustiveCheck: never = message;
210 |         throw new Error(`Unsupported message role: ${JSON.stringify(exhaustiveCheck)}`);
211 |     }
212 |   }).flat();
213 | }
214 | 
215 | /**
216 |  * Maps OpenAI finish reasons to AI SDK V2 finish reasons
217 |  */
218 | export function mapOpenAIFinishReason(
219 |   finishReason: string | null | undefined
220 | ): LanguageModelV2FinishReason {
221 |   switch (finishReason) {
222 |     case 'stop':
223 |       return 'stop';
224 |     case 'length':
225 |       return 'length';
226 |     case 'content_filter':
227 |       return 'content-filter';
228 |     case 'tool_calls':
229 |     case 'function_call':
230 |       return 'tool-calls';
231 |     default:
232 |       return 'unknown';
233 |   }
234 | }
235 | 
236 | /**
237 |  * Normalize model names to match Stripe's approved model list
238 |  * This function handles provider-specific normalization rules:
239 |  * - Anthropic: Removes date suffixes, -latest suffix, converts version dashes to dots
240 |  * - OpenAI: Removes date suffixes (with exceptions)
241 |  * - Google: Returns as-is
242 |  * 
243 |  * @param modelId - Model ID in provider/model format (e.g., 'anthropic/claude-3-5-sonnet-20241022')
244 |  * @returns Normalized model ID in the same format
245 |  */
246 | export function normalizeModelId(modelId: string): string {
247 |   // Split the model ID into provider and model parts
248 |   const parts = modelId.split('/');
249 |   if (parts.length !== 2) {
250 |     // If format is not provider/model, return as-is
251 |     return modelId;
252 |   }
253 | 
254 |   const [provider, model] = parts;
255 |   const normalizedProvider = provider.toLowerCase();
256 |   let normalizedModel = model;
257 | 
258 |   if (normalizedProvider === 'anthropic') {
259 |     // Remove date suffix (YYYYMMDD format at the end)
260 |     normalizedModel = normalizedModel.replace(/-\d{8}$/, '');
261 | 
262 |     // Remove -latest suffix
263 |     normalizedModel = normalizedModel.replace(/-latest$/, '');
264 | 
265 |     // Convert version number dashes to dots anywhere in the name
266 |     // Match patterns like claude-3-7, opus-4-1, sonnet-4-5, etc.
267 |     normalizedModel = normalizedModel.replace(/(-[a-z]+)?-(\d+)-(\d+)/g, '$1-$2.$3');
268 |   } else if (normalizedProvider === 'openai') {
269 |     // Exception: keep gpt-4o-2024-05-13 as is
270 |     if (normalizedModel === 'gpt-4o-2024-05-13') {
271 |       return modelId;
272 |     }
273 | 
274 |     // Remove date suffix in format -YYYY-MM-DD
275 |     normalizedModel = normalizedModel.replace(/-\d{4}-\d{2}-\d{2}$/, '');
276 |   }
277 |   // For other providers (google/gemini), return as is
278 | 
279 |   return `${provider}/${normalizedModel}`;
280 | }
281 | 
```

--------------------------------------------------------------------------------
/llm/token-meter/tests/token-meter-general.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Tests for TokenMeter - General Functionality
  3 |  * Tests for cross-provider features, unknown types, and edge cases
  4 |  */
  5 | 
  6 | import Stripe from 'stripe';
  7 | import {createTokenMeter} from '../token-meter';
  8 | import type {MeterConfig} from '../types';
  9 | 
 10 | // Mock Stripe
 11 | jest.mock('stripe');
 12 | 
 13 | describe('TokenMeter - General Functionality', () => {
 14 |   let mockMeterEventsCreate: jest.Mock;
 15 |   let config: MeterConfig;
 16 |   let consoleWarnSpy: jest.SpyInstance;
 17 |   const TEST_API_KEY = 'sk_test_mock_key';
 18 | 
 19 |   beforeEach(() => {
 20 |     jest.clearAllMocks();
 21 |     mockMeterEventsCreate = jest.fn().mockResolvedValue({});
 22 |     
 23 |     // Mock the Stripe constructor
 24 |     (Stripe as unknown as jest.Mock).mockImplementation(() => ({
 25 |       v2: {
 26 |         billing: {
 27 |           meterEvents: {
 28 |             create: mockMeterEventsCreate,
 29 |           },
 30 |         },
 31 |       },
 32 |     }));
 33 |     
 34 |     config = {};
 35 |     consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
 36 |   });
 37 | 
 38 |   afterEach(() => {
 39 |     consoleWarnSpy.mockRestore();
 40 |   });
 41 | 
 42 |   describe('Unknown Response Types', () => {
 43 |     it('should warn and not log for unknown response formats', async () => {
 44 |       const meter = createTokenMeter(TEST_API_KEY, config);
 45 | 
 46 |       const unknownResponse = {
 47 |         some: 'unknown',
 48 |         response: 'format',
 49 |         that: 'does not match any provider',
 50 |       };
 51 | 
 52 |       meter.trackUsage(unknownResponse as any, 'cus_123');
 53 | 
 54 |       expect(consoleWarnSpy).toHaveBeenCalledWith(
 55 |         'Unable to detect response type. Supported types: OpenAI ChatCompletion, Responses API, Embeddings'
 56 |       );
 57 |       await new Promise(resolve => setImmediate(resolve));
 58 |       expect(mockMeterEventsCreate).not.toHaveBeenCalled();
 59 |     });
 60 | 
 61 |     it('should handle null response', async () => {
 62 |       const meter = createTokenMeter(TEST_API_KEY, config);
 63 | 
 64 |       meter.trackUsage(null as any, 'cus_123');
 65 | 
 66 |       expect(consoleWarnSpy).toHaveBeenCalled();
 67 |       await new Promise(resolve => setImmediate(resolve));
 68 |       expect(mockMeterEventsCreate).not.toHaveBeenCalled();
 69 |     });
 70 | 
 71 |     it('should handle undefined response', async () => {
 72 |       const meter = createTokenMeter(TEST_API_KEY, config);
 73 | 
 74 |       meter.trackUsage(undefined as any, 'cus_123');
 75 | 
 76 |       expect(consoleWarnSpy).toHaveBeenCalled();
 77 |       await new Promise(resolve => setImmediate(resolve));
 78 |       expect(mockMeterEventsCreate).not.toHaveBeenCalled();
 79 |     });
 80 | 
 81 |     it('should handle empty object response', async () => {
 82 |       const meter = createTokenMeter(TEST_API_KEY, config);
 83 | 
 84 |       meter.trackUsage({} as any, 'cus_123');
 85 | 
 86 |       expect(consoleWarnSpy).toHaveBeenCalled();
 87 |       await new Promise(resolve => setImmediate(resolve));
 88 |       expect(mockMeterEventsCreate).not.toHaveBeenCalled();
 89 |     });
 90 |   });
 91 | 
 92 |   describe('Customer ID Handling', () => {
 93 |     it('should pass customer ID correctly for different providers', async () => {
 94 |       const meter = createTokenMeter(TEST_API_KEY, config);
 95 |       const customerId = 'cus_special_id_123';
 96 | 
 97 |       // OpenAI
 98 |       const openaiResponse = {
 99 |         id: 'chatcmpl-123',
100 |         object: 'chat.completion',
101 |         created: Date.now(),
102 |         model: 'gpt-4',
103 |         choices: [{index: 0, message: {role: 'assistant', content: 'Hi'}, finish_reason: 'stop'}],
104 |         usage: {prompt_tokens: 5, completion_tokens: 2, total_tokens: 7},
105 |       };
106 | 
107 |       meter.trackUsage(openaiResponse as any, customerId);
108 | 
109 |       await new Promise(resolve => setImmediate(resolve));
110 | 
111 |       expect(mockMeterEventsCreate).toHaveBeenCalledWith(
112 |         expect.objectContaining({
113 |           payload: expect.objectContaining({
114 |             stripe_customer_id: customerId,
115 |           }),
116 |         })
117 |       );
118 | 
119 |       mockMeterEventsCreate.mockClear();
120 | 
121 |       // Anthropic
122 |       const anthropicResponse = {
123 |         id: 'msg_123',
124 |         type: 'message',
125 |         role: 'assistant',
126 |         content: [{type: 'text', text: 'Hi'}],
127 |         model: 'claude-3-5-sonnet-20241022',
128 |         stop_reason: 'end_turn',
129 |         stop_sequence: null,
130 |         usage: {input_tokens: 5, output_tokens: 2},
131 |       };
132 | 
133 |       meter.trackUsage(anthropicResponse as any, customerId);
134 | 
135 |       await new Promise(resolve => setImmediate(resolve));
136 | 
137 |       expect(mockMeterEventsCreate).toHaveBeenCalledWith(
138 |         expect.objectContaining({
139 |           payload: expect.objectContaining({
140 |             stripe_customer_id: customerId,
141 |           }),
142 |         })
143 |       );
144 | 
145 |       mockMeterEventsCreate.mockClear();
146 | 
147 |       // Gemini
148 |       const geminiResponse = {
149 |         response: {
150 |           text: () => 'Hi',
151 |           usageMetadata: {promptTokenCount: 5, candidatesTokenCount: 2, totalTokenCount: 7},
152 |           modelVersion: 'gemini-1.5-pro',
153 |         },
154 |       };
155 | 
156 |       meter.trackUsage(geminiResponse as any, customerId);
157 | 
158 |       await new Promise(resolve => setImmediate(resolve));
159 | 
160 |       expect(mockMeterEventsCreate).toHaveBeenCalledWith(
161 |         expect.objectContaining({
162 |           payload: expect.objectContaining({
163 |             stripe_customer_id: customerId,
164 |           }),
165 |         })
166 |       );
167 |     });
168 |   });
169 | 
170 |   describe('Multiple Meter Instances', () => {
171 |     it('should allow multiple independent meter instances', async () => {
172 |       const config1: MeterConfig = {};
173 |       const config2: MeterConfig = {};
174 | 
175 |       const meter1 = createTokenMeter(TEST_API_KEY, config1);
176 |       const meter2 = createTokenMeter(TEST_API_KEY, config2);
177 | 
178 |       const response = {
179 |         id: 'chatcmpl-123',
180 |         object: 'chat.completion',
181 |         created: Date.now(),
182 |         model: 'gpt-4',
183 |         choices: [{index: 0, message: {role: 'assistant', content: 'Hi'}, finish_reason: 'stop'}],
184 |         usage: {prompt_tokens: 5, completion_tokens: 2, total_tokens: 7},
185 |       };
186 | 
187 |       meter1.trackUsage(response as any, 'cus_123');
188 |       meter2.trackUsage(response as any, 'cus_456');
189 | 
190 |       await new Promise(resolve => setImmediate(resolve));
191 | 
192 |       expect(mockMeterEventsCreate).toHaveBeenCalledWith(
193 |         expect.objectContaining({
194 |           payload: expect.objectContaining({stripe_customer_id: 'cus_123'}),
195 |         })
196 |       );
197 |       expect(mockMeterEventsCreate).toHaveBeenCalledWith(
198 |         expect.objectContaining({
199 |           payload: expect.objectContaining({stripe_customer_id: 'cus_456'}),
200 |         })
201 |       );
202 |     });
203 |   });
204 | 
205 |   describe('Provider Detection Accuracy', () => {
206 |     it('should correctly identify provider from response shape alone', async () => {
207 |       const meter = createTokenMeter(TEST_API_KEY, config);
208 | 
209 |       // Test that provider is detected purely from response structure
210 |       const responses = [
211 |         {
212 |           response: {
213 |             id: 'chatcmpl-123',
214 |             object: 'chat.completion',
215 |             model: 'gpt-4',
216 |             choices: [{message: {content: 'test'}}],
217 |             usage: {prompt_tokens: 1, completion_tokens: 1},
218 |           },
219 |           expectedProvider: 'openai',
220 |         },
221 |         {
222 |           response: {
223 |             id: 'msg_123',
224 |             type: 'message',
225 |             role: 'assistant',
226 |             content: [{type: 'text', text: 'test'}],
227 |             model: 'claude-3-opus',
228 |             usage: {input_tokens: 1, output_tokens: 1},
229 |           },
230 |           expectedProvider: 'anthropic',
231 |         },
232 |         {
233 |           response: {
234 |             response: {
235 |               text: () => 'test',
236 |               usageMetadata: {promptTokenCount: 1, candidatesTokenCount: 1},
237 |               modelVersion: 'gemini-2.0-flash-exp',
238 |             },
239 |           },
240 |           expectedProvider: 'google',
241 |         },
242 |       ];
243 | 
244 |       for (const {response, expectedProvider} of responses) {
245 |         mockMeterEventsCreate.mockClear();
246 |         meter.trackUsage(response as any, 'cus_123');
247 | 
248 |         await new Promise(resolve => setImmediate(resolve));
249 | 
250 |         expect(mockMeterEventsCreate).toHaveBeenCalledWith(
251 |           expect.objectContaining({
252 |             payload: expect.objectContaining({
253 |               model: expect.stringContaining(expectedProvider + '/'),
254 |             }),
255 |           })
256 |         );
257 |       }
258 |     });
259 |   });
260 | });
261 | 
262 | // Helper function to create mock streams with tee()
263 | function createMockStreamWithTee(chunks: any[]) {
264 |   return {
265 |     tee() {
266 |       const stream1 = {
267 |         async *[Symbol.asyncIterator]() {
268 |           for (const chunk of chunks) {
269 |             yield chunk;
270 |           }
271 |         },
272 |         tee() {
273 |           const s1 = {
274 |             async *[Symbol.asyncIterator]() {
275 |               for (const chunk of chunks) {
276 |                 yield chunk;
277 |               }
278 |             },
279 |           };
280 |           const s2 = {
281 |             async *[Symbol.asyncIterator]() {
282 |               for (const chunk of chunks) {
283 |                 yield chunk;
284 |               }
285 |             },
286 |           };
287 |           return [s1, s2];
288 |         },
289 |       };
290 |       const stream2 = {
291 |         async *[Symbol.asyncIterator]() {
292 |           for (const chunk of chunks) {
293 |             yield chunk;
294 |           }
295 |         },
296 |       };
297 |       return [stream1, stream2];
298 |     },
299 |     async *[Symbol.asyncIterator]() {
300 |       for (const chunk of chunks) {
301 |         yield chunk;
302 |       }
303 |     },
304 |   };
305 | }
306 | 
307 | 
```

--------------------------------------------------------------------------------
/llm/ai-sdk/meter/tests/ai-sdk-billing-wrapper-other-providers.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Tests for AI SDK billing wrapper with other providers and unsupported models
  3 |  * Tests error handling and edge cases with mocks
  4 |  */
  5 | 
  6 | import Stripe from 'stripe';
  7 | import {createOpenAI} from '@ai-sdk/openai';
  8 | import {meteredModel} from '../index';
  9 | 
 10 | // Mock Stripe
 11 | jest.mock('stripe');
 12 | 
 13 | describe('AI SDK Billing Wrapper - Other Providers', () => {
 14 |   let mockMeterEventsCreate: jest.Mock;
 15 |   const TEST_API_KEY = 'sk_test_mock_key';
 16 | 
 17 |   beforeEach(() => {
 18 |     mockMeterEventsCreate = jest.fn().mockResolvedValue({});
 19 |     
 20 |     // Mock the Stripe constructor
 21 |     (Stripe as unknown as jest.Mock).mockImplementation(() => ({
 22 |       v2: {
 23 |         billing: {
 24 |           meterEvents: {
 25 |             create: mockMeterEventsCreate,
 26 |           },
 27 |         },
 28 |       },
 29 |     }));
 30 |   });
 31 | 
 32 |   afterEach(() => {
 33 |     jest.clearAllMocks();
 34 |   });
 35 | 
 36 |   describe('OpenAI-Compatible Providers', () => {
 37 |     it('should work with Together AI (OpenAI-compatible)', async () => {
 38 |       const together = createOpenAI({
 39 |         apiKey: 'mock-key',
 40 |         baseURL: 'https://api.together.xyz/v1',
 41 |       });
 42 | 
 43 |       const model = meteredModel(
 44 |         together('meta-llama/Llama-3-70b-chat-hf'),
 45 |         TEST_API_KEY,
 46 |         'cus_test123'
 47 |       );
 48 | 
 49 |       const originalModel = together('meta-llama/Llama-3-70b-chat-hf');
 50 |       jest.spyOn(originalModel, 'doGenerate').mockResolvedValue({
 51 |         text: 'Together AI response',
 52 |         usage: {
 53 |           inputTokens: 12,
 54 |           outputTokens: 5,
 55 |         },
 56 |         finishReason: 'stop',
 57 |         rawResponse: {},
 58 |         warnings: [],
 59 |       } as any);
 60 | 
 61 |       // Copy the mock to our wrapped model's internal model
 62 |       (model as any).model.doGenerate = originalModel.doGenerate;
 63 | 
 64 |       await model.doGenerate({
 65 |         inputFormat: 'prompt',
 66 |         mode: {type: 'regular'},
 67 |         prompt: [],
 68 |       } as any);
 69 | 
 70 |       // Wait for fire-and-forget logging to complete
 71 |       await new Promise(resolve => setImmediate(resolve));
 72 | 
 73 |       expect(mockMeterEventsCreate).toHaveBeenCalledTimes(2);
 74 |     });
 75 | 
 76 |     it('should work with custom OpenAI-compatible providers', async () => {
 77 |       const customProvider = createOpenAI({
 78 |         apiKey: 'mock-key',
 79 |         baseURL: 'https://custom-ai.example.com/v1',
 80 |       });
 81 | 
 82 |       const model = meteredModel(
 83 |         customProvider('custom-model-v1'),
 84 |         TEST_API_KEY,
 85 |         'cus_test123'
 86 |       );
 87 | 
 88 |       const originalModel = customProvider('custom-model-v1');
 89 |       jest.spyOn(originalModel, 'doGenerate').mockResolvedValue({
 90 |         text: 'Custom response',
 91 |         usage: {
 92 |           inputTokens: 8,
 93 |           outputTokens: 3,
 94 |         },
 95 |         finishReason: 'stop',
 96 |         rawResponse: {},
 97 |         warnings: [],
 98 |       } as any);
 99 | 
100 |       (model as any).model.doGenerate = originalModel.doGenerate;
101 | 
102 |       await model.doGenerate({
103 |         inputFormat: 'prompt',
104 |         mode: {type: 'regular'},
105 |         prompt: [],
106 |       } as any);
107 | 
108 |       // Wait for fire-and-forget logging to complete
109 |       await new Promise(resolve => setImmediate(resolve));
110 | 
111 |       expect(mockMeterEventsCreate).toHaveBeenCalledTimes(2);
112 |       expect(mockMeterEventsCreate).toHaveBeenCalledWith(
113 |         expect.objectContaining({
114 |           payload: expect.objectContaining({
115 |             value: '8',
116 |             token_type: 'input',
117 |           }),
118 |         })
119 |       );
120 |     });
121 |   });
122 | 
123 |   describe('Unsupported Models', () => {
124 |     it('should throw error for model with v1 specification', () => {
125 |       const mockModel = {
126 |         modelId: 'mock-model-v1',
127 |         provider: 'mock-provider',
128 |         specificationVersion: 'v1',
129 |         doGenerate: jest.fn(),
130 |         doStream: jest.fn(),
131 |       } as any;
132 | 
133 |       expect(() => {
134 |         meteredModel(mockModel, TEST_API_KEY, 'cus_test123');
135 |       }).toThrow('Only LanguageModelV2 models are supported');
136 |     });
137 | 
138 |     it('should throw error for model without specification version', () => {
139 |       const mockModel = {
140 |         modelId: 'mock-model-unknown',
141 |         provider: 'unknown-provider',
142 |         doGenerate: jest.fn(),
143 |         doStream: jest.fn(),
144 |       } as any;
145 | 
146 |       expect(() => {
147 |         meteredModel(mockModel, TEST_API_KEY, 'cus_test123');
148 |       }).toThrow('Only LanguageModelV2 models are supported');
149 |     });
150 | 
151 |     it('should throw error for model with unknown specification version', () => {
152 |       const mockModel = {
153 |         modelId: 'mock-model-v99',
154 |         provider: 'future-provider',
155 |         specificationVersion: 'v99',
156 |         doGenerate: jest.fn(),
157 |         doStream: jest.fn(),
158 |       } as any;
159 | 
160 |       expect(() => {
161 |         meteredModel(mockModel, TEST_API_KEY, 'cus_test123');
162 |       }).toThrow('Only LanguageModelV2 models are supported');
163 |     });
164 | 
165 |     it('should provide clear error message', () => {
166 |       const mockModel = {specificationVersion: 'v1'} as any;
167 | 
168 |       expect(() => {
169 |         meteredModel(mockModel, TEST_API_KEY, 'cus_test123');
170 |       }).toThrow(/Only LanguageModelV2 models are supported/);
171 |       expect(() => {
172 |         meteredModel(mockModel, TEST_API_KEY, 'cus_test123');
173 |       }).toThrow(/Please use a supported provider/);
174 |     });
175 |   });
176 | 
177 |   describe('Provider Support', () => {
178 |     it('should support any v2 provider name', async () => {
179 |       const customProviderModel = {
180 |         modelId: 'custom-model-123',
181 |         provider: 'my-custom-provider',
182 |         specificationVersion: 'v2',
183 |         doGenerate: jest.fn().mockResolvedValue({
184 |           text: 'Custom response',
185 |           usage: {
186 |             inputTokens: 10,
187 |             outputTokens: 5,
188 |           },
189 |           finishReason: 'stop',
190 |           rawResponse: {},
191 |           warnings: [],
192 |         }),
193 |         doStream: jest.fn(),
194 |       } as any;
195 | 
196 |       const wrapped = meteredModel(
197 |         customProviderModel,
198 |         TEST_API_KEY,
199 |         'cus_test123'
200 |       );
201 | 
202 |       await wrapped.doGenerate({
203 |         inputFormat: 'prompt',
204 |         mode: {type: 'regular'},
205 |         prompt: [],
206 |       } as any);
207 | 
208 |       // Wait for fire-and-forget logging to complete
209 |       await new Promise(resolve => setImmediate(resolve));
210 | 
211 |       expect(wrapped).toBeDefined();
212 |       expect(wrapped.provider).toBe('my-custom-provider');
213 |       expect(mockMeterEventsCreate).toHaveBeenCalledWith(
214 |         expect.objectContaining({
215 |           payload: expect.objectContaining({
216 |             model: 'my-custom-provider/custom-model-123',
217 |           }),
218 |         })
219 |       );
220 |     });
221 | 
222 |     it('should provide helpful error message for unsupported models', () => {
223 |       const unsupportedModel = {
224 |         modelId: 'test',
225 |         provider: 'test-provider',
226 |         specificationVersion: 'v1',
227 |       } as any;
228 | 
229 |       try {
230 |         meteredModel(unsupportedModel, TEST_API_KEY, 'cus_test123');
231 |         fail('Should have thrown an error');
232 |       } catch (error: any) {
233 |         expect(error.message).toContain('Only LanguageModelV2 models are supported');
234 |         expect(error.message).toContain('specificationVersion "v2"');
235 |         expect(error.message).toContain('OpenAI, Anthropic, Google');
236 |       }
237 |     });
238 |   });
239 | 
240 |   describe('Edge Cases', () => {
241 |     it('should handle model with missing usage data', async () => {
242 |       const model = {
243 |         modelId: 'test-model',
244 |         provider: 'test-provider',
245 |         specificationVersion: 'v2',
246 |         doGenerate: jest.fn().mockResolvedValue({
247 |           text: 'Response',
248 |           usage: undefined,
249 |           finishReason: 'stop',
250 |           rawResponse: {},
251 |           warnings: [],
252 |         }),
253 |         doStream: jest.fn(),
254 |       } as any;
255 | 
256 |       const wrapped = meteredModel(model, TEST_API_KEY, 'cus_test123');
257 | 
258 |       await wrapped.doGenerate({
259 |         inputFormat: 'prompt',
260 |         mode: {type: 'regular'},
261 |         prompt: [],
262 |       } as any);
263 | 
264 |       // Wait for fire-and-forget logging to complete
265 |       await new Promise(resolve => setImmediate(resolve));
266 | 
267 |       // Should not create meter events with 0 tokens (code only sends when > 0)
268 |       expect(mockMeterEventsCreate).toHaveBeenCalledTimes(0);
269 |     });
270 | 
271 |     it('should handle model with partial usage data', async () => {
272 |       const model = {
273 |         modelId: 'test-model',
274 |         provider: 'test-provider',
275 |         specificationVersion: 'v2',
276 |         doGenerate: jest.fn().mockResolvedValue({
277 |           text: 'Response',
278 |           usage: {
279 |             inputTokens: 10,
280 |             outputTokens: undefined,
281 |           },
282 |           finishReason: 'stop',
283 |           rawResponse: {},
284 |           warnings: [],
285 |         }),
286 |         doStream: jest.fn(),
287 |       } as any;
288 | 
289 |       const wrapped = meteredModel(model, TEST_API_KEY, 'cus_test123');
290 | 
291 |       await wrapped.doGenerate({
292 |         inputFormat: 'prompt',
293 |         mode: {type: 'regular'},
294 |         prompt: [],
295 |       } as any);
296 | 
297 |       // Wait for fire-and-forget logging to complete
298 |       await new Promise(resolve => setImmediate(resolve));
299 | 
300 |       // Should handle partial data gracefully - only sends event for input tokens
301 |       // Output tokens with value 0 are not sent (code only sends when > 0)
302 |       expect(mockMeterEventsCreate).toHaveBeenCalledTimes(1);
303 |       expect(mockMeterEventsCreate).toHaveBeenCalledWith(
304 |         expect.objectContaining({
305 |           payload: expect.objectContaining({
306 |             value: '10',
307 |             token_type: 'input',
308 |           }),
309 |         })
310 |       );
311 |     });
312 |   });
313 | });
314 | 
```

--------------------------------------------------------------------------------
/llm/token-meter/examples/openai.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Sample Usage: OpenAI with Usage Tracking
  3 |  * This demonstrates how to use the generic token meter to automatically report
  4 |  * token usage to Stripe for billing purposes.
  5 |  */
  6 | 
  7 | import {config} from 'dotenv';
  8 | import {resolve} from 'path';
  9 | import OpenAI from 'openai';
 10 | import {createTokenMeter} from '..';
 11 | 
 12 | // Load .env from the examples folder
 13 | config({path: resolve(__dirname, '.env')});
 14 | 
 15 | // Load environment variables from .env file
 16 | const STRIPE_API_KEY = process.env.STRIPE_API_KEY!;
 17 | const STRIPE_CUSTOMER_ID = process.env.STRIPE_CUSTOMER_ID!;
 18 | const OPENAI_API_KEY = process.env.OPENAI_API_KEY!;
 19 | 
 20 | // Initialize the OpenAI client (standard SDK)
 21 | const openai = new OpenAI({
 22 |   apiKey: OPENAI_API_KEY,
 23 | });
 24 | 
 25 | // Create the token meter
 26 | const meter = createTokenMeter(STRIPE_API_KEY);
 27 | 
 28 | // Sample 1: Basic Chat Completion (non-streaming)
 29 | async function sampleBasicChatCompletion() {
 30 |   const response = await openai.chat.completions.create({
 31 |     model: 'gpt-4o-mini',
 32 |     messages: [
 33 |       {role: 'user', content: 'Say "Hello, World!" and nothing else.'},
 34 |     ],
 35 |     max_tokens: 20,
 36 |   });
 37 | 
 38 |   // Track usage with the meter
 39 |   meter.trackUsage(response, STRIPE_CUSTOMER_ID);
 40 | 
 41 |   console.log('Response:', response.choices[0]?.message?.content);
 42 |   console.log('Usage:', response.usage);
 43 | }
 44 | 
 45 | // Sample 2: Streaming Chat Completion
 46 | async function sampleStreamingChatCompletion() {
 47 |   const stream = await openai.chat.completions.create({
 48 |     model: 'gpt-4o-mini',
 49 |     messages: [
 50 |       {role: 'user', content: 'Count from 1 to 5, one number per line.'},
 51 |     ],
 52 |     stream: true,
 53 |     stream_options: {include_usage: true}, // Important for metering
 54 |     max_tokens: 50,
 55 |   });
 56 | 
 57 |   // Wrap the stream for metering
 58 |   const meteredStream = meter.trackUsageStreamOpenAI(stream, STRIPE_CUSTOMER_ID);
 59 | 
 60 |   let fullContent = '';
 61 |   for await (const chunk of meteredStream) {
 62 |     const content = chunk.choices[0]?.delta?.content || '';
 63 |     fullContent += content;
 64 |     process.stdout.write(content);
 65 |   }
 66 | 
 67 |   console.log('\n\nFull content:', fullContent);
 68 | }
 69 | 
 70 | // Sample 3: Chat Completion with Tools
 71 | async function sampleChatCompletionWithTools() {
 72 |   const tools: any[] = [
 73 |     {
 74 |       type: 'function',
 75 |       function: {
 76 |         name: 'get_weather',
 77 |         description: 'Get the current weather in a location',
 78 |         parameters: {
 79 |           type: 'object',
 80 |           properties: {
 81 |             location: {
 82 |               type: 'string',
 83 |               description: 'The city and state, e.g. San Francisco, CA',
 84 |             },
 85 |             unit: {
 86 |               type: 'string',
 87 |               enum: ['celsius', 'fahrenheit'],
 88 |             },
 89 |           },
 90 |           required: ['location'],
 91 |         },
 92 |       },
 93 |     },
 94 |   ];
 95 | 
 96 |   const response = await openai.chat.completions.create({
 97 |     model: 'gpt-4o-mini',
 98 |     messages: [{role: 'user', content: 'What is the weather in New York?'}],
 99 |     tools,
100 |     max_tokens: 100,
101 |   });
102 | 
103 |   // Track usage with the meter
104 |   meter.trackUsage(response, STRIPE_CUSTOMER_ID);
105 | 
106 |   console.log(
107 |     'Response:',
108 |     JSON.stringify(response.choices[0]?.message, null, 2)
109 |   );
110 |   console.log('Usage:', response.usage);
111 | }
112 | 
113 | // Sample 4: Streaming Chat Completion with Tools
114 | async function sampleStreamingChatCompletionWithTools() {
115 |   const tools: any[] = [
116 |     {
117 |       type: 'function',
118 |       function: {
119 |         name: 'calculate',
120 |         description: 'Calculate a mathematical expression',
121 |         parameters: {
122 |           type: 'object',
123 |           properties: {
124 |             expression: {
125 |               type: 'string',
126 |               description: 'The mathematical expression to calculate',
127 |             },
128 |           },
129 |           required: ['expression'],
130 |         },
131 |       },
132 |     },
133 |   ];
134 | 
135 |   const stream = await openai.chat.completions.create({
136 |     model: 'gpt-4o-mini',
137 |     messages: [{role: 'user', content: 'What is 25 * 4?'}],
138 |     tools,
139 |     stream: true,
140 |     stream_options: {include_usage: true}, // Important for metering
141 |     max_tokens: 100,
142 |   });
143 | 
144 |   // Wrap the stream for metering
145 |   const meteredStream = meter.trackUsageStreamOpenAI(stream, STRIPE_CUSTOMER_ID);
146 | 
147 |   const toolCalls = new Map<
148 |     number,
149 |     {id: string; name: string; arguments: string}
150 |   >();
151 | 
152 |   for await (const chunk of meteredStream) {
153 |     const choice = chunk.choices[0];
154 | 
155 |     // Handle tool calls
156 |     if (choice?.delta?.tool_calls) {
157 |       for (const toolCall of choice.delta.tool_calls) {
158 |         const index = toolCall.index;
159 |         if (index === undefined) continue;
160 | 
161 |         if (!toolCalls.has(index)) {
162 |           toolCalls.set(index, {id: '', name: '', arguments: ''});
163 |         }
164 |         const tc = toolCalls.get(index)!;
165 |         if (toolCall.id) tc.id = toolCall.id;
166 |         if (toolCall.function?.name) tc.name = toolCall.function.name;
167 |         if (toolCall.function?.arguments)
168 |           tc.arguments += toolCall.function.arguments;
169 |       }
170 |     }
171 | 
172 |     // Print usage if available
173 |     if (chunk.usage) {
174 |       console.log('\nUsage in stream:', chunk.usage);
175 |     }
176 |   }
177 | 
178 |   console.log('\nTool calls:', Array.from(toolCalls.values()));
179 | }
180 | 
181 | // Sample 5: Responses API (non-streaming)
182 | async function sampleResponsesAPIBasic() {
183 |   const response = await openai.responses.create({
184 |     model: 'gpt-4o-mini',
185 |     input: 'What is 2+2?',
186 |     instructions: 'You are a helpful math assistant.',
187 |     max_output_tokens: 50,
188 |   });
189 | 
190 |   // Track usage with the meter
191 |   meter.trackUsage(response, STRIPE_CUSTOMER_ID);
192 | 
193 |   console.log('Response:', JSON.stringify(response.output, null, 2));
194 |   console.log('Usage:', response.usage);
195 | }
196 | 
197 | // Sample 6: Responses API (streaming)
198 | async function sampleResponsesAPIStreaming() {
199 |   const stream = await openai.responses.create({
200 |     model: 'gpt-4o-mini',
201 |     input: 'Tell me a fun fact about cats.',
202 |     instructions: 'You are a helpful assistant.',
203 |     stream: true,
204 |     max_output_tokens: 100,
205 |   });
206 | 
207 |   // Wrap the stream for metering
208 |   const meteredStream = meter.trackUsageStreamOpenAI(stream, STRIPE_CUSTOMER_ID);
209 | 
210 |   let finalOutput: any = null;
211 |   let finalUsage: any = null;
212 | 
213 |   for await (const event of meteredStream) {
214 |     if (event.type === 'response.completed' && 'response' in event) {
215 |       finalOutput = event.response.output;
216 |       finalUsage = event.response.usage;
217 |     }
218 |   }
219 | 
220 |   console.log('Final output:', JSON.stringify(finalOutput, null, 2));
221 |   console.log('Usage:', finalUsage);
222 | }
223 | 
224 | // Sample 7: Responses API with parse (structured outputs)
225 | async function sampleResponsesAPIParse() {
226 |   // Note: The schema is defined inline in the API call
227 |   // Zod is not needed for this example
228 |   const response = await openai.responses.parse({
229 |     model: 'gpt-4o-mini',
230 |     input: 'What is 15 multiplied by 7?',
231 |     instructions:
232 |       'You are a helpful math assistant. Provide the answer and a brief explanation.',
233 |     text: {
234 |       format: {
235 |         type: 'json_schema',
236 |         name: 'math_response',
237 |         strict: true,
238 |         schema: {
239 |           type: 'object',
240 |           properties: {
241 |             answer: {type: 'number'},
242 |             explanation: {type: 'string'},
243 |           },
244 |           required: ['answer', 'explanation'],
245 |           additionalProperties: false,
246 |         },
247 |       },
248 |     },
249 |     max_output_tokens: 100,
250 |   });
251 | 
252 |   // Track usage with the meter
253 |   meter.trackUsage(response, STRIPE_CUSTOMER_ID);
254 | 
255 |   console.log('Parsed response:', response.output[0]);
256 |   console.log('Usage:', response.usage);
257 | }
258 | 
259 | // Sample 8: Embeddings
260 | async function sampleEmbeddings() {
261 |   const response = await openai.embeddings.create({
262 |     model: 'text-embedding-3-small',
263 |     input: 'The quick brown fox jumps over the lazy dog',
264 |   });
265 | 
266 |   // Track usage with the meter
267 |   meter.trackUsage(response, STRIPE_CUSTOMER_ID);
268 | 
269 |   console.log('Embedding dimensions:', response.data[0]?.embedding.length);
270 |   console.log('First 5 values:', response.data[0]?.embedding.slice(0, 5));
271 |   console.log('Usage:', response.usage);
272 | }
273 | 
274 | // Sample 9: Multiple messages conversation
275 | async function sampleConversation() {
276 |   const response = await openai.chat.completions.create({
277 |     model: 'gpt-4o-mini',
278 |     messages: [
279 |       {role: 'system', content: 'You are a helpful assistant.'},
280 |       {role: 'user', content: 'What is the capital of France?'},
281 |       {role: 'assistant', content: 'The capital of France is Paris.'},
282 |       {role: 'user', content: 'What is its population?'},
283 |     ],
284 |     max_tokens: 50,
285 |   });
286 | 
287 |   // Track usage with the meter
288 |   meter.trackUsage(response, STRIPE_CUSTOMER_ID);
289 | 
290 |   console.log('Response:', response.choices[0]?.message?.content);
291 |   console.log('Usage:', response.usage);
292 | }
293 | 
294 | // Run all samples
295 | async function runAllSamples() {
296 |   console.log('Starting OpenAI Usage Tracking Examples');
297 |   console.log(
298 |     'These examples show how to use the generic token meter with OpenAI\n'
299 |   );
300 | 
301 |   try {
302 |     await sampleBasicChatCompletion();
303 |     await sampleStreamingChatCompletion();
304 |     await sampleChatCompletionWithTools();
305 |     await sampleStreamingChatCompletionWithTools();
306 |     await sampleResponsesAPIBasic();
307 |     await sampleResponsesAPIStreaming();
308 |     await sampleResponsesAPIParse();
309 |     await sampleEmbeddings();
310 |     await sampleConversation();
311 | 
312 |     console.log('\n' + '='.repeat(80));
313 |     console.log('All examples completed successfully!');
314 |     console.log('='.repeat(80));
315 |   } catch (error) {
316 |     console.error('\n❌ Sample failed:', error);
317 |     throw error;
318 |   }
319 | }
320 | 
321 | // Run the samples
322 | runAllSamples().catch(console.error);
323 | 
324 | 
```

--------------------------------------------------------------------------------
/llm/ai-sdk/provider/tests/stripe-provider.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Tests for Stripe AI SDK Provider
  3 |  */
  4 | 
  5 | import {createStripe, stripe} from '../index';
  6 | import {StripeLanguageModel} from '../stripe-language-model';
  7 | 
  8 | describe('Stripe Provider', () => {
  9 |   describe('createStripe', () => {
 10 |     it('should create a provider instance', () => {
 11 |       const provider = createStripe({
 12 |         apiKey: 'sk_test_123',
 13 |         customerId: 'cus_123',
 14 |       });
 15 | 
 16 |       expect(provider).toBeDefined();
 17 |       expect(typeof provider).toBe('function');
 18 |       expect(provider.languageModel).toBeDefined();
 19 |     });
 20 | 
 21 |     it('should create a language model with provider function', () => {
 22 |       const provider = createStripe({
 23 |         apiKey: 'sk_test_123',
 24 |         customerId: 'cus_123',
 25 |       });
 26 | 
 27 |       const model = provider('openai/gpt-5');
 28 | 
 29 |       expect(model).toBeInstanceOf(StripeLanguageModel);
 30 |       expect(model.modelId).toBe('openai/gpt-5');
 31 |       expect(model.provider).toBe('stripe');
 32 |       expect(model.specificationVersion).toBe('v2');
 33 |     });
 34 | 
 35 |     it('should create a language model with languageModel method', () => {
 36 |       const provider = createStripe({
 37 |         apiKey: 'sk_test_123',
 38 |         customerId: 'cus_123',
 39 |       });
 40 | 
 41 |       const model = provider.languageModel('google/gemini-2.5-pro');
 42 | 
 43 |       expect(model).toBeInstanceOf(StripeLanguageModel);
 44 |       expect(model.modelId).toBe('google/gemini-2.5-pro');
 45 |     });
 46 | 
 47 |     it('should throw error when called with new keyword', () => {
 48 |       const provider = createStripe({
 49 |         apiKey: 'sk_test_123',
 50 |       });
 51 | 
 52 |       expect(() => {
 53 |         // @ts-expect-error - Testing error case
 54 |         new provider('openai/gpt-5');
 55 |       }).toThrow(
 56 |         'The Stripe provider function cannot be called with the new keyword.'
 57 |       );
 58 |     });
 59 | 
 60 |     it('should use default baseURL when not provided', () => {
 61 |       const provider = createStripe({
 62 |         apiKey: 'sk_test_123',
 63 |         customerId: 'cus_123',
 64 |       });
 65 | 
 66 |       const model = provider('openai/gpt-5') as any;
 67 |       expect(model.config.baseURL).toBe('https://llm.stripe.com');
 68 |     });
 69 | 
 70 |     it('should use custom baseURL when provided', () => {
 71 |       const provider = createStripe({
 72 |         apiKey: 'sk_test_123',
 73 |         customerId: 'cus_123',
 74 |         baseURL: 'https://custom.stripe.com',
 75 |       });
 76 | 
 77 |       const model = provider('openai/gpt-5') as any;
 78 |       expect(model.config.baseURL).toBe('https://custom.stripe.com');
 79 |     });
 80 | 
 81 |     it('should merge customer IDs from provider and model settings', () => {
 82 |       const provider = createStripe({
 83 |         apiKey: 'sk_test_123',
 84 |         customerId: 'cus_provider',
 85 |       });
 86 | 
 87 |       const model1 = provider('openai/gpt-5') as any;
 88 |       expect(model1.settings.customerId).toBe('cus_provider');
 89 | 
 90 |       const model2 = provider('openai/gpt-5', {
 91 |         customerId: 'cus_model',
 92 |       }) as any;
 93 |       expect(model2.settings.customerId).toBe('cus_model');
 94 |     });
 95 | 
 96 |     it('should throw error for text embedding models', () => {
 97 |       const provider = createStripe({
 98 |         apiKey: 'sk_test_123',
 99 |       });
100 | 
101 |       expect(() => {
102 |         provider.textEmbeddingModel('openai/text-embedding-3-small');
103 |       }).toThrow('Text embedding models are not yet supported by Stripe provider');
104 |     });
105 | 
106 |     it('should throw error for image models', () => {
107 |       const provider = createStripe({
108 |         apiKey: 'sk_test_123',
109 |       });
110 | 
111 |       expect(() => {
112 |         provider.imageModel('openai/dall-e-3');
113 |       }).toThrow('Image models are not yet supported by Stripe provider');
114 |     });
115 |   });
116 | 
117 |   describe('default stripe provider', () => {
118 |     beforeEach(() => {
119 |       // Clear environment variable
120 |       delete process.env.STRIPE_API_KEY;
121 |     });
122 | 
123 |     it('should be defined', () => {
124 |       expect(stripe).toBeDefined();
125 |       expect(typeof stripe).toBe('function');
126 |     });
127 | 
128 |     it('should use STRIPE_API_KEY from environment', () => {
129 |       process.env.STRIPE_API_KEY = 'sk_test_env';
130 | 
131 |       // This would throw if API key is not found
132 |       const model = stripe('openai/gpt-5', {customerId: 'cus_123'});
133 |       expect(model).toBeDefined();
134 |     });
135 |   });
136 | 
137 |   describe('API key handling', () => {
138 |     it('should throw error when API key is not provided', () => {
139 |       delete process.env.STRIPE_API_KEY;
140 | 
141 |       const provider = createStripe({
142 |         customerId: 'cus_123',
143 |       });
144 | 
145 |       expect(() => {
146 |         const model = provider('openai/gpt-5') as any;
147 |         model.config.headers();
148 |       }).toThrow(
149 |         'Stripe API key is required. Provide it via config.apiKey or STRIPE_API_KEY environment variable.'
150 |       );
151 |     });
152 | 
153 |     it('should use API key from config', () => {
154 |       const provider = createStripe({
155 |         apiKey: 'sk_test_config',
156 |         customerId: 'cus_123',
157 |       });
158 | 
159 |       const model = provider('openai/gpt-5') as any;
160 |       const headers = model.config.headers();
161 | 
162 |       expect(headers.Authorization).toBe('Bearer sk_test_config');
163 |     });
164 | 
165 |     it('should prefer config API key over environment', () => {
166 |       process.env.STRIPE_API_KEY = 'sk_test_env';
167 | 
168 |       const provider = createStripe({
169 |         apiKey: 'sk_test_config',
170 |         customerId: 'cus_123',
171 |       });
172 | 
173 |       const model = provider('openai/gpt-5') as any;
174 |       const headers = model.config.headers();
175 | 
176 |       expect(headers.Authorization).toBe('Bearer sk_test_config');
177 |     });
178 |   });
179 | 
180 |   describe('headers handling', () => {
181 |     it('should include custom headers from provider config', () => {
182 |       const provider = createStripe({
183 |         apiKey: 'sk_test_123',
184 |         customerId: 'cus_123',
185 |         headers: {
186 |           'X-Custom': 'provider-value',
187 |         },
188 |       });
189 | 
190 |       const model = provider('openai/gpt-5') as any;
191 |       const headers = model.config.headers();
192 | 
193 |       expect(headers['X-Custom']).toBe('provider-value');
194 |     });
195 | 
196 |     it('should include custom headers from model settings', () => {
197 |       const provider = createStripe({
198 |         apiKey: 'sk_test_123',
199 |       });
200 | 
201 |       const model = provider('openai/gpt-5', {
202 |         customerId: 'cus_123',
203 |         headers: {
204 |           'X-Model': 'model-value',
205 |         },
206 |       }) as any;
207 | 
208 |       expect(model.settings.headers['X-Model']).toBe('model-value');
209 |     });
210 |   });
211 | 
212 |   describe('supported models', () => {
213 |     const provider = createStripe({
214 |       apiKey: 'sk_test_123',
215 |       customerId: 'cus_123',
216 |     });
217 | 
218 |     const testModels = [
219 |       'openai/gpt-5',
220 |       'openai/gpt-4.1',
221 |       'openai/o3',
222 |       'google/gemini-2.5-pro',
223 |       'google/gemini-2.0-flash',
224 |       'anthropic/claude-sonnet-4',
225 |       'anthropic/claude-opus-4',
226 |     ];
227 | 
228 |     testModels.forEach((modelId) => {
229 |       it(`should create model for ${modelId}`, () => {
230 |         const model = provider(modelId);
231 |         expect(model.modelId).toBe(modelId);
232 |       });
233 |     });
234 |   });
235 | 
236 |   describe('model name normalization', () => {
237 |     const provider = createStripe({
238 |       apiKey: 'sk_test_123',
239 |       customerId: 'cus_123',
240 |     });
241 | 
242 |     describe('Anthropic models', () => {
243 |       it('should normalize model with date suffix (YYYYMMDD)', () => {
244 |         const model = provider('anthropic/claude-3-5-sonnet-20241022');
245 |         expect(model.modelId).toBe('anthropic/claude-3.5-sonnet');
246 |       });
247 | 
248 |       it('should normalize model with -latest suffix', () => {
249 |         const model = provider('anthropic/claude-sonnet-4-latest');
250 |         expect(model.modelId).toBe('anthropic/claude-sonnet-4');
251 |       });
252 | 
253 |       it('should normalize version dashes to dots', () => {
254 |         const model = provider('anthropic/claude-3-5-sonnet');
255 |         expect(model.modelId).toBe('anthropic/claude-3.5-sonnet');
256 |       });
257 | 
258 |       it('should handle combined normalization (date + version)', () => {
259 |         const model = provider('anthropic/claude-3-7-sonnet-20250115');
260 |         expect(model.modelId).toBe('anthropic/claude-3.7-sonnet');
261 |       });
262 | 
263 |       it('should normalize sonnet-4-5 to sonnet-4.5', () => {
264 |         const model = provider('anthropic/sonnet-4-5');
265 |         expect(model.modelId).toBe('anthropic/sonnet-4.5');
266 |       });
267 | 
268 |       it('should normalize opus-4-1 to opus-4.1', () => {
269 |         const model = provider('anthropic/opus-4-1');
270 |         expect(model.modelId).toBe('anthropic/opus-4.1');
271 |       });
272 |     });
273 | 
274 |     describe('OpenAI models', () => {
275 |       it('should normalize model with date suffix (YYYY-MM-DD)', () => {
276 |         const model = provider('openai/gpt-4-turbo-2024-04-09');
277 |         expect(model.modelId).toBe('openai/gpt-4-turbo');
278 |       });
279 | 
280 |       it('should keep gpt-4o-2024-05-13 as exception', () => {
281 |         const model = provider('openai/gpt-4o-2024-05-13');
282 |         expect(model.modelId).toBe('openai/gpt-4o-2024-05-13');
283 |       });
284 | 
285 |       it('should not normalize YYYYMMDD format (only YYYY-MM-DD)', () => {
286 |         const model = provider('openai/gpt-4-20241231');
287 |         expect(model.modelId).toBe('openai/gpt-4-20241231');
288 |       });
289 |     });
290 | 
291 |     describe('Google models', () => {
292 |       it('should keep Google models unchanged', () => {
293 |         const model = provider('google/gemini-2.5-pro');
294 |         expect(model.modelId).toBe('google/gemini-2.5-pro');
295 |       });
296 | 
297 |       it('should not remove any suffixes from Google models', () => {
298 |         const model = provider('google/gemini-2.5-pro-20250101');
299 |         expect(model.modelId).toBe('google/gemini-2.5-pro-20250101');
300 |       });
301 |     });
302 | 
303 |     describe('Other providers', () => {
304 |       it('should keep unknown provider models unchanged', () => {
305 |         const model = provider('custom/my-model-1-2-3');
306 |         expect(model.modelId).toBe('custom/my-model-1-2-3');
307 |       });
308 |     });
309 |   });
310 | });
311 | 
312 | 
```

--------------------------------------------------------------------------------
/tools/python/examples/openai/customer_support/emailer.py:
--------------------------------------------------------------------------------

```python
  1 | # pyright: strict
  2 | 
  3 | import imaplib
  4 | import email
  5 | import smtplib
  6 | from email.mime.text import MIMEText
  7 | from email.message import Message
  8 | from email.mime.multipart import MIMEMultipart
  9 | from email.utils import parseaddr
 10 | from typing import List, Tuple, Callable, Union, Awaitable
 11 | import asyncio
 12 | import json
 13 | import re
 14 | from datetime import datetime
 15 | from email.utils import parsedate_to_datetime
 16 | 
 17 | 
 18 | class Email:
 19 |     def __init__(
 20 |         self,
 21 |         from_address: str,
 22 |         to_address: str,
 23 |         subject: str,
 24 |         body: str,
 25 |         id: str = "",
 26 |         date: datetime = datetime.now(),
 27 |     ):
 28 |         self.id = id
 29 |         self.to_address = to_address
 30 |         self.from_address = from_address
 31 |         self.subject = subject
 32 |         self.body = body
 33 |         self.date = date
 34 | 
 35 |     def to_message(self, reply_id: str, reply_to: str) -> MIMEMultipart:
 36 |         msg = MIMEMultipart()
 37 |         msg["From"] = self.from_address
 38 |         msg["To"] = self.to_address
 39 |         msg["Subject"] = self.subject
 40 |         msg["In-Reply-To"] = reply_id
 41 |         msg["References"] = reply_id
 42 |         msg["Reply-To"] = reply_to
 43 |         msg.attach(MIMEText(f"<html><body>{self.body}</body></html>", "html"))
 44 |         return msg
 45 | 
 46 |     def to_dict(self):
 47 |         return {
 48 |             "id": self.id,
 49 |             "to": self.to_address,
 50 |             "from": self.from_address,
 51 |             "subject": self.subject,
 52 |             "body": self.body,
 53 |             "date": self.date.strftime("%a, %d %b %Y %H:%M:%S %z"),
 54 |         }
 55 | 
 56 | 
 57 | class Emailer:
 58 |     """
 59 |     Emailer is an IMAP/SMTP client that can be used to fetch and respond to emails.
 60 |     It was mostly vibe-coded so please make improvements!
 61 |     TODO: add agent replies to the context
 62 |     """
 63 | 
 64 |     def __init__(
 65 |         self,
 66 |         email_address: str,
 67 |         email_password: str,
 68 |         support_address: str = "",
 69 |         imap_server: str = "imap.gmail.com",
 70 |         imap_port: int = 993,
 71 |         smtp_server: str = "smtp.gmail.com",
 72 |         smtp_port: int = 587,
 73 |     ):
 74 |         # Email configuration
 75 |         self.email_address = email_address
 76 |         self.support_address = support_address if support_address else email_address
 77 |         self.email_password = email_password
 78 |         self.imap_server = imap_server
 79 |         self.imap_port = imap_port
 80 |         self.smtp_server = smtp_server
 81 |         self.smtp_port = smtp_port
 82 | 
 83 |     def _connect_to_email(self) -> Tuple[imaplib.IMAP4_SSL, smtplib.SMTP]:
 84 |         """Establish connections to email servers."""
 85 |         # Connect to IMAP server
 86 |         imap_conn = imaplib.IMAP4_SSL(self.imap_server, self.imap_port)
 87 |         imap_conn.login(self.email_address, self.email_password)
 88 | 
 89 |         # Connect to SMTP server
 90 |         smtp_conn = smtplib.SMTP(self.smtp_server, self.smtp_port)
 91 |         smtp_conn.starttls()
 92 |         smtp_conn.login(self.email_address, self.email_password)
 93 | 
 94 |         return imap_conn, smtp_conn
 95 | 
 96 |     def _get_body(self, email_message: Message) -> str:
 97 |         body: str = ""
 98 |         if email_message.is_multipart():
 99 |             for part in email_message.walk():
100 |                 if part.get_content_type() == "text/plain":
101 |                     payload = part.get_payload(decode=True)
102 |                     if isinstance(payload, bytes):
103 |                         body = payload.decode()
104 |                     break
105 |         else:
106 |             payload = email_message.get_payload(decode=True)
107 |             if isinstance(payload, bytes):
108 |                 body = payload.decode()
109 |             else:
110 |                 body = str(payload)
111 |         return self._strip_replies(body)
112 | 
113 |     def _strip_replies(self, raw_body: str) -> str:
114 |         lines = raw_body.split("\n")
115 |         pruned: List[str] = []
116 |         for line in lines:
117 |             # Stop if we see a typical reply indicator
118 |             if line.strip().startswith("On ") and " wrote:" in line:
119 |                 break
120 |             pruned.append(line)
121 |         return "\n".join(pruned).strip()
122 | 
123 |     def _parse_email(
124 |         self, imap_conn: imaplib.IMAP4_SSL, email_id: bytes
125 |     ) -> Union[Email, None]:
126 |         _, msg_data = imap_conn.fetch(email_id.decode(), "(BODY.PEEK[])")
127 |         if not msg_data or not msg_data[0]:
128 |             return None
129 |         msg_resp = msg_data[0]
130 |         if isinstance(msg_resp, tuple) and len(msg_resp) == 2:
131 |             email_body = msg_resp[1]
132 |         else:
133 |             return None
134 | 
135 |         email_message = email.message_from_bytes(email_body)
136 |         subject = email_message["subject"] or ""
137 |         from_address = parseaddr(email_message.get("From", ""))[1]
138 |         to_address = parseaddr(email_message.get("To", ""))[1]
139 |         date_str = email_message.get("Date", "")
140 |         date = datetime.now()
141 |         if date_str:
142 |             try:
143 |                 date = parsedate_to_datetime(date_str)
144 |             except Exception:
145 |                 pass
146 | 
147 |         body = self._get_body(email_message)
148 |         return Email(
149 |             id=email_id.decode(),
150 |             from_address=from_address,
151 |             to_address=to_address,
152 |             subject=subject,
153 |             body=body,
154 |             date=date,
155 |         )
156 | 
157 |     def _get_email_thread(
158 |         self, imap_conn: imaplib.IMAP4_SSL, email_id_bytes: bytes
159 |     ) -> List[Email]:
160 |         email = self._parse_email(imap_conn, email_id_bytes)
161 |         if not email:
162 |             return []
163 | 
164 |         thread = [email]
165 | 
166 |         # Try thread via X-GM-THRID (Gmail extension)
167 |         _, thrid_data = imap_conn.fetch(email.id, "(X-GM-THRID)")
168 |         match = None
169 |         if thrid_data and thrid_data[0]:
170 |             data = thrid_data[0]
171 |             if isinstance(data, bytes):
172 |                 match = re.search(r"X-GM-THRID\s+(\d+)", data.decode())
173 |             else:
174 |                 match = re.search(r"X-GM-THRID\s+(\d+)", str(data))
175 |         if match:
176 |             thread_id = match.group(1)
177 |             _, thread_ids = imap_conn.search(None, f"X-GM-THRID {thread_id}")
178 |             if thread_ids and thread_ids[0]:
179 |                 thread = [
180 |                     self._parse_email(imap_conn, mid) for mid in thread_ids[0].split()
181 |                 ]
182 |                 thread = [e for e in thread if e]
183 |                 thread.sort(key=lambda e: e.date)
184 |                 return thread
185 | 
186 |         # Fallback: use REFERENCES header
187 |         _, ref_data = imap_conn.fetch(
188 |             email.id, "(BODY.PEEK[HEADER.FIELDS (REFERENCES)])"
189 |         )
190 |         if ref_data and ref_data[0]:
191 |             ref_line = (
192 |                 ref_data[0][1].decode() if isinstance(ref_data[0][1], bytes) else ""
193 |             )
194 |             refs = re.findall(r"<([^>]+)>", ref_line)
195 |             for ref in refs:
196 |                 _, ref_ids = imap_conn.search(None, f'(HEADER Message-ID "<{ref}>")')
197 |                 if ref_ids and ref_ids[0]:
198 |                     for ref_id in ref_ids[0].split():
199 |                         ref_email = self._parse_email(imap_conn, ref_id)
200 |                         if ref_email and ref_email.id not in [e.id for e in thread]:
201 |                             thread.append(ref_email)
202 | 
203 |             # Sort emails in the thread by date (ascending order)
204 |             thread.sort(key=lambda e: e.date)
205 |             return thread
206 | 
207 |         return thread
208 | 
209 |     def _get_unread_emails(self, imap_conn: imaplib.IMAP4_SSL) -> List[List[Email]]:
210 |         imap_conn.select("INBOX")
211 |         _, msg_nums = imap_conn.search(None, f'(UNSEEN TO "{self.support_address}")')
212 |         emails: List[List[Email]] = []
213 | 
214 |         for email_id in msg_nums[0].split():
215 |             thread = self._get_email_thread(imap_conn, email_id)
216 |             emails.append(thread)
217 | 
218 |         return emails
219 | 
220 |     def mark_as_read(self, imap_conn: imaplib.IMAP4_SSL, message_id: str):
221 |         imap_conn.store(message_id, "+FLAGS", "\\Seen")
222 | 
223 |     def get_email_thread(self, email_id: str) -> List[Email]:
224 |         # Connect to email servers
225 |         imap_conn, smtp_conn = self._connect_to_email()
226 |         imap_conn.select("INBOX")
227 | 
228 |         # Get the thread
229 |         thread = self._get_email_thread(
230 |             imap_conn=imap_conn, email_id_bytes=email_id.encode()
231 |         )
232 | 
233 |         # Close connections
234 |         imap_conn.logout()
235 |         smtp_conn.quit()
236 | 
237 |         return thread
238 | 
239 |     async def process(
240 |         self,
241 |         respond: Callable[[List[Email]], Awaitable[Union[Email, None]]],
242 |         mark_read: bool = True,
243 |     ):
244 |         # Connect to email servers
245 |         imap_conn, smtp_conn = self._connect_to_email()
246 | 
247 |         # Get unread emails
248 |         print("Fetching unread emails...")
249 |         unread_emails = self._get_unread_emails(imap_conn)
250 |         for email_thread in unread_emails:
251 |             # Get the most recent email in the thread
252 |             most_recent = email_thread[-1]
253 | 
254 |             # Generate the response
255 |             response = await respond(email_thread)
256 | 
257 |             # If there is no response, skip this email and keep as unread
258 |             # in the inbox
259 |             if response is None:
260 |                 continue
261 | 
262 |             # Send the response
263 |             # Get the most recent email in the thread to reply to
264 |             print(
265 |                 f"Replying to '{response.to_address}' with:\n  {json.dumps(response.body)}"
266 |             )
267 |             smtp_conn.send_message(
268 |                 response.to_message(most_recent.id, self.support_address)
269 |             )
270 | 
271 |             # Mark the original email as read
272 |             if mark_read:
273 |                 self.mark_as_read(imap_conn, most_recent.id)
274 | 
275 |         # Close connections
276 |         imap_conn.logout()
277 |         smtp_conn.quit()
278 | 
279 |     async def run(
280 |         self,
281 |         respond: Callable[[List[Email]], Awaitable[Union[Email, None]]],
282 |         mark_read: bool = True,
283 |         delay: int = 60,
284 |     ):
285 |         while True:
286 |             # Process emails
287 |             await self.process(respond, mark_read)
288 |             # Wait before next check
289 |             print(f"Sleeping for {delay}s...")
290 |             await asyncio.sleep(delay)
291 | 
```

--------------------------------------------------------------------------------
/tools/modelcontextprotocol/eslint.config.mjs:
--------------------------------------------------------------------------------

```
  1 | import prettier from "eslint-plugin-prettier";
  2 | import _import from "eslint-plugin-import";
  3 | import { fixupPluginRules } from "@eslint/compat";
  4 | import globals from "globals";
  5 | import typescriptEslint from "@typescript-eslint/eslint-plugin";
  6 | import path from "node:path";
  7 | import { fileURLToPath } from "node:url";
  8 | import js from "@eslint/js";
  9 | import { FlatCompat } from "@eslint/eslintrc";
 10 | 
 11 | const __filename = fileURLToPath(import.meta.url);
 12 | const __dirname = path.dirname(__filename);
 13 | const compat = new FlatCompat({
 14 |     baseDirectory: __dirname,
 15 |     recommendedConfig: js.configs.recommended,
 16 |     allConfig: js.configs.all
 17 | });
 18 | 
 19 | export default [...compat.extends("plugin:prettier/recommended"), {
 20 |     plugins: {
 21 |         prettier,
 22 |         import: fixupPluginRules(_import),
 23 |     },
 24 | 
 25 |     languageOptions: {
 26 |         globals: {
 27 |             ...globals.node,
 28 |         },
 29 | 
 30 |         ecmaVersion: 2018,
 31 |         sourceType: "commonjs",
 32 |     },
 33 | 
 34 |     rules: {
 35 |         "accessor-pairs": "error",
 36 |         "array-bracket-spacing": ["error", "never"],
 37 |         "array-callback-return": "off",
 38 |         "arrow-parens": "error",
 39 |         "arrow-spacing": "error",
 40 |         "block-scoped-var": "off",
 41 |         "block-spacing": "off",
 42 | 
 43 |         "brace-style": ["error", "1tbs", {
 44 |             allowSingleLine: true,
 45 |         }],
 46 | 
 47 |         "capitalized-comments": "off",
 48 |         "class-methods-use-this": "off",
 49 |         "comma-dangle": "off",
 50 |         "comma-spacing": "off",
 51 |         "comma-style": ["error", "last"],
 52 |         complexity: "error",
 53 |         "computed-property-spacing": ["error", "never"],
 54 |         "consistent-return": "off",
 55 |         "consistent-this": "off",
 56 |         curly: "error",
 57 |         "default-case": "off",
 58 |         "dot-location": ["error", "property"],
 59 |         "dot-notation": "error",
 60 |         "eol-last": "error",
 61 |         eqeqeq: "off",
 62 |         "func-call-spacing": "error",
 63 |         "func-name-matching": "error",
 64 |         "func-names": "off",
 65 | 
 66 |         "func-style": ["error", "declaration", {
 67 |             allowArrowFunctions: true,
 68 |         }],
 69 | 
 70 |         "generator-star-spacing": "error",
 71 |         "global-require": "off",
 72 |         "guard-for-in": "error",
 73 |         "handle-callback-err": "off",
 74 |         "id-blacklist": "error",
 75 |         "id-length": "off",
 76 |         "id-match": "error",
 77 |         "import/extensions": "off",
 78 |         "init-declarations": "off",
 79 |         "jsx-quotes": "error",
 80 |         "key-spacing": "error",
 81 | 
 82 |         "keyword-spacing": ["error", {
 83 |             after: true,
 84 |             before: true,
 85 |         }],
 86 | 
 87 |         "line-comment-position": "off",
 88 |         "linebreak-style": ["error", "unix"],
 89 |         "lines-around-directive": "error",
 90 |         "max-depth": "error",
 91 |         "max-len": "off",
 92 |         "max-lines": "off",
 93 |         "max-nested-callbacks": "error",
 94 |         "max-params": "off",
 95 |         "max-statements": "off",
 96 |         "max-statements-per-line": "off",
 97 |         "multiline-ternary": "off",
 98 |         "new-cap": "off",
 99 |         "new-parens": "error",
100 |         "newline-after-var": "off",
101 |         "newline-before-return": "off",
102 |         "newline-per-chained-call": "off",
103 |         "no-alert": "error",
104 |         "no-array-constructor": "error",
105 |         "no-await-in-loop": "error",
106 |         "no-bitwise": "off",
107 |         "no-caller": "error",
108 |         "no-catch-shadow": "off",
109 |         "no-compare-neg-zero": "error",
110 |         "no-confusing-arrow": "error",
111 |         "no-continue": "off",
112 |         "no-div-regex": "error",
113 |         "no-duplicate-imports": "off",
114 |         "no-else-return": "off",
115 |         "no-empty-function": "off",
116 |         "no-eq-null": "off",
117 |         "no-eval": "error",
118 |         "no-extend-native": "error",
119 |         "no-extra-bind": "error",
120 |         "no-extra-label": "error",
121 |         "no-extra-parens": "off",
122 |         "no-floating-decimal": "error",
123 |         "no-implicit-globals": "error",
124 |         "no-implied-eval": "error",
125 |         "no-inline-comments": "off",
126 |         "no-inner-declarations": ["error", "functions"],
127 |         "no-invalid-this": "off",
128 |         "no-iterator": "error",
129 |         "no-label-var": "error",
130 |         "no-labels": "error",
131 |         "no-lone-blocks": "error",
132 |         "no-lonely-if": "error",
133 |         "no-loop-func": "error",
134 |         "no-magic-numbers": "off",
135 |         "no-mixed-requires": "error",
136 |         "no-multi-assign": "off",
137 |         "no-multi-spaces": "error",
138 |         "no-multi-str": "error",
139 |         "no-multiple-empty-lines": "error",
140 |         "no-native-reassign": "error",
141 |         "no-negated-condition": "off",
142 |         "no-negated-in-lhs": "error",
143 |         "no-nested-ternary": "error",
144 |         "no-new": "error",
145 |         "no-new-func": "error",
146 |         "no-new-object": "error",
147 |         "no-new-require": "error",
148 |         "no-new-wrappers": "error",
149 |         "no-octal-escape": "error",
150 |         "no-param-reassign": "off",
151 |         "no-path-concat": "error",
152 | 
153 |         "no-plusplus": ["error", {
154 |             allowForLoopAfterthoughts: true,
155 |         }],
156 | 
157 |         "no-process-env": "off",
158 |         "no-process-exit": "error",
159 |         "no-proto": "error",
160 |         "no-prototype-builtins": "off",
161 |         "no-restricted-globals": "error",
162 |         "no-restricted-imports": "error",
163 |         "no-restricted-modules": "error",
164 |         "no-restricted-properties": "error",
165 |         "no-restricted-syntax": "error",
166 |         "no-return-assign": "error",
167 |         "no-return-await": "error",
168 |         "no-script-url": "error",
169 |         "no-self-compare": "error",
170 |         "no-sequences": "error",
171 |         "no-shadow": "off",
172 |         "no-shadow-restricted-names": "error",
173 |         "no-spaced-func": "error",
174 |         "no-sync": "error",
175 |         "no-tabs": "error",
176 |         "no-template-curly-in-string": "error",
177 |         "no-ternary": "off",
178 |         "no-throw-literal": "error",
179 |         "no-trailing-spaces": "error",
180 |         "no-undef-init": "error",
181 |         "no-undefined": "off",
182 |         "no-underscore-dangle": "off",
183 |         "no-unmodified-loop-condition": "error",
184 |         "no-unneeded-ternary": "error",
185 |         "no-unused-expressions": "error",
186 | 
187 |         "no-unused-vars": ["error", {
188 |             args: "none",
189 |         }],
190 | 
191 |         "no-use-before-define": "off",
192 |         "no-useless-call": "error",
193 |         "no-useless-computed-key": "error",
194 |         "no-useless-concat": "error",
195 |         "no-useless-constructor": "error",
196 |         "no-useless-escape": "off",
197 |         "no-useless-rename": "error",
198 |         "no-useless-return": "error",
199 |         "no-var": "off",
200 |         "no-void": "error",
201 |         "no-warning-comments": "error",
202 |         "no-whitespace-before-property": "error",
203 |         "no-with": "error",
204 |         "nonblock-statement-body-position": "error",
205 |         "object-curly-newline": "off",
206 |         "object-curly-spacing": ["error", "never"],
207 |         "object-property-newline": "off",
208 |         "object-shorthand": "off",
209 |         "one-var": "off",
210 |         "one-var-declaration-per-line": "error",
211 |         "operator-assignment": ["error", "always"],
212 |         "operator-linebreak": "off",
213 |         "padded-blocks": "off",
214 |         "prefer-arrow-callback": "off",
215 |         "prefer-const": "error",
216 | 
217 |         "prefer-destructuring": ["error", {
218 |             array: false,
219 |             object: false,
220 |         }],
221 | 
222 |         "prefer-numeric-literals": "error",
223 |         "prefer-promise-reject-errors": "error",
224 |         "prefer-reflect": "off",
225 |         "prefer-rest-params": "off",
226 |         "prefer-spread": "off",
227 |         "prefer-template": "off",
228 |         "quote-props": "off",
229 | 
230 |         quotes: ["error", "single", {
231 |             avoidEscape: true,
232 |         }],
233 | 
234 |         radix: "error",
235 |         "require-await": "error",
236 |         "require-jsdoc": "off",
237 |         "rest-spread-spacing": "error",
238 |         semi: "off",
239 | 
240 |         "semi-spacing": ["error", {
241 |             after: true,
242 |             before: false,
243 |         }],
244 | 
245 |         "sort-imports": "off",
246 |         "sort-keys": "off",
247 |         "sort-vars": "error",
248 |         "space-before-blocks": "error",
249 |         "space-before-function-paren": "off",
250 |         "space-in-parens": ["error", "never"],
251 |         "space-infix-ops": "error",
252 |         "space-unary-ops": "error",
253 |         "spaced-comment": ["error", "always"],
254 |         strict: "off",
255 |         "symbol-description": "error",
256 |         "template-curly-spacing": "error",
257 |         "template-tag-spacing": "error",
258 |         "unicode-bom": ["error", "never"],
259 |         "valid-jsdoc": "off",
260 |         "vars-on-top": "off",
261 |         "wrap-regex": "off",
262 |         "yield-star-spacing": "error",
263 |         yoda: ["error", "never"],
264 |     },
265 | }, ...compat.extends(
266 |     "eslint:recommended",
267 |     "plugin:@typescript-eslint/eslint-recommended",
268 |     "plugin:@typescript-eslint/recommended",
269 |     "plugin:prettier/recommended",
270 | ).map(config => ({
271 |     ...config,
272 |     files: ["**/*.ts"],
273 | })), {
274 |     files: ["**/*.ts"],
275 | 
276 |     plugins: {
277 |         "@typescript-eslint": typescriptEslint,
278 |         prettier,
279 |     },
280 | 
281 |     rules: {
282 |         "@typescript-eslint/no-use-before-define": 0,
283 |         "@typescript-eslint/no-empty-interface": 0,
284 |         "@typescript-eslint/no-unused-vars": 0,
285 |         "@typescript-eslint/triple-slash-reference": 0,
286 |         "@typescript-eslint/ban-ts-comment": "off",
287 |         "@typescript-eslint/no-empty-function": 0,
288 |         "@typescript-eslint/no-require-imports": 0,
289 | 
290 |         "@typescript-eslint/naming-convention": ["error", {
291 |             selector: "default",
292 |             format: ["camelCase", "UPPER_CASE", "PascalCase"],
293 |             leadingUnderscore: "allow",
294 |         }, {
295 |             selector: "property",
296 |             format: null,
297 |         }],
298 | 
299 |         "@typescript-eslint/no-explicit-any": 0,
300 |         "@typescript-eslint/explicit-function-return-type": "off",
301 |         "@typescript-eslint/no-this-alias": "off",
302 |         "@typescript-eslint/no-var-requires": 0,
303 |         "prefer-rest-params": "off",
304 |     },
305 | }, {
306 |     files: ["test/**/*.ts"],
307 | 
308 |     rules: {
309 |         "@typescript-eslint/explicit-function-return-type": "off",
310 |     },
311 | }];
312 | 
```

--------------------------------------------------------------------------------
/tools/typescript/eslint.config.mjs:
--------------------------------------------------------------------------------

```
  1 | import prettier from "eslint-plugin-prettier";
  2 | import _import from "eslint-plugin-import";
  3 | import { fixupPluginRules } from "@eslint/compat";
  4 | import globals from "globals";
  5 | import typescriptEslint from "@typescript-eslint/eslint-plugin";
  6 | import path from "node:path";
  7 | import { fileURLToPath } from "node:url";
  8 | import js from "@eslint/js";
  9 | import { FlatCompat } from "@eslint/eslintrc";
 10 | 
 11 | const __filename = fileURLToPath(import.meta.url);
 12 | const __dirname = path.dirname(__filename);
 13 | const compat = new FlatCompat({
 14 |     baseDirectory: __dirname,
 15 |     recommendedConfig: js.configs.recommended,
 16 |     allConfig: js.configs.all
 17 | });
 18 | 
 19 | export default [...compat.extends("plugin:prettier/recommended"), {
 20 |     plugins: {
 21 |         prettier,
 22 |         import: fixupPluginRules(_import),
 23 |     },
 24 | 
 25 |     languageOptions: {
 26 |         globals: {
 27 |             ...globals.node,
 28 |         },
 29 | 
 30 |         ecmaVersion: 2018,
 31 |         sourceType: "commonjs",
 32 |     },
 33 | 
 34 |     rules: {
 35 |         "accessor-pairs": "error",
 36 |         "array-bracket-spacing": ["error", "never"],
 37 |         "array-callback-return": "off",
 38 |         "arrow-parens": "error",
 39 |         "arrow-spacing": "error",
 40 |         "block-scoped-var": "off",
 41 |         "block-spacing": "off",
 42 | 
 43 |         "brace-style": ["error", "1tbs", {
 44 |             allowSingleLine: true,
 45 |         }],
 46 | 
 47 |         "capitalized-comments": "off",
 48 |         "class-methods-use-this": "off",
 49 |         "comma-dangle": "off",
 50 |         "comma-spacing": "off",
 51 |         "comma-style": ["error", "last"],
 52 |         complexity: "error",
 53 |         "computed-property-spacing": ["error", "never"],
 54 |         "consistent-return": "off",
 55 |         "consistent-this": "off",
 56 |         curly: "error",
 57 |         "default-case": "off",
 58 |         "dot-location": ["error", "property"],
 59 |         "dot-notation": "error",
 60 |         "eol-last": "error",
 61 |         eqeqeq: "off",
 62 |         "func-call-spacing": "error",
 63 |         "func-name-matching": "error",
 64 |         "func-names": "off",
 65 | 
 66 |         "func-style": ["error", "declaration", {
 67 |             allowArrowFunctions: true,
 68 |         }],
 69 | 
 70 |         "generator-star-spacing": "error",
 71 |         "global-require": "off",
 72 |         "guard-for-in": "error",
 73 |         "handle-callback-err": "off",
 74 |         "id-blacklist": "error",
 75 |         "id-length": "off",
 76 |         "id-match": "error",
 77 |         "import/extensions": "off",
 78 |         "init-declarations": "off",
 79 |         "jsx-quotes": "error",
 80 |         "key-spacing": "error",
 81 | 
 82 |         "keyword-spacing": ["error", {
 83 |             after: true,
 84 |             before: true,
 85 |         }],
 86 | 
 87 |         "line-comment-position": "off",
 88 |         "linebreak-style": ["error", "unix"],
 89 |         "lines-around-directive": "error",
 90 |         "max-depth": "error",
 91 |         "max-len": "off",
 92 |         "max-lines": "off",
 93 |         "max-nested-callbacks": "error",
 94 |         "max-params": "off",
 95 |         "max-statements": "off",
 96 |         "max-statements-per-line": "off",
 97 |         "multiline-ternary": "off",
 98 |         "new-cap": "off",
 99 |         "new-parens": "error",
100 |         "newline-after-var": "off",
101 |         "newline-before-return": "off",
102 |         "newline-per-chained-call": "off",
103 |         "no-alert": "error",
104 |         "no-array-constructor": "error",
105 |         "no-await-in-loop": "error",
106 |         "no-bitwise": "off",
107 |         "no-caller": "error",
108 |         "no-catch-shadow": "off",
109 |         "no-compare-neg-zero": "error",
110 |         "no-confusing-arrow": "error",
111 |         "no-continue": "off",
112 |         "no-div-regex": "error",
113 |         "no-duplicate-imports": "off",
114 |         "no-else-return": "off",
115 |         "no-empty-function": "off",
116 |         "no-eq-null": "off",
117 |         "no-eval": "error",
118 |         "no-extend-native": "error",
119 |         "no-extra-bind": "error",
120 |         "no-extra-label": "error",
121 |         "no-extra-parens": "off",
122 |         "no-floating-decimal": "error",
123 |         "no-implicit-globals": "error",
124 |         "no-implied-eval": "error",
125 |         "no-inline-comments": "off",
126 |         "no-inner-declarations": ["error", "functions"],
127 |         "no-invalid-this": "off",
128 |         "no-iterator": "error",
129 |         "no-label-var": "error",
130 |         "no-labels": "error",
131 |         "no-lone-blocks": "error",
132 |         "no-lonely-if": "error",
133 |         "no-loop-func": "error",
134 |         "no-magic-numbers": "off",
135 |         "no-mixed-requires": "error",
136 |         "no-multi-assign": "off",
137 |         "no-multi-spaces": "error",
138 |         "no-multi-str": "error",
139 |         "no-multiple-empty-lines": "error",
140 |         "no-native-reassign": "error",
141 |         "no-negated-condition": "off",
142 |         "no-negated-in-lhs": "error",
143 |         "no-nested-ternary": "error",
144 |         "no-new": "error",
145 |         "no-new-func": "error",
146 |         "no-new-object": "error",
147 |         "no-new-require": "error",
148 |         "no-new-wrappers": "error",
149 |         "no-octal-escape": "error",
150 |         "no-param-reassign": "off",
151 |         "no-path-concat": "error",
152 | 
153 |         "no-plusplus": ["error", {
154 |             allowForLoopAfterthoughts: true,
155 |         }],
156 | 
157 |         "no-process-env": "off",
158 |         "no-process-exit": "error",
159 |         "no-proto": "error",
160 |         "no-prototype-builtins": "off",
161 |         "no-restricted-globals": "error",
162 |         "no-restricted-imports": "error",
163 |         "no-restricted-modules": "error",
164 |         "no-restricted-properties": "error",
165 |         "no-restricted-syntax": "error",
166 |         "no-return-assign": "error",
167 |         "no-return-await": "error",
168 |         "no-script-url": "error",
169 |         "no-self-compare": "error",
170 |         "no-sequences": "error",
171 |         "no-shadow": "off",
172 |         "no-shadow-restricted-names": "error",
173 |         "no-spaced-func": "error",
174 |         "no-sync": "error",
175 |         "no-tabs": "error",
176 |         "no-template-curly-in-string": "error",
177 |         "no-ternary": "off",
178 |         "no-throw-literal": "error",
179 |         "no-trailing-spaces": "error",
180 |         "no-undef-init": "error",
181 |         "no-undefined": "off",
182 |         "no-underscore-dangle": "off",
183 |         "no-unmodified-loop-condition": "error",
184 |         "no-unneeded-ternary": "error",
185 |         "no-unused-expressions": "error",
186 | 
187 |         "no-unused-vars": ["error", {
188 |             args: "none",
189 |         }],
190 | 
191 |         "no-use-before-define": "off",
192 |         "no-useless-call": "error",
193 |         "no-useless-computed-key": "error",
194 |         "no-useless-concat": "error",
195 |         "no-useless-constructor": "error",
196 |         "no-useless-escape": "off",
197 |         "no-useless-rename": "error",
198 |         "no-useless-return": "error",
199 |         "no-var": "off",
200 |         "no-void": "error",
201 |         "no-warning-comments": "error",
202 |         "no-whitespace-before-property": "error",
203 |         "no-with": "error",
204 |         "nonblock-statement-body-position": "error",
205 |         "object-curly-newline": "off",
206 |         "object-curly-spacing": ["error", "never"],
207 |         "object-property-newline": "off",
208 |         "object-shorthand": "off",
209 |         "one-var": "off",
210 |         "one-var-declaration-per-line": "error",
211 |         "operator-assignment": ["error", "always"],
212 |         "operator-linebreak": "off",
213 |         "padded-blocks": "off",
214 |         "prefer-arrow-callback": "off",
215 |         "prefer-const": "error",
216 | 
217 |         "prefer-destructuring": ["error", {
218 |             array: false,
219 |             object: false,
220 |         }],
221 | 
222 |         "prefer-numeric-literals": "error",
223 |         "prefer-promise-reject-errors": "error",
224 |         "prefer-reflect": "off",
225 |         "prefer-rest-params": "off",
226 |         "prefer-spread": "off",
227 |         "prefer-template": "off",
228 |         "quote-props": "off",
229 | 
230 |         quotes: ["error", "single", {
231 |             avoidEscape: true,
232 |         }],
233 | 
234 |         radix: "error",
235 |         "require-await": "error",
236 |         "require-jsdoc": "off",
237 |         "rest-spread-spacing": "error",
238 |         semi: "off",
239 | 
240 |         "semi-spacing": ["error", {
241 |             after: true,
242 |             before: false,
243 |         }],
244 | 
245 |         "sort-imports": "off",
246 |         "sort-keys": "off",
247 |         "sort-vars": "error",
248 |         "space-before-blocks": "error",
249 |         "space-before-function-paren": "off",
250 |         "space-in-parens": ["error", "never"],
251 |         "space-infix-ops": "error",
252 |         "space-unary-ops": "error",
253 |         "spaced-comment": ["error", "always"],
254 |         strict: "off",
255 |         "symbol-description": "error",
256 |         "template-curly-spacing": "error",
257 |         "template-tag-spacing": "error",
258 |         "unicode-bom": ["error", "never"],
259 |         "valid-jsdoc": "off",
260 |         "vars-on-top": "off",
261 |         "wrap-regex": "off",
262 |         "yield-star-spacing": "error",
263 |         yoda: ["error", "never"],
264 |     },
265 | }, ...compat.extends(
266 |     "eslint:recommended",
267 |     "plugin:@typescript-eslint/eslint-recommended",
268 |     "plugin:@typescript-eslint/recommended",
269 |     "plugin:prettier/recommended",
270 | ).map(config => ({
271 |     ...config,
272 |     files: ["**/*.ts"],
273 | })), {
274 |     files: ["**/*.ts"],
275 | 
276 |     plugins: {
277 |         "@typescript-eslint": typescriptEslint,
278 |         prettier,
279 |     },
280 | 
281 |     rules: {
282 |         "@typescript-eslint/no-use-before-define": 0,
283 |         "@typescript-eslint/no-empty-interface": 0,
284 |         "@typescript-eslint/no-unused-vars": 0,
285 |         "@typescript-eslint/triple-slash-reference": 0,
286 |         "@typescript-eslint/ban-ts-comment": "off",
287 |         "@typescript-eslint/no-empty-function": 0,
288 |         "@typescript-eslint/no-require-imports": 0,
289 | 
290 |         "@typescript-eslint/naming-convention": ["error", {
291 |             selector: "default",
292 |             format: ["camelCase", "UPPER_CASE", "PascalCase"],
293 |             leadingUnderscore: "allow",
294 |         }, {
295 |             selector: "property",
296 |             format: null,
297 |         }],
298 | 
299 |         "@typescript-eslint/no-explicit-any": 0,
300 |         "@typescript-eslint/explicit-function-return-type": "off",
301 |         "@typescript-eslint/no-this-alias": "off",
302 |         "@typescript-eslint/no-var-requires": 0,
303 |         "prefer-rest-params": "off",
304 |     },
305 | }, {
306 |     files: ["test/**/*.ts"],
307 | 
308 |     rules: {
309 |         "@typescript-eslint/explicit-function-return-type": "off",
310 |     },
311 | }, {
312 |     files: ["examples/cloudflare/**/*.ts", "examples/cloudflare/**/*.js", "examples/cloudflare/**/*.mjs"],
313 |     ignores: [],
314 |     rules: {
315 |         // Disable all rules for cloudflare examples
316 |         ...Object.fromEntries(
317 |             Object.keys(typescriptEslint.rules).map(rule => [`@typescript-eslint/${rule}`, "off"])
318 |         ),
319 |         // Disable all base rules
320 |         "no-unused-vars": "off",
321 |         "no-undef": "off",
322 |         "no-console": "off",
323 |         "require-await": "off",
324 |         "prettier/prettier": "off",
325 |         "func-style": "off",
326 |         "no-warning-comments": "off",
327 |         "no-constant-condition": "off",
328 |         // Add any other rules you want to disable
329 |     }
330 | }];
331 | 
```

--------------------------------------------------------------------------------
/llm/token-meter/utils/type-detection.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Utilities for detecting response types from different AI providers
  3 |  */
  4 | 
  5 | import type OpenAI from 'openai';
  6 | import type {Stream} from 'openai/streaming';
  7 | import type Anthropic from '@anthropic-ai/sdk';
  8 | import type {Stream as AnthropicStream} from '@anthropic-ai/sdk/streaming';
  9 | import type {GenerateContentResult} from '@google/generative-ai';
 10 | 
 11 | /**
 12 |  * Provider types
 13 |  */
 14 | export type Provider = 'openai' | 'anthropic' | 'google' | 'unknown';
 15 | 
 16 | /**
 17 |  * Response type categories
 18 |  */
 19 | export type ResponseType =
 20 |   | 'chat_completion'
 21 |   | 'response_api'
 22 |   | 'embedding'
 23 |   | 'unknown';
 24 | 
 25 | /**
 26 |  * Detected response information
 27 |  */
 28 | export interface DetectedResponse {
 29 |   provider: Provider;
 30 |   type: ResponseType;
 31 |   model: string;
 32 |   inputTokens: number;
 33 |   outputTokens: number;
 34 | }
 35 | 
 36 | /**
 37 |  * Check if response is an OpenAI ChatCompletion
 38 |  */
 39 | function isOpenAIChatCompletion(response: any): response is OpenAI.ChatCompletion {
 40 |   return (
 41 |     response &&
 42 |     typeof response === 'object' &&
 43 |     'choices' in response &&
 44 |     'model' in response &&
 45 |     response.choices?.[0]?.message !== undefined
 46 |   );
 47 | }
 48 | 
 49 | /**
 50 |  * Check if response is an OpenAI Responses API response
 51 |  */
 52 | function isOpenAIResponse(response: any): response is OpenAI.Responses.Response {
 53 |   return (
 54 |     response &&
 55 |     typeof response === 'object' &&
 56 |     'output' in response &&
 57 |     'model' in response &&
 58 |     'usage' in response &&
 59 |     response.usage?.input_tokens !== undefined
 60 |   );
 61 | }
 62 | 
 63 | /**
 64 |  * Check if response is an OpenAI Embedding response
 65 |  */
 66 | function isOpenAIEmbedding(
 67 |   response: any
 68 | ): response is OpenAI.CreateEmbeddingResponse {
 69 |   return (
 70 |     response &&
 71 |     typeof response === 'object' &&
 72 |     'data' in response &&
 73 |     'model' in response &&
 74 |     Array.isArray(response.data) &&
 75 |     response.data?.[0]?.embedding !== undefined
 76 |   );
 77 | }
 78 | 
 79 | /**
 80 |  * Check if response is an Anthropic Message
 81 |  */
 82 | function isAnthropicMessage(
 83 |   response: any
 84 | ): response is Anthropic.Messages.Message {
 85 |   return (
 86 |     response &&
 87 |     typeof response === 'object' &&
 88 |     'content' in response &&
 89 |     'model' in response &&
 90 |     'usage' in response &&
 91 |     response.usage?.input_tokens !== undefined &&
 92 |     response.usage?.output_tokens !== undefined &&
 93 |     response.type === 'message'
 94 |   );
 95 | }
 96 | 
 97 | /**
 98 |  * Check if response is a Gemini GenerateContentResult
 99 |  */
100 | function isGeminiResponse(response: any): response is GenerateContentResult {
101 |   return (
102 |     response &&
103 |     typeof response === 'object' &&
104 |     'response' in response &&
105 |     response.response?.usageMetadata !== undefined &&
106 |     response.response?.usageMetadata?.promptTokenCount !== undefined
107 |   );
108 | }
109 | 
110 | /**
111 |  * Detect and extract usage information from a response
112 |  */
113 | export function detectResponse(response: any): DetectedResponse | null {
114 |   // OpenAI Chat Completion
115 |   if (isOpenAIChatCompletion(response)) {
116 |     return {
117 |       provider: 'openai',
118 |       type: 'chat_completion',
119 |       model: response.model,
120 |       inputTokens: response.usage?.prompt_tokens ?? 0,
121 |       outputTokens: response.usage?.completion_tokens ?? 0,
122 |     };
123 |   }
124 | 
125 |   // OpenAI Responses API
126 |   if (isOpenAIResponse(response)) {
127 |     return {
128 |       provider: 'openai',
129 |       type: 'response_api',
130 |       model: response.model,
131 |       inputTokens: response.usage?.input_tokens ?? 0,
132 |       outputTokens: response.usage?.output_tokens ?? 0,
133 |     };
134 |   }
135 | 
136 |   // OpenAI Embeddings
137 |   if (isOpenAIEmbedding(response)) {
138 |     return {
139 |       provider: 'openai',
140 |       type: 'embedding',
141 |       model: response.model,
142 |       inputTokens: response.usage?.prompt_tokens ?? 0,
143 |       outputTokens: 0, // Embeddings don't have output tokens
144 |     };
145 |   }
146 | 
147 |   // Anthropic Message
148 |   if (isAnthropicMessage(response)) {
149 |     return {
150 |       provider: 'anthropic',
151 |       type: 'chat_completion',
152 |       model: response.model,
153 |       inputTokens: response.usage?.input_tokens ?? 0,
154 |       outputTokens: response.usage?.output_tokens ?? 0,
155 |     };
156 |   }
157 | 
158 |   // Gemini GenerateContentResult
159 |   if (isGeminiResponse(response)) {
160 |     const usageMetadata = response.response.usageMetadata;
161 |     const baseOutputTokens = usageMetadata?.candidatesTokenCount ?? 0;
162 |     // thoughtsTokenCount is for extended thinking models, may not always be present
163 |     const reasoningTokens = (usageMetadata as any)?.thoughtsTokenCount ?? 0;
164 |     
165 |     // Extract model name from response if available
166 |     const model = (response.response as any)?.modelVersion || 'gemini';
167 | 
168 |     return {
169 |       provider: 'google',
170 |       type: 'chat_completion',
171 |       model,
172 |       inputTokens: usageMetadata?.promptTokenCount ?? 0,
173 |       outputTokens: baseOutputTokens + reasoningTokens, // Include reasoning tokens
174 |     };
175 |   }
176 | 
177 |   // Unknown response type
178 |   return null;
179 | }
180 | 
181 | /**
182 |  * Stream type detection
183 |  */
184 | export type StreamType = 'chat_completion' | 'response_api' | 'unknown';
185 | 
186 | /**
187 |  * Detect stream type by checking if it has OpenAI stream methods
188 |  * OpenAI streams have 'toReadableStream' method, Anthropic streams don't
189 |  */
190 | export function isOpenAIStream(stream: any): stream is Stream<any> {
191 |   return (
192 |     stream &&
193 |     typeof stream === 'object' &&
194 |     'tee' in stream &&
195 |     'toReadableStream' in stream
196 |   );
197 | }
198 | 
199 | /**
200 |  * Extract usage from OpenAI chat completion stream chunks
201 |  */
202 | export async function extractUsageFromChatStream(
203 |   stream: Stream<OpenAI.ChatCompletionChunk>
204 | ): Promise<DetectedResponse | null> {
205 |   let usage: any = {
206 |     prompt_tokens: 0,
207 |     completion_tokens: 0,
208 |   };
209 |   let model = '';
210 | 
211 |   try {
212 |     for await (const chunk of stream) {
213 |       if (chunk.model) {
214 |         model = chunk.model;
215 |       }
216 |       if (chunk.usage) {
217 |         usage = chunk.usage;
218 |       }
219 |     }
220 | 
221 |     if (model) {
222 |       return {
223 |         provider: 'openai',
224 |         type: 'chat_completion',
225 |         model,
226 |         inputTokens: usage.prompt_tokens ?? 0,
227 |         outputTokens: usage.completion_tokens ?? 0,
228 |       };
229 |     }
230 |   } catch (error) {
231 |     console.error('Error extracting usage from chat stream:', error);
232 |   }
233 | 
234 |   return null;
235 | }
236 | 
237 | /**
238 |  * Extract usage from OpenAI Responses API stream events
239 |  */
240 | export async function extractUsageFromResponseStream(
241 |   stream: Stream<OpenAI.Responses.ResponseStreamEvent>
242 | ): Promise<DetectedResponse | null> {
243 |   let usage: any = {
244 |     input_tokens: 0,
245 |     output_tokens: 0,
246 |   };
247 |   let model = '';
248 | 
249 |   try {
250 |     for await (const chunk of stream) {
251 |       if ('response' in chunk && chunk.response) {
252 |         if (chunk.response.model) {
253 |           model = chunk.response.model;
254 |         }
255 |         if (chunk.response.usage) {
256 |           usage = chunk.response.usage;
257 |         }
258 |       }
259 |     }
260 | 
261 |     if (model) {
262 |       return {
263 |         provider: 'openai',
264 |         type: 'response_api',
265 |         model,
266 |         inputTokens: usage.input_tokens ?? 0,
267 |         outputTokens: usage.output_tokens ?? 0,
268 |       };
269 |     }
270 |   } catch (error) {
271 |     console.error('Error extracting usage from response stream:', error);
272 |   }
273 | 
274 |   return null;
275 | }
276 | 
277 | /**
278 |  * Check if stream is an Anthropic stream
279 |  * Anthropic streams have 'controller' but NOT 'toReadableStream'
280 |  */
281 | export function isAnthropicStream(
282 |   stream: any
283 | ): stream is AnthropicStream<Anthropic.Messages.RawMessageStreamEvent> {
284 |   return (
285 |     stream &&
286 |     typeof stream === 'object' &&
287 |     'tee' in stream &&
288 |     'controller' in stream &&
289 |     !('toReadableStream' in stream)
290 |   );
291 | }
292 | 
293 | /**
294 |  * Extract usage from Anthropic message stream events
295 |  */
296 | export async function extractUsageFromAnthropicStream(
297 |   stream: AnthropicStream<Anthropic.Messages.RawMessageStreamEvent>
298 | ): Promise<DetectedResponse | null> {
299 |   const usage: {
300 |     input_tokens: number;
301 |     output_tokens: number;
302 |   } = {
303 |     input_tokens: 0,
304 |     output_tokens: 0,
305 |   };
306 |   let model = '';
307 | 
308 |   try {
309 |     for await (const chunk of stream) {
310 |       // Capture usage from message_start event (input tokens)
311 |       if (chunk.type === 'message_start') {
312 |         usage.input_tokens = chunk.message.usage.input_tokens ?? 0;
313 |         model = chunk.message.model;
314 |       }
315 |       // Capture usage from message_delta event (output tokens)
316 |       if (chunk.type === 'message_delta' && 'usage' in chunk) {
317 |         usage.output_tokens = chunk.usage.output_tokens ?? 0;
318 |       }
319 |     }
320 | 
321 |     if (model) {
322 |       return {
323 |         provider: 'anthropic',
324 |         type: 'chat_completion',
325 |         model,
326 |         inputTokens: usage.input_tokens,
327 |         outputTokens: usage.output_tokens,
328 |       };
329 |     }
330 |   } catch (error) {
331 |     console.error('Error extracting usage from Anthropic stream:', error);
332 |   }
333 | 
334 |   return null;
335 | }
336 | 
337 | /**
338 |  * Check if stream is a Gemini stream
339 |  * Gemini returns an object with {stream, response}, not just a stream
340 |  */
341 | export function isGeminiStream(stream: any): boolean {
342 |   // Gemini returns {stream: AsyncGenerator, response: Promise}
343 |   return (
344 |     stream &&
345 |     typeof stream === 'object' &&
346 |     'stream' in stream &&
347 |     'response' in stream &&
348 |     typeof stream.stream?.[Symbol.asyncIterator] === 'function' &&
349 |     !('tee' in stream)
350 |   );
351 | }
352 | 
353 | /**
354 |  * Extract usage from Gemini stream
355 |  * Gemini provides an object with both stream and response promise
356 |  */
357 | export async function extractUsageFromGeminiStream(
358 |   streamResult: any
359 | ): Promise<DetectedResponse | null> {
360 |   try {
361 |     // Gemini returns {stream, response}
362 |     // We need to consume the stream to get usage
363 |     let lastUsageMetadata: any = null;
364 | 
365 |     for await (const chunk of streamResult.stream) {
366 |       if (chunk.usageMetadata) {
367 |         lastUsageMetadata = chunk.usageMetadata;
368 |       }
369 |     }
370 | 
371 |     if (lastUsageMetadata) {
372 |       const baseOutputTokens = lastUsageMetadata?.candidatesTokenCount ?? 0;
373 |       // thoughtsTokenCount is for extended thinking models, may not always be present
374 |       const reasoningTokens = (lastUsageMetadata as any)?.thoughtsTokenCount ?? 0;
375 |       
376 |       // Get model from the response - this field is always present in real Gemini responses
377 |       const response = await streamResult.response;
378 |       const model = (response as any)?.modelVersion;
379 |       
380 |       if (!model) {
381 |         throw new Error('Gemini response is missing modelVersion field. This should never happen with real Gemini API responses.');
382 |       }
383 | 
384 |       return {
385 |         provider: 'google',
386 |         type: 'chat_completion',
387 |         model,
388 |         inputTokens: lastUsageMetadata?.promptTokenCount ?? 0,
389 |         outputTokens: baseOutputTokens + reasoningTokens,
390 |       };
391 |     }
392 |   } catch (error) {
393 |     console.error('Error extracting usage from Gemini stream:', error);
394 |   }
395 | 
396 |   return null;
397 | }
398 | 
399 | /**
400 |  * Detect stream type by examining first chunk (without consuming the stream)
401 |  * This is a heuristic approach - for now we'll try chat completion first,
402 |  * then fall back to response API
403 |  */
404 | export function detectStreamType(_stream: any): StreamType {
405 |   // For now, we'll assume chat completion by default
406 |   // In the future, we could peek at the first chunk to determine type
407 |   return 'chat_completion';
408 | }
409 | 
410 | 
```

--------------------------------------------------------------------------------
/tools/python/stripe_agent_toolkit/functions.py:
--------------------------------------------------------------------------------

```python
  1 | import stripe
  2 | from typing import Optional
  3 | from .configuration import Context
  4 | 
  5 | 
  6 | def create_customer(context: Context, name: str, email: Optional[str] = None):
  7 |     """
  8 |     Create a customer.
  9 | 
 10 |     Parameters:
 11 |         name (str): The name of the customer.
 12 |         email (str, optional): The email address of the customer.
 13 | 
 14 |     Returns:
 15 |         stripe.Customer: The created customer.
 16 |     """
 17 |     customer_data: dict = {"name": name}
 18 |     if email:
 19 |         customer_data["email"] = email
 20 |     if context.get("account") is not None:
 21 |         account = context.get("account")
 22 |         if account is not None:
 23 |             customer_data["stripe_account"] = account
 24 | 
 25 |     customer = stripe.Customer.create(**customer_data)
 26 |     return {"id": customer.id}
 27 | 
 28 | 
 29 | def list_customers(
 30 |     context: Context,
 31 |     email: Optional[str] = None,
 32 |     limit: Optional[int] = None,
 33 | ):
 34 |     """
 35 |     List Customers.
 36 | 
 37 |     Parameters:
 38 |         email (str, optional): The email address of the customer.
 39 |         limit (int, optional): The number of customers to return.
 40 | 
 41 |     Returns:
 42 |         stripe.ListObject: A list of customers.
 43 |     """
 44 |     customer_data: dict = {}
 45 |     if email:
 46 |         customer_data["email"] = email
 47 |     if limit:
 48 |         customer_data["limit"] = limit
 49 |     if context.get("account") is not None:
 50 |         account = context.get("account")
 51 |         if account is not None:
 52 |             customer_data["stripe_account"] = account
 53 | 
 54 |     customers = stripe.Customer.list(**customer_data)
 55 |     return [{"id": customer.id} for customer in customers.data]
 56 | 
 57 | 
 58 | def create_product(
 59 |     context: Context, name: str, description: Optional[str] = None
 60 | ):
 61 |     """
 62 |     Create a product.
 63 | 
 64 |     Parameters:
 65 |         name (str): The name of the product.
 66 |         description (str, optional): The description of the product.
 67 | 
 68 |     Returns:
 69 |         stripe.Product: The created product.
 70 |     """
 71 |     product_data: dict = {"name": name}
 72 |     if description:
 73 |         product_data["description"] = description
 74 |     if context.get("account") is not None:
 75 |         account = context.get("account")
 76 |         if account is not None:
 77 |             product_data["stripe_account"] = account
 78 | 
 79 |     return stripe.Product.create(**product_data)
 80 | 
 81 | 
 82 | def list_products(context: Context, limit: Optional[int] = None):
 83 |     """
 84 |     List Products.
 85 |     Parameters:
 86 |         limit (int, optional): The number of products to return.
 87 | 
 88 |     Returns:
 89 |         stripe.ListObject: A list of products.
 90 |     """
 91 |     product_data: dict = {}
 92 |     if limit:
 93 |         product_data["limit"] = limit
 94 |     if context.get("account") is not None:
 95 |         account = context.get("account")
 96 |         if account is not None:
 97 |             product_data["stripe_account"] = account
 98 | 
 99 |     return stripe.Product.list(**product_data).data
100 | 
101 | 
102 | def create_price(
103 |     context: Context, product: str, currency: str, unit_amount: int
104 | ):
105 |     """
106 |     Create a price.
107 | 
108 |     Parameters:
109 |         product (str): The ID of the product.
110 |         currency (str): The currency of the price.
111 |         unit_amount (int): The unit amount of the price.
112 | 
113 |     Returns:
114 |         stripe.Price: The created price.
115 |     """
116 |     price_data: dict = {
117 |         "product": product,
118 |         "currency": currency,
119 |         "unit_amount": unit_amount,
120 |     }
121 |     if context.get("account") is not None:
122 |         account = context.get("account")
123 |         if account is not None:
124 |             price_data["stripe_account"] = account
125 | 
126 |     return stripe.Price.create(**price_data)
127 | 
128 | 
129 | def list_prices(
130 |     context: Context,
131 |     product: Optional[str] = None,
132 |     limit: Optional[int] = None,
133 | ):
134 |     """
135 |     List Prices.
136 | 
137 |     Parameters:
138 |         product (str, optional): The ID of the product to list prices for.
139 |         limit (int, optional): The number of prices to return.
140 | 
141 |     Returns:
142 |         stripe.ListObject: A list of prices.
143 |     """
144 |     prices_data: dict = {}
145 |     if product:
146 |         prices_data["product"] = product
147 |     if limit:
148 |         prices_data["limit"] = limit
149 |     if context.get("account") is not None:
150 |         account = context.get("account")
151 |         if account is not None:
152 |             prices_data["stripe_account"] = account
153 | 
154 |     return stripe.Price.list(**prices_data).data
155 | 
156 | 
157 | def create_payment_link(context: Context, price: str, quantity: int, redirect_url: Optional[str] = None):
158 |     """
159 |     Create a payment link.
160 | 
161 |     Parameters:
162 |         price (str): The ID of the price.
163 |         quantity (int): The quantity of the product.
164 |         redirect_url (string, optional): The URL the customer will be redirected to after the purchase is complete.
165 | 
166 |     Returns:
167 |         stripe.PaymentLink: The created payment link.
168 |     """
169 |     payment_link_data: dict = {
170 |         "line_items": [{"price": price, "quantity": quantity}],
171 |     }
172 |     if context.get("account") is not None:
173 |         account = context.get("account")
174 |         if account is not None:
175 |             payment_link_data["stripe_account"] = account
176 | 
177 |     if redirect_url:
178 |         payment_link_data["after_completion"] = {"type": "redirect", "redirect": {"url": redirect_url}}
179 | 
180 |     payment_link = stripe.PaymentLink.create(**payment_link_data)
181 | 
182 |     return {"id": payment_link.id, "url": payment_link.url}
183 | 
184 | 
185 | def list_invoices(
186 |     context: Context,
187 |     customer: Optional[str] = None,
188 |     limit: Optional[int] = None,
189 | ):
190 |     """
191 |     List invoices.
192 | 
193 |     Parameters:
194 |         customer (str, optional): The ID of the customer.
195 |         limit (int, optional): The number of invoices to return.
196 | 
197 |     Returns:
198 |         stripe.ListObject: A list of invoices.
199 |     """
200 |     invoice_data: dict = {}
201 |     if customer:
202 |         invoice_data["customer"] = customer
203 |     if limit:
204 |         invoice_data["limit"] = limit
205 |     if context.get("account") is not None:
206 |         account = context.get("account")
207 |         if account is not None:
208 |             invoice_data["stripe_account"] = account
209 | 
210 |     return stripe.Invoice.list(**invoice_data).data
211 | 
212 | 
213 | def create_invoice(context: Context, customer: str, days_until_due: int = 30):
214 |     """
215 |     Create an invoice.
216 | 
217 |     Parameters:
218 |         customer (str): The ID of the customer.
219 |         days_until_due (int, optional): The number of days until the
220 |         invoice is due.
221 | 
222 |     Returns:
223 |         stripe.Invoice: The created invoice.
224 |     """
225 |     invoice_data: dict = {
226 |         "customer": customer,
227 |         "collection_method": "send_invoice",
228 |         "days_until_due": days_until_due,
229 |     }
230 |     if context.get("account") is not None:
231 |         account = context.get("account")
232 |         if account is not None:
233 |             invoice_data["stripe_account"] = account
234 | 
235 |     invoice = stripe.Invoice.create(**invoice_data)
236 | 
237 |     return {
238 |         "id": invoice.id,
239 |         "hosted_invoice_url": invoice.hosted_invoice_url,
240 |         "customer": invoice.customer,
241 |         "status": invoice.status,
242 |     }
243 | 
244 | 
245 | def create_invoice_item(
246 |     context: Context, customer: str, price: str, invoice: str
247 | ):
248 |     """
249 |     Create an invoice item.
250 | 
251 |     Parameters:
252 |         customer (str): The ID of the customer.
253 |         price (str): The ID of the price.
254 |         invoice (str): The ID of the invoice.
255 | 
256 |     Returns:
257 |         stripe.InvoiceItem: The created invoice item.
258 |     """
259 |     invoice_item_data: dict = {
260 |         "customer": customer,
261 |         "price": price,
262 |         "invoice": invoice,
263 |     }
264 |     if context.get("account") is not None:
265 |         account = context.get("account")
266 |         if account is not None:
267 |             invoice_item_data["stripe_account"] = account
268 | 
269 |     invoice_item = stripe.InvoiceItem.create(**invoice_item_data)
270 | 
271 |     return {"id": invoice_item.id, "invoice": invoice_item.invoice}
272 | 
273 | 
274 | def finalize_invoice(context: Context, invoice: str):
275 |     """
276 |     Finalize an invoice.
277 | 
278 |     Parameters:
279 |         invoice (str): The ID of the invoice.
280 | 
281 |     Returns:
282 |         stripe.Invoice: The finalized invoice.
283 |     """
284 |     invoice_data: dict = {"invoice": invoice}
285 |     if context.get("account") is not None:
286 |         account = context.get("account")
287 |         if account is not None:
288 |             invoice_data["stripe_account"] = account
289 | 
290 |     invoice_object = stripe.Invoice.finalize_invoice(**invoice_data)
291 | 
292 |     return {
293 |         "id": invoice_object.id,
294 |         "hosted_invoice_url": invoice_object.hosted_invoice_url,
295 |         "customer": invoice_object.customer,
296 |         "status": invoice_object.status,
297 |     }
298 | 
299 | 
300 | def retrieve_balance(
301 |     context: Context,
302 | ):
303 |     """
304 |     Retrieve the balance.
305 | 
306 |     Returns:
307 |         stripe.Balance: The balance.
308 |     """
309 |     balance_data: dict = {}
310 |     if context.get("account") is not None:
311 |         account = context.get("account")
312 |         if account is not None:
313 |             balance_data["stripe_account"] = account
314 | 
315 |     return stripe.Balance.retrieve(**balance_data)
316 | 
317 | 
318 | def create_refund(
319 |     context: Context, payment_intent: str, amount: Optional[int] = None
320 | ):
321 |     """
322 |     Create a refund.
323 | 
324 |     Parameters:
325 |         payment_intent (str): The ID of the payment intent.
326 |         amount (int, optional): The amount to refund in cents.
327 | 
328 |     Returns:
329 |         stripe.Refund: The created refund.
330 |     """
331 |     refund_data: dict = {
332 |         "payment_intent": payment_intent,
333 |     }
334 |     if amount:
335 |         refund_data["amount"] = amount
336 |         if context.get("account") is not None:
337 |             account = context.get("account")
338 |             if account is not None:
339 |                 refund_data["stripe_account"] = account
340 | 
341 |     return stripe.Refund.create(**refund_data)
342 | 
343 | def list_payment_intents(context: Context, customer: Optional[str] = None, limit: Optional[int] = None):
344 |     """
345 |     List payment intents.
346 | 
347 |     Parameters:
348 |         customer (str, optional): The ID of the customer to list payment intents for.
349 |         limit (int, optional): The number of payment intents to return.
350 | 
351 |     Returns:
352 |         stripe.ListObject: A list of payment intents.
353 |     """
354 |     payment_intent_data: dict = {}
355 |     if customer:
356 |         payment_intent_data["customer"] = customer
357 |     if limit:
358 |         payment_intent_data["limit"] = limit
359 |     if context.get("account") is not None:
360 |         account = context.get("account")
361 |         if account is not None:
362 |             payment_intent_data["stripe_account"] = account
363 | 
364 |     return stripe.PaymentIntent.list(**payment_intent_data).data
365 | 
366 | def create_billing_portal_session(context: Context, customer: str, return_url: Optional[str] = None):
367 |     """
368 |     Creates a session of the customer portal.
369 | 
370 |     Parameters:
371 |         customer (str): The ID of the customer to list payment intents for.
372 |         return_url (str, optional): The URL to return to after the session is complete.
373 | 
374 |     Returns:
375 |         stripe.BillingPortalSession: The created billing portal session.
376 |     """
377 |     billing_portal_session_data: dict = {
378 |         "customer": customer,
379 |     }
380 |     if return_url:
381 |         billing_portal_session_data["return_url"] = return_url
382 |     if context.get("account") is not None:
383 |         account = context.get("account")
384 |         if account is not None:
385 |             billing_portal_session_data["stripe_account"] = account
386 | 
387 |     session_object = stripe.billing_portal.Session.create(**billing_portal_session_data)
388 | 
389 |     return {
390 |         "id": session_object.id,
391 |         "customer": session_object.customer,
392 |         "url": session_object.url,
393 |     }
394 | 
```
Page 4/7FirstPrevNextLast