@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.
Files changed (40) hide show
  1. package/dist/attachment-client.d.ts +2 -2
  2. package/dist/attachment-client.d.ts.map +1 -1
  3. package/dist/attachment-client.js +2 -2
  4. package/dist/browser-use.d.ts +288 -0
  5. package/dist/browser-use.d.ts.map +1 -0
  6. package/dist/browser-use.js +6 -0
  7. package/dist/client.d.ts +14 -17
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +35 -29
  10. package/dist/constants.d.ts +12 -0
  11. package/dist/constants.d.ts.map +1 -1
  12. package/dist/constants.js +22 -0
  13. package/dist/index.d.ts +3 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +2 -0
  16. package/dist/llm-provider-client.d.ts +38 -0
  17. package/dist/llm-provider-client.d.ts.map +1 -0
  18. package/dist/llm-provider-client.js +57 -0
  19. package/dist/llm-provider-types.d.ts +97 -0
  20. package/dist/llm-provider-types.d.ts.map +1 -0
  21. package/dist/llm-provider-types.js +6 -0
  22. package/dist/types.d.ts +56 -70
  23. package/dist/types.d.ts.map +1 -1
  24. package/dist/wiki-changeset.d.ts +4 -0
  25. package/dist/wiki-changeset.d.ts.map +1 -0
  26. package/dist/wiki-changeset.js +11 -0
  27. package/dist/wiki-upload.d.ts +43 -0
  28. package/dist/wiki-upload.d.ts.map +1 -0
  29. package/dist/wiki-upload.js +87 -0
  30. package/package.json +1 -1
  31. package/src/attachment-client.ts +2 -2
  32. package/src/browser-use.ts +217 -0
  33. package/src/client.ts +60 -30
  34. package/src/constants.ts +22 -0
  35. package/src/index.ts +8 -0
  36. package/src/llm-provider-client.ts +96 -0
  37. package/src/llm-provider-types.ts +104 -0
  38. package/src/types.ts +83 -101
  39. package/src/wiki-changeset.ts +13 -0
  40. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/sdk",
3
- "version": "1.56.1",
3
+ "version": "1.57.1",
4
4
  "description": "TypeScript client SDK for Parall — REST + WebSocket client, shared types",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -5,7 +5,7 @@ import type {
5
5
  PresignUploadRequest,
6
6
  } from './attachment-types.js';
7
7
  import { ENDPOINTS } from './constants.js';
8
- import { WechatClient } from './wechat-client.js';
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 WechatClient {
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 binary uploads can
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 fetch(`${this.baseUrlFor(path)}${path}`, {
588
+ res = await sendMultipartRequest({
574
589
  method,
590
+ url: `${this.baseUrlFor(path)}${path}`,
575
591
  headers,
576
592
  body,
577
- signal: AbortSignal.timeout(5 * 60 * 1000),
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
- 'name' | 'avatar_url' | 'smart_routing_strategy' | 'smart_routing_agent_id' | 'timezone'
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: { path: string; file: Blob; message?: string },
2965
+ params: WikiFileUploadParams,
2927
2966
  ): Promise<WikiFileUploadResponse> {
2928
2967
  // `POST /uploads` always writes to the wiki's default branch — the
2929
- // backend's parseUpload silently drops any `parent_ref` field, so
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 = new FormData();
2933
- fd.append('path', params.path);
2934
- fd.append('file', params.file);
2935
- if (params.message) fd.append('message', params.message);
2936
- return this.multipartRequest('POST', ENDPOINTS.WIKI_UPLOADS(orgId, wikiId), fd);
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 binary file into a changeset's feature branch (read scope +
2941
- * author-only). Used by reader flow: propose a changeset, attach
2942
- * binary, PATCH markdown that references it. On merge the binary
2943
- * squashes into the default branch.
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: { path: string; file: Blob; message?: string },
2988
+ params: WikiChangesetFileUploadParams,
2950
2989
  ): Promise<WikiFileUploadResponse> {
2951
- const fd = new FormData();
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
+ }