@xandreed/plugin-models 0.14.0 → 0.15.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 (151) hide show
  1. package/README.md +1 -1
  2. package/dist/auth/anthropicOAuth.d.ts +30 -10
  3. package/dist/auth/anthropicOAuth.js +15 -18
  4. package/dist/auth/auth-store.conformance.d.ts +63 -0
  5. package/dist/auth/auth-store.conformance.js +152 -0
  6. package/dist/auth/credential-input.entity.d.ts +23 -5
  7. package/dist/auth/credential-input.entity.functions.d.ts +15 -5
  8. package/dist/auth/credential-input.entity.functions.js +12 -9
  9. package/dist/auth/credential-input.entity.js +5 -3
  10. package/dist/auth/credential-refresh.entity.d.ts +11 -3
  11. package/dist/auth/credential-refresh.entity.functions.d.ts +48 -6
  12. package/dist/auth/credential-refresh.entity.functions.js +12 -3
  13. package/dist/auth/localAuth.d.ts +10 -1
  14. package/dist/auth/localAuth.js +53 -79
  15. package/dist/auth/memory-auth.adapter.d.ts +12 -4
  16. package/dist/auth/memory-auth.adapter.js +13 -25
  17. package/dist/auth/openAiCodexOAuth.d.ts +26 -5
  18. package/dist/auth/openAiCodexOAuth.js +17 -28
  19. package/dist/auth/token-response.entity.d.ts +21 -0
  20. package/dist/auth/token-response.entity.functions.d.ts +20 -0
  21. package/dist/auth/token-response.entity.functions.js +10 -0
  22. package/dist/auth/token-response.entity.js +10 -0
  23. package/dist/compat/index.d.ts +2 -0
  24. package/dist/compat/index.js +1 -0
  25. package/dist/conformance/index.d.ts +6 -0
  26. package/dist/conformance/index.js +3 -0
  27. package/dist/index.d.ts +19 -19
  28. package/dist/index.js +17 -69
  29. package/dist/llm/chat-wire.entity.d.ts +123 -0
  30. package/dist/llm/chat-wire.entity.functions.d.ts +28 -0
  31. package/dist/llm/chat-wire.entity.functions.js +10 -0
  32. package/dist/llm/chat-wire.entity.js +37 -0
  33. package/dist/llm/compat-telemetry.adapter.d.ts +6 -0
  34. package/dist/llm/compat-telemetry.adapter.js +21 -0
  35. package/dist/llm/compat.d.ts +50 -30
  36. package/dist/llm/compat.js +149 -133
  37. package/dist/llm/model-completion.adapter.d.ts +49 -0
  38. package/dist/llm/model-completion.adapter.js +77 -0
  39. package/dist/llm/model-completion.entity.d.ts +4 -0
  40. package/dist/llm/model-completion.entity.functions.d.ts +3 -0
  41. package/dist/llm/model-completion.entity.functions.js +7 -0
  42. package/dist/llm/model-completion.entity.js +7 -0
  43. package/dist/llm/model-retry-policy.entity.d.ts +37 -0
  44. package/dist/llm/model-retry-policy.entity.functions.d.ts +16 -0
  45. package/dist/llm/model-retry-policy.entity.functions.js +33 -0
  46. package/dist/llm/model-retry-policy.entity.js +23 -0
  47. package/dist/llm/model-settings.entity.d.ts +85 -0
  48. package/dist/llm/model-settings.entity.functions.d.ts +11 -0
  49. package/dist/llm/model-settings.entity.functions.js +49 -0
  50. package/dist/llm/model-settings.entity.js +28 -0
  51. package/dist/llm/model-usage.entity.d.ts +61 -0
  52. package/dist/llm/model-usage.entity.functions.d.ts +11 -0
  53. package/dist/llm/model-usage.entity.functions.js +24 -0
  54. package/dist/llm/model-usage.entity.js +19 -0
  55. package/dist/llm/modelCatalog.d.ts +29 -8
  56. package/dist/llm/modelCatalog.js +28 -66
  57. package/dist/llm/openAiCodex.d.ts +15 -7
  58. package/dist/llm/openAiCodex.js +57 -45
  59. package/dist/llm/openAiCodexWebSocket.d.ts +48 -15
  60. package/dist/llm/openAiCodexWebSocket.js +101 -147
  61. package/dist/llm/prompt-cache-frame.entity.d.ts +18 -0
  62. package/dist/llm/prompt-cache-frame.entity.js +10 -0
  63. package/dist/llm/prompt-cache.entity.d.ts +57 -0
  64. package/dist/llm/prompt-cache.entity.functions.d.ts +8 -0
  65. package/dist/llm/prompt-cache.entity.functions.js +73 -0
  66. package/dist/llm/prompt-cache.entity.js +27 -0
  67. package/dist/llm/provider-client.adapter.d.ts +9 -0
  68. package/dist/llm/provider-client.adapter.js +60 -0
  69. package/dist/llm/provider-failure.entity.d.ts +42 -0
  70. package/dist/llm/provider-failure.entity.functions.d.ts +8 -0
  71. package/dist/llm/provider-failure.entity.functions.js +67 -0
  72. package/dist/llm/provider-failure.entity.js +20 -0
  73. package/dist/llm/provider-profile.entity.d.ts +79 -0
  74. package/dist/llm/provider-profile.entity.functions.d.ts +23 -0
  75. package/dist/llm/provider-profile.entity.functions.js +83 -0
  76. package/dist/llm/provider-profile.entity.js +10 -0
  77. package/dist/llm/providers.d.ts +116 -34
  78. package/dist/llm/providers.js +298 -129
  79. package/dist/llm/retry.d.ts +11 -47
  80. package/dist/llm/retry.js +33 -52
  81. package/dist/llm/router.d.ts +54 -36
  82. package/dist/llm/router.js +91 -78
  83. package/dist/llm/sse.d.ts +44 -37
  84. package/dist/llm/sse.js +43 -35
  85. package/dist/llm/thinking-binding.entity.d.ts +49 -0
  86. package/dist/llm/thinking-binding.entity.functions.d.ts +21 -0
  87. package/dist/llm/thinking-binding.entity.functions.js +16 -0
  88. package/dist/llm/thinking-binding.entity.js +11 -0
  89. package/dist/llm/utilityLlm.d.ts +14 -5
  90. package/dist/llm/utilityLlm.js +18 -13
  91. package/dist/plugin.adapter.d.ts +94 -6
  92. package/dist/plugin.adapter.js +18 -13
  93. package/dist/ports/auth-store.port.d.ts +73 -0
  94. package/dist/ports/auth-store.port.js +24 -0
  95. package/dist/ports/host-credentials.port.d.ts +2 -1
  96. package/dist/ports/model-call-completion.port.d.ts +9 -0
  97. package/dist/ports/model-call-completion.port.js +2 -0
  98. package/dist/ports/model-endpoints.port.d.ts +60 -26
  99. package/dist/ports/model-endpoints.port.js +17 -3
  100. package/dist/ports/model-retry-policy.port.d.ts +17 -0
  101. package/dist/ports/model-retry-policy.port.js +3 -0
  102. package/dist/ports/model-settings.port.d.ts +32 -0
  103. package/dist/ports/model-settings.port.js +4 -0
  104. package/dist/ports/models-transport.port.d.ts +9 -2
  105. package/dist/ports/models-transport.port.js +4 -1
  106. package/dist/ports/prompt-cache-frame.port.d.ts +21 -0
  107. package/dist/ports/prompt-cache-frame.port.js +4 -0
  108. package/dist/ports/settings-store.port.d.ts +70 -0
  109. package/dist/ports/settings-store.port.js +33 -0
  110. package/dist/ports/thinking-binding.port.d.ts +7 -0
  111. package/dist/ports/thinking-binding.port.js +4 -0
  112. package/dist/settings/localSettings.d.ts +2 -1
  113. package/dist/settings/localSettings.js +17 -20
  114. package/dist/settings/memory-settings.adapter.d.ts +3 -1
  115. package/dist/settings/memory-settings.adapter.js +1 -1
  116. package/dist/settings/roleView.d.ts +3 -1
  117. package/dist/settings/roleView.js +1 -1
  118. package/dist/settings/settings-store.conformance.d.ts +47 -0
  119. package/dist/settings/settings-store.conformance.js +100 -0
  120. package/dist/settings/settings-value.entity.d.ts +10 -2
  121. package/dist/settings/settings-value.entity.functions.d.ts +31 -9
  122. package/dist/settings/settings-value.entity.functions.js +5 -6
  123. package/dist/storage/json-file.d.ts +18 -0
  124. package/dist/storage/json-file.js +26 -0
  125. package/dist/transport/http-proxy.entity.d.ts +17 -3
  126. package/dist/transport/model-request.conformance.d.ts +13 -0
  127. package/dist/transport/model-request.conformance.js +36 -0
  128. package/dist/transport/models-transport.adapter.d.ts +18 -17
  129. package/dist/transport/models-transport.adapter.js +28 -31
  130. package/dist/transport/resilient-http-client.adapter.d.ts +73 -0
  131. package/dist/transport/resilient-http-client.adapter.js +177 -0
  132. package/package.json +11 -5
  133. package/dist/auth/anthropicOAuth.test.d.ts +0 -1
  134. package/dist/auth/credential-refresh.entity.functions.test.d.ts +0 -1
  135. package/dist/auth/localAuth.test.d.ts +0 -1
  136. package/dist/auth/memory-auth.adapter.test.d.ts +0 -1
  137. package/dist/llm/compat.test.d.ts +0 -1
  138. package/dist/llm/modelCatalog.test.d.ts +0 -1
  139. package/dist/llm/openAiCodexWebSocket.test.d.ts +0 -1
  140. package/dist/llm/providers.test.d.ts +0 -1
  141. package/dist/llm/retry.test.d.ts +0 -1
  142. package/dist/llm/router.test.d.ts +0 -1
  143. package/dist/llm/sse.test.d.ts +0 -1
  144. package/dist/plugin.adapter.test.d.ts +0 -1
  145. package/dist/settings/localSettings.test.d.ts +0 -1
  146. package/dist/settings/memory-settings.adapter.test.d.ts +0 -1
  147. package/dist/settings/roleView.test.d.ts +0 -1
  148. package/dist/settings/settings-value.entity.functions.test.d.ts +0 -1
  149. package/dist/transport/http-proxy.entity.functions.test.d.ts +0 -1
  150. package/dist/transport/models-transport.adapter.test.d.ts +0 -1
  151. package/dist/transport/recording-proxy.testing.d.ts +0 -33
