@parall/sdk 1.56.1 → 1.57.1
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/attachment-client.d.ts +2 -2
- package/dist/attachment-client.d.ts.map +1 -1
- package/dist/attachment-client.js +2 -2
- package/dist/browser-use.d.ts +288 -0
- package/dist/browser-use.d.ts.map +1 -0
- package/dist/browser-use.js +6 -0
- package/dist/client.d.ts +14 -17
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +35 -29
- package/dist/constants.d.ts +12 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +22 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/llm-provider-client.d.ts +38 -0
- package/dist/llm-provider-client.d.ts.map +1 -0
- package/dist/llm-provider-client.js +57 -0
- package/dist/llm-provider-types.d.ts +97 -0
- package/dist/llm-provider-types.d.ts.map +1 -0
- package/dist/llm-provider-types.js +6 -0
- package/dist/types.d.ts +56 -70
- package/dist/types.d.ts.map +1 -1
- package/dist/wiki-changeset.d.ts +4 -0
- package/dist/wiki-changeset.d.ts.map +1 -0
- package/dist/wiki-changeset.js +11 -0
- package/dist/wiki-upload.d.ts +43 -0
- package/dist/wiki-upload.d.ts.map +1 -0
- package/dist/wiki-upload.js +87 -0
- package/package.json +1 -1
- package/src/attachment-client.ts +2 -2
- package/src/browser-use.ts +217 -0
- package/src/client.ts +60 -30
- package/src/constants.ts +22 -0
- package/src/index.ts +8 -0
- package/src/llm-provider-client.ts +96 -0
- package/src/llm-provider-types.ts +104 -0
- package/src/types.ts +83 -101
- package/src/wiki-changeset.ts +13 -0
- package/src/wiki-upload.ts +142 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export function createWikiUploadFormData(params) {
|
|
2
|
+
const form = new FormData();
|
|
3
|
+
form.append('path', params.path);
|
|
4
|
+
form.append('file', params.file);
|
|
5
|
+
if (params.message)
|
|
6
|
+
form.append('message', params.message);
|
|
7
|
+
if (params.uploadId)
|
|
8
|
+
form.append('upload_id', params.uploadId);
|
|
9
|
+
if (params.conflict)
|
|
10
|
+
form.append('conflict', params.conflict);
|
|
11
|
+
if ('contentRoute' in params && params.contentRoute) {
|
|
12
|
+
form.append('content_route', params.contentRoute);
|
|
13
|
+
}
|
|
14
|
+
return form;
|
|
15
|
+
}
|
|
16
|
+
/** Send one multipart request. XHR is used only when a browser caller asks
|
|
17
|
+
* for upload progress; fetch remains the transport everywhere else. Auth
|
|
18
|
+
* refresh and API error decoding stay in ParallClient above this boundary. */
|
|
19
|
+
export function sendMultipartRequest(options) {
|
|
20
|
+
if (options.onProgress && typeof XMLHttpRequest !== 'undefined') {
|
|
21
|
+
return multipartXHR(options, options.onProgress);
|
|
22
|
+
}
|
|
23
|
+
const timeoutSignal = AbortSignal.timeout(options.timeoutMs);
|
|
24
|
+
const signal = options.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal;
|
|
25
|
+
return fetch(options.url, {
|
|
26
|
+
method: options.method,
|
|
27
|
+
headers: options.headers,
|
|
28
|
+
body: options.body,
|
|
29
|
+
signal,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function multipartXHR(options, onProgress) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const xhr = new XMLHttpRequest();
|
|
35
|
+
let settled = false;
|
|
36
|
+
const finish = (fn) => {
|
|
37
|
+
if (settled)
|
|
38
|
+
return;
|
|
39
|
+
settled = true;
|
|
40
|
+
options.signal?.removeEventListener('abort', abortFromSignal);
|
|
41
|
+
fn();
|
|
42
|
+
};
|
|
43
|
+
const abortFromSignal = () => xhr.abort();
|
|
44
|
+
xhr.open(options.method, options.url, true);
|
|
45
|
+
xhr.timeout = options.timeoutMs;
|
|
46
|
+
for (const [name, value] of Object.entries(options.headers)) {
|
|
47
|
+
xhr.setRequestHeader(name, value);
|
|
48
|
+
}
|
|
49
|
+
xhr.upload.onprogress = (event) => {
|
|
50
|
+
try {
|
|
51
|
+
onProgress(event.loaded, event.lengthComputable ? event.total : 0);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Observer failures must not cancel an otherwise healthy upload.
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
xhr.onload = () => finish(() => {
|
|
58
|
+
const responseHeaders = new Headers();
|
|
59
|
+
for (const line of xhr
|
|
60
|
+
.getAllResponseHeaders()
|
|
61
|
+
.trim()
|
|
62
|
+
.split(/[\r\n]+/)) {
|
|
63
|
+
if (!line)
|
|
64
|
+
continue;
|
|
65
|
+
const separator = line.indexOf(':');
|
|
66
|
+
if (separator > 0) {
|
|
67
|
+
responseHeaders.append(line.slice(0, separator).trim(), line.slice(separator + 1).trim());
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const responseBody = xhr.status === 204 || xhr.responseText === '' ? null : xhr.responseText;
|
|
71
|
+
resolve(new Response(responseBody, {
|
|
72
|
+
status: xhr.status,
|
|
73
|
+
statusText: xhr.statusText,
|
|
74
|
+
headers: responseHeaders,
|
|
75
|
+
}));
|
|
76
|
+
});
|
|
77
|
+
xhr.onerror = () => finish(() => reject(new TypeError('Network request failed')));
|
|
78
|
+
xhr.ontimeout = () => finish(() => reject(new DOMException('Request timed out', 'TimeoutError')));
|
|
79
|
+
xhr.onabort = () => finish(() => reject(new DOMException('Request aborted', 'AbortError')));
|
|
80
|
+
if (options.signal?.aborted) {
|
|
81
|
+
finish(() => reject(new DOMException('Request aborted', 'AbortError')));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
options.signal?.addEventListener('abort', abortFromSignal, { once: true });
|
|
85
|
+
xhr.send(options.body);
|
|
86
|
+
});
|
|
87
|
+
}
|
package/package.json
CHANGED
package/src/attachment-client.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
PresignUploadRequest,
|
|
6
6
|
} from './attachment-types.js';
|
|
7
7
|
import { ENDPOINTS } from './constants.js';
|
|
8
|
-
import {
|
|
8
|
+
import { LLMProviderClient } from './llm-provider-client.js';
|
|
9
9
|
|
|
10
10
|
const COMPLETION_RETRY_DELAYS_MS = [250, 750] as const;
|
|
11
11
|
|
|
@@ -16,7 +16,7 @@ function isRetryableCompletionError(error: unknown): boolean {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/** Attachment upload lifecycle and download URL methods. */
|
|
19
|
-
export abstract class AttachmentClient extends
|
|
19
|
+
export abstract class AttachmentClient extends LLMProviderClient {
|
|
20
20
|
async getUploadPresignUrl(orgId: string, req: PresignUploadRequest): Promise<PresignResponse> {
|
|
21
21
|
return this.request('POST', ENDPOINTS.UPLOAD_PRESIGN(orgId), req);
|
|
22
22
|
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
export type BrowserUseAction =
|
|
2
|
+
| 'read'
|
|
3
|
+
| 'tabs'
|
|
4
|
+
| 'wait'
|
|
5
|
+
| 'snapshot'
|
|
6
|
+
| 'get'
|
|
7
|
+
| 'screenshot'
|
|
8
|
+
| 'open'
|
|
9
|
+
| 'navigate'
|
|
10
|
+
| 'viewport'
|
|
11
|
+
| 'click'
|
|
12
|
+
| 'fill'
|
|
13
|
+
| 'type'
|
|
14
|
+
| 'press'
|
|
15
|
+
| 'scroll'
|
|
16
|
+
| 'close'
|
|
17
|
+
| 'recover'
|
|
18
|
+
| 'eval';
|
|
19
|
+
|
|
20
|
+
/** Org-scoped PostHog rollout gate. Missing, non-boolean, and false values are
|
|
21
|
+
* all disabled; Server admission remains authoritative after any UI check. */
|
|
22
|
+
export const BROWSER_USE_FEATURE_FLAG = 'browser-use';
|
|
23
|
+
|
|
24
|
+
export function isBrowserUseEnabled(flags: Record<string, unknown> | null | undefined): boolean {
|
|
25
|
+
return flags?.[BROWSER_USE_FEATURE_FLAG] === true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type BrowserUseGrant = 'browser.read' | 'browser.interact' | 'browser.advanced';
|
|
29
|
+
|
|
30
|
+
export interface BrowserUseTabTarget {
|
|
31
|
+
tab_id: string;
|
|
32
|
+
browser_generation: number;
|
|
33
|
+
document_generation?: never;
|
|
34
|
+
snapshot_id?: never;
|
|
35
|
+
ref?: never;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface BrowserUseRefTarget {
|
|
39
|
+
tab_id: string;
|
|
40
|
+
browser_generation: number;
|
|
41
|
+
document_generation: number;
|
|
42
|
+
snapshot_id: string;
|
|
43
|
+
ref: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type BrowserUseTarget = BrowserUseTabTarget | BrowserUseRefTarget;
|
|
47
|
+
|
|
48
|
+
export interface BrowserUseViewport {
|
|
49
|
+
width: number;
|
|
50
|
+
height: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface BrowserUseRequestBase {
|
|
54
|
+
contract_version: 'browser-use-operation-v1';
|
|
55
|
+
request_id: string;
|
|
56
|
+
profile_id: string;
|
|
57
|
+
/** Canonical RFC3339 UTC instant, no more than two minutes in the future. */
|
|
58
|
+
deadline: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type NoTarget = { target?: never };
|
|
62
|
+
type TabTarget = { target: BrowserUseTabTarget };
|
|
63
|
+
|
|
64
|
+
type BrowserUseLocator =
|
|
65
|
+
| { target: BrowserUseTabTarget; params: { selector: string } }
|
|
66
|
+
| { target: BrowserUseRefTarget; params: Record<string, never> };
|
|
67
|
+
|
|
68
|
+
type BrowserUseTextLocator<T extends 'fill' | 'type'> =
|
|
69
|
+
| { action: T; target: BrowserUseTabTarget; params: { selector: string; text: string } }
|
|
70
|
+
| { action: T; target: BrowserUseRefTarget; params: { text: string } };
|
|
71
|
+
|
|
72
|
+
export type BrowserUseActionRequest =
|
|
73
|
+
| ({ action: 'read'; params: { url: string } } & NoTarget)
|
|
74
|
+
| ({ action: 'read'; params: Record<string, never> } & TabTarget)
|
|
75
|
+
| ({ action: 'tabs'; params: Record<string, never> } & NoTarget)
|
|
76
|
+
| ({
|
|
77
|
+
action: 'wait';
|
|
78
|
+
params:
|
|
79
|
+
| { state: 'load'; timeout_ms?: number; selector?: never }
|
|
80
|
+
| {
|
|
81
|
+
selector: string;
|
|
82
|
+
state?: 'attached' | 'detached' | 'visible' | 'hidden';
|
|
83
|
+
timeout_ms?: number;
|
|
84
|
+
};
|
|
85
|
+
} & TabTarget)
|
|
86
|
+
| ({
|
|
87
|
+
action: 'snapshot';
|
|
88
|
+
params: { interactive?: boolean; compact?: boolean; depth?: number };
|
|
89
|
+
} & TabTarget)
|
|
90
|
+
| ({ action: 'get'; params: { prop: 'title' | 'url' } } & TabTarget)
|
|
91
|
+
| ({ action: 'get'; params: { prop: 'text' | 'html' | 'value'; selector: string } } & TabTarget)
|
|
92
|
+
| { action: 'get'; params: { prop: 'text' | 'html' | 'value' }; target: BrowserUseRefTarget }
|
|
93
|
+
| ({ action: 'get'; params: { prop: 'attr'; selector: string; name: string } } & TabTarget)
|
|
94
|
+
| { action: 'get'; params: { prop: 'attr'; name: string }; target: BrowserUseRefTarget }
|
|
95
|
+
| ({ action: 'screenshot'; params: Record<string, never> } & TabTarget)
|
|
96
|
+
| ({
|
|
97
|
+
action: 'open';
|
|
98
|
+
params: { url: string; hidden?: boolean; viewport?: BrowserUseViewport };
|
|
99
|
+
} & NoTarget)
|
|
100
|
+
| ({ action: 'navigate'; params: { url: string } } & TabTarget)
|
|
101
|
+
| ({ action: 'viewport'; params: BrowserUseViewport } & TabTarget)
|
|
102
|
+
| ({ action: 'click' } & BrowserUseLocator)
|
|
103
|
+
| BrowserUseTextLocator<'fill'>
|
|
104
|
+
| BrowserUseTextLocator<'type'>
|
|
105
|
+
| ({ action: 'press'; params: { key: string } } & TabTarget)
|
|
106
|
+
| ({
|
|
107
|
+
action: 'scroll';
|
|
108
|
+
params: { direction?: 'up' | 'down' | 'left' | 'right'; pixels?: number };
|
|
109
|
+
} & TabTarget)
|
|
110
|
+
| ({ action: 'close'; params: Record<string, never> } & TabTarget)
|
|
111
|
+
| ({ action: 'recover'; params: Record<string, never> } & TabTarget)
|
|
112
|
+
| ({ action: 'eval'; params: { expression: string; timeout_ms?: number } } & TabTarget);
|
|
113
|
+
|
|
114
|
+
export type BrowserUseOperationRequest = BrowserUseRequestBase & BrowserUseActionRequest;
|
|
115
|
+
|
|
116
|
+
export type BrowserUseOperationPhase =
|
|
117
|
+
| 'accepted'
|
|
118
|
+
| 'started'
|
|
119
|
+
| 'succeeded'
|
|
120
|
+
| 'failed'
|
|
121
|
+
| 'outcome_unknown';
|
|
122
|
+
|
|
123
|
+
export interface BrowserUseOperationError {
|
|
124
|
+
code: string;
|
|
125
|
+
message?: string;
|
|
126
|
+
hint?: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface BrowserUseTabState {
|
|
130
|
+
tab_id: string;
|
|
131
|
+
profile_id: string;
|
|
132
|
+
browser_generation: number;
|
|
133
|
+
document_generation: number;
|
|
134
|
+
url: string;
|
|
135
|
+
title: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface BrowserUseTabSummary extends BrowserUseTabState {
|
|
139
|
+
loading: boolean;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface BrowserUseArticle {
|
|
143
|
+
title: string | null;
|
|
144
|
+
byline: string | null;
|
|
145
|
+
excerpt: string | null;
|
|
146
|
+
text: string;
|
|
147
|
+
published_time: string | null;
|
|
148
|
+
site_name: string | null;
|
|
149
|
+
length: number;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface BrowserUseSuccessDataMap {
|
|
153
|
+
read: {
|
|
154
|
+
profile_id: string;
|
|
155
|
+
url: string;
|
|
156
|
+
title: string;
|
|
157
|
+
article: BrowserUseArticle | null;
|
|
158
|
+
};
|
|
159
|
+
tabs: { tabs: BrowserUseTabSummary[] };
|
|
160
|
+
wait: BrowserUseTabState & { matched: true };
|
|
161
|
+
snapshot: BrowserUseTabState & { snapshot: string; snapshot_id: string };
|
|
162
|
+
get: BrowserUseTabState & { value: string | null };
|
|
163
|
+
screenshot: BrowserUseTabState & { format: 'png'; base64: string };
|
|
164
|
+
open: BrowserUseTabState;
|
|
165
|
+
navigate: BrowserUseTabState;
|
|
166
|
+
viewport: BrowserUseTabState & { viewport: BrowserUseViewport };
|
|
167
|
+
click: BrowserUseTabState & { clicked: true };
|
|
168
|
+
fill: BrowserUseTabState & { filled: true; characters: number };
|
|
169
|
+
type: BrowserUseTabState & { typed: true; characters: number };
|
|
170
|
+
press: BrowserUseTabState & { pressed: true };
|
|
171
|
+
scroll: BrowserUseTabState & { scrolled: true };
|
|
172
|
+
close: BrowserUseTabState & { closed: true };
|
|
173
|
+
recover: BrowserUseTabState & { recovered_from_tab_id: string };
|
|
174
|
+
eval: BrowserUseTabState & { value: unknown };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Durable Browser Use proof. `data` is encrypted/transient: it is present
|
|
178
|
+
* only while result_available is true and disappears after result_expires_at;
|
|
179
|
+
* the terminal receipt and result_digest remain durable. */
|
|
180
|
+
export interface BrowserUseOperationReceiptBase {
|
|
181
|
+
contract_version: 'browser-use-operation-v1';
|
|
182
|
+
operation_id: string;
|
|
183
|
+
request_id: string;
|
|
184
|
+
profile_id: string;
|
|
185
|
+
profile_version: number;
|
|
186
|
+
profile_config_revision: number;
|
|
187
|
+
required_grant: BrowserUseGrant;
|
|
188
|
+
phase: BrowserUseOperationPhase;
|
|
189
|
+
dispatched?: boolean;
|
|
190
|
+
receipt_id?: string;
|
|
191
|
+
receipt_digest?: string;
|
|
192
|
+
occurred_at?: string;
|
|
193
|
+
tab_id?: string;
|
|
194
|
+
browser_generation?: number;
|
|
195
|
+
duration_ms?: number;
|
|
196
|
+
result_digest?: string;
|
|
197
|
+
result_available: boolean;
|
|
198
|
+
result_expires_at?: string;
|
|
199
|
+
error?: BrowserUseOperationError;
|
|
200
|
+
deadline: string;
|
|
201
|
+
accepted_at: string;
|
|
202
|
+
started_at?: string;
|
|
203
|
+
terminal_at?: string;
|
|
204
|
+
updated_at: string;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export type BrowserUseOperationReceiptFor<A extends BrowserUseAction> =
|
|
208
|
+
BrowserUseOperationReceiptBase & {
|
|
209
|
+
action: A;
|
|
210
|
+
data?: BrowserUseSuccessDataMap[A];
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/** Mapped union: checking `action` narrows `data` to that action's stable
|
|
214
|
+
* success shape instead of leaving the two fields independently unioned. */
|
|
215
|
+
export type BrowserUseOperationReceipt<A extends BrowserUseAction = BrowserUseAction> = {
|
|
216
|
+
[K in A]: BrowserUseOperationReceiptFor<K>;
|
|
217
|
+
}[A];
|
package/src/client.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { type BrowserViewerRequestOptions, browserViewerRequestOptions } from './browser-viewer.js';
|
|
2
2
|
import { API_BASE, ENDPOINTS, WIKI_BASE } from './constants.js';
|
|
3
3
|
import { AttachmentClient } from './attachment-client.js';
|
|
4
|
+
import {
|
|
5
|
+
createWikiUploadFormData,
|
|
6
|
+
sendMultipartRequest,
|
|
7
|
+
type WikiChangesetFileUploadParams,
|
|
8
|
+
type WikiFileUploadParams,
|
|
9
|
+
type WikiFileUploadResponse,
|
|
10
|
+
} from './wiki-upload.js';
|
|
11
|
+
import { normalizeWikiChangeset } from './wiki-changeset.js';
|
|
4
12
|
import type {
|
|
5
13
|
AddTeamMemberRequest,
|
|
6
14
|
AgentClip,
|
|
@@ -94,6 +102,7 @@ import type {
|
|
|
94
102
|
CreateCommentRequest,
|
|
95
103
|
CreateExternalConnectionInput,
|
|
96
104
|
CreateExternalTriggerInput,
|
|
105
|
+
CreatePersonalApiKeyRequest,
|
|
97
106
|
CreateScheduleInput,
|
|
98
107
|
CreateSetupIntentResponse,
|
|
99
108
|
CreateTeamRequest,
|
|
@@ -176,6 +185,7 @@ import type {
|
|
|
176
185
|
OutboundRefsRequest,
|
|
177
186
|
OutboundRefsResponse,
|
|
178
187
|
PaginatedResponse,
|
|
188
|
+
PersonalApiKey,
|
|
179
189
|
PlatformConfigResponse,
|
|
180
190
|
PlatformModelsResponse,
|
|
181
191
|
PublishRegistryClipRequest,
|
|
@@ -262,7 +272,6 @@ import type {
|
|
|
262
272
|
WikiCommit,
|
|
263
273
|
WikiDiff,
|
|
264
274
|
WikiFilePreviewUrlResponse,
|
|
265
|
-
WikiFileUploadResponse,
|
|
266
275
|
WikiNodeSectionArtifact,
|
|
267
276
|
WikiOperation,
|
|
268
277
|
WikiOperationsResponse,
|
|
@@ -551,7 +560,7 @@ export class ParallClient extends AttachmentClient {
|
|
|
551
560
|
* Multipart upload variant of `request`. Same auth / refresh / error
|
|
552
561
|
* handling, but lets the caller hand us a prepared `FormData` (file +
|
|
553
562
|
* text fields) and skips the JSON content-type. Used by
|
|
554
|
-
* uploadWikiFile / uploadWikiFileToChangeset — wiki
|
|
563
|
+
* uploadWikiFile / uploadWikiFileToChangeset — wiki file uploads can
|
|
555
564
|
* hit the 100 MiB cap, so a longer 5-minute timeout is used so a
|
|
556
565
|
* 50 MiB blob on a slow connection doesn't get chopped at 15 s.
|
|
557
566
|
*/
|
|
@@ -560,6 +569,11 @@ export class ParallClient extends AttachmentClient {
|
|
|
560
569
|
path: string,
|
|
561
570
|
body: FormData,
|
|
562
571
|
retried = false,
|
|
572
|
+
opts?: {
|
|
573
|
+
signal?: AbortSignal;
|
|
574
|
+
onProgress?: (uploadedBytes: number, totalBytes: number) => void;
|
|
575
|
+
timeoutMs?: number;
|
|
576
|
+
},
|
|
563
577
|
): Promise<T> {
|
|
564
578
|
if (!retried) {
|
|
565
579
|
await this.ensureFreshToken(path);
|
|
@@ -568,13 +582,17 @@ export class ParallClient extends AttachmentClient {
|
|
|
568
582
|
const { 'Content-Type': _drop, ...headers } = this.buildHeaders(path);
|
|
569
583
|
void _drop;
|
|
570
584
|
|
|
585
|
+
const timeoutMs = opts?.timeoutMs ?? 5 * 60 * 1000;
|
|
571
586
|
let res: Response;
|
|
572
587
|
try {
|
|
573
|
-
res = await
|
|
588
|
+
res = await sendMultipartRequest({
|
|
574
589
|
method,
|
|
590
|
+
url: `${this.baseUrlFor(path)}${path}`,
|
|
575
591
|
headers,
|
|
576
592
|
body,
|
|
577
|
-
|
|
593
|
+
timeoutMs,
|
|
594
|
+
signal: opts?.signal,
|
|
595
|
+
onProgress: opts?.onProgress,
|
|
578
596
|
});
|
|
579
597
|
} catch (err) {
|
|
580
598
|
throw ParallClient.normalizeFetchError(err);
|
|
@@ -586,7 +604,7 @@ export class ParallClient extends AttachmentClient {
|
|
|
586
604
|
if (!retried && !isAuthPath && this.getRefreshToken) {
|
|
587
605
|
const refreshed = await this.tryRefresh();
|
|
588
606
|
if (refreshed) {
|
|
589
|
-
return this.multipartRequest<T>(method, path, body, true);
|
|
607
|
+
return this.multipartRequest<T>(method, path, body, true, opts);
|
|
590
608
|
}
|
|
591
609
|
}
|
|
592
610
|
if (this.onTokenExpired && !isAuthPath) {
|
|
@@ -687,6 +705,22 @@ export class ParallClient extends AttachmentClient {
|
|
|
687
705
|
return this.request('DELETE', ENDPOINTS.USER_AVATAR);
|
|
688
706
|
}
|
|
689
707
|
|
|
708
|
+
// ---- Personal API keys (org-scoped; JWT session required) ----
|
|
709
|
+
|
|
710
|
+
/** Mints an org-scoped personal API key. The plaintext is returned once and never again. */
|
|
711
|
+
async createPersonalApiKey(req: CreatePersonalApiKeyRequest): Promise<ApiKey> {
|
|
712
|
+
return this.request('POST', ENDPOINTS.PERSONAL_API_KEYS, req);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async listPersonalApiKeys(): Promise<PersonalApiKey[]> {
|
|
716
|
+
const res = await this.request<{ data: PersonalApiKey[] }>('GET', ENDPOINTS.PERSONAL_API_KEYS);
|
|
717
|
+
return res.data;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
async revokePersonalApiKey(keyId: string): Promise<void> {
|
|
721
|
+
return this.request('DELETE', ENDPOINTS.PERSONAL_API_KEY(keyId));
|
|
722
|
+
}
|
|
723
|
+
|
|
690
724
|
async uploadAgentAvatar(
|
|
691
725
|
orgId: string,
|
|
692
726
|
agentId: string,
|
|
@@ -735,7 +769,12 @@ export class ParallClient extends AttachmentClient {
|
|
|
735
769
|
data: Partial<
|
|
736
770
|
Pick<
|
|
737
771
|
Organization,
|
|
738
|
-
|
|
772
|
+
| 'name'
|
|
773
|
+
| 'avatar_url'
|
|
774
|
+
| 'smart_routing_strategy'
|
|
775
|
+
| 'smart_routing_agent_id'
|
|
776
|
+
| 'timezone'
|
|
777
|
+
| 'llm_byok_fallback_enabled'
|
|
739
778
|
>
|
|
740
779
|
>,
|
|
741
780
|
): Promise<Organization> {
|
|
@@ -2923,39 +2962,38 @@ export class ParallClient extends AttachmentClient {
|
|
|
2923
2962
|
async uploadWikiFile(
|
|
2924
2963
|
orgId: string,
|
|
2925
2964
|
wikiId: string,
|
|
2926
|
-
params:
|
|
2965
|
+
params: WikiFileUploadParams,
|
|
2927
2966
|
): Promise<WikiFileUploadResponse> {
|
|
2928
2967
|
// `POST /uploads` always writes to the wiki's default branch — the
|
|
2929
|
-
// backend's parseUpload
|
|
2968
|
+
// backend's parseUpload does not accept a `parent_ref` field, so
|
|
2930
2969
|
// surfacing one in this signature would mislead callers. To target
|
|
2931
2970
|
// a feature branch, use uploadWikiFileToChangeset.
|
|
2932
|
-
const fd =
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2971
|
+
const fd = createWikiUploadFormData(params);
|
|
2972
|
+
return this.multipartRequest('POST', ENDPOINTS.WIKI_UPLOADS(orgId, wikiId), fd, false, {
|
|
2973
|
+
signal: params.signal,
|
|
2974
|
+
onProgress: params.onProgress,
|
|
2975
|
+
});
|
|
2937
2976
|
}
|
|
2938
2977
|
|
|
2939
2978
|
/**
|
|
2940
|
-
* Upload a
|
|
2941
|
-
* author-only).
|
|
2942
|
-
*
|
|
2943
|
-
*
|
|
2979
|
+
* Upload a file into a changeset's feature branch (read scope +
|
|
2980
|
+
* author-only). The default remains binary-only; contentRoute `auto`
|
|
2981
|
+
* opts into reviewable text for the Web library upload flow. On merge the
|
|
2982
|
+
* branch contents squash into the default branch.
|
|
2944
2983
|
*/
|
|
2945
2984
|
async uploadWikiFileToChangeset(
|
|
2946
2985
|
orgId: string,
|
|
2947
2986
|
wikiId: string,
|
|
2948
2987
|
changesetId: string,
|
|
2949
|
-
params:
|
|
2988
|
+
params: WikiChangesetFileUploadParams,
|
|
2950
2989
|
): Promise<WikiFileUploadResponse> {
|
|
2951
|
-
const fd =
|
|
2952
|
-
fd.append('path', params.path);
|
|
2953
|
-
fd.append('file', params.file);
|
|
2954
|
-
if (params.message) fd.append('message', params.message);
|
|
2990
|
+
const fd = createWikiUploadFormData(params);
|
|
2955
2991
|
return this.multipartRequest(
|
|
2956
2992
|
'POST',
|
|
2957
2993
|
ENDPOINTS.WIKI_CHANGESET_FILES(orgId, wikiId, changesetId),
|
|
2958
2994
|
fd,
|
|
2995
|
+
false,
|
|
2996
|
+
{ signal: params.signal, onProgress: params.onProgress },
|
|
2959
2997
|
);
|
|
2960
2998
|
}
|
|
2961
2999
|
|
|
@@ -4405,14 +4443,6 @@ export class ParallClient extends AttachmentClient {
|
|
|
4405
4443
|
}
|
|
4406
4444
|
}
|
|
4407
4445
|
|
|
4408
|
-
function normalizeWikiChangeset(changeset: WikiChangeset): WikiChangeset {
|
|
4409
|
-
return {
|
|
4410
|
-
...changeset,
|
|
4411
|
-
changed_paths: changeset.changed_paths ?? [],
|
|
4412
|
-
file_changes: changeset.file_changes ?? [],
|
|
4413
|
-
};
|
|
4414
|
-
}
|
|
4415
|
-
|
|
4416
4446
|
export class ApiError extends Error {
|
|
4417
4447
|
extras?: Record<string, unknown>;
|
|
4418
4448
|
/** Retry-After delta seconds when the server supplies one. */
|
package/src/constants.ts
CHANGED
|
@@ -361,6 +361,8 @@ export const ENDPOINTS = {
|
|
|
361
361
|
// Users
|
|
362
362
|
USERS_ME: `${API_BASE}/users/me`,
|
|
363
363
|
USER_AVATAR: `${API_BASE}/users/me/avatar`,
|
|
364
|
+
PERSONAL_API_KEYS: `${API_BASE}/users/me/api-keys`,
|
|
365
|
+
PERSONAL_API_KEY: (keyId: string) => `${API_BASE}/users/me/api-keys/${keyId}`,
|
|
364
366
|
USER: (id: string) => `${API_BASE}/users/${id}`,
|
|
365
367
|
|
|
366
368
|
// WebSocket ticket
|
|
@@ -379,6 +381,26 @@ export const ENDPOINTS = {
|
|
|
379
381
|
TEAM_MEMBER: (orgId: string, teamId: string, userId: string) =>
|
|
380
382
|
`${API_BASE}/orgs/${orgId}/teams/${teamId}/members/${userId}`,
|
|
381
383
|
ORG_MEMBERS_ONLINE: (orgId: string) => `${API_BASE}/orgs/${orgId}/members/online`,
|
|
384
|
+
LLM_PROVIDERS: (orgId: string) => `${API_BASE}/orgs/${orgId}/llm-providers`,
|
|
385
|
+
LLM_PROVIDER: (orgId: string, providerId: string) =>
|
|
386
|
+
`${API_BASE}/orgs/${orgId}/llm-providers/${providerId}`,
|
|
387
|
+
ORG_LLM_MODELS: (orgId: string) => `${API_BASE}/orgs/${orgId}/llm-models`,
|
|
388
|
+
ORG_LLM_MODEL: (orgId: string, modelRowId: string) =>
|
|
389
|
+
`${API_BASE}/orgs/${orgId}/llm-models/${modelRowId}`,
|
|
390
|
+
/**
|
|
391
|
+
* Platform catalog merged with this org's own model rows. Pass `runtime` to
|
|
392
|
+
* filter org-added models to the ones that runtime can actually reach —
|
|
393
|
+
* without it a picker can offer a model the write path rejects.
|
|
394
|
+
*/
|
|
395
|
+
ORG_MODEL_CATALOG: (orgId: string, runtime?: string, includeModel?: string) => {
|
|
396
|
+
const params = new URLSearchParams();
|
|
397
|
+
if (runtime) params.set('runtime', runtime);
|
|
398
|
+
// include_model keeps a hidden legacy model the caller is still pinned to
|
|
399
|
+
// representable, matching the public catalog.
|
|
400
|
+
if (includeModel) params.set('include_model', includeModel);
|
|
401
|
+
const qs = params.toString();
|
|
402
|
+
return `${API_BASE}/orgs/${orgId}/models${qs ? `?${qs}` : ''}`;
|
|
403
|
+
},
|
|
382
404
|
ORG_MEMBER: (orgId: string, userId: string) => `${API_BASE}/orgs/${orgId}/members/${userId}`,
|
|
383
405
|
ORG_MEMBER_CHATS: (orgId: string, memberId: string) =>
|
|
384
406
|
`${API_BASE}/orgs/${orgId}/members/${memberId}/chats`,
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
export * from './browser-viewer.js';
|
|
2
|
+
export { BROWSER_USE_FEATURE_FLAG, isBrowserUseEnabled } from './browser-use.js';
|
|
2
3
|
export * from './attachment-types.js';
|
|
3
4
|
export type { ParallClientOptions } from './client.js';
|
|
4
5
|
export { ApiError, ParallClient } from './client.js';
|
|
5
6
|
export * from './constants.js';
|
|
7
|
+
export * from './llm-provider-types.js';
|
|
6
8
|
export * from './subject.js';
|
|
7
9
|
export * from './task-label-types.js';
|
|
8
10
|
export * from './types.js';
|
|
9
11
|
export * from './wechat-types.js';
|
|
12
|
+
export type {
|
|
13
|
+
WikiChangesetFileUploadParams,
|
|
14
|
+
WikiFileUploadParams,
|
|
15
|
+
WikiFileUploadResponse,
|
|
16
|
+
WikiStoredAs,
|
|
17
|
+
} from './wiki-upload.js';
|
|
10
18
|
export type {
|
|
11
19
|
ParallWsOptions,
|
|
12
20
|
WsClientEventMap,
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { ENDPOINTS } from './constants.js';
|
|
2
|
+
import type {
|
|
3
|
+
CreateLLMProviderRequest,
|
|
4
|
+
CreateOrgLLMModelRequest,
|
|
5
|
+
LLMProvider,
|
|
6
|
+
OrgLLMModel,
|
|
7
|
+
UpdateOrgLLMModelRequest,
|
|
8
|
+
UpdateLLMProviderRequest,
|
|
9
|
+
} from './llm-provider-types.js';
|
|
10
|
+
import type { PlatformModelInfo } from './types.js';
|
|
11
|
+
import { WechatClient } from './wechat-client.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Org-configured model providers (BYOK) and the model rows bound to them.
|
|
15
|
+
*
|
|
16
|
+
* Writes are org-admin only server-side: a provider row carries a credential
|
|
17
|
+
* and decides where the org's LLM spend goes.
|
|
18
|
+
*
|
|
19
|
+
* Design: docs/engineering-design/org-llm-provider-byok-design.md
|
|
20
|
+
*/
|
|
21
|
+
export abstract class LLMProviderClient extends WechatClient {
|
|
22
|
+
async getLLMProviders(orgId: string): Promise<LLMProvider[]> {
|
|
23
|
+
const response = await this.request<{ data: LLMProvider[] }>(
|
|
24
|
+
'GET',
|
|
25
|
+
ENDPOINTS.LLM_PROVIDERS(orgId),
|
|
26
|
+
);
|
|
27
|
+
return response.data;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async createLLMProvider(orgId: string, data: CreateLLMProviderRequest): Promise<LLMProvider> {
|
|
31
|
+
return this.request('POST', ENDPOINTS.LLM_PROVIDERS(orgId), data);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Omitting `api_key` leaves the stored credential untouched. */
|
|
35
|
+
async updateLLMProvider(
|
|
36
|
+
orgId: string,
|
|
37
|
+
providerId: string,
|
|
38
|
+
data: UpdateLLMProviderRequest,
|
|
39
|
+
): Promise<LLMProvider> {
|
|
40
|
+
return this.request('PATCH', ENDPOINTS.LLM_PROVIDER(orgId, providerId), data);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Fails with PROVIDER_IN_USE while model rows still point at it. */
|
|
44
|
+
async deleteLLMProvider(orgId: string, providerId: string): Promise<void> {
|
|
45
|
+
return this.request('DELETE', ENDPOINTS.LLM_PROVIDER(orgId, providerId));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async getOrgLLMModels(orgId: string): Promise<OrgLLMModel[]> {
|
|
49
|
+
const response = await this.request<{ data: OrgLLMModel[] }>(
|
|
50
|
+
'GET',
|
|
51
|
+
ENDPOINTS.ORG_LLM_MODELS(orgId),
|
|
52
|
+
);
|
|
53
|
+
return response.data;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async createOrgLLMModel(orgId: string, data: CreateOrgLLMModelRequest): Promise<OrgLLMModel> {
|
|
57
|
+
return this.request('POST', ENDPOINTS.ORG_LLM_MODELS(orgId), data);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Patches a model row in place. Repointing a provider or fixing an upstream
|
|
62
|
+
* name this way avoids the delete-and-recreate window, during which an
|
|
63
|
+
* org-only model has no upstream at all and its pinned agents fail.
|
|
64
|
+
*/
|
|
65
|
+
async updateOrgLLMModel(
|
|
66
|
+
orgId: string,
|
|
67
|
+
modelRowId: string,
|
|
68
|
+
data: UpdateOrgLLMModelRequest,
|
|
69
|
+
): Promise<OrgLLMModel> {
|
|
70
|
+
return this.request('PATCH', ENDPOINTS.ORG_LLM_MODEL(orgId, modelRowId), data);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async deleteOrgLLMModel(orgId: string, modelRowId: string): Promise<void> {
|
|
74
|
+
return this.request('DELETE', ENDPOINTS.ORG_LLM_MODEL(orgId, modelRowId));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The catalog as this org sees it: platform models with the org's rows
|
|
79
|
+
* merged in. Distinct from the public model catalog, which stays org-agnostic
|
|
80
|
+
* and cacheable — use this one wherever an org context exists.
|
|
81
|
+
*
|
|
82
|
+
* Pass `runtime` on a picker: org-added models are then filtered to what that
|
|
83
|
+
* runtime's protocol can reach, matching the server-side pin validation.
|
|
84
|
+
*/
|
|
85
|
+
async getOrgModelCatalog(
|
|
86
|
+
orgId: string,
|
|
87
|
+
runtime?: string,
|
|
88
|
+
includeModel?: string,
|
|
89
|
+
): Promise<PlatformModelInfo[]> {
|
|
90
|
+
const response = await this.request<{ data: PlatformModelInfo[] }>(
|
|
91
|
+
'GET',
|
|
92
|
+
ENDPOINTS.ORG_MODEL_CATALOG(orgId, runtime, includeModel),
|
|
93
|
+
);
|
|
94
|
+
return response.data;
|
|
95
|
+
}
|
|
96
|
+
}
|