deepline 0.1.55 → 0.1.57
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/cli/index.js +461 -62
- package/dist/cli/index.mjs +461 -62
- package/dist/index.d.mts +220 -34
- package/dist/index.d.ts +220 -34
- package/dist/index.js +22 -4
- package/dist/index.mjs +22 -4
- package/dist/repo/apps/play-runner-workers/src/entry.ts +43 -95
- package/dist/repo/apps/play-runner-workers/src/runtime/dataset-handles.ts +35 -7
- package/dist/repo/apps/play-runner-workers/src/runtime/tool-http-errors.ts +198 -0
- package/dist/repo/sdk/src/client.ts +33 -2
- package/dist/repo/sdk/src/play.ts +167 -33
- package/dist/repo/sdk/src/release.ts +3 -3
- package/dist/repo/sdk/src/types.ts +21 -0
- package/dist/repo/shared_libs/play-runtime/csv-rename.ts +55 -3
- package/dist/repo/shared_libs/plays/dataset.ts +25 -1
- package/package.json +1 -1
|
@@ -120,6 +120,7 @@ import {
|
|
|
120
120
|
import {
|
|
121
121
|
applyCsvRenameProjection,
|
|
122
122
|
stripCsvProjectedFields,
|
|
123
|
+
stripCsvProjectionMetadata,
|
|
123
124
|
cloneCsvAliasedRow,
|
|
124
125
|
type CsvRenameOptions,
|
|
125
126
|
} from '../../../shared_libs/play-runtime/csv-rename';
|
|
@@ -129,6 +130,12 @@ import type {
|
|
|
129
130
|
LiveNodeProgressMap,
|
|
130
131
|
LiveNodeProgressSnapshot,
|
|
131
132
|
} from './runtime/live-progress';
|
|
133
|
+
import {
|
|
134
|
+
ToolHttpError,
|
|
135
|
+
extractErrorBilling,
|
|
136
|
+
isHardBillingToolHttpError,
|
|
137
|
+
normalizeToolHttpErrorMessage,
|
|
138
|
+
} from './runtime/tool-http-errors';
|
|
132
139
|
|
|
133
140
|
// The play's default export. The bundler injects this — see bundle-play-file.ts.
|
|
134
141
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
@@ -194,23 +201,6 @@ const EXECUTE_TOOL_METADATA_HEADER = 'x-deepline-include-tool-metadata';
|
|
|
194
201
|
const EXECUTE_RESPONSE_CONTRACT_HEADER = 'x-deepline-execute-response-contract';
|
|
195
202
|
const V2_EXECUTE_RESPONSE_CONTRACT = 'v2-tool-execution-result';
|
|
196
203
|
|
|
197
|
-
class ToolHttpError extends Error {
|
|
198
|
-
readonly billing: Record<string, unknown> | null;
|
|
199
|
-
|
|
200
|
-
constructor(message: string, billing: Record<string, unknown> | null) {
|
|
201
|
-
super(message);
|
|
202
|
-
this.name = 'ToolHttpError';
|
|
203
|
-
this.billing = billing;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function formatCreditAmount(value: unknown): string {
|
|
208
|
-
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
209
|
-
return String(value ?? '-');
|
|
210
|
-
}
|
|
211
|
-
return Number(value.toFixed(8)).toString();
|
|
212
|
-
}
|
|
213
|
-
|
|
214
204
|
function getStringField(value: unknown, key: string): string | null {
|
|
215
205
|
if (!isRecord(value)) return null;
|
|
216
206
|
const field = value[key];
|
|
@@ -226,71 +216,6 @@ function getObjectField(
|
|
|
226
216
|
return isRecord(field) ? field : null;
|
|
227
217
|
}
|
|
228
218
|
|
|
229
|
-
function isInsufficientCreditsBilling(
|
|
230
|
-
billing: Record<string, unknown> | null,
|
|
231
|
-
): billing is Record<string, unknown> {
|
|
232
|
-
return billing?.kind === 'insufficient_credits';
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function formatInsufficientCreditsMessage(input: {
|
|
236
|
-
billing: Record<string, unknown>;
|
|
237
|
-
toolId: string;
|
|
238
|
-
}): string {
|
|
239
|
-
const operation =
|
|
240
|
-
getStringField(input.billing, 'operation_id') ??
|
|
241
|
-
getStringField(input.billing, 'operation') ??
|
|
242
|
-
input.toolId;
|
|
243
|
-
const balance = formatCreditAmount(input.billing.balance_credits);
|
|
244
|
-
const required = formatCreditAmount(input.billing.required_credits);
|
|
245
|
-
const recommended = formatCreditAmount(
|
|
246
|
-
input.billing.recommended_add_credits ?? input.billing.needed_credits,
|
|
247
|
-
);
|
|
248
|
-
const billingUrl = getStringField(input.billing, 'billing_url');
|
|
249
|
-
const addSuffix =
|
|
250
|
-
billingUrl && recommended !== '-'
|
|
251
|
-
? ` Add >=${recommended} at ${billingUrl}.`
|
|
252
|
-
: billingUrl
|
|
253
|
-
? ` Add credits at ${billingUrl}.`
|
|
254
|
-
: '';
|
|
255
|
-
return `Workspace balance ${balance} < required ${required} for ${operation}.${addSuffix}`;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function normalizeToolHttpErrorMessage(input: {
|
|
259
|
-
toolId: string;
|
|
260
|
-
status: number;
|
|
261
|
-
attempt: number;
|
|
262
|
-
maxAttempts: number;
|
|
263
|
-
bodyText: string;
|
|
264
|
-
}): ToolHttpError {
|
|
265
|
-
let parsed: Record<string, unknown> | null = null;
|
|
266
|
-
try {
|
|
267
|
-
const candidate = JSON.parse(input.bodyText);
|
|
268
|
-
parsed = isRecord(candidate) ? candidate : null;
|
|
269
|
-
} catch {
|
|
270
|
-
parsed = null;
|
|
271
|
-
}
|
|
272
|
-
const billing = getObjectField(parsed, 'billing');
|
|
273
|
-
if (isInsufficientCreditsBilling(billing)) {
|
|
274
|
-
return new ToolHttpError(
|
|
275
|
-
`tool ${input.toolId} ${input.status} attempt ${input.attempt}/${input.maxAttempts}: ${formatInsufficientCreditsMessage(
|
|
276
|
-
{
|
|
277
|
-
billing,
|
|
278
|
-
toolId: input.toolId,
|
|
279
|
-
},
|
|
280
|
-
)}`,
|
|
281
|
-
billing,
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
return new ToolHttpError(
|
|
285
|
-
`tool ${input.toolId} ${input.status} attempt ${input.attempt}/${input.maxAttempts}: ${input.bodyText.slice(0, 500)}`,
|
|
286
|
-
billing,
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function extractErrorBilling(error: unknown): Record<string, unknown> | null {
|
|
291
|
-
return error instanceof ToolHttpError ? error.billing : null;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
219
|
/** R2 binding injected by the Worker runtime (when present in deploy metadata). */
|
|
295
220
|
type WorkerEnv = {
|
|
296
221
|
PLAYS_BUCKET?: R2Bucket;
|
|
@@ -690,6 +615,23 @@ function publicCsvInputRow<T extends Record<string, unknown>>(row: T): T {
|
|
|
690
615
|
) as T;
|
|
691
616
|
}
|
|
692
617
|
|
|
618
|
+
function publicCsvOutputRow<T extends Record<string, unknown>>(row: T): T {
|
|
619
|
+
const stripped = stripCsvProjectionMetadata(row) as Record<string, unknown>;
|
|
620
|
+
const publicRow: Record<string, unknown> = {};
|
|
621
|
+
for (const fieldName of Reflect.ownKeys(stripped)) {
|
|
622
|
+
if (
|
|
623
|
+
typeof fieldName === 'string' &&
|
|
624
|
+
fieldName.startsWith('__deepline')
|
|
625
|
+
) {
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
const descriptor = Object.getOwnPropertyDescriptor(stripped, fieldName);
|
|
629
|
+
if (!descriptor) continue;
|
|
630
|
+
Object.defineProperty(publicRow, fieldName, descriptor);
|
|
631
|
+
}
|
|
632
|
+
return publicRow as T;
|
|
633
|
+
}
|
|
634
|
+
|
|
693
635
|
/**
|
|
694
636
|
* Strip credentials and JWT-shaped tokens from any string before it lands in
|
|
695
637
|
* a log buffer or upstream error message. The harness routinely echoes
|
|
@@ -1330,7 +1272,7 @@ async function callToolDirect(
|
|
|
1330
1272
|
bodyText: text,
|
|
1331
1273
|
});
|
|
1332
1274
|
const retryable =
|
|
1333
|
-
res.status === 429 ||
|
|
1275
|
+
(res.status === 429 && !isHardBillingToolHttpError(lastError)) ||
|
|
1334
1276
|
(res.status >= 500 && WORKER_RETRY_SAFE_5XX_TOOLS.has(toolId));
|
|
1335
1277
|
if (!retryable || attempt >= maxAttempts) {
|
|
1336
1278
|
throw lastError;
|
|
@@ -3742,9 +3684,8 @@ function createMinimalWorkerCtx(
|
|
|
3742
3684
|
? completedRow.__deeplineRowKey
|
|
3743
3685
|
: derivePlayRowIdentity(publicCsvInputRow(completedRow), name);
|
|
3744
3686
|
if (key) {
|
|
3745
|
-
const
|
|
3746
|
-
|
|
3747
|
-
void _rowKey;
|
|
3687
|
+
const cleanedRow = publicCsvOutputRow(completedRow);
|
|
3688
|
+
delete cleanedRow.__deeplineRowKey;
|
|
3748
3689
|
resultByKey.set(key, cleanedRow as T & Record<string, unknown>);
|
|
3749
3690
|
}
|
|
3750
3691
|
}
|
|
@@ -3763,8 +3704,9 @@ function createMinimalWorkerCtx(
|
|
|
3763
3704
|
return resultByKey.get(key);
|
|
3764
3705
|
})
|
|
3765
3706
|
.filter((row): row is T & Record<string, unknown> => Boolean(row));
|
|
3707
|
+
const publicOut = out.map((row) => publicCsvOutputRow(row));
|
|
3766
3708
|
const hashStartedAt = nowMs();
|
|
3767
|
-
const hash = await hashJson(
|
|
3709
|
+
const hash = await hashJson(publicOut);
|
|
3768
3710
|
recordRunnerPerfTrace({
|
|
3769
3711
|
req,
|
|
3770
3712
|
phase: 'runner.map_chunk.hash',
|
|
@@ -3797,7 +3739,7 @@ function createMinimalWorkerCtx(
|
|
|
3797
3739
|
rowsSkipped,
|
|
3798
3740
|
outputDatasetId: `map:${name}`,
|
|
3799
3741
|
hash,
|
|
3800
|
-
preview:
|
|
3742
|
+
preview: toWorkflowSerializableValue(publicOut.slice(0, 5)),
|
|
3801
3743
|
cachedRows:
|
|
3802
3744
|
out.length <= WORKER_DATASET_IN_MEMORY_ROWS
|
|
3803
3745
|
? serializeDurableStepValue(out)
|
|
@@ -4333,13 +4275,15 @@ function createMinimalWorkerCtx(
|
|
|
4333
4275
|
req,
|
|
4334
4276
|
allowInline:
|
|
4335
4277
|
options?.timeoutMs == null && !childNeedsWorkflowScheduler,
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4278
|
+
body: {
|
|
4279
|
+
name: resolvedName,
|
|
4280
|
+
input: isRecord(input) ? input : {},
|
|
4281
|
+
orgId: req.orgId,
|
|
4282
|
+
callbackBaseUrl: req.callbackUrl,
|
|
4283
|
+
baseUrl: req.baseUrl,
|
|
4284
|
+
parentExecutorToken: req.executorToken,
|
|
4285
|
+
userEmail: req.userEmail ?? '',
|
|
4286
|
+
profile: 'workers_edge',
|
|
4343
4287
|
manifest: childManifest,
|
|
4344
4288
|
childPlayManifests: req.childPlayManifests ?? null,
|
|
4345
4289
|
internalRunPlay: {
|
|
@@ -5344,6 +5288,10 @@ function serializePlayReturnValue(value: unknown): unknown {
|
|
|
5344
5288
|
return serializeValue(value, 0);
|
|
5345
5289
|
}
|
|
5346
5290
|
|
|
5291
|
+
function toWorkflowSerializableValue<T>(value: T): T {
|
|
5292
|
+
return serializeValue(value, 0) as T;
|
|
5293
|
+
}
|
|
5294
|
+
|
|
5347
5295
|
/**
|
|
5348
5296
|
* Hard cap on the trimmed result body persisted into Convex run state.
|
|
5349
5297
|
* Convex docs are bounded (~1 MiB per field) so we keep this comfortably
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
applyCsvRenameProjection,
|
|
3
|
+
stripCsvProjectedFields,
|
|
4
|
+
stripCsvProjectionMetadata,
|
|
3
5
|
type CsvRenameOptions,
|
|
4
6
|
} from '../../../../shared_libs/play-runtime/csv-rename';
|
|
5
7
|
import {
|
|
@@ -38,7 +40,32 @@ const datasetCountHints = new WeakMap<object, number | null>();
|
|
|
38
40
|
const datasetCapabilities = new WeakMap<object, WorkerDatasetCapabilities>();
|
|
39
41
|
|
|
40
42
|
function cloneRow<T extends DatasetRow>(row: T): T {
|
|
41
|
-
|
|
43
|
+
const cloned: DatasetRow = {};
|
|
44
|
+
for (const key of Reflect.ownKeys(row)) {
|
|
45
|
+
const descriptor = Object.getOwnPropertyDescriptor(row, key);
|
|
46
|
+
if (!descriptor) continue;
|
|
47
|
+
Object.defineProperty(cloned, key, descriptor);
|
|
48
|
+
}
|
|
49
|
+
return cloned as T;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function internalDatasetRow<T extends DatasetRow>(row: T): T {
|
|
53
|
+
const stripped = stripCsvProjectionMetadata(row) as DatasetRow;
|
|
54
|
+
const publicRow: DatasetRow = {};
|
|
55
|
+
for (const key of Reflect.ownKeys(stripped)) {
|
|
56
|
+
if (typeof key === 'string' && key.startsWith('__deepline')) continue;
|
|
57
|
+
const descriptor = Object.getOwnPropertyDescriptor(stripped, key);
|
|
58
|
+
if (!descriptor) continue;
|
|
59
|
+
Object.defineProperty(publicRow, key, descriptor);
|
|
60
|
+
}
|
|
61
|
+
return publicRow as T;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function materializedDatasetRow<T extends DatasetRow>(row: T): T {
|
|
65
|
+
const stripped = stripCsvProjectedFields(row) as DatasetRow;
|
|
66
|
+
return Object.fromEntries(
|
|
67
|
+
Object.entries(stripped).filter(([key]) => !key.startsWith('__deepline')),
|
|
68
|
+
) as T;
|
|
42
69
|
}
|
|
43
70
|
|
|
44
71
|
function registerChunkReader<T extends DatasetRow>(
|
|
@@ -138,10 +165,10 @@ export function createPersistedDatasetHandle<T extends DatasetRow>(input: {
|
|
|
138
165
|
const count = Math.max(0, Math.floor(input.count));
|
|
139
166
|
const previewRows = (input.previewRows ?? [])
|
|
140
167
|
.slice(0, WORKER_DATASET_PREVIEW_ROWS)
|
|
141
|
-
.map(
|
|
168
|
+
.map(materializedDatasetRow);
|
|
142
169
|
const cachedRows =
|
|
143
170
|
input.cachedRows && input.cachedRows.length <= WORKER_DATASET_IN_MEMORY_ROWS
|
|
144
|
-
? input.cachedRows.map(
|
|
171
|
+
? input.cachedRows.map(internalDatasetRow)
|
|
145
172
|
: null;
|
|
146
173
|
|
|
147
174
|
async function loadRows(limit: number, offset: number): Promise<T[]> {
|
|
@@ -157,7 +184,7 @@ export function createPersistedDatasetHandle<T extends DatasetRow>(input: {
|
|
|
157
184
|
) {
|
|
158
185
|
return cachedRows
|
|
159
186
|
.slice(normalizedOffset, normalizedOffset + normalizedLimit)
|
|
160
|
-
.map(
|
|
187
|
+
.map(internalDatasetRow);
|
|
161
188
|
}
|
|
162
189
|
const startedAt = input.nowMs();
|
|
163
190
|
const rows = await input.readRows({
|
|
@@ -172,7 +199,7 @@ export function createPersistedDatasetHandle<T extends DatasetRow>(input: {
|
|
|
172
199
|
offset: normalizedOffset,
|
|
173
200
|
rows: rows.length,
|
|
174
201
|
});
|
|
175
|
-
return rows.map(
|
|
202
|
+
return rows.map(internalDatasetRow);
|
|
176
203
|
}
|
|
177
204
|
|
|
178
205
|
async function* readChunks(chunkSize: number): AsyncGenerator<T[], void, void> {
|
|
@@ -205,14 +232,15 @@ export function createPersistedDatasetHandle<T extends DatasetRow>(input: {
|
|
|
205
232
|
workProgress: input.workProgress,
|
|
206
233
|
resolvers: {
|
|
207
234
|
count: async () => count,
|
|
208
|
-
peek: async (limit) =>
|
|
235
|
+
peek: async (limit) =>
|
|
236
|
+
(await loadRows(Math.max(0, limit), 0)).map(materializedDatasetRow),
|
|
209
237
|
materialize: async (limit) => {
|
|
210
238
|
const rows: T[] = [];
|
|
211
239
|
const maxRows = limit ?? count;
|
|
212
240
|
for await (const chunk of readChunks(STREAM_MATERIALIZE_CHUNK_ROWS)) {
|
|
213
241
|
for (const row of chunk) {
|
|
214
242
|
if (rows.length >= maxRows) return rows;
|
|
215
|
-
rows.push(row);
|
|
243
|
+
rows.push(materializedDatasetRow(row));
|
|
216
244
|
}
|
|
217
245
|
}
|
|
218
246
|
return rows;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
export class ToolHttpError extends Error {
|
|
2
|
+
readonly billing: Record<string, unknown> | null;
|
|
3
|
+
|
|
4
|
+
constructor(message: string, billing: Record<string, unknown> | null) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'ToolHttpError';
|
|
7
|
+
this.billing = billing;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function formatCreditAmount(value: unknown): string {
|
|
12
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
13
|
+
return String(value ?? '-');
|
|
14
|
+
}
|
|
15
|
+
return Number(value.toFixed(8)).toString();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
19
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getStringField(value: unknown, key: string): string | null {
|
|
23
|
+
if (!isRecord(value)) return null;
|
|
24
|
+
const field = value[key];
|
|
25
|
+
return typeof field === 'string' && field.trim() ? field : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getObjectField(
|
|
29
|
+
value: unknown,
|
|
30
|
+
key: string,
|
|
31
|
+
): Record<string, unknown> | null {
|
|
32
|
+
if (!isRecord(value)) return null;
|
|
33
|
+
const field = value[key];
|
|
34
|
+
return isRecord(field) ? field : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isInsufficientCreditsBilling(
|
|
38
|
+
billing: Record<string, unknown> | null,
|
|
39
|
+
): billing is Record<string, unknown> {
|
|
40
|
+
return billing?.kind === 'insufficient_credits';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isHardBillingFailurePayload(
|
|
44
|
+
payload: Record<string, unknown> | null,
|
|
45
|
+
): payload is Record<string, unknown> {
|
|
46
|
+
if (!payload) return false;
|
|
47
|
+
const category = String(
|
|
48
|
+
payload.error_category ?? payload.errorCategory ?? '',
|
|
49
|
+
).toLowerCase();
|
|
50
|
+
const code = String(payload.code ?? payload.error_code ?? '').toUpperCase();
|
|
51
|
+
const message = String(
|
|
52
|
+
payload.error ?? payload.message ?? payload.failure_description ?? '',
|
|
53
|
+
).toLowerCase();
|
|
54
|
+
if (category === 'billing') return true;
|
|
55
|
+
if (
|
|
56
|
+
code === 'INSUFFICIENT_CREDITS' ||
|
|
57
|
+
code === 'BILLING_CAP_EXCEEDED' ||
|
|
58
|
+
code === 'MONTHLY_BILLING_LIMIT_EXCEEDED'
|
|
59
|
+
) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
return (
|
|
63
|
+
(message.includes('billing cap') ||
|
|
64
|
+
message.includes('monthly billing limit') ||
|
|
65
|
+
message.includes('rolling 30-day organization billing cap') ||
|
|
66
|
+
message.includes('insufficient credits')) &&
|
|
67
|
+
!message.includes('rate limit')
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeHardBillingPayload(
|
|
72
|
+
payload: Record<string, unknown>,
|
|
73
|
+
): Record<string, unknown> {
|
|
74
|
+
return {
|
|
75
|
+
kind: 'billing_cap_exceeded',
|
|
76
|
+
code:
|
|
77
|
+
typeof payload.code === 'string' && payload.code.trim()
|
|
78
|
+
? payload.code
|
|
79
|
+
: 'MONTHLY_BILLING_LIMIT_EXCEEDED',
|
|
80
|
+
error_category: 'billing',
|
|
81
|
+
failure_origin:
|
|
82
|
+
typeof payload.failure_origin === 'string' && payload.failure_origin.trim()
|
|
83
|
+
? payload.failure_origin
|
|
84
|
+
: 'deepline_billing',
|
|
85
|
+
message:
|
|
86
|
+
typeof payload.error === 'string' && payload.error.trim()
|
|
87
|
+
? payload.error
|
|
88
|
+
: typeof payload.message === 'string' && payload.message.trim()
|
|
89
|
+
? payload.message
|
|
90
|
+
: 'Deepline billing cap exceeded.',
|
|
91
|
+
...payload,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function formatHardBillingFailureMessage(input: {
|
|
96
|
+
billing: Record<string, unknown>;
|
|
97
|
+
toolId: string;
|
|
98
|
+
status: number;
|
|
99
|
+
attempt: number;
|
|
100
|
+
maxAttempts: number;
|
|
101
|
+
}): string {
|
|
102
|
+
const code = getStringField(input.billing, 'code');
|
|
103
|
+
const message =
|
|
104
|
+
getStringField(input.billing, 'message') ??
|
|
105
|
+
getStringField(input.billing, 'error') ??
|
|
106
|
+
'Deepline billing cap exceeded.';
|
|
107
|
+
return [
|
|
108
|
+
`tool ${input.toolId} ${input.status} attempt ${input.attempt}/${input.maxAttempts}:`,
|
|
109
|
+
'Deepline billing cap exceeded.',
|
|
110
|
+
'Run halted before marking remaining rows processed.',
|
|
111
|
+
code ? `code=${code}.` : '',
|
|
112
|
+
message,
|
|
113
|
+
]
|
|
114
|
+
.filter(Boolean)
|
|
115
|
+
.join(' ');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function formatInsufficientCreditsMessage(input: {
|
|
119
|
+
billing: Record<string, unknown>;
|
|
120
|
+
toolId: string;
|
|
121
|
+
}): string {
|
|
122
|
+
const operation =
|
|
123
|
+
getStringField(input.billing, 'operation_id') ??
|
|
124
|
+
getStringField(input.billing, 'operation') ??
|
|
125
|
+
input.toolId;
|
|
126
|
+
const balance = formatCreditAmount(input.billing.balance_credits);
|
|
127
|
+
const required = formatCreditAmount(input.billing.required_credits);
|
|
128
|
+
const recommended = formatCreditAmount(
|
|
129
|
+
input.billing.recommended_add_credits ?? input.billing.needed_credits,
|
|
130
|
+
);
|
|
131
|
+
const billingUrl = getStringField(input.billing, 'billing_url');
|
|
132
|
+
const addSuffix =
|
|
133
|
+
billingUrl && recommended !== '-'
|
|
134
|
+
? ` Add >=${recommended} at ${billingUrl}.`
|
|
135
|
+
: billingUrl
|
|
136
|
+
? ` Add credits at ${billingUrl}.`
|
|
137
|
+
: '';
|
|
138
|
+
return `Workspace balance ${balance} < required ${required} for ${operation}.${addSuffix}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function normalizeToolHttpErrorMessage(input: {
|
|
142
|
+
toolId: string;
|
|
143
|
+
status: number;
|
|
144
|
+
attempt: number;
|
|
145
|
+
maxAttempts: number;
|
|
146
|
+
bodyText: string;
|
|
147
|
+
}): ToolHttpError {
|
|
148
|
+
let parsed: Record<string, unknown> | null = null;
|
|
149
|
+
try {
|
|
150
|
+
const candidate = JSON.parse(input.bodyText);
|
|
151
|
+
parsed = isRecord(candidate) ? candidate : null;
|
|
152
|
+
} catch {
|
|
153
|
+
parsed = null;
|
|
154
|
+
}
|
|
155
|
+
const billing = getObjectField(parsed, 'billing');
|
|
156
|
+
if (isInsufficientCreditsBilling(billing)) {
|
|
157
|
+
return new ToolHttpError(
|
|
158
|
+
`tool ${input.toolId} ${input.status} attempt ${input.attempt}/${input.maxAttempts}: ${formatInsufficientCreditsMessage(
|
|
159
|
+
{
|
|
160
|
+
billing,
|
|
161
|
+
toolId: input.toolId,
|
|
162
|
+
},
|
|
163
|
+
)}`,
|
|
164
|
+
billing,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
const hardBillingPayload = isHardBillingFailurePayload(billing)
|
|
168
|
+
? normalizeHardBillingPayload(billing)
|
|
169
|
+
: isHardBillingFailurePayload(parsed)
|
|
170
|
+
? normalizeHardBillingPayload(parsed)
|
|
171
|
+
: null;
|
|
172
|
+
if (hardBillingPayload) {
|
|
173
|
+
return new ToolHttpError(
|
|
174
|
+
formatHardBillingFailureMessage({
|
|
175
|
+
billing: hardBillingPayload,
|
|
176
|
+
toolId: input.toolId,
|
|
177
|
+
status: input.status,
|
|
178
|
+
attempt: input.attempt,
|
|
179
|
+
maxAttempts: input.maxAttempts,
|
|
180
|
+
}),
|
|
181
|
+
hardBillingPayload,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
return new ToolHttpError(
|
|
185
|
+
`tool ${input.toolId} ${input.status} attempt ${input.attempt}/${input.maxAttempts}: ${input.bodyText.slice(0, 500)}`,
|
|
186
|
+
billing,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function extractErrorBilling(
|
|
191
|
+
error: unknown,
|
|
192
|
+
): Record<string, unknown> | null {
|
|
193
|
+
return error instanceof ToolHttpError ? error.billing : null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function isHardBillingToolHttpError(error: unknown): boolean {
|
|
197
|
+
return error instanceof ToolHttpError && isHardBillingFailurePayload(error.billing);
|
|
198
|
+
}
|
|
@@ -485,6 +485,37 @@ export class DeeplineClient {
|
|
|
485
485
|
return `deepline plays run ${target} --input '{...}' --watch`;
|
|
486
486
|
}
|
|
487
487
|
|
|
488
|
+
private starterPlayPath(play: Pick<PlayListItem, 'name' | 'reference'>): string {
|
|
489
|
+
const target = play.reference || play.name;
|
|
490
|
+
const unqualifiedName = target.split('/').pop() || play.name;
|
|
491
|
+
const safeName = unqualifiedName
|
|
492
|
+
.trim()
|
|
493
|
+
.toLowerCase()
|
|
494
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
495
|
+
.replace(/-+/g, '-')
|
|
496
|
+
.replace(/^-|-$/g, '');
|
|
497
|
+
return `./${safeName || 'play'}.play.ts`;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private playCloneEditStarter(
|
|
501
|
+
play: Pick<
|
|
502
|
+
PlayListItem,
|
|
503
|
+
'name' | 'reference' | 'canClone' | 'canEdit' | 'origin' | 'ownerType'
|
|
504
|
+
>,
|
|
505
|
+
): PlayDescription['cloneEditStarter'] | undefined {
|
|
506
|
+
const readonlyPrebuilt =
|
|
507
|
+
(play.origin === 'prebuilt' || play.ownerType === 'deepline') &&
|
|
508
|
+
!play.canEdit;
|
|
509
|
+
if (!play.canClone && !readonlyPrebuilt) return undefined;
|
|
510
|
+
const target = play.reference || play.name;
|
|
511
|
+
const path = this.starterPlayPath(play);
|
|
512
|
+
return {
|
|
513
|
+
path,
|
|
514
|
+
command: `deepline plays get ${target} --source --out ${path}`,
|
|
515
|
+
checkCommand: `deepline plays check ${path}`,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
488
519
|
private summarizePlayListItem(
|
|
489
520
|
play: PlayListItem,
|
|
490
521
|
options?: { compact?: boolean },
|
|
@@ -496,6 +527,7 @@ export class DeeplineClient {
|
|
|
496
527
|
'rowOutputSchema',
|
|
497
528
|
);
|
|
498
529
|
const runCommand = this.playRunCommand(play, { csvInput });
|
|
530
|
+
const cloneEditStarter = this.playCloneEditStarter(play);
|
|
499
531
|
return {
|
|
500
532
|
name: play.name,
|
|
501
533
|
...(play.reference ? { reference: play.reference } : {}),
|
|
@@ -515,6 +547,7 @@ export class DeeplineClient {
|
|
|
515
547
|
...(rowOutputSchema ? { rowOutputSchema } : {}),
|
|
516
548
|
runCommand,
|
|
517
549
|
examples: [runCommand],
|
|
550
|
+
...(cloneEditStarter ? { cloneEditStarter } : {}),
|
|
518
551
|
currentPublishedVersion: play.currentPublishedVersion ?? null,
|
|
519
552
|
isDraftDirty: play.isDraftDirty,
|
|
520
553
|
};
|
|
@@ -1472,12 +1505,10 @@ export class DeeplineClient {
|
|
|
1472
1505
|
|
|
1473
1506
|
async searchPlays(options: {
|
|
1474
1507
|
query: string;
|
|
1475
|
-
origin?: 'prebuilt' | 'owned';
|
|
1476
1508
|
compact?: boolean;
|
|
1477
1509
|
}): Promise<PlayDescription[]> {
|
|
1478
1510
|
const params = new URLSearchParams();
|
|
1479
1511
|
params.set('search', options.query.trim());
|
|
1480
|
-
if (options.origin) params.set('origin', options.origin);
|
|
1481
1512
|
const response = await this.http.get<{ plays: PlayListItem[] }>(
|
|
1482
1513
|
`/api/v2/plays?${params.toString()}`,
|
|
1483
1514
|
);
|