@semiont/inference 0.5.29 → 0.5.30

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 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, options?): Promise<string>;
95
- generateTextWithMetadata(prompt, maxTokens, temperature, options?): Promise<InferenceResponse>;
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; // context window (Anthropic: max input; Ollama: shared input+output)
100
- maxOutputTokens: number; // max output per generation (Ollama mirrors the shared window here)
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 — the SDK delivering unparsed tool input as a string, a missing array, an unhonoured grammar — **throws** (`Structured response could not be read: …`). It is never coerced to `[]`: an empty extraction is a legitimate, distinct outcome, and conflating the two silently discards real data (STRUCTURED-INFERENCE).
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 nests it under the forced tool's
11
- * `input_schema`; Ollama sends it as `format: { type: 'array', items: … }`),
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
- var NONSTREAMING_MAX_OUTPUT_TOKENS = Math.floor(128e3 / 6);
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: { contextTokens: info.max_input_tokens, maxOutputTokens: info.max_tokens },
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 Error(
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 Error(
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 Error(
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 Error(
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", { model: this.modelId });
366
- throw new Error("Empty response from Ollama");
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 Error(
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 Error(
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.29",
3
+ "version": "0.5.30",
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.120.0",
31
- "@semiont/core": "0.5.29",
32
- "@semiont/observability": "0.5.29"
30
+ "@anthropic-ai/sdk": "^0.122.0",
31
+ "@semiont/core": "0.5.30",
32
+ "@semiont/observability": "0.5.30",
33
+ "undici": "^7.29.0"
33
34
  },
34
35
  "devDependencies": {
35
36
  "@vitest/coverage-v8": "^4.1.11",