@plasius/ai 1.1.4 → 1.1.6
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/CHANGELOG.md +22 -2
- package/README.md +101 -2
- package/dist/components/pixelverse/balance.d.ts +6 -2
- package/dist/components/pixelverse/balance.d.ts.map +1 -1
- package/dist/components/pixelverse/balance.js +13 -23
- package/dist/components/pixelverse/index.d.ts +1 -1
- package/dist/components/pixelverse/index.d.ts.map +1 -1
- package/dist/components/pixelverse/index.js +1 -1
- package/dist/components/pixelverse/video-generation-editor.d.ts +10 -0
- package/dist/components/pixelverse/video-generation-editor.d.ts.map +1 -0
- package/dist/components/pixelverse/video-generation-editor.js +79 -0
- package/dist/platform/adapter-platform.d.ts +60 -0
- package/dist/platform/adapter-platform.d.ts.map +1 -0
- package/dist/platform/adapter-platform.js +222 -0
- package/dist/platform/gemini-adapter.d.ts +15 -0
- package/dist/platform/gemini-adapter.d.ts.map +1 -0
- package/dist/platform/gemini-adapter.js +293 -0
- package/dist/platform/http-resilience.d.ts +19 -0
- package/dist/platform/http-resilience.d.ts.map +1 -0
- package/dist/platform/http-resilience.js +126 -0
- package/dist/platform/index.d.ts +22 -1
- package/dist/platform/index.d.ts.map +1 -1
- package/dist/platform/index.js +24 -0
- package/dist/platform/openai-adapter.d.ts +24 -0
- package/dist/platform/openai-adapter.d.ts.map +1 -0
- package/dist/platform/openai-adapter.js +398 -0
- package/dist/platform/video-provider-adapter.d.ts +54 -0
- package/dist/platform/video-provider-adapter.d.ts.map +1 -0
- package/dist/platform/video-provider-adapter.js +165 -0
- package/dist/platform/video-provider-platform.d.ts +13 -0
- package/dist/platform/video-provider-platform.d.ts.map +1 -0
- package/dist/platform/video-provider-platform.js +102 -0
- package/dist-cjs/components/pixelverse/balance.d.ts +6 -2
- package/dist-cjs/components/pixelverse/balance.d.ts.map +1 -1
- package/dist-cjs/components/pixelverse/balance.js +13 -23
- package/dist-cjs/components/pixelverse/index.d.ts +1 -1
- package/dist-cjs/components/pixelverse/index.d.ts.map +1 -1
- package/dist-cjs/components/pixelverse/index.js +1 -1
- package/dist-cjs/components/pixelverse/video-generation-editor.d.ts +10 -0
- package/dist-cjs/components/pixelverse/video-generation-editor.d.ts.map +1 -0
- package/dist-cjs/components/pixelverse/video-generation-editor.js +85 -0
- package/dist-cjs/platform/adapter-platform.d.ts +60 -0
- package/dist-cjs/platform/adapter-platform.d.ts.map +1 -0
- package/dist-cjs/platform/adapter-platform.js +225 -0
- package/dist-cjs/platform/gemini-adapter.d.ts +15 -0
- package/dist-cjs/platform/gemini-adapter.d.ts.map +1 -0
- package/dist-cjs/platform/gemini-adapter.js +296 -0
- package/dist-cjs/platform/http-resilience.d.ts +19 -0
- package/dist-cjs/platform/http-resilience.d.ts.map +1 -0
- package/dist-cjs/platform/http-resilience.js +129 -0
- package/dist-cjs/platform/index.d.ts +22 -1
- package/dist-cjs/platform/index.d.ts.map +1 -1
- package/dist-cjs/platform/index.js +30 -1
- package/dist-cjs/platform/openai-adapter.d.ts +24 -0
- package/dist-cjs/platform/openai-adapter.d.ts.map +1 -0
- package/dist-cjs/platform/openai-adapter.js +401 -0
- package/dist-cjs/platform/video-provider-adapter.d.ts +54 -0
- package/dist-cjs/platform/video-provider-adapter.d.ts.map +1 -0
- package/dist-cjs/platform/video-provider-adapter.js +168 -0
- package/dist-cjs/platform/video-provider-platform.d.ts +13 -0
- package/dist-cjs/platform/video-provider-platform.d.ts.map +1 -0
- package/dist-cjs/platform/video-provider-platform.js +105 -0
- package/docs/api-reference.md +59 -0
- package/docs/architecture.md +5 -1
- package/docs/providers.md +24 -6
- package/package.json +6 -6
- package/src/components/pixelverse/balance.tsx +22 -35
- package/src/components/pixelverse/index.ts +1 -1
- package/src/components/pixelverse/video-generation-editor.tsx +164 -0
- package/src/platform/adapter-platform.ts +440 -0
- package/src/platform/gemini-adapter.ts +391 -0
- package/src/platform/http-resilience.ts +198 -0
- package/src/platform/index.ts +68 -0
- package/src/platform/openai-adapter.ts +552 -0
- package/src/platform/video-provider-adapter.ts +303 -0
- package/src/platform/video-provider-platform.ts +208 -0
- package/dist/components/pixelverse/pixelverseeditor.d.ts +0 -16
- package/dist/components/pixelverse/pixelverseeditor.d.ts.map +0 -1
- package/dist/components/pixelverse/pixelverseeditor.js +0 -21
- package/dist/platform/openai.d.ts +0 -8
- package/dist/platform/openai.d.ts.map +0 -1
- package/dist/platform/openai.js +0 -61
- package/dist/platform/pixelverse.d.ts +0 -6
- package/dist/platform/pixelverse.d.ts.map +0 -1
- package/dist/platform/pixelverse.js +0 -196
- package/dist-cjs/components/pixelverse/pixelverseeditor.d.ts +0 -16
- package/dist-cjs/components/pixelverse/pixelverseeditor.d.ts.map +0 -1
- package/dist-cjs/components/pixelverse/pixelverseeditor.js +0 -27
- package/dist-cjs/platform/openai.d.ts +0 -8
- package/dist-cjs/platform/openai.d.ts.map +0 -1
- package/dist-cjs/platform/openai.js +0 -67
- package/dist-cjs/platform/pixelverse.d.ts +0 -6
- package/dist-cjs/platform/pixelverse.d.ts.map +0 -1
- package/dist-cjs/platform/pixelverse.js +0 -199
- package/src/components/pixelverse/pixelverseeditor.mocule.css +0 -0
- package/src/components/pixelverse/pixelverseeditor.tsx +0 -74
- package/src/platform/openai.ts +0 -123
- package/src/platform/pixelverse.ts +0 -309
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AICapability,
|
|
5
|
+
type AdapterChatRequest,
|
|
6
|
+
type AdapterGenerateImageRequest,
|
|
7
|
+
type AdapterGenerateModelRequest,
|
|
8
|
+
type AICapabilityAdapter,
|
|
9
|
+
type ChatCompletion,
|
|
10
|
+
type Completion,
|
|
11
|
+
type ImageCompletion,
|
|
12
|
+
type ModelCompletion,
|
|
13
|
+
} from "./index.js";
|
|
14
|
+
import { fetchWithPolicy, type HttpClientPolicy } from "./http-resilience.js";
|
|
15
|
+
|
|
16
|
+
export interface GeminiAdapterOptions {
|
|
17
|
+
id?: string;
|
|
18
|
+
baseUrl?: string;
|
|
19
|
+
fetchFn?: typeof fetch;
|
|
20
|
+
httpPolicy?: HttpClientPolicy;
|
|
21
|
+
defaultModels?: {
|
|
22
|
+
chat?: string;
|
|
23
|
+
image?: string;
|
|
24
|
+
model?: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeBaseUrl(baseUrl: string | undefined): string {
|
|
29
|
+
const normalized = (baseUrl ?? "https://generativelanguage.googleapis.com/v1beta").trim();
|
|
30
|
+
return normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveFetch(fetchFn?: typeof fetch): typeof fetch {
|
|
34
|
+
const resolved = fetchFn ?? (typeof fetch !== "undefined" ? fetch : undefined);
|
|
35
|
+
if (!resolved) {
|
|
36
|
+
throw new Error("No fetch implementation available for Gemini adapter.");
|
|
37
|
+
}
|
|
38
|
+
return resolved;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
42
|
+
return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function asString(value: unknown): string | undefined {
|
|
46
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function parseResponseBody(response: Response): Promise<unknown> {
|
|
50
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
51
|
+
if (contentType.includes("application/json")) {
|
|
52
|
+
return await response.json();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const text = await response.text();
|
|
56
|
+
if (!text) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(text) as unknown;
|
|
61
|
+
} catch {
|
|
62
|
+
return text;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function resolveErrorMessage(body: unknown, fallback: string): string {
|
|
67
|
+
const payload = asRecord(body);
|
|
68
|
+
const nestedError = asRecord(payload.error);
|
|
69
|
+
return (
|
|
70
|
+
asString(nestedError.message) ??
|
|
71
|
+
asString(payload.message) ??
|
|
72
|
+
asString(payload.error) ??
|
|
73
|
+
fallback
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function requireApiKey(apiKey: string): string {
|
|
78
|
+
const trimmed = apiKey.trim();
|
|
79
|
+
if (!trimmed) {
|
|
80
|
+
throw new Error("Gemini API key is required.");
|
|
81
|
+
}
|
|
82
|
+
return trimmed;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function createBaseCompletion(
|
|
86
|
+
type: string,
|
|
87
|
+
model: string,
|
|
88
|
+
requestor: string,
|
|
89
|
+
durationMs: number,
|
|
90
|
+
usage?: Record<string, number>
|
|
91
|
+
): Completion {
|
|
92
|
+
return {
|
|
93
|
+
partitionKey: requestor,
|
|
94
|
+
id: crypto.randomUUID(),
|
|
95
|
+
type,
|
|
96
|
+
model,
|
|
97
|
+
createdAt: new Date().toISOString(),
|
|
98
|
+
durationMs,
|
|
99
|
+
usage: usage ?? {},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function extractGeminiText(body: unknown): string {
|
|
104
|
+
const root = asRecord(body);
|
|
105
|
+
const candidates = Array.isArray(root.candidates) ? root.candidates : [];
|
|
106
|
+
const first = candidates[0] && typeof candidates[0] === "object" ? asRecord(candidates[0]) : {};
|
|
107
|
+
const content = asRecord(first.content);
|
|
108
|
+
const parts = Array.isArray(content.parts) ? content.parts : [];
|
|
109
|
+
const text = parts
|
|
110
|
+
.map((part) => {
|
|
111
|
+
const maybePart = asRecord(part);
|
|
112
|
+
return asString(maybePart.text) ?? "";
|
|
113
|
+
})
|
|
114
|
+
.join("")
|
|
115
|
+
.trim();
|
|
116
|
+
|
|
117
|
+
if (!text) {
|
|
118
|
+
throw new Error("Gemini response did not contain text output.");
|
|
119
|
+
}
|
|
120
|
+
return text;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function extractGeminiUsage(body: unknown): Record<string, number> | undefined {
|
|
124
|
+
const root = asRecord(body);
|
|
125
|
+
const usageMetadata = asRecord(root.usageMetadata);
|
|
126
|
+
const promptTokenCount = usageMetadata.promptTokenCount;
|
|
127
|
+
const candidatesTokenCount = usageMetadata.candidatesTokenCount;
|
|
128
|
+
const totalTokenCount = usageMetadata.totalTokenCount;
|
|
129
|
+
const usage: Record<string, number> = {};
|
|
130
|
+
|
|
131
|
+
if (typeof promptTokenCount === "number") {
|
|
132
|
+
usage.inputTokens = promptTokenCount;
|
|
133
|
+
}
|
|
134
|
+
if (typeof candidatesTokenCount === "number") {
|
|
135
|
+
usage.outputTokens = candidatesTokenCount;
|
|
136
|
+
}
|
|
137
|
+
if (typeof totalTokenCount === "number") {
|
|
138
|
+
usage.totalTokens = totalTokenCount;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return Object.keys(usage).length > 0 ? usage : undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function extractGeminiImage(body: unknown): URL {
|
|
145
|
+
const root = asRecord(body);
|
|
146
|
+
const predictions = Array.isArray(root.predictions) ? root.predictions : [];
|
|
147
|
+
const firstPrediction =
|
|
148
|
+
predictions[0] && typeof predictions[0] === "object"
|
|
149
|
+
? asRecord(predictions[0])
|
|
150
|
+
: {};
|
|
151
|
+
|
|
152
|
+
const bytesBase64Encoded =
|
|
153
|
+
asString(firstPrediction.bytesBase64Encoded) ??
|
|
154
|
+
asString(asRecord(firstPrediction.image).bytesBase64Encoded);
|
|
155
|
+
|
|
156
|
+
if (!bytesBase64Encoded) {
|
|
157
|
+
throw new Error("Gemini image response did not contain base64 image bytes.");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const mimeType =
|
|
161
|
+
asString(firstPrediction.mimeType) ??
|
|
162
|
+
asString(asRecord(firstPrediction.image).mimeType) ??
|
|
163
|
+
"image/png";
|
|
164
|
+
|
|
165
|
+
return new URL(`data:${mimeType};base64,${bytesBase64Encoded}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function parseGeneratedModel(text: string): {
|
|
169
|
+
modelId: string;
|
|
170
|
+
artifactUrl?: URL;
|
|
171
|
+
} {
|
|
172
|
+
const trimmed = text.trim();
|
|
173
|
+
if (!trimmed) {
|
|
174
|
+
return {
|
|
175
|
+
modelId: crypto.randomUUID(),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const parsed = JSON.parse(trimmed) as unknown;
|
|
181
|
+
const root = asRecord(parsed);
|
|
182
|
+
const modelId = asString(root.modelId);
|
|
183
|
+
const artifactUrl = asString(root.artifactUrl);
|
|
184
|
+
if (modelId) {
|
|
185
|
+
return {
|
|
186
|
+
modelId,
|
|
187
|
+
artifactUrl: artifactUrl ? new URL(artifactUrl) : undefined,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
} catch {
|
|
191
|
+
// Fallback below.
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
modelId: trimmed,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function withApiKey(path: string, apiKey: string): string {
|
|
200
|
+
const delimiter = path.includes("?") ? "&" : "?";
|
|
201
|
+
return `${path}${delimiter}key=${encodeURIComponent(apiKey)}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function createGeminiAdapter(
|
|
205
|
+
options: GeminiAdapterOptions = {}
|
|
206
|
+
): AICapabilityAdapter {
|
|
207
|
+
const providerId = (options.id ?? "gemini").trim() || "gemini";
|
|
208
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
209
|
+
const fetcher = resolveFetch(options.fetchFn);
|
|
210
|
+
|
|
211
|
+
const chatWithAI = async (request: AdapterChatRequest): Promise<ChatCompletion> => {
|
|
212
|
+
const startedAt = performance.now();
|
|
213
|
+
const apiKey = requireApiKey(request.apiKey);
|
|
214
|
+
const resolvedModel = request.model || options.defaultModels?.chat || "gemini-2.0-flash";
|
|
215
|
+
|
|
216
|
+
const response = await fetchWithPolicy({
|
|
217
|
+
url: withApiKey(`${baseUrl}/models/${resolvedModel}:generateContent`, apiKey),
|
|
218
|
+
operation: "Gemini chat request",
|
|
219
|
+
fetchFn: fetcher,
|
|
220
|
+
policy: options.httpPolicy,
|
|
221
|
+
createRequestInit: () => ({
|
|
222
|
+
method: "POST",
|
|
223
|
+
headers: {
|
|
224
|
+
"Content-Type": "application/json",
|
|
225
|
+
"X-Request-Id": request.traceId,
|
|
226
|
+
"X-Plasius-Client": "@plasius/ai/gemini-adapter",
|
|
227
|
+
},
|
|
228
|
+
body: JSON.stringify({
|
|
229
|
+
contents: [
|
|
230
|
+
{
|
|
231
|
+
role: "user",
|
|
232
|
+
parts: [{ text: request.input }],
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
...(request.context
|
|
236
|
+
? {
|
|
237
|
+
systemInstruction: {
|
|
238
|
+
parts: [{ text: request.context }],
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
: {}),
|
|
242
|
+
}),
|
|
243
|
+
}),
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const body = await parseResponseBody(response);
|
|
247
|
+
if (!response.ok) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
resolveErrorMessage(
|
|
250
|
+
body,
|
|
251
|
+
`Gemini chat request failed (${response.status} ${response.statusText}).`
|
|
252
|
+
)
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const message = extractGeminiText(body);
|
|
257
|
+
const usage = extractGeminiUsage(body);
|
|
258
|
+
const durationMs = performance.now() - startedAt;
|
|
259
|
+
const base = createBaseCompletion("chat", resolvedModel, request.userId, durationMs, usage);
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
...base,
|
|
263
|
+
message,
|
|
264
|
+
outputUser: "assistant",
|
|
265
|
+
};
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const generateImage = async (
|
|
269
|
+
request: AdapterGenerateImageRequest
|
|
270
|
+
): Promise<ImageCompletion> => {
|
|
271
|
+
const startedAt = performance.now();
|
|
272
|
+
const apiKey = requireApiKey(request.apiKey);
|
|
273
|
+
const resolvedModel =
|
|
274
|
+
request.model || options.defaultModels?.image || "imagen-3.0-generate-002";
|
|
275
|
+
const combinedPrompt = request.context
|
|
276
|
+
? `${request.context}\n\n${request.input}`
|
|
277
|
+
: request.input;
|
|
278
|
+
|
|
279
|
+
const response = await fetchWithPolicy({
|
|
280
|
+
url: withApiKey(`${baseUrl}/models/${resolvedModel}:predict`, apiKey),
|
|
281
|
+
operation: "Gemini image generation",
|
|
282
|
+
fetchFn: fetcher,
|
|
283
|
+
policy: options.httpPolicy,
|
|
284
|
+
createRequestInit: () => ({
|
|
285
|
+
method: "POST",
|
|
286
|
+
headers: {
|
|
287
|
+
"Content-Type": "application/json",
|
|
288
|
+
"X-Request-Id": request.traceId,
|
|
289
|
+
"X-Plasius-Client": "@plasius/ai/gemini-adapter",
|
|
290
|
+
},
|
|
291
|
+
body: JSON.stringify({
|
|
292
|
+
instances: [{ prompt: combinedPrompt }],
|
|
293
|
+
parameters: { sampleCount: 1 },
|
|
294
|
+
}),
|
|
295
|
+
}),
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const body = await parseResponseBody(response);
|
|
299
|
+
if (!response.ok) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
resolveErrorMessage(
|
|
302
|
+
body,
|
|
303
|
+
`Gemini image generation failed (${response.status} ${response.statusText}).`
|
|
304
|
+
)
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const url = extractGeminiImage(body);
|
|
309
|
+
const durationMs = performance.now() - startedAt;
|
|
310
|
+
const base = createBaseCompletion("image", resolvedModel, request.userId, durationMs);
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
...base,
|
|
314
|
+
url,
|
|
315
|
+
};
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const generateModel = async (
|
|
319
|
+
request: AdapterGenerateModelRequest
|
|
320
|
+
): Promise<ModelCompletion> => {
|
|
321
|
+
const startedAt = performance.now();
|
|
322
|
+
const apiKey = requireApiKey(request.apiKey);
|
|
323
|
+
const resolvedModel = request.model || options.defaultModels?.model || "gemini-2.0-flash";
|
|
324
|
+
|
|
325
|
+
const systemInstruction = [
|
|
326
|
+
request.context,
|
|
327
|
+
"Return JSON only with fields modelId (string) and optional artifactUrl (string URL).",
|
|
328
|
+
]
|
|
329
|
+
.filter(Boolean)
|
|
330
|
+
.join("\n");
|
|
331
|
+
|
|
332
|
+
const response = await fetchWithPolicy({
|
|
333
|
+
url: withApiKey(`${baseUrl}/models/${resolvedModel}:generateContent`, apiKey),
|
|
334
|
+
operation: "Gemini model generation",
|
|
335
|
+
fetchFn: fetcher,
|
|
336
|
+
policy: options.httpPolicy,
|
|
337
|
+
createRequestInit: () => ({
|
|
338
|
+
method: "POST",
|
|
339
|
+
headers: {
|
|
340
|
+
"Content-Type": "application/json",
|
|
341
|
+
"X-Request-Id": request.traceId,
|
|
342
|
+
"X-Plasius-Client": "@plasius/ai/gemini-adapter",
|
|
343
|
+
},
|
|
344
|
+
body: JSON.stringify({
|
|
345
|
+
contents: [
|
|
346
|
+
{
|
|
347
|
+
role: "user",
|
|
348
|
+
parts: [{ text: request.input }],
|
|
349
|
+
},
|
|
350
|
+
],
|
|
351
|
+
generationConfig: {
|
|
352
|
+
responseMimeType: "application/json",
|
|
353
|
+
},
|
|
354
|
+
systemInstruction: {
|
|
355
|
+
parts: [{ text: systemInstruction }],
|
|
356
|
+
},
|
|
357
|
+
}),
|
|
358
|
+
}),
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const body = await parseResponseBody(response);
|
|
362
|
+
if (!response.ok) {
|
|
363
|
+
throw new Error(
|
|
364
|
+
resolveErrorMessage(
|
|
365
|
+
body,
|
|
366
|
+
`Gemini model generation failed (${response.status} ${response.statusText}).`
|
|
367
|
+
)
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const text = extractGeminiText(body);
|
|
372
|
+
const parsed = parseGeneratedModel(text);
|
|
373
|
+
const usage = extractGeminiUsage(body);
|
|
374
|
+
const durationMs = performance.now() - startedAt;
|
|
375
|
+
const base = createBaseCompletion("model", resolvedModel, request.userId, durationMs, usage);
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
...base,
|
|
379
|
+
modelId: parsed.modelId,
|
|
380
|
+
artifactUrl: parsed.artifactUrl,
|
|
381
|
+
};
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
id: providerId,
|
|
386
|
+
capabilities: [AICapability.Chat, AICapability.Image, AICapability.Model],
|
|
387
|
+
chatWithAI,
|
|
388
|
+
generateImage,
|
|
389
|
+
generateModel,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
export interface HttpClientPolicy {
|
|
2
|
+
maxAttempts?: number;
|
|
3
|
+
timeoutMs?: number;
|
|
4
|
+
baseDelayMs?: number;
|
|
5
|
+
maxDelayMs?: number;
|
|
6
|
+
jitterRatio?: number;
|
|
7
|
+
respectRetryAfter?: boolean;
|
|
8
|
+
retryableMethods?: string[];
|
|
9
|
+
retryableStatusCodes?: number[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface FetchWithPolicyOptions {
|
|
13
|
+
url: string;
|
|
14
|
+
operation: string;
|
|
15
|
+
fetchFn: typeof fetch;
|
|
16
|
+
policy?: HttpClientPolicy;
|
|
17
|
+
createRequestInit: () => RequestInit | Promise<RequestInit>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DEFAULT_RETRYABLE_STATUS_CODES = [408, 409, 425, 429, 500, 502, 503, 504];
|
|
21
|
+
const DEFAULT_RETRYABLE_METHODS = ["GET", "HEAD", "OPTIONS", "PUT", "DELETE", "POST"];
|
|
22
|
+
|
|
23
|
+
function normalizePolicy(policy: HttpClientPolicy | undefined): Required<HttpClientPolicy> {
|
|
24
|
+
return {
|
|
25
|
+
maxAttempts: Math.max(1, policy?.maxAttempts ?? 3),
|
|
26
|
+
timeoutMs: Math.max(1, policy?.timeoutMs ?? 30_000),
|
|
27
|
+
baseDelayMs: Math.max(0, policy?.baseDelayMs ?? 250),
|
|
28
|
+
maxDelayMs: Math.max(0, policy?.maxDelayMs ?? 4_000),
|
|
29
|
+
jitterRatio: Math.max(0, policy?.jitterRatio ?? 0.2),
|
|
30
|
+
respectRetryAfter: policy?.respectRetryAfter ?? true,
|
|
31
|
+
retryableMethods:
|
|
32
|
+
policy?.retryableMethods?.map((method) => method.toUpperCase()) ??
|
|
33
|
+
DEFAULT_RETRYABLE_METHODS,
|
|
34
|
+
retryableStatusCodes: policy?.retryableStatusCodes ?? DEFAULT_RETRYABLE_STATUS_CODES,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseRetryAfterMs(header: string | null): number | undefined {
|
|
39
|
+
if (!header) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const asNumber = Number(header);
|
|
44
|
+
if (Number.isFinite(asNumber)) {
|
|
45
|
+
return Math.max(0, Math.round(asNumber * 1000));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const asDate = Date.parse(header);
|
|
49
|
+
if (Number.isFinite(asDate)) {
|
|
50
|
+
return Math.max(0, asDate - Date.now());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function computeBackoffDelayMs(
|
|
57
|
+
retryIndex: number,
|
|
58
|
+
baseDelayMs: number,
|
|
59
|
+
maxDelayMs: number,
|
|
60
|
+
jitterRatio: number
|
|
61
|
+
): number {
|
|
62
|
+
const exponent = Math.max(0, retryIndex - 1);
|
|
63
|
+
const cappedExponential = Math.min(maxDelayMs, baseDelayMs * 2 ** exponent);
|
|
64
|
+
if (cappedExponential === 0) {
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const jitter = cappedExponential * jitterRatio * Math.random();
|
|
69
|
+
return Math.round(cappedExponential + jitter);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function sleep(ms: number): Promise<void> {
|
|
73
|
+
if (ms <= 0) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isAbortError(error: unknown): boolean {
|
|
80
|
+
return error instanceof DOMException && error.name === "AbortError";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isRetryableError(error: unknown): boolean {
|
|
84
|
+
return error instanceof TypeError || isAbortError(error);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function fetchWithTimeout(
|
|
88
|
+
fetchFn: typeof fetch,
|
|
89
|
+
url: string,
|
|
90
|
+
init: RequestInit,
|
|
91
|
+
timeoutMs: number
|
|
92
|
+
): Promise<Response> {
|
|
93
|
+
const externalSignal = init.signal ?? null;
|
|
94
|
+
if (externalSignal?.aborted) {
|
|
95
|
+
throw externalSignal.reason;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const controller = new AbortController();
|
|
99
|
+
const timeout = setTimeout(() => {
|
|
100
|
+
controller.abort(new Error(`Request timed out after ${timeoutMs}ms.`));
|
|
101
|
+
}, timeoutMs);
|
|
102
|
+
|
|
103
|
+
const abortRelay = () => {
|
|
104
|
+
controller.abort(externalSignal?.reason);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (externalSignal) {
|
|
108
|
+
externalSignal.addEventListener("abort", abortRelay, { once: true });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
return await fetchFn(url, {
|
|
113
|
+
...init,
|
|
114
|
+
signal: controller.signal,
|
|
115
|
+
});
|
|
116
|
+
} finally {
|
|
117
|
+
clearTimeout(timeout);
|
|
118
|
+
if (externalSignal) {
|
|
119
|
+
externalSignal.removeEventListener("abort", abortRelay);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function fetchWithPolicy(options: FetchWithPolicyOptions): Promise<Response> {
|
|
125
|
+
const policy = normalizePolicy(options.policy);
|
|
126
|
+
let lastError: unknown;
|
|
127
|
+
|
|
128
|
+
for (let attempt = 1; attempt <= policy.maxAttempts; attempt += 1) {
|
|
129
|
+
const requestInit = await options.createRequestInit();
|
|
130
|
+
const method = (requestInit.method ?? "GET").toUpperCase();
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const response = await fetchWithTimeout(
|
|
134
|
+
options.fetchFn,
|
|
135
|
+
options.url,
|
|
136
|
+
requestInit,
|
|
137
|
+
policy.timeoutMs
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
if (response.ok || attempt >= policy.maxAttempts) {
|
|
141
|
+
return response;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const canRetryMethod = policy.retryableMethods.includes(method);
|
|
145
|
+
const canRetryStatus = policy.retryableStatusCodes.includes(response.status);
|
|
146
|
+
if (!canRetryMethod || !canRetryStatus) {
|
|
147
|
+
return response;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const retryAfterMs = policy.respectRetryAfter
|
|
151
|
+
? parseRetryAfterMs(response.headers.get("retry-after"))
|
|
152
|
+
: undefined;
|
|
153
|
+
const backoffMs = computeBackoffDelayMs(
|
|
154
|
+
attempt,
|
|
155
|
+
policy.baseDelayMs,
|
|
156
|
+
policy.maxDelayMs,
|
|
157
|
+
policy.jitterRatio
|
|
158
|
+
);
|
|
159
|
+
const delayMs = Math.max(retryAfterMs ?? 0, backoffMs);
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
await response.body?.cancel();
|
|
163
|
+
} catch {
|
|
164
|
+
// Ignore cancellation errors and continue retry flow.
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
await sleep(delayMs);
|
|
168
|
+
continue;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
lastError = error;
|
|
171
|
+
|
|
172
|
+
const canRetryMethod = policy.retryableMethods.includes(method);
|
|
173
|
+
const externalAborted = requestInit.signal?.aborted ?? false;
|
|
174
|
+
const shouldRetry =
|
|
175
|
+
attempt < policy.maxAttempts &&
|
|
176
|
+
canRetryMethod &&
|
|
177
|
+
!externalAborted &&
|
|
178
|
+
isRetryableError(error);
|
|
179
|
+
|
|
180
|
+
if (!shouldRetry) {
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const delayMs = computeBackoffDelayMs(
|
|
185
|
+
attempt,
|
|
186
|
+
policy.baseDelayMs,
|
|
187
|
+
policy.maxDelayMs,
|
|
188
|
+
policy.jitterRatio
|
|
189
|
+
);
|
|
190
|
+
await sleep(delayMs);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
throw (
|
|
195
|
+
lastError ??
|
|
196
|
+
new Error(`${options.operation} failed without a recoverable response.`)
|
|
197
|
+
);
|
|
198
|
+
}
|
package/src/platform/index.ts
CHANGED
|
@@ -168,6 +168,34 @@ export interface VideoCompletion extends Completion {
|
|
|
168
168
|
url: URL;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
export const modelCompletionSchema = createSchema(
|
|
172
|
+
{
|
|
173
|
+
modelId: field
|
|
174
|
+
.string()
|
|
175
|
+
.description("Identifier for the generated model")
|
|
176
|
+
.version("1.0"),
|
|
177
|
+
artifactUrl: field
|
|
178
|
+
.string()
|
|
179
|
+
.description("Optional URL to a generated model artifact")
|
|
180
|
+
.version("1.0")
|
|
181
|
+
.optional()
|
|
182
|
+
.as<URL>(),
|
|
183
|
+
},
|
|
184
|
+
"modelCompletion",
|
|
185
|
+
{
|
|
186
|
+
version: "1.0",
|
|
187
|
+
table: "completions",
|
|
188
|
+
schemaValidator: () => {
|
|
189
|
+
return true;
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
export interface ModelCompletion extends Completion {
|
|
195
|
+
modelId: string;
|
|
196
|
+
artifactUrl?: URL;
|
|
197
|
+
}
|
|
198
|
+
|
|
171
199
|
export const balanceCompletionSchema = createSchema(
|
|
172
200
|
{
|
|
173
201
|
balance: field.number().description("Current balance").version("1.0"),
|
|
@@ -193,6 +221,7 @@ export enum AICapability {
|
|
|
193
221
|
Image,
|
|
194
222
|
Video,
|
|
195
223
|
Balance,
|
|
224
|
+
Model,
|
|
196
225
|
}
|
|
197
226
|
|
|
198
227
|
export interface AIPlatform {
|
|
@@ -232,6 +261,45 @@ export interface AIPlatform {
|
|
|
232
261
|
context: string,
|
|
233
262
|
model: string
|
|
234
263
|
) => Promise<VideoCompletion>;
|
|
264
|
+
generateModel: (
|
|
265
|
+
userId: string,
|
|
266
|
+
input: string,
|
|
267
|
+
context: string,
|
|
268
|
+
model: string
|
|
269
|
+
) => Promise<ModelCompletion>;
|
|
235
270
|
checkBalance: (userId: string) => Promise<BalanceCompletion>;
|
|
236
271
|
currentBalance: number;
|
|
237
272
|
}
|
|
273
|
+
|
|
274
|
+
export type {
|
|
275
|
+
HttpClientPolicy,
|
|
276
|
+
} from "./http-resilience.js";
|
|
277
|
+
export type {
|
|
278
|
+
ProviderBalance,
|
|
279
|
+
VideoGenerationRequest,
|
|
280
|
+
VideoGenerationResult,
|
|
281
|
+
VideoJobResult,
|
|
282
|
+
VideoJobState,
|
|
283
|
+
VideoProviderAdapter,
|
|
284
|
+
VideoProviderRequestContext,
|
|
285
|
+
} from "./video-provider-adapter.js";
|
|
286
|
+
export { createHttpVideoProviderAdapter } from "./video-provider-adapter.js";
|
|
287
|
+
export type {
|
|
288
|
+
AdapterBalanceRequest,
|
|
289
|
+
AdapterChatRequest,
|
|
290
|
+
AdapterGenerateImageRequest,
|
|
291
|
+
AdapterGenerateModelRequest,
|
|
292
|
+
AdapterPlatformProps,
|
|
293
|
+
AdapterRequestContext,
|
|
294
|
+
AdapterSynthesizeSpeechRequest,
|
|
295
|
+
AdapterTranscribeSpeechRequest,
|
|
296
|
+
AdapterVideoRequest,
|
|
297
|
+
AICapabilityAdapter,
|
|
298
|
+
} from "./adapter-platform.js";
|
|
299
|
+
export { createAdapterPlatform } from "./adapter-platform.js";
|
|
300
|
+
export type { VideoProviderPlatformProps } from "./video-provider-platform.js";
|
|
301
|
+
export { createVideoProviderPlatform } from "./video-provider-platform.js";
|
|
302
|
+
export type { OpenAIAdapterOptions } from "./openai-adapter.js";
|
|
303
|
+
export { createOpenAIAdapter } from "./openai-adapter.js";
|
|
304
|
+
export type { GeminiAdapterOptions } from "./gemini-adapter.js";
|
|
305
|
+
export { createGeminiAdapter } from "./gemini-adapter.js";
|