@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.
@@ -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
@@ -158,6 +158,9 @@
158
158
  "vision": {
159
159
  "type": "boolean"
160
160
  },
161
+ "tools": {
162
+ "type": "boolean"
163
+ },
161
164
  "status": {
162
165
  "type": "string",
163
166
  "enum": ["production", "preview", "retired"]
@@ -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,2 @@
1
+ import type { DecideDialectName, DecisionDialect } from "../../../types/index.js";
2
+ export declare const getDecisionDialect: (name: DecideDialectName) => DecisionDialect;
@@ -0,0 +1,5 @@
1
+ import { systemOneDialect } from "./systemOne.js";
2
+ const DIALECTS = {
3
+ "system-one": systemOneDialect,
4
+ };
5
+ export const getDecisionDialect = (name) => DIALECTS[name];
@@ -0,0 +1,2 @@
1
+ import type { DecisionDialect } from "../../../types/index.js";
2
+ export declare const systemOneDialect: DecisionDialect;
@@ -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 declare const isRecord: (v: unknown) => v is Record<string, unknown>;
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 const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
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
- if (question.type === "boolean") {
214
- return {
215
- type: "noul",
216
- instructions: question.instructions,
217
- ...(question.criteria ? { criteria: question.criteria } : {}),
218
- };
219
- }
220
- return question;
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
- if (!isRecord(decoded) || !isRecord(decoded.answers)) {
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 reported = this.reportedConfidence(decoded);
402
- const answers = {};
403
- for (const [id, raw] of Object.entries(decoded.answers)) {
404
- const parsed = parseDecisionAnswer(raw, reported[id]);
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
@@ -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 and the
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 through to the model-registry
132
- * default, exactly like an entry that never set supportsTools at all. */
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