@hunterzhu/pulse-adapters 0.4.2 → 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 +1 -0
- package/dist/index.js +1 -0
- package/dist/providers/canonical/capabilities.d.ts +19 -0
- package/dist/providers/canonical/capabilities.js +164 -0
- package/dist/providers/canonical/index.d.ts +6 -0
- package/dist/providers/canonical/index.js +6 -0
- package/dist/providers/canonical/lowering.d.ts +17 -0
- package/dist/providers/canonical/lowering.js +122 -0
- package/dist/providers/canonical/mappers/anthropic.d.ts +6 -0
- package/dist/providers/canonical/mappers/anthropic.js +153 -0
- package/dist/providers/canonical/mappers/openai.d.ts +3 -0
- package/dist/providers/canonical/mappers/openai.js +140 -0
- package/dist/providers/canonical/polyfill.d.ts +10 -0
- package/dist/providers/canonical/polyfill.js +98 -0
- package/dist/providers/canonical/types.d.ts +109 -0
- package/dist/providers/canonical/types.js +1 -0
- package/dist/providers/openai-compat.d.ts +1 -1
- package/dist/providers/openai-compat.js +27 -88
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -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,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 {};
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
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
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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 (
|
|
79
|
-
this.
|
|
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
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hunterzhu/pulse-adapters",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
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.
|
|
22
|
-
"@hunterzhu/pulse-tool-sdk": "0.4.
|
|
21
|
+
"@hunterzhu/pulse-runtime": "0.4.3",
|
|
22
|
+
"@hunterzhu/pulse-tool-sdk": "0.4.3",
|
|
23
23
|
"exceljs": "4.4.0",
|
|
24
24
|
"pdfjs-dist": "4.10.38"
|
|
25
25
|
}
|