@semiont/inference 0.5.23 → 0.5.25

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,10 +90,16 @@ interface InferenceClient {
90
90
  readonly type: string; // 'anthropic' | 'ollama' | 'mock'
91
91
  readonly modelId: string; // configured model name
92
92
 
93
+ limits(): Promise<InferenceLimits>;
93
94
  generateText(prompt, maxTokens, temperature, options?): Promise<string>;
94
95
  generateTextWithMetadata(prompt, maxTokens, temperature, options?): Promise<InferenceResponse>;
95
96
  }
96
97
 
98
+ 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)
101
+ }
102
+
97
103
  interface InferenceResponse {
98
104
  text: string;
99
105
  stopReason: 'end_turn' | 'max_tokens' | 'stop_sequence' | string;
@@ -115,9 +121,22 @@ Each implementation honors the contract with its provider's mechanism:
115
121
 
116
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).
117
123
 
124
+ ### Provider limits
125
+
126
+ `limits()` publishes the provider's **actual** context/output ceilings for the configured model — discovered from the provider itself, never hand-maintained constants:
127
+
128
+ - **Anthropic**: the Models API (`models.retrieve`) — `max_input_tokens` / `max_tokens`.
129
+ - **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).
130
+
131
+ 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.
132
+
133
+ Two request-time behaviors ride on the limits:
134
+ - **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.
135
+ - **Anthropic streams internally** above the SDK's non-streaming output ceiling (≈21K tokens) — same interface, same response shape.
136
+
118
137
  ### `MockInferenceClient`
119
138
 
120
- 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.
139
+ 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.
121
140
 
