@jdsl/provider 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
+ # providers
2
+
3
+ To install dependencies:
4
+
5
+ ```bash
6
+ bun install
7
+ ```
8
+
9
+ To run:
10
+
11
+ ```bash
12
+ bun run 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/index.ts ADDED
@@ -0,0 +1,41 @@
1
+ import { Effect } from "effect";
2
+ import { generateText as generateTextAiSdk, streamText as streamTextAiSdk } from "ai";
3
+
4
+ import { AiModel } from "./src/aiModel.ts";
5
+
6
+ export class LanguageModel extends Effect.Service<LanguageModel>()(
7
+ "LanguageModel",
8
+ {
9
+ effect: Effect.gen(function* () {
10
+ const aiModel = yield* AiModel;
11
+
12
+ const generateText = (text: string) => Effect.gen(function* () {
13
+ const model = yield* aiModel.getModel();
14
+
15
+ const fromAsync = (prompt: string) => Effect.tryPromise(async () => {
16
+ const response = await generateTextAiSdk({
17
+ model: model,
18
+ prompt: prompt
19
+ })
20
+ return response
21
+ })
22
+ return yield* fromAsync(text)
23
+ })
24
+
25
+ const streamText = (text: string) => Effect.gen(function* () {
26
+ const model = yield* aiModel.getModel();
27
+
28
+ const fromAsync = (prompt: string) => Effect.tryPromise(async () => {
29
+ const response = await streamTextAiSdk({
30
+ model: model,
31
+ prompt: prompt
32
+ })
33
+ return response
34
+ })
35
+ return yield* fromAsync(text)
36
+ })
37
+
38
+ return { generateText, streamText } as const
39
+ })
40
+ }
41
+ ) { }
package/main.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { createProviderRegistry, generateText} from 'ai';
2
+ import { openai } from '@ai-sdk/openai';
3
+ import { anthropic } from '@ai-sdk/anthropic';
4
+ import { google } from '@ai-sdk/google';
5
+
6
+ google("gemini-2.0-flash");
7
+ export const registry = createProviderRegistry({
8
+ // register provider with prefix and direct provider import:
9
+ anthropic,
10
+ openai,
11
+ });
12
+
13
+ const { text } = await generateText({
14
+ model: registry.languageModel('openai:gpt-5.1'), // default separator
15
+ // or with custom separator:
16
+ // model: customSeparatorRegistry.languageModel('openai > gpt-5.1'),
17
+ prompt: 'Invent a new holiday and describe its traditions.',
18
+ });
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@jdsl/provider",
3
+ "version": "0.1.0",
4
+ "module": "index.ts",
5
+ "type": "module",
6
+ "devDependencies": {
7
+ "@types/bun": "latest"
8
+ },
9
+ "peerDependencies": {
10
+ "typescript": "^5"
11
+ },
12
+ "dependencies": {
13
+ "@ai-sdk/anthropic": "^3.0.38",
14
+ "@ai-sdk/google": "^3.0.22",
15
+ "@ai-sdk/openai": "^3.0.26",
16
+ "ai": "^6.0.77",
17
+ "effect": "^3.21.0"
18
+ },
19
+ "exports": {
20
+ ".": "./index.ts",
21
+ "./aimodel": "./src/aiModel.ts",
22
+ "./models-dev": "./src/models/models-dev.ts",
23
+ "./config": "./src/config.ts",
24
+ "./providers": "./src/providers.ts",
25
+ "./context": "./src/context/context.ts"
26
+ }
27
+ }
package/src/aiModel.ts ADDED
@@ -0,0 +1,43 @@
1
+ import { Data, Effect } from "effect";
2
+
3
+ import { createGoogleGenerativeAI, type GoogleGenerativeAIProvider } from "@ai-sdk/google";
4
+ import { createOpenAI, type OpenAIProvider } from "@ai-sdk/openai";
5
+ import { createAnthropic, type AnthropicProvider } from "@ai-sdk/anthropic";
6
+
7
+ import { AiModelConfig } from "./config.ts";
8
+ import { AiProvider } from "./providers.ts";
9
+
10
+ export class AiError extends Data.TaggedError("AiError")<{msg: string}>{};
11
+ export type AiModelProvider = AnthropicProvider | GoogleGenerativeAIProvider | OpenAIProvider;
12
+
13
+ export class AiModel extends Effect.Service<AiModel>()(
14
+ "AiModel",
15
+ {
16
+ effect: Effect.gen(function* () {
17
+ const modelConfig = yield* AiModelConfig;
18
+ const modelProvider = yield* AiProvider;
19
+
20
+ const getModel = () => Effect.gen(function*() {
21
+ const config = yield* modelConfig.getConfig();
22
+ const provider = yield* modelProvider.getProvider();
23
+
24
+ switch (provider) {
25
+ case "anthropic":
26
+ return createAnthropic(config)(yield* modelProvider.getModelName());
27
+
28
+ case "google":
29
+ return createGoogleGenerativeAI(config) (yield* modelProvider.getModelName());
30
+
31
+ case "openai":
32
+ return createOpenAI(config) (yield* modelProvider.getModelName());
33
+
34
+ default:
35
+ return yield* new AiError({msg: `${provider} not supported yet`});
36
+ }
37
+ });
38
+
39
+ return {getModel} as const;
40
+ })
41
+ }
42
+ ){}
43
+
package/src/config.ts ADDED
@@ -0,0 +1,85 @@
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 "./providers.ts";
7
+
8
+ export const ProvidersList = Schema.Literal("anthropic", "google", "openai", "recon");
9
+ export type Providers = typeof ProvidersList.Type;
10
+
11
+ const ProviderConfig = Schema.Struct({
12
+ apiKey: Schema.optional(Schema.String),
13
+ authToken: Schema.optional(Schema.String)
14
+ })
15
+
16
+ const ConfigSchema = Schema.Struct({
17
+ "anthropic": Schema.optional(ProviderConfig),
18
+ "google": Schema.optional(ProviderConfig),
19
+ "openai": Schema.optional(ProviderConfig),
20
+ "recon": Schema.optional(ProviderConfig)
21
+ })
22
+
23
+ interface Config extends Schema.Schema.Type<typeof ConfigSchema> {}
24
+
25
+ export class AiModelConfig extends Effect.Service<AiModelConfig>()(
26
+ "AiModelConfig",
27
+ {
28
+ effect: Effect.gen(function* () {
29
+ const fs = yield* FileSystem.FileSystem;
30
+ const path = yield* Path.Path;
31
+ const provider = yield* AiProvider;
32
+
33
+ let config: Config;
34
+
35
+ const home = homedir();
36
+ const configFilename = "auth.json";
37
+ const configDir = ".local/share/recon";
38
+ const configPath = path.join(home, configDir, configFilename);
39
+
40
+ const openConfig = (filePath: string) => Effect.gen(function* () {
41
+ const configResult = yield* fs.readFileString(filePath);
42
+ const parsedJsonObj = JSON.parse(configResult);
43
+ const config = yield* Schema.decode(ConfigSchema)(parsedJsonObj);
44
+ return config as Config;
45
+ })
46
+
47
+ const saveConfig = (cfg: Config) => Effect.gen(function*() {
48
+ const oldConfig = yield* Effect.either(openConfig(configPath));
49
+ let newConfig: Config;
50
+
51
+ if (Either.isLeft(oldConfig)) {
52
+ newConfig = {};
53
+ } else {
54
+ const encodedConfig = yield* Schema.encode(ConfigSchema)(cfg);
55
+ newConfig = {...oldConfig.right, ...encodedConfig };
56
+ }
57
+ config = newConfig;
58
+ const jsonString = JSON.stringify(newConfig, null, 2);
59
+
60
+ const dirPath = dirname(configPath);
61
+ const dirExists = yield* fs.exists(dirPath);
62
+ if (!dirExists) {
63
+ yield* fs.makeDirectory(dirPath, {recursive: true});
64
+ }
65
+
66
+ yield* fs.writeFileString(configPath, jsonString, );
67
+ })
68
+
69
+ const getConfig = () => Effect.gen(function*() {
70
+ const currentProvider = yield* provider.getProvider()
71
+ return config[currentProvider] ?? {}
72
+ })
73
+
74
+ const configResult = yield* Effect.either(openConfig(configPath));
75
+ if (Either.isLeft(configResult)) {
76
+ yield* saveConfig({});
77
+ config = {};
78
+ } else {
79
+ config = configResult.right;
80
+ }
81
+
82
+ return { getConfig, saveConfig } as const;
83
+ })
84
+ }
85
+ ) { }
@@ -0,0 +1,35 @@
1
+ import { Effect } from "effect";
2
+
3
+ export interface Content {
4
+ type: "text" | "image",
5
+ text: string
6
+ };
7
+
8
+ export interface Message {
9
+ role: "user" | "assistant" | "system";
10
+ content: string | Content[]
11
+ };
12
+
13
+ export interface Context {
14
+ system?: string ;
15
+ message?: string | Message;
16
+ };
17
+
18
+ export class ContextWindow extends Effect.Service<ContextWindow>()(
19
+ "ContextWindow",
20
+ {
21
+ effect: Effect.gen(function* () {
22
+ const systemInstructions: string[] = [];
23
+ const messages: Message[] = []
24
+
25
+ const addSystemInstruction = (instruction: string) => Effect.sync(() => systemInstructions.push(instruction));
26
+ const addMessage = (message: Message) => Effect.sync(() => messages.push(message));
27
+
28
+ const join = () => Effect.sync(() => {
29
+ const system = systemInstructions.join("\n");
30
+ return {systemInstructions: system, messages: messages};
31
+ })
32
+ return { addSystemInstruction, addMessage, join } as const;
33
+ })
34
+ }
35
+ ) { }
@@ -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,136 @@
1
+ import { homedir } from "os";
2
+ import { dirname } from "path";
3
+ import { stat } from "fs";
4
+ import { promisify } from "util";
5
+
6
+ import { Data, Effect, Either, Schema } from "effect";
7
+ import { FileSystem, Path } from "@effect/platform";
8
+
9
+ import { type ModelProviders, type Provider, ModelsDevSchema } from "./modelSchema.ts";
10
+ import type { Providers } from "../config.ts";
11
+
12
+ const CacheMetadataSchema = Schema.Struct({
13
+ size: Schema.Number,
14
+ modified: Schema.Date,
15
+ created: Schema.Date
16
+ })
17
+ interface CacheMetadata extends Schema.Schema.Type<typeof CacheMetadataSchema> { }
18
+
19
+ export class ModelsDevError extends Data.TaggedError("ModelsDevError")<{ msg: string, error?: unknown }> { }
20
+ export class HttpError extends Data.TaggedError("HttpError")<{ status: number, statusText: string }> { }
21
+ export class JSONParseError extends Data.TaggedError("JSONParseError")<{ error: unknown }> { }
22
+ export class FetchError extends Data.TaggedError("FetchError")<{ error: unknown }> { }
23
+ export class CacheError extends Data.TaggedError("CacheError")<{ msg: string }> { }
24
+
25
+ export class ModelsDev extends Effect.Service<ModelsDev>()(
26
+ "ModelsDev",
27
+ {
28
+ effect: Effect.gen(function* () {
29
+ const fs = yield* FileSystem.FileSystem;
30
+ const path = yield* Path.Path;
31
+
32
+ let models: ModelProviders;
33
+
34
+ const home = homedir();
35
+ const modelCacheFilename = "models-dev.json";
36
+ const modelCacheDir = ".local/share/recon"
37
+ const modelCachePath = path.join(home, modelCacheDir, modelCacheFilename);
38
+
39
+ const fetchModels = Effect.gen(function* () {
40
+ const getModels = Effect.tryPromise({
41
+ try: async () => {
42
+ const url = "https://models.dev/api.json";
43
+ const response = await fetch(url);
44
+ if (!response.ok) {
45
+ throw new HttpError({ status: response.status, statusText: response.statusText })
46
+ }
47
+ try {
48
+ const jsonResponse = await response.json() as any;
49
+ return jsonResponse;
50
+ } catch (error) {
51
+ throw new JSONParseError({ error })
52
+ }
53
+ },
54
+ catch: (error) => {
55
+ if (error instanceof HttpError || error instanceof JSONParseError) {
56
+ return error
57
+ }
58
+ return new FetchError({ error })
59
+ }
60
+ })
61
+
62
+ const models = yield* getModels
63
+ const result = yield* Schema.decode(ModelsDevSchema)(models);
64
+ return result as ModelProviders
65
+ })
66
+
67
+ const updateModels = Effect.gen(function* () {
68
+ const cacheStats = yield* Effect.either(getCacheMetadata());
69
+ const now = new Date();
70
+ const ttlInMillis = 24 * 3600 * 1000;
71
+ if (Either.isRight(cacheStats)) {
72
+ const stats = cacheStats.right;
73
+ const elapsedTime = now.getTime() - Date.parse(stats.modified);
74
+ if (elapsedTime > ttlInMillis) {
75
+ return true;
76
+ }
77
+ }
78
+ return false
79
+ })
80
+
81
+ const getCacheMetadata = () => Effect.gen(function* () {
82
+ const fromAsync = (path: string) => Effect.tryPromise(async () => {
83
+ const statPromise = promisify(stat);
84
+ return await statPromise(path);
85
+ })
86
+ const stats = yield* fromAsync(modelCachePath)
87
+ return yield* Schema.encode(CacheMetadataSchema)({ size: stats.size, modified: stats.mtime, created: stats.ctime });
88
+ })
89
+
90
+ const getProvider = (provider: Providers) => Effect.gen(function* () {
91
+ const results = models[provider];
92
+ if (!results) {
93
+ return yield* new ModelsDevError({ msg: `invalid ${provider}. Could not find ${provider} on models.dev providers` })
94
+ }
95
+ return results as Provider
96
+ });
97
+
98
+ const listProviders = () => Effect.sync(() => {
99
+ return Object.keys(models);
100
+ });
101
+
102
+ const openModelCache = (filePath: string) => Effect.gen(function* () {
103
+ const cacheResult = yield* fs.readFileString(filePath);
104
+ const parsedJsonObj = JSON.parse(cacheResult);
105
+ const cache = yield* Schema.decode(ModelsDevSchema)(parsedJsonObj);
106
+ return cache as ModelProviders
107
+ })
108
+
109
+ const saveModelCache = (models: ModelProviders) => Effect.gen(function* () {
110
+ const dirPath = dirname(modelCachePath);
111
+ const dirExists = yield* fs.exists(dirPath);
112
+ const jsonString = JSON.stringify(models, null, 2);
113
+ if (!dirExists) {
114
+ yield* fs.makeDirectory(dirPath, { recursive: true });
115
+ }
116
+ yield* fs.writeFileString(modelCachePath, jsonString);
117
+ })
118
+
119
+ const modelsCache = yield* Effect.either(openModelCache(modelCachePath))
120
+ if (Either.isLeft(modelsCache)) {
121
+ models = yield* fetchModels;
122
+ yield* saveModelCache(models);
123
+ }
124
+ else {
125
+ const status = yield* updateModels;
126
+ if (status) {
127
+ models = yield* fetchModels;
128
+ yield* saveModelCache(models);
129
+ } else {
130
+ models = modelsCache.right
131
+ }
132
+ }
133
+ return { getProvider, listProviders } as const
134
+ })
135
+ }
136
+ ) { }
@@ -0,0 +1,59 @@
1
+ import { Data, Effect, Either } from "effect";
2
+ import type { Providers } from "./config.ts";
3
+ import { ModelsDev } from "./models/models-dev.ts";
4
+ import type { Model } from "./models/modelSchema.ts";
5
+
6
+ export class AiProviderError extends Data.TaggedError("AiProviderError")<{ msg: string}> { }
7
+ export class AiProvider extends Effect.Service<AiProvider>()(
8
+ "AiProvider",
9
+ {
10
+ effect: Effect.gen(function*(){
11
+ const modelsDev = yield* ModelsDev;
12
+ let provider: Providers = "recon";
13
+ let modelName: string = "";
14
+
15
+ const chooseProvider = (name: Providers) => Effect.gen(function*() {
16
+ const providers = yield* modelsDev.listProviders();
17
+ if (!providers.includes(name)) {
18
+ return yield* new AiProviderError({msg: `${name} is not a supported provider`});
19
+ }
20
+ provider = name;
21
+ })
22
+
23
+ const chooseModel = (name: string) => Effect.gen(function* () {
24
+ const modelList = yield* listModels();
25
+ if (!modelList.includes(name)) {
26
+ return yield* new AiProviderError({msg: `${getProvider()} does not have a model ${name}`});
27
+ }
28
+ modelName = name
29
+ })
30
+
31
+ const getProvider = () => Effect.succeed(provider);
32
+
33
+ const getModelName = () => Effect.gen(function* (){
34
+ yield* chooseModel(modelName);
35
+ return modelName
36
+ })
37
+
38
+ const getModelSpecs = () => Effect.gen(function* () {
39
+ const results = yield* modelsDev.getProvider(provider);
40
+ const modelSpecs = results.models[modelName];
41
+
42
+ if (!modelSpecs) {
43
+ return yield* new AiProviderError({msg: `${provider} does not have a model named ${modelName}.`});
44
+ }
45
+ return modelSpecs as Model;
46
+ })
47
+
48
+ const listModels = () => Effect.gen(function* () {
49
+ const models = yield* Effect.either(modelsDev.getProvider(provider));
50
+ if (Either.isLeft(models)) {
51
+ return [] as string[];
52
+ }
53
+ return Object.keys(models.right.models);
54
+ });
55
+
56
+ return {chooseModel, chooseProvider, getModelName, getModelSpecs, getProvider, listModels} as const;
57
+ })
58
+ }
59
+ ){}
package/tsconfig.json ADDED
@@ -0,0 +1,30 @@
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
+ "baseUrl": ".",
11
+
12
+ // Bundler mode
13
+ "moduleResolution": "nodenext",
14
+ "allowImportingTsExtensions": true,
15
+ "verbatimModuleSyntax": true,
16
+ "noEmit": true,
17
+
18
+ // Best practices
19
+ "strict": true,
20
+ "skipLibCheck": true,
21
+ "noFallthroughCasesInSwitch": true,
22
+ "noUncheckedIndexedAccess": true,
23
+ "noImplicitOverride": true,
24
+
25
+ // Some stricter flags (disabled by default)
26
+ "noUnusedLocals": false,
27
+ "noUnusedParameters": false,
28
+ "noPropertyAccessFromIndexSignature": false
29
+ }
30
+ }