@aparte/provider-ai-sdk 0.27.4 → 0.28.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 +34 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +126 -1
- package/dist/index.js.map +1 -1
- package/dist/sources.d.ts +43 -0
- package/dist/sources.d.ts.map +1 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -31,6 +31,40 @@ aparteGlobalConfig.registerAIProvider(createAiSdkProvider({
|
|
|
31
31
|
`ai` is a **peerDependency pinned to the verified major** (`^7`) — this bridge is the only
|
|
32
32
|
aparté module touching the SDK's types. `@aparte/core` is a **peer dependency**.
|
|
33
33
|
|
|
34
|
+
## Sources and citations
|
|
35
|
+
|
|
36
|
+
A model that searches the web or reads documents reports what it drew on as `source` parts
|
|
37
|
+
in the SDK's stream. The bridge collects them and ends the turn with one `sources` event, just
|
|
38
|
+
before `done`: aparté lists the sources under the reply, and puts a chip in the text wherever
|
|
39
|
+
the vendor says a passage is cited. Nothing to configure — a model that reports no source
|
|
40
|
+
leaves the stream as it was.
|
|
41
|
+
|
|
42
|
+
<!-- doc-check: skip @ai-sdk/perplexity is the reader's vendor package, not one this repo installs -->
|
|
43
|
+
```ts
|
|
44
|
+
import { createPerplexity } from '@ai-sdk/perplexity';
|
|
45
|
+
import { createAiSdkProvider } from '@aparte/provider-ai-sdk';
|
|
46
|
+
import { aparteGlobalConfig } from '@aparte/core';
|
|
47
|
+
|
|
48
|
+
// Sonar searches the web: each page it reads arrives as a `source` part.
|
|
49
|
+
aparteGlobalConfig.registerAIProvider(createAiSdkProvider({
|
|
50
|
+
id: 'perplexity',
|
|
51
|
+
name: 'Perplexity',
|
|
52
|
+
models: [{ id: 'sonar', name: 'Sonar' }],
|
|
53
|
+
languageModel: (modelId, auth) =>
|
|
54
|
+
createPerplexity({ apiKey: typeof auth === 'string' ? auth : auth?.['apiKey'] })(modelId),
|
|
55
|
+
}));
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
- A `url` source becomes `{ id, url, title }`; a `document` source `{ id, title }` (its filename
|
|
59
|
+
when it has no title) and no link. The same URL reported twice is one source.
|
|
60
|
+
- From `ai` 7.0.138 a `text-end` part may carry `citations`: each names a source and, when the
|
|
61
|
+
vendor says so, the passage it supports, with offsets relative to that text part. The bridge
|
|
62
|
+
rebases them onto the whole reply, so the chip lands after the right sentence. A citation with
|
|
63
|
+
no offsets supports its whole text part; a vendor's `citedText` becomes the source's snippet.
|
|
64
|
+
On an older `ai` the field is simply absent and only the list of sources is shown.
|
|
65
|
+
- A non-streamed request (`stream: false`) that cites sources returns the stream
|
|
66
|
+
`[text, sources, done]` instead of a bare string; one that cites nothing still returns the string.
|
|
67
|
+
|
|
34
68
|
**Works on the server too.** This bridge touches no DOM, and core resolves its DOM-free entry
|
|
35
69
|
under Node's export condition — so it runs in a backend, an Electron main process or an SSR
|
|
36
70
|
pass as well as in the browser.
|
package/dist/index.d.ts
CHANGED
|
@@ -82,9 +82,10 @@ export declare function toToolChoice(choice: AparteChatRequest['toolChoice']): T
|
|
|
82
82
|
/**
|
|
83
83
|
* Map the AI SDK `fullStream` to aparté's event stream:
|
|
84
84
|
* `text-delta`→text · `reasoning-delta`→thinking · `tool-call`→tool_use ·
|
|
85
|
-
* `finish`→done{usage} · `error`→error
|
|
86
|
-
*
|
|
87
|
-
* tool
|
|
85
|
+
* `finish`→done{usage} · `error`→error · `source` (and, from ai 7.0.138, the
|
|
86
|
+
* `citations` of a `text-end`) → one `sources` event just before the `done`.
|
|
87
|
+
* Everything else (step markers, tool-input deltas, files) is dropped — aparté's
|
|
88
|
+
* loop consumes whole tool calls, not input deltas.
|
|
88
89
|
*/
|
|
89
90
|
export declare function fullStreamToAparteEvents(fullStream: AsyncIterable<{
|
|
90
91
|
type: string;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,KAAK,EACR,gBAAgB,EAChB,aAAa,EACb,4BAA4B,EAC5B,iBAAiB,EAEjB,iBAAiB,EACjB,iBAAiB,EAGpB,MAAM,cAAc,CAAC;AAGtB,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,KAAK,EACR,gBAAgB,EAChB,aAAa,EACb,4BAA4B,EAC5B,iBAAiB,EAEjB,iBAAiB,EACjB,iBAAiB,EAGpB,MAAM,cAAc,CAAC;AAGtB,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAK3E,MAAM,WAAW,oBAAoB;IACjC,6EAA6E;IAC7E,EAAE,EAAE,MAAM,CAAC;IACX;;;;OAIG;IACH,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,aAAa,CAAC;IAC1F,uCAAuC;IACvC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mBAAmB;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sBAAsB;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iCAAiC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,4CAA4C;IAC5C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,aAAa,EAAE,CAAC;IACzB,mDAAmD;IACnD,YAAY,CAAC,EAAE,4BAA4B,CAAC;CAC/C;AAwBD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,iBAAiB,EAAE,GAAG,YAAY,EAAE,CAyE7E;AAED,iGAAiG;AACjG,wBAAgB,SAAS,CAAC,KAAK,EAAE,WAAW,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC,GAAG,OAAO,CAQjF;AAED,6FAA6F;AAC7F,wBAAgB,YAAY,CAAC,MAAM,EAAE,iBAAiB,CAAC,YAAY,CAAC,GAAG,UAAU,CAAC,OAAO,CAAC,GAAG,SAAS,CAIrG;AAgDD;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACpC,UAAU,EAAE,aAAa,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACtE,cAAc,CAAC,iBAAiB,CAAC,CAmGnC;AAUD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,oBAAoB,GAAG,gBAAgB,CAkDhF;AAED,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,109 @@
|
|
|
1
1
|
import { contentToText, AparteErrorCode } from "@aparte/core";
|
|
2
2
|
import { tool, jsonSchema, streamText } from "ai";
|
|
3
|
+
const str = (value) => typeof value === "string" && value ? value : void 0;
|
|
4
|
+
const num = (value) => typeof value === "number" && Number.isFinite(value) ? Math.trunc(value) : void 0;
|
|
5
|
+
function createSourceCollector() {
|
|
6
|
+
const sources = [];
|
|
7
|
+
const citations = [];
|
|
8
|
+
const byUrl = /* @__PURE__ */ new Map();
|
|
9
|
+
const ids = /* @__PURE__ */ new Set();
|
|
10
|
+
const parts = /* @__PURE__ */ new Map();
|
|
11
|
+
let total = 0;
|
|
12
|
+
const addSource = (part, snippet) => {
|
|
13
|
+
const p = part;
|
|
14
|
+
if (!p || typeof p !== "object") return void 0;
|
|
15
|
+
const url = p["sourceType"] === "url" ? str(p["url"]) : void 0;
|
|
16
|
+
const title = p["sourceType"] === "url" || p["sourceType"] === "document" ? str(p["title"]) ?? str(p["filename"]) : void 0;
|
|
17
|
+
if (url === void 0 && title === void 0) return void 0;
|
|
18
|
+
const known = url !== void 0 ? byUrl.get(url) : sources.find((s) => s.id === p["id"] && s.url === void 0);
|
|
19
|
+
if (known) {
|
|
20
|
+
known.title ??= title;
|
|
21
|
+
known.snippet ??= snippet;
|
|
22
|
+
return known.id;
|
|
23
|
+
}
|
|
24
|
+
let id = str(p["id"]) ?? `s${sources.length + 1}`;
|
|
25
|
+
for (let n = 2; ids.has(id); n++) id = `${str(p["id"]) ?? "s"}-${n}`;
|
|
26
|
+
ids.add(id);
|
|
27
|
+
const source = {
|
|
28
|
+
id,
|
|
29
|
+
...url !== void 0 ? { url } : {},
|
|
30
|
+
...title !== void 0 ? { title } : {},
|
|
31
|
+
...snippet !== void 0 ? { snippet } : {}
|
|
32
|
+
};
|
|
33
|
+
sources.push(source);
|
|
34
|
+
if (url !== void 0) byUrl.set(url, source);
|
|
35
|
+
return id;
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
addSource,
|
|
39
|
+
/** A `text-start`: the part begins where the text so far ends. */
|
|
40
|
+
textStart(id) {
|
|
41
|
+
parts.set(id, { start: total, length: 0 });
|
|
42
|
+
},
|
|
43
|
+
/** A `text-delta`. A part whose `text-start` was not seen begins at its first delta. */
|
|
44
|
+
textDelta(id, delta) {
|
|
45
|
+
let part = parts.get(id);
|
|
46
|
+
if (!part) {
|
|
47
|
+
part = { start: total, length: 0 };
|
|
48
|
+
parts.set(id, part);
|
|
49
|
+
}
|
|
50
|
+
part.length += delta.length;
|
|
51
|
+
total += delta.length;
|
|
52
|
+
},
|
|
53
|
+
/**
|
|
54
|
+
* A `text-end`, and the `citations` it carries (ai 7.0.138+). Offsets are relative to
|
|
55
|
+
* the part: they are rebased onto the call's text, and kept inside the part they
|
|
56
|
+
* belong to. A citation with no offsets supports the part as a whole, so its chip goes
|
|
57
|
+
* at the part's end. Two sources on one passage are one citation.
|
|
58
|
+
*/
|
|
59
|
+
textEnd(id, list) {
|
|
60
|
+
if (!Array.isArray(list)) return;
|
|
61
|
+
const part = parts.get(id) ?? { start: total, length: 0 };
|
|
62
|
+
const within = (value) => part.start + Math.min(Math.max(value, 0), part.length);
|
|
63
|
+
for (const c of list) {
|
|
64
|
+
const sourceId = addSource(c?.source, str(c?.citedText));
|
|
65
|
+
if (sourceId === void 0) continue;
|
|
66
|
+
const from = num(c.startIndex);
|
|
67
|
+
const to = num(c.endIndex);
|
|
68
|
+
if (to !== void 0 && to <= 0) continue;
|
|
69
|
+
const end = within(to ?? part.length);
|
|
70
|
+
const start = from !== void 0 && within(from) < end ? within(from) : void 0;
|
|
71
|
+
const same = citations.find((x) => x.start === start && x.end === end);
|
|
72
|
+
if (same) {
|
|
73
|
+
if (!same.sources.includes(sourceId)) same.sources.push(sourceId);
|
|
74
|
+
} else {
|
|
75
|
+
citations.push({ ...start !== void 0 ? { start } : {}, end, sources: [sourceId] });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
/** The `sources` event, or `undefined` when the call cited nothing: the stream is then untouched. */
|
|
80
|
+
finish() {
|
|
81
|
+
if (!sources.length) return void 0;
|
|
82
|
+
return { type: "sources", sources, ...citations.length ? { citations } : {} };
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function nonStreamedAnswer(text, sources, content) {
|
|
87
|
+
const cited = createSourceCollector();
|
|
88
|
+
for (const source of sources) cited.addSource(source);
|
|
89
|
+
content.forEach((entry, i) => {
|
|
90
|
+
const part = entry;
|
|
91
|
+
if (part["type"] !== "text") return;
|
|
92
|
+
cited.textStart(String(i));
|
|
93
|
+
cited.textDelta(String(i), part["text"]);
|
|
94
|
+
cited.textEnd(String(i), part["citations"]);
|
|
95
|
+
});
|
|
96
|
+
const event = cited.finish();
|
|
97
|
+
if (!event) return text;
|
|
98
|
+
return new ReadableStream({
|
|
99
|
+
start(controller) {
|
|
100
|
+
controller.enqueue({ type: "text", delta: text });
|
|
101
|
+
controller.enqueue(event);
|
|
102
|
+
controller.enqueue({ type: "done" });
|
|
103
|
+
controller.close();
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
3
107
|
const LEGACY_TOOL_ROLES = /* @__PURE__ */ new Set(["tool_call", "tool_result"]);
|
|
4
108
|
const warnedLegacyRoles = /* @__PURE__ */ new Set();
|
|
5
109
|
function warnLegacyToolRole(role) {
|
|
@@ -121,21 +225,40 @@ function errorEvent(message, err) {
|
|
|
121
225
|
}
|
|
122
226
|
function fullStreamToAparteEvents(fullStream) {
|
|
123
227
|
let iterator;
|
|
228
|
+
const cited = createSourceCollector();
|
|
229
|
+
let sourcesSent = false;
|
|
124
230
|
return new ReadableStream({
|
|
125
231
|
async start(controller) {
|
|
126
232
|
iterator = fullStream[Symbol.asyncIterator]();
|
|
233
|
+
const sendSources = () => {
|
|
234
|
+
if (sourcesSent) return;
|
|
235
|
+
sourcesSent = true;
|
|
236
|
+
const sources = cited.finish();
|
|
237
|
+
if (sources) controller.enqueue(sources);
|
|
238
|
+
};
|
|
127
239
|
try {
|
|
128
240
|
while (true) {
|
|
129
241
|
const { value: part, done } = await iterator.next();
|
|
130
242
|
if (done) break;
|
|
131
243
|
switch (part.type) {
|
|
244
|
+
case "text-start":
|
|
245
|
+
cited.textStart(part["id"]);
|
|
246
|
+
break;
|
|
132
247
|
case "text-delta":
|
|
248
|
+
cited.textDelta(part["id"], part["text"]);
|
|
133
249
|
controller.enqueue({ type: "text", delta: part["text"] });
|
|
134
250
|
break;
|
|
251
|
+
case "text-end":
|
|
252
|
+
cited.textEnd(part["id"], part["citations"]);
|
|
253
|
+
break;
|
|
254
|
+
case "source":
|
|
255
|
+
cited.addSource(part);
|
|
256
|
+
break;
|
|
135
257
|
case "reasoning-delta":
|
|
136
258
|
controller.enqueue({ type: "thinking", delta: part["text"] });
|
|
137
259
|
break;
|
|
138
260
|
case "tool-call":
|
|
261
|
+
sendSources();
|
|
139
262
|
controller.enqueue({
|
|
140
263
|
type: "tool_use",
|
|
141
264
|
id: part["toolCallId"],
|
|
@@ -145,6 +268,7 @@ function fullStreamToAparteEvents(fullStream) {
|
|
|
145
268
|
break;
|
|
146
269
|
case "finish":
|
|
147
270
|
{
|
|
271
|
+
sendSources();
|
|
148
272
|
const finishReason = toAparteFinishReason(part["finishReason"]);
|
|
149
273
|
controller.enqueue({
|
|
150
274
|
type: "done",
|
|
@@ -212,7 +336,8 @@ function createAiSdkProvider(opts) {
|
|
|
212
336
|
...request.tools?.length ? { tools: toToolSet(request.tools), toolChoice: toToolChoice(request.toolChoice) ?? "auto" } : {}
|
|
213
337
|
});
|
|
214
338
|
if (request.stream === false) {
|
|
215
|
-
|
|
339
|
+
const [text, sources, content] = await Promise.all([result.text, result.sources, result.content]);
|
|
340
|
+
return nonStreamedAnswer(text, sources, content);
|
|
216
341
|
}
|
|
217
342
|
return fullStreamToAparteEvents(result.fullStream);
|
|
218
343
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * @aparte/provider-ai-sdk — bridge any Vercel AI SDK model into aparté.\n *\n * aparté's own wire concern is deliberately tiny: `@aparte/provider-openai-compat`\n * covers the one de-facto-standard format. EVERYTHING else (Anthropic, Google,\n * Bedrock, 25+ vendors) rides the AI SDK ecosystem through this bridge: you\n * bring your `@ai-sdk/*` package and hand its model to `createAiSdkProvider`;\n * the bridge runs `streamText` (single step — aparté's agent loop owns the\n * multi-turn) and maps `fullStream` parts to aparté's `AparteStreamEvent`s.\n *\n * ```ts\n * import { createAnthropic } from '@ai-sdk/anthropic';\n * import { createAiSdkProvider } from '@aparte/provider-ai-sdk';\n *\n * aparteGlobalConfig.registerAIProvider(createAiSdkProvider({\n * id: 'anthropic',\n * name: 'Anthropic',\n * models: [{ id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5' }],\n * languageModel: (modelId, auth) =>\n * createAnthropic({\n * apiKey: typeof auth === 'string' ? auth : auth?.['apiKey'],\n * headers: { 'anthropic-dangerous-direct-browser-access': 'true' }, // BYOK\n * })(modelId),\n * }));\n * ```\n *\n * DEPENDENCY POLICY: `ai` is a **peerDependency pinned to the verified major**\n * (`^7`). The AI SDK moves fast (v5→v7 in about a year); this bridge is the\n * ONLY aparté module touching its types, so a breaking major costs one small\n * package bump — widened per-major after verification, never speculatively.\n *\n * The bridge is an `AparteAIProvider` with a `chat()` (it owns its I/O via the\n * SDK — same shape as `@aparte/provider-transformers`), driven by\n * `AparteDirectTransport`'s delegation branch, which forwards `ctx.signal` so a user\n * \"stop\" aborts the underlying vendor call.\n *\n * NOTE (loop contract): the bridge never sees `toolChoice: { name, input }` —\n * the agent loop (engine `runStreamAgent` / core `_streamLoop`) executes that\n * synthetic call itself and strips `toolChoice` before the transport call.\n * Only `'auto' | 'none' | { name }` reach this module.\n */\n\nimport type {\n AparteAIProvider,\n AparteAIModel,\n AparteAIProviderConfigSchema,\n AparteChatRequest,\n AparteChatResponse,\n AparteChatMessage,\n AparteStreamEvent,\n AparteUsage,\n AparteFinishReason,\n} from '@aparte/core';\nimport { AparteErrorCode, contentToText } from '@aparte/core';\nimport { streamText, jsonSchema, tool } from 'ai';\nimport type { LanguageModel, ModelMessage, ToolSet, ToolChoice } from 'ai';\n\n// ─── Options ─────────────────────────────────────────────────────────────────\n\nexport interface AiSdkProviderOptions {\n /** Provider id used across aparté (key resolution, model picker, events). */\n id: string;\n /**\n * Resolve the AI SDK model for a chat call. `auth` is the key/config the\n * aparté key-resolver produced — rebuild the vendor provider per call for\n * UI-driven BYOK, or ignore it if your factory already carries the key.\n */\n languageModel: (modelId: string, auth?: string | Record<string, string>) => LanguageModel;\n /** Display name (defaults to `id`). */\n name?: string;\n /** Brand icon (SVG string / data URI / icon-provider key). */\n icon?: string;\n /** Brand color. */\n color?: string;\n /** Short tag line. */\n description?: string;\n /** Where the user gets a key. */\n helpUrl?: string;\n /** Whether the vendor offers free models. */\n hasFreeModels?: boolean;\n /** Model runs locally (no key expected). */\n isLocal?: boolean;\n /** Model list — **consumer data** (the AI SDK has no model-listing API). */\n models?: AparteAIModel[];\n /** Override the default apiKey settings schema. */\n configSchema?: AparteAIProviderConfigSchema;\n}\n\n// ─── aparté ⇄ AI SDK shaping ─────────────────────────────────────────────────\n\n/**\n * The two roles a tool turn used to wear. `AparteChatMessage` no longer allows either, so\n * the comparison is on the role as a string: a message carrying one comes from code built\n * against an older aparté, which no type here can catch. Warned once per role, then\n * skipped — the fallthrough below maps anything unrecognised as a `user` turn, so without\n * this the model would read a tool result as something the person typed.\n */\nconst LEGACY_TOOL_ROLES = new Set(['tool_call', 'tool_result']);\nconst warnedLegacyRoles = new Set<string>();\nfunction warnLegacyToolRole(role: string): void {\n if (warnedLegacyRoles.has(role)) return;\n warnedLegacyRoles.add(role);\n console.warn(\n `[@aparte/provider-ai-sdk] Skipping a message with the removed role \"${role}\".`\n + \" A tool turn is now an assistant message carrying its calls — { role: 'assistant',\"\n + \" content: <what it said>, toolCalls } — followed by one { role: 'tool', content:\"\n + ' <result>, toolCallId, toolName } per call. This warning is removed in 0.18.',\n );\n}\n\n/**\n * AparteChatMessage[] → AI SDK ModelMessage[]. Handles a tool turn (an assistant\n * message carrying `toolCalls`, then the `tool` messages answering them); the tool\n * name for a result is the message's own `toolName`, else the call that declared it.\n */\nexport function toModelMessages(messages: AparteChatMessage[]): ModelMessage[] {\n // toolCallId → toolName, the FALLBACK for a `tool` message that names no tool of\n // its own. A hand-built history need carry no assistant turn to scan, which is\n // where the name used to become 'unknown'.\n const toolNames = new Map<string, string>();\n for (const msg of messages) {\n if (msg.role === 'assistant') {\n for (const tc of msg.toolCalls ?? []) toolNames.set(tc.id, tc.name);\n }\n }\n\n const out: ModelMessage[] = [];\n for (const msg of messages) {\n const role: string = msg.role;\n if (LEGACY_TOOL_ROLES.has(role)) {\n warnLegacyToolRole(role);\n continue;\n }\n if (msg.role === 'tool') {\n out.push({\n role: 'tool',\n content: [{\n type: 'tool-result',\n toolCallId: msg.toolCallId ?? '',\n toolName: msg.toolName ?? toolNames.get(msg.toolCallId ?? '') ?? 'unknown',\n output: { type: 'text', value: contentToText(msg.content) },\n }],\n });\n continue;\n }\n if (msg.role === 'system') {\n out.push({ role: 'system', content: contentToText(msg.content) });\n continue;\n }\n if (msg.role === 'assistant') {\n if (msg.toolCalls?.length) {\n const text = contentToText(msg.content);\n out.push({\n role: 'assistant',\n content: [\n // No empty text part: '' is what an assistant that said nothing\n // before its calls now carries, and a part saying nothing is a\n // part the model still reads.\n ...(text ? [{ type: 'text' as const, text }] : []),\n ...msg.toolCalls.map(tc => ({\n type: 'tool-call' as const,\n toolCallId: tc.id,\n toolName: tc.name,\n input: tc.input,\n })),\n ],\n });\n continue;\n }\n out.push({ role: 'assistant', content: contentToText(msg.content) });\n continue;\n }\n // user (default) — keep multimodal parts.\n if (typeof msg.content === 'string') {\n out.push({ role: 'user', content: msg.content });\n } else {\n out.push({\n role: 'user',\n // Two arms, exhaustive: a part is text or an image. A third used to answer\n // the removed `AparteFilePart` with an empty text part — a branch nothing\n // could reach, standing in for a file bridge that was never written.\n content: msg.content.map(p => (p.type === 'text'\n ? { type: 'text' as const, text: p.text }\n : { type: 'image' as const, image: p.image })),\n });\n }\n }\n return out;\n}\n\n/** AparteTool[] → AI SDK ToolSet — declaration only, NO `execute` (aparté's loop runs tools). */\nexport function toToolSet(tools: NonNullable<AparteChatRequest['tools']>): ToolSet {\n return Object.fromEntries(tools.map(t => [\n t.name,\n tool({\n description: t.description,\n inputSchema: jsonSchema(t.inputSchema as Parameters<typeof jsonSchema>[0]),\n }),\n ]));\n}\n\n/** aparté toolChoice → AI SDK toolChoice (the synthetic {name,input} never reaches here). */\nexport function toToolChoice(choice: AparteChatRequest['toolChoice']): ToolChoice<ToolSet> | undefined {\n if (choice === 'auto' || choice === 'none') return choice;\n if (choice && typeof choice === 'object') return { type: 'tool', toolName: choice.name };\n return undefined;\n}\n\n/** fullStream `finish.totalUsage` → AparteUsage. */\nfunction toAparteUsage(u: { inputTokens?: number; outputTokens?: number; totalTokens?: number; inputTokenDetails?: { cacheReadTokens?: number } }): AparteUsage {\n return {\n inputTokens: u.inputTokens ?? 0,\n outputTokens: u.outputTokens ?? 0,\n totalTokens: u.totalTokens,\n cacheReadTokens: u.inputTokenDetails?.cacheReadTokens,\n };\n}\n\n/**\n * fullStream `finish.finishReason` → AparteFinishReason. The SDK's own list is the same\n * but for `'unknown'`, which is \"the vendor said something we could not classify\" — the\n * `'other'` of ours. A value that is neither (a newer SDK) is left off rather than guessed.\n */\nfunction toAparteFinishReason(reason: unknown): AparteFinishReason | undefined {\n switch (reason) {\n case 'stop': case 'length': case 'content-filter': case 'tool-calls': case 'error': case 'other':\n return reason;\n case 'unknown':\n return 'other';\n default:\n return undefined;\n }\n}\n\n/**\n * What servers write when the prompt is longer than the model's window (OpenAI's \"maximum\n * context length\", LM Studio's \"Context size has been exceeded\", llama.cpp's\n * `exceed_context_size_error`, Anthropic's \"prompt is too long\"). The SDK hands a failed\n * call over as an `APICallError` whose message — and, for some vendors, response body — is\n * the vendor's own sentence, so the match is the same one `@aparte/provider-openai-compat`\n * makes on a wire body.\n */\nconst CONTEXT_OVERFLOW = /maximum context length|context length|context size has been exceeded|exceed_context_size_error|exceeds the available context size|prompt is too long|context_length_exceeded/i;\n\n/**\n * The error event for a failure the SDK reported. An overflow carries its code, so the\n * client can compact and resend instead of showing the vendor's sentence.\n */\nfunction errorEvent(message: string, err: unknown): AparteStreamEvent {\n const body = (err as { responseBody?: unknown } | null | undefined)?.responseBody;\n const overflow = CONTEXT_OVERFLOW.test(message) || (typeof body === 'string' && CONTEXT_OVERFLOW.test(body));\n return overflow ? { type: 'error', message, code: AparteErrorCode.USAGE_CONTEXT_EXCEEDED } : { type: 'error', message };\n}\n\n/**\n * Map the AI SDK `fullStream` to aparté's event stream:\n * `text-delta`→text · `reasoning-delta`→thinking · `tool-call`→tool_use ·\n * `finish`→done{usage} · `error`→error. Everything else (step markers,\n * tool-input deltas, sources, files) is dropped — aparté's loop consumes whole\n * tool calls, not input deltas.\n */\nexport function fullStreamToAparteEvents(\n fullStream: AsyncIterable<{ type: string } & Record<string, unknown>>,\n): ReadableStream<AparteStreamEvent> {\n // Held so `cancel()` can signal the AI SDK's iterator to stop (calling\n // `.return()` propagates cancellation the same way breaking a `for await`\n // loop would), instead of leaving `start()` draining `fullStream` to its\n // natural end after the consumer has already walked away.\n let iterator: AsyncIterator<{ type: string } & Record<string, unknown>> | undefined;\n\n return new ReadableStream<AparteStreamEvent>({\n async start(controller) {\n iterator = fullStream[Symbol.asyncIterator]();\n try {\n while (true) {\n const { value: part, done } = await iterator.next();\n if (done) break;\n\n switch (part.type) {\n case 'text-delta':\n controller.enqueue({ type: 'text', delta: part['text'] as string });\n break;\n case 'reasoning-delta':\n controller.enqueue({ type: 'thinking', delta: part['text'] as string });\n break;\n case 'tool-call':\n controller.enqueue({\n type: 'tool_use',\n id: part['toolCallId'] as string,\n name: part['toolName'] as string,\n input: (part['input'] ?? {}) as Record<string, unknown>,\n });\n break;\n case 'finish':\n {\n const finishReason = toAparteFinishReason(part['finishReason']);\n controller.enqueue({\n type: 'done',\n usage: toAparteUsage(part['totalUsage'] as Parameters<typeof toAparteUsage>[0]),\n ...(finishReason ? { finishReason } : {}),\n });\n }\n // Terminal: stop reading `fullStream` so a stray second\n // finish/error part (if the SDK ever emits one) can't\n // enqueue past the done event.\n return;\n case 'error': {\n const err = part['error'];\n controller.enqueue(errorEvent(err instanceof Error ? err.message : String(err), err));\n return;\n }\n // 'abort', step markers, tool-input-* deltas, sources… → dropped.\n }\n }\n } catch (err: unknown) {\n // AbortError surfaces here when ctx.signal fires mid-stream: the\n // consumer (agent loop) cancelled on purpose — end quietly.\n if ((err as { name?: string })?.name !== 'AbortError') {\n controller.enqueue(errorEvent((err as Error | undefined)?.message ?? 'Stream error', err));\n }\n } finally {\n /*\n * Told to stop on EVERY exit, not only when the consumer cancels: the\n * `finish` and `error` branches `return` straight out of the loop, so\n * they used to reach this `finally` having said nothing to the SDK —\n * after an error part the vendor call was left to drain on its own,\n * which is the very thing `cancel()` exists to prevent. Best effort:\n * an iterator that objects to being settled must not become the\n * stream's error.\n */\n try { await iterator?.return?.(undefined); } catch { /* best effort */ }\n controller.close();\n }\n },\n async cancel(reason) {\n await iterator?.return?.(reason);\n },\n });\n}\n\n// ─── The provider factory ────────────────────────────────────────────────────\n\nconst DEFAULT_CONFIG_SCHEMA = (opts: AiSdkProviderOptions): AparteAIProviderConfigSchema => ({\n fields: opts.isLocal\n ? []\n : [{ id: 'apiKey', type: 'password', label: 'API Key', required: true }],\n});\n\n/**\n * Wrap an AI SDK model factory into an `AparteAIProvider`. The returned provider\n * owns its I/O through the SDK (`chat()` shape — `AparteDirectTransport` delegates\n * to it and forwards the abort signal).\n */\nexport function createAiSdkProvider(opts: AiSdkProviderOptions): AparteAIProvider {\n const displayName = opts.name ?? opts.id;\n\n return {\n id: opts.id,\n\n getMetadata() {\n return {\n id: opts.id,\n name: displayName,\n icon: opts.icon,\n color: opts.color,\n description: opts.description,\n helpUrl: opts.helpUrl,\n hasFreeModels: opts.hasFreeModels,\n isLocal: opts.isLocal,\n configSchema: opts.configSchema ?? DEFAULT_CONFIG_SCHEMA(opts),\n };\n },\n\n getModels(): AparteAIModel[] {\n return opts.models ?? [];\n },\n\n async chat(\n request: AparteChatRequest,\n auth?: string | Record<string, string>,\n ctx?: { providerId: string; signal?: AbortSignal },\n ): Promise<AparteChatResponse> {\n const model = opts.languageModel(request.modelId ?? '', auth);\n\n const result = streamText({\n model,\n messages: toModelMessages(request.messages),\n temperature: request.temperature,\n maxOutputTokens: request.maxTokens,\n seed: request.seed,\n abortSignal: ctx?.signal,\n ...(request.tools?.length\n ? { tools: toToolSet(request.tools), toolChoice: toToolChoice(request.toolChoice) ?? 'auto' }\n : {}),\n });\n\n if (request.stream === false) {\n return await result.text;\n }\n return fullStreamToAparteEvents(result.fullStream);\n },\n };\n}\n\nexport type { AparteAIProvider, AparteAIModel } from '@aparte/core';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/sources.ts","../src/index.ts"],"sourcesContent":["/**\n * sources.ts — the sources a reply cites, read off the AI SDK's `fullStream`.\n *\n * Two things come out of the SDK. A `source` part is something the model drew on — a web\n * page, a document — with no place in the text. From ai 7.0.138 a `text-end` part may also\n * carry `citations`: each one names a source and, when the vendor says so, the passage it\n * supports, with offsets relative to THAT text part. aparté's offsets are into all the text\n * the call streamed, so a part's citations are rebased on where the part began.\n *\n * Both end in one `sources` event, emitted by the bridge just before `done`. The\n * `citations` field is read without being typed: the installed SDK may predate it, and a\n * field that is not there is the same as an empty list.\n */\n\nimport type { AparteSource, AparteCitation, AparteStreamEvent, AparteChatResponse } from '@aparte/core';\n\n/** The event {@link createSourceCollector} builds: what `AparteStreamEventMap['sources']` carries. */\nexport type SourcesEvent = Extract<AparteStreamEvent, { type: 'sources' }>;\n\n/** A non-empty string, or nothing. */\nconst str = (value: unknown): string | undefined => (typeof value === 'string' && value ? value : undefined);\n\nconst num = (value: unknown): number | undefined => (typeof value === 'number' && Number.isFinite(value) ? Math.trunc(value) : undefined);\n\n/** Collects sources and citations as parts stream by. One collector per call. */\nexport function createSourceCollector() {\n const sources: AparteSource[] = [];\n const citations: AparteCitation[] = [];\n /** De-duplicated by URL: a page the SDK reports twice is one source. */\n const byUrl = new Map<string, AparteSource>();\n const ids = new Set<string>();\n /** Where each text part began in the call's text, and how much of it has streamed. */\n const parts = new Map<string, { start: number; length: number }>();\n let total = 0;\n\n /**\n * A `source` part (or the `source` of a citation) as an aparté source: `url` carries its\n * URL and title, `document` its title (the filename when it has no other). The id of the\n * SDK is kept unless another source took it. Returns the id, `undefined` for a part that\n * says nothing about where the answer came from.\n */\n const addSource = (part: unknown, snippet?: string): string | undefined => {\n const p = part as Record<string, unknown> | null | undefined;\n if (!p || typeof p !== 'object') return undefined;\n const url = p['sourceType'] === 'url' ? str(p['url']) : undefined;\n const title = p['sourceType'] === 'url' || p['sourceType'] === 'document'\n ? str(p['title']) ?? str(p['filename'])\n : undefined;\n if (url === undefined && title === undefined) return undefined;\n\n const known = url !== undefined ? byUrl.get(url) : sources.find(s => s.id === p['id'] && s.url === undefined);\n if (known) {\n // A later mention can carry what the first one lacked.\n known.title ??= title;\n known.snippet ??= snippet;\n return known.id;\n }\n let id = str(p['id']) ?? `s${sources.length + 1}`;\n for (let n = 2; ids.has(id); n++) id = `${str(p['id']) ?? 's'}-${n}`;\n ids.add(id);\n const source: AparteSource = {\n id,\n ...(url !== undefined ? { url } : {}),\n ...(title !== undefined ? { title } : {}),\n ...(snippet !== undefined ? { snippet } : {}),\n };\n sources.push(source);\n if (url !== undefined) byUrl.set(url, source);\n return id;\n };\n\n return {\n addSource,\n\n /** A `text-start`: the part begins where the text so far ends. */\n textStart(id: string): void {\n parts.set(id, { start: total, length: 0 });\n },\n\n /** A `text-delta`. A part whose `text-start` was not seen begins at its first delta. */\n textDelta(id: string, delta: string): void {\n let part = parts.get(id);\n if (!part) { part = { start: total, length: 0 }; parts.set(id, part); }\n part.length += delta.length;\n total += delta.length;\n },\n\n /**\n * A `text-end`, and the `citations` it carries (ai 7.0.138+). Offsets are relative to\n * the part: they are rebased onto the call's text, and kept inside the part they\n * belong to. A citation with no offsets supports the part as a whole, so its chip goes\n * at the part's end. Two sources on one passage are one citation.\n */\n textEnd(id: string, list: unknown): void {\n if (!Array.isArray(list)) return;\n const part = parts.get(id) ?? { start: total, length: 0 };\n const within = (value: number): number => part.start + Math.min(Math.max(value, 0), part.length);\n for (const c of list) {\n const sourceId = addSource(c?.source, str(c?.citedText));\n if (sourceId === undefined) continue;\n const from = num(c.startIndex);\n const to = num(c.endIndex);\n // An end at the part's very start (or before it) is no passage: the source stays, listed.\n if (to !== undefined && to <= 0) continue;\n const end = within(to ?? part.length);\n const start = from !== undefined && within(from) < end ? within(from) : undefined;\n const same = citations.find(x => x.start === start && x.end === end);\n if (same) {\n if (!same.sources.includes(sourceId)) same.sources.push(sourceId);\n } else {\n citations.push({ ...(start !== undefined ? { start } : {}), end, sources: [sourceId] });\n }\n }\n },\n\n /** The `sources` event, or `undefined` when the call cited nothing: the stream is then untouched. */\n finish(): SourcesEvent | undefined {\n if (!sources.length) return undefined;\n return { type: 'sources', sources, ...(citations.length ? { citations } : {}) };\n },\n };\n}\n\n/**\n * A non-streamed answer: its text, as a string. A bare string cannot carry sources, so an\n * answer that cites some is returned as the stream it would have been — `[text, sources,\n * done]`, which `AparteChatResponse` allows. `sources` is the SDK's `result.sources`;\n * `content` its `result.content`, whose text parts carry the `citations` (ai 7.0.138+).\n */\nexport function nonStreamedAnswer(\n text: string,\n sources: ReadonlyArray<unknown>,\n content: ReadonlyArray<unknown>,\n): AparteChatResponse {\n const cited = createSourceCollector();\n for (const source of sources) cited.addSource(source);\n // A text part's citations are read as the stream would read them, one id per part.\n content.forEach((entry, i) => {\n const part = entry as Record<string, unknown>;\n if (part['type'] !== 'text') return;\n cited.textStart(String(i));\n cited.textDelta(String(i), part['text'] as string);\n cited.textEnd(String(i), part['citations']);\n });\n const event = cited.finish();\n if (!event) return text;\n return new ReadableStream<AparteStreamEvent>({\n start(controller) {\n controller.enqueue({ type: 'text', delta: text });\n controller.enqueue(event);\n controller.enqueue({ type: 'done' });\n controller.close();\n },\n });\n}\n","/**\n * @aparte/provider-ai-sdk — bridge any Vercel AI SDK model into aparté.\n *\n * aparté's own wire concern is deliberately tiny: `@aparte/provider-openai-compat`\n * covers the one de-facto-standard format. EVERYTHING else (Anthropic, Google,\n * Bedrock, 25+ vendors) rides the AI SDK ecosystem through this bridge: you\n * bring your `@ai-sdk/*` package and hand its model to `createAiSdkProvider`;\n * the bridge runs `streamText` (single step — aparté's agent loop owns the\n * multi-turn) and maps `fullStream` parts to aparté's `AparteStreamEvent`s.\n *\n * ```ts\n * import { createAnthropic } from '@ai-sdk/anthropic';\n * import { createAiSdkProvider } from '@aparte/provider-ai-sdk';\n *\n * aparteGlobalConfig.registerAIProvider(createAiSdkProvider({\n * id: 'anthropic',\n * name: 'Anthropic',\n * models: [{ id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5' }],\n * languageModel: (modelId, auth) =>\n * createAnthropic({\n * apiKey: typeof auth === 'string' ? auth : auth?.['apiKey'],\n * headers: { 'anthropic-dangerous-direct-browser-access': 'true' }, // BYOK\n * })(modelId),\n * }));\n * ```\n *\n * DEPENDENCY POLICY: `ai` is a **peerDependency pinned to the verified major**\n * (`^7`). The AI SDK moves fast (v5→v7 in about a year); this bridge is the\n * ONLY aparté module touching its types, so a breaking major costs one small\n * package bump — widened per-major after verification, never speculatively.\n *\n * The bridge is an `AparteAIProvider` with a `chat()` (it owns its I/O via the\n * SDK — same shape as `@aparte/provider-transformers`), driven by\n * `AparteDirectTransport`'s delegation branch, which forwards `ctx.signal` so a user\n * \"stop\" aborts the underlying vendor call.\n *\n * NOTE (loop contract): the bridge never sees `toolChoice: { name, input }` —\n * the agent loop (engine `runStreamAgent` / core `_streamLoop`) executes that\n * synthetic call itself and strips `toolChoice` before the transport call.\n * Only `'auto' | 'none' | { name }` reach this module.\n */\n\nimport type {\n AparteAIProvider,\n AparteAIModel,\n AparteAIProviderConfigSchema,\n AparteChatRequest,\n AparteChatResponse,\n AparteChatMessage,\n AparteStreamEvent,\n AparteUsage,\n AparteFinishReason,\n} from '@aparte/core';\nimport { AparteErrorCode, contentToText } from '@aparte/core';\nimport { streamText, jsonSchema, tool } from 'ai';\nimport type { LanguageModel, ModelMessage, ToolSet, ToolChoice } from 'ai';\nimport { createSourceCollector, nonStreamedAnswer } from './sources.js';\n\n// ─── Options ─────────────────────────────────────────────────────────────────\n\nexport interface AiSdkProviderOptions {\n /** Provider id used across aparté (key resolution, model picker, events). */\n id: string;\n /**\n * Resolve the AI SDK model for a chat call. `auth` is the key/config the\n * aparté key-resolver produced — rebuild the vendor provider per call for\n * UI-driven BYOK, or ignore it if your factory already carries the key.\n */\n languageModel: (modelId: string, auth?: string | Record<string, string>) => LanguageModel;\n /** Display name (defaults to `id`). */\n name?: string;\n /** Brand icon (SVG string / data URI / icon-provider key). */\n icon?: string;\n /** Brand color. */\n color?: string;\n /** Short tag line. */\n description?: string;\n /** Where the user gets a key. */\n helpUrl?: string;\n /** Whether the vendor offers free models. */\n hasFreeModels?: boolean;\n /** Model runs locally (no key expected). */\n isLocal?: boolean;\n /** Model list — **consumer data** (the AI SDK has no model-listing API). */\n models?: AparteAIModel[];\n /** Override the default apiKey settings schema. */\n configSchema?: AparteAIProviderConfigSchema;\n}\n\n// ─── aparté ⇄ AI SDK shaping ─────────────────────────────────────────────────\n\n/**\n * The two roles a tool turn used to wear. `AparteChatMessage` no longer allows either, so\n * the comparison is on the role as a string: a message carrying one comes from code built\n * against an older aparté, which no type here can catch. Warned once per role, then\n * skipped — the fallthrough below maps anything unrecognised as a `user` turn, so without\n * this the model would read a tool result as something the person typed.\n */\nconst LEGACY_TOOL_ROLES = new Set(['tool_call', 'tool_result']);\nconst warnedLegacyRoles = new Set<string>();\nfunction warnLegacyToolRole(role: string): void {\n if (warnedLegacyRoles.has(role)) return;\n warnedLegacyRoles.add(role);\n console.warn(\n `[@aparte/provider-ai-sdk] Skipping a message with the removed role \"${role}\".`\n + \" A tool turn is now an assistant message carrying its calls — { role: 'assistant',\"\n + \" content: <what it said>, toolCalls } — followed by one { role: 'tool', content:\"\n + ' <result>, toolCallId, toolName } per call. This warning is removed in 0.18.',\n );\n}\n\n/**\n * AparteChatMessage[] → AI SDK ModelMessage[]. Handles a tool turn (an assistant\n * message carrying `toolCalls`, then the `tool` messages answering them); the tool\n * name for a result is the message's own `toolName`, else the call that declared it.\n */\nexport function toModelMessages(messages: AparteChatMessage[]): ModelMessage[] {\n // toolCallId → toolName, the FALLBACK for a `tool` message that names no tool of\n // its own. A hand-built history need carry no assistant turn to scan, which is\n // where the name used to become 'unknown'.\n const toolNames = new Map<string, string>();\n for (const msg of messages) {\n if (msg.role === 'assistant') {\n for (const tc of msg.toolCalls ?? []) toolNames.set(tc.id, tc.name);\n }\n }\n\n const out: ModelMessage[] = [];\n for (const msg of messages) {\n const role: string = msg.role;\n if (LEGACY_TOOL_ROLES.has(role)) {\n warnLegacyToolRole(role);\n continue;\n }\n if (msg.role === 'tool') {\n out.push({\n role: 'tool',\n content: [{\n type: 'tool-result',\n toolCallId: msg.toolCallId ?? '',\n toolName: msg.toolName ?? toolNames.get(msg.toolCallId ?? '') ?? 'unknown',\n output: { type: 'text', value: contentToText(msg.content) },\n }],\n });\n continue;\n }\n if (msg.role === 'system') {\n out.push({ role: 'system', content: contentToText(msg.content) });\n continue;\n }\n if (msg.role === 'assistant') {\n if (msg.toolCalls?.length) {\n const text = contentToText(msg.content);\n out.push({\n role: 'assistant',\n content: [\n // No empty text part: '' is what an assistant that said nothing\n // before its calls now carries, and a part saying nothing is a\n // part the model still reads.\n ...(text ? [{ type: 'text' as const, text }] : []),\n ...msg.toolCalls.map(tc => ({\n type: 'tool-call' as const,\n toolCallId: tc.id,\n toolName: tc.name,\n input: tc.input,\n })),\n ],\n });\n continue;\n }\n out.push({ role: 'assistant', content: contentToText(msg.content) });\n continue;\n }\n // user (default) — keep multimodal parts.\n if (typeof msg.content === 'string') {\n out.push({ role: 'user', content: msg.content });\n } else {\n out.push({\n role: 'user',\n // Two arms, exhaustive: a part is text or an image. A third used to answer\n // the removed `AparteFilePart` with an empty text part — a branch nothing\n // could reach, standing in for a file bridge that was never written.\n content: msg.content.map(p => (p.type === 'text'\n ? { type: 'text' as const, text: p.text }\n : { type: 'image' as const, image: p.image })),\n });\n }\n }\n return out;\n}\n\n/** AparteTool[] → AI SDK ToolSet — declaration only, NO `execute` (aparté's loop runs tools). */\nexport function toToolSet(tools: NonNullable<AparteChatRequest['tools']>): ToolSet {\n return Object.fromEntries(tools.map(t => [\n t.name,\n tool({\n description: t.description,\n inputSchema: jsonSchema(t.inputSchema as Parameters<typeof jsonSchema>[0]),\n }),\n ]));\n}\n\n/** aparté toolChoice → AI SDK toolChoice (the synthetic {name,input} never reaches here). */\nexport function toToolChoice(choice: AparteChatRequest['toolChoice']): ToolChoice<ToolSet> | undefined {\n if (choice === 'auto' || choice === 'none') return choice;\n if (choice && typeof choice === 'object') return { type: 'tool', toolName: choice.name };\n return undefined;\n}\n\n/** fullStream `finish.totalUsage` → AparteUsage. */\nfunction toAparteUsage(u: { inputTokens?: number; outputTokens?: number; totalTokens?: number; inputTokenDetails?: { cacheReadTokens?: number } }): AparteUsage {\n return {\n inputTokens: u.inputTokens ?? 0,\n outputTokens: u.outputTokens ?? 0,\n totalTokens: u.totalTokens,\n cacheReadTokens: u.inputTokenDetails?.cacheReadTokens,\n };\n}\n\n/**\n * fullStream `finish.finishReason` → AparteFinishReason. The SDK's own list is the same\n * but for `'unknown'`, which is \"the vendor said something we could not classify\" — the\n * `'other'` of ours. A value that is neither (a newer SDK) is left off rather than guessed.\n */\nfunction toAparteFinishReason(reason: unknown): AparteFinishReason | undefined {\n switch (reason) {\n case 'stop': case 'length': case 'content-filter': case 'tool-calls': case 'error': case 'other':\n return reason;\n case 'unknown':\n return 'other';\n default:\n return undefined;\n }\n}\n\n/**\n * What servers write when the prompt is longer than the model's window (OpenAI's \"maximum\n * context length\", LM Studio's \"Context size has been exceeded\", llama.cpp's\n * `exceed_context_size_error`, Anthropic's \"prompt is too long\"). The SDK hands a failed\n * call over as an `APICallError` whose message — and, for some vendors, response body — is\n * the vendor's own sentence, so the match is the same one `@aparte/provider-openai-compat`\n * makes on a wire body.\n */\nconst CONTEXT_OVERFLOW = /maximum context length|context length|context size has been exceeded|exceed_context_size_error|exceeds the available context size|prompt is too long|context_length_exceeded/i;\n\n/**\n * The error event for a failure the SDK reported. An overflow carries its code, so the\n * client can compact and resend instead of showing the vendor's sentence.\n */\nfunction errorEvent(message: string, err: unknown): AparteStreamEvent {\n const body = (err as { responseBody?: unknown } | null | undefined)?.responseBody;\n const overflow = CONTEXT_OVERFLOW.test(message) || (typeof body === 'string' && CONTEXT_OVERFLOW.test(body));\n return overflow ? { type: 'error', message, code: AparteErrorCode.USAGE_CONTEXT_EXCEEDED } : { type: 'error', message };\n}\n\n/**\n * Map the AI SDK `fullStream` to aparté's event stream:\n * `text-delta`→text · `reasoning-delta`→thinking · `tool-call`→tool_use ·\n * `finish`→done{usage} · `error`→error · `source` (and, from ai 7.0.138, the\n * `citations` of a `text-end`) → one `sources` event just before the `done`.\n * Everything else (step markers, tool-input deltas, files) is dropped — aparté's\n * loop consumes whole tool calls, not input deltas.\n */\nexport function fullStreamToAparteEvents(\n fullStream: AsyncIterable<{ type: string } & Record<string, unknown>>,\n): ReadableStream<AparteStreamEvent> {\n // Held so `cancel()` can signal the AI SDK's iterator to stop (calling\n // `.return()` propagates cancellation the same way breaking a `for await`\n // loop would), instead of leaving `start()` draining `fullStream` to its\n // natural end after the consumer has already walked away.\n let iterator: AsyncIterator<{ type: string } & Record<string, unknown>> | undefined;\n const cited = createSourceCollector();\n let sourcesSent = false;\n\n return new ReadableStream<AparteStreamEvent>({\n async start(controller) {\n iterator = fullStream[Symbol.asyncIterator]();\n const sendSources = (): void => {\n if (sourcesSent) return;\n sourcesSent = true;\n const sources = cited.finish();\n if (sources) controller.enqueue(sources);\n };\n try {\n while (true) {\n const { value: part, done } = await iterator.next();\n if (done) break;\n\n switch (part.type) {\n case 'text-start':\n cited.textStart(part['id'] as string);\n break;\n case 'text-delta':\n cited.textDelta(part['id'] as string, part['text'] as string);\n controller.enqueue({ type: 'text', delta: part['text'] as string });\n break;\n case 'text-end':\n cited.textEnd(part['id'] as string, part['citations']);\n break;\n case 'source':\n cited.addSource(part);\n break;\n case 'reasoning-delta':\n controller.enqueue({ type: 'thinking', delta: part['text'] as string });\n break;\n case 'tool-call':\n // The sources first, as at the end: a call the loop refuses, or one\n // with no handler, ends the turn before the `finish` part is read.\n sendSources();\n controller.enqueue({\n type: 'tool_use',\n id: part['toolCallId'] as string,\n name: part['toolName'] as string,\n input: (part['input'] ?? {}) as Record<string, unknown>,\n });\n break;\n case 'finish':\n {\n // The sources first: their offsets are into the text, which is\n // complete now, and `done` ends the turn.\n sendSources();\n const finishReason = toAparteFinishReason(part['finishReason']);\n controller.enqueue({\n type: 'done',\n usage: toAparteUsage(part['totalUsage'] as Parameters<typeof toAparteUsage>[0]),\n ...(finishReason ? { finishReason } : {}),\n });\n }\n // Terminal: stop reading `fullStream` so a stray second\n // finish/error part (if the SDK ever emits one) can't\n // enqueue past the done event.\n return;\n case 'error': {\n const err = part['error'];\n controller.enqueue(errorEvent(err instanceof Error ? err.message : String(err), err));\n return;\n }\n // 'abort', step markers, tool-input-* deltas… → dropped.\n }\n }\n } catch (err: unknown) {\n // AbortError surfaces here when ctx.signal fires mid-stream: the\n // consumer (agent loop) cancelled on purpose — end quietly.\n if ((err as { name?: string })?.name !== 'AbortError') {\n controller.enqueue(errorEvent((err as Error | undefined)?.message ?? 'Stream error', err));\n }\n } finally {\n /*\n * Told to stop on EVERY exit, not only when the consumer cancels: the\n * `finish` and `error` branches `return` straight out of the loop, so\n * they used to reach this `finally` having said nothing to the SDK —\n * after an error part the vendor call was left to drain on its own,\n * which is the very thing `cancel()` exists to prevent. Best effort:\n * an iterator that objects to being settled must not become the\n * stream's error.\n */\n try { await iterator?.return?.(undefined); } catch { /* best effort */ }\n controller.close();\n }\n },\n async cancel(reason) {\n await iterator?.return?.(reason);\n },\n });\n}\n\n// ─── The provider factory ────────────────────────────────────────────────────\n\nconst DEFAULT_CONFIG_SCHEMA = (opts: AiSdkProviderOptions): AparteAIProviderConfigSchema => ({\n fields: opts.isLocal\n ? []\n : [{ id: 'apiKey', type: 'password', label: 'API Key', required: true }],\n});\n\n/**\n * Wrap an AI SDK model factory into an `AparteAIProvider`. The returned provider\n * owns its I/O through the SDK (`chat()` shape — `AparteDirectTransport` delegates\n * to it and forwards the abort signal).\n */\nexport function createAiSdkProvider(opts: AiSdkProviderOptions): AparteAIProvider {\n const displayName = opts.name ?? opts.id;\n\n return {\n id: opts.id,\n\n getMetadata() {\n return {\n id: opts.id,\n name: displayName,\n icon: opts.icon,\n color: opts.color,\n description: opts.description,\n helpUrl: opts.helpUrl,\n hasFreeModels: opts.hasFreeModels,\n isLocal: opts.isLocal,\n configSchema: opts.configSchema ?? DEFAULT_CONFIG_SCHEMA(opts),\n };\n },\n\n getModels(): AparteAIModel[] {\n return opts.models ?? [];\n },\n\n async chat(\n request: AparteChatRequest,\n auth?: string | Record<string, string>,\n ctx?: { providerId: string; signal?: AbortSignal },\n ): Promise<AparteChatResponse> {\n const model = opts.languageModel(request.modelId ?? '', auth);\n\n const result = streamText({\n model,\n messages: toModelMessages(request.messages),\n temperature: request.temperature,\n maxOutputTokens: request.maxTokens,\n seed: request.seed,\n abortSignal: ctx?.signal,\n ...(request.tools?.length\n ? { tools: toToolSet(request.tools), toolChoice: toToolChoice(request.toolChoice) ?? 'auto' }\n : {}),\n });\n\n if (request.stream === false) {\n const [text, sources, content] = await Promise.all([result.text, result.sources, result.content]);\n return nonStreamedAnswer(text, sources, content);\n }\n return fullStreamToAparteEvents(result.fullStream);\n },\n };\n}\n\nexport type { AparteAIProvider, AparteAIModel } from '@aparte/core';\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sources.ts — the sources a reply cites, read off the AI SDK's `fullStream`.
|
|
3
|
+
*
|
|
4
|
+
* Two things come out of the SDK. A `source` part is something the model drew on — a web
|
|
5
|
+
* page, a document — with no place in the text. From ai 7.0.138 a `text-end` part may also
|
|
6
|
+
* carry `citations`: each one names a source and, when the vendor says so, the passage it
|
|
7
|
+
* supports, with offsets relative to THAT text part. aparté's offsets are into all the text
|
|
8
|
+
* the call streamed, so a part's citations are rebased on where the part began.
|
|
9
|
+
*
|
|
10
|
+
* Both end in one `sources` event, emitted by the bridge just before `done`. The
|
|
11
|
+
* `citations` field is read without being typed: the installed SDK may predate it, and a
|
|
12
|
+
* field that is not there is the same as an empty list.
|
|
13
|
+
*/
|
|
14
|
+
import type { AparteStreamEvent, AparteChatResponse } from '@aparte/core';
|
|
15
|
+
/** The event {@link createSourceCollector} builds: what `AparteStreamEventMap['sources']` carries. */
|
|
16
|
+
export type SourcesEvent = Extract<AparteStreamEvent, {
|
|
17
|
+
type: 'sources';
|
|
18
|
+
}>;
|
|
19
|
+
/** Collects sources and citations as parts stream by. One collector per call. */
|
|
20
|
+
export declare function createSourceCollector(): {
|
|
21
|
+
addSource: (part: unknown, snippet?: string) => string | undefined;
|
|
22
|
+
/** A `text-start`: the part begins where the text so far ends. */
|
|
23
|
+
textStart(id: string): void;
|
|
24
|
+
/** A `text-delta`. A part whose `text-start` was not seen begins at its first delta. */
|
|
25
|
+
textDelta(id: string, delta: string): void;
|
|
26
|
+
/**
|
|
27
|
+
* A `text-end`, and the `citations` it carries (ai 7.0.138+). Offsets are relative to
|
|
28
|
+
* the part: they are rebased onto the call's text, and kept inside the part they
|
|
29
|
+
* belong to. A citation with no offsets supports the part as a whole, so its chip goes
|
|
30
|
+
* at the part's end. Two sources on one passage are one citation.
|
|
31
|
+
*/
|
|
32
|
+
textEnd(id: string, list: unknown): void;
|
|
33
|
+
/** The `sources` event, or `undefined` when the call cited nothing: the stream is then untouched. */
|
|
34
|
+
finish(): SourcesEvent | undefined;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* A non-streamed answer: its text, as a string. A bare string cannot carry sources, so an
|
|
38
|
+
* answer that cites some is returned as the stream it would have been — `[text, sources,
|
|
39
|
+
* done]`, which `AparteChatResponse` allows. `sources` is the SDK's `result.sources`;
|
|
40
|
+
* `content` its `result.content`, whose text parts carry the `citations` (ai 7.0.138+).
|
|
41
|
+
*/
|
|
42
|
+
export declare function nonStreamedAnswer(text: string, sources: ReadonlyArray<unknown>, content: ReadonlyArray<unknown>): AparteChatResponse;
|
|
43
|
+
//# sourceMappingURL=sources.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sources.d.ts","sourceRoot":"","sources":["../src/sources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAgC,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAExG,sGAAsG;AACtG,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,iBAAiB,EAAE;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC,CAAC;AAO3E,iFAAiF;AACjF,wBAAgB,qBAAqB;sBAgBR,OAAO,YAAY,MAAM,KAAG,MAAM,GAAG,SAAS;IAiCnE,kEAAkE;kBACpD,MAAM,GAAG,IAAI;IAI3B,wFAAwF;kBAC1E,MAAM,SAAS,MAAM,GAAG,IAAI;IAO1C;;;;;OAKG;gBACS,MAAM,QAAQ,OAAO,GAAG,IAAI;IAsBxC,qGAAqG;cAC3F,YAAY,GAAG,SAAS;EAKzC;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC7B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,EAC/B,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,GAChC,kBAAkB,CAqBpB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aparte/provider-ai-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"homepage": "https://apartejs.dev/providers/ai/ai-sdk/",
|
|
5
5
|
"description": "Bridge any Vercel AI SDK model into aparté — bring your own @ai-sdk/* package (Anthropic, Google, OpenAI, 25+ vendors) and aparté renders it.",
|
|
6
6
|
"type": "module",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"node": ">=18"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@aparte/core": ">=0.
|
|
28
|
+
"@aparte/core": ">=0.28.0 <1.0.0",
|
|
29
29
|
"ai": "^7.0.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"typescript": "^5.4.0",
|
|
36
36
|
"vite": "^6.0.0",
|
|
37
37
|
"zod": "^4.1.8",
|
|
38
|
-
"@aparte/core": "0.
|
|
38
|
+
"@aparte/core": "0.28.0"
|
|
39
39
|
},
|
|
40
40
|
"keywords": [
|
|
41
41
|
"ai",
|