@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/dist/storage.js CHANGED
@@ -38,25 +38,71 @@ function chainCredentials(...sources) {
38
38
  }
39
39
  };
40
40
  }
41
- function staticConfig(services) {
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 list(serviceKey) {
46
- return (services[serviceKey] ?? []).map((provider) => ({
47
- ...provider,
48
- health: health.get(`${serviceKey}:${provider.providerId}`) ?? provider.health
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, providerId, next) {
52
- health.set(`${serviceKey}:${providerId}`, next);
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((provider) => provider.enabled).sort((a, b) => a.priority - b.priority);
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 entry of configured) {
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 spec = byId.get(entry.providerId);
88
- if (!spec) {
89
- attempts.push({ providerId: entry.providerId, outcome: "skipped", error: "not registered" });
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
- if (entry.health?.status === "offline") {
93
- attempts.push({ providerId: entry.providerId, outcome: "skipped", error: "offline" });
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
- options.before?.({ service: serviceKey, provider: entry.providerId });
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 spec.invoke({
99
- config: entry.config,
100
- secret: (field) => options.credentials.get(entry.secretRef, field),
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: entry.providerId, outcome: "sent" });
104
- await options.config.recordHealth(serviceKey, entry.providerId, nextHealth(entry.health, "success"));
105
- const sent = { ok: true, result, provider: entry.providerId, attempts };
106
- options.after?.(sent);
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 = spec.classify(error);
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: entry.providerId,
190
+ providerId: instance.providerId,
191
+ instanceKey: instance.instanceKey,
192
+ providerMethod: attachment.providerMethod,
121
193
  outcome: "failed",
122
194
  kind,
123
- error: error instanceof Error ? error.message : String(error)
195
+ durationMs: performance.now() - startedAt,
196
+ failure: { kind, ...code ? { code } : {}, message },
197
+ error: message
124
198
  });
125
- await options.config.recordHealth(serviceKey, entry.providerId, nextHealth(entry.health, kind));
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
- return { call };
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.9";
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 = defineProvider({
538
+ var s3 = defineSingleMethodProvider({
385
539
  id: "s3",
386
- service: "storage",
540
+ method: "request",
387
541
  label: "S3-compatible storage",
388
542
  multiInstance: true,
389
543
  credentials: {
@@ -1,4 +1,4 @@
1
- import { defineProvider } from './index';
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 = ReturnType<typeof defineProvider<TranslateRequest, TranslateResult>>;
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: import("./index").ProviderSpec<TranslateRequest, TranslateResult>;
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 [import("./index").ProviderSpec<TranslateRequest, TranslateResult>];
72
+ export declare const translationProviders: readonly [ProviderDefinition<Record<"translate", ProviderMethodSpec<TranslateRequest, TranslateResult>>>];
@@ -38,25 +38,71 @@ function chainCredentials(...sources) {
38
38
  }
39
39
  };
40
40
  }
41
- function staticConfig(services) {
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 list(serviceKey) {
46
- return (services[serviceKey] ?? []).map((provider) => ({
47
- ...provider,
48
- health: health.get(`${serviceKey}:${provider.providerId}`) ?? provider.health
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, providerId, next) {
52
- health.set(`${serviceKey}:${providerId}`, next);
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((provider) => provider.enabled).sort((a, b) => a.priority - b.priority);
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 entry of configured) {
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 spec = byId.get(entry.providerId);
88
- if (!spec) {
89
- attempts.push({ providerId: entry.providerId, outcome: "skipped", error: "not registered" });
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
- if (entry.health?.status === "offline") {
93
- attempts.push({ providerId: entry.providerId, outcome: "skipped", error: "offline" });
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
- options.before?.({ service: serviceKey, provider: entry.providerId });
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 spec.invoke({
99
- config: entry.config,
100
- secret: (field) => options.credentials.get(entry.secretRef, field),
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: entry.providerId, outcome: "sent" });
104
- await options.config.recordHealth(serviceKey, entry.providerId, nextHealth(entry.health, "success"));
105
- const sent = { ok: true, result, provider: entry.providerId, attempts };
106
- options.after?.(sent);
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 = spec.classify(error);
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: entry.providerId,
190
+ providerId: instance.providerId,
191
+ instanceKey: instance.instanceKey,
192
+ providerMethod: attachment.providerMethod,
121
193
  outcome: "failed",
122
194
  kind,
123
- error: error instanceof Error ? error.message : String(error)
195
+ durationMs: performance.now() - startedAt,
196
+ failure: { kind, ...code ? { code } : {}, message },
197
+ error: message
124
198
  });
125
- await options.config.recordHealth(serviceKey, entry.providerId, nextHealth(entry.health, kind));
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
- return { call };
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.9";
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 = defineProvider({
305
+ var googleAiStudio = defineSingleMethodProvider({
152
306
  id: "google-ai-studio",
153
- service: "translation",
307
+ method: "translate",
154
308
  label: "Google AI Studio",
155
309
  credentials: {
156
310
  type: "object",