@opengeni/api-router 2.6.4 → 2.10.0-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/access-grant-rls.d.ts +3 -0
- package/dist/app.d.ts +2 -1
- package/dist/app.js +3 -1
- package/dist/auth/organization-user-setup.d.ts +23 -0
- package/dist/{chunk-XTLI3CBH.js → chunk-XXH7DW34.js} +6866 -3554
- package/dist/chunk-XXH7DW34.js.map +1 -0
- package/dist/codemode.d.ts +6 -0
- package/dist/github-access.d.ts +25 -2
- package/dist/http/request-source.d.ts +14 -0
- package/dist/index.js +19 -3
- package/dist/index.js.map +1 -1
- package/dist/integrations/oauth-client.d.ts +2 -11
- package/dist/integrations/personal-github-repositories.d.ts +41 -2
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/mcp/server.d.ts +158 -1
- package/dist/mcp/session-view.d.ts +56 -2
- package/dist/mcp/session-wait.d.ts +1 -1
- package/dist/mcp-oauth.d.ts +19 -0
- package/dist/model-catalog.d.ts +5 -31
- package/dist/routes/codex.d.ts +27 -2
- package/dist/routes/github.d.ts +2 -0
- package/dist/routes/organization-model-providers.d.ts +3 -0
- package/dist/routes/workspace-artifacts.d.ts +2 -1
- package/dist/work-discovery-observability.d.ts +1 -1
- package/dist/workspace-artifact-content.d.ts +28 -0
- package/dist/workspace-artifact-provenance.d.ts +7 -0
- package/dist/workspace-deletion.d.ts +6 -0
- package/dist/workspace-tool-gateway-observability.d.ts +17 -0
- package/dist/workspace-tool-gateway.d.ts +118 -0
- package/package.json +19 -18
- package/src/access-grant-rls.ts +40 -0
- package/src/app.ts +296 -53
- package/src/auth/organization-user-setup.ts +95 -5
- package/src/codemode.ts +33 -4
- package/src/github-access.ts +117 -1
- package/src/http/auth.ts +11 -0
- package/src/http/request-source.ts +37 -0
- package/src/http/sse.ts +15 -7
- package/src/index.ts +18 -2
- package/src/integrations/oauth-client.ts +168 -203
- package/src/integrations/personal-github-repositories.ts +238 -9
- package/src/integrations/slack-app-home.ts +2 -2
- package/src/integrations/slack-interactions.ts +77 -37
- package/src/mcp/company-brain-governed-writes.ts +2 -2
- package/src/mcp/company-profile-agent-admin.ts +1 -1
- package/src/mcp/remember.ts +1 -1
- package/src/mcp/server.ts +504 -133
- package/src/mcp/session-view.ts +20 -1
- package/src/mcp/session-wait.ts +3 -0
- package/src/mcp-oauth.ts +539 -0
- package/src/model-catalog.ts +45 -337
- package/src/routes/automations.ts +178 -19
- package/src/routes/codex.ts +242 -73
- package/src/routes/connections.ts +271 -16
- package/src/routes/documents.ts +67 -19
- package/src/routes/files.ts +54 -6
- package/src/routes/github.ts +116 -15
- package/src/routes/organization-memberships.ts +59 -16
- package/src/routes/organization-model-providers.ts +231 -0
- package/src/routes/packs.ts +1 -0
- package/src/routes/personal-github.ts +39 -0
- package/src/routes/pr-review.ts +72 -4
- package/src/routes/scheduled-tasks.ts +40 -13
- package/src/routes/sessions.ts +153 -65
- package/src/routes/supergrok.ts +3 -2
- package/src/routes/workspace-artifacts.ts +176 -67
- package/src/routes/workspaces.ts +405 -62
- package/src/sandbox/channel-a.ts +17 -4
- package/src/work-discovery-observability.ts +3 -3
- package/src/workspace-artifact-content.ts +163 -0
- package/src/workspace-artifact-provenance.ts +104 -0
- package/src/workspace-deletion.ts +64 -0
- package/src/workspace-tool-gateway-observability.ts +100 -0
- package/src/workspace-tool-gateway.ts +691 -0
- package/dist/chunk-XTLI3CBH.js.map +0 -1
package/src/routes/workspaces.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import {
|
|
2
3
|
AddWorkspaceMemberRequest,
|
|
3
4
|
CreateWorkspaceRequest,
|
|
@@ -7,16 +8,25 @@ import {
|
|
|
7
8
|
ListWorkspaceMembersResponse,
|
|
8
9
|
SetWorkspaceDefaultRigRequest,
|
|
9
10
|
UpdateWorkspaceMemberRequest,
|
|
11
|
+
CreateWorkspaceGatewayCustomModelRequest,
|
|
12
|
+
CreateWorkspaceOpenRouterCustomModelRequest,
|
|
13
|
+
DeleteWorkspaceGatewayCustomModelRequest,
|
|
14
|
+
DeleteWorkspaceOpenRouterCustomModelRequest,
|
|
10
15
|
UpdateWorkspaceModelPolicyRequest,
|
|
11
16
|
UpdateWorkspaceRequest,
|
|
12
17
|
UpdateWorkspaceSettingsRequest,
|
|
13
18
|
WORKSPACE_CONTROL_ACTOR_MAX_BYTES,
|
|
14
19
|
WorkspaceModelCatalogResponse,
|
|
20
|
+
WorkspaceGatewayCustomModel,
|
|
21
|
+
WorkspaceGatewayCustomModelsResponse,
|
|
22
|
+
WorkspaceOpenRouterCustomModel,
|
|
23
|
+
WorkspaceOpenRouterCustomModelsResponse,
|
|
15
24
|
WorkspaceRealtimeModelCatalogResponse,
|
|
16
25
|
WorkspaceInferenceControlRequest,
|
|
17
26
|
Workspace,
|
|
18
27
|
WorkspaceMember,
|
|
19
28
|
workspaceControlUtf8Bytes,
|
|
29
|
+
stableJson,
|
|
20
30
|
type AccessContext,
|
|
21
31
|
type Permission,
|
|
22
32
|
type WorkspaceMemberCandidate,
|
|
@@ -25,7 +35,6 @@ import {
|
|
|
25
35
|
import {
|
|
26
36
|
allWorkspacePermissions,
|
|
27
37
|
createWorkspace,
|
|
28
|
-
deleteWorkspaceIfQuiescent,
|
|
29
38
|
ensureWorkspaceByExternalIdentity,
|
|
30
39
|
findWorkspaceByExternalIdentity,
|
|
31
40
|
getManagedUserProfilesByIds,
|
|
@@ -36,6 +45,14 @@ import {
|
|
|
36
45
|
normalizeWorkspaceMembershipPermissions,
|
|
37
46
|
listWorkspaceControlEvents,
|
|
38
47
|
listSharedWorkspacesForAccount,
|
|
48
|
+
listWorkspaceGatewayCustomModels,
|
|
49
|
+
listWorkspaceOpenRouterCustomModels,
|
|
50
|
+
createWorkspaceGatewayCustomModel,
|
|
51
|
+
createWorkspaceOpenRouterCustomModel,
|
|
52
|
+
deleteWorkspaceGatewayCustomModel,
|
|
53
|
+
deleteWorkspaceOpenRouterCustomModel,
|
|
54
|
+
replayWorkspaceGatewayCustomModelCreate,
|
|
55
|
+
replayWorkspaceOpenRouterCustomModelCreate,
|
|
39
56
|
listWorkspacesForSubject,
|
|
40
57
|
nestedPostgresSqlState,
|
|
41
58
|
removeWorkspaceMember,
|
|
@@ -50,7 +67,14 @@ import {
|
|
|
50
67
|
workspaceControlRequestLockTimeoutMs,
|
|
51
68
|
workspaceXaiSubscriptionActive,
|
|
52
69
|
workspaceVercelAiGatewayConnectionActive,
|
|
70
|
+
workspaceOpenRouterConnectionActive,
|
|
71
|
+
organizationModelProviderConnectionActiveForWorkspace,
|
|
72
|
+
listOrganizationModelProviderCustomModelsForWorkspace,
|
|
73
|
+
WorkspaceGatewayCustomModelHistoryLimitError,
|
|
74
|
+
WorkspaceOpenRouterCustomModelHistoryLimitError,
|
|
53
75
|
WorkspaceExternalIdentityConflictError,
|
|
76
|
+
WorkspaceGatewayCustomModelLimitError,
|
|
77
|
+
WorkspaceOpenRouterCustomModelLimitError,
|
|
54
78
|
WorkspaceLimitExceededError,
|
|
55
79
|
} from "@opengeni/db";
|
|
56
80
|
import { boundWorkspaceControlHttpPage } from "@opengeni/events";
|
|
@@ -63,6 +87,7 @@ import {
|
|
|
63
87
|
requireAccessContext,
|
|
64
88
|
requireAccessGrant,
|
|
65
89
|
requireFreshAccessGrant,
|
|
90
|
+
resolveWorkspaceCatalogSettings,
|
|
66
91
|
} from "@opengeni/core";
|
|
67
92
|
import { requireLimit } from "@opengeni/core";
|
|
68
93
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
@@ -75,14 +100,20 @@ import { boundedLimit } from "../http/common";
|
|
|
75
100
|
import { ApiHttpError } from "../http/api-error";
|
|
76
101
|
import { browserSseDeliveryOptions, sseWorkspaceControlStream } from "../http/sse";
|
|
77
102
|
import { buildWorkspaceModelCatalog } from "../model-catalog";
|
|
78
|
-
import {
|
|
79
|
-
import { workspaceDeleteObserver } from "../workspace-delete-observability";
|
|
103
|
+
import { deleteWorkspaceForRequest } from "../workspace-deletion";
|
|
80
104
|
import {
|
|
81
105
|
AI_GATEWAY_REALTIME_MODELS,
|
|
82
106
|
CODEX_REALTIME_MODEL_ID,
|
|
83
107
|
SUPERGROK_REALTIME_MODEL_ID,
|
|
84
108
|
canonicalizeConfiguredModelId,
|
|
85
109
|
configuredStaticUsageLimits,
|
|
110
|
+
configuredGatewayUpstreamModelIds,
|
|
111
|
+
configuredGatewayWorkspaceProductModelIds,
|
|
112
|
+
configuredModelInputIdentities,
|
|
113
|
+
configuredOpenRouterUpstreamModelIds,
|
|
114
|
+
configuredOpenRouterWorkspaceProductModelIds,
|
|
115
|
+
WORKSPACE_GATEWAY_MODEL_ID_PREFIX,
|
|
116
|
+
WORKSPACE_OPENROUTER_MODEL_ID_PREFIX,
|
|
86
117
|
type Settings,
|
|
87
118
|
} from "@opengeni/config";
|
|
88
119
|
|
|
@@ -96,6 +127,46 @@ export function canonicalWorkspacePolicyModelIds(
|
|
|
96
127
|
return [...new Set(modelIds.map((modelId) => canonicalizeConfiguredModelId(settings, modelId)))];
|
|
97
128
|
}
|
|
98
129
|
|
|
130
|
+
function projectWorkspaceGatewayCustomModel(model: {
|
|
131
|
+
id: string;
|
|
132
|
+
upstreamModelId: string;
|
|
133
|
+
label: string | null;
|
|
134
|
+
version: number;
|
|
135
|
+
createdAt: Date;
|
|
136
|
+
updatedAt: Date;
|
|
137
|
+
}) {
|
|
138
|
+
return WorkspaceGatewayCustomModel.parse({
|
|
139
|
+
id: model.id,
|
|
140
|
+
upstreamModelId: model.upstreamModelId,
|
|
141
|
+
label: model.label,
|
|
142
|
+
version: model.version,
|
|
143
|
+
createdAt: model.createdAt.toISOString(),
|
|
144
|
+
updatedAt: model.updatedAt.toISOString(),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function projectWorkspaceOpenRouterCustomModel(model: {
|
|
149
|
+
id: string;
|
|
150
|
+
upstreamModelId: string;
|
|
151
|
+
label: string | null;
|
|
152
|
+
version: number;
|
|
153
|
+
createdAt: Date;
|
|
154
|
+
updatedAt: Date;
|
|
155
|
+
}) {
|
|
156
|
+
return WorkspaceOpenRouterCustomModel.parse({
|
|
157
|
+
id: model.id,
|
|
158
|
+
upstreamModelId: model.upstreamModelId,
|
|
159
|
+
label: model.label,
|
|
160
|
+
version: model.version,
|
|
161
|
+
createdAt: model.createdAt.toISOString(),
|
|
162
|
+
updatedAt: model.updatedAt.toISOString(),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function workspaceCustomModelRequestHash(value: unknown): string {
|
|
167
|
+
return createHash("sha256").update(stableJson(value), "utf8").digest("hex");
|
|
168
|
+
}
|
|
169
|
+
|
|
99
170
|
type WorkspaceMemberProjectionInput = Omit<WorkspaceMemberValue, "permissions"> & {
|
|
100
171
|
permissions: unknown;
|
|
101
172
|
};
|
|
@@ -192,7 +263,9 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
192
263
|
name: payload.name,
|
|
193
264
|
slug: payload.slug ?? null,
|
|
194
265
|
...(payload.agentInstructions !== undefined
|
|
195
|
-
? {
|
|
266
|
+
? {
|
|
267
|
+
agentInstructions: normalizeAgentInstructions(payload.agentInstructions),
|
|
268
|
+
}
|
|
196
269
|
: {}),
|
|
197
270
|
maxWorkspacesPerAccount: workspaceLimit(deps),
|
|
198
271
|
});
|
|
@@ -317,30 +390,351 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
317
390
|
const workspaceId = c.req.param("workspaceId");
|
|
318
391
|
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
319
392
|
const [
|
|
393
|
+
resolvedCatalog,
|
|
320
394
|
policy,
|
|
321
395
|
codexSubscriptionActive,
|
|
322
396
|
xaiSubscriptionActive,
|
|
323
397
|
workspaceGatewayConnectionActive,
|
|
398
|
+
workspaceGatewayCustomModels,
|
|
399
|
+
openRouterConnectionActive,
|
|
400
|
+
workspaceOpenRouterCustomModels,
|
|
401
|
+
organizationGatewayConnectionActive,
|
|
402
|
+
organizationOpenRouterConnectionActive,
|
|
403
|
+
organizationGatewayCustomModels,
|
|
404
|
+
organizationOpenRouterCustomModels,
|
|
324
405
|
] = await Promise.all([
|
|
406
|
+
deps.resolveCatalogSettings(),
|
|
325
407
|
getWorkspaceModelPolicy(deps.db, workspaceId),
|
|
326
408
|
workspaceCodexSubscriptionActive(deps.db, deps.settings, workspaceId),
|
|
327
409
|
workspaceXaiSubscriptionActive(deps.db, deps.settings, workspaceId, grant.subjectId),
|
|
328
410
|
workspaceVercelAiGatewayConnectionActive(deps.db, workspaceId),
|
|
411
|
+
listWorkspaceGatewayCustomModels(deps.db, {
|
|
412
|
+
accountId: grant.accountId,
|
|
413
|
+
workspaceId,
|
|
414
|
+
}),
|
|
415
|
+
workspaceOpenRouterConnectionActive(deps.db, workspaceId),
|
|
416
|
+
listWorkspaceOpenRouterCustomModels(deps.db, {
|
|
417
|
+
accountId: grant.accountId,
|
|
418
|
+
workspaceId,
|
|
419
|
+
}),
|
|
420
|
+
organizationModelProviderConnectionActiveForWorkspace(deps.db, {
|
|
421
|
+
accountId: grant.accountId,
|
|
422
|
+
workspaceId,
|
|
423
|
+
providerKind: "vercel_gateway",
|
|
424
|
+
}),
|
|
425
|
+
organizationModelProviderConnectionActiveForWorkspace(deps.db, {
|
|
426
|
+
accountId: grant.accountId,
|
|
427
|
+
workspaceId,
|
|
428
|
+
providerKind: "openrouter",
|
|
429
|
+
}),
|
|
430
|
+
listOrganizationModelProviderCustomModelsForWorkspace(deps.db, {
|
|
431
|
+
accountId: grant.accountId,
|
|
432
|
+
workspaceId,
|
|
433
|
+
providerKind: "vercel_gateway",
|
|
434
|
+
}),
|
|
435
|
+
listOrganizationModelProviderCustomModelsForWorkspace(deps.db, {
|
|
436
|
+
accountId: grant.accountId,
|
|
437
|
+
workspaceId,
|
|
438
|
+
providerKind: "openrouter",
|
|
439
|
+
}),
|
|
329
440
|
]);
|
|
330
441
|
c.header("cache-control", "private, no-store");
|
|
331
442
|
return c.json(
|
|
332
443
|
WorkspaceModelCatalogResponse.parse(
|
|
333
444
|
buildWorkspaceModelCatalog({
|
|
334
|
-
settings:
|
|
445
|
+
settings: resolvedCatalog.settings,
|
|
335
446
|
policy,
|
|
336
447
|
codexSubscriptionActive,
|
|
337
448
|
xaiSubscriptionActive,
|
|
338
449
|
workspaceGatewayConnectionActive,
|
|
450
|
+
workspaceGatewayCustomModels,
|
|
451
|
+
workspaceOpenRouterConnectionActive: openRouterConnectionActive,
|
|
452
|
+
workspaceOpenRouterCustomModels,
|
|
453
|
+
organizationGatewayConnectionActive,
|
|
454
|
+
organizationOpenRouterConnectionActive,
|
|
455
|
+
organizationGatewayCustomModels,
|
|
456
|
+
organizationOpenRouterCustomModels,
|
|
339
457
|
}),
|
|
340
458
|
),
|
|
341
459
|
);
|
|
342
460
|
});
|
|
343
461
|
|
|
462
|
+
app.get("/v1/workspaces/:workspaceId/gateway-custom-models", async (c) => {
|
|
463
|
+
const workspaceId = c.req.param("workspaceId");
|
|
464
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
465
|
+
const models = await listWorkspaceGatewayCustomModels(deps.db, {
|
|
466
|
+
accountId: grant.accountId,
|
|
467
|
+
workspaceId,
|
|
468
|
+
});
|
|
469
|
+
c.header("cache-control", "private, no-store");
|
|
470
|
+
return c.json(
|
|
471
|
+
WorkspaceGatewayCustomModelsResponse.parse({
|
|
472
|
+
models: models.map(projectWorkspaceGatewayCustomModel),
|
|
473
|
+
}),
|
|
474
|
+
);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
app.post("/v1/workspaces/:workspaceId/gateway-custom-models", async (c) => {
|
|
478
|
+
const workspaceId = c.req.param("workspaceId");
|
|
479
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
480
|
+
const parsed = CreateWorkspaceGatewayCustomModelRequest.safeParse(
|
|
481
|
+
await c.req.json().catch(() => null),
|
|
482
|
+
);
|
|
483
|
+
if (!parsed.success) {
|
|
484
|
+
throw new HTTPException(422, { message: "invalid Gateway custom model" });
|
|
485
|
+
}
|
|
486
|
+
const requestHash = workspaceCustomModelRequestHash({
|
|
487
|
+
action: "create",
|
|
488
|
+
upstreamModelId: parsed.data.upstreamModelId,
|
|
489
|
+
label: parsed.data.label ?? null,
|
|
490
|
+
});
|
|
491
|
+
const replay = await replayWorkspaceGatewayCustomModelCreate(deps.db, {
|
|
492
|
+
accountId: grant.accountId,
|
|
493
|
+
workspaceId,
|
|
494
|
+
operationId: parsed.data.operationId,
|
|
495
|
+
requestHash,
|
|
496
|
+
});
|
|
497
|
+
if (replay.outcome === "conflict") {
|
|
498
|
+
throw new HTTPException(409, {
|
|
499
|
+
message: "Gateway custom model operation conflicts with current state",
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
if (replay.outcome === "success") {
|
|
503
|
+
return c.json(projectWorkspaceGatewayCustomModel(replay.model), 201);
|
|
504
|
+
}
|
|
505
|
+
const catalog = await deps.resolveCatalogSettings();
|
|
506
|
+
if (configuredGatewayUpstreamModelIds(catalog.settings).includes(parsed.data.upstreamModelId)) {
|
|
507
|
+
throw new HTTPException(422, {
|
|
508
|
+
message: "Gateway model is already included in the deployment catalog",
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
const customProductId = `${WORKSPACE_GATEWAY_MODEL_ID_PREFIX}${parsed.data.upstreamModelId}`;
|
|
512
|
+
const deploymentProductIds = new Set([
|
|
513
|
+
...configuredModelInputIdentities(catalog.settings),
|
|
514
|
+
...configuredGatewayWorkspaceProductModelIds(catalog.settings),
|
|
515
|
+
]);
|
|
516
|
+
if (deploymentProductIds.has(customProductId)) {
|
|
517
|
+
throw new HTTPException(422, {
|
|
518
|
+
message: "Gateway model product id conflicts with the deployment catalog",
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
try {
|
|
522
|
+
const model = await createWorkspaceGatewayCustomModel(deps.db, {
|
|
523
|
+
accountId: grant.accountId,
|
|
524
|
+
workspaceId,
|
|
525
|
+
upstreamModelId: parsed.data.upstreamModelId,
|
|
526
|
+
label: parsed.data.label ?? null,
|
|
527
|
+
operationId: parsed.data.operationId,
|
|
528
|
+
requestHash,
|
|
529
|
+
createdBySubjectId: grant.subjectId,
|
|
530
|
+
});
|
|
531
|
+
if (!model || model.retiredAt) {
|
|
532
|
+
throw new HTTPException(409, {
|
|
533
|
+
message: "Gateway custom model operation conflicts with current state",
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
return c.json(projectWorkspaceGatewayCustomModel(model), 201);
|
|
537
|
+
} catch (error) {
|
|
538
|
+
if (
|
|
539
|
+
error instanceof WorkspaceGatewayCustomModelLimitError ||
|
|
540
|
+
error instanceof WorkspaceGatewayCustomModelHistoryLimitError
|
|
541
|
+
) {
|
|
542
|
+
throw new HTTPException(422, { message: error.message });
|
|
543
|
+
}
|
|
544
|
+
if (nestedPostgresSqlState(error) === "23505") {
|
|
545
|
+
throw new HTTPException(422, {
|
|
546
|
+
message: "Gateway custom model already exists",
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
throw error;
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
app.delete("/v1/workspaces/:workspaceId/gateway-custom-models/:customModelId", async (c) => {
|
|
554
|
+
const workspaceId = c.req.param("workspaceId");
|
|
555
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
556
|
+
const customModelId = c.req.param("customModelId");
|
|
557
|
+
if (
|
|
558
|
+
!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(
|
|
559
|
+
customModelId,
|
|
560
|
+
)
|
|
561
|
+
) {
|
|
562
|
+
throw new HTTPException(422, {
|
|
563
|
+
message: "invalid Gateway custom model id",
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
const parsed = DeleteWorkspaceGatewayCustomModelRequest.safeParse(
|
|
567
|
+
await c.req.json().catch(() => null),
|
|
568
|
+
);
|
|
569
|
+
if (!parsed.success) {
|
|
570
|
+
throw new HTTPException(422, {
|
|
571
|
+
message: "invalid Gateway custom model deletion",
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
const removed = await deleteWorkspaceGatewayCustomModel(deps.db, {
|
|
575
|
+
accountId: grant.accountId,
|
|
576
|
+
workspaceId,
|
|
577
|
+
customModelId,
|
|
578
|
+
expectedVersion: parsed.data.expectedVersion,
|
|
579
|
+
operationId: parsed.data.operationId,
|
|
580
|
+
requestHash: workspaceCustomModelRequestHash({
|
|
581
|
+
action: "delete",
|
|
582
|
+
customModelId,
|
|
583
|
+
expectedVersion: parsed.data.expectedVersion,
|
|
584
|
+
}),
|
|
585
|
+
});
|
|
586
|
+
if (removed.outcome === "not_found") {
|
|
587
|
+
throw new HTTPException(404, {
|
|
588
|
+
message: "Gateway custom model not found",
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
if (removed.outcome === "conflict") {
|
|
592
|
+
throw new HTTPException(409, {
|
|
593
|
+
message: "Gateway custom model changed; reload and retry",
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
return c.body(null, 204);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
app.get("/v1/workspaces/:workspaceId/openrouter-custom-models", async (c) => {
|
|
600
|
+
const workspaceId = c.req.param("workspaceId");
|
|
601
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
602
|
+
const models = await listWorkspaceOpenRouterCustomModels(deps.db, {
|
|
603
|
+
accountId: grant.accountId,
|
|
604
|
+
workspaceId,
|
|
605
|
+
});
|
|
606
|
+
c.header("cache-control", "private, no-store");
|
|
607
|
+
return c.json(
|
|
608
|
+
WorkspaceOpenRouterCustomModelsResponse.parse({
|
|
609
|
+
models: models.map(projectWorkspaceOpenRouterCustomModel),
|
|
610
|
+
}),
|
|
611
|
+
);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
app.post("/v1/workspaces/:workspaceId/openrouter-custom-models", async (c) => {
|
|
615
|
+
const workspaceId = c.req.param("workspaceId");
|
|
616
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
617
|
+
const parsed = CreateWorkspaceOpenRouterCustomModelRequest.safeParse(
|
|
618
|
+
await c.req.json().catch(() => null),
|
|
619
|
+
);
|
|
620
|
+
if (!parsed.success) {
|
|
621
|
+
throw new HTTPException(422, { message: "invalid OpenRouter custom model" });
|
|
622
|
+
}
|
|
623
|
+
const requestHash = workspaceCustomModelRequestHash({
|
|
624
|
+
action: "create",
|
|
625
|
+
upstreamModelId: parsed.data.upstreamModelId,
|
|
626
|
+
label: parsed.data.label ?? null,
|
|
627
|
+
});
|
|
628
|
+
const replay = await replayWorkspaceOpenRouterCustomModelCreate(deps.db, {
|
|
629
|
+
accountId: grant.accountId,
|
|
630
|
+
workspaceId,
|
|
631
|
+
operationId: parsed.data.operationId,
|
|
632
|
+
requestHash,
|
|
633
|
+
});
|
|
634
|
+
if (replay.outcome === "conflict") {
|
|
635
|
+
throw new HTTPException(409, {
|
|
636
|
+
message: "OpenRouter custom model operation conflicts with current state",
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
if (replay.outcome === "success") {
|
|
640
|
+
return c.json(projectWorkspaceOpenRouterCustomModel(replay.model), 201);
|
|
641
|
+
}
|
|
642
|
+
const catalog = await deps.resolveCatalogSettings();
|
|
643
|
+
if (
|
|
644
|
+
configuredOpenRouterUpstreamModelIds(catalog.settings).includes(parsed.data.upstreamModelId)
|
|
645
|
+
) {
|
|
646
|
+
throw new HTTPException(422, {
|
|
647
|
+
message: "OpenRouter model is already included in the deployment catalog",
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
const customProductId = `${WORKSPACE_OPENROUTER_MODEL_ID_PREFIX}${parsed.data.upstreamModelId}`;
|
|
651
|
+
const deploymentProductIds = new Set([
|
|
652
|
+
...configuredModelInputIdentities(catalog.settings),
|
|
653
|
+
...configuredOpenRouterWorkspaceProductModelIds(catalog.settings),
|
|
654
|
+
]);
|
|
655
|
+
if (deploymentProductIds.has(customProductId)) {
|
|
656
|
+
throw new HTTPException(422, {
|
|
657
|
+
message: "OpenRouter model product id conflicts with the deployment catalog",
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
try {
|
|
661
|
+
const model = await createWorkspaceOpenRouterCustomModel(deps.db, {
|
|
662
|
+
accountId: grant.accountId,
|
|
663
|
+
workspaceId,
|
|
664
|
+
upstreamModelId: parsed.data.upstreamModelId,
|
|
665
|
+
label: parsed.data.label ?? null,
|
|
666
|
+
operationId: parsed.data.operationId,
|
|
667
|
+
requestHash,
|
|
668
|
+
createdBySubjectId: grant.subjectId,
|
|
669
|
+
});
|
|
670
|
+
if (!model || model.retiredAt) {
|
|
671
|
+
throw new HTTPException(409, {
|
|
672
|
+
message: "OpenRouter custom model operation conflicts with current state",
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
return c.json(projectWorkspaceOpenRouterCustomModel(model), 201);
|
|
676
|
+
} catch (error) {
|
|
677
|
+
if (
|
|
678
|
+
error instanceof WorkspaceOpenRouterCustomModelLimitError ||
|
|
679
|
+
error instanceof WorkspaceOpenRouterCustomModelHistoryLimitError
|
|
680
|
+
) {
|
|
681
|
+
throw new HTTPException(422, { message: error.message });
|
|
682
|
+
}
|
|
683
|
+
if (nestedPostgresSqlState(error) === "23505") {
|
|
684
|
+
throw new HTTPException(422, {
|
|
685
|
+
message: "OpenRouter custom model already exists",
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
throw error;
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
app.delete("/v1/workspaces/:workspaceId/openrouter-custom-models/:customModelId", async (c) => {
|
|
693
|
+
const workspaceId = c.req.param("workspaceId");
|
|
694
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
695
|
+
const customModelId = c.req.param("customModelId");
|
|
696
|
+
if (
|
|
697
|
+
!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(
|
|
698
|
+
customModelId,
|
|
699
|
+
)
|
|
700
|
+
) {
|
|
701
|
+
throw new HTTPException(422, {
|
|
702
|
+
message: "invalid OpenRouter custom model id",
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
const parsed = DeleteWorkspaceOpenRouterCustomModelRequest.safeParse(
|
|
706
|
+
await c.req.json().catch(() => null),
|
|
707
|
+
);
|
|
708
|
+
if (!parsed.success) {
|
|
709
|
+
throw new HTTPException(422, {
|
|
710
|
+
message: "invalid OpenRouter custom model deletion",
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
const removed = await deleteWorkspaceOpenRouterCustomModel(deps.db, {
|
|
714
|
+
accountId: grant.accountId,
|
|
715
|
+
workspaceId,
|
|
716
|
+
customModelId,
|
|
717
|
+
expectedVersion: parsed.data.expectedVersion,
|
|
718
|
+
operationId: parsed.data.operationId,
|
|
719
|
+
requestHash: workspaceCustomModelRequestHash({
|
|
720
|
+
action: "delete",
|
|
721
|
+
customModelId,
|
|
722
|
+
expectedVersion: parsed.data.expectedVersion,
|
|
723
|
+
}),
|
|
724
|
+
});
|
|
725
|
+
if (removed.outcome === "not_found") {
|
|
726
|
+
throw new HTTPException(404, {
|
|
727
|
+
message: "OpenRouter custom model not found",
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
if (removed.outcome === "conflict") {
|
|
731
|
+
throw new HTTPException(409, {
|
|
732
|
+
message: "OpenRouter custom model changed; reload and retry",
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
return c.body(null, 204);
|
|
736
|
+
});
|
|
737
|
+
|
|
344
738
|
app.get("/v1/workspaces/:workspaceId/realtime-model-catalog", async (c) => {
|
|
345
739
|
const workspaceId = c.req.param("workspaceId");
|
|
346
740
|
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
@@ -417,11 +811,15 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
417
811
|
const workspaceId = c.req.param("workspaceId");
|
|
418
812
|
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
419
813
|
const payload = UpdateWorkspaceModelPolicyRequest.parse(await c.req.json());
|
|
814
|
+
const catalog = await resolveWorkspaceCatalogSettings(deps.db, deps.settings, {
|
|
815
|
+
accountId: grant.accountId,
|
|
816
|
+
workspaceId,
|
|
817
|
+
});
|
|
420
818
|
const policy = await upsertWorkspaceModelPolicy(deps.db, {
|
|
421
819
|
accountId: grant.accountId,
|
|
422
820
|
workspaceId,
|
|
423
821
|
allowedProviders: payload.allowedProviders ?? null,
|
|
424
|
-
allowedModels: canonicalWorkspacePolicyModelIds(
|
|
822
|
+
allowedModels: canonicalWorkspacePolicyModelIds(catalog.settings, payload.allowedModels),
|
|
425
823
|
});
|
|
426
824
|
return c.json(policy);
|
|
427
825
|
});
|
|
@@ -513,65 +911,10 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
513
911
|
app.delete("/v1/workspaces/:workspaceId", async (c) => {
|
|
514
912
|
const workspaceId = c.req.param("workspaceId");
|
|
515
913
|
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
516
|
-
|
|
517
|
-
// session/lease before checking runtime quiescence, then returns the exact
|
|
518
|
-
// external schedules removed by the cascade. A racing cold->warm transition
|
|
519
|
-
// can therefore never erase the only provider/capture ownership receipt.
|
|
520
|
-
const deleteObserver = workspaceDeleteObserver(deps.observability, {
|
|
521
|
-
accountId: grant.accountId,
|
|
522
|
-
workspaceId,
|
|
523
|
-
});
|
|
524
|
-
const deleted = await deleteWorkspaceIfQuiescent(deps.db, {
|
|
914
|
+
await deleteWorkspaceForRequest(deps, {
|
|
525
915
|
accountId: grant.accountId,
|
|
526
916
|
workspaceId,
|
|
527
|
-
...(deleteObserver ? { observer: deleteObserver } : {}),
|
|
528
917
|
});
|
|
529
|
-
if (deleted.status === "not_found") {
|
|
530
|
-
throw new HTTPException(404, { message: "workspace not found" });
|
|
531
|
-
}
|
|
532
|
-
if (deleted.status === "only_workspace") {
|
|
533
|
-
throw new HTTPException(409, {
|
|
534
|
-
message: "cannot delete the account's only workspace",
|
|
535
|
-
});
|
|
536
|
-
}
|
|
537
|
-
if (deleted.status === "active_sessions") {
|
|
538
|
-
throw new HTTPException(409, {
|
|
539
|
-
message: "stop the workspace's running sessions before deleting it",
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
if (deleted.status === "active_video_generations") {
|
|
543
|
-
throw new HTTPException(409, {
|
|
544
|
-
message: "wait for the workspace's active video generations to finish before deleting it",
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
if (deleted.status === "active_background_commands") {
|
|
548
|
-
throw new HTTPException(409, {
|
|
549
|
-
message: "pause or cancel the workspace's background commands before deleting it",
|
|
550
|
-
});
|
|
551
|
-
}
|
|
552
|
-
if (deleted.status === "live_sandboxes") {
|
|
553
|
-
throw new HTTPException(409, {
|
|
554
|
-
message: "wait for the workspace's active sandboxes to finish draining before deleting it",
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
if (deleted.status !== "deleted") {
|
|
558
|
-
throw new Error(`Unhandled workspace deletion outcome: ${deleted.status}`);
|
|
559
|
-
}
|
|
560
|
-
// The cleanup claims were inserted in the same transaction as the cascade.
|
|
561
|
-
// Try them immediately; failures are released to the replica-safe outbox
|
|
562
|
-
// pump, so a process crash or Temporal outage cannot orphan the schedules.
|
|
563
|
-
await processTemporalScheduleCleanupClaims(
|
|
564
|
-
{
|
|
565
|
-
db: deps.db,
|
|
566
|
-
deleteSchedule: async (temporalScheduleId) => {
|
|
567
|
-
await deps.workflowClient.deleteScheduledTaskSchedule({
|
|
568
|
-
temporalScheduleId,
|
|
569
|
-
});
|
|
570
|
-
},
|
|
571
|
-
...(deps.observability ? { observability: deps.observability } : {}),
|
|
572
|
-
},
|
|
573
|
-
deleted.temporalScheduleCleanups,
|
|
574
|
-
);
|
|
575
918
|
return c.body(null, 204);
|
|
576
919
|
});
|
|
577
920
|
|
package/src/sandbox/channel-a.ts
CHANGED
|
@@ -65,9 +65,11 @@ import {
|
|
|
65
65
|
NatsOpStreamTransport,
|
|
66
66
|
SandboxResumeIdentityMismatchError,
|
|
67
67
|
SandboxResumeIdentityUnavailableError,
|
|
68
|
+
RoutingActiveRouteChangedError,
|
|
68
69
|
RoutingWorkspaceRootChangedError,
|
|
69
70
|
SelfhostedWorkspaceRootChangedError,
|
|
70
71
|
ChannelAConflictError,
|
|
72
|
+
ChannelAFileSystemRouteChangedError,
|
|
71
73
|
ChannelANotFoundError,
|
|
72
74
|
ChannelAUnsupportedError,
|
|
73
75
|
ChannelAUnavailableError,
|
|
@@ -612,11 +614,18 @@ async function withChannelAOperation<T>(
|
|
|
612
614
|
codemodeTokenFileFromEnvironment(environment, session.id),
|
|
613
615
|
)
|
|
614
616
|
: credentialSession;
|
|
615
|
-
const
|
|
617
|
+
const fileSystemAuthority = await routingSession.fileSystemAuthority();
|
|
618
|
+
const fileSystemEpoch =
|
|
619
|
+
fileSystemAuthority.backendKind === "selfhosted"
|
|
620
|
+
? fileSystemAuthority.activeEpoch
|
|
621
|
+
: (lease?.leaseEpoch ?? fileSystemAuthority.activeEpoch);
|
|
616
622
|
const service = new SandboxChannelAService({
|
|
617
623
|
session: scopedSession as ChannelASession,
|
|
618
|
-
workspaceRoot:
|
|
619
|
-
leaseEpoch:
|
|
624
|
+
workspaceRoot: fileSystemAuthority.root,
|
|
625
|
+
leaseEpoch: fileSystemEpoch,
|
|
626
|
+
...(fileSystemAuthority.backendKind === "selfhosted"
|
|
627
|
+
? { providerPathMode: "workspace-relative" as const, fileReadScope: "machine" as const }
|
|
628
|
+
: {}),
|
|
620
629
|
emit,
|
|
621
630
|
});
|
|
622
631
|
const result = await fn({
|
|
@@ -1200,8 +1209,10 @@ export function mapChannelAError(error: unknown, waitSignal?: AbortSignal): unkn
|
|
|
1200
1209
|
)
|
|
1201
1210
|
return new HTTPException(409, { message: error.message });
|
|
1202
1211
|
if (
|
|
1212
|
+
error instanceof RoutingActiveRouteChangedError ||
|
|
1203
1213
|
error instanceof RoutingWorkspaceRootChangedError ||
|
|
1204
|
-
error instanceof SelfhostedWorkspaceRootChangedError
|
|
1214
|
+
error instanceof SelfhostedWorkspaceRootChangedError ||
|
|
1215
|
+
error instanceof ChannelAFileSystemRouteChangedError
|
|
1205
1216
|
)
|
|
1206
1217
|
return new ApiHttpError(409, {
|
|
1207
1218
|
code: "conflict",
|
|
@@ -1299,8 +1310,10 @@ export function channelAOperationFailureDiagnostic(
|
|
|
1299
1310
|
if (
|
|
1300
1311
|
error instanceof SandboxResumeIdentityMismatchError ||
|
|
1301
1312
|
error instanceof SandboxResumeIdentityUnavailableError ||
|
|
1313
|
+
error instanceof RoutingActiveRouteChangedError ||
|
|
1302
1314
|
error instanceof RoutingWorkspaceRootChangedError ||
|
|
1303
1315
|
error instanceof SelfhostedWorkspaceRootChangedError ||
|
|
1316
|
+
error instanceof ChannelAFileSystemRouteChangedError ||
|
|
1304
1317
|
(error instanceof HTTPException && error.status === 409)
|
|
1305
1318
|
) {
|
|
1306
1319
|
return {
|
|
@@ -19,7 +19,7 @@ export type WorkDiscoveryObservation = {
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
type WorkDiscoveryObservedRow = {
|
|
22
|
-
relatedWork
|
|
22
|
+
relatedWork?: {
|
|
23
23
|
match: { class: WorkDiscoveryMatchClass } | null;
|
|
24
24
|
possibleOverlap: boolean;
|
|
25
25
|
};
|
|
@@ -35,8 +35,8 @@ export function summarizeWorkDiscoveryRows(
|
|
|
35
35
|
const matchCounts: WorkDiscoveryObservation["matchCounts"] = {};
|
|
36
36
|
let overlapCount = 0;
|
|
37
37
|
for (const row of rows) {
|
|
38
|
-
if (row.relatedWork
|
|
39
|
-
const matchClass = row.relatedWork
|
|
38
|
+
if (row.relatedWork?.possibleOverlap) overlapCount += 1;
|
|
39
|
+
const matchClass = row.relatedWork?.match?.class;
|
|
40
40
|
if (matchClass) matchCounts[matchClass] = (matchCounts[matchClass] ?? 0) + 1;
|
|
41
41
|
}
|
|
42
42
|
return { resultCount: rows.length, overlapCount, matchCounts };
|