genai-lite 0.11.0 → 0.12.0

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.
@@ -1,4 +1,4 @@
1
- import type { LLMResponse, LLMFailureResponse } from "../types";
1
+ import type { LLMResponse, LLMFailureResponse, LLMStreamEvent } from "../types";
2
2
  import type { ILLMClientAdapter, InternalLLMChatRequest, AdapterRequestOptions } from "./types";
3
3
  import type { Logger } from "../../logging/types";
4
4
  /**
@@ -33,6 +33,7 @@ export declare class AnthropicClientAdapter implements ILLMClientAdapter {
33
33
  * @returns Promise resolving to success or failure response
34
34
  */
35
35
  sendMessage(request: InternalLLMChatRequest, apiKey: string, options?: AdapterRequestOptions): Promise<LLMResponse | LLMFailureResponse>;
36
+ streamMessage(request: InternalLLMChatRequest, apiKey: string, options?: AdapterRequestOptions): AsyncIterable<LLMStreamEvent>;
36
37
  /**
37
38
  * Validates Anthropic API key format
38
39
  *
@@ -48,6 +49,10 @@ export declare class AnthropicClientAdapter implements ILLMClientAdapter {
48
49
  name: string;
49
50
  version: string;
50
51
  };
52
+ private prepareMessageRequest;
53
+ private mapAnthropicUsage;
54
+ private mergeAnthropicUsage;
55
+ private createSyntheticMessage;
51
56
  /**
52
57
  * Recursively adds additionalProperties: false to all object schemas
53
58
  * Required by Anthropic's strict mode for structured outputs
@@ -42,89 +42,7 @@ class AnthropicClientAdapter {
42
42
  */
