@xandreed/plugin-models 0.14.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/auth/anthropicOAuth.d.ts +30 -10
- package/dist/auth/anthropicOAuth.js +15 -18
- package/dist/auth/auth-store.conformance.d.ts +63 -0
- package/dist/auth/auth-store.conformance.js +152 -0
- package/dist/auth/credential-input.entity.d.ts +23 -5
- package/dist/auth/credential-input.entity.functions.d.ts +15 -5
- package/dist/auth/credential-input.entity.functions.js +12 -9
- package/dist/auth/credential-input.entity.js +5 -3
- package/dist/auth/credential-refresh.entity.d.ts +11 -3
- package/dist/auth/credential-refresh.entity.functions.d.ts +48 -6
- package/dist/auth/credential-refresh.entity.functions.js +12 -3
- package/dist/auth/localAuth.d.ts +10 -1
- package/dist/auth/localAuth.js +53 -79
- package/dist/auth/memory-auth.adapter.d.ts +12 -4
- package/dist/auth/memory-auth.adapter.js +13 -25
- package/dist/auth/openAiCodexOAuth.d.ts +26 -5
- package/dist/auth/openAiCodexOAuth.js +17 -28
- package/dist/auth/token-response.entity.d.ts +21 -0
- package/dist/auth/token-response.entity.functions.d.ts +20 -0
- package/dist/auth/token-response.entity.functions.js +10 -0
- package/dist/auth/token-response.entity.js +10 -0
- package/dist/compat/index.d.ts +2 -0
- package/dist/compat/index.js +1 -0
- package/dist/conformance/index.d.ts +6 -0
- package/dist/conformance/index.js +3 -0
- package/dist/index.d.ts +19 -19
- package/dist/index.js +17 -69
- package/dist/llm/chat-wire.entity.d.ts +123 -0
- package/dist/llm/chat-wire.entity.functions.d.ts +28 -0
- package/dist/llm/chat-wire.entity.functions.js +10 -0
- package/dist/llm/chat-wire.entity.js +37 -0
- package/dist/llm/compat-telemetry.adapter.d.ts +6 -0
- package/dist/llm/compat-telemetry.adapter.js +21 -0
- package/dist/llm/compat.d.ts +50 -30
- package/dist/llm/compat.js +149 -133
- package/dist/llm/model-completion.adapter.d.ts +49 -0
- package/dist/llm/model-completion.adapter.js +77 -0
- package/dist/llm/model-completion.entity.d.ts +4 -0
- package/dist/llm/model-completion.entity.functions.d.ts +3 -0
- package/dist/llm/model-completion.entity.functions.js +7 -0
- package/dist/llm/model-completion.entity.js +7 -0
- package/dist/llm/model-retry-policy.entity.d.ts +37 -0
- package/dist/llm/model-retry-policy.entity.functions.d.ts +16 -0
- package/dist/llm/model-retry-policy.entity.functions.js +33 -0
- package/dist/llm/model-retry-policy.entity.js +23 -0
- package/dist/llm/model-settings.entity.d.ts +85 -0
- package/dist/llm/model-settings.entity.functions.d.ts +11 -0
- package/dist/llm/model-settings.entity.functions.js +49 -0
- package/dist/llm/model-settings.entity.js +28 -0
- package/dist/llm/model-usage.entity.d.ts +61 -0
- package/dist/llm/model-usage.entity.functions.d.ts +11 -0
- package/dist/llm/model-usage.entity.functions.js +24 -0
- package/dist/llm/model-usage.entity.js +19 -0
- package/dist/llm/modelCatalog.d.ts +29 -8
- package/dist/llm/modelCatalog.js +28 -66
- package/dist/llm/openAiCodex.d.ts +15 -7
- package/dist/llm/openAiCodex.js +57 -45
- package/dist/llm/openAiCodexWebSocket.d.ts +48 -15
- package/dist/llm/openAiCodexWebSocket.js +101 -147
- package/dist/llm/prompt-cache-frame.entity.d.ts +18 -0
- package/dist/llm/prompt-cache-frame.entity.js +10 -0
- package/dist/llm/prompt-cache.entity.d.ts +57 -0
- package/dist/llm/prompt-cache.entity.functions.d.ts +8 -0
- package/dist/llm/prompt-cache.entity.functions.js +73 -0
- package/dist/llm/prompt-cache.entity.js +27 -0
- package/dist/llm/provider-client.adapter.d.ts +9 -0
- package/dist/llm/provider-client.adapter.js +60 -0
- package/dist/llm/provider-failure.entity.d.ts +42 -0
- package/dist/llm/provider-failure.entity.functions.d.ts +8 -0
- package/dist/llm/provider-failure.entity.functions.js +67 -0
- package/dist/llm/provider-failure.entity.js +20 -0
- package/dist/llm/provider-profile.entity.d.ts +79 -0
- package/dist/llm/provider-profile.entity.functions.d.ts +23 -0
- package/dist/llm/provider-profile.entity.functions.js +83 -0
- package/dist/llm/provider-profile.entity.js +10 -0
- package/dist/llm/providers.d.ts +116 -34
- package/dist/llm/providers.js +298 -129
- package/dist/llm/retry.d.ts +11 -47
- package/dist/llm/retry.js +33 -52
- package/dist/llm/router.d.ts +54 -36
- package/dist/llm/router.js +91 -78
- package/dist/llm/sse.d.ts +44 -37
- package/dist/llm/sse.js +43 -35
- package/dist/llm/thinking-binding.entity.d.ts +49 -0
- package/dist/llm/thinking-binding.entity.functions.d.ts +21 -0
- package/dist/llm/thinking-binding.entity.functions.js +16 -0
- package/dist/llm/thinking-binding.entity.js +11 -0
- package/dist/llm/utilityLlm.d.ts +14 -5
- package/dist/llm/utilityLlm.js +18 -13
- package/dist/plugin.adapter.d.ts +94 -6
- package/dist/plugin.adapter.js +18 -13
- package/dist/ports/auth-store.port.d.ts +73 -0
- package/dist/ports/auth-store.port.js +24 -0
- package/dist/ports/host-credentials.port.d.ts +2 -1
- package/dist/ports/model-call-completion.port.d.ts +9 -0
- package/dist/ports/model-call-completion.port.js +2 -0
- package/dist/ports/model-endpoints.port.d.ts +60 -26
- package/dist/ports/model-endpoints.port.js +17 -3
- package/dist/ports/model-retry-policy.port.d.ts +17 -0
- package/dist/ports/model-retry-policy.port.js +3 -0
- package/dist/ports/model-settings.port.d.ts +32 -0
- package/dist/ports/model-settings.port.js +4 -0
- package/dist/ports/models-transport.port.d.ts +9 -2
- package/dist/ports/models-transport.port.js +4 -1
- package/dist/ports/prompt-cache-frame.port.d.ts +21 -0
- package/dist/ports/prompt-cache-frame.port.js +4 -0
- package/dist/ports/settings-store.port.d.ts +70 -0
- package/dist/ports/settings-store.port.js +33 -0
- package/dist/ports/thinking-binding.port.d.ts +7 -0
- package/dist/ports/thinking-binding.port.js +4 -0
- package/dist/settings/localSettings.d.ts +2 -1
- package/dist/settings/localSettings.js +17 -20
- package/dist/settings/memory-settings.adapter.d.ts +3 -1
- package/dist/settings/memory-settings.adapter.js +1 -1
- package/dist/settings/roleView.d.ts +3 -1
- package/dist/settings/roleView.js +1 -1
- package/dist/settings/settings-store.conformance.d.ts +47 -0
- package/dist/settings/settings-store.conformance.js +100 -0
- package/dist/settings/settings-value.entity.d.ts +10 -2
- package/dist/settings/settings-value.entity.functions.d.ts +31 -9
- package/dist/settings/settings-value.entity.functions.js +5 -6
- package/dist/storage/json-file.d.ts +18 -0
- package/dist/storage/json-file.js +26 -0
- package/dist/transport/http-proxy.entity.d.ts +17 -3
- package/dist/transport/model-request.conformance.d.ts +13 -0
- package/dist/transport/model-request.conformance.js +36 -0
- package/dist/transport/models-transport.adapter.d.ts +18 -17
- package/dist/transport/models-transport.adapter.js +28 -31
- package/dist/transport/resilient-http-client.adapter.d.ts +73 -0
- package/dist/transport/resilient-http-client.adapter.js +177 -0
- package/package.json +11 -5
- package/dist/auth/anthropicOAuth.test.d.ts +0 -1
- package/dist/auth/credential-refresh.entity.functions.test.d.ts +0 -1
- package/dist/auth/localAuth.test.d.ts +0 -1
- package/dist/auth/memory-auth.adapter.test.d.ts +0 -1
- package/dist/llm/compat.test.d.ts +0 -1
- package/dist/llm/modelCatalog.test.d.ts +0 -1
- package/dist/llm/openAiCodexWebSocket.test.d.ts +0 -1
- package/dist/llm/providers.test.d.ts +0 -1
- package/dist/llm/retry.test.d.ts +0 -1
- package/dist/llm/router.test.d.ts +0 -1
- package/dist/llm/sse.test.d.ts +0 -1
- package/dist/plugin.adapter.test.d.ts +0 -1
- package/dist/settings/localSettings.test.d.ts +0 -1
- package/dist/settings/memory-settings.adapter.test.d.ts +0 -1
- package/dist/settings/roleView.test.d.ts +0 -1
- package/dist/settings/settings-value.entity.functions.test.d.ts +0 -1
- package/dist/transport/http-proxy.entity.functions.test.d.ts +0 -1
- package/dist/transport/models-transport.adapter.test.d.ts +0 -1
- package/dist/transport/recording-proxy.testing.d.ts +0 -33
package/dist/llm/providers.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
},
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
...
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
...
|
|
58
|
-
|
|
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
|
|
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
|
-
}),
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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.
|
|
78
|
-
|
|
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:
|
|
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
|
|
90
|
-
const endpoint = Option.fromNullishOr(endpoints[selection.provider]);
|
|
91
|
-
if (Option.isSome(endpoint))
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return Effect.
|
|
103
|
-
|
|
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 (
|
|
146
|
-
return
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
})
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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));
|
package/dist/llm/retry.d.ts
CHANGED
|
@@ -1,54 +1,18 @@
|
|
|
1
1
|
import { AiError } from "effect/ai";
|
|
2
2
|
import { Effect, Stream } from "effect";
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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
|
-
/**
|
|
37
|
-
export declare const
|
|
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>;
|