@opengeni/api-router 0.21.14 → 0.23.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/app.d.ts +1 -0
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +0 -30
- package/dist/{chunk-R5PDSH2A.js → chunk-T4T2PGU4.js} +3989 -1356
- package/dist/chunk-T4T2PGU4.js.map +1 -0
- package/dist/http/sse.d.ts +2 -0
- package/dist/index.js +29 -9
- package/dist/index.js.map +1 -1
- package/dist/integrations/oauth-client.d.ts +8 -0
- package/dist/integrations/slack-interactions.d.ts +7 -1
- package/dist/mcp/receipts.d.ts +28 -0
- package/dist/mcp/scheduled-task-view.d.ts +350 -0
- package/dist/mcp/toolspace.d.ts +9 -0
- package/dist/routes/transcription-recordings.d.ts +3 -0
- package/dist/sandbox/auth-callout.d.ts +2 -0
- package/dist/sandbox/channel-a.d.ts +5 -1
- package/dist/transcription/segmenter.d.ts +10 -0
- package/dist/transcription/service.d.ts +5 -0
- package/package.json +12 -12
- package/src/app.ts +39 -6
- package/src/auth/managed-auth.ts +0 -16
- package/src/http/sse.ts +101 -6
- package/src/index.ts +28 -3
- package/src/integrations/oauth-client.ts +36 -56
- package/src/integrations/slack-interactions.ts +123 -15
- package/src/mcp/documents.ts +42 -25
- package/src/mcp/receipts.ts +95 -0
- package/src/mcp/scheduled-task-view.ts +608 -0
- package/src/mcp/server.ts +812 -182
- package/src/mcp/toolspace.ts +75 -71
- package/src/observability.ts +3 -3
- package/src/routes/api-keys.ts +7 -1
- package/src/routes/codex.ts +7 -4
- package/src/routes/connections.ts +74 -3
- package/src/routes/enrollments.ts +54 -12
- package/src/routes/environments.ts +60 -11
- package/src/routes/files.ts +175 -65
- package/src/routes/install.ts +31 -1
- package/src/routes/machines.ts +1 -1
- package/src/routes/scheduled-tasks.ts +39 -14
- package/src/routes/sessions.ts +77 -12
- package/src/routes/transcription-recordings.ts +754 -0
- package/src/routes/transcriptions.ts +2 -0
- package/src/sandbox/auth-callout.ts +16 -4
- package/src/sandbox/channel-a.ts +124 -7
- package/src/sandbox/enrollment.ts +13 -3
- package/src/sandbox/machines.ts +1 -1
- package/src/sandbox/viewer.ts +29 -20
- package/src/transcription/providers/azure-openai.ts +4 -3
- package/src/transcription/providers/codex-subscription.ts +4 -1
- package/src/transcription/providers/openai.ts +7 -2
- package/src/transcription/segmenter.ts +260 -0
- package/src/transcription/service.ts +111 -10
- package/dist/chunk-R5PDSH2A.js.map +0 -1
|
@@ -5,8 +5,10 @@ import {
|
|
|
5
5
|
import { type ApiRouteDeps, requireAccessGrant, TranscriptionServiceError } from "@opengeni/core";
|
|
6
6
|
import { getWorkspace } from "@opengeni/db";
|
|
7
7
|
import type { Hono } from "hono";
|
|
8
|
+
import { registerResumableTranscriptionRoutes } from "./transcription-recordings";
|
|
8
9
|
|
|
9
10
|
export function registerTranscriptionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
11
|
+
registerResumableTranscriptionRoutes(app, deps);
|
|
10
12
|
app.post("/v1/workspaces/:workspaceId/transcriptions", async (c) => {
|
|
11
13
|
const workspaceId = c.req.param("workspaceId");
|
|
12
14
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:create");
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
// to, the `server_id` for the response `aud`, and the presented `auth_token`);
|
|
11
11
|
// 2. VALIDATES the bearer with verifyEnrollmentBearer (HMAC, via
|
|
12
12
|
// resolveEnrollmentSigningSecret) — an invalid/expired/forged bearer is denied;
|
|
13
|
-
// 3. confirms the enrollment is still ACTIVE in the DB
|
|
14
|
-
//
|
|
13
|
+
// 3. confirms the enrollment is still ACTIVE in the DB at the exact credential
|
|
14
|
+
// generation (a revoked or re-enrolled machine denies an old bearer);
|
|
15
15
|
// 4. signs a NATS user JWT granting pub/sub ONLY `agent.<ws>.>` + `_INBOX.>`
|
|
16
16
|
// (deny-all-else by an allow-list) and returns it inside a signed
|
|
17
17
|
// authorization-response JWT.
|
|
@@ -48,6 +48,8 @@ import { observabilityEventLogger } from "../observability";
|
|
|
48
48
|
|
|
49
49
|
/** The NATS subject nats-server publishes authorization requests on (ADR-26). */
|
|
50
50
|
export const AUTH_CALLOUT_SUBJECT = "$SYS.REQ.USER.AUTH";
|
|
51
|
+
/** Keep live NATS credentials short-lived while never outliving the bearer. */
|
|
52
|
+
export const NATS_USER_JWT_TTL_SECONDS = 5 * 60;
|
|
51
53
|
|
|
52
54
|
export interface AuthCalloutDeps {
|
|
53
55
|
db: Database;
|
|
@@ -123,13 +125,23 @@ export async function handleAuthorizationRequest(
|
|
|
123
125
|
// Belt-and-braces: the bearer's agentId/enrollmentId must match the row we found.
|
|
124
126
|
// (verifyEnrollmentBearer already binds them; this guards a future schema where
|
|
125
127
|
// agentId != enrollmentId.)
|
|
126
|
-
if (
|
|
128
|
+
if (
|
|
129
|
+
enrollment.workspaceId !== claims.workspaceId ||
|
|
130
|
+
enrollment.id !== claims.enrollmentId ||
|
|
131
|
+
enrollment.id !== claims.agentId ||
|
|
132
|
+
claims.agentId !== claims.enrollmentId ||
|
|
133
|
+
claims.subjectPrefix !== `agent.${claims.workspaceId}.${claims.agentId}`
|
|
134
|
+
) {
|
|
127
135
|
return deny("enrollment identity mismatch");
|
|
128
136
|
}
|
|
137
|
+
if (enrollment.credentialGeneration !== claims.credentialGeneration) {
|
|
138
|
+
return deny("enrollment credential generation mismatch");
|
|
139
|
+
}
|
|
129
140
|
|
|
130
141
|
// GRANT: a user JWT scoped to ONLY this workspace's agent subtree + the reply
|
|
131
142
|
// inbox. This allow-list IS the per-workspace isolation boundary.
|
|
132
143
|
const permissions = workspaceAgentPermissions(claims.workspaceId);
|
|
144
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
133
145
|
const userJwt = mintUserJwt({
|
|
134
146
|
userPublicKey: decoded.userNkey,
|
|
135
147
|
accountSeed: deps.callout.accountSeed,
|
|
@@ -142,7 +154,7 @@ export async function handleAuthorizationRequest(
|
|
|
142
154
|
audienceAccount: deps.callout.accountName,
|
|
143
155
|
// Tie the credential's life to the bearer's remaining life: a revoked/expired
|
|
144
156
|
// enrollment cannot outlive its bearer at the NATS layer either.
|
|
145
|
-
expiresAtSeconds: claims.exp,
|
|
157
|
+
expiresAtSeconds: Math.min(claims.exp, nowSeconds + NATS_USER_JWT_TTL_SECONDS),
|
|
146
158
|
});
|
|
147
159
|
const response = mintAuthResponse({
|
|
148
160
|
userPublicKey: decoded.userNkey,
|
package/src/sandbox/channel-a.ts
CHANGED
|
@@ -30,6 +30,7 @@ import type { Session } from "@opengeni/contracts";
|
|
|
30
30
|
import {
|
|
31
31
|
acquireLease,
|
|
32
32
|
getSandboxSessionEnvelope,
|
|
33
|
+
getEnrollment,
|
|
33
34
|
getSandbox,
|
|
34
35
|
loadWorkspaceEnvironmentForRun,
|
|
35
36
|
markWarmLeaseInstanceLost,
|
|
@@ -49,6 +50,7 @@ import {
|
|
|
49
50
|
isProviderSandboxNotFoundError,
|
|
50
51
|
SandboxChannelAService,
|
|
51
52
|
NatsControlRpc,
|
|
53
|
+
NatsOpStreamTransport,
|
|
52
54
|
ChannelAConflictError,
|
|
53
55
|
ChannelANotFoundError,
|
|
54
56
|
ChannelAUnsupportedError,
|
|
@@ -90,6 +92,97 @@ export type ChannelAHandle = {
|
|
|
90
92
|
requestId: string;
|
|
91
93
|
};
|
|
92
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Provider handles are lightweight references to a lease-owned sandbox, but
|
|
97
|
+
* reconstructing one is not free: Modal resume-by-id plus its first command can
|
|
98
|
+
* dominate a small Git/files read. Workspace panels issue several independent
|
|
99
|
+
* Channel-A requests together, so reuse the exact fenced handle briefly instead
|
|
100
|
+
* of making every request reattach to the same warm instance.
|
|
101
|
+
*
|
|
102
|
+
* The key includes the session, lease epoch, and immutable provider instance id.
|
|
103
|
+
* A rotation can therefore never inherit an old handle. Entries are bounded and
|
|
104
|
+
* expire opportunistically; eviction only drops local references and never
|
|
105
|
+
* terminates the lease-owned sandbox.
|
|
106
|
+
*/
|
|
107
|
+
const CHANNEL_A_HANDLE_CACHE_TTL_MS = 300_000;
|
|
108
|
+
const CHANNEL_A_HANDLE_CACHE_MAX_ENTRIES = 64;
|
|
109
|
+
type CachedEstablishedHandle = {
|
|
110
|
+
promise: Promise<EstablishedSandboxSession>;
|
|
111
|
+
lastUsedAt: number;
|
|
112
|
+
};
|
|
113
|
+
const establishedHandleCache = new Map<string, CachedEstablishedHandle>();
|
|
114
|
+
|
|
115
|
+
function establishedHandleCacheKey(
|
|
116
|
+
workspaceId: string,
|
|
117
|
+
sessionId: string,
|
|
118
|
+
lease: LeaseSnapshot,
|
|
119
|
+
): string {
|
|
120
|
+
return [workspaceId, sessionId, lease.leaseEpoch, lease.instanceId ?? ""].join("\u0000");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function pruneEstablishedHandleCache(now: number): void {
|
|
124
|
+
for (const [key, entry] of establishedHandleCache) {
|
|
125
|
+
if (now - entry.lastUsedAt > CHANNEL_A_HANDLE_CACHE_TTL_MS) {
|
|
126
|
+
establishedHandleCache.delete(key);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
while (establishedHandleCache.size >= CHANNEL_A_HANDLE_CACHE_MAX_ENTRIES) {
|
|
130
|
+
const oldestKey = establishedHandleCache.keys().next().value as string | undefined;
|
|
131
|
+
if (oldestKey === undefined) break;
|
|
132
|
+
establishedHandleCache.delete(oldestKey);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function establishCachedHandle(
|
|
137
|
+
key: string,
|
|
138
|
+
establish: () => Promise<EstablishedSandboxSession>,
|
|
139
|
+
): Promise<EstablishedSandboxSession> {
|
|
140
|
+
const now = Date.now();
|
|
141
|
+
pruneEstablishedHandleCache(now);
|
|
142
|
+
const cached = establishedHandleCache.get(key);
|
|
143
|
+
if (cached) {
|
|
144
|
+
cached.lastUsedAt = now;
|
|
145
|
+
// Refresh insertion order so the bounded map evicts the least-recently used
|
|
146
|
+
// exact lease identity first.
|
|
147
|
+
establishedHandleCache.delete(key);
|
|
148
|
+
establishedHandleCache.set(key, cached);
|
|
149
|
+
return await cached.promise;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const promise = establish();
|
|
153
|
+
const entry: CachedEstablishedHandle = { promise, lastUsedAt: now };
|
|
154
|
+
establishedHandleCache.set(key, entry);
|
|
155
|
+
try {
|
|
156
|
+
return await promise;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (establishedHandleCache.get(key) === entry) establishedHandleCache.delete(key);
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Reuse the exact lease-fenced provider handle across API-direct surfaces.
|
|
164
|
+
* Stream capability negotiation and the first Files/Changes reads commonly run
|
|
165
|
+
* back-to-back; sharing this handle avoids paying the same Modal resume twice. */
|
|
166
|
+
export async function establishCachedChannelAHandle(
|
|
167
|
+
workspaceId: string,
|
|
168
|
+
sessionId: string,
|
|
169
|
+
lease: LeaseSnapshot,
|
|
170
|
+
establish: () => Promise<EstablishedSandboxSession>,
|
|
171
|
+
): Promise<EstablishedSandboxSession> {
|
|
172
|
+
return await establishCachedHandle(
|
|
173
|
+
establishedHandleCacheKey(workspaceId, sessionId, lease),
|
|
174
|
+
establish,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function rememberEstablishedHandle(key: string, established: EstablishedSandboxSession): void {
|
|
179
|
+
pruneEstablishedHandleCache(Date.now());
|
|
180
|
+
establishedHandleCache.set(key, {
|
|
181
|
+
promise: Promise.resolve(established),
|
|
182
|
+
lastUsedAt: Date.now(),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
93
186
|
/**
|
|
94
187
|
* Run a Channel-A op against a live box, API-direct. Acquires an exact direct holder
|
|
95
188
|
* (warming the box when cold), resumes by id, builds the service, runs `fn`, and
|
|
@@ -169,7 +262,12 @@ export async function withChannelA<T>(
|
|
|
169
262
|
leaseEpoch: lease?.leaseEpoch ?? session.activeEpoch,
|
|
170
263
|
emit,
|
|
171
264
|
});
|
|
172
|
-
|
|
265
|
+
const result = await fn({ service, lease, routingSession, requestId });
|
|
266
|
+
// The direct request has accepted the result in memory. Finalize every
|
|
267
|
+
// Connected Machine backend the routing proxy reached so a mid-request
|
|
268
|
+
// route transition cannot leave completed output retained until TTL.
|
|
269
|
+
await routingSession.finalizeOpStreamOps().catch(() => undefined);
|
|
270
|
+
return result;
|
|
173
271
|
};
|
|
174
272
|
|
|
175
273
|
// A machine-targeted top-level session has an honest selfhosted HOME label.
|
|
@@ -190,6 +288,7 @@ export async function withChannelA<T>(
|
|
|
190
288
|
message: "machine-home session points to an unavailable Connected Machine",
|
|
191
289
|
});
|
|
192
290
|
}
|
|
291
|
+
const enrollment = await getEnrollment(db, workspaceId, sandbox.enrollmentId);
|
|
193
292
|
const built = await buildSelfhostedBackendSession({
|
|
194
293
|
workspaceId,
|
|
195
294
|
agentId: sandbox.enrollmentId,
|
|
@@ -200,6 +299,17 @@ export async function withChannelA<T>(
|
|
|
200
299
|
workingDir: pointer.workingDir,
|
|
201
300
|
timeoutMs: settings.sandboxSelfhostedControlTimeoutMs,
|
|
202
301
|
execTimeoutMs: settings.sandboxSelfhostedExecTimeoutMs,
|
|
302
|
+
...(settings.agentOpStreamEnabled === true &&
|
|
303
|
+
enrollment?.opStream === true &&
|
|
304
|
+
bus.getOpStreamConnection
|
|
305
|
+
? {
|
|
306
|
+
opStream: {
|
|
307
|
+
transport: new NatsOpStreamTransport(
|
|
308
|
+
async () => bus.getOpStreamConnection?.() ?? null,
|
|
309
|
+
),
|
|
310
|
+
},
|
|
311
|
+
}
|
|
312
|
+
: {}),
|
|
203
313
|
});
|
|
204
314
|
established = {
|
|
205
315
|
client: built.client,
|
|
@@ -271,6 +381,7 @@ export async function withChannelA<T>(
|
|
|
271
381
|
|
|
272
382
|
let established: EstablishedSandboxSession | undefined;
|
|
273
383
|
let leaseSnapshot: LeaseSnapshot = acquired.lease;
|
|
384
|
+
let establishedCacheKey: string | null = null;
|
|
274
385
|
|
|
275
386
|
try {
|
|
276
387
|
const envelope = await getSandboxSessionEnvelope(db, workspaceId, session.id);
|
|
@@ -303,6 +414,8 @@ export async function withChannelA<T>(
|
|
|
303
414
|
});
|
|
304
415
|
established = result.established;
|
|
305
416
|
leaseSnapshot = result.lease;
|
|
417
|
+
establishedCacheKey = establishedHandleCacheKey(workspaceId, session.id, leaseSnapshot);
|
|
418
|
+
rememberEstablishedHandle(establishedCacheKey, established);
|
|
306
419
|
} catch (error) {
|
|
307
420
|
throw new HTTPException(409, {
|
|
308
421
|
message: `sandbox not available (${error instanceof Error ? error.message : "spawn failed"})`,
|
|
@@ -323,17 +436,21 @@ export async function withChannelA<T>(
|
|
|
323
436
|
});
|
|
324
437
|
}
|
|
325
438
|
leaseSnapshot = live;
|
|
439
|
+
establishedCacheKey = establishedHandleCacheKey(workspaceId, session.id, live);
|
|
326
440
|
try {
|
|
327
|
-
established = await
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
441
|
+
established = await establishCachedChannelAHandle(workspaceId, session.id, live, () =>
|
|
442
|
+
establishSandboxSessionFromEnvelope(settings, live.resumeState, {
|
|
443
|
+
sessionId: session.id,
|
|
444
|
+
recovery: "resume-only",
|
|
445
|
+
backendOverride: session.sandboxBackend,
|
|
446
|
+
environment,
|
|
447
|
+
}),
|
|
448
|
+
);
|
|
333
449
|
} catch (error) {
|
|
334
450
|
if (!isProviderSandboxNotFoundError(session.sandboxBackend, error)) {
|
|
335
451
|
throw error;
|
|
336
452
|
}
|
|
453
|
+
establishedHandleCache.delete(establishedCacheKey);
|
|
337
454
|
const marked = await markWarmLeaseInstanceLost(db, {
|
|
338
455
|
accountId,
|
|
339
456
|
workspaceId,
|
|
@@ -75,8 +75,9 @@ export const DEVICE_POLL_INTERVAL_SECONDS = 5;
|
|
|
75
75
|
// box) caused a self-hosted agent to drop PERMANENTLY one hour after connecting: the
|
|
76
76
|
// bearer expired and the auth-callout rejected every reconnect ("re-enroll may be
|
|
77
77
|
// required"). A long-lived bearer is safe because the auth-callout RE-CHECKS the
|
|
78
|
-
// enrollment status on every (re)connect
|
|
79
|
-
//
|
|
78
|
+
// enrollment status AND credential generation on every (re)connect
|
|
79
|
+
// (auth-callout.ts) — a revoked machine or an old pre-re-enrollment bearer is denied
|
|
80
|
+
// regardless of bearer life. The short NATS user-JWT cap bounds already-live access.
|
|
80
81
|
export const ENROLLMENT_BEARER_TTL_SECONDS = 30 * 24 * 3600;
|
|
81
82
|
// The relay PRODUCER token (the `ogr_` token; M8b) is ENROLLMENT-scoped,
|
|
82
83
|
// NOT per-stream: the agent presents it on every channel registration for the life
|
|
@@ -370,6 +371,7 @@ export async function exchangeEnrollToken(
|
|
|
370
371
|
secret,
|
|
371
372
|
workspaceId: claims.workspaceId,
|
|
372
373
|
agentId: enrollment.id,
|
|
374
|
+
credentialGeneration: enrollment.credentialGeneration,
|
|
373
375
|
consentedScreenControl: enrollment.allowScreenControl,
|
|
374
376
|
});
|
|
375
377
|
return { ok: true, credentials };
|
|
@@ -430,6 +432,7 @@ export async function pollDeviceEnrollment(
|
|
|
430
432
|
secret,
|
|
431
433
|
workspaceId: request.workspaceId,
|
|
432
434
|
agentId: enrollment.id,
|
|
435
|
+
credentialGeneration: enrollment.credentialGeneration,
|
|
433
436
|
consentedScreenControl: enrollment.allowScreenControl,
|
|
434
437
|
});
|
|
435
438
|
|
|
@@ -455,7 +458,13 @@ export async function pollDeviceEnrollment(
|
|
|
455
458
|
* bearer so an agent using it as the connect-token credential works uniformly. */
|
|
456
459
|
async function buildEnrollmentCredentials(
|
|
457
460
|
services: EnrollmentServices,
|
|
458
|
-
input: {
|
|
461
|
+
input: {
|
|
462
|
+
secret: string;
|
|
463
|
+
workspaceId: string;
|
|
464
|
+
agentId: string;
|
|
465
|
+
credentialGeneration: number;
|
|
466
|
+
consentedScreenControl: boolean;
|
|
467
|
+
},
|
|
459
468
|
): Promise<EnrollmentCredentialsResponse> {
|
|
460
469
|
const { settings } = services;
|
|
461
470
|
// The control-plane subject prefix the agent subscribes to: agent.<ws>.<id>.
|
|
@@ -466,6 +475,7 @@ async function buildEnrollmentCredentials(
|
|
|
466
475
|
workspaceId: input.workspaceId,
|
|
467
476
|
agentId: input.agentId,
|
|
468
477
|
enrollmentId: input.agentId,
|
|
478
|
+
credentialGeneration: input.credentialGeneration,
|
|
469
479
|
subjectPrefix,
|
|
470
480
|
exp,
|
|
471
481
|
});
|
package/src/sandbox/machines.ts
CHANGED
|
@@ -219,7 +219,7 @@ export async function listMachines(
|
|
|
219
219
|
// onto the machines (no N+1). Each machine is probed for liveness.
|
|
220
220
|
const [sandboxes, enrollments, metricsByEnrollment] = await Promise.all([
|
|
221
221
|
listSandboxes(db, workspaceId),
|
|
222
|
-
listEnrollments(db, workspaceId),
|
|
222
|
+
listEnrollments(db, workspaceId, { status: "active" }),
|
|
223
223
|
readMachineMetricsLatestForWorkspace(db, workspaceId),
|
|
224
224
|
]);
|
|
225
225
|
const enrollmentById = new Map(enrollments.map((e) => [e.id, e]));
|
package/src/sandbox/viewer.ts
CHANGED
|
@@ -74,6 +74,7 @@ import {
|
|
|
74
74
|
} from "@opengeni/runtime/sandbox";
|
|
75
75
|
import { relayConfigFromSettings } from "@opengeni/core";
|
|
76
76
|
import { establishApiSandboxSpawner } from "./rematerialize";
|
|
77
|
+
import { establishCachedChannelAHandle } from "./channel-a";
|
|
77
78
|
|
|
78
79
|
/** The minimal services a viewer op needs: the DB + settings (lease cadence +
|
|
79
80
|
* the sandbox client construction the leaf reads from settings). The bus is
|
|
@@ -730,18 +731,22 @@ export async function mintDesktopStream(
|
|
|
730
731
|
// SAME stable run-env the turn declares, so a later turn finds no env delta.
|
|
731
732
|
const environment = await sessionAttachEnvironment(services, workspaceId, session);
|
|
732
733
|
try {
|
|
734
|
+
const establish = () =>
|
|
735
|
+
(services.establishSandboxSession ?? establishSandboxSessionFromEnvelope)(
|
|
736
|
+
settings,
|
|
737
|
+
envelope,
|
|
738
|
+
{
|
|
739
|
+
sessionId: session.id,
|
|
740
|
+
recovery: "resume-only",
|
|
741
|
+
backendOverride: session.sandboxBackend,
|
|
742
|
+
environment,
|
|
743
|
+
},
|
|
744
|
+
);
|
|
733
745
|
established = input.establish
|
|
734
746
|
? await input.establish(envelope)
|
|
735
|
-
:
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
{
|
|
739
|
-
sessionId: session.id,
|
|
740
|
-
recovery: "resume-only",
|
|
741
|
-
backendOverride: session.sandboxBackend,
|
|
742
|
-
environment,
|
|
743
|
-
},
|
|
744
|
-
);
|
|
747
|
+
: services.establishSandboxSession
|
|
748
|
+
? await establish()
|
|
749
|
+
: await establishCachedChannelAHandle(workspaceId, session.id, lease, establish);
|
|
745
750
|
} catch (error) {
|
|
746
751
|
await retireMissingWarmLease(services, { accountId, workspaceId, session, lease }, error);
|
|
747
752
|
return null;
|
|
@@ -955,18 +960,22 @@ export async function mintTerminalStream(
|
|
|
955
960
|
// declares, so a later turn finds no manifest-env delta.
|
|
956
961
|
const environment = await sessionAttachEnvironment(services, workspaceId, session);
|
|
957
962
|
try {
|
|
963
|
+
const establish = () =>
|
|
964
|
+
(services.establishSandboxSession ?? establishSandboxSessionFromEnvelope)(
|
|
965
|
+
settings,
|
|
966
|
+
envelope,
|
|
967
|
+
{
|
|
968
|
+
sessionId: session.id,
|
|
969
|
+
recovery: "resume-only",
|
|
970
|
+
backendOverride: session.sandboxBackend,
|
|
971
|
+
environment,
|
|
972
|
+
},
|
|
973
|
+
);
|
|
958
974
|
established = input.establish
|
|
959
975
|
? await input.establish(envelope)
|
|
960
|
-
:
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
{
|
|
964
|
-
sessionId: session.id,
|
|
965
|
-
recovery: "resume-only",
|
|
966
|
-
backendOverride: session.sandboxBackend,
|
|
967
|
-
environment,
|
|
968
|
-
},
|
|
969
|
-
);
|
|
976
|
+
: services.establishSandboxSession
|
|
977
|
+
? await establish()
|
|
978
|
+
: await establishCachedChannelAHandle(workspaceId, session.id, lease, establish);
|
|
970
979
|
} catch (error) {
|
|
971
980
|
await retireMissingWarmLease(services, { accountId, workspaceId, session, lease }, error);
|
|
972
981
|
return null;
|
|
@@ -13,13 +13,14 @@ export function createAzureOpenAiTranscriptionProvider(input: {
|
|
|
13
13
|
const url = `${input.endpoint}/openai/deployments/${encodeURIComponent(input.deployment)}/audio/transcriptions?api-version=${encodeURIComponent(input.apiVersion)}`;
|
|
14
14
|
return {
|
|
15
15
|
id: "azure-openai",
|
|
16
|
+
supportsServerDeadline: true,
|
|
16
17
|
available: () => Boolean(input.apiKey || input.adToken),
|
|
17
|
-
async transcribe({ audio, mimeType, filename, signal }) {
|
|
18
|
+
async transcribe({ audio, mimeType, filename, requestId, signal }) {
|
|
18
19
|
const form = new FormData();
|
|
19
20
|
form.append("file", new Blob([Uint8Array.from(audio).buffer], { type: mimeType }), filename);
|
|
20
21
|
const headers: Record<string, string> = input.apiKey
|
|
21
|
-
? { "api-key": input.apiKey }
|
|
22
|
-
: { Authorization: `Bearer ${input.adToken}
|
|
22
|
+
? { "api-key": input.apiKey, "x-opengeni-request-id": requestId }
|
|
23
|
+
: { Authorization: `Bearer ${input.adToken}`, "x-opengeni-request-id": requestId };
|
|
23
24
|
let response: Response;
|
|
24
25
|
try {
|
|
25
26
|
response = await fetchImpl(url, {
|
|
@@ -36,9 +36,10 @@ export function createCodexSubscriptionTranscriptionProvider(input: {
|
|
|
36
36
|
});
|
|
37
37
|
return {
|
|
38
38
|
id: "codex-subscription",
|
|
39
|
+
supportsServerDeadline: true,
|
|
39
40
|
experimental: true,
|
|
40
41
|
available: probe,
|
|
41
|
-
async transcribe({ audio, mimeType, filename, workspaceId, signal }) {
|
|
42
|
+
async transcribe({ audio, mimeType, filename, workspaceId, requestId, signal }) {
|
|
42
43
|
const account = (await listCodexAccountStatuses(input.db, workspaceId)).find(
|
|
43
44
|
(candidate) => candidate.isActive && candidate.status === "active",
|
|
44
45
|
);
|
|
@@ -73,6 +74,8 @@ export function createCodexSubscriptionTranscriptionProvider(input: {
|
|
|
73
74
|
originator: CODEX_ORIGINATOR,
|
|
74
75
|
"User-Agent": `${CODEX_ORIGINATOR}/${CODEX_CLIENT_VERSION}`,
|
|
75
76
|
version: CODEX_CLIENT_VERSION,
|
|
77
|
+
// Observability only; the upstream API is not treated as idempotent.
|
|
78
|
+
"x-opengeni-request-id": requestId,
|
|
76
79
|
},
|
|
77
80
|
body: form,
|
|
78
81
|
...(signal ? { signal } : {}),
|
|
@@ -9,8 +9,9 @@ export function createOpenAiTranscriptionProvider(input: {
|
|
|
9
9
|
const fetchImpl = input.fetch ?? fetch;
|
|
10
10
|
return {
|
|
11
11
|
id: "openai",
|
|
12
|
+
supportsServerDeadline: true,
|
|
12
13
|
available: () => true,
|
|
13
|
-
async transcribe({ audio, mimeType, filename, signal }) {
|
|
14
|
+
async transcribe({ audio, mimeType, filename, requestId, signal }) {
|
|
14
15
|
const form = new FormData();
|
|
15
16
|
form.append("file", audioBlob(audio, mimeType), filename);
|
|
16
17
|
form.append("model", input.model);
|
|
@@ -18,7 +19,11 @@ export function createOpenAiTranscriptionProvider(input: {
|
|
|
18
19
|
try {
|
|
19
20
|
response = await fetchImpl(`${input.baseUrl}/audio/transcriptions`, {
|
|
20
21
|
method: "POST",
|
|
21
|
-
headers: {
|
|
22
|
+
headers: {
|
|
23
|
+
Authorization: `Bearer ${input.apiKey}`,
|
|
24
|
+
// Observability only; the upstream API is not treated as idempotent.
|
|
25
|
+
"x-opengeni-request-id": requestId,
|
|
26
|
+
},
|
|
22
27
|
body: form,
|
|
23
28
|
...(signal ? { signal } : {}),
|
|
24
29
|
});
|