@hunterzhu/pulse-adapters 0.2.1 → 0.3.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.
|
@@ -29,6 +29,10 @@ export class AnthropicAdapter {
|
|
|
29
29
|
for (const block of params.request.blocks) {
|
|
30
30
|
if (block.kind === 'system' || block.kind === 'policy' || block.kind === 'tools')
|
|
31
31
|
continue;
|
|
32
|
+
// Provider-native tool history is currently projected only by compatible
|
|
33
|
+
// OpenAI adapters. Keep Anthropic on the legacy history projection.
|
|
34
|
+
if (block.kind === 'provider_history')
|
|
35
|
+
continue;
|
|
32
36
|
if (block.kind === 'conversation' && Array.isArray(block.content)) {
|
|
33
37
|
for (const item of block.content) {
|
|
34
38
|
if (!item || typeof item !== 'object' || Array.isArray(item))
|
|
@@ -23,5 +23,5 @@ export declare function providerResponseError(detail: string): Error & {
|
|
|
23
23
|
export declare function parseProviderJson(response: Response): Promise<unknown>;
|
|
24
24
|
/** Read provider SSE frames without treating incomplete tool arguments as executable input. */
|
|
25
25
|
export declare function consumeProviderSse(response: Response, onEvent?: (event: ProviderSseEvent) => void): Promise<ProviderSseEvent[]>;
|
|
26
|
-
export declare function normalizeOpenAIResponse(response: any, toolNameAliases?: ReadonlyMap<string, string
|
|
26
|
+
export declare function normalizeOpenAIResponse(response: any, toolNameAliases?: ReadonlyMap<string, string>, provider?: string): LLMResult;
|
|
27
27
|
export declare function normalizeAnthropicResponse(response: any): LLMResult;
|
|
@@ -110,7 +110,7 @@ export async function consumeProviderSse(response, onEvent) {
|
|
|
110
110
|
flush();
|
|
111
111
|
return events;
|
|
112
112
|
}
|
|
113
|
-
export function normalizeOpenAIResponse(response, toolNameAliases) {
|
|
113
|
+
export function normalizeOpenAIResponse(response, toolNameAliases, provider = 'openai') {
|
|
114
114
|
const root = providerRecord(response, 'OpenAI response');
|
|
115
115
|
if (!Array.isArray(root.choices) || root.choices.length === 0)
|
|
116
116
|
throw providerResponseError('OpenAI response must contain at least one choice');
|
|
@@ -122,8 +122,10 @@ export function normalizeOpenAIResponse(response, toolNameAliases) {
|
|
|
122
122
|
const finishReason = normalizeOpenAIFinishReason(choice.finish_reason, refusal, toolCalls.length > 0);
|
|
123
123
|
const rawUsage = root.usage === undefined ? undefined : providerRecord(root.usage, 'OpenAI usage');
|
|
124
124
|
const promptDetails = rawUsage?.prompt_tokens_details === undefined ? undefined : providerRecord(rawUsage.prompt_tokens_details, 'OpenAI prompt token details');
|
|
125
|
-
const usage = normalizeUsage(rawUsage === undefined ? undefined : { ...rawUsage, cached_tokens: rawUsage.cached_tokens ?? promptDetails?.cached_tokens ?? rawUsage.cache_read_input_tokens }, { input: 'prompt_tokens', output: 'completion_tokens', cached: 'cached_tokens' }, 'OpenAI');
|
|
126
|
-
|
|
125
|
+
const usage = normalizeUsage(rawUsage === undefined ? undefined : { ...rawUsage, cached_tokens: rawUsage.cached_tokens ?? promptDetails?.cached_tokens ?? rawUsage.cache_read_input_tokens, reasoning_tokens: rawUsage.completion_tokens_details?.reasoning_tokens ?? rawUsage.reasoning_tokens }, { input: 'prompt_tokens', output: 'completion_tokens', cached: 'cached_tokens', reasoning: 'reasoning_tokens' }, 'OpenAI');
|
|
126
|
+
const reasoningContent = typeof message.reasoning_content === 'string' ? message.reasoning_content : undefined;
|
|
127
|
+
const measuredUsage = usage === undefined ? undefined : { ...usage, visibleOutputChars: text.length };
|
|
128
|
+
return { text, ...(parseStructured(text) === undefined ? {} : { structured: parseStructured(text) }), toolCalls, ...(refusal === undefined ? {} : { refusal }), finishReason, ...(measuredUsage === undefined ? {} : { usage: measuredUsage }), ...(provider !== 'deepseek' || reasoningContent === undefined ? {} : { providerContinuation: { provider, reasoningContent } }) };
|
|
127
129
|
}
|
|
128
130
|
export function normalizeAnthropicResponse(response) {
|
|
129
131
|
const root = providerRecord(response, 'Anthropic response');
|
|
@@ -140,7 +142,8 @@ export function normalizeAnthropicResponse(response) {
|
|
|
140
142
|
const refusal = refusalBlock?.text;
|
|
141
143
|
const finishReason = normalizeAnthropicFinishReason(root.stop_reason, refusal, toolCalls.length > 0);
|
|
142
144
|
const usage = normalizeUsage(root.usage, { input: 'input_tokens', output: 'output_tokens', cached: 'cache_read_input_tokens' }, 'Anthropic');
|
|
143
|
-
|
|
145
|
+
const measuredUsage = usage === undefined ? undefined : { ...usage, visibleOutputChars: text.length };
|
|
146
|
+
return { text, ...(parseStructured(text) === undefined ? {} : { structured: parseStructured(text) }), toolCalls, ...(refusal === undefined ? {} : { refusal }), finishReason, ...(measuredUsage === undefined ? {} : { usage: measuredUsage }) };
|
|
144
147
|
}
|
|
145
148
|
function providerRecord(value, label) {
|
|
146
149
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
@@ -180,10 +183,11 @@ function normalizeUsage(raw, fields, provider) {
|
|
|
180
183
|
const inputTokens = providerMetric(value[fields.input], `${provider} input tokens`);
|
|
181
184
|
const outputTokens = providerMetric(value[fields.output], `${provider} output tokens`);
|
|
182
185
|
const cachedInputTokens = providerMetric(value[fields.cached], `${provider} cached input tokens`);
|
|
186
|
+
const reasoningTokens = fields.reasoning === undefined ? undefined : providerMetric(value[fields.reasoning], `${provider} reasoning tokens`);
|
|
183
187
|
if (inputTokens !== undefined && cachedInputTokens !== undefined && cachedInputTokens > inputTokens)
|
|
184
188
|
throw providerResponseError(`${provider} cached input tokens exceed input tokens`);
|
|
185
189
|
const cost = value.cost === undefined ? undefined : normalizeCost(value.cost, provider);
|
|
186
|
-
return { ...(inputTokens === undefined ? {} : { inputTokens }), ...(outputTokens === undefined ? {} : { outputTokens }), ...(cachedInputTokens === undefined ? {} : { cachedInputTokens }), ...(inputTokens !== undefined && cachedInputTokens !== undefined ? { uncachedInputTokens: inputTokens - cachedInputTokens } : {}), ...(cost === undefined ? {} : { cost }) };
|
|
190
|
+
return { ...(inputTokens === undefined ? {} : { inputTokens }), ...(outputTokens === undefined ? {} : { outputTokens }), ...(cachedInputTokens === undefined ? {} : { cachedInputTokens }), ...(reasoningTokens === undefined ? {} : { reasoningTokens }), ...(inputTokens !== undefined && cachedInputTokens !== undefined ? { uncachedInputTokens: inputTokens - cachedInputTokens } : {}), ...(cost === undefined ? {} : { cost }) };
|
|
187
191
|
}
|
|
188
192
|
function normalizeCost(raw, provider) {
|
|
189
193
|
const value = providerRecord(raw, `${provider} cost`);
|
|
@@ -202,7 +206,10 @@ function normalizeOpenAIToolCalls(raw, toolNameAliases) {
|
|
|
202
206
|
const value = providerRecord(call, 'OpenAI tool call');
|
|
203
207
|
const fn = providerRecord(value.function, 'OpenAI tool function');
|
|
204
208
|
const providerName = requiredProviderString(fn.name, 'OpenAI tool name');
|
|
205
|
-
|
|
209
|
+
const providerToolCallId = typeof value.id === 'string' && value.id.length > 0 ? value.id : undefined;
|
|
210
|
+
if (typeof fn.arguments !== 'string')
|
|
211
|
+
throw providerResponseError('OpenAI tool arguments must be a JSON string');
|
|
212
|
+
return { toolCallId: `pulse-tool-${index + 1}`, ...(providerToolCallId === undefined ? {} : { providerToolCallId }), providerToolName: providerName, providerToolArguments: fn.arguments, name: toolNameAliases?.get(providerName) ?? providerName, input: parseJson(fn.arguments) };
|
|
206
213
|
});
|
|
207
214
|
}
|
|
208
215
|
function normalizeOpenAIFinishReason(raw, refusal, hasTools) {
|
|
@@ -16,8 +16,4 @@ export declare class OpenAICompatibleAdapter implements ProviderAdapter {
|
|
|
16
16
|
maxOutputTokens?: number;
|
|
17
17
|
}): Promise<LLMResult>;
|
|
18
18
|
}
|
|
19
|
-
export declare function toOpenAIMessages(request: LLMRequestProjection): Array<
|
|
20
|
-
role: 'system' | 'user' | 'assistant';
|
|
21
|
-
content: string;
|
|
22
|
-
name?: string;
|
|
23
|
-
}>;
|
|
19
|
+
export declare function toOpenAIMessages(request: LLMRequestProjection, provider?: string): Array<Record<string, unknown>>;
|
|
@@ -22,7 +22,7 @@ export class OpenAICompatibleAdapter {
|
|
|
22
22
|
const streaming = params.onObservation !== undefined;
|
|
23
23
|
const { definitions: tools, aliases: toolNameAliases } = toolDefinitions(params.request);
|
|
24
24
|
const includeReasoning = Boolean(this.config.reasoningEffort) && !this.reasoningUnsupported;
|
|
25
|
-
const messages = toMessages(params.request);
|
|
25
|
+
const messages = toMessages(params.request, this.config.provider);
|
|
26
26
|
const deepSeekJsonMode = params.outputSchema !== undefined && this.config.provider === 'deepseek';
|
|
27
27
|
if (deepSeekJsonMode)
|
|
28
28
|
messages.unshift({ role: 'system', content: `Return only a JSON object matching this schema. The application will validate the result:\n${JSON.stringify(params.outputSchema)}` });
|
|
@@ -35,7 +35,7 @@ export class OpenAICompatibleAdapter {
|
|
|
35
35
|
if (!response.ok)
|
|
36
36
|
throw await providerHttpErrorFromResponse(response);
|
|
37
37
|
if (!streaming || !response.headers.get('content-type')?.includes('text/event-stream'))
|
|
38
|
-
return normalizeOpenAIResponse(await parseProviderJson(response), toolNameAliases);
|
|
38
|
+
return normalizeOpenAIResponse(await parseProviderJson(response), toolNameAliases, this.config.provider);
|
|
39
39
|
const events = await consumeProviderSse(response, (event) => {
|
|
40
40
|
const delta = event.data?.choices?.[0]?.delta;
|
|
41
41
|
if (typeof delta?.content === 'string')
|
|
@@ -44,6 +44,7 @@ export class OpenAICompatibleAdapter {
|
|
|
44
44
|
params.onObservation?.(delta.refusal);
|
|
45
45
|
});
|
|
46
46
|
const content = [];
|
|
47
|
+
const reasoningContent = [];
|
|
47
48
|
const refusals = [];
|
|
48
49
|
const toolCalls = new Map();
|
|
49
50
|
let finishReason;
|
|
@@ -56,6 +57,8 @@ export class OpenAICompatibleAdapter {
|
|
|
56
57
|
if (typeof delta?.content === 'string') {
|
|
57
58
|
content.push(delta.content);
|
|
58
59
|
}
|
|
60
|
+
if (typeof delta?.reasoning_content === 'string')
|
|
61
|
+
reasoningContent.push(delta.reasoning_content);
|
|
59
62
|
if (typeof delta?.refusal === 'string') {
|
|
60
63
|
refusals.push(delta.refusal);
|
|
61
64
|
}
|
|
@@ -67,7 +70,7 @@ export class OpenAICompatibleAdapter {
|
|
|
67
70
|
if (event.data.usage !== undefined)
|
|
68
71
|
usage = event.data.usage;
|
|
69
72
|
}
|
|
70
|
-
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);
|
|
73
|
+
return normalizeOpenAIResponse({ choices: [{ message: { content: content.join('') || null, ...(reasoningContent.length ? { reasoning_content: reasoningContent.join('') } : {}), ...(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, this.config.provider);
|
|
71
74
|
}
|
|
72
75
|
catch (cause) {
|
|
73
76
|
if (params.signal.aborted)
|
|
@@ -82,7 +85,7 @@ export class OpenAICompatibleAdapter {
|
|
|
82
85
|
}
|
|
83
86
|
}
|
|
84
87
|
}
|
|
85
|
-
export function toOpenAIMessages(request) {
|
|
88
|
+
export function toOpenAIMessages(request, provider = 'openai') {
|
|
86
89
|
const messages = [];
|
|
87
90
|
for (const block of request.blocks) {
|
|
88
91
|
const content = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
|
|
@@ -102,15 +105,30 @@ export function toOpenAIMessages(request) {
|
|
|
102
105
|
messages.push({ role: 'system', content });
|
|
103
106
|
continue;
|
|
104
107
|
}
|
|
105
|
-
if (block.kind === 'history')
|
|
108
|
+
if (block.kind === 'history' && request.contextSpec.providerHistory !== undefined)
|
|
109
|
+
continue;
|
|
110
|
+
if (block.kind === 'provider_history' && request.contextSpec.providerHistory !== undefined) {
|
|
111
|
+
for (const item of request.contextSpec.providerHistory) {
|
|
112
|
+
if (item.role === 'user')
|
|
113
|
+
messages.push({ role: 'user', content: item.content });
|
|
114
|
+
else if (item.role === 'assistant')
|
|
115
|
+
messages.push({ role: 'assistant', content: item.content, ...(provider === 'deepseek' && item.reasoningContent !== undefined ? { reasoning_content: item.reasoningContent } : {}), ...(item.toolCalls === undefined ? {} : { tool_calls: item.toolCalls.map((call) => ({ id: call.id, type: 'function', function: { name: call.name, arguments: call.arguments } })) }) });
|
|
116
|
+
else
|
|
117
|
+
messages.push({ role: 'tool', tool_call_id: item.toolCallId, name: item.name, content: item.content });
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
else if (block.kind === 'history')
|
|
106
122
|
messages.push({ role: 'assistant', content });
|
|
123
|
+
else if (block.kind === 'provider_history')
|
|
124
|
+
continue;
|
|
107
125
|
else
|
|
108
126
|
messages.push({ role: 'user', name: block.kind, content });
|
|
109
127
|
}
|
|
110
128
|
return messages;
|
|
111
129
|
}
|
|
112
|
-
function toMessages(request) {
|
|
113
|
-
return toOpenAIMessages(request);
|
|
130
|
+
function toMessages(request, provider) {
|
|
131
|
+
return toOpenAIMessages(request, provider);
|
|
114
132
|
}
|
|
115
133
|
function nextToolIndex(toolCalls) {
|
|
116
134
|
let next = 0;
|
|
@@ -82,7 +82,7 @@ function toJson(value, seen = new Set()) {
|
|
|
82
82
|
function candidateMetadata(candidate, attempts, usage, slotWaitMs, routes) {
|
|
83
83
|
return { selected: { id: candidate.id, providerId: candidate.providerId }, routes, attempts: attempts.map((attempt) => {
|
|
84
84
|
const recorded = usage.get(attempt.attemptId);
|
|
85
|
-
const usageJson = recorded === undefined ? undefined : { ...(recorded.inputTokens === undefined ? {} : { inputTokens: recorded.inputTokens }), ...(recorded.outputTokens === undefined ? {} : { outputTokens: recorded.outputTokens }), ...(recorded.cachedInputTokens === undefined ? {} : { cachedInputTokens: recorded.cachedInputTokens }), ...(recorded.uncachedInputTokens === undefined ? {} : { uncachedInputTokens: recorded.uncachedInputTokens }), ...(recorded.latencyMs === undefined ? {} : { latencyMs: recorded.latencyMs }), ...(recorded.cost === undefined ? {} : { cost: recorded.cost }) };
|
|
85
|
+
const usageJson = recorded === undefined ? undefined : { ...(recorded.inputTokens === undefined ? {} : { inputTokens: recorded.inputTokens }), ...(recorded.outputTokens === undefined ? {} : { outputTokens: recorded.outputTokens }), ...(recorded.reasoningTokens === undefined ? {} : { reasoningTokens: recorded.reasoningTokens }), ...(recorded.visibleOutputChars === undefined ? {} : { visibleOutputChars: recorded.visibleOutputChars }), ...(recorded.cachedInputTokens === undefined ? {} : { cachedInputTokens: recorded.cachedInputTokens }), ...(recorded.uncachedInputTokens === undefined ? {} : { uncachedInputTokens: recorded.uncachedInputTokens }), ...(recorded.latencyMs === undefined ? {} : { latencyMs: recorded.latencyMs }), ...(recorded.cost === undefined ? {} : { cost: recorded.cost }) };
|
|
86
86
|
const waited = slotWaitMs.get(attempt.attemptId);
|
|
87
87
|
return { effectId: attempt.effectId, attemptId: attempt.attemptId, attemptNo: attempt.attemptNo, modelId: attempt.candidate.id, providerId: attempt.candidate.providerId, ...(waited === undefined ? {} : { slotWaitMs: waited }), ...(usageJson === undefined ? {} : { usage: usageJson }) };
|
|
88
88
|
}) };
|
|
@@ -163,10 +163,10 @@ export function createModelEffectExecutor(config) {
|
|
|
163
163
|
observations.push(observation);
|
|
164
164
|
};
|
|
165
165
|
const output = assignRuntimeToolCallIds(validateAdapterResult(await provider.executeAttempt({ request: projection, signal, model: attempt.candidate.id, ...(input.outputSchema === undefined ? {} : { outputSchema: input.outputSchema }), ...(typeof routeRequirements.maxOutputTokens === 'number' ? { maxOutputTokens: routeRequirements.maxOutputTokens } : {}), onObservation })), effect.id);
|
|
166
|
-
const measuredUsage = output.usage
|
|
166
|
+
const measuredUsage = { ...(output.usage ?? {}), visibleOutputChars: output.text.length, latencyMs: output.usage?.latencyMs ?? Math.max(0, Date.now() - startedAt), ...(output.usage?.uncachedInputTokens === undefined && output.usage?.inputTokens !== undefined && output.usage.cachedInputTokens !== undefined ? { uncachedInputTokens: Math.max(0, output.usage.inputTokens - output.usage.cachedInputTokens) } : {}) };
|
|
167
167
|
usage.set(attempt.attemptId, measuredUsage);
|
|
168
168
|
if (output.finishReason === 'length' && input.outputSchema !== undefined)
|
|
169
|
-
throw Object.assign(new OutputValidationError('adapter', 'OUTPUT_TRUNCATED', 'Model output reached its token limit;
|
|
169
|
+
throw Object.assign(new OutputValidationError('adapter', 'OUTPUT_TRUNCATED', 'Model output reached its token limit; incomplete output was rejected.', { outputTokens: output.usage?.outputTokens ?? null, reasoningTokens: output.usage?.reasoningTokens ?? null, visibleOutputChars: output.text.length, maxOutputTokens: routeRequirements.maxOutputTokens ?? config.requirements?.maxOutputTokens ?? null }), { retryable: false });
|
|
170
170
|
if (output.finishReason === 'refusal') {
|
|
171
171
|
recordFeedback('refused', 0);
|
|
172
172
|
throw new OutputValidationError('adapter', 'MODEL_REFUSAL', output.refusal ?? 'Provider refused the request.');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hunterzhu/pulse-adapters",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/zhuhengtan/Pulse"
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@anthropic-ai/sandbox-runtime": "0.0.77",
|
|
20
|
-
"@hunterzhu/pulse-runtime": "0.
|
|
21
|
-
"@hunterzhu/pulse-tool-sdk": "0.
|
|
20
|
+
"@hunterzhu/pulse-runtime": "0.3.0",
|
|
21
|
+
"@hunterzhu/pulse-tool-sdk": "0.3.0",
|
|
22
22
|
"exceljs": "4.4.0",
|
|
23
23
|
"pdfjs-dist": "4.10.38"
|
|
24
24
|
}
|