dsh-plugin-subscriptions 0.1.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.
Files changed (44) hide show
  1. package/README.md +93 -0
  2. package/README.zh.md +93 -0
  3. package/cordis.patch.yml +12 -0
  4. package/lib/auth/jwt.d.ts +10 -0
  5. package/lib/auth/jwt.js +25 -0
  6. package/lib/auth/oauth-flow.d.ts +91 -0
  7. package/lib/auth/oauth-flow.js +227 -0
  8. package/lib/auth/pkce.d.ts +31 -0
  9. package/lib/auth/pkce.js +35 -0
  10. package/lib/auth/rpc.d.ts +51 -0
  11. package/lib/auth/rpc.js +83 -0
  12. package/lib/auth/store.d.ts +90 -0
  13. package/lib/auth/store.js +137 -0
  14. package/lib/client/SubscriptionsSection.d.ts +30 -0
  15. package/lib/client/SubscriptionsSection.js +290 -0
  16. package/lib/client/index.d.ts +31 -0
  17. package/lib/client/index.js +35 -0
  18. package/lib/client/locales.d.ts +45 -0
  19. package/lib/client/locales.js +43 -0
  20. package/lib/client.js +546 -0
  21. package/lib/client.js.map +1 -0
  22. package/lib/index.d.ts +34 -0
  23. package/lib/index.js +2932 -0
  24. package/lib/providers/claude.d.ts +60 -0
  25. package/lib/providers/claude.js +243 -0
  26. package/lib/providers/codex.d.ts +96 -0
  27. package/lib/providers/codex.js +391 -0
  28. package/lib/providers/common.d.ts +185 -0
  29. package/lib/providers/common.js +302 -0
  30. package/lib/providers/grok.d.ts +90 -0
  31. package/lib/providers/grok.js +337 -0
  32. package/lib/tools/image-generate.d.ts +60 -0
  33. package/lib/tools/image-generate.js +142 -0
  34. package/lib/tools/x-search.d.ts +58 -0
  35. package/lib/tools/x-search.js +195 -0
  36. package/lib/translate/anthropic.d.ts +120 -0
  37. package/lib/translate/anthropic.js +370 -0
  38. package/lib/translate/resolved.d.ts +35 -0
  39. package/lib/translate/resolved.js +40 -0
  40. package/lib/translate/responses.d.ts +127 -0
  41. package/lib/translate/responses.js +352 -0
  42. package/lib/translate/sse.d.ts +21 -0
  43. package/lib/translate/sse.js +56 -0
  44. package/package.json +83 -0
