@providerkit/core 0.12.2 → 0.12.3

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/src/types.ts CHANGED
@@ -10,7 +10,10 @@
10
10
  /**
11
11
  * How hard the model thinks before answering. Absent = the provider's own
12
12
  * default (never sent). Passed through verbatim on OpenAI-shape
13
- * (`reasoning_effort`); mapped to thinking budgets on Anthropic-shape.
13
+ * (`reasoning_effort`). On Anthropic-shape, mapped to adaptive thinking and
14
+ * `output_config.effort` for current Claude models, where `none` is the lowest
15
+ * effort on a model that cannot stop thinking; and to thinking budgets for
16
+ * Claude 4.5 and older and for every other vendor on that wire.
14
17
  * Support varies per model — an unsupported level comes back as a clean 400.
15
18
  *
16
19
  * Ordered least → most; the type derives from the array so the runtime guard
@@ -39,7 +42,31 @@ export interface ImagePart {
39
42
  data: string;
40
43
  }
41
44
 
42
- export type ContentPart = TextPart | ImagePart;
45
+ /** A file's media type: anything but an image's, because an image is an
46
+ * `ImagePart`, which every adapter can send. */
47
+ export type FileMimeType =
48
+ `application/${string}` | `audio/${string}` | `video/${string}` | `text/${string}`;
49
+
50
+ /**
51
+ * A file the model reads — a PDF, a recording, a video — as base64 bytes,
52
+ * never a URL. Only the Gemini adapter sends one, as `inlineData`, the way it
53
+ * sends an image. Every other adapter refuses it with an `invalid`
54
+ * ProviderError before any request goes out, rather than drop it: a dropped
55
+ * attachment is a model answering about a file it never saw.
56
+ *
57
+ * Gemini takes inline data up to 100 MB per request, 50 MB for a PDF (Gemini
58
+ * API "File input methods", read 2026-09-27). The request carries the base64
59
+ * text, which is a third larger than the file. Past that limit the documented
60
+ * route is Gemini's File API, which this package does not wrap. Nothing here
61
+ * enforces the limit; Gemini answers an oversized request itself.
62
+ */
63
+ export interface FilePart {
64
+ type: "file";
65
+ mimeType: FileMimeType;
66
+ data: string;
67
+ }
68
+
69
+ export type ContentPart = TextPart | ImagePart | FilePart;
43
70
 
44
71
  /**
45
72
  * A tool the model asked to run. `arguments` is the RAW JSON string, not a
@@ -182,6 +209,83 @@ export interface JsonOutput {
182
209
  strict?: boolean;
183
210
  }
184
211
 
212
+ /**
213
+ * Read the JSON out of a model's answer. Models wrap it: a ```json fence, an
214
+ * unmarked fence, a sentence before it, notes after it. This takes the first
215
+ * fenced block marked `json` or not marked at all. With no such block, it
216
+ * takes the text from the first `{` or `[` to the bracket that closes it, and
217
+ * with no bracket either, the whole answer (a bare number, say). Then it
218
+ * parses that.
219
+ *
220
+ * It returns `unknown` on purpose: parsed is not validated, and a model's JSON
221
+ * has the schema's shape only once the caller has checked it.
222
+ *
223
+ * @throws {JsonAnswerError} when that text is not JSON. The error carries the
224
+ * answer, so a log shows what the model actually said.
225
+ */
226
+ export function parseJsonAnswer(text: string): unknown {
227
+ const candidate = fencedJson(text) ?? bracketed(text);
228
+ try {
229
+ return JSON.parse(candidate);
230
+ } catch (error) {
231
+ throw new JsonAnswerError(text, error);
232
+ }
233
+ }
234
+
235
+ /** An answer `parseJsonAnswer` found no JSON in. `text` is the answer, cut to
236
+ * 2,000 characters like a provider's error body; `cause` is the SyntaxError. */
237
+ export class JsonAnswerError extends Error {
238
+ readonly text: string;
239
+
240
+ constructor(text: string, cause: unknown) {
241
+ const reason = cause instanceof Error ? cause.message : String(cause);
242
+ super(`The model's answer is not JSON: ${reason}`, { cause });
243
+ this.name = "JsonAnswerError";
244
+ this.text = text.slice(0, 2_000);
245
+ }
246
+ }
247
+
248
+ /** A fenced block, whose fences count only at the start of a line. A JSON
249
+ * string can't hold a raw newline, so no line of JSON starts with a fence, and
250
+ * a value carrying one (a code sample) never ends the block early. `[^\S\n]`
251
+ * is a space, a tab, or the `\r` of a CRLF line. */
252
+ const FENCED_BLOCK = /^[^\S\n]*```[^\S\n]*([^\s`]*)[^\n]*\n([\s\S]*?)^[^\S\n]*```/gm;
253
+
254
+ function fencedJson(text: string): string | undefined {
255
+ for (const [, info = "", body = ""] of text.matchAll(FENCED_BLOCK)) {
256
+ const language = info.toLowerCase();
257
+ if (language === "" || language === "json") return body;
258
+ }
259
+ return undefined;
260
+ }
261
+
262
+ /** From the first `{` or `[` to the bracket that closes it, skipping brackets
263
+ * inside strings. An answer cut off before it closes keeps its tail, and
264
+ * JSON.parse says what is wrong with it.
265
+ *
266
+ * ponytail: the FIRST opener wins, so prose with a bracket before the JSON
267
+ * ("see [1]: {…}") picks the prose's. The upgrade is to try the next opener
268
+ * when one fails to parse, once a model that writes that turns up. */
269
+ function bracketed(text: string): string {
270
+ const start = text.search(/[[{]/);
271
+ if (start === -1) return text;
272
+ let depth = 0;
273
+ let inString = false;
274
+ for (let i = start; i < text.length; i++) {
275
+ const char = text[i];
276
+ if (inString) {
277
+ if (char === "\\") i++;
278
+ else if (char === '"') inString = false;
279
+ } else if (char === '"') inString = true;
280
+ else if (char === "{" || char === "[") depth++;
281
+ else if (char === "}" || char === "]") {
282
+ depth--;
283
+ if (depth === 0) return text.slice(start, i + 1);
284
+ }
285
+ }
286
+ return text.slice(start);
287
+ }
288
+
185
289
  /**
186
290
  * How the schema rides on a call that ALSO carries tools.
187
291
  *