@semiont/inference 0.5.24 → 0.5.26

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/).
@@ -90,34 +90,59 @@ 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;
100
106
  }
101
107
  ```
102
108
 
103
- ### JSON output mode
109
+ ### Structured generation
104
110
 
105
- 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:
106
112
 
107
113
  ```typescript
108
- const json = await client.generateText(prompt, 1000, 0, { format: 'json' });
109
- 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
+ });
110
120
  ```
111
121
 
112
122
  Each implementation honors the contract with its provider's mechanism:
113
- - **Ollama**: grammar-constrained sampling — the request's `format` field carries a minimal array schema.
114
- - **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).
127
+
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).
129
+
130
+ ### Provider limits
131
+
132
+ `limits()` publishes the provider's **actual** context/output ceilings for the configured model — discovered from the provider itself, never hand-maintained constants:
133
+
134
+ - **Anthropic**: the Models API (`models.retrieve`) — `max_input_tokens` / `max_tokens`.
135
+ - **Ollama**: `POST /api/show` — the model's context window. Input and output share that window, so it is published as both fields (`maxOutputTokens === contextTokens` signals a shared window).
136
+
137
+ 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.
115
138
 
116
- 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).
139
+ Two request-time behaviors ride on the limits:
140
+ - **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.
141
+ - **Anthropic streams internally** above the SDK's non-streaming output ceiling (≈21K tokens) — same interface, same response shape.
117
142
 
118
143
  ### `MockInferenceClient`
119
144
 
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.
145
+ A scripted test double ([src/implementations/mock.ts](src/implementations/mock.ts)): construct it with a list of canned responses, then inspect `calls` (recorded prompt/maxTokens/temperature/options per invocation). `reset()` and `setResponses()` helpers included. An optional third constructor argument injects `InferenceLimits` for chunking/budget tests; the default is generous (1M/1M) so ordinary tests never trip window guards.
121
146
 
122
147
  ```typescript
123
148
  import { MockInferenceClient } from '@semiont/inference';
package/dist/index.d.ts CHANGED
@@ -5,41 +5,89 @@ 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 {
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;
38
+ }
39
+ /**
40
+ * A provider's actual ceilings for the configured model, discovered from the
41
+ * provider itself (Anthropic Models API; Ollama `/api/show`) — never
42
+ * hand-maintained constants. Detection budget arithmetic derives from these.
43
+ */
44
+ interface InferenceLimits {
13
45
  /**
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.
46
+ * The context window in tokens. Semantics differ by provider shape:
47
+ * Anthropic reports maximum *input* tokens (output has its own ceiling);
48
+ * Ollama reports the *shared* input+output window and mirrors it in
49
+ * `maxOutputTokens` (there is no separate output ceiling), so
50
+ * `maxOutputTokens === contextTokens` signals a shared window.
27
51
  */
28
- format?: 'json';
52
+ contextTokens: number;
53
+ /** Maximum output tokens per generation. */
54
+ maxOutputTokens: number;
29
55
  }
30
56
  interface InferenceClient {
31
57
  /** Provider type identifier (e.g. 'anthropic', 'ollama') */
32
58
  readonly type: string;
33
59
  /** Model identifier used for generation (e.g. 'claude-opus-4-6', 'llama3') */
34
60
  readonly modelId: string;
61
+ /**
62
+ * The provider's actual context/output ceilings for `modelId`. Discovered
63
+ * lazily on first call and cached for the client's lifetime; a failed
64
+ * discovery is NOT cached — the next call retries. Throws when the ceilings
65
+ * cannot be determined (unknown model, discovery endpoint unreachable):
66
+ * fail-loud, never a guessed floor.
67
+ */
68
+ limits(): Promise<InferenceLimits>;
35
69
  /**
36
70
  * Generate text from a prompt (simple interface)
37
71
  */
38
- generateText(prompt: string, maxTokens: number, temperature: number, options?: InferenceOptions): Promise<string>;
72
+ generateText(prompt: string, maxTokens: number, temperature: number): Promise<string>;
39
73
  /**
40
74
  * Generate text with detailed response information
41
75
  */
42
- 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>>;
43
91
  }
