@umituz/react-native-ai-gemini-provider 3.0.40 → 3.0.42

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.
Files changed (89) hide show
  1. package/package.json +1 -4
  2. package/src/application/builders/config-builder.ts +102 -0
  3. package/src/application/builders/index.ts +8 -0
  4. package/src/application/dtos/generation-request.dto.ts +89 -0
  5. package/src/application/dtos/index.ts +8 -0
  6. package/src/application/index.ts +16 -0
  7. package/src/application/providers/gemini-provider.ts +135 -0
  8. package/src/application/providers/index.ts +6 -0
  9. package/src/application/use-cases/generate-json.use-case.ts +73 -0
  10. package/src/application/use-cases/generate-text.use-case.ts +81 -0
  11. package/src/application/use-cases/index.ts +20 -0
  12. package/src/application/use-cases/stream-content.use-case.ts +46 -0
  13. package/src/domain/entities/error.types.ts +0 -5
  14. package/src/domain/entities/gemini.types.ts +3 -1
  15. package/src/domain/index.ts +16 -0
  16. package/src/domain/repositories/index.ts +19 -0
  17. package/src/domain/repositories/streaming.repository.ts +41 -0
  18. package/src/domain/repositories/structured-text.repository.ts +41 -0
  19. package/src/domain/repositories/text-generation.repository.ts +38 -0
  20. package/src/domain/services/validation.service.ts +157 -0
  21. package/src/domain/value-objects/api-key.vo.ts +55 -0
  22. package/src/domain/value-objects/index.ts +8 -0
  23. package/src/domain/value-objects/model-name.vo.ts +66 -0
  24. package/src/domain/value-objects/timeout.vo.ts +69 -0
  25. package/src/index.ts +110 -25
  26. package/src/infrastructure/external/gemini-client.singleton.ts +49 -0
  27. package/src/infrastructure/external/gemini-sdk.adapter.ts +143 -0
  28. package/src/infrastructure/external/index.ts +7 -0
  29. package/src/infrastructure/index.ts +16 -0
  30. package/src/infrastructure/mappers/content.mapper.ts +80 -0
  31. package/src/infrastructure/mappers/error.mapper.ts +152 -0
  32. package/src/infrastructure/mappers/index.ts +7 -0
  33. package/src/infrastructure/mappers/response.mapper.ts +165 -0
  34. package/src/infrastructure/repositories/base-gemini.repository.ts +94 -0
  35. package/src/infrastructure/repositories/gemini-streaming.repository.impl.ts +119 -0
  36. package/src/infrastructure/repositories/gemini-structured-text.repository.impl.ts +108 -0
  37. package/src/infrastructure/repositories/gemini-text.repository.impl.ts +76 -0
  38. package/src/infrastructure/repositories/index.ts +10 -0
  39. package/src/infrastructure/utils/index.ts +6 -0
  40. package/src/presentation/hooks/index.ts +8 -0
  41. package/src/presentation/hooks/use-gemini.hook.ts +181 -0
  42. package/src/presentation/hooks/use-operation-manager.hook.ts +67 -0
  43. package/src/presentation/index.ts +10 -0
  44. package/src/presentation/providers/gemini-provider.tsx +93 -0
  45. package/src/presentation/providers/index.ts +10 -0
  46. package/dist/domain/entities/error.types.d.ts +0 -96
  47. package/dist/domain/entities/gemini.types.d.ts +0 -128
  48. package/dist/domain/entities/index.d.ts +0 -6
  49. package/dist/domain/entities/models.d.ts +0 -23
  50. package/dist/index.d.ts +0 -15
  51. package/dist/infrastructure/services/BaseService.d.ts +0 -29
  52. package/dist/infrastructure/services/ChatSession.d.ts +0 -63
  53. package/dist/infrastructure/services/GeminiClient.d.ts +0 -16
  54. package/dist/infrastructure/services/GeminiProvider.d.ts +0 -10
  55. package/dist/infrastructure/services/Streaming.d.ts +0 -7
  56. package/dist/infrastructure/services/StructuredText.d.ts +0 -6
  57. package/dist/infrastructure/services/TextGeneration.d.ts +0 -8
  58. package/dist/infrastructure/services/index.d.ts +0 -6
  59. package/dist/infrastructure/telemetry/TelemetryHooks.d.ts +0 -41
  60. package/dist/infrastructure/telemetry/index.d.ts +0 -4
  61. package/dist/infrastructure/utils/async/execute-state.util.d.ts +0 -49
  62. package/dist/infrastructure/utils/async/index.d.ts +0 -4
  63. package/dist/infrastructure/utils/content-mapper.util.d.ts +0 -45
  64. package/dist/infrastructure/utils/error-mapper.util.d.ts +0 -2
  65. package/dist/infrastructure/utils/gemini-data-transformer.util.d.ts +0 -2
  66. package/dist/infrastructure/utils/json-parser.util.d.ts +0 -9
  67. package/dist/infrastructure/utils/stream-processor.util.d.ts +0 -14
  68. package/dist/presentation/hooks/index.d.ts +0 -1
  69. package/dist/presentation/hooks/useGemini.d.ts +0 -17
  70. package/dist/presentation/hooks/useOperationManager.d.ts +0 -23
  71. package/dist/providers/ConfigBuilder.d.ts +0 -46
  72. package/dist/providers/ProviderFactory.d.ts +0 -25
  73. package/dist/providers/index.d.ts +0 -7
  74. package/src/infrastructure/services/BaseService.ts +0 -53
  75. package/src/infrastructure/services/ChatSession.ts +0 -199
  76. package/src/infrastructure/services/GeminiClient.ts +0 -112
  77. package/src/infrastructure/services/Streaming.ts +0 -56
  78. package/src/infrastructure/services/StructuredText.ts +0 -57
  79. package/src/infrastructure/services/TextGeneration.ts +0 -57
  80. package/src/infrastructure/telemetry/TelemetryHooks.ts +0 -110
  81. package/src/infrastructure/utils/async/execute-state.util.ts +0 -93
  82. package/src/infrastructure/utils/content-mapper.util.ts +0 -175
  83. package/src/infrastructure/utils/error-mapper.util.ts +0 -145
  84. package/src/infrastructure/utils/gemini-data-transformer.util.ts +0 -40
  85. package/src/infrastructure/utils/text-calculations.util.ts +0 -70
  86. package/src/presentation/hooks/useGemini.ts +0 -125
  87. package/src/presentation/hooks/useOperationManager.ts +0 -88
  88. package/src/providers/ConfigBuilder.ts +0 -112
  89. package/src/providers/ProviderFactory.ts +0 -65
