@juspay/neurolink 12.49.9 → 12.49.11
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/CHANGELOG.md +2 -2
- package/dist/browser/neurolink.min.js +368 -368
- package/dist/providers/catalog/loader.js +6 -0
- package/dist/providers/catalog/provider-catalog.schema.json +3 -0
- package/dist/providers/catalog/reka.json +4 -1
- package/dist/providers/catalog/schema.d.ts +1 -0
- package/dist/providers/catalog/schema.js +1 -0
- package/dist/providers/configuredOpenAICompat.d.ts +7 -0
- package/dist/providers/configuredOpenAICompat.js +11 -0
- package/dist/providers/decide/dialects/index.d.ts +2 -0
- package/dist/providers/decide/dialects/index.js +5 -0
- package/dist/providers/decide/dialects/systemOne.d.ts +2 -0
- package/dist/providers/decide/dialects/systemOne.js +104 -0
- package/dist/providers/decide/guards.d.ts +4 -0
- package/dist/providers/decide/guards.js +29 -0
- package/dist/providers/systemOneDecision.d.ts +7 -3
- package/dist/providers/systemOneDecision.js +20 -117
- package/dist/types/decision.d.ts +17 -0
- package/dist/types/providerCatalog.d.ts +22 -6
- package/dist/types/providers.d.ts +11 -0
- package/docs-site/static/search-index.json +2 -2
- package/package.json +1 -1
|
@@ -113,6 +113,9 @@ export function getCatalogJsonEntries() {
|
|
|
113
113
|
}
|
|
114
114
|
export function buildCatalogEntries() {
|
|
115
115
|
return CATALOG_JSON_ENTRIES.map((entry) => {
|
|
116
|
+
// Only models whose entry sets `tools` explicitly contribute a row —
|
|
117
|
+
// see CatalogModelSpec.tools and OpenAICompatCatalogEntry.modelToolOverrides.
|
|
118
|
+
const modelToolOverrides = Object.fromEntries(Object.entries(entry.models.catalog).flatMap(([modelId, spec]) => spec.tools !== undefined ? [[modelId, spec.tools]] : []));
|
|
116
119
|
const base = {
|
|
117
120
|
providerName: entry.id,
|
|
118
121
|
aliases: [entry.id, ...entry.aliases],
|
|
@@ -135,6 +138,9 @@ export function buildCatalogEntries() {
|
|
|
135
138
|
...(typeof entry.capabilities.tools === "boolean"
|
|
136
139
|
? { supportsTools: entry.capabilities.tools }
|
|
137
140
|
: {}),
|
|
141
|
+
...(Object.keys(modelToolOverrides).length > 0
|
|
142
|
+
? { modelToolOverrides }
|
|
143
|
+
: {}),
|
|
138
144
|
// Unlike `tools`, `capabilities.structuredOutputWithTools` is a plain
|
|
139
145
|
// required boolean in the schema (no "model-dependent" member), so this
|
|
140
146
|
// is a direct passthrough — `false` and `true` both carry meaning to
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"output": 2
|
|
20
20
|
},
|
|
21
21
|
"vision": true,
|
|
22
|
+
"tools": true,
|
|
22
23
|
"status": "production",
|
|
23
24
|
"description": "NeuroLink default. Listed as reka-flash in the baseline model table on https://docs.reka.ai/chat/models.md (intro: \"Our baseline models always available for public access are:\") and named as the model in the code examples on https://docs.reka.ai/chat/overview.md, https://docs.reka.ai/chat/function-calling.md and https://docs.reka.ai/chat/api-reference/create.md (retrieved 2026-09-29). https://docs.reka.ai/pricing.md has a row named \"Reka Flash\" described as \"Fast and cost-efficient model for most tasks\" with $0.80 under \"Input Tokens (per 1M)\" and $2.00 under \"Output Tokens (per 1M)\"; https://docs.reka.ai/chat/models.md ends with \"For full listing of models and pricing, see the Reka API Pricing page\". https://docs.reka.ai/chat/function-calling.md says \"Currently, only Reka Flash supports function calling.\" Vision is true: the first curl example on https://docs.reka.ai/overview.md sends an image_url content part to model \"reka-flash\", and the Quickstart's \"First request\" example (https://docs.reka.ai/quickstart.md) carries the comment '# or \"reka-flash\"' beside an image_url request. No context window or maximum output figure is set for this model; models.defaultContextWindow and models.defaultMaxOutputTokens apply."
|
|
24
25
|
},
|
|
@@ -28,6 +29,7 @@
|
|
|
28
29
|
"output": 0.1
|
|
29
30
|
},
|
|
30
31
|
"vision": true,
|
|
32
|
+
"tools": false,
|
|
31
33
|
"status": "production",
|
|
32
34
|
"description": "Listed as \"reka-edge (or reka-edge-2603)\" in the baseline model table on https://docs.reka.ai/chat/models.md and on https://docs.reka.ai/quickstart.md (retrieved 2026-09-29). The Quickstart's \"First request\" example sends an image_url content part to model \"reka-edge\", and the example response on https://docs.reka.ai/chat/api-reference/get.md lists the id \"reka-edge\". https://docs.reka.ai/pricing.md has a row named \"Reka Edge\" described as \"Compact model ideal for on-device execution\" with $0.10 under \"Input Tokens (per 1M)\" and $0.10 under \"Output Tokens (per 1M)\". https://docs.reka.ai/chat/function-calling.md says \"Currently, only Reka Flash supports function calling.\" No context window or maximum output figure is set for this model; models.defaultContextWindow and models.defaultMaxOutputTokens apply."
|
|
33
35
|
},
|
|
@@ -37,6 +39,7 @@
|
|
|
37
39
|
"output": 0.1
|
|
38
40
|
},
|
|
39
41
|
"vision": true,
|
|
42
|
+
"tools": false,
|
|
40
43
|
"status": "production",
|
|
41
44
|
"description": "Named in the same table row as reka-edge, \"reka-edge (or reka-edge-2603)\", on https://docs.reka.ai/chat/models.md and https://docs.reka.ai/quickstart.md, and shown as {\"id\": \"reka-edge-2603\"} in the example output of GET https://api.reka.ai/v1/models on https://docs.reka.ai/chat/models.md, which says \"All of these model IDs can be used as values for the Chat API's model parameter\" (retrieved 2026-09-29). Vision and prices are those of the row's reka-edge example and its \"Reka Edge\" pricing row (https://docs.reka.ai/quickstart.md, https://docs.reka.ai/pricing.md). No context window or maximum output figure is set for this model; models.defaultContextWindow and models.defaultMaxOutputTokens apply."
|
|
42
45
|
}
|
|
@@ -88,7 +91,7 @@
|
|
|
88
91
|
"3. Authentication: the vendor's REST examples send the header X-Api-Key (https://docs.reka.ai/chat/overview.md) and the OpenAPI file https://docs.reka.ai/openapi/api-reference.yaml declares one security scheme, an apiKey in the header X-Api-Key; the OpenAI Python SDK examples pass api_key=\"YOUR_API_KEY\" to the client (https://docs.reka.ai/quickstart.md)",
|
|
89
92
|
"4. Set {apiKeyEnvVar} in your .env file"
|
|
90
93
|
],
|
|
91
|
-
"description": "Docs-verified only: the vendor's GET https://api.reka.ai/v1/models needs a key (it answers HTTP 401 without one), so the three model ids come from https://docs.reka.ai/chat/models.md and the roster has not been checked; the entry is not yet live-verified. Endpoint https://api.reka.ai/v1: the Quickstart (https://docs.reka.ai/quickstart.md) says \"our API is fully OpenAI-compatible\" and shows the OpenAI Python SDK with base_url \"https://api.reka.ai/v1\"; https://docs.reka.ai/chat/overview.md shows POST https://api.reka.ai/v1/chat/completions with a messages array and reads the reply from response.choices[0].message.content. Unauthenticated GETs made 2026-09-29: /models answered HTTP 401 with the JSON body {\"error\":{\"message\":\"Unauthorized\",\"type\":\"authentication_error\",\"code\":null,\"param\":null}}; /chat/completions answered HTTP 405 with header allow: POST and the body {\"detail\":\"Method Not Allowed\"}. No POST request was sent. errorRules carry the statuses https://docs.reka.ai/errors.md lists (401 authentication_error, 429 rate_limit_error, 500 server_error) plus a pattern rule on the FAQ's \"P001: Insufficient Balance\" text. capabilities.streaming is true because https://docs.reka.ai/chat/overview.md says \"The Chat API supports streaming\". capabilities.tools is model-dependent because https://docs.reka.ai/chat/function-calling.md documents the tools parameter and tool_calls and says \"Currently, only Reka Flash supports function calling.\" capabilities.structuredOutput is false: the ChatRequest on https://docs.reka.ai/chat/api-reference/create.md lists messages, model, frequency_penalty, max_tokens, presence_penalty, seed, stop, stream, temperature, tool_choice, tools, top_k and top_p. capabilities.toolsWithStreaming and capabilities.structuredOutputWithTools are false because no request combining them was sent. capabilities.embeddings and capabilities.thinking are false. models.defaultContextWindow (16384) and models.defaultMaxOutputTokens (4096) are placeholders the vendor does not publish; the Chat API reference describes max_tokens as \"limited by the model's context length. Defaults to 1024.\" Each model's status field holds production, a placeholder for a value the catalog schema requires; it is not a vendor statement. billingPolicy holds free-tier, one of the three values the catalog schema allows (free-tier, free-with-card, no-free-tier). The vendor wording behind it: on https://platform.reka.ai/pricing the Pay as you go tile has a \"Start for free\" link to /sign-up and the page carries the text \"Free credits used\"; https://docs.reka.ai/pricing.md says \"Get started with no upfront costs.\"; https://docs.reka.ai/resources/faqs.md answers \"Do I need to pre-pay?\" with \"Yes. Add credits to your account before making requests.\" apiKeyFormat is null, so NeuroLink's key-format check is not applied."
|
|
94
|
+
"description": "Docs-verified only: the vendor's GET https://api.reka.ai/v1/models needs a key (it answers HTTP 401 without one), so the three model ids come from https://docs.reka.ai/chat/models.md and the roster has not been checked; the entry is not yet live-verified. Endpoint https://api.reka.ai/v1: the Quickstart (https://docs.reka.ai/quickstart.md) says \"our API is fully OpenAI-compatible\" and shows the OpenAI Python SDK with base_url \"https://api.reka.ai/v1\"; https://docs.reka.ai/chat/overview.md shows POST https://api.reka.ai/v1/chat/completions with a messages array and reads the reply from response.choices[0].message.content. Unauthenticated GETs made 2026-09-29: /models answered HTTP 401 with the JSON body {\"error\":{\"message\":\"Unauthorized\",\"type\":\"authentication_error\",\"code\":null,\"param\":null}}; /chat/completions answered HTTP 405 with header allow: POST and the body {\"detail\":\"Method Not Allowed\"}. No POST request was sent. errorRules carry the statuses https://docs.reka.ai/errors.md lists (401 authentication_error, 429 rate_limit_error, 500 server_error) plus a pattern rule on the FAQ's \"P001: Insufficient Balance\" text. capabilities.streaming is true because https://docs.reka.ai/chat/overview.md says \"The Chat API supports streaming\". capabilities.tools is model-dependent because https://docs.reka.ai/chat/function-calling.md documents the tools parameter and tool_calls and says \"Currently, only Reka Flash supports function calling.\" That sentence is why reka-flash's models.catalog entry sets tools: true and reka-edge/reka-edge-2603 set tools: false (see CatalogModelSpec.tools) instead of leaving every model to the provider-level model-dependent default. capabilities.structuredOutput is false: the ChatRequest on https://docs.reka.ai/chat/api-reference/create.md lists messages, model, frequency_penalty, max_tokens, presence_penalty, seed, stop, stream, temperature, tool_choice, tools, top_k and top_p. capabilities.toolsWithStreaming and capabilities.structuredOutputWithTools are false because no request combining them was sent. capabilities.embeddings and capabilities.thinking are false. models.defaultContextWindow (16384) and models.defaultMaxOutputTokens (4096) are placeholders the vendor does not publish; the Chat API reference describes max_tokens as \"limited by the model's context length. Defaults to 1024.\" Each model's status field holds production, a placeholder for a value the catalog schema requires; it is not a vendor statement. billingPolicy holds free-tier, one of the three values the catalog schema allows (free-tier, free-with-card, no-free-tier). The vendor wording behind it: on https://platform.reka.ai/pricing the Pay as you go tile has a \"Start for free\" link to /sign-up and the page carries the text \"Free credits used\"; https://docs.reka.ai/pricing.md says \"Get started with no upfront costs.\"; https://docs.reka.ai/resources/faqs.md answers \"Do I need to pre-pay?\" with \"Yes. Add credits to your account before making requests.\" apiKeyFormat is null, so NeuroLink's key-format check is not applied."
|
|
92
95
|
},
|
|
93
96
|
"evidence": {
|
|
94
97
|
"rosterVerified": {
|
|
@@ -46,6 +46,7 @@ export declare const providerCatalogJsonSchema: z.ZodObject<{
|
|
|
46
46
|
cachedInput: z.ZodOptional<z.ZodNumber>;
|
|
47
47
|
}, z.core.$strict>>;
|
|
48
48
|
vision: z.ZodBoolean;
|
|
49
|
+
tools: z.ZodOptional<z.ZodBoolean>;
|
|
49
50
|
status: z.ZodEnum<{
|
|
50
51
|
production: "production";
|
|
51
52
|
preview: "preview";
|
|
@@ -23,6 +23,7 @@ const catalogModelSpecSchema = z.strictObject({
|
|
|
23
23
|
maxOutputTokens: z.number().optional(),
|
|
24
24
|
pricingPerMTok: catalogPricingPerMTokSchema.optional(),
|
|
25
25
|
vision: z.boolean(),
|
|
26
|
+
tools: z.boolean().optional(),
|
|
26
27
|
status: catalogModelStatusSchema,
|
|
27
28
|
description: z.string(),
|
|
28
29
|
enumMember: z
|
|
@@ -62,6 +62,13 @@ export declare class ConfiguredOpenAICompatProvider extends OpenAIChatCompletion
|
|
|
62
62
|
* every unknown id. A vendor that declares tools: false must never receive
|
|
63
63
|
* a `tools` array — Mancer's free model rejects one with 400 — so the
|
|
64
64
|
* declaration wins here and the registry is only consulted otherwise.
|
|
65
|
+
* A per-model entry in `modelToolOverrides` (from a "model-dependent"
|
|
66
|
+
* provider whose catalog names which specific models accept tools — see
|
|
67
|
+
* CatalogModelSpec.tools) wins over all of this: it is the same kind of
|
|
68
|
+
* wire-probed fact as `supportsTools`, just scoped to one model instead of
|
|
69
|
+
* the whole provider, so it is checked first and can both grant and
|
|
70
|
+
* withhold tools regardless of what the provider-level flag says.
|
|
71
|
+
*
|
|
65
72
|
* Like the other entry-reading overrides above, this runs only after
|
|
66
73
|
* construction: BaseProvider merely closes over it.
|
|
67
74
|
*/
|
|
@@ -125,10 +125,21 @@ export class ConfiguredOpenAICompatProvider extends OpenAIChatCompletionsProvide
|
|
|
125
125
|
* every unknown id. A vendor that declares tools: false must never receive
|
|
126
126
|
* a `tools` array — Mancer's free model rejects one with 400 — so the
|
|
127
127
|
* declaration wins here and the registry is only consulted otherwise.
|
|
128
|
+
* A per-model entry in `modelToolOverrides` (from a "model-dependent"
|
|
129
|
+
* provider whose catalog names which specific models accept tools — see
|
|
130
|
+
* CatalogModelSpec.tools) wins over all of this: it is the same kind of
|
|
131
|
+
* wire-probed fact as `supportsTools`, just scoped to one model instead of
|
|
132
|
+
* the whole provider, so it is checked first and can both grant and
|
|
133
|
+
* withhold tools regardless of what the provider-level flag says.
|
|
134
|
+
*
|
|
128
135
|
* Like the other entry-reading overrides above, this runs only after
|
|
129
136
|
* construction: BaseProvider merely closes over it.
|
|
130
137
|
*/
|
|
131
138
|
supportsTools() {
|
|
139
|
+
const override = this.entry.modelToolOverrides?.[this.modelName];
|
|
140
|
+
if (override !== undefined) {
|
|
141
|
+
return override;
|
|
142
|
+
}
|
|
132
143
|
if (this.entry.supportsTools === false) {
|
|
133
144
|
return false;
|
|
134
145
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { asNumber, asNumberMap, asStringMap, isRecord } from "../guards.js";
|
|
2
|
+
/**
|
|
3
|
+
* Last-resort confidence, when no transport reported one.
|
|
4
|
+
*
|
|
5
|
+
* The peak probability is what a calibrated confidence approximates, but it is
|
|
6
|
+
* NOT the same number, so a threshold tuned against a vendor's calibrated
|
|
7
|
+
* figure does not transfer unexamined; an even distribution lands near 1/N
|
|
8
|
+
* rather than 0.
|
|
9
|
+
*/
|
|
10
|
+
function deriveConfidence(probabilities) {
|
|
11
|
+
const values = Object.values(probabilities);
|
|
12
|
+
return values.length > 0 ? Math.max(...values) : 0;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Validate one answer off the wire. Returns null rather than throwing so a
|
|
16
|
+
* single malformed answer degrades to "unanswered" instead of failing the
|
|
17
|
+
* whole batch — the batch may hold hundreds of usable answers.
|
|
18
|
+
*/
|
|
19
|
+
function parseDecisionAnswer(raw, reportedConfidence) {
|
|
20
|
+
if (!isRecord(raw)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
switch (raw.type) {
|
|
24
|
+
// "noul" is the System One wire's own spelling; "boolean" is the neutral
|
|
25
|
+
// one (Vercel's gateway), which renames both the type and the field.
|
|
26
|
+
// Accepting both keeps one parser for every transport.
|
|
27
|
+
case "noul":
|
|
28
|
+
case "boolean": {
|
|
29
|
+
const probability = asNumber(raw.noul) ?? asNumber(raw.probability);
|
|
30
|
+
return probability === undefined
|
|
31
|
+
? null
|
|
32
|
+
: { type: "boolean", probability };
|
|
33
|
+
}
|
|
34
|
+
case "choice": {
|
|
35
|
+
const probabilities = asNumberMap(raw.probabilities);
|
|
36
|
+
if (typeof raw.choice !== "string" || !probabilities) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
type: "choice",
|
|
41
|
+
choice: raw.choice,
|
|
42
|
+
confidence: asNumber(raw.confidence) ??
|
|
43
|
+
reportedConfidence ??
|
|
44
|
+
deriveConfidence(probabilities),
|
|
45
|
+
probabilities,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
case "score": {
|
|
49
|
+
const score = asNumber(raw.score);
|
|
50
|
+
const legend = asStringMap(raw.legend);
|
|
51
|
+
const probabilities = asNumberMap(raw.probabilities);
|
|
52
|
+
if (score === undefined || !legend || !probabilities) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
type: "score",
|
|
57
|
+
score,
|
|
58
|
+
confidence: asNumber(raw.confidence) ??
|
|
59
|
+
reportedConfidence ??
|
|
60
|
+
deriveConfidence(probabilities),
|
|
61
|
+
legend,
|
|
62
|
+
probabilities,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
default:
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The System One wire calls the yes/no primitive `noul`; this codebase and
|
|
71
|
+
* every SDK exposing it call it `boolean`.
|
|
72
|
+
*/
|
|
73
|
+
function encodeQuestion(question) {
|
|
74
|
+
if (question.type === "boolean") {
|
|
75
|
+
return {
|
|
76
|
+
type: "noul",
|
|
77
|
+
instructions: question.instructions,
|
|
78
|
+
...(question.criteria ? { criteria: question.criteria } : {}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return question;
|
|
82
|
+
}
|
|
83
|
+
export const systemOneDialect = {
|
|
84
|
+
name: "system-one",
|
|
85
|
+
encodeQuestion,
|
|
86
|
+
readAnswers: (decoded, reportedConfidence) => {
|
|
87
|
+
const raw = decoded.answers;
|
|
88
|
+
if (!isRecord(raw)) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
const answers = {};
|
|
92
|
+
const dropped = [];
|
|
93
|
+
for (const [id, value] of Object.entries(raw)) {
|
|
94
|
+
const parsed = parseDecisionAnswer(value, reportedConfidence[id]);
|
|
95
|
+
if (parsed) {
|
|
96
|
+
answers[id] = parsed;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
dropped.push(id);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { answers, dropped };
|
|
103
|
+
},
|
|
104
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const isRecord: (v: unknown) => v is Record<string, unknown>;
|
|
2
|
+
export declare const asNumber: (v: unknown) => number | undefined;
|
|
3
|
+
export declare const asNumberMap: (v: unknown) => Record<string, number> | undefined;
|
|
4
|
+
export declare const asStringMap: (v: unknown) => Record<string, string> | undefined;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2
|
+
export const asNumber = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
3
|
+
export const asNumberMap = (v) => {
|
|
4
|
+
if (!isRecord(v)) {
|
|
5
|
+
return undefined;
|
|
6
|
+
}
|
|
7
|
+
const out = {};
|
|
8
|
+
for (const [k, raw] of Object.entries(v)) {
|
|
9
|
+
const n = asNumber(raw);
|
|
10
|
+
if (n === undefined) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
out[k] = n;
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
};
|
|
17
|
+
export const asStringMap = (v) => {
|
|
18
|
+
if (!isRecord(v)) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
const out = {};
|
|
22
|
+
for (const [k, raw] of Object.entries(v)) {
|
|
23
|
+
if (typeof raw !== "string") {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
out[k] = raw;
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
};
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import type { AIProviderName } from "../constants/enums.js";
|
|
2
2
|
import { BaseProvider } from "../core/baseProvider.js";
|
|
3
3
|
import { ProviderError } from "../types/index.js";
|
|
4
|
-
import type { DecisionError, DecisionQuestion, DecisionRequest, DecisionResult, DecisionPreparedMedia, DecisionState, LanguageModel, StreamOptions, StreamResult, ValidationSchema } from "../types/index.js";
|
|
4
|
+
import type { DecisionDialect, DecisionError, DecisionQuestion, DecisionRequest, DecisionResult, DecisionPreparedMedia, DecisionState, LanguageModel, StreamOptions, StreamResult, ValidationSchema } from "../types/index.js";
|
|
5
5
|
/**
|
|
6
6
|
* Generous enough for a cold start (measured at 2.0–2.7s after idle on Jev)
|
|
7
7
|
* while still bounded. Every internal caller is fail-open, so this only bites
|
|
8
8
|
* on a genuine stall.
|
|
9
9
|
*/
|
|
10
10
|
export declare const DEFAULT_DECISION_TIMEOUT_MS = 5000;
|
|
11
|
-
export
|
|
12
|
-
export declare const asNumber: (v: unknown) => number | undefined;
|
|
11
|
+
export { asNumber, isRecord } from "./decide/guards.js";
|
|
13
12
|
/**
|
|
14
13
|
* FastAPI's request-validation envelope, `{"detail":[{loc, msg, input}]}`,
|
|
15
14
|
* reduced to its field paths and messages. `input` echoes the caller's own
|
|
@@ -73,6 +72,11 @@ export declare abstract class SystemOneDecisionProvider extends BaseProvider {
|
|
|
73
72
|
* every SDK exposing it call it `boolean`.
|
|
74
73
|
*/
|
|
75
74
|
protected encodeQuestion(question: DecisionQuestion): Record<string, unknown>;
|
|
75
|
+
/**
|
|
76
|
+
* The wire family this provider speaks. A provider on another layout
|
|
77
|
+
* overrides it; every one so far speaks System One.
|
|
78
|
+
*/
|
|
79
|
+
protected dialect(): DecisionDialect;
|
|
76
80
|
/**
|
|
77
81
|
* How long a rate-limited or unavailable response asks the caller to wait.
|
|
78
82
|
* Undefined keeps the default backoff. A wait longer than what is left of the
|
|
@@ -7,6 +7,8 @@ import { prepareDecisionMedia } from "../utils/decisionMedia.js";
|
|
|
7
7
|
import { logger } from "../utils/logger.js";
|
|
8
8
|
import { redactUrlsInText } from "../utils/logSanitize.js";
|
|
9
9
|
import { estimateDecisionStateTokens, resolveDecisionLimitsReading, } from "../utils/decisionLimits.js";
|
|
10
|
+
import { systemOneDialect } from "./decide/dialects/systemOne.js";
|
|
11
|
+
import { asNumber, isRecord } from "./decide/guards.js";
|
|
10
12
|
/**
|
|
11
13
|
* Generous enough for a cold start (measured at 2.0–2.7s after idle on Jev)
|
|
12
14
|
* while still bounded. Every internal caller is fail-open, so this only bites
|
|
@@ -14,102 +16,7 @@ import { estimateDecisionStateTokens, resolveDecisionLimitsReading, } from "../u
|
|
|
14
16
|
*/
|
|
15
17
|
export const DEFAULT_DECISION_TIMEOUT_MS = 5000;
|
|
16
18
|
const DEFAULT_MAX_RETRIES = 1;
|
|
17
|
-
export
|
|
18
|
-
export const asNumber = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
19
|
-
const asNumberMap = (v) => {
|
|
20
|
-
if (!isRecord(v)) {
|
|
21
|
-
return undefined;
|
|
22
|
-
}
|
|
23
|
-
const out = {};
|
|
24
|
-
for (const [k, raw] of Object.entries(v)) {
|
|
25
|
-
const n = asNumber(raw);
|
|
26
|
-
if (n === undefined) {
|
|
27
|
-
return undefined;
|
|
28
|
-
}
|
|
29
|
-
out[k] = n;
|
|
30
|
-
}
|
|
31
|
-
return out;
|
|
32
|
-
};
|
|
33
|
-
const asStringMap = (v) => {
|
|
34
|
-
if (!isRecord(v)) {
|
|
35
|
-
return undefined;
|
|
36
|
-
}
|
|
37
|
-
const out = {};
|
|
38
|
-
for (const [k, raw] of Object.entries(v)) {
|
|
39
|
-
if (typeof raw !== "string") {
|
|
40
|
-
return undefined;
|
|
41
|
-
}
|
|
42
|
-
out[k] = raw;
|
|
43
|
-
}
|
|
44
|
-
return out;
|
|
45
|
-
};
|
|
46
|
-
/**
|
|
47
|
-
* Last-resort confidence, when no transport reported one.
|
|
48
|
-
*
|
|
49
|
-
* The peak probability is what a calibrated confidence approximates, but it is
|
|
50
|
-
* NOT the same number, so a threshold tuned against a vendor's calibrated
|
|
51
|
-
* figure does not transfer unexamined; an even distribution lands near 1/N
|
|
52
|
-
* rather than 0.
|
|
53
|
-
*/
|
|
54
|
-
function deriveConfidence(probabilities) {
|
|
55
|
-
const values = Object.values(probabilities);
|
|
56
|
-
return values.length > 0 ? Math.max(...values) : 0;
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Validate one answer off the wire. Returns null rather than throwing so a
|
|
60
|
-
* single malformed answer degrades to "unanswered" instead of failing the
|
|
61
|
-
* whole batch — the batch may hold hundreds of usable answers.
|
|
62
|
-
*/
|
|
63
|
-
function parseDecisionAnswer(raw, reportedConfidence) {
|
|
64
|
-
if (!isRecord(raw)) {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
switch (raw.type) {
|
|
68
|
-
// "noul" is the System One wire's own spelling; "boolean" is the neutral
|
|
69
|
-
// one (Vercel's gateway), which renames both the type and the field.
|
|
70
|
-
// Accepting both keeps one parser for every transport.
|
|
71
|
-
case "noul":
|
|
72
|
-
case "boolean": {
|
|
73
|
-
const probability = asNumber(raw.noul) ?? asNumber(raw.probability);
|
|
74
|
-
return probability === undefined
|
|
75
|
-
? null
|
|
76
|
-
: { type: "boolean", probability };
|
|
77
|
-
}
|
|
78
|
-
case "choice": {
|
|
79
|
-
const probabilities = asNumberMap(raw.probabilities);
|
|
80
|
-
if (typeof raw.choice !== "string" || !probabilities) {
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
return {
|
|
84
|
-
type: "choice",
|
|
85
|
-
choice: raw.choice,
|
|
86
|
-
confidence: asNumber(raw.confidence) ??
|
|
87
|
-
reportedConfidence ??
|
|
88
|
-
deriveConfidence(probabilities),
|
|
89
|
-
probabilities,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
case "score": {
|
|
93
|
-
const score = asNumber(raw.score);
|
|
94
|
-
const legend = asStringMap(raw.legend);
|
|
95
|
-
const probabilities = asNumberMap(raw.probabilities);
|
|
96
|
-
if (score === undefined || !legend || !probabilities) {
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
return {
|
|
100
|
-
type: "score",
|
|
101
|
-
score,
|
|
102
|
-
confidence: asNumber(raw.confidence) ??
|
|
103
|
-
reportedConfidence ??
|
|
104
|
-
deriveConfidence(probabilities),
|
|
105
|
-
legend,
|
|
106
|
-
probabilities,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
default:
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
19
|
+
export { asNumber, isRecord } from "./decide/guards.js";
|
|
113
20
|
/**
|
|
114
21
|
* FastAPI's request-validation envelope, `{"detail":[{loc, msg, input}]}`,
|
|
115
22
|
* reduced to its field paths and messages. `input` echoes the caller's own
|
|
@@ -210,14 +117,14 @@ export class SystemOneDecisionProvider extends BaseProvider {
|
|
|
210
117
|
* every SDK exposing it call it `boolean`.
|
|
211
118
|
*/
|
|
212
119
|
encodeQuestion(question) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
return
|
|
120
|
+
return this.dialect().encodeQuestion(question);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The wire family this provider speaks. A provider on another layout
|
|
124
|
+
* overrides it; every one so far speaks System One.
|
|
125
|
+
*/
|
|
126
|
+
dialect() {
|
|
127
|
+
return systemOneDialect;
|
|
221
128
|
}
|
|
222
129
|
/**
|
|
223
130
|
* How long a rate-limited or unavailable response asks the caller to wait.
|
|
@@ -389,7 +296,10 @@ export class SystemOneDecisionProvider extends BaseProvider {
|
|
|
389
296
|
continue;
|
|
390
297
|
}
|
|
391
298
|
const decoded = this.readDecisionPayload(payload);
|
|
392
|
-
|
|
299
|
+
const reading = isRecord(decoded)
|
|
300
|
+
? this.dialect().readAnswers(decoded, this.reportedConfidence(decoded))
|
|
301
|
+
: undefined;
|
|
302
|
+
if (!isRecord(decoded) || !reading) {
|
|
393
303
|
throw this.decisionError({
|
|
394
304
|
kind: "server",
|
|
395
305
|
message: `${label} returned a response without an answers map.`,
|
|
@@ -398,19 +308,12 @@ export class SystemOneDecisionProvider extends BaseProvider {
|
|
|
398
308
|
retryable: false,
|
|
399
309
|
});
|
|
400
310
|
}
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if (parsed) {
|
|
406
|
-
answers[id] = parsed;
|
|
407
|
-
}
|
|
408
|
-
else {
|
|
409
|
-
logger.warn(`${label}: dropped unparseable answer "${id}"`, {
|
|
410
|
-
requestId,
|
|
411
|
-
});
|
|
412
|
-
}
|
|
311
|
+
for (const id of reading.dropped) {
|
|
312
|
+
logger.warn(`${label}: dropped unparseable answer "${id}"`, {
|
|
313
|
+
requestId,
|
|
314
|
+
});
|
|
413
315
|
}
|
|
316
|
+
const answers = { ...reading.answers };
|
|
414
317
|
const usage = isRecord(decoded.usage) ? decoded.usage : {};
|
|
415
318
|
return {
|
|
416
319
|
// `resolvedModel`, not `this.modelName`: when the caller pinned a
|
package/dist/types/decision.d.ts
CHANGED
|
@@ -522,3 +522,20 @@ export type DecisionBooleanGate = {
|
|
|
522
522
|
/** Minimum {@link decisionBooleanConfidence} to act at all. Default 0.4. */
|
|
523
523
|
minConfidence?: number;
|
|
524
524
|
};
|
|
525
|
+
/** The wire family a decide provider speaks. */
|
|
526
|
+
export type DecideDialectName = "system-one";
|
|
527
|
+
export type DecisionAnswerReading = {
|
|
528
|
+
readonly answers: Readonly<Record<string, DecisionAnswer>>;
|
|
529
|
+
/** Ids of answers the server returned that could not be parsed. */
|
|
530
|
+
readonly dropped: readonly string[];
|
|
531
|
+
};
|
|
532
|
+
/**
|
|
533
|
+
* How questions are written to, and answers read from, a decide wire. One
|
|
534
|
+
* dialect serves every vendor that speaks the same layout.
|
|
535
|
+
*/
|
|
536
|
+
export type DecisionDialect = {
|
|
537
|
+
readonly name: DecideDialectName;
|
|
538
|
+
encodeQuestion: (question: DecisionQuestion) => Record<string, unknown>;
|
|
539
|
+
/** `undefined` means the response has no usable answers container. */
|
|
540
|
+
readAnswers: (decoded: Readonly<Record<string, unknown>>, reportedConfidence: Readonly<Record<string, number>>) => DecisionAnswerReading | undefined;
|
|
541
|
+
};
|
|
@@ -27,6 +27,17 @@ export type CatalogModelSpec = {
|
|
|
27
27
|
maxOutputTokens?: number;
|
|
28
28
|
pricingPerMTok?: CatalogPricingPerMTok;
|
|
29
29
|
vision: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Per-model override of the provider-level `capabilities.tools`, for a
|
|
32
|
+
* "model-dependent" provider whose vendor docs name which specific models
|
|
33
|
+
* accept tool definitions (e.g. Reka: "Currently, only Reka Flash
|
|
34
|
+
* supports function calling"). Absent means "inherit the provider-level
|
|
35
|
+
* answer", same as every model before this field existed. Only meaningful
|
|
36
|
+
* when `capabilities.tools` is `"model-dependent"` — a provider that
|
|
37
|
+
* already declares a plain `true`/`false` has no need for it, since that
|
|
38
|
+
* single value already applies to every model it serves.
|
|
39
|
+
*/
|
|
40
|
+
tools?: boolean;
|
|
30
41
|
status: CatalogModelStatus;
|
|
31
42
|
description: string;
|
|
32
43
|
/**
|
|
@@ -123,13 +134,18 @@ export type CatalogEvidence = {
|
|
|
123
134
|
export type CatalogCapabilities = {
|
|
124
135
|
text: boolean;
|
|
125
136
|
streaming: boolean;
|
|
126
|
-
/** "model-dependent" when tool support varies per served model
|
|
127
|
-
* vendor doesn't reject `tools` for unsupported ones (the model just
|
|
128
|
-
* never emits tool_calls) — e.g. HuggingFace's router. Maps to
|
|
137
|
+
/** "model-dependent" when tool support varies per served model. Maps to
|
|
129
138
|
* ProviderDescriptor.toolSupport's own "model-dependent" member and
|
|
130
|
-
* leaves OpenAICompatCatalogEntry.supportsTools unset so
|
|
131
|
-
* ConfiguredOpenAICompatProvider falls
|
|
132
|
-
*
|
|
139
|
+
* leaves OpenAICompatCatalogEntry.supportsTools unset, so
|
|
140
|
+
* ConfiguredOpenAICompatProvider.supportsTools() falls back to whichever
|
|
141
|
+
* of its two remaining sources applies: a model whose own
|
|
142
|
+
* models.catalog[id].tools is set (see CatalogModelSpec.tools) answers
|
|
143
|
+
* from that; any other model falls through further, to the
|
|
144
|
+
* model-registry default (true for an unregistered id) — the same
|
|
145
|
+
* behavior every model had before CatalogModelSpec.tools existed, and
|
|
146
|
+
* still correct for a vendor that doesn't reject `tools` for an
|
|
147
|
+
* unsupported model (the model just never emits tool_calls), e.g.
|
|
148
|
+
* HuggingFace's router. */
|
|
133
149
|
tools: boolean | "model-dependent";
|
|
134
150
|
toolsWithStreaming: boolean;
|
|
135
151
|
structuredOutput: boolean;
|
|
@@ -1043,6 +1043,17 @@ export type OpenAICompatCatalogEntry = {
|
|
|
1043
1043
|
* same default every hand-written provider uses.
|
|
1044
1044
|
*/
|
|
1045
1045
|
supportsTools?: boolean;
|
|
1046
|
+
/**
|
|
1047
|
+
* Per-model override of `supportsTools`, from catalog models whose entry
|
|
1048
|
+
* sets `tools` explicitly (CatalogModelSpec.tools) — keyed by the model id
|
|
1049
|
+
* exactly as it appears in `models.catalog`. Checked first by
|
|
1050
|
+
* ConfiguredOpenAICompatProvider.supportsTools(), so it can both grant and
|
|
1051
|
+
* withhold tools regardless of what `supportsTools` says; a model id
|
|
1052
|
+
* absent from this map falls through to `supportsTools` unchanged. Built
|
|
1053
|
+
* only from models that declare `tools`, so a provider with no per-model
|
|
1054
|
+
* overrides gets no field here at all (not an empty object).
|
|
1055
|
+
*/
|
|
1056
|
+
modelToolOverrides?: Record<string, boolean>;
|
|
1046
1057
|
/**
|
|
1047
1058
|
* Whether the vendor accepts native tool definitions and `response_format`
|
|
1048
1059
|
* in the SAME request, from the catalog's
|