nexus-ai-pro 2.11.0 → 2.13.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/API_STABILITY.md +69 -0
- package/CHANGELOG.md +185 -1
- package/README.md +14 -5
- package/dist/core/lifecycle.d.ts +3 -0
- package/dist/core/lifecycle.js +108 -5
- package/dist/core/nexus.d.ts +11 -1
- package/dist/core/nexus.js +54 -8
- package/dist/credentials/index.d.ts +179 -0
- package/dist/credentials/index.js +206 -0
- package/dist/embeddings/adapters.d.ts +3 -3
- package/dist/embeddings/adapters.js +16 -10
- package/dist/embeddings/engine.js +7 -2
- package/dist/embeddings/errors.d.ts +5 -1
- package/dist/embeddings/errors.js +3 -1
- package/dist/evaluate/automation.d.ts +256 -0
- package/dist/evaluate/automation.js +368 -0
- package/dist/evaluate/index.d.ts +2 -1
- package/dist/evaluate/index.js +1 -0
- package/dist/evaluate/media.d.ts +146 -0
- package/dist/evaluate/media.js +447 -0
- package/dist/evaluate/online.d.ts +25 -0
- package/dist/evaluate/online.js +29 -3
- package/dist/evaluate/trace.d.ts +21 -0
- package/dist/evaluate/trace.js +162 -0
- package/dist/images/google.js +6 -1
- package/dist/images/manager.js +3 -0
- package/dist/images/openai.js +4 -1
- package/dist/images/operation.js +1 -0
- package/dist/media/catalog.d.ts +239 -0
- package/dist/media/catalog.js +319 -0
- package/dist/providers/anthropic.js +11 -5
- package/dist/providers/azure-openai.js +1 -0
- package/dist/providers/cohere.d.ts +3 -2
- package/dist/providers/cohere.js +6 -4
- package/dist/providers/google.d.ts +3 -2
- package/dist/providers/google.js +9 -7
- package/dist/providers/openai.js +17 -7
- package/dist/residency/errors.d.ts +18 -0
- package/dist/residency/errors.js +18 -0
- package/dist/residency/index.d.ts +93 -0
- package/dist/residency/index.js +225 -0
- package/dist/router/failover.d.ts +13 -0
- package/dist/router/failover.js +26 -4
- package/dist/server/deployments.d.ts +109 -7
- package/dist/server/deployments.js +221 -47
- package/dist/server/http.js +3 -0
- package/dist/server/regions.d.ts +88 -0
- package/dist/server/regions.js +167 -0
- package/dist/server/runs.d.ts +26 -0
- package/dist/server/runs.js +59 -11
- package/dist/server/server.js +3 -2
- package/dist/tracing/index.d.ts +1 -1
- package/dist/tracing/index.js +1 -1
- package/dist/tracing/instrument.d.ts +16 -0
- package/dist/tracing/instrument.js +29 -0
- package/dist/tracing/otlp-grpc.d.ts +61 -0
- package/dist/tracing/otlp-grpc.js +229 -0
- package/dist/tracing/otlp-proto.d.ts +24 -0
- package/dist/tracing/otlp-proto.js +192 -0
- package/dist/tracing/otlp.d.ts +69 -10
- package/dist/tracing/otlp.js +65 -14
- package/dist/tracing/spool.d.ts +111 -0
- package/dist/tracing/spool.js +245 -0
- package/dist/tracing/tracer.d.ts +8 -0
- package/dist/tracing/tracer.js +8 -1
- package/dist/types/config.d.ts +5 -0
- package/dist/types/embeddings.d.ts +5 -0
- package/dist/types/evaluate.d.ts +106 -0
- package/dist/types/images.d.ts +5 -0
- package/dist/types/lifecycle.d.ts +71 -0
- package/dist/types/residency.d.ts +66 -0
- package/dist/types/residency.js +1 -0
- package/dist/types/server.d.ts +35 -0
- package/dist/utils/scrub.d.ts +12 -0
- package/dist/utils/scrub.js +49 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist-cjs/core/lifecycle.d.ts +3 -0
- package/dist-cjs/core/lifecycle.js +109 -5
- package/dist-cjs/core/nexus.d.ts +11 -1
- package/dist-cjs/core/nexus.js +53 -7
- package/dist-cjs/credentials/index.d.ts +179 -0
- package/dist-cjs/credentials/index.js +219 -0
- package/dist-cjs/embeddings/adapters.d.ts +3 -3
- package/dist-cjs/embeddings/adapters.js +16 -10
- package/dist-cjs/embeddings/engine.js +7 -2
- package/dist-cjs/embeddings/errors.d.ts +5 -1
- package/dist-cjs/embeddings/errors.js +3 -1
- package/dist-cjs/evaluate/automation.d.ts +256 -0
- package/dist-cjs/evaluate/automation.js +377 -0
- package/dist-cjs/evaluate/index.d.ts +2 -1
- package/dist-cjs/evaluate/index.js +4 -1
- package/dist-cjs/evaluate/media.d.ts +146 -0
- package/dist-cjs/evaluate/media.js +453 -0
- package/dist-cjs/evaluate/online.d.ts +25 -0
- package/dist-cjs/evaluate/online.js +29 -3
- package/dist-cjs/evaluate/trace.d.ts +21 -0
- package/dist-cjs/evaluate/trace.js +167 -0
- package/dist-cjs/images/google.js +6 -1
- package/dist-cjs/images/manager.js +3 -0
- package/dist-cjs/images/openai.js +4 -1
- package/dist-cjs/images/operation.js +1 -0
- package/dist-cjs/media/catalog.d.ts +239 -0
- package/dist-cjs/media/catalog.js +323 -0
- package/dist-cjs/providers/anthropic.js +11 -5
- package/dist-cjs/providers/azure-openai.js +1 -0
- package/dist-cjs/providers/cohere.d.ts +3 -2
- package/dist-cjs/providers/cohere.js +6 -4
- package/dist-cjs/providers/google.d.ts +3 -2
- package/dist-cjs/providers/google.js +9 -7
- package/dist-cjs/providers/openai.js +17 -7
- package/dist-cjs/residency/errors.d.ts +18 -0
- package/dist-cjs/residency/errors.js +22 -0
- package/dist-cjs/residency/index.d.ts +93 -0
- package/dist-cjs/residency/index.js +232 -0
- package/dist-cjs/router/failover.d.ts +13 -0
- package/dist-cjs/router/failover.js +26 -4
- package/dist-cjs/server/deployments.d.ts +109 -7
- package/dist-cjs/server/deployments.js +221 -47
- package/dist-cjs/server/http.js +3 -0
- package/dist-cjs/server/regions.d.ts +88 -0
- package/dist-cjs/server/regions.js +170 -0
- package/dist-cjs/server/runs.d.ts +26 -0
- package/dist-cjs/server/runs.js +59 -11
- package/dist-cjs/server/server.js +3 -2
- package/dist-cjs/tracing/index.d.ts +1 -1
- package/dist-cjs/tracing/index.js +2 -1
- package/dist-cjs/tracing/instrument.d.ts +16 -0
- package/dist-cjs/tracing/instrument.js +30 -0
- package/dist-cjs/tracing/otlp-grpc.d.ts +61 -0
- package/dist-cjs/tracing/otlp-grpc.js +237 -0
- package/dist-cjs/tracing/otlp-proto.d.ts +24 -0
- package/dist-cjs/tracing/otlp-proto.js +196 -0
- package/dist-cjs/tracing/otlp.d.ts +69 -10
- package/dist-cjs/tracing/otlp.js +69 -15
- package/dist-cjs/tracing/spool.d.ts +111 -0
- package/dist-cjs/tracing/spool.js +248 -0
- package/dist-cjs/tracing/tracer.d.ts +8 -0
- package/dist-cjs/tracing/tracer.js +8 -1
- package/dist-cjs/types/config.d.ts +5 -0
- package/dist-cjs/types/embeddings.d.ts +5 -0
- package/dist-cjs/types/evaluate.d.ts +106 -0
- package/dist-cjs/types/images.d.ts +5 -0
- package/dist-cjs/types/lifecycle.d.ts +71 -0
- package/dist-cjs/types/residency.d.ts +66 -0
- package/dist-cjs/types/residency.js +2 -0
- package/dist-cjs/types/server.d.ts +35 -0
- package/dist-cjs/utils/scrub.d.ts +12 -0
- package/dist-cjs/utils/scrub.js +53 -0
- package/dist-cjs/version.d.ts +1 -1
- package/dist-cjs/version.js +1 -1
- package/llms.txt +2 -1
- package/package.json +105 -1
package/API_STABILITY.md
CHANGED
|
@@ -41,6 +41,75 @@ Both are covered by this policy:
|
|
|
41
41
|
Deep imports into `dist`, `dist-cjs`, or `src` are not supported. Import only from `nexus-ai-pro` or
|
|
42
42
|
one of its explicit subpaths.
|
|
43
43
|
|
|
44
|
+
## 2.13 stage (2.13.0)
|
|
45
|
+
|
|
46
|
+
Everything 2.13 adds is stable from the start and additive, unless this section says otherwise.
|
|
47
|
+
|
|
48
|
+
New and stable:
|
|
49
|
+
- `nexus-ai-pro/server/regions`: `regionalGateway()`, `RegionalGateway`, `RegionalGatewayOptions`,
|
|
50
|
+
`RegionTarget`, `RegionHealth`, and `RegionHealthOptions`;
|
|
51
|
+
- `RunManagerOptions.region` and `residency`, as `AgentServerOptions` carries them; `ThreadRecord.region`,
|
|
52
|
+
`RunRecord.region`, and `ReplicaReport.region`; a regional server's ids start with its region;
|
|
53
|
+
- `nexus-ai-pro/residency`: `residency()`, `Residency`, `ResidencyOptions`, `RegionRule`,
|
|
54
|
+
`RegionDefinition`, `ResidencyPolicy`, `ResidencyDecision`, `ResidencyTarget`, `ResidencyPlacement`,
|
|
55
|
+
`CallResidency`, `ResidencyError`, `regionalTraceStore()`, `RegionalTraceStore`,
|
|
56
|
+
`RegionalTraceStoreOptions`, `regionalAssetStore()`, and `RegionalAssetStoreOptions`;
|
|
57
|
+
- `LifecycleConfig.residency`, `ProviderCallContext.residency`, and `ImageRequestBase.tenantId`;
|
|
58
|
+
- `nexus-ai-pro/credentials`: `tenantCredentials()`, `TenantCredentials`, `TenantCredentialsOptions`,
|
|
59
|
+
`TenantCredentialStats`, `CredentialError`, `parseSecret()`, `credentialRequest()`, `SecretSource`,
|
|
60
|
+
`awsSecrets()`, `gcpSecrets()`, `azureSecrets()`, `vaultSecrets()`, `envSecrets()`,
|
|
61
|
+
`memorySecrets()`, `MemorySecrets`, `AwsSecretsClient`, `GcpSecretsClient`, `AzureSecretsClient`,
|
|
62
|
+
and `VaultSecretsOptions`;
|
|
63
|
+
- `LifecycleConfig.credentials`, `CredentialResolver`, `CredentialRequest`, and `ProviderCredential`;
|
|
64
|
+
`ProviderCallContext.tenantId` and `credential`; `OperationTicket.refreshCredential()` and
|
|
65
|
+
`scrub()`;
|
|
66
|
+
- `OpenAIProviderConfig.credentialHeader`, `EmbeddingRequest.tenantId`, and
|
|
67
|
+
`EmbeddingProviderError.attempts`.
|
|
68
|
+
|
|
69
|
+
## 2.12 stage (2.12.0)
|
|
70
|
+
|
|
71
|
+
Everything 2.12 adds is stable from the start and additive, unless this section says otherwise.
|
|
72
|
+
|
|
73
|
+
New and stable:
|
|
74
|
+
- `nexus-ai-pro/tracing/spool`: `spoolingExporter()`, `SpoolingExporter`, `SpoolingExporterOptions`,
|
|
75
|
+
`TraceBatchSender`, `SpoolStats`, and `SpoolDrainReport`;
|
|
76
|
+
- `OtlpTraceExporter.send()`;
|
|
77
|
+
- `RunManagerOptions.tracer` and `AgentServerOptions.tracer`, and `RunRecord.traceId`;
|
|
78
|
+
- `Deployments.evaluationWorker()` and `DeploymentEvaluationWorkerOptions`; `RunEvaluationOptions`
|
|
79
|
+
gains `traces`, `operations`, `pollMs`, and the worker's options, and `RunEvaluationReport` gains
|
|
80
|
+
`failed`;
|
|
81
|
+
- in `nexus-ai-pro/evaluate`: `traceEvidence()`, `mediaParts()`, `EvaluationTrace`, `MediaPart`,
|
|
82
|
+
`TraceToolCall`, `TraceRetrieval`, `TraceDocument`, `TraceModelCall`, `TraceMedia`, and
|
|
83
|
+
`EvaluationContext.trace`;
|
|
84
|
+
- `OnlineEvaluationWorkerOptions.withTrace`, `onScored`, and `onError`, and `OnlineScored`;
|
|
85
|
+
- `traceRetriever()`, `RetrieverLike`, and `Tracer.within()`;
|
|
86
|
+
- `nexus-ai-pro/evaluate/automation`: `automationRules()`, `deploymentScores()`, `traceScores()`,
|
|
87
|
+
`memoryScores()`, `openReview()`, `sendWebhook()`, `markUnhealthy()`, `AutomationRule`,
|
|
88
|
+
`AutomationRules`, `AutomationRulesOptions`, `AutomationEvent`, `AutomationAction`,
|
|
89
|
+
`AutomationStatus`, `AutomationWebhookOptions`, `ScoreSample`, `ScoreSource`, `MemoryScoreSource`,
|
|
90
|
+
`ReviewOpener`, and `RuleStatistic`;
|
|
91
|
+
- `Deployments.markUnhealthy()`, `markHealthy()`, and `scores()`, `RunScore`, the `health`
|
|
92
|
+
`DeploymentChange`, `DeploymentRecord.unhealthy`, `RevisionHealthMark`,
|
|
93
|
+
`DeploymentChangeRecord.revision` and `healthy`, and `CanaryDecision.unhealthy`;
|
|
94
|
+
- `nexus-ai-pro/evaluate/media`: `mediaChecks()`, `mediaJudge()`, `mediaOf()`, `resolveMedia()`,
|
|
95
|
+
`MediaCheckOptions`, `MediaJudgeOptions`, `MediaResolveOptions`, `MediaSelection`, `MediaRange`,
|
|
96
|
+
`ResolvedMedia`, and `MediaSubjectKind`;
|
|
97
|
+
- `nexus-ai-pro/media/catalog`: `mediaCatalog()`, `catalogedAssetStore()`, `MediaCatalog`,
|
|
98
|
+
`MediaCatalogOptions`, `MediaCatalogEntry`, `MediaCatalogQuery`, `MediaCatalogPage`,
|
|
99
|
+
`MediaCatalogStats`, `MediaCatalogRecordOptions`, `CatalogScore`, `CatalogMediaKind`, and
|
|
100
|
+
`CatalogedAssetStoreOptions`;
|
|
101
|
+
- `nexus-ai-pro/tracing/otlp-grpc`: `OtlpGrpcTraceExporter`, `OtlpGrpcTraceExporterOptions`, and
|
|
102
|
+
`OtlpGrpcError`;
|
|
103
|
+
- in `nexus-ai-pro/tracing/otlp`: `OtlpTraceExporterOptions.protocol`, `encodeOtlpTraceRequest()`,
|
|
104
|
+
`decodeOtlpTraceResponse()`, `OtlpExportRequest`, `OtlpKeyValue`, `OtlpAttributeValue`,
|
|
105
|
+
`OtlpPartialSuccess`, and `OtlpPartialSuccessError`.
|
|
106
|
+
|
|
107
|
+
Changed: `DeploymentChange` gains a `health` action, so a `switch` over its actions that must be
|
|
108
|
+
exhaustive needs a case for it.
|
|
109
|
+
|
|
110
|
+
Changed: `Deployments.evaluate()` runs through the online-evaluation worker, scoring up to 4 runs at
|
|
111
|
+
once rather than one, so evaluators that are not safe to call concurrently need `concurrency: 1`.
|
|
112
|
+
|
|
44
113
|
## 2.11 stage (2.11.0)
|
|
45
114
|
|
|
46
115
|
Everything 2.11 adds is stable from the start and additive, unless this section says otherwise.
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,188 @@ Notable changes to this project are recorded here. The format follows [Keep a Ch
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.13.0] - 2026-10-19
|
|
8
|
+
|
|
9
|
+
Regions and residency, and credentials per tenant.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Agent servers across regions.** `regionalGateway()`, on the new `nexus-ai-pro/server/regions`, sits
|
|
14
|
+
in front of one agent server per region. A thread or run goes to the region its id names and waits
|
|
15
|
+
there while that region is down, since its data is there and nowhere else; new work goes to the
|
|
16
|
+
tenant's home region and fails over only to another region the tenant may reach; a tenant that may
|
|
17
|
+
reach no region served is refused with a 403 naming the rule. Regions are checked through their
|
|
18
|
+
`/health`, marked down by failed requests, and taken out by an operator with `markDown()`. An agent
|
|
19
|
+
server given `region` stamps it on its threads, runs, ids, replica report, and health, and, given
|
|
20
|
+
`residency`, refuses tenants that may not reach it.
|
|
21
|
+
- **Residency: each tenant's requests and data kept in its regions.** `residency()`, on the new
|
|
22
|
+
`nexus-ai-pro/residency`, says which regions each tenant may reach (a map, or a function over the
|
|
23
|
+
tenant's record), which providers run in each region, and each provider's twin inside a region.
|
|
24
|
+
`LifecycleConfig.residency` applies it to every family: a completion or stream, its fallbacks and
|
|
25
|
+
its context summary included, is sent to the provider routing chose when that runs inside the
|
|
26
|
+
tenant's regions and to its twin there when not; an embedding never falls back outside them; and a
|
|
27
|
+
provider anything would reach outside them is refused with a `ResidencyError` naming the rule,
|
|
28
|
+
before anything is sent. `regionalTraceStore()` and `regionalAssetStore()` write each tenant's
|
|
29
|
+
traces, every run of a trace together, and assets in its home region, and read them only from its
|
|
30
|
+
regions.
|
|
31
|
+
- **Credentials per tenant.** `LifecycleConfig.credentials` resolves each provider's key per tenant,
|
|
32
|
+
when a call is made, in every family that runs through the lifecycle: completions, streams,
|
|
33
|
+
embeddings, and images send it in place of the configured key. `tenantCredentials()`, on the new
|
|
34
|
+
`nexus-ai-pro/credentials`, reads it from a secret source, cached for `cacheMs`, shared by
|
|
35
|
+
concurrent calls, carried by the last key while the source is down for up to `staleMs`, and
|
|
36
|
+
refusing a tenant with no key unless told to fall back. Sources for AWS Secrets Manager, Google
|
|
37
|
+
Secret Manager, Azure Key Vault, HashiCorp Vault, the environment, and memory, each over the
|
|
38
|
+
vendor's own client with no dependency.
|
|
39
|
+
- A completion or stream whose provider refuses its credential is resolved again and made once more
|
|
40
|
+
on the new key; in any other family, the next operation reads the key again. Every key and header
|
|
41
|
+
value an operation resolves is removed from the errors it raises, before an audit event, a log,
|
|
42
|
+
a trace, or the health report sees them.
|
|
43
|
+
- `ProviderCallContext.residency`, the operation's `CallResidency`; `ImageRequestBase.tenantId`; and
|
|
44
|
+
an agent server's trace root records its run's tenant as `metadata.nexus.tenant`.
|
|
45
|
+
- `ProviderCallContext.tenantId` and `credential()`; `OperationTicket.refreshCredential()` and
|
|
46
|
+
`scrub()`; `OpenAIProviderConfig.credentialHeader`; `EmbeddingRequest.tenantId`; and
|
|
47
|
+
`EmbeddingProviderError.attempts`, each attempt's error.
|
|
48
|
+
|
|
49
|
+
### Proof
|
|
50
|
+
|
|
51
|
+
- `tests/regions.test.ts`: the proof that closes 2.13. Three regions, each an agent server with its own
|
|
52
|
+
state, traces, and provider, behind a gateway, with every tenant's key from one secret manager. A run
|
|
53
|
+
of four model calls has its key rotated and revoked after the second; the third and fourth are
|
|
54
|
+
answered on the new key. Then the EU tenant's home region goes down: its new work fails over to the
|
|
55
|
+
other EU region, its thread waits for its own, a tenant held to that region waits too, and nothing
|
|
56
|
+
of either reaches the US: no thread, run, trace, or provider call. Without region ids, the server's
|
|
57
|
+
check, a thread's region kept, new work held to the tenant's regions, failover, checks, the
|
|
58
|
+
operator's mark, or the gateway's own refusal, a test fails.
|
|
59
|
+
- `tests/residency.test.ts`: against a provider server per region, a tenant pinned to the EU is
|
|
60
|
+
answered by the EU twin of the provider routing chose, completions and streams alike, and nothing
|
|
61
|
+
of it reaches the US; a provider with no twin there is refused with the rule, nothing sent; an
|
|
62
|
+
embedding whose EU provider fails never falls back to the US; an image provider outside the regions
|
|
63
|
+
is refused; and each tenant's traces and assets land in its home region and are read only from its
|
|
64
|
+
regions. Without placement, twins, the decision, the routed check, the fallback filter, the image
|
|
65
|
+
check, traces kept together, the tenant view, the asset home, or the rule checks, a test fails.
|
|
66
|
+
- `tests/tenant-credentials.test.ts`: against a provider of the test's own that takes only live keys,
|
|
67
|
+
each tenant's calls carry its own key through completions, streams, and embeddings; in a run of five
|
|
68
|
+
calls whose key is rotated and revoked after the second, the third is answered on the new key and
|
|
69
|
+
every call after uses it; a key the provider quotes in its refusal reaches no error, stack, cause,
|
|
70
|
+
audit event, log, trace, or health report. Without the call's credential, the refresh, its limit,
|
|
71
|
+
either scrub, an adapter's use of the key, the cache's expiry, its single read, its stale key, or
|
|
72
|
+
its refusal, a test fails.
|
|
73
|
+
|
|
74
|
+
## [2.12.0] - 2026-10-18
|
|
75
|
+
|
|
76
|
+
Observability that loses nothing, and media you can evaluate.
|
|
77
|
+
|
|
78
|
+
### Added
|
|
79
|
+
|
|
80
|
+
- **Spans that a collector being down cannot lose.** `spoolingExporter()`, on the new
|
|
81
|
+
`nexus-ai-pro/tracing/spool`, keeps each batch the collector refuses in an operation store (the
|
|
82
|
+
memory, Redis, Postgres, or SQLite one), retries it with backoff, and dead-letters it only once it is
|
|
83
|
+
too old or has failed too often, under a bound on what waits. Every replica on the store drains the
|
|
84
|
+
same spool, each batch once. `writeAhead` spools each batch before sending it. `stats()` reports
|
|
85
|
+
what was exported, retried, spooled, dropped, and dead-lettered, and the age of the oldest batch
|
|
86
|
+
waiting.
|
|
87
|
+
- `OtlpTraceExporter.send()`: sends runs as one batch now, and rejects when the collector refuses
|
|
88
|
+
them.
|
|
89
|
+
- **A deployment judged by what its runs did.** An agent server given a `tracer` makes each run the
|
|
90
|
+
root of a trace, with its input and output, and everything the assistant traces through the same
|
|
91
|
+
tracer beneath it; the run records the trace as `RunRecord.traceId`. `Deployments.evaluate()`
|
|
92
|
+
given those `traces` hands each evaluator the run's input and an `EvaluationTrace` of its tool
|
|
93
|
+
calls, retrievals, model calls, and media, read by the new `traceEvidence()`, which `mediaParts()`
|
|
94
|
+
backs. An assistant that traces itself is found by the run id it records.
|
|
95
|
+
- `Deployments.evaluationWorker()`: the online-evaluation worker over a deployment's runs, to start
|
|
96
|
+
on every replica, scoring each run once between them as it finishes.
|
|
97
|
+
- `onlineEvaluationWorker()` takes `withTrace`, which gives evaluators `context.trace`, and
|
|
98
|
+
`onScored`, which hears each run's scores once they are written, with `onError` for its failures.
|
|
99
|
+
- `traceRetriever()`: every retrieval as a `retriever` run, with the query and the documents.
|
|
100
|
+
- **Media you can evaluate.** On the new `nexus-ai-pro/evaluate/media`, `mediaChecks()` reads the
|
|
101
|
+
media an evaluation is about, from bytes, base64, a data URL, a URL it is allowed to fetch, a path,
|
|
102
|
+
or an asset store, and scores each check asked: the count, kind, MIME type, size, dimensions,
|
|
103
|
+
aspect ratio, and a WAV's duration. `mediaJudge()` shows them to a vision or audio model beside a
|
|
104
|
+
rubric and the brief, a stored asset sent as its bytes, and records the judge in an experiment.
|
|
105
|
+
`mediaOf()` finds the media in an output, or in what the run's trace says it produced, and
|
|
106
|
+
`resolveMedia()` reads one.
|
|
107
|
+
- **Automation rules.** On the new `nexus-ai-pro/evaluate/automation`, `automationRules()` judges
|
|
108
|
+
a score over each revision's most recent runs, and when it falls below a threshold over enough
|
|
109
|
+
samples (optionally by the upper bound of a confidence interval), acts once, across every replica
|
|
110
|
+
sharing its `state`: `openReview()` sends the worst runs to people, `sendWebhook()` posts a signed
|
|
111
|
+
event, and `markUnhealthy()` marks the revision unhealthy, which rolls a canary back. Rules read
|
|
112
|
+
scores a deployment's run records hold (`deploymentScores()`), a trace store's
|
|
113
|
+
(`traceScores()`), or this process's (`memoryScores()`), and `observe()` judges them as an online
|
|
114
|
+
evaluation writes them. A rule fires once per fall, recovers, and can repeat.
|
|
115
|
+
- **A revision's health.** `Deployments.markUnhealthy()` and `markHealthy()`: a `health` change in
|
|
116
|
+
the deployment's history, kept in `DeploymentRecord.unhealthy`. A canary marked unhealthy is rolled
|
|
117
|
+
back at once, and a canary guard rolls back any canary marked since it began, its `CanaryDecision`
|
|
118
|
+
carrying the mark. The studio shows each revision's health and marks one from the deployment view.
|
|
119
|
+
`Deployments.scores()` reads the scores of an assistant's runs.
|
|
120
|
+
- **A catalog of every stored asset.** `mediaCatalog()`, on the new `nexus-ai-pro/media/catalog`,
|
|
121
|
+
keeps one record of each stored asset in any long-term store (Postgres, SQLite, Redis, or memory):
|
|
122
|
+
what it is, who made it, for which run, its tags, and its latest score per key, written by
|
|
123
|
+
compare-and-set. `catalogedAssetStore()` wraps any asset store so every asset stored through it is
|
|
124
|
+
recorded and every one deleted is forgotten, an entry expiring with its asset. `list()` finds
|
|
125
|
+
entries by kind, type, provider, model, run, store, tag, time, and score, a page at a time;
|
|
126
|
+
`attach()` scores the assets a run's output refers to; `stats()`, `purgeExpired()`, and
|
|
127
|
+
`reconcile()` keep account.
|
|
128
|
+
- **OTLP over gRPC.** `OtlpGrpcTraceExporter`, on the new `nexus-ai-pro/tracing/otlp-grpc`, sends
|
|
129
|
+
spans to a collector's port 4317 with no gRPC library: the trace service's `Export` call framed by
|
|
130
|
+
hand over Node's HTTP/2 client, gzip-compressed, with metadata, a deadline, TLS options, one
|
|
131
|
+
connection kept and reopened, and a failed call rejecting with an `OtlpGrpcError` so a spool keeps
|
|
132
|
+
the batch. `OtlpTraceExporter` takes `protocol: 'http/protobuf'`, and both read a collector's
|
|
133
|
+
partial success into an `OtlpPartialSuccessError` for `onError`. `encodeOtlpTraceRequest()` and
|
|
134
|
+
`decodeOtlpTraceResponse()` are OTLP's protobuf encoding, written by hand, on any runtime.
|
|
135
|
+
- `Tracer.within()`: runs a function with a run started by `startRun()` in scope.
|
|
136
|
+
|
|
137
|
+
### Changed
|
|
138
|
+
|
|
139
|
+
- `Deployments.evaluate()` runs through the online-evaluation worker, drained once, instead of beside
|
|
140
|
+
it. Replicas given one `operations` store score each run once between them, even calling at the
|
|
141
|
+
same moment, where before two calls could score a run twice; it takes the worker's budget, review
|
|
142
|
+
queue, and concurrency (4 at a time by default, one before), and `RunEvaluationReport` counts
|
|
143
|
+
`failed` evaluations. Which runs a `sampleRate` picks is unchanged.
|
|
144
|
+
|
|
145
|
+
### Proof
|
|
146
|
+
|
|
147
|
+
- `tests/trace-spool.test.ts`: a collector stopped for an hour while a span finishes every second,
|
|
148
|
+
and the process restarted halfway: every one of the 3,600 spans reaches it, once, on the memory,
|
|
149
|
+
SQLite, Postgres, and Redis operation stores. The bound, dead letters, two replicas on one spool,
|
|
150
|
+
and write-ahead through a process that dies mid-send. Without the spool, its bound, its claim, its
|
|
151
|
+
dead letters, write-ahead, or taking over a lapsed lease, a test fails.
|
|
152
|
+
- `tests/media-evaluation.test.ts`: checks that read PNG, GIF, WAV, and PDF media from a store,
|
|
153
|
+
base64, a data URL, and a fetch only when allowed; a judge model shown a stored asset as its bytes
|
|
154
|
+
beside the rubric and the brief; media found through the trace; and an experiment over a target
|
|
155
|
+
that draws. Without the sniffing, the store, the wildcard, the tolerance, the duration, the size
|
|
156
|
+
limit, the count, the media shown, the brief, or the trace fallback, a test fails.
|
|
157
|
+
- `tests/automation-rules.test.ts`: the proof that a canary is rolled back on a media evaluator's
|
|
158
|
+
score. A designer assistant's canary draws its banners square; two replicas score 80 runs with a
|
|
159
|
+
media judge shown each banner, and a rule on the judge's score marks the canary unhealthy, rolling
|
|
160
|
+
it back, opening review items for the five worst banners, and sending one signed webhook, once
|
|
161
|
+
between the replicas. The live revision is untouched, new runs go to it, and the media catalog
|
|
162
|
+
finds each of the canary's banners by its score. Then minimum samples, scopes, windows, the
|
|
163
|
+
confidence bound, the pass rate, recovery, repeats, filters, sources, failing actions, and a guard
|
|
164
|
+
rolling back a canary a rule only marked. Without the rollback, the guard's mark, the stale-mark
|
|
165
|
+
check, the claim, the minimum, the window, recovery, the bound, counting each run once, the
|
|
166
|
+
revision scope, the signature, or the repeat, a test fails.
|
|
167
|
+
- `tests/media-catalog.test.ts`: on the memory, SQLite, Postgres, and Redis stores, forty assets of
|
|
168
|
+
five kinds, two providers, and two tenants, some deleted and some expiring: every page lists each
|
|
169
|
+
remaining asset once, every filter finds exactly its assets, expired ones are never read and are
|
|
170
|
+
purged with their assets, and ten evaluators scoring one asset at once keep all ten scores. An
|
|
171
|
+
asset deleted around the catalog is reconciled away, and one the catalog cannot record is not left
|
|
172
|
+
stored. Without the forgetting, the expiry, the purge, compare-and-set, the cursor, the filters,
|
|
173
|
+
the compensation, or the reconciliation, a test fails.
|
|
174
|
+
- `tests/otlp-grpc.test.ts`: the OpenTelemetry Collector itself, in a container, takes spans over
|
|
175
|
+
gRPC and sends every one on as it came, ids, times, kinds, attributes, and status. Against a gRPC
|
|
176
|
+
server of the test's, with a protobuf reader written apart from the encoder: compression and
|
|
177
|
+
metadata, a refusal, a deadline, a partial success, a collector that is down, one that restarts
|
|
178
|
+
under a live connection, and a spool that keeps what is refused. Without the compressed flag, the
|
|
179
|
+
status, the deadline, the metadata, the parent span, the status message, typed values, the
|
|
180
|
+
reconnection, or the retry on a dropped connection, a test fails.
|
|
181
|
+
- `tests/deployment-trace-evaluation.test.ts`: a traced server whose assistant retrieves a policy,
|
|
182
|
+
calls a tool, and asks a model about a photo; its evaluators see the input, the tool call, the
|
|
183
|
+
documents, the model call, and the photo once. Two replicas evaluating at once over one operation
|
|
184
|
+
store score each of 40 runs once, where queues of their own score runs twice. A worker scores runs
|
|
185
|
+
as they finish and hears each score once, and an assistant tracing itself is found. Without the
|
|
186
|
+
trace, the input, the run scope, the shared queue, the fallback, the media deduplication, or the
|
|
187
|
+
trace written before the run settles, a test fails.
|
|
188
|
+
|
|
7
189
|
## [2.11.0] - 2026-10-18
|
|
8
190
|
|
|
9
191
|
Workspaces, and telephony in full.
|
|
@@ -2764,7 +2946,9 @@ shared policy vocabulary is in place for a future release that revisits this.
|
|
|
2764
2946
|
- URL fetching rejects unsafe private-network targets and limits response reads.
|
|
2765
2947
|
- CLI and audit findings no longer disclose detected secret values.
|
|
2766
2948
|
|
|
2767
|
-
[Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.
|
|
2949
|
+
[Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.13.0...HEAD
|
|
2950
|
+
[2.13.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.12.0...v2.13.0
|
|
2951
|
+
[2.12.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.11.0...v2.12.0
|
|
2768
2952
|
[2.11.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.10.0...v2.11.0
|
|
2769
2953
|
[2.10.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.9.0...v2.10.0
|
|
2770
2954
|
[2.9.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.8.0...v2.9.0
|
package/README.md
CHANGED
|
@@ -167,6 +167,7 @@ from the doc comments. The guides live in the repository, so these links go to G
|
|
|
167
167
|
| [Agent server](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/server.md) | Self-hosted HTTP server for assistants: threads, durable runs, resumable streams, cron, a worker queue, and metrics |
|
|
168
168
|
| [Deployments](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/deployments.md) | Revisions and canaries with automatic rollback, autoscaling on Kubernetes and Helm, and per-tenant limits |
|
|
169
169
|
| [Tenancy](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/tenancy.md) | One tenant model: a tenant's view of every store, tenant graph runs, and the isolation suite |
|
|
170
|
+
| [Regions and credentials](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/regions.md) | Agent servers across regions behind a gateway, residency rules that keep each tenant's requests, traces, and assets in its regions, and each tenant's provider keys from a secret manager |
|
|
170
171
|
| [Workflows](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/workflows.md) | Ready-made chains and domain workflows |
|
|
171
172
|
| [Command line](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/cli.md) | The `nexus` command |
|
|
172
173
|
| [Packaging and install weight](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/packaging.md) | Every entry point and what it costs |
|
|
@@ -303,14 +304,22 @@ Shipped so far:
|
|
|
303
304
|
- 2.11 workspaces, and telephony in full: an agent's files apart from where its code runs, in memory,
|
|
304
305
|
on disk, in any long-term store, read-only, or a composite of several; and a second phone provider,
|
|
305
306
|
Telnyx, beside Twilio, both answering, transferring, conferencing, recording under a retention
|
|
306
|
-
policy, and sending and receiving messages
|
|
307
|
+
policy, and sending and receiving messages;
|
|
308
|
+
- 2.12 observability that loses nothing, and media you can evaluate: spans spooled through a collector
|
|
309
|
+
outage, OTLP over gRPC, deployment evaluation that reads each run's whole trace, evaluators that
|
|
310
|
+
read and judge images, documents, audio, and video, a catalog of every stored asset, and automation
|
|
311
|
+
rules that roll a canary back when a score falls;
|
|
312
|
+
- 2.13 regions and residency, and credentials per tenant: agent servers across regions behind a
|
|
313
|
+
gateway that fails over only within a tenant's regions, residency rules that keep each tenant's
|
|
314
|
+
provider calls, traces, and assets in its regions, and each tenant's provider keys from a secret
|
|
315
|
+
manager, rotated without a restart and kept out of every error.
|
|
307
316
|
|
|
308
317
|
Upgrading from 1.x: `npx nexus migrate src --write` moves your imports, and
|
|
309
318
|
[MIGRATING.md](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/MIGRATING.md) covers the rest.
|
|
310
|
-
Next, 2.
|
|
311
|
-
|
|
312
|
-
Pinecone, images, the Gemini embedding batch tier, media understanding, video, Gemini Live, SIP,
|
|
313
|
-
both phone providers graduate once the live workflow passes against each. The
|
|
319
|
+
Next, 2.14 makes policy a reviewed file, checked by the command line before it ships, brings
|
|
320
|
+
per-tenant keys to every family, and lets an evaluation run an agent over emulated tools with nothing
|
|
321
|
+
sent. Pinecone, images, the Gemini embedding batch tier, media understanding, video, Gemini Live, SIP,
|
|
322
|
+
and both phone providers graduate once the live workflow passes against each. The
|
|
314
323
|
[roadmap](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/ROADMAP.md) lists what comes after, in the order
|
|
315
324
|
applications ask for it.
|
|
316
325
|
|
package/dist/core/lifecycle.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { ProviderHealthOutcome, LifecycleConfig, LifecycleStage, OperationD
|
|
|
3
3
|
import type { AuditLogger } from '../ops/audit-logger.js';
|
|
4
4
|
import type { MetricsCollector } from '../ops/metrics.js';
|
|
5
5
|
import { type RateLimiter } from '../ops/rate-limiter.js';
|
|
6
|
+
import type { CallResidency, ResidencyPolicy } from '../types/residency.js';
|
|
6
7
|
/** Every lifecycle stage, in the order an operation passes through them. */
|
|
7
8
|
export declare const LIFECYCLE_STAGES: readonly LifecycleStage[];
|
|
8
9
|
/** Raised when `lifecycle.authorize` refuses an operation, before any provider is called. */
|
|
@@ -114,3 +115,5 @@ export declare class OperationLifecycle implements OperationLifecycleLike {
|
|
|
114
115
|
private checkRateLimit;
|
|
115
116
|
private authorize;
|
|
116
117
|
}
|
|
118
|
+
/** An operation's residency, as the families that call providers check it. */
|
|
119
|
+
export declare function callResidency(policy: ResidencyPolicy, decision: CallResidency['decision']): CallResidency;
|
package/dist/core/lifecycle.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NexusRateLimitError } from '../ops/rate-limiter.js';
|
|
2
2
|
import { generateRequestId } from '../utils/ids.js';
|
|
3
|
+
import { scrubSecrets } from '../utils/scrub.js';
|
|
3
4
|
import { linkSignals } from '../utils/signals.js';
|
|
4
5
|
export const LIFECYCLE_STAGES = [
|
|
5
6
|
'validate',
|
|
@@ -51,12 +52,24 @@ export class OperationLifecycle {
|
|
|
51
52
|
...(deadline !== undefined ? { deadline } : {}),
|
|
52
53
|
...(options.traceContext ? { traceContext: options.traceContext } : {}),
|
|
53
54
|
...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
|
|
55
|
+
...(descriptor.tenantId !== undefined ? { tenantId: descriptor.tenantId } : {}),
|
|
54
56
|
};
|
|
55
|
-
const
|
|
57
|
+
const credentials = this.runtime.config?.credentials
|
|
58
|
+
? new CallCredentials(this.runtime.config.credentials, descriptor)
|
|
59
|
+
: undefined;
|
|
60
|
+
if (credentials)
|
|
61
|
+
context.credential = (provider) => credentials.resolve(provider);
|
|
62
|
+
const ticket = new Ticket(this.runtime, descriptor, context, startedAt, call.dispose, credentials);
|
|
56
63
|
try {
|
|
57
64
|
await this.runtime.metrics?.recordRequest(labelsOf(descriptor));
|
|
58
65
|
await this.checkRateLimit(descriptor);
|
|
59
66
|
await this.authorize(descriptor);
|
|
67
|
+
const policy = this.runtime.config?.residency;
|
|
68
|
+
if (policy) {
|
|
69
|
+
context.residency = callResidency(policy, await policy.regionsFor(descriptor.tenantId));
|
|
70
|
+
if (descriptor.provider)
|
|
71
|
+
context.residency.check(descriptor.provider);
|
|
72
|
+
}
|
|
60
73
|
}
|
|
61
74
|
catch (error) {
|
|
62
75
|
await ticket.fail(error);
|
|
@@ -73,8 +86,10 @@ export class OperationLifecycle {
|
|
|
73
86
|
await plan.inputPolicy?.(ticket.context);
|
|
74
87
|
await plan.resolveAssets?.(ticket.context);
|
|
75
88
|
const routed = await plan.route?.();
|
|
76
|
-
if (routed?.provider)
|
|
89
|
+
if (routed?.provider) {
|
|
90
|
+
ticket.context.residency?.check(routed.provider);
|
|
77
91
|
ticket.descriptor.provider = routed.provider;
|
|
92
|
+
}
|
|
78
93
|
if (routed?.model)
|
|
79
94
|
ticket.descriptor.model = routed.model;
|
|
80
95
|
if (this.runtime.config?.budget)
|
|
@@ -107,7 +122,9 @@ export class OperationLifecycle {
|
|
|
107
122
|
!this.runtime.rateLimit?.enabled &&
|
|
108
123
|
!config?.authorize &&
|
|
109
124
|
!config?.budget &&
|
|
110
|
-
!config?.hooks
|
|
125
|
+
!config?.hooks &&
|
|
126
|
+
!config?.credentials &&
|
|
127
|
+
!config?.residency);
|
|
111
128
|
}
|
|
112
129
|
async checkRateLimit(descriptor) {
|
|
113
130
|
const { rateLimiter, rateLimit } = this.runtime;
|
|
@@ -142,20 +159,98 @@ export class OperationLifecycle {
|
|
|
142
159
|
throw new OperationDeniedError(descriptor);
|
|
143
160
|
}
|
|
144
161
|
}
|
|
162
|
+
export function callResidency(policy, decision) {
|
|
163
|
+
return {
|
|
164
|
+
decision,
|
|
165
|
+
allows: (provider) => policy.allows(policy.regionOf(provider), decision),
|
|
166
|
+
place: (provider) => policy.place(provider, decision),
|
|
167
|
+
check: (provider) => {
|
|
168
|
+
const region = policy.regionOf(provider);
|
|
169
|
+
policy.check({ kind: 'provider', name: provider, ...(region ? { region } : {}) }, decision);
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
class CallCredentials {
|
|
174
|
+
resolver;
|
|
175
|
+
descriptor;
|
|
176
|
+
resolved = new Map();
|
|
177
|
+
secrets = new Set();
|
|
178
|
+
constructor(resolver, descriptor) {
|
|
179
|
+
this.resolver = resolver;
|
|
180
|
+
this.descriptor = descriptor;
|
|
181
|
+
}
|
|
182
|
+
resolve(provider) {
|
|
183
|
+
let found = this.resolved.get(provider);
|
|
184
|
+
if (found)
|
|
185
|
+
return found;
|
|
186
|
+
found = Promise.resolve(this.resolver.resolve({
|
|
187
|
+
provider,
|
|
188
|
+
...(this.descriptor.tenantId !== undefined ? { tenantId: this.descriptor.tenantId } : {}),
|
|
189
|
+
family: this.descriptor.family,
|
|
190
|
+
operation: this.descriptor.operation,
|
|
191
|
+
})).then((credential) => {
|
|
192
|
+
if (credential?.apiKey)
|
|
193
|
+
this.secrets.add(credential.apiKey);
|
|
194
|
+
for (const value of Object.values(credential?.headers ?? {}))
|
|
195
|
+
this.secrets.add(value);
|
|
196
|
+
return credential;
|
|
197
|
+
});
|
|
198
|
+
this.resolved.set(provider, found);
|
|
199
|
+
found.catch(() => {
|
|
200
|
+
if (this.resolved.get(provider) === found)
|
|
201
|
+
this.resolved.delete(provider);
|
|
202
|
+
});
|
|
203
|
+
return found;
|
|
204
|
+
}
|
|
205
|
+
async refresh(provider) {
|
|
206
|
+
this.resolved.delete(provider);
|
|
207
|
+
await this.resolver.invalidate?.({
|
|
208
|
+
provider,
|
|
209
|
+
...(this.descriptor.tenantId !== undefined ? { tenantId: this.descriptor.tenantId } : {}),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
async refreshAll() {
|
|
213
|
+
for (const provider of [...this.resolved.keys()])
|
|
214
|
+
await this.refresh(provider);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function refusedCredential(error, depth = 0) {
|
|
218
|
+
if (!error || typeof error !== 'object' || depth > 4)
|
|
219
|
+
return false;
|
|
220
|
+
const failed = error;
|
|
221
|
+
if (failed.category === 'auth' || failed.status === 401 || failed.statusCode === 401)
|
|
222
|
+
return true;
|
|
223
|
+
for (const list of [failed.attempts, failed.errors]) {
|
|
224
|
+
if (Array.isArray(list) && list.some((item) => refusedCredential(item, depth + 1)))
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
return refusedCredential(failed.cause, depth + 1);
|
|
228
|
+
}
|
|
145
229
|
class Ticket {
|
|
146
230
|
runtime;
|
|
147
231
|
descriptor;
|
|
148
232
|
context;
|
|
149
233
|
startedAt;
|
|
150
234
|
release;
|
|
235
|
+
credentials;
|
|
151
236
|
reservation;
|
|
152
237
|
finished = false;
|
|
153
|
-
constructor(runtime, descriptor, context, startedAt, release = () => undefined) {
|
|
238
|
+
constructor(runtime, descriptor, context, startedAt, release = () => undefined, credentials) {
|
|
154
239
|
this.runtime = runtime;
|
|
155
240
|
this.descriptor = descriptor;
|
|
156
241
|
this.context = context;
|
|
157
242
|
this.startedAt = startedAt;
|
|
158
243
|
this.release = release;
|
|
244
|
+
this.credentials = credentials;
|
|
245
|
+
}
|
|
246
|
+
async refreshCredential(provider) {
|
|
247
|
+
if (!this.credentials)
|
|
248
|
+
return false;
|
|
249
|
+
await this.credentials.refresh(provider);
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
scrub(error) {
|
|
253
|
+
return this.credentials ? scrubSecrets(error, this.credentials.secrets) : error;
|
|
159
254
|
}
|
|
160
255
|
async reserve(estimate) {
|
|
161
256
|
const budget = this.runtime.config?.budget;
|
|
@@ -193,9 +288,13 @@ class Ticket {
|
|
|
193
288
|
});
|
|
194
289
|
}
|
|
195
290
|
async fail(error) {
|
|
291
|
+
this.scrub(error);
|
|
196
292
|
if (this.finished)
|
|
197
293
|
return;
|
|
198
294
|
this.finished = true;
|
|
295
|
+
if (this.credentials && refusedCredential(error)) {
|
|
296
|
+
await this.credentials.refreshAll().catch(() => undefined);
|
|
297
|
+
}
|
|
199
298
|
const budget = this.runtime.config?.budget;
|
|
200
299
|
if (budget && this.reservation)
|
|
201
300
|
await budget.release(this.reservation);
|
|
@@ -239,7 +338,11 @@ function outcomeOf(error, signal) {
|
|
|
239
338
|
error instanceof BudgetExceededError ||
|
|
240
339
|
error instanceof NexusRateLimitError ||
|
|
241
340
|
(error instanceof Error &&
|
|
242
|
-
(error.name === 'CostBudgetError' ||
|
|
341
|
+
(error.name === 'CostBudgetError' ||
|
|
342
|
+
error.name === 'TenantLimitError' ||
|
|
343
|
+
error.name === 'NexusSecurityError' ||
|
|
344
|
+
error.name === 'ResidencyError' ||
|
|
345
|
+
error.name === 'CredentialError'))) {
|
|
243
346
|
return 'denied';
|
|
244
347
|
}
|
|
245
348
|
if (signal.aborted ||
|
package/dist/core/nexus.d.ts
CHANGED
|
@@ -289,7 +289,17 @@ export declare class NexusAI {
|
|
|
289
289
|
/** A completion as the lifecycle sees it. */
|
|
290
290
|
private describe;
|
|
291
291
|
private startOptions;
|
|
292
|
-
/**
|
|
292
|
+
/**
|
|
293
|
+
* A route kept inside the regions the operation may reach: each attempt sent to its provider when
|
|
294
|
+
* that runs inside them, to the provider's twin inside them when there is one, and dropped when
|
|
295
|
+
* neither. With nothing left, the operation is refused with a `ResidencyError` naming the rule.
|
|
296
|
+
*/
|
|
297
|
+
private placeRoute;
|
|
298
|
+
/**
|
|
299
|
+
* How failover runs the provider calls of one operation, with the context each attempt hands on,
|
|
300
|
+
* and, given the operation's ticket, how a refused credential is resolved again and every error
|
|
301
|
+
* cleaned of the credentials it resolved.
|
|
302
|
+
*/
|
|
293
303
|
private executionOptions;
|
|
294
304
|
/**
|
|
295
305
|
* Times a lifecycle stage into the trace when the lifecycle has work to do, so a client with
|
package/dist/core/nexus.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { configuredProviders } from '../providers/lazy.js';
|
|
2
|
+
import { ResidencyError } from '../residency/errors.js';
|
|
2
3
|
import { Router, FailoverExecutor } from '../router/index.js';
|
|
3
4
|
import { Logger } from '../utils/logger.js';
|
|
4
5
|
import { SecurityPipeline } from '../security/index.js';
|
|
@@ -23,7 +24,7 @@ import { ProviderHealthMonitor } from '../ops/health.js';
|
|
|
23
24
|
import { CircuitBreaker } from '../ops/circuit-breaker.js';
|
|
24
25
|
import { runBatch } from '../jobs/batch.js';
|
|
25
26
|
import { JobQueue } from '../jobs/queue.js';
|
|
26
|
-
import { OperationLifecycle } from './lifecycle.js';
|
|
27
|
+
import { callResidency, OperationLifecycle } from './lifecycle.js';
|
|
27
28
|
export class NexusAI {
|
|
28
29
|
images;
|
|
29
30
|
lifecycle;
|
|
@@ -137,7 +138,7 @@ export class NexusAI {
|
|
|
137
138
|
}
|
|
138
139
|
context = await this.pipeline.runHook('afterSecurity', context);
|
|
139
140
|
const decision = await this.pipeline.trace(context, 'routing', () => {
|
|
140
|
-
return this.router.route(context.request, this.config, this.providers, this.health.snapshot(), this.circuitBreaker.openProviders());
|
|
141
|
+
return this.placeRoute(this.router.route(context.request, this.config, this.providers, this.health.snapshot(), this.circuitBreaker.openProviders()), ticket.context.residency);
|
|
141
142
|
});
|
|
142
143
|
context.route = decision;
|
|
143
144
|
this.logger.info('route decision', decision);
|
|
@@ -197,7 +198,7 @@ export class NexusAI {
|
|
|
197
198
|
}
|
|
198
199
|
context = await this.pipeline.runHook('beforeProvider', context);
|
|
199
200
|
const response = await this.pipeline.trace(context, 'providerCall', () => {
|
|
200
|
-
return this.failover.complete(context.request, decision, this.providers, this.executionOptions(ticket.context));
|
|
201
|
+
return this.failover.complete(context.request, decision, this.providers, this.executionOptions(ticket.context, ticket));
|
|
201
202
|
});
|
|
202
203
|
context.response = response;
|
|
203
204
|
context = await this.pipeline.runHook('afterProvider', context);
|
|
@@ -638,8 +639,43 @@ export class NexusAI {
|
|
|
638
639
|
...(this.config.auditLog?.includeInput ? { audit: { request } } : {}),
|
|
639
640
|
};
|
|
640
641
|
}
|
|
641
|
-
|
|
642
|
-
|
|
642
|
+
placeRoute(decision, residency) {
|
|
643
|
+
if (!residency)
|
|
644
|
+
return decision;
|
|
645
|
+
const attempts = [
|
|
646
|
+
{ providerName: decision.providerName, model: decision.model, ...decision.primary },
|
|
647
|
+
...decision.fallbacks,
|
|
648
|
+
];
|
|
649
|
+
const placed = [];
|
|
650
|
+
const seen = new Set();
|
|
651
|
+
for (const attempt of attempts) {
|
|
652
|
+
const where = residency.place(attempt.providerName);
|
|
653
|
+
if (!where || !this.providers.has(where.provider))
|
|
654
|
+
continue;
|
|
655
|
+
const key = `${where.provider}\u0000${attempt.model}`;
|
|
656
|
+
if (seen.has(key))
|
|
657
|
+
continue;
|
|
658
|
+
seen.add(key);
|
|
659
|
+
placed.push({ ...attempt, providerName: where.provider });
|
|
660
|
+
}
|
|
661
|
+
const [first, ...rest] = placed;
|
|
662
|
+
if (!first) {
|
|
663
|
+
residency.check(decision.providerName);
|
|
664
|
+
throw new ResidencyError({ kind: 'provider', name: decision.providerName }, residency.decision, 'its twin is not registered');
|
|
665
|
+
}
|
|
666
|
+
const { providerName, model, ...limits } = first;
|
|
667
|
+
return {
|
|
668
|
+
providerName,
|
|
669
|
+
model,
|
|
670
|
+
reason: providerName === decision.providerName
|
|
671
|
+
? decision.reason
|
|
672
|
+
: `${decision.reason}; sent to ${providerName} inside ${residency.decision.rule}`,
|
|
673
|
+
fallbacks: rest,
|
|
674
|
+
...(Object.keys(limits).length > 0 ? { primary: limits } : {}),
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
executionOptions(context, ticket) {
|
|
678
|
+
const { requestId, traceContext, idempotencyKey, tenantId, credential } = context;
|
|
643
679
|
return {
|
|
644
680
|
timeoutMs: this.config.timeout,
|
|
645
681
|
retry: this.config.retry,
|
|
@@ -648,7 +684,13 @@ export class NexusAI {
|
|
|
648
684
|
requestId,
|
|
649
685
|
...(traceContext ? { traceContext } : {}),
|
|
650
686
|
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
687
|
+
...(tenantId !== undefined ? { tenantId } : {}),
|
|
688
|
+
...(credential ? { credential } : {}),
|
|
651
689
|
},
|
|
690
|
+
...(ticket?.refreshCredential && credential
|
|
691
|
+
? { refreshCredential: (provider) => ticket.refreshCredential?.(provider) ?? Promise.resolve(false) }
|
|
692
|
+
: {}),
|
|
693
|
+
...(ticket?.scrub && credential ? { scrub: (error) => ticket.scrub?.(error) ?? error } : {}),
|
|
652
694
|
};
|
|
653
695
|
}
|
|
654
696
|
lifecycleStep(context, name, fn) {
|
|
@@ -721,7 +763,7 @@ export class NexusAI {
|
|
|
721
763
|
: { ok: true, value: optimizationResult.value, findings: [], guardrailsApplied: [] };
|
|
722
764
|
if (this.isSecurityEnabled())
|
|
723
765
|
this.security.assertSafe(securityResult);
|
|
724
|
-
const decision = this.router.route(securityResult.value, this.config, this.providers, this.health.snapshot(), this.circuitBreaker.openProviders());
|
|
766
|
+
const decision = this.placeRoute(this.router.route(securityResult.value, this.config, this.providers, this.health.snapshot(), this.circuitBreaker.openProviders()), ticket.context.residency);
|
|
725
767
|
this.logger.info('stream route decision', {
|
|
726
768
|
...decision,
|
|
727
769
|
tokensSaved: optimizationResult.usage.savedTokens,
|
|
@@ -740,7 +782,7 @@ export class NexusAI {
|
|
|
740
782
|
if (shared)
|
|
741
783
|
await ticket.reserve(estimate.totalCost);
|
|
742
784
|
}
|
|
743
|
-
const stream = this.failover.stream(routedRequest, decision, this.providers, this.executionOptions(ticket.context));
|
|
785
|
+
const stream = this.failover.stream(routedRequest, decision, this.providers, this.executionOptions(ticket.context, ticket));
|
|
744
786
|
return {
|
|
745
787
|
stream: this.isSecurityEnabled() ? protectStreamOutput(stream, this.security, routedRequest.signal) : stream,
|
|
746
788
|
request: routedRequest,
|
|
@@ -785,7 +827,11 @@ export class NexusAI {
|
|
|
785
827
|
contextWindowSummary: true,
|
|
786
828
|
},
|
|
787
829
|
};
|
|
788
|
-
const
|
|
830
|
+
const policy = this.config.lifecycle?.residency;
|
|
831
|
+
const routed = this.router.route(summaryRequest, this.config, this.providers, this.health.snapshot());
|
|
832
|
+
const decision = policy
|
|
833
|
+
? this.placeRoute(routed, callResidency(policy, await policy.regionsFor(summaryRequest.tenantId)))
|
|
834
|
+
: routed;
|
|
789
835
|
const routedRequest = { ...summaryRequest, model: resolveModel(decision.model, this.config).model };
|
|
790
836
|
const response = await this.lifecycle.run({ ...this.describe(routedRequest, 'summarize'), provider: decision.providerName }, {
|
|
791
837
|
execute: (callContext) => this.failover.complete(routedRequest, decision, this.providers, this.executionOptions(callContext)),
|