@semiont/inference 0.5.25 → 0.5.27

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
@@ -12,7 +12,7 @@ This package provides the **core AI primitives** for the Semiont platform:
12
12
  - The `InferenceClient` interface (provider abstraction)
13
13
  - Client implementations for Anthropic and Ollama, plus a scripted mock for tests
14
14
  - A `createInferenceClient()` factory that selects the implementation from config
15
- - Cross-provider JSON output mode (`format: 'json'`)
15
+ - Cross-provider structured generation (`generateStructured` — parsed elements or a throw, never a silent `[]`)
16
16
  - Usage metrics via `@semiont/observability`
17
17
 
18
18
  For **application-specific AI logic** (semantic processing, prompt engineering, response parsing), see [@semiont/make-meaning](../make-meaning/).
@@ -106,20 +106,26 @@ interface InferenceResponse {
106
106
  }
107
107
  ```
108
108
 
109
- ### JSON output mode
109
+ ### Structured generation
110
110
 
111
- Pass `{ format: 'json' }` as `options` to constrain output to a **parseable top-level JSON array**, regardless of provider:
111
+ `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:
112
112
 
113
113
  ```typescript
114
- const json = await client.generateText(prompt, 1000, 0, { format: 'json' });
115
- const items = JSON.parse(json); // guaranteed to be an array
114
+ const { items } = await client.generateStructured<Entity>(prompt, 1000, 0, {
115
+ type: 'object',
116
+ properties: { exact: { type: 'string' } },
117
+ required: ['exact'],
118
+ additionalProperties: false,
119
+ });
116
120
  ```
117
121
 
118
122
  Each implementation honors the contract with its provider's mechanism:
119
- - **Ollama**: grammar-constrained sampling — the request's `format` field carries a minimal array schema.
120
- - **Anthropic**: forced structured tool-usea single tool is offered and forced via `tool_choice`, so the model answers by filling the tool's input, which the API serializes as escaped JSON. The array is carried under an `items` property (tool inputs must be objects) and unwrapped to a top-level array on return.
123
+ - **Ollama**: grammar-constrained sampling — the request's `format` field carries the caller's element schema wrapped in an array schema.
124
+ - **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
+
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).
121
127
 
122
- Current callers all expect arrays (entity extraction, motivation detection). If an object-emitting caller appears, the option grows a `root: 'array' | 'object'` field — see the notes in [src/interface.ts](src/interface.ts).
128
+ 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).
123
129
 
124
130
  ### Provider limits
125
131
 
package/dist/index.d.ts CHANGED
@@ -5,27 +5,36 @@ interface InferenceResponse {
5
5
  stopReason: 'end_turn' | 'max_tokens' | 'stop_sequence' | string;
6
6
  }
7
7
  /**
8
- * Per-call options. Drift here when output discipline matters more than
9
- * raw model behavior e.g. forcing valid JSON for entity extraction
10
- * where a parse failure in the consumer is silent and useless.
8
+ * Raw JSON Schema for ONE array element of a structured generation — a plain
9
+ * 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: … }`),
12
+ * so anything richer would abstract one shape with two consumers.
13
+ *
14
+ * Constrain it to what both providers enforce: objects,
15
+ * `string`/`number`/`boolean`/`null`, `enum`, `const`, `required`, and
16
+ * `additionalProperties: false`. Numeric and string constraints (`minimum`,
17
+ * `maxLength`) are NOT enforced by Anthropic strict mode — declaring them
18
+ * buys nothing and misleads the reader.
11
19
  */
