nexus-ai-pro 2.17.0 → 2.19.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 (125) hide show
  1. package/API_STABILITY.md +50 -0
  2. package/CHANGELOG.md +122 -1
  3. package/README.md +12 -3
  4. package/dist/capabilities/negotiate.js +5 -0
  5. package/dist/decisions/completion.d.ts +48 -0
  6. package/dist/decisions/completion.js +195 -0
  7. package/dist/decisions/core.d.ts +525 -0
  8. package/dist/decisions/core.js +493 -0
  9. package/dist/decisions/index.d.ts +9 -0
  10. package/dist/decisions/index.js +3 -0
  11. package/dist/decisions/integrations.d.ts +142 -0
  12. package/dist/decisions/integrations.js +170 -0
  13. package/dist/decisions/mock.d.ts +32 -0
  14. package/dist/decisions/mock.js +47 -0
  15. package/dist/decisions/typesafe.d.ts +45 -0
  16. package/dist/decisions/typesafe.js +144 -0
  17. package/dist/evaluate/media.d.ts +6 -2
  18. package/dist/evaluate/media.js +7 -21
  19. package/dist/index.d.ts +1 -1
  20. package/dist/mcp/client.d.ts +25 -2
  21. package/dist/mcp/client.js +82 -47
  22. package/dist/mcp/server.d.ts +1 -1
  23. package/dist/mcp/server.js +4 -1
  24. package/dist/protocols/acp-editor.js +42 -15
  25. package/dist/providers/google.js +28 -0
  26. package/dist/providers/ollama.js +23 -0
  27. package/dist/providers/openai.d.ts +6 -0
  28. package/dist/providers/openai.js +26 -0
  29. package/dist/router/failover.js +19 -1
  30. package/dist/server/index.d.ts +1 -0
  31. package/dist/server/list-page.d.ts +38 -0
  32. package/dist/server/list-page.js +49 -0
  33. package/dist/server/regions.d.ts +15 -1
  34. package/dist/server/regions.js +70 -0
  35. package/dist/server/runs.d.ts +24 -0
  36. package/dist/server/runs.js +18 -4
  37. package/dist/server/server.js +17 -5
  38. package/dist/tracing/index.d.ts +2 -2
  39. package/dist/tracing/instrument.d.ts +7 -2
  40. package/dist/tracing/instrument.js +69 -2
  41. package/dist/tracing/otlp-proto.js +18 -0
  42. package/dist/tracing/otlp.d.ts +26 -1
  43. package/dist/tracing/otlp.js +19 -0
  44. package/dist/tracing/tracer.d.ts +36 -1
  45. package/dist/tracing/tracer.js +68 -2
  46. package/dist/types/lifecycle.d.ts +1 -1
  47. package/dist/types/messages.d.ts +9 -0
  48. package/dist/types/providers.d.ts +2 -0
  49. package/dist/types/response.d.ts +35 -0
  50. package/dist/types/tracing.d.ts +33 -1
  51. package/dist/utils/media-duration.d.ts +20 -0
  52. package/dist/utils/media-duration.js +334 -0
  53. package/dist/version.d.ts +1 -1
  54. package/dist/version.js +1 -1
  55. package/dist/video/google.d.ts +8 -0
  56. package/dist/video/google.js +31 -9
  57. package/dist/video/index.d.ts +18 -3
  58. package/dist/video/index.js +46 -3
  59. package/dist/video/mock.js +3 -1
  60. package/dist/video/runway.d.ts +2 -0
  61. package/dist/video/runway.js +45 -11
  62. package/dist/workflow/chains.d.ts +13 -1
  63. package/dist/workflow/chains.js +40 -0
  64. package/dist-cjs/capabilities/negotiate.js +5 -0
  65. package/dist-cjs/decisions/completion.d.ts +48 -0
  66. package/dist-cjs/decisions/completion.js +198 -0
  67. package/dist-cjs/decisions/core.d.ts +525 -0
  68. package/dist-cjs/decisions/core.js +509 -0
  69. package/dist-cjs/decisions/index.d.ts +9 -0
  70. package/dist-cjs/decisions/index.js +27 -0
  71. package/dist-cjs/decisions/integrations.d.ts +142 -0
  72. package/dist-cjs/decisions/integrations.js +178 -0
  73. package/dist-cjs/decisions/mock.d.ts +32 -0
  74. package/dist-cjs/decisions/mock.js +51 -0
  75. package/dist-cjs/decisions/typesafe.d.ts +45 -0
  76. package/dist-cjs/decisions/typesafe.js +148 -0
  77. package/dist-cjs/evaluate/media.d.ts +6 -2
  78. package/dist-cjs/evaluate/media.js +9 -21
  79. package/dist-cjs/index.d.ts +1 -1
  80. package/dist-cjs/mcp/client.d.ts +25 -2
  81. package/dist-cjs/mcp/client.js +82 -47
  82. package/dist-cjs/mcp/server.d.ts +1 -1
  83. package/dist-cjs/mcp/server.js +4 -1
  84. package/dist-cjs/protocols/acp-editor.js +42 -15
  85. package/dist-cjs/providers/google.js +28 -0
  86. package/dist-cjs/providers/ollama.js +23 -0
  87. package/dist-cjs/providers/openai.d.ts +6 -0
  88. package/dist-cjs/providers/openai.js +26 -0
  89. package/dist-cjs/router/failover.js +19 -1
  90. package/dist-cjs/server/index.d.ts +1 -0
  91. package/dist-cjs/server/list-page.d.ts +38 -0
  92. package/dist-cjs/server/list-page.js +56 -0
  93. package/dist-cjs/server/regions.d.ts +15 -1
  94. package/dist-cjs/server/regions.js +70 -0
  95. package/dist-cjs/server/runs.d.ts +24 -0
  96. package/dist-cjs/server/runs.js +18 -4
  97. package/dist-cjs/server/server.js +17 -5
  98. package/dist-cjs/tracing/index.d.ts +2 -2
  99. package/dist-cjs/tracing/instrument.d.ts +7 -2
  100. package/dist-cjs/tracing/instrument.js +69 -2
  101. package/dist-cjs/tracing/otlp-proto.js +18 -0
  102. package/dist-cjs/tracing/otlp.d.ts +26 -1
  103. package/dist-cjs/tracing/otlp.js +19 -0
  104. package/dist-cjs/tracing/tracer.d.ts +36 -1
  105. package/dist-cjs/tracing/tracer.js +68 -2
  106. package/dist-cjs/types/lifecycle.d.ts +1 -1
  107. package/dist-cjs/types/messages.d.ts +9 -0
  108. package/dist-cjs/types/providers.d.ts +2 -0
  109. package/dist-cjs/types/response.d.ts +35 -0
  110. package/dist-cjs/types/tracing.d.ts +33 -1
  111. package/dist-cjs/utils/media-duration.d.ts +20 -0
  112. package/dist-cjs/utils/media-duration.js +337 -0
  113. package/dist-cjs/version.d.ts +1 -1
  114. package/dist-cjs/version.js +1 -1
  115. package/dist-cjs/video/google.d.ts +8 -0
  116. package/dist-cjs/video/google.js +31 -9
  117. package/dist-cjs/video/index.d.ts +18 -3
  118. package/dist-cjs/video/index.js +46 -3
  119. package/dist-cjs/video/mock.js +3 -1
  120. package/dist-cjs/video/runway.d.ts +2 -0
  121. package/dist-cjs/video/runway.js +45 -11
  122. package/dist-cjs/workflow/chains.d.ts +13 -1
  123. package/dist-cjs/workflow/chains.js +40 -0
  124. package/llms.txt +2 -1
  125. package/package.json +41 -2
