@hunterzhu/pulse-adapters 0.1.3 → 0.1.4

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.
@@ -1,4 +1,4 @@
1
- import { consumeProviderSse, normalizeAnthropicResponse, parseProviderJson, providerHttpError, providerNetworkError, providerResponseError } from './normalize.js';
1
+ import { consumeProviderSse, normalizeAnthropicResponse, parseProviderJson, providerHttpErrorFromResponse, providerNetworkError, providerResponseError } from './normalize.js';
2
2
  export class AnthropicAdapter {
3
3
  id;
4
4
  config;
@@ -16,7 +16,7 @@ export class AnthropicAdapter {
16
16
  try {
17
17
  const response = await fetch(`${(this.config.baseURL ?? 'https://api.anthropic.com').replace(/\/$/, '')}/v1/messages`, { method: 'POST', signal: params.signal, headers: { 'content-type': 'application/json', ...(this.config.apiKey ? { 'x-api-key': this.config.apiKey } : {}), 'anthropic-version': '2023-06-01' }, body: JSON.stringify(body) });
18
18
  if (!response.ok)
19
- throw providerHttpError(response.status);
19
+ throw await providerHttpErrorFromResponse(response);
20
20
  if (!streaming || !response.headers.get('content-type')?.includes('text/event-stream'))
21
21
  return normalizeAnthropicResponse(await parseProviderJson(response));
22
22
  const events = await consumeProviderSse(response);
@@ -3,10 +3,15 @@ export interface ProviderSseEvent {
3
3
  event?: string;
4
4
  data: any;
5
5
  }
6
- export declare function providerHttpError(status: number): Error & {
6
+ export declare function providerHttpError(status: number, detail?: string): Error & {
7
7
  code: string;
8
8
  retryable: boolean;
9
9
  };
10
+ /** Preserve the provider's actionable error message without copying arbitrary response bodies into logs. */
11
+ export declare function providerHttpErrorFromResponse(response: Response): Promise<Error & {
12
+ code: string;
13
+ retryable: boolean;
14
+ }>;
10
15
  export declare function providerNetworkError(cause: unknown): Error & {
11
16
  code: string;
12
17
  retryable: boolean;
@@ -18,5 +23,5 @@ export declare function providerResponseError(detail: string): Error & {
18
23
  export declare function parseProviderJson(response: Response): Promise<unknown>;
19
24
  /** Read provider SSE frames without treating incomplete tool arguments as executable input. */
20
25
  export declare function consumeProviderSse(response: Response): Promise<ProviderSseEvent[]>;
21
- export declare function normalizeOpenAIResponse(response: any): LLMResult;
26
+ export declare function normalizeOpenAIResponse(response: any, toolNameAliases?: ReadonlyMap<string, string>): LLMResult;
22
27
  export declare function normalizeAnthropicResponse(response: any): LLMResult;
@@ -1,6 +1,43 @@
1
- export function providerHttpError(status) {
1
+ export function providerHttpError(status, detail) {
2
2
  const retryable = status === 408 || status === 425 || status === 429 || status >= 500;
3
- return Object.assign(new Error(`PROVIDER_HTTP_${status}`), { code: `PROVIDER_HTTP_${status}`, retryable });
3
+ const suffix = detail === undefined || detail.length === 0 ? '' : `: ${detail}`;
4
+ return Object.assign(new Error(`PROVIDER_HTTP_${status}${suffix}`), { code: `PROVIDER_HTTP_${status}`, retryable });
5
+ }
6
+ /** Preserve the provider's actionable error message without copying arbitrary response bodies into logs. */
7
+ export async function providerHttpErrorFromResponse(response) {
8
+ let raw = '';
9
+ try {
10
+ raw = typeof response.text === 'function' ? await response.text() : '';
11
+ }
12
+ catch { /* keep the status-only error */ }
13
+ return providerHttpError(response.status, summarizeProviderError(raw));
14
+ }
15
+ function summarizeProviderError(raw) {
16
+ if (!raw.trim())
17
+ return undefined;
18
+ try {
19
+ const parsed = JSON.parse(raw);
20
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
21
+ const root = parsed;
22
+ const error = root.error;
23
+ if (typeof error === 'string')
24
+ return truncateProviderDetail(error);
25
+ if (error && typeof error === 'object' && !Array.isArray(error)) {
26
+ const value = error;
27
+ const fields = [value.message, value.type, value.code, value.param].filter((field) => typeof field === 'string' && field.length > 0);
28
+ if (fields.length > 0)
29
+ return truncateProviderDetail(fields.join(' | '));
30
+ }
31
+ const message = root.message;
32
+ if (typeof message === 'string' && message.length > 0)
33
+ return truncateProviderDetail(message);
34
+ }
35
+ }
36
+ catch { /* fall back to a bounded plain-text detail */ }
37
+ return truncateProviderDetail(raw.replace(/\s+/g, ' ').trim());
38
+ }
39
+ function truncateProviderDetail(value) {
40
+ return value.length <= 500 ? value : `${value.slice(0, 497)}...`;
4
41
  }
5
42
  export function providerNetworkError(cause) {
6
43
  return Object.assign(new Error('PROVIDER_NETWORK_ERROR'), { code: 'PROVIDER_NETWORK_ERROR', retryable: true, cause });
@@ -71,13 +108,13 @@ export async function consumeProviderSse(response) {
71
108
  flush();
72
109
  return events;
73
110
  }
74
- export function normalizeOpenAIResponse(response) {
111
+ export function normalizeOpenAIResponse(response, toolNameAliases) {
75
112
  const root = providerRecord(response, 'OpenAI response');
76
113
  if (!Array.isArray(root.choices) || root.choices.length === 0)
77
114
  throw providerResponseError('OpenAI response must contain at least one choice');
78
115
  const choice = providerRecord(root.choices[0], 'OpenAI choice');
79
116
  const message = providerRecord(choice.message, 'OpenAI message');
80
- const toolCalls = message.tool_calls === undefined ? [] : normalizeOpenAIToolCalls(message.tool_calls);
117
+ const toolCalls = message.tool_calls === undefined ? [] : normalizeOpenAIToolCalls(message.tool_calls, toolNameAliases);
81
118
  const refusal = message.refusal === undefined ? undefined : requiredProviderString(message.refusal, 'OpenAI refusal');
82
119
  const text = providerText(message.content, 'OpenAI message content');
83
120
  const finishReason = normalizeOpenAIFinishReason(choice.finish_reason, refusal, toolCalls.length > 0);
@@ -156,13 +193,14 @@ function normalizeCost(raw, provider) {
156
193
  throw providerResponseError(`${provider} pricing version must be a string`);
157
194
  return { amount: value.amount, currency: value.currency, source: 'reported', ...(value.pricing_version === undefined ? {} : { pricingVersion: value.pricing_version }) };
158
195
  }
159
- function normalizeOpenAIToolCalls(raw) {
196
+ function normalizeOpenAIToolCalls(raw, toolNameAliases) {
160
197
  if (!Array.isArray(raw))
161
198
  throw providerResponseError('OpenAI tool_calls must be an array');
162
199
  return raw.map((call, index) => {
163
200
  const value = providerRecord(call, 'OpenAI tool call');
164
201
  const fn = providerRecord(value.function, 'OpenAI tool function');
165
- return { toolCallId: `pulse-tool-${index + 1}`, name: requiredProviderString(fn.name, 'OpenAI tool name'), input: parseJson(fn.arguments) };
202
+ const providerName = requiredProviderString(fn.name, 'OpenAI tool name');
203
+ return { toolCallId: `pulse-tool-${index + 1}`, name: toolNameAliases?.get(providerName) ?? providerName, input: parseJson(fn.arguments) };
166
204
  });
167
205
  }
168
206
  function normalizeOpenAIFinishReason(raw, refusal, hasTools) {
@@ -1,4 +1,4 @@
1
- import { consumeProviderSse, normalizeOpenAIResponse, parseProviderJson, providerHttpError, providerNetworkError } from './normalize.js';
1
+ import { consumeProviderSse, normalizeOpenAIResponse, parseProviderJson, providerHttpErrorFromResponse, providerNetworkError } from './normalize.js';
2
2
  export class OpenAICompatibleAdapter {
3
3
  id;
4
4
  config;
@@ -11,14 +11,14 @@ export class OpenAICompatibleAdapter {
11
11
  }
12
12
  async executeAttempt(params) {
13
13
  const streaming = params.onObservation !== undefined;
14
- const tools = toolDefinitions(params.request);
14
+ const { definitions: tools, aliases: toolNameAliases } = toolDefinitions(params.request);
15
15
  const body = { ...(params.model ?? this.config.defaultModel ? { model: params.model ?? this.config.defaultModel } : {}), ...((params.maxOutputTokens ?? this.config.maxOutputTokens) === undefined ? {} : { max_tokens: params.maxOutputTokens ?? this.config.maxOutputTokens }), messages: toMessages(params.request), ...(tools.length ? { tools, ...(this.config.toolChoice === undefined ? {} : { tool_choice: this.config.toolChoice }) } : {}), ...(params.outputSchema === undefined ? {} : { response_format: { type: 'json_schema', json_schema: { name: 'pulse_output', strict: true, schema: params.outputSchema } } }), ...(streaming ? { stream: true, stream_options: { include_usage: true } } : {}) };
16
16
  try {
17
17
  const response = await fetch(`${this.baseURL.replace(/\/$/, '')}/chat/completions`, { method: 'POST', signal: params.signal, headers: { 'content-type': 'application/json', ...(this.config.apiKey ? { authorization: `Bearer ${this.config.apiKey}` } : {}), ...(this.config.extraHeaders ?? {}) }, body: JSON.stringify(body) });
18
18
  if (!response.ok)
19
- throw providerHttpError(response.status);
19
+ throw await providerHttpErrorFromResponse(response);
20
20
  if (!streaming || !response.headers.get('content-type')?.includes('text/event-stream'))
21
- return normalizeOpenAIResponse(await parseProviderJson(response));
21
+ return normalizeOpenAIResponse(await parseProviderJson(response), toolNameAliases);
22
22
  const events = await consumeProviderSse(response);
23
23
  const content = [];
24
24
  const refusals = [];
@@ -55,7 +55,7 @@ export class OpenAICompatibleAdapter {
55
55
  if (event.data.usage !== undefined)
56
56
  usage = event.data.usage;
57
57
  }
58
- return normalizeOpenAIResponse({ choices: [{ message: { content: content.join('') || null, ...(refusals.length ? { refusal: refusals.join('') } : {}), ...(toolCalls.size ? { tool_calls: [...toolCalls.entries()].sort(([left], [right]) => left - right).map(([, call]) => ({ id: call.id, function: { name: call.name, arguments: call.arguments } })) } : {}) }, finish_reason: finishReason ?? 'stop' }], ...(usage === undefined ? {} : { usage }) });
58
+ return normalizeOpenAIResponse({ choices: [{ message: { content: content.join('') || null, ...(refusals.length ? { refusal: refusals.join('') } : {}), ...(toolCalls.size ? { tool_calls: [...toolCalls.entries()].sort(([left], [right]) => left - right).map(([, call]) => ({ id: call.id, function: { name: call.name, arguments: call.arguments } })) } : {}) }, finish_reason: finishReason ?? 'stop' }], ...(usage === undefined ? {} : { usage }) }, toolNameAliases);
59
59
  }
60
60
  catch (cause) {
61
61
  if (params.signal.aborted)
@@ -83,5 +83,18 @@ function toolDefinitions(request) {
83
83
  const block = request.blocks.find((candidate) => candidate.kind === 'tools');
84
84
  const content = block?.content;
85
85
  const values = Array.isArray(content) ? content : content && typeof content === 'object' && !Array.isArray(content) && Array.isArray(content.tools) ? content.tools : [];
86
- return values.filter((value) => typeof value === 'object' && value !== null && !Array.isArray(value) && typeof value.name === 'string').map((value) => ({ type: 'function', function: { name: value.name, ...(typeof value.description === 'string' ? { description: value.description } : {}), parameters: (value.inputSchema ?? value.parameters ?? {}) } }));
86
+ const aliases = new Map();
87
+ const used = new Set();
88
+ const definitions = values.filter((value) => typeof value === 'object' && value !== null && !Array.isArray(value) && typeof value.name === 'string').map((value, index) => {
89
+ const originalName = value.name;
90
+ const baseName = originalName.replace(/[^a-zA-Z0-9_-]/g, '_') || `tool_${index + 1}`;
91
+ let providerName = baseName;
92
+ let suffix = 2;
93
+ while (used.has(providerName))
94
+ providerName = `${baseName}_${suffix++}`;
95
+ used.add(providerName);
96
+ aliases.set(providerName, originalName);
97
+ return { type: 'function', function: { name: providerName, ...(typeof value.description === 'string' ? { description: value.description } : {}), parameters: (value.inputSchema ?? value.parameters ?? {}) } };
98
+ });
99
+ return { definitions, aliases };
87
100
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hunterzhu/pulse-adapters",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/zhuhengtan/Pulse"
@@ -16,7 +16,7 @@
16
16
  "registry": "https://registry.npmjs.org"
17
17
  },
18
18
  "dependencies": {
19
- "@hunterzhu/pulse-runtime": "0.1.3",
20
- "@hunterzhu/pulse-tool-sdk": "0.1.3"
19
+ "@hunterzhu/pulse-runtime": "0.1.4",
20
+ "@hunterzhu/pulse-tool-sdk": "0.1.4"
21
21
  }
22
22
  }