12
- interface InferenceOptions {
13
- /**
14
- * Constrain output to a parseable JSON array. Every implementation
15
- * MUST satisfy this contract using whatever mechanism its provider
16
- * supports Ollama uses grammar-constrained sampling
17
- * (`format: "json"`); Anthropic uses forced structured tool-use (a
18
- * single tool, forced via `tool_choice`, whose array result the API
19
- * serializes as escaped JSON) and unwraps the array on return.
20
- * Callers can rely on the returned `text` being a top-level JSON
21
- * array regardless of provider.
22
- *
23
- * Current callers all expect arrays (entity extraction, motivation
24
- * detection). If an object-emitting caller appears, this option
25
- * grows a `root: 'array' | 'object'` field; do not silently drop
26
- * the constraint.
27
- */
28
- format?: 'json';
20
+ type ElementSchema = Record<string, unknown>;
21
+ /**
22
+ * A structured generation's result: the elements the model produced, plus the
23
+ * provider's stop reason (consumers gate on 'max_tokens' truncation is data
24
+ * loss, not "fewer items").
25
+ *
26
+ * `items` is `T[]`, never a string: there is no representable value meaning
27
+ * "here is some text I could not read." An implementation that cannot deliver
28
+ * the array THROWS failure is distinct from empty by construction.
29
+ *
30
+ * `T` is a caller assertion, not a runtime guarantee: nothing verifies the
31
+ * element schema and `T` agree, and the type parameter is erased. Declare the
32
+ * schema and `T` adjacently at the call site so drift is visible in one
33
+ * place, and keep per-element structural guards on the consuming side.
34
+ */
35
+ interface StructuredResponse<T> {
36
+ items: T[];
37
+ stopReason: 'end_turn' | 'max_tokens' | 'stop_sequence' | string;
29
38
  }
30
39
  /**
31
40
  * A provider's actual ceilings for the configured model, discovered from the
@@ -60,11 +69,25 @@ interface InferenceClient {
60
69
  /**
61
70
  * Generate text from a prompt (simple interface)
62
71
  */
63
- generateText(prompt: string, maxTokens: number, temperature: number, options?: InferenceOptions): Promise<string>;
72
+ generateText(prompt: string, maxTokens: number, temperature: number): Promise<string>;
64
73
  /**
65
74
  * Generate text with detailed response information
66
75
  */
67
- generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number, options?: InferenceOptions): Promise<InferenceResponse>;
76
+ generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number): Promise<InferenceResponse>;
77
+ /**
78
+ * Generate a JSON array whose elements satisfy `elementSchema`, as parsed
79
+ * values — the structured counterpart of `generateTextWithMetadata`, and
80
+ * the ONLY generation surface detection may use.
81
+ *
82
+ * The return type carries the guarantee the old `format: 'json'` option
83
+ * left in a comment: callers receive `T[]` or an exception. When the
84
+ * provider's answer cannot be read as an array (the SDK hands tool input
85
+ * over as an unparsed string, the response is missing the array, the
86
+ * grammar was not honoured), implementations THROW a
87
+ * "Structured response could not be read" error — they never coerce to
88
+ * `[]`, because empty is a legitimate, distinct outcome.
89
+ */
90
+ generateStructured<T>(prompt: string, maxTokens: number, temperature: number, elementSchema: ElementSchema): Promise<StructuredResponse<T>>;
68
91
  }
69
92
 
70
93
  type InferenceClientType = 'anthropic' | 'ollama';
@@ -82,13 +105,18 @@ declare class AnthropicInferenceClient implements InferenceClient {
82
105
  readonly modelId: string;
83
106
  private client;
84
107
  private logger?;
85
- private limitsPromise?;
108
+ private discoveryPromise?;
86
109
  constructor(apiKey: string, model: string, baseURL?: string, logger?: Logger);
87
110
  limits(): Promise<InferenceLimits>;
88
- private discoverLimits;
111
+ private discover;
112
+ private discoverModel;
89
113
  private requestMessage;
90
- generateText(prompt: string, maxTokens: number, temperature: number, options?: InferenceOptions): Promise<string>;
91
- generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number, options?: InferenceOptions): Promise<InferenceResponse>;
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>>;
117
+ /** Issue the request, recording an error metric if the transport throws. */
118
+ private recordedRequest;
119
+ private recordError;
92
120
  }
93
121
 
94
122
  declare class OllamaInferenceClient implements InferenceClient {
@@ -100,8 +128,10 @@ declare class OllamaInferenceClient implements InferenceClient {
100
128
  constructor(model: string, baseURL?: string, logger?: Logger);
101
129
  limits(): Promise<InferenceLimits>;
102
130
  private discoverLimits;
103
- generateText(prompt: string, maxTokens: number, temperature: number, options?: InferenceOptions): Promise<string>;
104
- generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number, options?: InferenceOptions): Promise<InferenceResponse>;
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>>;
134
+ private generate;
105
135
  }
