@axiom-lattice/gateway 4.5.0 → 4.6.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/embed/{embed-page-AgentDetailSideApp-I4ZT3MQQ.js → embed-page-AgentDetailSideApp-7NYLPLAX.js} +1 -1
- package/dist/embed/{embed-page-AgentGovernanceWorkbench-HVFB6EPY.js → embed-page-AgentGovernanceWorkbench-LNGNSI7X.js} +2 -2
- package/dist/embed/{embed-page-MessageGraph-CZQROEO7.js → embed-page-MessageGraph-E6DZDT65.js} +1 -1
- package/dist/embed/{embed-page-ReactChart-WMWBSL54.js → embed-page-ReactChart-3VE7F5VJ.js} +1 -1
- package/dist/embed/{embed-page-SkillFlow-5JLJ5R57.js → embed-page-SkillFlow-XMMHEHQL.js} +1 -1
- package/dist/embed/{embed-page-TasksSideApp-5BYTO4KT.js → embed-page-TasksSideApp-QM5TZ7EG.js} +1 -1
- package/dist/embed/{embed-page-TeamWorkspace-5KN7ZZWQ.js → embed-page-TeamWorkspace-E7RFY3ZY.js} +1 -1
- package/dist/embed/{embed-page-TopologyRuntimeView-S6XAQHDF.js → embed-page-TopologyRuntimeView-DLPDUB7Z.js} +1 -1
- package/dist/embed/{embed-page-WorkflowAutomationView-E6G446HW.js → embed-page-WorkflowAutomationView-FKSVOHHB.js} +1 -1
- package/dist/embed/embed-page-chunk-EXCJ42IB.js +2 -0
- package/dist/embed/{embed-page-chunk-MUE3GWJV.js → embed-page-chunk-H6TK3FAR.js} +1 -1
- package/dist/embed/{embed-page-chunk-QU52HUXW.js → embed-page-chunk-HEMNMC4X.js} +4 -4
- package/dist/embed/{embed-page-chunk-2JOGNXZT.js → embed-page-chunk-I7PWCKAG.js} +9 -9
- package/dist/embed/{embed-page-chunk-KDWA6VOG.js → embed-page-chunk-KN7D66KL.js} +1 -1
- package/dist/embed/{embed-page-chunk-7GAD6Q52.js → embed-page-chunk-U276WBDI.js} +2078 -2078
- package/dist/embed/{embed-page-chunk-YQ3BA27D.js → embed-page-chunk-WWTU55RX.js} +1 -1
- package/dist/embed/embed-page.js +1 -1
- package/dist/index.js +1830 -644
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1335 -147
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -7
- package/dist/embed/embed-page-chunk-DSQ4DIQY.js +0 -2
package/dist/index.mjs
CHANGED
|
@@ -42,7 +42,7 @@ import staticPlugin from "@fastify/static";
|
|
|
42
42
|
import path4 from "path";
|
|
43
43
|
|
|
44
44
|
// src/routes/index.ts
|
|
45
|
-
import { randomUUID as
|
|
45
|
+
import { randomUUID as randomUUID24 } from "crypto";
|
|
46
46
|
import {
|
|
47
47
|
descriptorDataValue as descriptorDataValue3,
|
|
48
48
|
snapshotExactRecord as snapshotExactRecord14,
|
|
@@ -1911,9 +1911,9 @@ async function getConfig(request, reply) {
|
|
|
1911
1911
|
import { registerModelLattice, modelLatticeManager } from "@axiom-lattice/core";
|
|
1912
1912
|
async function getModels(request, reply) {
|
|
1913
1913
|
try {
|
|
1914
|
-
const allLattices = modelLatticeManager.
|
|
1914
|
+
const allLattices = modelLatticeManager.getAllLatticesByTenant("default");
|
|
1915
1915
|
const models = allLattices.map((lattice) => {
|
|
1916
|
-
const config = lattice.client.
|
|
1916
|
+
const config = lattice.client.getConfig();
|
|
1917
1917
|
const displayName = config.displayName || lattice.key.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
1918
1918
|
return {
|
|
1919
1919
|
key: lattice.key,
|
|
@@ -1921,7 +1921,13 @@ async function getModels(request, reply) {
|
|
|
1921
1921
|
provider: config.provider || "openai",
|
|
1922
1922
|
displayName,
|
|
1923
1923
|
streaming: config.streaming || false,
|
|
1924
|
-
|
|
1924
|
+
// The key itself is NOT returned. `hasApiKey` is what a client needs to
|
|
1925
|
+
// render "configured — leave blank to keep"; the value was previously
|
|
1926
|
+
// sent in cleartext to every caller of this endpoint, which put provider
|
|
1927
|
+
// credentials in any browser that loaded the model settings. Clients
|
|
1928
|
+
// that edit a model therefore leave the key field empty, which
|
|
1929
|
+
// `updateModels` reads as "keep the existing one".
|
|
1930
|
+
hasApiKey: Boolean(config.apiKey),
|
|
1925
1931
|
baseURL: config.baseURL || "",
|
|
1926
1932
|
maxTokens: config.maxTokens,
|
|
1927
1933
|
temperature: config.temperature,
|
|
@@ -1963,6 +1969,8 @@ async function updateModels(request, reply) {
|
|
|
1963
1969
|
continue;
|
|
1964
1970
|
}
|
|
1965
1971
|
try {
|
|
1972
|
+
const existingConfig = modelLatticeManager.hasLattice(modelConfig.key) ? modelLatticeManager.getModelLattice(modelConfig.key).client.getConfig() : void 0;
|
|
1973
|
+
const effectiveApiKey = modelConfig.clearApiKey ? void 0 : modelConfig.apiKey || existingConfig?.apiKey;
|
|
1966
1974
|
if (modelLatticeManager.hasLattice(modelConfig.key)) {
|
|
1967
1975
|
modelLatticeManager.removeLattice(modelConfig.key);
|
|
1968
1976
|
}
|
|
@@ -1971,7 +1979,7 @@ async function updateModels(request, reply) {
|
|
|
1971
1979
|
model: modelConfig.model,
|
|
1972
1980
|
displayName: modelConfig.displayName,
|
|
1973
1981
|
streaming: modelConfig.streaming ?? false,
|
|
1974
|
-
apiKey:
|
|
1982
|
+
apiKey: effectiveApiKey,
|
|
1975
1983
|
baseURL: modelConfig.baseURL,
|
|
1976
1984
|
maxTokens: modelConfig.maxTokens,
|
|
1977
1985
|
temperature: modelConfig.temperature,
|
|
@@ -15659,13 +15667,1136 @@ function registerCapabilityBundleRoutes(app2, controller) {
|
|
|
15659
15667
|
app2.post("/api/projects/:projectId/capability-preview", controller.postProjectPreview.bind(controller));
|
|
15660
15668
|
}
|
|
15661
15669
|
|
|
15662
|
-
// src/
|
|
15670
|
+
// src/controllers/model-providers.ts
|
|
15671
|
+
import { ZodError as ZodError3 } from "zod";
|
|
15672
|
+
|
|
15673
|
+
// src/services/model-provider/ModelProviderService.ts
|
|
15663
15674
|
import { randomUUID as randomUUID15 } from "crypto";
|
|
15675
|
+
import { toModelKey } from "@axiom-lattice/protocols";
|
|
15676
|
+
import {
|
|
15677
|
+
assertSafeUpstreamUrl,
|
|
15678
|
+
UnsafeUpstreamUrlError
|
|
15679
|
+
} from "@axiom-lattice/core";
|
|
15680
|
+
|
|
15681
|
+
// src/services/model-provider/errors.ts
|
|
15682
|
+
var ModelProviderServiceError = class extends Error {
|
|
15683
|
+
/** Creates a model provider service error. */
|
|
15684
|
+
constructor(code, statusCode, message, retryable = false) {
|
|
15685
|
+
super(message);
|
|
15686
|
+
this.code = code;
|
|
15687
|
+
this.statusCode = statusCode;
|
|
15688
|
+
this.retryable = retryable;
|
|
15689
|
+
this.name = "ModelProviderServiceError";
|
|
15690
|
+
}
|
|
15691
|
+
};
|
|
15692
|
+
|
|
15693
|
+
// src/services/model-provider/discovery.ts
|
|
15694
|
+
var MAX_DISCOVERED_MODELS = 1e3;
|
|
15695
|
+
var MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
15696
|
+
var DEFAULT_DISCOVERY_TIMEOUT_MS = 8e3;
|
|
15697
|
+
function resolveDiscoveryTimeoutMs(raw = process.env.LATTICE_MODEL_DISCOVERY_TIMEOUT_MS) {
|
|
15698
|
+
if (!raw) {
|
|
15699
|
+
return DEFAULT_DISCOVERY_TIMEOUT_MS;
|
|
15700
|
+
}
|
|
15701
|
+
const parsed = Number.parseInt(raw, 10);
|
|
15702
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
15703
|
+
return DEFAULT_DISCOVERY_TIMEOUT_MS;
|
|
15704
|
+
}
|
|
15705
|
+
return parsed;
|
|
15706
|
+
}
|
|
15707
|
+
function buildModelsEndpoint(baseURL) {
|
|
15708
|
+
const url = new URL(baseURL);
|
|
15709
|
+
const trimmed = url.pathname.replace(/\/+$/, "");
|
|
15710
|
+
url.pathname = trimmed.endsWith("/v1") ? `${trimmed}/models` : `${trimmed}/v1/models`;
|
|
15711
|
+
url.search = "";
|
|
15712
|
+
url.hash = "";
|
|
15713
|
+
return url.toString();
|
|
15714
|
+
}
|
|
15715
|
+
function parseModelsPayload(payload) {
|
|
15716
|
+
const entries = Array.isArray(payload) ? payload : typeof payload === "object" && payload !== null ? payload.data : void 0;
|
|
15717
|
+
if (!Array.isArray(entries)) {
|
|
15718
|
+
throw new ModelProviderServiceError(
|
|
15719
|
+
"UPSTREAM_INVALID_RESPONSE",
|
|
15720
|
+
502,
|
|
15721
|
+
"Upstream did not return a model list"
|
|
15722
|
+
);
|
|
15723
|
+
}
|
|
15724
|
+
const models = [];
|
|
15725
|
+
for (const entry of entries.slice(0, MAX_DISCOVERED_MODELS)) {
|
|
15726
|
+
if (typeof entry !== "object" || entry === null) {
|
|
15727
|
+
continue;
|
|
15728
|
+
}
|
|
15729
|
+
const record = entry;
|
|
15730
|
+
const id3 = typeof record.id === "string" ? record.id.trim() : "";
|
|
15731
|
+
if (id3.length === 0) {
|
|
15732
|
+
continue;
|
|
15733
|
+
}
|
|
15734
|
+
models.push({
|
|
15735
|
+
id: id3,
|
|
15736
|
+
displayName: typeof record.name === "string" ? record.name : void 0,
|
|
15737
|
+
ownedBy: typeof record.owned_by === "string" ? record.owned_by : void 0,
|
|
15738
|
+
createdAt: parseUpstreamCreated(record.created),
|
|
15739
|
+
raw: record
|
|
15740
|
+
});
|
|
15741
|
+
}
|
|
15742
|
+
return models;
|
|
15743
|
+
}
|
|
15744
|
+
function parseUpstreamCreated(created) {
|
|
15745
|
+
if (typeof created !== "number" || !Number.isFinite(created)) {
|
|
15746
|
+
return void 0;
|
|
15747
|
+
}
|
|
15748
|
+
const date = new Date(created * 1e3);
|
|
15749
|
+
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
15750
|
+
}
|
|
15751
|
+
var openAiCompatibleAdapter = {
|
|
15752
|
+
protocol: "openai-compatible",
|
|
15753
|
+
async discover({ baseURL, apiKey, signal }) {
|
|
15754
|
+
const endpoint = buildModelsEndpoint(baseURL);
|
|
15755
|
+
const headers = { Accept: "application/json" };
|
|
15756
|
+
if (apiKey) {
|
|
15757
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
15758
|
+
}
|
|
15759
|
+
let response;
|
|
15760
|
+
try {
|
|
15761
|
+
response = await fetch(endpoint, {
|
|
15762
|
+
method: "GET",
|
|
15763
|
+
headers,
|
|
15764
|
+
// Never follow redirects: a redirected request could reach an
|
|
15765
|
+
// address that bypassed the SSRF checks.
|
|
15766
|
+
redirect: "manual",
|
|
15767
|
+
signal
|
|
15768
|
+
});
|
|
15769
|
+
} catch (error) {
|
|
15770
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
15771
|
+
throw new ModelProviderServiceError(
|
|
15772
|
+
"UPSTREAM_TIMEOUT",
|
|
15773
|
+
504,
|
|
15774
|
+
"Upstream did not respond in time",
|
|
15775
|
+
true
|
|
15776
|
+
);
|
|
15777
|
+
}
|
|
15778
|
+
throw new ModelProviderServiceError(
|
|
15779
|
+
"UPSTREAM_UNREACHABLE",
|
|
15780
|
+
502,
|
|
15781
|
+
"Upstream could not be reached",
|
|
15782
|
+
true
|
|
15783
|
+
);
|
|
15784
|
+
}
|
|
15785
|
+
if (response.status >= 300 && response.status < 400) {
|
|
15786
|
+
throw new ModelProviderServiceError(
|
|
15787
|
+
"UPSTREAM_REDIRECT_BLOCKED",
|
|
15788
|
+
502,
|
|
15789
|
+
"Upstream returned a redirect; configure the final URL directly"
|
|
15790
|
+
);
|
|
15791
|
+
}
|
|
15792
|
+
if (response.status === 401 || response.status === 403) {
|
|
15793
|
+
throw new ModelProviderServiceError(
|
|
15794
|
+
"UPSTREAM_AUTH_FAILED",
|
|
15795
|
+
502,
|
|
15796
|
+
"Upstream rejected the configured credentials"
|
|
15797
|
+
);
|
|
15798
|
+
}
|
|
15799
|
+
if (response.status === 429) {
|
|
15800
|
+
throw new ModelProviderServiceError(
|
|
15801
|
+
"UPSTREAM_RATE_LIMITED",
|
|
15802
|
+
502,
|
|
15803
|
+
"Upstream rate limited the request",
|
|
15804
|
+
true
|
|
15805
|
+
);
|
|
15806
|
+
}
|
|
15807
|
+
if (!response.ok) {
|
|
15808
|
+
throw new ModelProviderServiceError(
|
|
15809
|
+
"UPSTREAM_UNREACHABLE",
|
|
15810
|
+
502,
|
|
15811
|
+
"Upstream returned an error response",
|
|
15812
|
+
response.status >= 500
|
|
15813
|
+
);
|
|
15814
|
+
}
|
|
15815
|
+
const body = await readBodyWithLimit(response, MAX_RESPONSE_BYTES);
|
|
15816
|
+
let payload;
|
|
15817
|
+
try {
|
|
15818
|
+
payload = JSON.parse(body);
|
|
15819
|
+
} catch {
|
|
15820
|
+
throw new ModelProviderServiceError(
|
|
15821
|
+
"UPSTREAM_INVALID_RESPONSE",
|
|
15822
|
+
502,
|
|
15823
|
+
"Upstream response was not valid JSON"
|
|
15824
|
+
);
|
|
15825
|
+
}
|
|
15826
|
+
return parseModelsPayload(payload);
|
|
15827
|
+
}
|
|
15828
|
+
};
|
|
15829
|
+
async function readBodyWithLimit(response, maxBytes) {
|
|
15830
|
+
if (!response.body) {
|
|
15831
|
+
return response.text();
|
|
15832
|
+
}
|
|
15833
|
+
const reader = response.body.getReader();
|
|
15834
|
+
const decoder = new TextDecoder();
|
|
15835
|
+
let received = 0;
|
|
15836
|
+
let text = "";
|
|
15837
|
+
try {
|
|
15838
|
+
for (; ; ) {
|
|
15839
|
+
const { done, value } = await reader.read();
|
|
15840
|
+
if (done) {
|
|
15841
|
+
break;
|
|
15842
|
+
}
|
|
15843
|
+
if (!value) {
|
|
15844
|
+
continue;
|
|
15845
|
+
}
|
|
15846
|
+
received += value.byteLength;
|
|
15847
|
+
if (received > maxBytes) {
|
|
15848
|
+
await reader.cancel();
|
|
15849
|
+
throw new ModelProviderServiceError(
|
|
15850
|
+
"UPSTREAM_INVALID_RESPONSE",
|
|
15851
|
+
502,
|
|
15852
|
+
"Upstream response was too large"
|
|
15853
|
+
);
|
|
15854
|
+
}
|
|
15855
|
+
text += decoder.decode(value, { stream: true });
|
|
15856
|
+
}
|
|
15857
|
+
} finally {
|
|
15858
|
+
reader.releaseLock();
|
|
15859
|
+
}
|
|
15860
|
+
return text + decoder.decode();
|
|
15861
|
+
}
|
|
15862
|
+
var adapters = /* @__PURE__ */ new Map([
|
|
15863
|
+
[openAiCompatibleAdapter.protocol, openAiCompatibleAdapter]
|
|
15864
|
+
]);
|
|
15865
|
+
function getDiscoveryAdapter(protocol) {
|
|
15866
|
+
return adapters.get(protocol);
|
|
15867
|
+
}
|
|
15868
|
+
|
|
15869
|
+
// src/services/model-provider/ModelProviderService.ts
|
|
15870
|
+
var PHASE1_REGISTRY_TENANT = "default";
|
|
15871
|
+
var ALLOWED_LLM_PROVIDERS = /* @__PURE__ */ new Set(["openai"]);
|
|
15872
|
+
var unsafeReasonToErrorCode = {
|
|
15873
|
+
INVALID_BASE_URL: "INVALID_BASE_URL",
|
|
15874
|
+
UNSUPPORTED_SCHEME: "INVALID_BASE_URL",
|
|
15875
|
+
CREDENTIALS_IN_URL: "INVALID_BASE_URL"
|
|
15876
|
+
};
|
|
15877
|
+
var ModelProviderService = class {
|
|
15878
|
+
constructor(deps) {
|
|
15879
|
+
this.deps = deps;
|
|
15880
|
+
this.idFactory = deps.idFactory ?? (() => randomUUID15());
|
|
15881
|
+
}
|
|
15882
|
+
/**
|
|
15883
|
+
* List the tenant's provider configurations.
|
|
15884
|
+
*/
|
|
15885
|
+
async list(userId, tenantId) {
|
|
15886
|
+
await this.assertMember(userId, tenantId);
|
|
15887
|
+
const providers = await this.deps.providerStore.listProviders(tenantId);
|
|
15888
|
+
const models = await this.deps.providerStore.listModels(tenantId);
|
|
15889
|
+
const modelCounts = /* @__PURE__ */ new Map();
|
|
15890
|
+
for (const model of models) {
|
|
15891
|
+
modelCounts.set(
|
|
15892
|
+
model.providerId,
|
|
15893
|
+
(modelCounts.get(model.providerId) ?? 0) + 1
|
|
15894
|
+
);
|
|
15895
|
+
}
|
|
15896
|
+
return providers.map(
|
|
15897
|
+
(provider) => this.toPublicEntry(provider, modelCounts.get(provider.id) ?? 0)
|
|
15898
|
+
);
|
|
15899
|
+
}
|
|
15900
|
+
/**
|
|
15901
|
+
* Read one provider configuration.
|
|
15902
|
+
*/
|
|
15903
|
+
async get(userId, tenantId, id3) {
|
|
15904
|
+
await this.assertMember(userId, tenantId);
|
|
15905
|
+
const provider = await this.requireProvider(tenantId, id3);
|
|
15906
|
+
const models = await this.deps.providerStore.listModels(tenantId, id3);
|
|
15907
|
+
return this.toPublicEntry(provider, models.length);
|
|
15908
|
+
}
|
|
15909
|
+
/**
|
|
15910
|
+
* The full model catalog visible to a tenant.
|
|
15911
|
+
*/
|
|
15912
|
+
async catalog(userId, tenantId) {
|
|
15913
|
+
await this.assertMember(userId, tenantId);
|
|
15914
|
+
const providers = await this.deps.providerStore.listProviders(tenantId);
|
|
15915
|
+
const snapshot = await this.deps.providerStore.listModels(tenantId);
|
|
15916
|
+
const providerNames = new Map(
|
|
15917
|
+
providers.map((provider) => [provider.id, provider.name])
|
|
15918
|
+
);
|
|
15919
|
+
const disabledProviderIds = new Set(
|
|
15920
|
+
providers.filter((provider) => !provider.enabled).map((p) => p.id)
|
|
15921
|
+
);
|
|
15922
|
+
const modelCounts = /* @__PURE__ */ new Map();
|
|
15923
|
+
for (const model of snapshot) {
|
|
15924
|
+
modelCounts.set(
|
|
15925
|
+
model.providerId,
|
|
15926
|
+
(modelCounts.get(model.providerId) ?? 0) + 1
|
|
15927
|
+
);
|
|
15928
|
+
}
|
|
15929
|
+
const models = snapshot.map((model) => ({
|
|
15930
|
+
modelKey: toModelKey(model.providerId, model.modelId),
|
|
15931
|
+
displayName: model.displayName,
|
|
15932
|
+
providerId: model.providerId,
|
|
15933
|
+
providerName: providerNames.get(model.providerId) ?? model.providerId,
|
|
15934
|
+
ownedBy: model.ownedBy,
|
|
15935
|
+
source: "snapshot",
|
|
15936
|
+
enabled: model.enabled,
|
|
15937
|
+
stale: model.stale ?? false,
|
|
15938
|
+
executable: model.enabled && // A disabled provider is a kill switch: its models stay visible in the
|
|
15939
|
+
// catalog but are no longer reported as runnable.
|
|
15940
|
+
!disabledProviderIds.has(model.providerId) && this.deps.registry.has(
|
|
15941
|
+
tenantId,
|
|
15942
|
+
toModelKey(model.providerId, model.modelId)
|
|
15943
|
+
)
|
|
15944
|
+
}));
|
|
15945
|
+
const known = new Set(models.map((model) => model.modelKey));
|
|
15946
|
+
for (const modelKey2 of this.deps.registry.listRegistered(tenantId)) {
|
|
15947
|
+
if (known.has(modelKey2)) {
|
|
15948
|
+
continue;
|
|
15949
|
+
}
|
|
15950
|
+
models.push({
|
|
15951
|
+
modelKey: modelKey2,
|
|
15952
|
+
providerId: "",
|
|
15953
|
+
providerName: "registered",
|
|
15954
|
+
source: "registered",
|
|
15955
|
+
enabled: true,
|
|
15956
|
+
stale: false,
|
|
15957
|
+
executable: true
|
|
15958
|
+
});
|
|
15959
|
+
}
|
|
15960
|
+
return {
|
|
15961
|
+
providers: providers.map(
|
|
15962
|
+
(provider) => this.toPublicEntry(provider, modelCounts.get(provider.id) ?? 0)
|
|
15963
|
+
),
|
|
15964
|
+
models
|
|
15965
|
+
};
|
|
15966
|
+
}
|
|
15967
|
+
/**
|
|
15968
|
+
* Create a provider configuration. Does not run discovery.
|
|
15969
|
+
*/
|
|
15970
|
+
async create(userId, tenantId, input) {
|
|
15971
|
+
await this.assertAdmin(userId, tenantId);
|
|
15972
|
+
this.assertHasApiKey(input.apiKey);
|
|
15973
|
+
if (!getDiscoveryAdapter(input.protocol)) {
|
|
15974
|
+
throw new ModelProviderServiceError(
|
|
15975
|
+
"UNSUPPORTED_PROTOCOL",
|
|
15976
|
+
400,
|
|
15977
|
+
`Unsupported provider protocol`
|
|
15978
|
+
);
|
|
15979
|
+
}
|
|
15980
|
+
await this.assertSafeUrl(input.baseURL);
|
|
15981
|
+
const existing = await this.deps.providerStore.getProviderByName(
|
|
15982
|
+
tenantId,
|
|
15983
|
+
input.name
|
|
15984
|
+
);
|
|
15985
|
+
if (existing) {
|
|
15986
|
+
throw new ModelProviderServiceError(
|
|
15987
|
+
"PROVIDER_NAME_CONFLICT",
|
|
15988
|
+
409,
|
|
15989
|
+
"A provider with this name already exists"
|
|
15990
|
+
);
|
|
15991
|
+
}
|
|
15992
|
+
const id3 = this.idFactory();
|
|
15993
|
+
try {
|
|
15994
|
+
const created = await this.deps.providerStore.createProvider(
|
|
15995
|
+
tenantId,
|
|
15996
|
+
id3,
|
|
15997
|
+
{ ...input, enabled: input.enabled ?? true }
|
|
15998
|
+
);
|
|
15999
|
+
return this.toPublicEntry(created, 0);
|
|
16000
|
+
} catch (error) {
|
|
16001
|
+
throw this.mapStoreError(error);
|
|
16002
|
+
}
|
|
16003
|
+
}
|
|
16004
|
+
/**
|
|
16005
|
+
* Update a provider configuration.
|
|
16006
|
+
* An omitted or empty `apiKey` keeps the stored key.
|
|
16007
|
+
*/
|
|
16008
|
+
async update(userId, tenantId, id3, updates) {
|
|
16009
|
+
await this.assertAdmin(userId, tenantId);
|
|
16010
|
+
const existing = await this.requireProvider(tenantId, id3);
|
|
16011
|
+
if (updates.clearApiKey || !(updates.apiKey?.trim() || existing.apiKey)) {
|
|
16012
|
+
this.assertHasApiKey(void 0);
|
|
16013
|
+
}
|
|
16014
|
+
if (updates.protocol && !getDiscoveryAdapter(updates.protocol)) {
|
|
16015
|
+
throw new ModelProviderServiceError(
|
|
16016
|
+
"UNSUPPORTED_PROTOCOL",
|
|
16017
|
+
400,
|
|
16018
|
+
`Unsupported provider protocol`
|
|
16019
|
+
);
|
|
16020
|
+
}
|
|
16021
|
+
if (updates.baseURL && updates.baseURL !== existing.baseURL) {
|
|
16022
|
+
await this.assertSafeUrl(updates.baseURL);
|
|
16023
|
+
}
|
|
16024
|
+
if (updates.name && updates.name !== existing.name) {
|
|
16025
|
+
const conflicting = await this.deps.providerStore.getProviderByName(
|
|
16026
|
+
tenantId,
|
|
16027
|
+
updates.name
|
|
16028
|
+
);
|
|
16029
|
+
if (conflicting && conflicting.id !== id3) {
|
|
16030
|
+
throw new ModelProviderServiceError(
|
|
16031
|
+
"PROVIDER_NAME_CONFLICT",
|
|
16032
|
+
409,
|
|
16033
|
+
"A provider with this name already exists"
|
|
16034
|
+
);
|
|
16035
|
+
}
|
|
16036
|
+
}
|
|
16037
|
+
try {
|
|
16038
|
+
const updated = await this.deps.providerStore.updateProvider(
|
|
16039
|
+
tenantId,
|
|
16040
|
+
id3,
|
|
16041
|
+
updates
|
|
16042
|
+
);
|
|
16043
|
+
if (!updated) {
|
|
16044
|
+
throw new ModelProviderServiceError(
|
|
16045
|
+
"PROVIDER_NOT_FOUND",
|
|
16046
|
+
404,
|
|
16047
|
+
"Model provider not found"
|
|
16048
|
+
);
|
|
16049
|
+
}
|
|
16050
|
+
const models = await this.deps.providerStore.listModels(tenantId, id3);
|
|
16051
|
+
this.syncRegistry(tenantId, updated, models);
|
|
16052
|
+
return this.toPublicEntry(updated, models.length);
|
|
16053
|
+
} catch (error) {
|
|
16054
|
+
throw this.mapStoreError(error);
|
|
16055
|
+
}
|
|
16056
|
+
}
|
|
16057
|
+
/**
|
|
16058
|
+
* Delete a provider configuration, its snapshot, and any registry entries
|
|
16059
|
+
* that no other provider in the tenant still supplies.
|
|
16060
|
+
*/
|
|
16061
|
+
async remove(userId, tenantId, id3) {
|
|
16062
|
+
await this.assertAdmin(userId, tenantId);
|
|
16063
|
+
await this.requireProvider(tenantId, id3);
|
|
16064
|
+
const snapshot = await this.deps.providerStore.listModels(tenantId, id3);
|
|
16065
|
+
await this.deps.providerStore.deleteProvider(tenantId, id3);
|
|
16066
|
+
for (const model of snapshot) {
|
|
16067
|
+
this.deps.registry.remove(tenantId, toModelKey(id3, model.modelId));
|
|
16068
|
+
}
|
|
16069
|
+
}
|
|
16070
|
+
/**
|
|
16071
|
+
* Discover the provider's models and replace its snapshot.
|
|
16072
|
+
*/
|
|
16073
|
+
async discover(userId, tenantId, id3) {
|
|
16074
|
+
await this.assertAdmin(userId, tenantId);
|
|
16075
|
+
const provider = await this.requireProvider(tenantId, id3);
|
|
16076
|
+
if (!provider.enabled) {
|
|
16077
|
+
throw new ModelProviderServiceError(
|
|
16078
|
+
"PROVIDER_DISABLED",
|
|
16079
|
+
400,
|
|
16080
|
+
"Model provider is disabled"
|
|
16081
|
+
);
|
|
16082
|
+
}
|
|
16083
|
+
const adapter = getDiscoveryAdapter(provider.protocol);
|
|
16084
|
+
if (!adapter) {
|
|
16085
|
+
throw new ModelProviderServiceError(
|
|
16086
|
+
"UNSUPPORTED_PROTOCOL",
|
|
16087
|
+
400,
|
|
16088
|
+
`Unsupported provider protocol`
|
|
16089
|
+
);
|
|
16090
|
+
}
|
|
16091
|
+
await this.assertSafeUrl(provider.baseURL);
|
|
16092
|
+
const controller = new AbortController();
|
|
16093
|
+
const timeout = setTimeout(
|
|
16094
|
+
() => controller.abort(),
|
|
16095
|
+
resolveDiscoveryTimeoutMs()
|
|
16096
|
+
);
|
|
16097
|
+
const previous = this.shouldRegister(tenantId) ? await this.deps.providerStore.listModels(tenantId, id3) : [];
|
|
16098
|
+
let discovered;
|
|
16099
|
+
try {
|
|
16100
|
+
discovered = await adapter.discover({
|
|
16101
|
+
baseURL: provider.baseURL,
|
|
16102
|
+
apiKey: provider.apiKey,
|
|
16103
|
+
signal: controller.signal
|
|
16104
|
+
});
|
|
16105
|
+
} catch (error) {
|
|
16106
|
+
if (error instanceof ModelProviderServiceError) {
|
|
16107
|
+
await this.deps.providerStore.markProviderStatus(
|
|
16108
|
+
tenantId,
|
|
16109
|
+
id3,
|
|
16110
|
+
"error",
|
|
16111
|
+
error.code
|
|
16112
|
+
);
|
|
16113
|
+
throw error;
|
|
16114
|
+
}
|
|
16115
|
+
throw error;
|
|
16116
|
+
} finally {
|
|
16117
|
+
clearTimeout(timeout);
|
|
16118
|
+
}
|
|
16119
|
+
const now = /* @__PURE__ */ new Date();
|
|
16120
|
+
const inserted = await this.deps.providerStore.replaceProviderModels(
|
|
16121
|
+
tenantId,
|
|
16122
|
+
id3,
|
|
16123
|
+
discovered
|
|
16124
|
+
);
|
|
16125
|
+
await this.deps.providerStore.markProviderStatus(
|
|
16126
|
+
tenantId,
|
|
16127
|
+
id3,
|
|
16128
|
+
"ok",
|
|
16129
|
+
void 0,
|
|
16130
|
+
now
|
|
16131
|
+
);
|
|
16132
|
+
const registered = [];
|
|
16133
|
+
if (this.shouldRegister(tenantId)) {
|
|
16134
|
+
const snapshot = await this.deps.providerStore.listModels(tenantId, id3);
|
|
16135
|
+
const currentKeys = new Set(
|
|
16136
|
+
snapshot.map((model) => toModelKey(id3, model.modelId))
|
|
16137
|
+
);
|
|
16138
|
+
for (const model of previous) {
|
|
16139
|
+
const modelKey2 = toModelKey(id3, model.modelId);
|
|
16140
|
+
if (!currentKeys.has(modelKey2)) {
|
|
16141
|
+
this.deps.registry.remove(tenantId, modelKey2);
|
|
16142
|
+
}
|
|
16143
|
+
}
|
|
16144
|
+
registered.push(...this.syncRegistry(tenantId, provider, snapshot));
|
|
16145
|
+
}
|
|
16146
|
+
return {
|
|
16147
|
+
discovered,
|
|
16148
|
+
registered,
|
|
16149
|
+
status: "ok"
|
|
16150
|
+
};
|
|
16151
|
+
}
|
|
16152
|
+
/**
|
|
16153
|
+
* Bring the execution registry in line with a provider's current state.
|
|
16154
|
+
*
|
|
16155
|
+
* Registration copies `apiKey` and `baseURL` by value, so a rotated
|
|
16156
|
+
* credential or a changed endpoint only takes effect once the model is
|
|
16157
|
+
* re-registered. Disabled models — and every model of a disabled provider —
|
|
16158
|
+
* are removed instead.
|
|
16159
|
+
*
|
|
16160
|
+
* @returns The model keys left registered
|
|
16161
|
+
*/
|
|
16162
|
+
/**
|
|
16163
|
+
* Probe an upstream without touching the store.
|
|
16164
|
+
*
|
|
16165
|
+
* Powers the provider form's "test latency" and "list models" actions, which
|
|
16166
|
+
* validate a base URL and API key the user has typed but not yet saved. The
|
|
16167
|
+
* URL is validated exactly as on create/update and is never persisted, so the
|
|
16168
|
+
* "discover resolves its URL from the store" rule is not weakened — nothing
|
|
16169
|
+
* a probe learns can reach the snapshot.
|
|
16170
|
+
*/
|
|
16171
|
+
async probe(userId, tenantId, input) {
|
|
16172
|
+
await this.assertAdmin(userId, tenantId);
|
|
16173
|
+
const protocol = input.protocol ?? "openai-compatible";
|
|
16174
|
+
const adapter = getDiscoveryAdapter(protocol);
|
|
16175
|
+
if (!adapter) {
|
|
16176
|
+
throw new ModelProviderServiceError(
|
|
16177
|
+
"UNSUPPORTED_PROTOCOL",
|
|
16178
|
+
400,
|
|
16179
|
+
`Unsupported provider protocol`
|
|
16180
|
+
);
|
|
16181
|
+
}
|
|
16182
|
+
await this.assertSafeUrl(input.baseURL);
|
|
16183
|
+
let apiKey = input.apiKey || void 0;
|
|
16184
|
+
if (!apiKey && input.providerId) {
|
|
16185
|
+
const existing = await this.requireProvider(tenantId, input.providerId);
|
|
16186
|
+
apiKey = existing.apiKey;
|
|
16187
|
+
}
|
|
16188
|
+
const controller = new AbortController();
|
|
16189
|
+
const timeout = setTimeout(
|
|
16190
|
+
() => controller.abort(),
|
|
16191
|
+
resolveDiscoveryTimeoutMs()
|
|
16192
|
+
);
|
|
16193
|
+
const startedAt = Date.now();
|
|
16194
|
+
try {
|
|
16195
|
+
const models = await adapter.discover({
|
|
16196
|
+
baseURL: input.baseURL,
|
|
16197
|
+
apiKey,
|
|
16198
|
+
signal: controller.signal
|
|
16199
|
+
});
|
|
16200
|
+
return {
|
|
16201
|
+
reachable: true,
|
|
16202
|
+
latencyMs: Date.now() - startedAt,
|
|
16203
|
+
models
|
|
16204
|
+
};
|
|
16205
|
+
} catch (error) {
|
|
16206
|
+
if (error instanceof ModelProviderServiceError) {
|
|
16207
|
+
return {
|
|
16208
|
+
reachable: false,
|
|
16209
|
+
latencyMs: Date.now() - startedAt,
|
|
16210
|
+
models: [],
|
|
16211
|
+
error: error.code
|
|
16212
|
+
};
|
|
16213
|
+
}
|
|
16214
|
+
throw error;
|
|
16215
|
+
} finally {
|
|
16216
|
+
clearTimeout(timeout);
|
|
16217
|
+
}
|
|
16218
|
+
}
|
|
16219
|
+
/**
|
|
16220
|
+
* Set exactly which models this provider contributes.
|
|
16221
|
+
*
|
|
16222
|
+
* This is the "add model" action: the caller passes the ids it picked from
|
|
16223
|
+
* the live list, and the provider's model set becomes precisely those. Ids
|
|
16224
|
+
* are expected to have come from a probe — model names cannot be guessed,
|
|
16225
|
+
* since a wrong id surfaces later as "model not found" or an authorisation
|
|
16226
|
+
* error rather than at configuration time.
|
|
16227
|
+
*/
|
|
16228
|
+
async setModels(userId, tenantId, id3, selection) {
|
|
16229
|
+
await this.assertAdmin(userId, tenantId);
|
|
16230
|
+
const provider = await this.requireProvider(tenantId, id3);
|
|
16231
|
+
const previous = await this.deps.providerStore.listModels(tenantId, id3);
|
|
16232
|
+
const previousById = new Map(
|
|
16233
|
+
previous.map((model) => [model.modelId, model])
|
|
16234
|
+
);
|
|
16235
|
+
await this.deps.providerStore.replaceProviderModels(
|
|
16236
|
+
tenantId,
|
|
16237
|
+
id3,
|
|
16238
|
+
selection.map((entry) => ({
|
|
16239
|
+
id: entry.modelId,
|
|
16240
|
+
enabled: entry.enabled,
|
|
16241
|
+
// A model the upstream stopped offering stays in the set, flagged
|
|
16242
|
+
// rather than dropped: deleting it would silently break anything
|
|
16243
|
+
// referencing its key.
|
|
16244
|
+
stale: entry.stale ?? false,
|
|
16245
|
+
// The selection carries only ids and states, so a model already in the
|
|
16246
|
+
// snapshot keeps the metadata discovery gave it.
|
|
16247
|
+
displayName: previousById.get(entry.modelId)?.displayName,
|
|
16248
|
+
ownedBy: previousById.get(entry.modelId)?.ownedBy
|
|
16249
|
+
}))
|
|
16250
|
+
);
|
|
16251
|
+
const models = await this.deps.providerStore.listModels(tenantId, id3);
|
|
16252
|
+
const kept = new Set(models.map((model) => model.modelId));
|
|
16253
|
+
for (const model of previous) {
|
|
16254
|
+
if (!kept.has(model.modelId)) {
|
|
16255
|
+
this.deps.registry.remove(
|
|
16256
|
+
tenantId,
|
|
16257
|
+
toModelKey(provider.id, model.modelId)
|
|
16258
|
+
);
|
|
16259
|
+
}
|
|
16260
|
+
}
|
|
16261
|
+
this.syncRegistry(tenantId, provider, models);
|
|
16262
|
+
return this.toPublicEntry(provider, models.length);
|
|
16263
|
+
}
|
|
16264
|
+
syncRegistry(tenantId, provider, models) {
|
|
16265
|
+
if (!this.shouldRegister(tenantId)) {
|
|
16266
|
+
return [];
|
|
16267
|
+
}
|
|
16268
|
+
const registered = [];
|
|
16269
|
+
for (const model of models) {
|
|
16270
|
+
const modelKey2 = toModelKey(provider.id, model.modelId);
|
|
16271
|
+
if (!provider.enabled || !model.enabled) {
|
|
16272
|
+
this.deps.registry.remove(tenantId, modelKey2);
|
|
16273
|
+
continue;
|
|
16274
|
+
}
|
|
16275
|
+
this.deps.registry.register(
|
|
16276
|
+
tenantId,
|
|
16277
|
+
modelKey2,
|
|
16278
|
+
this.toLlmConfig(provider, model.modelId)
|
|
16279
|
+
);
|
|
16280
|
+
registered.push(modelKey2);
|
|
16281
|
+
}
|
|
16282
|
+
return registered;
|
|
16283
|
+
}
|
|
16284
|
+
/**
|
|
16285
|
+
* Enable or disable a single discovered model.
|
|
16286
|
+
*/
|
|
16287
|
+
async setModelEnabled(userId, tenantId, id3, modelId, enabled) {
|
|
16288
|
+
await this.assertAdmin(userId, tenantId);
|
|
16289
|
+
const provider = await this.requireProvider(tenantId, id3);
|
|
16290
|
+
const updated = await this.deps.providerStore.setModelEnabled(
|
|
16291
|
+
tenantId,
|
|
16292
|
+
id3,
|
|
16293
|
+
modelId,
|
|
16294
|
+
enabled
|
|
16295
|
+
);
|
|
16296
|
+
if (!updated) {
|
|
16297
|
+
throw new ModelProviderServiceError(
|
|
16298
|
+
"MODEL_KEY_CONFLICT",
|
|
16299
|
+
404,
|
|
16300
|
+
"Model not found for this provider"
|
|
16301
|
+
);
|
|
16302
|
+
}
|
|
16303
|
+
const models = await this.deps.providerStore.listModels(tenantId, id3);
|
|
16304
|
+
this.syncRegistry(tenantId, provider, models);
|
|
16305
|
+
return this.toPublicEntry(provider, models.length);
|
|
16306
|
+
}
|
|
16307
|
+
/**
|
|
16308
|
+
* Whether discovered models should be registered for execution.
|
|
16309
|
+
*/
|
|
16310
|
+
shouldRegister(tenantId) {
|
|
16311
|
+
return tenantId === PHASE1_REGISTRY_TENANT;
|
|
16312
|
+
}
|
|
16313
|
+
/**
|
|
16314
|
+
* Build the runtime LLM config for a discovered model.
|
|
16315
|
+
*
|
|
16316
|
+
* `apiKeyEnvName`, `extra` and `modelKwargs` are deliberately omitted — they
|
|
16317
|
+
* would let a tenant name an arbitrary process environment variable to read
|
|
16318
|
+
* credentials from, or inject arbitrary constructor options.
|
|
16319
|
+
*/
|
|
16320
|
+
toLlmConfig(provider, modelId) {
|
|
16321
|
+
if (!ALLOWED_LLM_PROVIDERS.has(provider.llmProvider)) {
|
|
16322
|
+
throw new ModelProviderServiceError(
|
|
16323
|
+
"UNSUPPORTED_PROTOCOL",
|
|
16324
|
+
400,
|
|
16325
|
+
"Provider is configured with an unsupported LLM provider"
|
|
16326
|
+
);
|
|
16327
|
+
}
|
|
16328
|
+
return {
|
|
16329
|
+
provider: provider.llmProvider,
|
|
16330
|
+
model: modelId,
|
|
16331
|
+
displayName: modelId,
|
|
16332
|
+
streaming: true,
|
|
16333
|
+
apiKey: provider.apiKey,
|
|
16334
|
+
baseURL: provider.baseURL,
|
|
16335
|
+
// Chat Completions and the Responses API are different routes with
|
|
16336
|
+
// different request fields; without this the gateway would post
|
|
16337
|
+
// `messages` to an endpoint expecting `input`.
|
|
16338
|
+
...provider.apiStyle === "responses" ? { useResponsesApi: true } : {}
|
|
16339
|
+
};
|
|
16340
|
+
}
|
|
16341
|
+
/**
|
|
16342
|
+
* The sole projection from an internal provider entry to an API-safe shape.
|
|
16343
|
+
*/
|
|
16344
|
+
toPublicEntry(provider, modelCount) {
|
|
16345
|
+
return {
|
|
16346
|
+
id: provider.id,
|
|
16347
|
+
tenantId: provider.tenantId,
|
|
16348
|
+
name: provider.name,
|
|
16349
|
+
displayName: provider.displayName,
|
|
16350
|
+
protocol: provider.protocol,
|
|
16351
|
+
apiStyle: provider.apiStyle,
|
|
16352
|
+
llmProvider: provider.llmProvider,
|
|
16353
|
+
baseURL: provider.baseURL,
|
|
16354
|
+
hasApiKey: Boolean(provider.apiKey),
|
|
16355
|
+
apiKeyHint: provider.apiKeyHint,
|
|
16356
|
+
enabled: provider.enabled,
|
|
16357
|
+
status: provider.status,
|
|
16358
|
+
lastError: provider.lastError,
|
|
16359
|
+
lastDiscoveredAt: provider.lastDiscoveredAt?.toISOString(),
|
|
16360
|
+
modelCount,
|
|
16361
|
+
createdAt: provider.createdAt.toISOString(),
|
|
16362
|
+
updatedAt: provider.updatedAt.toISOString()
|
|
16363
|
+
};
|
|
16364
|
+
}
|
|
16365
|
+
/**
|
|
16366
|
+
* Reject a provider that would end up without a credential.
|
|
16367
|
+
*
|
|
16368
|
+
* @param apiKey - The key the provider would hold, if any
|
|
16369
|
+
* @throws {ModelProviderServiceError} When no key is present
|
|
16370
|
+
*/
|
|
16371
|
+
assertHasApiKey(apiKey) {
|
|
16372
|
+
if (!apiKey?.trim()) {
|
|
16373
|
+
throw new ModelProviderServiceError(
|
|
16374
|
+
"INVALID_INPUT",
|
|
16375
|
+
400,
|
|
16376
|
+
"A model provider must have an API key"
|
|
16377
|
+
);
|
|
16378
|
+
}
|
|
16379
|
+
}
|
|
16380
|
+
/**
|
|
16381
|
+
* Load a provider or fail with 404.
|
|
16382
|
+
*
|
|
16383
|
+
* Callers always pass the session's tenant, so a provider belonging to
|
|
16384
|
+
* another tenant is indistinguishable from a missing one.
|
|
16385
|
+
*/
|
|
16386
|
+
async requireProvider(tenantId, id3) {
|
|
16387
|
+
const provider = await this.deps.providerStore.getProviderById(tenantId, id3);
|
|
16388
|
+
if (!provider) {
|
|
16389
|
+
throw new ModelProviderServiceError(
|
|
16390
|
+
"PROVIDER_NOT_FOUND",
|
|
16391
|
+
404,
|
|
16392
|
+
"Model provider not found"
|
|
16393
|
+
);
|
|
16394
|
+
}
|
|
16395
|
+
return provider;
|
|
16396
|
+
}
|
|
16397
|
+
/**
|
|
16398
|
+
* Require that the caller belongs to the tenant.
|
|
16399
|
+
* An absent userId means the unauthenticated development fallback.
|
|
16400
|
+
*/
|
|
16401
|
+
async assertMember(userId, tenantId) {
|
|
16402
|
+
if (!userId) {
|
|
16403
|
+
return;
|
|
16404
|
+
}
|
|
16405
|
+
const link = await this.deps.userTenantLinkStore.getLink(userId, tenantId);
|
|
16406
|
+
if (!link) {
|
|
16407
|
+
throw new ModelProviderServiceError(
|
|
16408
|
+
"TENANT_ACCESS_DENIED",
|
|
16409
|
+
403,
|
|
16410
|
+
"Tenant access denied"
|
|
16411
|
+
);
|
|
16412
|
+
}
|
|
16413
|
+
}
|
|
16414
|
+
/**
|
|
16415
|
+
* Require that the caller is a tenant owner or admin.
|
|
16416
|
+
* Roles are read from the tenant link, never from the session token.
|
|
16417
|
+
*/
|
|
16418
|
+
async assertAdmin(userId, tenantId) {
|
|
16419
|
+
await this.assertMember(userId, tenantId);
|
|
16420
|
+
if (!userId) {
|
|
16421
|
+
return;
|
|
16422
|
+
}
|
|
16423
|
+
const link = await this.deps.userTenantLinkStore.getLink(userId, tenantId);
|
|
16424
|
+
if (link?.role !== "owner" && link?.role !== "admin") {
|
|
16425
|
+
throw new ModelProviderServiceError(
|
|
16426
|
+
"FORBIDDEN_ROLE",
|
|
16427
|
+
403,
|
|
16428
|
+
"Only tenant owner or admin can manage model providers"
|
|
16429
|
+
);
|
|
16430
|
+
}
|
|
16431
|
+
}
|
|
16432
|
+
/**
|
|
16433
|
+
* Validate the shape of an upstream URL and translate failures into service
|
|
16434
|
+
* errors.
|
|
16435
|
+
*
|
|
16436
|
+
* Addresses are deliberately not restricted: other tenant-configured URLs in
|
|
16437
|
+
* this platform (metrics servers, MCP servers) reach wherever they point, and
|
|
16438
|
+
* self-hosted model servers normally sit on the gateway's own network.
|
|
16439
|
+
*
|
|
16440
|
+
* The mapping is explicit so the public error codes stay decoupled from the
|
|
16441
|
+
* reasons the URL utility happens to use internally.
|
|
16442
|
+
*/
|
|
16443
|
+
async assertSafeUrl(baseURL) {
|
|
16444
|
+
try {
|
|
16445
|
+
await assertSafeUpstreamUrl(baseURL);
|
|
16446
|
+
} catch (error) {
|
|
16447
|
+
if (error instanceof UnsafeUpstreamUrlError) {
|
|
16448
|
+
throw new ModelProviderServiceError(
|
|
16449
|
+
unsafeReasonToErrorCode[error.reason],
|
|
16450
|
+
400,
|
|
16451
|
+
error.message
|
|
16452
|
+
);
|
|
16453
|
+
}
|
|
16454
|
+
throw error;
|
|
16455
|
+
}
|
|
16456
|
+
}
|
|
16457
|
+
/**
|
|
16458
|
+
* Translate a store-level unique violation into a stable service error.
|
|
16459
|
+
*/
|
|
16460
|
+
mapStoreError(error) {
|
|
16461
|
+
if (error instanceof ModelProviderServiceError) {
|
|
16462
|
+
return error;
|
|
16463
|
+
}
|
|
16464
|
+
const code = error?.code;
|
|
16465
|
+
if (code === "23505") {
|
|
16466
|
+
return new ModelProviderServiceError(
|
|
16467
|
+
"PROVIDER_NAME_CONFLICT",
|
|
16468
|
+
409,
|
|
16469
|
+
"A provider with this name already exists"
|
|
16470
|
+
);
|
|
16471
|
+
}
|
|
16472
|
+
if (code === "23503") {
|
|
16473
|
+
return new ModelProviderServiceError(
|
|
16474
|
+
"INVALID_INPUT",
|
|
16475
|
+
400,
|
|
16476
|
+
"Model provider references a missing record"
|
|
16477
|
+
);
|
|
16478
|
+
}
|
|
16479
|
+
throw error;
|
|
16480
|
+
}
|
|
16481
|
+
};
|
|
16482
|
+
|
|
16483
|
+
// src/schemas/model-provider.ts
|
|
16484
|
+
import { z as z9 } from "zod";
|
|
16485
|
+
var providerId = z9.string().trim().min(1).max(255);
|
|
16486
|
+
var providerParamsSchema = z9.object({ providerId }).strict();
|
|
16487
|
+
var protocolSchema = z9.enum(["openai-compatible"]);
|
|
16488
|
+
var apiStyleSchema = z9.enum(["chat-completions", "responses"]);
|
|
16489
|
+
var createProviderSchema = z9.object({
|
|
16490
|
+
name: z9.string().trim().min(1).max(255),
|
|
16491
|
+
displayName: z9.string().trim().max(255).optional(),
|
|
16492
|
+
protocol: protocolSchema,
|
|
16493
|
+
apiStyle: apiStyleSchema.optional(),
|
|
16494
|
+
baseURL: z9.string().trim().min(1).max(2048),
|
|
16495
|
+
// Required, not optional: see CreateModelProviderRequest.apiKey — a
|
|
16496
|
+
// keyless provider would make the gateway send its own env credential.
|
|
16497
|
+
apiKey: z9.string().trim().min(1).max(1024),
|
|
16498
|
+
enabled: z9.boolean().optional()
|
|
16499
|
+
}).strict();
|
|
16500
|
+
var updateProviderSchema = createProviderSchema.partial().extend({
|
|
16501
|
+
/**
|
|
16502
|
+
* Remove the stored API key. Needed because an empty `apiKey` means "keep":
|
|
16503
|
+
* without this a leaked credential could not be revoked.
|
|
16504
|
+
*/
|
|
16505
|
+
clearApiKey: z9.boolean().optional()
|
|
16506
|
+
}).strict();
|
|
16507
|
+
var setModelEnabledSchema = z9.object({
|
|
16508
|
+
modelId: z9.string().trim().min(1).max(512),
|
|
16509
|
+
enabled: z9.boolean()
|
|
16510
|
+
}).strict();
|
|
16511
|
+
var discoverSchema = z9.object({}).strict();
|
|
16512
|
+
var setModelsSchema = z9.object({
|
|
16513
|
+
models: z9.array(
|
|
16514
|
+
z9.object({
|
|
16515
|
+
modelId: z9.string().trim().min(1).max(512),
|
|
16516
|
+
enabled: z9.boolean(),
|
|
16517
|
+
stale: z9.boolean().optional()
|
|
16518
|
+
}).strict()
|
|
16519
|
+
).max(1e3)
|
|
16520
|
+
}).strict();
|
|
16521
|
+
var probeProviderSchema = z9.object({
|
|
16522
|
+
baseURL: z9.string().trim().min(1).max(2048),
|
|
16523
|
+
apiKey: z9.string().max(1024).optional(),
|
|
16524
|
+
providerId: z9.string().trim().min(1).max(255).optional(),
|
|
16525
|
+
protocol: protocolSchema.optional()
|
|
16526
|
+
}).strict();
|
|
16527
|
+
|
|
16528
|
+
// src/controllers/tenant-scope.ts
|
|
16529
|
+
var TenantScopeError2 = class extends Error {
|
|
16530
|
+
/** Creates a tenant scope error. */
|
|
16531
|
+
constructor(code, statusCode, message) {
|
|
16532
|
+
super(message);
|
|
16533
|
+
this.code = code;
|
|
16534
|
+
this.statusCode = statusCode;
|
|
16535
|
+
this.name = "TenantScopeError";
|
|
16536
|
+
}
|
|
16537
|
+
};
|
|
16538
|
+
function resolveTenantScope(request, authRequired2) {
|
|
16539
|
+
const authenticated = request.user?.tenantId;
|
|
16540
|
+
const header2 = request.headers["x-tenant-id"];
|
|
16541
|
+
const requested = Array.isArray(header2) ? header2[0] : header2;
|
|
16542
|
+
if (authenticated) {
|
|
16543
|
+
if (requested && requested !== authenticated) {
|
|
16544
|
+
throw new TenantScopeError2(
|
|
16545
|
+
"TENANT_ACCESS_DENIED",
|
|
16546
|
+
403,
|
|
16547
|
+
"Tenant access denied"
|
|
16548
|
+
);
|
|
16549
|
+
}
|
|
16550
|
+
return { tenantId: authenticated, userId: request.user?.id };
|
|
16551
|
+
}
|
|
16552
|
+
if (authRequired2) {
|
|
16553
|
+
throw new TenantScopeError2(
|
|
16554
|
+
"UNAUTHENTICATED",
|
|
16555
|
+
401,
|
|
16556
|
+
"Console session required"
|
|
16557
|
+
);
|
|
16558
|
+
}
|
|
16559
|
+
return { tenantId: requested || "default" };
|
|
16560
|
+
}
|
|
16561
|
+
|
|
16562
|
+
// src/controllers/model-providers.ts
|
|
16563
|
+
var logger4 = new Logger({ serviceName: "lattice/gateway/model-providers" });
|
|
16564
|
+
function sendError2(reply, error) {
|
|
16565
|
+
if (error instanceof ModelProviderServiceError) {
|
|
16566
|
+
return reply.status(error.statusCode).send({
|
|
16567
|
+
success: false,
|
|
16568
|
+
error: {
|
|
16569
|
+
code: error.code,
|
|
16570
|
+
message: error.message,
|
|
16571
|
+
retryable: error.retryable
|
|
16572
|
+
}
|
|
16573
|
+
});
|
|
16574
|
+
}
|
|
16575
|
+
if (error instanceof TenantScopeError2) {
|
|
16576
|
+
return reply.status(error.statusCode).send({
|
|
16577
|
+
success: false,
|
|
16578
|
+
error: {
|
|
16579
|
+
code: error.code,
|
|
16580
|
+
message: error.message,
|
|
16581
|
+
retryable: false
|
|
16582
|
+
}
|
|
16583
|
+
});
|
|
16584
|
+
}
|
|
16585
|
+
if (error instanceof ZodError3) {
|
|
16586
|
+
return reply.status(400).send({
|
|
16587
|
+
success: false,
|
|
16588
|
+
error: {
|
|
16589
|
+
code: "INVALID_INPUT",
|
|
16590
|
+
message: "Invalid request",
|
|
16591
|
+
retryable: false
|
|
16592
|
+
}
|
|
16593
|
+
});
|
|
16594
|
+
}
|
|
16595
|
+
logger4.error("Model provider controller error", {
|
|
16596
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
16597
|
+
});
|
|
16598
|
+
return reply.status(500).send({
|
|
16599
|
+
success: false,
|
|
16600
|
+
error: {
|
|
16601
|
+
code: "INTERNAL_ERROR",
|
|
16602
|
+
message: "Internal server error",
|
|
16603
|
+
retryable: false
|
|
16604
|
+
}
|
|
16605
|
+
});
|
|
16606
|
+
}
|
|
16607
|
+
var ModelProviderController = class {
|
|
16608
|
+
constructor(deps, options = {}) {
|
|
16609
|
+
this.service = new ModelProviderService(deps);
|
|
16610
|
+
this.authRequired = options.authRequired ?? true;
|
|
16611
|
+
}
|
|
16612
|
+
/** GET /api/tenant-models */
|
|
16613
|
+
async getCatalog(request, reply) {
|
|
16614
|
+
try {
|
|
16615
|
+
const scope = resolveTenantScope(request, this.authRequired);
|
|
16616
|
+
const catalog = await this.service.catalog(scope.userId, scope.tenantId);
|
|
16617
|
+
return reply.send({ success: true, data: catalog });
|
|
16618
|
+
} catch (error) {
|
|
16619
|
+
return sendError2(reply, error);
|
|
16620
|
+
}
|
|
16621
|
+
}
|
|
16622
|
+
/** GET /api/model-providers */
|
|
16623
|
+
async listProviders(request, reply) {
|
|
16624
|
+
try {
|
|
16625
|
+
const scope = resolveTenantScope(request, this.authRequired);
|
|
16626
|
+
const records = await this.service.list(scope.userId, scope.tenantId);
|
|
16627
|
+
return reply.send({
|
|
16628
|
+
success: true,
|
|
16629
|
+
data: { records, total: records.length }
|
|
16630
|
+
});
|
|
16631
|
+
} catch (error) {
|
|
16632
|
+
return sendError2(reply, error);
|
|
16633
|
+
}
|
|
16634
|
+
}
|
|
16635
|
+
/** GET /api/model-providers/:providerId */
|
|
16636
|
+
async getProvider(request, reply) {
|
|
16637
|
+
try {
|
|
16638
|
+
const scope = resolveTenantScope(request, this.authRequired);
|
|
16639
|
+
const { providerId: providerId2 } = providerParamsSchema.parse(request.params);
|
|
16640
|
+
return reply.send({
|
|
16641
|
+
success: true,
|
|
16642
|
+
data: await this.service.get(scope.userId, scope.tenantId, providerId2)
|
|
16643
|
+
});
|
|
16644
|
+
} catch (error) {
|
|
16645
|
+
return sendError2(reply, error);
|
|
16646
|
+
}
|
|
16647
|
+
}
|
|
16648
|
+
/** POST /api/model-providers */
|
|
16649
|
+
async createProvider(request, reply) {
|
|
16650
|
+
try {
|
|
16651
|
+
const scope = resolveTenantScope(request, this.authRequired);
|
|
16652
|
+
const input = createProviderSchema.parse(request.body);
|
|
16653
|
+
return reply.status(201).send({
|
|
16654
|
+
success: true,
|
|
16655
|
+
data: await this.service.create(scope.userId, scope.tenantId, input)
|
|
16656
|
+
});
|
|
16657
|
+
} catch (error) {
|
|
16658
|
+
return sendError2(reply, error);
|
|
16659
|
+
}
|
|
16660
|
+
}
|
|
16661
|
+
/** PUT /api/model-providers/:providerId */
|
|
16662
|
+
async updateProvider(request, reply) {
|
|
16663
|
+
try {
|
|
16664
|
+
const scope = resolveTenantScope(request, this.authRequired);
|
|
16665
|
+
const { providerId: providerId2 } = providerParamsSchema.parse(request.params);
|
|
16666
|
+
const input = updateProviderSchema.parse(request.body);
|
|
16667
|
+
return reply.send({
|
|
16668
|
+
success: true,
|
|
16669
|
+
data: await this.service.update(
|
|
16670
|
+
scope.userId,
|
|
16671
|
+
scope.tenantId,
|
|
16672
|
+
providerId2,
|
|
16673
|
+
input
|
|
16674
|
+
)
|
|
16675
|
+
});
|
|
16676
|
+
} catch (error) {
|
|
16677
|
+
return sendError2(reply, error);
|
|
16678
|
+
}
|
|
16679
|
+
}
|
|
16680
|
+
/** DELETE /api/model-providers/:providerId */
|
|
16681
|
+
async deleteProvider(request, reply) {
|
|
16682
|
+
try {
|
|
16683
|
+
const scope = resolveTenantScope(request, this.authRequired);
|
|
16684
|
+
const { providerId: providerId2 } = providerParamsSchema.parse(request.params);
|
|
16685
|
+
await this.service.remove(scope.userId, scope.tenantId, providerId2);
|
|
16686
|
+
return reply.send({ success: true, data: null });
|
|
16687
|
+
} catch (error) {
|
|
16688
|
+
return sendError2(reply, error);
|
|
16689
|
+
}
|
|
16690
|
+
}
|
|
16691
|
+
/** POST /api/model-providers/probe */
|
|
16692
|
+
async probeProvider(request, reply) {
|
|
16693
|
+
try {
|
|
16694
|
+
const scope = resolveTenantScope(request, this.authRequired);
|
|
16695
|
+
const input = probeProviderSchema.parse(request.body);
|
|
16696
|
+
return reply.send({
|
|
16697
|
+
success: true,
|
|
16698
|
+
data: await this.service.probe(scope.userId, scope.tenantId, input)
|
|
16699
|
+
});
|
|
16700
|
+
} catch (error) {
|
|
16701
|
+
return sendError2(reply, error);
|
|
16702
|
+
}
|
|
16703
|
+
}
|
|
16704
|
+
/** PUT /api/model-providers/:providerId/models */
|
|
16705
|
+
async setProviderModels(request, reply) {
|
|
16706
|
+
try {
|
|
16707
|
+
const scope = resolveTenantScope(request, this.authRequired);
|
|
16708
|
+
const { providerId: providerId2 } = providerParamsSchema.parse(request.params);
|
|
16709
|
+
const { models } = setModelsSchema.parse(request.body);
|
|
16710
|
+
return reply.send({
|
|
16711
|
+
success: true,
|
|
16712
|
+
data: await this.service.setModels(
|
|
16713
|
+
scope.userId,
|
|
16714
|
+
scope.tenantId,
|
|
16715
|
+
providerId2,
|
|
16716
|
+
models
|
|
16717
|
+
)
|
|
16718
|
+
});
|
|
16719
|
+
} catch (error) {
|
|
16720
|
+
return sendError2(reply, error);
|
|
16721
|
+
}
|
|
16722
|
+
}
|
|
16723
|
+
/** POST /api/model-providers/:providerId/discover */
|
|
16724
|
+
async discoverProvider(request, reply) {
|
|
16725
|
+
try {
|
|
16726
|
+
const scope = resolveTenantScope(request, this.authRequired);
|
|
16727
|
+
const { providerId: providerId2 } = providerParamsSchema.parse(request.params);
|
|
16728
|
+
discoverSchema.parse(request.body ?? {});
|
|
16729
|
+
return reply.send({
|
|
16730
|
+
success: true,
|
|
16731
|
+
data: await this.service.discover(
|
|
16732
|
+
scope.userId,
|
|
16733
|
+
scope.tenantId,
|
|
16734
|
+
providerId2
|
|
16735
|
+
)
|
|
16736
|
+
});
|
|
16737
|
+
} catch (error) {
|
|
16738
|
+
return sendError2(reply, error);
|
|
16739
|
+
}
|
|
16740
|
+
}
|
|
16741
|
+
/** POST /api/model-providers/:providerId/models/enabled */
|
|
16742
|
+
async setModelEnabled(request, reply) {
|
|
16743
|
+
try {
|
|
16744
|
+
const scope = resolveTenantScope(request, this.authRequired);
|
|
16745
|
+
const { providerId: providerId2 } = providerParamsSchema.parse(request.params);
|
|
16746
|
+
const { modelId, enabled } = setModelEnabledSchema.parse(request.body);
|
|
16747
|
+
return reply.send({
|
|
16748
|
+
success: true,
|
|
16749
|
+
data: await this.service.setModelEnabled(
|
|
16750
|
+
scope.userId,
|
|
16751
|
+
scope.tenantId,
|
|
16752
|
+
providerId2,
|
|
16753
|
+
modelId,
|
|
16754
|
+
enabled
|
|
16755
|
+
)
|
|
16756
|
+
});
|
|
16757
|
+
} catch (error) {
|
|
16758
|
+
return sendError2(reply, error);
|
|
16759
|
+
}
|
|
16760
|
+
}
|
|
16761
|
+
};
|
|
16762
|
+
function registerModelProviderRoutes(app2, controller) {
|
|
16763
|
+
app2.get("/api/tenant-models", controller.getCatalog.bind(controller));
|
|
16764
|
+
app2.get("/api/model-providers", controller.listProviders.bind(controller));
|
|
16765
|
+
app2.post("/api/model-providers", controller.createProvider.bind(controller));
|
|
16766
|
+
app2.post("/api/model-providers/probe", controller.probeProvider.bind(controller));
|
|
16767
|
+
app2.get(
|
|
16768
|
+
"/api/model-providers/:providerId",
|
|
16769
|
+
controller.getProvider.bind(controller)
|
|
16770
|
+
);
|
|
16771
|
+
app2.put(
|
|
16772
|
+
"/api/model-providers/:providerId",
|
|
16773
|
+
controller.updateProvider.bind(controller)
|
|
16774
|
+
);
|
|
16775
|
+
app2.delete(
|
|
16776
|
+
"/api/model-providers/:providerId",
|
|
16777
|
+
controller.deleteProvider.bind(controller)
|
|
16778
|
+
);
|
|
16779
|
+
app2.post(
|
|
16780
|
+
"/api/model-providers/:providerId/discover",
|
|
16781
|
+
controller.discoverProvider.bind(controller)
|
|
16782
|
+
);
|
|
16783
|
+
app2.put(
|
|
16784
|
+
"/api/model-providers/:providerId/models",
|
|
16785
|
+
controller.setProviderModels.bind(controller)
|
|
16786
|
+
);
|
|
16787
|
+
app2.post(
|
|
16788
|
+
"/api/model-providers/:providerId/models/enabled",
|
|
16789
|
+
controller.setModelEnabled.bind(controller)
|
|
16790
|
+
);
|
|
16791
|
+
}
|
|
16792
|
+
|
|
16793
|
+
// src/services/collaboration/ProjectRoomService.ts
|
|
16794
|
+
import { randomUUID as randomUUID16 } from "crypto";
|
|
15664
16795
|
var ProjectRoomService = class {
|
|
15665
16796
|
/** Creates a project room service from explicitly injected collaborators. */
|
|
15666
16797
|
constructor(deps) {
|
|
15667
16798
|
this.deps = deps;
|
|
15668
|
-
this.idFactory = deps.idFactory ??
|
|
16799
|
+
this.idFactory = deps.idFactory ?? randomUUID16;
|
|
15669
16800
|
}
|
|
15670
16801
|
/**
|
|
15671
16802
|
* Authorizes read access and ensures the project's main room.
|
|
@@ -15725,7 +16856,7 @@ var ProjectRoomService = class {
|
|
|
15725
16856
|
};
|
|
15726
16857
|
|
|
15727
16858
|
// src/services/collaboration/ProjectMembershipService.ts
|
|
15728
|
-
import { randomUUID as
|
|
16859
|
+
import { randomUUID as randomUUID17 } from "crypto";
|
|
15729
16860
|
var ProjectMembershipServiceError = class extends Error {
|
|
15730
16861
|
constructor(code, statusCode, message) {
|
|
15731
16862
|
super(message);
|
|
@@ -15741,7 +16872,7 @@ function isMembershipConflict(error) {
|
|
|
15741
16872
|
var ProjectMembershipService = class {
|
|
15742
16873
|
constructor(deps) {
|
|
15743
16874
|
this.deps = deps;
|
|
15744
|
-
this.idFactory = deps.idFactory ??
|
|
16875
|
+
this.idFactory = deps.idFactory ?? randomUUID17;
|
|
15745
16876
|
this.mutationMutex = deps.projectMutationMutex ?? projectMutationMutex;
|
|
15746
16877
|
}
|
|
15747
16878
|
async list(actor3) {
|
|
@@ -15875,7 +17006,7 @@ var ProjectMembershipService = class {
|
|
|
15875
17006
|
};
|
|
15876
17007
|
|
|
15877
17008
|
// src/services/collaboration/ProjectRosterService.ts
|
|
15878
|
-
import { randomUUID as
|
|
17009
|
+
import { randomUUID as randomUUID18 } from "crypto";
|
|
15879
17010
|
var ProjectRosterServiceError = class extends Error {
|
|
15880
17011
|
/** Creates a stable roster service error. */
|
|
15881
17012
|
constructor(code, statusCode, message) {
|
|
@@ -15894,7 +17025,7 @@ var ProjectRosterService = class {
|
|
|
15894
17025
|
/** Creates a roster service from explicitly injected collaborators. */
|
|
15895
17026
|
constructor(deps) {
|
|
15896
17027
|
this.deps = deps;
|
|
15897
|
-
this.idFactory = deps.idFactory ??
|
|
17028
|
+
this.idFactory = deps.idFactory ?? randomUUID18;
|
|
15898
17029
|
this.mutationMutex = deps.projectMutationMutex ?? projectMutationMutex;
|
|
15899
17030
|
}
|
|
15900
17031
|
/**
|
|
@@ -16160,7 +17291,7 @@ function publicProjectBot(project, roomId, agent, now) {
|
|
|
16160
17291
|
}
|
|
16161
17292
|
|
|
16162
17293
|
// src/services/collaboration/RoomMessageService.ts
|
|
16163
|
-
import { createHash as createHash6, randomUUID as
|
|
17294
|
+
import { createHash as createHash6, randomUUID as randomUUID19 } from "crypto";
|
|
16164
17295
|
|
|
16165
17296
|
// src/services/collaboration/ProjectBotMembershipSnapshot.ts
|
|
16166
17297
|
import { snapshotExactRecord as snapshotExactRecord5 } from "@axiom-lattice/protocols";
|
|
@@ -16249,7 +17380,7 @@ var RoomMessageService = class {
|
|
|
16249
17380
|
constructor(deps, options = {}) {
|
|
16250
17381
|
this.deps = deps;
|
|
16251
17382
|
this.idempotentOutcomes = /* @__PURE__ */ new Map();
|
|
16252
|
-
this.idFactory = deps.idFactory ??
|
|
17383
|
+
this.idFactory = deps.idFactory ?? randomUUID19;
|
|
16253
17384
|
const limit = options.idempotentOutcomeLimit ?? 1e3;
|
|
16254
17385
|
if (!Number.isSafeInteger(limit) || limit < 1) throw new RangeError("idempotentOutcomeLimit must be a positive safe integer");
|
|
16255
17386
|
this.idempotentOutcomeLimit = limit;
|
|
@@ -16498,11 +17629,11 @@ function matchesNormalizedPayload(message, input) {
|
|
|
16498
17629
|
}
|
|
16499
17630
|
|
|
16500
17631
|
// src/routes/project-rooms.ts
|
|
16501
|
-
import { ZodError as
|
|
17632
|
+
import { ZodError as ZodError4 } from "zod";
|
|
16502
17633
|
import { toProjectRoomPublicMessage } from "@axiom-lattice/protocols";
|
|
16503
17634
|
|
|
16504
17635
|
// src/services/collaboration/RoomInterruptService.ts
|
|
16505
|
-
import { randomUUID as
|
|
17636
|
+
import { randomUUID as randomUUID20 } from "crypto";
|
|
16506
17637
|
var RoomInterruptServiceError = class extends Error {
|
|
16507
17638
|
/** Creates a stable room interrupt error. */
|
|
16508
17639
|
constructor(code, statusCode, message) {
|
|
@@ -16516,7 +17647,7 @@ var RoomInterruptService = class {
|
|
|
16516
17647
|
/** Creates the service from explicitly injected stores and dispatcher. */
|
|
16517
17648
|
constructor(deps) {
|
|
16518
17649
|
this.deps = deps;
|
|
16519
|
-
this.idFactory = deps.idFactory ??
|
|
17650
|
+
this.idFactory = deps.idFactory ?? randomUUID20;
|
|
16520
17651
|
}
|
|
16521
17652
|
/** Resolves a pending clarify interrupt with no answers. */
|
|
16522
17653
|
async skip(input) {
|
|
@@ -16574,52 +17705,52 @@ var RoomInterruptService = class {
|
|
|
16574
17705
|
};
|
|
16575
17706
|
|
|
16576
17707
|
// src/schemas/project-room.ts
|
|
16577
|
-
import { z as
|
|
16578
|
-
var id2 =
|
|
16579
|
-
var isoDate =
|
|
16580
|
-
var expectedUpdatedAt =
|
|
16581
|
-
var projectRoomParamsSchema =
|
|
16582
|
-
var membershipParamsSchema =
|
|
16583
|
-
var roomMessagesQuerySchema =
|
|
16584
|
-
beforeCreatedAt:
|
|
17708
|
+
import { z as z10 } from "zod";
|
|
17709
|
+
var id2 = z10.string().trim().min(1).max(128);
|
|
17710
|
+
var isoDate = z10.string().datetime({ offset: true }).transform((value) => new Date(value));
|
|
17711
|
+
var expectedUpdatedAt = z10.object({ expectedUpdatedAt: isoDate }).strict();
|
|
17712
|
+
var projectRoomParamsSchema = z10.object({ projectId: id2 }).strict();
|
|
17713
|
+
var membershipParamsSchema = z10.object({ projectId: id2, membershipId: id2 }).strict();
|
|
17714
|
+
var roomMessagesQuerySchema = z10.object({
|
|
17715
|
+
beforeCreatedAt: z10.string().datetime({ offset: true }).transform((value) => new Date(value)).optional(),
|
|
16585
17716
|
beforeId: id2.optional(),
|
|
16586
|
-
limit:
|
|
17717
|
+
limit: z10.coerce.number().int().min(1).max(100).optional()
|
|
16587
17718
|
}).strict().refine((value) => value.beforeCreatedAt === void 0 === (value.beforeId === void 0), { message: "Both cursor fields are required" });
|
|
16588
|
-
var createRoomMessageSchema =
|
|
16589
|
-
text:
|
|
16590
|
-
mentions:
|
|
16591
|
-
|
|
16592
|
-
|
|
17719
|
+
var createRoomMessageSchema = z10.object({
|
|
17720
|
+
text: z10.string(),
|
|
17721
|
+
mentions: z10.array(z10.union([
|
|
17722
|
+
z10.object({ type: z10.literal("team") }).strict(),
|
|
17723
|
+
z10.object({ type: z10.literal("bot"), membershipId: id2 }).strict()
|
|
16593
17724
|
])).optional(),
|
|
16594
17725
|
replyToMessageId: id2.optional()
|
|
16595
17726
|
}).strict();
|
|
16596
|
-
var addProjectMemberSchema =
|
|
16597
|
-
var updateProjectMemberSchema =
|
|
17727
|
+
var addProjectMemberSchema = z10.object({ userId: id2, role: z10.enum(["owner", "admin", "member", "viewer"]) }).strict();
|
|
17728
|
+
var updateProjectMemberSchema = z10.object({ role: z10.enum(["owner", "admin", "member", "viewer"]), expectedUpdatedAt: isoDate }).strict();
|
|
16598
17729
|
var mutationExpectedUpdatedAtSchema = expectedUpdatedAt;
|
|
16599
|
-
var roomMessageRetryParamsSchema =
|
|
16600
|
-
var emptyObjectSchema =
|
|
16601
|
-
var idempotencyKeySchema =
|
|
16602
|
-
var addProjectBotSchema =
|
|
17730
|
+
var roomMessageRetryParamsSchema = z10.object({ projectId: id2, messageId: id2 }).strict();
|
|
17731
|
+
var emptyObjectSchema = z10.object({}).strict();
|
|
17732
|
+
var idempotencyKeySchema = z10.string().refine((value) => value.trim().length >= 1 && value.trim().length <= 200 && /^[\x20-\x7e]+$/.test(value.trim()), "Invalid idempotency key").transform((value) => value.trim());
|
|
17733
|
+
var addProjectBotSchema = z10.object({
|
|
16603
17734
|
assistantId: id2,
|
|
16604
|
-
role:
|
|
16605
|
-
title:
|
|
16606
|
-
responsibility:
|
|
16607
|
-
mentionName:
|
|
17735
|
+
role: z10.enum(["coordinator", "specialist"]),
|
|
17736
|
+
title: z10.string(),
|
|
17737
|
+
responsibility: z10.string().optional(),
|
|
17738
|
+
mentionName: z10.string()
|
|
16608
17739
|
}).strict();
|
|
16609
|
-
var updateProjectBotSchema =
|
|
16610
|
-
role:
|
|
16611
|
-
title:
|
|
16612
|
-
responsibility:
|
|
16613
|
-
mentionName:
|
|
17740
|
+
var updateProjectBotSchema = z10.object({
|
|
17741
|
+
role: z10.enum(["coordinator", "specialist"]).optional(),
|
|
17742
|
+
title: z10.string().optional(),
|
|
17743
|
+
responsibility: z10.string().optional(),
|
|
17744
|
+
mentionName: z10.string().optional(),
|
|
16614
17745
|
expectedUpdatedAt: isoDate
|
|
16615
17746
|
}).strict().refine((value) => value.role !== void 0 || value.title !== void 0 || value.responsibility !== void 0 || value.mentionName !== void 0, { message: "At least one mutable field is required" });
|
|
16616
|
-
var resumeInterruptSchema =
|
|
17747
|
+
var resumeInterruptSchema = z10.object({
|
|
16617
17748
|
membershipId: id2,
|
|
16618
|
-
answers:
|
|
16619
|
-
questionIndex:
|
|
16620
|
-
otherText:
|
|
16621
|
-
selectedOptions:
|
|
16622
|
-
filePath:
|
|
17749
|
+
answers: z10.array(z10.object({
|
|
17750
|
+
questionIndex: z10.number().int().nonnegative(),
|
|
17751
|
+
otherText: z10.string().optional(),
|
|
17752
|
+
selectedOptions: z10.array(z10.string()).optional(),
|
|
17753
|
+
filePath: z10.string().optional()
|
|
16623
17754
|
}).strict()).optional()
|
|
16624
17755
|
}).strict();
|
|
16625
17756
|
|
|
@@ -16717,7 +17848,7 @@ async function run(reply, operation) {
|
|
|
16717
17848
|
const result = await operation();
|
|
16718
17849
|
return reply.code(result.statusCode ?? 200).send({ success: true, data: result.data });
|
|
16719
17850
|
} catch (error) {
|
|
16720
|
-
if (error instanceof
|
|
17851
|
+
if (error instanceof ZodError4) return reply.code(400).send({ success: false, error: { code: "INVALID_REQUEST", message: "Invalid request", retryable: false } });
|
|
16721
17852
|
if (error instanceof RoomInterruptServiceError) return reply.code(error.statusCode).send({ success: false, error: { code: error.code, message: error.message, retryable: false } });
|
|
16722
17853
|
if (error instanceof ProjectRoomAuthError || error instanceof ProjectCollaborationError || error instanceof ProjectMembershipServiceError || error instanceof ProjectRosterServiceError || error instanceof RoomMessageServiceError) return reply.code(error.statusCode).send({ success: false, error: { code: error.code, message: error.message, retryable: error instanceof RoomMessageServiceError && error.retryable } });
|
|
16723
17854
|
return reply.code(500).send({ success: false, error: { code: "INTERNAL_ERROR", message: "Internal server error", retryable: false } });
|
|
@@ -16725,7 +17856,7 @@ async function run(reply, operation) {
|
|
|
16725
17856
|
}
|
|
16726
17857
|
|
|
16727
17858
|
// src/routes/project-roster.ts
|
|
16728
|
-
import { ZodError as
|
|
17859
|
+
import { ZodError as ZodError5 } from "zod";
|
|
16729
17860
|
var ProjectRosterAuthError = class extends Error {
|
|
16730
17861
|
constructor() {
|
|
16731
17862
|
super(...arguments);
|
|
@@ -16765,15 +17896,15 @@ async function execute(reply, operation) {
|
|
|
16765
17896
|
const result = await operation();
|
|
16766
17897
|
return reply.code(result.statusCode ?? 200).send({ success: true, data: result.data });
|
|
16767
17898
|
} catch (error) {
|
|
16768
|
-
if (error instanceof
|
|
17899
|
+
if (error instanceof ZodError5) return reply.code(400).send({ success: false, error: { code: "INVALID_REQUEST", message: "Invalid request", retryable: false } });
|
|
16769
17900
|
if (error instanceof ProjectRosterAuthError || error instanceof ProjectCollaborationError || error instanceof ProjectRosterServiceError) return reply.code(error.statusCode).send({ success: false, error: { code: error.code, message: error.message, retryable: false } });
|
|
16770
17901
|
return reply.code(500).send({ success: false, error: { code: "INTERNAL_ERROR", message: "Internal server error", retryable: false } });
|
|
16771
17902
|
}
|
|
16772
17903
|
}
|
|
16773
17904
|
|
|
16774
17905
|
// src/routes/cospace.ts
|
|
16775
|
-
import { randomUUID as
|
|
16776
|
-
import { ZodError as
|
|
17906
|
+
import { randomUUID as randomUUID21 } from "crypto";
|
|
17907
|
+
import { ZodError as ZodError6 } from "zod";
|
|
16777
17908
|
import {
|
|
16778
17909
|
generateDirectHumanProjectId,
|
|
16779
17910
|
generateDirectBotProjectId
|
|
@@ -16806,7 +17937,7 @@ async function computeUnread(deps, tenantId, userId, roomId, lastMessage) {
|
|
|
16806
17937
|
});
|
|
16807
17938
|
}
|
|
16808
17939
|
function registerCoSpaceRoutes(app2, deps) {
|
|
16809
|
-
const idFactory = deps.idFactory ?? (() =>
|
|
17940
|
+
const idFactory = deps.idFactory ?? (() => randomUUID21());
|
|
16810
17941
|
app2.get(
|
|
16811
17942
|
"/api/workspaces/:workspaceId/rooms",
|
|
16812
17943
|
async (request, reply) => {
|
|
@@ -17327,7 +18458,7 @@ function registerCoSpaceRoutes(app2, deps) {
|
|
|
17327
18458
|
data: result.data
|
|
17328
18459
|
});
|
|
17329
18460
|
} catch (error) {
|
|
17330
|
-
if (error instanceof
|
|
18461
|
+
if (error instanceof ZodError6) {
|
|
17331
18462
|
return reply.code(400).send({
|
|
17332
18463
|
success: false,
|
|
17333
18464
|
error: {
|
|
@@ -17360,9 +18491,9 @@ function registerCoSpaceRoutes(app2, deps) {
|
|
|
17360
18491
|
}
|
|
17361
18492
|
|
|
17362
18493
|
// src/routes/push-notifications.ts
|
|
17363
|
-
import { ZodError as
|
|
17364
|
-
var subscriptionSchema =
|
|
17365
|
-
var deleteSchema =
|
|
18494
|
+
import { ZodError as ZodError7, z as z11 } from "zod";
|
|
18495
|
+
var subscriptionSchema = z11.object({ endpoint: z11.string().min(1), keys: z11.object({ p256dh: z11.string().min(1), auth: z11.string().min(1) }).strict() }).strict();
|
|
18496
|
+
var deleteSchema = z11.object({ endpoint: z11.string().min(1) }).strict();
|
|
17366
18497
|
function actor2(request) {
|
|
17367
18498
|
const user = request.user;
|
|
17368
18499
|
return user?.id && user.tenantId ? { tenantId: user.tenantId, userId: user.id } : null;
|
|
@@ -17390,7 +18521,7 @@ function registerPushNotificationRoutes(app2, deps) {
|
|
|
17390
18521
|
});
|
|
17391
18522
|
return reply.code(204).send();
|
|
17392
18523
|
} catch (error) {
|
|
17393
|
-
if (error instanceof
|
|
18524
|
+
if (error instanceof ZodError7) {
|
|
17394
18525
|
return reply.code(400).send({ success: false, error: { code: "INVALID_REQUEST", message: "Invalid request", retryable: false } });
|
|
17395
18526
|
}
|
|
17396
18527
|
return reply.code(500).send({ success: false, error: { code: "INTERNAL_ERROR", message: "Internal server error", retryable: false } });
|
|
@@ -17404,7 +18535,7 @@ function registerPushNotificationRoutes(app2, deps) {
|
|
|
17404
18535
|
await deps.subscriptionStore.delete(current.tenantId, current.userId, body.endpoint);
|
|
17405
18536
|
return reply.code(204).send();
|
|
17406
18537
|
} catch (error) {
|
|
17407
|
-
if (error instanceof
|
|
18538
|
+
if (error instanceof ZodError7) {
|
|
17408
18539
|
return reply.code(400).send({ success: false, error: { code: "INVALID_REQUEST", message: "Invalid request", retryable: false } });
|
|
17409
18540
|
}
|
|
17410
18541
|
return reply.code(500).send({ success: false, error: { code: "INTERNAL_ERROR", message: "Internal server error", retryable: false } });
|
|
@@ -17762,7 +18893,7 @@ function exactValues(value, required, optional = []) {
|
|
|
17762
18893
|
}
|
|
17763
18894
|
|
|
17764
18895
|
// src/services/collaboration/CoordinatorWakeNotifier.ts
|
|
17765
|
-
import { randomUUID as
|
|
18896
|
+
import { randomUUID as randomUUID22 } from "crypto";
|
|
17766
18897
|
import {
|
|
17767
18898
|
snapshotExactRecord as snapshotExactRecord7
|
|
17768
18899
|
} from "@axiom-lattice/protocols";
|
|
@@ -17778,7 +18909,7 @@ var CoordinatorWakeNotifier = class {
|
|
|
17778
18909
|
this.accepted = /* @__PURE__ */ new Set();
|
|
17779
18910
|
this.maxPending = deps.maxPending ?? 1024;
|
|
17780
18911
|
if (!Number.isSafeInteger(this.maxPending) || this.maxPending < 1) throw new RangeError("maxPending must be a positive safe integer");
|
|
17781
|
-
this.idFactory = deps.idFactory ??
|
|
18912
|
+
this.idFactory = deps.idFactory ?? randomUUID22;
|
|
17782
18913
|
}
|
|
17783
18914
|
/** Admits one notification without awaiting any store or dispatch operation. */
|
|
17784
18915
|
handle(value, anchorMessageId) {
|
|
@@ -19380,7 +20511,7 @@ function isRecord3(value) {
|
|
|
19380
20511
|
import { taskLifecycleNotifier } from "@axiom-lattice/core";
|
|
19381
20512
|
|
|
19382
20513
|
// src/services/collaboration/ProjectRoomEventBroker.ts
|
|
19383
|
-
import { randomUUID as
|
|
20514
|
+
import { randomUUID as randomUUID23 } from "crypto";
|
|
19384
20515
|
import {
|
|
19385
20516
|
parseProjectRoomEventId,
|
|
19386
20517
|
ProjectRoomBrokerCapacityError,
|
|
@@ -19409,7 +20540,7 @@ var ProjectRoomEventBroker = class {
|
|
|
19409
20540
|
this.subscriberCount = 0;
|
|
19410
20541
|
this.usageClock = 0;
|
|
19411
20542
|
this.closed = false;
|
|
19412
|
-
this.epoch = options.epoch ??
|
|
20543
|
+
this.epoch = options.epoch ?? randomUUID23();
|
|
19413
20544
|
if (!UUID_PATTERN.test(this.epoch)) {
|
|
19414
20545
|
throw new TypeError("Project Room event epoch must be a lowercase canonical UUID");
|
|
19415
20546
|
}
|
|
@@ -21146,7 +22277,7 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
|
|
|
21146
22277
|
});
|
|
21147
22278
|
}
|
|
21148
22279
|
if (projectTaskComposition) app2.addHook("onClose", async () => projectTaskComposition.lease.release());
|
|
21149
|
-
const taskThreadService = projectTaskComposition?.lease.runtime.threads ?? new ProjectTaskThreadService({ threads: threadStore, idFactory:
|
|
22280
|
+
const taskThreadService = projectTaskComposition?.lease.runtime.threads ?? new ProjectTaskThreadService({ threads: threadStore, idFactory: randomUUID24 });
|
|
21150
22281
|
const taskLifecycle = projectTaskComposition?.lease.runtime.lifecycle ?? createTaskLifecycleService({ taskStore, workItemStore: taskWorkItemStore });
|
|
21151
22282
|
const reassignmentAuthorizer = createProjectTaskReassignmentAuthorizer(
|
|
21152
22283
|
new ProjectTaskOwnerPolicy(projectBotMembershipStore),
|
|
@@ -21255,6 +22386,16 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
|
|
|
21255
22386
|
bundleStore: getStoreLattice17("default", "capabilityBundle").store,
|
|
21256
22387
|
projectStore
|
|
21257
22388
|
}, { authRequired: process.env.AUTH_REQUIRED === "true" }));
|
|
22389
|
+
registerModelProviderRoutes(app2, new ModelProviderController({
|
|
22390
|
+
providerStore: getStoreLattice17("default", "modelProvider").store,
|
|
22391
|
+
userTenantLinkStore,
|
|
22392
|
+
registry: {
|
|
22393
|
+
register: (tenantId, modelKey2, config) => modelLatticeManager3.registerLatticeWithTenant(tenantId, modelKey2, config),
|
|
22394
|
+
has: (tenantId, modelKey2) => modelLatticeManager3.hasLatticeWithTenant(tenantId, modelKey2),
|
|
22395
|
+
remove: (tenantId, modelKey2) => modelLatticeManager3.removeLatticeWithTenant(tenantId, modelKey2),
|
|
22396
|
+
listRegistered: (tenantId) => modelLatticeManager3.getLatticeKeysByTenant(tenantId)
|
|
22397
|
+
}
|
|
22398
|
+
}, { authRequired: process.env.AUTH_REQUIRED === "true" }));
|
|
21258
22399
|
registerAgentWebAppRoutes(app2, {
|
|
21259
22400
|
service: new AgentWebAppService({
|
|
21260
22401
|
webAppStore,
|
|
@@ -21759,7 +22900,7 @@ function tryCreateProjectTaskRuntime(input) {
|
|
|
21759
22900
|
function createRuntime(input, stores, log2) {
|
|
21760
22901
|
const controller = new AbortController();
|
|
21761
22902
|
const mutex = new ProjectTaskExecutionMutex();
|
|
21762
|
-
const threads = new ProjectTaskThreadService({ threads: input.threadStore, idFactory:
|
|
22903
|
+
const threads = new ProjectTaskThreadService({ threads: input.threadStore, idFactory: randomUUID24 });
|
|
21763
22904
|
const lifecycle = createTaskLifecycleService({ taskStore: input.taskStore, workItemStore: input.taskWorkItemStore });
|
|
21764
22905
|
const projection = new RoomProjectionService({
|
|
21765
22906
|
messages: input.projectRoomMessageStore,
|
|
@@ -21768,7 +22909,7 @@ function createRuntime(input, stores, log2) {
|
|
|
21768
22909
|
warnings: input.taskWorkItemStore,
|
|
21769
22910
|
log: log2,
|
|
21770
22911
|
realtime: input.realtime,
|
|
21771
|
-
idFactory:
|
|
22912
|
+
idFactory: randomUUID24
|
|
21772
22913
|
});
|
|
21773
22914
|
const agentFor = (task, thread) => agentInstanceManager6.getAgent({
|
|
21774
22915
|
tenant_id: task.tenantId,
|
|
@@ -22595,7 +23736,7 @@ var collectionRegistration = {
|
|
|
22595
23736
|
|
|
22596
23737
|
// src/export_registrations/channel-installation.registration.ts
|
|
22597
23738
|
import { getStoreLattice as getStoreLattice24 } from "@axiom-lattice/core";
|
|
22598
|
-
import { randomUUID as
|
|
23739
|
+
import { randomUUID as randomUUID25 } from "crypto";
|
|
22599
23740
|
var UUID_PATTERN2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
22600
23741
|
var PUBLIC_CHANNELS = /* @__PURE__ */ new Set(["lark", "email", "slack", "wechat"]);
|
|
22601
23742
|
function isPublicChannel(channel) {
|
|
@@ -22754,7 +23895,7 @@ var channelInstallationRegistration = {
|
|
|
22754
23895
|
}
|
|
22755
23896
|
id3 = sourceId;
|
|
22756
23897
|
} else {
|
|
22757
|
-
id3 =
|
|
23898
|
+
id3 = randomUUID25();
|
|
22758
23899
|
}
|
|
22759
23900
|
await store.createInstallation(tenantId, id3, createInput);
|
|
22760
23901
|
return { newId: id3 };
|
|
@@ -23268,13 +24409,13 @@ function createRateLimitMiddleware(maxRequests = 10, windowMs = 60 * 1e3, maxEnt
|
|
|
23268
24409
|
}
|
|
23269
24410
|
|
|
23270
24411
|
// src/router/middlewares/auditLogger.ts
|
|
23271
|
-
var
|
|
24412
|
+
var logger5 = new Logger({ serviceName: "lattice/gateway/audit" });
|
|
23272
24413
|
function createAuditLoggerMiddleware() {
|
|
23273
24414
|
return async (ctx, next) => {
|
|
23274
24415
|
const start2 = Date.now();
|
|
23275
24416
|
try {
|
|
23276
24417
|
await next();
|
|
23277
|
-
|
|
24418
|
+
logger5.info("message routed", {
|
|
23278
24419
|
event: "message:routed",
|
|
23279
24420
|
channel: ctx.inboundMessage.channel,
|
|
23280
24421
|
senderId: ctx.inboundMessage.sender.id,
|
|
@@ -23284,7 +24425,7 @@ function createAuditLoggerMiddleware() {
|
|
|
23284
24425
|
status: "success"
|
|
23285
24426
|
});
|
|
23286
24427
|
} catch (error) {
|
|
23287
|
-
|
|
24428
|
+
logger5.error(
|
|
23288
24429
|
error instanceof Error ? error.message : String(error),
|
|
23289
24430
|
{
|
|
23290
24431
|
event: "message:error",
|
|
@@ -23365,6 +24506,7 @@ var configureSwagger = async (app2, customSwaggerConfig, customSwaggerUiConfig)
|
|
|
23365
24506
|
};
|
|
23366
24507
|
|
|
23367
24508
|
// src/services/agent_task_consumer.ts
|
|
24509
|
+
import { randomUUID as randomUUID26 } from "crypto";
|
|
23368
24510
|
import { eventBus as eventBus3, AGENT_TASK_EVENT, agentInstanceManager as agentInstanceManager7, QueueMode as QueueMode2 } from "@axiom-lattice/core";
|
|
23369
24511
|
var handleAgentTask = async (taskRequest, retryCount = 0) => {
|
|
23370
24512
|
const {
|
|
@@ -23384,50 +24526,96 @@ var handleAgentTask = async (taskRequest, retryCount = 0) => {
|
|
|
23384
24526
|
`\u5F00\u59CB\u5904\u7406\u4EFB\u52A1 [assistant_id: ${assistant_id}, thread_id: ${thread_id}]`
|
|
23385
24527
|
);
|
|
23386
24528
|
const agent = agentInstanceManager7.getAgent({ assistant_id, thread_id, tenant_id, workspace_id: runConfig?.workspaceId, project_id: runConfig?.projectId, custom_run_config: runConfig });
|
|
23387
|
-
await agent.addMessage({ input, command, custom_run_config: runConfig }, QueueMode2.STEER);
|
|
23388
24529
|
if (callback_event) {
|
|
23389
|
-
|
|
23390
|
-
|
|
23391
|
-
|
|
23392
|
-
|
|
23393
|
-
|
|
23394
|
-
|
|
23395
|
-
|
|
23396
|
-
|
|
23397
|
-
|
|
23398
|
-
|
|
23399
|
-
|
|
23400
|
-
|
|
23401
|
-
|
|
23402
|
-
|
|
23403
|
-
|
|
23404
|
-
|
|
23405
|
-
|
|
23406
|
-
|
|
23407
|
-
|
|
23408
|
-
|
|
23409
|
-
|
|
23410
|
-
|
|
24530
|
+
const callbackEvent = callback_event;
|
|
24531
|
+
const messageId = typeof input?.id === "string" && input.id !== "" ? input.id : randomUUID26();
|
|
24532
|
+
let settled = false;
|
|
24533
|
+
const subscriptions = [];
|
|
24534
|
+
const dispose = () => {
|
|
24535
|
+
for (const [eventName, handler] of subscriptions) {
|
|
24536
|
+
agent.unsubscribe(eventName, handler);
|
|
24537
|
+
}
|
|
24538
|
+
};
|
|
24539
|
+
const settle = (publish) => {
|
|
24540
|
+
if (settled) return;
|
|
24541
|
+
settled = true;
|
|
24542
|
+
dispose();
|
|
24543
|
+
publish();
|
|
24544
|
+
};
|
|
24545
|
+
const belongsToMessage = (evt) => evt?.messageId === messageId;
|
|
24546
|
+
const onCompleted = (evt) => {
|
|
24547
|
+
if (!belongsToMessage(evt)) return;
|
|
24548
|
+
settle(() => {
|
|
24549
|
+
eventBus3.publish(callbackEvent, {
|
|
24550
|
+
success: true,
|
|
24551
|
+
state: evt.state
|
|
24552
|
+
});
|
|
24553
|
+
if (main_thread_id && main_tenant_id) {
|
|
24554
|
+
try {
|
|
24555
|
+
const mainAgent = agentInstanceManager7.getAgent({
|
|
24556
|
+
assistant_id: main_assistant_id ?? assistant_id,
|
|
24557
|
+
thread_id: main_thread_id,
|
|
24558
|
+
tenant_id: main_tenant_id,
|
|
24559
|
+
workspace_id: runConfig?.workspaceId,
|
|
24560
|
+
project_id: runConfig?.projectId
|
|
24561
|
+
});
|
|
24562
|
+
if (mainAgent) {
|
|
24563
|
+
const messages = evt.state?.values?.messages;
|
|
24564
|
+
const lastAIMessage = messages?.filter((m) => m.type === "ai" || m.getType?.() === "ai").pop();
|
|
24565
|
+
const summary = lastAIMessage?.content?.substring(0, 500) ?? "(no output)";
|
|
24566
|
+
mainAgent.addMessage(
|
|
24567
|
+
{
|
|
24568
|
+
input: {
|
|
24569
|
+
message: `[Async task completed]
|
|
23411
24570
|
task_id: ${thread_id}
|
|
23412
24571
|
${summary}`
|
|
24572
|
+
}
|
|
23413
24573
|
}
|
|
23414
|
-
|
|
23415
|
-
|
|
23416
|
-
|
|
23417
|
-
|
|
23418
|
-
|
|
24574
|
+
).catch((err) => {
|
|
24575
|
+
console.error("Failed to notify main thread:", err);
|
|
24576
|
+
});
|
|
24577
|
+
mainAgent.updateAsyncTaskStatus(thread_id, "completed");
|
|
24578
|
+
}
|
|
24579
|
+
} catch (err) {
|
|
24580
|
+
console.error("Failed to notify main thread:", err);
|
|
23419
24581
|
}
|
|
23420
|
-
} catch (err) {
|
|
23421
|
-
console.error("Failed to notify main thread:", err);
|
|
23422
24582
|
}
|
|
23423
|
-
}
|
|
23424
|
-
});
|
|
23425
|
-
agent.subscribeOnce("message:interrupted", (evt) => {
|
|
23426
|
-
eventBus3.publish(callback_event, {
|
|
23427
|
-
success: true,
|
|
23428
|
-
state: evt.state
|
|
23429
24583
|
});
|
|
23430
|
-
}
|
|
24584
|
+
};
|
|
24585
|
+
const onInterrupted = (evt) => {
|
|
24586
|
+
if (!belongsToMessage(evt)) return;
|
|
24587
|
+
settle(() => {
|
|
24588
|
+
eventBus3.publish(callbackEvent, {
|
|
24589
|
+
success: true,
|
|
24590
|
+
state: evt.state
|
|
24591
|
+
});
|
|
24592
|
+
});
|
|
24593
|
+
};
|
|
24594
|
+
const onFailed = (evt) => {
|
|
24595
|
+
if (!belongsToMessage(evt)) return;
|
|
24596
|
+
settle(() => {
|
|
24597
|
+
eventBus3.publish(callbackEvent, {
|
|
24598
|
+
success: false,
|
|
24599
|
+
error: evt?.error instanceof Error ? evt.error.message : typeof evt?.error === "string" ? evt.error : "Agent execution failed"
|
|
24600
|
+
});
|
|
24601
|
+
});
|
|
24602
|
+
};
|
|
24603
|
+
subscriptions.push(
|
|
24604
|
+
["message:completed", onCompleted],
|
|
24605
|
+
["message:interrupted", onInterrupted],
|
|
24606
|
+
["message:failed", onFailed]
|
|
24607
|
+
);
|
|
24608
|
+
agent.subscribe("message:completed", onCompleted);
|
|
24609
|
+
agent.subscribe("message:interrupted", onInterrupted);
|
|
24610
|
+
agent.subscribe("message:failed", onFailed);
|
|
24611
|
+
try {
|
|
24612
|
+
await agent.addMessage({ input: { ...input, id: messageId }, command, custom_run_config: runConfig }, QueueMode2.STEER);
|
|
24613
|
+
} catch (error) {
|
|
24614
|
+
dispose();
|
|
24615
|
+
throw error;
|
|
24616
|
+
}
|
|
24617
|
+
} else {
|
|
24618
|
+
await agent.addMessage({ input, command, custom_run_config: runConfig }, QueueMode2.STEER);
|
|
23431
24619
|
}
|
|
23432
24620
|
return true;
|
|
23433
24621
|
} catch (error) {
|
|
@@ -23643,7 +24831,7 @@ import {
|
|
|
23643
24831
|
snapshotExactArray as snapshotExactArray3,
|
|
23644
24832
|
snapshotExactRecord as snapshotExactRecord15
|
|
23645
24833
|
} from "@axiom-lattice/protocols";
|
|
23646
|
-
import { randomUUID as
|
|
24834
|
+
import { randomUUID as randomUUID27 } from "crypto";
|
|
23647
24835
|
var BindingNotFoundError = class extends Error {
|
|
23648
24836
|
constructor(message) {
|
|
23649
24837
|
super(message);
|
|
@@ -23965,7 +25153,7 @@ var MessageRouter = class {
|
|
|
23965
25153
|
}
|
|
23966
25154
|
if (!threadId) {
|
|
23967
25155
|
const threadStore = getStoreLattice27("default", "thread").store;
|
|
23968
|
-
const newThreadId =
|
|
25156
|
+
const newThreadId = randomUUID27();
|
|
23969
25157
|
console.log({
|
|
23970
25158
|
event: "dispatch:thread:create",
|
|
23971
25159
|
agentId,
|
|
@@ -24011,7 +25199,7 @@ var MessageRouter = class {
|
|
|
24011
25199
|
workspace_id: binding?.workspaceId || "",
|
|
24012
25200
|
project_id: binding?.projectId || ""
|
|
24013
25201
|
});
|
|
24014
|
-
const inputMessageId =
|
|
25202
|
+
const inputMessageId = randomUUID27();
|
|
24015
25203
|
const replyTarget = internalRoom ? correlatedRoomReplyTarget(message.replyTarget, inputMessageId) : message.replyTarget;
|
|
24016
25204
|
const replyAdapter = replyTarget ? this.adapterRegistry.get(replyTarget.adapterChannel) : void 0;
|
|
24017
25205
|
if (options.trustedMessageOptions && (!this.trustedDispatcher || !replyTarget || !replyAdapter)) {
|
|
@@ -24191,9 +25379,9 @@ var MessageRouter = class {
|
|
|
24191
25379
|
};
|
|
24192
25380
|
|
|
24193
25381
|
// src/channels/room/RoomChannelAdapter.ts
|
|
24194
|
-
import { z as
|
|
25382
|
+
import { z as z12 } from "zod";
|
|
24195
25383
|
import { snapshotExactRecord as snapshotExactRecord16 } from "@axiom-lattice/protocols";
|
|
24196
|
-
var roomConfigSchema =
|
|
25384
|
+
var roomConfigSchema = z12.object({}).strict();
|
|
24197
25385
|
function createRoomChannelAdapter(writer) {
|
|
24198
25386
|
return {
|
|
24199
25387
|
channel: "room",
|
|
@@ -24312,7 +25500,7 @@ var DEFAULT_LOGGER_CONFIG = {
|
|
|
24312
25500
|
loggerName: "lattice/gateway"
|
|
24313
25501
|
};
|
|
24314
25502
|
var loggerLattice = initializeLogger(DEFAULT_LOGGER_CONFIG);
|
|
24315
|
-
var
|
|
25503
|
+
var logger6 = loggerLattice.client;
|
|
24316
25504
|
function initializeLogger(config) {
|
|
24317
25505
|
if (loggerLatticeManager.hasLattice("default")) {
|
|
24318
25506
|
loggerLatticeManager.removeLattice("default");
|
|
@@ -24399,7 +25587,7 @@ app.setErrorHandler((error, request, reply) => {
|
|
|
24399
25587
|
"x-request-id": getHeaderValue(request.headers["x-request-id"]),
|
|
24400
25588
|
"x-user-id": getHeaderValue(request.headers["x-user-id"])
|
|
24401
25589
|
};
|
|
24402
|
-
|
|
25590
|
+
logger6.error(
|
|
24403
25591
|
`\u8BF7\u6C42\u9519\u8BEF: ${request.method} ${request.url} error:${error.message}`,
|
|
24404
25592
|
{
|
|
24405
25593
|
...context,
|
|
@@ -24434,14 +25622,14 @@ async function restoreMcpConnections() {
|
|
|
24434
25622
|
const storeLattice = getStoreLattice28("default", "mcp");
|
|
24435
25623
|
const store = storeLattice.store;
|
|
24436
25624
|
if (!store) {
|
|
24437
|
-
|
|
25625
|
+
logger6.info("MCP store not configured, skipping connection restoration");
|
|
24438
25626
|
return;
|
|
24439
25627
|
}
|
|
24440
25628
|
const configs = (await store.getAllConfigsWithoutTenant()).filter(
|
|
24441
25629
|
(c2) => c2.status === "connected"
|
|
24442
25630
|
);
|
|
24443
25631
|
if (configs.length === 0) return;
|
|
24444
|
-
|
|
25632
|
+
logger6.info(`Restoring ${configs.length} MCP server connections...`);
|
|
24445
25633
|
for (const config of configs) {
|
|
24446
25634
|
try {
|
|
24447
25635
|
const connection = convertToConnection2(config.config);
|
|
@@ -24451,7 +25639,7 @@ async function restoreMcpConnections() {
|
|
|
24451
25639
|
selectedTools: config.selectedTools
|
|
24452
25640
|
});
|
|
24453
25641
|
} catch (err) {
|
|
24454
|
-
|
|
25642
|
+
logger6.warn(`Failed to prepare MCP server "${config.tenantId}/${config.key}"`, { error: err });
|
|
24455
25643
|
}
|
|
24456
25644
|
}
|
|
24457
25645
|
await mcpManager3.connectAll();
|
|
@@ -24467,15 +25655,15 @@ async function restoreMcpConnections() {
|
|
|
24467
25655
|
results.forEach((result, index) => {
|
|
24468
25656
|
if (result.status === "rejected") {
|
|
24469
25657
|
const config = configs[index];
|
|
24470
|
-
|
|
25658
|
+
logger6.warn(
|
|
24471
25659
|
`Failed to register tools for MCP server "${config.tenantId}/${config.key}"`,
|
|
24472
25660
|
{ error: result.reason }
|
|
24473
25661
|
);
|
|
24474
25662
|
}
|
|
24475
25663
|
});
|
|
24476
|
-
|
|
25664
|
+
logger6.info(`MCP connection restoration complete: ${configs.length} servers`);
|
|
24477
25665
|
} catch (err) {
|
|
24478
|
-
|
|
25666
|
+
logger6.error("MCP connection restoration failed", { error: err });
|
|
24479
25667
|
}
|
|
24480
25668
|
}
|
|
24481
25669
|
var start = async (config) => {
|
|
@@ -24488,7 +25676,7 @@ var start = async (config) => {
|
|
|
24488
25676
|
file: config.loggerConfig.file || DEFAULT_LOGGER_CONFIG.file
|
|
24489
25677
|
};
|
|
24490
25678
|
loggerLattice = initializeLogger(loggerConfig);
|
|
24491
|
-
|
|
25679
|
+
logger6 = loggerLattice.client;
|
|
24492
25680
|
}
|
|
24493
25681
|
app.decorate("loggerLattice", loggerLattice);
|
|
24494
25682
|
const projectRoomRealtimeMode = resolveProjectRoomRealtimeMode();
|
|
@@ -24498,7 +25686,7 @@ var start = async (config) => {
|
|
|
24498
25686
|
tasks: getStoreLattice28("default", "task").store,
|
|
24499
25687
|
rooms: getStoreLattice28("default", "projectRoom").store,
|
|
24500
25688
|
memberships: getStoreLattice28("default", "projectBotMembership").store,
|
|
24501
|
-
log: { warn: (message, context) =>
|
|
25689
|
+
log: { warn: (message, context) => logger6.warn(message, context) }
|
|
24502
25690
|
});
|
|
24503
25691
|
}
|
|
24504
25692
|
let channelDeps;
|
|
@@ -24517,7 +25705,7 @@ var start = async (config) => {
|
|
|
24517
25705
|
createRateLimitMiddleware(),
|
|
24518
25706
|
createAuditLoggerMiddleware()
|
|
24519
25707
|
],
|
|
24520
|
-
warn: (message, context) =>
|
|
25708
|
+
warn: (message, context) => logger6.warn(message, context),
|
|
24521
25709
|
realtime: projectRoomRealtimeRuntime?.publisher
|
|
24522
25710
|
});
|
|
24523
25711
|
const bindingStore = infrastructure.bindingRegistry;
|
|
@@ -24531,10 +25719,10 @@ var start = async (config) => {
|
|
|
24531
25719
|
installationStore,
|
|
24532
25720
|
bindingRegistry: bindingStore,
|
|
24533
25721
|
realtime: projectRoomRealtimeRuntime?.publisher,
|
|
24534
|
-
warn: (message, context) =>
|
|
25722
|
+
warn: (message, context) => logger6.warn(message, context)
|
|
24535
25723
|
};
|
|
24536
25724
|
} catch (err) {
|
|
24537
|
-
|
|
25725
|
+
logger6.warn("Channel infrastructure unavailable", {
|
|
24538
25726
|
error: err instanceof Error ? err.message : String(err)
|
|
24539
25727
|
});
|
|
24540
25728
|
}
|
|
@@ -24542,7 +25730,7 @@ var start = async (config) => {
|
|
|
24542
25730
|
try {
|
|
24543
25731
|
const menuStore = getStoreLattice28("default", "menu").store;
|
|
24544
25732
|
setMenuRegistry(menuStore);
|
|
24545
|
-
|
|
25733
|
+
logger6.info("Menu registry initialized");
|
|
24546
25734
|
} catch {
|
|
24547
25735
|
}
|
|
24548
25736
|
registerAllBuiltinEntities();
|
|
@@ -24551,7 +25739,7 @@ var start = async (config) => {
|
|
|
24551
25739
|
}
|
|
24552
25740
|
const routes = registerLatticeRoutes(app, channelDeps, {
|
|
24553
25741
|
projectTaskEnabled: process.env.AXIOM_PROJECT_TASK_EXECUTION_ENABLED !== "false",
|
|
24554
|
-
projectTaskLog: { warn: (message, context) =>
|
|
25742
|
+
projectTaskLog: { warn: (message, context) => logger6.warn(message, context) },
|
|
24555
25743
|
projectRoomRealtimeRuntime
|
|
24556
25744
|
});
|
|
24557
25745
|
try {
|
|
@@ -24579,9 +25767,9 @@ var start = async (config) => {
|
|
|
24579
25767
|
getProjectStore: () => projectStore
|
|
24580
25768
|
});
|
|
24581
25769
|
a2a.registerA2ARoutes(app);
|
|
24582
|
-
|
|
25770
|
+
logger6.info("A2A standard routes registered");
|
|
24583
25771
|
} catch (err) {
|
|
24584
|
-
|
|
25772
|
+
logger6.warn("A2A standard routes unavailable", {
|
|
24585
25773
|
error: err instanceof Error ? err.message : String(err)
|
|
24586
25774
|
});
|
|
24587
25775
|
}
|
|
@@ -24618,15 +25806,15 @@ var start = async (config) => {
|
|
|
24618
25806
|
registerOpenFilesRoutes(app);
|
|
24619
25807
|
registerOpenAuditRoutes(app);
|
|
24620
25808
|
registerOpenExposureRoutes(app);
|
|
24621
|
-
|
|
25809
|
+
logger6.info("Open API routes registered (kb + sandbox providers, session exchange)");
|
|
24622
25810
|
} catch (err) {
|
|
24623
|
-
|
|
25811
|
+
logger6.warn("Open API routes unavailable", {
|
|
24624
25812
|
error: err instanceof Error ? err.message : String(err)
|
|
24625
25813
|
});
|
|
24626
25814
|
}
|
|
24627
25815
|
if (!sandboxLatticeManager2.hasLattice("default")) {
|
|
24628
25816
|
sandboxLatticeManager2.registerLattice("default", getConfiguredSandboxProvider());
|
|
24629
|
-
|
|
25817
|
+
logger6.info("Registered sandbox manager from env configuration");
|
|
24630
25818
|
}
|
|
24631
25819
|
try {
|
|
24632
25820
|
const { ResourceController } = await import("./resources-DXPXAXEZ.mjs");
|
|
@@ -24636,12 +25824,12 @@ var start = async (config) => {
|
|
|
24636
25824
|
store: sharedResourceStore,
|
|
24637
25825
|
sandboxManager: sandboxLatticeManager2,
|
|
24638
25826
|
cache,
|
|
24639
|
-
logger:
|
|
25827
|
+
logger: logger6
|
|
24640
25828
|
});
|
|
24641
25829
|
registerResourceRoutes(app, resourceController);
|
|
24642
|
-
|
|
25830
|
+
logger6.info("Resource share routes registered");
|
|
24643
25831
|
} catch (err) {
|
|
24644
|
-
|
|
25832
|
+
logger6.warn("Resource share infrastructure unavailable", {
|
|
24645
25833
|
error: err instanceof Error ? err.message : String(err)
|
|
24646
25834
|
});
|
|
24647
25835
|
}
|
|
@@ -24653,17 +25841,17 @@ var start = async (config) => {
|
|
|
24653
25841
|
{ deps: { router: channelDeps.router } }
|
|
24654
25842
|
);
|
|
24655
25843
|
} catch (err) {
|
|
24656
|
-
|
|
25844
|
+
logger6.error("Failed to start channel connections", {
|
|
24657
25845
|
error: err instanceof Error ? err.message : String(err)
|
|
24658
25846
|
});
|
|
24659
25847
|
}
|
|
24660
25848
|
}
|
|
24661
25849
|
const target_port = config?.port || Number(process.env.PORT) || 4001;
|
|
24662
25850
|
if (routes.projectTaskRuntime) {
|
|
24663
|
-
await prepareProjectTaskRuntime(routes.projectTaskRuntime,
|
|
25851
|
+
await prepareProjectTaskRuntime(routes.projectTaskRuntime, logger6);
|
|
24664
25852
|
}
|
|
24665
25853
|
await app.listen({ port: target_port, host: "0.0.0.0" });
|
|
24666
|
-
|
|
25854
|
+
logger6.info(`Lattice Gateway is running on port: ${target_port}`);
|
|
24667
25855
|
const queueServiceConfig = config?.queueServiceConfig;
|
|
24668
25856
|
if (queueServiceConfig) {
|
|
24669
25857
|
setQueueServiceType(queueServiceConfig.type);
|
|
@@ -24674,16 +25862,16 @@ var start = async (config) => {
|
|
|
24674
25862
|
}
|
|
24675
25863
|
if (process.env.AXIOM_RESTORE_PENDING_ON_STARTUP === "true") {
|
|
24676
25864
|
agentInstanceManager9.restore().then((stats) => {
|
|
24677
|
-
|
|
25865
|
+
logger6.info(`Agent recovery complete: ${stats.restored} threads restored, ${stats.errors} errors`);
|
|
24678
25866
|
}).catch((error) => {
|
|
24679
|
-
|
|
25867
|
+
logger6.error("Agent recovery failed", { error });
|
|
24680
25868
|
});
|
|
24681
25869
|
}
|
|
24682
25870
|
restoreMcpConnections().catch((error) => {
|
|
24683
|
-
|
|
25871
|
+
logger6.error("MCP connection restoration failed", { error });
|
|
24684
25872
|
});
|
|
24685
25873
|
} catch (err) {
|
|
24686
|
-
|
|
25874
|
+
logger6.error("Server start failed", { error: err });
|
|
24687
25875
|
process.exit(1);
|
|
24688
25876
|
}
|
|
24689
25877
|
};
|