@forgezero/providers 0.1.8 → 0.1.10

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.
@@ -0,0 +1,66 @@
1
+ // src/realtime.ts
2
+ import { realtimeBatchBytes, realtimeHmac, validateRealtimeBatch } from "@forgezero/runtime/realtime";
3
+
4
+ class RealtimeProviderError extends Error {
5
+ code;
6
+ constructor(code, message) {
7
+ super(message);
8
+ this.code = code;
9
+ this.name = "RealtimeProviderError";
10
+ }
11
+ }
12
+ var producerPattern = /^[A-Za-z0-9][A-Za-z0-9_.:@/-]{0,127}$/;
13
+ function cloudflareRealtime(options) {
14
+ const endpoint = new URL(options.endpoint);
15
+ if (endpoint.protocol !== "https:" || endpoint.username || endpoint.password || endpoint.search || endpoint.hash || !producerPattern.test(options.producer) || new TextEncoder().encode(options.secret).byteLength < 32) {
16
+ throw new RealtimeProviderError("INVALID_CONFIG", "Realtime endpoint, producer, or secret is invalid.");
17
+ }
18
+ endpoint.pathname = "/__fz/realtime/publish";
19
+ const timeout = options.requestTimeoutMs ?? 5000;
20
+ if (!Number.isSafeInteger(timeout) || timeout < 100 || timeout > 30000) {
21
+ throw new RealtimeProviderError("INVALID_CONFIG", "Realtime timeout is invalid.");
22
+ }
23
+ const fetcher = options.fetcher ?? fetch;
24
+ return {
25
+ async publish(input) {
26
+ const batch = validateRealtimeBatch(input);
27
+ const body = realtimeBatchBytes(batch);
28
+ const timestamp = Math.floor(Date.now() / 1000).toString();
29
+ const signature = await realtimeHmac(options.secret, `publish
30
+ ${options.producer}
31
+ ${timestamp}
32
+ ${body}`);
33
+ let response;
34
+ try {
35
+ response = await fetcher(endpoint, {
36
+ method: "POST",
37
+ body,
38
+ redirect: "error",
39
+ signal: AbortSignal.timeout(timeout),
40
+ headers: {
41
+ "content-type": "application/json",
42
+ "x-fz-realtime-producer": options.producer,
43
+ "x-fz-realtime-timestamp": timestamp,
44
+ "x-fz-realtime-signature": signature,
45
+ "idempotency-key": batch.batchId
46
+ }
47
+ });
48
+ } catch {
49
+ throw new RealtimeProviderError("UNAVAILABLE", "Realtime edge is unavailable.");
50
+ }
51
+ if (!response.ok) {
52
+ await response.body?.cancel();
53
+ throw new RealtimeProviderError(response.status >= 500 ? "UNAVAILABLE" : "REFUSED", `Realtime edge returned HTTP ${response.status}.`);
54
+ }
55
+ const result = await response.json();
56
+ if (!Number.isSafeInteger(result.delivered) || !Number.isSafeInteger(result.shards)) {
57
+ throw new RealtimeProviderError("UNAVAILABLE", "Realtime edge returned malformed evidence.");
58
+ }
59
+ return { delivered: result.delivered, shards: result.shards };
60
+ }
61
+ };
62
+ }
63
+ export {
64
+ cloudflareRealtime,
65
+ RealtimeProviderError
66
+ };
package/dist/storage.d.ts CHANGED
@@ -133,5 +133,5 @@ export type StorageRequest = {
133
133
  expiresInSec?: number;
134
134
  method?: 'GET' | 'PUT';
135
135
  };
136
- export declare const s3: import("./index").ProviderSpec<StorageRequest, unknown>;
137
- export declare const storageProviders: readonly [import("./index").ProviderSpec<StorageRequest, unknown>];
136
+ export declare const s3: import("./index").ProviderDefinition<Record<"request", import("./index").ProviderMethodSpec<StorageRequest, unknown>>>;
137
+ export declare const storageProviders: readonly [import("./index").ProviderDefinition<Record<"request", import("./index").ProviderMethodSpec<StorageRequest, unknown>>>];
package/dist/storage.js CHANGED
@@ -38,25 +38,45 @@ function chainCredentials(...sources) {
38
38
  }
39
39
  };
40
40
  }
