@hunterzhu/pulse-adapters 0.4.1 → 0.4.3

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.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './providers/canonical/index.js';
1
2
  export * from './providers/types.js';
2
3
  export * from './providers/normalize.js';
3
4
  export * from './providers/mock.js';
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './providers/canonical/index.js';
1
2
  export * from './providers/types.js';
2
3
  export * from './providers/normalize.js';
3
4
  export * from './providers/mock.js';
@@ -0,0 +1,19 @@
1
+ import type { ModelCapabilityProfile } from './types.js';
2
+ export declare const DEFAULT_OPENAI_PROFILE: ModelCapabilityProfile;
3
+ export declare const OPENAI_REASONING_PROFILE: ModelCapabilityProfile;
4
+ export declare const ANTHROPIC_THINKING_PROFILE: ModelCapabilityProfile;
5
+ export declare const DEFAULT_ANTHROPIC_PROFILE: ModelCapabilityProfile;
6
+ export declare const DEEPSEEK_PROFILE: ModelCapabilityProfile;
7
+ export declare const DEFAULT_COMPATIBLE_PROFILE: ModelCapabilityProfile;
8
+ declare class CapabilityRegistry {
9
+ private readonly customProfiles;
10
+ registerProfile(pattern: string, profile: Partial<ModelCapabilityProfile>): void;
11
+ unregisterProfile(pattern: string): void;
12
+ resolve(model?: string, provider?: string, override?: Partial<ModelCapabilityProfile>): ModelCapabilityProfile;
13
+ private matchCustom;
14
+ private resolveBase;
15
+ private mergeProfile;
16
+ }
17
+ export declare const capabilityRegistry: CapabilityRegistry;
18
+ export declare function resolveModelCapability(model?: string, provider?: string, override?: Partial<ModelCapabilityProfile>): ModelCapabilityProfile;
19
+ export {};
@@ -0,0 +1,164 @@
1
+ export const DEFAULT_OPENAI_PROFILE = {
2
+ id: 'openai-default',
3
+ provider: 'openai',
4
+ structuredOutput: 'native_strict',
5
+ reasoning: 'effort_param',
6
+ constraints: {
7
+ disallowTemperatureWithReasoning: false,
8
+ maxTokensParamName: 'max_tokens',
9
+ supportsSystemMessage: true,
10
+ supportsParallelTools: true,
11
+ },
12
+ };
13
+ export const OPENAI_REASONING_PROFILE = {
14
+ id: 'openai-reasoning',
15
+ provider: 'openai',
16
+ structuredOutput: 'native_strict',
17
+ reasoning: 'effort_param',
18
+ constraints: {
19
+ disallowTemperatureWithReasoning: true,
20
+ defaultTemperatureWithReasoning: 1.0,
21
+ maxTokensParamName: 'max_completion_tokens',
22
+ supportsSystemMessage: true,
23
+ supportsParallelTools: true,
24
+ },
25
+ };
26
+ export const ANTHROPIC_THINKING_PROFILE = {
27
+ id: 'anthropic-thinking',
28
+ provider: 'anthropic',
29
+ structuredOutput: 'native_loose',
30
+ reasoning: 'budget_tokens',
31
+ constraints: {
32
+ disallowTemperatureWithReasoning: true,
33
+ defaultTemperatureWithReasoning: 1.0,
34
+ maxTokensParamName: 'max_tokens',
35
+ supportsSystemMessage: true,
36
+ supportsParallelTools: true,
37
+ },
38
+ };
39
+ export const DEFAULT_ANTHROPIC_PROFILE = {
40
+ id: 'anthropic-default',
41
+ provider: 'anthropic',
42
+ structuredOutput: 'native_loose',
43
+ reasoning: 'unsupported',
44
+ constraints: {
45
+ disallowTemperatureWithReasoning: false,
46
+ maxTokensParamName: 'max_tokens',
47
+ supportsSystemMessage: true,
48
+ supportsParallelTools: true,
49
+ },
50
+ };
51
+ export const DEEPSEEK_PROFILE = {
52
+ id: 'deepseek-default',
53
+ provider: 'deepseek',
54
+ structuredOutput: 'json_object_only',
55
+ reasoning: 'effort_param',
56
+ constraints: {
57
+ disallowTemperatureWithReasoning: false,
58
+ maxTokensParamName: 'max_tokens',
59
+ supportsSystemMessage: true,
60
+ supportsParallelTools: true,
61
+ },
62
+ };
63
+ export const DEFAULT_COMPATIBLE_PROFILE = {
64
+ id: 'openai-compatible-default',
65
+ provider: 'openai-compatible',
66
+ structuredOutput: 'native_strict',
67
+ reasoning: 'effort_param',
68
+ constraints: {
69
+ disallowTemperatureWithReasoning: false,
70
+ maxTokensParamName: 'max_tokens',
71
+ supportsSystemMessage: true,
72
+ supportsParallelTools: true,
73
+ },
74
+ };
75
+ class CapabilityRegistry {
76
+ customProfiles = new Map();
77
+ registerProfile(pattern, profile) {
78
+ const key = pattern.trim().toLowerCase();
79
+ if (!key)
80
+ return;
81
+ this.customProfiles.set(key, profile);
82
+ }
83
+ unregisterProfile(pattern) {
84
+ this.customProfiles.delete(pattern.trim().toLowerCase());
85
+ }
86
+ resolve(model, provider, override) {
87
+ const normProvider = (provider ?? 'openai').toLowerCase();
88
+ const normModel = (model ?? '').toLowerCase();
89
+ const custom = this.matchCustom(normProvider, normModel);
90
+ const base = this.resolveBase(normModel, normProvider);
91
+ return this.mergeProfile(base, custom, override);
92
+ }
93
+ matchCustom(normProvider, normModel) {
94
+ let best;
95
+ for (const [pattern, profile] of this.customProfiles) {
96
+ if (!profileMatches(normProvider, normModel, pattern))
97
+ continue;
98
+ if (!best || pattern.length > best.length)
99
+ best = { length: pattern.length, profile };
100
+ }
101
+ return best?.profile;
102
+ }
103
+ resolveBase(normModel, normProvider) {
104
+ if (normProvider === 'anthropic') {
105
+ if (normModel.includes('claude-3-7') || normModel.includes('thinking')) {
106
+ return { ...ANTHROPIC_THINKING_PROFILE, provider: normProvider, id: `anthropic:${normModel}` };
107
+ }
108
+ return { ...DEFAULT_ANTHROPIC_PROFILE, provider: normProvider, id: `anthropic:${normModel || 'default'}` };
109
+ }
110
+ // DeepSeek json_object mode follows the protocol id. A model name that
111
+ // merely contains "deepseek" keeps the caller's structured-output contract.
112
+ if (normProvider === 'deepseek') {
113
+ return { ...DEEPSEEK_PROFILE, provider: normProvider, id: `deepseek:${normModel || 'default'}` };
114
+ }
115
+ if (normProvider === 'openai' || normProvider === 'openai-compatible') {
116
+ if (isOpenAIReasoningModel(normModel)) {
117
+ return { ...OPENAI_REASONING_PROFILE, provider: normProvider, id: `${normProvider}:${normModel}` };
118
+ }
119
+ return { ...DEFAULT_OPENAI_PROFILE, provider: normProvider, id: `${normProvider}:${normModel || 'default'}` };
120
+ }
121
+ return { ...DEFAULT_COMPATIBLE_PROFILE, provider: normProvider, id: `${normProvider}:${normModel || 'default'}` };
122
+ }
123
+ mergeProfile(base, ...overrides) {
124
+ let result = { ...base, constraints: { ...base.constraints } };
125
+ for (const over of overrides) {
126
+ if (!over)
127
+ continue;
128
+ result = {
129
+ ...result,
130
+ ...over,
131
+ constraints: {
132
+ ...result.constraints,
133
+ ...(over.constraints ?? {}),
134
+ },
135
+ };
136
+ }
137
+ return result;
138
+ }
139
+ }
140
+ export const capabilityRegistry = new CapabilityRegistry();
141
+ export function resolveModelCapability(model, provider, override) {
142
+ return capabilityRegistry.resolve(model, provider, override);
143
+ }
144
+ /** o1/o3 and their suffixed ids. `o10` and `gpt-4o` do not match. */
145
+ function isOpenAIReasoningModel(model) {
146
+ return /^o[13](?:$|[-_.])/.test(model);
147
+ }
148
+ function profileMatches(provider, model, pattern) {
149
+ if (!pattern)
150
+ return false;
151
+ if (pattern === `${provider}:${model}`)
152
+ return true;
153
+ return modelMatchesPattern(model, pattern);
154
+ }
155
+ function modelMatchesPattern(model, pattern) {
156
+ if (!pattern || !model)
157
+ return false;
158
+ if (model === pattern)
159
+ return true;
160
+ if (!model.startsWith(pattern))
161
+ return false;
162
+ const next = model.charAt(pattern.length);
163
+ return next === '-' || next === '_' || next === '.' || next === '/' || next === ':';
164
+ }
@@ -0,0 +1,6 @@
1
+ export * from './types.js';
2
+ export * from './capabilities.js';
3
+ export * from './lowering.js';
4
+ export * from './polyfill.js';
5
+ export * from './mappers/openai.js';
6
+ export * from './mappers/anthropic.js';
@@ -0,0 +1,6 @@
1
+ export * from './types.js';
2
+ export * from './capabilities.js';
3
+ export * from './lowering.js';
4
+ export * from './polyfill.js';
5
+ export * from './mappers/openai.js';
6
+ export * from './mappers/anthropic.js';
@@ -0,0 +1,17 @@
1
+ import type { JsonValue, LLMRequestProjection } from '@hunterzhu/pulse-runtime';
2
+ import type { ProviderToolChoice } from '../types.js';
3
+ import type { CanonicalLLMRequest } from './types.js';
4
+ export interface LoweringParams {
5
+ request: LLMRequestProjection;
6
+ model?: string | undefined;
7
+ outputSchema?: JsonValue | undefined;
8
+ maxOutputTokens?: number | undefined;
9
+ reasoningEffort?: 'low' | 'medium' | 'high' | undefined;
10
+ toolChoice?: ProviderToolChoice | undefined;
11
+ stream?: boolean | undefined;
12
+ }
13
+ export interface LoweringResult {
14
+ canonical: CanonicalLLMRequest;
15
+ toolNameAliases: ReadonlyMap<string, string>;
16
+ }
17
+ export declare function lowerProjectionToCanonical(params: LoweringParams): LoweringResult;
@@ -0,0 +1,122 @@
1
+ export function lowerProjectionToCanonical(params) {
2
+ const { request } = params;
3
+ const { definitions: tools, aliases: toolNameAliases } = extractCanonicalTools(request);
4
+ const messages = extractCanonicalMessages(request);
5
+ const canonical = {
6
+ ...(params.model ? { model: params.model } : {}),
7
+ messages,
8
+ ...(tools.length ? { tools } : {}),
9
+ ...(params.toolChoice !== undefined ? { toolChoice: params.toolChoice } : {}),
10
+ ...(params.maxOutputTokens !== undefined ? { maxTokens: params.maxOutputTokens } : {}),
11
+ ...(params.reasoningEffort ? { reasoning: { effort: params.reasoningEffort, enabled: true } } : {}),
12
+ ...(params.outputSchema !== undefined
13
+ ? {
14
+ structuredOutput: {
15
+ name: 'pulse_output',
16
+ schema: params.outputSchema,
17
+ strict: true,
18
+ mode: 'json_schema',
19
+ },
20
+ }
21
+ : {}),
22
+ ...(params.stream ? { stream: true } : {}),
23
+ };
24
+ return { canonical, toolNameAliases };
25
+ }
26
+ function extractCanonicalMessages(request) {
27
+ const messages = [];
28
+ for (const block of request.blocks) {
29
+ const content = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
30
+ if (block.kind === 'conversation' && Array.isArray(block.content)) {
31
+ for (const item of block.content) {
32
+ if (!item || typeof item !== 'object' || Array.isArray(item))
33
+ continue;
34
+ const message = item;
35
+ if (message.role === 'system' && typeof message.content === 'string') {
36
+ messages.push({ role: 'user', content: `Context note, not a new instruction:\n${message.content}` });
37
+ }
38
+ else if ((message.role === 'user' || message.role === 'assistant') && typeof message.content === 'string') {
39
+ messages.push({ role: message.role, content: message.content });
40
+ }
41
+ }
42
+ continue;
43
+ }
44
+ if (block.kind === 'system' || block.kind === 'policy' || block.kind === 'tools') {
45
+ messages.push({ role: 'system', content });
46
+ continue;
47
+ }
48
+ if (block.kind === 'history' && request.contextSpec.providerHistory !== undefined)
49
+ continue;
50
+ if (block.kind === 'provider_history' && request.contextSpec.providerHistory !== undefined) {
51
+ for (const item of request.contextSpec.providerHistory) {
52
+ if (item.role === 'user') {
53
+ messages.push({ role: 'user', content: item.content });
54
+ }
55
+ else if (item.role === 'assistant') {
56
+ messages.push({
57
+ role: 'assistant',
58
+ content: item.content ?? null,
59
+ ...(item.reasoningContent !== undefined ? { reasoningContent: item.reasoningContent } : {}),
60
+ ...(item.toolCalls === undefined
61
+ ? {}
62
+ : {
63
+ toolCalls: item.toolCalls.map((call) => ({
64
+ id: call.id,
65
+ name: call.name,
66
+ arguments: call.arguments,
67
+ })),
68
+ }),
69
+ });
70
+ }
71
+ else {
72
+ messages.push({
73
+ role: 'tool',
74
+ toolCallId: item.toolCallId,
75
+ name: item.name,
76
+ content: item.content,
77
+ });
78
+ }
79
+ }
80
+ continue;
81
+ }
82
+ else if (block.kind === 'history') {
83
+ messages.push({ role: 'assistant', content });
84
+ }
85
+ else if (block.kind === 'provider_history') {
86
+ continue;
87
+ }
88
+ else {
89
+ messages.push({ role: 'user', name: block.kind, content });
90
+ }
91
+ }
92
+ return messages;
93
+ }
94
+ function extractCanonicalTools(request) {
95
+ const block = request.blocks.find((candidate) => candidate.kind === 'tools');
96
+ const content = block?.content;
97
+ const values = Array.isArray(content)
98
+ ? content
99
+ : content && typeof content === 'object' && !Array.isArray(content) && Array.isArray(content.tools)
100
+ ? content.tools
101
+ : [];
102
+ const aliases = new Map();
103
+ const used = new Set();
104
+ const definitions = values
105
+ .filter((value) => typeof value === 'object' && value !== null && !Array.isArray(value) && typeof value.name === 'string')
106
+ .map((value, index) => {
107
+ const originalName = value.name;
108
+ const baseName = originalName.replace(/[^a-zA-Z0-9_-]/g, '_') || `tool_${index + 1}`;
109
+ let providerName = baseName;
110
+ let suffix = 2;
111
+ while (used.has(providerName))
112
+ providerName = `${baseName}_${suffix++}`;
113
+ used.add(providerName);
114
+ aliases.set(providerName, originalName);
115
+ return {
116
+ name: providerName,
117
+ ...(typeof value.description === 'string' ? { description: value.description } : {}),
118
+ parameters: (value.inputSchema ?? value.parameters ?? {}),
119
+ };
120
+ });
121
+ return { definitions, aliases };
122
+ }
@@ -0,0 +1,6 @@
1
+ import type { CanonicalLLMRequest, CanonicalStreamEvent, ModelCapabilityProfile } from '../types.js';
2
+ export declare function toAnthropicPayload(canonical: CanonicalLLMRequest, _capability: ModelCapabilityProfile): Record<string, unknown>;
3
+ export declare function parseAnthropicSseEvents(event: {
4
+ event?: string;
5
+ data: any;
6
+ }): CanonicalStreamEvent[];
@@ -0,0 +1,153 @@
1
+ export function toAnthropicPayload(canonical, _capability) {
2
+ const systemParts = [];
3
+ const messages = [];
4
+ const appendBlocks = (role, blocks) => {
5
+ if (blocks.length === 0)
6
+ return;
7
+ const previous = messages.at(-1);
8
+ if (previous?.role === role)
9
+ previous.content.push(...blocks);
10
+ else
11
+ messages.push({ role, content: [...blocks] });
12
+ };
13
+ for (const msg of canonical.messages) {
14
+ if (msg.role === 'system') {
15
+ const text = textFromContent(msg.content);
16
+ if (text !== undefined)
17
+ systemParts.push(text);
18
+ continue;
19
+ }
20
+ if (msg.role === 'tool') {
21
+ appendBlocks('user', [{ type: 'tool_result', tool_use_id: msg.toolCallId ?? '', content: textFromContent(msg.content) ?? '' }]);
22
+ continue;
23
+ }
24
+ if (msg.role === 'assistant') {
25
+ const blocks = [];
26
+ const text = textFromContent(msg.content);
27
+ if (text !== undefined)
28
+ blocks.push({ type: 'text', text });
29
+ for (const call of msg.toolCalls ?? []) {
30
+ blocks.push({ type: 'tool_use', id: call.id, name: call.name, input: parseToolInput(call.arguments) });
31
+ }
32
+ appendBlocks('assistant', blocks);
33
+ continue;
34
+ }
35
+ const text = textFromContent(msg.content);
36
+ if (text !== undefined)
37
+ appendBlocks('user', [{ type: 'text', text }]);
38
+ }
39
+ const system = systemParts.join('\n');
40
+ const maxTokens = canonical.maxTokens ?? 4096;
41
+ const body = {
42
+ ...(canonical.model ? { model: canonical.model } : {}),
43
+ max_tokens: maxTokens,
44
+ ...(canonical.temperature !== undefined ? { temperature: canonical.temperature } : {}),
45
+ ...(system.length > 0 ? { system } : {}),
46
+ messages,
47
+ };
48
+ // Thinking
49
+ if (canonical.reasoning?.budgetTokens) {
50
+ body.thinking = {
51
+ type: 'enabled',
52
+ budget_tokens: canonical.reasoning.budgetTokens,
53
+ };
54
+ }
55
+ // Tools
56
+ if (canonical.tools && canonical.tools.length > 0) {
57
+ body.tools = canonical.tools.map((t) => ({
58
+ name: t.name,
59
+ ...(t.description ? { description: t.description } : {}),
60
+ input_schema: t.parameters,
61
+ }));
62
+ const toolChoice = canonical.toolChoice === undefined ? undefined : mapAnthropicToolChoice(canonical.toolChoice);
63
+ if (toolChoice !== undefined)
64
+ body.tool_choice = toolChoice;
65
+ }
66
+ // Structured output
67
+ if (canonical.structuredOutput?.schema) {
68
+ body.output_format = {
69
+ type: 'json_schema',
70
+ schema: canonical.structuredOutput.schema,
71
+ };
72
+ }
73
+ // Streaming
74
+ if (canonical.stream) {
75
+ body.stream = true;
76
+ }
77
+ return body;
78
+ }
79
+ function textFromContent(content) {
80
+ if (typeof content === 'string')
81
+ return content;
82
+ if (!Array.isArray(content))
83
+ return undefined;
84
+ const text = content.flatMap((part) => part.type === 'text' ? [part.text] : []).join('');
85
+ return text.length > 0 ? text : undefined;
86
+ }
87
+ function parseToolInput(argumentsText) {
88
+ try {
89
+ const parsed = JSON.parse(argumentsText);
90
+ if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed))
91
+ return parsed;
92
+ return { arguments: parsed };
93
+ }
94
+ catch {
95
+ return { arguments: argumentsText };
96
+ }
97
+ }
98
+ function mapAnthropicToolChoice(choice) {
99
+ if (choice === 'auto')
100
+ return { type: 'auto' };
101
+ if (choice === 'required')
102
+ return { type: 'any' };
103
+ if (choice === 'none')
104
+ return undefined;
105
+ if (typeof choice === 'object' && 'function' in choice) {
106
+ return { type: 'tool', name: choice.function.name };
107
+ }
108
+ return undefined;
109
+ }
110
+ export function parseAnthropicSseEvents(event) {
111
+ const events = [];
112
+ if (!event.data || typeof event.data !== 'object')
113
+ return events;
114
+ const data = event.data;
115
+ if (event.event === 'content_block_delta' && data.delta && typeof data.delta === 'object') {
116
+ if (data.delta.type === 'text_delta' && typeof data.delta.text === 'string') {
117
+ events.push({ type: 'text_delta', text: data.delta.text });
118
+ }
119
+ if (data.delta.type === 'thinking_delta' && typeof data.delta.thinking === 'string') {
120
+ events.push({ type: 'reasoning_delta', text: data.delta.thinking });
121
+ }
122
+ if (data.delta.type === 'input_json_delta' && typeof data.delta.partial_json === 'string') {
123
+ events.push({
124
+ type: 'tool_call_delta',
125
+ index: Number(data.index ?? 0),
126
+ argumentsDelta: data.delta.partial_json,
127
+ });
128
+ }
129
+ }
130
+ if (event.event === 'message_delta') {
131
+ if (typeof data.delta?.stop_reason === 'string') {
132
+ events.push({ type: 'finish', reason: data.delta.stop_reason });
133
+ }
134
+ if (data.usage && typeof data.usage === 'object') {
135
+ events.push({
136
+ type: 'usage',
137
+ usage: {
138
+ ...(typeof data.usage.output_tokens === 'number' ? { outputTokens: data.usage.output_tokens } : {}),
139
+ },
140
+ });
141
+ }
142
+ }
143
+ if (event.event === 'message_start' && data.message?.usage && typeof data.message.usage === 'object') {
144
+ events.push({
145
+ type: 'usage',
146
+ usage: {
147
+ ...(typeof data.message.usage.input_tokens === 'number' ? { inputTokens: data.message.usage.input_tokens } : {}),
148
+ ...(typeof data.message.usage.output_tokens === 'number' ? { outputTokens: data.message.usage.output_tokens } : {}),
149
+ },
150
+ });
151
+ }
152
+ return events;
153
+ }
@@ -0,0 +1,3 @@
1
+ import type { CanonicalLLMRequest, CanonicalStreamEvent, ModelCapabilityProfile } from '../types.js';
2
+ export declare function toOpenAIPayload(canonical: CanonicalLLMRequest, capability: ModelCapabilityProfile, provider?: string): Record<string, unknown>;
3
+ export declare function parseOpenAISseEvents(raw: unknown): CanonicalStreamEvent[];
@@ -0,0 +1,140 @@
1
+ export function toOpenAIPayload(canonical, capability, provider = 'openai') {
2
+ const messages = canonical.messages.map((msg) => {
3
+ const base = {
4
+ role: msg.role,
5
+ content: msg.content === null ? null : typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
6
+ };
7
+ if (msg.name)
8
+ base.name = msg.name;
9
+ if (msg.toolCallId)
10
+ base.tool_call_id = msg.toolCallId;
11
+ if (msg.toolCalls && msg.toolCalls.length > 0) {
12
+ base.tool_calls = msg.toolCalls.map((call) => ({
13
+ id: call.id,
14
+ type: 'function',
15
+ function: {
16
+ name: call.name,
17
+ arguments: call.arguments,
18
+ },
19
+ }));
20
+ }
21
+ if (provider === 'deepseek' && msg.reasoningContent !== undefined) {
22
+ base.reasoning_content = msg.reasoningContent;
23
+ }
24
+ return base;
25
+ });
26
+ const body = {
27
+ ...(canonical.model ? { model: canonical.model } : {}),
28
+ messages,
29
+ };
30
+ // Token limits
31
+ if (canonical.maxTokens !== undefined) {
32
+ if (capability.constraints.maxTokensParamName === 'max_completion_tokens') {
33
+ body.max_completion_tokens = canonical.maxTokens;
34
+ }
35
+ else {
36
+ body.max_tokens = canonical.maxTokens;
37
+ }
38
+ }
39
+ // Sampling hyperparameters
40
+ if (canonical.temperature !== undefined) {
41
+ body.temperature = canonical.temperature;
42
+ }
43
+ if (canonical.topP !== undefined) {
44
+ body.top_p = canonical.topP;
45
+ }
46
+ if (canonical.stop) {
47
+ body.stop = canonical.stop;
48
+ }
49
+ // Reasoning
50
+ if (canonical.reasoning?.effort) {
51
+ body.reasoning_effort = canonical.reasoning.effort;
52
+ }
53
+ // Tools
54
+ if (canonical.tools && canonical.tools.length > 0) {
55
+ body.tools = canonical.tools.map((tool) => ({
56
+ type: 'function',
57
+ function: {
58
+ name: tool.name,
59
+ ...(tool.description ? { description: tool.description } : {}),
60
+ parameters: tool.parameters,
61
+ },
62
+ }));
63
+ if (canonical.toolChoice !== undefined) {
64
+ body.tool_choice = canonical.toolChoice;
65
+ }
66
+ }
67
+ // Structured output
68
+ if (canonical.structuredOutput) {
69
+ if (canonical.structuredOutput.mode === 'json_schema') {
70
+ body.response_format = {
71
+ type: 'json_schema',
72
+ json_schema: {
73
+ name: canonical.structuredOutput.name ?? 'pulse_output',
74
+ strict: canonical.structuredOutput.strict ?? true,
75
+ schema: canonical.structuredOutput.schema,
76
+ },
77
+ };
78
+ }
79
+ else if (canonical.structuredOutput.mode === 'json_object') {
80
+ body.response_format = { type: 'json_object' };
81
+ }
82
+ }
83
+ // Streaming
84
+ if (canonical.stream) {
85
+ body.stream = true;
86
+ body.stream_options = { include_usage: true };
87
+ }
88
+ return body;
89
+ }
90
+ export function parseOpenAISseEvents(raw) {
91
+ const events = [];
92
+ if (!raw || typeof raw !== 'object')
93
+ return events;
94
+ const root = raw;
95
+ if (root.usage && typeof root.usage === 'object') {
96
+ const u = root.usage;
97
+ events.push({
98
+ type: 'usage',
99
+ usage: {
100
+ ...(typeof u.prompt_tokens === 'number' ? { inputTokens: u.prompt_tokens } : {}),
101
+ ...(typeof u.completion_tokens === 'number' ? { outputTokens: u.completion_tokens } : {}),
102
+ ...(typeof u.completion_tokens_details?.reasoning_tokens === 'number'
103
+ ? { reasoningTokens: u.completion_tokens_details.reasoning_tokens }
104
+ : {}),
105
+ ...(typeof u.prompt_tokens_details?.cached_tokens === 'number'
106
+ ? { cachedInputTokens: u.prompt_tokens_details.cached_tokens }
107
+ : {}),
108
+ },
109
+ });
110
+ }
111
+ if (Array.isArray(root.choices) && root.choices.length > 0) {
112
+ const choice = root.choices[0];
113
+ const delta = choice.delta;
114
+ if (typeof delta?.content === 'string' && delta.content.length > 0) {
115
+ events.push({ type: 'text_delta', text: delta.content });
116
+ }
117
+ if (typeof delta?.reasoning_content === 'string' && delta.reasoning_content.length > 0) {
118
+ events.push({ type: 'reasoning_delta', text: delta.reasoning_content });
119
+ }
120
+ if (typeof delta?.refusal === 'string' && delta.refusal.length > 0) {
121
+ events.push({ type: 'refusal_delta', refusal: delta.refusal });
122
+ }
123
+ if (Array.isArray(delta?.tool_calls)) {
124
+ for (const call of delta.tool_calls) {
125
+ const fn = call.function;
126
+ events.push({
127
+ type: 'tool_call_delta',
128
+ index: typeof call.index === 'number' ? call.index : 0,
129
+ ...(typeof call.id === 'string' ? { id: call.id } : {}),
130
+ ...(typeof fn?.name === 'string' ? { name: fn.name } : {}),
131
+ argumentsDelta: typeof fn?.arguments === 'string' ? fn.arguments : '',
132
+ });
133
+ }
134
+ }
135
+ if (typeof choice.finish_reason === 'string') {
136
+ events.push({ type: 'finish', reason: choice.finish_reason });
137
+ }
138
+ }
139
+ return events;
140
+ }
@@ -0,0 +1,10 @@
1
+ import type { AdaptedCanonicalRequest, CanonicalLLMRequest, ModelCapabilityProfile } from './types.js';
2
+ export declare class DynamicCapabilityProbe {
3
+ private readonly unsupportedReasoning;
4
+ private key;
5
+ isReasoningUnsupported(provider: string, model?: string): boolean;
6
+ markReasoningUnsupported(provider: string, model?: string): void;
7
+ reasoningParameterRejected(cause: unknown): boolean;
8
+ }
9
+ export declare const globalProbe: DynamicCapabilityProbe;
10
+ export declare function adaptCanonicalRequest(original: CanonicalLLMRequest, capability: ModelCapabilityProfile, probe?: DynamicCapabilityProbe, toolNameAliases?: ReadonlyMap<string, string>): AdaptedCanonicalRequest;