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.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +278 -0
  3. package/guidance/effect-first-development.md +1247 -0
  4. package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
  5. package/guidance/post__parse-dont-validate.md +109 -0
  6. package/guidance/progressive-disclosure-guidance.md +38 -0
  7. package/package.json +63 -0
  8. package/patterns/avoid-any.md +37 -0
  9. package/patterns/avoid-data-tagged-error.md +34 -0
  10. package/patterns/avoid-direct-json.md +51 -0
  11. package/patterns/avoid-direct-tag-checks.md +54 -0
  12. package/patterns/avoid-expect-in-if.md +52 -0
  13. package/patterns/avoid-mutable-state.md +70 -0
  14. package/patterns/avoid-native-fetch.md +61 -0
  15. package/patterns/avoid-node-imports.md +86 -0
  16. package/patterns/avoid-non-null-assertion.md +44 -0
  17. package/patterns/avoid-object-type.md +46 -0
  18. package/patterns/avoid-option-getorthrow.md +39 -0
  19. package/patterns/avoid-platform-coupling.md +43 -0
  20. package/patterns/avoid-process-env.md +43 -0
  21. package/patterns/avoid-react-hooks.md +73 -0
  22. package/patterns/avoid-schema-suffix.md +45 -0
  23. package/patterns/avoid-sync-fs.md +68 -0
  24. package/patterns/avoid-try-catch.md +47 -0
  25. package/patterns/avoid-ts-ignore.md +38 -0
  26. package/patterns/avoid-untagged-errors.md +67 -0
  27. package/patterns/avoid-yield-ref.md +46 -0
  28. package/patterns/casting-awareness.md +46 -0
  29. package/patterns/context-tag-extends.md +84 -0
  30. package/patterns/effect-catchall-default.md +61 -0
  31. package/patterns/effect-promise-vs-trypromise.md +47 -0
  32. package/patterns/effect-run-in-body.md +58 -0
  33. package/patterns/imperative-loops.md +76 -0
  34. package/patterns/prefer-arr-sort.md +52 -0
  35. package/patterns/prefer-duration-values.md +56 -0
  36. package/patterns/prefer-effect-fn.md +161 -0
  37. package/patterns/prefer-match-over-switch.md +48 -0
  38. package/patterns/prefer-option-over-null.md +56 -0
  39. package/patterns/prefer-redacted-config.md +70 -0
  40. package/patterns/prefer-schema-class.md +54 -0
  41. package/patterns/require-effect-concurrency.md +83 -0
  42. package/patterns/stream-large-files.md +63 -0
  43. package/patterns/throw-in-effect-gen.md +62 -0
  44. package/patterns/use-clock-service.md +45 -0
  45. package/patterns/use-command-executor-service.md +54 -0
  46. package/patterns/use-console-service.md +54 -0
  47. package/patterns/use-filesystem-service.md +59 -0
  48. package/patterns/use-http-client-service.md +77 -0
  49. package/patterns/use-path-service.md +53 -0
  50. package/patterns/use-random-service.md +45 -0
  51. package/patterns/use-temp-file-scoped.md +66 -0
  52. package/patterns/vm-in-wrong-file.md +51 -0
  53. package/patterns/yield-in-for-loop.md +61 -0
  54. package/skills/effect-ai-chat/SKILL.md +472 -0
  55. package/skills/effect-ai-language-model/SKILL.md +652 -0
  56. package/skills/effect-ai-prompt/SKILL.md +752 -0
  57. package/skills/effect-ai-provider/SKILL.md +668 -0
  58. package/skills/effect-ai-streaming/SKILL.md +418 -0
  59. package/skills/effect-ai-tool/SKILL.md +1132 -0
  60. package/skills/effect-atom-rpc/SKILL.md +488 -0
  61. package/skills/effect-atom-state/SKILL.md +640 -0
  62. package/skills/effect-batching/SKILL.md +614 -0
  63. package/skills/effect-cache/SKILL.md +570 -0
  64. package/skills/effect-cli/SKILL.md +523 -0
  65. package/skills/effect-command-executor/SKILL.md +675 -0
  66. package/skills/effect-concurrency-testing/SKILL.md +612 -0
  67. package/skills/effect-config/SKILL.md +580 -0
  68. package/skills/effect-context-witness/SKILL.md +274 -0
  69. package/skills/effect-domain-modeling/SKILL.md +1212 -0
  70. package/skills/effect-domain-predicates/SKILL.md +867 -0
  71. package/skills/effect-error-handling/SKILL.md +1581 -0
  72. package/skills/effect-fiber/SKILL.md +731 -0
  73. package/skills/effect-filesystem/SKILL.md +624 -0
  74. package/skills/effect-graph/SKILL.md +571 -0
  75. package/skills/effect-http-api/SKILL.md +1760 -0
  76. package/skills/effect-http-client/SKILL.md +989 -0
  77. package/skills/effect-http-server/SKILL.md +920 -0
  78. package/skills/effect-incremental-migration/SKILL.md +362 -0
  79. package/skills/effect-layer-design/SKILL.md +642 -0
  80. package/skills/effect-managed-runtime/SKILL.md +395 -0
  81. package/skills/effect-mcp-server/SKILL.md +608 -0
  82. package/skills/effect-observability/SKILL.md +719 -0
  83. package/skills/effect-optics/SKILL.md +554 -0
  84. package/skills/effect-parallelization/SKILL.md +668 -0
  85. package/skills/effect-path/SKILL.md +296 -0
  86. package/skills/effect-pattern-matching/SKILL.md +914 -0
  87. package/skills/effect-platform-abstraction/SKILL.md +1175 -0
  88. package/skills/effect-platform-layers/SKILL.md +514 -0
  89. package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
  90. package/skills/effect-react-composition/SKILL.md +986 -0
  91. package/skills/effect-react-vm/SKILL.md +675 -0
  92. package/skills/effect-rpc-api/SKILL.md +624 -0
  93. package/skills/effect-rpc-client/SKILL.md +666 -0
  94. package/skills/effect-rpc-cluster/SKILL.md +1623 -0
  95. package/skills/effect-rpc-server/SKILL.md +767 -0
  96. package/skills/effect-scheduling/SKILL.md +124 -0
  97. package/skills/effect-schema-composition/SKILL.md +975 -0
  98. package/skills/effect-schema-v4/SKILL.md +691 -0
  99. package/skills/effect-scope/SKILL.md +682 -0
  100. package/skills/effect-service-implementation/SKILL.md +656 -0
  101. package/skills/effect-socket/SKILL.md +703 -0
  102. package/skills/effect-sql/SKILL.md +781 -0
  103. package/skills/effect-stream/SKILL.md +765 -0
  104. package/skills/effect-testing/SKILL.md +1331 -0
  105. package/skills/effect-typeclass-design/SKILL.md +161 -0
  106. package/skills/effect-wide-events/Article.md +66 -0
  107. package/skills/effect-wide-events/SKILL.md +95 -0
  108. package/skills/effect-workflow/SKILL.md +810 -0
  109. package/src/agent-policy.ts +22 -0
  110. package/src/enforcer.ts +104 -0
  111. package/src/frontmatter.ts +34 -0
  112. package/src/guidance.ts +66 -0
  113. package/src/index.ts +38 -0
  114. package/src/pattern-catalog.ts +115 -0
  115. package/src/pattern-matcher.ts +178 -0
  116. package/src/pattern.ts +97 -0
  117. package/src/skills.ts +29 -0
  118. 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`