@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/retry.js
CHANGED
|
@@ -1,58 +1,39 @@
|
|
|
1
1
|
import { AiError } from "effect/ai";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (
|
|
11
|
-
return
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}, rateLimited = (waitMs) => waitMs !== void 0 && waitMs > MAX_HONORED_RETRY_AFTER_MS ? "permanent" : "transient";
|
|
15
|
-
export const classifyLlmError = (error) => {
|
|
16
|
-
if (!AiError.isAiError(error)) {
|
|
17
|
-
const e = error;
|
|
18
|
-
return e?._tag === "HttpClientError" && e.reason?._tag === "TransportError" ? "transient" : "permanent";
|
|
19
|
-
}
|
|
2
|
+
import { HttpClientError } from "effect/http";
|
|
3
|
+
import { Effect, Option, Predicate, Stream } from "effect";
|
|
4
|
+
import { CurrentEmptyResponseTolerance, modelFailureOf, stampAiErrorFailure } from "@xandreed/core";
|
|
5
|
+
import { modelRetryDefaults } from "./model-retry-policy.entity.js";
|
|
6
|
+
export const LLM_REQUEST_TIMEOUT_MS = modelRetryDefaults.requestTimeoutMs, LLM_CALL_BUDGET_MS = modelRetryDefaults.totalTimeoutMs, classifyLlmError = (error, _now = 0) => {
|
|
7
|
+
const failure = modelFailureOf(error);
|
|
8
|
+
if (Option.exists(failure, (kind) => ["ContextOverflow", "Authentication", "InvalidRequest", "Quota", "InvalidResponse"].includes(kind)))
|
|
9
|
+
return "permanent";
|
|
10
|
+
if (Option.exists(failure, (kind) => ["RateLimit", "Transient", "Timeout"].includes(kind)))
|
|
11
|
+
return "transient";
|
|
12
|
+
if (!AiError.isAiError(error))
|
|
13
|
+
return HttpClientError.isHttpClientError(error) && error.reason._tag === "TransportError" ? "transient" : "permanent";
|
|
20
14
|
const reason = error.reason, response = "http" in reason ? reason.http?.response : void 0;
|
|
21
|
-
if (response !== void 0)
|
|
22
|
-
|
|
23
|
-
return rateLimited(retryAfterMillis(response.headers));
|
|
24
|
-
return response.status >= 500 ? "transient" : "permanent";
|
|
25
|
-
}
|
|
26
|
-
if (reason._tag === "RateLimitError")
|
|
27
|
-
return rateLimited(reason.retryAfter === void 0 ? void 0 : Duration.toMillis(reason.retryAfter));
|
|
15
|
+
if (response !== void 0)
|
|
16
|
+
return response.status === 429 || response.status >= 500 ? "transient" : "permanent";
|
|
28
17
|
return reason._tag === "InternalProviderError" || reason._tag === "NetworkError" || reason._tag === "UnknownError" ? "transient" : "permanent";
|
|
29
18
|
};
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
19
|
+
const emptyError = (label, method) => stampAiErrorFailure(AiError.make({
|
|
20
|
+
module: "Router",
|
|
21
|
+
method,
|
|
22
|
+
reason: new AiError.UnknownError({ description: `${label} returned an empty response (no text, tool call, or reasoning) \u2014 treated as a transient provider failure` })
|
|
23
|
+
}), "Transient"), partType = (part) => Predicate.hasProperty(part, "type") && Predicate.isString(part.type) ? part.type : "", isContentPart = (part) => {
|
|
24
|
+
const type = partType(part);
|
|
25
|
+
return type === "text" || type === "tool-call" || type === "reasoning";
|
|
36
26
|
};
|
|
37
|
-
export const rejectEmptyResponse = (label) => (effect) => Effect.service(CurrentEmptyResponseTolerance).pipe(Effect.flatMap((tolerant) => tolerant ? effect : effect.pipe(Effect.filterOrFail((res) => res.content.some(isContentPart), () => emptyError(label, "generateText")))))
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
schedule: fastRetries,
|
|
42
|
-
while: (error) => classifyLlmError(error) === "transient"
|
|
43
|
-
}), Effect.tapError((error) => classifyLlmError(error) === "transient" ? Effect.logWarning(`${label}: transient failure exhausted retries: ${String(error)}`) : Effect.void));
|
|
44
|
-
const isContentPartEncoded = (part) => {
|
|
45
|
-
const p = part;
|
|
46
|
-
if (p === null || typeof p !== "object")
|
|
47
|
-
return !1;
|
|
48
|
-
if (p.type === "tool-call")
|
|
27
|
+
export const rejectEmptyResponse = (label) => (effect) => Effect.service(CurrentEmptyResponseTolerance).pipe(Effect.flatMap((tolerant) => tolerant ? effect : effect.pipe(Effect.filterOrFail((res) => Option.isSome(modelFailureOf(res.content)) || res.content.some(isContentPart), () => emptyError(label, "generateText")))));
|
|
28
|
+
const carriesMetadata = (part) => Predicate.hasProperty(part, "metadata") && Predicate.isObject(part.metadata) && Object.keys(part.metadata).length > 0, isStreamContentPart = (part) => {
|
|
29
|
+
const type = partType(part);
|
|
30
|
+
if (type === "tool-call")
|
|
49
31
|
return !0;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}));
|
|
32
|
+
if ((type === "reasoning-start" || type === "reasoning-delta" || type === "reasoning-end") && carriesMetadata(part))
|
|
33
|
+
return !0;
|
|
34
|
+
return (type === "text-delta" || type === "reasoning-delta") && Predicate.hasProperty(part, "delta") && Predicate.isString(part.delta) && part.delta.length > 0;
|
|
35
|
+
}, isFinishPart = (part) => partType(part) === "finish";
|
|
36
|
+
export const rejectEmptyStream = (label) => (stream) => Stream.unwrap(Effect.map(Effect.service(CurrentEmptyResponseTolerance), (tolerant) => tolerant ? stream : stream.pipe(Stream.mapAccumEffect(() => !1, (seen, part) => {
|
|
37
|
+
const content = seen || Option.isSome(modelFailureOf(part)) || isStreamContentPart(part);
|
|
38
|
+
return !content && isFinishPart(part) ? Effect.fail(emptyError(label, "streamText")) : Effect.succeed([content, [part]]);
|
|
39
|
+
}))));
|
package/dist/llm/router.d.ts
CHANGED
|
@@ -1,34 +1,47 @@
|
|
|
1
|
-
import { LanguageModel } from "effect/ai";
|
|
1
|
+
import { AiError, LanguageModel } from "effect/ai";
|
|
2
|
+
import type { Response } from "effect/ai";
|
|
2
3
|
import { HttpClient } from "effect/http";
|
|
3
4
|
import { Effect, Layer, Option, Stream } from "effect";
|
|
4
|
-
import { AuthStore
|
|
5
|
+
import { AuthStore } from "../ports/auth-store.port.js";
|
|
6
|
+
import { SettingsStore } from "../ports/settings-store.port.js";
|
|
5
7
|
import type { ModelSelection } from "@xandreed/core";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
import type { EngineSettings } from "../ports/settings-store.port.js";
|
|
9
|
+
import type { PromptOptions, ProviderServices } from "./providers.js";
|
|
10
|
+
import type { ModelRequestCall } from "../transport/resilient-http-client.adapter.js";
|
|
11
|
+
/** Apply the persisted model effort only when a dedicated agent has not
|
|
12
|
+
* already pinned a complete call policy in its own execution profile. */
|
|
13
|
+
export declare const withConfiguredEffort: <A, E, R>(effect: Effect.Effect<A, E, R>, effort: EngineSettings["reasoningEffort"]) => Effect.Effect<A, E, R>;
|
|
14
|
+
/** One routed call's settled response, as {@link generateOver} returns it. */
|
|
15
|
+
type Generated = {
|
|
10
16
|
readonly content: ReadonlyArray<unknown>;
|
|
11
17
|
readonly usage: unknown;
|
|
12
|
-
}
|
|
18
|
+
};
|
|
19
|
+
/** Build + call one provider generateText for an explicit selection, over
|
|
20
|
+
* the given provider services (a routed model's, read where its layer was
|
|
21
|
+
* built). `isFallback` only labels telemetry — the fallback rung must be
|
|
22
|
+
* visible in Grafana without changing the metric identities. `budget` is
|
|
23
|
+
* the routed call's, shared with its fallback rung; a call made without
|
|
24
|
+
* one gets its own. */
|
|
25
|
+
export declare const generateOver: (services: ProviderServices) => <O extends PromptOptions>(selection: ModelSelection, options: O, isFallback?: boolean, budget?: ModelRequestCall) => Effect.Effect<Generated, AiError.AiError, AuthStore | HttpClient.HttpClient>;
|
|
26
|
+
/** {@link generateOver} over the provider services where it runs
|
|
27
|
+
* (`CurrentModelEndpoints`, `CurrentModelSettings`, `ModelsTransport`), for a call made outside a
|
|
28
|
+
* routed model. */
|
|
29
|
+
export declare const generateWith: <O extends PromptOptions>(selection: ModelSelection, options: O, isFallback?: boolean, budget?: ModelRequestCall) => Effect.Effect<Generated, AiError.AiError, AuthStore | HttpClient.HttpClient>;
|
|
30
|
+
/** The fallback rung uses its reserved last attempt only after eligible primary
|
|
31
|
+
* HTTP failures exhaust their allowance. A committed response, permanent failure
|
|
32
|
+
* or elapsed deadline cannot start another model call. */
|
|
33
|
+
export declare const withFallbackRung: <A, E, R>(primary: ModelSelection, fallback: Option.Option<ModelSelection>, call: (selection: ModelSelection, isFallback: boolean) => Effect.Effect<A, E, R>, budget: ModelRequestCall) => Effect.Effect<A, E | AiError.AiError, R>;
|
|
13
34
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* through — a fallback can't fix those; an unset or identical fallback is a
|
|
18
|
-
* no-op. Exported pure-in-shape so the rung's decision table is testable
|
|
19
|
-
* without building providers.
|
|
20
|
-
*/
|
|
21
|
-
export declare const withFallbackRung: <A, R>(primary: ModelSelection, fallback: Option.Option<ModelSelection>, call: (selection: ModelSelection, isFallback: boolean) => Effect.Effect<A, unknown, R>) => Effect.Effect<A, unknown, R>;
|
|
22
|
-
/**
|
|
23
|
-
* Stamp the RESOLVED selection onto the finish part — the trail must answer
|
|
24
|
-
* "which model produced this message" from the db alone. The response MUST be
|
|
35
|
+
* Stamp the RESOLVED selection onto the finish part (core's
|
|
36
|
+
* `stampResponseModel`) — the trail must answer "which model produced this
|
|
37
|
+
* message" from the db alone. The response MUST be
|
|
25
38
|
* rebuilt as a real `GenerateTextResponse`: it's a class whose
|
|
26
39
|
* text/finishReason/usage are prototype GETTERS, so a `{...res}` spread
|
|
27
40
|
* silently strips them — finishReason read as undefined and the loop treated
|
|
28
41
|
* every tool turn as "done" (the one-tool-call-then-dead bug, live-caught).
|
|
29
42
|
*/
|
|
30
43
|
export declare const stampResponse: (res: {
|
|
31
|
-
readonly content: ReadonlyArray<
|
|
44
|
+
readonly content: ReadonlyArray<Response.AnyPart>;
|
|
32
45
|
}, label: string) => LanguageModel.GenerateTextResponse<never>;
|
|
33
46
|
/**
|
|
34
47
|
* The stream twin of `generateWith`'s telemetry: accumulate over the flowing
|
|
@@ -37,29 +50,34 @@ export declare const stampResponse: (res: {
|
|
|
37
50
|
* content. Outcome + duration finalize per terminal state (`onDone` /
|
|
38
51
|
* `tapErrorCause`), so retries inside count once, like the settled path.
|
|
39
52
|
*/
|
|
40
|
-
export declare const tapStreamTelemetry: (label: string) => <E, R>(stream: Stream.Stream<
|
|
41
|
-
|
|
42
|
-
* stream twin of {@link generateWith}: same resolution, same option
|
|
43
|
-
* shaping (anthropic cache breakpoints inherit for free), same stamp, the
|
|
44
|
-
* stream-aware retry, and finish-part telemetry. */
|
|
45
|
-
export declare const streamWith: (selection: ModelSelection, options: unknown) => Stream.Stream<unknown, unknown, AuthStore | HttpClient.HttpClient>;
|
|
53
|
+
export declare const tapStreamTelemetry: (label: string) => <A, E, R>(stream: Stream.Stream<A, E, R>) => Stream.Stream<A, E, R>;
|
|
54
|
+
export declare const throughTransport: (services: ProviderServices) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
|
|
46
55
|
/**
|
|
47
|
-
* The
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* `
|
|
51
|
-
*
|
|
52
|
-
*
|
|
56
|
+
* The provider services of a routed model's layer, read once where it is
|
|
57
|
+
* built: the configured endpoints (`CurrentModelEndpoints`), per-model controls
|
|
58
|
+
* (`CurrentModelSettings`), the transport
|
|
59
|
+
* (`ModelsTransport`), and an OpenAI subscription session cache of the
|
|
60
|
+
* layer's own: each conversation keeps its session id while the layer
|
|
61
|
+
* lives, bounded to the conversations used last, and two layers (two agents
|
|
62
|
+
* in one process) never share one.
|
|
53
63
|
*/
|
|
54
|
-
export declare const
|
|
64
|
+
export declare const layerProviderServices: Effect.Effect<ProviderServices>;
|
|
55
65
|
/**
|
|
56
66
|
* `LanguageModelLive` — the engine loop's `LanguageModel`, routed per call.
|
|
57
67
|
* Requires `SettingsStore` + `AuthStore`; brings its own fetch-backed
|
|
58
|
-
* `HttpClient` so drivers don't have to.
|
|
59
|
-
*
|
|
68
|
+
* `HttpClient` so drivers don't have to. Reads `CurrentModelEndpoints`,
|
|
69
|
+
* `CurrentModelSettings` and `ModelsTransport` once, where it is built: built over a transport (as the
|
|
70
|
+
* models plugin builds it), every call goes through that transport.
|
|
71
|
+
*
|
|
72
|
+
* @public
|
|
60
73
|
*/
|
|
61
|
-
export declare const LanguageModelLive: Layer.Layer<LanguageModel.LanguageModel, never,
|
|
74
|
+
export declare const LanguageModelLive: Layer.Layer<LanguageModel.LanguageModel, never, AuthStore | SettingsStore>;
|
|
62
75
|
/** A profile-pinned routed model. Unlike LanguageModelLive it never consults
|
|
63
76
|
* SettingsStore; dedicated agents use this so global role edits cannot
|
|
64
|
-
* silently change an evalled execution profile.
|
|
77
|
+
* silently change an evalled execution profile. Like it, it reads its
|
|
78
|
+
* provider services once, where it is built.
|
|
79
|
+
*
|
|
80
|
+
* @public
|
|
81
|
+
*/
|
|
65
82
|
export declare const LanguageModelSelectionLive: (primary: ModelSelection, fallback: Option.Option<ModelSelection>) => Layer.Layer<LanguageModel.LanguageModel, never, AuthStore>;
|
|
83
|
+
export {};
|
package/dist/llm/router.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { AiError, LanguageModel } from "effect/ai";
|
|
2
2
|
import { FetchHttpClient, HttpClient } from "effect/http";
|
|
3
|
-
import { Clock, Duration, Effect, Layer, Metric, Option, Ref, Stream } from "effect";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
3
|
+
import { Cause, Clock, Config, Context, Duration, Effect, Layer, Metric, Option, Predicate, Ref, References, Result, Stream } from "effect";
|
|
4
|
+
import { CurrentModelCallPolicy, CurrentTraceContent, describeModel, formatModelSelection, harnessError, messagesOf, parseModelSelection, resolvingModel, stampPartModel, stampResponseModel, storedJson } from "@xandreed/core";
|
|
5
|
+
import { AuthError, AuthStore } from "../ports/auth-store.port.js";
|
|
6
|
+
import { SettingsStore } from "../ports/settings-store.port.js";
|
|
7
|
+
import { extractUsage } from "@xandreed/core/internal";
|
|
8
|
+
import { buildProviderOver, endpointOwnKey, modelSettingsFailure, prependClaudeCode, providerServices, resolveMessagesConfiguration } from "./providers.js";
|
|
9
|
+
import { makeOpenAiCodexSessions } from "./openAiCodexWebSocket.js";
|
|
8
10
|
import { transportFetch } from "../transport/models-transport.adapter.js";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
retryableLlm,
|
|
13
|
-
retryableLlmStream
|
|
14
|
-
} from "./retry.js";
|
|
11
|
+
import { rejectEmptyResponse, rejectEmptyStream } from "./retry.js";
|
|
12
|
+
import { CurrentModelRequestCall } from "../ports/model-retry-policy.port.js";
|
|
13
|
+
import { enterModelFallback, makeModelRequestCall } from "../transport/resilient-http-client.adapter.js";
|
|
15
14
|
const configError = (message) => AiError.make({ module: "Router", method: "selection", reason: new AiError.UnknownError({ description: message }) }), llmInputTokens = Metric.counter("llm.usage.input_tokens", {
|
|
16
15
|
description: "prompt tokens consumed by routed LLM calls",
|
|
17
16
|
incremental: !0
|
|
@@ -47,10 +46,7 @@ const configError = (message) => AiError.make({ module: "Router", method: "selec
|
|
|
47
46
|
Metric.update(byModel(llmCacheReadTokens, label), usage.cacheReadTokens),
|
|
48
47
|
Metric.update(byModel(llmCacheWriteTokens, label), usage.cacheWriteTokens),
|
|
49
48
|
Metric.update(byModel(llmReasoningTokens, label), usage.reasoningTokens)
|
|
50
|
-
], { discard: !0 }), tracedContent = Effect.service(CurrentTraceContent).pipe(Effect.map((capture) => (content) => capture ||
|
|
51
|
-
const cached = selection.provider === "anthropic" ? withAnthropicCacheBreakpoints(options) : options;
|
|
52
|
-
return shouldPrependClaudeCode ? prependClaudeCode(cached) : cached;
|
|
53
|
-
}, currentSelection = Effect.gen(function* () {
|
|
49
|
+
], { concurrency: 1, discard: !0 }), traceContentVariable = Config.option(Config.String("EFFERENT_TRACE_CONTENT")).pipe(Effect.map(Option.exists((value) => value === "1")), Effect.catch((error) => Effect.logWarning(`EFFERENT_TRACE_CONTENT cannot be read: ${error.message}`).pipe(Effect.as(!1)))), tracedContent = Effect.all([Effect.service(CurrentTraceContent), traceContentVariable], { concurrency: 1 }).pipe(Effect.map(([capture, variable]) => (content) => capture || variable ? content.slice(0, 500) : "[redacted]")), shapeOptions = (built, options) => built.prependClaudeCode ? prependClaudeCode(options) : options, currentSelection = Effect.gen(function* () {
|
|
54
50
|
const loaded = yield* (yield* SettingsStore).load.pipe(Effect.mapError((e) => configError(`settings load failed: ${e.message}`))), raw = Option.getOrElse(loaded.model, () => "");
|
|
55
51
|
return {
|
|
56
52
|
primary: yield* Option.match(parseModelSelection(raw), {
|
|
@@ -60,15 +56,28 @@ const configError = (message) => AiError.make({ module: "Router", method: "selec
|
|
|
60
56
|
fallback: Option.flatMap(loaded.fallbackModel, parseModelSelection),
|
|
61
57
|
effort: loaded.reasoningEffort
|
|
62
58
|
};
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
59
|
+
});
|
|
60
|
+
export const withConfiguredEffort = (effect, effort) => Effect.service(CurrentModelCallPolicy).pipe(Effect.flatMap((current) => Option.isSome(current) || Option.isNone(effort) ? effect : effect.pipe(Effect.provideService(CurrentModelCallPolicy, Option.some({ effort: effort.value })))));
|
|
61
|
+
const describeSelection = Effect.fnUntraced(function* (primary, fallback, effort, services) {
|
|
62
|
+
const current = yield* CurrentModelCallPolicy, policy = Option.orElse(current, () => Option.map(effort, (value) => ({ effort: value }))), endpoint = Object.hasOwn(services.endpoints, primary.provider) ? Option.fromNullishOr(services.endpoints[primary.provider]) : Option.none(), messages = Option.filter(endpoint, (value) => value.api === "anthropic-messages"), configuration = (configured) => resolveMessagesConfiguration(primary, services.modelSettings, policy, services.thinkingPrefixMismatch, configured), native = yield* Option.match(messages, {
|
|
63
|
+
onSome: () => Effect.fromResult(configuration(messages)).pipe(Effect.map((value) => value.settings), Effect.mapError((error) => harnessError("request.model", error.message, error))),
|
|
64
|
+
onNone: () => Effect.succeed(Option.isNone(endpoint) && primary.provider === "anthropic" ? {
|
|
65
|
+
messagesProfile: { kind: "requested", apiVersion: "2023-06-01", ...Result.match(configuration(Option.none()), {
|
|
66
|
+
onSuccess: (value) => ({ settings: value.settings }),
|
|
67
|
+
onFailure: (error) => ({ controls: Object.hasOwn(services.modelSettings, formatModelSelection(primary)) ? services.modelSettings[formatModelSelection(primary)] ?? {} : {}, unresolved: error.reason })
|
|
68
|
+
}) }
|
|
69
|
+
} : {})
|
|
70
|
+
}), settings = yield* storedJson({
|
|
67
71
|
fallback: Option.getOrElse(Option.map(fallback, formatModelSelection), () => null),
|
|
68
|
-
policy: Option.getOrElse(
|
|
69
|
-
|
|
70
|
-
}))
|
|
71
|
-
|
|
72
|
+
policy: Option.getOrElse(policy, () => null),
|
|
73
|
+
...native
|
|
74
|
+
}).pipe(Effect.mapError((error) => harnessError("request.model", `the routed model's settings are not JSON: ${error.message}`, error)));
|
|
75
|
+
return { provider: primary.provider, model: primary.modelId, settings };
|
|
76
|
+
}), credentialFor = Effect.fn("models.credentials")(function* (selection, endpoints) {
|
|
77
|
+
const own = yield* Option.match(Object.hasOwn(endpoints, selection.provider) ? Option.fromNullishOr(endpoints[selection.provider]) : Option.none(), {
|
|
78
|
+
onNone: () => Effect.succeedNone,
|
|
79
|
+
onSome: (endpoint) => endpointOwnKey(endpoint).pipe(Effect.mapError((error) => new AuthError({ provider: selection.provider, message: `cannot read the endpoint's key variable: ${error.message}` })))
|
|
80
|
+
});
|
|
72
81
|
if (Option.isSome(own))
|
|
73
82
|
return { credential: void 0, key: own.value };
|
|
74
83
|
const auth = yield* AuthStore;
|
|
@@ -76,44 +85,45 @@ const configError = (message) => AiError.make({ module: "Router", method: "selec
|
|
|
76
85
|
credential: Option.getOrUndefined(yield* auth.get(selection.provider)),
|
|
77
86
|
key: Option.getOrUndefined(yield* auth.resolveKey(selection.provider))
|
|
78
87
|
};
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
}), authenticationFailure = (error) => AiError.make({
|
|
89
|
+
module: "Router",
|
|
90
|
+
method: "credentials",
|
|
91
|
+
reason: new AiError.AuthenticationError({ kind: "Unknown", description: error.message, metadata: { router: { provider: error.provider } } })
|
|
92
|
+
}), providerFor = (selection, services) => credentialFor(selection, services.endpoints).pipe(Effect.mapError(authenticationFailure), Effect.flatMap(({ credential, key }) => buildProviderOver(services)(selection, credential, key).pipe(Effect.mapError((error) => error._tag === "AuthError" ? authenticationFailure(error) : modelSettingsFailure(error))))), recorded = (label, isFallback) => (call) => call.pipe(Effect.tap((res) => countUsage(label, usageOf(res.usage))), Effect.tap(() => countRequest(label, "ok", isFallback)), Effect.tapError(() => countRequest(label, "error", isFallback)), Effect.timed, Effect.tap(([duration]) => Metric.update(byModel(llmDuration, label), duration)), Effect.map(([, res]) => res)), modelAttributes = (selection) => ({ "llm.model": formatModelSelection(selection) }), providerCall = (selection) => (call) => call.pipe(Effect.annotateLogs(modelAttributes(selection)), Effect.withSpan("providers.generate", { attributes: modelAttributes(selection) })), providerStream = (selection) => (stream) => stream.pipe(Stream.updateService(References.CurrentLogAnnotations, (annotations) => ({ ...annotations, ...modelAttributes(selection) })), Stream.withSpan("providers.generate", { attributes: modelAttributes(selection) }));
|
|
93
|
+
export const generateOver = (services) => (selection, options, isFallback = !1, budget) => Effect.gen(function* () {
|
|
94
|
+
const label = formatModelSelection(selection), traced = yield* tracedContent, bound = budget ?? (yield* makeModelRequestCall({ reserveFallback: !1 })), built = yield* providerFor(selection, services);
|
|
95
|
+
return yield* built.svc.generateText(shapeOptions(built, options)).pipe(rejectEmptyResponse(label), Effect.provideService(CurrentModelRequestCall, Option.some(bound)), Effect.map((res) => stampResponse(res, label)), Effect.tap((res) => Effect.annotateCurrentSpan({
|
|
83
96
|
"llm.finish_reason": String(res.finishReason),
|
|
84
97
|
...usageAttributes(usageOf(res.usage)),
|
|
85
98
|
"llm.response_chars": res.text.length,
|
|
86
|
-
"llm.tool_calls": res.content.flatMap((part) =>
|
|
87
|
-
const p = part;
|
|
88
|
-
return p.type === "tool-call" ? [p.name ?? ""] : [];
|
|
89
|
-
}).join(","),
|
|
99
|
+
"llm.tool_calls": res.content.flatMap((part) => part.type === "tool-call" ? [part.name] : []).join(","),
|
|
90
100
|
"llm.reasoning": traced(res.reasoningText ?? ""),
|
|
91
101
|
"llm.fallback": isFallback
|
|
92
|
-
})),
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
102
|
+
})), recorded(label, isFallback));
|
|
103
|
+
}).pipe(Effect.scoped, providerCall(selection)), generateWith = (selection, options, isFallback = !1, budget) => Effect.flatMap(providerServices, (services) => generateOver(services)(selection, options, isFallback, budget));
|
|
104
|
+
const objectOver = (services) => (selection, options, isFallback, budget) => Effect.gen(function* () {
|
|
105
|
+
const label = formatModelSelection(selection), built = yield* providerFor(selection, services);
|
|
106
|
+
return yield* built.svc.generateObject(shapeOptions(built, options)).pipe(Effect.provideService(CurrentModelRequestCall, Option.some(budget)), Effect.tap((res) => Effect.annotateCurrentSpan({
|
|
107
|
+
"llm.finish_reason": String(res.finishReason),
|
|
108
|
+
...usageAttributes(usageOf(res.usage)),
|
|
109
|
+
"llm.fallback": isFallback
|
|
110
|
+
})), recorded(label, isFallback));
|
|
111
|
+
}).pipe(Effect.scoped, providerCall(selection));
|
|
112
|
+
export const withFallbackRung = (primary, fallback, call, budget) => call(primary, !1).pipe(Effect.catch((error) => Effect.flatMap(enterModelFallback(budget, error), (again) => Option.match(Option.filter(fallback, (fb) => again && formatModelSelection(fb) !== formatModelSelection(primary)), {
|
|
96
113
|
onNone: () => Effect.fail(error),
|
|
97
|
-
onSome: (fb) => Effect.logWarning(
|
|
98
|
-
})));
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const p = part;
|
|
111
|
-
if (p.type === "text-delta")
|
|
112
|
-
return { ...stats, textChars: stats.textChars + (p.delta ?? "").length };
|
|
113
|
-
if (p.type === "reasoning-delta")
|
|
114
|
-
return { ...stats, reasoning: stats.reasoning + (p.delta ?? "") };
|
|
115
|
-
if (p.type === "tool-call")
|
|
116
|
-
return { ...stats, toolNames: [...stats.toolNames, p.name ?? ""] };
|
|
114
|
+
onSome: (fb) => Effect.logWarning(`exhausted retries (${String(error).slice(0, 200)}) \u2014 falling back`).pipe(Effect.annotateLogs({ ...modelAttributes(primary), "llm.fallback_model": formatModelSelection(fb) }), Effect.andThen(call(fb, !0)))
|
|
115
|
+
})))), stampResponse = (res, label) => new LanguageModel.GenerateTextResponse(stampResponseModel(res.content, label));
|
|
116
|
+
const fieldOf = (part, key) => Predicate.hasProperty(part, key) ? part[key] : void 0, textOf = (part, key) => {
|
|
117
|
+
const value = fieldOf(part, key);
|
|
118
|
+
return Predicate.isString(value) ? value : "";
|
|
119
|
+
}, accumulateStats = (stats, part) => {
|
|
120
|
+
const type = textOf(part, "type");
|
|
121
|
+
if (type === "text-delta")
|
|
122
|
+
return { ...stats, textChars: stats.textChars + textOf(part, "delta").length };
|
|
123
|
+
if (type === "reasoning-delta")
|
|
124
|
+
return { ...stats, reasoning: stats.reasoning + textOf(part, "delta") };
|
|
125
|
+
if (type === "tool-call")
|
|
126
|
+
return { ...stats, toolNames: [...stats.toolNames, textOf(part, "name")] };
|
|
117
127
|
return stats;
|
|
118
128
|
};
|
|
119
129
|
export const tapStreamTelemetry = (label) => (stream) => Stream.unwrap(Effect.gen(function* () {
|
|
@@ -123,43 +133,46 @@ export const tapStreamTelemetry = (label) => (stream) => Stream.unwrap(Effect.ge
|
|
|
123
133
|
toolNames: []
|
|
124
134
|
}), startedAt = yield* Clock.currentTimeMillis, traced = yield* tracedContent, recordDuration = Clock.currentTimeMillis.pipe(Effect.flatMap((now) => Metric.update(byModel(llmDuration, label), Duration.millis(now - startedAt))));
|
|
125
135
|
return stream.pipe(Stream.mapEffect((part) => {
|
|
126
|
-
|
|
127
|
-
if (p.type !== "finish")
|
|
136
|
+
if (textOf(part, "type") !== "finish")
|
|
128
137
|
return Ref.update(stats, (s) => accumulateStats(s, part)).pipe(Effect.as(part));
|
|
138
|
+
const usage = usageOf(fieldOf(part, "usage"));
|
|
129
139
|
return Ref.get(stats).pipe(Effect.flatMap((s) => Effect.annotateCurrentSpan({
|
|
130
|
-
"llm.finish_reason": String(
|
|
131
|
-
...usageAttributes(
|
|
140
|
+
"llm.finish_reason": String(fieldOf(part, "reason")),
|
|
141
|
+
...usageAttributes(usage),
|
|
132
142
|
"llm.response_chars": s.textChars,
|
|
133
143
|
"llm.tool_calls": s.toolNames.join(","),
|
|
134
144
|
"llm.reasoning": traced(s.reasoning)
|
|
135
|
-
})), Effect.andThen(countUsage(label,
|
|
145
|
+
})), Effect.andThen(countUsage(label, usage)), Effect.as(part));
|
|
136
146
|
}), Stream.onEnd(countRequest(label, "ok").pipe(Effect.andThen(recordDuration))), Stream.tapCause(() => countRequest(label, "error").pipe(Effect.andThen(recordDuration))));
|
|
137
|
-
})), streamWith = (selection, options) => Stream.unwrap(Effect.gen(function* () {
|
|
138
|
-
const label = formatModelSelection(selection), { credential, key } = yield* credentialFor(selection), built = yield* buildProvider(selection, credential, key);
|
|
139
|
-
return built.svc.streamText(shapeOptions(selection, built.prependClaudeCode, options)).pipe(retryableLlmStream(label), Stream.map((part) => stampModel(part, label)), tapStreamTelemetry(label), Stream.withSpan("providers.generate", {
|
|
140
|
-
attributes: { "llm.model": label }
|
|
141
|
-
}));
|
|
142
147
|
}));
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
148
|
+
const streamOver = (services) => (selection, options, budget) => Stream.unwrap(Effect.gen(function* () {
|
|
149
|
+
const label = formatModelSelection(selection), built = yield* providerFor(selection, services);
|
|
150
|
+
return built.svc.streamText(shapeOptions(built, options)).pipe(rejectEmptyStream(label), Stream.provideService(CurrentModelRequestCall, Option.some(budget)), Stream.map((part) => stampPartModel(part, label)), tapStreamTelemetry(label));
|
|
151
|
+
})).pipe(providerStream(selection)), withConfiguredEffortStream = (stream, effort) => Stream.unwrap(Effect.service(CurrentModelCallPolicy).pipe(Effect.map((current) => Option.isSome(current) || Option.isNone(effort) ? stream : stream.pipe(Stream.provideService(CurrentModelCallPolicy, Option.some({ effort: effort.value })))))), requestFetch = (services) => Effect.map(Effect.context(), (context) => Option.match(services.transport, {
|
|
152
|
+
onNone: () => Context.get(context, FetchHttpClient.Fetch),
|
|
153
|
+
onSome: (transport) => Option.getOrElse(Option.filter(Option.fromNullishOr(Context.getOrUndefined(context, FetchHttpClient.Fetch)), () => Option.contains(services.managedTransport, transport)), () => transportFetch(transport))
|
|
154
|
+
}));
|
|
155
|
+
export const throughTransport = (services) => (effect) => Effect.flatMap(requestFetch(services), (fetch) => Effect.provideService(effect, FetchHttpClient.Fetch, fetch));
|
|
156
|
+
const throughTransportStream = (services) => (stream) => Stream.unwrap(Effect.map(requestFetch(services), (fetch) => Stream.provideService(stream, FetchHttpClient.Fetch, fetch)));
|
|
157
|
+
export const layerProviderServices = Effect.all([providerServices, makeOpenAiCodexSessions()], { concurrency: 1 }).pipe(Effect.map(([services, sessions]) => ({ ...services, sessions: Option.some(sessions) })));
|
|
158
|
+
const selectionModel = (primary, fallback, effort, stores, http, services) => {
|
|
146
159
|
const service = {
|
|
147
160
|
[LanguageModel.TypeId]: LanguageModel.TypeId,
|
|
148
|
-
generateText: (options) => withConfiguredEffort(withFallbackRung(primary, fallback, (selection, isFallback) =>
|
|
149
|
-
generateObject: () => Effect.
|
|
150
|
-
streamText: (options) => withConfiguredEffortStream(
|
|
161
|
+
generateText: (options) => makeModelRequestCall({ reserveFallback: Option.exists(fallback, (value) => formatModelSelection(value) !== formatModelSelection(primary)) }).pipe(Effect.flatMap((budget) => withConfiguredEffort(withFallbackRung(primary, fallback, (selection, isFallback) => generateOver(services)(selection, options, isFallback, budget), budget), effort)), throughTransport(services), Effect.provide(stores), Effect.provideService(HttpClient.HttpClient, http)),
|
|
162
|
+
generateObject: (options) => makeModelRequestCall({ reserveFallback: Option.exists(fallback, (value) => formatModelSelection(value) !== formatModelSelection(primary)) }).pipe(Effect.flatMap((budget) => withConfiguredEffort(withFallbackRung(primary, fallback, (selection, isFallback) => objectOver(services)(selection, options, isFallback, budget), budget), effort)), throughTransport(services), Effect.provide(stores), Effect.provideService(HttpClient.HttpClient, http)),
|
|
163
|
+
streamText: (options) => withConfiguredEffortStream(Stream.unwrap(Effect.map(makeModelRequestCall({ reserveFallback: !1 }), (budget) => streamOver(services)(primary, options, budget))), effort).pipe(throughTransportStream(services), Stream.provideContext(stores), Stream.provideService(HttpClient.HttpClient, http))
|
|
151
164
|
};
|
|
152
|
-
return describeModel(service, describeSelection(primary, fallback, effort));
|
|
165
|
+
return describeModel(service, describeSelection(primary, fallback, effort, services));
|
|
153
166
|
};
|
|
154
167
|
export const LanguageModelLive = Layer.effect(LanguageModel.LanguageModel, Effect.gen(function* () {
|
|
155
|
-
const
|
|
168
|
+
const stores = Context.pick(AuthStore, SettingsStore)(yield* Effect.context()), services = yield* layerProviderServices, http = yield* HttpClient.HttpClient, selected = currentSelection.pipe(Effect.map(({ primary, fallback, effort }) => selectionModel(primary, fallback, effort, stores, http, services)), Effect.provide(stores)), service = {
|
|
156
169
|
[LanguageModel.TypeId]: LanguageModel.TypeId,
|
|
157
170
|
generateText: (options) => selected.pipe(Effect.flatMap((model) => model.generateText(options))),
|
|
158
|
-
generateObject: () => Effect.
|
|
171
|
+
generateObject: (options) => selected.pipe(Effect.flatMap((model) => model.generateObject(options))),
|
|
159
172
|
streamText: (options) => Stream.unwrap(selected.pipe(Effect.map((model) => model.streamText(options))))
|
|
160
|
-
}, asHarness = (error) =>
|
|
161
|
-
return resolvingModel(describeModel(service, currentSelection.pipe(Effect.flatMap(({ primary, fallback, effort }) => describeSelection(primary, fallback, effort)), Effect.provide(
|
|
173
|
+
}, asHarness = (error) => harnessError("request.model", messagesOf(Cause.fail(error)), error);
|
|
174
|
+
return resolvingModel(describeModel(service, currentSelection.pipe(Effect.mapError(asHarness), Effect.flatMap(({ primary, fallback, effort }) => describeSelection(primary, fallback, effort, services)), Effect.provide(stores))), selected.pipe(Effect.mapError(asHarness)));
|
|
162
175
|
})).pipe(Layer.provide(FetchHttpClient.layer)), LanguageModelSelectionLive = (primary, fallback) => Layer.effect(LanguageModel.LanguageModel, Effect.gen(function* () {
|
|
163
|
-
const
|
|
164
|
-
return selectionModel(primary, fallback, Option.none(),
|
|
176
|
+
const stores = Context.pick(AuthStore)(yield* Effect.context()), http = yield* HttpClient.HttpClient;
|
|
177
|
+
return selectionModel(primary, fallback, Option.none(), stores, http, yield* layerProviderServices);
|
|
165
178
|
})).pipe(Layer.provide(FetchHttpClient.layer));
|
package/dist/llm/sse.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { AiError } from "effect/ai";
|
|
2
2
|
import type { Response } from "effect/ai";
|
|
3
|
-
import { Stream } from "effect";
|
|
3
|
+
import { Effect, Option, Schema, Stream } from "effect";
|
|
4
|
+
import type { ModelAttempt } from "./model-completion.adapter.js";
|
|
5
|
+
import type { CompletionUsage } from "./chat-wire.entity.js";
|
|
4
6
|
/**
|
|
5
7
|
* The OpenAI-compatible SSE wire → `@effect/ai` `StreamPartEncoded`s, as a
|
|
6
8
|
* PURE state machine over `data:` lines. Owned here so `compat.ts` imports
|
|
@@ -10,30 +12,17 @@ import { Stream } from "effect";
|
|
|
10
12
|
* text/reasoning delta or a completed tool call — empty deltas emit nothing.
|
|
11
13
|
* The retry gate (S2) and the engine fold (S3) both hinge on that.
|
|
12
14
|
*
|
|
13
|
-
*
|
|
15
|
+
* An end marker is concatenated after the wire lines so the flush (open
|
|
14
16
|
* chunk ends → accumulated tool calls → finish) runs even when the gateway
|
|
15
|
-
* never sends `[DONE]`; a second
|
|
17
|
+
* never sends `[DONE]`; a second flush is idempotent via `flushed`. A body
|
|
18
|
+
* that ends with neither `[DONE]` nor a finish reason was cut off on the
|
|
19
|
+
* way (a dropped connection, a gateway that gave up): its finish is
|
|
20
|
+
* `unknown`, which the loop treats as a cut when the reply holds tool calls.
|
|
21
|
+
*
|
|
22
|
+
* Each chunk is decoded (`chat-wire.entity.ts`): a `data:` line that is no
|
|
23
|
+
* chunk (not JSON, `null`, a field of the wrong type) fails the stream with
|
|
24
|
+
* the provider's `InvalidResponseError`, never the model's invalid output.
|
|
16
25
|
*/
|
|
17
|
-
/** The usage object OpenAI-compatible completions carry (streaming sends it
|
|
18
|
-
* on the last chunk under `stream_options: {include_usage: true}`). */
|
|
19
|
-
export interface CompletionUsage {
|
|
20
|
-
readonly prompt_tokens?: number;
|
|
21
|
-
readonly completion_tokens?: number;
|
|
22
|
-
readonly total_tokens?: number;
|
|
23
|
-
/** DeepSeek-style cache accounting. */
|
|
24
|
-
readonly prompt_cache_hit_tokens?: number;
|
|
25
|
-
/** Moonshot-style cache accounting. */
|
|
26
|
-
readonly cached_tokens?: number;
|
|
27
|
-
/** OpenAI-native cache reads; gateways (OpenCode's among them) add the cache writes. */
|
|
28
|
-
readonly prompt_tokens_details?: {
|
|
29
|
-
readonly cached_tokens?: number;
|
|
30
|
-
readonly cache_write_tokens?: number;
|
|
31
|
-
};
|
|
32
|
-
/** OpenAI-native reasoning accounting, counted inside `completion_tokens`. */
|
|
33
|
-
readonly completion_tokens_details?: {
|
|
34
|
-
readonly reasoning_tokens?: number;
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
26
|
/** How a compat endpoint's usage is read. */
|
|
38
27
|
export interface UsageReading {
|
|
39
28
|
/**
|
|
@@ -49,24 +38,42 @@ export interface UsageReading {
|
|
|
49
38
|
* parser — the gateway fronts upstreams with three cache vocabularies.
|
|
50
39
|
* `prompt_tokens` already counts the cached and written tokens: it is the
|
|
51
40
|
* full input, and `completion_tokens` the full output, reasoning included. */
|
|
52
|
-
export declare const usageFromCompletion: (usage: CompletionUsage | undefined, reading?: UsageReading) =>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
41
|
+
export declare const usageFromCompletion: (usage: CompletionUsage | null | undefined, reading?: UsageReading) => typeof Response.Usage.Encoded;
|
|
42
|
+
/** The finish reasons that cut a response short, as `@effect/ai` names them. */
|
|
43
|
+
type WireCut = "length" | "content-filter" | "error" | "other";
|
|
44
|
+
/**
|
|
45
|
+
* A wire response's finish reason: one that ended on its own (`stop`,
|
|
46
|
+
* `tool-calls`), a cut, or `unknown`: a stream that ended with neither a
|
|
47
|
+
* finish reason nor `[DONE]`, which may have been cut off anywhere.
|
|
48
|
+
*/
|
|
49
|
+
export type WireFinishReason = "stop" | "tool-calls" | WireCut | "unknown";
|
|
50
|
+
/**
|
|
51
|
+
* The wire's `finish_reason` as `@effect/ai`'s finish reason, for the settled
|
|
52
|
+
* and the streamed parser alike, decided before any tool call's arguments are
|
|
53
|
+
* read. A response that ended on its own (a settling word, or no reason at
|
|
54
|
+
* all on a whole completion or a stream the server ended with `[DONE]`)
|
|
55
|
+
* finishes on `tool-calls` when it holds tool calls, else on `stop`. A cut
|
|
56
|
+
* stays a cut whatever the response holds, so the loop runs no call of a
|
|
57
|
+
* cut reply. A stream that ended with neither is the flush's `unknown`.
|
|
58
|
+
*/
|
|
59
|
+
export declare const finishReasonFromWire: (raw: Option.Option<string>, hasToolCalls: boolean) => WireFinishReason;
|
|
60
|
+
/**
|
|
61
|
+
* A tool call's params from its wire arguments, once the response's finish
|
|
62
|
+
* reason is known. Arguments that do not parse fail a response that ended on
|
|
63
|
+
* its own with the model's invalid output (`invalid`: the loop's corrective
|
|
64
|
+
* path). A cut response's call, or an `unknown` one's, is kept with params
|
|
65
|
+
* `{}`: its arguments may end mid-JSON, and the loop answers each call of a
|
|
66
|
+
* cut reply as cut and never runs it.
|
|
67
|
+
*/
|
|
68
|
+
export declare const toolCallParams: (args: string, reason: WireFinishReason, invalid: () => AiError.AiError) => Effect.Effect<Schema.Json, AiError.AiError>;
|
|
66
69
|
/** Parse a streaming chat-completions body into encoded stream parts. */
|
|
67
70
|
export declare const sseStreamParts: (options: {
|
|
68
71
|
readonly moduleName: string;
|
|
69
72
|
readonly body: ReadableStream<Uint8Array>;
|
|
73
|
+
readonly attempt?: Option.Option<ModelAttempt>;
|
|
70
74
|
/** How the final chunk's usage is read (see `usageFromCompletion`). */
|
|
71
75
|
readonly usage?: UsageReading;
|
|
76
|
+
/** Observe raw cumulative counters before SDK defaults; a finish signal establishes output finality. */
|
|
77
|
+
readonly observeUsage?: (usage: unknown, final: boolean) => Effect.Effect<void>;
|
|
72
78
|
}) => Stream.Stream<Response.StreamPartEncoded, AiError.AiError>;
|
|
79
|
+
export {};
|