41
- function staticConfig(services) {
41
+ function staticConfig(config) {
42
42
  const health = new Map;
43
43
  return {
44
44
  name: "static",
45
- async list(serviceKey) {
46
- return (services[serviceKey] ?? []).map((provider) => ({
47
- ...provider,
48
- health: health.get(`${serviceKey}:${provider.providerId}`) ?? provider.health
45
+ async provider(instanceKey) {
46
+ const provider = config.providers[instanceKey];
47
+ return provider ? { instanceKey, ...provider, config: { ...provider.config } } : undefined;
48
+ },
49
+ async list(serviceKey, methodKey) {
50
+ return (config.services[serviceKey]?.[methodKey] ?? []).map((attachment) => ({
51
+ ...attachment,
52
+ health: health.get(`${serviceKey}:${methodKey}:${attachment.instanceKey}:${attachment.providerMethod}`) ?? attachment.health
49
53
  }));
50
54
  },
51
- async recordHealth(serviceKey, providerId, next) {
52
- health.set(`${serviceKey}:${providerId}`, next);
55
+ async recordHealth(serviceKey, methodKey, instanceKey, providerMethod, next) {
56
+ health.set(`${serviceKey}:${methodKey}:${instanceKey}:${providerMethod}`, next);
53
57
  }
54
58
  };
55
59
  }
60
+ function defineProviderMethod(method) {
61
+ return method;
62
+ }
56
63
  function defineProvider(spec) {
57
64
  return spec;
58
65
  }
66
+ function defineSingleMethodProvider(spec) {
67
+ const { method, invoke, classify, ...identity } = spec;
68
+ return defineProvider({
69
+ ...identity,
70
+ methods: { [method]: defineProviderMethod({ invoke, classify }) }
71
+ });
72
+ }
59
73
  var STRIKES_TO_OFFLINE = 3;
74
+ function serviceMethod() {
75
+ return Object.freeze({});
76
+ }
77
+ function defineService(definition) {
78
+ return definition;
79
+ }
60
80
  function nextHealth(current, kind) {
61
81
  if (kind === "success")
62
82
  return { strikes: 0, status: "ok" };
@@ -71,80 +91,117 @@ function nextHealth(current, kind) {
71
91
  }
72
92
  function createRegistry(options) {
73
93
  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);
94
+ async function call(serviceKey, methodKey, args, callOptions = {}) {
95
+ const configured = [...await options.config.list(serviceKey, methodKey)].filter((attachment) => attachment.enabled).sort((a, b) => a.priority - b.priority);
76
96
  const attempts = [];
77
- for (const entry of configured) {
97
+ for (const attachment of configured) {
78
98
  if (callOptions.signal?.aborted) {
79
99
  const cancelled = {
80
100
  ok: false,
101
+ fallbackUsed: attempts.length > 0,
81
102
  attempts,
82
- error: new ProviderError("CALL_ABORTED", `The "${serviceKey}" call was cancelled.`)
103
+ error: new ProviderError("CALL_ABORTED", `The "${serviceKey}.${methodKey}" call was cancelled.`)
83
104
  };
84
- options.after?.(cancelled);
105
+ await options.after?.(cancelled);
85
106
  return cancelled;
86
107
  }
87
- const spec = byId.get(entry.providerId);
88
- if (!spec) {
89
- attempts.push({ providerId: entry.providerId, outcome: "skipped", error: "not registered" });
108
+ const instance = await options.config.provider(attachment.instanceKey);
109
+ if (!instance) {
110
+ attempts.push({ providerId: "unknown", instanceKey: attachment.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance not registered" });
111
+ continue;
112
+ }
113
+ const spec = byId.get(instance.providerId);
114
+ const method = spec?.methods[attachment.providerMethod];
115
+ if (!spec || !method) {
116
+ attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: !spec ? "provider not registered" : "method not supported" });
90
117
  continue;
91
118
  }
92
- if (entry.health?.status === "offline") {
93
- attempts.push({ providerId: entry.providerId, outcome: "skipped", error: "offline" });
119
+ if (!instance.enabled) {
120
+ attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance disabled" });
94
121
  continue;
95
122
  }
96
- options.before?.({ service: serviceKey, provider: entry.providerId });
123
+ if (attachment.health?.status === "offline") {
124
+ attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "offline" });
125
+ continue;
126
+ }
127
+ await options.before?.({ service: serviceKey, method: methodKey, provider: instance.providerId, instance: instance.instanceKey });
128
+ const startedAt = performance.now();
97
129
  try {
98
- const result = await spec.invoke({
99
- config: entry.config,
100
- secret: (field) => options.credentials.get(entry.secretRef, field),
130
+ const result = await method.invoke({
131
+ config: instance.config,
132
+ secret: (field) => options.credentials.get(instance.secretRef, field),
101
133
  signal: callOptions.signal
102
134
  }, 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);
135
+ attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "sent", durationMs: performance.now() - startedAt });
136
+ await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, "success"));
137
+ const sent = {
138
+ ok: true,
139
+ result,
140
+ provider: instance.providerId,
141
+ instance: instance.instanceKey,
142
+ method: attachment.providerMethod,
143
+ selected: { providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod },
144
+ fallbackUsed: attempts.length > 1,
145
+ attempts
146
+ };
147
+ await options.after?.(sent);
107
148
  return sent;
108
149
  } catch (error) {
109
150
  if (callOptions.signal?.aborted) {
110
151
  const cancelled = {
111
152
  ok: false,
153
+ fallbackUsed: attempts.length > 0,
112
154
  attempts,
113
- error: new ProviderError("CALL_ABORTED", `The "${serviceKey}" call was cancelled.`)
155
+ error: new ProviderError("CALL_ABORTED", `The "${serviceKey}.${methodKey}" call was cancelled.`)
114
156
  };
115
- options.after?.(cancelled);
157
+ await options.after?.(cancelled);
116
158
  return cancelled;
117
159
  }
118
- const kind = spec.classify(error);
160
+ const kind = method.classify(error);
161
+ const message = error instanceof Error ? error.message : String(error);
162
+ const code = error instanceof ProviderError ? error.code : typeof error?.code === "string" ? error.code : undefined;
119
163
  attempts.push({
120
- providerId: entry.providerId,
164
+ providerId: instance.providerId,
165
+ instanceKey: instance.instanceKey,
166
+ providerMethod: attachment.providerMethod,
121
167
  outcome: "failed",
122
168
  kind,
123
- error: error instanceof Error ? error.message : String(error)
169
+ durationMs: performance.now() - startedAt,
170
+ failure: { kind, ...code ? { code } : {}, message },
171
+ error: message
124
172
  });
125
- await options.config.recordHealth(serviceKey, entry.providerId, nextHealth(entry.health, kind));
173
+ await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, kind));
126
174
  if (kind === "terminal") {
127
175
  const refused = {
128
176
  ok: false,
177
+ fallbackUsed: attempts.length > 1,
129
178
  attempts,
130
179
  error: new ProviderError("PAYLOAD_REJECTED", "The request was refused as malformed; no provider will accept it.")
131
180
  };
132
- options.after?.(refused);
181
+ await options.after?.(refused);
133
182
  return refused;
134
183
  }
135
184
  }
136
185
  }
137
186
  const failed = {
138
187
  ok: false,
188
+ fallbackUsed: attempts.length > 1,
139
189
  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.`)
190
+ 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
191
  };
142
- options.after?.(failed);
192
+ await options.after?.(failed);
143
193
  return failed;
144
194
  }
145
- return { call };
195
+ function service(definition) {
196
+ return {
197
+ call(method, args, callOptions) {
198
+ return call(definition.key, method, args, callOptions);
199
+ }
200
+ };
201
+ }
202
+ return { call, service };
146
203
  }
147
- var VERSION = "0.1.8";
204
+ var VERSION = "0.1.10";
148
205
 
149
206
  // src/storage.ts
150
207
  var encoder = new TextEncoder;
@@ -381,9 +438,9 @@ var extractS3Message = (xml) => {
381
438
  const message = between(xml, "<Message>", "</Message>");
382
439
  return code || message ? `${code ?? ""}${code && message ? ": " : ""}${message ?? ""}` : undefined;
383
440
  };
384
- var s3 = defineProvider({
441
+ var s3 = defineSingleMethodProvider({
385
442
  id: "s3",
386
- service: "storage",
443
+ method: "request",
387
444
  label: "S3-compatible storage",
388
445
  multiInstance: true,
389
446
  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,45 @@ function chainCredentials(...sources) {
38
38
  }
39
39
  };
40
40
  }
41
- function staticConfig(services) {
41
+ function staticConfig(config) {
42
42
  const health = new Map;
43
43
  return {
44
44
  name: "static",
45
- async list(serviceKey) {
46
- return (services[serviceKey] ?? []).map((provider) => ({
47
- ...provider,
48
- health: health.get(`${serviceKey}:${provider.providerId}`) ?? provider.health
45
+ async provider(instanceKey) {
46
+ const provider = config.providers[instanceKey];
47
+ return provider ? { instanceKey, ...provider, config: { ...provider.config } } : undefined;
48
+ },
49
+ async list(serviceKey, methodKey) {
50
+ return (config.services[serviceKey]?.[methodKey] ?? []).map((attachment) => ({
51
+ ...attachment,
52
+ health: health.get(`${serviceKey}:${methodKey}:${attachment.instanceKey}:${attachment.providerMethod}`) ?? attachment.health
49
53
  }));
50
54
  },
51
- async recordHealth(serviceKey, providerId, next) {
52
- health.set(`${serviceKey}:${providerId}`, next);
55
+ async recordHealth(serviceKey, methodKey, instanceKey, providerMethod, next) {
56
+ health.set(`${serviceKey}:${methodKey}:${instanceKey}:${providerMethod}`, next);
53
57
  }
54
58
  };
55
59
  }
60
+ function defineProviderMethod(method) {
61
+ return method;
62
+ }
56
63
  function defineProvider(spec) {
57
64
  return spec;
58
65
  }
66
+ function defineSingleMethodProvider(spec) {
67
+ const { method, invoke, classify, ...identity } = spec;
68
+ return defineProvider({
69
+ ...identity,
70
+ methods: { [method]: defineProviderMethod({ invoke, classify }) }
71
+ });
72
+ }
59
73
  var STRIKES_TO_OFFLINE = 3;
74
+ function serviceMethod() {
75
+ return Object.freeze({});
76
+ }
77
+ function defineService(definition) {
78
+ return definition;
79
+ }
60
80
  function nextHealth(current, kind) {
61
81
  if (kind === "success")
62
82
  return { strikes: 0, status: "ok" };
@@ -71,86 +91,123 @@ function nextHealth(current, kind) {
71
91
  }
72
92
  function createRegistry(options) {
73
93
  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);
94
+ async function call(serviceKey, methodKey, args, callOptions = {}) {
95
+ const configured = [...await options.config.list(serviceKey, methodKey)].filter((attachment) => attachment.enabled).sort((a, b) => a.priority - b.priority);
76
96
  const attempts = [];
77
- for (const entry of configured) {
97
+ for (const attachment of configured) {
78
98
  if (callOptions.signal?.aborted) {
79
99
  const cancelled = {
80
100
  ok: false,
101
+ fallbackUsed: attempts.length > 0,
81
102
  attempts,
82
- error: new ProviderError("CALL_ABORTED", `The "${serviceKey}" call was cancelled.`)
103
+ error: new ProviderError("CALL_ABORTED", `The "${serviceKey}.${methodKey}" call was cancelled.`)
83
104
  };
84
- options.after?.(cancelled);
105
+ await options.after?.(cancelled);
85
106
  return cancelled;
86
107
  }
87
- const spec = byId.get(entry.providerId);
88
- if (!spec) {
89
- attempts.push({ providerId: entry.providerId, outcome: "skipped", error: "not registered" });
108
+ const instance = await options.config.provider(attachment.instanceKey);
109
+ if (!instance) {
110
+ attempts.push({ providerId: "unknown", instanceKey: attachment.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance not registered" });
111
+ continue;
112
+ }
113
+ const spec = byId.get(instance.providerId);
114
+ const method = spec?.methods[attachment.providerMethod];
115
+ if (!spec || !method) {
116
+ attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: !spec ? "provider not registered" : "method not supported" });
90
117
  continue;
91
118
  }
92
- if (entry.health?.status === "offline") {
93
- attempts.push({ providerId: entry.providerId, outcome: "skipped", error: "offline" });
119
+ if (!instance.enabled) {
120
+ attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance disabled" });
94
121
  continue;
95
122
  }
96
- options.before?.({ service: serviceKey, provider: entry.providerId });
123
+ if (attachment.health?.status === "offline") {
124
+ attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "offline" });
125
+ continue;
126
+ }
127
+ await options.before?.({ service: serviceKey, method: methodKey, provider: instance.providerId, instance: instance.instanceKey });
128
+ const startedAt = performance.now();
97
129
  try {
98
- const result = await spec.invoke({
99
- config: entry.config,
100
- secret: (field) => options.credentials.get(entry.secretRef, field),
130
+ const result = await method.invoke({
131
+ config: instance.config,
132
+ secret: (field) => options.credentials.get(instance.secretRef, field),
101
133
  signal: callOptions.signal
102
134
  }, 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);
135
+ attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "sent", durationMs: performance.now() - startedAt });
136
+ await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, "success"));
137
+ const sent = {
138
+ ok: true,
139
+ result,
140
+ provider: instance.providerId,
141
+ instance: instance.instanceKey,
142
+ method: attachment.providerMethod,
143
+ selected: { providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod },
144
+ fallbackUsed: attempts.length > 1,
145
+ attempts
146
+ };
147
+ await options.after?.(sent);
107
148
  return sent;
108
149
  } catch (error) {
109
150
  if (callOptions.signal?.aborted) {
110
151
  const cancelled = {
111
152
  ok: false,
153
+ fallbackUsed: attempts.length > 0,
112
154
  attempts,
113
- error: new ProviderError("CALL_ABORTED", `The "${serviceKey}" call was cancelled.`)
155
+ error: new ProviderError("CALL_ABORTED", `The "${serviceKey}.${methodKey}" call was cancelled.`)
114
156
  };
115
- options.after?.(cancelled);
157
+ await options.after?.(cancelled);
116
158
  return cancelled;
117
159
  }
118
- const kind = spec.classify(error);
160
+ const kind = method.classify(error);
161
+ const message = error instanceof Error ? error.message : String(error);
162
+ const code = error instanceof ProviderError ? error.code : typeof error?.code === "string" ? error.code : undefined;
119
163
  attempts.push({
120
- providerId: entry.providerId,
164
+ providerId: instance.providerId,
165
+ instanceKey: instance.instanceKey,
166
+ providerMethod: attachment.providerMethod,
121
167
  outcome: "failed",
122
168
  kind,
123
- error: error instanceof Error ? error.message : String(error)
169
+ durationMs: performance.now() - startedAt,
170
+ failure: { kind, ...code ? { code } : {}, message },
171
+ error: message
124
172
  });
125
- await options.config.recordHealth(serviceKey, entry.providerId, nextHealth(entry.health, kind));
173
+ await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, kind));
126
174
  if (kind === "terminal") {
127
175
  const refused = {
128
176
  ok: false,
177
+ fallbackUsed: attempts.length > 1,
129
178
  attempts,
130
179
  error: new ProviderError("PAYLOAD_REJECTED", "The request was refused as malformed; no provider will accept it.")
131
180
  };
132
- options.after?.(refused);
181
+ await options.after?.(refused);
133
182
  return refused;
134
183
  }
135
184
  }
136
185
  }
137
186
  const failed = {
138
187
  ok: false,
188
+ fallbackUsed: attempts.length > 1,
139
189
  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.`)
190
+ 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
191
  };
142
- options.after?.(failed);
192
+ await options.after?.(failed);
143
193
  return failed;
144
194
  }
145
- return { call };
195
+ function service(definition) {
196
+ return {
197
+ call(method, args, callOptions) {
198
+ return call(definition.key, method, args, callOptions);
199
+ }
200
+ };
201
+ }
202
+ return { call, service };
146
203
  }
147
- var VERSION = "0.1.8";
204
+ var VERSION = "0.1.10";
148
205
 
149
206
  // src/translation.ts
150
207
  var GOOGLE_ENDPOINT = "https://generativelanguage.googleapis.com/v1beta/models";
151
- var googleAiStudio = defineProvider({
208
+ var googleAiStudio = defineSingleMethodProvider({
152
209
  id: "google-ai-studio",
153
- service: "translation",
210
+ method: "translate",
154
211
  label: "Google AI Studio",
155
212
  credentials: {
156
213
  type: "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgezero/providers",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -42,12 +42,16 @@
42
42
  "./translation": {
43
43
  "types": "./dist/translation.d.ts",
44
44
  "default": "./dist/translation.js"
45
- }
46
- },
45
+ },
46
+ "./realtime": {
47
+ "types": "./dist/realtime.d.ts",
48
+ "default": "./dist/realtime.js"
49
+ }
50
+ },
47
51
  "scripts": {
48
52
  "check": "tsc --noEmit",
49
53
  "prebuild": "rm -rf dist",
50
- "build": "bun build src/index.ts src/email.ts src/chain.ts src/database.ts src/http.ts src/pool.ts src/storage.ts src/binance.ts src/translation.ts --root src --outdir dist --target browser --format esm --packages external && tsc --emitDeclarationOnly --declaration --noEmit false --outDir dist",
54
+ "build": "bun build src/index.ts src/email.ts src/chain.ts src/database.ts src/http.ts src/pool.ts src/storage.ts src/binance.ts src/translation.ts src/realtime.ts --root src --outdir dist --target browser --format esm --packages external && tsc --emitDeclarationOnly --declaration --noEmit false --outDir dist",
51
55
  "prepublishOnly": "bun run check && bun run build"
52
56
  },
53
57
  "devDependencies": {
@@ -82,6 +86,6 @@
82
86
  "LICENSE"
83
87
  ],
84
88
  "dependencies": {
85
- "@forgezero/runtime": "^0.1.4"
89
+ "@forgezero/runtime": "^0.1.6"
86
90
  }
87
91
  }