44
92
 
45
93
  type InferenceClientType = 'anthropic' | 'ollama';
@@ -57,9 +105,18 @@ declare class AnthropicInferenceClient implements InferenceClient {
57
105
  readonly modelId: string;
58
106
  private client;
59
107
  private logger?;
108
+ private discoveryPromise?;
60
109
  constructor(apiKey: string, model: string, baseURL?: string, logger?: Logger);
61
- generateText(prompt: string, maxTokens: number, temperature: number, options?: InferenceOptions): Promise<string>;
62
- generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number, options?: InferenceOptions): Promise<InferenceResponse>;
110
+ limits(): Promise<InferenceLimits>;
111
+ private discover;
112
+ private discoverModel;
113
+ private requestMessage;
114
+ generateText(prompt: string, maxTokens: number, temperature: number): Promise<string>;
115
+ generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number): Promise<InferenceResponse>;
116
+ generateStructured<T>(prompt: string, maxTokens: number, temperature: number, elementSchema: ElementSchema): Promise<StructuredResponse<T>>;
117
+ /** Issue the request, recording an error metric if the transport throws. */
118
+ private recordedRequest;
119
+ private recordError;
63
120
  }
64
121
 
65
122
  declare class OllamaInferenceClient implements InferenceClient {
@@ -67,9 +124,14 @@ declare class OllamaInferenceClient implements InferenceClient {
67
124
  readonly modelId: string;
68
125
  private baseURL;
69
126
  private logger?;
127
+ private limitsPromise?;
70
128
  constructor(model: string, baseURL?: string, logger?: Logger);
71
- generateText(prompt: string, maxTokens: number, temperature: number, options?: InferenceOptions): Promise<string>;
72
- generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number, options?: InferenceOptions): Promise<InferenceResponse>;
129
+ limits(): Promise<InferenceLimits>;
130
+ private discoverLimits;
131
+ generateText(prompt: string, maxTokens: number, temperature: number): Promise<string>;
132
+ generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number): Promise<InferenceResponse>;
133
+ generateStructured<T>(prompt: string, maxTokens: number, temperature: number, elementSchema: ElementSchema): Promise<StructuredResponse<T>>;
134
+ private generate;
73
135
  }
74
136
 