106
136
 
107
137
  declare class MockInferenceClient implements InferenceClient {
@@ -115,15 +145,24 @@ declare class MockInferenceClient implements InferenceClient {
115
145
  prompt: string;
116
146
  maxTokens: number;
117
147
  temperature: number;
118
- options?: InferenceOptions;
148
+ elementSchema?: ElementSchema;
119
149
  }>;
120
150
  constructor(responses?: string[], stopReasons?: string[], limits?: InferenceLimits);
121
151
  limits(): Promise<InferenceLimits>;
122
- generateText(prompt: string, maxTokens: number, temperature: number, options?: InferenceOptions): Promise<string>;
123
- generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number, options?: InferenceOptions): Promise<InferenceResponse>;
152
+ generateText(prompt: string, maxTokens: number, temperature: number): Promise<string>;
153
+ generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number): Promise<InferenceResponse>;
154
+ /**
155
+ * Structured surface: pops the same responses queue and PARSES the entry,
156
+ * mirroring the real contract — a queued string that is not a JSON array
157
+ * throws "could not be read", so tests inject the malformed shape simply by
158
+ * queuing it (`setResponses(['not json'])`). The element schema is recorded
159
+ * on `calls` so tests can assert what the caller declared.
160
+ */
161
+ generateStructured<T>(prompt: string, maxTokens: number, temperature: number, elementSchema: ElementSchema): Promise<StructuredResponse<T>>;
162
+ private nextResponse;
124
163
  reset(): void;
125
164
  setResponses(responses: string[], stopReasons?: string[]): void;
126
165
  }
127
166
 
128
167
  export { AnthropicInferenceClient, MockInferenceClient, OllamaInferenceClient, createInferenceClient };
129
- export type { InferenceClient, InferenceClientConfig, InferenceClientType, InferenceLimits, InferenceOptions, InferenceResponse };
168
+ export type { ElementSchema, InferenceClient, InferenceClientConfig, InferenceClientType, InferenceLimits, InferenceResponse, StructuredResponse };
package/dist/index.js CHANGED
@@ -1,26 +1,14 @@
1
1
  // src/implementations/anthropic.ts
2
2
  import Anthropic from "@anthropic-ai/sdk";
3
+ import { isObject } from "@semiont/core";
3
4
  import { recordInferenceUsage } from "@semiont/observability";
4
5
  var NONSTREAMING_MAX_OUTPUT_TOKENS = Math.floor(128e3 / 6);
