@semiont/inference 0.5.29 → 0.5.31
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 +41 -10
- package/dist/index.d.ts +113 -17
- package/dist/index.js +122 -55
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -90,22 +90,47 @@ interface InferenceClient {
|
|
|
90
90
|
readonly type: string; // 'anthropic' | 'ollama' | 'mock'
|
|
91
91
|
readonly modelId: string; // configured model name
|
|
92
92
|
|
|
93
|
+
// Declared per-provider capabilities — consumers read these instead of
|
|
94
|
+
// switching on provider identity:
|
|
95
|
+
readonly maxConcurrency: number; // independent calls that gain from running at once
|
|
96
|
+
// (Anthropic 4; Ollama 1 — one GPU, no parallel gain)
|
|
97
|
+
readonly verifyDetectionYield: boolean; // whether detection count-verifies extractions
|
|
98
|
+
// (true for real providers; Mock false — tests opt in)
|
|
99
|
+
|
|
93
100
|
limits(): Promise<InferenceLimits>;
|
|
94
|
-
generateText(prompt, maxTokens, temperature,
|
|
95
|
-
generateTextWithMetadata(prompt, maxTokens, temperature,
|
|
101
|
+
generateText(prompt, maxTokens, temperature, signal?): Promise<string>;
|
|
102
|
+
generateTextWithMetadata(prompt, maxTokens, temperature, signal?): Promise<InferenceResponse>;
|
|
103
|
+
generateStructured<T>(prompt, maxTokens, temperature, elementSchema, signal?): Promise<StructuredResponse<T>>;
|
|
96
104
|
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Whatever varies by provider is a **capability declared here**, hard-coded per implementation — downstream packages are written purely in terms of this contract and never sniff provider identity. A new provider must take a position on each capability (tests pin the declarations).
|
|
108
|
+
|
|
109
|
+
Every generation method takes a trailing optional `AbortSignal`: aborting tears down the underlying transport (and, on Anthropic, the SDK's internal retry loop) so a cancelled call rejects promptly instead of surviving as a billed background request. Implementations must honor it — accepting and ignoring the signal is a defect.
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
97
112
|
|
|
98
113
|
interface InferenceLimits {
|
|
99
|
-
contextTokens: number;
|
|
100
|
-
maxOutputTokens: number;
|
|
114
|
+
contextTokens: number; // context window (Anthropic: max input; Ollama: shared input+output)
|
|
115
|
+
maxOutputTokens: number; // max output per generation (Ollama mirrors the shared window here)
|
|
116
|
+
outputTokensPerHour?: number; // provider's worst-case output-rate model, when it
|
|
117
|
+
// publishes one (Anthropic: 128_000; absent for Ollama)
|
|
101
118
|
}
|
|
102
119
|
|
|
103
120
|
interface InferenceResponse {
|
|
104
121
|
text: string;
|
|
105
122
|
stopReason: 'end_turn' | 'max_tokens' | 'stop_sequence' | string;
|
|
123
|
+
usage?: TokenUsage; // the PROVIDER's own token counts — never estimated;
|
|
124
|
+
} // absent means unreported, not zero
|
|
125
|
+
|
|
126
|
+
interface TokenUsage {
|
|
127
|
+
inputTokens: number;
|
|
128
|
+
outputTokens: number;
|
|
106
129
|
}
|
|
107
130
|
```
|
|
108
131
|
|
|
132
|
+
`StructuredResponse<T>` carries the same optional `usage`, so a consumer can pair what a call yielded with what it cost from one return value.
|
|
133
|
+
|
|
109
134
|
### Structured generation
|
|
110
135
|
|
|
111
136
|
`generateStructured(prompt, maxTokens, temperature, elementSchema)` returns **parsed array elements**, not text — the JSON guarantee lives in the return type (`StructuredResponse<T> = { items: T[]; stopReason }`), not in a comment:
|
|
@@ -123,7 +148,7 @@ Each implementation honors the contract with its provider's mechanism:
|
|
|
123
148
|
- **Ollama**: grammar-constrained sampling — the request's `format` field carries the caller's element schema wrapped in an array schema.
|
|
124
149
|
- **Anthropic**: response-level structured output — `output_config.format` carries the caller's element schema under an **array root** (accepted on both live-config models; `.plans/spikes/output-config-array-root.md`), so the response text IS the schema-conforming JSON. No tools, no wrapper, no unwrap.
|
|
125
150
|
|
|
126
|
-
A response that cannot be read as an array —
|
|
151
|
+
A response that cannot be read as an array — unparseable output, a missing array, an unhonoured grammar, or an **empty response** (which throws with the stop reason that produced it, so thinking-exhaustion classifies as the truncation it is) — **throws a typed `StructuredReadError`** carrying the provider's `stopReason`, because the cause classifies differently downstream: `max_tokens` is truncation (an identical retry truncates identically, so detection subdivides), `'unknown'` (no stop reason at all) is measured size-correlated on real documents (detection subdivides that too, while keeping it retryable), and everything else is model misbehavior a retry may fix. It is never coerced to `[]`: an empty extraction is a legitimate, distinct outcome, and conflating the two silently discards real data.
|
|
127
152
|
|
|
128
153
|
Current callers all expect arrays (entity extraction, motivation detection). If an object-emitting caller appears, `generateStructured` grows a sibling, not an option — see the notes in [src/interface.ts](src/interface.ts).
|
|
129
154
|
|
|
@@ -131,18 +156,20 @@ Current callers all expect arrays (entity extraction, motivation detection). If
|
|
|
131
156
|
|
|
132
157
|
`limits()` publishes the provider's **actual** context/output ceilings for the configured model — discovered from the provider itself, never hand-maintained constants:
|
|
133
158
|
|
|
134
|
-
- **Anthropic**: the Models API (`models.retrieve`) — `max_input_tokens` / `max_tokens
|
|
135
|
-
- **Ollama**: `POST /api/show` — the model's context window. Input and output share that window, so it is published as both fields (`maxOutputTokens === contextTokens` signals a shared window).
|
|
159
|
+
- **Anthropic**: the Models API (`models.retrieve`) — `max_input_tokens` / `max_tokens` — plus `outputTokensPerHour: 128_000`, the SDK's own worst-case rate model (the `calculateNonstreamingTimeout` constant): the one duration statement the provider surface makes, which detection's duration-safe budgets derive from.
|
|
160
|
+
- **Ollama**: `POST /api/show` — the model's context window. Input and output share that window, so it is published as both fields (`maxOutputTokens === contextTokens` signals a shared window). No rate is published — local hardware's rate is unknowable a priori. Absence does **not** mean no duration bound: the detection consumer applies its own conservative assumed floor rate instead, because an unbounded output budget turned model repetition loops into hour-long transient burns.
|
|
136
161
|
|
|
137
162
|
Discovery is lazy and cached per client; a failed discovery is **not** cached — the next call retries. When ceilings cannot be determined (unknown model, endpoint unreachable), `limits()` **throws**: fail-loud, never a guessed floor.
|
|
138
163
|
|
|
139
164
|
Two request-time behaviors ride on the limits:
|
|
140
165
|
- **Ollama sets `num_ctx` explicitly** on every generate request — sized to the prompt estimate + output budget, capped at the model window. Without it, Ollama's model-*default* window silently clips large prompts. A request that genuinely cannot fit **throws** instead of being clipped.
|
|
166
|
+
- **The Ollama adapter owns its transport timeouts.** With `stream: false`, Ollama sends no response headers until generation completes, and Node's default fetch would kill any call generating longer than ~5 minutes (undici's `headersTimeout`) — a ceiling below every deliberate bound, owned by nobody. Generate requests run on a per-request undici@7 dispatcher with those timeouts disabled; the caller's `AbortSignal` is the one bound. The undici `^7` pin is load-bearing (the built-in fetch rejects an undici@8 Agent) and test-gated.
|
|
167
|
+
- **Cloud-routed Ollama models are reported honestly, not corrected**: hidden thinking returned despite `think: false` is surfaced on the response and warned (it inflates `eval_count`, which is documented at the field), and the structured `format` is advisory rather than grammar-enforced on that path — violations surface as `StructuredReadError`.
|
|
141
168
|
- **Anthropic streams internally** above the SDK's non-streaming output ceiling (≈21K tokens) — same interface, same response shape.
|
|
142
169
|
|
|
143
170
|
### `MockInferenceClient`
|
|
144
171
|
|
|
145
|
-
A scripted test double ([src/implementations/mock.ts](src/implementations/mock.ts)): construct it with a list of canned responses, then inspect `calls` (recorded prompt/maxTokens/temperature/options per invocation). `reset()` and `setResponses()` helpers included. An optional third constructor argument injects `InferenceLimits` for chunking/budget tests; the default is generous (1M/1M) so ordinary tests never trip window guards.
|
|
172
|
+
A scripted test double ([src/implementations/mock.ts](src/implementations/mock.ts)): construct it with a list of canned responses, then inspect `calls` (recorded prompt/maxTokens/temperature/options per invocation). `reset()` and `setResponses()` helpers included. An optional third constructor argument injects `InferenceLimits` for chunking/budget tests; the default is generous (1M/1M window plus a generous published rate) so ordinary tests never trip window guards, duration caps, or the count-verifier. Its capabilities are deterministic-test defaults — `maxConcurrency: 1`, `verifyDetectionYield: false` — so a test exercising concurrency or verification declares its own client rather than paying a surprise call.
|
|
146
173
|
|
|
147
174
|
```typescript
|
|
148
175
|
import { MockInferenceClient } from '@semiont/inference';
|
|
@@ -195,10 +222,14 @@ Every generation records a usage metric through `@semiont/observability`'s `reco
|
|
|
195
222
|
|
|
196
223
|
### Adding a New Provider
|
|
197
224
|
|
|
198
|
-
1. Implement `InferenceClient` interface in `src/implementations/`
|
|
225
|
+
1. Implement the `InferenceClient` interface in `src/implementations/` — including a
|
|
226
|
+
position on each declared capability (`maxConcurrency`: does this provider gain from
|
|
227
|
+
concurrent independent calls? `verifyDetectionYield`: should detection count-verify
|
|
228
|
+
its extractions?). The capability pins in `factory.test.ts` fail until you take one.
|
|
199
229
|
2. Add type to `InferenceClientType` union in `src/factory.ts`
|
|
200
230
|
3. Add case in `createInferenceClient()` switch
|
|
201
|
-
4. Application code in `@semiont/make-meaning` requires no changes
|
|
231
|
+
4. Application code in `@semiont/make-meaning` and `@semiont/jobs` requires no changes —
|
|
232
|
+
consumers read capabilities off the contract instead of switching on provider identity
|
|
202
233
|
|
|
203
234
|
## Dependencies
|
|
204
235
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import { Logger } from '@semiont/core';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* What the call actually cost, as the PROVIDER counted it — never estimated
|
|
5
|
+
* here. Optional because a provider may not report it (and a call that fails
|
|
6
|
+
* before generating has nothing to report); absent means unknown, and a
|
|
7
|
+
* consumer must treat it as unknown rather than substituting a guess.
|
|
8
|
+
*/
|
|
9
|
+
interface TokenUsage {
|
|
10
|
+
inputTokens: number;
|
|
11
|
+
outputTokens: number;
|
|
12
|
+
}
|
|
3
13
|
interface InferenceResponse {
|
|
4
14
|
text: string;
|
|
5
15
|
stopReason: 'end_turn' | 'max_tokens' | 'stop_sequence' | string;
|
|
16
|
+
usage?: TokenUsage;
|
|
6
17
|
}
|
|
7
18
|
/**
|
|
8
19
|
* Raw JSON Schema for ONE array element of a structured generation — a plain
|
|
9
20
|
* object, not a TS type and not a validator instance. Both providers consume
|
|
10
|
-
* JSON Schema directly (Anthropic
|
|
11
|
-
* `
|
|
21
|
+
* JSON Schema directly (Anthropic as the array-root schema under
|
|
22
|
+
* `output_config.format`; Ollama as `format: { type: 'array', items: … }`),
|
|
12
23
|
* so anything richer would abstract one shape with two consumers.
|
|
13
24
|
*
|
|
14
25
|
* Constrain it to what both providers enforce: objects,
|
|
@@ -35,6 +46,7 @@ type ElementSchema = Record<string, unknown>;
|
|
|
35
46
|
interface StructuredResponse<T> {
|
|
36
47
|
items: T[];
|
|
37
48
|
stopReason: 'end_turn' | 'max_tokens' | 'stop_sequence' | string;
|
|
49
|
+
usage?: TokenUsage;
|
|
38
50
|
}
|
|
39
51
|
/**
|
|
40
52
|
* A provider's actual ceilings for the configured model, discovered from the
|
|
@@ -52,12 +64,80 @@ interface InferenceLimits {
|
|
|
52
64
|
contextTokens: number;
|
|
53
65
|
/** Maximum output tokens per generation. */
|
|
54
66
|
maxOutputTokens: number;
|
|
67
|
+
/**
|
|
68
|
+
* The provider's own worst-case output-rate model, in output tokens per
|
|
69
|
+
* hour, when it publishes one. Anthropic's SDK projects a call's maximum
|
|
70
|
+
* duration as `max_tokens / rate` (client.js `calculateNonstreamingTimeout`,
|
|
71
|
+
* 128_000/hour) and refuses non-streaming calls projected past 10 minutes —
|
|
72
|
+
* the one duration statement that provider surface makes. Consumers with
|
|
73
|
+
* their own call deadline derive a duration-safe output budget from it
|
|
74
|
+
* (ABANDONED-INFERENCE P4). Absent for providers whose rates are
|
|
75
|
+
* unknowable a priori (Ollama — local hardware). Absence does NOT mean no
|
|
76
|
+
* duration bound: the detection consumer applies its own conservative
|
|
77
|
+
* assumed floor rate instead (OLLAMA-DETECTION-TESTING P3b) — an unbounded
|
|
78
|
+
* budget turned model repetition loops into hour-long transient burns.
|
|
79
|
+
*/
|
|
80
|
+
outputTokensPerHour?: number;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Thrown when a structured generation's response cannot be read as the
|
|
84
|
+
* requested array — never coerced to `[]` (empty is a legitimate, distinct
|
|
85
|
+
* outcome). One class for every implementation, because the message shape
|
|
86
|
+
* and the classification contract must not diverge between providers.
|
|
87
|
+
*
|
|
88
|
+
* Carries the provider's stop reason because the cause classifies
|
|
89
|
+
* differently downstream: `max_tokens` means the JSON was cut off by the
|
|
90
|
+
* output budget — the same input truncates the same way, so a retry is
|
|
91
|
+
* guaranteed waste — while any other reason is model misbehavior a retry
|
|
92
|
+
* may legitimately fix.
|
|
93
|
+
*
|
|
94
|
+
* Also thrown — on either generation path — when a response arrives EMPTY:
|
|
95
|
+
* a thinking model can exhaust the whole output budget on hidden reasoning
|
|
96
|
+
* before its first response character (measured live, gpt-oss:120b-cloud
|
|
97
|
+
* 2026-09-05). Truncated-to-nothing is still truncation, and it needs the
|
|
98
|
+
* same stop-reason ride to classify correctly.
|
|
99
|
+
*/
|
|
100
|
+
declare class StructuredReadError extends Error {
|
|
101
|
+
readonly stopReason: string;
|
|
102
|
+
readonly name = "StructuredReadError";
|
|
103
|
+
constructor(detail: string, stopReason: string, options?: ErrorOptions);
|
|
55
104
|
}
|
|
56
105
|
interface InferenceClient {
|
|
57
106
|
/** Provider type identifier (e.g. 'anthropic', 'ollama') */
|
|
58
107
|
readonly type: string;
|
|
59
108
|
/** Model identifier used for generation (e.g. 'claude-opus-4-6', 'llama3') */
|
|
60
109
|
readonly modelId: string;
|
|
110
|
+
/**
|
|
111
|
+
* How many INDEPENDENT inference calls a caller should run concurrently
|
|
112
|
+
* against this provider for a throughput gain (DETECTION-QUALITY-THROUGHPUT
|
|
113
|
+
* P6 — detection's per-type fan-out reads this).
|
|
114
|
+
*
|
|
115
|
+
* This is a property of the provider's economics, which is why it lives on
|
|
116
|
+
* the provider and not in the caller. A HOSTED API whose per-account rate
|
|
117
|
+
* limit sits far above one job's usage has real spare capacity, so >1
|
|
118
|
+
* genuinely parallelizes. A LOCAL single-model server (Ollama) is 1: its
|
|
119
|
+
* throughput is hardware-bound, so concurrent requests only queue or split
|
|
120
|
+
* one GPU — no aggregate speedup, and N live KV-cache contexts is memory
|
|
121
|
+
* pressure that can OOM. There is no honest default across those two worlds,
|
|
122
|
+
* so this is required, not optional.
|
|
123
|
+
*
|
|
124
|
+
* Hard-coded per implementation for now; the natural seam for future
|
|
125
|
+
* per-provider or admin tuning (a value that later comes from config changes
|
|
126
|
+
* only where this is SET, not the callers).
|
|
127
|
+
*/
|
|
128
|
+
readonly maxConcurrency: number;
|
|
129
|
+
/**
|
|
130
|
+
* Whether detection should run the count-verifier against this provider's
|
|
131
|
+
* extractions (OLLAMA-DETECTION-TESTING P3c; universalized to every real
|
|
132
|
+
* provider by user ruling 2026-09-05 — unverified completeness is not a
|
|
133
|
+
* savings). Declared HERE, per implementation, because @semiont/jobs does no
|
|
134
|
+
* provider-specific switching (architecture ruling, same date): whatever
|
|
135
|
+
* varies by provider is a capability on this contract, like
|
|
136
|
+
* `maxConcurrency`. The mock alone defaults false, so deterministic tests
|
|
137
|
+
* opt in explicitly rather than paying a queue-popping count call by
|
|
138
|
+
* surprise.
|
|
139
|
+
*/
|
|
140
|
+
readonly verifyDetectionYield: boolean;
|
|
61
141
|
/**
|
|
62
142
|
* The provider's actual context/output ceilings for `modelId`. Discovered
|
|
63
143
|
* lazily on first call and cached for the client's lifetime; a failed
|
|
@@ -67,13 +147,23 @@ interface InferenceClient {
|
|
|
67
147
|
*/
|
|
68
148
|
limits(): Promise<InferenceLimits>;
|
|
69
149
|
/**
|
|
70
|
-
* Generate text from a prompt (simple interface)
|
|
150
|
+
* Generate text from a prompt (simple interface).
|
|
151
|
+
*
|
|
152
|
+
* `signal` (here and on every generation method — a trailing optional
|
|
153
|
+
* parameter, deliberately not an options bag; STRUCTURED-INFERENCE removed
|
|
154
|
+
* that shape on purpose): true cancellation, ABANDONED-INFERENCE P1.
|
|
155
|
+
* Implementations MUST thread it to their transport so an abort tears down
|
|
156
|
+
* the underlying request — and, for SDKs with internal retry loops, ends
|
|
157
|
+
* those too — rejecting promptly. Accepting the parameter and ignoring it
|
|
158
|
+
* is a defect worse than not having it: cancellation tests pass against
|
|
159
|
+
* such an adapter while zombie requests keep running (and billing) in
|
|
160
|
+
* production.
|
|
71
161
|
*/
|
|
72
|
-
generateText(prompt: string, maxTokens: number, temperature: number): Promise<string>;
|
|
162
|
+
generateText(prompt: string, maxTokens: number, temperature: number, signal?: AbortSignal): Promise<string>;
|
|
73
163
|
/**
|
|
74
164
|
* Generate text with detailed response information
|
|
75
165
|
*/
|
|
76
|
-
generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number): Promise<InferenceResponse>;
|
|
166
|
+
generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number, signal?: AbortSignal): Promise<InferenceResponse>;
|
|
77
167
|
/**
|
|
78
168
|
* Generate a JSON array whose elements satisfy `elementSchema`, as parsed
|
|
79
169
|
* values — the structured counterpart of `generateTextWithMetadata`, and
|
|
@@ -87,7 +177,7 @@ interface InferenceClient {
|
|
|
87
177
|
* "Structured response could not be read" error — they never coerce to
|
|
88
178
|
* `[]`, because empty is a legitimate, distinct outcome.
|
|
89
179
|
*/
|
|
90
|
-
generateStructured<T>(prompt: string, maxTokens: number, temperature: number, elementSchema: ElementSchema): Promise<StructuredResponse<T>>;
|
|
180
|
+
generateStructured<T>(prompt: string, maxTokens: number, temperature: number, elementSchema: ElementSchema, signal?: AbortSignal): Promise<StructuredResponse<T>>;
|
|
91
181
|
}
|
|
92
182
|
|
|
93
183
|
type InferenceClientType = 'anthropic' | 'ollama';
|
|
@@ -102,6 +192,8 @@ declare function createInferenceClient(config: InferenceClientConfig, logger?: L
|
|
|
102
192
|
|
|
103
193
|
declare class AnthropicInferenceClient implements InferenceClient {
|
|
104
194
|
readonly type: "anthropic";
|
|
195
|
+
readonly maxConcurrency = 4;
|
|
196
|
+
readonly verifyDetectionYield = true;
|
|
105
197
|
readonly modelId: string;
|
|
106
198
|
private client;
|
|
107
199
|
private logger?;
|
|
@@ -111,9 +203,9 @@ declare class AnthropicInferenceClient implements InferenceClient {
|
|
|
111
203
|
private discover;
|
|
112
204
|
private discoverModel;
|
|
113
205
|
private requestMessage;
|
|
114
|
-
generateText(prompt: string, maxTokens: number, temperature: number): Promise<string>;
|
|
115
|
-
generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number): Promise<InferenceResponse>;
|
|
116
|
-
generateStructured<T>(prompt: string, maxTokens: number, temperature: number, elementSchema: ElementSchema): Promise<StructuredResponse<T>>;
|
|
206
|
+
generateText(prompt: string, maxTokens: number, temperature: number, signal?: AbortSignal): Promise<string>;
|
|
207
|
+
generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number, signal?: AbortSignal): Promise<InferenceResponse>;
|
|
208
|
+
generateStructured<T>(prompt: string, maxTokens: number, temperature: number, elementSchema: ElementSchema, signal?: AbortSignal): Promise<StructuredResponse<T>>;
|
|
117
209
|
/** Issue the request, recording an error metric if the transport throws. */
|
|
118
210
|
private recordedRequest;
|
|
119
211
|
private recordError;
|
|
@@ -121,6 +213,8 @@ declare class AnthropicInferenceClient implements InferenceClient {
|
|
|
121
213
|
|
|
122
214
|
declare class OllamaInferenceClient implements InferenceClient {
|
|
123
215
|
readonly type: "ollama";
|
|
216
|
+
readonly maxConcurrency = 1;
|
|
217
|
+
readonly verifyDetectionYield = true;
|
|
124
218
|
readonly modelId: string;
|
|
125
219
|
private baseURL;
|
|
126
220
|
private logger?;
|
|
@@ -128,15 +222,17 @@ declare class OllamaInferenceClient implements InferenceClient {
|
|
|
128
222
|
constructor(model: string, baseURL?: string, logger?: Logger);
|
|
129
223
|
limits(): Promise<InferenceLimits>;
|
|
130
224
|
private discoverLimits;
|
|
131
|
-
generateText(prompt: string, maxTokens: number, temperature: number): Promise<string>;
|
|
132
|
-
generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number): Promise<InferenceResponse>;
|
|
133
|
-
generateStructured<T>(prompt: string, maxTokens: number, temperature: number, elementSchema: ElementSchema): Promise<StructuredResponse<T>>;
|
|
225
|
+
generateText(prompt: string, maxTokens: number, temperature: number, signal?: AbortSignal): Promise<string>;
|
|
226
|
+
generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number, signal?: AbortSignal): Promise<InferenceResponse>;
|
|
227
|
+
generateStructured<T>(prompt: string, maxTokens: number, temperature: number, elementSchema: ElementSchema, signal?: AbortSignal): Promise<StructuredResponse<T>>;
|
|
134
228
|
private generate;
|
|
135
229
|
}
|
|
136
230
|
|
|
137
231
|
declare class MockInferenceClient implements InferenceClient {
|
|
138
232
|
readonly type: "mock";
|
|
139
233
|
readonly modelId: "mock-model";
|
|
234
|
+
readonly maxConcurrency = 1;
|
|
235
|
+
readonly verifyDetectionYield = false;
|
|
140
236
|
private responses;
|
|
141
237
|
private responseIndex;
|
|
142
238
|
private stopReasons;
|
|
@@ -149,8 +245,8 @@ declare class MockInferenceClient implements InferenceClient {
|
|
|
149
245
|
}>;
|
|
150
246
|
constructor(responses?: string[], stopReasons?: string[], limits?: InferenceLimits);
|
|
151
247
|
limits(): Promise<InferenceLimits>;
|
|
152
|
-
generateText(prompt: string, maxTokens: number, temperature: number): Promise<string>;
|
|
153
|
-
generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number): Promise<InferenceResponse>;
|
|
248
|
+
generateText(prompt: string, maxTokens: number, temperature: number, signal?: AbortSignal): Promise<string>;
|
|
249
|
+
generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number, signal?: AbortSignal): Promise<InferenceResponse>;
|
|
154
250
|
/**
|
|
155
251
|
* Structured surface: pops the same responses queue and PARSES the entry,
|
|
156
252
|
* mirroring the real contract — a queued string that is not a JSON array
|
|
@@ -158,11 +254,11 @@ declare class MockInferenceClient implements InferenceClient {
|
|
|
158
254
|
* queuing it (`setResponses(['not json'])`). The element schema is recorded
|
|
159
255
|
* on `calls` so tests can assert what the caller declared.
|
|
160
256
|
*/
|
|
161
|
-
generateStructured<T>(prompt: string, maxTokens: number, temperature: number, elementSchema: ElementSchema): Promise<StructuredResponse<T>>;
|
|
257
|
+
generateStructured<T>(prompt: string, maxTokens: number, temperature: number, elementSchema: ElementSchema, signal?: AbortSignal): Promise<StructuredResponse<T>>;
|
|
162
258
|
private nextResponse;
|
|
163
259
|
reset(): void;
|
|
164
260
|
setResponses(responses: string[], stopReasons?: string[]): void;
|
|
165
261
|
}
|
|
166
262
|
|
|
167
|
-
export { AnthropicInferenceClient, MockInferenceClient, OllamaInferenceClient, createInferenceClient };
|
|
168
|
-
export type { ElementSchema, InferenceClient, InferenceClientConfig, InferenceClientType, InferenceLimits, InferenceResponse, StructuredResponse };
|
|
263
|
+
export { AnthropicInferenceClient, MockInferenceClient, OllamaInferenceClient, StructuredReadError, createInferenceClient };
|
|
264
|
+
export type { ElementSchema, InferenceClient, InferenceClientConfig, InferenceClientType, InferenceLimits, InferenceResponse, StructuredResponse, TokenUsage };
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,32 @@
|
|
|
2
2
|
import Anthropic from "@anthropic-ai/sdk";
|
|
3
3
|
import { isObject } from "@semiont/core";
|
|
4
4
|
import { recordInferenceUsage } from "@semiont/observability";
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
// src/interface.ts
|
|
7
|
+
var StructuredReadError = class extends Error {
|
|
8
|
+
constructor(detail, stopReason, options) {
|
|
9
|
+
super(`Structured response could not be read: ${detail} (stop_reason: ${stopReason})`, options);
|
|
10
|
+
this.stopReason = stopReason;
|
|
11
|
+
}
|
|
12
|
+
stopReason;
|
|
13
|
+
name = "StructuredReadError";
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// src/implementations/anthropic.ts
|
|
17
|
+
var OUTPUT_TOKENS_PER_HOUR = 128e3;
|
|
18
|
+
var NONSTREAMING_MAX_OUTPUT_TOKENS = Math.floor(OUTPUT_TOKENS_PER_HOUR / 6);
|
|
6
19
|
var AnthropicInferenceClient = class {
|
|
7
20
|
type = "anthropic";
|
|
21
|
+
// Hosted API: a single detection job uses a sliver of the account rate limit
|
|
22
|
+
// (measured 2026-09-04: ~1 request / 72 s, zero 429s at 4 concurrent types),
|
|
23
|
+
// so independent calls genuinely parallelize. Conservative until an 8-way run
|
|
24
|
+
// measures the next step (DETECTION-QUALITY-THROUGHPUT P6).
|
|
25
|
+
maxConcurrency = 4;
|
|
26
|
+
// Universal for real providers (user ruling 2026-09-05): "no observed
|
|
27
|
+
// collapse" here was absence-of-looking, and the unexplained ~2× yield gap
|
|
28
|
+
// vs gemma on the same document is exactly what verification answers. The
|
|
29
|
+
// ~2× billed input is the accepted cost.
|
|
30
|
+
verifyDetectionYield = true;
|
|
8
31
|
modelId;
|
|
9
32
|
client;
|
|
10
33
|
logger;
|
|
@@ -42,21 +65,25 @@ var AnthropicInferenceClient = class {
|
|
|
42
65
|
const raw = info;
|
|
43
66
|
const structuredOutputsSupported = isObject(raw) && isObject(raw["capabilities"]) && isObject(raw["capabilities"]["structured_outputs"]) && raw["capabilities"]["structured_outputs"]["supported"] === true;
|
|
44
67
|
return {
|
|
45
|
-
limits: {
|
|
68
|
+
limits: {
|
|
69
|
+
contextTokens: info.max_input_tokens,
|
|
70
|
+
maxOutputTokens: info.max_tokens,
|
|
71
|
+
outputTokensPerHour: OUTPUT_TOKENS_PER_HOUR
|
|
72
|
+
},
|
|
46
73
|
structuredOutputsSupported
|
|
47
74
|
};
|
|
48
75
|
}
|
|
49
|
-
requestMessage(params) {
|
|
76
|
+
requestMessage(params, signal) {
|
|
50
77
|
if (params.max_tokens > NONSTREAMING_MAX_OUTPUT_TOKENS) {
|
|
51
|
-
return this.client.messages.stream(params).finalMessage();
|
|
78
|
+
return this.client.messages.stream(params, { signal }).finalMessage();
|
|
52
79
|
}
|
|
53
|
-
return this.client.messages.create(params);
|
|
80
|
+
return this.client.messages.create(params, { signal });
|
|
54
81
|
}
|
|
55
|
-
async generateText(prompt, maxTokens, temperature) {
|
|
56
|
-
const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature);
|
|
82
|
+
async generateText(prompt, maxTokens, temperature, signal) {
|
|
83
|
+
const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature, signal);
|
|
57
84
|
return response.text;
|
|
58
85
|
}
|
|
59
|
-
async generateTextWithMetadata(prompt, maxTokens, temperature) {
|
|
86
|
+
async generateTextWithMetadata(prompt, maxTokens, temperature, signal) {
|
|
60
87
|
this.logger?.debug("Generating text with inference client", {
|
|
61
88
|
model: this.modelId,
|
|
62
89
|
promptLength: prompt.length,
|
|
@@ -70,7 +97,7 @@ var AnthropicInferenceClient = class {
|
|
|
70
97
|
messages: [{ role: "user", content: prompt }]
|
|
71
98
|
};
|
|
72
99
|
const start = performance.now();
|
|
73
|
-
const response = await this.recordedRequest(params, start);
|
|
100
|
+
const response = await this.recordedRequest(params, start, signal);
|
|
74
101
|
const textContent = response.content.find((c) => c.type === "text");
|
|
75
102
|
if (!textContent || textContent.type !== "text") {
|
|
76
103
|
this.recordError(start, response);
|
|
@@ -92,14 +119,15 @@ var AnthropicInferenceClient = class {
|
|
|
92
119
|
this.logger?.info("Text generation completed", {
|
|
93
120
|
model: this.modelId,
|
|
94
121
|
textLength: text.length,
|
|
95
|
-
stopReason: response.stop_reason
|
|
122
|
+
stopReason: response.stop_reason,
|
|
123
|
+
requestId: requestIdOf(response)
|
|
96
124
|
});
|
|
97
125
|
return {
|
|
98
126
|
text,
|
|
99
127
|
stopReason: response.stop_reason || "unknown"
|
|
100
128
|
};
|
|
101
129
|
}
|
|
102
|
-
async generateStructured(prompt, maxTokens, temperature, elementSchema) {
|
|
130
|
+
async generateStructured(prompt, maxTokens, temperature, elementSchema, signal) {
|
|
103
131
|
const discovery = await this.discover();
|
|
104
132
|
if (!discovery.structuredOutputsSupported) {
|
|
105
133
|
throw new Error(
|
|
@@ -127,7 +155,7 @@ var AnthropicInferenceClient = class {
|
|
|
127
155
|
}
|
|
128
156
|
};
|
|
129
157
|
const start = performance.now();
|
|
130
|
-
const response = await this.recordedRequest(params, start);
|
|
158
|
+
const response = await this.recordedRequest(params, start, signal);
|
|
131
159
|
const textContent = response.content.find((c) => c.type === "text");
|
|
132
160
|
if (!textContent || textContent.type !== "text") {
|
|
133
161
|
this.recordError(start, response);
|
|
@@ -147,10 +175,7 @@ var AnthropicInferenceClient = class {
|
|
|
147
175
|
textLength: textContent.text.length,
|
|
148
176
|
stopReason: response.stop_reason
|
|
149
177
|
});
|
|
150
|
-
throw new
|
|
151
|
-
`Structured response could not be read: response is not valid JSON (stop_reason: ${response.stop_reason})`,
|
|
152
|
-
{ cause: err }
|
|
153
|
-
);
|
|
178
|
+
throw new StructuredReadError("response is not valid JSON", response.stop_reason || "unknown", { cause: err });
|
|
154
179
|
}
|
|
155
180
|
if (!Array.isArray(parsed)) {
|
|
156
181
|
this.recordError(start, response);
|
|
@@ -159,9 +184,7 @@ var AnthropicInferenceClient = class {
|
|
|
159
184
|
parsedType: typeof parsed,
|
|
160
185
|
stopReason: response.stop_reason
|
|
161
186
|
});
|
|
162
|
-
throw new
|
|
163
|
-
`Structured response could not be read: parsed to ${typeof parsed}, not an array (stop_reason: ${response.stop_reason})`
|
|
164
|
-
);
|
|
187
|
+
throw new StructuredReadError(`parsed to ${typeof parsed}, not an array`, response.stop_reason || "unknown");
|
|
165
188
|
}
|
|
166
189
|
recordInferenceUsage({
|
|
167
190
|
provider: this.type,
|
|
@@ -174,17 +197,19 @@ var AnthropicInferenceClient = class {
|
|
|
174
197
|
this.logger?.info("Structured generation completed", {
|
|
175
198
|
model: this.modelId,
|
|
176
199
|
items: parsed.length,
|
|
177
|
-
stopReason: response.stop_reason
|
|
200
|
+
stopReason: response.stop_reason,
|
|
201
|
+
requestId: requestIdOf(response)
|
|
178
202
|
});
|
|
179
203
|
return {
|
|
180
204
|
items: parsed,
|
|
181
|
-
stopReason: response.stop_reason || "unknown"
|
|
205
|
+
stopReason: response.stop_reason || "unknown",
|
|
206
|
+
...usageOf(response)
|
|
182
207
|
};
|
|
183
208
|
}
|
|
184
209
|
/** Issue the request, recording an error metric if the transport throws. */
|
|
185
|
-
async recordedRequest(params, start) {
|
|
210
|
+
async recordedRequest(params, start, signal) {
|
|
186
211
|
try {
|
|
187
|
-
return await this.requestMessage(params);
|
|
212
|
+
return await this.requestMessage(params, signal);
|
|
188
213
|
} catch (err) {
|
|
189
214
|
recordInferenceUsage({
|
|
190
215
|
provider: this.type,
|
|
@@ -206,14 +231,34 @@ var AnthropicInferenceClient = class {
|
|
|
206
231
|
});
|
|
207
232
|
}
|
|
208
233
|
};
|
|
234
|
+
function requestIdOf(response) {
|
|
235
|
+
if (isObject(response) && typeof response["_request_id"] === "string") {
|
|
236
|
+
return response["_request_id"];
|
|
237
|
+
}
|
|
238
|
+
return void 0;
|
|
239
|
+
}
|
|
240
|
+
function usageOf(response) {
|
|
241
|
+
const { input_tokens, output_tokens } = response.usage ?? {};
|
|
242
|
+
if (input_tokens === void 0 || output_tokens === void 0) return {};
|
|
243
|
+
return { usage: { inputTokens: input_tokens, outputTokens: output_tokens } };
|
|
244
|
+
}
|
|
209
245
|
|
|
210
246
|
// src/implementations/ollama.ts
|
|
247
|
+
import { Agent } from "undici";
|
|
211
248
|
import { estimateTokens, isNumber, isObject as isObject2 } from "@semiont/core";
|
|
212
249
|
import { recordInferenceUsage as recordInferenceUsage2 } from "@semiont/observability";
|
|
250
|
+
var unboundedTransport = new Agent({ headersTimeout: 0, bodyTimeout: 0 });
|
|
213
251
|
var NUM_CTX_ESTIMATE_SLACK = 0.2;
|
|
214
252
|
var NUM_CTX_TEMPLATE_ALLOWANCE = 64;
|
|
215
253
|
var OllamaInferenceClient = class {
|
|
216
254
|
type = "ollama";
|
|
255
|
+
// Local single model: generation throughput is hardware-bound, so concurrent
|
|
256
|
+
// requests queue or split one GPU for no aggregate speedup — and each live
|
|
257
|
+
// context costs KV-cache memory. Detection runs its types sequentially here
|
|
258
|
+
// (DETECTION-QUALITY-THROUGHPUT P6).
|
|
259
|
+
maxConcurrency = 1;
|
|
260
|
+
// Where the collapse risk was MEASURED (F7) — the verifier's original home.
|
|
261
|
+
verifyDetectionYield = true;
|
|
217
262
|
modelId;
|
|
218
263
|
baseURL;
|
|
219
264
|
logger;
|
|
@@ -251,15 +296,15 @@ var OllamaInferenceClient = class {
|
|
|
251
296
|
}
|
|
252
297
|
return { contextTokens, maxOutputTokens: contextTokens };
|
|
253
298
|
}
|
|
254
|
-
async generateText(prompt, maxTokens, temperature) {
|
|
255
|
-
const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature);
|
|
299
|
+
async generateText(prompt, maxTokens, temperature, signal) {
|
|
300
|
+
const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature, signal);
|
|
256
301
|
return response.text;
|
|
257
302
|
}
|
|
258
|
-
async generateTextWithMetadata(prompt, maxTokens, temperature) {
|
|
259
|
-
return this.generate(prompt, maxTokens, temperature, void 0);
|
|
303
|
+
async generateTextWithMetadata(prompt, maxTokens, temperature, signal) {
|
|
304
|
+
return this.generate(prompt, maxTokens, temperature, void 0, signal);
|
|
260
305
|
}
|
|
261
|
-
async generateStructured(prompt, maxTokens, temperature, elementSchema) {
|
|
262
|
-
const response = await this.generate(prompt, maxTokens, temperature, elementSchema);
|
|
306
|
+
async generateStructured(prompt, maxTokens, temperature, elementSchema, signal) {
|
|
307
|
+
const response = await this.generate(prompt, maxTokens, temperature, elementSchema, signal);
|
|
263
308
|
let parsed;
|
|
264
309
|
try {
|
|
265
310
|
parsed = JSON.parse(response.text);
|
|
@@ -269,10 +314,7 @@ var OllamaInferenceClient = class {
|
|
|
269
314
|
textLength: response.text.length,
|
|
270
315
|
stopReason: response.stopReason
|
|
271
316
|
});
|
|
272
|
-
throw new
|
|
273
|
-
`Structured response could not be read: response is not valid JSON (stop_reason: ${response.stopReason})`,
|
|
274
|
-
{ cause: err }
|
|
275
|
-
);
|
|
317
|
+
throw new StructuredReadError("response is not valid JSON", response.stopReason, { cause: err });
|
|
276
318
|
}
|
|
277
319
|
if (!Array.isArray(parsed)) {
|
|
278
320
|
this.logger?.error("Structured response could not be read", {
|
|
@@ -280,13 +322,11 @@ var OllamaInferenceClient = class {
|
|
|
280
322
|
parsedType: typeof parsed,
|
|
281
323
|
stopReason: response.stopReason
|
|
282
324
|
});
|
|
283
|
-
throw new
|
|
284
|
-
`Structured response could not be read: parsed to ${typeof parsed}, not an array (stop_reason: ${response.stopReason})`
|
|
285
|
-
);
|
|
325
|
+
throw new StructuredReadError(`parsed to ${typeof parsed}, not an array`, response.stopReason);
|
|
286
326
|
}
|
|
287
|
-
return { items: parsed, stopReason: response.stopReason };
|
|
327
|
+
return { items: parsed, stopReason: response.stopReason, ...response.usage ? { usage: response.usage } : {} };
|
|
288
328
|
}
|
|
289
|
-
async generate(prompt, maxTokens, temperature, elementSchema) {
|
|
329
|
+
async generate(prompt, maxTokens, temperature, elementSchema, signal) {
|
|
290
330
|
this.logger?.debug("Generating text with Ollama", {
|
|
291
331
|
model: this.modelId,
|
|
292
332
|
promptLength: prompt.length,
|
|
@@ -326,7 +366,9 @@ var OllamaInferenceClient = class {
|
|
|
326
366
|
res = await fetch(url, {
|
|
327
367
|
method: "POST",
|
|
328
368
|
headers: { "Content-Type": "application/json" },
|
|
329
|
-
body: JSON.stringify(body)
|
|
369
|
+
body: JSON.stringify(body),
|
|
370
|
+
signal,
|
|
371
|
+
dispatcher: unboundedTransport
|
|
330
372
|
});
|
|
331
373
|
} catch (err) {
|
|
332
374
|
recordInferenceUsage2({
|
|
@@ -353,6 +395,13 @@ var OllamaInferenceClient = class {
|
|
|
353
395
|
throw new Error(`Ollama API error (${res.status}): ${body2}`);
|
|
354
396
|
}
|
|
355
397
|
const data = await res.json();
|
|
398
|
+
const stopReason = mapStopReason(data.done_reason);
|
|
399
|
+
if (data.thinking) {
|
|
400
|
+
this.logger?.warn("Model produced hidden thinking despite think:false", {
|
|
401
|
+
model: this.modelId,
|
|
402
|
+
thinkingChars: data.thinking.length
|
|
403
|
+
});
|
|
404
|
+
}
|
|
356
405
|
if (!data.response) {
|
|
357
406
|
recordInferenceUsage2({
|
|
358
407
|
provider: this.type,
|
|
@@ -362,8 +411,12 @@ var OllamaInferenceClient = class {
|
|
|
362
411
|
inputTokens: data.prompt_eval_count,
|
|
363
412
|
outputTokens: data.eval_count
|
|
364
413
|
});
|
|
365
|
-
this.logger?.error("Empty response from Ollama", {
|
|
366
|
-
|
|
414
|
+
this.logger?.error("Empty response from Ollama", {
|
|
415
|
+
model: this.modelId,
|
|
416
|
+
stopReason,
|
|
417
|
+
thinkingChars: data.thinking?.length
|
|
418
|
+
});
|
|
419
|
+
throw new StructuredReadError("response is empty", stopReason);
|
|
367
420
|
}
|
|
368
421
|
recordInferenceUsage2({
|
|
369
422
|
provider: this.type,
|
|
@@ -373,7 +426,6 @@ var OllamaInferenceClient = class {
|
|
|
373
426
|
inputTokens: data.prompt_eval_count,
|
|
374
427
|
outputTokens: data.eval_count
|
|
375
428
|
});
|
|
376
|
-
const stopReason = mapStopReason(data.done_reason);
|
|
377
429
|
this.logger?.info("Text generation completed", {
|
|
378
430
|
model: this.modelId,
|
|
379
431
|
textLength: data.response.length,
|
|
@@ -381,7 +433,8 @@ var OllamaInferenceClient = class {
|
|
|
381
433
|
});
|
|
382
434
|
return {
|
|
383
435
|
text: data.response,
|
|
384
|
-
stopReason
|
|
436
|
+
stopReason,
|
|
437
|
+
...data.prompt_eval_count !== void 0 && data.eval_count !== void 0 ? { usage: { inputTokens: data.prompt_eval_count, outputTokens: data.eval_count } } : {}
|
|
385
438
|
};
|
|
386
439
|
}
|
|
387
440
|
};
|
|
@@ -439,11 +492,22 @@ function createInferenceClient(config, logger) {
|
|
|
439
492
|
// src/implementations/mock.ts
|
|
440
493
|
var GENEROUS_LIMITS = {
|
|
441
494
|
contextTokens: 1e6,
|
|
442
|
-
maxOutputTokens: 1e6
|
|
495
|
+
maxOutputTokens: 1e6,
|
|
496
|
+
// Generous RATE too: without one the mock reads as rate-silent, which since
|
|
497
|
+
// OLLAMA-DETECTION-TESTING P3b/P3c opts consumers into the assumed duration
|
|
498
|
+
// floor and the count-verifier. Tests exercise those by injecting
|
|
499
|
+
// rate-silent limits deliberately, never by the default.
|
|
500
|
+
outputTokensPerHour: 36e8
|
|
443
501
|
};
|
|
444
502
|
var MockInferenceClient = class {
|
|
445
503
|
type = "mock";
|
|
446
504
|
modelId = "mock-model";
|
|
505
|
+
// Deterministic default for tests; a test exercising concurrency sets its own.
|
|
506
|
+
maxConcurrency = 1;
|
|
507
|
+
// Deterministic default: a count call pops the shared response queue, so
|
|
508
|
+
// tests exercising the verifier declare their own true rather than every
|
|
509
|
+
// consumer paying it by surprise.
|
|
510
|
+
verifyDetectionYield = false;
|
|
447
511
|
responses = [];
|
|
448
512
|
responseIndex = 0;
|
|
449
513
|
stopReasons = [];
|
|
@@ -457,11 +521,12 @@ var MockInferenceClient = class {
|
|
|
457
521
|
async limits() {
|
|
458
522
|
return this.injectedLimits;
|
|
459
523
|
}
|
|
460
|
-
async generateText(prompt, maxTokens, temperature) {
|
|
461
|
-
const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature);
|
|
524
|
+
async generateText(prompt, maxTokens, temperature, signal) {
|
|
525
|
+
const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature, signal);
|
|
462
526
|
return response.text;
|
|
463
527
|
}
|
|
464
|
-
async generateTextWithMetadata(prompt, maxTokens, temperature) {
|
|
528
|
+
async generateTextWithMetadata(prompt, maxTokens, temperature, signal) {
|
|
529
|
+
throwIfAborted(signal);
|
|
465
530
|
this.calls.push({ prompt, maxTokens, temperature });
|
|
466
531
|
return this.nextResponse();
|
|
467
532
|
}
|
|
@@ -472,22 +537,18 @@ var MockInferenceClient = class {
|
|
|
472
537
|
* queuing it (`setResponses(['not json'])`). The element schema is recorded
|
|
473
538
|
* on `calls` so tests can assert what the caller declared.
|
|
474
539
|
*/
|
|
475
|
-
async generateStructured(prompt, maxTokens, temperature, elementSchema) {
|
|
540
|
+
async generateStructured(prompt, maxTokens, temperature, elementSchema, signal) {
|
|
541
|
+
throwIfAborted(signal);
|
|
476
542
|
this.calls.push({ prompt, maxTokens, temperature, elementSchema });
|
|
477
543
|
const { text, stopReason } = this.nextResponse();
|
|
478
544
|
let parsed;
|
|
479
545
|
try {
|
|
480
546
|
parsed = JSON.parse(text);
|
|
481
547
|
} catch (err) {
|
|
482
|
-
throw new
|
|
483
|
-
`Structured response could not be read: response is not valid JSON (stop_reason: ${stopReason})`,
|
|
484
|
-
{ cause: err }
|
|
485
|
-
);
|
|
548
|
+
throw new StructuredReadError("response is not valid JSON", stopReason, { cause: err });
|
|
486
549
|
}
|
|
487
550
|
if (!Array.isArray(parsed)) {
|
|
488
|
-
throw new
|
|
489
|
-
`Structured response could not be read: parsed to ${typeof parsed}, not an array (stop_reason: ${stopReason})`
|
|
490
|
-
);
|
|
551
|
+
throw new StructuredReadError(`parsed to ${typeof parsed}, not an array`, stopReason);
|
|
491
552
|
}
|
|
492
553
|
return { items: parsed, stopReason };
|
|
493
554
|
}
|
|
@@ -510,10 +571,16 @@ var MockInferenceClient = class {
|
|
|
510
571
|
this.responseIndex = 0;
|
|
511
572
|
}
|
|
512
573
|
};
|
|
574
|
+
function throwIfAborted(signal) {
|
|
575
|
+
if (signal?.aborted) {
|
|
576
|
+
throw new DOMException("This operation was aborted", "AbortError");
|
|
577
|
+
}
|
|
578
|
+
}
|
|
513
579
|
export {
|
|
514
580
|
AnthropicInferenceClient,
|
|
515
581
|
MockInferenceClient,
|
|
516
582
|
OllamaInferenceClient,
|
|
583
|
+
StructuredReadError,
|
|
517
584
|
createInferenceClient
|
|
518
585
|
};
|
|
519
586
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/implementations/anthropic.ts","../src/implementations/ollama.ts","../src/factory.ts","../src/implementations/mock.ts"],"sourcesContent":["// Anthropic Claude implementation of InferenceClient interface\n\nimport Anthropic from '@anthropic-ai/sdk';\nimport { isObject, type Logger } from '@semiont/core';\nimport { recordInferenceUsage } from '@semiont/observability';\nimport { ElementSchema, InferenceClient, InferenceLimits, InferenceResponse, StructuredResponse } from '../interface.js';\n\n// The SDK refuses non-streaming create() calls whose projected duration\n// exceeds its 10-minute timeout: it throws when\n// (60min × max_tokens) / 128_000 > 10min, i.e. above 128_000/6 ≈ 21,333\n// output tokens (client.js, calculateNonstreamingTimeout). Above that we\n// stream internally and assemble the final message — same request shape,\n// same response handling, same interface.\nconst NONSTREAMING_MAX_OUTPUT_TOKENS = Math.floor(128_000 / 6);\n\n// Structured generation rides `output_config.format` — response-level\n// structured output: the response TEXT is the schema-conforming JSON, with a\n// top-level ARRAY root (accepted on both live-config models — spike\n// 2026-08-06, `.plans/spikes/output-config-array-root.md`). This replaced\n// the pre-structured-outputs scaffolding: a forced `emit_json_array` tool\n// whose object-only input required an `items` wrapper and an unwrap — and\n// the unwrap was the exact line that silently coerced an unreadable payload\n// to `[]` (STRUCTURED-INFERENCE §Problem). There is no tool-input\n// accumulation step left for the SDK to hand over unparsed; the read path is\n// now the same parse-and-verify shape as Ollama's.\n\n/**\n * Everything one Models API call teaches us about the configured model. The\n * capability stays PRIVATE to this client: its only consumer is the gate in\n * `generateStructured`, so it does not cross the `InferenceClient` interface\n * (no speculative surface — widen `InferenceLimits` only when an external\n * consumer exists).\n */\ninterface ModelDiscovery {\n limits: InferenceLimits;\n structuredOutputsSupported: boolean;\n}\n\nexport class AnthropicInferenceClient implements InferenceClient {\n readonly type = 'anthropic' as const;\n readonly modelId: string;\n private client: Anthropic;\n private logger?: Logger;\n private discoveryPromise?: Promise<ModelDiscovery>;\n\n constructor(apiKey: string, model: string, baseURL?: string, logger?: Logger) {\n this.client = new Anthropic({\n apiKey,\n baseURL: baseURL || 'https://api.anthropic.com',\n });\n this.modelId = model;\n this.logger = logger;\n }\n\n limits(): Promise<InferenceLimits> {\n return this.discover().then(d => d.limits);\n }\n\n private discover(): Promise<ModelDiscovery> {\n if (!this.discoveryPromise) {\n this.discoveryPromise = this.discoverModel().catch((err: unknown) => {\n // Never cache a failed discovery — a transient outage would otherwise\n // pin every future call to the same rejection.\n this.discoveryPromise = undefined;\n throw err;\n });\n }\n return this.discoveryPromise;\n }\n\n private async discoverModel(): Promise<ModelDiscovery> {\n // The Models API publishes the actual ceilings AND capabilities per\n // model — no hand-maintained table to go stale when a new model ships,\n // and the API's own metadata outranks documentation prose when the two\n // disagree (measured: the docs' supported-model list was stale while\n // `capabilities.structured_outputs.supported` was correct).\n const info = await this.client.models.retrieve(this.modelId).catch((err: unknown) => {\n throw new Error(\n `Failed to discover model limits for '${this.modelId}' from the Models API`,\n { cause: err },\n );\n });\n if (info.max_input_tokens == null || info.max_tokens == null) {\n throw new Error(`Models API reports no context/output ceilings for '${this.modelId}'`);\n }\n // `capabilities` is not declared on the SDK's ModelInfo type — narrow\n // through the core guards rather than casting. Absent metadata reads as\n // unsupported: the gate then refuses loudly (D4), never guesses.\n const raw: unknown = info;\n const structuredOutputsSupported =\n isObject(raw) &&\n isObject(raw['capabilities']) &&\n isObject(raw['capabilities']['structured_outputs']) &&\n raw['capabilities']['structured_outputs']['supported'] === true;\n return {\n limits: { contextTokens: info.max_input_tokens, maxOutputTokens: info.max_tokens },\n structuredOutputsSupported,\n };\n }\n\n private requestMessage(params: Anthropic.MessageCreateParamsNonStreaming): Promise<Anthropic.Message> {\n if (params.max_tokens > NONSTREAMING_MAX_OUTPUT_TOKENS) {\n return this.client.messages.stream(params).finalMessage();\n }\n return this.client.messages.create(params);\n }\n\n async generateText(prompt: string, maxTokens: number, temperature: number): Promise<string> {\n const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature);\n return response.text;\n }\n\n async generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number): Promise<InferenceResponse> {\n this.logger?.debug('Generating text with inference client', {\n model: this.modelId,\n promptLength: prompt.length,\n maxTokens,\n temperature,\n });\n\n const params: Anthropic.MessageCreateParamsNonStreaming = {\n model: this.modelId,\n max_tokens: maxTokens,\n temperature,\n messages: [{ role: 'user', content: prompt }],\n };\n\n const start = performance.now();\n const response = await this.recordedRequest(params, start);\n\n const textContent = response.content.find(c => c.type === 'text');\n if (!textContent || textContent.type !== 'text') {\n this.recordError(start, response);\n this.logger?.error('No text content in inference response', {\n model: this.modelId,\n contentTypes: response.content.map(c => c.type)\n });\n throw new Error('No text content in inference response');\n }\n const text = textContent.text;\n\n recordInferenceUsage({\n provider: this.type,\n model: this.modelId,\n durationMs: performance.now() - start,\n outcome: 'success',\n inputTokens: response.usage?.input_tokens,\n outputTokens: response.usage?.output_tokens,\n });\n\n this.logger?.info('Text generation completed', {\n model: this.modelId,\n textLength: text.length,\n stopReason: response.stop_reason\n });\n\n return {\n text,\n stopReason: response.stop_reason || 'unknown'\n };\n }\n\n async generateStructured<T>(\n prompt: string,\n maxTokens: number,\n temperature: number,\n elementSchema: ElementSchema,\n ): Promise<StructuredResponse<T>> {\n // Capability gate (D3/D4): model choice is deployment config, so the\n // client asks the provider whether the configured model can honour\n // strictness — and REFUSES when it cannot. Silent fallback to\n // unconstrained tool use is exactly the behaviour that turned 202 real\n // entities into a green empty job. The discovery is the same cached\n // Models API call `limits()` uses; no extra round trip.\n const discovery = await this.discover();\n if (!discovery.structuredOutputsSupported) {\n throw new Error(\n `Model '${this.modelId}' does not report support for strict structured outputs ` +\n `(Models API capabilities.structured_outputs) — refusing rather than degrading to ` +\n `unconstrained tool use, which silently discards unreadable results. Re-point the ` +\n `inference.model key that pins this worker/actor in .semiont/semiontconfig/*.toml ` +\n `(e.g. environments.<env>.workers.<job-type>.inference.model) at a model that ` +\n `reports supported: true.`,\n );\n }\n\n this.logger?.debug('Generating structured output with inference client', {\n model: this.modelId,\n promptLength: prompt.length,\n maxTokens,\n temperature,\n });\n\n const params: Anthropic.MessageCreateParamsNonStreaming = {\n model: this.modelId,\n max_tokens: maxTokens,\n temperature,\n messages: [{ role: 'user', content: prompt }],\n // Response-level structured output with an ARRAY root: the response\n // text IS the schema-conforming JSON. No tools, no prefill.\n output_config: {\n format: {\n type: 'json_schema',\n schema: { type: 'array', items: elementSchema },\n },\n },\n };\n\n const start = performance.now();\n const response = await this.recordedRequest(params, start);\n\n const textContent = response.content.find(c => c.type === 'text');\n if (!textContent || textContent.type !== 'text') {\n this.recordError(start, response);\n this.logger?.error('No text content in structured inference response', {\n model: this.modelId,\n contentTypes: response.content.map(c => c.type)\n });\n throw new Error('No text content in structured inference response');\n }\n\n // Anything that does not read as an array is a THROW, never a coerced\n // `[]` — \"we could not read the model\" must never be conflated with\n // \"the model found nothing\": that conflation is what silently discarded\n // 202 real entities as a green empty result. A truncated (`max_tokens`)\n // response surfaces here too, as unparseable JSON naming its stop_reason.\n let parsed: unknown;\n try {\n parsed = JSON.parse(textContent.text);\n } catch (err) {\n this.recordError(start, response);\n this.logger?.error('Structured response could not be read', {\n model: this.modelId,\n textLength: textContent.text.length,\n stopReason: response.stop_reason,\n });\n throw new Error(\n `Structured response could not be read: response is not valid JSON (stop_reason: ${response.stop_reason})`,\n { cause: err },\n );\n }\n if (!Array.isArray(parsed)) {\n this.recordError(start, response);\n this.logger?.error('Structured response could not be read', {\n model: this.modelId,\n parsedType: typeof parsed,\n stopReason: response.stop_reason,\n });\n throw new Error(\n `Structured response could not be read: parsed to ${typeof parsed}, not an array (stop_reason: ${response.stop_reason})`,\n );\n }\n\n recordInferenceUsage({\n provider: this.type,\n model: this.modelId,\n durationMs: performance.now() - start,\n outcome: 'success',\n inputTokens: response.usage?.input_tokens,\n outputTokens: response.usage?.output_tokens,\n });\n\n this.logger?.info('Structured generation completed', {\n model: this.modelId,\n items: parsed.length,\n stopReason: response.stop_reason\n });\n\n return {\n items: parsed as T[],\n stopReason: response.stop_reason || 'unknown',\n };\n }\n\n /** Issue the request, recording an error metric if the transport throws. */\n private async recordedRequest(params: Anthropic.MessageCreateParamsNonStreaming, start: number): Promise<Anthropic.Message> {\n try {\n return await this.requestMessage(params);\n } catch (err) {\n recordInferenceUsage({\n provider: this.type,\n model: this.modelId,\n durationMs: performance.now() - start,\n outcome: 'error',\n });\n throw err;\n }\n }\n\n private recordError(start: number, response: Anthropic.Message): void {\n recordInferenceUsage({\n provider: this.type,\n model: this.modelId,\n durationMs: performance.now() - start,\n outcome: 'error',\n inputTokens: response.usage?.input_tokens,\n outputTokens: response.usage?.output_tokens,\n });\n }\n}\n","// Ollama implementation of InferenceClient interface\n// Uses native Ollama HTTP API (no SDK dependency)\n\nimport { estimateTokens, isNumber, isObject } from '@semiont/core';\nimport type { Logger } from '@semiont/core';\nimport { recordInferenceUsage } from '@semiont/observability';\nimport { ElementSchema, InferenceClient, InferenceLimits, InferenceResponse, StructuredResponse } from '../interface.js';\n\n// Slack added to the chars/4 prompt estimate when sizing `num_ctx`:\n// proportional to the estimate (the heuristic's error grows with prompt size)\n// plus a small fixed allowance for the model's chat template. The risk profile\n// is asymmetric — an undersized window silently clips input (the exact hole\n// managed num_ctx exists to close) while an oversized one only costs memory —\n// so the slack leans generous. Always capped at the model's real window.\nconst NUM_CTX_ESTIMATE_SLACK = 0.2;\nconst NUM_CTX_TEMPLATE_ALLOWANCE = 64;\n\ninterface OllamaGenerateResponse {\n response: string;\n done: boolean;\n done_reason?: string;\n /** Number of prompt tokens evaluated. Available on most Ollama versions. */\n prompt_eval_count?: number;\n /** Number of tokens generated. */\n eval_count?: number;\n}\n\nexport class OllamaInferenceClient implements InferenceClient {\n readonly type = 'ollama' as const;\n readonly modelId: string;\n private baseURL: string;\n private logger?: Logger;\n\n private limitsPromise?: Promise<InferenceLimits>;\n\n constructor(model: string, baseURL?: string, logger?: Logger) {\n this.baseURL = (baseURL || 'http://localhost:11434').replace(/\\/+$/, '');\n this.modelId = model;\n this.logger = logger;\n }\n\n limits(): Promise<InferenceLimits> {\n if (!this.limitsPromise) {\n this.limitsPromise = this.discoverLimits().catch((err: unknown) => {\n // Never cache a failed discovery — a transient outage would otherwise\n // pin every future call to the same rejection.\n this.limitsPromise = undefined;\n throw err;\n });\n }\n return this.limitsPromise;\n }\n\n private async discoverLimits(): Promise<InferenceLimits> {\n const res = await fetch(`${this.baseURL}/api/show`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ model: this.modelId }),\n });\n if (!res.ok) {\n throw new Error(\n `Failed to discover model limits: /api/show returned ${res.status} for '${this.modelId}'`,\n );\n }\n const data: unknown = await res.json();\n const modelInfo = isObject(data) && isObject(data['model_info']) ? data['model_info'] : undefined;\n const contextTokens = readContextLength(modelInfo);\n if (contextTokens === undefined) {\n throw new Error(`/api/show reports no context length for '${this.modelId}'`);\n }\n // Shared window: input and output draw from the same context — there is\n // no separate output ceiling, so the window is published as both (the\n // `maxOutputTokens === contextTokens` shape consumers key the split on).\n return { contextTokens, maxOutputTokens: contextTokens };\n }\n\n async generateText(prompt: string, maxTokens: number, temperature: number): Promise<string> {\n const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature);\n return response.text;\n }\n\n async generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number): Promise<InferenceResponse> {\n return this.generate(prompt, maxTokens, temperature, undefined);\n }\n\n async generateStructured<T>(\n prompt: string,\n maxTokens: number,\n temperature: number,\n elementSchema: ElementSchema,\n ): Promise<StructuredResponse<T>> {\n // Grammar-constrained sampling: the schema goes to Ollama's `format`\n // parameter, which constrains generation itself — same mechanism as the\n // old bare array schema, now element-typed. The response text is then\n // parsed here, and anything that does not read as an array is a THROW,\n // never a coerced [] — \"could not read the model\" must stay distinct\n // from \"the model found nothing.\"\n const response = await this.generate(prompt, maxTokens, temperature, elementSchema);\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(response.text);\n } catch (err) {\n this.logger?.error('Structured response could not be read', {\n model: this.modelId,\n textLength: response.text.length,\n stopReason: response.stopReason,\n });\n throw new Error(\n `Structured response could not be read: response is not valid JSON (stop_reason: ${response.stopReason})`,\n { cause: err },\n );\n }\n if (!Array.isArray(parsed)) {\n this.logger?.error('Structured response could not be read', {\n model: this.modelId,\n parsedType: typeof parsed,\n stopReason: response.stopReason,\n });\n throw new Error(\n `Structured response could not be read: parsed to ${typeof parsed}, not an array (stop_reason: ${response.stopReason})`,\n );\n }\n\n return { items: parsed as T[], stopReason: response.stopReason };\n }\n\n private async generate(\n prompt: string,\n maxTokens: number,\n temperature: number,\n elementSchema: ElementSchema | undefined,\n ): Promise<InferenceResponse> {\n this.logger?.debug('Generating text with Ollama', {\n model: this.modelId,\n promptLength: prompt.length,\n maxTokens,\n temperature,\n structured: elementSchema !== undefined,\n });\n\n // Managed context window: size num_ctx to cover this request, capped at\n // the model's discovered window. Without an explicit num_ctx Ollama uses\n // the model's *default* window and SILENTLY CLIPS any prompt beyond it —\n // input loss with no error (found 2026-07-30).\n const limits = await this.limits();\n const promptTokens = estimateTokens(prompt);\n if (promptTokens + maxTokens > limits.contextTokens) {\n throw new Error(\n `Prompt (~${promptTokens} tokens) + output budget (${maxTokens}) exceed the ` +\n `'${this.modelId}' context window (${limits.contextTokens} tokens)`,\n );\n }\n const numCtx = Math.min(\n limits.contextTokens,\n promptTokens + maxTokens\n + Math.ceil(promptTokens * NUM_CTX_ESTIMATE_SLACK) + NUM_CTX_TEMPLATE_ALLOWANCE,\n );\n\n const url = `${this.baseURL}/api/generate`;\n const start = performance.now();\n\n // Ollama's `format` parameter accepts either the literal string\n // `\"json\"` (any valid JSON, including objects, numbers, etc.) or a\n // JSON schema (constrains the top-level shape). The structured contract\n // is \"an array of elements matching the caller's schema,\" so we pass an\n // array schema wrapping it — the bare `\"json\"` string would let the\n // model satisfy \"valid JSON\" with `{\"entities\": [...]}` and break every\n // consumer that maps over the top-level value.\n const body: Record<string, unknown> = {\n model: this.modelId,\n prompt,\n stream: false,\n think: false,\n options: {\n num_predict: maxTokens,\n num_ctx: numCtx,\n temperature,\n },\n };\n if (elementSchema !== undefined) {\n body['format'] = { type: 'array', items: elementSchema };\n }\n\n let res: Response;\n try {\n res = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n });\n } catch (err) {\n recordInferenceUsage({\n provider: this.type,\n model: this.modelId,\n durationMs: performance.now() - start,\n outcome: 'error',\n });\n throw err;\n }\n\n if (!res.ok) {\n recordInferenceUsage({\n provider: this.type,\n model: this.modelId,\n durationMs: performance.now() - start,\n outcome: 'error',\n });\n const body = await res.text();\n this.logger?.error('Ollama API error', {\n model: this.modelId,\n status: res.status,\n body,\n });\n throw new Error(`Ollama API error (${res.status}): ${body}`);\n }\n\n const data = await res.json() as OllamaGenerateResponse;\n\n if (!data.response) {\n recordInferenceUsage({\n provider: this.type,\n model: this.modelId,\n durationMs: performance.now() - start,\n outcome: 'error',\n inputTokens: data.prompt_eval_count,\n outputTokens: data.eval_count,\n });\n this.logger?.error('Empty response from Ollama', { model: this.modelId });\n throw new Error('Empty response from Ollama');\n }\n\n recordInferenceUsage({\n provider: this.type,\n model: this.modelId,\n durationMs: performance.now() - start,\n outcome: 'success',\n inputTokens: data.prompt_eval_count,\n outputTokens: data.eval_count,\n });\n\n const stopReason = mapStopReason(data.done_reason);\n\n this.logger?.info('Text generation completed', {\n model: this.modelId,\n textLength: data.response.length,\n stopReason,\n });\n\n return {\n text: data.response,\n stopReason,\n };\n }\n}\n\n/**\n * The context length lives in `model_info` under an architecture-prefixed key\n * (e.g. `llama.context_length`); `general.architecture` names the prefix.\n * Falls back to any `*.context_length` key for models whose metadata omits\n * the architecture field.\n */\nfunction readContextLength(modelInfo: Record<string, unknown> | undefined): number | undefined {\n if (!modelInfo) return undefined;\n const arch = modelInfo['general.architecture'];\n if (typeof arch === 'string') {\n const direct = modelInfo[`${arch}.context_length`];\n if (isNumber(direct) && direct > 0) return direct;\n }\n const fallbackKey = Object.keys(modelInfo).find(k => k.endsWith('.context_length'));\n if (fallbackKey !== undefined) {\n const fallback = modelInfo[fallbackKey];\n if (isNumber(fallback) && fallback > 0) return fallback;\n }\n return undefined;\n}\n\nfunction mapStopReason(doneReason: string | undefined): string {\n switch (doneReason) {\n case 'stop': return 'end_turn';\n case 'length': return 'max_tokens';\n default: return doneReason || 'unknown';\n }\n}\n","// Factory for creating inference client instances based on configuration\n\nimport type { Logger } from '@semiont/core';\nimport { InferenceClient } from './interface.js';\nimport { AnthropicInferenceClient } from './implementations/anthropic.js';\nimport { OllamaInferenceClient } from './implementations/ollama.js';\n\nexport type InferenceClientType = 'anthropic' | 'ollama';\n\nexport interface InferenceClientConfig {\n type: InferenceClientType;\n apiKey?: string;\n model: string;\n endpoint?: string;\n baseURL?: string;\n}\n\nexport function createInferenceClient(config: InferenceClientConfig, logger?: Logger): InferenceClient {\n switch (config.type) {\n case 'anthropic': {\n if (!config.apiKey || config.apiKey.trim() === '') {\n throw new Error('apiKey is required for Anthropic inference client');\n }\n return new AnthropicInferenceClient(\n config.apiKey,\n config.model,\n config.endpoint || config.baseURL,\n logger\n );\n }\n\n case 'ollama': {\n return new OllamaInferenceClient(\n config.model,\n config.endpoint || config.baseURL,\n logger\n );\n }\n\n default:\n throw new Error(`Unsupported inference client type: ${config.type}`);\n }\n}\n","// Mock implementation of InferenceClient for testing\n\nimport { ElementSchema, InferenceClient, InferenceLimits, InferenceResponse, StructuredResponse } from '../interface.js';\n\n// Generous defaults so existing consumers never trip chunking or window\n// guards unless a test injects tighter limits deliberately.\nconst GENEROUS_LIMITS: InferenceLimits = {\n contextTokens: 1_000_000,\n maxOutputTokens: 1_000_000,\n};\n\nexport class MockInferenceClient implements InferenceClient {\n readonly type = 'mock' as const;\n readonly modelId = 'mock-model' as const;\n private responses: string[] = [];\n private responseIndex: number = 0;\n private stopReasons: string[] = [];\n private injectedLimits: InferenceLimits;\n public calls: Array<{ prompt: string; maxTokens: number; temperature: number; elementSchema?: ElementSchema }> = [];\n\n constructor(responses: string[] = ['Mock response'], stopReasons?: string[], limits?: InferenceLimits) {\n this.responses = responses;\n this.stopReasons = stopReasons || responses.map(() => 'end_turn');\n this.injectedLimits = limits ?? GENEROUS_LIMITS;\n }\n\n async limits(): Promise<InferenceLimits> {\n return this.injectedLimits;\n }\n\n async generateText(prompt: string, maxTokens: number, temperature: number): Promise<string> {\n const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature);\n return response.text;\n }\n\n async generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number): Promise<InferenceResponse> {\n this.calls.push({ prompt, maxTokens, temperature });\n return this.nextResponse();\n }\n\n /**\n * Structured surface: pops the same responses queue and PARSES the entry,\n * mirroring the real contract — a queued string that is not a JSON array\n * throws \"could not be read\", so tests inject the malformed shape simply by\n * queuing it (`setResponses(['not json'])`). The element schema is recorded\n * on `calls` so tests can assert what the caller declared.\n */\n async generateStructured<T>(\n prompt: string,\n maxTokens: number,\n temperature: number,\n elementSchema: ElementSchema,\n ): Promise<StructuredResponse<T>> {\n this.calls.push({ prompt, maxTokens, temperature, elementSchema });\n const { text, stopReason } = this.nextResponse();\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch (err) {\n throw new Error(\n `Structured response could not be read: response is not valid JSON (stop_reason: ${stopReason})`,\n { cause: err },\n );\n }\n if (!Array.isArray(parsed)) {\n throw new Error(\n `Structured response could not be read: parsed to ${typeof parsed}, not an array (stop_reason: ${stopReason})`,\n );\n }\n return { items: parsed as T[], stopReason };\n }\n\n private nextResponse(): InferenceResponse {\n const text = this.responses[this.responseIndex];\n const stopReason = this.stopReasons[this.responseIndex] || 'end_turn';\n\n if (this.responseIndex < this.responses.length - 1) {\n this.responseIndex++;\n }\n\n return { text, stopReason };\n }\n\n // Test helper methods\n reset(): void {\n this.calls = [];\n this.responseIndex = 0;\n }\n\n setResponses(responses: string[], stopReasons?: string[]): void {\n this.responses = responses;\n this.stopReasons = stopReasons || responses.map(() => 'end_turn');\n this.responseIndex = 0;\n }\n}\n"],"mappings":"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","names":["isObject","recordInferenceUsage","body"]}
|
|
1
|
+
{"version":3,"sources":["../src/implementations/anthropic.ts","../src/interface.ts","../src/implementations/ollama.ts","../src/factory.ts","../src/implementations/mock.ts"],"sourcesContent":["// Anthropic Claude implementation of InferenceClient interface\n\nimport Anthropic from '@anthropic-ai/sdk';\nimport { isObject, type Logger } from '@semiont/core';\nimport { recordInferenceUsage } from '@semiont/observability';\nimport { ElementSchema, InferenceClient, InferenceLimits, InferenceResponse, StructuredReadError, StructuredResponse, TokenUsage } from '../interface.js';\n\n// The SDK's worst-case output-rate model: client.js's\n// calculateNonstreamingTimeout projects a call's maximum duration as\n// (60min × max_tokens) / 128_000 and refuses non-streaming create() calls\n// projected past its 10-minute timeout. ONE constant, two derivations: the\n// streaming switch below, and `limits().outputTokensPerHour` — the single\n// duration statement this provider surface makes, which detection's\n// duration budget derives from (ABANDONED-INFERENCE P4). Invalidated if\n// the SDK revises its rate model: check calculateNonstreamingTimeout on\n// SDK upgrades.\nconst OUTPUT_TOKENS_PER_HOUR = 128_000;\n\n// Above ~21,333 output tokens (the 10-minute projection) we stream\n// internally and assemble the final message — same request shape, same\n// response handling, same interface.\nconst NONSTREAMING_MAX_OUTPUT_TOKENS = Math.floor(OUTPUT_TOKENS_PER_HOUR / 6);\n\n// Structured generation rides `output_config.format` — response-level\n// structured output: the response TEXT is the schema-conforming JSON, with a\n// top-level ARRAY root (accepted on both live-config models — spike\n// 2026-08-06, `.plans/spikes/output-config-array-root.md`). This replaced\n// the pre-structured-outputs scaffolding: a forced `emit_json_array` tool\n// whose object-only input required an `items` wrapper and an unwrap — and\n// the unwrap was the exact line that silently coerced an unreadable payload\n// to `[]` (STRUCTURED-INFERENCE §Problem). There is no tool-input\n// accumulation step left for the SDK to hand over unparsed; the read path is\n// now the same parse-and-verify shape as Ollama's.\n\n/**\n * Everything one Models API call teaches us about the configured model. The\n * capability stays PRIVATE to this client: its only consumer is the gate in\n * `generateStructured`, so it does not cross the `InferenceClient` interface\n * (no speculative surface — widen `InferenceLimits` only when an external\n * consumer exists).\n */\ninterface ModelDiscovery {\n limits: InferenceLimits;\n structuredOutputsSupported: boolean;\n}\n\nexport class AnthropicInferenceClient implements InferenceClient {\n readonly type = 'anthropic' as const;\n // Hosted API: a single detection job uses a sliver of the account rate limit\n // (measured 2026-09-04: ~1 request / 72 s, zero 429s at 4 concurrent types),\n // so independent calls genuinely parallelize. Conservative until an 8-way run\n // measures the next step (DETECTION-QUALITY-THROUGHPUT P6).\n readonly maxConcurrency = 4;\n // Universal for real providers (user ruling 2026-09-05): \"no observed\n // collapse\" here was absence-of-looking, and the unexplained ~2× yield gap\n // vs gemma on the same document is exactly what verification answers. The\n // ~2× billed input is the accepted cost.\n readonly verifyDetectionYield = true;\n readonly modelId: string;\n private client: Anthropic;\n private logger?: Logger;\n private discoveryPromise?: Promise<ModelDiscovery>;\n\n constructor(apiKey: string, model: string, baseURL?: string, logger?: Logger) {\n this.client = new Anthropic({\n apiKey,\n baseURL: baseURL || 'https://api.anthropic.com',\n });\n this.modelId = model;\n this.logger = logger;\n }\n\n limits(): Promise<InferenceLimits> {\n return this.discover().then(d => d.limits);\n }\n\n private discover(): Promise<ModelDiscovery> {\n if (!this.discoveryPromise) {\n this.discoveryPromise = this.discoverModel().catch((err: unknown) => {\n // Never cache a failed discovery — a transient outage would otherwise\n // pin every future call to the same rejection.\n this.discoveryPromise = undefined;\n throw err;\n });\n }\n return this.discoveryPromise;\n }\n\n private async discoverModel(): Promise<ModelDiscovery> {\n // The Models API publishes the actual ceilings AND capabilities per\n // model — no hand-maintained table to go stale when a new model ships,\n // and the API's own metadata outranks documentation prose when the two\n // disagree (measured: the docs' supported-model list was stale while\n // `capabilities.structured_outputs.supported` was correct).\n const info = await this.client.models.retrieve(this.modelId).catch((err: unknown) => {\n throw new Error(\n `Failed to discover model limits for '${this.modelId}' from the Models API`,\n { cause: err },\n );\n });\n if (info.max_input_tokens == null || info.max_tokens == null) {\n throw new Error(`Models API reports no context/output ceilings for '${this.modelId}'`);\n }\n // `capabilities` is not declared on the SDK's ModelInfo type — narrow\n // through the core guards rather than casting. Absent metadata reads as\n // unsupported: the gate then refuses loudly (D4), never guesses.\n const raw: unknown = info;\n const structuredOutputsSupported =\n isObject(raw) &&\n isObject(raw['capabilities']) &&\n isObject(raw['capabilities']['structured_outputs']) &&\n raw['capabilities']['structured_outputs']['supported'] === true;\n return {\n limits: {\n contextTokens: info.max_input_tokens,\n maxOutputTokens: info.max_tokens,\n outputTokensPerHour: OUTPUT_TOKENS_PER_HOUR,\n },\n structuredOutputsSupported,\n };\n }\n\n private requestMessage(params: Anthropic.MessageCreateParamsNonStreaming, signal?: AbortSignal): Promise<Anthropic.Message> {\n // The signal rides the SDK's RequestOptions: an abort tears down the live\n // attempt AND is checked between the SDK's internal retries, so a\n // cancelled call cannot survive as a background zombie inside the SDK's\n // own retry/backoff loop (ABANDONED-INFERENCE P0 caught one completing\n // 24–34 minutes after abandonment). For visibility into those internal\n // retries themselves, the SDK's ANTHROPIC_LOG=debug env knob logs every\n // attempt — nothing to re-implement here.\n if (params.max_tokens > NONSTREAMING_MAX_OUTPUT_TOKENS) {\n return this.client.messages.stream(params, { signal }).finalMessage();\n }\n return this.client.messages.create(params, { signal });\n }\n\n async generateText(prompt: string, maxTokens: number, temperature: number, signal?: AbortSignal): Promise<string> {\n const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature, signal);\n return response.text;\n }\n\n async generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number, signal?: AbortSignal): Promise<InferenceResponse> {\n this.logger?.debug('Generating text with inference client', {\n model: this.modelId,\n promptLength: prompt.length,\n maxTokens,\n temperature,\n });\n\n const params: Anthropic.MessageCreateParamsNonStreaming = {\n model: this.modelId,\n max_tokens: maxTokens,\n temperature,\n messages: [{ role: 'user', content: prompt }],\n };\n\n const start = performance.now();\n const response = await this.recordedRequest(params, start, signal);\n\n const textContent = response.content.find(c => c.type === 'text');\n if (!textContent || textContent.type !== 'text') {\n this.recordError(start, response);\n this.logger?.error('No text content in inference response', {\n model: this.modelId,\n contentTypes: response.content.map(c => c.type)\n });\n throw new Error('No text content in inference response');\n }\n const text = textContent.text;\n\n recordInferenceUsage({\n provider: this.type,\n model: this.modelId,\n durationMs: performance.now() - start,\n outcome: 'success',\n inputTokens: response.usage?.input_tokens,\n outputTokens: response.usage?.output_tokens,\n });\n\n this.logger?.info('Text generation completed', {\n model: this.modelId,\n textLength: text.length,\n stopReason: response.stop_reason,\n requestId: requestIdOf(response),\n });\n\n return {\n text,\n stopReason: response.stop_reason || 'unknown'\n };\n }\n\n async generateStructured<T>(\n prompt: string,\n maxTokens: number,\n temperature: number,\n elementSchema: ElementSchema,\n signal?: AbortSignal,\n ): Promise<StructuredResponse<T>> {\n // Capability gate (D3/D4): model choice is deployment config, so the\n // client asks the provider whether the configured model can honour\n // strictness — and REFUSES when it cannot. Silent fallback to\n // unconstrained tool use is exactly the behaviour that turned 202 real\n // entities into a green empty job. The discovery is the same cached\n // Models API call `limits()` uses; no extra round trip.\n const discovery = await this.discover();\n if (!discovery.structuredOutputsSupported) {\n throw new Error(\n `Model '${this.modelId}' does not report support for strict structured outputs ` +\n `(Models API capabilities.structured_outputs) — refusing rather than degrading to ` +\n `unconstrained tool use, which silently discards unreadable results. Re-point the ` +\n `inference.model key that pins this worker/actor in .semiont/semiontconfig/*.toml ` +\n `(e.g. environments.<env>.workers.<job-type>.inference.model) at a model that ` +\n `reports supported: true.`,\n );\n }\n\n this.logger?.debug('Generating structured output with inference client', {\n model: this.modelId,\n promptLength: prompt.length,\n maxTokens,\n temperature,\n });\n\n const params: Anthropic.MessageCreateParamsNonStreaming = {\n model: this.modelId,\n max_tokens: maxTokens,\n temperature,\n messages: [{ role: 'user', content: prompt }],\n // Response-level structured output with an ARRAY root: the response\n // text IS the schema-conforming JSON. No tools, no prefill.\n output_config: {\n format: {\n type: 'json_schema',\n schema: { type: 'array', items: elementSchema },\n },\n },\n };\n\n const start = performance.now();\n const response = await this.recordedRequest(params, start, signal);\n\n const textContent = response.content.find(c => c.type === 'text');\n if (!textContent || textContent.type !== 'text') {\n this.recordError(start, response);\n this.logger?.error('No text content in structured inference response', {\n model: this.modelId,\n contentTypes: response.content.map(c => c.type)\n });\n throw new Error('No text content in structured inference response');\n }\n\n // Anything that does not read as an array is a THROW, never a coerced\n // `[]` — \"we could not read the model\" must never be conflated with\n // \"the model found nothing\": that conflation is what silently discarded\n // 202 real entities as a green empty result. A truncated (`max_tokens`)\n // response surfaces here too, as unparseable JSON naming its stop_reason.\n let parsed: unknown;\n try {\n parsed = JSON.parse(textContent.text);\n } catch (err) {\n this.recordError(start, response);\n this.logger?.error('Structured response could not be read', {\n model: this.modelId,\n textLength: textContent.text.length,\n stopReason: response.stop_reason,\n });\n throw new StructuredReadError('response is not valid JSON', response.stop_reason || 'unknown', { cause: err });\n }\n if (!Array.isArray(parsed)) {\n this.recordError(start, response);\n this.logger?.error('Structured response could not be read', {\n model: this.modelId,\n parsedType: typeof parsed,\n stopReason: response.stop_reason,\n });\n throw new StructuredReadError(`parsed to ${typeof parsed}, not an array`, response.stop_reason || 'unknown');\n }\n\n recordInferenceUsage({\n provider: this.type,\n model: this.modelId,\n durationMs: performance.now() - start,\n outcome: 'success',\n inputTokens: response.usage?.input_tokens,\n outputTokens: response.usage?.output_tokens,\n });\n\n this.logger?.info('Structured generation completed', {\n model: this.modelId,\n items: parsed.length,\n stopReason: response.stop_reason,\n requestId: requestIdOf(response),\n });\n\n return {\n items: parsed as T[],\n stopReason: response.stop_reason || 'unknown',\n ...usageOf(response),\n };\n }\n\n /** Issue the request, recording an error metric if the transport throws. */\n private async recordedRequest(params: Anthropic.MessageCreateParamsNonStreaming, start: number, signal?: AbortSignal): Promise<Anthropic.Message> {\n try {\n return await this.requestMessage(params, signal);\n } catch (err) {\n recordInferenceUsage({\n provider: this.type,\n model: this.modelId,\n durationMs: performance.now() - start,\n outcome: 'error',\n });\n throw err;\n }\n }\n\n private recordError(start: number, response: Anthropic.Message): void {\n recordInferenceUsage({\n provider: this.type,\n model: this.modelId,\n durationMs: performance.now() - start,\n outcome: 'error',\n inputTokens: response.usage?.input_tokens,\n outputTokens: response.usage?.output_tokens,\n });\n }\n}\n\n/**\n * The provider's request id, for correlating our logs with Anthropic's and\n * telling one attempt from another. The SDK attaches `_request_id` to the\n * returned message at runtime but does not declare it on the `Message` type,\n * so it is read through a guard rather than a cast.\n */\nfunction requestIdOf(response: unknown): string | undefined {\n if (isObject(response) && typeof response['_request_id'] === 'string') {\n return response['_request_id'];\n }\n return undefined;\n}\n\n/**\n * The provider's own token counts, shaped for `TokenUsage`. Absent when the\n * SDK reports none — never zero-filled: a zero would read as \"this call cost\n * nothing\", which is a different claim from \"we do not know\".\n */\nfunction usageOf(response: { usage?: { input_tokens?: number; output_tokens?: number } }): { usage?: TokenUsage } {\n const { input_tokens, output_tokens } = response.usage ?? {};\n if (input_tokens === undefined || output_tokens === undefined) return {};\n return { usage: { inputTokens: input_tokens, outputTokens: output_tokens } };\n}\n","// Inference client interface - all implementations must follow this contract\n\n/**\n * What the call actually cost, as the PROVIDER counted it — never estimated\n * here. Optional because a provider may not report it (and a call that fails\n * before generating has nothing to report); absent means unknown, and a\n * consumer must treat it as unknown rather than substituting a guess.\n */\nexport interface TokenUsage {\n inputTokens: number;\n outputTokens: number;\n}\n\nexport interface InferenceResponse {\n text: string;\n stopReason: 'end_turn' | 'max_tokens' | 'stop_sequence' | string;\n usage?: TokenUsage;\n}\n\n/**\n * Raw JSON Schema for ONE array element of a structured generation — a plain\n * object, not a TS type and not a validator instance. Both providers consume\n * JSON Schema directly (Anthropic as the array-root schema under\n * `output_config.format`; Ollama as `format: { type: 'array', items: … }`),\n * so anything richer would abstract one shape with two consumers.\n *\n * Constrain it to what both providers enforce: objects,\n * `string`/`number`/`boolean`/`null`, `enum`, `const`, `required`, and\n * `additionalProperties: false`. Numeric and string constraints (`minimum`,\n * `maxLength`) are NOT enforced by Anthropic strict mode — declaring them\n * buys nothing and misleads the reader.\n */\nexport type ElementSchema = Record<string, unknown>;\n\n/**\n * A structured generation's result: the elements the model produced, plus the\n * provider's stop reason (consumers gate on 'max_tokens' — truncation is data\n * loss, not \"fewer items\").\n *\n * `items` is `T[]`, never a string: there is no representable value meaning\n * \"here is some text I could not read.\" An implementation that cannot deliver\n * the array THROWS — failure is distinct from empty by construction.\n *\n * `T` is a caller assertion, not a runtime guarantee: nothing verifies the\n * element schema and `T` agree, and the type parameter is erased. Declare the\n * schema and `T` adjacently at the call site so drift is visible in one\n * place, and keep per-element structural guards on the consuming side.\n */\nexport interface StructuredResponse<T> {\n items: T[];\n stopReason: 'end_turn' | 'max_tokens' | 'stop_sequence' | string;\n usage?: TokenUsage;\n}\n\n/**\n * A provider's actual ceilings for the configured model, discovered from the\n * provider itself (Anthropic Models API; Ollama `/api/show`) — never\n * hand-maintained constants. Detection budget arithmetic derives from these.\n */\nexport interface InferenceLimits {\n /**\n * The context window in tokens. Semantics differ by provider shape:\n * Anthropic reports maximum *input* tokens (output has its own ceiling);\n * Ollama reports the *shared* input+output window and mirrors it in\n * `maxOutputTokens` (there is no separate output ceiling), so\n * `maxOutputTokens === contextTokens` signals a shared window.\n */\n contextTokens: number;\n /** Maximum output tokens per generation. */\n maxOutputTokens: number;\n /**\n * The provider's own worst-case output-rate model, in output tokens per\n * hour, when it publishes one. Anthropic's SDK projects a call's maximum\n * duration as `max_tokens / rate` (client.js `calculateNonstreamingTimeout`,\n * 128_000/hour) and refuses non-streaming calls projected past 10 minutes —\n * the one duration statement that provider surface makes. Consumers with\n * their own call deadline derive a duration-safe output budget from it\n * (ABANDONED-INFERENCE P4). Absent for providers whose rates are\n * unknowable a priori (Ollama — local hardware). Absence does NOT mean no\n * duration bound: the detection consumer applies its own conservative\n * assumed floor rate instead (OLLAMA-DETECTION-TESTING P3b) — an unbounded\n * budget turned model repetition loops into hour-long transient burns.\n */\n outputTokensPerHour?: number;\n}\n\n/**\n * Thrown when a structured generation's response cannot be read as the\n * requested array — never coerced to `[]` (empty is a legitimate, distinct\n * outcome). One class for every implementation, because the message shape\n * and the classification contract must not diverge between providers.\n *\n * Carries the provider's stop reason because the cause classifies\n * differently downstream: `max_tokens` means the JSON was cut off by the\n * output budget — the same input truncates the same way, so a retry is\n * guaranteed waste — while any other reason is model misbehavior a retry\n * may legitimately fix.\n *\n * Also thrown — on either generation path — when a response arrives EMPTY:\n * a thinking model can exhaust the whole output budget on hidden reasoning\n * before its first response character (measured live, gpt-oss:120b-cloud\n * 2026-09-05). Truncated-to-nothing is still truncation, and it needs the\n * same stop-reason ride to classify correctly.\n */\nexport class StructuredReadError extends Error {\n override readonly name = 'StructuredReadError';\n constructor(detail: string, readonly stopReason: string, options?: ErrorOptions) {\n super(`Structured response could not be read: ${detail} (stop_reason: ${stopReason})`, options);\n }\n}\n\nexport interface InferenceClient {\n /** Provider type identifier (e.g. 'anthropic', 'ollama') */\n readonly type: string;\n\n /** Model identifier used for generation (e.g. 'claude-opus-4-6', 'llama3') */\n readonly modelId: string;\n\n /**\n * How many INDEPENDENT inference calls a caller should run concurrently\n * against this provider for a throughput gain (DETECTION-QUALITY-THROUGHPUT\n * P6 — detection's per-type fan-out reads this).\n *\n * This is a property of the provider's economics, which is why it lives on\n * the provider and not in the caller. A HOSTED API whose per-account rate\n * limit sits far above one job's usage has real spare capacity, so >1\n * genuinely parallelizes. A LOCAL single-model server (Ollama) is 1: its\n * throughput is hardware-bound, so concurrent requests only queue or split\n * one GPU — no aggregate speedup, and N live KV-cache contexts is memory\n * pressure that can OOM. There is no honest default across those two worlds,\n * so this is required, not optional.\n *\n * Hard-coded per implementation for now; the natural seam for future\n * per-provider or admin tuning (a value that later comes from config changes\n * only where this is SET, not the callers).\n */\n readonly maxConcurrency: number;\n\n /**\n * Whether detection should run the count-verifier against this provider's\n * extractions (OLLAMA-DETECTION-TESTING P3c; universalized to every real\n * provider by user ruling 2026-09-05 — unverified completeness is not a\n * savings). Declared HERE, per implementation, because @semiont/jobs does no\n * provider-specific switching (architecture ruling, same date): whatever\n * varies by provider is a capability on this contract, like\n * `maxConcurrency`. The mock alone defaults false, so deterministic tests\n * opt in explicitly rather than paying a queue-popping count call by\n * surprise.\n */\n readonly verifyDetectionYield: boolean;\n\n /**\n * The provider's actual context/output ceilings for `modelId`. Discovered\n * lazily on first call and cached for the client's lifetime; a failed\n * discovery is NOT cached — the next call retries. Throws when the ceilings\n * cannot be determined (unknown model, discovery endpoint unreachable):\n * fail-loud, never a guessed floor.\n */\n limits(): Promise<InferenceLimits>;\n\n /**\n * Generate text from a prompt (simple interface).\n *\n * `signal` (here and on every generation method — a trailing optional\n * parameter, deliberately not an options bag; STRUCTURED-INFERENCE removed\n * that shape on purpose): true cancellation, ABANDONED-INFERENCE P1.\n * Implementations MUST thread it to their transport so an abort tears down\n * the underlying request — and, for SDKs with internal retry loops, ends\n * those too — rejecting promptly. Accepting the parameter and ignoring it\n * is a defect worse than not having it: cancellation tests pass against\n * such an adapter while zombie requests keep running (and billing) in\n * production.\n */\n generateText(prompt: string, maxTokens: number, temperature: number, signal?: AbortSignal): Promise<string>;\n\n /**\n * Generate text with detailed response information\n */\n generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number, signal?: AbortSignal): Promise<InferenceResponse>;\n\n /**\n * Generate a JSON array whose elements satisfy `elementSchema`, as parsed\n * values — the structured counterpart of `generateTextWithMetadata`, and\n * the ONLY generation surface detection may use.\n *\n * The return type carries the guarantee the old `format: 'json'` option\n * left in a comment: callers receive `T[]` or an exception. When the\n * provider's answer cannot be read as an array (the SDK hands tool input\n * over as an unparsed string, the response is missing the array, the\n * grammar was not honoured), implementations THROW a\n * \"Structured response could not be read\" error — they never coerce to\n * `[]`, because empty is a legitimate, distinct outcome.\n */\n generateStructured<T>(\n prompt: string,\n maxTokens: number,\n temperature: number,\n elementSchema: ElementSchema,\n signal?: AbortSignal,\n ): Promise<StructuredResponse<T>>;\n}\n","// Ollama implementation of InferenceClient interface\n// Uses native Ollama HTTP API (no SDK dependency)\n\nimport { Agent } from 'undici';\nimport { estimateTokens, isNumber, isObject } from '@semiont/core';\nimport type { Logger } from '@semiont/core';\nimport { recordInferenceUsage } from '@semiont/observability';\nimport { ElementSchema, InferenceClient, InferenceLimits, InferenceResponse, StructuredReadError, StructuredResponse } from '../interface.js';\n\n// With `stream: false` Ollama sends nothing — not even response headers —\n// until the whole generation finishes, so any transport-level header timeout\n// is a hidden generation ceiling: undici's 300s default killed every longer\n// generation as a retryable-looking `TypeError: fetch failed` before the\n// caller's own bound could fire. Both transport timeouts are disabled here so\n// the caller's AbortSignal is the single bound on a generate call.\n//\n// The assertion bridges a declaration skew only: @types/node types this slot\n// via undici-types@8, while the Agent must come from undici@7 to match Node\n// 24's bundled copy — an undici@8 Agent is rejected at runtime with\n// UND_ERR_INVALID_ARG (measured 2026-09-04; transport.test.ts gates both the\n// runtime compatibility and the failure shape).\ntype FetchDispatcher = NonNullable<RequestInit['dispatcher']>;\nexport const unboundedTransport = new Agent({ headersTimeout: 0, bodyTimeout: 0 }) as unknown as FetchDispatcher;\n\n// Slack added to the chars/4 prompt estimate when sizing `num_ctx`:\n// proportional to the estimate (the heuristic's error grows with prompt size)\n// plus a small fixed allowance for the model's chat template. The risk profile\n// is asymmetric — an undersized window silently clips input (the exact hole\n// managed num_ctx exists to close) while an oversized one only costs memory —\n// so the slack leans generous. Always capped at the model's real window.\nconst NUM_CTX_ESTIMATE_SLACK = 0.2;\nconst NUM_CTX_TEMPLATE_ALLOWANCE = 64;\n\ninterface OllamaGenerateResponse {\n response: string;\n done: boolean;\n done_reason?: string;\n /**\n * Hidden reasoning from a thinking model. Requests always send\n * `think: false`, but cloud-hosted reasoning models ignore it (measured\n * live, gpt-oss:120b-cloud 2026-09-05): the thinking happens anyway,\n * spends the caller's `num_predict` budget, and its tokens are folded\n * invisibly into `eval_count`.\n */\n thinking?: string;\n /** Number of prompt tokens evaluated. Available on most Ollama versions. */\n prompt_eval_count?: number;\n /** Number of tokens generated — INCLUDING any hidden thinking tokens. */\n eval_count?: number;\n}\n\nexport class OllamaInferenceClient implements InferenceClient {\n readonly type = 'ollama' as const;\n // Local single model: generation throughput is hardware-bound, so concurrent\n // requests queue or split one GPU for no aggregate speedup — and each live\n // context costs KV-cache memory. Detection runs its types sequentially here\n // (DETECTION-QUALITY-THROUGHPUT P6).\n readonly maxConcurrency = 1;\n // Where the collapse risk was MEASURED (F7) — the verifier's original home.\n readonly verifyDetectionYield = true;\n readonly modelId: string;\n private baseURL: string;\n private logger?: Logger;\n\n private limitsPromise?: Promise<InferenceLimits>;\n\n constructor(model: string, baseURL?: string, logger?: Logger) {\n this.baseURL = (baseURL || 'http://localhost:11434').replace(/\\/+$/, '');\n this.modelId = model;\n this.logger = logger;\n }\n\n limits(): Promise<InferenceLimits> {\n if (!this.limitsPromise) {\n this.limitsPromise = this.discoverLimits().catch((err: unknown) => {\n // Never cache a failed discovery — a transient outage would otherwise\n // pin every future call to the same rejection.\n this.limitsPromise = undefined;\n throw err;\n });\n }\n return this.limitsPromise;\n }\n\n private async discoverLimits(): Promise<InferenceLimits> {\n const res = await fetch(`${this.baseURL}/api/show`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ model: this.modelId }),\n });\n if (!res.ok) {\n throw new Error(\n `Failed to discover model limits: /api/show returned ${res.status} for '${this.modelId}'`,\n );\n }\n const data: unknown = await res.json();\n const modelInfo = isObject(data) && isObject(data['model_info']) ? data['model_info'] : undefined;\n const contextTokens = readContextLength(modelInfo);\n if (contextTokens === undefined) {\n throw new Error(`/api/show reports no context length for '${this.modelId}'`);\n }\n // Shared window: input and output draw from the same context — there is\n // no separate output ceiling, so the window is published as both (the\n // `maxOutputTokens === contextTokens` shape consumers key the split on).\n return { contextTokens, maxOutputTokens: contextTokens };\n }\n\n async generateText(prompt: string, maxTokens: number, temperature: number, signal?: AbortSignal): Promise<string> {\n const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature, signal);\n return response.text;\n }\n\n async generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number, signal?: AbortSignal): Promise<InferenceResponse> {\n return this.generate(prompt, maxTokens, temperature, undefined, signal);\n }\n\n async generateStructured<T>(\n prompt: string,\n maxTokens: number,\n temperature: number,\n elementSchema: ElementSchema,\n signal?: AbortSignal,\n ): Promise<StructuredResponse<T>> {\n // Grammar-constrained sampling: the schema goes to Ollama's `format`\n // parameter, which constrains generation itself — same mechanism as the\n // old bare array schema, now element-typed. The response text is then\n // parsed here, and anything that does not read as an array is a THROW,\n // never a coerced [] — \"could not read the model\" must stay distinct\n // from \"the model found nothing.\"\n const response = await this.generate(prompt, maxTokens, temperature, elementSchema, signal);\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(response.text);\n } catch (err) {\n this.logger?.error('Structured response could not be read', {\n model: this.modelId,\n textLength: response.text.length,\n stopReason: response.stopReason,\n });\n throw new StructuredReadError('response is not valid JSON', response.stopReason, { cause: err });\n }\n if (!Array.isArray(parsed)) {\n this.logger?.error('Structured response could not be read', {\n model: this.modelId,\n parsedType: typeof parsed,\n stopReason: response.stopReason,\n });\n throw new StructuredReadError(`parsed to ${typeof parsed}, not an array`, response.stopReason);\n }\n\n return { items: parsed as T[], stopReason: response.stopReason, ...(response.usage ? { usage: response.usage } : {}) };\n }\n\n private async generate(\n prompt: string,\n maxTokens: number,\n temperature: number,\n elementSchema: ElementSchema | undefined,\n signal?: AbortSignal,\n ): Promise<InferenceResponse> {\n this.logger?.debug('Generating text with Ollama', {\n model: this.modelId,\n promptLength: prompt.length,\n maxTokens,\n temperature,\n structured: elementSchema !== undefined,\n });\n\n // Managed context window: size num_ctx to cover this request, capped at\n // the model's discovered window. Without an explicit num_ctx Ollama uses\n // the model's *default* window and SILENTLY CLIPS any prompt beyond it —\n // input loss with no error (found 2026-07-30).\n const limits = await this.limits();\n const promptTokens = estimateTokens(prompt);\n if (promptTokens + maxTokens > limits.contextTokens) {\n throw new Error(\n `Prompt (~${promptTokens} tokens) + output budget (${maxTokens}) exceed the ` +\n `'${this.modelId}' context window (${limits.contextTokens} tokens)`,\n );\n }\n const numCtx = Math.min(\n limits.contextTokens,\n promptTokens + maxTokens\n + Math.ceil(promptTokens * NUM_CTX_ESTIMATE_SLACK) + NUM_CTX_TEMPLATE_ALLOWANCE,\n );\n\n const url = `${this.baseURL}/api/generate`;\n const start = performance.now();\n\n // Ollama's `format` parameter accepts either the literal string\n // `\"json\"` (any valid JSON, including objects, numbers, etc.) or a\n // JSON schema (constrains the top-level shape). The structured contract\n // is \"an array of elements matching the caller's schema,\" so we pass an\n // array schema wrapping it — the bare `\"json\"` string would let the\n // model satisfy \"valid JSON\" with `{\"entities\": [...]}` and break every\n // consumer that maps over the top-level value.\n const body: Record<string, unknown> = {\n model: this.modelId,\n prompt,\n stream: false,\n think: false,\n options: {\n num_predict: maxTokens,\n num_ctx: numCtx,\n temperature,\n },\n };\n if (elementSchema !== undefined) {\n // Grammar-enforced for locally served models. Cloud-routed models\n // (`*-cloud`) treat this as ADVISORY only — a schema-violating response\n // is possible there (measured live 2026-09-05: bare strings where the\n // schema required objects) and surfaces as a StructuredReadError.\n body['format'] = { type: 'array', items: elementSchema };\n }\n\n let res: Response;\n try {\n // True cancellation (ABANDONED-INFERENCE P1): the signal tears down the\n // socket, so an aborted call cannot keep generating on the server's\n // dime after its job is gone.\n res = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n signal,\n dispatcher: unboundedTransport,\n });\n } catch (err) {\n recordInferenceUsage({\n provider: this.type,\n model: this.modelId,\n durationMs: performance.now() - start,\n outcome: 'error',\n });\n throw err;\n }\n\n if (!res.ok) {\n recordInferenceUsage({\n provider: this.type,\n model: this.modelId,\n durationMs: performance.now() - start,\n outcome: 'error',\n });\n const body = await res.text();\n this.logger?.error('Ollama API error', {\n model: this.modelId,\n status: res.status,\n body,\n });\n throw new Error(`Ollama API error (${res.status}): ${body}`);\n }\n\n const data = await res.json() as OllamaGenerateResponse;\n const stopReason = mapStopReason(data.done_reason);\n\n if (data.thinking) {\n // The adapter cannot prevent ignored `think: false` — it can only make\n // the cost visible: the thinking billed, and `eval_count` (and any rate\n // derived from it) is inflated by tokens that never reached the\n // response.\n this.logger?.warn('Model produced hidden thinking despite think:false', {\n model: this.modelId,\n thinkingChars: data.thinking.length,\n });\n }\n\n if (!data.response) {\n recordInferenceUsage({\n provider: this.type,\n model: this.modelId,\n durationMs: performance.now() - start,\n outcome: 'error',\n inputTokens: data.prompt_eval_count,\n outputTokens: data.eval_count,\n });\n this.logger?.error('Empty response from Ollama', {\n model: this.modelId,\n stopReason,\n thinkingChars: data.thinking?.length,\n });\n // Truncated-to-nothing is still truncation: a thinking model can burn\n // the entire output budget before its first response character. The\n // stop reason must ride the error so `max_tokens` classifies\n // deterministic (and subdivides) instead of masquerading as an\n // unrecognized retryable mystery; any other stop reason stays\n // retryable, exactly as an untyped error did.\n throw new StructuredReadError('response is empty', stopReason);\n }\n\n recordInferenceUsage({\n provider: this.type,\n model: this.modelId,\n durationMs: performance.now() - start,\n outcome: 'success',\n inputTokens: data.prompt_eval_count,\n outputTokens: data.eval_count,\n });\n\n this.logger?.info('Text generation completed', {\n model: this.modelId,\n textLength: data.response.length,\n stopReason,\n });\n\n return {\n text: data.response,\n stopReason,\n ...(data.prompt_eval_count !== undefined && data.eval_count !== undefined\n ? { usage: { inputTokens: data.prompt_eval_count, outputTokens: data.eval_count } }\n : {}),\n };\n }\n}\n\n/**\n * The context length lives in `model_info` under an architecture-prefixed key\n * (e.g. `llama.context_length`); `general.architecture` names the prefix.\n * Falls back to any `*.context_length` key for models whose metadata omits\n * the architecture field.\n */\nfunction readContextLength(modelInfo: Record<string, unknown> | undefined): number | undefined {\n if (!modelInfo) return undefined;\n const arch = modelInfo['general.architecture'];\n if (typeof arch === 'string') {\n const direct = modelInfo[`${arch}.context_length`];\n if (isNumber(direct) && direct > 0) return direct;\n }\n const fallbackKey = Object.keys(modelInfo).find(k => k.endsWith('.context_length'));\n if (fallbackKey !== undefined) {\n const fallback = modelInfo[fallbackKey];\n if (isNumber(fallback) && fallback > 0) return fallback;\n }\n return undefined;\n}\n\nfunction mapStopReason(doneReason: string | undefined): string {\n switch (doneReason) {\n case 'stop': return 'end_turn';\n case 'length': return 'max_tokens';\n default: return doneReason || 'unknown';\n }\n}\n","// Factory for creating inference client instances based on configuration\n\nimport type { Logger } from '@semiont/core';\nimport { InferenceClient } from './interface.js';\nimport { AnthropicInferenceClient } from './implementations/anthropic.js';\nimport { OllamaInferenceClient } from './implementations/ollama.js';\n\nexport type InferenceClientType = 'anthropic' | 'ollama';\n\nexport interface InferenceClientConfig {\n type: InferenceClientType;\n apiKey?: string;\n model: string;\n endpoint?: string;\n baseURL?: string;\n}\n\nexport function createInferenceClient(config: InferenceClientConfig, logger?: Logger): InferenceClient {\n switch (config.type) {\n case 'anthropic': {\n if (!config.apiKey || config.apiKey.trim() === '') {\n throw new Error('apiKey is required for Anthropic inference client');\n }\n return new AnthropicInferenceClient(\n config.apiKey,\n config.model,\n config.endpoint || config.baseURL,\n logger\n );\n }\n\n case 'ollama': {\n return new OllamaInferenceClient(\n config.model,\n config.endpoint || config.baseURL,\n logger\n );\n }\n\n default:\n throw new Error(`Unsupported inference client type: ${config.type}`);\n }\n}\n","// Mock implementation of InferenceClient for testing\n\nimport { ElementSchema, InferenceClient, InferenceLimits, InferenceResponse, StructuredReadError, StructuredResponse } from '../interface.js';\n\n// Generous defaults so existing consumers never trip chunking or window\n// guards unless a test injects tighter limits deliberately.\nconst GENEROUS_LIMITS: InferenceLimits = {\n contextTokens: 1_000_000,\n maxOutputTokens: 1_000_000,\n // Generous RATE too: without one the mock reads as rate-silent, which since\n // OLLAMA-DETECTION-TESTING P3b/P3c opts consumers into the assumed duration\n // floor and the count-verifier. Tests exercise those by injecting\n // rate-silent limits deliberately, never by the default.\n outputTokensPerHour: 3_600_000_000,\n};\n\nexport class MockInferenceClient implements InferenceClient {\n readonly type = 'mock' as const;\n readonly modelId = 'mock-model' as const;\n // Deterministic default for tests; a test exercising concurrency sets its own.\n readonly maxConcurrency = 1;\n // Deterministic default: a count call pops the shared response queue, so\n // tests exercising the verifier declare their own true rather than every\n // consumer paying it by surprise.\n readonly verifyDetectionYield = false;\n private responses: string[] = [];\n private responseIndex: number = 0;\n private stopReasons: string[] = [];\n private injectedLimits: InferenceLimits;\n public calls: Array<{ prompt: string; maxTokens: number; temperature: number; elementSchema?: ElementSchema }> = [];\n\n constructor(responses: string[] = ['Mock response'], stopReasons?: string[], limits?: InferenceLimits) {\n this.responses = responses;\n this.stopReasons = stopReasons || responses.map(() => 'end_turn');\n this.injectedLimits = limits ?? GENEROUS_LIMITS;\n }\n\n async limits(): Promise<InferenceLimits> {\n return this.injectedLimits;\n }\n\n async generateText(prompt: string, maxTokens: number, temperature: number, signal?: AbortSignal): Promise<string> {\n const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature, signal);\n return response.text;\n }\n\n async generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number, signal?: AbortSignal): Promise<InferenceResponse> {\n throwIfAborted(signal);\n this.calls.push({ prompt, maxTokens, temperature });\n return this.nextResponse();\n }\n\n /**\n * Structured surface: pops the same responses queue and PARSES the entry,\n * mirroring the real contract — a queued string that is not a JSON array\n * throws \"could not be read\", so tests inject the malformed shape simply by\n * queuing it (`setResponses(['not json'])`). The element schema is recorded\n * on `calls` so tests can assert what the caller declared.\n */\n async generateStructured<T>(\n prompt: string,\n maxTokens: number,\n temperature: number,\n elementSchema: ElementSchema,\n signal?: AbortSignal,\n ): Promise<StructuredResponse<T>> {\n throwIfAborted(signal);\n this.calls.push({ prompt, maxTokens, temperature, elementSchema });\n const { text, stopReason } = this.nextResponse();\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch (err) {\n throw new StructuredReadError('response is not valid JSON', stopReason, { cause: err });\n }\n if (!Array.isArray(parsed)) {\n throw new StructuredReadError(`parsed to ${typeof parsed}, not an array`, stopReason);\n }\n return { items: parsed as T[], stopReason };\n }\n\n private nextResponse(): InferenceResponse {\n const text = this.responses[this.responseIndex];\n const stopReason = this.stopReasons[this.responseIndex] || 'end_turn';\n\n if (this.responseIndex < this.responses.length - 1) {\n this.responseIndex++;\n }\n\n return { text, stopReason };\n }\n\n // Test helper methods\n reset(): void {\n this.calls = [];\n this.responseIndex = 0;\n }\n\n setResponses(responses: string[], stopReasons?: string[]): void {\n this.responses = responses;\n this.stopReasons = stopReasons || responses.map(() => 'end_turn');\n this.responseIndex = 0;\n }\n}\n\n/**\n * The mock honors the signal like a real adapter (ABANDONED-INFERENCE P1's\n * accept-and-drop trap: an adapter that takes the parameter and ignores it\n * lets cancellation tests pass while proving nothing). The mock resolves\n * synchronously, so an entry check is the whole contract — rejecting the way\n * an aborted `fetch` does, with an `AbortError`-named DOMException.\n */\nfunction throwIfAborted(signal?: AbortSignal): void {\n if (signal?.aborted) {\n throw new DOMException('This operation was aborted', 'AbortError');\n }\n}\n"],"mappings":"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","names":["isObject","recordInferenceUsage","isObject","recordInferenceUsage","body"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@semiont/inference",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.31",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=24.0.0"
|
|
6
6
|
},
|
|
@@ -27,9 +27,10 @@
|
|
|
27
27
|
"test:coverage": "vitest run --coverage"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@anthropic-ai/sdk": "^0.
|
|
31
|
-
"@semiont/core": "0.5.
|
|
32
|
-
"@semiont/observability": "0.5.
|
|
30
|
+
"@anthropic-ai/sdk": "^0.123.0",
|
|
31
|
+
"@semiont/core": "0.5.31",
|
|
32
|
+
"@semiont/observability": "0.5.31",
|
|
33
|
+
"undici": "^7.29.0"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
35
36
|
"@vitest/coverage-v8": "^4.1.11",
|