@hunterzhu/pulse-adapters 0.1.3 → 0.1.5

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,13 @@
1
- import { consumeProviderSse, normalizeAnthropicResponse, parseProviderJson, providerHttpError, providerNetworkError, providerResponseError } from './normalize.js';
1
+ import { consumeProviderSse, normalizeAnthropicResponse, parseProviderJson, providerHttpErrorFromResponse, providerNetworkError, providerResponseError } from './normalize.js';
2
+ function anthropicThinking(effort, maxTokens) {
3
+ if (!effort)
4
+ return {};
5
+ const requested = effort === 'high' ? 8_000 : effort === 'medium' ? 2_048 : 1_024;
6
+ const budget = Math.min(requested, maxTokens - 1_024);
7
+ if (budget < 1_024 || budget >= maxTokens)
8
+ return {};
9
+ return { thinking: { type: 'enabled', budget_tokens: budget } };
10
+ }
2
11
  export class AnthropicAdapter {
3
12
  id;
4
13
  config;
@@ -12,11 +21,12 @@ export class AnthropicAdapter {
12
21
  const messages = [{ role: 'user', content: params.request.blocks.filter((block) => !['system', 'policy', 'tools'].includes(block.kind)).map((block) => ({ type: 'text', text: typeof block.content === 'string' ? block.content : JSON.stringify(block.content) })) }];
13
22
  const streaming = params.onObservation !== undefined;
14
23
  const tools = toolDefinitions(params.request);
15
- const body = { ...(params.model ?? this.config.defaultModel ? { model: params.model ?? this.config.defaultModel } : {}), max_tokens: params.maxOutputTokens ?? this.config.maxOutputTokens ?? 4096, ...(system ? { system } : {}), messages, ...(tools.length ? { tools, ...(this.config.toolChoice === undefined ? {} : { tool_choice: anthropicToolChoice(this.config.toolChoice) }) } : {}), ...(params.outputSchema === undefined ? {} : { output_format: { type: 'json_schema', schema: params.outputSchema } }), ...(streaming ? { stream: true } : {}) };
24
+ const maxTokens = params.maxOutputTokens ?? this.config.maxOutputTokens ?? 4096;
25
+ const body = { ...(params.model ?? this.config.defaultModel ? { model: params.model ?? this.config.defaultModel } : {}), max_tokens: maxTokens, ...anthropicThinking(this.config.reasoningEffort, maxTokens), ...(system ? { system } : {}), messages, ...(tools.length ? { tools, ...(this.config.toolChoice === undefined ? {} : { tool_choice: anthropicToolChoice(this.config.toolChoice) }) } : {}), ...(params.outputSchema === undefined ? {} : { output_format: { type: 'json_schema', schema: params.outputSchema } }), ...(streaming ? { stream: true } : {}) };
16
26
  try {
17
27
  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
28
  if (!response.ok)
19
- throw providerHttpError(response.status);
29
+ throw await providerHttpErrorFromResponse(response);
20
30
  if (!streaming || !response.headers.get('content-type')?.includes('text/event-stream'))
21
31
  return normalizeAnthropicResponse(await parseProviderJson(response));
22
32
  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,10 +1,11 @@
1
- import type { JsonValue, LLMRequestProjection } from '@hunterzhu/pulse-runtime';
1
+ import type { JsonValue, LLMRequestProjection, LLMResult } from '@hunterzhu/pulse-runtime';
2
2
  import type { ProviderAdapter, ProviderPresetConfig } from './types.js';
3
3
  export declare class OpenAICompatibleAdapter implements ProviderAdapter {
4
4
  readonly id: string;
5
5
  private readonly config;
6
6
  readonly name = "OpenAI Compatible";
7
7
  private readonly baseURL;
8
+ private reasoningUnsupported;
8
9
  constructor(id: string, config: ProviderPresetConfig);
9
10
  executeAttempt(params: {
10
11
  request: LLMRequestProjection;
@@ -13,7 +14,7 @@ export declare class OpenAICompatibleAdapter implements ProviderAdapter {
13
14
  model?: string;
14
15
  outputSchema?: JsonValue;
15
16
  maxOutputTokens?: number;
16
- }): Promise<import("@hunterzhu/pulse-runtime").LLMResult>;
17
+ }): Promise<LLMResult>;
17
18
  }
18
19
  export declare function toOpenAIMessages(request: LLMRequestProjection): Array<{
19
20
  role: 'system' | 'user' | 'assistant';
@@ -1,9 +1,18 @@
1
- import { consumeProviderSse, normalizeOpenAIResponse, parseProviderJson, providerHttpError, providerNetworkError } from './normalize.js';
1
+ import { consumeProviderSse, normalizeOpenAIResponse, parseProviderJson, providerHttpErrorFromResponse, providerNetworkError } from './normalize.js';
2
+ function reasoningParameterRejected(cause) {
3
+ if (!(cause instanceof Error))
4
+ return false;
5
+ const code = cause.code;
6
+ if (code !== 'PROVIDER_HTTP_400')
7
+ return false;
8
+ return /reasoning_effort|unknown parameter|unrecognized (?:request )?argument|extra fields? not permitted/i.test(cause.message);
9
+ }
2
10
  export class OpenAICompatibleAdapter {
3
11
  id;
4
12
  config;
5
13
  name = 'OpenAI Compatible';
6
14
  baseURL;
15
+ reasoningUnsupported = false;
7
16
  constructor(id, config) {
8
17
  this.id = id;
9
18
  this.config = config;
@@ -11,14 +20,15 @@ export class OpenAICompatibleAdapter {
11
20
  }
12
21
  async executeAttempt(params) {
13
22
  const streaming = params.onObservation !== undefined;
14
- const tools = toolDefinitions(params.request);
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 } } : {}) };
23
+ const { definitions: tools, aliases: toolNameAliases } = toolDefinitions(params.request);
24
+ const includeReasoning = Boolean(this.config.reasoningEffort) && !this.reasoningUnsupported;
25
+ 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 }), ...(includeReasoning ? { reasoning_effort: this.config.reasoningEffort } : {}), 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
26
  try {
17
27
  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
28
  if (!response.ok)
19
- throw providerHttpError(response.status);
29
+ throw await providerHttpErrorFromResponse(response);
20
30
  if (!streaming || !response.headers.get('content-type')?.includes('text/event-stream'))
21
- return normalizeOpenAIResponse(await parseProviderJson(response));
31
+ return normalizeOpenAIResponse(await parseProviderJson(response), toolNameAliases);
22
32
  const events = await consumeProviderSse(response);
23
33
  const content = [];
24
34
  const refusals = [];
@@ -55,11 +65,15 @@ export class OpenAICompatibleAdapter {
55
65
  if (event.data.usage !== undefined)
56
66
  usage = event.data.usage;
57
67
  }
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 }) });
68
+ 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
69
  }
