@chainfuse/ai-tools 0.1.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.
@@ -0,0 +1,137 @@
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
+ }
@@ -0,0 +1,9 @@
1
+ import type { AzureChatModels, AzureEmbeddingModels, 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
+ textEmbeddingModelAvailability: AzureEmbeddingModels[] | string[];
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,101 @@
1
+ import type { PrefixedUuid, RawCoordinate, UuidExport } from '@chainfuse/types';
2
+ import type { Ai, IncomingRequestCfProperties } from '@cloudflare/workers-types/experimental';
3
+ import type haversine from 'haversine-distance';
4
+ export interface AiConfig {
5
+ gateway: {
6
+ accountId: string;
7
+ apiToken: string;
8
+ };
9
+ geoRouting?: {
10
+ userCoordinate?: RawCoordinate;
11
+ country?: IncomingRequestCfProperties['country'];
12
+ continent?: IncomingRequestCfProperties['continent'];
13
+ };
14
+ environment: 'production' | 'preview';
15
+ providers: AiConfigProviders;
16
+ }
17
+ export interface AiConfigProviders {
18
+ anthropic: AiConfigAnthropic;
19
+ azureOpenAi: AiConfigAzOpenai;
20
+ openAi: AiConfigOaiOpenai;
21
+ workersAi: AiConfigWorkersai;
22
+ }
23
+ export interface AiConfigAnthropic {
24
+ apiToken: `sk-ant-${string}`;
25
+ }
26
+ export interface AiConfigAzOpenai {
27
+ apiTokens: Record<`AZURE_API_KEY_${string}`, string>;
28
+ }
29
+ export interface AiConfigOaiOpenai {
30
+ apiToken: `sk-${string}`;
31
+ organization: `org-${string}`;
32
+ }
33
+ export interface AiConfigWorkersaiRest {
34
+ apiToken: string;
35
+ }
36
+ /**
37
+ * @deprecated Not functional. Use REST instead
38
+ */
39
+ export type AiConfigWorkersaiBinding = Ai;
40
+ export type AiConfigWorkersai = AiConfigWorkersaiRest | AiConfigWorkersaiBinding;
41
+ /**
42
+ * It's a UUID, but the last block is SHA256 of the request body
43
+ */
44
+ export type AiRequestIdempotencyId = UuidExport['utf8'];
45
+ export interface AiRequestExecutor {
46
+ type: 'worker' | 'queue' | 'workflow' | 'githubCicd';
47
+ id: string;
48
+ }
49
+ export interface AiRequestConfig {
50
+ /**
51
+ * Sets if a response should be cached (1 month) or for any custom duration
52
+ * @default true
53
+ */
54
+ cache?: boolean | number;
55
+ dataspaceId: PrefixedUuid | UuidExport['utf8'] | UuidExport['hex'];
56
+ /**
57
+ * Service identification of caller
58
+ */
59
+ executor: AiRequestExecutor;
60
+ /**
61
+ * Identify the same request across multiple calls. If not provided, a new id will be generated
62
+ * Structure: <UUIDv7>.<last 8 of SHA256 of body>
63
+ */
64
+ idempotencyId?: AiRequestIdempotencyId;
65
+ /**
66
+ * Logging includes anything up to the ai call. For ai call info, see ai gateway
67
+ * @default false (on production environment), true (on preview environment)
68
+ */
69
+ logging?: boolean;
70
+ /**
71
+ * Force a response to be generated even if a matching request is already cached (without changing cache setting)
72
+ * @default false
73
+ */
74
+ skipCache?: boolean;
75
+ }
76
+ export interface AiRequestMetadataServerInfo {
77
+ name: 'anthropic' | 'cloudflare' | 'openai';
78
+ }
79
+ export interface AiRequestMetadataServerInfoWithLocation {
80
+ name: `${'azure' | 'google'}-${string}`;
81
+ /**
82
+ * @returns distance in meters
83
+ */
84
+ distance: ReturnType<typeof haversine>;
85
+ }
86
+ export interface AiRequestMetadataTiming {
87
+ modelTime?: number;
88
+ fromCache: boolean;
89
+ totalRoundtripTime: number;
90
+ }
91
+ export interface AiRequestMetadata {
92
+ dataspaceId: AiRequestConfig['dataspaceId'];
93
+ serverInfo: AiRequestMetadataServerInfo | AiRequestMetadataServerInfoWithLocation | string;
94
+ idempotencyId: AiRequestIdempotencyId;
95
+ executor: AiRequestExecutor | string;
96
+ timing: AiRequestMetadataTiming | string;
97
+ }
98
+ /**
99
+ * Extracts the chunk type from an asynchronous iterable.
100
+ */
101
+ export type AiStreamChunkType<T> = T extends AsyncIterable<infer U> ? Awaited<U> : never;
package/dist/types.mjs ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@chainfuse/ai-tools",
3
+ "version": "0.1.0",
4
+ "description": "",
5
+ "author": "ChainFuse",
6
+ "homepage": "https://github.com/ChainFuse/packages/tree/main/packages/ai-tools#readme",
7
+ "license": "Apache-2.0",
8
+ "main": "./dist/index.mjs",
9
+ "directories": {
10
+ "lib": "dist",
11
+ "test": "__tests__"
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "provenance": true
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/ChainFuse/packages.git"
23
+ },
24
+ "scripts": {
25
+ "fmt": "prettier --check .",
26
+ "fmt:fix": "prettier --write .",
27
+ "lint": "eslint .",
28
+ "lint:fix": "npm run lint -- --fix",
29
+ "clean": "npx -y rimraf@latest ./dist ./.tsbuildinfo",
30
+ "build": "tsc",
31
+ "build:clean": "npm run build -- --build --clean && npm run build",
32
+ "pretest": "tsc --project tsconfig.tests.json",
33
+ "test": "node --env-file-if-exists=.dev.vars --enable-source-maps --test --experimental-test-coverage --test-reporter=spec --test-reporter-destination=stdout"
34
+ },
35
+ "type": "module",
36
+ "bugs": {
37
+ "url": "https://github.com/ChainFuse/packages/issues"
38
+ },
39
+ "types": "./dist/index.d.mts",
40
+ "engines": {
41
+ "node": ">=22.11.0"
42
+ },
43
+ "exports": {
44
+ ".": {
45
+ "import": "./dist/index.mjs",
46
+ "types": "./dist/index.d.mts"
47
+ }
48
+ },
49
+ "prettier": "@demosjarco/prettier-config",
50
+ "dependencies": {
51
+ "@ai-sdk/anthropic": "^1.0.6",
52
+ "@ai-sdk/azure": "^1.0.15",
53
+ "@ai-sdk/openai": "^1.0.5",
54
+ "@chainfuse/helpers": "^0.5.0",
55
+ "@chainfuse/types": "^1.4.0",
56
+ "ai": "^4.0.25",
57
+ "chalk": "^5.4.1",
58
+ "haversine-distance": "^1.2.3",
59
+ "workers-ai-provider": "^0.0.10"
60
+ },
61
+ "devDependencies": {
62
+ "@cloudflare/workers-types": "^4.20241230.0",
63
+ "openai": "^4.77.3"
64
+ },
65
+ "gitHead": "13881adbb875a4b1680448205ff9e0ac55577f72"
66
+ }