Make text completion provider layers effectful

This commit is contained in:
elpresidank 2026-06-02 05:09:15 -05:00
parent 36f629b341
commit fe4f5777c9
9 changed files with 393 additions and 116 deletions

View file

@ -12,15 +12,15 @@ Verified source roots:
- Effect v4 subtree: `/home/elpresidank/YeeBois/projects/beep-effect2/.repos/effect-v4` - Effect v4 subtree: `/home/elpresidank/YeeBois/projects/beep-effect2/.repos/effect-v4`
- Installed Effect beta used by this workspace: `ts/node_modules/effect` - Installed Effect beta used by this workspace: `ts/node_modules/effect`
Current signal counts from `ts/packages` after the 2026-06-02 client streaming Current signal counts from `ts/packages` after the 2026-06-02 text completion
callback Effect boundary slice: provider effectful layer slice:
| Signal | Count | | Signal | Count |
| --- | ---: | | --- | ---: |
| `Effect.runPromise` | 172 | | `Effect.runPromise` | 172 |
| `Effect.runPromiseWith` | 0 | | `Effect.runPromiseWith` | 0 |
| `Effect.cached` | 0 | | `Effect.cached` | 0 |
| `Layer.succeed` | 18 | | `Layer.succeed` | 12 |
| `Map<` | 88 | | `Map<` | 88 |
| `WebSocket` | 74 | | `WebSocket` | 74 |
| `new Map` | 60 | | `new Map` | 60 |
@ -151,6 +151,12 @@ Notes:
centralizing legacy callback request failures in `runLegacyStreamingRequest`. centralizing legacy callback request failures in `runLegacyStreamingRequest`.
The public callback facades still return/ignore Promises where required, but The public callback facades still return/ignore Promises where required, but
failure mapping now uses `Effect.tryPromise` and `Effect.catch`. failure mapping now uses `Effect.tryPromise` and `Effect.catch`.
- The text completion provider effectful layer slice dropped six
`Layer.succeed` matches by moving OpenAI, OpenAI-compatible, Azure OpenAI,
Claude, Mistral, and Ollama processor layers onto
`makeTextCompletionLayer(makeXProviderEffect(config))`. SDK construction and
config lookup now live in Effect; sync `makeXProvider` exports remain
compatibility facades.
- `Record<string, any>` and `throwLibrarianServiceError` are now clean in - `Record<string, any>` and `throwLibrarianServiceError` are now clean in
`ts/packages`. `ts/packages`.
@ -1065,6 +1071,33 @@ Notes:
- `cd ts && bun run test` - `cd ts && bun run test`
- `git diff --check` - `git diff --check`
### 2026-06-02: Text Completion Provider Effectful Layer Slice
- Status: migrated and root-verified.
- Completed:
- Added shared `makeTextCompletionLayer` for constructing `Llm` from an
effectful `LlmProvider`.
- Added `makeOpenAIProviderEffect`,
`makeOpenAICompatibleProviderEffect`, `makeAzureOpenAIProviderEffect`,
`makeClaudeProviderEffect`, `makeMistralProviderEffect`, and
`makeOllamaProviderEffect`.
- Processor `program.layer` definitions now use `Layer.effect` via the
shared helper instead of constructing providers inside `Layer.succeed`.
- Provider object assembly is split into pure `makeXProviderFromClient`
helpers so Promise-returning provider methods remain external
compatibility facades and do not trigger `effect(runEffectInsideEffect)`.
- Added tests for explicit provider config, shared `Llm` layer provisioning,
and tagged missing-config errors.
- Verification:
- `bunx --bun vitest run src/__tests__/text-completion-providers.test.ts src/__tests__/text-completion-common.test.ts`
- `bun run --cwd ts/packages/flow build`
- `bun run --cwd ts/packages/flow test`
- `cd ts && bun run check:tsgo`
- `cd ts && bun run check`
- `cd ts && bun run build`
- `cd ts && bun run test`
- `git diff --check`
## Subagent Findings To Preserve ## Subagent Findings To Preserve
- MCP/workbench: - MCP/workbench:
@ -1124,7 +1157,9 @@ Notes:
Azure currently use Chat Completions while `@effect/ai-openai` is Responses Azure currently use Chat Completions while `@effect/ai-openai` is Responses
API oriented, and no installed Azure/Mistral/Ollama Effect AI provider is API oriented, and no installed Azure/Mistral/Ollama Effect AI provider is
available. Anthropic is the closest direct provider swap, but must preserve available. Anthropic is the closest direct provider swap, but must preserve
text, token counts, streaming final usage, and rate-limit mapping. text, token counts, streaming final usage, and rate-limit mapping. The
local provider layer-construction cleanup is complete; remaining provider
work is adapter/parity work, not `Layer.succeed` cleanup.
- FalkorDB scoped lifecycle is complete for triples query/store. Use the - FalkorDB scoped lifecycle is complete for triples query/store. Use the
fakeable client/graph factory pattern from that slice for future storage fakeable client/graph factory pattern from that slice for future storage
client tests. client tests.
@ -1138,13 +1173,53 @@ Notes:
## Ranked Findings ## Ranked Findings
### P2: Provider Layer And Effect AI Cleanup ### P0: Broker Backend Effect-Native Runtime
- TrustGraph evidence:
- `ts/packages/base/src/backend/types.ts`
- `ts/packages/base/src/backend/nats.ts`
- `ts/packages/base/src/messaging/runtime.ts`
- `ts/packages/base/src/processor/flow-processor.ts`
- Effect primitives:
- `Layer`, `Scope`, `Stream`, `Schedule`, `Queue`,
`Effect.acquireRelease`, and `Effect.tryPromise`.
- Rewrite shape:
- Introduce an Effect-native broker service/layer with scoped NATS
acquisition and stream/schedule-based consumer loops.
- Keep `PubSubBackend` as the compatibility adapter boundary; Effect native
`PubSub` remains in-process only.
- Tests:
- Fake backend ack/nak/backoff/stop tests, NATS close finalizer tests, and
config-push stream tests.
### P1: Gateway Dispatcher Ownership And Serialization
- TrustGraph evidence:
- `ts/packages/flow/src/gateway/dispatch/manager.ts`
- `ts/packages/flow/src/gateway/dispatch/serialize.ts`
- `ts/packages/flow/src/gateway/server.ts`
- Effect primitives:
- `Layer`, `Scope`, `Effect.acquireUseRelease`, `Effect.try`, `Result.try`,
and typed dispatch errors.
- Rewrite shape:
- Track whether the dispatcher owns `PubSubBackend` so injected backends are
not closed.
- Use `Effect.acquireUseRelease` for one-shot gateway producers so producer
close runs even when send fails.
- Replace throwing gateway serialization helpers with Effect/Result-returning
helpers mapped to typed dispatch or wire errors.
- Longer term, move `createGateway` to a scoped `createGatewayEffect` while
keeping Fastify route `Effect.runPromise` calls as host boundaries.
- Tests:
- Injected pubsub is not closed, one-shot producer closes on send failure,
and malformed gateway payloads return typed dispatch errors.
### P2: Effect AI Provider Adapter Cleanup
- TrustGraph evidence: - TrustGraph evidence:
- `ts/packages/flow/src/model/text-completion/*.ts` - `ts/packages/flow/src/model/text-completion/*.ts`
- Effect primitives: - Effect primitives:
- `Config`, `ConfigProvider`, `Metric`, `Logger`, - `effect/unstable/ai/LanguageModel`, `effect/unstable/ai/EmbeddingModel`,
`effect/unstable/ai/LanguageModel`, `effect/unstable/ai/EmbeddingModel`,
Effect AI OpenAI/Anthropic provider layers. Effect AI OpenAI/Anthropic provider layers.
- Rewrite shape: - Rewrite shape:
- Add an Effect AI adapter layer beside the current `LlmProvider` contract - Add an Effect AI adapter layer beside the current `LlmProvider` contract
@ -1190,8 +1265,10 @@ Notes:
## Recommended PR Order ## Recommended PR Order
1. Provider layer and Effect AI cleanup. 1. Gateway dispatcher ownership and serialization.
2. MCP parity/deletion decision and workbench platform polish. 2. Broker backend Effect-native runtime.
3. Effect AI provider adapter cleanup.
4. MCP parity/deletion decision and workbench platform polish.
## No-Op Rules ## No-Op Rules