60
70
  catch (cause) {
61
71
  if (params.signal.aborted)
62
72
  throw Object.assign(new Error('Provider request was cancelled.'), { code: 'PROVIDER_REQUEST_CANCELLED', retryable: false, cause });
73
+ if (includeReasoning && reasoningParameterRejected(cause)) {
74
+ this.reasoningUnsupported = true;
75
+ return this.executeAttempt(params);
76
+ }
63
77
  if (cause instanceof Error && 'code' in cause && typeof cause.code === 'string' && 'retryable' in cause && typeof cause.retryable === 'boolean')
64
78
  throw cause;
65
79
  throw providerNetworkError(cause);
@@ -83,5 +97,18 @@ function toolDefinitions(request) {
83
97
  const block = request.blocks.find((candidate) => candidate.kind === 'tools');
84
98
  const content = block?.content;
85
99
  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 ?? {}) } }));
100
+ const aliases = new Map();
101
+ const used = new Set();
102
+ const definitions = values.filter((value) => typeof value === 'object' && value !== null && !Array.isArray(value) && typeof value.name === 'string').map((value, index) => {
103
+ const originalName = value.name;
104
+ const baseName = originalName.replace(/[^a-zA-Z0-9_-]/g, '_') || `tool_${index + 1}`;
105
+ let providerName = baseName;
106
+ let suffix = 2;
107
+ while (used.has(providerName))
108
+ providerName = `${baseName}_${suffix++}`;
109
+ used.add(providerName);
110
+ aliases.set(providerName, originalName);
111
+ return { type: 'function', function: { name: providerName, ...(typeof value.description === 'string' ? { description: value.description } : {}), parameters: (value.inputSchema ?? value.parameters ?? {}) } };
112
+ });
113
+ return { definitions, aliases };
87
114
  }
@@ -25,4 +25,5 @@ export interface ProviderPresetConfig {
25
25
  maxOutputTokens?: number;
26
26
  toolChoice?: ProviderToolChoice;
27
27
  extraHeaders?: Record<string, string>;
28
+ reasoningEffort?: 'low' | 'medium' | 'high' | undefined;
28
29
  }
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.5",
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.5",
20
+ "@hunterzhu/pulse-tool-sdk": "0.1.5"
21
21
  }
22
22
  }