@providerkit/core 0.12.1 → 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/dist/context.js +2 -2
- package/dist/context.js.map +1 -1
- package/dist/errors.d.ts +8 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +10 -0
- package/dist/errors.js.map +1 -1
- package/dist/presets.d.ts +3 -2
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js.map +1 -1
- package/dist/providers/anthropic.d.ts +5 -4
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +137 -16
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +2 -0
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/openai.d.ts +23 -21
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +88 -37
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/responses.d.ts +1 -1
- package/dist/providers/responses.d.ts.map +1 -1
- package/dist/providers/responses.js +16 -8
- package/dist/providers/responses.js.map +1 -1
- package/dist/types.d.ts +47 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +84 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/context.ts +2 -2
- package/src/errors.ts +16 -0
- package/src/presets.ts +3 -2
- package/src/providers/anthropic.ts +152 -23
- package/src/providers/gemini.ts +2 -0
- package/src/providers/openai.ts +86 -38
- package/src/providers/responses.ts +20 -10
- package/src/types.ts +106 -2
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// The event names arrive on the SSE `event:` line and are repeated inside each
|
|
11
11
|
// payload's own `type`. The transport yields only `data:` payloads, so this
|
|
12
12
|
// adapter reads `type` — which is what survives, and what gateways agree on.
|
|
13
|
-
import { streamError } from "../errors.ts";
|
|
13
|
+
import { fileRefused, streamError } from "../errors.ts";
|
|
14
14
|
import { streamSse, apiUrl } from "../transport.ts";
|
|
15
15
|
import type {
|
|
16
16
|
ChatMessage,
|
|
@@ -76,13 +76,16 @@ type ResponsesInputItem =
|
|
|
76
76
|
| { type: "function_call"; call_id: string; name: string; arguments: string }
|
|
77
77
|
| { type: "function_call_output"; call_id: string; output: string | ResponsesContentPart[] };
|
|
78
78
|
|
|
79
|
-
function partsToResponses(
|
|
79
|
+
function partsToResponses(
|
|
80
|
+
content: string | ContentPart[],
|
|
81
|
+
provider: string,
|
|
82
|
+
): ResponsesContentPart[] {
|
|
80
83
|
if (typeof content === "string") return [{ type: "input_text", text: content }];
|
|
81
|
-
return content.map((part): ResponsesContentPart =>
|
|
82
|
-
part.type === "text"
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
);
|
|
84
|
+
return content.map((part): ResponsesContentPart => {
|
|
85
|
+
if (part.type === "text") return { type: "input_text", text: part.text };
|
|
86
|
+
if (part.type === "file") throw fileRefused(provider, "Responses", part);
|
|
87
|
+
return { type: "input_image", image_url: toDataUri(part) };
|
|
88
|
+
});
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
/** A tool result is a bare string unless it carried images — then the content-
|
|
@@ -106,7 +109,10 @@ function toolOutput(
|
|
|
106
109
|
* become several items (its text, then one `function_call` per tool it asked
|
|
107
110
|
* for), which is why a message maps to a list rather than to one item.
|
|
108
111
|
*/
|
|
109
|
-
export function toResponsesInput(
|
|
112
|
+
export function toResponsesInput(
|
|
113
|
+
messages: readonly ChatMessage[],
|
|
114
|
+
provider = "openai-responses",
|
|
115
|
+
): {
|
|
110
116
|
instructions?: string;
|
|
111
117
|
input: unknown[];
|
|
112
118
|
} {
|
|
@@ -122,7 +128,11 @@ export function toResponsesInput(messages: readonly ChatMessage[]): {
|
|
|
122
128
|
break; // lifted into `instructions` above
|
|
123
129
|
|
|
124
130
|
case "user":
|
|
125
|
-
input.push({
|
|
131
|
+
input.push({
|
|
132
|
+
type: "message",
|
|
133
|
+
role: "user",
|
|
134
|
+
content: partsToResponses(message.content, provider),
|
|
135
|
+
});
|
|
126
136
|
break;
|
|
127
137
|
|
|
128
138
|
case "tool":
|
|
@@ -237,7 +247,7 @@ export function createResponsesProvider(config: ResponsesConfig): Provider {
|
|
|
237
247
|
opts: StreamOptions = {},
|
|
238
248
|
): AsyncIterable<ProviderChunk> {
|
|
239
249
|
const effort = opts.effort ?? config.effort;
|
|
240
|
-
const { instructions, input } = toResponsesInput(messages);
|
|
250
|
+
const { instructions, input } = toResponsesInput(messages, id);
|
|
241
251
|
|
|
242
252
|
const request: Record<string, unknown> = {
|
|
243
253
|
model: opts.model ?? config.model,
|
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`)
|
|
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
|
-
|
|
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
|
*
|