smoltalk 0.14.1 → 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 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()`
@@ -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 embeddings models have their own cost paths,
40
- // so they are never priced here even if they carry text-token rates.
41
- if (model.type === "image" || model.type === "embeddings") {
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
- export type ModelType = SpeechToTextModel | TextToSpeechModel | TextModel | EmbeddingsModel | ImageModel;
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";
@@ -176,6 +190,13 @@ export declare const textToSpeechModels: readonly [{
176
190
  readonly perCharacterCost: 0.00004;
177
191
  readonly maxInputChars: 200;
178
192
  readonly formats: readonly ["wav"];
193
+ }, {
194
+ readonly type: "text-to-speech";
195
+ readonly modelName: "gemini-3.1-flash-tts-preview";
196
+ readonly provider: "google";
197
+ readonly inputTokenCost: 1;
198
+ readonly outputAudioTokenCost: 20;
199
+ readonly formats: readonly ["pcm", "wav"];
179
200
  }, {
180
201
  readonly type: "text-to-speech";
181
202
  readonly modelName: "gemini-2.5-flash-preview-tts";
@@ -688,7 +709,7 @@ export declare const textModels: readonly [{
688
709
  readonly inputTokenCost: 5;
689
710
  readonly cachedInputTokenCost: 0.5;
690
711
  readonly outputTokenCost: 22.5;
691
- readonly thresholdTokens: 200000;
712
+ readonly thresholdTokens: 272000;
692
713
  };
693
714
  readonly reasoning: {
694
715
  readonly levels: readonly ["none", "low", "medium", "high", "xhigh"];
@@ -778,7 +799,7 @@ export declare const textModels: readonly [{
778
799
  readonly longContext: {
779
800
  readonly inputTokenCost: 60;
780
801
  readonly outputTokenCost: 270;
781
- readonly thresholdTokens: 200000;
802
+ readonly thresholdTokens: 272000;
782
803
  };
783
804
  readonly reasoning: {
784
805
  readonly levels: readonly ["medium", "high", "xhigh"];
@@ -813,7 +834,7 @@ export declare const textModels: readonly [{
813
834
  readonly inputTokenCost: 10;
814
835
  readonly cachedInputTokenCost: 1;
815
836
  readonly outputTokenCost: 45;
816
- readonly thresholdTokens: 200000;
837
+ readonly thresholdTokens: 272000;
817
838
  };
818
839
  readonly reasoning: {
819
840
  readonly levels: readonly ["none", "low", "medium", "high", "xhigh"];
@@ -845,7 +866,7 @@ export declare const textModels: readonly [{
845
866
  readonly longContext: {
846
867
  readonly inputTokenCost: 60;
847
868
  readonly outputTokenCost: 270;
848
- readonly thresholdTokens: 200000;
869
+ readonly thresholdTokens: 272000;
849
870
  };
850
871
  readonly reasoning: {
851
872
  readonly levels: readonly ["none", "low", "medium", "high", "xhigh"];
@@ -866,6 +887,41 @@ export declare const textModels: readonly [{
866
887
  readonly structuredOutput: true;
867
888
  readonly temperatureSupported: false;
868
889
  readonly provider: "openai-responses";
890
+ }, {
891
+ readonly type: "text";
892
+ readonly modelName: "gpt-6-astra";
893
+ readonly description: "GPT-6 Astra is OpenAI's most capable model, built for the hardest end-to-end work: complex reasoning, coding, computer use, research, and document creation. 1M context window. Standard pricing for ≤272K input tokens; prompts above that are billed at 2x input/cache and 1.5x output for the whole request. Knowledge cutoff: April 2026.";
894
+ readonly maxInputTokens: 1050000;
895
+ readonly maxOutputTokens: 128000;
896
+ readonly inputTokenCost: 10;
897
+ readonly cachedInputTokenCost: 1;
898
+ readonly outputTokenCost: 50;
899
+ readonly outputTokensPerSecond: 69;
900
+ readonly longContext: {
901
+ readonly inputTokenCost: 20;
902
+ readonly cachedInputTokenCost: 2;
903
+ readonly outputTokenCost: 75;
904
+ readonly thresholdTokens: 272000;
905
+ };
906
+ readonly reasoning: {
907
+ readonly levels: readonly ["low", "medium", "high", "xhigh", "max"];
908
+ readonly defaultLevel: "medium";
909
+ readonly canDisable: false;
910
+ readonly outputsThinking: false;
911
+ readonly outputsSignatures: false;
912
+ };
913
+ readonly modalities: {
914
+ readonly input: readonly ["text", "image", "pdf"];
915
+ readonly output: readonly ["text"];
916
+ };
917
+ readonly knowledge: "2026-04-30";
918
+ readonly releaseDate: "2026-09-04";
919
+ readonly lastUpdated: "2026-09-04";
920
+ readonly family: "gpt";
921
+ readonly openWeights: false;
922
+ readonly structuredOutput: true;
923
+ readonly temperatureSupported: false;
924
+ readonly provider: "openai";
869
925
  }, {
870
926
  readonly type: "text";
871
927
  readonly modelName: "gpt-5.6-sol";
@@ -875,12 +931,12 @@ export declare const textModels: readonly [{
875
931
  readonly inputTokenCost: 4;
876
932
  readonly cachedInputTokenCost: 0.4;
877
933
  readonly outputTokenCost: 20;
878
- readonly outputTokensPerSecond: 71;
934
+ readonly outputTokensPerSecond: 77;
879
935
  readonly longContext: {
880
936
  readonly inputTokenCost: 8;
881
937
  readonly cachedInputTokenCost: 0.8;
882
938
  readonly outputTokenCost: 30;
883
- readonly thresholdTokens: 200000;
939
+ readonly thresholdTokens: 272000;
884
940
  };
885
941
  readonly reasoning: {
886
942
  readonly levels: readonly ["none", "low", "medium", "high", "xhigh", "max"];
@@ -910,12 +966,12 @@ export declare const textModels: readonly [{
910
966
  readonly inputTokenCost: 2;
911
967
  readonly cachedInputTokenCost: 0.2;
912
968
  readonly outputTokenCost: 12;
913
- readonly outputTokensPerSecond: 98;
969
+ readonly outputTokensPerSecond: 106;
914
970
  readonly longContext: {
915
971
  readonly inputTokenCost: 4;
916
972
  readonly cachedInputTokenCost: 0.4;
917
973
  readonly outputTokenCost: 18;
918
- readonly thresholdTokens: 200000;
974
+ readonly thresholdTokens: 272000;
919
975
  };
920
976
  readonly reasoning: {
921
977
  readonly levels: readonly ["none", "low", "medium", "high", "xhigh", "max"];
@@ -945,12 +1001,12 @@ export declare const textModels: readonly [{
945
1001
  readonly inputTokenCost: 0.2;
946
1002
  readonly cachedInputTokenCost: 0.02;
947
1003
  readonly outputTokenCost: 1.2;
948
- readonly outputTokensPerSecond: 116;
1004
+ readonly outputTokensPerSecond: 165;
949
1005
  readonly longContext: {
950
1006
  readonly inputTokenCost: 0.4;
951
1007
  readonly cachedInputTokenCost: 0.04;
952
1008
  readonly outputTokenCost: 1.8;
953
- readonly thresholdTokens: 200000;
1009
+ readonly thresholdTokens: 272000;
954
1010
  };
955
1011
  readonly reasoning: {
956
1012
  readonly levels: readonly ["none", "low", "medium", "high", "xhigh", "max"];
@@ -980,7 +1036,7 @@ export declare const textModels: readonly [{
980
1036
  readonly inputTokenCost: 2;
981
1037
  readonly cachedInputTokenCost: 0.2;
982
1038
  readonly outputTokenCost: 12;
983
- readonly outputTokensPerSecond: 133;
1039
+ readonly outputTokensPerSecond: 124;
984
1040
  readonly longContext: {
985
1041
  readonly inputTokenCost: 4;
986
1042
  readonly cachedInputTokenCost: 0.4;
@@ -1043,7 +1099,7 @@ export declare const textModels: readonly [{
1043
1099
  readonly inputTokenCost: 0.75;
1044
1100
  readonly cachedInputTokenCost: 0.075;
1045
1101
  readonly outputTokenCost: 3.75;
1046
- readonly outputTokensPerSecond: 312;
1102
+ readonly outputTokensPerSecond: 329;
1047
1103
  readonly inputAudioTokenCost: 1.5;
1048
1104
  readonly reasoning: {
1049
1105
  readonly levels: readonly ["low", "medium", "high"];
@@ -1455,6 +1511,7 @@ export declare const textModels: readonly [{
1455
1511
  readonly cachedInputTokenCost: 0.25;
1456
1512
  readonly cacheCreationInputTokenCost: 12.5;
1457
1513
  readonly outputTokenCost: 50;
1514
+ readonly outputTokensPerSecond: 69;
1458
1515
  readonly reasoning: {
1459
1516
  readonly thinkingStyle: "adaptive";
1460
1517
  readonly levels: readonly ["low", "medium", "high", "xhigh", "max"];
@@ -1484,6 +1541,7 @@ export declare const textModels: readonly [{
1484
1541
  readonly cachedInputTokenCost: 0.5;
1485
1542
  readonly cacheCreationInputTokenCost: 6.25;
1486
1543
  readonly outputTokenCost: 25;
1544
+ readonly outputTokensPerSecond: 59;
1487
1545
  readonly reasoning: {
1488
1546
  readonly thinkingStyle: "adaptive";
1489
1547
  readonly levels: readonly ["low", "medium", "high", "xhigh", "max"];
@@ -1625,7 +1683,7 @@ export declare const textModels: readonly [{
1625
1683
  readonly cachedInputTokenCost: 0.2;
1626
1684
  readonly cacheCreationInputTokenCost: 2.5;
1627
1685
  readonly outputTokenCost: 10;
1628
- readonly outputTokensPerSecond: 71;
1686
+ readonly outputTokensPerSecond: 81;
1629
1687
  readonly reasoning: {
1630
1688
  readonly thinkingStyle: "adaptive";
1631
1689
  readonly levels: readonly ["low", "medium", "high", "xhigh", "max"];
@@ -1899,6 +1957,7 @@ export declare const imageModels: readonly [{
1899
1957
  readonly costPerImage: 0.034;
1900
1958
  }];
1901
1959
  export declare const embeddingsModels: EmbeddingsModel[];
1960
+ export declare const decisionModels: DecisionModel[];
1902
1961
  export type TextModelName = (typeof textModels)[number]["modelName"];
1903
1962
  export type ImageModelName = (typeof imageModels)[number]["modelName"];
1904
1963
  export type SpeechToTextModelName = (typeof speechToTextModels)[number]["modelName"];
@@ -1962,4 +2021,5 @@ export declare function audioInputConstraints(model: ModelType): {
1962
2021
  supportedMimeTypes?: readonly string[];
1963
2022
  };
1964
2023
  export declare function isEmbeddingsModel(model: ModelType): model is EmbeddingsModel;
2024
+ export declare function isDecisionModel(model: ModelType): model is DecisionModel;
1965
2025
  export declare const ModelNameSchema: z.ZodString;
package/dist/models.js CHANGED
@@ -92,6 +92,14 @@ export const textToSpeechModels = [
92
92
  },
93
93
  // Gemini TTS is token-billed (text input + audio output). No maxInputChars:
94
94
  // Gemini documents a 32k-token context, and characters are not a sound proxy.
95
+ {
96
+ type: "text-to-speech",
97
+ modelName: "gemini-3.1-flash-tts-preview",
98
+ provider: "google",
99
+ inputTokenCost: 1.0, // $/1M text-input tokens, verified 2026-09-21
100
+ outputAudioTokenCost: 20.0, // $/1M audio-output tokens
101
+ formats: ["pcm", "wav"],
102
+ },
95
103
  {
96
104
  type: "text-to-speech",
97
105
  modelName: "gemini-2.5-flash-preview-tts",
@@ -626,7 +634,7 @@ export const textModels = [
626
634
  inputTokenCost: 5,
627
635
  cachedInputTokenCost: 0.5,
628
636
  outputTokenCost: 22.5,
629
- thresholdTokens: 200000,
637
+ thresholdTokens: 272000,
630
638
  },
631
639
  reasoning: {
632
640
  levels: ["none", "low", "medium", "high", "xhigh"],
@@ -719,7 +727,7 @@ export const textModels = [
719
727
  longContext: {
720
728
  inputTokenCost: 60,
721
729
  outputTokenCost: 270,
722
- thresholdTokens: 200000,
730
+ thresholdTokens: 272000,
723
731
  },
724
732
  reasoning: {
725
733
  levels: ["medium", "high", "xhigh"],
@@ -755,7 +763,7 @@ export const textModels = [
755
763
  inputTokenCost: 10,
756
764
  cachedInputTokenCost: 1,
757
765
  outputTokenCost: 45,
758
- thresholdTokens: 200000,
766
+ thresholdTokens: 272000,
759
767
  },
760
768
  reasoning: {
761
769
  levels: ["none", "low", "medium", "high", "xhigh"],
@@ -788,7 +796,7 @@ export const textModels = [
788
796
  longContext: {
789
797
  inputTokenCost: 60,
790
798
  outputTokenCost: 270,
791
- thresholdTokens: 200000,
799
+ thresholdTokens: 272000,
792
800
  },
793
801
  reasoning: {
794
802
  levels: ["none", "low", "medium", "high", "xhigh"],
@@ -810,6 +818,42 @@ export const textModels = [
810
818
  temperatureSupported: false,
811
819
  provider: "openai-responses",
812
820
  },
821
+ {
822
+ type: "text",
823
+ modelName: "gpt-6-astra",
824
+ description: "GPT-6 Astra is OpenAI's most capable model, built for the hardest end-to-end work: complex reasoning, coding, computer use, research, and document creation. 1M context window. Standard pricing for ≤272K input tokens; prompts above that are billed at 2x input/cache and 1.5x output for the whole request. Knowledge cutoff: April 2026.",
825
+ maxInputTokens: 1050000,
826
+ maxOutputTokens: 128000,
827
+ inputTokenCost: 10,
828
+ cachedInputTokenCost: 1,
829
+ outputTokenCost: 50,
830
+ outputTokensPerSecond: 69,
831
+ longContext: {
832
+ inputTokenCost: 20,
833
+ cachedInputTokenCost: 2,
834
+ outputTokenCost: 75,
835
+ thresholdTokens: 272000,
836
+ },
837
+ reasoning: {
838
+ levels: ["low", "medium", "high", "xhigh", "max"],
839
+ defaultLevel: "medium",
840
+ canDisable: false,
841
+ outputsThinking: false,
842
+ outputsSignatures: false,
843
+ },
844
+ modalities: {
845
+ input: ["text", "image", "pdf"],
846
+ output: ["text"],
847
+ },
848
+ knowledge: "2026-04-30",
849
+ releaseDate: "2026-09-04",
850
+ lastUpdated: "2026-09-04",
851
+ family: "gpt",
852
+ openWeights: false,
853
+ structuredOutput: true,
854
+ temperatureSupported: false,
855
+ provider: "openai",
856
+ },
813
857
  {
814
858
  type: "text",
815
859
  modelName: "gpt-5.6-sol",
@@ -819,12 +863,12 @@ export const textModels = [
819
863
  inputTokenCost: 4,
820
864
  cachedInputTokenCost: 0.4,
821
865
  outputTokenCost: 20,
822
- outputTokensPerSecond: 71,
866
+ outputTokensPerSecond: 77,
823
867
  longContext: {
824
868
  inputTokenCost: 8,
825
869
  cachedInputTokenCost: 0.8,
826
870
  outputTokenCost: 30,
827
- thresholdTokens: 200000,
871
+ thresholdTokens: 272000,
828
872
  },
829
873
  reasoning: {
830
874
  levels: ["none", "low", "medium", "high", "xhigh", "max"],
@@ -855,12 +899,12 @@ export const textModels = [
855
899
  inputTokenCost: 2,
856
900
  cachedInputTokenCost: 0.2,
857
901
  outputTokenCost: 12,
858
- outputTokensPerSecond: 98,
902
+ outputTokensPerSecond: 106,
859
903
  longContext: {
860
904
  inputTokenCost: 4,
861
905
  cachedInputTokenCost: 0.4,
862
906
  outputTokenCost: 18,
863
- thresholdTokens: 200000,
907
+ thresholdTokens: 272000,
864
908
  },
865
909
  reasoning: {
866
910
  levels: ["none", "low", "medium", "high", "xhigh", "max"],
@@ -891,12 +935,12 @@ export const textModels = [
891
935
  inputTokenCost: 0.2,
892
936
  cachedInputTokenCost: 0.02,
893
937
  outputTokenCost: 1.2,
894
- outputTokensPerSecond: 116,
938
+ outputTokensPerSecond: 165,
895
939
  longContext: {
896
940
  inputTokenCost: 0.4,
897
941
  cachedInputTokenCost: 0.04,
898
942
  outputTokenCost: 1.8,
899
- thresholdTokens: 200000,
943
+ thresholdTokens: 272000,
900
944
  },
901
945
  reasoning: {
902
946
  levels: ["none", "low", "medium", "high", "xhigh", "max"],
@@ -927,7 +971,7 @@ export const textModels = [
927
971
  inputTokenCost: 2,
928
972
  cachedInputTokenCost: 0.2,
929
973
  outputTokenCost: 12,
930
- outputTokensPerSecond: 133,
974
+ outputTokensPerSecond: 124,
931
975
  longContext: {
932
976
  inputTokenCost: 4,
933
977
  cachedInputTokenCost: 0.4,
@@ -992,7 +1036,7 @@ export const textModels = [
992
1036
  inputTokenCost: 0.75,
993
1037
  cachedInputTokenCost: 0.075,
994
1038
  outputTokenCost: 3.75,
995
- outputTokensPerSecond: 312,
1039
+ outputTokensPerSecond: 329,
996
1040
  inputAudioTokenCost: 1.5,
997
1041
  reasoning: {
998
1042
  levels: ["low", "medium", "high"],
@@ -1424,6 +1468,7 @@ export const textModels = [
1424
1468
  cachedInputTokenCost: 0.25,
1425
1469
  cacheCreationInputTokenCost: 12.5,
1426
1470
  outputTokenCost: 50,
1471
+ outputTokensPerSecond: 69,
1427
1472
  reasoning: {
1428
1473
  thinkingStyle: "adaptive",
1429
1474
  levels: ["low", "medium", "high", "xhigh", "max"],
@@ -1454,6 +1499,7 @@ export const textModels = [
1454
1499
  cachedInputTokenCost: 0.5,
1455
1500
  cacheCreationInputTokenCost: 6.25,
1456
1501
  outputTokenCost: 25,
1502
+ outputTokensPerSecond: 59,
1457
1503
  reasoning: {
1458
1504
  thinkingStyle: "adaptive",
1459
1505
  levels: ["low", "medium", "high", "xhigh", "max"],
@@ -1600,7 +1646,7 @@ export const textModels = [
1600
1646
  cachedInputTokenCost: 0.2,
1601
1647
  cacheCreationInputTokenCost: 2.5,
1602
1648
  outputTokenCost: 10,
1603
- outputTokensPerSecond: 71,
1649
+ outputTokensPerSecond: 81,
1604
1650
  reasoning: {
1605
1651
  thinkingStyle: "adaptive",
1606
1652
  levels: ["low", "medium", "high", "xhigh", "max"],
@@ -1916,6 +1962,24 @@ export const embeddingsModels = [
1916
1962
  tokenCost: 0.2,
1917
1963
  },
1918
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
+ ];
1919
1983
  export const hostedTools = [
1920
1984
  {
1921
1985
  name: "web_search",
@@ -1939,7 +2003,7 @@ export const hostedTools = [
1939
2003
  category: "code_execution",
1940
2004
  description: "Run code in a sandboxed container.",
1941
2005
  providerToolId: "code_execution",
1942
- pricing: { unit: "per_hour", amount: 0.05, freeAllowance: "50 container-hours/day", note: "Free when used with web_search or web_fetch." },
2006
+ pricing: { unit: "per_hour", amount: 0.05, freeAllowance: "1,550 container-hours/month", note: "Free when used with web_search or web_fetch. 5-minute minimum execution time per container." },
1943
2007
  },
1944
2008
  {
1945
2009
  name: "web_search",
@@ -2014,7 +2078,12 @@ export const hostedTools = [
2014
2078
  description: "Grounding with Google Maps (Gemini 3 only).",
2015
2079
  providerToolId: "google_maps",
2016
2080
  models: ["gemini-3-pro-preview", "gemini-3.1-pro-preview", "gemini-3-flash-preview", "gemini-3.5-flash", "gemini-3.6-flash", "gemini-3.7-flash", "gemini-3.8-flash", "gemini-3.1-flash-lite", "gemini-3.5-flash-lite"],
2017
- pricing: { unit: "per_call", note: "Gemini 3 family only; see Google pricing." },
2081
+ pricing: {
2082
+ unit: "per_call",
2083
+ amount: 0.014,
2084
+ freeAllowance: "5,000 grounded prompts/month (Gemini 3)",
2085
+ note: "$14 per 1,000 search queries on the Gemini 3 family.",
2086
+ },
2018
2087
  },
2019
2088
  {
2020
2089
  name: "web_search",
@@ -2051,6 +2120,7 @@ function baselineModels() {
2051
2120
  ...textToSpeechModels,
2052
2121
  ...registeredTextModels,
2053
2122
  ...embeddingsModels,
2123
+ ...decisionModels,
2054
2124
  ];
2055
2125
  }
2056
2126
  /**
@@ -2210,6 +2280,9 @@ export function audioInputConstraints(model) {
2210
2280
  export function isEmbeddingsModel(model) {
2211
2281
  return model.type === "embeddings";
2212
2282
  }
2283
+ export function isDecisionModel(model) {
2284
+ return model.type === "decision";
2285
+ }
2213
2286
  export const ModelNameSchema = z
2214
2287
  .string()
2215
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({
@@ -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
  /**
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoltalk",
3
- "version": "0.14.1",
3
+ "version": "0.15.0",
4
4
  "description": "A common interface for LLM APIs",
5
5
  "homepage": "https://github.com/egonSchiele/smoltalk",
6
6
  "files": [