anyclaude-sdk 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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/dist/queue.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import type { ContentBlockParam } from './types/index.js';
2
2
  export type QueuedContent = string | ContentBlockParam[];
3
3
  export interface QueuedMessage {
4
+ /** Stable id for this queued item — use it to `remove()` a single message. */
5
+ id: string;
4
6
  content: QueuedContent;
5
7
  /** Epoch ms when enqueued. */
6
8
  at: number;
@@ -8,10 +10,19 @@ export interface QueuedMessage {
8
10
  export declare class MessageQueue {
9
11
  private items;
10
12
  private listeners;
11
- /** Enqueue a user message to be delivered at the next turn boundary. */
12
- push(content: QueuedContent): void;
13
+ /**
14
+ * Enqueue a user message to be delivered at the next turn boundary.
15
+ * Returns the item's stable `id` (pass it to `remove()` to cancel just this one).
16
+ */
17
+ push(content: QueuedContent): string;
13
18
  /** Remove and return the oldest queued message (FIFO), or undefined if empty. */
14
19
  shift(): QueuedMessage | undefined;
20
+ /**
21
+ * Remove a single pending message by its `id` (e.g. a per-pill ✕ in the UI).
22
+ * Returns true if an item was removed. No-op if the id isn't pending — already
23
+ * drained (shifted) items can't be cancelled.
24
+ */
25
+ remove(id: string): boolean;
15
26
  peek(): QueuedMessage | undefined;
16
27
  get size(): number;
17
28
  /** Snapshot of pending messages (does not drain). */
package/dist/queue.js CHANGED
@@ -1,12 +1,18 @@
1
+ import { uuid } from './util/ids.js';
1
2
  export class MessageQueue {
2
3
  constructor() {
3
4
  this.items = [];
4
5
  this.listeners = new Set();
5
6
  }
6
- /** Enqueue a user message to be delivered at the next turn boundary. */
7
+ /**
8
+ * Enqueue a user message to be delivered at the next turn boundary.
9
+ * Returns the item's stable `id` (pass it to `remove()` to cancel just this one).
10
+ */
7
11
  push(content) {
8
- this.items.push({ content, at: Date.now() });
12
+ const id = uuid();
13
+ this.items.push({ id, content, at: Date.now() });
9
14
  this.emit();
15
+ return id;
10
16
  }
11
17
  /** Remove and return the oldest queued message (FIFO), or undefined if empty. */
12
18
  shift() {
@@ -15,6 +21,19 @@ export class MessageQueue {
15
21
  this.emit();
16
22
  return m;
17
23
  }
24
+ /**
25
+ * Remove a single pending message by its `id` (e.g. a per-pill ✕ in the UI).
26
+ * Returns true if an item was removed. No-op if the id isn't pending — already
27
+ * drained (shifted) items can't be cancelled.
28
+ */
29
+ remove(id) {
30
+ const i = this.items.findIndex((m) => m.id === id);
31
+ if (i < 0)
32
+ return false;
33
+ this.items.splice(i, 1);
34
+ this.emit();
35
+ return true;
36
+ }
18
37
  peek() {
19
38
  return this.items[0];
20
39
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anyclaude-sdk",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
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,