@juspay/neurolink 12.18.1 → 12.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 (76) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/README.md +277 -75
  3. package/dist/browser/neurolink.min.js +396 -396
  4. package/dist/cli/commands/setup.js +2 -1
  5. package/dist/cli/factories/commandFactory.d.ts +22 -2
  6. package/dist/cli/factories/commandFactory.js +14 -2
  7. package/dist/cli/loop/optionsSchema.js +4 -0
  8. package/dist/cli/utils/classifierRouterFlags.js +39 -1
  9. package/dist/constants/enums.d.ts +13 -0
  10. package/dist/constants/enums.js +14 -0
  11. package/dist/context/budgetChecker.d.ts +3 -1
  12. package/dist/context/budgetChecker.js +20 -4
  13. package/dist/context/contextCompactor.d.ts +43 -3
  14. package/dist/context/contextCompactor.js +157 -41
  15. package/dist/context/contextDecision.d.ts +56 -0
  16. package/dist/context/contextDecision.js +234 -0
  17. package/dist/core/baseProvider.d.ts +27 -1
  18. package/dist/core/baseProvider.js +39 -0
  19. package/dist/core/toolRouting.d.ts +8 -0
  20. package/dist/core/toolRouting.js +52 -1
  21. package/dist/core/toolRoutingDecision.d.ts +35 -0
  22. package/dist/core/toolRoutingDecision.js +194 -0
  23. package/dist/factories/providerDescriptors.d.ts +15 -0
  24. package/dist/factories/providerDescriptors.js +57 -1
  25. package/dist/factories/providerRegistry.js +9 -1
  26. package/dist/index.d.ts +57 -1
  27. package/dist/index.js +60 -1
  28. package/dist/models/manifestRegistry.js +2 -0
  29. package/dist/models/manifests/typesafe.d.ts +16 -0
  30. package/dist/models/manifests/typesafe.js +42 -0
  31. package/dist/neurolink.d.ts +57 -1
  32. package/dist/neurolink.js +254 -10
  33. package/dist/observability/exporters/laminarExporter.js +3 -0
  34. package/dist/observability/exporters/posthogExporter.js +3 -0
  35. package/dist/observability/utils/spanSerializer.js +4 -0
  36. package/dist/providers/catalog/schema.d.ts +1 -1
  37. package/dist/providers/typesafe.d.ts +112 -0
  38. package/dist/providers/typesafe.js +654 -0
  39. package/dist/rag/pipeline/RAGPipeline.d.ts +3 -1
  40. package/dist/rag/pipeline/RAGPipeline.js +65 -4
  41. package/dist/rag/retrieval/searchDecision.d.ts +27 -0
  42. package/dist/rag/retrieval/searchDecision.js +124 -0
  43. package/dist/routing/classifierRouter.d.ts +74 -2
  44. package/dist/routing/classifierRouter.js +232 -32
  45. package/dist/routing/classifierStrategies.d.ts +33 -1
  46. package/dist/routing/classifierStrategies.js +237 -0
  47. package/dist/routing/index.d.ts +1 -1
  48. package/dist/routing/index.js +1 -1
  49. package/dist/routing/modelCatalog.d.ts +79 -0
  50. package/dist/routing/modelCatalog.js +382 -0
  51. package/dist/telemetry/tracers.d.ts +1 -0
  52. package/dist/telemetry/tracers.js +1 -0
  53. package/dist/types/classifierRouter.d.ts +160 -4
  54. package/dist/types/cli.d.ts +17 -1
  55. package/dist/types/config.d.ts +12 -0
  56. package/dist/types/context.d.ts +55 -1
  57. package/dist/types/decision.d.ts +200 -0
  58. package/dist/types/decision.js +21 -0
  59. package/dist/types/generate.d.ts +20 -0
  60. package/dist/types/index.d.ts +1 -0
  61. package/dist/types/index.js +2 -0
  62. package/dist/types/providers.d.ts +59 -1
  63. package/dist/types/providers.js +5 -0
  64. package/dist/types/rag.d.ts +48 -0
  65. package/dist/types/span.d.ts +11 -0
  66. package/dist/types/span.js +11 -0
  67. package/dist/types/stream.d.ts +13 -0
  68. package/dist/types/toolRouting.d.ts +56 -0
  69. package/dist/utils/decisionAnswers.d.ts +56 -0
  70. package/dist/utils/decisionAnswers.js +95 -0
  71. package/dist/utils/modelChoices.js +13 -1
  72. package/dist/utils/pricing.js +11 -0
  73. package/dist/utils/providerConfig.d.ts +4 -0
  74. package/dist/utils/providerConfig.js +17 -0
  75. package/docs-site/static/search-index.json +1 -1
  76. package/package.json +16 -3
