@fased/fased 0.1.57 → 0.1.59
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/agents/anthropic-payload-policy.js +188 -0
- package/dist/agents/anthropic-transport-stream.js +736 -0
- package/dist/agents/copilot-dynamic-headers.js +1 -1
- package/dist/agents/current-model-catalog.js +5 -2
- package/dist/agents/custom-api-registry.js +21 -0
- package/dist/agents/google-transport-stream.js +543 -0
- package/dist/agents/model-catalog-status.js +6 -1
- package/dist/agents/models-config.js +2 -1
- package/dist/agents/openai-completions-compat.js +49 -0
- package/dist/agents/openai-responses-payload-policy.js +100 -0
- package/dist/agents/openai-tool-schema.js +130 -0
- package/dist/agents/openai-transport-stream.js +1040 -0
- package/dist/agents/pi-embedded-runner/compact.js +9 -0
- package/dist/agents/pi-embedded-runner/run/attempt.js +9 -0
- package/dist/agents/pi-embedded-runner/session-manager-init.js +5 -20
- package/dist/agents/provider-extension-catalog-manifest.js +2 -2
- package/dist/agents/provider-request-config.js +1 -1
- package/dist/agents/provider-stream.js +29 -0
- package/dist/agents/provider-transport-fetch.js +89 -0
- package/dist/agents/provider-transport-stream.js +72 -0
- package/dist/agents/system-prompt-cache-boundary.js +1 -1
- package/dist/agents/system-prompt.js +1 -1
- package/dist/agents/transport-message-transform.js +143 -0
- package/dist/agents/transport-stream-shared.js +81 -0
- package/dist/brand.js +1 -1
- package/dist/build-info.json +3 -3
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/dist/channels/delivery.js +78 -0
- package/dist/channels/plugins/catalog.js +10 -5
- package/dist/cli/components-cli.js +16 -13
- package/dist/cli/program/register.subclis.js +9 -0
- package/dist/cli/providers-cli.js +79 -6
- package/dist/cli/services-cli.js +28 -0
- package/dist/commands/auth-choice.js +1 -1
- package/dist/commands/models/list.auth-overview.js +12 -0
- package/dist/commands/onboard-channels.js +5 -0
- package/dist/commands/onboard-helpers.js +1 -1
- package/dist/commands/vllm-setup.js +12 -3
- package/dist/control-ui/assets/{app-DltSyY4Y.js → app-S_jg-ADS.js} +1706 -1675
- package/dist/control-ui/assets/{index-D5JlBp7f.js → index-C22rDOFm.js} +3 -3
- package/dist/control-ui/assets/{providers-Cc89iMdx.js → providers-QMn4w2_f.js} +3 -3
- package/dist/control-ui/index.html +1 -1
- package/dist/control-ui/version.json +1 -1
- package/dist/gateway/control-reply-text.js +10 -1
- package/dist/gateway/method-scopes.js +2 -0
- package/dist/gateway/protocol/schema/agents-models-skills.js +1 -0
- package/dist/gateway/server-chat.js +3 -3
- package/dist/gateway/server-methods/channels.js +10 -2
- package/dist/gateway/server-methods/chat.js +19 -12
- package/dist/gateway/server-methods/models.js +4 -0
- package/dist/gateway/server-methods/services.js +47 -2
- package/dist/gateway/server-methods-list.js +2 -0
- package/dist/gateway/session-event-payloads.js +20 -9
- package/dist/infra/google-api-base-url.js +29 -0
- package/dist/plugin-sdk/{accounts-CmMwe9Dt.js → accounts-DxblobYM.js} +1 -1
- package/dist/plugin-sdk/{actions-DYViFQiX.js → actions-D4UNimWa.js} +2 -2
- package/dist/plugin-sdk/{active-listener-DVbpcisf.js → active-listener-CFuy106s.js} +1 -1
- package/dist/plugin-sdk/{audio-preflight-D-Mfay8i.js → audio-preflight-CxZixyhW.js} +25 -25
- package/dist/plugin-sdk/{audit-JImYFOzJ.js → audit-Cs0K7LBp.js} +1 -1
- package/dist/plugin-sdk/{channel-access-cxCK2mci.js → channel-access-D5aHoTY8.js} +1 -1
- package/dist/plugin-sdk/{channel-access-configure-B_qYjml8.js → channel-access-configure-Dvch9mMQ.js} +1 -1
- package/dist/plugin-sdk/channel-plugin-common.d.ts +2 -2
- package/dist/plugin-sdk/channel-plugin-common.js +4 -4
- package/dist/plugin-sdk/{chunk-CF8nvZNj.js → chunk-B0FUGc5A.js} +1 -1
- package/dist/plugin-sdk/{chunk-items-yLFoO571.js → chunk-items-Cy9a3X7x.js} +1 -1
- package/dist/plugin-sdk/command-status.js +25 -25
- package/dist/plugin-sdk/{commands-registry-CK4KmMG0.js → commands-registry-DmDRhRAU.js} +2 -2
- package/dist/plugin-sdk/{common-Cky4hpqr.js → common-CfUOowOp.js} +1 -1
- package/dist/plugin-sdk/{config-Dbxj_jUP.js → config-mxULv0fQ.js} +4 -1
- package/dist/plugin-sdk/{config-schema-aKPMn6ML.js → config-schema-D8E0LX8X.js} +4422 -486
- package/dist/plugin-sdk/device-pair.d.ts +1 -1
- package/dist/plugin-sdk/discord.d.ts +4 -4
- package/dist/plugin-sdk/discord.js +19 -19
- package/dist/plugin-sdk/{handle-action-BSNDD2Vc.js → handle-action-DbVCF9HZ.js} +13 -13
- package/dist/plugin-sdk/{helpers-CDo2XBSr.js → helpers-BXbdgs5H.js} +1 -1
- package/dist/plugin-sdk/{image-ops-DbeBxbn4.js → image-ops-D7jPxaph.js} +1 -1
- package/dist/plugin-sdk/index.d.ts +15 -15
- package/dist/plugin-sdk/index.js +27 -27
- package/dist/plugin-sdk/{login-CWXvFTd8.js → login-BRxww6nv.js} +3 -3
- package/dist/plugin-sdk/{login-qr-CJB2zjmM.js → login-qr-DJXsk9XH.js} +3 -3
- package/dist/plugin-sdk/{manager-CbiFFLKS.js → manager-Y2S_5IlI.js} +2 -2
- package/dist/plugin-sdk/{markdown-tables-Bskt9r8i.js → markdown-tables-DHLFQqaF.js} +1 -1
- package/dist/plugin-sdk/{media-B3aAg5rh.js → media-Czo4carg.js} +2 -2
- package/dist/plugin-sdk/{monitor-BYGbTI5V.js → monitor-CFHFAUOj.js} +32 -32
- package/dist/plugin-sdk/{monitor-BJs6z7AT.js → monitor-xS9A8Upr.js} +29 -29
- package/dist/plugin-sdk/ollama-runtime.js +970 -0
- package/dist/plugin-sdk/{openresponses-http-BGCl2dS2.js → openresponses-http-BcUbeVV6.js} +25 -25
- package/dist/plugin-sdk/{optional-runtime-dependency-BlTVkcAg.js → optional-runtime-dependency-DdMbxS24.js} +1 -1
- package/dist/plugin-sdk/{outbound-CIemga5_.js → outbound-Db8mRN6P.js} +6 -6
- package/dist/plugin-sdk/{outbound-attachment-2RckWqrk.js → outbound-attachment-BqFjcypb.js} +2 -2
- package/dist/plugin-sdk/{pairing-message-r-VQfhFB.js → pairing-message-B3GC_7CD.js} +1 -1
- package/dist/plugin-sdk/{pairing-message-C403mq4K.d.ts → pairing-message-CxonJmwM.d.ts} +1 -1
- package/dist/plugin-sdk/{pairing-store-uPnLfGgB.js → pairing-store-CQikgIbw.js} +2 -2
- package/dist/plugin-sdk/{pi-model-discovery-BpEilAym.js → pi-model-discovery-CY3m5zJP.js} +1 -1
- package/dist/plugin-sdk/{pi-model-discovery-runtime-DwNdFaz-.js → pi-model-discovery-runtime-DSh1T2pn.js} +2 -2
- package/dist/plugin-sdk/provider-web-search-config-contract.d.ts +1 -1
- package/dist/plugin-sdk/{pw-ai-H7UgunQQ.js → pw-ai-AFUqn-tQ.js} +3 -3
- package/dist/plugin-sdk/{replies-CKMwkH4G.js → replies-Fzh9PhXl.js} +2 -2
- package/dist/plugin-sdk/{resolve-outbound-target-B68fSoGi.js → resolve-outbound-target-KpF3co6T.js} +1 -1
- package/dist/plugin-sdk/{resolve-route-B8fOfihG.js → resolve-route-CDl74O19.js} +1 -1
- package/dist/plugin-sdk/{retry-policy-B6GQldxB.js → retry-policy-D4uiqUOG.js} +1 -1
- package/dist/plugin-sdk/{runtime-nVJl3PlS.js → runtime-Bb6KRnCC.js} +6 -4
- package/dist/plugin-sdk/{runtime-dependency-DlqVSsSs.js → runtime-dependency-DLDmB94f.js} +11 -11
- package/dist/plugin-sdk/sat-runtime.d.ts +2 -2
- package/dist/plugin-sdk/sat-runtime.js +25 -25
- package/dist/plugin-sdk/{send-CPK_HlYW.js → send-5nFktR1n.js} +5 -5
- package/dist/plugin-sdk/{send-hSu1HWms.js → send-Bfzf11VY.js} +7 -7
- package/dist/plugin-sdk/{send-D3DQOg2a.js → send-C4k4JuJN.js} +6 -6
- package/dist/plugin-sdk/{send-BHokB4TR.js → send-D-uDKGIq.js} +6 -6
- package/dist/plugin-sdk/{send-BCJ4hi3t.js → send-Du8ykP1k.js} +8 -8
- package/dist/plugin-sdk/{session-DQ4Dr-mf.js → session-CS-Td9EM.js} +2 -2
- package/dist/plugin-sdk/{skill-commands-CmdoKLh_.js → skill-commands-CFAlXWHk.js} +2 -2
- package/dist/plugin-sdk/{slack-B70NHpMF.js → slack-DXYGbizH.js} +37 -37
- package/dist/plugin-sdk/{slack-actions-fSmP981b.js → slack-actions-BcMkNv1Z.js} +11 -11
- package/dist/plugin-sdk/slack.d.ts +4 -4
- package/dist/plugin-sdk/slack.js +9 -9
- package/dist/plugin-sdk/{sticker-cache-B4dG2sKU.js → sticker-cache-Dr3rYfU1.js} +3 -3
- package/dist/plugin-sdk/{tables-EX4Hy-a-.js → tables-bFXyRpkx.js} +1 -1
- package/dist/plugin-sdk/{targets-BeGUCGWK.js → targets-DH7a9lKB.js} +1 -1
- package/dist/plugin-sdk/{telegram-actions-CM8SJgc9.js → telegram-actions-Besbw_Hg.js} +28 -28
- package/dist/plugin-sdk/telegram.d.ts +4 -4
- package/dist/plugin-sdk/telegram.js +8 -8
- package/dist/plugin-sdk/{thread-bindings.lifecycle-tHK_1mvp.js → thread-bindings.lifecycle-CgOvvdw8.js} +3 -3
- package/dist/plugin-sdk/{thread-bindings.state-D7xG1GHu.js → thread-bindings.state-CQpk44Hq.js} +2 -2
- package/dist/plugin-sdk/{tmp-fased-dir-Ouj58axY.d.ts → tmp-fased-dir-DQ00xQ5D.d.ts} +1 -1
- package/dist/plugin-sdk/{types-B4hj6HgR.d.ts → types-B6zx-Dl2.d.ts} +1 -1
- package/dist/plugin-sdk/{types-dWZFX1fk.d.ts → types-eUM7hcDz.d.ts} +1 -1
- package/dist/plugin-sdk/{web-BFolfZ3i.js → web-Q1-G--JH.js} +30 -30
- package/dist/plugin-sdk/{whatsapp-actions-CYmleHCz.js → whatsapp-actions-BtSc2CBv.js} +13 -13
- package/dist/plugin-sdk/{whatsapp-heartbeat-BEwm4GK3.js → whatsapp-heartbeat-Bg9TuYzl.js} +2 -2
- package/dist/plugin-sdk/whatsapp.d.ts +9 -9
- package/dist/plugin-sdk/whatsapp.js +12 -12
- package/dist/plugin-sdk/{wsl-BtMDzMv5.js → wsl-C4DGFkOd.js} +3 -3
- package/dist/plugin-sdk/{zod-schema.providers-core-FaYR6KdD.d.ts → zod-schema.providers-core-CIzCjXH9.d.ts} +61 -61
- package/dist/plugins/provider-runtime.js +79 -0
- package/dist/providers/refresh.js +14 -0
- package/extensions/acpx/package.json +1 -1
- package/extensions/bluebubbles/package.json +1 -1
- package/extensions/copilot-proxy/package.json +1 -1
- package/extensions/diagnostics-otel/package.json +1 -1
- package/extensions/fased-federation/package.json +1 -1
- package/extensions/google-gemini-cli-auth/package.json +1 -1
- package/extensions/imessage/package.json +1 -1
- package/extensions/irc/package.json +1 -1
- package/extensions/line/package.json +1 -1
- package/extensions/llm-task/package.json +1 -1
- package/extensions/lobster/package.json +1 -1
- package/extensions/matrix/CHANGELOG.md +12 -0
- package/extensions/matrix/package.json +1 -1
- package/extensions/mattermost/package.json +1 -1
- package/extensions/memory-core/package.json +2 -2
- package/extensions/memory-lancedb/package.json +1 -1
- package/extensions/minimax-portal-auth/package.json +1 -1
- package/extensions/msteams/CHANGELOG.md +12 -0
- package/extensions/msteams/package.json +1 -1
- package/extensions/nextcloud-talk/package.json +1 -1
- package/extensions/nostr/CHANGELOG.md +12 -0
- package/extensions/nostr/package.json +1 -1
- package/extensions/open-prose/package.json +1 -1
- package/extensions/sat-mining/package.json +1 -1
- package/extensions/signal/package.json +1 -1
- package/extensions/synology-chat/package.json +1 -1
- package/extensions/tlon/package.json +1 -1
- package/extensions/twitch/CHANGELOG.md +12 -0
- package/extensions/twitch/package.json +1 -1
- package/extensions/voice-call/CHANGELOG.md +12 -0
- package/extensions/voice-call/package.json +1 -1
- package/extensions/zalo/CHANGELOG.md +12 -0
- package/extensions/zalo/package.json +1 -1
- package/extensions/zalouser/CHANGELOG.md +12 -0
- package/extensions/zalouser/package.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,970 @@
|
|
|
1
|
+
import { DEFAULT_CONTEXT_TOKENS } from "../agents/defaults.js";
|
|
2
|
+
import { normalizeProviderId } from "../agents/provider-id.js";
|
|
3
|
+
import { formatErrorMessage } from "../infra/errors.js";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { createAssistantMessageEventStream, streamSimple } from "@mariozechner/pi-ai/compat";
|
|
6
|
+
|
|
7
|
+
//#region src/plugin-sdk/ollama-runtime.ts
|
|
8
|
+
const OLLAMA_NATIVE_BASE_URL = "http://127.0.0.1:11434";
|
|
9
|
+
const MAX_SAFE_INTEGER_ABS_STR = String(Number.MAX_SAFE_INTEGER);
|
|
10
|
+
const CHARS_PER_TOKEN_ESTIMATE = 4;
|
|
11
|
+
const OLLAMA_OPTION_PARAM_KEYS = new Set([
|
|
12
|
+
"num_keep",
|
|
13
|
+
"seed",
|
|
14
|
+
"num_predict",
|
|
15
|
+
"top_k",
|
|
16
|
+
"top_p",
|
|
17
|
+
"min_p",
|
|
18
|
+
"typical_p",
|
|
19
|
+
"repeat_last_n",
|
|
20
|
+
"temperature",
|
|
21
|
+
"repeat_penalty",
|
|
22
|
+
"presence_penalty",
|
|
23
|
+
"frequency_penalty",
|
|
24
|
+
"stop",
|
|
25
|
+
"num_ctx",
|
|
26
|
+
"num_batch",
|
|
27
|
+
"num_gpu",
|
|
28
|
+
"main_gpu",
|
|
29
|
+
"use_mmap",
|
|
30
|
+
"num_thread"
|
|
31
|
+
]);
|
|
32
|
+
const OLLAMA_TOP_LEVEL_PARAM_KEYS = new Set([
|
|
33
|
+
"format",
|
|
34
|
+
"keep_alive",
|
|
35
|
+
"truncate",
|
|
36
|
+
"shift"
|
|
37
|
+
]);
|
|
38
|
+
const GARBLED_VISIBLE_TEXT_MODEL_RE = /\b(?:glm|kimi)\b/i;
|
|
39
|
+
const GARBLED_VISIBLE_TEXT_MIN_CHARS = 80;
|
|
40
|
+
const GARBLED_VISIBLE_TEXT_SYMBOL_RE = /[$#%&="'_~`^|\\/*+\-[\]{}()<>:;,.!?]/gu;
|
|
41
|
+
const LETTER_OR_DIGIT_RE = /[\p{L}\p{N}]/gu;
|
|
42
|
+
function isAsciiDigit(ch) {
|
|
43
|
+
return ch !== undefined && ch >= "0" && ch <= "9";
|
|
44
|
+
}
|
|
45
|
+
function parseJsonNumberToken(input, start) {
|
|
46
|
+
let idx = start;
|
|
47
|
+
if (input[idx] === "-") {
|
|
48
|
+
idx += 1;
|
|
49
|
+
}
|
|
50
|
+
if (idx >= input.length) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
if (input[idx] === "0") {
|
|
54
|
+
idx += 1;
|
|
55
|
+
} else if (isAsciiDigit(input[idx]) && input[idx] !== "0") {
|
|
56
|
+
while (isAsciiDigit(input[idx])) {
|
|
57
|
+
idx += 1;
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
let isInteger = true;
|
|
63
|
+
if (input[idx] === ".") {
|
|
64
|
+
isInteger = false;
|
|
65
|
+
idx += 1;
|
|
66
|
+
if (!isAsciiDigit(input[idx])) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
while (isAsciiDigit(input[idx])) {
|
|
70
|
+
idx += 1;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (input[idx] === "e" || input[idx] === "E") {
|
|
74
|
+
isInteger = false;
|
|
75
|
+
idx += 1;
|
|
76
|
+
if (input[idx] === "+" || input[idx] === "-") {
|
|
77
|
+
idx += 1;
|
|
78
|
+
}
|
|
79
|
+
if (!isAsciiDigit(input[idx])) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
while (isAsciiDigit(input[idx])) {
|
|
83
|
+
idx += 1;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
token: input.slice(start, idx),
|
|
88
|
+
end: idx,
|
|
89
|
+
isInteger
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function isUnsafeIntegerLiteral(token) {
|
|
93
|
+
const digits = token[0] === "-" ? token.slice(1) : token;
|
|
94
|
+
if (digits.length < MAX_SAFE_INTEGER_ABS_STR.length) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
if (digits.length > MAX_SAFE_INTEGER_ABS_STR.length) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
return digits > MAX_SAFE_INTEGER_ABS_STR;
|
|
101
|
+
}
|
|
102
|
+
function quoteUnsafeIntegerLiterals(input) {
|
|
103
|
+
let out = "";
|
|
104
|
+
let inString = false;
|
|
105
|
+
let escaped = false;
|
|
106
|
+
let idx = 0;
|
|
107
|
+
while (idx < input.length) {
|
|
108
|
+
const ch = input[idx] ?? "";
|
|
109
|
+
if (inString) {
|
|
110
|
+
out += ch;
|
|
111
|
+
if (escaped) {
|
|
112
|
+
escaped = false;
|
|
113
|
+
} else if (ch === "\\") {
|
|
114
|
+
escaped = true;
|
|
115
|
+
} else if (ch === "\"") {
|
|
116
|
+
inString = false;
|
|
117
|
+
}
|
|
118
|
+
idx += 1;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (ch === "\"") {
|
|
122
|
+
inString = true;
|
|
123
|
+
out += ch;
|
|
124
|
+
idx += 1;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (ch === "-" || isAsciiDigit(ch)) {
|
|
128
|
+
const parsed = parseJsonNumberToken(input, idx);
|
|
129
|
+
if (parsed) {
|
|
130
|
+
out += parsed.isInteger && isUnsafeIntegerLiteral(parsed.token) ? `"${parsed.token}"` : parsed.token;
|
|
131
|
+
idx = parsed.end;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
out += ch;
|
|
136
|
+
idx += 1;
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
function parseJsonPreservingUnsafeIntegers(input) {
|
|
141
|
+
return JSON.parse(quoteUnsafeIntegerLiterals(input));
|
|
142
|
+
}
|
|
143
|
+
function parseJsonObjectPreservingUnsafeIntegers(value) {
|
|
144
|
+
if (typeof value === "string") {
|
|
145
|
+
try {
|
|
146
|
+
const parsed = parseJsonPreservingUnsafeIntegers(value);
|
|
147
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
153
|
+
}
|
|
154
|
+
function normalizeOllamaWireModelId(modelId, providerId) {
|
|
155
|
+
const trimmed = modelId.trim();
|
|
156
|
+
if (!trimmed) {
|
|
157
|
+
return trimmed;
|
|
158
|
+
}
|
|
159
|
+
const candidates = [
|
|
160
|
+
providerId?.trim(),
|
|
161
|
+
normalizeProviderId(providerId ?? ""),
|
|
162
|
+
"ollama"
|
|
163
|
+
].filter((candidate) => Boolean(candidate));
|
|
164
|
+
for (const candidate of new Set(candidates)) {
|
|
165
|
+
const prefix = `${candidate}/`;
|
|
166
|
+
if (trimmed.startsWith(prefix)) {
|
|
167
|
+
return trimmed.slice(prefix.length);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return trimmed;
|
|
171
|
+
}
|
|
172
|
+
function countMatches(text, re) {
|
|
173
|
+
re.lastIndex = 0;
|
|
174
|
+
return Array.from(text.matchAll(re)).length;
|
|
175
|
+
}
|
|
176
|
+
function maxCharacterFrequency(text) {
|
|
177
|
+
const counts = new Map();
|
|
178
|
+
let max = 0;
|
|
179
|
+
for (const char of text) {
|
|
180
|
+
const count = (counts.get(char) ?? 0) + 1;
|
|
181
|
+
counts.set(char, count);
|
|
182
|
+
max = Math.max(max, count);
|
|
183
|
+
}
|
|
184
|
+
return max;
|
|
185
|
+
}
|
|
186
|
+
function isLikelyGarbledVisibleText(params) {
|
|
187
|
+
if (!GARBLED_VISIBLE_TEXT_MODEL_RE.test(params.modelId)) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
const compact = params.text.replace(/\s+/g, "");
|
|
191
|
+
if (compact.length < GARBLED_VISIBLE_TEXT_MIN_CHARS) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
const letterOrDigitCount = countMatches(compact, LETTER_OR_DIGIT_RE);
|
|
195
|
+
const symbolCount = countMatches(compact, GARBLED_VISIBLE_TEXT_SYMBOL_RE);
|
|
196
|
+
const maxFrequency = maxCharacterFrequency(compact);
|
|
197
|
+
return letterOrDigitCount / compact.length < .08 && symbolCount / compact.length > .6 && (maxFrequency / compact.length > .22 || /[$#%&="'_~`^|\\/*+\-[\]{}()<>:;,.!?]{12,}/u.test(compact));
|
|
198
|
+
}
|
|
199
|
+
function resolveOllamaBaseUrlForRun(params) {
|
|
200
|
+
return params.providerBaseUrl?.trim() || params.modelBaseUrl?.trim() || OLLAMA_NATIVE_BASE_URL;
|
|
201
|
+
}
|
|
202
|
+
function resolveConfiguredProvider(params) {
|
|
203
|
+
const providers = params.config?.models?.providers;
|
|
204
|
+
const providerId = params.providerId?.trim();
|
|
205
|
+
if (!providers || !providerId) {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
const direct = providers[providerId];
|
|
209
|
+
if (direct) {
|
|
210
|
+
return direct;
|
|
211
|
+
}
|
|
212
|
+
const normalized = normalizeProviderId(providerId);
|
|
213
|
+
for (const [candidateId, candidate] of Object.entries(providers)) {
|
|
214
|
+
if (normalizeProviderId(candidateId) === normalized) {
|
|
215
|
+
return candidate;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
function isOllamaCompatProvider(model) {
|
|
221
|
+
const providerId = normalizeProviderId(model.provider ?? "");
|
|
222
|
+
if (providerId === "ollama" || providerId.includes("ollama")) {
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
if (!model.baseUrl) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
const parsed = new URL(model.baseUrl);
|
|
230
|
+
const host = parsed.hostname.toLowerCase();
|
|
231
|
+
const localhost = host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
232
|
+
return localhost && parsed.port === "11434";
|
|
233
|
+
} catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function resolveOllamaCompatNumCtxEnabled(params) {
|
|
238
|
+
return resolveConfiguredProvider(params)?.injectNumCtxForOpenAICompat !== false;
|
|
239
|
+
}
|
|
240
|
+
function shouldInjectOllamaCompatNumCtx(params) {
|
|
241
|
+
return params.model.api === "openai-completions" && isOllamaCompatProvider(params.model) && resolveOllamaCompatNumCtxEnabled({
|
|
242
|
+
config: params.config,
|
|
243
|
+
providerId: params.providerId
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function streamWithPayloadPatch(baseFn, model, context, options, patch) {
|
|
247
|
+
const streamFn = baseFn ?? streamSimple;
|
|
248
|
+
return streamFn(model, context, {
|
|
249
|
+
...options,
|
|
250
|
+
onPayload: (payload, payloadModel) => {
|
|
251
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
252
|
+
patch(payload);
|
|
253
|
+
}
|
|
254
|
+
(options?.onPayload)?.(payload, payloadModel);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
function wrapOllamaCompatNumCtx(baseFn, numCtx) {
|
|
259
|
+
return (model, context, options) => streamWithPayloadPatch(baseFn, model, context, options, (payloadRecord) => {
|
|
260
|
+
if (!payloadRecord.options || typeof payloadRecord.options !== "object") {
|
|
261
|
+
payloadRecord.options = {};
|
|
262
|
+
}
|
|
263
|
+
payloadRecord.options.num_ctx = numCtx;
|
|
264
|
+
normalizeOllamaCompatMessageToolArgs(payloadRecord);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
function resolveOllamaThinkValue(thinkingLevel) {
|
|
268
|
+
if (thinkingLevel === "off") {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
if (thinkingLevel === "low" || thinkingLevel === "medium" || thinkingLevel === "high") {
|
|
272
|
+
return thinkingLevel;
|
|
273
|
+
}
|
|
274
|
+
if (thinkingLevel === "minimal") {
|
|
275
|
+
return "low";
|
|
276
|
+
}
|
|
277
|
+
if (thinkingLevel === "xhigh" || thinkingLevel === "adaptive" || thinkingLevel === "max") {
|
|
278
|
+
return "high";
|
|
279
|
+
}
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
function resolveOllamaThinkParamValue(params) {
|
|
283
|
+
const raw = params?.think ?? params?.thinking;
|
|
284
|
+
if (typeof raw === "boolean") {
|
|
285
|
+
return raw;
|
|
286
|
+
}
|
|
287
|
+
return resolveOllamaThinkValue(raw);
|
|
288
|
+
}
|
|
289
|
+
function shouldForwardNativeOllamaThink(model, think) {
|
|
290
|
+
return think === false || model?.reasoning !== false;
|
|
291
|
+
}
|
|
292
|
+
function resolveOllamaConfiguredNumCtx(model) {
|
|
293
|
+
const raw = model.params?.num_ctx;
|
|
294
|
+
return typeof raw === "number" && Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : undefined;
|
|
295
|
+
}
|
|
296
|
+
function resolveOllamaNumCtx(model) {
|
|
297
|
+
return resolveOllamaConfiguredNumCtx(model) ?? Math.max(1, Math.floor(model.contextWindow ?? model.maxTokens ?? DEFAULT_CONTEXT_TOKENS));
|
|
298
|
+
}
|
|
299
|
+
function createOllamaThinkingWrapper(baseFn, think) {
|
|
300
|
+
return (model, context, options) => streamWithPayloadPatch(baseFn, model, context, options, (payloadRecord) => {
|
|
301
|
+
payloadRecord.think = think;
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
function createConfiguredOllamaCompatStreamWrapper(ctx) {
|
|
305
|
+
let streamFn = ctx.streamFn;
|
|
306
|
+
const model = ctx.model;
|
|
307
|
+
if (model && shouldInjectOllamaCompatNumCtx({
|
|
308
|
+
model,
|
|
309
|
+
config: ctx.config,
|
|
310
|
+
providerId: typeof model.provider === "string" ? model.provider : ctx.provider
|
|
311
|
+
})) {
|
|
312
|
+
streamFn = wrapOllamaCompatNumCtx(streamFn, resolveOllamaNumCtx(model));
|
|
313
|
+
}
|
|
314
|
+
const configuredThink = model ? resolveOllamaThinkParamValue(model.params) : undefined;
|
|
315
|
+
const runtimeThink = model?.api === "ollama" ? resolveOllamaThinkValue(ctx.thinkingLevel) : undefined;
|
|
316
|
+
const think = runtimeThink === false && configuredThink !== undefined ? undefined : runtimeThink;
|
|
317
|
+
if (think !== undefined && shouldForwardNativeOllamaThink(model, think)) {
|
|
318
|
+
streamFn = createOllamaThinkingWrapper(streamFn, think);
|
|
319
|
+
}
|
|
320
|
+
return streamFn;
|
|
321
|
+
}
|
|
322
|
+
const createConfiguredOllamaCompatNumCtxWrapper = createConfiguredOllamaCompatStreamWrapper;
|
|
323
|
+
function buildOllamaChatRequest(params) {
|
|
324
|
+
return {
|
|
325
|
+
model: normalizeOllamaWireModelId(params.modelId, params.providerId),
|
|
326
|
+
messages: params.messages,
|
|
327
|
+
stream: params.stream ?? true,
|
|
328
|
+
...params.tools && params.tools.length > 0 ? { tools: params.tools } : {},
|
|
329
|
+
...params.options ? { options: params.options } : {},
|
|
330
|
+
...params.requestParams
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function buildUsageWithNoCost(params) {
|
|
334
|
+
const input = params.input ?? 0;
|
|
335
|
+
const output = params.output ?? 0;
|
|
336
|
+
const cacheRead = params.cacheRead ?? 0;
|
|
337
|
+
const cacheWrite = params.cacheWrite ?? 0;
|
|
338
|
+
return {
|
|
339
|
+
input,
|
|
340
|
+
output,
|
|
341
|
+
cacheRead,
|
|
342
|
+
cacheWrite,
|
|
343
|
+
totalTokens: params.totalTokens ?? input + output,
|
|
344
|
+
cost: {
|
|
345
|
+
input: 0,
|
|
346
|
+
output: 0,
|
|
347
|
+
cacheRead: 0,
|
|
348
|
+
cacheWrite: 0,
|
|
349
|
+
total: 0
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function buildStreamAssistantMessage(params) {
|
|
354
|
+
return {
|
|
355
|
+
role: "assistant",
|
|
356
|
+
content: params.content,
|
|
357
|
+
stopReason: params.stopReason,
|
|
358
|
+
api: params.model.api,
|
|
359
|
+
provider: params.model.provider,
|
|
360
|
+
model: params.model.id,
|
|
361
|
+
usage: params.usage,
|
|
362
|
+
timestamp: params.timestamp ?? Date.now()
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
function buildStreamErrorAssistantMessage(params) {
|
|
366
|
+
return {
|
|
367
|
+
...buildStreamAssistantMessage({
|
|
368
|
+
model: params.model,
|
|
369
|
+
content: [],
|
|
370
|
+
stopReason: "error",
|
|
371
|
+
usage: buildUsageWithNoCost({})
|
|
372
|
+
}),
|
|
373
|
+
stopReason: "error",
|
|
374
|
+
errorMessage: params.errorMessage
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
function safeJsonLength(value) {
|
|
378
|
+
try {
|
|
379
|
+
const serialized = JSON.stringify(value);
|
|
380
|
+
return typeof serialized === "string" ? serialized.length : 0;
|
|
381
|
+
} catch {
|
|
382
|
+
return 0;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function estimateTokensFromChars(chars) {
|
|
386
|
+
return Number.isFinite(chars) && chars > 0 ? Math.max(1, Math.round(chars / CHARS_PER_TOKEN_ESTIMATE)) : 0;
|
|
387
|
+
}
|
|
388
|
+
function estimateOllamaPromptTokens(params) {
|
|
389
|
+
let chars = 0;
|
|
390
|
+
for (const message of params.messages) {
|
|
391
|
+
chars += message.content.length;
|
|
392
|
+
chars += safeJsonLength(message.images);
|
|
393
|
+
chars += safeJsonLength(message.tool_calls);
|
|
394
|
+
chars += message.tool_name?.length ?? 0;
|
|
395
|
+
}
|
|
396
|
+
chars += safeJsonLength(params.tools);
|
|
397
|
+
return estimateTokensFromChars(chars);
|
|
398
|
+
}
|
|
399
|
+
function estimateOllamaCompletionTokens(response) {
|
|
400
|
+
return estimateTokensFromChars(response.message.content.length + (response.message.thinking?.length ?? 0) + (response.message.reasoning?.length ?? 0) + safeJsonLength(response.message.tool_calls));
|
|
401
|
+
}
|
|
402
|
+
function resolveUsageCount(value, fallback) {
|
|
403
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
404
|
+
return value;
|
|
405
|
+
}
|
|
406
|
+
return typeof fallback === "number" && Number.isFinite(fallback) && fallback > 0 ? fallback : 0;
|
|
407
|
+
}
|
|
408
|
+
function extractTextContent(content) {
|
|
409
|
+
if (typeof content === "string") {
|
|
410
|
+
return content;
|
|
411
|
+
}
|
|
412
|
+
if (!Array.isArray(content)) {
|
|
413
|
+
return "";
|
|
414
|
+
}
|
|
415
|
+
return content.filter((part) => part.type === "text").map((part) => part.text).join("");
|
|
416
|
+
}
|
|
417
|
+
function extractOllamaImages(content) {
|
|
418
|
+
if (!Array.isArray(content)) {
|
|
419
|
+
return [];
|
|
420
|
+
}
|
|
421
|
+
return content.filter((part) => part.type === "image").map((part) => part.data);
|
|
422
|
+
}
|
|
423
|
+
function ensureArgsObject(value) {
|
|
424
|
+
return parseJsonObjectPreservingUnsafeIntegers(value) ?? {};
|
|
425
|
+
}
|
|
426
|
+
function normalizeOllamaCompatMessageToolArgs(payloadRecord) {
|
|
427
|
+
const messages = payloadRecord.messages;
|
|
428
|
+
if (!Array.isArray(messages)) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
for (const message of messages) {
|
|
432
|
+
if (!message || typeof message !== "object" || Array.isArray(message)) {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const messageRecord = message;
|
|
436
|
+
const functionCall = messageRecord.function_call;
|
|
437
|
+
if (functionCall && typeof functionCall === "object" && !Array.isArray(functionCall)) {
|
|
438
|
+
const functionRecord = functionCall;
|
|
439
|
+
if (Object.hasOwn(functionRecord, "arguments")) {
|
|
440
|
+
functionRecord.arguments = ensureArgsObject(functionRecord.arguments);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const toolCalls = messageRecord.tool_calls;
|
|
444
|
+
if (!Array.isArray(toolCalls)) {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
for (const toolCall of toolCalls) {
|
|
448
|
+
const fn = toolCall && typeof toolCall === "object" ? toolCall.function : undefined;
|
|
449
|
+
if (fn && typeof fn === "object" && !Array.isArray(fn) && Object.hasOwn(fn, "arguments")) {
|
|
450
|
+
fn.arguments = ensureArgsObject(fn.arguments);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function normalizeOllamaResponseToolArgs(response) {
|
|
456
|
+
const toolCalls = response.message?.tool_calls;
|
|
457
|
+
if (!toolCalls?.length) {
|
|
458
|
+
return response;
|
|
459
|
+
}
|
|
460
|
+
for (const toolCall of toolCalls) {
|
|
461
|
+
toolCall.function.arguments = ensureArgsObject(toolCall.function.arguments);
|
|
462
|
+
}
|
|
463
|
+
return response;
|
|
464
|
+
}
|
|
465
|
+
function isRecord(value) {
|
|
466
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
467
|
+
}
|
|
468
|
+
function inferOllamaSchemaType(schema) {
|
|
469
|
+
if (schema.properties && isRecord(schema.properties)) {
|
|
470
|
+
return "object";
|
|
471
|
+
}
|
|
472
|
+
if (schema.items) {
|
|
473
|
+
return "array";
|
|
474
|
+
}
|
|
475
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
|
|
476
|
+
const values = schema.enum.filter((value) => value !== null);
|
|
477
|
+
if (values.length > 0 && values.every((value) => typeof value === "string")) {
|
|
478
|
+
return "string";
|
|
479
|
+
}
|
|
480
|
+
if (values.length > 0 && values.every((value) => typeof value === "number")) {
|
|
481
|
+
return "number";
|
|
482
|
+
}
|
|
483
|
+
if (values.length > 0 && values.every((value) => typeof value === "boolean")) {
|
|
484
|
+
return "boolean";
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return undefined;
|
|
488
|
+
}
|
|
489
|
+
function normalizeOllamaToolSchema(schema, isRoot = false) {
|
|
490
|
+
if (!isRecord(schema)) {
|
|
491
|
+
return {
|
|
492
|
+
type: "object",
|
|
493
|
+
properties: {}
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
const normalized = {};
|
|
497
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
498
|
+
if (key === "properties" && isRecord(value)) {
|
|
499
|
+
normalized.properties = Object.fromEntries(Object.entries(value).map(([name, child]) => [name, normalizeOllamaToolSchema(child)]));
|
|
500
|
+
} else if (key === "items") {
|
|
501
|
+
normalized.items = Array.isArray(value) ? value.map((entry) => normalizeOllamaToolSchema(entry)) : normalizeOllamaToolSchema(value);
|
|
502
|
+
} else if ((key === "anyOf" || key === "oneOf" || key === "allOf") && Array.isArray(value)) {
|
|
503
|
+
normalized[key] = value.map((entry) => normalizeOllamaToolSchema(entry));
|
|
504
|
+
} else {
|
|
505
|
+
normalized[key] = value;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
const schemaType = normalized.type;
|
|
509
|
+
if (typeof schemaType !== "string" && (!Array.isArray(schemaType) || !schemaType.some((entry) => typeof entry === "string" && entry !== "null"))) {
|
|
510
|
+
normalized.type = inferOllamaSchemaType(normalized) ?? (isRoot ? "object" : "string");
|
|
511
|
+
}
|
|
512
|
+
if (normalized.type === "object" && !isRecord(normalized.properties)) {
|
|
513
|
+
normalized.properties = {};
|
|
514
|
+
}
|
|
515
|
+
return normalized;
|
|
516
|
+
}
|
|
517
|
+
function readOllamaToolCallId(value) {
|
|
518
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
519
|
+
}
|
|
520
|
+
function normalizeOllamaToolCallName(rawName, options = {}) {
|
|
521
|
+
const trimmed = rawName.trim();
|
|
522
|
+
const available = options.availableToolNames;
|
|
523
|
+
if (!trimmed || available?.has(trimmed)) {
|
|
524
|
+
return trimmed;
|
|
525
|
+
}
|
|
526
|
+
const stripped = trimmed.replace(/^(?:functions?|tools?)[./_-]+/iu, "").trim();
|
|
527
|
+
if (available) {
|
|
528
|
+
return available.has(stripped) ? stripped : trimmed;
|
|
529
|
+
}
|
|
530
|
+
return stripped;
|
|
531
|
+
}
|
|
532
|
+
function extractToolCalls(content, options = {}) {
|
|
533
|
+
if (!Array.isArray(content)) {
|
|
534
|
+
return [];
|
|
535
|
+
}
|
|
536
|
+
const result = [];
|
|
537
|
+
for (const part of content) {
|
|
538
|
+
if (part.type === "toolCall") {
|
|
539
|
+
result.push({ function: {
|
|
540
|
+
name: normalizeOllamaToolCallName(part.name, options),
|
|
541
|
+
arguments: ensureArgsObject(part.arguments)
|
|
542
|
+
} });
|
|
543
|
+
} else if (part.type === "tool_use") {
|
|
544
|
+
result.push({ function: {
|
|
545
|
+
name: normalizeOllamaToolCallName(part.name, options),
|
|
546
|
+
arguments: ensureArgsObject(part.input)
|
|
547
|
+
} });
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return result;
|
|
551
|
+
}
|
|
552
|
+
function buildOllamaToolNameSet(tools) {
|
|
553
|
+
const names = new Set();
|
|
554
|
+
for (const tool of tools ?? []) {
|
|
555
|
+
if (typeof tool.name === "string" && tool.name.trim()) {
|
|
556
|
+
names.add(tool.name.trim());
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return names.size > 0 ? names : undefined;
|
|
560
|
+
}
|
|
561
|
+
function convertToOllamaMessages(messages, system, options = {}) {
|
|
562
|
+
const result = [];
|
|
563
|
+
if (system) {
|
|
564
|
+
result.push({
|
|
565
|
+
role: "system",
|
|
566
|
+
content: system
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
for (const msg of messages) {
|
|
570
|
+
if (msg.role === "user") {
|
|
571
|
+
const text = extractTextContent(msg.content);
|
|
572
|
+
const images = extractOllamaImages(msg.content);
|
|
573
|
+
result.push({
|
|
574
|
+
role: "user",
|
|
575
|
+
content: text,
|
|
576
|
+
...images.length ? { images } : {}
|
|
577
|
+
});
|
|
578
|
+
} else if (msg.role === "assistant") {
|
|
579
|
+
const text = extractTextContent(msg.content);
|
|
580
|
+
const toolCalls = extractToolCalls(msg.content, options);
|
|
581
|
+
result.push({
|
|
582
|
+
role: "assistant",
|
|
583
|
+
content: text,
|
|
584
|
+
...toolCalls.length ? { tool_calls: toolCalls } : {}
|
|
585
|
+
});
|
|
586
|
+
} else if (msg.role === "tool" || msg.role === "toolResult") {
|
|
587
|
+
const toolName = typeof msg.toolName === "string" ? msg.toolName : undefined;
|
|
588
|
+
result.push({
|
|
589
|
+
role: "tool",
|
|
590
|
+
content: extractTextContent(msg.content),
|
|
591
|
+
...toolName ? { tool_name: toolName } : {}
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return result;
|
|
596
|
+
}
|
|
597
|
+
function extractOllamaTools(tools) {
|
|
598
|
+
return (tools ?? []).filter((tool) => typeof tool.name === "string" && tool.name).map((tool) => ({
|
|
599
|
+
type: "function",
|
|
600
|
+
function: {
|
|
601
|
+
name: tool.name,
|
|
602
|
+
description: typeof tool.description === "string" ? tool.description : "",
|
|
603
|
+
parameters: normalizeOllamaToolSchema(tool.parameters, true)
|
|
604
|
+
}
|
|
605
|
+
}));
|
|
606
|
+
}
|
|
607
|
+
function buildAssistantMessage(response, modelInfo, usageFallback, options = {}) {
|
|
608
|
+
const content = [];
|
|
609
|
+
const text = response.message.content || "";
|
|
610
|
+
if (text) {
|
|
611
|
+
content.push({
|
|
612
|
+
type: "text",
|
|
613
|
+
text
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
const toolCalls = response.message.tool_calls;
|
|
617
|
+
if (toolCalls?.length) {
|
|
618
|
+
for (const toolCall of toolCalls) {
|
|
619
|
+
content.push({
|
|
620
|
+
type: "toolCall",
|
|
621
|
+
id: readOllamaToolCallId(toolCall.id) ?? `ollama_call_${randomUUID()}`,
|
|
622
|
+
name: normalizeOllamaToolCallName(toolCall.function.name, options),
|
|
623
|
+
arguments: ensureArgsObject(toolCall.function.arguments)
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return buildStreamAssistantMessage({
|
|
628
|
+
model: modelInfo,
|
|
629
|
+
content,
|
|
630
|
+
stopReason: toolCalls?.length ? "toolUse" : "stop",
|
|
631
|
+
usage: buildUsageWithNoCost({
|
|
632
|
+
input: resolveUsageCount(response.prompt_eval_count, usageFallback?.input),
|
|
633
|
+
output: resolveUsageCount(response.eval_count, usageFallback?.output)
|
|
634
|
+
})
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
async function* parseNdjsonStream(reader) {
|
|
638
|
+
const decoder = new TextDecoder();
|
|
639
|
+
let buffer = "";
|
|
640
|
+
while (true) {
|
|
641
|
+
const { done, value } = await reader.read();
|
|
642
|
+
if (done) {
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
buffer += decoder.decode(value, { stream: true });
|
|
646
|
+
const lines = buffer.split("\n");
|
|
647
|
+
buffer = lines.pop() ?? "";
|
|
648
|
+
for (const line of lines) {
|
|
649
|
+
const trimmed = line.trim();
|
|
650
|
+
if (trimmed) {
|
|
651
|
+
yield normalizeOllamaResponseToolArgs(parseJsonPreservingUnsafeIntegers(trimmed));
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (buffer.trim()) {
|
|
656
|
+
yield normalizeOllamaResponseToolArgs(parseJsonPreservingUnsafeIntegers(buffer.trim()));
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
function resolveOllamaChatUrl(baseUrl) {
|
|
660
|
+
const trimmed = baseUrl.trim().replace(/\/+$/, "");
|
|
661
|
+
const normalizedBase = trimmed.replace(/\/v1$/i, "");
|
|
662
|
+
return `${normalizedBase || OLLAMA_NATIVE_BASE_URL}/api/chat`;
|
|
663
|
+
}
|
|
664
|
+
function resolveOllamaModelHeaders(model) {
|
|
665
|
+
return model.headers && typeof model.headers === "object" && !Array.isArray(model.headers) ? model.headers : undefined;
|
|
666
|
+
}
|
|
667
|
+
function resolveOllamaModelOptions(model) {
|
|
668
|
+
const options = {};
|
|
669
|
+
for (const [key, value] of Object.entries(model.params ?? {})) {
|
|
670
|
+
if (key !== "num_ctx" && value !== undefined && OLLAMA_OPTION_PARAM_KEYS.has(key)) {
|
|
671
|
+
options[key] = value;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
const numCtx = resolveOllamaNumCtx(model);
|
|
675
|
+
if (numCtx !== undefined) {
|
|
676
|
+
options.num_ctx = numCtx;
|
|
677
|
+
}
|
|
678
|
+
return options;
|
|
679
|
+
}
|
|
680
|
+
function resolveOllamaTopLevelParams(model) {
|
|
681
|
+
const requestParams = {};
|
|
682
|
+
for (const [key, value] of Object.entries(model.params ?? {})) {
|
|
683
|
+
if (value !== undefined && OLLAMA_TOP_LEVEL_PARAM_KEYS.has(key)) {
|
|
684
|
+
requestParams[key] = value;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
const think = resolveOllamaThinkParamValue(model.params);
|
|
688
|
+
if (think !== undefined && shouldForwardNativeOllamaThink(model, think)) {
|
|
689
|
+
requestParams.think = think;
|
|
690
|
+
}
|
|
691
|
+
return Object.keys(requestParams).length ? requestParams : undefined;
|
|
692
|
+
}
|
|
693
|
+
function isNonSecretApiKeyMarker(value) {
|
|
694
|
+
return typeof value === "string" && /(?:^|[-_])(local|placeholder|none|dummy)$/i.test(value);
|
|
695
|
+
}
|
|
696
|
+
function createOllamaStreamFn(baseUrl, defaultHeaders) {
|
|
697
|
+
const chatUrl = resolveOllamaChatUrl(baseUrl);
|
|
698
|
+
return (model, context, options) => {
|
|
699
|
+
const stream = createAssistantMessageEventStream();
|
|
700
|
+
const run = async () => {
|
|
701
|
+
try {
|
|
702
|
+
const availableToolNames = buildOllamaToolNameSet(context.tools);
|
|
703
|
+
const toolCallNameOptions = availableToolNames ? { availableToolNames } : {};
|
|
704
|
+
const ollamaMessages = convertToOllamaMessages(context.messages ?? [], context.systemPrompt, toolCallNameOptions);
|
|
705
|
+
const ollamaTools = extractOllamaTools(context.tools);
|
|
706
|
+
const ollamaOptions = resolveOllamaModelOptions(model);
|
|
707
|
+
if (typeof options?.temperature === "number") {
|
|
708
|
+
ollamaOptions.temperature = options.temperature;
|
|
709
|
+
}
|
|
710
|
+
if (typeof options?.maxTokens === "number") {
|
|
711
|
+
ollamaOptions.num_predict = options.maxTokens;
|
|
712
|
+
}
|
|
713
|
+
const body = buildOllamaChatRequest({
|
|
714
|
+
modelId: model.id,
|
|
715
|
+
providerId: model.provider,
|
|
716
|
+
messages: ollamaMessages,
|
|
717
|
+
tools: ollamaTools,
|
|
718
|
+
options: ollamaOptions,
|
|
719
|
+
requestParams: resolveOllamaTopLevelParams(model),
|
|
720
|
+
stream: true
|
|
721
|
+
});
|
|
722
|
+
(options?.onPayload)?.(body, model);
|
|
723
|
+
const headers = {
|
|
724
|
+
"Content-Type": "application/json",
|
|
725
|
+
...defaultHeaders,
|
|
726
|
+
...options?.headers
|
|
727
|
+
};
|
|
728
|
+
const apiKey = options?.apiKey;
|
|
729
|
+
if (apiKey && (!headers.Authorization || !isNonSecretApiKeyMarker(apiKey))) {
|
|
730
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
731
|
+
}
|
|
732
|
+
const response = await fetch(chatUrl, {
|
|
733
|
+
method: "POST",
|
|
734
|
+
headers,
|
|
735
|
+
body: JSON.stringify(body),
|
|
736
|
+
...options?.signal ? { signal: options.signal } : {}
|
|
737
|
+
});
|
|
738
|
+
if (!response.ok) {
|
|
739
|
+
const errorText = await response.text().catch(() => "unknown error");
|
|
740
|
+
throw new Error(`${response.status} ${errorText}`);
|
|
741
|
+
}
|
|
742
|
+
if (!response.body) {
|
|
743
|
+
throw new Error("Ollama API returned empty response body");
|
|
744
|
+
}
|
|
745
|
+
const reader = response.body.getReader();
|
|
746
|
+
let accumulatedContent = "";
|
|
747
|
+
let accumulatedThinking = "";
|
|
748
|
+
const accumulatedToolCalls = [];
|
|
749
|
+
let finalResponse;
|
|
750
|
+
const modelInfo = {
|
|
751
|
+
api: model.api,
|
|
752
|
+
provider: model.provider,
|
|
753
|
+
id: model.id
|
|
754
|
+
};
|
|
755
|
+
let streamStarted = false;
|
|
756
|
+
let thinkingStarted = false;
|
|
757
|
+
let thinkingEnded = false;
|
|
758
|
+
let textBlockStarted = false;
|
|
759
|
+
let textBlockClosed = false;
|
|
760
|
+
const textContentIndex = () => thinkingStarted ? 1 : 0;
|
|
761
|
+
const buildCurrentContent = () => {
|
|
762
|
+
const parts = [];
|
|
763
|
+
if (accumulatedThinking) {
|
|
764
|
+
parts.push({
|
|
765
|
+
type: "thinking",
|
|
766
|
+
thinking: accumulatedThinking
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
if (accumulatedContent) {
|
|
770
|
+
parts.push({
|
|
771
|
+
type: "text",
|
|
772
|
+
text: accumulatedContent
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
return parts;
|
|
776
|
+
};
|
|
777
|
+
const closeThinkingBlock = () => {
|
|
778
|
+
if (!thinkingStarted || thinkingEnded) {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
thinkingEnded = true;
|
|
782
|
+
stream.push({
|
|
783
|
+
type: "thinking_end",
|
|
784
|
+
contentIndex: 0,
|
|
785
|
+
content: accumulatedThinking,
|
|
786
|
+
partial: buildStreamAssistantMessage({
|
|
787
|
+
model: modelInfo,
|
|
788
|
+
content: buildCurrentContent(),
|
|
789
|
+
stopReason: "stop",
|
|
790
|
+
usage: buildUsageWithNoCost({})
|
|
791
|
+
})
|
|
792
|
+
});
|
|
793
|
+
};
|
|
794
|
+
const closeTextBlock = () => {
|
|
795
|
+
if (!textBlockStarted || textBlockClosed) {
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
textBlockClosed = true;
|
|
799
|
+
stream.push({
|
|
800
|
+
type: "text_end",
|
|
801
|
+
contentIndex: textContentIndex(),
|
|
802
|
+
content: accumulatedContent,
|
|
803
|
+
partial: buildStreamAssistantMessage({
|
|
804
|
+
model: modelInfo,
|
|
805
|
+
content: buildCurrentContent(),
|
|
806
|
+
stopReason: "stop",
|
|
807
|
+
usage: buildUsageWithNoCost({})
|
|
808
|
+
})
|
|
809
|
+
});
|
|
810
|
+
};
|
|
811
|
+
for await (const chunk of parseNdjsonStream(reader)) {
|
|
812
|
+
const thinkingDelta = chunk.message?.thinking ?? chunk.message?.reasoning;
|
|
813
|
+
if (thinkingDelta) {
|
|
814
|
+
if (!streamStarted) {
|
|
815
|
+
streamStarted = true;
|
|
816
|
+
stream.push({
|
|
817
|
+
type: "start",
|
|
818
|
+
partial: buildStreamAssistantMessage({
|
|
819
|
+
model: modelInfo,
|
|
820
|
+
content: [],
|
|
821
|
+
stopReason: "stop",
|
|
822
|
+
usage: buildUsageWithNoCost({})
|
|
823
|
+
})
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
if (!thinkingStarted) {
|
|
827
|
+
thinkingStarted = true;
|
|
828
|
+
stream.push({
|
|
829
|
+
type: "thinking_start",
|
|
830
|
+
contentIndex: 0,
|
|
831
|
+
partial: buildStreamAssistantMessage({
|
|
832
|
+
model: modelInfo,
|
|
833
|
+
content: buildCurrentContent(),
|
|
834
|
+
stopReason: "stop",
|
|
835
|
+
usage: buildUsageWithNoCost({})
|
|
836
|
+
})
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
accumulatedThinking += thinkingDelta;
|
|
840
|
+
stream.push({
|
|
841
|
+
type: "thinking_delta",
|
|
842
|
+
contentIndex: 0,
|
|
843
|
+
delta: thinkingDelta,
|
|
844
|
+
partial: buildStreamAssistantMessage({
|
|
845
|
+
model: modelInfo,
|
|
846
|
+
content: buildCurrentContent(),
|
|
847
|
+
stopReason: "stop",
|
|
848
|
+
usage: buildUsageWithNoCost({})
|
|
849
|
+
})
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
if (chunk.message?.content) {
|
|
853
|
+
const delta = chunk.message.content;
|
|
854
|
+
if (thinkingStarted && !thinkingEnded) {
|
|
855
|
+
closeThinkingBlock();
|
|
856
|
+
}
|
|
857
|
+
if (!streamStarted) {
|
|
858
|
+
streamStarted = true;
|
|
859
|
+
stream.push({
|
|
860
|
+
type: "start",
|
|
861
|
+
partial: buildStreamAssistantMessage({
|
|
862
|
+
model: modelInfo,
|
|
863
|
+
content: [],
|
|
864
|
+
stopReason: "stop",
|
|
865
|
+
usage: buildUsageWithNoCost({})
|
|
866
|
+
})
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
if (!textBlockStarted) {
|
|
870
|
+
textBlockStarted = true;
|
|
871
|
+
stream.push({
|
|
872
|
+
type: "text_start",
|
|
873
|
+
contentIndex: textContentIndex(),
|
|
874
|
+
partial: buildStreamAssistantMessage({
|
|
875
|
+
model: modelInfo,
|
|
876
|
+
content: buildCurrentContent(),
|
|
877
|
+
stopReason: "stop",
|
|
878
|
+
usage: buildUsageWithNoCost({})
|
|
879
|
+
})
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
accumulatedContent += delta;
|
|
883
|
+
stream.push({
|
|
884
|
+
type: "text_delta",
|
|
885
|
+
contentIndex: textContentIndex(),
|
|
886
|
+
delta,
|
|
887
|
+
partial: buildStreamAssistantMessage({
|
|
888
|
+
model: modelInfo,
|
|
889
|
+
content: buildCurrentContent(),
|
|
890
|
+
stopReason: "stop",
|
|
891
|
+
usage: buildUsageWithNoCost({})
|
|
892
|
+
})
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
if (chunk.message?.tool_calls) {
|
|
896
|
+
closeThinkingBlock();
|
|
897
|
+
closeTextBlock();
|
|
898
|
+
accumulatedToolCalls.push(...chunk.message.tool_calls);
|
|
899
|
+
}
|
|
900
|
+
if (chunk.done) {
|
|
901
|
+
finalResponse = chunk;
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
if (!finalResponse) {
|
|
906
|
+
throw new Error("Ollama API stream ended without a final response");
|
|
907
|
+
}
|
|
908
|
+
if (isLikelyGarbledVisibleText({
|
|
909
|
+
text: accumulatedContent,
|
|
910
|
+
modelId: model.id
|
|
911
|
+
})) {
|
|
912
|
+
throw new Error(`Ollama returned non-linguistic garbled visible text for ${model.id}; retry or switch models`);
|
|
913
|
+
}
|
|
914
|
+
finalResponse.message.content = accumulatedContent;
|
|
915
|
+
if (accumulatedContent) {
|
|
916
|
+
delete finalResponse.message.thinking;
|
|
917
|
+
delete finalResponse.message.reasoning;
|
|
918
|
+
} else if (accumulatedThinking) {
|
|
919
|
+
finalResponse.message.thinking = "";
|
|
920
|
+
}
|
|
921
|
+
if (accumulatedToolCalls.length > 0) {
|
|
922
|
+
finalResponse.message.tool_calls = accumulatedToolCalls;
|
|
923
|
+
}
|
|
924
|
+
const assistantMessage = buildAssistantMessage(finalResponse, modelInfo, {
|
|
925
|
+
input: estimateOllamaPromptTokens({
|
|
926
|
+
messages: ollamaMessages,
|
|
927
|
+
tools: ollamaTools
|
|
928
|
+
}),
|
|
929
|
+
output: estimateOllamaCompletionTokens(finalResponse)
|
|
930
|
+
}, toolCallNameOptions);
|
|
931
|
+
closeThinkingBlock();
|
|
932
|
+
closeTextBlock();
|
|
933
|
+
stream.push({
|
|
934
|
+
type: "done",
|
|
935
|
+
reason: assistantMessage.stopReason === "toolUse" ? "toolUse" : "stop",
|
|
936
|
+
message: assistantMessage
|
|
937
|
+
});
|
|
938
|
+
} catch (err) {
|
|
939
|
+
stream.push({
|
|
940
|
+
type: "error",
|
|
941
|
+
reason: "error",
|
|
942
|
+
error: buildStreamErrorAssistantMessage({
|
|
943
|
+
model: {
|
|
944
|
+
api: String(model.api),
|
|
945
|
+
provider: String(model.provider),
|
|
946
|
+
id: String(model.id)
|
|
947
|
+
},
|
|
948
|
+
errorMessage: formatErrorMessage(err)
|
|
949
|
+
})
|
|
950
|
+
});
|
|
951
|
+
} finally {
|
|
952
|
+
stream.end();
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
queueMicrotask(() => void run());
|
|
956
|
+
return stream;
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
function readStringValue(value) {
|
|
960
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
961
|
+
}
|
|
962
|
+
function createConfiguredOllamaStreamFn(params) {
|
|
963
|
+
return createOllamaStreamFn(resolveOllamaBaseUrlForRun({
|
|
964
|
+
modelBaseUrl: readStringValue(params.model.baseUrl),
|
|
965
|
+
providerBaseUrl: params.providerBaseUrl
|
|
966
|
+
}), resolveOllamaModelHeaders(params.model));
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
//#endregion
|
|
970
|
+
export { createConfiguredOllamaCompatStreamWrapper, createConfiguredOllamaStreamFn };
|