@proposit/proposit-core 1.5.1 → 1.7.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/dist/extensions/argument-ingestion/shared/resolve-llm-stage-options.d.ts.map +1 -1
- package/dist/extensions/argument-ingestion/shared/resolve-llm-stage-options.js +6 -0
- package/dist/extensions/argument-ingestion/shared/resolve-llm-stage-options.js.map +1 -1
- package/dist/extensions/argument-ingestion/shared/types.d.ts +18 -7
- package/dist/extensions/argument-ingestion/shared/types.d.ts.map +1 -1
- package/dist/extensions/argument-ingestion/stages/axiom-indicator-detection.d.ts.map +1 -1
- package/dist/extensions/argument-ingestion/stages/axiom-indicator-detection.js +4 -2
- package/dist/extensions/argument-ingestion/stages/axiom-indicator-detection.js.map +1 -1
- package/dist/extensions/argument-ingestion/stages/citation-source-detection.d.ts.map +1 -1
- package/dist/extensions/argument-ingestion/stages/citation-source-detection.js +4 -2
- package/dist/extensions/argument-ingestion/stages/citation-source-detection.js.map +1 -1
- package/dist/extensions/argument-ingestion/stages/claim-canonicalization.d.ts.map +1 -1
- package/dist/extensions/argument-ingestion/stages/claim-canonicalization.js +2 -1
- package/dist/extensions/argument-ingestion/stages/claim-canonicalization.js.map +1 -1
- package/dist/extensions/argument-ingestion/stages/claim-mention-extraction.d.ts.map +1 -1
- package/dist/extensions/argument-ingestion/stages/claim-mention-extraction.js +4 -2
- package/dist/extensions/argument-ingestion/stages/claim-mention-extraction.js.map +1 -1
- package/dist/extensions/argument-ingestion/stages/claim-type-classification.d.ts.map +1 -1
- package/dist/extensions/argument-ingestion/stages/claim-type-classification.js +4 -2
- package/dist/extensions/argument-ingestion/stages/claim-type-classification.js.map +1 -1
- package/dist/extensions/argument-ingestion/stages/conclusion-selection.d.ts.map +1 -1
- package/dist/extensions/argument-ingestion/stages/conclusion-selection.js +2 -1
- package/dist/extensions/argument-ingestion/stages/conclusion-selection.js.map +1 -1
- package/dist/extensions/argument-ingestion/stages/relation-extraction.d.ts.map +1 -1
- package/dist/extensions/argument-ingestion/stages/relation-extraction.js +2 -1
- package/dist/extensions/argument-ingestion/stages/relation-extraction.js.map +1 -1
- package/dist/extensions/argument-ingestion/stages/segmentation.d.ts.map +1 -1
- package/dist/extensions/argument-ingestion/stages/segmentation.js +2 -1
- package/dist/extensions/argument-ingestion/stages/segmentation.js.map +1 -1
- package/dist/extensions/ollama/errors.d.ts +73 -0
- package/dist/extensions/ollama/errors.d.ts.map +1 -0
- package/dist/extensions/ollama/errors.js +228 -0
- package/dist/extensions/ollama/errors.js.map +1 -0
- package/dist/extensions/ollama/index.d.ts +6 -0
- package/dist/extensions/ollama/index.d.ts.map +1 -0
- package/dist/extensions/ollama/index.js +17 -0
- package/dist/extensions/ollama/index.js.map +1 -0
- package/dist/extensions/ollama/provider.d.ts +21 -0
- package/dist/extensions/ollama/provider.d.ts.map +1 -0
- package/dist/extensions/ollama/provider.js +375 -0
- package/dist/extensions/ollama/provider.js.map +1 -0
- package/dist/extensions/ollama/structured-output.d.ts +18 -0
- package/dist/extensions/ollama/structured-output.d.ts.map +1 -0
- package/dist/extensions/ollama/structured-output.js +164 -0
- package/dist/extensions/ollama/structured-output.js.map +1 -0
- package/dist/extensions/ollama/timeout-fetch.d.ts +24 -0
- package/dist/extensions/ollama/timeout-fetch.d.ts.map +1 -0
- package/dist/extensions/ollama/timeout-fetch.js +76 -0
- package/dist/extensions/ollama/timeout-fetch.js.map +1 -0
- package/dist/extensions/ollama/types.d.ts +179 -0
- package/dist/extensions/ollama/types.d.ts.map +1 -0
- package/dist/extensions/ollama/types.js +7 -0
- package/dist/extensions/ollama/types.js.map +1 -0
- package/dist/extensions/openai/provider.d.ts +26 -0
- package/dist/extensions/openai/provider.d.ts.map +1 -1
- package/dist/extensions/openai/provider.js +334 -9
- package/dist/extensions/openai/provider.js.map +1 -1
- package/dist/extensions/openai/types.d.ts +3 -0
- package/dist/extensions/openai/types.d.ts.map +1 -1
- package/package.json +17 -2
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// Ollama-provider error classes + the `classifyOllamaError` mapping.
|
|
2
|
+
//
|
|
3
|
+
// The framework's `llmStage` retry policy classifies provider errors
|
|
4
|
+
// by inspecting a `retryReason` tag on the thrown object (see
|
|
5
|
+
// `src/lib/pipelines/stage-helpers.ts` #classifyError) — NOT by class
|
|
6
|
+
// identity. The failure *codes* live in the SDK-free
|
|
7
|
+
// `src/lib/pipelines/failure-codes.ts`. So these Ollama classes carry
|
|
8
|
+
// the same `retryReason` tags as the OpenAI provider's classes and set
|
|
9
|
+
// their `code` from the lib constants. They deliberately do NOT import
|
|
10
|
+
// from `extensions/openai/` and do NOT touch `src/lib/`.
|
|
11
|
+
//
|
|
12
|
+
// Mapping for the framework's default retry policy
|
|
13
|
+
// (`retryOn: ["schema_validation", "transient"]`):
|
|
14
|
+
//
|
|
15
|
+
// * `TransientLlmError` — `retryReason: "transient"`. Genuinely
|
|
16
|
+
// transient local hiccups: mid-stream `ECONNRESET`/socket drop,
|
|
17
|
+
// undici timeout cause-codes (`UND_ERR_HEADERS_TIMEOUT` /
|
|
18
|
+
// `UND_ERR_BODY_TIMEOUT` / `UND_ERR_CONNECT_TIMEOUT` — a long local
|
|
19
|
+
// thinking-model generation that outran the dispatcher timeout),
|
|
20
|
+
// cold-model-load 5xx (model pulled but still loading into VRAM),
|
|
21
|
+
// generic 5xx. Retried by the default policy.
|
|
22
|
+
// * `RateLimitLlmError` — `retryReason: "rate_limit"`. A local
|
|
23
|
+
// daemon rarely rate-limits, but a remote-Ollama / proxy setup can
|
|
24
|
+
// return 429; mapped for contract parity. Not in the default
|
|
25
|
+
// `retryOn`, so it fails fast (callers can opt in).
|
|
26
|
+
// * `SchemaValidationLlmError` — tagged `transient`; the framework's
|
|
27
|
+
// schema-retry path handles it. Thrown only for genuinely
|
|
28
|
+
// malformed / non-parseable JSON in the model's reply — NOT for a
|
|
29
|
+
// context-overflow (which is deterministic; see below).
|
|
30
|
+
// * `NonRetryableLlmError` — no tag; framework surfaces it
|
|
31
|
+
// immediately as `LLM_NON_RETRYABLE_ERROR`. Used for: daemon
|
|
32
|
+
// unreachable (`ECONNREFUSED`), model-not-pulled (404), and
|
|
33
|
+
// context-length / eval errors (deterministic — a retry re-fails).
|
|
34
|
+
// * `ToolLoopExhaustedError` — surfaces from the function-tool loop
|
|
35
|
+
// when the round cap is hit. Non-retryable (no tag).
|
|
36
|
+
import { LLM_NON_RETRYABLE_ERROR, LLM_RATE_LIMITED, LLM_TRANSIENT_ERROR, } from "../../lib/pipelines/failure-codes.js";
|
|
37
|
+
export class TransientLlmError extends Error {
|
|
38
|
+
retryReason = "transient";
|
|
39
|
+
code = LLM_TRANSIENT_ERROR;
|
|
40
|
+
status;
|
|
41
|
+
constructor(args) {
|
|
42
|
+
super(args.message);
|
|
43
|
+
this.name = "TransientLlmError";
|
|
44
|
+
this.status = args.status;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export class RateLimitLlmError extends Error {
|
|
48
|
+
retryReason = "rate_limit";
|
|
49
|
+
code = LLM_RATE_LIMITED;
|
|
50
|
+
status;
|
|
51
|
+
constructor(args) {
|
|
52
|
+
super(args.message);
|
|
53
|
+
this.name = "RateLimitLlmError";
|
|
54
|
+
this.status = args.status;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Thrown when the model's structured-output reply is genuinely
|
|
59
|
+
* malformed / non-parseable JSON. Tagged `transient` so the framework's
|
|
60
|
+
* default retry policy retries — a single re-roll often produces
|
|
61
|
+
* conforming output.
|
|
62
|
+
*
|
|
63
|
+
* **Do not** route a context-length overflow here: an overflow is
|
|
64
|
+
* deterministic and re-fails on the retried (still-oversized) prompt.
|
|
65
|
+
* `classifyOllamaError` routes overflow to {@link NonRetryableLlmError}.
|
|
66
|
+
*/
|
|
67
|
+
export class SchemaValidationLlmError extends Error {
|
|
68
|
+
retryReason = "transient";
|
|
69
|
+
status;
|
|
70
|
+
constructor(args) {
|
|
71
|
+
super(args.message);
|
|
72
|
+
this.name = "SchemaValidationLlmError";
|
|
73
|
+
this.status = args.status;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export class NonRetryableLlmError extends Error {
|
|
77
|
+
code = LLM_NON_RETRYABLE_ERROR;
|
|
78
|
+
status;
|
|
79
|
+
constructor(args) {
|
|
80
|
+
super(args.message);
|
|
81
|
+
this.name = "NonRetryableLlmError";
|
|
82
|
+
this.status = args.status;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export class ToolLoopExhaustedError extends Error {
|
|
86
|
+
rounds;
|
|
87
|
+
constructor(args) {
|
|
88
|
+
super(args.message);
|
|
89
|
+
this.name = "ToolLoopExhaustedError";
|
|
90
|
+
this.rounds = args.rounds;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// -- error-shape probes ---------------------------------------------------
|
|
94
|
+
//
|
|
95
|
+
// The `ollama` SDK throws plain `Error`s (sometimes a `ResponseError`
|
|
96
|
+
// carrying a `status_code`) and lets low-level `fetch` failures bubble
|
|
97
|
+
// up. undici wraps a connection refusal as `TypeError: fetch failed`
|
|
98
|
+
// with a `.cause` carrying the Node `code`. We probe both the error
|
|
99
|
+
// itself and one level of `.cause` for the Node-style `code` and an
|
|
100
|
+
// HTTP-ish `status` / `status_code`.
|
|
101
|
+
function nodeCodeOf(err) {
|
|
102
|
+
const direct = readCode(err);
|
|
103
|
+
if (direct !== undefined)
|
|
104
|
+
return direct;
|
|
105
|
+
if (typeof err === "object" && err !== null) {
|
|
106
|
+
return readCode(err.cause);
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
function readCode(value) {
|
|
111
|
+
if (typeof value !== "object" || value === null)
|
|
112
|
+
return undefined;
|
|
113
|
+
const code = value.code;
|
|
114
|
+
return typeof code === "string" ? code : undefined;
|
|
115
|
+
}
|
|
116
|
+
function statusOf(err) {
|
|
117
|
+
if (typeof err !== "object" || err === null)
|
|
118
|
+
return undefined;
|
|
119
|
+
// `status_code` is the `ollama` SDK's ResponseError wire field — an
|
|
120
|
+
// external snake_case name, exempt from the camelCase rule.
|
|
121
|
+
/* eslint-disable @typescript-eslint/naming-convention */
|
|
122
|
+
const e = err;
|
|
123
|
+
if (typeof e.status === "number")
|
|
124
|
+
return e.status;
|
|
125
|
+
if (typeof e.status_code === "number")
|
|
126
|
+
return e.status_code;
|
|
127
|
+
/* eslint-enable @typescript-eslint/naming-convention */
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
function messageOf(err) {
|
|
131
|
+
if (err instanceof Error)
|
|
132
|
+
return err.message;
|
|
133
|
+
if (typeof err === "string")
|
|
134
|
+
return err;
|
|
135
|
+
return String(err);
|
|
136
|
+
}
|
|
137
|
+
const CONTEXT_OVERFLOW_PATTERN = /context (?:length|window)|maximum context|exceeds the (?:maximum )?context|too (?:many|long).*token|num_ctx/i;
|
|
138
|
+
const MODEL_NOT_FOUND_PATTERN = /model .* not found|not found, try pulling|no such model|pull(?:ing)? it/i;
|
|
139
|
+
/**
|
|
140
|
+
* Map an error surfaced by the `ollama` SDK (or an underlying `fetch`
|
|
141
|
+
* failure) to one of the framework-recognized provider error classes.
|
|
142
|
+
*
|
|
143
|
+
* The mapping is deliberately exhaustive on the cases the reviewer
|
|
144
|
+
* called out so they can't silently regress:
|
|
145
|
+
*
|
|
146
|
+
* - `ECONNREFUSED` → NonRetryable (daemon down; a retry
|
|
147
|
+
* won't bring it up within backoff).
|
|
148
|
+
* - model-not-pulled (404) → NonRetryable (`ollama pull` hint).
|
|
149
|
+
* - context-overflow/eval err → NonRetryable (deterministic; never
|
|
150
|
+
* SchemaValidationLlmError).
|
|
151
|
+
* - `ECONNRESET` / socket drop→ Transient.
|
|
152
|
+
* - undici timeout cause-codes→ Transient (`UND_ERR_HEADERS_TIMEOUT` /
|
|
153
|
+
* `UND_ERR_BODY_TIMEOUT` / `UND_ERR_CONNECT_TIMEOUT`; a long local
|
|
154
|
+
* thinking-model generation outran the timeout — retryable).
|
|
155
|
+
* - cold-load / generic 5xx → Transient.
|
|
156
|
+
* - 429 → RateLimit (remote/proxy setups).
|
|
157
|
+
* - anything else → NonRetryable (safe fail-fast default).
|
|
158
|
+
*/
|
|
159
|
+
export function classifyOllamaError(err) {
|
|
160
|
+
const code = nodeCodeOf(err);
|
|
161
|
+
const status = statusOf(err);
|
|
162
|
+
const message = messageOf(err);
|
|
163
|
+
// Daemon unreachable — a connection refusal is the canonical
|
|
164
|
+
// "`ollama serve` isn't running on :11434" failure. A retry within
|
|
165
|
+
// the framework's short backoff won't bring it back up.
|
|
166
|
+
if (code === "ECONNREFUSED") {
|
|
167
|
+
return new NonRetryableLlmError({
|
|
168
|
+
message: `Cannot reach the Ollama daemon (${code}). Is \`ollama serve\` running on :11434? Original error: ${message}`,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
// Mid-stream socket drop / transient connection loss — retryable.
|
|
172
|
+
// Includes undici's timeout cause-codes: a long local thinking-model
|
|
173
|
+
// generation that outruns the dispatcher's headers/body timeout (or a
|
|
174
|
+
// connect timeout) is transient against a still-working daemon, NOT a
|
|
175
|
+
// deterministic failure. The framework's default `retryOn: ["transient"]`
|
|
176
|
+
// then retries instead of dying `LLM_NON_RETRYABLE_ERROR`. undici wraps
|
|
177
|
+
// these as a `TypeError: fetch failed` whose `.cause.code` is the
|
|
178
|
+
// `UND_ERR_*` value — `nodeCodeOf` already probes one level of `.cause`.
|
|
179
|
+
if (code === "ECONNRESET" ||
|
|
180
|
+
code === "ETIMEDOUT" ||
|
|
181
|
+
code === "EPIPE" ||
|
|
182
|
+
code === "UND_ERR_HEADERS_TIMEOUT" ||
|
|
183
|
+
code === "UND_ERR_BODY_TIMEOUT" ||
|
|
184
|
+
code === "UND_ERR_CONNECT_TIMEOUT") {
|
|
185
|
+
return new TransientLlmError({
|
|
186
|
+
message: `Transient connection error talking to the Ollama daemon (${code}): ${message}`,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
// Model not pulled — Ollama returns a 404 whose body says the model
|
|
190
|
+
// was not found. Deterministic until the user pulls it.
|
|
191
|
+
if (status === 404 || MODEL_NOT_FOUND_PATTERN.test(message)) {
|
|
192
|
+
return new NonRetryableLlmError({
|
|
193
|
+
message: `Ollama model not found. Run \`ollama pull <model>\` to download it first. Original error: ${message}`,
|
|
194
|
+
status,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
// Context-length overflow / model-eval error — DETERMINISTIC. Must
|
|
198
|
+
// be NonRetryable: routing this to SchemaValidationLlmError (tagged
|
|
199
|
+
// `transient`) would burn a guaranteed-failing second attempt on
|
|
200
|
+
// the same oversized prompt.
|
|
201
|
+
if (CONTEXT_OVERFLOW_PATTERN.test(message)) {
|
|
202
|
+
return new NonRetryableLlmError({
|
|
203
|
+
message: `Ollama request exceeded the model's context window (deterministic — retrying the same prompt will re-fail). Shorten the input or raise \`num_ctx\`. Original error: ${message}`,
|
|
204
|
+
status,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
if (status === 429) {
|
|
208
|
+
return new RateLimitLlmError({
|
|
209
|
+
message: `Ollama returned 429 (rate-limited; typical of a remote/proxied daemon): ${message}`,
|
|
210
|
+
status,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
// Cold-model-load (model pulled but still loading into VRAM) and
|
|
214
|
+
// any other 5xx are transient — a retry after backoff can succeed
|
|
215
|
+
// once the model is resident.
|
|
216
|
+
if (status !== undefined && status >= 500) {
|
|
217
|
+
return new TransientLlmError({
|
|
218
|
+
message: `Ollama returned ${status.toString()} (possibly a cold model load into VRAM): ${message}`,
|
|
219
|
+
status,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
// Safe default: fail fast rather than retry an unrecognized error.
|
|
223
|
+
return new NonRetryableLlmError({
|
|
224
|
+
message: `Unclassified Ollama error: ${message}`,
|
|
225
|
+
status,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../../src/extensions/ollama/errors.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { OllamaProvider } from "./provider.js";
|
|
2
|
+
export type { TOllamaProviderConfig, TOllamaClient, TOllamaChatRequest, TOllamaChatResponse, } from "./types.js";
|
|
3
|
+
export { typeboxToJsonSchema } from "./structured-output.js";
|
|
4
|
+
export type { TOllamaJsonSchema } from "./structured-output.js";
|
|
5
|
+
export { NonRetryableLlmError, RateLimitLlmError, SchemaValidationLlmError, ToolLoopExhaustedError, TransientLlmError, classifyOllamaError, } from "./errors.js";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/extensions/ollama/index.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC9C,YAAY,EACR,qBAAqB,EACrB,aAAa,EACb,kBAAkB,EAClB,mBAAmB,GACtB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAA;AAC5D,YAAY,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC/D,OAAO,EACH,oBAAoB,EACpB,iBAAiB,EACjB,wBAAwB,EACxB,sBAAsB,EACtB,iBAAiB,EACjB,mBAAmB,GACtB,MAAM,aAAa,CAAA"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Barrel for the Ollama provider extension.
|
|
2
|
+
//
|
|
3
|
+
// Public surface consumed via the `@proposit/proposit-core/extensions/ollama`
|
|
4
|
+
// subpath export: the provider constructor + its config type + the
|
|
5
|
+
// standard-JSON-schema converter + the error classes (which callers may
|
|
6
|
+
// `instanceof`-match for finer-grained observability).
|
|
7
|
+
//
|
|
8
|
+
// NOTE: the error class names (`NonRetryableLlmError`, …) intentionally
|
|
9
|
+
// mirror the OpenAI provider's names but are *distinct* classes living
|
|
10
|
+
// in this extension. They are surfaced only from this subpath (NOT the
|
|
11
|
+
// package root) to avoid colliding with the root-exported OpenAI error
|
|
12
|
+
// classes. The framework classifies by the `retryReason` tag, not class
|
|
13
|
+
// identity, so the duplication is intentional and harmless.
|
|
14
|
+
export { OllamaProvider } from "./provider.js";
|
|
15
|
+
export { typeboxToJsonSchema } from "./structured-output.js";
|
|
16
|
+
export { NonRetryableLlmError, RateLimitLlmError, SchemaValidationLlmError, ToolLoopExhaustedError, TransientLlmError, classifyOllamaError, } from "./errors.js";
|
|
17
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/extensions/ollama/index.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,EAAE;AACF,8EAA8E;AAC9E,mEAAmE;AACnE,wEAAwE;AACxE,uDAAuD;AACvD,EAAE;AACF,wEAAwE;AACxE,uEAAuE;AACvE,uEAAuE;AACvE,uEAAuE;AACvE,wEAAwE;AACxE,4DAA4D;AAE5D,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAO9C,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAA;AAE5D,OAAO,EACH,oBAAoB,EACpB,iBAAiB,EACjB,wBAAwB,EACxB,sBAAsB,EACtB,iBAAiB,EACjB,mBAAmB,GACtB,MAAM,aAAa,CAAA"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { TLlmProvider, TLlmRequest, TLlmResponse } from "../../lib/llm/types.js";
|
|
2
|
+
import type { TOllamaProviderConfig } from "./types.js";
|
|
3
|
+
export declare class OllamaProvider implements TLlmProvider {
|
|
4
|
+
private readonly config;
|
|
5
|
+
private clientPromise;
|
|
6
|
+
private readonly maxToolRounds;
|
|
7
|
+
private readonly numCtx;
|
|
8
|
+
private readonly requestTimeoutMs;
|
|
9
|
+
private readonly stream;
|
|
10
|
+
constructor(config?: TOllamaProviderConfig);
|
|
11
|
+
respond<T>(req: TLlmRequest<T>): Promise<TLlmResponse<T>>;
|
|
12
|
+
private runChatLoop;
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the SDK client: the injected one, or a freshly imported
|
|
15
|
+
* `Ollama` instance. Memoized so the dynamic import + construction
|
|
16
|
+
* runs at most once.
|
|
17
|
+
*/
|
|
18
|
+
private getClient;
|
|
19
|
+
private importAndConstructClient;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../src/extensions/ollama/provider.ts"],"names":[],"mappings":"AAqCA,OAAO,KAAK,EACR,YAAY,EACZ,WAAW,EACX,YAAY,EAGf,MAAM,wBAAwB,CAAA;AAc/B,OAAO,KAAK,EAOR,qBAAqB,EAExB,MAAM,YAAY,CAAA;AAkBnB,qBAAa,cAAe,YAAW,YAAY;IAC/C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuB;IAC9C,OAAO,CAAC,aAAa,CAAsC;IAC3D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAQ;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAQ;IACzC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,MAAM,CAAC,EAAE,qBAAqB;IAUpC,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;YAmCjD,WAAW;IAiJzB;;;;OAIG;IACH,OAAO,CAAC,SAAS;YAQH,wBAAwB;CAiCzC"}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
// Concrete `TLlmProvider` backed by a local Ollama daemon via the
|
|
2
|
+
// official `ollama` npm SDK.
|
|
3
|
+
//
|
|
4
|
+
// Dev/test only — production stays on OpenAI. The provider exists so a
|
|
5
|
+
// developer can run the entire LLM-backed stack (notably the v2
|
|
6
|
+
// argument-ingestion pipeline) against a self-hosted model
|
|
7
|
+
// (`qwen3.6:latest`) with zero API cost.
|
|
8
|
+
//
|
|
9
|
+
// Deliberate divergences from the in-repo OpenAI provider:
|
|
10
|
+
// * Uses the official `ollama` SDK (an optional peer) rather than raw
|
|
11
|
+
// `fetch`. A missing package surfaces as an actionable error at
|
|
12
|
+
// construction time (dynamic-import-or-throw).
|
|
13
|
+
// * Structured output goes through the Ollama provider's own
|
|
14
|
+
// standard-JSON-schema converter (`./structured-output.ts`), not
|
|
15
|
+
// the OpenAI strict-mode converter.
|
|
16
|
+
// * `reasoningEffort` is ignored (no Ollama analogue);
|
|
17
|
+
// `maxOutputTokens` maps to `options.num_predict` (positive values
|
|
18
|
+
// only — never 0; -1/-2 are Ollama sentinels we never emit).
|
|
19
|
+
// * Thinking is left ON (the SDK/model default) — a prior finding
|
|
20
|
+
// showed `think: false` degrades structured-output fidelity (the
|
|
21
|
+
// model drops the required object wrapper → bare array, failing
|
|
22
|
+
// `Value.Check`). This trades latency (thinking-on stages can run
|
|
23
|
+
// several minutes) for correctness; the generous `requestTimeoutMs`
|
|
24
|
+
// default (see below) accommodates the latency.
|
|
25
|
+
// * A generous per-request timeout (`requestTimeoutMs`, default 20 min)
|
|
26
|
+
// is applied via a PER-PROVIDER undici `Agent` passed as the SDK
|
|
27
|
+
// client's `fetch` — never `setGlobalDispatcher`; a library must not
|
|
28
|
+
// mutate global state. See `./timeout-fetch.ts`.
|
|
29
|
+
// * Errors are classified by `./errors.ts` #classifyOllamaError, which
|
|
30
|
+
// carries the same `retryReason` tags + lib failure-codes as the
|
|
31
|
+
// OpenAI provider. No `ollama → openai` dependency, no lib change.
|
|
32
|
+
//
|
|
33
|
+
// `AbortSignal` is honored by registering an abort listener that calls
|
|
34
|
+
// the SDK client's `abort()`; the SDK then rejects the in-flight
|
|
35
|
+
// `chat()` with an `AbortError`, which the provider re-throws verbatim
|
|
36
|
+
// so `llmStage`'s mid-flight-abort detector marks the stage `skipped`.
|
|
37
|
+
import { debugLlmFailure, debugLlmRequest, debugLlmResponse, } from "../../lib/pipelines/debug-log.js";
|
|
38
|
+
import { typeboxToJsonSchema } from "./structured-output.js";
|
|
39
|
+
import { buildTimeoutFetch } from "./timeout-fetch.js";
|
|
40
|
+
import { NonRetryableLlmError, SchemaValidationLlmError, ToolLoopExhaustedError, classifyOllamaError, } from "./errors.js";
|
|
41
|
+
const STAGE_ID_MARKER = /<!--\s*stage-id:\s*([^\s>]+)\s*-->/;
|
|
42
|
+
const DEFAULT_BASE_URL = "http://localhost:11434";
|
|
43
|
+
const DEFAULT_MAX_TOOL_ROUNDS = 6;
|
|
44
|
+
// Generous default context window. Ollama silently truncates prompts
|
|
45
|
+
// longer than `num_ctx` (no error — the model emits schema-valid JSON
|
|
46
|
+
// from a truncated prompt), and its per-model default is often ~4096,
|
|
47
|
+
// well under a real multi-KB ingestion prompt. See `TOllamaProviderConfig.numCtx`.
|
|
48
|
+
const DEFAULT_NUM_CTX = 32768;
|
|
49
|
+
// Generous per-request timeout for local thinking models. undici's 300s
|
|
50
|
+
// default aborts long structured-extraction generations with
|
|
51
|
+
// UND_ERR_HEADERS_TIMEOUT; 20 min gives qwen3.6-with-thinking room. The
|
|
52
|
+
// timeout is applied via a PER-PROVIDER undici Agent (never global state)
|
|
53
|
+
// — see ./timeout-fetch.ts and TOllamaProviderConfig.requestTimeoutMs.
|
|
54
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 1_200_000;
|
|
55
|
+
export class OllamaProvider {
|
|
56
|
+
config;
|
|
57
|
+
clientPromise = null;
|
|
58
|
+
maxToolRounds;
|
|
59
|
+
numCtx;
|
|
60
|
+
requestTimeoutMs;
|
|
61
|
+
stream;
|
|
62
|
+
constructor(config) {
|
|
63
|
+
this.config = config ?? {};
|
|
64
|
+
this.maxToolRounds =
|
|
65
|
+
this.config.maxToolCallRounds ?? DEFAULT_MAX_TOOL_ROUNDS;
|
|
66
|
+
this.numCtx = this.config.numCtx ?? DEFAULT_NUM_CTX;
|
|
67
|
+
this.requestTimeoutMs =
|
|
68
|
+
this.config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
69
|
+
this.stream = this.config.stream ?? true;
|
|
70
|
+
}
|
|
71
|
+
async respond(req) {
|
|
72
|
+
// Already-aborted short-circuit — don't even construct the
|
|
73
|
+
// client or call the daemon.
|
|
74
|
+
if (req.signal?.aborted) {
|
|
75
|
+
throw abortError();
|
|
76
|
+
}
|
|
77
|
+
const client = await this.getClient();
|
|
78
|
+
const convertedSchema = typeboxToJsonSchema(req.outputSchema);
|
|
79
|
+
const tools = req.tools ? translateTools(req.tools) : undefined;
|
|
80
|
+
const stageIdMatch = STAGE_ID_MARKER.exec(req.systemPrompt);
|
|
81
|
+
const debugStageId = stageIdMatch ? stageIdMatch[1] : null;
|
|
82
|
+
// Wire the AbortSignal to the SDK client's abort(). The SDK
|
|
83
|
+
// rejects the in-flight chat() with an AbortError when this
|
|
84
|
+
// fires.
|
|
85
|
+
const onAbort = () => {
|
|
86
|
+
client.abort();
|
|
87
|
+
};
|
|
88
|
+
req.signal?.addEventListener("abort", onAbort, { once: true });
|
|
89
|
+
try {
|
|
90
|
+
return await this.runChatLoop({
|
|
91
|
+
client,
|
|
92
|
+
req,
|
|
93
|
+
convertedSchema,
|
|
94
|
+
tools,
|
|
95
|
+
debugStageId,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
req.signal?.removeEventListener("abort", onAbort);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async runChatLoop(args) {
|
|
103
|
+
const { client, req, convertedSchema, tools, debugStageId } = args;
|
|
104
|
+
// Running message array. Tool-call rounds append the model's
|
|
105
|
+
// tool_calls echo + the tool result before re-calling.
|
|
106
|
+
const messages = [
|
|
107
|
+
{ role: "system", content: req.systemPrompt },
|
|
108
|
+
{ role: "user", content: req.userMessage },
|
|
109
|
+
];
|
|
110
|
+
let lastUsage = { input: 0, output: 0 };
|
|
111
|
+
for (let round = 0; round < this.maxToolRounds; round += 1) {
|
|
112
|
+
const chatRequest = {
|
|
113
|
+
model: req.model,
|
|
114
|
+
messages,
|
|
115
|
+
// Build `format` from the single converted object so the
|
|
116
|
+
// schema can't drift from any prompt-grounding copy.
|
|
117
|
+
format: convertedSchema,
|
|
118
|
+
stream: this.stream,
|
|
119
|
+
};
|
|
120
|
+
if (tools) {
|
|
121
|
+
chatRequest.tools = tools;
|
|
122
|
+
}
|
|
123
|
+
// `temperature: 0` for deterministic structured output;
|
|
124
|
+
// `num_ctx` set generously so Ollama doesn't silently
|
|
125
|
+
// truncate a real multi-KB ingestion prompt (its per-model
|
|
126
|
+
// default is often ~4096). `maxOutputTokens` → num_predict,
|
|
127
|
+
// positive only: 0 means "generate nothing"; -1/-2 are Ollama
|
|
128
|
+
// sentinels we never emit.
|
|
129
|
+
const options = {
|
|
130
|
+
temperature: 0,
|
|
131
|
+
num_ctx: this.numCtx,
|
|
132
|
+
};
|
|
133
|
+
if (req.maxOutputTokens !== undefined && req.maxOutputTokens > 0) {
|
|
134
|
+
options.num_predict = req.maxOutputTokens;
|
|
135
|
+
}
|
|
136
|
+
chatRequest.options = options;
|
|
137
|
+
debugLlmRequest({
|
|
138
|
+
stageId: debugStageId,
|
|
139
|
+
model: req.model,
|
|
140
|
+
maxOutputTokens: req.maxOutputTokens,
|
|
141
|
+
reasoningEffort: req.reasoningEffort,
|
|
142
|
+
systemPromptLen: req.systemPrompt.length,
|
|
143
|
+
userMessageLen: req.userMessage.length,
|
|
144
|
+
systemPromptHead: req.systemPrompt,
|
|
145
|
+
userMessageHead: req.userMessage,
|
|
146
|
+
});
|
|
147
|
+
let response;
|
|
148
|
+
try {
|
|
149
|
+
const raw = await client.chat(chatRequest);
|
|
150
|
+
response = isAsyncIterable(raw) ? await collectStream(raw) : raw;
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
// Mid-flight abort: the SDK rejects with an AbortError
|
|
154
|
+
// when our signal listener called client.abort().
|
|
155
|
+
// Re-throw verbatim so llmStage marks the stage skipped.
|
|
156
|
+
if (isAbortError(err) || req.signal?.aborted) {
|
|
157
|
+
throw abortError();
|
|
158
|
+
}
|
|
159
|
+
const classified = classifyOllamaError(err);
|
|
160
|
+
debugLlmFailure({
|
|
161
|
+
stageId: debugStageId,
|
|
162
|
+
model: req.model,
|
|
163
|
+
errorName: classified.name,
|
|
164
|
+
errorMessage: classified.message,
|
|
165
|
+
tokenUsage: lastUsage,
|
|
166
|
+
});
|
|
167
|
+
throw classified;
|
|
168
|
+
}
|
|
169
|
+
lastUsage = mergeUsage(lastUsage, {
|
|
170
|
+
input: response.prompt_eval_count ?? 0,
|
|
171
|
+
output: response.eval_count ?? 0,
|
|
172
|
+
});
|
|
173
|
+
const toolCalls = response.message.tool_calls ?? [];
|
|
174
|
+
if (toolCalls.length > 0) {
|
|
175
|
+
// Echo the assistant tool-call message, then append one
|
|
176
|
+
// tool-result message per call before looping.
|
|
177
|
+
messages.push({
|
|
178
|
+
role: "assistant",
|
|
179
|
+
content: response.message.content,
|
|
180
|
+
tool_calls: toolCalls,
|
|
181
|
+
});
|
|
182
|
+
for (const call of toolCalls) {
|
|
183
|
+
const handler = findFunctionHandler(req.tools, call.function.name);
|
|
184
|
+
if (!handler) {
|
|
185
|
+
throw new NonRetryableLlmError({
|
|
186
|
+
message: `Ollama requested unknown function tool "${call.function.name}".`,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
const handlerResult = await handler.handler(call.function.arguments);
|
|
190
|
+
messages.push({
|
|
191
|
+
role: "tool",
|
|
192
|
+
content: typeof handlerResult === "string"
|
|
193
|
+
? handlerResult
|
|
194
|
+
: JSON.stringify(handlerResult),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const text = response.message.content;
|
|
200
|
+
if (text === undefined || text === "") {
|
|
201
|
+
throw new SchemaValidationLlmError({
|
|
202
|
+
message: "Ollama chat response carried no assistant text content.",
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
const parsed = safeParseJson(text);
|
|
206
|
+
debugLlmResponse({
|
|
207
|
+
stageId: debugStageId,
|
|
208
|
+
outputTextLen: text.length,
|
|
209
|
+
tokenUsage: lastUsage,
|
|
210
|
+
});
|
|
211
|
+
return {
|
|
212
|
+
output: parsed,
|
|
213
|
+
tokenUsage: lastUsage,
|
|
214
|
+
// The Ollama chat response is not request-id-bearing;
|
|
215
|
+
// `rawResponseId` is optional, so leaving it undefined is
|
|
216
|
+
// contract-legal. Do not fabricate one.
|
|
217
|
+
rawResponseId: undefined,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
throw new ToolLoopExhaustedError({
|
|
221
|
+
message: `Function-tool agent loop exceeded ${this.maxToolRounds.toString()} rounds without a final response.`,
|
|
222
|
+
rounds: this.maxToolRounds,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Resolve the SDK client: the injected one, or a freshly imported
|
|
227
|
+
* `Ollama` instance. Memoized so the dynamic import + construction
|
|
228
|
+
* runs at most once.
|
|
229
|
+
*/
|
|
230
|
+
getClient() {
|
|
231
|
+
if (this.config.client) {
|
|
232
|
+
return Promise.resolve(this.config.client);
|
|
233
|
+
}
|
|
234
|
+
this.clientPromise ??= this.importAndConstructClient();
|
|
235
|
+
return this.clientPromise;
|
|
236
|
+
}
|
|
237
|
+
async importAndConstructClient() {
|
|
238
|
+
const baseUrl = this.config.baseUrl ?? DEFAULT_BASE_URL;
|
|
239
|
+
const importOllama = this.config.importOllama ??
|
|
240
|
+
(() => import("ollama"));
|
|
241
|
+
let mod;
|
|
242
|
+
try {
|
|
243
|
+
mod = await importOllama();
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
throw new Error("OllamaProvider: the optional `ollama` package is not installed. " +
|
|
247
|
+
"Run `pnpm add ollama` (it is declared as an optional peerDependency) " +
|
|
248
|
+
"or pass a pre-built `client` via the provider config. " +
|
|
249
|
+
`Original import error: ${err instanceof Error ? err.message : String(err)}`);
|
|
250
|
+
}
|
|
251
|
+
// Per-provider raised-timeout fetch (no global mutation). Falls
|
|
252
|
+
// back to the SDK default fetch when undici is unavailable or the
|
|
253
|
+
// caller set requestTimeoutMs to 0.
|
|
254
|
+
const timeoutFetch = await buildTimeoutFetch(this.requestTimeoutMs, this.config.importUndici);
|
|
255
|
+
const sdkConfig = {
|
|
256
|
+
host: baseUrl,
|
|
257
|
+
};
|
|
258
|
+
if (timeoutFetch) {
|
|
259
|
+
sdkConfig.fetch = timeoutFetch;
|
|
260
|
+
}
|
|
261
|
+
return new mod.Ollama(sdkConfig);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// -- helpers --------------------------------------------------------------
|
|
265
|
+
function abortError() {
|
|
266
|
+
const e = new Error("The Ollama request was aborted.");
|
|
267
|
+
e.name = "AbortError";
|
|
268
|
+
return e;
|
|
269
|
+
}
|
|
270
|
+
function isAbortError(err) {
|
|
271
|
+
return (typeof err === "object" &&
|
|
272
|
+
err !== null &&
|
|
273
|
+
err.name === "AbortError");
|
|
274
|
+
}
|
|
275
|
+
function safeParseJson(raw) {
|
|
276
|
+
try {
|
|
277
|
+
return JSON.parse(raw);
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
throw new SchemaValidationLlmError({
|
|
281
|
+
message: `Ollama returned malformed JSON in structured-output content: ${err instanceof Error ? err.message : String(err)}`,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function isAsyncIterable(value) {
|
|
286
|
+
return (typeof value === "object" &&
|
|
287
|
+
value !== null &&
|
|
288
|
+
Symbol.asyncIterator in value);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Consume a streamed `chat()` generation and synthesize a single
|
|
292
|
+
* `TOllamaChatResponse`: concatenated `message.content`, tool_calls
|
|
293
|
+
* captured from any chunk that carries them, and the eval counts from
|
|
294
|
+
* the final (`done: true`) chunk. The synthesized response feeds the
|
|
295
|
+
* existing one-shot processing path unchanged, so `respond()`'s
|
|
296
|
+
* contract is preserved.
|
|
297
|
+
*/
|
|
298
|
+
async function collectStream(iterable) {
|
|
299
|
+
let content = "";
|
|
300
|
+
let role = "assistant";
|
|
301
|
+
let toolCalls;
|
|
302
|
+
let promptEvalCount = 0;
|
|
303
|
+
let evalCount = 0;
|
|
304
|
+
for await (const chunk of iterable) {
|
|
305
|
+
const msg = chunk.message;
|
|
306
|
+
if (msg) {
|
|
307
|
+
content += msg.content ?? "";
|
|
308
|
+
if (msg.role)
|
|
309
|
+
role = msg.role;
|
|
310
|
+
// Ollama emits tool_calls complete within a single chunk
|
|
311
|
+
// (not OpenAI-style per-index deltas), so take the latest
|
|
312
|
+
// chunk that carries them — concatenating would DUPLICATE
|
|
313
|
+
// calls. Ingestion is tool-free; only tool-using callers
|
|
314
|
+
// exercise this path.
|
|
315
|
+
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
|
316
|
+
toolCalls = msg.tool_calls;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// Last-wins: the synthesized response carries the FINAL chunk's
|
|
320
|
+
// single-round eval counts, NOT a cumulative sum across chunks.
|
|
321
|
+
// The terminal chunk reports this round's complete terminal
|
|
322
|
+
// counts, so taking the last value is the correct per-round
|
|
323
|
+
// figure. `runChatLoop`'s `mergeUsage` then SUMS these per-round
|
|
324
|
+
// terminal counts across tool-call rounds — summing the chunk
|
|
325
|
+
// values here instead would double-count within a round.
|
|
326
|
+
if (chunk.prompt_eval_count !== undefined) {
|
|
327
|
+
promptEvalCount = chunk.prompt_eval_count;
|
|
328
|
+
}
|
|
329
|
+
if (chunk.eval_count !== undefined) {
|
|
330
|
+
evalCount = chunk.eval_count;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
message: { role, content, tool_calls: toolCalls },
|
|
335
|
+
done: true,
|
|
336
|
+
prompt_eval_count: promptEvalCount,
|
|
337
|
+
eval_count: evalCount,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function mergeUsage(accumulated, next) {
|
|
341
|
+
return {
|
|
342
|
+
input: accumulated.input + next.input,
|
|
343
|
+
output: accumulated.output + next.output,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
function translateTools(tools) {
|
|
347
|
+
return tools.map((tool) => {
|
|
348
|
+
if (tool.kind === "function") {
|
|
349
|
+
return {
|
|
350
|
+
type: "function",
|
|
351
|
+
function: {
|
|
352
|
+
name: tool.name,
|
|
353
|
+
description: tool.description,
|
|
354
|
+
parameters: typeboxToJsonSchema(tool.parameters),
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
// Hosted-tool kinds (web_search / file_search / mcp) have no
|
|
359
|
+
// local Ollama equivalent. Fail fast and legibly.
|
|
360
|
+
throw new NonRetryableLlmError({
|
|
361
|
+
message: `Tool kind "${tool.kind}" is not supported by the Ollama provider. Only kind "function" (local handler) is supported.`,
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
function findFunctionHandler(tools, name) {
|
|
366
|
+
if (!tools)
|
|
367
|
+
return undefined;
|
|
368
|
+
for (const tool of tools) {
|
|
369
|
+
if (tool.kind === "function" && tool.name === name) {
|
|
370
|
+
return tool;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
//# sourceMappingURL=provider.js.map
|