@jdsl/router 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.
package/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # @jdsl/router
2
+
3
+ To install dependencies:
4
+
5
+ ```bash
6
+ bun install
7
+ ```
8
+
9
+ To run:
10
+
11
+ ```bash
12
+ bun run ./src/index.ts
13
+ ```
14
+
15
+ This project was created using `bun init` in bun v1.3.0. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@jdsl/router",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "author": "Machar Kook",
6
+ "license": "Apache-2.0",
7
+ "module": "./src/index.ts",
8
+ "devDependencies": {
9
+ "@types/bun": "latest"
10
+ },
11
+ "peerDependencies": {
12
+ "typescript": "^5"
13
+ },
14
+ "dependencies": {
15
+ "@effect/cli": "^0.75.2",
16
+ "@effect/platform": "^0.96.2",
17
+ "@effect/platform-bun": "^0.90.0",
18
+ "@effect/platform-node": "^0.107.0",
19
+ "effect": "3.21.0"
20
+ },
21
+ "exports": {
22
+ ".": "./src/index.ts",
23
+ "./config": "./src/config.ts",
24
+ "./error": "./src/types",
25
+ "./models-dev": "./src/models/models-dev.ts"
26
+ }
27
+ }
package/src/config.ts ADDED
@@ -0,0 +1,96 @@
1
+ import { homedir } from "os";
2
+ import { dirname } from "path";
3
+
4
+ import { Either, Effect, Schema } from "effect";
5
+ import { FileSystem, Path } from "@effect/platform";
6
+ import { AiProvider } from "../../provider/src/providers.ts";
7
+ import { LoadAPIKeyError } from "./types.ts";
8
+
9
+ export const ProvidersList = Schema.Literal("anthropic", "deepseek", "google", "nvidia", "openai", "recon", "zhipuai", "zai", "zai-coding-plan", "zhipuai-coding-plan");
10
+ export type Providers = typeof ProvidersList.Type;
11
+
12
+ const ProviderConfig = Schema.Struct({
13
+ apiKey: Schema.Array(Schema.String),
14
+ });
15
+
16
+ const ConfigSchema = Schema.Struct({
17
+ "anthropic": Schema.optional(ProviderConfig),
18
+ "deepseek": Schema.optional(ProviderConfig),
19
+ "google": Schema.optional(ProviderConfig),
20
+ "nvidia": Schema.optional(ProviderConfig),
21
+ "openai": Schema.optional(ProviderConfig),
22
+ "recon": Schema.optional(ProviderConfig),
23
+ "zhipuai": Schema.optional(ProviderConfig),
24
+ "zai": Schema.optional(ProviderConfig),
25
+ "zai-coding-plan": Schema.optional(ProviderConfig),
26
+ "zhipuai-coding-plan": Schema.optional(ProviderConfig),
27
+ })
28
+
29
+ interface Config extends Schema.Schema.Type<typeof ConfigSchema> { }
30
+
31
+ export class AiModelConfig extends Effect.Service<AiModelConfig>()(
32
+ "AiModelConfig",
33
+ {
34
+ effect: Effect.gen(function* () {
35
+ const fs = yield* FileSystem.FileSystem;
36
+ const path = yield* Path.Path;
37
+ const provider = yield* AiProvider;
38
+
39
+ let config: Config;
40
+
41
+ const home = homedir();
42
+ const configFilename = "auth.json";
43
+ const configDir = ".local/share/recon";
44
+ const configPath = path.join(home, configDir, configFilename);
45
+
46
+ const openConfig = (filePath: string) => Effect.gen(function* () {
47
+ const configResult = yield* fs.readFileString(filePath);
48
+ const parsedJsonObj = JSON.parse(configResult);
49
+ const config = yield* Schema.decode(ConfigSchema)(parsedJsonObj);
50
+ return config as Config;
51
+ })
52
+
53
+ const saveConfig = (cfg: Config) => Effect.gen(function* () {
54
+ const oldConfig = yield* Effect.either(openConfig(configPath));
55
+ let newConfig: Config;
56
+
57
+ if (Either.isLeft(oldConfig)) {
58
+ newConfig = {};
59
+ } else {
60
+ const encodedConfig = yield* Schema.encode(ConfigSchema)(cfg);
61
+ newConfig = { ...oldConfig.right, ...encodedConfig };
62
+ }
63
+ config = newConfig;
64
+ const jsonString = JSON.stringify(newConfig, null, 2);
65
+
66
+ const dirPath = dirname(configPath);
67
+ const dirExists = yield* fs.exists(dirPath);
68
+ if (!dirExists) {
69
+ yield* fs.makeDirectory(dirPath, { recursive: true });
70
+ }
71
+
72
+ yield* fs.writeFileString(configPath, jsonString,);
73
+ })
74
+
75
+ const getConfig = () => Effect.gen(function* () {
76
+ const currentProvider = yield* provider.getProvider();
77
+ const cfg = config[currentProvider];
78
+ return cfg ? cfg : yield* new LoadAPIKeyError({name: "config", msg: `${currentProvider} apiKey/auth is missing`, isRetryable: false})
79
+ })
80
+
81
+ const listConfig = () => Effect.gen(function* () {
82
+ return config;
83
+ })
84
+
85
+ const configResult = yield* Effect.either(openConfig(configPath));
86
+ if (Either.isLeft(configResult)) {
87
+ yield* saveConfig({});
88
+ config = {};
89
+ } else {
90
+ config = configResult.right;
91
+ }
92
+
93
+ return { getConfig, listConfig, saveConfig } as const;
94
+ })
95
+ }
96
+ ) { }
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { Effect, Layer } from "effect";
2
+
3
+ export class Router extends Effect.Service<Router>()(
4
+ "Router",
5
+ {
6
+ effect: Effect.gen(function* () {
7
+ const getConfig = () => "Router";
8
+
9
+ return { getConfig } as const;
10
+ })
11
+ }
12
+ ){}
13
+
14
+ export const RoundRobinRouter = Layer.effect(Router, Effect.gen(function* () {
15
+ return Router.make({
16
+ getConfig: () => "Round Robin Router"
17
+ })
18
+ }))
19
+
@@ -0,0 +1,81 @@
1
+ import { Schema } from "effect"
2
+
3
+ const DateString = Schema.String.pipe(
4
+ Schema.pattern(/^\d{4}-\d{2}(-\d{2})?$/)
5
+ );
6
+
7
+ const InterleavedObject = Schema.Struct({
8
+ field: Schema.Literal("reasoning_content", "reasoning_details")
9
+ });
10
+
11
+ const InterleavedSchema = Schema.Union(
12
+ Schema.Boolean,
13
+ InterleavedObject
14
+ );
15
+
16
+ const CostSchema = Schema.Struct({
17
+ input: Schema.Number,
18
+ output: Schema.Number,
19
+
20
+ reasoning: Schema.optional(Schema.Number),
21
+ cache_read: Schema.optional(Schema.Number),
22
+ cache_write: Schema.optional(Schema.Number),
23
+ input_audio: Schema.optional(Schema.Number),
24
+ output_audio: Schema.optional(Schema.Number)
25
+ });
26
+
27
+ const LimitSchema = Schema.Struct({
28
+ context: Schema.Number,
29
+ input: Schema.optional(Schema.Number),
30
+ output: Schema.Number
31
+ });
32
+
33
+ const ModalitiesSchema = Schema.Struct({
34
+ input: Schema.Array(Schema.String),
35
+ output: Schema.Array(Schema.String)
36
+ });
37
+
38
+ const StatusSchema = Schema.Literal(
39
+ "alpha",
40
+ "beta",
41
+ "deprecated"
42
+ );
43
+
44
+ const ModelSchema = Schema.Struct({
45
+ name: Schema.String,
46
+ id: Schema.String,
47
+ family: Schema.optional(Schema.String),
48
+
49
+ attachment: Schema.Boolean,
50
+ reasoning: Schema.Boolean,
51
+ tool_call: Schema.Boolean,
52
+ structured_output: Schema.optional(Schema.Boolean),
53
+ temperature: Schema.optional(Schema.Boolean),
54
+
55
+ knowledge: Schema.optional(DateString),
56
+ release_date: DateString,
57
+ last_updated: DateString,
58
+ open_weights: Schema.Boolean,
59
+
60
+ interleaved: Schema.optional(InterleavedSchema),
61
+
62
+ cost: Schema.optional(CostSchema),
63
+ limit: LimitSchema,
64
+ modalities: ModalitiesSchema,
65
+ status: Schema.optional(StatusSchema)
66
+ });
67
+ export interface Model extends Schema.Schema.Type<typeof ModelSchema> { };
68
+
69
+ const ProviderSchema = Schema.Struct({
70
+ id: Schema.String,
71
+ name: Schema.String,
72
+ npm: Schema.String,
73
+ env: Schema.Array(Schema.String),
74
+ doc: Schema.String,
75
+ api: Schema.optional(Schema.String),
76
+ models: Schema.Record({key: Schema.String, value: ModelSchema})
77
+ });
78
+ export interface Provider extends Schema.Schema.Type<typeof ProviderSchema> { };
79
+
80
+ export const ModelsDevSchema = Schema.Record({key: Schema.String, value: ProviderSchema});
81
+ export interface ModelProviders extends Schema.Schema.Type<typeof ModelsDevSchema> { };
@@ -0,0 +1,91 @@
1
+ import { homedir } from "os";
2
+
3
+ import { Data, Effect, Option, Schema } from "effect";
4
+ import { FileSystem, Path } from "@effect/platform";
5
+
6
+ import { type ModelProviders, type Provider, ModelsDevSchema } from "./modelSchema.ts";
7
+ import type { Providers } from "../config.ts";
8
+ import { NoSuchModelError } from "../types.ts";
9
+
10
+ export class FetchError extends Data.TaggedError("FetchError")<{ name: string, msg: string, isRetryable: boolean }> { }
11
+ export class CacheError extends Data.TaggedError("CacheError")<{ msg: string }> { }
12
+
13
+ export class ModelsDev extends Effect.Service<ModelsDev>()(
14
+ "ModelsDev",
15
+ {
16
+ effect: Effect.gen(function* () {
17
+ const fs = yield* FileSystem.FileSystem;
18
+ const path = yield* Path.Path;
19
+
20
+ let models: ModelProviders;
21
+
22
+ const home = homedir();
23
+ const modelCacheFilename = "models-dev.json";
24
+ const modelCacheDir = ".local/share/recon"
25
+ const modelCachePath = path.join(home, modelCacheDir, modelCacheFilename);
26
+
27
+ const fetchModels = Effect.gen(function* () {
28
+ const getModels = Effect.tryPromise(async () => {
29
+ const url = "https://models.dev/api.json";
30
+ const response = await fetch(url);
31
+ const jsonResponse = await response.json() as any;
32
+ return jsonResponse;
33
+ }).pipe(
34
+ Effect.mapError(e => {
35
+ new FetchError({name: "FetchError", msg: e.message, isRetryable: true});
36
+ })
37
+ );
38
+ const models = yield* getModels
39
+ const result = yield* Schema.decode(ModelsDevSchema)(models);
40
+ return result as ModelProviders
41
+ })
42
+
43
+ const saveModelCache = (models: ModelProviders) => Effect.gen(function* () {
44
+ const dirPath = path.dirname(modelCachePath);
45
+ const dirExists = yield* fs.exists(dirPath);
46
+ const jsonString = JSON.stringify(models, null, 2);
47
+ if (!dirExists) {
48
+ yield* fs.makeDirectory(dirPath, { recursive: true });
49
+ }
50
+ yield* fs.writeFileString(modelCachePath, jsonString);
51
+ })
52
+
53
+ const openModelCache = (filePath: string) => Effect.gen(function* () {
54
+ const stats = yield* fs.stat(filePath)
55
+ const now = new Date();
56
+ const ttlInMillis = 24 * 3600 * 1000;
57
+ const modifiedTime = Option.getOrThrow(stats.mtime);
58
+ if ((modifiedTime.getTime() - now.getTime()) > ttlInMillis) {
59
+ return yield* new CacheError({ msg: "Saved models cache has expired and should be updated" })
60
+ }
61
+
62
+ const cacheResult = yield* fs.readFileString(filePath);
63
+ const parsedJsonObj = JSON.parse(cacheResult);
64
+ const cache = yield* Schema.decode(ModelsDevSchema)(parsedJsonObj);
65
+ return cache as ModelProviders
66
+ });
67
+
68
+ const getProvider = (provider: Providers) => Effect.gen(function* () {
69
+ const results = models[provider];
70
+ if (!results) {
71
+ return yield* new NoSuchModelError({ name: "NoSuchModelError", msg: `invalid ${provider}. Could not find ${provider} on models.dev providers`, isRetryable: false })
72
+ }
73
+ return results as Provider
74
+ });
75
+
76
+ const listProviders = () => Effect.sync(() => {
77
+ return Object.keys(models);
78
+ });
79
+
80
+ models = yield* openModelCache(modelCachePath).pipe(
81
+ Effect.catchAll(() => Effect.gen(function* () {
82
+ const modelsCache = yield* fetchModels;
83
+ yield* saveModelCache(modelsCache);
84
+ return modelsCache;
85
+ }))
86
+ )
87
+
88
+ return { getProvider, listProviders } as const;
89
+ })
90
+ }
91
+ ) { }
package/src/types.ts ADDED
@@ -0,0 +1,88 @@
1
+ import { Data, Effect } from "effect";
2
+ import type { UnknownException } from "effect/Cause";
3
+
4
+ import {
5
+ APICallError as UnknownAPICallError,
6
+ LoadAPIKeyError as UnknownLoadAPIKeyError,
7
+ JSONParseError as UnknownJSONParseError,
8
+ NoSuchModelError as UnknownNoSuchModelError,
9
+ NoSuchProviderError as UnknownNoSuchProviderError,
10
+ RetryError as UnknownRetryError,
11
+ type ContentPart,
12
+ type FinishReason,
13
+ type LanguageModelUsage,
14
+ type ReasoningOutput,
15
+ type ToolSet
16
+ } from "ai";
17
+
18
+ export interface GenerateTextResponse {
19
+ content: ContentPart<ToolSet>[];
20
+ text: string;
21
+ reasoning: ReasoningOutput[];
22
+ reasoningText: string | undefined;
23
+ finishReason: FinishReason;
24
+ usage: LanguageModelUsage;
25
+ totalUsage: LanguageModelUsage;
26
+ }
27
+
28
+ export interface StreamTextResponse {
29
+ content: PromiseLike<ContentPart<ToolSet>[]>;
30
+ text: PromiseLike<string>;
31
+ textStream: AsyncIterable<string>;
32
+ reasoning: PromiseLike<ReasoningOutput[]>;
33
+ reasoningText: PromiseLike<string | undefined>;
34
+ finishReason: PromiseLike<FinishReason>;
35
+ usage: PromiseLike<LanguageModelUsage>;
36
+ totalUsage: PromiseLike<LanguageModelUsage>;
37
+ }
38
+
39
+ export interface AiError {
40
+ name: string;
41
+ msg: string;
42
+ isRetryable: boolean;
43
+ }
44
+
45
+ export class APICallError extends Data.TaggedError("ApiCallError")<AiError> { };
46
+ export class LoadAPIKeyError extends Data.TaggedError("LoadAPIKeyError")<AiError> { };
47
+ export class JSONParseError extends Data.TaggedError("JSONParseError")<AiError> { };
48
+ export class NoSuchModelError extends Data.TaggedError("NoSuchModelError")<AiError> { };
49
+ export class NoSuchProviderError extends Data.TaggedError("NoSuchProviderError")<AiError> { };
50
+ export class RetryError extends Data.TaggedError("RetryError")<AiError> { };
51
+ export class InvalidApiKeyError extends Data.TaggedError("InvalidApiKeyError")<AiError> { };
52
+ export class InsufficientBalanceError extends Data.TaggedError("InsufficientBalanceError")<AiError> { };
53
+ export class RateLimitError extends Data.TaggedError("RateLimitError")<AiError> { };
54
+ export class ServerError extends Data.TaggedError("ServerError")<AiError> { };
55
+
56
+
57
+ export const mapLanguageModelError = (e: Effect.Effect<GenerateTextResponse | StreamTextResponse, UnknownException, never>) => Effect.mapError(e, (e) => {
58
+ if (UnknownAPICallError.isInstance(e.cause)) {
59
+ const cause = e.cause;
60
+ if (cause.statusCode === 400 || cause.statusCode === 401) {
61
+ return new InvalidApiKeyError({ name: "InvalidApiKeyError", msg: cause.message, isRetryable: cause.isRetryable });
62
+ } else if (cause.statusCode === 403) {
63
+ return new InsufficientBalanceError({ name: "InsufficientBalance", msg: cause.message, isRetryable: cause.isRetryable });
64
+ } else if (cause.statusCode === 429) {
65
+ return new RateLimitError({ name: "RateLimitError", msg: cause.message, isRetryable: cause.isRetryable });
66
+ } else if (cause.statusCode === 500) {
67
+ return new ServerError({ name: "ServerError", msg: cause.message, isRetryable: cause.isRetryable });
68
+ }
69
+ return new APICallError({ name: "APICallError", msg: cause.message, isRetryable: cause.isRetryable });
70
+ }
71
+ if (UnknownLoadAPIKeyError.isInstance(e.cause)) {
72
+ return new LoadAPIKeyError({ name: "LoadAPIKeyError", msg: e.cause.message, isRetryable: false });
73
+ }
74
+ if (UnknownJSONParseError.isInstance(e.cause)) {
75
+ return new JSONParseError({ name: "JSONParseError", msg: e.cause.message, isRetryable: false });
76
+ }
77
+ if (UnknownNoSuchModelError.isInstance(e.cause)) {
78
+ return new NoSuchModelError({ name: "NoSuchModelError", msg: e.cause.message, isRetryable: false });
79
+ }
80
+ if (UnknownNoSuchProviderError.isInstance(e.cause)) {
81
+ return new NoSuchProviderError({ name: "NoSuchProviderError", msg: e.cause.message, isRetryable: false });
82
+ }
83
+ if (UnknownRetryError.isInstance(e.cause)) {
84
+ return new RetryError({ name: "RetryError", msg: e.cause.message, isRetryable: false });
85
+ }
86
+
87
+ return new InvalidApiKeyError({ name: "UnkownCertificateError", msg: "Forbidden Resource, Authorization Failed", isRetryable: false });
88
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "nodenext",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "nodenext",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "noEmit": true,
16
+
17
+ // Best practices
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedIndexedAccess": true,
22
+ "noImplicitOverride": true,
23
+
24
+ // Some stricter flags (disabled by default)
25
+ "noUnusedLocals": false,
26
+ "noUnusedParameters": false,
27
+ "noPropertyAccessFromIndexSignature": false
28
+ }
29
+ }