@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.
- package/dist/agent-evals.d.mts +17 -3
- package/dist/agent-evals.d.ts +17 -3
- package/dist/agent-evals.js +1067 -153
- package/dist/agent-evals.js.map +1 -1
- package/dist/agent-evals.mjs +1066 -154
- package/dist/agent-evals.mjs.map +1 -1
- package/dist/agent-harness.d.mts +9 -2
- package/dist/agent-harness.d.ts +9 -2
- package/dist/agent-harness.js +273 -5
- package/dist/agent-harness.js.map +1 -1
- package/dist/agent-harness.mjs +271 -6
- package/dist/agent-harness.mjs.map +1 -1
- package/dist/cli/index.js +293 -34
- package/dist/client-BZ8NuQ_e.d.ts +1080 -0
- package/dist/client-CBQFvuKf.d.mts +1080 -0
- package/dist/index.d.mts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +668 -39
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +659 -40
- package/dist/index.mjs.map +1 -1
- package/dist/spend-tAz2a16I.d.mts +1593 -0
- package/dist/spend-tAz2a16I.d.ts +1593 -0
- package/dist/spend.d.mts +2 -0
- package/dist/spend.d.ts +2 -0
- package/dist/spend.js +111 -0
- package/dist/spend.js.map +1 -0
- package/dist/spend.mjs +107 -0
- package/dist/spend.mjs.map +1 -0
- package/package.json +7 -1
- package/dist/client-eE9nTfvp.d.mts +0 -2483
- package/dist/client-eE9nTfvp.d.ts +0 -2483
package/dist/spend.d.mts
ADDED
|
@@ -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';
|
package/dist/spend.d.ts
ADDED
|
@@ -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.
|
|
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
|
},
|