@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.
Files changed (23) hide show
  1. package/dist/embed/{embed-page-AgentDetailSideApp-I4ZT3MQQ.js → embed-page-AgentDetailSideApp-7NYLPLAX.js} +1 -1
  2. package/dist/embed/{embed-page-AgentGovernanceWorkbench-HVFB6EPY.js → embed-page-AgentGovernanceWorkbench-LNGNSI7X.js} +2 -2
  3. package/dist/embed/{embed-page-MessageGraph-CZQROEO7.js → embed-page-MessageGraph-E6DZDT65.js} +1 -1
  4. package/dist/embed/{embed-page-ReactChart-WMWBSL54.js → embed-page-ReactChart-3VE7F5VJ.js} +1 -1
  5. package/dist/embed/{embed-page-SkillFlow-5JLJ5R57.js → embed-page-SkillFlow-XMMHEHQL.js} +1 -1
  6. package/dist/embed/{embed-page-TasksSideApp-5BYTO4KT.js → embed-page-TasksSideApp-QM5TZ7EG.js} +1 -1
  7. package/dist/embed/{embed-page-TeamWorkspace-5KN7ZZWQ.js → embed-page-TeamWorkspace-E7RFY3ZY.js} +1 -1
  8. package/dist/embed/{embed-page-TopologyRuntimeView-S6XAQHDF.js → embed-page-TopologyRuntimeView-DLPDUB7Z.js} +1 -1
  9. package/dist/embed/{embed-page-WorkflowAutomationView-E6G446HW.js → embed-page-WorkflowAutomationView-FKSVOHHB.js} +1 -1
  10. package/dist/embed/embed-page-chunk-EXCJ42IB.js +2 -0
  11. package/dist/embed/{embed-page-chunk-MUE3GWJV.js → embed-page-chunk-H6TK3FAR.js} +1 -1
  12. package/dist/embed/{embed-page-chunk-QU52HUXW.js → embed-page-chunk-HEMNMC4X.js} +4 -4
  13. package/dist/embed/{embed-page-chunk-2JOGNXZT.js → embed-page-chunk-I7PWCKAG.js} +9 -9
  14. package/dist/embed/{embed-page-chunk-KDWA6VOG.js → embed-page-chunk-KN7D66KL.js} +1 -1
  15. package/dist/embed/{embed-page-chunk-7GAD6Q52.js → embed-page-chunk-U276WBDI.js} +2078 -2078
  16. package/dist/embed/{embed-page-chunk-YQ3BA27D.js → embed-page-chunk-WWTU55RX.js} +1 -1
  17. package/dist/embed/embed-page.js +1 -1
  18. package/dist/index.js +1830 -644
  19. package/dist/index.js.map +1 -1
  20. package/dist/index.mjs +1335 -147
  21. package/dist/index.mjs.map +1 -1
  22. package/package.json +7 -7
  23. 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 randomUUID23 } from "crypto";
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.getAllLattices();
1914
+ const allLattices = modelLatticeManager.getAllLatticesByTenant("default");
1915
1915
  const models = allLattices.map((lattice) => {
1916
- const config = lattice.client.config || {};
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
- apiKey: config.apiKey || "",
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: modelConfig.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/services/collaboration/ProjectRoomService.ts
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 ?? randomUUID15;
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 randomUUID16 } from "crypto";
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 ?? randomUUID16;
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 randomUUID17 } from "crypto";
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 ?? randomUUID17;
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 randomUUID18 } from "crypto";
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 ?? randomUUID18;
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 ZodError3 } from "zod";
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 randomUUID19 } from "crypto";
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 ?? randomUUID19;
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 z9 } from "zod";
16578
- var id2 = z9.string().trim().min(1).max(128);
16579
- var isoDate = z9.string().datetime({ offset: true }).transform((value) => new Date(value));
16580
- var expectedUpdatedAt = z9.object({ expectedUpdatedAt: isoDate }).strict();
16581
- var projectRoomParamsSchema = z9.object({ projectId: id2 }).strict();
16582
- var membershipParamsSchema = z9.object({ projectId: id2, membershipId: id2 }).strict();
16583
- var roomMessagesQuerySchema = z9.object({
16584
- beforeCreatedAt: z9.string().datetime({ offset: true }).transform((value) => new Date(value)).optional(),
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: z9.coerce.number().int().min(1).max(100).optional()
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 = z9.object({
16589
- text: z9.string(),
16590
- mentions: z9.array(z9.union([
16591
- z9.object({ type: z9.literal("team") }).strict(),
16592
- z9.object({ type: z9.literal("bot"), membershipId: id2 }).strict()
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 = z9.object({ userId: id2, role: z9.enum(["owner", "admin", "member", "viewer"]) }).strict();
16597
- var updateProjectMemberSchema = z9.object({ role: z9.enum(["owner", "admin", "member", "viewer"]), expectedUpdatedAt: isoDate }).strict();
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 = z9.object({ projectId: id2, messageId: id2 }).strict();
16600
- var emptyObjectSchema = z9.object({}).strict();
16601
- var idempotencyKeySchema = z9.string().refine((value) => value.trim().length >= 1 && value.trim().length <= 200 && /^[\x20-\x7e]+$/.test(value.trim()), "Invalid idempotency key").transform((value) => value.trim());
16602
- var addProjectBotSchema = z9.object({
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: z9.enum(["coordinator", "specialist"]),
16605
- title: z9.string(),
16606
- responsibility: z9.string().optional(),
16607
- mentionName: z9.string()
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 = z9.object({
16610
- role: z9.enum(["coordinator", "specialist"]).optional(),
16611
- title: z9.string().optional(),
16612
- responsibility: z9.string().optional(),
16613
- mentionName: z9.string().optional(),
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 = z9.object({
17747
+ var resumeInterruptSchema = z10.object({
16617
17748
  membershipId: id2,
16618
- answers: z9.array(z9.object({
16619
- questionIndex: z9.number().int().nonnegative(),
16620
- otherText: z9.string().optional(),
16621
- selectedOptions: z9.array(z9.string()).optional(),
16622
- filePath: z9.string().optional()
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 ZodError3) return reply.code(400).send({ success: false, error: { code: "INVALID_REQUEST", message: "Invalid request", retryable: false } });
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 ZodError4 } from "zod";
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 ZodError4) return reply.code(400).send({ success: false, error: { code: "INVALID_REQUEST", message: "Invalid request", retryable: false } });
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 randomUUID20 } from "crypto";
16776
- import { ZodError as ZodError5 } from "zod";
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 ?? (() => randomUUID20());
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 ZodError5) {
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 ZodError6, z as z10 } from "zod";
17364
- var subscriptionSchema = z10.object({ endpoint: z10.string().min(1), keys: z10.object({ p256dh: z10.string().min(1), auth: z10.string().min(1) }).strict() }).strict();
17365
- var deleteSchema = z10.object({ endpoint: z10.string().min(1) }).strict();
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 ZodError6) {
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 ZodError6) {
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 randomUUID21 } from "crypto";
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 ?? randomUUID21;
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 randomUUID22 } from "crypto";
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 ?? randomUUID22();
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: randomUUID23 });
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: randomUUID23 });
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: randomUUID23
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 randomUUID24 } from "crypto";
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 = randomUUID24();
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 logger4 = new Logger({ serviceName: "lattice/gateway/audit" });
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
- logger4.info("message routed", {
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
- logger4.error(
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
- agent.subscribeOnce("message:completed", (evt) => {
23390
- eventBus3.publish(callback_event, {
23391
- success: true,
23392
- state: evt.state
23393
- });
23394
- if (main_thread_id && main_tenant_id) {
23395
- try {
23396
- const mainAgent = agentInstanceManager7.getAgent({
23397
- assistant_id: main_assistant_id ?? assistant_id,
23398
- thread_id: main_thread_id,
23399
- tenant_id: main_tenant_id,
23400
- workspace_id: runConfig?.workspaceId,
23401
- project_id: runConfig?.projectId
23402
- });
23403
- if (mainAgent) {
23404
- const messages = evt.state?.values?.messages;
23405
- const lastAIMessage = messages?.filter((m) => m.type === "ai" || m.getType?.() === "ai").pop();
23406
- const summary = lastAIMessage?.content?.substring(0, 500) ?? "(no output)";
23407
- mainAgent.addMessage(
23408
- {
23409
- input: {
23410
- message: `[Async task completed]
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
- ).catch((err) => {
23416
- console.error("Failed to notify main thread:", err);
23417
- });
23418
- mainAgent.updateAsyncTaskStatus(thread_id, "completed");
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 randomUUID25 } from "crypto";
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 = randomUUID25();
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 = randomUUID25();
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 z11 } from "zod";
25382
+ import { z as z12 } from "zod";
24195
25383
  import { snapshotExactRecord as snapshotExactRecord16 } from "@axiom-lattice/protocols";
24196
- var roomConfigSchema = z11.object({}).strict();
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 logger5 = loggerLattice.client;
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
- logger5.error(
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
- logger5.info("MCP store not configured, skipping connection restoration");
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
- logger5.info(`Restoring ${configs.length} MCP server connections...`);
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
- logger5.warn(`Failed to prepare MCP server "${config.tenantId}/${config.key}"`, { error: err });
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
- logger5.warn(
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
- logger5.info(`MCP connection restoration complete: ${configs.length} servers`);
25664
+ logger6.info(`MCP connection restoration complete: ${configs.length} servers`);
24477
25665
  } catch (err) {
24478
- logger5.error("MCP connection restoration failed", { error: err });
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
- logger5 = loggerLattice.client;
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) => logger5.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) => logger5.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) => logger5.warn(message, context)
25722
+ warn: (message, context) => logger6.warn(message, context)
24535
25723
  };
24536
25724
  } catch (err) {
24537
- logger5.warn("Channel infrastructure unavailable", {
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
- logger5.info("Menu registry initialized");
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) => logger5.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
- logger5.info("A2A standard routes registered");
25770
+ logger6.info("A2A standard routes registered");
24583
25771
  } catch (err) {
24584
- logger5.warn("A2A standard routes unavailable", {
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
- logger5.info("Open API routes registered (kb + sandbox providers, session exchange)");
25809
+ logger6.info("Open API routes registered (kb + sandbox providers, session exchange)");
24622
25810
  } catch (err) {
24623
- logger5.warn("Open API routes unavailable", {
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
- logger5.info("Registered sandbox manager from env configuration");
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: logger5
25827
+ logger: logger6
24640
25828
  });
24641
25829
  registerResourceRoutes(app, resourceController);
24642
- logger5.info("Resource share routes registered");
25830
+ logger6.info("Resource share routes registered");
24643
25831
  } catch (err) {
24644
- logger5.warn("Resource share infrastructure unavailable", {
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
- logger5.error("Failed to start channel connections", {
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, logger5);
25851
+ await prepareProjectTaskRuntime(routes.projectTaskRuntime, logger6);
24664
25852
  }
24665
25853
  await app.listen({ port: target_port, host: "0.0.0.0" });
24666
- logger5.info(`Lattice Gateway is running on port: ${target_port}`);
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
- logger5.info(`Agent recovery complete: ${stats.restored} threads restored, ${stats.errors} errors`);
25865
+ logger6.info(`Agent recovery complete: ${stats.restored} threads restored, ${stats.errors} errors`);
24678
25866
  }).catch((error) => {
24679
- logger5.error("Agent recovery failed", { error });
25867
+ logger6.error("Agent recovery failed", { error });
24680
25868
  });
24681
25869
  }
24682
25870
  restoreMcpConnections().catch((error) => {
24683
- logger5.error("MCP connection restoration failed", { error });
25871
+ logger6.error("MCP connection restoration failed", { error });
24684
25872
  });
24685
25873
  } catch (err) {
24686
- logger5.error("Server start failed", { error: err });
25874
+ logger6.error("Server start failed", { error: err });
24687
25875
  process.exit(1);
24688
25876
  }
24689
25877
  };