@@ -1,163 +1,332 @@
1
- import { LanguageModel, Prompt } from "effect/ai";
1
+ import { AiError, LanguageModel, Prompt } from "effect/ai";
2
2
  import { AnthropicClient, AnthropicLanguageModel } from "@effect/ai-anthropic";
3
3
  import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai";
4
- import { HttpClient, HttpClientRequest } from "effect/http";
5
- import { Effect, Option, Redacted } from "effect";
6
- import { AuthError, CurrentModelCallPolicy, CurrentPromptCacheKey, describeModel, formatModelSelection, modelRequestDescriptorOf } from "@xandreed/core";
4
+ import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";
5
+ import { Config, Effect, Match, Metric, Option, Predicate, Redacted, Ref, Result, Stream } from "effect";
6
+ import { isTurnError, CurrentModelCallPolicy, CurrentPromptCacheKey, describeModel, formatModelSelection, modelFailureOf, modelRequestDescriptorOf, stampAiErrorFailure } from "@xandreed/core";
7
+ import { isVolatileContext, stripVolatileContext } from "@xandreed/core/internal";
8
+ import { CurrentPromptCacheFrame } from "../ports/prompt-cache-frame.port.js";
9
+ import { PromptCacheError } from "./prompt-cache.entity.js";
10
+ import { planBreakpoints } from "./prompt-cache.entity.functions.js";
11
+ import { AuthError } from "../ports/auth-store.port.js";
7
12
  import { ANTHROPIC_OAUTH_BETA, CLAUDE_CODE_SYSTEM } from "../auth/anthropicOAuth.js";
8
13
  import { makeCompatLanguageModel, promptWithImageLabels } from "./compat.js";
9
- import { imageInputFor, usesOpenCodeResponses } from "./modelCatalog.js";
14
+ import { publicReasoningEffort, usesOpenCodeResponses } from "./modelCatalog.js";
10
15
  import { makeOpenAiCodexLanguageModel, OPENAI_CODEX_API_URL } from "./openAiCodex.js";
16
+ import { CurrentOpenAiCodexSessions } from "./openAiCodexWebSocket.js";
17
+ import { BuiltInProvider } from "./provider-profile.entity.js";
18
+ import { anthropicProfileOf, profileOf } from "./provider-profile.entity.functions.js";
11
19
  import { openAiCodexAccountId } from "../auth/openAiCodexOAuth.js";
12
20
  import { CurrentModelEndpoints } from "../ports/model-endpoints.port.js";
13
- import { ModelsTransport } from "../ports/models-transport.port.js";
14
- export const OPENCODE_CHAT_URL = "https://opencode.ai/zen/go/v1/chat/completions", OPENCODE_RESPONSES_API_URL = "https://opencode.ai/zen/v1", GOOGLE_CHAT_URL = "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions";
15
-
21
+ import { CurrentManagedModelsTransport, ModelsTransport } from "../ports/models-transport.port.js";
22
+ import { CurrentModelSettings } from "../ports/model-settings.port.js";
23
+ import { CurrentModelCallPurpose } from "../ports/model-call-completion.port.js";
24
+ import { ModelSettingsError, ModelThinking } from "./model-settings.entity.js";
25
+ import { resolveAnthropicSettings, validateAnthropicRequest } from "./model-settings.entity.functions.js";
26
+ import { CurrentThinkingPrefixMismatch } from "../ports/thinking-binding.port.js";
27
+ import { THINKING_BINDING_BETA } from "./thinking-binding.entity.js";
28
+ import { bindingFor, droppedInBody, droppedInMessageStart, firstEventData, mergeBetas } from "./thinking-binding.entity.functions.js";
29
+ import { classifyAiError } from "./provider-failure.entity.functions.js";
30
+ import { providerFailureProtocol, withAnthropicFailures, withOpenAiFailures } from "./provider-client.adapter.js";
31
+ import { retainModelAttempt } from "./model-completion.adapter.js";
32
+ import { observedResponse, resilientHttpClient, withModelRequestCall, withModelRequestStream } from "../transport/resilient-http-client.adapter.js";
33
+ export { GOOGLE_CHAT_URL, OPENCODE_CHAT_URL, OPENCODE_RESPONSES_API_URL } from "./provider-profile.entity.functions.js";
16
34
  export { usesOpenCodeResponses };
