opencode-effect-enforcer 0.2.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/LICENSE +21 -0
- package/README.md +278 -0
- package/guidance/effect-first-development.md +1247 -0
- package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
- package/guidance/post__parse-dont-validate.md +109 -0
- package/guidance/progressive-disclosure-guidance.md +38 -0
- package/package.json +63 -0
- package/patterns/avoid-any.md +37 -0
- package/patterns/avoid-data-tagged-error.md +34 -0
- package/patterns/avoid-direct-json.md +51 -0
- package/patterns/avoid-direct-tag-checks.md +54 -0
- package/patterns/avoid-expect-in-if.md +52 -0
- package/patterns/avoid-mutable-state.md +70 -0
- package/patterns/avoid-native-fetch.md +61 -0
- package/patterns/avoid-node-imports.md +86 -0
- package/patterns/avoid-non-null-assertion.md +44 -0
- package/patterns/avoid-object-type.md +46 -0
- package/patterns/avoid-option-getorthrow.md +39 -0
- package/patterns/avoid-platform-coupling.md +43 -0
- package/patterns/avoid-process-env.md +43 -0
- package/patterns/avoid-react-hooks.md +73 -0
- package/patterns/avoid-schema-suffix.md +45 -0
- package/patterns/avoid-sync-fs.md +68 -0
- package/patterns/avoid-try-catch.md +47 -0
- package/patterns/avoid-ts-ignore.md +38 -0
- package/patterns/avoid-untagged-errors.md +67 -0
- package/patterns/avoid-yield-ref.md +46 -0
- package/patterns/casting-awareness.md +46 -0
- package/patterns/context-tag-extends.md +84 -0
- package/patterns/effect-catchall-default.md +61 -0
- package/patterns/effect-promise-vs-trypromise.md +47 -0
- package/patterns/effect-run-in-body.md +58 -0
- package/patterns/imperative-loops.md +76 -0
- package/patterns/prefer-arr-sort.md +52 -0
- package/patterns/prefer-duration-values.md +56 -0
- package/patterns/prefer-effect-fn.md +161 -0
- package/patterns/prefer-match-over-switch.md +48 -0
- package/patterns/prefer-option-over-null.md +56 -0
- package/patterns/prefer-redacted-config.md +70 -0
- package/patterns/prefer-schema-class.md +54 -0
- package/patterns/require-effect-concurrency.md +83 -0
- package/patterns/stream-large-files.md +63 -0
- package/patterns/throw-in-effect-gen.md +62 -0
- package/patterns/use-clock-service.md +45 -0
- package/patterns/use-command-executor-service.md +54 -0
- package/patterns/use-console-service.md +54 -0
- package/patterns/use-filesystem-service.md +59 -0
- package/patterns/use-http-client-service.md +77 -0
- package/patterns/use-path-service.md +53 -0
- package/patterns/use-random-service.md +45 -0
- package/patterns/use-temp-file-scoped.md +66 -0
- package/patterns/vm-in-wrong-file.md +51 -0
- package/patterns/yield-in-for-loop.md +61 -0
- package/skills/effect-ai-chat/SKILL.md +472 -0
- package/skills/effect-ai-language-model/SKILL.md +652 -0
- package/skills/effect-ai-prompt/SKILL.md +752 -0
- package/skills/effect-ai-provider/SKILL.md +668 -0
- package/skills/effect-ai-streaming/SKILL.md +418 -0
- package/skills/effect-ai-tool/SKILL.md +1132 -0
- package/skills/effect-atom-rpc/SKILL.md +488 -0
- package/skills/effect-atom-state/SKILL.md +640 -0
- package/skills/effect-batching/SKILL.md +614 -0
- package/skills/effect-cache/SKILL.md +570 -0
- package/skills/effect-cli/SKILL.md +523 -0
- package/skills/effect-command-executor/SKILL.md +675 -0
- package/skills/effect-concurrency-testing/SKILL.md +612 -0
- package/skills/effect-config/SKILL.md +580 -0
- package/skills/effect-context-witness/SKILL.md +274 -0
- package/skills/effect-domain-modeling/SKILL.md +1212 -0
- package/skills/effect-domain-predicates/SKILL.md +867 -0
- package/skills/effect-error-handling/SKILL.md +1581 -0
- package/skills/effect-fiber/SKILL.md +731 -0
- package/skills/effect-filesystem/SKILL.md +624 -0
- package/skills/effect-graph/SKILL.md +571 -0
- package/skills/effect-http-api/SKILL.md +1760 -0
- package/skills/effect-http-client/SKILL.md +989 -0
- package/skills/effect-http-server/SKILL.md +920 -0
- package/skills/effect-incremental-migration/SKILL.md +362 -0
- package/skills/effect-layer-design/SKILL.md +642 -0
- package/skills/effect-managed-runtime/SKILL.md +395 -0
- package/skills/effect-mcp-server/SKILL.md +608 -0
- package/skills/effect-observability/SKILL.md +719 -0
- package/skills/effect-optics/SKILL.md +554 -0
- package/skills/effect-parallelization/SKILL.md +668 -0
- package/skills/effect-path/SKILL.md +296 -0
- package/skills/effect-pattern-matching/SKILL.md +914 -0
- package/skills/effect-platform-abstraction/SKILL.md +1175 -0
- package/skills/effect-platform-layers/SKILL.md +514 -0
- package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
- package/skills/effect-react-composition/SKILL.md +986 -0
- package/skills/effect-react-vm/SKILL.md +675 -0
- package/skills/effect-rpc-api/SKILL.md +624 -0
- package/skills/effect-rpc-client/SKILL.md +666 -0
- package/skills/effect-rpc-cluster/SKILL.md +1623 -0
- package/skills/effect-rpc-server/SKILL.md +767 -0
- package/skills/effect-scheduling/SKILL.md +124 -0
- package/skills/effect-schema-composition/SKILL.md +975 -0
- package/skills/effect-schema-v4/SKILL.md +691 -0
- package/skills/effect-scope/SKILL.md +682 -0
- package/skills/effect-service-implementation/SKILL.md +656 -0
- package/skills/effect-socket/SKILL.md +703 -0
- package/skills/effect-sql/SKILL.md +781 -0
- package/skills/effect-stream/SKILL.md +765 -0
- package/skills/effect-testing/SKILL.md +1331 -0
- package/skills/effect-typeclass-design/SKILL.md +161 -0
- package/skills/effect-wide-events/Article.md +66 -0
- package/skills/effect-wide-events/SKILL.md +95 -0
- package/skills/effect-workflow/SKILL.md +810 -0
- package/src/agent-policy.ts +22 -0
- package/src/enforcer.ts +104 -0
- package/src/frontmatter.ts +34 -0
- package/src/guidance.ts +66 -0
- package/src/index.ts +38 -0
- package/src/pattern-catalog.ts +115 -0
- package/src/pattern-matcher.ts +178 -0
- package/src/pattern.ts +97 -0
- package/src/skills.ts +29 -0
- package/src/write-projection.ts +66 -0
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: effect-ai-provider
|
|
3
|
+
description: Configure and compose AI provider layers using @effect/ai packages. Covers Anthropic, OpenAI, OpenAI-Compat, and OpenRouter providers with config management, model abstraction, ExecutionPlan fallback, and runtime overrides for language model integration.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Effect AI Provider
|
|
7
|
+
|
|
8
|
+
Configure AI provider layers for language model integration using Effect's AI ecosystem.
|
|
9
|
+
|
|
10
|
+
## When to Use This Skill
|
|
11
|
+
|
|
12
|
+
Use this skill when:
|
|
13
|
+
|
|
14
|
+
- Integrating AI language models (Anthropic, OpenAI, OpenRouter, etc.) into Effect applications
|
|
15
|
+
- Setting up multi-provider AI architectures with ExecutionPlan fallback
|
|
16
|
+
- Implementing stateful chat conversations with context history
|
|
17
|
+
- Managing AI provider configuration and API keys securely
|
|
18
|
+
- Composing AI capabilities with other Effect services
|
|
19
|
+
|
|
20
|
+
## Import Patterns
|
|
21
|
+
|
|
22
|
+
**CRITICAL**: Always use namespace imports. Use `{ }` destructured imports for the `effect` package barrel exports.
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
// From the "effect" barrel — destructured
|
|
26
|
+
import {
|
|
27
|
+
Config,
|
|
28
|
+
Effect,
|
|
29
|
+
ExecutionPlan,
|
|
30
|
+
Layer,
|
|
31
|
+
Ref,
|
|
32
|
+
Schema,
|
|
33
|
+
Context,
|
|
34
|
+
Stream
|
|
35
|
+
} from 'effect';
|
|
36
|
+
|
|
37
|
+
// From "effect/unstable/ai" — namespace imports
|
|
38
|
+
import {
|
|
39
|
+
AiError,
|
|
40
|
+
Chat,
|
|
41
|
+
LanguageModel,
|
|
42
|
+
Model,
|
|
43
|
+
Prompt,
|
|
44
|
+
Tool,
|
|
45
|
+
Toolkit
|
|
46
|
+
} from 'effect/unstable/ai';
|
|
47
|
+
// Or individually:
|
|
48
|
+
import * as LanguageModel from 'effect/unstable/ai/LanguageModel';
|
|
49
|
+
import * as Chat from 'effect/unstable/ai/Chat';
|
|
50
|
+
import * as Model from 'effect/unstable/ai/Model';
|
|
51
|
+
import * as Prompt from 'effect/unstable/ai/Prompt';
|
|
52
|
+
import * as AiError from 'effect/unstable/ai/AiError';
|
|
53
|
+
|
|
54
|
+
// Anthropic
|
|
55
|
+
import { AnthropicClient, AnthropicLanguageModel } from '@effect/ai-anthropic';
|
|
56
|
+
|
|
57
|
+
// OpenAI
|
|
58
|
+
import {
|
|
59
|
+
OpenAiClient,
|
|
60
|
+
OpenAiClientGenerated,
|
|
61
|
+
OpenAiLanguageModel,
|
|
62
|
+
OpenAiSchema,
|
|
63
|
+
OpenAiTool
|
|
64
|
+
} from '@effect/ai-openai';
|
|
65
|
+
|
|
66
|
+
// OpenRouter
|
|
67
|
+
import {
|
|
68
|
+
OpenRouterClient,
|
|
69
|
+
OpenRouterLanguageModel
|
|
70
|
+
} from '@effect/ai-openrouter';
|
|
71
|
+
|
|
72
|
+
// HTTP client (required by all providers)
|
|
73
|
+
import { FetchHttpClient } from 'effect/unstable/http';
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Provider Layer Pattern
|
|
77
|
+
|
|
78
|
+
Every provider exposes two constructors:
|
|
79
|
+
|
|
80
|
+
- **`model(modelId, config?)`** — returns a `Model.Model` (preferred for `ExecutionPlan` and `Effect.provide`)
|
|
81
|
+
- **`layer({ model, config? })`** — returns a raw `Layer<LanguageModel.LanguageModel, never, Client>`
|
|
82
|
+
|
|
83
|
+
```haskell
|
|
84
|
+
-- Model constructor (preferred)
|
|
85
|
+
ProviderLanguageModel.model :: (modelId, config?) → Model.Model<providerName, LanguageModel, Client>
|
|
86
|
+
|
|
87
|
+
-- Layer constructor
|
|
88
|
+
ProviderLanguageModel.layer :: { model, config? } → Layer LanguageModel Client
|
|
89
|
+
|
|
90
|
+
-- Client layer
|
|
91
|
+
ProviderClient.layerConfig :: { apiKey } → Layer Client HttpClient
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Anthropic Provider
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { AnthropicClient, AnthropicLanguageModel } from '@effect/ai-anthropic';
|
|
98
|
+
import { Config, Layer } from 'effect';
|
|
99
|
+
import { FetchHttpClient } from 'effect/unstable/http';
|
|
100
|
+
|
|
101
|
+
// Client layer (reusable across models)
|
|
102
|
+
const AnthropicClientLayer = AnthropicClient.layerConfig({
|
|
103
|
+
apiKey: Config.redacted('ANTHROPIC_API_KEY')
|
|
104
|
+
}).pipe(Layer.provide(FetchHttpClient.layer));
|
|
105
|
+
|
|
106
|
+
// Option A: model() — returns Model.Model (preferred)
|
|
107
|
+
const claudeModel = AnthropicLanguageModel.model('claude-opus-4-6');
|
|
108
|
+
// Use with: Effect.provide(claudeModel) or in ExecutionPlan
|
|
109
|
+
|
|
110
|
+
// Option B: layer() — returns raw Layer<LanguageModel>
|
|
111
|
+
const AnthropicLive = AnthropicLanguageModel.layer({
|
|
112
|
+
model: 'claude-sonnet-4-20250514'
|
|
113
|
+
}).pipe(Layer.provide(AnthropicClientLayer));
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Anthropic capability detection preserves the lower output limits and structured-output support of known legacy Claude models. Unknown and newly released model identifiers default to modern capabilities: native structured outputs and `128_000` output tokens. Override capability detection with `structuredOutputs: false` (or `true`) when a model or compatible endpoint differs from that default; set `max_tokens` separately when the provider's output limit differs.
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
const compatibleClaude = AnthropicLanguageModel.model('future-claude-model', {
|
|
120
|
+
structuredOutputs: false,
|
|
121
|
+
max_tokens: 8192
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## OpenAI Provider
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import { OpenAiClient, OpenAiLanguageModel } from '@effect/ai-openai';
|
|
129
|
+
import { Config, Layer } from 'effect';
|
|
130
|
+
import { FetchHttpClient } from 'effect/unstable/http';
|
|
131
|
+
|
|
132
|
+
const OpenAiClientLayer = OpenAiClient.layerConfig({
|
|
133
|
+
apiKey: Config.redacted('OPENAI_API_KEY')
|
|
134
|
+
}).pipe(Layer.provide(FetchHttpClient.layer));
|
|
135
|
+
|
|
136
|
+
// model() constructor (preferred)
|
|
137
|
+
const gptModel = OpenAiLanguageModel.model('gpt-5.2');
|
|
138
|
+
|
|
139
|
+
// layer() constructor
|
|
140
|
+
const OpenAiLive = OpenAiLanguageModel.layer({
|
|
141
|
+
model: 'gpt-4.1'
|
|
142
|
+
}).pipe(Layer.provide(OpenAiClientLayer));
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Public OpenAI modules:
|
|
146
|
+
|
|
147
|
+
- `OpenAiSchema` — typed Responses API request/response and SSE event schemas
|
|
148
|
+
- `OpenAiClient` — handwritten service with `createResponse`, `createResponseStream`, and `createEmbedding`
|
|
149
|
+
- `OpenAiClientGenerated` — generated direct endpoint access when you need raw OpenAI API coverage
|
|
150
|
+
- `OpenAiTool` — OpenAI provider-defined tools for native capabilities
|
|
151
|
+
|
|
152
|
+
`OpenAiSchema.ResponseStreamEvent` accepts both flat OpenAI error events and compatible-provider events with details nested under `error`, normalizing both to the same decoded error event shape.
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
const client = yield* OpenAiClient.OpenAiClient;
|
|
156
|
+
const [body] = yield* client.createResponse({
|
|
157
|
+
model: 'gpt-4.1',
|
|
158
|
+
input: 'Say hello'
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### OpenAI Provider-Defined Tools
|
|
163
|
+
|
|
164
|
+
Use `OpenAiTool` for OpenAI-native tools instead of hand-rolling provider-defined descriptors:
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
const NativeTools = Toolkit.make(
|
|
168
|
+
OpenAiTool.WebSearch({}),
|
|
169
|
+
OpenAiTool.FileSearch({ vector_store_ids: ['vs_123'] }),
|
|
170
|
+
OpenAiTool.Mcp({
|
|
171
|
+
server_label: 'docs',
|
|
172
|
+
server_url: 'https://mcp.example.com/mcp'
|
|
173
|
+
})
|
|
174
|
+
);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Available hosted/provider tools include `WebSearch`, `CodeInterpreter`, `FileSearch`, `ImageGeneration`, and `Mcp` (`customName: "OpenAiMcp"`). MCP tool approval requests/results use this canonical `OpenAiMcp` name and the normal Effect AI approval request/response parts. Handler-required local tools such as `Shell`, `LocalShell`, and `ApplyPatch` run in your environment; provide handlers only behind explicit sandboxing, authorization, and audit policy.
|
|
178
|
+
|
|
179
|
+
## OpenAI-Compatible Providers
|
|
180
|
+
|
|
181
|
+
Use `apiUrl` with `@effect/ai-openai` for OpenAI-compatible APIs (Azure OpenAI, local models, etc.):
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { OpenAiClient, OpenAiConfig } from '@effect/ai-openai';
|
|
185
|
+
import { Config, Layer } from 'effect';
|
|
186
|
+
import { FetchHttpClient, HttpClient, HttpClientRequest } from 'effect/unstable/http';
|
|
187
|
+
|
|
188
|
+
const CompatibleClientLayer = OpenAiClient.layerConfig({
|
|
189
|
+
apiKey: Config.redacted('OPENAI_COMPAT_API_KEY'),
|
|
190
|
+
apiUrl: Config.succeed('https://my-provider.example.com/v1')
|
|
191
|
+
}).pipe(Layer.provide(FetchHttpClient.layer));
|
|
192
|
+
|
|
193
|
+
// Keep withClientTransform for middleware/proxy/tracing/header transforms.
|
|
194
|
+
const withAuditHeader = OpenAiConfig.withClientTransform(
|
|
195
|
+
HttpClient.mapRequest(HttpClientRequest.setHeader('x-audit-source', 'writer'))
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const program = myEffect.pipe(withAuditHeader);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## OpenRouter Provider
|
|
202
|
+
|
|
203
|
+
Multi-provider access through unified interface:
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
import {
|
|
207
|
+
OpenRouterClient,
|
|
208
|
+
OpenRouterLanguageModel
|
|
209
|
+
} from '@effect/ai-openrouter';
|
|
210
|
+
import { Config, Layer } from 'effect';
|
|
211
|
+
import { FetchHttpClient } from 'effect/unstable/http';
|
|
212
|
+
|
|
213
|
+
const OpenRouterClientLayer = OpenRouterClient.layerConfig({
|
|
214
|
+
apiKey: Config.redacted('OPENROUTER_API_KEY')
|
|
215
|
+
}).pipe(Layer.provide(FetchHttpClient.layer));
|
|
216
|
+
|
|
217
|
+
// model() constructor — use provider-prefixed model IDs
|
|
218
|
+
const routerModel = OpenRouterLanguageModel.model('anthropic/claude-sonnet-4');
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## ExecutionPlan (Multi-Provider Fallback)
|
|
222
|
+
|
|
223
|
+
`ExecutionPlan` defines a strategy for trying multiple providers with different configurations and retry counts:
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
import { AnthropicClient, AnthropicLanguageModel } from '@effect/ai-anthropic';
|
|
227
|
+
import { OpenAiClient, OpenAiLanguageModel } from '@effect/ai-openai';
|
|
228
|
+
import { Effect, ExecutionPlan, Layer } from 'effect';
|
|
229
|
+
import { LanguageModel } from 'effect/unstable/ai';
|
|
230
|
+
|
|
231
|
+
// Try cheaper model first, fall back to more expensive one
|
|
232
|
+
const DraftPlan = ExecutionPlan.make(
|
|
233
|
+
{
|
|
234
|
+
provide: OpenAiLanguageModel.model('gpt-5.2'),
|
|
235
|
+
attempts: 3 // retry up to 3 times before falling back
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
provide: AnthropicLanguageModel.model('claude-opus-4-6'),
|
|
239
|
+
attempts: 2
|
|
240
|
+
}
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
// Inside a Layer.effect, call captureRequirements to capture current services
|
|
244
|
+
const draftsModel = yield* DraftPlan.captureRequirements;
|
|
245
|
+
// This satisfies the plan's client requirements from the current context
|
|
246
|
+
|
|
247
|
+
// Apply the plan to an effect
|
|
248
|
+
const result = yield* myEffect.pipe(Effect.withExecutionPlan(draftsModel));
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Observing ExecutionPlan Lifecycle
|
|
252
|
+
|
|
253
|
+
`Effect.withExecutionPlan` and `Stream.withExecutionPlan` accept an optional `onEvent` observer. Events are the `ExecutionPlan.Event` tagged union: `AttemptStart`, `AttemptSuccess`, and `AttemptFailure`.
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
const result = yield* myEffect.pipe(
|
|
257
|
+
Effect.withExecutionPlan(draftsModel, {
|
|
258
|
+
onEvent: (event) =>
|
|
259
|
+
Effect.logInfo('AI provider attempt').pipe(
|
|
260
|
+
Effect.annotateLogs({
|
|
261
|
+
event: event._tag,
|
|
262
|
+
attempt: event.attempt,
|
|
263
|
+
stepAttempt: event.stepAttempt,
|
|
264
|
+
stepIndex: event.stepIndex
|
|
265
|
+
})
|
|
266
|
+
})
|
|
267
|
+
);
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
- `attempt` is cumulative and 1-based across the plan; `stepAttempt` is 1-based within a step; `stepIndex` is 0-based.
|
|
271
|
+
- Success and failure events include attempt `duration`; failure includes the full `Cause`, including defects and interruption.
|
|
272
|
+
- Every start is paired with one terminal event, including interruption. Observers are awaited in order, should stay cheap, and their defects are ignored so telemetry cannot change the attempt outcome.
|
|
273
|
+
- For `Stream.withExecutionPlan`, an attempt truncated by downstream cancellation is reported as `AttemptSuccess`; use `preventFallbackOnPartialStream` when mixing partial output with fallback output is unacceptable.
|
|
274
|
+
|
|
275
|
+
## Chat Service (Stateful Conversations)
|
|
276
|
+
|
|
277
|
+
Maintain conversation history with automatic context management:
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
import { Effect, Ref } from 'effect';
|
|
281
|
+
import { Chat, Prompt } from 'effect/unstable/ai';
|
|
282
|
+
|
|
283
|
+
// Create with system prompt
|
|
284
|
+
const session =
|
|
285
|
+
yield*
|
|
286
|
+
Chat.fromPrompt(
|
|
287
|
+
Prompt.empty.pipe(Prompt.setSystem('You are a helpful assistant.'))
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
// Or create empty
|
|
291
|
+
const emptySession = yield* Chat.empty;
|
|
292
|
+
|
|
293
|
+
// Or from raw messages
|
|
294
|
+
const agentSession =
|
|
295
|
+
yield*
|
|
296
|
+
Chat.fromPrompt([
|
|
297
|
+
{ role: 'system', content: 'You are an assistant.' },
|
|
298
|
+
{ role: 'user', content: 'Hello' }
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
// Generate text (history is maintained automatically)
|
|
302
|
+
const response =
|
|
303
|
+
yield*
|
|
304
|
+
session
|
|
305
|
+
.generateText({
|
|
306
|
+
prompt: 'What is Effect?'
|
|
307
|
+
})
|
|
308
|
+
.pipe(Effect.provide(modelLayer));
|
|
309
|
+
|
|
310
|
+
// Access conversation history
|
|
311
|
+
const history = yield* Ref.get(session.history);
|
|
312
|
+
|
|
313
|
+
// Export for persistence
|
|
314
|
+
const json = yield* session.exportJson;
|
|
315
|
+
|
|
316
|
+
// Restore from persisted state
|
|
317
|
+
const restored = yield* Chat.fromJson(json);
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Config Override Pattern
|
|
321
|
+
|
|
322
|
+
Runtime configuration adjustment on a per-effect basis using `withConfigOverride` (dual API):
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
import { AnthropicLanguageModel } from '@effect/ai-anthropic';
|
|
326
|
+
|
|
327
|
+
// Apply overrides to any effect that uses the LanguageModel
|
|
328
|
+
const result =
|
|
329
|
+
yield*
|
|
330
|
+
model.generateText({ prompt: '...' }).pipe(
|
|
331
|
+
AnthropicLanguageModel.withConfigOverride({
|
|
332
|
+
temperature: 0.7,
|
|
333
|
+
max_tokens: 4096
|
|
334
|
+
})
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
// Also available for OpenAI:
|
|
338
|
+
import { OpenAiLanguageModel } from '@effect/ai-openai';
|
|
339
|
+
|
|
340
|
+
const result2 =
|
|
341
|
+
yield*
|
|
342
|
+
model.generateText({ prompt: '...' }).pipe(
|
|
343
|
+
OpenAiLanguageModel.withConfigOverride({
|
|
344
|
+
temperature: 0.9,
|
|
345
|
+
reasoning: { effort: 'medium', summary: 'auto' },
|
|
346
|
+
text: { verbosity: 'low' },
|
|
347
|
+
strictJsonSchema: true,
|
|
348
|
+
fileIdPrefixes: ['file-']
|
|
349
|
+
})
|
|
350
|
+
);
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
`OpenAiLanguageModel.Config` accepts Responses API request fields plus `fileIdPrefixes`, `text.verbosity`, restored `reasoning` config, and `strictJsonSchema`. Do not manually send library-only fields (`fileIdPrefixes`, `strictJsonSchema`) to OpenAI APIs; the language model strips them before request construction.
|
|
354
|
+
|
|
355
|
+
Reasoning effort also accepts `'max'` for OpenAI-compatible providers that expose that level, in addition to the standard OpenAI effort values.
|
|
356
|
+
|
|
357
|
+
OpenAI error classification distinguishes temporary rate limits from exhausted account quota. HTTP 402 responses, and HTTP 429 responses whose code or type is `insufficient_quota` or `billing_insufficient_balance`, become `AiError` values with reason `QuotaExhaustedError`; `error.isRetryable` is `false`, so do not retry them without explicit user action. Ordinary 429 responses remain retryable `RateLimitError` values and preserve retry metadata when available.
|
|
358
|
+
|
|
359
|
+
## Model.make — Model Abstraction
|
|
360
|
+
|
|
361
|
+
Wrap provider layers with metadata. Takes 3 positional arguments: `(providerName, modelId, layer)`:
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
import { Model } from 'effect/unstable/ai';
|
|
365
|
+
|
|
366
|
+
// This is what ProviderLanguageModel.model() calls internally:
|
|
367
|
+
const Claude = Model.make(
|
|
368
|
+
'anthropic', // provider name
|
|
369
|
+
'claude-sonnet-4-20250514', // model identifier
|
|
370
|
+
AnthropicLanguageModel.layer({ model: 'claude-sonnet-4-20250514' })
|
|
371
|
+
);
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
In practice, use the provider's `.model()` shorthand instead of calling `Model.make` directly:
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
// Preferred — equivalent to Model.make("anthropic", "claude-opus-4-6", layer)
|
|
378
|
+
const claudeModel = AnthropicLanguageModel.model('claude-opus-4-6');
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
## Context.Service Pattern
|
|
382
|
+
|
|
383
|
+
Define services using the shape-as-type-parameter pattern:
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
import { Effect, Context, Stream } from 'effect';
|
|
387
|
+
|
|
388
|
+
export class AiWriter extends Context.Service<
|
|
389
|
+
AiWriter,
|
|
390
|
+
{
|
|
391
|
+
draftAnnouncement(
|
|
392
|
+
product: string
|
|
393
|
+
): Effect.Effect<string, AiWriterError>;
|
|
394
|
+
streamHighlights(version: string): Stream.Stream<string, AiWriterError>;
|
|
395
|
+
}
|
|
396
|
+
>()('myapp/AiWriter') {
|
|
397
|
+
static readonly layer = Layer.effect(
|
|
398
|
+
AiWriter,
|
|
399
|
+
Effect.gen(function* () {
|
|
400
|
+
const model = AnthropicLanguageModel.model('claude-opus-4-6');
|
|
401
|
+
const modelLayer = yield* model.captureRequirements;
|
|
402
|
+
|
|
403
|
+
const draftAnnouncement = Effect.fn('AiWriter.draftAnnouncement')(
|
|
404
|
+
function* (product: string) {
|
|
405
|
+
const lm = yield* LanguageModel.LanguageModel;
|
|
406
|
+
const response = yield* lm.generateText({
|
|
407
|
+
prompt: `Write a launch announcement for ${product}`
|
|
408
|
+
});
|
|
409
|
+
return response.text;
|
|
410
|
+
},
|
|
411
|
+
Effect.provide(modelLayer),
|
|
412
|
+
Effect.mapError((e) => AiWriterError.fromAiError(e))
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
return AiWriter.of({ draftAnnouncement, streamHighlights });
|
|
416
|
+
})
|
|
417
|
+
).pipe(Layer.provide(AnthropicClientLayer));
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
## Custom Error Wrapping
|
|
422
|
+
|
|
423
|
+
Wrap `AiError` into domain-specific tagged errors:
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
import { Schema } from 'effect';
|
|
427
|
+
import { AiError } from 'effect/unstable/ai';
|
|
428
|
+
|
|
429
|
+
export class MyAiError extends Schema.TaggedError<MyAiError>()(
|
|
430
|
+
'MyAiError',
|
|
431
|
+
{
|
|
432
|
+
reason: AiError.AiErrorReason
|
|
433
|
+
}
|
|
434
|
+
) {
|
|
435
|
+
static fromAiError(error: AiError.AiError) {
|
|
436
|
+
return new MyAiError({ reason: error.reason });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Usage: Effect.mapError((e) => MyAiError.fromAiError(e))
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
## Available Providers
|
|
444
|
+
|
|
445
|
+
| Package | Provider | Models |
|
|
446
|
+
| ----------------------- | ------------- | ----------------------------------------------- |
|
|
447
|
+
| `@effect/ai-anthropic` | Anthropic | Claude Opus 4, Claude Sonnet 4, etc. |
|
|
448
|
+
| `@effect/ai-openai` | OpenAI | GPT-5, GPT-4.1, o-series, etc. |
|
|
449
|
+
| `@effect/ai-openai` | OpenAI-Compat | Any OpenAI-compatible API via `apiUrl` |
|
|
450
|
+
| `@effect/ai-openrouter` | OpenRouter | Multi-provider proxy (any model ID) |
|
|
451
|
+
|
|
452
|
+
**Note**: There are no `@effect/ai-google` or `@effect/ai-amazon-bedrock` packages. Use OpenRouter to access Google/Bedrock models.
|
|
453
|
+
|
|
454
|
+
## Complete Working Example
|
|
455
|
+
|
|
456
|
+
Full application with ExecutionPlan, Chat, and streaming:
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
import { AnthropicClient, AnthropicLanguageModel } from '@effect/ai-anthropic';
|
|
460
|
+
import { OpenAiClient, OpenAiLanguageModel } from '@effect/ai-openai';
|
|
461
|
+
import {
|
|
462
|
+
Config,
|
|
463
|
+
Effect,
|
|
464
|
+
ExecutionPlan,
|
|
465
|
+
Layer,
|
|
466
|
+
Ref,
|
|
467
|
+
Schema,
|
|
468
|
+
Context,
|
|
469
|
+
Stream
|
|
470
|
+
} from 'effect';
|
|
471
|
+
import {
|
|
472
|
+
AiError,
|
|
473
|
+
Chat,
|
|
474
|
+
LanguageModel,
|
|
475
|
+
Model,
|
|
476
|
+
Prompt,
|
|
477
|
+
type Response
|
|
478
|
+
} from 'effect/unstable/ai';
|
|
479
|
+
import { FetchHttpClient } from 'effect/unstable/http';
|
|
480
|
+
|
|
481
|
+
// ---------------------------------------------------------------------------
|
|
482
|
+
// Provider client layers
|
|
483
|
+
// ---------------------------------------------------------------------------
|
|
484
|
+
|
|
485
|
+
const AnthropicClientLayer = AnthropicClient.layerConfig({
|
|
486
|
+
apiKey: Config.redacted('ANTHROPIC_API_KEY')
|
|
487
|
+
}).pipe(Layer.provide(FetchHttpClient.layer));
|
|
488
|
+
|
|
489
|
+
const OpenAiClientLayer = OpenAiClient.layerConfig({
|
|
490
|
+
apiKey: Config.redacted('OPENAI_API_KEY')
|
|
491
|
+
}).pipe(Layer.provide(FetchHttpClient.layer));
|
|
492
|
+
|
|
493
|
+
// ---------------------------------------------------------------------------
|
|
494
|
+
// ExecutionPlan — try cheap model first, fall back to expensive
|
|
495
|
+
// ---------------------------------------------------------------------------
|
|
496
|
+
|
|
497
|
+
const DraftPlan = ExecutionPlan.make(
|
|
498
|
+
{ provide: OpenAiLanguageModel.model('gpt-5.2'), attempts: 3 },
|
|
499
|
+
{ provide: AnthropicLanguageModel.model('claude-opus-4-6'), attempts: 2 }
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
// ---------------------------------------------------------------------------
|
|
503
|
+
// Custom error type
|
|
504
|
+
// ---------------------------------------------------------------------------
|
|
505
|
+
|
|
506
|
+
export class WriterError extends Schema.TaggedError<WriterError>()(
|
|
507
|
+
'WriterError',
|
|
508
|
+
{
|
|
509
|
+
reason: AiError.AiErrorReason
|
|
510
|
+
}
|
|
511
|
+
) {
|
|
512
|
+
static fromAiError(error: AiError.AiError) {
|
|
513
|
+
return new WriterError({ reason: error.reason });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ---------------------------------------------------------------------------
|
|
518
|
+
// Service definition
|
|
519
|
+
// ---------------------------------------------------------------------------
|
|
520
|
+
|
|
521
|
+
export class AiWriter extends Context.Service<
|
|
522
|
+
AiWriter,
|
|
523
|
+
{
|
|
524
|
+
draft(
|
|
525
|
+
product: string
|
|
526
|
+
): Effect.Effect<{ provider: string; text: string }, WriterError>;
|
|
527
|
+
chat(message: string): Effect.Effect<string, WriterError>;
|
|
528
|
+
streamHighlights(version: string): Stream.Stream<string, WriterError>;
|
|
529
|
+
}
|
|
530
|
+
>()('app/AiWriter') {
|
|
531
|
+
static readonly layer = Layer.effect(
|
|
532
|
+
AiWriter,
|
|
533
|
+
Effect.gen(function* () {
|
|
534
|
+
const draftsModel = yield* DraftPlan.captureRequirements;
|
|
535
|
+
const chatModel = OpenAiLanguageModel.model('gpt-4.1');
|
|
536
|
+
const chatModelLayer = yield* chatModel.captureRequirements;
|
|
537
|
+
|
|
538
|
+
// --- Chat session with history ---
|
|
539
|
+
const session = yield* Chat.fromPrompt(
|
|
540
|
+
Prompt.empty.pipe(
|
|
541
|
+
Prompt.setSystem('You are a helpful writing assistant.')
|
|
542
|
+
)
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
const draft = Effect.fn('AiWriter.draft')(
|
|
546
|
+
function* (product: string) {
|
|
547
|
+
const provider = yield* Model.ProviderName;
|
|
548
|
+
const lm = yield* LanguageModel.LanguageModel;
|
|
549
|
+
const response = yield* lm.generateText({
|
|
550
|
+
prompt: `Write a launch announcement for ${product}.`
|
|
551
|
+
});
|
|
552
|
+
return { provider, text: response.text };
|
|
553
|
+
},
|
|
554
|
+
Effect.withExecutionPlan(draftsModel),
|
|
555
|
+
Effect.mapError((e) => WriterError.fromAiError(e))
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
const chat = Effect.fn('AiWriter.chat')(
|
|
559
|
+
function* (message: string) {
|
|
560
|
+
const response = yield* session
|
|
561
|
+
.generateText({ prompt: message })
|
|
562
|
+
.pipe(Effect.provide(chatModelLayer));
|
|
563
|
+
const history = yield* Ref.get(session.history);
|
|
564
|
+
yield* Effect.logInfo(
|
|
565
|
+
`History: ${history.content.length} messages`
|
|
566
|
+
);
|
|
567
|
+
return response.text;
|
|
568
|
+
},
|
|
569
|
+
Effect.mapError((e) => WriterError.fromAiError(e))
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
const streamHighlights = (version: string) =>
|
|
573
|
+
LanguageModel.streamText({
|
|
574
|
+
prompt: `Release highlights for v${version} as bullets.`
|
|
575
|
+
}).pipe(
|
|
576
|
+
Stream.filter(
|
|
577
|
+
(part): part is Response.TextDeltaPart =>
|
|
578
|
+
part.type === 'text-delta'
|
|
579
|
+
),
|
|
580
|
+
Stream.map((part) => part.delta),
|
|
581
|
+
Stream.provide(chatModelLayer),
|
|
582
|
+
Stream.mapError((e) => WriterError.fromAiError(e))
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
return AiWriter.of({ draft, chat, streamHighlights });
|
|
586
|
+
})
|
|
587
|
+
).pipe(Layer.provide([OpenAiClientLayer, AnthropicClientLayer]));
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// ---------------------------------------------------------------------------
|
|
591
|
+
// Usage
|
|
592
|
+
// ---------------------------------------------------------------------------
|
|
593
|
+
|
|
594
|
+
const program = Effect.gen(function* () {
|
|
595
|
+
const writer = yield* AiWriter;
|
|
596
|
+
const result = yield* writer.draft('Effect Cloud');
|
|
597
|
+
yield* Effect.logInfo(`Provider: ${result.provider}, Text: ${result.text}`);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
Effect.runPromise(program.pipe(Effect.provide(AiWriter.layer)));
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
## Anti-Patterns
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
// WRONG: Hardcoded API keys
|
|
607
|
+
AnthropicClient.layerConfig({ apiKey: 'sk-...' });
|
|
608
|
+
|
|
609
|
+
// RIGHT: Config.redacted for secrets
|
|
610
|
+
AnthropicClient.layerConfig({ apiKey: Config.redacted('ANTHROPIC_API_KEY') });
|
|
611
|
+
|
|
612
|
+
// WRONG: Missing FetchHttpClient layer
|
|
613
|
+
AnthropicClient.layerConfig({ apiKey: Config.redacted('KEY') });
|
|
614
|
+
// Will fail at runtime — providers require an HttpClient
|
|
615
|
+
|
|
616
|
+
// RIGHT: Always provide an HTTP client layer
|
|
617
|
+
AnthropicClient.layerConfig({ apiKey: Config.redacted('KEY') }).pipe(
|
|
618
|
+
Layer.provide(FetchHttpClient.layer)
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
// WRONG: Old Chat.make API
|
|
622
|
+
const chat = yield* Chat.make({ system: 'You are helpful' });
|
|
623
|
+
|
|
624
|
+
// RIGHT: Chat.fromPrompt with Prompt composition
|
|
625
|
+
const chat =
|
|
626
|
+
yield*
|
|
627
|
+
Chat.fromPrompt(Prompt.empty.pipe(Prompt.setSystem('You are helpful')));
|
|
628
|
+
|
|
629
|
+
// WRONG: Old Model.make API with object arg
|
|
630
|
+
Model.make({ name: 'claude', layer: AnthropicLive });
|
|
631
|
+
|
|
632
|
+
// RIGHT: Model.make with 3 positional args (or use .model() shorthand)
|
|
633
|
+
Model.make('anthropic', 'claude-opus-4-6', anthropicLayer);
|
|
634
|
+
// Better: AnthropicLanguageModel.model("claude-opus-4-6")
|
|
635
|
+
|
|
636
|
+
// WRONG: Importing non-existent providers
|
|
637
|
+
import { GoogleClient } from '@effect/ai-google'; // Does NOT exist
|
|
638
|
+
import { BedrockClient } from '@effect/ai-amazon-bedrock'; // Does NOT exist
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
## Quality Checklist
|
|
642
|
+
|
|
643
|
+
- [ ] Use `Config.redacted` for API keys (never hardcode)
|
|
644
|
+
- [ ] Provide `FetchHttpClient.layer` to all client layers
|
|
645
|
+
- [ ] Use `.model()` constructor for `ExecutionPlan` and `Effect.provide`
|
|
646
|
+
- [ ] Use `ExecutionPlan` for multi-provider fallback with retry
|
|
647
|
+
- [ ] Use `withConfigOverride` for per-effect config adjustments
|
|
648
|
+
- [ ] Use `apiUrl` for OpenAI-compatible base URLs; reserve client transforms for middleware/proxy/tracing/headers
|
|
649
|
+
- [ ] Use `OpenAiTool` for OpenAI provider-defined tools
|
|
650
|
+
- [ ] Use `Chat.fromPrompt` / `Chat.empty` / `Chat.fromJson` (not `Chat.make`)
|
|
651
|
+
- [ ] Wrap `AiError` into a domain-specific `Schema.TaggedError`
|
|
652
|
+
- [ ] Use `Context.Service` with shape type parameter for service definitions
|
|
653
|
+
|
|
654
|
+
## Related Skills
|
|
655
|
+
|
|
656
|
+
- effect-ai-language-model - Using LanguageModel service for text/object/stream generation
|
|
657
|
+
- effect-ai-prompt - Building prompts with Prompt composition operators
|
|
658
|
+
- effect-ai-tool - Defining tools and toolkits for agentic loops
|
|
659
|
+
- effect-ai-streaming - Streaming response patterns and accumulation
|
|
660
|
+
- effect-layer-design - General Effect layer composition patterns
|
|
661
|
+
|
|
662
|
+
## References
|
|
663
|
+
|
|
664
|
+
- `packages/ai/anthropic/src/AnthropicLanguageModel.ts`
|
|
665
|
+
- `packages/ai/openai/src/OpenAiLanguageModel.ts`
|
|
666
|
+
- `packages/ai/openrouter/src/OpenRouterLanguageModel.ts`
|
|
667
|
+
- `ai-docs/src/71_ai/10_language-model.ts`
|
|
668
|
+
- `ai-docs/src/71_ai/30_chat.ts`
|