@chainfuse/ai-tools 0.8.0 → 0.10.0

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/base.d.mts CHANGED
@@ -4,4 +4,6 @@ export declare class AiBase {
4
4
  protected _config: AiConfig;
5
5
  constructor(config: AiConfig);
6
6
  get config(): Readonly<AiConfig>;
7
+ protected get gatewayName(): "nocost" | "production-passive-bot" | "production-passive-human" | "production-active-bot" | "production-active-human" | "preview-passive-bot" | "preview-passive-human" | "preview-active-bot" | "preview-active-human";
8
+ protected get gatewayLog(): boolean;
7
9
  }
package/dist/base.mjs CHANGED
@@ -8,4 +8,20 @@ export class AiBase {
8
8
  get config() {
9
9
  return Object.freeze(this._config);
10
10
  }
11
+ get gatewayName() {
12
+ if (this.config.billing.noCost) {
13
+ return 'nocost';
14
+ }
15
+ else {
16
+ return `${this.config.billing.environment}-${this.config.billing.action}-${this.config.billing.user}`;
17
+ }
18
+ }
19
+ get gatewayLog() {
20
+ if (this.config.billing.noCost) {
21
+ return true;
22
+ }
23
+ else {
24
+ return this.config.billing.environment !== 'production';
25
+ }
26
+ }
11
27
  }
package/dist/models.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { LanguageModelValues, TextEmbeddingModelValues } from '@chainfuse/types';
2
- import { wrapLanguageModel, type embed, type embedMany } from 'ai';
2
+ import type { embed, embedMany, wrapLanguageModel } from 'ai';
3
3
  import { AiBase } from './base.mjs';
4
- import { AiRegistry } from './registry.mjs';
4
+ import type { AiRegistry } from './registry.mjs';
5
5
  import type { AiRequestConfig } from './types.mjs';
6
6
  type ProvidersReturnType = Awaited<ReturnType<AiRegistry['providers']>>;
7
7
  type ValidProviders = keyof ProvidersReturnType;
package/dist/models.mjs CHANGED
@@ -1,15 +1,15 @@
1
- import { extractReasoningMiddleware, wrapLanguageModel } from 'ai';
2
1
  import { AiBase } from './base.mjs';
