@opengeni/sdk 0.1.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/README.md +157 -0
- package/dist/index.d.ts +1196 -0
- package/dist/index.js +955 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
- package/src/client.ts +846 -0
- package/src/errors.ts +40 -0
- package/src/index.ts +142 -0
- package/src/proxy.ts +171 -0
- package/src/sse.ts +90 -0
- package/src/stream.ts +192 -0
- package/src/types.ts +1036 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
import { OpenGeniApiError } from "./errors";
|
|
2
|
+
import { streamSessionEvents, type SessionEventStreamTransport, type StreamSessionEventsOptions } from "./stream";
|
|
3
|
+
import type {
|
|
4
|
+
AccessContext,
|
|
5
|
+
ApiKey,
|
|
6
|
+
BillingEntitlementsResponse,
|
|
7
|
+
BillingSummary,
|
|
8
|
+
BillingUsageResponse,
|
|
9
|
+
CapabilityCatalogItem,
|
|
10
|
+
CapabilityCatalogResponse,
|
|
11
|
+
CapabilityInstallation,
|
|
12
|
+
ClientSessionEventInput,
|
|
13
|
+
CompactSessionContextResult,
|
|
14
|
+
CompleteFileUploadResponse,
|
|
15
|
+
CreateApiKeyRequest,
|
|
16
|
+
CreateApiKeyResponse,
|
|
17
|
+
CreateCapabilityCatalogItemRequest,
|
|
18
|
+
CreateCheckoutRequest,
|
|
19
|
+
CreateCheckoutResponse,
|
|
20
|
+
CreateDocumentBaseRequest,
|
|
21
|
+
CreateFileUploadRequest,
|
|
22
|
+
CreateFileUploadResponse,
|
|
23
|
+
CreateGitHubAppManifestRequest,
|
|
24
|
+
CreateGitHubAppManifestResponse,
|
|
25
|
+
CreateScheduledTaskRequest,
|
|
26
|
+
CreateSessionRequest,
|
|
27
|
+
CreateWorkspaceEnvironmentRequest,
|
|
28
|
+
CreateWorkspaceRequest,
|
|
29
|
+
DiscoverMcpCapabilitiesResponse,
|
|
30
|
+
Document,
|
|
31
|
+
DocumentBase,
|
|
32
|
+
DocumentSearchResponse,
|
|
33
|
+
EnableCapabilityRequest,
|
|
34
|
+
EnablePackRequest,
|
|
35
|
+
FileAsset,
|
|
36
|
+
FileDownloadUrlResponse,
|
|
37
|
+
GetPackResponse,
|
|
38
|
+
GitHubAppInfo,
|
|
39
|
+
GitHubRepositoriesResponse,
|
|
40
|
+
ListApiKeysResponse,
|
|
41
|
+
ListPacksResponse,
|
|
42
|
+
PackInstallation,
|
|
43
|
+
ReasoningEffort,
|
|
44
|
+
RegisterCapabilityPackRequest,
|
|
45
|
+
ResourceRef,
|
|
46
|
+
ScheduledTask,
|
|
47
|
+
ScheduledTaskRun,
|
|
48
|
+
Session,
|
|
49
|
+
SessionEvent,
|
|
50
|
+
SessionGoal,
|
|
51
|
+
SessionTurn,
|
|
52
|
+
ToolRef,
|
|
53
|
+
UpdateScheduledTaskRequest,
|
|
54
|
+
UpdateSessionGoalRequest,
|
|
55
|
+
UpdateSessionTurnRequest,
|
|
56
|
+
UpdateWorkspaceEnvironmentRequest,
|
|
57
|
+
UpdateWorkspaceRequest,
|
|
58
|
+
UploadFileInput,
|
|
59
|
+
WorkspaceEnvironment,
|
|
60
|
+
WorkspaceEnvironmentVariableMetadata,
|
|
61
|
+
WorkspaceRegisteredPack,
|
|
62
|
+
Workspace,
|
|
63
|
+
} from "./types";
|
|
64
|
+
|
|
65
|
+
export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
66
|
+
|
|
67
|
+
export type OpenGeniClientOptions = {
|
|
68
|
+
/** Base URL of the OpenGeni API, e.g. `https://api.example.com`. */
|
|
69
|
+
baseUrl: string;
|
|
70
|
+
/** OpenGeni API key, sent as `Authorization: Bearer <apiKey>`. */
|
|
71
|
+
apiKey?: string;
|
|
72
|
+
/** Extra headers (static or computed per request) merged into every call. */
|
|
73
|
+
headers?: Record<string, string> | (() => Record<string, string>);
|
|
74
|
+
/** Custom fetch implementation. Defaults to the global `fetch`. */
|
|
75
|
+
fetch?: FetchLike;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type SendMessageInput = {
|
|
79
|
+
text: string;
|
|
80
|
+
resources?: ResourceRef[];
|
|
81
|
+
tools?: ToolRef[];
|
|
82
|
+
model?: string;
|
|
83
|
+
reasoningEffort?: ReasoningEffort;
|
|
84
|
+
clientEventId?: string;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export type SteerMessageResult = {
|
|
88
|
+
/** The accepted `user.message` event. */
|
|
89
|
+
accepted: SessionEvent;
|
|
90
|
+
/**
|
|
91
|
+
* The turn created for the message, when it could be located — usually
|
|
92
|
+
* still queued, but already claimed (running/requires_action or even
|
|
93
|
+
* finished) when the worker picked it up mid-call.
|
|
94
|
+
*/
|
|
95
|
+
turn: SessionTurn | null;
|
|
96
|
+
/** True when the running turn was interrupted to make way for the message. */
|
|
97
|
+
interrupted: boolean;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Typed client for the OpenGeni public API. Framework-agnostic: only needs
|
|
102
|
+
* WHATWG `fetch` + streams, so it runs in Node 18+, Bun, Deno, browsers, and
|
|
103
|
+
* edge runtimes.
|
|
104
|
+
*/
|
|
105
|
+
export class OpenGeniClient {
|
|
106
|
+
private readonly baseUrl: string;
|
|
107
|
+
private readonly options: OpenGeniClientOptions;
|
|
108
|
+
private readonly fetchImpl: FetchLike;
|
|
109
|
+
|
|
110
|
+
constructor(options: OpenGeniClientOptions) {
|
|
111
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
112
|
+
this.options = options;
|
|
113
|
+
// Bind lazily so environments that polyfill fetch after module load work.
|
|
114
|
+
this.fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- Session lifecycle ---------------------------------------------------
|
|
118
|
+
|
|
119
|
+
async createSession(workspaceId: string, request: CreateSessionRequest): Promise<Session> {
|
|
120
|
+
return await this.requestJson<Session>("POST", `/v1/workspaces/${workspaceId}/sessions`, request);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async getSession(workspaceId: string, sessionId: string): Promise<Session> {
|
|
124
|
+
return await this.requestJson<Session>("GET", `/v1/workspaces/${workspaceId}/sessions/${sessionId}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async listSessions(workspaceId: string, options: { limit?: number } = {}): Promise<Session[]> {
|
|
128
|
+
return await this.requestJson<Session[]>("GET", `/v1/workspaces/${workspaceId}/sessions`, undefined, {
|
|
129
|
+
...(options.limit !== undefined ? { limit: String(options.limit) } : {}),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async listTurns(workspaceId: string, sessionId: string, options: { limit?: number } = {}): Promise<SessionTurn[]> {
|
|
134
|
+
return await this.requestJson<SessionTurn[]>("GET", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/turns`, undefined, {
|
|
135
|
+
...(options.limit !== undefined ? { limit: String(options.limit) } : {}),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// --- Scheduled tasks -------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
async listScheduledTasks(workspaceId: string, options: { limit?: number } = {}): Promise<ScheduledTask[]> {
|
|
142
|
+
return await this.requestJson<ScheduledTask[]>("GET", `/v1/workspaces/${workspaceId}/scheduled-tasks`, undefined, {
|
|
143
|
+
...(options.limit !== undefined ? { limit: String(options.limit) } : {}),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async getScheduledTask(workspaceId: string, taskId: string): Promise<ScheduledTask> {
|
|
148
|
+
return await this.requestJson<ScheduledTask>("GET", `/v1/workspaces/${workspaceId}/scheduled-tasks/${taskId}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// --- Events: replay, send, stream ----------------------------------------
|
|
152
|
+
|
|
153
|
+
/** Replay durable events by sequence: events with `sequence > after`, ascending. */
|
|
154
|
+
async listEvents(
|
|
155
|
+
workspaceId: string,
|
|
156
|
+
sessionId: string,
|
|
157
|
+
options: { after?: number; limit?: number } = {},
|
|
158
|
+
): Promise<SessionEvent[]> {
|
|
159
|
+
return await this.requestJson<SessionEvent[]>("GET", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/events`, undefined, {
|
|
160
|
+
...(options.after !== undefined ? { after: String(options.after) } : {}),
|
|
161
|
+
...(options.limit !== undefined ? { limit: String(options.limit) } : {}),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** POST a user/control event to the session. Returns the accepted event. */
|
|
166
|
+
async sendEvent(workspaceId: string, sessionId: string, event: ClientSessionEventInput): Promise<SessionEvent> {
|
|
167
|
+
return await this.requestJson<SessionEvent>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/events`, event);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async sendMessage(workspaceId: string, sessionId: string, message: string | SendMessageInput): Promise<SessionEvent> {
|
|
171
|
+
const input = typeof message === "string" ? { text: message } : message;
|
|
172
|
+
const { clientEventId, ...payload } = input;
|
|
173
|
+
return await this.sendEvent(workspaceId, sessionId, {
|
|
174
|
+
type: "user.message",
|
|
175
|
+
...(clientEventId !== undefined ? { clientEventId } : {}),
|
|
176
|
+
payload,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async interrupt(
|
|
181
|
+
workspaceId: string,
|
|
182
|
+
sessionId: string,
|
|
183
|
+
options: { reason?: string; clientEventId?: string } = {},
|
|
184
|
+
): Promise<SessionEvent> {
|
|
185
|
+
return await this.sendEvent(workspaceId, sessionId, {
|
|
186
|
+
type: "user.interrupt",
|
|
187
|
+
...(options.clientEventId !== undefined ? { clientEventId: options.clientEventId } : {}),
|
|
188
|
+
payload: options.reason !== undefined ? { reason: options.reason } : {},
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async sendApprovalDecision(
|
|
193
|
+
workspaceId: string,
|
|
194
|
+
sessionId: string,
|
|
195
|
+
decision: { approvalId: string; decision: "approve" | "reject"; message?: string; clientEventId?: string },
|
|
196
|
+
): Promise<SessionEvent> {
|
|
197
|
+
const { clientEventId, ...payload } = decision;
|
|
198
|
+
return await this.sendEvent(workspaceId, sessionId, {
|
|
199
|
+
type: "user.approvalDecision",
|
|
200
|
+
...(clientEventId !== undefined ? { clientEventId } : {}),
|
|
201
|
+
payload,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Live-stream a session's events with automatic reconnect, resume from the
|
|
207
|
+
* last seen sequence, gap backfill, and duplicate suppression. See
|
|
208
|
+
* {@link streamSessionEvents} for the delivery guarantees.
|
|
209
|
+
*/
|
|
210
|
+
streamEvents(
|
|
211
|
+
workspaceId: string,
|
|
212
|
+
sessionId: string,
|
|
213
|
+
options: StreamSessionEventsOptions = {},
|
|
214
|
+
): AsyncGenerator<SessionEvent, void, void> {
|
|
215
|
+
return streamSessionEvents(this.eventStreamTransport(workspaceId, sessionId), options);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** The transport `streamEvents` runs on; useful for custom streaming layers. */
|
|
219
|
+
eventStreamTransport(workspaceId: string, sessionId: string): SessionEventStreamTransport {
|
|
220
|
+
return {
|
|
221
|
+
openStream: async (after, signal) => await this.openEventStream(workspaceId, sessionId, { after, ...(signal ? { signal } : {}) }),
|
|
222
|
+
listEvents: async (after, limit) => await this.listEvents(workspaceId, sessionId, { after, limit }),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Open one raw SSE connection (no reconnect). Most callers want `streamEvents`. */
|
|
227
|
+
async openEventStream(
|
|
228
|
+
workspaceId: string,
|
|
229
|
+
sessionId: string,
|
|
230
|
+
options: { after?: number; signal?: AbortSignal } = {},
|
|
231
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
232
|
+
const url = this.url(`/v1/workspaces/${workspaceId}/sessions/${sessionId}/events/stream`, {
|
|
233
|
+
after: String(options.after ?? 0),
|
|
234
|
+
});
|
|
235
|
+
const response = await this.fetchImpl(url, {
|
|
236
|
+
method: "GET",
|
|
237
|
+
headers: { ...this.headers(), Accept: "text/event-stream" },
|
|
238
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
239
|
+
});
|
|
240
|
+
if (!response.ok) {
|
|
241
|
+
throw new OpenGeniApiError(response.status, await safeText(response));
|
|
242
|
+
}
|
|
243
|
+
if (!response.body) {
|
|
244
|
+
throw new OpenGeniApiError(response.status, "SSE response did not include a readable body");
|
|
245
|
+
}
|
|
246
|
+
return response.body;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// --- Turn queue ------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
/** Edit a still-queued turn (prompt, model, resources, tools, ...). */
|
|
252
|
+
async updateQueuedTurn(
|
|
253
|
+
workspaceId: string,
|
|
254
|
+
sessionId: string,
|
|
255
|
+
turnId: string,
|
|
256
|
+
update: UpdateSessionTurnRequest,
|
|
257
|
+
): Promise<SessionTurn> {
|
|
258
|
+
return await this.requestJson<SessionTurn>(
|
|
259
|
+
"PATCH",
|
|
260
|
+
`/v1/workspaces/${workspaceId}/sessions/${sessionId}/turns/${turnId}`,
|
|
261
|
+
update,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Reorder the queued turns. `turnIds` must all reference queued turns; the
|
|
267
|
+
* server assigns positions in the given order and returns the queue.
|
|
268
|
+
*/
|
|
269
|
+
async reorderQueuedTurns(workspaceId: string, sessionId: string, turnIds: string[]): Promise<SessionTurn[]> {
|
|
270
|
+
return await this.requestJson<SessionTurn[]>(
|
|
271
|
+
"POST",
|
|
272
|
+
`/v1/workspaces/${workspaceId}/sessions/${sessionId}/turns/reorder`,
|
|
273
|
+
{ turnIds },
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Cancel a queued turn before it is claimed. Returns the cancelled turn. */
|
|
278
|
+
async deleteQueuedTurn(workspaceId: string, sessionId: string, turnId: string): Promise<SessionTurn> {
|
|
279
|
+
return await this.requestJson<SessionTurn>(
|
|
280
|
+
"DELETE",
|
|
281
|
+
`/v1/workspaces/${workspaceId}/sessions/${sessionId}/turns/${turnId}`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Steer: deliver a message *now* instead of behind the queue. Sends the
|
|
287
|
+
* message, promotes its queued turn to the front, and interrupts the
|
|
288
|
+
* running turn so the session picks the steer turn up next. On a session
|
|
289
|
+
* that is not running this degrades gracefully to a plain queued message.
|
|
290
|
+
*
|
|
291
|
+
* The steer turn is located by `triggerEventId` across ALL turns (retried
|
|
292
|
+
* briefly in case the server is still materializing it) — not just the
|
|
293
|
+
* queued ones, because the worker can claim the steer turn before it is
|
|
294
|
+
* ever observed queued, and a claimed steer turn means the message is
|
|
295
|
+
* already being delivered: interrupting then would cancel the very message
|
|
296
|
+
* being steered. If the turn cannot be found while other turns are queued,
|
|
297
|
+
* the interrupt is also skipped — stopping the running turn would otherwise
|
|
298
|
+
* promote someone else's queued work over this message — and the call
|
|
299
|
+
* degrades to a plain queued send (`interrupted: false`).
|
|
300
|
+
*/
|
|
301
|
+
async steerMessage(
|
|
302
|
+
workspaceId: string,
|
|
303
|
+
sessionId: string,
|
|
304
|
+
message: string | SendMessageInput,
|
|
305
|
+
): Promise<SteerMessageResult> {
|
|
306
|
+
const accepted = await this.sendMessage(workspaceId, sessionId, message);
|
|
307
|
+
let steerTurn: SessionTurn | null = null;
|
|
308
|
+
let queued: SessionTurn[] = [];
|
|
309
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
310
|
+
if (attempt > 0) {
|
|
311
|
+
await delay(150 * attempt);
|
|
312
|
+
}
|
|
313
|
+
const turns = await this.listTurns(workspaceId, sessionId);
|
|
314
|
+
queued = turns
|
|
315
|
+
.filter((turn) => turn.status === "queued")
|
|
316
|
+
.sort((a, b) => a.position - b.position || a.createdAt.localeCompare(b.createdAt));
|
|
317
|
+
// Match against every turn, whatever its status: a steer turn that is
|
|
318
|
+
// already running/requires_action (or even finished) was claimed before
|
|
319
|
+
// this listing — that is delivery, not grounds for an interrupt.
|
|
320
|
+
steerTurn = turns.find((turn) => turn.triggerEventId === accepted.id) ?? null;
|
|
321
|
+
if (steerTurn) {
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const steerTurnQueued = steerTurn?.status === "queued";
|
|
326
|
+
if (steerTurn && steerTurnQueued && queued.length > 1) {
|
|
327
|
+
const front = steerTurn;
|
|
328
|
+
await this.reorderQueuedTurns(workspaceId, sessionId, [
|
|
329
|
+
front.id,
|
|
330
|
+
...queued.filter((turn) => turn.id !== front.id).map((turn) => turn.id),
|
|
331
|
+
]);
|
|
332
|
+
}
|
|
333
|
+
// Interrupting is only safe when the next claim is provably this message:
|
|
334
|
+
// either the steer turn sits queued (now at the front), or no turn
|
|
335
|
+
// materialized yet AND nothing else is queued. A steer turn observed in
|
|
336
|
+
// any non-queued state was already claimed — skip the interrupt.
|
|
337
|
+
const canDeliverNext = steerTurnQueued || (steerTurn === null && queued.length === 0);
|
|
338
|
+
const session = await this.getSession(workspaceId, sessionId);
|
|
339
|
+
// If the previously running turn already finished and the session claimed
|
|
340
|
+
// the steer turn itself, interrupting now would cancel the very message
|
|
341
|
+
// being steered. `activeTurnId` is the claim check; the residual window
|
|
342
|
+
// between this read and the interrupt landing is accepted (an interrupt
|
|
343
|
+
// can never be atomic with a status read over HTTP).
|
|
344
|
+
const steerTurnAlreadyActive = steerTurn !== null && session.activeTurnId === steerTurn.id;
|
|
345
|
+
const interrupted = canDeliverNext
|
|
346
|
+
&& !steerTurnAlreadyActive
|
|
347
|
+
&& (session.status === "running" || session.status === "requires_action");
|
|
348
|
+
if (interrupted) {
|
|
349
|
+
await this.interrupt(workspaceId, sessionId, { reason: "steer" });
|
|
350
|
+
}
|
|
351
|
+
return { accepted, turn: steerTurn, interrupted };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// --- Goals -------------------------------------------------------------------
|
|
355
|
+
|
|
356
|
+
/** The session's goal. 404s when the session never had one. */
|
|
357
|
+
async getGoal(workspaceId: string, sessionId: string): Promise<SessionGoal> {
|
|
358
|
+
return await this.requestJson<SessionGoal>("GET", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/goal`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async updateGoal(workspaceId: string, sessionId: string, request: UpdateSessionGoalRequest): Promise<SessionGoal> {
|
|
362
|
+
return await this.requestJson<SessionGoal>("PATCH", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/goal`, request);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Pause the goal loop: the session stops self-continuing until resumed. */
|
|
366
|
+
async pauseGoal(workspaceId: string, sessionId: string, options: { rationale?: string } = {}): Promise<SessionGoal> {
|
|
367
|
+
return await this.updateGoal(workspaceId, sessionId, {
|
|
368
|
+
status: "paused",
|
|
369
|
+
...(options.rationale !== undefined ? { rationale: options.rationale } : {}),
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Resume a paused goal: resets counters and re-arms the continuation loop. */
|
|
374
|
+
async resumeGoal(workspaceId: string, sessionId: string): Promise<SessionGoal> {
|
|
375
|
+
return await this.updateGoal(workspaceId, sessionId, { status: "active" });
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// --- Operator context controls (/clear, /compact) ---------------------------
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Clear the session's conversation context. Destructive and audit-preserving:
|
|
382
|
+
* the server supersedes (never deletes) the live history and emits a
|
|
383
|
+
* `session.context.cleared` event. Refused (409) while a turn is in flight or
|
|
384
|
+
* awaiting action. `confirm:true` is sent so an accidental call cannot wipe
|
|
385
|
+
* context — the destructive intent is explicit on the wire.
|
|
386
|
+
*/
|
|
387
|
+
async clearSessionContext(workspaceId: string, sessionId: string): Promise<void> {
|
|
388
|
+
await this.requestVoid("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/context/clear`, { confirm: true });
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Trigger conversation compaction now. On the client-managed (Azure) path this
|
|
393
|
+
* queues a forced compaction the worker honors before the next turn
|
|
394
|
+
* (`status:"queued"`); on a server-managed provider or when compaction is off
|
|
395
|
+
* it is a no-op (`status:"noop"`) with an explanatory message.
|
|
396
|
+
*/
|
|
397
|
+
async compactSessionContext(workspaceId: string, sessionId: string): Promise<CompactSessionContextResult> {
|
|
398
|
+
return await this.requestJson<CompactSessionContextResult>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/context/compact`, {});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// --- Access + workspaces -----------------------------------------------------
|
|
402
|
+
|
|
403
|
+
/** The caller's access context: subject, account + workspace grants, defaults. */
|
|
404
|
+
async getAccessContext(): Promise<AccessContext> {
|
|
405
|
+
return await this.requestJson<AccessContext>("GET", "/v1/access/me");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async listWorkspaces(): Promise<Workspace[]> {
|
|
409
|
+
return await this.requestJson<Workspace[]>("GET", "/v1/workspaces");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async createWorkspace(request: CreateWorkspaceRequest): Promise<Workspace> {
|
|
413
|
+
return await this.requestJson<Workspace>("POST", "/v1/workspaces", request);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async getWorkspace(workspaceId: string): Promise<Workspace> {
|
|
417
|
+
return await this.requestJson<Workspace>("GET", `/v1/workspaces/${workspaceId}`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async updateWorkspace(workspaceId: string, request: UpdateWorkspaceRequest): Promise<Workspace> {
|
|
421
|
+
return await this.requestJson<Workspace>("PATCH", `/v1/workspaces/${workspaceId}`, request);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// --- Scheduled tasks (write + runs) -------------------------------------------
|
|
425
|
+
|
|
426
|
+
async createScheduledTask(workspaceId: string, request: CreateScheduledTaskRequest): Promise<ScheduledTask> {
|
|
427
|
+
return await this.requestJson<ScheduledTask>("POST", `/v1/workspaces/${workspaceId}/scheduled-tasks`, request);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async updateScheduledTask(workspaceId: string, taskId: string, request: UpdateScheduledTaskRequest): Promise<ScheduledTask> {
|
|
431
|
+
return await this.requestJson<ScheduledTask>("PATCH", `/v1/workspaces/${workspaceId}/scheduled-tasks/${taskId}`, request);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async pauseScheduledTask(workspaceId: string, taskId: string): Promise<ScheduledTask> {
|
|
435
|
+
return await this.requestJson<ScheduledTask>("POST", `/v1/workspaces/${workspaceId}/scheduled-tasks/${taskId}/pause`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async resumeScheduledTask(workspaceId: string, taskId: string): Promise<ScheduledTask> {
|
|
439
|
+
return await this.requestJson<ScheduledTask>("POST", `/v1/workspaces/${workspaceId}/scheduled-tasks/${taskId}/resume`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Fire the task immediately (manual trigger), independent of its schedule.
|
|
444
|
+
* Pass a stable `triggerId` to make a retried trigger idempotent — the same
|
|
445
|
+
* token charges once and starts one run. Omit it and each call is distinct.
|
|
446
|
+
*/
|
|
447
|
+
async triggerScheduledTask(workspaceId: string, taskId: string, options: { triggerId?: string } = {}): Promise<ScheduledTask> {
|
|
448
|
+
return await this.requestJson<ScheduledTask>(
|
|
449
|
+
"POST",
|
|
450
|
+
`/v1/workspaces/${workspaceId}/scheduled-tasks/${taskId}/trigger`,
|
|
451
|
+
options.triggerId ? { triggerId: options.triggerId } : undefined,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async deleteScheduledTask(workspaceId: string, taskId: string): Promise<void> {
|
|
456
|
+
await this.requestJson<unknown>("DELETE", `/v1/workspaces/${workspaceId}/scheduled-tasks/${taskId}`);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async listScheduledTaskRuns(
|
|
460
|
+
workspaceId: string,
|
|
461
|
+
taskId: string,
|
|
462
|
+
options: { limit?: number } = {},
|
|
463
|
+
): Promise<ScheduledTaskRun[]> {
|
|
464
|
+
return await this.requestJson<ScheduledTaskRun[]>(
|
|
465
|
+
"GET",
|
|
466
|
+
`/v1/workspaces/${workspaceId}/scheduled-tasks/${taskId}/runs`,
|
|
467
|
+
undefined,
|
|
468
|
+
{ ...(options.limit !== undefined ? { limit: String(options.limit) } : {}) },
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// --- Environments --------------------------------------------------------------
|
|
473
|
+
// Variable values are write-only: reads return name/version metadata only.
|
|
474
|
+
|
|
475
|
+
async listEnvironments(workspaceId: string): Promise<WorkspaceEnvironment[]> {
|
|
476
|
+
return await this.requestJson<WorkspaceEnvironment[]>("GET", `/v1/workspaces/${workspaceId}/environments`);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async createEnvironment(workspaceId: string, request: CreateWorkspaceEnvironmentRequest): Promise<WorkspaceEnvironment> {
|
|
480
|
+
return await this.requestJson<WorkspaceEnvironment>("POST", `/v1/workspaces/${workspaceId}/environments`, request);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async getEnvironment(workspaceId: string, environmentId: string): Promise<WorkspaceEnvironment> {
|
|
484
|
+
return await this.requestJson<WorkspaceEnvironment>("GET", `/v1/workspaces/${workspaceId}/environments/${environmentId}`);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async updateEnvironment(
|
|
488
|
+
workspaceId: string,
|
|
489
|
+
environmentId: string,
|
|
490
|
+
request: UpdateWorkspaceEnvironmentRequest,
|
|
491
|
+
): Promise<WorkspaceEnvironment> {
|
|
492
|
+
return await this.requestJson<WorkspaceEnvironment>(
|
|
493
|
+
"PATCH",
|
|
494
|
+
`/v1/workspaces/${workspaceId}/environments/${environmentId}`,
|
|
495
|
+
request,
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async deleteEnvironment(workspaceId: string, environmentId: string): Promise<void> {
|
|
500
|
+
await this.requestJson<unknown>("DELETE", `/v1/workspaces/${workspaceId}/environments/${environmentId}`);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** Create or rotate a variable. The value never comes back on any read. */
|
|
504
|
+
async setEnvironmentVariable(
|
|
505
|
+
workspaceId: string,
|
|
506
|
+
environmentId: string,
|
|
507
|
+
name: string,
|
|
508
|
+
value: string,
|
|
509
|
+
): Promise<WorkspaceEnvironmentVariableMetadata> {
|
|
510
|
+
return await this.requestJson<WorkspaceEnvironmentVariableMetadata>(
|
|
511
|
+
"PUT",
|
|
512
|
+
`/v1/workspaces/${workspaceId}/environments/${environmentId}/variables/${encodeURIComponent(name)}`,
|
|
513
|
+
{ value },
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async deleteEnvironmentVariable(workspaceId: string, environmentId: string, name: string): Promise<void> {
|
|
518
|
+
await this.requestJson<unknown>(
|
|
519
|
+
"DELETE",
|
|
520
|
+
`/v1/workspaces/${workspaceId}/environments/${environmentId}/variables/${encodeURIComponent(name)}`,
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// --- Files -----------------------------------------------------------------------
|
|
525
|
+
|
|
526
|
+
/** Step 1 of the upload flow: returns the pre-signed PUT target. */
|
|
527
|
+
async beginFileUpload(workspaceId: string, request: CreateFileUploadRequest): Promise<CreateFileUploadResponse> {
|
|
528
|
+
return await this.requestJson<CreateFileUploadResponse>("POST", `/v1/workspaces/${workspaceId}/files/uploads`, request);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** Step 3 of the upload flow: server verifies the object and marks it ready. */
|
|
532
|
+
async completeFileUpload(workspaceId: string, uploadId: string): Promise<FileAsset> {
|
|
533
|
+
const response = await this.requestJson<CompleteFileUploadResponse>(
|
|
534
|
+
"POST",
|
|
535
|
+
`/v1/workspaces/${workspaceId}/files/uploads/${uploadId}/complete`,
|
|
536
|
+
);
|
|
537
|
+
return response.file;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* The whole upload flow as one call: begin -> PUT the bytes to the signed
|
|
542
|
+
* URL (with its required headers; no API auth is sent to object storage)
|
|
543
|
+
* -> complete. Returns the ready `FileAsset`.
|
|
544
|
+
*/
|
|
545
|
+
async uploadFile(workspaceId: string, input: UploadFileInput): Promise<FileAsset> {
|
|
546
|
+
// Copy Uint8Array views into a Blob so byte offsets/shared buffers can't
|
|
547
|
+
// leak surrounding bytes into the PUT body.
|
|
548
|
+
const body: Blob | ArrayBuffer | string = input.data instanceof Uint8Array
|
|
549
|
+
? new Blob([input.data.slice()])
|
|
550
|
+
: input.data;
|
|
551
|
+
const sizeBytes = typeof body === "string"
|
|
552
|
+
? new TextEncoder().encode(body).byteLength
|
|
553
|
+
: body instanceof Blob ? body.size : body.byteLength;
|
|
554
|
+
const upload = await this.beginFileUpload(workspaceId, {
|
|
555
|
+
filename: input.filename,
|
|
556
|
+
contentType: input.contentType,
|
|
557
|
+
sizeBytes,
|
|
558
|
+
...(input.sha256 !== undefined ? { sha256: input.sha256 } : {}),
|
|
559
|
+
});
|
|
560
|
+
const putResponse = await this.fetchImpl(upload.putUrl, {
|
|
561
|
+
method: "PUT",
|
|
562
|
+
// The backend's requiredHeaders already carry the canonical lowercase
|
|
563
|
+
// `content-type` for every storage backend (Azure/S3/GCS). Do NOT also set
|
|
564
|
+
// a `Content-Type` key here: WHATWG Headers treats the two casings as the
|
|
565
|
+
// same header and comma-joins their values (e.g. "text/plain, text/plain"),
|
|
566
|
+
// which the object store persists verbatim and COMPLETE then rejects (422),
|
|
567
|
+
// and which breaks S3's presigned-URL signature.
|
|
568
|
+
headers: { ...upload.requiredHeaders },
|
|
569
|
+
body,
|
|
570
|
+
});
|
|
571
|
+
if (!putResponse.ok) {
|
|
572
|
+
throw new OpenGeniApiError(putResponse.status, await safeText(putResponse));
|
|
573
|
+
}
|
|
574
|
+
return await this.completeFileUpload(workspaceId, upload.uploadId);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async getFile(workspaceId: string, fileId: string): Promise<FileAsset> {
|
|
578
|
+
return await this.requestJson<FileAsset>("GET", `/v1/workspaces/${workspaceId}/files/${fileId}`);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** Mint a short-lived signed download URL for a ready file. */
|
|
582
|
+
async createFileDownloadUrl(workspaceId: string, fileId: string): Promise<FileDownloadUrlResponse> {
|
|
583
|
+
return await this.requestJson<FileDownloadUrlResponse>("POST", `/v1/workspaces/${workspaceId}/files/${fileId}/download-url`);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// --- Documents ----------------------------------------------------------------------
|
|
587
|
+
|
|
588
|
+
async createDocumentBase(workspaceId: string, request: CreateDocumentBaseRequest): Promise<DocumentBase> {
|
|
589
|
+
return await this.requestJson<DocumentBase>("POST", `/v1/workspaces/${workspaceId}/document-bases`, request);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async listDocumentBases(workspaceId: string): Promise<DocumentBase[]> {
|
|
593
|
+
return await this.requestJson<DocumentBase[]>("GET", `/v1/workspaces/${workspaceId}/document-bases`);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async getDocumentBase(workspaceId: string, baseId: string): Promise<DocumentBase> {
|
|
597
|
+
return await this.requestJson<DocumentBase>("GET", `/v1/workspaces/${workspaceId}/document-bases/${baseId}`);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** Index an uploaded file into the base. The file must be `ready`. */
|
|
601
|
+
async addDocument(workspaceId: string, baseId: string, request: { fileId: string }): Promise<Document> {
|
|
602
|
+
return await this.requestJson<Document>("POST", `/v1/workspaces/${workspaceId}/document-bases/${baseId}/documents`, request);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async listDocuments(workspaceId: string, baseId: string): Promise<Document[]> {
|
|
606
|
+
return await this.requestJson<Document[]>("GET", `/v1/workspaces/${workspaceId}/document-bases/${baseId}/documents`);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** Retry indexing for a failed document. */
|
|
610
|
+
async reindexDocument(workspaceId: string, baseId: string, documentId: string): Promise<Document> {
|
|
611
|
+
return await this.requestJson<Document>(
|
|
612
|
+
"POST",
|
|
613
|
+
`/v1/workspaces/${workspaceId}/document-bases/${baseId}/documents/${documentId}/reindex`,
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async searchDocuments(
|
|
618
|
+
workspaceId: string,
|
|
619
|
+
baseId: string,
|
|
620
|
+
request: { query: string; limit?: number },
|
|
621
|
+
): Promise<DocumentSearchResponse> {
|
|
622
|
+
return await this.requestJson<DocumentSearchResponse>(
|
|
623
|
+
"POST",
|
|
624
|
+
`/v1/workspaces/${workspaceId}/document-bases/${baseId}/search`,
|
|
625
|
+
request,
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// --- Capability packs ------------------------------------------------------------------
|
|
630
|
+
|
|
631
|
+
/** Built-in + registered packs, with the workspace's installations. */
|
|
632
|
+
async listPacks(workspaceId: string): Promise<ListPacksResponse> {
|
|
633
|
+
return await this.requestJson<ListPacksResponse>("GET", `/v1/workspaces/${workspaceId}/packs`);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** Register (or replace) a workspace-scoped pack from a manifest. */
|
|
637
|
+
async registerPack(workspaceId: string, manifest: RegisterCapabilityPackRequest): Promise<WorkspaceRegisteredPack> {
|
|
638
|
+
return await this.requestJson<WorkspaceRegisteredPack>("POST", `/v1/workspaces/${workspaceId}/packs`, manifest);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async getPack(workspaceId: string, packId: string): Promise<GetPackResponse> {
|
|
642
|
+
return await this.requestJson<GetPackResponse>("GET", `/v1/workspaces/${workspaceId}/packs/${encodeURIComponent(packId)}`);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
async enablePack(workspaceId: string, packId: string, request: EnablePackRequest = {}): Promise<PackInstallation> {
|
|
646
|
+
return await this.requestJson<PackInstallation>(
|
|
647
|
+
"POST",
|
|
648
|
+
`/v1/workspaces/${workspaceId}/packs/${encodeURIComponent(packId)}/enable`,
|
|
649
|
+
request,
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/** Unregister a workspace-scoped pack (built-in packs cannot be deleted). */
|
|
654
|
+
async deletePack(workspaceId: string, packId: string): Promise<void> {
|
|
655
|
+
await this.requestVoid("DELETE", `/v1/workspaces/${workspaceId}/packs/${encodeURIComponent(packId)}`);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async listPackInstallations(workspaceId: string): Promise<PackInstallation[]> {
|
|
659
|
+
return await this.requestJson<PackInstallation[]>("GET", `/v1/workspaces/${workspaceId}/packs/installations`);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// --- Capabilities -------------------------------------------------------------------------
|
|
663
|
+
|
|
664
|
+
async listCapabilities(workspaceId: string): Promise<CapabilityCatalogResponse> {
|
|
665
|
+
return await this.requestJson<CapabilityCatalogResponse>("GET", `/v1/workspaces/${workspaceId}/capabilities`);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** Add a manual capability catalog item (e.g. a remote MCP server). */
|
|
669
|
+
async createCapability(workspaceId: string, request: CreateCapabilityCatalogItemRequest): Promise<CapabilityCatalogItem> {
|
|
670
|
+
return await this.requestJson<CapabilityCatalogItem>("POST", `/v1/workspaces/${workspaceId}/capabilities`, request);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
async enableCapability(
|
|
674
|
+
workspaceId: string,
|
|
675
|
+
capabilityId: string,
|
|
676
|
+
request: EnableCapabilityRequest = {},
|
|
677
|
+
): Promise<CapabilityInstallation> {
|
|
678
|
+
return await this.requestJson<CapabilityInstallation>(
|
|
679
|
+
"POST",
|
|
680
|
+
`/v1/workspaces/${workspaceId}/capabilities/${encodeURIComponent(capabilityId)}/enable`,
|
|
681
|
+
request,
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
async disableCapability(workspaceId: string, capabilityId: string): Promise<CapabilityInstallation> {
|
|
686
|
+
return await this.requestJson<CapabilityInstallation>(
|
|
687
|
+
"POST",
|
|
688
|
+
`/v1/workspaces/${workspaceId}/capabilities/${encodeURIComponent(capabilityId)}/disable`,
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/** Search the official MCP registry for installable capabilities. */
|
|
693
|
+
async discoverMcpCapabilities(
|
|
694
|
+
workspaceId: string,
|
|
695
|
+
options: { query?: string; limit?: number } = {},
|
|
696
|
+
): Promise<DiscoverMcpCapabilitiesResponse> {
|
|
697
|
+
return await this.requestJson<DiscoverMcpCapabilitiesResponse>(
|
|
698
|
+
"GET",
|
|
699
|
+
`/v1/workspaces/${workspaceId}/capabilities/discovery/mcp-registry`,
|
|
700
|
+
undefined,
|
|
701
|
+
{
|
|
702
|
+
...(options.query !== undefined ? { query: options.query } : {}),
|
|
703
|
+
...(options.limit !== undefined ? { limit: String(options.limit) } : {}),
|
|
704
|
+
},
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// --- GitHub ----------------------------------------------------------------------------------
|
|
709
|
+
|
|
710
|
+
/** GitHub App configuration status + a signed install URL when configured. */
|
|
711
|
+
async getGitHubApp(workspaceId: string): Promise<GitHubAppInfo> {
|
|
712
|
+
return await this.requestJson<GitHubAppInfo>("GET", `/v1/workspaces/${workspaceId}/github/app`);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Browser entry point that plants the CSRF cookie and forwards to GitHub's
|
|
717
|
+
* install page. Open this in a browser (it redirects); `state` comes from
|
|
718
|
+
* `getGitHubApp().installUrl` or a github_connect_link tool.
|
|
719
|
+
*/
|
|
720
|
+
githubConnectUrl(workspaceId: string, state: string): string {
|
|
721
|
+
return this.url(`/v1/workspaces/${workspaceId}/github/connect`, { state });
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
async listGitHubRepositories(workspaceId: string): Promise<GitHubRepositoriesResponse> {
|
|
725
|
+
return await this.requestJson<GitHubRepositoriesResponse>("GET", `/v1/workspaces/${workspaceId}/github/repositories`);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/** Re-sync the installation's repository list from GitHub. */
|
|
729
|
+
async syncGitHubRepositories(workspaceId: string): Promise<GitHubRepositoriesResponse> {
|
|
730
|
+
return await this.requestJson<GitHubRepositoriesResponse>("POST", `/v1/workspaces/${workspaceId}/github/repositories/sync`);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/** Build a GitHub App manifest + the GitHub URL to submit it to. */
|
|
734
|
+
async createGitHubAppManifest(
|
|
735
|
+
workspaceId: string,
|
|
736
|
+
request: CreateGitHubAppManifestRequest = {},
|
|
737
|
+
): Promise<CreateGitHubAppManifestResponse> {
|
|
738
|
+
return await this.requestJson<CreateGitHubAppManifestResponse>(
|
|
739
|
+
"POST",
|
|
740
|
+
`/v1/workspaces/${workspaceId}/github/app-manifest`,
|
|
741
|
+
request,
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// --- API keys ----------------------------------------------------------------------------------
|
|
746
|
+
|
|
747
|
+
async listApiKeys(workspaceId: string): Promise<ApiKey[]> {
|
|
748
|
+
const response = await this.requestJson<ListApiKeysResponse>("GET", `/v1/workspaces/${workspaceId}/api-keys`);
|
|
749
|
+
return response.apiKeys;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/** The returned `token` is shown once; only its prefix is stored. */
|
|
753
|
+
async createApiKey(workspaceId: string, request: CreateApiKeyRequest): Promise<CreateApiKeyResponse> {
|
|
754
|
+
return await this.requestJson<CreateApiKeyResponse>("POST", `/v1/workspaces/${workspaceId}/api-keys`, request);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/** Revoke an API key. Returns the revoked key. */
|
|
758
|
+
async deleteApiKey(workspaceId: string, apiKeyId: string): Promise<ApiKey> {
|
|
759
|
+
return await this.requestJson<ApiKey>("DELETE", `/v1/workspaces/${workspaceId}/api-keys/${apiKeyId}`);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// --- Billing (account-scoped) --------------------------------------------------------------------
|
|
763
|
+
|
|
764
|
+
async getBilling(options: { accountId?: string } = {}): Promise<BillingSummary> {
|
|
765
|
+
return await this.requestJson<BillingSummary>("GET", "/v1/billing", undefined, {
|
|
766
|
+
...(options.accountId !== undefined ? { accountId: options.accountId } : {}),
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
async getBillingUsage(options: { accountId?: string; workspaceId?: string } = {}): Promise<BillingUsageResponse> {
|
|
771
|
+
return await this.requestJson<BillingUsageResponse>("GET", "/v1/billing/usage", undefined, {
|
|
772
|
+
...(options.accountId !== undefined ? { accountId: options.accountId } : {}),
|
|
773
|
+
...(options.workspaceId !== undefined ? { workspaceId: options.workspaceId } : {}),
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
async getBillingEntitlements(options: { accountId?: string } = {}): Promise<BillingEntitlementsResponse> {
|
|
778
|
+
return await this.requestJson<BillingEntitlementsResponse>("GET", "/v1/billing/entitlements", undefined, {
|
|
779
|
+
...(options.accountId !== undefined ? { accountId: options.accountId } : {}),
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/** Start a Stripe checkout for prepaid credits. */
|
|
784
|
+
async createBillingCheckout(request: CreateCheckoutRequest): Promise<CreateCheckoutResponse> {
|
|
785
|
+
return await this.requestJson<CreateCheckoutResponse>("POST", "/v1/billing/checkout", request);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// --- Internals -------------------------------------------------------------
|
|
789
|
+
|
|
790
|
+
private headers(): Record<string, string> {
|
|
791
|
+
const extra = typeof this.options.headers === "function" ? this.options.headers() : this.options.headers;
|
|
792
|
+
return {
|
|
793
|
+
...(this.options.apiKey ? { Authorization: `Bearer ${this.options.apiKey}` } : {}),
|
|
794
|
+
...extra,
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
private url(path: string, query: Record<string, string> = {}): string {
|
|
799
|
+
const params = new URLSearchParams(query).toString();
|
|
800
|
+
return `${this.baseUrl}${path}${params ? `?${params}` : ""}`;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
private async requestJson<T>(method: string, path: string, body?: unknown, query: Record<string, string> = {}): Promise<T> {
|
|
804
|
+
const response = await this.fetchImpl(this.url(path, query), {
|
|
805
|
+
method,
|
|
806
|
+
headers: {
|
|
807
|
+
...this.headers(),
|
|
808
|
+
Accept: "application/json",
|
|
809
|
+
...(body !== undefined ? { "Content-Type": "application/json" } : {}),
|
|
810
|
+
},
|
|
811
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
812
|
+
});
|
|
813
|
+
if (!response.ok) {
|
|
814
|
+
throw new OpenGeniApiError(response.status, await safeText(response));
|
|
815
|
+
}
|
|
816
|
+
return (await response.json()) as T;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/** Like `requestJson` for endpoints that respond with no body (204). */
|
|
820
|
+
private async requestVoid(method: string, path: string, body?: unknown): Promise<void> {
|
|
821
|
+
const response = await this.fetchImpl(this.url(path), {
|
|
822
|
+
method,
|
|
823
|
+
headers: {
|
|
824
|
+
...this.headers(),
|
|
825
|
+
Accept: "application/json",
|
|
826
|
+
...(body !== undefined ? { "Content-Type": "application/json" } : {}),
|
|
827
|
+
},
|
|
828
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
829
|
+
});
|
|
830
|
+
if (!response.ok) {
|
|
831
|
+
throw new OpenGeniApiError(response.status, await safeText(response));
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
async function safeText(response: Response): Promise<string> {
|
|
837
|
+
try {
|
|
838
|
+
return await response.text();
|
|
839
|
+
} catch {
|
|
840
|
+
return "";
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function delay(ms: number): Promise<void> {
|
|
845
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
846
|
+
}
|