5
- var JSON_ARRAY_TOOL = {
6
- name: "emit_json_array",
7
- description: 'Return your entire answer by calling this tool. Put the JSON array of results under the "items" property, and emit no prose.',
8
- input_schema: {
9
- type: "object",
10
- properties: {
11
- // Element shape is unconstrained here — the prompt carries the per-element
12
- // schema; the tool only enforces that the top-level result is an array.
13
- items: { type: "array", items: {} }
14
- },
15
- required: ["items"]
16
- }
17
- };
18
6
  var AnthropicInferenceClient = class {
19
7
  type = "anthropic";
20
8
  modelId;
21
9
  client;
22
10
  logger;
23
- limitsPromise;
11
+ discoveryPromise;
24
12
  constructor(apiKey, model, baseURL, logger) {
25
13
  this.client = new Anthropic({
26
14
  apiKey,
@@ -30,15 +18,18 @@ var AnthropicInferenceClient = class {
30
18
  this.logger = logger;
31
19
  }
32
20
  limits() {
33
- if (!this.limitsPromise) {
34
- this.limitsPromise = this.discoverLimits().catch((err) => {
35
- this.limitsPromise = void 0;
21
+ return this.discover().then((d) => d.limits);
22
+ }
23
+ discover() {
24
+ if (!this.discoveryPromise) {
25
+ this.discoveryPromise = this.discoverModel().catch((err) => {
26
+ this.discoveryPromise = void 0;
36
27
  throw err;
37
28
  });
38
29
  }
39
- return this.limitsPromise;
30
+ return this.discoveryPromise;
40
31
  }
41
- async discoverLimits() {
32
+ async discoverModel() {
42
33
  const info = await this.client.models.retrieve(this.modelId).catch((err) => {
43
34
  throw new Error(
44
35
  `Failed to discover model limits for '${this.modelId}' from the Models API`,
@@ -48,7 +39,12 @@ var AnthropicInferenceClient = class {
48
39
  if (info.max_input_tokens == null || info.max_tokens == null) {
49
40
  throw new Error(`Models API reports no context/output ceilings for '${this.modelId}'`);
50
41
  }
51
- return { contextTokens: info.max_input_tokens, maxOutputTokens: info.max_tokens };
42
+ const raw = info;
43
+ const structuredOutputsSupported = isObject(raw) && isObject(raw["capabilities"]) && isObject(raw["capabilities"]["structured_outputs"]) && raw["capabilities"]["structured_outputs"]["supported"] === true;
44
+ return {
45
+ limits: { contextTokens: info.max_input_tokens, maxOutputTokens: info.max_tokens },
46
+ structuredOutputsSupported
47
+ };
52
48
  }
53
49
  requestMessage(params) {
54
50
  if (params.max_tokens > NONSTREAMING_MAX_OUTPUT_TOKENS) {
@@ -56,85 +52,116 @@ var AnthropicInferenceClient = class {
56
52
  }
57
53
  return this.client.messages.create(params);
58
54
  }
59
- async generateText(prompt, maxTokens, temperature, options) {
60
- const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature, options);
55
+ async generateText(prompt, maxTokens, temperature) {
56
+ const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature);
61
57
  return response.text;
62
58
  }
63
- async generateTextWithMetadata(prompt, maxTokens, temperature, options) {
64
- const jsonMode = options?.format === "json";
59
+ async generateTextWithMetadata(prompt, maxTokens, temperature) {
65
60
  this.logger?.debug("Generating text with inference client", {
66
61
  model: this.modelId,
67
62
  promptLength: prompt.length,
68
63
  maxTokens,
69
- temperature,
70
- format: options?.format
64
+ temperature
71
65
  });
72
66
  const params = {
73
67
  model: this.modelId,
74
68
  max_tokens: maxTokens,
75
69
  temperature,
76
- messages: [{ role: "user", content: prompt }],
77
- // JSON mode → force the structured-output tool. No prefill assistant
78
- // turn: the constraint now lives in the tool call, not in free text.
79
- ...jsonMode ? { tools: [JSON_ARRAY_TOOL], tool_choice: { type: "tool", name: JSON_ARRAY_TOOL.name } } : {}
70
+ messages: [{ role: "user", content: prompt }]
80
71
  };
81
72
  const start = performance.now();
82
- let response;
83
- try {
84
- response = await this.requestMessage(params);
85
- } catch (err) {
86
- recordInferenceUsage({
87
- provider: this.type,
73
+ const response = await this.recordedRequest(params, start);
74
+ const textContent = response.content.find((c) => c.type === "text");
75
+ if (!textContent || textContent.type !== "text") {
76
+ this.recordError(start, response);
77
+ this.logger?.error("No text content in inference response", {
88
78
  model: this.modelId,
89
- durationMs: performance.now() - start,
90
- outcome: "error"
79
+ contentTypes: response.content.map((c) => c.type)
91
80
  });
92
- throw err;
81
+ throw new Error("No text content in inference response");
93
82
  }
94
- this.logger?.debug("Inference response received", {
83
+ const text = textContent.text;
84
+ recordInferenceUsage({
85
+ provider: this.type,
86
+ model: this.modelId,
87
+ durationMs: performance.now() - start,
88
+ outcome: "success",
89
+ inputTokens: response.usage?.input_tokens,
90
+ outputTokens: response.usage?.output_tokens
91
+ });
92
+ this.logger?.info("Text generation completed", {
95
93
  model: this.modelId,
96
- contentBlocks: response.content.length,
94
+ textLength: text.length,
97
95
  stopReason: response.stop_reason
98
96
  });
99
- let text;
100
- if (jsonMode) {
101
- const toolUse = response.content.find((c) => c.type === "tool_use");
102
- if (!toolUse || toolUse.type !== "tool_use") {
103
- recordInferenceUsage({
104
- provider: this.type,
105
- model: this.modelId,
106
- durationMs: performance.now() - start,
107
- outcome: "error",
108
- inputTokens: response.usage?.input_tokens,
109
- outputTokens: response.usage?.output_tokens
110
- });
111
- this.logger?.error("No tool_use content in inference response", {
112
- model: this.modelId,
113
- contentTypes: response.content.map((c) => c.type)
114
- });
115
- throw new Error("No tool_use content in inference response");
116
- }
117
- const input = toolUse.input;
118
- const items = Array.isArray(input.items) ? input.items : [];
119
- text = JSON.stringify(items);
120
- } else {
121
- const textContent = response.content.find((c) => c.type === "text");
122
- if (!textContent || textContent.type !== "text") {
123
- recordInferenceUsage({
124
- provider: this.type,
125
- model: this.modelId,
126
- durationMs: performance.now() - start,
127
- outcome: "error",
128
- inputTokens: response.usage?.input_tokens,
129
- outputTokens: response.usage?.output_tokens
130
- });
131
- this.logger?.error("No text content in inference response", {
132
- model: this.modelId,
133
- contentTypes: response.content.map((c) => c.type)
134
- });
135
- throw new Error("No text content in inference response");
97
+ return {
98
+ text,
99
+ stopReason: response.stop_reason || "unknown"
100
+ };
101
+ }
102
+ async generateStructured(prompt, maxTokens, temperature, elementSchema) {
103
+ const discovery = await this.discover();
104
+ if (!discovery.structuredOutputsSupported) {
105
+ throw new Error(
106
+ `Model '${this.modelId}' does not report support for strict structured outputs (Models API capabilities.structured_outputs) \u2014 refusing rather than degrading to unconstrained tool use, which silently discards unreadable results. Re-point the inference.model key that pins this worker/actor in .semiont/semiontconfig/*.toml (e.g. environments.<env>.workers.<job-type>.inference.model) at a model that reports supported: true.`
107
+ );
108
+ }
109
+ this.logger?.debug("Generating structured output with inference client", {
110
+ model: this.modelId,
111
+ promptLength: prompt.length,
112
+ maxTokens,
113
+ temperature
114
+ });
115
+ const params = {
116
+ model: this.modelId,
117
+ max_tokens: maxTokens,
118
+ temperature,
119
+ messages: [{ role: "user", content: prompt }],
120
+ // Response-level structured output with an ARRAY root: the response
121
+ // text IS the schema-conforming JSON. No tools, no prefill.
122
+ output_config: {
123
+ format: {
124
+ type: "json_schema",
125
+ schema: { type: "array", items: elementSchema }
126
+ }
136
127
  }
137
- text = textContent.text;
128
+ };
129
+ const start = performance.now();
130
+ const response = await this.recordedRequest(params, start);
131
+ const textContent = response.content.find((c) => c.type === "text");
132
+ if (!textContent || textContent.type !== "text") {
133
+ this.recordError(start, response);
134
+ this.logger?.error("No text content in structured inference response", {
135
+ model: this.modelId,
136
+ contentTypes: response.content.map((c) => c.type)
137
+ });
138
+ throw new Error("No text content in structured inference response");
139
+ }
140
+ let parsed;
141
+ try {
142
+ parsed = JSON.parse(textContent.text);
143
+ } catch (err) {
144
+ this.recordError(start, response);
145
+ this.logger?.error("Structured response could not be read", {
146
+ model: this.modelId,
147
+ textLength: textContent.text.length,
148
+ stopReason: response.stop_reason
149
+ });
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
+ );
154
+ }
155
+ if (!Array.isArray(parsed)) {
156
+ this.recordError(start, response);
157
+ this.logger?.error("Structured response could not be read", {
158
+ model: this.modelId,
159
+ parsedType: typeof parsed,
160
+ stopReason: response.stop_reason
161
+ });
162
+ throw new Error(
163
+ `Structured response could not be read: parsed to ${typeof parsed}, not an array (stop_reason: ${response.stop_reason})`
164
+ );
138
165
  }
139
166
  recordInferenceUsage({
140
167
  provider: this.type,
@@ -144,20 +171,44 @@ var AnthropicInferenceClient = class {
144
171
  inputTokens: response.usage?.input_tokens,
145
172
  outputTokens: response.usage?.output_tokens
146
173
  });
147
- this.logger?.info("Text generation completed", {
174
+ this.logger?.info("Structured generation completed", {
148
175
  model: this.modelId,
149
- textLength: text.length,
176
+ items: parsed.length,
150
177
  stopReason: response.stop_reason
151
178
  });
152
179
  return {
153
- text,
180
+ items: parsed,
154
181
  stopReason: response.stop_reason || "unknown"
155
182
  };
156
183
  }
184
+ /** Issue the request, recording an error metric if the transport throws. */
185
+ async recordedRequest(params, start) {
186
+ try {
187
+ return await this.requestMessage(params);
188
+ } catch (err) {
189
+ recordInferenceUsage({
190
+ provider: this.type,
191
+ model: this.modelId,
192
+ durationMs: performance.now() - start,
193
+ outcome: "error"
194
+ });
195
+ throw err;
196
+ }
197
+ }
198
+ recordError(start, response) {
199
+ recordInferenceUsage({
200
+ provider: this.type,
201
+ model: this.modelId,
202
+ durationMs: performance.now() - start,
203
+ outcome: "error",
204
+ inputTokens: response.usage?.input_tokens,
205
+ outputTokens: response.usage?.output_tokens
206
+ });
207
+ }
157
208
  };
158
209
 
159
210
  // src/implementations/ollama.ts
160
- import { estimateTokens, isNumber, isObject } from "@semiont/core";
211
+ import { estimateTokens, isNumber, isObject as isObject2 } from "@semiont/core";
161
212
  import { recordInferenceUsage as recordInferenceUsage2 } from "@semiont/observability";
162
213
  var NUM_CTX_ESTIMATE_SLACK = 0.2;
163
214
  var NUM_CTX_TEMPLATE_ALLOWANCE = 64;
@@ -193,24 +244,55 @@ var OllamaInferenceClient = class {
193
244
  );
194
245
  }
195
246
  const data = await res.json();
196
- const modelInfo = isObject(data) && isObject(data["model_info"]) ? data["model_info"] : void 0;
247
+ const modelInfo = isObject2(data) && isObject2(data["model_info"]) ? data["model_info"] : void 0;
197
248
  const contextTokens = readContextLength(modelInfo);
198
249
  if (contextTokens === void 0) {
199
250
  throw new Error(`/api/show reports no context length for '${this.modelId}'`);
200
251
  }
201
252
  return { contextTokens, maxOutputTokens: contextTokens };
202
253
  }
203
- async generateText(prompt, maxTokens, temperature, options) {
204
- const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature, options);
254
+ async generateText(prompt, maxTokens, temperature) {
255
+ const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature);
205
256
  return response.text;
206
257
  }
207
- async generateTextWithMetadata(prompt, maxTokens, temperature, options) {
258
+ async generateTextWithMetadata(prompt, maxTokens, temperature) {
259
+ return this.generate(prompt, maxTokens, temperature, void 0);
260
+ }
261
+ async generateStructured(prompt, maxTokens, temperature, elementSchema) {
262
+ const response = await this.generate(prompt, maxTokens, temperature, elementSchema);
263
+ let parsed;
264
+ try {
265
+ parsed = JSON.parse(response.text);
266
+ } catch (err) {
267
+ this.logger?.error("Structured response could not be read", {
268
+ model: this.modelId,
269
+ textLength: response.text.length,
270
+ stopReason: response.stopReason
271
+ });
272
+ throw new Error(
273
+ `Structured response could not be read: response is not valid JSON (stop_reason: ${response.stopReason})`,
274
+ { cause: err }
275
+ );
276
+ }
277
+ if (!Array.isArray(parsed)) {
278
+ this.logger?.error("Structured response could not be read", {
279
+ model: this.modelId,
280
+ parsedType: typeof parsed,
281
+ stopReason: response.stopReason
282
+ });
283
+ throw new Error(
284
+ `Structured response could not be read: parsed to ${typeof parsed}, not an array (stop_reason: ${response.stopReason})`
285
+ );
286
+ }
287
+ return { items: parsed, stopReason: response.stopReason };
288
+ }
289
+ async generate(prompt, maxTokens, temperature, elementSchema) {
208
290
  this.logger?.debug("Generating text with Ollama", {
209
291
  model: this.modelId,
210
292
  promptLength: prompt.length,
211
293
  maxTokens,
212
294
  temperature,
213
- format: options?.format
295
+ structured: elementSchema !== void 0
214
296
  });
215
297
  const limits = await this.limits();
216
298
  const promptTokens = estimateTokens(prompt);
@@ -236,8 +318,8 @@ var OllamaInferenceClient = class {
236
318
  temperature
237
319
  }
238
320
  };
239
- if (options?.format === "json") {
240
- body["format"] = { type: "array", items: {} };
321
+ if (elementSchema !== void 0) {
322
+ body["format"] = { type: "array", items: elementSchema };
241
323
  }
242
324
  let res;
243
325
  try {
@@ -375,12 +457,41 @@ var MockInferenceClient = class {
375
457
  async limits() {
376
458
  return this.injectedLimits;
377
459
  }
378
- async generateText(prompt, maxTokens, temperature, options) {
379
- const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature, options);
460
+ async generateText(prompt, maxTokens, temperature) {
461
+ const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature);
380
462
  return response.text;
381
463
  }
382
- async generateTextWithMetadata(prompt, maxTokens, temperature, options) {
383
- this.calls.push({ prompt, maxTokens, temperature, ...options ? { options } : {} });
464
+ async generateTextWithMetadata(prompt, maxTokens, temperature) {
465
+ this.calls.push({ prompt, maxTokens, temperature });
466
+ return this.nextResponse();
467
+ }
468
+ /**
469
+ * Structured surface: pops the same responses queue and PARSES the entry,
470
+ * mirroring the real contract — a queued string that is not a JSON array
471
+ * throws "could not be read", so tests inject the malformed shape simply by
472
+ * queuing it (`setResponses(['not json'])`). The element schema is recorded
473
+ * on `calls` so tests can assert what the caller declared.
474
+ */
475
+ async generateStructured(prompt, maxTokens, temperature, elementSchema) {
476
+ this.calls.push({ prompt, maxTokens, temperature, elementSchema });
477
+ const { text, stopReason } = this.nextResponse();
478
+ let parsed;
479
+ try {
480
+ parsed = JSON.parse(text);
481
+ } 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
+ );
486
+ }
487
+ 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
+ );
491
+ }
492
+ return { items: parsed, stopReason };
493
+ }
494
+ nextResponse() {
384
495
  const text = this.responses[this.responseIndex];
385
496
  const stopReason = this.stopReasons[this.responseIndex] || "end_turn";
386
497
  if (this.responseIndex < this.responses.length - 1) {
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 type { Logger } from '@semiont/core';\nimport { recordInferenceUsage } from '@semiont/observability';\nimport { InferenceClient, InferenceLimits, InferenceOptions, InferenceResponse } 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// Forced-tool channel for JSON mode. Anthropic has no grammar-constrained\n// sampling like Ollama's `format`; the equivalent hard guarantee is a *tool\n// call*. We offer exactly one tool and force it via `tool_choice`, so the model\n// must answer by filling the tool's input — which the API serializes as\n// properly-escaped JSON. That kills both free-text failure modes at the source:\n// trailing prose after the `]` (variant 1) and an unescaped `\"` inside a string\n// (variant 2), neither of which a prefill could prevent.\n//\n// A tool's input must be an *object*, so the array is carried under `items`\n// and unwrapped on return (see generateTextWithMetadata) — the caller still\n// receives a top-level JSON array in `text`, exactly as on Ollama.\nconst JSON_ARRAY_TOOL: Anthropic.Tool = {\n name: 'emit_json_array',\n description:\n 'Return your entire answer by calling this tool. Put the JSON array of results under the \"items\" property, and emit no prose.',\n input_schema: {\n type: 'object',\n properties: {\n // Element shape is unconstrained here — the prompt carries the per-element\n // schema; the tool only enforces that the top-level result is an array.\n items: { type: 'array', items: {} },\n },\n required: ['items'],\n },\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 limitsPromise?: Promise<InferenceLimits>;\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 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 // The Models API publishes the actual ceilings per model — no\n // hand-maintained table to go stale when a new model ships.\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 return { contextTokens: info.max_input_tokens, maxOutputTokens: info.max_tokens };\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, options?: InferenceOptions): Promise<string> {\n const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature, options);\n return response.text;\n }\n\n async generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number, options?: InferenceOptions): Promise<InferenceResponse> {\n const jsonMode = options?.format === 'json';\n\n this.logger?.debug('Generating text with inference client', {\n model: this.modelId,\n promptLength: prompt.length,\n maxTokens,\n temperature,\n format: options?.format,\n });\n\n const params: Anthropic.MessageCreateParamsNonStreaming = {\n model: this.modelId,\n max_tokens: maxTokens,\n temperature,\n messages: [{ role: 'user', content: prompt }],\n // JSON mode → force the structured-output tool. No prefill assistant\n // turn: the constraint now lives in the tool call, not in free text.\n ...(jsonMode\n ? { tools: [JSON_ARRAY_TOOL], tool_choice: { type: 'tool' as const, name: JSON_ARRAY_TOOL.name } }\n : {}),\n };\n\n const start = performance.now();\n let response: Anthropic.Message;\n try {\n response = 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 this.logger?.debug('Inference response received', {\n model: this.modelId,\n contentBlocks: response.content.length,\n stopReason: response.stop_reason\n });\n\n let text: string;\n if (jsonMode) {\n // The answer arrives as a tool_use block, not text. Unwrap the `items`\n // array and re-serialize it so `text` is a complete, parseable top-level\n // JSON array — the cross-provider contract every consumer reads.\n const toolUse = response.content.find(c => c.type === 'tool_use');\n if (!toolUse || toolUse.type !== 'tool_use') {\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 this.logger?.error('No tool_use content in inference response', {\n model: this.modelId,\n contentTypes: response.content.map(c => c.type)\n });\n throw new Error('No tool_use content in inference response');\n }\n // `input` is typed `unknown` by the SDK. A truncated (`max_tokens`)\n // response may carry partial or absent `items` — fall back to the partial\n // array, or `[]` if absent; the consumer flags truncation via stopReason.\n const input = toolUse.input as { items?: unknown };\n const items = Array.isArray(input.items) ? input.items : [];\n text = JSON.stringify(items);\n } else {\n const textContent = response.content.find(c => c.type === 'text');\n if (!textContent || textContent.type !== 'text') {\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 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 text = textContent.text;\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('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","// 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 { InferenceClient, InferenceLimits, InferenceOptions, InferenceResponse } 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, options?: InferenceOptions): Promise<string> {\n const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature, options);\n return response.text;\n }\n\n async generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number, options?: InferenceOptions): Promise<InferenceResponse> {\n this.logger?.debug('Generating text with Ollama', {\n model: this.modelId,\n promptLength: prompt.length,\n maxTokens,\n temperature,\n format: options?.format,\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 contract on the\n // inference side is \"parseable JSON array,\" so we pass a minimal\n // array schema rather than the bare `\"json\"` string — without it,\n // the model can satisfy \"valid JSON\" by emitting `{\"entities\": [...]}`\n // and break every consumer that expects to call `.map` on the\n // top-level value. The schema's `items: {}` keeps element shape\n // unconstrained — the prompt still carries the per-element schema;\n // we only enforce the outer array.\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 (options?.format === 'json') {\n body['format'] = { type: 'array', items: {} };\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 { InferenceClient, InferenceLimits, InferenceOptions, InferenceResponse } 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; options?: InferenceOptions }> = [];\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, options?: InferenceOptions): Promise<string> {\n const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature, options);\n return response.text;\n }\n\n async generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number, options?: InferenceOptions): Promise<InferenceResponse> {\n this.calls.push({ prompt, maxTokens, temperature, ...(options ? { options } : {}) });\n\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":["recordInferenceUsage","body"]}
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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@semiont/inference",
3
- "version": "0.5.25",
3
+ "version": "0.5.27",
4
4
  "engines": {
5
5
  "node": ">=24.0.0"
6
6
  },
@@ -28,8 +28,8 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sdk": "^0.115.0",
31
- "@semiont/core": "0.5.25",
32
- "@semiont/observability": "0.5.25"
31
+ "@semiont/core": "0.5.27",
32
+ "@semiont/observability": "0.5.27"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@vitest/coverage-v8": "^4.1.8",