75
137
  declare class MockInferenceClient implements InferenceClient {
@@ -78,18 +140,29 @@ declare class MockInferenceClient implements InferenceClient {
78
140
  private responses;
79
141
  private responseIndex;
80
142
  private stopReasons;
143
+ private injectedLimits;
81
144
  calls: Array<{
82
145
  prompt: string;
83
146
  maxTokens: number;
84
147
  temperature: number;
85
- options?: InferenceOptions;
148
+ elementSchema?: ElementSchema;
86
149
  }>;
87
- constructor(responses?: string[], stopReasons?: string[]);
88
- generateText(prompt: string, maxTokens: number, temperature: number, options?: InferenceOptions): Promise<string>;
89
- generateTextWithMetadata(prompt: string, maxTokens: number, temperature: number, options?: InferenceOptions): Promise<InferenceResponse>;
150
+ constructor(responses?: string[], stopReasons?: string[], limits?: InferenceLimits);
151
+ limits(): Promise<InferenceLimits>;
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;
90
163
  reset(): void;
91
164
  setResponses(responses: string[], stopReasons?: string[]): void;
92
165
  }
93
166
 
94
167
  export { AnthropicInferenceClient, MockInferenceClient, OllamaInferenceClient, createInferenceClient };
95
- export type { InferenceClient, InferenceClientConfig, InferenceClientType, InferenceOptions, InferenceResponse };
168
+ export type { ElementSchema, InferenceClient, InferenceClientConfig, InferenceClientType, InferenceLimits, InferenceResponse, StructuredResponse };
package/dist/index.js CHANGED
@@ -1,24 +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
- var JSON_ARRAY_TOOL = {
5
- name: "emit_json_array",
6
- description: 'Return your entire answer by calling this tool. Put the JSON array of results under the "items" property, and emit no prose.',
7
- input_schema: {
8
- type: "object",
9
- properties: {
10
- // Element shape is unconstrained here — the prompt carries the per-element
11
- // schema; the tool only enforces that the top-level result is an array.
12
- items: { type: "array", items: {} }
13
- },
14
- required: ["items"]
15
- }
16
- };
5
+ var NONSTREAMING_MAX_OUTPUT_TOKENS = Math.floor(128e3 / 6);
17
6
  var AnthropicInferenceClient = class {
18
7
  type = "anthropic";
19
8
  modelId;
20
9
  client;
21
10
  logger;
11
+ discoveryPromise;
22
12
  constructor(apiKey, model, baseURL, logger) {
23
13
  this.client = new Anthropic({
24
14
  apiKey,
@@ -27,84 +17,151 @@ var AnthropicInferenceClient = class {
27
17
  this.modelId = model;
28
18
  this.logger = logger;
29
19
  }
30
- async generateText(prompt, maxTokens, temperature, options) {
31
- const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature, options);
20
+ limits() {
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;
27
+ throw err;
28
+ });
29
+ }
30
+ return this.discoveryPromise;
31
+ }
32
+ async discoverModel() {
33
+ const info = await this.client.models.retrieve(this.modelId).catch((err) => {
34
+ throw new Error(
35
+ `Failed to discover model limits for '${this.modelId}' from the Models API`,
36
+ { cause: err }
37
+ );
38
+ });
39
+ if (info.max_input_tokens == null || info.max_tokens == null) {
40
+ throw new Error(`Models API reports no context/output ceilings for '${this.modelId}'`);
41
+ }
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
+ };
48
+ }
49
+ requestMessage(params) {
50
+ if (params.max_tokens > NONSTREAMING_MAX_OUTPUT_TOKENS) {
51
+ return this.client.messages.stream(params).finalMessage();
52
+ }
53
+ return this.client.messages.create(params);
54
+ }
55
+ async generateText(prompt, maxTokens, temperature) {
56
+ const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature);
32
57
  return response.text;
33
58
  }
34
- async generateTextWithMetadata(prompt, maxTokens, temperature, options) {
35
- const jsonMode = options?.format === "json";
59
+ async generateTextWithMetadata(prompt, maxTokens, temperature) {
36
60
  this.logger?.debug("Generating text with inference client", {
37
61
  model: this.modelId,
38
62
  promptLength: prompt.length,
39
63
  maxTokens,
40
- temperature,
41
- format: options?.format
64
+ temperature
42
65
  });
66
+ const params = {
67
+ model: this.modelId,
68
+ max_tokens: maxTokens,
69
+ temperature,
70
+ messages: [{ role: "user", content: prompt }]
71
+ };
43
72
  const start = performance.now();
44
- let response;
45
- try {
46
- response = await this.client.messages.create({
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", {
47
78
  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 } } : {}
79
+ contentTypes: response.content.map((c) => c.type)
54
80
  });
55
- } catch (err) {
56
- recordInferenceUsage({
57
- provider: this.type,
58
- model: this.modelId,
59
- durationMs: performance.now() - start,
60
- outcome: "error"
61
- });
62
- throw err;
81
+ throw new Error("No text content in inference response");
63
82
  }
64
- 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", {
65
93
  model: this.modelId,
66
- contentBlocks: response.content.length,
94
+ textLength: text.length,
67
95
  stopReason: response.stop_reason
68
96
  });
