@opengeni/api-router 2.6.3-canary.0 → 2.6.4-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.d.ts +2 -1
- package/dist/app.js +3 -1
- package/dist/{chunk-XTLI3CBH.js → chunk-UOI7KAP3.js} +1010 -410
- package/dist/chunk-UOI7KAP3.js.map +1 -0
- package/dist/index.js +14 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/scheduled-task-view.d.ts +3 -3
- package/dist/model-catalog.d.ts +5 -31
- package/package.json +18 -18
- package/src/app.ts +34 -9
- package/src/index.ts +13 -1
- package/src/integrations/slack-interactions.ts +77 -37
- package/src/mcp/server.ts +11 -2
- package/src/model-catalog.ts +45 -337
- package/src/routes/automations.ts +186 -19
- package/src/routes/connections.ts +271 -16
- package/src/routes/github.ts +0 -8
- package/src/routes/pr-review.ts +80 -4
- package/src/routes/scheduled-tasks.ts +40 -13
- package/src/routes/sessions.ts +72 -47
- package/src/routes/supergrok.ts +3 -2
- package/src/routes/workspaces.ts +373 -3
- package/dist/chunk-XTLI3CBH.js.map +0 -1
package/src/routes/sessions.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
GatewayRealtimeConnectRequest,
|
|
13
13
|
ClientSessionEvent,
|
|
14
14
|
CompactSessionContextRequest,
|
|
15
|
+
CreateSessionRequest,
|
|
15
16
|
DeleteSessionQueueItemRequest,
|
|
16
17
|
EditSessionQueueItemRequest,
|
|
17
18
|
EndSessionRealtimeRequest,
|
|
@@ -47,6 +48,7 @@ import {
|
|
|
47
48
|
SessionEventSemanticClass,
|
|
48
49
|
SessionEventType,
|
|
49
50
|
SessionMcpServerId,
|
|
51
|
+
SaveNewSessionDraftRequest,
|
|
50
52
|
compactSessionEventResult,
|
|
51
53
|
sessionEventLatestClassToSemanticClass,
|
|
52
54
|
SaveComposerDraftRequest,
|
|
@@ -231,6 +233,7 @@ import {
|
|
|
231
233
|
requirePermission,
|
|
232
234
|
requireSessionAuthorization,
|
|
233
235
|
requireSessionAuthorizationListScope,
|
|
236
|
+
resolveWorkspaceCatalogSettings,
|
|
234
237
|
withResolvedSessionAuthorization,
|
|
235
238
|
SESSION_AUTHORIZATION_DEFAULT_REAUTHORIZE_MS,
|
|
236
239
|
SessionAuthorizationDeniedError,
|
|
@@ -549,6 +552,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
549
552
|
}
|
|
550
553
|
let session: Session;
|
|
551
554
|
try {
|
|
555
|
+
CreateSessionRequest.parse(payload);
|
|
552
556
|
session = await createSessionForRequest(deps, grant, workspaceId, payload, authorization);
|
|
553
557
|
} catch (error) {
|
|
554
558
|
return sessionCreateErrorResponse(c, error);
|
|
@@ -562,7 +566,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
562
566
|
app.get("/v1/workspaces/:workspaceId/new-session-draft", async (c) => {
|
|
563
567
|
const workspaceId = c.req.param("workspaceId");
|
|
564
568
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
565
|
-
|
|
569
|
+
const catalog = await resolveWorkspaceCatalogSettings(db, settings, {
|
|
570
|
+
accountId: grant.accountId,
|
|
571
|
+
workspaceId,
|
|
572
|
+
});
|
|
573
|
+
return c.json(
|
|
574
|
+
await getActorNewSessionDraft({ settings: catalog.settings, db }, grant, workspaceId),
|
|
575
|
+
);
|
|
566
576
|
});
|
|
567
577
|
|
|
568
578
|
app.put("/v1/workspaces/:workspaceId/new-session-draft", async (c) => {
|
|
@@ -587,9 +597,14 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
587
597
|
);
|
|
588
598
|
}
|
|
589
599
|
try {
|
|
600
|
+
SaveNewSessionDraftRequest.parse(payload);
|
|
601
|
+
const catalog = await resolveWorkspaceCatalogSettings(db, settings, {
|
|
602
|
+
accountId: grant.accountId,
|
|
603
|
+
workspaceId,
|
|
604
|
+
});
|
|
590
605
|
return c.json(
|
|
591
606
|
await saveActorNewSessionDraft(
|
|
592
|
-
{ settings, db, objectStorage },
|
|
607
|
+
{ settings: catalog.settings, db, objectStorage },
|
|
593
608
|
grant,
|
|
594
609
|
workspaceId,
|
|
595
610
|
payload,
|
|
@@ -2898,28 +2913,33 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2898
2913
|
const sessionId = c.req.param("sessionId");
|
|
2899
2914
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
2900
2915
|
const payload = parseSteerSessionAdmission(await c.req.json().catch(() => null));
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
? {
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2916
|
+
let result: Awaited<ReturnType<typeof acceptSessionUserMessage>>;
|
|
2917
|
+
try {
|
|
2918
|
+
result = await acceptSessionUserMessage(deps, grant, workspaceId, sessionId, {
|
|
2919
|
+
text: payload.text,
|
|
2920
|
+
annotations: payload.annotations,
|
|
2921
|
+
modelContext: payload.modelContext ?? null,
|
|
2922
|
+
resources: payload.resources,
|
|
2923
|
+
model: payload.model ?? null,
|
|
2924
|
+
reasoningEffort: payload.reasoningEffort ?? null,
|
|
2925
|
+
latencyMode: payload.latencyMode ?? null,
|
|
2926
|
+
mcpCredentialUpdates: payload.mcpCredentialUpdates ?? [],
|
|
2927
|
+
connectionAuthorities: payload.connectionAuthorities,
|
|
2928
|
+
...(payload.personalResourceAttachment
|
|
2929
|
+
? { personalResourceAttachment: payload.personalResourceAttachment }
|
|
2930
|
+
: {}),
|
|
2931
|
+
authorization,
|
|
2932
|
+
delivery: "steer",
|
|
2933
|
+
origin: "human",
|
|
2934
|
+
...(payload.controlEtag !== undefined ? { controlEtag: payload.controlEtag } : {}),
|
|
2935
|
+
...(payload.expectedDraftRevision !== undefined
|
|
2936
|
+
? { expectedDraftRevision: payload.expectedDraftRevision }
|
|
2937
|
+
: {}),
|
|
2938
|
+
...(payload.clientEventId ? { clientEventId: payload.clientEventId } : {}),
|
|
2939
|
+
});
|
|
2940
|
+
} catch (error) {
|
|
2941
|
+
return commandConflictResponse(c, error);
|
|
2942
|
+
}
|
|
2923
2943
|
return c.json(result, 202);
|
|
2924
2944
|
});
|
|
2925
2945
|
|
|
@@ -2975,29 +2995,34 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2975
2995
|
}
|
|
2976
2996
|
}
|
|
2977
2997
|
if (event.type === "user.message") {
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
2998
|
+
let result: Awaited<ReturnType<typeof acceptSessionUserMessage>>;
|
|
2999
|
+
try {
|
|
3000
|
+
result = await acceptSessionUserMessage(deps, grant, workspaceId, sessionId, {
|
|
3001
|
+
text: event.payload.text,
|
|
3002
|
+
annotations: event.payload.annotations,
|
|
3003
|
+
modelContext: event.payload.modelContext ?? null,
|
|
3004
|
+
resources: event.payload.resources ?? [],
|
|
3005
|
+
model: event.payload.model ?? null,
|
|
3006
|
+
reasoningEffort: event.payload.reasoningEffort ?? null,
|
|
3007
|
+
latencyMode: event.payload.latencyMode ?? null,
|
|
3008
|
+
mcpCredentialUpdates: event.payload.mcpCredentialUpdates ?? [],
|
|
3009
|
+
connectionAuthorities: event.payload.connectionAuthorities,
|
|
3010
|
+
...(event.payload.personalResourceAttachment
|
|
3011
|
+
? { personalResourceAttachment: event.payload.personalResourceAttachment }
|
|
3012
|
+
: {}),
|
|
3013
|
+
authorization,
|
|
3014
|
+
...(event.payload.controlEtag !== undefined
|
|
3015
|
+
? { controlEtag: event.payload.controlEtag }
|
|
3016
|
+
: {}),
|
|
3017
|
+
...(event.payload.expectedDraftRevision !== undefined
|
|
3018
|
+
? { expectedDraftRevision: event.payload.expectedDraftRevision }
|
|
3019
|
+
: {}),
|
|
3020
|
+
...(event.clientEventId ? { clientEventId: event.clientEventId } : {}),
|
|
3021
|
+
});
|
|
3022
|
+
} catch (error) {
|
|
3023
|
+
return commandConflictResponse(c, error);
|
|
3024
|
+
}
|
|
3025
|
+
return c.json(result.accepted, 202);
|
|
3001
3026
|
}
|
|
3002
3027
|
|
|
3003
3028
|
if (event.type === "user.approvalDecision") {
|
package/src/routes/supergrok.ts
CHANGED
|
@@ -552,8 +552,9 @@ export function registerSuperGrokRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
552
552
|
} catch {
|
|
553
553
|
valid = false;
|
|
554
554
|
}
|
|
555
|
-
const
|
|
556
|
-
|
|
555
|
+
const catalogSettings = valid ? (await deps.resolveCatalogSettings()).settings : null;
|
|
556
|
+
const catalog = catalogSettings
|
|
557
|
+
? configuredModels(withXaiSubscriptionCatalogProvider(catalogSettings))
|
|
557
558
|
.filter(
|
|
558
559
|
(model) =>
|
|
559
560
|
model.credentialSource.kind === "connected_subscription" &&
|
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,
|
|
@@ -36,6 +46,14 @@ import {
|
|
|
36
46
|
normalizeWorkspaceMembershipPermissions,
|
|
37
47
|
listWorkspaceControlEvents,
|
|
38
48
|
listSharedWorkspacesForAccount,
|
|
49
|
+
listWorkspaceGatewayCustomModels,
|
|
50
|
+
listWorkspaceOpenRouterCustomModels,
|
|
51
|
+
createWorkspaceGatewayCustomModel,
|
|
52
|
+
createWorkspaceOpenRouterCustomModel,
|
|
53
|
+
deleteWorkspaceGatewayCustomModel,
|
|
54
|
+
deleteWorkspaceOpenRouterCustomModel,
|
|
55
|
+
replayWorkspaceGatewayCustomModelCreate,
|
|
56
|
+
replayWorkspaceOpenRouterCustomModelCreate,
|
|
39
57
|
listWorkspacesForSubject,
|
|
40
58
|
nestedPostgresSqlState,
|
|
41
59
|
removeWorkspaceMember,
|
|
@@ -50,7 +68,12 @@ import {
|
|
|
50
68
|
workspaceControlRequestLockTimeoutMs,
|
|
51
69
|
workspaceXaiSubscriptionActive,
|
|
52
70
|
workspaceVercelAiGatewayConnectionActive,
|
|
71
|
+
workspaceOpenRouterConnectionActive,
|
|
72
|
+
WorkspaceGatewayCustomModelHistoryLimitError,
|
|
73
|
+
WorkspaceOpenRouterCustomModelHistoryLimitError,
|
|
53
74
|
WorkspaceExternalIdentityConflictError,
|
|
75
|
+
WorkspaceGatewayCustomModelLimitError,
|
|
76
|
+
WorkspaceOpenRouterCustomModelLimitError,
|
|
54
77
|
WorkspaceLimitExceededError,
|
|
55
78
|
} from "@opengeni/db";
|
|
56
79
|
import { boundWorkspaceControlHttpPage } from "@opengeni/events";
|
|
@@ -63,6 +86,7 @@ import {
|
|
|
63
86
|
requireAccessContext,
|
|
64
87
|
requireAccessGrant,
|
|
65
88
|
requireFreshAccessGrant,
|
|
89
|
+
resolveWorkspaceCatalogSettings,
|
|
66
90
|
} from "@opengeni/core";
|
|
67
91
|
import { requireLimit } from "@opengeni/core";
|
|
68
92
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
@@ -83,6 +107,13 @@ import {
|
|
|
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,323 @@ 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,
|
|
324
401
|
] = await Promise.all([
|
|
402
|
+
deps.resolveCatalogSettings(),
|
|
325
403
|
getWorkspaceModelPolicy(deps.db, workspaceId),
|
|
326
404
|
workspaceCodexSubscriptionActive(deps.db, deps.settings, workspaceId),
|
|
327
405
|
workspaceXaiSubscriptionActive(deps.db, deps.settings, workspaceId, grant.subjectId),
|
|
328
406
|
workspaceVercelAiGatewayConnectionActive(deps.db, workspaceId),
|
|
407
|
+
listWorkspaceGatewayCustomModels(deps.db, {
|
|
408
|
+
accountId: grant.accountId,
|
|
409
|
+
workspaceId,
|
|
410
|
+
}),
|
|
411
|
+
workspaceOpenRouterConnectionActive(deps.db, workspaceId),
|
|
412
|
+
listWorkspaceOpenRouterCustomModels(deps.db, {
|
|
413
|
+
accountId: grant.accountId,
|
|
414
|
+
workspaceId,
|
|
415
|
+
}),
|
|
329
416
|
]);
|
|
330
417
|
c.header("cache-control", "private, no-store");
|
|
331
418
|
return c.json(
|
|
332
419
|
WorkspaceModelCatalogResponse.parse(
|
|
333
420
|
buildWorkspaceModelCatalog({
|
|
334
|
-
settings:
|
|
421
|
+
settings: resolvedCatalog.settings,
|
|
335
422
|
policy,
|
|
336
423
|
codexSubscriptionActive,
|
|
337
424
|
xaiSubscriptionActive,
|
|
338
425
|
workspaceGatewayConnectionActive,
|
|
426
|
+
workspaceGatewayCustomModels,
|
|
427
|
+
workspaceOpenRouterConnectionActive: openRouterConnectionActive,
|
|
428
|
+
workspaceOpenRouterCustomModels,
|
|
339
429
|
}),
|
|
340
430
|
),
|
|
341
431
|
);
|
|
342
432
|
});
|
|
343
433
|
|
|
434
|
+
app.get("/v1/workspaces/:workspaceId/gateway-custom-models", async (c) => {
|
|
435
|
+
const workspaceId = c.req.param("workspaceId");
|
|
436
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
437
|
+
const models = await listWorkspaceGatewayCustomModels(deps.db, {
|
|
438
|
+
accountId: grant.accountId,
|
|
439
|
+
workspaceId,
|
|
440
|
+
});
|
|
441
|
+
c.header("cache-control", "private, no-store");
|
|
442
|
+
return c.json(
|
|
443
|
+
WorkspaceGatewayCustomModelsResponse.parse({
|
|
444
|
+
models: models.map(projectWorkspaceGatewayCustomModel),
|
|
445
|
+
}),
|
|
446
|
+
);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
app.post("/v1/workspaces/:workspaceId/gateway-custom-models", async (c) => {
|
|
450
|
+
const workspaceId = c.req.param("workspaceId");
|
|
451
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
452
|
+
const parsed = CreateWorkspaceGatewayCustomModelRequest.safeParse(
|
|
453
|
+
await c.req.json().catch(() => null),
|
|
454
|
+
);
|
|
455
|
+
if (!parsed.success) {
|
|
456
|
+
throw new HTTPException(422, { message: "invalid Gateway custom model" });
|
|
457
|
+
}
|
|
458
|
+
const requestHash = workspaceCustomModelRequestHash({
|
|
459
|
+
action: "create",
|
|
460
|
+
upstreamModelId: parsed.data.upstreamModelId,
|
|
461
|
+
label: parsed.data.label ?? null,
|
|
462
|
+
});
|
|
463
|
+
const replay = await replayWorkspaceGatewayCustomModelCreate(deps.db, {
|
|
464
|
+
accountId: grant.accountId,
|
|
465
|
+
workspaceId,
|
|
466
|
+
operationId: parsed.data.operationId,
|
|
467
|
+
requestHash,
|
|
468
|
+
});
|
|
469
|
+
if (replay.outcome === "conflict") {
|
|
470
|
+
throw new HTTPException(409, {
|
|
471
|
+
message: "Gateway custom model operation conflicts with current state",
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
if (replay.outcome === "success") {
|
|
475
|
+
return c.json(projectWorkspaceGatewayCustomModel(replay.model), 201);
|
|
476
|
+
}
|
|
477
|
+
const catalog = await deps.resolveCatalogSettings();
|
|
478
|
+
if (configuredGatewayUpstreamModelIds(catalog.settings).includes(parsed.data.upstreamModelId)) {
|
|
479
|
+
throw new HTTPException(422, {
|
|
480
|
+
message: "Gateway model is already included in the deployment catalog",
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
const customProductId = `${WORKSPACE_GATEWAY_MODEL_ID_PREFIX}${parsed.data.upstreamModelId}`;
|
|
484
|
+
const deploymentProductIds = new Set([
|
|
485
|
+
...configuredModelInputIdentities(catalog.settings),
|
|
486
|
+
...configuredGatewayWorkspaceProductModelIds(catalog.settings),
|
|
487
|
+
]);
|
|
488
|
+
if (deploymentProductIds.has(customProductId)) {
|
|
489
|
+
throw new HTTPException(422, {
|
|
490
|
+
message: "Gateway model product id conflicts with the deployment catalog",
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
try {
|
|
494
|
+
const model = await createWorkspaceGatewayCustomModel(deps.db, {
|
|
495
|
+
accountId: grant.accountId,
|
|
496
|
+
workspaceId,
|
|
497
|
+
upstreamModelId: parsed.data.upstreamModelId,
|
|
498
|
+
label: parsed.data.label ?? null,
|
|
499
|
+
operationId: parsed.data.operationId,
|
|
500
|
+
requestHash,
|
|
501
|
+
createdBySubjectId: grant.subjectId,
|
|
502
|
+
});
|
|
503
|
+
if (!model || model.retiredAt) {
|
|
504
|
+
throw new HTTPException(409, {
|
|
505
|
+
message: "Gateway custom model operation conflicts with current state",
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
return c.json(projectWorkspaceGatewayCustomModel(model), 201);
|
|
509
|
+
} catch (error) {
|
|
510
|
+
if (
|
|
511
|
+
error instanceof WorkspaceGatewayCustomModelLimitError ||
|
|
512
|
+
error instanceof WorkspaceGatewayCustomModelHistoryLimitError
|
|
513
|
+
) {
|
|
514
|
+
throw new HTTPException(422, { message: error.message });
|
|
515
|
+
}
|
|
516
|
+
if (nestedPostgresSqlState(error) === "23505") {
|
|
517
|
+
throw new HTTPException(422, {
|
|
518
|
+
message: "Gateway custom model already exists",
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
throw error;
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
app.delete("/v1/workspaces/:workspaceId/gateway-custom-models/:customModelId", async (c) => {
|
|
526
|
+
const workspaceId = c.req.param("workspaceId");
|
|
527
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
528
|
+
const customModelId = c.req.param("customModelId");
|
|
529
|
+
if (
|
|
530
|
+
!/^[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(
|
|
531
|
+
customModelId,
|
|
532
|
+
)
|
|
533
|
+
) {
|
|
534
|
+
throw new HTTPException(422, {
|
|
535
|
+
message: "invalid Gateway custom model id",
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
const parsed = DeleteWorkspaceGatewayCustomModelRequest.safeParse(
|
|
539
|
+
await c.req.json().catch(() => null),
|
|
540
|
+
);
|
|
541
|
+
if (!parsed.success) {
|
|
542
|
+
throw new HTTPException(422, {
|
|
543
|
+
message: "invalid Gateway custom model deletion",
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
const removed = await deleteWorkspaceGatewayCustomModel(deps.db, {
|
|
547
|
+
accountId: grant.accountId,
|
|
548
|
+
workspaceId,
|
|
549
|
+
customModelId,
|
|
550
|
+
expectedVersion: parsed.data.expectedVersion,
|
|
551
|
+
operationId: parsed.data.operationId,
|
|
552
|
+
requestHash: workspaceCustomModelRequestHash({
|
|
553
|
+
action: "delete",
|
|
554
|
+
customModelId,
|
|
555
|
+
expectedVersion: parsed.data.expectedVersion,
|
|
556
|
+
}),
|
|
557
|
+
});
|
|
558
|
+
if (removed.outcome === "not_found") {
|
|
559
|
+
throw new HTTPException(404, {
|
|
560
|
+
message: "Gateway custom model not found",
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
if (removed.outcome === "conflict") {
|
|
564
|
+
throw new HTTPException(409, {
|
|
565
|
+
message: "Gateway custom model changed; reload and retry",
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
return c.body(null, 204);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
app.get("/v1/workspaces/:workspaceId/openrouter-custom-models", async (c) => {
|
|
572
|
+
const workspaceId = c.req.param("workspaceId");
|
|
573
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
574
|
+
const models = await listWorkspaceOpenRouterCustomModels(deps.db, {
|
|
575
|
+
accountId: grant.accountId,
|
|
576
|
+
workspaceId,
|
|
577
|
+
});
|
|
578
|
+
c.header("cache-control", "private, no-store");
|
|
579
|
+
return c.json(
|
|
580
|
+
WorkspaceOpenRouterCustomModelsResponse.parse({
|
|
581
|
+
models: models.map(projectWorkspaceOpenRouterCustomModel),
|
|
582
|
+
}),
|
|
583
|
+
);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
app.post("/v1/workspaces/:workspaceId/openrouter-custom-models", async (c) => {
|
|
587
|
+
const workspaceId = c.req.param("workspaceId");
|
|
588
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
589
|
+
const parsed = CreateWorkspaceOpenRouterCustomModelRequest.safeParse(
|
|
590
|
+
await c.req.json().catch(() => null),
|
|
591
|
+
);
|
|
592
|
+
if (!parsed.success) {
|
|
593
|
+
throw new HTTPException(422, { message: "invalid OpenRouter custom model" });
|
|
594
|
+
}
|
|
595
|
+
const requestHash = workspaceCustomModelRequestHash({
|
|
596
|
+
action: "create",
|
|
597
|
+
upstreamModelId: parsed.data.upstreamModelId,
|
|
598
|
+
label: parsed.data.label ?? null,
|
|
599
|
+
});
|
|
600
|
+
const replay = await replayWorkspaceOpenRouterCustomModelCreate(deps.db, {
|
|
601
|
+
accountId: grant.accountId,
|
|
602
|
+
workspaceId,
|
|
603
|
+
operationId: parsed.data.operationId,
|
|
604
|
+
requestHash,
|
|
605
|
+
});
|
|
606
|
+
if (replay.outcome === "conflict") {
|
|
607
|
+
throw new HTTPException(409, {
|
|
608
|
+
message: "OpenRouter custom model operation conflicts with current state",
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
if (replay.outcome === "success") {
|
|
612
|
+
return c.json(projectWorkspaceOpenRouterCustomModel(replay.model), 201);
|
|
613
|
+
}
|
|
614
|
+
const catalog = await deps.resolveCatalogSettings();
|
|
615
|
+
if (
|
|
616
|
+
configuredOpenRouterUpstreamModelIds(catalog.settings).includes(parsed.data.upstreamModelId)
|
|
617
|
+
) {
|
|
618
|
+
throw new HTTPException(422, {
|
|
619
|
+
message: "OpenRouter model is already included in the deployment catalog",
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
const customProductId = `${WORKSPACE_OPENROUTER_MODEL_ID_PREFIX}${parsed.data.upstreamModelId}`;
|
|
623
|
+
const deploymentProductIds = new Set([
|
|
624
|
+
...configuredModelInputIdentities(catalog.settings),
|
|
625
|
+
...configuredOpenRouterWorkspaceProductModelIds(catalog.settings),
|
|
626
|
+
]);
|
|
627
|
+
if (deploymentProductIds.has(customProductId)) {
|
|
628
|
+
throw new HTTPException(422, {
|
|
629
|
+
message: "OpenRouter model product id conflicts with the deployment catalog",
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
try {
|
|
633
|
+
const model = await createWorkspaceOpenRouterCustomModel(deps.db, {
|
|
634
|
+
accountId: grant.accountId,
|
|
635
|
+
workspaceId,
|
|
636
|
+
upstreamModelId: parsed.data.upstreamModelId,
|
|
637
|
+
label: parsed.data.label ?? null,
|
|
638
|
+
operationId: parsed.data.operationId,
|
|
639
|
+
requestHash,
|
|
640
|
+
createdBySubjectId: grant.subjectId,
|
|
641
|
+
});
|
|
642
|
+
if (!model || model.retiredAt) {
|
|
643
|
+
throw new HTTPException(409, {
|
|
644
|
+
message: "OpenRouter custom model operation conflicts with current state",
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
return c.json(projectWorkspaceOpenRouterCustomModel(model), 201);
|
|
648
|
+
} catch (error) {
|
|
649
|
+
if (
|
|
650
|
+
error instanceof WorkspaceOpenRouterCustomModelLimitError ||
|
|
651
|
+
error instanceof WorkspaceOpenRouterCustomModelHistoryLimitError
|
|
652
|
+
) {
|
|
653
|
+
throw new HTTPException(422, { message: error.message });
|
|
654
|
+
}
|
|
655
|
+
if (nestedPostgresSqlState(error) === "23505") {
|
|
656
|
+
throw new HTTPException(422, {
|
|
657
|
+
message: "OpenRouter custom model already exists",
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
throw error;
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
app.delete("/v1/workspaces/:workspaceId/openrouter-custom-models/:customModelId", async (c) => {
|
|
665
|
+
const workspaceId = c.req.param("workspaceId");
|
|
666
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
667
|
+
const customModelId = c.req.param("customModelId");
|
|
668
|
+
if (
|
|
669
|
+
!/^[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(
|
|
670
|
+
customModelId,
|
|
671
|
+
)
|
|
672
|
+
) {
|
|
673
|
+
throw new HTTPException(422, {
|
|
674
|
+
message: "invalid OpenRouter custom model id",
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
const parsed = DeleteWorkspaceOpenRouterCustomModelRequest.safeParse(
|
|
678
|
+
await c.req.json().catch(() => null),
|
|
679
|
+
);
|
|
680
|
+
if (!parsed.success) {
|
|
681
|
+
throw new HTTPException(422, {
|
|
682
|
+
message: "invalid OpenRouter custom model deletion",
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
const removed = await deleteWorkspaceOpenRouterCustomModel(deps.db, {
|
|
686
|
+
accountId: grant.accountId,
|
|
687
|
+
workspaceId,
|
|
688
|
+
customModelId,
|
|
689
|
+
expectedVersion: parsed.data.expectedVersion,
|
|
690
|
+
operationId: parsed.data.operationId,
|
|
691
|
+
requestHash: workspaceCustomModelRequestHash({
|
|
692
|
+
action: "delete",
|
|
693
|
+
customModelId,
|
|
694
|
+
expectedVersion: parsed.data.expectedVersion,
|
|
695
|
+
}),
|
|
696
|
+
});
|
|
697
|
+
if (removed.outcome === "not_found") {
|
|
698
|
+
throw new HTTPException(404, {
|
|
699
|
+
message: "OpenRouter custom model not found",
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
if (removed.outcome === "conflict") {
|
|
703
|
+
throw new HTTPException(409, {
|
|
704
|
+
message: "OpenRouter custom model changed; reload and retry",
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
return c.body(null, 204);
|
|
708
|
+
});
|
|
709
|
+
|
|
344
710
|
app.get("/v1/workspaces/:workspaceId/realtime-model-catalog", async (c) => {
|
|
345
711
|
const workspaceId = c.req.param("workspaceId");
|
|
346
712
|
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
@@ -417,11 +783,15 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
417
783
|
const workspaceId = c.req.param("workspaceId");
|
|
418
784
|
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
419
785
|
const payload = UpdateWorkspaceModelPolicyRequest.parse(await c.req.json());
|
|
786
|
+
const catalog = await resolveWorkspaceCatalogSettings(deps.db, deps.settings, {
|
|
787
|
+
accountId: grant.accountId,
|
|
788
|
+
workspaceId,
|
|
789
|
+
});
|
|
420
790
|
const policy = await upsertWorkspaceModelPolicy(deps.db, {
|
|
421
791
|
accountId: grant.accountId,
|
|
422
792
|
workspaceId,
|
|
423
793
|
allowedProviders: payload.allowedProviders ?? null,
|
|
424
|
-
allowedModels: canonicalWorkspacePolicyModelIds(
|
|
794
|
+
allowedModels: canonicalWorkspacePolicyModelIds(catalog.settings, payload.allowedModels),
|
|
425
795
|
});
|
|
426
796
|
return c.json(policy);
|
|
427
797
|
});
|