package/API_STABILITY.md CHANGED
@@ -41,6 +41,56 @@ 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.19 stage (2.19.0)
45
+
46
+ Everything 2.19 adds is stable from the start and additive, unless this section says otherwise.
47
+
48
+ New and stable:
49
+ - in the root: `CompletionRequest.logprobs`, `NexusResponse.logprobs`, `TokenLogprob`, and
50
+ `ModelCapabilities.logprobs`;
51
+ - in `nexus-ai-pro/workflows`: `ClassifyRouteOptions.decisions`.
52
+
53
+ New and experimental, until the live workflow passes against TypeSafe AI's Jev and a model that
54
+ reports log probabilities: the typed decisions family, `nexus-ai-pro/decisions`,
55
+ `nexus-ai-pro/decisions/typesafe`, and `nexus-ai-pro/decisions/mock`.
56
+
57
+ Changed on purpose: `OperationFamily` and `RunKind` widen with `decision`.
58
+
59
+ ## 2.18 stage (2.18.0)
60
+
61
+ Everything 2.18 adds is stable from the start and additive, unless this section says otherwise.
62
+
63
+ New and stable:
64
+ - in `nexus-ai-pro/tracing`: `RunEvent`, `RunLink`, `RunAttributeValue`, `RunEventInput`, `RunLinkInput`,
65
+ `Run.events`, `droppedEvents`, and `links`, `Tracer.event()`, `RunHandle.event()` and `link()`,
66
+ `StartRunOptions.links`, `FinishRunOptions.events` and `links`, `TracerOptions.maxEventsPerRun`, and
67
+ the event names `retry`, `failover`, `progress`, `guardrail`, `cache`, and `capability_warning`;
68
+ - in `nexus-ai-pro/tracing/otlp`: `OtlpSpanEvent`, `OtlpSpanLink`, and `OtlpSpan.events`,
69
+ `droppedEventsCount`, and `links`;
70
+ - in the root: `ResponseAttempt` and `ResponseMeta.failedAttempts`;
71
+ - in `nexus-ai-pro/evaluate/media`: `readMediaDuration()` and `MediaDuration`;
72
+ - in `nexus-ai-pro/video`, experimental with the family: `VideoModelCapabilities.maxInputVideoSeconds`;
73
+ - in `nexus-ai-pro/server/regions`: `RegionalGatewayOptions.lists`, `RegionListGap`, and a merged
74
+ list's `regions`, `incomplete`, and `nextCursor`;
75
+ - in `nexus-ai-pro/server`: `ListPage`, `RunManager.listThreads()` and `listRuns()`, and the lists'
76
+ `cursor` and `nextCursor`;
77
+ - in `nexus-ai-pro/mcp`: `McpClientOptions.reconnect`, `McpClient.onToolsChanged()`, and
78
+ `SseClientOptions.reopen`;
79
+ - in `nexus-ai-pro/video`, experimental with the family: `VideoGenerateRequest.referenceVideos` and
80
+ `VideoProviderRequest.referenceVideos`, the `reference-videos` input kind,
81
+ `VideoModelCapabilities.maxReferenceVideos` and `referenceVideoSeconds`, and
82
+ `VideoConformanceOptions.referenceVideo`;
83
+ - in `nexus-ai-pro/video/google`, experimental with the family:
84
+ `GoogleVideoProviderConfig.interactionsBaseUrl`, and Gemini Omni Flash on Vertex AI.
85
+
86
+ Changed on purpose:
87
+ - a list through the regional gateway is merged from every region the tenant may reach;
88
+ `lists: 'home'` keeps the earlier answer;
89
+ - an agent server's lists are ordered by last write, then id, and a `limit` that is not a whole
90
+ number above 0 is refused;
91
+ - `McpServer` says `tools.listChanged`, and `register()` sends the notification;
92
+ - `VideoInputKind` widens with `reference-videos`.
93
+
44
94
  ## 2.17 stage (2.17.0)
