anyclaude-sdk 0.5.0 → 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.
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anyclaude-sdk",
3
- "version": "0.5.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,