@salimassili/ai-costguard 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/LICENSE +21 -0
  3. package/README.md +281 -103
  4. package/benchmarks/run.mjs +229 -0
  5. package/dist/cli.d.ts +50 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +178 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/core/CostGuard.d.ts +3 -4
  10. package/dist/core/CostGuard.d.ts.map +1 -1
  11. package/dist/core/CostGuard.js +1 -2
  12. package/dist/core/CostGuard.js.map +1 -1
  13. package/dist/core/GuardCore.d.ts +93 -13
  14. package/dist/core/GuardCore.d.ts.map +1 -1
  15. package/dist/core/GuardCore.js +372 -158
  16. package/dist/core/GuardCore.js.map +1 -1
  17. package/dist/core/GuardFree.d.ts +42 -18
  18. package/dist/core/GuardFree.d.ts.map +1 -1
  19. package/dist/core/GuardFree.js +95 -140
  20. package/dist/core/GuardFree.js.map +1 -1
  21. package/dist/core/GuardPro.d.ts +85 -5
  22. package/dist/core/GuardPro.d.ts.map +1 -1
  23. package/dist/core/GuardPro.js +216 -121
  24. package/dist/core/GuardPro.js.map +1 -1
  25. package/dist/core/event-log.d.ts +37 -0
  26. package/dist/core/event-log.d.ts.map +1 -0
  27. package/dist/core/event-log.js +49 -0
  28. package/dist/core/event-log.js.map +1 -0
  29. package/dist/core/events.d.ts +20 -0
  30. package/dist/core/events.d.ts.map +1 -0
  31. package/dist/core/events.js +46 -0
  32. package/dist/core/events.js.map +1 -0
  33. package/dist/core/similarity.d.ts +13 -0
  34. package/dist/core/similarity.d.ts.map +1 -0
  35. package/dist/core/similarity.js +51 -0
  36. package/dist/core/similarity.js.map +1 -0
  37. package/dist/core/tokenizer.d.ts +18 -0
  38. package/dist/core/tokenizer.d.ts.map +1 -0
  39. package/dist/core/tokenizer.js +137 -0
  40. package/dist/core/tokenizer.js.map +1 -0
  41. package/dist/core/types.d.ts +153 -5
  42. package/dist/core/types.d.ts.map +1 -1
  43. package/dist/core/types.js +0 -3
  44. package/dist/core/types.js.map +1 -1
  45. package/dist/core/webhooks.d.ts +15 -0
  46. package/dist/core/webhooks.d.ts.map +1 -0
  47. package/dist/core/webhooks.js +58 -0
  48. package/dist/core/webhooks.js.map +1 -0
  49. package/dist/dashboard.d.ts +73 -0
  50. package/dist/dashboard.d.ts.map +1 -0
  51. package/dist/dashboard.js +201 -0
  52. package/dist/dashboard.js.map +1 -0
  53. package/dist/index.d.ts +3 -4
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +1 -2
  56. package/dist/index.js.map +1 -1
  57. package/dist/pricing/index.d.ts +19 -2
  58. package/dist/pricing/index.d.ts.map +1 -1
  59. package/dist/pricing/index.js +93 -13
  60. package/dist/pricing/index.js.map +1 -1
  61. package/dist/pro.d.ts +3 -0
  62. package/dist/pro.d.ts.map +1 -0
  63. package/dist/pro.js +2 -0
  64. package/dist/pro.js.map +1 -0
  65. package/docs/BENCHMARKS.md +51 -0
  66. package/docs/DASHBOARD.md +61 -0
  67. package/docs/INTEGRATIONS.md +153 -0
  68. package/examples/integrations/anthropic-workflow-budget.mjs +36 -0
  69. package/examples/integrations/ci-budget-check.mjs +32 -0
  70. package/examples/integrations/crewai-budget-gate.mjs +31 -0
  71. package/examples/integrations/langchain-retry-storm.mjs +32 -0
  72. package/examples/integrations/mastra-agent.mjs +41 -0
  73. package/examples/integrations/openai-agent-loop.mjs +44 -0
  74. package/examples/integrations/vercel-ai-chatbot.mjs +29 -0
  75. package/package.json +35 -7
