@coldiq/mcp 0.3.11 → 0.3.12
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/client.d.ts.map +1 -1
- package/dist/client.js +4 -0
- package/dist/client.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/session-usage.d.ts +59 -0
- package/dist/session-usage.d.ts.map +1 -0
- package/dist/session-usage.js +96 -0
- package/dist/session-usage.js.map +1 -0
- package/dist/tools/get-session-usage.d.ts +10 -0
- package/dist/tools/get-session-usage.d.ts.map +1 -0
- package/dist/tools/get-session-usage.js +14 -0
- package/dist/tools/get-session-usage.js.map +1 -0
- package/package.json +1 -1
- package/src/client.ts +4 -0
- package/src/index.ts +8 -0
- package/src/session-usage.ts +130 -0
- package/src/tools/get-session-usage.ts +18 -0
- package/tests/executor.test.ts +69 -0
- package/tests/session-usage.test.ts +130 -0
- package/tests/tools/get-session-usage.test.ts +44 -0
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,OAAO,CAAA;IACb,oFAAoF;IACpF,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC,CAAA;AAMD,wBAAgB,UAAU,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAqBjE;AAED,wBAAsB,OAAO,CAC3B,MAAM,EAAE,KAAK,GAAG,MAAM,EACtB,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,OAAO,EACd,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACnC,OAAO,CAAC,WAAW,CAAC,CA6DtB"}
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Agent, setGlobalDispatcher } from 'undici';
|
|
2
|
+
import { recordChargeFromHeaders } from './session-usage.js';
|
|
2
3
|
let _apiUrl;
|
|
3
4
|
let _apiKey;
|
|
4
5
|
let _httpTimeoutMs;
|
|
@@ -54,6 +55,9 @@ export async function callApi(method, path, body, queryParams) {
|
|
|
54
55
|
res.headers.forEach((value, key) => {
|
|
55
56
|
headers[key.toLowerCase()] = value;
|
|
56
57
|
});
|
|
58
|
+
// Tally this call's credit charge for the per-session usage report. No-op
|
|
59
|
+
// unless the response carries a charge header, so polls/errors are skipped.
|
|
60
|
+
recordChargeFromHeaders(path, headers);
|
|
57
61
|
return { ok: res.ok, status: res.status, data, headers };
|
|
58
62
|
}
|
|
59
63
|
catch (err) {
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAA;
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import { searchSeoName, searchSeoDescription, searchSeoSchema, searchSeoHandler,
|
|
|
20
20
|
import { findSignalsName, findSignalsDescription, findSignalsSchema, findSignalsHandler, } from './tools/find-signals.js';
|
|
21
21
|
import { fetchPageContentName, fetchPageContentDescription, fetchPageContentSchema, fetchPageContentHandler, } from './tools/fetch-page-content.js';
|
|
22
22
|
import { getCreditBalanceName, getCreditBalanceDescription, getCreditBalanceSchema, getCreditBalanceHandler, } from './tools/get-credit-balance.js';
|
|
23
|
+
import { getSessionUsageName, getSessionUsageDescription, getSessionUsageSchema, getSessionUsageHandler, } from './tools/get-session-usage.js';
|
|
23
24
|
import { extractPostEngagementName, extractPostEngagementDescription, extractPostEngagementSchema, extractPostEngagementHandler, } from './tools/extract-post-engagement.js';
|
|
24
25
|
import { getPlaceReviewsName, getPlaceReviewsDescription, getPlaceReviewsSchema, getPlaceReviewsHandler, } from './tools/get-place-reviews.js';
|
|
25
26
|
import { listDataSourcesName, listDataSourcesDescription, listDataSourcesSchema, listDataSourcesHandler, } from './tools/list-data-sources.js';
|
|
@@ -59,6 +60,7 @@ server.tool(searchSeoName, searchSeoDescription, searchSeoSchema, searchSeoHandl
|
|
|
59
60
|
server.tool(findSignalsName, findSignalsDescription, findSignalsSchema, findSignalsHandler);
|
|
60
61
|
server.tool(fetchPageContentName, fetchPageContentDescription, fetchPageContentSchema, fetchPageContentHandler);
|
|
61
62
|
server.tool(getCreditBalanceName, getCreditBalanceDescription, getCreditBalanceSchema, getCreditBalanceHandler);
|
|
63
|
+
server.tool(getSessionUsageName, getSessionUsageDescription, getSessionUsageSchema, getSessionUsageHandler);
|
|
62
64
|
server.tool(listDataSourcesName, listDataSourcesDescription, listDataSourcesSchema, listDataSourcesHandler);
|
|
63
65
|
server.tool(extractPostEngagementName, extractPostEngagementDescription, extractPostEngagementSchema, extractPostEngagementHandler);
|
|
64
66
|
server.tool(getPlaceReviewsName, getPlaceReviewsDescription, getPlaceReviewsSchema, getPlaceReviewsHandler);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process ColdIQ credit accumulator for one MCP session ("campaign").
|
|
3
|
+
*
|
|
4
|
+
* The ColdIQ MCP server runs as a stdio subprocess spawned per client
|
|
5
|
+
* conversation, so a module-level singleton here is naturally scoped to a
|
|
6
|
+
* single campaign: it starts empty when the process boots and is discarded
|
|
7
|
+
* when the conversation's server exits. We tally the per-call charges the API
|
|
8
|
+
* already reports via the `X-ColdIQ-Credits-Charged` / `-Remaining` response
|
|
9
|
+
* headers (the same signal the executor surfaces in `_meta`), so reporting
|
|
10
|
+
* session spend never issues an extra billable call.
|
|
11
|
+
*
|
|
12
|
+
* Caveats this module deliberately accepts (and the tool surfaces to the user):
|
|
13
|
+
* - It only counts calls made during the current process lifetime; resuming or
|
|
14
|
+
* restarting the conversation starts a fresh tally.
|
|
15
|
+
* - `charged` reflects what the API billed at call time. For async jobs that is
|
|
16
|
+
* the submit-time reservation; later webhook settlement is not visible here.
|
|
17
|
+
*/
|
|
18
|
+
export interface EndpointUsage {
|
|
19
|
+
/** Number of billed calls to this endpoint this session. */
|
|
20
|
+
calls: number;
|
|
21
|
+
/** Total net ColdIQ credits charged across those calls. */
|
|
22
|
+
credits: number;
|
|
23
|
+
}
|
|
24
|
+
export interface SessionUsageSnapshot {
|
|
25
|
+
/** ISO 8601 timestamp of when this MCP session (process) started. */
|
|
26
|
+
startedAt: string;
|
|
27
|
+
/** Milliseconds elapsed since the session started. */
|
|
28
|
+
durationMs: number;
|
|
29
|
+
/** Count of calls that charged credits this session. */
|
|
30
|
+
billedCalls: number;
|
|
31
|
+
/** Sum of net credits charged across all billed calls this session. */
|
|
32
|
+
totalCreditsCharged: number;
|
|
33
|
+
/** Balance the API reported after the most recent billed call, if any. */
|
|
34
|
+
balanceAfterLastCharge?: number;
|
|
35
|
+
/** Per-endpoint breakdown, highest spend first. */
|
|
36
|
+
byEndpoint: Array<{
|
|
37
|
+
endpoint: string;
|
|
38
|
+
calls: number;
|
|
39
|
+
credits: number;
|
|
40
|
+
}>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Record a single billed call. `charged` is the net credits for this call;
|
|
44
|
+
* `remaining`, when present, is the authoritative balance afterwards.
|
|
45
|
+
* Non-finite or negative charges are ignored so a malformed header can't
|
|
46
|
+
* corrupt the running total.
|
|
47
|
+
*/
|
|
48
|
+
export declare function recordCharge(endpoint: string, charged: number, remaining?: number): void;
|
|
49
|
+
/**
|
|
50
|
+
* Record a charge from a response's (lowercased) header map. A no-op when the
|
|
51
|
+
* response carries no charge header — polls, errors, and unbilled routes are
|
|
52
|
+
* silently skipped, so this is safe to call after every request.
|
|
53
|
+
*/
|
|
54
|
+
export declare function recordChargeFromHeaders(endpoint: string, headers: Record<string, string>): void;
|
|
55
|
+
/** Build a snapshot of session spend so far. `now` is injectable for tests. */
|
|
56
|
+
export declare function getSessionUsage(now?: number): SessionUsageSnapshot;
|
|
57
|
+
/** Test-only: reset the accumulator to a known clean state. */
|
|
58
|
+
export declare function resetSessionUsage(startedAt?: number): void;
|
|
59
|
+
//# sourceMappingURL=session-usage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-usage.d.ts","sourceRoot":"","sources":["../src/session-usage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,MAAM,WAAW,aAAa;IAC5B,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAA;IACb,2DAA2D;IAC3D,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,qEAAqE;IACrE,SAAS,EAAE,MAAM,CAAA;IACjB,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAA;IAClB,wDAAwD;IACxD,WAAW,EAAE,MAAM,CAAA;IACnB,uEAAuE;IACvE,mBAAmB,EAAE,MAAM,CAAA;IAC3B,0EAA0E;IAC1E,sBAAsB,CAAC,EAAE,MAAM,CAAA;IAC/B,mDAAmD;IACnD,UAAU,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACxE;AA6BD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAcxF;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAI/F;AAED,+EAA+E;AAC/E,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAmB,GAAG,oBAAoB,CAgB9E;AAED,+DAA+D;AAC/D,wBAAgB,iBAAiB,CAAC,SAAS,GAAE,MAAmB,GAAG,IAAI,CAMtE"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process ColdIQ credit accumulator for one MCP session ("campaign").
|
|
3
|
+
*
|
|
4
|
+
* The ColdIQ MCP server runs as a stdio subprocess spawned per client
|
|
5
|
+
* conversation, so a module-level singleton here is naturally scoped to a
|
|
6
|
+
* single campaign: it starts empty when the process boots and is discarded
|
|
7
|
+
* when the conversation's server exits. We tally the per-call charges the API
|
|
8
|
+
* already reports via the `X-ColdIQ-Credits-Charged` / `-Remaining` response
|
|
9
|
+
* headers (the same signal the executor surfaces in `_meta`), so reporting
|
|
10
|
+
* session spend never issues an extra billable call.
|
|
11
|
+
*
|
|
12
|
+
* Caveats this module deliberately accepts (and the tool surfaces to the user):
|
|
13
|
+
* - It only counts calls made during the current process lifetime; resuming or
|
|
14
|
+
* restarting the conversation starts a fresh tally.
|
|
15
|
+
* - `charged` reflects what the API billed at call time. For async jobs that is
|
|
16
|
+
* the submit-time reservation; later webhook settlement is not visible here.
|
|
17
|
+
*/
|
|
18
|
+
const CHARGED_HEADER = 'x-coldiq-credits-charged';
|
|
19
|
+
const REMAINING_HEADER = 'x-coldiq-credits-remaining';
|
|
20
|
+
const state = {
|
|
21
|
+
startedAt: Date.now(),
|
|
22
|
+
billedCalls: 0,
|
|
23
|
+
totalCreditsCharged: 0,
|
|
24
|
+
byEndpoint: new Map(),
|
|
25
|
+
};
|
|
26
|
+
function parseHeaderNumber(raw) {
|
|
27
|
+
if (raw === undefined)
|
|
28
|
+
return undefined;
|
|
29
|
+
const n = Number(raw);
|
|
30
|
+
return Number.isFinite(n) ? n : undefined;
|
|
31
|
+
}
|
|
32
|
+
// Credits can be fractional (settlement refunds). Round to 4 dp so summed
|
|
33
|
+
// floating-point noise never leaks into the reported totals.
|
|
34
|
+
function round(n) {
|
|
35
|
+
return Math.round(n * 10000) / 10000;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Record a single billed call. `charged` is the net credits for this call;
|
|
39
|
+
* `remaining`, when present, is the authoritative balance afterwards.
|
|
40
|
+
* Non-finite or negative charges are ignored so a malformed header can't
|
|
41
|
+
* corrupt the running total.
|
|
42
|
+
*/
|
|
43
|
+
export function recordCharge(endpoint, charged, remaining) {
|
|
44
|
+
if (!Number.isFinite(charged) || charged < 0)
|
|
45
|
+
return;
|
|
46
|
+
state.billedCalls += 1;
|
|
47
|
+
state.totalCreditsCharged += charged;
|
|
48
|
+
if (remaining !== undefined && Number.isFinite(remaining)) {
|
|
49
|
+
state.balanceAfterLastCharge = remaining;
|
|
50
|
+
}
|
|
51
|
+
const existing = state.byEndpoint.get(endpoint);
|
|
52
|
+
if (existing) {
|
|
53
|
+
existing.calls += 1;
|
|
54
|
+
existing.credits += charged;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
state.byEndpoint.set(endpoint, { calls: 1, credits: charged });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Record a charge from a response's (lowercased) header map. A no-op when the
|
|
62
|
+
* response carries no charge header — polls, errors, and unbilled routes are
|
|
63
|
+
* silently skipped, so this is safe to call after every request.
|
|
64
|
+
*/
|
|
65
|
+
export function recordChargeFromHeaders(endpoint, headers) {
|
|
66
|
+
const charged = parseHeaderNumber(headers[CHARGED_HEADER]);
|
|
67
|
+
if (charged === undefined)
|
|
68
|
+
return;
|
|
69
|
+
recordCharge(endpoint, charged, parseHeaderNumber(headers[REMAINING_HEADER]));
|
|
70
|
+
}
|
|
71
|
+
/** Build a snapshot of session spend so far. `now` is injectable for tests. */
|
|
72
|
+
export function getSessionUsage(now = Date.now()) {
|
|
73
|
+
const byEndpoint = Array.from(state.byEndpoint.entries())
|
|
74
|
+
.map(([endpoint, u]) => ({ endpoint, calls: u.calls, credits: round(u.credits) }))
|
|
75
|
+
.sort((a, b) => b.credits - a.credits);
|
|
76
|
+
const snap = {
|
|
77
|
+
startedAt: new Date(state.startedAt).toISOString(),
|
|
78
|
+
durationMs: now - state.startedAt,
|
|
79
|
+
billedCalls: state.billedCalls,
|
|
80
|
+
totalCreditsCharged: round(state.totalCreditsCharged),
|
|
81
|
+
byEndpoint,
|
|
82
|
+
};
|
|
83
|
+
if (state.balanceAfterLastCharge !== undefined) {
|
|
84
|
+
snap.balanceAfterLastCharge = state.balanceAfterLastCharge;
|
|
85
|
+
}
|
|
86
|
+
return snap;
|
|
87
|
+
}
|
|
88
|
+
/** Test-only: reset the accumulator to a known clean state. */
|
|
89
|
+
export function resetSessionUsage(startedAt = Date.now()) {
|
|
90
|
+
state.startedAt = startedAt;
|
|
91
|
+
state.billedCalls = 0;
|
|
92
|
+
state.totalCreditsCharged = 0;
|
|
93
|
+
state.balanceAfterLastCharge = undefined;
|
|
94
|
+
state.byEndpoint.clear();
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=session-usage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-usage.js","sourceRoot":"","sources":["../src/session-usage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const getSessionUsageName = "get_session_usage";
|
|
2
|
+
export declare const getSessionUsageDescription: string;
|
|
3
|
+
export declare const getSessionUsageSchema: {};
|
|
4
|
+
export declare function getSessionUsageHandler(_input: Record<string, unknown>): Promise<{
|
|
5
|
+
content: {
|
|
6
|
+
type: "text";
|
|
7
|
+
text: string;
|
|
8
|
+
}[];
|
|
9
|
+
}>;
|
|
10
|
+
//# sourceMappingURL=get-session-usage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-session-usage.d.ts","sourceRoot":"","sources":["../../src/tools/get-session-usage.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,mBAAmB,sBAAsB,CAAA;AAEtD,eAAO,MAAM,0BAA0B,QAMuB,CAAA;AAE9D,eAAO,MAAM,qBAAqB,IAAK,CAAA;AAEvC,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;;GAG3E"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { getSessionUsage } from '../session-usage.js';
|
|
2
|
+
export const getSessionUsageName = 'get_session_usage';
|
|
3
|
+
export const getSessionUsageDescription = 'Report the ColdIQ credits spent so far in THIS conversation (the current MCP session). ' +
|
|
4
|
+
'Returns the total credits charged, the number of billed calls, a per-endpoint breakdown of where ' +
|
|
5
|
+
'credits went (highest spend first), and the account balance reported after the most recent billed call. ' +
|
|
6
|
+
'Use this to answer "how many credits has this chat used" or "what did I spend credits on here". ' +
|
|
7
|
+
'Note: it counts only calls made during the current session and resets if the conversation restarts; ' +
|
|
8
|
+
'for the authoritative live balance use get_credit_balance.';
|
|
9
|
+
export const getSessionUsageSchema = {};
|
|
10
|
+
export async function getSessionUsageHandler(_input) {
|
|
11
|
+
const usage = getSessionUsage();
|
|
12
|
+
return { content: [{ type: 'text', text: JSON.stringify({ data: usage }) }] };
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=get-session-usage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-session-usage.js","sourceRoot":"","sources":["../../src/tools/get-session-usage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAErD,MAAM,CAAC,MAAM,mBAAmB,GAAG,mBAAmB,CAAA;AAEtD,MAAM,CAAC,MAAM,0BAA0B,GACrC,yFAAyF;IACzF,mGAAmG;IACnG,0GAA0G;IAC1G,kGAAkG;IAClG,sGAAsG;IACtG,4DAA4D,CAAA;AAE9D,MAAM,CAAC,MAAM,qBAAqB,GAAG,EAAE,CAAA;AAEvC,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,MAA+B;IAC1E,MAAM,KAAK,GAAG,eAAe,EAAE,CAAA;IAC/B,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,CAAA;AACxF,CAAC"}
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Agent, setGlobalDispatcher } from 'undici'
|
|
2
|
+
import { recordChargeFromHeaders } from './session-usage.js'
|
|
2
3
|
|
|
3
4
|
export type ApiResponse = {
|
|
4
5
|
ok: boolean
|
|
@@ -75,6 +76,9 @@ export async function callApi(
|
|
|
75
76
|
res.headers.forEach((value, key) => {
|
|
76
77
|
headers[key.toLowerCase()] = value
|
|
77
78
|
})
|
|
79
|
+
// Tally this call's credit charge for the per-session usage report. No-op
|
|
80
|
+
// unless the response carries a charge header, so polls/errors are skipped.
|
|
81
|
+
recordChargeFromHeaders(path, headers)
|
|
78
82
|
return { ok: res.ok, status: res.status, data, headers }
|
|
79
83
|
} catch (err) {
|
|
80
84
|
const isTimeout =
|
package/src/index.ts
CHANGED
|
@@ -130,6 +130,13 @@ import {
|
|
|
130
130
|
getCreditBalanceHandler,
|
|
131
131
|
} from './tools/get-credit-balance.js'
|
|
132
132
|
|
|
133
|
+
import {
|
|
134
|
+
getSessionUsageName,
|
|
135
|
+
getSessionUsageDescription,
|
|
136
|
+
getSessionUsageSchema,
|
|
137
|
+
getSessionUsageHandler,
|
|
138
|
+
} from './tools/get-session-usage.js'
|
|
139
|
+
|
|
133
140
|
import {
|
|
134
141
|
extractPostEngagementName,
|
|
135
142
|
extractPostEngagementDescription,
|
|
@@ -192,6 +199,7 @@ server.tool(searchSeoName, searchSeoDescription, searchSeoSchema, searchSeoHandl
|
|
|
192
199
|
server.tool(findSignalsName, findSignalsDescription, findSignalsSchema, findSignalsHandler)
|
|
193
200
|
server.tool(fetchPageContentName, fetchPageContentDescription, fetchPageContentSchema, fetchPageContentHandler)
|
|
194
201
|
server.tool(getCreditBalanceName, getCreditBalanceDescription, getCreditBalanceSchema, getCreditBalanceHandler)
|
|
202
|
+
server.tool(getSessionUsageName, getSessionUsageDescription, getSessionUsageSchema, getSessionUsageHandler)
|
|
195
203
|
server.tool(listDataSourcesName, listDataSourcesDescription, listDataSourcesSchema, listDataSourcesHandler)
|
|
196
204
|
server.tool(extractPostEngagementName, extractPostEngagementDescription, extractPostEngagementSchema, extractPostEngagementHandler)
|
|
197
205
|
server.tool(getPlaceReviewsName, getPlaceReviewsDescription, getPlaceReviewsSchema, getPlaceReviewsHandler)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process ColdIQ credit accumulator for one MCP session ("campaign").
|
|
3
|
+
*
|
|
4
|
+
* The ColdIQ MCP server runs as a stdio subprocess spawned per client
|
|
5
|
+
* conversation, so a module-level singleton here is naturally scoped to a
|
|
6
|
+
* single campaign: it starts empty when the process boots and is discarded
|
|
7
|
+
* when the conversation's server exits. We tally the per-call charges the API
|
|
8
|
+
* already reports via the `X-ColdIQ-Credits-Charged` / `-Remaining` response
|
|
9
|
+
* headers (the same signal the executor surfaces in `_meta`), so reporting
|
|
10
|
+
* session spend never issues an extra billable call.
|
|
11
|
+
*
|
|
12
|
+
* Caveats this module deliberately accepts (and the tool surfaces to the user):
|
|
13
|
+
* - It only counts calls made during the current process lifetime; resuming or
|
|
14
|
+
* restarting the conversation starts a fresh tally.
|
|
15
|
+
* - `charged` reflects what the API billed at call time. For async jobs that is
|
|
16
|
+
* the submit-time reservation; later webhook settlement is not visible here.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const CHARGED_HEADER = 'x-coldiq-credits-charged'
|
|
20
|
+
const REMAINING_HEADER = 'x-coldiq-credits-remaining'
|
|
21
|
+
|
|
22
|
+
export interface EndpointUsage {
|
|
23
|
+
/** Number of billed calls to this endpoint this session. */
|
|
24
|
+
calls: number
|
|
25
|
+
/** Total net ColdIQ credits charged across those calls. */
|
|
26
|
+
credits: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SessionUsageSnapshot {
|
|
30
|
+
/** ISO 8601 timestamp of when this MCP session (process) started. */
|
|
31
|
+
startedAt: string
|
|
32
|
+
/** Milliseconds elapsed since the session started. */
|
|
33
|
+
durationMs: number
|
|
34
|
+
/** Count of calls that charged credits this session. */
|
|
35
|
+
billedCalls: number
|
|
36
|
+
/** Sum of net credits charged across all billed calls this session. */
|
|
37
|
+
totalCreditsCharged: number
|
|
38
|
+
/** Balance the API reported after the most recent billed call, if any. */
|
|
39
|
+
balanceAfterLastCharge?: number
|
|
40
|
+
/** Per-endpoint breakdown, highest spend first. */
|
|
41
|
+
byEndpoint: Array<{ endpoint: string; calls: number; credits: number }>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface MutableState {
|
|
45
|
+
startedAt: number
|
|
46
|
+
billedCalls: number
|
|
47
|
+
totalCreditsCharged: number
|
|
48
|
+
balanceAfterLastCharge?: number
|
|
49
|
+
byEndpoint: Map<string, EndpointUsage>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const state: MutableState = {
|
|
53
|
+
startedAt: Date.now(),
|
|
54
|
+
billedCalls: 0,
|
|
55
|
+
totalCreditsCharged: 0,
|
|
56
|
+
byEndpoint: new Map(),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseHeaderNumber(raw: string | undefined): number | undefined {
|
|
60
|
+
if (raw === undefined) return undefined
|
|
61
|
+
const n = Number(raw)
|
|
62
|
+
return Number.isFinite(n) ? n : undefined
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Credits can be fractional (settlement refunds). Round to 4 dp so summed
|
|
66
|
+
// floating-point noise never leaks into the reported totals.
|
|
67
|
+
function round(n: number): number {
|
|
68
|
+
return Math.round(n * 10000) / 10000
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Record a single billed call. `charged` is the net credits for this call;
|
|
73
|
+
* `remaining`, when present, is the authoritative balance afterwards.
|
|
74
|
+
* Non-finite or negative charges are ignored so a malformed header can't
|
|
75
|
+
* corrupt the running total.
|
|
76
|
+
*/
|
|
77
|
+
export function recordCharge(endpoint: string, charged: number, remaining?: number): void {
|
|
78
|
+
if (!Number.isFinite(charged) || charged < 0) return
|
|
79
|
+
state.billedCalls += 1
|
|
80
|
+
state.totalCreditsCharged += charged
|
|
81
|
+
if (remaining !== undefined && Number.isFinite(remaining)) {
|
|
82
|
+
state.balanceAfterLastCharge = remaining
|
|
83
|
+
}
|
|
84
|
+
const existing = state.byEndpoint.get(endpoint)
|
|
85
|
+
if (existing) {
|
|
86
|
+
existing.calls += 1
|
|
87
|
+
existing.credits += charged
|
|
88
|
+
} else {
|
|
89
|
+
state.byEndpoint.set(endpoint, { calls: 1, credits: charged })
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Record a charge from a response's (lowercased) header map. A no-op when the
|
|
95
|
+
* response carries no charge header — polls, errors, and unbilled routes are
|
|
96
|
+
* silently skipped, so this is safe to call after every request.
|
|
97
|
+
*/
|
|
98
|
+
export function recordChargeFromHeaders(endpoint: string, headers: Record<string, string>): void {
|
|
99
|
+
const charged = parseHeaderNumber(headers[CHARGED_HEADER])
|
|
100
|
+
if (charged === undefined) return
|
|
101
|
+
recordCharge(endpoint, charged, parseHeaderNumber(headers[REMAINING_HEADER]))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Build a snapshot of session spend so far. `now` is injectable for tests. */
|
|
105
|
+
export function getSessionUsage(now: number = Date.now()): SessionUsageSnapshot {
|
|
106
|
+
const byEndpoint = Array.from(state.byEndpoint.entries())
|
|
107
|
+
.map(([endpoint, u]) => ({ endpoint, calls: u.calls, credits: round(u.credits) }))
|
|
108
|
+
.sort((a, b) => b.credits - a.credits)
|
|
109
|
+
|
|
110
|
+
const snap: SessionUsageSnapshot = {
|
|
111
|
+
startedAt: new Date(state.startedAt).toISOString(),
|
|
112
|
+
durationMs: now - state.startedAt,
|
|
113
|
+
billedCalls: state.billedCalls,
|
|
114
|
+
totalCreditsCharged: round(state.totalCreditsCharged),
|
|
115
|
+
byEndpoint,
|
|
116
|
+
}
|
|
117
|
+
if (state.balanceAfterLastCharge !== undefined) {
|
|
118
|
+
snap.balanceAfterLastCharge = state.balanceAfterLastCharge
|
|
119
|
+
}
|
|
120
|
+
return snap
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Test-only: reset the accumulator to a known clean state. */
|
|
124
|
+
export function resetSessionUsage(startedAt: number = Date.now()): void {
|
|
125
|
+
state.startedAt = startedAt
|
|
126
|
+
state.billedCalls = 0
|
|
127
|
+
state.totalCreditsCharged = 0
|
|
128
|
+
state.balanceAfterLastCharge = undefined
|
|
129
|
+
state.byEndpoint.clear()
|
|
130
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { getSessionUsage } from '../session-usage.js'
|
|
2
|
+
|
|
3
|
+
export const getSessionUsageName = 'get_session_usage'
|
|
4
|
+
|
|
5
|
+
export const getSessionUsageDescription =
|
|
6
|
+
'Report the ColdIQ credits spent so far in THIS conversation (the current MCP session). ' +
|
|
7
|
+
'Returns the total credits charged, the number of billed calls, a per-endpoint breakdown of where ' +
|
|
8
|
+
'credits went (highest spend first), and the account balance reported after the most recent billed call. ' +
|
|
9
|
+
'Use this to answer "how many credits has this chat used" or "what did I spend credits on here". ' +
|
|
10
|
+
'Note: it counts only calls made during the current session and resets if the conversation restarts; ' +
|
|
11
|
+
'for the authoritative live balance use get_credit_balance.'
|
|
12
|
+
|
|
13
|
+
export const getSessionUsageSchema = {}
|
|
14
|
+
|
|
15
|
+
export async function getSessionUsageHandler(_input: Record<string, unknown>) {
|
|
16
|
+
const usage = getSessionUsage()
|
|
17
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ data: usage }) }] }
|
|
18
|
+
}
|
package/tests/executor.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
2
|
import { initClient } from '../src/client.js'
|
|
3
3
|
import { executeWithFallback } from '../src/executor.js'
|
|
4
|
+
import { getSessionUsage, resetSessionUsage } from '../src/session-usage.js'
|
|
4
5
|
import * as registry from '../src/registry.js'
|
|
5
6
|
import type { ProviderEntry } from '../src/registry.js'
|
|
6
7
|
|
|
@@ -225,6 +226,74 @@ describe('executor exception safety', () => {
|
|
|
225
226
|
})
|
|
226
227
|
})
|
|
227
228
|
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// Session-usage integration — the async create/poll path must record a charge
|
|
231
|
+
// exactly once. Locks in the "no double count" guarantee against future
|
|
232
|
+
// refactors of executeAsync's header merging.
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
describe('executor records session credit usage exactly once on the async path', () => {
|
|
236
|
+
const originalFetch = globalThis.fetch
|
|
237
|
+
|
|
238
|
+
beforeEach(() => {
|
|
239
|
+
initClient('http://test-api.local', 'test-key-123')
|
|
240
|
+
resetSessionUsage(0)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
afterEach(() => {
|
|
244
|
+
globalThis.fetch = originalFetch
|
|
245
|
+
vi.restoreAllMocks()
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('charge header on the create response is counted once; polls add nothing', async () => {
|
|
249
|
+
stubProviders([
|
|
250
|
+
makeProvider({
|
|
251
|
+
id: 'async-charged',
|
|
252
|
+
endpoint: '/async-charged',
|
|
253
|
+
hasResult: (data) => (data as Record<string, unknown>).done === true,
|
|
254
|
+
async: {
|
|
255
|
+
extractId: () => 'job-1',
|
|
256
|
+
pollEndpoint: (id) => `/poll/${id}`,
|
|
257
|
+
pollIntervalMs: 5,
|
|
258
|
+
timeoutMs: 500,
|
|
259
|
+
// Complete only once the poll body says so, forcing ≥1 poll round.
|
|
260
|
+
isComplete: (data) => (data as Record<string, unknown>).done === true,
|
|
261
|
+
},
|
|
262
|
+
}),
|
|
263
|
+
])
|
|
264
|
+
|
|
265
|
+
let pollCount = 0
|
|
266
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
267
|
+
const u = url.toString()
|
|
268
|
+
if (u.includes('/poll/')) {
|
|
269
|
+
pollCount++
|
|
270
|
+
// First poll still processing (no charge header), second completes.
|
|
271
|
+
const body = pollCount >= 2 ? { done: true } : { status: 'processing' }
|
|
272
|
+
return new Response(JSON.stringify(body), { status: 200 })
|
|
273
|
+
}
|
|
274
|
+
// Create response — carries the billing headers, like the real API.
|
|
275
|
+
return new Response(JSON.stringify({ job_id: 'job-1' }), {
|
|
276
|
+
status: 200,
|
|
277
|
+
headers: {
|
|
278
|
+
'X-ColdIQ-Credits-Charged': '5',
|
|
279
|
+
'X-ColdIQ-Credits-Remaining': '120',
|
|
280
|
+
},
|
|
281
|
+
})
|
|
282
|
+
}) as typeof fetch
|
|
283
|
+
|
|
284
|
+
const result = await executeWithFallback('find_people', { domain: 'coldiq.com' })
|
|
285
|
+
|
|
286
|
+
expect('data' in result).toBe(true)
|
|
287
|
+
expect(pollCount).toBeGreaterThanOrEqual(2)
|
|
288
|
+
|
|
289
|
+
const usage = getSessionUsage()
|
|
290
|
+
expect(usage.billedCalls).toBe(1)
|
|
291
|
+
expect(usage.totalCreditsCharged).toBe(5)
|
|
292
|
+
expect(usage.balanceAfterLastCharge).toBe(120)
|
|
293
|
+
expect(usage.byEndpoint).toEqual([{ endpoint: '/async-charged', calls: 1, credits: 5 }])
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
|
|
228
297
|
// ---------------------------------------------------------------------------
|
|
229
298
|
// postFilter integration (existing tests)
|
|
230
299
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
recordCharge,
|
|
4
|
+
recordChargeFromHeaders,
|
|
5
|
+
getSessionUsage,
|
|
6
|
+
resetSessionUsage,
|
|
7
|
+
} from '../src/session-usage.js'
|
|
8
|
+
import { initClient, callApi } from '../src/client.js'
|
|
9
|
+
|
|
10
|
+
describe('session-usage accumulator', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
resetSessionUsage(1_000)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('starts empty', () => {
|
|
16
|
+
const usage = getSessionUsage(1_000)
|
|
17
|
+
expect(usage.billedCalls).toBe(0)
|
|
18
|
+
expect(usage.totalCreditsCharged).toBe(0)
|
|
19
|
+
expect(usage.byEndpoint).toEqual([])
|
|
20
|
+
expect(usage.balanceAfterLastCharge).toBeUndefined()
|
|
21
|
+
expect(usage.startedAt).toBe(new Date(1_000).toISOString())
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('accumulates charges and tracks the last reported balance', () => {
|
|
25
|
+
recordCharge('/prospeo/find-email', 1, 99)
|
|
26
|
+
recordCharge('/prospeo/find-email', 1, 98)
|
|
27
|
+
recordCharge('/companyenrich/companies/search', 3, 95)
|
|
28
|
+
|
|
29
|
+
const usage = getSessionUsage(2_000)
|
|
30
|
+
expect(usage.billedCalls).toBe(3)
|
|
31
|
+
expect(usage.totalCreditsCharged).toBe(5)
|
|
32
|
+
expect(usage.balanceAfterLastCharge).toBe(95)
|
|
33
|
+
expect(usage.durationMs).toBe(1_000)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('breaks down per endpoint sorted by credits descending', () => {
|
|
37
|
+
recordCharge('/cheap', 1)
|
|
38
|
+
recordCharge('/expensive', 10)
|
|
39
|
+
recordCharge('/cheap', 1)
|
|
40
|
+
|
|
41
|
+
const { byEndpoint } = getSessionUsage()
|
|
42
|
+
expect(byEndpoint).toEqual([
|
|
43
|
+
{ endpoint: '/expensive', calls: 1, credits: 10 },
|
|
44
|
+
{ endpoint: '/cheap', calls: 2, credits: 2 },
|
|
45
|
+
])
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('rounds fractional credit sums to avoid float noise', () => {
|
|
49
|
+
recordCharge('/x', 0.1)
|
|
50
|
+
recordCharge('/x', 0.2)
|
|
51
|
+
expect(getSessionUsage().totalCreditsCharged).toBe(0.3)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('ignores malformed and negative charges', () => {
|
|
55
|
+
recordCharge('/x', Number.NaN)
|
|
56
|
+
recordCharge('/x', -5)
|
|
57
|
+
recordCharge('/x', Infinity)
|
|
58
|
+
const usage = getSessionUsage()
|
|
59
|
+
expect(usage.billedCalls).toBe(0)
|
|
60
|
+
expect(usage.totalCreditsCharged).toBe(0)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('recordChargeFromHeaders', () => {
|
|
64
|
+
it('records when the charge header is present', () => {
|
|
65
|
+
recordChargeFromHeaders('/find-email', {
|
|
66
|
+
'x-coldiq-credits-charged': '2',
|
|
67
|
+
'x-coldiq-credits-remaining': '198',
|
|
68
|
+
})
|
|
69
|
+
const usage = getSessionUsage()
|
|
70
|
+
expect(usage.billedCalls).toBe(1)
|
|
71
|
+
expect(usage.totalCreditsCharged).toBe(2)
|
|
72
|
+
expect(usage.balanceAfterLastCharge).toBe(198)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('is a no-op when no charge header is present (e.g. a poll response)', () => {
|
|
76
|
+
recordChargeFromHeaders('/poll', { 'content-type': 'application/json' })
|
|
77
|
+
expect(getSessionUsage().billedCalls).toBe(0)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('records the charge even when the remaining header is absent', () => {
|
|
81
|
+
recordChargeFromHeaders('/x', { 'x-coldiq-credits-charged': '4' })
|
|
82
|
+
const usage = getSessionUsage()
|
|
83
|
+
expect(usage.totalCreditsCharged).toBe(4)
|
|
84
|
+
expect(usage.balanceAfterLastCharge).toBeUndefined()
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('callApi records credit charges into session usage', () => {
|
|
90
|
+
const originalFetch = globalThis.fetch
|
|
91
|
+
|
|
92
|
+
beforeEach(() => {
|
|
93
|
+
initClient('http://test-api.local', 'test-key')
|
|
94
|
+
resetSessionUsage(0)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
afterEach(() => {
|
|
98
|
+
globalThis.fetch = originalFetch
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('tallies a charge surfaced in response headers', async () => {
|
|
102
|
+
globalThis.fetch = vi.fn(async () =>
|
|
103
|
+
new Response(JSON.stringify({ email: 'michel@coldiq.com' }), {
|
|
104
|
+
status: 200,
|
|
105
|
+
headers: {
|
|
106
|
+
'X-ColdIQ-Credits-Charged': '1',
|
|
107
|
+
'X-ColdIQ-Credits-Remaining': '249',
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
) as typeof fetch
|
|
111
|
+
|
|
112
|
+
await callApi('POST', '/prospeo/find-email', { domain: 'coldiq.com' })
|
|
113
|
+
|
|
114
|
+
const usage = getSessionUsage()
|
|
115
|
+
expect(usage.billedCalls).toBe(1)
|
|
116
|
+
expect(usage.totalCreditsCharged).toBe(1)
|
|
117
|
+
expect(usage.balanceAfterLastCharge).toBe(249)
|
|
118
|
+
expect(usage.byEndpoint).toEqual([{ endpoint: '/prospeo/find-email', calls: 1, credits: 1 }])
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('does not tally calls that return no charge header', async () => {
|
|
122
|
+
globalThis.fetch = vi.fn(async () =>
|
|
123
|
+
new Response(JSON.stringify({ status: 'processing' }), { status: 200 }),
|
|
124
|
+
) as typeof fetch
|
|
125
|
+
|
|
126
|
+
await callApi('GET', '/jobs/abc123')
|
|
127
|
+
|
|
128
|
+
expect(getSessionUsage().billedCalls).toBe(0)
|
|
129
|
+
})
|
|
130
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
getSessionUsageHandler,
|
|
4
|
+
getSessionUsageName,
|
|
5
|
+
getSessionUsageSchema,
|
|
6
|
+
} from '../../src/tools/get-session-usage.js'
|
|
7
|
+
import { recordCharge, resetSessionUsage } from '../../src/session-usage.js'
|
|
8
|
+
|
|
9
|
+
describe('get_session_usage handler', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
resetSessionUsage()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('is registered as a zero-arg tool', () => {
|
|
15
|
+
expect(getSessionUsageName).toBe('get_session_usage')
|
|
16
|
+
expect(getSessionUsageSchema).toEqual({})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('returns an empty snapshot before any billed call', async () => {
|
|
20
|
+
const result = await getSessionUsageHandler({})
|
|
21
|
+
expect(result.isError).toBeFalsy()
|
|
22
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
23
|
+
expect(parsed.data.billedCalls).toBe(0)
|
|
24
|
+
expect(parsed.data.totalCreditsCharged).toBe(0)
|
|
25
|
+
expect(parsed.data.byEndpoint).toEqual([])
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('reflects recorded charges with a per-endpoint breakdown', async () => {
|
|
29
|
+
recordCharge('/prospeo/find-email', 1, 99)
|
|
30
|
+
recordCharge('/companyenrich/companies/search', 3, 96)
|
|
31
|
+
|
|
32
|
+
const result = await getSessionUsageHandler({})
|
|
33
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
34
|
+
|
|
35
|
+
expect(parsed.data.billedCalls).toBe(2)
|
|
36
|
+
expect(parsed.data.totalCreditsCharged).toBe(4)
|
|
37
|
+
expect(parsed.data.balanceAfterLastCharge).toBe(96)
|
|
38
|
+
expect(parsed.data.byEndpoint[0]).toEqual({
|
|
39
|
+
endpoint: '/companyenrich/companies/search',
|
|
40
|
+
calls: 1,
|
|
41
|
+
credits: 3,
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
})
|