@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 +2 -0
- package/dist/base.mjs +16 -0
- package/dist/models.d.mts +2 -2
- package/dist/models.mjs +5 -5
- package/dist/providers/customProviders.d.mts +2 -2
- package/dist/providers/customProviders.mjs +23 -9
- package/dist/providers/rawProviders.d.mts +5 -3
- package/dist/providers/rawProviders.mjs +38 -31
- package/dist/registry.mjs +5 -7
- package/dist/serverSelector.d.mts +17 -0
- package/dist/serverSelector.mjs +151 -0
- package/dist/types.d.mts +20 -3
- package/package.json +11 -11
- package/dist/serverSelector/azure.d.mts +0 -5
- package/dist/serverSelector/azure.mjs +0 -284
- package/dist/serverSelector/base.d.mts +0 -22
- package/dist/serverSelector/base.mjs +0 -137
- package/dist/serverSelector/types.d.mts +0 -10
- package/dist/serverSelector/types.mjs +0 -1
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 {
|
|
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
|
|
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,
|
|
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 {
|
|
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,
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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 {
|
|
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:
|
|
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,
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
103
|
-
lon: Helpers.precisionFloat(
|
|
104
|
-
},
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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?:
|
|
11
|
+
userCoordinate?: Coordinate;
|
|
11
12
|
country?: IncomingRequestCfProperties['country'];
|
|
12
13
|
continent?: IncomingRequestCfProperties['continent'];
|
|
13
14
|
};
|
|
14
|
-
|
|
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.
|
|
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
|
|
52
|
-
"@ai-sdk/azure": "^1.2
|
|
53
|
-
"@ai-sdk/google": "^1.
|
|
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
|
|
56
|
-
"@chainfuse/helpers": "^2.
|
|
57
|
-
"@chainfuse/types": "^1.
|
|
58
|
-
"ai": "^4.
|
|
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.
|
|
64
|
+
"@cloudflare/workers-types": "^4.20250321.0",
|
|
65
65
|
"openai": "^4.89.0"
|
|
66
66
|
},
|
|
67
|
-
"gitHead": "
|
|
67
|
+
"gitHead": "f0eaab21228260be471a6fac0874068139677035"
|
|
68
68
|
}
|
|
@@ -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 {};
|