@bitbaum/ai-kit 1.4.0 → 1.4.2
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/capability/classify.d.ts +4 -3
- package/dist/capability/classify.js +30 -9
- package/dist/complete.d.ts +30 -2
- package/dist/grounding/verify.js +19 -1
- package/dist/index.d.ts +1 -1
- package/dist-cjs/grounding/verify.js +19 -1
- package/package.json +4 -4
- package/src/capability/classify.ts +30 -9
- package/src/complete.ts +24 -2
- package/src/grounding/verify.ts +19 -1
- package/src/index.ts +1 -0
|
@@ -23,9 +23,10 @@ import type { Classification } from "./types.js";
|
|
|
23
23
|
/**
|
|
24
24
|
* Does this error body explicitly say the model cannot do tools?
|
|
25
25
|
*
|
|
26
|
-
* Conservative on purpose — see the header.
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* Conservative on purpose — see the header. Both failures are real and neither
|
|
27
|
+
* is free: a false positive downgrades a working model for a reason nobody can
|
|
28
|
+
* see, and a false negative leaves the caller repeating a request it will
|
|
29
|
+
* never learn from. Match on "names tools AND negates support", nothing looser.
|
|
29
30
|
*/
|
|
30
31
|
export declare function saysToolsUnsupported(body: string): boolean;
|
|
31
32
|
export type ToolAttempt = {
|
|
@@ -4,29 +4,50 @@
|
|
|
4
4
|
* Every entry names tools or functions explicitly. Deliberately absent:
|
|
5
5
|
* "invalid request", "bad parameter", "unsupported" on its own — each of those
|
|
6
6
|
* appears in vendor 400s for a dozen unrelated reasons, and a match on one of
|
|
7
|
-
* them would silently disable a working model.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* them would silently disable a working model.
|
|
8
|
+
*
|
|
9
|
+
* But "when in doubt, record nothing" is only cheap for a caller that can
|
|
10
|
+
* retry freely, and the real caller cannot. A caller that sends definitions
|
|
11
|
+
* ALSO drops whatever prose fallback it has, because definitions are supposed
|
|
12
|
+
* to replace it — so a refusal this list fails to recognise costs a whole turn
|
|
13
|
+
* in which the model can neither call a tool nor be told how to act without
|
|
14
|
+
* one, and it costs that turn EVERY turn, forever, because nothing is ever
|
|
15
|
+
* learned. A missed phrasing is not one wasted request; it is a permanently
|
|
16
|
+
* mute model.
|
|
17
|
+
*
|
|
18
|
+
* So the bar is: does the vendor NAME tools or functions, and NEGATE support?
|
|
19
|
+
* Both halves, explicitly. Everything meeting that bar belongs here, including
|
|
20
|
+
* the boring grammatical variants — plural, `are`, `cannot use` — which is
|
|
21
|
+
* where the first real gap was found (`tools are not supported by this model`
|
|
22
|
+
* matched nothing at all).
|
|
10
23
|
*/
|
|
11
24
|
const TOOLS_UNSUPPORTED_PATTERNS = [
|
|
12
|
-
/tool[\s_-]?(use|call|calls|calling)\s+(is\s+)?(not|un)[\s_-]?support/i,
|
|
25
|
+
/tool[\s_-]?(use|call|calls|calling)\s+(is\s+|are\s+)?(not|un)[\s_-]?support/i,
|
|
13
26
|
/does\s+not\s+support\s+tool/i,
|
|
14
27
|
/doesn'?t\s+support\s+tool/i,
|
|
15
28
|
/no\s+support\s+for\s+tool/i,
|
|
16
|
-
/function[\s_-]?call(ing)?\s+(is\s+)?(not|un)[\s_-]?support/i,
|
|
29
|
+
/function[\s_-]?call(ing)?\s+(is\s+|are\s+)?(not|un)[\s_-]?support/i,
|
|
17
30
|
/does\s+not\s+support\s+function/i,
|
|
18
31
|
/doesn'?t\s+support\s+function/i,
|
|
19
32
|
/model\s+.{0,60}?\s+does\s+not\s+support\s+(the\s+)?(`?tools`?|`?functions`?)/i,
|
|
20
33
|
/unsupported\s+parameter:?\s*'?"?tools?"?'?/i,
|
|
21
34
|
/unknown\s+(field|parameter):?\s*'?"?tools?"?'?/i,
|
|
22
|
-
|
|
35
|
+
// "tools are not supported", "tool is not valid", "functions are unsupported".
|
|
36
|
+
// The singular-and-`is` form of this was already here; the plural-and-`are`
|
|
37
|
+
// form is the one vendors actually write, and it matched nothing.
|
|
38
|
+
/`?(tools?|functions?)`?\s+(is|are)\s+(not\s+(a\s+)?(valid|supported|allowed|available)|unsupported)/i,
|
|
39
|
+
// "this model cannot use tools" / "can't call functions".
|
|
40
|
+
/(cannot|can'?t|is\s+unable\s+to)\s+(use|call|handle|execute)\s+(`?tools?`?|`?functions?`?)/i,
|
|
41
|
+
// "no tool support", "without function support".
|
|
42
|
+
/no\s+(`?tools?`?|`?functions?`?)\s+support/i,
|
|
23
43
|
];
|
|
24
44
|
/**
|
|
25
45
|
* Does this error body explicitly say the model cannot do tools?
|
|
26
46
|
*
|
|
27
|
-
* Conservative on purpose — see the header.
|
|
28
|
-
*
|
|
29
|
-
*
|
|
47
|
+
* Conservative on purpose — see the header. Both failures are real and neither
|
|
48
|
+
* is free: a false positive downgrades a working model for a reason nobody can
|
|
49
|
+
* see, and a false negative leaves the caller repeating a request it will
|
|
50
|
+
* never learn from. Match on "names tools AND negates support", nothing looser.
|
|
30
51
|
*/
|
|
31
52
|
export function saysToolsUnsupported(body) {
|
|
32
53
|
if (!body)
|
package/dist/complete.d.ts
CHANGED
|
@@ -59,10 +59,38 @@ import { type Env, type Link, type Provider } from "./chain.js";
|
|
|
59
59
|
import type { HealthTracker } from "./health.js";
|
|
60
60
|
import { type RateLimitKind } from "./limits.js";
|
|
61
61
|
import { type QuotaReading } from "./meter.js";
|
|
62
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* A piece of a message, for the models that accept more than text.
|
|
64
|
+
*
|
|
65
|
+
* The same shape every provider here already speaks, because it is OpenAI's —
|
|
66
|
+
* `image_url.url` takes a `data:` URL or an `https:` one.
|
|
67
|
+
*/
|
|
68
|
+
export type ContentPart = {
|
|
69
|
+
type: "text";
|
|
70
|
+
text: string;
|
|
71
|
+
} | {
|
|
72
|
+
type: "image_url";
|
|
73
|
+
image_url: {
|
|
74
|
+
url: string;
|
|
75
|
+
detail?: "auto" | "low" | "high";
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* One message in the OpenAI chat-completions shape every provider here speaks.
|
|
80
|
+
*
|
|
81
|
+
* `content` accepts parts as well as a string because the implementation
|
|
82
|
+
* always carried them: `callLink` forwards `messages` into the request body
|
|
83
|
+
* untouched, so a multimodal message has worked at runtime since the first
|
|
84
|
+
* release while the type insisted it could not. A consumer that needed to send
|
|
85
|
+
* a screenshot therefore had to cast around our own type — and a cast written
|
|
86
|
+
* to work around a library is a thing the next consumer copies.
|
|
87
|
+
*
|
|
88
|
+
* Backward compatible by construction: every existing caller passes a string,
|
|
89
|
+
* and a string is still a `ChatMessage["content"]`.
|
|
90
|
+
*/
|
|
63
91
|
export interface ChatMessage {
|
|
64
92
|
role: "system" | "user" | "assistant" | "tool";
|
|
65
|
-
content: string;
|
|
93
|
+
content: string | ContentPart[];
|
|
66
94
|
/** Present on `role: "tool"` replies; passed through untouched. */
|
|
67
95
|
tool_call_id?: string;
|
|
68
96
|
name?: string;
|
package/dist/grounding/verify.js
CHANGED
|
@@ -183,7 +183,25 @@ function properNounRuns(text) {
|
|
|
183
183
|
const out = [];
|
|
184
184
|
// Strip fenced and inline code — quoted identifiers are usually the user's
|
|
185
185
|
// own or a literal under discussion, not a claim about the world.
|
|
186
|
-
|
|
186
|
+
//
|
|
187
|
+
// Markdown table pipes become line breaks for the same reason a full stop is
|
|
188
|
+
// a boundary: a cell is its own utterance, and a run must not span two of
|
|
189
|
+
// them. Observed on a live answer that laid its findings out as a table —
|
|
190
|
+
// the header row `| Category | Item | Status | Notes |` yielded the runs
|
|
191
|
+
// "Item" and "Item Status Notes Pending", the last of those having run on
|
|
192
|
+
// into the FIRST CELL OF THE NEXT ROW. The answer was entirely grounded and
|
|
193
|
+
// was reported as fabricating, which is the worst direction for this check
|
|
194
|
+
// to fail in: a warning that fires on correct answers teaches the operator
|
|
195
|
+
// to dismiss the warning.
|
|
196
|
+
//
|
|
197
|
+
// Splitting per cell also makes each header word cell-initial, and
|
|
198
|
+
// sentence-initial single words are already skipped for exactly this reason.
|
|
199
|
+
// Recall is barely touched: a fabricated name INSIDE a cell still trips its
|
|
200
|
+
// remaining tokens, the same trade already accepted at sentence starts.
|
|
201
|
+
const prose = text
|
|
202
|
+
.replace(/```[\s\S]*?```/g, " ")
|
|
203
|
+
.replace(/`[^`]*`/g, " ")
|
|
204
|
+
.replace(/\|/g, "\n");
|
|
187
205
|
for (const sentence of prose.split(/(?<=[.!?:\n])\s+/)) {
|
|
188
206
|
const tokens = sentence.match(/[A-Za-z][A-Za-z0-9&.'’-]*/g) ?? [];
|
|
189
207
|
let run = [];
|
package/dist/index.d.ts
CHANGED
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
export { type Provider, type Env, type Link, type CostVerdict, providerModels, withEnvPrefix, freeChain, modelCost, modelCostAt, paidModelsIn, dayCapacityTokens, usableChain, chainFrom, } from "./chain.js";
|
|
53
53
|
export { type CatalogVerdict, type CheckCatalogOptions, checkCatalog, hasRot, deadProviders, catalogReport, } from "./catalog.js";
|
|
54
54
|
export { type ChainAttemptFailure, type TryChainOptions, ChainExhaustedError, tryChain, } from "./attempt.js";
|
|
55
|
-
export { type ChatMessage, type ToolCall, type CompleteOptions, type CompleteResult, LinkFailure, complete, linkId, } from "./complete.js";
|
|
55
|
+
export { type ChatMessage, type ContentPart, type ToolCall, type CompleteOptions, type CompleteResult, LinkFailure, complete, linkId, } from "./complete.js";
|
|
56
56
|
export { type HealthStatus, type Health, type HealthTrackerOptions, type HealthTracker, createHealthTracker, } from "./health.js";
|
|
57
57
|
export { type LivenessResult, type LivenessOptions, type LivenessProbe, type AiHealthHandlerOptions, createLivenessProbe, createAiHealthHandler, } from "./liveness.js";
|
|
58
58
|
export { type RateLimitKind, classifyRateLimit, retryAfterSeconds, humanizeWait, rateLimitMessage, } from "./limits.js";
|
|
@@ -187,7 +187,25 @@ function properNounRuns(text) {
|
|
|
187
187
|
const out = [];
|
|
188
188
|
// Strip fenced and inline code — quoted identifiers are usually the user's
|
|
189
189
|
// own or a literal under discussion, not a claim about the world.
|
|
190
|
-
|
|
190
|
+
//
|
|
191
|
+
// Markdown table pipes become line breaks for the same reason a full stop is
|
|
192
|
+
// a boundary: a cell is its own utterance, and a run must not span two of
|
|
193
|
+
// them. Observed on a live answer that laid its findings out as a table —
|
|
194
|
+
// the header row `| Category | Item | Status | Notes |` yielded the runs
|
|
195
|
+
// "Item" and "Item Status Notes Pending", the last of those having run on
|
|
196
|
+
// into the FIRST CELL OF THE NEXT ROW. The answer was entirely grounded and
|
|
197
|
+
// was reported as fabricating, which is the worst direction for this check
|
|
198
|
+
// to fail in: a warning that fires on correct answers teaches the operator
|
|
199
|
+
// to dismiss the warning.
|
|
200
|
+
//
|
|
201
|
+
// Splitting per cell also makes each header word cell-initial, and
|
|
202
|
+
// sentence-initial single words are already skipped for exactly this reason.
|
|
203
|
+
// Recall is barely touched: a fabricated name INSIDE a cell still trips its
|
|
204
|
+
// remaining tokens, the same trade already accepted at sentence starts.
|
|
205
|
+
const prose = text
|
|
206
|
+
.replace(/```[\s\S]*?```/g, " ")
|
|
207
|
+
.replace(/`[^`]*`/g, " ")
|
|
208
|
+
.replace(/\|/g, "\n");
|
|
191
209
|
for (const sentence of prose.split(/(?<=[.!?:\n])\s+/)) {
|
|
192
210
|
const tokens = sentence.match(/[A-Za-z][A-Za-z0-9&.'’-]*/g) ?? [];
|
|
193
211
|
let run = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitbaum/ai-kit",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Mao Nakamoto",
|
|
6
6
|
"homepage": "https://github.com/bitbaum/ai-kit#readme",
|
|
@@ -90,12 +90,12 @@
|
|
|
90
90
|
},
|
|
91
91
|
"devDependencies": {
|
|
92
92
|
"@eslint/js": "^10.0.1",
|
|
93
|
-
"@types/node": "^26.
|
|
94
|
-
"eslint": "^10.
|
|
93
|
+
"@types/node": "^26.5.0",
|
|
94
|
+
"eslint": "^10.10.0",
|
|
95
95
|
"globals": "^17.12.0",
|
|
96
96
|
"prettier": "3.9.6",
|
|
97
97
|
"typescript": "^6.0.3",
|
|
98
|
-
"typescript-eslint": "^8.
|
|
98
|
+
"typescript-eslint": "^8.70.0"
|
|
99
99
|
},
|
|
100
100
|
"dependencies": {
|
|
101
101
|
"ai-forms": "^0.1.2"
|
|
@@ -27,30 +27,51 @@ import type { Classification } from "./types.js";
|
|
|
27
27
|
* Every entry names tools or functions explicitly. Deliberately absent:
|
|
28
28
|
* "invalid request", "bad parameter", "unsupported" on its own — each of those
|
|
29
29
|
* appears in vendor 400s for a dozen unrelated reasons, and a match on one of
|
|
30
|
-
* them would silently disable a working model.
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* them would silently disable a working model.
|
|
31
|
+
*
|
|
32
|
+
* But "when in doubt, record nothing" is only cheap for a caller that can
|
|
33
|
+
* retry freely, and the real caller cannot. A caller that sends definitions
|
|
34
|
+
* ALSO drops whatever prose fallback it has, because definitions are supposed
|
|
35
|
+
* to replace it — so a refusal this list fails to recognise costs a whole turn
|
|
36
|
+
* in which the model can neither call a tool nor be told how to act without
|
|
37
|
+
* one, and it costs that turn EVERY turn, forever, because nothing is ever
|
|
38
|
+
* learned. A missed phrasing is not one wasted request; it is a permanently
|
|
39
|
+
* mute model.
|
|
40
|
+
*
|
|
41
|
+
* So the bar is: does the vendor NAME tools or functions, and NEGATE support?
|
|
42
|
+
* Both halves, explicitly. Everything meeting that bar belongs here, including
|
|
43
|
+
* the boring grammatical variants — plural, `are`, `cannot use` — which is
|
|
44
|
+
* where the first real gap was found (`tools are not supported by this model`
|
|
45
|
+
* matched nothing at all).
|
|
33
46
|
*/
|
|
34
47
|
const TOOLS_UNSUPPORTED_PATTERNS: RegExp[] = [
|
|
35
|
-
/tool[\s_-]?(use|call|calls|calling)\s+(is\s+)?(not|un)[\s_-]?support/i,
|
|
48
|
+
/tool[\s_-]?(use|call|calls|calling)\s+(is\s+|are\s+)?(not|un)[\s_-]?support/i,
|
|
36
49
|
/does\s+not\s+support\s+tool/i,
|
|
37
50
|
/doesn'?t\s+support\s+tool/i,
|
|
38
51
|
/no\s+support\s+for\s+tool/i,
|
|
39
|
-
/function[\s_-]?call(ing)?\s+(is\s+)?(not|un)[\s_-]?support/i,
|
|
52
|
+
/function[\s_-]?call(ing)?\s+(is\s+|are\s+)?(not|un)[\s_-]?support/i,
|
|
40
53
|
/does\s+not\s+support\s+function/i,
|
|
41
54
|
/doesn'?t\s+support\s+function/i,
|
|
42
55
|
/model\s+.{0,60}?\s+does\s+not\s+support\s+(the\s+)?(`?tools`?|`?functions`?)/i,
|
|
43
56
|
/unsupported\s+parameter:?\s*'?"?tools?"?'?/i,
|
|
44
57
|
/unknown\s+(field|parameter):?\s*'?"?tools?"?'?/i,
|
|
45
|
-
|
|
58
|
+
// "tools are not supported", "tool is not valid", "functions are unsupported".
|
|
59
|
+
// The singular-and-`is` form of this was already here; the plural-and-`are`
|
|
60
|
+
// form is the one vendors actually write, and it matched nothing.
|
|
61
|
+
/`?(tools?|functions?)`?\s+(is|are)\s+(not\s+(a\s+)?(valid|supported|allowed|available)|unsupported)/i,
|
|
62
|
+
// "this model cannot use tools" / "can't call functions".
|
|
63
|
+
/(cannot|can'?t|is\s+unable\s+to)\s+(use|call|handle|execute)\s+(`?tools?`?|`?functions?`?)/i,
|
|
64
|
+
// "no tool support", "without function support".
|
|
65
|
+
/no\s+(`?tools?`?|`?functions?`?)\s+support/i,
|
|
46
66
|
];
|
|
47
67
|
|
|
48
68
|
/**
|
|
49
69
|
* Does this error body explicitly say the model cannot do tools?
|
|
50
70
|
*
|
|
51
|
-
* Conservative on purpose — see the header.
|
|
52
|
-
*
|
|
53
|
-
*
|
|
71
|
+
* Conservative on purpose — see the header. Both failures are real and neither
|
|
72
|
+
* is free: a false positive downgrades a working model for a reason nobody can
|
|
73
|
+
* see, and a false negative leaves the caller repeating a request it will
|
|
74
|
+
* never learn from. Match on "names tools AND negates support", nothing looser.
|
|
54
75
|
*/
|
|
55
76
|
export function saysToolsUnsupported(body: string): boolean {
|
|
56
77
|
if (!body) return false;
|
package/src/complete.ts
CHANGED
|
@@ -63,10 +63,32 @@ import { classifyRateLimit, retryAfterSeconds, type RateLimitKind } from "./limi
|
|
|
63
63
|
import { readQuota, readingFromRefusal, type QuotaReading } from "./meter.js";
|
|
64
64
|
import { parseTextToolCalls, stripToolCallLines, toolNamesFrom } from "./tool-protocol.js";
|
|
65
65
|
|
|
66
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* A piece of a message, for the models that accept more than text.
|
|
68
|
+
*
|
|
69
|
+
* The same shape every provider here already speaks, because it is OpenAI's —
|
|
70
|
+
* `image_url.url` takes a `data:` URL or an `https:` one.
|
|
71
|
+
*/
|
|
72
|
+
export type ContentPart =
|
|
73
|
+
| { type: "text"; text: string }
|
|
74
|
+
| { type: "image_url"; image_url: { url: string; detail?: "auto" | "low" | "high" } };
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* One message in the OpenAI chat-completions shape every provider here speaks.
|
|
78
|
+
*
|
|
79
|
+
* `content` accepts parts as well as a string because the implementation
|
|
80
|
+
* always carried them: `callLink` forwards `messages` into the request body
|
|
81
|
+
* untouched, so a multimodal message has worked at runtime since the first
|
|
82
|
+
* release while the type insisted it could not. A consumer that needed to send
|
|
83
|
+
* a screenshot therefore had to cast around our own type — and a cast written
|
|
84
|
+
* to work around a library is a thing the next consumer copies.
|
|
85
|
+
*
|
|
86
|
+
* Backward compatible by construction: every existing caller passes a string,
|
|
87
|
+
* and a string is still a `ChatMessage["content"]`.
|
|
88
|
+
*/
|
|
67
89
|
export interface ChatMessage {
|
|
68
90
|
role: "system" | "user" | "assistant" | "tool";
|
|
69
|
-
content: string;
|
|
91
|
+
content: string | ContentPart[];
|
|
70
92
|
/** Present on `role: "tool"` replies; passed through untouched. */
|
|
71
93
|
tool_call_id?: string;
|
|
72
94
|
name?: string;
|
package/src/grounding/verify.ts
CHANGED
|
@@ -201,7 +201,25 @@ function properNounRuns(text: string): string[] {
|
|
|
201
201
|
const out: string[] = [];
|
|
202
202
|
// Strip fenced and inline code — quoted identifiers are usually the user's
|
|
203
203
|
// own or a literal under discussion, not a claim about the world.
|
|
204
|
-
|
|
204
|
+
//
|
|
205
|
+
// Markdown table pipes become line breaks for the same reason a full stop is
|
|
206
|
+
// a boundary: a cell is its own utterance, and a run must not span two of
|
|
207
|
+
// them. Observed on a live answer that laid its findings out as a table —
|
|
208
|
+
// the header row `| Category | Item | Status | Notes |` yielded the runs
|
|
209
|
+
// "Item" and "Item Status Notes Pending", the last of those having run on
|
|
210
|
+
// into the FIRST CELL OF THE NEXT ROW. The answer was entirely grounded and
|
|
211
|
+
// was reported as fabricating, which is the worst direction for this check
|
|
212
|
+
// to fail in: a warning that fires on correct answers teaches the operator
|
|
213
|
+
// to dismiss the warning.
|
|
214
|
+
//
|
|
215
|
+
// Splitting per cell also makes each header word cell-initial, and
|
|
216
|
+
// sentence-initial single words are already skipped for exactly this reason.
|
|
217
|
+
// Recall is barely touched: a fabricated name INSIDE a cell still trips its
|
|
218
|
+
// remaining tokens, the same trade already accepted at sentence starts.
|
|
219
|
+
const prose = text
|
|
220
|
+
.replace(/```[\s\S]*?```/g, " ")
|
|
221
|
+
.replace(/`[^`]*`/g, " ")
|
|
222
|
+
.replace(/\|/g, "\n");
|
|
205
223
|
|
|
206
224
|
for (const sentence of prose.split(/(?<=[.!?:\n])\s+/)) {
|
|
207
225
|
const tokens = sentence.match(/[A-Za-z][A-Za-z0-9&.'’-]*/g) ?? [];
|