35
+ export const withPrompt = (options, f) => ({ ...options, prompt: f(Prompt.make(options.prompt)) });
17
36
  const withImageLabels = (model) => {
18
- const relabeled = (options) => ({
19
- ...options,
20
- prompt: promptWithImageLabels(Prompt.make(options.prompt))
21
- });
37
+ const relabeled = (options) => withPrompt(options, promptWithImageLabels);
22
38
  return {
23
39
  ...model,
24
40
  generateText: (options) => model.generateText(relabeled(options)),
25
41
  generateObject: (options) => model.generateObject(relabeled(options)),
26
42
  streamText: (options) => model.streamText(relabeled(options))
27
43
  };
28
- }, anthropicOAuthTransform = (access) => (client) => client.pipe(HttpClient.mapRequest((req) => req.pipe(HttpClientRequest.setHeaders({
29
- Authorization: `Bearer ${Redacted.value(access)}`,
30
- "anthropic-beta": ANTHROPIC_OAUTH_BETA
31
- })))), claudeCodePrompt = Prompt.make([{ role: "system", content: CLAUDE_CODE_SYSTEM }]);
32
- export const prependClaudeCode = (options) => ({
33
- ...options,
34
- prompt: Prompt.concat(claudeCodePrompt, Prompt.make(options.prompt))
35
- }), withAnthropicCacheBreakpoints = (options) => {
36
- const messages = Prompt.make(options.prompt).content;
37
- if (messages.length === 0)
38
- return options;
39
- const lastSystem = messages.reduce((acc, m, i) => m.role === "system" ? i : acc, -1), nonSystem = messages.flatMap((m, i) => m.role === "system" ? [] : [i]), stampIdx = new Set([
40
- ...lastSystem >= 0 ? [lastSystem] : [],
41
- ...nonSystem.slice(-2)
42
- ]), stamped = messages.map((msg, i) => {
43
- if (!stampIdx.has(i))
44
- return msg;
45
- const anthropic = msg.options.anthropic ?? {};
46
- if (anthropic.cacheControl !== void 0)
47
- return msg;
48
- return Prompt.makeMessage(msg.role, {
49
- ...msg,
50
- options: {
51
- ...msg.options,
52
- anthropic: { ...anthropic, cacheControl: { type: "ephemeral" } }
53
- }
44
+ }, withStrippedVolatileContext = (model) => ({
45
+ ...model,
46
+ generateText: (options) => model.generateText(withoutVolatilePrompt(options)),
47
+ generateObject: (options) => model.generateObject(withoutVolatilePrompt(options)),
48
+ streamText: (options) => model.streamText(withoutVolatilePrompt(options))
49
+ }), withProviderFailures = (model, protocol, selection, limits) => {
50
+ const actual = { provider: selection.provider, modelId: selection.modelId }, bounds = { ...limits }, failure = (error) => {
51
+ if (!AiError.isAiError(error))
52
+ return error;
53
+ const classified = classifyAiError(error, protocol);
54
+ return Option.contains(modelFailureOf(classified), "ContextOverflow") ? stampAiErrorFailure(classified, "ContextOverflow", { model: { ...actual }, limits: { ...bounds } }) : classified;
55
+ };
56
+ return {
57
+ ...model,
58
+ generateText: (options) => model.generateText(options).pipe(Effect.mapError(failure)),
59
+ generateObject: (options) => model.generateObject(options).pipe(Effect.mapError(failure)),
60
+ streamText: (options) => model.streamText(options).pipe(Stream.mapError(failure))
61
+ };
62
+ }, withProviderAccounting = (model, selection) => ({
63
+ ...model,
64
+ generateText: (options) => withModelRequestCall("Text", selection, model.generateText(options)).pipe(Effect.catch((error) => isTurnError(error) ? Effect.die(error) : Effect.fail(error))),
65
+ generateObject: (options) => withModelRequestCall("Object", selection, model.generateObject(options)).pipe(Effect.catch((error) => isTurnError(error) ? Effect.die(error) : Effect.fail(error))),
66
+ streamText: (options) => withModelRequestStream("Stream", selection, model.streamText(options))
67
+ }), withAnthropicBetas = (betas) => (client) => betas.length === 0 ? client : client.pipe(HttpClient.mapRequest((req) => HttpClientRequest.setHeader(req, "anthropic-beta", mergeBetas(Option.fromNullishOr(req.headers["anthropic-beta"]), betas)))), anthropicOAuthTransform = (access) => (client) => client.pipe(HttpClient.mapRequest((req) => HttpClientRequest.setHeader(req, "Authorization", `Bearer ${Redacted.value(access)}`)), withAnthropicBetas(ANTHROPIC_OAUTH_BETA.split(","))), llmThinkingDropped = Metric.counter("llm.thinking.dropped", {
68
+ description: "thinking blocks the provider dropped from a request whose conversation changed before them",
69
+ incremental: !0
70
+ }), reportDropped = (label) => (reasons) => reasons.length === 0 ? Effect.void : Effect.gen(function* () {
71
+ yield* Effect.annotateCurrentSpan("llm.thinking.dropped", reasons.length);
72
+ yield* Effect.forEach(reasons, (reason) => Metric.update(Metric.withAttributes(llmThinkingDropped, { "llm.model": label, reason }), 1), { concurrency: 1, discard: !0 });
73
+ yield* Effect.logWarning(`the provider dropped ${reasons.length} thinking block(s) from the request: the conversation before them changed (${[...new Set(reasons)].join(", ")})`);
74
+ }), firstEventObserver = (report) => Effect.gen(function* () {
75
+ const decoder = new TextDecoder, pending = yield* Ref.make(Option.some(""));
76
+ return (bytes) => Ref.modify(pending, (seen) => Option.match(seen, {
77
+ onNone: () => [Option.none(), Option.none()],
78
+ onSome: (before) => {
79
+ const text = before + decoder.decode(bytes, { stream: !0 }), first = firstEventData(text);
80
+ return Option.isSome(first) ? [first, Option.none()] : [Option.none(), Option.some(text)];
81
+ }
82
+ })).pipe(Effect.flatMap(Option.match({ onNone: () => Effect.void, onSome: (data) => report(droppedInMessageStart(data)) })));
83
+ }), reportingDroppedThinking = (label) => (client) => HttpClient.transformResponse(client, Effect.flatMap((response) => {
84
+ const rebuilt = (body) => retainModelAttempt(response, HttpClientResponse.fromWeb(response.request, new Response(body, { status: response.status, headers: new Headers(Object.entries(response.headers)) })));
85
+ return (response.headers["content-type"] ?? "").includes("text/event-stream") ? firstEventObserver(reportDropped(label)).pipe(Effect.flatMap((observe) => observedResponse(response, observe)), Effect.map((observed) => observed)) : response.text.pipe(Effect.tap((text) => reportDropped(label)(droppedInBody(text))), Effect.map(rebuilt));
86
+ })), claudeCodePrompt = Prompt.make([{ role: "system", content: CLAUDE_CODE_SYSTEM }]);
87
+ export const prependClaudeCode = (options) => withPrompt(options, (prompt) => Prompt.concat(claudeCodePrompt, prompt));
88
+ const withoutVolatilePrompt = (options) => withPrompt(options, (prompt) => Prompt.fromMessages(prompt.content.map((message) => {
89
+ const cleaned = stripVolatileContext(message.options);
90
+ return cleaned === message.options ? message : Prompt.makeMessage(message.role, { ...message, options: cleaned });
91
+ }))), prepareCachePrompt = (options, policy, capability) => {
92
+ const prompt = Prompt.make(options.prompt), firstVolatile = prompt.content.findIndex((message) => isVolatileContext(message.options)), cut = firstVolatile < 0 ? prompt.content.length : firstVolatile, relocatedVolatile = prompt.content.slice(cut).some((message) => message.role === "system"), ttl = capability && !relocatedVolatile ? Match.valueTags(policy, {
93
+ Auto: (value) => Option.some(value.ttl),
94
+ TopLevel: () => Option.none(),
95
+ Off: () => Option.none()
96
+ }) : Option.none(), stable = prompt.content.slice(0, cut), lastSystem = stable.findLastIndex((message) => message.role === "system"), targets = [...stable.flatMap((message, index) => message.role === "system" ? [] : [index]).slice(-2).toReversed().map((index, priority) => ({ index, priority })), ...lastSystem < 0 ? [] : [{ index: lastSystem, priority: 2 }]], prepared = prompt.content.reduce((state, message, index) => {
97
+ const target = targets.find((value) => value.index === index), own = message.options.anthropic, cache = Predicate.hasProperty(own, "cacheControl") ? own.cacheControl : void 0, candidate = target !== void 0 && cache === void 0 && (own == null || Predicate.isObject(own)) ? Option.map(ttl, (value) => ({ priority: target.priority, control: value === "5m" ? { type: "ephemeral" } : { type: "ephemeral", ttl: value } })) : Option.none(), cleaned = stripVolatileContext(message.options), next = Option.match(candidate, {
98
+ onNone: () => cleaned === message.options ? message : Prompt.makeMessage(message.role, { ...message, options: cleaned }),
99
+ onSome: ({ control }) => Prompt.makeMessage(message.role, { ...message, options: { ...cleaned, anthropic: { ...Predicate.isObject(own) ? own : {}, cacheControl: control } } })
54
100
  });
55
- });
101
+ return { messages: [...state.messages, next], candidates: [...state.candidates, ...Option.toArray(candidate)] };
102
+ }, { messages: [], candidates: [] });
103
+ return { options: { ...options, prompt: Prompt.fromMessages(prepared.messages) }, frame: { candidates: prepared.candidates, policy, capability } };
104
+ }, cacheField = (value, key) => Predicate.hasProperty(value, key) ? value[key] : void 0, cachePathKey = (path) => path.join("/"), cacheableBlock = (block) => {
105
+ const type = cacheField(block, "type");
106
+ return type !== "thinking" && type !== "redacted_thinking" && (type !== "text" || typeof cacheField(block, "text") === "string" && cacheField(block, "text") !== "");
107
+ }, cacheSlotsInBlocks = (blocks, path) => Array.isArray(blocks) ? blocks.flatMap((block, index) => {
108
+ const at = [...path, index];
109
+ return [
110
+ ...cacheField(block, "type") === "tool_result" ? cacheSlotsInBlocks(cacheField(block, "content"), [...at, "content"]) : [],
111
+ { path: at, control: cacheField(block, "cache_control"), cacheable: cacheableBlock(block) }
112
+ ];
113
+ }) : [], preparedCacheSlots = (payload) => [
114
+ ...cacheSlotsInBlocks(cacheField(payload, "tools"), ["tools"]),
115
+ ...cacheSlotsInBlocks(cacheField(payload, "system"), ["system"]),
116
+ ...Option.match(Option.filter(Option.fromNullishOr(cacheField(payload, "messages")), Array.isArray), {
117
+ onNone: () => [],
118
+ onSome: (messages) => messages.flatMap((message, index) => cacheSlotsInBlocks(cacheField(message, "content"), ["messages", index, "content"]))
119
+ })
120
+ ], cacheFailure = (error) => AiError.make({
121
+ module: "Models",
122
+ method: "promptCache",
123
+ reason: new AiError.InvalidRequestError({ description: error.message })
124
+ }), withoutCacheField = (value) => Object.fromEntries(Object.entries(value).filter(([key]) => key !== "cache_control")), rewriteCachePayload = (payload, patches, topLevel, capability) => {
125
+ const blocks = (value, path) => Array.isArray(value) ? value.map((block, index) => {
126
+ if (!Predicate.isObject(block))
127
+ return block;
128
+ const at = [...path, index], nested = cacheField(block, "type") === "tool_result" && Array.isArray(cacheField(block, "content")) ? { ...block, content: blocks(cacheField(block, "content"), [...at, "content"]) } : block, patch = patches.get(cachePathKey(at));
129
+ return patch === void 0 ? nested : { ...withoutCacheField(nested), ...Option.match(patch, { onNone: () => ({}), onSome: (control) => ({ cache_control: control }) }) };
130
+ }) : value, messages = cacheField(payload, "messages");
56
131
  return {
57
- ...options,
58
- prompt: Prompt.fromMessages(stamped)
132
+ ...withoutCacheField(payload),
133
+ ...cacheField(payload, "tools") === void 0 ? {} : { tools: blocks(cacheField(payload, "tools"), ["tools"]) },
134
+ ...cacheField(payload, "system") === void 0 ? {} : { system: blocks(cacheField(payload, "system"), ["system"]) },
135
+ ...Array.isArray(messages) ? { messages: messages.map((message, index) => Predicate.isObject(message) ? { ...message, content: blocks(cacheField(message, "content"), ["messages", index, "content"]) } : message) } : {},
136
+ ...Option.match(topLevel, { onNone: () => capability && cacheField(payload, "cache_control") === null ? { cache_control: null } : {}, onSome: (control) => ({ cache_control: control }) })
59
137
  };
60
- };
61
- const missingKey = (selection) => new AuthError({
138
+ }, finalizeCachePayload = (payload) => Effect.gen(function* () {
139
+ const frame = yield* CurrentPromptCacheFrame, slots = preparedCacheSlots(payload), origin = (slot) => Option.fromNullishOr(frame.candidates.find((candidate) => candidate.control === slot.control));
140
+ if (slots.some((slot) => !slot.cacheable && slot.control != null && Option.isNone(origin(slot))))
141
+ return yield* Effect.fail(cacheFailure(new PromptCacheError({ reason: "Control", message: "a prompt cache control needs a nonempty cacheable content block" })));
142
+ const eligible = slots.filter((slot) => slot.cacheable), plan = yield* Effect.fromResult(planBreakpoints({
143
+ positions: eligible.map((slot) => Option.match(origin(slot), {
144
+ onSome: ({ priority }) => ({ _tag: "Auto", priority }),
145
+ onNone: () => slot.control == null ? { _tag: "Unmarked" } : { _tag: "Caller", control: slot.control }
146
+ })),
147
+ policy: frame.policy,
148
+ capability: frame.capability,
149
+ callerTopLevel: cacheField(payload, "cache_control")
150
+ })).pipe(Effect.mapError(cacheFailure)), selected = new Set(eligible.flatMap((slot, index) => plan.selectedAuto.includes(index) ? [cachePathKey(slot.path)] : [])), patches = new Map(slots.flatMap((slot) => Option.isSome(origin(slot)) ? [[cachePathKey(slot.path), selected.has(cachePathKey(slot.path)) ? Option.map(origin(slot), (candidate) => candidate.control) : Option.none()]] : !frame.capability ? [[cachePathKey(slot.path), Option.none()]] : []));
151
+ return rewriteCachePayload(payload, patches, plan.topLevel, frame.capability);
152
+ }), withCachePlanning = (client) => ({
153
+ ...client,
154
+ createMessage: (options) => finalizeCachePayload(options.payload).pipe(Effect.flatMap((payload) => client.createMessage({ ...options, payload }))),
155
+ createMessageStream: (options) => finalizeCachePayload(options.payload).pipe(Effect.flatMap((payload) => client.createMessageStream({ ...options, payload })))
156
+ }), missingKey = (selection) => new AuthError({
62
157
  provider: selection.provider,
63
158
  message: `no credential for ${selection.provider} \u2014 sign in through the AuthStore (storage "local" keeps it in ~/.efferent/runtime/auth.json), or give the models plugin one in credentials or credentialEnv (storage "memory")`
64
- }), openCodeResponsesConfig = (cacheKey, policy) => ({
65
- strictJsonSchema: !1,
66
- prompt_cache_key: Option.getOrElse(cacheKey, () => "efferent"),
67
- ...Option.match(policy, {
68
- onNone: () => ({}),
69
- onSome: (value) => ({
70
- ...value.maxOutputTokens === void 0 ? {} : { max_output_tokens: value.maxOutputTokens },
71
- reasoning: {
72
- effort: value.effort === "xhigh" || value.effort === "max" ? "high" : value.effort === "none" ? "low" : value.effort
73
- }
74
- })
159
+ }), notWired = (selection) => new AuthError({
160
+ provider: selection.provider,
161
+ message: `provider "${selection.provider}" is not wired (built in: ${BuiltInProvider.literals.join(", ")}; add others as models plugin endpoints)`
162
+ }), responsesPolicy = (policy, sendsEffort) => Option.match(policy, {
163
+ onNone: () => ({}),
164
+ onSome: (value) => ({
165
+ ...value.maxOutputTokens === void 0 ? {} : { max_output_tokens: value.maxOutputTokens },
166
+ ...sendsEffort ? { reasoning: { effort: publicReasoningEffort(value.effort) } } : {}
75
167
  })
76
168
  });
77
- export const chatCompletionsUrl = (baseUrl) => `${baseUrl.replace(/\/+$/, "")}/chat/completions`, endpointOwnKey = (endpoint) => Option.fromNullishOr(endpoint.apiKey).pipe(Option.map((key) => Redacted.isRedacted(key) ? key : Redacted.make(key)), Option.orElse(() => Option.flatMap(Option.fromNullishOr(endpoint.apiKeyEnv), (name) => Option.map(Option.fromNullishOr(process.env[name]), (value) => Redacted.make(value)))));
78
- const endpointKey = (endpoint, stored) => endpointOwnKey(endpoint).pipe(Option.orElse(() => Option.fromNullishOr(stored)), Option.map(Redacted.value), Option.filter((value) => value.length > 0)), compatFetch = (transport) => Option.match(transport, { onNone: () => ({}), onSome: ({ fetch }) => ({ fetchImpl: fetch }) }), codexWebSocketProxy = (transport) => Option.match(Option.flatMap(transport, (found) => Option.fromNullishOr(found.webSocketAgent)), {
169
+ export const chatCompletionsUrl = (baseUrl) => `${baseUrl.replace(/\/+$/, "")}/chat/completions`, endpointOwnKey = (endpoint) => Option.match(Option.fromNullishOr(endpoint.apiKey), {
170
+ onSome: (key) => Effect.succeed(Option.some(Redacted.isRedacted(key) ? key : Redacted.make(key))),
171
+ onNone: () => Option.match(Option.fromNullishOr(endpoint.apiKeyEnv), {
172
+ onNone: () => Effect.succeedNone,
173
+ onSome: (name) => Config.option(Config.Redacted(name))
174
+ })
175
+ });
176
+ const endpointKey = (selection, endpoint, stored) => endpointOwnKey(endpoint).pipe(Effect.mapError((error) => new AuthError({ provider: selection.provider, message: `cannot read the endpoint's key variable: ${error.message}` })), Effect.map((own) => own.pipe(Option.orElse(() => Option.fromNullishOr(stored)), Option.map(Redacted.value), Option.filter((value) => value.length > 0))));
177
+ export const providerServices = Effect.all({
178
+ endpoints: Effect.service(CurrentModelEndpoints),
179
+ modelSettings: Effect.service(CurrentModelSettings),
180
+ transport: Effect.serviceOption(ModelsTransport),
181
+ managedTransport: Effect.service(CurrentManagedModelsTransport),
182
+ sessions: Effect.service(CurrentOpenAiCodexSessions),
183
+ thinkingPrefixMismatch: Effect.service(CurrentThinkingPrefixMismatch)
184
+ }, { concurrency: 1 });
185
+ const compatFetch = (transport, managed) => Option.match(Option.filter(transport, (value) => !Option.contains(managed, value)), { onNone: () => ({}), onSome: ({ fetch }) => ({ fetchImpl: fetch }) }), codexWebSocketProxy = (selection, transport) => Option.match(Option.flatMap(transport, (found) => Option.fromNullishOr(found.webSocketAgent)), {
79
186
  onNone: () => Effect.succeed(Option.none()),
80
- onSome: (route) => route(new URL(OPENAI_CODEX_API_URL)).pipe(Effect.mapError((error) => new AuthError({ provider: "openai-codex", message: error.message })))
81
- }), endpointModel = (selection, url, key, standardOnly, images, transport) => makeCompatLanguageModel({
187
+ onSome: (route) => route(new URL(OPENAI_CODEX_API_URL)).pipe(Effect.mapError((error) => new AuthError({ provider: selection.provider, message: error.message })))
188
+ }), endpointModel = (selection, url, key, standardOnly, images, transport, managedTransport) => makeCompatLanguageModel({
82
189
  moduleName: selection.provider,
83
190
  chatUrl: chatCompletionsUrl(url),
84
191
  model: selection.modelId,
85
192
  standardOnly,
86
193
  images,
87
- ...compatFetch(transport),
194
+ ...compatFetch(transport, managedTransport),
88
195
  ...Option.match(key, { onNone: () => ({}), onSome: (apiKey) => ({ apiKey }) })
89
- }).pipe(Effect.map((svc) => ({ svc, prependClaudeCode: !1, images }))), buildProviderService = (selection, credential, key) => Effect.flatMap(Effect.all({ endpoints: Effect.service(CurrentModelEndpoints), transport: Effect.serviceOption(ModelsTransport) }), ({ endpoints, transport }) => {
90
- const endpoint = Option.fromNullishOr(endpoints[selection.provider]);
91
- if (Option.isSome(endpoint))
92
- return endpointModel(selection, endpoint.value.url, endpointKey(endpoint.value, key), endpoint.value.standardOnly ?? !0, endpoint.value.images ?? !1, transport);
93
- if (credential?.type === "local" && credential.baseUrl !== void 0)
94
- return endpointModel(selection, credential.baseUrl, Option.none(), !0, !1, transport);
95
- return buildBuiltInProvider(selection, credential, key, transport);
96
- }), buildBuiltInProvider = (selection, credential, key, transport) => {
97
- const oauth = credential?.type === "oauth", images = imageInputFor(formatModelSelection(selection));
98
- if (selection.provider === "opencode") {
99
- if (key === void 0)
100
- return Effect.fail(missingKey(selection));
101
- if (usesOpenCodeResponses(selection.modelId))
102
- return Effect.all({
103
- cacheKey: Effect.service(CurrentPromptCacheKey),
104
- policy: Effect.service(CurrentModelCallPolicy)
105
- }).pipe(Effect.flatMap(({ cacheKey, policy }) => OpenAiClient.make({ apiKey: key, apiUrl: OPENCODE_RESPONSES_API_URL }).pipe(Effect.flatMap((client) => OpenAiLanguageModel.make({
106
- model: selection.modelId,
107
- config: openCodeResponsesConfig(cacheKey, policy)
108
- }).pipe(Effect.provideService(OpenAiClient.OpenAiClient, client))), Effect.map((svc) => ({ svc, prependClaudeCode: !1, images })))));
109
- return makeCompatLanguageModel({
110
- moduleName: "OpenCode",
111
- chatUrl: OPENCODE_CHAT_URL,
112
- apiKey: Redacted.value(key),
113
- model: selection.modelId,
114
- images,
115
- ...compatFetch(transport)
116
- }).pipe(Effect.map((svc) => ({ svc, prependClaudeCode: !1, images })));
117
- }
118
- if (selection.provider === "google")
119
- return key === void 0 ? Effect.fail(missingKey(selection)) : makeCompatLanguageModel({
120
- moduleName: "Google",
121
- chatUrl: GOOGLE_CHAT_URL,
122
- apiKey: Redacted.value(key),
123
- model: selection.modelId,
124
- standardOnly: !0,
125
- usage: { reasoning: !1 },
126
- images,
127
- ...compatFetch(transport)
128
- }).pipe(Effect.map((svc) => ({ svc, prependClaudeCode: !1, images })));
129
- if (selection.provider === "anthropic") {
130
- if (key === void 0)
131
- return Effect.fail(missingKey(selection));
132
- return (oauth ? AnthropicClient.make({
133
- apiKey: void 0,
134
- transformClient: anthropicOAuthTransform(key)
135
- }) : AnthropicClient.make({ apiKey: key })).pipe(Effect.flatMap((client) => AnthropicLanguageModel.make({ model: selection.modelId }).pipe(Effect.provideService(AnthropicClient.AnthropicClient, client))), Effect.map((svc) => ({ svc, prependClaudeCode: oauth, images })));
136
- }
137
- if (selection.provider === "openai-codex") {
138
- if (key === void 0 || credential?.type !== "oauth")
139
- return Effect.fail(missingKey(selection));
140
- const accountId = Option.orElse(Option.fromNullishOr(credential.accountId), () => openAiCodexAccountId(Redacted.value(key)));
141
- if (Option.isNone(accountId))
142
- return Effect.fail(new AuthError({ provider: selection.provider, message: "the OpenAI subscription token has no ChatGPT account id \u2014 sign in to the subscription again and store the new token" }));
143
- return codexWebSocketProxy(transport).pipe(Effect.flatMap((webSocketProxy) => makeOpenAiCodexLanguageModel({ model: selection.modelId, accessToken: key, accountId: accountId.value, webSocketProxy })), Effect.map((svc) => ({ svc, prependClaudeCode: !1, images })));
196
+ }).pipe(Effect.map((svc) => ({ svc, settings: Option.none(), prependClaudeCode: !1, images, route: "compat" }))), buildProviderService = (selection, credential, key, services) => {
197
+ const endpoint = Object.hasOwn(services.endpoints, selection.provider) ? Option.fromNullishOr(services.endpoints[selection.provider]) : Option.none();
198
+ if (Option.isSome(endpoint)) {
199
+ const configured = endpoint.value;
200
+ return Effect.flatMap(endpointKey(selection, configured, key), (own) => {
201
+ if (configured.api === "anthropic-messages") {
202
+ const model = anthropicProfileOf(selection.modelId);
203
+ return anthropicModel({ selection, model, modelSettings: services.modelSettings, thinkingPrefixMismatch: services.thinkingPrefixMismatch }, {
204
+ _tag: "Api",
205
+ key: Option.map(own, Redacted.make),
206
+ endpoint: Option.some(configured)
207
+ }).pipe(Effect.map((built) => ({ ...built, images: configured.images ?? model.images, route: "anthropic" })));
208
+ }
209
+ return otherRouteSettings(selection, services.modelSettings).pipe(Effect.andThen(endpointModel(selection, configured.url, own, configured.standardOnly ?? !0, configured.images ?? !1, services.transport, services.managedTransport)));
210
+ });
144
211
  }
145
- if (selection.provider === "openai")
146
- return key === void 0 ? Effect.fail(missingKey(selection)) : Effect.flatMap(Effect.service(CurrentPromptCacheKey), (cacheKey) => OpenAiClient.make({ apiKey: key }).pipe(Effect.flatMap((client) => OpenAiLanguageModel.make({
212
+ if (credential?.type === "local" && credential.baseUrl !== void 0)
213
+ return otherRouteSettings(selection, services.modelSettings).pipe(Effect.andThen(endpointModel(selection, credential.baseUrl, Option.none(), !0, !1, services.transport, services.managedTransport)));
214
+ return buildBuiltInProvider(selection, credential, key, services);
215
+ };
216
+ export const anthropicBaseUrl = (base) => base.replace(/\/+$/, "").replace(/\/v1$/, ""), modelSettingsFailure = (error) => AiError.make({
217
+ module: "Models",
218
+ method: "settings",
219
+ reason: new AiError.InvalidRequestError({ description: error.message })
220
+ });
221
+ const otherRouteSettings = (selection, settings) => Object.hasOwn(settings, formatModelSelection(selection)) && Object.keys(settings[formatModelSelection(selection)] ?? {}).length > 0 ? Effect.fail(new ModelSettingsError({ reason: "Selection", message: "per-model settings require an Anthropic Messages route for this selection" })) : Effect.void, forcedTool = (choice) => choice === "required" || Predicate.isObject(choice) && (("tool" in choice) || choice.mode === "required"), pinnedAnthropicNativeJson = (model) => typeof model !== "string" || ["claude-sonnet-4-5", "claude-opus-4-5", "claude-haiku-4-5", "claude-opus-4-1"].some((id) => model.includes(id)) || !["claude-sonnet-4-0", "claude-sonnet-4-20250514", "claude-opus-4-0", "claude-opus-4-20250514", "claude-3-"].some((id) => model.includes(id)), withAnthropicSettings = (model, resolved, wire, binding, capability) => {
222
+ const prepare = (options, object = !1) => Effect.flatMap(Effect.all([Effect.serviceOption(AnthropicLanguageModel.Config), Effect.service(CurrentModelCallPurpose)], { concurrency: 1 }), ([ambient, purpose]) => {
223
+ const { thinking: _thinking, output_config: _outputConfig, ...unowned } = Option.getOrElse(ambient, () => ({})), nativeJson = unowned.structuredOutputs ?? pinnedAnthropicNativeJson(wire.model);
224
+ return Effect.fromResult(validateAnthropicRequest(resolved, { forcedTool: forcedTool(options.toolChoice) || object && !nativeJson, binding })).pipe(Effect.mapError(modelSettingsFailure), Effect.map(() => ({ config: { ...unowned, ...wire }, ...prepareCachePrompt(options, purpose === "Helper" ? { _tag: "Off" } : resolved.promptCache, capability) })));
225
+ });
226
+ return {
227
+ ...model,
228
+ generateText: (options) => prepare(options).pipe(Effect.flatMap(({ config, options: shaped, frame }) => model.generateText(shaped).pipe(Effect.provideService(AnthropicLanguageModel.Config, config), Effect.provideService(CurrentPromptCacheFrame, frame)))),
229
+ generateObject: (options) => prepare(options, !0).pipe(Effect.flatMap(({ config, options: shaped, frame }) => model.generateObject(shaped).pipe(Effect.provideService(AnthropicLanguageModel.Config, config), Effect.provideService(CurrentPromptCacheFrame, frame)))),
230
+ streamText: (options) => Stream.unwrap(prepare(options).pipe(Effect.map(({ config, options: shaped, frame }) => model.streamText(shaped).pipe(Stream.provideService(AnthropicLanguageModel.Config, config), Stream.provideService(CurrentPromptCacheFrame, frame)))))
231
+ };
232
+ };
233
+ export const resolveMessagesConfiguration = (selection, modelSettings, policy, thinkingPrefixMismatch, endpoint) => Result.flatMap(resolveAnthropicSettings(formatModelSelection(selection), modelSettings, policy), (resolved) => {
234
+ const binding = bindingFor(anthropicProfileOf(selection.modelId).bindsThinking && Option.exists(resolved.thinking, (value) => value._tag === "Adaptive"), thinkingPrefixMismatch);
235
+ return Result.map(validateAnthropicRequest(resolved, { forcedTool: !1, binding: Option.isSome(binding) }), () => {
236
+ const wire = {
147
237
  model: selection.modelId,
148
- config: {
149
- prompt_cache_key: Option.getOrElse(cacheKey, () => "efferent")
238
+ max_tokens: resolved.maxOutputTokens,
239
+ ...Option.match(resolved.effort, { onNone: () => ({}), onSome: (effort) => ({ output_config: { effort } }) }),
240
+ ...Option.match(resolved.thinking, { onNone: () => ({}), onSome: (thinking) => ({ thinking: ModelThinking.match(thinking, {
241
+ Adaptive: (value) => ({ type: "adaptive", display: value.display ?? "summarized", ...Option.match(binding, { onNone: () => ({}), onSome: (behavior) => ({ block_binding: { prefix_mismatch_behavior: behavior } }) }) }),
242
+ Budget: (value) => ({ type: "enabled", budget_tokens: value.budgetTokens, display: value.display ?? "summarized" }),
243
+ BetweenTools: () => ({ type: "between_tools" }),
244
+ Off: () => ({ type: "disabled" })
245
+ }) }) })
246
+ }, settings = {
247
+ ...wire,
248
+ promptCache: resolved.promptCache,
249
+ cacheControl: Option.match(endpoint, { onNone: () => !0, onSome: (value) => value.cacheControl ?? !0 }),
250
+ ...Option.match(endpoint, { onNone: () => ({}), onSome: (value) => ({ endpoint: { api: value.api, url: anthropicBaseUrl(value.url), auth: value.auth ?? "x-api-key", apiVersion: value.apiVersion ?? "2023-06-01", betas: value.betas ?? [], cacheControl: value.cacheControl ?? !0 } }) }),
251
+ ...Option.match(binding, { onNone: () => ({}), onSome: (behavior) => ({ thinkingPrefixMismatch: behavior }) })
252
+ };
253
+ return { resolved, binding, wire, settings };
254
+ });
255
+ });
256
+ const requestClient = (client, protocol) => resilientHttpClient(client, { protocol, adaptResponse: (response) => response }), anthropicModel = ({ selection, modelSettings, thinkingPrefixMismatch }, authentication) => Effect.gen(function* () {
257
+ const endpoint = Match.valueTags(authentication, { Api: (value) => value.endpoint, Subscription: () => Option.none() }), { resolved, binding, wire, settings } = yield* Effect.fromResult(resolveMessagesConfiguration(selection, modelSettings, yield* CurrentModelCallPolicy, thinkingPrefixMismatch, endpoint)), bound = (client) => Option.match(binding, {
258
+ onNone: () => client,
259
+ onSome: () => client.pipe(withAnthropicBetas([THINKING_BINDING_BETA]), reportingDroppedThinking(formatModelSelection(selection)))
260
+ }), options = Match.valueTags(authentication, {
261
+ Subscription: ({ access }) => ({ transformClient: (client) => bound(requestClient(anthropicOAuthTransform(access)(client), "anthropic")) }),
262
+ Api: ({ key, endpoint }) => ({
263
+ ...Option.match(key, { onNone: () => ({}), onSome: (apiKey) => Option.exists(endpoint, (value) => value.auth === "bearer") ? {} : { apiKey } }),
264
+ ...Option.match(endpoint, { onNone: () => ({}), onSome: (value) => ({ apiUrl: anthropicBaseUrl(value.url), ...value.apiVersion === void 0 ? {} : { apiVersion: value.apiVersion } }) }),
265
+ transformClient: (client) => {
266
+ const authenticated = Option.match(Option.filter(endpoint, (value) => value.auth === "bearer"), {
267
+ onNone: () => client,
268
+ onSome: () => Option.match(key, { onNone: () => client, onSome: (apiKey) => client.pipe(HttpClient.mapRequest((request) => HttpClientRequest.setHeader(request, "Authorization", `Bearer ${Redacted.value(apiKey)}`))) })
269
+ });
270
+ return bound(requestClient(authenticated.pipe(withAnthropicBetas(Option.match(endpoint, { onNone: () => [], onSome: (value) => value.betas ?? [] }))), "anthropic"));
150
271
  }
151
- }).pipe(Effect.provideService(OpenAiClient.OpenAiClient, client))), Effect.map((svc) => ({ svc, prependClaudeCode: !1, images }))));
152
- return Effect.fail(new AuthError({
153
- provider: selection.provider,
154
- message: `provider "${selection.provider}" is not wired (built in: opencode, google, anthropic, openai, openai-codex; add others as models plugin endpoints)`
155
- }));
156
- };
157
- export const buildProvider = (selection, credential, key) => buildProviderService(selection, credential, key).pipe(Effect.flatMap((built) => Effect.gen(function* () {
158
- const cacheKey = yield* Effect.service(CurrentPromptCacheKey), policy = yield* Effect.service(CurrentModelCallPolicy), responses = selection.provider === "opencode" && usesOpenCodeResponses(selection.modelId), settings = selection.provider === "anthropic" ? { systemPrelude: built.prependClaudeCode ? CLAUDE_CODE_SYSTEM : null } : responses ? openCodeResponsesConfig(cacheKey, policy) : { prompt_cache_key: Option.getOrElse(cacheKey, () => "efferent") }, descriptor = { provider: selection.provider, model: selection.modelId, settings };
272
+ })
273
+ }), client = withAnthropicFailures(withCachePlanning(yield* AnthropicClient.make(options))), service = yield* AnthropicLanguageModel.make({ model: selection.modelId, config: wire }).pipe(Effect.provideService(AnthropicClient.AnthropicClient, client)), oauth = authentication._tag === "Subscription";
274
+ return {
275
+ svc: withAnthropicSettings(service, resolved, wire, Option.isSome(binding), Option.match(endpoint, { onNone: () => !0, onSome: (value) => value.cacheControl ?? !0 })),
276
+ settings: Option.some({ ...wire, systemPrelude: oauth ? CLAUDE_CODE_SYSTEM : null, ...settings }),
277
+ prependClaudeCode: oauth,
278
+ overflowLimits: { ...resolved.limits, maxOutputTokens: resolved.maxOutputTokens }
279
+ };
280
+ }), responsesModel = (route, { selection, model, key }) => Effect.all({ cacheKey: Effect.service(CurrentPromptCacheKey), policy: Effect.service(CurrentModelCallPolicy) }, { concurrency: 1 }).pipe(Effect.flatMap(({ cacheKey, policy }) => {
281
+ const config = {
282
+ ...Option.match(route.strictJsonSchema, { onNone: () => ({}), onSome: (strictJsonSchema) => ({ strictJsonSchema }) }),
283
+ prompt_cache_key: Option.getOrElse(cacheKey, () => "efferent"),
284
+ ...responsesPolicy(policy, route.effort === "always" || model.efforts.length > 0)
285
+ };
286
+ return OpenAiClient.make({ apiKey: key, transformClient: (client) => requestClient(client, "openai"), ...Option.match(route.apiUrl, { onNone: () => ({}), onSome: (apiUrl) => ({ apiUrl }) }) }).pipe(Effect.flatMap((client) => OpenAiLanguageModel.make({ model: selection.modelId, config }).pipe(Effect.provideService(OpenAiClient.OpenAiClient, withOpenAiFailures(client)))), Effect.map((svc) => ({ svc, settings: Option.some(config), prependClaudeCode: !1 })));
287
+ })), codexModel = ({ selection, credential, key, transport, sessions }) => {
288
+ if (credential?.type !== "oauth")
289
+ return Effect.fail(missingKey(selection));
290
+ const accountId = Option.orElse(Option.fromNullishOr(credential.accountId), () => openAiCodexAccountId(Redacted.value(key)));
291
+ if (Option.isNone(accountId))
292
+ return Effect.fail(new AuthError({ provider: selection.provider, message: "the OpenAI subscription token has no ChatGPT account id \u2014 sign in to the subscription again and store the new token" }));
293
+ return codexWebSocketProxy(selection, transport).pipe(Effect.flatMap((webSocketProxy) => makeOpenAiCodexLanguageModel({ model: selection.modelId, accessToken: key, accountId: accountId.value, webSocketProxy }).pipe(Effect.provideService(CurrentOpenAiCodexSessions, sessions))), Effect.map((svc) => ({ svc, settings: Option.none(), prependClaudeCode: !1 })));
294
+ }, chatModel = (route, { selection, model, key, transport, managedTransport }) => makeCompatLanguageModel({
295
+ moduleName: route.moduleName,
296
+ chatUrl: route.chatUrl,
297
+ apiKey: Redacted.value(key),
298
+ model: selection.modelId,
299
+ standardOnly: route.standardOnly,
300
+ usage: route.usage,
301
+ images: model.images,
302
+ ...compatFetch(transport, managedTransport)
303
+ }).pipe(Effect.map((svc) => ({ svc, settings: Option.none(), prependClaudeCode: !1 }))), buildBuiltInProvider = (selection, credential, key, { transport, managedTransport, sessions, thinkingPrefixMismatch, modelSettings }) => Option.match(profileOf(selection.provider), {
304
+ onNone: () => Effect.fail(notWired(selection)),
305
+ onSome: (profile) => Option.match(Option.fromNullishOr(key), {
306
+ onNone: () => Effect.fail(missingKey(selection)),
307
+ onSome: (found) => {
308
+ const call = { selection, model: profile.modelOf(selection.modelId), credential, key: found, transport, managedTransport, sessions, thinkingPrefixMismatch, modelSettings }, route = profile.routeOf(selection.modelId), built = Match.valueTags(route, {
309
+ anthropic: () => anthropicModel(call, credential?.type === "oauth" ? { _tag: "Subscription", access: found } : { _tag: "Api", key: Option.some(found), endpoint: Option.none() }),
310
+ "openai-responses": (responses) => responsesModel(responses, call),
311
+ "openai-codex": () => codexModel(call),
312
+ compat: (chat) => chatModel(chat, call)
313
+ });
314
+ return Effect.map(route._tag === "anthropic" ? built : otherRouteSettings(selection, modelSettings).pipe(Effect.andThen(built)), (routed) => ({ ...routed, images: call.model.images, route: route._tag }));
315
+ }
316
+ })
317
+ }), descriptorOf = (selection, built) => Option.match(built.settings, {
318
+ onSome: (settings) => Effect.succeed({ provider: selection.provider, model: selection.modelId, settings }),
319
+ onNone: () => modelRequestDescriptorOf(built.svc).pipe(Effect.map(Option.match({
320
+ onNone: () => ({ provider: selection.provider, model: selection.modelId, settings: {} }),
321
+ onSome: (value) => ({ ...value, provider: selection.provider })
322
+ })))
323
+ });
324
+ export const buildProviderOver = (services) => (selection, credential, key) => buildProviderService(selection, credential, key, services).pipe(Effect.map((built) => {
325
+ const classified = withProviderAccounting(withProviderFailures(built.svc, providerFailureProtocol(built.route), selection, built.overflowLimits ?? {}), selection), labeled = built.images ? classified : withImageLabels(classified);
159
326
  return {
160
- ...built,
161
- svc: describeModel(built.images ? built.svc : withImageLabels(built.svc), modelRequestDescriptorOf(built.svc).pipe(Effect.map((existing) => Option.match(existing, { onNone: () => descriptor, onSome: (value) => ({ ...value, provider: selection.provider }) }))))
327
+ svc: describeModel(built.route === "anthropic" ? labeled : withStrippedVolatileContext(labeled), descriptorOf(selection, built)),
328
+ prependClaudeCode: built.prependClaudeCode,
329
+ images: built.images,
330
+ route: built.route
162
331
  };
163
- })));
332
+ })), buildProvider = (selection, credential, key) => Effect.flatMap(providerServices, (services) => buildProviderOver(services)(selection, credential, key));
@@ -1,54 +1,18 @@
1
1
  import { AiError } from "effect/ai";
2
2
  import { Effect, Stream } from "effect";
3
- /**
4
- * Transient-failure resilience for every routed LLM call — the survival
5
- * learnings from the previous line, v1-scoped:
6
- *
7
- * - Retry ONLY transients (429 / 5xx / transport / timeout / empty body) —
8
- * never a 4xx client error or a decode failure, which are permanent.
9
- * - Every request is bounded by a {@link LLM_REQUEST_TIMEOUT_MS} fiber-level
10
- * timeout (official provider clients ship none), classified transient.
11
- * - An HTTP 200 with no text/tool-call/reasoning content is rejected as a
12
- * transient failure (the "empty response that fake-completes a turn
13
- * mid-thought" class) and rides the same retries.
14
- * - A 429 whose `Retry-After` exceeds {@link MAX_HONORED_RETRY_AFTER_MS} is
15
- * a daily quota, not an outage — {@link classifyLlmError} calls it
16
- * PERMANENT so it fails fast instead of burning retries into the same
17
- * wall (the uncapped-Retry-After lesson: never park a run on a quota).
18
- * The fast-retry schedule itself never sleeps on the header.
19
- *
20
- * The previous line's multi-phase patient ladder (30-minute interactive
21
- * outage waits) is deliberately NOT ported yet — it needs a per-run
22
- * interaction policy the engine doesn't carry; add it when live use demands.
23
- */
24
- export declare const LLM_REQUEST_TIMEOUT_MS = 300000;
25
- export declare const MAX_HONORED_RETRY_AFTER_MS = 60000;
3
+ /** Default bound on a streamed request's pending headers; configure CurrentModelRetryPolicy per call. @public */
4
+ export declare const LLM_REQUEST_TIMEOUT_MS: number;
5
+ /** Default total request budget, shared with a configured fallback. @public */
6
+ export declare const LLM_CALL_BUDGET_MS: number;
26
7
  export type ErrorClass = "transient" | "permanent";
27
- export declare const classifyLlmError: (error: unknown) => ErrorClass;
28
- /** Reject an HTTP-200-but-empty response so it retries instead of
29
- * fake-completing the turn — UNLESS the loop has declared tolerance
8
+ /** Semantic fallback eligibility. HTTP retries and Retry-After waits belong to the request policy. */
9
+ export declare const classifyLlmError: (error: unknown, _now?: number) => ErrorClass;
10
+ /** Reject an HTTP-200-but-empty response instead of fake-completing
11
+ * the turn — UNLESS the loop has declared tolerance
30
12
  * ({@link CurrentEmptyResponseTolerance}): once a run has executed tool
31
- * calls, a following empty response is the model finishing, and rejecting
32
- * it would park the turn in the patient outage ladder. */
13
+ * calls, a following empty response is the model finishing. */
33
14
  export declare const rejectEmptyResponse: (label: string) => <A extends {
34
15
  readonly content: ReadonlyArray<unknown>;
35
16
  }, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E | AiError.AiError, R>;
36
- /** Timeout + transient-only retries around one routed LLM connect. */
37
- export declare const retryableLlm: (label: string) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E | AiError.AiError, R>;
38
- /**
39
- * The STREAM twin of {@link retryableLlm}, with semantics a settled call
40
- * doesn't need:
41
- *
42
- * - Retry covers connection + PRE-first-content failures only. Once a
43
- * content part has flowed to the consumer, a replayed stream would emit
44
- * duplicates — so after content, failures are final (a `Ref`-armed gate on
45
- * the retry schedule).
46
- * - The empty guard WITHHOLDS the finish part: a finish with zero content
47
- * parts fails as the transient empty-response error instead of
48
- * fake-completing the turn — and, being pre-content by definition, it
49
- * rides the same retries.
50
- * - The timeout is per-PART idle time (bounds time-to-first-part AND a
51
- * mid-stream hang), not whole-request — a long healthy stream never trips
52
- * it while tokens flow.
53
- */
54
- export declare const retryableLlmStream: (label: string, timeoutMs?: number) => <A, E, R>(stream: Stream.Stream<A, E, R>) => Stream.Stream<A, E | AiError.AiError, R>;
17
+ /** Preserve streamed content and semantic failures; reject an empty finish without replaying its request. */
18
+ export declare const rejectEmptyStream: (label: string) => <A, E, R>(stream: Stream.Stream<A, E, R>) => Stream.Stream<A, E | AiError.AiError, R>;