69
- let text;
70
- if (jsonMode) {
71
- const toolUse = response.content.find((c) => c.type === "tool_use");
72
- if (!toolUse || toolUse.type !== "tool_use") {
73
- recordInferenceUsage({
74
- provider: this.type,
75
- model: this.modelId,
76
- durationMs: performance.now() - start,
77
- outcome: "error",
78
- inputTokens: response.usage?.input_tokens,
79
- outputTokens: response.usage?.output_tokens
80
- });
81
- this.logger?.error("No tool_use content in inference response", {
82
- model: this.modelId,
83
- contentTypes: response.content.map((c) => c.type)
84
- });
85
- throw new Error("No tool_use content in inference response");
86
- }
87
- const input = toolUse.input;
88
- const items = Array.isArray(input.items) ? input.items : [];
89
- text = JSON.stringify(items);
90
- } else {
91
- const textContent = response.content.find((c) => c.type === "text");
92
- if (!textContent || textContent.type !== "text") {
93
- recordInferenceUsage({
94
- provider: this.type,
95
- model: this.modelId,
96
- durationMs: performance.now() - start,
97
- outcome: "error",
98
- inputTokens: response.usage?.input_tokens,
99
- outputTokens: response.usage?.output_tokens
100
- });
101
- this.logger?.error("No text content in inference response", {
102
- model: this.modelId,
103
- contentTypes: response.content.map((c) => c.type)
104
- });
105
- 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
+ }
106
127
  }
107
- 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
+ );
108
165
  }
