mixdog 0.9.175 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/context-renewal-live-check.mjs +2 -1
- package/scripts/plan-runtime-release-tag.mjs +57 -0
- package/src/defaults/skills/xlsx/SKILL.md +1 -0
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +7 -12
- package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +20 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -0
- package/src/runtime/agent/orchestrator/runtime-core/goal-tasks.mjs +12 -1
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-status.mjs +7 -3
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/benign-exit.mjs +7 -3
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +39 -3
- package/src/runtime/memory/lib/runtime-fetcher.mjs +94 -23
- package/src/runtime/office/capabilities-catalog.mjs +2 -0
- package/src/runtime/office/capabilities-signatures.mjs +12 -6
- package/src/runtime/office/capabilities.mjs +30 -10
- package/src/runtime/office/core/office-actions-open.mjs +1 -6
- package/src/runtime/office/core/office-sessions.mjs +9 -0
- package/src/runtime/office/portable/portable-xlsx-operations.mjs +2 -49
- package/src/runtime/office/portable/xlsx-contract.mjs +37 -3
- package/src/runtime/office/tool-defs.mjs +2 -2
- package/src/runtime/shared/llm/cost.mjs +16 -4
- package/src/runtime/shared/llm/model-catalog-projection.mjs +3 -2
- package/src/runtime/shared/llm/model-catalog.mjs +48 -0
- package/src/runtime/shared/llm/model-pricing-rates.mjs +11 -4
- package/src/runtime/shared/llm/usage-accounting.mjs +12 -11
- package/src/runtime/shared/llm/usage-context.mjs +7 -0
- package/src/runtime/shared/llm/usage-ledger-repair.mjs +3 -1
- package/src/runtime/shared/llm/usage-ledger.mjs +1 -1
- package/src/runtime/shared/tool-card-model/card-status.mjs +7 -5
- package/src/runtime/shared/tool-card-model/git-result.mjs +10 -0
- package/src/runtime/shared/tool-card-model/terminal-status.mjs +6 -2
- package/src/runtime/shared/tool-card-model.golden-cases.mjs +2 -1
- package/src/runtime/shared/tool-result-summary.mjs +1 -0
- package/src/runtime/shared/tool-status.mjs +35 -2
- package/src/runtime/tidy/report.mjs +2 -2
- package/src/runtime/tidy/tool.mjs +6 -1
- package/src/tui/dist/index.mjs +88 -37
- package/src/tui/session/session-api/settings.mjs +16 -10
- package/src/vendor/statusline/src/gateway/route-meta.mjs +7 -2
|
@@ -139,6 +139,7 @@ export const CATALOG = {
|
|
|
139
139
|
properties: {
|
|
140
140
|
paragraph: ['style'],
|
|
141
141
|
font: ['name', 'nameEastAsia', 'size', 'bold', 'italic', 'underline', 'color', 'hidden'],
|
|
142
|
+
documentFont: ['name', 'nameEastAsia', 'size', 'color'],
|
|
142
143
|
page: [
|
|
143
144
|
'pageSize',
|
|
144
145
|
'orientation',
|
|
@@ -169,6 +170,7 @@ export const CATALOG = {
|
|
|
169
170
|
'headerRows',
|
|
170
171
|
'keepWithNext',
|
|
171
172
|
],
|
|
173
|
+
tableStyle: ['style', 'alignment', 'columnAlignments', 'columnWidths', 'borders', 'shading'],
|
|
172
174
|
tableCell: [
|
|
173
175
|
'fillColor',
|
|
174
176
|
'horizontalAlignment',
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
// Operation input signatures: which fields each Office operation requires,
|
|
2
2
|
// accepts, or takes as alternatives, per format.
|
|
3
|
+
import { XLSX_VALIDATION_OPERATORS, XLSX_VALIDATION_TYPES } from './portable/xlsx-contract.mjs';
|
|
3
4
|
|
|
4
|
-
export function signature(
|
|
5
|
-
|
|
5
|
+
export function signature(
|
|
6
|
+
required = [],
|
|
7
|
+
optional = [],
|
|
8
|
+
{ oneOf = [], propertySets = [], propertyBackends = {}, notes = '' } = {}
|
|
9
|
+
) {
|
|
10
|
+
return { required, optional, oneOf, propertySets, propertyBackends, notes };
|
|
6
11
|
}
|
|
7
12
|
|
|
8
13
|
export const COMMON_SIGNATURES = {
|
|
@@ -66,9 +71,10 @@ export const FORMAT_SIGNATURES = {
|
|
|
66
71
|
'properties.alignment places the table on the page (left, center, right); properties.columnAlignments sets the text of each column (one of left, center, right, justify per column); without it a column of figures (184,200, 2.1%, 2.6억 원) sets right and the rest left. Without style, borders, or shading the table takes a bold header row with a rule under it and hairlines between rows; headerBold:false keeps the header plain. properties.headerRows (default 1) is how many rows the header takes: a two-level header, its group label merged across the columns it spans (merge_table_cells), is headerRows:2, and every header row is set bold on its bottom edge and repeats on a continuation page.',
|
|
67
72
|
}),
|
|
68
73
|
set_table_style: signature(['table', 'properties'], [], {
|
|
69
|
-
propertySets: ['
|
|
74
|
+
propertySets: ['tableStyle'],
|
|
75
|
+
propertyBackends: { columnWidths: ['microsoft-office-com'] },
|
|
70
76
|
notes:
|
|
71
|
-
'Replaces
|
|
77
|
+
'Replaces table-level properties; columnAlignments re-aligns existing cells. columnWidths is Microsoft Office only. For existing cell fonts or fills use set_table_cell_style, not set_table_style.',
|
|
72
78
|
}),
|
|
73
79
|
merge_table_cells: signature(['table', 'row', 'col'], ['rowSpan', 'colSpan'], {
|
|
74
80
|
notes:
|
|
@@ -91,7 +97,7 @@ export const FORMAT_SIGNATURES = {
|
|
|
91
97
|
move_paragraph: signature(['paragraph', 'index']),
|
|
92
98
|
set_paragraph_style: signature(['paragraph', 'style']),
|
|
93
99
|
set_document_font: signature(['properties'], [], {
|
|
94
|
-
propertySets: ['
|
|
100
|
+
propertySets: ['documentFont'],
|
|
95
101
|
notes:
|
|
96
102
|
"The document's own face, size, and ink — what every paragraph without its own reads, and what a paragraph added later in Word or by append_text starts from. properties: name, nameEastAsia, size, color.",
|
|
97
103
|
}),
|
|
@@ -325,7 +331,7 @@ export const FORMAT_SIGNATURES = {
|
|
|
325
331
|
['sheet', 'type', 'operator', 'formula2', 'inputMessage', 'errorMessage'],
|
|
326
332
|
{
|
|
327
333
|
notes:
|
|
328
|
-
|
|
334
|
+
`type: ${XLSX_VALIDATION_TYPES.join(', ')}. Without type, choices ("a,b,c" or $A$1:$A$9) mean list; other formulas mean custom. Numeric/date/time/textLength rules take operator: ${XLSX_VALIDATION_OPERATORS.join(', ')} (default between). between/notBetween require formula2; other operators use formula1. Example: type:"whole", operator:"greaterThanOrEqual", formula1:"0". Under protect_sheet unlock entry cells with set_style properties:{ locked:false }.`,
|
|
329
335
|
}
|
|
330
336
|
),
|
|
331
337
|
freeze_panes: signature([], ['sheet', 'row', 'column'], {
|
|
@@ -3,6 +3,7 @@ import { plainObject } from './shared/values.mjs';
|
|
|
3
3
|
import { FIELD_ALIASES, OPERATION_ALIASES, PROPERTY_ALIASES } from './capabilities-aliases.mjs';
|
|
4
4
|
import { BACKENDS, CATALOG, COMMON, VIRTUAL_OPERATIONS } from './capabilities-catalog.mjs';
|
|
5
5
|
import { COMMON_SIGNATURES, FORMAT_SIGNATURES, signature } from './capabilities-signatures.mjs';
|
|
6
|
+
import { normalizeXlsxValidation } from './portable/xlsx-contract.mjs';
|
|
6
7
|
|
|
7
8
|
export { OFFICE_ACTIONS } from './capabilities-catalog.mjs';
|
|
8
9
|
|
|
@@ -141,6 +142,19 @@ function describeHint(format, backend, operation) {
|
|
|
141
142
|
})}.`;
|
|
142
143
|
}
|
|
143
144
|
|
|
145
|
+
function operationProperties(catalog, signatureValue, backend) {
|
|
146
|
+
return Object.fromEntries(
|
|
147
|
+
signatureValue.propertySets
|
|
148
|
+
.filter((name) => catalog.properties[name])
|
|
149
|
+
.map((name) => [
|
|
150
|
+
name,
|
|
151
|
+
catalog.properties[name].filter(
|
|
152
|
+
(field) => !backend || !signatureValue.propertyBackends[field] || signatureValue.propertyBackends[field].includes(backend)
|
|
153
|
+
),
|
|
154
|
+
])
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
144
158
|
function operationDescription(format, backend, catalog, requested) {
|
|
145
159
|
const knownOperations = catalogOperations(format);
|
|
146
160
|
const operation = resolveOperationAlias(format, requested);
|
|
@@ -152,11 +166,7 @@ function operationDescription(format, backend, catalog, requested) {
|
|
|
152
166
|
}
|
|
153
167
|
const available = operationsForBackend(format, backend);
|
|
154
168
|
const signatureValue = operationSignature(format, operation);
|
|
155
|
-
const properties =
|
|
156
|
-
signatureValue.propertySets
|
|
157
|
-
.filter((name) => catalog.properties[name])
|
|
158
|
-
.map((name) => [name, catalog.properties[name]])
|
|
159
|
-
);
|
|
169
|
+
const properties = operationProperties(catalog, signatureValue, backend);
|
|
160
170
|
return {
|
|
161
171
|
name: operation,
|
|
162
172
|
...(VIRTUAL_OPERATIONS.has(operation) || VIRTUAL_OPERATIONS.has(`${format}:${operation}`) ? { virtual: true } : {}),
|
|
@@ -311,10 +321,10 @@ function hoistGeometryProperties(signatureValue, operation, allowed) {
|
|
|
311
321
|
delete operation.properties;
|
|
312
322
|
}
|
|
313
323
|
|
|
314
|
-
function propertyKeySet(catalog, signatureValue) {
|
|
324
|
+
function propertyKeySet(catalog, signatureValue, backend) {
|
|
315
325
|
return new Set(
|
|
316
|
-
signatureValue
|
|
317
|
-
.
|
|
326
|
+
Object.values(operationProperties(catalog, signatureValue, backend))
|
|
327
|
+
.flat()
|
|
318
328
|
.map((entry) => String(entry).split('.')[0])
|
|
319
329
|
);
|
|
320
330
|
}
|
|
@@ -474,14 +484,24 @@ function operationContractFaults(batch, operation, index) {
|
|
|
474
484
|
if (cellFault) return [cellFault];
|
|
475
485
|
hoistComposeSheetTable(format, name, operation);
|
|
476
486
|
hoistGeometryProperties(signatureValue, operation, allowed);
|
|
477
|
-
const propertyKeys = propertyKeySet(catalog, signatureValue);
|
|
487
|
+
const propertyKeys = propertyKeySet(catalog, signatureValue, backend);
|
|
478
488
|
const missing = signatureValue.required.filter((field) => !suppliedField(operation, field, stableTargets));
|
|
479
|
-
|
|
489
|
+
const faults = [
|
|
480
490
|
unknownFieldsFault(batch, name, operation, index, { allowed, propertyKeys, missing }),
|
|
481
491
|
unknownPropertiesFault(batch, name, operation, index, propertyKeys),
|
|
482
492
|
...docxTableAlignmentFaults(format, name, operation, index),
|
|
483
493
|
requiredInputFault(batch, name, operation, index, signatureValue, stableTargets),
|
|
484
494
|
].filter(Boolean);
|
|
495
|
+
if (!faults.length && format === 'xlsx' && name === 'add_validation') {
|
|
496
|
+
try {
|
|
497
|
+
Object.assign(operation, normalizeXlsxValidation(operation));
|
|
498
|
+
} catch (error) {
|
|
499
|
+
faults.push(
|
|
500
|
+
`XLSX operation "${name}" at operation ${index + 1}: ${error.message}. ${describeHint(format, backend, name)}`
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return faults;
|
|
485
505
|
}
|
|
486
506
|
|
|
487
507
|
export function assertOfficeOperationContracts({ format = '', backend = '', operations = [] } = {}) {
|
|
@@ -10,12 +10,7 @@ import {
|
|
|
10
10
|
releaseOfficeSession,
|
|
11
11
|
toolResult,
|
|
12
12
|
} from './office-core.mjs';
|
|
13
|
-
import { createSession, openSession, snapshot } from './office-sessions.mjs';
|
|
14
|
-
|
|
15
|
-
function initialOfficeOperations(args) {
|
|
16
|
-
if (Array.isArray(args.operations) && args.operations.length) return args.operations;
|
|
17
|
-
return Array.isArray(args.design?.operations) ? args.design.operations : [];
|
|
18
|
-
}
|
|
13
|
+
import { createSession, initialOfficeOperations, openSession, snapshot } from './office-sessions.mjs';
|
|
19
14
|
|
|
20
15
|
async function discardFailedOfficeSession(session, { action, initialEditSettled }) {
|
|
21
16
|
if (session.reused) return;
|
|
@@ -3,6 +3,7 @@ import { constants as fsConstants } from 'node:fs';
|
|
|
3
3
|
import { basename, dirname, extname, isAbsolute, join, resolve } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
|
+
import { assertOfficeOperationContracts } from '../capabilities.mjs';
|
|
6
7
|
import {
|
|
7
8
|
callMicrosoftOffice,
|
|
8
9
|
detectMicrosoftOffice,
|
|
@@ -496,6 +497,11 @@ async function createMicrosoftOfficeSession(creation, args, mode) {
|
|
|
496
497
|
});
|
|
497
498
|
}
|
|
498
499
|
|
|
500
|
+
export function initialOfficeOperations(args) {
|
|
501
|
+
if (Array.isArray(args.operations) && args.operations.length) return args.operations;
|
|
502
|
+
return Array.isArray(args.design?.operations) ? args.design.operations : [];
|
|
503
|
+
}
|
|
504
|
+
|
|
499
505
|
export async function createSession(args, cwd, dataDir) {
|
|
500
506
|
const requestedPath = String(args.path || args.output || '').trim();
|
|
501
507
|
if (!requestedPath) throw new Error('create requires path or output');
|
|
@@ -506,6 +512,8 @@ export async function createSession(args, cwd, dataDir) {
|
|
|
506
512
|
if (format !== inferredFormat)
|
|
507
513
|
throw new Error(`Office create format ${args.format} does not match target .${fileKind}`);
|
|
508
514
|
assertCreateContentFields(format, args);
|
|
515
|
+
const initialOperations = initialOfficeOperations(args);
|
|
516
|
+
assertOfficeOperationContracts({ format, operations: initialOperations });
|
|
509
517
|
// Whether this call brings the file into being decides what a failed create
|
|
510
518
|
// may clean up afterwards: a file it wrote itself, never one already there.
|
|
511
519
|
const targetExisted = await exists(target);
|
|
@@ -528,6 +536,7 @@ export async function createSession(args, cwd, dataDir) {
|
|
|
528
536
|
throw new Error('Office create requires visible, background, or portable mode');
|
|
529
537
|
}
|
|
530
538
|
const selected = await selectMode(requestedMode, format, target);
|
|
539
|
+
assertOfficeOperationContracts({ format, backend: selected.backend, operations: initialOperations });
|
|
531
540
|
if (selected.backend === 'mixdog-ooxml') return createPortableOoxmlSession(creation);
|
|
532
541
|
return createMicrosoftOfficeSession(creation, args, selected.mode);
|
|
533
542
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Worksheet-level operations the portable XLSX backend applies: sheet
|
|
2
2
|
// management, images, header/footer, conditional formats, validation, sort,
|
|
3
3
|
// autofit, pivot tables and charts.
|
|
4
|
-
import { conditionalFormatKind, listValidationChoices,
|
|
4
|
+
import { conditionalFormatKind, listValidationChoices, normalizeXlsxValidation } from './xlsx-contract.mjs';
|
|
5
5
|
import { nextRelationshipId, partRelationshipPath, zipText } from './portable-opc.mjs';
|
|
6
6
|
import {
|
|
7
7
|
OFFICE_RELATIONSHIP_BASE,
|
|
@@ -45,29 +45,6 @@ function headerFooterFields(text) {
|
|
|
45
45
|
.replace(/\{file\}/gi, '&F');
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
// Cell validation as both backends express it: the OOXML names here, the Excel
|
|
49
|
-
// enumeration in the COM host.
|
|
50
|
-
const XLSX_VALIDATION_TYPES = Object.freeze({
|
|
51
|
-
list: 'list',
|
|
52
|
-
whole: 'whole',
|
|
53
|
-
decimal: 'decimal',
|
|
54
|
-
date: 'date',
|
|
55
|
-
time: 'time',
|
|
56
|
-
textlength: 'textLength',
|
|
57
|
-
custom: 'custom',
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
const XLSX_VALIDATION_OPERATORS = Object.freeze({
|
|
61
|
-
between: 'between',
|
|
62
|
-
notbetween: 'notBetween',
|
|
63
|
-
equal: 'equal',
|
|
64
|
-
notequal: 'notEqual',
|
|
65
|
-
greaterthan: 'greaterThan',
|
|
66
|
-
lessthan: 'lessThan',
|
|
67
|
-
greaterthanorequal: 'greaterThanOrEqual',
|
|
68
|
-
lessthanorequal: 'lessThanOrEqual',
|
|
69
|
-
});
|
|
70
|
-
|
|
71
48
|
export const TABLE_CONTENT_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml';
|
|
72
49
|
|
|
73
50
|
// A built-in table style bands the range from the workbook's own theme, which
|
|
@@ -330,18 +307,6 @@ async function expressionConditionalRule(zip, op, priority) {
|
|
|
330
307
|
|
|
331
308
|
// The comparison a bounded validation applies; a second bound without one
|
|
332
309
|
// means between.
|
|
333
|
-
function validationOperator(op, type) {
|
|
334
|
-
const requested = String(op.operator || '').trim();
|
|
335
|
-
let operator = '';
|
|
336
|
-
if (requested && Object.hasOwn(XLSX_VALIDATION_OPERATORS, requested.toLowerCase())) {
|
|
337
|
-
operator = XLSX_VALIDATION_OPERATORS[requested.toLowerCase()];
|
|
338
|
-
} else if (op.formula2 != null && !['list', 'custom'].includes(type)) operator = 'between';
|
|
339
|
-
if (requested && !operator) {
|
|
340
|
-
throw new Error(`add_validation operator must be one of ${Object.keys(XLSX_VALIDATION_OPERATORS).join(', ')}`);
|
|
341
|
-
}
|
|
342
|
-
return operator;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
310
|
function dataValidationXml(op, { type, operator, reference }) {
|
|
346
311
|
const formula = (value) => `${xmlEncode(String(value).replace(/^=/, ''))}`;
|
|
347
312
|
return (
|
|
@@ -359,19 +324,7 @@ function dataValidationXml(op, { type, operator, reference }) {
|
|
|
359
324
|
/** One data validation over a range, appended to the validations already there. */
|
|
360
325
|
export function addWorksheetValidation(zip, sheet, xml, op) {
|
|
361
326
|
const reference = areaReference(parseAreaRange(op.range));
|
|
362
|
-
|
|
363
|
-
// operation, so it is the default; the other kinds guard a number, a
|
|
364
|
-
// date, or a length, and take a second bound. A formula that states a
|
|
365
|
-
// rule rather than naming choices is that rule, not a dropdown of one
|
|
366
|
-
// entry: "서울,부산" and $A$1:$A$9 are lists, B2>0 is a custom check.
|
|
367
|
-
const kind = String(op.type || (listValidationFormula(op.formula1) ? 'list' : 'custom'))
|
|
368
|
-
.trim()
|
|
369
|
-
.toLowerCase();
|
|
370
|
-
const type = Object.hasOwn(XLSX_VALIDATION_TYPES, kind) ? XLSX_VALIDATION_TYPES[kind] : '';
|
|
371
|
-
if (!type) {
|
|
372
|
-
throw new Error(`add_validation type must be one of ${Object.keys(XLSX_VALIDATION_TYPES).join(', ')}`);
|
|
373
|
-
}
|
|
374
|
-
const operator = validationOperator(op, type);
|
|
327
|
+
const { type, operator = '' } = normalizeXlsxValidation(op);
|
|
375
328
|
const existing = worksheetSection(xml, 'dataValidations');
|
|
376
329
|
const previous = existing ? containerBody(existing[0], 'dataValidations') : '';
|
|
377
330
|
const count = (previous.match(/<dataValidation\b/g) || []).length + 1;
|
|
@@ -246,6 +246,42 @@ export function listValidationChoices(formula1) {
|
|
|
246
246
|
return `"${text}"`;
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
export const XLSX_VALIDATION_TYPES = Object.freeze(['list', 'whole', 'decimal', 'date', 'time', 'textLength', 'custom']);
|
|
250
|
+
export const XLSX_VALIDATION_OPERATORS = Object.freeze([
|
|
251
|
+
'between',
|
|
252
|
+
'notBetween',
|
|
253
|
+
'equal',
|
|
254
|
+
'notEqual',
|
|
255
|
+
'greaterThan',
|
|
256
|
+
'lessThan',
|
|
257
|
+
'greaterThanOrEqual',
|
|
258
|
+
'lessThanOrEqual',
|
|
259
|
+
]);
|
|
260
|
+
|
|
261
|
+
// Resolve and validate once, before either Excel COM or the portable writer
|
|
262
|
+
// can interpret an unknown name as a different rule.
|
|
263
|
+
export function normalizeXlsxValidation(operation) {
|
|
264
|
+
if (!String(operation.formula1 ?? '').trim()) throw new Error('add_validation requires non-empty formula1');
|
|
265
|
+
const requestedType = String(operation.type ?? '').trim() || (listValidationFormula(operation.formula1) ? 'list' : 'custom');
|
|
266
|
+
const type = XLSX_VALIDATION_TYPES.find((value) => value.toLowerCase() === requestedType.toLowerCase());
|
|
267
|
+
if (!type) throw new Error(`add_validation type must be one of ${XLSX_VALIDATION_TYPES.join(', ')}`);
|
|
268
|
+
const requestedOperator = String(operation.operator ?? '').trim();
|
|
269
|
+
const bounded = !['list', 'custom'].includes(type);
|
|
270
|
+
const operator = requestedOperator
|
|
271
|
+
? XLSX_VALIDATION_OPERATORS.find((value) => value.toLowerCase() === requestedOperator.toLowerCase())
|
|
272
|
+
: bounded ? 'between' : '';
|
|
273
|
+
if (requestedOperator && !operator) {
|
|
274
|
+
throw new Error(`add_validation operator must be one of ${XLSX_VALIDATION_OPERATORS.join(', ')}`);
|
|
275
|
+
}
|
|
276
|
+
if (!bounded && requestedOperator) {
|
|
277
|
+
throw new Error(`add_validation operator is unsupported for type "${type}"; omit operator`);
|
|
278
|
+
}
|
|
279
|
+
if (['between', 'notBetween'].includes(operator) && !String(operation.formula2 ?? '').trim()) {
|
|
280
|
+
throw new Error(`add_validation operator "${operator}" requires formula2; for one bound use a comparison operator`);
|
|
281
|
+
}
|
|
282
|
+
return { type, ...(operator ? { operator } : {}) };
|
|
283
|
+
}
|
|
284
|
+
|
|
249
285
|
// Three ways a sheet marks its numbers: a rule that paints the cells it picks,
|
|
250
286
|
// a scale that colors every cell by where its value sits, and a bar drawn in
|
|
251
287
|
// the cell. The first needs a formula and a format; the other two carry their
|
|
@@ -394,9 +430,7 @@ function validateXlsxOperation(operation) {
|
|
|
394
430
|
// Both backends write the kind named here, so the default is settled once:
|
|
395
431
|
// a formula that names choices is a dropdown, a formula that states a test
|
|
396
432
|
// is a custom rule. Writing B2>0 as a list would offer it as one entry.
|
|
397
|
-
if (op === 'add_validation'
|
|
398
|
-
operation.type = listValidationFormula(operation.formula1) ? 'list' : 'custom';
|
|
399
|
-
}
|
|
433
|
+
if (op === 'add_validation') Object.assign(operation, normalizeXlsxValidation(operation));
|
|
400
434
|
if (op === 'set_style' && operation.cell) parseXlsxCell(operation.cell);
|
|
401
435
|
if (operation.range && RANGE_OPERATIONS.includes(op)) validateRangeOperation(operation, op);
|
|
402
436
|
else if (op === 'set_range') throw new Error('XLSX set_range requires range');
|
|
@@ -26,7 +26,7 @@ export const TOOL_DEFS = [
|
|
|
26
26
|
type: 'string',
|
|
27
27
|
enum: OFFICE_ACTIONS,
|
|
28
28
|
description:
|
|
29
|
-
'detect/describe discover; author:
|
|
29
|
+
'detect/describe discover; author: HTML/pptxgenjs PPTX; transactions/recover/begin/diff/commit/rollback checkpoints; create/attach/open; snapshot/get/query; batch edits; issues/qa/validate review; render exports PDF and page images; save/finalize/close; secure: PDF passwords.',
|
|
30
30
|
},
|
|
31
31
|
path: { type: 'string', description: 'Document path; relative paths resolve from the caller project.' },
|
|
32
32
|
script: { type: 'string', description: 'author: HTML slides or a pptxgenjs script (pptx skill).' },
|
|
@@ -75,7 +75,7 @@ export const TOOL_DEFS = [
|
|
|
75
75
|
description:
|
|
76
76
|
'auto defaults to background with Office, otherwise portable. Only explicit attach (alias live) co-edits an open file; visible opens a window; background edits a copy.',
|
|
77
77
|
},
|
|
78
|
-
output: { type: 'string', description: 'Output copy
|
|
78
|
+
output: { type: 'string', description: 'Output copy; render: exported PDF path. Defaults beside source.' },
|
|
79
79
|
target: { type: 'string', description: 'Stable path from snapshot/query, e.g. /body/p[2].' },
|
|
80
80
|
query: { type: 'string', description: 'Case-insensitive value search; with pdf-layout, where the text sits.' },
|
|
81
81
|
queryKind: {
|
|
@@ -64,7 +64,7 @@ export function priceUsage(args) {
|
|
|
64
64
|
pricingProvider: meta?.pricingProvider || identity.pricingProvider,
|
|
65
65
|
pricingSource: meta?.pricingSource || null,
|
|
66
66
|
...(args.inputTokensKnown === false ? { inputTokensKnown: false } : {}),
|
|
67
|
-
...(args.
|
|
67
|
+
...(args.requestServiceTier ? { requestServiceTier: args.requestServiceTier } : {}),
|
|
68
68
|
...(args.serviceTier ? { serviceTier: args.serviceTier } : {}),
|
|
69
69
|
...(written1h ? { cacheWrite1hTokens: written1h } : {}),
|
|
70
70
|
};
|
|
@@ -78,7 +78,19 @@ export function priceUsage(args) {
|
|
|
78
78
|
},
|
|
79
79
|
};
|
|
80
80
|
const promptTokens = input + cached + written;
|
|
81
|
-
|
|
81
|
+
// The tier the request itself carried decides its rate: subscription quota
|
|
82
|
+
// is charged by the tier sent, and an Anthropic fast request either runs
|
|
83
|
+
// fast or is rejected (its retry is a new, standard request). Only the
|
|
84
|
+
// OpenAI API bills a ramp-downgraded Priority request at Standard rates,
|
|
85
|
+
// reporting it as service_tier "default".
|
|
86
|
+
const sentTier = args.requestServiceTier || '';
|
|
87
|
+
const premium =
|
|
88
|
+
(sentTier === 'priority' || sentTier === 'fast') && !(args.kind === 'api' && args.serviceTier === 'default');
|
|
89
|
+
// Anthropic fast mode is a flat 2x on fast-capable Opus; every other
|
|
90
|
+
// premium request needs the model's published Fast/Priority rates.
|
|
91
|
+
const anthropicFast = premium && sentTier === 'fast' && supportsAnthropicFastMode(identity.pricingModel);
|
|
92
|
+
const rateMeta = premium && !anthropicFast ? meta.fastPricing || {} : meta;
|
|
93
|
+
if (args.historicalAggregate && rateMeta.pricingTiers?.some((tier) => promptTokens > tier.aboveInputTokens)) {
|
|
82
94
|
// A daily sum cannot establish which individual requests crossed a
|
|
83
95
|
// context boundary. Do not price the whole day as one huge prompt.
|
|
84
96
|
return { input, costUsd: null, rates: { ...provenance, unpricedReason: 'request-boundaries-unavailable' } };
|
|
@@ -93,10 +105,10 @@ export function priceUsage(args) {
|
|
|
93
105
|
if (!peak) multiplier *= meta.offPeakMultiplier;
|
|
94
106
|
}
|
|
95
107
|
// Fast mode bills 2x standard rates on every fast-capable Opus.
|
|
96
|
-
if (
|
|
108
|
+
if (anthropicFast) multiplier *= 2;
|
|
97
109
|
const keys = PRICING_RATE_KEYS;
|
|
98
110
|
const tokens = [input, n(args.outputTokens), cached, written - written1h];
|
|
99
|
-
const tierRates = ratesForPrompt(
|
|
111
|
+
const tierRates = ratesForPrompt(rateMeta, promptTokens);
|
|
100
112
|
const rates = {
|
|
101
113
|
...provenance,
|
|
102
114
|
...Object.fromEntries(keys.map((key) => [key, tierRates[key] == null ? null : tierRates[key] * multiplier])),
|
|
@@ -144,10 +144,11 @@ function projectLitellmRow(row) {
|
|
|
144
144
|
const value = row[field];
|
|
145
145
|
if (typeof value === 'number' && Number.isFinite(value)) out[field] = value;
|
|
146
146
|
}
|
|
147
|
-
// Preserve the published context-rate
|
|
147
|
+
// Preserve the published context-rate and Priority-tier columns (read by
|
|
148
|
+
// litellmPricing), not just the base price.
|
|
148
149
|
for (const [field, value] of Object.entries(row)) {
|
|
149
150
|
if (
|
|
150
|
-
/^(?:input_cost_per_token|output_cost_per_token|cache_read_input_token_cost|cache_creation_input_token_cost)_above_\d+k_tokens
|
|
151
|
+
/^(?:input_cost_per_token|output_cost_per_token|cache_read_input_token_cost|cache_creation_input_token_cost)(?:_above_\d+k_tokens)?(?:_priority)?$/.test(
|
|
151
152
|
field
|
|
152
153
|
) &&
|
|
153
154
|
typeof value === 'number' &&
|
|
@@ -443,6 +443,44 @@ const PRICED_VARIANTS = Object.freeze({
|
|
|
443
443
|
}),
|
|
444
444
|
});
|
|
445
445
|
|
|
446
|
+
// Cursor-billed rates no catalog publishes: Cursor's first-party SKUs and Fast
|
|
447
|
+
// rates Cursor sells beyond the vendor's own catalog. `fast` is the rate of a
|
|
448
|
+
// request Cursor ran in Fast mode. Other Cursor Fast requests bill at the
|
|
449
|
+
// vendor's Fast rate (Anthropic fast mode, OpenAI Priority/Fast).
|
|
450
|
+
// Source: https://cursor.com/docs/models-and-pricing — list $/M, verified 2026-09-30.
|
|
451
|
+
const CURSOR_PRICING = Object.freeze({
|
|
452
|
+
'composer-2.5': {
|
|
453
|
+
inputCostPerM: 0.5,
|
|
454
|
+
cacheReadCostPerM: 0.2,
|
|
455
|
+
outputCostPerM: 2.5,
|
|
456
|
+
fast: { inputCostPerM: 3, cacheReadCostPerM: 0.5, outputCostPerM: 15 },
|
|
457
|
+
},
|
|
458
|
+
'grok-4.7': {
|
|
459
|
+
fast: {
|
|
460
|
+
inputCostPerM: 4,
|
|
461
|
+
cacheReadCostPerM: 1,
|
|
462
|
+
outputCostPerM: 12,
|
|
463
|
+
// Fast long context (>256k input) bills at 3x the standard rates.
|
|
464
|
+
pricingTiers: [{ aboveInputTokens: 256000, inputCostPerM: 6, cacheReadCostPerM: 1.5, outputCostPerM: 18 }],
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
'grok-4.6': { fast: { inputCostPerM: 4, cacheReadCostPerM: 1, outputCostPerM: 12 } },
|
|
468
|
+
'grok-4.5': { fast: { inputCostPerM: 4, cacheReadCostPerM: 1, outputCostPerM: 18 } },
|
|
469
|
+
'claude-opus-4-7': {
|
|
470
|
+
fast: { inputCostPerM: 30, cacheWriteCostPerM: 37.5, cacheReadCostPerM: 3, outputCostPerM: 150 },
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
function listedRates(row) {
|
|
475
|
+
return {
|
|
476
|
+
...Object.fromEntries(PRICING_RATE_KEYS.map((key) => [key, row[key] ?? null])),
|
|
477
|
+
pricingTiers: (row.pricingTiers || []).map((tier) => ({
|
|
478
|
+
aboveInputTokens: tier.aboveInputTokens,
|
|
479
|
+
...Object.fromEntries(PRICING_RATE_KEYS.map((key) => [key, tier[key] ?? null])),
|
|
480
|
+
})),
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
446
484
|
function scaledRates(row, factor) {
|
|
447
485
|
return Object.fromEntries(PRICING_RATE_KEYS.map((key) => [key, row?.[key] == null ? null : row[key] * factor]));
|
|
448
486
|
}
|
|
@@ -512,6 +550,13 @@ function lookupModelMetadata(originalId, provider, catalog, modelsDevCatalog) {
|
|
|
512
550
|
};
|
|
513
551
|
}
|
|
514
552
|
}
|
|
553
|
+
const cursorRow = /^cursor-(?:oauth|api)$/.test(String(provider || '').toLowerCase()) ? CURSOR_PRICING[id] : null;
|
|
554
|
+
if (cursorRow) {
|
|
555
|
+
if (!PRICING_RATE_KEYS.some((key) => meta?.[key] != null)) {
|
|
556
|
+
meta = { ...meta, ...listedRates(cursorRow), pricingSource: 'override' };
|
|
557
|
+
}
|
|
558
|
+
if (cursorRow.fast) meta = { ...meta, fastPricing: listedRates(cursorRow.fast) };
|
|
559
|
+
}
|
|
515
560
|
if (providerUsesEndpointScopedLimits(provider) && !providerNative && meta) {
|
|
516
561
|
// OAuth/backend routes can expose smaller account/backend windows than
|
|
517
562
|
// the public API SKU. External catalogs and manual overrides remain useful
|
|
@@ -546,10 +591,13 @@ function lookupModelMetadata(originalId, provider, catalog, modelsDevCatalog) {
|
|
|
546
591
|
|
|
547
592
|
function _normalize(entry) {
|
|
548
593
|
if (!entry || typeof entry !== 'object') return null;
|
|
594
|
+
// OpenAI's Priority processing (since renamed Fast mode) columns.
|
|
595
|
+
const fastPricing = litellmPricing(entry, '_priority');
|
|
549
596
|
return {
|
|
550
597
|
contextWindow: entry.max_input_tokens || entry.max_tokens || null,
|
|
551
598
|
outputTokens: entry.max_output_tokens || null,
|
|
552
599
|
...litellmPricing(entry),
|
|
600
|
+
...(PRICING_RATE_KEYS.some((key) => fastPricing[key] != null) ? { fastPricing } : {}),
|
|
553
601
|
...(entry.off_peak_multiplier ? { offPeakMultiplier: entry.off_peak_multiplier } : {}),
|
|
554
602
|
supportsVision: entry.supports_vision === true,
|
|
555
603
|
supportsFunctionCalling: entry.supports_function_calling === true,
|
|
@@ -20,24 +20,31 @@ function rates(source, keys, multiplier) {
|
|
|
20
20
|
);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
// `suffix` selects a service-tier column set (e.g. `_priority`), published as
|
|
24
|
+
// `<key>_priority` and `<key>_above_<N>k_tokens_priority`.
|
|
25
|
+
export function litellmPricing(entry, suffix = '') {
|
|
24
26
|
const thresholds = new Set();
|
|
27
|
+
const tierKey = new RegExp(`^_above_(\\d+)k_tokens${suffix}$`);
|
|
25
28
|
for (const key of Object.keys(entry || {})) {
|
|
26
29
|
for (const prefix of LITELLM_KEYS) {
|
|
27
30
|
if (!key.startsWith(`${prefix}_above_`)) continue;
|
|
28
|
-
const match = key.slice(prefix.length).match(
|
|
31
|
+
const match = key.slice(prefix.length).match(tierKey);
|
|
29
32
|
if (match) thresholds.add(Number(match[1]) * 1000);
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
35
|
return {
|
|
33
|
-
...rates(
|
|
36
|
+
...rates(
|
|
37
|
+
entry,
|
|
38
|
+
LITELLM_KEYS.map((key) => `${key}${suffix}`),
|
|
39
|
+
1_000_000
|
|
40
|
+
),
|
|
34
41
|
pricingTiers: [...thresholds]
|
|
35
42
|
.sort((a, b) => a - b)
|
|
36
43
|
.map((aboveInputTokens) => ({
|
|
37
44
|
aboveInputTokens,
|
|
38
45
|
...rates(
|
|
39
46
|
entry,
|
|
40
|
-
LITELLM_KEYS.map((key) => `${key}_above_${aboveInputTokens / 1000}k_tokens`),
|
|
47
|
+
LITELLM_KEYS.map((key) => `${key}_above_${aboveInputTokens / 1000}k_tokens${suffix}`),
|
|
41
48
|
1_000_000
|
|
42
49
|
),
|
|
43
50
|
})),
|
|
@@ -23,8 +23,18 @@ export async function accountProviderSend(provider, instance, send, model, opts
|
|
|
23
23
|
} catch (error) {
|
|
24
24
|
openingError = error;
|
|
25
25
|
}
|
|
26
|
+
const identity = {
|
|
27
|
+
provider,
|
|
28
|
+
requestedModel: model,
|
|
29
|
+
sessionId,
|
|
30
|
+
sourceType,
|
|
31
|
+
inputTokensInclusive,
|
|
32
|
+
};
|
|
26
33
|
const record = async (result) => {
|
|
27
34
|
if (!result?.usage) return;
|
|
35
|
+
// A nested send (e.g. a fallback model re-send) already stamped its own
|
|
36
|
+
// final attempt's tier; the outer context only saw the abandoned attempt.
|
|
37
|
+
result.requestServiceTier ??= identity.requestServiceTier || '';
|
|
28
38
|
if (openingError) throw openingError;
|
|
29
39
|
if (!ledger) return;
|
|
30
40
|
const usage = result.usage;
|
|
@@ -46,7 +56,7 @@ export async function accountProviderSend(provider, instance, send, model, opts
|
|
|
46
56
|
cacheWrite1hTokens: usage.cacheWrite1hTokens,
|
|
47
57
|
costUsd: usage.costUsd,
|
|
48
58
|
serviceTier: result.serviceTier || usage.raw?.service_tier,
|
|
49
|
-
|
|
59
|
+
requestServiceTier: result.requestServiceTier,
|
|
50
60
|
responseId: result.responseId,
|
|
51
61
|
// The account this send is bound to, so quota history can tell one
|
|
52
62
|
// connected subscription's records from another's.
|
|
@@ -68,16 +78,7 @@ export async function accountProviderSend(provider, instance, send, model, opts
|
|
|
68
78
|
};
|
|
69
79
|
let result;
|
|
70
80
|
try {
|
|
71
|
-
result = await withUsageContext(
|
|
72
|
-
{
|
|
73
|
-
provider,
|
|
74
|
-
requestedModel: model,
|
|
75
|
-
sessionId,
|
|
76
|
-
sourceType,
|
|
77
|
-
inputTokensInclusive,
|
|
78
|
-
},
|
|
79
|
-
send
|
|
80
|
-
);
|
|
81
|
+
result = await withUsageContext(identity, send);
|
|
81
82
|
} catch (error) {
|
|
82
83
|
// Only provider-reported partial usage is recordable; never invent
|
|
83
84
|
// tokens for a failed request or reinterpret an error as a success.
|
|
@@ -4,3 +4,10 @@ import { AsyncLocalStorage } from 'node:async_hooks';
|
|
|
4
4
|
const context = new AsyncLocalStorage();
|
|
5
5
|
export const withUsageContext = (identity, send) => context.run(identity, send);
|
|
6
6
|
export const currentUsageContext = () => context.getStore();
|
|
7
|
+
// Each wire attempt stamps the tier it actually sent on its own send's
|
|
8
|
+
// context object, so the last attempt (the one that produced the usage) wins
|
|
9
|
+
// and concurrent sends never observe each other's tier.
|
|
10
|
+
export const noteRequestServiceTier = (tier) => {
|
|
11
|
+
const identity = context.getStore();
|
|
12
|
+
if (identity) identity.requestServiceTier = tier || '';
|
|
13
|
+
};
|
|
@@ -81,8 +81,10 @@ function* planUsageRepair(rows, { providerOverrides = {}, price = priceUsage })
|
|
|
81
81
|
cacheReadTokens: row.cache_read,
|
|
82
82
|
cacheWriteTokens: row.cache_write,
|
|
83
83
|
cacheWrite1hTokens: oldRates?.cacheWrite1hTokens,
|
|
84
|
-
|
|
84
|
+
// Rows written before the sent tier was recorded carry `fast`.
|
|
85
|
+
requestServiceTier: oldRates?.requestServiceTier ?? (oldRates?.fast ? 'fast' : undefined),
|
|
85
86
|
serviceTier: oldRates?.serviceTier,
|
|
87
|
+
kind,
|
|
86
88
|
ts: row.ts,
|
|
87
89
|
historical: true,
|
|
88
90
|
});
|
|
@@ -232,7 +232,7 @@ export function makeUsageRecord(args) {
|
|
|
232
232
|
const ts = Number(args.ts ?? Date.now());
|
|
233
233
|
if (!Number.isSafeInteger(ts) || ts <= 0) throw new Error('Usage requires a valid timestamp');
|
|
234
234
|
const kind = args.kind || usageRouteKind(provider);
|
|
235
|
-
const priced = priceUsage({ ...args, provider, model, ts });
|
|
235
|
+
const priced = priceUsage({ ...args, provider, model, ts, kind });
|
|
236
236
|
const supplied =
|
|
237
237
|
args.costUsd !== null &&
|
|
238
238
|
args.costUsd !== undefined &&
|
|
@@ -8,7 +8,8 @@ import { agentTerminalDetail, isAgentTool } from './agent-surface.mjs';
|
|
|
8
8
|
import { backgroundTaskElapsed, backgroundTaskFailureDetail, isBackgroundTaskTool } from './background-task.mjs';
|
|
9
9
|
import { shellDisplayStatus } from './shell-surface.mjs';
|
|
10
10
|
import { normalizeTerminalStatus, resultTerminalStatus, shellResultElapsed } from './terminal-status.mjs';
|
|
11
|
-
import {
|
|
11
|
+
import { gitTerminalStatus } from './git-result.mjs';
|
|
12
|
+
import { isUserControlCancellation } from '../tool-status.mjs';
|
|
12
13
|
|
|
13
14
|
function shellFragments(base, display, isShellSurface) {
|
|
14
15
|
if (!isShellSurface) return { shellStatus: '', shellElapsed: '' };
|
|
@@ -59,16 +60,17 @@ export function deriveCardStatus(base, { normalizedName, parsedArgs, isShellSurf
|
|
|
59
60
|
!pending && isAgentTool(normalizedName) && !agentHeaderFailure
|
|
60
61
|
? agentTerminalDetail(parsedArgs?.status, isError, elapsed, parsedArgs?.error)
|
|
61
62
|
: '';
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
(gitResultExitCode(display.displayedResultText) !== null || Boolean(gitResultError(display.displayedResultText)));
|
|
65
|
-
const failedOrCompleted = isError || failedCount > 0 || gitFailed ? 'failed' : 'completed';
|
|
63
|
+
const gitStatus = normalizedName === 'git' ? gitTerminalStatus(display.displayedResultText) : '';
|
|
64
|
+
const failedOrCompleted = isError || failedCount > 0 ? 'failed' : 'completed';
|
|
66
65
|
const terminalStatus = pending
|
|
67
66
|
? 'running'
|
|
68
67
|
: shellStatus ||
|
|
68
|
+
gitStatus ||
|
|
69
69
|
normalizeTerminalStatus(display.backgroundMeta?.status) ||
|
|
70
70
|
normalizeTerminalStatus(parsedArgs?.status) ||
|
|
71
71
|
resultTerminalStatus(display.displayedResultText) ||
|
|
72
|
+
// A one-line error body is hidden from display, so read the raw result.
|
|
73
|
+
(isUserControlCancellation(base.rt) ? 'cancelled' : '') ||
|
|
72
74
|
failedOrCompleted;
|
|
73
75
|
return {
|
|
74
76
|
shellStatus,
|