122
141
  ```typescript
123
142
  import { MockInferenceClient } from '@semiont/inference';
package/dist/index.d.ts CHANGED
@@ -27,11 +27,36 @@ interface InferenceOptions {
27
27
  */
28
28
  format?: 'json';
29
29
  }
30
+ /**
31
+ * A provider's actual ceilings for the configured model, discovered from the
32
+ * provider itself (Anthropic Models API; Ollama `/api/show`) — never
33
+ * hand-maintained constants. Detection budget arithmetic derives from these.
34
+ */
35
+ interface InferenceLimits {
36
+ /**
37
+ * The context window in tokens. Semantics differ by provider shape:
38
+ * Anthropic reports maximum *input* tokens (output has its own ceiling);
39
+ * Ollama reports the *shared* input+output window and mirrors it in
40
+ * `maxOutputTokens` (there is no separate output ceiling), so
41
+ * `maxOutputTokens === contextTokens` signals a shared window.
42
+ */
43
+ contextTokens: number;
44
+ /** Maximum output tokens per generation. */
45
+ maxOutputTokens: number;
46
+ }
30
47
  interface InferenceClient {
31
48
  /** Provider type identifier (e.g. 'anthropic', 'ollama') */
32
49
  readonly type: string;
33
50
  /** Model identifier used for generation (e.g. 'claude-opus-4-6', 'llama3') */
34
51
  readonly modelId: string;
52
+ /**
53
+ * The provider's actual context/output ceilings for `modelId`. Discovered
54
+ * lazily on first call and cached for the client's lifetime; a failed
55
+ * discovery is NOT cached — the next call retries. Throws when the ceilings
56
+ * cannot be determined (unknown model, discovery endpoint unreachable):
57
+ * fail-loud, never a guessed floor.
58
+ */
59
+ limits(): Promise<InferenceLimits>;
35
60
  /**
36
61
  * Generate text from a prompt (simple interface)
37
62
  */
@@ -57,7 +82,11 @@ declare class AnthropicInferenceClient implements InferenceClient {
57
82
  readonly modelId: string;
58
83
  private client;
59
84
  private logger?;
85
+ private limitsPromise?;
60
86
  constructor(apiKey: string, model: string, baseURL?: string, logger?: Logger);
87
+ limits(): Promise<InferenceLimits>;
88
+ private discoverLimits;
89
+ private requestMessage;
61
90
  generateText(prompt: string, maxTokens: number, temperature: number, options?: InferenceOptions): Promise<string>;
62
91
  generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number, options?: InferenceOptions): Promise<InferenceResponse>;
63
92
  }
@@ -67,7 +96,10 @@ declare class OllamaInferenceClient implements InferenceClient {
67
96
  readonly modelId: string;
68
97
  private baseURL;
69
98
  private logger?;
99
+ private limitsPromise?;
70
100
  constructor(model: string, baseURL?: string, logger?: Logger);
101
+ limits(): Promise<InferenceLimits>;
102
+ private discoverLimits;
71
103
  generateText(prompt: string, maxTokens: number, temperature: number, options?: InferenceOptions): Promise<string>;
72
104
  generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number, options?: InferenceOptions): Promise<InferenceResponse>;
73
105
  }
@@ -78,13 +110,15 @@ declare class MockInferenceClient implements InferenceClient {
78
110
  private responses;
79
111
  private responseIndex;
80
112
  private stopReasons;
113
+ private injectedLimits;
81
114
  calls: Array<{
82
115
  prompt: string;
83
116
  maxTokens: number;
84
117
  temperature: number;
85
118
  options?: InferenceOptions;
86
119
  }>;
87
- constructor(responses?: string[], stopReasons?: string[]);
120
+ constructor(responses?: string[], stopReasons?: string[], limits?: InferenceLimits);
121
+ limits(): Promise<InferenceLimits>;
88
122
  generateText(prompt: string, maxTokens: number, temperature: number, options?: InferenceOptions): Promise<string>;
89
123
  generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number, options?: InferenceOptions): Promise<InferenceResponse>;
90
124
  reset(): void;
@@ -92,4 +126,4 @@ declare class MockInferenceClient implements InferenceClient {
92
126
  }
93
127
 
94
128
  export { AnthropicInferenceClient, MockInferenceClient, OllamaInferenceClient, createInferenceClient };
95
- export type { InferenceClient, InferenceClientConfig, InferenceClientType, InferenceOptions, InferenceResponse };
129
+ export type { InferenceClient, InferenceClientConfig, InferenceClientType, InferenceLimits, InferenceOptions, InferenceResponse };
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // src/implementations/anthropic.ts
2
2
  import Anthropic from "@anthropic-ai/sdk";
3
3
  import { recordInferenceUsage } from "@semiont/observability";
4
+ var NONSTREAMING_MAX_OUTPUT_TOKENS = Math.floor(128e3 / 6);
4
5
  var JSON_ARRAY_TOOL = {
5
6
  name: "emit_json_array",
6
7
  description: 'Return your entire answer by calling this tool. Put the JSON array of results under the "items" property, and emit no prose.',
@@ -19,6 +20,7 @@ var AnthropicInferenceClient = class {
19
20
  modelId;
20
21
  client;
21
22
  logger;
23
+ limitsPromise;
22
24
  constructor(apiKey, model, baseURL, logger) {
23
25
  this.client = new Anthropic({
24
26
  apiKey,
@@ -27,6 +29,33 @@ var AnthropicInferenceClient = class {
27
29
  this.modelId = model;
28
30
  this.logger = logger;
29
31
  }
32
+ limits() {
33
+ if (!this.limitsPromise) {
34
+ this.limitsPromise = this.discoverLimits().catch((err) => {
35
+ this.limitsPromise = void 0;
36
+ throw err;
37
+ });
38
+ }
39
+ return this.limitsPromise;
40
+ }
41
+ async discoverLimits() {
42
+ const info = await this.client.models.retrieve(this.modelId).catch((err) => {
43
+ throw new Error(
44
+ `Failed to discover model limits for '${this.modelId}' from the Models API`,
45
+ { cause: err }
46
+ );
47
+ });
48
+ if (info.max_input_tokens == null || info.max_tokens == null) {
49
+ throw new Error(`Models API reports no context/output ceilings for '${this.modelId}'`);
50
+ }
51
+ return { contextTokens: info.max_input_tokens, maxOutputTokens: info.max_tokens };
52
+ }
53
+ requestMessage(params) {
54
+ if (params.max_tokens > NONSTREAMING_MAX_OUTPUT_TOKENS) {
55
+ return this.client.messages.stream(params).finalMessage();
56
+ }
57
+ return this.client.messages.create(params);
58
+ }
30
59
  async generateText(prompt, maxTokens, temperature, options) {
31
60
  const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature, options);
32
61
  return response.text;
@@ -40,18 +69,19 @@ var AnthropicInferenceClient = class {
40
69
  temperature,
41
70
  format: options?.format
42
71
  });
72
+ const params = {
73
+ model: this.modelId,
74
+ max_tokens: maxTokens,
75
+ 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 } } : {}
80
+ };
43
81
  const start = performance.now();
44
82
  let response;
45
83
  try {
46
- response = await this.client.messages.create({
47
- model: this.modelId,
48
- max_tokens: maxTokens,
49
- temperature,
50
- messages: [{ role: "user", content: prompt }],
51
- // JSON mode → force the structured-output tool. No prefill assistant
52
- // turn: the constraint now lives in the tool call, not in free text.
53
- ...jsonMode ? { tools: [JSON_ARRAY_TOOL], tool_choice: { type: "tool", name: JSON_ARRAY_TOOL.name } } : {}
54
- });
84
+ response = await this.requestMessage(params);
55
85
  } catch (err) {
56
86
  recordInferenceUsage({
57
87
  provider: this.type,
@@ -127,17 +157,49 @@ var AnthropicInferenceClient = class {
127
157
  };
128
158
 
129
159
  // src/implementations/ollama.ts
160
+ import { estimateTokens, isNumber, isObject } from "@semiont/core";
130
161
  import { recordInferenceUsage as recordInferenceUsage2 } from "@semiont/observability";
162
+ var NUM_CTX_ESTIMATE_SLACK = 0.2;
163
+ var NUM_CTX_TEMPLATE_ALLOWANCE = 64;
131
164
  var OllamaInferenceClient = class {
132
165
  type = "ollama";
133
166
  modelId;
134
167
  baseURL;
135
168
  logger;
169
+ limitsPromise;
136
170
  constructor(model, baseURL, logger) {
137
171
  this.baseURL = (baseURL || "http://localhost:11434").replace(/\/+$/, "");
138
172
  this.modelId = model;
139
173
  this.logger = logger;
140
174
  }
175
+ limits() {
176
+ if (!this.limitsPromise) {
177
+ this.limitsPromise = this.discoverLimits().catch((err) => {
178
+ this.limitsPromise = void 0;
179
+ throw err;
180
+ });
181
+ }
182
+ return this.limitsPromise;
183
+ }
184
+ async discoverLimits() {
185
+ const res = await fetch(`${this.baseURL}/api/show`, {
186
+ method: "POST",
187
+ headers: { "Content-Type": "application/json" },
188
+ body: JSON.stringify({ model: this.modelId })
189
+ });
190
+ if (!res.ok) {
191
+ throw new Error(
192
+ `Failed to discover model limits: /api/show returned ${res.status} for '${this.modelId}'`
193
+ );
194
+ }
195
+ const data = await res.json();
196
+ const modelInfo = isObject(data) && isObject(data["model_info"]) ? data["model_info"] : void 0;
197
+ const contextTokens = readContextLength(modelInfo);
198
+ if (contextTokens === void 0) {
199
+ throw new Error(`/api/show reports no context length for '${this.modelId}'`);
200
+ }
201
+ return { contextTokens, maxOutputTokens: contextTokens };
202
+ }
141
203
  async generateText(prompt, maxTokens, temperature, options) {
142
204
  const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature, options);
143
205
  return response.text;
@@ -150,6 +212,17 @@ var OllamaInferenceClient = class {
150
212
  temperature,
151
213
  format: options?.format
152
214
  });
215
+ const limits = await this.limits();
216
+ const promptTokens = estimateTokens(prompt);
217
+ if (promptTokens + maxTokens > limits.contextTokens) {
218
+ throw new Error(
219
+ `Prompt (~${promptTokens} tokens) + output budget (${maxTokens}) exceed the '${this.modelId}' context window (${limits.contextTokens} tokens)`
220
+ );
221
+ }
222
+ const numCtx = Math.min(
223
+ limits.contextTokens,
224
+ promptTokens + maxTokens + Math.ceil(promptTokens * NUM_CTX_ESTIMATE_SLACK) + NUM_CTX_TEMPLATE_ALLOWANCE
225
+ );
153
226
  const url = `${this.baseURL}/api/generate`;
154
227
  const start = performance.now();
155
228
  const body = {
@@ -159,6 +232,7 @@ var OllamaInferenceClient = class {
159
232
  think: false,
160
233
  options: {
161
234
  num_predict: maxTokens,
235
+ num_ctx: numCtx,
162
236
  temperature
163
237
  }
164
238
  };
@@ -229,6 +303,20 @@ var OllamaInferenceClient = class {
229
303
  };
230
304
  }
231
305
  };
306
+ function readContextLength(modelInfo) {
307
+ if (!modelInfo) return void 0;
308
+ const arch = modelInfo["general.architecture"];
309
+ if (typeof arch === "string") {
310
+ const direct = modelInfo[`${arch}.context_length`];
311
+ if (isNumber(direct) && direct > 0) return direct;
312
+ }
313
+ const fallbackKey = Object.keys(modelInfo).find((k) => k.endsWith(".context_length"));
314
+ if (fallbackKey !== void 0) {
315
+ const fallback = modelInfo[fallbackKey];
316
+ if (isNumber(fallback) && fallback > 0) return fallback;
317
+ }
318
+ return void 0;
319
+ }
232
320
  function mapStopReason(doneReason) {
233
321
  switch (doneReason) {
234
322
  case "stop":
@@ -267,16 +355,25 @@ function createInferenceClient(config, logger) {
267
355
  }
268
356
 
269
357
  // src/implementations/mock.ts
358
+ var GENEROUS_LIMITS = {
359
+ contextTokens: 1e6,
360
+ maxOutputTokens: 1e6
361
+ };
270
362
  var MockInferenceClient = class {
271
363
  type = "mock";
272
364
  modelId = "mock-model";
273
365
  responses = [];
274
366
  responseIndex = 0;
275
367
  stopReasons = [];
368
+ injectedLimits;
276
369
  calls = [];
277
- constructor(responses = ["Mock response"], stopReasons) {
370
+ constructor(responses = ["Mock response"], stopReasons, limits) {
278
371
  this.responses = responses;
279
372
  this.stopReasons = stopReasons || responses.map(() => "end_turn");
373
+ this.injectedLimits = limits ?? GENEROUS_LIMITS;
374
+ }
375
+ async limits() {
376
+ return this.injectedLimits;
280
377
  }
281
378
  async generateText(prompt, maxTokens, temperature, options) {
282
379
  const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature, options);
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, InferenceOptions, InferenceResponse } from '../interface.js';\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\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 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 start = performance.now();\n let response: Awaited<ReturnType<typeof this.client.messages.create>>;\n try {\n response = await this.client.messages.create({\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 } 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 type { Logger } from '@semiont/core';\nimport { recordInferenceUsage } from '@semiont/observability';\nimport { InferenceClient, InferenceOptions, InferenceResponse } from '../interface.js';\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 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 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 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 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\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, InferenceOptions, InferenceResponse } from '../interface.js';\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 public calls: Array<{ prompt: string; maxTokens: number; temperature: number; options?: InferenceOptions }> = [];\n\n constructor(responses: string[] = ['Mock response'], stopReasons?: string[]) {\n this.responses = responses;\n this.stopReasons = stopReasons || responses.map(() => 'end_turn');\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 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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@semiont/inference",
3
- "version": "0.5.23",
3
+ "version": "0.5.25",
4
4
  "engines": {
5
5
  "node": ">=24.0.0"
6
6
  },
@@ -27,13 +27,13 @@
27
27
  "test:coverage": "vitest run --coverage"
28
28
  },
29
29
  "dependencies": {
30
- "@anthropic-ai/sdk": "^0.112.5",
31
- "@semiont/core": "0.5.23",
32
- "@semiont/observability": "0.5.23"
30
+ "@anthropic-ai/sdk": "^0.115.0",
31
+ "@semiont/core": "0.5.25",
32
+ "@semiont/observability": "0.5.25"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@vitest/coverage-v8": "^4.1.8",
36
- "rollup": "^4.61.0",
36
+ "rollup": "^4.62.3",
37
37
  "rollup-plugin-dts": "^6.4.1",
38
38
  "tsup": "^8.0.1",
39
39
  "typescript": "^6.0.2",