45
95
 
46
96
  Everything 2.17 adds is stable from the start and additive, unless this section says otherwise.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,125 @@ Notable changes to this project are recorded here. The format follows [Keep a Ch
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.19.0] - 2026-10-24
8
+
9
+ Typed decisions: a yes or no, a choice, or a score, answered with probabilities and a confidence read
10
+ from them, from TypeSafe AI's Jev or from any model, acted on when sure and sent to a person when not.
11
+
12
+ ### Added
13
+
14
+ - **Typed decisions.** `nexus-ai-pro/decisions` asks a `DecisionRequest`'s questions of one state: a
15
+ `YesNoQuestion`, a `ChoiceQuestion` of up to 255 options, or a `ScoreQuestion` of up to ten levels,
16
+ each answered in isolation with its probabilities and a confidence read the same way from any
17
+ provider. Answers are typed by their questions, so a choice's answer names only its options.
18
+ `DecisionManager` refuses a malformed question before it is sent, fails over, caches, traces each
19
+ decision as a run of kind `decision`, and runs it as an operation of the client, family
20
+ `decision`, budgeted at its model's price and on the tenant's key.
21
+ - **TypeSafe AI's Jev.** `TypeSafeDecisionProvider`, on `nexus-ai-pro/decisions/typesafe`, speaks the
22
+ System One API: every question of a decision in one call, its refusals naming the field at fault,
23
+ priced at $42 per billion input tokens with output free.
24
+ - **Decisions from any model.** `completionDecisions()` asks a completion model each question for a
25
+ one-token code, and reads its probabilities from that token's log probabilities where the provider
26
+ reports them, or counts them over samples where it does not, or refuses to.
27
+ - **Gates and calibration.** `gate()` acts, sends to a person, or abstains by confidence; `calibrate()`
28
+ measures how answers' probabilities track how often they are right, and chooses the threshold that
29
+ holds a target accuracy on the application's own labelled answers.
30
+ - **Decisions where the package decides.** `decisionRoute()` for a graph's branch, `decisionPermissions()`
31
+ for an agent's tool calls, `decisionGuard()` and `decisionModel()` as agent middleware,
32
+ `decisionEvaluator()` and `reviewUnsure()` for evaluation, and `classifyRoute()`'s `decisions`:
33
+ an uncertain answer goes to a person.
34
+ - **Log probabilities.** `CompletionRequest.logprobs` asks for each answer token's log probability and
35
+ its alternatives, on `NexusResponse.logprobs` as `TokenLogprob` values, from OpenAI's Chat
36
+ Completions, the OpenAI-compatible adapters that report them, Gemini, and Ollama;
37
+ `ModelCapabilities.logprobs` declares a model that cannot.
38
+ - `MockDecisionProvider`, on `nexus-ai-pro/decisions/mock`, and `runDecisionConformance()`.
39
+
40
+ ### Changed
41
+
42
+ - `OperationFamily` and `RunKind` have `decision`; a `switch` over either that names every one needs
43
+ the new one.
44
+
45
+ ### Fixed
46
+
47
+ - A test that waits forever fails at five minutes, and every child process the tests start
48
+ synchronously has a timeout of its own: the 2.18.0 publish's first attempt waited two hours on one.
49
+
50
+ ## [2.18.0] - 2026-10-22
51
+
52
+ Span events and links, media that knows its length, a tenant's threads and runs listed and paged
53
+ across all of its regions, MCP servers that reconnect and change their tools mid-session, and videos
54
+ made after reference videos.
55
+
56
+ ### Added
57
+
58
+ - **Span events and links.** A run records what happened inside it as `RunEvent` values, each at its
59
+ time, and the runs it came from beside its parent as `RunLink` values. `Tracer.event()` records on
60
+ the run in scope, `RunHandle.event()` and `link()` on one run; `StartRunOptions.links` take a link
61
+ or a W3C `traceparent`, and `FinishRunOptions` take the `events` and `links` a run learns as it
62
+ ends. `maxEventsPerRun` (128) keeps a run's events bounded, counting the rest as `droppedEvents`.
63
+ Both reach OTLP as span events and links, in JSON, in protobuf, and over gRPC. The studio shows them
64
+ on a run, each link opening the run it names, and `#/traces/<trace>/<run>` opens one run of a trace.
65
+ - **Events where they happen.** A graph's retry is a `retry` event of its task's run, a tool's
66
+ progress a `progress` event of the tool's, and a task after a step of several, such as a fan-out's
67
+ join, links to each of them. A model run has the attempts that failed before its answer, as `retry`
68
+ and `failover` at the times they failed, and its guardrails, cache answer, and capability warnings.
69
+ - **Failed attempts on the response.** `ResponseMeta.failedAttempts` lists each `ResponseAttempt` that
70
+ failed before the answer: its provider, model, error, category, and time; a stream's last chunk
71
+ carries them too.
72
+ - **Media that knows its length.** `readMediaDuration()`, on `nexus-ai-pro/evaluate/media`, reads the
73
+ length of WAV, MP4 and MOV (fragmented MP4 included), WebM and Matroska, MP3, Ogg Vorbis and Opus,
74
+ and FLAC from their own headers, with no dependency. `mediaChecks()` holds audio and video to a
75
+ `durationMs` range with it, and a video model's `maxInputVideoSeconds` refuses a longer video before
76
+ it is sent: 10 seconds on Gemini Omni Flash, 30 on Veo.
77
+ - **Lists across regions.** The regional gateway answers `GET /threads` and `GET /runs` from every
78
+ region the tenant may reach, merged newest first and cut to the limit, naming the regions that
79
+ answered and, as `RegionListGap` values under `incomplete`, those that were down or failed. A
80
+ refusal every region gives is the answer. `lists: 'home'` keeps the home region's list alone.
81
+ - **Lists that page.** An agent server's `GET /threads`, `GET /runs`, and `GET /threads/:id/runs`
82
+ take a `cursor` and answer with a `nextCursor` while more follow; `RunManager.listThreads()` and
83
+ `listRuns()` return a `ListPage`. A cursor names a place in the order every region lists by, so the
84
+ regional gateway pages a merged list with it too, each record once, in order, however the records
85
+ fall between regions. A region that does not page by it contributes only what comes after it.
86
+ - **MCP that reconnects.** With `McpClientOptions.reconnect`, a client whose connection closed opens
87
+ it again at the next request and greets the server again: an SSE stream that ended, with
88
+ `SseClientOptions.reopen`, or a stdio server that exited, started again. A stream that cannot open,
89
+ or names no endpoint in time, is let go at once, so the next request tries again.
90
+ - **Tools that change.** `McpClient.onToolsChanged()` hears a server's
91
+ `notifications/tools/list_changed`. `McpServer.register()` sends one to a connected client, and the
92
+ server says it will. The editor bridge reconnects an editor's SSE servers within a session and lists
93
+ a server's tools again before the turn after it says they changed, keeping the last list when that
94
+ fails.
95
+ - **Reference videos.** `VideoGenerateRequest.referenceVideos` gives a model videos to take after.
96
+ Models declare `reference-videos` among their inputs, with `maxReferenceVideos` and
97
+ `referenceVideoSeconds`, each and together, read from a video's headers before it is sent. Gemini
98
+ Omni Flash takes three of three seconds each, as does Runway's `gemini_omni_flash_1.1`; Runway's
99
+ Seedance 2 models, `hailuo3`, and `wan3` take 15 seconds together, beside reference images. The
100
+ conformance suite checks them with `referenceVideo`.
101
+ - **Gemini Omni Flash on Vertex AI.** `GoogleVideoProvider` serves `gemini-omni-1.1-flash-preview` on
102
+ Vertex AI's Interactions API, at the global location for the project its `baseUrl` names, or at
103
+ `interactionsBaseUrl`.
104
+
105
+ ### Changed
106
+
107
+ - A list of threads or runs through the regional gateway comes from every region the tenant may
108
+ reach, not its home region alone; `RegionalGatewayOptions.lists: 'home'` keeps the 2.17 answer.
109
+ - An agent server lists threads and runs by when each was last written, then by id, the order its
110
+ cursor pages by. A `limit` that is not a whole number above 0 is refused with a 400.
111
+ - `McpServer` says its tools change, as `tools.listChanged`, and `register()` tells a connected
112
+ client.
113
+ - `VideoInputKind` has `reference-videos`; a `switch` over it that names every kind needs the new one.
114
+ - Tested on Node.js 26 as well as 22 and 24: CI runs the release gate on each, on the newest patch of
115
+ its line, so a Node.js security release, such as 2026-10-21's, is what the package is tested and
116
+ published on the day it ships.
117
+
118
+ ### Fixed
119
+
120
+ - A tenant's threads and runs on a server several tenants share: a list read the newest records of
121
+ every tenant and then kept the tenant's, so another tenant's newer ones could crowd its page out,
122
+ down to an empty one. A list now reads until the tenant's page is full.
123
+ - The studio showed a run's metadata, its feedback, and a tool's progress as
124
+ `[object HTMLHeadingElement]` rather than their content.
125
+
7
126
  ## [2.17.0] - 2026-10-21
