@granular-software/sdk 0.4.37 → 0.4.39

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.
@@ -0,0 +1,2 @@
1
+ export { G as GranularSpendContext, l as OpenAIUsageSpendEvent, m as RecordOpenAIUsageSpendOptions, o as RecordOpenAIUsageSpendResult, p as buildOpenAISpendEventId, r as recordOpenAIUsageSpend, t as toGranularHttpBase } from './spend-tAz2a16I.mjs';
2
+ import '@granular-software/policy-engine';
@@ -0,0 +1,2 @@
1
+ export { G as GranularSpendContext, l as OpenAIUsageSpendEvent, m as RecordOpenAIUsageSpendOptions, o as RecordOpenAIUsageSpendResult, p as buildOpenAISpendEventId, r as recordOpenAIUsageSpend, t as toGranularHttpBase } from './spend-tAz2a16I.js';
2
+ import '@granular-software/policy-engine';
package/dist/spend.js ADDED
@@ -0,0 +1,111 @@
1
+ 'use strict';
2
+
3
+ // src/spend.ts
4
+ function toGranularHttpBase(apiUrl) {
5
+ const url = new URL(apiUrl);
6
+ if (url.protocol === "ws:") {
7
+ url.protocol = "http:";
8
+ } else if (url.protocol === "wss:") {
9
+ url.protocol = "https:";
10
+ }
11
+ url.pathname = url.pathname.replace(/\/ws\/connect$/, "").replace(/\/ws$/, "");
12
+ if (!url.pathname || url.pathname === "/") {
13
+ url.pathname = "/granular";
14
+ }
15
+ url.search = "";
16
+ url.hash = "";
17
+ return url.toString().replace(/\/$/, "");
18
+ }
19
+ function cleanIdPart(value) {
20
+ return value.replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
21
+ }
22
+ function buildOpenAISpendEventId(usage, context = {}) {
23
+ const requestId = usage.requestId?.trim();
24
+ if (!requestId) return void 0;
25
+ const scope = context.sessionId || context.environmentId || context.subjectId || context.sandboxId || "global";
26
+ return ["spend", "openai", scope, requestId].map(cleanIdPart).join("_");
27
+ }
28
+ function pricingEffectiveAtSeconds(value) {
29
+ if (!value) return null;
30
+ const parsed = Date.parse(value);
31
+ return Number.isFinite(parsed) ? Math.floor(parsed / 1e3) : null;
32
+ }
33
+ function compactContext(context) {
34
+ return Object.fromEntries(
35
+ Object.entries(context).filter(
36
+ ([, value]) => value != null && value !== ""
37
+ )
38
+ );
39
+ }
40
+ function omitTenantId(context) {
41
+ const scopedContext = { ...context };
42
+ delete scopedContext.tenantId;
43
+ return scopedContext;
44
+ }
45
+ async function recordOpenAIUsageSpend(options) {
46
+ const usageContext = compactContext({
47
+ ...options.usage.usageContext || {},
48
+ ...options.context || {}
49
+ });
50
+ const context = omitTenantId(usageContext);
51
+ const spendEventId = options.usage.spendEventId || buildOpenAISpendEventId(options.usage, context);
52
+ const metadata = {
53
+ ...options.metadata || {},
54
+ ...options.usage.rawUsage !== void 0 ? { openaiUsage: options.usage.rawUsage } : {},
55
+ usageContext: context
56
+ };
57
+ const response = await fetch(
58
+ `${toGranularHttpBase(options.apiUrl)}/control/spend/events`,
59
+ {
60
+ method: "POST",
61
+ cache: "no-store",
62
+ headers: {
63
+ Authorization: `Bearer ${options.token}`,
64
+ "Content-Type": "application/json"
65
+ },
66
+ body: JSON.stringify({
67
+ ...spendEventId ? { spendEventId } : {},
68
+ sandboxId: context.sandboxId || null,
69
+ environmentId: context.environmentId || null,
70
+ sessionId: context.sessionId || null,
71
+ subjectId: context.subjectId || null,
72
+ permissionProfileId: context.permissionProfileId || null,
73
+ source: "openai",
74
+ lineItemType: "llm_tokens",
75
+ provider: options.usage.provider,
76
+ model: options.usage.model,
77
+ operation: options.usage.operation || "chat.completions",
78
+ requestId: options.usage.requestId || null,
79
+ inputTokens: options.usage.inputTokens,
80
+ outputTokens: options.usage.outputTokens,
81
+ cachedInputTokens: options.usage.cachedInputTokens,
82
+ reasoningTokens: options.usage.reasoningTokens,
83
+ quantity: options.usage.totalTokens,
84
+ quantityUnit: "tokens",
85
+ inputPricePerMillionMicros: options.usage.inputPricePerMillionMicros,
86
+ cachedInputPricePerMillionMicros: options.usage.cachedInputPricePerMillionMicros,
87
+ outputPricePerMillionMicros: options.usage.outputPricePerMillionMicros,
88
+ amountMicros: options.usage.amountMicros,
89
+ currency: options.usage.currency,
90
+ pricingSource: options.usage.pricingSource,
91
+ pricingEffectiveAt: pricingEffectiveAtSeconds(
92
+ options.usage.pricingEffectiveAt
93
+ ),
94
+ estimated: false,
95
+ metadata
96
+ })
97
+ }
98
+ );
99
+ if (!response.ok) {
100
+ throw new Error(
101
+ `Granular spend event failed (${response.status}): ${await response.text()}`
102
+ );
103
+ }
104
+ return response.json();
105
+ }
106
+
107
+ exports.buildOpenAISpendEventId = buildOpenAISpendEventId;
108
+ exports.recordOpenAIUsageSpend = recordOpenAIUsageSpend;
109
+ exports.toGranularHttpBase = toGranularHttpBase;
110
+ //# sourceMappingURL=spend.js.map
111
+ //# sourceMappingURL=spend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/spend.ts"],"names":[],"mappings":"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","file":"spend.js","sourcesContent":["import type { OpenAITokenSpend } from \"./openai-usage\";\nimport type { GranularQuotaProgress } from \"./types\";\n\nexport interface GranularSpendContext {\n tenantId?: string | null;\n sandboxId?: string | null;\n environmentId?: string | null;\n sessionId?: string | null;\n subjectId?: string | null;\n permissionProfileId?: string | null;\n [key: string]: string | null | undefined;\n}\n\nexport interface OpenAIUsageSpendEvent extends OpenAITokenSpend {\n spendEventId?: string;\n source?: \"openai\";\n lineItemType?: \"llm_tokens\";\n operation?: string;\n requestId?: string | null;\n usageContext?: GranularSpendContext;\n rawUsage?: unknown;\n}\n\nexport interface RecordOpenAIUsageSpendOptions {\n apiUrl: string;\n token: string;\n usage: OpenAIUsageSpendEvent;\n context?: GranularSpendContext | null;\n metadata?: Record<string, unknown> | null;\n}\n\nexport interface RecordOpenAIUsageSpendResult {\n spendEventId: string;\n inserted: boolean;\n quota: GranularQuotaProgress | null;\n}\n\nexport function toGranularHttpBase(apiUrl: string): string {\n const url = new URL(apiUrl);\n if (url.protocol === \"ws:\") {\n url.protocol = \"http:\";\n } else if (url.protocol === \"wss:\") {\n url.protocol = \"https:\";\n }\n\n url.pathname = url.pathname\n .replace(/\\/ws\\/connect$/, \"\")\n .replace(/\\/ws$/, \"\");\n if (!url.pathname || url.pathname === \"/\") {\n url.pathname = \"/granular\";\n }\n url.search = \"\";\n url.hash = \"\";\n return url.toString().replace(/\\/$/, \"\");\n}\n\nfunction cleanIdPart(value: string): string {\n return value.replace(/[^a-zA-Z0-9_-]+/g, \"_\").replace(/^_+|_+$/g, \"\");\n}\n\nexport function buildOpenAISpendEventId(\n usage: Pick<OpenAIUsageSpendEvent, \"requestId\">,\n context: GranularSpendContext = {},\n): string | undefined {\n const requestId = usage.requestId?.trim();\n if (!requestId) return undefined;\n\n const scope =\n context.sessionId ||\n context.environmentId ||\n context.subjectId ||\n context.sandboxId ||\n \"global\";\n return [\"spend\", \"openai\", scope, requestId].map(cleanIdPart).join(\"_\");\n}\n\nfunction pricingEffectiveAtSeconds(value: string | null | undefined) {\n if (!value) return null;\n const parsed = Date.parse(value);\n return Number.isFinite(parsed) ? Math.floor(parsed / 1000) : null;\n}\n\nfunction compactContext(context: GranularSpendContext): GranularSpendContext {\n return Object.fromEntries(\n Object.entries(context).filter(\n ([, value]) => value != null && value !== \"\",\n ),\n ) as GranularSpendContext;\n}\n\nfunction omitTenantId(context: GranularSpendContext): GranularSpendContext {\n const scopedContext = { ...context };\n delete scopedContext.tenantId;\n return scopedContext;\n}\n\nexport async function recordOpenAIUsageSpend(\n options: RecordOpenAIUsageSpendOptions,\n): Promise<RecordOpenAIUsageSpendResult> {\n const usageContext = compactContext({\n ...(options.usage.usageContext || {}),\n ...(options.context || {}),\n });\n const context = omitTenantId(usageContext);\n const spendEventId =\n options.usage.spendEventId ||\n buildOpenAISpendEventId(options.usage, context);\n const metadata = {\n ...(options.metadata || {}),\n ...(options.usage.rawUsage !== undefined\n ? { openaiUsage: options.usage.rawUsage }\n : {}),\n usageContext: context,\n };\n\n const response = await fetch(\n `${toGranularHttpBase(options.apiUrl)}/control/spend/events`,\n {\n method: \"POST\",\n cache: \"no-store\",\n headers: {\n Authorization: `Bearer ${options.token}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n ...(spendEventId ? { spendEventId } : {}),\n sandboxId: context.sandboxId || null,\n environmentId: context.environmentId || null,\n sessionId: context.sessionId || null,\n subjectId: context.subjectId || null,\n permissionProfileId: context.permissionProfileId || null,\n source: \"openai\",\n lineItemType: \"llm_tokens\",\n provider: options.usage.provider,\n model: options.usage.model,\n operation: options.usage.operation || \"chat.completions\",\n requestId: options.usage.requestId || null,\n inputTokens: options.usage.inputTokens,\n outputTokens: options.usage.outputTokens,\n cachedInputTokens: options.usage.cachedInputTokens,\n reasoningTokens: options.usage.reasoningTokens,\n quantity: options.usage.totalTokens,\n quantityUnit: \"tokens\",\n inputPricePerMillionMicros: options.usage.inputPricePerMillionMicros,\n cachedInputPricePerMillionMicros:\n options.usage.cachedInputPricePerMillionMicros,\n outputPricePerMillionMicros: options.usage.outputPricePerMillionMicros,\n amountMicros: options.usage.amountMicros,\n currency: options.usage.currency,\n pricingSource: options.usage.pricingSource,\n pricingEffectiveAt: pricingEffectiveAtSeconds(\n options.usage.pricingEffectiveAt,\n ),\n estimated: false,\n metadata,\n }),\n },\n );\n\n if (!response.ok) {\n throw new Error(\n `Granular spend event failed (${response.status}): ${await response.text()}`,\n );\n }\n\n return response.json() as Promise<RecordOpenAIUsageSpendResult>;\n}\n"]}
package/dist/spend.mjs ADDED
@@ -0,0 +1,107 @@
1
+ // src/spend.ts
2
+ function toGranularHttpBase(apiUrl) {
3
+ const url = new URL(apiUrl);
4
+ if (url.protocol === "ws:") {
5
+ url.protocol = "http:";
6
+ } else if (url.protocol === "wss:") {
7
+ url.protocol = "https:";
8
+ }
9
+ url.pathname = url.pathname.replace(/\/ws\/connect$/, "").replace(/\/ws$/, "");
10
+ if (!url.pathname || url.pathname === "/") {
11
+ url.pathname = "/granular";
12
+ }
13
+ url.search = "";
14
+ url.hash = "";
15
+ return url.toString().replace(/\/$/, "");
16
+ }
17
+ function cleanIdPart(value) {
18
+ return value.replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
19
+ }
20
+ function buildOpenAISpendEventId(usage, context = {}) {
21
+ const requestId = usage.requestId?.trim();
22
+ if (!requestId) return void 0;
23
+ const scope = context.sessionId || context.environmentId || context.subjectId || context.sandboxId || "global";
24
+ return ["spend", "openai", scope, requestId].map(cleanIdPart).join("_");
25
+ }
26
+ function pricingEffectiveAtSeconds(value) {
27
+ if (!value) return null;
28
+ const parsed = Date.parse(value);
29
+ return Number.isFinite(parsed) ? Math.floor(parsed / 1e3) : null;
30
+ }
31
+ function compactContext(context) {
32
+ return Object.fromEntries(
33
+ Object.entries(context).filter(
34
+ ([, value]) => value != null && value !== ""
35
+ )
36
+ );
37
+ }
38
+ function omitTenantId(context) {
39
+ const scopedContext = { ...context };
40
+ delete scopedContext.tenantId;
41
+ return scopedContext;
42
+ }
43
+ async function recordOpenAIUsageSpend(options) {
44
+ const usageContext = compactContext({
45
+ ...options.usage.usageContext || {},
46
+ ...options.context || {}
47
+ });
48
+ const context = omitTenantId(usageContext);
49
+ const spendEventId = options.usage.spendEventId || buildOpenAISpendEventId(options.usage, context);
50
+ const metadata = {
51
+ ...options.metadata || {},
52
+ ...options.usage.rawUsage !== void 0 ? { openaiUsage: options.usage.rawUsage } : {},
53
+ usageContext: context
54
+ };
55
+ const response = await fetch(
56
+ `${toGranularHttpBase(options.apiUrl)}/control/spend/events`,
57
+ {
58
+ method: "POST",
59
+ cache: "no-store",
60
+ headers: {
61
+ Authorization: `Bearer ${options.token}`,
62
+ "Content-Type": "application/json"
63
+ },
64
+ body: JSON.stringify({
65
+ ...spendEventId ? { spendEventId } : {},
66
+ sandboxId: context.sandboxId || null,
67
+ environmentId: context.environmentId || null,
68
+ sessionId: context.sessionId || null,
69
+ subjectId: context.subjectId || null,
70
+ permissionProfileId: context.permissionProfileId || null,
71
+ source: "openai",
72
+ lineItemType: "llm_tokens",
73
+ provider: options.usage.provider,
74
+ model: options.usage.model,
75
+ operation: options.usage.operation || "chat.completions",
76
+ requestId: options.usage.requestId || null,
77
+ inputTokens: options.usage.inputTokens,
78
+ outputTokens: options.usage.outputTokens,
79
+ cachedInputTokens: options.usage.cachedInputTokens,
80
+ reasoningTokens: options.usage.reasoningTokens,
81
+ quantity: options.usage.totalTokens,
82
+ quantityUnit: "tokens",
83
+ inputPricePerMillionMicros: options.usage.inputPricePerMillionMicros,
84
+ cachedInputPricePerMillionMicros: options.usage.cachedInputPricePerMillionMicros,
85
+ outputPricePerMillionMicros: options.usage.outputPricePerMillionMicros,
86
+ amountMicros: options.usage.amountMicros,
87
+ currency: options.usage.currency,
88
+ pricingSource: options.usage.pricingSource,
89
+ pricingEffectiveAt: pricingEffectiveAtSeconds(
90
+ options.usage.pricingEffectiveAt
91
+ ),
92
+ estimated: false,
93
+ metadata
94
+ })
95
+ }
96
+ );
97
+ if (!response.ok) {
98
+ throw new Error(
99
+ `Granular spend event failed (${response.status}): ${await response.text()}`
100
+ );
101
+ }
102
+ return response.json();
103
+ }
104
+
105
+ export { buildOpenAISpendEventId, recordOpenAIUsageSpend, toGranularHttpBase };
106
+ //# sourceMappingURL=spend.mjs.map
107
+ //# sourceMappingURL=spend.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/spend.ts"],"names":[],"mappings":"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","file":"spend.mjs","sourcesContent":["import type { OpenAITokenSpend } from \"./openai-usage\";\nimport type { GranularQuotaProgress } from \"./types\";\n\nexport interface GranularSpendContext {\n tenantId?: string | null;\n sandboxId?: string | null;\n environmentId?: string | null;\n sessionId?: string | null;\n subjectId?: string | null;\n permissionProfileId?: string | null;\n [key: string]: string | null | undefined;\n}\n\nexport interface OpenAIUsageSpendEvent extends OpenAITokenSpend {\n spendEventId?: string;\n source?: \"openai\";\n lineItemType?: \"llm_tokens\";\n operation?: string;\n requestId?: string | null;\n usageContext?: GranularSpendContext;\n rawUsage?: unknown;\n}\n\nexport interface RecordOpenAIUsageSpendOptions {\n apiUrl: string;\n token: string;\n usage: OpenAIUsageSpendEvent;\n context?: GranularSpendContext | null;\n metadata?: Record<string, unknown> | null;\n}\n\nexport interface RecordOpenAIUsageSpendResult {\n spendEventId: string;\n inserted: boolean;\n quota: GranularQuotaProgress | null;\n}\n\nexport function toGranularHttpBase(apiUrl: string): string {\n const url = new URL(apiUrl);\n if (url.protocol === \"ws:\") {\n url.protocol = \"http:\";\n } else if (url.protocol === \"wss:\") {\n url.protocol = \"https:\";\n }\n\n url.pathname = url.pathname\n .replace(/\\/ws\\/connect$/, \"\")\n .replace(/\\/ws$/, \"\");\n if (!url.pathname || url.pathname === \"/\") {\n url.pathname = \"/granular\";\n }\n url.search = \"\";\n url.hash = \"\";\n return url.toString().replace(/\\/$/, \"\");\n}\n\nfunction cleanIdPart(value: string): string {\n return value.replace(/[^a-zA-Z0-9_-]+/g, \"_\").replace(/^_+|_+$/g, \"\");\n}\n\nexport function buildOpenAISpendEventId(\n usage: Pick<OpenAIUsageSpendEvent, \"requestId\">,\n context: GranularSpendContext = {},\n): string | undefined {\n const requestId = usage.requestId?.trim();\n if (!requestId) return undefined;\n\n const scope =\n context.sessionId ||\n context.environmentId ||\n context.subjectId ||\n context.sandboxId ||\n \"global\";\n return [\"spend\", \"openai\", scope, requestId].map(cleanIdPart).join(\"_\");\n}\n\nfunction pricingEffectiveAtSeconds(value: string | null | undefined) {\n if (!value) return null;\n const parsed = Date.parse(value);\n return Number.isFinite(parsed) ? Math.floor(parsed / 1000) : null;\n}\n\nfunction compactContext(context: GranularSpendContext): GranularSpendContext {\n return Object.fromEntries(\n Object.entries(context).filter(\n ([, value]) => value != null && value !== \"\",\n ),\n ) as GranularSpendContext;\n}\n\nfunction omitTenantId(context: GranularSpendContext): GranularSpendContext {\n const scopedContext = { ...context };\n delete scopedContext.tenantId;\n return scopedContext;\n}\n\nexport async function recordOpenAIUsageSpend(\n options: RecordOpenAIUsageSpendOptions,\n): Promise<RecordOpenAIUsageSpendResult> {\n const usageContext = compactContext({\n ...(options.usage.usageContext || {}),\n ...(options.context || {}),\n });\n const context = omitTenantId(usageContext);\n const spendEventId =\n options.usage.spendEventId ||\n buildOpenAISpendEventId(options.usage, context);\n const metadata = {\n ...(options.metadata || {}),\n ...(options.usage.rawUsage !== undefined\n ? { openaiUsage: options.usage.rawUsage }\n : {}),\n usageContext: context,\n };\n\n const response = await fetch(\n `${toGranularHttpBase(options.apiUrl)}/control/spend/events`,\n {\n method: \"POST\",\n cache: \"no-store\",\n headers: {\n Authorization: `Bearer ${options.token}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n ...(spendEventId ? { spendEventId } : {}),\n sandboxId: context.sandboxId || null,\n environmentId: context.environmentId || null,\n sessionId: context.sessionId || null,\n subjectId: context.subjectId || null,\n permissionProfileId: context.permissionProfileId || null,\n source: \"openai\",\n lineItemType: \"llm_tokens\",\n provider: options.usage.provider,\n model: options.usage.model,\n operation: options.usage.operation || \"chat.completions\",\n requestId: options.usage.requestId || null,\n inputTokens: options.usage.inputTokens,\n outputTokens: options.usage.outputTokens,\n cachedInputTokens: options.usage.cachedInputTokens,\n reasoningTokens: options.usage.reasoningTokens,\n quantity: options.usage.totalTokens,\n quantityUnit: \"tokens\",\n inputPricePerMillionMicros: options.usage.inputPricePerMillionMicros,\n cachedInputPricePerMillionMicros:\n options.usage.cachedInputPricePerMillionMicros,\n outputPricePerMillionMicros: options.usage.outputPricePerMillionMicros,\n amountMicros: options.usage.amountMicros,\n currency: options.usage.currency,\n pricingSource: options.usage.pricingSource,\n pricingEffectiveAt: pricingEffectiveAtSeconds(\n options.usage.pricingEffectiveAt,\n ),\n estimated: false,\n metadata,\n }),\n },\n );\n\n if (!response.ok) {\n throw new Error(\n `Granular spend event failed (${response.status}): ${await response.text()}`,\n );\n }\n\n return response.json() as Promise<RecordOpenAIUsageSpendResult>;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@granular-software/sdk",
3
- "version": "0.4.37",
3
+ "version": "0.4.39",
4
4
  "description": "TypeScript SDK and CLI for Granular - define, build, and deploy AI sandboxes",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -20,6 +20,11 @@
20
20
  "types": "./dist/agent-evals.d.ts",
21
21
  "import": "./dist/agent-evals.mjs",
22
22
  "require": "./dist/agent-evals.js"
23
+ },
24
+ "./spend": {
25
+ "types": "./dist/spend.d.ts",
26
+ "import": "./dist/spend.mjs",
27
+ "require": "./dist/spend.js"
23
28
  }
24
29
  },
25
30
  "bin": {
@@ -64,6 +69,7 @@
64
69
  "chokidar": "^4.0.3",
65
70
  "commander": "^13.1.0",
66
71
  "dotenv": "^16.4.7",
72
+ "openai": "^6.38.0",
67
73
  "ora": "^8.2.0",
68
74
  "zod-to-json-schema": "^3.25.1"
69
75
  },