@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 +15 -0
- package/index.ts +41 -0
- package/main.ts +18 -0
- package/package.json +27 -0
- package/src/aiModel.ts +43 -0
- package/src/config.ts +85 -0
- package/src/context/context.ts +35 -0
- package/src/models/modelSchema.ts +81 -0
- package/src/models/models-dev.ts +136 -0
- package/src/providers.ts +59 -0
- package/tsconfig.json +30 -0
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
|
+
) { }
|
package/src/providers.ts
ADDED
|
@@ -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
|
+
}
|