@@ -5,6 +5,7 @@ import type { UnknownRecord, JsonValue, StreamingCapability } from "./common.js"
5
5
  import type { NeuroLink } from "../neurolink.js";
6
6
  import { AIProviderName, AnthropicModels, BedrockModels, DeepSeekModels, GoogleAIModels, LlamaCppModels, LMStudioModels, NvidiaNimModels, OpenAIModels, VertexModels } from "../constants/enums.js";
7
7
  import type { ValidationSchema } from "./aliases.js";
8
+ import type { DecisionRequest, DecisionResult } from "./decision.js";
8
9
  import type { EnhancedGenerateResult, TextGenerationOptions } from "./generate.js";
9
10
  import type { MultimodalAudioEntry } from "./file.js";
10
11
  import type { StreamOptions, StreamResult } from "./stream.js";
@@ -273,6 +274,27 @@ export type NeurolinkCredentials = {
273
274
  apiKey?: string;
274
275
  baseURL?: string;
275
276
  };
277
+ /** TypeSafe (Jev) — the `decide` inference type, not text generation. */
278
+ typesafe?: {
279
+ apiKey?: string;
280
+ baseURL?: string;
281
+ /**
282
+ * Which transport carries the decision request.
283
+ *
284
+ * - `"direct"` — TypeSafe's own API, `POST /v1/systemone`.
285
+ * - `"gateway"` — Vercel's AI Gateway evaluation-model endpoint, which
286
+ * bills through an existing Vercel account instead of a TypeSafe one.
287
+ *
288
+ * Omitted resolves automatically: `gateway` when only a gateway key is
289
+ * present, `direct` otherwise. The two differ on the wire — the gateway
290
+ * already uses the neutral `boolean`/`probability` vocabulary, carries
291
+ * the model in a header rather than the body, and omits `confidence`
292
+ * entirely — but a caller sees the identical `DecisionResult` either way.
293
+ */
294
+ transport?: "direct" | "gateway";
295
+ /** Vercel AI Gateway key. Defaults to `AI_GATEWAY_API_KEY`. */
296
+ gatewayApiKey?: string;
297
+ };
276
298
  };
277
299
  /**
278
300
  * Voyage AI /embeddings response shape.
@@ -767,6 +789,16 @@ export type AIProvider = {
767
789
  * Generate embedding vectors for multiple text inputs in batch.
768
790
  */
769
791
  embedMany(texts: string[], modelName?: string): Promise<number[][]>;
