@forgezero/providers 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +240 -55
- package/dist/binance.js +192 -38
- package/dist/chain.d.ts +1 -1
- package/dist/chain.js +192 -38
- package/dist/database.d.ts +2 -2
- package/dist/database.js +192 -38
- package/dist/email.d.ts +26 -44
- package/dist/email.js +257 -127
- package/dist/http.d.ts +2 -2
- package/dist/http.js +192 -38
- package/dist/index.d.ts +127 -17
- package/dist/index.js +197 -36
- package/dist/pool.js +192 -38
- package/dist/storage.d.ts +2 -2
- package/dist/storage.js +192 -38
- package/dist/translation.d.ts +6 -4
- package/dist/translation.js +192 -38
- package/package.json +5 -5
package/dist/storage.js
CHANGED
|
@@ -38,25 +38,71 @@ function chainCredentials(...sources) {
|
|
|
38
38
|
}
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
-
function
|
|
41
|
+
function scopeCredentials(source, reference) {
|
|
42
|
+
return {
|
|
43
|
+
name: `${source.name}:${reference}`,
|
|
44
|
+
get: (field) => source.get(reference, field)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function chainScopedCredentials(...sources) {
|
|
48
|
+
return {
|
|
49
|
+
name: sources.map((source) => source.name).join("+"),
|
|
50
|
+
async get(field) {
|
|
51
|
+
let last;
|
|
52
|
+
for (const source of sources) {
|
|
53
|
+
try {
|
|
54
|
+
return await source.get(field);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
last = error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
throw last instanceof Error ? last : new ProviderError("CREDENTIAL_MISSING", `No scoped source held field "${field}".`);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function staticConfig(config) {
|
|
64
|
+
const providers = new Map(config.providers.map((provider) => [provider.instanceKey, provider]));
|
|
65
|
+
if (providers.size !== config.providers.length) {
|
|
66
|
+
throw new ProviderError("CONFIG_DUPLICATE_INSTANCE", "Provider instance keys must be unique.");
|
|
67
|
+
}
|
|
42
68
|
const health = new Map;
|
|
43
69
|
return {
|
|
44
70
|
name: "static",
|
|
45
|
-
async
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
71
|
+
async provider(instanceKey) {
|
|
72
|
+
const provider = providers.get(instanceKey);
|
|
73
|
+
return provider ? { ...provider, config: { ...provider.config } } : undefined;
|
|
74
|
+
},
|
|
75
|
+
async list(serviceKey, methodKey) {
|
|
76
|
+
return (config.services[serviceKey]?.[methodKey] ?? []).map((attachment) => ({
|
|
77
|
+
...attachment,
|
|
78
|
+
health: health.get(`${serviceKey}:${methodKey}:${attachment.instanceKey}:${attachment.providerMethod}`) ?? attachment.health
|
|
49
79
|
}));
|
|
50
80
|
},
|
|
51
|
-
async recordHealth(serviceKey,
|
|
52
|
-
health.set(`${serviceKey}:${
|
|
81
|
+
async recordHealth(serviceKey, methodKey, instanceKey, providerMethod, next) {
|
|
82
|
+
health.set(`${serviceKey}:${methodKey}:${instanceKey}:${providerMethod}`, next);
|
|
53
83
|
}
|
|
54
84
|
};
|
|
55
85
|
}
|
|
86
|
+
function defineProviderMethod(method) {
|
|
87
|
+
return method;
|
|
88
|
+
}
|
|
56
89
|
function defineProvider(spec) {
|
|
57
90
|
return spec;
|
|
58
91
|
}
|
|
92
|
+
function defineSingleMethodProvider(spec) {
|
|
93
|
+
const { method, invoke, classify, ...identity } = spec;
|
|
94
|
+
return defineProvider({
|
|
95
|
+
...identity,
|
|
96
|
+
methods: { [method]: defineProviderMethod({ invoke, classify }) }
|
|
97
|
+
});
|
|
98
|
+
}
|
|
59
99
|
var STRIKES_TO_OFFLINE = 3;
|
|
100
|
+
function serviceMethod() {
|
|
101
|
+
return Object.freeze({});
|
|
102
|
+
}
|
|
103
|
+
function defineService(definition) {
|
|
104
|
+
return definition;
|
|
105
|
+
}
|
|
60
106
|
function nextHealth(current, kind) {
|
|
61
107
|
if (kind === "success")
|
|
62
108
|
return { strikes: 0, status: "ok" };
|
|
@@ -71,80 +117,188 @@ function nextHealth(current, kind) {
|
|
|
71
117
|
}
|
|
72
118
|
function createRegistry(options) {
|
|
73
119
|
const byId = new Map(options.providers.map((provider) => [provider.id, provider]));
|
|
74
|
-
async function call(serviceKey, args, callOptions = {}) {
|
|
75
|
-
const configured = [...await options.config.list(serviceKey)].filter((
|
|
120
|
+
async function call(serviceKey, methodKey, args, callOptions = {}) {
|
|
121
|
+
const configured = [...await options.config.list(serviceKey, methodKey)].filter((attachment) => attachment.enabled).sort((a, b) => a.priority - b.priority);
|
|
76
122
|
const attempts = [];
|
|
77
|
-
for (const
|
|
123
|
+
for (const attachment of configured) {
|
|
78
124
|
if (callOptions.signal?.aborted) {
|
|
79
125
|
const cancelled = {
|
|
80
126
|
ok: false,
|
|
127
|
+
fallbackUsed: attempts.length > 0,
|
|
81
128
|
attempts,
|
|
82
|
-
error: new ProviderError("CALL_ABORTED", `The "${serviceKey}" call was cancelled.`)
|
|
129
|
+
error: new ProviderError("CALL_ABORTED", `The "${serviceKey}.${methodKey}" call was cancelled.`)
|
|
83
130
|
};
|
|
84
|
-
options.after?.(cancelled);
|
|
131
|
+
await options.after?.(cancelled);
|
|
85
132
|
return cancelled;
|
|
86
133
|
}
|
|
87
|
-
const
|
|
88
|
-
if (!
|
|
89
|
-
attempts.push({ providerId:
|
|
134
|
+
const instance = await options.config.provider(attachment.instanceKey);
|
|
135
|
+
if (!instance) {
|
|
136
|
+
attempts.push({ providerId: "unknown", instanceKey: attachment.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance not registered" });
|
|
90
137
|
continue;
|
|
91
138
|
}
|
|
92
|
-
|
|
93
|
-
|
|
139
|
+
const spec = byId.get(instance.providerId);
|
|
140
|
+
const method = spec?.methods[attachment.providerMethod];
|
|
141
|
+
if (!spec || !method) {
|
|
142
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: !spec ? "provider not registered" : "method not supported" });
|
|
94
143
|
continue;
|
|
95
144
|
}
|
|
96
|
-
|
|
145
|
+
if (!instance.enabled) {
|
|
146
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance disabled" });
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (attachment.health?.status === "offline") {
|
|
150
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "offline" });
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
await options.before?.({ service: serviceKey, method: methodKey, provider: instance.providerId, instance: instance.instanceKey });
|
|
154
|
+
const startedAt = performance.now();
|
|
97
155
|
try {
|
|
98
|
-
const result = await
|
|
99
|
-
config:
|
|
100
|
-
secret: (field) => options.credentials.get(
|
|
156
|
+
const result = await method.invoke({
|
|
157
|
+
config: instance.config,
|
|
158
|
+
secret: (field) => options.credentials.get(instance.secretRef, field),
|
|
101
159
|
signal: callOptions.signal
|
|
102
160
|
}, args);
|
|
103
|
-
attempts.push({ providerId:
|
|
104
|
-
await options.config.recordHealth(serviceKey,
|
|
105
|
-
const sent = {
|
|
106
|
-
|
|
161
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "sent", durationMs: performance.now() - startedAt });
|
|
162
|
+
await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, "success"));
|
|
163
|
+
const sent = {
|
|
164
|
+
ok: true,
|
|
165
|
+
result,
|
|
166
|
+
provider: instance.providerId,
|
|
167
|
+
instance: instance.instanceKey,
|
|
168
|
+
method: attachment.providerMethod,
|
|
169
|
+
selected: { providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod },
|
|
170
|
+
fallbackUsed: attempts.length > 1,
|
|
171
|
+
attempts
|
|
172
|
+
};
|
|
173
|
+
await options.after?.(sent);
|
|
107
174
|
return sent;
|
|
108
175
|
} catch (error) {
|
|
109
176
|
if (callOptions.signal?.aborted) {
|
|
110
177
|
const cancelled = {
|
|
111
178
|
ok: false,
|
|
179
|
+
fallbackUsed: attempts.length > 0,
|
|
112
180
|
attempts,
|
|
113
|
-
error: new ProviderError("CALL_ABORTED", `The "${serviceKey}" call was cancelled.`)
|
|
181
|
+
error: new ProviderError("CALL_ABORTED", `The "${serviceKey}.${methodKey}" call was cancelled.`)
|
|
114
182
|
};
|
|
115
|
-
options.after?.(cancelled);
|
|
183
|
+
await options.after?.(cancelled);
|
|
116
184
|
return cancelled;
|
|
117
185
|
}
|
|
118
|
-
const kind =
|
|
186
|
+
const kind = method.classify(error);
|
|
187
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
188
|
+
const code = error instanceof ProviderError ? error.code : typeof error?.code === "string" ? error.code : undefined;
|
|
119
189
|
attempts.push({
|
|
120
|
-
providerId:
|
|
190
|
+
providerId: instance.providerId,
|
|
191
|
+
instanceKey: instance.instanceKey,
|
|
192
|
+
providerMethod: attachment.providerMethod,
|
|
121
193
|
outcome: "failed",
|
|
122
194
|
kind,
|
|
123
|
-
|
|
195
|
+
durationMs: performance.now() - startedAt,
|
|
196
|
+
failure: { kind, ...code ? { code } : {}, message },
|
|
197
|
+
error: message
|
|
124
198
|
});
|
|
125
|
-
await options.config.recordHealth(serviceKey,
|
|
199
|
+
await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, kind));
|
|
126
200
|
if (kind === "terminal") {
|
|
127
201
|
const refused = {
|
|
128
202
|
ok: false,
|
|
203
|
+
fallbackUsed: attempts.length > 1,
|
|
129
204
|
attempts,
|
|
130
205
|
error: new ProviderError("PAYLOAD_REJECTED", "The request was refused as malformed; no provider will accept it.")
|
|
131
206
|
};
|
|
132
|
-
options.after?.(refused);
|
|
207
|
+
await options.after?.(refused);
|
|
133
208
|
return refused;
|
|
134
209
|
}
|
|
135
210
|
}
|
|
136
211
|
}
|
|
137
212
|
const failed = {
|
|
138
213
|
ok: false,
|
|
214
|
+
fallbackUsed: attempts.length > 1,
|
|
139
215
|
attempts,
|
|
140
|
-
error: new ProviderError(attempts.length === 0 ? "NO_PROVIDER" : "ALL_PROVIDERS_FAILED", attempts.length === 0 ? `No provider is configured for "${serviceKey}".` : `Every provider for "${serviceKey}" failed or was skipped.`)
|
|
216
|
+
error: new ProviderError(attempts.length === 0 ? "NO_PROVIDER" : "ALL_PROVIDERS_FAILED", attempts.length === 0 ? `No provider method is configured for "${serviceKey}.${methodKey}".` : `Every provider method for "${serviceKey}.${methodKey}" failed or was skipped.`)
|
|
141
217
|
};
|
|
142
|
-
options.after?.(failed);
|
|
218
|
+
await options.after?.(failed);
|
|
143
219
|
return failed;
|
|
144
220
|
}
|
|
145
|
-
|
|
221
|
+
function service(definition) {
|
|
222
|
+
return {
|
|
223
|
+
call(method, args, callOptions) {
|
|
224
|
+
return call(definition.key, method, args, callOptions);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return { call, service };
|
|
229
|
+
}
|
|
230
|
+
function createService(definition, methods, options = {}) {
|
|
231
|
+
const providers = new Map;
|
|
232
|
+
const credentials = new Map;
|
|
233
|
+
const instances = [];
|
|
234
|
+
const serviceMethods = {};
|
|
235
|
+
for (const methodKey of Object.keys(definition.methods)) {
|
|
236
|
+
const attachments = methods[methodKey];
|
|
237
|
+
if (!Array.isArray(attachments)) {
|
|
238
|
+
throw new ProviderError("CONFIG_METHOD_MISSING", `Service method "${definition.key}.${methodKey}" needs an attachment array.`);
|
|
239
|
+
}
|
|
240
|
+
const priorities = new Set;
|
|
241
|
+
serviceMethods[methodKey] = attachments.map((attachment, index) => {
|
|
242
|
+
if (!Number.isInteger(attachment.priority) || attachment.priority < 1 || attachment.priority > 1000) {
|
|
243
|
+
throw new ProviderError("CONFIG_PRIORITY", `Priority for "${definition.key}.${methodKey}" must be an integer from 1 to 1000.`);
|
|
244
|
+
}
|
|
245
|
+
if (priorities.has(attachment.priority)) {
|
|
246
|
+
throw new ProviderError("CONFIG_PRIORITY_DUPLICATE", `Priorities for "${definition.key}.${methodKey}" must be unique.`);
|
|
247
|
+
}
|
|
248
|
+
priorities.add(attachment.priority);
|
|
249
|
+
if (!attachment.provider.methods[attachment.method]) {
|
|
250
|
+
throw new ProviderError("CONFIG_METHOD_UNSUPPORTED", `Provider "${attachment.provider.id}" does not support "${attachment.method}".`);
|
|
251
|
+
}
|
|
252
|
+
const existing = providers.get(attachment.provider.id);
|
|
253
|
+
if (existing && existing !== attachment.provider) {
|
|
254
|
+
throw new ProviderError("CONFIG_PROVIDER_DUPLICATE", `Provider id "${attachment.provider.id}" has more than one definition.`);
|
|
255
|
+
}
|
|
256
|
+
providers.set(attachment.provider.id, attachment.provider);
|
|
257
|
+
const internalKey = `${definition.key}:${methodKey}:${index}`;
|
|
258
|
+
credentials.set(internalKey, attachment.credentials);
|
|
259
|
+
instances.push({
|
|
260
|
+
instanceKey: internalKey,
|
|
261
|
+
providerId: attachment.provider.id,
|
|
262
|
+
enabled: attachment.enabled ?? true,
|
|
263
|
+
config: { ...attachment.config ?? {} },
|
|
264
|
+
secretRef: internalKey
|
|
265
|
+
});
|
|
266
|
+
return {
|
|
267
|
+
instanceKey: internalKey,
|
|
268
|
+
providerMethod: attachment.method,
|
|
269
|
+
priority: attachment.priority,
|
|
270
|
+
enabled: attachment.enabled ?? true
|
|
271
|
+
};
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
const registry = createRegistry({
|
|
275
|
+
providers: [...providers.values()],
|
|
276
|
+
config: staticConfig({ providers: instances, services: { [definition.key]: serviceMethods } }),
|
|
277
|
+
credentials: {
|
|
278
|
+
name: [...new Set([...credentials.values()].map((source) => source.name))].join("+"),
|
|
279
|
+
async get(reference, field) {
|
|
280
|
+
const source = credentials.get(reference);
|
|
281
|
+
if (!source)
|
|
282
|
+
throw new ProviderError("CREDENTIAL_SOURCE_MISSING", "The provider credential source is unavailable.");
|
|
283
|
+
return source.get(field);
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
...options
|
|
287
|
+
});
|
|
288
|
+
const dynamic = registry.service(definition);
|
|
289
|
+
return {
|
|
290
|
+
async call(method, args, callOptions) {
|
|
291
|
+
const result = await dynamic.call(method, args, callOptions);
|
|
292
|
+
const { instance: _instance, selected, attempts, ...rest } = result;
|
|
293
|
+
return {
|
|
294
|
+
...rest,
|
|
295
|
+
...selected ? { selected: { providerId: selected.providerId, providerMethod: selected.providerMethod } } : {},
|
|
296
|
+
attempts: attempts.map(({ instanceKey: _instanceKey, ...attempt }) => attempt)
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
};
|
|
146
300
|
}
|
|
147
|
-
var VERSION = "0.1.
|
|
301
|
+
var VERSION = "0.1.11";
|
|
148
302
|
|
|
149
303
|
// src/storage.ts
|
|
150
304
|
var encoder = new TextEncoder;
|
|
@@ -381,9 +535,9 @@ var extractS3Message = (xml) => {
|
|
|
381
535
|
const message = between(xml, "<Message>", "</Message>");
|
|
382
536
|
return code || message ? `${code ?? ""}${code && message ? ": " : ""}${message ?? ""}` : undefined;
|
|
383
537
|
};
|
|
384
|
-
var s3 =
|
|
538
|
+
var s3 = defineSingleMethodProvider({
|
|
385
539
|
id: "s3",
|
|
386
|
-
|
|
540
|
+
method: "request",
|
|
387
541
|
label: "S3-compatible storage",
|
|
388
542
|
multiInstance: true,
|
|
389
543
|
credentials: {
|
package/dist/translation.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type ProviderDefinition, type ProviderMethodSpec } from './index';
|
|
2
2
|
/**
|
|
3
3
|
* Machine translation, as a service with providers behind it.
|
|
4
4
|
*
|
|
@@ -41,7 +41,9 @@ export interface TranslateResult {
|
|
|
41
41
|
model?: string;
|
|
42
42
|
}
|
|
43
43
|
/** Anything that can translate. A tenant may supply their own. */
|
|
44
|
-
export type TranslationProvider =
|
|
44
|
+
export type TranslationProvider = ProviderDefinition<{
|
|
45
|
+
translate: ProviderMethodSpec<TranslateRequest, TranslateResult>;
|
|
46
|
+
}>;
|
|
45
47
|
/**
|
|
46
48
|
* Google AI Studio, on the free tier.
|
|
47
49
|
*
|
|
@@ -53,7 +55,7 @@ export type TranslationProvider = ReturnType<typeof defineProvider<TranslateRequ
|
|
|
53
55
|
* and parsing whatever comes back is how translation pipelines start returning
|
|
54
56
|
* markdown fences on a Tuesday; a schema makes the shape the model's problem.
|
|
55
57
|
*/
|
|
56
|
-
export declare const googleAiStudio:
|
|
58
|
+
export declare const googleAiStudio: ProviderDefinition<Record<"translate", ProviderMethodSpec<TranslateRequest, TranslateResult>>>;
|
|
57
59
|
export declare class TranslationError extends Error {
|
|
58
60
|
readonly code: 'NO_CREDENTIAL' | 'RATE_LIMITED' | 'REQUEST_FAILED' | 'EMPTY_RESPONSE' | 'LENGTH_MISMATCH';
|
|
59
61
|
readonly status?: number | undefined;
|
|
@@ -67,4 +69,4 @@ export declare class TranslationError extends Error {
|
|
|
67
69
|
* both costs three lines and removes a class of "worked until we changed model".
|
|
68
70
|
*/
|
|
69
71
|
export declare function parseStrings(raw: string): string[];
|
|
70
|
-
export declare const translationProviders: readonly [
|
|
72
|
+
export declare const translationProviders: readonly [ProviderDefinition<Record<"translate", ProviderMethodSpec<TranslateRequest, TranslateResult>>>];
|
package/dist/translation.js
CHANGED
|
@@ -38,25 +38,71 @@ function chainCredentials(...sources) {
|
|
|
38
38
|
}
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
-
function
|
|
41
|
+
function scopeCredentials(source, reference) {
|
|
42
|
+
return {
|
|
43
|
+
name: `${source.name}:${reference}`,
|
|
44
|
+
get: (field) => source.get(reference, field)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function chainScopedCredentials(...sources) {
|
|
48
|
+
return {
|
|
49
|
+
name: sources.map((source) => source.name).join("+"),
|
|
50
|
+
async get(field) {
|
|
51
|
+
let last;
|
|
52
|
+
for (const source of sources) {
|
|
53
|
+
try {
|
|
54
|
+
return await source.get(field);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
last = error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
throw last instanceof Error ? last : new ProviderError("CREDENTIAL_MISSING", `No scoped source held field "${field}".`);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function staticConfig(config) {
|
|
64
|
+
const providers = new Map(config.providers.map((provider) => [provider.instanceKey, provider]));
|
|
65
|
+
if (providers.size !== config.providers.length) {
|
|
66
|
+
throw new ProviderError("CONFIG_DUPLICATE_INSTANCE", "Provider instance keys must be unique.");
|
|
67
|
+
}
|
|
42
68
|
const health = new Map;
|
|
43
69
|
return {
|
|
44
70
|
name: "static",
|
|
45
|
-
async
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
71
|
+
async provider(instanceKey) {
|
|
72
|
+
const provider = providers.get(instanceKey);
|
|
73
|
+
return provider ? { ...provider, config: { ...provider.config } } : undefined;
|
|
74
|
+
},
|
|
75
|
+
async list(serviceKey, methodKey) {
|
|
76
|
+
return (config.services[serviceKey]?.[methodKey] ?? []).map((attachment) => ({
|
|
77
|
+
...attachment,
|
|
78
|
+
health: health.get(`${serviceKey}:${methodKey}:${attachment.instanceKey}:${attachment.providerMethod}`) ?? attachment.health
|
|
49
79
|
}));
|
|
50
80
|
},
|
|
51
|
-
async recordHealth(serviceKey,
|
|
52
|
-
health.set(`${serviceKey}:${
|
|
81
|
+
async recordHealth(serviceKey, methodKey, instanceKey, providerMethod, next) {
|
|
82
|
+
health.set(`${serviceKey}:${methodKey}:${instanceKey}:${providerMethod}`, next);
|
|
53
83
|
}
|
|
54
84
|
};
|
|
55
85
|
}
|
|
86
|
+
function defineProviderMethod(method) {
|
|
87
|
+
return method;
|
|
88
|
+
}
|
|
56
89
|
function defineProvider(spec) {
|
|
57
90
|
return spec;
|
|
58
91
|
}
|
|
92
|
+
function defineSingleMethodProvider(spec) {
|
|
93
|
+
const { method, invoke, classify, ...identity } = spec;
|
|
94
|
+
return defineProvider({
|
|
95
|
+
...identity,
|
|
96
|
+
methods: { [method]: defineProviderMethod({ invoke, classify }) }
|
|
97
|
+
});
|
|
98
|
+
}
|
|
59
99
|
var STRIKES_TO_OFFLINE = 3;
|
|
100
|
+
function serviceMethod() {
|
|
101
|
+
return Object.freeze({});
|
|
102
|
+
}
|
|
103
|
+
function defineService(definition) {
|
|
104
|
+
return definition;
|
|
105
|
+
}
|
|
60
106
|
function nextHealth(current, kind) {
|
|
61
107
|
if (kind === "success")
|
|
62
108
|
return { strikes: 0, status: "ok" };
|
|
@@ -71,86 +117,194 @@ function nextHealth(current, kind) {
|
|
|
71
117
|
}
|
|
72
118
|
function createRegistry(options) {
|
|
73
119
|
const byId = new Map(options.providers.map((provider) => [provider.id, provider]));
|
|
74
|
-
async function call(serviceKey, args, callOptions = {}) {
|
|
75
|
-
const configured = [...await options.config.list(serviceKey)].filter((
|
|
120
|
+
async function call(serviceKey, methodKey, args, callOptions = {}) {
|
|
121
|
+
const configured = [...await options.config.list(serviceKey, methodKey)].filter((attachment) => attachment.enabled).sort((a, b) => a.priority - b.priority);
|
|
76
122
|
const attempts = [];
|
|
77
|
-
for (const
|
|
123
|
+
for (const attachment of configured) {
|
|
78
124
|
if (callOptions.signal?.aborted) {
|
|
79
125
|
const cancelled = {
|
|
80
126
|
ok: false,
|
|
127
|
+
fallbackUsed: attempts.length > 0,
|
|
81
128
|
attempts,
|
|
82
|
-
error: new ProviderError("CALL_ABORTED", `The "${serviceKey}" call was cancelled.`)
|
|
129
|
+
error: new ProviderError("CALL_ABORTED", `The "${serviceKey}.${methodKey}" call was cancelled.`)
|
|
83
130
|
};
|
|
84
|
-
options.after?.(cancelled);
|
|
131
|
+
await options.after?.(cancelled);
|
|
85
132
|
return cancelled;
|
|
86
133
|
}
|
|
87
|
-
const
|
|
88
|
-
if (!
|
|
89
|
-
attempts.push({ providerId:
|
|
134
|
+
const instance = await options.config.provider(attachment.instanceKey);
|
|
135
|
+
if (!instance) {
|
|
136
|
+
attempts.push({ providerId: "unknown", instanceKey: attachment.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance not registered" });
|
|
90
137
|
continue;
|
|
91
138
|
}
|
|
92
|
-
|
|
93
|
-
|
|
139
|
+
const spec = byId.get(instance.providerId);
|
|
140
|
+
const method = spec?.methods[attachment.providerMethod];
|
|
141
|
+
if (!spec || !method) {
|
|
142
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: !spec ? "provider not registered" : "method not supported" });
|
|
94
143
|
continue;
|
|
95
144
|
}
|
|
96
|
-
|
|
145
|
+
if (!instance.enabled) {
|
|
146
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance disabled" });
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (attachment.health?.status === "offline") {
|
|
150
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "offline" });
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
await options.before?.({ service: serviceKey, method: methodKey, provider: instance.providerId, instance: instance.instanceKey });
|
|
154
|
+
const startedAt = performance.now();
|
|
97
155
|
try {
|
|
98
|
-
const result = await
|
|
99
|
-
config:
|
|
100
|
-
secret: (field) => options.credentials.get(
|
|
156
|
+
const result = await method.invoke({
|
|
157
|
+
config: instance.config,
|
|
158
|
+
secret: (field) => options.credentials.get(instance.secretRef, field),
|
|
101
159
|
signal: callOptions.signal
|
|
102
160
|
}, args);
|
|
103
|
-
attempts.push({ providerId:
|
|
104
|
-
await options.config.recordHealth(serviceKey,
|
|
105
|
-
const sent = {
|
|
106
|
-
|
|
161
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "sent", durationMs: performance.now() - startedAt });
|
|
162
|
+
await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, "success"));
|
|
163
|
+
const sent = {
|
|
164
|
+
ok: true,
|
|
165
|
+
result,
|
|
166
|
+
provider: instance.providerId,
|
|
167
|
+
instance: instance.instanceKey,
|
|
168
|
+
method: attachment.providerMethod,
|
|
169
|
+
selected: { providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod },
|
|
170
|
+
fallbackUsed: attempts.length > 1,
|
|
171
|
+
attempts
|
|
172
|
+
};
|
|
173
|
+
await options.after?.(sent);
|
|
107
174
|
return sent;
|
|
108
175
|
} catch (error) {
|
|
109
176
|
if (callOptions.signal?.aborted) {
|
|
110
177
|
const cancelled = {
|
|
111
178
|
ok: false,
|
|
179
|
+
fallbackUsed: attempts.length > 0,
|
|
112
180
|
attempts,
|
|
113
|
-
error: new ProviderError("CALL_ABORTED", `The "${serviceKey}" call was cancelled.`)
|
|
181
|
+
error: new ProviderError("CALL_ABORTED", `The "${serviceKey}.${methodKey}" call was cancelled.`)
|
|
114
182
|
};
|
|
115
|
-
options.after?.(cancelled);
|
|
183
|
+
await options.after?.(cancelled);
|
|
116
184
|
return cancelled;
|
|
117
185
|
}
|
|
118
|
-
const kind =
|
|
186
|
+
const kind = method.classify(error);
|
|
187
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
188
|
+
const code = error instanceof ProviderError ? error.code : typeof error?.code === "string" ? error.code : undefined;
|
|
119
189
|
attempts.push({
|
|
120
|
-
providerId:
|
|
190
|
+
providerId: instance.providerId,
|
|
191
|
+
instanceKey: instance.instanceKey,
|
|
192
|
+
providerMethod: attachment.providerMethod,
|
|
121
193
|
outcome: "failed",
|
|
122
194
|
kind,
|
|
123
|
-
|
|
195
|
+
durationMs: performance.now() - startedAt,
|
|
196
|
+
failure: { kind, ...code ? { code } : {}, message },
|
|
197
|
+
error: message
|
|
124
198
|
});
|
|
125
|
-
await options.config.recordHealth(serviceKey,
|
|
199
|
+
await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, kind));
|
|
126
200
|
if (kind === "terminal") {
|
|
127
201
|
const refused = {
|
|
128
202
|
ok: false,
|
|
203
|
+
fallbackUsed: attempts.length > 1,
|
|
129
204
|
attempts,
|
|
130
205
|
error: new ProviderError("PAYLOAD_REJECTED", "The request was refused as malformed; no provider will accept it.")
|
|
131
206
|
};
|
|
132
|
-
options.after?.(refused);
|
|
207
|
+
await options.after?.(refused);
|
|
133
208
|
return refused;
|
|
134
209
|
}
|
|
135
210
|
}
|
|
136
211
|
}
|
|
137
212
|
const failed = {
|
|
138
213
|
ok: false,
|
|
214
|
+
fallbackUsed: attempts.length > 1,
|
|
139
215
|
attempts,
|
|
140
|
-
error: new ProviderError(attempts.length === 0 ? "NO_PROVIDER" : "ALL_PROVIDERS_FAILED", attempts.length === 0 ? `No provider is configured for "${serviceKey}".` : `Every provider for "${serviceKey}" failed or was skipped.`)
|
|
216
|
+
error: new ProviderError(attempts.length === 0 ? "NO_PROVIDER" : "ALL_PROVIDERS_FAILED", attempts.length === 0 ? `No provider method is configured for "${serviceKey}.${methodKey}".` : `Every provider method for "${serviceKey}.${methodKey}" failed or was skipped.`)
|
|
141
217
|
};
|
|
142
|
-
options.after?.(failed);
|
|
218
|
+
await options.after?.(failed);
|
|
143
219
|
return failed;
|
|
144
220
|
}
|
|
145
|
-
|
|
221
|
+
function service(definition) {
|
|
222
|
+
return {
|
|
223
|
+
call(method, args, callOptions) {
|
|
224
|
+
return call(definition.key, method, args, callOptions);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return { call, service };
|
|
229
|
+
}
|
|
230
|
+
function createService(definition, methods, options = {}) {
|
|
231
|
+
const providers = new Map;
|
|
232
|
+
const credentials = new Map;
|
|
233
|
+
const instances = [];
|
|
234
|
+
const serviceMethods = {};
|
|
235
|
+
for (const methodKey of Object.keys(definition.methods)) {
|
|
236
|
+
const attachments = methods[methodKey];
|
|
237
|
+
if (!Array.isArray(attachments)) {
|
|
238
|
+
throw new ProviderError("CONFIG_METHOD_MISSING", `Service method "${definition.key}.${methodKey}" needs an attachment array.`);
|
|
239
|
+
}
|
|
240
|
+
const priorities = new Set;
|
|
241
|
+
serviceMethods[methodKey] = attachments.map((attachment, index) => {
|
|
242
|
+
if (!Number.isInteger(attachment.priority) || attachment.priority < 1 || attachment.priority > 1000) {
|
|
243
|
+
throw new ProviderError("CONFIG_PRIORITY", `Priority for "${definition.key}.${methodKey}" must be an integer from 1 to 1000.`);
|
|
244
|
+
}
|
|
245
|
+
if (priorities.has(attachment.priority)) {
|
|
246
|
+
throw new ProviderError("CONFIG_PRIORITY_DUPLICATE", `Priorities for "${definition.key}.${methodKey}" must be unique.`);
|
|
247
|
+
}
|
|
248
|
+
priorities.add(attachment.priority);
|
|
249
|
+
if (!attachment.provider.methods[attachment.method]) {
|
|
250
|
+
throw new ProviderError("CONFIG_METHOD_UNSUPPORTED", `Provider "${attachment.provider.id}" does not support "${attachment.method}".`);
|
|
251
|
+
}
|
|
252
|
+
const existing = providers.get(attachment.provider.id);
|
|
253
|
+
if (existing && existing !== attachment.provider) {
|
|
254
|
+
throw new ProviderError("CONFIG_PROVIDER_DUPLICATE", `Provider id "${attachment.provider.id}" has more than one definition.`);
|
|
255
|
+
}
|
|
256
|
+
providers.set(attachment.provider.id, attachment.provider);
|
|
257
|
+
const internalKey = `${definition.key}:${methodKey}:${index}`;
|
|
258
|
+
credentials.set(internalKey, attachment.credentials);
|
|
259
|
+
instances.push({
|
|
260
|
+
instanceKey: internalKey,
|
|
261
|
+
providerId: attachment.provider.id,
|
|
262
|
+
enabled: attachment.enabled ?? true,
|
|
263
|
+
config: { ...attachment.config ?? {} },
|
|
264
|
+
secretRef: internalKey
|
|
265
|
+
});
|
|
266
|
+
return {
|
|
267
|
+
instanceKey: internalKey,
|
|
268
|
+
providerMethod: attachment.method,
|
|
269
|
+
priority: attachment.priority,
|
|
270
|
+
enabled: attachment.enabled ?? true
|
|
271
|
+
};
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
const registry = createRegistry({
|
|
275
|
+
providers: [...providers.values()],
|
|
276
|
+
config: staticConfig({ providers: instances, services: { [definition.key]: serviceMethods } }),
|
|
277
|
+
credentials: {
|
|
278
|
+
name: [...new Set([...credentials.values()].map((source) => source.name))].join("+"),
|
|
279
|
+
async get(reference, field) {
|
|
280
|
+
const source = credentials.get(reference);
|
|
281
|
+
if (!source)
|
|
282
|
+
throw new ProviderError("CREDENTIAL_SOURCE_MISSING", "The provider credential source is unavailable.");
|
|
283
|
+
return source.get(field);
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
...options
|
|
287
|
+
});
|
|
288
|
+
const dynamic = registry.service(definition);
|
|
289
|
+
return {
|
|
290
|
+
async call(method, args, callOptions) {
|
|
291
|
+
const result = await dynamic.call(method, args, callOptions);
|
|
292
|
+
const { instance: _instance, selected, attempts, ...rest } = result;
|
|
293
|
+
return {
|
|
294
|
+
...rest,
|
|
295
|
+
...selected ? { selected: { providerId: selected.providerId, providerMethod: selected.providerMethod } } : {},
|
|
296
|
+
attempts: attempts.map(({ instanceKey: _instanceKey, ...attempt }) => attempt)
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
};
|
|
146
300
|
}
|
|
147
|
-
var VERSION = "0.1.
|
|
301
|
+
var VERSION = "0.1.11";
|
|
148
302
|
|
|
149
303
|
// src/translation.ts
|
|
150
304
|
var GOOGLE_ENDPOINT = "https://generativelanguage.googleapis.com/v1beta/models";
|
|
151
|
-
var googleAiStudio =
|
|
305
|
+
var googleAiStudio = defineSingleMethodProvider({
|
|
152
306
|
id: "google-ai-studio",
|
|
153
|
-
|
|
307
|
+
method: "translate",
|
|
154
308
|
label: "Google AI Studio",
|
|
155
309
|
credentials: {
|
|
156
310
|
type: "object",
|