109
166
  recordInferenceUsage({
110
167
  provider: this.type,
@@ -114,42 +171,140 @@ var AnthropicInferenceClient = class {
114
171
  inputTokens: response.usage?.input_tokens,
115
172
  outputTokens: response.usage?.output_tokens
116
173
  });
117
- this.logger?.info("Text generation completed", {
174
+ this.logger?.info("Structured generation completed", {
118
175
  model: this.modelId,
119
- textLength: text.length,
176
+ items: parsed.length,
120
177
  stopReason: response.stop_reason
121
178
  });
122
179
  return {
123
- text,
180
+ items: parsed,
124
181
  stopReason: response.stop_reason || "unknown"
125
182
  };
126
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
+ }
127
208
  };
128
209
 
129
210
  // src/implementations/ollama.ts
211
+ import { estimateTokens, isNumber, isObject as isObject2 } from "@semiont/core";
130
212
  import { recordInferenceUsage as recordInferenceUsage2 } from "@semiont/observability";
213
+ var NUM_CTX_ESTIMATE_SLACK = 0.2;
214
+ var NUM_CTX_TEMPLATE_ALLOWANCE = 64;
131
215
  var OllamaInferenceClient = class {
132
216
  type = "ollama";
133
217
  modelId;
134
218
  baseURL;
135
219
  logger;
220
+ limitsPromise;
136
221
  constructor(model, baseURL, logger) {
137
222
  this.baseURL = (baseURL || "http://localhost:11434").replace(/\/+$/, "");
138
223
  this.modelId = model;
139
224
  this.logger = logger;
140
225
  }
141
- async generateText(prompt, maxTokens, temperature, options) {
142
- const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature, options);
226
+ limits() {
227
+ if (!this.limitsPromise) {
228
+ this.limitsPromise = this.discoverLimits().catch((err) => {
229
+ this.limitsPromise = void 0;
230
+ throw err;
231
+ });
232
+ }
233
+ return this.limitsPromise;
234
+ }
235
+ async discoverLimits() {
236
+ const res = await fetch(`${this.baseURL}/api/show`, {
237
+ method: "POST",
238
+ headers: { "Content-Type": "application/json" },
239
+ body: JSON.stringify({ model: this.modelId })
240
+ });
241
+ if (!res.ok) {
242
+ throw new Error(
243
+ `Failed to discover model limits: /api/show returned ${res.status} for '${this.modelId}'`
244
+ );
245
+ }
246
+ const data = await res.json();
247
+ const modelInfo = isObject2(data) && isObject2(data["model_info"]) ? data["model_info"] : void 0;
248
+ const contextTokens = readContextLength(modelInfo);
249
+ if (contextTokens === void 0) {
250
+ throw new Error(`/api/show reports no context length for '${this.modelId}'`);
251
+ }
252
+ return { contextTokens, maxOutputTokens: contextTokens };
253
+ }
254
+ async generateText(prompt, maxTokens, temperature) {
255
+ const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature);
143
256
  return response.text;
144
257
  }
145
- 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) {
146
290
  this.logger?.debug("Generating text with Ollama", {
147
291
  model: this.modelId,
148
292
  promptLength: prompt.length,
149
293
  maxTokens,
150
294
  temperature,
151
- format: options?.format
295
+ structured: elementSchema !== void 0
152
296
  });
297
+ const limits = await this.limits();
298
+ const promptTokens = estimateTokens(prompt);
299
+ if (promptTokens + maxTokens > limits.contextTokens) {
300
+ throw new Error(
301
+ `Prompt (~${promptTokens} tokens) + output budget (${maxTokens}) exceed the '${this.modelId}' context window (${limits.contextTokens} tokens)`
302
+ );
303
+ }
304
+ const numCtx = Math.min(
305
+ limits.contextTokens,
306
+ promptTokens + maxTokens + Math.ceil(promptTokens * NUM_CTX_ESTIMATE_SLACK) + NUM_CTX_TEMPLATE_ALLOWANCE
307
+ );
153
308
  const url = `${this.baseURL}/api/generate`;
154
309
  const start = performance.now();
155
310
  const body = {
@@ -159,11 +314,12 @@ var OllamaInferenceClient = class {
159
314
  think: false,
160
315
  options: {
161
316
  num_predict: maxTokens,
317
+ num_ctx: numCtx,
162
318
  temperature
163
319
  }
164
320
  };
165
- if (options?.format === "json") {
166
- body["format"] = { type: "array", items: {} };
321
+ if (elementSchema !== void 0) {
322
+ body["format"] = { type: "array", items: elementSchema };
167
323
  }
168
324
  let res;
169
325
  try {
@@ -229,6 +385,20 @@ var OllamaInferenceClient = class {
229
385
  };
230
386
  }
231
387
  };
388
+ function readContextLength(modelInfo) {
389
+ if (!modelInfo) return void 0;
390
+ const arch = modelInfo["general.architecture"];
391
+ if (typeof arch === "string") {
392
+ const direct = modelInfo[`${arch}.context_length`];
393
+ if (isNumber(direct) && direct > 0) return direct;
394
+ }
395
+ const fallbackKey = Object.keys(modelInfo).find((k) => k.endsWith(".context_length"));
396
+ if (fallbackKey !== void 0) {
397
+ const fallback = modelInfo[fallbackKey];
398
+ if (isNumber(fallback) && fallback > 0) return fallback;
399
+ }
400
+ return void 0;
401
+ }
232
402
  function mapStopReason(doneReason) {
233
403
  switch (doneReason) {
234
404
  case "stop":
@@ -267,23 +437,61 @@ function createInferenceClient(config, logger) {
267
437
  }
268
438
 
269
439
  // src/implementations/mock.ts
440
+ var GENEROUS_LIMITS = {
441
+ contextTokens: 1e6,
442
+ maxOutputTokens: 1e6
443
+ };
270
444
  var MockInferenceClient = class {
271
445
  type = "mock";
272
446
  modelId = "mock-model";
273
447
  responses = [];
274
448
  responseIndex = 0;
275
449
  stopReasons = [];
450
+ injectedLimits;
276
451
  calls = [];
277
- constructor(responses = ["Mock response"], stopReasons) {
452
+ constructor(responses = ["Mock response"], stopReasons, limits) {
278
453
  this.responses = responses;
279
454
  this.stopReasons = stopReasons || responses.map(() => "end_turn");
455
+ this.injectedLimits = limits ?? GENEROUS_LIMITS;
280
456
  }
281
- async generateText(prompt, maxTokens, temperature, options) {
282
- const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature, options);
457
+ async limits() {
458
+ return this.injectedLimits;
459
+ }
460
+ async generateText(prompt, maxTokens, temperature) {
461
+ const response = await this.generateTextWithMetadata(prompt, maxTokens, temperature);
283
462
  return response.text;
284
463
  }
285
- async generateTextWithMetadata(prompt, maxTokens, temperature, options) {
286
- 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() {
287
495
  const text = this.responses[this.responseIndex];
288
496
  const stopReason = this.stopReasons[this.responseIndex] || "end_turn";
289
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, 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 { 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.24",
3
+ "version": "0.5.26",
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.24",
32
- "@semiont/observability": "0.5.24"
30
+ "@anthropic-ai/sdk": "^0.115.0",
31
+ "@semiont/core": "0.5.26",
32
+ "@semiont/observability": "0.5.26"
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",