extrait 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/index.cjs +701 -35
- package/dist/index.js +701 -35
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1597,8 +1597,14 @@ function createOpenAICompatibleAdapter(options) {
|
|
|
1597
1597
|
async stream(request, callbacks = {}) {
|
|
1598
1598
|
const usesResponses = shouldUseResponsesAPI(options, request);
|
|
1599
1599
|
const usesMCP = hasMCPClients(request.mcpClients);
|
|
1600
|
-
if (usesResponses
|
|
1601
|
-
|
|
1600
|
+
if (usesResponses) {
|
|
1601
|
+
if (usesMCP) {
|
|
1602
|
+
return streamWithResponsesAPIWithMCP(options, fetcher, responsesPath, request, callbacks);
|
|
1603
|
+
}
|
|
1604
|
+
return streamWithResponsesAPIPassThrough(options, fetcher, responsesPath, request, callbacks);
|
|
1605
|
+
}
|
|
1606
|
+
if (usesMCP) {
|
|
1607
|
+
return streamWithChatCompletionsWithMCP(options, fetcher, path, request, callbacks);
|
|
1602
1608
|
}
|
|
1603
1609
|
const response = await fetcher(buildURL(options.baseURL, path), {
|
|
1604
1610
|
method: "POST",
|
|
@@ -1883,6 +1889,316 @@ async function completeWithResponsesAPIWithMCP(options, fetcher, path, request)
|
|
|
1883
1889
|
toolExecutions: toolExecutions.length > 0 ? toolExecutions : undefined
|
|
1884
1890
|
};
|
|
1885
1891
|
}
|
|
1892
|
+
async function streamWithChatCompletionsWithMCP(options, fetcher, path, request, callbacks) {
|
|
1893
|
+
const maxToolRounds = normalizeMaxToolRounds(request.maxToolRounds ?? options.defaultMaxToolRounds);
|
|
1894
|
+
let messages = buildMessages(request);
|
|
1895
|
+
let aggregatedUsage;
|
|
1896
|
+
let finishReason;
|
|
1897
|
+
let lastPayload;
|
|
1898
|
+
const executedToolCalls = [];
|
|
1899
|
+
const toolExecutions = [];
|
|
1900
|
+
callbacks.onStart?.();
|
|
1901
|
+
for (let round = 1;round <= maxToolRounds + 1; round += 1) {
|
|
1902
|
+
const mcpToolset = await resolveMCPToolset(request.mcpClients);
|
|
1903
|
+
const transportTools = toProviderFunctionTools(mcpToolset);
|
|
1904
|
+
const response = await fetcher(buildURL(options.baseURL, path), {
|
|
1905
|
+
method: "POST",
|
|
1906
|
+
headers: buildHeaders(options),
|
|
1907
|
+
body: JSON.stringify(cleanUndefined({
|
|
1908
|
+
...options.defaultBody,
|
|
1909
|
+
...request.body,
|
|
1910
|
+
model: options.model,
|
|
1911
|
+
messages,
|
|
1912
|
+
temperature: request.temperature,
|
|
1913
|
+
max_tokens: request.maxTokens,
|
|
1914
|
+
tools: transportTools,
|
|
1915
|
+
tool_choice: request.toolChoice,
|
|
1916
|
+
parallel_tool_calls: request.parallelToolCalls,
|
|
1917
|
+
stream: true
|
|
1918
|
+
}))
|
|
1919
|
+
});
|
|
1920
|
+
if (!response.ok) {
|
|
1921
|
+
const message = await response.text();
|
|
1922
|
+
throw new Error(`HTTP ${response.status}: ${message}`);
|
|
1923
|
+
}
|
|
1924
|
+
let roundText = "";
|
|
1925
|
+
let roundUsage;
|
|
1926
|
+
let roundFinishReason;
|
|
1927
|
+
const streamedToolCalls = new Map;
|
|
1928
|
+
await consumeSSE(response, (data) => {
|
|
1929
|
+
if (data === "[DONE]") {
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
const json = safeJSONParse(data);
|
|
1933
|
+
if (!isRecord2(json)) {
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
lastPayload = json;
|
|
1937
|
+
const delta = pickAssistantDelta(json);
|
|
1938
|
+
const chunkUsage = pickUsage(json);
|
|
1939
|
+
const chunkFinishReason = pickFinishReason(json);
|
|
1940
|
+
collectOpenAIStreamToolCalls(json, streamedToolCalls);
|
|
1941
|
+
roundUsage = mergeUsage(roundUsage, chunkUsage);
|
|
1942
|
+
if (chunkFinishReason) {
|
|
1943
|
+
roundFinishReason = chunkFinishReason;
|
|
1944
|
+
}
|
|
1945
|
+
if (delta) {
|
|
1946
|
+
roundText += delta;
|
|
1947
|
+
callbacks.onToken?.(delta);
|
|
1948
|
+
}
|
|
1949
|
+
if (delta || chunkUsage || chunkFinishReason) {
|
|
1950
|
+
const chunk = {
|
|
1951
|
+
textDelta: delta,
|
|
1952
|
+
raw: json,
|
|
1953
|
+
usage: chunkUsage,
|
|
1954
|
+
finishReason: chunkFinishReason
|
|
1955
|
+
};
|
|
1956
|
+
callbacks.onChunk?.(chunk);
|
|
1957
|
+
}
|
|
1958
|
+
});
|
|
1959
|
+
aggregatedUsage = mergeUsage(aggregatedUsage, roundUsage);
|
|
1960
|
+
if (roundFinishReason) {
|
|
1961
|
+
finishReason = roundFinishReason;
|
|
1962
|
+
}
|
|
1963
|
+
const calledTools = buildOpenAIStreamToolCalls(streamedToolCalls);
|
|
1964
|
+
if (calledTools.length === 0) {
|
|
1965
|
+
const out2 = {
|
|
1966
|
+
text: roundText,
|
|
1967
|
+
raw: lastPayload,
|
|
1968
|
+
usage: aggregatedUsage,
|
|
1969
|
+
finishReason,
|
|
1970
|
+
toolCalls: executedToolCalls.length > 0 ? executedToolCalls : undefined,
|
|
1971
|
+
toolExecutions: toolExecutions.length > 0 ? toolExecutions : undefined
|
|
1972
|
+
};
|
|
1973
|
+
callbacks.onComplete?.(out2);
|
|
1974
|
+
return out2;
|
|
1975
|
+
}
|
|
1976
|
+
if (round > maxToolRounds) {
|
|
1977
|
+
throw new Error(`Tool call loop exceeded maxToolRounds (${maxToolRounds}).`);
|
|
1978
|
+
}
|
|
1979
|
+
const outputs = await executeMCPToolCalls(calledTools, mcpToolset, {
|
|
1980
|
+
round,
|
|
1981
|
+
request,
|
|
1982
|
+
provider: "openai-compatible",
|
|
1983
|
+
model: options.model
|
|
1984
|
+
});
|
|
1985
|
+
executedToolCalls.push(...outputs.map((entry) => entry.call));
|
|
1986
|
+
toolExecutions.push(...outputs.map((entry) => entry.execution));
|
|
1987
|
+
const assistantMessage = buildOpenAIAssistantToolMessage(roundText, calledTools);
|
|
1988
|
+
const toolMessages = outputs.map((entry) => ({
|
|
1989
|
+
role: "tool",
|
|
1990
|
+
tool_call_id: entry.call.id,
|
|
1991
|
+
content: stringifyToolOutput(entry.call.error ? { error: entry.call.error } : entry.call.output)
|
|
1992
|
+
}));
|
|
1993
|
+
messages = [...messages, assistantMessage, ...toolMessages];
|
|
1994
|
+
}
|
|
1995
|
+
const out = {
|
|
1996
|
+
text: "",
|
|
1997
|
+
raw: lastPayload,
|
|
1998
|
+
usage: aggregatedUsage,
|
|
1999
|
+
finishReason,
|
|
2000
|
+
toolCalls: executedToolCalls.length > 0 ? executedToolCalls : undefined,
|
|
2001
|
+
toolExecutions: toolExecutions.length > 0 ? toolExecutions : undefined
|
|
2002
|
+
};
|
|
2003
|
+
callbacks.onComplete?.(out);
|
|
2004
|
+
return out;
|
|
2005
|
+
}
|
|
2006
|
+
async function streamWithResponsesAPIPassThrough(options, fetcher, path, request, callbacks) {
|
|
2007
|
+
const body = isRecord2(request.body) ? request.body : undefined;
|
|
2008
|
+
const response = await fetcher(buildURL(options.baseURL, path), {
|
|
2009
|
+
method: "POST",
|
|
2010
|
+
headers: buildHeaders(options),
|
|
2011
|
+
body: JSON.stringify(cleanUndefined({
|
|
2012
|
+
...options.defaultBody,
|
|
2013
|
+
...request.body,
|
|
2014
|
+
model: options.model,
|
|
2015
|
+
input: buildResponsesInput(request),
|
|
2016
|
+
previous_response_id: pickString(body?.previous_response_id),
|
|
2017
|
+
temperature: request.temperature,
|
|
2018
|
+
max_output_tokens: request.maxTokens,
|
|
2019
|
+
stream: true
|
|
2020
|
+
}))
|
|
2021
|
+
});
|
|
2022
|
+
if (!response.ok) {
|
|
2023
|
+
const message = await response.text();
|
|
2024
|
+
throw new Error(`HTTP ${response.status}: ${message}`);
|
|
2025
|
+
}
|
|
2026
|
+
callbacks.onStart?.();
|
|
2027
|
+
let text = "";
|
|
2028
|
+
let usage;
|
|
2029
|
+
let finishReason;
|
|
2030
|
+
let lastPayload;
|
|
2031
|
+
await consumeSSE(response, (data) => {
|
|
2032
|
+
if (data === "[DONE]") {
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
const json = safeJSONParse(data);
|
|
2036
|
+
if (!isRecord2(json)) {
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
const roundPayload = pickResponsesStreamPayload(json);
|
|
2040
|
+
if (roundPayload) {
|
|
2041
|
+
lastPayload = roundPayload;
|
|
2042
|
+
}
|
|
2043
|
+
const delta = pickResponsesStreamTextDelta(json);
|
|
2044
|
+
const chunkUsage = pickResponsesStreamUsage(json);
|
|
2045
|
+
const chunkFinishReason = pickResponsesStreamFinishReason(json);
|
|
2046
|
+
usage = mergeUsage(usage, chunkUsage);
|
|
2047
|
+
if (chunkFinishReason) {
|
|
2048
|
+
finishReason = chunkFinishReason;
|
|
2049
|
+
}
|
|
2050
|
+
if (delta) {
|
|
2051
|
+
text += delta;
|
|
2052
|
+
callbacks.onToken?.(delta);
|
|
2053
|
+
}
|
|
2054
|
+
if (delta || chunkUsage || chunkFinishReason) {
|
|
2055
|
+
const chunk = {
|
|
2056
|
+
textDelta: delta,
|
|
2057
|
+
raw: json,
|
|
2058
|
+
usage: chunkUsage,
|
|
2059
|
+
finishReason: chunkFinishReason
|
|
2060
|
+
};
|
|
2061
|
+
callbacks.onChunk?.(chunk);
|
|
2062
|
+
}
|
|
2063
|
+
});
|
|
2064
|
+
const finalPayload = lastPayload ?? {};
|
|
2065
|
+
const out = {
|
|
2066
|
+
text: text.length > 0 ? text : pickResponsesText(finalPayload) || pickAssistantText(finalPayload),
|
|
2067
|
+
raw: finalPayload,
|
|
2068
|
+
usage: mergeUsage(usage, pickUsage(finalPayload)),
|
|
2069
|
+
finishReason: finishReason ?? pickResponsesFinishReason(finalPayload) ?? pickFinishReason(finalPayload)
|
|
2070
|
+
};
|
|
2071
|
+
callbacks.onComplete?.(out);
|
|
2072
|
+
return out;
|
|
2073
|
+
}
|
|
2074
|
+
async function streamWithResponsesAPIWithMCP(options, fetcher, path, request, callbacks) {
|
|
2075
|
+
const maxToolRounds = normalizeMaxToolRounds(request.maxToolRounds ?? options.defaultMaxToolRounds);
|
|
2076
|
+
let input = buildResponsesInput(request);
|
|
2077
|
+
let previousResponseId = pickString(isRecord2(request.body) ? request.body.previous_response_id : undefined);
|
|
2078
|
+
let aggregatedUsage;
|
|
2079
|
+
let finishReason;
|
|
2080
|
+
let lastPayload;
|
|
2081
|
+
const executedToolCalls = [];
|
|
2082
|
+
const toolExecutions = [];
|
|
2083
|
+
callbacks.onStart?.();
|
|
2084
|
+
for (let round = 1;round <= maxToolRounds + 1; round += 1) {
|
|
2085
|
+
const mcpToolset = await resolveMCPToolset(request.mcpClients);
|
|
2086
|
+
const transportTools = toResponsesTools(toProviderFunctionTools(mcpToolset));
|
|
2087
|
+
const response = await fetcher(buildURL(options.baseURL, path), {
|
|
2088
|
+
method: "POST",
|
|
2089
|
+
headers: buildHeaders(options),
|
|
2090
|
+
body: JSON.stringify(cleanUndefined({
|
|
2091
|
+
...options.defaultBody,
|
|
2092
|
+
...request.body,
|
|
2093
|
+
model: options.model,
|
|
2094
|
+
input,
|
|
2095
|
+
previous_response_id: previousResponseId,
|
|
2096
|
+
temperature: request.temperature,
|
|
2097
|
+
max_output_tokens: request.maxTokens,
|
|
2098
|
+
tools: transportTools,
|
|
2099
|
+
tool_choice: request.toolChoice,
|
|
2100
|
+
parallel_tool_calls: request.parallelToolCalls,
|
|
2101
|
+
stream: true
|
|
2102
|
+
}))
|
|
2103
|
+
});
|
|
2104
|
+
if (!response.ok) {
|
|
2105
|
+
const message = await response.text();
|
|
2106
|
+
throw new Error(`HTTP ${response.status}: ${message}`);
|
|
2107
|
+
}
|
|
2108
|
+
let roundText = "";
|
|
2109
|
+
let roundUsage;
|
|
2110
|
+
let roundFinishReason;
|
|
2111
|
+
let roundPayload;
|
|
2112
|
+
const streamedToolCalls = new Map;
|
|
2113
|
+
await consumeSSE(response, (data) => {
|
|
2114
|
+
if (data === "[DONE]") {
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
const json = safeJSONParse(data);
|
|
2118
|
+
if (!isRecord2(json)) {
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
const payload = pickResponsesStreamPayload(json);
|
|
2122
|
+
if (payload) {
|
|
2123
|
+
roundPayload = payload;
|
|
2124
|
+
lastPayload = payload;
|
|
2125
|
+
}
|
|
2126
|
+
const delta = pickResponsesStreamTextDelta(json);
|
|
2127
|
+
const chunkUsage = pickResponsesStreamUsage(json);
|
|
2128
|
+
const chunkFinishReason = pickResponsesStreamFinishReason(json);
|
|
2129
|
+
collectResponsesStreamToolCalls(json, streamedToolCalls);
|
|
2130
|
+
roundUsage = mergeUsage(roundUsage, chunkUsage);
|
|
2131
|
+
if (chunkFinishReason) {
|
|
2132
|
+
roundFinishReason = chunkFinishReason;
|
|
2133
|
+
}
|
|
2134
|
+
if (delta) {
|
|
2135
|
+
roundText += delta;
|
|
2136
|
+
callbacks.onToken?.(delta);
|
|
2137
|
+
}
|
|
2138
|
+
if (delta || chunkUsage || chunkFinishReason) {
|
|
2139
|
+
const chunk = {
|
|
2140
|
+
textDelta: delta,
|
|
2141
|
+
raw: json,
|
|
2142
|
+
usage: chunkUsage,
|
|
2143
|
+
finishReason: chunkFinishReason
|
|
2144
|
+
};
|
|
2145
|
+
callbacks.onChunk?.(chunk);
|
|
2146
|
+
}
|
|
2147
|
+
});
|
|
2148
|
+
aggregatedUsage = mergeUsage(aggregatedUsage, roundUsage);
|
|
2149
|
+
const payloadUsage = roundPayload ? pickUsage(roundPayload) : undefined;
|
|
2150
|
+
aggregatedUsage = mergeUsage(aggregatedUsage, payloadUsage);
|
|
2151
|
+
if (roundFinishReason) {
|
|
2152
|
+
finishReason = roundFinishReason;
|
|
2153
|
+
} else if (roundPayload) {
|
|
2154
|
+
finishReason = pickResponsesFinishReason(roundPayload) ?? finishReason;
|
|
2155
|
+
}
|
|
2156
|
+
const payloadToolCalls = roundPayload ? pickResponsesToolCalls(roundPayload) : [];
|
|
2157
|
+
const streamedCalls = buildResponsesStreamToolCalls(streamedToolCalls);
|
|
2158
|
+
const providerToolCalls = payloadToolCalls.length > 0 ? payloadToolCalls : streamedCalls;
|
|
2159
|
+
const functionCalls = providerToolCalls.filter((toolCall) => toolCall.type === "function" && typeof toolCall.id === "string" && typeof toolCall.name === "string");
|
|
2160
|
+
if (functionCalls.length === 0) {
|
|
2161
|
+
const finalText = roundText.length > 0 ? roundText : roundPayload ? pickResponsesText(roundPayload) || pickAssistantText(roundPayload) : "";
|
|
2162
|
+
const out2 = {
|
|
2163
|
+
text: finalText,
|
|
2164
|
+
raw: roundPayload ?? lastPayload,
|
|
2165
|
+
usage: aggregatedUsage,
|
|
2166
|
+
finishReason,
|
|
2167
|
+
toolCalls: executedToolCalls.length > 0 ? executedToolCalls : undefined,
|
|
2168
|
+
toolExecutions: toolExecutions.length > 0 ? toolExecutions : undefined
|
|
2169
|
+
};
|
|
2170
|
+
callbacks.onComplete?.(out2);
|
|
2171
|
+
return out2;
|
|
2172
|
+
}
|
|
2173
|
+
if (round > maxToolRounds) {
|
|
2174
|
+
throw new Error(`Tool call loop exceeded maxToolRounds (${maxToolRounds}).`);
|
|
2175
|
+
}
|
|
2176
|
+
const outputs = await executeMCPToolCalls(functionCalls, mcpToolset, {
|
|
2177
|
+
round,
|
|
2178
|
+
request,
|
|
2179
|
+
provider: "openai-compatible",
|
|
2180
|
+
model: options.model
|
|
2181
|
+
});
|
|
2182
|
+
executedToolCalls.push(...outputs.map((entry) => entry.call));
|
|
2183
|
+
toolExecutions.push(...outputs.map((entry) => entry.execution));
|
|
2184
|
+
input = outputs.map((entry) => ({
|
|
2185
|
+
type: "function_call_output",
|
|
2186
|
+
call_id: entry.call.id,
|
|
2187
|
+
output: stringifyToolOutput(entry.call.error ? { error: entry.call.error } : entry.call.output)
|
|
2188
|
+
}));
|
|
2189
|
+
previousResponseId = pickString(roundPayload?.id);
|
|
2190
|
+
}
|
|
2191
|
+
const out = {
|
|
2192
|
+
text: pickResponsesText(lastPayload ?? {}) || pickAssistantText(lastPayload ?? {}),
|
|
2193
|
+
raw: lastPayload,
|
|
2194
|
+
usage: aggregatedUsage,
|
|
2195
|
+
finishReason,
|
|
2196
|
+
toolCalls: executedToolCalls.length > 0 ? executedToolCalls : undefined,
|
|
2197
|
+
toolExecutions: toolExecutions.length > 0 ? toolExecutions : undefined
|
|
2198
|
+
};
|
|
2199
|
+
callbacks.onComplete?.(out);
|
|
2200
|
+
return out;
|
|
2201
|
+
}
|
|
1886
2202
|
function shouldUseResponsesAPI(options, request) {
|
|
1887
2203
|
if (options.path?.includes("/responses")) {
|
|
1888
2204
|
return true;
|
|
@@ -2039,6 +2355,190 @@ function pickAssistantDelta(payload) {
|
|
|
2039
2355
|
}
|
|
2040
2356
|
return "";
|
|
2041
2357
|
}
|
|
2358
|
+
function collectOpenAIStreamToolCalls(payload, state) {
|
|
2359
|
+
const choices = payload.choices;
|
|
2360
|
+
if (!Array.isArray(choices) || choices.length === 0 || !isRecord2(choices[0])) {
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
2363
|
+
const delta = choices[0].delta;
|
|
2364
|
+
if (!isRecord2(delta) || !Array.isArray(delta.tool_calls)) {
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2367
|
+
for (const rawToolCall of delta.tool_calls) {
|
|
2368
|
+
if (!isRecord2(rawToolCall)) {
|
|
2369
|
+
continue;
|
|
2370
|
+
}
|
|
2371
|
+
const index = toFiniteNumber(rawToolCall.index);
|
|
2372
|
+
const toolIndex = index !== undefined ? Math.floor(index) : 0;
|
|
2373
|
+
const existing = state.get(toolIndex) ?? {
|
|
2374
|
+
index: toolIndex,
|
|
2375
|
+
argumentsText: ""
|
|
2376
|
+
};
|
|
2377
|
+
const id = pickString(rawToolCall.id);
|
|
2378
|
+
if (id) {
|
|
2379
|
+
existing.id = id;
|
|
2380
|
+
}
|
|
2381
|
+
const type = pickString(rawToolCall.type);
|
|
2382
|
+
if (type) {
|
|
2383
|
+
existing.type = type;
|
|
2384
|
+
}
|
|
2385
|
+
const functionCall = isRecord2(rawToolCall.function) ? rawToolCall.function : undefined;
|
|
2386
|
+
const name = pickString(functionCall?.name);
|
|
2387
|
+
if (name) {
|
|
2388
|
+
existing.name = `${existing.name ?? ""}${name}`;
|
|
2389
|
+
}
|
|
2390
|
+
const argumentsDelta = pickString(functionCall?.arguments);
|
|
2391
|
+
if (argumentsDelta) {
|
|
2392
|
+
existing.argumentsText += argumentsDelta;
|
|
2393
|
+
}
|
|
2394
|
+
state.set(toolIndex, existing);
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
function buildOpenAIStreamToolCalls(state) {
|
|
2398
|
+
return [...state.values()].sort((a, b) => a.index - b.index).map((entry) => ({
|
|
2399
|
+
id: entry.id ?? "",
|
|
2400
|
+
type: entry.type ?? "function",
|
|
2401
|
+
name: entry.name,
|
|
2402
|
+
arguments: entry.argumentsText.length > 0 ? entry.argumentsText : {}
|
|
2403
|
+
}));
|
|
2404
|
+
}
|
|
2405
|
+
function buildOpenAIAssistantToolMessage(text, toolCalls) {
|
|
2406
|
+
return {
|
|
2407
|
+
role: "assistant",
|
|
2408
|
+
content: text,
|
|
2409
|
+
tool_calls: toolCalls.map((call) => ({
|
|
2410
|
+
id: call.id,
|
|
2411
|
+
type: "function",
|
|
2412
|
+
function: {
|
|
2413
|
+
name: call.name,
|
|
2414
|
+
arguments: typeof call.arguments === "string" ? call.arguments : JSON.stringify(call.arguments ?? {})
|
|
2415
|
+
}
|
|
2416
|
+
}))
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
function pickResponsesStreamPayload(payload) {
|
|
2420
|
+
if (isRecord2(payload.response)) {
|
|
2421
|
+
return payload.response;
|
|
2422
|
+
}
|
|
2423
|
+
if ("output" in payload || "output_text" in payload || "status" in payload || "id" in payload) {
|
|
2424
|
+
return payload;
|
|
2425
|
+
}
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
function pickResponsesStreamTextDelta(payload) {
|
|
2429
|
+
const eventType = pickString(payload.type) ?? "";
|
|
2430
|
+
if (!eventType.includes("output_text.delta")) {
|
|
2431
|
+
return "";
|
|
2432
|
+
}
|
|
2433
|
+
const direct = pickString(payload.delta);
|
|
2434
|
+
if (direct) {
|
|
2435
|
+
return direct;
|
|
2436
|
+
}
|
|
2437
|
+
if (isRecord2(payload.delta)) {
|
|
2438
|
+
return pickString(payload.delta.text) ?? pickString(payload.delta.output_text) ?? "";
|
|
2439
|
+
}
|
|
2440
|
+
return "";
|
|
2441
|
+
}
|
|
2442
|
+
function pickResponsesStreamUsage(payload) {
|
|
2443
|
+
const direct = pickUsage(payload);
|
|
2444
|
+
if (direct) {
|
|
2445
|
+
return direct;
|
|
2446
|
+
}
|
|
2447
|
+
if (isRecord2(payload.response)) {
|
|
2448
|
+
return pickUsage(payload.response);
|
|
2449
|
+
}
|
|
2450
|
+
return;
|
|
2451
|
+
}
|
|
2452
|
+
function pickResponsesStreamFinishReason(payload) {
|
|
2453
|
+
const eventType = pickString(payload.type);
|
|
2454
|
+
if (eventType === "response.completed") {
|
|
2455
|
+
return "completed";
|
|
2456
|
+
}
|
|
2457
|
+
if (eventType === "response.failed") {
|
|
2458
|
+
return "failed";
|
|
2459
|
+
}
|
|
2460
|
+
const directStatus = pickString(payload.status);
|
|
2461
|
+
if (directStatus) {
|
|
2462
|
+
return directStatus;
|
|
2463
|
+
}
|
|
2464
|
+
if (isRecord2(payload.response)) {
|
|
2465
|
+
return pickString(payload.response.status);
|
|
2466
|
+
}
|
|
2467
|
+
return;
|
|
2468
|
+
}
|
|
2469
|
+
function collectResponsesStreamToolCalls(payload, state) {
|
|
2470
|
+
if (isRecord2(payload.response)) {
|
|
2471
|
+
collectResponsesStreamToolCallsFromOutput(payload.response.output, state);
|
|
2472
|
+
}
|
|
2473
|
+
collectResponsesStreamToolCallsFromOutput(payload.output, state);
|
|
2474
|
+
if (isRecord2(payload.item)) {
|
|
2475
|
+
const itemKey = pickString(payload.item_id) ?? pickString(payload.call_id);
|
|
2476
|
+
collectResponsesStreamToolCallsFromItem(payload.item, state, itemKey);
|
|
2477
|
+
}
|
|
2478
|
+
if (isRecord2(payload.output_item)) {
|
|
2479
|
+
const itemKey = pickString(payload.item_id) ?? pickString(payload.call_id);
|
|
2480
|
+
collectResponsesStreamToolCallsFromItem(payload.output_item, state, itemKey);
|
|
2481
|
+
}
|
|
2482
|
+
const eventType = pickString(payload.type) ?? "";
|
|
2483
|
+
if (eventType.includes("function_call_arguments.delta")) {
|
|
2484
|
+
const key = pickString(payload.item_id) ?? pickString(payload.call_id) ?? "function_call";
|
|
2485
|
+
const existing = state.get(key) ?? {
|
|
2486
|
+
key,
|
|
2487
|
+
argumentsText: ""
|
|
2488
|
+
};
|
|
2489
|
+
const delta = pickString(payload.delta) ?? (isRecord2(payload.delta) ? pickString(payload.delta.text) ?? pickString(payload.delta.arguments) : undefined) ?? pickString(payload.arguments_delta);
|
|
2490
|
+
if (delta) {
|
|
2491
|
+
existing.argumentsText += delta;
|
|
2492
|
+
}
|
|
2493
|
+
state.set(key, existing);
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
function collectResponsesStreamToolCallsFromOutput(output, state) {
|
|
2497
|
+
if (!Array.isArray(output)) {
|
|
2498
|
+
return;
|
|
2499
|
+
}
|
|
2500
|
+
for (const item of output) {
|
|
2501
|
+
if (!isRecord2(item)) {
|
|
2502
|
+
continue;
|
|
2503
|
+
}
|
|
2504
|
+
collectResponsesStreamToolCallsFromItem(item, state);
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
function collectResponsesStreamToolCallsFromItem(item, state, forcedKey) {
|
|
2508
|
+
const type = pickString(item.type);
|
|
2509
|
+
if (type !== "function_call" && !type?.includes("tool") && !type?.includes("mcp")) {
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
const key = forcedKey ?? pickString(item.call_id) ?? pickString(item.id) ?? `call_${state.size}`;
|
|
2513
|
+
const existing = state.get(key) ?? {
|
|
2514
|
+
key,
|
|
2515
|
+
argumentsText: ""
|
|
2516
|
+
};
|
|
2517
|
+
const callId = pickString(item.call_id) ?? pickString(item.id);
|
|
2518
|
+
if (callId) {
|
|
2519
|
+
existing.id = callId;
|
|
2520
|
+
}
|
|
2521
|
+
if (type) {
|
|
2522
|
+
existing.type = type;
|
|
2523
|
+
}
|
|
2524
|
+
const name = pickString(item.name);
|
|
2525
|
+
if (name) {
|
|
2526
|
+
existing.name = name;
|
|
2527
|
+
}
|
|
2528
|
+
const argumentsText = pickString(item.arguments);
|
|
2529
|
+
if (argumentsText && argumentsText.length >= existing.argumentsText.length) {
|
|
2530
|
+
existing.argumentsText = argumentsText;
|
|
2531
|
+
}
|
|
2532
|
+
state.set(key, existing);
|
|
2533
|
+
}
|
|
2534
|
+
function buildResponsesStreamToolCalls(state) {
|
|
2535
|
+
return [...state.values()].map((entry) => ({
|
|
2536
|
+
id: entry.id ?? entry.key,
|
|
2537
|
+
type: entry.type === "function_call" ? "function" : entry.type ?? "function",
|
|
2538
|
+
name: entry.name,
|
|
2539
|
+
arguments: entry.argumentsText.length > 0 ? entry.argumentsText : {}
|
|
2540
|
+
}));
|
|
2541
|
+
}
|
|
2042
2542
|
function pickResponsesText(payload) {
|
|
2043
2543
|
const outputText = payload.output_text;
|
|
2044
2544
|
if (typeof outputText === "string") {
|
|
@@ -2131,22 +2631,6 @@ function pickResponsesFinishReason(payload) {
|
|
|
2131
2631
|
}
|
|
2132
2632
|
return;
|
|
2133
2633
|
}
|
|
2134
|
-
async function streamViaComplete(callbacks, complete) {
|
|
2135
|
-
callbacks.onStart?.();
|
|
2136
|
-
const response = await complete();
|
|
2137
|
-
if (response.text.length > 0) {
|
|
2138
|
-
callbacks.onToken?.(response.text);
|
|
2139
|
-
}
|
|
2140
|
-
callbacks.onChunk?.({
|
|
2141
|
-
textDelta: response.text,
|
|
2142
|
-
raw: response.raw,
|
|
2143
|
-
done: true,
|
|
2144
|
-
usage: response.usage,
|
|
2145
|
-
finishReason: response.finishReason
|
|
2146
|
-
});
|
|
2147
|
-
callbacks.onComplete?.(response);
|
|
2148
|
-
return response;
|
|
2149
|
-
}
|
|
2150
2634
|
|
|
2151
2635
|
// src/providers/anthropic-compatible.ts
|
|
2152
2636
|
var DEFAULT_ANTHROPIC_MAX_TOKENS = 1024;
|
|
@@ -2165,7 +2649,7 @@ function createAnthropicCompatibleAdapter(options) {
|
|
|
2165
2649
|
},
|
|
2166
2650
|
async stream(request, callbacks = {}) {
|
|
2167
2651
|
if (hasMCPClients(request.mcpClients)) {
|
|
2168
|
-
return
|
|
2652
|
+
return streamWithMCPToolLoop(options, fetcher, path, request, callbacks);
|
|
2169
2653
|
}
|
|
2170
2654
|
const response = await fetcher(buildURL(options.baseURL, path), {
|
|
2171
2655
|
method: "POST",
|
|
@@ -2339,6 +2823,127 @@ async function completeWithMCPToolLoop(options, fetcher, path, request) {
|
|
|
2339
2823
|
toolExecutions: toolExecutions.length > 0 ? toolExecutions : undefined
|
|
2340
2824
|
};
|
|
2341
2825
|
}
|
|
2826
|
+
async function streamWithMCPToolLoop(options, fetcher, path, request, callbacks) {
|
|
2827
|
+
const maxToolRounds = normalizeMaxToolRounds(request.maxToolRounds ?? options.defaultMaxToolRounds);
|
|
2828
|
+
let messages = [{ role: "user", content: request.prompt }];
|
|
2829
|
+
let aggregatedUsage;
|
|
2830
|
+
let finishReason;
|
|
2831
|
+
let lastPayload;
|
|
2832
|
+
const toolCalls = [];
|
|
2833
|
+
const toolExecutions = [];
|
|
2834
|
+
callbacks.onStart?.();
|
|
2835
|
+
for (let round = 1;round <= maxToolRounds + 1; round += 1) {
|
|
2836
|
+
const mcpToolset = await resolveMCPToolset(request.mcpClients);
|
|
2837
|
+
const tools = toAnthropicTools(toProviderFunctionTools(mcpToolset));
|
|
2838
|
+
const response = await fetcher(buildURL(options.baseURL, path), {
|
|
2839
|
+
method: "POST",
|
|
2840
|
+
headers: buildHeaders2(options),
|
|
2841
|
+
body: JSON.stringify(cleanUndefined({
|
|
2842
|
+
...options.defaultBody,
|
|
2843
|
+
...request.body,
|
|
2844
|
+
model: options.model,
|
|
2845
|
+
system: request.systemPrompt,
|
|
2846
|
+
messages,
|
|
2847
|
+
temperature: request.temperature,
|
|
2848
|
+
max_tokens: resolveMaxTokens(request.maxTokens, options.defaultMaxTokens),
|
|
2849
|
+
tools,
|
|
2850
|
+
tool_choice: toAnthropicToolChoice(request.toolChoice),
|
|
2851
|
+
stream: true
|
|
2852
|
+
}))
|
|
2853
|
+
});
|
|
2854
|
+
if (!response.ok) {
|
|
2855
|
+
const message = await response.text();
|
|
2856
|
+
throw new Error(`HTTP ${response.status}: ${message}`);
|
|
2857
|
+
}
|
|
2858
|
+
let roundText = "";
|
|
2859
|
+
let roundUsage;
|
|
2860
|
+
let roundFinishReason;
|
|
2861
|
+
const streamedToolCalls = new Map;
|
|
2862
|
+
await consumeSSE(response, (data) => {
|
|
2863
|
+
if (data === "[DONE]") {
|
|
2864
|
+
return;
|
|
2865
|
+
}
|
|
2866
|
+
const json = safeJSONParse(data);
|
|
2867
|
+
if (!isRecord2(json)) {
|
|
2868
|
+
return;
|
|
2869
|
+
}
|
|
2870
|
+
lastPayload = json;
|
|
2871
|
+
const delta = pickAnthropicDelta(json);
|
|
2872
|
+
const chunkUsage = pickUsage2(json);
|
|
2873
|
+
const chunkFinishReason = pickFinishReason2(json);
|
|
2874
|
+
collectAnthropicStreamToolCalls(json, streamedToolCalls);
|
|
2875
|
+
roundUsage = mergeUsage(roundUsage, chunkUsage);
|
|
2876
|
+
if (chunkFinishReason) {
|
|
2877
|
+
roundFinishReason = chunkFinishReason;
|
|
2878
|
+
}
|
|
2879
|
+
if (delta) {
|
|
2880
|
+
roundText += delta;
|
|
2881
|
+
callbacks.onToken?.(delta);
|
|
2882
|
+
}
|
|
2883
|
+
if (delta || chunkUsage || chunkFinishReason) {
|
|
2884
|
+
const chunk = {
|
|
2885
|
+
textDelta: delta,
|
|
2886
|
+
raw: json,
|
|
2887
|
+
usage: chunkUsage,
|
|
2888
|
+
finishReason: chunkFinishReason
|
|
2889
|
+
};
|
|
2890
|
+
callbacks.onChunk?.(chunk);
|
|
2891
|
+
}
|
|
2892
|
+
});
|
|
2893
|
+
aggregatedUsage = mergeUsage(aggregatedUsage, roundUsage);
|
|
2894
|
+
if (roundFinishReason) {
|
|
2895
|
+
finishReason = roundFinishReason;
|
|
2896
|
+
}
|
|
2897
|
+
const calledTools = buildAnthropicStreamToolCalls(streamedToolCalls);
|
|
2898
|
+
if (calledTools.length === 0) {
|
|
2899
|
+
const out2 = {
|
|
2900
|
+
text: roundText,
|
|
2901
|
+
raw: lastPayload,
|
|
2902
|
+
usage: aggregatedUsage,
|
|
2903
|
+
finishReason,
|
|
2904
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
2905
|
+
toolExecutions: toolExecutions.length > 0 ? toolExecutions : undefined
|
|
2906
|
+
};
|
|
2907
|
+
callbacks.onComplete?.(out2);
|
|
2908
|
+
return out2;
|
|
2909
|
+
}
|
|
2910
|
+
if (round > maxToolRounds) {
|
|
2911
|
+
throw new Error(`Tool call loop exceeded maxToolRounds (${maxToolRounds}).`);
|
|
2912
|
+
}
|
|
2913
|
+
const toolResultContent = [];
|
|
2914
|
+
const outputs = await executeMCPToolCalls(calledTools, mcpToolset, {
|
|
2915
|
+
round,
|
|
2916
|
+
request,
|
|
2917
|
+
provider: "anthropic-compatible",
|
|
2918
|
+
model: options.model
|
|
2919
|
+
});
|
|
2920
|
+
toolCalls.push(...outputs.map((entry) => entry.call));
|
|
2921
|
+
toolExecutions.push(...outputs.map((entry) => entry.execution));
|
|
2922
|
+
for (const entry of outputs) {
|
|
2923
|
+
toolResultContent.push({
|
|
2924
|
+
type: "tool_result",
|
|
2925
|
+
tool_use_id: entry.call.id,
|
|
2926
|
+
...entry.call.error ? { is_error: true } : {},
|
|
2927
|
+
content: stringifyToolOutput(entry.call.error ? { error: entry.call.error } : entry.call.output)
|
|
2928
|
+
});
|
|
2929
|
+
}
|
|
2930
|
+
messages = [
|
|
2931
|
+
...messages,
|
|
2932
|
+
{ role: "assistant", content: buildAnthropicAssistantToolContent(roundText, calledTools) },
|
|
2933
|
+
{ role: "user", content: toolResultContent }
|
|
2934
|
+
];
|
|
2935
|
+
}
|
|
2936
|
+
const out = {
|
|
2937
|
+
text: "",
|
|
2938
|
+
raw: lastPayload,
|
|
2939
|
+
usage: aggregatedUsage,
|
|
2940
|
+
finishReason,
|
|
2941
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
2942
|
+
toolExecutions: toolExecutions.length > 0 ? toolExecutions : undefined
|
|
2943
|
+
};
|
|
2944
|
+
callbacks.onComplete?.(out);
|
|
2945
|
+
return out;
|
|
2946
|
+
}
|
|
2342
2947
|
function buildHeaders2(options) {
|
|
2343
2948
|
return {
|
|
2344
2949
|
"content-type": "application/json",
|
|
@@ -2401,6 +3006,83 @@ function pickAnthropicDelta(payload) {
|
|
|
2401
3006
|
}
|
|
2402
3007
|
return "";
|
|
2403
3008
|
}
|
|
3009
|
+
function collectAnthropicStreamToolCalls(payload, state) {
|
|
3010
|
+
const eventType = pickString(payload.type);
|
|
3011
|
+
if (!eventType) {
|
|
3012
|
+
return;
|
|
3013
|
+
}
|
|
3014
|
+
if (eventType === "content_block_start" && isRecord2(payload.content_block)) {
|
|
3015
|
+
const block = payload.content_block;
|
|
3016
|
+
if (pickString(block.type) !== "tool_use") {
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
3019
|
+
const index = pickContentBlockIndex(payload.index);
|
|
3020
|
+
const existing = state.get(index) ?? {
|
|
3021
|
+
index,
|
|
3022
|
+
argumentsText: ""
|
|
3023
|
+
};
|
|
3024
|
+
const id = pickString(block.id);
|
|
3025
|
+
if (id) {
|
|
3026
|
+
existing.id = id;
|
|
3027
|
+
}
|
|
3028
|
+
const name = pickString(block.name);
|
|
3029
|
+
if (name) {
|
|
3030
|
+
existing.name = name;
|
|
3031
|
+
}
|
|
3032
|
+
if ("input" in block) {
|
|
3033
|
+
existing.input = block.input;
|
|
3034
|
+
}
|
|
3035
|
+
state.set(index, existing);
|
|
3036
|
+
return;
|
|
3037
|
+
}
|
|
3038
|
+
if (eventType === "content_block_delta" && isRecord2(payload.delta)) {
|
|
3039
|
+
const delta = payload.delta;
|
|
3040
|
+
if (pickString(delta.type) !== "input_json_delta") {
|
|
3041
|
+
return;
|
|
3042
|
+
}
|
|
3043
|
+
const index = pickContentBlockIndex(payload.index);
|
|
3044
|
+
const existing = state.get(index) ?? {
|
|
3045
|
+
index,
|
|
3046
|
+
argumentsText: ""
|
|
3047
|
+
};
|
|
3048
|
+
const partial = pickString(delta.partial_json);
|
|
3049
|
+
if (partial) {
|
|
3050
|
+
existing.argumentsText += partial;
|
|
3051
|
+
}
|
|
3052
|
+
state.set(index, existing);
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
function pickContentBlockIndex(value) {
|
|
3056
|
+
const numeric = toFiniteNumber(value);
|
|
3057
|
+
if (numeric !== undefined) {
|
|
3058
|
+
return Math.floor(numeric);
|
|
3059
|
+
}
|
|
3060
|
+
return 0;
|
|
3061
|
+
}
|
|
3062
|
+
function buildAnthropicStreamToolCalls(state) {
|
|
3063
|
+
return [...state.values()].sort((a, b) => a.index - b.index).map((entry) => ({
|
|
3064
|
+
id: entry.id ?? "",
|
|
3065
|
+
type: "function",
|
|
3066
|
+
name: entry.name,
|
|
3067
|
+
arguments: entry.argumentsText.length > 0 ? entry.argumentsText : entry.input ?? {}
|
|
3068
|
+
}));
|
|
3069
|
+
}
|
|
3070
|
+
function buildAnthropicAssistantToolContent(text, toolCalls) {
|
|
3071
|
+
const content = [];
|
|
3072
|
+
if (text.length > 0) {
|
|
3073
|
+
content.push({ type: "text", text });
|
|
3074
|
+
}
|
|
3075
|
+
for (const call of toolCalls) {
|
|
3076
|
+
const parsedArguments = typeof call.arguments === "string" ? parseToolArguments(call.arguments) : call.arguments;
|
|
3077
|
+
content.push({
|
|
3078
|
+
type: "tool_use",
|
|
3079
|
+
id: call.id,
|
|
3080
|
+
name: call.name,
|
|
3081
|
+
input: isRecord2(parsedArguments) ? parsedArguments : {}
|
|
3082
|
+
});
|
|
3083
|
+
}
|
|
3084
|
+
return content;
|
|
3085
|
+
}
|
|
2404
3086
|
function pickUsage2(payload) {
|
|
2405
3087
|
const fromUsage = extractUsageObject(payload.usage);
|
|
2406
3088
|
if (fromUsage) {
|
|
@@ -2480,22 +3162,6 @@ function toAnthropicToolChoice(value) {
|
|
|
2480
3162
|
}
|
|
2481
3163
|
return value;
|
|
2482
3164
|
}
|
|
2483
|
-
async function streamViaComplete2(callbacks, complete) {
|
|
2484
|
-
callbacks.onStart?.();
|
|
2485
|
-
const response = await complete();
|
|
2486
|
-
if (response.text.length > 0) {
|
|
2487
|
-
callbacks.onToken?.(response.text);
|
|
2488
|
-
}
|
|
2489
|
-
callbacks.onChunk?.({
|
|
2490
|
-
textDelta: response.text,
|
|
2491
|
-
raw: response.raw,
|
|
2492
|
-
done: true,
|
|
2493
|
-
usage: response.usage,
|
|
2494
|
-
finishReason: response.finishReason
|
|
2495
|
-
});
|
|
2496
|
-
callbacks.onComplete?.(response);
|
|
2497
|
-
return response;
|
|
2498
|
-
}
|
|
2499
3165
|
|
|
2500
3166
|
// src/providers/registry.ts
|
|
2501
3167
|
class InMemoryProviderRegistry {
|