@hunterzhu/pulse-adapters 0.4.2 → 0.4.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.
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;
@@ -0,0 +1,98 @@
1
+ export class DynamicCapabilityProbe {
2
+ unsupportedReasoning = new Set();
3
+ key(provider, model) {
4
+ return `${provider.toLowerCase()}:${(model ?? '').toLowerCase()}`;
5
+ }
6
+ isReasoningUnsupported(provider, model) {
7
+ return this.unsupportedReasoning.has(this.key(provider, model));
8
+ }
9
+ markReasoningUnsupported(provider, model) {
10
+ this.unsupportedReasoning.add(this.key(provider, model));
11
+ }
12
+ reasoningParameterRejected(cause) {
13
+ if (!(cause instanceof Error))
14
+ return false;
15
+ const code = cause.code;
16
+ if (code !== 'PROVIDER_HTTP_400')
17
+ return false;
18
+ return /reasoning_effort/i.test(cause.message);
19
+ }
20
+ }
21
+ export const globalProbe = new DynamicCapabilityProbe();
22
+ export function adaptCanonicalRequest(original, capability, probe = globalProbe, toolNameAliases = new Map()) {
23
+ const messages = [...original.messages.map((m) => ({ ...m }))];
24
+ const request = {
25
+ ...original,
26
+ messages,
27
+ ...(original.tools ? { tools: [...original.tools] } : {}),
28
+ };
29
+ let clientSchemaValidationRequired = false;
30
+ // 1. Structured Output Adaptation
31
+ if (request.structuredOutput) {
32
+ if (capability.structuredOutput === 'json_object_only') {
33
+ const schemaPrompt = `Return only a JSON object matching this schema. The application will validate the result:\n${JSON.stringify(request.structuredOutput.schema)}`;
34
+ injectSystemInstruction(messages, schemaPrompt);
35
+ request.structuredOutput = {
36
+ ...request.structuredOutput,
37
+ mode: 'json_object',
38
+ };
39
+ clientSchemaValidationRequired = true;
40
+ }
41
+ else if (capability.structuredOutput === 'unsupported') {
42
+ const schemaPrompt = `Return only a JSON object matching this schema. The application will validate the result:\n${JSON.stringify(request.structuredOutput.schema)}`;
43
+ injectSystemInstruction(messages, schemaPrompt);
44
+ delete request.structuredOutput;
45
+ clientSchemaValidationRequired = true;
46
+ }
47
+ else {
48
+ request.structuredOutput = {
49
+ ...request.structuredOutput,
50
+ mode: 'json_schema',
51
+ };
52
+ if (capability.structuredOutput === 'native_loose') {
53
+ clientSchemaValidationRequired = true;
54
+ }
55
+ }
56
+ }
57
+ // 2. Reasoning / Thinking Adaptation
58
+ const reasoningDisabled = probe.isReasoningUnsupported(capability.provider, request.model) ||
59
+ capability.reasoning === 'unsupported';
60
+ if (reasoningDisabled || !request.reasoning?.effort) {
61
+ delete request.reasoning;
62
+ }
63
+ else if (capability.reasoning === 'budget_tokens') {
64
+ const maxTokens = request.maxTokens ?? 4096;
65
+ const requested = request.reasoning.effort === 'high' ? 8_000 : request.reasoning.effort === 'medium' ? 2_048 : 1_024;
66
+ const budget = Math.min(requested, maxTokens - 1_024);
67
+ if (budget >= 1_024 && budget < maxTokens) {
68
+ request.reasoning = {
69
+ ...request.reasoning,
70
+ budgetTokens: budget,
71
+ enabled: true,
72
+ };
73
+ if (capability.constraints.disallowTemperatureWithReasoning) {
74
+ request.temperature = capability.constraints.defaultTemperatureWithReasoning ?? 1.0;
75
+ }
76
+ }
77
+ else {
78
+ delete request.reasoning;
79
+ }
80
+ }
81
+ else if (capability.reasoning === 'effort_param') {
82
+ if (capability.constraints.disallowTemperatureWithReasoning) {
83
+ delete request.temperature;
84
+ }
85
+ }
86
+ else if (capability.reasoning === 'prompt_mode') {
87
+ delete request.reasoning;
88
+ }
89
+ return {
90
+ request,
91
+ capability,
92
+ clientSchemaValidationRequired,
93
+ toolNameAliases,
94
+ };
95
+ }
96
+ function injectSystemInstruction(messages, instruction) {
97
+ messages.unshift({ role: 'system', content: instruction });
98
+ }
@@ -0,0 +1,109 @@
1
+ import type { JsonValue } from '@hunterzhu/pulse-runtime';
2
+ import type { ProviderToolChoice } from '../types.js';
3
+ export type CanonicalRole = 'system' | 'user' | 'assistant' | 'tool';
4
+ export interface CanonicalTextContent {
5
+ type: 'text';
6
+ text: string;
7
+ }
8
+ export interface CanonicalImageContent {
9
+ type: 'image';
10
+ source: {
11
+ type: 'base64' | 'url';
12
+ mediaType: string;
13
+ data: string;
14
+ };
15
+ }
16
+ export type CanonicalContentPart = CanonicalTextContent | CanonicalImageContent;
17
+ export interface CanonicalToolCall {
18
+ id: string;
19
+ name: string;
20
+ arguments: string;
21
+ }
22
+ export interface CanonicalMessage {
23
+ role: CanonicalRole;
24
+ content: string | null | CanonicalContentPart[];
25
+ name?: string;
26
+ toolCallId?: string;
27
+ toolCalls?: CanonicalToolCall[];
28
+ reasoningContent?: string;
29
+ }
30
+ export interface CanonicalToolDefinition {
31
+ name: string;
32
+ description?: string;
33
+ parameters: JsonValue;
34
+ }
35
+ export type CanonicalToolChoice = ProviderToolChoice;
36
+ export interface CanonicalStructuredOutput {
37
+ name?: string;
38
+ schema: JsonValue;
39
+ strict?: boolean;
40
+ description?: string;
41
+ mode?: 'json_schema' | 'json_object';
42
+ }
43
+ export interface CanonicalReasoningConfig {
44
+ effort?: 'low' | 'medium' | 'high';
45
+ budgetTokens?: number;
46
+ enabled?: boolean;
47
+ }
48
+ export interface CanonicalLLMRequest {
49
+ model?: string;
50
+ messages: CanonicalMessage[];
51
+ tools?: CanonicalToolDefinition[];
52
+ toolChoice?: CanonicalToolChoice;
53
+ temperature?: number;
54
+ topP?: number;
55
+ maxTokens?: number;
56
+ stop?: string[];
57
+ reasoning?: CanonicalReasoningConfig;
58
+ structuredOutput?: CanonicalStructuredOutput;
59
+ stream?: boolean;
60
+ }
61
+ export type CanonicalStreamEvent = {
62
+ type: 'text_delta';
63
+ text: string;
64
+ } | {
65
+ type: 'reasoning_delta';
66
+ text: string;
67
+ } | {
68
+ type: 'tool_call_delta';
69
+ index: number;
70
+ id?: string;
71
+ name?: string;
72
+ argumentsDelta: string;
73
+ } | {
74
+ type: 'refusal_delta';
75
+ refusal: string;
76
+ } | {
77
+ type: 'usage';
78
+ usage: {
79
+ inputTokens?: number;
80
+ outputTokens?: number;
81
+ reasoningTokens?: number;
82
+ cachedInputTokens?: number;
83
+ };
84
+ } | {
85
+ type: 'finish';
86
+ reason: string;
87
+ };
88
+ export type StructuredOutputCapability = 'native_strict' | 'native_loose' | 'json_object_only' | 'unsupported';
89
+ export type ReasoningCapability = 'effort_param' | 'budget_tokens' | 'prompt_mode' | 'unsupported';
90
+ export interface ModelCapabilityConstraints {
91
+ disallowTemperatureWithReasoning?: boolean;
92
+ defaultTemperatureWithReasoning?: number;
93
+ maxTokensParamName?: 'max_tokens' | 'max_completion_tokens';
94
+ supportsSystemMessage?: boolean;
95
+ supportsParallelTools?: boolean;
96
+ }
97
+ export interface ModelCapabilityProfile {
98
+ id: string;
99
+ provider: string;
100
+ structuredOutput: StructuredOutputCapability;
101
+ reasoning: ReasoningCapability;
102
+ constraints: ModelCapabilityConstraints;
103
+ }
104
+ export interface AdaptedCanonicalRequest {
105
+ request: CanonicalLLMRequest;
106
+ capability: ModelCapabilityProfile;
107
+ clientSchemaValidationRequired: boolean;
108
+ toolNameAliases: ReadonlyMap<string, string>;
109
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -232,23 +232,81 @@ function parseDsmlJson(value) {
232
232
  return undefined;
233
233
  }
234
234
  }