792
+ /**
793
+ * Evaluate a state against a batch of typed questions — the `decide`
794
+ * inference type. Implemented by BaseProvider (throws by default, as
795
+ * `embed` does); only providers whose descriptor declares `"decide"` in
796
+ * `inferenceKinds` override it.
797
+ *
798
+ * Optional so external AIProvider implementations still compile; callers
799
+ * must treat absence as "this provider cannot decide".
800
+ */
801
+ decide?(request: DecisionRequest): Promise<DecisionResult>;
770
802
  setupToolExecutor(sdk: {
771
803
  customTools: Map<string, unknown>;
772
804
  executeTool: (toolName: string, params: unknown) => Promise<unknown>;
@@ -1887,11 +1919,22 @@ export type ProviderRegistration = {
1887
1919
  aliases?: string[];
1888
1920
  descriptor?: ProviderDescriptor;
1889
1921
  };
1922
+ /**
1923
+ * The kinds of inference a provider can serve.
1924
+ *
1925
+ * `generate` and `stream` both produce text. `decide` produces typed,
1926
+ * calibrated judgements and no text at all — see src/lib/types/decision.ts.
1927
+ * They are peers: a provider may serve any subset.
1928
+ */
1929
+ export type InferenceKind = "generate" | "stream" | "decide";
1930
+ /** What a descriptor means when it declares no `inferenceKinds`. */
1931
+ export declare const DEFAULT_INFERENCE_KINDS: readonly InferenceKind[];
1890
1932
  /**
1891
1933
  * Single source of truth for one AI provider's static identity: how it's
1892
1934
  * addressed (name/aliases), how it's authenticated (credentialsKey/envVars),
1893
1935
  * what it defaults to (defaultModel), and how the rest of the codebase
1894
- * should treat it (toolSupport/localRuntime/healthCheck). Every consumer
1936
+ * should treat it (toolSupport/localRuntime/healthCheck/inferenceKinds).
1937
+ * Every consumer
1895
1938
  * that used to hand-maintain its own provider table (CLI choices,
1896
1939
  * CREDENTIAL_KEY_MAP, env-var checks, health-check dispatch, auto-select
1897
1940
  * priority, PROMPT_ONLY_TOOL_PROVIDERS) derives from PROVIDER_DESCRIPTORS
@@ -1902,6 +1945,20 @@ export type ProviderDescriptor = {
1902
1945
  name: AIProviderName;
1903
1946
  /** Alternate spellings accepted by the CLI and the alias index (kebab-case, shorthand, legacy names). Does not include `name` itself. */
1904
1947
  aliases: readonly string[];
1948
+ /**
1949
+ * Which inference types this provider actually serves. Omitted means
1950
+ * `["generate", "stream"]`, which is what every text provider is — so every
1951
+ * existing descriptor keeps its meaning untouched.
1952
+ *
1953
+ * This is the one declarative statement of a provider's modality. Before it
1954
+ * existed, modality was only implied (by `toolSupport`, `healthCheck` and
1955
+ * the auto-select ranks), which is why embedding-only providers like Voyage
1956
+ * and Jina have to implement `getAISDKModel()` as a throw: nothing could
1957
+ * express that they are not text providers. Consumers that build a
1958
+ * generation fallback chain, run the health sweep, or offer CLI model
1959
+ * choices should filter on this rather than special-case a provider name.
1960
+ */
1961
+ inferenceKinds?: readonly InferenceKind[];
1905
1962
  /** Key into NeurolinkCredentials for per-call/per-instance credential overrides. */
1906
1963
  credentialsKey: keyof NeurolinkCredentials;
1907
1964
  /** Environment variables this provider reads at runtime. */
@@ -1959,6 +2016,7 @@ export type ProviderDescriptor = {
1959
2016
  timeouts?: {
1960
2017
  generateMs?: number;
1961
2018
  streamMs?: number;
2019
+ decideMs?: number;
1962
2020
  };
1963
2021
  /** Ascending priority (1 = tried first) in the auto-select fallback chain used by getBestProvider(). Undefined = not part of the auto-select chain. */
1964
2022
  autoSelectPriority?: number;
@@ -109,3 +109,8 @@ export const DEFAULT_PROVIDER_CONFIGS = [
109
109
  models: [OpenAIModels.GPT_4O, OpenAIModels.GPT_4O_MINI],
110
110
  },
111
111
  ];
112
+ /** What a descriptor means when it declares no `inferenceKinds`. */
113
+ export const DEFAULT_INFERENCE_KINDS = [
114
+ "generate",
115
+ "stream",
116
+ ];
@@ -1,5 +1,6 @@
1
1
  import type { Tool } from "./tools.js";
2
2
  import type { EmbedInput } from "./providers.js";
3
+ import type { DecisionCallerFn } from "./decision.js";
3
4
  /**
4
5
  * RAG Type Definitions
5
6
  *
@@ -234,6 +235,16 @@ export type RAGPipelineConfig = {
234
235
  rerankingModel?: EmbeddingModelConfig;
235
236
  /** Multi-modal RAG configuration (image + text embeddings) */
236
237
  multiModal?: MultiModalRAGConfig;
238
+ /**
239
+ * Fail-open decision caller — typically a bound `NeuroLink.tryDecide`.
240
+ *
241
+ * When supplied AND a decision provider is configured, each query gets its
242
+ * own retrieval plan (`topK`, `hybrid`, `graph`, `rerank`) instead of the
243
+ * four static config values. An explicit per-call `QueryOptions` field
244
+ * always wins, and a capability this config did not enable can never be
245
+ * turned on by the plan.
246
+ */
247
+ decide?: DecisionCallerFn;
237
248
  };
238
249
  /**
239
250
  * Ingestion options
@@ -272,6 +283,12 @@ export type QueryOptions = {
272
283
  systemPrompt?: string;
273
284
  /** Temperature for generation */
274
285
  temperature?: number;
286
+ /**
287
+ * Set false to skip per-query retrieval planning even when a decision
288
+ * provider is configured. Fields passed explicitly here already override
289
+ * the plan, so this is for turning the extra round trip off entirely.
290
+ */
291
+ plan?: boolean;
275
292
  };