43
43
  async sendMessage(request, apiKey, options) {
44
44
  try {
45
- // Check if structured output is requested - need beta API
46
- const useStructuredOutput = request.settings.structuredOutput?.schema &&
47
- request.settings.structuredOutput.enabled !== false;
48
- // Initialize Anthropic client
49
- const anthropic = new sdk_1.default({
50
- apiKey,
51
- ...(this.baseURL && { baseURL: this.baseURL }),
52
- maxRetries: 0, // retries are owned by the unified LLMService retry layer
53
- });
54
- // Per-request transport options (abort signal, timeout)
55
- const requestTransportOptions = {
56
- ...(options?.signal && { signal: options.signal }),
57
- ...(options?.timeoutMs !== undefined && { timeout: options.timeoutMs }),
58
- };
59
- // Format messages for Anthropic API (Claude has specific requirements)
60
- const { messages, systemMessage } = this.formatMessagesForAnthropic(request);
61
- // Prepare API call parameters
62
- const messageParams = {
63
- model: request.modelId,
64
- messages: messages,
65
- max_tokens: request.settings.maxTokens,
66
- temperature: request.settings.temperature,
67
- top_p: request.settings.topP,
68
- ...(request.settings.topK !== undefined && {
69
- top_k: request.settings.topK,
70
- }),
71
- ...(systemMessage && { system: systemMessage }),
72
- ...(request.settings.stopSequences.length > 0 && {
73
- stop_sequences: request.settings.stopSequences,
74
- }),
75
- };
76
- // Handle structured output configuration for Anthropic
77
- // Note: Structured output requires the beta API endpoint
78
- if (useStructuredOutput) {
79
- const so = request.settings.structuredOutput;
80
- // Anthropic requires additionalProperties: false on all object schemas
81
- const processedSchema = so.strict !== false
82
- ? this.addAdditionalPropertiesFalse(so.schema)
83
- : so.schema;
84
- // Anthropic's format: output_format.schema is the schema directly
85
- messageParams.output_format = {
86
- type: 'json_schema',
87
- name: so.name,
88
- schema: processedSchema,
89
- strict: so.strict !== false,
90
- };
91
- }
92
- // Handle reasoning/thinking configuration for Claude models
93
- if (request.settings.reasoning && !request.settings.reasoning.exclude) {
94
- const reasoning = request.settings.reasoning;
95
- let budgetTokens;
96
- // Convert reasoning settings to Anthropic's thinking format
97
- if (reasoning.maxTokens !== undefined) {
98
- budgetTokens = Math.max(reasoning.maxTokens, 1024); // Minimum 1024
99
- }
100
- else if (reasoning.effort) {
101
- // Convert effort levels to token budgets
102
- // Max budget for Anthropic is 32000
103
- const maxBudget = 32000;
104
- switch (reasoning.effort) {
105
- case 'high':
106
- budgetTokens = Math.floor(maxBudget * 0.8);
107
- break;
108
- case 'medium':
109
- budgetTokens = Math.floor(maxBudget * 0.5);
110
- break;
111
- case 'low':
112
- budgetTokens = Math.floor(maxBudget * 0.2);
113
- break;
114
- }
115
- }
116
- else if (reasoning.enabled !== false) {
117
- // Use default budget
118
- budgetTokens = 10000;
119
- }
120
- if (budgetTokens !== undefined) {
121
- // Add thinking configuration to the request
122
- messageParams.thinking = {
123
- type: "enabled",
124
- budget_tokens: Math.min(budgetTokens, 32000) // Cap at max
125
- };
126
- }
127
- }
45
+ const { anthropic, messageParams, requestTransportOptions, useStructuredOutput, messages, } = this.prepareMessageRequest(request, apiKey, options);
128
46
  this.logger.info(`Making Anthropic API call for model: ${request.modelId}`);
129
47
  this.logger.debug(`Anthropic API parameters:`, {
130
48
  model: messageParams.model,
@@ -163,6 +81,127 @@ class AnthropicClientAdapter {
163
81
  return this.createErrorResponse(error, request);
164
82
  }
165
83
  }
84
+ async *streamMessage(request, apiKey, options) {
85
+ const accumulator = {
86
+ id: "",
87
+ model: request.modelId,
88
+ created: Math.floor(Date.now() / 1000),
89
+ content: "",
90
+ reasoning: "",
91
+ stopReason: null,
92
+ };
93
+ let sawEvent = false;
94
+ let started = false;
95
+ try {
96
+ const { anthropic, messageParams, requestTransportOptions, useStructuredOutput, } = this.prepareMessageRequest(request, apiKey, options);
97
+ this.logger.info(`Making Anthropic streaming API call for model: ${request.modelId}`);
98
+ const streamOptions = useStructuredOutput
99
+ ? {
100
+ headers: {
101
+ "anthropic-beta": "structured-outputs-2025-11-13",
102
+ },
103
+ ...requestTransportOptions,
104
+ }
105
+ : requestTransportOptions;
106
+ const stream = Object.keys(streamOptions).length > 0
107
+ ? anthropic.messages.stream(messageParams, streamOptions)
108
+ : anthropic.messages.stream(messageParams);
109
+ for await (const event of stream) {
110
+ sawEvent = true;
111
+ if (event.type === "message_start") {
112
+ accumulator.id = event.message.id || accumulator.id;
113
+ accumulator.model = event.message.model || accumulator.model;
114
+ accumulator.stopReason = event.message.stop_reason ?? accumulator.stopReason;
115
+ accumulator.usage = this.mergeAnthropicUsage(accumulator.usage, event.message.usage);
116
+ if (!started) {
117
+ started = true;
118
+ yield {
119
+ type: "start",
120
+ provider: request.providerId,
121
+ model: accumulator.model,
122
+ id: accumulator.id,
123
+ created: accumulator.created,
124
+ };
125
+ }
126
+ if (event.message.usage) {
127
+ yield {
128
+ type: "usage",
129
+ usage: this.mapAnthropicUsage(accumulator.usage),
130
+ };
131
+ }
132
+ continue;
133
+ }
134
+ if (!started) {
135
+ started = true;
136
+ yield {
137
+ type: "start",
138
+ provider: request.providerId,
139
+ model: accumulator.model,
140
+ id: accumulator.id,
141
+ created: accumulator.created,
142
+ };
143
+ }
144
+ if (event.type === "content_block_start") {
145
+ const block = event.content_block;
146
+ if (block?.type === "text" && typeof block.text === "string" && block.text.length > 0) {
147
+ accumulator.content += block.text;
148
+ yield { type: "content_delta", delta: block.text, index: event.index };
149
+ }
150
+ else if (block?.type === "thinking" &&
151
+ typeof block.thinking === "string" &&
152
+ block.thinking.length > 0) {
153
+ accumulator.reasoning += block.thinking;
154
+ if (request.settings.reasoning?.exclude !== true) {
155
+ yield { type: "reasoning_delta", delta: block.thinking, index: event.index };
156
+ }
157
+ }
158
+ }
159
+ else if (event.type === "content_block_delta") {
160
+ const delta = event.delta;
161
+ if (delta?.type === "text_delta" && typeof delta.text === "string" && delta.text.length > 0) {
162
+ accumulator.content += delta.text;
163
+ yield { type: "content_delta", delta: delta.text, index: event.index };
164
+ }
165
+ else if (delta?.type === "thinking_delta" &&
166
+ typeof delta.thinking === "string" &&
167
+ delta.thinking.length > 0) {
168
+ accumulator.reasoning += delta.thinking;
169
+ if (request.settings.reasoning?.exclude !== true) {
170
+ yield { type: "reasoning_delta", delta: delta.thinking, index: event.index };
171
+ }
172
+ }
173
+ }
174
+ else if (event.type === "message_delta") {
175
+ accumulator.stopReason = event.delta.stop_reason ?? accumulator.stopReason;
176
+ accumulator.usage = this.mergeAnthropicUsage(accumulator.usage, event.usage);
177
+ if (event.usage) {
178
+ yield {
179
+ type: "usage",
180
+ usage: this.mapAnthropicUsage(accumulator.usage),
181
+ };
182
+ }
183
+ }
184
+ }
185
+ const response = this.createSuccessResponse(this.createSyntheticMessage(request, accumulator), request);
186
+ yield { type: "complete", response };
187
+ }
188
+ catch (error) {
189
+ this.logger.error("Anthropic streaming API error:", error);
190
+ const errorResponse = this.createErrorResponse(error, request);
191
+ if (sawEvent || accumulator.content.length > 0 || accumulator.reasoning.length > 0) {
192
+ const partial = this.createSuccessResponse(this.createSyntheticMessage(request, accumulator), request);
193
+ errorResponse.partialResponse = {
194
+ id: partial.id,
195
+ provider: partial.provider,
196
+ model: partial.model,
197
+ created: partial.created,
198
+ choices: partial.choices,
199
+ usage: partial.usage,
200
+ };
201
+ }
202
+ yield { type: "error", error: errorResponse };
203
+ }
204
+ }
166
205
  /**
167
206
  * Validates Anthropic API key format
168
207
  *
@@ -183,6 +222,142 @@ class AnthropicClientAdapter {
183
222
  version: "1.0.0",
184
223
  };
185
224
  }
225
+ prepareMessageRequest(request, apiKey, options) {
226
+ // Check if structured output is requested - need beta API
227
+ const useStructuredOutput = !!(request.settings.structuredOutput?.schema &&
228
+ request.settings.structuredOutput.enabled !== false);
229
+ // Initialize Anthropic client
230
+ const anthropic = new sdk_1.default({
231
+ apiKey,
232
+ ...(this.baseURL && { baseURL: this.baseURL }),
233
+ maxRetries: 0, // retries are owned by the unified LLMService retry layer
234
+ });
235
+ // Per-request transport options (abort signal, timeout)
236
+ const requestTransportOptions = {
237
+ ...(options?.signal && { signal: options.signal }),
238
+ ...(options?.timeoutMs !== undefined && { timeout: options.timeoutMs }),
239
+ };
240
+ // Format messages for Anthropic API (Claude has specific requirements)
241
+ const { messages, systemMessage } = this.formatMessagesForAnthropic(request);
242
+ // Prepare API call parameters
243
+ const messageParams = {
244
+ model: request.modelId,
245
+ messages: messages,
246
+ max_tokens: request.settings.maxTokens,
247
+ temperature: request.settings.temperature,
248
+ top_p: request.settings.topP,
249
+ ...(request.settings.topK !== undefined && {
250
+ top_k: request.settings.topK,
251
+ }),
252
+ ...(systemMessage && { system: systemMessage }),
253
+ ...(request.settings.stopSequences.length > 0 && {
254
+ stop_sequences: request.settings.stopSequences,
255
+ }),
256
+ };
257
+ // Handle structured output configuration for Anthropic
258
+ // Note: Structured output requires the beta API endpoint
259
+ if (useStructuredOutput) {
260
+ const so = request.settings.structuredOutput;
261
+ // Anthropic requires additionalProperties: false on all object schemas
262
+ const processedSchema = so.strict !== false
263
+ ? this.addAdditionalPropertiesFalse(so.schema)
264
+ : so.schema;
265
+ // Anthropic's format: output_format.schema is the schema directly
266
+ messageParams.output_format = {
267
+ type: 'json_schema',
268
+ name: so.name,
269
+ schema: processedSchema,
270
+ strict: so.strict !== false,
271
+ };
272
+ }
273
+ // Handle reasoning/thinking configuration for Claude models
274
+ if (request.settings.reasoning && !request.settings.reasoning.exclude) {
275
+ const reasoning = request.settings.reasoning;
276
+ let budgetTokens;
277
+ // Convert reasoning settings to Anthropic's thinking format
278
+ if (reasoning.maxTokens !== undefined) {
279
+ budgetTokens = Math.max(reasoning.maxTokens, 1024); // Minimum 1024
280
+ }
281
+ else if (reasoning.effort) {
282
+ // Convert effort levels to token budgets
283
+ // Max budget for Anthropic is 32000
284
+ const maxBudget = 32000;
285
+ switch (reasoning.effort) {
286
+ case 'high':
287
+ budgetTokens = Math.floor(maxBudget * 0.8);
288
+ break;
289
+ case 'medium':
290
+ budgetTokens = Math.floor(maxBudget * 0.5);
291
+ break;
292
+ case 'low':
293
+ budgetTokens = Math.floor(maxBudget * 0.2);
294
+ break;
295
+ }
296
+ }
297
+ else if (reasoning.enabled !== false) {
298
+ // Use default budget
299
+ budgetTokens = 10000;
300
+ }
301
+ if (budgetTokens !== undefined) {
302
+ // Add thinking configuration to the request
303
+ messageParams.thinking = {
304
+ type: "enabled",
305
+ budget_tokens: Math.min(budgetTokens, 32000), // Cap at max
306
+ };
307
+ }
308
+ }
309
+ return {
310
+ anthropic,
311
+ messageParams,
312
+ requestTransportOptions,
313
+ useStructuredOutput,
314
+ messages,
315
+ };
316
+ }
317
+ mapAnthropicUsage(usage) {
318
+ const promptTokens = usage.input_tokens ?? 0;
319
+ const completionTokens = usage.output_tokens ?? 0;
320
+ return {
321
+ prompt_tokens: promptTokens,
322
+ completion_tokens: completionTokens,
323
+ total_tokens: promptTokens + completionTokens,
324
+ };
325
+ }
326
+ mergeAnthropicUsage(current, next) {
327
+ if (!next) {
328
+ return current;
329
+ }
330
+ return {
331
+ ...(current || {}),
332
+ ...next,
333
+ input_tokens: next.input_tokens ?? current?.input_tokens ?? 0,
334
+ output_tokens: next.output_tokens ?? current?.output_tokens ?? 0,
335
+ };
336
+ }
337
+ createSyntheticMessage(request, accumulator) {
338
+ const content = [];
339
+ if (accumulator.reasoning) {
340
+ content.push({
341
+ type: "thinking",
342
+ thinking: accumulator.reasoning,
343
+ signature: "",
344
+ });
345
+ }
346
+ content.push({
347
+ type: "text",
348
+ text: accumulator.content,
349
+ });
350
+ return {
351
+ id: accumulator.id || `anthropic-stream-${Date.now()}`,
352
+ type: "message",
353
+ role: "assistant",
354
+ model: accumulator.model || request.modelId,
355
+ content,
356
+ stop_reason: accumulator.stopReason,
357
+ stop_sequence: null,
358
+ usage: accumulator.usage,
359
+ };
360
+ }
186
361
  /**
187
362
  * Recursively adds additionalProperties: false to all object schemas
188
363
  * Required by Anthropic's strict mode for structured outputs
@@ -322,9 +497,11 @@ class AnthropicClientAdapter {
322
497
  * @returns Standardized LLM response
323
498
  */
324
499
  createSuccessResponse(completion, request) {
325
- // Anthropic returns content as an array of content blocks
326
- const contentBlock = completion.content[0];
327
- if (!contentBlock || contentBlock.type !== "text") {
500
+ // Anthropic returns content as an array of content blocks. Thinking-capable
501
+ // models may place a thinking block before the text block.
502
+ const textBlocks = completion.content.filter((block) => block.type === "text");
503
+ const textContent = textBlocks.map((block) => block.text || "").join("");
504
+ if (textBlocks.length === 0) {
328
505
  throw new Error("Invalid completion structure from Anthropic API");
329
506
  }
330
507
  // Extract thinking/reasoning content if available
@@ -334,6 +511,13 @@ class AnthropicClientAdapter {
334
511
  if (completion.thinking_content) {
335
512
  reasoning = completion.thinking_content;
336
513
  }
514
+ const thinkingContent = completion.content
515
+ .filter((block) => block.type === "thinking" && typeof block.thinking === "string")
516
+ .map((block) => block.thinking)
517
+ .join("");
518
+ if (!reasoning && thinkingContent) {
519
+ reasoning = thinkingContent;
520
+ }
337
521
  // Check for reasoning details that need to be preserved
338
522
  if (completion.reasoning_details) {
339
523
  reasoning_details = completion.reasoning_details;
@@ -343,7 +527,7 @@ class AnthropicClientAdapter {
343
527
  const choice = {
344
528
  message: {
345
529
  role: "assistant",
346
- content: contentBlock.text,
530
+ content: textContent,
347
531
  },
348
532
  finish_reason: finishReason,
349
533
  index: 0,
@@ -1,4 +1,4 @@
1
- import type { LLMResponse, LLMFailureResponse } from "../types";
1
+ import type { LLMResponse, LLMFailureResponse, LLMStreamEvent } from "../types";
2
2
  import type { ILLMClientAdapter, InternalLLMChatRequest, AdapterRequestOptions } from "./types";
3
3
  import type { Logger } from "../../logging/types";
4
4
  /**
@@ -33,6 +33,7 @@ export declare class GeminiClientAdapter implements ILLMClientAdapter {
33
33
  * @returns Promise resolving to success or failure response
34
34
  */
35
35
  sendMessage(request: InternalLLMChatRequest, apiKey: string, options?: AdapterRequestOptions): Promise<LLMResponse | LLMFailureResponse>;
36
+ streamMessage(request: InternalLLMChatRequest, apiKey: string, options?: AdapterRequestOptions): AsyncIterable<LLMStreamEvent>;
36
37
  /**
37
38
  * Validates Gemini API key format
38
39
  *
@@ -48,6 +49,12 @@ export declare class GeminiClientAdapter implements ILLMClientAdapter {
48
49
  name: string;
49
50
  version: string;
50
51
  };
52
+ private prepareGenerateContentRequest;
53
+ private createTransportState;
54
+ private getTransportErrorContext;
55
+ private updateGeminiAccumulatorMetadata;
56
+ private processGeminiStreamChunk;
57
+ private createSyntheticGeminiResponse;
51
58
  /**
52
59
  * Formats the internal LLM request for Gemini API
53
60
  *