View file

@ -0,0 +1,86 @@
import { describe, expect, it } from "@effect/vitest";
import { Llm } from "@trustgraph/base";
import { ConfigProvider, Effect } from "effect";
import { makeAzureOpenAIProviderEffect } from "../model/text-completion/azure-openai.js";
import { makeClaudeProviderEffect } from "../model/text-completion/claude.js";
import { makeTextCompletionLayer } from "../model/text-completion/common.js";
import { makeMistralProviderEffect } from "../model/text-completion/mistral.js";
import { makeOllamaProviderEffect } from "../model/text-completion/ollama.js";
import { makeOpenAICompatibleProviderEffect } from "../model/text-completion/openai-compatible.js";
import { makeOpenAIProviderEffect } from "../model/text-completion/openai.js";
const emptyConfig = ConfigProvider.layer(
ConfigProvider.fromEnv({ env: {} }),
);
describe("text completion provider construction", () => {
it.effect(
"constructs providers from explicit config through Effect",
Effect.fnUntraced(function* () {
const providers = yield* Effect.all([
makeOpenAIProviderEffect({ id: "openai", apiKey: "test-key" }),
makeOpenAICompatibleProviderEffect({
id: "openai-compatible",
baseUrl: "http://localhost:1234/v1",
}),
makeAzureOpenAIProviderEffect({
id: "azure-openai",
apiKey: "test-key",
endpoint: "https://example.openai.azure.com",
}),
makeClaudeProviderEffect({ id: "claude", apiKey: "test-key" }),
makeMistralProviderEffect({ id: "mistral", apiKey: "test-key" }),
makeOllamaProviderEffect({
id: "ollama",
ollamaUrl: "http://localhost:11434",
}),
]).pipe(
Effect.provide(emptyConfig),
);
expect(providers.map((provider) => provider.supportsStreaming())).toEqual([
true,
true,
true,
true,
true,
true,
]);
}),
);
it.effect(
"provides Llm through the shared Effect layer helper",
Effect.fnUntraced(function* () {
const llm = yield* Effect.gen(function* () {
return yield* Llm;
}).pipe(
Effect.provide(
makeTextCompletionLayer(makeOllamaProviderEffect({
id: "ollama",
ollamaUrl: "http://localhost:11434",
})),
),
Effect.provide(emptyConfig),
);
expect(llm.supportsStreaming()).toBe(true);
}),
);
it.effect(
"fails missing required config as a tagged config error",
Effect.fnUntraced(function* () {
const error = yield* makeOpenAIProviderEffect({ id: "openai" }).pipe(
Effect.flip,
Effect.provide(emptyConfig),
);
expect(error).toMatchObject({
_tag: "TextCompletionConfigError",
provider: "OpenAI",
key: "OPENAI_TOKEN",
});
}),
);
});