@@ -0,0 +1,195 @@
1
+ /**
2
+ * `x_search` tool: run xAI's hosted X (Twitter) search through the grok
3
+ * subscription's OAuth session. The wire call is a non-streaming Responses
4
+ * request carrying the built-in `x_search` tool definition; the canonical
5
+ * output is `{ answer, citations }`.
6
+ */
7
+ import { defineTool } from '@deepseek-ai/dsh-tools';
8
+ import { httpLlmError, TokenManager } from '../providers/common.js';
9
+ /** Endpoint the search request is posted to. */
10
+ export const X_SEARCH_URL = 'https://api.x.ai/v1/responses';
11
+ /** Grok model the search runs on (a catalog model of the grok provider). */
12
+ export const X_SEARCH_MODEL = 'grok-4';
13
+ /** xAI caps each handle filter list at ten entries. */
14
+ const MAX_HANDLES = 10;
15
+ /**
16
+ * Validate and assemble the request facts from tool arguments. Throws plain
17
+ * Errors for argument problems the schema DSL cannot express (non-empty
18
+ * query, handle caps, mutually exclusive filters).
19
+ */
20
+ export function buildXSearchRequest(args) {
21
+ const query = args.query.trim();
22
+ if (query.length === 0)
23
+ throw new Error('x_search: query must be a non-empty string');
24
+ const allowed = normalizeHandles(args.allowed_x_handles, 'allowed_x_handles');
25
+ const excluded = normalizeHandles(args.excluded_x_handles, 'excluded_x_handles');
26
+ if (allowed.length > 0 && excluded.length > 0) {
27
+ throw new Error('x_search: allowed_x_handles and excluded_x_handles cannot be used together');
28
+ }
29
+ const tool = { type: 'x_search' };
30
+ if (allowed.length > 0)
31
+ tool.allowed_x_handles = allowed;
32
+ if (excluded.length > 0)
33
+ tool.excluded_x_handles = excluded;
34
+ if (args.from_date !== undefined && args.from_date.trim().length > 0)
35
+ tool.from_date = args.from_date.trim();
36
+ if (args.to_date !== undefined && args.to_date.trim().length > 0)
37
+ tool.to_date = args.to_date.trim();
38
+ if (args.enable_image_understanding === true)
39
+ tool.enable_image_understanding = true;
40
+ if (args.enable_video_understanding === true)
41
+ tool.enable_video_understanding = true;
42
+ return { query, tool };
43
+ }
44
+ /** Strip `@` prefixes, drop blanks, and enforce the provider's handle cap. */
45
+ function normalizeHandles(value, field) {
46
+ if (value === undefined)
47
+ return [];
48
+ const handles = value.map(handle => handle.trim().replace(/^@+/, '')).filter(handle => handle.length > 0);
49
+ if (handles.length > MAX_HANDLES)
50
+ throw new Error(`x_search: ${field} supports at most ${MAX_HANDLES} handles`);
51
+ return handles;
52
+ }
53
+ function isRecord(value) {
54
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
55
+ }
56
+ /**
57
+ * Extract the answer text and citation URLs from a Responses payload: the
58
+ * `output_text` shortcut or message output parts for the answer, and both
59
+ * top-level `citations` and inline `url_citation` annotations for sources.
60
+ */
61
+ export function parseXSearchResponse(payload) {
62
+ const body = isRecord(payload) ? payload : {};
63
+ let answer = typeof body.output_text === 'string' ? body.output_text.trim() : '';
64
+ const citations = [];
65
+ const push = (url) => {
66
+ if (typeof url === 'string' && url.length > 0 && !citations.includes(url))
67
+ citations.push(url);
68
+ };
69
+ if (Array.isArray(body.citations)) {
70
+ for (const citation of body.citations)
71
+ push(citation);
72
+ }
73
+ const parts = [];
74
+ if (Array.isArray(body.output)) {
75
+ for (const item of body.output) {
76
+ if (!isRecord(item) || item.type !== 'message' || !Array.isArray(item.content))
77
+ continue;
78
+ for (const part of item.content) {
79
+ if (!isRecord(part))
80
+ continue;
81
+ if ((part.type === 'output_text' || part.type === 'text')
82
+ && typeof part.text === 'string' && part.text.trim().length > 0) {
83
+ parts.push(part.text.trim());
84
+ }
85
+ if (Array.isArray(part.annotations)) {
86
+ for (const annotation of part.annotations) {
87
+ if (isRecord(annotation) && annotation.type === 'url_citation')
88
+ push(annotation.url);
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
94
+ if (answer.length === 0)
95
+ answer = parts.join('\n\n');
96
+ return { answer, citations };
97
+ }
98
+ /** Bound a call-card title's query. */
99
+ function truncate(text, max = 60) {
100
+ return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
101
+ }
102
+ /**
103
+ * Build the `x_search` tool definition.
104
+ * @param options - grok session source and fetch implementation.
105
+ * @returns the tool to register on `ctx.tools`.
106
+ */
107
+ export function createXSearchTool(options) {
108
+ return defineTool({
109
+ name: 'x_search',
110
+ description: "Search X (Twitter) posts, profiles, and threads using the grok subscription's hosted xAI x_search. "
111
+ + 'Use this for current discussion, reactions, or claims on X rather than general web pages.',
112
+ parameters: {
113
+ query: { type: 'string', required: true, description: 'What to look up on X.' },
114
+ allowed_x_handles: {
115
+ type: 'array',
116
+ items: { type: 'string' },
117
+ description: 'X handles to include exclusively (max 10).',
118
+ },
119
+ excluded_x_handles: {
120
+ type: 'array',
121
+ items: { type: 'string' },
122
+ description: 'X handles to exclude (max 10).',
123
+ },
124
+ from_date: { type: 'string', description: 'Optional start date in YYYY-MM-DD format.' },
125
+ to_date: { type: 'string', description: 'Optional end date in YYYY-MM-DD format.' },
126
+ enable_image_understanding: {
127
+ type: 'boolean',
128
+ description: 'Whether xAI should analyze images attached to matching posts.',
129
+ },
130
+ enable_video_understanding: {
131
+ type: 'boolean',
132
+ description: 'Whether xAI should analyze videos attached to matching posts.',
133
+ },
134
+ },
135
+ output: {
136
+ schema: {
137
+ type: 'object',
138
+ properties: {
139
+ answer: { type: 'string', required: true },
140
+ citations: { type: 'array', items: { type: 'string' }, required: true },
141
+ },
142
+ additionalProperties: false,
143
+ },
144
+ render: (_args, value) => [{
145
+ type: 'text',
146
+ text: value.citations.length > 0
147
+ ? `${value.answer}\n\nSources:\n${value.citations.map(citation => `- ${citation}`).join('\n')}`
148
+ : value.answer,
149
+ }],
150
+ presentationMeta: (_args, value) => ({ answer: value.answer, citations: value.citations }),
151
+ },
152
+ presentCall: args => ({
153
+ card: 'generic',
154
+ title: `x_search: ${truncate(args.query)}`,
155
+ kind: 'search',
156
+ }),
157
+ presentResult: (_args, result) => {
158
+ if (result.isError || !isRecord(result.meta))
159
+ return undefined;
160
+ const citations = Array.isArray(result.meta.citations) ? result.meta.citations : [];
161
+ return {
162
+ card: 'web',
163
+ kind: 'search',
164
+ sources: citations.filter((citation) => typeof citation === 'string')
165
+ .map(url => ({ url })),
166
+ ...typeof result.meta.answer === 'string' && result.meta.answer.length > 0
167
+ ? { answer: result.meta.answer }
168
+ : {},
169
+ truncated: false,
170
+ };
171
+ },
172
+ async execute(args, exec) {
173
+ const request = buildXSearchRequest(args);
174
+ const session = await options.tokens.session();
175
+ const response = await (options.fetchFn ?? fetch)(X_SEARCH_URL, {
176
+ method: 'POST',
177
+ headers: {
178
+ 'authorization': `Bearer ${session.accessToken}`,
179
+ 'content-type': 'application/json',
180
+ 'accept': 'application/json',
181
+ },
182
+ body: JSON.stringify({
183
+ model: X_SEARCH_MODEL,
184
+ input: [{ role: 'user', content: request.query }],
185
+ tools: [request.tool],
186
+ store: false,
187
+ }),
188
+ signal: exec.signal,
189
+ });
190
+ if (!response.ok)
191
+ throw await httpLlmError(response, 'x_search');
192
+ return parseXSearchResponse(await response.json());
193
+ },
194
+ });
195
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Translate between the harness message vocabulary and the Anthropic Messages
3
+ * API wire format used by the claude provider: request message assembly, tool
4
+ * schema mapping, and a push-model SSE-event → StreamChunk state machine
5
+ * ({@link AnthropicStreamTranslator}) so tests need no streams.
6
+ */
7
+ import { LlmError } from '@deepseek-ai/dsh-llm';
8
+ import type { StreamChunk, ToolSchema } from '@deepseek-ai/dsh-llm';
9
+ import type { TranslatableMessage } from './resolved.js';
10
+ /**
11
+ * The Claude Code identity block. The subscription endpoint rejects requests
12
+ * that do not present as Claude Code, so this block is REQUIRED as the first
13
+ * system entry on every request.
14
+ */
15
+ export declare const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
16
+ /** One Anthropic request message. */
17
+ export interface AnthropicMessage {
18
+ role: 'user' | 'assistant';
19
+ content: Record<string, unknown>[];
20
+ }
21
+ /**
22
+ * Convert harness messages into Anthropic messages. Consecutive same-role
23
+ * messages merge into one message with multiple content blocks; tool results
24
+ * arrive as user messages with `tool_result` blocks; system-role messages are
25
+ * handled by {@link toAnthropicSystem} and skipped here. Reasoning blocks are
26
+ * not replayed (v1). Images must arrive pre-resolved
27
+ * ({@link TranslatableMessage}); an unresolved ImageBlock is skipped because
28
+ * its bytes are unreachable here.
29
+ * @param messages - ordered conversation messages with resolved images.
30
+ * @returns Anthropic messages in conversation order.
31
+ */
32
+ export declare function toAnthropicMessages(messages: readonly TranslatableMessage[]): AnthropicMessage[];
33
+ /**
34
+ * Build the Anthropic `system` array: the mandatory Claude Code identity
35
+ * block, then the explicit system prompt, then any system-role messages.
36
+ * @param system - explicit system prompt, when set.
37
+ * @param messages - conversation messages; their system-role text is appended.
38
+ * @returns the system content blocks.
39
+ */
40
+ export declare function toAnthropicSystem(system?: string, messages?: readonly TranslatableMessage[]): Record<string, unknown>[];
41
+ /**
42
+ * Map harness tool schemas to Anthropic tools.
43
+ * @param tools - tool schemas from the request.
44
+ * @returns Anthropic `tools` array entries.
45
+ */
46
+ export declare function toAnthropicTools(tools: readonly ToolSchema[]): Record<string, unknown>[];
47
+ /** The subset of Anthropic SSE event shapes this translator reads. */
48
+ export interface AnthropicStreamEvent {
49
+ type: string;
50
+ index?: number;
51
+ message?: {
52
+ usage?: {
53
+ input_tokens?: number;
54
+ output_tokens?: number;
55
+ cache_read_input_tokens?: number;
56
+ cache_creation_input_tokens?: number;
57
+ };
58
+ };
59
+ content_block?: {
60
+ type?: string;
61
+ id?: string;
62
+ name?: string;
63
+ };
64
+ delta?: {
65
+ type?: string;
66
+ text?: string;
67
+ thinking?: string;
68
+ partial_json?: string;
69
+ stop_reason?: string;
70
+ };
71
+ usage?: {
72
+ output_tokens?: number;
73
+ };
74
+ error?: {
75
+ type?: string;
76
+ message?: string;
77
+ };
78
+ }
79
+ /**
80
+ * Classify an Anthropic `error` event into a thrown LlmError.
81
+ * @param error - the wire error object.
82
+ * @returns the mapped error.
83
+ */
84
+ export declare function anthropicFailure(error: {
85
+ type?: string;
86
+ message?: string;
87
+ } | undefined): LlmError;
88
+ /**
89
+ * Push-model Anthropic SSE translator: feed each parsed event object to
90
+ * {@link push} and collect the emitted harness StreamChunks. Block indexes
91
+ * are allocated in first-seen order; `usage` is emitted before the terminal
92
+ * `finish`, and nothing is emitted after it. `error` events throw
93
+ * {@link LlmError}.
94
+ */
95
+ export declare class AnthropicStreamTranslator {
96
+ private blocks;
97
+ private nextIndex;
98
+ private sawAnyBlock;
99
+ private pendingUsage;
100
+ private outputTokens;
101
+ private stopReason;
102
+ private usageEmitted;
103
+ /** Set once `message_stop` produced the terminal finish chunk. */
104
+ terminated: boolean;
105
+ private open;
106
+ private emitUsage;
107
+ /**
108
+ * Process one parsed Anthropic SSE event.
109
+ * @param event - the parsed event object.
110
+ * @returns the StreamChunks this event produced (possibly none).
111
+ */
112
+ push(event: AnthropicStreamEvent): StreamChunk[];
113
+ }
114
+ /**
115
+ * Consume an Anthropic SSE byte stream and yield harness StreamChunks.
116
+ * @param stream - raw response body.
117
+ * @param onActivity - transport-activity callback for the idle watchdog.
118
+ * @returns the chunk stream; throws when the stream ends before `message_stop`.
119
+ */
120
+ export declare function streamAnthropic(stream: ReadableStream<Uint8Array>, onActivity?: () => void): AsyncGenerator<StreamChunk>;
@@ -0,0 +1,370 @@
1
+ /**
2
+ * Translate between the harness message vocabulary and the Anthropic Messages
3
+ * API wire format used by the claude provider: request message assembly, tool
4
+ * schema mapping, and a push-model SSE-event → StreamChunk state machine
5
+ * ({@link AnthropicStreamTranslator}) so tests need no streams.
6
+ */
7
+ import { CallId, CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, LlmError, } from '@deepseek-ai/dsh-llm';
8
+ import { parseSse } from './sse.js';
9
+ /**
10
+ * The Claude Code identity block. The subscription endpoint rejects requests
11
+ * that do not present as Claude Code, so this block is REQUIRED as the first
12
+ * system entry on every request.
13
+ */
14
+ export const CLAUDE_CODE_IDENTITY = 'You are Claude Code, Anthropic\'s official CLI for Claude.';
15
+ /** Flatten a tool result's content to plain text for `tool_result`. */
16
+ function toolResultText(block) {
17
+ return block.content.map(part => (part.type === 'text' ? part.text : '')).join('');
18
+ }
19
+ /** Parse a tool call's raw JSON arguments into Anthropic's object-shaped `input`. */
20
+ function parseToolInput(raw) {
21
+ try {
22
+ const parsed = JSON.parse(raw);
23
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
24
+ return parsed;
25
+ }
26
+ return {};
27
+ }
28
+ catch {
29
+ // The model produced malformed JSON; an empty object keeps the request valid.
30
+ return {};
31
+ }
32
+ }
33
+ /**
34
+ * Convert harness messages into Anthropic messages. Consecutive same-role
35
+ * messages merge into one message with multiple content blocks; tool results
36
+ * arrive as user messages with `tool_result` blocks; system-role messages are
37
+ * handled by {@link toAnthropicSystem} and skipped here. Reasoning blocks are
38
+ * not replayed (v1). Images must arrive pre-resolved
39
+ * ({@link TranslatableMessage}); an unresolved ImageBlock is skipped because
40
+ * its bytes are unreachable here.
41
+ * @param messages - ordered conversation messages with resolved images.
42
+ * @returns Anthropic messages in conversation order.
43
+ */
44
+ export function toAnthropicMessages(messages) {
45
+ const out = [];
46
+ for (const message of messages) {
47
+ if (message.role === 'system')
48
+ continue;
49
+ const role = message.role;
50
+ const blocks = [];
51
+ for (const block of message.content) {
52
+ switch (block.type) {
53
+ case 'text':
54
+ blocks.push({ type: 'text', text: block.text });
55
+ break;
56
+ case 'tool-call':
57
+ blocks.push({
58
+ type: 'tool_use',
59
+ id: String(block.id),
60
+ name: block.name,
61
+ input: parseToolInput(block.arguments),
62
+ });
63
+ break;
64
+ case 'tool-result':
65
+ blocks.push({
66
+ type: 'tool_result',
67
+ tool_use_id: String(block.toolCallId),
68
+ content: toolResultText(block),
69
+ ...block.isError === true ? { is_error: true } : {},
70
+ });
71
+ break;
72
+ case 'image':
73
+ if ('dataBase64' in block) {
74
+ blocks.push({
75
+ type: 'image',
76
+ source: { type: 'base64', media_type: block.mediaType, data: block.dataBase64 },
77
+ });
78
+ }
79
+ // An unresolved ImageBlock carries only an attachment reference; the
80
+ // adapter resolves images before translation, so this is skipped.
81
+ break;
82
+ default:
83
+ // reasoning (not replayed), unknown blocks.
84
+ break;
85
+ }
86
+ }
87
+ if (blocks.length === 0)
88
+ continue;
89
+ const last = out[out.length - 1];
90
+ if (last !== undefined && last.role === role)
91
+ last.content.push(...blocks);
92
+ else
93
+ out.push({ role, content: blocks });
94
+ }
95
+ return out;
96
+ }
97
+ /**
98
+ * Build the Anthropic `system` array: the mandatory Claude Code identity
99
+ * block, then the explicit system prompt, then any system-role messages.
100
+ * @param system - explicit system prompt, when set.
101
+ * @param messages - conversation messages; their system-role text is appended.
102
+ * @returns the system content blocks.
103
+ */
104
+ export function toAnthropicSystem(system, messages) {
105
+ const blocks = [{ type: 'text', text: CLAUDE_CODE_IDENTITY }];
106
+ if (system !== undefined && system.length > 0)
107
+ blocks.push({ type: 'text', text: system });
108
+ for (const message of messages ?? []) {
109
+ if (message.role !== 'system')
110
+ continue;
111
+ for (const block of message.content) {
112
+ if (block.type === 'text')
113
+ blocks.push({ type: 'text', text: block.text });
114
+ }
115
+ }
116
+ return blocks;
117
+ }
118
+ /**
119
+ * Map harness tool schemas to Anthropic tools.
120
+ * @param tools - tool schemas from the request.
121
+ * @returns Anthropic `tools` array entries.
122
+ */
123
+ export function toAnthropicTools(tools) {
124
+ return tools.map(tool => ({
125
+ name: tool.name,
126
+ description: tool.description,
127
+ input_schema: tool.parameters,
128
+ }));
129
+ }
130
+ /** Assemble the final ContentBlock for one open block. */
131
+ function closeBlock(block) {
132
+ switch (block.kind) {
133
+ case 'text':
134
+ return { type: 'text', text: block.text };
135
+ case 'reasoning':
136
+ return { type: 'reasoning', text: block.text };
137
+ case 'tool-call':
138
+ return {
139
+ type: 'tool-call',
140
+ id: CallId(block.callId),
141
+ name: block.name ?? '',
142
+ arguments: block.text,
143
+ };
144
+ }
145
+ }
146
+ /**
147
+ * Classify an Anthropic `error` event into a thrown LlmError.
148
+ * @param error - the wire error object.
149
+ * @returns the mapped error.
150
+ */
151
+ export function anthropicFailure(error) {
152
+ const type = error?.type ?? 'unknown_error';
153
+ const message = error?.message ?? `Anthropic reported ${type}`;
154
+ if (type === 'invalid_request_error' && /prompt is too long/i.test(message)) {
155
+ return new LlmError(message, CONTEXT_WINDOW_EXCEEDED_CODE);
156
+ }
157
+ if (type === 'rate_limit_error')
158
+ return new LlmError(message, 'RATE_LIMIT');
159
+ if (type === 'authentication_error')
160
+ return new LlmError(message, 'AUTH');
161
+ return new LlmError(message, 'SERVER');
162
+ }
163
+ /**
164
+ * Push-model Anthropic SSE translator: feed each parsed event object to
165
+ * {@link push} and collect the emitted harness StreamChunks. Block indexes
166
+ * are allocated in first-seen order; `usage` is emitted before the terminal
167
+ * `finish`, and nothing is emitted after it. `error` events throw
168
+ * {@link LlmError}.
169
+ */
170
+ export class AnthropicStreamTranslator {
171
+ blocks = new Map();
172
+ nextIndex = 0;
173
+ sawAnyBlock = false;
174
+ pendingUsage;
175
+ outputTokens;
176
+ stopReason = 'stop';
177
+ usageEmitted = false;
178
+ /** Set once `message_stop` produced the terminal finish chunk. */
179
+ terminated = false;
180
+ open(wireIndex, kind, chunks, callId = '', name) {
181
+ const block = {
182
+ index: this.nextIndex++,
183
+ kind,
184
+ text: '',
185
+ callId,
186
+ ...name === undefined ? {} : { name },
187
+ };
188
+ this.blocks.set(wireIndex, block);
189
+ this.sawAnyBlock = true;
190
+ chunks.push({ type: 'block-start', index: block.index, blockType: kind });
191
+ return block;
192
+ }
193
+ emitUsage(chunks) {
194
+ if (this.usageEmitted)
195
+ return;
196
+ this.usageEmitted = true;
197
+ const usage = {
198
+ inputTokens: this.pendingUsage?.inputTokens ?? 0,
199
+ outputTokens: this.outputTokens ?? 0,
200
+ ...this.pendingUsage?.cacheReadTokens !== undefined
201
+ ? { cacheReadTokens: this.pendingUsage.cacheReadTokens }
202
+ : {},
203
+ ...this.pendingUsage?.cacheWriteTokens !== undefined
204
+ ? { cacheWriteTokens: this.pendingUsage.cacheWriteTokens }
205
+ : {},
206
+ };
207
+ chunks.push({ type: 'usage', usage });
208
+ }
209
+ /**
210
+ * Process one parsed Anthropic SSE event.
211
+ * @param event - the parsed event object.
212
+ * @returns the StreamChunks this event produced (possibly none).
213
+ */
214
+ push(event) {
215
+ if (this.terminated)
216
+ return [];
217
+ const chunks = [];
218
+ switch (event.type) {
219
+ case 'message_start': {
220
+ const usage = event.message?.usage;
221
+ if (usage !== undefined) {
222
+ this.pendingUsage = {
223
+ inputTokens: usage.input_tokens ?? 0,
224
+ ...usage.cache_read_input_tokens !== undefined
225
+ ? { cacheReadTokens: usage.cache_read_input_tokens }
226
+ : {},
227
+ ...usage.cache_creation_input_tokens !== undefined
228
+ ? { cacheWriteTokens: usage.cache_creation_input_tokens }
229
+ : {},
230
+ };
231
+ this.outputTokens = usage.output_tokens ?? this.outputTokens;
232
+ }
233
+ return chunks;
234
+ }
235
+ case 'content_block_start': {
236
+ const wireIndex = event.index ?? 0;
237
+ const block = event.content_block;
238
+ switch (block?.type) {
239
+ case 'text':
240
+ this.open(wireIndex, 'text', chunks);
241
+ break;
242
+ case 'thinking':
243
+ this.open(wireIndex, 'reasoning', chunks);
244
+ break;
245
+ case 'tool_use': {
246
+ const opened = this.open(wireIndex, 'tool-call', chunks, block.id ?? '', block.name);
247
+ chunks.push({
248
+ type: 'tool-call-delta',
249
+ index: opened.index,
250
+ id: CallId(opened.callId),
251
+ ...block.name === undefined ? {} : { name: block.name },
252
+ argumentsDelta: '',
253
+ });
254
+ break;
255
+ }
256
+ default:
257
+ break;
258
+ }
259
+ return chunks;
260
+ }
261
+ case 'content_block_delta': {
262
+ const wireIndex = event.index ?? 0;
263
+ const block = this.blocks.get(wireIndex);
264
+ const delta = event.delta;
265
+ if (block === undefined || delta === undefined)
266
+ return chunks;
267
+ switch (delta.type) {
268
+ case 'text_delta':
269
+ block.text += delta.text ?? '';
270
+ chunks.push({ type: 'text-delta', index: block.index, text: delta.text ?? '' });
271
+ break;
272
+ case 'thinking_delta':
273
+ block.text += delta.thinking ?? '';
274
+ chunks.push({ type: 'reasoning-delta', index: block.index, text: delta.thinking ?? '' });
275
+ break;
276
+ case 'input_json_delta':
277
+ block.text += delta.partial_json ?? '';
278
+ chunks.push({
279
+ type: 'tool-call-delta',
280
+ index: block.index,
281
+ id: CallId(block.callId),
282
+ ...block.name === undefined ? {} : { name: block.name },
283
+ argumentsDelta: delta.partial_json ?? '',
284
+ });
285
+ break;
286
+ default:
287
+ // signature_delta and future deltas carry no harness content.
288
+ break;
289
+ }
290
+ return chunks;
291
+ }
292
+ case 'content_block_stop': {
293
+ const wireIndex = event.index ?? 0;
294
+ const block = this.blocks.get(wireIndex);
295
+ if (block === undefined)
296
+ return chunks;
297
+ this.blocks.delete(wireIndex);
298
+ chunks.push({ type: 'block-end', index: block.index, block: closeBlock(block) });
299
+ return chunks;
300
+ }
301
+ case 'message_delta': {
302
+ if (event.usage?.output_tokens !== undefined)
303
+ this.outputTokens = event.usage.output_tokens;
304
+ switch (event.delta?.stop_reason) {
305
+ case 'end_turn':
306
+ case 'stop_sequence':
307
+ this.stopReason = 'stop';
308
+ break;
309
+ case 'tool_use':
310
+ this.stopReason = 'tool-calls';
311
+ break;
312
+ case 'max_tokens':
313
+ this.stopReason = 'max-tokens';
314
+ break;
315
+ default:
316
+ break;
317
+ }
318
+ return chunks;
319
+ }
320
+ case 'message_stop': {
321
+ this.terminated = true;
322
+ for (const [wireIndex, block] of [...this.blocks]) {
323
+ this.blocks.delete(wireIndex);
324
+ chunks.push({ type: 'block-end', index: block.index, block: closeBlock(block) });
325
+ }
326
+ this.emitUsage(chunks);
327
+ if (this.stopReason === 'stop' && !this.sawAnyBlock) {
328
+ chunks.push({
329
+ type: 'finish',
330
+ reason: {
331
+ kind: 'error',
332
+ failure: { message: 'model returned a completed response with no content', code: EMPTY_RESPONSE_CODE },
333
+ },
334
+ });
335
+ }
336
+ else {
337
+ chunks.push({ type: 'finish', reason: { kind: this.stopReason } });
338
+ }
339
+ return chunks;
340
+ }
341
+ case 'error':
342
+ throw anthropicFailure(event.error);
343
+ default:
344
+ // ping and future event types carry no harness content.
345
+ return chunks;
346
+ }
347
+ }
348
+ }
349
+ /**
350
+ * Consume an Anthropic SSE byte stream and yield harness StreamChunks.
351
+ * @param stream - raw response body.
352
+ * @param onActivity - transport-activity callback for the idle watchdog.
353
+ * @returns the chunk stream; throws when the stream ends before `message_stop`.
354
+ */
355
+ export async function* streamAnthropic(stream, onActivity) {
356
+ const translator = new AnthropicStreamTranslator();
357
+ for await (const sseEvent of parseSse(stream, onActivity)) {
358
+ let event;
359
+ try {
360
+ event = JSON.parse(sseEvent.data);
361
+ }
362
+ catch {
363
+ throw new LlmError(`malformed SSE payload: ${sseEvent.data.slice(0, 120)}`, 'MALFORMED_RESPONSE');
364
+ }
365
+ yield* translator.push(event);
366
+ if (translator.terminated)
367
+ return;
368
+ }
369
+ throw new LlmError('Anthropic SSE stream ended before message_stop', 'STREAM_CLOSED');
370
+ }