@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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
+
"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.
|
|
20
|
-
"@hunterzhu/pulse-tool-sdk": "0.1.
|
|
19
|
+
"@hunterzhu/pulse-runtime": "0.1.4",
|
|
20
|
+
"@hunterzhu/pulse-tool-sdk": "0.1.4"
|
|
21
21
|
}
|
|
22
22
|
}
|