View file

@ -11,11 +11,10 @@
import { AzureOpenAI } from "openai"; import { AzureOpenAI } from "openai";
import { NodeRuntime } from "@effect/platform-node"; import { NodeRuntime } from "@effect/platform-node";
import { import {
Llm,
makeLlmService, makeLlmService,
makeFlowProcessorProgram, makeFlowProcessorProgram,
makeLlmServiceShape,
makeLlmSpecs, makeLlmSpecs,
type Llm,
type LlmProvider, type LlmProvider,
type ProcessorConfig, type ProcessorConfig,
type LlmResult, type LlmResult,
@ -24,11 +23,13 @@ import {
import { Effect, Layer, ManagedRuntime, Stream } from "effect"; import { Effect, Layer, ManagedRuntime, Stream } from "effect";
import { import {
llmStreamPart, llmStreamPart,
makeTextCompletionLayer,
optionalStringConfig, optionalStringConfig,
providerStatusError, providerStatusError,
requiredString, requiredString,
streamTextCompletionChunks, streamTextCompletionChunks,
toAsyncGenerator, toAsyncGenerator,
type TextCompletionConfigError,
type TextCompletionRuntimeError, type TextCompletionRuntimeError,
} from "./common.ts"; } from "./common.ts";
@ -79,24 +80,21 @@ const loadAzureOpenAIConfig = Effect.fn("loadAzureOpenAIConfig")(function* (
apiKey, apiKey,
endpoint, endpoint,
apiVersion, apiVersion,
}; } satisfies ResolvedAzureOpenAIConfig;
}); });
const mapAzureOpenAIError = (error: unknown): TextCompletionRuntimeError => const mapAzureOpenAIError = (error: unknown): TextCompletionRuntimeError =>
providerStatusError("AzureOpenAI", error); providerStatusError("AzureOpenAI", error);
export function makeAzureOpenAIProvider(config: AzureOpenAIProcessorConfig): LlmProvider { const makeAzureOpenAIProviderFromClient = (
resolved: ResolvedAzureOpenAIConfig,
client: AzureOpenAI,
): LlmProvider => {
const { const {
defaultModel, defaultModel,
defaultTemperature, defaultTemperature,
maxOutput, maxOutput,
apiKey, } = resolved;
endpoint,
apiVersion,
} = Effect.runSync(loadAzureOpenAIConfig(config)) satisfies ResolvedAzureOpenAIConfig;
const client = new AzureOpenAI({ apiKey, apiVersion, endpoint });
Effect.runSync(Effect.log("[AzureOpenAI] LLM service initialized"));
return { return {
generateContent: ( generateContent: (
@ -174,9 +172,31 @@ export function makeAzureOpenAIProvider(config: AzureOpenAIProcessorConfig): Llm
return toAsyncGenerator(Stream.toAsyncIterable(stream), mapAzureOpenAIError); return toAsyncGenerator(Stream.toAsyncIterable(stream), mapAzureOpenAIError);
}, },
}; } satisfies LlmProvider;
};
export function makeAzureOpenAIProvider(config: AzureOpenAIProcessorConfig): LlmProvider {
return Effect.runSync(makeAzureOpenAIProviderEffect(config));
} }
export const makeAzureOpenAIProviderEffect = Effect.fn("makeAzureOpenAIProvider")(function*(
config: AzureOpenAIProcessorConfig,
) {
const resolved = yield* loadAzureOpenAIConfig(config);
const client = yield* Effect.try({
try: () =>
new AzureOpenAI({
apiKey: resolved.apiKey,
apiVersion: resolved.apiVersion,
endpoint: resolved.endpoint,
}),
catch: mapAzureOpenAIError,
});
yield* Effect.log("[AzureOpenAI] LLM service initialized");
return makeAzureOpenAIProviderFromClient(resolved, client);
});
export type AzureOpenAIProcessor = ReturnType<typeof makeAzureOpenAIProcessor>; export type AzureOpenAIProcessor = ReturnType<typeof makeAzureOpenAIProcessor>;
export function makeAzureOpenAIProcessor( export function makeAzureOpenAIProcessor(
@ -187,14 +207,14 @@ export function makeAzureOpenAIProcessor(
export const AzureOpenAIProcessor = makeAzureOpenAIProcessor; export const AzureOpenAIProcessor = makeAzureOpenAIProcessor;
export const program = makeFlowProcessorProgram<ProcessorConfig, never, Llm>({ export const program = makeFlowProcessorProgram<
AzureOpenAIProcessorConfig,
TextCompletionConfigError | TextCompletionRuntimeError,
Llm
>({
id: "text-completion", id: "text-completion",
specs: () => makeLlmSpecs(), specs: () => makeLlmSpecs(),
layer: (config) => layer: (config) => makeTextCompletionLayer(makeAzureOpenAIProviderEffect(config)),
Layer.succeed(
Llm,
Llm.of(makeLlmServiceShape(makeAzureOpenAIProvider(config))),
),
}); });
const azureOpenAITextCompletionRuntime = ManagedRuntime.make(Layer.empty); const azureOpenAITextCompletionRuntime = ManagedRuntime.make(Layer.empty);

View file

@ -7,11 +7,10 @@
import Anthropic from "@anthropic-ai/sdk"; import Anthropic from "@anthropic-ai/sdk";
import { NodeRuntime } from "@effect/platform-node"; import { NodeRuntime } from "@effect/platform-node";
import { import {
Llm,
makeLlmService, makeLlmService,
makeFlowProcessorProgram, makeFlowProcessorProgram,
makeLlmServiceShape,
makeLlmSpecs, makeLlmSpecs,
type Llm,
type LlmProvider, type LlmProvider,
type ProcessorConfig, type ProcessorConfig,
type LlmResult, type LlmResult,
@ -20,11 +19,13 @@ import {
import { Effect, Layer, ManagedRuntime, Stream } from "effect"; import { Effect, Layer, ManagedRuntime, Stream } from "effect";
import { import {
llmStreamPart, llmStreamPart,
makeTextCompletionLayer,
optionalStringConfig, optionalStringConfig,
providerStatusError, providerStatusError,
requiredString, requiredString,
streamTextCompletionChunks, streamTextCompletionChunks,
toAsyncGenerator, toAsyncGenerator,
type TextCompletionConfigError,
type TextCompletionRuntimeError, type TextCompletionRuntimeError,
} from "./common.ts"; } from "./common.ts";
@ -61,17 +62,15 @@ const loadClaudeConfig = Effect.fn("loadClaudeConfig")(function*(config: ClaudeP
const mapClaudeError = (error: unknown): TextCompletionRuntimeError => const mapClaudeError = (error: unknown): TextCompletionRuntimeError =>
providerStatusError("Claude", error); providerStatusError("Claude", error);
export function makeClaudeProvider(config: ClaudeProcessorConfig): LlmProvider { const makeClaudeProviderFromClient = (
resolved: ResolvedClaudeConfig,
client: Anthropic,
): LlmProvider => {
const { const {
defaultModel, defaultModel,
defaultTemperature, defaultTemperature,
maxOutput, maxOutput,
apiKey, } = resolved;
} = Effect.runSync(loadClaudeConfig(config)) satisfies ResolvedClaudeConfig;
const client = new Anthropic({ apiKey });
Effect.runSync(Effect.log("[Claude] LLM service initialized"));
return { return {
generateContent: ( generateContent: (
@ -161,9 +160,26 @@ export function makeClaudeProvider(config: ClaudeProcessorConfig): LlmProvider {
return toAsyncGenerator(Stream.toAsyncIterable(stream), mapClaudeError); return toAsyncGenerator(Stream.toAsyncIterable(stream), mapClaudeError);
}, },
}; } satisfies LlmProvider;
};
export function makeClaudeProvider(config: ClaudeProcessorConfig): LlmProvider {
return Effect.runSync(makeClaudeProviderEffect(config));
} }
export const makeClaudeProviderEffect = Effect.fn("makeClaudeProvider")(function*(
config: ClaudeProcessorConfig,
) {
const resolved = yield* loadClaudeConfig(config);
const client = yield* Effect.try({
try: () => new Anthropic({ apiKey: resolved.apiKey }),
catch: mapClaudeError,
});
yield* Effect.log("[Claude] LLM service initialized");
return makeClaudeProviderFromClient(resolved, client);
});
export type ClaudeProcessor = ReturnType<typeof makeClaudeProcessor>; export type ClaudeProcessor = ReturnType<typeof makeClaudeProcessor>;
export function makeClaudeProcessor( export function makeClaudeProcessor(
@ -174,14 +190,14 @@ export function makeClaudeProcessor(
export const ClaudeProcessor = makeClaudeProcessor; export const ClaudeProcessor = makeClaudeProcessor;
export const program = makeFlowProcessorProgram<ProcessorConfig, never, Llm>({ export const program = makeFlowProcessorProgram<
ClaudeProcessorConfig,
TextCompletionConfigError | TextCompletionRuntimeError,
Llm
>({
id: "text-completion", id: "text-completion",
specs: () => makeLlmSpecs(), specs: () => makeLlmSpecs(),
layer: (config) => layer: (config) => makeTextCompletionLayer(makeClaudeProviderEffect(config)),
Layer.succeed(
Llm,
Llm.of(makeLlmServiceShape(makeClaudeProvider(config))),
),
}); });
const claudeTextCompletionRuntime = ManagedRuntime.make(Layer.empty); const claudeTextCompletionRuntime = ManagedRuntime.make(Layer.empty);

View file

@ -1,9 +1,12 @@
import { import {
Llm,
TooManyRequestsError, TooManyRequestsError,
errorMessage, errorMessage,
makeLlmServiceShape,
type LlmChunk, type LlmChunk,
type LlmProvider,
} from "@trustgraph/base"; } from "@trustgraph/base";
import { Config, Effect, Ref, Result, Stream } from "effect"; import { Config, Effect, Layer, Ref, Result, Stream } from "effect";
import * as O from "effect/Option"; import * as O from "effect/Option";
import * as Predicate from "effect/Predicate"; import * as Predicate from "effect/Predicate";
import * as S from "effect/Schema"; import * as S from "effect/Schema";
@ -29,6 +32,17 @@ export type TextCompletionRuntimeError =
| TextCompletionProviderError | TextCompletionProviderError
| TooManyRequestsError; | TooManyRequestsError;
export const makeTextCompletionLayer = <E, R>(
provider: Effect.Effect<LlmProvider, E, R>,
): Layer.Layer<Llm, E, R> =>
Layer.effect(Llm)(
provider.pipe(
Effect.map((resolvedProvider) =>
Llm.of(makeLlmServiceShape(resolvedProvider))
),
),
);
type StreamingTokenTotals = { type StreamingTokenTotals = {
readonly inToken: number; readonly inToken: number;
readonly outToken: number; readonly outToken: number;

View file

@ -9,11 +9,10 @@
import { Mistral } from "@mistralai/mistralai"; import { Mistral } from "@mistralai/mistralai";
import { NodeRuntime } from "@effect/platform-node"; import { NodeRuntime } from "@effect/platform-node";
import { import {
Llm,
makeLlmService, makeLlmService,
makeFlowProcessorProgram, makeFlowProcessorProgram,
makeLlmServiceShape,
makeLlmSpecs, makeLlmSpecs,
type Llm,
type LlmProvider, type LlmProvider,
type ProcessorConfig, type ProcessorConfig,
type LlmResult, type LlmResult,
@ -22,12 +21,14 @@ import {
import { Effect, Layer, ManagedRuntime, Stream } from "effect"; import { Effect, Layer, ManagedRuntime, Stream } from "effect";
import { import {
llmStreamPart, llmStreamPart,
makeTextCompletionLayer,
optionalStringConfig, optionalStringConfig,
providerStatusError, providerStatusError,
requiredString, requiredString,
streamTextCompletionChunks, streamTextCompletionChunks,
textFromContent, textFromContent,
toAsyncGenerator, toAsyncGenerator,
type TextCompletionConfigError,
type TextCompletionRuntimeError, type TextCompletionRuntimeError,
} from "./common.ts"; } from "./common.ts";
@ -67,17 +68,15 @@ const loadMistralConfig = Effect.fn("loadMistralConfig")(function*(config: Mistr
const mapMistralError = (error: unknown): TextCompletionRuntimeError => const mapMistralError = (error: unknown): TextCompletionRuntimeError =>
providerStatusError("Mistral", error); providerStatusError("Mistral", error);
export function makeMistralProvider(config: MistralProcessorConfig): LlmProvider { const makeMistralProviderFromClient = (
resolved: ResolvedMistralConfig,
client: Mistral,
): LlmProvider => {
const { const {
defaultModel, defaultModel,
defaultTemperature, defaultTemperature,
maxOutput, maxOutput,
apiKey, } = resolved;
} = Effect.runSync(loadMistralConfig(config)) satisfies ResolvedMistralConfig;
const client = new Mistral({ apiKey });
Effect.runSync(Effect.log("[Mistral] LLM service initialized"));
return { return {
generateContent: ( generateContent: (
@ -153,9 +152,26 @@ export function makeMistralProvider(config: MistralProcessorConfig): LlmProvider
return toAsyncGenerator(Stream.toAsyncIterable(stream), mapMistralError); return toAsyncGenerator(Stream.toAsyncIterable(stream), mapMistralError);
}, },
}; } satisfies LlmProvider;
};
export function makeMistralProvider(config: MistralProcessorConfig): LlmProvider {
return Effect.runSync(makeMistralProviderEffect(config));
} }
export const makeMistralProviderEffect = Effect.fn("makeMistralProvider")(function*(
config: MistralProcessorConfig,
) {
const resolved = yield* loadMistralConfig(config);
const client = yield* Effect.try({
try: () => new Mistral({ apiKey: resolved.apiKey }),
catch: mapMistralError,
});
yield* Effect.log("[Mistral] LLM service initialized");
return makeMistralProviderFromClient(resolved, client);
});
export type MistralProcessor = ReturnType<typeof makeMistralProcessor>; export type MistralProcessor = ReturnType<typeof makeMistralProcessor>;
export function makeMistralProcessor( export function makeMistralProcessor(
@ -166,14 +182,14 @@ export function makeMistralProcessor(
export const MistralProcessor = makeMistralProcessor; export const MistralProcessor = makeMistralProcessor;
export const program = makeFlowProcessorProgram<ProcessorConfig, never, Llm>({ export const program = makeFlowProcessorProgram<
MistralProcessorConfig,
TextCompletionConfigError | TextCompletionRuntimeError,
Llm
>({
id: "text-completion", id: "text-completion",
specs: () => makeLlmSpecs(), specs: () => makeLlmSpecs(),
layer: (config) => layer: (config) => makeTextCompletionLayer(makeMistralProviderEffect(config)),
Layer.succeed(
Llm,
Llm.of(makeLlmServiceShape(makeMistralProvider(config))),
),
}); });
const mistralTextCompletionRuntime = ManagedRuntime.make(Layer.empty); const mistralTextCompletionRuntime = ManagedRuntime.make(Layer.empty);

View file

@ -9,11 +9,10 @@
import { Ollama } from "ollama"; import { Ollama } from "ollama";
import { NodeRuntime } from "@effect/platform-node"; import { NodeRuntime } from "@effect/platform-node";
import { import {
Llm,
makeLlmService, makeLlmService,
makeFlowProcessorProgram, makeFlowProcessorProgram,
makeLlmServiceShape,
makeLlmSpecs, makeLlmSpecs,
type Llm,
type LlmProvider, type LlmProvider,
type ProcessorConfig, type ProcessorConfig,
type LlmResult, type LlmResult,
@ -22,10 +21,12 @@ import {
import { Effect, Layer, ManagedRuntime, Stream } from "effect"; import { Effect, Layer, ManagedRuntime, Stream } from "effect";
import { import {
llmStreamPart, llmStreamPart,
makeTextCompletionLayer,
optionalStringConfig, optionalStringConfig,
providerRuntimeError, providerRuntimeError,
streamTextCompletionChunks, streamTextCompletionChunks,
toAsyncGenerator, toAsyncGenerator,
type TextCompletionConfigError,
type TextCompletionRuntimeError, type TextCompletionRuntimeError,
} from "./common.ts"; } from "./common.ts";
@ -55,14 +56,11 @@ const loadOllamaConfig = Effect.fn("loadOllamaConfig")(function*(config: OllamaP
const mapOllamaError = (error: unknown): TextCompletionRuntimeError => const mapOllamaError = (error: unknown): TextCompletionRuntimeError =>
providerRuntimeError("Ollama", error); providerRuntimeError("Ollama", error);
export function makeOllamaProvider(config: OllamaProcessorConfig): LlmProvider { const makeOllamaProviderFromClient = (
const { defaultModel, host } = Effect.runSync(loadOllamaConfig(config)) satisfies ResolvedOllamaConfig; resolved: ResolvedOllamaConfig,
client: Ollama,
const client = new Ollama({ host }); ): LlmProvider => {
const { defaultModel } = resolved;
Effect.runSync(Effect.log(
`[Ollama] LLM service initialized (host=${host}, model=${defaultModel})`,
));
return { return {
generateContent: ( generateContent: (
@ -130,9 +128,28 @@ export function makeOllamaProvider(config: OllamaProcessorConfig): LlmProvider {
return toAsyncGenerator(Stream.toAsyncIterable(stream), mapOllamaError); return toAsyncGenerator(Stream.toAsyncIterable(stream), mapOllamaError);
}, },
}; } satisfies LlmProvider;
};
export function makeOllamaProvider(config: OllamaProcessorConfig): LlmProvider {
return Effect.runSync(makeOllamaProviderEffect(config));
} }
export const makeOllamaProviderEffect = Effect.fn("makeOllamaProvider")(function*(
config: OllamaProcessorConfig,
) {
const resolved = yield* loadOllamaConfig(config);
const client = yield* Effect.try({
try: () => new Ollama({ host: resolved.host }),
catch: mapOllamaError,
});
yield* Effect.log(
`[Ollama] LLM service initialized (host=${resolved.host}, model=${resolved.defaultModel})`,
);
return makeOllamaProviderFromClient(resolved, client);
});
export type OllamaProcessor = ReturnType<typeof makeOllamaProcessor>; export type OllamaProcessor = ReturnType<typeof makeOllamaProcessor>;
export function makeOllamaProcessor( export function makeOllamaProcessor(
@ -143,14 +160,14 @@ export function makeOllamaProcessor(
export const OllamaProcessor = makeOllamaProcessor; export const OllamaProcessor = makeOllamaProcessor;
export const program = makeFlowProcessorProgram<ProcessorConfig, never, Llm>({ export const program = makeFlowProcessorProgram<
OllamaProcessorConfig,
TextCompletionConfigError | TextCompletionRuntimeError,
Llm
>({
id: "text-completion", id: "text-completion",
specs: () => makeLlmSpecs(), specs: () => makeLlmSpecs(),
layer: (config) => layer: (config) => makeTextCompletionLayer(makeOllamaProviderEffect(config)),
Layer.succeed(
Llm,
Llm.of(makeLlmServiceShape(makeOllamaProvider(config))),
),
}); });
const ollamaTextCompletionRuntime = ManagedRuntime.make(Layer.empty); const ollamaTextCompletionRuntime = ManagedRuntime.make(Layer.empty);

View file

@ -12,11 +12,10 @@
import OpenAI from "openai"; import OpenAI from "openai";
import { NodeRuntime } from "@effect/platform-node"; import { NodeRuntime } from "@effect/platform-node";
import { import {
Llm,
makeLlmService, makeLlmService,
makeFlowProcessorProgram, makeFlowProcessorProgram,
makeLlmServiceShape,
makeLlmSpecs, makeLlmSpecs,
type Llm,
type LlmProvider, type LlmProvider,
type ProcessorConfig, type ProcessorConfig,
type LlmResult, type LlmResult,
@ -25,11 +24,13 @@ import {
import { Effect, Layer, ManagedRuntime, Stream } from "effect"; import { Effect, Layer, ManagedRuntime, Stream } from "effect";
import { import {
llmStreamPart, llmStreamPart,
makeTextCompletionLayer,
optionalStringConfig, optionalStringConfig,
providerStatusError, providerStatusError,
requiredString, requiredString,
streamTextCompletionChunks, streamTextCompletionChunks,
toAsyncGenerator, toAsyncGenerator,
type TextCompletionConfigError,
type TextCompletionRuntimeError, type TextCompletionRuntimeError,
} from "./common.ts"; } from "./common.ts";
@ -75,20 +76,15 @@ const loadOpenAICompatibleConfig = Effect.fn("loadOpenAICompatibleConfig")(funct
const mapOpenAICompatibleError = (error: unknown): TextCompletionRuntimeError => const mapOpenAICompatibleError = (error: unknown): TextCompletionRuntimeError =>
providerStatusError("OpenAI-Compatible", error); providerStatusError("OpenAI-Compatible", error);
export function makeOpenAICompatibleProvider( const makeOpenAICompatibleProviderFromClient = (
config: OpenAICompatibleProcessorConfig, resolved: ResolvedOpenAICompatibleConfig,
): LlmProvider { client: OpenAI,
): LlmProvider => {
const { const {
defaultModel, defaultModel,
defaultTemperature, defaultTemperature,
maxOutput, maxOutput,
apiKey, } = resolved;
baseURL,
} = Effect.runSync(loadOpenAICompatibleConfig(config)) satisfies ResolvedOpenAICompatibleConfig;
const client = new OpenAI({ baseURL, apiKey });
Effect.runSync(Effect.log("[OpenAI-Compatible] LLM service initialized"));
return { return {
generateContent: ( generateContent: (
@ -165,9 +161,28 @@ export function makeOpenAICompatibleProvider(
return toAsyncGenerator(Stream.toAsyncIterable(stream), mapOpenAICompatibleError); return toAsyncGenerator(Stream.toAsyncIterable(stream), mapOpenAICompatibleError);
}, },
}; } satisfies LlmProvider;
};
export function makeOpenAICompatibleProvider(
config: OpenAICompatibleProcessorConfig,
): LlmProvider {
return Effect.runSync(makeOpenAICompatibleProviderEffect(config));
} }
export const makeOpenAICompatibleProviderEffect = Effect.fn("makeOpenAICompatibleProvider")(function*(
config: OpenAICompatibleProcessorConfig,
) {
const resolved = yield* loadOpenAICompatibleConfig(config);
const client = yield* Effect.try({
try: () => new OpenAI({ baseURL: resolved.baseURL, apiKey: resolved.apiKey }),
catch: mapOpenAICompatibleError,
});
yield* Effect.log("[OpenAI-Compatible] LLM service initialized");
return makeOpenAICompatibleProviderFromClient(resolved, client);
});
export type OpenAICompatibleProcessor = ReturnType<typeof makeOpenAICompatibleProcessor>; export type OpenAICompatibleProcessor = ReturnType<typeof makeOpenAICompatibleProcessor>;
export function makeOpenAICompatibleProcessor( export function makeOpenAICompatibleProcessor(
@ -178,14 +193,14 @@ export function makeOpenAICompatibleProcessor(
export const OpenAICompatibleProcessor = makeOpenAICompatibleProcessor; export const OpenAICompatibleProcessor = makeOpenAICompatibleProcessor;
export const program = makeFlowProcessorProgram<ProcessorConfig, never, Llm>({ export const program = makeFlowProcessorProgram<
OpenAICompatibleProcessorConfig,
TextCompletionConfigError | TextCompletionRuntimeError,
Llm
>({
id: "text-completion", id: "text-completion",
specs: () => makeLlmSpecs(), specs: () => makeLlmSpecs(),
layer: (config) => layer: (config) => makeTextCompletionLayer(makeOpenAICompatibleProviderEffect(config)),
Layer.succeed(
Llm,
Llm.of(makeLlmServiceShape(makeOpenAICompatibleProvider(config))),
),
}); });
const openAICompatibleTextCompletionRuntime = ManagedRuntime.make(Layer.empty); const openAICompatibleTextCompletionRuntime = ManagedRuntime.make(Layer.empty);

View file

@ -7,11 +7,10 @@
import OpenAI from "openai"; import OpenAI from "openai";
import { NodeRuntime } from "@effect/platform-node"; import { NodeRuntime } from "@effect/platform-node";
import { import {
Llm,
makeLlmService, makeLlmService,
makeFlowProcessorProgram, makeFlowProcessorProgram,
makeLlmServiceShape,
makeLlmSpecs, makeLlmSpecs,
type Llm,
type LlmProvider, type LlmProvider,
type ProcessorConfig, type ProcessorConfig,
type LlmResult, type LlmResult,
@ -20,11 +19,13 @@ import {
import { Effect, Layer, ManagedRuntime, Stream } from "effect"; import { Effect, Layer, ManagedRuntime, Stream } from "effect";
import { import {
llmStreamPart, llmStreamPart,
makeTextCompletionLayer,
optionalStringConfig, optionalStringConfig,
providerStatusError, providerStatusError,
requiredString, requiredString,
streamTextCompletionChunks, streamTextCompletionChunks,
toAsyncGenerator, toAsyncGenerator,
type TextCompletionConfigError,
type TextCompletionRuntimeError, type TextCompletionRuntimeError,
} from "./common.ts"; } from "./common.ts";
@ -64,21 +65,15 @@ const loadOpenAIConfig = Effect.fn("loadOpenAIConfig")(function*(config: OpenAIP
const mapOpenAIError = (error: unknown): TextCompletionRuntimeError => const mapOpenAIError = (error: unknown): TextCompletionRuntimeError =>
providerStatusError("OpenAI", error); providerStatusError("OpenAI", error);
export function makeOpenAIProvider(config: OpenAIProcessorConfig): LlmProvider { const makeOpenAIProviderFromClient = (
resolved: ResolvedOpenAIConfig,
client: OpenAI,
): LlmProvider => {
const { const {
defaultModel, defaultModel,
defaultTemperature, defaultTemperature,
maxOutput, maxOutput,
apiKey, } = resolved;
baseURL,
} = Effect.runSync(loadOpenAIConfig(config)) satisfies ResolvedOpenAIConfig;
const client = new OpenAI({
apiKey,
baseURL,
});
Effect.runSync(Effect.log("[OpenAI] LLM service initialized"));
return { return {
generateContent: ( generateContent: (
@ -156,9 +151,30 @@ export function makeOpenAIProvider(config: OpenAIProcessorConfig): LlmProvider {
return toAsyncGenerator(Stream.toAsyncIterable(stream), mapOpenAIError); return toAsyncGenerator(Stream.toAsyncIterable(stream), mapOpenAIError);
}, },
}; } satisfies LlmProvider;
};
export function makeOpenAIProvider(config: OpenAIProcessorConfig): LlmProvider {
return Effect.runSync(makeOpenAIProviderEffect(config));
} }
export const makeOpenAIProviderEffect = Effect.fn("makeOpenAIProvider")(function*(
config: OpenAIProcessorConfig,
) {
const resolved = yield* loadOpenAIConfig(config);
const client = yield* Effect.try({
try: () =>
new OpenAI({
apiKey: resolved.apiKey,
baseURL: resolved.baseURL,
}),
catch: mapOpenAIError,
});
yield* Effect.log("[OpenAI] LLM service initialized");
return makeOpenAIProviderFromClient(resolved, client);
});
export type OpenAIProcessor = ReturnType<typeof makeOpenAIProcessor>; export type OpenAIProcessor = ReturnType<typeof makeOpenAIProcessor>;
export function makeOpenAIProcessor( export function makeOpenAIProcessor(
@ -169,14 +185,14 @@ export function makeOpenAIProcessor(
export const OpenAIProcessor = makeOpenAIProcessor; export const OpenAIProcessor = makeOpenAIProcessor;
export const program = makeFlowProcessorProgram<ProcessorConfig, never, Llm>({ export const program = makeFlowProcessorProgram<
OpenAIProcessorConfig,
TextCompletionConfigError | TextCompletionRuntimeError,
Llm
>({
id: "text-completion", id: "text-completion",
specs: () => makeLlmSpecs(), specs: () => makeLlmSpecs(),
layer: (config) => layer: (config) => makeTextCompletionLayer(makeOpenAIProviderEffect(config)),
Layer.succeed(
Llm,
Llm.of(makeLlmServiceShape(makeOpenAIProvider(config))),
),
}); });
const openAITextCompletionRuntime = ManagedRuntime.make(Layer.empty); const openAITextCompletionRuntime = ManagedRuntime.make(Layer.empty);