235
- /** DeepSeek sometimes writes only JSON tool objects instead of tool_calls. Each object needs an explicit tool name; argument-shaped JSON and trailing prose stay text. */
235
+ /** DeepSeek sometimes writes tool calls as JSON or markdown fences instead of tool_calls. A bare object still needs an explicit tool name. Argument-shaped JSON, a single tool object after prose, and any trailing prose stay text. */
236
236
  function recoverDeepSeekJsonToolCalls(text, toolNameAliases) {
237
- const leading = takeLeadingJsonValues(text.trim());
237
+ const trimmed = text.trim();
238
+ return recoverStrictJsonToolCalls(trimmed, toolNameAliases) ?? recoverMarkdownToolFences(trimmed, toolNameAliases);
239
+ }
240
+ function recoverStrictJsonToolCalls(text, toolNameAliases) {
241
+ const leading = takeLeadingJsonValues(text);
238
242
  if (leading === undefined || leading.values.length === 0 || leading.rest.trim().length > 0)
239
243
  return undefined;
240
244
  const objects = leading.values.filter((value) => !isEmptyJsonValue(value));
241
245
  if (objects.length === 0)
242
246
  return undefined;
247
+ if (objects.length === 1 && Array.isArray(objects[0]) && objects[0].every((item) => argumentObject(item) !== undefined))
248
+ return toolCallsFrom(objects[0], toolNameAliases);
243
249
  if (objects.length === 1 && objects[0] && typeof objects[0] === 'object' && !Array.isArray(objects[0])) {
244
250
  const batch = objects[0].tool_calls ?? objects[0].toolCalls;
245
- if (Array.isArray(batch)) {
246
- const calls = batch.map((item) => jsonToolCall(item, toolNameAliases));
247
- return calls.every((call) => call !== undefined) && calls.length > 0 ? calls : undefined;
251
+ if (Array.isArray(batch))
252
+ return toolCallsFrom(batch, toolNameAliases);
253
+ }
254
+ return toolCallsFrom(objects, toolNameAliases);
255
+ }
256
+ /** A message that is only `tool.name` headings followed by JSON fences is a tool batch, not a finished answer. */
257
+ function recoverMarkdownToolFences(text, toolNameAliases) {
258
+ const lines = text.split(/\r?\n/);
259
+ const calls = [];
260
+ let index = 0;
261
+ const skipBlank = () => { while (index < lines.length && lines[index].trim() === '')
262
+ index++; };
263
+ skipBlank();
264
+ if (index >= lines.length)
265
+ return undefined;
266
+ while (index < lines.length) {
267
+ skipBlank();
268
+ if (index >= lines.length)
269
+ break;
270
+ const providerToolName = lines[index].trim();
271
+ if (!/^[A-Za-z][\w-]*(?:\.[\w-]+)+$/.test(providerToolName))
272
+ return undefined;
273
+ index++;
274
+ if (lines[index]?.trim() !== '```json' && lines[index]?.trim() !== '```')
275
+ return undefined;
276
+ index++;
277
+ const body = [];
278
+ while (index < lines.length && lines[index].trim() !== '```')
279
+ body.push(lines[index++]);
280
+ if (index >= lines.length)
281
+ return undefined;
282
+ index++;
283
+ let parsed;
284
+ try {
285
+ parsed = JSON.parse(body.join('\n'));
248
286
  }
287
+ catch {
288
+ return undefined;
289
+ }
290
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
291
+ return undefined;
292
+ const input = parsed;
293
+ calls.push({ providerToolName, name: toolNameAliases?.get(providerToolName) ?? providerToolName, input, providerToolArguments: JSON.stringify(input) });
249
294
  }
250
- const calls = objects.map((value) => jsonToolCall(value, toolNameAliases));
251
- return calls.every((call) => call !== undefined) ? calls : undefined;
295
+ return calls.length > 0 ? calls : undefined;
296
+ }
297
+ function toolCallsFrom(values, toolNameAliases) {
298
+ const calls = values.map((value) => jsonToolCall(value, toolNameAliases));
299
+ return calls.every((call) => call !== undefined) && calls.length > 0 ? calls : undefined;
300
+ }
301
+ function argumentObject(value) {
302
+ if (!value || typeof value !== 'object' || Array.isArray(value))
303
+ return undefined;
304
+ const record = value;
305
+ const named = typeof record.tool === 'string' && record.tool.length > 0 ? record.tool : typeof record.name === 'string' && record.name.length > 0 ? record.name : undefined;
306
+ const raw = record.arguments ?? record.input ?? record.parameters ?? record.args;
307
+ if (named === undefined || !raw || typeof raw !== 'object' || Array.isArray(raw))
308
+ return undefined;
309
+ return raw;
252
310
  }
253
311
  function isEmptyJsonValue(value) {
254
312
  if (Array.isArray(value))
@@ -263,7 +321,7 @@ function jsonToolCall(value, toolNameAliases) {
263
321
  const providerToolName = named;
264
322
  if (providerToolName === undefined)
265
323
  return undefined;
266
- const raw = record.arguments ?? record.input ?? record.parameters;
324
+ const raw = record.arguments ?? record.input ?? record.parameters ?? record.args;
267
325
  const input = raw && typeof raw === 'object' && !Array.isArray(raw) ? { ...raw } : { ...record };
268
326
  if (raw === undefined) {
269
327
  delete input.tool;
@@ -5,7 +5,7 @@ export declare class OpenAICompatibleAdapter implements ProviderAdapter {
5
5
  private readonly config;
6
6
  readonly name = "OpenAI Compatible";
7
7
  private readonly baseURL;
8
- private reasoningUnsupported;
8
+ private readonly probe;
9
9
  constructor(id: string, config: ProviderPresetConfig);
10
10
  executeAttempt(params: {
11
11
  request: LLMRequestProjection;
@@ -1,18 +1,11 @@
1
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
+ import { adaptCanonicalRequest, DynamicCapabilityProbe, lowerProjectionToCanonical, parseOpenAISseEvents, resolveModelCapability, toOpenAIPayload } from './canonical/index.js';
10
3
  export class OpenAICompatibleAdapter {
11
4
  id;
12
5
  config;
13
6
  name = 'OpenAI Compatible';
14
7
  baseURL;
15
- reasoningUnsupported = false;
8
+ probe = new DynamicCapabilityProbe();
16
9
  constructor(id, config) {
17
10
  this.id = id;
18
11
  this.config = config;
@@ -20,16 +13,19 @@ export class OpenAICompatibleAdapter {
20
13
  }
21
14
  async executeAttempt(params) {
22
15
  const streaming = params.onObservation !== undefined;
23
- const { definitions: tools, aliases: toolNameAliases } = toolDefinitions(params.request);
24
- const includeReasoning = Boolean(this.config.reasoningEffort) && !this.reasoningUnsupported;
25
- const messages = toMessages(params.request, this.config.provider);
26
- const deepSeekJsonMode = params.outputSchema !== undefined && this.config.provider === 'deepseek';
27
- if (deepSeekJsonMode)
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)}` });
29
- const responseFormat = params.outputSchema === undefined ? undefined : deepSeekJsonMode
30
- ? { type: 'json_object' }
31
- : { type: 'json_schema', json_schema: { name: 'pulse_output', strict: true, schema: params.outputSchema } };
32
- 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, ...(tools.length ? { tools, ...(this.config.toolChoice === undefined ? {} : { tool_choice: this.config.toolChoice }) } : {}), ...(responseFormat === undefined ? {} : { response_format: responseFormat }), ...(streaming ? { stream: true, stream_options: { include_usage: true } } : {}) };
16
+ const { canonical, toolNameAliases } = lowerProjectionToCanonical({
17
+ request: params.request,
18
+ model: params.model ?? this.config.defaultModel,
19
+ outputSchema: params.outputSchema,
20
+ maxOutputTokens: params.maxOutputTokens ?? this.config.maxOutputTokens,
21
+ reasoningEffort: this.config.reasoningEffort,
22
+ toolChoice: this.config.toolChoice,
23
+ stream: streaming,
24
+ });
25
+ const capability = resolveModelCapability(canonical.model, this.config.provider);
26
+ const { request: adapted } = adaptCanonicalRequest(canonical, capability, this.probe, toolNameAliases);
27
+ const sentReasoning = Boolean(adapted.reasoning?.effort);
28
+ const body = toOpenAIPayload(adapted, capability, this.config.provider);
33
29
  try {
34
30
  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) });
35
31
  if (!response.ok)
@@ -37,11 +33,13 @@ export class OpenAICompatibleAdapter {
37
33
  if (!streaming || !response.headers.get('content-type')?.includes('text/event-stream'))
38
34
  return normalizeOpenAIResponse(await parseProviderJson(response), toolNameAliases, this.config.provider);
39
35
  const events = await consumeProviderSse(response, (event) => {
40
- const delta = event.data?.choices?.[0]?.delta;
41
- if (typeof delta?.content === 'string')
42
- params.onObservation?.(delta.content);
43
- if (typeof delta?.refusal === 'string')
44
- params.onObservation?.(delta.refusal);
36
+ const streamEvents = parseOpenAISseEvents(event.data);
37
+ for (const ev of streamEvents) {
38
+ if (ev.type === 'text_delta')
39
+ params.onObservation?.(ev.text);
40
+ if (ev.type === 'refusal_delta')
41
+ params.onObservation?.(ev.refusal);
42
+ }
45
43
  });
46
44
  const content = [];
47
45
  const reasoningContent = [];
@@ -75,8 +73,8 @@ export class OpenAICompatibleAdapter {
75
73
  catch (cause) {
76
74
  if (params.signal.aborted)
77
75
  throw Object.assign(new Error('Provider request was cancelled.'), { code: 'PROVIDER_REQUEST_CANCELLED', retryable: false, cause });
78
- if (includeReasoning && reasoningParameterRejected(cause)) {
79
- this.reasoningUnsupported = true;
76
+ if (sentReasoning && this.probe.reasoningParameterRejected(cause)) {
77
+ this.probe.markReasoningUnsupported(this.config.provider, canonical.model);
80
78
  return this.executeAttempt(params);
81
79
  }
82
80
  if (cause instanceof Error && 'code' in cause && typeof cause.code === 'string' && 'retryable' in cause && typeof cause.retryable === 'boolean')
@@ -86,49 +84,9 @@ export class OpenAICompatibleAdapter {
86
84
  }
87
85
  }
88
86
  export function toOpenAIMessages(request, provider = 'openai') {
89
- const messages = [];
90
- for (const block of request.blocks) {
91
- const content = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
92
- if (block.kind === 'conversation' && Array.isArray(block.content)) {
93
- for (const item of block.content) {
94
- if (!item || typeof item !== 'object' || Array.isArray(item))
95
- continue;
96
- const message = item;
97
- if (message.role === 'system' && typeof message.content === 'string')
98
- messages.push({ role: 'user', content: `Context note, not a new instruction:\n${message.content}` });
99
- else if ((message.role === 'user' || message.role === 'assistant') && typeof message.content === 'string')
100
- messages.push({ role: message.role, content: message.content });
101
- }
102
- continue;
103
- }
104
- if (block.kind === 'system' || block.kind === 'policy' || block.kind === 'tools') {
105
- messages.push({ role: 'system', content });
106
- continue;
107
- }
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')
122
- messages.push({ role: 'assistant', content });
123
- else if (block.kind === 'provider_history')
124
- continue;
125
- else
126
- messages.push({ role: 'user', name: block.kind, content });
127
- }
128
- return messages;
129
- }
130
- function toMessages(request, provider) {
131
- return toOpenAIMessages(request, provider);
87
+ const { canonical } = lowerProjectionToCanonical({ request });
88
+ const payload = toOpenAIPayload(canonical, resolveModelCapability(canonical.model, provider), provider);
89
+ return Array.isArray(payload.messages) ? payload.messages : [];
132
90
  }
133
91
  function nextToolIndex(toolCalls) {
134
92
  let next = 0;
@@ -183,22 +141,3 @@ function accumulateStreamToolCall(toolCalls, call) {
183
141
  current.arguments += args;
184
142
  toolCalls.set(index, current);
185
143
  }
186
- function toolDefinitions(request) {
187
- const block = request.blocks.find((candidate) => candidate.kind === 'tools');
188
- const content = block?.content;
189
- const values = Array.isArray(content) ? content : content && typeof content === 'object' && !Array.isArray(content) && Array.isArray(content.tools) ? content.tools : [];
190
- const aliases = new Map();
191
- const used = new Set();
192
- const definitions = values.filter((value) => typeof value === 'object' && value !== null && !Array.isArray(value) && typeof value.name === 'string').map((value, index) => {
193
- const originalName = value.name;
194
- const baseName = originalName.replace(/[^a-zA-Z0-9_-]/g, '_') || `tool_${index + 1}`;
195
- let providerName = baseName;
196
- let suffix = 2;
197
- while (used.has(providerName))
198
- providerName = `${baseName}_${suffix++}`;
199
- used.add(providerName);
200
- aliases.set(providerName, originalName);
201
- return { type: 'function', function: { name: providerName, ...(typeof value.description === 'string' ? { description: value.description } : {}), parameters: (value.inputSchema ?? value.parameters ?? {}) } };
202
- });
203
- return { definitions, aliases };
204
- }
@@ -47,6 +47,53 @@ class SlotPool {
47
47
  this.slots.set(key, slot);
48
48
  } ; return slot; }
49
49
  }
50
+ function jsonCandidates(text) {
51
+ const found = [];
52
+ const push = (value) => {
53
+ try {
54
+ const parsed = JSON.parse(value);
55
+ found.push(parsed);
56
+ if (typeof parsed === 'string' && parsed !== value)
57
+ push(parsed);
58
+ }
59
+ catch { /* keep scanning */ }
60
+ };
61
+ const trimmed = text.trim();
62
+ if (trimmed)
63
+ push(trimmed);
64
+ for (let start = 0; start < text.length; start++) {
65
+ if (text[start] !== '{')
66
+ continue;
67
+ let depth = 0;
68
+ let inString = false;
69
+ let escaped = false;
70
+ for (let end = start; end < text.length; end++) {
71
+ const char = text[end];
72
+ if (inString) {
73
+ if (escaped)
74
+ escaped = false;
75
+ else if (char === '\\')
76
+ escaped = true;
77
+ else if (char === '"')
78
+ inString = false;
79
+ continue;
80
+ }
81
+ if (char === '"')
82
+ inString = true;
83
+ else if (char === '{')
84
+ depth++;
85
+ else if (char === '}') {
86
+ depth--;
87
+ if (depth === 0) {
88
+ push(text.slice(start, end + 1));
89
+ start = end;
90
+ break;
91
+ }
92
+ }
93
+ }
94
+ }
95
+ return found;
96
+ }
50
97
  function toJson(value, seen = new Set()) {
51
98
  if (value === null || typeof value === 'string' || typeof value === 'boolean')
52
99
  return value;
@@ -172,13 +219,15 @@ export function createModelEffectExecutor(config) {
172
219
  throw new OutputValidationError('adapter', 'MODEL_REFUSAL', output.refusal ?? 'Provider refused the request.');
173
220
  }
174
221
  if (input.outputSchema !== undefined) {
175
- const candidateValue = output.structured ?? output.text;
176
- if (!validateJsonSchema(candidateValue, input.outputSchema)) {
177
- lastSchemaViolation = toJson(candidateValue);
222
+ const candidates = output.structured === undefined ? jsonCandidates(output.text) : [output.structured, ...jsonCandidates(output.text)];
223
+ const matched = candidates.find((candidate) => validateJsonSchema(candidate, input.outputSchema));
224
+ if (matched === undefined) {
225
+ lastSchemaViolation = toJson(candidates[0] ?? output.text);
178
226
  failedForSchema = true;
179
227
  recordFeedback('schema_rejected', 0);
180
228
  throw new OutputValidationError('structured', 'OUTPUT_SCHEMA_VIOLATION', 'Provider output did not match the declared schema');
181
229
  }
230
+ output.structured = matched;
182
231
  }
183
232
  failedForSchema = false;
184
233
  recordFeedback('succeeded', 1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hunterzhu/pulse-adapters",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "license": "PolyForm-Noncommercial-1.0.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,8 +18,8 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@anthropic-ai/sandbox-runtime": "0.0.77",
21
- "@hunterzhu/pulse-runtime": "0.4.2",
22
- "@hunterzhu/pulse-tool-sdk": "0.4.2",
21
+ "@hunterzhu/pulse-runtime": "0.4.4",
22
+ "@hunterzhu/pulse-tool-sdk": "0.4.4",
23
23
  "exceljs": "4.4.0",
24
24
  "pdfjs-dist": "4.10.38"
25
25
  }