@hunterzhu/pulse-adapters 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +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/normalize.js +220 -3
- package/dist/providers/openai-compat.d.ts +1 -1
- package/dist/providers/openai-compat.js +27 -88
- package/dist/tools/filesystem.js +13 -4
- package/dist/tools/shell.js +8 -0
- package/package.json +3 -3
|
@@ -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 {};
|
|
@@ -116,10 +116,31 @@ export function normalizeOpenAIResponse(response, toolNameAliases, provider = 'o
|
|
|
116
116
|
throw providerResponseError('OpenAI response must contain at least one choice');
|
|
117
117
|
const choice = providerRecord(root.choices[0], 'OpenAI choice');
|
|
118
118
|
const message = providerRecord(choice.message, 'OpenAI message');
|
|
119
|
-
const
|
|
119
|
+
const nativeToolCalls = choice.finish_reason === 'length' || message.tool_calls === undefined ? [] : normalizeOpenAIToolCalls(message.tool_calls, toolNameAliases);
|
|
120
120
|
const refusal = message.refusal == null ? undefined : requiredProviderString(message.refusal, 'OpenAI refusal');
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
let text = providerText(message.content, 'OpenAI message content');
|
|
122
|
+
let finishReason = normalizeOpenAIFinishReason(choice.finish_reason, refusal, nativeToolCalls.length > 0);
|
|
123
|
+
let toolCalls = nativeToolCalls;
|
|
124
|
+
if (provider === 'deepseek' && (finishReason === 'stop' || nativeToolCalls.length > 0)) {
|
|
125
|
+
const recovered = recoverDeepSeekDsml(text, toolNameAliases, nativeToolCalls.length === 0);
|
|
126
|
+
text = recovered.text;
|
|
127
|
+
if (nativeToolCalls.length === 0 && recovered.truncated) {
|
|
128
|
+
toolCalls = [];
|
|
129
|
+
finishReason = 'length';
|
|
130
|
+
}
|
|
131
|
+
else if (nativeToolCalls.length === 0 && recovered.calls.length > 0) {
|
|
132
|
+
toolCalls = recovered.calls.map((call, index) => ({ toolCallId: `pulse-tool-${index + 1}`, providerToolCallId: `dsml-${index + 1}`, providerToolName: call.providerToolName, providerToolArguments: call.providerToolArguments, name: call.name, input: call.input }));
|
|
133
|
+
finishReason = 'tool_calls';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (provider === 'deepseek' && nativeToolCalls.length === 0 && finishReason === 'stop' && toolCalls.length === 0) {
|
|
137
|
+
const jsonCalls = recoverDeepSeekJsonToolCalls(text, toolNameAliases);
|
|
138
|
+
if (jsonCalls !== undefined) {
|
|
139
|
+
text = '';
|
|
140
|
+
toolCalls = jsonCalls.map((call, index) => ({ toolCallId: `pulse-tool-${index + 1}`, providerToolCallId: `json-${index + 1}`, providerToolName: call.providerToolName, providerToolArguments: call.providerToolArguments, name: call.name, input: call.input }));
|
|
141
|
+
finishReason = 'tool_calls';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
123
144
|
const rawUsage = root.usage === undefined ? undefined : providerRecord(root.usage, 'OpenAI usage');
|
|
124
145
|
const promptDetails = rawUsage?.prompt_tokens_details === undefined ? undefined : providerRecord(rawUsage.prompt_tokens_details, 'OpenAI prompt token details');
|
|
125
146
|
const usage = normalizeUsage(rawUsage === undefined ? undefined : { ...rawUsage, cached_tokens: rawUsage.cached_tokens ?? promptDetails?.cached_tokens ?? rawUsage.cache_read_input_tokens, reasoning_tokens: rawUsage.completion_tokens_details?.reasoning_tokens ?? rawUsage.reasoning_tokens }, { input: 'prompt_tokens', output: 'completion_tokens', cached: 'cached_tokens', reasoning: 'reasoning_tokens' }, 'OpenAI');
|
|
@@ -127,6 +148,202 @@ export function normalizeOpenAIResponse(response, toolNameAliases, provider = 'o
|
|
|
127
148
|
const measuredUsage = usage === undefined ? undefined : { ...usage, visibleOutputChars: text.length };
|
|
128
149
|
return { text, ...(parseStructured(text) === undefined ? {} : { structured: parseStructured(text) }), toolCalls, ...(refusal === undefined ? {} : { refusal }), finishReason, ...(measuredUsage === undefined ? {} : { usage: measuredUsage }), ...(provider !== 'deepseek' || reasoningContent === undefined ? {} : { providerContinuation: { provider, reasoningContent } }) };
|
|
129
150
|
}
|
|
151
|
+
/** DeepSeek V4/V4.1 may emit DSML tool markup in content instead of tool_calls. One or more U+FF5C bars are accepted. */
|
|
152
|
+
function recoverDeepSeekDsml(text, toolNameAliases, parseCalls) {
|
|
153
|
+
const blocks = takeDsmlBlocks(text);
|
|
154
|
+
if (!blocks.changed)
|
|
155
|
+
return { text, calls: [], truncated: false };
|
|
156
|
+
if (blocks.truncated || !parseCalls)
|
|
157
|
+
return { text: blocks.text, calls: [], truncated: blocks.truncated };
|
|
158
|
+
const calls = [];
|
|
159
|
+
for (const inner of blocks.inners) {
|
|
160
|
+
const parsed = parseDsmlInvokes(inner, toolNameAliases);
|
|
161
|
+
if (parsed === undefined)
|
|
162
|
+
return { text: blocks.text, calls: [], truncated: true };
|
|
163
|
+
calls.push(...parsed);
|
|
164
|
+
}
|
|
165
|
+
return { text: blocks.text, calls, truncated: false };
|
|
166
|
+
}
|
|
167
|
+
function takeDsmlBlocks(text) {
|
|
168
|
+
const inners = [];
|
|
169
|
+
let cursor = 0;
|
|
170
|
+
let cleaned = '';
|
|
171
|
+
let changed = false;
|
|
172
|
+
while (cursor < text.length) {
|
|
173
|
+
const open = nextDsmlMatch(DSML_BLOCK_OPEN, text, cursor);
|
|
174
|
+
if (open === undefined)
|
|
175
|
+
break;
|
|
176
|
+
const close = nextDsmlMatch(DSML_BLOCK_CLOSE, text, open.end);
|
|
177
|
+
const nested = nextDsmlMatch(DSML_BLOCK_OPEN, text, open.end);
|
|
178
|
+
cleaned += text.slice(cursor, open.start);
|
|
179
|
+
changed = true;
|
|
180
|
+
if (close === undefined || (nested !== undefined && nested.start < close.start)) {
|
|
181
|
+
return { text: cleaned.trim(), inners: [], truncated: true, changed: true };
|
|
182
|
+
}
|
|
183
|
+
inners.push(text.slice(open.end, close.start));
|
|
184
|
+
cursor = close.end;
|
|
185
|
+
}
|
|
186
|
+
if (!changed)
|
|
187
|
+
return { text, inners, truncated: false, changed: false };
|
|
188
|
+
cleaned += text.slice(cursor);
|
|
189
|
+
return { text: cleaned.trim(), inners, truncated: false, changed: true };
|
|
190
|
+
}
|
|
191
|
+
function parseDsmlInvokes(inner, toolNameAliases) {
|
|
192
|
+
const opened = countDsmlTags(DSML_INVOKE_OPEN, inner);
|
|
193
|
+
if (opened !== countDsmlTags(DSML_INVOKE_CLOSE, inner))
|
|
194
|
+
return undefined;
|
|
195
|
+
const calls = [];
|
|
196
|
+
const invoke = new RegExp(DSML_INVOKE_PATTERN.source, 'g');
|
|
197
|
+
let match;
|
|
198
|
+
while ((match = invoke.exec(inner)) !== null) {
|
|
199
|
+
const providerToolName = requiredProviderString(match[1], 'OpenAI tool name');
|
|
200
|
+
const parameters = parseDsmlParameters(match[2] ?? '');
|
|
201
|
+
if (parameters === undefined)
|
|
202
|
+
return undefined;
|
|
203
|
+
calls.push({ providerToolName, name: toolNameAliases?.get(providerToolName) ?? providerToolName, input: parameters, providerToolArguments: JSON.stringify(parameters) });
|
|
204
|
+
}
|
|
205
|
+
return calls.length === opened ? calls : undefined;
|
|
206
|
+
}
|
|
207
|
+
function parseDsmlParameters(body) {
|
|
208
|
+
const opened = countDsmlTags(DSML_PARAMETER_OPEN, body);
|
|
209
|
+
if (opened !== countDsmlTags(DSML_PARAMETER_CLOSE, body))
|
|
210
|
+
return undefined;
|
|
211
|
+
const input = {};
|
|
212
|
+
const parameter = new RegExp(DSML_PARAMETER_PATTERN.source, 'g');
|
|
213
|
+
let match;
|
|
214
|
+
while ((match = parameter.exec(body)) !== null) {
|
|
215
|
+
const name = requiredProviderString(match[1], 'OpenAI tool name');
|
|
216
|
+
if (match[2] === 'true')
|
|
217
|
+
input[name] = match[3] ?? '';
|
|
218
|
+
else {
|
|
219
|
+
const parsed = parseDsmlJson(match[3] ?? '');
|
|
220
|
+
if (parsed === undefined)
|
|
221
|
+
return undefined;
|
|
222
|
+
input[name] = parsed;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return Object.keys(input).length === opened ? input : undefined;
|
|
226
|
+
}
|
|
227
|
+
function parseDsmlJson(value) {
|
|
228
|
+
try {
|
|
229
|
+
return JSON.parse(value);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
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. */
|
|
236
|
+
function recoverDeepSeekJsonToolCalls(text, toolNameAliases) {
|
|
237
|
+
const leading = takeLeadingJsonValues(text.trim());
|
|
238
|
+
if (leading === undefined || leading.values.length === 0 || leading.rest.trim().length > 0)
|
|
239
|
+
return undefined;
|
|
240
|
+
const objects = leading.values.filter((value) => !isEmptyJsonValue(value));
|
|
241
|
+
if (objects.length === 0)
|
|
242
|
+
return undefined;
|
|
243
|
+
if (objects.length === 1 && objects[0] && typeof objects[0] === 'object' && !Array.isArray(objects[0])) {
|
|
244
|
+
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;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const calls = objects.map((value) => jsonToolCall(value, toolNameAliases));
|
|
251
|
+
return calls.every((call) => call !== undefined) ? calls : undefined;
|
|
252
|
+
}
|
|
253
|
+
function isEmptyJsonValue(value) {
|
|
254
|
+
if (Array.isArray(value))
|
|
255
|
+
return value.length === 0;
|
|
256
|
+
return value !== null && typeof value === 'object' && Object.keys(value).length === 0;
|
|
257
|
+
}
|
|
258
|
+
function jsonToolCall(value, toolNameAliases) {
|
|
259
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
260
|
+
return undefined;
|
|
261
|
+
const record = value;
|
|
262
|
+
const named = typeof record.tool === 'string' && record.tool.length > 0 ? record.tool : typeof record.name === 'string' && record.name.length > 0 ? record.name : undefined;
|
|
263
|
+
const providerToolName = named;
|
|
264
|
+
if (providerToolName === undefined)
|
|
265
|
+
return undefined;
|
|
266
|
+
const raw = record.arguments ?? record.input ?? record.parameters;
|
|
267
|
+
const input = raw && typeof raw === 'object' && !Array.isArray(raw) ? { ...raw } : { ...record };
|
|
268
|
+
if (raw === undefined) {
|
|
269
|
+
delete input.tool;
|
|
270
|
+
delete input.name;
|
|
271
|
+
}
|
|
272
|
+
return { providerToolName, name: toolNameAliases?.get(providerToolName) ?? providerToolName, input, providerToolArguments: JSON.stringify(input) };
|
|
273
|
+
}
|
|
274
|
+
function takeLeadingJsonValues(text) {
|
|
275
|
+
if (!text.startsWith('{') && !text.startsWith('['))
|
|
276
|
+
return undefined;
|
|
277
|
+
const values = [];
|
|
278
|
+
let cursor = 0;
|
|
279
|
+
while (cursor < text.length) {
|
|
280
|
+
while (cursor < text.length && /\s/.test(text[cursor]))
|
|
281
|
+
cursor++;
|
|
282
|
+
if (cursor >= text.length)
|
|
283
|
+
break;
|
|
284
|
+
if (text[cursor] !== '{' && text[cursor] !== '[')
|
|
285
|
+
break;
|
|
286
|
+
const end = jsonValueEnd(text, cursor);
|
|
287
|
+
if (end === undefined)
|
|
288
|
+
return undefined;
|
|
289
|
+
try {
|
|
290
|
+
values.push(JSON.parse(text.slice(cursor, end)));
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
cursor = end;
|
|
296
|
+
}
|
|
297
|
+
return values.length > 0 ? { values, rest: text.slice(cursor) } : undefined;
|
|
298
|
+
}
|
|
299
|
+
function jsonValueEnd(text, start) {
|
|
300
|
+
const opening = text[start];
|
|
301
|
+
if (opening !== '{' && opening !== '[')
|
|
302
|
+
return undefined;
|
|
303
|
+
const stack = [];
|
|
304
|
+
let inString = false;
|
|
305
|
+
let escaped = false;
|
|
306
|
+
for (let index = start; index < text.length; index++) {
|
|
307
|
+
const char = text[index];
|
|
308
|
+
if (inString) {
|
|
309
|
+
if (escaped)
|
|
310
|
+
escaped = false;
|
|
311
|
+
else if (char === '\\')
|
|
312
|
+
escaped = true;
|
|
313
|
+
else if (char === '"')
|
|
314
|
+
inString = false;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (char === '"') {
|
|
318
|
+
inString = true;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (char === '{' || char === '[')
|
|
322
|
+
stack.push(char === '{' ? '}' : ']');
|
|
323
|
+
else if (char === '}' || char === ']') {
|
|
324
|
+
if (stack.pop() !== char)
|
|
325
|
+
return undefined;
|
|
326
|
+
if (stack.length === 0)
|
|
327
|
+
return index + 1;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
function countDsmlTags(pattern, value) {
|
|
333
|
+
return value.match(new RegExp(pattern.source, 'g'))?.length ?? 0;
|
|
334
|
+
}
|
|
335
|
+
function nextDsmlMatch(pattern, value, from) {
|
|
336
|
+
const match = new RegExp(pattern.source, 'g').exec(value.slice(from));
|
|
337
|
+
return match === null ? undefined : { start: from + match.index, end: from + match.index + match[0].length };
|
|
338
|
+
}
|
|
339
|
+
const DSML_BLOCK_OPEN = /<\uFF5C+DSML\uFF5C+\s*(?:tool_calls|calls)>/;
|
|
340
|
+
const DSML_BLOCK_CLOSE = /<\/\uFF5C+DSML\uFF5C+\s*(?:tool_calls|calls)>/;
|
|
341
|
+
const DSML_INVOKE_OPEN = /<\uFF5C+DSML\uFF5C+\s*invoke\b/;
|
|
342
|
+
const DSML_INVOKE_CLOSE = /<\/\uFF5C+DSML\uFF5C+\s*invoke>/;
|
|
343
|
+
const DSML_INVOKE_PATTERN = /<\uFF5C+DSML\uFF5C+\s*invoke\s+name="([^"]+)"\s*\/?>([\s\S]*?)<\/\uFF5C+DSML\uFF5C+\s*invoke>/;
|
|
344
|
+
const DSML_PARAMETER_OPEN = /<\uFF5C+DSML\uFF5C+\s*parameter\b/;
|
|
345
|
+
const DSML_PARAMETER_CLOSE = /<\/\uFF5C+DSML\uFF5C+\s*parameter>/;
|
|
346
|
+
const DSML_PARAMETER_PATTERN = /<\uFF5C+DSML\uFF5C+\s*parameter\s+name="([^"]+)"\s+string="(true|false)"\s*>([\s\S]*?)<\/\uFF5C+DSML\uFF5C+\s*parameter>/;
|
|
130
347
|
export function normalizeAnthropicResponse(response) {
|
|
131
348
|
const root = providerRecord(response, 'Anthropic response');
|
|
132
349
|
if (!Array.isArray(root.content))
|
|
@@ -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/dist/tools/filesystem.js
CHANGED
|
@@ -31,8 +31,14 @@ export class FilesystemTool {
|
|
|
31
31
|
this.root = root;
|
|
32
32
|
this.lockTimeoutMs = lockTimeoutMs;
|
|
33
33
|
}
|
|
34
|
-
safe(path) {
|
|
35
|
-
|
|
34
|
+
safe(path) {
|
|
35
|
+
const root = resolve(this.root);
|
|
36
|
+
const target = resolve(root, path);
|
|
37
|
+
const within = relative(root, target);
|
|
38
|
+
if (within.startsWith('..') || isAbsolute(within))
|
|
39
|
+
throw filesystemError('PATH_OUTSIDE_SANDBOX');
|
|
40
|
+
return target;
|
|
41
|
+
}
|
|
36
42
|
async existing(path) {
|
|
37
43
|
const target = this.safe(path);
|
|
38
44
|
const [root, resolved] = await Promise.all([realpath(this.root), realpath(target)]);
|
|
@@ -122,8 +128,11 @@ export class FilesystemTool {
|
|
|
122
128
|
await handle.close();
|
|
123
129
|
}
|
|
124
130
|
}
|
|
125
|
-
async list(path = '.', signal) {
|
|
126
|
-
|
|
131
|
+
async list(path = '.', signal) {
|
|
132
|
+
if (signal?.aborted)
|
|
133
|
+
throw filesystemError('ABORTED');
|
|
134
|
+
return readdir(await this.existing(path));
|
|
135
|
+
}
|
|
127
136
|
async write(path, content, signal) { if (signal?.aborted)
|
|
128
137
|
throw filesystemError('ABORTED'); await writeFile(await this.writable(path), content, 'utf8'); }
|
|
129
138
|
async move(source, destination, expectedHash, signal) {
|