@xandreed/plugin-models 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/auth/anthropicOAuth.d.ts +16 -2
- package/dist/auth/anthropicOAuth.js +14 -11
- package/dist/auth/credential-input.entity.d.ts +34 -0
- package/dist/auth/credential-input.entity.functions.d.ts +8 -0
- package/dist/auth/credential-input.entity.functions.js +9 -0
- package/dist/auth/credential-input.entity.js +20 -0
- package/dist/auth/credential-refresh.entity.d.ts +9 -0
- package/dist/auth/credential-refresh.entity.functions.d.ts +15 -0
- package/dist/auth/credential-refresh.entity.functions.js +12 -0
- package/dist/auth/credential-refresh.entity.functions.test.d.ts +1 -0
- package/dist/auth/credential-refresh.entity.js +2 -0
- package/dist/auth/localAuth.d.ts +2 -1
- package/dist/auth/localAuth.js +41 -43
- package/dist/auth/memory-auth.adapter.d.ts +27 -0
- package/dist/auth/memory-auth.adapter.js +47 -0
- package/dist/auth/memory-auth.adapter.test.d.ts +1 -0
- package/dist/auth/openAiCodexOAuth.d.ts +3 -2
- package/dist/auth/openAiCodexOAuth.js +9 -8
- package/dist/index.d.ts +23 -6
- package/dist/index.js +28 -1
- package/dist/llm/compat.d.ts +39 -8
- package/dist/llm/compat.js +80 -29
- package/dist/llm/modelCatalog.d.ts +20 -1
- package/dist/llm/modelCatalog.js +19 -7
- package/dist/llm/openAiCodex.d.ts +4 -1
- package/dist/llm/openAiCodex.js +2 -2
- package/dist/llm/openAiCodexWebSocket.d.ts +23 -3
- package/dist/llm/openAiCodexWebSocket.js +125 -71
- package/dist/llm/openAiCodexWebSocket.test.d.ts +1 -0
- package/dist/llm/providers.d.ts +18 -4
- package/dist/llm/providers.js +49 -24
- package/dist/llm/router.d.ts +11 -1
- package/dist/llm/router.js +55 -31
- package/dist/llm/sse.d.ts +25 -3
- package/dist/llm/sse.js +8 -8
- package/dist/llm/utilityLlm.d.ts +1 -1
- package/dist/llm/utilityLlm.js +4 -4
- package/dist/plugin.adapter.d.ts +64 -4
- package/dist/plugin.adapter.js +48 -10
- package/dist/ports/host-credentials.port.d.ts +21 -0
- package/dist/ports/host-credentials.port.js +4 -0
- package/dist/ports/model-endpoints.port.d.ts +16 -3
- package/dist/ports/model-endpoints.port.js +7 -3
- package/dist/ports/models-transport.port.d.ts +46 -0
- package/dist/ports/models-transport.port.js +4 -0
- package/dist/settings/localSettings.js +6 -46
- package/dist/settings/memory-settings.adapter.d.ts +10 -0
- package/dist/settings/memory-settings.adapter.js +11 -0
- package/dist/settings/memory-settings.adapter.test.d.ts +1 -0
- package/dist/settings/settings-value.entity.d.ts +7 -0
- package/dist/settings/settings-value.entity.functions.d.ts +29 -0
- package/dist/settings/settings-value.entity.functions.js +43 -0
- package/dist/settings/settings-value.entity.functions.test.d.ts +1 -0
- package/dist/settings/settings-value.entity.js +2 -0
- package/dist/transport/http-proxy.entity.d.ts +90 -0
- package/dist/transport/http-proxy.entity.functions.d.ts +51 -0
- package/dist/transport/http-proxy.entity.functions.js +112 -0
- package/dist/transport/http-proxy.entity.functions.test.d.ts +1 -0
- package/dist/transport/http-proxy.entity.js +42 -0
- package/dist/transport/models-transport.adapter.d.ts +48 -0
- package/dist/transport/models-transport.adapter.js +102 -0
- package/dist/transport/models-transport.adapter.test.d.ts +1 -0
- package/dist/transport/recording-proxy.testing.d.ts +33 -0
- package/dist/transport/undici-fetch.adapter.d.ts +13 -0
- package/dist/transport/undici-fetch.adapter.js +1 -0
- package/package.json +4 -3
package/dist/llm/providers.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { LanguageModel } from "effect/ai";
|
|
2
2
|
import { HttpClient } from "effect/http";
|
|
3
|
-
import { Effect, Redacted } from "effect";
|
|
3
|
+
import { Effect, Option, Redacted } from "effect";
|
|
4
4
|
import type { Scope } from "effect";
|
|
5
5
|
import { AuthError } from "@xandreed/core";
|
|
6
6
|
import type { Credential, ModelSelection } from "@xandreed/core";
|
|
7
|
+
import { usesOpenCodeResponses } from "./modelCatalog.js";
|
|
8
|
+
import type { ModelEndpoint } from "../ports/model-endpoints.port.js";
|
|
7
9
|
/**
|
|
8
10
|
* Per-provider `LanguageModel` construction. Built PER REQUEST from a
|
|
9
11
|
* freshly-resolved key (never captured at layer build), so a credential or
|
|
@@ -20,12 +22,13 @@ export declare const OPENCODE_CHAT_URL = "https://opencode.ai/zen/go/v1/chat/com
|
|
|
20
22
|
export declare const OPENCODE_RESPONSES_API_URL = "https://opencode.ai/zen/v1";
|
|
21
23
|
/** Gemini through its OpenAI-compatible chat completions (Bearer API key). */
|
|
22
24
|
export declare const GOOGLE_CHAT_URL = "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions";
|
|
23
|
-
|
|
24
|
-
export declare const usesOpenCodeResponses: (modelId: string) => boolean;
|
|
25
|
+
export { usesOpenCodeResponses };
|
|
25
26
|
export interface BuiltProvider {
|
|
26
27
|
readonly svc: LanguageModel.LanguageModel;
|
|
27
28
|
/** Anthropic subscription auth requires the Claude Code system block first. */
|
|
28
29
|
readonly prependClaudeCode: boolean;
|
|
30
|
+
/** The route sends images; when it does not, `buildProvider` has the model send them as label lines. */
|
|
31
|
+
readonly images: boolean;
|
|
29
32
|
}
|
|
30
33
|
/** Prepend the Claude Code system block (subscription-auth requirement). */
|
|
31
34
|
export declare const prependClaudeCode: (options: unknown) => unknown;
|
|
@@ -40,5 +43,16 @@ export declare const prependClaudeCode: (options: unknown) => unknown;
|
|
|
40
43
|
export declare const withAnthropicCacheBreakpoints: (options: unknown) => unknown;
|
|
41
44
|
/** `<url>/chat/completions` of an OpenAI-compatible base URL. */
|
|
42
45
|
export declare const chatCompletionsUrl: (baseUrl: string) => string;
|
|
43
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* An endpoint's own key: `apiKey`, then the environment variable `apiKeyEnv`
|
|
48
|
+
* names when it is set. An empty one is still the endpoint's own: it sends
|
|
49
|
+
* no key, and the AuthStore's is not used.
|
|
50
|
+
*/
|
|
51
|
+
export declare const endpointOwnKey: (endpoint: ModelEndpoint) => Option.Option<Redacted.Redacted<string>>;
|
|
52
|
+
/**
|
|
53
|
+
* Preserve adapter descriptors and describe the public configuration of
|
|
54
|
+
* SDK-backed models. On a route that reads no images (`imageInputFor`, a
|
|
55
|
+
* configured endpoint's `images`), every request is sent with its images as
|
|
56
|
+
* label lines (`promptWithImageLabels`) instead of a provider error.
|
|
57
|
+
*/
|
|
44
58
|
export declare const buildProvider: (selection: ModelSelection, credential: Credential | undefined, key: Redacted.Redacted<string> | undefined) => Effect.Effect<BuiltProvider, AuthError, HttpClient.HttpClient | Scope.Scope>;
|
package/dist/llm/providers.js
CHANGED
|
@@ -3,14 +3,29 @@ import { AnthropicClient, AnthropicLanguageModel } from "@effect/ai-anthropic";
|
|
|
3
3
|
import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai";
|
|
4
4
|
import { HttpClient, HttpClientRequest } from "effect/http";
|
|
5
5
|
import { Effect, Option, Redacted } from "effect";
|
|
6
|
-
import { AuthError, CurrentModelCallPolicy, CurrentPromptCacheKey, describeModel, modelRequestDescriptorOf } from "@xandreed/core";
|
|
6
|
+
import { AuthError, CurrentModelCallPolicy, CurrentPromptCacheKey, describeModel, formatModelSelection, modelRequestDescriptorOf } from "@xandreed/core";
|
|
7
7
|
import { ANTHROPIC_OAUTH_BETA, CLAUDE_CODE_SYSTEM } from "../auth/anthropicOAuth.js";
|
|
8
|
-
import { makeCompatLanguageModel } from "./compat.js";
|
|
9
|
-
import {
|
|
8
|
+
import { makeCompatLanguageModel, promptWithImageLabels } from "./compat.js";
|
|
9
|
+
import { imageInputFor, usesOpenCodeResponses } from "./modelCatalog.js";
|
|
10
|
+
import { makeOpenAiCodexLanguageModel, OPENAI_CODEX_API_URL } from "./openAiCodex.js";
|
|
10
11
|
import { openAiCodexAccountId } from "../auth/openAiCodexOAuth.js";
|
|
11
12
|
import { CurrentModelEndpoints } from "../ports/model-endpoints.port.js";
|
|
12
|
-
|
|
13
|
-
const
|
|
13
|
+
import { ModelsTransport } from "../ports/models-transport.port.js";
|
|
14
|
+
export const OPENCODE_CHAT_URL = "https://opencode.ai/zen/go/v1/chat/completions", OPENCODE_RESPONSES_API_URL = "https://opencode.ai/zen/v1", GOOGLE_CHAT_URL = "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions";
|
|
15
|
+
|
|
16
|
+
export { usesOpenCodeResponses };
|
|
17
|
+
const withImageLabels = (model) => {
|
|
18
|
+
const relabeled = (options) => ({
|
|
19
|
+
...options,
|
|
20
|
+
prompt: promptWithImageLabels(Prompt.make(options.prompt))
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
...model,
|
|
24
|
+
generateText: (options) => model.generateText(relabeled(options)),
|
|
25
|
+
generateObject: (options) => model.generateObject(relabeled(options)),
|
|
26
|
+
streamText: (options) => model.streamText(relabeled(options))
|
|
27
|
+
};
|
|
28
|
+
}, anthropicOAuthTransform = (access) => (client) => client.pipe(HttpClient.mapRequest((req) => req.pipe(HttpClientRequest.setHeaders({
|
|
14
29
|
Authorization: `Bearer ${Redacted.value(access)}`,
|
|
15
30
|
"anthropic-beta": ANTHROPIC_OAUTH_BETA
|
|
16
31
|
})))), claudeCodePrompt = Prompt.make([{ role: "system", content: CLAUDE_CODE_SYSTEM }]);
|
|
@@ -45,7 +60,7 @@ export const prependClaudeCode = (options) => ({
|
|
|
45
60
|
};
|
|
46
61
|
const missingKey = (selection) => new AuthError({
|
|
47
62
|
provider: selection.provider,
|
|
48
|
-
message: `no credential for ${selection.provider} \u2014
|
|
63
|
+
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")`
|
|
49
64
|
}), openCodeResponsesConfig = (cacheKey, policy) => ({
|
|
50
65
|
strictJsonSchema: !1,
|
|
51
66
|
prompt_cache_key: Option.getOrElse(cacheKey, () => "efferent"),
|
|
@@ -59,22 +74,27 @@ const missingKey = (selection) => new AuthError({
|
|
|
59
74
|
})
|
|
60
75
|
})
|
|
61
76
|
});
|
|
62
|
-
export const chatCompletionsUrl = (baseUrl) => `${baseUrl.replace(/\/+$/, "")}/chat/completions
|
|
63
|
-
const endpointKey = (endpoint, stored) =>
|
|
77
|
+
export const chatCompletionsUrl = (baseUrl) => `${baseUrl.replace(/\/+$/, "")}/chat/completions`, endpointOwnKey = (endpoint) => Option.fromNullishOr(endpoint.apiKey).pipe(Option.map((key) => Redacted.isRedacted(key) ? key : Redacted.make(key)), Option.orElse(() => Option.flatMap(Option.fromNullishOr(endpoint.apiKeyEnv), (name) => Option.map(Option.fromNullishOr(process.env[name]), (value) => Redacted.make(value)))));
|
|
78
|
+
const endpointKey = (endpoint, stored) => endpointOwnKey(endpoint).pipe(Option.orElse(() => Option.fromNullishOr(stored)), Option.map(Redacted.value), Option.filter((value) => value.length > 0)), compatFetch = (transport) => Option.match(transport, { onNone: () => ({}), onSome: ({ fetch }) => ({ fetchImpl: fetch }) }), codexWebSocketProxy = (transport) => Option.match(Option.flatMap(transport, (found) => Option.fromNullishOr(found.webSocketAgent)), {
|
|
79
|
+
onNone: () => Effect.succeed(Option.none()),
|
|
80
|
+
onSome: (route) => route(new URL(OPENAI_CODEX_API_URL)).pipe(Effect.mapError((error) => new AuthError({ provider: "openai-codex", message: error.message })))
|
|
81
|
+
}), endpointModel = (selection, url, key, standardOnly, images, transport) => makeCompatLanguageModel({
|
|
64
82
|
moduleName: selection.provider,
|
|
65
83
|
chatUrl: chatCompletionsUrl(url),
|
|
66
84
|
model: selection.modelId,
|
|
67
85
|
standardOnly,
|
|
86
|
+
images,
|
|
87
|
+
...compatFetch(transport),
|
|
68
88
|
...Option.match(key, { onNone: () => ({}), onSome: (apiKey) => ({ apiKey }) })
|
|
69
|
-
}).pipe(Effect.map((svc) => ({ svc, prependClaudeCode: !1 }))), buildProviderService = (selection, credential, key) => Effect.flatMap(Effect.service(CurrentModelEndpoints), (endpoints) => {
|
|
89
|
+
}).pipe(Effect.map((svc) => ({ svc, prependClaudeCode: !1, images }))), buildProviderService = (selection, credential, key) => Effect.flatMap(Effect.all({ endpoints: Effect.service(CurrentModelEndpoints), transport: Effect.serviceOption(ModelsTransport) }), ({ endpoints, transport }) => {
|
|
70
90
|
const endpoint = Option.fromNullishOr(endpoints[selection.provider]);
|
|
71
91
|
if (Option.isSome(endpoint))
|
|
72
|
-
return endpointModel(selection, endpoint.value.url, endpointKey(endpoint.value, key), endpoint.value.standardOnly ?? !0);
|
|
92
|
+
return endpointModel(selection, endpoint.value.url, endpointKey(endpoint.value, key), endpoint.value.standardOnly ?? !0, endpoint.value.images ?? !1, transport);
|
|
73
93
|
if (credential?.type === "local" && credential.baseUrl !== void 0)
|
|
74
|
-
return endpointModel(selection, credential.baseUrl, Option.none(), !0);
|
|
75
|
-
return buildBuiltInProvider(selection, credential, key);
|
|
76
|
-
}), buildBuiltInProvider = (selection, credential, key) => {
|
|
77
|
-
const oauth = credential?.type === "oauth";
|
|
94
|
+
return endpointModel(selection, credential.baseUrl, Option.none(), !0, !1, transport);
|
|
95
|
+
return buildBuiltInProvider(selection, credential, key, transport);
|
|
96
|
+
}), buildBuiltInProvider = (selection, credential, key, transport) => {
|
|
97
|
+
const oauth = credential?.type === "oauth", images = imageInputFor(formatModelSelection(selection));
|
|
78
98
|
if (selection.provider === "opencode") {
|
|
79
99
|
if (key === void 0)
|
|
80
100
|
return Effect.fail(missingKey(selection));
|
|
@@ -85,13 +105,15 @@ const endpointKey = (endpoint, stored) => Option.fromNullishOr(endpoint.apiKey).
|
|
|
85
105
|
}).pipe(Effect.flatMap(({ cacheKey, policy }) => OpenAiClient.make({ apiKey: key, apiUrl: OPENCODE_RESPONSES_API_URL }).pipe(Effect.flatMap((client) => OpenAiLanguageModel.make({
|
|
86
106
|
model: selection.modelId,
|
|
87
107
|
config: openCodeResponsesConfig(cacheKey, policy)
|
|
88
|
-
}).pipe(Effect.provideService(OpenAiClient.OpenAiClient, client))), Effect.map((svc) => ({ svc, prependClaudeCode: !1 })))));
|
|
108
|
+
}).pipe(Effect.provideService(OpenAiClient.OpenAiClient, client))), Effect.map((svc) => ({ svc, prependClaudeCode: !1, images })))));
|
|
89
109
|
return makeCompatLanguageModel({
|
|
90
110
|
moduleName: "OpenCode",
|
|
91
111
|
chatUrl: OPENCODE_CHAT_URL,
|
|
92
112
|
apiKey: Redacted.value(key),
|
|
93
|
-
model: selection.modelId
|
|
94
|
-
|
|
113
|
+
model: selection.modelId,
|
|
114
|
+
images,
|
|
115
|
+
...compatFetch(transport)
|
|
116
|
+
}).pipe(Effect.map((svc) => ({ svc, prependClaudeCode: !1, images })));
|
|
95
117
|
}
|
|
96
118
|
if (selection.provider === "google")
|
|
97
119
|
return key === void 0 ? Effect.fail(missingKey(selection)) : makeCompatLanguageModel({
|
|
@@ -99,23 +121,26 @@ const endpointKey = (endpoint, stored) => Option.fromNullishOr(endpoint.apiKey).
|
|
|
99
121
|
chatUrl: GOOGLE_CHAT_URL,
|
|
100
122
|
apiKey: Redacted.value(key),
|
|
101
123
|
model: selection.modelId,
|
|
102
|
-
standardOnly: !0
|
|
103
|
-
|
|
124
|
+
standardOnly: !0,
|
|
125
|
+
usage: { reasoning: !1 },
|
|
126
|
+
images,
|
|
127
|
+
...compatFetch(transport)
|
|
128
|
+
}).pipe(Effect.map((svc) => ({ svc, prependClaudeCode: !1, images })));
|
|
104
129
|
if (selection.provider === "anthropic") {
|
|
105
130
|
if (key === void 0)
|
|
106
131
|
return Effect.fail(missingKey(selection));
|
|
107
132
|
return (oauth ? AnthropicClient.make({
|
|
108
133
|
apiKey: void 0,
|
|
109
134
|
transformClient: anthropicOAuthTransform(key)
|
|
110
|
-
}) : 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 })));
|
|
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 })));
|
|
111
136
|
}
|
|
112
137
|
if (selection.provider === "openai-codex") {
|
|
113
138
|
if (key === void 0 || credential?.type !== "oauth")
|
|
114
139
|
return Effect.fail(missingKey(selection));
|
|
115
140
|
const accountId = Option.orElse(Option.fromNullishOr(credential.accountId), () => openAiCodexAccountId(Redacted.value(key)));
|
|
116
141
|
if (Option.isNone(accountId))
|
|
117
|
-
return Effect.fail(new AuthError({ provider: selection.provider, message: "the OpenAI subscription token has no ChatGPT account id \u2014 sign in again
|
|
118
|
-
return makeOpenAiCodexLanguageModel({ model: selection.modelId, accessToken: key, accountId: accountId.value })
|
|
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 })));
|
|
119
144
|
}
|
|
120
145
|
if (selection.provider === "openai")
|
|
121
146
|
return key === void 0 ? Effect.fail(missingKey(selection)) : Effect.flatMap(Effect.service(CurrentPromptCacheKey), (cacheKey) => OpenAiClient.make({ apiKey: key }).pipe(Effect.flatMap((client) => OpenAiLanguageModel.make({
|
|
@@ -123,7 +148,7 @@ const endpointKey = (endpoint, stored) => Option.fromNullishOr(endpoint.apiKey).
|
|
|
123
148
|
config: {
|
|
124
149
|
prompt_cache_key: Option.getOrElse(cacheKey, () => "efferent")
|
|
125
150
|
}
|
|
126
|
-
}).pipe(Effect.provideService(OpenAiClient.OpenAiClient, client))), Effect.map((svc) => ({ svc, prependClaudeCode: !1 }))));
|
|
151
|
+
}).pipe(Effect.provideService(OpenAiClient.OpenAiClient, client))), Effect.map((svc) => ({ svc, prependClaudeCode: !1, images }))));
|
|
127
152
|
return Effect.fail(new AuthError({
|
|
128
153
|
provider: selection.provider,
|
|
129
154
|
message: `provider "${selection.provider}" is not wired (built in: opencode, google, anthropic, openai, openai-codex; add others as models plugin endpoints)`
|
|
@@ -133,6 +158,6 @@ export const buildProvider = (selection, credential, key) => buildProviderServic
|
|
|
133
158
|
const cacheKey = yield* Effect.service(CurrentPromptCacheKey), policy = yield* Effect.service(CurrentModelCallPolicy), responses = selection.provider === "opencode" && usesOpenCodeResponses(selection.modelId), settings = selection.provider === "anthropic" ? { systemPrelude: built.prependClaudeCode ? CLAUDE_CODE_SYSTEM : null } : responses ? openCodeResponsesConfig(cacheKey, policy) : { prompt_cache_key: Option.getOrElse(cacheKey, () => "efferent") }, descriptor = { provider: selection.provider, model: selection.modelId, settings };
|
|
134
159
|
return {
|
|
135
160
|
...built,
|
|
136
|
-
svc: describeModel(built.svc, modelRequestDescriptorOf(built.svc).pipe(Effect.map((existing) => Option.match(existing, { onNone: () => descriptor, onSome: (value) => ({ ...value, provider: selection.provider }) }))))
|
|
161
|
+
svc: describeModel(built.images ? built.svc : withImageLabels(built.svc), modelRequestDescriptorOf(built.svc).pipe(Effect.map((existing) => Option.match(existing, { onNone: () => descriptor, onSome: (value) => ({ ...value, provider: selection.provider }) }))))
|
|
137
162
|
};
|
|
138
163
|
})));
|
package/dist/llm/router.d.ts
CHANGED
|
@@ -43,10 +43,20 @@ export declare const tapStreamTelemetry: (label: string) => <E, R>(stream: Strea
|
|
|
43
43
|
* shaping (anthropic cache breakpoints inherit for free), same stamp, the
|
|
44
44
|
* stream-aware retry, and finish-part telemetry. */
|
|
45
45
|
export declare const streamWith: (selection: ModelSelection, options: unknown) => Stream.Stream<unknown, unknown, AuthStore | HttpClient.HttpClient>;
|
|
46
|
+
/**
|
|
47
|
+
* The fetch the SDK providers' HTTP client uses on a call: the
|
|
48
|
+
* `ModelsTransport`'s (which knows the proxy), when the model was built over
|
|
49
|
+
* one. It is provided innermost, inside the replayed build context, so a
|
|
50
|
+
* `FetchHttpClient.Fetch` in that context or around the call cannot send the
|
|
51
|
+
* call past the proxy. Without a transport the call keeps whatever fetch is
|
|
52
|
+
* in force, the global one by default.
|
|
53
|
+
*/
|
|
54
|
+
export declare const transportFetchOf: Effect.Effect<Option.Option<typeof fetch>, never, never>;
|
|
46
55
|
/**
|
|
47
56
|
* `LanguageModelLive` — the engine loop's `LanguageModel`, routed per call.
|
|
48
57
|
* Requires `SettingsStore` + `AuthStore`; brings its own fetch-backed
|
|
49
|
-
* `HttpClient` so drivers don't have to.
|
|
58
|
+
* `HttpClient` so drivers don't have to. Built over a `ModelsTransport`
|
|
59
|
+
* (as the models plugin builds it), every call goes through that transport.
|
|
50
60
|
*/
|
|
51
61
|
export declare const LanguageModelLive: Layer.Layer<LanguageModel.LanguageModel, never, SettingsStore | AuthStore>;
|
|
52
62
|
/** A profile-pinned routed model. Unlike LanguageModelLive it never consults
|
package/dist/llm/router.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { AiError, LanguageModel } from "effect/ai";
|
|
2
2
|
import { FetchHttpClient, HttpClient } from "effect/http";
|
|
3
3
|
import { Clock, Duration, Effect, Layer, Metric, Option, Ref, Stream } from "effect";
|
|
4
|
-
import { AuthStore, CurrentModelCallPolicy, describeModel, formatModelSelection, HarnessError, parseModelSelection, resolvingModel, SettingsStore } from "@xandreed/core";
|
|
5
|
-
import { buildProvider, prependClaudeCode, withAnthropicCacheBreakpoints } from "./providers.js";
|
|
4
|
+
import { AuthStore, CurrentModelCallPolicy, CurrentTraceContent, describeModel, extractUsage, formatModelSelection, HarnessError, parseModelSelection, replayableContext, resolvingModel, SettingsStore } from "@xandreed/core";
|
|
5
|
+
import { buildProvider, endpointOwnKey, prependClaudeCode, withAnthropicCacheBreakpoints } from "./providers.js";
|
|
6
|
+
import { CurrentModelEndpoints } from "../ports/model-endpoints.port.js";
|
|
7
|
+
import { ModelsTransport } from "../ports/models-transport.port.js";
|
|
8
|
+
import { transportFetch } from "../transport/models-transport.adapter.js";
|
|
6
9
|
import {
|
|
7
10
|
classifyLlmError,
|
|
8
11
|
rejectEmptyResponse,
|
|
@@ -15,6 +18,15 @@ const configError = (message) => AiError.make({ module: "Router", method: "selec
|
|
|
15
18
|
}), llmOutputTokens = Metric.counter("llm.usage.output_tokens", {
|
|
16
19
|
description: "completion tokens produced by routed LLM calls",
|
|
17
20
|
incremental: !0
|
|
21
|
+
}), llmCacheReadTokens = Metric.counter("llm.usage.cache_read_tokens", {
|
|
22
|
+
description: "prompt tokens of routed LLM calls read from the provider's prompt cache",
|
|
23
|
+
incremental: !0
|
|
24
|
+
}), llmCacheWriteTokens = Metric.counter("llm.usage.cache_write_tokens", {
|
|
25
|
+
description: "prompt tokens of routed LLM calls written to the provider's prompt cache",
|
|
26
|
+
incremental: !0
|
|
27
|
+
}), llmReasoningTokens = Metric.counter("llm.usage.reasoning_tokens", {
|
|
28
|
+
description: "completion tokens of routed LLM calls spent on reasoning",
|
|
29
|
+
incremental: !0
|
|
18
30
|
}), llmRequests = Metric.counter("llm.requests", {
|
|
19
31
|
description: "routed LLM calls by final outcome (after retries)",
|
|
20
32
|
incremental: !0
|
|
@@ -23,17 +35,26 @@ const configError = (message) => AiError.make({ module: "Router", method: "selec
|
|
|
23
35
|
}), byModel = (metric, label) => Metric.withAttributes(metric, { "llm.model": label }), countRequest = (label, outcome, isFallback) => Metric.update(Metric.withAttributes(byModel(llmRequests, label), {
|
|
24
36
|
outcome,
|
|
25
37
|
...isFallback === void 0 ? {} : { fallback: isFallback ? "true" : "false" }
|
|
26
|
-
}), 1),
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
38
|
+
}), 1), usageOf = (usage) => extractUsage(usage, []), usageAttributes = (usage) => ({
|
|
39
|
+
"llm.usage.input_tokens": usage.inputTokens,
|
|
40
|
+
"llm.usage.output_tokens": usage.outputTokens,
|
|
41
|
+
"llm.usage.cache_read_tokens": usage.cacheReadTokens,
|
|
42
|
+
"llm.usage.cache_write_tokens": usage.cacheWriteTokens,
|
|
43
|
+
"llm.usage.reasoning_tokens": usage.reasoningTokens
|
|
44
|
+
}), countUsage = (label, usage) => Effect.all([
|
|
45
|
+
Metric.update(byModel(llmInputTokens, label), usage.inputTokens),
|
|
46
|
+
Metric.update(byModel(llmOutputTokens, label), usage.outputTokens),
|
|
47
|
+
Metric.update(byModel(llmCacheReadTokens, label), usage.cacheReadTokens),
|
|
48
|
+
Metric.update(byModel(llmCacheWriteTokens, label), usage.cacheWriteTokens),
|
|
49
|
+
Metric.update(byModel(llmReasoningTokens, label), usage.reasoningTokens)
|
|
50
|
+
], { discard: !0 }), tracedContent = Effect.service(CurrentTraceContent).pipe(Effect.map((capture) => (content) => capture || process.env.EFFERENT_TRACE_CONTENT === "1" ? content.slice(0, 500) : "[redacted]")), shapeOptions = (selection, shouldPrependClaudeCode, options) => {
|
|
30
51
|
const cached = selection.provider === "anthropic" ? withAnthropicCacheBreakpoints(options) : options;
|
|
31
52
|
return shouldPrependClaudeCode ? prependClaudeCode(cached) : cached;
|
|
32
53
|
}, currentSelection = Effect.gen(function* () {
|
|
33
54
|
const loaded = yield* (yield* SettingsStore).load.pipe(Effect.mapError((e) => configError(`settings load failed: ${e.message}`))), raw = Option.getOrElse(loaded.model, () => "");
|
|
34
55
|
return {
|
|
35
56
|
primary: yield* Option.match(parseModelSelection(raw), {
|
|
36
|
-
onNone: () => Effect.fail(configError(raw.length === 0 ?
|
|
57
|
+
onNone: () => Effect.fail(configError(raw.length === 0 ? `no model configured \u2014 set the models plugin's model option, or a SettingsStore model, to "<provider>:<modelId>"` : `the configured model "${raw}" is not a "<provider>:<modelId>" selection`)),
|
|
37
58
|
onSome: Effect.succeed
|
|
38
59
|
}),
|
|
39
60
|
fallback: Option.flatMap(loaded.fallbackModel, parseModelSelection),
|
|
@@ -46,24 +67,29 @@ const configError = (message) => AiError.make({ module: "Router", method: "selec
|
|
|
46
67
|
fallback: Option.getOrElse(Option.map(fallback, formatModelSelection), () => null),
|
|
47
68
|
policy: Option.getOrElse(Option.orElse(current, () => Option.map(effort, (value) => ({ effort: value }))), () => null)
|
|
48
69
|
}
|
|
49
|
-
})))
|
|
70
|
+
}))), credentialFor = (selection) => Effect.gen(function* () {
|
|
71
|
+
const endpoints = yield* Effect.service(CurrentModelEndpoints), own = Option.flatMap(Option.fromNullishOr(endpoints[selection.provider]), endpointOwnKey);
|
|
72
|
+
if (Option.isSome(own))
|
|
73
|
+
return { credential: void 0, key: own.value };
|
|
74
|
+
const auth = yield* AuthStore;
|
|
75
|
+
return {
|
|
76
|
+
credential: Option.getOrUndefined(yield* auth.get(selection.provider)),
|
|
77
|
+
key: Option.getOrUndefined(yield* auth.resolveKey(selection.provider))
|
|
78
|
+
};
|
|
79
|
+
});
|
|
50
80
|
export const generateWith = (selection, options, isFallback = !1) => Effect.gen(function* () {
|
|
51
|
-
const
|
|
81
|
+
const label = formatModelSelection(selection), traced = yield* tracedContent, { credential, key } = yield* credentialFor(selection), built = yield* buildProvider(selection, credential, key);
|
|
52
82
|
return yield* built.svc.generateText(shapeOptions(selection, built.prependClaudeCode, options)).pipe(rejectEmptyResponse(label), retryableLlm(label), Effect.map((res) => stampResponse(res, label)), Effect.tap((res) => Effect.annotateCurrentSpan({
|
|
53
83
|
"llm.finish_reason": String(res.finishReason),
|
|
54
|
-
|
|
55
|
-
"llm.usage.output_tokens": usageTotals(res.usage).output,
|
|
84
|
+
...usageAttributes(usageOf(res.usage)),
|
|
56
85
|
"llm.response_chars": res.text.length,
|
|
57
86
|
"llm.tool_calls": res.content.flatMap((part) => {
|
|
58
87
|
const p = part;
|
|
59
88
|
return p.type === "tool-call" ? [p.name ?? ""] : [];
|
|
60
89
|
}).join(","),
|
|
61
|
-
"llm.reasoning":
|
|
90
|
+
"llm.reasoning": traced(res.reasoningText ?? ""),
|
|
62
91
|
"llm.fallback": isFallback
|
|
63
|
-
})), Effect.tap((res) => Effect.
|
|
64
|
-
Metric.update(byModel(llmInputTokens, label), usageTotals(res.usage).input),
|
|
65
|
-
Metric.update(byModel(llmOutputTokens, label), usageTotals(res.usage).output)
|
|
66
|
-
], { discard: !0 })), 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), Effect.withSpan("providers.generate", {
|
|
92
|
+
})), 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), Effect.withSpan("providers.generate", {
|
|
67
93
|
attributes: { "llm.model": label }
|
|
68
94
|
}));
|
|
69
95
|
}).pipe(Effect.scoped), withFallbackRung = (primary, fallback, call) => call(primary, !1).pipe(Effect.catch((error) => Option.match(Option.filter(fallback, (fb) => formatModelSelection(fb) !== formatModelSelection(primary) && classifyLlmError(error) === "transient"), {
|
|
@@ -95,40 +121,38 @@ export const tapStreamTelemetry = (label) => (stream) => Stream.unwrap(Effect.ge
|
|
|
95
121
|
textChars: 0,
|
|
96
122
|
reasoning: "",
|
|
97
123
|
toolNames: []
|
|
98
|
-
}), startedAt = yield* Clock.currentTimeMillis, recordDuration = Clock.currentTimeMillis.pipe(Effect.flatMap((now) => Metric.update(byModel(llmDuration, label), Duration.millis(now - startedAt))));
|
|
124
|
+
}), startedAt = yield* Clock.currentTimeMillis, traced = yield* tracedContent, recordDuration = Clock.currentTimeMillis.pipe(Effect.flatMap((now) => Metric.update(byModel(llmDuration, label), Duration.millis(now - startedAt))));
|
|
99
125
|
return stream.pipe(Stream.mapEffect((part) => {
|
|
100
126
|
const p = part;
|
|
101
127
|
if (p.type !== "finish")
|
|
102
128
|
return Ref.update(stats, (s) => accumulateStats(s, part)).pipe(Effect.as(part));
|
|
103
129
|
return Ref.get(stats).pipe(Effect.flatMap((s) => Effect.annotateCurrentSpan({
|
|
104
130
|
"llm.finish_reason": String(p.reason),
|
|
105
|
-
|
|
106
|
-
"llm.usage.output_tokens": usageTotals(p.usage).output,
|
|
131
|
+
...usageAttributes(usageOf(p.usage)),
|
|
107
132
|
"llm.response_chars": s.textChars,
|
|
108
133
|
"llm.tool_calls": s.toolNames.join(","),
|
|
109
|
-
"llm.reasoning":
|
|
110
|
-
})), Effect.andThen(Effect.
|
|
111
|
-
Metric.update(byModel(llmInputTokens, label), usageTotals(p.usage).input),
|
|
112
|
-
Metric.update(byModel(llmOutputTokens, label), usageTotals(p.usage).output)
|
|
113
|
-
], { discard: !0 })), Effect.as(part));
|
|
134
|
+
"llm.reasoning": traced(s.reasoning)
|
|
135
|
+
})), Effect.andThen(countUsage(label, usageOf(p.usage))), Effect.as(part));
|
|
114
136
|
}), Stream.onEnd(countRequest(label, "ok").pipe(Effect.andThen(recordDuration))), Stream.tapCause(() => countRequest(label, "error").pipe(Effect.andThen(recordDuration))));
|
|
115
137
|
})), streamWith = (selection, options) => Stream.unwrap(Effect.gen(function* () {
|
|
116
|
-
const
|
|
138
|
+
const label = formatModelSelection(selection), { credential, key } = yield* credentialFor(selection), built = yield* buildProvider(selection, credential, key);
|
|
117
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", {
|
|
118
140
|
attributes: { "llm.model": label }
|
|
119
141
|
}));
|
|
120
142
|
}));
|
|
121
|
-
const 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 }))))))
|
|
143
|
+
const 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 }))))));
|
|
144
|
+
export const transportFetchOf = Effect.map(Effect.serviceOption(ModelsTransport), Option.map(transportFetch));
|
|
145
|
+
const withFetch = (fetch) => (effect) => Option.match(fetch, { onNone: () => effect, onSome: (value) => Effect.provideService(effect, FetchHttpClient.Fetch, value) }), withFetchStream = (fetch) => (stream) => Option.match(fetch, { onNone: () => stream, onSome: (value) => Stream.provideService(stream, FetchHttpClient.Fetch, value) }), selectionModel = (primary, fallback, effort, context, http, fetch) => {
|
|
122
146
|
const service = {
|
|
123
147
|
[LanguageModel.TypeId]: LanguageModel.TypeId,
|
|
124
|
-
generateText: (options) => withConfiguredEffort(withFallbackRung(primary, fallback, (selection, isFallback) => generateWith(selection, options, isFallback)), effort).pipe(Effect.provide(context), Effect.provideService(HttpClient.HttpClient, http)),
|
|
148
|
+
generateText: (options) => withConfiguredEffort(withFallbackRung(primary, fallback, (selection, isFallback) => generateWith(selection, options, isFallback)), effort).pipe(withFetch(fetch), Effect.provide(context), Effect.provideService(HttpClient.HttpClient, http)),
|
|
125
149
|
generateObject: () => Effect.fail(configError("generateObject is not wired on the new line yet")),
|
|
126
|
-
streamText: (options) => withConfiguredEffortStream(streamWith(primary, options), effort).pipe(Stream.provideContext(context), Stream.provideService(HttpClient.HttpClient, http))
|
|
150
|
+
streamText: (options) => withConfiguredEffortStream(streamWith(primary, options), effort).pipe(withFetchStream(fetch), Stream.provideContext(context), Stream.provideService(HttpClient.HttpClient, http))
|
|
127
151
|
};
|
|
128
152
|
return describeModel(service, describeSelection(primary, fallback, effort));
|
|
129
153
|
};
|
|
130
154
|
export const LanguageModelLive = Layer.effect(LanguageModel.LanguageModel, Effect.gen(function* () {
|
|
131
|
-
const context = yield* Effect.context(), http = yield* HttpClient.HttpClient, selected = currentSelection.pipe(Effect.map(({ primary, fallback, effort }) => selectionModel(primary, fallback, effort, context, http)), Effect.provide(context)), service = {
|
|
155
|
+
const context = replayableContext(yield* Effect.context()), http = yield* HttpClient.HttpClient, fetch = yield* transportFetchOf, selected = currentSelection.pipe(Effect.map(({ primary, fallback, effort }) => selectionModel(primary, fallback, effort, context, http, fetch)), Effect.provide(context)), service = {
|
|
132
156
|
[LanguageModel.TypeId]: LanguageModel.TypeId,
|
|
133
157
|
generateText: (options) => selected.pipe(Effect.flatMap((model) => model.generateText(options))),
|
|
134
158
|
generateObject: () => Effect.fail(configError("generateObject is not wired on the new line yet")),
|
|
@@ -136,6 +160,6 @@ export const LanguageModelLive = Layer.effect(LanguageModel.LanguageModel, Effec
|
|
|
136
160
|
}, asHarness = (error) => new HarnessError({ code: "request.model", message: String(error) });
|
|
137
161
|
return resolvingModel(describeModel(service, currentSelection.pipe(Effect.flatMap(({ primary, fallback, effort }) => describeSelection(primary, fallback, effort)), Effect.provide(context), Effect.mapError(asHarness))), selected.pipe(Effect.mapError(asHarness)));
|
|
138
162
|
})).pipe(Layer.provide(FetchHttpClient.layer)), LanguageModelSelectionLive = (primary, fallback) => Layer.effect(LanguageModel.LanguageModel, Effect.gen(function* () {
|
|
139
|
-
const context = yield* Effect.context(), http = yield* HttpClient.HttpClient;
|
|
140
|
-
return selectionModel(primary, fallback, Option.none(), context, http);
|
|
163
|
+
const context = replayableContext(yield* Effect.context()), http = yield* HttpClient.HttpClient;
|
|
164
|
+
return selectionModel(primary, fallback, Option.none(), context, http, yield* transportFetchOf);
|
|
141
165
|
})).pipe(Layer.provide(FetchHttpClient.layer));
|
package/dist/llm/sse.d.ts
CHANGED
|
@@ -24,22 +24,42 @@ export interface CompletionUsage {
|
|
|
24
24
|
readonly prompt_cache_hit_tokens?: number;
|
|
25
25
|
/** Moonshot-style cache accounting. */
|
|
26
26
|
readonly cached_tokens?: number;
|
|
27
|
-
/** OpenAI-native cache
|
|
27
|
+
/** OpenAI-native cache reads; gateways (OpenCode's among them) add the cache writes. */
|
|
28
28
|
readonly prompt_tokens_details?: {
|
|
29
29
|
readonly cached_tokens?: number;
|
|
30
|
+
readonly cache_write_tokens?: number;
|
|
30
31
|
};
|
|
32
|
+
/** OpenAI-native reasoning accounting, counted inside `completion_tokens`. */
|
|
33
|
+
readonly completion_tokens_details?: {
|
|
34
|
+
readonly reasoning_tokens?: number;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** How a compat endpoint's usage is read. */
|
|
38
|
+
export interface UsageReading {
|
|
39
|
+
/**
|
|
40
|
+
* Read `completion_tokens_details.reasoning_tokens` (default true). The
|
|
41
|
+
* built-in `google` provider sets false: Gemini's `completion_tokens` may
|
|
42
|
+
* leave its thoughts out, which would break reasoning inside the output,
|
|
43
|
+
* so its reasoning reads 0. Configured endpoints and `local` credentials
|
|
44
|
+
* read it.
|
|
45
|
+
*/
|
|
46
|
+
readonly reasoning?: boolean;
|
|
31
47
|
}
|
|
32
48
|
/** The vendor cached-token fallback chain, shared with the non-streaming
|
|
33
49
|
* parser — the gateway fronts upstreams with three cache vocabularies.
|
|
34
|
-
* `prompt_tokens` already counts the cached tokens: it is the
|
|
35
|
-
|
|
50
|
+
* `prompt_tokens` already counts the cached and written tokens: it is the
|
|
51
|
+
* full input, and `completion_tokens` the full output, reasoning included. */
|
|
52
|
+
export declare const usageFromCompletion: (usage: CompletionUsage | undefined, reading?: UsageReading) => {
|
|
36
53
|
inputTokens: {
|
|
37
54
|
total: number;
|
|
38
55
|
uncached: number;
|
|
39
56
|
cacheRead: number;
|
|
57
|
+
cacheWrite: number;
|
|
40
58
|
};
|
|
41
59
|
outputTokens: {
|
|
42
60
|
total: number;
|
|
61
|
+
text: number;
|
|
62
|
+
reasoning: number;
|
|
43
63
|
};
|
|
44
64
|
};
|
|
45
65
|
export declare const finishReasonFromWire: (raw: string | undefined) => "stop" | "length" | "content-filter" | "tool-calls";
|
|
@@ -47,4 +67,6 @@ export declare const finishReasonFromWire: (raw: string | undefined) => "stop" |
|
|
|
47
67
|
export declare const sseStreamParts: (options: {
|
|
48
68
|
readonly moduleName: string;
|
|
49
69
|
readonly body: ReadableStream<Uint8Array>;
|
|
70
|
+
/** How the final chunk's usage is read (see `usageFromCompletion`). */
|
|
71
|
+
readonly usage?: UsageReading;
|
|
50
72
|
}) => Stream.Stream<Response.StreamPartEncoded, AiError.AiError>;
|
package/dist/llm/sse.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { AiError } from "effect/ai";
|
|
2
2
|
import { Effect, Option, Result, Stream } from "effect";
|
|
3
|
-
export const usageFromCompletion = (usage) => {
|
|
4
|
-
const input = usage?.prompt_tokens ?? 0, cached = usage?.prompt_cache_hit_tokens ?? usage?.cached_tokens ?? usage?.prompt_tokens_details?.cached_tokens ?? 0;
|
|
3
|
+
export const usageFromCompletion = (usage, reading = {}) => {
|
|
4
|
+
const input = usage?.prompt_tokens ?? 0, cached = usage?.prompt_cache_hit_tokens ?? usage?.cached_tokens ?? usage?.prompt_tokens_details?.cached_tokens ?? 0, written = usage?.prompt_tokens_details?.cache_write_tokens ?? 0, output = usage?.completion_tokens ?? 0, reasoning = reading.reasoning === !1 ? 0 : usage?.completion_tokens_details?.reasoning_tokens ?? 0;
|
|
5
5
|
return {
|
|
6
|
-
inputTokens: { total: input, uncached: Math.max(0, input - cached), cacheRead: cached },
|
|
7
|
-
outputTokens: { total:
|
|
6
|
+
inputTokens: { total: input, uncached: Math.max(0, input - cached - written), cacheRead: cached, cacheWrite: written },
|
|
7
|
+
outputTokens: { total: output, text: Math.max(0, output - reasoning), reasoning }
|
|
8
8
|
};
|
|
9
9
|
}, finishReasonFromWire = (raw) => {
|
|
10
10
|
if (raw === "tool_calls")
|
|
@@ -83,7 +83,7 @@ const initialState = {
|
|
|
83
83
|
usage: chunk.usage !== null && chunk.usage !== void 0 ? Option.some(chunk.usage) : folded.usage
|
|
84
84
|
};
|
|
85
85
|
return Effect.succeed([next, parts]);
|
|
86
|
-
}, flush = (moduleName, state) => state.flushed ? Effect.succeed([state, []]) : Effect.gen(function* () {
|
|
86
|
+
}, flush = (moduleName, reading, state) => state.flushed ? Effect.succeed([state, []]) : Effect.gen(function* () {
|
|
87
87
|
const [closed, closeParts] = emitThen(closeText(state), closeReasoning), toolParts = yield* Effect.forEach(closed.toolCalls, (call, index) => Effect.try({
|
|
88
88
|
try: () => ({
|
|
89
89
|
type: "tool-call",
|
|
@@ -95,13 +95,13 @@ const initialState = {
|
|
|
95
95
|
})), finish = {
|
|
96
96
|
type: "finish",
|
|
97
97
|
reason: toolParts.length > 0 ? "tool-calls" : finishReasonFromWire(Option.getOrUndefined(closed.finishReason)),
|
|
98
|
-
usage: usageFromCompletion(Option.getOrUndefined(closed.usage))
|
|
98
|
+
usage: usageFromCompletion(Option.getOrUndefined(closed.usage), reading)
|
|
99
99
|
};
|
|
100
100
|
return [
|
|
101
101
|
{ ...closed, flushed: !0 },
|
|
102
102
|
[...closeParts, ...toolParts, finish]
|
|
103
103
|
];
|
|
104
|
-
}), DONE = "[DONE]", step = (moduleName, state, payload) => payload === DONE ? flush(moduleName, state) : Effect.try({
|
|
104
|
+
}), DONE = "[DONE]", step = (moduleName, reading, state, payload) => payload === DONE ? flush(moduleName, reading, state) : Effect.try({
|
|
105
105
|
try: () => JSON.parse(payload),
|
|
106
106
|
catch: () => malformed(moduleName, `a data: line was not JSON: ${payload.slice(0, 200)}`)
|
|
107
107
|
}).pipe(Effect.flatMap((chunk) => applyChunk(moduleName, state, chunk)));
|
|
@@ -115,4 +115,4 @@ export const sseStreamParts = (options) => Stream.fromReadableStream({
|
|
|
115
115
|
}).pipe(Stream.decodeText(), Stream.splitLines, Stream.filterMap((line) => {
|
|
116
116
|
const trimmed = line.trim();
|
|
117
117
|
return trimmed.startsWith("data:") ? Result.succeed(trimmed.slice(5).trim()) : Result.fail(line);
|
|
118
|
-
}), Stream.concat(Stream.succeed(DONE)), Stream.mapAccumEffect(() => initialState, (state, payload) => step(options.moduleName, state, payload)));
|
|
118
|
+
}), Stream.concat(Stream.succeed(DONE)), Stream.mapAccumEffect(() => initialState, (state, payload) => step(options.moduleName, options.usage ?? {}, state, payload)));
|
package/dist/llm/utilityLlm.d.ts
CHANGED
|
@@ -3,6 +3,6 @@ import { AuthStore, SettingsStore, UtilityLlm } from "@xandreed/core";
|
|
|
3
3
|
/**
|
|
4
4
|
* The fast helper tier: one-shot completions on `fastModel ?? model`. No
|
|
5
5
|
* toolkit, no cache breakpoints, no loop — a title/digest call that can never
|
|
6
|
-
* park a turn.
|
|
6
|
+
* park a turn. Built over a `ModelsTransport`, its calls go through it.
|
|
7
7
|
*/
|
|
8
8
|
export declare const UtilityLlmLive: Layer.Layer<UtilityLlm, never, SettingsStore | AuthStore>;
|
package/dist/llm/utilityLlm.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Prompt } from "effect/ai";
|
|
2
2
|
import { FetchHttpClient, HttpClient } from "effect/http";
|
|
3
3
|
import { Effect, Layer, Option } from "effect";
|
|
4
|
-
import { AuthStore, extractUsage, parseModelSelection, SettingsStore, UtilityError, UtilityLlm } from "@xandreed/core";
|
|
5
|
-
import { generateWith } from "./router.js";
|
|
4
|
+
import { AuthStore, extractUsage, parseModelSelection, replayableContext, SettingsStore, UtilityError, UtilityLlm } from "@xandreed/core";
|
|
5
|
+
import { generateWith, transportFetchOf } from "./router.js";
|
|
6
6
|
export const UtilityLlmLive = Layer.effect(UtilityLlm, Effect.gen(function* () {
|
|
7
|
-
const context = yield* Effect.context(), http = yield* HttpClient.HttpClient, settings = yield* SettingsStore;
|
|
7
|
+
const context = replayableContext(yield* Effect.context()), http = yield* HttpClient.HttpClient, fetch = yield* transportFetchOf, settings = yield* SettingsStore;
|
|
8
8
|
return {
|
|
9
9
|
complete: (prompt) => Effect.gen(function* () {
|
|
10
10
|
const loaded = yield* settings.load.pipe(Effect.mapError((e) => new UtilityError({ message: e.message }))), raw = Option.getOrElse(Option.orElse(loaded.fastModel, () => loaded.model), () => ""), selection = yield* Option.match(parseModelSelection(raw), {
|
|
@@ -15,7 +15,7 @@ export const UtilityLlmLive = Layer.effect(UtilityLlm, Effect.gen(function* () {
|
|
|
15
15
|
tools: [],
|
|
16
16
|
toolChoice: "none",
|
|
17
17
|
responseFormat: { type: "text" }
|
|
18
|
-
}).pipe(Effect.mapError((e) => new UtilityError({ message: String(e) })), Effect.provide(context), Effect.provideService(HttpClient.HttpClient, http));
|
|
18
|
+
}).pipe(Effect.mapError((e) => new UtilityError({ message: String(e) })), (call) => Option.match(fetch, { onNone: () => call, onSome: (value) => Effect.provideService(call, FetchHttpClient.Fetch, value) }), Effect.provide(context), Effect.provideService(HttpClient.HttpClient, http));
|
|
19
19
|
return { text: res.content.flatMap((p) => {
|
|
20
20
|
const part = p;
|
|
21
21
|
return part.type === "text" ? [part.text ?? ""] : [];
|
package/dist/plugin.adapter.d.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { LanguageModel } from "effect/ai";
|
|
2
|
-
import {
|
|
2
|
+
import { Config as EffectConfig, Redacted } from "effect";
|
|
3
|
+
import { AuthStore, HarnessError, ModelCatalog, SessionEnvironment, SettingsStore, UtilityLlm } from "@xandreed/core";
|
|
4
|
+
/**
|
|
5
|
+
* The models plugin: the routed `LanguageModel`, the fast `UtilityLlm`, the
|
|
6
|
+
* `ModelCatalog`, and the `AuthStore` and `SettingsStore` they read.
|
|
7
|
+
* Runtime-scoped: built once per `Agent.define`, so credentials, refreshed
|
|
8
|
+
* tokens and settings edits held in memory last across turns. A turn's own
|
|
9
|
+
* services win over it (a per-turn `LanguageModel` replaces the routed one).
|
|
10
|
+
* When a proxy applies (the `proxy` option, else the environment's
|
|
11
|
+
* `https_proxy` / `HTTPS_PROXY` and `http_proxy` / `HTTP_PROXY`), every call
|
|
12
|
+
* it makes (the providers, the OpenAI-compatible endpoints, OAuth token
|
|
13
|
+
* refreshes, the OpenAI subscription's WebSocket) goes through one
|
|
14
|
+
* `ModelsTransport`, the `noProxy` / `NO_PROXY` hosts and loopback direct;
|
|
15
|
+
* so do they through a host's own transport.
|
|
16
|
+
*/
|
|
3
17
|
export declare const modelsPlugin: import("@xandreed/core").TypedPlugin<{
|
|
4
18
|
readonly model: string;
|
|
5
19
|
readonly fastModel: string;
|
|
@@ -8,11 +22,34 @@ export declare const modelsPlugin: import("@xandreed/core").TypedPlugin<{
|
|
|
8
22
|
readonly endpoints: {
|
|
9
23
|
readonly [x: string]: {
|
|
10
24
|
readonly url: string;
|
|
11
|
-
readonly
|
|
25
|
+
readonly images?: boolean;
|
|
26
|
+
readonly apiKey?: string | Redacted.Redacted<string>;
|
|
12
27
|
readonly apiKeyEnv?: string;
|
|
13
28
|
readonly standardOnly?: boolean;
|
|
14
29
|
};
|
|
15
30
|
};
|
|
31
|
+
readonly storage: "local" | "memory";
|
|
32
|
+
readonly credentials: {
|
|
33
|
+
readonly [x: string]: {
|
|
34
|
+
readonly type: "api_key";
|
|
35
|
+
readonly key: Redacted.Redacted<string>;
|
|
36
|
+
} | {
|
|
37
|
+
readonly type: "oauth";
|
|
38
|
+
readonly access: Redacted.Redacted<string>;
|
|
39
|
+
readonly refresh: Redacted.Redacted<string>;
|
|
40
|
+
readonly expires: number;
|
|
41
|
+
readonly accountId?: string;
|
|
42
|
+
readonly installationId?: string;
|
|
43
|
+
} | {
|
|
44
|
+
readonly type: "local";
|
|
45
|
+
readonly baseUrl?: string;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
readonly credentialEnv: {
|
|
49
|
+
readonly [x: string]: string;
|
|
50
|
+
};
|
|
51
|
+
readonly proxy: string | Redacted.Redacted<string>;
|
|
52
|
+
readonly noProxy: string;
|
|
16
53
|
}, {
|
|
17
54
|
readonly model: string;
|
|
18
55
|
readonly fastModel: string;
|
|
@@ -21,10 +58,33 @@ export declare const modelsPlugin: import("@xandreed/core").TypedPlugin<{
|
|
|
21
58
|
readonly endpoints: {
|
|
22
59
|
readonly [x: string]: {
|
|
23
60
|
readonly url: string;
|
|
24
|
-
readonly
|
|
61
|
+
readonly images?: boolean;
|
|
62
|
+
readonly apiKey?: string | Redacted.Redacted<string>;
|
|
25
63
|
readonly apiKeyEnv?: string;
|
|
26
64
|
readonly standardOnly?: boolean;
|
|
27
65
|
};
|
|
28
66
|
};
|
|
29
|
-
|
|
67
|
+
readonly storage: "local" | "memory";
|
|
68
|
+
readonly credentials: {
|
|
69
|
+
readonly [x: string]: {
|
|
70
|
+
readonly type: "api_key";
|
|
71
|
+
readonly key: Redacted.Redacted<string>;
|
|
72
|
+
} | {
|
|
73
|
+
readonly type: "oauth";
|
|
74
|
+
readonly access: Redacted.Redacted<string>;
|
|
75
|
+
readonly refresh: Redacted.Redacted<string>;
|
|
76
|
+
readonly expires: number;
|
|
77
|
+
readonly accountId?: string;
|
|
78
|
+
readonly installationId?: string;
|
|
79
|
+
} | {
|
|
80
|
+
readonly type: "local";
|
|
81
|
+
readonly baseUrl?: string;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
readonly credentialEnv: {
|
|
85
|
+
readonly [x: string]: string;
|
|
86
|
+
};
|
|
87
|
+
readonly proxy: string | Redacted.Redacted<string>;
|
|
88
|
+
readonly noProxy: string;
|
|
89
|
+
}, SettingsStore | AuthStore | ModelCatalog | UtilityLlm | LanguageModel.LanguageModel, HarnessError | import("@xandreed/core").AuthError | EffectConfig.ConfigError, SessionEnvironment>;
|
|
30
90
|
export default modelsPlugin;
|