276
293
  /**
277
294
  * Query response
@@ -294,6 +311,8 @@ export type RAGResponse = {
294
311
  retrievalMethod: string;
295
312
  chunksRetrieved: number;
296
313
  reranked: boolean;
314
+ /** The per-query retrieval plan, when one was decided. */
315
+ plan?: SearchPlanResult;
297
316
  };
298
317
  };
299
318
  /**
@@ -1474,3 +1493,32 @@ export type ImageLoaderOptions = {
1474
1493
  /** Custom headers for URL fetches */
1475
1494
  headers?: Record<string, string>;
1476
1495
  };
1496
+ /**
1497
+ * What a pipeline is *able* to do, handed to the per-search planner so it
1498
+ * never suggests a capability that is not configured.
1499
+ */
1500
+ export type SearchPlanCapabilities = {
1501
+ /** The configured topK a breadth reading is scaled against. */
1502
+ defaultTopK: number;
1503
+ canHybrid: boolean;
1504
+ canGraph: boolean;
1505
+ canRerank: boolean;
1506
+ /** Per-call timeout override for the decision request. */
1507
+ timeoutMs?: number;
1508
+ };
1509
+ /**
1510
+ * A suggested retrieval plan for one query. Every field is optional and means
1511
+ * "use this unless the caller passed an explicit option".
1512
+ */
1513
+ export type SearchPlanResult = {
1514
+ topK?: number;
1515
+ hybrid?: boolean;
1516
+ graph?: boolean;
1517
+ rerank?: boolean;
1518
+ /** Index into the breadth rubric the topK was derived from. */
1519
+ breadthLevel?: number;
1520
+ /** Resolved decision model id, for telemetry. */
1521
+ model: string;
1522
+ /** Round trip in milliseconds. */
1523
+ latencyMs: number;
1524
+ };
@@ -15,6 +15,17 @@ export declare enum SpanType {
15
15
  TOOL_CALL = "tool.call",
16
16
  /** LLM generation request */
17
17
  MODEL_GENERATION = "model.generation",
18
+ /**
19
+ * Decision-model request (the `decide` inference type).
20
+ *
21
+ * Deliberately NOT folded into MODEL_GENERATION. A decision emits no text,
22
+ * so it has no output tokens, no finish reason and a latency distribution
23
+ * roughly an order of magnitude tighter; counting it as a generation would
24
+ * distort generation counts, p50/p95 latency and the output-token
25
+ * aggregate simultaneously, and would make a cost dashboard attribute
26
+ * fractions of a cent to the same bucket as a frontier model.
27
+ */
28
+ MODEL_DECISION = "model.decision",
18
29
  /** Embedding generation (reserved for future embedding API) */
19
30
  EMBEDDING = "embedding",
20
31
  /** Retrieval operation (reserved for future RAG support) */
@@ -16,6 +16,17 @@ export var SpanType;
16
16
  SpanType["TOOL_CALL"] = "tool.call";
17
17
  /** LLM generation request */
18
18
  SpanType["MODEL_GENERATION"] = "model.generation";
19
+ /**
20
+ * Decision-model request (the `decide` inference type).
21
+ *
22
+ * Deliberately NOT folded into MODEL_GENERATION. A decision emits no text,
23
+ * so it has no output tokens, no finish reason and a latency distribution
24
+ * roughly an order of magnitude tighter; counting it as a generation would
25
+ * distort generation counts, p50/p95 latency and the output-token
26
+ * aggregate simultaneously, and would make a cost dashboard attribute
27
+ * fractions of a cent to the same bucket as a frontier model.
28
+ */
29
+ SpanType["MODEL_DECISION"] = "model.decision";
19
30
  /** Embedding generation (reserved for future embedding API) */
20
31
  SpanType["EMBEDDING"] = "embedding";
21
32
  /** Retrieval operation (reserved for future RAG support) */
@@ -293,6 +293,19 @@ export type ExecutionControlOptions = {
293
293
  beforeStepTimeoutMs?: number;
294
294
  };
295
295
  export type StreamOptions = {
296
+ /**
297
+ * Fraction of the model's context window at which compaction runs for this
298
+ * request, replacing the 0.8 default.
299
+ *
300
+ * A request that needs only the current message does not need the whole
301
+ * window kept warm for it, and compacting earlier is free; a request that
302
+ * depends on the entire conversation should compact as late as possible.
303
+ * When the classifier router runs with a decision model it fills this in
304
+ * per request — always at or below the default, never above, because a
305
+ * request that overflows the window fails hard and `ModelPool` records that
306
+ * failure as a permanent cooldown.
307
+ */
308
+ compactionThreshold?: number;
296
309
  /**
297
310
  * Opt this stream call into the knowledge grounding configured on the
298
311
  * NeuroLink instance. Defaults to `false` when omitted.
@@ -16,6 +16,7 @@
16
16
  * identical to routing being disabled.
17
17
  */
18
18
  import type { GenerateOptions, GenerateResult } from "./generate.js";
19
+ import type { DecisionCallerFn } from "./decision.js";
19
20
  /** One routable server as declared by the host application. */
20
21
  export type ToolRoutingServerDescriptor = {
21
22
  /**
@@ -171,6 +172,17 @@ export type ToolRoutingConfig = {
171
172
  * off (or fails) the granularity falls back to `"server"` automatically.
172
173
  */
173
174
  granularity?: "server" | "tool";
175
+ /**
176
+ * How confidently a decision model must rule a server OUT before its tools
177
+ * are withheld. Default 0.6.
178
+ *
179
+ * Only consulted when a decision provider is configured, in which case one
180
+ * calibrated yes/no question per server replaces the generative router. The
181
+ * bar is deliberately asymmetric and high: keeping an unneeded server costs
182
+ * a few hundred tokens, while dropping a needed one costs the turn, because
183
+ * the model cannot call — or even ask for — a tool it was never shown.
184
+ */
185
+ minDropConfidence?: number;
174
186
  };
175
187
  /** Catalog entry pairing a server descriptor with its registered tool names. */
176
188
  export type ToolRoutingCatalogEntry = {
@@ -248,6 +260,12 @@ export type ToolRoutingDecision = {
248
260
  * applied (outcome === "applied").
249
261
  */
250
262
  granularity?: "server" | "tool";
263
+ /**
264
+ * Which strategy produced this decision. Absent on skip/fail-open paths that
265
+ * never reached one. "decision" is the calibrated per-server router,
266
+ * "embedding" the L2 retriever, "llm" the generative router.
267
+ */
268
+ strategy?: "decision" | "embedding" | "llm";
251
269
  };
252
270
  /** Parameters for `resolveToolRoutingExclusions()`. */
253
271
  export type ToolRoutingResolutionParams = {
@@ -265,6 +283,25 @@ export type ToolRoutingResolutionParams = {
265
283
  timeoutMs: number;
266
284
  /** Invokes the router LLM — `NeuroLink.generate` bound by the caller. */
267
285
  generateFn: (options: GenerateOptions) => Promise<GenerateResult>;
286
+ /**
287
+ * Invokes a decision model — `NeuroLink.tryDecide` bound by the caller.
288
+ * When supplied AND a decision provider is configured, one yes/no question
289
+ * per server replaces the router LLM call: ~400ms and ~$0.00002 instead of
290
+ * a generative call on a 15-second budget, and a calibrated probability per
291
+ * server instead of a list that cannot express doubt.
292
+ *
293
+ * Omitting it reproduces the LLM-only behaviour exactly, and so does
294
+ * supplying it with no decision provider configured — the caller's bound
295
+ * `tryDecide` returns null and the LLM path runs.
296
+ */
297
+ decideFn?: DecisionCallerFn;
298
+ /**
299
+ * How confidently the decision router must answer "no" before a server's
300
+ * tools are withheld. Default 0.6. Deliberately asymmetric: a wrongly
301
+ * dropped server breaks the turn, a wrongly kept one costs a few hundred
302
+ * tokens.
303
+ */
304
+ decisionMinDropConfidence?: number;
268
305
  /**
269
306
  * Optional callback invoked once per resolution with a structured summary of
270
307
  * the routing decision. Called on every return path (applied, skipped,
@@ -348,3 +385,22 @@ export type ToolRetrievalSelectOptions = {
348
385
  */
349
386
  timeoutMs?: number;
350
387
  };
388
+ /**
389
+ * What the decision-model tool router concluded. Distinct from
390
+ * {@link ToolRoutingDecision}, which is the telemetry record for a routing
391
+ * turn regardless of which strategy produced it.
392
+ */
393
+ export type ToolRoutingDecisionOutcome = {
394
+ /** Servers whose tools stay available. */
395
+ selectedServerIds: string[];
396
+ /** Servers the model confidently ruled out. */
397
+ excludedServerIds: string[];
398
+ /** Flattened tool names to add to the request denylist. */
399
+ excludedToolNames: string[];
400
+ /** How many servers came back with a usable, confident answer. */
401
+ answeredCount: number;
402
+ /** Resolved decision model id, for telemetry. */
403
+ model: string;
404
+ /** Round trip in milliseconds. */
405
+ latencyMs: number;
406
+ };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Readers for a decision model's answer map.
3
+ *
4
+ * Every reader validates at runtime and returns `undefined` for a missing id
5
+ * **or** a type mismatch, so no call site needs a type assertion (CLAUDE.md
6
+ * rule 14) and "no opinion" stays distinguishable from "an opinion of 0".
7
+ *
8
+ * @module utils/decisionAnswers
9
+ */
10
+ import type { DecisionAnswerMap, DecisionBooleanGate, DecisionChoiceReading, DecisionScoreReading, ProviderDescriptor } from "../types/index.js";
11
+ /**
12
+ * Read a yes/no answer as a probability.
13
+ *
14
+ * Returns undefined when the id is absent or the answer was a different
15
+ * type — a caller can therefore tell "not answered" from "answered 0".
16
+ */
17
+ export declare function readDecisionBoolean(answers: DecisionAnswerMap, id: string): number | undefined;
18
+ /**
19
+ * Read a choice answer, including the full ranking.
20
+ *
21
+ * `ranked` is the distribution sorted highest-first. This is what makes one
22
+ * choice question over N options a ranking of all N — the basis for picking
23
+ * from a large catalogue in a single request.
24
+ */
25
+ export declare function readDecisionChoice(answers: DecisionAnswerMap, id: string): DecisionChoiceReading | undefined;
26
+ export declare function readDecisionScore(answers: DecisionAnswerMap, id: string): DecisionScoreReading | undefined;
27
+ /**
28
+ * A yes/no answer carries no confidence of its own, so certainty has to be
29
+ * inferred from how far the probability sits from a coin flip. 0.5 → 0, and
30
+ * 0 or 1 → 1.
31
+ */
32
+ export declare function decisionBooleanConfidence(probability: number): number;
33
+ /** Whether a descriptor declares support for a given inference type. */
34
+ export declare function servesInferenceKind(descriptor: Pick<ProviderDescriptor, "inferenceKinds">, kind: "generate" | "stream" | "decide"): boolean;
35
+ /**
36
+ * Read a yes/no answer as an actionable decision, or `undefined` when there
37
+ * is not enough signal to act.
38
+ *
39
+ * Three outcomes, and keeping them distinct is the whole point: `true` (a
40
+ * confident yes), `false` (a confident no), and `undefined` (unanswered, the
41
+ * wrong type, or too close to a coin flip). Every consumer of the `decide`
42
+ * inference type needs exactly this, so the bars live here rather than being
43
+ * re-invented — inconsistently — at each call site.
44
+ */
45
+ export declare function gateDecisionBoolean(answers: DecisionAnswerMap, id: string, gate?: DecisionBooleanGate): boolean | undefined;
46
+ /**
47
+ * Build a question id that survives a round trip.
48
+ *
49
+ * Question ids are the only thing tying an answer back to what it was asked
50
+ * about, and a batch mixes heterogeneous questions (one per server, one per
51
+ * message, plus gates), so the namespace prefix is what demultiplexes them.
52
+ * The index — not the subject's own name — is the key, because ids from the
53
+ * wild (server ids, model ids, file paths) are not guaranteed to be distinct
54
+ * after any normalisation the wire might apply.
55
+ */
56
+ export declare function decisionKey(namespace: string, index: number): string;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Readers for a decision model's answer map.
3
+ *
4
+ * Every reader validates at runtime and returns `undefined` for a missing id
5
+ * **or** a type mismatch, so no call site needs a type assertion (CLAUDE.md
6
+ * rule 14) and "no opinion" stays distinguishable from "an opinion of 0".
7
+ *
8
+ * @module utils/decisionAnswers
9
+ */
10
+ import { DEFAULT_INFERENCE_KINDS } from "../types/index.js";
11
+ /**
12
+ * Read a yes/no answer as a probability.
13
+ *
14
+ * Returns undefined when the id is absent or the answer was a different
15
+ * type — a caller can therefore tell "not answered" from "answered 0".
16
+ */
17
+ export function readDecisionBoolean(answers, id) {
18
+ const answer = answers[id];
19
+ return answer?.type === "boolean" ? answer.probability : undefined;
20
+ }
21
+ /**
22
+ * Read a choice answer, including the full ranking.
23
+ *
24
+ * `ranked` is the distribution sorted highest-first. This is what makes one
25
+ * choice question over N options a ranking of all N — the basis for picking
26
+ * from a large catalogue in a single request.
27
+ */
28
+ export function readDecisionChoice(answers, id) {
29
+ const answer = answers[id];
30
+ if (answer?.type !== "choice") {
31
+ return undefined;
32
+ }
33
+ const { choice, confidence, probabilities } = answer;
34
+ const ranked = Object.entries(probabilities)
35
+ .map(([name, probability]) => ({ name, probability }))
36
+ .sort((a, b) => b.probability - a.probability);
37
+ return { choice, confidence, probabilities, ranked };
38
+ }
39
+ export function readDecisionScore(answers, id) {
40
+ const answer = answers[id];
41
+ if (answer?.type !== "score") {
42
+ return undefined;
43
+ }
44
+ const { score, confidence, legend, probabilities } = answer;
45
+ return { score, confidence, legend, probabilities };
46
+ }
47
+ /**
48
+ * A yes/no answer carries no confidence of its own, so certainty has to be
49
+ * inferred from how far the probability sits from a coin flip. 0.5 → 0, and
50
+ * 0 or 1 → 1.
51
+ */
52
+ export function decisionBooleanConfidence(probability) {
53
+ return Math.abs(probability - 0.5) * 2;
54
+ }
55
+ /** Whether a descriptor declares support for a given inference type. */
56
+ export function servesInferenceKind(descriptor, kind) {
57
+ return (descriptor.inferenceKinds ?? DEFAULT_INFERENCE_KINDS).includes(kind);
58
+ }
59
+ /** Default bars for {@link gateDecisionBoolean}. */
60
+ const DEFAULT_BOOLEAN_MIN_PROBABILITY = 0.5;
61
+ const DEFAULT_BOOLEAN_MIN_CONFIDENCE = 0.4;
62
+ /**
63
+ * Read a yes/no answer as an actionable decision, or `undefined` when there
64
+ * is not enough signal to act.
65
+ *
66
+ * Three outcomes, and keeping them distinct is the whole point: `true` (a
67
+ * confident yes), `false` (a confident no), and `undefined` (unanswered, the
68
+ * wrong type, or too close to a coin flip). Every consumer of the `decide`
69
+ * inference type needs exactly this, so the bars live here rather than being
70
+ * re-invented — inconsistently — at each call site.
71
+ */
72
+ export function gateDecisionBoolean(answers, id, gate) {
73
+ const probability = readDecisionBoolean(answers, id);
74
+ if (probability === undefined) {
75
+ return undefined;
76
+ }
77
+ const minConfidence = gate?.minConfidence ?? DEFAULT_BOOLEAN_MIN_CONFIDENCE;
78
+ if (decisionBooleanConfidence(probability) < minConfidence) {
79
+ return undefined;
80
+ }
81
+ return (probability >= (gate?.minProbability ?? DEFAULT_BOOLEAN_MIN_PROBABILITY));
82
+ }
83
+ /**
84
+ * Build a question id that survives a round trip.
85
+ *
86
+ * Question ids are the only thing tying an answer back to what it was asked
87
+ * about, and a batch mixes heterogeneous questions (one per server, one per
88
+ * message, plus gates), so the namespace prefix is what demultiplexes them.
89
+ * The index — not the subject's own name — is the key, because ids from the
90
+ * wild (server ids, model ids, file paths) are not guaranteed to be distinct
91
+ * after any normalisation the wire might apply.
92
+ */
93
+ export function decisionKey(namespace, index) {
94
+ return `${namespace}__${index}`;
95
+ }
@@ -2,7 +2,7 @@
2
2
  * Centralized model choices for CLI commands
3
3
  * Derives choices from model enums to ensure consistency
4
4
  */
5
- import { AIProviderName, OpenAIModels, AnthropicModels, GoogleAIModels, BedrockModels, VertexModels, OllamaModels, AzureOpenAIModels, LiteLLMModels, HuggingFaceModels, SageMakerModels, OpenRouterModels, DeepSeekModels, NvidiaNimModels, CohereModels, VoyageModels, JinaModels, StabilityModels, IdeogramModels, RecraftModels, ReplicateModels, } from "../constants/enums.js";
5
+ import { AIProviderName, OpenAIModels, AnthropicModels, GoogleAIModels, BedrockModels, VertexModels, OllamaModels, AzureOpenAIModels, LiteLLMModels, HuggingFaceModels, SageMakerModels, OpenRouterModels, DeepSeekModels, NvidiaNimModels, CohereModels, VoyageModels, JinaModels, StabilityModels, IdeogramModels, RecraftModels, TypeSafeModels, ReplicateModels, } from "../constants/enums.js";
6
6
  import { getCatalogJsonEntries } from "../providers/catalog/loader.js";
7
7
  /**
8
8
  * Looks up the JSON catalog entry for a provider, if it is one of the 9
@@ -393,6 +393,16 @@ const TOP_MODELS_CONFIG = {
393
393
  },
394
394
  { model: RecraftModels.RECRAFT_V2, description: "V2" },
395
395
  ],
396
+ [AIProviderName.TYPESAFE]: [
397
+ {
398
+ model: TypeSafeModels.JEV_LATEST,
399
+ description: "Recommended - Jev (decisions, not text)",
400
+ },
401
+ {
402
+ model: TypeSafeModels.JEV_PREVIEW,
403
+ description: "Preview build of Jev",
404
+ },
405
+ ],
396
406
  [AIProviderName.AUTO]: [],
397
407
  };
398
408
  /**
@@ -432,6 +442,7 @@ export const DEFAULT_MODELS = {
432
442
  [AIProviderName.STABILITY]: StabilityModels.STABLE_IMAGE_ULTRA,
433
443
  [AIProviderName.IDEOGRAM]: IdeogramModels.IDEOGRAM_V3,
434
444
  [AIProviderName.RECRAFT]: RecraftModels.RECRAFT_V3,
445
+ [AIProviderName.TYPESAFE]: TypeSafeModels.JEV_LATEST,
435
446
  };
436
447
  /**
437
448
  * Model enum mappings for getAllModels.
@@ -464,6 +475,7 @@ const MODEL_ENUMS = {
464
475
  [AIProviderName.STABILITY]: StabilityModels,
465
476
  [AIProviderName.IDEOGRAM]: IdeogramModels,
466
477
  [AIProviderName.RECRAFT]: RecraftModels,
478
+ [AIProviderName.TYPESAFE]: TypeSafeModels,
467
479
  [AIProviderName.AUTO]: null,
468
480
  };
469
481
  /**
@@ -634,6 +634,17 @@ const PRICING = {
634
634
  output: 0,
635
635
  },
636
636
  },
637
+ typesafe: {
638
+ // TypeSafe bills per million INPUT tokens only. Output is REPORTED but
639
+ // not billed: measured live, a decision returns roughly 17 output tokens
640
+ // per question (1 question → 21, 20 questions → 354), and the rate on
641
+ // them is zero. So `output: 0` is a price, not a token count — do not
642
+ // "correct" it by reading a non-zero `usage.output_tokens` off a
643
+ // response. A three-question decision costs ~$0.000013 in total.
644
+ // Rates from typesafe.ai/pricing (checked 2026-09-20).
645
+ _default: { input: 0.042 / 1_000_000, output: 0 },
646
+ "jev-latest": { input: 0.042 / 1_000_000, output: 0 },
647
+ },
637
648
  stability: {
638
649
  // Stability AI bills per image; symbolic per-token rate.
639
650
  _default: { input: 0, output: 0.04 / 1_000 },
@@ -396,3 +396,7 @@ export declare function resolveOpenAICompatConfig(entry: OpenAICompatConfigInput
396
396
  apiKey: string;
397
397
  baseURL: string;
398
398
  };
399
+ /**
400
+ * TypeSafe (Jev) — the `decide` inference type. Not a text provider.
401
+ */
402
+ export declare function createTypeSafeConfig(): ProviderConfigOptions;
@@ -1307,3 +1307,20 @@ export function resolveOpenAICompatConfig(entry, credentials) {
1307
1307
  }
1308
1308
  return { apiKey, baseURL };
1309
1309
  }
1310
+ /**
1311
+ * TypeSafe (Jev) — the `decide` inference type. Not a text provider.
1312
+ */
1313
+ export function createTypeSafeConfig() {
1314
+ return {
1315
+ providerName: "TypeSafe (Jev)",
1316
+ envVarName: "TYPESAFE_API_KEY",
1317
+ setupUrl: "https://console.typesafe.ai/keys",
1318
+ description: "API key",
1319
+ instructions: [
1320
+ "1. Visit: https://console.typesafe.ai/keys",
1321
+ "2. Sign in to your TypeSafe account",
1322
+ "3. Create a new API key",
1323
+ "4. Set TYPESAFE_API_KEY in your .env file",
1324
+ ],
1325
+ };
1326
+ }