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 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";
@@ -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({
@@ -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.2",
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": [