@hunterzhu/pulse-adapters 0.2.1 → 0.4.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>): LLMResult;
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
- return { text, ...(parseStructured(text) === undefined ? {} : { structured: parseStructured(text) }), toolCalls, ...(refusal === undefined ? {} : { refusal }), finishReason, ...(usage === undefined ? {} : { usage }) };
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
- return { text, ...(parseStructured(text) === undefined ? {} : { structured: parseStructured(text) }), toolCalls, ...(refusal === undefined ? {} : { refusal }), finishReason, ...(usage === undefined ? {} : { usage }) };
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
- return { toolCallId: `pulse-tool-${index + 1}`, name: toolNameAliases?.get(providerName) ?? providerName, input: parseJson(fn.arguments) };
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 === undefined ? { latencyMs: Math.max(0, Date.now() - startedAt) } : { ...output.usage, 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) } : {}) };
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; increase maxOutputTokens before retrying.'), { retryable: false });
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.2.1",
3
+ "version": "0.4.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.2.1",
21
- "@hunterzhu/pulse-tool-sdk": "0.2.1",
20
+ "@hunterzhu/pulse-runtime": "0.4.0",
21
+ "@hunterzhu/pulse-tool-sdk": "0.4.0",
22
22
  "exceljs": "4.4.0",
23
23
  "pdfjs-dist": "4.10.38"
24
24
  }