@granular-software/sdk 0.4.37 → 0.4.38
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 +16 -3
- package/dist/agent-evals.d.ts +16 -3
- package/dist/agent-evals.js +341 -105
- package/dist/agent-evals.js.map +1 -1
- package/dist/agent-evals.mjs +340 -105
- package/dist/agent-evals.mjs.map +1 -1
- package/dist/cli/index.js +129 -3
- package/dist/client-BNbWA9jQ.d.ts +1064 -0
- package/dist/client-HDJgcJC5.d.mts +1064 -0
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +231 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +225 -4
- package/dist/index.mjs.map +1 -1
- package/dist/spend-rzS1rlFr.d.mts +1559 -0
- package/dist/spend-rzS1rlFr.d.ts +1559 -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/agent-evals.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var promises = require('fs/promises');
|
|
4
4
|
var path = require('path');
|
|
5
|
+
var OpenAI = require('openai');
|
|
5
6
|
var Automerge = require('@automerge/automerge');
|
|
6
7
|
|
|
7
8
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -25,6 +26,7 @@ function _interopNamespace(e) {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
29
|
+
var OpenAI__default = /*#__PURE__*/_interopDefault(OpenAI);
|
|
28
30
|
var Automerge__namespace = /*#__PURE__*/_interopNamespace(Automerge);
|
|
29
31
|
|
|
30
32
|
var __create = Object.create;
|
|
@@ -4643,6 +4645,7 @@ function withPromptTranscriptTimeout(promise) {
|
|
|
4643
4645
|
var Session = class {
|
|
4644
4646
|
client;
|
|
4645
4647
|
clientId;
|
|
4648
|
+
initialQuota;
|
|
4646
4649
|
jobsMap = /* @__PURE__ */ new Map();
|
|
4647
4650
|
pendingAgentMessagesByJobId = /* @__PURE__ */ new Map();
|
|
4648
4651
|
eventListeners = /* @__PURE__ */ new Map();
|
|
@@ -4658,9 +4661,10 @@ var Session = class {
|
|
|
4658
4661
|
promptCache = /* @__PURE__ */ new Map();
|
|
4659
4662
|
/** Prompt ids locally answered before the document sync catches up. */
|
|
4660
4663
|
hiddenPromptIds = /* @__PURE__ */ new Set();
|
|
4661
|
-
constructor(client, clientId) {
|
|
4664
|
+
constructor(client, clientId, options = {}) {
|
|
4662
4665
|
this.client = client;
|
|
4663
4666
|
this.clientId = clientId || `client_${Date.now()}`;
|
|
4667
|
+
this.initialQuota = options.initialQuota || null;
|
|
4664
4668
|
this.setupEventHandlers();
|
|
4665
4669
|
this.setupToolInvokeHandler();
|
|
4666
4670
|
}
|
|
@@ -4709,6 +4713,16 @@ var Session = class {
|
|
|
4709
4713
|
get document() {
|
|
4710
4714
|
return this.client.doc;
|
|
4711
4715
|
}
|
|
4716
|
+
get quota() {
|
|
4717
|
+
return this.getQuota();
|
|
4718
|
+
}
|
|
4719
|
+
getQuota() {
|
|
4720
|
+
const quota = this.client.doc.billing?.quota;
|
|
4721
|
+
if (quota && typeof quota === "object") {
|
|
4722
|
+
return quota;
|
|
4723
|
+
}
|
|
4724
|
+
return this.initialQuota;
|
|
4725
|
+
}
|
|
4712
4726
|
get sessionId() {
|
|
4713
4727
|
return this.client.currentSessionId;
|
|
4714
4728
|
}
|
|
@@ -11331,6 +11345,110 @@ async function invokeRegisteredEffect(effectMap, request) {
|
|
|
11331
11345
|
return resolved.handler(request.input, context);
|
|
11332
11346
|
}
|
|
11333
11347
|
|
|
11348
|
+
// src/spend.ts
|
|
11349
|
+
function toGranularHttpBase(apiUrl) {
|
|
11350
|
+
const url = new URL(apiUrl);
|
|
11351
|
+
if (url.protocol === "ws:") {
|
|
11352
|
+
url.protocol = "http:";
|
|
11353
|
+
} else if (url.protocol === "wss:") {
|
|
11354
|
+
url.protocol = "https:";
|
|
11355
|
+
}
|
|
11356
|
+
url.pathname = url.pathname.replace(/\/ws\/connect$/, "").replace(/\/ws$/, "");
|
|
11357
|
+
if (!url.pathname || url.pathname === "/") {
|
|
11358
|
+
url.pathname = "/granular";
|
|
11359
|
+
}
|
|
11360
|
+
url.search = "";
|
|
11361
|
+
url.hash = "";
|
|
11362
|
+
return url.toString().replace(/\/$/, "");
|
|
11363
|
+
}
|
|
11364
|
+
function cleanIdPart(value) {
|
|
11365
|
+
return value.replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
|
|
11366
|
+
}
|
|
11367
|
+
function buildOpenAISpendEventId(usage, context = {}) {
|
|
11368
|
+
const requestId = usage.requestId?.trim();
|
|
11369
|
+
if (!requestId) return void 0;
|
|
11370
|
+
const scope = context.sessionId || context.environmentId || context.subjectId || context.sandboxId || "global";
|
|
11371
|
+
return ["spend", "openai", scope, requestId].map(cleanIdPart).join("_");
|
|
11372
|
+
}
|
|
11373
|
+
function pricingEffectiveAtSeconds(value) {
|
|
11374
|
+
if (!value) return null;
|
|
11375
|
+
const parsed = Date.parse(value);
|
|
11376
|
+
return Number.isFinite(parsed) ? Math.floor(parsed / 1e3) : null;
|
|
11377
|
+
}
|
|
11378
|
+
function compactContext(context) {
|
|
11379
|
+
return Object.fromEntries(
|
|
11380
|
+
Object.entries(context).filter(
|
|
11381
|
+
([, value]) => value != null && value !== ""
|
|
11382
|
+
)
|
|
11383
|
+
);
|
|
11384
|
+
}
|
|
11385
|
+
function omitTenantId(context) {
|
|
11386
|
+
const scopedContext = { ...context };
|
|
11387
|
+
delete scopedContext.tenantId;
|
|
11388
|
+
return scopedContext;
|
|
11389
|
+
}
|
|
11390
|
+
async function recordOpenAIUsageSpend(options) {
|
|
11391
|
+
const usageContext = compactContext({
|
|
11392
|
+
...options.usage.usageContext || {},
|
|
11393
|
+
...options.context || {}
|
|
11394
|
+
});
|
|
11395
|
+
const context = omitTenantId(usageContext);
|
|
11396
|
+
const spendEventId = options.usage.spendEventId || buildOpenAISpendEventId(options.usage, context);
|
|
11397
|
+
const metadata = {
|
|
11398
|
+
...options.metadata || {},
|
|
11399
|
+
...options.usage.rawUsage !== void 0 ? { openaiUsage: options.usage.rawUsage } : {},
|
|
11400
|
+
usageContext: context
|
|
11401
|
+
};
|
|
11402
|
+
const response = await fetch(
|
|
11403
|
+
`${toGranularHttpBase(options.apiUrl)}/control/spend/events`,
|
|
11404
|
+
{
|
|
11405
|
+
method: "POST",
|
|
11406
|
+
cache: "no-store",
|
|
11407
|
+
headers: {
|
|
11408
|
+
Authorization: `Bearer ${options.token}`,
|
|
11409
|
+
"Content-Type": "application/json"
|
|
11410
|
+
},
|
|
11411
|
+
body: JSON.stringify({
|
|
11412
|
+
...spendEventId ? { spendEventId } : {},
|
|
11413
|
+
sandboxId: context.sandboxId || null,
|
|
11414
|
+
environmentId: context.environmentId || null,
|
|
11415
|
+
sessionId: context.sessionId || null,
|
|
11416
|
+
subjectId: context.subjectId || null,
|
|
11417
|
+
permissionProfileId: context.permissionProfileId || null,
|
|
11418
|
+
source: "openai",
|
|
11419
|
+
lineItemType: "llm_tokens",
|
|
11420
|
+
provider: options.usage.provider,
|
|
11421
|
+
model: options.usage.model,
|
|
11422
|
+
operation: options.usage.operation || "chat.completions",
|
|
11423
|
+
requestId: options.usage.requestId || null,
|
|
11424
|
+
inputTokens: options.usage.inputTokens,
|
|
11425
|
+
outputTokens: options.usage.outputTokens,
|
|
11426
|
+
cachedInputTokens: options.usage.cachedInputTokens,
|
|
11427
|
+
reasoningTokens: options.usage.reasoningTokens,
|
|
11428
|
+
quantity: options.usage.totalTokens,
|
|
11429
|
+
quantityUnit: "tokens",
|
|
11430
|
+
inputPricePerMillionMicros: options.usage.inputPricePerMillionMicros,
|
|
11431
|
+
cachedInputPricePerMillionMicros: options.usage.cachedInputPricePerMillionMicros,
|
|
11432
|
+
outputPricePerMillionMicros: options.usage.outputPricePerMillionMicros,
|
|
11433
|
+
amountMicros: options.usage.amountMicros,
|
|
11434
|
+
currency: options.usage.currency,
|
|
11435
|
+
pricingSource: options.usage.pricingSource,
|
|
11436
|
+
pricingEffectiveAt: pricingEffectiveAtSeconds(
|
|
11437
|
+
options.usage.pricingEffectiveAt
|
|
11438
|
+
),
|
|
11439
|
+
estimated: false,
|
|
11440
|
+
metadata
|
|
11441
|
+
})
|
|
11442
|
+
}
|
|
11443
|
+
);
|
|
11444
|
+
if (!response.ok) {
|
|
11445
|
+
throw new Error(
|
|
11446
|
+
`Granular spend event failed (${response.status}): ${await response.text()}`
|
|
11447
|
+
);
|
|
11448
|
+
}
|
|
11449
|
+
return response.json();
|
|
11450
|
+
}
|
|
11451
|
+
|
|
11334
11452
|
// ../metamodel-enum/src/index.ts
|
|
11335
11453
|
function renderInlineStringUnion(values) {
|
|
11336
11454
|
return values.map((value) => JSON.stringify(value)).join(" | ");
|
|
@@ -14113,7 +14231,7 @@ var EnvironmentSession = class extends Session {
|
|
|
14113
14231
|
/** The last known graph container status, updated by checkReadiness() or on heartbeat */
|
|
14114
14232
|
graphContainerStatus = null;
|
|
14115
14233
|
constructor(client, environment, clientId, options = {}) {
|
|
14116
|
-
super(client, clientId);
|
|
14234
|
+
super(client, clientId, { initialQuota: options.initialQuota });
|
|
14117
14235
|
this.environment = environment;
|
|
14118
14236
|
this.sessionDataRoutePrefix = options.sessionDataRoutePrefix || "/orchestrator/ws/sessions";
|
|
14119
14237
|
}
|
|
@@ -14912,6 +15030,15 @@ var Granular = class _Granular {
|
|
|
14912
15030
|
const environment = this.bindEnvironmentHandle(envData);
|
|
14913
15031
|
return this.bindWebSocketEnvironmentSession(environment, clientId, minted);
|
|
14914
15032
|
}
|
|
15033
|
+
async recordOpenAIUsageSpend(usage, context, options) {
|
|
15034
|
+
return recordOpenAIUsageSpend({
|
|
15035
|
+
apiUrl: this.apiUrl,
|
|
15036
|
+
token: this.apiKey,
|
|
15037
|
+
usage,
|
|
15038
|
+
context,
|
|
15039
|
+
metadata: options?.metadata
|
|
15040
|
+
});
|
|
15041
|
+
}
|
|
14915
15042
|
/**
|
|
14916
15043
|
* Mark a session closed in the control plane. If `environment` is the connected handle for that
|
|
14917
15044
|
* `sessionId`, disconnects the WebSocket so the runtime tears down cleanly.
|
|
@@ -15066,7 +15193,8 @@ var Granular = class _Granular {
|
|
|
15066
15193
|
const environmentSession = new EnvironmentSession(
|
|
15067
15194
|
client,
|
|
15068
15195
|
environment,
|
|
15069
|
-
clientId
|
|
15196
|
+
clientId,
|
|
15197
|
+
{ initialQuota: session.quota || null }
|
|
15070
15198
|
);
|
|
15071
15199
|
await environmentSession.hello();
|
|
15072
15200
|
return environmentSession;
|
|
@@ -17275,12 +17403,107 @@ ${knownFactsBlock}
|
|
|
17275
17403
|
${input.request?.trim() || "Use the latest user message in the conversation."}`;
|
|
17276
17404
|
}
|
|
17277
17405
|
|
|
17406
|
+
// src/openai-usage.ts
|
|
17407
|
+
var OPENAI_PRICING_SOURCE_URL = "https://developers.openai.com/api/docs/models/gpt-5.4/";
|
|
17408
|
+
var OPENAI_PRICING_EFFECTIVE_DATE = "2026-05-19";
|
|
17409
|
+
var OPENAI_MODEL_PRICING_USD_PER_MILLION = {
|
|
17410
|
+
"gpt-5.4": {
|
|
17411
|
+
provider: "openai",
|
|
17412
|
+
model: "gpt-5.4",
|
|
17413
|
+
currency: "USD",
|
|
17414
|
+
inputUsdPerMillion: 2.5,
|
|
17415
|
+
cachedInputUsdPerMillion: 0.25,
|
|
17416
|
+
outputUsdPerMillion: 15,
|
|
17417
|
+
sourceUrl: OPENAI_PRICING_SOURCE_URL,
|
|
17418
|
+
effectiveDate: OPENAI_PRICING_EFFECTIVE_DATE
|
|
17419
|
+
}
|
|
17420
|
+
};
|
|
17421
|
+
function asRecord5(value) {
|
|
17422
|
+
return value && typeof value === "object" ? value : null;
|
|
17423
|
+
}
|
|
17424
|
+
function numberField(record, key) {
|
|
17425
|
+
const value = record?.[key];
|
|
17426
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
17427
|
+
}
|
|
17428
|
+
function microsPerMillion(usdPerMillion) {
|
|
17429
|
+
return Math.round(usdPerMillion * 1e6);
|
|
17430
|
+
}
|
|
17431
|
+
function getOpenAIModelPricing(model) {
|
|
17432
|
+
return OPENAI_MODEL_PRICING_USD_PER_MILLION[model] || null;
|
|
17433
|
+
}
|
|
17434
|
+
function normalizeOpenAIUsage(rawUsage) {
|
|
17435
|
+
const usage = asRecord5(rawUsage);
|
|
17436
|
+
if (!usage) {
|
|
17437
|
+
return {
|
|
17438
|
+
inputTokens: 0,
|
|
17439
|
+
cachedInputTokens: 0,
|
|
17440
|
+
uncachedInputTokens: 0,
|
|
17441
|
+
outputTokens: 0,
|
|
17442
|
+
reasoningTokens: 0,
|
|
17443
|
+
totalTokens: 0
|
|
17444
|
+
};
|
|
17445
|
+
}
|
|
17446
|
+
const inputTokens = numberField(usage, "prompt_tokens") || numberField(usage, "input_tokens");
|
|
17447
|
+
const outputTokens = numberField(usage, "completion_tokens") || numberField(usage, "output_tokens");
|
|
17448
|
+
const totalTokens = numberField(usage, "total_tokens") || inputTokens + outputTokens;
|
|
17449
|
+
const inputDetails = asRecord5(usage.prompt_tokens_details) || asRecord5(usage.input_tokens_details);
|
|
17450
|
+
const outputDetails = asRecord5(usage.completion_tokens_details) || asRecord5(usage.output_tokens_details);
|
|
17451
|
+
const cachedInputTokens = Math.min(
|
|
17452
|
+
inputTokens,
|
|
17453
|
+
numberField(inputDetails, "cached_tokens") || numberField(inputDetails, "cached_input_tokens")
|
|
17454
|
+
);
|
|
17455
|
+
const reasoningTokens = numberField(outputDetails, "reasoning_tokens") || numberField(outputDetails, "reasoning_output_tokens");
|
|
17456
|
+
return {
|
|
17457
|
+
inputTokens,
|
|
17458
|
+
cachedInputTokens,
|
|
17459
|
+
uncachedInputTokens: Math.max(inputTokens - cachedInputTokens, 0),
|
|
17460
|
+
outputTokens,
|
|
17461
|
+
reasoningTokens,
|
|
17462
|
+
totalTokens
|
|
17463
|
+
};
|
|
17464
|
+
}
|
|
17465
|
+
function calculateOpenAITokenSpend(model, rawUsage) {
|
|
17466
|
+
const pricing = getOpenAIModelPricing(model);
|
|
17467
|
+
if (!pricing) return null;
|
|
17468
|
+
const usage = normalizeOpenAIUsage(rawUsage);
|
|
17469
|
+
const inputPricePerMillionMicros = microsPerMillion(
|
|
17470
|
+
pricing.inputUsdPerMillion
|
|
17471
|
+
);
|
|
17472
|
+
const cachedInputPricePerMillionMicros = microsPerMillion(
|
|
17473
|
+
pricing.cachedInputUsdPerMillion
|
|
17474
|
+
);
|
|
17475
|
+
const outputPricePerMillionMicros = microsPerMillion(
|
|
17476
|
+
pricing.outputUsdPerMillion
|
|
17477
|
+
);
|
|
17478
|
+
const amountMicros = Math.round(
|
|
17479
|
+
(usage.uncachedInputTokens * inputPricePerMillionMicros + usage.cachedInputTokens * cachedInputPricePerMillionMicros + usage.outputTokens * outputPricePerMillionMicros) / 1e6
|
|
17480
|
+
);
|
|
17481
|
+
return {
|
|
17482
|
+
provider: "openai",
|
|
17483
|
+
model,
|
|
17484
|
+
inputTokens: usage.inputTokens,
|
|
17485
|
+
cachedInputTokens: usage.cachedInputTokens,
|
|
17486
|
+
uncachedInputTokens: usage.uncachedInputTokens,
|
|
17487
|
+
outputTokens: usage.outputTokens,
|
|
17488
|
+
reasoningTokens: usage.reasoningTokens,
|
|
17489
|
+
totalTokens: usage.totalTokens,
|
|
17490
|
+
amountMicros,
|
|
17491
|
+
currency: "USD",
|
|
17492
|
+
inputPricePerMillionMicros,
|
|
17493
|
+
cachedInputPricePerMillionMicros,
|
|
17494
|
+
outputPricePerMillionMicros,
|
|
17495
|
+
pricingSource: pricing.sourceUrl,
|
|
17496
|
+
pricingEffectiveAt: pricing.effectiveDate,
|
|
17497
|
+
usage
|
|
17498
|
+
};
|
|
17499
|
+
}
|
|
17500
|
+
|
|
17278
17501
|
// src/agent-evals.ts
|
|
17279
17502
|
var DEFAULT_CONTROLLER_BUDGETS = {
|
|
17280
17503
|
maxIterations: 6,
|
|
17281
17504
|
maxNoProgressIterations: 2
|
|
17282
17505
|
};
|
|
17283
|
-
function
|
|
17506
|
+
function asRecord6(value) {
|
|
17284
17507
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
17285
17508
|
return value;
|
|
17286
17509
|
}
|
|
@@ -17333,9 +17556,9 @@ function asArray3(value) {
|
|
|
17333
17556
|
return Array.isArray(value) ? value : [value];
|
|
17334
17557
|
}
|
|
17335
17558
|
var GPT_54_TOKEN_PRICING_USD_PER_MILLION = {
|
|
17336
|
-
input:
|
|
17337
|
-
cachedInput: 0.
|
|
17338
|
-
output:
|
|
17559
|
+
input: 2.5,
|
|
17560
|
+
cachedInput: 0.25,
|
|
17561
|
+
output: 15
|
|
17339
17562
|
};
|
|
17340
17563
|
function emptyTokenUsage() {
|
|
17341
17564
|
return {
|
|
@@ -17352,14 +17575,24 @@ function emptyTokenUsage() {
|
|
|
17352
17575
|
missingUsageCalls: 0
|
|
17353
17576
|
};
|
|
17354
17577
|
}
|
|
17355
|
-
function
|
|
17578
|
+
function numberField2(record, key) {
|
|
17356
17579
|
const value = record?.[key];
|
|
17357
17580
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
17358
17581
|
}
|
|
17359
17582
|
function calculateTokenCost(input) {
|
|
17360
|
-
const
|
|
17361
|
-
|
|
17362
|
-
|
|
17583
|
+
const spend = input.model ? calculateOpenAITokenSpend(input.model, {
|
|
17584
|
+
input_tokens: input.uncachedInputTokens + input.cachedInputTokens,
|
|
17585
|
+
output_tokens: input.outputTokens,
|
|
17586
|
+
input_tokens_details: { cached_tokens: input.cachedInputTokens }
|
|
17587
|
+
}) : null;
|
|
17588
|
+
const pricing = spend ? {
|
|
17589
|
+
input: spend.inputPricePerMillionMicros / 1e6,
|
|
17590
|
+
cachedInput: spend.cachedInputPricePerMillionMicros / 1e6,
|
|
17591
|
+
output: spend.outputPricePerMillionMicros / 1e6
|
|
17592
|
+
} : GPT_54_TOKEN_PRICING_USD_PER_MILLION;
|
|
17593
|
+
const inputCostUsd = input.uncachedInputTokens * pricing.input / 1e6;
|
|
17594
|
+
const cachedInputCostUsd = input.cachedInputTokens * pricing.cachedInput / 1e6;
|
|
17595
|
+
const outputCostUsd = input.outputTokens * pricing.output / 1e6;
|
|
17363
17596
|
return {
|
|
17364
17597
|
inputCostUsd,
|
|
17365
17598
|
cachedInputCostUsd,
|
|
@@ -17368,18 +17601,20 @@ function calculateTokenCost(input) {
|
|
|
17368
17601
|
};
|
|
17369
17602
|
}
|
|
17370
17603
|
function extractTokenUsageFromRaw(raw) {
|
|
17371
|
-
const usage =
|
|
17604
|
+
const usage = asRecord6(asRecord6(raw)?.usage);
|
|
17372
17605
|
if (!usage) return null;
|
|
17373
|
-
const
|
|
17374
|
-
const
|
|
17375
|
-
const
|
|
17606
|
+
const model = typeof asRecord6(raw)?.model === "string" ? asRecord6(raw)?.model : void 0;
|
|
17607
|
+
const inputTokens = numberField2(usage, "prompt_tokens") || numberField2(usage, "input_tokens");
|
|
17608
|
+
const outputTokens = numberField2(usage, "completion_tokens") || numberField2(usage, "output_tokens");
|
|
17609
|
+
const details = asRecord6(usage.prompt_tokens_details) || asRecord6(usage.input_tokens_details);
|
|
17376
17610
|
const cachedInputTokens = Math.min(
|
|
17377
17611
|
inputTokens,
|
|
17378
|
-
|
|
17612
|
+
numberField2(details, "cached_tokens") || numberField2(details, "cached_input_tokens")
|
|
17379
17613
|
);
|
|
17380
17614
|
const uncachedInputTokens = Math.max(inputTokens - cachedInputTokens, 0);
|
|
17381
|
-
const totalTokens =
|
|
17615
|
+
const totalTokens = numberField2(usage, "total_tokens") || inputTokens + outputTokens;
|
|
17382
17616
|
const costs = calculateTokenCost({
|
|
17617
|
+
model,
|
|
17383
17618
|
uncachedInputTokens,
|
|
17384
17619
|
cachedInputTokens,
|
|
17385
17620
|
outputTokens
|
|
@@ -17430,7 +17665,7 @@ function aggregateTokenUsage(usages) {
|
|
|
17430
17665
|
}
|
|
17431
17666
|
function aggregateConversationTokenUsage(conversation) {
|
|
17432
17667
|
return aggregateTokenUsage(
|
|
17433
|
-
conversation.logTurns.flatMap(
|
|
17668
|
+
(conversation.logTurns || []).flatMap(
|
|
17434
17669
|
(turn) => turn.iterations.map((iteration) => iteration.tokenUsage)
|
|
17435
17670
|
)
|
|
17436
17671
|
);
|
|
@@ -17464,8 +17699,8 @@ function formatTokenUsage(usage) {
|
|
|
17464
17699
|
];
|
|
17465
17700
|
}
|
|
17466
17701
|
function getJobAgentMessages(liveDoc, jobId) {
|
|
17467
|
-
const jobsById =
|
|
17468
|
-
const job =
|
|
17702
|
+
const jobsById = asRecord6(asRecord6(liveDoc.jobs)?.byId);
|
|
17703
|
+
const job = asRecord6(jobsById?.[jobId]);
|
|
17469
17704
|
const agentMessages = job?.agentMessages;
|
|
17470
17705
|
return Array.isArray(agentMessages) ? agentMessages : [];
|
|
17471
17706
|
}
|
|
@@ -17524,12 +17759,12 @@ function buildHistory(entries) {
|
|
|
17524
17759
|
);
|
|
17525
17760
|
}
|
|
17526
17761
|
function getOpenPromptsFromDoc(liveDoc) {
|
|
17527
|
-
const jobsById =
|
|
17762
|
+
const jobsById = asRecord6(asRecord6(liveDoc?.jobs)?.byId) || {};
|
|
17528
17763
|
const prompts = [];
|
|
17529
17764
|
for (const job of Object.values(jobsById)) {
|
|
17530
|
-
const promptRecords =
|
|
17765
|
+
const promptRecords = asRecord6(asRecord6(job)?.prompts) || {};
|
|
17531
17766
|
for (const raw of Object.values(promptRecords)) {
|
|
17532
|
-
const record =
|
|
17767
|
+
const record = asRecord6(raw);
|
|
17533
17768
|
if (!record || record.status !== "open" || typeof record.promptId !== "string")
|
|
17534
17769
|
continue;
|
|
17535
17770
|
const prompt = normalizePrompt({
|
|
@@ -17550,11 +17785,11 @@ function getOpenPromptsFromDoc(liveDoc) {
|
|
|
17550
17785
|
return prompts;
|
|
17551
17786
|
}
|
|
17552
17787
|
function filterPromptsByBoundary(liveDoc, prompts, boundaryTimestamp) {
|
|
17553
|
-
const jobsById =
|
|
17788
|
+
const jobsById = asRecord6(asRecord6(liveDoc?.jobs)?.byId) || {};
|
|
17554
17789
|
return prompts.filter((prompt) => {
|
|
17555
17790
|
for (const jobRecord of Object.values(jobsById)) {
|
|
17556
|
-
const promptsById =
|
|
17557
|
-
const promptRecord =
|
|
17791
|
+
const promptsById = asRecord6(asRecord6(jobRecord)?.prompts) || {};
|
|
17792
|
+
const promptRecord = asRecord6(promptsById[prompt.id]);
|
|
17558
17793
|
const openedAt = Number(promptRecord?.openedAt) || 0;
|
|
17559
17794
|
if (openedAt >= boundaryTimestamp) return true;
|
|
17560
17795
|
}
|
|
@@ -17686,6 +17921,29 @@ function createOpenAIChatTurnGenerator(options) {
|
|
|
17686
17921
|
""
|
|
17687
17922
|
);
|
|
17688
17923
|
const model = options.model || "gpt-5.4";
|
|
17924
|
+
const client = new OpenAI__default.default({
|
|
17925
|
+
apiKey: options.apiKey,
|
|
17926
|
+
baseURL: baseUrl,
|
|
17927
|
+
defaultHeaders: options.headers
|
|
17928
|
+
});
|
|
17929
|
+
const emitUsage = async (rawUsage, requestId, usageContext) => {
|
|
17930
|
+
if (!rawUsage || !options.onUsage) return;
|
|
17931
|
+
const spend = calculateOpenAITokenSpend(model, rawUsage);
|
|
17932
|
+
if (!spend) return;
|
|
17933
|
+
const mergedUsageContext = {
|
|
17934
|
+
...options.usageContext || {},
|
|
17935
|
+
...usageContext || {}
|
|
17936
|
+
};
|
|
17937
|
+
await options.onUsage({
|
|
17938
|
+
...spend,
|
|
17939
|
+
source: "openai",
|
|
17940
|
+
lineItemType: "llm_tokens",
|
|
17941
|
+
operation: "chat.completions",
|
|
17942
|
+
requestId: requestId || null,
|
|
17943
|
+
usageContext: mergedUsageContext,
|
|
17944
|
+
rawUsage
|
|
17945
|
+
});
|
|
17946
|
+
};
|
|
17689
17947
|
return async (input) => {
|
|
17690
17948
|
const messages = [
|
|
17691
17949
|
{
|
|
@@ -17702,80 +17960,46 @@ ${modelOutputInstruction()}`
|
|
|
17702
17960
|
messages,
|
|
17703
17961
|
response_format: { type: "json_object" }
|
|
17704
17962
|
};
|
|
17705
|
-
if (input.onTextDelta) {
|
|
17706
|
-
payload.stream = true;
|
|
17707
|
-
}
|
|
17708
17963
|
if (typeof options.temperature === "number") {
|
|
17709
17964
|
payload.temperature = options.temperature;
|
|
17710
17965
|
}
|
|
17711
17966
|
let lastError = null;
|
|
17712
17967
|
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
17713
17968
|
try {
|
|
17714
|
-
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
17715
|
-
method: "POST",
|
|
17716
|
-
headers: {
|
|
17717
|
-
"content-type": "application/json",
|
|
17718
|
-
authorization: `Bearer ${options.apiKey}`,
|
|
17719
|
-
...options.headers
|
|
17720
|
-
},
|
|
17721
|
-
body: JSON.stringify(payload)
|
|
17722
|
-
});
|
|
17723
|
-
if (!response.ok) {
|
|
17724
|
-
const errorText = await response.text();
|
|
17725
|
-
if (attempt < 3 && (response.status >= 500 || response.status === 429)) {
|
|
17726
|
-
await sleep2(500 * attempt);
|
|
17727
|
-
continue;
|
|
17728
|
-
}
|
|
17729
|
-
throw new Error(
|
|
17730
|
-
`OpenAI chat generation failed: ${response.status} ${errorText}`
|
|
17731
|
-
);
|
|
17732
|
-
}
|
|
17733
17969
|
let raw;
|
|
17734
17970
|
let text = "";
|
|
17735
|
-
|
|
17971
|
+
let usage = null;
|
|
17972
|
+
let requestId = null;
|
|
17973
|
+
if (input.onTextDelta) {
|
|
17736
17974
|
const onTextDelta = input.onTextDelta;
|
|
17737
|
-
const
|
|
17738
|
-
|
|
17739
|
-
|
|
17740
|
-
|
|
17741
|
-
|
|
17742
|
-
|
|
17743
|
-
|
|
17744
|
-
|
|
17745
|
-
const
|
|
17746
|
-
const
|
|
17747
|
-
|
|
17748
|
-
)?.content;
|
|
17749
|
-
const deltaText = typeof delta === "string" ? delta : Array.isArray(delta) ? delta.map((part) => asRecord5(part)?.text || "").join("") : "";
|
|
17750
|
-
if (!deltaText) return;
|
|
17975
|
+
const stream = await client.chat.completions.create({
|
|
17976
|
+
...payload,
|
|
17977
|
+
stream: true,
|
|
17978
|
+
stream_options: { include_usage: true }
|
|
17979
|
+
});
|
|
17980
|
+
for await (const event of stream) {
|
|
17981
|
+
requestId = requestId || event.id || event._request_id || null;
|
|
17982
|
+
usage = event.usage || usage;
|
|
17983
|
+
const delta = event.choices?.[0]?.delta?.content;
|
|
17984
|
+
const deltaText = typeof delta === "string" ? delta : Array.isArray(delta) ? delta.map((part) => asRecord6(part)?.text || "").join("") : "";
|
|
17985
|
+
if (!deltaText) continue;
|
|
17751
17986
|
text += deltaText;
|
|
17752
17987
|
await onTextDelta(deltaText);
|
|
17753
|
-
};
|
|
17754
|
-
while (true) {
|
|
17755
|
-
const { value, done } = await reader.read();
|
|
17756
|
-
if (done) break;
|
|
17757
|
-
buffer += decoder.decode(value, { stream: true });
|
|
17758
|
-
while (true) {
|
|
17759
|
-
const lineEnd = buffer.indexOf("\n");
|
|
17760
|
-
if (lineEnd === -1) break;
|
|
17761
|
-
const line = buffer.slice(0, lineEnd);
|
|
17762
|
-
buffer = buffer.slice(lineEnd + 1);
|
|
17763
|
-
await processStreamLine(line);
|
|
17764
|
-
}
|
|
17765
17988
|
}
|
|
17766
|
-
|
|
17767
|
-
if (buffer.trim()) {
|
|
17768
|
-
await processStreamLine(buffer);
|
|
17769
|
-
}
|
|
17770
|
-
raw = { streamed: true };
|
|
17989
|
+
raw = { streamed: true, model, usage, request_id: requestId };
|
|
17771
17990
|
} else {
|
|
17772
|
-
const
|
|
17773
|
-
|
|
17774
|
-
|
|
17775
|
-
|
|
17991
|
+
const completion = await client.chat.completions.create(
|
|
17992
|
+
payload
|
|
17993
|
+
);
|
|
17994
|
+
raw = completion;
|
|
17995
|
+
usage = completion.usage;
|
|
17996
|
+
requestId = (typeof completion.id === "string" ? completion.id : null) || (typeof completion._request_id === "string" ? completion._request_id : null);
|
|
17997
|
+
const content = asRecord6(
|
|
17998
|
+
asRecord6(completion.choices?.[0])?.message
|
|
17776
17999
|
)?.content;
|
|
17777
|
-
text = typeof content === "string" ? content : Array.isArray(content) ? content.map((part) =>
|
|
18000
|
+
text = typeof content === "string" ? content : Array.isArray(content) ? content.map((part) => asRecord6(part)?.text || "").join("") : "";
|
|
17778
18001
|
}
|
|
18002
|
+
await emitUsage(usage, requestId, input.usageContext);
|
|
17779
18003
|
const parsed = extractJsonObject(text);
|
|
17780
18004
|
if (!parsed) {
|
|
17781
18005
|
if (attempt < 3) {
|
|
@@ -17795,9 +18019,10 @@ ${modelOutputInstruction()}`
|
|
|
17795
18019
|
};
|
|
17796
18020
|
} catch (error) {
|
|
17797
18021
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
17798
|
-
|
|
18022
|
+
const status = Number(error?.status);
|
|
18023
|
+
if (attempt < 3 && (Number.isFinite(status) && (status >= 500 || status === 429) || /socket connection was closed unexpectedly|ECONNRESET|network|timed out/i.test(
|
|
17799
18024
|
lastError.message
|
|
17800
|
-
)) {
|
|
18025
|
+
))) {
|
|
17801
18026
|
await sleep2(500 * attempt);
|
|
17802
18027
|
continue;
|
|
17803
18028
|
}
|
|
@@ -17830,12 +18055,12 @@ async function withTimeout2(promise, ms, label) {
|
|
|
17830
18055
|
}
|
|
17831
18056
|
}
|
|
17832
18057
|
function getActionSummary(liveDoc, jobId) {
|
|
17833
|
-
const jobsById =
|
|
17834
|
-
const job =
|
|
18058
|
+
const jobsById = asRecord6(asRecord6(liveDoc?.jobs)?.byId) || {};
|
|
18059
|
+
const job = asRecord6(jobsById[jobId]);
|
|
17835
18060
|
const summary = Array.isArray(job?.actionSummary) ? job.actionSummary.filter(
|
|
17836
18061
|
(line) => typeof line === "string"
|
|
17837
18062
|
) : [];
|
|
17838
|
-
const trace = Array.isArray(job?.actionTrace) ? job.actionTrace.map((event) =>
|
|
18063
|
+
const trace = Array.isArray(job?.actionTrace) ? job.actionTrace.map((event) => asRecord6(event)).filter((event) => Boolean(event)) : [];
|
|
17839
18064
|
const traceLines = trace.map((event) => {
|
|
17840
18065
|
const kind = typeof event.kind === "string" ? event.kind : "";
|
|
17841
18066
|
const action = typeof event.action === "string" ? event.action : "";
|
|
@@ -17856,9 +18081,9 @@ function getActionSummary(liveDoc, jobId) {
|
|
|
17856
18081
|
}
|
|
17857
18082
|
function normalizeHeapSnapshot2(heap) {
|
|
17858
18083
|
return {
|
|
17859
|
-
entriesByPath:
|
|
17860
|
-
listsByName:
|
|
17861
|
-
variablesByName:
|
|
18084
|
+
entriesByPath: asRecord6(heap?.entriesByPath) || {},
|
|
18085
|
+
listsByName: asRecord6(heap?.listsByName) || {},
|
|
18086
|
+
variablesByName: asRecord6(heap?.variablesByName) || {},
|
|
17862
18087
|
updatedAt: typeof heap?.updatedAt === "number" ? heap.updatedAt : Date.now()
|
|
17863
18088
|
};
|
|
17864
18089
|
}
|
|
@@ -17893,9 +18118,9 @@ async function waitForJobOutcome(input) {
|
|
|
17893
18118
|
input.boundaryTimestamp
|
|
17894
18119
|
);
|
|
17895
18120
|
lastPromptCount = prompts.length;
|
|
17896
|
-
const messages = asArray3(
|
|
18121
|
+
const messages = asArray3(asRecord6(liveDoc.conversation)?.messages);
|
|
17897
18122
|
lastMessageCount = messages.length;
|
|
17898
|
-
lastJobSummary =
|
|
18123
|
+
lastJobSummary = asRecord6(asRecord6(liveDoc.jobs)?.byId)?.[input.job.id] || null;
|
|
17899
18124
|
if (prompts.length > 0) {
|
|
17900
18125
|
return { kind: "prompt", prompts, liveDoc, stdout, stderr };
|
|
17901
18126
|
}
|
|
@@ -18079,7 +18304,8 @@ function jsonBlock(value) {
|
|
|
18079
18304
|
}
|
|
18080
18305
|
function buildSessionLogReport(input) {
|
|
18081
18306
|
const { conversation, result, error } = input;
|
|
18082
|
-
const
|
|
18307
|
+
const logTurns = conversation.logTurns || [];
|
|
18308
|
+
const systemPrompts = logTurns.flatMap(
|
|
18083
18309
|
(turn) => turn.iterations.map((iteration) => ({
|
|
18084
18310
|
turn,
|
|
18085
18311
|
iteration
|
|
@@ -18108,7 +18334,7 @@ function buildSessionLogReport(input) {
|
|
|
18108
18334
|
"",
|
|
18109
18335
|
"## Conversation"
|
|
18110
18336
|
];
|
|
18111
|
-
for (const turn of
|
|
18337
|
+
for (const turn of logTurns) {
|
|
18112
18338
|
lines.push("", `### Turn ${turn.turnNumber}: ${turn.turnId}`, "");
|
|
18113
18339
|
lines.push("**User**", "");
|
|
18114
18340
|
lines.push(turn.request, "");
|
|
@@ -18306,7 +18532,7 @@ async function runAgentEvalSuite(options) {
|
|
|
18306
18532
|
promptInteractions: completed.promptInteractions,
|
|
18307
18533
|
result: completed.result,
|
|
18308
18534
|
heap: normalizeHeapSnapshot2(
|
|
18309
|
-
|
|
18535
|
+
asRecord6(
|
|
18310
18536
|
cloneJson(conversation.environment.document)?.heap
|
|
18311
18537
|
)
|
|
18312
18538
|
),
|
|
@@ -18416,7 +18642,7 @@ async function runAgentEvalSuite(options) {
|
|
|
18416
18642
|
finalResult = result;
|
|
18417
18643
|
} catch (error) {
|
|
18418
18644
|
const failureMessage = error instanceof Error ? error.message : String(error);
|
|
18419
|
-
const failedTurn = conversation.logTurns[conversation.logTurns.length - 1];
|
|
18645
|
+
const failedTurn = conversation.logTurns?.[conversation.logTurns.length - 1];
|
|
18420
18646
|
if (failedTurn && !failedTurn.completed) {
|
|
18421
18647
|
failedTurn.error = failureMessage;
|
|
18422
18648
|
const failedIteration = latestIterationLog(failedTurn);
|
|
@@ -18549,7 +18775,7 @@ function createAgentEvalHarness(options) {
|
|
|
18549
18775
|
}
|
|
18550
18776
|
function buildCheckContext(conversation, completed, turnDir) {
|
|
18551
18777
|
const liveDoc = cloneJson(conversation.environment.document);
|
|
18552
|
-
const heap = normalizeHeapSnapshot2(
|
|
18778
|
+
const heap = normalizeHeapSnapshot2(asRecord6(liveDoc?.heap));
|
|
18553
18779
|
return {
|
|
18554
18780
|
conversation,
|
|
18555
18781
|
environment: conversation.environment,
|
|
@@ -18643,7 +18869,7 @@ function createAgentEvalHarness(options) {
|
|
|
18643
18869
|
result: resumed.result,
|
|
18644
18870
|
stdout: [...pending.stdout, ...resumed.stdout],
|
|
18645
18871
|
agentMessages: getJobAgentMessages(liveDoc, pending.job.id),
|
|
18646
|
-
sessionHeap: normalizeHeapSnapshot2(
|
|
18872
|
+
sessionHeap: normalizeHeapSnapshot2(asRecord6(liveDoc?.heap))
|
|
18647
18873
|
});
|
|
18648
18874
|
const responseText = presentation.responseText || pending.finalReply || "Done.";
|
|
18649
18875
|
pending.conversation.history.push({
|
|
@@ -18767,7 +18993,7 @@ function createAgentEvalHarness(options) {
|
|
|
18767
18993
|
environmentId: conversation.environment.environmentId,
|
|
18768
18994
|
domainRevision: conversation.environment.domainRevision
|
|
18769
18995
|
},
|
|
18770
|
-
heapSummary: projectHeapSummary(
|
|
18996
|
+
heapSummary: projectHeapSummary(asRecord6(liveDoc?.heap), {
|
|
18771
18997
|
focus: heapFocus
|
|
18772
18998
|
}),
|
|
18773
18999
|
referentSummary: projectConversationReferentSummary(liveDoc),
|
|
@@ -18789,7 +19015,14 @@ function createAgentEvalHarness(options) {
|
|
|
18789
19015
|
history: buildHistory(conversation.history),
|
|
18790
19016
|
request,
|
|
18791
19017
|
attempt: 1,
|
|
18792
|
-
tools
|
|
19018
|
+
tools,
|
|
19019
|
+
usageContext: {
|
|
19020
|
+
sandboxId: conversation.environment.sandboxId,
|
|
19021
|
+
environmentId: conversation.environment.environmentId,
|
|
19022
|
+
sessionId: conversation.environment.sessionId,
|
|
19023
|
+
subjectId: conversation.environment.subjectId,
|
|
19024
|
+
permissionProfileId: conversation.environment.permissionProfileId
|
|
19025
|
+
}
|
|
18793
19026
|
}),
|
|
18794
19027
|
chatTimeoutMs,
|
|
18795
19028
|
`chat generation for ${conversation.label} iteration ${iteration + 1}`
|
|
@@ -18938,7 +19171,7 @@ function createAgentEvalHarness(options) {
|
|
|
18938
19171
|
const settledLiveDoc = cloneJson(
|
|
18939
19172
|
conversation.environment.document
|
|
18940
19173
|
);
|
|
18941
|
-
const sessionHeap = normalizeHeapSnapshot2(
|
|
19174
|
+
const sessionHeap = normalizeHeapSnapshot2(asRecord6(settledLiveDoc?.heap));
|
|
18942
19175
|
const presentation = resolveJobPresentation({
|
|
18943
19176
|
jobId: job.id,
|
|
18944
19177
|
result: outcome.result,
|
|
@@ -19062,7 +19295,10 @@ function createAgentTester(options) {
|
|
|
19062
19295
|
model: options.openai?.model || options.model,
|
|
19063
19296
|
baseUrl: options.openai?.baseUrl,
|
|
19064
19297
|
temperature: options.openai?.temperature,
|
|
19065
|
-
headers: options.openai?.headers
|
|
19298
|
+
headers: options.openai?.headers,
|
|
19299
|
+
onUsage: async (usage) => {
|
|
19300
|
+
await granular.recordOpenAIUsageSpend(usage, usage.usageContext);
|
|
19301
|
+
}
|
|
19066
19302
|
});
|
|
19067
19303
|
let resolvedEnvironmentId = "environmentId" in options.target ? options.target.environmentId : null;
|
|
19068
19304
|
let connectSeeded = false;
|