@umituz/react-native-ai-gemini-provider 3.0.0 → 3.0.1
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/package.json +1 -1
- package/src/domain/entities/models.ts +0 -8
- package/src/infrastructure/services/index.ts +0 -1
- package/src/infrastructure/telemetry/index.ts +0 -1
- package/src/infrastructure/utils/async/index.ts +0 -2
- package/src/infrastructure/utils/content-mapper.util.ts +1 -1
- package/src/infrastructure/utils/error-mapper.util.ts +1 -9
- package/src/infrastructure/utils/json-parser.util.ts +1 -50
- package/src/infrastructure/utils/stream-processor.util.ts +3 -90
- package/src/presentation/hooks/index.ts +0 -1
- package/src/infrastructure/interceptors/BaseInterceptor.ts +0 -78
- package/src/infrastructure/interceptors/RequestInterceptors.ts +0 -30
- package/src/infrastructure/interceptors/ResponseInterceptors.ts +0 -35
- package/src/infrastructure/interceptors/index.ts +0 -12
- package/src/infrastructure/utils/index.ts +0 -41
- package/src/infrastructure/utils/validation.ts +0 -175
package/package.json
CHANGED
|
@@ -16,11 +16,3 @@ export const DEFAULT_MODELS = {
|
|
|
16
16
|
/** Default model for text generation */
|
|
17
17
|
TEXT: GEMINI_MODELS.TEXT.FLASH_LITE,
|
|
18
18
|
} as const;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Pricing information for Gemini models
|
|
22
|
-
* Prices are per 1M tokens (USD)
|
|
23
|
-
*/
|
|
24
|
-
export const MODEL_PRICING = {
|
|
25
|
-
[GEMINI_MODELS.TEXT.FLASH_LITE]: { input: 0.10, output: 0.40, freePerDay: 1000 },
|
|
26
|
-
} as const;
|
|
@@ -3,4 +3,3 @@ export { textGeneration } from "./TextGeneration";
|
|
|
3
3
|
export { structuredText } from "./StructuredText";
|
|
4
4
|
export { streaming } from "./Streaming";
|
|
5
5
|
export { geminiProvider, GeminiProvider } from "./GeminiProvider";
|
|
6
|
-
export { BaseGeminiService, type BaseRequestOptions } from "./BaseService";
|
|
@@ -76,7 +76,7 @@ export function createTextContent(
|
|
|
76
76
|
/**
|
|
77
77
|
* Transform SDK candidate to domain format
|
|
78
78
|
*/
|
|
79
|
-
|
|
79
|
+
function transformCandidate(
|
|
80
80
|
candidate: {
|
|
81
81
|
content: { parts: Array<{ text?: string }>; role?: string };
|
|
82
82
|
finishReason?: string;
|
|
@@ -87,7 +87,7 @@ function matchesPattern(message: string, patterns: string[]): boolean {
|
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
function mapGeminiError(error: unknown): GeminiErrorInfo {
|
|
91
91
|
const message = error instanceof Error ? error.message : String(error);
|
|
92
92
|
const statusCode = getStatusCode(error);
|
|
93
93
|
|
|
@@ -114,14 +114,6 @@ export function mapGeminiError(error: unknown): GeminiErrorInfo {
|
|
|
114
114
|
};
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
export function isGeminiErrorRetryable(error: unknown): boolean {
|
|
118
|
-
return mapGeminiError(error).retryable;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function categorizeGeminiError(error: unknown): GeminiErrorType {
|
|
122
|
-
return mapGeminiError(error).type;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
117
|
export function createGeminiError(error: unknown): GeminiError {
|
|
126
118
|
const errorInfo = mapGeminiError(error);
|
|
127
119
|
return GeminiError.fromError(error, errorInfo);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
/**
|
|
7
7
|
* Clean JSON text by removing markdown code blocks and extra whitespace
|
|
8
8
|
*/
|
|
9
|
-
|
|
9
|
+
function cleanJsonText(text: string): string {
|
|
10
10
|
if (!text || typeof text !== "string") {
|
|
11
11
|
return "";
|
|
12
12
|
}
|
|
@@ -39,52 +39,3 @@ export function parseJsonResponse<T>(text: string): T {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
/**
|
|
43
|
-
* Safely parse JSON with optional fallback value
|
|
44
|
-
*/
|
|
45
|
-
export function safeParseJson<T>(
|
|
46
|
-
text: string,
|
|
47
|
-
fallback: T
|
|
48
|
-
): T {
|
|
49
|
-
try {
|
|
50
|
-
return parseJsonResponse<T>(text);
|
|
51
|
-
} catch {
|
|
52
|
-
return fallback;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Extract and parse JSON from a larger text response
|
|
58
|
-
* Looks for JSON objects within markdown code blocks or standalone
|
|
59
|
-
*/
|
|
60
|
-
export function extractJsonFromText<T>(text: string): T | null {
|
|
61
|
-
// Try to find JSON in code blocks first
|
|
62
|
-
const codeBlockMatch = text.match(/```json\s*([\s\S]*?)\s*```/);
|
|
63
|
-
if (codeBlockMatch) {
|
|
64
|
-
try {
|
|
65
|
-
return JSON.parse(codeBlockMatch[1].trim()) as T;
|
|
66
|
-
} catch {
|
|
67
|
-
// Continue to other methods
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Try to find JSON object boundaries
|
|
72
|
-
const firstBrace = text.indexOf("{");
|
|
73
|
-
const lastBrace = text.lastIndexOf("}");
|
|
74
|
-
|
|
75
|
-
if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
|
|
76
|
-
try {
|
|
77
|
-
const jsonStr = text.substring(firstBrace, lastBrace + 1);
|
|
78
|
-
return JSON.parse(jsonStr) as T;
|
|
79
|
-
} catch {
|
|
80
|
-
// Continue to fallback
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Try parsing the whole text as JSON
|
|
85
|
-
try {
|
|
86
|
-
return parseJsonResponse<T>(text);
|
|
87
|
-
} catch {
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
* Reusable stream handling logic
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
interface StreamChunk {
|
|
7
7
|
text: () => string;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
type ChunkCallback = (text: string) => void;
|
|
11
|
+
type ErrorLogger = (error: unknown, context?: string) => void;
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Process async stream with chunk callback
|
|
@@ -67,90 +67,3 @@ function logError(
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
/**
|
|
71
|
-
* Create a buffered stream processor
|
|
72
|
-
* Accumulates chunks until a condition is met
|
|
73
|
-
*/
|
|
74
|
-
export class BufferedStreamProcessor {
|
|
75
|
-
private buffer = "";
|
|
76
|
-
private fullText = "";
|
|
77
|
-
|
|
78
|
-
constructor(
|
|
79
|
-
private onFlush: (text: string) => void,
|
|
80
|
-
private flushCondition: (buffer: string) => boolean = () => false
|
|
81
|
-
) {}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Process a single chunk
|
|
85
|
-
*/
|
|
86
|
-
processChunk(chunk: StreamChunk): void {
|
|
87
|
-
try {
|
|
88
|
-
const chunkText = chunk.text();
|
|
89
|
-
if (!chunkText) return;
|
|
90
|
-
|
|
91
|
-
this.buffer += chunkText;
|
|
92
|
-
this.fullText += chunkText;
|
|
93
|
-
|
|
94
|
-
if (this.flushCondition(this.buffer)) {
|
|
95
|
-
this.flush();
|
|
96
|
-
}
|
|
97
|
-
} catch (error) {
|
|
98
|
-
// Ignore chunk errors
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Flush buffer to callback
|
|
104
|
-
*/
|
|
105
|
-
flush(): void {
|
|
106
|
-
if (this.buffer) {
|
|
107
|
-
try {
|
|
108
|
-
this.onFlush(this.buffer);
|
|
109
|
-
this.buffer = "";
|
|
110
|
-
} catch {
|
|
111
|
-
// Ignore callback errors
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Get accumulated full text
|
|
118
|
-
*/
|
|
119
|
-
getFullText(): string {
|
|
120
|
-
return this.fullText;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Process entire stream
|
|
125
|
-
*/
|
|
126
|
-
async processStream(stream: AsyncIterable<StreamChunk>): Promise<string> {
|
|
127
|
-
for await (const chunk of stream) {
|
|
128
|
-
this.processChunk(chunk);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Flush remaining buffer
|
|
132
|
-
this.flush();
|
|
133
|
-
|
|
134
|
-
return this.fullText;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Common flush conditions
|
|
140
|
-
*/
|
|
141
|
-
export const flushConditions = {
|
|
142
|
-
/**
|
|
143
|
-
* Flush on newline
|
|
144
|
-
*/
|
|
145
|
-
onNewline: (buffer: string): boolean => buffer.includes("\n"),
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Flush when buffer reaches size
|
|
149
|
-
*/
|
|
150
|
-
onSize: (size: number) => (buffer: string): boolean => buffer.length >= size,
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Flush on pattern match
|
|
154
|
-
*/
|
|
155
|
-
onPattern: (pattern: RegExp) => (buffer: string): boolean => pattern.test(buffer),
|
|
156
|
-
};
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Base Interceptor Class
|
|
3
|
-
* Eliminates code duplication between Request and Response interceptors
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { telemetryHooks } from "../telemetry";
|
|
7
|
-
|
|
8
|
-
export type InterceptorErrorStrategy = "fail" | "skip" | "log";
|
|
9
|
-
|
|
10
|
-
export interface BaseContext {
|
|
11
|
-
model: string;
|
|
12
|
-
feature?: string;
|
|
13
|
-
timestamp: number;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export abstract class BaseInterceptor<TContext extends BaseContext> {
|
|
17
|
-
protected interceptors: Array<(context: TContext) => TContext | Promise<TContext>> = [];
|
|
18
|
-
protected errorStrategy: InterceptorErrorStrategy = "fail";
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Register an interceptor
|
|
22
|
-
*/
|
|
23
|
-
use(interceptor: (context: TContext) => TContext | Promise<TContext>): () => void {
|
|
24
|
-
this.interceptors.push(interceptor);
|
|
25
|
-
|
|
26
|
-
// Return unsubscribe function
|
|
27
|
-
return () => {
|
|
28
|
-
const index = this.interceptors.indexOf(interceptor);
|
|
29
|
-
if (index > -1) {
|
|
30
|
-
this.interceptors.splice(index, 1);
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Set error handling strategy
|
|
37
|
-
*/
|
|
38
|
-
setErrorStrategy(strategy: InterceptorErrorStrategy): void {
|
|
39
|
-
this.errorStrategy = strategy;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Clear all interceptors
|
|
44
|
-
*/
|
|
45
|
-
clear(): void {
|
|
46
|
-
this.interceptors = [];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Get interceptor count
|
|
51
|
-
*/
|
|
52
|
-
count(): number {
|
|
53
|
-
return this.interceptors.length;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Handle interceptor error based on strategy
|
|
58
|
-
*/
|
|
59
|
-
protected handleError(context: TContext, error: unknown): void {
|
|
60
|
-
telemetryHooks.logError(
|
|
61
|
-
context.model,
|
|
62
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
63
|
-
context.feature
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
if (this.errorStrategy === "fail") {
|
|
67
|
-
throw new Error(
|
|
68
|
-
`Interceptor failed: ${error instanceof Error ? error.message : String(error)}`
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
// For "skip" and "log", we just continue (error already logged)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Apply interceptors - to be implemented by subclasses
|
|
76
|
-
*/
|
|
77
|
-
abstract apply(context: TContext): Promise<TContext>;
|
|
78
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { BaseInterceptor, type BaseContext } from "./BaseInterceptor";
|
|
2
|
-
|
|
3
|
-
export interface RequestContext extends BaseContext {
|
|
4
|
-
payload: Record<string, unknown>;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export type RequestInterceptor = (context: RequestContext) => RequestContext | Promise<RequestContext>;
|
|
8
|
-
|
|
9
|
-
class RequestInterceptors extends BaseInterceptor<RequestContext> {
|
|
10
|
-
/**
|
|
11
|
-
* Apply all interceptors to a request context
|
|
12
|
-
* Interceptors are called in order (first registered = first called)
|
|
13
|
-
*/
|
|
14
|
-
async apply(context: RequestContext): Promise<RequestContext> {
|
|
15
|
-
let result = context;
|
|
16
|
-
|
|
17
|
-
for (const interceptor of this.interceptors) {
|
|
18
|
-
try {
|
|
19
|
-
result = await interceptor(result);
|
|
20
|
-
} catch (error) {
|
|
21
|
-
this.handleError(context, error);
|
|
22
|
-
// If we get here, strategy was "skip" or "log" - continue with previous result
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return result;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export const requestInterceptors = new RequestInterceptors();
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { BaseInterceptor, type BaseContext } from "./BaseInterceptor";
|
|
2
|
-
|
|
3
|
-
export interface ResponseContext<T = unknown> extends BaseContext {
|
|
4
|
-
data: T;
|
|
5
|
-
duration: number;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export type ResponseInterceptor<T = unknown> = (
|
|
9
|
-
context: ResponseContext<T>,
|
|
10
|
-
) => ResponseContext<T> | Promise<ResponseContext<T>>;
|
|
11
|
-
|
|
12
|
-
class ResponseInterceptors extends BaseInterceptor<ResponseContext<unknown>> {
|
|
13
|
-
/**
|
|
14
|
-
* Apply all interceptors to a response context
|
|
15
|
-
* Interceptors are called in reverse order (last registered = first called)
|
|
16
|
-
*/
|
|
17
|
-
async apply<T>(context: ResponseContext<T>): Promise<ResponseContext<T>> {
|
|
18
|
-
let result: ResponseContext<unknown> = context;
|
|
19
|
-
|
|
20
|
-
// Apply in reverse order (last added = first processed)
|
|
21
|
-
for (let i = this.interceptors.length - 1; i >= 0; i--) {
|
|
22
|
-
const interceptor = this.interceptors[i];
|
|
23
|
-
try {
|
|
24
|
-
result = await interceptor(result);
|
|
25
|
-
} catch (error) {
|
|
26
|
-
this.handleError(context, error);
|
|
27
|
-
// If we get here, strategy was "skip" or "log" - continue with previous result
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return result as ResponseContext<T>;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export const responseInterceptors = new ResponseInterceptors();
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Interceptors Module - Internal Use Only
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export { BaseInterceptor } from "./BaseInterceptor";
|
|
6
|
-
export type { BaseContext, InterceptorErrorStrategy } from "./BaseInterceptor";
|
|
7
|
-
|
|
8
|
-
export { requestInterceptors } from "./RequestInterceptors";
|
|
9
|
-
export type { RequestContext, RequestInterceptor } from "./RequestInterceptors";
|
|
10
|
-
|
|
11
|
-
export { responseInterceptors } from "./ResponseInterceptors";
|
|
12
|
-
export type { ResponseContext, ResponseInterceptor } from "./ResponseInterceptors";
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
export {
|
|
2
|
-
mapGeminiError,
|
|
3
|
-
isGeminiErrorRetryable,
|
|
4
|
-
categorizeGeminiError,
|
|
5
|
-
createGeminiError
|
|
6
|
-
} from "./error-mapper.util";
|
|
7
|
-
|
|
8
|
-
export { extractTextFromResponse } from "./gemini-data-transformer.util";
|
|
9
|
-
|
|
10
|
-
export {
|
|
11
|
-
cleanJsonText,
|
|
12
|
-
parseJsonResponse,
|
|
13
|
-
safeParseJson,
|
|
14
|
-
extractJsonFromText
|
|
15
|
-
} from "./json-parser.util";
|
|
16
|
-
|
|
17
|
-
export {
|
|
18
|
-
toSdkContent,
|
|
19
|
-
createTextContent,
|
|
20
|
-
transformCandidate,
|
|
21
|
-
transformResponse,
|
|
22
|
-
extractTextFromParts
|
|
23
|
-
} from "./content-mapper.util";
|
|
24
|
-
|
|
25
|
-
export {
|
|
26
|
-
validateModelName,
|
|
27
|
-
validateApiKey,
|
|
28
|
-
validateSchema,
|
|
29
|
-
validatePrompt,
|
|
30
|
-
validateOrThrow,
|
|
31
|
-
validators,
|
|
32
|
-
compose,
|
|
33
|
-
type ValidationRule,
|
|
34
|
-
} from "./validation";
|
|
35
|
-
|
|
36
|
-
export {
|
|
37
|
-
executeWithState,
|
|
38
|
-
type AsyncStateCallbacks,
|
|
39
|
-
type AsyncStateSetters,
|
|
40
|
-
type AsyncStateConfig
|
|
41
|
-
} from "./async";
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validation utilities
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export type ValidationRule<T = unknown> = (value: T) => string | null;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Compose multiple validation rules into one
|
|
9
|
-
*/
|
|
10
|
-
export function compose<T>(...rules: ValidationRule<T>[]): ValidationRule<T> {
|
|
11
|
-
return (value: T): string | null => {
|
|
12
|
-
for (const rule of rules) {
|
|
13
|
-
const error = rule(value);
|
|
14
|
-
if (error) return error;
|
|
15
|
-
}
|
|
16
|
-
return null;
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Validate that value is not empty
|
|
22
|
-
*/
|
|
23
|
-
export function required(fieldName: string = "Field"): ValidationRule<string> {
|
|
24
|
-
return (value: string): string | null => {
|
|
25
|
-
if (!value || typeof value !== "string" || value.trim().length === 0) {
|
|
26
|
-
return `${fieldName} is required`;
|
|
27
|
-
}
|
|
28
|
-
return null;
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Validate minimum length
|
|
34
|
-
*/
|
|
35
|
-
export function minLength(min: number, fieldName: string = "Field"): ValidationRule<string> {
|
|
36
|
-
return (value: string): string | null => {
|
|
37
|
-
if (value.trim().length < min) {
|
|
38
|
-
return `${fieldName} must be at least ${min} characters`;
|
|
39
|
-
}
|
|
40
|
-
return null;
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Validate maximum length
|
|
46
|
-
*/
|
|
47
|
-
export function maxLength(max: number, fieldName: string = "Field"): ValidationRule<string> {
|
|
48
|
-
return (value: string): string | null => {
|
|
49
|
-
if (value.length > max) {
|
|
50
|
-
return `${fieldName} must be at most ${max} characters`;
|
|
51
|
-
}
|
|
52
|
-
return null;
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Validate string starts with prefix
|
|
58
|
-
*/
|
|
59
|
-
export function startsWith(prefix: string, fieldName: string = "Field"): ValidationRule<string> {
|
|
60
|
-
return (value: string): string | null => {
|
|
61
|
-
if (!value.startsWith(prefix)) {
|
|
62
|
-
return `${fieldName} must start with "${prefix}"`;
|
|
63
|
-
}
|
|
64
|
-
return null;
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Validate number is in range
|
|
70
|
-
*/
|
|
71
|
-
export function inRange(min: number, max: number, fieldName: string = "Value"): ValidationRule<number> {
|
|
72
|
-
return (value: number): string | null => {
|
|
73
|
-
if (typeof value !== "number" || value < min || value > max) {
|
|
74
|
-
return `${fieldName} must be between ${min} and ${max}`;
|
|
75
|
-
}
|
|
76
|
-
return null;
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Validate object has required properties
|
|
82
|
-
*/
|
|
83
|
-
export function hasProperties(...props: string[]): ValidationRule<Record<string, unknown>> {
|
|
84
|
-
return (value: Record<string, unknown>): string | null => {
|
|
85
|
-
const missing = props.filter((prop) => !(prop in value) || value[prop] === undefined);
|
|
86
|
-
if (missing.length > 0) {
|
|
87
|
-
return `Missing required properties: ${missing.join(", ")}`;
|
|
88
|
-
}
|
|
89
|
-
return null;
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Validate object structure (for schemas)
|
|
95
|
-
*/
|
|
96
|
-
export function isValidSchema(): ValidationRule<Record<string, unknown>> {
|
|
97
|
-
return compose(
|
|
98
|
-
(schema): string | null => {
|
|
99
|
-
if (!schema || typeof schema !== "object") {
|
|
100
|
-
return "Schema must be a non-empty object";
|
|
101
|
-
}
|
|
102
|
-
return null;
|
|
103
|
-
},
|
|
104
|
-
(schema): string | null => {
|
|
105
|
-
if (Object.keys(schema).length === 0) {
|
|
106
|
-
return "Schema must contain at least one property";
|
|
107
|
-
}
|
|
108
|
-
return null;
|
|
109
|
-
},
|
|
110
|
-
hasProperties("type"),
|
|
111
|
-
(schema): string | null => {
|
|
112
|
-
const schemaType = schema.type;
|
|
113
|
-
if (schemaType !== "object" && schemaType !== "array") {
|
|
114
|
-
return `Schema type must be "object" or "array", got "${String(schemaType)}"`;
|
|
115
|
-
}
|
|
116
|
-
return null;
|
|
117
|
-
},
|
|
118
|
-
(schema): string | null => {
|
|
119
|
-
if (schema.type === "object" && !("properties" in schema)) {
|
|
120
|
-
return 'Object schema must have a "properties" field';
|
|
121
|
-
}
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Helper to validate and throw on error
|
|
129
|
-
*/
|
|
130
|
-
export function validateOrThrow<T>(value: T, rule: ValidationRule<T>): void {
|
|
131
|
-
const error = rule(value);
|
|
132
|
-
if (error) {
|
|
133
|
-
throw new Error(error);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Pre-built composite validators
|
|
139
|
-
*/
|
|
140
|
-
export const validators = {
|
|
141
|
-
apiKey: compose(
|
|
142
|
-
required("API key"),
|
|
143
|
-
minLength(10, "API key")
|
|
144
|
-
),
|
|
145
|
-
|
|
146
|
-
modelName: compose(
|
|
147
|
-
required("Model name"),
|
|
148
|
-
startsWith("gemini-", "Model name")
|
|
149
|
-
),
|
|
150
|
-
|
|
151
|
-
prompt: compose(
|
|
152
|
-
required("Prompt"),
|
|
153
|
-
minLength(3, "Prompt")
|
|
154
|
-
),
|
|
155
|
-
|
|
156
|
-
timeout: inRange(1, 300000, "Timeout"),
|
|
157
|
-
|
|
158
|
-
schema: isValidSchema(),
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
export function validateModelName(modelName: string): void {
|
|
162
|
-
validateOrThrow(modelName, validators.modelName);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export function validateApiKey(apiKey: string): void {
|
|
166
|
-
validateOrThrow(apiKey, validators.apiKey);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export function validateSchema(schema: Record<string, unknown>): void {
|
|
170
|
-
validateOrThrow(schema, validators.schema);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function validatePrompt(prompt: string): void {
|
|
174
|
-
validateOrThrow(prompt, validators.prompt);
|
|
175
|
-
}
|