8
127
 
9
128
  The editor's MCP servers over every transport, decided by what their servers say they do; and video
@@ -3103,7 +3222,9 @@ shared policy vocabulary is in place for a future release that revisits this.
3103
3222
  - URL fetching rejects unsafe private-network targets and limits response reads.
3104
3223
  - CLI and audit findings no longer disclose detected secret values.
3105
3224
 
3106
- [Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.17.0...HEAD
3225
+ [Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.19.0...HEAD
3226
+ [2.19.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.18.0...v2.19.0
3227
+ [2.18.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.17.0...v2.18.0
3107
3228
  [2.17.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.16.0...v2.17.0
3108
3229
  [2.16.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.15.1...v2.16.0
3109
3230
  [2.15.1]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.15.0...v2.15.1
package/README.md CHANGED
@@ -156,6 +156,7 @@ from the doc comments. The guides live in the repository, so these links go to G
156
156
  | [Retrieval](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/retrieval.md) | Hybrid search, reranking, MMR, parent documents, and Redis, Pinecone, Weaviate, and Chroma stores |
157
157
  | [Embeddings and retrieval](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/embeddings.md) | Embeddings as a routed operation family, and RAG helpers |
158
158
  | [Media understanding](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/media.md) | Reading documents and images page by page, describing images, and answering questions about them, through any vision model or OCR |
159
+ | [Typed decisions](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/decisions.md) | A yes or no, a choice, or a score, answered with probabilities and a calibrated confidence, from TypeSafe AI's Jev or any model, so a graph, an approval, or a guardrail acts when it is sure and asks a person when not |
159
160
  | [Video generation](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/video.md) | Videos from a prompt or a first frame, on Google Veo and Runway, as durable jobs streamed into an asset store and priced per second |
160
161
  | [Images (experimental)](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/images.md) | Generation and masked edits over three backends, input safety, moderation, asset stores |
161
162
  | [Voice](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/voice.md) | Transcription, speech, voice turns, and voice sessions |
@@ -327,12 +328,20 @@ Shipped so far:
327
328
  as it is written;
328
329
  - 2.17 the editor's MCP servers over every transport, and video you can extend or edit: an ACP agent
329
330
  connects stdio, HTTP, and SSE servers and decides each tool by what its server says it does, and a
330
- video is extended from its own job or edited on Gemini Omni Flash, Veo, and Runway.
331
+ video is extended from its own job or edited on Gemini Omni Flash, Veo, and Runway;
332
+ - 2.18 span events and links, as OpenTelemetry has them, media that knows its length from its own
333
+ headers, a tenant's threads and runs listed and paged across all of its regions, MCP servers that
334
+ reconnect and change their tools within a session, and videos made after reference videos;
335
+ - 2.19 typed decisions: a yes or no, a choice, or a score, answered with probabilities and a
336
+ confidence, from TypeSafe AI's Jev or from any model's log probabilities, gated by thresholds a
337
+ calibration chooses, so a graph's branch, an approval, or a guardrail acts when it is sure and asks
338
+ a person when it is not.
331
339
 
332
340
  Upgrading from 1.x: `npx nexus migrate src --write` moves your imports, and
333
341
  [MIGRATING.md](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/MIGRATING.md) covers the rest.
334
- Next, 2.18 records events and links on spans, as OpenTelemetry has them, and reads the length of
335
- compressed audio and video, so an evaluation holds a clip to its duration. Pinecone, images, the Gemini
342
+ Next, 2.20 reads a service's own API description into emulated tools, and lets an emulator hold
343
+ what a whole evaluation did, so examples that build on each other are evaluated with nothing sent.
344
+ Pinecone, images, the Gemini
336
345
  embedding batch tier, media understanding, video, Gemini Live, SIP, and both phone providers
337
346
  graduate once the live workflow passes against each. The
338
347
  [roadmap](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/ROADMAP.md) lists what comes after, in the order
@@ -95,6 +95,11 @@ export function negotiateCompletionRequest(request, capabilities, options = {})
95
95
  target.seed = undefined;
96
96
  });
97
97
  }
98
+ if (request.logprobs !== undefined && capabilities.logprobs === false) {
99
+ refuse('logprobs', request.logprobs, 'the model does not report token log probabilities', (target) => {
100
+ target.logprobs = undefined;
101
+ });
102
+ }
98
103
  if (request.topK !== undefined && capabilities.topK === false) {
99
104
  refuse('topK', request.topK, 'the model does not support top-k sampling', (target) => {
100
105
  target.topK = undefined;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Decisions from any completion model: each question asked on its own, answered with a one-token code,
3
+ * and its probabilities read from the code token's log probabilities, or counted over samples where
4
+ * the model reports none.
5
+ */
6
+ import type { CompletionRequest } from '../types/messages.js';
7
+ import type { NexusResponse } from '../types/response.js';
8
+ import { type DecisionProvider } from './core.js';
9
+ /** Anything that completes a request, such as a `NexusAI` client. */
10
+ export interface DecisionCompletionClient {
11
+ /** Runs one completion. */
12
+ complete(request: CompletionRequest): Promise<NexusResponse>;
13
+ }
14
+ /** How a completion model's probabilities are read. */
15
+ export type CompletionDecisionMethod = 'auto' | 'logprobs' | 'sampling';
16
+ /** Options for `completionDecisions()`. */
17
+ export interface CompletionDecisionsOptions {
18
+ /** The model to ask, as the client names it. */
19
+ model: string;
20
+ /** The provider's name in results. Defaults to `model`. */
21
+ name?: string;
22
+ /**
23
+ * How probabilities are read: `logprobs`, from one call's token log probabilities; `sampling`, by
24
+ * counting the answers of several calls; or `auto`, the default: log probabilities, and sampling for
25
+ * a model that reports none or refuses to, which it then asks by sampling from the start.
26
+ */
27
+ method?: CompletionDecisionMethod;
28
+ /** Calls per question, when sampling. Defaults to 8. */
29
+ samples?: number;
30
+ /** The temperature samples are drawn at. Defaults to 1. */
31
+ temperature?: number;
32
+ /** Questions asked at once. Defaults to 8. */
33
+ concurrency?: number;
34
+ /** Instructions added to every question, such as what the application's terms mean. */
35
+ instructions?: string;
36
+ }
37
+ /**
38
+ * Any completion model as a decision provider, through a client's own `complete()`, so every call is
39
+ * routed, authorized, budgeted, traced, and priced as every completion is. Each question is asked on
40
+ * its own, at temperature 0, to answer with one code: a letter per option, Y or N, or a level's digit.
41
+ * Its probabilities are read from the log probabilities of that token's alternatives where the model
42
+ * reports them, the way TypeSafe AI's Jev reads its own; elsewhere the question is asked `samples`
43
+ * times, and each answer counted. Sampled probabilities come in steps of one sample, and cost as many
44
+ * calls.
45
+ */
46
+ export declare function completionDecisions(client: DecisionCompletionClient, options: CompletionDecisionsOptions): DecisionProvider & {
47
+ readonly sampledModels: ReadonlySet<string>;
48
+ };
@@ -0,0 +1,195 @@
1
+ import { DecisionError, } from './core.js';
2
+ const LOGPROB_OPTIONS = 20;
3
+ export function completionDecisions(client, options) {
4
+ const method = options.method ?? 'auto';
5
+ const sampled = new Set();
6
+ const info = {
7
+ name: options.name ?? options.model,
8
+ capabilities: {
9
+ types: ['yes-no', 'choice', 'score'],
10
+ maxOptions: 255,
11
+ maxLevels: 10,
12
+ defaultModel: options.model,
13
+ },
14
+ chargedByItsCalls: true,
15
+ };
16
+ const ask = async (model, state, question, context, usage) => {
17
+ const codes = codesOf(question);
18
+ const prompt = promptOf(state, question, codes, options.instructions);
19
+ const request = (extra) => ({
20
+ model,
21
+ messages: [
22
+ {
23
+ role: 'system',
24
+ content: 'You answer one question about a state with exactly one of the codes given, and nothing else.',
25
+ },
26
+ { role: 'user', content: prompt },
27
+ ],
28
+ signal: context.signal,
29
+ ...(context.tenantId !== undefined ? { tenantId: context.tenantId } : {}),
30
+ ...extra,
31
+ });
32
+ const readable = codes.length <= LOGPROB_OPTIONS && codes.every((code) => code.length === 1);
33
+ if (method !== 'sampling' && readable && !sampled.has(model)) {
34
+ let response;
35
+ try {
36
+ response = await client.complete(request({ temperature: 0, maxTokens: 2, logprobs: { top: 20 } }));
37
+ }
38
+ catch (error) {
39
+ if (method === 'logprobs' || !refusedOption(error))
40
+ throw error;
41
+ }
42
+ if (response) {
43
+ usage.add(response);
44
+ const weights = weightsFrom(response.logprobs, codes);
45
+ if (weights)
46
+ return distributionOf(question, codes, weights);
47
+ if (method === 'logprobs') {
48
+ throw new DecisionError(`"${model}" reported no log probabilities for the codes it was given`, 'DECISION_FAILED');
49
+ }
50
+ }
51
+ sampled.add(model);
52
+ }
53
+ if (!readable && method === 'logprobs') {
54
+ throw new DecisionError(`A question of ${codes.length} options cannot be read from one token's ${LOGPROB_OPTIONS} alternatives`, 'DECISION_UNSUPPORTED');
55
+ }
56
+ const count = Math.max(1, Math.floor(options.samples ?? 8));
57
+ const answers = await Promise.all(Array.from({ length: count }, () => client.complete(request({ temperature: options.temperature ?? 1, maxTokens: 4 })).then((response) => {
58
+ usage.add(response);
59
+ return codeIn(response.content, codes);
60
+ })));
61
+ const weights = codes.map((code) => answers.filter((answer) => answer === code).length);
62
+ if (weights.every((weight) => weight === 0)) {
63
+ throw new DecisionError(`"${model}" answered none of ${count} samples with a code it was given`, 'DECISION_FAILED');
64
+ }
65
+ return distributionOf(question, codes, weights);
66
+ };
67
+ return {
68
+ info,
69
+ sampledModels: sampled,
70
+ async decide(request, context) {
71
+ const totals = new Totals();
72
+ const entries = Object.entries(request.questions);
73
+ const distributions = {};
74
+ const limit = Math.max(1, Math.floor(options.concurrency ?? 8));
75
+ let next = 0;
76
+ const worker = async () => {
77
+ while (next < entries.length) {
78
+ const [name, question] = entries[next++];
79
+ distributions[name] = await ask(request.model, request.state, question, context, totals);
80
+ }
81
+ };
82
+ await Promise.all(Array.from({ length: Math.min(limit, entries.length) }, worker));
83
+ return {
84
+ model: request.model,
85
+ distributions,
86
+ usage: totals.usage(),
87
+ ...(totals.cost() ? { cost: totals.cost() } : {}),
88
+ };
89
+ },
90
+ };
91
+ }
92
+ class Totals {
93
+ inputTokens = 0;
94
+ outputTokens = 0;
95
+ calls = 0;
96
+ amount = 0;
97
+ priced = false;
98
+ add(response) {
99
+ this.calls += 1;
100
+ this.inputTokens += response.meta.usage?.inputTokens ?? 0;
101
+ this.outputTokens += response.meta.usage?.outputTokens ?? 0;
102
+ if (response.meta.cost) {
103
+ this.amount += response.meta.cost.amount;
104
+ this.priced = true;
105
+ }
106
+ }
107
+ usage() {
108
+ return { inputTokens: this.inputTokens, outputTokens: this.outputTokens, calls: this.calls };
109
+ }
110
+ cost() {
111
+ return this.priced ? { amount: this.amount, currency: 'USD', basis: 'estimated' } : undefined;
112
+ }
113
+ }
114
+ function codesOf(question) {
115
+ if (question.type === 'yes-no')
116
+ return ['Y', 'N'];
117
+ if (question.type === 'score')
118
+ return question.levels.map((_, level) => String(level));
119
+ return question.options.length <= 26
120
+ ? question.options.map((_, index) => String.fromCharCode(65 + index))
121
+ : question.options.map((_, index) => String(index + 1));
122
+ }
123
+ function promptOf(state, question, codes, instructions) {
124
+ const text = typeof state === 'string' ? state : JSON.stringify(state, null, 2);
125
+ const lines = [`State:\n${text}`, ''];
126
+ if (instructions)
127
+ lines.push(instructions, '');
128
+ if (question.type === 'yes-no') {
129
+ lines.push(`Question: ${question.question ?? 'Is the statement true of the state?'}`);
130
+ lines.push(`Y: yes${question.criteria?.yes ? `, ${question.criteria.yes}` : ''}`);
131
+ lines.push(`N: no${question.criteria?.no ? `, ${question.criteria.no}` : ''}`);
132
+ }
133
+ else if (question.type === 'choice') {
134
+ lines.push(`Question: ${question.question ?? 'Which option fits the state?'}`);
135
+ question.options.forEach((option, index) => {
136
+ lines.push(`${codes[index]}: ${option.name}${option.description ? `, ${option.description}` : ''}`);
137
+ });
138
+ }
139
+ else {
140
+ lines.push(`Question: ${question.question ?? 'Which level fits the state?'}`);
141
+ question.levels.forEach((level, index) => {
142
+ lines.push(`${codes[index]}: ${level}`);
143
+ });
144
+ }
145
+ lines.push('', `Answer with one code: ${codes.join(', ')}.`);
146
+ return lines.join('\n');
147
+ }
148
+ function codeOf(token) {
149
+ return token.replace(/^[\s"'`*(]+|[\s"'`*).:,]+$/g, '').toUpperCase();
150
+ }
151
+ function weightsFrom(logprobs, codes) {
152
+ const first = logprobs?.find((item) => item.token.trim() !== '');
153
+ if (!first)
154
+ return undefined;
155
+ const candidates = first.top?.length ? first.top : [{ token: first.token, logprob: first.logprob }];
156
+ const weights = codes.map(() => 0);
157
+ let found = false;
158
+ for (const candidate of candidates) {
159
+ const index = matchCode(candidate.token, codes);
160
+ if (index < 0 || !Number.isFinite(candidate.logprob))
161
+ continue;
162
+ weights[index] = weights[index] + Math.exp(candidate.logprob);
163
+ found = true;
164
+ }
165
+ return found ? weights : undefined;
166
+ }
167
+ function codeIn(content, codes) {
168
+ const head = content.trim().split(/[\s:,.)]+/)[0] ?? '';
169
+ return codes[matchCode(head, codes)];
170
+ }
171
+ function matchCode(token, codes) {
172
+ const code = codeOf(token);
173
+ const index = codes.indexOf(code);
174
+ if (index >= 0 || codes.length !== 2 || codes[0] !== 'Y' || codes[1] !== 'N')
175
+ return index;
176
+ if (code === 'YES' || code === 'TRUE')
177
+ return 0;
178
+ return code === 'NO' || code === 'FALSE' ? 1 : -1;
179
+ }
180
+ function distributionOf(question, codes, weights) {
181
+ const total = weights.reduce((sum, weight) => sum + weight, 0);
182
+ const share = (index) => (total > 0 ? (weights[index] ?? 0) / total : 1 / codes.length);
183
+ if (question.type === 'yes-no')
184
+ return { type: 'yes-no', yes: share(0) };
185
+ if (question.type === 'score')
186
+ return { type: 'score', probabilities: codes.map((_, index) => share(index)) };
187
+ return {
188
+ type: 'choice',
189
+ probabilities: Object.fromEntries(question.options.map((option, index) => [option.name, share(index)])),
190
+ };
191
+ }
192
+ function refusedOption(error) {
193
+ const status = error?.status;
194
+ return status === 400 || status === 422;
195
+ }