@@ -0,0 +1,153 @@
1
+ # Integrations
2
+
3
+ AI CostGuard works best when it sits immediately before an AI provider call or agent step. The examples in `examples/integrations` use mocked SDK surfaces so they run without API keys or paid requests.
4
+
5
+ Run them after building the package:
6
+
7
+ ```bash
8
+ npm run build
9
+ node examples/integrations/openai-agent-loop.mjs
10
+ node examples/integrations/anthropic-workflow-budget.mjs
11
+ node examples/integrations/vercel-ai-chatbot.mjs
12
+ node examples/integrations/langchain-retry-storm.mjs
13
+ node examples/integrations/mastra-agent.mjs
14
+ node examples/integrations/crewai-budget-gate.mjs
15
+ node examples/integrations/ci-budget-check.mjs
16
+ ```
17
+
18
+ ## OpenAI SDK
19
+
20
+ Use `guard()` around the OpenAI client. AI CostGuard guards `chat.completions.create`, `responses.create`, and `completions.create` by default.
21
+
22
+ ```ts
23
+ import OpenAI from 'openai';
24
+ import { guard } from '@salimassili/ai-costguard';
25
+
26
+ const openai = guard(new OpenAI({ apiKey: process.env.OPENAI_API_KEY }), {
27
+ budget: 5,
28
+ scope: { projectId: 'api', sessionId: 'agent-run-1' },
29
+ });
30
+
31
+ await openai.chat.completions.create({
32
+ model: 'gpt-4o-mini',
33
+ messages: [{ role: 'user', content: 'Summarize this ticket.' }],
34
+ max_tokens: 200,
35
+ });
36
+ ```
37
+
38
+ Runnable mock: `examples/integrations/openai-agent-loop.mjs`
39
+
40
+ ## Anthropic SDK
41
+
42
+ Use `guard()` around the Anthropic client. AI CostGuard guards `messages.create` by default.
43
+
44
+ ```ts
45
+ import Anthropic from '@anthropic-ai/sdk';
46
+ import { guard } from '@salimassili/ai-costguard';
47
+
48
+ const anthropic = guard(new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }), {
49
+ budget: 2,
50
+ scope: { projectId: 'workflow', sessionId: 'daily-run' },
51
+ });
52
+
53
+ await anthropic.messages.create({
54
+ model: 'claude-haiku-4.5',
55
+ max_tokens: 300,
56
+ messages: [{ role: 'user', content: 'Draft the daily workflow summary.' }],
57
+ });
58
+ ```
59
+
60
+ Runnable mock: `examples/integrations/anthropic-workflow-budget.mjs`
61
+
62
+ ## Vercel AI SDK
63
+
64
+ Vercel AI SDK calls are often function-style. Use `guardFunction()` around your `generateText` adapter and pass a request object that includes `model`, prompt/messages, and output token limits.
65
+
66
+ ```ts
67
+ import { generateText } from 'ai';
68
+ import { openai } from '@ai-sdk/openai';
69
+ import { guardFunction } from '@salimassili/ai-costguard';
70
+
71
+ const guardedGenerateText = guardFunction(
72
+ (request) => generateText({ model: openai(request.model), prompt: request.prompt }),
73
+ { budget: 1, scope: { projectId: 'chatbot' } }
74
+ );
75
+
76
+ await guardedGenerateText({
77
+ model: 'gpt-4o-mini',
78
+ prompt: 'Answer the user in one paragraph.',
79
+ max_tokens: 200,
80
+ });
81
+ ```
82
+
83
+ Runnable mock: `examples/integrations/vercel-ai-chatbot.mjs`
84
+
85
+ ## LangChain
86
+
87
+ LangChain shapes vary by model wrapper. The most reliable pattern is to guard a small adapter function that normalizes LangChain inputs into an object with model, prompt/messages, and max token fields.
88
+
89
+ ```ts
90
+ import { guardFunction } from '@salimassili/ai-costguard';
91
+
92
+ const invoke = guardFunction(
93
+ (request) => chatModel.invoke(request.prompt),
94
+ {
95
+ budget: 2,
96
+ retryThreshold: 2,
97
+ scope: { projectId: 'retrieval-agent', sessionId: 'run-42' },
98
+ }
99
+ );
100
+
101
+ await invoke({
102
+ model: 'gpt-4o-mini',
103
+ prompt: 'retry failed retrieval after timeout',
104
+ max_tokens: 150,
105
+ });
106
+ ```
107
+
108
+ Runnable mock: `examples/integrations/langchain-retry-storm.mjs`
109
+
110
+ ## Mastra
111
+
112
+ For object-style agent runners, guard the agent object and set `guardedMethods` to the method path you want to protect.
113
+
114
+ ```ts
115
+ import { guard } from '@salimassili/ai-costguard';
116
+
117
+ const app = guard(mastraApp, {
118
+ budget: 10,
119
+ guardedMethods: ['agent.run'],
120
+ scope: { projectId: 'mastra-agent' },
121
+ pricingOverrides: [
122
+ {
123
+ model: 'internal-agent-model',
124
+ inputPer1kTokens: 0.001,
125
+ outputPer1kTokens: 0.002,
126
+ lastUpdated: '2026-06-08',
127
+ source: 'internal pricing',
128
+ },
129
+ ],
130
+ });
131
+ ```
132
+
133
+ Runnable mock: `examples/integrations/mastra-agent.mjs`
134
+
135
+ ## CrewAI
136
+
137
+ CrewAI is Python-native, so this TypeScript package cannot instrument internal Python SDK calls directly. Practical options:
138
+
139
+ - Use `aifw check` / `ai-costguard check` in CI or before launching a CrewAI run.
140
+ - Wrap a Node launcher or API boundary with `guardFunction()` before it starts the Python workflow.
141
+ - Use provider-side billing alerts for final reconciliation.
142
+
143
+ Runnable mock: `examples/integrations/crewai-budget-gate.mjs`
144
+
145
+ ## CI Budget Check
146
+
147
+ Use the CLI to fail a pipeline before a planned agent run can exceed budget.
148
+
149
+ ```bash
150
+ ai-costguard check --budget 0.25 --model gpt-4o-mini --input-tokens 800 --tokens 1200 --max-steps 20
151
+ ```
152
+
153
+ Runnable example: `examples/integrations/ci-budget-check.mjs`
@@ -0,0 +1,36 @@
1
+ import { guard, GuardError } from '@salimassili/ai-costguard';
2
+
3
+ const fakeAnthropic = {
4
+ messages: {
5
+ create: async (request) => ({
6
+ id: 'mock-anthropic-message',
7
+ content: [{ type: 'text', text: 'mocked Anthropic response' }],
8
+ usage: { input_tokens: 120, output_tokens: request.max_tokens ?? 100 },
9
+ }),
10
+ },
11
+ };
12
+
13
+ const anthropic = guard(fakeAnthropic, {
14
+ budget: 0.002,
15
+ scope: { projectId: 'anthropic-workflow-demo', sessionId: 'daily-summary' },
16
+ });
17
+
18
+ await anthropic.messages.create({
19
+ model: 'claude-haiku-4.5',
20
+ max_tokens: 100,
21
+ messages: [{ role: 'user', content: 'Draft a short workflow summary.' }],
22
+ });
23
+
24
+ try {
25
+ await anthropic.messages.create({
26
+ model: 'claude-haiku-4.5',
27
+ max_tokens: 1000,
28
+ messages: [{ role: 'user', content: 'Draft a much longer workflow summary.' }],
29
+ });
30
+ } catch (error) {
31
+ if (error instanceof GuardError) {
32
+ console.log(JSON.stringify({ blocked: true, code: error.code, estimatedCost: error.context.estimatedCost }, null, 2));
33
+ } else {
34
+ throw error;
35
+ }
36
+ }
@@ -0,0 +1,32 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { dirname, join } from 'node:path';
4
+
5
+ const here = dirname(fileURLToPath(import.meta.url));
6
+ const cliPath = join(here, '..', '..', 'dist', 'cli.js');
7
+
8
+ const result = spawnSync(
9
+ process.execPath,
10
+ [
11
+ cliPath,
12
+ 'check',
13
+ '--budget',
14
+ '0.25',
15
+ '--model',
16
+ 'gpt-4o-mini',
17
+ '--input-tokens',
18
+ '800',
19
+ '--tokens',
20
+ '1200',
21
+ '--max-steps',
22
+ '20',
23
+ ],
24
+ { encoding: 'utf8' }
25
+ );
26
+
27
+ if (result.status !== 0) {
28
+ process.stderr.write(result.stderr || result.stdout);
29
+ process.exitCode = result.status ?? 1;
30
+ } else {
31
+ process.stdout.write(result.stdout);
32
+ }
@@ -0,0 +1,31 @@
1
+ import { guardFunction, GuardError } from '@salimassili/ai-costguard';
2
+
3
+ async function mockCrewAiLauncher(request) {
4
+ return {
5
+ command: 'crewai run',
6
+ launched: false,
7
+ reason: 'mock example only; no Python process or paid API call is started',
8
+ request,
9
+ };
10
+ }
11
+
12
+ const runCrewAi = guardFunction(mockCrewAiLauncher, {
13
+ budget: 0.01,
14
+ scope: { projectId: 'crewai-demo', sessionId: 'crew-run-1' },
15
+ });
16
+
17
+ try {
18
+ const result = await runCrewAi({
19
+ model: 'gpt-4o-mini',
20
+ prompt: 'Run a CrewAI research workflow with a strict budget gate.',
21
+ max_tokens: 500,
22
+ });
23
+
24
+ console.log(JSON.stringify({ ok: true, command: result.command, launched: result.launched }, null, 2));
25
+ } catch (error) {
26
+ if (error instanceof GuardError) {
27
+ console.log(JSON.stringify({ ok: false, code: error.code }, null, 2));
28
+ } else {
29
+ throw error;
30
+ }
31
+ }
@@ -0,0 +1,32 @@
1
+ import { guardFunction, GuardError } from '@salimassili/ai-costguard';
2
+
3
+ async function mockLangChainInvoke(request) {
4
+ return {
5
+ content: `mock LangChain response for ${request.prompt}`,
6
+ usage: { prompt_tokens: 32, completion_tokens: request.max_tokens ?? 64 },
7
+ };
8
+ }
9
+
10
+ const invoke = guardFunction(mockLangChainInvoke, {
11
+ budget: 1,
12
+ retryThreshold: 2,
13
+ scope: { projectId: 'langchain-demo', sessionId: 'retriever-run' },
14
+ });
15
+
16
+ const prompts = [
17
+ 'retry failed retrieval after timeout for customer A',
18
+ 'again after 429 error for customer B',
19
+ 'repeat after failed vector search for customer C',
20
+ ];
21
+
22
+ for (const prompt of prompts) {
23
+ try {
24
+ await invoke({ model: 'gpt-4o-mini', prompt, max_tokens: 64 });
25
+ } catch (error) {
26
+ if (error instanceof GuardError) {
27
+ console.log(JSON.stringify({ blocked: true, code: error.code }, null, 2));
28
+ } else {
29
+ throw error;
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,41 @@
1
+ import { guard, GuardError } from '@salimassili/ai-costguard';
2
+
3
+ const fakeMastraAgent = {
4
+ agent: {
5
+ run: async (request) => ({
6
+ output: `mock Mastra agent result for ${request.prompt}`,
7
+ usage: { inputTokens: 50, outputTokens: request.max_tokens ?? 100 },
8
+ }),
9
+ },
10
+ };
11
+
12
+ const guardedAgent = guard(fakeMastraAgent, {
13
+ budget: 0.01,
14
+ guardedMethods: ['agent.run'],
15
+ scope: { projectId: 'mastra-demo', sessionId: 'agent-run-1' },
16
+ pricingOverrides: [
17
+ {
18
+ model: 'mastra-demo-model',
19
+ inputPer1kTokens: 0.001,
20
+ outputPer1kTokens: 0.002,
21
+ lastUpdated: '2026-06-08',
22
+ source: 'example override',
23
+ },
24
+ ],
25
+ });
26
+
27
+ try {
28
+ const result = await guardedAgent.agent.run({
29
+ model: 'mastra-demo-model',
30
+ prompt: 'Plan the next workflow step.',
31
+ max_tokens: 100,
32
+ });
33
+
34
+ console.log(JSON.stringify({ ok: true, output: result.output }, null, 2));
35
+ } catch (error) {
36
+ if (error instanceof GuardError) {
37
+ console.log(JSON.stringify({ ok: false, code: error.code }, null, 2));
38
+ } else {
39
+ throw error;
40
+ }
41
+ }
@@ -0,0 +1,44 @@
1
+ import { guard, GuardError } from '@salimassili/ai-costguard';
2
+
3
+ const fakeOpenAI = {
4
+ chat: {
5
+ completions: {
6
+ create: async (request) => ({
7
+ id: `mock-${request.messages.length}`,
8
+ choices: [{ message: { content: 'mocked OpenAI response' } }],
9
+ usage: { prompt_tokens: 24, completion_tokens: request.max_tokens ?? 16 },
10
+ }),
11
+ },
12
+ },
13
+ };
14
+
15
+ const openai = guard(fakeOpenAI, {
16
+ budget: 1,
17
+ loopSimilarityThreshold: 0.9,
18
+ loopMinRepeats: 2,
19
+ scope: { projectId: 'openai-agent-demo', sessionId: 'run-1' },
20
+ });
21
+
22
+ let blocked = false;
23
+
24
+ for (let step = 1; step <= 3; step++) {
25
+ try {
26
+ await openai.chat.completions.create({
27
+ model: 'gpt-4o-mini',
28
+ messages: [{ role: 'user', content: 'inspect the same failing tool result and try to continue' }],
29
+ max_tokens: 64,
30
+ });
31
+ } catch (error) {
32
+ if (error instanceof GuardError) {
33
+ blocked = true;
34
+ console.log(JSON.stringify({ blocked, step, code: error.code }, null, 2));
35
+ break;
36
+ }
37
+
38
+ throw error;
39
+ }
40
+ }
41
+
42
+ if (!blocked) {
43
+ throw new Error('Expected loop protection to block the repeated agent prompt.');
44
+ }
@@ -0,0 +1,29 @@
1
+ import { guardFunction, GuardError } from '@salimassili/ai-costguard';
2
+
3
+ async function mockGenerateText(request) {
4
+ return {
5
+ text: `mocked answer for ${request.prompt}`,
6
+ usage: { prompt_tokens: 40, completion_tokens: request.max_tokens ?? 80 },
7
+ };
8
+ }
9
+
10
+ const generateText = guardFunction(mockGenerateText, {
11
+ budget: 0.001,
12
+ scope: { projectId: 'vercel-chatbot-demo', sessionId: 'chat-123' },
13
+ });
14
+
15
+ try {
16
+ const result = await generateText({
17
+ model: 'gpt-4o-mini',
18
+ prompt: 'Answer the user in one paragraph.',
19
+ max_tokens: 80,
20
+ });
21
+
22
+ console.log(JSON.stringify({ ok: true, text: result.text }, null, 2));
23
+ } catch (error) {
24
+ if (error instanceof GuardError) {
25
+ console.log(JSON.stringify({ ok: false, code: error.code }, null, 2));
26
+ } else {
27
+ throw error;
28
+ }
29
+ }
package/package.json CHANGED
@@ -1,27 +1,53 @@
1
1
  {
2
2
  "name": "@salimassili/ai-costguard",
3
- "version": "1.2.0",
4
- "description": "AI Agent Loop & Cost Firewall. Detects infinite loops in autonomous AI agents and kills them before they burn your API budget.",
3
+ "version": "2.0.0",
4
+ "description": "Local-first runtime safety layer for AI agents that blocks runaway costs, loops, retries, and budget overruns before API calls execute.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
8
8
  "types": "./dist/index.d.ts",
9
+ "sideEffects": false,
10
+ "bin": {
11
+ "aifw": "./dist/cli.js",
12
+ "ai-costguard": "./dist/cli.js"
13
+ },
9
14
  "exports": {
10
15
  ".": {
11
16
  "types": "./dist/index.d.ts",
12
17
  "import": "./dist/index.js",
13
18
  "default": "./dist/index.js"
19
+ },
20
+ "./pro": {
21
+ "types": "./dist/pro.d.ts",
22
+ "import": "./dist/pro.js",
23
+ "default": "./dist/pro.js"
24
+ },
25
+ "./pricing": {
26
+ "types": "./dist/pricing/index.d.ts",
27
+ "import": "./dist/pricing/index.js",
28
+ "default": "./dist/pricing/index.js"
14
29
  }
15
30
  },
16
31
  "files": [
17
32
  "dist",
18
- "README.md"
33
+ "docs/DASHBOARD.md",
34
+ "docs/INTEGRATIONS.md",
35
+ "docs/BENCHMARKS.md",
36
+ "examples/integrations",
37
+ "benchmarks/run.mjs",
38
+ "README.md",
39
+ "CHANGELOG.md",
40
+ "LICENSE"
19
41
  ],
20
42
  "scripts": {
21
43
  "clean": "rimraf dist",
22
44
  "build": "npm run clean && tsc",
23
- "test": "node --experimental-vm-modules test/smoke.mjs",
24
- "prepublishOnly": "npm run build"
45
+ "typecheck": "tsc --noEmit",
46
+ "test": "npm run build && node --test --experimental-test-coverage --test-coverage-include=dist/**/*.js --test-coverage-lines=80 --test-coverage-functions=80 --test-coverage-branches=70 \"test/**/*.test.mjs\"",
47
+ "smoke": "node test/smoke-examples.mjs",
48
+ "benchmark": "node benchmarks/run.mjs",
49
+ "preflight": "node scripts/preflight.js",
50
+ "prepublishOnly": "npm run preflight"
25
51
  },
26
52
  "keywords": [
27
53
  "ai",
@@ -29,7 +55,9 @@
29
55
  "llm",
30
56
  "cost-control",
31
57
  "agent",
32
- "firewall"
58
+ "runtime-safety",
59
+ "ai-agents",
60
+ "budget-guard"
33
61
  ],
34
62
  "author": "Salim Assili",
35
63
  "license": "MIT",
@@ -40,7 +68,7 @@
40
68
  "rimraf": "^5.0.10",
41
69
  "typescript": "^5.3.3"
42
70
  },
43
- "dependencies": {
71
+ "optionalDependencies": {
44
72
  "ioredis": "^5.10.1"
45
73
  }
46
74
  }