anyclaude-sdk 0.4.9 → 0.6.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/dist/agent.d.ts +5 -0
- package/dist/agent.js +15 -1
- package/dist/anthropic-endpoint.d.ts +97 -0
- package/dist/anthropic-endpoint.js +271 -0
- package/dist/llm/dialects.d.ts +32 -0
- package/dist/llm/dialects.js +218 -0
- package/dist/llm/index.d.ts +3 -0
- package/dist/llm/index.js +9 -0
- package/dist/llm/inlineTools.d.ts +2 -3
- package/dist/llm/inlineTools.js +5 -66
- package/dist/llm/openai.d.ts +15 -0
- package/dist/llm/openai.js +17 -9
- package/dist/llm/profiles.d.ts +35 -0
- package/dist/llm/profiles.js +123 -0
- package/dist/llm/repair.d.ts +20 -0
- package/dist/llm/repair.js +96 -0
- package/dist/loop.d.ts +7 -0
- package/dist/loop.js +50 -36
- package/package.json +5 -1
package/dist/agent.d.ts
CHANGED
|
@@ -49,6 +49,11 @@ export interface AgentOptions {
|
|
|
49
49
|
content: string | ContentBlockParam[];
|
|
50
50
|
is_error?: boolean;
|
|
51
51
|
}>;
|
|
52
|
+
/** Validate tool arguments before executing; on malformed/incomplete JSON,
|
|
53
|
+
* return a corrective `is_error` tool_result (with the expected schema) so the
|
|
54
|
+
* model self-heals instead of running with garbage. Default `true`. The single
|
|
55
|
+
* biggest reliability win for weak/cheap models. See `anyclaude-sdk/llm` repair. */
|
|
56
|
+
repairToolCalls?: boolean;
|
|
52
57
|
cwd?: string;
|
|
53
58
|
sessionId?: string;
|
|
54
59
|
abortController?: AbortController;
|
package/dist/agent.js
CHANGED
|
@@ -24,6 +24,7 @@ import { defaultSystemPrompt, defaultSubagentPrompt } from './prompt.js';
|
|
|
24
24
|
import { DEFAULT_MAX_RESULT_CHARS, maybePersistLargeResult } from './persist.js';
|
|
25
25
|
import { computeCostUSD, contextWindowFor } from './util/pricing.js';
|
|
26
26
|
import { estimateTokens, summarizeHistory } from './compact.js';
|
|
27
|
+
import { validateToolArguments } from './llm/repair.js';
|
|
27
28
|
import { uuid } from './util/ids.js';
|
|
28
29
|
/** Wrap a single text prompt into the async-iterable form runAgent expects. */
|
|
29
30
|
async function* singleUserPrompt(text) {
|
|
@@ -203,6 +204,7 @@ export async function* runAgent(options) {
|
|
|
203
204
|
: undefined;
|
|
204
205
|
const messageQueue = options.messageQueue;
|
|
205
206
|
const clientTools = new Set(options.clientTools ?? []);
|
|
207
|
+
const repairToolCalls = options.repairToolCalls !== false;
|
|
206
208
|
// Teammates: a shared Mailbox + TaskBoard (reused from the parent when this
|
|
207
209
|
// is a sub-agent) + team tools + coordinator prompt.
|
|
208
210
|
const teamEnabled = options.team === true;
|
|
@@ -717,7 +719,19 @@ export async function* runAgent(options) {
|
|
|
717
719
|
let content = '';
|
|
718
720
|
let isError = false;
|
|
719
721
|
let extraContext = '';
|
|
720
|
-
|
|
722
|
+
// Repair: validate args against the tool schema before running a server
|
|
723
|
+
// tool; on malformed/incomplete JSON, return a corrective tool_result so
|
|
724
|
+
// the model retries with valid JSON instead of executing with garbage.
|
|
725
|
+
const repairCheck = repairToolCalls && tool && tool.run
|
|
726
|
+
? validateToolArguments(tool.def, call.function.arguments)
|
|
727
|
+
: null;
|
|
728
|
+
if (repairCheck && repairCheck.ok)
|
|
729
|
+
input = repairCheck.input;
|
|
730
|
+
if (repairCheck && !repairCheck.ok) {
|
|
731
|
+
content = repairCheck.error;
|
|
732
|
+
isError = true;
|
|
733
|
+
}
|
|
734
|
+
else if (!tool || !tool.run) {
|
|
721
735
|
// Unknown, or a run-less (client-delegated) tool that somehow reached
|
|
722
736
|
// server execution — both are errors here (delegated tools are handled
|
|
723
737
|
// above via clientTools).
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { ChatMsg, LLMClient, StreamResult, ToolDef } from './types/index.js';
|
|
2
|
+
export interface AnthropicTextBlock {
|
|
3
|
+
type: 'text';
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
export interface AnthropicImageBlock {
|
|
7
|
+
type: 'image';
|
|
8
|
+
source: {
|
|
9
|
+
type: 'base64';
|
|
10
|
+
media_type: string;
|
|
11
|
+
data: string;
|
|
12
|
+
} | {
|
|
13
|
+
type: 'url';
|
|
14
|
+
url: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export interface AnthropicToolUseBlock {
|
|
18
|
+
type: 'tool_use';
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
input: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
export interface AnthropicToolResultBlock {
|
|
24
|
+
type: 'tool_result';
|
|
25
|
+
tool_use_id: string;
|
|
26
|
+
content: string | Array<{
|
|
27
|
+
type: string;
|
|
28
|
+
text?: string;
|
|
29
|
+
[k: string]: unknown;
|
|
30
|
+
}>;
|
|
31
|
+
is_error?: boolean;
|
|
32
|
+
}
|
|
33
|
+
export type AnthropicContentBlock = AnthropicTextBlock | AnthropicImageBlock | AnthropicToolUseBlock | AnthropicToolResultBlock | {
|
|
34
|
+
type: string;
|
|
35
|
+
[k: string]: unknown;
|
|
36
|
+
};
|
|
37
|
+
export interface AnthropicMessage {
|
|
38
|
+
role: 'user' | 'assistant';
|
|
39
|
+
content: string | AnthropicContentBlock[];
|
|
40
|
+
}
|
|
41
|
+
export interface AnthropicMessagesRequest {
|
|
42
|
+
model: string;
|
|
43
|
+
max_tokens?: number;
|
|
44
|
+
system?: string | Array<{
|
|
45
|
+
type: 'text';
|
|
46
|
+
text: string;
|
|
47
|
+
[k: string]: unknown;
|
|
48
|
+
}>;
|
|
49
|
+
messages: AnthropicMessage[];
|
|
50
|
+
tools?: Array<{
|
|
51
|
+
name: string;
|
|
52
|
+
description?: string;
|
|
53
|
+
input_schema: Record<string, unknown>;
|
|
54
|
+
}>;
|
|
55
|
+
tool_choice?: {
|
|
56
|
+
type: 'auto' | 'any' | 'tool' | 'none';
|
|
57
|
+
name?: string;
|
|
58
|
+
};
|
|
59
|
+
temperature?: number;
|
|
60
|
+
stream?: boolean;
|
|
61
|
+
[k: string]: unknown;
|
|
62
|
+
}
|
|
63
|
+
/** The neutral request the SDK's LLMClient consumes. */
|
|
64
|
+
export interface ChatRequest {
|
|
65
|
+
messages: ChatMsg[];
|
|
66
|
+
tools: ToolDef[];
|
|
67
|
+
model: string;
|
|
68
|
+
maxTokens?: number;
|
|
69
|
+
temperature?: number;
|
|
70
|
+
stream: boolean;
|
|
71
|
+
}
|
|
72
|
+
/** Convert Anthropic `tools` into the SDK's OpenAI-shape `ToolDef[]`. */
|
|
73
|
+
export declare function anthropicToolsToDefs(tools: AnthropicMessagesRequest['tools']): ToolDef[];
|
|
74
|
+
/**
|
|
75
|
+
* Convert an Anthropic Messages request into the neutral `ChatRequest` the SDK
|
|
76
|
+
* consumes. Anthropic packs `tool_result` blocks inside user messages; we split
|
|
77
|
+
* them into separate `tool` ChatMsgs (OpenAI shape). `tool_use` blocks on an
|
|
78
|
+
* assistant message become `tool_calls`.
|
|
79
|
+
*/
|
|
80
|
+
export declare function anthropicToChat(body: AnthropicMessagesRequest): ChatRequest;
|
|
81
|
+
/** Build a non-streaming Anthropic Messages response object from a StreamResult. */
|
|
82
|
+
export declare function streamResultToAnthropicMessage(result: StreamResult, opts: {
|
|
83
|
+
model: string;
|
|
84
|
+
id?: string;
|
|
85
|
+
}): Record<string, unknown>;
|
|
86
|
+
/**
|
|
87
|
+
* Run a turn through `llm` and yield the Anthropic Messages **SSE** event
|
|
88
|
+
* sequence as strings (message_start -> content_block_* -> message_delta ->
|
|
89
|
+
* message_stop). Text streams live; tool calls (native or dialect-recovered)
|
|
90
|
+
* are emitted as `tool_use` blocks with a single `input_json_delta`. Pipe the
|
|
91
|
+
* yielded strings straight to an HTTP response body.
|
|
92
|
+
*/
|
|
93
|
+
export declare function anthropicSSE(llm: LLMClient, req: ChatRequest, opts?: {
|
|
94
|
+
model: string;
|
|
95
|
+
signal?: AbortSignal;
|
|
96
|
+
id?: string;
|
|
97
|
+
}): AsyncGenerator<string>;
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { uuid } from './util/ids.js';
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Request: Anthropic Messages -> ChatMsg[] + ToolDef[]
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
/** Convert Anthropic `tools` into the SDK's OpenAI-shape `ToolDef[]`. */
|
|
6
|
+
export function anthropicToolsToDefs(tools) {
|
|
7
|
+
if (!tools?.length)
|
|
8
|
+
return [];
|
|
9
|
+
return tools.map((t) => ({
|
|
10
|
+
type: 'function',
|
|
11
|
+
function: {
|
|
12
|
+
name: t.name,
|
|
13
|
+
description: t.description ?? '',
|
|
14
|
+
parameters: t.input_schema ?? {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
function systemToText(system) {
|
|
22
|
+
if (!system)
|
|
23
|
+
return '';
|
|
24
|
+
if (typeof system === 'string')
|
|
25
|
+
return system;
|
|
26
|
+
return system.map((b) => b.text ?? '').join('\n');
|
|
27
|
+
}
|
|
28
|
+
function anthropicImageToBlock(b) {
|
|
29
|
+
if (b.source.type === 'base64') {
|
|
30
|
+
return { type: 'image', source: { type: 'base64', media_type: b.source.media_type, data: b.source.data } };
|
|
31
|
+
}
|
|
32
|
+
// URL images — pass through as a text marker; most OpenAI-compatible chat
|
|
33
|
+
// endpoints want a data URL, which we don't have here.
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
function resultBlockToText(content) {
|
|
37
|
+
if (typeof content === 'string')
|
|
38
|
+
return content;
|
|
39
|
+
return content
|
|
40
|
+
.map((c) => (c.type === 'text' ? (c.text ?? '') : JSON.stringify(c)))
|
|
41
|
+
.filter(Boolean)
|
|
42
|
+
.join('\n');
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Convert an Anthropic Messages request into the neutral `ChatRequest` the SDK
|
|
46
|
+
* consumes. Anthropic packs `tool_result` blocks inside user messages; we split
|
|
47
|
+
* them into separate `tool` ChatMsgs (OpenAI shape). `tool_use` blocks on an
|
|
48
|
+
* assistant message become `tool_calls`.
|
|
49
|
+
*/
|
|
50
|
+
export function anthropicToChat(body) {
|
|
51
|
+
const messages = [];
|
|
52
|
+
const system = systemToText(body.system);
|
|
53
|
+
if (system)
|
|
54
|
+
messages.push({ role: 'system', content: system });
|
|
55
|
+
for (const msg of body.messages) {
|
|
56
|
+
if (typeof msg.content === 'string') {
|
|
57
|
+
messages.push({ role: msg.role, content: msg.content });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (msg.role === 'assistant') {
|
|
61
|
+
const textParts = [];
|
|
62
|
+
const toolCalls = [];
|
|
63
|
+
for (const b of msg.content) {
|
|
64
|
+
if (b.type === 'text')
|
|
65
|
+
textParts.push(b.text);
|
|
66
|
+
else if (b.type === 'tool_use') {
|
|
67
|
+
const tu = b;
|
|
68
|
+
toolCalls.push({
|
|
69
|
+
id: tu.id,
|
|
70
|
+
type: 'function',
|
|
71
|
+
function: { name: tu.name, arguments: JSON.stringify(tu.input ?? {}) },
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
messages.push({
|
|
76
|
+
role: 'assistant',
|
|
77
|
+
content: textParts.join('\n'),
|
|
78
|
+
tool_calls: toolCalls.length ? toolCalls : undefined,
|
|
79
|
+
});
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
// user: tool_result blocks become separate `tool` msgs; remaining
|
|
83
|
+
// text/image content becomes a user msg (after the tool results).
|
|
84
|
+
const userBlocks = [];
|
|
85
|
+
for (const b of msg.content) {
|
|
86
|
+
if (b.type === 'tool_result') {
|
|
87
|
+
const tr = b;
|
|
88
|
+
messages.push({
|
|
89
|
+
role: 'tool',
|
|
90
|
+
tool_call_id: tr.tool_use_id,
|
|
91
|
+
content: resultBlockToText(tr.content),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
else if (b.type === 'text') {
|
|
95
|
+
userBlocks.push({ type: 'text', text: b.text });
|
|
96
|
+
}
|
|
97
|
+
else if (b.type === 'image') {
|
|
98
|
+
const img = anthropicImageToBlock(b);
|
|
99
|
+
if (img)
|
|
100
|
+
userBlocks.push(img);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (userBlocks.length) {
|
|
104
|
+
const onlyText = userBlocks.length === 1 && userBlocks[0].type === 'text';
|
|
105
|
+
messages.push({
|
|
106
|
+
role: 'user',
|
|
107
|
+
content: onlyText ? userBlocks[0].text : userBlocks,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
messages,
|
|
113
|
+
tools: anthropicToolsToDefs(body.tools),
|
|
114
|
+
model: body.model,
|
|
115
|
+
maxTokens: body.max_tokens,
|
|
116
|
+
temperature: body.temperature,
|
|
117
|
+
stream: !!body.stream,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Response: StreamResult -> Anthropic Message (non-streaming)
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
function mapStopReason(reason, hasTools) {
|
|
124
|
+
if (hasTools)
|
|
125
|
+
return 'tool_use';
|
|
126
|
+
switch (reason) {
|
|
127
|
+
case 'max_tokens':
|
|
128
|
+
return 'max_tokens';
|
|
129
|
+
case 'tool_use':
|
|
130
|
+
return 'tool_use';
|
|
131
|
+
default:
|
|
132
|
+
return 'end_turn';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function safeJson(s) {
|
|
136
|
+
try {
|
|
137
|
+
const v = JSON.parse(s || '{}');
|
|
138
|
+
return v && typeof v === 'object' ? v : {};
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return {};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** Build a non-streaming Anthropic Messages response object from a StreamResult. */
|
|
145
|
+
export function streamResultToAnthropicMessage(result, opts) {
|
|
146
|
+
const content = [];
|
|
147
|
+
if (result.text)
|
|
148
|
+
content.push({ type: 'text', text: result.text });
|
|
149
|
+
for (const tc of result.toolCalls) {
|
|
150
|
+
content.push({ type: 'tool_use', id: tc.id, name: tc.function.name, input: safeJson(tc.function.arguments) });
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
id: opts.id ?? 'msg_' + uuid().replace(/-/g, '').slice(0, 24),
|
|
154
|
+
type: 'message',
|
|
155
|
+
role: 'assistant',
|
|
156
|
+
model: opts.model,
|
|
157
|
+
content,
|
|
158
|
+
stop_reason: mapStopReason(result.stopReason ?? null, result.toolCalls.length > 0),
|
|
159
|
+
stop_sequence: null,
|
|
160
|
+
usage: {
|
|
161
|
+
input_tokens: result.usage?.input_tokens ?? 0,
|
|
162
|
+
output_tokens: result.usage?.output_tokens ?? 0,
|
|
163
|
+
cache_read_input_tokens: result.usage?.cache_read_input_tokens ?? 0,
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Response: run the LLM and emit the Anthropic SSE event sequence.
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
function sse(event, data) {
|
|
171
|
+
return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Run a turn through `llm` and yield the Anthropic Messages **SSE** event
|
|
175
|
+
* sequence as strings (message_start -> content_block_* -> message_delta ->
|
|
176
|
+
* message_stop). Text streams live; tool calls (native or dialect-recovered)
|
|
177
|
+
* are emitted as `tool_use` blocks with a single `input_json_delta`. Pipe the
|
|
178
|
+
* yielded strings straight to an HTTP response body.
|
|
179
|
+
*/
|
|
180
|
+
export async function* anthropicSSE(llm, req, opts = { model: '' }) {
|
|
181
|
+
const model = opts.model || req.model;
|
|
182
|
+
const msgId = opts.id ?? 'msg_' + uuid().replace(/-/g, '').slice(0, 24);
|
|
183
|
+
yield sse('message_start', {
|
|
184
|
+
type: 'message_start',
|
|
185
|
+
message: {
|
|
186
|
+
id: msgId,
|
|
187
|
+
type: 'message',
|
|
188
|
+
role: 'assistant',
|
|
189
|
+
model,
|
|
190
|
+
content: [],
|
|
191
|
+
stop_reason: null,
|
|
192
|
+
stop_sequence: null,
|
|
193
|
+
usage: { input_tokens: 0, output_tokens: 0 },
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
// Live text streaming with a push queue; suppress tokens once inline
|
|
197
|
+
// tool-call markup begins (the dialect parser recovers the call from the
|
|
198
|
+
// final result instead — we don't want raw <tool_call> markup as text).
|
|
199
|
+
const queue = [];
|
|
200
|
+
let resolveNext = null;
|
|
201
|
+
let streamedText = '';
|
|
202
|
+
let inToolMarkup = false;
|
|
203
|
+
let textOpen = false;
|
|
204
|
+
let nextIndex = 0;
|
|
205
|
+
let textIndex = 0;
|
|
206
|
+
const push = (s) => {
|
|
207
|
+
queue.push(s);
|
|
208
|
+
resolveNext?.();
|
|
209
|
+
resolveNext = null;
|
|
210
|
+
};
|
|
211
|
+
const sp = llm.streamChat(req.messages, {
|
|
212
|
+
model,
|
|
213
|
+
tools: req.tools.length ? req.tools : undefined,
|
|
214
|
+
signal: opts.signal,
|
|
215
|
+
onToken: (delta) => {
|
|
216
|
+
streamedText += delta;
|
|
217
|
+
if (!inToolMarkup && /<tool_call|<function\s*=/.test(streamedText))
|
|
218
|
+
inToolMarkup = true;
|
|
219
|
+
if (inToolMarkup)
|
|
220
|
+
return;
|
|
221
|
+
if (!textOpen) {
|
|
222
|
+
textOpen = true;
|
|
223
|
+
textIndex = nextIndex++;
|
|
224
|
+
push(sse('content_block_start', { type: 'content_block_start', index: textIndex, content_block: { type: 'text', text: '' } }));
|
|
225
|
+
}
|
|
226
|
+
push(sse('content_block_delta', { type: 'content_block_delta', index: textIndex, delta: { type: 'text_delta', text: delta } }));
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
let result;
|
|
230
|
+
let done = false;
|
|
231
|
+
sp.then((r) => {
|
|
232
|
+
result = r;
|
|
233
|
+
}, () => { }).finally(() => {
|
|
234
|
+
done = true;
|
|
235
|
+
resolveNext?.();
|
|
236
|
+
resolveNext = null;
|
|
237
|
+
});
|
|
238
|
+
// Drain text deltas as they arrive.
|
|
239
|
+
while (!done || queue.length) {
|
|
240
|
+
if (queue.length) {
|
|
241
|
+
yield queue.shift();
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
await new Promise((res) => (resolveNext = res));
|
|
245
|
+
}
|
|
246
|
+
result = await sp.catch(() => undefined);
|
|
247
|
+
if (textOpen) {
|
|
248
|
+
yield sse('content_block_stop', { type: 'content_block_stop', index: textIndex });
|
|
249
|
+
}
|
|
250
|
+
const toolCalls = result?.toolCalls ?? [];
|
|
251
|
+
for (const tc of toolCalls) {
|
|
252
|
+
const idx = nextIndex++;
|
|
253
|
+
yield sse('content_block_start', {
|
|
254
|
+
type: 'content_block_start',
|
|
255
|
+
index: idx,
|
|
256
|
+
content_block: { type: 'tool_use', id: tc.id, name: tc.function.name, input: {} },
|
|
257
|
+
});
|
|
258
|
+
yield sse('content_block_delta', {
|
|
259
|
+
type: 'content_block_delta',
|
|
260
|
+
index: idx,
|
|
261
|
+
delta: { type: 'input_json_delta', partial_json: tc.function.arguments || '{}' },
|
|
262
|
+
});
|
|
263
|
+
yield sse('content_block_stop', { type: 'content_block_stop', index: idx });
|
|
264
|
+
}
|
|
265
|
+
yield sse('message_delta', {
|
|
266
|
+
type: 'message_delta',
|
|
267
|
+
delta: { stop_reason: mapStopReason(result?.stopReason ?? null, toolCalls.length > 0), stop_sequence: null },
|
|
268
|
+
usage: { output_tokens: result?.usage?.output_tokens ?? 0 },
|
|
269
|
+
});
|
|
270
|
+
yield sse('message_stop', { type: 'message_stop' });
|
|
271
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ToolCall } from '../types/index.js';
|
|
2
|
+
export interface ParsedToolCalls {
|
|
3
|
+
/** Tool calls recovered from the text (empty if none matched). */
|
|
4
|
+
calls: ToolCall[];
|
|
5
|
+
/** The text with tool-call markup removed (safe to show the user). */
|
|
6
|
+
cleanedText: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ToolDialect {
|
|
9
|
+
/** Stable id, e.g. 'xml-function' | 'hermes' | 'json-fence'. */
|
|
10
|
+
name: string;
|
|
11
|
+
/** Cheap presence check — does this dialect's markup appear at all? */
|
|
12
|
+
test(text: string): boolean;
|
|
13
|
+
/** Extract calls + strip markup. `idBase` seeds generated call ids. */
|
|
14
|
+
parse(text: string, idBase?: number): ParsedToolCalls;
|
|
15
|
+
}
|
|
16
|
+
export declare const xmlFunctionDialect: ToolDialect;
|
|
17
|
+
export declare const hermesDialect: ToolDialect;
|
|
18
|
+
export declare const jsonFenceDialect: ToolDialect;
|
|
19
|
+
/** All built-in dialects, keyed by name. */
|
|
20
|
+
export declare const dialects: Record<string, ToolDialect>;
|
|
21
|
+
/** Default attempt order — xml-function first preserves original behavior. */
|
|
22
|
+
export declare const DEFAULT_DIALECTS: string[];
|
|
23
|
+
/**
|
|
24
|
+
* Try a list of dialects (by name) against text and return the first that
|
|
25
|
+
* yields tool calls. Falls back to `{ calls: [], cleanedText: text }`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function parseToolCalls(text: string, opts?: {
|
|
28
|
+
dialects?: string[];
|
|
29
|
+
idBase?: number;
|
|
30
|
+
}): ParsedToolCalls;
|
|
31
|
+
/** True if ANY of the given dialects (default: all) detects tool-call markup. */
|
|
32
|
+
export declare function hasToolCalls(text: string, order?: string[]): boolean;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/** Strip a single leading newline and trailing whitespace from a param value. */
|
|
2
|
+
function trimEdges(v) {
|
|
3
|
+
return v.replace(/^\r?\n/, '').replace(/\s+$/, '');
|
|
4
|
+
}
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// xml-function — <function=name><parameter=key>value</parameter></function>
|
|
7
|
+
// (closing tags optional; wrapper <tool_call> optional). This is the original
|
|
8
|
+
// anyclaude inline format and stays first in the default order for back-compat.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
const XML_FUNCTION_MARKER = /<function\s*=/;
|
|
11
|
+
export const xmlFunctionDialect = {
|
|
12
|
+
name: 'xml-function',
|
|
13
|
+
test: (text) => XML_FUNCTION_MARKER.test(text),
|
|
14
|
+
parse(text, idBase = 0) {
|
|
15
|
+
if (!text || !XML_FUNCTION_MARKER.test(text))
|
|
16
|
+
return { calls: [], cleanedText: text };
|
|
17
|
+
const calls = [];
|
|
18
|
+
const markerRe = /<function\s*=\s*([^>\s]+)\s*>/g;
|
|
19
|
+
const markers = [];
|
|
20
|
+
let m;
|
|
21
|
+
while ((m = markerRe.exec(text)) !== null) {
|
|
22
|
+
markers.push({ name: m[1], bodyStart: markerRe.lastIndex, markerStart: m.index });
|
|
23
|
+
}
|
|
24
|
+
for (let i = 0; i < markers.length; i++) {
|
|
25
|
+
const cur = markers[i];
|
|
26
|
+
const end = i + 1 < markers.length ? markers[i + 1].markerStart : text.length;
|
|
27
|
+
let body = text.slice(cur.bodyStart, end);
|
|
28
|
+
body = body.replace(/<\/function>[\s\S]*$/, '').replace(/<\/tool_call>[\s\S]*$/, '');
|
|
29
|
+
const args = {};
|
|
30
|
+
const parts = body.split(/<parameter\s*=/).slice(1);
|
|
31
|
+
for (const part of parts) {
|
|
32
|
+
const gt = part.indexOf('>');
|
|
33
|
+
if (gt < 0)
|
|
34
|
+
continue;
|
|
35
|
+
const key = part.slice(0, gt).trim();
|
|
36
|
+
let val = part.slice(gt + 1);
|
|
37
|
+
val = val
|
|
38
|
+
.replace(/<\/parameter>[\s\S]*$/, '')
|
|
39
|
+
.replace(/<\/function>[\s\S]*$/, '')
|
|
40
|
+
.replace(/<\/tool_call>[\s\S]*$/, '');
|
|
41
|
+
args[key] = trimEdges(val);
|
|
42
|
+
}
|
|
43
|
+
calls.push({
|
|
44
|
+
id: `call_inline_${idBase + i}`,
|
|
45
|
+
type: 'function',
|
|
46
|
+
function: { name: cur.name.trim(), arguments: JSON.stringify(args) },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const cut = text.search(/<tool_call>|<function\s*=/);
|
|
50
|
+
const cleanedText = cut >= 0 ? text.slice(0, cut).trim() : text;
|
|
51
|
+
return { calls, cleanedText };
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// hermes — <tool_call>{"name": "...", "arguments": {...}}</tool_call>
|
|
56
|
+
// Accepts "arguments" | "parameters" | "args"; tolerates a missing closing tag.
|
|
57
|
+
// Used by Qwen, Hermes/NousResearch, and many Ollama-served models.
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
const HERMES_OPEN = /<tool_call>/i;
|
|
60
|
+
export const hermesDialect = {
|
|
61
|
+
name: 'hermes',
|
|
62
|
+
test: (text) => HERMES_OPEN.test(text) && text.includes('{'),
|
|
63
|
+
parse(text, idBase = 0) {
|
|
64
|
+
if (!HERMES_OPEN.test(text))
|
|
65
|
+
return { calls: [], cleanedText: text };
|
|
66
|
+
const calls = [];
|
|
67
|
+
const blockRe = /<tool_call>\s*([\s\S]*?)(?:<\/tool_call>|$)/gi;
|
|
68
|
+
let m;
|
|
69
|
+
let i = 0;
|
|
70
|
+
while ((m = blockRe.exec(text)) !== null) {
|
|
71
|
+
const obj = extractFirstJsonObject(m[1]);
|
|
72
|
+
if (!obj)
|
|
73
|
+
continue;
|
|
74
|
+
const call = jsonToToolCall(obj, idBase + i);
|
|
75
|
+
if (call) {
|
|
76
|
+
calls.push(call);
|
|
77
|
+
i++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const cut = text.search(HERMES_OPEN);
|
|
81
|
+
const cleanedText = cut >= 0 ? text.slice(0, cut).trim() : text;
|
|
82
|
+
return { calls, cleanedText };
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// json-fence — a fenced code block whose JSON looks like a tool call:
|
|
87
|
+
// ```json | ```tool_call | ```tool
|
|
88
|
+
// {"name": "...", "arguments": {...}} (also "tool"/"args"/"parameters")
|
|
89
|
+
// ```
|
|
90
|
+
// Conservative: only treats a block as a call when it has BOTH a name key
|
|
91
|
+
// (name|tool|function) AND an args key (arguments|args|parameters|input), so
|
|
92
|
+
// ordinary JSON the model prints for the user is not misread as a tool call.
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
const FENCE_RE = /```(?:json|tool_call|tool)?\s*\n?([\s\S]*?)```/gi;
|
|
95
|
+
export const jsonFenceDialect = {
|
|
96
|
+
name: 'json-fence',
|
|
97
|
+
test: (text) => /```/.test(text) && /"(name|tool|function)"\s*:/.test(text),
|
|
98
|
+
parse(text, idBase = 0) {
|
|
99
|
+
const calls = [];
|
|
100
|
+
let firstMatchIndex = -1;
|
|
101
|
+
let m;
|
|
102
|
+
let i = 0;
|
|
103
|
+
while ((m = FENCE_RE.exec(text)) !== null) {
|
|
104
|
+
const obj = extractFirstJsonObject(m[1]);
|
|
105
|
+
if (!obj)
|
|
106
|
+
continue;
|
|
107
|
+
const call = jsonToToolCall(obj, idBase + i);
|
|
108
|
+
if (call) {
|
|
109
|
+
if (firstMatchIndex < 0)
|
|
110
|
+
firstMatchIndex = m.index;
|
|
111
|
+
calls.push(call);
|
|
112
|
+
i++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
FENCE_RE.lastIndex = 0;
|
|
116
|
+
const cleanedText = firstMatchIndex >= 0 ? text.slice(0, firstMatchIndex).trim() : text;
|
|
117
|
+
return { calls, cleanedText };
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
/** All built-in dialects, keyed by name. */
|
|
121
|
+
export const dialects = {
|
|
122
|
+
'xml-function': xmlFunctionDialect,
|
|
123
|
+
hermes: hermesDialect,
|
|
124
|
+
'json-fence': jsonFenceDialect,
|
|
125
|
+
};
|
|
126
|
+
/** Default attempt order — xml-function first preserves original behavior. */
|
|
127
|
+
export const DEFAULT_DIALECTS = ['xml-function', 'hermes', 'json-fence'];
|
|
128
|
+
/**
|
|
129
|
+
* Try a list of dialects (by name) against text and return the first that
|
|
130
|
+
* yields tool calls. Falls back to `{ calls: [], cleanedText: text }`.
|
|
131
|
+
*/
|
|
132
|
+
export function parseToolCalls(text, opts = {}) {
|
|
133
|
+
if (!text)
|
|
134
|
+
return { calls: [], cleanedText: text };
|
|
135
|
+
const order = opts.dialects ?? DEFAULT_DIALECTS;
|
|
136
|
+
for (const name of order) {
|
|
137
|
+
const d = dialects[name];
|
|
138
|
+
if (!d || !d.test(text))
|
|
139
|
+
continue;
|
|
140
|
+
const parsed = d.parse(text, opts.idBase ?? 0);
|
|
141
|
+
if (parsed.calls.length)
|
|
142
|
+
return parsed;
|
|
143
|
+
}
|
|
144
|
+
return { calls: [], cleanedText: text };
|
|
145
|
+
}
|
|
146
|
+
/** True if ANY of the given dialects (default: all) detects tool-call markup. */
|
|
147
|
+
export function hasToolCalls(text, order = DEFAULT_DIALECTS) {
|
|
148
|
+
return order.some((n) => dialects[n]?.test(text));
|
|
149
|
+
}
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// helpers
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
/** Find and parse the first balanced top-level `{...}` JSON object in a string. */
|
|
154
|
+
function extractFirstJsonObject(s) {
|
|
155
|
+
const start = s.indexOf('{');
|
|
156
|
+
if (start < 0)
|
|
157
|
+
return null;
|
|
158
|
+
let depth = 0;
|
|
159
|
+
let inStr = false;
|
|
160
|
+
let esc = false;
|
|
161
|
+
for (let i = start; i < s.length; i++) {
|
|
162
|
+
const ch = s[i];
|
|
163
|
+
if (inStr) {
|
|
164
|
+
if (esc)
|
|
165
|
+
esc = false;
|
|
166
|
+
else if (ch === '\\')
|
|
167
|
+
esc = true;
|
|
168
|
+
else if (ch === '"')
|
|
169
|
+
inStr = false;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (ch === '"')
|
|
173
|
+
inStr = true;
|
|
174
|
+
else if (ch === '{')
|
|
175
|
+
depth++;
|
|
176
|
+
else if (ch === '}') {
|
|
177
|
+
depth--;
|
|
178
|
+
if (depth === 0) {
|
|
179
|
+
const slice = s.slice(start, i + 1);
|
|
180
|
+
try {
|
|
181
|
+
const v = JSON.parse(slice);
|
|
182
|
+
return v && typeof v === 'object' ? v : null;
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
/** Coerce a `{name|tool|function, arguments|args|parameters|input}` object into a ToolCall. */
|
|
193
|
+
function jsonToToolCall(obj, idx) {
|
|
194
|
+
// Some emitters wrap as { "tool_call": {...} } or { "function": {name, arguments} }.
|
|
195
|
+
if (obj.tool_call && typeof obj.tool_call === 'object') {
|
|
196
|
+
return jsonToToolCall(obj.tool_call, idx);
|
|
197
|
+
}
|
|
198
|
+
let name = obj.name ?? obj.tool ?? obj.function;
|
|
199
|
+
let rawArgs = obj.arguments ?? obj.args ?? obj.parameters ?? obj.input;
|
|
200
|
+
// Nested OpenAI shape: { function: { name, arguments } }
|
|
201
|
+
if (name && typeof name === 'object') {
|
|
202
|
+
const fn = name;
|
|
203
|
+
rawArgs = rawArgs ?? fn.arguments ?? fn.args;
|
|
204
|
+
name = fn.name;
|
|
205
|
+
}
|
|
206
|
+
if (typeof name !== 'string' || !name)
|
|
207
|
+
return null;
|
|
208
|
+
const argsStr = typeof rawArgs === 'string'
|
|
209
|
+
? rawArgs
|
|
210
|
+
: rawArgs === undefined
|
|
211
|
+
? '{}'
|
|
212
|
+
: JSON.stringify(rawArgs);
|
|
213
|
+
return {
|
|
214
|
+
id: `call_inline_${idx}`,
|
|
215
|
+
type: 'function',
|
|
216
|
+
function: { name: name.trim(), arguments: argsStr || '{}' },
|
|
217
|
+
};
|
|
218
|
+
}
|
package/dist/llm/index.d.ts
CHANGED
|
@@ -2,4 +2,7 @@ export * from './openai.js';
|
|
|
2
2
|
export * from './anthropic.js';
|
|
3
3
|
export * from './responses.js';
|
|
4
4
|
export { hasInlineToolCalls, parseInlineToolCalls } from './inlineTools.js';
|
|
5
|
+
export { parseToolCalls, hasToolCalls, dialects, DEFAULT_DIALECTS, xmlFunctionDialect, hermesDialect, jsonFenceDialect, type ToolDialect, type ParsedToolCalls, } from './dialects.js';
|
|
6
|
+
export { profileForModel, toolGuidancePrompt, builtinProfiles, genericProfile, type ModelProfile, } from './profiles.js';
|
|
7
|
+
export { validateToolArguments, schemaHint, type ArgValidation } from './repair.js';
|
|
5
8
|
export type { LLMClient, ChatMsg, StreamResult, ToolCall, ToolDef, StopReason, Usage, ContentBlockParam, } from '../types/index.js';
|
package/dist/llm/index.js
CHANGED
|
@@ -4,3 +4,12 @@ export * from './responses.js';
|
|
|
4
4
|
// Inline tool-call parsing — recover tool calls a model emitted as TEXT
|
|
5
5
|
// (e.g. weak models that narrate tool calls instead of using native function calls).
|
|
6
6
|
export { hasInlineToolCalls, parseInlineToolCalls } from './inlineTools.js';
|
|
7
|
+
// Tool-call dialects — pluggable parsers for the inline formats cheap/open
|
|
8
|
+
// models use (xml-function, hermes, json-fence) when they skip native tool_calls.
|
|
9
|
+
export { parseToolCalls, hasToolCalls, dialects, DEFAULT_DIALECTS, xmlFunctionDialect, hermesDialect, jsonFenceDialect, } from './dialects.js';
|
|
10
|
+
// Model profiles — per-model quirks (dialects, tool_choice, parallel, temperature,
|
|
11
|
+
// guidance) for reliable tool use across the long tail of OpenAI-compatible endpoints.
|
|
12
|
+
export { profileForModel, toolGuidancePrompt, builtinProfiles, genericProfile, } from './profiles.js';
|
|
13
|
+
// Tool-call repair — validate args before executing and feed the model a
|
|
14
|
+
// corrective tool_result so it self-heals (the big reliability win for weak models).
|
|
15
|
+
export { validateToolArguments, schemaHint } from './repair.js';
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { ToolCall } from '../types/index.js';
|
|
2
2
|
export declare function hasInlineToolCalls(text: string): boolean;
|
|
3
3
|
/**
|
|
4
|
-
* Extract inline tool calls from assistant text
|
|
5
|
-
* the text with the tool-call markup removed.
|
|
6
|
-
* original text and an empty array.
|
|
4
|
+
* Extract inline tool calls from assistant text across all built-in dialects.
|
|
5
|
+
* Returns the parsed calls and the text with the tool-call markup removed.
|
|
7
6
|
*/
|
|
8
7
|
export declare function parseInlineToolCalls(text: string): {
|
|
9
8
|
calls: ToolCall[];
|
package/dist/llm/inlineTools.js
CHANGED
|
@@ -1,72 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
// instead of native function-calling blocks. Several relays and open models use
|
|
3
|
-
// an "XML" tool-call format like:
|
|
4
|
-
//
|
|
5
|
-
// <tool_call>
|
|
6
|
-
// <function=write_file>
|
|
7
|
-
// <parameter=path>index.html</parameter>
|
|
8
|
-
// <parameter=content>
|
|
9
|
-
// <!DOCTYPE html> ...
|
|
10
|
-
// </parameter>
|
|
11
|
-
// </function>
|
|
12
|
-
// </tool_call>
|
|
13
|
-
//
|
|
14
|
-
// Parameters may or may not have closing </parameter> tags, and the wrapper
|
|
15
|
-
// <tool_call> may be absent. This parser is tolerant of all those variants and
|
|
16
|
-
// also strips the markup out of the user-visible text.
|
|
17
|
-
const FUNCTION_MARKER = /<function\s*=/;
|
|
1
|
+
import { hasToolCalls, parseToolCalls } from './dialects.js';
|
|
18
2
|
export function hasInlineToolCalls(text) {
|
|
19
|
-
return
|
|
3
|
+
return hasToolCalls(text);
|
|
20
4
|
}
|
|
21
5
|
/**
|
|
22
|
-
* Extract inline tool calls from assistant text
|
|
23
|
-
* the text with the tool-call markup removed.
|
|
24
|
-
* original text and an empty array.
|
|
6
|
+
* Extract inline tool calls from assistant text across all built-in dialects.
|
|
7
|
+
* Returns the parsed calls and the text with the tool-call markup removed.
|
|
25
8
|
*/
|
|
26
9
|
export function parseInlineToolCalls(text) {
|
|
27
|
-
|
|
28
|
-
return { calls: [], cleanedText: text };
|
|
29
|
-
const calls = [];
|
|
30
|
-
const markerRe = /<function\s*=\s*([^>\s]+)\s*>/g;
|
|
31
|
-
const markers = [];
|
|
32
|
-
let m;
|
|
33
|
-
while ((m = markerRe.exec(text)) !== null) {
|
|
34
|
-
markers.push({ name: m[1], bodyStart: markerRe.lastIndex, markerStart: m.index });
|
|
35
|
-
}
|
|
36
|
-
for (let i = 0; i < markers.length; i++) {
|
|
37
|
-
const cur = markers[i];
|
|
38
|
-
const end = i + 1 < markers.length ? markers[i + 1].markerStart : text.length;
|
|
39
|
-
let body = text.slice(cur.bodyStart, end);
|
|
40
|
-
// Trim at the function/tool_call closers if present.
|
|
41
|
-
body = body.replace(/<\/function>[\s\S]*$/, '').replace(/<\/tool_call>[\s\S]*$/, '');
|
|
42
|
-
const args = {};
|
|
43
|
-
const parts = body.split(/<parameter\s*=/).slice(1);
|
|
44
|
-
for (const part of parts) {
|
|
45
|
-
const gt = part.indexOf('>');
|
|
46
|
-
if (gt < 0)
|
|
47
|
-
continue;
|
|
48
|
-
const key = part.slice(0, gt).trim();
|
|
49
|
-
let val = part.slice(gt + 1);
|
|
50
|
-
// Value ends at its own closer (or the function/tool_call closer, or the
|
|
51
|
-
// next parameter — already removed by the split).
|
|
52
|
-
val = val
|
|
53
|
-
.replace(/<\/parameter>[\s\S]*$/, '')
|
|
54
|
-
.replace(/<\/function>[\s\S]*$/, '')
|
|
55
|
-
.replace(/<\/tool_call>[\s\S]*$/, '');
|
|
56
|
-
args[key] = trimEdges(val);
|
|
57
|
-
}
|
|
58
|
-
calls.push({
|
|
59
|
-
id: `call_inline_${i}`,
|
|
60
|
-
type: 'function',
|
|
61
|
-
function: { name: cur.name.trim(), arguments: JSON.stringify(args) },
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
// Everything from the first tool-call/function marker onward is markup.
|
|
65
|
-
const cut = text.search(/<tool_call>|<function\s*=/);
|
|
66
|
-
const cleanedText = cut >= 0 ? text.slice(0, cut).trim() : text;
|
|
67
|
-
return { calls, cleanedText };
|
|
68
|
-
}
|
|
69
|
-
/** Strip a single leading newline and trailing whitespace from a param value. */
|
|
70
|
-
function trimEdges(v) {
|
|
71
|
-
return v.replace(/^\r?\n/, '').replace(/\s+$/, '');
|
|
10
|
+
return parseToolCalls(text);
|
|
72
11
|
}
|
package/dist/llm/openai.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ChatMsg, ContentBlockParam, LLMClient } from '../types/index.js';
|
|
2
|
+
import { type ModelProfile } from './profiles.js';
|
|
2
3
|
export interface OpenAIClientOptions {
|
|
3
4
|
/** API key, or a function returning one per request (for round-robin key pools). */
|
|
4
5
|
apiKey?: string | (() => string | undefined);
|
|
@@ -16,6 +17,20 @@ export interface OpenAIClientOptions {
|
|
|
16
17
|
reasoningEffort?: string;
|
|
17
18
|
/** Allow the model to batch multiple tool calls → sets `parallel_tool_calls` (when tools present). */
|
|
18
19
|
parallelToolCalls?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Per-model quirks for reliable tool use on cheap/open endpoints. Pass a
|
|
22
|
+
* `ModelProfile`, a built-in name ('qwen'|'deepseek'|'moonshot'|'zhipu'|
|
|
23
|
+
* 'mistral'|'llama'|'openai'|'anthropic'|'generic'), or omit to AUTO-DETECT
|
|
24
|
+
* from the model id. The profile supplies inline tool-call dialects + sane
|
|
25
|
+
* tool_choice / parallel / temperature defaults; explicit options above always win.
|
|
26
|
+
*/
|
|
27
|
+
profile?: string | ModelProfile;
|
|
28
|
+
/**
|
|
29
|
+
* Inline tool-call dialects to attempt when the model emits tool calls as TEXT
|
|
30
|
+
* instead of native function-calls (e.g. ['hermes','json-fence','xml-function']).
|
|
31
|
+
* Overrides the profile's dialects. Set `[]` to disable inline recovery.
|
|
32
|
+
*/
|
|
33
|
+
toolDialects?: string[];
|
|
19
34
|
}
|
|
20
35
|
/**
|
|
21
36
|
* Creates an LLMClient backed by any OpenAI-compatible /chat/completions
|
package/dist/llm/openai.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseToolCalls } from './dialects.js';
|
|
2
|
+
import { profileForModel } from './profiles.js';
|
|
2
3
|
/**
|
|
3
4
|
* Creates an LLMClient backed by any OpenAI-compatible /chat/completions
|
|
4
5
|
* endpoint (OpenAI, Groq, Together, OpenRouter, local llama.cpp, etc.).
|
|
@@ -12,14 +13,20 @@ export function createOpenAIClient(options = {}) {
|
|
|
12
13
|
return {
|
|
13
14
|
async streamChat(messages, opts) {
|
|
14
15
|
const model = opts.model || defaultModel;
|
|
16
|
+
// Resolve a model profile (explicit > auto-detect from model id). It only
|
|
17
|
+
// fills gaps — any option set explicitly on the client always wins.
|
|
18
|
+
const profile = profileForModel(options.profile ?? model);
|
|
19
|
+
const dialects = options.toolDialects ?? profile.dialects;
|
|
20
|
+
const temperature = options.temperature ?? profile.temperature;
|
|
21
|
+
const parallel = options.parallelToolCalls ?? profile.parallelToolCalls;
|
|
15
22
|
const body = {
|
|
16
23
|
model,
|
|
17
24
|
messages: messages.map(toOpenAIMessage),
|
|
18
25
|
stream: true,
|
|
19
26
|
stream_options: { include_usage: true },
|
|
20
27
|
};
|
|
21
|
-
if (
|
|
22
|
-
body.temperature =
|
|
28
|
+
if (temperature !== undefined)
|
|
29
|
+
body.temperature = temperature;
|
|
23
30
|
if (options.maxTokens !== undefined)
|
|
24
31
|
body.max_tokens = options.maxTokens;
|
|
25
32
|
// Reasoning models (e.g. xAI grok-4.x): 'none' → 0 reasoning tokens (cheaper/faster).
|
|
@@ -27,9 +34,9 @@ export function createOpenAIClient(options = {}) {
|
|
|
27
34
|
body.reasoning_effort = options.reasoningEffort;
|
|
28
35
|
if (opts.tools?.length) {
|
|
29
36
|
body.tools = opts.tools;
|
|
30
|
-
body.tool_choice = 'auto';
|
|
31
|
-
if (
|
|
32
|
-
body.parallel_tool_calls =
|
|
37
|
+
body.tool_choice = profile.toolChoice ?? 'auto';
|
|
38
|
+
if (parallel !== undefined)
|
|
39
|
+
body.parallel_tool_calls = parallel;
|
|
33
40
|
}
|
|
34
41
|
const apiKey = typeof options.apiKey === 'function' ? options.apiKey() : options.apiKey;
|
|
35
42
|
const headers = {
|
|
@@ -102,10 +109,11 @@ export function createOpenAIClient(options = {}) {
|
|
|
102
109
|
function: { name: t.name, arguments: t.args || '{}' },
|
|
103
110
|
}));
|
|
104
111
|
// Fallback: some endpoints emit tool calls as inline text rather than
|
|
105
|
-
// native tool_calls. Parse them
|
|
112
|
+
// native tool_calls. Parse them with the profile's dialects and clean the
|
|
113
|
+
// visible text. (Empty `dialects` — e.g. for native GPT/Claude — skips this.)
|
|
106
114
|
let finalText = text;
|
|
107
|
-
if (!toolCalls.length) {
|
|
108
|
-
const inline =
|
|
115
|
+
if (!toolCalls.length && (!dialects || dialects.length)) {
|
|
116
|
+
const inline = parseToolCalls(text, { dialects });
|
|
109
117
|
if (inline.calls.length) {
|
|
110
118
|
toolCalls.push(...inline.calls);
|
|
111
119
|
finalText = inline.cleanedText;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ToolDef } from '../types/index.js';
|
|
2
|
+
export interface ModelProfile {
|
|
3
|
+
/** Stable id, e.g. 'qwen' | 'deepseek' | 'openai' | 'generic'. */
|
|
4
|
+
name: string;
|
|
5
|
+
/** Match by model id (already lowercased before this is called). */
|
|
6
|
+
match: (model: string) => boolean;
|
|
7
|
+
/** Inline dialects to attempt (in order) when native tool_calls are absent. */
|
|
8
|
+
dialects?: string[];
|
|
9
|
+
/** tool_choice to send when tools are present. */
|
|
10
|
+
toolChoice?: 'auto' | 'required' | 'none';
|
|
11
|
+
/** parallel_tool_calls. Some models break or loop on parallel calls. */
|
|
12
|
+
parallelToolCalls?: boolean;
|
|
13
|
+
/** Suggested temperature for stable tool use (lower = more deterministic). */
|
|
14
|
+
temperature?: number;
|
|
15
|
+
/** Whether a short tool-use scaffolding prompt helps this family. */
|
|
16
|
+
injectToolGuidance?: boolean;
|
|
17
|
+
/** Human note surfaced in the compatibility matrix / docs. */
|
|
18
|
+
note?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare const builtinProfiles: ModelProfile[];
|
|
21
|
+
/** Catch-all for unknown models: try everything, guide, keep parallel off. */
|
|
22
|
+
export declare const genericProfile: ModelProfile;
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a profile for a model id. Pass a `ModelProfile` to use it verbatim, a
|
|
25
|
+
* string name to look up a built-in, or a model id to auto-detect. Unknown →
|
|
26
|
+
* `genericProfile`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function profileForModel(model?: string | ModelProfile): ModelProfile;
|
|
29
|
+
/**
|
|
30
|
+
* A short, model-agnostic tool-use scaffolding prompt for weak models that
|
|
31
|
+
* narrate tool calls instead of using native function-calling. Append it to the
|
|
32
|
+
* system prompt (e.g. via `query({ appendSystemPrompt })`) when a profile sets
|
|
33
|
+
* `injectToolGuidance`. Lists the available tools so the model knows the names.
|
|
34
|
+
*/
|
|
35
|
+
export declare function toolGuidancePrompt(tools: ToolDef[]): string;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { DEFAULT_DIALECTS } from './dialects.js';
|
|
2
|
+
const has = (...needles) => (model) => needles.some((n) => model.includes(n));
|
|
3
|
+
// Ordered most-specific → most-general; first match wins.
|
|
4
|
+
export const builtinProfiles = [
|
|
5
|
+
{
|
|
6
|
+
name: 'openai',
|
|
7
|
+
match: has('gpt-', 'gpt4', 'o1', 'o3', 'o4', 'chatgpt'),
|
|
8
|
+
dialects: [], // native tool_calls are reliable
|
|
9
|
+
toolChoice: 'auto',
|
|
10
|
+
parallelToolCalls: true,
|
|
11
|
+
note: 'Native tool_calls; no inline fallback needed.',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: 'anthropic',
|
|
15
|
+
match: has('claude'),
|
|
16
|
+
dialects: [],
|
|
17
|
+
toolChoice: 'auto',
|
|
18
|
+
note: 'Native tool use; clean function-calling.',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'qwen',
|
|
22
|
+
match: has('qwen', 'qwq'),
|
|
23
|
+
dialects: ['hermes', 'xml-function', 'json-fence'],
|
|
24
|
+
toolChoice: 'auto',
|
|
25
|
+
parallelToolCalls: false,
|
|
26
|
+
temperature: 0.3,
|
|
27
|
+
injectToolGuidance: true,
|
|
28
|
+
note: 'Hermes-style <tool_call>{json}</tool_call>; parallel calls unreliable.',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'deepseek',
|
|
32
|
+
match: has('deepseek'),
|
|
33
|
+
dialects: ['json-fence', 'hermes', 'xml-function'],
|
|
34
|
+
toolChoice: 'auto',
|
|
35
|
+
parallelToolCalls: false,
|
|
36
|
+
temperature: 0.3,
|
|
37
|
+
injectToolGuidance: true,
|
|
38
|
+
note: 'Often emits tool calls in JSON code fences; keep parallel off.',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'moonshot',
|
|
42
|
+
match: has('kimi', 'moonshot'),
|
|
43
|
+
dialects: ['hermes', 'json-fence'],
|
|
44
|
+
toolChoice: 'auto',
|
|
45
|
+
parallelToolCalls: false,
|
|
46
|
+
note: 'Kimi/Moonshot — Hermes-style; Anthropic-compatible endpoint also offered.',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'zhipu',
|
|
50
|
+
match: has('glm', 'zhipu', 'chatglm'),
|
|
51
|
+
dialects: ['xml-function', 'hermes', 'json-fence'],
|
|
52
|
+
toolChoice: 'auto',
|
|
53
|
+
parallelToolCalls: false,
|
|
54
|
+
injectToolGuidance: true,
|
|
55
|
+
note: 'GLM/Zhipu — mixed dialects; sponsors claude-code-router as a cheap backend.',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'mistral',
|
|
59
|
+
match: has('mistral', 'mixtral', 'codestral', 'devstral', 'magistral'),
|
|
60
|
+
dialects: ['json-fence', 'hermes', 'xml-function'],
|
|
61
|
+
toolChoice: 'auto',
|
|
62
|
+
parallelToolCalls: false,
|
|
63
|
+
temperature: 0.2,
|
|
64
|
+
injectToolGuidance: true,
|
|
65
|
+
note: 'Tool-calling historically fragile; low temperature + repair recommended.',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'llama',
|
|
69
|
+
match: has('llama', 'codellama'),
|
|
70
|
+
dialects: ['json-fence', 'hermes', 'xml-function'],
|
|
71
|
+
toolChoice: 'auto',
|
|
72
|
+
parallelToolCalls: false,
|
|
73
|
+
temperature: 0.3,
|
|
74
|
+
injectToolGuidance: true,
|
|
75
|
+
note: 'Llama family (often via Ollama) — inline fallback + guidance help a lot.',
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
/** Catch-all for unknown models: try everything, guide, keep parallel off. */
|
|
79
|
+
export const genericProfile = {
|
|
80
|
+
name: 'generic',
|
|
81
|
+
match: () => true,
|
|
82
|
+
dialects: DEFAULT_DIALECTS,
|
|
83
|
+
toolChoice: 'auto',
|
|
84
|
+
parallelToolCalls: false,
|
|
85
|
+
injectToolGuidance: true,
|
|
86
|
+
note: 'Unknown model — full inline fallback + guidance, parallel off, repair on.',
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Resolve a profile for a model id. Pass a `ModelProfile` to use it verbatim, a
|
|
90
|
+
* string name to look up a built-in, or a model id to auto-detect. Unknown →
|
|
91
|
+
* `genericProfile`.
|
|
92
|
+
*/
|
|
93
|
+
export function profileForModel(model) {
|
|
94
|
+
if (model && typeof model === 'object')
|
|
95
|
+
return model;
|
|
96
|
+
const id = (model ?? '').toLowerCase();
|
|
97
|
+
if (id) {
|
|
98
|
+
const byName = builtinProfiles.find((p) => p.name === id);
|
|
99
|
+
if (byName)
|
|
100
|
+
return byName;
|
|
101
|
+
const byMatch = builtinProfiles.find((p) => p.match(id));
|
|
102
|
+
if (byMatch)
|
|
103
|
+
return byMatch;
|
|
104
|
+
}
|
|
105
|
+
return genericProfile;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* A short, model-agnostic tool-use scaffolding prompt for weak models that
|
|
109
|
+
* narrate tool calls instead of using native function-calling. Append it to the
|
|
110
|
+
* system prompt (e.g. via `query({ appendSystemPrompt })`) when a profile sets
|
|
111
|
+
* `injectToolGuidance`. Lists the available tools so the model knows the names.
|
|
112
|
+
*/
|
|
113
|
+
export function toolGuidancePrompt(tools) {
|
|
114
|
+
const names = tools.map((t) => `- ${t.function.name}: ${t.function.description}`).join('\n');
|
|
115
|
+
return [
|
|
116
|
+
'When you need to use a tool, prefer the native function-calling format.',
|
|
117
|
+
'If you cannot, emit EXACTLY one tool call per turn as a single JSON object',
|
|
118
|
+
'wrapped in <tool_call>…</tool_call> tags, with this shape:',
|
|
119
|
+
'<tool_call>{"name": "<tool_name>", "arguments": { /* params */ }}</tool_call>',
|
|
120
|
+
'Do not wrap it in prose. Use only these tools:',
|
|
121
|
+
names,
|
|
122
|
+
].join('\n');
|
|
123
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ToolDef } from '../types/index.js';
|
|
2
|
+
export interface ArgValidation {
|
|
3
|
+
/** True when the arguments parsed and satisfied required props / basic types. */
|
|
4
|
+
ok: boolean;
|
|
5
|
+
/** Parsed arguments (best-effort: `{}` when unparseable). */
|
|
6
|
+
input: Record<string, unknown>;
|
|
7
|
+
/** When `ok` is false, a concise, model-facing explanation + schema hint. */
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Validate raw tool-call argument JSON against a tool definition.
|
|
12
|
+
*
|
|
13
|
+
* - Unparseable JSON → `ok:false` with the parse error and the expected schema.
|
|
14
|
+
* - Missing required properties → `ok:false` listing them.
|
|
15
|
+
* - Wrong primitive type on a provided property → `ok:false`.
|
|
16
|
+
* - No def (unknown tool / client tool) → parse-only; never blocks.
|
|
17
|
+
*/
|
|
18
|
+
export declare function validateToolArguments(def: ToolDef | undefined, rawArgs: string): ArgValidation;
|
|
19
|
+
/** A compact one-line schema hint: `{ path: string (required), recursive?: boolean }`. */
|
|
20
|
+
export declare function schemaHint(def: ToolDef): string;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate raw tool-call argument JSON against a tool definition.
|
|
3
|
+
*
|
|
4
|
+
* - Unparseable JSON → `ok:false` with the parse error and the expected schema.
|
|
5
|
+
* - Missing required properties → `ok:false` listing them.
|
|
6
|
+
* - Wrong primitive type on a provided property → `ok:false`.
|
|
7
|
+
* - No def (unknown tool / client tool) → parse-only; never blocks.
|
|
8
|
+
*/
|
|
9
|
+
export function validateToolArguments(def, rawArgs) {
|
|
10
|
+
const raw = rawArgs?.trim() ? rawArgs : '{}';
|
|
11
|
+
let parsed;
|
|
12
|
+
try {
|
|
13
|
+
parsed = JSON.parse(raw);
|
|
14
|
+
}
|
|
15
|
+
catch (e) {
|
|
16
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
17
|
+
return {
|
|
18
|
+
ok: false,
|
|
19
|
+
input: {},
|
|
20
|
+
error: def
|
|
21
|
+
? `Arguments for "${def.function.name}" were not valid JSON (${msg}). Call the tool again with a single valid JSON object matching: ${schemaHint(def)}`
|
|
22
|
+
: `Tool arguments were not valid JSON (${msg}). Send a single valid JSON object.`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
26
|
+
return {
|
|
27
|
+
ok: false,
|
|
28
|
+
input: {},
|
|
29
|
+
error: def
|
|
30
|
+
? `Arguments for "${def.function.name}" must be a JSON object. Expected: ${schemaHint(def)}`
|
|
31
|
+
: 'Tool arguments must be a JSON object.',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const input = parsed;
|
|
35
|
+
// No schema to check against (unknown / client-delegated tool) — accept.
|
|
36
|
+
if (!def)
|
|
37
|
+
return { ok: true, input };
|
|
38
|
+
const props = (def.function.parameters?.properties ?? {});
|
|
39
|
+
const required = def.function.parameters?.required ?? [];
|
|
40
|
+
const missing = required.filter((k) => input[k] === undefined || input[k] === null);
|
|
41
|
+
if (missing.length) {
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
input,
|
|
45
|
+
error: `Missing required argument${missing.length > 1 ? 's' : ''} for "${def.function.name}": ${missing
|
|
46
|
+
.map((k) => `"${k}"`)
|
|
47
|
+
.join(', ')}. Call it again including ${missing.length > 1 ? 'them' : 'it'}. Expected: ${schemaHint(def)}`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// Light primitive type check on provided props.
|
|
51
|
+
for (const [key, val] of Object.entries(input)) {
|
|
52
|
+
const want = props[key]?.type;
|
|
53
|
+
if (!want || val === null || val === undefined)
|
|
54
|
+
continue;
|
|
55
|
+
if (!matchesType(val, want)) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
input,
|
|
59
|
+
error: `Argument "${key}" for "${def.function.name}" should be ${want}, got ${jsType(val)}. Call it again with the correct type. Expected: ${schemaHint(def)}`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { ok: true, input };
|
|
64
|
+
}
|
|
65
|
+
/** A compact one-line schema hint: `{ path: string (required), recursive?: boolean }`. */
|
|
66
|
+
export function schemaHint(def) {
|
|
67
|
+
const props = (def.function.parameters?.properties ?? {});
|
|
68
|
+
const required = new Set(def.function.parameters?.required ?? []);
|
|
69
|
+
const parts = Object.entries(props).map(([k, v]) => {
|
|
70
|
+
const req = required.has(k);
|
|
71
|
+
return `${k}${req ? '' : '?'}: ${v?.type ?? 'any'}${req ? ' (required)' : ''}`;
|
|
72
|
+
});
|
|
73
|
+
return `{ ${parts.join(', ')} }`;
|
|
74
|
+
}
|
|
75
|
+
function jsType(v) {
|
|
76
|
+
if (Array.isArray(v))
|
|
77
|
+
return 'array';
|
|
78
|
+
return typeof v;
|
|
79
|
+
}
|
|
80
|
+
function matchesType(v, want) {
|
|
81
|
+
switch (want) {
|
|
82
|
+
case 'string':
|
|
83
|
+
return typeof v === 'string';
|
|
84
|
+
case 'number':
|
|
85
|
+
case 'integer':
|
|
86
|
+
return typeof v === 'number';
|
|
87
|
+
case 'boolean':
|
|
88
|
+
return typeof v === 'boolean';
|
|
89
|
+
case 'array':
|
|
90
|
+
return Array.isArray(v);
|
|
91
|
+
case 'object':
|
|
92
|
+
return typeof v === 'object' && !Array.isArray(v) && v !== null;
|
|
93
|
+
default:
|
|
94
|
+
return true; // unknown/`any` — don't block
|
|
95
|
+
}
|
|
96
|
+
}
|
package/dist/loop.d.ts
CHANGED
|
@@ -35,6 +35,13 @@ export interface RunToolLoopOptions {
|
|
|
35
35
|
includePartialMessages?: boolean;
|
|
36
36
|
/** Correlation id stamped on every emitted SDKMessage. */
|
|
37
37
|
sessionId?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Validate tool arguments before executing; on malformed/incomplete JSON,
|
|
40
|
+
* feed the model a corrective `is_error` tool_result (with the expected
|
|
41
|
+
* schema) instead of running the tool with garbage, so it self-heals.
|
|
42
|
+
* Default `true`. Set `false` to pass raw args straight through.
|
|
43
|
+
*/
|
|
44
|
+
repairToolCalls?: boolean;
|
|
38
45
|
}
|
|
39
46
|
/**
|
|
40
47
|
* Run the bare tool loop, yielding SDKMessages until the model stops or maxTurns.
|
package/dist/loop.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { toolByName, toolDefs } from './tools/index.js';
|
|
2
|
+
import { validateToolArguments } from './llm/repair.js';
|
|
2
3
|
import { uuid } from './util/ids.js';
|
|
3
4
|
const emptyUsage = () => ({ input_tokens: 0, output_tokens: 0 });
|
|
4
5
|
function addUsage(t, b) {
|
|
@@ -78,6 +79,7 @@ export async function* runToolLoop(opts) {
|
|
|
78
79
|
const { history, llm, model, ctx, signal, canUseTool, onClientTool } = opts;
|
|
79
80
|
const tools = opts.tools;
|
|
80
81
|
const clientTools = new Set(opts.clientTools ?? []);
|
|
82
|
+
const repair = opts.repairToolCalls !== false;
|
|
81
83
|
const maxTurns = opts.maxTurns ?? 50;
|
|
82
84
|
const sessionId = opts.sessionId ?? uuid();
|
|
83
85
|
const emitPartial = !!opts.includePartialMessages;
|
|
@@ -187,50 +189,62 @@ export async function* runToolLoop(opts) {
|
|
|
187
189
|
const tool = byName.get(name);
|
|
188
190
|
let content = '';
|
|
189
191
|
let isError = false;
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
isError = true;
|
|
197
|
-
}
|
|
198
|
-
else {
|
|
199
|
-
try {
|
|
200
|
-
const r = await onClientTool({ tool_use_id: call.id, name, input });
|
|
201
|
-
content = (typeof r.content === 'string' ? r.content : JSON.stringify(r.content ?? ''));
|
|
202
|
-
isError = !!r.is_error;
|
|
203
|
-
}
|
|
204
|
-
catch (err) {
|
|
205
|
-
content = `Error (client) ${name}: ${err instanceof Error ? err.message : String(err)}`;
|
|
206
|
-
isError = true;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
else if (!tool) {
|
|
211
|
-
content = `Error: unknown tool "${name}"`;
|
|
192
|
+
// Repair: validate args against the tool's schema before running. On a
|
|
193
|
+
// malformed/incomplete call, hand the model a corrective tool_result so
|
|
194
|
+
// it retries with valid JSON instead of executing with garbage.
|
|
195
|
+
const check = repair && tool ? validateToolArguments(tool.def, call.function.arguments) : null;
|
|
196
|
+
if (check && !check.ok) {
|
|
197
|
+
content = check.error;
|
|
212
198
|
isError = true;
|
|
213
199
|
}
|
|
214
200
|
else {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
201
|
+
if (check)
|
|
202
|
+
input = check.input;
|
|
203
|
+
// Delegated tool (listed in clientTools, or has no `run`): execute on the
|
|
204
|
+
// host via onClientTool instead of `ctx` — never touches the server FS.
|
|
205
|
+
const delegated = clientTools.has(name) || (tool != null && !tool.run);
|
|
206
|
+
if (delegated) {
|
|
207
|
+
if (!onClientTool) {
|
|
208
|
+
content = `No client executor for "${name}" (delegated tool; pass onClientTool).`;
|
|
209
|
+
isError = true;
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
try {
|
|
213
|
+
const r = await onClientTool({ tool_use_id: call.id, name, input });
|
|
214
|
+
content = (typeof r.content === 'string' ? r.content : JSON.stringify(r.content ?? ''));
|
|
215
|
+
isError = !!r.is_error;
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
content = `Error (client) ${name}: ${err instanceof Error ? err.message : String(err)}`;
|
|
219
|
+
isError = true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else if (!tool) {
|
|
224
|
+
content = `Error: unknown tool "${name}"`;
|
|
220
225
|
isError = true;
|
|
221
226
|
}
|
|
222
227
|
else {
|
|
223
|
-
|
|
224
|
-
input
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
content =
|
|
228
|
-
isError = !!r.isError;
|
|
229
|
-
}
|
|
230
|
-
catch (err) {
|
|
231
|
-
content = `Error executing ${name}: ${err instanceof Error ? err.message : String(err)}`;
|
|
228
|
+
const decision = canUseTool
|
|
229
|
+
? await canUseTool(name, input, { signal, toolUseId: call.id })
|
|
230
|
+
: { behavior: 'allow' };
|
|
231
|
+
if (decision.behavior === 'deny') {
|
|
232
|
+
content = `Permission denied: ${decision.message}`;
|
|
232
233
|
isError = true;
|
|
233
234
|
}
|
|
235
|
+
else {
|
|
236
|
+
if ('updatedInput' in decision && decision.updatedInput)
|
|
237
|
+
input = decision.updatedInput;
|
|
238
|
+
try {
|
|
239
|
+
const r = await tool.run(input, ctx);
|
|
240
|
+
content = r.content;
|
|
241
|
+
isError = !!r.isError;
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
content = `Error executing ${name}: ${err instanceof Error ? err.message : String(err)}`;
|
|
245
|
+
isError = true;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
234
248
|
}
|
|
235
249
|
}
|
|
236
250
|
const textOut = resultToText(content);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anyclaude-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Standalone, browser-compatible SDK providing Claude Code agent capabilities (tools, tool loop, multi-turn, MCP, sub-agents, sessions) against any OpenAI/Anthropic-compatible LLM endpoint. Runs in the browser (WebContainer), Node, and Bun — no backend required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -67,6 +67,10 @@
|
|
|
67
67
|
"./loop": {
|
|
68
68
|
"types": "./dist/loop.d.ts",
|
|
69
69
|
"import": "./dist/loop.js"
|
|
70
|
+
},
|
|
71
|
+
"./anthropic-endpoint": {
|
|
72
|
+
"types": "./dist/anthropic-endpoint.d.ts",
|
|
73
|
+
"import": "./dist/anthropic-endpoint.js"
|
|
70
74
|
}
|
|
71
75
|
},
|
|
72
76
|
"sideEffects": false,
|