@@ -1,110 +0,0 @@
1
-
2
- export interface TelemetryEvent {
3
- type: "error";
4
- timestamp: number;
5
- model?: string;
6
- feature?: string;
7
- metadata?: Record<string, unknown>;
8
- }
9
-
10
- export type TelemetryListener = (event: TelemetryEvent) => void;
11
-
12
- class TelemetryHooks {
13
- private listeners: TelemetryListener[] = [];
14
- private failedListeners: Set<TelemetryListener> = new Set();
15
- private readonly MAX_FAILURES = 3;
16
- private listenerFailureCounts = new Map<TelemetryListener, number>();
17
-
18
- /**
19
- * Register a telemetry listener
20
- *
21
- * @returns Unsubscribe function - IMPORTANT: Call this when done listening to prevent memory leaks
22
- *
23
- * @example
24
- * ```ts
25
- * const unsubscribe = telemetryHooks.subscribe((event) => console.log(event));
26
- * // ... later when done
27
- * unsubscribe(); // Prevents memory leak
28
- * ```
29
- */
30
- subscribe(listener: TelemetryListener): () => void {
31
- this.listeners.push(listener);
32
- // Remove from failed listeners on new subscription (in case it's being re-added)
33
- this.failedListeners.delete(listener);
34
- this.listenerFailureCounts.set(listener, 0);
35
-
36
- return () => {
37
- const index = this.listeners.indexOf(listener);
38
- if (index > -1) {
39
- this.listeners.splice(index, 1);
40
- }
41
- // Clean up failure tracking when listener is removed
42
- this.failedListeners.delete(listener);
43
- this.listenerFailureCounts.delete(listener);
44
- };
45
- }
46
-
47
- /**
48
- * Emit a telemetry event to all listeners
49
- */
50
- emit(event: TelemetryEvent): void {
51
- // Snapshot to prevent mutation during iteration
52
- const snapshot = [...this.listeners];
53
- for (const listener of snapshot) {
54
- // Skip listeners that have failed too many times
55
- if (this.failedListeners.has(listener)) {
56
- continue;
57
- }
58
-
59
- try {
60
- listener(event);
61
- // Reset failure count and remove from failed listeners on success
62
- const previousFailures = this.listenerFailureCounts.get(listener) || 0;
63
- if (previousFailures > 0) {
64
- this.listenerFailureCounts.set(listener, 0);
65
- this.failedListeners.delete(listener);
66
- }
67
- } catch (error) {
68
- // Track failures
69
- const failureCount = (this.listenerFailureCounts.get(listener) || 0) + 1;
70
- this.listenerFailureCounts.set(listener, failureCount);
71
-
72
- // If listener fails too many times, blacklist it
73
- if (failureCount >= this.MAX_FAILURES) {
74
- this.failedListeners.add(listener);
75
- }
76
- }
77
- }
78
- }
79
-
80
- /**
81
- * Log error
82
- */
83
- logError(model: string, error: Error, feature?: string): void {
84
- this.emit({
85
- type: "error",
86
- timestamp: Date.now(),
87
- model,
88
- feature,
89
- metadata: {
90
- error: error.message,
91
- errorType: error.name,
92
- },
93
- });
94
- }
95
-
96
- /**
97
- * Remove a specific listener
98
- */
99
- unsubscribe(listener: TelemetryListener): void {
100
- const index = this.listeners.indexOf(listener);
101
- if (index > -1) {
102
- this.listeners.splice(index, 1);
103
- }
104
- // Clean up failure tracking
105
- this.failedListeners.delete(listener);
106
- this.listenerFailureCounts.delete(listener);
107
- }
108
- }
109
-
110
- export const telemetryHooks = new TelemetryHooks();
@@ -1,93 +0,0 @@
1
- /**
2
- * Async State Execution Utilities
3
- * Utilities for managing asynchronous operation state
4
- */
5
-
6
- /**
7
- * Callbacks for async operation outcomes
8
- */
9
- export interface AsyncStateCallbacks<T = string> {
10
- onSuccess?: (result: T) => void;
11
- onError?: (error: string) => void;
12
- }
13
-
14
- /**
15
- * Setter functions for updating state
16
- */
17
- export interface AsyncStateSetters<T = string, U = unknown> {
18
- setIsLoading: (value: boolean) => void;
19
- setError: (value: string | null) => void;
20
- setResult: (value: T | null) => void;
21
- setSecondaryResult?: (value: U | null) => void;
22
- }
23
-
24
- /**
25
- * Configuration for executeWithState
26
- */
27
- export interface AsyncStateConfig<T = string> {
28
- resetState?: boolean;
29
- transformResult?: (result: T) => T;
30
- }
31
-
32
- /**
33
- * Execute an async operation with automatic state management
34
- *
35
- * @param setters - State setter functions
36
- * @param callbacks - Optional callbacks for success/error
37
- * @param execute - The async operation to execute
38
- * @param onResult - Function to handle successful result
39
- * @param config - Optional configuration
40
- *
41
- * @returns The result or null if failed/aborted
42
- *
43
- * @example
44
- * ```ts
45
- * const result = await executeWithState(
46
- * { setIsLoading, setError, setResult },
47
- * { onSuccess: console.log },
48
- * () => apiCall(),
49
- * (data) => setResult(data)
50
- * );
51
- * ```
52
- */
53
- export async function executeWithState<T, U = unknown>(
54
- setters: AsyncStateSetters<T, U>,
55
- callbacks: AsyncStateCallbacks<T>,
56
- execute: () => Promise<T>,
57
- onResult: (result: T) => void,
58
- config: AsyncStateConfig<T> = {},
59
- ): Promise<T | null> {
60
- const {
61
- resetState = true,
62
- transformResult,
63
- } = config;
64
-
65
- if (resetState) {
66
- setters.setError(null);
67
- setters.setResult(null);
68
- setters.setSecondaryResult?.(null);
69
- }
70
-
71
- setters.setIsLoading(true);
72
-
73
- try {
74
- const result = await execute();
75
-
76
- // Apply transformation if provided
77
- const finalResult = transformResult ? transformResult(result) : result;
78
-
79
- onResult(finalResult);
80
- callbacks.onSuccess?.(finalResult);
81
-
82
- return finalResult;
83
- } catch (err) {
84
- const errorMessage = err instanceof Error ? err.message : "Operation failed";
85
-
86
- setters.setError(errorMessage);
87
- callbacks.onError?.(errorMessage);
88
-
89
- return null;
90
- } finally {
91
- setters.setIsLoading(false);
92
- }
93
- }
@@ -1,175 +0,0 @@
1
- /**
2
- * Content Mapper Utilities
3
- * Handles transformation between domain content and SDK format
4
- */
5
-
6
- import type { Part } from "@google/generative-ai";
7
- import type {
8
- GeminiContent,
9
- GeminiPart,
10
- GeminiFinishReason,
11
- GeminiSafetyRating,
12
- GeminiResponse,
13
- GeminiHarmCategory,
14
- } from "../../domain/entities";
15
-
16
- const VALID_FINISH_REASONS: readonly string[] = [
17
- "FINISH_REASON_UNSPECIFIED",
18
- "STOP",
19
- "MAX_TOKENS",
20
- "SAFETY",
21
- "RECITATION",
22
- "OTHER",
23
- ] as const;
24
-
25
- const VALID_HARM_CATEGORIES: readonly string[] = [
26
- "HARM_CATEGORY_HARASSMENT",
27
- "HARM_CATEGORY_HATE_SPEECH",
28
- "HARM_CATEGORY_SEXUALLY_EXPLICIT",
29
- "HARM_CATEGORY_DANGEROUS_CONTENT",
30
- ] as const;
31
-
32
- const VALID_PROBABILITIES: readonly string[] = [
33
- "NEGLIGIBLE",
34
- "LOW",
35
- "MEDIUM",
36
- "HIGH",
37
- ] as const;
38
-
39
- function isValidFinishReason(value: string): value is GeminiFinishReason {
40
- return VALID_FINISH_REASONS.includes(value);
41
- }
42
-
43
- function isValidHarmCategory(value: string): value is GeminiHarmCategory {
44
- return VALID_HARM_CATEGORIES.includes(value);
45
- }
46
-
47
- function isValidProbability(value: string): value is GeminiSafetyRating["probability"] {
48
- return VALID_PROBABILITIES.includes(value);
49
- }
50
-
51
- /**
52
- * Convert domain content to SDK format
53
- * Preserves both text and inlineData parts
54
- */
55
- export function toSdkContent(contents: GeminiContent[]): Array<{
56
- role: string;
57
- parts: Part[];
58
- }> {
59
- return contents.map((content) => ({
60
- role: content.role || "user",
61
- parts: content.parts.map((part) => {
62
- if ("text" in part) {
63
- return { text: part.text } as Part;
64
- }
65
- // For inlineData and other part types, cast to SDK Part type
66
- return part as Part;
67
- }),
68
- }));
69
- }
70
-
71
- /**
72
- * Create a simple text content
73
- */
74
- export function createTextContent(
75
- text: string,
76
- role: "user" | "model" = "user"
77
- ): GeminiContent {
78
- return {
79
- parts: [{ text }],
80
- role,
81
- };
82
- }
83
-
84
- /**
85
- * Transform SDK candidate to domain format
86
- */
87
- function transformCandidate(
88
- candidate: {
89
- content: { parts: Array<{ text?: string }>; role?: string };
90
- finishReason?: string;
91
- safetyRatings?: Array<{
92
- category: string;
93
- probability: string;
94
- }>;
95
- }
96
- ): {
97
- content: GeminiContent;
98
- finishReason?: GeminiFinishReason;
99
- safetyRatings?: GeminiSafetyRating[];
100
- } {
101
- const transformedParts: GeminiPart[] = [];
102
-
103
- for (const part of candidate.content.parts) {
104
- if ("text" in part && typeof part.text === "string") {
105
- transformedParts.push({ text: part.text });
106
- }
107
- }
108
-
109
- const finishReason: GeminiFinishReason | undefined =
110
- candidate.finishReason && isValidFinishReason(candidate.finishReason)
111
- ? candidate.finishReason
112
- : undefined;
113
-
114
- const safetyRatings: GeminiSafetyRating[] | undefined = candidate.safetyRatings
115
- ? candidate.safetyRatings
116
- .filter((rating) => isValidHarmCategory(rating.category) && isValidProbability(rating.probability))
117
- .map((rating) => ({
118
- category: rating.category as GeminiHarmCategory,
119
- probability: rating.probability as GeminiSafetyRating["probability"],
120
- }))
121
- : undefined;
122
-
123
- const role = candidate.content.role === "user" || candidate.content.role === "model"
124
- ? candidate.content.role
125
- : "model";
126
-
127
- return {
128
- content: {
129
- parts: transformedParts,
130
- role,
131
- },
132
- finishReason,
133
- safetyRatings,
134
- };
135
- }
136
-
137
- /**
138
- * Transform SDK response to domain format
139
- */
140
- export function transformResponse(
141
- response: {
142
- candidates?: Array<{
143
- content: { parts: Array<{ text?: string }>; role?: string };
144
- finishReason?: string;
145
- safetyRatings?: Array<{
146
- category: string;
147
- probability: string;
148
- }>;
149
- }>;
150
- usageMetadata?: {
151
- promptTokenCount?: number;
152
- candidatesTokenCount?: number;
153
- totalTokenCount?: number;
154
- };
155
- }
156
- ): GeminiResponse {
157
- return {
158
- candidates: response.candidates?.map(transformCandidate),
159
- usageMetadata: response.usageMetadata ? {
160
- promptTokenCount: response.usageMetadata.promptTokenCount,
161
- candidatesTokenCount: response.usageMetadata.candidatesTokenCount,
162
- totalTokenCount: response.usageMetadata.totalTokenCount,
163
- } : undefined,
164
- };
165
- }
166
-
167
- /**
168
- * Extract text from content parts
169
- */
170
- export function extractTextFromParts(parts: GeminiPart[] | undefined): string {
171
- if (!parts || parts.length === 0) return "";
172
- return parts
173
- .map((part) => ("text" in part ? (part.text || "") : ""))
174
- .join("");
175
- }
@@ -1,145 +0,0 @@
1
-
2
- import {
3
- GeminiErrorType,
4
- type GeminiErrorInfo,
5
- type GeminiApiError,
6
- GeminiError,
7
- } from "../../domain/entities";
8
-
9
- const ERROR_PATTERNS: Array<{
10
- pattern: RegExp | string[];
11
- type: GeminiErrorType;
12
- retryable: boolean;
13
- }> = [
14
- {
15
- pattern: ["quota", "resource exhausted", "429"],
16
- type: GeminiErrorType.QUOTA_EXCEEDED,
17
- retryable: true,
18
- },
19
- {
20
- pattern: ["rate limit", "too many requests"],
21
- type: GeminiErrorType.RATE_LIMIT,
22
- retryable: true,
23
- },
24
- {
25
- pattern: ["unauthorized", "invalid api key", "401", "403", "permission"],
26
- type: GeminiErrorType.AUTHENTICATION,
27
- retryable: false,
28
- },
29
- {
30
- pattern: ["safety", "safety filter", "harmful", "blocked by safety"],
31
- type: GeminiErrorType.SAFETY,
32
- retryable: false,
33
- },
34
- {
35
- pattern: ["model not found", "404", "not found"],
36
- type: GeminiErrorType.MODEL_NOT_FOUND,
37
- retryable: false,
38
- },
39
- {
40
- pattern: ["network", "fetch failed", "connection", "socket"],
41
- type: GeminiErrorType.NETWORK,
42
- retryable: true,
43
- },
44
- {
45
- pattern: ["timeout", "timed out"],
46
- type: GeminiErrorType.TIMEOUT,
47
- retryable: true,
48
- },
49
- {
50
- pattern: ["500", "502", "503", "504", "internal server", "unavailable"],
51
- type: GeminiErrorType.SERVER,
52
- retryable: true,
53
- },
54
- {
55
- pattern: ["invalid", "bad request", "400"],
56
- type: GeminiErrorType.VALIDATION,
57
- retryable: false,
58
- },
59
- ];
60
-
61
- function getStatusCode(error: unknown): number | undefined {
62
- if (error && typeof error === "object") {
63
- const err = error as Record<string, unknown>;
64
- if (typeof err.status === "number") return err.status;
65
- if (typeof err.statusCode === "number") return err.statusCode;
66
-
67
- const response = err.response as GeminiApiError | undefined;
68
- if (response?.error?.code) return response.error.code;
69
- }
70
- return undefined;
71
- }
72
-
73
- function matchesPattern(message: string, patterns: string[]): boolean {
74
- const lower = message.toLowerCase();
75
-
76
- return patterns.some((pattern) => {
77
- const lowerPattern = pattern.toLowerCase();
78
-
79
- // Use word boundary matching for accuracy
80
- // This prevents "invalid" from matching "valid"
81
- const words = lowerPattern.split(/\s+/);
82
- return words.every((word) => {
83
- const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
84
- return new RegExp(`\\b${escaped}\\b`, 'i').test(lower);
85
- });
86
- });
87
- }
88
-
89
- /** Map HTTP status codes to error types as a primary classification signal */
90
- const STATUS_CODE_MAP: Record<number, { type: GeminiErrorType; retryable: boolean }> = {
91
- 400: { type: GeminiErrorType.VALIDATION, retryable: false },
92
- 401: { type: GeminiErrorType.AUTHENTICATION, retryable: false },
93
- 403: { type: GeminiErrorType.AUTHENTICATION, retryable: false },
94
- 404: { type: GeminiErrorType.MODEL_NOT_FOUND, retryable: false },
95
- 429: { type: GeminiErrorType.RATE_LIMIT, retryable: true },
96
- 500: { type: GeminiErrorType.SERVER, retryable: true },
97
- 502: { type: GeminiErrorType.SERVER, retryable: true },
98
- 503: { type: GeminiErrorType.SERVER, retryable: true },
99
- 504: { type: GeminiErrorType.SERVER, retryable: true },
100
- };
101
-
102
- function mapGeminiError(error: unknown): GeminiErrorInfo {
103
- const message = error instanceof Error ? error.message : String(error);
104
- const statusCode = getStatusCode(error);
105
-
106
- // Primary: classify by HTTP status code when available
107
- if (statusCode && STATUS_CODE_MAP[statusCode]) {
108
- const { type, retryable } = STATUS_CODE_MAP[statusCode];
109
- return {
110
- type,
111
- messageKey: `error.gemini.${type.toLowerCase()}`,
112
- retryable,
113
- originalError: error,
114
- statusCode,
115
- };
116
- }
117
-
118
- // Secondary: classify by error message pattern
119
- for (const { pattern, type, retryable } of ERROR_PATTERNS) {
120
- const patterns = Array.isArray(pattern) ? pattern : [pattern.source];
121
-
122
- if (matchesPattern(message, patterns)) {
123
- return {
124
- type,
125
- messageKey: `error.gemini.${type.toLowerCase()}`,
126
- retryable,
127
- originalError: error,
128
- statusCode,
129
- };
130
- }
131
- }
132
-
133
- return {
134
- type: GeminiErrorType.UNKNOWN,
135
- messageKey: "error.gemini.unknown",
136
- retryable: false,
137
- originalError: error,
138
- statusCode,
139
- };
140
- }
141
-
142
- export function createGeminiError(error: unknown): GeminiError {
143
- const errorInfo = mapGeminiError(error);
144
- return GeminiError.fromError(error, errorInfo);
145
- }
@@ -1,40 +0,0 @@
1
-
2
- import type { GeminiResponse } from "../../domain/entities";
3
-
4
-
5
- export function extractTextFromResponse(response: GeminiResponse): string {
6
- if (!response.candidates || response.candidates.length === 0) {
7
- throw new Error("No response candidates");
8
- }
9
-
10
- const candidate = response.candidates[0];
11
-
12
- // Handle all finish reasons appropriately
13
- switch (candidate.finishReason) {
14
- case "SAFETY":
15
- throw new Error("Content blocked by safety filters");
16
- case "RECITATION":
17
- throw new Error("Content blocked due to recitation concerns");
18
- case "MAX_TOKENS":
19
- case "FINISH_REASON_UNSPECIFIED":
20
- case "OTHER":
21
- case "STOP":
22
- case undefined:
23
- // Continue to extract text
24
- break;
25
- }
26
-
27
- if (!candidate.content?.parts) {
28
- throw new Error("No content in response candidate");
29
- }
30
-
31
- const textPart = candidate.content.parts.find(
32
- (p): p is { text: string } => "text" in p && typeof p.text === "string",
33
- );
34
-
35
- if (!textPart) {
36
- throw new Error("No text in response");
37
- }
38
-
39
- return textPart.text;
40
- }
@@ -1,70 +0,0 @@
1
- /**
2
- * Text Calculation Utilities
3
- * Reusable functions for text length and character budget calculations
4
- */
5
-
6
- /**
7
- * Calculate total character count in an array of text objects
8
- * @param items - Array of objects with text content
9
- * @param getText - Function to extract text from each item
10
- * @returns Total character count
11
- */
12
- export function calculateTotalChars<T>(
13
- items: readonly T[],
14
- getItemLength: (item: T) => number
15
- ): number {
16
- return items.reduce((sum, item) => sum + getItemLength(item), 0);
17
- }
18
-
19
- /**
20
- * Check if adding an item would exceed the character budget
21
- * @param currentTotal - Current total character count
22
- * @param itemLength - Length of item to add
23
- * @param maxBudget - Maximum allowed budget
24
- * @returns true if within budget, false otherwise
25
- */
26
- export function fitsWithinBudget(
27
- currentTotal: number,
28
- itemLength: number,
29
- maxBudget: number
30
- ): boolean {
31
- return currentTotal + itemLength <= maxBudget;
32
- }
33
-
34
- /**
35
- * Trim array to fit within character budget while keeping minimum items
36
- * Keeps the last `minItems` regardless of budget, then adds as many as possible from the end
37
- *
38
- * @param items - Array to trim
39
- * @param getItemLength - Function to get length of each item
40
- * @param maxBudget - Maximum total length allowed
41
- * @param minItems - Minimum number of items to keep (from the end)
42
- * @returns Trimmed array
43
- */
44
- export function trimArrayByCharBudget<T>(
45
- items: readonly T[],
46
- getItemLength: (item: T) => number,
47
- maxBudget: number,
48
- minItems: number
49
- ): T[] {
50
- if (items.length <= minItems) return [...items];
51
-
52
- // First, guarantee minimum items (from the end)
53
- const guaranteedMin = items.slice(-minItems);
54
- const remaining = items.slice(0, -minItems);
55
-
56
- // Calculate current total
57
- let totalChars = calculateTotalChars(guaranteedMin, getItemLength);
58
- const trimmed: T[] = [...guaranteedMin];
59
-
60
- // Add older items in reverse order until budget is exceeded
61
- for (let i = remaining.length - 1; i >= 0; i--) {
62
- const itemLength = getItemLength(remaining[i]);
63
- if (!fitsWithinBudget(totalChars, itemLength, maxBudget)) break;
64
-
65
- trimmed.unshift(remaining[i]);
66
- totalChars += itemLength;
67
- }
68
-
69
- return trimmed;
70
- }