3
- import { AiRegistry } from './registry.mjs';
4
2
  export class AiModel extends AiBase {
5
3
  wrappedLanguageModel(args, modelOrProvider, model) {
6
- return new AiRegistry(this.config).registry(args).then((registry) => wrapLanguageModel({
4
+ return import('./registry.mjs')
5
+ .then(({ AiRegistry }) => new AiRegistry(this.config).registry(args))
6
+ .then((registry) => import('ai').then(({ extractReasoningMiddleware, wrapLanguageModel }) => wrapLanguageModel({
7
7
  model: registry.languageModel(model ? `${modelOrProvider}:${model}` : modelOrProvider),
8
8
  middleware: [extractReasoningMiddleware({ tagName: 'think' }), this.middleware],
9
- }));
9
+ })));
10
10
  }
11
11
  wrappedTextEmbeddingModel(args, modelOrProvider, model) {
12
- return new AiRegistry(this.config).registry(args).then((registry) => registry.textEmbeddingModel(model ? `${modelOrProvider}:${model}` : modelOrProvider));
12
+ return import('./registry.mjs').then(({ AiRegistry }) => new AiRegistry(this.config).registry(args)).then((registry) => registry.textEmbeddingModel(model ? `${modelOrProvider}:${model}` : modelOrProvider));
13
13
  }
14
14
  get middleware() {
15
15
  return {};
@@ -1,11 +1,11 @@
1
1
  import type { GoogleGenerativeAIProvider } from '@ai-sdk/google';
2
2
  import type { OpenAICompatibleProvider } from '@ai-sdk/openai-compatible';
3
3
  import { AiBase } from '../base.mjs';
4
- import type { AiRequestConfig } from '../types.mjs';
4
+ import type { AiRequestConfig, AzureServers } from '../types.mjs';
5
5
  import type { AzureOpenAIProvider, WorkersAIProvider } from './types.mjs';
6
6
  export declare class AiCustomProviders extends AiBase {
7
7
  oaiOpenai(args: AiRequestConfig): Promise<import("@ai-sdk/openai").OpenAIProvider>;
8
- azOpenai(args: AiRequestConfig, [server, ...servers]?: import("../serverSelector/types.mts").Server[]): Promise<AzureOpenAIProvider>;
8
+ azOpenai(args: AiRequestConfig, filteredServers?: AzureServers): Promise<AzureOpenAIProvider>;
9
9
  anthropic(args: AiRequestConfig): Promise<import("@ai-sdk/anthropic").AnthropicProvider>;
10
10
  private static workersAiIsRest;
11
11
  cfWorkersAi(args: AiRequestConfig): Promise<WorkersAIProvider | OpenAICompatibleProvider<"@cf/qwen/qwen1.5-0.5b-chat" | "@cf/google/gemma-2b-it-lora" | "@hf/nexusflow/starling-lm-7b-beta" | "@cf/meta/llama-3-8b-instruct" | "@cf/meta/llama-3.2-3b-instruct" | "@hf/thebloke/llamaguard-7b-awq" | "@hf/thebloke/neural-chat-7b-v3-1-awq" | "@cf/meta/llama-guard-3-8b" | "@cf/meta/llama-2-7b-chat-fp16" | "@cf/mistral/mistral-7b-instruct-v0.1" | "@cf/mistral/mistral-7b-instruct-v0.2-lora" | "@cf/tinyllama/tinyllama-1.1b-chat-v1.0" | "@hf/mistral/mistral-7b-instruct-v0.2" | "@cf/fblgit/una-cybertron-7b-v2-bf16" | "@cf/deepseek-ai/deepseek-r1-distill-qwen-32b" | "@cf/thebloke/discolm-german-7b-v1-awq" | "@cf/meta/llama-2-7b-chat-int8" | "@cf/meta/llama-3.1-8b-instruct-fp8" | "@hf/thebloke/mistral-7b-instruct-v0.1-awq" | "@cf/qwen/qwen1.5-7b-chat-awq" | "@cf/meta/llama-3.2-1b-instruct" | "@hf/thebloke/llama-2-13b-chat-awq" | "@hf/thebloke/deepseek-coder-6.7b-base-awq" | "@cf/meta-llama/llama-2-7b-chat-hf-lora" | "@cf/meta/llama-3.3-70b-instruct-fp8-fast" | "@hf/thebloke/openhermes-2.5-mistral-7b-awq" | "@hf/thebloke/deepseek-coder-6.7b-instruct-awq" | "@cf/deepseek-ai/deepseek-math-7b-instruct" | "@cf/tiiuae/falcon-7b-instruct" | "@hf/nousresearch/hermes-2-pro-mistral-7b" | "@cf/meta/llama-3.1-8b-instruct" | "@cf/meta/llama-3.1-8b-instruct-awq" | "@hf/thebloke/zephyr-7b-beta-awq" | "@cf/google/gemma-7b-it-lora" | "@cf/qwen/qwen1.5-1.8b-chat" | "@cf/meta/llama-3-8b-instruct-awq" | "@cf/meta/llama-3.2-11b-vision-instruct" | "@cf/defog/sqlcoder-7b-2" | "@cf/microsoft/phi-2" | "@hf/meta-llama/meta-llama-3-8b-instruct" | "@hf/google/gemma-7b-it" | "@cf/qwen/qwen1.5-14b-chat-awq" | "@cf/openchat/openchat-3.5-0106", "@cf/qwen/qwen1.5-0.5b-chat" | "@cf/google/gemma-2b-it-lora" | "@hf/nexusflow/starling-lm-7b-beta" | "@cf/meta/llama-3-8b-instruct" | "@cf/meta/llama-3.2-3b-instruct" | "@hf/thebloke/llamaguard-7b-awq" | "@hf/thebloke/neural-chat-7b-v3-1-awq" | "@cf/meta/llama-guard-3-8b" | "@cf/meta/llama-2-7b-chat-fp16" | "@cf/mistral/mistral-7b-instruct-v0.1" | "@cf/mistral/mistral-7b-instruct-v0.2-lora" | "@cf/tinyllama/tinyllama-1.1b-chat-v1.0" | "@hf/mistral/mistral-7b-instruct-v0.2" | "@cf/fblgit/una-cybertron-7b-v2-bf16" | "@cf/deepseek-ai/deepseek-r1-distill-qwen-32b" | "@cf/thebloke/discolm-german-7b-v1-awq" | "@cf/meta/llama-2-7b-chat-int8" | "@cf/meta/llama-3.1-8b-instruct-fp8" | "@hf/thebloke/mistral-7b-instruct-v0.1-awq" | "@cf/qwen/qwen1.5-7b-chat-awq" | "@cf/meta/llama-3.2-1b-instruct" | "@hf/thebloke/llama-2-13b-chat-awq" | "@hf/thebloke/deepseek-coder-6.7b-base-awq" | "@cf/meta-llama/llama-2-7b-chat-hf-lora" | "@cf/meta/llama-3.3-70b-instruct-fp8-fast" | "@hf/thebloke/openhermes-2.5-mistral-7b-awq" | "@hf/thebloke/deepseek-coder-6.7b-instruct-awq" | "@cf/deepseek-ai/deepseek-math-7b-instruct" | "@cf/tiiuae/falcon-7b-instruct" | "@hf/nousresearch/hermes-2-pro-mistral-7b" | "@cf/meta/llama-3.1-8b-instruct" | "@cf/meta/llama-3.1-8b-instruct-awq" | "@hf/thebloke/zephyr-7b-beta-awq" | "@cf/google/gemma-7b-it-lora" | "@cf/qwen/qwen1.5-1.8b-chat" | "@cf/meta/llama-3-8b-instruct-awq" | "@cf/meta/llama-3.2-11b-vision-instruct" | "@cf/defog/sqlcoder-7b-2" | "@cf/microsoft/phi-2" | "@hf/meta-llama/meta-llama-3-8b-instruct" | "@hf/google/gemma-7b-it" | "@cf/qwen/qwen1.5-14b-chat-awq" | "@cf/openchat/openchat-3.5-0106", "@cf/baai/bge-m3" | "@cf/baai/bge-small-en-v1.5" | "@cf/baai/bge-base-en-v1.5" | "@cf/baai/bge-large-en-v1.5">>;
@@ -3,21 +3,24 @@ import { AiModels, enabledCloudflareLlmProviders } from '@chainfuse/types';
3
3
  import { APICallError, customProvider, TypeValidationError, wrapLanguageModel } from 'ai';
4
4
  import { ZodError } from 'zod';
5
5
  import { AiBase } from '../base.mjs';
6
- import { AzureServerSelector } from '../serverSelector/azure.mjs';
6
+ import { ServerSelector } from "../serverSelector.mjs";
7
7
  import { AiRawProviders } from './rawProviders.mjs';
8
8
  export class AiCustomProviders extends AiBase {
9
9
  oaiOpenai(args) {
10
10
  return new AiRawProviders(this.config).oaiOpenai(args);
11
11
  }
12
- async azOpenai(args, [server, ...servers] = new AzureServerSelector(this.config).closestServers()) {
12
+ async azOpenai(args, filteredServers) {
13
+ if (!filteredServers)
14
+ filteredServers = await new ServerSelector(this.config).closestServers(await import('@chainfuse/types/ai-tools/catalog/azure').then(({ azureCatalog }) => azureCatalog));
15
+ const [server, ...servers] = filteredServers;
13
16
  const raw = new AiRawProviders(this.config);
14
17
  return customProvider({
15
18
  // @ts-expect-error override for types
16
19
  languageModels: await server.languageModelAvailability.reduce(async (accPromise, model) => {
17
20
  const acc = await accPromise;
18
21
  // @ts-expect-error override for types
19
- acc[model] = wrapLanguageModel({
20
- model: (await raw.azOpenai(args, server))(model),
22
+ acc[model.name] = wrapLanguageModel({
23
+ model: (await raw.azOpenai(args, server, 'inputTokenCost' in model || 'outputTokenCost' in model ? { inputTokenCost: model.inputTokenCost, outputTokenCost: model.outputTokenCost } : undefined))(model.name),
21
24
  middleware: {
22
25
  wrapGenerate: async ({ doGenerate, model, params }) => {
23
26
  try {
@@ -28,23 +31,34 @@ export class AiCustomProviders extends AiBase {
28
31
  if (APICallError.isInstance(error)) {
29
32
  const idempotencyId = new Headers(error.responseHeaders).get('X-Idempotency-Id');
30
33
  const lastServer = new URL(error.url).pathname.split('/')[5];
31
- const compatibleServers = new AzureServerSelector(this.config).closestServers(model.modelId);
34
+ const compatibleServers = await new ServerSelector(this.config).closestServers(await import('@chainfuse/types/ai-tools/catalog/azure').then(({ azureCatalog }) => azureCatalog), model.modelId);
32
35
  const lastServerIndex = compatibleServers.findIndex((s) => s.id.toLowerCase() === lastServer.toLowerCase());
33
- if (args.logging ?? this.config.environment !== 'production')
36
+ if (args.logging ?? this.gatewayLog)
34
37
  console.error('ai', 'custom provider', this.chalk.rgb(...Helpers.uniqueIdColor(idempotencyId))(`[${idempotencyId}]`), this.chalk.red('FAIL'), compatibleServers[lastServerIndex].id, 'REMAINING', JSON.stringify(compatibleServers.slice(lastServerIndex + 1).map((s) => s.id)));
35
38
  // Should retry with the next server
36
39
  const leftOverServers = compatibleServers.slice(lastServerIndex + 1);
37
40
  const errors = [error];
38
41
  for (const nextServer of leftOverServers) {
39
42
  try {
40
- if (args.logging ?? this.config.environment !== 'production')
43
+ if (args.logging ?? this.gatewayLog)
41
44
  console.error('ai', 'custom provider', this.chalk.rgb(...Helpers.uniqueIdColor(idempotencyId))(`[${idempotencyId}]`), this.chalk.blue('FALLBACK'), nextServer.id, 'REMAINING', JSON.stringify(leftOverServers.slice(leftOverServers.indexOf(nextServer) + 1).map((s) => s.id)));
42
45
  // Must be double awaited to prevent a promise from being returned
43
- return await (await raw.azOpenai({ ...args, idempotencyId }, nextServer))(model.modelId).doGenerate(params);
46
+ return await (await raw.azOpenai({ ...args, idempotencyId }, nextServer, (() => {
47
+ const foundModel = server.languageModelAvailability.find((languageModel) => languageModel.name === model.modelId);
48
+ if (foundModel && ('inputTokenCost' in foundModel || 'outputTokenCost' in foundModel)) {
49
+ return {
50
+ inputTokenCost: foundModel.inputTokenCost,
51
+ outputTokenCost: foundModel.outputTokenCost,
52
+ };
53
+ }
54
+ else {
55
+ return undefined;
56
+ }
57
+ })()))(model.modelId).doGenerate(params);
44
58
  }
45
59
  catch (nextServerError) {
46
60
  if (APICallError.isInstance(nextServerError)) {
47
- if (args.logging ?? this.config.environment !== 'production')
61
+ if (args.logging ?? this.gatewayLog)
48
62
  console.error('ai', 'custom provider', this.chalk.rgb(...Helpers.uniqueIdColor(idempotencyId))(`[${idempotencyId}]`), this.chalk.red('FAIL'), nextServer.id, 'REMAINING', JSON.stringify(leftOverServers.slice(leftOverServers.indexOf(nextServer) + 1).map((s) => s.id)));
49
63
  errors.push(nextServerError);
50
64
  }
@@ -1,14 +1,16 @@
1
1
  import type { OpenAICompatibleProvider } from '@ai-sdk/openai-compatible';
2
2
  import type { cloudflareModelPossibilities } from '@chainfuse/types';
3
3
  import { AiBase } from '../base.mjs';
4
- import type { Server } from '../serverSelector/types.mjs';
5
- import type { AiRequestConfig } from '../types.mjs';
4
+ import type { AiRequestConfig, Servers } from '../types.mjs';
6
5
  import type { WorkersAIProvider } from './types.mts';
7
6
  export declare class AiRawProviders extends AiBase {
8
7
  private readonly cacheTtl;
9
8
  private updateGatewayLog;
10
9
  oaiOpenai(args: AiRequestConfig): Promise<import("@ai-sdk/openai").OpenAIProvider>;
11
- azOpenai(args: AiRequestConfig, server: Server): Promise<import("@ai-sdk/azure").AzureOpenAIProvider>;
10
+ azOpenai(args: AiRequestConfig, server: Servers[number], cost?: {
11
+ inputTokenCost?: number;
12
+ outputTokenCost?: number;
13
+ }): Promise<import("@ai-sdk/azure").AzureOpenAIProvider>;
12
14
  anthropic(args: AiRequestConfig): Promise<import("@ai-sdk/anthropic").AnthropicProvider>;
13
15
  custom(args: AiRequestConfig): Promise<OpenAICompatibleProvider<string, string, string>>;
14
16
  googleAi(args: AiRequestConfig): Promise<import("@ai-sdk/google").GoogleGenerativeAIProvider>;
@@ -1,12 +1,12 @@
1
- import { BufferHelpers, CryptoHelpers, DnsHelpers, Helpers, NetHelpers } from '@chainfuse/helpers';
2
- import haversine from 'haversine-distance';
3
- import { z } from 'zod';
1
+ import { BufferHelpers, CryptoHelpers, Helpers } from '@chainfuse/helpers';
4
2
  import { AiBase } from '../base.mjs';
5
3
  export class AiRawProviders extends AiBase {
6
4
  // 2628288 seconds is what cf defines as 1 month in their cache rules
7
5
  cacheTtl = 2628288;
8
6
  async updateGatewayLog(response, metadataHeader, startRoundTrip, modelTime) {
9
- const updateMetadata = NetHelpers.cfApi(this.config.gateway.apiToken).then((cf) => cf.aiGateway.logs.edit(this.config.environment, response.headers.get('cf-aig-log-id'), {
7
+ const updateMetadata = import('@chainfuse/helpers')
8
+ .then(({ NetHelpers }) => NetHelpers.cfApi(this.config.gateway.apiToken))
9
+ .then((cf) => cf.aiGateway.logs.edit(this.gatewayName, response.headers.get('cf-aig-log-id'), {
10
10
  account_id: this.config.gateway.accountId,
11
11
  metadata: {
12
12
  ...Object.entries({
@@ -34,7 +34,7 @@ export class AiRawProviders extends AiBase {
34
34
  }
35
35
  oaiOpenai(args) {
36
36
  return import('@ai-sdk/openai').then(async ({ createOpenAI }) => createOpenAI({
37
- baseURL: new URL(['v1', this.config.gateway.accountId, this.config.environment, 'openai'].join('/'), 'https://gateway.ai.cloudflare.com').toString(),
37
+ baseURL: new URL(['v1', this.config.gateway.accountId, this.gatewayName, 'openai'].join('/'), 'https://gateway.ai.cloudflare.com').toString(),
38
38
  apiKey: this.config.providers.openAi.apiToken,
39
39
  organization: this.config.providers.openAi.organization,
40
40
  headers: {
@@ -61,10 +61,10 @@ export class AiRawProviders extends AiBase {
61
61
  metadataHeader.idempotencyId = `${metadataHeader.idempotencyId}-${(await CryptoHelpers.getHash('SHA-256', await new Request(input, rawInit).arrayBuffer())).slice(0, 12)}`;
62
62
  headers.set('cf-aig-metadata', JSON.stringify(metadataHeader));
63
63
  }
64
- if (args.logging ?? this.config.environment !== 'production')
64
+ if (args.logging ?? this.gatewayLog)
65
65
  console.info('ai', 'raw provider', this.chalk.rgb(...Helpers.uniqueIdColor(metadataHeader.idempotencyId))(`[${metadataHeader.idempotencyId}]`), this.chalk.magenta(rawInit?.method), this.chalk.magenta(new URL(new Request(input).url).pathname));
66
66
  return fetch(input, { ...rawInit, headers }).then(async (response) => {
67
- if (args.logging ?? this.config.environment !== 'production')
67
+ if (args.logging ?? this.gatewayLog)
68
68
  console.info('ai', 'raw provider', this.chalk.rgb(...Helpers.uniqueIdColor(metadataHeader.idempotencyId))(`[${metadataHeader.idempotencyId}]`), response.ok ? this.chalk.green(response.status) : this.chalk.red(response.status), response.ok ? this.chalk.green(new URL(response.url).pathname) : this.chalk.red(new URL(response.url).pathname));
69
69
  await this.updateGatewayLog(response, metadataHeader, startRoundTrip, response.headers.has('openai-processing-ms') ? parseInt(response.headers.get('openai-processing-ms')) : undefined);
70
70
  // Inject it to have it available for retries
@@ -82,7 +82,7 @@ export class AiRawProviders extends AiBase {
82
82
  },
83
83
  }));
84
84
  }
85
- azOpenai(args, server) {
85
+ azOpenai(args, server, cost) {
86
86
  return import('@ai-sdk/azure').then(async ({ createAzure }) => createAzure({
87
87
  apiKey: this.config.providers.azureOpenAi.apiTokens[`AZURE_API_KEY_${server.id.toUpperCase().replaceAll('-', '_')}`],
88
88
  /**
@@ -90,18 +90,22 @@ export class AiRawProviders extends AiBase {
90
90
  * From the table, pick the `Latest GA release` for `Data plane - inference`
91
91
  */
92
92
  apiVersion: '2024-10-21',
93
- baseURL: new URL(['v1', this.config.gateway.accountId, this.config.environment, 'azure-openai', server.id.toLowerCase()].join('/'), 'https://gateway.ai.cloudflare.com').toString(),
93
+ baseURL: new URL(['v1', this.config.gateway.accountId, this.gatewayName, 'azure-openai', server.id.toLowerCase()].join('/'), 'https://gateway.ai.cloudflare.com').toString(),
94
94
  headers: {
95
95
  'cf-aig-authorization': `Bearer ${this.config.gateway.apiToken}`,
96
+ ...(cost && { 'cf-aig-custom-cost': JSON.stringify({ per_token_in: cost.inputTokenCost ?? undefined, per_token_out: cost.outputTokenCost ?? undefined }) }),
96
97
  'cf-aig-metadata': JSON.stringify({
97
98
  dataspaceId: (await BufferHelpers.uuidConvert(args.dataspaceId)).utf8,
98
99
  messageId: (await BufferHelpers.uuidConvert(args.messageId)).utf8,
99
100
  serverInfo: JSON.stringify({
100
101
  name: `azure-${server.id}`,
101
- distance: haversine({
102
- lat: Helpers.precisionFloat(this.config.geoRouting?.userCoordinate?.lat ?? '0'),
103
- lon: Helpers.precisionFloat(this.config.geoRouting?.userCoordinate?.lon ?? '0'),
104
- }, server.coordinate),
102
+ distance: await import('haversine-distance').then(async ({ default: haversine }) => haversine(await import("../serverSelector.mjs").then(({ ServerSelector }) => new ServerSelector(this.config).determineLocation().then(({ coordinate }) => ({
103
+ lat: Helpers.precisionFloat(coordinate.lat),
104
+ lon: Helpers.precisionFloat(coordinate.lon),
105
+ }))), {
106
+ lat: Helpers.precisionFloat(server.coordinate.lat),
107
+ lon: Helpers.precisionFloat(server.coordinate.lon),
108
+ })),
105
109
  }),
106
110
  // Generate incomplete id because we don't have the body to hash yet. Fill it in in the `fetch()`
107
111
  idempotencyId: args.idempotencyId ?? (await BufferHelpers.generateUuid).utf8.slice(0, 23),
@@ -118,10 +122,10 @@ export class AiRawProviders extends AiBase {
118
122
  metadataHeader.idempotencyId = `${metadataHeader.idempotencyId}-${(await CryptoHelpers.getHash('SHA-256', await new Request(input, rawInit).arrayBuffer())).slice(0, 12)}`;
119
123
  headers.set('cf-aig-metadata', JSON.stringify(metadataHeader));
120
124
  }
121
- if (args.logging ?? this.config.environment !== 'production')
125
+ if (args.logging ?? this.gatewayLog)
122
126
  console.info('ai', 'raw provider', this.chalk.rgb(...Helpers.uniqueIdColor(metadataHeader.idempotencyId))(`[${metadataHeader.idempotencyId}]`), this.chalk.magenta(rawInit?.method), this.chalk.magenta(new URL(new Request(input).url).pathname));
123
127
  return fetch(input, { ...rawInit, headers }).then(async (response) => {
124
- if (args.logging ?? this.config.environment !== 'production')
128
+ if (args.logging ?? this.gatewayLog)
125
129
  console.info('ai', 'raw provider', this.chalk.rgb(...Helpers.uniqueIdColor(metadataHeader.idempotencyId))(`[${metadataHeader.idempotencyId}]`), response.ok ? this.chalk.green(response.status) : this.chalk.red(response.status), response.ok ? this.chalk.green(new URL(response.url).pathname) : this.chalk.red(new URL(response.url).pathname));
126
130
  await this.updateGatewayLog(response, metadataHeader, startRoundTrip, response.headers.has('x-envoy-upstream-service-time') ? parseInt(response.headers.get('x-envoy-upstream-service-time')) : undefined);
127
131
  // Inject it to have it available for retries
@@ -141,7 +145,7 @@ export class AiRawProviders extends AiBase {
141
145
  }
142
146
  anthropic(args) {
143
147
  return import('@ai-sdk/anthropic').then(async ({ createAnthropic }) => createAnthropic({
144
- baseURL: new URL(['v1', this.config.gateway.accountId, this.config.environment, 'anthropic'].join('/'), 'https://gateway.ai.cloudflare.com').toString(),
148
+ baseURL: new URL(['v1', this.config.gateway.accountId, this.gatewayName, 'anthropic'].join('/'), 'https://gateway.ai.cloudflare.com').toString(),
145
149
  apiKey: this.config.providers.anthropic.apiToken,
146
150
  headers: {
147
151
  'cf-aig-authorization': `Bearer ${this.config.gateway.apiToken}`,
@@ -166,10 +170,10 @@ export class AiRawProviders extends AiBase {
166
170
  metadataHeader.idempotencyId = `${metadataHeader.idempotencyId}-${(await CryptoHelpers.getHash('SHA-256', await new Request(input, rawInit).arrayBuffer())).slice(0, 12)}`;
167
171
  headers.set('cf-aig-metadata', JSON.stringify(metadataHeader));
168
172
  }
169
- if (args.logging ?? this.config.environment !== 'production')
173
+ if (args.logging ?? this.gatewayLog)
170
174
  console.info('ai', 'raw provider', this.chalk.rgb(...Helpers.uniqueIdColor(metadataHeader.idempotencyId))(`[${metadataHeader.idempotencyId}]`), this.chalk.magenta(rawInit?.method), this.chalk.magenta(new URL(new Request(input).url).pathname));
171
175
  return fetch(input, { ...rawInit, headers }).then(async (response) => {
172
- if (args.logging ?? this.config.environment !== 'production')
176
+ if (args.logging ?? this.gatewayLog)
173
177
  console.info('ai', 'raw provider', this.chalk.rgb(...Helpers.uniqueIdColor(metadataHeader.idempotencyId))(`[${metadataHeader.idempotencyId}]`), response.ok ? this.chalk.green(response.status) : this.chalk.red(response.status), response.ok ? this.chalk.green(new URL(response.url).pathname) : this.chalk.red(new URL(response.url).pathname));
174
178
  await this.updateGatewayLog(response, metadataHeader, startRoundTrip);
175
179
  // Inject it to have it available for retries
@@ -189,8 +193,10 @@ export class AiRawProviders extends AiBase {
189
193
  }
190
194
  custom(args) {
191
195
  if (this.config.providers.custom?.url) {
196
+ return import('zod')
197
+ .then(({ z }) =>
192
198
  // Verify that the custom provider url is a valid URL
193
- return z
199
+ z
194
200
  .string()
195
201
  .trim()
196
202
  .url()
@@ -203,13 +209,14 @@ export class AiRawProviders extends AiBase {
203
209
  .trim()
204
210
  .ip()
205
211
  .safeParseAsync(customProviderUrl.hostname)
206
- .then(async ({ success }) => {
212
+ .then(({ success }) => ({ customProviderUrl, success }))))
213
+ .then(async ({ customProviderUrl, success }) => {
207
214
  if (success) {
208
215
  throw new Error('IP custom providers not allowed');
209
216
  }
210
217
  else {
211
218
  // Run domain through ZT policies
212
- const doh = new DnsHelpers(new URL('dns-query', `https://${this.config.providers.custom?.dohId}.cloudflare-gateway.com`));
219
+ const doh = await import('@chainfuse/helpers').then(({ DnsHelpers }) => new DnsHelpers(new URL('dns-query', `https://${this.config.providers.custom?.dohId}.cloudflare-gateway.com`)));
213
220
  const aCheck = doh.query(customProviderUrl.hostname, 'A', undefined, undefined, 2 * 1000);
214
221
  const aaaaCheck = doh.query(customProviderUrl.hostname, 'AAAA', undefined, undefined, 2 * 1000);
215
222
  return Promise.allSettled([aCheck, aaaaCheck]).then((checks) => {
@@ -249,10 +256,10 @@ export class AiRawProviders extends AiBase {
249
256
  idempotencyId = `${idempotencyId}-${(await CryptoHelpers.getHash('SHA-256', await new Request(input, rawInit).arrayBuffer())).slice(0, 12)}`;
250
257
  headers.set('X-Idempotency-Id', idempotencyId);
251
258
  }
252
- if (args.logging ?? this.config.environment !== 'production')
259
+ if (args.logging ?? this.gatewayLog)
253
260
  console.info('ai', 'raw provider', this.chalk.rgb(...Helpers.uniqueIdColor(idempotencyId))(`[${idempotencyId}]`), this.chalk.magenta(rawInit?.method), this.chalk.magenta(new URL(new Request(input).url).pathname));
254
261
  return fetch(input, { ...rawInit, headers }).then(async (response) => {
255
- if (args.logging ?? this.config.environment !== 'production')
262
+ if (args.logging ?? this.gatewayLog)
256
263
  console.info('ai', 'raw provider', this.chalk.rgb(...Helpers.uniqueIdColor(idempotencyId))(`[${idempotencyId}]`), response.ok ? this.chalk.green(response.status) : this.chalk.red(response.status), response.ok ? this.chalk.green(new URL(response.url).pathname) : this.chalk.red(new URL(response.url).pathname));
257
264
  // Inject it to have it available for retries
258
265
  const mutableHeaders = new Headers(response.headers);
@@ -274,7 +281,7 @@ export class AiRawProviders extends AiBase {
274
281
  }
275
282
  });
276
283
  }
277
- }))
284
+ })
278
285
  .catch(() => {
279
286
  throw new Error('Invalid custom provider url');
280
287
  });
@@ -298,7 +305,7 @@ export class AiRawProviders extends AiBase {
298
305
  * `v1beta` is the only one that supports function calls as of now
299
306
  * @link https://ai.google.dev/gemini-api/docs/api-versions
300
307
  */
301
- baseURL: new URL(['v1', this.config.gateway.accountId, this.config.environment, 'google-ai-studio', 'v1beta'].join('/'), 'https://gateway.ai.cloudflare.com').toString(),
308
+ baseURL: new URL(['v1', this.config.gateway.accountId, this.gatewayName, 'google-ai-studio', 'v1beta'].join('/'), 'https://gateway.ai.cloudflare.com').toString(),
302
309
  apiKey: this.config.providers.googleAi.apiToken,
303
310
  headers: {
304
311
  'cf-aig-authorization': `Bearer ${this.config.gateway.apiToken}`,
@@ -323,10 +330,10 @@ export class AiRawProviders extends AiBase {
323
330
  metadataHeader.idempotencyId = `${metadataHeader.idempotencyId}-${(await CryptoHelpers.getHash('SHA-256', await new Request(input, rawInit).arrayBuffer())).slice(0, 12)}`;
324
331
  headers.set('cf-aig-metadata', JSON.stringify(metadataHeader));
325
332
  }
326
- if (args.logging ?? this.config.environment !== 'production')
333
+ if (args.logging ?? this.gatewayLog)
327
334
  console.info('ai', 'raw provider', this.chalk.rgb(...Helpers.uniqueIdColor(metadataHeader.idempotencyId))(`[${metadataHeader.idempotencyId}]`), this.chalk.magenta(rawInit?.method), this.chalk.magenta(new URL(new Request(input).url).pathname));
328
335
  return fetch(input, { ...rawInit, headers }).then(async (response) => {
329
- if (args.logging ?? this.config.environment !== 'production')
336
+ if (args.logging ?? this.gatewayLog)
330
337
  console.info('ai', 'raw provider', this.chalk.rgb(...Helpers.uniqueIdColor(metadataHeader.idempotencyId))(`[${metadataHeader.idempotencyId}]`), response.ok ? this.chalk.green(response.status) : this.chalk.red(response.status), response.ok ? this.chalk.green(new URL(response.url).pathname) : this.chalk.red(new URL(response.url).pathname));
331
338
  await this.updateGatewayLog(response, metadataHeader, startRoundTrip);
332
339
  // Inject it to have it available for retries
@@ -346,7 +353,7 @@ export class AiRawProviders extends AiBase {
346
353
  }
347
354
  restWorkersAi(args) {
348
355
  return import('@ai-sdk/openai-compatible').then(async ({ createOpenAICompatible }) => createOpenAICompatible({
349
- baseURL: new URL(['v1', this.config.gateway.accountId, this.config.environment, 'workers-ai', 'v1'].join('/'), 'https://gateway.ai.cloudflare.com').toString(),
356
+ baseURL: new URL(['v1', this.config.gateway.accountId, this.gatewayName, 'workers-ai', 'v1'].join('/'), 'https://gateway.ai.cloudflare.com').toString(),
350
357
  apiKey: this.config.providers.workersAi.apiToken,
351
358
  headers: {
352
359
  'cf-aig-authorization': `Bearer ${this.config.gateway.apiToken}`,
@@ -372,10 +379,10 @@ export class AiRawProviders extends AiBase {
372
379
  metadataHeader.idempotencyId = `${metadataHeader.idempotencyId}-${(await CryptoHelpers.getHash('SHA-256', await new Request(input, rawInit).arrayBuffer())).slice(0, 12)}`;
373
380
  headers.set('cf-aig-metadata', JSON.stringify(metadataHeader));
374
381
  }
375
- if (args.logging ?? this.config.environment !== 'production')
382
+ if (args.logging ?? this.gatewayLog)
376
383
  console.info('ai', 'raw provider', this.chalk.rgb(...Helpers.uniqueIdColor(metadataHeader.idempotencyId))(`[${metadataHeader.idempotencyId}]`), this.chalk.magenta(rawInit?.method), this.chalk.magenta(new URL(new Request(input).url).pathname));
377
384
  return fetch(input, { ...rawInit, headers }).then(async (response) => {
378
- if (args.logging ?? this.config.environment !== 'production')
385
+ if (args.logging ?? this.gatewayLog)
379
386
  console.info('ai', 'raw provider', this.chalk.rgb(...Helpers.uniqueIdColor(metadataHeader.idempotencyId))(`[${metadataHeader.idempotencyId}]`), response.ok ? this.chalk.green(response.status) : this.chalk.red(response.status), response.ok ? this.chalk.green(new URL(response.url).pathname) : this.chalk.red(new URL(response.url).pathname));
380
387
  await this.updateGatewayLog(response, metadataHeader, startRoundTrip);
381
388
  // Inject it to have it available for retries
@@ -397,7 +404,7 @@ export class AiRawProviders extends AiBase {
397
404
  return import('workers-ai-provider').then(async ({ createWorkersAI }) => createWorkersAI({
398
405
  binding: this.config.providers.workersAi,
399
406
  gateway: {
400
- id: this.config.environment,
407
+ id: this.gatewayName,
401
408
  ...(args.cache && { cacheTtl: typeof args.cache === 'boolean' ? (args.cache ? this.cacheTtl : 0) : args.cache }),
402
409
  ...(args.skipCache && { skipCache: true }),
403
410
  metadata: {
package/dist/registry.mjs CHANGED
@@ -1,18 +1,16 @@
1
- import { experimental_createProviderRegistry as createProviderRegistry } from 'ai';
2
1
  import { AiBase } from './base.mjs';
3
- import { AiCustomProviders } from './providers/customProviders.mjs';
4
2
  export class AiRegistry extends AiBase {
5
- async providers(args) {
6
- return Object.freeze({
3
+ providers(args) {
4
+ return import('./providers/customProviders.mjs').then(async ({ AiCustomProviders }) => Object.freeze({
7
5
  openai: await new AiCustomProviders(this.config).oaiOpenai(args),
8
6
  azure: await new AiCustomProviders(this.config).azOpenai(args),
9
7
  anthropic: await new AiCustomProviders(this.config).anthropic(args),
10
8
  custom: await new AiCustomProviders(this.config).custom(args),
11
9
  'google.generative-ai': await new AiCustomProviders(this.config).googleAi(args),
12
10
  workersai: await new AiCustomProviders(this.config).cfWorkersAi(args),
13
- });
11
+ }));
14
12
  }
15
- async registry(args) {
16
- return createProviderRegistry(await this.providers(args));
13
+ registry(args) {
14
+ return import('ai').then(async ({ experimental_createProviderRegistry: createProviderRegistry }) => createProviderRegistry(await this.providers(args)));
17
15
  }
18
16
  }
@@ -0,0 +1,17 @@
1
+ import type { Coordinate } from '@chainfuse/types';
2
+ import type { IncomingRequestCfProperties } from '@cloudflare/workers-types/experimental';
3
+ import { AiBase } from './base.mts';
4
+ import type { PrivacyRegion, Servers } from './types.mjs';
5
+ export declare class ServerSelector extends AiBase {
6
+ static determinePrivacyRegion(country?: IncomingRequestCfProperties['country'], continent?: IncomingRequestCfProperties['continent']): ("APPs" | "LGPD" | "GDPR" | "PIPEDA" | "APPI" | "UK-GDPR" | "PIPA" | "PoPIA" | "revFADP" | "NPDA" | "PDP")[];
7
+ determineLocation(geoRouting?: {
8
+ userCoordinate?: Coordinate;
9
+ country?: IncomingRequestCfProperties["country"];
10
+ continent?: IncomingRequestCfProperties["continent"];
11
+ } | undefined): Promise<{
12
+ coordinate: Coordinate;
13
+ country: IncomingRequestCfProperties['country'];
14
+ continent: IncomingRequestCfProperties['continent'];
15
+ }>;
16
+ closestServers(servers: Servers, requiredCapability?: string, userCoordinate?: Coordinate, privacyRegion?: PrivacyRegion[]): Promise<Servers>;
17
+ }
@@ -0,0 +1,151 @@
1
+ import { AiBase } from "./base.mjs";
2
+ export class ServerSelector extends AiBase {
3
+ static determinePrivacyRegion(country, continent) {
4
+ const regions = new Set();
5
+ if (country) {
6
+ switch (country.toUpperCase()) {
7
+ case 'AU':
8
+ regions.add('APPs');
9
+ break;
10
+ case 'BR':
11
+ regions.add('LGPD');
12
+ break;
13
+ case 'CA':
14
+ regions.add('PIPEDA');
15
+ regions.add('GDPR');
16
+ regions.add('revFADP');
17
+ regions.add('UK-GDPR');
18
+ break;
19
+ case 'IN':
20
+ regions.add('PDP');
21
+ break;
22
+ case 'JP':
23
+ regions.add('APPI');
24
+ regions.add('GDPR');
25
+ regions.add('UK-GDPR');
26
+ break;
27
+ case 'KR':
28
+ regions.add('PIPA');
29
+ regions.add('GDPR');
30
+ regions.add('UK-GDPR');
31
+ break;
32
+ case 'NO':
33
+ regions.add('NPDA');
34
+ regions.add('PIPEDA');
35
+ regions.add('GDPR');
36
+ regions.add('APPI');
37
+ regions.add('PIPA');
38
+ regions.add('revFADP');
39
+ regions.add('UK-GDPR');
40
+ break;
41
+ case 'ZA':
42
+ regions.add('PoPIA');
43
+ break;
44
+ case 'CH':
45
+ regions.add('revFADP');
46
+ regions.add('PIPEDA');
47
+ regions.add('GDPR');
48
+ regions.add('APPI');
49
+ regions.add('PIPA');
50
+ regions.add('NPDA');
51
+ regions.add('revFADP');
52
+ regions.add('UK-GDPR');
53
+ break;
54
+ case 'GB':
55
+ regions.add('UK-GDPR');
56
+ regions.add('PIPEDA');
57
+ regions.add('GDPR');
58
+ regions.add('APPI');
59
+ regions.add('PIPA');
60
+ regions.add('NPDA');
61
+ regions.add('revFADP');
62
+ regions.add('UK-GDPR');
63
+ break;
64
+ }
65
+ }
66
+ if (continent) {
67
+ switch (continent.toUpperCase()) {
68
+ case 'EU':
69
+ regions.add('GDPR');
70
+ regions.add('PIPEDA');
71
+ regions.add('APPI');
72
+ regions.add('PIPA');
73
+ regions.add('NPDA');
74
+ regions.add('revFADP');
75
+ regions.add('UK-GDPR');
76
+ break;
77
+ }
78
+ }
79
+ return Array.from(regions);
80
+ }
81
+ async determineLocation(geoRouting = this.config.geoRouting) {
82
+ if (!geoRouting?.userCoordinate?.lat || !geoRouting?.userCoordinate?.lon || !geoRouting?.country || !geoRouting?.continent) {
83
+ console.warn('Location not provided, falling back to nearest Cloudflare POP', 'WARNING: This is slow');
84
+ try {
85
+ const geoJson = await fetch(new URL('https://workers.cloudflare.com/cf.json')).then((geoResponse) => geoResponse.json().then((json) => json));
86
+ return {
87
+ coordinate: {
88
+ lat: geoRouting?.userCoordinate?.lat ?? geoJson.latitude ?? '0',
89
+ lon: geoRouting?.userCoordinate?.lon ?? geoJson.longitude ?? '0',
90
+ },
91
+ country: geoRouting?.country ?? geoJson.country,
92
+ continent: geoRouting?.continent ?? geoJson.continent,
93
+ };
94
+ }
95
+ catch (error) {
96
+ console.error('Failed to use nearest Cloudflare POP, service distance and privacy regions will be wrong', error);
97
+ }
98
+ }
99
+ return {
100
+ coordinate: {
101
+ lat: geoRouting?.userCoordinate?.lat ?? '0',
102
+ lon: geoRouting?.userCoordinate?.lon ?? '0',
103
+ },
104
+ country: geoRouting?.country,
105
+ continent: geoRouting?.continent,
106
+ };
107
+ }
108
+ async closestServers(servers, requiredCapability, userCoordinate, privacyRegion) {
109
+ if (!userCoordinate || !privacyRegion) {
110
+ const { coordinate, country, continent } = await this.determineLocation();
111
+ if (!userCoordinate)
112
+ userCoordinate = coordinate;
113
+ if (!privacyRegion)
114
+ privacyRegion = ServerSelector.determinePrivacyRegion(country, continent);
115
+ }
116
+ // Skip over the rest of logic if the server can't handle the incoming request
117
+ // @ts-expect-error it's always strings, just sometimes string literals
118
+ const featureFilteredServers = requiredCapability ? servers.filter((server) => server.languageModelAvailability.includes(requiredCapability) || server.textEmbeddingModelAvailability.includes(requiredCapability)) : servers;
119
+ if (featureFilteredServers.length > 0) {
120
+ // Skip over servers not in the save privacy region except if undefined, then you can use any
121
+ const privacyRegionFilteredServers = featureFilteredServers.filter((server) => privacyRegion.length === 0 || ('privacyRegion' in server && privacyRegion.includes(server.privacyRegion)));
122
+ if (privacyRegionFilteredServers.length > 0) {
123
+ // Calculate distance for each server and store it as a tuple [Server, distance]
124
+ const serversWithDistances = await Promise.all([import('haversine-distance'), import('@chainfuse/helpers')]).then(([{ default: haversine }, { Helpers }]) => privacyRegionFilteredServers.map((server) => {
125
+ // Match decimal point length
126
+ return [
127
+ server,
128
+ haversine({
129
+ lat: Helpers.precisionFloat(userCoordinate.lat),
130
+ lon: Helpers.precisionFloat(userCoordinate.lon),
131
+ }, {
132
+ lat: Helpers.precisionFloat(server.coordinate.lat),
133
+ lon: Helpers.precisionFloat(server.coordinate.lon),
134
+ }),
135
+ ];
136
+ }));
137
+ // Sort the servers by distance
138
+ serversWithDistances.sort((a, b) => a[1] - b[1]);
139
+ // Extract the ids of the sorted servers
140
+ const sortedServers = serversWithDistances.map(([server]) => server);
141
+ return sortedServers;
142
+ }
143
+ else {
144
+ throw new Error(`No server with the capability ${requiredCapability} available in a region covered under ${JSON.stringify(privacyRegion)}`);
145
+ }
146
+ }
147
+ else {
148
+ throw new Error(`No server with the capability ${requiredCapability} available`);
149
+ }
150
+ }
151
+ }
package/dist/types.d.mts CHANGED
@@ -1,4 +1,5 @@
1
- import type { PrefixedUuid, RawCoordinate, UuidExport } from '@chainfuse/types';
1
+ import type { Coordinate, PrefixedUuid, UuidExport } from '@chainfuse/types';
2
+ import type { azureCatalog } from '@chainfuse/types/ai-tools/catalog/azure';
2
3
  import type { Ai, ExecutionContext, IncomingRequestCfProperties } from '@cloudflare/workers-types/experimental';
3
4
  import type haversine from 'haversine-distance';
4
5
  export interface AiConfig {
@@ -7,11 +8,22 @@ export interface AiConfig {
7
8
  apiToken: string;
8
9
  };
9
10
  geoRouting?: {
10
- userCoordinate?: RawCoordinate;
11
+ userCoordinate?: Coordinate;
11
12
  country?: IncomingRequestCfProperties['country'];
12
13
  continent?: IncomingRequestCfProperties['continent'];
13
14
  };
14
- environment: 'production' | 'preview';
15
+ billing: {
16
+ noCost: true;
17
+ } | ({
18
+ noCost: false;
19
+ environment: 'production' | 'preview';
20
+ } & ({
21
+ user: 'bot';
22
+ action: 'passive';
23
+ } | {
24
+ user: 'human' | 'bot';
25
+ action: 'active';
26
+ }));
15
27
  providers: AiConfigProviders;
16
28
  backgroundContext?: ExecutionContext;
17
29
  }
@@ -54,6 +66,11 @@ export interface AiConfigWorkersaiRest {
54
66
  apiToken: string;
55
67
  }
56
68
  export type AiConfigWorkersaiBinding<T extends Ai = Ai> = T;
69
+ export type AzureServers = typeof azureCatalog;
70
+ export type Servers = AzureServers;
71
+ export type PrivacyRegion = Extract<Servers[number], {
72
+ privacyRegion: string;
73
+ }>['privacyRegion'];
57
74
  /**
58
75
  * It's a UUID, but the last block is SHA256 of the request body
59
76
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chainfuse/ai-tools",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "",
5
5
  "author": "ChainFuse",
6
6
  "homepage": "https://github.com/ChainFuse/packages/tree/main/packages/ai-tools#readme",
@@ -24,7 +24,7 @@
24
24
  "scripts": {
25
25
  "fmt": "prettier --check .",
26
26
  "fmt:fix": "prettier --write .",
27
- "lint": "eslint .",
27
+ "lint": "eslint src",
28
28
  "lint:fix": "npm run lint -- --fix",
29
29
  "clean": "npx -y rimraf@latest ./dist ./.tsbuildinfo",
30
30
  "build": "tsc",
@@ -48,21 +48,21 @@
48
48
  },
49
49
  "prettier": "@demosjarco/prettier-config",
50
50
  "dependencies": {
51
- "@ai-sdk/anthropic": "^1.1.19",
52
- "@ai-sdk/azure": "^1.2.7",
53
- "@ai-sdk/google": "^1.1.27",
51
+ "@ai-sdk/anthropic": "^1.2.1",
52
+ "@ai-sdk/azure": "^1.3.2",
53
+ "@ai-sdk/google": "^1.2.3",
54
54
  "@ai-sdk/openai": "^1.0.5",
55
- "@ai-sdk/openai-compatible": "^0.1.17",
56
- "@chainfuse/helpers": "^2.0.3",
57
- "@chainfuse/types": "^1.7.3",
58
- "ai": "^4.1.66",
55
+ "@ai-sdk/openai-compatible": "^0.2.1",
56
+ "@chainfuse/helpers": "^2.2.1",
57
+ "@chainfuse/types": "^2.1.1",
58
+ "ai": "^4.2.5",
59
59
  "chalk": "^5.4.1",
60
60
  "haversine-distance": "^1.2.3",
61
61
  "workers-ai-provider": "^0.2.0"
62
62
  },
63
63
  "devDependencies": {
64
- "@cloudflare/workers-types": "^4.20250319.0",
64
+ "@cloudflare/workers-types": "^4.20250321.0",
65
65
  "openai": "^4.89.0"
66
66
  },
67
- "gitHead": "a48db66b6313a868ab0affa50a934ad13f6bf391"
67
+ "gitHead": "f0eaab21228260be471a6fac0874068139677035"
68
68
  }
@@ -1,5 +0,0 @@
1
- import { ServerSelector } from './base.mjs';
2
- import type { Server } from './types.mjs';
3
- export declare class AzureServerSelector extends ServerSelector {
4
- readonly servers: Set<Server>;
5
- }
@@ -1,284 +0,0 @@
1
- import { PrivacyRegion, ServerSelector } from './base.mjs';
2
- export class AzureServerSelector extends ServerSelector {
3
- // From: https://gist.github.com/demosjarco/2091b3a197e530f1402e9dfec6666cd8
4
- servers = new Set([
5
- {
6
- id: 'OpenAi-AU-NewSouthWales',
7
- coordinate: {
8
- lat: -33.86,
9
- lon: 151.2094,
10
- },
11
- region: PrivacyRegion.Australian_Privacy_Principles,
12
- languageModelAvailability: ['gpt-35-turbo', 'gpt-4-turbo', 'gpt-4o', 'gpt-4o-mini'],
13
- imageModelAvailability: ['dall-e-3'],
14
- textEmbeddingModelAvailability: ['text-embedding-3-large', 'text-embedding-3-small'],
15
- },
16
- {
17
- id: 'OpenAi-BR-SaoPauloState',
18
- coordinate: {
19
- lat: -23.55,
20
- lon: -46.633,
21
- },
22
- region: PrivacyRegion.Brazil_General_Data_protection_Law,
23
- languageModelAvailability: ['gpt-4-turbo', 'gpt-4o', 'gpt-4o-mini'],
24
- imageModelAvailability: [],
25
- textEmbeddingModelAvailability: [],
26
- },
27
- {
28
- id: 'OpenAI-CA-Toronto',
29
- coordinate: {
30
- lat: 43.653,
31
- lon: -79.383,
32
- },
33
- region: PrivacyRegion.Canada_Personal_Information_Protection_and_Electronic_Documents_Act,
34
- languageModelAvailability: [],
35
- imageModelAvailability: [],
36
- textEmbeddingModelAvailability: [],
37
- },
38
- {
39
- id: 'OpenAI-CA-Quebec',
40
- coordinate: {
41
- lat: 46.817,
42
- lon: -71.217,
43
- },
44
- region: PrivacyRegion.Canada_Personal_Information_Protection_and_Electronic_Documents_Act,
45
- languageModelAvailability: ['gpt-35-turbo', 'gpt-4-turbo', 'gpt-4o', 'gpt-4o-mini'],
46
- imageModelAvailability: [],
47
- textEmbeddingModelAvailability: ['text-embedding-3-small', 'text-embedding-3-large'],
48
- },
49
- {
50
- id: 'OpenAi-US-Virginia',
51
- coordinate: {
52
- lat: 37.3719,
53
- lon: -79.8164,
54
- },
55
- languageModelAvailability: ['gpt-35-turbo', 'gpt-4-turbo', 'gpt-4o', 'gpt-4o-mini'],
56
- imageModelAvailability: ['dall-e-3'],
57
- textEmbeddingModelAvailability: ['text-embedding-3-small', 'text-embedding-3-large'],
58
- },
59
- {
60
- id: 'OpenAi-US-Virginia2',
61
- coordinate: {
62
- lat: 36.6681,
63
- lon: -78.3889,
64
- },
65
- languageModelAvailability: ['gpt-35-turbo', 'gpt-4-turbo', 'gpt-4o', 'gpt-4o-mini', 'o1', 'o3-mini'],
66
- imageModelAvailability: [],
67
- textEmbeddingModelAvailability: ['text-embedding-3-small', 'text-embedding-3-large'],
68
- },
69
- {
70
- id: 'OpenAi-EU-Paris',
71
- coordinate: {
72
- lat: 46.3772,
73
- lon: 2.373,
74
- },
75
- region: PrivacyRegion.General_Data_Protection_Regulation,
76
- languageModelAvailability: ['gpt-35-turbo', 'gpt-4-turbo', 'gpt-4o', 'gpt-4o-mini'],
77
- imageModelAvailability: [],
78
- textEmbeddingModelAvailability: ['text-embedding-3-large'],
79
- },
80
- {
81
- id: 'OpenAi-EU-Frankfurt',
82
- coordinate: {
83
- lat: 50.110924,
84
- lon: 8.682127,
85
- },
86
- region: PrivacyRegion.General_Data_Protection_Regulation,
87
- languageModelAvailability: ['gpt-4-turbo', 'gpt-4o', 'gpt-4o-mini'],
88
- imageModelAvailability: [],
89
- textEmbeddingModelAvailability: [],
90
- },
91
- {
92
- id: 'OpenAi-JP-Tokyo',
93
- coordinate: {
94
- lat: 35.68,
95
- lon: 139.77,
96
- },
97
- region: PrivacyRegion.Japan_Act_on_the_Protection_of_Personal_Information,
98
- languageModelAvailability: ['gpt-35-turbo', 'gpt-4-turbo', 'gpt-4o', 'gpt-4o-mini'],
99
- imageModelAvailability: [],
100
- textEmbeddingModelAvailability: ['text-embedding-3-large', 'text-embedding-3-small'],
101
- },
102
- {
103
- id: 'OpenAi-KR-Seoul',
104
- coordinate: {
105
- lat: 37.5665,
106
- lon: 126.978,
107
- },
108
- region: PrivacyRegion.Korean_Personal_Information_Protection_Act,
109
- languageModelAvailability: ['gpt-4-turbo', 'gpt-4o', 'gpt-4o-mini'],
110
- imageModelAvailability: [],
111
- textEmbeddingModelAvailability: [],
112
- },
113
- {
114
- id: 'OpenAi-US-Illinois',
115
- coordinate: {
116
- lat: 41.8819,
117
- lon: -87.6278,
118
- },
119
- languageModelAvailability: ['gpt-35-turbo', 'gpt-4-turbo', 'gpt-4o', 'gpt-4o-mini'],
120
- imageModelAvailability: [],
121
- textEmbeddingModelAvailability: [],
122
- },
123
- {
124
- id: 'OpenAi-NO-Oslo',
125
- coordinate: {
126
- lat: 59.913868,
127
- lon: 10.752245,
128
- },
129
- region: PrivacyRegion.Norwegian_Personal_Data_Act,
130
- languageModelAvailability: ['gpt-4-turbo', 'gpt-4o', 'gpt-4o-mini'],
131
- imageModelAvailability: [],
132
- textEmbeddingModelAvailability: ['text-embedding-3-large'],
133
- },
134
- {
135
- id: 'OpenAi-EU-Warsaw',
136
- coordinate: {
137
- lat: 52.23334,
138
- lon: 21.01666,
139
- },
140
- region: PrivacyRegion.General_Data_Protection_Regulation,
141
- languageModelAvailability: ['gpt-4-turbo', 'gpt-4o', 'gpt-4o-mini'],
142
- imageModelAvailability: [],
143
- textEmbeddingModelAvailability: ['text-embedding-3-large'],
144
- },
145
- {
146
- id: 'OpenAi-ZA-Johannesburg',
147
- coordinate: {
148
- lat: 28.21837,
149
- lon: -25.73134,
150
- },
151
- region: PrivacyRegion.SouthAfrica_Protection_Personal_Information_Act,
152
- languageModelAvailability: ['gpt-4-turbo', 'gpt-4o', 'gpt-4o-mini'],
153
- imageModelAvailability: [],
154
- textEmbeddingModelAvailability: [],
155
- },
156
- {
157
- id: 'OpenAi-US-Texas',
158
- coordinate: {
159
- lat: 29.4167,
160
- lon: -98.5,
161
- },
162
- languageModelAvailability: ['gpt-35-turbo', 'gpt-4-turbo', 'gpt-4o', 'gpt-4o-mini'],
163
- imageModelAvailability: [],
164
- textEmbeddingModelAvailability: [],
165
- },
166
- {
167
- id: 'OpenAi-IN-Chennai',
168
- coordinate: {
169
- lat: 12.9822,
170
- lon: 80.1636,
171
- },
172
- region: PrivacyRegion.Indian_Personal_Protection,
173
- languageModelAvailability: ['gpt-35-turbo', 'gpt-4-turbo', 'gpt-4o', 'gpt-4o-mini'],
174
- imageModelAvailability: [],
175
- textEmbeddingModelAvailability: ['text-embedding-3-large'],
176
- },
177
- {
178
- id: 'OpenAi-SG-Singapore',
179
- coordinate: {
180
- lat: 1.283,
181
- lon: 103.833,
182
- },
183
- languageModelAvailability: [],
184
- imageModelAvailability: [],
185
- textEmbeddingModelAvailability: [],
186
- },
187
- {
188
- id: 'OpenAi-EU-Gavle',
189
- coordinate: {
190
- lat: 60.67488,
191
- lon: 17.14127,
192
- },
193
- region: PrivacyRegion.General_Data_Protection_Regulation,
194
- languageModelAvailability: ['gpt-35-turbo', 'gpt-4-turbo', 'gpt-4o-mini', 'gpt-4o', 'o1', 'o3-mini'],
195
- imageModelAvailability: ['dall-e-3'],
196
- textEmbeddingModelAvailability: ['text-embedding-3-large'],
197
- },
198
- {
199
- id: 'OpenAi-EU-Madrid',
200
- coordinate: {
201
- lat: 3.4209,
202
- lon: 40.4259,
203
- },
204
- region: PrivacyRegion.General_Data_Protection_Regulation,
205
- languageModelAvailability: ['gpt-4-turbo', 'gpt-4o'],
206
- imageModelAvailability: [],
207
- textEmbeddingModelAvailability: [],
208
- },
209
- {
210
- id: 'OpenAi-CH-Geneva',
211
- coordinate: {
212
- lat: 46.204391,
213
- lon: 6.143158,
214
- },
215
- region: PrivacyRegion.Swiss_Federal_Act_on_Data_Protection,
216
- languageModelAvailability: [],
217
- imageModelAvailability: [],
218
- textEmbeddingModelAvailability: [],
219
- },
220
- {
221
- id: 'OpenAi-AE-Dubai',
222
- coordinate: {
223
- lat: 25.266666,
224
- lon: 55.316666,
225
- },
226
- languageModelAvailability: ['gpt-4-turbo', 'gpt-4o-mini'],
227
- imageModelAvailability: [],
228
- textEmbeddingModelAvailability: [],
229
- },
230
- {
231
- id: 'OpenAi-CH-Zurich',
232
- coordinate: {
233
- lat: 47.451542,
234
- lon: 8.564572,
235
- },
236
- region: PrivacyRegion.Swiss_Federal_Act_on_Data_Protection,
237
- languageModelAvailability: ['gpt-35-turbo', 'gpt-4-turbo', 'gpt-4o', 'gpt-4o-mini'],
238
- imageModelAvailability: [],
239
- textEmbeddingModelAvailability: ['text-embedding-3-large', 'text-embedding-3-small'],
240
- },
241
- {
242
- id: 'OpenAi-UK-London',
243
- coordinate: {
244
- lat: 50.941,
245
- lon: -0.799,
246
- },
247
- region: PrivacyRegion.UK_General_Data_Protection_Regulation,
248
- languageModelAvailability: ['gpt-35-turbo', 'gpt-4-turbo', 'gpt-4o', 'gpt-4o-mini'],
249
- imageModelAvailability: [],
250
- textEmbeddingModelAvailability: ['text-embedding-3-large'],
251
- },
252
- {
253
- id: 'OpenAi-EU-Netherlands',
254
- coordinate: {
255
- lat: 52.3667,
256
- lon: 4.9,
257
- },
258
- region: PrivacyRegion.General_Data_Protection_Regulation,
259
- languageModelAvailability: ['gpt-4-turbo', 'gpt-4o', 'gpt-4o-mini'],
260
- imageModelAvailability: [],
261
- textEmbeddingModelAvailability: [],
262
- },
263
- {
264
- id: 'OpenAi-US-California',
265
- coordinate: {
266
- lat: 37.783,
267
- lon: -122.417,
268
- },
269
- languageModelAvailability: ['gpt-35-turbo', 'gpt-4-turbo', 'gpt-4o', 'gpt-4o-mini'],
270
- imageModelAvailability: [],
271
- textEmbeddingModelAvailability: [],
272
- },
273
- {
274
- id: 'OpenAi-US-Phoenix',
275
- coordinate: {
276
- lat: 33.448376,
277
- lon: -112.074036,
278
- },
279
- languageModelAvailability: ['gpt-35-turbo', 'gpt-4-turbo', 'gpt-4o', 'gpt-4o-mini'],
280
- imageModelAvailability: [],
281
- textEmbeddingModelAvailability: ['text-embedding-3-large'],
282
- },
283
- ]);
284
- }
@@ -1,22 +0,0 @@
1
- import type { RawCoordinate } from '@chainfuse/types';
2
- import type { IncomingRequestCfProperties } from '@cloudflare/workers-types/experimental';
3
- import { AiBase } from '../base.mjs';
4
- import type { Server } from './types.mjs';
5
- export declare enum PrivacyRegion {
6
- Australian_Privacy_Principles = "APPs",
7
- Brazil_General_Data_protection_Law = "LGPD",
8
- Canada_Personal_Information_Protection_and_Electronic_Documents_Act = "PIPEDA",
9
- General_Data_Protection_Regulation = "GDPR",
10
- Indian_Personal_Protection = "PDP",
11
- Japan_Act_on_the_Protection_of_Personal_Information = "APPI",
12
- Korean_Personal_Information_Protection_Act = "PIPA",
13
- Norwegian_Personal_Data_Act = "NPDA",
14
- SouthAfrica_Protection_Personal_Information_Act = "PoPIA",
15
- Swiss_Federal_Act_on_Data_Protection = "revFADP",
16
- UK_General_Data_Protection_Regulation = "UK-GDPR"
17
- }
18
- export declare abstract class ServerSelector extends AiBase {
19
- readonly servers: Set<Server>;
20
- static determinePrivacyRegion(country?: IncomingRequestCfProperties['country'], continent?: IncomingRequestCfProperties['continent']): PrivacyRegion[];
21
- closestServers(requiredCapability?: (Server['languageModelAvailability'] | Server['textEmbeddingModelAvailability'])[number], userCoordinate?: RawCoordinate, privacyRegion?: PrivacyRegion[]): Server[];
22
- }
@@ -1,137 +0,0 @@
1
- import { Helpers } from '@chainfuse/helpers';
2
- import haversine from 'haversine-distance';
3
- import { AiBase } from '../base.mjs';
4
- export var PrivacyRegion;
5
- (function (PrivacyRegion) {
6
- PrivacyRegion["Australian_Privacy_Principles"] = "APPs";
7
- PrivacyRegion["Brazil_General_Data_protection_Law"] = "LGPD";
8
- PrivacyRegion["Canada_Personal_Information_Protection_and_Electronic_Documents_Act"] = "PIPEDA";
9
- PrivacyRegion["General_Data_Protection_Regulation"] = "GDPR";
10
- PrivacyRegion["Indian_Personal_Protection"] = "PDP";
11
- PrivacyRegion["Japan_Act_on_the_Protection_of_Personal_Information"] = "APPI";
12
- PrivacyRegion["Korean_Personal_Information_Protection_Act"] = "PIPA";
13
- PrivacyRegion["Norwegian_Personal_Data_Act"] = "NPDA";
14
- PrivacyRegion["SouthAfrica_Protection_Personal_Information_Act"] = "PoPIA";
15
- PrivacyRegion["Swiss_Federal_Act_on_Data_Protection"] = "revFADP";
16
- PrivacyRegion["UK_General_Data_Protection_Regulation"] = "UK-GDPR";
17
- })(PrivacyRegion || (PrivacyRegion = {}));
18
- export class ServerSelector extends AiBase {
19
- servers = new Set();
20
- static determinePrivacyRegion(country, continent) {
21
- const regions = new Set();
22
- if (country) {
23
- switch (country.toUpperCase()) {
24
- case 'AU':
25
- regions.add(PrivacyRegion.Australian_Privacy_Principles);
26
- break;
27
- case 'BR':
28
- regions.add(PrivacyRegion.Brazil_General_Data_protection_Law);
29
- break;
30
- case 'CA':
31
- regions.add(PrivacyRegion.Canada_Personal_Information_Protection_and_Electronic_Documents_Act);
32
- regions.add(PrivacyRegion.General_Data_Protection_Regulation);
33
- regions.add(PrivacyRegion.Swiss_Federal_Act_on_Data_Protection);
34
- regions.add(PrivacyRegion.UK_General_Data_Protection_Regulation);
35
- break;
36
- case 'IN':
37
- regions.add(PrivacyRegion.Indian_Personal_Protection);
38
- break;
39
- case 'JP':
40
- regions.add(PrivacyRegion.Japan_Act_on_the_Protection_of_Personal_Information);
41
- regions.add(PrivacyRegion.General_Data_Protection_Regulation);
42
- regions.add(PrivacyRegion.UK_General_Data_Protection_Regulation);
43
- break;
44
- case 'KR':
45
- regions.add(PrivacyRegion.Korean_Personal_Information_Protection_Act);
46
- regions.add(PrivacyRegion.General_Data_Protection_Regulation);
47
- regions.add(PrivacyRegion.UK_General_Data_Protection_Regulation);
48
- break;
49
- case 'NO':
50
- regions.add(PrivacyRegion.Norwegian_Personal_Data_Act);
51
- regions.add(PrivacyRegion.Canada_Personal_Information_Protection_and_Electronic_Documents_Act);
52
- regions.add(PrivacyRegion.General_Data_Protection_Regulation);
53
- regions.add(PrivacyRegion.Japan_Act_on_the_Protection_of_Personal_Information);
54
- regions.add(PrivacyRegion.Korean_Personal_Information_Protection_Act);
55
- regions.add(PrivacyRegion.Swiss_Federal_Act_on_Data_Protection);
56
- regions.add(PrivacyRegion.UK_General_Data_Protection_Regulation);
57
- break;
58
- case 'ZA':
59
- regions.add(PrivacyRegion.SouthAfrica_Protection_Personal_Information_Act);
60
- break;
61
- case 'CH':
62
- regions.add(PrivacyRegion.Swiss_Federal_Act_on_Data_Protection);
63
- regions.add(PrivacyRegion.Canada_Personal_Information_Protection_and_Electronic_Documents_Act);
64
- regions.add(PrivacyRegion.General_Data_Protection_Regulation);
65
- regions.add(PrivacyRegion.Japan_Act_on_the_Protection_of_Personal_Information);
66
- regions.add(PrivacyRegion.Korean_Personal_Information_Protection_Act);
67
- regions.add(PrivacyRegion.Norwegian_Personal_Data_Act);
68
- regions.add(PrivacyRegion.Swiss_Federal_Act_on_Data_Protection);
69
- regions.add(PrivacyRegion.UK_General_Data_Protection_Regulation);
70
- break;
71
- case 'GB':
72
- regions.add(PrivacyRegion.UK_General_Data_Protection_Regulation);
73
- regions.add(PrivacyRegion.Canada_Personal_Information_Protection_and_Electronic_Documents_Act);
74
- regions.add(PrivacyRegion.General_Data_Protection_Regulation);
75
- regions.add(PrivacyRegion.Japan_Act_on_the_Protection_of_Personal_Information);
76
- regions.add(PrivacyRegion.Korean_Personal_Information_Protection_Act);
77
- regions.add(PrivacyRegion.Norwegian_Personal_Data_Act);
78
- regions.add(PrivacyRegion.Swiss_Federal_Act_on_Data_Protection);
79
- regions.add(PrivacyRegion.UK_General_Data_Protection_Regulation);
80
- break;
81
- }
82
- }
83
- if (continent) {
84
- switch (continent.toUpperCase()) {
85
- case 'EU':
86
- regions.add(PrivacyRegion.General_Data_Protection_Regulation);
87
- regions.add(PrivacyRegion.Canada_Personal_Information_Protection_and_Electronic_Documents_Act);
88
- regions.add(PrivacyRegion.Japan_Act_on_the_Protection_of_Personal_Information);
89
- regions.add(PrivacyRegion.Korean_Personal_Information_Protection_Act);
90
- regions.add(PrivacyRegion.Norwegian_Personal_Data_Act);
91
- regions.add(PrivacyRegion.Swiss_Federal_Act_on_Data_Protection);
92
- regions.add(PrivacyRegion.UK_General_Data_Protection_Regulation);
93
- break;
94
- }
95
- }
96
- return Array.from(regions);
97
- }
98
- closestServers(requiredCapability, userCoordinate = {
99
- lat: this.config.geoRouting?.userCoordinate?.lat ?? '0',
100
- lon: this.config.geoRouting?.userCoordinate?.lon ?? '0',
101
- }, privacyRegion = ServerSelector.determinePrivacyRegion(this.config.geoRouting?.country, this.config.geoRouting?.continent)) {
102
- // Skip over the rest of logic if the server can't handle the incoming request
103
- // @ts-expect-error it's always strings, just sometimes string literals
104
- const featureFilteredServers = requiredCapability ? Array.from(this.servers).filter((server) => server.languageModelAvailability.includes(requiredCapability) || server.textEmbeddingModelAvailability.includes(requiredCapability)) : Array.from(this.servers);
105
- if (featureFilteredServers.length > 0) {
106
- // Skip over servers not in the save privacy region except if undefined, then you can use any
107
- const privacyRegionFilteredServers = featureFilteredServers.filter((server) => privacyRegion.length === 0 || (server.region && privacyRegion.includes(server.region)));
108
- if (privacyRegionFilteredServers.length > 0) {
109
- // Calculate distance for each server and store it as a tuple [Server, distance]
110
- const serversWithDistances = privacyRegionFilteredServers.map((server) => {
111
- // Match decimal point length
112
- return [
113
- server,
114
- haversine({
115
- lat: Helpers.precisionFloat(userCoordinate.lat),
116
- lon: Helpers.precisionFloat(userCoordinate.lon),
117
- }, {
118
- lat: server.coordinate.lat,
119
- lon: server.coordinate.lon,
120
- }),
121
- ];
122
- });
123
- // Sort the servers by distance
124
- serversWithDistances.sort((a, b) => a[1] - b[1]);
125
- // Extract the ids of the sorted servers
126
- const sortedServers = serversWithDistances.map(([server]) => server);
127
- return sortedServers;
128
- }
129
- else {
130
- throw new Error(`No server with the capability ${requiredCapability} available in a region covered under ${JSON.stringify(privacyRegion)}`);
131
- }
132
- }
133
- else {
134
- throw new Error(`No server with the capability ${requiredCapability} available`);
135
- }
136
- }
137
- }
@@ -1,10 +0,0 @@
1
- import type { AzureChatModels, AzureEmbeddingModels, AzureImageModels, Coordinate } from '@chainfuse/types';
2
- import type { PrivacyRegion } from './base.mjs';
3
- export interface Server {
4
- id: string;
5
- coordinate: Coordinate;
6
- region?: PrivacyRegion;
7
- languageModelAvailability: AzureChatModels[] | string[];
8
- imageModelAvailability: AzureImageModels[] | string[];
9
- textEmbeddingModelAvailability: AzureEmbeddingModels[] | string[];
10
- }
@@ -1 +0,0 @@
1
- export {};