@opengeni/api-router 2.11.0-canary.0 → 2.11.2-canary.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/dist/app.js +1 -1
- package/dist/{chunk-EAYCECQK.js → chunk-5X3XM6CE.js} +1841 -1528
- package/dist/chunk-5X3XM6CE.js.map +1 -0
- package/dist/http/sse.d.ts +4 -3
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/integrations/github-skill-source.d.ts +1 -5
- package/dist/mcp/remember.d.ts +1 -1
- package/dist/mcp/scheduled-task-view.d.ts +3 -3
- package/dist/mcp/server.d.ts +4 -4
- package/dist/routes/skill-content.d.ts +4 -0
- package/dist/routes/skill-install-authority.d.ts +6 -0
- package/dist/transcription/providers/xai-subscription.d.ts +1 -0
- package/dist/transcription/service.d.ts +2 -1
- package/package.json +19 -19
- package/src/app.ts +18 -0
- package/src/http/sse.ts +9 -25
- package/src/index.ts +3 -1
- package/src/integrations/github-skill-source.ts +2 -142
- package/src/mcp/company-brain-governed-writes.ts +15 -21
- package/src/mcp/remember.ts +14 -15
- package/src/mcp/server.ts +7 -4
- package/src/routes/automations.ts +35 -0
- package/src/routes/packs.ts +61 -8
- package/src/routes/plugins.ts +48 -5
- package/src/routes/preference-registry.ts +12 -93
- package/src/routes/sessions.ts +52 -20
- package/src/routes/skill-content.ts +226 -0
- package/src/routes/skill-install-authority.ts +64 -0
- package/src/routes/skills.ts +38 -3
- package/src/routes/transcription-recordings.ts +30 -1
- package/src/routes/transcriptions.ts +4 -0
- package/src/transcription/providers/codex-subscription.ts +2 -0
- package/src/transcription/providers/openai.ts +1 -0
- package/src/transcription/providers/xai-subscription.ts +49 -3
- package/src/transcription/service.ts +43 -6
- package/src/xai-subscription-auth.ts +9 -2
- package/dist/chunk-EAYCECQK.js.map +0 -1
package/src/routes/skills.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
portableSkillCapabilityId,
|
|
14
14
|
portableSkillPluginKey,
|
|
15
15
|
requireAccessGrant,
|
|
16
|
+
requireAccessGrantAuthorization,
|
|
16
17
|
resolveSkillImport,
|
|
17
18
|
type ApiRouteDeps,
|
|
18
19
|
type GitHubSkillSourceClient,
|
|
@@ -23,7 +24,9 @@ import {
|
|
|
23
24
|
listInstalledSkills,
|
|
24
25
|
PortableSkillInstallationVersionConflictError,
|
|
25
26
|
PortableSkillInstallationVersionRequiredError,
|
|
27
|
+
PortableSkillSourcePathConflictError,
|
|
26
28
|
uninstallPortableSkill,
|
|
29
|
+
SkillSourceRemovalAuthorityError,
|
|
27
30
|
} from "@opengeni/db";
|
|
28
31
|
import { loadSkillLibrarySkill, skillLibraryRepositoryUrl } from "@opengeni/runtime/skill-library";
|
|
29
32
|
import { createHash } from "node:crypto";
|
|
@@ -31,6 +34,8 @@ import { HTTPException } from "hono/http-exception";
|
|
|
31
34
|
import type { Hono } from "hono";
|
|
32
35
|
|
|
33
36
|
import { createGitHubSkillSourceClient } from "../integrations/github-skill-source";
|
|
37
|
+
import { registerSkillContentRoutes } from "./skill-content";
|
|
38
|
+
import { skillInstallerActor, skillRemovalActor } from "./skill-install-authority";
|
|
34
39
|
|
|
35
40
|
export type SkillRouteOverrides = Readonly<{
|
|
36
41
|
github?: GitHubSkillSourceClient;
|
|
@@ -41,6 +46,7 @@ export function registerSkillRoutes(
|
|
|
41
46
|
deps: ApiRouteDeps,
|
|
42
47
|
overrides: SkillRouteOverrides = {},
|
|
43
48
|
): void {
|
|
49
|
+
registerSkillContentRoutes(app, deps);
|
|
44
50
|
const github = overrides.github ?? createGitHubSkillSourceClient(deps.settings);
|
|
45
51
|
|
|
46
52
|
app.get("/v1/workspaces/:workspaceId/skills", async (c) => {
|
|
@@ -55,7 +61,14 @@ export function registerSkillRoutes(
|
|
|
55
61
|
|
|
56
62
|
app.post("/v1/workspaces/:workspaceId/skills/library/:libraryId/install", async (c) => {
|
|
57
63
|
const workspaceId = c.req.param("workspaceId");
|
|
58
|
-
const
|
|
64
|
+
const access = await requireAccessGrantAuthorization(
|
|
65
|
+
c,
|
|
66
|
+
deps,
|
|
67
|
+
workspaceId,
|
|
68
|
+
"capabilities:manage",
|
|
69
|
+
);
|
|
70
|
+
const { grant } = access;
|
|
71
|
+
const skillActor = skillInstallerActor(access);
|
|
59
72
|
const libraryId = decodeURIComponent(c.req.param("libraryId"));
|
|
60
73
|
const payload = InstallLibrarySkillRequest.parse(await c.req.json());
|
|
61
74
|
let loaded: ReturnType<typeof loadSkillLibrarySkill>;
|
|
@@ -81,6 +94,7 @@ export function registerSkillRoutes(
|
|
|
81
94
|
});
|
|
82
95
|
try {
|
|
83
96
|
const installed = await installPortableSkill(deps.db, {
|
|
97
|
+
skillActor,
|
|
84
98
|
accountId: grant.accountId,
|
|
85
99
|
workspaceId,
|
|
86
100
|
subjectId: grant.subjectId,
|
|
@@ -136,7 +150,14 @@ export function registerSkillRoutes(
|
|
|
136
150
|
|
|
137
151
|
app.post("/v1/workspaces/:workspaceId/skills/install", async (c) => {
|
|
138
152
|
const workspaceId = c.req.param("workspaceId");
|
|
139
|
-
const
|
|
153
|
+
const access = await requireAccessGrantAuthorization(
|
|
154
|
+
c,
|
|
155
|
+
deps,
|
|
156
|
+
workspaceId,
|
|
157
|
+
"capabilities:manage",
|
|
158
|
+
);
|
|
159
|
+
const { grant } = access;
|
|
160
|
+
const skillActor = skillInstallerActor(access);
|
|
140
161
|
const payload = InstallSkillRequest.parse(await c.req.json());
|
|
141
162
|
const resolved = await resolveForRoute(payload.url, github);
|
|
142
163
|
if (
|
|
@@ -153,6 +174,7 @@ export function registerSkillRoutes(
|
|
|
153
174
|
);
|
|
154
175
|
try {
|
|
155
176
|
const installed = await installPortableSkill(deps.db, {
|
|
177
|
+
skillActor,
|
|
156
178
|
accountId: grant.accountId,
|
|
157
179
|
workspaceId,
|
|
158
180
|
subjectId: grant.subjectId,
|
|
@@ -204,13 +226,21 @@ export function registerSkillRoutes(
|
|
|
204
226
|
|
|
205
227
|
app.delete("/v1/workspaces/:workspaceId/skills/:capabilityId", async (c) => {
|
|
206
228
|
const workspaceId = c.req.param("workspaceId");
|
|
207
|
-
const
|
|
229
|
+
const access = await requireAccessGrantAuthorization(
|
|
230
|
+
c,
|
|
231
|
+
deps,
|
|
232
|
+
workspaceId,
|
|
233
|
+
"capabilities:manage",
|
|
234
|
+
);
|
|
235
|
+
const { grant } = access;
|
|
236
|
+
const skillActor = skillRemovalActor(access);
|
|
208
237
|
const capabilityId = decodeURIComponent(c.req.param("capabilityId"));
|
|
209
238
|
const payload = UninstallSkillRequest.parse(await c.req.json());
|
|
210
239
|
try {
|
|
211
240
|
return c.json(
|
|
212
241
|
UninstallSkillResult.parse(
|
|
213
242
|
await uninstallPortableSkill(deps.db, {
|
|
243
|
+
...(skillActor ? { skillActor } : {}),
|
|
214
244
|
accountId: grant.accountId,
|
|
215
245
|
workspaceId,
|
|
216
246
|
capabilityId,
|
|
@@ -224,12 +254,17 @@ export function registerSkillRoutes(
|
|
|
224
254
|
message: "The Skill changed after preview. Review uninstall impact again.",
|
|
225
255
|
});
|
|
226
256
|
}
|
|
257
|
+
if (error instanceof SkillSourceRemovalAuthorityError)
|
|
258
|
+
throw new HTTPException(403, { message: error.message });
|
|
227
259
|
throw error;
|
|
228
260
|
}
|
|
229
261
|
});
|
|
230
262
|
}
|
|
231
263
|
|
|
232
264
|
function portableSkillMutationHttpError(error: unknown): Error {
|
|
265
|
+
if (error instanceof PortableSkillSourcePathConflictError) {
|
|
266
|
+
return new HTTPException(409, { message: error.message });
|
|
267
|
+
}
|
|
233
268
|
if (error instanceof PortableSkillInstallationVersionRequiredError) {
|
|
234
269
|
return new HTTPException(400, { message: error.message });
|
|
235
270
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
resolveWorkspaceVoiceInputEnabled,
|
|
6
6
|
TRANSCRIPTION_RECORDING_PROVIDER_SEGMENT_SECONDS,
|
|
7
7
|
TRANSCRIPTION_RECORDING_RECOVERY_RETRY_AFTER_MILLISECONDS,
|
|
8
|
+
WorkspaceVoiceInputSettings,
|
|
8
9
|
type TranscriptionRecordingErrorCode,
|
|
9
10
|
type TranscriptionRecordingResponse,
|
|
10
11
|
type UploadTranscriptionRecordingChunkResponse,
|
|
@@ -58,6 +59,7 @@ type RecordingAuthority = {
|
|
|
58
59
|
accountId: string;
|
|
59
60
|
workspaceId: string;
|
|
60
61
|
subjectId: string;
|
|
62
|
+
voiceInput?: WorkspaceVoiceInputSettings | undefined;
|
|
61
63
|
};
|
|
62
64
|
|
|
63
65
|
class RecordingProcessingError extends Error {
|
|
@@ -357,6 +359,8 @@ export function registerResumableTranscriptionRoutes(app: Hono, deps: ApiRouteDe
|
|
|
357
359
|
let authority: RecordingAuthority | null = null;
|
|
358
360
|
let attemptId: string | null = null;
|
|
359
361
|
let segmentNumber: number | null = null;
|
|
362
|
+
let attemptedProvider: string | null = null;
|
|
363
|
+
let fallbackAllowed = false;
|
|
360
364
|
try {
|
|
361
365
|
authority = await requireRecordingAuthority(c, deps, true);
|
|
362
366
|
const service = deps.transcription;
|
|
@@ -372,6 +376,8 @@ export function registerResumableTranscriptionRoutes(app: Hono, deps: ApiRouteDe
|
|
|
372
376
|
}
|
|
373
377
|
const selectedProvider = service.selectProvider
|
|
374
378
|
? await service.selectProvider({
|
|
379
|
+
preferredProvider: authority.voiceInput?.preferredProvider,
|
|
380
|
+
fallbackEnabled: authority.voiceInput?.fallbackEnabled,
|
|
375
381
|
workspaceId: authority.workspaceId,
|
|
376
382
|
subjectId: authority.subjectId,
|
|
377
383
|
})
|
|
@@ -395,6 +401,8 @@ export function registerResumableTranscriptionRoutes(app: Hono, deps: ApiRouteDe
|
|
|
395
401
|
);
|
|
396
402
|
}
|
|
397
403
|
segmentNumber = claim.segment.segmentNumber;
|
|
404
|
+
attemptedProvider = claim.segment.providerId;
|
|
405
|
+
fallbackAllowed = claim.fallbackAllowed === true;
|
|
398
406
|
const segmentKey = claim.segment.objectKey;
|
|
399
407
|
const stored = deps.objectStorage
|
|
400
408
|
? await retryWhileMissing(async () => deps.objectStorage!.getObjectBytes(segmentKey))
|
|
@@ -452,12 +460,32 @@ export function registerResumableTranscriptionRoutes(app: Hono, deps: ApiRouteDe
|
|
|
452
460
|
} catch (error) {
|
|
453
461
|
if (authority && attemptId && segmentNumber !== null) {
|
|
454
462
|
const failure = processingFailure(error);
|
|
463
|
+
// Only an explicit rejection may move an untouched recording to another vendor.
|
|
464
|
+
// The database fences this against stale attempts and completed segments.
|
|
465
|
+
const fallbackProviderId =
|
|
466
|
+
fallbackAllowed &&
|
|
467
|
+
error instanceof TranscriptionServiceError &&
|
|
468
|
+
error.fallbackSafe &&
|
|
469
|
+
authority.voiceInput?.fallbackEnabled !== false &&
|
|
470
|
+
attemptedProvider &&
|
|
471
|
+
deps.transcription?.selectProvider
|
|
472
|
+
? await Promise.resolve(
|
|
473
|
+
deps.transcription.selectProvider({
|
|
474
|
+
workspaceId: authority.workspaceId,
|
|
475
|
+
subjectId: authority.subjectId,
|
|
476
|
+
preferredProvider: authority.voiceInput?.preferredProvider,
|
|
477
|
+
excludedProviders: [attemptedProvider],
|
|
478
|
+
afterProvider: attemptedProvider,
|
|
479
|
+
}),
|
|
480
|
+
).catch(() => null)
|
|
481
|
+
: null;
|
|
455
482
|
const persisted = await failTranscriptionRecordingSegment(deps.db, {
|
|
456
483
|
workspaceId: authority.workspaceId,
|
|
457
484
|
subjectId: authority.subjectId,
|
|
458
485
|
recordingId: uuidParam(c, "recordingId"),
|
|
459
486
|
segmentNumber,
|
|
460
487
|
attemptId,
|
|
488
|
+
fallbackProviderId,
|
|
461
489
|
errorCode: failure.code,
|
|
462
490
|
retryable: failure.retryable,
|
|
463
491
|
}).catch(() => null);
|
|
@@ -505,14 +533,15 @@ async function requireRecordingAuthority(
|
|
|
505
533
|
throw new TranscriptionRecordingNotFoundError("Workspace not found");
|
|
506
534
|
}
|
|
507
535
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:create");
|
|
536
|
+
const workspace = await getWorkspace(deps.db, workspaceId);
|
|
508
537
|
if (requirePolicy) {
|
|
509
|
-
const workspace = await getWorkspace(deps.db, workspaceId);
|
|
510
538
|
if (!workspace) throw new TranscriptionRecordingNotFoundError("Workspace not found");
|
|
511
539
|
if (resolveWorkspaceVoiceInputEnabled(workspace.settings) === false) {
|
|
512
540
|
throw new RecordingProcessingError("Voice input is disabled", "policy_blocked", false);
|
|
513
541
|
}
|
|
514
542
|
}
|
|
515
543
|
return {
|
|
544
|
+
voiceInput: WorkspaceVoiceInputSettings.safeParse(workspace?.settings.voiceInput).data,
|
|
516
545
|
accountId: grant.accountId,
|
|
517
546
|
workspaceId,
|
|
518
547
|
subjectId: grant.subjectId,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
resolveWorkspaceVoiceInputEnabled,
|
|
3
|
+
WorkspaceVoiceInputSettings,
|
|
3
4
|
type TranscribeAudioResponse,
|
|
4
5
|
} from "@opengeni/contracts";
|
|
5
6
|
import { type ApiRouteDeps, requireAccessGrant, TranscriptionServiceError } from "@opengeni/core";
|
|
@@ -17,6 +18,7 @@ export function registerTranscriptionRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
17
18
|
if (resolveWorkspaceVoiceInputEnabled(workspace.settings) === false) {
|
|
18
19
|
return c.json({ code: "policy_blocked" }, 403);
|
|
19
20
|
}
|
|
21
|
+
const preferences = WorkspaceVoiceInputSettings.safeParse(workspace.settings.voiceInput).data;
|
|
20
22
|
const service = deps.transcription;
|
|
21
23
|
if (!service || !(await service.available({ workspaceId, subjectId: grant.subjectId }))) {
|
|
22
24
|
return c.json({ code: "unavailable" }, 503);
|
|
@@ -24,6 +26,8 @@ export function registerTranscriptionRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
24
26
|
try {
|
|
25
27
|
const body = await audioRequest(c.req.raw, service.limits().maxSizeBytes);
|
|
26
28
|
const result = await service.transcribe({
|
|
29
|
+
preferredProvider: preferences?.preferredProvider,
|
|
30
|
+
fallbackEnabled: preferences?.fallbackEnabled,
|
|
27
31
|
workspaceId,
|
|
28
32
|
accountId: grant.accountId,
|
|
29
33
|
subjectId: grant.subjectId,
|
|
@@ -45,6 +45,7 @@ export function createCodexSubscriptionTranscriptionProvider(input: {
|
|
|
45
45
|
);
|
|
46
46
|
if (!account) {
|
|
47
47
|
throw new TranscriptionServiceError({
|
|
48
|
+
fallbackSafe: true,
|
|
48
49
|
code: "unavailable",
|
|
49
50
|
message: "Transcription is unavailable.",
|
|
50
51
|
});
|
|
@@ -55,6 +56,7 @@ export function createCodexSubscriptionTranscriptionProvider(input: {
|
|
|
55
56
|
token = await resolver.getToken();
|
|
56
57
|
} catch {
|
|
57
58
|
throw new TranscriptionServiceError({
|
|
59
|
+
fallbackSafe: true,
|
|
58
60
|
code: "unavailable",
|
|
59
61
|
message: "Transcription is unavailable.",
|
|
60
62
|
});
|
|
@@ -53,6 +53,7 @@ function audioBlob(audio: Uint8Array, mimeType: string): Blob {
|
|
|
53
53
|
export function responseError(status: number): TranscriptionServiceError {
|
|
54
54
|
if (status === 401 || status === 403) {
|
|
55
55
|
return new TranscriptionServiceError({
|
|
56
|
+
fallbackSafe: true,
|
|
56
57
|
code: "unavailable",
|
|
57
58
|
message: "Transcription is unavailable.",
|
|
58
59
|
});
|
|
@@ -62,6 +62,7 @@ export function createXaiSubscriptionTranscriptionProvider(input: {
|
|
|
62
62
|
} catch {
|
|
63
63
|
throw new TranscriptionServiceError({
|
|
64
64
|
code: "unavailable",
|
|
65
|
+
fallbackSafe: true,
|
|
65
66
|
message: "Transcription is unavailable.",
|
|
66
67
|
});
|
|
67
68
|
}
|
|
@@ -86,16 +87,28 @@ export function createXaiSubscriptionTranscriptionProvider(input: {
|
|
|
86
87
|
...(signal ? { signal } : {}),
|
|
87
88
|
});
|
|
88
89
|
};
|
|
90
|
+
const tokenForRequest = async (refresh: boolean) => {
|
|
91
|
+
try {
|
|
92
|
+
return await (refresh ? auth.context.refresh() : auth.context.getToken());
|
|
93
|
+
} catch {
|
|
94
|
+
throw new TranscriptionServiceError({
|
|
95
|
+
code: "unavailable",
|
|
96
|
+
message: "Reconnect the SuperGrok account to use transcription.",
|
|
97
|
+
fallbackSafe: true,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
};
|
|
89
101
|
let response: Response;
|
|
90
102
|
try {
|
|
91
|
-
let token = await
|
|
103
|
+
let token = await tokenForRequest(false);
|
|
92
104
|
response = await request(token.accessToken);
|
|
93
|
-
if (response.status === 401) {
|
|
105
|
+
if (response.status === 401 || (await isXaiInvalidCredentialResponse(response))) {
|
|
94
106
|
await response.body?.cancel().catch(() => undefined);
|
|
95
|
-
token = await
|
|
107
|
+
token = await tokenForRequest(true);
|
|
96
108
|
response = await request(token.accessToken);
|
|
97
109
|
}
|
|
98
110
|
} catch (error) {
|
|
111
|
+
if (error instanceof TranscriptionServiceError) throw error;
|
|
99
112
|
throw fetchError(error);
|
|
100
113
|
}
|
|
101
114
|
if (!response.ok) throw responseError(response.status);
|
|
@@ -108,3 +121,36 @@ export function createXaiSubscriptionTranscriptionProvider(input: {
|
|
|
108
121
|
},
|
|
109
122
|
};
|
|
110
123
|
}
|
|
124
|
+
|
|
125
|
+
export async function isXaiInvalidCredentialResponse(response: Response): Promise<boolean> {
|
|
126
|
+
if (response.status !== 403) return false;
|
|
127
|
+
const reader = response.clone().body?.getReader();
|
|
128
|
+
if (!reader) return false;
|
|
129
|
+
const chunks: Uint8Array[] = [];
|
|
130
|
+
let size = 0;
|
|
131
|
+
try {
|
|
132
|
+
while (true) {
|
|
133
|
+
const chunk = await reader.read();
|
|
134
|
+
if (chunk.done) break;
|
|
135
|
+
size += chunk.value.byteLength;
|
|
136
|
+
if (size > 16_384) return false;
|
|
137
|
+
chunks.push(chunk.value);
|
|
138
|
+
}
|
|
139
|
+
const bytes = new Uint8Array(size);
|
|
140
|
+
let offset = 0;
|
|
141
|
+
for (const chunk of chunks) {
|
|
142
|
+
bytes.set(chunk, offset);
|
|
143
|
+
offset += chunk.byteLength;
|
|
144
|
+
}
|
|
145
|
+
const body = JSON.parse(new TextDecoder().decode(bytes));
|
|
146
|
+
return (
|
|
147
|
+
typeof body?.error === "string" &&
|
|
148
|
+
body.error.includes("[WKE=unauthenticated:bad-credentials]")
|
|
149
|
+
);
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
} finally {
|
|
153
|
+
void reader.cancel().catch(() => undefined);
|
|
154
|
+
reader.releaseLock();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -74,7 +74,7 @@ export function createTranscriptionService(input: {
|
|
|
74
74
|
);
|
|
75
75
|
},
|
|
76
76
|
async selectProvider(context) {
|
|
77
|
-
return (await firstAvailable(providers, context))?.id ?? null;
|
|
77
|
+
return (await firstAvailable(orderedProviders(providers, context), context))?.id ?? null;
|
|
78
78
|
},
|
|
79
79
|
async transcribe(request) {
|
|
80
80
|
const mimeType = normalizeMimeType(request.mimeType);
|
|
@@ -106,12 +106,10 @@ export function createTranscriptionService(input: {
|
|
|
106
106
|
workspaceId: request.workspaceId,
|
|
107
107
|
subjectId: request.subjectId,
|
|
108
108
|
})
|
|
109
|
-
: await firstAvailable(providers,
|
|
110
|
-
workspaceId: request.workspaceId,
|
|
111
|
-
subjectId: request.subjectId,
|
|
112
|
-
});
|
|
109
|
+
: await firstAvailable(orderedProviders(providers, request), request);
|
|
113
110
|
if (!provider) {
|
|
114
111
|
throw new TranscriptionServiceError({
|
|
112
|
+
fallbackSafe: true,
|
|
115
113
|
code: "unavailable",
|
|
116
114
|
message: "Transcription is unavailable.",
|
|
117
115
|
});
|
|
@@ -164,6 +162,23 @@ export function createTranscriptionService(input: {
|
|
|
164
162
|
retryable: true,
|
|
165
163
|
});
|
|
166
164
|
}
|
|
165
|
+
if (
|
|
166
|
+
error instanceof TranscriptionServiceError &&
|
|
167
|
+
error.fallbackSafe &&
|
|
168
|
+
!request.providerId &&
|
|
169
|
+
request.fallbackEnabled !== false &&
|
|
170
|
+
!request.signal?.aborted
|
|
171
|
+
) {
|
|
172
|
+
const excludedProviders = [...(request.excludedProviders ?? []), provider.id];
|
|
173
|
+
if (
|
|
174
|
+
await firstAvailable(
|
|
175
|
+
orderedProviders(providers, { ...request, excludedProviders }),
|
|
176
|
+
request,
|
|
177
|
+
)
|
|
178
|
+
) {
|
|
179
|
+
return await this.transcribe({ ...request, excludedProviders });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
167
182
|
throw error;
|
|
168
183
|
} finally {
|
|
169
184
|
deadline.dispose();
|
|
@@ -217,12 +232,34 @@ function createProviderRequestDeadline(
|
|
|
217
232
|
};
|
|
218
233
|
}
|
|
219
234
|
|
|
235
|
+
export function orderedProviders(
|
|
236
|
+
providers: readonly TranscriptionProvider[],
|
|
237
|
+
context: TranscriptionAvailabilityContext,
|
|
238
|
+
): TranscriptionProvider[] {
|
|
239
|
+
const preferred = providers.filter((provider) => provider.id === context.preferredProvider);
|
|
240
|
+
const ordered = context.preferredProvider
|
|
241
|
+
? context.fallbackEnabled === false
|
|
242
|
+
? preferred
|
|
243
|
+
: [...preferred, ...providers.filter((provider) => provider.id !== context.preferredProvider)]
|
|
244
|
+
: context.fallbackEnabled === false
|
|
245
|
+
? providers.slice(0, 1)
|
|
246
|
+
: [...providers];
|
|
247
|
+
const remaining = context.afterProvider
|
|
248
|
+
? ordered.slice(ordered.findIndex((provider) => provider.id === context.afterProvider) + 1)
|
|
249
|
+
: ordered;
|
|
250
|
+
return remaining.filter((provider) => !context.excludedProviders?.includes(provider.id));
|
|
251
|
+
}
|
|
252
|
+
|
|
220
253
|
async function firstAvailable(
|
|
221
254
|
providers: readonly TranscriptionProvider[],
|
|
222
255
|
context: TranscriptionAvailabilityContext,
|
|
223
256
|
) {
|
|
224
257
|
for (const provider of providers) {
|
|
225
|
-
|
|
258
|
+
try {
|
|
259
|
+
if (await provider.available(context)) return provider;
|
|
260
|
+
} catch {
|
|
261
|
+
/* No audio sent; another configured provider may be ready. */
|
|
262
|
+
}
|
|
226
263
|
}
|
|
227
264
|
return null;
|
|
228
265
|
}
|
|
@@ -77,14 +77,20 @@ export async function buildXaiSubscriptionAuthorization(input: {
|
|
|
77
77
|
userId: credential.providerAccountId,
|
|
78
78
|
};
|
|
79
79
|
};
|
|
80
|
-
|
|
80
|
+
const authorization: XaiSubscriptionAuthorization = {
|
|
81
81
|
credentialId,
|
|
82
82
|
authoritySnapshot,
|
|
83
83
|
rotationEnabled: selected.rotationEnabled,
|
|
84
84
|
context: {
|
|
85
85
|
clientVersion: XAI_CLIENT_VERSION,
|
|
86
86
|
...(input.sessionId ? { sessionId: input.sessionId } : {}),
|
|
87
|
-
getToken: async () =>
|
|
87
|
+
getToken: async () => {
|
|
88
|
+
const expiresAt = xaiAccessTokenExpiry(credential.secret.accessToken ?? "");
|
|
89
|
+
if (expiresAt && expiresAt.getTime() <= Date.now() + 60_000) {
|
|
90
|
+
return await authorization.context.refresh();
|
|
91
|
+
}
|
|
92
|
+
return tokenSnapshot();
|
|
93
|
+
},
|
|
88
94
|
refresh: async () => {
|
|
89
95
|
const observedAccessToken = credential.secret.accessToken;
|
|
90
96
|
const observedRefreshToken = credential.secret.refreshToken;
|
|
@@ -129,4 +135,5 @@ export async function buildXaiSubscriptionAuthorization(input: {
|
|
|
129
135
|
},
|
|
130
136
|
},
|
|
131
137
|
};
|
|
138
|
+
return authorization;
|
|
132
139
|
}
|