@tanstack-ai-browser/apfel 0.0.0-stage → 0.0.1
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/LICENSE +21 -0
- package/README.md +17 -2
- package/dist/adapter.d.ts +91 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +584 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/tool-calls.d.ts +32 -0
- package/dist/tool-calls.d.ts.map +1 -0
- package/dist/tool-calls.js +123 -0
- package/package.json +54 -4
- package/src/adapter.ts +771 -0
- package/src/index.ts +11 -0
- package/src/tool-calls.ts +134 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool calls Apple's model writes as text. apfel turns the model's tool
|
|
3
|
+
* calls into OpenAI `tool_calls`, but when it doesn't recognize one it
|
|
4
|
+
* passes it on as text instead, like
|
|
5
|
+
* `{"tool_calls": [{"function": "get_weather", "arguments": {…}}]}`.
|
|
6
|
+
*/
|
|
7
|
+
/** Where Apple's model starts a tool call it writes as text. */
|
|
8
|
+
export const JSON_TOOL_CALLS_MARKER = '{"tool_calls"';
|
|
9
|
+
/** The `{…}` that starts at `text[start]`, braces in strings ignored. */
|
|
10
|
+
function balancedObject(text, start) {
|
|
11
|
+
let depth = 0;
|
|
12
|
+
let inString = false;
|
|
13
|
+
for (let i = start; i < text.length; i++) {
|
|
14
|
+
const c = text[i];
|
|
15
|
+
if (inString) {
|
|
16
|
+
if (c === "\\")
|
|
17
|
+
i++;
|
|
18
|
+
else if (c === '"')
|
|
19
|
+
inString = false;
|
|
20
|
+
}
|
|
21
|
+
else if (c === '"')
|
|
22
|
+
inString = true;
|
|
23
|
+
else if (c === "{")
|
|
24
|
+
depth++;
|
|
25
|
+
else if (c === "}" && --depth === 0)
|
|
26
|
+
return text.slice(start, i + 1);
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
/** JSON arguments, read leniently: stray strings without a key are dropped. */
|
|
31
|
+
function parseJsonArgs(raw) {
|
|
32
|
+
for (const candidate of [raw, raw.replace(/,\s*"[^"]*"\s*(?=[,}])/g, "")]) {
|
|
33
|
+
try {
|
|
34
|
+
const value = JSON.parse(candidate);
|
|
35
|
+
const args = typeof value === "string" ? JSON.parse(value) : value;
|
|
36
|
+
if (args && typeof args === "object") {
|
|
37
|
+
return args;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Try the next reading.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Tool calls Apple's model wrote as JSON text. Its JSON varies and is often
|
|
48
|
+
* broken (`"function": "name"`, `"name: "name"`, stray strings in the
|
|
49
|
+
* arguments), so instead of parsing the whole thing, this finds each quoted
|
|
50
|
+
* name of a tool that exists and reads the `"arguments"` object after it.
|
|
51
|
+
*/
|
|
52
|
+
export function parseJsonToolCalls(text, toolNames) {
|
|
53
|
+
const mentions = [];
|
|
54
|
+
for (const name of toolNames) {
|
|
55
|
+
const quoted = `"${name}"`;
|
|
56
|
+
for (let at = text.indexOf(quoted); at >= 0; at = text.indexOf(quoted, at + 1)) {
|
|
57
|
+
mentions.push({ name, at });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
mentions.sort((a, b) => a.at - b.at);
|
|
61
|
+
const calls = [];
|
|
62
|
+
mentions.forEach(({ name, at }, i) => {
|
|
63
|
+
// The arguments belong to the last mention of a name before them. The
|
|
64
|
+
// model sometimes writes a broken "arguments" and then a good one: the
|
|
65
|
+
// first that reads is used.
|
|
66
|
+
const part = text.slice(at, mentions[i + 1]?.at ?? text.length);
|
|
67
|
+
for (const key of part.matchAll(/"arguments"?\s*:\s*/g)) {
|
|
68
|
+
const from = key.index + key[0].length;
|
|
69
|
+
const raw = part[from] === "{"
|
|
70
|
+
? balancedObject(part, from)
|
|
71
|
+
: /^"(?:[^"\\]|\\.)*"/.exec(part.slice(from))?.[0];
|
|
72
|
+
const args = raw ? parseJsonArgs(raw) : null;
|
|
73
|
+
if (args) {
|
|
74
|
+
calls.push({ name, args });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
return calls;
|
|
80
|
+
}
|
|
81
|
+
/** Length of the longest end of `text` that could start `marker`. */
|
|
82
|
+
function partialMarker(text, marker) {
|
|
83
|
+
for (let n = Math.min(text.length, marker.length); n > 0; n--) {
|
|
84
|
+
if (marker.startsWith(text.slice(-n)))
|
|
85
|
+
return n;
|
|
86
|
+
}
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Splits a streaming answer into the text before the first tool call written
|
|
91
|
+
* as text (passed through as it arrives) and the tool-call JSON after it
|
|
92
|
+
* (held back and parsed once the reply ends).
|
|
93
|
+
*/
|
|
94
|
+
export function createToolCallSplitter(marker = JSON_TOOL_CALLS_MARKER) {
|
|
95
|
+
let held = "";
|
|
96
|
+
let markup = null;
|
|
97
|
+
/** Text that can be shown now. */
|
|
98
|
+
function push(text) {
|
|
99
|
+
if (markup !== null) {
|
|
100
|
+
markup += text;
|
|
101
|
+
return "";
|
|
102
|
+
}
|
|
103
|
+
held += text;
|
|
104
|
+
const at = held.indexOf(marker);
|
|
105
|
+
if (at >= 0) {
|
|
106
|
+
const before = held.slice(0, at);
|
|
107
|
+
markup = held.slice(at);
|
|
108
|
+
held = "";
|
|
109
|
+
return before;
|
|
110
|
+
}
|
|
111
|
+
const keep = partialMarker(held, marker);
|
|
112
|
+
const out = held.slice(0, held.length - keep);
|
|
113
|
+
held = held.slice(out.length);
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
/** The rest of the text, and the tool-call JSON if any. */
|
|
117
|
+
function flush() {
|
|
118
|
+
const rest = held;
|
|
119
|
+
held = "";
|
|
120
|
+
return { text: rest, markup };
|
|
121
|
+
}
|
|
122
|
+
return { push, flush };
|
|
123
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack-ai-browser/apfel",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "TanStack AI adapter for Apple's on-device Foundation Model, served by apfel",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "daveycodez",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/daveycodez/tanstack-ai-browser.git",
|
|
10
|
+
"directory": "packages/apfel"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/daveycodez/tanstack-ai-browser/tree/main/packages/apfel#readme",
|
|
13
|
+
"bugs": "https://github.com/daveycodez/tanstack-ai-browser/issues",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"tanstack-ai",
|
|
16
|
+
"tanstack",
|
|
17
|
+
"ai",
|
|
18
|
+
"llm",
|
|
19
|
+
"apple",
|
|
20
|
+
"foundation-models",
|
|
21
|
+
"apfel",
|
|
22
|
+
"on-device",
|
|
23
|
+
"local-ai"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"main": "./dist/index.js",
|
|
28
|
+
"module": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
"./package.json": "./package.json",
|
|
32
|
+
".": {
|
|
33
|
+
"@tanstack-ai-browser/source": "./src/index.ts",
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"import": "./dist/index.js",
|
|
36
|
+
"default": "./dist/index.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"src",
|
|
41
|
+
"dist",
|
|
42
|
+
"!**/*.tsbuildinfo"
|
|
43
|
+
],
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"nx": {
|
|
48
|
+
"name": "apfel"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@tanstack/ai": "^1.0.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@tanstack/ai": "1.0.0"
|
|
55
|
+
}
|
|
56
|
+
}
|