smoltalk 0.14.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -0
- package/dist/decide.d.ts +91 -0
- package/dist/decide.js +223 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/model.js +4 -3
- package/dist/models.d.ts +17 -1
- package/dist/models.js +22 -0
- package/dist/types.d.ts +5 -1
- package/dist/types.js +2 -1
- package/dist/util/provider.d.ts +2 -0
- package/dist/util/provider.js +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -548,6 +548,96 @@ same way and needs `smoltalk-llama-cpp` >= 0.5.0; the vector is computed in
|
|
|
548
548
|
process. See that package's README for the embedding caveats (dimension
|
|
549
549
|
truncation, one model file per role).
|
|
550
550
|
|
|
551
|
+
## Decision models (Jev, Laya)
|
|
552
|
+
|
|
553
|
+
A decision model does not write text. You send it a state and a map of typed
|
|
554
|
+
questions, and it answers every question in one pass with probabilities.
|
|
555
|
+
TypeSafe's Jev is one. Laya is an open-weights model that speaks the same
|
|
556
|
+
protocol.
|
|
557
|
+
|
|
558
|
+
```typescript
|
|
559
|
+
import { decide } from "smoltalk";
|
|
560
|
+
|
|
561
|
+
const r = await decide(
|
|
562
|
+
{ subject: "Refund not received", body: "I cancelled two weeks ago..." },
|
|
563
|
+
{
|
|
564
|
+
department: {
|
|
565
|
+
type: "choice",
|
|
566
|
+
instructions: "Which team should handle this?",
|
|
567
|
+
criteria: { billing: "payments, refunds", support: "help, bugs" },
|
|
568
|
+
},
|
|
569
|
+
urgency: {
|
|
570
|
+
type: "score",
|
|
571
|
+
instructions: "How urgent?",
|
|
572
|
+
criteria: ["not urgent", "urgent", "critical"],
|
|
573
|
+
},
|
|
574
|
+
churn: { type: "noul", instructions: "Likely to cancel?" },
|
|
575
|
+
},
|
|
576
|
+
{ model: "jev-latest" },
|
|
577
|
+
);
|
|
578
|
+
if (r.success) {
|
|
579
|
+
r.value.answers.department; // { type: "choice", choice: "billing", confidence: 0.86, probabilities: {...} }
|
|
580
|
+
r.value.answers.urgency; // { type: "score", score: 1.2, confidence: 0.6, legend: {...}, probabilities: {...} }
|
|
581
|
+
r.value.answers.churn; // { type: "noul", noul: 0.1 }
|
|
582
|
+
}
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
The three question types:
|
|
586
|
+
|
|
587
|
+
- `noul` is a yes/no question. The answer is the probability of yes.
|
|
588
|
+
- `choice` picks one option from `criteria`, a map of option key to
|
|
589
|
+
description. The answer is the key, a confidence, and a probability per
|
|
590
|
+
option.
|
|
591
|
+
- `score` places the state on `criteria`, an ordered list of level
|
|
592
|
+
descriptions. The answer is an expected level, which may be fractional, a
|
|
593
|
+
confidence, a legend, and a probability per level.
|
|
594
|
+
|
|
595
|
+
The key comes from `config.apiKey.typesafe` or `TYPESAFE_API_KEY`. Cost is
|
|
596
|
+
priced from the registry entry of the model you asked for, so a model the
|
|
597
|
+
registry does not know has no cost.
|
|
598
|
+
|
|
599
|
+
To use a Laya server, run `laya-serve` and point the provider at it. Laya
|
|
600
|
+
needs no key, but the provider requires one, so pass any value:
|
|
601
|
+
|
|
602
|
+
```typescript
|
|
603
|
+
import { decide } from "smoltalk";
|
|
604
|
+
|
|
605
|
+
const r = await decide(
|
|
606
|
+
"I was charged twice for my subscription.",
|
|
607
|
+
{ refund: { type: "noul", instructions: "Is the customer asking for money back?" } },
|
|
608
|
+
{
|
|
609
|
+
model: "laya",
|
|
610
|
+
provider: "typesafe",
|
|
611
|
+
apiKey: { typesafe: "unused" },
|
|
612
|
+
baseUrl: { typesafe: "http://localhost:8000" },
|
|
613
|
+
},
|
|
614
|
+
);
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
`provider: "typesafe"` is required for any model name the registry does not
|
|
618
|
+
know. It says "this endpoint speaks the decision protocol".
|
|
619
|
+
|
|
620
|
+
OpenRouter and Vercel AI Gateway also serve Jev over this protocol, so no
|
|
621
|
+
TypeSafe account is needed. OpenRouter's model name, `jev-1.13`, is in the
|
|
622
|
+
registry:
|
|
623
|
+
|
|
624
|
+
```typescript
|
|
625
|
+
import { decide } from "smoltalk";
|
|
626
|
+
|
|
627
|
+
const r = await decide(
|
|
628
|
+
"I was charged twice for my subscription.",
|
|
629
|
+
{ refund: { type: "noul", instructions: "Is the customer asking for money back?" } },
|
|
630
|
+
{
|
|
631
|
+
model: "jev-1.13",
|
|
632
|
+
apiKey: { typesafe: process.env.OPENROUTER_API_KEY },
|
|
633
|
+
baseUrl: { typesafe: "https://openrouter.ai/api" },
|
|
634
|
+
},
|
|
635
|
+
);
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
Vercel's base URL is `https://ai-gateway.vercel.sh/typesafe` and its model
|
|
639
|
+
name is `typesafe-ai/jev`, which needs `provider: "typesafe"`.
|
|
640
|
+
|
|
551
641
|
## Audio (STT/TTS)
|
|
552
642
|
|
|
553
643
|
Three audio primitives. `transcribe()` (speech-to-text) and `speak()`
|
package/dist/decide.d.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { ModelDataBlob } from "./modelData.js";
|
|
2
|
+
import { Result } from "./types/result.js";
|
|
3
|
+
import { TokenUsage } from "./types/tokenUsage.js";
|
|
4
|
+
import { CostEstimate } from "./types/costEstimate.js";
|
|
5
|
+
export declare const DECISION_PROVIDER = "typesafe";
|
|
6
|
+
export type NoulQuestion = {
|
|
7
|
+
type: "noul";
|
|
8
|
+
instructions: string;
|
|
9
|
+
criteria?: {
|
|
10
|
+
true: string;
|
|
11
|
+
false: string;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
export type ChoiceQuestion = {
|
|
15
|
+
type: "choice";
|
|
16
|
+
instructions: string;
|
|
17
|
+
/** Option key to its description. */
|
|
18
|
+
criteria: Record<string, string>;
|
|
19
|
+
};
|
|
20
|
+
export type ScoreQuestion = {
|
|
21
|
+
type: "score";
|
|
22
|
+
instructions: string;
|
|
23
|
+
/** Ordered level descriptions, lowest first. At least two. */
|
|
24
|
+
criteria: string[];
|
|
25
|
+
};
|
|
26
|
+
export type DecisionQuestion = NoulQuestion | ChoiceQuestion | ScoreQuestion;
|
|
27
|
+
export type NoulAnswer = {
|
|
28
|
+
type: "noul";
|
|
29
|
+
/** Probability that the answer is yes, 0 to 1. */
|
|
30
|
+
noul: number;
|
|
31
|
+
};
|
|
32
|
+
export type ChoiceAnswer = {
|
|
33
|
+
type: "choice";
|
|
34
|
+
choice: string;
|
|
35
|
+
confidence: number;
|
|
36
|
+
/** Option key to probability. */
|
|
37
|
+
probabilities: Record<string, number>;
|
|
38
|
+
};
|
|
39
|
+
export type ScoreAnswer = {
|
|
40
|
+
type: "score";
|
|
41
|
+
/** Expected level. May be fractional. */
|
|
42
|
+
score: number;
|
|
43
|
+
confidence: number;
|
|
44
|
+
/** Level index to description. */
|
|
45
|
+
legend: Record<string, string>;
|
|
46
|
+
/** Level index to probability. */
|
|
47
|
+
probabilities: Record<string, number>;
|
|
48
|
+
};
|
|
49
|
+
export type DecisionAnswer = NoulAnswer | ChoiceAnswer | ScoreAnswer;
|
|
50
|
+
/** What the model reads before answering: text, or a JSON value. */
|
|
51
|
+
export type DecisionState = string | object;
|
|
52
|
+
export type DecideConfig = {
|
|
53
|
+
model: string;
|
|
54
|
+
/** Required when the model is not in the registry. */
|
|
55
|
+
provider?: string;
|
|
56
|
+
/** API keys, nested by provider. Falls back to TYPESAFE_API_KEY. */
|
|
57
|
+
apiKey?: {
|
|
58
|
+
typesafe?: string;
|
|
59
|
+
[provider: string]: string | undefined;
|
|
60
|
+
};
|
|
61
|
+
/** Custom base URLs, nested by provider. Falls back to TYPESAFE_BASE_URL,
|
|
62
|
+
* then TypeSafe's host. Point `typesafe` at a Laya server to use Laya. */
|
|
63
|
+
baseUrl?: {
|
|
64
|
+
typesafe?: string;
|
|
65
|
+
[provider: string]: string | undefined;
|
|
66
|
+
};
|
|
67
|
+
/** Refreshed model data to layer over the baked-in registry. */
|
|
68
|
+
modelData?: ModelDataBlob;
|
|
69
|
+
abortSignal?: AbortSignal;
|
|
70
|
+
};
|
|
71
|
+
export type DecideResult = {
|
|
72
|
+
answers: Record<string, DecisionAnswer>;
|
|
73
|
+
usage: TokenUsage;
|
|
74
|
+
cost?: CostEstimate;
|
|
75
|
+
/** The versioned model the server reports, e.g. `jev-1.13`. */
|
|
76
|
+
model: string;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Ask a decision model one or more typed questions about a state.
|
|
80
|
+
*
|
|
81
|
+
* ```ts
|
|
82
|
+
* const r = await decide(ticket, {
|
|
83
|
+
* department: { type: "choice", instructions: "Which team?", criteria: { billing: "refunds", support: "bugs" } },
|
|
84
|
+
* churn: { type: "noul", instructions: "Likely to cancel?" },
|
|
85
|
+
* }, { model: "jev-latest" });
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* Cost is priced from the registry entry of the **requested** model, so a
|
|
89
|
+
* model the registry does not know, such as a Laya checkpoint, has no cost.
|
|
90
|
+
*/
|
|
91
|
+
export declare function decide(state: DecisionState, questions: Record<string, DecisionQuestion>, config: DecideConfig): Promise<Result<DecideResult>>;
|
package/dist/decide.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision models: typed questions in, probabilities out.
|
|
3
|
+
*
|
|
4
|
+
* A decision model such as TypeSafe's Jev, or the open-weights Laya, does not
|
|
5
|
+
* generate text. You send it a state (text or JSON) and a map of questions,
|
|
6
|
+
* and it answers every question in one forward pass. There are three question
|
|
7
|
+
* types: a yes/no `noul`, a `choice` from a fixed set of options, and a
|
|
8
|
+
* `score` on an ordered rubric. Every answer carries probabilities.
|
|
9
|
+
*
|
|
10
|
+
* `decide()` follows the shape of `embed()`: payload first, config last,
|
|
11
|
+
* provider and key and base URL resolved through the shared helpers. There
|
|
12
|
+
* is one provider, `typesafe`, which is the wire protocol. A Laya server
|
|
13
|
+
* speaks the same protocol, so it is reached by setting `baseUrl.typesafe`.
|
|
14
|
+
*/
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
import { success, failure } from "./types/result.js";
|
|
17
|
+
import { resolveProvider, resolveApiKey, resolveBaseUrl } from "./util/provider.js";
|
|
18
|
+
import { isDecisionModel, resolveModelForProvider } from "./models.js";
|
|
19
|
+
import { round } from "./util/util.js";
|
|
20
|
+
export const DECISION_PROVIDER = "typesafe";
|
|
21
|
+
const NoulAnswerSchema = z.object({
|
|
22
|
+
type: z.literal("noul"),
|
|
23
|
+
noul: z.number(),
|
|
24
|
+
});
|
|
25
|
+
const ChoiceAnswerSchema = z.object({
|
|
26
|
+
type: z.literal("choice"),
|
|
27
|
+
choice: z.string(),
|
|
28
|
+
confidence: z.number(),
|
|
29
|
+
probabilities: z.record(z.string(), z.number()),
|
|
30
|
+
});
|
|
31
|
+
const ScoreAnswerSchema = z.object({
|
|
32
|
+
type: z.literal("score"),
|
|
33
|
+
score: z.number(),
|
|
34
|
+
confidence: z.number(),
|
|
35
|
+
legend: z.record(z.string(), z.string()),
|
|
36
|
+
probabilities: z.record(z.string(), z.number()),
|
|
37
|
+
});
|
|
38
|
+
const DecisionAnswerSchema = z.discriminatedUnion("type", [
|
|
39
|
+
NoulAnswerSchema,
|
|
40
|
+
ChoiceAnswerSchema,
|
|
41
|
+
ScoreAnswerSchema,
|
|
42
|
+
]);
|
|
43
|
+
const ResponseSchema = z.object({
|
|
44
|
+
model: z.string().optional(),
|
|
45
|
+
answers: z.record(z.string(), DecisionAnswerSchema),
|
|
46
|
+
usage: z
|
|
47
|
+
.object({
|
|
48
|
+
input_tokens: z.number(),
|
|
49
|
+
output_tokens: z.number().optional(),
|
|
50
|
+
})
|
|
51
|
+
.optional(),
|
|
52
|
+
});
|
|
53
|
+
function errorMessage(err) {
|
|
54
|
+
if (err instanceof Error) {
|
|
55
|
+
return err.message;
|
|
56
|
+
}
|
|
57
|
+
return String(err);
|
|
58
|
+
}
|
|
59
|
+
/** A problem with the questions that no server would accept. Checked before
|
|
60
|
+
* any request so a bad question map never costs a round trip. */
|
|
61
|
+
function checkQuestions(questions, maxQuestions) {
|
|
62
|
+
const names = Object.keys(questions);
|
|
63
|
+
if (names.length === 0) {
|
|
64
|
+
return "No questions given. A decision request needs at least one question.";
|
|
65
|
+
}
|
|
66
|
+
if (maxQuestions !== undefined && names.length > maxQuestions) {
|
|
67
|
+
return `Too many questions: ${names.length} given, the model accepts at most ${maxQuestions} per request.`;
|
|
68
|
+
}
|
|
69
|
+
for (const name of names) {
|
|
70
|
+
const q = questions[name];
|
|
71
|
+
if (q.type === "choice" && Object.keys(q.criteria).length < 2) {
|
|
72
|
+
return `Question "${name}" is a choice with fewer than two options.`;
|
|
73
|
+
}
|
|
74
|
+
if (q.type === "score" && q.criteria.length < 2) {
|
|
75
|
+
return `Question "${name}" is a score with fewer than two levels.`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
function isProbability(n) {
|
|
81
|
+
return n >= 0 && n <= 1;
|
|
82
|
+
}
|
|
83
|
+
/** A response that parsed but does not answer what was asked. The Zod
|
|
84
|
+
* schema only says each field is a number or a string; this checks the
|
|
85
|
+
* numbers are probabilities and the answer fits the question it is for. */
|
|
86
|
+
function checkAnswers(questions, answers) {
|
|
87
|
+
for (const name of Object.keys(questions)) {
|
|
88
|
+
const q = questions[name];
|
|
89
|
+
const a = answers[name];
|
|
90
|
+
if (a === undefined) {
|
|
91
|
+
return `The response has no answer for question "${name}".`;
|
|
92
|
+
}
|
|
93
|
+
if (a.type !== q.type) {
|
|
94
|
+
return `Question "${name}" is a ${q.type} but the answer is a ${a.type}.`;
|
|
95
|
+
}
|
|
96
|
+
if (a.type === "noul" && !isProbability(a.noul)) {
|
|
97
|
+
return `Question "${name}" has a noul of ${a.noul}, which is not between 0 and 1.`;
|
|
98
|
+
}
|
|
99
|
+
if ((a.type === "choice" || a.type === "score") && !isProbability(a.confidence)) {
|
|
100
|
+
return `Question "${name}" has a confidence of ${a.confidence}, which is not between 0 and 1.`;
|
|
101
|
+
}
|
|
102
|
+
if (q.type === "choice" && a.type === "choice") {
|
|
103
|
+
if (!(a.choice in q.criteria)) {
|
|
104
|
+
return `Question "${name}" answered "${a.choice}", which is not one of its options.`;
|
|
105
|
+
}
|
|
106
|
+
const unknown = Object.keys(a.probabilities).find((k) => !(k in q.criteria));
|
|
107
|
+
if (unknown !== undefined) {
|
|
108
|
+
return `Question "${name}" has a probability for "${unknown}", which is not one of its options.`;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (q.type === "score" && a.type === "score") {
|
|
112
|
+
const levels = q.criteria.length;
|
|
113
|
+
if (Object.keys(a.legend).length !== levels) {
|
|
114
|
+
return `Question "${name}" has ${levels} levels but the answer's legend has ${Object.keys(a.legend).length}.`;
|
|
115
|
+
}
|
|
116
|
+
if (Object.keys(a.probabilities).length !== levels) {
|
|
117
|
+
return `Question "${name}" has ${levels} levels but the answer has ${Object.keys(a.probabilities).length} probabilities.`;
|
|
118
|
+
}
|
|
119
|
+
if (a.score < 0 || a.score > levels - 1) {
|
|
120
|
+
return `Question "${name}" has a score of ${a.score}, outside its ${levels} levels.`;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
function calculateDecisionCost(model, inputTokens) {
|
|
127
|
+
if (model === undefined || model.inputTokenCost === undefined) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
const inputCost = round((inputTokens * model.inputTokenCost) / 1_000_000, 6);
|
|
131
|
+
return { inputCost, outputCost: 0, totalCost: inputCost, currency: "USD" };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Ask a decision model one or more typed questions about a state.
|
|
135
|
+
*
|
|
136
|
+
* ```ts
|
|
137
|
+
* const r = await decide(ticket, {
|
|
138
|
+
* department: { type: "choice", instructions: "Which team?", criteria: { billing: "refunds", support: "bugs" } },
|
|
139
|
+
* churn: { type: "noul", instructions: "Likely to cancel?" },
|
|
140
|
+
* }, { model: "jev-latest" });
|
|
141
|
+
* ```
|
|
142
|
+
*
|
|
143
|
+
* Cost is priced from the registry entry of the **requested** model, so a
|
|
144
|
+
* model the registry does not know, such as a Laya checkpoint, has no cost.
|
|
145
|
+
*/
|
|
146
|
+
export async function decide(state, questions, config) {
|
|
147
|
+
let provider;
|
|
148
|
+
try {
|
|
149
|
+
provider = resolveProvider(config.model, config.provider, config.modelData);
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
return failure(errorMessage(err));
|
|
153
|
+
}
|
|
154
|
+
if (provider !== DECISION_PROVIDER) {
|
|
155
|
+
return failure(`Provider "${provider}" does not answer decisions. Only "${DECISION_PROVIDER}" does; set config.provider to it for a model the registry does not know.`);
|
|
156
|
+
}
|
|
157
|
+
// The registry entry for the requested name, when there is one. It sets
|
|
158
|
+
// the question cap and the price. A Laya model has no entry and no price.
|
|
159
|
+
const found = resolveModelForProvider(provider, config.model, config.modelData);
|
|
160
|
+
const registryModel = found && isDecisionModel(found) ? found : undefined;
|
|
161
|
+
const questionProblem = checkQuestions(questions, registryModel?.maxQuestions);
|
|
162
|
+
if (questionProblem) {
|
|
163
|
+
return failure(questionProblem);
|
|
164
|
+
}
|
|
165
|
+
const apiKey = resolveApiKey(provider, config);
|
|
166
|
+
if (!apiKey) {
|
|
167
|
+
return failure("No TypeSafe API key provided. Set config.apiKey.typesafe or the TYPESAFE_API_KEY environment variable.");
|
|
168
|
+
}
|
|
169
|
+
const baseUrl = resolveBaseUrl(provider, config).replace(/\/+$/, "");
|
|
170
|
+
if (config.abortSignal?.aborted) {
|
|
171
|
+
return failure("Request was aborted");
|
|
172
|
+
}
|
|
173
|
+
let response;
|
|
174
|
+
try {
|
|
175
|
+
response = await fetch(`${baseUrl}/v1/systemone`, {
|
|
176
|
+
method: "POST",
|
|
177
|
+
headers: {
|
|
178
|
+
Authorization: `Bearer ${apiKey}`,
|
|
179
|
+
"Content-Type": "application/json",
|
|
180
|
+
},
|
|
181
|
+
body: JSON.stringify({ model: config.model, state, questions }),
|
|
182
|
+
signal: config.abortSignal,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
if (config.abortSignal?.aborted) {
|
|
187
|
+
return failure("Request was aborted");
|
|
188
|
+
}
|
|
189
|
+
return failure(`Decision request failed: ${errorMessage(err)}`);
|
|
190
|
+
}
|
|
191
|
+
const text = await response.text();
|
|
192
|
+
if (!response.ok) {
|
|
193
|
+
return failure(`Decision request failed with status ${response.status}: ${text}`);
|
|
194
|
+
}
|
|
195
|
+
let body;
|
|
196
|
+
try {
|
|
197
|
+
body = JSON.parse(text);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return failure(`Decision response is not JSON: ${text.slice(0, 200)}`);
|
|
201
|
+
}
|
|
202
|
+
const parsed = ResponseSchema.safeParse(body);
|
|
203
|
+
if (!parsed.success) {
|
|
204
|
+
const issue = parsed.error.issues[0];
|
|
205
|
+
const where = issue.path.length > 0 ? ` at ${issue.path.join(".")}` : "";
|
|
206
|
+
return failure(`Decision response has an unexpected shape${where}: ${issue.message}`);
|
|
207
|
+
}
|
|
208
|
+
const answers = parsed.data.answers;
|
|
209
|
+
const answerProblem = checkAnswers(questions, answers);
|
|
210
|
+
if (answerProblem) {
|
|
211
|
+
return failure(answerProblem);
|
|
212
|
+
}
|
|
213
|
+
// Output is free, so only input tokens are priced. The server's output
|
|
214
|
+
// count is still reported: Jev sends 0, but a gateway may count its own.
|
|
215
|
+
const inputTokens = parsed.data.usage?.input_tokens ?? 0;
|
|
216
|
+
const outputTokens = parsed.data.usage?.output_tokens ?? 0;
|
|
217
|
+
return success({
|
|
218
|
+
answers,
|
|
219
|
+
usage: { inputTokens, outputTokens },
|
|
220
|
+
cost: calculateDecisionCost(registryModel, inputTokens),
|
|
221
|
+
model: parsed.data.model ?? config.model,
|
|
222
|
+
});
|
|
223
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export { loadLlamaCpp } from "./clients/llamaCppLoader.js";
|
|
|
13
13
|
export type { LlamaCppModule } from "./clients/llamaCppLoader.js";
|
|
14
14
|
export * from "./classes/ToolCall.js";
|
|
15
15
|
export * from "./embed.js";
|
|
16
|
+
export * from "./decide.js";
|
|
17
|
+
export { resolveProvider } from "./util/provider.js";
|
|
16
18
|
export * from "./image.js";
|
|
17
19
|
export { uploadFile, deleteFile, registerFileProvider, DEFAULT_UPLOAD_BYTES } from "./files.js";
|
|
18
20
|
export type { UploadFileOptions, FileProviderContext, FileProvider } from "./files.js";
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,8 @@ export * from "./functions.js";
|
|
|
13
13
|
export { loadLlamaCpp } from "./clients/llamaCppLoader.js";
|
|
14
14
|
export * from "./classes/ToolCall.js";
|
|
15
15
|
export * from "./embed.js";
|
|
16
|
+
export * from "./decide.js";
|
|
17
|
+
export { resolveProvider } from "./util/provider.js";
|
|
16
18
|
export * from "./image.js";
|
|
17
19
|
// Explicit (not `export *`) so the test-only `_resetForTests` stays off the public surface.
|
|
18
20
|
export { uploadFile, deleteFile, registerFileProvider, DEFAULT_UPLOAD_BYTES } from "./files.js";
|
package/dist/model.js
CHANGED
|
@@ -36,9 +36,10 @@ export class Model {
|
|
|
36
36
|
return null;
|
|
37
37
|
}
|
|
38
38
|
// This token engine prices text generation and token-billed audio models
|
|
39
|
-
// (e.g. Gemini TTS). Image and
|
|
40
|
-
// so they are never priced here even if they carry text-token
|
|
41
|
-
|
|
39
|
+
// (e.g. Gemini TTS). Image, embeddings, and decision models have their own
|
|
40
|
+
// cost paths, so they are never priced here even if they carry text-token
|
|
41
|
+
// rates.
|
|
42
|
+
if (model.type === "image" || model.type === "embeddings" || model.type === "decision") {
|
|
42
43
|
return null;
|
|
43
44
|
}
|
|
44
45
|
// BaseModel token-rate fields, read structurally across the model union.
|
package/dist/models.d.ts
CHANGED
|
@@ -115,7 +115,21 @@ export type EmbeddingsModel = {
|
|
|
115
115
|
provider: string;
|
|
116
116
|
tokenCost?: number;
|
|
117
117
|
};
|
|
118
|
-
|
|
118
|
+
/**
|
|
119
|
+
* A decision model answers typed questions about a state with probabilities
|
|
120
|
+
* instead of generating text. See `lib/decide.ts`.
|
|
121
|
+
*/
|
|
122
|
+
export type DecisionModel = {
|
|
123
|
+
type: "decision";
|
|
124
|
+
modelName: string;
|
|
125
|
+
provider: string;
|
|
126
|
+
description?: string;
|
|
127
|
+
/** Cost per 1M input tokens, in dollars. Output is free. */
|
|
128
|
+
inputTokenCost?: number;
|
|
129
|
+
/** The most questions one request may carry. */
|
|
130
|
+
maxQuestions?: number;
|
|
131
|
+
};
|
|
132
|
+
export type ModelType = SpeechToTextModel | TextToSpeechModel | TextModel | EmbeddingsModel | ImageModel | DecisionModel;
|
|
119
133
|
export declare const speechToTextModels: readonly [{
|
|
120
134
|
readonly type: "speech-to-text";
|
|
121
135
|
readonly modelName: "whisper-1";
|
|
@@ -1943,6 +1957,7 @@ export declare const imageModels: readonly [{
|
|
|
1943
1957
|
readonly costPerImage: 0.034;
|
|
1944
1958
|
}];
|
|
1945
1959
|
export declare const embeddingsModels: EmbeddingsModel[];
|
|
1960
|
+
export declare const decisionModels: DecisionModel[];
|
|
1946
1961
|
export type TextModelName = (typeof textModels)[number]["modelName"];
|
|
1947
1962
|
export type ImageModelName = (typeof imageModels)[number]["modelName"];
|
|
1948
1963
|
export type SpeechToTextModelName = (typeof speechToTextModels)[number]["modelName"];
|
|
@@ -2006,4 +2021,5 @@ export declare function audioInputConstraints(model: ModelType): {
|
|
|
2006
2021
|
supportedMimeTypes?: readonly string[];
|
|
2007
2022
|
};
|
|
2008
2023
|
export declare function isEmbeddingsModel(model: ModelType): model is EmbeddingsModel;
|
|
2024
|
+
export declare function isDecisionModel(model: ModelType): model is DecisionModel;
|
|
2009
2025
|
export declare const ModelNameSchema: z.ZodString;
|
package/dist/models.js
CHANGED
|
@@ -1962,6 +1962,24 @@ export const embeddingsModels = [
|
|
|
1962
1962
|
tokenCost: 0.2,
|
|
1963
1963
|
},
|
|
1964
1964
|
];
|
|
1965
|
+
export const decisionModels = [
|
|
1966
|
+
{
|
|
1967
|
+
type: "decision",
|
|
1968
|
+
modelName: "jev-latest",
|
|
1969
|
+
provider: "typesafe",
|
|
1970
|
+
description: "TypeSafe's System One model. Answers yes/no, choice, and score questions about a state with calibrated probabilities. Does not generate text.",
|
|
1971
|
+
inputTokenCost: 0.042,
|
|
1972
|
+
maxQuestions: 64,
|
|
1973
|
+
},
|
|
1974
|
+
{
|
|
1975
|
+
type: "decision",
|
|
1976
|
+
modelName: "jev-1.13",
|
|
1977
|
+
provider: "typesafe",
|
|
1978
|
+
description: "Jev 1.13, the name OpenRouter serves it under (baseUrl.typesafe = https://openrouter.ai/api). Same price and protocol as jev-latest.",
|
|
1979
|
+
inputTokenCost: 0.042,
|
|
1980
|
+
maxQuestions: 64,
|
|
1981
|
+
},
|
|
1982
|
+
];
|
|
1965
1983
|
export const hostedTools = [
|
|
1966
1984
|
{
|
|
1967
1985
|
name: "web_search",
|
|
@@ -2102,6 +2120,7 @@ function baselineModels() {
|
|
|
2102
2120
|
...textToSpeechModels,
|
|
2103
2121
|
...registeredTextModels,
|
|
2104
2122
|
...embeddingsModels,
|
|
2123
|
+
...decisionModels,
|
|
2105
2124
|
];
|
|
2106
2125
|
}
|
|
2107
2126
|
/**
|
|
@@ -2261,6 +2280,9 @@ export function audioInputConstraints(model) {
|
|
|
2261
2280
|
export function isEmbeddingsModel(model) {
|
|
2262
2281
|
return model.type === "embeddings";
|
|
2263
2282
|
}
|
|
2283
|
+
export function isDecisionModel(model) {
|
|
2284
|
+
return model.type === "decision";
|
|
2285
|
+
}
|
|
2264
2286
|
export const ModelNameSchema = z
|
|
2265
2287
|
.string()
|
|
2266
2288
|
.regex(/^[a-zA-Z0-9._:@/-]+$/, "Model name must only contain letters, numbers, dots, underscores, hyphens, colons, slashes, and @");
|
package/dist/types.d.ts
CHANGED
|
@@ -162,8 +162,12 @@ export type PromptResult = {
|
|
|
162
162
|
stopReason?: StopReason;
|
|
163
163
|
/** The untouched provider finish/stop-reason value (e.g. `end_turn`, `MAX_TOKENS`). */
|
|
164
164
|
rawStopReason?: string;
|
|
165
|
+
/** Whatever the provider returned beyond the text, for callers that want
|
|
166
|
+
* it on the assistant message. A decision model puts its full answers
|
|
167
|
+
* with probabilities here. */
|
|
168
|
+
rawData?: unknown;
|
|
165
169
|
};
|
|
166
|
-
export declare function promptResult({ output, toolCalls, thinkingBlocks, usage, cost, model, hostedToolResults, stopReason, rawStopReason, }: Partial<PromptResult>): PromptResult;
|
|
170
|
+
export declare function promptResult({ output, toolCalls, thinkingBlocks, usage, cost, model, hostedToolResults, stopReason, rawStopReason, rawData, }: Partial<PromptResult>): PromptResult;
|
|
167
171
|
export type StreamChunk = {
|
|
168
172
|
type: "text";
|
|
169
173
|
text: string;
|
package/dist/types.js
CHANGED
|
@@ -4,7 +4,7 @@ import z from "zod";
|
|
|
4
4
|
export * from "./types/costEstimate.js";
|
|
5
5
|
export * from "./types/tokenUsage.js";
|
|
6
6
|
export * from "./types/stopReason.js";
|
|
7
|
-
export function promptResult({ output, toolCalls, thinkingBlocks, usage, cost, model, hostedToolResults, stopReason, rawStopReason, }) {
|
|
7
|
+
export function promptResult({ output, toolCalls, thinkingBlocks, usage, cost, model, hostedToolResults, stopReason, rawStopReason, rawData, }) {
|
|
8
8
|
return {
|
|
9
9
|
output: output || null,
|
|
10
10
|
toolCalls: toolCalls || [],
|
|
@@ -15,6 +15,7 @@ export function promptResult({ output, toolCalls, thinkingBlocks, usage, cost, m
|
|
|
15
15
|
hostedToolResults,
|
|
16
16
|
stopReason,
|
|
17
17
|
rawStopReason,
|
|
18
|
+
rawData,
|
|
18
19
|
};
|
|
19
20
|
}
|
|
20
21
|
export const ThinkingBlockSchema = z.object({
|
package/dist/util/provider.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ type NestedKeyConfig = {
|
|
|
20
20
|
liteLlm?: string;
|
|
21
21
|
openAiCompat?: string;
|
|
22
22
|
groq?: string;
|
|
23
|
+
typesafe?: string;
|
|
23
24
|
/** Arbitrary provider names, for keys targeting a custom-registered provider. */
|
|
24
25
|
[provider: string]: string | undefined;
|
|
25
26
|
};
|
|
@@ -30,6 +31,7 @@ type NestedKeyConfig = {
|
|
|
30
31
|
liteLlm?: string;
|
|
31
32
|
openAiCompat?: string;
|
|
32
33
|
mlx?: string;
|
|
34
|
+
typesafe?: string;
|
|
33
35
|
};
|
|
34
36
|
};
|
|
35
37
|
/**
|
package/dist/util/provider.js
CHANGED
|
@@ -39,6 +39,8 @@ export function resolveApiKey(provider, config) {
|
|
|
39
39
|
return k?.openAiCompat || process.env.OPENAI_COMPAT_API_KEY;
|
|
40
40
|
case "groq":
|
|
41
41
|
return k?.groq || process.env.GROQ_API_KEY;
|
|
42
|
+
case "typesafe":
|
|
43
|
+
return k?.typesafe || process.env.TYPESAFE_API_KEY;
|
|
42
44
|
default:
|
|
43
45
|
return config.apiKey?.[provider];
|
|
44
46
|
}
|
|
@@ -66,6 +68,9 @@ export function resolveBaseUrl(provider, config) {
|
|
|
66
68
|
return b?.openAiCompat || process.env.OPENAI_COMPAT_BASE_URL;
|
|
67
69
|
case "mlx":
|
|
68
70
|
return b?.mlx || process.env.MLX_BASE_URL || "http://127.0.0.1:8080/v1";
|
|
71
|
+
case "typesafe":
|
|
72
|
+
// A Laya server (`laya-serve`) speaks the same protocol; point this at it.
|
|
73
|
+
return b?.typesafe || process.env.TYPESAFE_BASE_URL || "https://api.typesafe.ai";
|
|
69
74
|
default:
|
|
70
75
|
return undefined;
|
|
71
76
|
}
|